./0000755000004100000410000000000013004613605011241 5ustar www-datawww-data./CMakeLists.txt0000644000004100000410000000523413004613604014004 0ustar www-datawww-datacmake_minimum_required(VERSION 2.8.9) set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") project("webbrowser") find_program(INTLTOOL_MERGE intltool-merge) if(NOT INTLTOOL_MERGE) message(FATAL_ERROR "Could not find intltool-merge, please install the intltool package") endif() find_program(INTLTOOL_EXTRACT intltool-extract) if(NOT INTLTOOL_EXTRACT) message(FATAL_ERROR "Could not find intltool-extract, please install the intltool package") endif() # Standard install paths include(GNUInstallDirs) string(TOLOWER "${CMAKE_BUILD_TYPE}" cmake_build_type_lower) include(EnableCoverageReport) ##################################################################### # Enable code coverage calculation with gcov/gcovr/lcov # Usage: # * Switch build type to coverage (use ccmake or cmake-gui) # * Invoke make, make test, make coverage (or ninja if you use that backend) # * Find html report in subdir coveragereport # * Find xml report feasible for jenkins in coverage.xml ##################################################################### if(cmake_build_type_lower MATCHES coverage) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage" ) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --coverage" ) set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} --coverage" ) set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --coverage" ) ENABLE_COVERAGE_REPORT(EXCLUDES tests/*|.*moc_.*.cpp FILTER tests/* moc_*.cpp) endif() # for unity8 components set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") # enable QML debugging if(CMAKE_BUILD_TYPE MATCHES DEBUG OR CMAKE_BUILD_TYPE MATCHES "Debug") add_definitions(-DQT_QML_DEBUG) endif() include(qt5) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) set(DESKTOP_FILE webbrowser-app.desktop) # uninstall target configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" IMMEDIATE @ONLY) add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) add_subdirectory(src) enable_testing() add_subdirectory(tests) # make non compiled files (QML, JS, images, etc.) visible in QtCreator file(GLOB_RECURSE NON_COMPILED_FILES *.qml *.js *.png *.sci *.py *.pot *.po *.qdoc *.qdocconf *.css) add_custom_target(NON_COMPILED_TARGET ALL SOURCES ${NON_COMPILED_FILES}) # for dh_translations to extract the domain # (regarding syntax consistency, see http://pad.lv/1181187) set (GETTEXT_PACKAGE "webbrowser-app") add_subdirectory(po) add_subdirectory(doc) install(FILES webbrowser-app.png screenshot.png DESTINATION ${CMAKE_INSTALL_DATADIR}/webbrowser-app) add_subdirectory(click-hooks) ./README0000644000004100000410000001020613004613604012117 0ustar www-datawww-datawebbrowser-app is a lightweight web browser tailored for Ubuntu, based on the Oxide web engine and using the Ubuntu UI components. It requires Qt 5.4 to build and run. = Building = The build system uses cmake. To compile, simply invoke cmake and then make: $ cmake . $ make The application can also be cross compiled for an ARM target on a X86 host. To do that, just pass this additional parameter to cmake: $ cmake -DCMAKE_TOOLCHAIN_FILE=cmake/ubuntu-arm-linux-gnueabihf.cmake . = Running = webbrowser-app can be run from the development branch without the need to install any files. Just run: $ ./src/app/webbrowser/webbrowser-app The executable accepts command line switches and parameters. To find out which, just run: $ ./src/app/webbrowser/webbrowser-app --help = Unit tests = To run the unit tests, you can use the commands below: $ make test - or - $ ctest = Automated UI tests = webbrowser-app uses autopilot (https://launchpad.net/autopilot) to test its UI. To run the tests locally, you will need to install python3-autopilot and autopilot-qt5. Then do the following: $ cd tests/autopilot/ $ autopilot3 run webbrowser_app You can get a list of all available tests with the following command: $ autopilot3 list webbrowser_app In order to run the tests in a virtual machine with an environment closer to what a user will get in Ubuntu Touch, see the Dep8 tests section. = Code coverage = To generate a report with detailed code coverage, you need to re-run cmake with "CMAKE_BUILD_TYPE=coverage": $ cmake -DCMAKE_BUILD_TYPE=coverage . $ make $ make test $ make coverage This will generate a coverage report in XML format (coverage.xml) and an interactive human-readable report in HTML format (coveragereport/index.html). = Dep8 tests = Dep8 tests exercise the package "as-installed". Currently, the webbrowser-app has one suite of dep8 tests that uses autopilot (https://launchpad.net/autopilot) to test from the point of view of the user. To run the tests you will need autopkgtest: $ sudo apt-get install autopkgtest You can use multiple test beds to execute the tests. Below you will find instructions to run them in a virtual machine You can find more information with: $ man adt-run == Run dep8 tests == To run the tests in a qemu virtual machine, you will first have to create it (see /usr/share/doc/autopkgtest/README.running-tests.rst.gz). We output the image to ~/ rather than the current directory, so it will be in a safer place to avoid rebuilding images every time. You can store it in any directory you wish. This image is better consumed "fresh", building it daily will avoid long updates/upgrades when running the tests. $ adt-buildvm-ubuntu-cloud -r $(lsb_release -c -s) -a amd64 -o ~/ Then run the tests using adt-run with the qemu virtualization host against the current archive. $ adt-run -B -U --unbuilt-tree . \ -o ~/adt-browser-test/$(date +%Y-%m-%d-%H-%M) \ --- qemu ~/adt-$(lsb_release -c -s)-amd64-cloud.img The tests can also be run on a local phone. $ adt-run -B -U --unbuilt-tree . \ -o ~/adt-browser-test/$(date +%Y-%m-%d-%H-%M) \ --- ssh -s adb -- -p --serial == Examine the dep8 autopilot results == To examine the test results, which are in subunit format, additional tools are required, such as trv (https://launchpad.net/trv). You can find the results file in the directory ~/adt-browser-test/$(date +%Y-%m-%d-%H-%M)/artifacts. = Settings = webbrowser-app supports a limited set of custom settings, persisted on disk in the following INI-like file: $HOME/.config/webbrowser-app/webbrowser-app.conf The following keys are supported: - 'homepage': a URL that the browser will open when launched if no URL is specified on the command line - 'searchengine': a custom search engine specification, looked up in $HOME/.local/share/webbrowser-app/searchengines/{value}.xml and following the OpenSearch document description format (http://www.opensearch.org/Specifications/OpenSearch/1.1) - restoreSession: whether to restore the previous browsing session at startup (defaults to true) ./cmake_uninstall.cmake.in0000644000004100000410000000211713004613604016021 0ustar www-datawww-data# Source: http://www.cmake.org/Wiki/CMake_FAQ#Can_I_do_.22make_uninstall.22_with_CMake.3F if (NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") message(FATAL_ERROR "Cannot find install manifest: \"@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt\"") endif(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") file(READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files) string(REGEX REPLACE "\n" ";" files "${files}") list(REVERSE files) foreach (file ${files}) message(STATUS "Uninstalling \"$ENV{DESTDIR}${file}\"") if (EXISTS "$ENV{DESTDIR}${file}") execute_process( COMMAND @CMAKE_COMMAND@ -E remove "$ENV{DESTDIR}${file}" OUTPUT_VARIABLE rm_out RESULT_VARIABLE rm_retval ) if(NOT ${rm_retval} EQUAL 0) message(FATAL_ERROR "Problem when removing \"$ENV{DESTDIR}${file}\"") endif (NOT ${rm_retval} EQUAL 0) else (EXISTS "$ENV{DESTDIR}${file}") message(STATUS "File \"$ENV{DESTDIR}${file}\" does not exist.") endif (EXISTS "$ENV{DESTDIR}${file}") endforeach(file) ./COPYING0000644000004100000410000010451313004613604012277 0ustar www-datawww-data GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ./src/0000755000004100000410000000000013004613605012030 5ustar www-datawww-data./src/CMakeLists.txt0000644000004100000410000000005713004613604014571 0ustar www-datawww-dataadd_subdirectory(Ubuntu) add_subdirectory(app) ./src/Ubuntu/0000755000004100000410000000000013004613623013312 5ustar www-datawww-data./src/Ubuntu/CMakeLists.txt0000644000004100000410000000200413004613623016046 0ustar www-datawww-dataif(NOT CMAKE_CROSSCOMPILING) find_program(QMAKE_EXECUTABLE qmake) if(QMAKE_EXECUTABLE STREQUAL "QMAKE_EXECUTABLE-NOTFOUND") message(FATAL_ERROR "qmake not found") endif() execute_process( COMMAND ${QMAKE_EXECUTABLE} -query QT_INSTALL_QML RESULT_VARIABLE RESULT OUTPUT_VARIABLE QT_INSTALL_QML OUTPUT_STRIP_TRAILING_WHITESPACE ) if(NOT RESULT EQUAL 0) message(FATAL_ERROR "Failed to determine QT_INSTALL_QML from qmake") endif() else() # qmake isn't multi-arch aware as it installs arch-specific mkspec files # in to /usr/share, so we can't use it here (we'd need a qmake binary # for the host arch using data for the target arch) set(QT_INSTALL_QML "/usr/lib/${CMAKE_LIBRARY_ARCHITECTURE}/qt5/qml") endif() execute_process(COMMAND lsb_release --short --release OUTPUT_VARIABLE UBUNTU_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE) add_definitions(-DUBUNTU_VERSION="${UBUNTU_VERSION}") add_subdirectory(Components) add_subdirectory(Web) ./src/Ubuntu/Web/0000755000004100000410000000000013004613623014027 5ustar www-datawww-data./src/Ubuntu/Web/CMakeLists.txt0000644000004100000410000000224513004613604016571 0ustar www-datawww-dataproject(ubuntu-web-plugin) find_package(Qt5Core REQUIRED) find_package(Qt5Gui REQUIRED) find_package(Qt5Qml REQUIRED) set(UBUNTU_WEB_IMPORTS_DIR "${QT_INSTALL_QML}/Ubuntu/Web") set(PLUGIN ubuntu-web-plugin) set(PLUGIN_SRC plugin.cpp) add_library(${PLUGIN} MODULE ${PLUGIN_SRC}) target_link_libraries(${PLUGIN} Qt5::Core Qt5::Gui Qt5::Qml ) file(GLOB PLUGIN_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} *.qml qmldir *.js *.png) install(TARGETS ${PLUGIN} DESTINATION ${UBUNTU_WEB_IMPORTS_DIR}) install(FILES ${PLUGIN_FILES} DESTINATION ${UBUNTU_WEB_IMPORTS_DIR}) if(NOT ${CMAKE_CURRENT_BINARY_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR}) # copy qml files over to build dir to be able to import them uninstalled foreach(_file ${PLUGIN_FILES}) add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${_file} DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${_file} COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/${_file} ${CMAKE_CURRENT_BINARY_DIR}/${_file}) endforeach(_file) add_custom_target(copy_files_to_build_dir DEPENDS ${PLUGIN_FILES}) add_dependencies(${PLUGIN} copy_files_to_build_dir) endif() ./src/Ubuntu/Web/ItemSelector02.qml0000644000004100000410000000365513004613604017313 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 as ListItems import Ubuntu.Components.Popups 1.3 Popover { id: itemSelector property QtObject selectorModel: model caller: parent contentWidth: Math.min(parent.width - units.gu(10), units.gu(40)) property real listContentHeight: 0 // intermediate property to avoid binding loop contentHeight: Math.min(parent.height - units.gu(10), listContentHeight) ListView { clip: true width: itemSelector.contentWidth height: itemSelector.contentHeight model: selectorModel.items delegate: ListItem { ListItemLayout { title.text: model.text } enabled: model.enabled selected: model.selected onClicked: { selectorModel.items.select(model.index) selectorModel.accept() } } section.property: "group" section.delegate: ListItems.Header { text: section } onContentHeightChanged: itemSelector.listContentHeight = contentHeight } Component.onCompleted: show() onVisibleChanged: { if (!visible) { selectorModel.cancel() } } } ./src/Ubuntu/Web/fb-no-appbanner.js0000644000004100000410000000250513004613604017333 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ // ==UserScript== // @include https://m.facebook.com/* // ==/UserScript== // Ensure that the facebook mobile site never shows its app banner, which // suggests installing a native Android/iOS application based on naïve // parsing of the UA string. // The banner does not currently have any class or id that would make sure we // can identify it easily. But we know that it does always appear just before // the login form, so we find it that way. var login = document.getElementsByClassName("mobile-login-form"); if (login.length === 1) { var appbanner = login[0].previousSibling; if (appbanner) { appbanner.parentNode.removeChild(appbanner); } } ./src/Ubuntu/Web/qmldir0000644000004100000410000000025113004613604015237 0ustar www-datawww-datamodule Ubuntu.Web plugin ubuntu-web-plugin WebView 0.2 UbuntuWebView02.qml WebContext 0.2 UbuntuWebContext.qml singleton SharedWebContext 0.2 UbuntuSharedWebContext.qml ./src/Ubuntu/Web/UbuntuSharedWebContext.qml0000644000004100000410000000153113004613604021155 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ pragma Singleton import QtQml 2.0 QtObject { property alias customUA: context.userAgent property QtObject sharedContext: UbuntuWebContext { id: context } } ./src/Ubuntu/Web/ua-overrides-desktop.js0000644000004100000410000000374413004613623020451 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ .pragma library var overrides = [ ["^https?:\/\/(www\.)?google\.com\/calendar", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chromium/35.0.1870.2 Chrome/35.0.1870.2 Safari/537.36"], ["^http:\/\/chrome\.angrybirds\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/35.0.1870.2 Safari/537.36"], // http://pad.lv/1284158 ["^https?:\/\/(www\.)?youtube\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/35.0.1870.2 Safari/537.36"], // http://pad.lv/1412880 ["^https?:\/\/(www\.)?google\..+\/maps", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/35.0.1870.2 Safari/537.36"], // http://pad.lv/1503506, http://pad.lv/1551649 ["^https?:\/\/mail\.google\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/47.0.2526.106 Safari/537.36"], // http://pad.lv/1452616 // Google hangouts (https://launchpad.net/bugs/1565055) ["^https?:\/\/hangouts\.google\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/49.0.2623.87 Safari/537.36"], ["^https?:\/\/talkgadget\.google\.com\/hangouts\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/49.0.2623.87 Safari/537.36"], ["^https?:\/\/plus\.google\.com\/hangouts\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/49.0.2623.87 Safari/537.36"], ]; ./src/Ubuntu/Web/plugin.cpp0000644000004100000410000002033313004613623016032 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "plugin.h" // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include class UbuntuWebPluginContext : public QObject { Q_OBJECT Q_PROPERTY(QString cacheLocation READ cacheLocation NOTIFY cacheLocationChanged) Q_PROPERTY(QString dataLocation READ dataLocation NOTIFY dataLocationChanged) Q_PROPERTY(qreal screenDiagonal READ screenDiagonal NOTIFY screenDiagonalChanged) Q_PROPERTY(int cacheSizeHint READ cacheSizeHint NOTIFY cacheSizeHintChanged) Q_PROPERTY(QString webviewDevtoolsDebugHost READ devtoolsHost CONSTANT) Q_PROPERTY(int webviewDevtoolsDebugPort READ devtoolsPort CONSTANT) Q_PROPERTY(QStringList webviewHostMappingRules READ hostMappingRules CONSTANT) Q_PROPERTY(QString ubuntuVersion READ ubuntuVersion CONSTANT) public: UbuntuWebPluginContext(QObject* parent = 0); QString cacheLocation() const; QString dataLocation() const; qreal screenDiagonal() const; int cacheSizeHint() const; QString devtoolsHost(); int devtoolsPort(); QStringList hostMappingRules(); QString ubuntuVersion() const; Q_SIGNALS: void cacheLocationChanged() const; void dataLocationChanged() const; void screenDiagonalChanged() const; void cacheSizeHintChanged() const; private Q_SLOTS: void onFocusWindowChanged(QWindow* window); void updateScreen(); private: qreal m_screenDiagonal; // in millimeters QString m_devtoolsHost; int m_devtoolsPort; QStringList m_hostMappingRules; bool m_hostMappingRulesQueried; }; UbuntuWebPluginContext::UbuntuWebPluginContext(QObject* parent) : QObject(parent) , m_screenDiagonal(0) , m_devtoolsPort(-2) , m_hostMappingRulesQueried(false) { connect(qApp, SIGNAL(applicationNameChanged()), SIGNAL(cacheLocationChanged())); connect(qApp, SIGNAL(applicationNameChanged()), SIGNAL(dataLocationChanged())); connect(qApp, SIGNAL(applicationNameChanged()), SIGNAL(cacheSizeHintChanged())); updateScreen(); connect(qApp, SIGNAL(focusWindowChanged(QWindow*)), SLOT(onFocusWindowChanged(QWindow*))); } void UbuntuWebPluginContext::updateScreen() { QWindow* window = qApp->focusWindow(); if (window) { QScreen* screen = window->screen(); if (screen) { QSizeF size = screen->physicalSize(); qreal diagonal = qSqrt(size.width() * size.width() + size.height() * size.height()); if (diagonal != m_screenDiagonal) { m_screenDiagonal = diagonal; Q_EMIT screenDiagonalChanged(); } } } } QString UbuntuWebPluginContext::cacheLocation() const { QDir location(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); if (!location.exists()) { QDir::root().mkpath(location.absolutePath()); } return location.absolutePath(); } QString UbuntuWebPluginContext::dataLocation() const { QDir location(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); if (!location.exists()) { QDir::root().mkpath(location.absolutePath()); } else { // Prior to fixing https://launchpad.net/bugs/1424726, chromium’s cache // data was written to the data location. Purge the old cache data. QDir(location.absoluteFilePath("Cache")).removeRecursively(); } return location.absolutePath(); } qreal UbuntuWebPluginContext::screenDiagonal() const { return m_screenDiagonal; } int UbuntuWebPluginContext::cacheSizeHint() const { if (QCoreApplication::applicationName() == "webbrowser-app") { // Let chromium decide the optimum cache size based on available disk space return 0; } else { // For webapps and other embedders, determine the cache size hint // using heuristics based on the disk space (total, and available). QStorageInfo storageInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); const int MB = 1024 * 1024; // The total cache size for all apps should not exceed 10% of the total disk space int maxSharedCache = storageInfo.bytesTotal() / MB * 0.1; // One given app is allowed to use up to 5% o the total cache size int maxAppCacheAllowance = maxSharedCache * 0.05; // Ensure it never exceeds 200 MB though int maxAppCacheAbsolute = qMin(maxAppCacheAllowance, 200); // Never use more than 20% of the available disk space int maxAppCacheRelative = storageInfo.bytesAvailable() / MB * 0.2; // Never set a size hint below 5 MB, as that would result in a very inefficient cache return qMax(5, qMin(maxAppCacheAbsolute, maxAppCacheRelative)); } } QStringList UbuntuWebPluginContext::hostMappingRules() { static const QString HOST_MAPPING_RULES_SEP = ","; if (!m_hostMappingRulesQueried) { const char* HOST_MAPPING_RULES_ENV_VAR = "UBUNTU_WEBVIEW_HOST_MAPPING_RULES"; if (qEnvironmentVariableIsSet(HOST_MAPPING_RULES_ENV_VAR)) { QString rules(qgetenv(HOST_MAPPING_RULES_ENV_VAR)); // from http://src.chromium.org/svn/trunk/src/net/base/host_mapping_rules.h m_hostMappingRules = rules.split(HOST_MAPPING_RULES_SEP); } m_hostMappingRulesQueried = true; } return m_hostMappingRules; } QString UbuntuWebPluginContext::devtoolsHost() { if (m_devtoolsHost.isNull()) { const char* DEVTOOLS_HOST_ENV_VAR = "UBUNTU_WEBVIEW_DEVTOOLS_HOST"; if (qEnvironmentVariableIsSet(DEVTOOLS_HOST_ENV_VAR)) { m_devtoolsHost = qgetenv(DEVTOOLS_HOST_ENV_VAR); } else { m_devtoolsHost = ""; } } return m_devtoolsHost; } int UbuntuWebPluginContext::devtoolsPort() { if (m_devtoolsPort == -2) { const int DEVTOOLS_INVALID_PORT = -1; m_devtoolsPort = DEVTOOLS_INVALID_PORT; const char* DEVTOOLS_PORT_ENV_VAR = "UBUNTU_WEBVIEW_DEVTOOLS_PORT"; if (qEnvironmentVariableIsSet(DEVTOOLS_PORT_ENV_VAR)) { QByteArray environmentVarValue = qgetenv(DEVTOOLS_PORT_ENV_VAR); bool ok = false; int value = environmentVarValue.toInt(&ok); if (ok) { m_devtoolsPort = value; } } if (m_devtoolsPort <= 0) { m_devtoolsPort = DEVTOOLS_INVALID_PORT; } } return m_devtoolsPort; } QString UbuntuWebPluginContext::ubuntuVersion() const { return QStringLiteral(UBUNTU_VERSION); } void UbuntuWebPluginContext::onFocusWindowChanged(QWindow* window) { updateScreen(); if (window) { connect(window, SIGNAL(screenChanged(QScreen*)), SLOT(updateScreen())); } } void UbuntuBrowserPlugin::initializeEngine(QQmlEngine* engine, const char* uri) { Q_UNUSED(uri); QQmlContext* context = engine->rootContext(); context->setContextObject(new UbuntuWebPluginContext(context)); } void UbuntuBrowserPlugin::registerTypes(const char* uri) { Q_ASSERT(uri == QLatin1String("Ubuntu.Components.Extras.Browser") || uri == QLatin1String("Ubuntu.Web")); if (uri == QLatin1String("Ubuntu.Components.Extras.Browser")) { qmlInfo(0) << "WARNING: the use of the Ubuntu.Components.Extras.Browser " "namespace is deprecated, please consider updating your " "applications to import Ubuntu.Web instead."; } } #include "plugin.moc" ./src/Ubuntu/Web/selection02.js0000644000004100000410000000142113004613604016511 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ document.defaultView.addEventListener('scroll', function(event) { oxide.sendMessage('scroll', {}); }); ./src/Ubuntu/Web/twitter-no-omniprompt.js0000644000004100000410000000215513004613604020705 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ // ==UserScript== // @include https://mobile.twitter.com/* // ==/UserScript== // Ensure that the twitter mobile site never shows its "omniprompt" header, // which suggests installing a native Android/iOS application based on // naïve parsing of the UA string. if (document.body) { document.body.classList.add("no-omniprompt"); } var androidPrompt = document.querySelector(".client-prompt"); if (androidPrompt) { androidPrompt.style.display = "none"; }./src/Ubuntu/Web/UserAgent02.qml0000644000004100000410000000627513004613623016613 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQml 2.0 import com.canonical.Oxide 1.15 /* * Useful documentation: * http://en.wikipedia.org/wiki/User_agent#Format * https://developer.mozilla.org/en-US/docs/Gecko_user_agent_string_reference * https://wiki.mozilla.org/B2G/User_Agent * https://github.com/mozilla-b2g/gaia/blob/master/build/ua-override-prefs.js * https://developers.google.com/chrome/mobile/docs/user-agent */ QtObject { // Empirical value: screens smaller than 19cm are considered small enough that a // mobile UA string is used, screens bigger than that will get desktop content. readonly property bool smallScreen: screenDiagonal < 190 // %1: Ubuntu version, e.g. "14.04" // %2: optional token to specify further attributes of the platform, e.g. "like Android" // %3: optional hardware ID token // %4: WebKit version, e.g. "537.36" // %5: Chromium version, e.g. "35.0.1870.2" // %6: Optional token to provide additional free-form information, e.g. "Mobile" // %7: Safari version, e.g. "537.36" // %8: Optional token, in case some extra bits are needed to make things work (e.g. an extra form factor info etc.) // // note #1: "Mozilla/5.0" is misinformation, but it is a legacy token that // virtually every single UA out there has, it seems unwise to remove it // note #2: "AppleWebKit", as opposed to plain "WebKit", does make a // difference in the content served by certain sites (e.g. gmail.com) readonly property string _template: "Mozilla/5.0 (Linux; Ubuntu %1%2%3) AppleWebKit/%4 Chromium/%5 %6Safari/%7%8" readonly property string _attributes: smallScreen ? "like Android 4.4" : "" readonly property string _hardwareID: "" // See chromium/src/content/webkit_version.h.in in oxide’s source tree. readonly property string _webkitVersion: "537.36" readonly property string _chromiumVersion: Oxide.chromiumVersion readonly property string _formFactor: smallScreen ? "Mobile" : "" readonly property string _more: "" property string defaultUA: { var ua = _template ua = ua.arg(ubuntuVersion) // %1 ua = ua.arg((_attributes !== "") ? " %1".arg(_attributes) : "") // %2 ua = ua.arg((_hardwareID !== "") ? "; %1".arg(_hardwareID) : "") // %3 ua = ua.arg(_webkitVersion) // %4 ua = ua.arg(_chromiumVersion) // %5 ua = ua.arg((_formFactor !== "") ? "%1 ".arg(_formFactor) : "") // %6 ua = ua.arg(_webkitVersion) // %7 ua = ua.arg((_more !== "") ? " %1".arg(_more) : "") // %8 return ua } } ./src/Ubuntu/Web/ua-overrides-mobile.js0000644000004100000410000000427013004613623020242 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ .pragma library var overrides = [ ["^https?:\/\/mail\.google\.com\/", "Mozilla/5.0 (Linux; Ubuntu 14.04 like Android 4.4) AppleWebKit/537.36 Chromium/35.0.1870.2 Mobile Safari"], ["^https?:\/\/(www|m)\.youtube\.com\/", "Mozilla/5.0 (Linux; Ubuntu 14.04 like Android 4.4;) AppleWebKit/537.36 Chromium/35.0.1870.2 Mobile Safari/537.36"], // http://pad.lv/1228415, http://pad.lv/1415107, http://pad.lv/1417258, http://pad.lv/1499394 ["^http:\/\/chrome\.angrybirds\.com\/", "Mozilla/5.0 (Linux; Ubuntu 14.04 like Android 4.4;) AppleWebKit/537.36 Chrome/35.0.1870.2 Mobile Safari/537.36"], // http://pad.lv/1284158 ["^https?:\/\/(\w+\.)*hsbc\.com\.br\/", "Mozilla/5.0 (Linux; Ubuntu 14.04 like Android 4.4;) AppleWebKit/537.36 Chrome/35.0.1870.2 Mobile Safari/537.36"], // http://pad.lv/1380657 ["^http:\/\/(\w+\.)*espn\.(go\.)?com\/", "Mozilla/5.0 (Linux; Ubuntu 14.04 like Android 4.4;) AppleWebKit/537.36 Chrome/35.0.1870.2 Mobile Safari/537.36"], // http://pad.lv/1316259 // Google hangouts (https://launchpad.net/bugs/1565055) ["^https?:\/\/hangouts\.google\.com\/", "Mozilla/5.0 (Linux; Ubuntu 14.04 like Android 4.4;) AppleWebKit/537.36 Chrome/49.0.2623.87 Mobile Safari/537.36"], ["^https?:\/\/talkgadget\.google\.com\/hangouts\/", "Mozilla/5.0 (Linux; Ubuntu 14.04 like Android 4.4;) AppleWebKit/537.36 Chrome/49.0.2623.87 Mobile Safari/537.36"], ["^https?:\/\/plus\.google\.com\/hangouts\/", "Mozilla/5.0 (Linux; Ubuntu 14.04 like Android 4.4;) AppleWebKit/537.36 Chrome/49.0.2623.87 Mobile Safari/537.36"], ]; ./src/Ubuntu/Web/UbuntuWebView02.qml0000644000004100000410000002535313004613604017466 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import QtQuick.Window 2.2 import com.canonical.Oxide 1.12 as Oxide import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import "." // QTBUG-34418 Oxide.WebView { id: _webview /** * Client overridable function called before the default treatment of a * valid navigation request. This function can stop the navigation request * if it sets the 'action' field of the request to IgnoreRequest. * */ function navigationRequestedDelegate(request) { } context: SharedWebContext.sharedContext messageHandlers: [ Oxide.ScriptMessageHandler { msgId: "scroll" contexts: ["oxide://selection/"] callback: function(msg, frame) { internal.dismissCurrentContextualMenu() } } ] onNavigationRequested: { request.action = Oxide.NavigationRequest.ActionAccept; navigationRequestedDelegate(request); } preferences.passwordEchoEnabled: Qt.inputMethod.visible popupMenu: ItemSelector02 { automaticOrientation: false } Item { id: contextualRectangle visible: false readonly property real locationBarOffset: _webview.locationBarController.height + _webview.locationBarController.offset x: internal.contextModel ? internal.contextModel.position.x : 0 y: internal.contextModel ? internal.contextModel.position.y + locationBarOffset : 0 } // XXX: This property is deprecated in favour of contextModel. property QtObject contextualData: QtObject { property url href property string title property url img function clear() { href = '' title = '' img = '' } } property var contextualActions // type: ActionList contextMenu: ActionSelectionPopover { objectName: "contextMenu" actions: contextualActions caller: contextualRectangle // Override default implementation to prevent context menu from stealing // active focus when shown (https://launchpad.net/bugs/1526884). function show() { visible = true __foreground.show() } Component.onCompleted: { internal.dismissCurrentContextualMenu() internal.contextModel = model var empty = true if (actions) { for (var i in actions.actions) { if (actions.actions[i].enabled) { empty = false break } } } if (empty) { internal.dismissCurrentContextualMenu() } else { contextualData.clear() contextualData.href = model.linkUrl contextualData.title = model.linkText if ((model.mediaType == Oxide.WebView.MediaTypeImage) && model.hasImageContents) { contextualData.img = model.srcUrl } show() } } onVisibleChanged: { if (!visible) { internal.dismissCurrentContextualMenu() } } Binding { // Ensure the context menu doesn’t steal focus from // the webview when one of its actions is activated // (https://launchpad.net/bugs/1526884). target: __foreground property: "activeFocusOnPress" value: false } } readonly property QtObject contextModel: internal.contextModel property var selectionActions // type: ActionList onSelectionActionsChanged: console.warn("WARNING: the 'selectionActions' property is deprecated and ignored.") function copy() { console.warn("WARNING: the copy() function is deprecated and does nothing.") } touchSelectionController.handle: Image { objectName: "touchSelectionHandle" readonly property int handleOrientation: orientation width: units.gu(1.5) height: units.gu(1.5) source: "handle.png" Component.onCompleted: horizontalPaddingRatio = 0.5 } UbuntuShape { objectName: "touchSelectionActions" // FIXME: hide contextual actions while resizing the // selection (needs an additional API in oxide?) visible: _webview.activeFocus && _webview.touchSelectionController.active && !selectionOutOfSight aspect: UbuntuShape.DropShadow backgroundColor: "white" readonly property int padding: units.gu(1) width: touchSelectionActionsRow.width + padding * 2 height: childrenRect.height + padding * 2 readonly property rect bounds: _webview.touchSelectionController.bounds readonly property bool selectionOutOfSight: (bounds.x > _webview.width) || ((bounds.x + bounds.width) < 0) || (bounds.y > _webview.height) || ((bounds.y + bounds.height) < 0) readonly property real handleHeight: units.gu(1.5) readonly property real spacing: units.gu(1) readonly property bool fitsBelow: (bounds.y + bounds.height + handleHeight + spacing + height) <= _webview.height readonly property bool fitsAbove: (bounds.y - spacing - height) >= (_webview.locationBarController.height + _webview.locationBarController.offset) readonly property real xCentered: bounds.x + (bounds.width - width) / 2 x: ((xCentered >= 0) && ((xCentered + width) <= _webview.width)) ? xCentered : (xCentered < 0) ? 0 : _webview.width - width y: fitsBelow ? (bounds.y + bounds.height + handleHeight + spacing) : fitsAbove ? (bounds.y - spacing - height) : (_webview.height + _webview.locationBarController.height + _webview.locationBarController.offset - height) / 2 ActionList { id: touchSelectionActions Action { name: "selectall" text: i18n.dtr('ubuntu-ui-toolkit', "Select All") iconName: "edit-select-all" enabled: _webview.editingCapabilities & Oxide.WebView.SelectAllCapability visible: enabled onTriggered: _webview.executeEditingCommand(Oxide.WebView.EditingCommandSelectAll) } Action { name: "cut" text: i18n.dtr('ubuntu-ui-toolkit', "Cut") iconName: "edit-cut" enabled: _webview.editingCapabilities & Oxide.WebView.CutCapability visible: enabled onTriggered: _webview.executeEditingCommand(Oxide.WebView.EditingCommandCut) } Action { name: "copy" text: i18n.dtr('ubuntu-ui-toolkit', "Copy") iconName: "edit-copy" enabled: _webview.editingCapabilities & Oxide.WebView.CopyCapability visible: enabled onTriggered: _webview.executeEditingCommand(Oxide.WebView.EditingCommandCopy) } Action { name: "paste" text: i18n.dtr('ubuntu-ui-toolkit', "Paste") iconName: "edit-paste" enabled: _webview.editingCapabilities & Oxide.WebView.PasteCapability visible: enabled onTriggered: _webview.executeEditingCommand(Oxide.WebView.EditingCommandPaste) } } Row { id: touchSelectionActionsRow x: parent.padding y: parent.padding width: { // work around what seems to be a bug in Row’s childrenRect.width var w = 0 for (var i in visibleChildren) { w += visibleChildren[i].width } return w } height: units.gu(6) Repeater { model: touchSelectionActions.actions.length AbstractButton { objectName: "touchSelectionAction_" + action.name anchors { top: parent.top bottom: parent.bottom } width: Math.max(units.gu(5), implicitWidth) + units.gu(2) action: touchSelectionActions.actions[modelData] styleName: "ToolbarButtonStyle" activeFocusOnPress: false } } } } QtObject { id: internal property int lastLoadRequestStatus: -1 property QtObject contextModel: null function dismissCurrentContextualMenu() { var model = contextModel contextModel = null if (model) { model.close() } } onContextModelChanged: if (!contextModel) _webview.contextualData.clear() } readonly property bool lastLoadSucceeded: internal.lastLoadRequestStatus === Oxide.LoadEvent.TypeSucceeded readonly property bool lastLoadStopped: internal.lastLoadRequestStatus === Oxide.LoadEvent.TypeStopped readonly property bool lastLoadFailed: internal.lastLoadRequestStatus === Oxide.LoadEvent.TypeFailed onLoadEvent: { if (!event.isError) { internal.lastLoadRequestStatus = event.type } internal.dismissCurrentContextualMenu() } readonly property int screenOrientation: Screen.orientation onScreenOrientationChanged: { internal.dismissCurrentContextualMenu() } onJavaScriptConsoleMessage: { if (_webview.incognito) { return } var msg = "[JS] (%1:%2) %3".arg(sourceId).arg(lineNumber).arg(message) if (level === Oxide.WebView.LogSeverityVerbose) { console.log(msg) } else if (level === Oxide.WebView.LogSeverityInfo) { console.info(msg) } else if (level === Oxide.WebView.LogSeverityWarning) { console.warn(msg) } else if ((level === Oxide.WebView.LogSeverityError) || (level === Oxide.WebView.LogSeverityErrorReport) || (level === Oxide.WebView.LogSeverityFatal)) { console.error(msg) } } } ./src/Ubuntu/Web/UbuntuWebContext.qml0000644000004100000410000000660013004613604020030 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import com.canonical.Oxide 1.9 as Oxide Oxide.WebContext { id: oxideContext readonly property string defaultUserAgent: __ua.defaultUA dataPath: dataLocation cachePath: cacheLocation maxCacheSizeHint: cacheSizeHint userAgent: defaultUserAgent sessionCookieMode: { if (typeof webContextSessionCookieMode !== 'undefined') { if (webContextSessionCookieMode === "persistent") { return Oxide.WebContext.SessionCookieModePersistent } else if (webContextSessionCookieMode === "restored") { return Oxide.WebContext.SessionCookieModeRestored } } return Oxide.WebContext.SessionCookieModeEphemeral } userScripts: [ Oxide.UserScript { context: "oxide://smartbanners/" url: Qt.resolvedUrl("smartbanners.js") incognitoEnabled: true matchAllFrames: true }, Oxide.UserScript { context: "oxide://twitter-no-omniprompt/" url: Qt.resolvedUrl("twitter-no-omniprompt.js") incognitoEnabled: true matchAllFrames: true }, Oxide.UserScript { context: "oxide://fb-no-appbanner/" url: Qt.resolvedUrl("fb-no-appbanner.js") incognitoEnabled: true matchAllFrames: true }, Oxide.UserScript { context: "oxide://selection/" url: Qt.resolvedUrl("selection02.js") incognitoEnabled: true matchAllFrames: true } ] property QtObject __ua: UserAgent02 { onSmallScreenChanged: reloadOverrides() Component.onCompleted: reloadOverrides() property string _target: "" function reloadOverrides() { var target = smallScreen ? "mobile" : "desktop" if (target == _target) return _target = target var script = "ua-overrides-%1.js".arg(target) var temp = null try { temp = Qt.createQmlObject('import QtQml 2.0; import "%1" as Overrides; QtObject { readonly property var overrides: Overrides.overrides }'.arg(script), oxideContext) } catch (e) { console.error("No overrides found for", target) } if (temp !== null) { console.log("Loaded %1 UA override(s) from %2".arg(temp.overrides.length).arg(Qt.resolvedUrl(script))) userAgentOverrides = temp.overrides temp.destroy() } } } devtoolsEnabled: webviewDevtoolsDebugPort !== -1 devtoolsPort: webviewDevtoolsDebugPort devtoolsIp: webviewDevtoolsDebugHost hostMappingRules: webviewHostMappingRules } ./src/Ubuntu/Web/plugin.h0000644000004100000410000000202113004613604015470 0ustar www-datawww-data/* * Copyright 2013 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __PLUGIN_H__ #define __PLUGIN_H__ // Qt #include class UbuntuBrowserPlugin : public QQmlExtensionPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") public: void initializeEngine(QQmlEngine* engine, const char* uri); void registerTypes(const char* uri); }; #endif // __PLUGIN_H__ ./src/Ubuntu/Web/handle@27.png0000644000004100000410000000056413004613604016245 0ustar www-datawww-dataPNG  IHDR"":G bKGD pHYs  tIME 5:nItEXtComment̖IDATX10 E=2& 8F[ڥb`3'TXHU)J$~]nk6V&pEڟE`ǟ*&o!}RWL0r^gh| `|88' ω!:C%1WD/T )s<A"H  "ibgq"K)j&5P|jq IENDB`./src/Ubuntu/Web/smartbanners.js0000644000004100000410000000220713004613604017064 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ // ==UserScript== // @run-at document-start // ==/UserScript== // Ensure that so-called "smart banners" generated by the smartbanner jQuery // plugin (http://jasny.github.io/jquery.smartbanner/) that advertise native // Android/iOS applications based on a very naïve parsing of the UA string // are never shown. var exdate = new Date(); exdate.setFullYear(exdate.getFullYear() + 5); try { document.cookie = "sb-closed=true;path=/;expires=" + exdate.toUTCString(); } catch (e) {} ./src/Ubuntu/Components/0000755000004100000410000000000013004613605015437 5ustar www-datawww-data./src/Ubuntu/Components/CMakeLists.txt0000644000004100000410000000003113004613604020170 0ustar www-datawww-dataadd_subdirectory(Extras) ./src/Ubuntu/Components/Extras/0000755000004100000410000000000013004613605016705 5ustar www-datawww-data./src/Ubuntu/Components/Extras/CMakeLists.txt0000644000004100000410000000003213004613604021437 0ustar www-datawww-dataadd_subdirectory(Browser) ./src/Ubuntu/Components/Extras/Browser/0000755000004100000410000000000013004613605020330 5ustar www-datawww-data./src/Ubuntu/Components/Extras/Browser/CMakeLists.txt0000644000004100000410000000232013004613604023064 0ustar www-datawww-dataproject(webbrowser-plugin) find_package(Qt5Core REQUIRED) find_package(Qt5Gui REQUIRED) find_package(Qt5Qml REQUIRED) set(WEBBROWSER_IMPORTS_DIR "${QT_INSTALL_QML}/Ubuntu/Components/Extras/Browser") set(PLUGIN ubuntu-ui-extras-browser-plugin) set(PLUGIN_SRC plugin.cpp) add_library(${PLUGIN} MODULE ${PLUGIN_SRC}) target_link_libraries(${PLUGIN} Qt5::Core Qt5::Gui Qt5::Qml ) file(GLOB QML_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} *.qml qmldir *.js) install(TARGETS ${PLUGIN} DESTINATION ${WEBBROWSER_IMPORTS_DIR}) install(FILES ${QML_FILES} DESTINATION ${WEBBROWSER_IMPORTS_DIR}) if(NOT ${CMAKE_CURRENT_BINARY_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR}) # copy qml files and assets over to build dir to be able to import them uninstalled foreach(_file ${QML_FILES}) add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${_file} DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${_file} COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/${_file} ${CMAKE_CURRENT_BINARY_DIR}/${_file}) endforeach(_file) add_custom_target(copy_files_to_build_dir_legacy DEPENDS ${QML_FILES}) add_dependencies(${PLUGIN} copy_files_to_build_dir_legacy) endif() ./src/Ubuntu/Components/Extras/Browser/ItemSelector02.qml0000777000004100000410000000000013004613604030432 2../../../Web/ItemSelector02.qmlustar www-datawww-data./src/Ubuntu/Components/Extras/Browser/qmldir0000644000004100000410000000043113004613604021540 0ustar www-datawww-datamodule Ubuntu.Components.Extras.Browser plugin ubuntu-ui-extras-browser-plugin UbuntuWebView 0.2 UbuntuWebView02.qml singleton UbuntuSharedWebContext 0.2 UbuntuSharedWebContext.qml singleton SharedWebContext 0.2 UbuntuSharedWebContext.qml UbuntuWebContext 0.2 UbuntuWebContext.qml ./src/Ubuntu/Components/Extras/Browser/UbuntuSharedWebContext.qml0000777000004100000410000000000013004613604034160 2../../../Web/UbuntuSharedWebContext.qmlustar www-datawww-data./src/Ubuntu/Components/Extras/Browser/ua-overrides-desktop.js0000777000004100000410000000000013004613604032726 2../../../Web/ua-overrides-desktop.jsustar www-datawww-data./src/Ubuntu/Components/Extras/Browser/plugin.cpp0000777000004100000410000000000013004613604025706 2../../../Web/plugin.cppustar www-datawww-data./src/Ubuntu/Components/Extras/Browser/selection02.js0000777000004100000410000000000013004613604027054 2../../../Web/selection02.jsustar www-datawww-data./src/Ubuntu/Components/Extras/Browser/UserAgent02.qml0000777000004100000410000000000013004613604027226 2../../../Web/UserAgent02.qmlustar www-datawww-data./src/Ubuntu/Components/Extras/Browser/ua-overrides-mobile.js0000777000004100000410000000000013004613604032322 2../../../Web/ua-overrides-mobile.jsustar www-datawww-data./src/Ubuntu/Components/Extras/Browser/UbuntuWebView02.qml0000777000004100000410000000000013004613604030762 2../../../Web/UbuntuWebView02.qmlustar www-datawww-data./src/Ubuntu/Components/Extras/Browser/UbuntuWebContext.qml0000777000004100000410000000000013004613604031702 2../../../Web/UbuntuWebContext.qmlustar www-datawww-data./src/Ubuntu/Components/Extras/Browser/plugin.h0000777000004100000410000000000013004613604025020 2../../../Web/plugin.hustar www-datawww-data./src/app/0000755000004100000410000000000013004613623012610 5ustar www-datawww-data./src/app/CMakeLists.txt0000644000004100000410000000272713004613604015357 0ustar www-datawww-dataproject(webbrowser-common) find_package(Qt5Core REQUIRED) find_package(Qt5Gui REQUIRED) find_package(Qt5Network REQUIRED) find_package(Qt5Qml REQUIRED) find_package(Qt5Quick REQUIRED) find_package(Qt5Widgets REQUIRED) include(FindPkgConfig) pkg_check_modules(LIBAPPARMOR REQUIRED libapparmor) add_subdirectory(unity8) configure_file( config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h @ONLY) set(COMMONLIB webbrowser-common) set(COMMONLIB_SRC browserapplication.cpp favicon-fetcher.cpp meminfo.cpp mime-database.cpp session-storage.cpp single-instance-manager.cpp webbrowser-window.cpp qquickshortcut.cpp ) add_library(${COMMONLIB} STATIC ${COMMONLIB_SRC}) include_directories(${unity8_SOURCE_DIR}/libs/UbuntuGestures ${unity8_SOURCE_DIR}/plugins ${LIBAPPARMOR_INCLUDE_DIRS} ${Qt5Gui_PRIVATE_INCLUDE_DIRS}) target_link_libraries(${COMMONLIB} Qt5::Core Qt5::Gui Qt5::Network Qt5::Qml Qt5::Quick Qt5::Widgets UbuntuGesturesQml InputInfo ${LIBAPPARMOR_LDFLAGS} ) file(GLOB QML_FILES *.qml) install(FILES ${QML_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/webbrowser-app) file(GLOB JS_FILES *.js) install(FILES ${JS_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/webbrowser-app) install(DIRECTORY actions DESTINATION ${CMAKE_INSTALL_DATADIR}/webbrowser-app FILES_MATCHING PATTERN *.qml) add_subdirectory(webbrowser) add_subdirectory(webcontainer) ./src/app/mime-database.cpp0000644000004100000410000000340513004613604016006 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "mime-database.h" MimeDatabase::MimeDatabase(QObject* parent) : QObject(parent) { } QString MimeDatabase::filenameToMimeType(const QString& filename) const { QMimeType type = m_database.mimeTypeForFile(filename, QMimeDatabase::MatchExtension); if (!type.isDefault()) { return type.name(); } return QString(); } /*! Provide the system icon name for a given mimetype */ QString MimeDatabase::iconForMimetype(const QString& mimetypeString) const { QMimeType mimetype = m_database.mimeTypeForName(mimetypeString); if (mimetype.iconName().isEmpty() || !QIcon::hasThemeIcon(mimetype.iconName())) { if (QIcon::hasThemeIcon(mimetype.genericIconName())) { return mimetype.genericIconName(); } else { return ""; } } else { return mimetype.iconName(); } } /*! Provide the user friendly name for a given mimetype */ QString MimeDatabase::nameForMimetype(const QString& mimetypeString) const { QMimeType mimetype = m_database.mimeTypeForName(mimetypeString); return mimetype.comment(); } ./src/app/ChromeButton.qml0000644000004100000410000000227413004613604015740 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 AbstractButton { property real iconSize: width property alias iconName: icon.name property alias iconColor: icon.color Rectangle { anchors.fill: parent color: Theme.palette.selected.background visible: parent.pressed } Icon { id: icon anchors.centerIn: parent width: parent.iconSize height: width } opacity: enabled ? 1.0 : 0.3 Behavior on width { UbuntuNumberAnimation {} } } ./src/app/GeolocationPermissionRequest.qml0000644000004100000410000000251613004613604021213 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 Dialog { id: dialog property QtObject request: null title: i18n.tr("Permission Request") text: i18n.tr("This page wants to know your device’s location.") Button { objectName: "deny" text: i18n.tr("Deny") color: UbuntuColors.warmGrey onClicked: { request.deny() PopupUtils.close(dialog) } } Button { objectName: "allow" text: i18n.tr("Allow") color: UbuntuColors.orange onClicked: { request.allow() PopupUtils.close(dialog) } } } ./src/app/session-storage.h0000644000004100000410000000274213004613604016112 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __SESSION_STORAGE_H__ #define __SESSION_STORAGE_H__ // Qt #include #include #include #include class SessionStorage : public QObject { Q_OBJECT Q_PROPERTY(QString dataFile READ dataFile WRITE setDataFile NOTIFY dataFileChanged) Q_PROPERTY(bool locked READ isLocked NOTIFY lockedChanged) public: SessionStorage(QObject* parent = 0); const QString& dataFile() const; void setDataFile(const QString& dataFile); bool isLocked() const; Q_INVOKABLE void store(const QString& data) const; Q_INVOKABLE QString retrieve() const; Q_SIGNALS: void dataFileChanged() const; void lockedChanged() const; private: QString m_dataFile; QScopedPointer m_lock; }; #endif // __SESSION_STORAGE_H__ ./src/app/single-instance-manager.h0000644000004100000410000000247313004613623017462 0ustar www-datawww-data/* * Copyright 2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __SINGLE_INSTANCE_MANAGER_H__ #define __SINGLE_INSTANCE_MANAGER_H__ // Qt #include #include class QString; class QStringList; class SingleInstanceManager : public QObject { Q_OBJECT public: SingleInstanceManager(QObject* parent=nullptr); bool run(const QStringList& arguments, const QString& appId); Q_SIGNALS: void newInstanceLaunched(const QStringList& arguments) const; private Q_SLOTS: void onNewInstanceConnected(); void onReadyRead(); void onDisconnected(); private: QLocalServer m_server; bool listen(const QString& name); }; #endif // __SINGLE_INSTANCE_MANAGER__ ./src/app/webbrowser-window.h0000644000004100000410000000231213004613604016444 0ustar www-datawww-data/* * Copyright 2013 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __WEBBROWSER_WINDOW_H__ #define __WEBBROWSER_WINDOW_H__ #include #include class WebBrowserWindow : public QObject { Q_OBJECT Q_PROPERTY(QQuickWindow *window READ window WRITE setWindow NOTIFY windowChanged) public: explicit WebBrowserWindow(QObject *parent = 0); QQuickWindow * window() const; void setWindow(QQuickWindow *); public slots: void raise(); signals: void windowChanged(QQuickWindow *); private: QQuickWindow * _window; }; #endif // __WEBBROWSER_WINDOW_H__ ./src/app/Favicon.qml0000644000004100000410000000241413004613604014710 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import webbrowsercommon.private 0.1 Item { property alias source: fetcher.url property bool fallbackIcon: true property alias shouldCache: fetcher.shouldCache width: units.dp(16) height: units.dp(16) Image { id: image source: fetcher.localUrl anchors.fill: parent } FaviconFetcher { id: fetcher } Icon { anchors.fill: parent name: "stock_website" visible: parent.fallbackIcon && ((image.status !== Image.Ready) || !image.source.toString()) } } ./src/app/ContentShareDialog.qml0000644000004100000410000000274413004613604017046 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import Ubuntu.Content 1.3 PopupBase { id: shareDialog anchors.fill: parent property var activeTransfer property var items: [] property alias contentType: peerPicker.contentType Rectangle { anchors.fill: parent ContentPeerPicker { id: peerPicker handler: ContentHandler.Share visible: parent.visible onPeerSelected: { activeTransfer = peer.request() activeTransfer.items = shareDialog.items activeTransfer.state = ContentTransfer.Charged PopupUtils.close(shareDialog) } onCancelPressed: { PopupUtils.close(shareDialog) } } } } ./src/app/CertificateVerificationDialog.qml0000644000004100000410000000241613004613604021232 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 as Popups Popups.Dialog { title: i18n.tr("This connection is untrusted") // TRANSLATORS: %1 refers to the hostname text: i18n.tr("You are trying to securely reach %1, but the security certificate of this website is not trusted.").arg(model.hostname) Button { text: i18n.tr("Proceed anyway") color: "red" onClicked: model.accept() } Button { text: i18n.tr("Back to safety") color: "green" onClicked: model.reject() } Component.onCompleted: show() } ./src/app/PromptDialog.qml0000644000004100000410000000233213004613604015723 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 ModalDialog { title: i18n.tr("JavaScript Prompt") TextField { id: input text: model.defaultValue onAccepted: model.accept(input.text) } Button { text: i18n.tr("OK") color: "green" onClicked: model.accept(input.text) } Button { text: i18n.tr("Cancel") color: UbuntuColors.coolGrey onClicked: model.reject() } Binding { target: model property: "currentValue" value: input.text } } ./src/app/ChromeController.qml0000644000004100000410000000677313004613604016620 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import com.canonical.Oxide 1.7 as Oxide Item { visible: false property var webview property bool forceHide: false property bool forceShow: false property int defaultMode: internal.modeAuto onWebviewChanged: internal.updateVisibility() onForceHideChanged: internal.updateVisibility() onForceShowChanged: internal.updateVisibility() QtObject { id: internal readonly property int modeAuto: Oxide.LocationBarController.ModeAuto readonly property int modeShown: Oxide.LocationBarController.ModeShown readonly property int modeHidden: Oxide.LocationBarController.ModeHidden function updateVisibility() { if (!webview) { return } webview.locationBarController.animated = false if (forceHide) { webview.locationBarController.mode = internal.modeHidden } else if (forceShow) { webview.locationBarController.mode = internal.modeShown } else if (!webview.fullscreen) { webview.locationBarController.mode = defaultMode if (webview.locationBarController.mode == internal.modeAuto) { webview.locationBarController.show(false) } } webview.locationBarController.animated = true } } Connections { target: webview onFullscreenChanged: { if (webview.fullscreen) { webview.locationBarController.mode = internal.modeHidden } else if (!forceHide) { if (forceShow) { webview.locationBarController.mode = internal.modeShown } else { webview.locationBarController.mode = defaultMode if (webview.locationBarController.mode == internal.modeAuto) { webview.locationBarController.show(true) } } } } onLoadingStateChanged: { if (webview.loading && !webview.fullscreen && !forceHide && !forceShow && (webview.locationBarController.mode == internal.modeAuto)) { webview.locationBarController.show(true) } } onLoadEvent: { // When loading, force ModeShown until the load is committed or stopped, // to work around https://launchpad.net/bugs/1453908. if (forceHide || forceShow) return if (event.type == Oxide.LoadEvent.TypeStarted) { webview.locationBarController.mode = internal.modeShown } else if ((event.type == Oxide.LoadEvent.TypeCommitted) || (event.type == Oxide.LoadEvent.TypeStopped)) { webview.locationBarController.mode = defaultMode } } } } ./src/app/ContentHandler.qml0000644000004100000410000000224113004613604016231 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Content 1.3 import "MimeTypeMapper.js" as MimeTypeMapper Item { signal exportFromDownloads(var transfer, var mimetypeFilter, bool multiSelect) Connections { target: ContentHub onExportRequested: { exportFromDownloads(transfer, MimeTypeMapper.mimeTypeRegexForContentType(transfer.contentType), transfer.selectionType === ContentTransfer.Multiple) } } } ./src/app/ProxyAuthenticationDialog.qml0000644000004100000410000000167413004613604020473 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 AuthenticationDialog { title: i18n.tr("Proxy authentication required.") // TRANSLATORS: %1 refers to the proxy address, %2 refers to the proxy port text: i18n.tr("The website %1:%2 requires authentication.").arg(model.hostname).arg(model.port) } ./src/app/MediaAccessDialog.qml0000644000004100000410000000600613004613604016605 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 Dialog { property var request modal: true Label { elide: Text.ElideRight textSize: Label.Large color: theme.palette.normal.overlayText text: i18n.tr("Permission") } Label { color: theme.palette.normal.baseText wrapMode: Text.Wrap text: (request.isForAudio && request.isForVideo) ? i18n.tr("Allow this domain to access your camera and microphone?") : (request.isForVideo ? i18n.tr("Allow this domain to access your camera?") : i18n.tr("Allow this domain to access your microphone?")) } Label { color: theme.palette.normal.baseText wrapMode: Text.Wrap text: (request.embedder.toString() !== request.origin.toString()) // TRANSLATORS: %1 is the URL of the site requesting access to camera and/or microphone and %2 is the URL of the site that embeds it ? i18n.tr("%1 (embedded in %2)").arg(request.origin).arg(request.embedder) : request.origin } Item { height: units.gu(2) Rectangle { anchors { left: parent.left right: parent.right bottom: parent.bottom } height: units.dp(1) color: theme.palette.normal.base } } Row { height: units.gu(4) spacing: units.gu(2) layoutDirection: Qt.RightToLeft Button { objectName: "mediaAccessDialog.allowButton" text: i18n.tr("Yes") color: UbuntuColors.green width: units.gu(10) onClicked: { request.allow() hide() } } Button { objectName: "mediaAccessDialog.denyButton" text: i18n.tr("No") color: UbuntuColors.lightGrey width: units.gu(10) onClicked: { request.deny() hide() } } } // adjust default dialog visuals to custom design requirements // (should not be needed when updated dialog implementation lands in UITK) Binding { target: __foreground property: "margins" value: units.gu(2) } } ./src/app/config.h.in0000644000004100000410000000253113004613623014634 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __CONFIG_H__ #define __CONFIG_H__ #include #include #include #define REMOTE_INSPECTOR_PORT 9221 inline bool isRunningInstalled() { static bool installed = (QCoreApplication::applicationDirPath() == QDir("@CMAKE_INSTALL_FULL_BINDIR@").canonicalPath()); return installed; } inline QString UbuntuBrowserDirectory() { if (isRunningInstalled()) { return QString("@CMAKE_INSTALL_FULL_DATADIR@/webbrowser-app"); } else { return QString("@CMAKE_SOURCE_DIR@/src/app"); } } inline QString UbuntuBrowserImportsDirectory() { return QString("@CMAKE_BINARY_DIR@/src"); } #endif // __CONFIG_H__ ./src/app/ModalDialog.qml0000644000004100000410000000205113004613604015474 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 as Popups Popups.Dialog { text: model.message // Set the parent at construction time, instead of letting show() // set it later on, which for some reason results in the size of // the dialog not being updated. parent: QuickUtils.rootItem(this) Component.onCompleted: show() } ./src/app/AuthenticationDialog.qml0000644000004100000410000000317413004613604017426 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 as Popups Popups.Dialog { title: i18n.tr("Authentication required.") // TRANSLATORS: %1 refers to the URL of the current website text: i18n.tr("The website %1 requires authentication.").arg(model.hostname) function accept() { return model.accept(usernameInput.text, passwordInput.text) } TextField { id: usernameInput placeholderText: i18n.tr("Username") text: model.prefilledUsername onAccepted: accept() } TextField { id: passwordInput placeholderText: i18n.tr("Password") echoMode: TextInput.Password onAccepted: accept() } Button { text: i18n.tr("OK") color: "green" onClicked: accept() } Button { text: i18n.tr("Cancel") color: UbuntuColors.coolGrey onClicked: model.reject() } Component.onCompleted: show() } ./src/app/BeforeUnloadDialog.qml0000644000004100000410000000167113004613604017014 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 ModalDialog { title: i18n.tr("Confirm Navigation") Button { text: i18n.tr("Leave") onClicked: model.accept() } Button { text: i18n.tr("Stay") onClicked: model.reject() } } ./src/app/session-storage.cpp0000644000004100000410000000544413004613604016447 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ // system #include // Qt #include #include // local #include "session-storage.h" SessionStorage::SessionStorage(QObject* parent) : QObject(parent) {} const QString& SessionStorage::dataFile() const { return m_dataFile; } void SessionStorage::setDataFile(const QString& dataFile) { if (m_dataFile != dataFile) { m_dataFile = dataFile; Q_EMIT dataFileChanged(); bool locked = false; if (m_lock) { locked = m_lock->isLocked(); } if (!m_dataFile.isEmpty()) { m_lock.reset(new QLockFile(m_dataFile + ".lock")); m_lock->setStaleLockTime(0); m_lock->tryLock(); if (locked != m_lock->isLocked()) { Q_EMIT lockedChanged(); } } else { m_lock.reset(); if (locked) { Q_EMIT lockedChanged(); } } } } // 'isLocked' means that the session storage file is in use by this instance // of the app. There is only one session file for all instances of the app, // so the first instance locks it and is allowed to save its session, whereas // other instances discard their sessions when closed. // This has no effect on devices where there can only be one instance of an // app at any given time, it’s mostly useful on desktop to avoid instances // overwriting each other’s sessions. bool SessionStorage::isLocked() const { if (m_lock) { return m_lock->isLocked(); } return false; } void SessionStorage::store(const QString& data) const { if (m_dataFile.isEmpty()) { return; } QString tempName = m_dataFile + "." + \ QString::number(QDateTime::currentDateTime().toMSecsSinceEpoch());; QFile file(tempName); if (file.open(QIODevice::WriteOnly)) { file.write(data.toUtf8()); file.close(); rename(tempName.toUtf8().constData(), m_dataFile.toUtf8().constData()); } } QString SessionStorage::retrieve() const { QFile file(m_dataFile); if (file.open(QIODevice::ReadOnly)) { return file.readAll(); } return QString(); } ./src/app/ErrorSheet.qml0000644000004100000410000000305713004613604015411 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 Rectangle { property string url signal refreshClicked() Column { anchors.fill: parent anchors.margins: units.gu(4) spacing: units.gu(3) Label { width: parent.width fontSize: "x-large" text: i18n.tr("Network Error") } Label { width: parent.width // TRANSLATORS: %1 refers to the URL of the current page text: i18n.tr("It appears you are having trouble viewing: %1.").arg(url) wrapMode: Text.Wrap } Label { width: parent.width text: i18n.tr("Please check your network settings and try refreshing the page.") wrapMode: Text.Wrap } Button { text: i18n.tr("Refresh page") onClicked: refreshClicked() } } } ./src/app/qquickshortcut.cpp0000644000004100000410000001724213004613604016412 0ustar www-datawww-data/**************************************************************************** ** ** Copyright (C) 2015 The Qt Company Ltd. ** Contact: http://www.qt.io/licensing/ ** ** This file is part of the QtQuick module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL21$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see http://www.qt.io/terms-conditions. For further ** information use the contact form at http://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 or version 3 as published by the Free ** Software Foundation and appearing in the file LICENSE.LGPLv21 and ** LICENSE.LGPLv3 included in the packaging of this file. Please review the ** following information to ensure the GNU Lesser General Public License ** requirements will be met: https://www.gnu.org/licenses/lgpl.html and ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** As a special exception, The Qt Company gives you certain additional ** rights. These rights are described in The Qt Company LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qquickshortcut_p.h" #include #include #include QT_BEGIN_NAMESPACE /*! \qmltype Shortcut \instantiates QQuickShortcut \inqmlmodule QtQuick \since 5.5 \ingroup qtquick-input \brief Provides keyboard shortcuts The Shortcut type provides a way of handling keyboard shortcuts. The shortcut can be set to one of the \l{QKeySequence::StandardKey}{standard keyboard shortcuts}, or it can be described with a string containing a sequence of up to four key presses that are needed to \l{Shortcut::activated}{activate} the shortcut. \qml Item { id: view property int currentIndex Shortcut { sequence: StandardKey.NextChild onActivated: view.currentIndex++ } } \endqml \sa Keys */ /*! \qmlsignal QtQuick::Shortcut::activated() This signal is emitted when the shortcut is activated. The corresponding handler is \c onActivated. */ /*! \qmlsignal QtQuick::Shortcut::activatedAmbiguously() This signal is emitted when the shortcut is activated ambigously, meaning that it matches the start of more than one shortcut. The corresponding handler is \c onActivatedAmbiguously. */ QQuickShortcut::QQuickShortcut(QObject *parent) : QObject(parent), m_id(0), m_enabled(true), m_completed(false), m_autorepeat(true), m_context(Qt::WindowShortcut) { } QQuickShortcut::~QQuickShortcut() { ungrabShortcut(); } /*! \qmlproperty keysequence QtQuick::Shortcut::sequence This property holds the shortcut's key sequence. The key sequence can be set to one of the \l{QKeySequence::StandardKey}{standard keyboard shortcuts}, or it can be described with a string containing a sequence of up to four key presses that are needed to \l{Shortcut::activated}{activate} the shortcut. The default value is an empty key sequence. \qml Shortcut { sequence: "Ctrl+E,Ctrl+W" onActivated: edit.wrapMode = TextEdit.Wrap } \endqml */ QVariant QQuickShortcut::sequence() const { return m_sequence; } void QQuickShortcut::setSequence(const QVariant &sequence) { if (sequence == m_sequence) return; QKeySequence shortcut; if (sequence.type() == QVariant::Int) shortcut = QKeySequence(static_cast(sequence.toInt())); else shortcut = QKeySequence::fromString(sequence.toString()); grabShortcut(shortcut, m_context); m_sequence = sequence; m_shortcut = shortcut; emit sequenceChanged(); } /*! \qmlproperty bool QtQuick::Shortcut::enabled This property holds whether the shortcut is enabled. The default value is \c true. */ bool QQuickShortcut::isEnabled() const { return m_enabled; } void QQuickShortcut::setEnabled(bool enabled) { if (enabled == m_enabled) return; if (m_id) QGuiApplicationPrivate::instance()->shortcutMap.setShortcutEnabled(enabled, m_id, this); m_enabled = enabled; emit enabledChanged(); } /*! \qmlproperty bool QtQuick::Shortcut::autoRepeat This property holds whether the shortcut can auto repeat. The default value is \c true. */ bool QQuickShortcut::autoRepeat() const { return m_autorepeat; } void QQuickShortcut::setAutoRepeat(bool repeat) { if (repeat == m_autorepeat) return; if (m_id) QGuiApplicationPrivate::instance()->shortcutMap.setShortcutAutoRepeat(repeat, m_id, this); m_autorepeat = repeat; emit autoRepeatChanged(); } /*! \qmlproperty enumeration QtQuick::Shortcut::context This property holds the \l{Qt::ShortcutContext}{shortcut context}. Supported values are: \list \li \c Qt.WindowShortcut (default) - The shortcut is active when its parent item is in an active top-level window. \li \c Qt.ApplicationShortcut - The shortcut is active when one of the application's windows are active. \endlist \qml Shortcut { sequence: StandardKey.Quit context: Qt.ApplicationShortcut onActivated: Qt.quit() } \endqml */ Qt::ShortcutContext QQuickShortcut::context() const { return m_context; } void QQuickShortcut::setContext(Qt::ShortcutContext context) { if (context == m_context) return; grabShortcut(m_shortcut, context); m_context = context; emit contextChanged(); } void QQuickShortcut::classBegin() { } void QQuickShortcut::componentComplete() { m_completed = true; grabShortcut(m_shortcut, m_context); } bool QQuickShortcut::event(QEvent *event) { if (m_enabled && event->type() == QEvent::Shortcut) { QShortcutEvent *se = static_cast(event); if (se->shortcutId() == m_id && se->key() == m_shortcut){ if (se->isAmbiguous()) emit activatedAmbiguously(); else emit activated(); return true; } } return false; } static bool qQuickShortcutContextMatcher(QObject *obj, Qt::ShortcutContext context) { switch (context) { case Qt::ApplicationShortcut: return true; case Qt::WindowShortcut: while (obj && !obj->isWindowType()) { obj = obj->parent(); if (QQuickItem *item = qobject_cast(obj)) obj = item->window(); } return obj && obj == QGuiApplication::focusWindow(); default: return false; } } void QQuickShortcut::grabShortcut(const QKeySequence &sequence, Qt::ShortcutContext context) { ungrabShortcut(); if (m_completed && !sequence.isEmpty()) { QGuiApplicationPrivate *pApp = QGuiApplicationPrivate::instance(); m_id = pApp->shortcutMap.addShortcut(this, sequence, context, qQuickShortcutContextMatcher); if (!m_enabled) pApp->shortcutMap.setShortcutEnabled(false, m_id, this); if (!m_autorepeat) pApp->shortcutMap.setShortcutAutoRepeat(false, m_id, this); } } void QQuickShortcut::ungrabShortcut() { if (m_id) { QGuiApplicationPrivate::instance()->shortcutMap.removeShortcut(m_id, this); m_id = 0; } } QT_END_NAMESPACE ./src/app/ThinProgressBar.qml0000644000004100000410000000164113004613604016400 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 ProgressBar { property var webview height: units.dp(3) showProgressPercentage: false value: webview ? webview.loadProgress / 100 : 0.0 visible: webview ? webview.loading : false } ./src/app/webcontainer/0000755000004100000410000000000013004613623015270 5ustar www-datawww-data./src/app/webcontainer/WebappWebview.qml0000644000004100000410000002113713004613604020555 0ustar www-datawww-data/* * Copyright 2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import com.canonical.Oxide 1.8 as Oxide import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import Ubuntu.Web 0.2 import "../actions" as Actions import ".." WebViewImpl { id: webappWebview property bool wide: false signal openUrlExternallyRequested(string url) filePicker: filePickerLoader.item property QtObject contextModel: null contextualActions: ActionList { Actions.OpenLinkInWebBrowser { objectName: "OpenLinkInWebBrowser" enabled: contextModel && contextModel.linkUrl.toString() onTriggered: openUrlExternallyRequested(contextModel.linkUrl) } Actions.CopyLink { enabled: contextModel && contextModel.linkUrl.toString() onTriggered: Clipboard.push(["text/plain", contextModel.linkUrl.toString()]) objectName: "CopyLinkContextualAction" } Actions.SaveLink { enabled: contextModel && contextModel.linkUrl.toString() onTriggered: contextModel.saveLink() objectName: "SaveLinkContextualAction" } Actions.Share { objectName: "ShareContextualAction" enabled: (contentHandlerLoader.status == Loader.Ready) && contextModel && (contextModel.linkUrl.toString() || contextModel.selectionText) onTriggered: { if (contextModel.linkUrl.toString()) { internal.shareLink(contextModel.linkUrl.toString(), contextModel.linkText) } else if (contextModel.selectionText) { internal.shareText(contextModel.selectionText) } } } Actions.CopyImage { enabled: contextModel && (contextModel.mediaType === Oxide.WebView.MediaTypeImage) && contextModel.srcUrl.toString() onTriggered: Clipboard.push(["text/plain", contextModel.srcUrl.toString()]) objectName: "CopyImageContextualAction" } Actions.SaveImage { enabled: contextModel && ((contextModel.mediaType === Oxide.WebView.MediaTypeImage) || (contextModel.mediaType === Oxide.WebView.MediaTypeCanvas)) && contextModel.hasImageContents onTriggered: contextModel.saveMedia() objectName: "SaveImageContextualAction" } Actions.Undo { enabled: contextModel && contextModel.isEditable && (contextModel.editFlags & Oxide.WebView.UndoCapability) onTriggered: executeEditingCommand(Oxide.WebView.EditingCommandUndo) objectName: "UndoContextualAction" } Actions.Redo { enabled: contextModel && contextModel.isEditable && (contextModel.editFlags & Oxide.WebView.RedoCapability) onTriggered: executeEditingCommand(Oxide.WebView.EditingCommandRedo) objectName: "RedoContextualAction" } Actions.Cut { enabled: contextModel && contextModel.isEditable && (contextModel.editFlags & Oxide.WebView.CutCapability) onTriggered: executeEditingCommand(Oxide.WebView.EditingCommandCut) objectName: "CutContextualAction" } Actions.Copy { enabled: contextModel && contextModel.isEditable && (contextModel.editFlags & Oxide.WebView.CopyCapability) onTriggered: executeEditingCommand(Oxide.WebView.EditingCommandCopy) objectName: "CopyContextualAction" } Actions.Paste { enabled: contextModel && contextModel.isEditable && (contextModel.editFlags & Oxide.WebView.PasteCapability) onTriggered: executeEditingCommand(Oxide.WebView.EditingCommandPaste) objectName: "PasteContextualAction" } Actions.Erase { enabled: contextModel && contextModel.isEditable && (contextModel.editFlags & Oxide.WebView.EraseCapability) onTriggered: executeEditingCommand(Oxide.WebView.EditingCommandErase) objectName: "EraseContextualAction" } Actions.SelectAll { enabled: contextModel && contextModel.isEditable && (contextModel.editFlags & Oxide.WebView.SelectAllCapability) onTriggered: executeEditingCommand(Oxide.WebView.EditingCommandSelectAll) objectName: "SelectAllContextualAction" } } function contextMenuOnCompleted(menu) { if (!menu || !menu.contextModel) { return } contextModel = menu.contextModel var isImageMediaType = ((contextModel.mediaType === Oxide.WebView.MediaTypeImage) || (contextModel.mediaType === Oxide.WebView.MediaTypeCanvas)) && contextModel.hasImageContents; if (contextModel.linkUrl.toString() || contextModel.srcUrl.toString() || contextModel.selectionText || (contextModel.isEditable && contextModel.editFlags) || isImageMediaType) { menu.show() } else { contextModel.close() } } Component { id: contextMenuNarrowComponent ContextMenuMobile { actions: contextualActions Component.onCompleted: webappWebview.contextMenuOnCompleted(this) } } Component { id: contextMenuWideComponent ContextMenuWide { associatedWebview: webappWebview parent: webappWebview actions: contextualActions Component.onCompleted: webappWebview.contextMenuOnCompleted(this) } } contextMenu: webappWebview.wide ? contextMenuWideComponent : contextMenuNarrowComponent onGeolocationPermissionRequested: { if (__runningConfined && (request.origin == request.embedder)) { // When running confined, querying the location service will trigger // a system prompt (trust store), so no need for a custom one. request.allow() } else { requestGeolocationPermission(request) } } Loader { id: contentHandlerLoader source: "../ContentHandler.qml" asynchronous: true } QtObject { id: internal function instantiateShareComponent() { var component = Qt.createComponent("../Share.qml") if (component.status === Component.Ready) { var share = component.createObject(webappWebview) share.onDone.connect(share.destroy) return share } return null } function shareLink(url, title) { var share = instantiateShareComponent() if (share) share.shareLink(url, title) } function shareText(text) { var share = instantiateShareComponent() if (share) share.shareText(text) } } onShowDownloadDialog: { if (downloadDialogLoader.status === Loader.Ready) { var downloadDialog = PopupUtils.open(downloadDialogLoader.item, webappWebview, {"contentType" : contentType, "downloadId" : downloadId, "singleDownload" : downloader, "filename" : filename, "mimeType" : mimeType}) downloadDialog.startDownload.connect(startDownload) } } Loader { id: downloadDialogLoader source: "ContentDownloadDialog.qml" asynchronous: true } Loader { id: filePickerLoader source: "ContentPickerDialog.qml" asynchronous: true } } ./src/app/webcontainer/CMakeLists.txt0000644000004100000410000000234513004613604020033 0ustar www-datawww-dataproject(webapp-container) find_package(Qt5DBus REQUIRED) find_package(Qt5Sql REQUIRED) include_directories( ${CMAKE_BINARY_DIR} ${webbrowser-common_SOURCE_DIR} ${webbrowser-common_BINARY_DIR} ) set(WEBAPP_CONTAINER webapp-container) set(WEBAPP_CONTAINER_SRC chrome-cookie-store.cpp cookie-store.cpp online-accounts-cookie-store.cpp oxide-cookie-helper.cpp local-cookie-store.cpp webapp-container.cpp webapp-container-helper.cpp session-utils.cpp url-pattern-utils.cpp scheme-filter.cpp intent-parser.cpp ) add_executable(${WEBAPP_CONTAINER} ${WEBAPP_CONTAINER_SRC}) target_link_libraries(${WEBAPP_CONTAINER} Qt5::Core Qt5::DBus Qt5::Gui Qt5::Qml Qt5::Quick Qt5::Sql ${COMMONLIB} ) install(TARGETS ${WEBAPP_CONTAINER} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) file(GLOB QML_FILES *.qml *.js) install(FILES ${QML_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/webbrowser-app/webcontainer) install(DIRECTORY actions DESTINATION ${CMAKE_INSTALL_DATADIR}/webbrowser-app/webcontainer FILES_MATCHING PATTERN *.qml) install(DIRECTORY assets DESTINATION ${CMAKE_INSTALL_DATADIR}/webbrowser-app/webcontainer FILES_MATCHING PATTERN *.png) ./src/app/webcontainer/chrome-cookie-store.h0000644000004100000410000000356213004613604021324 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 CHROME_COOKIE_STORE_H #define CHROME_COOKIE_STORE_H #include "cookie-store.h" #include #include class OxideCookieHelper; class ChromeCookieStore : public CookieStore { Q_OBJECT Q_PROPERTY(QString dbPath READ dbPath WRITE setDbPath NOTIFY dbPathChanged) Q_PROPERTY(QObject* oxideStoreBackend READ oxideStoreBackend WRITE setOxideStoreBackend NOTIFY oxideStoreBackendChanged) public: ChromeCookieStore(QObject* parent = 0); // dbpaths void setDbPath(const QString& path); QString dbPath() const; // oxideStoreBackend void setOxideStoreBackend(QObject* backend); QObject* oxideStoreBackend() const; // CookieStore overrides QDateTime lastUpdateTimeStamp() const Q_DECL_OVERRIDE; Q_SIGNALS: void dbPathChanged(); void oxideStoreBackendChanged(); private Q_SLOTS: void oxideCookiesReceived(int requestId, const QVariant& cookies); void oxideCookiesUpdated(const QList& failedCookies); private: virtual void doGetCookies() Q_DECL_OVERRIDE; virtual void doSetCookies(const Cookies& cookies) Q_DECL_OVERRIDE; private: OxideCookieHelper* m_cookieHelper; QString m_dbPath; }; #endif // CHROME_COOKIE_STORE_H ./src/app/webcontainer/local-cookie-store.cpp0000644000004100000410000001561713004613604021500 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "local-cookie-store.h" #include #include #include #include #include #include static int connectionCounter = 0; static qint64 dateTimeToChrome(const QDateTime &time) { /* Chrome uses Mon Jan 01 00:00:00 UTC 1601 as the epoch, hence the * magic number */ return (time.toMSecsSinceEpoch() + 11644473600000) * 1000; } static QDateTime dateTimeFromChrome(qint64 chromeTimeStamp) { qint64 msecsSinceEpoch = chromeTimeStamp / 1000 - 11644473600000; return QDateTime::fromMSecsSinceEpoch(msecsSinceEpoch); } LocalCookieStore::LocalCookieStore(QObject* parent): CookieStore(parent) { QString connectionName = QString("LocalCookieStore-%1").arg(connectionCounter++); m_db = QSqlDatabase::addDatabase("QSQLITE", connectionName); } void LocalCookieStore::doGetCookies() { Cookies cookies; m_db.setDatabaseName(m_dbPath); if (Q_UNLIKELY(!m_db.open())) { qCritical() << "Could not open cookie database:" << m_dbPath << m_db.lastError(); return; } QSqlQuery q(m_db); q.exec("SELECT host_key, name, value, path, expires_utc, secure, httponly, has_expires FROM cookies;"); while (q.next()) { /* Build the cookie string from its parts */ QNetworkCookie cookie(q.value(1).toString().toUtf8(), q.value(2).toString().toUtf8()); cookie.setSecure(q.value(5).toBool()); cookie.setHttpOnly(q.value(6).toBool()); if (q.value(7).toBool()) { QDateTime expires = dateTimeFromChrome(q.value(4).toULongLong()); cookie.setExpirationDate(expires); } cookie.setDomain(q.value(0).toString()); cookie.setPath(q.value(3).toString()); cookies.append(cookie); } m_db.close(); emit gotCookies(cookies); } QDateTime LocalCookieStore::lastUpdateTimeStamp() const { QFileInfo dbFileInfo(m_dbPath); return dbFileInfo.lastModified(); } bool LocalCookieStore::createDb() { if (Q_UNLIKELY(!m_db.transaction())) return false; QSqlQuery q(m_db); bool ok; ok = q.exec("CREATE TABLE meta(key LONGVARCHAR NOT NULL UNIQUE PRIMARY KEY," "value LONGVARCHAR)"); if (Q_UNLIKELY(!ok)) { m_db.rollback(); return false; } ok = q.exec("CREATE TABLE cookies (creation_utc INTEGER NOT NULL UNIQUE PRIMARY KEY," "host_key TEXT NOT NULL," "name TEXT NOT NULL," "value TEXT NOT NULL," "path TEXT NOT NULL," "expires_utc INTEGER NOT NULL," "secure INTEGER NOT NULL," "httponly INTEGER NOT NULL," "last_access_utc INTEGER NOT NULL," "has_expires INTEGER NOT NULL DEFAULT 1," "persistent INTEGER NOT NULL DEFAULT 1," "priority INTEGER NOT NULL DEFAULT 1," "encrypted_value BLOB DEFAULT '')"); if (Q_UNLIKELY(!ok)) { m_db.rollback(); return false; } ok = q.exec("CREATE INDEX domain ON cookies(host_key)"); if (Q_UNLIKELY(!ok)) { m_db.rollback(); return false; } ok = q.exec("INSERT INTO meta (key, value) VALUES ('version', '7')"); if (Q_UNLIKELY(!ok)) { m_db.rollback(); return false; } ok = q.exec("INSERT INTO meta (key, value) VALUES ('last_compatible_version', '5')"); if (Q_UNLIKELY(!ok)) { m_db.rollback(); return false; } return m_db.commit(); } void LocalCookieStore::doSetCookies(const Cookies& parsedCookies) { m_db.setDatabaseName(m_dbPath); if (!m_db.open()) { qCritical() << "Could not open cookie database:" << m_dbPath << m_db.lastError().text(); return; } QSqlQuery q(m_db); // Check whether the table already exists q.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='cookies'"); if (!q.next() && !createDb()) { qCritical() << "Could not create cookie database:" << m_dbPath << m_db.lastError().text(); return; } q.prepare("INSERT INTO cookies (creation_utc," "host_key, name, value, path," "expires_utc, secure, httponly, last_access_utc," "has_expires, persistent, priority, encrypted_value) " "VALUES (:creation_utc," ":host_key, :name, :value, :path," ":expires_utc, :secure, :httponly, :last_access_utc," ":has_expires, :persistent, :priority, :encrypted_value)"); qint64 lastTimestamp = 0; Q_FOREACH(const QNetworkCookie &cookie, parsedCookies) { quint64 timestamp = dateTimeToChrome(QDateTime::currentDateTimeUtc()); /* Make sure that every cicle iteration marks a different timestamp */ if (timestamp <= lastTimestamp) timestamp = lastTimestamp + 1; q.bindValue(":creation_utc", timestamp); q.bindValue(":host_key", cookie.domain()); q.bindValue(":name", cookie.name()); q.bindValue(":value", cookie.value()); q.bindValue(":path", cookie.path()); q.bindValue(":expires_utc", dateTimeToChrome(cookie.expirationDate().toUTC())); q.bindValue(":secure", cookie.isSecure()); q.bindValue(":httponly", cookie.isHttpOnly()); q.bindValue(":last_access_utc", timestamp); q.bindValue(":has_expires", cookie.expirationDate().isValid()); q.bindValue(":persistent", true); q.bindValue(":priority", 1); q.bindValue(":encrypted_value", 1); q.exec(); lastTimestamp = timestamp; } m_db.close(); emit cookiesSet(true); } void LocalCookieStore::setDbPath(const QString &path) { // If path is a URL, strip the initial "file://" QString normalizedPath = path.startsWith("file://") ? path.mid(7) : path; if (normalizedPath != m_dbPath) { if (Q_UNLIKELY(!normalizedPath.startsWith('/'))) { qWarning() << "Invalid database path (must be absolute):" << path; return; } m_dbPath = normalizedPath; qDebug() << "Using local cookie db: " << m_dbPath; Q_EMIT dbPathChanged(); } } QString LocalCookieStore::dbPath () const { return m_dbPath; } ./src/app/webcontainer/online-accounts-cookie-store.cpp0000644000004100000410000000773313004613604023507 0ustar www-datawww-data/* * Copyright 2013-2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "online-accounts-cookie-store.h" #include #include #include #include #include #include #if defined(ONLINE_ACCOUNTS_COOKIE_STORE_OBJECT) # error ONLINE_ACCOUNTS_COOKIE_STORE_OBJECT already defined #else # define ONLINE_ACCOUNTS_COOKIE_STORE_OBJECT "com.nokia.singlesignonui" #endif #if defined(ONLINE_ACCOUNTS_COOKIE_STORE_PATH) # error ONLINE_ACCOUNTS_COOKIE_STORE_PATH already defined #else # define ONLINE_ACCOUNTS_COOKIE_STORE_PATH "/SignonUi" #endif #if defined(ONLINE_ACCOUNTS_COOKIE_STORE_METHOD) # error ONLINE_ACCOUNTS_COOKIE_STORE_METHOD already defined #else # define ONLINE_ACCOUNTS_COOKIE_STORE_METHOD "cookiesForIdentity" #endif class OnlineAccountsCookieStorePrivate { public: OnlineAccountsCookieStorePrivate(): _id(0), m_connection(QDBusConnection::sessionBus()) {} quint32 _id; QDBusConnection m_connection; }; OnlineAccountsCookieStore::OnlineAccountsCookieStore(QObject *parent) : CookieStore(parent), d_ptr(new OnlineAccountsCookieStorePrivate()) { qDBusRegisterMetaType(); } OnlineAccountsCookieStore::~OnlineAccountsCookieStore() { delete d_ptr; } quint32 OnlineAccountsCookieStore::accountId () const { Q_D(const OnlineAccountsCookieStore); return d->_id; } void OnlineAccountsCookieStore::setAccountId (quint32 id) { Q_D(OnlineAccountsCookieStore); if (accountId() != id) { d->_id = id; Q_EMIT accountIdChanged(); } } void OnlineAccountsCookieStore::doGetCookies() { Q_D(const OnlineAccountsCookieStore); QDBusMessage message = QDBusMessage::createMethodCall(ONLINE_ACCOUNTS_COOKIE_STORE_OBJECT, ONLINE_ACCOUNTS_COOKIE_STORE_PATH, ONLINE_ACCOUNTS_COOKIE_STORE_OBJECT, ONLINE_ACCOUNTS_COOKIE_STORE_METHOD); message.setArguments(QVariantList() << accountId()); QDBusMessage reply = d->m_connection.call(message); if (reply.type() == QDBusMessage::ErrorMessage) { qWarning() << "Got error:" << reply.errorMessage(); emit gotCookies(Cookies()); } QList arguments = reply.arguments(); if ( ! arguments.count()) { qWarning() << "Invalid number arguments to get online accounts cookies call."; emit gotCookies(Cookies()); } if (arguments.count() > 1) { qint64 timeStamp = arguments.at(1).toLongLong(); if (timeStamp != 0) { qDebug() << "Got a cookie timestamp of" << timeStamp << "from Online Accounts DBUS cookiesForIdentity() call."; QDateTime t = QDateTime::fromMSecsSinceEpoch(timeStamp * 1000); updateLastUpdateTimestamp(t); } } emit gotCookies(fromDbusCookies(qdbus_cast(arguments.front()))); } Cookies OnlineAccountsCookieStore::fromDbusCookies(const OnlineAccountsCookies& cookies) { Cookies parsedCookies; Q_FOREACH(const QByteArray &cookie, cookies) { parsedCookies.append(QNetworkCookie::parseCookies(cookie)); } return parsedCookies; } void OnlineAccountsCookieStore::doSetCookies(const Cookies& cookies) { Q_UNUSED(cookies); } ./src/app/webcontainer/local-cookie-store.h0000644000004100000410000000255113004613604021136 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 LOCAL_COOKIE_STORE_H #define LOCAL_COOKIE_STORE_H #include "cookie-store.h" #include class LocalCookieStore : public CookieStore { Q_OBJECT Q_PROPERTY(QString dbPath READ dbPath WRITE setDbPath NOTIFY dbPathChanged) public: LocalCookieStore(QObject* parent = 0); void setDbPath(const QString& path); QString dbPath() const; QDateTime lastUpdateTimeStamp() const Q_DECL_OVERRIDE; Q_SIGNALS: void dbPathChanged(); private: virtual void doGetCookies() Q_DECL_OVERRIDE; virtual void doSetCookies(const Cookies& cookies) Q_DECL_OVERRIDE; bool createDb(); private: QString m_dbPath; QSqlDatabase m_db; }; #endif // LOCAL_COOKIE_STORE_H ./src/app/webcontainer/ContextMenuWide.qml0000644000004100000410000001110313004613604021060 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 as ListItems import Ubuntu.Components.Popups 1.3 as Popups import com.canonical.Oxide 1.8 as Oxide Popups.Popover { id: contextMenu objectName: "contextMenuWide" property QtObject contextModel: model property ActionList actions: null property var associatedWebview: null QtObject { id: internal readonly property int lastEnabledActionIndex: { var last = -1 for (var i in actions.actions) { if (actions.actions[i].enabled) { last = i } } return last } readonly property real locationBarOffset: contextMenu.associatedWebview.locationBarController.height + contextMenu.associatedWebview.locationBarController.offset } Rectangle { anchors.fill: parent color: "#ececec" } Column { anchors { left: parent.left right: parent.right } Label { id: titleLabel objectName: "titleLabel" text: contextModel.srcUrl.toString() ? contextModel.srcUrl : contextModel.linkUrl anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) } height: units.gu(5) visible: text fontSize: "x-small" color: "#888888" elide: Text.ElideRight verticalAlignment: Text.AlignVCenter } ListItems.ThinDivider { anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) } visible: titleLabel.visible } Repeater { model: actions.actions delegate: ListItems.Empty { action: actions.actions[index] objectName: action.objectName + "_item" visible: action.enabled showDivider: false height: units.gu(5) Label { anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) verticalCenter: parent.verticalCenter } fontSize: "small" text: action.text } ListItems.ThinDivider { visible: index < internal.lastEnabledActionIndex anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) bottom: parent.bottom } } onTriggered: contextMenu.hide() } } } Item { id: positioner visible: false parent: contextMenu.associatedWebview x: contextModel.position.x y: contextModel.position.y + internal.locationBarOffset } caller: positioner onVisibleChanged: { if (!visible) { contextModel.close() } } // Override default implementation to prevent context menu from stealing // active focus when shown (https://launchpad.net/bugs/1526884). function show() { visible = true __foreground.show() } Binding { // Ensure the context menu doesn’t steal focus from // the webview when one of its actions is activated // (https://launchpad.net/bugs/1526884). target: __foreground property: "activeFocusOnPress" value: false } } ./src/app/webcontainer/AccountItem.qml0000644000004100000410000000164113004613604020217 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 as ListItem ListItem.Subtitled { id: root property string providerName property string accountName text: providerName subText: accountName } ./src/app/webcontainer/chrome-cookie-store.cpp0000644000004100000410000000604713004613604021660 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "chrome-cookie-store.h" #include "oxide-cookie-helper.h" #include #include #include ChromeCookieStore::ChromeCookieStore(QObject* parent): CookieStore(parent), m_cookieHelper(new OxideCookieHelper(this)) { QObject::connect(m_cookieHelper, SIGNAL(oxideStoreBackendChanged()), this, SIGNAL(oxideStoreBackendChanged())); QObject::connect(m_cookieHelper, SIGNAL(cookiesSet(const QList&)), this, SLOT(oxideCookiesUpdated(const QList&))); } void ChromeCookieStore::setOxideStoreBackend(QObject* backend) { m_cookieHelper->setOxideStoreBackend(backend); } QObject* ChromeCookieStore::oxideStoreBackend() const { return m_cookieHelper->oxideStoreBackend(); } void ChromeCookieStore::oxideCookiesReceived(int requestId, const QVariant& cookies) { Q_UNUSED(requestId); emit gotCookies(OxideCookieHelper::cookiesFromVariant(cookies)); } void ChromeCookieStore::oxideCookiesUpdated(const QList& failedCookies) { if (!failedCookies.isEmpty()) { qWarning() << "Couldn't set some cookies:" << failedCookies; } emit cookiesSet(failedCookies.isEmpty()); } void ChromeCookieStore::doGetCookies() { QObject* backend = m_cookieHelper->oxideStoreBackend(); if ( ! backend) return; QObject::connect(backend, SIGNAL(getCookiesResponse(int, const QVariant&)), this, SLOT(oxideCookiesReceived(int, const QVariant&))); QMetaObject::invokeMethod(backend, "getAllCookies", Qt::DirectConnection); } QDateTime ChromeCookieStore::lastUpdateTimeStamp() const { QFileInfo dbFileInfo(m_dbPath); return dbFileInfo.lastModified(); } void ChromeCookieStore::doSetCookies(const Cookies& cookies) { m_cookieHelper->setCookies(cookies); } void ChromeCookieStore::setDbPath(const QString &path) { // If path is a URL, strip the initial "file://" QString normalizedPath = path.startsWith("file://") ? path.mid(7) : path; if (normalizedPath != m_dbPath) { if (Q_UNLIKELY(!normalizedPath.startsWith('/'))) { qWarning() << "Invalid database path (must be absolute):" << path; return; } m_dbPath = normalizedPath; Q_EMIT dbPathChanged(); } } QString ChromeCookieStore::dbPath () const { return m_dbPath; } ./src/app/webcontainer/url-pattern-utils.h0000644000004100000410000000255213004613604021057 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 URLPATTERNUTILS_H #define URLPATTERNUTILS_H #include #include class QUrl; namespace UrlPatternUtils { /** * @brief transformWebappSearchPatternToSafePattern * @param doTransformUrlPath * @return */ QString transformWebappSearchPatternToSafePattern(const QString& , bool doTransformUrlPath = true); /** * @brief isLocalHtml5ApplicationHomeUrl * @return */ bool isLocalHtml5ApplicationHomeUrl(const QUrl&); /** * @brief filterAndTransformUrlPatterns * @param includePatterns * @return */ QStringList filterAndTransformUrlPatterns(const QStringList & includePatterns); } #endif // URLPATTERNUTILS_H ./src/app/webcontainer/Chrome.qml0000644000004100000410000000750513004613604017226 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import ".." ChromeBase { id: chrome property bool navigationButtonsVisible: false property bool accountSwitcher: false property alias chromeTextLabelColor: chromeTextLabel.color signal chooseAccount() FocusScope { anchors { fill: parent margins: units.gu(1) } focus: true ChromeButton { id: backButton objectName: "backButton" iconName: "previous" iconSize: 0.6 * height height: parent.height visible: chrome.navigationButtonsVisible width: visible ? height : 0 anchors { left: parent.left verticalCenter: parent.verticalCenter } enabled: chrome.webview ? chrome.webview.canGoBack : false onTriggered: chrome.webview.goBack() } ChromeButton { id: forwardButton objectName: "forwardButton" iconName: "next" iconSize: 0.6 * height height: parent.height visible: chrome.navigationButtonsVisible && enabled width: visible ? height : 0 anchors { left: backButton.right verticalCenter: parent.verticalCenter } enabled: chrome.webview ? chrome.webview.canGoForward : false onTriggered: chrome.webview.goForward() } Item { id: faviconContainer height: parent.height width: height anchors.left: forwardButton.right Favicon { anchors.centerIn: parent source: chrome.webview ? chrome.webview.icon : null } } Label { id: chromeTextLabel anchors { left: faviconContainer.right right: reloadButton.left rightMargin: units.gu(1) verticalCenter: parent.verticalCenter } text: chrome.webview.title ? chrome.webview.title : chrome.webview.url elide: Text.ElideRight } ChromeButton { id: reloadButton objectName: "reloadButton" iconName: "reload" iconSize: 0.6 * height height: parent.height visible: chrome.navigationButtonsVisible width: visible ? height : 0 anchors { right: accountsButton.left verticalCenter: parent.verticalCenter } enabled: chrome.webview.url && chrome.webview.url !== "" onTriggered: chrome.webview.reload() } ChromeButton { id: accountsButton objectName: "accountsButton" iconName: "contact" iconSize: 0.6 * height height: parent.height width: visible ? height : 0 anchors { right: parent.right verticalCenter: parent.verticalCenter } visible: accountSwitcher onTriggered: chrome.chooseAccount() } } } ./src/app/webcontainer/WebappContainerWebview.qml0000644000004100000410000001166113004613623022422 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Unity.Action 1.1 as UnityActions import Ubuntu.UnityWebApps 0.1 as UnityWebApps import "../actions" as Actions import ".." Item { id: containerWebview property string url: "" property bool developerExtrasEnabled: false property string webappName: "" property url dataPath property var currentWebview: webappContainerWebViewLoader.item ? webappContainerWebViewLoader.item.currentWebview : null property var webappUrlPatterns property string localUserAgentOverride: "" property string popupRedirectionUrlPrefixPattern: "" property url webviewOverrideFile: "" property bool blockOpenExternalUrls: false property bool runningLocalApplication: false property bool wide: false property bool openExternalUrlInOverlay: false property bool popupBlockerEnabled: true signal samlRequestUrlPatternReceived(string urlPattern) signal themeColorMetaInformationDetected(string theme_color) onWideChanged: { if (webappContainerWebViewLoader.item && webappContainerWebViewLoader.item.wide !== undefined) { webappContainerWebViewLoader.item.wide = wide } } Component { id: mediaAccessDialogComponent MediaAccessDialog { objectName: "mediaAccessDialog" } } PopupWindowController { id: popupController objectName: "popupController" webappUrlPatterns: containerWebview.webappUrlPatterns mainWebappView: containerWebview.currentWebview blockOpenExternalUrls: containerWebview.blockOpenExternalUrls mediaAccessDialogComponent: mediaAccessDialogComponent wide: containerWebview.wide onInitializeOverlayViewsWithUrls: { if (webappContainerWebViewLoader.item) { for (var i in urls) { webappContainerWebViewLoader .item .openOverlayForUrl(urls[i]) } } } } Connections { target: webappContainerWebViewLoader.item onSamlRequestUrlPatternReceived: { samlRequestUrlPatternReceived(urlPattern) } } Connections { target: webappContainerWebViewLoader.item onThemeColorMetaInformationDetected: { themeColorMetaInformationDetected(theme_color) } } Loader { id: webappContainerWebViewLoader objectName: "containerWebviewLoader" anchors.fill: parent } onUrlChanged: if (webappContainerWebViewLoader.item) webappContainerWebViewLoader.item.url = url Component.onCompleted: { var webappEngineSource = Qt.resolvedUrl("WebViewImplOxide.qml"); // This is an experimental, UNSUPPORTED, API // It loads an alternative webview, adjusted for a specific webapp if (webviewOverrideFile.toString()) { console.log("Loading custom webview from " + webviewOverrideFile); webappEngineSource = webviewOverrideFile; } webappContainerWebViewLoader.setSource( webappEngineSource, { localUserAgentOverride: containerWebview.localUserAgentOverride , url: containerWebview.url , webappName: containerWebview.webappName , dataPath: dataPath , webappUrlPatterns: containerWebview.webappUrlPatterns , developerExtrasEnabled: containerWebview.developerExtrasEnabled , popupRedirectionUrlPrefixPattern: containerWebview.popupRedirectionUrlPrefixPattern , blockOpenExternalUrls: containerWebview.blockOpenExternalUrls , runningLocalApplication: containerWebview.runningLocalApplication , popupController: popupController , overlayViewsParent: containerWebview.parent , wide: containerWebview.wide , mediaAccessDialogComponent: mediaAccessDialogComponent , openExternalUrlInOverlay: containerWebview.openExternalUrlInOverlay , popupBlockerEnabled: containerWebview.popupBlockerEnabled}) } } ./src/app/webcontainer/WebApp.qml0000644000004100000410000002405413004613623017166 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import com.canonical.Oxide 1.5 as Oxide import Ubuntu.Components 1.3 import Ubuntu.Unity.Action 1.1 as UnityActions import Ubuntu.UnityWebApps 0.1 as UnityWebApps import Qt.labs.settings 1.0 import "../actions" as Actions import ".." import "ColorUtils.js" as ColorUtils BrowserView { id: webapp objectName: "webappBrowserView" currentWebview: containerWebView.currentWebview property alias url: containerWebView.url property bool accountSwitcher property string webappModelSearchPath: "" property var webappUrlPatterns property alias popupRedirectionUrlPrefixPattern: containerWebView.popupRedirectionUrlPrefixPattern property alias webviewOverrideFile: containerWebView.webviewOverrideFile property alias blockOpenExternalUrls: containerWebView.blockOpenExternalUrls property alias localUserAgentOverride: containerWebView.localUserAgentOverride property alias dataPath: containerWebView.dataPath property alias runningLocalApplication: containerWebView.runningLocalApplication property alias openExternalUrlInOverlay: containerWebView.openExternalUrlInOverlay property alias popupBlockerEnabled: containerWebView.popupBlockerEnabled property string webappName: "" property bool backForwardButtonsVisible: false property bool chromeVisible: false readonly property bool chromeless: !chromeVisible && !backForwardButtonsVisible && !accountSwitcher readonly property real themeColorTextContrastFactor: 3.0 signal chooseAccount() // Used for testing. There is a bug that currently prevents non visual Qt objects // to be introspectable from AP which makes directly accessing the settings object // not possible https://bugs.launchpad.net/autopilot-qt/+bug/1273956 property alias generatedUrlPatterns: urlPatternSettings.generatedUrlPatterns actions: [ Actions.Back { enabled: webapp.backForwardButtonsVisible && containerWebView.currentWebview && containerWebView.currentWebview.canGoBack onTriggered: containerWebView.currentWebview.goBack() }, Actions.Forward { enabled: webapp.backForwardButtonsVisible && containerWebView.currentWebview && containerWebView.currentWebview.canGoForward onTriggered: containerWebView.currentWebview.goForward() }, Actions.Reload { onTriggered: containerWebView.currentWebview.reload() } ] Settings { id: urlPatternSettings property string generatedUrlPatterns } function addGeneratedUrlPattern(urlPattern) { if (urlPattern.trim().length === 0) { return; } var patterns = [] if (urlPatternSettings.generatedUrlPatterns && urlPatternSettings.generatedUrlPatterns.trim().length !== 0) { try { patterns = JSON.parse(urlPatternSettings.generatedUrlPatterns) } catch(e) { console.error("Invalid JSON content found in url patterns file") } if (! (patterns instanceof Array)) { console.error("Invalid JSON content type found in url patterns file (not an array)") patterns = [] } } if (patterns.indexOf(urlPattern) < 0) { patterns.push(urlPattern) urlPatternSettings.generatedUrlPatterns = JSON.stringify(patterns) } } function mergeUrlPatternSets(p1, p2) { if ( ! (p1 instanceof Array)) { return (p2 instanceof Array) ? p2 : [] } if ( ! (p2 instanceof Array)) { return (p1 instanceof Array) ? p1 : [] } var p1hash = {} var result = [] for (var i1 in p1) { p1hash[p1[i1]] = 1 result.push(p1[i1]) } for (var i2 in p2) { if (! (p2[i2] in p1hash)) { result.push(p2[i2]) } } return result } Item { id: webviewContainer anchors.fill: parent WebappContainerWebview { id: containerWebView objectName: "webview" wide: webapp.wide anchors { left: parent.left right: parent.right top: parent.top } height: parent.height - osk.height developerExtrasEnabled: webapp.developerExtrasEnabled onThemeColorMetaInformationDetected: { var color = webappContainerHelper.rgbColorFromCSSColor(theme_color) if (!webapp.chromeless && chromeLoader.item && color.length) { chromeLoader.item.backgroundColor = theme_color chromeLoader.item.chromeTextLabelColor = ColorUtils.getMostConstrastedColor( color, Qt.darker(theme_color, themeColorTextContrastFactor), Qt.lighter(theme_color, themeColorTextContrastFactor)) } } onSamlRequestUrlPatternReceived: { addGeneratedUrlPattern(urlPattern) } webappUrlPatterns: mergeUrlPatternSets(urlPatternSettings.generatedUrlPatterns, webapp.webappUrlPatterns) /** * Use the --webapp parameter value w/ precedence, but also take into account * the fact that a webapp 'name' can come from a webapp-properties.json file w/o * being explictly defined here. */ webappName: webapp.webappName === "" ? unityWebapps.name : webapp.webappName Loader { anchors { fill: containerWebView topMargin: (!webapp.chromeless && chromeLoader.item.state == "shown") ? chromeLoader.item.height : 0 } active: containerWebView.currentWebview && (webProcessMonitor.crashed || (webProcessMonitor.killed && !containerWebView.currentWebview.loading)) sourceComponent: SadPage { webview: containerWebView.currentWebview objectName: "mainWebviewSadPage" } WebProcessMonitor { id: webProcessMonitor webview: containerWebView.currentWebview } asynchronous: true } } Loader { anchors { fill: containerWebView topMargin: (!webapp.chromeless && chromeLoader.item.state == "shown") ? chromeLoader.item.height : 0 } sourceComponent: ErrorSheet { visible: containerWebView.currentWebview && containerWebView.currentWebview.lastLoadFailed url: containerWebView.currentWebview ? containerWebView.currentWebview.url : "" onRefreshClicked: { if (containerWebView.currentWebview) containerWebView.currentWebview.reload() } } asynchronous: true } Loader { id: chromeLoader anchors { top: parent.top left: parent.left right: parent.right } sourceComponent: webapp.chromeless ? progressbarComponent : chromeComponent Component { id: chromeComponent Chrome { webview: webapp.currentWebview navigationButtonsVisible: webapp.backForwardButtonsVisible accountSwitcher: webapp.accountSwitcher anchors { left: parent.left right: parent.right } height: units.gu(6) y: webapp.currentWebview ? containerWebView.currentWebview.locationBarController.offset : 0 onChooseAccount: webapp.chooseAccount() } } Component { id: progressbarComponent ThinProgressBar { webview: webapp.currentWebview anchors { left: parent.left right: parent.right top: parent.top } } } } Binding { when: webapp.currentWebview && !webapp.chromeless target: webapp.currentWebview ? webapp.currentWebview.locationBarController : null property: 'height' value: webapp.currentWebview.visible ? chromeLoader.item.height : 0 } ChromeController { webview: webapp.currentWebview forceHide: webapp.chromeless defaultMode: webapp.hasTouchScreen ? Oxide.LocationBarController.ModeAuto : Oxide.LocationBarController.ModeShown } } UnityWebApps.UnityWebApps { id: unityWebapps name: webappName bindee: containerWebView.currentWebview actionsContext: actionManager.globalContext model: UnityWebApps.UnityWebappsAppModel { searchPath: webappModelSearchPath } injectExtraUbuntuApis: runningLocalApplication injectExtraContentShareCapabilities: !runningLocalApplication } } ./src/app/webcontainer/webapp-container.qml0000644000004100000410000002453613004613623021253 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.UnityWebApps 0.1 as UnityWebApps import Ubuntu.Web 0.2 import webcontainer.private 0.1 import ".." BrowserWindow { id: root objectName: "webappContainer" property bool backForwardButtonsVisible: true property bool chromeVisible: true property string localCookieStoreDbPath: "" property string url: "" property url webappIcon: "" property string webappName: "" property string webappModelSearchPath: "" property var webappUrlPatterns property string accountProvider: "" property bool accountSwitcher: false property string popupRedirectionUrlPrefixPattern: "" property url webviewOverrideFile: "" property var __webappCookieStore: null property alias webContextSessionCookieMode: webappViewLoader.webContextSessionCookieMode property string localUserAgentOverride: "" property bool blockOpenExternalUrls: false property bool openExternalUrlInOverlay: false property bool popupBlockerEnabled: true currentWebview: webappViewLoader.item ? webappViewLoader.item.currentWebview : null property bool runningLocalApplication: false title: getWindowTitle() // Used for testing signal schemeUriHandleFilterResult(string uri) function getWindowTitle() { var webappViewTitle = webappViewLoader.item ? webappViewLoader.item.title : "" var name = getWebappName() if (typeof(name) === 'string' && name.length !== 0) { return name } else if (webappViewTitle) { // TRANSLATORS: %1 refers to the current page’s title return i18n.tr("%1 - Ubuntu Web Browser").arg(webappViewTitle) } else { return i18n.tr("Ubuntu Web Browser") } } Component { id: webappViewComponent WebApp { id: browser url: accountProvider.length !== 0 ? "" : root.url accountSwitcher: root.accountSwitcher dataPath: webappDataLocation chromeVisible: root.chromeVisible backForwardButtonsVisible: root.backForwardButtonsVisible developerExtrasEnabled: root.developerExtrasEnabled webappModelSearchPath: root.webappModelSearchPath webappUrlPatterns: root.webappUrlPatterns blockOpenExternalUrls: root.blockOpenExternalUrls openExternalUrlInOverlay: root.openExternalUrlInOverlay popupBlockerEnabled: root.popupBlockerEnabled popupRedirectionUrlPrefixPattern: root.popupRedirectionUrlPrefixPattern localUserAgentOverride: getLocalUserAgentOverrideIfAny() runningLocalApplication: root.runningLocalApplication webviewOverrideFile: root.webviewOverrideFile anchors.fill: parent webbrowserWindow: webbrowserWindowProxy onWebappNameChanged: { if (root.webappName !== browser.webappName) { root.webappName = browser.webappName; root.title = getWindowTitle(); } } onChooseAccount: { showAccountsPage() onlineAccountsController.showAccountSwitcher() } } } function getWebappName() { /** Any webapp name coming from the command line takes over. A webapp can also be defined by a specific drop-in webapp-properties.json file that can bundle a few specific 'properties' (as the name implies) instead of having them listed in the command line. */ if (webappName) return webappName return webappModelSearchPath && webappModel.providesSingleInlineWebapp() ? webappModel.getSingleInlineWebappName() : "" } function getLocalUserAgentOverrideIfAny() { if (localUserAgentOverride.length !== 0) return localUserAgentOverride var name = getWebappName() if (name && webappModel.exists(name)) return webappModel.userAgentOverrideFor(name) return "" } UnityWebApps.UnityWebappsAppModel { id: webappModel searchPath: root.webappModelSearchPath onModelContentChanged: { var name = getWebappName() if (name && root.url.length === 0) { var idx = webappModel.getWebappIndex(name) root.url = webappModel.data( idx, UnityWebApps.UnityWebappsAppModel.Homepage) } } } // Because of https://launchpad.net/bugs/1398046, it's important that this // is the first child Loader { id: webappViewLoader anchors.fill: parent property var webContextSessionCookieMode: "" property var webappDataLocation onLoaded: { var context = item.currentWebview.context onlineAccountsController.setupWebcontextForAccount(context) } } OnlineAccountsController { id: onlineAccountsController anchors.fill: parent z: -1 // This is needed to have the dialogs shown; see above comment about bug 1398046 providerId: accountProvider applicationId: unversionedAppId accountSwitcher: root.accountSwitcher webappName: getWebappName() webappIcon: root.webappIcon onAccountSelected: { var newWebappDataLocation = dataLocation + accountDataLocation console.log("Loading webview on " + newWebappDataLocation) if (newWebappDataLocation == webappViewLoader.webappDataLocation) { showWebView() return } webappViewLoader.sourceComponent = null webappViewLoader.webappDataLocation = newWebappDataLocation // If we need to preserve session cookies, make sure that the // mode is "restored" and not "persistent", or the cookies // transferred from OA would be lost. // We check if the webContextSessionCookieMode is defined and, if so, // we override it in the webapp loader. if (willMoveCookies && typeof webContextSessionCookieMode === "string") { webappViewLoader.webContextSessionCookieMode = "restored" } webappViewLoader.sourceComponent = webappViewComponent } onContextReady: startBrowsing() onQuitRequested: Qt.quit() } Component.onCompleted: { i18n.domain = "webbrowser-app" } function showWebView() { onlineAccountsController.visible = false webappViewLoader.visible = true } function showAccountsPage() { webappViewLoader.visible = false onlineAccountsController.visible = true } function startBrowsing() { console.log("Start browsing") // This will activate the UnityWebApp element used in WebApp.qml webappViewLoader.item.webappName = root.webappName // As we use StateSaver to restore the URL, we need to check first if // it has not been set previously before setting the URL to the default property // homepage. var webView = webappViewLoader.item.currentWebview var current_url = webView.url.toString(); if (!current_url || current_url.length === 0) { webView.url = root.url } showWebView() } function makeUrlFromResult(result) { var scheme = null var hostname = null var url = root.currentWebview.url || root.url if (result.host && result.host.length !== 0) { hostname = result.host } else { var matchHostname = url.toString().match(/.*:\/\/([^/]*)\/.*/) if (matchHostname.length > 1) { hostname = matchHostname[1] } } if (result.scheme && result.scheme.length !== 0) { scheme = result.scheme } else { var matchScheme = url.toString().match(/(.*):\/\/[^/]*\/.*/) if (matchScheme.length > 1) { scheme = matchScheme[1] } } return scheme + '://' + hostname + "/" + (result.path ? result.path : "") } /** * */ function translateHandlerUri(uri) { // var scheme = uri.substr(0, uri.indexOf(":")) if (scheme.indexOf("http") === 0) { schemeUriHandleFilterResult(uri) return uri } var result = webappSchemeFilter.applyFilter(uri) var mapped_uri = makeUrlFromResult(result) uri = mapped_uri // Report the result of the intent uri filtering (if any) // Done for testing purposed. It is not possible at this point // to have AP call a slot and retrieve its result synchronously. schemeUriHandleFilterResult(uri) return uri } onOpenUrls: { // only consider the first one (if multiple) if (urls.length === 0 || !root.currentWebview) { return; } var requestedUrl = urls[0].toString(); if (popupRedirectionUrlPrefixPattern.length !== 0 && requestedUrl.match(popupRedirectionUrlPrefixPattern)) { return; } requestedUrl = translateHandlerUri(requestedUrl); // Add a small guard to prevent browsing to invalid urls if (currentWebview && currentWebview.shouldAllowNavigationTo && !currentWebview.shouldAllowNavigationTo(requestedUrl)) { return; } root.url = requestedUrl root.currentWebview.url = requestedUrl } } ./src/app/webcontainer/AccountsSplashScreen.qml0000644000004100000410000000571513004613604022104 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 SplashScreen { id: root property string providerName property bool accountMandatory: true signal chooseAccount() signal skip() signal quitRequested() Column { anchors { left: parent.left right: parent.right } spacing: units.gu(2) Label { anchors.left: parent.left anchors.right: parent.right horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap // TRANSLATORS: %1 refers to the application name, %2 refers to the account provider text: i18n.tr("%1 needs to access your %2 online account.").arg(root.applicationName).arg(root.providerName) visible: root.accountMandatory } Label { anchors.left: parent.left anchors.right: parent.right horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap // TRANSLATORS: %1 refers to the application name, %2 refers to the account provider text: i18n.tr("%1 would like to access your %2 online account.").arg(root.applicationName).arg(root.providerName) visible: !root.accountMandatory } Label { anchors.left: parent.left anchors.right: parent.right horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap text: i18n.tr("Choose an account now, or skip this step and choose an online account later.") visible: !root.accountMandatory } Item { anchors.left: parent.left anchors.right: parent.right anchors.margins: units.gu(1) height: units.gu(6) Button { anchors.left: parent.left width: parent.width / 2 - units.gu(1) text: root.accountMandatory ? i18n.tr("Close the app") : i18n.tr("Skip") onClicked: root.accountMandatory ? root.quitRequested() : root.skip() } Button { anchors.right: parent.right width: parent.width / 2 - units.gu(1) text: i18n.tr("Choose account") onClicked: root.chooseAccount() } } } } ./src/app/webcontainer/SadPage.qml0000644000004100000410000000303013004613604017302 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 Rectangle { property var webview Column { anchors { fill: parent margins: units.gu(4) } spacing: units.gu(4) Image { anchors.horizontalCenter: parent.horizontalCenter source: "assets/tab-error.png" } Label { anchors { left: parent.left right: parent.right } wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter text: webview ? i18n.tr("Oops, something went wrong.") : "" } Button { anchors.horizontalCenter: parent.horizontalCenter objectName: "reloadButton" text: i18n.tr("Reload") color: UbuntuColors.green onClicked: webview.reload() } } } ./src/app/webcontainer/webapp-container.cpp0000644000004100000410000004663113004613623021244 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "config.h" #include "webapp-container.h" #include "chrome-cookie-store.h" #include "scheme-filter.h" #include "local-cookie-store.h" #include "online-accounts-cookie-store.h" #include "session-utils.h" #include "url-pattern-utils.h" #include "webapp-container-helper.h" // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const char privateModuleUri[] = "webcontainer.private"; namespace { /* Hack to clear the local data of the webapp, when it's integrated with OA: * https://bugs.launchpad.net/bugs/1371659 * This is needed because cookie sets from different accounts might not * completely overwrite each other, and therefore we end up with an * inconsistent cookie jar. */ static void clearCookiesHack(const QString &provider) { if (provider.isEmpty()) { qWarning() << "--clear-cookies only works with an accountProvider" << endl; return; } /* check both ~/.local/share and ~/.cache, as the data will eventually be * moving from the first to the latter. */ QStringList baseDirs; baseDirs << QStandardPaths::writableLocation(QStandardPaths::DataLocation); baseDirs << QStandardPaths::writableLocation(QStandardPaths::CacheLocation); Q_FOREACH(const QString &baseDir, baseDirs) { QDir dir(baseDir); dir.removeRecursively(); } } } const QString WebappContainer::URL_PATTERN_SEPARATOR = ","; const QString WebappContainer::LOCAL_SCHEME_FILTER_FILENAME = "local-scheme-filter.js"; WebappContainer::WebappContainer(int& argc, char** argv): BrowserApplication(argc, argv), m_accountSwitcher(false), m_storeSessionCookies(false), m_backForwardButtonsVisible(false), m_addressBarVisible(false), m_localWebappManifest(false), m_openExternalUrlInOverlay(false), m_webappContainerHelper(new WebappContainerHelper()) { } QString WebappContainer::appId() const { Q_FOREACH(const QString& argument, m_arguments) { if (argument.startsWith("--app-id=")) { return argument.split("--app-id=")[1]; } } return QString(); } bool WebappContainer::initialize() { earlyEnvironment(); if (qgetenv("APP_ID").isEmpty()) { QString id = appId(); if (id.isEmpty()) { qCritical() << "The application has been launched with no " "explicit or system provided app id. " "An application id can be set by using the --app-id " "command line parameter and setting it to a unique " "application specific value or using the APP_ID environment " "variable."; return false; } qputenv("APP_ID", id.toUtf8()); } if (BrowserApplication::initialize( "webcontainer/webapp-container.qml", QString::fromUtf8(qgetenv("APP_ID")))) { parseCommandLine(); parseExtraConfiguration(); if (m_localWebappManifest) m_webappModelSearchPath = "."; if (!m_webappModelSearchPath.isEmpty()) { QDir searchDir(m_webappModelSearchPath); searchDir.makeAbsolute(); if (searchDir.exists()) { m_window->setProperty("webappModelSearchPath", searchDir.path()); } } if ( ! m_localCookieStoreDbPath.isEmpty()) { m_window->setProperty("localCookieStoreDbPath", m_localCookieStoreDbPath); } m_window->setProperty("webappName", m_webappName); QFileInfo iconInfo(m_webappIcon); QUrl iconUrl; if (iconInfo.isReadable()) { iconUrl = QUrl::fromLocalFile(iconInfo.absoluteFilePath()); } m_window->setProperty("webappIcon", iconUrl); m_window->setProperty("backForwardButtonsVisible", m_backForwardButtonsVisible); m_window->setProperty("chromeVisible", m_addressBarVisible); m_window->setProperty("accountProvider", m_accountProvider); m_window->setProperty("accountSwitcher", m_accountSwitcher); m_window->setProperty("openExternalUrlInOverlay", m_openExternalUrlInOverlay); m_window->setProperty("webappUrlPatterns", m_webappUrlPatterns); QQmlContext* context = m_engine->rootContext(); if (m_storeSessionCookies) { QString sessionCookieMode = SessionUtils::firstRun(m_webappName) ? QStringLiteral("persistent") : QStringLiteral("restored"); qDebug() << "Setting session cookie mode to" << sessionCookieMode; m_window->setProperty("webContextSessionCookieMode", sessionCookieMode); } context->setContextProperty("webappContainerHelper", m_webappContainerHelper.data()); if ( ! m_popupRedirectionUrlPrefixPattern.isEmpty()) { const QString WEBAPP_CONTAINER_DO_NOT_FILTER_PATTERN_URL_ENV_VAR = qgetenv("WEBAPP_CONTAINER_DO_NOT_FILTER_PATTERN_URL"); m_window->setProperty( "popupRedirectionUrlPrefixPattern", WEBAPP_CONTAINER_DO_NOT_FILTER_PATTERN_URL_ENV_VAR == "1" ? m_popupRedirectionUrlPrefixPattern : UrlPatternUtils::transformWebappSearchPatternToSafePattern( m_popupRedirectionUrlPrefixPattern, false)); } if (!m_userAgentOverride.isEmpty()) { m_window->setProperty("localUserAgentOverride", m_userAgentOverride); } // Experimental, unsupported API, to override the webview QFileInfo overrideFile("webview-override.qml"); if (overrideFile.exists()) { m_window->setProperty("webviewOverrideFile", QUrl::fromLocalFile(overrideFile.absoluteFilePath())); } const QString WEBAPP_CONTAINER_BLOCK_OPEN_URL_EXTERNALLY_ENV_VAR = qgetenv("WEBAPP_CONTAINER_BLOCK_OPEN_URL_EXTERNALLY"); if (WEBAPP_CONTAINER_BLOCK_OPEN_URL_EXTERNALLY_ENV_VAR == "1") { m_window->setProperty("blockOpenExternalUrls", true); } bool runningLocalApp = false; QList urls = this->urls(); if (!urls.isEmpty()) { QUrl homeUrl = urls.last(); m_window->setProperty("url", homeUrl); if (UrlPatternUtils::isLocalHtml5ApplicationHomeUrl(homeUrl)) { qDebug() << "Started as a local application container."; runningLocalApp = true; } } else if (m_webappModelSearchPath.isEmpty() && m_webappName.isEmpty()) { qCritical() << "No starting homepage provided"; return false; } // Otherwise, assume that the homepage will come from a locally defined // webapp-properties.json file pulled from the webapp model element // or from a default local system install (if any). m_window->setProperty("runningLocalApplication", runningLocalApp); // Handle the invalid runtime conditions for the local apps if (runningLocalApp && !isValidLocalApplicationRunningContext()) { qCritical() << "Cannot run a local HTML5 application, invalid command line flags detected."; return false; } // Handle an optional scheme handler filter. The default *catch all* filter does nothing. setupLocalSchemeFilterIfAny(context, m_webappModelSearchPath); if (qEnvironmentVariableIsSet("WEBAPP_CONTAINER_BLOCKER_DISABLED") && QString(qgetenv("WEBAPP_CONTAINER_BLOCKER_DISABLED")) == "1") { m_window->setProperty("popupBlockerEnabled", false); } m_component->completeCreate(); return true; } else { return false; } } void WebappContainer::setupLocalSchemeFilterIfAny(QQmlContext* context, const QString& webappSearchPath) { if(!context) { return; } QDir searchPath(webappSearchPath.isEmpty() ? QDir::currentPath() : webappSearchPath); bool hasValidLocalSchemeFilterFile = false; QMap content = SchemeFilter::parseValidLocalSchemeFilterFile( hasValidLocalSchemeFilterFile, searchPath.filePath(LOCAL_SCHEME_FILTER_FILENAME)); if (hasValidLocalSchemeFilterFile) { qDebug() << "Using local scheme filter file:" << LOCAL_SCHEME_FILTER_FILENAME; } m_schemeFilter.reset(new SchemeFilter(content)); context->setContextProperty("webappSchemeFilter", m_schemeFilter.data()); } bool WebappContainer::isValidLocalApplicationRunningContext() const { return m_webappModelSearchPath.isEmpty() && m_popupRedirectionUrlPrefixPattern.isEmpty() && m_webappUrlPatterns.isEmpty() && m_webappName.isEmpty(); } void WebappContainer::qmlEngineCreated(QQmlEngine* engine) { if (engine) { qmlRegisterType(privateModuleUri, 0, 1, "ChromeCookieStore"); qmlRegisterType(privateModuleUri, 0, 1, "LocalCookieStore"); qmlRegisterType(privateModuleUri, 0, 1, "OnlineAccountsCookieStore"); } } void WebappContainer::printUsage() const { QTextStream out(stdout); QString command = QFileInfo(QCoreApplication::applicationFilePath()).fileName(); out << "Usage: " << command << " [-h|--help]" " [--fullscreen]" " [--maximized]" " [--inspector]" " [--app-id=APP_ID]" " [--homepage=URL]" " [--webapp=name]" " [--name=NAME]" " [--icon=PATH]" " [--webappModelSearchPath=PATH]" " [--webappUrlPatterns=URL_PATTERNS]" " [--accountProvider=PROVIDER_NAME]" " [--accountSwitcher]" " [--enable-back-forward]" " [--enable-addressbar]" " [--store-session-cookies]" " [--user-agent-string=USER_AGENT]" " [URL]" << endl; out << "Options:" << endl; out << " -h, --help display this help message and exit" << endl; out << " --fullscreen display full screen" << endl; out << " --local-webapp-manifest configure the webapp assuming that it has a local manifest.json file" << endl; out << " --maximized opens the application maximized" << endl; out << " --inspector[=PORT] run a remote inspector on a specified port or " << REMOTE_INSPECTOR_PORT << " as the default port" << endl; out << " --app-id=APP_ID run the application with a specific APP_ID" << endl; out << " --homepage=URL override any URL passed as an argument" << endl; out << " --webapp=name try to match the webapp by name with an installed integration script" << endl; out << " --name=NAME display name of the webapp, shown in the splash screen" << endl; out << " --icon=PATH Icon to be shown in the splash screen. PATH can be an absolute or path relative to CWD" << endl; out << " --webappModelSearchPath=PATH alter the search path for installed webapps and set it to PATH. PATH can be an absolute or path relative to CWD" << endl; out << " --webappUrlPatterns=URL_PATTERNS list of comma-separated url patterns (wildcard based) that the webapp is allowed to navigate to" << endl; out << " --accountProvider=PROVIDER_NAME Online account provider for the application if the application is to reuse a local account." << endl; out << " --accountSwitcher enable switching between different Online Accounts identities" << endl; out << " --store-session-cookies store session cookies on disk" << endl; out << " --enable-media-hub-audio enable media-hub for audio playback" << endl; out << " --user-agent-string=USER_AGENT overrides the default User Agent with the provided one." << endl; out << " --open-external-url-in-overlay if url patterns are defined, all external urls are opened in overlay instead of browser" << endl; out << "Chrome options (if none specified, no chrome is shown by default):" << endl; out << " --enable-back-forward enable the display of the back and forward buttons (implies --enable-addressbar)" << endl; out << " --enable-addressbar enable the display of a minimal chrome (favicon and title)" << endl; } void WebappContainer::earlyEnvironment() { Q_FOREACH(const QString& argument, m_arguments) { if (argument.startsWith("--enable-media-hub-audio")) { qputenv("OXIDE_ENABLE_MEDIA_HUB_AUDIO", QString("1").toLocal8Bit().constData()); } } } void WebappContainer::parseCommandLine() { Q_FOREACH(const QString& argument, m_arguments) { if (argument.startsWith("--webappModelSearchPath=")) { m_webappModelSearchPath = argument.split("--webappModelSearchPath=")[1]; } else if (argument.startsWith("--webapp=")) { // We use the name as a reference instead of the URL with a subsequent step to match it with a webapp. // TODO: validate that it is fine in all cases (country dependent, etc…). QString name = argument.split("--webapp=")[1]; m_webappName = QByteArray::fromBase64(name.toUtf8()).trimmed(); } else if (argument.startsWith("--name=")) { m_webappName = argument.split("--name=")[1]; } else if (argument.startsWith("--icon=")) { m_webappIcon = argument.split("--icon=")[1]; } else if (argument.startsWith("--webappUrlPatterns=")) { QString tail = argument.split("--webappUrlPatterns=")[1]; if (!tail.isEmpty()) { QStringList includePatterns = tail.split(URL_PATTERN_SEPARATOR); m_webappUrlPatterns = UrlPatternUtils::filterAndTransformUrlPatterns(includePatterns); } } else if (argument.startsWith("--accountProvider=")) { m_accountProvider = argument.split("--accountProvider=")[1]; } else if (argument == "--accountSwitcher") { m_accountSwitcher = true; } else if (argument == "--clear-cookies") { qWarning() << argument << " is an unsupported option: it can be removed without notice..." << endl; clearCookiesHack(m_accountProvider); } else if (argument == "--store-session-cookies") { m_storeSessionCookies = true; } else if (argument == "--enable-back-forward") { m_backForwardButtonsVisible = true; } else if (argument == "--enable-addressbar") { m_addressBarVisible = true; } else if (argument == "--local-webapp-manifest") { m_localWebappManifest = true; } else if (argument.startsWith("--popup-redirection-url-prefix=")) { m_popupRedirectionUrlPrefixPattern = argument.split("--popup-redirection-url-prefix=")[1]; } else if (argument.startsWith("--local-cookie-db-path=")) { m_localCookieStoreDbPath = argument.split("--local-cookie-db-path=")[1]; } else if (argument.startsWith("--user-agent-string=")) { m_userAgentOverride = argument.split("--user-agent-string=")[1]; } else if (argument == "--open-external-url-in-overlay") { m_openExternalUrlInOverlay = true; } } } void WebappContainer::parseExtraConfiguration() { // Add potential extra url patterns not listed in the command line m_webappUrlPatterns.append( UrlPatternUtils::filterAndTransformUrlPatterns( getExtraWebappUrlPatterns().split(URL_PATTERN_SEPARATOR))); } QString WebappContainer::getExtraWebappUrlPatterns() const { static const QString EXTRA_APP_URL_PATTERNS_CONF_FILENAME = "extra-url-patterns.conf"; QString extraUrlPatternsFilename = QString("%1/%2") .arg(QStandardPaths::writableLocation(QStandardPaths::DataLocation)) .arg(EXTRA_APP_URL_PATTERNS_CONF_FILENAME); QString extraPatterns; QFileInfo f(extraUrlPatternsFilename); if (f.exists() && f.isReadable()) { QSettings extraUrlPatternsSetting(f.absoluteFilePath(), QSettings::IniFormat); extraUrlPatternsSetting.beginGroup("Extra Patterns"); QVariant patternsValue = extraUrlPatternsSetting.value("Patterns"); // The line can contain comma separated args Patterns=1,2,3. In this case // QSettings interprets this as a StringList instead of giving us // the raw value. if (patternsValue.type() == QVariant::StringList) extraPatterns = patternsValue.toStringList().join(","); else extraPatterns = patternsValue.toString(); if ( ! extraPatterns.isEmpty()) { qDebug() << "Found extra url patterns to be added to the list of allowed urls: " << extraPatterns; } extraUrlPatternsSetting.endGroup(); } return extraPatterns; } bool WebappContainer::isValidLocalResource(const QString& resourceName) const { QFileInfo info(resourceName); return info.isFile() && info.exists(); } bool WebappContainer::shouldNotValidateCommandLineUrls() const { return qEnvironmentVariableIsSet("WEBAPP_CONTAINER_SHOULD_NOT_VALIDATE_CLI_URLS") && QString(qgetenv("WEBAPP_CONTAINER_SHOULD_NOT_VALIDATE_CLI_URLS")) == "1"; } QList WebappContainer::urls() const { QList urls; Q_FOREACH(const QString& argument, m_arguments) { if (!argument.startsWith("-")) { // This is used for testing to avoid having existing // resources to run against. if (shouldNotValidateCommandLineUrls()) { urls.append(argument.startsWith("file://") ? argument : (QString("file://") + argument)); continue; } QUrl url; if (isValidLocalResource(argument)) { url = QUrl::fromLocalFile(QFileInfo(argument).absoluteFilePath()); } else { url = QUrl::fromUserInput(argument); } if (url.isValid()) { urls.append(url); } } } return urls; } int main(int argc, char** argv) { QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); WebappContainer app(argc, argv); if (app.initialize()) { return app.run(); } else { return 0; } } ./src/app/webcontainer/scheme-filter.cpp0000644000004100000410000001277613004613604020537 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "scheme-filter.h" #include #include #include #include #include #include #include #include #include #include #include "intent-parser.h" class SchemeFilterPrivate { public: static const QString DEFAULT_PASS_THROUGH_FILTER; public: SchemeFilterPrivate(const QMap& content); QJSValue evaluate(const QString & filterFunction, const QUrl& uri); QJSValue evaluate(const QUrl& uri); QJSValue evaluate(QJSValue & function, const QUrl& uri); bool hasFilterFor(const QUrl& uri); private: QJSValue callFunction( QJSValue & function , const QString& scheme , const QString& uri , const QString& host); QMap _filterFunctionsPerScheme; QJSEngine _engine; }; // static const QString SchemeFilterPrivate::DEFAULT_PASS_THROUGH_FILTER = "(function(uri) { return uri; })"; SchemeFilterPrivate::SchemeFilterPrivate(const QMap& content) { Q_FOREACH(QString scheme, content.keys()) { QJSValue v = _engine.evaluate(content[scheme]); if (v.isCallable()) { _filterFunctionsPerScheme[scheme] = v; } } } bool SchemeFilterPrivate::hasFilterFor(const QUrl& uri) { return _filterFunctionsPerScheme.contains(uri.scheme()); } QJSValue SchemeFilterPrivate::callFunction(QJSValue & function , const QString& scheme , const QString& path , const QString& host) { if (!function.isCallable()) { qCritical() << "Invalid intent filter function (not callable)"; return QJSValue(); } QVariantMap o; o.insert("scheme", scheme); o.insert("path", path); o.insert("host", host); QJSValueList jsargs; jsargs << _engine.toScriptValue(o); return function.call(jsargs); } QJSValue SchemeFilterPrivate::evaluate(const QString & filterFunction, const QUrl& uri) { QJSValue f = _engine.evaluate(filterFunction); return evaluate(f, uri); } QJSValue SchemeFilterPrivate::evaluate(const QUrl& uri) { return evaluate(_filterFunctionsPerScheme[uri.scheme()], uri); } QJSValue SchemeFilterPrivate::evaluate(QJSValue & function, const QUrl& uri) { QString scheme; QString path; QString host; if (uri.scheme() == "intent") { IntentUriDescription intent = parseIntentUri(uri); scheme = intent.scheme; path = intent.uriPath; host = intent.host; } else { scheme = uri.scheme(); path = uri.path(); host = uri.host(); } return callFunction( function, scheme, path, host); } // static QMap SchemeFilter::parseValidLocalSchemeFilterFile( bool & isValid, const QString& filename) { QFile f(filename); if (!f.exists() || !f.open(QIODevice::ReadOnly)) { isValid = false; return QMap(); } QString content = f.readAll(); QJsonDocument document(QJsonDocument::fromJson(content.toUtf8())); if (document.isNull() || document.isEmpty() || !document.isObject()) { isValid = false; return QMap(); } QMap parsedContent; QJsonObject root = document.object(); Q_FOREACH(const QString& k, root.keys()) { QJsonValue v = root.value(k); if (v.isString()) { QJSEngine engine; QJSValue result = engine.evaluate(v.toString(), filename); if (result.isNull() || !result.isCallable()) { isValid = false; return QMap(); } parsedContent[k] = v.toString(); } } isValid = true; return parsedContent; } SchemeFilter::SchemeFilter(const QMap& content, QObject *parent) : QObject(parent), d_ptr(new SchemeFilterPrivate(content)) {} SchemeFilter::~SchemeFilter() { delete d_ptr; } bool SchemeFilter::hasFilterFor(const QUrl& uri) { Q_D(SchemeFilter); return d->hasFilterFor(uri); } QVariantMap SchemeFilter::applyFilter(const QUrl& uri) { Q_D(SchemeFilter); if (! hasFilterFor(uri)) { return d->evaluate( SchemeFilterPrivate::DEFAULT_PASS_THROUGH_FILTER, uri) .toVariant().toMap(); } QJSValue value; // Special case to parse schemes we know about & want to provide helpr w/ value = d->evaluate(uri); QVariantMap result; if (value.isObject() && value.toVariant().canConvert(QVariant::Map)) { result = value.toVariant().toMap(); } return result; } ./src/app/webcontainer/ContextMenuMobile.qml0000644000004100000410000001132013004613604021400 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 as ListItems import Ubuntu.Components.Popups 1.3 as Popups import com.canonical.Oxide 1.8 as Oxide Popups.Dialog { property QtObject contextModel: model property ActionList actions: null objectName: "contextMenuMobile" QtObject { id: internal readonly property bool isImage: (contextModel.mediaType === Oxide.WebView.MediaTypeImage) || (contextModel.mediaType === Oxide.WebView.MediaTypeCanvas) } Row { id: header spacing: units.gu(2) anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) } height: units.gu(2 * title.lineCount + 3) visible: title.text Icon { width: units.gu(2) height: units.gu(2) anchors { top: parent.top topMargin: units.gu(2) } name: internal.isImage ? "stock_image" : "" // work around the lack of a standard stock_link symbolic icon in the theme Component.onCompleted: { if (!name) { source = "assets/stock_link.svg" } } } Label { id: title objectName: "titleLabel" text: contextModel.srcUrl.toString() ? contextModel.srcUrl : contextModel.linkUrl width: parent.width - units.gu(4) anchors { top: parent.top topMargin: units.gu(2) bottom: parent.bottom } fontSize: "x-small" maximumLineCount: 2 wrapMode: Text.Wrap height: contentHeight } } ListItems.ThinDivider { anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) } visible: header.visible } Repeater { model: actions.actions delegate: ListItems.Empty { action: actions.actions[index] objectName: action.objectName + "_item" visible: action.enabled showDivider: false height: units.gu(5) Label { anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) verticalCenter: parent.verticalCenter } fontSize: "x-small" text: action.text } ListItems.ThinDivider { anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) bottom: parent.bottom } } onTriggered: contextModel.close() } } ListItems.Empty { objectName: "cancelAction" height: units.gu(5) showDivider: false Label { anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) verticalCenter: parent.verticalCenter } fontSize: "x-small" text: i18n.tr("Cancel") } onTriggered: contextModel.close() } // adjust default dialog visuals to custom requirements for the context menu Binding { target: __foreground property: "margins" value: 0 } Binding { target: __foreground property: "itemSpacing" value: 0 } // We can’t prevent the dialog from stealing the focus from // the webview, but we can at least restore it when the // dialog is closed (https://launchpad.net/bugs/1526884). Component.onDestruction: Oxide.WebView.view.forceActiveFocus() } ./src/app/webcontainer/online-accounts-cookie-store.h0000644000004100000410000000315213004613604023143 0ustar www-datawww-data/* * Copyright 2013-2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 ONLINE_ACCOUNTS_COOKIE_STORE_H #define ONLINE_ACCOUNTS_COOKIE_STORE_H #include #include #include "cookie-store.h" class OnlineAccountsCookieStorePrivate; class OnlineAccountsCookieStore : public CookieStore { Q_OBJECT Q_PROPERTY(quint32 accountId READ accountId WRITE setAccountId NOTIFY accountIdChanged) public: OnlineAccountsCookieStore(QObject *parent = 0); ~OnlineAccountsCookieStore(); quint32 accountId () const; void setAccountId (quint32); Q_SIGNALS: void accountIdChanged(); private: typedef QList OnlineAccountsCookies; virtual void doGetCookies() Q_DECL_OVERRIDE; virtual void doSetCookies(const Cookies& cookies) Q_DECL_OVERRIDE; static Cookies fromDbusCookies(const OnlineAccountsCookies& cookies); private: OnlineAccountsCookieStorePrivate * d_ptr; Q_DECLARE_PRIVATE(OnlineAccountsCookieStore) }; #endif // ONLINE_ACCOUNTS_COOKIE_STORE_H ./src/app/webcontainer/intent-parser.h0000644000004100000410000000231313004613604020232 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 _INTENT_PARSER_H_ #define _INTENT_PARSER_H_ #include class QUrl; struct IntentUriDescription { QString uriPath; // optional QString host; QString package; QString action; QString category; QString component; QString scheme; }; /** * @brief Parse a URI that is supposed to be an intent as defined here * * https://developer.chrome.com/multidevice/android/intents * * @param intentUri * @return */ IntentUriDescription parseIntentUri(const QUrl& intentUri); #endif // _INTENT_PARSER_H_ ./src/app/webcontainer/assets/0000755000004100000410000000000013004613605016572 5ustar www-datawww-data./src/app/webcontainer/assets/stock_link.svg0000644000004100000410000002016413004613604021455 0ustar www-datawww-data image/svg+xml ./src/app/webcontainer/assets/tab-error@27.png0000644000004100000410000002235013004613604021447 0ustar www-datawww-dataPNG  IHDR ϜsBITUF pHYs+tEXtSoftwarewww.inkscape.org<$gIDATx ew]'K MF #\83(##BFE Ψa9΁QaKȐHHttky.sR^۫ջ?]ɩA @ gd(g\( gX( gؒPd gXX5x6˝}@fS,?> =j΢6ͻ]f,`SbY(lY4@by$TWx^΢6!׆H˵6G31P ƾzѱbc?~XC<͢69k|32>ԉN-O,X~RX;w/}&Ka+*kUW徭hM++x.)rq͊*|Hy.;E3s,b&I(ׇs\֥Yg Ugq KWYUѬ3@r,mCyy`B4X &UҚWQ>3cXcQW\9:LUO@,c8{wgby{%ǂwž9Lof =r, [_t慿.wѬ3@\cO\iWrg^f:s1vtxW'`ξx0uureggX˫?]OCu::;$:L\cOJg4'lr]_sb`[sm_SEUc_^/j<6u `י},U7J6#$0ܱ/O=xSbϬ:63 {Na=eFgnm_걩:63 ˿{~R 75~)v}1`jѦre z̢m-IrZЮ/79&>%()ܷꪑi!}y7}ޒF Xn׋ħ¾sD^s= iRs__tS]53 +/N°&qgOՙ̝.5+[nЙm?Ť+oYy (ي``Jg@,?e_:xKbtf{,7c.- 0^1Cgl\4?ݺ\=60}yO>Ǥ%:3rc_^)XZy \ښcRǦU#:3r_AsD|μSg`;r]_>zEϾ\̧?T~ Np^R sig?u @QnczlK:3cR\$+%M:|1$%}Ftfrݗ'8\Wtf @fm_NCajg>Cg }tL˕μ0O^sڙ/\eg 4r :3׊}?&xlޗߛ @rp [0GIP:3˓],Wy,&p^;2$Zx @~x0ycS^53|ӇTcS_~ @VbII m,WM}Lg }ҩw{;S=63 f2Mrx}yH:w*w.og93 ׊<+ s3C:b o0/C`o0쩮uk6]0Iq @vx09J@ƻkw0:⟥H,D_³(܊`̤;bMoJo>Kg`Pܩ/OoيcR`DÚLyo-`͂Y4}f%GZ9LLO4/Gq6&۾N}x}b$k˯8 rsb`ޙ`Q=l @rрcE0﬽΁s,uf _f,XG_Y؊#bx5G]Kە+5`3Kga&x|M0lœ+4vww.)x6rN3] WOjI @p 'aFrR(tјg<ϫgcɶGYI1n;ktzPg|gzفN`VѬ3rx8C}9]W)ltڱWl @XpȱǙbu +/JWتμ8uWŒG/Y?'1^M;s}KQ| X0>Y &k~5 xno^ufl]_i9&N|?{<f5}ypSX.&|`7{g?14<~˳uLjy]Nf+/{gv ;dx$}|{;/VWug|WLr1:ߗll Xno/ ;^)TyVڙǺ m ?վ\L}}`|Y`96uq61<3 k0>~KثjgbcRFIv+ZsuڣŮ\{I3ۺ/lZ^GޝX`>ˎBDSb ,)Nf6{5VL lE_>8ӱ\Lh06N|l+o^ױj8@_nݗ߾?Y 3sW`>Y+/(wh6Зr5YdKW٫ű\_X,v9'߹$ɽt0tw 0}yŹJ%ODy8Aglr k3O|l9^vq`u_.毌sޕ+N!k"덳E_0yW(|zr[Llҭɐr0Og0}yxc^kvONt0[<<,鶟49`xL8盬8<0`nzHx8{;ʝy8`H_sk;:kyl<sm_oB]9_9~ :/W)*֕sl sLJW:q6QnFmo`Ol2ݗ}yI`86s]֙җ#v~-uq-`9Z_>XWεS37hK_~l9/?ЗGO6{iSH;sq-`yq[i憗c:3@xHWM=g!wclk=~Gm ؤq6@.rt4kxv:q6@fĺP?6l }}pr_zpxJ;/sq Zk=$ 8.Khnȟ6u{Cg6C_yq<8(>78;8+8383wNp^p~|8 G<v-!|fp  V׾?J+_- rAq6@yWC+.{=qifҵ;w0. o4C-zg0Ⱦ?4U5]^FOμvD3@&y$;xhor9_.El)d>}M=ܝ=^f[חw .z v?/Mcyv`1c}w^:G<0sTokPI[^{ 񱩻8[g|_{L7{4pw`N;lp 󱩯^%w߂KҾmJpy+?ԏ>W>ݶ`q E0>qy<D3@+s٭. `byu0Ǧ8`}sOItL*ԕsu8l @X#]Ǧ;h`}y㘔wl-`庾te[L;qxq`̾|mQprg)?"0~w 6?vQ3nL0lJ_.*6ůyo-`:3囟GL[/[ftfML-`Glm{_9ǤlvwLg tLHllڙGtfd|GlM_3/=bh]zjY0ܗwuez96ukڙg[tfl7.#]|:a6:i_.(Uy!kwf`&K̺Ksa`j3YXWfo(?kj`،`mbC`ugO6#pl6˃ن3@|F0PWfmK0cً*S>,6;񅿶ً }'Muw` @0 +QKw3wlԩ`-6!Jب~>cH0’6mr\ `|F#t׉4fC0?L_`Q9L_}2q[C F3;s鑏'֕Yw_~0878l 07pf/#_T٫o uGJ_ ق!N7_2c*Gi,ޓقcgYg{ Z|7Up~yO6ԙ+<[ؕM _}헂Ҷ hN %w}nݬ_h_}/oz+V# ^b%nC(>{3*Wwe m8w={" _OW.bY0lb4F"^ox z"}}^ˁIENDB`./src/app/webcontainer/webapp-container.h0000644000004100000410000000460613004613623020705 0ustar www-datawww-data/* * Copyright 2013 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __WEBAPP_CONTAINER_H__ #define __WEBAPP_CONTAINER_H__ #include "browserapplication.h" // Qt #include #include #include class SchemeFilter; class QQmlContext; class WebappContainerHelper; class WebappContainer : public BrowserApplication { Q_OBJECT public: WebappContainer(int& argc, char** argv); bool initialize(); protected: void qmlEngineCreated(QQmlEngine *); virtual QList urls() const; private: virtual void printUsage() const; void earlyEnvironment(); void parseCommandLine(); void parseExtraConfiguration(); QString getExtraWebappUrlPatterns() const; bool isValidLocalApplicationRunningContext() const; bool isValidLocalResource(const QString& resourceName) const; bool shouldNotValidateCommandLineUrls() const; bool isValidLocalIntentFilterFile(const QString& filename) const; void setupLocalSchemeFilterIfAny(QQmlContext* context, const QString& webappSearchPath); QString appId() const; private: QString m_webappName; QString m_webappIcon; QString m_webappModelSearchPath; QStringList m_webappUrlPatterns; QString m_accountProvider; bool m_accountSwitcher; bool m_storeSessionCookies; bool m_backForwardButtonsVisible; bool m_addressBarVisible; bool m_localWebappManifest; bool m_openExternalUrlInOverlay; QString m_popupRedirectionUrlPrefixPattern; QString m_localCookieStoreDbPath; QString m_userAgentOverride; QScopedPointer m_webappContainerHelper; QScopedPointer m_schemeFilter; static const QString URL_PATTERN_SEPARATOR; static const QString LOCAL_SCHEME_FILTER_FILENAME; }; #endif // __WEBAPP_CONTAINER_H__ ./src/app/webcontainer/oxide-cookie-helper.cpp0000644000004100000410000002201213004613604021624 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "oxide-cookie-helper.h" #include #include #include #include #include typedef QList Cookies; class OxideCookieHelperPrivate : public QObject { Q_OBJECT Q_DECLARE_PUBLIC(OxideCookieHelper) public: OxideCookieHelperPrivate(OxideCookieHelper* q); void setCookies(const QList& cookies); QList cookiesWithDomain(const QList& cookies, const QString& domain); private Q_SLOTS: void oxideCookiesUpdated(int requestId, const QVariant& failedCookiesVariant); private: QObject* m_backend; QMap m_pendingCalls; QList m_failedCookies; mutable OxideCookieHelper* q_ptr; }; OxideCookieHelperPrivate::OxideCookieHelperPrivate(OxideCookieHelper* q): QObject(q), m_backend(0), q_ptr(q) { qRegisterMetaType >(); } void OxideCookieHelperPrivate::setCookies(const QList& cookies) { Q_Q(OxideCookieHelper); if (Q_UNLIKELY(!m_backend)) { qCritical() << "No Oxide backend set!"; return; } if (Q_UNLIKELY(!m_pendingCalls.isEmpty())) { qCritical() << "A call to setCookies() is already in progress"; return; } m_failedCookies.clear(); if (cookies.isEmpty()) { /* We don't simply use Q_EMIT because we want the signal to be emitted * asynchronously */ QMetaObject::invokeMethod(q, "cookiesSet", Qt::QueuedConnection, Q_ARG(QList, cookies)); return; } /* Since Oxide does not support setting cookies for different domains in a * single call to setCookies(), we group the cookies by their domain, and * perform a separate call to Oxide's setCookies() for each domain. * * Cookies whose domain doesn't start with a "." are host cookies, and need * to be treated specially: we will use their domain as host (that is, we * will pass it as first argument in the setNetworkCookies() call), and * will unset their domain in the cookie. */ QMap cookiesPerHost; Q_FOREACH(const QNetworkCookie &cookie, cookies) { QNetworkCookie c(cookie); /* We use the domain (without any starting dot) as host */ QString host = c.domain(); if (host.startsWith('.')) { host = host.mid(1); } else { /* No starting dot => this is a host cookie */ c.setDomain(QString()); } /* This creates an empty list if the host is new in the map */ QList &domainCookies = cookiesPerHost[host]; domainCookies.append(c); } /* Grouping done, perform the calls */ QMapIterator it(cookiesPerHost); while (it.hasNext()) { it.next(); QUrl url; url.setScheme("http"); url.setHost(it.key()); int requestId = -1; QMetaObject::invokeMethod(m_backend, "setNetworkCookies", Qt::DirectConnection, Q_RETURN_ARG(int, requestId), Q_ARG(QUrl, url), Q_ARG(QList, it.value())); if (Q_UNLIKELY(requestId == -1)) { m_failedCookies.append(cookiesWithDomain(it.value(), url.host())); } else { m_pendingCalls.insert(requestId, url.host()); } } /* If all the calls failed, we need to emit a reply here */ if (m_pendingCalls.isEmpty()) { /* We don't simply use Q_EMIT because we want the signal to be emitted * asynchronously */ QMetaObject::invokeMethod(q, "cookiesSet", Qt::QueuedConnection, Q_ARG(QList, m_failedCookies)); } } QList OxideCookieHelperPrivate::cookiesWithDomain(const QList& cookies, const QString& domain) { QList restoredCookies; Q_FOREACH(const QNetworkCookie& cookie, cookies) { QNetworkCookie c(cookie); if (c.domain().isEmpty()) { c.setDomain(domain); } restoredCookies.append(c); } return restoredCookies; } void OxideCookieHelperPrivate::oxideCookiesUpdated(int requestId, const QVariant& failedCookiesVariant) { Q_Q(OxideCookieHelper); QString host = m_pendingCalls.value(requestId); QList failedCookies = OxideCookieHelper::cookiesFromVariant(failedCookiesVariant); m_failedCookies.append(cookiesWithDomain(failedCookies, host)); m_pendingCalls.remove(requestId); if (m_pendingCalls.isEmpty()) { Q_EMIT q->cookiesSet(m_failedCookies); } } OxideCookieHelper::OxideCookieHelper(QObject* parent): QObject(parent), d_ptr(new OxideCookieHelperPrivate(this)) { } QList OxideCookieHelper::cookiesFromVariant(const QVariant& cookies) { if (!cookies.canConvert(QMetaType::QVariantList)) { return QList(); } QList networkCookies; QList cl = cookies.toList(); Q_FOREACH(QVariant cookie, cl) { if (!cookie.canConvert(QVariant::Map)) { continue; } QNetworkCookie nc; QVariantMap vm = cookie.toMap(); if (!vm.contains("name") || !vm.contains("value")) { continue; } nc.setName(vm.value("name").toByteArray()); nc.setValue(vm.value("value").toByteArray()); nc.setDomain(vm.value("domain").toString()); nc.setPath(vm.value("path").toString()); if (vm.contains("httponly") && vm.value("httponly").canConvert(QVariant::Bool)) { nc.setHttpOnly(vm.value("httponly").toBool()); } if (vm.contains("issecure") && vm.value("issecure").canConvert(QVariant::Bool)) { nc.setSecure(vm.value("issecure").toBool()); } if (vm.contains("expirationdate")) { QVariant value = vm.value("expirationdate"); if (value.canConvert(QVariant::DateTime)) { nc.setExpirationDate(value.toDateTime()); } else if (value.canConvert(QVariant::LongLong)) { bool ok = false; qlonglong date = value.toLongLong(&ok); if (ok) nc.setExpirationDate(QDateTime::fromMSecsSinceEpoch(date)); } } networkCookies.append(nc); } return networkCookies; } QVariant OxideCookieHelper::variantFromCookies(const QList& cookies) { /* Taken straight from Oxide's networkCookiesToVariant() method defined in * qt/quick/api/oxideqquickwebcontext.cc */ QList list; Q_FOREACH(QNetworkCookie cookie, cookies) { QVariantMap c; c.insert("name", QVariant(QString(cookie.name()))); c.insert("value", QVariant(QString(cookie.value()))); c.insert("domain", QVariant(cookie.domain())); c.insert("path", QVariant(cookie.path())); c.insert("httponly", QVariant(cookie.isHttpOnly())); c.insert("issecure", QVariant(cookie.isSecure())); c.insert("issessioncookie", QVariant(cookie.isSessionCookie())); if (cookie.expirationDate().isValid()) { c.insert("expirationdate", QVariant(cookie.expirationDate())); } else { c.insert("expirationdate", QVariant()); } list.append(c); } return QVariant(list); } void OxideCookieHelper::setOxideStoreBackend(QObject* backend) { Q_D(OxideCookieHelper); if (d->m_backend == backend) return; if (d->m_backend) { QObject::disconnect(d->m_backend, 0, this, 0); } d->m_backend = backend; if (backend) { QObject::connect(backend, SIGNAL(setCookiesResponse(int, const QVariant&)), d, SLOT(oxideCookiesUpdated(int, const QVariant&))); } Q_EMIT oxideStoreBackendChanged(); } QObject* OxideCookieHelper::oxideStoreBackend() const { Q_D(const OxideCookieHelper); return d->m_backend; } void OxideCookieHelper::setCookies(const QList& cookies) { Q_D(OxideCookieHelper); d->setCookies(cookies); } #include "oxide-cookie-helper.moc" ./src/app/webcontainer/SplashScreen.qml0000644000004100000410000000350513004613604020377 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.OnlineAccounts 0.1 Rectangle { id: root property string applicationName property alias iconSource: icon.source default property alias contents: contentsHolder.data anchors.fill: parent Flickable { anchors.fill: parent contentHeight: Math.max(contentItem.childrenRect.height, height) Column { anchors { left: parent.left right: parent.right verticalCenter: parent.verticalCenter } spacing: units.gu(2) Icon { id: icon anchors.horizontalCenter: parent.horizontalCenter width: units.gu(10) height: width } Label { anchors.horizontalCenter: parent.horizontalCenter fontSize: "x-large" text: root.applicationName } Item { id: contentsHolder anchors.left: parent.left anchors.right: parent.right height: childrenRect.height } } } } ./src/app/webcontainer/url-pattern-utils.cpp0000644000004100000410000001565313004613604021420 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "url-pattern-utils.h" #include #include #include namespace { /** * Tests for the validity of a given webapp url pattern. It follows * the following grammar: * * := :// * := 'http' | 'https' * := '*' + + * := '.' + * := '/' * * @param pattern pattern that is to be tested for validity * @return true if the url is valid, false otherwise */ bool isValidWebappUrlPattern(const QString& pattern) { static QRegularExpression grammar("^http(s|s\\?)?://[^\\.]+\\.[^\\.\\*\\?]+\\.[^\\.\\*\\?]+(\\.[^\\.\\*\\?/]+)*/.*$"); return grammar.match(pattern).hasMatch(); } /** * A Google related webapp is treated very specifically. The issue * with those Google service related webapps is that the all rely * on a common authentication mechanism and one endup being redirected * to the auth urls automatically when needed. * Most of the redirections can be match against discrete url patterns * but there is always a country dependant step in the auth patterns, e.g. * https://accounts.google.com/* or https://accounts.google.ca/* etc. * * Since by default we dont allow a wildcard in the TLD position, the only * solution then is to manually enter the list of top level country related * domains. * In this context, we isolate the Google case, with a more restricted set of patterns * that should adhere to the following grammar: * * := :// * := 'http' | 'https' * := + '.google' * := '.' ( | ) * := ('com' | 'co') '.' * := + * := '/' * * So for example we allow 'https://accounts.google.* /' but not 'https://*.google.* /' * (the spaces are there to avoid the confusion w/ a end of comment token. * * IMPORTAN NOTE: the '*' wildcard in the TLD position is not a REAL wildcard in the * sense that it corresponds to [^\\./], so it wont match ('google.com.evildomain') as usual. * * The non-terminal in the grammar above comes from: * * https://en.wikipedia.org/wiki/List_of_Google_domains * * @param pattern pattern that is to be tested for validity * @return true if the url is valid, false otherwise */ bool isValidGoogleUrlPattern(const QString& pattern) { static QRegularExpression grammar("^http(s|s\\?)?://[^\\.\\?\\*]+\\.google\\.((com|co)\\.)?[^\\.\\?]+/.*$"); return grammar.match(pattern).hasMatch(); } /* A strict URL pattern has a strict domain name with no wildcard pattern, * and possibly regular path and protocol globs. * * @param pattern pattern that is to be tested for validity * @return true if the url is valid, false otherwise */ bool isValidStrictUrlPattern(const QString& pattern) { static QRegularExpression grammar("^http(s|s\\?)?://[^\\.\\*\\?]+\\.[^\\.\\*\\?]+(\\.[^\\.\\*\\?/]+)*/.*$"); return grammar.match(pattern).hasMatch(); } QString toSafeHostnamePartPattern(const QString& hostnamePart) { QString localHostnamePart = hostnamePart; return localHostnamePart.replace("*", "[^\\./]*"); } QString toSafeUrlPathPartPattern(const QString& urlPathPart) { QString localUrlPathPart = urlPathPart; return localUrlPathPart.replace("*", "[^\\s]*"); } } // namespace { QString UrlPatternUtils::transformWebappSearchPatternToSafePattern( const QString& pattern, bool doTransformUrlPath) { QString transformedPattern; if (isValidWebappUrlPattern(pattern) || isValidStrictUrlPattern(pattern)) { QRegularExpression urlRe("(.+://)([^/]+)(.+)"); QRegularExpressionMatch match = urlRe.match(pattern); if (match.hasMatch()) { // We make a distinction between the wildcard found in the // hostname part and the one found later. The former being more // restricted and should not be replaced by the same regexp pattern // as the latter. // A less restrictive hostname pattern might lead to the following // situation where e.g. // http://bady.guy.com/phishing.ebay.com/ // matches // https?://*.ebay.com/* QString scheme = match.captured(1); QString hostname = toSafeHostnamePartPattern(match.captured(2)); QString tail = doTransformUrlPath ? toSafeUrlPathPartPattern(match.captured(3)) : match.captured(3); // reconstruct transformedPattern = QString("%1%2%3").arg(scheme).arg(hostname).arg(tail); } } else if (isValidGoogleUrlPattern(pattern)) { QRegularExpression urlRe("(.+://)([^\\./]+\\.google\\.)([^/]+)(.+)"); QRegularExpressionMatch match = urlRe.match(pattern); if (match.hasMatch()) { QString scheme = match.captured(1); QString hostname = match.captured(2); QString tld = match.captured(3).replace("*", "[^\\./]*"); QString tail = toSafeUrlPathPartPattern(match.captured(4)); // reconstruct transformedPattern = QString("%1%2%3%4") .arg(scheme) .arg(hostname) .arg(tld) .arg(tail); } } return transformedPattern; } QStringList UrlPatternUtils::filterAndTransformUrlPatterns(const QStringList & includePatterns) { QStringList patterns; Q_FOREACH(const QString& includePattern, includePatterns) { QString pattern = includePattern.trimmed(); if (pattern.isEmpty()) continue; QString safePattern = transformWebappSearchPatternToSafePattern(pattern); if ( ! safePattern.isEmpty()) { patterns.append(safePattern); } else { qDebug() << "Ignoring empty or invalid webapp URL pattern:" << pattern; } } return patterns; } bool UrlPatternUtils::isLocalHtml5ApplicationHomeUrl(const QUrl& url) { return url.isLocalFile(); } ./src/app/webcontainer/session-utils.h0000644000004100000410000000151413004613604020262 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 SESSIONUTILS_H #define SESSIONUTILS_H #include namespace SessionUtils { bool firstRun(const QString &webappName); } #endif // SESSIONUTILS_H ./src/app/webcontainer/webapp-container-helper.h0000644000004100000410000000264113004613604022156 0ustar www-datawww-data/* * Copyright 2014, 2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 WEBAPPCONTAINERHELPER_H #define WEBAPPCONTAINERHELPER_H #include #include class WebappContainerHelper : public QObject { Q_OBJECT public: WebappContainerHelper(QObject* parent = 0); ~WebappContainerHelper(); /** * Expects a CSS color string http://www.w3schools.com/css/css_colors.asp * and returns a rr,gg,bb formatted string. * * If the provided string is not a valid CSS color string, an empty string * is returned. */ Q_INVOKABLE QString rgbColorFromCSSColor(const QString& cssColor); private Q_SLOTS: void browseToUrl(QObject* webview, const QUrl& url); Q_SIGNALS: void browseToUrlRequested(QObject* webview, const QUrl& url); }; #endif // WEBAPPCONTAINERHELPER_H ./src/app/webcontainer/webapp-container-helper.cpp0000644000004100000410000000524513004613604022514 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "webapp-container-helper.h" #include #include #include #include #include WebappContainerHelper::WebappContainerHelper(QObject* parent) : QObject(parent) { connect(this, SIGNAL(browseToUrlRequested(QObject*, const QUrl&)), this, SLOT(browseToUrl(QObject*, const QUrl&)), Qt::QueuedConnection); } WebappContainerHelper::~WebappContainerHelper() { disconnect(this, SIGNAL(browseToUrlRequested(QObject*, const QUrl&)), this, SLOT(browseToUrl(QObject*, const QUrl&))); } void WebappContainerHelper::browseToUrl(QObject* webview, const QUrl& url) { if ( ! webview) { return; } const QMetaObject* metaobject = webview->metaObject(); int urlPropIdx = metaobject->indexOfProperty("url"); if (urlPropIdx == -1) { return; } metaobject->property(urlPropIdx).write(webview, QVariant(url)); } QString WebappContainerHelper::rgbColorFromCSSColor(const QString& cssColor) { QString color = cssColor.trimmed().toLower(); if (color.isEmpty()) { return QString(); } QString returnColorFormat = "%1,%2,%3"; if (color.startsWith("rgb")) { QRegExp rgbColorRe("rgb\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)"); if (rgbColorRe.exactMatch(color)) { return returnColorFormat .arg(rgbColorRe.cap(1).toInt()) .arg(rgbColorRe.cap(2).toInt()) .arg(rgbColorRe.cap(3).toInt()); } return QString(); } else if (color.startsWith("#")) { QString hexColor = color.mid(1); if (hexColor.size() < 6 && hexColor.size() != 3) { color = "#" + QString(6 - hexColor.size(), '0') + hexColor; } } QColor returnColor(color); return returnColor.isValid() ? returnColorFormat .arg(returnColor.red()) .arg(returnColor.green()) .arg(returnColor.blue()) .toLower() : QString(); } ./src/app/webcontainer/ContentPickerDialog.qml0000644000004100000410000000627013004613604021677 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 as Popups import Ubuntu.Content 1.3 import "../MimeTypeMapper.js" as MimeTypeMapper import ".." Component { Popups.PopupBase { id: picker objectName: "contentPickerDialog" // Set the parent at construction time, instead of letting show() // set it later on, which for some reason results in the size of // the dialog not being updated. parent: QuickUtils.rootItem(this) property var activeTransfer Rectangle { anchors.fill: parent ContentTransferHint { anchors.fill: parent activeTransfer: picker.activeTransfer } ContentPeerPicker { id: peerPicker anchors.fill: parent visible: true contentType: ContentType.All handler: ContentHandler.Source onPeerSelected: { if (model.allowMultipleFiles) { peer.selectionType = ContentTransfer.Multiple } else { peer.selectionType = ContentTransfer.Single } picker.activeTransfer = peer.request() stateChangeConnection.target = picker.activeTransfer } onCancelPressed: { model.reject() } } } Connections { id: stateChangeConnection target: null onStateChanged: { if (picker.activeTransfer.state === ContentTransfer.Charged) { var selectedItems = [] for(var i in picker.activeTransfer.items) { selectedItems.push(String(picker.activeTransfer.items[i].url).replace("file://", "")) } model.accept(selectedItems) } } } Component.onCompleted: { if(acceptTypes.length === 1) { var contentType = MimeTypeMapper.mimeTypeToContentType(acceptTypes[0]) if(contentType == ContentType.Unknown) { // If we don't recognise the type, allow uploads from any app contentType = ContentType.All } peerPicker.contentType = contentType } else { peerPicker.contentType = ContentType.All } show() } } } ./src/app/webcontainer/cookie-store.cpp0000644000004100000410000000611513004613604020401 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "cookie-store.h" class CookieStoreRequest : public QObject { Q_OBJECT public: CookieStoreRequest(CookieStore* cookieStore, QObject* parent = 0) : _cookieStore(cookieStore) {} CookieStore* _cookieStore; public Q_SLOTS: void cookiesReceived(const Cookies& cookies) { emit gotCookies(cookies, this); } void cookiesUpdated(bool status) { emit cookiesSet(status); } Q_SIGNALS: void gotCookies(const Cookies& cookies, CookieStoreRequest* request); void cookiesSet(bool status); }; CookieStore::CookieStore(QObject* parent): QObject(parent) { qRegisterMetaType(); qRegisterMetaType >("QList"); qRegisterMetaType("Cookies"); } void CookieStore::getCookies() { doGetCookies(); } void CookieStore::setCookies(const Cookies& cookies) { doSetCookies(cookies); } void CookieStore::doGetCookies() { Q_UNIMPLEMENTED(); } void CookieStore::doSetCookies(const Cookies& cookies) { Q_UNUSED(cookies); Q_UNIMPLEMENTED(); } QDateTime CookieStore::lastUpdateTimeStamp() const { return _lastUpdateTimeStamp; } void CookieStore::updateLastUpdateTimestamp(const QDateTime& timestamp) { _lastUpdateTimeStamp = timestamp; } void CookieStore::cookiesReceived(const Cookies& cookies , CookieStoreRequest* request) { if (Q_UNLIKELY(!request)) return; QDateTime lastRemoteCookieUpdate = request->_cookieStore->lastUpdateTimeStamp(); QDateTime lastLocalCookieUpdate = lastUpdateTimeStamp(); delete request; if (lastRemoteCookieUpdate.isValid() && lastLocalCookieUpdate.isValid() && (lastRemoteCookieUpdate < lastLocalCookieUpdate)) { Q_EMIT moved(false); return; } connect(this, &CookieStore::cookiesSet, this, &CookieStore::moved); setCookies(cookies); } void CookieStore::moveFrom(CookieStore* store) { if (Q_UNLIKELY(!store)) return; CookieStoreRequest* storeRequest = new CookieStoreRequest(store); _currentStoreRequests.insert(storeRequest, true); connect(store, &CookieStore::gotCookies, storeRequest, &CookieStoreRequest::cookiesReceived); connect(storeRequest, &CookieStoreRequest::gotCookies, this, &CookieStore::cookiesReceived); store->getCookies(); } #include "cookie-store.moc" ./src/app/webcontainer/cookie-store.h0000644000004100000410000000360513004613604020047 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __COOKIE_STORE_H__ #define __COOKIE_STORE_H__ #include #include #include #include #include #include typedef QList Cookies; Q_DECLARE_METATYPE(Cookies); class CookieStoreRequest; class CookieStore : public QObject { Q_OBJECT Q_PROPERTY(QDateTime lastUpdateTimeStamp READ lastUpdateTimeStamp \ NOTIFY lastUpdateTimeStampChanged) public: CookieStore(QObject* parent = 0); virtual QDateTime lastUpdateTimeStamp() const; Q_INVOKABLE void getCookies(); Q_INVOKABLE void setCookies(const Cookies& cookies); Q_INVOKABLE void moveFrom(CookieStore* store); Q_SIGNALS: void moved(bool); void lastUpdateTimeStampChanged(); void gotCookies(const Cookies& cookies); void cookiesSet(bool status); private Q_SLOTS: void cookiesReceived(const Cookies& cookies, CookieStoreRequest* request); protected: void updateLastUpdateTimestamp(const QDateTime& timestamp); private: virtual void doGetCookies(); virtual void doSetCookies(const Cookies& Cookies); private: QHash _currentStoreRequests; QDateTime _lastUpdateTimeStamp; }; #endif // __COOKIE_STORE_H__ ./src/app/webcontainer/PopupWindowController.qml0000644000004100000410000002021413004613604022340 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import com.canonical.Oxide 1.0 as Oxide import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import Qt.labs.settings 1.0 Item { id: controller property var webappUrlPatterns property var mainWebappView property var views: [] property bool blockOpenExternalUrls: false property var mediaAccessDialogComponent property bool wide: false // Used to access runtime behavior during tests signal openExternalUrlTriggered(string url) signal newViewCreated(string url) signal windowOverlayOpenAnimationDone() signal initializeOverlayViewsWithUrls(var urls) readonly property int maxSimultaneousViews: 3 Settings { id: webviewOverlayUrlsSettings property string overlayUrls } QtObject { id: internal property var urlPerOverlayView } function updateOverlayUrlsSettings() { var urls = [] webviewOverlayUrlsSettings.overlayUrls = "[]" for (var i in internal.urlPerOverlayView) { urls.push(internal.urlPerOverlayView[i].toString()) } webviewOverlayUrlsSettings.overlayUrls = JSON.stringify(urls) } function onUrlUpdatedForOverlay(overlayView, url) { if (!internal.urlPerOverlayView) { internal.urlPerOverlayView = {} } internal.urlPerOverlayView[overlayView] = url updateOverlayUrlsSettings() } Connections { target: Qt.application onAboutToQuit: { webviewOverlayUrlsSettings.overlayUrls = "[]" } } Component.onCompleted: { if (webviewOverlayUrlsSettings.overlayUrls && webviewOverlayUrlsSettings.overlayUrls.length > 0) { try { var urls = JSON.parse(webviewOverlayUrlsSettings.overlayUrls) if (typeof(urls) === 'object' && urls.length != undefined && urls.length > 0) { initializeOverlayViewsWithUrls(urls) } } catch (e) {} } } function openUrlExternally(url) { if (!blockOpenExternalUrls) { Qt.openUrlExternally(url) } openExternalUrlTriggered(url) } function onOverlayMoved(popup, diffY) { if ((popup.y + diffY) > 0) { popup.y += diffY } } function handleNewViewAdded(view) { if (views.length !== 0) { var topView = views[views.length-1] } views.push(view) view.webviewUrlChanged.connect(function(webviewUrl) { onUrlUpdatedForOverlay(view, webviewUrl) }) } function handleOpenInUrlBrowserForView(url, view) { handleViewRemoved(view) openExternalUrlTriggered(url) openUrlExternally(url) } function createViewSlidingHandlerFor(newView, viewBelow) { var parentHeight = viewBelow.parent.height return function() { if (viewBelow && newView) { viewBelow.opacity = newView.y / parentHeight } } } function topViewOnStack() { if (views.length !== 0) { return views[views.length-1] } return mainWebappView } function handleViewRemoved(view) { if (views.length === 0) { return } var topMostView = views[views.length-1] if (topMostView !== view) { return } views.pop() if (internal.urlPerOverlayView) { delete internal.urlPerOverlayView[topMostView] updateOverlayUrlsSettings() } var parentHeight = topMostView.parent.height var nextView = topViewOnStack() nextView.visible = true function onViewSlidingOut() { if (topMostView.y >= (topMostView.parent.height - 10)) { topMostView.yChanged.disconnect(onViewSlidingOut) topMostView.destroy() updateViewVisibility(nextView, true) } } topMostView.yChanged.connect(onViewSlidingOut) topMostView.y = topMostView.parent.height } function updateViewVisibility(view, visible) { if (view) { view.opacity = visible ? 1.0 : 0.0 } } function createPopupView(parentView, params, isRequestFromMainWebappWebview, context) { var view = popupWebOverlayFactory.createObject( parentView, params); var topMostView = topViewOnStack() // handle opacity updates of the view below this one // when the view is sliding view.yChanged.connect( createViewSlidingHandlerFor(view, topMostView)) function onViewSlidingIn() { var parentHeight = view.parent.height if (view.y <= 10) { view.yChanged.disconnect(onViewSlidingIn) updateViewVisibility(topMostView, false) } } view.yChanged.connect(onViewSlidingIn) view.y = 0 handleNewViewAdded(view) newViewCreated(view.url) } function createPopupViewForRequest(parentView, request, isRequestFromMainWebappWebview, context) { createPopupView(parentView, { request: request, webContext: context, popupWindowController: controller, mediaAccessDialogComponent: mediaAccessDialogComponent }, isRequestFromMainWebappWebview, context) } function createPopupViewForUrl(parentView, overlayUrl, isRequestFromMainWebappWebview, context) { createPopupView(parentView, { url: overlayUrl, webContext: context, popupWindowController: controller, mediaAccessDialogComponent: mediaAccessDialogComponent }, isRequestFromMainWebappWebview, context) } Component { id: popupWebOverlayFactory PopupWindowOverlay { id: overlay height: parent.height width: parent.width wide: controller.wide y: overlay.parent.height // Poor mans heuristic to know when an overlay has been // loaded and is in full view. We cannot rely on the // NumberAnimation running/started since they dont // work properly when inside a Behavior onYChanged: { if (y === 0) { windowOverlayOpenAnimationDone() } } Behavior on y { NumberAnimation { duration: 500 easing.type: Easing.InOutQuad } } } } function handleNewForegroundNavigationRequest( url, request, isRequestFromMainWebappWebview) { if (views.length >= maxSimultaneousViews) { request.action = Oxide.NavigationRequest.ActionReject // Default to open externally, maybe should present a dialog openUrlExternally(url.toString()) console.log("Maximum number of popup overlay opened, opening: " + url + " in the browser") return false } return true } } ./src/app/webcontainer/OnlineAccountsController.qml0000644000004100000410000000364513004613604023002 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 Loader { id: root property string providerId: "" property string applicationId: "" property bool accountSwitcher: false property string webappName: "" property url webappIcon signal accountSelected(string accountDataLocation, bool willMoveCookies) signal contextReady() signal quitRequested() function setupWebcontextForAccount(webcontext) { if (item) { item.setupWebcontextForAccount(webcontext) } else { root.contextReady() } } function showAccountSwitcher() { if (item) item.showAccountSwitcher() } Component.onCompleted: { if (providerId.length === 0) { accountSelected("", false) } else { setSource("AccountsPage.qml", { "providerId": providerId, "applicationId": applicationId, "accountSwitcher": accountSwitcher, "webappName": webappName, "webappIcon": webappIcon, }) } } Connections { target: item onAccountSelected: root.accountSelected(accountDataLocation, willMoveCookies) onContextReady: root.contextReady() onQuitRequested: root.quitRequested() } } ./src/app/webcontainer/AccountChooserDialog.qml0000644000004100000410000000677113004613604022054 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Qt.labs.settings 1.0 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 as ListItem import Ubuntu.OnlineAccounts 0.1 import Ubuntu.OnlineAccounts.Client 0.1 SplashScreen { id: root property string providerId: "" property string applicationId: "" property bool accountMandatory: true property var accountsModel: null signal accountSelected(int accountId) signal cancel() property var __selectedAccount: settings.selectedAccount Settings { id: settings property int selectedAccount } Setup { id: setup applicationId: root.applicationId providerId: root.providerId onFinished: { if ("accountId" in reply) { root.chooseAccount(reply.accountId) } else { root.cancel() } } } Column { anchors { left: parent.left; right: parent.right } spacing: units.gu(1) ListItem.Caption { text: i18n.tr("No accounts have been linked to this webapp; press the item below to add an account.") visible: accountsModel.count === 0 } Repeater { model: accountsModel AccountItem { providerName: model.providerName accountName: model.displayName selected: model.accountId === root.__selectedAccount onClicked: root.onConfirmed(model.accountId) } } ListItem.Standard { id: addAccountButton text: i18n.tr("Add account") iconName: "add" selected: root.__selectedAccount === -1 onClicked: root.onConfirmed(-1) } ListItem.Standard { id: skipButton visible: !root.accountMandatory text: i18n.tr("Don't use an account") selected: root.__selectedAccount === -2 onClicked: root.onConfirmed(-2) } Button { anchors.left: parent.left anchors.right: parent.right anchors.margins: units.gu(1) text: i18n.tr("Cancel") onClicked: root.cancel() } } function chooseAccount(accountId) { for (var i = 0; i < accountsModel.count; i++) { if (accountsModel.get(i, "accountId") === accountId) { settings.selectedAccount = accountId root.accountSelected(accountId) return } } // The selected account was not found settings.selectedAccount = -1 root.cancel() } function onConfirmed(account) { if (account === -2) { root.selectedAccount(0) } else if (account === -1) { setup.exec() } else { chooseAccount(account) } } } ./src/app/webcontainer/webapp-specific-page-metadata-collector.js0000644000004100000410000000433113004613604025343 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ (function() { function detectThemeColorMetaInformation() { var theme_color_meta = document.head.querySelector('meta[name="theme-color"]'); if (theme_color_meta) { oxide.sendMessage( 'webapp-specific-page-metadata-detected', { type: 'theme-color', baseurl: document.location.href, theme_color: theme_color_meta.getAttribute('content') }); return true; } return false; } function detectManifestMetaInformation() { var manifest = document.head.querySelector('link[rel="manifest"]'); if (manifest && manifest.getAttribute('href')) { oxide.sendMessage( 'webapp-specific-page-metadata-detected', { type: 'manifest', baseurl: document.location.href, manifest: manifest.href }); return true; } return false; } function extractWebAppMetaInfo() { var detectors = [detectThemeColorMetaInformation, detectManifestMetaInformation] for (var i in detectors) { if (detectors[i]()) { break; } } } extractWebAppMetaInfo(); var MutationObserver = window.MutationObserver || window.WebKitMutationObserver; var observer = new MutationObserver(function(mutations) { extractWebAppMetaInfo(); }); observer.observe(document, {childList: true, subtree: true, attributes: true }); })(); ./src/app/webcontainer/session-utils.cpp0000644000004100000410000000606613004613604020624 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "session-utils.h" #include #include #include #include #include static void createTimestampFile(const QFileInfo ×tampFile) { timestampFile.dir().mkpath("."); QFile file(timestampFile.filePath()); file.open(QIODevice::WriteOnly); } /** * Returns whether this is the first time that the webapp "webappName" is run * in the current user's session. */ bool SessionUtils::firstRun(const QString &webappName) { /* Return true if this is the first time that the webapp "webappName" is * run in the current user's session. */ /* If the application is running confined, the heuristics below won't work, * as the application hasn't access to the upstart session data. In that * case, let's skip all the checks and just return false. */ if (!qEnvironmentVariableIsEmpty("APP_ID")) { return false; } if (Q_UNLIKELY(webappName.isEmpty())) { /* Assume first run */ return true; } QString xdgRuntimeDir(QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation)); QFileInfo timestampFile(QString("%1/webapp-container/%2.stamp"). arg(xdgRuntimeDir).arg(webappName)); if (!timestampFile.exists()) { createTimestampFile(timestampFile); return true; } /* If the file stamp is there, it might be a stale file from a previous * session (XDG_RUNTIME_DIR is cleared only when rebooting, not when * logging out); in order to detect this situation, we compare the time of * the file with the time of when the user session started. * We use the upstart timestamp files to obtain the latter. */ QDir upstartSessionDir(QString("%1/upstart/sessions").arg(xdgRuntimeDir)); upstartSessionDir.setNameFilters(QStringList() << "*.session"); /* We want the newest file there */ upstartSessionDir.setSorting(QDir::Time); QFileInfoList sessionFiles = upstartSessionDir.entryInfoList(); if (sessionFiles.isEmpty()) { /* This shouldn't happen in Unity; play safe and assume it's the first * run */ return true; } const QFileInfo &lastSession = sessionFiles.first(); if (timestampFile.lastModified() < lastSession.lastModified()) { createTimestampFile(timestampFile); return true; } else { return false; } } ./src/app/webcontainer/ContentDownloadDialog.qml0000644000004100000410000000420313004613604022223 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import Ubuntu.Content 1.3 import webbrowsercommon.private 0.1 import ".." Component { PopupBase { id: downloadDialog objectName: "downloadDialog" anchors.fill: parent property var activeTransfer property var downloadId property var singleDownload property var mimeType property var filename property var icon: MimeDatabase.iconForMimetype(mimeType) property alias contentType: peerPicker.contentType ContentPeerModel { id: peerModel handler: ContentHandler.Destination contentType: downloadDialog.contentType } Rectangle { id: pickerRect anchors.fill: parent visible: true ContentPeerPicker { id: peerPicker handler: ContentHandler.Destination objectName: "contentPeerPicker" visible: parent.visible onPeerSelected: { activeTransfer = peer.request() activeTransfer.downloadId = downloadDialog.downloadId activeTransfer.state = ContentTransfer.Downloading PopupUtils.close(downloadDialog) } onCancelPressed: { PopupUtils.close(downloadDialog) } } } } } ./src/app/webcontainer/WebViewImplOxide.qml0000644000004100000410000002576713004613623021207 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import QtQuick.Window 2.2 import com.canonical.Oxide 1.8 as Oxide import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import Ubuntu.UnityWebApps 0.1 as UnityWebApps import Ubuntu.Web 0.2 import ".." WebappWebview { id: webview property bool developerExtrasEnabled: false property string webappName: "" property string localUserAgentOverride: "" property var webappUrlPatterns: null property string popupRedirectionUrlPrefixPattern: "" property url dataPath property var popupController property var overlayViewsParent: webview.parent property var mediaAccessDialogComponent property bool openExternalUrlInOverlay: false property bool popupBlockerEnabled: true // Mostly used for testing & avoid external urls to // "leak" in the default browser. External URLs corresponds // to URLs that are not included in the set defined by the url patterns // (if any) or navigations resulting in new windows being created. property bool blockOpenExternalUrls: false signal samlRequestUrlPatternReceived(string urlPattern) signal themeColorMetaInformationDetected(string theme_color) // Those signals are used for testing purposes to externally // track down the various internal logic & steps of a popup lifecycle. signal openExternalUrlTriggered(string url) signal gotRedirectionUrl(string url) property bool runningLocalApplication: false onLoadEvent: { var url = event.url.toString() if (event.type === Oxide.LoadEvent.TypeRedirected && url.indexOf("SAMLRequest") > 0) { handleSamlRequestNavigation(url) } } function openOverlayForUrl(overlayUrl) { if (popupController) { popupController.createPopupViewForUrl( overlayViewsParent, overlayUrl, true, context) } } currentWebview: webview context: WebContext { dataPath: webview.dataPath userAgent: localUserAgentOverride ? localUserAgentOverride : defaultUserAgent popupBlockerEnabled: webview.popupBlockerEnabled userScripts: [ Oxide.UserScript { context: "oxide://webapp-specific-page-metadata-collector/" url: Qt.resolvedUrl("webapp-specific-page-metadata-collector.js") incognitoEnabled: false matchAllFrames: false } ] } Component.onCompleted: webappSpecificMessageHandler.createObject( webview, { msgId: "webapp-specific-page-metadata-detected", contexts: ["oxide://webapp-specific-page-metadata-collector/"], callback: function(msg, frame) { handlePageMetadata(msg.args) } }); Component { id: webappSpecificMessageHandler Oxide.ScriptMessageHandler { } } onOpenUrlExternallyRequested: openUrlExternally(url, false) preferences.allowFileAccessFromFileUrls: runningLocalApplication preferences.allowUniversalAccessFromFileUrls: runningLocalApplication preferences.localStorageEnabled: true preferences.appCacheEnabled: true onNewViewRequested: popupController.createPopupViewForRequest(overlayViewsParent, request, true, context) Connections { target: webview.visible ? webview : null /** * We are only connecting to the mediaAccessPermission signal if we are * the only webview currently visible (no overlay present). In the case of an overlay * and the absence of a signal handler in this main view, oxide's default behavior * is triggered and the request is denied. * * See the browser's webbrowser/Browser.qml source for additional comments. */ onMediaAccessPermissionRequested: PopupUtils.open(mediaAccessDialogComponent, null, { request: request }) } StateSaver.properties: "url" StateSaver.enabled: !runningLocalApplication function handleSAMLRequestPattern(urlPattern) { webappUrlPatterns.push(urlPattern) samlRequestUrlPatternReceived(urlPattern) } function isRunningAsANamedWebapp() { return webview.webappName && typeof(webview.webappName) === 'string' && webview.webappName.length != 0 } function haveValidUrlPatterns() { return webappUrlPatterns && webappUrlPatterns.length !== 0 } function isNewForegroundWebViewDisposition(disposition) { return disposition === Oxide.NavigationRequest.DispositionNewPopup || disposition === Oxide.NavigationRequest.DispositionNewForegroundTab; } function openUrlExternally(url, isTriggeredByUserNavigation) { if (openExternalUrlInOverlay && isTriggeredByUserNavigation) { popupController.createPopupViewForUrl(overlayViewsParent, url, true, context) return } else { webview.openExternalUrlTriggered(url) if (! webview.blockOpenExternalUrls) { Qt.openUrlExternally(url) } } } function shouldAllowNavigationTo(url) { // The list of url patterns defined by the webapp takes precedence over command line if (isRunningAsANamedWebapp()) { if (unityWebapps.model.exists(unityWebapps.name) && unityWebapps.model.doesUrlMatchesWebapp(unityWebapps.name, url)) { return true; } } // We still take the possible additional patterns specified in the command line // (the in the case of finer grained ones specifically for the container and not // as an 'install source' for the webapp). if (haveValidUrlPatterns()) { for (var i = 0; i < webappUrlPatterns.length; ++i) { var pattern = webappUrlPatterns[i] if (url.match(pattern)) { return true; } } } return false; } function handleSamlRequestNavigation(url) { var urlRegExp = new RegExp("https?://([^?/]+)") var match = urlRegExp.exec(url) var host = match[1] var escapeDotsRegExp = new RegExp("\\.", "g") var hostPattern = "https?://" + host.replace(escapeDotsRegExp, "\\.") + "/*" console.log("SAML request detected. Adding host pattern: " + hostPattern) handleSAMLRequestPattern(hostPattern) } function navigationRequestedDelegate(request) { var url = request.url.toString() if (runningLocalApplication && url.indexOf("file://") !== 0) { request.action = Oxide.NavigationRequest.ActionReject openUrlExternally(url, true) return } request.action = Oxide.NavigationRequest.ActionReject if (isNewForegroundWebViewDisposition(request.disposition)) { var shouldAcceptRequest = popupController.handleNewForegroundNavigationRequest( url, request, true); if (shouldAcceptRequest) { request.action = Oxide.NavigationRequest.ActionAccept } return } // Pass-through if we are not running as a named webapp (--webapp='Gmail') // or if we dont have a list of url patterns specified to filter the // browsing actions if ( ! webview.haveValidUrlPatterns() && ! webview.isRunningAsANamedWebapp()) { request.action = Oxide.NavigationRequest.ActionAccept return } if (webview.shouldAllowNavigationTo(url)) request.action = Oxide.NavigationRequest.ActionAccept // SAML requests are used for instance by Google Apps for your domain; // they are implemented as a HTTP redirect to a URL containing the // query parameter called "SAMLRequest". // Besides letting the request through, we must also add the SAML // domain to the list of the allowed hosts. if (request.disposition === Oxide.NavigationRequest.DispositionCurrentTab && url.indexOf("SAMLRequest") > 0) { handleSamlRequestNavigation(url) request.action = Oxide.NavigationRequest.ActionAccept } if (request.action === Oxide.NavigationRequest.ActionReject) { console.debug('Opening: ' + url + ' in the browser window.') openUrlExternally(url, true) } } // Small shim needed when running as a webapp to wire-up connections // with the webview (message received, etc…). // This is being called (and expected) internally by the webapps // component as a way to bind to a webview lookalike without // reaching out directly to its internals (see it as an interface). function getUnityWebappsProxies() { var eventHandlers = { onAppRaised: function () { if (webbrowserWindow) { try { webbrowserWindow.raise(); } catch (e) { console.debug('Error while raising: ' + e); } } } }; return UnityWebAppsUtils.makeProxiesForWebViewBindee(webview, eventHandlers) } function handlePageMetadata(metadata) { if (metadata.type === 'manifest') { var request = new XMLHttpRequest(); request.onreadystatechange = function() { if (request.readyState === XMLHttpRequest.DONE) { try { var manifest = JSON.parse(request.responseText); if (manifest['theme_color'] && manifest['theme_color'].length !== 0) { themeColorMetaInformationDetected(manifest['theme_color']) } } catch(e) {} } } request.open("GET", metadata.manifest); request.send(); } else if (metadata.type === 'theme-color') { if (metadata['theme_color'] && metadata['theme_color'].length !== 0) { themeColorMetaInformationDetected(metadata['theme_color']) } } } } ./src/app/webcontainer/oxide-cookie-helper.h0000644000004100000410000000322613004613604021277 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 OXIDE_COOKIE_HELPER_H #define OXIDE_COOKIE_HELPER_H #include #include #include #include class OxideCookieHelperPrivate; class OxideCookieHelper : public QObject { Q_OBJECT Q_PROPERTY(QObject* oxideStoreBackend READ oxideStoreBackend \ WRITE setOxideStoreBackend NOTIFY oxideStoreBackendChanged) public: OxideCookieHelper(QObject* parent = 0); // oxideStoreBackend void setOxideStoreBackend(QObject* backend); QObject* oxideStoreBackend() const; static QList cookiesFromVariant(const QVariant& cookies); static QVariant variantFromCookies(const QList& cookies); public Q_SLOTS: void setCookies(const QList& cookies); Q_SIGNALS: void oxideStoreBackendChanged(); void cookiesSet(const QList& failedCookies); private: OxideCookieHelperPrivate* d_ptr; Q_DECLARE_PRIVATE(OxideCookieHelper) }; #endif // OXIDE_COOKIE_HELPER_H ./src/app/webcontainer/scheme-filter.h0000644000004100000410000000261713004613604020175 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 _SCHEME_FILTER_H_ #define _SCHEME_FILTER_H_ #include #include #include #include #include class SchemeFilterPrivate; /** * @brief The SchemeFilter class */ class SchemeFilter : public QObject { Q_OBJECT public: SchemeFilter(const QMap& content, QObject *parent = 0); ~SchemeFilter(); static QMap parseValidLocalSchemeFilterFile( bool & isValid, const QString& filename); Q_INVOKABLE QVariantMap applyFilter(const QUrl& uri); Q_INVOKABLE bool hasFilterFor(const QUrl& uri); private: SchemeFilterPrivate* d_ptr; Q_DECLARE_PRIVATE(SchemeFilter) }; #endif // _SCHEME_FILTER_H_ ./src/app/webcontainer/ColorUtils.js0000644000004100000410000000507513004613604017733 0ustar www-datawww-data/* * Copyright 2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * Returns the color which has the most contrast with a base color. * * The contrast is based on the contrast ration found definition found here: * https://www.w3.org/TR/WCAG20/#contrast-ratiodef * * @param baseColor a string with format ,, corresponding to the base color * @param darkenedBaseColor the darker version of the base color * @param lightenedBaseColor the lighter version of the base color * @return defaultLightenColor or defaultDarkenColor depending on better contrast * color string for special cases ("white", "black") */ function getMostConstrastedColor( baseColor, darkenedBaseColor, lightenedBaseColor) { function toLuminanceFactor(cc) { return (cc <= 0.03928) ? (cc / 12.92) : Math.pow(((cc + 0.055) / 1.055), 2.4) } function getRelativeLuminance(c) { return 0.2126 * toLuminanceFactor(c.r) + 0.7152 * toLuminanceFactor(c.g) + 0.0722 * toLuminanceFactor(c.b) } function getContrastRatio(lighterColorLuminance, darkerColorLuminance) { return (lighterColorLuminance + 0.05) / (darkerColorLuminance + 0.05) } var components = baseColor.split(",") // special case for black if (components[0].trim() === "0" && components[1].trim() === "0" && components[2].trim() === "0") { return "white" } if (components[0].trim() === "255" && components[1].trim() === "255" && components[2].trim() === "255") { return "black" } var color = { r: parseInt(components[0])/255, g: parseInt(components[1])/255, b: parseInt(components[2])/255 } var CONTRAST_LIGHT_ITEM_THRESHOLD = 3.0 if (getContrastRatio(0.0, getRelativeLuminance(color)) >= CONTRAST_LIGHT_ITEM_THRESHOLD) { return darkenedBaseColor } return lightenedBaseColor } ./src/app/webcontainer/AccountsPage.qml0000644000004100000410000001000113004613604020346 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.OnlineAccounts 0.1 import webcontainer.private 0.1 Item { id: root property alias providerId: accountsLogic.providerId property alias applicationId: accountsLogic.applicationId property alias accountSwitcher: accountsLogic.accountSwitcher property string webappName: "" property url webappIcon signal accountSelected(string accountDataLocation, bool willMoveCookies) signal contextReady() signal quitRequested() property string __providerName: providerId property string __lastAccountDataLocation: "" anchors.fill: parent AccountsLogic { id: accountsLogic onSplashScreenRequested: showSplashScreen() onAccountSelected: root.__emitAccountSelected(credentialsId, willMoveCookies) onContextReady: root.contextReady() onQuitRequested: root.quitRequested() onErrorScreenRequested: showErrorScreen(message) } AccountsSplashScreen { id: splashScreen visible: false providerName: root.__providerName applicationName: root.webappName iconSource: root.webappIcon onChooseAccount: root.showAccountSwitcher() onQuitRequested: root.quitRequested() onSkip: accountsLogic.proceedWithNoAccount() } // Only temporarily used for bug https://bugs.launchpad.net/bugs/1441873 AccountErrorScreen { id: errorScreen visible: false onClosed: root.quitRequested() } Loader { id: accountChooserLoader anchors.fill: parent } Component { id: accountChooserComponent AccountChooserDialog { id: accountChooser applicationName: root.webappName iconSource: root.webappIcon providerId: root.providerId applicationId: root.applicationId accountsModel: accountsLogic.accountsModel onCancel: { accountChooserLoader.sourceComponent = null root.accountSelected(root.__lastAccountDataLocation, false) } onAccountSelected: { accountChooserLoader.sourceComponent = null accountsLogic.setupAccount(accountId) } } } ProviderModel { id: providerModel applicationId: root.applicationId } function __setupProviderData() { for (var i = 0; i < providerModel.count; i++) { if (providerModel.get(i, "providerId") === root.providerId) { root.__providerName = providerModel.get(i, "displayName") break } } } Component.onCompleted: { __setupProviderData() } function showErrorScreen(message) { errorScreen.message = message errorScreen.visible = true } function showSplashScreen() { splashScreen.visible = true } function showAccountSwitcher() { accountChooserLoader.sourceComponent = accountChooserComponent } function __emitAccountSelected(credentialsId, willMoveCookies) { __lastAccountDataLocation = credentialsId > 0 ? ("/id-" + credentialsId) : "" accountSelected(__lastAccountDataLocation, willMoveCookies) } function setupWebcontextForAccount(webcontext) { accountsLogic.setupWebcontextForAccount(webcontext) } } ./src/app/webcontainer/AccountsLogic.qml0000644000004100000410000001637513004613604020553 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Qt.labs.settings 1.0 import Ubuntu.OnlineAccounts 0.1 import Ubuntu.OnlineAccounts.Client 0.1 import webcontainer.private 0.1 Item { id: root property alias providerId: accountsModelObject.provider property alias applicationId: accountsModelObject.applicationId property bool accountSwitcher: false property var accountsModel: accountsModelObject signal splashScreenRequested() signal errorScreenRequested(string message) signal accountSelected(int credentialsId, bool willMoveCookies) signal contextReady() signal quitRequested() property var __account: null property int __credentialsId: __account ? __account.authData.credentialsId : 0 Timer { id: checkTimer running: true repeat: false onTriggered: checkAccounts() interval: 100 } AccountServiceModel { id: accountsModelObject } // This is only used if accountSwitcher is false Setup { id: setup applicationId: root.applicationId providerId: root.providerId onFinished: { if ("accountId" in reply) { root.checkAccounts() } else if ("errorName" in reply) { root.errorScreenRequested(i18n.tr("Account window could not be opened. You can only create one account at a time; please try again after closing all other account windows.")) } else { root.quitRequested() } } } Settings { id: settings property int selectedAccount: -1 property string initializedAccounts: "[]" } Component { id: accountComponent AccountService { } } Component { id: onlineAccountStoreComponent OnlineAccountsCookieStore { } } Component { id: oxideCookieStoreComponent ChromeCookieStore { } } function checkAccounts() { checkTimer.stop() console.log("Accounts: " + accountsModel.count) /* If account switching is not supported, we just pick the first * account here. */ if (!accountSwitcher) { if (accountsModel.count === 0) { setup.exec() } else { settings.selectedAccount = accountsModel.get(0, "accountId") setupAccount(settings.selectedAccount) } return } if (accountsModel.count === 0) { settings.selectedAccount = -1 } else if (settings.selectedAccount > 0) { // check that the account exists for (var i = 0; i < accountsModel.count; i++) { if (accountsModel.get(i, "accountId") === settings.selectedAccount) { break; } } if (i >= accountsModel.count) { // The selected account was not found settings.selectedAccount = -1 } } if (settings.selectedAccount < 0) { splashScreenRequested() } else { setupAccount(settings.selectedAccount) } } function proceedWithNoAccount() { __account = null settings.selectedAccount = 0 accountSelected(__credentialsId, false) } function setupAccount(accountId) { console.log("Setup account " + accountId) if (__account && accountId === __account.accountId) { console.log("Same as current account") accountSelected(__credentialsId, false) return } __account = null for (var i = 0; i < accountsModel.count; i++) { if (accountsModel.get(i, "accountId") === accountId) { var accountHandle = accountsModel.get(i, "accountServiceHandle") __account = accountComponent.createObject(root, { objectHandle: accountHandle }) break; } } console.log("Credentials ID: " + __credentialsId) settings.selectedAccount = accountId accountSelected(__credentialsId, mustMoveCookies(accountId)) } function login(account, callback) { console.log("Preparing for login") function authenticatedCallback() { console.log("Authenticated!") account.authenticated.disconnect(authenticatedCallback) callback(true) } account.authenticated.connect(authenticatedCallback) function errorCallback() { console.log("Authentication error!") account.authenticationError.disconnect(errorCallback) callback(false) } account.authenticationError.connect(errorCallback) account.authenticate(null) } function mustMoveCookies(accountId) { var initializedAccounts try { initializedAccounts = JSON.parse(settings.initializedAccounts) } catch(e) { initializedAccounts = [] } return initializedAccounts.indexOf(accountId) < 0 } function rememberCookiesMoved(accountId) { var initializedAccounts = JSON.parse(settings.initializedAccounts) if (initializedAccounts.indexOf(accountId) < 0) { initializedAccounts.push(accountId) settings.initializedAccounts = JSON.stringify(initializedAccounts) } } function onCookiesMoved(result) { if (!result) { console.log("Cookies were not moved") } else { console.log("cookies moved") } // Even if the cookies were not moved, we don't want to retry rememberCookiesMoved(__account.accountId) contextReady() } function setupWebcontextForAccount(webcontext) { if (!__account || !mustMoveCookies(__account.accountId)) { contextReady() return } login(__account, function(authenticated) { if (!authenticated) { errorScreenRequested(i18n.it("Authentication failed")) } else { console.log("Authentication succeeded, moving cookies") var accountsCookieStore = onlineAccountStoreComponent.createObject(root, { "accountId": __credentialsId }) var webappCookieStore = oxideCookieStoreComponent.createObject(root, { "oxideStoreBackend": webcontext.cookieManager, "dbPath": webcontext.dataPath + "/cookies.sqlite" }) webappCookieStore.moved.connect(onCookiesMoved) webappCookieStore.moveFrom(accountsCookieStore) } }) } } ./src/app/webcontainer/AccountErrorScreen.qml0000644000004100000410000000253613004613604021556 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 Rectangle { id: root property alias message: messageLabel.text signal closed() anchors.fill: parent Column { anchors.fill: parent anchors.margins: units.gu(4) spacing: units.gu(3) Label { width: parent.width fontSize: "x-large" text: i18n.tr("Account error") } Label { id: messageLabel width: parent.width wrapMode: Text.Wrap } Button { anchors.horizontalCenter: parent.horizontalCenter text: i18n.tr("Close") onClicked: root.closed() } } } ./src/app/webcontainer/intent-parser.cpp0000644000004100000410000000526613004613604020577 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "intent-parser.h" #include #include namespace { const char INTENT_SCHEME_STRING[] = "intent"; const char INTENT_START_FRAGMENT_TAG[] = "Intent"; const char INTENT_URI_PACKAGE_PREFIX[] = "package="; const char INTENT_URI_ACTION_PREFIX[] = "action="; const char INTENT_URI_CATEGORY_PREFIX[] = "category="; const char INTENT_URI_COMPONENT_PREFIX[] = "component="; const char INTENT_URI_SCHEME_PREFIX[] = "scheme="; const char INTENT_END_FRAGMENT_TAG[] = ";end"; void trimUriSeparator(QString& uriComponent) { uriComponent.remove(QRegExp("^/*")).remove(QRegExp("/*$")); } } IntentUriDescription parseIntentUri(const QUrl& intentUri) { IntentUriDescription result; if (intentUri.scheme() != INTENT_SCHEME_STRING || !intentUri.fragment().startsWith(INTENT_START_FRAGMENT_TAG) || !intentUri.fragment().endsWith(INTENT_END_FRAGMENT_TAG)) { return result; } QString host = intentUri.host(); trimUriSeparator(host); QString path = intentUri.path(); if (intentUri.hasQuery()) { path += "?" + intentUri.query(); trimUriSeparator(path); } result.host = host; result.uriPath = path; QStringList infos = intentUri.fragment().split(";"); Q_FOREACH(const QString& info, infos) { if (info.startsWith(INTENT_URI_PACKAGE_PREFIX)) { result.package = info.split(INTENT_URI_PACKAGE_PREFIX)[1]; } else if (info.startsWith(INTENT_URI_ACTION_PREFIX)) { result.action = info.split(INTENT_URI_ACTION_PREFIX)[1]; } else if (info.startsWith(INTENT_URI_CATEGORY_PREFIX)) { result.category = info.split(INTENT_URI_CATEGORY_PREFIX)[1]; } else if (info.startsWith(INTENT_URI_COMPONENT_PREFIX)) { result.component = info.split(INTENT_URI_COMPONENT_PREFIX)[1]; } else if (info.startsWith(INTENT_URI_SCHEME_PREFIX)) { result.scheme = info.split(INTENT_URI_SCHEME_PREFIX)[1]; } } return result; } ./src/app/webcontainer/PopupWindowOverlay.qml0000644000004100000410000001634613004613604021651 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import QtQuick.Window 2.2 import com.canonical.Oxide 1.4 as Oxide import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import ".." Item { id: popup property var popupWindowController property var webContext property alias currentWebview: popupWebview property alias request: popupWebview.request property alias url: popupWebview.url property var mediaAccessDialogComponent property alias wide: popupWebview.wide signal webviewUrlChanged(url webviewUrl) Rectangle { color: "#F2F1F0" anchors.fill: parent } Item { id: menubar height: units.gu(6) width: parent.width anchors { top: parent.top horizontalCenter: parent.horizontalCenter } ChromeButton { id: closeButton objectName: "overlayCloseButton" anchors { left: parent.left verticalCenter: parent.verticalCenter } height: parent.height width: height iconName: "dropdown-menu" iconSize: 0.6 * height enabled: true visible: true MouseArea { anchors.fill: parent onClicked: { if (popupWindowController) { popupWindowController.handleViewRemoved(popup) } } } } Item { anchors { top: parent.top bottom: parent.bottom left: closeButton.right right: buttonOpenInBrowser.left } Label { anchors { rightMargin: units.gu(2) verticalCenter: parent.verticalCenter left: parent.left right: parent.right } text: popupWebview.title ? popupWebview.title : popupWebview.url elide: Text.ElideRight } MouseArea { anchors.fill: parent property int initMouseY: 0 property int prevMouseY: 0 onPressed: { initMouseY = mouse.y prevMouseY = initMouseY } onReleased: { if ((prevMouseY - initMouseY) > (popup.height / 8) || popup.y > popup.height/2) { if (popupWindowController) { popupWindowController.handleViewRemoved(popup) return } } popup.y = 0 } onMouseYChanged: { if (popupWindowController) { var diff = mouseY - initMouseY prevMouseY = mouseY popupWindowController.onOverlayMoved(popup, diff) } } } } ChromeButton { id: buttonOpenInBrowser objectName: "overlayButtonOpenInBrowser" anchors { right: parent.right verticalCenter: parent.verticalCenter rightMargin: units.gu(1) } height: parent.height width: height iconName: "external-link" iconSize: 0.6 * height enabled: true visible: true MouseArea { anchors.fill: parent onClicked: { if (popupWindowController) { popupWindowController.handleOpenInUrlBrowserForView( popupWebview.url, popup) } } } } } WebappWebview { id: popupWebview objectName: "overlayWebview" context: webContext onUrlChanged: webviewUrlChanged(popupWebview.url) Connections { target: popupWebview.visible ? popupWebview : null /** * We are only connecting to the mediaAccessPermission signal if we are the currently * visible overlay. If other overlays slide over this one, oxide will deny (by default) * all media access requests for this overlay. * * See the browser's webbrowser/Browser.qml source for additional comments. */ onMediaAccessPermissionRequested: PopupUtils.open(mediaAccessDialogComponent, null, { request: request }) } onOpenUrlExternallyRequested: { if (popupWindowController) { popupWindowController.openUrlExternally(url) } } anchors { bottom: parent.bottom left: parent.left right: parent.right top: menubar.bottom } onNewViewRequested: { if (popupWindowController) { popupWindowController.createPopupViewForRequest( popup.parent, request, false, context) } } function isNewForegroundWebViewDisposition(disposition) { return disposition === Oxide.NavigationRequest.DispositionNewPopup || disposition === Oxide.NavigationRequest.DispositionNewForegroundTab; } onNavigationRequested: { var url = request.url.toString() request.action = Oxide.NavigationRequest.ActionAccept if (isNewForegroundWebViewDisposition(request.disposition)) { var shouldAcceptRequest = popupWindowController.handleNewForegroundNavigationRequest( url, request, false); if (!shouldAcceptRequest) { request.action = Oxide.NavigationRequest.ActionReject } } } onCloseRequested: { if (popupWindowController) { popupWindowController.handleViewRemoved(popup) } } Loader { anchors { fill: popupWebview } active: webProcessMonitor.crashed || (webProcessMonitor.killed && !popupWebview.currentWebview.loading) sourceComponent: SadPage { webview: popupWebview objectName: "overlaySadPage" } WebProcessMonitor { id: webProcessMonitor webview: popupWebview } asynchronous: true } } } ./src/app/UrlUtils.js0000644000004100000410000000474413004613604014741 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ 'use strict'; function removeScheme(url) { var rest = url.toString() var indexOfScheme = rest.indexOf("://") if (indexOfScheme !== -1) { rest = rest.slice(indexOfScheme + 3) } return rest } function extractAuthority(url) { var authority = removeScheme(url) var indexOfPath = authority.indexOf("/") if (indexOfPath !== -1) { authority = authority.slice(0, indexOfPath) } return authority } function extractHost(url) { var host = extractAuthority(url) var indexOfAt = host.indexOf("@") if (indexOfAt !== -1) { host = host.slice(indexOfAt + 1) } var indexOfColon = host.indexOf(":") if (indexOfColon !== -1) { host = host.slice(0, indexOfColon) } return host } function fixUrl(address) { var url = address if (address.toLowerCase() == "about:blank") { return address.toLowerCase() } else if (address.match(/^data:/i)) { return "data:" + address.substr(5) } else if (address.substr(0, 1) == "/") { url = "file://" + address } else if (address.indexOf("://") == -1) { url = "http://" + address } return url } function looksLikeAUrl(address) { if (address.match(/^data:/i)) { return true; } var terms = address.split(/\s/) if (terms.length > 1) { return false } if (address.toLowerCase() == "about:blank") { return true } if (address.substr(0, 1) == "/") { return true } if (address.match(/^https?:\/\//i) || address.match(/^file:\/\//i) || address.match(/^[a-z]+:\/\//i)) { return true } if (address.split('/', 1)[0].match(/\.[a-zA-Z]{2,}$/)) { return true } if (address.split('/', 1)[0].match(/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}/)) { return true } return false } ./src/app/ConfirmDialog.qml0000644000004100000410000000167513004613604016050 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 ModalDialog { title: i18n.tr("JavaScript Confirmation") Button { text: i18n.tr("OK") onClicked: model.accept() } Button { text: i18n.tr("Cancel") onClicked: model.reject() } } ./src/app/Downloader.qml0000644000004100000410000000671513004613604015431 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import Ubuntu.DownloadManager 1.2 import Ubuntu.Content 1.3 import "MimeTypeMapper.js" as MimeTypeMapper import "FileExtensionMapper.js" as FileExtensionMapper Item { id: downloadItem property string filename property string mimeType signal showDownloadDialog(string downloadId, var contentType, var downloader, string filename, string mimeType) Component { id: metadataComponent Metadata { showInIndicator: true } } Component { id: downloadComponent SingleDownload { id: downloader autoStart: false property var contentType property string url onDownloadIdChanged: { showDownloadDialog(downloadId, contentType, downloader, downloadItem.filename, downloadItem.mimeType) } } } function download(url, contentType, headers, metadata) { var properties = {'contentType': contentType, 'metadata': metadata, 'url': url} if (headers) { properties['headers'] = headers } var singleDownload = downloadComponent.createObject(downloadItem, properties) singleDownload.download(url) } function downloadPicture(url, headers) { var metadata = metadataComponent.createObject(downloadItem) downloadItem.mimeType = "image/*" download(url, ContentType.Pictures, headers, metadata) } function downloadMimeType(url, mimeType, headers, filename) { var metadata = metadataComponent.createObject(downloadItem) var contentType = MimeTypeMapper.mimeTypeToContentType(mimeType) if (contentType == ContentType.Unknown && filename) { // If we can't determine the content type from the mime-type // attempt to discover it from the file extension contentType = FileExtensionMapper.filenameToContentType(filename) } if (mimeType == "application/zip" && is7digital(url)) { // This is problably an album download from 7digital (although we // can't be 100% certain). 7digital albums are served as a zip // so we let download manager extract the zip and send its contents // on to the selected application via content-hub contentType = ContentType.Music metadata.extract = true } if (!filename) { filename = url.toString().split("/").pop() } metadata.title = filename downloadItem.filename = filename downloadItem.mimeType = mimeType download(url, contentType, headers, metadata) } function is7digital(url) { return url.toString().search(/[^\/]+:\/\/[^\/]*7digital.com\//) !== -1 } } ./src/app/mime-database.h0000644000004100000410000000231413004613604015451 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __MIME_DATABASE_H__ #define __MIME_DATABASE_H__ #include #include #include class MimeDatabase : public QObject { Q_OBJECT public: explicit MimeDatabase(QObject* parent=0); Q_INVOKABLE QString filenameToMimeType(const QString& filename) const; Q_INVOKABLE QString iconForMimetype(const QString& mimetypeString) const; Q_INVOKABLE QString nameForMimetype(const QString& mimetypeString) const; private: QMimeDatabase m_database; }; #endif // __MIME_DATABASE_H__ ./src/app/MimeTypeMapper.js0000644000004100000410000000670713004613604016055 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ function mimeTypeToContentType(mimeType) { if (mimeType.search("image/") === 0) { return ContentType.Pictures } else if (mimeType.search("audio/") === 0) { return ContentType.Music } else if (mimeType.search("video/") === 0) { return ContentType.Videos } else if (mimeType.search("text/x-vcard") === 0 || mimeType.search("text/vcard") === 0) { return ContentType.Contacts } else if (mimeType.search("application/epub[+]zip") === 0 || mimeType.search("application/vnd\.amazon\.ebook") === 0 || mimeType.search("application/x-mobipocket-ebook") === 0 || mimeType.search("application/x-fictionbook+xml") === 0 || mimeType.search("application/x-ms-reader") === 0) { return ContentType.EBooks } else if (mimeType.search("text/") === 0 || mimeType.search("application/pdf") === 0 || mimeType.search("application/x-pdf") === 0 || mimeType.search("application/vnd\.pdf") === 0 || mimeType.search("application/vnd\.oasis\.opendocument") === 0 || mimeType.search("application/msword") === 0 || mimeType.search("application/vnd\.openxmlformats-officedocument\.wordprocessingml\.document") === 0 || mimeType.search("application/vnd\.openxmlformats-officedocument\.spreadsheetml\.sheet") === 0 || mimeType.search("application/vnd\.openxmlformats-officedocument\.presentationml\.presentation") === 0 || mimeType.search("application/vnd\.ms-excel") === 0 || mimeType.search("application/vnd\.ms-powerpoint") === 0) { return ContentType.Documents } else { return ContentType.Unknown } } function mimeTypeRegexForContentType(contentType) { switch (contentType) { case ContentType.Pictures: return /image\/.*/; case ContentType.Music: return /audio\/.*/; case ContentType.Videos: return /video\/.*/; case ContentType.Contacts: return /text\/(x-vcard|vcard)/; case ContentType.EBooks: return /application\/(epub.*|vnd.amazon.ebook|x-mobipocket-ebook|x-fictionbook+xml|x-ms-reader)/; case ContentType.Documents: return /(text\/.*|application\/pdf|application\/x-pdf|application\/vnd\.pdf|application\/vnd\.oasis\.opendocument.*|application\/msword|application\/vnd\.openxmlformats-officedocument\.wordprocessingml\.document|application\/vnd\.openxmlformats-officedocument\.spreadsheetml\.sheet|application\/vnd\.openxmlformats-officedocument\.presentationml\.presentation|application\/vnd\.ms-excel|application\/vnd\.ms-powerpoint)/; case ContentType.Unknown: case ContentType.All: return /.*/; } } ./src/app/webbrowser-window.cpp0000644000004100000410000000222413004613604017001 0ustar www-datawww-data/* * Copyright 2013 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "webbrowser-window.h" WebBrowserWindow::WebBrowserWindow(QObject *parent) : QObject(parent), _window(0) {} QQuickWindow * WebBrowserWindow::window() const { return _window; } void WebBrowserWindow::setWindow(QQuickWindow * window) { if (_window != window) { _window = window; Q_EMIT windowChanged(window); } } void WebBrowserWindow::raise() { if ( ! _window) return; _window->raise(); _window->show(); } ./src/app/BrowserWindow.qml0000644000004100000410000000405513004613604016141 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import QtQuick.Window 2.2 import Ubuntu.Components 1.3 Window { id: window property bool developerExtrasEnabled: false property bool forceFullscreen: false property var currentWebview: null signal openUrls(var urls) contentOrientation: Screen.orientation width: units.gu(100) height: units.gu(75) QtObject { id: internal property int currentWindowState: Window.Windowed } Connections { target: window.currentWebview onFullscreenChanged: window.setFullscreen(window.currentWebview.fullscreen) } function setFullscreen(fullscreen) { if (!window.forceFullscreen) { if (fullscreen) { internal.currentWindowState = window.visibility window.visibility = Window.FullScreen } else { window.visibility = internal.currentWindowState } } } // Handle runtime requests to open urls as defined // by the freedesktop application dbus interface's open // method for DBUS application activation: // http://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#dbus // The dispatch on the org.freedesktop.Application if is done per appId at the // url-dispatcher/upstart level. Connections { target: UriHandler onOpened: window.openUrls(uris) } } ./src/app/favicon-fetcher.h0000644000004100000410000000374613004613604016035 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __FAVICON_FETCHER_H__ #define __FAVICON_FETCHER_H__ // Qt #include #include #include #include #include class QNetworkAccessManager; class QNetworkReply; class FaviconFetcher : public QObject { Q_OBJECT Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) Q_PROPERTY(QUrl localUrl READ localUrl NOTIFY localUrlChanged) Q_PROPERTY(bool shouldCache READ shouldCache WRITE setShouldCache NOTIFY shouldCacheChanged) public: FaviconFetcher(QObject* parent=0); ~FaviconFetcher(); const QUrl& url() const; void setUrl(const QUrl& url); const QUrl& localUrl() const; bool shouldCache() const; void setShouldCache(bool shouldCache); const QString& cacheLocation() const; Q_SIGNALS: void urlChanged() const; void localUrlChanged() const; void shouldCacheChanged() const; private Q_SLOTS: void download(const QUrl& url); void downloadFinished(QNetworkReply* reply); private: void setLocalUrl(const QUrl& url); bool m_shouldCache; QString m_cacheLocation; QScopedPointer m_manager; QNetworkReply* m_reply; QUrl m_url; QString m_filepath; int m_redirections; QUrl m_localUrl; }; #endif // __FAVICON_FETCHER_H__ ./src/app/favicon-fetcher.cpp0000644000004100000410000001242013004613604016355 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "favicon-fetcher.h" // Qt #include #include #include #include #include #include #include #include #include #include #include #include #define MAX_REDIRECTIONS 5 #define CACHE_EXPIRATION_DAYS 100 FaviconFetcher::FaviconFetcher(QObject* parent) : QObject(parent) , m_shouldCache(true) , m_reply(0) , m_redirections(0) { QDir cacheLocation(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/favicons"); m_cacheLocation = cacheLocation.absolutePath(); if (!cacheLocation.exists()) { QDir::root().mkpath(m_cacheLocation); } } FaviconFetcher::~FaviconFetcher() { if (m_reply) { m_reply->abort(); delete m_reply; } } const QUrl& FaviconFetcher::url() const { return m_url; } void FaviconFetcher::setUrl(const QUrl& url) { if (url != m_url) { m_url = url; Q_EMIT urlChanged(); setLocalUrl(QUrl()); if (m_reply) { m_reply->abort(); m_reply = 0; } if (!url.isValid()) { return; } if (url.isLocalFile()) { setLocalUrl(url); return; } QString id = url.toString(QUrl::None); QString extension; int extensionIndex = id.lastIndexOf("."); if (extensionIndex != -1) { extension = id.mid(extensionIndex); } QString hash(QCryptographicHash::hash(id.toUtf8(), QCryptographicHash::Md5).toHex()); m_filepath = m_cacheLocation + "/" + hash + extension; QFileInfo fileinfo(m_filepath); if (fileinfo.exists() && (fileinfo.lastModified().daysTo(QDateTime::currentDateTime()) < CACHE_EXPIRATION_DAYS)) { setLocalUrl(QUrl::fromLocalFile(m_filepath)); } else { m_redirections = 0; download(url); } } } const QUrl& FaviconFetcher::localUrl() const { return m_localUrl; } void FaviconFetcher::setLocalUrl(const QUrl& url) { if (url != m_localUrl) { m_localUrl = url; Q_EMIT localUrlChanged(); } } bool FaviconFetcher::shouldCache() const { return m_shouldCache; } void FaviconFetcher::setShouldCache(bool shouldCache) { if (shouldCache != m_shouldCache) { m_shouldCache = shouldCache; Q_EMIT shouldCacheChanged(); } } const QString& FaviconFetcher::cacheLocation() const { return m_cacheLocation; } void FaviconFetcher::download(const QUrl& url) { if (!m_manager) { m_manager.reset(new QNetworkAccessManager()); connect(m_manager.data(), SIGNAL(finished(QNetworkReply*)), this, SLOT(downloadFinished(QNetworkReply*))); } QNetworkRequest request(url); request.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true); // For some reason slashdot.org closes the connection with the default // user agent string ("Mozilla/5.0"). Weird. request.setHeader(QNetworkRequest::UserAgentHeader, QString("Mozilla")); m_reply = m_manager->get(request); } void FaviconFetcher::downloadFinished(QNetworkReply* reply) { if (reply->error() == QNetworkReply::OperationCanceledError) { reply->deleteLater(); return; } QUrl url = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); if (url.isEmpty()) { if (reply->error() == QNetworkReply::NoError) { QByteArray data = reply->readAll(); QImage image = QImage::fromData(data); if (m_shouldCache && image.save(m_filepath)) { setLocalUrl(QUrl::fromLocalFile(m_filepath)); } else { QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); if (image.save(&buffer, "PNG")) { setLocalUrl(QUrl("data:image/png;base64," + ba.toBase64())); } } } reply->deleteLater(); m_reply = 0; } else { reply->deleteLater(); m_reply = 0; if (++m_redirections < MAX_REDIRECTIONS) { QMetaObject::invokeMethod(this, "download", Qt::QueuedConnection, Q_ARG(QUrl, url)); } else { qWarning() << "Failed to download" << m_url.toString().toUtf8().data() << ": too many redirections"; } } } ./src/app/WebViewImpl.qml0000644000004100000410000000777413004613604015533 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import Ubuntu.Web 0.2 import webbrowsercommon.private 0.1 import "actions" as Actions WebView { id: webview property var currentWebview: webview /*experimental.certificateVerificationDialog: CertificateVerificationDialog {} experimental.proxyAuthenticationDialog: ProxyAuthenticationDialog {}*/ alertDialog: AlertDialog {} confirmDialog: ConfirmDialog {} promptDialog: PromptDialog {} beforeUnloadDialog: BeforeUnloadDialog {} signal showDownloadDialog(string downloadId, var contentType, var downloader, string filename, string mimeType) QtObject { id: internal readonly property var downloadMimeTypesBlacklist: [ "application/x-shockwave-flash", // http://launchpad.net/bugs/1379806 ] } onFullscreenRequested: webview.fullscreen = fullscreen onDownloadRequested: { if (!request.suggestedFilename && request.mimeType && internal.downloadMimeTypesBlacklist.indexOf(request.mimeType) > -1) { return } if (downloadLoader.status == Loader.Ready) { var headers = { } if (request.cookies.length > 0) { headers["Cookie"] = request.cookies.join(";") } if (request.referrer) { headers["Referer"] = request.referrer } headers["User-Agent"] = webview.context.userAgent // Work around https://launchpad.net/bugs/1487090 by guessing the mime type // from the suggested filename or URL if oxide hasn’t provided one, or if // the server has provided the generic application/octet-stream mime type. var mimeType = request.mimeType if (!mimeType || mimeType == "application/octet-stream") { mimeType = MimeDatabase.filenameToMimeType(request.suggestedFilename) } if (!mimeType) { var scheme = request.url.toString().split('://').shift().toLowerCase() var filename = request.url.toString().split('/').pop().split('?').shift() if ((scheme == "file") || (filename.indexOf('.') > -1)) { mimeType = MimeDatabase.filenameToMimeType(filename) } } downloadLoader.item.downloadMimeType(request.url, mimeType, headers, request.suggestedFilename) } else { // Desktop form factor case Qt.openUrlExternally(request.url) } } onHttpAuthenticationRequested: { PopupUtils.open(Qt.resolvedUrl("HttpAuthenticationDialog.qml"), webview.currentWebview, {"request": request}) } Loader { id: downloadLoader source: "Downloader.qml" asynchronous: true } Connections { target: downloadLoader.item onShowDownloadDialog: { showDownloadDialog(downloadId, contentType, downloader, filename, mimeType) } } function requestGeolocationPermission(request) { PopupUtils.open(Qt.resolvedUrl("GeolocationPermissionRequest.qml"), webview.currentWebview, {"request": request}) // TODO: we might want to store the answer to avoid requesting // the permission everytime the user visits this site. } } ./src/app/InvalidCertificateErrorSheet.qml0000644000004100000410000002464213004613604021066 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import com.canonical.Oxide 1.0 as Oxide Rectangle { property var certificateError signal allowed() signal denied() Connections { target: certificateError ? certificateError : null onCancelled: denied() } Flickable { anchors.fill: parent anchors.margins: units.gu(4) contentHeight: errorCol.height Column { id: errorCol anchors.centerIn: parent width: parent.width spacing: units.gu(3) Icon { anchors.horizontalCenter: parent.horizontalCenter name: "security-alert" width: units.gu(4) height: width } Label { width: parent.width text: certificateError ? i18n.tr("This site security certificate is not trusted.\n") + textForError(certificateError.certError) : "" wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter fontSize: "x-small" } Label { width: parent.width text: i18n.tr("Learn more") font.underline: true fontSize: "x-small" horizontalAlignment: Text.AlignHCenter visible: !moreInfo.visible MouseArea { anchors.fill: parent onClicked: { moreInfo.visible = true } } } Column { id: moreInfo width: parent.width visible: false spacing: units.gu(1) Label { fontSize: "x-small" width: parent.width wrapMode: Text.Wrap // TRANSLATORS: %1 refers to the SSL certificate's serial number text: i18n.tr("Serial number:\n%1").arg(certificateError ? certificateError.certificate.serialNumber : "") } Label { fontSize: "x-small" width: parent.width wrapMode: Text.Wrap // TRANSLATORS: %1 refers to the SSL certificate's subject display name text: i18n.tr("Subject:\n%1").arg(certificateError ? certificateError.certificate.subjectDisplayName : "") } Label { id: subjectAddress fontSize: "x-small" width: parent.width wrapMode: Text.Wrap // TRANSLATORS: %1 refers to the SSL certificate's subject's address text: i18n.tr("Subject address:\n%1").arg(certificateError ? (certificateError.certificate.getSubjectInfo(Oxide.SslCertificate.PrincipalAttrOrganizationName).join(", ") + "\n" + certificateError.certificate.getSubjectInfo(Oxide.SslCertificate.PrincipalAttrLocalityName).join(", ") + "\n" + certificateError.certificate.getSubjectInfo(Oxide.SslCertificate.PrincipalAttrStateOrProvinceName).join(", ") + "\n" + certificateError.certificate.getSubjectInfo(Oxide.SslCertificate.PrincipalAttrCountryName).join(", ")).replace(/\n+/g, "\n") : "") } Label { fontSize: "x-small" width: parent.width wrapMode: Text.Wrap // TRANSLATORS: %1 refers to the SSL certificate's issuer display name text: i18n.tr("Issuer:\n%1").arg(certificateError ? certificateError.certificate.issuerDisplayName : "") } Label { id: issuerAddress fontSize: "x-small" width: parent.width wrapMode: Text.Wrap // TRANSLATORS: %1 refers to the SSL certificate's issuer's address text: i18n.tr("Issuer address:\n%1").arg(certificateError ? (certificateError.certificate.getIssuerInfo(Oxide.SslCertificate.PrincipalAttrOrganizationName).join(", ") + "\n" + certificateError.certificate.getIssuerInfo(Oxide.SslCertificate.PrincipalAttrLocalityName).join(", ") + "\n" + certificateError.certificate.getIssuerInfo(Oxide.SslCertificate.PrincipalAttrStateOrProvinceName).join(", ") + "\n" + certificateError.certificate.getIssuerInfo(Oxide.SslCertificate.PrincipalAttrCountryName).join(", ")).replace(/\n+/g, "\n") : "") } Label { fontSize: "x-small" width: parent.width wrapMode: Text.Wrap // TRANSLATORS: %1 refers to the SSL certificate's start date text: i18n.tr("Valid from:\n%1").arg(certificateError ? certificateError.certificate.effectiveDate.toLocaleString() : "") } Label { fontSize: "x-small" width: parent.width wrapMode: Text.Wrap // TRANSLATORS: %1 refers to the SSL certificate's expiry date text: i18n.tr("Valid until:\n%1").arg(certificateError ? certificateError.certificate.expiryDate.toLocaleString() : "") } Label { fontSize: "x-small" width: parent.width wrapMode: Text.Wrap // TRANSLATORS: %1 refers to the SSL certificate's SHA1 fingerprint text: i18n.tr("Fingerprint (SHA1):\n%1").arg(certificateError ? certificateError.certificate.fingerprintSHA1 : "") } } Label { width: parent.width visible: certificateError ? certificateError.overridable : false text: i18n.tr("You should not proceed, especially if you have never seen this warning before for this site.") wrapMode: Text.Wrap fontSize: "x-small" horizontalAlignment: Text.AlignHCenter } Button { text: i18n.tr("Proceed anyway") anchors.horizontalCenter: parent.horizontalCenter visible: certificateError ? certificateError.overridable : false onClicked: { certificateError.allow() allowed() } } Button { id: backButton anchors.horizontalCenter: parent.horizontalCenter visible: certificateError ? certificateError.overridable : false text: i18n.tr("Back to safety") onClicked: { certificateError.deny() denied() } color: UbuntuColors.orange } } } function textForError(error) { switch(error) { case Oxide.CertificateError.ErrorBadIdentity: // TRANSLATORS: %1 refers to the domain name of the SSL certificate return i18n.tr("You attempted to reach %1 but the server presented a security certificate which does not match the identity of the site.").arg(certificateError ? certificateError.url : "") case Oxide.CertificateError.ErrorExpired: // TRANSLATORS: %1 refers to the domain name of the SSL certificate return i18n.tr("You attempted to reach %1 but the server presented a security certificate which has expired.").arg(certificateError ? certificateError.url : "") case Oxide.CertificateError.ErrorDateInvalid: // TRANSLATORS: %1 refers to the domain name of the SSL certificate return i18n.tr("You attempted to reach %1 but the server presented a security certificate which contains invalid dates.").arg(certificateError ? certificateError.url : "") case Oxide.CertificateError.ErrorAuthorityInvalid: // TRANSLATORS: %1 refers to the domain name of the SSL certificate return i18n.tr("You attempted to reach %1 but the server presented a security certificate issued by an entity that is not trusted.").arg(certificateError ? certificateError.url : "") case Oxide.CertificateError.ErrorRevoked: // TRANSLATORS: %1 refers to the domain name of the SSL certificate return i18n.tr("You attempted to reach %1 but the server presented a security certificate that has been revoked.").arg(certificateError ? certificateError.url : "") case Oxide.CertificateError.ErrorInvalid: // TRANSLATORS: %1 refers to the domain name of the SSL certificate return i18n.tr("You attempted to reach %1 but the server presented an invalid security certificate.").arg(certificateError ? certificateError.url : "") case Oxide.CertificateError.ErrorInsecure: // TRANSLATORS: %1 refers to the domain name of the SSL certificate return i18n.tr("You attempted to reach %1 but the server presented an insecure security certificate.").arg(certificateError ? certificateError.url : "") default: // TRANSLATORS: %1 refers to the domain name of the SSL certificate return i18n.tr("This site security certificate is not trusted\nYou attempted to reach %1 but the server presented a security certificate which failed our security checks for an unknown reason.").arg(certificateError ? certificateError.url : "") } } onVisibleChanged: { if (!visible) { moreInfo.visible = false } } } ./src/app/actions/0000755000004100000410000000000013004613605014250 5ustar www-datawww-data./src/app/actions/SelectAll.qml0000644000004100000410000000135213004613604016633 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Select all") } ./src/app/actions/Cut.qml0000644000004100000410000000134313004613604015516 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Cut") } ./src/app/actions/Bookmark.qml0000644000004100000410000000201413004613604016524 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 import Ubuntu.Unity.Action 1.1 as UnityActions UnityActions.Action { text: i18n.tr("Bookmark") // TRANSLATORS: This is a free-form list of keywords associated to the 'Bookmark' action. // Keywords may actually be sentences, and must be separated by semi-colons. keywords: i18n.tr("Add This Page to Bookmarks") } ./src/app/actions/SaveLink.qml0000644000004100000410000000135113004613604016476 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Save link") } ./src/app/actions/OpenImageInNewTab.qml0000644000004100000410000000137213004613604020221 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Open image in new tab") } ./src/app/actions/Reload.qml0000644000004100000410000000177013004613604016175 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 import Ubuntu.Unity.Action 1.1 as UnityActions UnityActions.Action { text: i18n.tr("Reload") // TRANSLATORS: This is a free-form list of keywords associated to the 'Reload' action. // Keywords may actually be sentences, and must be separated by semi-colons. keywords: i18n.tr("Leave Page") } ./src/app/actions/Paste.qml0000644000004100000410000000134513004613604016041 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Paste") } ./src/app/actions/OpenLinkInWebBrowser.qml0000644000004100000410000000136713004613604021001 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Open link in WebBrowser") } ./src/app/actions/Undo.qml0000644000004100000410000000134413004613604015671 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Undo") } ./src/app/actions/OpenLinkInNewTab.qml0000644000004100000410000000137113004613604020073 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Open link in new tab") } ./src/app/actions/Back.qml0000644000004100000410000000176413004613604015632 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 import Ubuntu.Unity.Action 1.1 as UnityActions UnityActions.Action { text: i18n.tr("Back") // TRANSLATORS: This is a free-form list of keywords associated to the 'Back' action. // Keywords may actually be sentences, and must be separated by semi-colons. keywords: i18n.tr("Older Page") } ./src/app/actions/CopyLink.qml0000644000004100000410000000135613004613604016517 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Copy link") } ./src/app/actions/GoTo.qml0000644000004100000410000000204713004613604015635 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 import Ubuntu.Unity.Action 1.1 as UnityActions UnityActions.Action { text: i18n.tr("Goto") // TRANSLATORS: This is a free-form list of keywords associated to the 'Goto' action. // Keywords may actually be sentences, and must be separated by semi-colons. keywords: i18n.tr("Address;URL;www") parameterType: UnityActions.Action.String } ./src/app/actions/ClearHistory.qml0000644000004100000410000000202413004613604017370 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 import Ubuntu.Unity.Action 1.1 as UnityActions UnityActions.Action { text: i18n.tr("Clear History") // TRANSLATORS: This is a free-form list of keywords associated to the 'Clear History' action. // Keywords may actually be sentences, and must be separated by semi-colons. keywords: i18n.tr("Clear Navigation History") } ./src/app/actions/Copy.qml0000644000004100000410000000135113004613604015674 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Copy") } ./src/app/actions/SaveImage.qml0000644000004100000410000000135713004613604016631 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Save image") } ./src/app/actions/BookmarkLink.qml0000644000004100000410000000136213004613604017347 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Bookmark link") } ./src/app/actions/NewTab.qml0000644000004100000410000000177613004613604016155 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 import Ubuntu.Unity.Action 1.1 as UnityActions UnityActions.Action { text: i18n.tr("New Tab") // TRANSLATORS: This is a free-form list of keywords associated to the 'New Tab' action. // Keywords may actually be sentences, and must be separated by semi-colons. keywords: i18n.tr("Open a New Tab") } ./src/app/actions/OpenLinkInNewBackgroundTab.qml0000644000004100000410000000140413004613604022070 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Open link in new background tab") } ./src/app/actions/Share.qml0000644000004100000410000000140013004613604016017 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Share") iconName: "share" } ./src/app/actions/Erase.qml0000644000004100000410000000134513004613604016024 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Erase") } ./src/app/actions/CopyImage.qml0000644000004100000410000000135713004613604016645 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Copy image") } ./src/app/actions/FindInPage.qml0000644000004100000410000000200313004613604016721 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 import Ubuntu.Unity.Action 1.1 as UnityActions UnityActions.Action { text: i18n.tr("Find in page") // TRANSLATORS: This is a free-form list of keywords associated to the 'Find in Page' action. // Keywords may actually be sentences, and must be separated by semi-colons. keywords: i18n.tr("Search in Page") } ./src/app/actions/Forward.qml0000644000004100000410000000177213004613604016375 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 import Ubuntu.Unity.Action 1.1 as UnityActions UnityActions.Action { text: i18n.tr("Forward") // TRANSLATORS: This is a free-form list of keywords associated to the 'Forward' action. // Keywords may actually be sentences, and must be separated by semi-colons. keywords: i18n.tr("Newer Page") } ./src/app/actions/OpenVideoInNewTab.qml0000644000004100000410000000136513004613604020247 0ustar www-datawww-data/* * Copyright 2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Open video in new tab") } ./src/app/actions/Redo.qml0000644000004100000410000000134413004613604015655 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Redo") } ./src/app/actions/SaveVideo.qml0000644000004100000410000000135213004613604016650 0ustar www-datawww-data/* * Copyright 2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Ubuntu.Components 1.3 Action { text: i18n.tr("Save video") } ./src/app/WebProcessMonitor.qml0000644000004100000410000000443313004613604016752 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import com.canonical.Oxide 1.8 as Oxide Item { id: monitor visible: false property var webview: null readonly property bool killed: webview && (webview.webProcessStatus == Oxide.WebView.WebProcessKilled) readonly property bool crashed: webview && (webview.webProcessStatus == Oxide.WebView.WebProcessCrashed) // When the renderer process is killed (most likely by the system’s // OOM killer), try to reload the page once, and if this results in // the process being killed again within one minute, then display // the sad tab. readonly property int killedRetries: internal.killedRetries QtObject { id: internal property int killedRetries: 0 } Connections { target: webview onWebProcessStatusChanged: { if (webview.webProcessStatus == Oxide.WebView.WebProcessKilled) { if (internal.killedRetries == 0) { // Do not attempt reloading right away, this would result in a crash delayedReload.restart() } } } } Timer { id: delayedReload interval: 100 onTriggered: { monitorTimer.restart() monitor.webview.reload() internal.killedRetries++ } } Timer { id: monitorTimer interval: 60000 // 1 minute onTriggered: internal.killedRetries = 0 } onWebviewChanged: { internal.killedRetries = 0 delayedReload.stop() monitorTimer.stop() } } ./src/app/webbrowser/0000755000004100000410000000000013004613623014771 5ustar www-datawww-data./src/app/webbrowser/TopSitesModel.qml0000644000004100000410000000157513004613604020246 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 SortFilterModel { sort { order: Qt.DescendingOrder property: "visits" } filter { pattern: /^false$/ property: "hidden" } } ./src/app/webbrowser/CMakeLists.txt0000644000004100000410000000445713004613604017542 0ustar www-datawww-dataproject(webbrowser-app) find_package(Qt5Concurrent REQUIRED) find_package(Qt5Sql REQUIRED) include_directories( ${CMAKE_BINARY_DIR} ${webbrowser-common_SOURCE_DIR} ${webbrowser-common_BINARY_DIR} ) set(WEBBROWSER_APP_MODELS_SRC bookmarks-model.cpp bookmarks-folder-model.cpp bookmarks-folderlist-model.cpp downloads-model.cpp history-domain-model.cpp history-domainlist-model.cpp history-lastvisitdatelist-model.cpp history-model.cpp limit-proxy-model.cpp tabs-model.cpp text-search-filter-model.cpp ) set(WEBBROWSER_APP_MODELS webbrowser-app-models) add_library(${WEBBROWSER_APP_MODELS} STATIC ${WEBBROWSER_APP_MODELS_SRC}) target_link_libraries(${WEBBROWSER_APP_MODELS} Qt5::Core Qt5::Sql ) set(WEBBROWSER_APP_SRC cache-deleter.cpp file-operations.cpp searchengine.cpp webbrowser-app.cpp ) set(WEBBROWSER_APP webbrowser-app) add_executable(${WEBBROWSER_APP} ${WEBBROWSER_APP_SRC}) target_link_libraries(${WEBBROWSER_APP} Qt5::Concurrent Qt5::Core Qt5::Qml Qt5::Quick ${COMMONLIB} ${WEBBROWSER_APP_MODELS} ) install(TARGETS ${WEBBROWSER_APP} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) file(GLOB QML_FILES *.qml qmldir *.js) install(FILES ${QML_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/webbrowser-app/webbrowser) install(DIRECTORY assets DESTINATION ${CMAKE_INSTALL_DATADIR}/webbrowser-app/webbrowser FILES_MATCHING PATTERN *.png PATTERN *.svg PATTERN *.sci) install(DIRECTORY searchengines DESTINATION ${CMAKE_INSTALL_DATADIR}/webbrowser-app/webbrowser FILES_MATCHING PATTERN *.xml) configure_file(${DESKTOP_FILE}.in.in ${DESKTOP_FILE}.in @ONLY) add_custom_target(${DESKTOP_FILE} ALL COMMENT "Merging translations into ${DESKTOP_FILE}" COMMAND ${INTLTOOL_MERGE} -d -u ${CMAKE_SOURCE_DIR}/po ${CMAKE_CURRENT_BINARY_DIR}/${DESKTOP_FILE}.in ${DESKTOP_FILE}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${DESKTOP_FILE} DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) install(FILES "webbrowser-app.url-dispatcher" DESTINATION ${CMAKE_INSTALL_DATADIR}/url-dispatcher/urls) install(FILES "webbrowser-app-content-hub.json" DESTINATION ${CMAKE_INSTALL_DATADIR}/content-hub/peers RENAME webbrowser-app ) ./src/app/webbrowser/UrlDelegate.qml0000644000004100000410000000535413004613604017707 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import QtQuick.Layouts 1.1 import Ubuntu.Components 1.3 import ".." ListItem { id: urlDelegate property alias icon: icon.source property alias title: title.text property alias url: url.text property alias headerComponent: headerComponentLoader.sourceComponent property bool removable: true divider.visible: false signal removed() RowLayout { anchors { verticalCenter: parent.verticalCenter left: parent.left leftMargin: units.gu(1.5) right: parent.right rightMargin: units.gu(1.5) } spacing: units.gu(1) Loader { id: headerComponentLoader anchors.verticalCenter: parent.verticalCenter visible: status == Loader.Ready } Favicon { id: icon anchors.verticalCenter: parent.verticalCenter } Column { Layout.fillWidth: true anchors.verticalCenter: parent.verticalCenter Label { id: title anchors { left: parent.left right: parent.right } fontSize: "x-small" color: UbuntuColors.darkGrey wrapMode: Text.Wrap elide: Text.ElideRight maximumLineCount: 1 } Label { id: url anchors { left: parent.left right: parent.right } fontSize: "xx-small" color: UbuntuColors.darkGrey wrapMode: Text.Wrap elide: Text.ElideRight maximumLineCount: 1 } } } ListItemActions { id: listItemActions actions: [ Action { objectName: "leadingAction.delete" iconName: "delete" onTriggered: urlDelegate.removed() } ] } leadingActions: removable ? listItemActions : null } ./src/app/webbrowser/bookmarks-model.cpp0000644000004100000410000003154113004613604020566 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "bookmarks-model.h" // Qt #include #include #define CONNECTION_NAME "webbrowser-app-bookmarks" /*! \class BookmarksModel \brief List model that stores information about bookmarked websites. BookmarksModel is a list model that stores bookmark entries for quick access to favourite websites. For a given URL, the following information is stored: page title and URL to the favorite icon if any. The model is sorted alphabetically at all times (by URL). The information is persistently stored on disk in a SQLite database. The database is read at startup to populate the model, and whenever a new entry is added to the model or an entry is removed from the model the database is updated. However the model doesn’t monitor the database for external changes. */ BookmarksModel::BookmarksModel(QObject* parent) : QAbstractListModel(parent) { m_database = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), CONNECTION_NAME); } BookmarksModel::~BookmarksModel() { m_database.close(); m_database = QSqlDatabase(); QSqlDatabase::removeDatabase(CONNECTION_NAME); } void BookmarksModel::resetDatabase(const QString& databaseName) { beginResetModel(); m_folders.clear(); m_urls.clear(); m_orderedEntries.clear(); m_database.close(); m_database.setDatabaseName(databaseName); m_database.open(); createOrAlterDatabaseSchema(); endResetModel(); populateFromDatabase(); Q_EMIT rowCountChanged(); } void BookmarksModel::createOrAlterDatabaseSchema() { QSqlQuery createQuery(m_database); QString query = QLatin1String("CREATE TABLE IF NOT EXISTS bookmarks " "(url VARCHAR, title VARCHAR, icon VARCHAR, " "created INTEGER, folderId INTEGER);"); createQuery.prepare(query); createQuery.exec(); QSqlQuery createFolderQuery(m_database); query = QLatin1String("CREATE TABLE IF NOT EXISTS folders " "(folderId INTEGER PRIMARY KEY, folder VARCHAR);"); createFolderQuery.prepare(query); createFolderQuery.exec(); // Older version of the database schema didn’t have 'created' and/or // 'folderId' columns QSqlQuery tableInfoQuery(m_database); query = QLatin1String("PRAGMA TABLE_INFO(bookmarks);"); tableInfoQuery.prepare(query); tableInfoQuery.exec(); bool missingCreatedColumn = true; bool missingFolderIdColumn = true; while (tableInfoQuery.next()) { if (tableInfoQuery.value("name").toString() == "created") { missingCreatedColumn = false; } if (tableInfoQuery.value("name").toString() == "folderId") { missingFolderIdColumn = false; } if (!missingCreatedColumn && !missingFolderIdColumn) { break; } } if (missingCreatedColumn) { QSqlQuery addCreatedColumnQuery(m_database); query = QLatin1String("ALTER TABLE bookmarks ADD COLUMN created INTEGER;"); addCreatedColumnQuery.prepare(query); addCreatedColumnQuery.exec(); // the default for the column is an empty value, which is interpreted as zero // when converted to a number. Zero represents a date far in the past, so // any newly created bookmark will correctly be represented as more recent than any other } if (missingFolderIdColumn) { QSqlQuery addFolderColumnQuery(m_database); query = QLatin1String("ALTER TABLE bookmarks ADD COLUMN folderId INTEGER;"); addFolderColumnQuery.prepare(query); addFolderColumnQuery.exec(); } } void BookmarksModel::populateFromDatabase() { //Add default empty folder m_folders.insert(0, ""); Q_EMIT folderAdded(""); QSqlQuery populateFolderQuery(m_database); QString query = QLatin1String("SELECT folderId, folder FROM folders;"); populateFolderQuery.prepare(query); populateFolderQuery.exec(); while (populateFolderQuery.next()) { QString folder = populateFolderQuery.value(1).toString(); m_folders.insert(populateFolderQuery.value(0).toInt(), folder); Q_EMIT folderAdded(folder); } QSqlQuery populateQuery(m_database); query = QLatin1String("SELECT url, title, icon, created, folderId " "FROM bookmarks ORDER BY created DESC;"); populateQuery.prepare(query); populateQuery.exec(); int count = 0; while (populateQuery.next()) { BookmarkEntry entry; entry.url = populateQuery.value(0).toUrl(); entry.title = populateQuery.value(1).toString(); entry.icon = populateQuery.value(2).toUrl(); entry.created = QDateTime::fromMSecsSinceEpoch(populateQuery.value(3).toULongLong()); entry.folderId = populateQuery.value(4).toInt(); if (m_folders.contains(entry.folderId)) { entry.folder = m_folders.value(entry.folderId); } else { entry.folderId = 0; updateExistingEntryInDatabase(entry); } beginInsertRows(QModelIndex(), count, count); m_urls.insert(entry.url); m_orderedEntries.append(entry); endInsertRows(); ++count; } } QHash BookmarksModel::roleNames() const { static QHash roles; if (roles.isEmpty()) { roles[Url] = "url"; roles[Title] = "title"; roles[Icon] = "icon"; roles[Created] = "created"; roles[Folder] = "folder"; } return roles; } int BookmarksModel::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); return m_orderedEntries.count(); } QVariant BookmarksModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } const BookmarkEntry& entry = m_orderedEntries.at(index.row()); switch (role) { case Url: return entry.url; case Title: return entry.title; case Icon: return entry.icon; case Created: return entry.created; case Folder: return entry.folder; default: return QVariant(); } } const QString BookmarksModel::databasePath() const { return m_database.databaseName(); } void BookmarksModel::setDatabasePath(const QString& path) { if (path != databasePath()) { if (path.isEmpty()) { resetDatabase(":memory:"); } else { resetDatabase(path); } Q_EMIT databasePathChanged(); } } QStringList BookmarksModel::folders() const { return m_folders.values(); } int BookmarksModel::addFolder(const QString& folder) { int newFolderId = insertNewFolderInDatabase(folder); if (newFolderId != 0) { m_folders.insert(newFolderId, folder); Q_EMIT folderAdded(folder); } return newFolderId; } /*! Test if a given URL is already bookmarked. Return true if the model contains an entry with the same URL, false otherwise. */ bool BookmarksModel::contains(const QUrl& url) const { return m_urls.contains(url); } /*! Add a given URL to the list of bookmarks. If the URL was previously bookmarked, do nothing. */ void BookmarksModel::add(const QUrl& url, const QString& title, const QUrl& icon, const QString& folder) { if (m_urls.contains(url)) { qWarning() << "URL already bookmarked:" << url; } else { beginInsertRows(QModelIndex(), 0, 0); BookmarkEntry entry; entry.url = url; entry.title = title; entry.icon = icon; entry.created = QDateTime::currentDateTime(); entry.folder = folder; entry.folderId = getFolderId(entry.folder); m_urls.insert(url); m_orderedEntries.prepend(entry); endInsertRows(); Q_EMIT added(url); insertNewEntryInDatabase(entry); Q_EMIT rowCountChanged(); } } void BookmarksModel::insertNewEntryInDatabase(const BookmarkEntry& entry) { QSqlQuery query(m_database); static QString insertStatement = QLatin1String("INSERT INTO bookmarks (url, " "title, icon, created, folderId) " "VALUES (?, ?, ?, ?, ?);"); query.prepare(insertStatement); query.addBindValue(entry.url.toString()); query.addBindValue(entry.title); query.addBindValue(entry.icon.toString()); query.addBindValue(entry.created.toMSecsSinceEpoch()); if (!entry.folderId) { query.addBindValue(QVariant()); } else { query.addBindValue(entry.folderId); } query.exec(); } /*! Remove a given URL from the list of bookmarks. If the URL was not previously bookmarked, do nothing. */ void BookmarksModel::remove(const QUrl& url) { if (m_urls.contains(url)) { int index = 0; Q_FOREACH(BookmarkEntry entry, m_orderedEntries) { if (entry.url == url) { beginRemoveRows(QModelIndex(), index, index); m_orderedEntries.removeAt(index); m_urls.remove(url); endRemoveRows(); Q_EMIT removed(url); removeExistingEntryFromDatabase(url); Q_EMIT rowCountChanged(); return; } else { index++; } }; } else { qWarning() << "Invalid bookmark:" << url; } } void BookmarksModel::removeExistingEntryFromDatabase(const QUrl& url) { QSqlQuery query(m_database); static QString deleteStatement = QLatin1String("DELETE FROM bookmarks WHERE url=?;"); query.prepare(deleteStatement); query.addBindValue(url.toString()); query.exec(); } void BookmarksModel::update(const QUrl& url, const QString& title, const QString& folder) { if (m_urls.contains(url)) { int index = 0; Q_FOREACH(BookmarkEntry entry, m_orderedEntries) { if (entry.url == url) { BookmarkEntry& updatedEntry = m_orderedEntries[index]; QVector roles; if (title != updatedEntry.title) { updatedEntry.title = title; roles << Title; } if (folder != updatedEntry.folder) { updatedEntry.folder = folder; updatedEntry.folderId = getFolderId(updatedEntry.folder); roles << Folder; } if (!roles.isEmpty()) { Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), roles); updateExistingEntryInDatabase(updatedEntry); } return; } else { index++; } }; } else { qWarning() << "Invalid bookmark:" << url; } } void BookmarksModel::updateExistingEntryInDatabase(const BookmarkEntry& entry) { QSqlQuery query(m_database); static QString updateStatement = QLatin1String("UPDATE bookmarks SET title=?, " "icon=?, created=?, folderId=? " "WHERE url=?;"); query.prepare(updateStatement); query.addBindValue(entry.title); query.addBindValue(entry.icon.toString()); query.addBindValue(entry.created.toMSecsSinceEpoch()); if (entry.folderId == 0) { query.addBindValue(QVariant()); } else { query.addBindValue(entry.folderId); } query.addBindValue(entry.url.toString()); query.exec(); } int BookmarksModel::getFolderId(const QString& folder) { QHashIterator i(m_folders); while (i.hasNext()) { i.next(); if (i.value() == folder) { return i.key(); } } return addFolder(folder); } int BookmarksModel::insertNewFolderInDatabase(const QString& folder) { QSqlQuery insertQuery(m_database); QString query = QLatin1String("INSERT INTO folders (folder) VALUES (?);"); insertQuery.prepare(query); insertQuery.addBindValue(folder); insertQuery.exec(); QSqlQuery selectQuery(m_database); query = QLatin1String("SELECT folderId FROM folders WHERE folder=?;"); selectQuery.prepare(query); selectQuery.addBindValue(folder); selectQuery.exec(); if (selectQuery.next()) { return selectQuery.value(0).toInt(); } return 0; } ./src/app/webbrowser/domain-utils.h0000644000004100000410000000262213004613604017550 0ustar www-datawww-data/* * Copyright 2013 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __DOMAIN_UTILS_H__ #define __DOMAIN_UTILS_H__ // Qt #include #include #include namespace DomainUtils { static const QString TOKEN_LOCAL = "(local)"; static const QString TOKEN_NONE = "(none)"; static QString extractTopLevelDomainName(const QUrl& url) { if (url.isLocalFile()) { return TOKEN_LOCAL; } QString host = url.host(); if (host.isEmpty()) { // XXX: (when) can this happen? return TOKEN_NONE; } QString tld = url.topLevelDomain(); if (tld.isEmpty()) { return host; } host.chop(tld.size()); QString sld = host.split(".").last(); return sld + tld; } } // namespace DomainUtils #endif // __DOMAIN_UTILS_H__ ./src/app/webbrowser/webbrowser-app.url-dispatcher0000644000004100000410000000007413004613604022600 0ustar www-datawww-data[ { "protocol": "http" }, { "protocol": "https" } ] ./src/app/webbrowser/history-lastvisitdatelist-model.h0000644000004100000410000000440313004613604023513 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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_LASTVISITDATELIST_MODEL_H__ #define __HISTORY_LASTVISITDATELIST_MODEL_H__ // Qt #include #include #include #include class HistoryLastVisitDateListModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QVariant sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged) Q_ENUMS(Roles) public: HistoryLastVisitDateListModel(QObject* parent=0); ~HistoryLastVisitDateListModel(); enum Roles { LastVisitDate = Qt::UserRole + 1 }; // reimplemented from QAbstractListModel QHash roleNames() const; int rowCount(const QModelIndex& parent=QModelIndex()) const; QVariant data(const QModelIndex& index, int role) const; QVariant sourceModel() const; void setSourceModel(QVariant sourceModel); Q_SIGNALS: void sourceModelChanged() const; private Q_SLOTS: void onRowsInserted(const QModelIndex& parent, int start, int end); void onRowsRemoved(const QModelIndex& parent, int start, int end); void onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row); void onModelReset(); private: QAbstractItemModel* m_sourceModel; int m_sourceModelRole; QMap*> m_lastVisitDates; QList m_orderedDates; void clearLastVisitDates(); void populateModel(); void insertNewHistoryEntry(QPersistentModelIndex* index, bool notify); void updateSourceModelRole(); }; #endif // __HISTORY_LASTVISITDATELIST_MODEL_H__ ./src/app/webbrowser/BookmarksFoldersViewWide.qml0000644000004100000410000001603113004613604022417 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import webbrowserapp.private 0.1 import "BookmarksModelUtils.js" as BookmarksModelUtils FocusScope { id: bookmarksFoldersViewWideItem property url homeBookmarkUrl signal bookmarkClicked(url url) signal bookmarkRemoved(url url) signal dragStarted() function restoreLastFocusedColumn() { if (internal.lastFocusedColumn && internal.lastFocusedColumn == bookmarksList && BookmarksModel.count > 0) { bookmarksList.forceActiveFocus() } else { folders.forceActiveFocus() } } onActiveFocusChanged: { if (activeFocus) { restoreLastFocusedColumn() } } ListView { id: folders objectName: "foldersList" anchors { top: parent.top bottom: parent.bottom left: parent.left } width: units.gu(25) onActiveFocusChanged: { if (activeFocus) { internal.lastFocusedColumn = folders } } model: BookmarksFolderListModel { id: bookmarksFolderListModel sourceModel: BookmarksModel } currentIndex: 0 Keys.onRightPressed: { if (!folders.currentItem) { return } if ((folders.currentItem.isAllBookmarksFolder && bookmarksList.model.length > 0) || bookmarksList.model.count > 0) { bookmarksList.focus = true } } delegate: ListItem { id: folderItem objectName: "folderItem" property alias name: dropArea.folderName property var model: entries readonly property bool isActiveFolder: ListView.isCurrentItem readonly property bool isAllBookmarksFolder: folder.length === 0 readonly property bool isCurrentDropTarget: dropArea.containsDrag && dropArea.drag.source.folder !== folder color: isCurrentDropTarget ? "green" : "transparent" Label { anchors { verticalCenter: parent.verticalCenter left: parent.left right: parent.right leftMargin: units.gu(2) rightMargin: units.gu(2) } fontSize: "small" text: isAllBookmarksFolder ? i18n.tr("All Bookmarks") : folderItem.name color: (isActiveFolder && !folders.activeFocus) ? UbuntuColors.orange : UbuntuColors.darkGrey } divider { // Hide the divider so that the highlight doesn’t overlap it // Do not set visible to false, otherwise the content item is resized. opacity: (!ListView.view.activeFocus || (index > ListView.view.currentIndex) || (index < (ListView.view.currentIndex - 1))) ? 1 : 0 Behavior on opacity { UbuntuNumberAnimation {} } } onClicked: folders.currentIndex = index DropArea { id: dropArea property string folderName: folder anchors.fill: parent } } highlight: ListViewHighlight {} } Scrollbar { flickableItem: folders } ListView { id: bookmarksList objectName: "bookmarksList" anchors { top: parent.top bottom: parent.bottom left: folders.right right: parent.right } onActiveFocusChanged: { if (activeFocus) { internal.lastFocusedColumn = bookmarksList } } model: { if (!folders.currentItem || !folders.currentItem.model) { return null } if (folders.currentItem.isAllBookmarksFolder) { return BookmarksModelUtils.prependHomepageToBookmarks(folders.currentItem.model, { title: i18n.tr("Homepage"), url: bookmarksFoldersViewWideItem.homeBookmarkUrl, folder: "" }) } return folders.currentItem.model } currentIndex: 0 delegate: DraggableUrlDelegateWide { objectName: "bookmarkItem" property var entry: folders.currentItem.isAllBookmarksFolder ? modelData : model property string folder: entry.folder readonly property bool isHomeBookmark: folder === "" && index === 0 clip: true title: entry.title icon: entry.icon ? entry.icon : "" url: entry.url removable: !isHomeBookmark draggable: !isHomeBookmark && contentItem.x === 0 onClicked: bookmarksFoldersViewWideItem.bookmarkClicked(url) onRemoved: bookmarksFoldersViewWideItem.bookmarkRemoved(url) // Larger margin to prevent interference from Scrollbar hovering area gripMargin: units.gu(4) onDragStarted: { // Remove interactivity to prevent the list from scrolling // while dragging near its margins. This ensures we can correctly // return the item to its original position on a failed drop. bookmarksList.interactive = false bookmarksFoldersViewWideItem.dragStarted() } onDragEnded: { bookmarksList.interactive = true if (dragAndDrop.target && dragAndDrop.target.folderName !== folder) { BookmarksModel.update(entry.url, entry.title, dragAndDrop.target.folderName) dragAndDrop.success = true } } } highlight: ListViewHighlight {} Keys.onReturnPressed: bookmarksFoldersViewWideItem.bookmarkClicked(currentItem.url) Keys.onDeletePressed: { if (currentItem.removable) { bookmarksFoldersViewWideItem.bookmarkRemoved(currentItem.url) if (bookmarksList.model.length === 0) { folders.focus = true } } } Keys.onLeftPressed: folders.focus = true } Scrollbar { flickableItem: bookmarksList } QtObject { id: internal property var lastFocusedColumn: folders } } ./src/app/webbrowser/ContextMenuWide.qml0000644000004100000410000001070113004613604020564 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 as ListItems import Ubuntu.Components.Popups 1.3 as Popups import com.canonical.Oxide 1.8 as Oxide Popups.Popover { id: contextMenu property QtObject contextModel: model property ActionList actions: null property var webview: null QtObject { id: internal readonly property int lastEnabledActionIndex: { var last = -1 for (var i in actions.actions) { if (actions.actions[i].enabled) { last = i } } return last } readonly property real locationBarOffset: contextMenu.webview.locationBarController.height + contextMenu.webview.locationBarController.offset } Rectangle { anchors.fill: parent color: "#ececec" } Column { anchors { left: parent.left right: parent.right } Label { id: titleLabel objectName: "titleLabel" text: contextModel.srcUrl.toString() ? contextModel.srcUrl : contextModel.linkUrl anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) } height: units.gu(5) visible: text fontSize: "x-small" color: "#888888" elide: Text.ElideRight verticalAlignment: Text.AlignVCenter } ListItems.ThinDivider { anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) } visible: titleLabel.visible } Repeater { model: actions.actions delegate: ListItems.Empty { action: actions.actions[index] objectName: action.objectName + "_item" visible: action.enabled showDivider: false height: units.gu(5) Label { anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) verticalCenter: parent.verticalCenter } fontSize: "small" text: action.text } ListItems.ThinDivider { visible: index < internal.lastEnabledActionIndex anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) bottom: parent.bottom } } onTriggered: contextMenu.hide() } } } Item { id: positioner visible: false parent: contextMenu.webview x: contextModel.position.x y: contextModel.position.y + internal.locationBarOffset } caller: positioner onVisibleChanged: { if (!visible) { contextModel.close() } } // Override default implementation to prevent context menu from stealing // active focus when shown (https://launchpad.net/bugs/1526884). function show() { visible = true __foreground.show() } Binding { // Ensure the context menu doesn’t steal focus from // the webview when one of its actions is activated // (https://launchpad.net/bugs/1526884). target: __foreground property: "activeFocusOnPress" value: false } } ./src/app/webbrowser/tabs-model.h0000644000004100000410000000435313004613604017175 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __TABS_MODEL_H__ #define __TABS_MODEL_H__ // Qt #include #include class QObject; class TabsModel : public QAbstractListModel { Q_OBJECT Q_ENUMS(Roles) Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) Q_PROPERTY(QObject* currentTab READ currentTab NOTIFY currentTabChanged) Q_PROPERTY(int count READ rowCount NOTIFY countChanged) public: TabsModel(QObject* parent=0); ~TabsModel(); enum Roles { Url = Qt::UserRole + 1, Title, Icon, Tab }; // reimplemented from QAbstractListModel QHash roleNames() const; int rowCount(const QModelIndex& parent=QModelIndex()) const; QVariant data(const QModelIndex& index, int role) const; int currentIndex() const; void setCurrentIndex(int index); QObject* currentTab() const; Q_INVOKABLE int add(QObject* tab); Q_INVOKABLE int insert(QObject* tab, int index); Q_INVOKABLE QObject* remove(int index); Q_INVOKABLE QObject* get(int index) const; Q_INVOKABLE void move(int from, int to); Q_SIGNALS: void currentIndexChanged() const; void currentTabChanged() const; void countChanged() const; private Q_SLOTS: void onUrlChanged(); void onTitleChanged(); void onIconChanged(); private: QList m_tabs; int m_currentIndex; bool checkValidTabIndex(int index) const; void setCurrentIndexNoCheck(int index); void onDataChanged(QObject* tab, int role); }; #endif // __TABS_MODEL_H__ ./src/app/webbrowser/downloads-model.h0000644000004100000410000000724013004613604020234 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __DOWNLOADS_MODEL_H__ #define __DOWNLOADS_MODEL_H__ #include #include #include #include #include #include #include class DownloadsModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QString databasePath READ databasePath WRITE setDatabasePath NOTIFY databasePathChanged) Q_PROPERTY(int count READ rowCount NOTIFY rowCountChanged) Q_ENUMS(Roles) public: DownloadsModel(QObject* parent=0); ~DownloadsModel(); enum Roles { DownloadId = Qt::UserRole + 1, Url, Path, Filename, Mimetype, Complete, Paused, Error, Created }; // reimplemented from QAbstractListModel QHash roleNames() const; int rowCount(const QModelIndex& parent=QModelIndex()) const; QVariant data(const QModelIndex& index, int role) const; bool canFetchMore(const QModelIndex &parent = QModelIndex()) const; void fetchMore(const QModelIndex &parent = QModelIndex()); const QString databasePath() const; void setDatabasePath(const QString& path); Q_INVOKABLE bool contains(const QString& downloadId) const; Q_INVOKABLE void add(const QString& downloadId, const QUrl& url, const QString& mimetype); Q_INVOKABLE void moveToDownloads(const QString& downloadId, const QString& path); Q_INVOKABLE void setPath(const QString& downloadId, const QString& path); Q_INVOKABLE void setComplete(const QString& downloadId, const bool complete); Q_INVOKABLE void setError(const QString& downloadId, const QString& error); Q_INVOKABLE void deleteDownload(const QString& path); Q_INVOKABLE void cancelDownload(const QString& downloadId); Q_INVOKABLE void pauseDownload(const QString& downloadId); Q_INVOKABLE void resumeDownload(const QString& downloadId); Q_SIGNALS: void databasePathChanged() const; void added(const QString& downloadId, const QUrl& url, const QString& mimetype) const; void pathChanged(const QString& downloadId, const QString& path) const; void completeChanged(const QString& downloadId, const bool complete) const; void errorChanged(const QString& downloadId, const QString& error) const; void deleted(const QString& path) const; void rowCountChanged(); private: QSqlDatabase m_database; int m_numRows; int m_fetchedCount; bool m_canFetchMore; struct DownloadEntry { QString downloadId; QUrl url; QString path; QString filename; QString mimetype; bool complete; bool paused; QString error; QDateTime created; }; QList m_orderedEntries; void resetDatabase(const QString& databaseName); void createOrAlterDatabaseSchema(); void insertNewEntryInDatabase(const DownloadEntry& entry); void removeExistingEntryFromDatabase(const QString& path); void reload(); }; #endif // __DOWNLOADS_MODEL_H__ ./src/app/webbrowser/webbrowser-app.qml0000644000004100000410000000450113004613604020442 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import QtQuick.Window 2.2 import Ubuntu.Components 1.3 import ".." BrowserWindow { id: window property alias urls: browser.initialUrls property alias newSession: browser.newSession currentWebview: browser.currentWebview title: { if (browser.title) { // TRANSLATORS: %1 refers to the current page’s title return i18n.tr("%1 - Ubuntu Web Browser").arg(browser.title) } else { return i18n.tr("Ubuntu Web Browser") } } Browser { id: browser anchors.fill: parent webbrowserWindow: webbrowserWindowProxy developerExtrasEnabled: window.developerExtrasEnabled fullscreen: window.visibility === Window.FullScreen Component.onCompleted: i18n.domain = "webbrowser-app" Keys.onPressed: { if ((event.key === Qt.Key_F11) && (event.modifiers === Qt.NoModifier)) { // F11 to toggle application-level fullscreen window.setFullscreen(window.visibility !== Window.FullScreen) if (currentWebview.fullscreen) { currentWebview.fullscreen = false } } } Keys.onEscapePressed: { // ESC to exit fullscreen, regardless of whether it was requested // by the page or toggled on by the user. window.setFullscreen(false) currentWebview.fullscreen = false } } onOpenUrls: { for (var i = 0; i < urls.length; ++i) { var setCurrent = (i == urls.length - 1) browser.openUrlInNewTab(urls[i], setCurrent, setCurrent) } } } ./src/app/webbrowser/TabPreview.qml0000644000004100000410000001032213004613604017551 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 Item { id: tabPreview property alias title: chrome.title property alias icon: chrome.icon property alias incognito: chrome.incognito property var tab readonly property url url: tab ? tab.url : "" // The first preview in the tabs list is a special case. // Since it’s the current tab, instead of displaying a // capture, the webview below it is displayed. property bool showContent: true signal selected() signal closed() TabChrome { id: chrome anchors { top: parent.top left: parent.left right: parent.right } tabWidth: units.gu(26) onSelected: tabPreview.selected() onClosed: tabPreview.closed() } Item { anchors { top: chrome.bottom topMargin: units.dp(-1) left: parent.left right: parent.right } height: parent.height clip: true Rectangle { anchors.fill: parent color: "#f4f4f4" visible: showContent } Image { visible: showContent && !previewContainer.visible source: "assets/tab-artwork.png" asynchronous: true fillMode: Image.PreserveAspectFit width: parent.width / 5 height: width anchors { right: parent.right rightMargin: -width / 5 bottom: parent.bottom bottomMargin: -height / 10 } } Label { visible: showContent && !previewContainer.visible text: i18n.tr("Tap to view") anchors { centerIn: parent verticalCenterOffset: units.gu(-2) } } Image { id: previewContainer visible: showContent && source.toString() && (status == Image.Ready) anchors { left: parent.left top: parent.top topMargin: -chrome.height } height: sourceSize.height fillMode: Image.Pad source: tabPreview.tab ? tabPreview.tab.preview : "" asynchronous: true cache: false onStatusChanged: { if (status == Image.Error) { // The cached preview doesn’t exist any longer tabPreview.tab.preview = "" } } } MouseArea { objectName: "selectArea" anchors.fill: parent acceptedButtons: Qt.AllButtons // 'clicked' events are emitted even if the cursor has been dragged // (http://doc.qt.io/qt-5/qml-qtquick-mousearea.html#clicked-signal), // but we don’t want a drag gesture to select the tab (when e.g. the // user has reached the top/bottom of the tabs view and starts another // gesture to drag further beyond the boundaries of the view). property point pos onPressed: { if (mouse.button == Qt.LeftButton) { pos = Qt.point(mouse.x, mouse.y) } } onReleased: { if (mouse.button == Qt.LeftButton) { var d = Math.sqrt(Math.pow(mouse.x - pos.x, 2) + Math.pow(mouse.y - pos.y, 2)) if (d < units.gu(1)) { tabPreview.selected() } } } } } } ./src/app/webbrowser/qmldir0000644000004100000410000000006013004613604016177 0ustar www-datawww-datasingleton PreviewManager 1.0 PreviewManager.qml ./src/app/webbrowser/SearchEngines.qml0000644000004100000410000000552713004613604020232 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Qt.labs.folderlistmodel 2.1 import webbrowserapp.private 0.1 Item { id: searchEngines property var searchPaths: [] readonly property var engines: ListModel {} Repeater { id: repeater model: searchEngines.searchPaths delegate: Item { property var folder: FolderListModel { folder: modelData showDirs: false nameFilters: ["*.xml"] sortField: FolderListModel.Name onCountChanged: delayedPopulation.restart() } } onItemRemoved: delayedPopulation.restart() } QtObject { id: internal function populateModel() { engines.clear() for (var i = repeater.count - 1; i >= 0; --i) { var folder = repeater.itemAt(i).folder for (var j = 0; j < folder.count; ++j) { var name = folder.get(j, "fileBaseName") var engine = searchEngineComponent.createObject(null, {filename: name}) var found = -1 for (var k = 0; k < engines.count; ++k) { if (engines.get(k).filename == name) { found = k break } } if (engine.valid && (found == -1)) { var insertIndex = 0 for (var k = 0; k < engines.count; ++k) { if (engines.get(k).filename > name) { insertIndex = k break } } engines.insert(k, {"filename": name}) } else if (!engine.valid && (found > -1)) { engines.remove(found) } } } } } Timer { id: delayedPopulation interval: 50 onTriggered: internal.populateModel() } Component { id: searchEngineComponent SearchEngine { searchPaths: searchEngines.searchPaths } } } ./src/app/webbrowser/Highlight.js0000644000004100000410000000311313004613604017233 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ function highlightTerms(text, terms) { var termsRe var highlight = '$&' var searchTerms = [] function escapeTerm(term) { // Escape special characters in a search term // (a simpler version of preg_quote). return term.replace(/[().?+|*^$]/g, '\\$&') } function setSearchTerms(terms) { searchTerms = terms termsRe = new RegExp(terms.map(escapeTerm).join('|'), 'ig') } // Highlight the matching terms in a case-insensitive manner if (text && text.toString()) { if (searchTerms !== terms) setSearchTerms(terms) if (searchTerms.length == 0) return text var highlighted = text.toString().replace(termsRe, highlight) highlighted = highlighted.replace(new RegExp('&', 'g'), '&') highlighted = '' + highlighted + '' return highlighted } else { return "" } } ./src/app/webbrowser/tabs-model.cpp0000644000004100000410000001524413004613604017531 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "tabs-model.h" // Qt #include #include #include /*! \class TabsModel \brief List model that stores the list of currently open tabs. TabsModel is a list model that stores the list of currently open tabs. Each tab holds a pointer to a Tab and associated metadata (URL, title, icon). The model doesn’t own the Tab, so it is the responsibility of whoever adds a tab to instantiate the corresponding Tab, and to destroy it after it’s removed from the model. */ TabsModel::TabsModel(QObject* parent) : QAbstractListModel(parent) , m_currentIndex(-1) { } TabsModel::~TabsModel() { } QHash TabsModel::roleNames() const { static QHash roles; if (roles.isEmpty()) { roles[Url] = "url"; roles[Title] = "title"; roles[Icon] = "icon"; roles[Tab] = "tab"; } return roles; } int TabsModel::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); return m_tabs.count(); } QVariant TabsModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } QObject* tab = m_tabs.at(index.row()); switch (role) { case Url: return tab->property("url"); case Title: return tab->property("title"); case Icon: return tab->property("icon"); case Tab: return QVariant::fromValue(tab); default: return QVariant(); } } int TabsModel::currentIndex() const { return m_currentIndex; } void TabsModel::setCurrentIndex(int index) { if (!checkValidTabIndex(index)) { return; } if (index != m_currentIndex) { m_currentIndex = index; Q_EMIT currentIndexChanged(); Q_EMIT currentTabChanged(); } } QObject* TabsModel::currentTab() const { if (m_tabs.isEmpty() || !checkValidTabIndex(m_currentIndex)) { return nullptr; } return m_tabs.at(m_currentIndex); } /*! Append a tab to the model and return the corresponding index in the model. It is the responsibility of the caller to instantiate the corresponding Tab beforehand. */ int TabsModel::add(QObject* tab) { return insert(tab, m_tabs.count()); } /*! Add a tab to the model at the specified index, and return the index itself, or -1 if the operation failed. It is the responsibility of the caller to instantiate the corresponding Tab beforehand. */ int TabsModel::insert(QObject* tab, int index) { if (tab == nullptr) { qWarning() << "Invalid Tab"; return -1; } index = qMax(qMin(index, m_tabs.count()), 0); beginInsertRows(QModelIndex(), index, index); m_tabs.insert(index, tab); connect(tab, SIGNAL(urlChanged()), SLOT(onUrlChanged())); connect(tab, SIGNAL(titleChanged()), SLOT(onTitleChanged())); connect(tab, SIGNAL(iconChanged()), SLOT(onIconChanged())); endInsertRows(); Q_EMIT countChanged(); if (m_currentIndex == -1) { // Set the index to zero if this is the first item that gets added to the // model, as it should not be possible to have items in the model but no // current tab. m_currentIndex = 0; Q_EMIT currentIndexChanged(); Q_EMIT currentTabChanged(); } else if (index == m_currentIndex) { Q_EMIT currentTabChanged(); } else if (index < m_currentIndex) { // Increment the index if we are inserting items before the current index. m_currentIndex++; Q_EMIT currentIndexChanged(); } return index; } /*! Given its index, remove a tab from the model, and return the corresponding Tab. It is the responsibility of the caller to destroy the corresponding Tab afterwards. */ QObject* TabsModel::remove(int index) { if (!checkValidTabIndex(index)) { return nullptr; } beginRemoveRows(QModelIndex(), index, index); QObject* tab = m_tabs.takeAt(index); tab->disconnect(this); endRemoveRows(); Q_EMIT countChanged(); if (index < m_currentIndex) { // If we removed any tab before the current one, decrease the // current index to match. m_currentIndex--; Q_EMIT currentIndexChanged(); } else if (index == m_currentIndex) { // If the current tab was removed, the following one (if any) is made // current. If it was the last tab in the model, the current index needs // to be decreased. if (m_currentIndex == m_tabs.count()) { m_currentIndex--; Q_EMIT currentIndexChanged(); } Q_EMIT currentTabChanged(); } return tab; } QObject* TabsModel::get(int index) const { if (!checkValidTabIndex(index)) { return nullptr; } return m_tabs.at(index); } void TabsModel::move(int from, int to) { if ((from == to) || !checkValidTabIndex(from) || !checkValidTabIndex(to)) { return; } beginMoveRows(QModelIndex(), from, from, QModelIndex(), to); m_tabs.move(from, to); endMoveRows(); if (m_currentIndex == from) { m_currentIndex = to; Q_EMIT currentIndexChanged(); } else if ((m_currentIndex >= to) && (m_currentIndex < from)) { m_currentIndex++; Q_EMIT currentIndexChanged(); } else if ((m_currentIndex > from) && (m_currentIndex <= to)) { m_currentIndex--; Q_EMIT currentIndexChanged(); } } bool TabsModel::checkValidTabIndex(int index) const { if ((index < 0) || (index >= m_tabs.count())) { qWarning() << "Invalid tab index:" << index; return false; } return true; } void TabsModel::onDataChanged(QObject* tab, int role) { int index = m_tabs.indexOf(tab); if (checkValidTabIndex(index)) { Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), QVector() << role); } } void TabsModel::onUrlChanged() { onDataChanged(sender(), Url); } void TabsModel::onTitleChanged() { onDataChanged(sender(), Title); } void TabsModel::onIconChanged() { onDataChanged(sender(), Icon); } ./src/app/webbrowser/text-search-filter-model.cpp0000644000004100000410000001045413004613604022310 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "text-search-filter-model.h" #include #include /*! \class TextSearchFilterModel \brief Proxy model that filters the contents of a model based on a list of keywords applied to multiple fields. TextSearchFilterModel is a proxy model that filters the contents of a model based on a list of terms string that is applied to multiple user defined fields (matching role names in the source model). An item in the source model is returned by this model if all the search terms are contained in any of the item's fields (i.e. given two search terms, if one is found in a field and the other in a different field, then the item will be returned, but it will not be returned if only one is found) If no searchTerms and/or no searchFields are present, all entries from the source model are returned. */ TextSearchFilterModel::TextSearchFilterModel(QObject* parent) : QSortFilterProxyModel(parent) { } QVariant TextSearchFilterModel::sourceModel() const { QAbstractItemModel* source = QSortFilterProxyModel::sourceModel(); return (source) ? QVariant::fromValue(source) : QVariant(); } void TextSearchFilterModel::setSourceModel(QVariant sourceModel) { QAbstractItemModel* currentSource = QSortFilterProxyModel::sourceModel(); QAbstractItemModel* newSource = qvariant_cast(sourceModel); if (newSource != currentSource) { updateSearchRoles(newSource); QSortFilterProxyModel::setSourceModel(newSource); Q_EMIT sourceModelChanged(); Q_EMIT countChanged(); } } void TextSearchFilterModel::setTerms(const QStringList& terms) { if (terms != m_terms) { m_terms = terms; invalidateFilter(); Q_EMIT termsChanged(); Q_EMIT countChanged(); } } const QStringList& TextSearchFilterModel::terms() const { return m_terms; } void TextSearchFilterModel::setSearchFields(const QStringList& searchFields) { if (searchFields != m_searchFields) { m_searchFields = searchFields; updateSearchRoles(QSortFilterProxyModel::sourceModel()); invalidateFilter(); Q_EMIT searchFieldsChanged(); Q_EMIT countChanged(); } } const QStringList& TextSearchFilterModel::searchFields() const { return m_searchFields; } void TextSearchFilterModel::updateSearchRoles(const QAbstractItemModel* model) { m_searchRoles.clear(); if (model) { Q_FOREACH(const QString& field, m_searchFields) { int role = model->roleNames().key(field.toUtf8(), -1); if (role != -1) { m_searchRoles.append(role); } else { qWarning() << "Source model does not have role matching field:" << field; } } } } bool TextSearchFilterModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { if (m_terms.isEmpty() || m_searchFields.isEmpty()) { return true; } QAbstractItemModel* source = QSortFilterProxyModel::sourceModel(); QModelIndex index = source->index(source_row, 0, source_parent); QSet foundTerms; int searchTermsCount = QSet::fromList(m_terms).count(); Q_FOREACH(int role, m_searchRoles) { QString value = source->data(index, role).toString(); Q_FOREACH (const QString& term, m_terms) { if (value.contains(term, Qt::CaseInsensitive)) { foundTerms.insert(term); } if (foundTerms.count() == searchTermsCount) { return true; } } } return false; } int TextSearchFilterModel::count() const { return rowCount(); } ./src/app/webbrowser/TabsBar.qml0000644000004100000410000001445313004613604017030 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import ".." Item { id: root property alias model: repeater.model property real minTabWidth: 0 //units.gu(6) property real maxTabWidth: units.gu(20) property real tabWidth: model ? Math.max(Math.min(tabsContainer.maxWidth / model.count, maxTabWidth), minTabWidth) : 0 property bool incognito: false property color fgColor: Theme.palette.normal.baseText property bool touchEnabled: true signal switchToTab(int index) signal requestNewTab(int index, bool makeCurrent) signal tabClosed(int index) MouseArea { anchors.fill: parent onWheel: { var angle = (wheel.angleDelta.x != 0) ? wheel.angleDelta.x : wheel.angleDelta.y if ((angle < 0) && (root.model.currentIndex < (root.model.count - 1))) { switchToTab(root.model.currentIndex + 1) } else if ((angle > 0) && (root.model.currentIndex > 0)) { switchToTab(root.model.currentIndex - 1) } } } MouseArea { id: newTabButton objectName: "newTabButton" anchors { left: tabsContainer.right leftMargin: units.gu(1) top: parent.top bottom: parent.bottom } width: height visible: !repeater.reordering Icon { width: units.gu(2) height: units.gu(2) anchors.centerIn: parent name: "add" color: incognito ? "white" : root.fgColor } onClicked: root.requestNewTab(root.model.count, true) } Component { id: contextualOptionsComponent ActionSelectionPopover { id: menu objectName: "tabContextualActions" property int targetIndex readonly property var tab: root.model.get(targetIndex) actions: ActionList { Action { objectName: "tab_action_new_tab" text: i18n.tr("New Tab") onTriggered: root.requestNewTab(menu.targetIndex + 1, false) } Action { objectName: "tab_action_reload" text: i18n.tr("Reload") enabled: menu.tab.url.toString().length > 0 onTriggered: menu.tab.reload() } Action { objectName: "tab_action_close_tab" text: i18n.tr("Close Tab") onTriggered: root.tabClosed(menu.targetIndex) } } } } Item { id: tabsContainer objectName: "tabsContainer" anchors { top: parent.top bottom: parent.bottom left: parent.left } width: tabWidth * root.model.count readonly property real maxWidth: root.width - newTabButton.width - units.gu(2) Repeater { id: repeater property bool reordering: false delegate: MouseArea { id: tabDelegate objectName: "tabDelegate" readonly property int tabIndex: index anchors.top: tabsContainer.top property real rightMargin: units.dp(1) width: tabWidth + rightMargin height: tabsContainer.height acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton readonly property bool dragging: drag.active drag { target: (pressedButtons === Qt.LeftButton) ? tabDelegate : null axis: Drag.XAxis minimumX: 0 maximumX: root.width - tabDelegate.width filterChildren: true } TabItem { anchors.fill: parent active: tabIndex === root.model.currentIndex hoverable: true incognito: root.incognito title: model.title ? model.title : (model.url.toString() ? model.url : i18n.tr("New tab")) icon: model.icon fgColor: root.fgColor touchEnabled: root.touchEnabled rightMargin: tabDelegate.rightMargin onClosed: root.tabClosed(index) onSelected: root.switchToTab(index) onContextMenu: PopupUtils.open(contextualOptionsComponent, tabDelegate, {"targetIndex": index}) } Binding { target: repeater property: "reordering" value: dragging } Binding on x { when: !dragging value: index * width } Behavior on x { NumberAnimation { duration: 250 } } onXChanged: { if (!dragging) return if (x < (index * width - width / 2)) { root.model.move(index, index - 1) } else if ((x > (index * width + width / 2)) && (index < (root.model.count - 1))) { root.model.move(index + 1, index) } } z: (root.model.currentIndex == index) ? 3 : 1 - index / root.model.count } } Rectangle { anchors { left: parent.left bottom: parent.bottom } width: root.width height: units.dp(1) color: "#cacaca" z: 2 } } } ./src/app/webbrowser/SadTab.qml0000644000004100000410000000543213004613604016645 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import com.canonical.Oxide 1.8 as Oxide Rectangle { property var webview signal closeTabRequested() Column { anchors { fill: parent margins: units.gu(4) } spacing: units.gu(4) Image { anchors.horizontalCenter: parent.horizontalCenter source: "assets/tab-error.png" } Label { anchors { left: parent.left right: parent.right } wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter text: webview ? i18n.tr("The rendering process has been closed for this tab.") : "" } Label { anchors { left: parent.left right: parent.right } wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter font.weight: Font.Light text: { if (!webview) { return "" } else if (webview.webProcessStatus == Oxide.WebView.WebProcessCrashed) { // TRANSLATORS: %1 is the URL of the page that crashed the renderer process return i18n.tr("Something went wrong while displaying %1.").arg(webview.url) } else if (webview.webProcessStatus == Oxide.WebView.WebProcessKilled) { return i18n.tr("The system is low on memory and can't display this webpage. Try closing unneeded tabs and reloading.") } else { return "" } } } Row { anchors.horizontalCenter: parent.horizontalCenter spacing: units.gu(2) Button { objectName: "closeTabButton" text: i18n.tr("Close tab") onClicked: closeTabRequested() } Button { objectName: "reloadButton" text: i18n.tr("Reload") color: UbuntuColors.green onClicked: webview.reload() } } } } ./src/app/webbrowser/bookmarks-folder-model.h0000644000004100000410000000340213004613604021477 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __BOOKMARKS_FOLDER_MODEL_H__ #define __BOOKMARKS_FOLDER_MODEL_H__ // Qt #include #include class BookmarksModel; class BookmarksFolderModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(BookmarksModel* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged) Q_PROPERTY(QString folder READ folder WRITE setFolder NOTIFY folderChanged) Q_PROPERTY(int count READ count NOTIFY countChanged) public: BookmarksFolderModel(QObject* parent=0); BookmarksModel* sourceModel() const; void setSourceModel(BookmarksModel* sourceModel); const QString& folder() const; void setFolder(const QString& domain); int count() const; Q_INVOKABLE QVariantMap get(int row) const; Q_SIGNALS: void sourceModelChanged() const; void folderChanged() const; void countChanged() const; protected: // reimplemented from QSortFilterProxyModel bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; private: QString m_folder; }; #endif // __BOOKMARKS_FOLDER_MODEL_H__ ./src/app/webbrowser/DownloadDelegate.qml0000644000004100000410000001770613004613604020720 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import ".." ListItem { id: downloadDelegate property var downloadManager property alias icon: mimeicon.name property alias image: thumbimage.source property alias title: title.text property alias url: url.text property string errorMessage property bool incomplete: false property string downloadId property var download property int progress: download ? download.progress : 0 property bool paused divider.visible: false signal removed() signal cancelled() height: visible ? (incomplete ? (paused ? units.gu(13) : units.gu(10)) : units.gu(7)) : 0 QtObject { id: internal function connectToDownloadObject() { if (incomplete && !download && downloadManager) { for(var i = 0; i < downloadManager.downloads.length; i++) { if (downloadManager.downloads[i].downloadId == downloadId) { download = downloadManager.downloads[i] } } } } } Component.onCompleted: internal.connectToDownloadObject() onDownloadManagerChanged: internal.connectToDownloadObject() Item { anchors { verticalCenter: parent.verticalCenter left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) } Item { id: iconContainer width: units.gu(3) height: width anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: downloadDelegate.incomplete ? -units.gu(1) : 0 Image { id: thumbimage asynchronous: true width: parent.width height: parent.height fillMode: Image.PreserveAspectFit sourceSize.width: parent.width sourceSize.height: parent.height anchors.verticalCenter: parent.verticalCenter } Image { id: mimeicon asynchronous: true anchors.fill: parent anchors.margins: units.gu(0.2) source: "image://theme/%1".arg(name != "" ? name : "save") visible: thumbimage.status !== Image.Ready cache: true property string name } } Item { anchors.top: iconContainer.top anchors.left: iconContainer.right anchors.leftMargin: units.gu(2) anchors.right: parent.right Column { id: detailsColumn width: parent.width - cancelColumn.width height: parent.height Label { id: title fontSize: "x-small" color: "#5d5d5d" elide: Text.ElideRight width: parent.width } Label { id: url fontSize: "x-small" color: "#5d5d5d" elide: Text.ElideRight width: parent.width } Item { height: error.visible ? units.gu(1) : units.gu(2) width: parent.width visible: downloadDelegate.incomplete } Item { id: error visible: incomplete && download === undefined || errorMessage !== "" height: units.gu(3) width: parent.width Icon { id: errorIcon width: units.gu(2) height: width anchors.verticalCenter: parent.verticalCenter name: "dialog-warning-symbolic" color: UbuntuColors.red } Label { width: parent.width - errorIcon.width anchors.left: errorIcon.right anchors.leftMargin: units.gu(1) anchors.verticalCenter: errorIcon.verticalCenter fontSize: "x-small" color: UbuntuColors.red text: errorMessage !== "" ? errorMessage : (incomplete && download === undefined) ? i18n.tr("Download failed") : "" elide: Text.ElideRight } } IndeterminateProgressBar { id: progressBar width: parent.width height: units.gu(0.5) visible: downloadDelegate.incomplete && !error.visible progress: downloadDelegate.progress // Work around UDM bug #1450144 indeterminateProgress: downloadDelegate.progress < 0 || downloadDelegate.progress > 100 } } Column { id: cancelColumn spacing: units.gu(1) anchors.top: detailsColumn.top anchors.left: detailsColumn.right anchors.leftMargin: units.gu(2) width: downloadDelegate.incomplete && !error.visible ? cancelButton.width + units.gu(2) : 0 Button { visible: downloadDelegate.incomplete && !error.visible id: cancelButton text: i18n.tr("Cancel") onClicked: { if (download) { download.cancel() cancelled() } } } Label { visible: !progressBar.indeterminateProgress && downloadDelegate.incomplete && !error.visible && !downloadDelegate.paused width: cancelButton.width horizontalAlignment: Text.AlignHCenter fontSize: "x-small" text: progressBar.progress + "%" } Button { visible: downloadDelegate.paused text: i18n.tr("Resume") width: cancelButton.width onClicked: { if (download) { download.resume() } } } } } } leadingActions: error.visible || !downloadDelegate.incomplete ? deleteActionList : null ListItemActions { id: deleteActionList actions: [ Action { objectName: "leadingAction.delete" iconName: "delete" enabled: error.visible || !downloadDelegate.incomplete onTriggered: error.visible ? downloadDelegate.cancelled() : downloadDelegate.removed() } ] } } ./src/app/webbrowser/BookmarksFoldersView.qml0000644000004100000410000002256113004613604021613 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 as ListItem import webbrowserapp.private 0.1 import "BookmarksModelUtils.js" as BookmarksModelUtils FocusScope { id: bookmarksFoldersViewItem property alias interactive: bookmarksFolderListView.interactive property url homeBookmarkUrl readonly property Item currentItem: bookmarksFolderListView.currentItem ? bookmarksFolderListView.currentItem.currentItem : null signal bookmarkClicked(url url) signal bookmarkRemoved(url url) height: bookmarksFolderListView.contentHeight BookmarksFolderListModel { id: bookmarksFolderListModel sourceModel: BookmarksModel } ListView { id: bookmarksFolderListView objectName: "bookmarksFolderListView" anchors.fill: parent interactive: false focus: true model: bookmarksFolderListModel delegate: Loader { objectName: "bookmarkFolderDelegateLoader" anchors { left: parent.left right: parent.right } height: active ? item.height : 0 active: (entries.count > 0) || !folder readonly property Item currentItem: active ? item.currentItem : null onActiveChanged: { if (!active && activeFocus) { bookmarksFolderListView.decrementCurrentIndex() } } sourceComponent: FocusScope { objectName: "bookmarkFolderDelegate" focus: true readonly property Item currentItem: activeFocus ? (bookmarkFolderHeader.focus ? bookmarkFolderHeader : bookmarksInFolderLoader.item.currentItem) : null function focusHeader() { bookmarkFolderHeader.focus = true } function focusBookmarks() { bookmarksInFolderLoader.focus = true } property string folderName: folder anchors { left: parent ? parent.left : undefined right: parent ? parent.right : undefined } height: childrenRect.height property bool expanded: folderName ? false : true Item { id: bookmarkFolderHeader objectName: "bookmarkFolderHeader" anchors { top: parent.top left: parent.left right: parent.right } height: units.gu(6.5) focus: true Row { anchors { left: parent.left leftMargin: units.gu(2.5) right: parent.right } height: units.gu(6) spacing: units.gu(1.5) Icon { id: expandedIcon name: expanded ? "go-down" : "go-next" height: units.gu(2) width: height anchors { leftMargin: units.gu(1) topMargin: units.gu(2) top: parent.top } } Label { width: parent.width - expandedIcon.width - units.gu(3) anchors.verticalCenter: expandedIcon.verticalCenter text: folderName ? folderName : i18n.tr("All Bookmarks") fontSize: "small" } } ListItem.ThinDivider { anchors { left: parent.left right: parent.right bottom: parent.bottom bottomMargin: units.gu(1) } } ListViewHighlight { anchors.fill: parent visible: hasKeyboard && parent.activeFocus } MouseArea { anchors.fill: parent onClicked: expanded = !expanded } Keys.onEnterPressed: expanded = !expanded Keys.onReturnPressed: expanded = !expanded Keys.onSpacePressed: expanded = !expanded } Loader { id: bookmarksInFolderLoader anchors { top: bookmarkFolderHeader.bottom left: parent.left right: parent.right } height: item ? item.contentHeight : 0 visible: status == Loader.Ready active: expanded onActiveChanged: { if (!active && focus) { focusHeader() } } sourceComponent: ListView { readonly property bool isAllBookmarksFolder: folder === "" focus: true interactive: false currentIndex: 0 model: { if (isAllBookmarksFolder) { return BookmarksModelUtils.prependHomepageToBookmarks(entries, { title: i18n.tr("Homepage"), url: bookmarksFoldersViewItem.homeBookmarkUrl }) } return entries } delegate: UrlDelegate{ id: urlDelegate objectName: "urlDelegate_%1".arg(index) property var entry: isAllBookmarksFolder ? modelData : model width: parent.width height: units.gu(5) removable: !isAllBookmarksFolder || index !== 0 icon: entry.icon ? entry.icon : "" title: entry.title ? entry.title : entry.url url: entry.url onClicked: bookmarksFoldersViewItem.bookmarkClicked(url) onRemoved: bookmarksFoldersViewItem.bookmarkRemoved(url) } highlight: ListViewHighlight {} Keys.onUpPressed: { if (currentIndex > 0) { --currentIndex } else { focusHeader() } } Keys.onDownPressed: { if (currentIndex < (count - 1)) { ++currentIndex } else { event.accepted = false } } Keys.onEnterPressed: currentItem.clicked() Keys.onReturnPressed: currentItem.clicked() Keys.onDeletePressed: currentItem.removed() } } Keys.onDownPressed: { if (expanded && !bookmarksInFolderLoader.focus) { focusBookmarks() } else { event.accepted = false } } } } Keys.onUpPressed: { var current = currentIndex --currentIndex while (currentItem && !currentItem.active) { --currentIndex } if (!currentItem) { currentIndex = current event.accepted = false } } Keys.onDownPressed: { var current = currentIndex ++currentIndex while (currentItem && !currentItem.active) { ++currentIndex } if (!currentItem || !currentItem.active) { currentIndex = current } } } // Initially focus the first bookmark Component.onCompleted: { if (bookmarksFolderListView.currentItem) { bookmarksFolderListView.currentItem.item.focusBookmarks() } } } ./src/app/webbrowser/BrowserTab.qml0000644000004100000410000001540513004613623017563 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Web 0.2 import com.canonical.Oxide 1.4 as Oxide import webbrowserapp.private 0.1 import "." FocusScope { id: tab property string uniqueId: this.toString() + "-" + Date.now() property url initialUrl property string initialTitle property url initialIcon property string restoreState property int restoreType property var request property Component webviewComponent readonly property var webview: webviewContainer.webview readonly property url url: webview ? webview.url : initialUrl readonly property string title: webview ? webview.title : initialTitle readonly property url icon: webview ? webview.icon : initialIcon property url preview property bool current: false readonly property real lastCurrent: internal.lastCurrent property bool incognito visible: false // Used as a workaround for https://launchpad.net/bugs/1502675 : // invoke this on a tab shortly before it is set current. signal aboutToShow() Connections { target: PreviewManager onPreviewSaved: { if (pageUrl !== url) return if (preview == previewUrl) { // Ensure that the preview URL actually changes, // for the image to be reloaded preview = "" } preview = previewUrl } } FocusScope { id: webviewContainer anchors.fill: parent focus: true property var webview: null } function load() { if (!webview && !internal.incubator) { var properties = {'tab': tab, 'incognito': incognito} if (restoreState) { properties['restoreState'] = restoreState properties['restoreType'] = restoreType } else { properties['url'] = initialUrl } var incubator = webviewComponent.incubateObject(webviewContainer, properties) if (incubator === null) { console.warn("Webview incubator failed to initialize") return } if (incubator.status === Component.Ready) { webviewContainer.webview = incubator.object return } internal.incubator = incubator incubator.onStatusChanged = function(status) { if (status === Component.Ready) { webviewContainer.webview = incubator.object } else if (status === Component.Error) { console.warn("Webview failed to incubate") } internal.incubator = null } } } function unload() { if (webview) { initialUrl = webview.url initialTitle = webview.title initialIcon = webview.icon restoreState = webview.currentState restoreType = Oxide.WebView.RestoreCurrentSession webview.destroy() gc() } } function reload() { if (webview) { webview.reload() } else { load() } } function close() { var _url = url unload() if (_url.toString()) PreviewManager.checkDelete(_url) destroy() } QtObject { id: internal property bool hiding: false property var incubator: null property real lastCurrent: 0 } // When current is set to false, delay hiding the tab contents to give it // an opportunity to grab an up-to-date capture. This works well if and // only if embedders do not set the 'visible' property directly or // indirectly on instances of a BrowserTab. onCurrentChanged: { internal.lastCurrent = Date.now() if (current) { internal.hiding = false z = 1 opacity = 1 visible = true } else if (visible && !internal.hiding) { z = -1 if (!webview || webview.incognito) { // XXX: Do not grab a capture in incognito mode, as we don’t // want to write anything to disk. This means tab previews won’t // be available. In the future, we’ll want to grab a capture // and cache it in memory, but QQuickItem::grabToImage doesn’t // allow that. visible = false return } if (url.toString().length === 0) { visible = false return } internal.hiding = true webview.grabToImage(function(result) { if (!internal.hiding) { return } internal.hiding = false visible = false PreviewManager.saveToDisk(result, url) }) } } // Take a capture of the current page shortly after it has finished // loading to give rendering an opportunity to complete. There is // unfortunately no signal to notify us when rendering has completed. Timer { id: delayedCapture interval: 500 onTriggered: { if (webview && current && visible && !internal.hiding) { webview.grabToImage(function(result) { PreviewManager.saveToDisk(result, url) }) } } } Connections { target: webview onLoadingStateChanged: { if (!webview.loading && !webview.incognito) { delayedCapture.restart() } } } onAboutToShow: { if (!current) { opacity = 0 z = 1 visible = true load() } } Component.onCompleted: { if (request) { // Instantiating the webview cannot be delayed because the request // object is destroyed after exiting the newViewRequested signal handler. var properties = {"tab": tab, "request": request, 'incognito': incognito} webviewContainer.webview = webviewComponent.createObject(webviewContainer, properties) } } } ./src/app/webbrowser/DraggableUrlDelegateWide.qml0000644000004100000410000000536013004613604022306 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 UrlDelegateWide { id: item z: Drag.active ? 1 : 0 // display on top of siblings while dragging Drag.active: gripArea.drag.active Drag.hotSpot.x: grip.x Drag.hotSpot.y: grip.y Drag.onActiveChanged: { if (item.Drag.active) { internal.positionBeforeDrag = Qt.point(x, y) item.dragStarted() } } property bool draggable: true property int gripMargin: units.gu(1) signal dragStarted() signal dragEnded(var dragAndDrop) // only monitors hover events without capturing any click or drag MouseArea { id: hoverArea anchors.fill: parent acceptedButtons: Qt.NoButton hoverEnabled: true } Icon { id: grip objectName: "dragGrip" anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: item.gripMargin width: units.gu(3) height: width name: "view-grid-symbolic" opacity: item.draggable && hoverArea.containsMouse ? 1.0 : 0.0 Behavior on opacity { NumberAnimation { duration: UbuntuAnimation.SnapDuration easing: UbuntuAnimation.StandardEasing } } MouseArea { id: gripArea anchors.fill: parent drag.target: item.draggable ? item : null onReleased: { var result = { success: false, target: item.Drag.target } item.dragEnded(result) if (result.success) item.Drag.drop() else { item.x = internal.positionBeforeDrag.x item.y = internal.positionBeforeDrag.y item.Drag.cancel() } } } } Rectangle { anchors.fill: parent color: "transparent" border.color: UbuntuColors.lightGrey border.width: 1 visible: item.Drag.active } QtObject { id: internal property point positionBeforeDrag } } ./src/app/webbrowser/UrlPreviewDelegate.qml0000644000004100000410000001060713004613604021246 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import webbrowserapp.private 0.1 import ".." import "." AbstractButton { id: preview property url icon property alias title: titleLabel.text property url url property bool showFavicon: true property alias previewHeight: previewShape.height property alias previewWidth: previewShape.width signal setCurrent() signal removed() onPressAndHold: previewShape.openContextMenu() Column { id: contentColumn anchors.left: parent.left anchors.top: parent.top spacing: units.gu(1) Item { anchors.left: parent.left anchors.right: parent.right height: titleLabel.height Loader { id: favicon anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter sourceComponent: Favicon { source: preview.icon anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter } active: preview.showFavicon } Label { id: titleLabel anchors.left: favicon.right anchors.leftMargin: showFavicon ? units.gu(1) : 0 anchors.right: parent.right anchors.top: parent.top text: preview.title elide: Text.ElideRight fontSize: "small" } } UbuntuShape { id: previewShape anchors.left: parent.left width: units.gu(26) height: units.gu(16) backgroundColor: "#f7f7f7" property url previewUrl: Qt.resolvedUrl(PreviewManager.previewPathFromUrl(preview.url)) readonly property bool hasPreview: FileOperations.exists(previewUrl) source: Image { id: previewImage source: previewShape.hasPreview ? previewShape.previewUrl : "" sourceSize.width: previewShape.width cache: false } sourceFillMode: UbuntuShape.PreserveAspectCrop sourceHorizontalAlignment: UbuntuShape.AlignLeft sourceVerticalAlignment: UbuntuShape.AlignTop Connections { target: PreviewManager onPreviewSaved: { if (pageUrl != preview.url) return previewImage.source = "" previewImage.source = previewShape.previewUrl } } function openContextMenu() { preview.setCurrent() PopupUtils.open(contextMenuComponent, previewShape) } Image { anchors.centerIn: parent width: units.gu(2.5) height: units.gu(2.5) source: previewShape.hasPreview ? "" : "assets/stock_website.png" fillMode: Image.PreserveAspectFit } } } MouseArea { anchors.fill: contentColumn acceptedButtons: Qt.RightButton onClicked: previewShape.openContextMenu() } Component { id: contextMenuComponent ActionSelectionPopover { objectName: "urlPreviewDelegate.contextMenu" grabDismissAreaEvents: true actions: ActionList { Action { objectName: "delete" text: i18n.tr("Remove") onTriggered: { preview.removed() preview.GridView.view.forceActiveFocus() } } } } } } ./src/app/webbrowser/file-operations.h0000644000004100000410000000235713004613604020250 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __FILE_OPERATIONS_H__ #define __FILE_OPERATIONS_H__ #include #include class QUrl; class FileOperations : public QObject { Q_OBJECT public: explicit FileOperations(QObject* parent=0); Q_INVOKABLE bool exists(const QUrl& path) const; Q_INVOKABLE bool remove(const QUrl& file) const; Q_INVOKABLE bool mkpath(const QUrl& path) const; Q_INVOKABLE QStringList filesInDirectory(const QUrl& directory, const QStringList& filters) const; }; #endif // __FILE_OPERATIONS_H__ ./src/app/webbrowser/Chrome.qml0000644000004100000410000000667113004613604016732 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import ".." ChromeBase { id: chrome property var tabsModel property alias tab: navigationBar.tab property alias searchUrl: navigationBar.searchUrl property alias text: navigationBar.text property alias bookmarked: navigationBar.bookmarked signal toggleBookmark() property alias drawerActions: navigationBar.drawerActions property alias drawerOpen: navigationBar.drawerOpen property alias requestedUrl: navigationBar.requestedUrl property alias canSimplifyText: navigationBar.canSimplifyText property alias findInPageMode: navigationBar.findInPageMode property alias editing: navigationBar.editing property alias incognito: navigationBar.incognito property alias showTabsBar: tabsBar.active property alias showFaviconInAddressBar: navigationBar.showFaviconInAddressBar property alias availableHeight: navigationBar.availableHeight readonly property alias bookmarkTogglePlaceHolder: navigationBar.bookmarkTogglePlaceHolder property bool touchEnabled: true signal switchToTab(int index) signal requestNewTab(int index, bool makeCurrent) signal tabClosed(int index) backgroundColor: incognito ? UbuntuColors.darkGrey : "#ffffff" implicitHeight: tabsBar.height + navigationBar.height + content.anchors.topMargin function selectAll() { navigationBar.selectAll() } FocusScope { id: content anchors.fill: parent anchors.topMargin: showTabsBar ? units.gu(1) : 0 focus: true Rectangle { anchors.fill: navigationBar color: (showTabsBar || !incognito) ? "#ffffff" : UbuntuColors.darkGrey } Loader { id: tabsBar sourceComponent: TabsBar { model: tabsModel incognito: chrome.incognito fgColor: navigationBar.fgColor touchEnabled: chrome.touchEnabled onSwitchToTab: chrome.switchToTab(index) onRequestNewTab: chrome.requestNewTab(index, makeCurrent) onTabClosed: chrome.tabClosed(index) } anchors { top: parent.top left: parent.left right: parent.right } height: active ? (touchEnabled ? units.gu(4) : units.gu(3)) : 0 } NavigationBar { id: navigationBar fgColor: "#111111" iconColor: (incognito && !showTabsBar) ? "white" : fgColor focus: true anchors { bottom: parent.bottom left: parent.left right: parent.right } height: units.gu(6) onToggleBookmark: chrome.toggleBookmark() } } } ./src/app/webbrowser/searchengine.h0000644000004100000410000000452713004613604017604 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __SEARCH_ENGINE_H__ #define __SEARCH_ENGINE_H__ // Qt #include #include #include class SearchEngine : public QObject { Q_OBJECT Q_PROPERTY(QStringList searchPaths READ searchPaths WRITE setSearchPaths NOTIFY searchPathsChanged) Q_PROPERTY(QString filename READ filename WRITE setFilename NOTIFY filenameChanged) Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QString description READ description NOTIFY descriptionChanged) Q_PROPERTY(QString urlTemplate READ urlTemplate NOTIFY urlTemplateChanged) Q_PROPERTY(QString suggestionsUrlTemplate READ suggestionsUrlTemplate NOTIFY suggestionsUrlTemplateChanged) Q_PROPERTY(bool valid READ isValid NOTIFY validChanged) public: SearchEngine(QObject* parent=0); const QStringList& searchPaths() const; void setSearchPaths(const QStringList& searchPaths); const QString& filename() const; void setFilename(const QString& filename); const QString& name() const; const QString& description() const; const QString& urlTemplate() const; const QString& suggestionsUrlTemplate() const; bool isValid() const; Q_SIGNALS: void searchPathsChanged() const; void filenameChanged() const; void nameChanged() const; void descriptionChanged() const; void urlTemplateChanged() const; void suggestionsUrlTemplateChanged() const; void validChanged() const; private: void locateAndParseDescription(); QStringList m_searchPaths; QString m_filename; QString m_name; QString m_description; QString m_template; QString m_suggestionsTemplate; }; #endif // __SEARCH_ENGINE_H__ ./src/app/webbrowser/file-operations.cpp0000644000004100000410000000300113004613604020566 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "file-operations.h" #include #include #include #include FileOperations::FileOperations(QObject* parent) : QObject(parent) { } bool FileOperations::exists(const QUrl& path) const { // works for both files and directories return QFileInfo::exists(path.toLocalFile()); } bool FileOperations::remove(const QUrl& file) const { return QFile::remove(file.toLocalFile()); } bool FileOperations::mkpath(const QUrl& path) const { return QDir::root().mkpath(path.toLocalFile()); } QStringList FileOperations::filesInDirectory(const QUrl& directory, const QStringList& filters) const { return QDir(directory.toLocalFile()).entryList(filters, QDir::Files, QDir::Unsorted); } ./src/app/webbrowser/Browser.qml0000644000004100000410000023101013004613623017124 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import QtQuick.Window 2.2 import Qt.labs.settings 1.0 import com.canonical.Oxide 1.8 as Oxide import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import Unity.InputInfo 0.1 import webbrowserapp.private 0.1 import webbrowsercommon.private 0.1 import "../actions" as Actions import "../UrlUtils.js" as UrlUtils import ".." import "." import "." as Local BrowserView { id: browser // Should be true when the containing window is fullscreen. property bool fullscreen: false currentWebview: tabsModel && tabsModel.currentTab ? tabsModel.currentTab.webview : null readonly property var downloadManager: (downloadHandlerLoader.status == Loader.Ready) ? downloadHandlerLoader.item : null property bool newSession: false property bool incognito: false readonly property var tabsModel: incognito ? privateTabsModelLoader.item : publicTabsModel // Restore only the n most recent tabs at startup, // to limit the overhead of instantiating too many // tab objects (see http://pad.lv/1376433). readonly property int maxTabsToRestore: 10 onTabsModelChanged: { if (incognito && privateTabsModelLoader.item) { browser.openUrlInNewTab("", true) } else if (!incognito && tabsModel.currentTab) { // If the system is low on memory, the current public tab might // have been unloaded while browsing incognito, so reload it. tabsModel.currentTab.load() } } Connections { target: currentWebview /* Note that we are connecting the mediaAccessPermissionRequested signal on the current webview only because we want all the tabs that are not visible to automatically deny the request but emit the signal again if the same origin requests permissions (which is the default behavior in oxide if we don't connect a signal handler), so that we can pop-up a dialog asking the user for permission. Design is working on a new component that allows per-tab non-modal dialogs that will allow asking permission to the user without blocking interaction with the rest of the page or the window. When ready all tabs will have their mediaAccessPermissionRequested signal handled by creating one of these new dialogs. */ onMediaAccessPermissionRequested: PopupUtils.open(mediaAccessDialogComponent, null, { request: request }) } InputDeviceModel { id: miceModel deviceFilter: InputInfo.Mouse } InputDeviceModel { id: touchPadModel deviceFilter: InputInfo.TouchPad } InputDeviceModel { id: touchScreenModel deviceFilter: InputInfo.TouchScreen } FilteredKeyboardModel { id: keyboardModel } Component { id: mediaAccessDialogComponent MediaAccessDialog { } } actions: [ Actions.GoTo { onTriggered: currentWebview.url = value }, Actions.Back { enabled: currentWebview ? currentWebview.canGoBack : false onTriggered: currentWebview.goBack() }, Actions.Forward { enabled: currentWebview ? currentWebview.canGoForward : false onTriggered: currentWebview.goForward() }, Actions.Reload { enabled: currentWebview onTriggered: currentWebview.reload() }, Actions.Bookmark { enabled: currentWebview onTriggered: internal.addBookmark(currentWebview.url, currentWebview.title, currentWebview.icon) }, Actions.NewTab { onTriggered: browser.openUrlInNewTab("", true) }, Actions.ClearHistory { onTriggered: HistoryModel.clearAll() }, Actions.FindInPage { enabled: !chrome.findInPageMode && !newTabViewLoader.active onTriggered: { chrome.findInPageMode = true chrome.focus = true } } ] Settings { id: settings property url homepage: settingsDefaults.homepage property string searchEngine: settingsDefaults.searchEngine property bool restoreSession: settingsDefaults.restoreSession property int newTabDefaultSection: settingsDefaults.newTabDefaultSection property string defaultAudioDevice property string defaultVideoDevice function restoreDefaults() { homepage = settingsDefaults.homepage searchEngine = settingsDefaults.searchEngine restoreSession = settingsDefaults.restoreSession newTabDefaultSection = settingsDefaults.newTabDefaultSection defaultAudioDevice = settingsDefaults.defaultAudioDevice defaultVideoDevice = settingsDefaults.defaultVideoDevice } } QtObject { id: settingsDefaults readonly property url homepage: "http://start.ubuntu.com" readonly property string searchEngine: "google" readonly property bool restoreSession: true readonly property int newTabDefaultSection: 0 readonly property string defaultAudioDevice: "" readonly property string defaultVideoDevice: "" } FocusScope { id: contentsContainer anchors.fill: parent visible: !settingsViewLoader.active && !historyViewLoader.active && !bookmarksViewLoader.active && !downloadsViewLoader.active FocusScope { id: tabContainer anchors { left: parent.left right: parent.right top: parent.top } height: parent.height - osk.height - bottomEdgeBar.height focus: !errorSheetLoader.focus && !invalidCertificateErrorSheetLoader.focus && !newTabViewLoader.focus && !sadTabLoader.focus Keys.onPressed: { if (tabContainer.visible && (event.key == Qt.Key_Backspace)) { // Not handled as a window-level shortcut as it would take // precedence over backspace events in HTML text fields // (https://launchpad.net/bugs/1569938). if (event.modifiers == Qt.NoModifier) { internal.historyGoBack() event.accepted = true } else if (event.modifiers == Qt.ShiftModifier) { internal.historyGoForward() event.accepted = true } } } } Loader { id: errorSheetLoader anchors { fill: tabContainer topMargin: (chrome.state == "shown") ? chrome.height : 0 } sourceComponent: ErrorSheet { visible: currentWebview ? currentWebview.lastLoadFailed : false url: currentWebview ? currentWebview.url : "" onRefreshClicked: currentWebview.reload() } focus: item.visible asynchronous: true } Loader { id: invalidCertificateErrorSheetLoader anchors { fill: tabContainer topMargin: (chrome.state == "shown") ? chrome.height : 0 } sourceComponent: InvalidCertificateErrorSheet { visible: currentWebview && currentWebview.certificateError != null certificateError: currentWebview ? currentWebview.certificateError : null onAllowed: { // Automatically allow future requests involving this // certificate for the duration of the session. internal.allowCertificateError(currentWebview.certificateError) currentWebview.resetCertificateError() } onDenied: { currentWebview.resetCertificateError() } } focus: item.visible asynchronous: true } Loader { id: newTabViewLoader anchors { fill: tabContainer topMargin: (chrome.state == "shown") ? chrome.height : 0 } // Avoid loading the new tab view if the webview is about to load // content. Since WebView.restoreState is not a notifyable property, // this can’t be achieved with a simple property binding. Connections { target: currentWebview onUrlChanged: { newTabViewLoader.active = false } } active: false focus: active asynchronous: true Connections { target: browser onCurrentWebviewChanged: { if (currentWebview) { var tab = tabsModel.currentTab newTabViewLoader.active = !tab.url.toString() && !tab.restoreState } } } sourceComponent: browser.incognito ? newPrivateTabView : (browser.wide ? newTabViewWide : newTabView) Component { id: newTabView NewTabView { anchors.fill: parent settingsObject: settings focus: true onBookmarkClicked: { chrome.requestedUrl = url currentWebview.url = url tabContainer.forceActiveFocus() } onBookmarkRemoved: BookmarksModel.remove(url) onHistoryEntryClicked: { chrome.requestedUrl = url currentWebview.url = url tabContainer.forceActiveFocus() } Keys.onUpPressed: chrome.focus = true } } Component { id: newTabViewWide NewTabViewWide { anchors.fill: parent settingsObject: settings focus: true onBookmarkClicked: { chrome.requestedUrl = url currentWebview.url = url tabContainer.forceActiveFocus() } onBookmarkRemoved: BookmarksModel.remove(url) onHistoryEntryClicked: { chrome.requestedUrl = url currentWebview.url = url tabContainer.forceActiveFocus() } Keys.onUpPressed: chrome.focus = true } } Component { id: newPrivateTabView NewPrivateTabView { anchors.fill: parent } } } Loader { id: sadTabLoader anchors { fill: tabContainer topMargin: (chrome.state == "shown") ? chrome.height : 0 } active: webProcessMonitor.crashed || (webProcessMonitor.killed && !currentWebview.loading) focus: active sourceComponent: SadTab { webview: currentWebview onCloseTabRequested: internal.closeCurrentTab() } WebProcessMonitor { id: webProcessMonitor webview: currentWebview } asynchronous: true } } FocusScope { id: recentView objectName: "recentView" anchors.fill: parent visible: bottomEdgeHandle.dragging || tabslist.animating || (state == "shown") onVisibleChanged: chrome.hidden = visible states: State { name: "shown" } function closeAndSwitchToTab(index) { recentView.reset() internal.switchToTab(index, false) } Keys.onEscapePressed: closeAndSwitchToTab(0) TabsList { id: tabslist anchors.fill: parent model: tabsModel readonly property real delegateMinHeight: units.gu(20) delegateHeight: { if (recentView.state == "shown") { return Math.max(height / 3, delegateMinHeight) } else if (bottomEdgeHandle.stage == 0) { return height } else if (bottomEdgeHandle.stage == 1) { return (1 - 1.8 * bottomEdgeHandle.dragFraction) * height } else if (bottomEdgeHandle.stage >= 2) { return Math.max(height / 3, delegateMinHeight) } else { return delegateMinHeight } } chromeHeight: chrome.height onScheduleTabSwitch: { chrome.hidden = false internal.nextTabIndex = index } onTabSelected: recentView.closeAndSwitchToTab(index) onTabClosed: internal.closeTab(index) } Local.Toolbar { id: recentToolbar objectName: "recentToolbar" anchors { left: parent.left right: parent.right } height: units.gu(7) state: "hidden" color: browser.incognito ? UbuntuColors.darkGrey : "#f6f6f6" Button { objectName: "doneButton" anchors { left: parent.left leftMargin: units.gu(2) verticalCenter: parent.verticalCenter } strokeColor: browser.incognito? "#f6f6f6" : UbuntuColors.darkGrey text: i18n.tr("Done") onClicked: recentView.closeAndSwitchToTab(0) } ToolbarAction { objectName: "newTabButton" anchors { right: parent.right rightMargin: units.gu(2) verticalCenter: parent.verticalCenter } height: parent.height - units.gu(2) text: i18n.tr("New Tab") iconName: browser.incognito ? "private-tab-new" : "add" color: browser.incognito ? "#f6f6f6" : "#808080" onClicked: { recentView.reset() browser.openUrlInNewTab("", true) } } } function reset() { state = "" recentToolbar.state = "hidden" tabslist.reset() internal.resetFocus() } } SearchEngine { id: currentSearchEngine searchPaths: searchEnginesSearchPaths filename: settings.searchEngine } ChromeController { id: chromeController webview: browser.currentWebview forceHide: browser.fullscreen forceShow: recentView.visible defaultMode: (internal.hasMouse && !internal.hasTouchScreen) ? Oxide.LocationBarController.ModeShown : Oxide.LocationBarController.ModeAuto } Chrome { id: chrome tab: internal.nextTab || tabsModel.currentTab webview: tab ? tab.webview : null tabsModel: browser.tabsModel searchUrl: currentSearchEngine.urlTemplate incognito: browser.incognito showTabsBar: browser.wide showFaviconInAddressBar: !browser.wide availableHeight: tabContainer.height - height - y touchEnabled: internal.hasTouchScreen property bool hidden: false y: hidden ? -height : webview ? webview.locationBarController.offset : 0 Behavior on y { enabled: recentView.visible NumberAnimation { duration: UbuntuAnimation.FastDuration } } function isCurrentUrlBookmarked() { return tab ? BookmarksModel.contains(tab.url) : false } bookmarked: isCurrentUrlBookmarked() onToggleBookmark: { if (isCurrentUrlBookmarked()) BookmarksModel.remove(tab.url) else internal.addBookmark(tab.url, tab.title, tab.icon) } onWebviewChanged: bookmarked = isCurrentUrlBookmarked() Connections { target: chrome.tab onUrlChanged: chrome.bookmarked = chrome.isCurrentUrlBookmarked() } Connections { target: BookmarksModel onCountChanged: chrome.bookmarked = chrome.isCurrentUrlBookmarked() } onSwitchToTab: internal.switchToTab(index, true) onRequestNewTab: browser.openUrlInNewTab("", makeCurrent, true, index) onTabClosed: internal.closeTab(index) onFindInPageModeChanged: { if (!chrome.findInPageMode) internal.resetFocus() else chrome.forceActiveFocus() } anchors { left: parent.left right: parent.right } drawerActions: [ Action { objectName: "share" text: i18n.tr("Share") iconName: "share" enabled: (contentHandlerLoader.status == Loader.Ready) && chrome.tab && chrome.tab.url.toString() onTriggered: internal.shareLink(chrome.tab.url, chrome.tab.title) }, Action { objectName: "bookmarks" text: i18n.tr("Bookmarks") iconName: "bookmark" onTriggered: bookmarksViewLoader.active = true }, Action { objectName: "history" text: i18n.tr("History") iconName: "history" onTriggered: historyViewLoader.active = true }, Action { objectName: "findinpage" text: i18n.tr("Find in page") iconName: "search" enabled: !chrome.findInPageMode && !newTabViewLoader.active onTriggered: chrome.findInPageMode = true }, Action { objectName: "downloads" text: i18n.tr("Downloads") iconName: "save" enabled: downloadHandlerLoader.status == Loader.Ready && contentHandlerLoader.status == Loader.Ready onTriggered: downloadsViewLoader.active = true }, Action { objectName: "privatemode" text: browser.incognito ? i18n.tr("Leave Private Mode") : i18n.tr("Private Mode") iconName: "private-browsing" iconSource: browser.incognito ? Qt.resolvedUrl("assets/private-browsing-exit.svg") : "" onTriggered: { if (browser.incognito) { if (tabsModel.count > 1) { PopupUtils.open(leavePrivateModeDialog) } else { browser.incognito = false internal.resetFocus() } } else { browser.incognito = true } } }, Action { objectName: "settings" text: i18n.tr("Settings") iconName: "settings" onTriggered: settingsViewLoader.active = true } ] canSimplifyText: !browser.wide editing: activeFocus || suggestionsList.activeFocus Keys.onDownPressed: { if (suggestionsList.count) suggestionsList.focus = true else if (newTabViewLoader.status == Loader.Ready) { newTabViewLoader.forceActiveFocus() } } Keys.onEscapePressed: { if (chrome.findInPageMode) { chrome.findInPageMode = false } else { internal.resetFocus() } } } Suggestions { id: suggestionsList opacity: ((chrome.state == "shown") && (activeFocus || chrome.activeFocus) && (count > 0) && !chrome.drawerOpen && !chrome.findInPageMode) ? 1.0 : 0.0 Behavior on opacity { UbuntuNumberAnimation {} } enabled: opacity > 0 anchors { top: chrome.bottom horizontalCenter: parent.horizontalCenter } width: chrome.width - units.gu(5) height: enabled ? Math.min(contentHeight, tabContainer.height - chrome.height - units.gu(2)) : 0 searchTerms: chrome.text.split(/\s+/g).filter(function(term) { return term.length > 0 }) Keys.onUpPressed: chrome.focus = true Keys.onEscapePressed: internal.resetFocus() models: searchTerms && searchTerms.length > 0 ? [historySuggestions, bookmarksSuggestions, searchSuggestions.limit(4)] : [] LimitProxyModel { id: historySuggestions limit: 2 readonly property string icon: "history" readonly property bool displayUrl: true sourceModel: TextSearchFilterModel { sourceModel: HistoryModel terms: suggestionsList.searchTerms searchFields: ["url", "title"] } } LimitProxyModel { id: bookmarksSuggestions limit: 2 readonly property string icon: "non-starred" readonly property bool displayUrl: true sourceModel: TextSearchFilterModel { sourceModel: BookmarksModel terms: suggestionsList.searchTerms searchFields: ["url", "title"] } } SearchSuggestions { id: searchSuggestions terms: suggestionsList.searchTerms searchEngine: currentSearchEngine active: (chrome.activeFocus || suggestionsList.activeFocus) && !browser.incognito && !chrome.findInPageMode && !UrlUtils.looksLikeAUrl(chrome.text.replace(/ /g, "+")) function limit(number) { var slice = results.slice(0, number) slice.icon = 'search' slice.displayUrl = false return slice } } onActivated: { browser.currentWebview.url = url tabContainer.forceActiveFocus() chrome.requestedUrl = url } } onWideChanged: { if (wide) { recentView.reset() } else { // In narrow mode, the tabslist is a stack: the current tab is always at the top. tabsModel.move(tabsModel.currentIndex, 0) } } BottomEdgeHandle { id: bottomEdgeHandle objectName: "bottomEdgeHandle" anchors { left: parent.left right: parent.right bottom: parent.bottom } height: units.gu(2) enabled: !browser.wide && (recentView.state == "") && browser.currentWebview && (Screen.orientation == Screen.primaryOrientation) onDraggingChanged: { if (dragging) { if (browser.currentWebview) { browser.currentWebview.fullscreen = false } } else { if (stage == 1) { if (tabsModel.count > 1) { tabslist.selectAndAnimateTab(1) } else { recentView.state = "shown" recentToolbar.state = "shown" } } else if (stage == 2) { recentView.state = "shown" recentToolbar.state = "shown" } else if (stage >= 3) { recentView.state = "shown" recentToolbar.state = "shown" } } } } Image { id: bottomEdgeHint source: "assets/bottom_edge_hint.png" property bool forceShow: false anchors { horizontalCenter: parent.horizontalCenter bottom: parent.bottom bottomMargin: (((chrome.state == "shown") && browser.currentWebview && !browser.currentWebview.fullscreen) || forceShow) ? 0 : -height Behavior on bottomMargin { UbuntuNumberAnimation {} } } visible: bottomEdgeHandle.enabled && !internal.hasMouse opacity: recentView.visible ? 0 : 1 Behavior on opacity { UbuntuNumberAnimation {} } Label { anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter verticalCenterOffset: units.dp(2) } fontSize: "small" // TRANSLATORS: %1 refers to the current number of tabs opened text: i18n.tr("(%1)").arg(tabsModel ? tabsModel.count : 0) } } MouseArea { id: bottomEdgeBar objectName: "bottomEdgeBar" anchors { left: parent.left right: parent.right bottom: parent.bottom } enabled: !browser.wide && internal.hasMouse && (osk.state == "hidden") && (recentView.state == "") visible: enabled height: visible ? units.gu(4) : 0 // Ensure that this ends up below the chrome, so that the // drawer menu’s inverse mouse area covers it. z: -1 onClicked: { recentView.state = "shown" recentToolbar.state = "shown" } Rectangle { anchors.fill: parent color: "#f7f7f7" border { width: units.dp(1) color: "#cdcdcd" } } Label { anchors.centerIn: parent color: "#5d5d5d" // TRANSLATORS: %1 refers to the current number of tabs opened text: i18n.tr("(%1)").arg(tabsModel ? tabsModel.count : 0) } } Loader { id: bookmarksViewLoader anchors.fill: parent active: false sourceComponent: browser.wide ? bookmarksViewWideComponent : bookmarksViewComponent onStatusChanged: { if (status == Loader.Ready) { bookmarksViewLoader.item.forceActiveFocus() } else { internal.resetFocus() } } Keys.onEscapePressed: bookmarksViewLoader.active = false onActiveChanged: { if (active) { chrome.findInPageMode = false forceActiveFocus() } } Connections { target: bookmarksViewLoader.item onBookmarkEntryClicked: { browser.openUrlInNewTab(url, true) bookmarksViewLoader.active = false } onDone: bookmarksViewLoader.active = false onNewTabClicked: { browser.openUrlInNewTab("", true) bookmarksViewLoader.active = false } } Component { id: bookmarksViewComponent BookmarksView { anchors.fill: parent homepageUrl: settings.homepage } } Component { id: bookmarksViewWideComponent BookmarksViewWide { anchors.fill: parent homepageUrl: settings.homepage } } } Loader { id: historyViewLoader anchors.fill: parent active: false sourceComponent: browser.wide ? historyViewWideComponent : historyViewComponent onStatusChanged: { if (status == Loader.Ready) { historyViewLoader.item.loadModel() historyViewLoader.item.forceActiveFocus() } else { internal.resetFocus() } } Keys.onEscapePressed: historyViewLoader.active = false onActiveChanged: { if (active) { chrome.findInPageMode = false forceActiveFocus() } } Component { id: historyViewComponent FocusScope { signal loadModel() onLoadModel: children[0].loadModel() HistoryView { anchors.fill: parent focus: !expandedHistoryViewLoader.focus visible: focus onSeeMoreEntriesClicked: { expandedHistoryViewLoader.model = model expandedHistoryViewLoader.active = true } onNewTabRequested: browser.openUrlInNewTab("", true) onDone: historyViewLoader.active = false } Loader { id: expandedHistoryViewLoader asynchronous: true anchors.fill: parent active: false focus: active property var model: null sourceComponent: ExpandedHistoryView { focus: true model: expandedHistoryViewLoader.model onHistoryEntryClicked: { browser.openUrlInNewTab(url, true) historyViewLoader.active = false } onHistoryEntryRemoved: { if (count == 1) { done() } HistoryModel.removeEntryByUrl(url) } onDone: expandedHistoryViewLoader.active = false } } } } Component { id: historyViewWideComponent HistoryViewWide { anchors.fill: parent Keys.onEscapePressed: { historyViewLoader.active = false internal.resetFocus() } onHistoryEntryClicked: { browser.openUrlInNewTab(url, true) done() } onNewTabRequested: browser.openUrlInNewTab("", true) onDone: historyViewLoader.active = false } } } Loader { id: settingsViewLoader anchors.fill: parent active: false onStatusChanged: { if (status == Loader.Ready) { settingsViewLoader.item.forceActiveFocus() } else { internal.resetFocus() } } Keys.onEscapePressed: settingsViewLoader.active = false onActiveChanged: { if (active) { chrome.findInPageMode = false forceActiveFocus() } } sourceComponent: SettingsPage { anchors.fill: parent focus: true settingsObject: settings onDone: settingsViewLoader.active = false } } Loader { id: downloadsViewLoader anchors.fill: parent active: false source: "DownloadsPage.qml" onStatusChanged: { if (status == Loader.Ready) { item.forceActiveFocus() } else { internal.resetFocus() } } Keys.onEscapePressed: active = false onActiveChanged: { if (active) { forceActiveFocus() } } Binding { target: downloadsViewLoader.item property: "downloadManager" value: browser.downloadManager } Connections { target: downloadsViewLoader.item onDone: downloadsViewLoader.active = false } } TabsModel { id: publicTabsModel } Loader { id: privateTabsModelLoader sourceComponent: browser.incognito ? privateTabsModelComponent : undefined Component { id: privateTabsModelComponent TabsModel { Component.onDestruction: { while (count > 0) { var tab = remove(count - 1) if (tab) { tab.close() } } } } } } Loader { id: downloadHandlerLoader source: "DownloadHandler.qml" asynchronous: true } Component { id: tabComponent BrowserTab { anchors.fill: parent current: tabsModel && tabsModel.currentTab === this focus: current Item { id: contextualMenuTarget visible: false } webviewComponent: WebViewImpl { id: webviewimpl property BrowserTab tab readonly property bool current: tab.current currentWebview: browser.currentWebview filePicker: filePickerLoader.item anchors.fill: parent focus: true enabled: current && !bottomEdgeHandle.dragging && !recentView.visible locationBarController { height: chrome.height mode: chromeController.defaultMode } //experimental.preferences.developerExtrasEnabled: developerExtrasEnabled preferences.localStorageEnabled: true preferences.appCacheEnabled: true property QtObject contextModel: null contextualActions: ActionList { Actions.OpenLinkInNewTab { objectName: "OpenLinkInNewTabContextualAction" enabled: contextModel && contextModel.linkUrl.toString() onTriggered: browser.openUrlInNewTab(contextModel.linkUrl, true) } Actions.OpenLinkInNewBackgroundTab { objectName: "OpenLinkInNewBackgroundTabContextualAction" enabled: contextModel && contextModel.linkUrl.toString() onTriggered: browser.openUrlInNewTab(contextModel.linkUrl, false) } Actions.BookmarkLink { objectName: "BookmarkLinkContextualAction" enabled: contextModel && contextModel.linkUrl.toString() && !BookmarksModel.contains(contextModel.linkUrl) onTriggered: { // position the menu target with a one-off assignement instead of a binding // since the contents of the contextModel have meaning only while the context // menu is active contextualMenuTarget.x = contextModel.position.x contextualMenuTarget.y = contextModel.position.y + locationBarController.height + locationBarController.offset internal.addBookmark(contextModel.linkUrl, contextModel.linkText, "", contextualMenuTarget) } } Actions.CopyLink { objectName: "CopyLinkContextualAction" enabled: contextModel && contextModel.linkUrl.toString() onTriggered: Clipboard.push(["text/plain", contextModel.linkUrl.toString()]) } Actions.SaveLink { objectName: "SaveLinkContextualAction" enabled: contextModel && contextModel.linkUrl.toString() onTriggered: contextModel.saveLink() } Actions.Share { objectName: "ShareContextualAction" enabled: (contentHandlerLoader.status == Loader.Ready) && contextModel && (contextModel.linkUrl.toString() || contextModel.selectionText) onTriggered: { if (contextModel.linkUrl.toString()) { internal.shareLink(contextModel.linkUrl.toString(), contextModel.linkText) } else if (contextModel.selectionText) { internal.shareText(contextModel.selectionText) } } } Actions.OpenImageInNewTab { objectName: "OpenImageInNewTabContextualAction" enabled: contextModel && (contextModel.mediaType === Oxide.WebView.MediaTypeImage) && contextModel.srcUrl.toString() onTriggered: browser.openUrlInNewTab(contextModel.srcUrl, true) } Actions.CopyImage { objectName: "CopyImageContextualAction" enabled: contextModel && (contextModel.mediaType === Oxide.WebView.MediaTypeImage) && contextModel.srcUrl.toString() onTriggered: Clipboard.push(["text/plain", contextModel.srcUrl.toString()]) } Actions.SaveImage { objectName: "SaveImageContextualAction" enabled: contextModel && ((contextModel.mediaType === Oxide.WebView.MediaTypeImage) || (contextModel.mediaType === Oxide.WebView.MediaTypeCanvas)) && contextModel.hasImageContents onTriggered: contextModel.saveMedia() } Actions.OpenVideoInNewTab { objectName: "OpenVideoInNewTabContextualAction" enabled: contextModel && (contextModel.mediaType === Oxide.WebView.MediaTypeVideo) && contextModel.srcUrl.toString() onTriggered: browser.openUrlInNewTab(contextModel.srcUrl, true) } Actions.SaveVideo { objectName: "SaveVideoContextualAction" enabled: contextModel && (contextModel.mediaType === Oxide.WebView.MediaTypeVideo) && contextModel.srcUrl.toString() onTriggered: contextModel.saveMedia() } Actions.Undo { objectName: "UndoContextualAction" enabled: contextModel && contextModel.isEditable && (contextModel.editFlags & Oxide.WebView.UndoCapability) onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandUndo) } Actions.Redo { objectName: "RedoContextualAction" enabled: contextModel && contextModel.isEditable && (contextModel.editFlags & Oxide.WebView.RedoCapability) onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandRedo) } Actions.Cut { objectName: "CutContextualAction" enabled: contextModel && contextModel.isEditable && (contextModel.editFlags & Oxide.WebView.CutCapability) onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandCut) } Actions.Copy { objectName: "CopyContextualAction" enabled: contextModel && (contextModel.selectionText || (contextModel.isEditable && (contextModel.editFlags & Oxide.WebView.CopyCapability))) onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandCopy) } Actions.Paste { objectName: "PasteContextualAction" enabled: contextModel && contextModel.isEditable && (contextModel.editFlags & Oxide.WebView.PasteCapability) onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandPaste) } Actions.Erase { objectName: "EraseContextualAction" enabled: contextModel && contextModel.isEditable && (contextModel.editFlags & Oxide.WebView.EraseCapability) onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandErase) } Actions.SelectAll { objectName: "SelectAllContextualAction" enabled: contextModel && contextModel.isEditable && (contextModel.editFlags & Oxide.WebView.SelectAllCapability) onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandSelectAll) } } function contextMenuOnCompleted(menu) { contextModel = menu.contextModel if (contextModel.linkUrl.toString() || contextModel.srcUrl.toString() || contextModel.selectionText || (contextModel.isEditable && contextModel.editFlags) || (((contextModel.mediaType == Oxide.WebView.MediaTypeImage) || (contextModel.mediaType == Oxide.WebView.MediaTypeCanvas)) && contextModel.hasImageContents)) { menu.show() } else { contextModel.close() } } Component { id: contextMenuNarrowComponent ContextMenuMobile { actions: contextualActions Component.onCompleted: webviewimpl.contextMenuOnCompleted(this) } } Component { id: contextMenuWideComponent ContextMenuWide { webview: webviewimpl parent: browser actions: contextualActions Component.onCompleted: webviewimpl.contextMenuOnCompleted(this) } } contextMenu: browser.wide ? contextMenuWideComponent : contextMenuNarrowComponent onNewViewRequested: { var tab = tabComponent.createObject(tabContainer, {"request": request, 'incognito': browser.incognito}) var setCurrent = (request.disposition == Oxide.NewViewRequest.DispositionNewForegroundTab) internal.addTab(tab, setCurrent) if (setCurrent) tabContainer.forceActiveFocus() } onCloseRequested: prepareToClose() onPrepareToCloseResponse: { if (proceed) { if (tab) { for (var i = 0; i < tabsModel.count; ++i) { if (tabsModel.get(i) === tab) { tabsModel.remove(i) break } } tab.close() } if (tabsModel.count === 0) { browser.openUrlInNewTab("", true, true) } } } QtObject { id: webviewInternal property url storedUrl: "" property bool titleSet: false property string title: "" } onLoadEvent: { if (event.type == Oxide.LoadEvent.TypeCommitted) { chrome.findInPageMode = false webviewInternal.titleSet = false webviewInternal.title = title } if (webviewimpl.incognito) { return } if ((event.type == Oxide.LoadEvent.TypeCommitted) && !event.isError && (300 > event.httpStatusCode) && (event.httpStatusCode >= 200)) { webviewInternal.storedUrl = event.url HistoryModel.add(event.url, title, icon) } } onTitleChanged: { if (!webviewInternal.titleSet && webviewInternal.storedUrl.toString()) { // Record the title to avoid updating the history database // every time the page dynamically updates its title. // We don’t want pages that update their title every second // to achieve an ugly "scrolling title" effect to flood the // history database with updates. webviewInternal.titleSet = true webviewInternal.title = title HistoryModel.update(webviewInternal.storedUrl, title, icon) } } onIconChanged: { if (webviewInternal.storedUrl.toString()) { HistoryModel.update(webviewInternal.storedUrl, webviewInternal.title, icon) } } onGeolocationPermissionRequested: requestGeolocationPermission(request) property var certificateError function resetCertificateError() { certificateError = null } onCertificateError: { if (!error.isMainFrame || error.isSubresource) { // Not a main frame document error, just block the content // (it’s not overridable anyway). return } if (internal.isCertificateErrorAllowed(error)) { error.allow() } else { certificateError = error error.onCancelled.connect(webviewimpl.resetCertificateError) } } onFullscreenChanged: { if (fullscreen) { fullscreenExitHintComponent.createObject(webviewimpl) } } Component { id: fullscreenExitHintComponent Rectangle { id: fullscreenExitHint objectName: "fullscreenExitHint" anchors.centerIn: parent height: units.gu(6) width: Math.min(units.gu(50), parent.width - units.gu(12)) radius: units.gu(1) color: "#3e3b39" opacity: 0.85 Behavior on opacity { UbuntuNumberAnimation { duration: UbuntuAnimation.SlowDuration } } onOpacityChanged: { if (opacity == 0.0) { fullscreenExitHint.destroy() } } // Delay showing the hint to prevent it from jumping up while the // webview is being resized (https://launchpad.net/bugs/1454097). visible: false Timer { running: true interval: 250 onTriggered: fullscreenExitHint.visible = true } Label { color: "white" font.weight: Font.Light anchors.centerIn: parent text: bottomEdgeHandle.enabled ? i18n.tr("Swipe Up To Exit Full Screen") : i18n.tr("Press ESC To Exit Full Screen") } Timer { running: fullscreenExitHint.visible interval: 2000 onTriggered: fullscreenExitHint.opacity = 0 } Connections { target: webviewimpl onFullscreenChanged: { if (!webviewimpl.fullscreen) { fullscreenExitHint.destroy() } } } Component.onCompleted: bottomEdgeHint.forceShow = true Component.onDestruction: bottomEdgeHint.forceShow = false } } onShowDownloadDialog: { if (downloadDialogLoader.status === Loader.Ready) { var downloadDialog = PopupUtils.open(downloadDialogLoader.item, browser, {"contentType" : contentType, "downloadId" : downloadId, "singleDownload" : downloader, "filename" : filename, "mimeType" : mimeType}) downloadDialog.startDownload.connect(startDownload) } } function showDownloadsPage() { downloadsViewLoader.active = true return downloadsViewLoader.item } function startDownload(downloadId, download, mimeType) { DownloadsModel.add(downloadId, download.url, mimeType) download.start() downloadsViewLoader.active = true } } } } Component { id: bookmarkOptionsComponent BookmarkOptions { folderModel: BookmarksFolderListModel { sourceModel: BookmarksModel } Component.onCompleted: forceActiveFocus() onVisibleChanged: { if (!visible) { BookmarksModel.remove(bookmarkUrl) } } Component.onDestruction: { if (BookmarksModel.contains(bookmarkUrl)) { BookmarksModel.update(bookmarkUrl, bookmarkTitle, bookmarkFolder) } } // Fragile workaround for https://launchpad.net/bugs/1546677. // By destroying the popover, its visibility isn’t changed to // false, and thus the bookmark is not removed. Keys.onEnterPressed: destroy() Keys.onReturnPressed: destroy() } } QtObject { id: internal property var closedTabHistory: [] property int nextTabIndex: -1 readonly property var nextTab: (nextTabIndex > -1) ? tabsModel.get(nextTabIndex) : null onNextTabChanged: { if (nextTab) { nextTab.aboutToShow() } } readonly property bool hasMouse: (miceModel.count + touchPadModel.count) > 0 readonly property bool hasTouchScreen: touchScreenModel.count > 0 function getOpenPages() { var urls = [] for (var i = 0; i < tabsModel.count; i++) { var url = tabsModel.get(i).url if (url.toString()) urls.push(url) // exclude "new tab" tabs } return urls } function instantiateShareComponent() { var component = Qt.createComponent("../Share.qml") if (component.status == Component.Ready) { var share = component.createObject(browser) share.onDone.connect(share.destroy) return share } return null } function shareLink(url, title) { var share = instantiateShareComponent() if (share) share.shareLink(url, title) } function shareText(text) { var share = instantiateShareComponent() if (share) share.shareText(text) } function addTab(tab, setCurrent, index) { if (index === undefined) index = tabsModel.add(tab) else index = tabsModel.insert(tab, index) if (setCurrent) { chrome.requestedUrl = tab.initialUrl switchToTab(index, true) } } function closeTab(index) { // Save the incognito state before removing the tab, because // removing the last tab in the model will switch out incognito // mode, thus causing the check below to fail and save the tab // into the undo stack when it should be forgotten instead. var wasIncognito = incognito var tab = tabsModel.remove(index) if (tab) { if (!wasIncognito && tab.url.toString().length > 0) { closedTabHistory.push({ state: session.serializeTabState(tab), index: index }) } tab.close() } if (tabsModel.count === 0) { browser.openUrlInNewTab("", true) recentView.reset() } } function closeCurrentTab() { if (tabsModel.count > 0) { closeTab(tabsModel.currentIndex) } } function undoCloseTab() { if (!incognito && closedTabHistory.length > 0) { var tabInfo = closedTabHistory.pop() var tab = session.createTabFromState(tabInfo.state) addTab(tab, true, tabInfo.index) } } function switchToPreviousTab() { if (browser.wide) { internal.switchToTab((tabsModel.currentIndex - 1 + tabsModel.count) % tabsModel.count, true) } else { internal.switchToTab(tabsModel.count - 1, true) } } function switchToNextTab() { if (browser.wide) { internal.switchToTab((tabsModel.currentIndex + 1) % tabsModel.count, true) } else { internal.switchToTab(tabsModel.count - 1, true) } } function switchToTab(index, delayed) { if (delayed) { nextTabIndex = index delayedTabSwitcher.restart() } else { tabsModel.currentIndex = index nextTabIndex = -1 var tab = tabsModel.currentTab if (recentView.visible) { recentView.focus = true } else if (tab) { if (!tab.url.toString() && !tab.initialUrl.toString()) { maybeFocusAddressBar() } else { tabContainer.forceActiveFocus() } } } } function focusAddressBar(selectContent) { chrome.forceActiveFocus() Qt.inputMethod.show() // work around http://pad.lv/1316057 if (selectContent) chrome.selectAll() } function resetFocus() { if (browser.currentWebview) { if (!browser.currentWebview.url.toString()) { internal.maybeFocusAddressBar() } else { contentsContainer.forceActiveFocus() } } } function maybeFocusAddressBar() { if (keyboardModel.count > 0) { focusAddressBar() } else { contentsContainer.forceActiveFocus() } } // Invalid certificates the user has explicitly allowed for this session property var allowedCertificateErrors: [] function allowCertificateError(error) { var host = UrlUtils.extractHost(error.url) var code = error.certError var fingerprint = error.certificate.fingerprintSHA1 allowedCertificateErrors.push([host, code, fingerprint]) } function isCertificateErrorAllowed(error) { var host = UrlUtils.extractHost(error.url) var code = error.certError var fingerprint = error.certificate.fingerprintSHA1 for (var i in allowedCertificateErrors) { var allowed = allowedCertificateErrors[i] if ((host == allowed[0]) && (code == allowed[1]) && (fingerprint == allowed[2])) { return true } } return false } function historyGoBack() { if (currentWebview && currentWebview.canGoBack) { internal.resetFocus() currentWebview.goBack() } } function historyGoForward() { if (currentWebview && currentWebview.canGoForward) { internal.resetFocus() currentWebview.goForward() } } property var currentBookmarkOptionsDialog: null function addBookmark(url, title, icon, location) { if (title == "") title = UrlUtils.removeScheme(url) BookmarksModel.add(url, title, icon, "") if (location === undefined) location = chrome.bookmarkTogglePlaceHolder var properties = {"bookmarkUrl": url, "bookmarkTitle": title} currentBookmarkOptionsDialog = PopupUtils.open(bookmarkOptionsComponent, location, properties) } } // Work around https://launchpad.net/bugs/1502675 by delaying the switch to // the next tab for a fraction of a second to avoid a black flash. Timer { id: delayedTabSwitcher interval: 50 onTriggered: internal.switchToTab(internal.nextTabIndex, false) } function openUrlInNewTab(url, setCurrent, load, index) { load = typeof load !== 'undefined' ? load : true var tab = tabComponent.createObject(tabContainer, {"initialUrl": url, 'incognito': browser.incognito}) internal.addTab(tab, setCurrent, index) if (load) { tab.load() } if (!url.toString()) { internal.maybeFocusAddressBar() } } SessionStorage { id: session dataFile: dataLocation + "/session.json" function save() { if (!locked) { return } var tabs = [] for (var i = 0; i < publicTabsModel.count; ++i) { var tab = publicTabsModel.get(i) tabs.push(serializeTabState(tab)) } store(JSON.stringify({tabs: tabs, currentIndex: publicTabsModel.currentIndex})) } property bool restoring: false function restore() { restoring = true _doRestore() restoring = false } function _doRestore() { if (!locked) { return } var state = null try { state = JSON.parse(retrieve()) } catch (e) { return } if (state) { var tabs = state.tabs if (tabs) { for (var i = 0; i < Math.min(tabs.length, browser.maxTabsToRestore); ++i) { var tab = createTabFromState(tabs[i]) internal.addTab(tab, false) } } if ('currentIndex' in state) { internal.switchToTab(state.currentIndex, false) } } } // Those two functions are used to save/restore the current state of a tab. function serializeTabState(tab) { var state = {} state.uniqueId = tab.uniqueId state.url = tab.url.toString() state.title = tab.title state.icon = tab.icon.toString() state.preview = tab.preview.toString() state.savedState = tab.webview ? tab.webview.currentState : tab.restoreState return state } function createTabFromState(state) { var properties = {'initialUrl': state.url, 'initialTitle': state.title} if ('uniqueId' in state) { properties["uniqueId"] = state.uniqueId } if ('icon' in state) { properties["initialIcon"] = state.icon } if ('preview' in state) { properties["preview"] = state.preview } if ('savedState' in state) { properties['restoreState'] = state.savedState properties['restoreType'] = Oxide.WebView.RestoreLastSessionExitedCleanly } return tabComponent.createObject(tabContainer, properties) } } Timer { id: delayedSessionSaver interval: 500 onTriggered: session.save() } Timer { // Save session periodically to mitigate state loss when the application crashes interval: 60000 // every minute repeat: true running: !browser.incognito onTriggered: delayedSessionSaver.restart() } Timer { id: exitFullscreenOnLostFocus interval: 500 onTriggered: { if (browser.currentWebview) browser.currentWebview.fullscreen = false } } Connections { target: Qt.application onStateChanged: { if (Qt.application.state != Qt.ApplicationActive) { if (!browser.incognito) { session.save() } if (browser.currentWebview) { // Workaround for a desktop bug where changing volume causes // the app to briefly lose focus to notify-osd, and therefore // exit fullscreen mode. We prevent this by exiting fullscreen // only if the focus remains lost for longer than a certain // threshold. See: https://launchpad.net/bugs/694224. if (__platformName == "xcb") exitFullscreenOnLostFocus.start() else browser.currentWebview.fullscreen = false } } else exitFullscreenOnLostFocus.stop() } onAboutToQuit: { if (!browser.incognito) { session.save() } } } Connections { target: browser.incognito ? null : publicTabsModel onCurrentTabChanged: delayedSessionSaver.restart() onCountChanged: delayedSessionSaver.restart() } onIncognitoChanged: { if (incognito) { // When going incognito, save the current session right // away, as periodic session saving is disabled. session.save() } } // Schedule various expensive tasks to a point after the initialization and // first rendering of the application have already happened. // // Scheduling a Timer with the shortest non-zero interval possible (1ms) will // effectively queue its onTriggered function to run immediately after anything // that is currently in the event loop queue at the moment the Timer starts. // // The tasks are: // - creating the webviews for all initial tabs. This should ideally be done // asynchronously via object incubation, but http://pad.lv/1359911 prevents it // - loading the HistoryModel and BookmarksModel from the database // - deleting any page screenshots that are no longer needed Timer { running: true interval: 1 onTriggered: { if (!browser.newSession && settings.restoreSession) { session.restore() } // Sanity check console.assert(tabsModel.count <= browser.maxTabsToRestore, "WARNING: too many tabs were restored") for (var i in browser.initialUrls) { browser.openUrlInNewTab(browser.initialUrls[i], true, false) } if (tabsModel.count == 0) { browser.openUrlInNewTab(settings.homepage, true, false) } if (!delayedTabSwitcher.running) { tabsModel.currentTab.load() } if (!tabsModel.currentTab.url.toString() && !tabsModel.currentTab.restoreState) { internal.maybeFocusAddressBar() } BookmarksModel.databasePath = dataLocation + "/bookmarks.sqlite" HistoryModel.databasePath = dataLocation + "/history.sqlite" DownloadsModel.databasePath = dataLocation + "/downloads.sqlite" // Note that the property setter for databasePath won't return until // the entire model has been loaded, so it is safe to call this here PreviewManager.cleanUnusedPreviews(internal.getOpenPages()) } } Connections { target: MemInfo onFreeChanged: { var freeMemRatio = (MemInfo.total > 0) ? (MemInfo.free / MemInfo.total) : 1.0 // Under that threshold, available memory is considered "low", and the // browser is going to try and free up memory from unused tabs. This // value was chosen empirically, it is subject to change to better // reflect what a system under memory pressure might look like. var lowOnMemory = (freeMemRatio < 0.2) if (lowOnMemory) { // Unload an inactive tab to (hopefully) free up some memory function getCandidate(model) { // Naive implementation that only takes into account the // last time a tab was current. In the future we might // want to take into account other parameters such as // whether the tab is currently playing audio/video. var candidate = null for (var i = 0; i < model.count; ++i) { var tab = model.get(i) if (tab.current || !tab.webview) { continue } if (!candidate || (candidate.lastCurrent > tab.lastCurrent)) { candidate = tab } } return candidate } var candidate = getCandidate(publicTabsModel) if (candidate) { console.warn("Unloading background tab (%1) to free up some memory".arg(candidate.url)) candidate.unload() return } else if (browser.incognito) { candidate = getCandidate(privateTabsModelLoader.item) if (candidate) { console.warn("Unloading a background incognito tab to free up some memory") candidate.unload() return } } console.warn("System low on memory, but unable to pick a tab to unload") } } } Connections { target: session.restoring ? null : tabsModel onCurrentIndexChanged: { // In narrow mode, the tabslist is a stack: // the current tab is always at the top. if (!browser.wide) { tabsModel.move(tabsModel.currentIndex, 0) } } onCurrentTabChanged: { chrome.findInPageMode = false var tab = tabsModel.currentTab if (tab) { tab.load() } internal.resetFocus() } onCountChanged: { if (tabsModel.count == 0) { if (browser.incognito) { browser.incognito = false internal.resetFocus() } else if (browser.wide) { Qt.quit() } } } } Component { id: leavePrivateModeDialog LeavePrivateModeDialog { id: dialogue objectName: "leavePrivateModeDialog" // This dialog inherits from PopupBase, which has a restoreActiveFocus // function that is called when the dialog is hidden. That keeps the // focus in the address bar/webview when we leave private mode. So any // change on the active focus should be done after the run of such // function Component.onDestruction: { if (!browser.incognito) { internal.resetFocus() } } onCancelButtonClicked: PopupUtils.close(dialogue) onOkButtonClicked: { PopupUtils.close(dialogue) browser.incognito = false } } } // TODO: internationalize non-standard key sequences? // Ctrl+Tab or Ctrl+PageDown: cycle through open tabs Shortcut { sequence: StandardKey.NextChild enabled: tabContainer.visible || recentView.visible onActivated: internal.switchToNextTab() } Shortcut { sequence: "Ctrl+PgDown" enabled: tabContainer.visible || recentView.visible onActivated: internal.switchToNextTab() } // Ctrl+Shift+Tab or Ctrl+PageUp: cycle through open tabs in reverse order Shortcut { sequence: StandardKey.PreviousChild enabled: tabContainer.visible || recentView.visible onActivated: internal.switchToPreviousTab() } Shortcut { sequence: "Ctrl+Shift+Tab" enabled: tabContainer.visible || recentView.visible onActivated: internal.switchToPreviousTab() } Shortcut { sequence: "Ctrl+PgUp" enabled: tabContainer.visible || recentView.visible onActivated: internal.switchToPreviousTab() } // Ctrl+W or Ctrl+F4: Close the current tab Shortcut { sequence: StandardKey.Close enabled: tabContainer.visible || recentView.visible onActivated: internal.closeCurrentTab() } Shortcut { sequence: "Ctrl+F4" enabled: tabContainer.visible || recentView.visible onActivated: internal.closeCurrentTab() } // Ctrl+Shift+W or Ctrl+Shift+T: Undo close tab Shortcut { sequence: "Ctrl+Shift+W" enabled: tabContainer.visible || recentView.visible onActivated: internal.undoCloseTab() } Shortcut { sequence: "Ctrl+Shift+T" enabled: tabContainer.visible || recentView.visible onActivated: internal.undoCloseTab() } // Ctrl+T: Open a new Tab Shortcut { sequence: StandardKey.AddTab enabled: tabContainer.visible || recentView.visible || bookmarksViewLoader.active || historyViewLoader.active onActivated: { openUrlInNewTab("", true) if (recentView.visible) recentView.reset() bookmarksViewLoader.active = false historyViewLoader.active = false } } // F6 or Ctrl+L or Alt+D: Select the content in the address bar Shortcut { sequence: "F6" enabled: tabContainer.visible onActivated: internal.focusAddressBar(true) } Shortcut { sequence: "Ctrl+L" enabled: tabContainer.visible onActivated: internal.focusAddressBar(true) } Shortcut { sequence: "Alt+D" enabled: tabContainer.visible onActivated: internal.focusAddressBar(true) } // Ctrl+D: Toggle bookmarked state on current Tab Shortcut { sequence: "Ctrl+D" enabled: tabContainer.visible onActivated: { if (internal.currentBookmarkOptionsDialog) { internal.currentBookmarkOptionsDialog.hide() } else if (currentWebview) { if (BookmarksModel.contains(currentWebview.url)) { BookmarksModel.remove(currentWebview.url) } else { internal.addBookmark(currentWebview.url, currentWebview.title, currentWebview.icon) } } } } // Ctrl+H: Show History Shortcut { sequence: "Ctrl+H" enabled: tabContainer.visible onActivated: historyViewLoader.active = true } // Ctrl+Shift+O: Show Bookmarks Shortcut { sequence: "Ctrl+Shift+O" enabled: tabContainer.visible onActivated: bookmarksViewLoader.active = true } // Alt+← or Backspace: Goes to the previous page in history Shortcut { sequence: StandardKey.Back enabled: tabContainer.visible onActivated: internal.historyGoBack() } // Alt+→ or Shift+Backspace: Goes to the next page in history Shortcut { sequence: StandardKey.Forward enabled: tabContainer.visible onActivated: internal.historyGoForward() } // F5 or Ctrl+R: Reload current Tab Shortcut { sequence: StandardKey.Refresh enabled: tabContainer.visible onActivated: if (currentWebview) currentWebview.reload() } Shortcut { sequence: "F5" enabled: tabContainer.visible onActivated: if (currentWebview) currentWebview.reload() } // Ctrl+F: Find in Page Shortcut { sequence: StandardKey.Find enabled: tabContainer.visible && !newTabViewLoader.active onActivated: chrome.findInPageMode = true } // Ctrl+J: Show downloads page Shortcut { sequence: "Ctrl+J" enabled: chrome.visible && downloadHandlerLoader.status == Loader.Ready && contentHandlerLoader.status == Loader.Ready && !downloadsViewLoader.active onActivated: downloadsViewLoader.active = true } // Ctrl+G: Find next Shortcut { sequence: StandardKey.FindNext enabled: currentWebview && chrome.findInPageMode onActivated: currentWebview.findController.next() } // Ctrl+Shift+G: Find previous Shortcut { sequence: StandardKey.FindPrevious enabled: currentWebview && chrome.findInPageMode onActivated: currentWebview.findController.previous() } Loader { id: contentHandlerLoader source: "../ContentHandler.qml" asynchronous: true } Connections { target: contentHandlerLoader.item onExportFromDownloads: { if (downloadHandlerLoader.status == Loader.Ready) { downloadsViewLoader.active = true downloadsViewLoader.item.mimetypeFilter = mimetypeFilter downloadsViewLoader.item.activeTransfer = transfer downloadsViewLoader.item.multiSelect = multiSelect downloadsViewLoader.item.pickingMode = true } } } Loader { id: downloadDialogLoader source: "ContentDownloadDialog.qml" asynchronous: true } Loader { id: filePickerLoader source: "ContentPickerDialog.qml" asynchronous: true } } ./src/app/webbrowser/IndeterminateProgressBar.qml0000644000004100000410000000352113004613604022446 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 Rectangle { id: progressBar property real progress property bool indeterminateProgress: false radius: width/3 color: Theme.palette.normal.base Rectangle { id: currentProgress height: parent.height radius: parent.radius anchors.left: parent.left anchors.leftMargin: 0 anchors.top: parent.top color: UbuntuColors.orange width: indeterminateProgress ? parent.width / 6 : (progress / 100) * parent.width SequentialAnimation { running: indeterminateProgress onRunningChanged: { currentProgress.anchors.leftMargin = 0; } loops: Animation.Infinite PropertyAnimation { target: currentProgress.anchors; property: "leftMargin"; from: 0.0; to: parent.width - parent.width / 6; duration: UbuntuAnimation.SleepyDuration; easing.type: Easing.InOutQuad; } PropertyAnimation { target: currentProgress.anchors; property: "leftMargin"; from: parent.width - parent.width / 6; to: 0; duration: UbuntuAnimation.SleepyDuration; easing.type: Easing.InOutQuad; } } } } ./src/app/webbrowser/NewTabViewWide.qml0000644000004100000410000001023413004613604020327 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import webbrowserapp.private 0.1 import "." FocusScope { id: newTabViewWide property QtObject settingsObject property alias selectedIndex: sections.selectedIndex readonly property bool inBookmarksView: newTabViewWide.selectedIndex === 1 signal bookmarkClicked(url url) signal bookmarkRemoved(url url) signal historyEntryClicked(url url) Keys.onTabPressed: selectedIndex = (selectedIndex + 1) % 2 Keys.onBacktabPressed: selectedIndex = Math.abs((selectedIndex - 1) % 2) onActiveFocusChanged: { if (activeFocus) { if (inBookmarksView) { bookmarksFoldersViewWide.restoreLastFocusedColumn() } else { topSitesList.focus = true } } } LimitProxyModel { id: topSitesModel limit: 10 sourceModel: TopSitesModel { model: HistoryModel } } BookmarksFoldersViewWide { id: bookmarksFoldersViewWide onBookmarkClicked: newTabViewWide.bookmarkClicked(url) onBookmarkRemoved: newTabViewWide.bookmarkRemoved(url) anchors { top: sectionsGroup.bottom left: parent.left right: parent.right bottom: parent.bottom topMargin: units.gu(2) rightMargin: units.gu(2) } visible: inBookmarksView homeBookmarkUrl: newTabViewWide.settingsObject.homepage } Rectangle { anchors.fill: parent visible: !inBookmarksView color: "#fbfbfb" } UrlPreviewGrid { id: topSitesList objectName: "topSitesList" anchors { top: sectionsGroup.bottom bottom: parent.bottom left: parent.left right: parent.right topMargin: units.gu(3) leftMargin: units.gu(4) } visible: !inBookmarksView model: topSitesModel showFavicons: true onActivated: newTabViewWide.historyEntryClicked(url) onRemoved: { HistoryModel.hide(url) PreviewManager.checkDelete(url) } } Scrollbar { flickableItem: topSitesList } Rectangle { id: sectionsGroup anchors { top: parent.top left: parent.left right: parent.right } color: "#ffffff" height: sections.height Sections { id: sections objectName: "sections" anchors { left: parent.left top: parent.top leftMargin: units.gu(2) } selectedIndex: settingsObject.newTabDefaultSection onSelectedIndexChanged: { settingsObject.newTabDefaultSection = selectedIndex if (selectedIndex === 0) { topSitesList.focus = true } else { bookmarksFoldersViewWide.restoreLastFocusedColumn() } } actions: [ Action { text: i18n.tr("Top sites") }, Action { text: i18n.tr("Bookmarks") } ] } Rectangle { // Divider, see Ubuntu/Components/Themes/Ambiance/1.3/PageHeaderStyle.qml anchors { left: parent.left right: parent.right top: parent.bottom } height: units.dp(1) color: Qt.rgba(0, 0, 0, 0.1) } } } ./src/app/webbrowser/SearchSuggestions.qml0000644000004100000410000000471713004613604021154 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import webbrowserapp.private 0.1 Item { property var terms property SearchEngine searchEngine property var results: [] property bool active: false onSearchEngineChanged: resetSearch() onTermsChanged: resetSearch() onActiveChanged: resetSearch() QtObject { id: request property var xhr: null function get(url) { if (xhr === null) { xhr = new XMLHttpRequest() xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.DONE) { results = parseResponse(xhr.responseText) } } } xhr.open("GET", url); xhr.send(); } function abort() { if (xhr) xhr.abort() } } Timer { id: limiter interval: 250 onTriggered: { if (terms.length > 0 && searchEngine) { var url = searchEngine.suggestionsUrlTemplate url = url.replace("{searchTerms}", encodeURIComponent(terms.join(" "))) request.get(url) } } } function parseResponse(response) { try { var data = JSON.parse(response) } catch (error) { return [] } if (data.length > 1) { return data[1].map(function(result) { return { title: result, url: searchEngine.urlTemplate.replace("{searchTerms}", encodeURIComponent(result)) } }) } else return [] } function resetSearch() { results = [] request.abort() if (active) limiter.restart() } } ./src/app/webbrowser/UrlPreviewGrid.qml0000644000004100000410000000560113004613604020417 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 GridView { id: grid property bool showFavicons: true property int horizontalMargin: units.gu(3) property int verticalMargin: units.gu(2.5) property int previewWidth: units.gu(17) property int previewHeight: units.gu(10) signal activated(url url) signal removed(url url) cellWidth: previewWidth + horizontalMargin * 2 cellHeight: previewHeight + verticalMargin * 2 + units.gu(4) // height of text + favicon + margins in delegate delegate: UrlPreviewDelegate { objectName: "topSiteItem" width: grid.cellWidth height: grid.cellHeight title: model.title icon: model.icon url: model.url showFavicon: grid.showFavicons previewHeight: grid.previewHeight previewWidth: grid.previewWidth onClicked: grid.activated(model.url) onSetCurrent: grid.currentIndex = index onRemoved: grid.removed(model.url) } highlight: Item { visible: viewHighlight.hasKeyboard && GridView.view && GridView.view.activeFocus ListViewHighlight { id: viewHighlight visible: true width: previewWidth + units.gu(2) height: previewHeight + units.gu(5) anchors { top: parent.top left: parent.left topMargin: (grid.cellHeight - height) / 2 - grid.verticalMargin - units.gu(0.5) leftMargin: (grid.cellWidth - width) / 2 - grid.horizontalMargin } } } Keys.onDeletePressed: removed(currentItem.url) Keys.onUpPressed: { var current = currentIndex moveCurrentIndexUp() if (current == currentIndex) { event.accepted = false } } Keys.onDownPressed: { var current = currentIndex moveCurrentIndexDown() if (currentIndex == current) { event.accepted = false } } Timer { // Work around a weird issue with the use of a LimitProxyModel in a // grid view, where the currentIndex is changed when populating the // model. running: true interval: 1 onTriggered: grid.currentIndex = 0 } } ./src/app/webbrowser/ContextMenuMobile.qml0000644000004100000410000001124613004613604021110 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 as ListItems import Ubuntu.Components.Popups 1.3 as Popups import com.canonical.Oxide 1.8 as Oxide Popups.Dialog { property QtObject contextModel: model property ActionList actions: null QtObject { id: internal readonly property bool isImage: (contextModel.mediaType === Oxide.WebView.MediaTypeImage) || (contextModel.mediaType === Oxide.WebView.MediaTypeCanvas) } Row { id: header spacing: units.gu(2) anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) } height: units.gu(2 * title.lineCount + 3) visible: title.text Icon { width: units.gu(2) height: units.gu(2) anchors { top: parent.top topMargin: units.gu(2) } name: internal.isImage ? "stock_image" : "" // work around the lack of a standard stock_link symbolic icon in the theme Component.onCompleted: { if (!name) { source = "assets/stock_link.svg" } } } Label { id: title objectName: "titleLabel" text: contextModel.srcUrl.toString() ? contextModel.srcUrl : contextModel.linkUrl width: parent.width - units.gu(4) anchors { top: parent.top topMargin: units.gu(2) bottom: parent.bottom } fontSize: "x-small" maximumLineCount: 2 wrapMode: Text.Wrap height: contentHeight } } ListItems.ThinDivider { anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) } visible: header.visible } Repeater { model: actions.actions delegate: ListItems.Empty { action: actions.actions[index] objectName: action.objectName + "_item" visible: action.enabled showDivider: false height: units.gu(5) Label { anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) verticalCenter: parent.verticalCenter } fontSize: "x-small" text: action.text } ListItems.ThinDivider { anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) bottom: parent.bottom } } onTriggered: contextModel.close() } } ListItems.Empty { objectName: "cancelAction" height: units.gu(5) showDivider: false Label { anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) verticalCenter: parent.verticalCenter } fontSize: "x-small" text: i18n.tr("Cancel") } onTriggered: contextModel.close() } // adjust default dialog visuals to custom requirements for the context menu Binding { target: __foreground property: "margins" value: 0 } Binding { target: __foreground property: "itemSpacing" value: 0 } // We can’t prevent the dialog from stealing the focus from // the webview, but we can at least restore it when the // dialog is closed (https://launchpad.net/bugs/1526884). Component.onDestruction: Oxide.WebView.view.forceActiveFocus() } ./src/app/webbrowser/HistoryView.qml0000644000004100000410000001516013004613604020002 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 as ListItems import webbrowserapp.private 0.1 import "." as Local FocusScope { id: historyView signal seeMoreEntriesClicked(var model) signal newTabRequested() signal done() Rectangle { anchors.fill: parent color: "#f6f6f6" } Timer { // Set the model asynchronously to ensure // the view is displayed as early as possible. id: loadModelTimer interval: 1 onTriggered: historyDomainListModel.sourceModel = HistoryModel } function loadModel() { loadModelTimer.restart() } ListView { id: domainsListView objectName: "domainsListView" focus: true currentIndex: 0 anchors { top: topBar.bottom left: parent.left right: parent.right bottom: toolbar.top } model: SortFilterModel { model: HistoryDomainListModel { id: historyDomainListModel } sort.property: "lastVisit" sort.order: Qt.DescendingOrder } section.property: "lastVisitDate" section.delegate: HistorySectionDelegate { width: parent.width - units.gu(2) anchors.left: parent.left anchors.leftMargin: units.gu(2) } delegate: UrlDelegate { id: urlDelegate objectName: "historyViewDomainDelegate" width: parent.width height: units.gu(5) readonly property int modelIndex: index title: model.domain url: lastVisitedTitle icon: model.lastVisitedIcon onClicked: { if (selectMode) { selected = !selected } else { historyView.seeMoreEntriesClicked(model.entries) } } onRemoved: HistoryModel.removeEntriesByDomain(model.domain) onPressAndHold: { selectMode = !selectMode if (selectMode) { domainsListView.ViewItems.selectedIndices = [index] } } } highlight: ListViewHighlight {} Keys.onEnterPressed: currentItem.clicked() Keys.onReturnPressed: currentItem.clicked() Keys.onDeletePressed: currentItem.removed() } Local.Toolbar { id: toolbar height: units.gu(7) anchors { left: parent.left right: parent.right bottom: parent.bottom } Button { objectName: "doneButton" anchors { left: parent.left leftMargin: units.gu(2) verticalCenter: parent.verticalCenter } strokeColor: UbuntuColors.darkGrey text: i18n.tr("Done") onClicked: historyView.done() } ToolbarAction { objectName: "newTabAction" anchors { right: parent.right rightMargin: units.gu(2) verticalCenter: parent.verticalCenter } height: parent.height - units.gu(2) text: i18n.tr("New tab") iconName: "tab-new" onClicked: { historyView.newTabRequested() historyView.done() } } } Local.Toolbar { id: topBar visible: domainsListView.ViewItems.selectMode height: visible ? units.gu(7) : 0 color: "#f7f7f7" Behavior on height { UbuntuNumberAnimation {} } anchors { left: parent.left right: parent.right top: parent.top } ToolbarAction { iconName: "close" objectName: "closeButton" text: i18n.tr("Cancel") onClicked: domainsListView.ViewItems.selectMode = false anchors { left: parent.left leftMargin: units.gu(2) } height: parent.height - units.gu(2) } ToolbarAction { iconName: "select" objectName: "selectAllButton" text: i18n.tr("Select all") onClicked: { if (domainsListView.ViewItems.selectedIndices.length === domainsListView.count) { domainsListView.ViewItems.selectedIndices = [] } else { var indices = [] for (var i = 0; i < domainsListView.count; ++i) { indices.push(i) } domainsListView.ViewItems.selectedIndices = indices } } anchors { right: deleteButton.left rightMargin: units.gu(2) } height: parent.height - units.gu(2) } ToolbarAction { id: deleteButton objectName: "deleteButton" iconName: "delete" text: i18n.tr("Delete") enabled: domainsListView.ViewItems.selectedIndices.length > 0 onClicked: { var indices = domainsListView.ViewItems.selectedIndices var domains = [] for (var i in indices) { domains.push(domainsListView.model.get(indices[i]).domain) } domainsListView.ViewItems.selectMode = false for (var j in domains) { HistoryModel.removeEntriesByDomain(domains[j]) } } anchors { right: parent.right rightMargin: units.gu(2) } height: parent.height - units.gu(2) } ListItems.ThinDivider { anchors { left: parent.left right: parent.right bottom: parent.bottom } } } } ./src/app/webbrowser/DownloadHandler.qml0000644000004100000410000000256213004613604020555 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.DownloadManager 1.2 import webbrowserapp.private 0.1 DownloadManager { onDownloadFinished: { if (DownloadsModel.contains(download.downloadId)) { DownloadsModel.moveToDownloads(download.downloadId, path) } DownloadsModel.setComplete(download.downloadId, true) } onDownloadPaused: { DownloadsModel.pauseDownload(download.downloadId) } onDownloadResumed: { DownloadsModel.resumeDownload(download.downloadId) } onDownloadCanceled: { DownloadsModel.cancelDownload(download.downloadId) } onErrorFound: { DownloadsModel.setError(download.downloadId, download.errorMessage) } } ./src/app/webbrowser/NavigationBar.qml0000644000004100000410000002666413004613623020246 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import ".." FocusScope { id: root property var tab property alias searchUrl: addressbar.searchUrl readonly property string text: addressbar.text property alias bookmarked: addressbar.bookmarked signal toggleBookmark() property list drawerActions readonly property bool drawerOpen: internal.openDrawer property alias requestedUrl: addressbar.requestedUrl property alias canSimplifyText: addressbar.canSimplifyText property alias findInPageMode: addressbar.findInPageMode property alias editing: addressbar.editing property alias incognito: addressbar.incognito property alias showFaviconInAddressBar: addressbar.showFavicon readonly property alias bookmarkTogglePlaceHolder: addressbar.bookmarkTogglePlaceHolder property color fgColor: Theme.palette.normal.baseText property color iconColor: UbuntuColors.darkGrey property real availableHeight onFindInPageModeChanged: if (findInPageMode) addressbar.text = "" onIncognitoChanged: findInPageMode = false function selectAll() { addressbar.selectAll() } FocusScope { anchors { fill: parent margins: units.gu(1) } focus: true ChromeButton { id: backButton objectName: "backButton" iconName: "previous" iconSize: 0.4 * height iconColor: root.iconColor height: root.height width: height * 0.8 anchors { left: parent.left verticalCenter: parent.verticalCenter } enabled: findInPageMode || (internal.webview ? internal.webview.canGoBack : false) onTriggered: findInPageMode ? (findInPageMode = false) : internal.webview.goBack() } ChromeButton { id: forwardButton objectName: "forwardButton" iconName: "next" iconSize: 0.4 * height iconColor: root.iconColor height: root.height visible: enabled width: visible ? height * 0.8 : 0 anchors { left: backButton.right verticalCenter: parent.verticalCenter } enabled: findInPageMode ? false : (internal.webview ? internal.webview.canGoForward : false) onTriggered: internal.webview.goForward() } AddressBar { id: addressbar fgColor: root.fgColor focus: true findInPageMode: findInPageMode findController: internal.webview ? internal.webview.findController : null securityStatus: internal.webview ? internal.webview.securityStatus : null anchors { left: parent.left // Work around https://launchpad.net/bugs/1546346 by ensuring // that the x coordinate of the text field is an integer. leftMargin: Math.round(backButton.width + forwardButton.width + units.gu(1)) right: rightButtonsBar.left rightMargin: units.gu(1) top: parent.top bottom: parent.bottom } icon: (internal.webview && internal.webview.certificateError) ? "" : tab ? tab.icon : "" loading: internal.webview ? internal.webview.loading : false onValidated: { if (!findInPageMode) { internal.webview.forceActiveFocus() internal.webview.url = requestedUrl } } onRequestReload: { internal.webview.forceActiveFocus() internal.webview.reload() } onRequestStop: internal.webview.stop() onToggleBookmark: root.toggleBookmark() Connections { target: internal.webview onUrlChanged: { // ensure that the URL actually changes so that the // address bar is updated in case the user has entered a // new address that redirects to where she previously was // (https://launchpad.net/bugs/1306615) addressbar.actualUrl = "" addressbar.actualUrl = internal.webview.url } } } Row { id: rightButtonsBar anchors { right: parent.right top: parent.top bottom: parent.bottom } ChromeButton { id: findPreviousButton objectName: "findPreviousButton" iconName: "up" iconSize: 0.5 * height height: root.height width: height * 0.8 anchors.verticalCenter: parent.verticalCenter visible: findInPageMode enabled: internal.webview && internal.webview.findController && internal.webview.findController.count > 1 onTriggered: internal.webview.findController.previous() } ChromeButton { id: findNextButton objectName: "findNextButton" iconName: "down" iconSize: 0.5 * height height: root.height width: height * 0.8 anchors.verticalCenter: parent.verticalCenter visible: findInPageMode enabled: internal.webview && internal.webview.findController && internal.webview.findController.count > 1 onTriggered: internal.webview.findController.next() } ChromeButton { id: drawerButton objectName: "drawerButton" iconName: "contextual-menu" iconSize: 0.5 * height iconColor: root.iconColor height: root.height width: height * 0.8 anchors.verticalCenter: parent.verticalCenter onTriggered: { if (!internal.openDrawer) { internal.openDrawer = drawerComponent.createObject(chrome) internal.openDrawer.opened = true } } } } } QtObject { id: internal property var openDrawer: null readonly property var webview: tab ? tab.webview : null } onTabChanged: { if (tab) { addressbar.actualUrl = tab.url if (!tab.url.toString() && editing) { addressbar.text = "" } } else { addressbar.actualUrl = "" } } Component { id: drawerComponent Item { id: drawer objectName: "drawer" property bool opened: false property bool closing: false onOpenedChanged: { if (!opened) { closing = true } } anchors { top: parent.bottom right: parent.right } width: units.gu(22) height: actionsListView.height clip: actionsListView.y != 0 InverseMouseArea { anchors.fill: parent enabled: drawer.opened onPressed: drawer.opened = false } Rectangle { anchors.fill: actionsListView color: Theme.palette.normal.background Rectangle { anchors { top: parent.top bottom: parent.bottom left: parent.left } width: units.dp(1) color: "#dedede" } Rectangle { anchors { left: parent.left right: parent.right bottom: parent.bottom } height: units.dp(1) color: "#dedede" } } ListView { id: actionsListView anchors { left: parent.left right: parent.right } height: Math.min(_contentHeight, availableHeight) // avoid a binding loop property real _contentHeight: 0 onContentHeightChanged: _contentHeight = contentHeight y: drawer.opened ? 0 : -height Behavior on y { UbuntuNumberAnimation {} } onYChanged: { if (drawer.closing && (y == -height)) { drawer.destroy() } } clip: true model: drawerActions delegate: AbstractButton { objectName: action.objectName anchors { left: parent.left right: parent.right } height: visible ? units.gu(6) : 0 visible: action.enabled action: modelData onClicked: drawer.opened = false Rectangle { anchors.fill: parent color: Theme.palette.selected.background visible: parent.pressed } Icon { id: actionIcon anchors { left: parent.left leftMargin: units.gu(2) verticalCenter: parent.verticalCenter } width: units.gu(2) height: width name: model.iconName Binding on source { when: model.iconSource.toString() value: model.iconSource } color: root.fgColor } Label { anchors { left: actionIcon.right leftMargin: units.gu(2) verticalCenter: parent.verticalCenter right: parent.right rightMargin: units.gu(1) } text: model.text fontSize: "small" color: root.fgColor elide: Text.ElideRight } } } } } } ./src/app/webbrowser/BottomEdgeHandle.qml0000644000004100000410000000241613004613604020653 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Gestures 0.1 DirectionalDragArea { direction: Direction.Upwards // default values taken from unity8’s EdgeDragArea component maxDeviation: units.gu(3) wideningAngle: 50 distanceThreshold: units.gu(1.5) minSpeed: 0 maxSilenceTime: 200 compositionTime: 60 readonly property real dragFraction: dragging ? Math.min(1.0, Math.max(0.0, sceneDistance / parent.height)) : 0.0 readonly property var thresholds: [0.05, 0.18, 0.36, 0.54, 1.0] readonly property int stage: thresholds.map(function(t) { return dragFraction <= t }).indexOf(true) } ./src/app/webbrowser/BookmarkOptions.qml0000644000004100000410000001165613004613604020635 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 Popover { id: bookmarkOptions property url bookmarkUrl property alias bookmarkTitle: titleTextField.text property alias folderModel: folderOptionSelector.model readonly property string bookmarkFolder: folderModel.get(folderOptionSelector.selectedIndex).folder contentHeight: bookmarkOptionsColumn.childrenRect.height + units.gu(2) Column { id: bookmarkOptionsColumn anchors { top: parent.top left: parent.left right: parent.right margins: units.gu(1) } spacing: units.gu(1) Label { font.bold: true text: i18n.tr("Bookmark Added") } Label { // TRANSLATORS: Field where the title of bookmarked URL can be changed text: i18n.tr("Name") fontSize: "small" } TextField { id: titleTextField objectName: "titleTextField" anchors { left: parent.left right: parent.right } inputMethodHints: Qt.ImhNoPredictiveText } Label { // TRANSLATORS: Field to choose the folder where bookmarked URL will be saved in text: i18n.tr("Save in") fontSize: "small" } OptionSelector { id: folderOptionSelector delegate: OptionSelectorDelegate { text: folder === "" ? i18n.tr("All Bookmarks") : folder } containerHeight: itemHeight * 3 } Item { anchors { left: parent.left right: parent.right } height: newFolderButton.height Button { id: newFolderButton objectName: "bookmarkOptions.newButton" text: i18n.tr("New Folder") onClicked: PopupUtils.open(newFolderDialog) } Button { id: okButton objectName: "bookmarkOptions.okButton" anchors.right: parent.right text: i18n.tr("OK") color: UbuntuColors.green onClicked: bookmarkOptions.destroy() } } } Component { id: newFolderDialog Dialog { id: dialogue objectName: "newFolderDialog" title: i18n.tr("Create new folder") Component.onCompleted: { folderTextField.forceActiveFocus() } function createNewFolder(folder) { Qt.inputMethod.hide() folderModel.createNewFolder(folder) folderOptionSelector.selectedIndex = folderModel.indexOf(folder) folderOptionSelector.currentlyExpanded = false PopupUtils.close(dialogue) } TextField { id: folderTextField objectName: "newFolderDialog.text" inputMethodHints: Qt.ImhNoPredictiveText placeholderText: i18n.tr("New Folder") onAccepted: createNewFolder(text) } Button { objectName: "newFolderDialog.cancelButton" anchors { left: parent.left right: parent.right } text: i18n.tr("Cancel") onClicked: PopupUtils.close(dialogue) } Button { objectName: "newFolderDialog.saveButton" anchors { left: parent.left right: parent.right } text: i18n.tr("Save") enabled: folderTextField.text color: UbuntuColors.green // Button took focus on press what makes the keyboard be // dismissed and that could make the Button moves between the // press and the release. Button onClicked is not triggered // if the release event happens outside of the button. // See: http://pad.lv/1415023 activeFocusOnPress: false onClicked: createNewFolder(folderTextField.text) } } } } ./src/app/webbrowser/history-lastvisitdatelist-model.cpp0000644000004100000410000002257613004613604024061 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "history-lastvisitdatelist-model.h" #include "history-model.h" // Qt #include #include /*! \class HistoryLastVisitDateListModel \brief List model that exposes a list of all last visit dates from history HistoryLastVisitDateListModel is a list model that exposes all last visit dates from the source model. Each item has one single role: 'lastVisitDate' for a date in which there is at least one url visited on the source model. A special entry is added to the beginning of the list to represent all dates. The source model needs to expose a role named 'lastVisitDate', from which the input dates will be read. If such role is not present, this model will not expose any dates. */ HistoryLastVisitDateListModel::HistoryLastVisitDateListModel(QObject* parent) : QAbstractListModel(parent) , m_sourceModel(0) { } HistoryLastVisitDateListModel::~HistoryLastVisitDateListModel() { clearLastVisitDates(); } QHash HistoryLastVisitDateListModel::roleNames() const { static QHash roles; if (roles.isEmpty()) { roles[LastVisitDate] = "lastVisitDate"; } return roles; } int HistoryLastVisitDateListModel::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); return m_orderedDates.count(); } QVariant HistoryLastVisitDateListModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } const QDate lastVisitDate = m_orderedDates.at(index.row()); switch (role) { case LastVisitDate: return lastVisitDate; default: return QVariant(); } } QVariant HistoryLastVisitDateListModel::sourceModel() const { return (m_sourceModel) ? QVariant::fromValue(m_sourceModel) : QVariant(); } void HistoryLastVisitDateListModel::setSourceModel(QVariant sourceModel) { QAbstractItemModel* newSourceModel = qvariant_cast(sourceModel); if (sourceModel.isValid() && (newSourceModel == 0) && !sourceModel.canConvert()) { qWarning() << "Only QAbstractItemModel-derived instances and null are" << "allowed as source models"; } if (newSourceModel != m_sourceModel) { beginResetModel(); if (m_sourceModel != 0) { m_sourceModel->disconnect(this); } clearLastVisitDates(); m_sourceModel = newSourceModel; updateSourceModelRole(); populateModel(); if (m_sourceModel != 0) { connect(m_sourceModel, SIGNAL(rowsInserted(const QModelIndex&, int, int)), SLOT(onRowsInserted(const QModelIndex&, int, int))); connect(m_sourceModel, SIGNAL(rowsRemoved(const QModelIndex&, int, int)), SLOT(onRowsRemoved(const QModelIndex&, int, int))); connect(m_sourceModel, SIGNAL(rowsMoved(const QModelIndex&, int, int, const QModelIndex&, int)), SLOT(onRowsMoved(const QModelIndex&, int, int, const QModelIndex&, int))); connect(m_sourceModel, SIGNAL(modelReset()), SLOT(onModelReset())); connect(m_sourceModel, SIGNAL(layoutChanged(QList, QAbstractItemModel::LayoutChangeHint)), SLOT(onModelReset())); } endResetModel(); Q_EMIT sourceModelChanged(); } } void HistoryLastVisitDateListModel::clearLastVisitDates() { m_orderedDates.clear(); Q_FOREACH(const QDate& lastVisitDate, m_lastVisitDates.keys()) { delete m_lastVisitDates.take(lastVisitDate); } } void HistoryLastVisitDateListModel::populateModel() { if (m_sourceModel != 0) { int count = m_sourceModel->rowCount(); for (int i = 0; i < count; ++i) { insertNewHistoryEntry(new QPersistentModelIndex(m_sourceModel->index(i, 0)), false); } } } void HistoryLastVisitDateListModel::onRowsInserted(const QModelIndex& parent, int start, int end) { for (int i = start; i <= end; ++i) { insertNewHistoryEntry(new QPersistentModelIndex(m_sourceModel->index(i, 0)), true); } } void HistoryLastVisitDateListModel::onRowsRemoved(const QModelIndex& parent, int start, int end) { QMap*>::iterator lastVisitDate = m_lastVisitDates.begin(); while (lastVisitDate != m_lastVisitDates.end()) { QList::iterator entry = lastVisitDate.value()->begin(); while (entry != lastVisitDate.value()->end()) { QPersistentModelIndex *index = *entry; if (!index->isValid()) { entry = lastVisitDate.value()->erase(entry); } else { ++entry; } } if (lastVisitDate.value()->isEmpty()) { int removeAt = m_orderedDates.indexOf(lastVisitDate.key()); beginRemoveRows(QModelIndex(), removeAt, removeAt); m_orderedDates.removeAt(removeAt); delete lastVisitDate.value(); lastVisitDate = m_lastVisitDates.erase(lastVisitDate); endRemoveRows(); } else { ++lastVisitDate; } } if (m_lastVisitDates.isEmpty()) { // Remove the default entry if model is empty beginRemoveRows(QModelIndex(), 0, 0); m_orderedDates.clear(); endRemoveRows(); } } void HistoryLastVisitDateListModel::onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row) { Q_UNUSED(parent); Q_UNUSED(destination); // Rows were moved in the source model, meaning that their last visit dates // were potentially updated, so we remove all the dates corresponding to // the rows before they were moved, and we add all the dates corresponding // to the rows after they were moved. // Determine a lower and upper bound for the dates that should be removed // by looking at the indexes surrounding the rows before they were moved. QDate lower = m_sourceModel->data(m_sourceModel->index((row < start) ? end + 1 : start, 0), m_sourceModelRole).toDate(); if (!lower.isValid()) { // The last row was moved too, we don’t have a strict lower bound. lower = m_orderedDates.last().addDays(-1); } QDate upper = m_sourceModel->data(m_sourceModel->index((row < start) ? end : start - 1, 0), m_sourceModelRole).toDate(); for (QDate i = upper.addDays(-1); i > lower; i = i.addDays(-1)) { if (m_orderedDates.contains(i)) { int removeAt = m_orderedDates.indexOf(i); beginRemoveRows(QModelIndex(), removeAt, removeAt); m_orderedDates.removeAt(removeAt); delete m_lastVisitDates.take(i); endRemoveRows(); } } // Now add back dates for all the rows after they were moved. for (int i = 0; i <= (end - start); ++i) { int index = i + row + ((row < start) ? 0 : start - end); insertNewHistoryEntry(new QPersistentModelIndex(m_sourceModel->index(index, 0)), true); } } void HistoryLastVisitDateListModel::updateSourceModelRole() { if (m_sourceModel && m_sourceModel->roleNames().count() > 0) { m_sourceModelRole = m_sourceModel->roleNames().key("lastVisitDate", -1); if (m_sourceModelRole == -1) { qWarning() << "No results will be returned because the sourceModel" << "does not have a role named \"lastVisitDate\""; } } } void HistoryLastVisitDateListModel::onModelReset() { beginResetModel(); updateSourceModelRole(); clearLastVisitDates(); populateModel(); endResetModel(); } void HistoryLastVisitDateListModel::insertNewHistoryEntry(QPersistentModelIndex* index, bool notify) { if (m_sourceModelRole == -1) { return; } QDate lastVisitDate = index->data(m_sourceModelRole).toDate(); if (!m_lastVisitDates.contains(lastVisitDate)) { if (m_orderedDates.isEmpty()) { // Add default entry to represent all dates if (notify) { beginInsertRows(QModelIndex(), 0, 0); } m_orderedDates.append(QDate()); if (notify) { endInsertRows(); } } int insertAt = 1; QList *entries = new QList(); while (insertAt < m_orderedDates.count()) { if (lastVisitDate > m_orderedDates.at(insertAt)) { break; } ++insertAt; } if (notify) { beginInsertRows(QModelIndex(), insertAt, insertAt); } m_orderedDates.insert(insertAt, lastVisitDate); m_lastVisitDates.insert(lastVisitDate, entries); if (notify) { endInsertRows(); } } m_lastVisitDates[lastVisitDate]->append(index); } ./src/app/webbrowser/AddressBar.qml0000644000004100000410000003507713004613604017531 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import com.canonical.Oxide 1.8 as Oxide import ".." import "../UrlUtils.js" as UrlUtils FocusScope { id: addressbar property alias icon: favicon.source property bool incognito: false property alias text: textField.text property bool bookmarked: false signal toggleBookmark() property url requestedUrl property url actualUrl signal validated() property bool loading signal requestReload() signal requestStop() property string searchUrl property bool canSimplifyText: true property bool editing: false property bool showFavicon: true property bool findInPageMode: false property var findController: null property color fgColor: Theme.palette.normal.baseText property var securityStatus: null readonly property Item bookmarkTogglePlaceHolder: bookmarkTogglePlaceHolderItem // XXX: for testing purposes only, do not use to modify the // contents/behaviour of the internals of the component. readonly property Item __textField: textField readonly property Item __actionButton: action readonly property Item __bookmarkToggle: bookmarkToggle function selectAll() { textField.selectAll() } Binding { target: findController property: "text" value: findInPageMode ? textField.text : "" } TextField { id: textField objectName: "addressBarTextField" anchors.fill: parent primaryItem: Item { id: icons width: iconsRow.anyIconVisible ? iconsRow.width + units.gu(1) : 0 height: units.gu(2) visible: !findInPageMode Row { id: iconsRow property bool anyIconVisible: favicon.visible || action.visible || secure.visible || insecure.visible || securityAlert.visible spacing: units.gu(1) anchors { top: parent.top bottom: parent.bottom horizontalCenter: parent.horizontalCenter } Favicon { id: favicon shouldCache: !addressbar.incognito anchors.verticalCenter: parent.verticalCenter visible: showFavicon && internal.idle && addressbar.actualUrl.toString() && !internal.securityWarning && !internal.securityError } Icon { id: action height: parent.height width: height visible: addressbar.editing || addressbar.loading || !addressbar.text enabled: addressbar.text opacity: enabled ? 1.0 : 0.3 readonly property bool reload: addressbar.activeFocus && addressbar.text && (addressbar.text == addressbar.actualUrl) readonly property bool looksLikeAUrl: UrlUtils.looksLikeAUrl(addressbar.text.trim()) name: addressbar.loading ? "stop" : reload ? "reload" : looksLikeAUrl ? "stock_website" : "search" color: addressbar.fgColor MouseArea { objectName: "actionButton" anchors { fill: parent margins: -units.gu(1) } onClicked: { if (addressbar.loading) { addressbar.requestStop() } else if (action.reload) { addressbar.requestReload() } else { textField.accepted() } } } } Icon { id: secure name: "network-secure" color: addressbar.fgColor height: parent.height width: height visible: internal.idle && internal.secureConnection } Image { id: insecure source: "assets/broken_lock.png" height: parent.height fillMode: Image.PreserveAspectFit visible: internal.idle && internal.securityError } Icon { id: securityAlert name: "security-alert" color: addressbar.fgColor height: parent.height width: height visible: internal.idle && internal.securityWarning } } Item { id: certificatePopoverPositioner anchors { top: iconsRow.top bottom: iconsRow.bottom left: iconsRow.left } width: units.gu(2) } MouseArea { enabled: internal.idle anchors { left: iconsRow.left leftMargin: -units.gu(1) right: iconsRow.right verticalCenter: parent.verticalCenter } height: textField.height onClicked: { if (internal.secureConnection || internal.securityError) { addressbar.showSecurityCertificateDetails() } } } } secondaryItem: Row { height: textField.height Label { objectName: "findInPageCounter" anchors.verticalCenter: parent.verticalCenter fontSize: "x-small" color: addressbar.fgColor opacity: findController && findController.count > 0 ? 1.0 : 0.6 visible: findInPageMode // TRANSLATORS: %2 refers to the total number of find in page results and %1 to the highlighted result text: i18n.tr("%1/%2").arg(current).arg(count) property int current: findController ? findController.current : 0 property int count: findController ? findController.count : 0 } MouseArea { id: bookmarkToggle objectName: "bookmarkToggle" height: parent.height width: visible ? height : 0 visible: !findInPageMode && internal.idle && addressbar.actualUrl.toString() Icon { height: parent.height - units.gu(2) width: height anchors.centerIn: parent name: addressbar.bookmarked ? "starred" : "non-starred" color: addressbar.bookmarked ? UbuntuColors.orange : addressbar.fgColor } onClicked: addressbar.toggleBookmark() Item { id: bookmarkTogglePlaceHolderItem anchors.fill: parent } } } font.pixelSize: FontUtils.sizeToPixels("small") color: addressbar.fgColor inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhUrlCharactersOnly placeholderText: findInPageMode ? i18n.tr("find in page") : i18n.tr("search or enter an address") // Work around the "fix" for http://pad.lv/1089370 which // unsets focus on the TextField when it becomes invisible // (to ensure the OSK is hidden). focus: true onVisibleChanged: { if (visible) { focus = true } } highlighted: true onAccepted: if (!internal.idle) internal.validate() Keys.onReturnPressed: { if (!findInPageMode) { accepted() } else if (event.modifiers & Qt.ShiftModifier) { findController.previous() } else { findController.next() } } } // Make sure that all the text is selected at the first click MouseArea { anchors { fill: parent leftMargin: icons.width rightMargin: bookmarkToggle.width } enabled: !addressbar.activeFocus onClicked: { textField.forceActiveFocus() textField.selectAll() } } QtObject { id: internal readonly property bool idle: !addressbar.loading && !addressbar.editing readonly property int securityLevel: addressbar.securityStatus ? addressbar.securityStatus.securityLevel : Oxide.SecurityStatus.SecurityLevelNone readonly property bool secureConnection: addressbar.securityStatus ? (securityLevel == Oxide.SecurityStatus.SecurityLevelSecure || securityLevel == Oxide.SecurityStatus.SecurityLevelSecureEV || securityLevel == Oxide.SecurityStatus.SecurityLevelWarning) : false readonly property bool securityWarning: addressbar.securityStatus ? (securityLevel == Oxide.SecurityStatus.SecurityLevelWarning) : false readonly property bool securityError: addressbar.securityStatus ? (securityLevel == Oxide.SecurityStatus.SecurityLevelError) : false property var securityCertificateDetails: null function escapeHtmlEntities(query) { return query.replace(/\W/, encodeURIComponent) } function buildSearchUrl(query) { var terms = query.split(/\s/).map(internal.escapeHtmlEntities) return addressbar.searchUrl.replace("{searchTerms}", terms.join("+")) } function validate() { var query = text.trim() if (UrlUtils.looksLikeAUrl(query)) { requestedUrl = UrlUtils.fixUrl(query) } else { requestedUrl = internal.buildSearchUrl(query) } validated() } function simplifyUrl(url) { var urlString = url.toString() if (urlString == "about:blank" || urlString.match(/^data:/i)) { return url } var hasProtocol = urlString.indexOf("://") != -1 var domain if (hasProtocol) { if (urlString.split("://")[0] == "file") { // Don't process file:// urls return url } domain = urlString.split('/')[2] } else { domain = urlString.split('/')[0] } if (typeof domain !== 'undefined' && domain.length > 0) { // Remove user component if present var userRemoved = domain.split('@')[1] if (typeof userRemoved !== 'undefined') { domain = userRemoved } // Remove port number if present domain = domain.split(':')[0] if (domain.lastIndexOf('.') != 3) { // http://www.com shouldn't be trimmed domain = domain.replace(/^www\./, "") } return domain } else { return url } } // has the URL in the address bar been simplified? property bool simplified: false } onIncognitoChanged: { if (incognito) { text = "" internal.simplified = false } } onEditingChanged: { if (findInPageMode) return if (editing && internal.simplified) { text = actualUrl internal.simplified = false } else if (!editing) { if (canSimplifyText && !loading && actualUrl.toString()) { text = internal.simplifyUrl(actualUrl) internal.simplified = true } else { text = actualUrl internal.simplified = false } } } onCanSimplifyTextChanged: { if (editing || findInPageMode) return if (canSimplifyText && !loading && actualUrl.toString()) { text = internal.simplifyUrl(actualUrl) internal.simplified = true } else if (!canSimplifyText && internal.simplified) { text = actualUrl internal.simplified = false } } onActualUrlChanged: { if (editing || findInPageMode) return if (canSimplifyText) { text = internal.simplifyUrl(actualUrl) internal.simplified = true } else { text = actualUrl internal.simplified = false } } onRequestedUrlChanged: { if (editing || findInPageMode) return if (canSimplifyText) { text = internal.simplifyUrl(requestedUrl) internal.simplified = true } else { text = requestedUrl internal.simplified = false } } onFindInPageModeChanged: { if (findInPageMode) return if (canSimplifyText) { text = internal.simplifyUrl(actualUrl) internal.simplified = true } else { text = actualUrl internal.simplified = false } } function showSecurityCertificateDetails() { if (!internal.securityCertificateDetails) { internal.securityCertificateDetails = PopupUtils.open(Qt.resolvedUrl("SecurityCertificatePopover.qml"), certificatePopoverPositioner, {"securityStatus": securityStatus}) } } function hideSecurityCertificateDetails() { if (internal.securityCertificateDetails) { var popup = internal.securityCertificateDetails internal.securityCertificateDetails = null PopupUtils.close(popup) } } } ./src/app/webbrowser/LeavePrivateModeDialog.qml0000644000004100000410000000250113004613604022015 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 Dialog { title: i18n.tr("Going to public mode will close all private tabs") signal cancelButtonClicked() signal okButtonClicked() Button { objectName: "leavePrivateModeDialog.cancelButton" anchors { left: parent.left; right: parent.right } text: i18n.tr("Cancel") onClicked: cancelButtonClicked() } Button { objectName: "leavePrivateModeDialog.okButton" anchors { left: parent.left; right: parent.right } text: i18n.tr("OK") color: UbuntuColors.green onClicked: okButtonClicked() } } ./src/app/webbrowser/Suggestions.qml0000644000004100000410000000564113004613604020023 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import "Highlight.js" as Highlight FocusScope { id: suggestions property var searchTerms property var models readonly property int count: models.reduce(internal.countItems, 0) readonly property alias contentHeight: suggestionsList.contentHeight signal activated(url url) Rectangle { anchors.fill: parent radius: units.gu(0.5) border { color: "#dedede" width: 1 } } clip: true ListView { id: suggestionsList objectName: "suggestionsList" anchors.fill: parent focus: true model: models.reduce(function(list, model) { var modelItems = [] // Models inheriting from QAbstractItemModel and JS arrays expose their // data differently, so we need to collect their items differently if (model.forEach) { model.forEach(function(item) { modelItems.push(item) }) } else { for (var i = 0; i < model.count; i++) modelItems.push(model.get(i)) } modelItems.forEach(function(item) { item["icon"] = model.icon item["displayUrl"] = model.displayUrl list.push(item) }) return list }, []) delegate: Suggestion { objectName: "suggestionDelegate_" + index width: suggestionsList.width showDivider: index < model.length - 1 title: selected ? modelData.title : Highlight.highlightTerms(modelData.title, searchTerms) subtitle: modelData.displayUrl ? (selected ? modelData.url : Highlight.highlightTerms(modelData.url, searchTerms)) : "" icon: modelData.icon selected: suggestionsList.activeFocus && ListView.isCurrentItem onActivated: suggestions.activated(modelData.url) } } Scrollbar { flickableItem: suggestionsList align: Qt.AlignTrailing } QtObject { id: internal function countItems(total, model) { return total + (model.hasOwnProperty("length") ? model.length : model.count) } } } ./src/app/webbrowser/limit-proxy-model.cpp0000644000004100000410000002570613004613604021101 0ustar www-datawww-data/* * Copyright 2014 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "limit-proxy-model.h" /*! \class LimitProxyModel \brief Identity model that limits the number of rows returned by a model LimitProxyModel is a proxy model that limits the number of rows returned by a model (i.e. only the first N entries are returned). This proxy model was copied from unity8's source tree with small changes, like the type of the sourceModel. */ LimitProxyModel::LimitProxyModel(QObject* parent) : QIdentityProxyModel(parent), m_limit(-1), m_sourceInserting(false), m_sourceRemoving(false), m_dataChangedBegin(-1), m_dataChangedEnd(-1) { connect(this, SIGNAL(modelReset()), SIGNAL(countChanged())); connect(this, SIGNAL(rowsInserted(QModelIndex,int,int)), SIGNAL(countChanged())); connect(this, SIGNAL(rowsRemoved(QModelIndex,int,int)), SIGNAL(countChanged())); connect(this, SIGNAL(rowsInserted(QModelIndex,int,int)), SIGNAL(unlimitedCountChanged())); connect(this, SIGNAL(rowsRemoved(QModelIndex,int,int)), SIGNAL(unlimitedCountChanged())); } QAbstractItemModel* LimitProxyModel::sourceModel() const { return qobject_cast(QIdentityProxyModel::sourceModel()); } void LimitProxyModel::setSourceModel(QObject* sourceModel) { QAbstractItemModel* model = qobject_cast(sourceModel); if (model != this->sourceModel()) { if (this->sourceModel() != NULL) { this->sourceModel()->disconnect(this); } QIdentityProxyModel::setSourceModel(model); if (this->sourceModel() != NULL) { // Disconnect the QIdentityProxyModel handling for rows removed/added... disconnect(this->sourceModel(), SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), this, NULL); disconnect(this->sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, NULL); disconnect(this->sourceModel(), SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, NULL); disconnect(this->sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, NULL); // ... and use our own connect(this->sourceModel(), SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(sourceRowsAboutToBeInserted(QModelIndex,int,int))); connect(this->sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(sourceRowsInserted(QModelIndex,int,int))); connect(this->sourceModel(), SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(sourceRowsAboutToBeRemoved(QModelIndex,int,int))); connect(this->sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(sourceRowsRemoved(QModelIndex,int,int))); } Q_EMIT sourceModelChanged(); } } int LimitProxyModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) // We are not a tree return 0; const int unlimitedCount = QIdentityProxyModel::rowCount(parent); return m_limit < 0 ? unlimitedCount : qMin(m_limit, unlimitedCount); } int LimitProxyModel::unlimitedRowCount(const QModelIndex &parent) const { if (parent.isValid()) // We are not a tree return 0; return QIdentityProxyModel::rowCount(parent); } int LimitProxyModel::limit() const { return m_limit; } void LimitProxyModel::setLimit(int limit) { if (limit != m_limit) { bool inserting = false; bool removing = false; const int oldCount = rowCount(); const int unlimitedCount = QIdentityProxyModel::rowCount(); if (m_limit < 0) { if (limit < oldCount) { removing = true; beginRemoveRows(QModelIndex(), limit, oldCount - 1); } } else if (limit < 0) { if (m_limit < unlimitedCount) { inserting = true; beginInsertRows(QModelIndex(), m_limit, unlimitedCount - 1); } } else { if (limit > m_limit && unlimitedCount > m_limit) { inserting = true; beginInsertRows(QModelIndex(), m_limit, qMin(limit, unlimitedCount) - 1); } else if (limit < m_limit && limit < oldCount) { removing = true; beginRemoveRows(QModelIndex(), limit, oldCount - 1); } } m_limit = limit; if (inserting) { endInsertRows(); } else if (removing) { endRemoveRows(); } Q_EMIT limitChanged(); } } void LimitProxyModel::sourceRowsAboutToBeInserted(const QModelIndex &parent, int start, int end) { if (m_limit < 0) { beginInsertRows(mapFromSource(parent), start, end); m_sourceInserting = true; } else if (start < m_limit) { const int nSourceAddedItems = end - start + 1; const int currentCount = QIdentityProxyModel::rowCount(); if (currentCount + nSourceAddedItems <= m_limit) { // After Inserting items we will be under the limit // so just proceed with the insertion normally beginInsertRows(mapFromSource(parent), start, end); m_sourceInserting = true; } else if (currentCount >= m_limit) { // We are already over the limit so to our users we are not adding items, just // changing it's data, i.e we had something like // A B C D E // with a limit of 5 // after inserting (let's say three 'F' at position 1) we will have // A F F F B // so we just need to signal a dataChanged from 1 to 4 m_dataChangedBegin = start; m_dataChangedEnd = m_limit - 1; } else { // currentCount < m_limit && currentCount + nSourceAddedItems > m_limit // We have less items than the limit but after adding them we will be over // To our users this means we need to insert some items and change the // data of some others, i.e we had something like // A B C // with a limit of 5 // after inserting (let's say three 'F' at position 1) we will have // A F F F B // so we need to signal an insetion from position 1 to 2, instead of from // position 1 to 3 and a after that a data changed from 3 to 4 const int nItemsToInsert = m_limit - currentCount; beginInsertRows(mapFromSource(parent), start, start + nItemsToInsert - 1); m_sourceInserting = true; m_dataChangedBegin = start + nItemsToInsert; m_dataChangedEnd = m_limit - 1; if (m_dataChangedBegin > m_dataChangedEnd) { // Just in case we were empty and insert 6 items with a limit of 5 // We don't want to signal a dataChanged from 5 to 4 m_dataChangedBegin = -1; m_dataChangedEnd = -1; } } } } void LimitProxyModel::sourceRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) { if (m_limit < 0) { beginRemoveRows(mapFromSource(parent), start, end); m_sourceRemoving = true; } else if (start < m_limit) { const int nSourceRemovedItems = end - start + 1; const int currentCount = QIdentityProxyModel::rowCount(); if (currentCount <= m_limit) { // We are already under the limit so // so just proceed with the removal normally beginRemoveRows(mapFromSource(parent), start, end); m_sourceRemoving = true; } else if (currentCount - nSourceRemovedItems >= m_limit) { // Even after removing items we will be at or over the limit // So to our users we are not removing anything, just changing the data // i.e. we had a internal model with // A B C D E F G H // and a limit of 5, our users just see // A B C D E // so if we remove 3 items starting at 1 we have to expose // A E F G H // that is, a dataChanged from 1 to 4 m_dataChangedBegin = start; m_dataChangedEnd = m_limit - 1; } else { // currentCount > m_limit && currentCount - nSourceRemovedItems < m_limit // We have more items than the limit but after removing we will be below it // So to our users we both removing and changing the data // i.e. we had a internal model with // A B C D E F G // and a limit of 5, our users just see // A B C D E // so if we remove items from 1 to 3 we have to expose // A E F G // that is, a remove from 4 to 4 and a dataChanged from 1 to 3 const int nItemsToRemove = m_limit - (currentCount - nSourceRemovedItems); beginRemoveRows(mapFromSource(parent), m_limit - nItemsToRemove, m_limit - 1); m_sourceRemoving = true; m_dataChangedBegin = start; m_dataChangedEnd = m_limit - nItemsToRemove - 1; if (m_dataChangedBegin > m_dataChangedEnd) { m_dataChangedBegin = -1; m_dataChangedEnd = -1; } } } } void LimitProxyModel::sourceRowsInserted(const QModelIndex & /*parent*/, int /*start*/, int /*end*/) { if (m_sourceInserting) { endInsertRows(); m_sourceInserting = false; } if (m_dataChangedBegin != -1 && m_dataChangedEnd != -1) { dataChanged(index(m_dataChangedBegin, 0), index(m_dataChangedEnd, 0)); m_dataChangedBegin = -1; m_dataChangedEnd = -1; } } void LimitProxyModel::sourceRowsRemoved(const QModelIndex & /*parent*/, int /*start*/, int /*end*/) { if (m_sourceRemoving) { endRemoveRows(); m_sourceRemoving = false; } if (m_dataChangedBegin != -1 && m_dataChangedEnd != -1) { dataChanged(index(m_dataChangedBegin, 0), index(m_dataChangedEnd, 0)); m_dataChangedBegin = -1; m_dataChangedEnd = -1; } } QVariantMap LimitProxyModel::get(int i) const { QVariantMap item; QHash roles = roleNames(); QModelIndex modelIndex = index(i, 0); if (modelIndex.isValid()) { Q_FOREACH(int role, roles.keys()) { QString roleName = QString::fromUtf8(roles.value(role)); item.insert(roleName, data(modelIndex, role)); } } return item; } ./src/app/webbrowser/searchengines/0000755000004100000410000000000013004613605017607 5ustar www-datawww-data./src/app/webbrowser/searchengines/yahoo.xml0000644000004100000410000000062013004613604021445 0ustar www-datawww-data Yahoo Yahoo Search ./src/app/webbrowser/searchengines/duckduckgo.xml0000644000004100000410000000060413004613604022453 0ustar www-datawww-data DuckDuckGo Search DuckDuckGo ./src/app/webbrowser/searchengines/google.xml0000644000004100000410000000065013004613604021605 0ustar www-datawww-data Google Google Search ./src/app/webbrowser/searchengines/wikipedia.xml0000644000004100000410000000065013004613604022277 0ustar www-datawww-data Wikipedia Wikipedia, the Free Encyclopedia ./src/app/webbrowser/searchengines/bing.xml0000644000004100000410000000056413004613604021254 0ustar www-datawww-data Bing Bing. Search by Microsoft. ./src/app/webbrowser/searchengines/qwant.xml0000644000004100000410000000061313004613604021462 0ustar www-datawww-data Qwant Search Qwant ./src/app/webbrowser/searchengines/baidu.xml0000644000004100000410000000062713004613604021421 0ustar www-datawww-data Baidu Baidu Search ./src/app/webbrowser/bookmarks-folder-model.cpp0000644000004100000410000000554513004613604022044 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "bookmarks-folder-model.h" #include "bookmarks-model.h" // Qt #include /*! \class BookmarksFolderModel \brief Proxy model that filters the contents of a bookmarks model based on a folder name BookmarksFolderModel is a proxy model that filters the contents of a bookmarks model based on a folder name. An entry in the bookmarks model matches if it is stored in a folder with the same name that the filter folder name (case-sensitive comparison). When no folder name is set (null or empty string), all entries that are not stored in any folder match. */ BookmarksFolderModel::BookmarksFolderModel(QObject* parent) : QSortFilterProxyModel(parent) { } BookmarksModel* BookmarksFolderModel::sourceModel() const { return qobject_cast(QSortFilterProxyModel::sourceModel()); } void BookmarksFolderModel::setSourceModel(BookmarksModel* sourceModel) { if (sourceModel != this->sourceModel()) { QSortFilterProxyModel::setSourceModel(sourceModel); Q_EMIT sourceModelChanged(); Q_EMIT countChanged(); } } const QString& BookmarksFolderModel::folder() const { return m_folder; } void BookmarksFolderModel::setFolder(const QString& folder) { if (folder != m_folder) { m_folder = folder; invalidate(); Q_EMIT folderChanged(); Q_EMIT countChanged(); } } int BookmarksFolderModel::count() const { return rowCount(); } QVariantMap BookmarksFolderModel::get(int row) const { if (row < 0 || row >= rowCount()) { return QVariantMap(); } QVariantMap res; QHash names = roleNames(); QHashIterator i(names); while (i.hasNext()) { i.next(); QModelIndex idx = index(row, 0); QVariant data = idx.data(i.key()); res[i.value()] = data; } return res; } bool BookmarksFolderModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { QModelIndex index = sourceModel()->index(source_row, 0, source_parent); QString folder = sourceModel()->data(index, BookmarksModel::Folder).toString(); return (folder.compare(m_folder, Qt::CaseSensitive) == 0); } ./src/app/webbrowser/assets/0000755000004100000410000000000013004613605016273 5ustar www-datawww-data./src/app/webbrowser/assets/tab-non-active@27.png0000644000004100000410000000145613004613604022066 0ustar www-datawww-dataPNG  IHDRlT*tgAMA a cHRMz&u0`:pQ<PLTEt@tRNS$RpqS&ln #Z`f05;?UxzAbKGDH pHYs  ~yIDATxENDEч6o]*aƈJYM%|l GF:M nlgfBkN޵ PHK#ή5g#SP;LYu u ulYfgʺʵ(&F`:JI#PS@Y/y5ec1e5~A _/~A_ ~/ E_/~A _/~A_ ~/~A_/~A E_/~A_ ~/~A_/~A R QfPIENDB`./src/app/webbrowser/assets/tab-hover-desktop@27.sci0000644000004100000410000000014313004613604022577 0ustar www-datawww-databorder.top: 0 border.bottom: 0 border.left: 16 border.right: 16 source: "tab-hover-desktop@27.png" ./src/app/webbrowser/assets/tab-non-active-desktop@27.png0000644000004100000410000000136613004613604023535 0ustar www-datawww-dataPNG  IHDRQ5sRGBIDATx1VeS#Ź`899E~!.$t]HCgm'99I?>Kcm&%fkI.1>c|,˙onVO>Г$_ۯ˲$?Ⱦն^*|y>ܓySoX / wb ֢㽿o[`}.|m{ Uۯ{)v˲ڞ5єsCvm/Nm0+S`{cj1؀[S!؀禮w ܛ>2k1؀S#Cvm${c!XǓ!  /_ ~@  /_ ~@  /_ ~@ 5/_ ~@ //_ ~@ //_ ~?QkŖIENDB`./src/app/webbrowser/assets/tab-active@27.sci0000644000004100000410000000013413004613604021260 0ustar www-datawww-databorder.top: 0 border.bottom: 0 border.left: 16 border.right: 16 source: "tab-active@27.png" ./src/app/webbrowser/assets/tab-active-desktop@27.png0000644000004100000410000000216413004613604022742 0ustar www-datawww-dataPNG  IHDRSI^*'gAMA a cHRMz&u0`:pQ<PLTEmmm]]]^^^^^^^^^^^^___]]]___^^^___]]]^^^___```aaafff]]]^^^hhhgggaaaaaaqqqyyyzzzbbbhhh```^^^```fffbbbxxxggg```jjjfffggghhhggghhhbbb___^^^```dddĀeeefff___}}}^^^hhhhhhbbbiiijjjccc]]]___gggggg___dddjjj^^^aaauuu______]]]^^^___^^^______]]]^^^]]]_ߧfstRNS)9IWbipvz|~X:?wzA-0 "%^d',hk23FGZclty{}ƖbKGDH pHYs  ~IDATxرjQs ٸADlm,il4BJ5#X BX,SX 6"( 26I0>j`??.w&㢼 ` _|ϟ濞*;8x%3:vfcoW{]Ffn*t^V?ܴ IDF<0hް"+5m)(װ=_T3Z|i9Z<wcx֮-,AM]- AQGF|Qא mw0e 0~c-{0lm 3PVfonT.nfl9 Lm F`P0 A( `P0(A` Q0( F`P0(A` `P0(A`P0 A( fZL#c!(UI;rIENDB`./src/app/webbrowser/assets/tab-hover@27.png0000644000004100000410000000145613004613604021146 0ustar www-datawww-dataPNG  IHDRlT*tgAMA a cHRMz&u0`:pQ<PLTE&8@tRNS$RpqS&ln #Z`f05;?UxzAbKGDH pHYs  ~yIDATxENDEч6o]*aƈJYM%|l GF:M nlgfBkN޵ PHK#ή5g#SP;LYu u ulYfgʺʵ(&F`:JI#PS@Y/y5ec1e5~A _/~A_ ~/ E_/~A _/~A_ ~/~A_/~A E_/~A_ ~/~A_/~A R QfPIENDB`./src/app/webbrowser/assets/tab-active-desktop@27.sci0000644000004100000410000000014413004613604022730 0ustar www-datawww-databorder.top: 0 border.bottom: 0 border.left: 16 border.right: 16 source: "tab-active-desktop@27.png" ./src/app/webbrowser/assets/stock_website.png0000644000004100000410000000724113004613604021651 0ustar www-datawww-dataPNG  IHDRFGr1!sRGB[IDATxŜ pUF"BaDJC„%@K P"KˆH #EeE("`h R&KP$aI!}9w_<37g}ޗj_a8p@zeeeqqq7W^u_qU% KKK̢Ù;ɇBVղe˻N)0lq޽<CPj䵀t'Al|'` '!H˵3ڶmOs׮]r4 $`!R?eLTL-7 3&ns;pC*ݻwStj6ʧh[Djmi'J?Z"]x }'UԽ_F1pQƌ??,,Q/w$n/9:t^׃zbxM9:OوsPhRH*¤aҟYʃP߇G@0uB`2"+(EEE{?T )))y,Ue^㰁#8*#ynJɅG"q迴 `/m{tS} Z}Ps{Bv,Y"PP^E|JrULڵ*p+9@RPdLP N:7xy)G k.^@)EuJU[l E :H":uhez]lgD{k8{lʕ+WvBJlO0 fcXei4@*Ц۷o+JS%ӈ'rٲ ݏyҿ`!yN/pAo'ңS#}vڜ!qT] OkĪbXWތ$MPOU/Pg^As@9ϱKGhP22ZoI%qo =0B;=FeJJ;ȶAf6iYwb52l)T(BƷif:2^)oaCʓhL%JUi ~d-9Bz8Xm "ӥ 4;PtHΖ# P#9Rriuvgۺ](Y]na͚5!yXn^";(" J;q"ieG-54@2dbe𔃳 31V QEz%* 5g"Cސ;@9t7x5]^f1Ӧ9`?["m`+'5ݡ*{0[UlQDɒjxMFΎơ\%aP<.U^PpbڽɊ5wĂ7?hU"d#%||V/c N'lDKZQQ\#K4m?B]>"ӥ.UěN' -AJ<)SɮȮ.Z$L{"Hz}A;1Nvu萡՝jINwB@*ǹIޟhRX~ Uo`8mDhp*[DZR`"T+;#_>jvʄHu1s{}am^W$%#7h{ktlzhewx/hIտ(a*5}v:}2N9~s:] ɘZr". 9zGer[04I&PVi,񮲫\?*L>Q?j*L%LU:kZj%/cpKdT`-C g(_W:CQPc$SjN FrQ``ƀl Lr""4Rocw:xQDx+ Wh?^JW|0r*ΨJvomU^1/5YO#*}43uP^5LUәW [;ʫ*fd@ /"`WWs%𾑖4A-# $ƕ@$U>90:aMJ0az ]UU%qP3l`%Woia6̵-h.>IcVIV9zh=c\J}ًĕ!jhpZSW3HWiaÆxBz533wL2|$G5wjy3i l@s edW5j$鷍aY ~R;BAI[`) e~ELcpBM&G^ǼL۟jm/27}#0Hiޘn7UKRtF0EѿAI㕮+00ɽJDҁB7c/XnjDnF{?9ɔ-Fh7P|GŞX XXV{2 mF{w{]rKW0Ucz~hWɜdD{)_Q b֏x-UnCБstV^)&/g9ѫ"?mI2 e|No)_1uIR1eN$I'߇ =ꢈf|*81 1FX@ΔԐM\ :y~3e9ȳc B"Lm^6rNc J ߂IENDB`./src/app/webbrowser/assets/tab-hover-desktop@27.png0000644000004100000410000000140013004613604022602 0ustar www-datawww-dataPNG  IHDRQ5sRGBIDATx۱jSq^ .NCũn= Hq*NIhTB5. hsx8c>TUッUխ$l&٪Q$/MUfIUdG9yU:cUMd7lVhte<.oK&` x m%GQX}Zkm:e7W[} {Uczw[`ߍ1@>mI @oIv  AtEXtSoftwareAdobe ImageReadyqe<ZIDATx]Ѫ!0< {!mmɖekZ$˿~a___$00s\2go-?v#]T3{v18;I h ~w["ie$J0[Uk]u`h3@6r 1``,9uwne ]U '¤{ᡙ9-M1ގgZg@=VϦOd p1eQv*XtC of~uoۜ uc=Zɣ]?w- y#VzS޺@WcĮ.)9RP;ʷ_ d_G MJs1sKa%]I)^Vނhf=Yfdy>}[G{ p0#f{o^74j UȩmkCxz?@b NΘ࢓+TU& ǥ} V ػcY!S1&ZFnYuaJT =o1zp(CϠ{5e~Ҡ7@M/AuJ@7@U/ԇTP;5 HaMӤwEPkW 5Ak 3w֜Z}V`^FKCuP/CsXp+N~a%A]5VA ZnͺoB4mԯ_[+ rG > JK]ި!lsZuQ ^ڹ8'X%1R݀~ Ie/͡DΦxhA,<; /v-{6Benhl"Z@ e UPD2YfeM<(W%1C52}.OH.ٕzG>+ PUFV| pъI5xwu"%B1E[X8$ I ƌco5VF<>rҀ /vmDPsi'0e-Z43d!M0C0l33R8ռZpn:C3M+S^m_(עjʛL/8m?lD\J4]PoYT6PWZTMdʕ5>D=16-XkHG};r]Zmz\a`J*X=8 ?QVfnܻX4rY(P6JZp3} >AyB|w.nٛcɵPQ֚]^~򸈔{U, j3 UNRRVY! Y43 UN/7)BCk-%?xK1ㅵo'xkY&Q,l6eO5yk+՞(uZ־ڼ7>sDM-hk\Ĕ34rCM!o]FR:2`YL@J`Zϡ}y֦.(ȶ RWu}5'$&הQ<': YQzΩ;#%WivAϹ8"P>J`ڙ8tWOuqb7rEŵ}d}&B?\1Zg\53=g< pu[;t3/n Xy=ᆗQ"YwS߹$c7\6i7|h,\ajr:D.4[}c9 خT|G(zyo8l?FQJDyoK Y?cG(Tޛax$N~lLhTFYVI3b\%ohq{0ALvb];w`;+y+U2`[r ͞^dW 6c?h1DZ,ט FV/EL-@vkt&e`WDaPޛgх„O8W(*ֵW~]Dq֬"!rnVdWt~LuKJD4%6P `yx-;J|&^C<ɶ 'NyCk2 tGYCePo cȫ#@:>ס=8(-EtOò*![(5aYMA-ظY؋aYՊO*^&-U {6^-xRtoQ9ʤY2̰{9\#w63\@UX6IA4`#d6' }0`HԟBvj䯞} ehw9-l)3Q)#9 v))wоz[@0 F{G8ҼD(M>lp/WC.Ф)75ȇgZN/,=.r艺r̙Ŕk`f.8[bSzlE& 0nh_l1 p 4K ;&OC,hպ ԛ+s>;۫"0 $ >2an:Oe8+KlPG =)M`عts6yNZ|6O"B켿bS >\{W}:p*7Ӻ?C_){-NZD 7cYD `Skkdc|re n1s9Q u)vSi!wʋg;>ÅC Z6/]/x H|͔~VNj#ZN^8 4Z/B3"+ǡ,؁soK{bV }P]23c}U`SԵ܄rKqB\ l7j۾СZkDyE-_;j!ʵ/-U6CLo^ !lZ[Ee`OUp&wL?᪶5Չ>貮z0J7&x=Up&w|B RRJl|dHV,̵M܉4]7tiڈoN tyIK.ϕ lBԏ N<"qz% I} j[_V}͵k¬55Rj*n;j=! hEw )~+b`OdjXaˮ+]+9v kExz83\B9aAHw_)/t Z TO+R-(sX7LbLM%泽QPsZK*Up(%x(꣕S3UF8iV 6uc{^ck R\ГsjU%zh{#QI ǻiTсErF@ uw͸1jس P_yiYpǟj[U=W7hhTD5B#l1OtǷujjo|JɎ"5K=Z6d-(K^Ԩ-Hkw3y;~4?-pp5 w|hLwf/YVD n;A)ljZđo@OjZk:qӦJkA;cd|? )-ͫynՔK`3*ԉGr(T5ģH5ufMS<3ƅ\h' $l%J"%KJ}oX"&0? M S< X(-@ E8UK *z`7fw?⮢\NiXia*/P, tf Ik~ U+=UH}y5x`n"|Dn̥T^IpB#s /[n%ը/q=!V2R.yW&kTza D״נİ҈3l|u0E!nzύ)*7tp1?s{h-7;ݶ%-0p6=À@U@c|z\s q8g l+B򫭰"@r](00r?տe l7gǥq0 S@ڔ3n c`<[Iwu102 wo-w]Y+3fIENDB`./src/app/webbrowser/assets/stock_link.svg0000644000004100000410000002016413004613604021156 0ustar www-datawww-data image/svg+xml ./src/app/webbrowser/assets/bottom_edge_hint@27.png0000644000004100000410000001443013004613604022565 0ustar www-datawww-dataPNG  IHDR`N)n<gAMA a cHRMz&u0`:pQ<7PLTEotRNS  !"#$%(&')*,+-./2034156:897;=ADGIECFHJNMOLK[hzYhּkPLpsMuRۍOaˀ^_ʆomohfeTQeg|ubKGDH pHYs  ~vIDATxͮnuw͟O٠H BA Ft@( h!r pN!P(€99gk9]U`vI@` D " z  bAD DW "&00@ b"DxEg_W  &+f0>`TYlɀR2A- ɩ K#UH$L,UӐ)j*HM %Nd&q"$͔Rfb*if PAM((P4B&‡a|-L@Z!lc  a )(:!(8R( Pfe,œRZ#ӐVM5XBTbp;O$ @R*Z_5rNO̖`0dP3%ٰ`, m#nSAy,]`Y#ja00E 0r)"W!ҙ/`;SԝpJDepva28UQ;A@'Tdx-IT-ay iXu%D'7Sd;f+!l*-܀, j,2;SmU8uu02KsjDf!LLCU8! hFy䰢-Bj{{؊zE+xp̍JlXhS ;(6Es ʽ\Kh$걣QP;4gٸ~x.H1ʴբYG[^Mg5Ĉ3Hx}K6!WK:y]_M *L.@;U4LYZ9T I&iz3Z1>7淗 -qm6iiu Ete,d1/FKiD6*SқYx8،ETrC6!N%ٛp(J*OjrE1`<`l-6;") _A$ƄU.F.dr#ZfU/}PE6)hV.9/}KXё-7}ɧ>M./ZF<OXLn5ZFH̉LvB3 BzZrᢺgj<|UKx~ܪ:xVĖ))LS3 ~ ̹MvoU_T6q]ٹjjڊ(nގ,eFWgJB쫱̺_L,SxS&*< @N@@\BI&E*TےHAE ɼb#-4;u«)'Dc% 貞L@;UdB<=H]o7ޤ ,i%hYDm+C婍1]QCNڲIoGΛ <+=XgޫiXnzuUh2d6(!ƙ @HT@CFZFF|/܂-Efl\c~,lv:,LkNҫyܢf#Waƙ|+Һo#nă(E-R0+͸1h'mQzVfP-K6ԶrU_,|je5m>6w:+7 x2\}z ~K8)tuiUMDj۹s&zzdVgVf~2ӴXkt]EAY˪ ~/rၚ/| vqGFmD[fՌlѸyM,(xYd^fXd0Z;s֪EaEM*I8mF RD}>tZ&:&XXnQ \7{يfN)MM)BGOi_a M~AV]bao&Y5C1.{jh"s UK>tmݼj-Z4mYiuL.yNlGHKOfs!7ڭAsKbMhܳ 7 .dJdJԬ%D݌DYznѰu"Rˌ* 8`..ӛCcnN4A"Jtl0^%mf)GIzA t)̎> lc+eQ$ %ݔ&ےƀdGIuF#K@9vz@QQ"c>lɸC(e"L V :YNjgxws?W/ۢ1vDu14}Xs;bN?AulwSLس{v3ez'V㺁rT.zL>^6%:L {d {}_׉AgQ \cAq)_ۋ~]l3]ҎrԕVAb01ɮХ&+턭¤E|c{/wK_ B Jj DV'{}nK9UrhM+diS8w,r/ӖhE#.u&LN0X/i4VVWShy$٘܍(s_W)+J1Ξْ@AIh@ʽO,q6D ^F_IxI8k( 8ESHӦ$c)WgLZ3 hcY~T%^G 1R(*PV&O-?mU"6Z`T I3`b$J"J +at AjH+#IDIz ċ18A P (F d,|F$ Ն4vp2RE A Nӥyj&Lq5Y@!E~>9O]/ v`2vSzf+P|/iW#70{4Yyh%f/96NJ18Ws&lf^e>җ1~%;-^cA294yUx ^!>V_WODM䷠9Ȼ:Ҝ‘>X)dJO\oW<_q.Cױ@k !|Nd[Ǜ zI]<_&7Gy#QqfƖ|%6"˰U9`-@x`|]JE #&W+uv/#Ɯ0 \9sK)ZhW~9`潂r;>$N&2AHFENK9;]N) 0hL[Wz=J2>ڽpY9|V_-^W)x@vmm]%Hl4zFAx]e9 \;VG b-)-;Lg'cu<|\5&\y]V+zҒGN/o!S (e7V?ǽ+ېĥD<өkQf2PGO^PBRv6f-XD<`G2G!Ek9ޭlKakJVC<|?6xG4(%{c>/L^%I4v:k+pFB[$I;Ɩ[CdQJnƜrd_ 7$Ih1. E{2$Ivt.v 0(F)&I$%֕.9u@ u* BH$itFm}Y[f 9job#3١q tH$I#;9n5cgAG]o6 6FvoD;8ZKJ$ISnsd[pD=)%I䜍rpc\Oel4X_{yWvlI$mE%:-UJg~xnLG%IpVFoM!G既vqt.v\ I$rho{pdvodoOiݢ I$[OQyK6 7 :lQxo}J< J$Vl=.6;ؑ/ 8ZPcJvoH$){|Ԕ:tt;Lt2xGfс$I-+R>F#Í$|$IwuȐ#0mr $IGXһM7p2":i2$Iҋ2RrgpLwmI$n#Ezj =zÍںxcQְc.2I$I%[o~.'€5E՟N$Ir7Z@ga\H$`dֺ[VxzUkQH$IT^ 5Zn連'sow~O$INJoKkb 8z1ξ?I$IiZ@+5aI{kvD/$cugVs8STz){WEM$IёYSYxhxl}xEkk:EGy w|C$It38Jbz:+C C 8SEba7ueoSɬ)86T{8d׉S̃/{_`0f~w+/ 'F֖u¿m pP wiߗ$ICFpchHʙu@^2 ֎|Z>&vpom 8{`F`3;83π8< 83π8< 83π8< 83π8< 83π8< 83π8< 83π8< 83π8< 83π8< 83π8< 83π8<eֈlepxepe$eWbpO 8zئRʴe3{=a5]'ee#8ep9mX>]I^<,;ۋ$IN^o-#[ci 3^Yuztu/|ǚqJz$Ԓؽ3s'ޣ/6Kg< k 7B[ddd$d ;80#3 Y;o`XONsE_L$tौ0ev|U$ɹeÛmKc1:X_R$iS <^yL&K Zo|֔Swkgk,Mw KcQ>nH.CӏkaByL[$ITkgեCC I:аc\nԆijp]a㩳6}^-܎oIҷ1tKܘ\gvq,C#4MI$ٵ YV~f15 w%Iѵ{.[TGoߓ$Iucuni+d,΢/'3Cc9i vlu!Iz<v[]{6!I1옃AEv1ңlI$mXvsvnf̥}_N$S`=7RSTbSeqI;;$٥ѾނeqV\rOJX;/T+Ik^sov/۰wk`gƿsX^pOw,{+j;5<57X.O/B@;$Izt%e.m*ԾgHJᡯި :>vr̥ԔG{gqܲ"Ik%(kopsZUa(Iވq_vz loqO~n!I#֌nSy=eYݚ ;j񩱕yߥѺo EO͝SÍhMa$ItKpcvJѰsb'G1-,<|itnOަro 7FwmT$mdo[y^>-E%zzq vr̍1ՠc},[D-%IՔN#<1C I$ٱ(_TpSh$ݦ2'wmxޠ##{{N%I匎Sj}=ܞ~tlYy$}W SkQQCeË36$I *^m#pv6Z[;䮗/[ _?^]p]PR*/a]_fIX_fY"Î޹K%X̻uuޮppԅsk9?#zױ7̰m]UoOY%q I 8wKII.[CjI2D.=!-Kg^_$~,%qg^ƂMz;}wxD~iYkݖRb|,I1VfkWd-#/94h]Jںtq2h`]Gg$IW|JY|jQ=Y?1\-%~(s@|gvjz7%E66Cpp萣Y]#iԆ# es;%>Pڀ# 29@Q\"IgٍG%9p l=sc  ^ $σyuQǭ[P%~1H-ѵ:6AK0,ҝ!I{%y] 6zoDDC6>o[Igl}ͱu5xs=(Fthmȑ=G#Z|K$qD俍388@#yQJ(Em2;3$iː H&:丗aXKѭ*s<Ĩ 2J.r+Í[6J9cyt(kGl> =aǼFֵw ++Iwq-[Q}dgppF.hQr,G$+>v֮pggppGfG떕{h81sS$)h'#qmRppG)G®νwѻ#ZJKQ귬DÀ .z>"6x *gvjFUt7Gz!c5=a%6^خ.ʷsZ-!pZm=:6@QI?|4{(imƧkgvs߱}^3I>u m}-rDE%i: 884n J$Iz{i؀ >Cm+K~q=7Ì̐#S ;CCIz}fTgAMA a cHRMz&u0`:pQ<PLTEmmm]]]^^^^^^^^^^^^___]]]___^^^___]]]^^^___```aaafff]]]^^^hhhgggaaaaaaqqqyyyzzzbbbhhh```^^^```fffbbbxxxggg```jjjfffggghhhggghhhbbb___^^^```dddĀeeefff___}}}^^^hhhhhhbbbiiijjjccc]]]___gggggg___dddjjj^^^aaauuu______]]]^^^___^^^______]]]^^^]]]_ߧfstRNS)9IWbipvz|~X:?wzA-0 "%^d',hk23FGZclty{}ƖbKGDH pHYs  ~IDATxرjQs ٸADlm,il4j:{KG|4 "XlDP\7dl9}t~\LƲ\p gGW,O78x-3:nfcK/3<91U[[B%+ ~iګӈ0ɇh"+5mO)(=_T3Z|m%Z<cxn,,AMW- AQV|Qא mo0e 0~g-{0lm S3PVfonT.ifl9 Lm F`P0 A( `P0(A` Q0( F`P0(A` `P0(A`P0 A( `P0(A` Q0( F`P0(A( `S T5xiz/5<'xIENDB`./src/app/webbrowser/assets/broken_lock@27.png0000644000004100000410000000216013004613604021540 0ustar www-datawww-dataPNG  IHDR=WӽgAMA a cHRMz&u0`:pQ<JPLTEIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII%$xltRNS `Ędvx!-0[\ra:8bt@>+,Zhg165(*? .sEC]RQB}Kutogx:X4赃KAj'(R@ǐxMtfMK7q6aQ2\~zUþNA: ЛHzw%8H 1R4} ϑR+>vAYu4p?R_i?"5yt_  AʌhN4[ٚhF3n4KB4C[Y"vb` VLXȯS?! _IENDB`./src/app/webbrowser/assets/tab-hover@27.sci0000644000004100000410000000013313004613604021127 0ustar www-datawww-databorder.top: 0 border.bottom: 0 border.left: 16 border.right: 16 source: "tab-hover@27.png" ./src/app/webbrowser/assets/tab-non-active-desktop@27.sci0000644000004100000410000000015013004613604023515 0ustar www-datawww-databorder.top: 0 border.bottom: 0 border.left: 16 border.right: 16 source: "tab-non-active-desktop@27.png" ./src/app/webbrowser/assets/tab-error@27.png0000644000004100000410000002235013004613604021150 0ustar www-datawww-dataPNG  IHDR ϜsBITUF pHYs+tEXtSoftwarewww.inkscape.org<$gIDATx ew]'K MF #\83(##BFE Ψa9΁QaKȐHHttky.sR^۫ջ?]ɩA @ gd(g\( gX( gؒPd gXX5x6˝}@fS,?> =j΢6ͻ]f,`SbY(lY4@by$TWx^΢6!׆H˵6G31P ƾzѱbc?~XC<͢69k|32>ԉN-O,X~RX;w/}&Ka+*kUW徭hM++x.)rq͊*|Hy.;E3s,b&I(ׇs\֥Yg Ugq KWYUѬ3@r,mCyy`B4X &UҚWQ>3cXcQW\9:LUO@,c8{wgby{%ǂwž9Lof =r, [_t慿.wѬ3@\cO\iWrg^f:s1vtxW'`ξx0uureggX˫?]OCu::;$:L\cOJg4'lr]_sb`[sm_SEUc_^/j<6u `י},U7J6#$0ܱ/O=xSbϬ:63 {Na=eFgnm_걩:63 ˿{~R 75~)v}1`jѦre z̢m-IrZЮ/79&>%()ܷꪑi!}y7}ޒF Xn׋ħ¾sD^s= iRs__tS]53 +/N°&qgOՙ̝.5+[nЙm?Ť+oYy (ي``Jg@,?e_:xKbtf{,7c.- 0^1Cgl\4?ݺ\=60}yO>Ǥ%:3rc_^)XZy \ښcRǦU#:3r_AsD|μSg`;r]_>zEϾ\̧?T~ Np^R sig?u @QnczlK:3cR\$+%M:|1$%}Ftfrݗ'8\Wtf @fm_NCajg>Cg }tL˕μ0O^sڙ/\eg 4r :3׊}?&xlޗߛ @rp [0GIP:3˓],Wy,&p^;2$Zx @~x0ycS^53|ӇTcS_~ @VbII m,WM}Lg }ҩw{;S=63 f2Mrx}yH:w*w.og93 ׊<+ s3C:b o0/C`o0쩮uk6]0Iq @vx09J@ƻkw0:⟥H,D_³(܊`̤;bMoJo>Kg`Pܩ/OoيcR`DÚLyo-`͂Y4}f%GZ9LLO4/Gq6&۾N}x}b$k˯8 rsb`ޙ`Q=l @rрcE0﬽΁s,uf _f,XG_Y؊#bx5G]Kە+5`3Kga&x|M0lœ+4vww.)x6rN3] WOjI @p 'aFrR(tјg<ϫgcɶGYI1n;ktzPg|gzفN`VѬ3rx8C}9]W)ltڱWl @XpȱǙbu +/JWتμ8uWŒG/Y?'1^M;s}KQ| X0>Y &k~5 xno^ufl]_i9&N|?{<f5}ypSX.&|`7{g?14<~˳uLjy]Nf+/{gv ;dx$}|{;/VWug|WLr1:ߗll Xno/ ;^)TyVڙǺ m ?վ\L}}`|Y`96uq61<3 k0>~KثjgbcRFIv+ZsuڣŮ\{I3ۺ/lZ^GޝX`>ˎBDSb ,)Nf6{5VL lE_>8ӱ\Lh06N|l+o^ױj8@_nݗ߾?Y 3sW`>Y+/(wh6Зr5YdKW٫ű\_X,v9'߹$ɽt0tw 0}yŹJ%ODy8Aglr k3O|l9^vq`u_.毌sޕ+N!k"덳E_0yW(|zr[Llҭɐr0Og0}yxc^kvONt0[<<,鶟49`xL8盬8<0`nzHx8{;ʝy8`H_sk;:kyl<sm_oB]9_9~ :/W)*֕sl sLJW:q6QnFmo`Ol2ݗ}yI`86s]֙җ#v~-uq-`9Z_>XWεS37hK_~l9/?ЗGO6{iSH;sq-`yq[i憗c:3@xHWM=g!wclk=~Gm ؤq6@.rt4kxv:q6@fĺP?6l }}pr_zpxJ;/sq Zk=$ 8.Khnȟ6u{Cg6C_yq<8(>78;8+8383wNp^p~|8 G<v-!|fp  V׾?J+_- rAq6@yWC+.{=qifҵ;w0. o4C-zg0Ⱦ?4U5]^FOμvD3@&y$;xhor9_.El)d>}M=ܝ=^f[חw .z v?/Mcyv`1c}w^:G<0sTokPI[^{ 񱩻8[g|_{L7{4pw`N;lp 󱩯^%w߂KҾmJpy+?ԏ>W>ݶ`q E0>qy<D3@+s٭. `byu0Ǧ8`}sOItL*ԕsu8l @X#]Ǧ;h`}y㘔wl-`庾te[L;qxq`̾|mQprg)?"0~w 6?vQ3nL0lJ_.*6ůyo-`:3囟GL[/[ftfML-`Glm{_9ǤlvwLg tLHllڙGtfd|GlM_3/=bh]zjY0ܗwuez96ukڙg[tfl7.#]|:a6:i_.(Uy!kwf`&K̺Ksa`j3YXWfo(?kj`،`mbC`ugO6#pl6˃ن3@|F0PWfmK0cً*S>,6;񅿶ً }'Muw` @0 +QKw3wlԩ`-6!Jب~>cH0’6mr\ `|F#t׉4fC0?L_`Q9L_}2q[C F3;s鑏'֕Yw_~0878l 07pf/#_T٫o uGJ_ ق!N7_2c*Gi,ޓقcgYg{ Z|7Up~yO6ԙ+<[ؕM _}헂Ҷ hN %w}nݬ_h_}/oz+V# ^b%nC(>{3*Wwe m8w={" _OW.bY0lb4F"^ox z"}}^ˁIENDB`./src/app/webbrowser/assets/toolbar-dropshadow.png0000644000004100000410000000175113004613604022616 0ustar www-datawww-dataPNG  IHDR fiTXtXML:com.adobe.xmp -v(tEXtSoftwareAdobe ImageReadyqe<IDAT[c````dLpb"-b&CIENDB`./src/app/webbrowser/assets/private-browsing-exit.svg0000644000004100000410000002474113004613604023274 0ustar www-datawww-data image/svg+xml ./src/app/webbrowser/assets/tab-non-active@27.sci0000644000004100000410000000014013004613604022045 0ustar www-datawww-databorder.top: 0 border.bottom: 0 border.left: 16 border.right: 16 source: "tab-non-active@27.png" ./src/app/webbrowser/NewTabView.qml0000644000004100000410000003613113004613604017522 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Qt.labs.settings 1.0 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 as ListItems import webbrowserapp.private 0.1 import "." FocusScope { id: newTabView property Settings settingsObject signal bookmarkClicked(url url) signal bookmarkRemoved(url url) signal historyEntryClicked(url url) TopSitesModel { id: topSitesModel model: HistoryModel } QtObject { id: internal property bool seeMoreBookmarksView: false property int bookmarksCountLimit: Math.min(4, numberOfBookmarks) property int numberOfBookmarks: BookmarksModel.count // Force the topsites section to reappear when remove a bookmark while // the bookmarks list is expanded and there aren't anymore > 5 // bookmarks onNumberOfBookmarksChanged: { if (numberOfBookmarks <= 4) { seeMoreBookmarksView = false } } function ensureCurrentItemVisible(container, currentItem) { if (container.activeFocus && currentItem) { var top = container.y + currentItem.mapToItem(container, 0, 0).y var height = currentItem.height if (top < flickable.contentY) { flickable.contentY = top } else if ((flickable.contentY + flickable.height) < (top + height)) { flickable.contentY = top + height - flickable.height } } } } Rectangle { anchors.fill: parent color: "#f6f6f6" } Flickable { id: flickable anchors.fill: parent contentHeight: contentScope.height focus: true onActiveFocusChanged: { if (activeFocus) { contentScope.forceActiveFocus() } } Behavior on contentY { UbuntuNumberAnimation {} } FocusScope { id: contentScope anchors { left: parent.left right: parent.right } height: childrenRect.height Item { id: bookmarkListHeader objectName: "bookmarkListHeader" height: units.gu(6) anchors { top: parent.top left: parent.left right: parent.right } Row { anchors { fill: parent leftMargin: units.gu(2) rightMargin: units.gu(2) } spacing: units.gu(1.5) Icon { id: starredIcon color: "#dd4814" name: "starred" height: units.gu(2) width: height anchors { leftMargin: units.gu(1) topMargin: units.gu(1) verticalCenter: moreButton.verticalCenter } } Label { width: parent.width - starredIcon.width - moreButton.width - units.gu(3) anchors.verticalCenter: moreButton.verticalCenter text: i18n.tr("Bookmarks") fontSize: "small" } Button { id: moreButton objectName: "bookmarks.moreButton" height: parent.height - units.gu(2) anchors { top: parent.top; topMargin: units.gu(1) } activeFocusOnPress: false strokeColor: UbuntuColors.darkGrey visible: internal.numberOfBookmarks > 4 text: internal.seeMoreBookmarksView ? i18n.tr("Less") : i18n.tr("More") onClicked: { internal.seeMoreBookmarksView = !internal.seeMoreBookmarksView bookmarkListHeader.focus = true } } } Keys.onEnterPressed: moreButton.clicked() Keys.onReturnPressed: moreButton.clicked() Keys.onSpacePressed: moreButton.clicked() Keys.onDownPressed: { if (internal.seeMoreBookmarksView) { bookmarksFolderListViewLoader.focus = true } else { bookmarkList.focus = true } } onActiveFocusChanged: internal.ensureCurrentItemVisible(this, this) } ListViewHighlight { anchors.fill: bookmarkListHeader visible: hasKeyboard && bookmarkListHeader.activeFocus } ListItems.ThinDivider { id: bookmarkListDivider anchors { top: bookmarkListHeader.bottom left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) } opacity: bookmarkListHeader.activeFocus ? 0 : 1 } Loader { id: bookmarksFolderListViewLoader anchors { top: bookmarkListDivider.bottom left: parent.left right: parent.right } active: internal.seeMoreBookmarksView height: active ? item.height : 0 sourceComponent: BookmarksFoldersView { focus: true interactive: false homeBookmarkUrl: newTabView.settingsObject.homepage onBookmarkClicked: newTabView.bookmarkClicked(url) onBookmarkRemoved: newTabView.bookmarkRemoved(url) onCurrentItemChanged: internal.ensureCurrentItemVisible(bookmarksFolderListViewLoader, currentItem) onActiveFocusChanged: internal.ensureCurrentItemVisible(bookmarksFolderListViewLoader, currentItem) } Keys.onUpPressed: { if (moreButton.visible) { bookmarkListHeader.focus = true } else { event.accepted = false } } } Loader { id: bookmarkList anchors { top: bookmarkListDivider.bottom left: parent.left right: parent.right } active: !internal.seeMoreBookmarksView height: active ? item.height : 0 focus: true LimitProxyModel { id: limitedBookmarksModel sourceModel: BookmarksModel limit: internal.bookmarksCountLimit } sourceComponent: ListView { objectName: "bookmarksList" focus: true interactive: false readonly property real delegateHeight: units.gu(5) height: count * delegateHeight model: limitedBookmarksModel.count + 1 delegate: UrlDelegate { objectName: (index == 0) ? "homepageBookmark" : "bookmark_%1".arg(index) anchors { left: parent.left right: parent.right } height: delegateHeight removable: index > 0 readonly property var data: BookmarksModel.count ? limitedBookmarksModel.get(index - 1) : null icon: (index > 0) ? data.icon : "" title: (index > 0) ? data.title : i18n.tr("Homepage") url: (index > 0) ? data.url : newTabView.settingsObject.homepage onClicked: newTabView.bookmarkClicked(url) onRemoved: { if (removable) { newTabView.bookmarkRemoved(url) } } } highlight: ListViewHighlight {} Keys.onEnterPressed: currentItem.clicked() Keys.onReturnPressed: currentItem.clicked() Keys.onDeletePressed: currentItem.removed() // Setting 'interactive' to false to prevent flicks also disables // keyboard navigation, so it needs to be manually implemented. Keys.onUpPressed: { var current = currentIndex decrementCurrentIndex() if (currentIndex == current) { event.accepted = false } } Keys.onDownPressed: { var current = currentIndex incrementCurrentIndex() if (currentIndex == current) { event.accepted = false } } onCurrentItemChanged: internal.ensureCurrentItemVisible(bookmarkList, currentItem) onActiveFocusChanged: internal.ensureCurrentItemVisible(bookmarkList, currentItem) } Keys.onUpPressed: { if (moreButton.visible) { bookmarkListHeader.focus = true } else { event.accepted = false } } Keys.onDownPressed: { if (topSitesGrid.visible) { topSitesGrid.focus = true } else { event.accepted = false } } } Item { id: topSitesHeader anchors { top: bookmarkList.bottom left: parent.left right: parent.right } visible: !internal.seeMoreBookmarksView height: visible ? units.gu(6) : 0 Label { anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) bottom: parent.bottom bottomMargin: units.gu(1) } opacity: internal.seeMoreBookmarksView ? 0.0 : 1.0 Behavior on opacity { UbuntuNumberAnimation {} } text: i18n.tr("Top sites") fontSize: "small" } } ListItems.ThinDivider { id: topSitesDivider anchors { top: topSitesHeader.bottom left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) } visible: topSitesHeader.visible } Label { objectName: "notopsites" anchors { top: topSitesDivider.bottom left: parent.left right: parent.right } visible: !internal.seeMoreBookmarksView && (topSitesModel.count == 0) height: visible ? units.gu(11) : 0 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter text: i18n.tr("You haven't visited any site yet") color: UbuntuColors.darkGrey } FocusScope { id: topSitesGrid anchors { top: topSitesDivider.bottom left: parent.left right: parent.right } visible: !internal.seeMoreBookmarksView && (topSitesModel.count > 0) height: visible ? grid.contentHeight + units.gu(1) : 0 clip: true UrlPreviewGrid { id: grid objectName: "topSitesList" focus: true anchors { left: parent.left leftMargin: units.gu(2) right: parent.right rightMargin: units.gu(2) top: parent.top topMargin: units.gu(2) bottom: parent.bottom } horizontalMargin: units.gu(1) verticalMargin: units.gu(1) opacity: internal.seeMoreBookmarksView ? 0.0 : 1.0 Behavior on opacity { UbuntuNumberAnimation {} } visible: opacity > 0 interactive: false model: LimitProxyModel { limit: 10 sourceModel: topSitesModel } showFavicons: false onActivated: newTabView.historyEntryClicked(url) onRemoved: { HistoryModel.hide(url) PreviewManager.checkDelete(url) } // Setting 'interactive' to false to prevent flicks also disables // keyboard navigation, so it needs to be manually implemented. Keys.onLeftPressed: moveCurrentIndexLeft() Keys.onRightPressed: moveCurrentIndexRight() onCurrentItemChanged: internal.ensureCurrentItemVisible(topSitesGrid, currentItem) onActiveFocusChanged: internal.ensureCurrentItemVisible(topSitesGrid, currentItem) onCountChanged: { if (activeFocus && (count == 0)) { bookmarkList.focus = true } } } Keys.onUpPressed: bookmarkList.focus = true } } } } ./src/app/webbrowser/NewPrivateTabView.qml0000644000004100000410000000323413004613604021053 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 Item { id: newPrivateTabView objectName: "newPrivateTabView" Icon { anchors { horizontalCenter: parent.horizontalCenter bottom: titleLabel.top } width: units.gu(10) height: width name: "private-browsing" } Label { id: titleLabel anchors.centerIn: parent text: i18n.tr("This is a private tab") color: UbuntuColors.darkGrey fontSize: "medium" } Label { anchors { horizontalCenter: parent.horizontalCenter top: titleLabel.bottom topMargin: units.gu(5) } width: units.gu(25) wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter text: i18n.tr("Pages that you view in this tab won't appear in your browser history.\nBookmarks you create will be preserved, however.") color: UbuntuColors.darkGrey fontSize: "x-small" } } ./src/app/webbrowser/ListViewHighlight.qml0000644000004100000410000000220713004613604021102 0ustar www-datawww-data/* * Copyright 2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import ".." Rectangle { color: "transparent" border { width: units.gu(0.18) color: UbuntuColors.orange } radius: units.gu(0.3) visible: hasKeyboard && ((ListView.view && ListView.view.activeFocus) || (GridView.view && GridView.view.activeFocus)) readonly property bool hasKeyboard: keyboardModel.count > 0 FilteredKeyboardModel { id: keyboardModel } } ./src/app/webbrowser/webbrowser-app.cpp0000644000004100000410000001172613004613623020443 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "bookmarks-model.h" #include "bookmarks-folderlist-model.h" #include "cache-deleter.h" #include "config.h" #include "downloads-model.h" #include "file-operations.h" #include "history-domainlist-model.h" #include "history-lastvisitdatelist-model.h" #include "history-model.h" #include "limit-proxy-model.h" #include "searchengine.h" #include "text-search-filter-model.h" #include "tabs-model.h" #include "webbrowser-app.h" // Qt #include #include #include #include #include #include #include #include #include WebbrowserApp::WebbrowserApp(int& argc, char** argv) : BrowserApplication(argc, argv) { } #define MAKE_SINGLETON_FACTORY(type) \ static QObject* type##_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine) { \ Q_UNUSED(engine); \ Q_UNUSED(scriptEngine); \ return new type(); \ } MAKE_SINGLETON_FACTORY(FileOperations) MAKE_SINGLETON_FACTORY(CacheDeleter) MAKE_SINGLETON_FACTORY(BookmarksModel) MAKE_SINGLETON_FACTORY(HistoryModel) MAKE_SINGLETON_FACTORY(DownloadsModel) bool WebbrowserApp::initialize() { const char* uri = "webbrowserapp.private"; qmlRegisterSingletonType(uri, 0, 1, "HistoryModel", HistoryModel_singleton_factory); qmlRegisterType(uri, 0, 1, "HistoryDomainListModel"); qmlRegisterType(uri, 0, 1, "HistoryLastVisitDateListModel"); qmlRegisterType(uri, 0 , 1, "LimitProxyModel"); qmlRegisterType(uri, 0, 1, "TabsModel"); qmlRegisterSingletonType(uri, 0, 1, "BookmarksModel", BookmarksModel_singleton_factory); qmlRegisterType(uri, 0, 1, "BookmarksFolderListModel"); qmlRegisterSingletonType(uri, 0, 1, "FileOperations", FileOperations_singleton_factory); qmlRegisterType(uri, 0, 1, "SearchEngine"); qmlRegisterSingletonType(uri, 0, 1, "CacheDeleter", CacheDeleter_singleton_factory); qmlRegisterSingletonType(uri, 0, 1, "DownloadsModel", DownloadsModel_singleton_factory); qmlRegisterType(uri, 0, 1, "TextSearchFilterModel"); if (BrowserApplication::initialize("webbrowser/webbrowser-app.qml", QStringLiteral("webbrowser-app"))) { QStringList searchEnginesSearchPaths; searchEnginesSearchPaths << QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/searchengines"; searchEnginesSearchPaths << UbuntuBrowserDirectory() + "/webbrowser/searchengines"; m_engine->rootContext()->setContextProperty("searchEnginesSearchPaths", searchEnginesSearchPaths); m_engine->rootContext()->setContextProperty("__platformName", platformName()); m_window->setProperty("newSession", m_arguments.contains("--new-session")); QVariantList urls; Q_FOREACH(const QUrl& url, this->urls()) { urls.append(url); } m_window->setProperty("urls", urls); m_component->completeCreate(); return true; } else { return false; } } void WebbrowserApp::printUsage() const { QTextStream out(stdout); QString command = QFileInfo(QCoreApplication::applicationFilePath()).fileName(); out << "Usage: " << command << " [-h|--help] [--fullscreen] [--maximized] [--inspector]" << " [--app-id=APP_ID] [--new-session] [URL]" << endl; out << "Options:" << endl; out << " -h, --help display this help message and exit" << endl; out << " --fullscreen display full screen" << endl; out << " --maximized opens the application maximized" << endl; out << " --inspector[=PORT] run a remote inspector on a specified port or " << REMOTE_INSPECTOR_PORT << " as the default port" << endl; out << " --app-id=APP_ID run the application with a specific APP_ID" << endl; out << " --new-session do not restore open tabs from the last session" << endl; } int main(int argc, char** argv) { QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); WebbrowserApp app(argc, argv); if (app.initialize()) { return app.run(); } else { return 0; } } ./src/app/webbrowser/Toolbar.qml0000644000004100000410000000322513004613604017107 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 Rectangle { id: toolbar Image { anchors { left: parent.left right: parent.right bottom: parent.top } source: "assets/toolbar-dropshadow.png" fillMode: Image.TileHorizontally } states: [ State { name: "hidden" PropertyChanges { target: toolbar y: toolbar.parent.height } }, State { name: "shown" PropertyChanges { target: toolbar y: toolbar.parent.height - toolbar.height } } ] state: "shown" readonly property bool isFullyShown: y == (parent.height - height) Behavior on y { UbuntuNumberAnimation { duration: UbuntuAnimation.BriskDuration } } MouseArea { anchors.fill: parent // do not propagate click events to items below } } ./src/app/webbrowser/history-domainlist-model.h0000644000004100000410000000436613004613604022112 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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_DOMAINLIST_MODEL_H__ #define __HISTORY_DOMAINLIST_MODEL_H__ // Qt #include #include #include class HistoryDomainModel; class HistoryModel; class HistoryDomainListModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(HistoryModel* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged) Q_ENUMS(Roles) public: HistoryDomainListModel(QObject* parent=0); ~HistoryDomainListModel(); enum Roles { Domain = Qt::UserRole + 1, LastVisit, LastVisitDate, LastVisitedTitle, LastVisitedIcon, Entries }; // reimplemented from QAbstractListModel QHash roleNames() const; int rowCount(const QModelIndex& parent=QModelIndex()) const; QVariant data(const QModelIndex& index, int role) const; HistoryModel* sourceModel() const; void setSourceModel(HistoryModel* sourceModel); Q_SIGNALS: void sourceModelChanged() const; private Q_SLOTS: void onRowsInserted(const QModelIndex& parent, int start, int end); void onModelReset(); void onDomainRowsRemoved(const QModelIndex& parent, int start, int end); void onDomainDataChanged(); private: HistoryModel* m_sourceModel; QMap m_domains; void clearDomains(); void populateModel(); void insertNewDomain(const QString& domain); QString getDomainFromSourceModel(const QModelIndex& index) const; void emitDataChanged(const QString& domain); }; #endif // __HISTORY_DOMAINLIST_MODEL_H__ ./src/app/webbrowser/ContentPickerDialog.qml0000644000004100000410000001005313004613604021372 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 as Popups import Ubuntu.Content 1.3 import com.canonical.Oxide 1.8 import "../MimeTypeMapper.js" as MimeTypeMapper import ".." Component { Popups.PopupBase { id: picker objectName: "contentPickerDialog" // Set the parent at construction time, instead of letting show() // set it later on, which for some reason results in the size of // the dialog not being updated. parent: QuickUtils.rootItem(this) property var activeTransfer Rectangle { anchors.fill: parent ContentTransferHint { anchors.fill: parent activeTransfer: picker.activeTransfer } ContentPeerPicker { id: peerPicker anchors.fill: parent visible: true contentType: ContentType.All handler: ContentHandler.Source onPeerSelected: { if (peer.appId == "webbrowser-app") { // If we're inside the browser and the user has // requested content from the browser then we // need to handle the transfer internally var downloadsPage = picker.WebView.view.showDownloadsPage() downloadsPage.mimetypeFilter = MimeTypeMapper.mimeTypeRegexForContentType(contentType) downloadsPage.multiSelect = model.allowMultipleFiles downloadsPage.selectMode = false downloadsPage.pickingMode = true downloadsPage.internalFilePicker = model Popups.PopupUtils.close(picker) } else { if (model.allowMultipleFiles) { peer.selectionType = ContentTransfer.Multiple } else { peer.selectionType = ContentTransfer.Single } picker.activeTransfer = peer.request() stateChangeConnection.target = picker.activeTransfer } } onCancelPressed: { model.reject() } } } Connections { id: stateChangeConnection target: null onStateChanged: { if (picker.activeTransfer.state === ContentTransfer.Charged) { var selectedItems = [] for(var i in picker.activeTransfer.items) { selectedItems.push(String(picker.activeTransfer.items[i].url).replace("file://", "")) } model.accept(selectedItems) } } } Component.onCompleted: { if(acceptTypes.length === 1) { var contentType = MimeTypeMapper.mimeTypeToContentType(acceptTypes[0]) if(contentType == ContentType.Unknown) { // If we don't recognise the type, allow uploads from any app contentType = ContentType.All } peerPicker.contentType = contentType } else { peerPicker.contentType = ContentType.All } show() } } } ./src/app/webbrowser/searchengine.cpp0000644000004100000410000001030613004613604020127 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ // local #include "searchengine.h" // Qt #include #include #include SearchEngine::SearchEngine(QObject* parent) : QObject(parent) {} const QStringList& SearchEngine::searchPaths() const { return m_searchPaths; } void SearchEngine::setSearchPaths(const QStringList& searchPaths) { if (searchPaths != m_searchPaths) { m_searchPaths = searchPaths; Q_EMIT searchPathsChanged(); locateAndParseDescription(); } } const QString& SearchEngine::filename() const { return m_filename; } void SearchEngine::setFilename(const QString& filename) { if (filename != m_filename) { m_filename = filename; Q_EMIT filenameChanged(); locateAndParseDescription(); } } const QString& SearchEngine::name() const { return m_name; } const QString& SearchEngine::description() const { return m_description; } const QString& SearchEngine::urlTemplate() const { return m_template; } const QString& SearchEngine::suggestionsUrlTemplate() const { return m_suggestionsTemplate; } bool SearchEngine::isValid() const { return !m_searchPaths.isEmpty() && !m_filename.isEmpty() && !m_name.isEmpty() && !m_template.isEmpty(); } void SearchEngine::locateAndParseDescription() { QString filepath; if (!m_filename.isEmpty()) { Q_FOREACH(const QString& path, m_searchPaths) { QDir dir(path); QString filename = m_filename + ".xml"; if (dir.exists(filename)) { filepath = dir.filePath(filename); break; } } } QString oldName = m_name; m_name.clear(); QString oldDescription = m_description; m_description.clear(); QString oldTemplate = m_template; m_template.clear(); QString oldSuggestionsTemplate = m_suggestionsTemplate; m_suggestionsTemplate.clear(); bool wasValid = isValid(); if (!filepath.isEmpty()) { QFile file(filepath); if (file.open(QIODevice::ReadOnly)) { // Parse OpenSearch description file // (http://www.opensearch.org/Specifications/OpenSearch/1.1) QXmlStreamReader parser(&file); while (!parser.atEnd()) { parser.readNext(); if (parser.isStartElement()) { QStringRef name = parser.name(); if (name == "ShortName") { m_name = parser.readElementText(); } else if (name == "Description") { m_description = parser.readElementText(); } else if (name == "Url") { QStringRef type = parser.attributes().value("type"); if (type == "text/html") { m_template = parser.attributes().value("template").toString(); } else if (type == "application/x-suggestions+json") { m_suggestionsTemplate = parser.attributes().value("template").toString(); } } } } } } if (m_name != oldName) { Q_EMIT nameChanged(); } if (m_description != oldDescription) { Q_EMIT descriptionChanged(); } if (m_template != oldTemplate) { Q_EMIT urlTemplateChanged(); } if (m_suggestionsTemplate != oldSuggestionsTemplate) { Q_EMIT suggestionsUrlTemplateChanged(); } if (isValid() != wasValid) { Q_EMIT validChanged(); } } ./src/app/webbrowser/history-model.cpp0000644000004100000410000004231213004613604020275 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "domain-utils.h" #include "history-model.h" // Qt #include #include #define CONNECTION_NAME "webbrowser-app-history" /*! \class HistoryModel \brief List model that stores information about navigation history. HistoryModel is a list model that stores history entries that contain metadata about navigation history. For a given URL, the following information is stored: domain name, page title, URL to the favorite icon if any, total number of visits, and timestamp of the most recent visit (UTC). The model is sorted chronologically at all times (most recent visit first). The information is persistently stored on disk in a SQLite database. The database is read at startup to populate the model, and whenever a new entry is added to the model the database is updated. However the model doesn’t monitor the database for external changes. */ HistoryModel::HistoryModel(QObject* parent) : QAbstractListModel(parent) { m_database = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), CONNECTION_NAME); } HistoryModel::~HistoryModel() { m_database.close(); m_database = QSqlDatabase(); QSqlDatabase::removeDatabase(CONNECTION_NAME); } void HistoryModel::resetDatabase(const QString& databaseName) { beginResetModel(); m_hiddenEntries.clear(); m_entries.clear(); m_database.close(); m_database.setDatabaseName(databaseName); m_database.open(); createOrAlterDatabaseSchema(); endResetModel(); populateFromDatabase(); } void HistoryModel::createOrAlterDatabaseSchema() { QMutexLocker ml(&m_dbMutex); QSqlQuery createQuery(m_database); QString query = QLatin1String("CREATE TABLE IF NOT EXISTS history " "(url VARCHAR, domain VARCHAR, title VARCHAR," " icon VARCHAR, visits INTEGER, lastVisit DATETIME);"); createQuery.prepare(query); createQuery.exec(); // The first version of the database schema didn’t have a 'domain' column QSqlQuery tableInfoQuery(m_database); query = QLatin1String("PRAGMA TABLE_INFO(history);"); tableInfoQuery.prepare(query); tableInfoQuery.exec(); while (tableInfoQuery.next()) { if (tableInfoQuery.value("name").toString() == "domain") { break; } } if (!tableInfoQuery.isValid()) { QSqlQuery addDomainColumnQuery(m_database); query = QLatin1String("ALTER TABLE history ADD COLUMN domain VARCHAR;"); addDomainColumnQuery.prepare(query); addDomainColumnQuery.exec(); // Updating all the entries in the database to add the domain is a // costly operation that would slow down the application startup, // do not do it here. } QSqlQuery createHiddenQuery(m_database); query = QLatin1String("CREATE TABLE IF NOT EXISTS history_hidden (url VARCHAR);"); createHiddenQuery.prepare(query); createHiddenQuery.exec(); } void HistoryModel::populateFromDatabase() { QSqlQuery populateQuery(m_database); QString query = QLatin1String("SELECT url, domain, title, icon, visits, lastVisit " "FROM history ORDER BY lastVisit DESC;"); populateQuery.prepare(query); populateQuery.exec(); QSqlQuery populateHiddenQuery(m_database); query = QLatin1String("SELECT url FROM history_hidden;"); populateHiddenQuery.prepare(query); populateHiddenQuery.exec(); while (populateHiddenQuery.next()) { m_hiddenEntries.append(populateHiddenQuery.value(0).toUrl()); } int count = 0; while (populateQuery.next()) { HistoryEntry entry; entry.url = populateQuery.value(0).toUrl(); entry.domain = populateQuery.value(1).toString(); if (entry.domain.isEmpty()) { entry.domain = DomainUtils::extractTopLevelDomainName(entry.url); } entry.title = populateQuery.value(2).toString(); entry.icon = populateQuery.value(3).toUrl(); entry.visits = populateQuery.value(4).toInt(); entry.lastVisit = QDateTime::fromTime_t(populateQuery.value(5).toInt()); entry.hidden = m_hiddenEntries.contains(entry.url); beginInsertRows(QModelIndex(), count, count); m_entries.append(entry); endInsertRows(); ++count; } } QHash HistoryModel::roleNames() const { static QHash roles; if (roles.isEmpty()) { roles[Url] = "url"; roles[Domain] = "domain"; roles[Title] = "title"; roles[Icon] = "icon"; roles[Visits] = "visits"; roles[LastVisit] = "lastVisit"; roles[LastVisitDate] = "lastVisitDate"; roles[LastVisitDateString] = "lastVisitDateString"; roles[Hidden] = "hidden"; } return roles; } int HistoryModel::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); return m_entries.count(); } QVariant HistoryModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } const HistoryEntry& entry = m_entries.at(index.row()); switch (role) { case Url: return entry.url; case Domain: return entry.domain; case Title: return entry.title; case Icon: return entry.icon; case Visits: return entry.visits; case LastVisit: return entry.lastVisit; case LastVisitDate: return entry.lastVisit.toLocalTime().date(); case LastVisitDateString: return entry.lastVisit.toLocalTime().date().toString(Qt::ISODate); case Hidden: return entry.hidden; default: return QVariant(); } } const QString HistoryModel::databasePath() const { return m_database.databaseName(); } void HistoryModel::setDatabasePath(const QString& path) { if (path != databasePath()) { if (path.isEmpty()) { resetDatabase(":memory:"); } else { resetDatabase(path); } Q_EMIT databasePathChanged(); } } int HistoryModel::getEntryIndex(const QUrl& url) const { for (int i = 0; i < m_entries.count(); ++i) { if (m_entries.at(i).url == url) { return i; } } return -1; } /*! Add an entry to the model. If an entry with the same URL already exists, it is updated. Otherwise a new entry is created and added to the model. Return the total number of visits for the URL. */ int HistoryModel::add(const QUrl& url, const QString& title, const QUrl& icon) { if (url.isEmpty()) { return 0; } int count = 1; QDateTime now = QDateTime::currentDateTimeUtc(); int index = getEntryIndex(url); if (index == -1) { HistoryEntry entry; entry.url = url; entry.domain = DomainUtils::extractTopLevelDomainName(url); entry.title = title; entry.icon = icon; entry.visits = 1; entry.lastVisit = now; entry.hidden = m_hiddenEntries.contains(entry.url); beginInsertRows(QModelIndex(), 0, 0); m_entries.prepend(entry); endInsertRows(); insertNewEntryInDatabase(entry); Q_EMIT rowCountChanged(); } else { QVector roles; roles << Visits; if (index == 0) { HistoryEntry& entry = m_entries.first(); if (title != entry.title) { entry.title = title; roles << Title; } if (icon != entry.icon) { entry.icon = icon; roles << Icon; } count = ++entry.visits; if (now != entry.lastVisit) { if (now.date() != entry.lastVisit.date()) { roles << LastVisitDate; roles << LastVisitDateString; } entry.lastVisit = now; roles << LastVisit; } } else { beginMoveRows(QModelIndex(), index, index, QModelIndex(), 0); HistoryEntry entry = m_entries.takeAt(index); if (title != entry.title) { entry.title = title; roles << Title; } if (icon != entry.icon) { entry.icon = icon; roles << Icon; } count = ++entry.visits; if (now != entry.lastVisit) { if (now.date() != entry.lastVisit.date()) { roles << LastVisitDate; roles << LastVisitDateString; } entry.lastVisit = now; roles << LastVisit; } m_entries.prepend(entry); endMoveRows(); } Q_EMIT dataChanged(this->index(0, 0), this->index(0, 0), roles); updateExistingEntryInDatabase(m_entries.first()); } return count; } /*! Update an existing entry in the model. If no entry with the same URL exists yet, do nothing (and return false). Otherwise the title and icon of the existing entry are updated (the number of visits remains unchanged). Return true if an update actually happened, false otherwise. */ bool HistoryModel::update(const QUrl& url, const QString& title, const QUrl& icon) { if (url.isEmpty()) { return false; } int index = getEntryIndex(url); if (index == -1) { return false; } QVector roles; const HistoryEntry& entry = m_entries.at(index); if (title != entry.title) { m_entries[index].title = title; roles << Title; } if (icon != entry.icon) { m_entries[index].icon = icon; roles << Icon; } if (roles.isEmpty()) { return false; } Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), roles); updateExistingEntryInDatabase(entry); return true; } /*! Remove a given URL from the history model. If the URL was not previously visited, do nothing. */ void HistoryModel::removeEntryByUrl(const QUrl& url) { if (url.isEmpty()) { return; } removeByIndex(getEntryIndex(url)); removeEntryFromDatabaseByUrl(url); Q_EMIT rowCountChanged(); } /*! Remove all urls last visited in a given DATE from the history model. */ void HistoryModel::removeEntriesByDate(const QDate& date) { if (!date.isValid()) { return; } for (int i = m_entries.count() - 1; i >= 0; --i) { if (m_entries.at(i).lastVisit.toLocalTime().date() == date) { removeByIndex(i); } } removeEntriesFromDatabaseByDate(date); Q_EMIT rowCountChanged(); } /*! Remove all urls from a given DOMAIN from the history model. */ void HistoryModel::removeEntriesByDomain(const QString& domain) { if (domain.isEmpty()) { return; } for (int i = m_entries.count() - 1; i >= 0; --i) { if (m_entries.at(i).domain == domain) { removeByIndex(i); } } removeEntriesFromDatabaseByDomain(domain); Q_EMIT rowCountChanged(); } void HistoryModel::removeByIndex(int index) { if (index >= 0) { beginRemoveRows(QModelIndex(), index, index); m_entries.removeAt(index); endRemoveRows(); } } void HistoryModel::insertNewEntryInDatabase(const HistoryEntry& entry) { QMutexLocker ml(&m_dbMutex); QSqlQuery query(m_database); static QString insertStatement = QLatin1String("INSERT INTO history (url, domain, title, icon, " "visits, lastVisit) VALUES (?, ?, ?, ?, 1, ?);"); query.prepare(insertStatement); query.addBindValue(entry.url.toString()); query.addBindValue(entry.domain); query.addBindValue(entry.title); query.addBindValue(entry.icon.toString()); query.addBindValue(entry.lastVisit.toTime_t()); query.exec(); } void HistoryModel::insertNewEntryInHiddenDatabase(const QUrl& url) { QMutexLocker ml(&m_dbMutex); QSqlQuery query(m_database); static QString insertStatement = QLatin1String("INSERT INTO history_hidden (url) VALUES (?);"); query.prepare(insertStatement); query.addBindValue(url.toString()); query.exec(); } void HistoryModel::updateExistingEntryInDatabase(const HistoryEntry& entry) { QMutexLocker ml(&m_dbMutex); QSqlQuery query(m_database); static QString updateStatement = QLatin1String("UPDATE history SET domain=?, title=?, icon=?, " "visits=?, lastVisit=? WHERE url=?;"); query.prepare(updateStatement); query.addBindValue(entry.domain); query.addBindValue(entry.title); query.addBindValue(entry.icon.toString()); query.addBindValue(entry.visits); query.addBindValue(entry.lastVisit.toTime_t()); query.addBindValue(entry.url.toString()); query.exec(); } void HistoryModel::removeEntryFromDatabaseByUrl(const QUrl& url) { QMutexLocker ml(&m_dbMutex); QSqlQuery query(m_database); static QString deleteStatement = QLatin1String("DELETE FROM history WHERE url=?;"); query.prepare(deleteStatement); query.addBindValue(url.toString()); query.exec(); } void HistoryModel::removeEntryFromHiddenDatabaseByUrl(const QUrl& url) { QMutexLocker ml(&m_dbMutex); QSqlQuery query(m_database); static QString deleteStatement = QLatin1String("DELETE FROM history_hidden WHERE url=?;"); query.prepare(deleteStatement); query.addBindValue(url.toString()); query.exec(); } void HistoryModel::removeEntriesFromDatabaseByDate(const QDate& date) { QMutexLocker ml(&m_dbMutex); QSqlQuery query(m_database); static QString deleteStatement = QLatin1String("DELETE FROM history WHERE lastVisit BETWEEN ? AND ?;"); query.prepare(deleteStatement); QDateTime dateTime = QDateTime(date); query.addBindValue(dateTime.toTime_t()); dateTime.setTime(QTime(23, 59, 59, 999)); query.addBindValue(dateTime.toTime_t()); query.exec(); } void HistoryModel::removeEntriesFromDatabaseByDomain(const QString& domain) { QMutexLocker ml(&m_dbMutex); QSqlQuery query(m_database); static QString deleteStatement = QLatin1String("DELETE FROM history WHERE domain=?;"); query.prepare(deleteStatement); query.addBindValue(domain); query.exec(); } void HistoryModel::clearAll() { if (!m_entries.isEmpty()) { beginResetModel(); m_hiddenEntries.clear(); m_entries.clear(); endResetModel(); clearDatabase(); Q_EMIT rowCountChanged(); } } void HistoryModel::clearDatabase() { QMutexLocker ml(&m_dbMutex); QSqlQuery deleteQuery(m_database); QString deleteStatement = QLatin1String("DELETE FROM history;"); deleteQuery.prepare(deleteStatement); deleteQuery.exec(); QSqlQuery deleteHiddenQuery(m_database); deleteStatement = QLatin1String("DELETE FROM history_hidden;"); deleteHiddenQuery.prepare(deleteStatement); deleteHiddenQuery.exec(); } /*! Mark an entry in the model as hidden. Add a new entry to the hidden list. If an entry with the URL exists, it is updated. */ void HistoryModel::hide(const QUrl& url) { if (url.isEmpty() || m_hiddenEntries.contains(url)) { return; } m_hiddenEntries.append(url); QVector roles; roles << Hidden; for (int i = 0; i < m_entries.count(); ++i) { HistoryEntry& entry = m_entries[i]; if (entry.url == url) { entry.hidden = true; Q_EMIT dataChanged(this->index(i, 0), this->index(i, 0), roles); } } insertNewEntryInHiddenDatabase(url); } /*! Mark an entry in the model as not hidden. If an entry with the URL exists on the hidden entries, it is removed. If an entry with the URL exists, it is updated. */ void HistoryModel::unHide(const QUrl& url) { if (url.isEmpty() || !m_hiddenEntries.contains(url)) { return; } m_hiddenEntries.removeAll(url); QVector roles; roles << Hidden; for (int i = 0; i < m_entries.count(); ++i) { HistoryEntry& entry = m_entries[i]; if (entry.url == url) { entry.hidden = false; Q_EMIT dataChanged(this->index(i, 0), this->index(i, 0), roles); } } removeEntryFromHiddenDatabaseByUrl(url); } QVariantMap HistoryModel::get(int i) const { QVariantMap item; QHash roles = roleNames(); QModelIndex modelIndex = index(i, 0); if (modelIndex.isValid()) { Q_FOREACH(int role, roles.keys()) { QString roleName = QString::fromUtf8(roles.value(role)); item.insert(roleName, data(modelIndex, role)); } } return item; } ./src/app/webbrowser/HistorySectionDelegate.qml0000644000004100000410000000400013004613604022116 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 as ListItem Item { height: units.gu(5.5) property string todaySectionTitle: i18n.tr("Last Visited") Label { anchors { left: parent.left right: parent.right top: parent.top topMargin: units.gu(1.5) } height: units.gu(2) text: { var today = new Date() var yesterday = new Date() yesterday.setDate(yesterday.getDate() - 1) var sectionDate = new Date(section) if ((sectionDate.getUTCFullYear() == today.getFullYear()) && (sectionDate.getUTCMonth() == today.getMonth())) { var dayDifference = sectionDate.getUTCDate() - today.getDate() if (dayDifference == 0) { return todaySectionTitle } else if (dayDifference == -1) { return i18n.tr("Yesterday") } } return Qt.formatDate(section, Qt.DefaultLocaleLongDate) } fontSize: "small" color: UbuntuColors.darkGrey } ListItem.ThinDivider { anchors { left: parent.left right: parent.right bottom: parent.bottom bottomMargin: units.gu(1) } } } ./src/app/webbrowser/SettingsDeviceSelector.qml0000644000004100000410000000434213004613604022127 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import com.canonical.Oxide 1.9 Item { property bool isAudio readonly property int devicesCount: internal.devices.length property alias enabled: selector.enabled property string defaultDevice signal deviceSelected(string id) implicitHeight: selector.height + units.gu(1) OptionSelector { id: selector anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: units.gu(1) containerHeight: itemHeight * model.length model: internal.devices delegate: OptionSelectorDelegate { text: modelData.displayName || i18n.tr("Default") } onDelegateClicked: deviceSelected(model[index].id) } QtObject { id: internal property var devices: isAudio ? Oxide.availableAudioCaptureDevices : Oxide.availableVideoCaptureDevices function updateDefaultDevice() { for (var i = 0; i < devices.length; i++) { if (defaultDevice === devices[i].id) { selector.selectedIndex = i return } } } } onDefaultDeviceChanged: internal.updateDefaultDevice() Connections { target: Oxide onAvailableAudioCaptureDevicesChanged: if (isAudio) internal.updateDefaultDevice() onAvailableVideoCaptureDevicesChanged: if (!isAudio) internal.updateDefaultDevice() } onIsAudioChanged: internal.updateDefaultDevice() } ./src/app/webbrowser/bookmarks-model.h0000644000004100000410000000572613004613604020241 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __BOOKMARKS_MODEL_H__ #define __BOOKMARKS_MODEL_H__ // Qt #include #include #include #include #include #include #include class BookmarksModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QString databasePath READ databasePath WRITE setDatabasePath NOTIFY databasePathChanged) Q_PROPERTY(int count READ rowCount NOTIFY rowCountChanged) Q_ENUMS(Roles) public: BookmarksModel(QObject* parent=0); ~BookmarksModel(); enum Roles { Url = Qt::UserRole + 1, Title, Icon, Created, Folder }; // reimplemented from QAbstractListModel QHash roleNames() const; int rowCount(const QModelIndex& parent=QModelIndex()) const; QVariant data(const QModelIndex& index, int role) const; const QString databasePath() const; void setDatabasePath(const QString& path); QStringList folders() const; int addFolder(const QString& folder); Q_INVOKABLE bool contains(const QUrl& url) const; Q_INVOKABLE void add(const QUrl& url, const QString& title, const QUrl& icon, const QString& folder); Q_INVOKABLE void remove(const QUrl& url); Q_INVOKABLE void update(const QUrl& url, const QString& title, const QString& folder); Q_SIGNALS: void databasePathChanged() const; void folderAdded(const QString& folder) const; void added(const QUrl& url) const; void removed(const QUrl& url) const; void rowCountChanged(); private: QSqlDatabase m_database; struct BookmarkEntry { QUrl url; QString title; QUrl icon; QDateTime created; int folderId; QString folder; }; QHash m_folders; QSet m_urls; QList m_orderedEntries; void resetDatabase(const QString& databaseName); void createOrAlterDatabaseSchema(); void populateFromDatabase(); void insertNewEntryInDatabase(const BookmarkEntry& entry); void removeExistingEntryFromDatabase(const QUrl& url); void updateExistingEntryInDatabase(const BookmarkEntry& entry); int getFolderId(const QString& folder); int insertNewFolderInDatabase(const QString& folder); }; #endif // __BOOKMARKS_MODEL_H__ ./src/app/webbrowser/history-domain-model.h0000644000004100000410000000440213004613604021205 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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_DOMAIN_MODEL_H__ #define __HISTORY_DOMAIN_MODEL_H__ // Qt #include #include #include #include class HistoryModel; class HistoryDomainModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(HistoryModel* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged) Q_PROPERTY(QString domain READ domain WRITE setDomain NOTIFY domainChanged) Q_PROPERTY(QDateTime lastVisit READ lastVisit NOTIFY lastVisitChanged) Q_PROPERTY(QString lastVisitedTitle READ lastVisitedTitle NOTIFY lastVisitedTitleChanged) Q_PROPERTY(QUrl lastVisitedIcon READ lastVisitedIcon NOTIFY lastVisitedIconChanged) public: HistoryDomainModel(QObject* parent=0); HistoryModel* sourceModel() const; void setSourceModel(HistoryModel* sourceModel); const QString& domain() const; void setDomain(const QString& domain); const QDateTime& lastVisit() const; const QString& lastVisitedTitle() const; const QUrl& lastVisitedIcon() const; Q_SIGNALS: void sourceModelChanged() const; void domainChanged() const; void lastVisitChanged() const; void lastVisitedTitleChanged() const; void lastVisitedIconChanged() const; protected: // reimplemented from QSortFilterProxyModel bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; private: QString m_domain; QDateTime m_lastVisit; QString m_lastVisitedTitle; QUrl m_lastVisitedIcon; private Q_SLOTS: void onModelChanged(); }; #endif // __HISTORY_DOMAIN_MODEL_H__ ./src/app/webbrowser/PreviewManager.qml0000644000004100000410000000562013004613604020422 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ pragma Singleton import QtQuick 2.4 import Ubuntu.Web 0.2 import webbrowserapp.private 0.1 Item { property string capturesDir: cacheLocation + "/captures" signal previewSaved(url pageUrl, url previewUrl) LimitProxyModel { id: topSites limit: 10 sourceModel: TopSitesModel { model: HistoryModel } function contains(url) { for (var i = 0; i < topSites.count; i++) { if (topSites.get(i).url == url) return true } return false } function containsHash(hash) { for (var i = 0; i < topSites.count; i++) { if (Qt.md5(topSites.get(i).url) == hash) return true } return false } } function previewPathFromUrl(url) { return "%1/%2.png".arg(capturesDir).arg(Qt.md5(url)) } function saveToDisk(data, url) { if (!FileOperations.exists(Qt.resolvedUrl(capturesDir))) { FileOperations.mkpath(Qt.resolvedUrl(capturesDir)) } var filepath = previewPathFromUrl(url) var previewUrl = "" if (data.saveToFile(filepath)) previewUrl = Qt.resolvedUrl(filepath) else console.warn("Failed to save preview to disk for %1 (path is %2)".arg(url).arg(filepath)) previewSaved(url, previewUrl) } function checkDelete(url) { if (!topSites.contains(url)) { FileOperations.remove(Qt.resolvedUrl(previewPathFromUrl(url))) } } // Remove all previews stored on disk that are not part of the top sites // and that are not for URLs in the doNotCleanUrls list function cleanUnusedPreviews(doNotCleanUrls) { var dir = Qt.resolvedUrl(capturesDir) var previews = FileOperations.filesInDirectory(dir, ["*.png", "*.jpg"]) var doNotCleanHashes = doNotCleanUrls.map(function(url) { return Qt.md5(url) }) for (var i in previews) { var preview = previews[i] var hash = preview.split('.')[0] if (!topSites.containsHash(hash) && (doNotCleanHashes.indexOf(hash) === -1)) { var file = Qt.resolvedUrl("%1/%2".arg(capturesDir).arg(preview)) FileOperations.remove(file) } } } } ./src/app/webbrowser/webbrowser-app-content-hub.json0000644000004100000410000000005013004613604023041 0ustar www-datawww-data{ "source": [ "all" ] } ./src/app/webbrowser/BrowserPageHeader.qml0000644000004100000410000000603013004613604021033 0ustar www-datawww-data /* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 as ListItem /* * Component to use as page header in settings page, download page and * subpages * * It has a back() signal fired when back button is pressed, a text * property to set the page title and an actions property which * displays action icons on the right of header. */ Item { id: root signal back() property string text property alias actions: actionBar.actions property alias color: title.color height: title.height + divider.height anchors { left: parent.left right: parent.right } MouseArea { // Prevent click events from propagating through to the view below anchors.fill: parent acceptedButtons: Qt.AllButtons } Rectangle { id: title height: units.gu(6) - divider.height anchors { left: parent.left; right: parent.right } color: "#f6f6f6" AbstractButton { id: backButton objectName: "backButton" width: height activeFocusOnPress: false onTriggered: root.back() anchors { top: parent.top bottom: parent.bottom left: parent.left } Rectangle { anchors.fill: parent anchors.leftMargin: units.gu(1) anchors.rightMargin: units.gu(1) color: "#E6E6E6" visible: parent.pressed } Icon { name: "back" anchors { fill: parent topMargin: units.gu(2) bottomMargin: units.gu(2) } } } Label { anchors { left: backButton.right verticalCenter: parent.verticalCenter } text: root.text fontSize: 'x-large' } ActionBar { id: actionBar anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter } } Rectangle { id: divider anchors { left: parent.left right: parent.right bottom: parent.bottom } height: units.dp(1) color: Qt.darker(title.color, 1.1) } } ./src/app/webbrowser/Thumbnailer.qml0000644000004100000410000000162213004613604017756 0ustar www-datawww-data/* * Copyright 2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQml 2.0 import Ubuntu.Thumbnailer 0.1 /* Because Thumbnailer isn't in the main repository the webbrowser can't depend on it. We use this file to dynamically load the thumbnailer image provider if it's installed. */ QtObject { } ./src/app/webbrowser/limit-proxy-model.h0000644000004100000410000000425113004613604020536 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __LIMIT_PROXY_MODEL_H__ #define __LIMIT_PROXY_MODEL_H__ // Qt #include class LimitProxyModel : public QIdentityProxyModel { Q_OBJECT Q_PROPERTY(QObject* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged) Q_PROPERTY(int limit READ limit WRITE setLimit NOTIFY limitChanged) Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(int unlimitedCount READ unlimitedRowCount NOTIFY unlimitedCountChanged) public: LimitProxyModel(QObject* parent=0); QAbstractItemModel* sourceModel() const; void setSourceModel(QObject* sourceModel); int limit() const; void setLimit(int limit); int rowCount(const QModelIndex &parent = QModelIndex()) const; int unlimitedRowCount(const QModelIndex &parent = QModelIndex()) const; Q_INVOKABLE QVariantMap get(int index) const; Q_SIGNALS: void sourceModelChanged() const; void limitChanged() const; void unlimitedCountChanged(); void countChanged(); private Q_SLOTS: void sourceRowsAboutToBeInserted(const QModelIndex &parent, int start, int end); void sourceRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); void sourceRowsInserted(const QModelIndex &parent, int start, int end); void sourceRowsRemoved(const QModelIndex &parent, int start, int end); private: int m_limit; bool m_sourceInserting; bool m_sourceRemoving; int m_dataChangedBegin; int m_dataChangedEnd; }; #endif // __LIMIT_PROXY_MODEL_H__ ./src/app/webbrowser/cache-deleter.h0000644000004100000410000000241513004613604017630 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __CACHE_DELETER_H__ #define __CACHE_DELETER_H__ #include #include #include #include class QString; class CacheDeleter : public QObject { Q_OBJECT public: explicit CacheDeleter(QObject* parent=0); Q_INVOKABLE void clear(const QString& cachePath, const QJSValue& callback=QJSValue::UndefinedValue); private: void doClear(const QString& cachePath); private Q_SLOTS: void onCleared(); private: QMutex m_mutex; QFutureWatcher m_clearWatcher; QJSValue m_callback; }; #endif // __CACHE_DELETER_H__ ./src/app/webbrowser/BookmarksModelUtils.js0000644000004100000410000000165013004613604021262 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /* Prepend the homepage bookmark item to bookmarks model */ function prependHomepageToBookmarks(model, homeEntry) { var items = [] items.push(homeEntry) for (var i = 0; i < model.count; i++) { items.push(model.get(i)) } return items } ./src/app/webbrowser/history-model.h0000644000004100000410000000650613004613604017747 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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_MODEL_H__ #define __HISTORY_MODEL_H__ // Qt #include #include #include #include #include #include #include class HistoryModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QString databasePath READ databasePath WRITE setDatabasePath NOTIFY databasePathChanged) Q_PROPERTY(int count READ rowCount NOTIFY rowCountChanged) Q_ENUMS(Roles) public: HistoryModel(QObject* parent=0); ~HistoryModel(); enum Roles { Url = Qt::UserRole + 1, Domain, Title, Icon, Visits, LastVisit, LastVisitDate, LastVisitDateString, Hidden, }; // reimplemented from QAbstractListModel QHash roleNames() const; int rowCount(const QModelIndex& parent=QModelIndex()) const; QVariant data(const QModelIndex& index, int role) const; const QString databasePath() const; void setDatabasePath(const QString& path); Q_INVOKABLE int add(const QUrl& url, const QString& title, const QUrl& icon); Q_INVOKABLE bool update(const QUrl& url, const QString& title, const QUrl& icon); Q_INVOKABLE void removeEntryByUrl(const QUrl& url); Q_INVOKABLE void removeEntriesByDate(const QDate& date); Q_INVOKABLE void removeEntriesByDomain(const QString& domain); Q_INVOKABLE void clearAll(); Q_INVOKABLE void hide(const QUrl& url); Q_INVOKABLE void unHide(const QUrl& url); Q_INVOKABLE QVariantMap get(int index) const; Q_SIGNALS: void databasePathChanged() const; void rowCountChanged(); protected: struct HistoryEntry { QUrl url; QString domain; QString title; QUrl icon; uint visits; QDateTime lastVisit; bool hidden; }; QList m_entries; int getEntryIndex(const QUrl& url) const; void updateExistingEntryInDatabase(const HistoryEntry& entry); private: QMutex m_dbMutex; QSqlDatabase m_database; QList m_hiddenEntries; void resetDatabase(const QString& databaseName); void createOrAlterDatabaseSchema(); void populateFromDatabase(); void removeByIndex(int index); void insertNewEntryInDatabase(const HistoryEntry& entry); void insertNewEntryInHiddenDatabase(const QUrl& url); void removeEntryFromDatabaseByUrl(const QUrl& url); void removeEntryFromHiddenDatabaseByUrl(const QUrl& url); void removeEntriesFromDatabaseByDate(const QDate& date); void removeEntriesFromDatabaseByDomain(const QString& domain); void clearDatabase(); }; #endif // __HISTORY_MODEL_H__ ./src/app/webbrowser/TabChrome.qml0000644000004100000410000000410713004613604017351 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 Item { id: tabChrome property alias title: tabItem.title property alias icon: tabItem.icon property alias incognito: tabItem.incognito property alias tabWidth: tabItem.width signal selected() signal closed() height: units.gu(4) Item { anchors { left: parent.left right: parent.right bottom: parent.bottom } height: units.gu(5) clip: true BorderImage { // We are basically splitting the shadow asset in two parts. // The left side is never scaled and it stays fixed below the // tab itself (with 4dp of the shadow poking out at the sides). // The right side will scale across the remaining width of the // component (which is empty and lets the previous preview show // through) border { left: tabWidth + units.dp(4) } anchors.fill: parent anchors.bottomMargin: - units.gu(3) height: units.gu(8) source: "assets/tab-shadow-narrow.png" } } TabItem { id: tabItem anchors.top: parent.top anchors.bottom: parent.bottom active: true hoverable: false fgColor: "#111111" onSelected: tabChrome.selected() onClosed: tabChrome.closed() } } ./src/app/webbrowser/downloads-model.cpp0000644000004100000410000003250413004613604020570 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "downloads-model.h" #include #include #include #include #include #include #include #include #define CONNECTION_NAME "webbrowser-app-downloads" /*! \class DownloadsModel \brief List model that stores information about downloaded files. DownloadsModel is a list model that stores information about files that have been downloaded by the browser and stored permanently (e.g. in ~/Downloads), as opposed to those that were sent directly to another application after download. For each download the original URL, the path to the downloaded file, the file mimetype and the download time are stored. The model is sorted chronologically to display the most recent download first. The information is persistently stored on disk in a SQLite database. The database is read at startup to populate the model, and whenever a new entry is added to the model or an entry is removed from the model the database is updated. Removing a download from the model also results in it being deleted from the disk. The model doesn’t monitor the database for external changes, but does check that downloaded files still exist when first populating. */ DownloadsModel::DownloadsModel(QObject* parent) : QAbstractListModel(parent) , m_numRows(0) , m_fetchedCount(0) , m_canFetchMore(true) { m_database = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), CONNECTION_NAME); } DownloadsModel::~DownloadsModel() { m_database.close(); m_database = QSqlDatabase(); QSqlDatabase::removeDatabase(CONNECTION_NAME); } void DownloadsModel::resetDatabase(const QString& databaseName) { beginResetModel(); m_orderedEntries.clear(); m_database.close(); m_database.setDatabaseName(databaseName); m_database.open(); m_numRows = 0; m_fetchedCount = 0; m_canFetchMore = true; createOrAlterDatabaseSchema(); endResetModel(); Q_EMIT rowCountChanged(); } void DownloadsModel::createOrAlterDatabaseSchema() { QSqlQuery createQuery(m_database); QString query = QLatin1String("CREATE TABLE IF NOT EXISTS downloads " "(downloadId VARCHAR, url VARCHAR, path VARCHAR, " "mimetype VARCHAR, complete BOOL, paused BOOL, " "error VARCHAR, created DATETIME DEFAULT " "CURRENT_TIMESTAMP);"); createQuery.prepare(query); createQuery.exec(); } void DownloadsModel::fetchMore(const QModelIndex &parent) { QSqlQuery populateQuery(m_database); QString query = QLatin1String("SELECT downloadId, url, path, mimetype, " "complete, error, created, paused " "FROM downloads ORDER BY created DESC LIMIT 100 OFFSET ?;"); populateQuery.prepare(query); populateQuery.addBindValue(m_fetchedCount); populateQuery.exec(); int count = 0; // size() isn't supported on the sqlite backend while (populateQuery.next()) { DownloadEntry entry; entry.downloadId = populateQuery.value(0).toString(); entry.url = populateQuery.value(1).toUrl(); entry.path = populateQuery.value(2).toString(); entry.mimetype = populateQuery.value(3).toString(); entry.complete = populateQuery.value(4).toBool(); entry.error = populateQuery.value(5).toString(); entry.created = QDateTime::fromTime_t(populateQuery.value(6).toInt()); entry.paused = populateQuery.value(7).toBool(); QFileInfo fileInfo(entry.path); if (fileInfo.exists()) { entry.filename = fileInfo.fileName(); } // Only list a completed entry if its file exists, however we don't // remove the entry if the file is missing as it may be stored on a // removable medium like an SD card in the future, so could reappear. if (!entry.complete || fileInfo.exists()) { beginInsertRows(QModelIndex(), m_numRows, m_numRows); m_orderedEntries.append(entry); endInsertRows(); m_numRows++; } count++; } m_fetchedCount += count; if (count == 0) { m_canFetchMore = false; } } QHash DownloadsModel::roleNames() const { static QHash roles; if (roles.isEmpty()) { roles[DownloadId] = "downloadId"; roles[Url] = "url"; roles[Path] = "path"; roles[Filename] = "filename"; roles[Mimetype] = "mimetype"; roles[Complete] = "complete"; roles[Paused] = "paused"; roles[Error] = "error"; roles[Created] = "created"; } return roles; } int DownloadsModel::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); return m_orderedEntries.count(); } QVariant DownloadsModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } const DownloadEntry& entry = m_orderedEntries.at(index.row()); switch (role) { case DownloadId: return entry.downloadId; case Url: return entry.url; case Path: return entry.path; case Filename: return entry.filename; case Mimetype: return entry.mimetype; case Complete: return entry.complete; case Paused: return entry.paused; case Error: return entry.error; case Created: return entry.created; default: return QVariant(); } } const QString DownloadsModel::databasePath() const { return m_database.databaseName(); } void DownloadsModel::setDatabasePath(const QString& path) { if (path != databasePath()) { if (path.isEmpty()) { resetDatabase(":memory:"); } else { resetDatabase(path); } Q_EMIT databasePathChanged(); } } bool DownloadsModel::contains(const QString& downloadId) const { Q_FOREACH(const DownloadEntry& entry, m_orderedEntries) { if (entry.downloadId == downloadId) { return true; } } return false; } /*! Add a download to the database. This should happen as soon as the download is started. */ void DownloadsModel::add(const QString& downloadId, const QUrl& url, const QString& mimetype) { beginInsertRows(QModelIndex(), 0, 0); DownloadEntry entry; entry.downloadId = downloadId; entry.complete = false; entry.paused = false; entry.url = url; entry.mimetype = mimetype; m_orderedEntries.prepend(entry); m_numRows++; m_fetchedCount++; endInsertRows(); Q_EMIT added(downloadId, url, mimetype); insertNewEntryInDatabase(entry); Q_EMIT rowCountChanged(); } void DownloadsModel::setPath(const QString& downloadId, const QString& path) { QSqlQuery query(m_database); // Override reported mimetype from server with detected mimetype from file once downloaded QMimeDatabase mimeDatabase; QString mimetype = mimeDatabase.mimeTypeForFile(path).name(); static QString updateStatement = QLatin1String("UPDATE downloads SET mimetype = ?, " "path = ? WHERE downloadId = ?"); query.prepare(updateStatement); query.addBindValue(mimetype); query.addBindValue(path); query.addBindValue(downloadId); query.exec(); Q_EMIT pathChanged(downloadId, path); } void DownloadsModel::setComplete(const QString& downloadId, const bool complete) { QSqlQuery query(m_database); static QString updateStatement = QLatin1String("UPDATE downloads SET complete = ? " "WHERE downloadId = ?"); query.prepare(updateStatement); query.addBindValue(complete); query.addBindValue(downloadId); query.exec(); Q_EMIT completeChanged(downloadId, complete); reload(); } void DownloadsModel::setError(const QString& downloadId, const QString& error) { QSqlQuery query(m_database); static QString updateStatement = QLatin1String("UPDATE downloads SET error = ? " "WHERE downloadId = ?"); query.prepare(updateStatement); query.addBindValue(error); query.addBindValue(downloadId); query.exec(); Q_EMIT errorChanged(downloadId, error); reload(); } void DownloadsModel::moveToDownloads(const QString& downloadId, const QString& path) { QFile file(path); if (file.exists()) { QFileInfo fi(path); QString suffix = fi.completeSuffix(); QString filename = fi.fileName(); QString filenameWithoutSuffix = filename.left(filename.size() - suffix.size()); QString dir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); QString destination = dir + QDir::separator() + filenameWithoutSuffix + suffix; // Avoid filename collision by automatically inserting an incremented // number into the filename if the original name already exists. int append = 1; while (QFile::exists(destination)) { destination = QString("%1%2.%3").arg(dir + QDir::separator() + filenameWithoutSuffix, QString::number(append), suffix); ++append; } if (file.rename(destination)) { setPath(downloadId, destination); } else { qWarning() << "Failed moving file from" << path << "to" << destination; } } else { qWarning() << "Download not found:" << path; } } void DownloadsModel::insertNewEntryInDatabase(const DownloadEntry& entry) { QSqlQuery query(m_database); static QString insertStatement = QLatin1String("INSERT INTO downloads (downloadId, url, " "mimetype) " "VALUES (?, ?, ?);"); query.prepare(insertStatement); query.addBindValue(entry.downloadId); query.addBindValue(entry.url); query.addBindValue(entry.mimetype); query.exec(); } /*! Remove a downloaded file from the list of downloads and delete the file. */ void DownloadsModel::deleteDownload(const QString& path) { int index = 0; Q_FOREACH(DownloadEntry entry, m_orderedEntries) { if (entry.path == path) { beginRemoveRows(QModelIndex(), index, index); m_orderedEntries.removeAt(index); endRemoveRows(); Q_EMIT deleted(path); removeExistingEntryFromDatabase(path); m_fetchedCount--; m_numRows--; Q_EMIT rowCountChanged(); QFile::remove(path); return; } else { index++; } } } /*! Remove a cancelled download from the model and the database. */ void DownloadsModel::cancelDownload(const QString& downloadId) { int index=0; Q_FOREACH(DownloadEntry entry, m_orderedEntries) { if (entry.downloadId == downloadId) { beginRemoveRows(QModelIndex(), index, index); m_orderedEntries.removeAt(index); QSqlQuery query(m_database); static QString deleteStatement = QLatin1String("DELETE FROM downloads WHERE downloadId=?;"); query.prepare(deleteStatement); query.addBindValue(downloadId); query.exec(); endRemoveRows(); m_fetchedCount--; m_numRows--; Q_EMIT rowCountChanged(); return; } else { index++; } } } void DownloadsModel::pauseDownload(const QString& downloadId) { QSqlQuery query(m_database); static QString pauseStatement = QLatin1String("UPDATE downloads SET paused=1 WHERE downloadId=?;"); query.prepare(pauseStatement); query.addBindValue(downloadId); query.exec(); reload(); } void DownloadsModel::resumeDownload(const QString& downloadId) { QSqlQuery query(m_database); static QString resumeStatement = QLatin1String("UPDATE downloads SET paused=0 WHERE downloadId=?;"); query.prepare(resumeStatement); query.addBindValue(downloadId); query.exec(); reload(); } void DownloadsModel::removeExistingEntryFromDatabase(const QString& path) { QSqlQuery query(m_database); static QString deleteStatement = QLatin1String("DELETE FROM downloads WHERE path=?;"); query.prepare(deleteStatement); query.addBindValue(path); query.exec(); } bool DownloadsModel::canFetchMore(const QModelIndex &parent) const { Q_UNUSED(parent) return m_canFetchMore; } void DownloadsModel::reload() { beginResetModel(); m_orderedEntries.clear(); m_canFetchMore = true; m_fetchedCount = 0; m_numRows = 0; endResetModel(); fetchMore(); Q_EMIT rowCountChanged(); } ./src/app/webbrowser/BookmarksViewWide.qml0000644000004100000410000000622413004613604021103 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 as ListItems import webbrowserapp.private 0.1 import "." as Local FocusScope { id: bookmarksViewWide property alias homepageUrl: bookmarksFoldersViewWide.homeBookmarkUrl signal bookmarkEntryClicked(url url) signal done() signal newTabClicked() Rectangle { anchors.fill: parent color: "#f6f6f6" } BookmarksFoldersViewWide { id: bookmarksFoldersViewWide anchors { top: topBar.bottom left: parent.left right: parent.right bottom: toolbar.top } focus: true onBookmarkClicked: bookmarksViewWide.bookmarkEntryClicked(url) onBookmarkRemoved: { if (BookmarksModel.count == 1) { done() } BookmarksModel.remove(url) } } Local.Toolbar { id: topBar height: units.gu(7) color: "#f7f7f7" anchors { left: parent.left right: parent.right top: parent.top } Label { anchors { top: parent.top left: parent.left topMargin: units.gu(2) leftMargin: units.gu(2) } text: i18n.tr("Bookmarks") } ListItems.ThinDivider { anchors { left: parent.left right: parent.right bottom: parent.bottom } } } Local.Toolbar { id: toolbar height: units.gu(7) anchors { left: parent.left right: parent.right bottom: parent.bottom } Button { objectName: "doneButton" anchors { left: parent.left leftMargin: units.gu(2) verticalCenter: parent.verticalCenter } strokeColor: UbuntuColors.darkGrey text: i18n.tr("Done") onClicked: bookmarksViewWide.done() } ToolbarAction { objectName: "newTabAction" anchors { right: parent.right rightMargin: units.gu(2) verticalCenter: parent.verticalCenter } height: parent.height - units.gu(2) text: i18n.tr("New tab") iconName: "tab-new" onClicked: bookmarksViewWide.newTabClicked() } } } ./src/app/webbrowser/history-domainlist-model.cpp0000644000004100000410000001620713004613604022442 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "history-domain-model.h" #include "history-domainlist-model.h" #include "history-model.h" // Qt #include /*! \class HistoryDomainListModel \brief List model that exposes history entries grouped by domain name HistoryDomainListModel is a list model that exposes history entries from a HistoryModel grouped by domain name. Each item in the list has three roles: 'domain' for the domain name, 'lastVisit' for the timestamp of the last page visited in this domain, and 'entries' for the corresponding HistoryDomainModel that contains all entries in this group. */ HistoryDomainListModel::HistoryDomainListModel(QObject* parent) : QAbstractListModel(parent) , m_sourceModel(0) { } HistoryDomainListModel::~HistoryDomainListModel() { clearDomains(); } QHash HistoryDomainListModel::roleNames() const { static QHash roles; if (roles.isEmpty()) { roles[Domain] = "domain"; roles[LastVisit] = "lastVisit"; roles[LastVisitDate] = "lastVisitDate"; roles[LastVisitedTitle] = "lastVisitedTitle"; roles[LastVisitedIcon] = "lastVisitedIcon"; roles[Entries] = "entries"; } return roles; } int HistoryDomainListModel::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); return m_domains.count(); } QVariant HistoryDomainListModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } const QString domain = m_domains.keys().at(index.row()); HistoryDomainModel* entries = m_domains.value(domain); switch (role) { case Domain: return domain; case LastVisit: return entries->lastVisit(); case LastVisitDate: return entries->lastVisit().toLocalTime().date(); case LastVisitedTitle: return entries->lastVisitedTitle(); case LastVisitedIcon: return entries->lastVisitedIcon(); case Entries: return QVariant::fromValue(entries); default: return QVariant(); } } HistoryModel* HistoryDomainListModel::sourceModel() const { return m_sourceModel; } void HistoryDomainListModel::setSourceModel(HistoryModel* sourceModel) { if (sourceModel != m_sourceModel) { beginResetModel(); if (m_sourceModel != 0) { m_sourceModel->disconnect(this); } clearDomains(); m_sourceModel = sourceModel; populateModel(); if (m_sourceModel != 0) { connect(m_sourceModel, SIGNAL(rowsInserted(const QModelIndex&, int, int)), SLOT(onRowsInserted(const QModelIndex&, int, int))); connect(m_sourceModel, SIGNAL(modelReset()), SLOT(onModelReset())); connect(m_sourceModel, SIGNAL(layoutChanged(QList, QAbstractItemModel::LayoutChangeHint)), SLOT(onModelReset())); } endResetModel(); Q_EMIT sourceModelChanged(); } } void HistoryDomainListModel::clearDomains() { Q_FOREACH(const QString& domain, m_domains.keys()) { delete m_domains.take(domain); } } void HistoryDomainListModel::populateModel() { if (m_sourceModel != 0) { int count = m_sourceModel->rowCount(); for (int i = 0; i < count; ++i) { QString domain = getDomainFromSourceModel(m_sourceModel->index(i, 0)); if (!m_domains.contains(domain)) { insertNewDomain(domain); } } } } void HistoryDomainListModel::onRowsInserted(const QModelIndex& parent, int start, int end) { for (int i = start; i <= end; ++i) { QString domain = getDomainFromSourceModel(m_sourceModel->index(i, 0, parent)); if (!m_domains.contains(domain)) { QStringList domains = m_domains.keys(); int insertAt = 0; while (insertAt < domains.count()) { if (domain.compare(domains.at(insertAt)) < 0) { break; } ++insertAt; } beginInsertRows(QModelIndex(), insertAt, insertAt); insertNewDomain(domain); endInsertRows(); } } } void HistoryDomainListModel::onModelReset() { beginResetModel(); clearDomains(); populateModel(); endResetModel(); } void HistoryDomainListModel::insertNewDomain(const QString& domain) { HistoryDomainModel* model = new HistoryDomainModel(this); model->setSourceModel(m_sourceModel); model->setDomain(domain); connect(model, SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(onDomainDataChanged())); connect(model, SIGNAL(rowsRemoved(QModelIndex, int, int)), SLOT(onDomainRowsRemoved(QModelIndex, int, int))); connect(model, SIGNAL(rowsMoved(QModelIndex, int, int, QModelIndex, int)), SLOT(onDomainDataChanged())); connect(model, SIGNAL(layoutChanged(QList, QAbstractItemModel::LayoutChangeHint)), SLOT(onDomainDataChanged())); connect(model, SIGNAL(dataChanged(QModelIndex, QModelIndex)), SLOT(onDomainDataChanged())); connect(model, SIGNAL(modelReset()), SLOT(onDomainDataChanged())); connect(model, SIGNAL(lastVisitChanged()), SLOT(onDomainDataChanged())); m_domains.insert(domain, model); } QString HistoryDomainListModel::getDomainFromSourceModel(const QModelIndex& index) const { return m_sourceModel->data(index, HistoryModel::Domain).toString(); } void HistoryDomainListModel::onDomainRowsRemoved(const QModelIndex& parent, int start, int end) { Q_UNUSED(parent); Q_UNUSED(start); Q_UNUSED(end); HistoryDomainModel* model = qobject_cast(sender()); if (model != 0) { const QString& domain = model->domain(); if (model->rowCount() == 0) { int removeAt = m_domains.keys().indexOf(domain); beginRemoveRows(QModelIndex(), removeAt, removeAt); delete m_domains.take(domain); endRemoveRows(); } else { emitDataChanged(domain); } } } void HistoryDomainListModel::onDomainDataChanged() { HistoryDomainModel* model = qobject_cast(sender()); if (model != 0) { emitDataChanged(model->domain()); } } void HistoryDomainListModel::emitDataChanged(const QString& domain) { int i = m_domains.keys().indexOf(domain); if (i != -1) { QModelIndex index = this->index(i, 0); Q_EMIT dataChanged(index, index, QVector() << LastVisit << LastVisitDate << LastVisitedTitle << LastVisitedIcon << Entries); } } ./src/app/webbrowser/Suggestion.qml0000644000004100000410000000504213004613604017633 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 as ListItem // Not using ListItem.Subtitled because it’s not themable, // and we want the subText to be on one line only. ListItem.Base { property alias title: label.text property alias subtitle: subLabel.text property alias icon: icon.name property url url signal activated(url url) __height: Math.max(middleVisuals.height, units.gu(6)) // disable focus handling activeFocusOnPress: false Item { id: middleVisuals anchors { left: parent.left right: parent.right verticalCenter: parent.verticalCenter } height: subLabel.visible ? label.height + subLabel.height : icon.height Icon { id: icon anchors { verticalCenter: parent.verticalCenter left: parent.left } width: units.gu(2) height: units.gu(2) color: UbuntuColors.darkGrey } Label { id: label anchors { top: subLabel.visible ? parent.top : undefined verticalCenter: subLabel.visible ? undefined : parent.verticalCenter left: icon.right leftMargin: units.gu(2) right: parent.right } color: selected ? "#DB4923" : UbuntuColors.darkGrey elide: Text.ElideRight } Label { id: subLabel anchors { top: label.bottom left: icon.right leftMargin: units.gu(2) right: parent.right } fontSize: "small" elide: Text.ElideRight visible: text !== "" color: selected ? "#DB4923" : UbuntuColors.darkGrey } } onClicked: activated(url) } ./src/app/webbrowser/BookmarksView.qml0000644000004100000410000000627413004613604020277 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 as ListItems import webbrowserapp.private 0.1 import "." as Local FocusScope { id: bookmarksView property alias homepageUrl: bookmarksFoldersView.homeBookmarkUrl signal bookmarkEntryClicked(url url) signal done() signal newTabClicked() Rectangle { anchors.fill: parent color: "#f6f6f6" } BookmarksFoldersView { id: bookmarksFoldersView anchors { top: topBar.bottom left: parent.left right: parent.right bottom: toolbar.top } interactive: true focus: true onBookmarkClicked: bookmarksView.bookmarkEntryClicked(url) onBookmarkRemoved: { if (BookmarksModel.count == 1) { done() } BookmarksModel.remove(url) } } Local.Toolbar { id: topBar height: units.gu(7) color: "#f7f7f7" anchors { left: parent.left right: parent.right top: parent.top } Label { anchors { top: parent.top left: parent.left topMargin: units.gu(2) leftMargin: units.gu(2) } text: i18n.tr("Bookmarks") } ListItems.ThinDivider { anchors { left: parent.left right: parent.right bottom: parent.bottom } } } Local.Toolbar { id: toolbar height: units.gu(7) anchors { left: parent.left right: parent.right bottom: parent.bottom } Button { objectName: "doneButton" anchors { left: parent.left leftMargin: units.gu(2) verticalCenter: parent.verticalCenter } activeFocusOnPress: false strokeColor: UbuntuColors.darkGrey text: i18n.tr("Done") onClicked: bookmarksView.done() } ToolbarAction { objectName: "newTabAction" anchors { right: parent.right rightMargin: units.gu(2) verticalCenter: parent.verticalCenter } height: parent.height - units.gu(2) text: i18n.tr("New tab") iconName: "tab-new" onClicked: bookmarksView.newTabClicked() } } } ./src/app/webbrowser/webbrowser-app.desktop.in.in0000644000004100000410000000116713004613604022341 0ustar www-datawww-data[Desktop Entry] Version=1.0 _Name=Browser _GenericName=Web Browser _Comment=Browse the World Wide Web _Keywords=Internet;WWW;Browser;Web;Explorer Type=Application Icon=@CMAKE_INSTALL_FULL_DATADIR@/webbrowser-app/webbrowser-app.png Exec=webbrowser-app %u Terminal=false Categories=Network;WebBrowser; MimeType=text/html;text/xml;application/xhtml+xml;x-scheme-handler/http;x-scheme-handler/https; X-Ubuntu-Touch=true X-Ubuntu-Gettext-Domain=webbrowser-app X-Ubuntu-Single-Instance=true X-Ubuntu-Default-Department-ID=web-browsers X-Screenshot=@CMAKE_INSTALL_FULL_DATADIR@/webbrowser-app/screenshot.png X-Ubuntu-Splash-Color=#FFFFFF ./src/app/webbrowser/HistoryViewWide.qml0000644000004100000410000004335113004613604020616 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 as ListItems import webbrowserapp.private 0.1 import "Highlight.js" as Highlight import "." as Local FocusScope { id: historyViewWide property bool searchMode: false readonly property bool selectMode: urlsListView.ViewItems.selectMode onSearchModeChanged: { if (searchMode) searchQuery.focus = true else { searchQuery.text = "" urlsListView.focus = true } } signal done() signal historyEntryClicked(url url) signal newTabRequested() Keys.onLeftPressed: lastVisitDateListView.forceActiveFocus() Keys.onRightPressed: urlsListView.forceActiveFocus() Keys.onUpPressed: if (searchMode) searchQuery.focus = true Keys.onPressed: { if (event.modifiers === Qt.ControlModifier && event.key === Qt.Key_F) { if (searchMode) searchQuery.focus = true else if (!selectMode) searchMode = true event.accepted = true } } Keys.onDeletePressed: { if (urlsListView.ViewItems.selectMode) { internal.removeSelected() } else { if (urlsListView.activeFocus) { HistoryModel.removeEntryByUrl(urlsListView.currentItem.siteUrl) if (urlsListView.count == 0) { lastVisitDateListView.currentIndex = 0 } } else { if (lastVisitDateListView.currentIndex == 0) { HistoryModel.clearAll() } else { HistoryModel.removeEntriesByDate(lastVisitDateListView.currentItem.lastVisitDate) lastVisitDateListView.currentIndex = 0 } } } } onActiveFocusChanged: { if (activeFocus) { urlsListView.forceActiveFocus() } } Rectangle { anchors.fill: parent } Timer { // Set the model asynchronously to ensure // the view is displayed as early as possible. id: loadModelTimer interval: 1 onTriggered: historySearchModel.sourceModel = HistoryModel } function loadModel() { loadModelTimer.restart() } TextSearchFilterModel { id: historySearchModel searchFields: ["title", "url"] terms: searchQuery.terms } Row { id: historyViewWideRow anchors { top: topBar.bottom left: parent.left bottom: bottomToolbar.top leftMargin: units.gu(2) rightMargin: units.gu(2) } spacing: units.gu(1) FocusScope { width: units.gu(40) height: parent.height ListView { id: lastVisitDateListView objectName: "lastVisitDateListView" anchors.fill: parent focus: true currentIndex: 0 onCurrentIndexChanged: urlsListView.ViewItems.selectedIndices = [] model: HistoryLastVisitDateListModel { sourceModel: historyLastVisitDateModel.model } delegate: ListItem { id: lastVisitDateDelegate objectName: "lastVisitDateDelegate" property var lastVisitDate: model.lastVisitDate anchors { left: parent.left right: parent.right rightMargin: units.gu(1) } width: parent.width height: units.gu(4) Label { objectName: "lastVisitDateDelegateLabel" anchors { top: parent.top left: parent.left topMargin: units.gu(1) leftMargin: units.gu(2) } height: parent.height text: { if (!lastVisitDate.isValid()) { return i18n.tr("All History") } var today = new Date() today.setHours(0, 0, 0, 0) var yesterday = new Date() yesterday.setDate(yesterday.getDate() - 1) yesterday.setHours(0, 0, 0, 0) var entryDate = new Date(lastVisitDate) entryDate.setHours(0, 0, 0, 0) if (entryDate.getTime() == today.getTime()) { return i18n.tr("Today") } else if (entryDate.getTime() == yesterday.getTime()) { return i18n.tr("Yesterday") } return Qt.formatDate(lastVisitDate, Qt.DefaultLocaleLongDate) } fontSize: "small" color: (!lastVisitDateListView.activeFocus && lastVisitDateDelegate.ListView.isCurrentItem) ? UbuntuColors.orange : UbuntuColors.darkGrey } divider { // Hide the divider so that the highlight doesn’t overlap it // Do not set visible to false, otherwise the content item is resized. opacity: (!ListView.view.activeFocus || (index > ListView.view.currentIndex) || (index < (ListView.view.currentIndex - 1))) ? 1 : 0 Behavior on opacity { UbuntuNumberAnimation {} } } onClicked: ListView.view.currentIndex = index ListView.onRemove: { if (ListView.isCurrentItem) { // For some reason, setting the current index here // results in it being reset to its previous value // right away. Delaying it with a timer so the // operation is queued does the trick. resetIndexTimer.restart() } } } highlight: ListViewHighlight {} Timer { id: resetIndexTimer interval: 0 onTriggered: lastVisitDateListView.currentIndex = 0 } } Keys.onUpPressed: { if (searchMode) { searchQuery.focus = true } else { event.accepted = false } } Scrollbar { flickableItem: lastVisitDateListView align: Qt.AlignTrailing } } Item { width: historyViewWide.width - lastVisitDateListView.width - historyViewWideRow.spacing - units.gu(4) height: parent.height ListView { id: urlsListView objectName: "urlsListView" anchors.fill: parent Keys.onReturnPressed: historyEntrySelected() Keys.onEnterPressed: historyEntrySelected() model: SortFilterModel { id: historyLastVisitDateModel readonly property date lastVisitDate: lastVisitDateListView.currentItem ? lastVisitDateListView.currentItem.lastVisitDate : "" filter { property: "lastVisitDateString" pattern: new RegExp(lastVisitDate.isValid() ? "^%1$".arg(Qt.formatDate(lastVisitDate, "yyyy-MM-dd")) : "") } // Until a valid HistoryModel is assigned the TextSearchFilterModel // will not report role names, and the HistoryLastVisitDateListModel // will emit warnings since it needs a dateLastVisit role to be // present. model: historySearchModel.sourceModel ? historySearchModel : null } clip: true onModelChanged: urlsListView.currentIndex = -1 onActiveFocusChanged: { if (!activeFocus) { urlsListView.currentIndex = -1 } else { urlsListView.currentIndex = 0 } } function historyEntrySelected() { if (urlsListView.ViewItems.selectMode) { currentItem.selected = !currentItem.selected } else { historyViewWide.historyEntryClicked(currentItem.siteUrl) } } // Only use sections for "All History" history list section.property: historyLastVisitDateModel.lastVisitDate.isValid() ? "" : "lastVisitDate" section.delegate: HistorySectionDelegate { width: parent.width - units.gu(3) anchors.left: parent.left anchors.leftMargin: units.gu(2) todaySectionTitle: i18n.tr("Today") } delegate: UrlDelegate{ objectName: "historyDelegate" width: parent.width - units.gu(1) height: units.gu(5) property url siteUrl: model.url icon: model.icon title: Highlight.highlightTerms(model.title ? model.title : model.url, searchQuery.terms) url: Highlight.highlightTerms(model.url, searchQuery.terms) headerComponent: Label { text: Qt.formatTime(model.lastVisit) fontSize: "xx-small" } onClicked: { if (selectMode) { selected = !selected } else { historyViewWide.historyEntryClicked(model.url) } } onRemoved: { HistoryModel.removeEntryByUrl(model.url) if (urlsListView.count == 0) { lastVisitDateListView.currentIndex = 0 } } onPressAndHold: { if (historyViewWide.searchMode) return selectMode = !selectMode if (selectMode) { urlsListView.ViewItems.selectedIndices = [index] } } } highlight: ListViewHighlight {} } Scrollbar { flickableItem: urlsListView align: Qt.AlignTrailing } } } Local.Toolbar { id: topBar height: units.gu(7) color: "#f7f7f7" anchors { left: parent.left right: parent.right top: parent.top } Keys.onEscapePressed: { if (searchQuery.activeFocus) { historyViewWide.searchMode = false } else { event.accepted = false } } Label { visible: !urlsListView.ViewItems.selectMode && !historyViewWide.searchMode anchors { top: parent.top left: parent.left topMargin: units.gu(2) leftMargin: units.gu(2) } text: i18n.tr("History") } ToolbarAction { objectName: "backButton" visible: historyViewWide.selectMode || historyViewWide.searchMode anchors { top: parent.top left: parent.left leftMargin: units.gu(2) } height: parent.height - units.gu(2) iconName: "back" text: i18n.tr("Cancel") onClicked: { if (historyViewWide.searchMode) { historyViewWide.searchMode = false } else { urlsListView.ViewItems.selectMode = false } lastVisitDateListView.forceActiveFocus() } } ToolbarAction { objectName: "selectButton" visible: urlsListView.ViewItems.selectMode anchors { top: parent.top right: deleteButton.left rightMargin: units.gu(2) } height: parent.height - units.gu(2) iconName: "select" text: i18n.tr("Select all") onClicked: internal.toggleSelectAll() } ToolbarAction { id: deleteButton objectName: "deleteButton" visible: urlsListView.ViewItems.selectMode anchors { top: parent.top right: parent.right rightMargin: units.gu(2) } height: parent.height - units.gu(2) iconName: "delete" text: i18n.tr("Delete") enabled: urlsListView.ViewItems.selectedIndices.length > 0 onClicked: internal.removeSelected() } TextField { id: searchQuery objectName: "searchQuery" anchors { verticalCenter: parent.verticalCenter right: parent.right rightMargin: units.gu(2) } width: urlsListView.width inputMethodHints: Qt.ImhNoPredictiveText primaryItem: Icon { height: parent.height - units.gu(2) width: height name: "search" } hasClearButton: true placeholderText: i18n.tr("search history") visible: historyViewWide.searchMode readonly property var terms: text.split(/\s+/g).filter(function(term) { return term.length > 0 }) Keys.onDownPressed: urlsListView.focus = true } ToolbarAction { id: searchButton iconName: "search" objectName: "searchButton" visible: !urlsListView.ViewItems.selectMode && !historyViewWide.searchMode anchors { verticalCenter: parent.verticalCenter right: parent.right rightMargin: units.gu(3.5) } height: parent.height - units.gu(2) onClicked: { historyViewWide.searchMode = true searchQuery.forceActiveFocus() } } ListItems.ThinDivider { anchors { left: parent.left right: parent.right bottom: parent.bottom } } } Local.Toolbar { id: bottomToolbar height: units.gu(7) anchors { left: parent.left right: parent.right bottom: parent.bottom } Button { objectName: "doneButton" anchors { left: parent.left leftMargin: units.gu(2) verticalCenter: parent.verticalCenter } strokeColor: UbuntuColors.darkGrey text: i18n.tr("Done") onClicked: historyViewWide.done() } ToolbarAction { objectName: "newTabButton" anchors { right: parent.right rightMargin: units.gu(2) verticalCenter: parent.verticalCenter } height: parent.height - units.gu(2) text: i18n.tr("New tab") iconName: "tab-new" onClicked: { historyViewWide.newTabRequested() historyViewWide.done() } } } QtObject { id: internal function toggleSelectAll() { if (urlsListView.ViewItems.selectedIndices.length === urlsListView.count) { urlsListView.ViewItems.selectedIndices = [] } else { var indices = [] for (var i = 0; i < urlsListView.count; ++i) { indices.push(i) } urlsListView.ViewItems.selectedIndices = indices } urlsListView.forceActiveFocus() } function removeSelected() { var indices = urlsListView.ViewItems.selectedIndices var urls = [] for (var i in indices) { urls.push(urlsListView.model.get(indices[i])["url"]) } if (urlsListView.count == urls.length) { lastVisitDateListView.currentIndex = 0 } urlsListView.ViewItems.selectMode = false for (var j in urls) { HistoryModel.removeEntryByUrl(urls[j]) } lastVisitDateListView.forceActiveFocus() } } } ./src/app/webbrowser/ContentDownloadDialog.qml0000644000004100000410000001327613004613604021736 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import Ubuntu.Content 1.3 import webbrowsercommon.private 0.1 import ".." Component { PopupBase { id: downloadDialog objectName: "downloadDialog" anchors.fill: parent property var activeTransfer property string downloadId property var singleDownload property string mimeType property string filename property string icon: MimeDatabase.iconForMimetype(mimeType) property alias contentType: peerPicker.contentType signal startDownload(string downloadId, var download, string mimeType) Component { id: downloadOptionsComponent Dialog { id: downloadOptionsDialog objectName: "downloadOptionsDialog" Column { spacing: units.gu(2) Item { width: parent.width height: mimetypeIcon.height Icon { id: mimetypeIcon name: icon != "" ? icon : "save" height: units.gu(4.5) width: height } Label { id: filenameLabel anchors.top: mimetypeIcon.top anchors.left: mimetypeIcon.right anchors.leftMargin: units.gu(2) anchors.right: parent.right anchors.rightMargin: units.gu(2) elide: Text.ElideMiddle text: downloadDialog.filename } Label { anchors.top: filenameLabel.bottom anchors.left: filenameLabel.left anchors.right: filenameLabel.right elide: Text.ElideRight font.capitalization: Font.Capitalize text: MimeDatabase.nameForMimetype(downloadDialog.mimeType) } } Label { width: parent.width text: i18n.tr("Choose an application to open this file or add it to the downloads folder.") wrapMode: Text.Wrap visible: peerModel.peers.length > 0 } Button { text: i18n.tr("Choose an application") objectName: "chooseAppButton" anchors.horizontalCenter: parent.horizontalCenter width: units.gu(22) height: units.gu(4) visible: peerModel.peers.length > 0 onClicked: { PopupUtils.close(downloadOptionsDialog) pickerRect.visible = true } } Button { text: i18n.tr("Download") objectName: "downloadFileButton" anchors.horizontalCenter: parent.horizontalCenter width: units.gu(22) height: units.gu(4) onClicked: { startDownload(downloadId, singleDownload, mimeType) PopupUtils.close(downloadDialog) } } Button { text: i18n.tr("Cancel") objectName: "cancelDownloadButton" anchors.horizontalCenter: parent.horizontalCenter width: units.gu(22) height: units.gu(4) onClicked: PopupUtils.close(downloadDialog) } } } } ContentPeerModel { id: peerModel handler: ContentHandler.Destination contentType: downloadDialog.contentType } Rectangle { id: pickerRect anchors.fill: parent visible: false ContentPeerPicker { id: peerPicker handler: ContentHandler.Destination objectName: "contentPeerPicker" visible: parent.visible onPeerSelected: { activeTransfer = peer.request() activeTransfer.downloadId = downloadDialog.downloadId activeTransfer.state = ContentTransfer.Downloading PopupUtils.close(downloadDialog) } onCancelPressed: { PopupUtils.close(downloadDialog) } } } Component.onCompleted: { PopupUtils.open(downloadOptionsComponent, downloadDialog) } } } ./src/app/webbrowser/webbrowser-app.h0000644000004100000410000000173713004613604020110 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __WEBBROWSER_APP_H__ #define __WEBBROWSER_APP_H__ #include "browserapplication.h" class WebbrowserApp : public BrowserApplication { Q_OBJECT public: WebbrowserApp(int& argc, char** argv); bool initialize(); private: virtual void printUsage() const; }; #endif // __WEBBROWSER_APP_H__ ./src/app/webbrowser/ExpandedHistoryView.qml0000644000004100000410000000722613004613604021457 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import ".." FocusScope { id: expandedHistoryView property alias model: entriesListView.model property alias count: entriesListView.count signal historyEntryClicked(url url) signal historyEntryRemoved(url url) signal done() MouseArea { // Prevent click events from propagating through to the view below anchors.fill: parent acceptedButtons: Qt.AllButtons } Rectangle { anchors.fill: parent color: "#f6f6f6" } ListView { id: entriesListView focus: true clip: true anchors { top: header.bottom topMargin: units.gu(1.5) bottom: parent.bottom bottomMargin: units.gu(1.5) left: parent.left right: parent.right } section.property: "lastVisitDate" section.delegate: HistorySectionDelegate { anchors { left: parent.left leftMargin: units.gu(1.5) right: parent.right } } delegate: UrlDelegate { id: entriesDelegate objectName: "entriesDelegate" width: parent.width height: units.gu(5) url: model.url title: model.title icon: model.icon onClicked: expandedHistoryView.historyEntryClicked(model.url) onRemoved: expandedHistoryView.historyEntryRemoved(model.url) } highlight: ListViewHighlight {} Keys.onEnterPressed: currentItem.clicked() Keys.onReturnPressed: currentItem.clicked() Keys.onDeletePressed: currentItem.removed() Keys.onEscapePressed: done() } Item { id: header objectName: "header" anchors { top: parent.top left: parent.left right: parent.right } height: units.gu(8) Rectangle { anchors { left: parent.left right: parent.right bottom: parent.bottom } height: units.dp(1) color: "#dedede" } UrlDelegate { anchors { left: parent.left right: doneButton.left rightMargin: units.gu(1) top: parent.top topMargin: -units.gu(0.7) } icon: model ? model.lastVisitedIcon : "" title: model ? model.domain : "" url: i18n.tr("%1 page", "%1 pages", entriesListView.count).arg(entriesListView.count) enabled: false } Button { id: doneButton strokeColor: UbuntuColors.darkGrey anchors { right: parent.right rightMargin: units.gu(2) verticalCenter: parent.verticalCenter } text: i18n.tr("Less") onClicked: expandedHistoryView.done() } } } ./src/app/webbrowser/UrlDelegateWide.qml0000644000004100000410000000405413004613604020514 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import ".." ListItem { id: urlDelegate property alias icon: icon.source property alias title: title.text property alias url: url.text property bool removable: true divider.visible: false height: units.gu(5) signal removed() Favicon { id: icon anchors { verticalCenter: parent.verticalCenter left: parent.left leftMargin: units.gu(1.5) } } Column { width: parent.width - icon.width - parent.spacing anchors { left: icon.right leftMargin: units.gu(1) verticalCenter: parent.verticalCenter } Label { id: title fontSize: "x-small" color: UbuntuColors.darkGrey wrapMode: Text.Wrap elide: Text.ElideRight maximumLineCount: 1 } Label { id: url fontSize: "xx-small" color: UbuntuColors.darkGrey wrapMode: Text.Wrap elide: Text.ElideRight maximumLineCount: 1 } } property var _deleteAction: Action { objectName: "leadingAction.delete" iconName: "delete" onTriggered: urlDelegate.removed() } leadingActions: ListItemActions { actions: removable ? [_deleteAction] : [] } } ./src/app/webbrowser/text-search-filter-model.h0000644000004100000410000000417313004613604021756 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 TEXTSEARCHFILTERMODEL_H #define TEXTSEARCHFILTERMODEL_H // Qt #include #include #include #include #include #include class TextSearchFilterModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(QVariant sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged) Q_PROPERTY(QStringList terms READ terms WRITE setTerms NOTIFY termsChanged) Q_PROPERTY(QStringList searchFields READ searchFields WRITE setSearchFields NOTIFY searchFieldsChanged) Q_PROPERTY(int count READ count NOTIFY countChanged) public: TextSearchFilterModel(QObject* parent=0); QVariant sourceModel() const; void setSourceModel(QVariant sourceModel); int count() const; const QStringList& terms() const; void setTerms(const QStringList&); const QStringList& searchFields() const; void setSearchFields(const QStringList&); Q_SIGNALS: void sourceModelChanged() const; void termsChanged() const; void searchFieldsChanged() const; void countChanged() const; protected: // reimplemented from QSortFilterProxyModel bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; private: void updateSearchRoles(const QAbstractItemModel* model); QStringList m_terms; QStringList m_searchFields; QList m_searchRoles; }; #endif // TEXTSEARCHFILTERMODEL_H ./src/app/webbrowser/history-domain-model.cpp0000644000004100000410000000741413004613604021546 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "history-domain-model.h" #include "history-model.h" // Qt #include /*! \class HistoryDomainModel \brief Proxy model that filters the contents of a history model based on a domain name HistoryDomainModel is a proxy model that filters the contents of a history model based on a domain name. An entry in the history model matches if the domain name extracted from its URL equals the filter domain name (case-insensitive comparison). When no domain name is set (null or empty string), all entries match. */ HistoryDomainModel::HistoryDomainModel(QObject* parent) : QSortFilterProxyModel(parent) { connect(this, SIGNAL(layoutChanged(QList, QAbstractItemModel::LayoutChangeHint)), SLOT(onModelChanged())); connect(this, SIGNAL(modelReset()), SLOT(onModelChanged())); connect(this, SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(onModelChanged())); connect(this, SIGNAL(rowsRemoved(QModelIndex, int, int)), SLOT(onModelChanged())); connect(this, SIGNAL(dataChanged(QModelIndex, QModelIndex, QVector)), SLOT(onModelChanged())); } HistoryModel* HistoryDomainModel::sourceModel() const { return qobject_cast(QSortFilterProxyModel::sourceModel()); } void HistoryDomainModel::setSourceModel(HistoryModel* sourceModel) { if (sourceModel != this->sourceModel()) { QSortFilterProxyModel::setSourceModel(sourceModel); Q_EMIT sourceModelChanged(); } } const QString& HistoryDomainModel::domain() const { return m_domain; } void HistoryDomainModel::setDomain(const QString& domain) { if (domain != m_domain) { m_domain = domain; invalidate(); Q_EMIT domainChanged(); } } const QDateTime& HistoryDomainModel::lastVisit() const { return m_lastVisit; } const QString& HistoryDomainModel::lastVisitedTitle() const { return m_lastVisitedTitle; } const QUrl& HistoryDomainModel::lastVisitedIcon() const { return m_lastVisitedIcon; } bool HistoryDomainModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { if (m_domain.isEmpty()) { return true; } QModelIndex index = sourceModel()->index(source_row, 0, source_parent); QString domain = sourceModel()->data(index, HistoryModel::Domain).toString(); return (domain.compare(m_domain, Qt::CaseInsensitive) == 0); } void HistoryDomainModel::onModelChanged() { // If the rowCount is zero all the history entries of this model were // removed. If that happens this domain will be removed from the list, so // we shouldn’t update its properties lest the update triggers a re-ordering // on any sort proxy model that uses this model as source, while removing an // entry. if (rowCount() > 0) { m_lastVisit = data(index(0, 0), HistoryModel::LastVisit).toDateTime(); m_lastVisitedTitle = data(index(0, 0), HistoryModel::Title).toString(); m_lastVisitedIcon = data(index(0, 0), HistoryModel::Icon).toUrl(); Q_EMIT lastVisitChanged(); Q_EMIT lastVisitedTitleChanged(); Q_EMIT lastVisitedIconChanged(); } } ./src/app/webbrowser/cache-deleter.cpp0000644000004100000410000000565613004613604020175 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "cache-deleter.h" #include #include #include #include #include #include #include CacheDeleter::CacheDeleter(QObject* parent) : QObject(parent) { connect(&m_clearWatcher, SIGNAL(finished()), SLOT(onCleared())); } void CacheDeleter::clear(const QString& cachePath, const QJSValue& callback) { QMutexLocker locker(&m_mutex); if (m_clearWatcher.isRunning()) { return; } if (!callback.isUndefined() && !callback.isCallable()) { qWarning() << "CacheDeleter::clear: 'callback' is not a function"; return; } m_callback = callback; m_clearWatcher.setFuture(QtConcurrent::run(this, &CacheDeleter::doClear, cachePath)); } /* * Poor man’s implementation of clearing oxide’s cache directory. * Until oxide grows an API to do that (https://launchpad.net/bugs/1260014), * this simply deletes selected files in the Cache directory to reclaim * space. This heavily relies on the resilience of the cache backend that * is expected to cope well with files disappearing under its feet. * Note that if cached data was kept in memory, it’s not evicted, so this * implementation doesn’t actually clear the cache completely. */ void CacheDeleter::doClear(const QString& cachePath) { // This assumes the cache is using chromium’s simple cache backend. QStringList nameFilters = QStringList() << "0*" << "1*" << "2*" << "3*" << "4*" << "5*" << "6*" << "7*" << "8*" << "9*" << "a*" << "b*" << "c*" << "d*" << "e*" << "f*" << "index"; QDir::Filters filters = QDir::Files | QDir::NoDotAndDotDot; QFileInfoList files = QDir(cachePath).entryInfoList(nameFilters, filters); Q_FOREACH(const QFileInfo& file, files) { QFile::remove(file.absoluteFilePath()); } nameFilters = QStringList() << "the-real-index"; files = QDir(cachePath + "/index-dir").entryInfoList(nameFilters, filters); Q_FOREACH(const QFileInfo& file, files) { QFile::remove(file.absoluteFilePath()); } } void CacheDeleter::onCleared() { if (!m_callback.isUndefined()) { m_callback.call(); m_callback = QJSValue::UndefinedValue; } } ./src/app/webbrowser/SettingsPage.qml0000644000004100000410000003465613004613604020116 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Qt.labs.settings 1.0 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import Ubuntu.Web 0.2 import webbrowserapp.private 0.1 import "../UrlUtils.js" as UrlUtils Item { id: settingsItem property QtObject settingsObject signal done() Rectangle { anchors.fill: parent color: "#f6f6f6" } SearchEngines { id: searchEngines searchPaths: searchEnginesSearchPaths } BrowserPageHeader { id: title onBack: settingsItem.done() text: i18n.tr("Settings") visible: !subpageContainer.visible } Flickable { anchors { top: title.bottom left: parent.left right: parent.right bottom: parent.bottom } visible: !subpageContainer.visible clip: true contentHeight: settingsCol.height Column { id: settingsCol width: parent.width ListItem { id: searchEngineListItem objectName: "searchengine" readonly property string currentSearchEngineDisplayName: currentSearchEngine.name SearchEngine { id: currentSearchEngine searchPaths: searchEngines.searchPaths filename: settingsObject.searchEngine } ListItemLayout { title.text: i18n.tr("Search engine") subtitle.text: searchEngineListItem.currentSearchEngineDisplayName } visible: searchEngines.engines.count > 1 onClicked: searchEngineComponent.createObject(subpageContainer) } ListItem { id: homepageListItem objectName: "homepage" readonly property url currentHomepage: settingsObject.homepage ListItemLayout { title.text: i18n.tr("Homepage") subtitle.text: homepageListItem.currentHomepage } onClicked: PopupUtils.open(homepageDialog) } ListItem { objectName: "restoreSession" ListItemLayout { title.text: i18n.tr("Restore previous session at startup") CheckBox { id: restoreSessionCheckbox SlotsLayout.position: SlotsLayout.Trailing onTriggered: settingsObject.restoreSession = checked } } Binding { target: restoreSessionCheckbox property: "checked" value: settingsObject.restoreSession } } ListItem { objectName: "privacy" ListItemLayout { title.text: i18n.tr("Privacy & permissions") } onClicked: privacyComponent.createObject(subpageContainer) } ListItem { objectName: "reset" ListItemLayout { title.text: i18n.tr("Reset browser settings") } onClicked: settingsObject.restoreDefaults() } } } Item { id: subpageContainer visible: children.length > 0 anchors.fill: parent Component { id: searchEngineComponent Item { id: searchEngineItem objectName: "searchEnginePage" anchors.fill: parent Rectangle { anchors.fill: parent color: "#f6f6f6" } BrowserPageHeader { id: searchEngineTitle onBack: searchEngineItem.destroy() text: i18n.tr("Search engine") } ListView { anchors { top: searchEngineTitle.bottom left: parent.left right: parent.right bottom: parent.bottom } clip: true model: searchEngines.engines delegate: ListItem { id: searchEngineDelegate objectName: "searchEngineDelegate" readonly property string displayName: delegateSearchEngine.name SearchEngine { id: delegateSearchEngine searchPaths: searchEngines.searchPaths filename: model.filename } ListItemLayout { title.text: searchEngineDelegate.displayName CheckBox { SlotsLayout.position: SlotsLayout.Trailing checked: settingsObject.searchEngine == delegateSearchEngine.filename onClicked: { settingsObject.searchEngine = delegateSearchEngine.filename searchEngineItem.destroy() } } } } } } } Component { id: privacyComponent Item { id: privacyItem objectName: "privacySettings" anchors.fill: parent Rectangle { anchors.fill: parent color: "#f6f6f6" } BrowserPageHeader { id: privacyTitle onBack: privacyItem.destroy() text: i18n.tr("Privacy & permissions") } Flickable { anchors { top: privacyTitle.bottom left: parent.left right: parent.right bottom: parent.bottom } clip: true contentHeight: privacyCol.height Column { id: privacyCol width: parent.width ListItem { objectName: "privacy.mediaAccess" ListItemLayout { title.text: i18n.tr("Camera & microphone") } onClicked: mediaAccessComponent.createObject(subpageContainer) } ListItem { objectName: "privacy.clearHistory" ListItemLayout { title.text: i18n.tr("Clear Browsing History") } enabled: HistoryModel.count > 0 onClicked: { var dialog = PopupUtils.open(privacyConfirmDialogComponent, privacyItem, {"title": i18n.tr("Clear Browsing History?")}) dialog.confirmed.connect(HistoryModel.clearAll) } } ListItem { objectName: "privacy.clearCache" ListItemLayout { title.text: i18n.tr("Clear Cache") } onClicked: { var dialog = PopupUtils.open(privacyConfirmDialogComponent, privacyItem, {"title": i18n.tr("Clear Cache?")}) dialog.confirmed.connect(function() { enabled = false; CacheDeleter.clear(cacheLocation + "/Cache2", function() { enabled = true }); }) } } } } Component { id: privacyConfirmDialogComponent Dialog { id: privacyConfirmDialog objectName: "privacyConfirmDialog" signal confirmed() Row { spacing: units.gu(2) anchors { left: parent.left right: parent.right } Button { objectName: "privacyConfirmDialog.cancelButton" width: (parent.width - parent.spacing) / 2 text: i18n.tr("Cancel") onClicked: PopupUtils.close(privacyConfirmDialog) } Button { objectName: "privacyConfirmDialog.confirmButton" width: (parent.width - parent.spacing) / 2 text: i18n.tr("Clear") color: UbuntuColors.green onClicked: { confirmed() PopupUtils.close(privacyConfirmDialog) } } } } } } } } Component { id: homepageDialog Dialog { id: dialogue objectName: "homepageDialog" title: i18n.tr("Homepage") Component.onCompleted: { homepageTextField.forceActiveFocus() homepageTextField.cursorPosition = homepageTextField.text.length } TextField { id: homepageTextField objectName: "homepageDialog.text" text: settingsObject.homepage inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText | Qt.ImhUrlCharactersOnly onAccepted: { if (UrlUtils.looksLikeAUrl(text)) { settingsObject.homepage = UrlUtils.fixUrl(text) PopupUtils.close(dialogue) } } } Button { objectName: "homepageDialog.cancelButton" anchors { left: parent.left right: parent.right } text: i18n.tr("Cancel") onClicked: PopupUtils.close(dialogue) } Button { objectName: "homepageDialog.saveButton" anchors { left: parent.left right: parent.right } text: i18n.tr("Save") enabled: UrlUtils.looksLikeAUrl(homepageTextField.text.trim()) color: "#3fb24f" onClicked: { settingsObject.homepage = UrlUtils.fixUrl(homepageTextField.text) PopupUtils.close(dialogue) } } } } Component { id: mediaAccessComponent Item { id: mediaAccessItem objectName: "mediaAccessSettings" anchors.fill: parent Rectangle { anchors.fill: parent color: "#f6f6f6" } BrowserPageHeader { id: mediaAccessTitle onBack: mediaAccessItem.destroy() text: i18n.tr("Camera & microphone") } Flickable { anchors { top: mediaAccessTitle.bottom left: parent.left right: parent.right bottom: parent.bottom } clip: true contentHeight: mediaAccessCol.height Column { id: mediaAccessCol width: parent.width ListItem { ListItemLayout { title.text: i18n.tr("Microphone") } } SettingsDeviceSelector { anchors.left: parent.left anchors.right: parent.right isAudio: true visible: devicesCount > 0 enabled: devicesCount > 1 defaultDevice: settingsObject.defaultAudioDevice onDeviceSelected: { SharedWebContext.sharedContext.defaultAudioCaptureDeviceId = id settingsObject.defaultAudioDevice = id } } ListItem { ListItemLayout { title.text: i18n.tr("Camera") } } SettingsDeviceSelector { anchors.left: parent.left anchors.right: parent.right isAudio: false visible: devicesCount > 0 enabled: devicesCount > 1 defaultDevice: settingsObject.defaultVideoDevice onDeviceSelected: { SharedWebContext.sharedContext.defaultVideoCaptureDeviceId = id settingsObject.defaultVideoDevice = id } } } } } } } ./src/app/webbrowser/TabItem.qml0000644000004100000410000001145013004613604017031 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import ".." Item { id: tabItem property bool incognito: false property bool active: false property bool hoverable: true property real rightMargin: 0 property alias title: label.text property alias icon: favicon.source property real dragMin: 0 property real dragMax: 0 readonly property bool dragging: mouseArea.drag.active property color fgColor: Theme.palette.normal.baseText property bool touchEnabled: true signal selected() signal closed() signal contextMenu() BorderImage { id: tabImage anchors.fill: parent anchors.rightMargin: tabItem.rightMargin source: "assets/tab-%1%2.sci".arg((active) ? "active" : (hoverArea.containsMouse ? "hover" : "non-active")) .arg(touchEnabled ? "" : "-desktop") Favicon { id: favicon anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: units.gu(2) shouldCache: !incognito } Item { anchors.left: favicon.right anchors.leftMargin: units.gu(1) anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: closeButton.left anchors.rightMargin: units.gu(1) Label { id: label anchors.fill: parent verticalAlignment: Text.AlignVCenter clip: true fontSize: "small" color: tabItem.fgColor } Rectangle { anchors.centerIn: parent width: label.paintedHeight height: label.width + units.gu(0.25) rotation: 90 gradient: Gradient { GradientStop { position: 0.0; color: active ? "#ffffff" : (hoverArea.containsMouse ? "#c5c5c5" : "#d2d2d2") } GradientStop { position: 0.33; color: "transparent" } } } } MouseArea { id: hoverArea anchors.fill: parent hoverEnabled: !tabItem.active && tabItem.hoverable } MouseArea { id: mouseArea anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right acceptedButtons: Qt.AllButtons onPressed: { if (mouse.button === Qt.LeftButton) { tabItem.selected() } else if (mouse.button === Qt.RightButton) { tabItem.contextMenu() } } onClicked: { if ((mouse.buttons === 0) && (mouse.button === Qt.MiddleButton)) { tabItem.closed() } } } AbstractButton { id: closeButton objectName: "closeButton" // On touch the tap area to close the tab occupies the whole right // hand side of the tab, while it covers only the close icon in // other form factors anchors.fill: touchEnabled ? undefined : closeIcon anchors.top: touchEnabled ? parent.top : undefined anchors.bottom: touchEnabled ? parent.bottom : undefined anchors.right: touchEnabled ? parent.right : undefined width: touchEnabled ? units.gu(4) : closeIcon.width onClicked: closed() MouseArea { anchors.fill: parent acceptedButtons: Qt.MiddleButton onClicked: closed() } } Icon { id: closeIcon height: units.gu(1.5) width: height anchors.right: parent.right anchors.rightMargin: units.gu(1) anchors.verticalCenter: parent.verticalCenter name: "close" color: tabItem.fgColor } } } ./src/app/webbrowser/SecurityCertificatePopover.qml0000644000004100000410000001641413004613604023036 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.ListItems 1.3 import Ubuntu.Components.Popups 1.3 import com.canonical.Oxide 1.0 as Oxide Popover { id: certificatePopover property var securityStatus readonly property bool isWarning: securityStatus.securityLevel == Oxide.SecurityStatus.SecurityLevelWarning readonly property bool isError: securityStatus.securityLevel == Oxide.SecurityStatus.SecurityLevelError Column { width: parent.width - units.gu(4) anchors.horizontalCenter: parent.horizontalCenter spacing: units.gu(0.5) Item { height: units.gu(1.5) width: parent.width } Column { width: parent.width visible: certificatePopover.isWarning || certificatePopover.isError spacing: units.gu(0.5) Row { width: parent.width spacing: units.gu(0.5) Icon { id: alertIcon name: "security-alert" height: units.gu(2) width: height } Column { width: parent.width - alertIcon.width - parent.spacing height: childrenRect.height spacing: units.gu(0.5) Label { width: parent.width wrapMode: Text.WordWrap fontSize: "x-small" text: certificatePopover.isWarning ? i18n.tr("This site has insecure content") : i18n.tr("Identity Not Verified") } Label { width: parent.width wrapMode: Text.WordWrap visible: certificatePopover.isError fontSize: "x-small" text: i18n.tr("The identity of this website has not been verified.") } Label { width: parent.width wrapMode: Text.WordWrap visible: certificatePopover.isError fontSize: "x-small" text: { switch (securityStatus.certStatus) { case Oxide.SecurityStatus.CertStatusBadIdentity: return i18n.tr("Server certificate does not match the identity of the site.") case Oxide.SecurityStatus.CertStatusExpired: return i18n.tr("Server certificate has expired.") case Oxide.SecurityStatus.CertStatusDateInvalid: return i18n.tr("Server certificate contains invalid dates.") case Oxide.SecurityStatus.CertStatusAuthorityInvalid: return i18n.tr("Server certificate is issued by an entity that is not trusted.") case Oxide.SecurityStatus.CertStatusRevoked: return i18n.tr("Server certificate has been revoked.") case Oxide.SecurityStatus.CertStatusInvalid: return i18n.tr("Server certificate is invalid.") case Oxide.SecurityStatus.CertStatusInsecure: return i18n.tr("Server certificate is insecure.") default: return i18n.tr("Server certificate failed our security checks for an unknown reason.") } } } } } ThinDivider { width: parent.width anchors.leftMargin: 0 anchors.rightMargin: 0 visible: !certificatePopover.isError } } Column { width: parent.width spacing: units.gu(0.5) visible: !certificatePopover.isError Label { width: parent.width wrapMode: Text.WordWrap text: i18n.tr("You are connected to") fontSize: "x-small" } Label { width: parent.width wrapMode: Text.WordWrap text: securityStatus.certificate.subjectDisplayName fontSize: "x-small" } ThinDivider { width: parent.width anchors.leftMargin: 0 anchors.rightMargin: 0 visible: orgName.visible || localityName.visible || stateName.visible || countryName.visible } Label { width: parent.width wrapMode: Text.WordWrap visible: orgName.visible text: i18n.tr("Which is run by") fontSize: "x-small" } Label { id: orgName width: parent.width wrapMode: Text.WordWrap visible: text.length > 0 text: securityStatus.certificate.getSubjectInfo(Oxide.SslCertificate.PrincipalAttrOrganizationName).join(", ") fontSize: "x-small" } Label { id: localityName width: parent.width wrapMode: Text.WordWrap visible: text.length > 0 text: securityStatus.certificate.getSubjectInfo(Oxide.SslCertificate.PrincipalAttrLocalityName).join(", ") fontSize: "x-small" } Label { id: stateName width: parent.width wrapMode: Text.WordWrap visible: text.length > 0 text: securityStatus.certificate.getSubjectInfo(Oxide.SslCertificate.PrincipalAttrStateOrProvinceName).join(", ") fontSize: "x-small" } Label { id: countryName width: parent.width wrapMode: Text.WordWrap visible: text.length > 0 text: securityStatus.certificate.getSubjectInfo(Oxide.SslCertificate.PrincipalAttrCountryName).join(", ") fontSize: "x-small" } } Item { height: units.gu(1) width: parent.width } } MouseArea { anchors.fill: parent onClicked: PopupUtils.close(certificatePopover) } } ./src/app/webbrowser/TabsList.qml0000644000004100000410000001220013004613604017223 0ustar www-datawww-data/* * Copyright 2014-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 Item { id: tabslist property real delegateHeight property real chromeHeight property alias model: repeater.model readonly property int count: repeater.count property bool incognito signal scheduleTabSwitch(int index) signal tabSelected(int index) signal tabClosed(int index) function reset() { flickable.contentY = 0 } readonly property bool animating: selectedAnimation.running TabChrome { id: invisibleTabChrome visible: false } Rectangle { anchors { top: parent.top left: parent.left right: parent.right } height: invisibleTabChrome.height color: "#111111" } Flickable { id: flickable anchors.fill: parent flickableDirection: Flickable.VerticalFlick boundsBehavior: Flickable.StopAtBounds contentWidth: width contentHeight: model ? (model.count - 1) * delegateHeight + height : 0 Repeater { id: repeater delegate: Loader { id: delegate asynchronous: true width: flickable.contentWidth height: (index == (repeater.model.count - 1)) ? flickable.height : delegateHeight Behavior on height { UbuntuNumberAnimation { duration: UbuntuAnimation.BriskDuration } } y: Math.max(flickable.contentY, index * delegateHeight) Behavior on y { enabled: !flickable.moving && !selectedAnimation.running UbuntuNumberAnimation { duration: UbuntuAnimation.BriskDuration } } opacity: selectedAnimation.running && (index > selectedAnimation.index) ? 0 : 1 Behavior on opacity { UbuntuNumberAnimation { duration: UbuntuAnimation.FastDuration } } readonly property string title: model.title ? model.title : (model.url.toString() ? model.url : i18n.tr("New tab")) readonly property string icon: model.icon active: (index >= 0) && ((flickable.contentY + flickable.height + delegateHeight / 2) >= (index * delegateHeight)) visible: flickable.contentY < ((index + 1) * delegateHeight) sourceComponent: TabPreview { title: delegate.title icon: delegate.icon incognito: tabslist.incognito tab: model.tab showContent: ((index > 0) && (delegate.y > flickable.contentY)) || !(tab.webview && tab.webview.visible) Binding { // Change the height of the location bar controller // for the first webview only, and only while the tabs // list view is visible. when: tabslist.visible && (index == 0) target: tab.webview ? tab.webview.locationBarController : null property: "height" value: invisibleTabChrome.height } onSelected: tabslist.selectAndAnimateTab(index) onClosed: tabslist.tabClosed(index) } } } PropertyAnimation { id: selectedAnimation property int index: 0 target: flickable property: "contentY" to: index * delegateHeight - chromeHeight + invisibleTabChrome.height duration: UbuntuAnimation.FastDuration onStopped: { // Delay switching the tab until after the animation has completed. delayedTabSelection.index = index delayedTabSelection.start() } } Timer { id: delayedTabSelection interval: 1 property int index: 0 onTriggered: tabslist.tabSelected(index) } } function selectAndAnimateTab(index) { // Animate tab into full view if (index == 0) { tabSelected(0) } else { selectedAnimation.index = index scheduleTabSwitch(index) selectedAnimation.start() } } } ./src/app/webbrowser/bookmarks-folderlist-model.cpp0000644000004100000410000001437413004613604022740 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "bookmarks-folder-model.h" #include "bookmarks-folderlist-model.h" #include "bookmarks-model.h" // Qt #include #include /*! \class BookmarksFolderListModel \brief List model that exposes bookmarks entries grouped by folder name BookmarksFolderListModel is a list model that exposes bookmarks entries from a BookmarksModel grouped by folder name. Each item in the list has two roles: 'folder' for the folder name and 'entries' for the corresponding BookmarksFolderModel that contains all entries in this group. */ BookmarksFolderListModel::BookmarksFolderListModel(QObject* parent) : QAbstractListModel(parent) , m_sourceModel(0) { } BookmarksFolderListModel::~BookmarksFolderListModel() { clearFolders(); } QHash BookmarksFolderListModel::roleNames() const { static QHash roles; if (roles.isEmpty()) { roles[Folder] = "folder"; roles[Entries] = "entries"; } return roles; } int BookmarksFolderListModel::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); return m_folders.count(); } QVariant BookmarksFolderListModel::data(const QModelIndex& index, int role) const { if (!index.isValid() || !checkValidFolderIndex(index.row())) { return QVariant(); } const QString folder = m_folders.keys().at(index.row()); BookmarksFolderModel* entries = m_folders.value(folder); switch (role) { case Folder: return folder; case Entries: return QVariant::fromValue(entries); default: return QVariant(); } } BookmarksModel* BookmarksFolderListModel::sourceModel() const { return m_sourceModel; } void BookmarksFolderListModel::setSourceModel(BookmarksModel* sourceModel) { if (sourceModel != m_sourceModel) { beginResetModel(); if (m_sourceModel != 0) { m_sourceModel->disconnect(this); } clearFolders(); m_sourceModel = sourceModel; populateModel(); if (m_sourceModel != 0) { connect(m_sourceModel, SIGNAL(folderAdded(const QString&)), SLOT(onFolderAdded(const QString&))); connect(m_sourceModel, SIGNAL(modelReset()), SLOT(onModelReset())); } endResetModel(); Q_EMIT sourceModelChanged(); Q_EMIT countChanged(); } } QVariantMap BookmarksFolderListModel::get(int row) const { if (!checkValidFolderIndex(row)) { return QVariantMap(); } QVariantMap res; QHash names = roleNames(); QHashIterator i(names); while (i.hasNext()) { i.next(); QModelIndex idx = index(row, 0); QVariant data = idx.data(i.key()); res[i.value()] = data; } return res; } int BookmarksFolderListModel::indexOf(const QString& folder) const { QStringList folders = m_folders.keys(); return folders.indexOf(folder); } void BookmarksFolderListModel::createNewFolder(const QString& folder) { m_sourceModel->addFolder(folder); } bool BookmarksFolderListModel::checkValidFolderIndex(int index) const { if ((index < 0) || (index >= m_folders.count())) { qWarning() << "Invalid folder index:" << index; return false; } return true; } void BookmarksFolderListModel::clearFolders() { Q_FOREACH(const QString& folder, m_folders.keys()) { delete m_folders.take(folder); } } void BookmarksFolderListModel::populateModel() { if (m_sourceModel != 0) { Q_FOREACH(const QString& folder, m_sourceModel->folders()) { if (!m_folders.contains(folder)) { addFolder(folder); } } } } void BookmarksFolderListModel::onFolderAdded(const QString& folder) { if (!m_folders.contains(folder)) { QStringList folders = m_folders.keys(); int insertAt = 0; while (insertAt < folders.count()) { if (folder.compare(folders.at(insertAt)) < 0) { break; } ++insertAt; } beginInsertRows(QModelIndex(), insertAt, insertAt); addFolder(folder); endInsertRows(); Q_EMIT countChanged(); } } void BookmarksFolderListModel::onModelReset() { beginResetModel(); clearFolders(); populateModel(); endResetModel(); Q_EMIT countChanged(); } void BookmarksFolderListModel::addFolder(const QString& folder) { BookmarksFolderModel* model = new BookmarksFolderModel(this); model->setSourceModel(m_sourceModel); model->setFolder(folder); connect(model, SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(onFolderDataChanged())); connect(model, SIGNAL(rowsRemoved(QModelIndex, int, int)), SLOT(onFolderDataChanged())); connect(model, SIGNAL(rowsMoved(QModelIndex, int, int, QModelIndex, int)), SLOT(onFolderDataChanged())); connect(model, SIGNAL(layoutChanged(QList, QAbstractItemModel::LayoutChangeHint)), SLOT(onFolderDataChanged())); connect(model, SIGNAL(dataChanged(QModelIndex, QModelIndex)), SLOT(onFolderDataChanged())); connect(model, SIGNAL(modelReset()), SLOT(onFolderDataChanged())); m_folders.insert(folder, model); } void BookmarksFolderListModel::onFolderDataChanged() { BookmarksFolderModel* model = qobject_cast(sender()); if (model != 0) { emitDataChanged(model->folder()); } } void BookmarksFolderListModel::emitDataChanged(const QString& folder) { int i = m_folders.keys().indexOf(folder); if (i != -1) { QModelIndex index = this->index(i, 0); Q_EMIT dataChanged(index, index, QVector() << Entries); } } ./src/app/webbrowser/ToolbarAction.qml0000644000004100000410000000341613004613604020247 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 AbstractButton { id: toolbarAction property alias iconName: icon.name property color color: Theme.palette.normal.baseText property alias text: label.text opacity: enabled ? 1.0 : 0.3 width: Math.max(label.paintedWidth, icon.width) activeFocusOnPress: false Item { anchors { top: parent.top left: parent.left right: parent.right } height: width Icon { id: icon width: units.gu(2) height: width anchors { top: parent.top topMargin: units.gu(1) horizontalCenter: parent.horizontalCenter } color: toolbarAction.color } } Label { id: label anchors { bottom: parent.bottom horizontalCenter: parent.horizontalCenter } horizontalAlignment: Text.AlignHCenter fontSize: "x-small" maximumLineCount: 1 elide: Text.ElideMiddle color: toolbarAction.color } } ./src/app/webbrowser/DownloadsPage.qml0000644000004100000410000002364313004613604020242 0ustar www-datawww-data/* * Copyright 2015-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Content 1.3 import webbrowserapp.private 0.1 import webbrowsercommon.private 0.1 import "../MimeTypeMapper.js" as MimeTypeMapper import ".." FocusScope { id: downloadsItem property var downloadManager // We can get file picking requests either via content-hub (activeTransfer) // Or via the internal oxide file picker (internalFilePicker) in the case // where the user wishes to upload a file from their previous downloads. property var activeTransfer property var internalFilePicker property bool selectMode property bool pickingMode property bool multiSelect property alias mimetypeFilter: downloadModelFilter.pattern signal done() Loader { id: thumbnailLoader source: "Thumbnailer.qml" } Rectangle { anchors.fill: parent color: "#fbfbfb" } BrowserPageHeader { id: title text: i18n.tr("Downloads") color: "#f7f7f7" actions: [ Action { text: i18n.tr("Confirm selection") iconName: "tick" visible: pickingMode enabled: downloadsListView.ViewItems.selectedIndices.length > 0 onTriggered: { var results = [] if (internalFilePicker) { for (var i = 0; i < downloadsListView.ViewItems.selectedIndices.length; i++) { var selectedDownload = downloadsListView.model.get(downloadsListView.ViewItems.selectedIndices[i]) results.push(selectedDownload.path) } internalFilePicker.accept(results) } else { for (var i = 0; i < downloadsListView.ViewItems.selectedIndices.length; i++) { var selectedDownload = downloadsListView.model.get(downloadsListView.ViewItems.selectedIndices[i]) results.push(resultComponent.createObject(downloadsItem, {"url": "file://" + selectedDownload.path})) } activeTransfer.items = results activeTransfer.state = ContentTransfer.Charged } downloadsItem.done() } }, Action { text: i18n.tr("Select all") iconName: "select" visible: selectMode onTriggered: { if (downloadsListView.ViewItems.selectedIndices.length === downloadsListView.count) { downloadsListView.ViewItems.selectedIndices = [] } else { var indices = [] for (var i = 0; i < downloadsListView.count; ++i) { indices.push(i) } downloadsListView.ViewItems.selectedIndices = indices } } }, Action { text: i18n.tr("Delete") iconName: "delete" visible: selectMode enabled: downloadsListView.ViewItems.selectedIndices.length > 0 onTriggered: { var toDelete = [] for (var i = 0; i < downloadsListView.ViewItems.selectedIndices.length; i++) { var selectedDownload = downloadsListView.model.get(downloadsListView.ViewItems.selectedIndices[i]) toDelete.push(selectedDownload.path) } for (var i = 0; i < toDelete.length; i++) { DownloadsModel.deleteDownload(toDelete[i]) } downloadsListView.ViewItems.selectedIndices = [] downloadsItem.selectMode = false } }, Action { iconName: "edit" visible: !selectMode && !pickingMode enabled: downloadsListView.count > 0 onTriggered: { selectMode = true multiSelect = true } } ] onBack: { if (selectMode) { selectMode = false } else { if (activeTransfer) { activeTransfer.state = ContentTransfer.Aborted } if (internalFilePicker) { internalFilePicker.reject() } downloadsItem.done() } } } Component { id: resultComponent ContentItem { } } ListView { id: downloadsListView clip: true focus: !exportPeerPicker.focus anchors { fill: parent topMargin: title.height } model: SortFilterModel { model: DownloadsModel filter { id: downloadModelFilter property: "mimetype" } } property int selectedIndex: -1 ViewItems.selectMode: downloadsItem.selectMode || downloadsItem.pickingMode ViewItems.onSelectedIndicesChanged: { if (downloadsItem.multiSelect) { return } // Enforce single selection mode to work around // the lack of such a feature in the UITK. if (ViewItems.selectedIndices.length > 1 && selectedIndex != -1) { var selection = ViewItems.selectedIndices selection.splice(selection.indexOf(selectedIndex), 1) selectedIndex = selection[0] ViewItems.selectedIndices = selection return } if (ViewItems.selectedIndices.length > 0) { selectedIndex = ViewItems.selectedIndices[0] } else { selectedIndex = -1 } } delegate: DownloadDelegate { downloadManager: downloadsItem.downloadManager downloadId: model.downloadId title: model.filename ? model.filename : model.url.toString().split('/').pop().split('?').shift() url: model.url image: model.complete && thumbnailLoader.status == Loader.Ready && (model.mimetype.indexOf("image") === 0 || model.mimetype.indexOf("video") === 0) ? "image://thumbnailer/file://" + model.path : "" icon: MimeDatabase.iconForMimetype(model.mimetype) incomplete: !model.complete visible: !(selectMode && incomplete) errorMessage: model.error paused: model.paused onClicked: { if (model.complete && !selectMode) { exportPeerPicker.contentType = MimeTypeMapper.mimeTypeToContentType(model.mimetype) exportPeerPicker.visible = true exportPeerPicker.path = model.path } } onPressAndHold: { if (downloadsItem.selectMode || downloadsItem.pickingMode) { return } downloadsItem.selectMode = true downloadsItem.multiSelect = true if (downloadsItem.selectMode) { downloadsListView.ViewItems.selectedIndices = [index] } } onRemoved: { if (model.complete) { DownloadsModel.deleteDownload(model.path) } } onCancelled: { DownloadsModel.cancelDownload(model.downloadId) } } highlight: ListViewHighlight {} Keys.onEnterPressed: currentItem.clicked() Keys.onReturnPressed: currentItem.clicked() Keys.onEscapePressed: { if (selectMode) { selectMode = false } else { event.accepted = false } } Keys.onSpacePressed: { if (selectMode || pickingMode) { currentItem.clicked() } } Keys.onDeletePressed: { if (!selectMode && !pickingMode) { currentItem.removed() } } } Label { id: emptyLabel anchors.centerIn: parent visible: downloadsListView.count == 0 wrapMode: Text.Wrap width: parent.width horizontalAlignment: Text.AlignHCenter text: i18n.tr("No downloads available") } Component { id: contentItemComponent ContentItem {} } ContentPeerPicker { id: exportPeerPicker visible: false focus: visible anchors.fill: parent handler: ContentHandler.Destination property string path onPeerSelected: { var transfer = peer.request() if (transfer.state === ContentTransfer.InProgress) { transfer.items = [contentItemComponent.createObject(downloadsItem, {"url": path})] transfer.state = ContentTransfer.Charged } visible = false } onCancelPressed: visible = false Keys.onEscapePressed: visible = false } } ./src/app/webbrowser/bookmarks-folderlist-model.h0000644000004100000410000000445613004613604022405 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __BOOKMARKS_FOLDERLIST_MODEL_H__ #define __BOOKMARKS_FOLDERLIST_MODEL_H__ // Qt #include #include #include class BookmarksFolderModel; class BookmarksModel; class BookmarksFolderListModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(BookmarksModel* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged) Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_ENUMS(Roles) public: BookmarksFolderListModel(QObject* parent=0); ~BookmarksFolderListModel(); enum Roles { Folder = Qt::UserRole + 1, Entries }; // reimplemented from QAbstractListModel QHash roleNames() const; int rowCount(const QModelIndex& parent=QModelIndex()) const; QVariant data(const QModelIndex& index, int role) const; BookmarksModel* sourceModel() const; void setSourceModel(BookmarksModel* sourceModel); Q_INVOKABLE QVariantMap get(int row) const; Q_INVOKABLE int indexOf(const QString& folder) const; Q_INVOKABLE void createNewFolder(const QString& folder); Q_SIGNALS: void sourceModelChanged() const; void countChanged() const; private Q_SLOTS: void onFolderAdded(const QString& folder); void onModelReset(); void onFolderDataChanged(); private: BookmarksModel* m_sourceModel; QMap m_folders; bool checkValidFolderIndex(int row) const; void clearFolders(); void populateModel(); void addFolder(const QString& folder); void emitDataChanged(const QString& folder); }; #endif // __BOOKMARKS_FOLDERLIST_MODEL_H__ ./src/app/FilteredKeyboardModel.qml0000644000004100000410000000201513004613604017520 0ustar www-datawww-data/* * Copyright 2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Unity.InputInfo 0.1 SortFilterModel { model: InputDeviceModel { deviceFilter: InputInfo.Keyboard } filter { // Filter out autopilot-emulated keyboards // (see https://launchpad.net/bugs/1542224). property: "name" pattern: /^(?!py-evdev-uinput).*$/ } } ./src/app/HttpAuthenticationDialog.qml0000644000004100000410000000442213004613604020263 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 as Popups Popups.Dialog { id: dialog title: i18n.tr("Authentication required.") // TRANSLATORS: %1 refers to the URL of the current website and %2 is a string that the website sends with more information about the authentication challenge (technically called "realm") text: request ? i18n.tr('The website at %1 requires authentication. The website says "%2"').arg(request.host).arg(request.realm) : "" property QtObject request: null Connections { target: request onCancelled: PopupUtils.close(dialog) } TextField { id: usernameInput objectName: "username" placeholderText: i18n.tr("Username") onAccepted: { request.allow(usernameInput.text, passwordInput.text) PopupUtils.close(dialog) } } TextField { id: passwordInput objectName: "password" placeholderText: i18n.tr("Password") echoMode: TextInput.Password onAccepted: { request.allow(usernameInput.text, passwordInput.text) PopupUtils.close(dialog) } } Button { objectName: "allow" text: i18n.tr("OK") color: UbuntuColors.green onClicked: { request.allow(usernameInput.text, passwordInput.text) PopupUtils.close(dialog) } } Button { objectName: "deny" text: i18n.tr("Cancel") color: UbuntuColors.coolGrey onClicked: { request.deny() PopupUtils.close(dialog) } } } ./src/app/unity8/0000755000004100000410000000000013004613605014050 5ustar www-datawww-data./src/app/unity8/CMakeLists.txt0000644000004100000410000000010213004613604016600 0ustar www-datawww-dataproject(unity8) add_subdirectory(libs) add_subdirectory(plugins) ./src/app/unity8/README0000644000004100000410000000140113004613604014723 0ustar www-datawww-dataCode in this directory was copied over from unity8. Ubuntu.Gestures: - first import: 2015-02-04, at revision 1583 of lp:unity8 - last sync: 2015-03-17, at revision 1663 of lp:unity8 Unity.InputInfo: - first import: 2016-01-28, at revision 2143 of lp:unity8 The structure of the directories has been kept identical, to ease syncing the code in the future. Minor changes were made to the build system to integrate it with the existing webbrowser-app code base. As of 2015-12-09, the SwipeArea component is available in the UITK, so we should start using it instead of building a local copy of Ubuntu.Gestures. This is currently blocked on https://launchpad.net/bugs/1459362. The Unity.InputInfo plugin should be replaced by an upstream Qt API in the near future. ./src/app/unity8/plugins/0000755000004100000410000000000013004613605015531 5ustar www-datawww-data./src/app/unity8/plugins/CMakeLists.txt0000644000004100000410000000006113004613604020265 0ustar www-datawww-dataadd_subdirectory(Ubuntu) add_subdirectory(Unity) ./src/app/unity8/plugins/Unity/0000755000004100000410000000000013004613605016641 5ustar www-datawww-data./src/app/unity8/plugins/Unity/CMakeLists.txt0000644000004100000410000000003413004613604021375 0ustar www-datawww-dataadd_subdirectory(InputInfo) ./src/app/unity8/plugins/Unity/InputInfo/0000755000004100000410000000000013004613605020554 5ustar www-datawww-data./src/app/unity8/plugins/Unity/InputInfo/CMakeLists.txt0000644000004100000410000000165013004613604023315 0ustar www-datawww-data# This is a temporary snapshot of the WIP QInputInfo API as we # require this in unity now but upstream isn't finished yet. # Eventually this should be dropped in favor of the upstream # QInputInfo API. project(InputInfo) find_package(Qt5Core REQUIRED) find_package(Qt5Quick REQUIRED) include(FindPkgConfig) pkg_check_modules(LIBUDEV REQUIRED libudev) pkg_check_modules(LIBEVDEV REQUIRED libevdev) include_directories( ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${LIBUDEV_INCLUDE_DIRS} ${LIBEVDEV_INCLUDE_DIRS} ) set(InputInfo_SOURCES plugin.cpp qinputinfo.cpp qdeclarativeinputdevicemodel.cpp linux/qinputdeviceinfo_linux.cpp ) add_library(InputInfo STATIC ${InputInfo_SOURCES} ) target_link_libraries(InputInfo ${LIBUDEV_LDFLAGS} ${LIBEVDEV_LDFLAGS} ) qt5_use_modules(InputInfo Core Qml Quick) #add_unity8_plugin(Unity.InputInfo 0.1 Unity/InputInfo TARGETS InputInfo) ./src/app/unity8/plugins/Unity/InputInfo/qinputinfo.cpp0000644000004100000410000001532013004613604023454 0ustar www-datawww-data/**************************************************************************** ** ** Copyright (C) 2014 Canonical, Ltd. and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the QtSystems module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. For licensing terms and ** conditions see http://qt.digia.com/licensing. For further information ** use the contact form at http://qt.digia.com/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qinputinfo.h" #if defined(UNITY_MOCKS) #include "qinputdeviceinfo_mock_p.h" #elif defined(Q_OS_LINUX) #include "linux/qinputdeviceinfo_linux_p.h" #endif Q_GLOBAL_STATIC(QInputDeviceManagerPrivate, inputDeviceManagerPrivate) QT_BEGIN_NAMESPACE QInputDeviceManagerPrivate * QInputDeviceManagerPrivate::instance() { QInputDeviceManagerPrivate *priv = inputDeviceManagerPrivate(); return priv; } QInputDevicePrivate::QInputDevicePrivate(QObject *parent) : QObject(parent), type(QInputDevice::Unknown) { } QInputDevice::QInputDevice(QObject *parent) : QObject(parent), d_ptr(new QInputDevicePrivate(this)) { } /* * Returns the name of this input device. */ QString QInputDevice::name() const { return d_ptr->name; } /* * Sets the name of this input device to \b name. */ void QInputDevice::setName(const QString &name) { d_ptr->name = name; } /* * Returns the device path of this device. */ QString QInputDevice::devicePath() const { return d_ptr->devicePath; } /* * Sets the device ppath of this device to /b path. */ void QInputDevice::setDevicePath(const QString &path) { d_ptr->devicePath = path; } /* * Returns the number of buttons this device has. */ QList QInputDevice::buttons() const { return d_ptr->buttons; } /* * Adds a button */ void QInputDevice::addButton(int buttonCode) { d_ptr->buttons.append(buttonCode); } /* * Returns the number of switch of this device. */ QList QInputDevice::switches() const { return d_ptr->switches; } /* * Adds a switch */ void QInputDevice::addSwitch(int switchCode) { d_ptr->switches.append(switchCode); } /* * Returns a list of the relative axis of this device */ QList QInputDevice::relativeAxis() const { return d_ptr->relativeAxis; } /* */ void QInputDevice::addRelativeAxis(int axisCode) { d_ptr->relativeAxis.append(axisCode); } /* * Returns a list of the absolute axis of this device */ QList QInputDevice::absoluteAxis() const { return d_ptr->absoluteAxis; } /* */ void QInputDevice::addAbsoluteAxis(int axisCode) { d_ptr->absoluteAxis.append(axisCode); } /* * Returns a QInputDevice::InputTypeFlags of all the types of types. */ QInputDevice::InputTypeFlags QInputDevice::type() const { return d_ptr->type; } /* */ void QInputDevice::setType(QInputDevice::InputTypeFlags type) //? setTypes? { d_ptr->type = type; } QInputDeviceManager::QInputDeviceManager(QObject *parent) : QObject(parent), d_ptr(inputDeviceManagerPrivate) { connect(d_ptr, &QInputDeviceManagerPrivate::deviceAdded,this,&QInputDeviceManager::addedDevice); connect(d_ptr, &QInputDeviceManagerPrivate::deviceRemoved,this,&QInputDeviceManager::deviceRemoved); connect(d_ptr,SIGNAL(ready()),this,SIGNAL(ready())); } /* * Returns a QMap of known input devices. */ QMap QInputDeviceManager::deviceMap() { return d_ptr->deviceMap; } /* */ void QInputDeviceManager::addedDevice(const QString & devicePath) { Q_EMIT deviceAdded(devicePath); } /* * Returns a QVector of InputDevices of type filter * */ QVector QInputDeviceManager::deviceListOfType(QInputDevice::InputType filter) { QVector dList; QMapIterator i(d_ptr->deviceMap); while (i.hasNext()) { i.next(); if (i.value()->type().testFlag(filter) || filter == QInputDevice::Unknown) { dList.append(i.value()); } } return dList; } /* * Returns the number of input devices with the currently set QInputDevice::InputType filter. * If no device filter has been set, returns number of all available input devices. * If filter has not been set, returns all available input devices */ int QInputDeviceManager::deviceCount() const { return deviceCount(static_cast< QInputDevice::InputType >(d_ptr->currentFilter)); } /* * Returns the number of input devices of the type filter. */ int QInputDeviceManager::deviceCount(const QInputDevice::InputType filter) const { int dList = 0; QMapIterator i(d_ptr->deviceMap); while (i.hasNext()) { i.next(); // qDebug() << i.value()->name() << i.value()->devicePath(); // qDebug() << i.value()->type() << i.value()->type().testFlag(filter); if (i.value()->type().testFlag(filter)) { dList++; } } return dList; } /* * Returns the currently set device filter. * */ QInputDevice::InputType QInputDeviceManager::deviceFilter() { return d_ptr->currentFilter; } /* * Sets the current input device filter to filter. * */ void QInputDeviceManager::setDeviceFilter(QInputDevice::InputType filter) { if (filter != d_ptr->currentFilter) { d_ptr->currentFilter = filter; Q_EMIT deviceFilterChanged(filter); } } QT_END_NAMESPACE ./src/app/unity8/plugins/Unity/InputInfo/qmldir0000644000004100000410000000010413004613604021761 0ustar www-datawww-datamodule Unity.InputInfo plugin InputInfo typeinfo InputInfo.qmltypes ./src/app/unity8/plugins/Unity/InputInfo/plugin.cpp0000644000004100000410000000176513004613604022566 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ // Qt #include // self #include "plugin.h" // local #include "qdeclarativeinputdevicemodel_p.h" void InputInfoPlugin::registerTypes(const char *uri) { int major = 0; int minor = 1; qmlRegisterType(uri, major, minor, "InputDeviceModel"); qmlRegisterType(uri, major, minor, "InputInfo"); } ./src/app/unity8/plugins/Unity/InputInfo/qdeclarativeinputdevicemodel_p.h0000644000004100000410000000714513004613604027177 0ustar www-datawww-data/**************************************************************************** ** ** Copyright (C) 2015 Jolla. ** Contact: http://www.qt-project.org/legal ** ** This file is part of the QtSystems module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. For licensing terms and ** conditions see http://qt.digia.com/licensing. For further information ** use the contact form at http://qt.digia.com/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #ifndef QDECLARATIVEINPUTDEVICEMODEL_H #define QDECLARATIVEINPUTDEVICEMODEL_H #include #include #include "qinputinfo.h" class QDeclarativeInputDeviceModel : public QAbstractListModel { Q_OBJECT Q_DISABLE_COPY(QDeclarativeInputDeviceModel) Q_PROPERTY(QInputDevice::InputType deviceFilter READ deviceFilter WRITE setDeviceFilter NOTIFY deviceFilterChanged) Q_PROPERTY(int count READ rowCount NOTIFY countChanged) public: enum ItemRoles { ServiceRole = Qt::UserRole + 1, NameRole, DevicePathRole, ButtonsRole, SwitchesRole, RelativeAxisRole, AbsoluteAxisRole, TypesRole }; explicit QDeclarativeInputDeviceModel(QObject *parent = 0); virtual ~QDeclarativeInputDeviceModel(); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; int rowCount(const QModelIndex &parent = QModelIndex()) const; void setDeviceFilter(QInputDevice::InputType filter); QInputDevice::InputType deviceFilter(); Q_INVOKABLE int indexOf(const QString &devicePath) const; Q_INVOKABLE QInputDevice *get(int index) const; QHash roleNames() const; Q_SIGNALS: void deviceAdded(const QString &devicePath); void deviceRemoved(const QString &devicePath); void deviceFilterChanged(const QInputDevice::InputType filter); void countChanged(); public Q_SLOTS: void updateDeviceList(); private: QInputDeviceManager *deviceInfo; QVector inputDevices; QInputDevice::InputType currentFilter; private Q_SLOTS: void addedDevice(const QString &); void removedDevice(const QString &path); }; #endif // QDECLARATIVEINPUTDEVICEMODEL_H ./src/app/unity8/plugins/Unity/InputInfo/linux/0000755000004100000410000000000013004613605021713 5ustar www-datawww-data./src/app/unity8/plugins/Unity/InputInfo/linux/qinputdeviceinfo_linux_p.h0000644000004100000410000000657213004613604027207 0ustar www-datawww-data/**************************************************************************** ** ** Copyright (C) 2014 Canonical, Ltd. and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the QtSystems module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. For licensing terms and ** conditions see http://qt.digia.com/licensing. For further information ** use the contact form at http://qt.digia.com/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #ifndef QINPUTDEVICEINFO_LINUX_P_H #define QINPUTDEVICEINFO_LINUX_P_H #include #include "qinputinfo.h" #include class QInputDevicePrivate : public QObject { Q_OBJECT public: explicit QInputDevicePrivate(QObject *parent = 0); QString name; QString devicePath; QList buttons; //keys QList switches; QList relativeAxis; QList absoluteAxis; QInputDevice::InputTypeFlags type; }; class QInputDeviceManagerPrivate : public QObject { Q_OBJECT public: explicit QInputDeviceManagerPrivate(QObject *parent = 0); ~QInputDeviceManagerPrivate(); QVector deviceList; QMap deviceMap; static QInputDeviceManagerPrivate * instance(); QInputDevice::InputType currentFilter; Q_SIGNALS: void deviceAdded(const QString &); void deviceRemoved(const QString &); void ready(); private: QInputDevice *addDevice(struct udev_device *udev); QInputDevice *addUdevDevice(struct udev_device *); QInputDevice *addDevice(const QString &path); void removeDevice(const QString &path); QSocketNotifier *notifier; int notifierFd; struct udev_monitor *udevMonitor; QInputDevice::InputTypeFlags getInputTypeFlags(struct udev_device *); struct udev *udevice; void addDetails(struct udev_device *); private Q_SLOTS: void onUDevChanges(); void init(); }; #endif // QINPUTDEVICEINFO_LINUX_P_H ./src/app/unity8/plugins/Unity/InputInfo/linux/qinputdeviceinfo_linux.cpp0000644000004100000410000002342013004613604027212 0ustar www-datawww-data/**************************************************************************** ** ** Copyright (C) 2014 Canonical, Ltd. and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the QtSystems module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. For licensing terms and ** conditions see http://qt.digia.com/licensing. For further information ** use the contact form at http://qt.digia.com/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qinputdeviceinfo_linux_p.h" #include #include #include #include #include #include #include QInputDeviceManagerPrivate::QInputDeviceManagerPrivate(QObject *parent) : QObject(parent), currentFilter(QInputDevice::Unknown), udevice(0) { QTimer::singleShot(250,this,SLOT(init())); } QInputDeviceManagerPrivate::~QInputDeviceManagerPrivate() { udev_unref(udevice); udev_monitor_unref(udevMonitor); } void QInputDeviceManagerPrivate::init() { if (!udevice) udevice = udev_new(); udev_list_entry *devices; udev_list_entry *dev_list_entry; udev_device *dev; QString subsystem = QStringLiteral("input"); struct udev_enumerate *enumerate = 0; if (udevice) { udevMonitor = udev_monitor_new_from_netlink(udevice, "udev"); udev_monitor_filter_add_match_subsystem_devtype(udevMonitor, subsystem.toLatin1(), NULL); enumerate = udev_enumerate_new(udevice); udev_enumerate_add_match_subsystem(enumerate, subsystem.toLatin1()); udev_monitor_enable_receiving(udevMonitor); notifierFd = udev_monitor_get_fd(udevMonitor); notifier = new QSocketNotifier(notifierFd, QSocketNotifier::Read, this); connect(notifier, SIGNAL(activated(int)), this, SLOT(onUDevChanges())); udev_enumerate_scan_devices(enumerate); devices = udev_enumerate_get_list_entry(enumerate); udev_list_entry_foreach(dev_list_entry, devices) { const char *path; path = udev_list_entry_get_name(dev_list_entry); dev = udev_device_new_from_syspath(udevice, path); if (qstrcmp(udev_device_get_subsystem(dev), "input") == 0 ) { QInputDevice *iDevice = addDevice(dev); if (iDevice && !iDevice->devicePath().isEmpty()) { deviceMap.insert(iDevice->devicePath(),iDevice); } } udev_device_unref(dev); } udev_enumerate_unref(enumerate); } // udev_unref(udevice); Q_FOREACH (const QString &devicePath, deviceMap.keys()) { Q_EMIT deviceAdded(devicePath); } Q_EMIT ready(); } QInputDevice::InputTypeFlags QInputDeviceManagerPrivate::getInputTypeFlags(struct udev_device *dev) { QInputDevice::InputTypeFlags flags = QInputDevice::Unknown; if (qstrcmp(udev_device_get_property_value(dev, "ID_INPUT_KEY"), "1") == 0 ) { flags |= QInputDevice::Button; } if (qstrcmp(udev_device_get_property_value(dev, "ID_INPUT_MOUSE"), "1") == 0) { flags |= QInputDevice::Mouse; } if (qstrcmp(udev_device_get_property_value(dev, "ID_INPUT_TOUCHPAD"), "1") == 0) { flags |= QInputDevice::TouchPad; } if (qstrcmp(udev_device_get_property_value(dev, "ID_INPUT_TOUCHSCREEN"), "1") == 0 || qstrcmp(udev_device_get_property_value(dev, "ID_INPUT_TABLET"), "1") == 0) { flags |= QInputDevice::TouchScreen; } if (qstrcmp(udev_device_get_property_value(dev, "ID_INPUT_KEYBOARD"), "1") == 0 ) { flags |= QInputDevice::Keyboard; } if (!QString::fromLatin1(udev_device_get_property_value(dev, "SW")).isEmpty()) { flags |= QInputDevice::Switch; } return flags; } QInputDevice *QInputDeviceManagerPrivate::addDevice(struct udev_device *udev) { QString eventPath = QString::fromLatin1(udev_device_get_sysname(udev)); if (eventPath.contains(QStringLiteral("event"))) eventPath.prepend(QStringLiteral("/dev/input/")); if (deviceMap.contains(eventPath)) { return Q_NULLPTR; } struct libevdev *dev = NULL; int fd; int rc = 1; QInputDevice *inputDevice; inputDevice = addUdevDevice(udev); if (!inputDevice) { return Q_NULLPTR; } eventPath = inputDevice->devicePath(); qDebug() << "Input device added:" << inputDevice->name() << inputDevice->devicePath() << inputDevice->type(); fd = open(eventPath.toLatin1(), O_RDONLY|O_NONBLOCK); if (fd == -1) { return inputDevice; } rc = libevdev_new_from_fd(fd, &dev); if (rc < 0) { qWarning() << "Failed to init libevdev ("<< strerror(-rc) << ")"; return Q_NULLPTR; } for (int i = 0; i < EV_MAX; i++) { if (i == EV_KEY || i == EV_SW || i == EV_REL || i == EV_REL || i == EV_ABS) { for (int j = 0; j < libevdev_event_type_get_max(i); j++) { if (libevdev_has_event_code(dev, i, j)) { switch (i) { case EV_KEY: inputDevice->addButton(j); break; case EV_SW: inputDevice->addSwitch(j); break; case EV_REL: inputDevice->addRelativeAxis(j); break; case EV_ABS: inputDevice->addAbsoluteAxis(j); break; }; } } } } return inputDevice; } void QInputDeviceManagerPrivate::addDetails(struct udev_device *) { } void QInputDeviceManagerPrivate::removeDevice(const QString &path) { // this path is not a full evdev path Q_FOREACH (const QString devicePath, deviceMap.keys()) { if (devicePath.contains(path)) { qDebug() << "Input device removed:" << deviceMap.value(devicePath)->name() << devicePath << deviceMap.value(devicePath)->type(); deviceMap.remove(devicePath); Q_EMIT deviceRemoved(devicePath); } } } QInputDevice *QInputDeviceManagerPrivate::addUdevDevice(struct udev_device *udev) { QInputDevice *iDevice; struct udev_list_entry *list; struct udev_list_entry *node; list = udev_device_get_properties_list_entry (udev); QString syspath = QString::fromLatin1(udev_device_get_syspath(udev)); QDir sysdir(syspath); QStringList infoList = sysdir.entryList(QStringList() << QStringLiteral("event*"),QDir::Dirs); if (infoList.count() > 0) { QString token = infoList.at(0); token.prepend(QStringLiteral("/dev/input/")); iDevice = new QInputDevice(this); iDevice->setDevicePath(token); } else { return Q_NULLPTR; } udev_list_entry_foreach (node, list) { QString key = QString::fromLatin1(udev_list_entry_get_name(node)); QString value = QString::fromLatin1(udev_list_entry_get_value(node)); if (key == QStringLiteral("NAME")) { iDevice->setName(value.remove(QStringLiteral("\""))); } } iDevice->setType(getInputTypeFlags(udev)); return iDevice; } void QInputDeviceManagerPrivate::onUDevChanges() { if (!udevMonitor) return; udev_device *dev = udev_monitor_receive_device(udevMonitor); if (dev) { if (qstrcmp(udev_device_get_subsystem(dev), "input") == 0 ) { QString eventPath = QString::fromLatin1(udev_device_get_sysname(dev)); QString action = QString::fromStdString(udev_device_get_action(dev)); if (!eventPath.contains(QStringLiteral("/dev/input/"))) eventPath.prepend(QStringLiteral("/dev/input/")); if (action == QStringLiteral("add")) { if (deviceMap.contains(eventPath)){ udev_device_unref(dev); return; } QInputDevice *iDevice = addDevice(dev); if (!iDevice) { delete iDevice; return; } iDevice->setType(getInputTypeFlags(dev)); udev_device_unref(dev); deviceMap.insert(eventPath,iDevice); Q_EMIT deviceAdded(eventPath); } else if (action == QStringLiteral("remove")) { removeDevice(eventPath); } } } } ./src/app/unity8/plugins/Unity/InputInfo/plugin.h0000644000004100000410000000167613004613604022234 0ustar www-datawww-data/* * Copyright 2015 Canonical Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef INPUTINFO_PLUGIN_H #define INPUTINFO_PLUGIN_H #include class InputInfoPlugin : public QQmlExtensionPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") public: void registerTypes(const char *uri); }; #endif // INPUTINFO_PLUGIN_H ./src/app/unity8/plugins/Unity/InputInfo/qdeclarativeinputdevicemodel.cpp0000644000004100000410000001521113004613604027204 0ustar www-datawww-data/**************************************************************************** ** ** Copyright (C) 2015 Jolla. ** Contact: http://www.qt-project.org/legal ** ** This file is part of the QtSystems module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. For licensing terms and ** conditions see http://qt.digia.com/licensing. For further information ** use the contact form at http://qt.digia.com/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qdeclarativeinputdevicemodel_p.h" #include "qinputinfo.h" QDeclarativeInputDeviceModel::QDeclarativeInputDeviceModel(QObject *parent) : QAbstractListModel(parent), deviceInfo(new QInputDeviceManager), currentFilter(QInputDevice::Unknown) { connect(deviceInfo,SIGNAL(ready()),this,SLOT(updateDeviceList())); connect(deviceInfo, &QInputDeviceManager::deviceAdded,this,&QDeclarativeInputDeviceModel::addedDevice); connect(deviceInfo, &QInputDeviceManager::deviceRemoved,this,&QDeclarativeInputDeviceModel::removedDevice); } QDeclarativeInputDeviceModel::~QDeclarativeInputDeviceModel() { delete deviceInfo; } QVariant QDeclarativeInputDeviceModel::data(const QModelIndex &index, int role) const { switch (role) { case ServiceRole: return QVariant::fromValue(static_cast(inputDevices.value(index.row()))); break; case NameRole: return QVariant::fromValue(static_cast(inputDevices.value(index.row())->name())); break; case DevicePathRole: return QVariant::fromValue(static_cast(inputDevices.value(index.row())->devicePath())); break; case ButtonsRole: return QVariant::fromValue(static_cast >(inputDevices.value(index.row())->buttons())); break; case SwitchesRole: return QVariant::fromValue(static_cast >(inputDevices.value(index.row())->switches())); break; case RelativeAxisRole: return QVariant::fromValue(static_cast >(inputDevices.value(index.row())->relativeAxis())); break; case AbsoluteAxisRole: return QVariant::fromValue(static_cast >(inputDevices.value(index.row())->absoluteAxis())); break; case TypesRole: return QVariant::fromValue(static_cast(inputDevices.value(index.row())->type())); break; }; return QVariant(); } int QDeclarativeInputDeviceModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return inputDevices.count(); } int QDeclarativeInputDeviceModel::indexOf(const QString &devicePath) const { int idx(-1); Q_FOREACH (QInputDevice *device, inputDevices) { idx++; if (device->devicePath() == devicePath) return idx; } return -1; } QInputDevice *QDeclarativeInputDeviceModel::get(int index) const { if (index < 0 || index > inputDevices.count()) return 0; return inputDevices.value(index); } void QDeclarativeInputDeviceModel::updateDeviceList() { QVector newDevices = deviceInfo->deviceListOfType(currentFilter); int numNew = newDevices.count(); for (int i = 0; i < numNew; i++) { int j = inputDevices.indexOf(newDevices.value(i)); if (j == -1) { beginInsertRows(QModelIndex(), i, i); inputDevices.insert(i, newDevices.value(i)); endInsertRows(); Q_EMIT countChanged(); } else if (i != j) { // changed its position -> move it QInputDevice* device = inputDevices.value(j); beginMoveRows(QModelIndex(), j, j, QModelIndex(), i); inputDevices.remove(j); inputDevices.insert(i, device); endMoveRows(); Q_EMIT countChanged(); } //else { QModelIndex changedIndex(this->index(j, 0, QModelIndex())); Q_EMIT dataChanged(changedIndex, changedIndex); } int numOld = inputDevices.count(); if (numOld > numNew) { beginRemoveRows(QModelIndex(), numNew, numOld - 1); inputDevices.remove(numNew, numOld - numNew); endRemoveRows(); Q_EMIT countChanged(); } } void QDeclarativeInputDeviceModel::addedDevice(const QString &devicePath) { updateDeviceList(); Q_EMIT deviceAdded(devicePath); } void QDeclarativeInputDeviceModel::removedDevice(const QString &devicePath) { updateDeviceList(); Q_EMIT deviceRemoved(devicePath); } QHash QDeclarativeInputDeviceModel::roleNames() const { QHash roles; roles[NameRole] = "name"; roles[DevicePathRole] = "devicePath"; roles[ButtonsRole] = "buttons"; roles[SwitchesRole] = "switches"; roles[RelativeAxisRole] = "rAxis"; roles[AbsoluteAxisRole] = "aAxis"; roles[TypesRole] = "types"; return roles; } /* * Returns the currently set device filter. * */ QInputDevice::InputType QDeclarativeInputDeviceModel::deviceFilter() { return currentFilter; } /* * Sets the current input device filter to filter. * */ void QDeclarativeInputDeviceModel::setDeviceFilter(QInputDevice::InputType filter) { if (filter != currentFilter) { deviceInfo->setDeviceFilter(filter); currentFilter = filter; updateDeviceList(); Q_EMIT deviceFilterChanged(filter); } } ./src/app/unity8/plugins/Unity/InputInfo/qinputinfo.h0000644000004100000410000001067713004613604023133 0ustar www-datawww-data/**************************************************************************** ** ** Copyright (C) 2014 Canonical, Ltd. and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the QtSystems module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. For licensing terms and ** conditions see http://qt.digia.com/licensing. For further information ** use the contact form at http://qt.digia.com/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #ifndef QINPUTINFO_H #define QINPUTINFO_H #include #include #include #include #include class QInputDeviceManagerPrivate; class QInputDevicePrivate; class QInputDevice; class QInputDeviceManager; class QInputDevice : public QObject { Q_OBJECT Q_ENUMS(InputType) Q_FLAGS(InputType InputTypeFlags) friend class QInputDeviceManagerPrivate; public: enum InputType { Unknown = 0, Button = 1, Mouse = 2, TouchPad = 4, TouchScreen = 8, Keyboard = 16, Switch = 32 }; Q_ENUMS(InputType) Q_DECLARE_FLAGS(InputTypeFlags, InputType) explicit QInputDevice(QObject *parent = 0); QString name() const; QString devicePath() const; QList buttons() const; //keys event code QList switches() const; QList relativeAxis() const; QList absoluteAxis() const; QInputDevice::InputTypeFlags type() const; private: QInputDevicePrivate *d_ptr; void setName(const QString &); void setDevicePath(const QString &); void addButton(int); void addSwitch(int); void addRelativeAxis(int); void addAbsoluteAxis(int); void setType(QInputDevice::InputTypeFlags flags); }; Q_DECLARE_METATYPE(QInputDevice::InputType) Q_DECLARE_METATYPE(QInputDevice::InputTypeFlags) class QInputDeviceManagerPrivate; class QInputDeviceManager : public QObject { Q_OBJECT Q_PROPERTY(int deviceCount READ deviceCount NOTIFY deviceCountChanged) Q_PROPERTY(QInputDevice::InputType deviceFilter READ deviceFilter WRITE setDeviceFilter NOTIFY deviceFilterChanged) public: explicit QInputDeviceManager(QObject *parent = 0); int deviceCount() const; int deviceCount(const QInputDevice::InputType filter) const; void setDeviceFilter(QInputDevice::InputType filter); QInputDevice::InputType deviceFilter(); QMap deviceMap(); Q_INVOKABLE QVector deviceListOfType(QInputDevice::InputType filter); Q_SIGNALS: void deviceAdded(const QString & devicePath); void deviceRemoved(const QString & devicePath); void ready(); void deviceCountChanged(int count); void deviceFilterChanged(const QInputDevice::InputType filter); public Q_SLOTS: void addedDevice(const QString & devicePath); private: Q_DISABLE_COPY(QInputDeviceManager) #if !defined(QT_SIMULATOR) QInputDeviceManagerPrivate *const d_ptr; Q_DECLARE_PRIVATE(QInputDeviceManager) #endif }; #endif // QINPUTINFO_H ./src/app/unity8/plugins/Ubuntu/0000755000004100000410000000000013004613605017013 5ustar www-datawww-data./src/app/unity8/plugins/Ubuntu/CMakeLists.txt0000644000004100000410000000003313004613604021546 0ustar www-datawww-dataadd_subdirectory(Gestures) ./src/app/unity8/plugins/Ubuntu/Gestures/0000755000004100000410000000000013004613605020614 5ustar www-datawww-data./src/app/unity8/plugins/Ubuntu/Gestures/CMakeLists.txt0000644000004100000410000000217213004613604023355 0ustar www-datawww-data# in order to include Qt's private headers remove_definitions(-DQT_NO_KEYWORDS) set(UbuntuGesturesQml_SOURCES # plugin.cpp AxisVelocityCalculator.cpp Direction.cpp DirectionalDragArea.cpp PressedOutsideNotifier.cpp TimeSource.cpp TouchDispatcher.cpp TouchGate.cpp ) add_definitions(-DUBUNTUGESTURESQML_LIBRARY) add_library(UbuntuGesturesQml STATIC ${UbuntuGesturesQml_SOURCES}) target_link_libraries(UbuntuGesturesQml UbuntuGestures) qt5_use_modules(UbuntuGesturesQml Core Quick) # So that Foo.cpp can #include "Foo.moc" include_directories(${CMAKE_CURRENT_BINARY_DIR}) include_directories(${unity8_SOURCE_DIR}/libs/UbuntuGestures) # There's no cmake var for v8 include path :-/ so create one LIST(GET Qt5Core_INCLUDE_DIRS 0 QtCoreDir0) SET(Qt5V8_PRIVATE_INCLUDE_DIR ${QtCoreDir0}/QtV8/${Qt5Core_VERSION_STRING}/QtV8) # DANGER! DANGER! Using Qt's private API! include_directories( ${Qt5Qml_PRIVATE_INCLUDE_DIRS} ${Qt5Quick_INCLUDE_DIRS} ${Qt5Quick_PRIVATE_INCLUDE_DIRS} ${Qt5V8_PRIVATE_INCLUDE_DIR} ) #add_unity8_plugin(Ubuntu.Gestures 0.1 Ubuntu/Gestures TARGETS UbuntuGesturesQml) # TODO ./src/app/unity8/plugins/Ubuntu/Gestures/qmldir0000644000004100000410000000011313004613604022021 0ustar www-datawww-datamodule Ubuntu.Gestures plugin UbuntuGesturesQml typeinfo Gestures.qmltypes ./src/app/unity8/plugins/Ubuntu/Gestures/TouchDispatcher.h0000644000004100000410000000567013004613604024065 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 UBUNTU_TOUCH_DISPATCHER_H #define UBUNTU_TOUCH_DISPATCHER_H #include "UbuntuGesturesQmlGlobal.h" #include #include /* Dispatches touches to the given target, converting the touch point coordinates accordingly. Also takes care of synthesizing mouse events in case the target doesn't work with touch events. */ class UBUNTUGESTURESQML_EXPORT TouchDispatcher { public: TouchDispatcher(); void setTargetItem(QQuickItem *target); QQuickItem *targetItem() { return m_targetItem; } void dispatch(QEvent::Type eventType, QTouchDevice *device, Qt::KeyboardModifiers modifiers, const QList &touchPoints, QWindow *window, ulong timestamp); private: void dispatchTouchBegin( QTouchDevice *device, Qt::KeyboardModifiers modifiers, const QList &touchPoints, QWindow *window, ulong timestamp); void dispatchAsTouch(QEvent::Type eventType, QTouchDevice *device, Qt::KeyboardModifiers modifiers, const QList &touchPoints, QWindow *window, ulong timestamp); void dispatchAsMouse( QTouchDevice *device, Qt::KeyboardModifiers modifiers, const QList &touchPoints, ulong timestamp); static void transformTouchPoints(QList &touchPoints, const QTransform &transform); QTouchEvent *createQTouchEvent(QEvent::Type eventType, QTouchDevice *device, Qt::KeyboardModifiers modifiers, const QList &touchPoints, QWindow *window, ulong timestamp); QMouseEvent *touchToMouseEvent(QEvent::Type type, const QTouchEvent::TouchPoint &p, ulong timestamp, Qt::KeyboardModifiers modifiers, bool transformNeeded = true); bool checkIfDoubleClicked(ulong newPressEventTimestamp); QPointer m_targetItem; enum { NoActiveTouch, DeliveringTouchEvents, DeliveringMouseEvents, TargetRejectedTouches } m_status; int m_touchMouseId; ulong m_touchMousePressTimestamp; }; #endif // UBUNTU_TOUCH_DISPATCHER_H ./src/app/unity8/plugins/Ubuntu/Gestures/plugin.cpp0000644000004100000410000000261713004613604022623 0ustar www-datawww-data/* * Copyright (C) 2013 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "plugin.h" #include "AxisVelocityCalculator.h" #include "Direction.h" #include "DirectionalDragArea.h" #include "PressedOutsideNotifier.h" #include "TouchGate.h" #include static QObject* directionSingleton(QQmlEngine* engine, QJSEngine* scriptEngine) { Q_UNUSED(engine); Q_UNUSED(scriptEngine); return new Direction; } void UbuntuGesturesQmlPlugin::registerTypes(const char *uri) { qmlRegisterSingletonType(uri, 0, 1, "Direction", directionSingleton); qmlRegisterType(uri, 0, 1, "DirectionalDragArea"); qmlRegisterType(uri, 0, 1, "AxisVelocityCalculator"); qmlRegisterType(uri, 0, 1, "PressedOutsideNotifier"); qmlRegisterType(uri, 0, 1, "TouchGate"); } ./src/app/unity8/plugins/Ubuntu/Gestures/TouchGate.h0000644000004100000410000000642313004613604022654 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 UBUNTU_TOUCH_GATE_H #define UBUNTU_TOUCH_GATE_H #include "UbuntuGesturesQmlGlobal.h" #include "TouchDispatcher.h" #include #include #include #define TOUCHGATE_DEBUG 0 class TouchOwnershipEvent; /* Blocks the passage of events until ownership over the related touch points is granted. Blocked touch events won't be discarded. Instead they will be buffered until ownership is granted. If ownership is given to another item, the event buffer is cleared. A TouchGate is useful as a mediator for items that do not understand, or gracefully handle, touch canceling. By having a TouchGate in front of them you guarantee that only owned touches (i.e., touches that won't be canceled later) reaches them. */ class UBUNTUGESTURESQML_EXPORT TouchGate : public QQuickItem { Q_OBJECT // Item that's going to receive the touch events that make it through the gate. Q_PROPERTY(QQuickItem* targetItem READ targetItem WRITE setTargetItem NOTIFY targetItemChanged) public: bool event(QEvent *e) override; QQuickItem *targetItem() { return m_dispatcher.targetItem(); } void setTargetItem(QQuickItem *item); Q_SIGNALS: void targetItemChanged(QQuickItem *item); void pressed(); protected: void touchEvent(QTouchEvent *event) override; private: class TouchEvent { public: TouchEvent(const QTouchEvent *event); bool removeTouch(int touchId); QEvent::Type eventType; QTouchDevice *device; Qt::KeyboardModifiers modifiers; QList touchPoints; QQuickItem *target; QWindow *window; ulong timestamp; }; void touchOwnershipEvent(TouchOwnershipEvent *event); bool isTouchPointOwned(int touchId) const; void storeTouchEvent(const QTouchEvent *event); void removeTouchFromStoredEvents(int touchId); void dispatchFullyOwnedEvents(); bool eventIsFullyOwned(const TouchEvent &event) const; void dispatchTouchEventToTarget(const TouchEvent &event); void dispatchTouchEventToTarget(QTouchEvent* event); void removeTouchInfoForEndedTouches(const QList &touchPoints); #if TOUCHGATE_DEBUG QString oldestPendingTouchIdsString(); #endif QList m_storedEvents; enum { OwnershipUndefined, OwnershipRequested, OwnershipGranted, }; class TouchInfo { public: TouchInfo() {ownership = OwnershipUndefined; ended = false;} int ownership; bool ended; }; QMap m_touchInfoMap; TouchDispatcher m_dispatcher; friend class tst_TouchGate; }; #endif // UBUNTU_TOUCH_GATE_H ./src/app/unity8/plugins/Ubuntu/Gestures/Gestures.qmltypes0000644000004100000410000001277213004613604024225 0ustar www-datawww-dataimport QtQuick.tooling 1.1 // This file describes the plugin-supplied types contained in the library. // It is used for QML tooling purposes only. // // This file was auto-generated by: // 'qmlplugindump -notrelocatable Ubuntu.Gestures 0.1 plugins' Module { Component { name: "AxisVelocityCalculator" prototype: "QObject" exports: ["Ubuntu.Gestures/AxisVelocityCalculator 0.1"] exportMetaObjectRevisions: [0] Property { name: "trackedPosition"; type: "double" } Signal { name: "trackedPositionChanged" Parameter { name: "value"; type: "double" } } Method { name: "calculate"; type: "double" } Method { name: "reset" } } Component { name: "Direction" prototype: "QObject" exports: ["Ubuntu.Gestures/Direction 0.1"] isCreatable: false isSingleton: true exportMetaObjectRevisions: [0] Enum { name: "Type" values: { "Rightwards": 0, "Leftwards": 1, "Downwards": 2, "Upwards": 3, "Horizontal": 4 } } Method { name: "isHorizontal" type: "bool" Parameter { name: "type"; type: "Direction::Type" } } Method { name: "isVertical" type: "bool" Parameter { name: "type"; type: "Direction::Type" } } Method { name: "isPositive" type: "bool" Parameter { name: "type"; type: "Direction::Type" } } } Component { name: "DirectionalDragArea" defaultProperty: "data" prototype: "QQuickItem" exports: ["Ubuntu.Gestures/DirectionalDragArea 0.1"] exportMetaObjectRevisions: [0] Enum { name: "Status" values: { "WaitingForTouch": 0, "Undecided": 1, "Recognized": 2 } } Property { name: "direction"; type: "Direction::Type" } Property { name: "distance"; type: "double"; isReadonly: true } Property { name: "sceneDistance"; type: "double"; isReadonly: true } Property { name: "touchX"; type: "double"; isReadonly: true } Property { name: "touchY"; type: "double"; isReadonly: true } Property { name: "touchSceneX"; type: "double"; isReadonly: true } Property { name: "touchSceneY"; type: "double"; isReadonly: true } Property { name: "status"; type: "Status"; isReadonly: true } Property { name: "dragging"; type: "bool"; isReadonly: true } Property { name: "maxDeviation"; type: "double" } Property { name: "wideningAngle"; type: "double" } Property { name: "distanceThreshold"; type: "double" } Property { name: "minSpeed"; type: "double" } Property { name: "maxSilenceTime"; type: "int" } Property { name: "compositionTime"; type: "int" } Signal { name: "directionChanged" Parameter { name: "direction"; type: "Direction::Type" } } Signal { name: "statusChanged" Parameter { name: "value"; type: "Status" } } Signal { name: "draggingChanged" Parameter { name: "value"; type: "bool" } } Signal { name: "distanceChanged" Parameter { name: "value"; type: "double" } } Signal { name: "sceneDistanceChanged" Parameter { name: "value"; type: "double" } } Signal { name: "maxDeviationChanged" Parameter { name: "value"; type: "double" } } Signal { name: "wideningAngleChanged" Parameter { name: "value"; type: "double" } } Signal { name: "distanceThresholdChanged" Parameter { name: "value"; type: "double" } } Signal { name: "minSpeedChanged" Parameter { name: "value"; type: "double" } } Signal { name: "maxSilenceTimeChanged" Parameter { name: "value"; type: "int" } } Signal { name: "compositionTimeChanged" Parameter { name: "value"; type: "int" } } Signal { name: "touchXChanged" Parameter { name: "value"; type: "double" } } Signal { name: "touchYChanged" Parameter { name: "value"; type: "double" } } Signal { name: "touchSceneXChanged" Parameter { name: "value"; type: "double" } } Signal { name: "touchSceneYChanged" Parameter { name: "value"; type: "double" } } Signal { name: "tapped" } } Component { name: "PressedOutsideNotifier" defaultProperty: "data" prototype: "QQuickItem" exports: ["Ubuntu.Gestures/PressedOutsideNotifier 0.1"] exportMetaObjectRevisions: [0] Signal { name: "pressedOutside" } } Component { name: "TouchGate" defaultProperty: "data" prototype: "QQuickItem" exports: ["Ubuntu.Gestures/TouchGate 0.1"] exportMetaObjectRevisions: [0] Property { name: "targetItem"; type: "QQuickItem"; isPointer: true } Signal { name: "targetItemChanged" Parameter { name: "item"; type: "QQuickItem"; isPointer: true } } Signal { name: "pressed" } } } ./src/app/unity8/plugins/Ubuntu/Gestures/UbuntuGesturesQmlGlobal.h0000644000004100000410000000145113004613604025564 0ustar www-datawww-data/* * Copyright (C) 2013 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 #if defined(UBUNTUGESTURESQML_LIBRARY) # define UBUNTUGESTURESQML_EXPORT Q_DECL_EXPORT #else # define UBUNTUGESTURESQML_EXPORT Q_DECL_IMPORT #endif ./src/app/unity8/plugins/Ubuntu/Gestures/PressedOutsideNotifier.cpp0000644000004100000410000000715113004613604025765 0ustar www-datawww-data/* * Copyright (C) 2013 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "PressedOutsideNotifier.h" #include PressedOutsideNotifier::PressedOutsideNotifier(QQuickItem *parent) : QQuickItem(parent) { connect(this, &QQuickItem::enabledChanged, this, &PressedOutsideNotifier::setupOrTearDownEventFiltering); m_signalEmissionTimer.setSingleShot(true); m_signalEmissionTimer.setInterval(0); // times out on the next iteration of the event loop connect(&m_signalEmissionTimer, &QTimer::timeout, this, &PressedOutsideNotifier::pressedOutside); } bool PressedOutsideNotifier::eventFilter(QObject *watched, QEvent *event) { Q_UNUSED(watched); Q_ASSERT(watched == m_filteredWindow); // We are already going to emit pressedOutside() anyway, thus no need // for new checks. // This case takes place when a QTouchEvent comes in and isn't handled by any item, // causing QQuickWindow to synthesize a QMouseEvent out of it, which would // be filtered by us as well and count as a second press, which is wrong. if (m_signalEmissionTimer.isActive()) { return false; } switch (event->type()) { case QEvent::MouseButtonPress: { QMouseEvent *mouseEvent = static_cast(event); QPointF p = mapFromScene(mouseEvent->windowPos()); if (!contains(p)) { m_signalEmissionTimer.start(); } break; } case QEvent::TouchBegin: processFilteredTouchBegin(static_cast(event)); default: break; } // let the event be handled further return false; } void PressedOutsideNotifier::itemChange(ItemChange change, const ItemChangeData &value) { if (change == QQuickItem::ItemSceneChange) { setupOrTearDownEventFiltering(); } QQuickItem::itemChange(change, value); } void PressedOutsideNotifier::setupOrTearDownEventFiltering() { if (isEnabled() && window()) { setupEventFiltering(); } else if (m_filteredWindow) { tearDownEventFiltering(); } } void PressedOutsideNotifier::setupEventFiltering() { QQuickWindow *currentWindow = window(); Q_ASSERT(currentWindow != nullptr); if (currentWindow == m_filteredWindow) return; if (m_filteredWindow) { m_filteredWindow->removeEventFilter(this); } currentWindow->installEventFilter(this); m_filteredWindow = currentWindow; } void PressedOutsideNotifier::tearDownEventFiltering() { m_filteredWindow->removeEventFilter(this); m_filteredWindow.clear(); } void PressedOutsideNotifier::processFilteredTouchBegin(QTouchEvent *event) { const QList &touchPoints = event->touchPoints(); for (int i = 0; i < touchPoints.count(); ++i) { const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i); if (touchPoint.state() == Qt::TouchPointPressed) { QPointF p = mapFromScene(touchPoint.pos()); if (!contains(p)) { m_signalEmissionTimer.start(); return; } } } } ./src/app/unity8/plugins/Ubuntu/Gestures/Direction.h0000644000004100000410000000273013004613604022706 0ustar www-datawww-data/* * Copyright (C) 2013 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 DIRECTION_H #define DIRECTION_H #include "UbuntuGesturesQmlGlobal.h" #include /* A Direction enum wrapper so that we can do things like "direction: Direction.Righwards" from QML. */ class UBUNTUGESTURESQML_EXPORT Direction : public QObject { Q_OBJECT Q_ENUMS(Type) public: enum Type { Rightwards, // Along the positive direction of the X axis Leftwards, // Along the negative direction of the X axis Downwards, // Along the positive direction of the Y axis Upwards, // Along the negative direction of the Y axis Horizontal // Along the X axis, in any direction }; Q_INVOKABLE static bool isHorizontal(Direction::Type type); Q_INVOKABLE static bool isVertical(Direction::Type type); Q_INVOKABLE static bool isPositive(Direction::Type type); }; #endif // DIRECTION_H ./src/app/unity8/plugins/Ubuntu/Gestures/AxisVelocityCalculator.cpp0000644000004100000410000001057013004613604025757 0ustar www-datawww-data/* * Copyright (C) 2013 - Canonical Ltd. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License, as * published by the Free Software Foundation; either version 2.1 or 3.0 * of the License. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranties of * MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR * PURPOSE. See the applicable version of the GNU Lesser General Public * License for more details. * * You should have received a copy of both the GNU Lesser General Public * License along with this program. If not, see * * Authored by: Daniel d'Andrada */ #include "AxisVelocityCalculator.h" #include using namespace UbuntuGestures; AxisVelocityCalculator::AxisVelocityCalculator(QObject *parent) : AxisVelocityCalculator(SharedTimeSource(new RealTimeSource), parent) { } AxisVelocityCalculator::AxisVelocityCalculator(const SharedTimeSource &timeSource, QObject *parent) : QObject(parent) , m_timeSource(timeSource) , m_trackedPosition(0.0) { reset(); } AxisVelocityCalculator::~AxisVelocityCalculator() { } qreal AxisVelocityCalculator::trackedPosition() const { return m_trackedPosition; } void AxisVelocityCalculator::setTrackedPosition(qreal newPosition) { processMovement(newPosition - m_trackedPosition); if (newPosition != m_trackedPosition) { m_trackedPosition = newPosition; Q_EMIT trackedPositionChanged(newPosition); } } void AxisVelocityCalculator::updateIdleTime() { processMovement(0); } void AxisVelocityCalculator::processMovement(qreal movement) { if (m_samplesRead == -1) { m_samplesRead = m_samplesWrite; } else if (m_samplesRead == m_samplesWrite) { /* the oldest value is going to be overwritten. so now the oldest will be the next one. */ m_samplesRead = (m_samplesRead + 1) % MAX_SAMPLES; } m_samples[m_samplesWrite].mov = movement; m_samples[m_samplesWrite].time = m_timeSource->msecsSinceReference(); m_samplesWrite = (m_samplesWrite + 1) % MAX_SAMPLES; } qreal AxisVelocityCalculator::calculate() { if (numSamples() < MIN_SAMPLES_NEEDED) { return 0.0; } updateIdleTime(); // consider the time elapsed since the last update and now int lastIndex; if (m_samplesWrite == 0) { lastIndex = MAX_SAMPLES - 1; } else { lastIndex = m_samplesWrite - 1; } qint64 currTime = m_samples[lastIndex].time; qreal totalTime = 0; qreal totalDistance = 0; int sampleIndex = (m_samplesRead + 1) % MAX_SAMPLES; qint64 previousTime = m_samples[m_samplesRead].time; while (sampleIndex != m_samplesWrite) { // Skip this sample if it's too old if (currTime - m_samples[sampleIndex].time <= AGE_OLDEST_SAMPLE) { int deltaTime = m_samples[sampleIndex].time - previousTime; totalDistance += m_samples[sampleIndex].mov; totalTime += deltaTime; } previousTime = m_samples[sampleIndex].time; sampleIndex = (sampleIndex + 1) % MAX_SAMPLES; } return totalDistance / totalTime; } void AxisVelocityCalculator::reset() { m_samplesRead = -1; m_samplesWrite = 0; } int AxisVelocityCalculator::numSamples() const { if (m_samplesRead == -1) { return 0; } else { if (m_samplesWrite == 0) { /* consider only what's to the right of m_samplesRead (including himself) */ return MAX_SAMPLES - m_samplesRead; } else if (m_samplesWrite == m_samplesRead) { return MAX_SAMPLES; /* buffer is full */ } else if (m_samplesWrite < m_samplesRead) { return (MAX_SAMPLES - m_samplesRead) + m_samplesWrite; } else { return m_samplesWrite - m_samplesRead; } } } void AxisVelocityCalculator::setTimeSource(const SharedTimeSource &timeSource) { m_timeSource = timeSource; if (numSamples() > 0) { qWarning("AxisVelocityCalculator: changing time source while there are samples present."); // Any existent samples are based on the old time source and are, therefore, incompatible // with this new one. reset(); } } ./src/app/unity8/plugins/Ubuntu/Gestures/TimeSource.h0000644000004100000410000000312613004613604023045 0ustar www-datawww-data/* * Copyright (C) 2013 - Canonical Ltd. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License, as * published by the Free Software Foundation; either version 2.1 or 3.0 * of the License. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranties of * MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR * PURPOSE. See the applicable version of the GNU Lesser General Public * License for more details. * * You should have received a copy of both the GNU Lesser General Public * License along with this program. If not, see * * Authored by: Daniel d'Andrada */ #ifndef UBUNTUGESTURES_TIMESOURCE_H #define UBUNTUGESTURES_TIMESOURCE_H #include "UbuntuGesturesQmlGlobal.h" #include namespace UbuntuGestures { /* Interface for a time source. */ class UBUNTUGESTURESQML_EXPORT TimeSource { public: virtual ~TimeSource() {} /* Returns the current time in milliseconds since some reference time in the past. */ virtual qint64 msecsSinceReference() = 0; }; typedef QSharedPointer SharedTimeSource; /* Implementation of a time source */ class RealTimeSourcePrivate; class RealTimeSource : public TimeSource { public: RealTimeSource(); virtual ~RealTimeSource(); qint64 msecsSinceReference() override; private: RealTimeSourcePrivate *d; }; } // namespace UbuntuGestures #endif // UBUNTUGESTURES_TIMESOURCE_H ./src/app/unity8/plugins/Ubuntu/Gestures/DirectionalDragArea.h0000644000004100000410000002737413004613604024625 0ustar www-datawww-data/* * Copyright (C) 2013,2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 DIRECTIONAL_DRAG_AREA_H #define DIRECTIONAL_DRAG_AREA_H #include #include "AxisVelocityCalculator.h" #include "UbuntuGesturesQmlGlobal.h" #include "Damper.h" #include "Direction.h" // lib UbuntuGestures #include #include class TouchOwnershipEvent; class UnownedTouchEvent; /* An area that detects axis-aligned single-finger drag gestures If a drag deviates too much from the components' direction recognition will fail. It will also fail if the drag or flick is too short. E.g. a noisy or fidgety click See doc/DirectionalDragArea.svg */ class UBUNTUGESTURESQML_EXPORT DirectionalDragArea : public QQuickItem { Q_OBJECT // The direction in which the gesture should move in order to be recognized. Q_PROPERTY(Direction::Type direction READ direction WRITE setDirection NOTIFY directionChanged) // The distance travelled by the finger along the axis specified by // DirectionalDragArea's direction. Q_PROPERTY(qreal distance READ distance NOTIFY distanceChanged) // The distance travelled by the finger along the axis specified by // DirectionalDragArea's direction in scene coordinates Q_PROPERTY(qreal sceneDistance READ sceneDistance NOTIFY sceneDistanceChanged) // Position of the touch point performing the drag relative to this item. Q_PROPERTY(qreal touchX READ touchX NOTIFY touchXChanged) Q_PROPERTY(qreal touchY READ touchY NOTIFY touchYChanged) // Position of the touch point performing the drag, in scene's coordinate system Q_PROPERTY(qreal touchSceneX READ touchSceneX NOTIFY touchSceneXChanged) Q_PROPERTY(qreal touchSceneY READ touchSceneY NOTIFY touchSceneYChanged) // The current status of the directional drag gesture area. Q_PROPERTY(Status status READ status NOTIFY statusChanged) // Whether a drag gesture is taking place // This will be true as long as status is Undecided or Recognized // When a gesture gets rejected, dragging turns to false. Q_PROPERTY(bool dragging READ dragging NOTIFY draggingChanged) ///// // stuff that will be set in stone at some point // How far the touch point can move away from its expected position before // it causes a rejection in the gesture recognition. This is to compensate // for both noise in the touch input signal and for the natural irregularities // in the finger movement. // Proper value is likely device-specific. Q_PROPERTY(qreal maxDeviation READ maxDeviation WRITE setMaxDeviation NOTIFY maxDeviationChanged) // Widening angle, in degrees // It's roughly the maximum angle a touch point can make relative to the // axis defined by the compoment's direction for it to be recognized as a // directional drag. Q_PROPERTY(qreal wideningAngle READ wideningAngle WRITE setWideningAngle NOTIFY wideningAngleChanged) // How far a touch point has to move from its initial position in order for // it to be recognized as a directional drag. Q_PROPERTY(qreal distanceThreshold READ distanceThreshold WRITE setDistanceThreshold NOTIFY distanceThresholdChanged) // Minimum speed a gesture needs to have in order to be recognized as a // directional drag. // In pixels per second Q_PROPERTY(qreal minSpeed READ minSpeed WRITE setMinSpeed NOTIFY minSpeedChanged) // A gesture will be rejected if more than maxSilenceTime milliseconds has // passed since we last got an input event from it (during Undecided state). // // Silence (i.e., lack of new input events) doesn't necessarily mean that the user's // finger is still (zero drag speed). In some cases the finger might be moving but // the driver's high noise filtering might cause those silence periods, specially // in the moments succeeding a press (talking about Galaxy Nexus here). Q_PROPERTY(int maxSilenceTime READ maxSilenceTime WRITE setMaxSilenceTime NOTIFY maxSilenceTimeChanged) // ///// // Maximum time (in milliseconds) after the start of a given touch point where // subsequent touch starts are grouped with the first one into an N-touches gesture // (e.g. a two-fingers tap or drag). Q_PROPERTY(int compositionTime READ compositionTime WRITE setCompositionTime NOTIFY compositionTimeChanged) Q_ENUMS(Direction) Q_ENUMS(Status) public: DirectionalDragArea(QQuickItem *parent = 0); Direction::Type direction() const; void setDirection(Direction::Type); // Describes the state of the directional drag gesture. enum Status { // Waiting for a new touch point to land on this area. No gesture is being processed // or tracked. WaitingForTouch, // A touch point has landed on this area but it's not know yet whether it is // performing a drag in the correct direction. // If it's decided that the touch point is not performing a directional drag gesture, // it will be rejected/ignored and status will return to WaitingForTouch. Undecided, //Recognizing, // There's a touch point in this area and it performed a drag in the correct // direction. // // Once recognized, the gesture state will move back to WaitingForTouch only once // that touch point ends. The gesture will remain in the Recognized state even if // the touch point starts moving in other directions or halts. Recognized, }; Status status() const { return m_status; } qreal distance() const; qreal sceneDistance() const; void updateSceneDistance(); qreal touchX() const; qreal touchY() const; qreal touchSceneX() const; qreal touchSceneY() const; bool dragging() const { return (m_status == Undecided) || (m_status == Recognized); } qreal maxDeviation() const { return m_dampedScenePos.maxDelta(); } void setMaxDeviation(qreal value); qreal wideningAngle() const; void setWideningAngle(qreal value); qreal distanceThreshold() const { return m_distanceThreshold; } void setDistanceThreshold(qreal value); qreal minSpeed() const { return m_minSpeed; } void setMinSpeed(qreal value); int maxSilenceTime() const { return m_maxSilenceTime; } void setMaxSilenceTime(int value); int compositionTime() const { return m_compositionTime; } void setCompositionTime(int value); // Replaces the existing Timer with the given one. // // Useful for providing a fake timer when testing. void setRecognitionTimer(UbuntuGestures::AbstractTimer *timer); // Useful for testing, where a fake time source can be supplied void setTimeSource(const UbuntuGestures::SharedTimeSource &timeSource); bool event(QEvent *e) override; // Maximum time, in milliseconds, between a press and a release, for a touch // sequence to be considered a tap. int maxTapDuration() const { return 300; } Q_SIGNALS: void directionChanged(Direction::Type direction); void statusChanged(Status value); void draggingChanged(bool value); void distanceChanged(qreal value); void sceneDistanceChanged(qreal value); void maxDeviationChanged(qreal value); void wideningAngleChanged(qreal value); void distanceThresholdChanged(qreal value); void minSpeedChanged(qreal value); void maxSilenceTimeChanged(int value); void compositionTimeChanged(int value); void touchXChanged(qreal value); void touchYChanged(qreal value); void touchSceneXChanged(qreal value); void touchSceneYChanged(qreal value); // TODO: I would rather not have such signal as it has nothing to do with drag gestures. // Remove when no longer used or move its implementation to the QML code that uses it // See maxTapDuration() void tapped(); protected: virtual void touchEvent(QTouchEvent *event); private Q_SLOTS: void checkSpeed(); void giveUpIfDisabledOrInvisible(); private: void touchEvent_absent(QTouchEvent *event); void touchEvent_undecided(QTouchEvent *event); void touchEvent_recognized(QTouchEvent *event); bool pointInsideAllowedArea() const; bool movingInRightDirection() const; bool movedFarEnough(const QPointF &point) const; const QTouchEvent::TouchPoint *fetchTargetTouchPoint(QTouchEvent *event); void setStatus(Status newStatus); void setPreviousPos(const QPointF &point); void setPreviousScenePos(const QPointF &point); void updateVelocityCalculator(const QPointF &point); bool isWithinTouchCompositionWindow(); void updateSceneDirectionVector(); // returns the scalar projection between the given vector (in scene coordinates) // and m_sceneDirectionVector qreal projectOntoDirectionVector(const QPointF &sceneVector) const; void touchOwnershipEvent(TouchOwnershipEvent *event); void unownedTouchEvent(UnownedTouchEvent *event); void unownedTouchEvent_undecided(UnownedTouchEvent *unownedTouchEvent); void watchPressedTouchPoints(const QList &touchPoints); bool recognitionIsDisabled() const; void emitSignalIfTapped(); Status m_status; QPointF m_startPos; QPointF m_startScenePos; QPointF m_previousPos; QPointF m_previousScenePos; qreal m_sceneDistance; int m_touchId; // A movement damper is used in some of the gesture recognition calculations // to get rid of noise or small oscillations in the touch position. DampedPointF m_dampedScenePos; QPointF m_previousDampedScenePos; // Unit vector in scene coordinates describing the direction of the gesture recognition QPointF m_sceneDirectionVector; Direction::Type m_direction; qreal m_wideningAngle; // in degrees qreal m_wideningFactor; // it's pow(cosine(m_wideningAngle), 2) qreal m_distanceThreshold; qreal m_distanceThresholdSquared; // it's pow(m_distanceThreshold, 2) qreal m_minSpeed; int m_maxSilenceTime; // in milliseconds int m_silenceTime; // in milliseconds int m_compositionTime; // in milliseconds int m_numSamplesOnLastSpeedCheck; UbuntuGestures::AbstractTimer *m_recognitionTimer; AxisVelocityCalculator *m_velocityCalculator; UbuntuGestures::SharedTimeSource m_timeSource; // Information about an active touch point struct ActiveTouchInfo { ActiveTouchInfo() : id(-1), startTime(-1) {} bool isValid() const { return id != -1; } void reset() { id = -1; } int id; qint64 startTime; }; class ActiveTouchesInfo { public: ActiveTouchesInfo(const UbuntuGestures::SharedTimeSource &timeSource); void update(QTouchEvent *event); qint64 touchStartTime(int id); bool isEmpty() const { return m_touchInfoPool.isEmpty(); } qint64 mostRecentStartTime(); UbuntuGestures::SharedTimeSource m_timeSource; private: void addTouchPoint(int touchId); void removeTouchPoint(int touchId); #if ACTIVETOUCHESINFO_DEBUG QString toString(); #endif Pool m_touchInfoPool; } m_activeTouches; friend class tst_DirectionalDragArea; }; #endif // DIRECTIONAL_DRAG_AREA_H ./src/app/unity8/plugins/Ubuntu/Gestures/TouchDispatcher.cpp0000644000004100000410000003321613004613604024415 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "TouchDispatcher.h" #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-pedantic" #include #pragma GCC diagnostic pop #define TOUCHDISPATCHER_DEBUG 0 #if TOUCHDISPATCHER_DEBUG #include #endif TouchDispatcher::TouchDispatcher() : m_status(NoActiveTouch) , m_touchMouseId(-1) , m_touchMousePressTimestamp(0) { } void TouchDispatcher::setTargetItem(QQuickItem *target) { if (target != m_targetItem) { m_targetItem = target; if (m_status != NoActiveTouch) { qWarning("[TouchDispatcher] Changing target item in the middle of a touch stream"); m_status = TargetRejectedTouches; } } } void TouchDispatcher::dispatch(QEvent::Type eventType, QTouchDevice *device, Qt::KeyboardModifiers modifiers, const QList &touchPoints, QWindow *window, ulong timestamp) { if (m_targetItem.isNull()) { qWarning("[TouchDispatcher] Cannot dispatch touch event because target item is null"); return; } if (eventType == QEvent::TouchBegin) { dispatchTouchBegin(device, modifiers, touchPoints, window, timestamp); } else if (eventType == QEvent::TouchUpdate || eventType == QEvent::TouchEnd) { if (m_status == DeliveringTouchEvents) { dispatchAsTouch(eventType, device, modifiers, touchPoints, window, timestamp); } else if (m_status == DeliveringMouseEvents) { dispatchAsMouse(device, modifiers, touchPoints, timestamp); } else { Q_ASSERT(m_status == TargetRejectedTouches); #if TOUCHDISPATCHER_DEBUG qDebug() << "[TouchDispatcher] Not dispatching touch event to" << m_targetItem.data() << "because it already rejected the touch stream."; #endif // Do nothing } if (eventType == QEvent::TouchEnd) { m_status = NoActiveTouch; m_touchMouseId = -1; } } else { // Should never happen qCritical() << "[TouchDispatcher] Unexpected event type" << eventType; Q_ASSERT(false); return; } } void TouchDispatcher::dispatchTouchBegin( QTouchDevice *device, Qt::KeyboardModifiers modifiers, const QList &touchPoints, QWindow *window, ulong timestamp) { Q_ASSERT(m_status == NoActiveTouch); QQuickItem *targetItem = m_targetItem.data(); if (!targetItem->isEnabled() || !targetItem->isVisible()) { #if TOUCHDISPATCHER_DEBUG qDebug() << "[TouchDispatcher] Cannot dispatch touch event to" << targetItem << "because it's disabled or invisible."; #endif return; } // Map touch points to targetItem coordinates QList targetTouchPoints = touchPoints; transformTouchPoints(targetTouchPoints, QQuickItemPrivate::get(targetItem)->windowToItemTransform()); QScopedPointer touchEvent( createQTouchEvent(QEvent::TouchBegin, device, modifiers, targetTouchPoints, window, timestamp)); #if TOUCHDISPATCHER_DEBUG qDebug() << "[TouchDispatcher] dispatching" << qPrintable(touchEventToString(touchEvent.data())) << "to" << targetItem; #endif QCoreApplication::sendEvent(targetItem, touchEvent.data()); if (touchEvent->isAccepted()) { #if TOUCHDISPATCHER_DEBUG qDebug() << "[TouchDispatcher] Item accepted the touch event."; #endif m_status = DeliveringTouchEvents; } else if (targetItem->acceptedMouseButtons() & Qt::LeftButton) { #if TOUCHDISPATCHER_DEBUG qDebug() << "[TouchDispatcher] Item rejected the touch event. Trying a QMouseEvent"; #endif // NB: Arbitrarily chose the first touch point to emulate the mouse pointer QScopedPointer mouseEvent( touchToMouseEvent(QEvent::MouseButtonPress, targetTouchPoints.at(0), timestamp, modifiers, false /* transformNeeded */)); Q_ASSERT(targetTouchPoints.at(0).state() == Qt::TouchPointPressed); #if TOUCHDISPATCHER_DEBUG qDebug() << "[TouchDispatcher] dispatching" << qPrintable(mouseEventToString(mouseEvent.data())) << "to" << m_targetItem.data(); #endif QCoreApplication::sendEvent(targetItem, mouseEvent.data()); if (mouseEvent->isAccepted()) { #if TOUCHDISPATCHER_DEBUG qDebug() << "[TouchDispatcher] Item accepted the QMouseEvent."; #endif m_status = DeliveringMouseEvents; m_touchMouseId = targetTouchPoints.at(0).id(); if (checkIfDoubleClicked(timestamp)) { QScopedPointer doubleClickEvent( touchToMouseEvent(QEvent::MouseButtonDblClick, targetTouchPoints.at(0), timestamp, modifiers, false /* transformNeeded */)); #if TOUCHDISPATCHER_DEBUG qDebug() << "[TouchDispatcher] dispatching" << qPrintable(mouseEventToString(doubleClickEvent.data())) << "to" << m_targetItem.data(); #endif QCoreApplication::sendEvent(targetItem, doubleClickEvent.data()); } } else { #if TOUCHDISPATCHER_DEBUG qDebug() << "[TouchDispatcher] Item rejected the QMouseEvent."; #endif m_status = TargetRejectedTouches; } } else { #if TOUCHDISPATCHER_DEBUG qDebug() << "[TouchDispatcher] Item rejected the touch event and does not accept mouse buttons."; #endif m_status = TargetRejectedTouches; } } void TouchDispatcher::dispatchAsTouch(QEvent::Type eventType, QTouchDevice *device, Qt::KeyboardModifiers modifiers, const QList &touchPoints, QWindow *window, ulong timestamp) { QQuickItem *targetItem = m_targetItem.data(); // Map touch points to targetItem coordinates QList targetTouchPoints = touchPoints; transformTouchPoints(targetTouchPoints, QQuickItemPrivate::get(targetItem)->windowToItemTransform()); QScopedPointer eventForTargetItem( createQTouchEvent(eventType, device, modifiers, targetTouchPoints, window, timestamp)); #if TOUCHDISPATCHER_DEBUG qDebug() << "[TouchDispatcher] dispatching" << qPrintable(touchEventToString(eventForTargetItem.data())) << "to" << targetItem; #endif QCoreApplication::sendEvent(targetItem, eventForTargetItem.data()); } void TouchDispatcher::dispatchAsMouse( QTouchDevice * /*device*/, Qt::KeyboardModifiers modifiers, const QList &touchPoints, ulong timestamp) { // TODO: Detect double clicks in order to synthesize QEvent::MouseButtonDblClick events accordingly Q_ASSERT(!touchPoints.isEmpty()); const QTouchEvent::TouchPoint *touchMouse = nullptr; if (m_touchMouseId != -1) { for (int i = 0; i < touchPoints.count() && !touchMouse; ++i) { const auto &touchPoint = touchPoints.at(i); if (touchPoint.id() == m_touchMouseId) { touchMouse = &touchPoint; } } Q_ASSERT(touchMouse); if (!touchMouse) { // should not happen, but deal with it just in case. qWarning("[TouchDispatcher] Didn't find touch with id %d, used for mouse pointer emulation.", m_touchMouseId); m_touchMouseId = touchPoints.at(0).id(); touchMouse = &touchPoints.at(0); } } else { // Try to find a new touch for mouse emulation for (int i = 0; i < touchPoints.count() && !touchMouse; ++i) { const auto &touchPoint = touchPoints.at(i); if (touchPoint.state() == Qt::TouchPointPressed) { touchMouse = &touchPoint; m_touchMouseId = touchMouse->id(); } } } if (touchMouse) { QEvent::Type eventType; if (touchMouse->state() == Qt::TouchPointPressed) { eventType = QEvent::MouseButtonPress; } if (touchMouse->state() == Qt::TouchPointReleased) { eventType = QEvent::MouseButtonRelease; m_touchMouseId = -1; } else { eventType = QEvent::MouseMove; } QScopedPointer mouseEvent(touchToMouseEvent(eventType, *touchMouse, timestamp, modifiers, true /* transformNeeded */)); #if TOUCHDISPATCHER_DEBUG qDebug() << "[TouchDispatcher] dispatching" << qPrintable(mouseEventToString(mouseEvent.data())) << "to" << m_targetItem.data(); #endif QCoreApplication::sendEvent(m_targetItem.data(), mouseEvent.data()); } } QTouchEvent *TouchDispatcher::createQTouchEvent(QEvent::Type eventType, QTouchDevice *device, Qt::KeyboardModifiers modifiers, const QList &touchPoints, QWindow *window, ulong timestamp) { Qt::TouchPointStates eventStates = 0; for (int i = 0; i < touchPoints.count(); i++) eventStates |= touchPoints[i].state(); // if all points have the same state, set the event type accordingly switch (eventStates) { case Qt::TouchPointPressed: eventType = QEvent::TouchBegin; break; case Qt::TouchPointReleased: eventType = QEvent::TouchEnd; break; default: eventType = QEvent::TouchUpdate; break; } QTouchEvent *touchEvent = new QTouchEvent(eventType); touchEvent->setWindow(window); touchEvent->setTarget(m_targetItem.data()); touchEvent->setDevice(device); touchEvent->setModifiers(modifiers); touchEvent->setTouchPoints(touchPoints); touchEvent->setTouchPointStates(eventStates); touchEvent->setTimestamp(timestamp); touchEvent->accept(); return touchEvent; } // NB: From QQuickWindow void TouchDispatcher::transformTouchPoints(QList &touchPoints, const QTransform &transform) { QMatrix4x4 transformMatrix(transform); for (int i=0; imapFromScene(p.scenePos()) : p.pos(), p.scenePos(), p.screenPos(), Qt::LeftButton, (type == QEvent::MouseButtonRelease ? Qt::NoButton : Qt::LeftButton), modifiers); me->setAccepted(true); me->setTimestamp(timestamp); QVector2D transformedVelocity = p.velocity(); if (transformNeeded) { QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item); QMatrix4x4 transformMatrix(itemPrivate->windowToItemTransform()); transformedVelocity = transformMatrix.mapVector(p.velocity()).toVector2D(); } // Add these later if needed: //QGuiApplicationPrivate::setMouseEventCapsAndVelocity(me, event->device()->capabilities(), transformedVelocity); //QGuiApplicationPrivate::setMouseEventSource(me, Qt::MouseEventSynthesizedByQt); return me; } /* Copied from qquickwindow.cpp which has: Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies) Under GPL 3.0 license. */ bool TouchDispatcher::checkIfDoubleClicked(ulong newPressEventTimestamp) { bool doubleClicked; if (m_touchMousePressTimestamp == 0) { // just initialize the variable m_touchMousePressTimestamp = newPressEventTimestamp; doubleClicked = false; } else { ulong timeBetweenPresses = newPressEventTimestamp - m_touchMousePressTimestamp; ulong doubleClickInterval = static_cast(qApp->styleHints()-> mouseDoubleClickInterval()); doubleClicked = timeBetweenPresses < doubleClickInterval; if (doubleClicked) { m_touchMousePressTimestamp = 0; } else { m_touchMousePressTimestamp = newPressEventTimestamp; } } return doubleClicked; } ./src/app/unity8/plugins/Ubuntu/Gestures/TouchGate.cpp0000644000004100000410000001656413004613604023216 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "TouchGate.h" #include #include #include #include #if TOUCHGATE_DEBUG #include #endif bool TouchGate::event(QEvent *e) { if (e->type() == TouchOwnershipEvent::touchOwnershipEventType()) { touchOwnershipEvent(static_cast(e)); return true; } else { return QQuickItem::event(e); } } void TouchGate::touchEvent(QTouchEvent *event) { #if TOUCHGATE_DEBUG qDebug() << "[TouchGate] got touch event" << qPrintable(touchEventToString(event)); #endif event->accept(); const QList &touchPoints = event->touchPoints(); bool goodToGo = true; for (int i = 0; i < touchPoints.count(); ++i) { const QTouchEvent::TouchPoint &touchPoint = touchPoints[i]; if (touchPoint.state() == Qt::TouchPointPressed) { Q_ASSERT(!m_touchInfoMap.contains(touchPoint.id())); m_touchInfoMap[touchPoint.id()].ownership = OwnershipRequested; m_touchInfoMap[touchPoint.id()].ended = false; TouchRegistry::instance()->requestTouchOwnership(touchPoint.id(), this); Q_EMIT pressed(); } goodToGo &= m_touchInfoMap.contains(touchPoint.id()) && m_touchInfoMap[touchPoint.id()].ownership == OwnershipGranted; if (touchPoint.state() == Qt::TouchPointReleased && m_touchInfoMap.contains(touchPoint.id())) { m_touchInfoMap[touchPoint.id()].ended = true; } } if (goodToGo) { if (m_storedEvents.isEmpty()) { // let it pass through dispatchTouchEventToTarget(event); } else { // Retain the event to ensure TouchGate dispatches them in order. // Otherwise the current event would come before the stored ones, which are older. #if TOUCHGATE_DEBUG qDebug("[TouchGate] Storing event because thouches %s are still pending ownership.", qPrintable(oldestPendingTouchIdsString())); #endif storeTouchEvent(event); } } else { // Retain events that have unowned touches storeTouchEvent(event); } } void TouchGate::touchOwnershipEvent(TouchOwnershipEvent *event) { // TODO: Optimization: batch those actions as TouchOwnershipEvents // might come one right after the other. Q_ASSERT(m_touchInfoMap.contains(event->touchId())); TouchInfo &touchInfo = m_touchInfoMap[event->touchId()]; if (event->gained()) { #if TOUCHGATE_DEBUG qDebug() << "[TouchGate] Got ownership of touch " << event->touchId(); #endif touchInfo.ownership = OwnershipGranted; } else { #if TOUCHGATE_DEBUG qDebug() << "[TouchGate] Lost ownership of touch " << event->touchId(); #endif m_touchInfoMap.remove(event->touchId()); removeTouchFromStoredEvents(event->touchId()); } dispatchFullyOwnedEvents(); } bool TouchGate::isTouchPointOwned(int touchId) const { return m_touchInfoMap[touchId].ownership == OwnershipGranted; } void TouchGate::storeTouchEvent(const QTouchEvent *event) { #if TOUCHGATE_DEBUG qDebug() << "[TouchGate] Storing" << qPrintable(touchEventToString(event)); #endif TouchEvent clonedEvent(event); m_storedEvents.append(std::move(clonedEvent)); } void TouchGate::removeTouchFromStoredEvents(int touchId) { int i = 0; while (i < m_storedEvents.count()) { TouchEvent &event = m_storedEvents[i]; bool removed = event.removeTouch(touchId); if (removed && event.touchPoints.isEmpty()) { m_storedEvents.removeAt(i); } else { ++i; } } } void TouchGate::dispatchFullyOwnedEvents() { while (!m_storedEvents.isEmpty() && eventIsFullyOwned(m_storedEvents.first())) { TouchEvent event = m_storedEvents.takeFirst(); dispatchTouchEventToTarget(event); } } #if TOUCHGATE_DEBUG QString TouchGate::oldestPendingTouchIdsString() { Q_ASSERT(!m_storedEvents.isEmpty()); QString str; const auto &touchPoints = m_storedEvents.first().touchPoints; for (int i = 0; i < touchPoints.count(); ++i) { if (!isTouchPointOwned(touchPoints[i].id())) { if (!str.isEmpty()) { str.append(", "); } str.append(QString::number(touchPoints[i].id())); } } return str; } #endif bool TouchGate::eventIsFullyOwned(const TouchGate::TouchEvent &event) const { for (int i = 0; i < event.touchPoints.count(); ++i) { if (!isTouchPointOwned(event.touchPoints[i].id())) { return false; } } return true; } void TouchGate::setTargetItem(QQuickItem *item) { // TODO: changing the target item while dispatch of touch events is taking place will // create a mess if (item == m_dispatcher.targetItem()) return; m_dispatcher.setTargetItem(item); Q_EMIT targetItemChanged(item); } void TouchGate::dispatchTouchEventToTarget(const TouchEvent &event) { removeTouchInfoForEndedTouches(event.touchPoints); m_dispatcher.dispatch(event.eventType, event.device, event.modifiers, event.touchPoints, event.window, event.timestamp); } void TouchGate::dispatchTouchEventToTarget(QTouchEvent* event) { removeTouchInfoForEndedTouches(event->touchPoints()); m_dispatcher.dispatch(event->type(), event->device(), event->modifiers(), event->touchPoints(), event->window(), event->timestamp()); } void TouchGate::removeTouchInfoForEndedTouches(const QList &touchPoints) { for (int i = 0; i < touchPoints.size(); ++i) {\ const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i); if (touchPoint.state() == Qt::TouchPointReleased) { Q_ASSERT(m_touchInfoMap.contains(touchPoint.id())); Q_ASSERT(m_touchInfoMap[touchPoint.id()].ended); Q_ASSERT(m_touchInfoMap[touchPoint.id()].ownership == OwnershipGranted); m_touchInfoMap.remove(touchPoint.id()); } } } TouchGate::TouchEvent::TouchEvent(const QTouchEvent *event) : eventType(event->type()) , device(event->device()) , modifiers(event->modifiers()) , touchPoints(event->touchPoints()) , target(qobject_cast(event->target())) , window(event->window()) , timestamp(event->timestamp()) { } bool TouchGate::TouchEvent::removeTouch(int touchId) { bool removed = false; for (int i = 0; i < touchPoints.count() && !removed; ++i) { if (touchPoints[i].id() == touchId) { touchPoints.removeAt(i); removed = true; } } return removed; } ./src/app/unity8/plugins/Ubuntu/Gestures/TimeSource.cpp0000644000004100000410000000235013004613604023376 0ustar www-datawww-data/* * Copyright (C) 2013 - Canonical Ltd. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License, as * published by the Free Software Foundation; either version 2.1 or 3.0 * of the License. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranties of * MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR * PURPOSE. See the applicable version of the GNU Lesser General Public * License for more details. * * You should have received a copy of both the GNU Lesser General Public * License along with this program. If not, see * * Authored by: Daniel d'Andrada */ #include "TimeSource.h" #include namespace UbuntuGestures { class RealTimeSourcePrivate { public: QElapsedTimer timer; }; } using namespace UbuntuGestures; RealTimeSource::RealTimeSource() : UbuntuGestures::TimeSource() , d(new RealTimeSourcePrivate) { d->timer.start(); } RealTimeSource::~RealTimeSource() { delete d; } qint64 RealTimeSource::msecsSinceReference() { return d->timer.elapsed(); } ./src/app/unity8/plugins/Ubuntu/Gestures/Direction.cpp0000644000004100000410000000211613004613604023237 0ustar www-datawww-data/* * Copyright (C) 2013 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "Direction.h" bool Direction::isHorizontal(Direction::Type type) { return type == Direction::Leftwards || type == Direction::Rightwards || type == Direction::Horizontal; } bool Direction::isVertical(Direction::Type type) { return type == Direction::Upwards || type == Direction::Downwards; } bool Direction::isPositive(Direction::Type type) { return type == Rightwards || type == Downwards || type == Horizontal; } ./src/app/unity8/plugins/Ubuntu/Gestures/DirectionalDragArea.cpp0000644000004100000410000006655113004613604025160 0ustar www-datawww-data/* * Copyright (C) 2013-2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #define ACTIVETOUCHESINFO_DEBUG 0 #define DIRECTIONALDRAGAREA_DEBUG 0 #include "DirectionalDragArea.h" #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-pedantic" #include #pragma GCC diagnostic pop // local #include "TouchOwnershipEvent.h" #include "TouchRegistry.h" #include "UnownedTouchEvent.h" using namespace UbuntuGestures; #if DIRECTIONALDRAGAREA_DEBUG #define ddaDebug(params) qDebug().nospace() << "[DDA(" << qPrintable(objectName()) << ")] " << params #include "DebugHelpers.h" namespace { const char *statusToString(DirectionalDragArea::Status status) { if (status == DirectionalDragArea::WaitingForTouch) { return "WaitingForTouch"; } else if (status == DirectionalDragArea::Undecided) { return "Undecided"; } else { return "Recognized"; } } } // namespace { #else // DIRECTIONALDRAGAREA_DEBUG #define ddaDebug(params) ((void)0) #endif // DIRECTIONALDRAGAREA_DEBUG DirectionalDragArea::DirectionalDragArea(QQuickItem *parent) : QQuickItem(parent) , m_status(WaitingForTouch) , m_sceneDistance(0) , m_touchId(-1) , m_direction(Direction::Rightwards) , m_wideningAngle(0) , m_wideningFactor(0) , m_distanceThreshold(0) , m_distanceThresholdSquared(0.) , m_minSpeed(0) , m_maxSilenceTime(200) , m_silenceTime(0) , m_compositionTime(60) , m_numSamplesOnLastSpeedCheck(0) , m_recognitionTimer(0) , m_velocityCalculator(0) , m_timeSource(new RealTimeSource) , m_activeTouches(m_timeSource) { setRecognitionTimer(new Timer(this)); m_recognitionTimer->setInterval(60); m_recognitionTimer->setSingleShot(false); m_velocityCalculator = new AxisVelocityCalculator(this); connect(this, &QQuickItem::enabledChanged, this, &DirectionalDragArea::giveUpIfDisabledOrInvisible); connect(this, &QQuickItem::visibleChanged, this, &DirectionalDragArea::giveUpIfDisabledOrInvisible); } Direction::Type DirectionalDragArea::direction() const { return m_direction; } void DirectionalDragArea::setDirection(Direction::Type direction) { if (direction != m_direction) { m_direction = direction; Q_EMIT directionChanged(m_direction); } } void DirectionalDragArea::setMaxDeviation(qreal value) { if (m_dampedScenePos.maxDelta() != value) { m_dampedScenePos.setMaxDelta(value); Q_EMIT maxDeviationChanged(value); } } qreal DirectionalDragArea::wideningAngle() const { return m_wideningAngle; } void DirectionalDragArea::setWideningAngle(qreal angle) { if (angle == m_wideningAngle) return; m_wideningAngle = angle; // wideningFactor = pow(cosine(angle), 2) { qreal angleRadians = angle * M_PI / 180.0; m_wideningFactor = qCos(angleRadians); m_wideningFactor = m_wideningFactor * m_wideningFactor; } Q_EMIT wideningAngleChanged(angle); } void DirectionalDragArea::setDistanceThreshold(qreal value) { if (m_distanceThreshold != value) { m_distanceThreshold = value; m_distanceThresholdSquared = m_distanceThreshold * m_distanceThreshold; Q_EMIT distanceThresholdChanged(value); } } void DirectionalDragArea::setMinSpeed(qreal value) { if (m_minSpeed != value) { m_minSpeed = value; Q_EMIT minSpeedChanged(value); } } void DirectionalDragArea::setMaxSilenceTime(int value) { if (m_maxSilenceTime != value) { m_maxSilenceTime = value; Q_EMIT maxSilenceTimeChanged(value); } } void DirectionalDragArea::setCompositionTime(int value) { if (m_compositionTime != value) { m_compositionTime = value; Q_EMIT compositionTimeChanged(value); } } void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer) { int interval = 0; bool timerWasRunning = false; bool wasSingleShot = false; // can be null when called from the constructor if (m_recognitionTimer) { interval = m_recognitionTimer->interval(); timerWasRunning = m_recognitionTimer->isRunning(); if (m_recognitionTimer->parent() == this) { delete m_recognitionTimer; } } m_recognitionTimer = timer; timer->setInterval(interval); timer->setSingleShot(wasSingleShot); connect(timer, &UbuntuGestures::AbstractTimer::timeout, this, &DirectionalDragArea::checkSpeed); if (timerWasRunning) { m_recognitionTimer->start(); } } void DirectionalDragArea::setTimeSource(const SharedTimeSource &timeSource) { m_timeSource = timeSource; m_velocityCalculator->setTimeSource(timeSource); m_activeTouches.m_timeSource = timeSource; } qreal DirectionalDragArea::distance() const { if (Direction::isHorizontal(m_direction)) { return m_previousPos.x() - m_startPos.x(); } else { return m_previousPos.y() - m_startPos.y(); } } void DirectionalDragArea::updateSceneDistance() { QPointF totalMovement = m_previousScenePos - m_startScenePos; m_sceneDistance = projectOntoDirectionVector(totalMovement); } qreal DirectionalDragArea::sceneDistance() const { return m_sceneDistance; } qreal DirectionalDragArea::touchX() const { return m_previousPos.x(); } qreal DirectionalDragArea::touchY() const { return m_previousPos.y(); } qreal DirectionalDragArea::touchSceneX() const { return m_previousScenePos.x(); } qreal DirectionalDragArea::touchSceneY() const { return m_previousScenePos.y(); } bool DirectionalDragArea::event(QEvent *event) { if (event->type() == TouchOwnershipEvent::touchOwnershipEventType()) { touchOwnershipEvent(static_cast(event)); return true; } else if (event->type() == UnownedTouchEvent::unownedTouchEventType()) { unownedTouchEvent(static_cast(event)); return true; } else { return QQuickItem::event(event); } } void DirectionalDragArea::touchOwnershipEvent(TouchOwnershipEvent *event) { if (event->gained()) { QVector ids; ids.append(event->touchId()); ddaDebug("grabbing touch"); grabTouchPoints(ids); // Work around for Qt bug. If we grab a touch that is being used for mouse pointer // emulation it will cause the emulation logic to go nuts. // Thus we have to also grab the mouse in this case. // // The fix for this bug has landed in Qt 5.4 (https://codereview.qt-project.org/96887) // TODO: Remove this workaround once we start using Qt 5.4 if (window()) { QQuickWindowPrivate *windowPrivate = QQuickWindowPrivate::get(window()); if (windowPrivate->touchMouseId == event->touchId() && window()->mouseGrabberItem()) { ddaDebug("removing mouse grabber"); window()->mouseGrabberItem()->ungrabMouse(); } } } else { // We still wanna know when it ends for keeping the composition time window up-to-date TouchRegistry::instance()->addTouchWatcher(m_touchId, this); setStatus(WaitingForTouch); } } void DirectionalDragArea::unownedTouchEvent(UnownedTouchEvent *unownedTouchEvent) { QTouchEvent *event = unownedTouchEvent->touchEvent(); Q_ASSERT(!event->touchPointStates().testFlag(Qt::TouchPointPressed)); ddaDebug("Unowned " << m_timeSource->msecsSinceReference() << " " << qPrintable(touchEventToString(event))); switch (m_status) { case WaitingForTouch: // do nothing break; case Undecided: Q_ASSERT(isEnabled() && isVisible()); unownedTouchEvent_undecided(unownedTouchEvent); break; default: // Recognized: // do nothing break; } m_activeTouches.update(event); } void DirectionalDragArea::unownedTouchEvent_undecided(UnownedTouchEvent *unownedTouchEvent) { const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(unownedTouchEvent->touchEvent()); if (!touchPoint) { qCritical() << "DirectionalDragArea[status=Undecided]: touch " << m_touchId << "missing from UnownedTouchEvent without first reaching state Qt::TouchPointReleased. " "Considering it as released."; TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this); setStatus(WaitingForTouch); return; } const QPointF &touchScenePos = touchPoint->scenePos(); if (touchPoint->state() == Qt::TouchPointReleased) { // touch has ended before recognition concluded ddaDebug("Touch has ended before recognition concluded"); TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this); emitSignalIfTapped(); setStatus(WaitingForTouch); return; } m_previousDampedScenePos.setX(m_dampedScenePos.x()); m_previousDampedScenePos.setY(m_dampedScenePos.y()); m_dampedScenePos.update(touchScenePos); updateVelocityCalculator(touchScenePos); if (!pointInsideAllowedArea()) { ddaDebug("Rejecting gesture because touch point is outside allowed area."); TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this); // We still wanna know when it ends for keeping the composition time window up-to-date TouchRegistry::instance()->addTouchWatcher(m_touchId, this); setStatus(WaitingForTouch); return; } if (!movingInRightDirection()) { ddaDebug("Rejecting gesture because touch point is moving in the wrong direction."); TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this); // We still wanna know when it ends for keeping the composition time window up-to-date TouchRegistry::instance()->addTouchWatcher(m_touchId, this); setStatus(WaitingForTouch); return; } setPreviousPos(touchPoint->pos()); setPreviousScenePos(touchScenePos); if (isWithinTouchCompositionWindow()) { // There's still time for some new touch to appear and ruin our party as it would be combined // with our m_touchId one and therefore deny the possibility of a single-finger gesture. ddaDebug("Sill within composition window. Let's wait more."); return; } if (movedFarEnough(touchScenePos)) { TouchRegistry::instance()->requestTouchOwnership(m_touchId, this); setStatus(Recognized); } else { ddaDebug("Didn't move far enough yet. Let's wait more."); } } void DirectionalDragArea::touchEvent(QTouchEvent *event) { // TODO: Consider when more than one touch starts in the same event (although it's not possible // with Mir's android-input). Have to track them all. Consider it a plus/bonus. ddaDebug(m_timeSource->msecsSinceReference() << " " << qPrintable(touchEventToString(event))); if (!isEnabled() || !isVisible()) { QQuickItem::touchEvent(event); return; } switch (m_status) { case WaitingForTouch: touchEvent_absent(event); break; case Undecided: touchEvent_undecided(event); break; default: // Recognized: touchEvent_recognized(event); break; } m_activeTouches.update(event); } void DirectionalDragArea::touchEvent_absent(QTouchEvent *event) { // TODO: accept/reject is for the whole event, not per touch id. See how that affects us. if (!event->touchPointStates().testFlag(Qt::TouchPointPressed)) { // Nothing to see here. No touch starting in this event. return; } // to be proven wrong, if that's the case bool allGood = true; if (isWithinTouchCompositionWindow()) { // too close to the last touch start. So we consider them as starting roughly at the same time. // Can't be a single-touch gesture. ddaDebug("A new touch point came in but we're still within time composition window. Ignoring it."); allGood = false; } const QList &touchPoints = event->touchPoints(); const QTouchEvent::TouchPoint *newTouchPoint = nullptr; for (int i = 0; i < touchPoints.count() && allGood; ++i) { const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i); if (touchPoint.state() == Qt::TouchPointPressed) { if (newTouchPoint) { // more than one touch starting in this QTouchEvent. Can't be a single-touch gesture allGood = false; } else { // that's our candidate m_touchId = touchPoint.id(); newTouchPoint = &touchPoint; } } } if (allGood) { Q_ASSERT(newTouchPoint); m_startPos = newTouchPoint->pos(); m_startScenePos = newTouchPoint->scenePos(); m_touchId = newTouchPoint->id(); m_dampedScenePos.reset(m_startScenePos); m_velocityCalculator->setTrackedPosition(0.); m_velocityCalculator->reset(); m_numSamplesOnLastSpeedCheck = 0; m_silenceTime = 0; setPreviousPos(m_startPos); setPreviousScenePos(m_startScenePos); updateSceneDirectionVector(); if (recognitionIsDisabled()) { // Behave like a dumb TouchArea ddaDebug("Gesture recognition is disabled. Requesting touch ownership immediately."); TouchRegistry::instance()->requestTouchOwnership(m_touchId, this); setStatus(Recognized); event->accept(); } else { // just monitor the touch points for now. TouchRegistry::instance()->addCandidateOwnerForTouch(m_touchId, this); setStatus(Undecided); // Let the item below have it. We will monitor it and grab it later if a gesture // gets recognized. event->ignore(); } } else { watchPressedTouchPoints(touchPoints); event->ignore(); } } void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event) { Q_ASSERT(event->type() == QEvent::TouchBegin); Q_ASSERT(fetchTargetTouchPoint(event) == nullptr); // We're not interested in new touch points. We already have our candidate (m_touchId). // But we do want to know when those new touches end for keeping the composition time // window up-to-date event->ignore(); watchPressedTouchPoints(event->touchPoints()); if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) { // multi-finger drags are not accepted ddaDebug("Multi-finger drags are not accepted"); TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this); // We still wanna know when it ends for keeping the composition time window up-to-date TouchRegistry::instance()->addTouchWatcher(m_touchId, this); setStatus(WaitingForTouch); } } void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event) { const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event); if (!touchPoint) { qCritical() << "DirectionalDragArea[status=Recognized]: touch " << m_touchId << "missing from QTouchEvent without first reaching state Qt::TouchPointReleased. " "Considering it as released."; setStatus(WaitingForTouch); } else { setPreviousPos(touchPoint->pos()); setPreviousScenePos(touchPoint->scenePos()); if (touchPoint->state() == Qt::TouchPointReleased) { emitSignalIfTapped(); setStatus(WaitingForTouch); } } } void DirectionalDragArea::watchPressedTouchPoints(const QList &touchPoints) { for (int i = 0; i < touchPoints.count(); ++i) { const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i); if (touchPoint.state() == Qt::TouchPointPressed) { TouchRegistry::instance()->addTouchWatcher(touchPoint.id(), this); } } } bool DirectionalDragArea::recognitionIsDisabled() const { return distanceThreshold() <= 0 && compositionTime() <= 0; } void DirectionalDragArea::emitSignalIfTapped() { qint64 touchDuration = m_timeSource->msecsSinceReference() - m_activeTouches.touchStartTime(m_touchId); if (touchDuration <= maxTapDuration()) { Q_EMIT tapped(); } } const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouchEvent *event) { const QList &touchPoints = event->touchPoints(); const QTouchEvent::TouchPoint *touchPoint = 0; for (int i = 0; i < touchPoints.size(); ++i) { if (touchPoints.at(i).id() == m_touchId) { touchPoint = &touchPoints.at(i); break; } } return touchPoint; } bool DirectionalDragArea::pointInsideAllowedArea() const { // NB: Using squared values to avoid computing the square root to find // the length totalMovement QPointF totalMovement(m_dampedScenePos.x() - m_startScenePos.x(), m_dampedScenePos.y() - m_startScenePos.y()); qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() + totalMovement.y() * totalMovement.y(); if (squaredTotalMovSize == 0.) { // didn't move return true; } qreal projectedMovement = projectOntoDirectionVector(totalMovement); qreal cosineAngleSquared = (projectedMovement * projectedMovement) / squaredTotalMovSize; // Same as: // angle_between_movement_vector_and_gesture_direction_vector <= widening_angle return cosineAngleSquared >= m_wideningFactor; } bool DirectionalDragArea::movingInRightDirection() const { if (m_direction == Direction::Horizontal) { return true; } else { QPointF movementVector(m_dampedScenePos.x() - m_previousDampedScenePos.x(), m_dampedScenePos.y() - m_previousDampedScenePos.y()); qreal scalarProjection = projectOntoDirectionVector(movementVector); return scalarProjection >= 0.; } } bool DirectionalDragArea::movedFarEnough(const QPointF &point) const { if (m_distanceThreshold <= 0.) { // distance threshold check is disabled return true; } else { QPointF totalMovement(point.x() - m_startScenePos.x(), point.y() - m_startScenePos.y()); qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() + totalMovement.y() * totalMovement.y(); return squaredTotalMovSize > m_distanceThresholdSquared; } } void DirectionalDragArea::checkSpeed() { Q_ASSERT(m_status == Undecided); if (m_velocityCalculator->numSamples() >= AxisVelocityCalculator::MIN_SAMPLES_NEEDED) { qreal speed = qFabs(m_velocityCalculator->calculate()); qreal minSpeedMsecs = m_minSpeed / 1000.0; if (speed < minSpeedMsecs) { ddaDebug("Rejecting gesture because it's below minimum speed."); TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this); TouchRegistry::instance()->addTouchWatcher(m_touchId, this); setStatus(WaitingForTouch); } } if (m_velocityCalculator->numSamples() == m_numSamplesOnLastSpeedCheck) { m_silenceTime += m_recognitionTimer->interval(); if (m_silenceTime > m_maxSilenceTime) { ddaDebug("Rejecting gesture because its silence time has been exceeded."); TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this); TouchRegistry::instance()->addTouchWatcher(m_touchId, this); setStatus(WaitingForTouch); } } else { m_silenceTime = 0; } m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples(); } void DirectionalDragArea::giveUpIfDisabledOrInvisible() { if (!isEnabled() || !isVisible()) { if (m_status == Undecided) { TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this); // We still wanna know when it ends for keeping the composition time window up-to-date TouchRegistry::instance()->addTouchWatcher(m_touchId, this); } if (m_status != WaitingForTouch) { ddaDebug("Resetting status because got disabled or made invisible"); setStatus(WaitingForTouch); } } } void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus) { if (newStatus == m_status) return; DirectionalDragArea::Status oldStatus = m_status; if (oldStatus == Undecided) { m_recognitionTimer->stop(); } m_status = newStatus; Q_EMIT statusChanged(m_status); ddaDebug(statusToString(oldStatus) << " -> " << statusToString(newStatus)); switch (newStatus) { case WaitingForTouch: Q_EMIT draggingChanged(false); break; case Undecided: m_recognitionTimer->start(); Q_EMIT draggingChanged(true); break; case Recognized: if (oldStatus == WaitingForTouch) Q_EMIT draggingChanged(true); break; default: // no-op break; } } void DirectionalDragArea::setPreviousPos(const QPointF &point) { bool xChanged = m_previousPos.x() != point.x(); bool yChanged = m_previousPos.y() != point.y(); m_previousPos = point; if (xChanged) { Q_EMIT touchXChanged(point.x()); if (Direction::isHorizontal(m_direction)) Q_EMIT distanceChanged(distance()); } if (yChanged) { Q_EMIT touchYChanged(point.y()); if (Direction::isVertical(m_direction)) Q_EMIT distanceChanged(distance()); } } void DirectionalDragArea::setPreviousScenePos(const QPointF &point) { bool xChanged = m_previousScenePos.x() != point.x(); bool yChanged = m_previousScenePos.y() != point.y(); if (!xChanged && !yChanged) return; qreal oldSceneDistance = sceneDistance(); m_previousScenePos = point; updateSceneDistance(); if (oldSceneDistance != sceneDistance()) { Q_EMIT sceneDistanceChanged(sceneDistance()); } if (xChanged) { Q_EMIT touchSceneXChanged(point.x()); } if (yChanged) { Q_EMIT touchSceneYChanged(point.y()); } } void DirectionalDragArea::updateVelocityCalculator(const QPointF &scenePos) { QPointF totalSceneMovement = scenePos - m_startScenePos; qreal scalarProjection = projectOntoDirectionVector(totalSceneMovement); m_velocityCalculator->setTrackedPosition(scalarProjection); } bool DirectionalDragArea::isWithinTouchCompositionWindow() { return compositionTime() > 0 && !m_activeTouches.isEmpty() && m_timeSource->msecsSinceReference() <= m_activeTouches.mostRecentStartTime() + (qint64)compositionTime(); } //************************** ActiveTouchesInfo ************************** DirectionalDragArea::ActiveTouchesInfo::ActiveTouchesInfo(const SharedTimeSource &timeSource) : m_timeSource(timeSource) { } void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event) { if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) { // nothing to update #if ACTIVETOUCHESINFO_DEBUG qDebug("[DDA::ActiveTouchesInfo] Nothing to Update"); #endif return; } const QList &touchPoints = event->touchPoints(); for (int i = 0; i < touchPoints.count(); ++i) { const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i); if (touchPoint.state() == Qt::TouchPointPressed) { addTouchPoint(touchPoint.id()); } else if (touchPoint.state() == Qt::TouchPointReleased) { removeTouchPoint(touchPoint.id()); } } } #if ACTIVETOUCHESINFO_DEBUG QString DirectionalDragArea::ActiveTouchesInfo::toString() { QString string = "("; { QTextStream stream(&string); m_touchInfoPool.forEach([&](Pool::Iterator &touchInfo) { stream << "(id=" << touchInfo->id << ",startTime=" << touchInfo->startTime << ")"; return true; }); } string.append(")"); return string; } #endif // ACTIVETOUCHESINFO_DEBUG void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(int touchId) { ActiveTouchInfo &activeTouchInfo = m_touchInfoPool.getEmptySlot(); activeTouchInfo.id = touchId; activeTouchInfo.startTime = m_timeSource->msecsSinceReference(); #if ACTIVETOUCHESINFO_DEBUG qDebug() << "[DDA::ActiveTouchesInfo]" << qPrintable(toString()); #endif } qint64 DirectionalDragArea::ActiveTouchesInfo::touchStartTime(int touchId) { qint64 result = -1; m_touchInfoPool.forEach([&](Pool::Iterator &touchInfo) { if (touchId == touchInfo->id) { result = touchInfo->startTime; return false; } else { return true; } }); Q_ASSERT(result != -1); return result; } void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(int touchId) { m_touchInfoPool.forEach([&](Pool::Iterator &touchInfo) { if (touchId == touchInfo->id) { m_touchInfoPool.freeSlot(touchInfo); return false; } else { return true; } }); #if ACTIVETOUCHESINFO_DEBUG qDebug() << "[DDA::ActiveTouchesInfo]" << qPrintable(toString()); #endif } qint64 DirectionalDragArea::ActiveTouchesInfo::mostRecentStartTime() { Q_ASSERT(!m_touchInfoPool.isEmpty()); qint64 highestStartTime = -1; m_touchInfoPool.forEach([&](Pool::Iterator &activeTouchInfo) { if (activeTouchInfo->startTime > highestStartTime) { highestStartTime = activeTouchInfo->startTime; } return true; }); return highestStartTime; } void DirectionalDragArea::updateSceneDirectionVector() { QPointF localOrigin(0., 0.); QPointF localDirection; switch (m_direction) { case Direction::Upwards: localDirection.rx() = 0.; localDirection.ry() = -1.; break; case Direction::Downwards: localDirection.rx() = 0.; localDirection.ry() = 1; break; case Direction::Leftwards: localDirection.rx() = -1.; localDirection.ry() = 0.; break; default: // Direction::Rightwards || Direction.Horizontal localDirection.rx() = 1.; localDirection.ry() = 0.; break; } QPointF sceneOrigin = mapToScene(localOrigin); QPointF sceneDirection = mapToScene(localDirection); m_sceneDirectionVector = sceneDirection - sceneOrigin; } qreal DirectionalDragArea::projectOntoDirectionVector(const QPointF &sceneVector) const { // same as dot product as m_sceneDirectionVector is a unit vector return sceneVector.x() * m_sceneDirectionVector.x() + sceneVector.y() * m_sceneDirectionVector.y(); } ./src/app/unity8/plugins/Ubuntu/Gestures/plugin.h0000644000004100000410000000161513004613604022265 0ustar www-datawww-data/* * Copyright (C) 2013 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 PLUGIN_H #define PLUGIN_H #include class UbuntuGesturesQmlPlugin : public QQmlExtensionPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") public: void registerTypes(const char *uri); }; #endif ./src/app/unity8/plugins/Ubuntu/Gestures/Damper.h0000644000004100000410000000432413004613604022177 0ustar www-datawww-data/* * Copyright (C) 2013 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 UBUNTU_GESTURES_DAMPER_H #define UBUNTU_GESTURES_DAMPER_H #include /* Decreases the oscillations of a value along an axis. */ template class Damper { public: Damper() : m_value(0), m_maxDelta(0) { } // Maximum delta between the raw value and its dampened counterpart. void setMaxDelta(Type maxDelta) { if (maxDelta < 0) qFatal("Damper::maxDelta must be a positive number."); m_maxDelta = maxDelta; } Type maxDelta() const { return m_maxDelta; } void reset(Type value) { m_value = value; } Type update(Type value) { Type delta = value - m_value; if (delta > 0 && delta > m_maxDelta) { m_value += delta - m_maxDelta; } else if (delta < 0 && delta < -m_maxDelta) { m_value += delta + m_maxDelta; } return m_value; } Type value() const { return m_value; } private: Type m_value; Type m_maxDelta; }; /* A point that has its movement dampened. */ class DampedPointF { public: void setMaxDelta(qreal maxDelta) { m_x.setMaxDelta(maxDelta); m_y.setMaxDelta(maxDelta); } qreal maxDelta() const { return m_x.maxDelta(); } void reset(const QPointF &point) { m_x.reset(point.x()); m_y.reset(point.y()); } void update(const QPointF &point) { m_x.update(point.x()); m_y.update(point.y()); } qreal x() const { return m_x.value(); } qreal y() const { return m_y.value(); } private: Damper m_x; Damper m_y; }; #endif // UBUNTU_GESTURES_DAMPER_H ./src/app/unity8/plugins/Ubuntu/Gestures/PressedOutsideNotifier.h0000644000004100000410000000320113004613604025422 0ustar www-datawww-data/* * Copyright (C) 2013 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 PRESSED_OUTSIDE_NOTIFIER_H #define PRESSED_OUTSIDE_NOTIFIER_H #include #include #include #include #include "UbuntuGesturesGlobal.h" /* Notifies when a point, mouse or touch, is pressed outside its area. Only enable it while needed. */ class UBUNTUGESTURES_EXPORT PressedOutsideNotifier : public QQuickItem { Q_OBJECT public: PressedOutsideNotifier(QQuickItem * parent = nullptr); // From QObject bool eventFilter(QObject *watched, QEvent *event) override; Q_SIGNALS: void pressedOutside(); protected: void itemChange(ItemChange change, const ItemChangeData &value) override; private Q_SLOTS: void setupOrTearDownEventFiltering(); private: void setupEventFiltering(); void tearDownEventFiltering(); void processFilteredTouchBegin(QTouchEvent *event); QPointer m_filteredWindow; // Emits pressedOutside() signal on timeout QTimer m_signalEmissionTimer; }; #endif // PRESSED_OUTSIDE_NOTIFIER_H ./src/app/unity8/plugins/Ubuntu/Gestures/AxisVelocityCalculator.h0000644000004100000410000001002513004613604025417 0ustar www-datawww-data/* * Copyright (C) 2013 - Canonical Ltd. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License, as * published by the Free Software Foundation; either version 2.1 or 3.0 * of the License. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranties of * MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR * PURPOSE. See the applicable version of the GNU Lesser General Public * License for more details. * * You should have received a copy of both the GNU Lesser General Public * License along with this program. If not, see * * Authored by: Daniel d'Andrada */ #ifndef VELOCITY_CALCULATOR_H #define VELOCITY_CALCULATOR_H #include "UbuntuGesturesQmlGlobal.h" #include #include #include "TimeSource.h" /* Estimates the current velocity of a finger based on recent movement along an axis Taking an estimate from a reasonable number of samples, instead of only from its last movement, removes wild variations in velocity caused by the jitter normally present in input from a touchscreen. Usage example: AxisVelocityCalculator { id: velocityCalculator trackedPosition: myMouseArea.mouseX } MouseArea { id: myMouseArea onReleased: { console.log("Drag velocity along the X axis before release was: " + velocityCalculator.calculate()) } } */ class UBUNTUGESTURESQML_EXPORT AxisVelocityCalculator : public QObject { Q_OBJECT /* Position whose movement will be tracked to calculate its velocity */ Q_PROPERTY(qreal trackedPosition READ trackedPosition WRITE setTrackedPosition NOTIFY trackedPositionChanged) public: /* Regular, simple, constructor */ AxisVelocityCalculator(QObject *parent = 0); /* Constructor that takes a TimeSource */ AxisVelocityCalculator(const UbuntuGestures::SharedTimeSource &timeSource, QObject *parent = 0); virtual ~AxisVelocityCalculator(); qreal trackedPosition() const; void setTrackedPosition(qreal value); /* Calculates the finger velocity, in axis units/millisecond */ Q_INVOKABLE qreal calculate(); /* Removes all stored movements from previous calls to setTrackedPosition() */ Q_INVOKABLE void reset(); int numSamples() const; /* Replaces the TimeSource with the given one. Useful for testing purposes. */ void setTimeSource(const UbuntuGestures::SharedTimeSource &timeSource); /* The minimum amount of samples needed for a velocity calculation. */ static const int MIN_SAMPLES_NEEDED = 2; /* Maximum number of movement samples stored */ static const int MAX_SAMPLES = 50; /* Age of the oldest sample considered in the velocity calculations, in milliseconds, compared to the most recent one. */ static const int AGE_OLDEST_SAMPLE = 100; Q_SIGNALS: void trackedPositionChanged(qreal value); private: /* Inform that trackedPosition remained motionless since the time it was last changed. It's the same as calling setTrackedPosition(trackedPosition()) */ void updateIdleTime(); /* How much the finger has moved since processMovement() was last called. */ void processMovement(qreal movement); class Sample { public: qreal mov; /* movement distance since last sample */ qint64 time; /* time, in milliseconds */ }; /* a circular buffer of samples */ Sample m_samples[MAX_SAMPLES]; int m_samplesRead; /* index of the oldest sample available. -1 if buffer is empty */ int m_samplesWrite; /* index where the next sample will be written */ UbuntuGestures::SharedTimeSource m_timeSource; qreal m_trackedPosition; }; #endif // VELOCITY_CALCULATOR_H ./src/app/unity8/libs/0000755000004100000410000000000013004613605015001 5ustar www-datawww-data./src/app/unity8/libs/CMakeLists.txt0000644000004100000410000000004113004613604017533 0ustar www-datawww-dataadd_subdirectory(UbuntuGestures) ./src/app/unity8/libs/UbuntuGestures/0000755000004100000410000000000013004613605020005 5ustar www-datawww-data./src/app/unity8/libs/UbuntuGestures/DebugHelpers.cpp0000644000004100000410000000516113004613604023064 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "DebugHelpers.h" #include #include QString touchPointStateToString(Qt::TouchPointState state) { switch (state) { case Qt::TouchPointPressed: return QString("pressed"); case Qt::TouchPointMoved: return QString("moved"); case Qt::TouchPointStationary: return QString("stationary"); case Qt::TouchPointReleased: return QString("released"); default: return QString("INVALID_STATE"); } } QString touchEventToString(const QTouchEvent *ev) { QString message; switch (ev->type()) { case QEvent::TouchBegin: message.append("TouchBegin "); break; case QEvent::TouchUpdate: message.append("TouchUpdate "); break; case QEvent::TouchEnd: message.append("TouchEnd "); break; case QEvent::TouchCancel: message.append("TouchCancel "); break; default: message.append("INVALID_TOUCH_EVENT_TYPE "); } foreach(const QTouchEvent::TouchPoint& touchPoint, ev->touchPoints()) { message.append( QString("(id:%1, state:%2, scenePos:(%3,%4)) ") .arg(touchPoint.id()) .arg(touchPointStateToString(touchPoint.state())) .arg(touchPoint.scenePos().x()) .arg(touchPoint.scenePos().y()) ); } return message; } QString mouseEventToString(const QMouseEvent *ev) { QString message; switch (ev->type()) { case QEvent::MouseButtonPress: message.append("MouseButtonPress "); break; case QEvent::MouseButtonRelease: message.append("MouseButtonRelease "); break; case QEvent::MouseButtonDblClick: message.append("MouseButtonDblClick "); break; case QEvent::MouseMove: message.append("MouseMove "); break; default: message.append("INVALID_MOUSE_EVENT_TYPE "); } message.append(QString("pos(%1, %2)").arg(ev->x()).arg(ev->y())); return message; } ./src/app/unity8/libs/UbuntuGestures/TouchRegistry.cpp0000644000004100000410000004131013004613604023322 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "TouchRegistry.h" #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-pedantic" #include #pragma GCC diagnostic pop #include "CandidateInactivityTimer.h" #include "Timer.h" #include "TouchOwnershipEvent.h" #include "UnownedTouchEvent.h" #define TOUCHREGISTRY_DEBUG 0 #if TOUCHREGISTRY_DEBUG #include "DebugHelpers.h" #define UG_DEBUG qDebug() << "[TouchRegistry]" #endif // TOUCHREGISTRY_DEBUG using namespace UbuntuGestures; TouchRegistry *TouchRegistry::m_instance = nullptr; TouchRegistry::TouchRegistry(QObject *parent) : TouchRegistry(parent, new TimerFactory) { } TouchRegistry::TouchRegistry(QObject *parent, AbstractTimerFactory *timerFactory) : QObject(parent) , m_inDispatchLoop(false) , m_timerFactory(timerFactory) { if (m_instance == nullptr) { m_instance = this; } else { qFatal("Cannot have more than one instance of TouchRegistry. It must be a singleton."); } } TouchRegistry::~TouchRegistry() { Q_ASSERT(m_instance != nullptr); m_instance = nullptr; delete m_timerFactory; } void TouchRegistry::update(const QTouchEvent *event) { #if TOUCHREGISTRY_DEBUG UG_DEBUG << "got" << qPrintable(touchEventToString(event)); #endif const QList &touchPoints = event->touchPoints(); for (int i = 0; i < touchPoints.count(); ++i) { const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i); if (touchPoint.state() == Qt::TouchPointPressed) { TouchInfo &touchInfo = m_touchInfoPool.getEmptySlot(); touchInfo.init(touchPoint.id()); } else if (touchPoint.state() == Qt::TouchPointReleased) { Pool::Iterator touchInfo = findTouchInfo(touchPoint.id()); touchInfo->physicallyEnded = true; } } deliverTouchUpdatesToUndecidedCandidatesAndWatchers(event); freeEndedTouchInfos(); } void TouchRegistry::deliverTouchUpdatesToUndecidedCandidatesAndWatchers(const QTouchEvent *event) { // TODO: Look into how we could optimize this whole thing. // Although it's not really a problem as we should have at most two candidates // for each point and there should not be many active points at any given moment. // But having three nested for-loops does scare. // TODO: Don't send it to the object that is already receiving the regular event // because QQuickWindow is sending it to him (i.e., he's the touch owner from Qt's point of view) // Problem is, we cannnot easily get this information. const QList &updatedTouchPoints = event->touchPoints(); // Maps an item to the touches in this event he should be informed about. // E.g.: a QTouchEvent might have three touches but a given item might be interested in only // one of them. So he will get a UnownedTouchEvent from this QTouchEvent containing only that // touch point. QMap> touchIdsForItems; // Build touchIdsForItems m_touchInfoPool.forEach([&](Pool::Iterator &touchInfo) { if (touchInfo->isOwned() && touchInfo->watchers.isEmpty()) return true; for (int j = 0; j < updatedTouchPoints.count(); ++j) { if (updatedTouchPoints[j].id() == touchInfo->id) { if (!touchInfo->isOwned()) { for (int i = 0; i < touchInfo->candidates.count(); ++i) { CandidateInfo &candidate = touchInfo->candidates[i]; Q_ASSERT(!candidate.item.isNull()); touchIdsForItems[candidate.item.data()].append(touchInfo->id); } } const QList> &watchers = touchInfo->watchers; for (int i = 0; i < watchers.count(); ++i) { if (!watchers[i].isNull()) { touchIdsForItems[watchers[i].data()].append(touchInfo->id); } } return true; } } return true; }); // TODO: Consider what happens if an item calls any of TouchRegistry's public methods // from the event handler callback. m_inDispatchLoop = true; auto it = touchIdsForItems.constBegin(); while (it != touchIdsForItems.constEnd()) { QQuickItem *item = it.key(); const QList &touchIds = it.value(); dispatchPointsToItem(event, touchIds, item); ++it; }; m_inDispatchLoop = false; } void TouchRegistry::freeEndedTouchInfos() { m_touchInfoPool.forEach([&](Pool::Iterator &touchInfo) { if (touchInfo->ended()) { m_touchInfoPool.freeSlot(touchInfo); } return true; }); } /* Extracts the touches with the given touchIds from event and send them in a UnownedTouchEvent to the given item */ void TouchRegistry::dispatchPointsToItem(const QTouchEvent *event, const QList &touchIds, QQuickItem *item) { Qt::TouchPointStates touchPointStates = 0; QList touchPoints; const QList &allTouchPoints = event->touchPoints(); QTransform windowToCandidateTransform = QQuickItemPrivate::get(item)->windowToItemTransform(); QMatrix4x4 windowToCandidateMatrix(windowToCandidateTransform); for (int i = 0; i < allTouchPoints.count(); ++i) { const QTouchEvent::TouchPoint &originalTouchPoint = allTouchPoints[i]; if (touchIds.contains(originalTouchPoint.id())) { QTouchEvent::TouchPoint touchPoint = originalTouchPoint; translateTouchPointFromScreenToWindowCoords(touchPoint); // Set the point's local coordinates to that of the item touchPoint.setRect(windowToCandidateTransform.mapRect(touchPoint.sceneRect())); touchPoint.setStartPos(windowToCandidateTransform.map(touchPoint.startScenePos())); touchPoint.setLastPos(windowToCandidateTransform.map(touchPoint.lastScenePos())); touchPoint.setVelocity(windowToCandidateMatrix.mapVector(touchPoint.velocity()).toVector2D()); touchPoints.append(touchPoint); touchPointStates |= touchPoint.state(); } } QTouchEvent *eventForItem = new QTouchEvent(event->type(), event->device(), event->modifiers(), touchPointStates, touchPoints); eventForItem->setWindow(event->window()); eventForItem->setTimestamp(event->timestamp()); eventForItem->setTarget(event->target()); UnownedTouchEvent unownedTouchEvent(eventForItem); #if TOUCHREGISTRY_DEBUG UG_DEBUG << "Sending unowned" << qPrintable(touchEventToString(eventForItem)) << "to" << item; #endif QCoreApplication::sendEvent(item, &unownedTouchEvent); } void TouchRegistry::translateTouchPointFromScreenToWindowCoords(QTouchEvent::TouchPoint &touchPoint) { touchPoint.setScreenRect(touchPoint.sceneRect()); touchPoint.setStartScreenPos(touchPoint.startScenePos()); touchPoint.setLastScreenPos(touchPoint.lastScenePos()); touchPoint.setSceneRect(touchPoint.rect()); touchPoint.setStartScenePos(touchPoint.startPos()); touchPoint.setLastScenePos(touchPoint.lastPos()); } bool TouchRegistry::eventFilter(QObject *watched, QEvent *event) { Q_UNUSED(watched); switch (event->type()) { case QEvent::TouchBegin: case QEvent::TouchUpdate: case QEvent::TouchEnd: case QEvent::TouchCancel: update(static_cast(event)); break; default: // do nothing break; } // Do not filter out the event. i.e., let it be handled further as // we're just monitoring events return false; } void TouchRegistry::addCandidateOwnerForTouch(int id, QQuickItem *candidate) { #if TOUCHREGISTRY_DEBUG UG_DEBUG << "addCandidateOwnerForTouch id" << id << "candidate" << candidate; #endif Pool::Iterator touchInfo = findTouchInfo(id); if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); } if (touchInfo->isOwned()) { qWarning("TouchRegistry: trying to add candidate owner for a touch that's already owned"); return; } // TODO: Check if candidate already exists CandidateInfo candidateInfo; candidateInfo.undecided = true; candidateInfo.item = candidate; candidateInfo.inactivityTimer = new CandidateInactivityTimer(id, candidate, *m_timerFactory, this); connect(candidateInfo.inactivityTimer, &CandidateInactivityTimer::candidateDefaulted, this, &TouchRegistry::rejectCandidateOwnerForTouch); touchInfo->candidates.append(candidateInfo); } void TouchRegistry::addTouchWatcher(int touchId, QQuickItem *watcher) { #if TOUCHREGISTRY_DEBUG UG_DEBUG << "addTouchWatcher id" << touchId << "watcher" << watcher; #endif Pool::Iterator touchInfo = findTouchInfo(touchId); if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); } // TODO: Check if watcher already exists touchInfo->watchers.append(watcher); } void TouchRegistry::removeCandidateOwnerForTouch(int id, QQuickItem *candidate) { #if TOUCHREGISTRY_DEBUG UG_DEBUG << "removeCandidateOwnerForTouch id" << id << "candidate" << candidate; #endif Pool::Iterator touchInfo = findTouchInfo(id); if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); } int indexRemoved = -1; // TODO: check if the candidate is in fact the owner of the touch for (int i = 0; i < touchInfo->candidates.count() && indexRemoved == -1; ++i) { CandidateInfo &candidateInfo = touchInfo->candidates[i]; if (candidateInfo.item == candidate) { Q_ASSERT(i > 0 || candidateInfo.undecided); if (i == 0 && !candidateInfo.undecided) { qCritical("TouchRegistry: touch owner is being removed."); } delete candidateInfo.inactivityTimer; candidateInfo.inactivityTimer = nullptr; touchInfo->candidates.removeAt(i); indexRemoved = i; } } if (indexRemoved == 0) { // the top candidate has been removed. if the new top candidate // wants the touch let him know he's now the owner. if (touchInfo->isOwned()) { touchInfo->notifyCandidatesOfOwnershipResolution(); } } if (!m_inDispatchLoop && touchInfo->ended()) { m_touchInfoPool.freeSlot(touchInfo); } } void TouchRegistry::requestTouchOwnership(int id, QQuickItem *candidate) { #if TOUCHREGISTRY_DEBUG UG_DEBUG << "requestTouchOwnership id " << id << "candidate" << candidate; #endif Pool::Iterator touchInfo = findTouchInfo(id); if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); } Q_ASSERT(!touchInfo->isOwned()); int candidateIndex = -1; for (int i = 0; i < touchInfo->candidates.count(); ++i) { CandidateInfo &candidateInfo = touchInfo->candidates[i]; if (candidateInfo.item == candidate) { candidateInfo.undecided = false; delete candidateInfo.inactivityTimer; candidateInfo.inactivityTimer = nullptr; candidateIndex = i; break; } } // add it as a candidate if not present yet if (candidateIndex < 0) { CandidateInfo candidateInfo; candidateInfo.undecided = false; candidateInfo.item = candidate; candidateInfo.inactivityTimer = nullptr; touchInfo->candidates.append(candidateInfo); // it's the last one candidateIndex = touchInfo->candidates.count() - 1; } // If it's the top candidate it means it's now the owner. Let // it know about it. if (candidateIndex == 0) { touchInfo->notifyCandidatesOfOwnershipResolution(); } } Pool::Iterator TouchRegistry::findTouchInfo(int id) { Pool::Iterator touchInfo; m_touchInfoPool.forEach([&](Pool::Iterator &someTouchInfo) -> bool { if (someTouchInfo->id == id) { touchInfo = someTouchInfo; return false; } else { return true; } }); return touchInfo; } void TouchRegistry::rejectCandidateOwnerForTouch(int id, QQuickItem *candidate) { // NB: It's technically possible that candidate is a dangling pointer at this point. // Although that would most likely be due to a bug in our code. // In any case, only dereference it after it's confirmed that it indeed exists. #if TOUCHREGISTRY_DEBUG UG_DEBUG << "rejectCandidateOwnerForTouch id" << id << "candidate" << (void*)candidate; #endif Pool::Iterator touchInfo = findTouchInfo(id); if (!touchInfo) { #if TOUCHREGISTRY_DEBUG UG_DEBUG << "Failed to find TouchInfo for id" << id; #endif return; } int rejectedCandidateIndex = -1; // Check if the given candidate is valid and still undecided for (int i = 0; i < touchInfo->candidates.count() && rejectedCandidateIndex == -1; ++i) { CandidateInfo &candidateInfo = touchInfo->candidates[i]; if (candidateInfo.item == candidate) { Q_ASSERT(i > 0 || candidateInfo.undecided); if (i == 0 && !candidateInfo.undecided) { qCritical() << "TouchRegistry: Can't reject item (" << (void*)candidate << ") as it already owns touch" << id; return; } else { // we found the guy and it's all fine. rejectedCandidateIndex = i; } } } // If we reached this point it's because the given candidate exists and is indeed undecided. Q_ASSERT(rejectedCandidateIndex >= 0 && rejectedCandidateIndex < touchInfo->candidates.size()); { TouchOwnershipEvent lostOwnershipEvent(id, false /*gained*/); QCoreApplication::sendEvent(candidate, &lostOwnershipEvent); } touchInfo->candidates.removeAt(rejectedCandidateIndex); if (rejectedCandidateIndex == 0) { // the top candidate has been removed. if the new top candidate // wants the touch let him know he's now the owner. if (touchInfo->isOwned()) { touchInfo->notifyCandidatesOfOwnershipResolution(); } } } ////////////////////////////////////// TouchRegistry::TouchInfo //////////////////////////////////// TouchRegistry::TouchInfo::TouchInfo(int id) { init(id); } void TouchRegistry::TouchInfo::reset() { id = -1; for (int i = 0; i < candidates.count(); ++i) { CandidateInfo &candidate = candidates[i]; delete candidate.inactivityTimer; candidate.inactivityTimer.clear(); // shoundn't be needed but anyway... } } void TouchRegistry::TouchInfo::init(int id) { this->id = id; physicallyEnded = false; candidates.clear(); watchers.clear(); } bool TouchRegistry::TouchInfo::isOwned() const { return !candidates.isEmpty() && !candidates.first().undecided; } bool TouchRegistry::TouchInfo::ended() const { Q_ASSERT(isValid()); return physicallyEnded && (isOwned() || candidates.isEmpty()); } void TouchRegistry::TouchInfo::notifyCandidatesOfOwnershipResolution() { Q_ASSERT(isOwned()); #if TOUCHREGISTRY_DEBUG UG_DEBUG << "sending TouchOwnershipEvent(id =" << id << " gained) to candidate" << candidates[0].item; #endif TouchOwnershipEvent gainedOwnershipEvent(id, true /*gained*/); QCoreApplication::sendEvent(candidates[0].item, &gainedOwnershipEvent); TouchOwnershipEvent lostOwnershipEvent(id, false /*gained*/); for (int i = 1; i < candidates.count(); ++i) { #if TOUCHREGISTRY_DEBUG UG_DEBUG << "sending TouchWonershipEvent(id =" << id << " lost) to candidate" << candidates[i].item; #endif QCoreApplication::sendEvent(candidates[i].item, &lostOwnershipEvent); } } ./src/app/unity8/libs/UbuntuGestures/CMakeLists.txt0000644000004100000410000000201713004613604022544 0ustar www-datawww-data# in order to include Qt's private headers remove_definitions(-DQT_NO_KEYWORDS) set(UbuntuGestures_SOURCES CandidateInactivityTimer.cpp DebugHelpers.cpp Timer.cpp TouchOwnershipEvent.cpp TouchRegistry.cpp UnownedTouchEvent.cpp ) add_definitions(-DUBUNTUGESTURES_LIBRARY) add_library(UbuntuGestures STATIC ${UbuntuGestures_SOURCES}) qt5_use_modules(UbuntuGestures Core Quick) # So that Foo.cpp can #include "Foo.moc" include_directories(${CMAKE_CURRENT_BINARY_DIR}) # There's no cmake var for v8 include path :-/ so create one LIST(GET Qt5Core_INCLUDE_DIRS 0 QtCoreDir0) if(${Qt5Core_VERSION_STRING} VERSION_LESS "5.1.0") SET(Qt5V8_PRIVATE_INCLUDE_DIR ${QtCoreDir0}/../QtV8/${Qt5Core_VERSION_STRING}/QtV8) else() SET(Qt5V8_PRIVATE_INCLUDE_DIR ${QtCoreDir0}/QtV8/${Qt5Core_VERSION_STRING}/QtV8) endif() # DANGER! DANGER! Using Qt's private API! include_directories( ${Qt5Qml_PRIVATE_INCLUDE_DIRS} ${Qt5Quick_INCLUDE_DIRS} ${Qt5Quick_PRIVATE_INCLUDE_DIRS} ${Qt5V8_PRIVATE_INCLUDE_DIR} ) ./src/app/unity8/libs/UbuntuGestures/Timer.h0000644000004100000410000000551113004613604021237 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 UBUNTUGESTURES_TIMER_H #define UBUNTUGESTURES_TIMER_H #include "UbuntuGesturesGlobal.h" #include #include #include namespace UbuntuGestures { /* Defines an interface for a Timer. Useful for tests. */ class UBUNTUGESTURES_EXPORT AbstractTimer : public QObject { Q_OBJECT public: AbstractTimer(QObject *parent) : QObject(parent), m_isRunning(false) {} virtual int interval() const = 0; virtual void setInterval(int msecs) = 0; virtual void start() { m_isRunning = true; } virtual void stop() { m_isRunning = false; } bool isRunning() const { return m_isRunning; } virtual bool isSingleShot() const = 0; virtual void setSingleShot(bool value) = 0; Q_SIGNALS: void timeout(); private: bool m_isRunning; }; /* Essentially a QTimer wrapper */ class UBUNTUGESTURES_EXPORT Timer : public AbstractTimer { Q_OBJECT public: Timer(QObject *parent = nullptr); int interval() const override; void setInterval(int msecs) override; void start() override; void stop() override; bool isSingleShot() const override; void setSingleShot(bool value) override; private: QTimer m_timer; }; /* For tests */ class UBUNTUGESTURES_EXPORT FakeTimer : public AbstractTimer { Q_OBJECT public: FakeTimer(QObject *parent = nullptr); virtual void emitTimeout() { Q_EMIT timeout(); } int interval() const override; void setInterval(int msecs) override; bool isSingleShot() const override; void setSingleShot(bool value) override; private: int m_interval; bool m_singleShot; }; class UBUNTUGESTURES_EXPORT AbstractTimerFactory { public: virtual ~AbstractTimerFactory() {} virtual AbstractTimer *createTimer(QObject *parent = nullptr) = 0; }; class UBUNTUGESTURES_EXPORT TimerFactory : public AbstractTimerFactory { public: AbstractTimer *createTimer(QObject *parent = nullptr) override { return new Timer(parent); } }; class UBUNTUGESTURES_EXPORT FakeTimerFactory : public AbstractTimerFactory { public: AbstractTimer *createTimer(QObject *parent = nullptr) override; void makeRunningTimersTimeout(); QList> timers; }; } // namespace UbuntuGestures #endif // UBUNTUGESTURES_TIMER_H ./src/app/unity8/libs/UbuntuGestures/TouchRegistry.h0000644000004100000410000001622713004613604023000 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 UNITY_TOUCHREGISTRY_H #define UNITY_TOUCHREGISTRY_H #include #include #include #include #include #include "UbuntuGesturesGlobal.h" #include "CandidateInactivityTimer.h" #include "Pool.h" namespace UbuntuGestures { class AbstractTimerFactory; } /* Where the ownership of touches is registered. Singleton used for adding a touch point ownership model analogous to the one described in the XInput 2.2 protocol[1] on top of the existing input dispatch logic in QQuickWindow. It provides a much more flexible and powerful way of dealing with pointer ownership than the existing mechanisms in Qt. Namely QQuickItem::grabTouchPoints, QuickItem::keepTouchGrab, QQuickItem::setFiltersChildMouseEvents, QQuickItem::ungrabTouchPoints and QQuickItem::touchUngrabEvent. Usage: 1- An item receives a a new touch point. If he's not sure whether he wants it yet, he calls: TouchRegistry::instance()->addCandidateOwnerForTouch(touchId, this); touchEvent->ignore(); Ignoring the event is crucial so that it can be seen by other interested parties, which will behave similarly. 2- That item will then start receiving UnownedTouchEvents for that touch from step 1. Once he's made a decision he calls either: TouchRegistry::instance()->requestTouchOwnership(touchId, this); If he wants the touch point or: TouchRegistry::instance()->removeCandidateOwnerForTouch(touchId, this);   if he does not want it. Candidates are put in a priority queue. The first one to call addCandidateOwnerForTouch() will take precedence over the others for receiving ownership over the touch point (from now on called simply top-candidate). If the top-candidate calls requestTouchOwnership() he will immediately receive a TouchOwnershipEvent(gained=true) for that touch point. He can then safely call QQuickItem::grabTouchPoints to actually get the owned touch points. The other candidates will receive TouchOwnershipEvent(gained=false) and will no longer receive UnownedTouchEvents for that touch point. They will have to undo whatever action they were performing with that touch point. But if the top-candidate calls removeCandidateOwnerForTouch() instead, he's popped from the candidacy queue and ownership is given to the new top-most candidate if he has already made his decision, that is. The TouchRegistry cannot enforce the results of this pointer ownership negotiation (i.e., who gets to grab the touch points) as that would clash with QQuickWindow's input event dispatching logic. The candidates have to respect the decision and grab the touch points themselves. If an item wants ownership over touches as soon as he receives the TouchBegin for them, his step 1 would be instead: TouchRegistry::instance()->requestTouchOwnership(touchId, this); return true; He would then be notified once ownership has been granted to him, from which point onwards he could safely assume other TouchRegistry users wouldn't snatch this touch away from him. Items oblivious to TouchRegistry will lose their touch points without warning, just like in plain Qt. [1] - http://www.x.org/releases/X11R7.7/doc/inputproto/XI2proto.txt (see multitouch-ownership) */ class UBUNTUGESTURES_EXPORT TouchRegistry : public QObject { Q_OBJECT public: TouchRegistry(QObject *parent = nullptr); // Useful for tests, where you should feed a fake timer TouchRegistry(QObject *parent, UbuntuGestures::AbstractTimerFactory *timerFactory); virtual ~TouchRegistry(); // Returns a pointer to the application's TouchRegistry instance. // If no instance has been allocated, null is returned. static TouchRegistry *instance() { return m_instance; } void update(const QTouchEvent *event); // Calls update() if the given event is a QTouchEvent bool eventFilter(QObject *watched, QEvent *event) override; // An item that might later request ownership over the given touch point. // He will be kept informed about that touch point through UnownedTouchEvents // All candidates must eventually decide whether they want to own the touch point // or not. That decision is informed through requestTouchOwnership() or // removeCandidateOwnerForTouch() void addCandidateOwnerForTouch(int id, QQuickItem *candidate); // The same as rejecting ownership of a touch void removeCandidateOwnerForTouch(int id, QQuickItem *candidate); // The candidate object wants to be the owner of the touch with the given id. // If he's currently the oldest/top-most candidate, he will get an ownership // event immediately. If not, he will get ownership if (or once) he becomes the // top-most candidate. void requestTouchOwnership(int id, QQuickItem *candidate); // An item that has no interest (effective or potential) in owning a touch point // but would nonetheless like to be kept up-to-date on its state. void addTouchWatcher(int touchId, QQuickItem *watcherItem); private Q_SLOTS: void rejectCandidateOwnerForTouch(int id, QQuickItem *candidate); private: class CandidateInfo { public: bool undecided; // TODO: Prune candidates that become null and resolve ownership accordingly. QPointer item; QPointer inactivityTimer; }; class TouchInfo { public: TouchInfo() : id(-1) {} TouchInfo(int id); bool isValid() const { return id >= 0; } void reset(); void init(int id); int id; bool physicallyEnded; bool isOwned() const; bool ended() const; void notifyCandidatesOfOwnershipResolution(); // TODO optimize storage (s/QList/Pool) QList candidates; QList> watchers; }; Pool::Iterator findTouchInfo(int id); void deliverTouchUpdatesToUndecidedCandidatesAndWatchers(const QTouchEvent *event); static void translateTouchPointFromScreenToWindowCoords(QTouchEvent::TouchPoint &touchPoint); static void dispatchPointsToItem(const QTouchEvent *event, const QList &touchIds, QQuickItem *item); void freeEndedTouchInfos(); Pool m_touchInfoPool; // the singleton instance static TouchRegistry *m_instance; bool m_inDispatchLoop; UbuntuGestures::AbstractTimerFactory *m_timerFactory; friend class tst_TouchRegistry; friend class tst_DirectionalDragArea; }; #endif // UNITY_TOUCHREGISTRY_H ./src/app/unity8/libs/UbuntuGestures/CandidateInactivityTimer.cpp0000644000004100000410000000254513004613604025437 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "CandidateInactivityTimer.h" namespace UbuntuGestures { CandidateInactivityTimer::CandidateInactivityTimer(int touchId, QQuickItem *candidate, AbstractTimerFactory &timerFactory, QObject *parent) : QObject(parent) , m_touchId(touchId) , m_candidate(candidate) { m_timer = timerFactory.createTimer(this); connect(m_timer, &AbstractTimer::timeout, this, &CandidateInactivityTimer::onTimeout); m_timer->setInterval(durationMs); m_timer->setSingleShot(true); m_timer->start(); } void CandidateInactivityTimer::onTimeout() { qWarning("[TouchRegistry] Candidate for touch %d defaulted!", m_touchId); Q_EMIT candidateDefaulted(m_touchId, m_candidate); } } // namespace UbuntuGestures ./src/app/unity8/libs/UbuntuGestures/CandidateInactivityTimer.h0000644000004100000410000000262213004613604025100 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 UBUNTUGESTURES_CANDIDATE_INACTIVITY_TIMER_H #define UBUNTUGESTURES_CANDIDATE_INACTIVITY_TIMER_H #include class QQuickItem; #include "Timer.h" namespace UbuntuGestures { class UBUNTUGESTURES_EXPORT CandidateInactivityTimer : public QObject { Q_OBJECT public: CandidateInactivityTimer(int touchId, QQuickItem *candidate, AbstractTimerFactory &timerFactory, QObject *parent = nullptr); const int durationMs = 350; Q_SIGNALS: void candidateDefaulted(int touchId, QQuickItem *candidate); private Q_SLOTS: void onTimeout(); private: AbstractTimer *m_timer; int m_touchId; QQuickItem *m_candidate; }; } // namespace UbuntuGestures #endif // UBUNTUGESTURES_CANDIDATE_INACTIVITY_TIMER_H ./src/app/unity8/libs/UbuntuGestures/DebugHelpers.h0000644000004100000410000000205713004613604022532 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 UBUNTUGESTURES_DEBUG_HELPER_H #define UBUNTUGESTURES_DEBUG_HELPER_H #include #include "UbuntuGesturesGlobal.h" class QMouseEvent; class QTouchEvent; UBUNTUGESTURES_EXPORT QString touchPointStateToString(Qt::TouchPointState state); UBUNTUGESTURES_EXPORT QString touchEventToString(const QTouchEvent *ev); UBUNTUGESTURES_EXPORT QString mouseEventToString(const QMouseEvent *ev); #endif // UBUNTUGESTURES_DEBUG_HELPER_H ./src/app/unity8/libs/UbuntuGestures/TouchOwnershipEvent.h0000644000004100000410000000257613004613604024152 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 UBUNTU_TOUCHOWNERSHIPEVENT_H #define UBUNTU_TOUCHOWNERSHIPEVENT_H #include #include "UbuntuGesturesGlobal.h" /* When an item get an ownership event for a touch it can grab/steal that touch with a clean conscience. */ class UBUNTUGESTURES_EXPORT TouchOwnershipEvent : public QEvent { public: TouchOwnershipEvent(int touchId, bool gained); static Type touchOwnershipEventType(); /* Whether ownership was gained (true) or lost (false) */ bool gained() const { return m_gained; } /* Id of the touch whose ownership was granted. */ int touchId() const { return m_touchId; } private: static Type m_touchOwnershipType; int m_touchId; bool m_gained; }; #endif // UBUNTU_TOUCHOWNERSHIPEVENT_H ./src/app/unity8/libs/UbuntuGestures/Timer.cpp0000644000004100000410000000436013004613604021573 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "Timer.h" namespace UbuntuGestures { Timer::Timer(QObject *parent) : AbstractTimer(parent) { m_timer.setSingleShot(false); connect(&m_timer, &QTimer::timeout, this, &AbstractTimer::timeout); } int Timer::interval() const { return m_timer.interval(); } void Timer::setInterval(int msecs) { m_timer.setInterval(msecs); } void Timer::start() { m_timer.start(); AbstractTimer::start(); } void Timer::stop() { m_timer.stop(); AbstractTimer::stop(); } bool Timer::isSingleShot() const { return m_timer.isSingleShot(); } void Timer::setSingleShot(bool value) { m_timer.setSingleShot(value); } /////////////////////////////////// FakeTimer ////////////////////////////////// FakeTimer::FakeTimer(QObject *parent) : UbuntuGestures::AbstractTimer(parent) , m_interval(0) , m_singleShot(false) { } int FakeTimer::interval() const { return m_interval; } void FakeTimer::setInterval(int msecs) { m_interval = msecs; } bool FakeTimer::isSingleShot() const { return m_singleShot; } void FakeTimer::setSingleShot(bool value) { m_singleShot = value; } /////////////////////////////////// FakeTimerFactory ////////////////////////////////// AbstractTimer *FakeTimerFactory::createTimer(QObject *parent) { FakeTimer *fakeTimer = new FakeTimer(parent); timers.append(fakeTimer); return fakeTimer; } void FakeTimerFactory::makeRunningTimersTimeout() { for (int i = 0; i < timers.count(); ++i) { FakeTimer *timer = timers[i].data(); if (timer && timer->isRunning()) { timer->emitTimeout(); } } } } // namespace UbuntuGestures ./src/app/unity8/libs/UbuntuGestures/UnownedTouchEvent.h0000644000004100000410000000272313004613604023605 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 UBUNTU_UNOWNEDTOUCHEVENT_H #define UBUNTU_UNOWNEDTOUCHEVENT_H #include #include #include "UbuntuGesturesGlobal.h" /* A touch event with touch points that do not belong the item receiving it. See TouchRegistry::addCandidateOwnerForTouch and TouchRegistry::addTouchWatcher */ class UBUNTUGESTURES_EXPORT UnownedTouchEvent : public QEvent { public: UnownedTouchEvent(QTouchEvent *touchEvent); static Type unownedTouchEventType(); // TODO: It might be cleaner to store the information directly in UnownedTouchEvent // instead of carrying around a synthesized QTouchEvent. But the latter option // is very convenient. QTouchEvent *touchEvent(); private: static Type m_unownedTouchEventType; QScopedPointer m_touchEvent; }; #endif // UBUNTU_UNOWNEDTOUCHEVENT_H ./src/app/unity8/libs/UbuntuGestures/UnownedTouchEvent.cpp0000644000004100000410000000224113004613604024133 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "UnownedTouchEvent.h" QEvent::Type UnownedTouchEvent::m_unownedTouchEventType = (QEvent::Type)-1; UnownedTouchEvent::UnownedTouchEvent(QTouchEvent *touchEvent) : QEvent(unownedTouchEventType()) , m_touchEvent(touchEvent) { } QEvent::Type UnownedTouchEvent::unownedTouchEventType() { if (m_unownedTouchEventType == (QEvent::Type)-1) { m_unownedTouchEventType = (QEvent::Type)registerEventType(); } return m_unownedTouchEventType; } QTouchEvent *UnownedTouchEvent::touchEvent() { return m_touchEvent.data(); } ./src/app/unity8/libs/UbuntuGestures/Pool.h0000644000004100000410000000737313004613604021100 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 UBUNTUGESTURES_POOL_H #define UBUNTUGESTURES_POOL_H #include #include "UbuntuGesturesGlobal.h" /* An object pool. Avoids unnecessary creations/initializations and deletions/destructions of items. Useful in a scenario where items are created and destroyed very frequently but the total number of items at any given time remains small. They're stored in a unordered fashion. To be used in Pool, ItemType needs to have the following methods: - ItemType(); A constructor that takes no parameters. An object contructed with it must return false if isValid() is called. - bool isValid() const; Returns wheter the object holds a valid , "filled" state or is empty. Used by Pool to check if the slot occupied by this object is actually available. - void reset(); Resets the object to its initial, empty, state. After calling this method, isValid() must return false. */ template class Pool { public: Pool() : m_lastUsedIndex(-1) { } class Iterator { public: Iterator() : index(-1), item(nullptr) {} Iterator(int index, ItemType *item) : index(index), item(item) {} ItemType *operator->() const { return item; } ItemType &operator*() const { return *item; } ItemType &value() const { return *item; } Iterator &operator= (const Iterator& other) { index = other.index; item = other.item; // by convention, always return *this return *this; } operator bool() const { return item != nullptr; } int index; ItemType *item; }; ItemType &getEmptySlot() { Q_ASSERT(m_lastUsedIndex < m_slots.size()); // Look for an in-between vacancy first for (int i = 0; i < m_lastUsedIndex; ++i) { ItemType &item = m_slots[i]; if (!item.isValid()) { return item; } } ++m_lastUsedIndex; if (m_lastUsedIndex >= m_slots.size()) { m_slots.resize(m_lastUsedIndex + 1); } return m_slots[m_lastUsedIndex]; } void freeSlot(Iterator &iterator) { m_slots[iterator.index].reset(); if (iterator.index == m_lastUsedIndex) { do { --m_lastUsedIndex; } while (m_lastUsedIndex >= 0 && !m_slots.at(m_lastUsedIndex).isValid()); } } // Iterates through all valid items (i.e. the occupied slots) // calling the given function, with the option of ending the loop early. // // bool Func(Iterator& item) // // Returning true means it wants to continue the "for" loop, false // terminates the loop. template void forEach(Func func) { Iterator it; for (it.index = 0; it.index <= m_lastUsedIndex; ++it.index) { it.item = &m_slots[it.index]; if (!it.item->isValid()) continue; if (!func(it)) break; } } bool isEmpty() const { return m_lastUsedIndex == -1; } private: QVector m_slots; int m_lastUsedIndex; }; #endif // UBUNTUGESTURES_POOL_H ./src/app/unity8/libs/UbuntuGestures/TouchOwnershipEvent.cpp0000644000004100000410000000214313004613604024473 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "TouchOwnershipEvent.h" QEvent::Type TouchOwnershipEvent::m_touchOwnershipType = (QEvent::Type)-1; TouchOwnershipEvent::TouchOwnershipEvent(int touchId, bool gained) : QEvent(touchOwnershipEventType()) , m_touchId(touchId) , m_gained(gained) { } QEvent::Type TouchOwnershipEvent::touchOwnershipEventType() { if (m_touchOwnershipType == (QEvent::Type)-1) { m_touchOwnershipType = (QEvent::Type)registerEventType(); } return m_touchOwnershipType; } ./src/app/unity8/libs/UbuntuGestures/UbuntuGesturesGlobal.h0000644000004100000410000000144013004613604024301 0ustar www-datawww-data/* * Copyright (C) 2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 #if defined(UBUNTUGESTURES_LIBRARY) # define UBUNTUGESTURES_EXPORT Q_DECL_EXPORT #else # define UBUNTUGESTURES_EXPORT Q_DECL_IMPORT #endif ./src/app/meminfo.h0000644000004100000410000000315113004613604014412 0ustar www-datawww-data/* * Copyright 2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __MEMINFO_H__ #define __MEMINFO_H__ // Qt #include #include class MemInfo : public QObject { Q_OBJECT Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged) Q_PROPERTY(int interval READ interval WRITE setInterval NOTIFY intervalChanged) // Expressed in kB Q_PROPERTY(int total READ total NOTIFY totalChanged) Q_PROPERTY(int free READ free NOTIFY freeChanged) public: MemInfo(QObject* parent=nullptr); ~MemInfo(); const bool active() const; void setActive(bool active); const int interval() const; void setInterval(int interval); const int total() const; const int free() const; Q_SIGNALS: void activeChanged() const; void intervalChanged() const; void totalChanged() const; void freeChanged() const; private Q_SLOTS: void update(); private: QTimer m_timer; int m_total; int m_free; }; #endif // __MEMINFO_H__ ./src/app/Share.qml0000644000004100000410000000305613004613604014370 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import Ubuntu.Content 1.3 Item { id: shareItem signal done() Component { id: shareDialog ContentShareDialog { Component.onDestruction: shareItem.done() } } Component { id: contentItemComponent ContentItem {} } QtObject { id: internal function share(name, url, text, contentType) { var sharePopup = PopupUtils.open(shareDialog, shareItem, {"contentType" : contentType}) sharePopup.items.push(contentItemComponent.createObject(shareItem, {"name" : name, "url" : url, "text": text})) } } function shareLink(url, title) { internal.share(title, url, "", ContentType.Links) } function shareText(text) { internal.share("", "", text, ContentType.Text) } } ./src/app/single-instance-manager.cpp0000644000004100000410000001464413004613623020020 0ustar www-datawww-data/* * Copyright 2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ // Implementation loosely based on Chromium’s ProcessSingleton // (https://code.google.com/p/chromium/codesearch#chromium/src/chrome/browser/process_singleton_posix.cc). // Qt #include #include #include #include #include #include #include #include // local #include "single-instance-manager.h" namespace { const int kWaitForRunningInstanceToRespondMs = 1000; const int kWaitForRunningInstanceToAckMs = 1000; const int kDataStreamVersion = QDataStream::Qt_5_0; const QString kHeaderToken = QStringLiteral("MESSAGE"); const QString kAckToken = QStringLiteral("ACK"); QString getProfilePathFromAppId(const QString& appId) { QString profilePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation); QStringList appIdParts = appId.split('_', QString::SkipEmptyParts); QString appDesktopName; // We try to get the "short app name" to try to uniquely identify // the single instance profile path. // In cases where you have a single click with multiple apps in it, // the "app name" as defined in the click manifest.json file will be // a proper way to distinguish a unique instance, it needs to take // the desktop name into account. // At the moment there is no clean way to get those click app name // paths, see: // https://launchpad.net/bugs/1555542 if (appIdParts.size() >= 3) { // Assume that we have a APP_ID that corresponds to: // __ appDesktopName = appIdParts[1]; } else { // We either run on desktop or as the webbrowser appDesktopName = appIdParts.first(); } return profilePath + QDir::separator() + appDesktopName; } } SingleInstanceManager::SingleInstanceManager(QObject* parent) : QObject(parent) {} bool SingleInstanceManager::listen(const QString& name) { if (m_server.listen(name)) { connect(&m_server, SIGNAL(newConnection()), SLOT(onNewInstanceConnected())); return true; } return false; } bool SingleInstanceManager::run(const QStringList& arguments, const QString& appId) { if (m_server.isListening()) { return false; } QDir profile(getProfilePathFromAppId(appId)); if (!profile.exists()) { if (!QDir::root().mkpath(profile.absolutePath())) { qCritical() << "Failed to create profile directory," "unable to ensure a single instance of the application"; return false; } } QString name = profile.absoluteFilePath(QStringLiteral("SingletonSocket")); // XXX: Unix domain sockets limit the length of the pathname to 108 characters. // We should probably handle QAbstractSocket::HostNotFoundError explicitly. if (listen(name)) { return true; } QLocalSocket socket; socket.connectToServer(name); if (socket.waitForConnected(kWaitForRunningInstanceToRespondMs)) { qWarning() << "Passing arguments to already running instance"; QByteArray block; QDataStream message(&block, QIODevice::WriteOnly); message.setVersion(kDataStreamVersion); message << kHeaderToken; Q_FOREACH(const QString& argument, arguments) { message << argument; } socket.write(block); socket.waitForBytesWritten(); if (socket.waitForReadyRead(kWaitForRunningInstanceToAckMs)) { block = socket.readAll(); QDataStream response(&block, QIODevice::ReadOnly); response.setVersion(kDataStreamVersion); QString ack; response >> ack; if (ack != kAckToken) { qCritical() << "Received malformed ack from already running instance"; } } else { qCritical() << "Already running instance did not acknowledge message reception"; } socket.disconnectFromServer(); } else { // Failed to talk to already running instance, assume it crashed. if (QLocalServer::removeServer(name)) { if (listen(name)) { return true; } else { qCritical() << "Failed to launch single instance:" << m_server.errorString(); } } else { qCritical() << "Failed to recover from a previous crash"; } } return false; } void SingleInstanceManager::onNewInstanceConnected() { if (m_server.hasPendingConnections()) { QLocalSocket* socket = m_server.nextPendingConnection(); connect(socket, SIGNAL(readyRead()), SLOT(onReadyRead())); connect(socket, SIGNAL(disconnected()), SLOT(onDisconnected())); } } void SingleInstanceManager::onReadyRead() { QLocalSocket* socket = qobject_cast(sender()); if (!socket) { return; } QByteArray block = socket->readAll(); QDataStream message(&block, QIODevice::ReadOnly); message.setVersion(kDataStreamVersion); QStringList arguments; while (!message.atEnd()) { QString token; message >> token; arguments << token; } if (arguments.takeFirst() != kHeaderToken) { qCritical() << "Received a malformed message from another instance"; return; } Q_EMIT newInstanceLaunched(arguments); // Send ack to new instance block.clear(); QDataStream ack(&block, QIODevice::WriteOnly); ack.setVersion(kDataStreamVersion); ack << kAckToken; socket->write(block); socket->flush(); } void SingleInstanceManager::onDisconnected() { QLocalSocket* socket = qobject_cast(sender()); if (socket) { socket->deleteLater(); } } ./src/app/BrowserView.qml0000644000004100000410000000324213004613604015601 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Unity.Action 1.1 as UnityActions FocusScope { property bool developerExtrasEnabled: false property var currentWebview: null property string title: currentWebview ? currentWebview.title : "" property var initialUrls property var webbrowserWindow: null property var osk: _osk property bool hasTouchScreen: false // See http://design.canonical.com/2015/05/to-converge-onto-mobile-tablet-and-desktop-think-grid-units/ readonly property bool wide: width >= units.gu(90) focus: true property QtObject actionManager: UnityActions.ActionManager { id: unityActionManager onQuit: Qt.quit() } property alias actions: unityActionManager.actions default property alias contents: contentsItem.data property alias automaticOrientation: contentsItem.automaticOrientation OrientationHelper { id: contentsItem KeyboardRectangle { id: _osk } } } ./src/app/KeyboardRectangle.qml0000644000004100000410000000431413004613604016711 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ // This file was originally part of the telephony application. // It is a workaround required to handle interaction with the OSK, // until the shell/WM takes care of that on behalf of the applications. import QtQuick 2.4 import Ubuntu.Components 1.3 Item { id: keyboardRect anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom height: Qt.inputMethod.visible ? Qt.inputMethod.keyboardRectangle.height : 0 Behavior on height { UbuntuNumberAnimation {} } states: [ State { name: "hidden" when: keyboardRect.height == 0 }, State { name: "shown" when: keyboardRect.height == Qt.inputMethod.keyboardRectangle.height } ] function recursiveFindFocusedItem(parent) { if (parent.activeFocus) { return parent; } for (var i in parent.children) { var child = parent.children[i]; if (child.activeFocus) { return child; } var item = recursiveFindFocusedItem(child); if (item != null) { return item; } } return null; } Connections { target: Qt.inputMethod onVisibleChanged: { if (!Qt.inputMethod.visible) { var focusedItem = recursiveFindFocusedItem(keyboardRect.parent); if (focusedItem != null) { focusedItem.focus = false; } } } } } ./src/app/FileExtensionMapper.js0000644000004100000410000001213713004613604017072 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ 'use strict'; function getExtension(filename) { var filenameParts = filename.split("."); if (filenameParts.length === 1 || (filenameParts[0] === "" && filenameParts.length === 2)) { return "" } return filenameParts.pop().toLowerCase(); } // Constructed from /etc/mime.types function filenameToContentType(filename) { switch(getExtension(filename)) { case "art": case "bmp": case "cdr": case "cdt": case "cpt": case "cr2": case "crw": case "djv": case "djvu": case "erf": case "gif": case "ico": case "ief": case "jng": case "jp2": case "jpe": case "jpeg": case "jpf": case "jpg": case "jpg2": case "jpm": case "jpx": case "nef": case "orf": case "pat": case "pbm": case "pcx": case "pgm": case "png": case "pnm": case "ppm": case "psd": case "ras": case "rgb": case "svg": case "svgz": case "tif": case "tiff": case "wbmp": case "xbm": case "xpm": case "xwd": return ContentType.Pictures; case "3gp": case "asf": case "asx": case "avi": case "axv": case "dif": case "dl": case "dv": case "fli": case "flv": case "gl": case "lsf": case "lsx": case "m4v": case "mkv": case "mng": case "mov": case "movie": case "mp4": case "mpe": case "mpeg": case "mpg": case "mpv": case "mxu": case "ogv": case "qt": case "ts": case "webm": case "wm": case "wmv": case "wmx": case "wvx": return ContentType.Videos; case "aif": case "aifc": case "aiff": case "amr": case "au": case "awb": case "axa": case "csd": case "flac": case "gsm": case "kar": case "m3u": case "m4a": case "mid": case "midi": case "mp2": case "mp3": case "mpega": case "mpga": case "oga": case "ogg": case "opus": case "orc": case "pls": case "ra": case "ram": case "rm": case "sco": case "sd2": case "sid": case "snd": case "spx": case "wav": case "wax": case "wma": return ContentType.Music; case "vcard": case "vcf": return ContentType.Contacts; case "323": case "appcache": case "asc": case "bib": case "boo": case "brf": case "c": case "c++": case "cc": case "cls": case "cpp": case "csh": case "css": case "csv": case "cxx": case "d": case "diff": case "doc": case "docx": case "etx": case "gcd": case "h": case "hh": case "h++": case "hpp": case "hs": case "htc": case "htm": case "html": case "hxx": case "ics": case "icz": case "jad": case "java": case "lhs": case "ltx": case "ly": case "mml": case "moc": case "odp": case "ods": case "odt": case "p": case "pas": case "patch": case "pdf": case "pl": case "pm": case "pot": case "ppt": case "pptx": case "py": case "rtx": case "scala": case "sct": case "sfv": case "sh": case "shtml": case "srt": case "sty": case "tcl": case "tex": case "text": case "tk": case "tm": case "tsv": case "ttl": case "txt": case "uls": case "vcs": case "wml": case "wmls": case "wsc": case "xls": case "xlsx": return ContentType.Documents; case "epub": case "mobi": case "lit": case "fb2": case "azw": case "tpz": return ContentType.EBooks; default: return ContentType.Unknown; } } ./src/app/ChromeBase.qml0000644000004100000410000000312213004613604015330 0ustar www-datawww-data/* * Copyright 2014-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 // use styled item otherwise Drawer button will steal focus from the AddressBar StyledItem { id: chrome objectName: "chromeBase" property var webview property alias backgroundColor: backgroundRect.color states: [ State { name: "shown" when: chrome.y == 0 } ] Rectangle { id: backgroundRect anchors.fill: parent color: Theme.palette.normal.background Rectangle { anchors { left: parent.left right: parent.right bottom: parent.bottom } height: units.dp(1) color: "#dedede" } } ThinProgressBar { webview: chrome.webview anchors { left: parent.left right: parent.right bottom: parent.bottom } z: 2 } } ./src/app/qquickshortcut_p.h0000644000004100000410000000677713004613604016411 0ustar www-datawww-data/**************************************************************************** ** ** Copyright (C) 2015 The Qt Company Ltd. ** Contact: http://www.qt.io/licensing/ ** ** This file is part of the QtQuick module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL21$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see http://www.qt.io/terms-conditions. For further ** information use the contact form at http://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 or version 3 as published by the Free ** Software Foundation and appearing in the file LICENSE.LGPLv21 and ** LICENSE.LGPLv3 included in the packaging of this file. Please review the ** following information to ensure the GNU Lesser General Public License ** requirements will be met: https://www.gnu.org/licenses/lgpl.html and ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** As a special exception, The Qt Company gives you certain additional ** rights. These rights are described in The Qt Company LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #ifndef QQUICKSHORTCUT_P_H #define QQUICKSHORTCUT_P_H // // W A R N I N G // ------------- // // This file is not part of the Qt API. It exists purely as an // implementation detail. This header file may change from version to // version without notice, or even be removed. // // We mean it. // #include #include #include #include QT_BEGIN_NAMESPACE class QQuickShortcut : public QObject, public QQmlParserStatus { Q_OBJECT Q_INTERFACES(QQmlParserStatus) Q_PROPERTY(QVariant sequence READ sequence WRITE setSequence NOTIFY sequenceChanged FINAL) Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged FINAL) Q_PROPERTY(bool autoRepeat READ autoRepeat WRITE setAutoRepeat NOTIFY autoRepeatChanged FINAL) Q_PROPERTY(Qt::ShortcutContext context READ context WRITE setContext NOTIFY contextChanged FINAL) public: explicit QQuickShortcut(QObject *parent = Q_NULLPTR); ~QQuickShortcut(); QVariant sequence() const; void setSequence(const QVariant &sequence); bool isEnabled() const; void setEnabled(bool enabled); bool autoRepeat() const; void setAutoRepeat(bool repeat); Qt::ShortcutContext context() const; void setContext(Qt::ShortcutContext context); Q_SIGNALS: void sequenceChanged(); void enabledChanged(); void autoRepeatChanged(); void contextChanged(); void activated(); void activatedAmbiguously(); protected: void classBegin() Q_DECL_OVERRIDE; void componentComplete() Q_DECL_OVERRIDE; bool event(QEvent *event) Q_DECL_OVERRIDE; void grabShortcut(const QKeySequence &sequence, Qt::ShortcutContext context); void ungrabShortcut(); private: int m_id; bool m_enabled; bool m_completed; bool m_autorepeat; QKeySequence m_shortcut; Qt::ShortcutContext m_context; QVariant m_sequence; }; QT_END_NAMESPACE #endif // QQUICKSHORTCUT_P_H ./src/app/browserapplication.cpp0000644000004100000410000002246013004613623017227 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ // system #include #include #include // Qt #include #include #include #include #include #include #include #include #include // local #include "browserapplication.h" #include "config.h" #include "favicon-fetcher.h" #include "meminfo.h" #include "mime-database.h" #include "qquickshortcut_p.h" #include "session-storage.h" #include "webbrowser-window.h" #include "TouchRegistry.h" #include "Ubuntu/Gestures/Direction.h" #include "Ubuntu/Gestures/DirectionalDragArea.h" #include "Unity/InputInfo/qdeclarativeinputdevicemodel_p.h" BrowserApplication::BrowserApplication(int& argc, char** argv) : QApplication(argc, argv) , m_engine(0) , m_window(0) , m_component(0) , m_webbrowserWindowProxy(0) { m_arguments = arguments(); m_arguments.removeFirst(); } BrowserApplication::~BrowserApplication() { if (m_webbrowserWindowProxy) { m_webbrowserWindowProxy->setWindow(NULL); } delete m_window; delete m_webbrowserWindowProxy; delete m_component; delete m_engine; } QString BrowserApplication::inspectorPort() const { QString port; Q_FOREACH(const QString& argument, m_arguments) { if (argument == "--inspector") { // default port port = QString::number(REMOTE_INSPECTOR_PORT); break; } if (argument.startsWith("--inspector=")) { port = argument.split("--inspector=")[1]; break; } } return port; } QString BrowserApplication::inspectorHost() const { QString host; Q_FOREACH(QHostAddress address, QNetworkInterface::allAddresses()) { if (!address.isLoopback() && (address.protocol() == QAbstractSocket::IPv4Protocol)) { host = address.toString(); break; } } return host; } #define MAKE_SINGLETON_FACTORY(type) \ static QObject* type##_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine) { \ Q_UNUSED(engine); \ Q_UNUSED(scriptEngine); \ return new type(); \ } MAKE_SINGLETON_FACTORY(MemInfo) MAKE_SINGLETON_FACTORY(MimeDatabase) MAKE_SINGLETON_FACTORY(Direction) bool BrowserApplication::initialize(const QString& qmlFileSubPath , const QString& appId) { Q_ASSERT(m_window == 0); if (appId.isEmpty()) { qCritical() << "Cannot initialize the runtime environment: " "no application id detected."; return false; } if (m_arguments.contains("--help") || m_arguments.contains("-h")) { printUsage(); return false; } // Ensure that application-specific data is written where it ought to. QStringList appIdParts = appId.split('_'); QCoreApplication::setApplicationName(appIdParts.first()); QCoreApplication::setOrganizationDomain(QCoreApplication::applicationName()); // Get also the the first two components of the app ID: _, // which is needed by Online Accounts. QString unversionedAppId = QStringList(appIdParts.mid(0, 2)).join('_'); // Ensure only one instance of the app is running. // For webapps using the container as a launcher, the predicate that // is used to determine if this running instance is a duplicate of // a running one, is based on the current APP_ID. // The app id is formed as: __ // Where the is specified in the the manifest.json as // "appName" and is specific for the whole click package. // The portion is based on the desktop file name and is a short // app name. This name is meaningful when more than one desktop file is // found in a given click package. // IMPORTANT: // 1. When a click application contains more than one desktop file // the bundle is considered a single app from the point of view of the // cache and resource file locations. THOSE FILES ARE THEN SHARED between // the instances. // 2. To make sure that if more than one desktop file is found in a click package, // those apps are not considered the same instance, the instance existance predicate // is based on the AND the detailed above. if (m_singleton.run(m_arguments, appId)) { connect(&m_singleton, SIGNAL(newInstanceLaunched(const QStringList&)), SLOT(onNewInstanceLaunched(const QStringList&))); } else { return false; } bool runningConfined = true; char* label; char* mode; if (aa_getcon(&label, &mode) != -1) { if (strcmp(label, "unconfined") == 0) { runningConfined = false; } free(label); } else if (errno == EINVAL) { runningConfined = false; } QString devtoolsPort = inspectorPort(); QString devtoolsHost = inspectorHost(); bool inspectorEnabled = !devtoolsPort.isEmpty(); if (inspectorEnabled) { qputenv("UBUNTU_WEBVIEW_DEVTOOLS_HOST", devtoolsHost.toUtf8()); qputenv("UBUNTU_WEBVIEW_DEVTOOLS_PORT", devtoolsPort.toUtf8()); } const char* uri = "webbrowsercommon.private"; qmlRegisterType(uri, 0, 1, "FaviconFetcher"); qmlRegisterSingletonType(uri, 0, 1, "MemInfo", MemInfo_singleton_factory); qmlRegisterSingletonType(uri, 0, 1, "MimeDatabase", MimeDatabase_singleton_factory); qmlRegisterType(uri, 0, 1, "SessionStorage"); qmlRegisterType(uri, 0, 1, "Shortcut"); const char* gesturesUri = "Ubuntu.Gestures"; qmlRegisterSingletonType(gesturesUri, 0, 1, "Direction", Direction_singleton_factory); qmlRegisterType(gesturesUri, 0, 1, "DirectionalDragArea"); const char* inputInfoUri = "Unity.InputInfo"; qmlRegisterType(inputInfoUri, 0, 1, "InputDeviceModel"); qmlRegisterType(inputInfoUri, 0, 1, "InputInfo"); m_engine = new QQmlEngine; connect(m_engine, SIGNAL(quit()), SLOT(quit())); if (!isRunningInstalled()) { m_engine->addImportPath(UbuntuBrowserImportsDirectory()); } qmlEngineCreated(m_engine); QQmlContext* context = m_engine->rootContext(); context->setContextProperty("__runningConfined", runningConfined); context->setContextProperty("unversionedAppId", unversionedAppId); m_component = new QQmlComponent(m_engine); m_component->loadUrl(QUrl::fromLocalFile(UbuntuBrowserDirectory() + "/" + qmlFileSubPath)); if (!m_component->isReady()) { qWarning() << m_component->errorString(); return false; } m_webbrowserWindowProxy = new WebBrowserWindow(); context->setContextProperty("webbrowserWindowProxy", m_webbrowserWindowProxy); QObject* browser = m_component->beginCreate(context); m_window = qobject_cast(browser); m_webbrowserWindowProxy->setWindow(m_window); m_window->installEventFilter(new TouchRegistry(this)); browser->setProperty("developerExtrasEnabled", inspectorEnabled); browser->setProperty("forceFullscreen", m_arguments.contains("--fullscreen")); bool hasTouchScreen = false; Q_FOREACH(const QTouchDevice* device, QTouchDevice::devices()) { if (device->type() == QTouchDevice::TouchScreen) { hasTouchScreen = true; } } browser->setProperty("hasTouchScreen", hasTouchScreen); return true; } void BrowserApplication::onNewInstanceLaunched(const QStringList& arguments) const { QVariantList urls; Q_FOREACH(const QString& argument, arguments) { if (!argument.startsWith(QStringLiteral("-"))) { QUrl url = QUrl::fromUserInput(argument); if (url.isValid()) { urls.append(url); } } } QMetaObject::invokeMethod(m_window, "openUrls", Q_ARG(QVariant, QVariant(urls))); m_window->requestActivate(); } void BrowserApplication::qmlEngineCreated(QQmlEngine*) {} int BrowserApplication::run() { Q_ASSERT(m_window != 0); if (m_arguments.contains("--fullscreen")) { m_window->showFullScreen(); } else if (m_arguments.contains("--maximized")) { m_window->showMaximized(); } else { m_window->show(); } return exec(); } QList BrowserApplication::urls() const { QList urls; Q_FOREACH(const QString& argument, m_arguments) { if (!argument.startsWith("-")) { QUrl url = QUrl::fromUserInput(argument); if (url.isValid()) { urls.append(url); } } } return urls; } ./src/app/browserapplication.h0000644000004100000410000000367613004613623016704 0ustar www-datawww-data/* * Copyright 2013-2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 __BROWSER_APPLICATION_H__ #define __BROWSER_APPLICATION_H__ // Qt #include #include #include #include #include // local #include "single-instance-manager.h" class QQmlComponent; class QQmlEngine; class QQuickWindow; class WebBrowserWindow; // We want the browser to be QApplication based rather than QGuiApplication // to provide a widget based file picker on the desktop, rather than the // QML fall back picker. class BrowserApplication : public QApplication { Q_OBJECT public: BrowserApplication(int& argc, char** argv); ~BrowserApplication(); bool initialize(const QString& qmlFileSubPath, const QString& appId); int run(); protected: virtual void printUsage() const = 0; virtual QList urls() const; virtual void qmlEngineCreated(QQmlEngine*); QStringList m_arguments; QQmlEngine* m_engine; QQuickWindow* m_window; QQmlComponent* m_component; private Q_SLOTS: void onNewInstanceLaunched(const QStringList& arguments) const; private: QString inspectorPort() const; QString inspectorHost() const; WebBrowserWindow *m_webbrowserWindowProxy; SingleInstanceManager m_singleton; }; #endif // __BROWSER_APPLICATION_H__ ./src/app/meminfo.cpp0000644000004100000410000000721213004613604014747 0ustar www-datawww-data/* * Copyright 2016 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "meminfo.h" // Qt #include #include #include #include #include MemInfo::MemInfo(QObject* parent) : QObject(parent) , m_total(0) , m_free(0) { // Default interval: 5000 ms m_timer.setInterval(5000); connect(&m_timer, SIGNAL(timeout()), SLOT(update())); // Active by default m_timer.start(); } MemInfo::~MemInfo() {} const bool MemInfo::active() const { return m_timer.isActive(); } void MemInfo::setActive(bool active) { if (active != m_timer.isActive()) { if (active) { m_timer.start(); } else { m_timer.stop(); } Q_EMIT activeChanged(); } } const int MemInfo::interval() const { return m_timer.interval(); } void MemInfo::setInterval(int interval) { if (interval != m_timer.interval()) { m_timer.setInterval(interval); Q_EMIT intervalChanged(); } } const int MemInfo::total() const { return m_total; } const int MemInfo::free() const { return m_free; } void MemInfo::update() { #if defined(Q_OS_LINUX) // Inspired by glibtop_get_mem_s() QFile meminfo(QStringLiteral("/proc/meminfo")); if (!meminfo.open(QIODevice::ReadOnly)) { return; } static QRegExp memTotalRegexp(QStringLiteral("MemTotal:\\s*(\\d+) kB\\n")); static QRegExp memFreeRegexp(QStringLiteral("MemFree:\\s*(\\d+) kB\\n")); static QRegExp buffersRegexp(QStringLiteral("Buffers:\\s*(\\d+) kB\\n")); static QRegExp cachedRegexp(QStringLiteral("Cached:\\s*(\\d+) kB\\n")); int parsedTotal = -1; int parsedFree = -1; int parsedBuffers = -1; int parsedCached = -1; while ((parsedTotal == -1) || (parsedFree == -1) || (parsedBuffers == -1) || (parsedCached == -1)) { QByteArray line = meminfo.readLine(); if (line.isEmpty()) { break; } if (memTotalRegexp.exactMatch(line)) { parsedTotal = memTotalRegexp.cap(1).toInt(); } else if (memFreeRegexp.exactMatch(line)) { parsedFree = memFreeRegexp.cap(1).toInt(); } else if (buffersRegexp.exactMatch(line)) { parsedBuffers = buffersRegexp.cap(1).toInt(); } else if (cachedRegexp.exactMatch(line)) { parsedCached = cachedRegexp.cap(1).toInt(); } } meminfo.close(); if ((parsedTotal != -1) && (parsedFree != -1) && (parsedBuffers != -1) && (parsedCached != -1)) { bool totalUpdated = false; if (parsedTotal != m_total) { m_total = parsedTotal; totalUpdated = true; } bool freeUpdated = false; int newFree = parsedFree + parsedCached + parsedBuffers; if (newFree != m_free) { m_free = newFree; freeUpdated = true; } if (totalUpdated) { Q_EMIT totalChanged(); } if (freeUpdated) { Q_EMIT freeChanged(); } } #endif // Q_OS_LINUX } ./src/app/AlertDialog.qml0000644000004100000410000000154013004613604015511 0ustar www-datawww-data/* * Copyright 2013-2015 Canonical Ltd. * * This file is part of webbrowser-app. * * webbrowser-app is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * webbrowser-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import Ubuntu.Components 1.3 ModalDialog { title: i18n.tr("JavaScript Alert") Button { text: i18n.tr("OK") onClicked: model.accept() } } ./screenshot.png0000644000004100000410000167552413004613604014147 0ustar www-datawww-dataPNG  IHDR 0n3bKGD pHYs  tIME !zIDATxw`F0pKgBvHBJ٣@P -QJi){۲7aoKz_~8>,N'D@M aaaBvaX=F_zoeerQE AjP(tq;v}Fi]0qEGGŵi4'|2n8  ? q\T/o޼3 жx<^Ν5ouuujjA Ph4uZwVVVcƌp8O}z.\P^^*r׮]ۼyJ"𒒒Z]+Wl޼ΎNɚfq\a`jrP(Ά n\\\k!wWppgϊR}ddݒDAK|D"  ԩSgϞ}Lf sΗ/_YݻoruuM~7ݒ?_~0a~3f5jʕϟP(No&O|W^A>ZǏ"""߿V񰰰ӧ_re˖-=mo/~01 #)..޻w%K~իW7we@@J}.Nb2~~~, EQF#xSN111VVVXL& 77򸸸.]xzz6֤ VMLLLOOj={${ܛs\666999ƺd? P(Mzʬd24`lvKNÿ+e ңG;VWW߸qC,EFFgff6d׮]M6 :}ٲe˦M8`bj~tEP N81o>r۷qwrrҥK3Z[[ÇSw_#8^]]gccZPPPTTD`EDD׏9믿n,ڽ{w^[d&ҤcbtW R>|_~jp:] x@qX\RRVi4JmF#V```@@{tLLy`0|>|H޽{SLݻÇ[8Aqqqjjʕ+L&yG\bE׬Ysm-R(vߟN# HuuP(h4,‚`t-((ɓ'NHƋNNN?*,, h8SԪgϞ`0 nT;d2‹ hٖ ֍Ef(.\?,(( []\\ |H$/..6j*@pAiw{n\޽{wuypׯLNN}pXN#Âb ҹs簰0$!!A[-ZYY(ڹsg;;;]uA&M4uT6rb2J_ҥX,o޳BA ƍi4ڲe V3c ӧ R4##͛7mΝy<ހݻגeVVVC yEjjjrrr^^^mmm޼yCիm1{uy`}0f#RPP`NcQgٓ'O...sH$pwwb2 B,ANo~]5brrr S$E$I$lѣ?s/ԩV L& u]v}iYY YԱ$ h4RI>s$#ݭMX;::Z?7j/"HƏ/ ׭['pСCqq |ݻwbcckjjZ>9Յ޽K/bZ}E\h:uo> T*ǎKx<&LxBP"8`0h4BP(?'5|mrZP(<''GsvjZT4+Bmlltzk[25c)lR-?lҫ<}VjT*ՕNlPXQQj4V #G|InnRd2111;v|+!OFA\On{bXt:=&&&))ʕ+YYY^^^~ڵk t~r9J222r?.`hjucc w-J5O}<<}.]DkrzQ:Nv%urrڻwo /ܸqc̙r~Z`? HihPqq7,,,u).Zh~;͛kkazhEPNVS( 999B {<{,33eeeI$ J%LGnٲ^RQ([[[+++ jjjZ="{w9::<RiPPI ^ٴi98n<Ӧߔ)S?~(1i$F CCCtׯ_z\R[[KذX,ed H^;ѣGuܹwXvSB1?QojC-Pr GipgJu']nػwoөak׮}ի[2 Ç={EVVO?TZZJP,--L&rtth4D 1hi3-]WW`0<<4_!Aq 9~B($&&6@.]ˍ{cr2JeiiIN[\\ܒ"5_~C\\.KJJjָ\T*Ս7,++ڷoFvvv>z.ub19H%XfF_ܹիӛ;֧OTJ6FTݻw[_pEQWWH5DEEYXX(x%YF 7ܹskjjt-L(FGGKҴ4],ENN7n7|=}> 50k .l,W`B"x?RA$&&n5۷/11믿޻wouu59@`s?c߾}Ǹ~BB?tZս)OՒs8i4f*T(Ϟ=ҥ-SICww7mժU2dHlllc%CvZ7JRn޼9==8Mqqqnn-j&b,T*ތ7=o֭޻wz0e]oݺA@rɊ+0>>>{1;tʕ{w^NNNHHHllӧOMπ !eeeAAAfQT8?{?ݻaâ\niiS.^hδ ruuߍ'OΟ?:???99CT*U&=yl0~$jf+APjjjV^zEY%Ϧ/˅Ba {x7k֬e˖uڵK.j?%HMM~ OOϠ Ar9ϟ5k֣GbɫW ?șMLłu1p@бdnذ!&&fڵ;1-,,㳳<"5EQ׭[N:;wnÆ w.۵kWϟ ? tssdnݺy&ԽFp8G7h4 R|hYrRS\VOhwR| ERRR [#o<-|Z Pxqӧ >q\T޺uu.J޽k\»w^R&Z|ԮVi\.7MЂARidW\S(Çխ|g}wJ$QFfgg?<++K$8x`wcWWW޽j„  ?ҥ/4ȑ#ƍnHNN|rII O---ɵ.\0,66S>|0==VV{yy3.==,kzaFuժ׊窢,q Oo_: awc9aBQEў=BxBc P)tw? ?(O+ 4BVSYXko猠47_MmAQiE0ϣ(..~٘1cr]ER^ڣG3gӦM#ݽ{ JLLźk˖-aaa'Nd2 Ν;˗/_f#l6{d;$',y)߲VᅦX{?SվyF ]r?1bȑ#5ͅ ,Yzjwww27222&Nb}+,--݇jaaQ]]iӦ)S ͊=(<kpa-R @jMe1hU*TڒB3 O4REWL΅UhmQ eh4AG7bX(T*Q:5EL([F***6z Ah~.Me2'-38+휵 wE)Mcmjё4 Eg %RiWj.]-\] Za8>j%VTgO/7r}L \Mm%A :;tnv]\\d2Y35<|k׮dpzrjA *Jhfn"QQQƤRT*utt/:h(mh+kPh,R*'\ߒCZ-uOwmvbqZZ9I =}%=uA ߤ899+טw~~~# JU^^ѻwﴴݻ:>>^ÇkZa-־()k[gr'~}ryYyyIiGn|U75-ZQIύ l nj* ԵLB>|EQwwCq8g fO%R}||sTyIpB!"((zE,oѰlKo#$⯋ 4p@v0,;;;<<\3ef⹯|8ś\K! Jq9{R\ E]QHQ)V,~Ƒۥ?LqDAZ٫ZFD7ߦZVK"7FC ΆFi3lr*ryy0UJE&/z K BPYQP$CN.^jm@-7lmm=nܸMBQtǏPk4yE^mLx*NNZR(M hMU A  H]^xv (X[ԺGa:ڠh#-:mðZ"+|61j@{ҊBh(ۂ`;ms,B[?*;I+C0*!|_ۈm), 8l\[gLgt]4dfeEPPj,T˃`$!"LͤhN۔Ea򗦪X/xB!Z2ն?6j6k,G~ M, cQ-L͞P(&vA=N.O\v71478v:r4eN2'(h6 Hddnj&٦=©|+ JJK4f%J5s((/.kdyZTkͶuuBeQP Iѕ>_X w8JmG?-iӧ`(:p@PxL=li!(CF9srCl]3jmmݯo,ޖ91;>)_8N_g1#{ jY#P(`n$ݤP(wˢ^qo0Dx7I$wvkh4L& d2{<{`!`0R)`]#J <"! *1𮩪rppx_{Bh4 xH$KKKm`+QTDR__A޲@|{~BPphPX,V(Rk?)$Oguu5Baٺce h  d2QyO6Q uP`@ ,X`QZ-@kX"J!z>}  ,X`  @X`  @X, `@_JR]|9))(ڽ{XAZ⪪h(dy ޝŭ]F]~΍ ׮][rFAA -/rȑ؃B9n`VTTٶm[NNdw&k鞞 %++Ν;-( p84 NAk/`5_eet_|=/tFa#~}lW_}uiUٳ 4hڵ&L P Sur?z_u̙{A6/3gP(4LT(J2X'lll/^` L/`5_}}' a۷obmD"yEQQV5lԨQcƌ߰a/^hkk 0q7# 6Pɓ'_vٳǏ5f35k4 (wJc S+`={f3?Ng%mD*aaaEӧOkjjܜccc233p ؽ{Z2e a;v|ѣGnڒVVV~[XXxxxu JKKJkLL'j0V-///,,*&888 ٹKR| tZFSSSc`ݿYYYͭ! ++ qsppBx{{7V\555555l6U/^TWWx< ]\\SD"モ D8wQbq]]]iiinnnMMM}}=Fl|=555##1**ʪ"feeD"RiaaѱcGBs(T*-..~EYY h쑱\. "(;;D(T*&١Cq%,++KII),,rAAA666 _|YYYiii٩SO6>>֭СCcYxqnnn}}=Y t7|ӻwoTTTL:8NN(a?~L2om0|>?WTTbx֬Y t~g555[n2dAJ nܸqe{V5k֢EB1cAϟEΝ;wΝ;oR}JTWC]Ǎz귞/ Í7T*VK ޱcq,XPVV&j59,_ èT?ؑX"??_&YPT*522СC]QTKKݻ/ 0:rN'o={OM Ν:u ^HKK>|S?[TT4ydLvڳgO͛VUU}W2ڵkhh(94޽{.\a4]J66K>LA4] cq\W&X!֣۷9rFy{{˫...͛7nݪ 2ʕ+ .{.Ǐ?~loo~z&*ABCC8F)++{yAAgFy1??[ŦLRPPfvگ_? {-YD L6SiiiUUUvvv "BaRR҇~h:{?bcccU*Ǐ?ŋ>ҥKATTTx<E|˗/333fϞ}̙;$&&N6 0++h[[[BQZZŋ~T,?66S$ݿ?99͛bx޽] MC@EEE^z]^^ MyHd> ׯ7Xx#""FYRR~޼y111 *83׮])**2X__DѣXyAAALLLLElllPPЍ7 /X $$dӦM&>[^^޻w^zCwӧ,߼ysXXܹs?k رf!W^u=""=ztXX_|A Ba.].^h>///::z ֫GDDtոz(iӦի@Z>zhDDDLL* &Lv杯興{d2ԧO9sA}޽{ii񾊊 1m4㵕{ /L jK""".]k-e>444$$dݺuhQbAߗN:9::VUU5;<3l0KKKUvvv;wtuu7~lW]]d2Ϟ=榿fڵB.۷ϸͬJV(y򤼼`Mȫr} j;?V;v# Z4CCC׬YCRLO @]v [[[GDDmجbM6 ӧO+ rPO?dIg޼yǛ`ֱcG 4Ç?n0sqj>SJ_D" rҥt;;:F6mڄ HVVVII-[8޻w;a6i$Fbܿ?;;[R/ &wssH$)))Mr>}L۷WTTܸqCVϛ7ﭽ)ʎ;p]lY zikk[SSSUU#}P(߿Zw||/;v,-&FFFb&brrr"CEiիW  p8RϞ=3^5k֬.]hڅ 5ͪU:ˎ;d2E&%&&'6}jBQXX؊Cbcc;AxyS7hZZׯ(p*<~EQ[YYY[[8 j N>MRGÑ|||BCClΝ;[=Ӂ%$$deej+++rٱlR/..n׮]VnzQTKZm߾}M=؅׮]*==}ԩ4mzjv~\ fO9ŃAS úufD"D"qCQA*h |.;hР̞={ů_ZCyyy,*++),,lpVpLA$w1AP%{jT9YpWՖK,Y`Ac8ŢC@cNNNӧO߸q#nnn&D;VVVTUU(|}}MEBhF*JRQԟ~iʕ%c0t:;% ^P__OPg1C6bĈ^IE*<yXd2~_T*DA30 A5J N{yy=yR1cN4hP=lH$ȸpBEEſ66ODkJd{'''3>aԩiښ={$Wd!H IP|||LRrR8Z$MݾqOB46nc]]]u)ML #G(Jvm1l:qDXֺ%d ٳgE .{!U=z`26l0y7jQA8ۈ% b߾}Ǐݻggg׵kK.͟?Ν;8tȦ2ҒluV]ZT*Jj+W̉=VQW-ɚif/C(Ǐ7x  !'1qo޼zj yf]]QQQAAA cКHPTaٳg322ޚ~ƌ,J~0 oFP|}} WWWGGGRy 3w@ XhիJevj`ɒ%aaa,kΜ9;v0n(,,ԟ:B"f͚K.DF~76[J\/?B/_ljgܹs\.aSNMOO7);-SYY1˥R;wL4o6vV-[aٳ;o͟?Ʀ… [ 3g4^ Byf={$g1]rq '_8غ؆  FeeI޽`Ԧ ?={; 8~e_۷oP(o޼O^~m'OBM4"JQݿc-[v]f;qĆ h4F $gLP(yyyl6N6lժUƟ%BR͝;799YVk4b^z9r 5jTiiZ j2"??_.w̙3SՋ/q({{{ "KJJ L&8wDBpȐ!d@777ZVRR2t-[rjjj6md0… L_.̔yIRB֩S'P(srrd2F 5W[[;~x@f?Yf3;w֬Y#JUhh 0gϞ͛7̙3 nܹsk֬AQӧl6\?azE#""bqJJH$hpttl0˗/wJd^^^G&O9&Nߏ3D 4vy@RXprƊ7oo>3hޛ8qRB߼yC^PΝ۹s'YUlmmCBBxӧO8qѣK qðh-[ܹsgͥK;%66x++gϮZ*>>>%%%%%HHHȈ#tbH$2~&D"93D>}z֭7o,+++++FqwwѣСC >eggm۶E_SL1'N|Xϝ;w/^vdkk;wÇܼ<ݹvvv㏍,RX,6XcBDEEٳg?~A~uxT*E"Q ϟ~CZ \Yƍѣӧ/\P[[F c m[om:݀Rɩd,Aϲ9LOOh4o_\\L{,--]\\k1P^^^TTTWWGx<[?% KK` o=99, 6moogz^P USSczLc%%%uuu,ǧ*قemm}r47` x;OvCuuuiiimm-NwttԩS[_555EEE|>xzz5Tҝ>6֙{f@XO6 @  ࿄ E-ZX,Rq;:2xDX`  @X,  @X, `3*@K8T*!h5J2;;AZ he`@/ۨQw>o޼jc/^\[[ '>>2X~ܹΝ;>}=;/^,YÇH~p7|SYY ,P^^>qM6eeeuԩGVVV.]zNT?>11Qն^jkk͛wqN%8BA߳{k$[S>|>KOOoh'NAA >|8dɒׯ@O?t޼ywٳg. #F9ѣG?SII8u =;;{ȑ.\0BUUȑ#lKoݺuԩgϞ\.߽{СC44mݘZѣ7nHiӦÇj |r(55uO}ۆq8nee~%##c>$c 8μyƌciii5z'OՑBwW\ 7/\jWTTܹmdΟ?ĉ,|޳?YxQ!uRiBB [n/++1G+zm|{ٳAXݺu[r^$Itt~=TW\!\,ŋ7۷oׯ./ GNOOOp/^7]SXXq)SY#]fw9rfq>| +]&͝;ɓ'EM8Fooٲӧ7$yv jժ={Z" 4_~b8%%NNNw}1R9t޽{ck׮͟?Μ93gO|~GV+WZvҤI\..!!ӧ1|>O>!ɓt:e2n (J$CFEE8^XXo:uקNߦJ:v2d#Nʼn׮][z'O6la)سgF0aBLL /^|eE\nc l6ʏ?X__߫W^z988`&?ѫW/{{{> e wާO{{{ O}vÇ ®]*77… [l|C ]n߾4p@///+++A]:cƌ+W6uإK`ճgϖ-[V[[۱cDZc>}z֭<طo~ŗ.] tppdtu%%%]™OYZZՕv˗/?Ci4ڐ!C􏑌țtk֬=zիc/7n\hh(#4Μ9~č76СCϝ;` P(oek~;G=z… ꫩS7,]Yjݻ?t:5hOHHs玓===ͼRT*մiӲ ;vlNP-((vڪU8q?1d2\~<*g̘3~xFSPPpeh޽{qzou-""bΝo,o coN6+u_T*[[[Tgn۶-'' +**p=zaÇO|~ÇWVV/tҫWfcɝ;w.,,l,D"6lXllH$2XUWWׯ_m۶vnbbbn޼ip v幹&L0˗/###w~}U_|EHHرc v?РBBB,X`"'QQQ_Nr ٵkʾ}>\Rl*%%%::ѣGZWɳ3i$U.]6gSW ={B\>r޽{ֶ5(XoZtttffΝ;͛ݒ>_~ KfffddMUf]x1<<߸x<:xࢢ"U=ӧOPPիWzZwᅴyWjJjӧO޹s] g >QnI\. l[{yym޼y֮]d2o߾mC}__ &(F JU*xyyݹ{JބoeddHR˖-d?c^L7*@G1ׯ_~ B9:32 r`͛L D7[dmmfBa5ҚY 믿d|MllqrΝ322vi>}zjkksrr,e"'ǏC 5kr 8h8`Pd/ Yr%t+W4^eaa1l0Zm>22¢ |o #Gho޼13=3/##]}8NRRR}}qE#pϞ=-0B?GMemm+ /^M5M|mggR_NNN;w7o^S(d".KѴZZ:::2޾jŊ5}XXXeFn߾MRǎ_T)))  & ô`r B?~:F31j׮]1 KIIi,Y ͛T*u„ &*}]]/ j [ ңG:111p8.>?hXj˖-g~a.]fϞ=x`777s322P5HP(|+WRSSkjjqssԩ\.oa"$PT&"wgϞӧEFFΝ;wȐ!...ԄIII/^|uEEVuvv 4hq A 2 E;v48FYXXڴ(h4Fh4n^^a/_\jqFQ4==0?32qEƆ2AEyy!\*Z]]zj@TTT4T*a~*j\[7o.]RRikk^[[s޽{n t:ӳcǎt:8qO<9m4qɛ:.LFz"HR)F31M;vŋ?]Ο?cǎm4Ep޽n/t@ pss31q BIOOGQSNfnJOOd#̯36668D[n_6EO8q;v;v… SL裏  =zX(aMjrttnݺe˖m۶8qgΜ9#WDNaJ_}UQQ!!!ԐGFNVM7:;;W^ݵkז-[?5{8L\ީSX&ZUMޢh4[j2˗k6'hڶ~M)yԿt DEju[4_dJ280qkaXA4OYb#͏6Bv~0A3gLNNd^^^ݺu#; zp;w0,$$WRRrsĈϟz t3ݸq`kӼ]^cl|946<~ȑ#wƍd77={.Y>}Ν;9`SO;8&>|zj*ܭ[7V[^^~ƍߝ-y֜eaaAz-PVcfP}i4ڀ wќ֭[۶m],K.:urرcǎ?ϟ?'?~0qq͘1C*6lݺuM[}\ &L_:u*++kSN]|7rk=7W_}Eڲeu2zhgO䗠V}k&ɔ\.}XVٳ~c~ޚcD-h43g.X6~Wޛ6m2cҥ0nwlLnٲEVs_?dȐnݽM.geeۿ{" :Mzt``;˷mۖtW>|duǟ?ԩSW_|YVU_03f,^XUyy'9} xk 饱)ccc;vnݺ=~x…(ߢƌsĉ#Gxxx\z7Ņ sŞ?[nׯ7 }޲>zgϒ:ujݺu_( ##G1t3vh`2C.ӛe0 C&7= !al6񲲲r\Nӛ83ٙEWMM͛7olllve<&0nW{ksD"ׯAZ`0>cDյ)7&d2aJO8 M}._Ϝ93}t@0m4h>ydKKK.Zn߾`|pJj &At:F&K^mh"AȉN:ezm{mJr߾}ki ~)N?}t:hvܹa1Griin# -i'OH$P(ѳ_;rmQ5k,իSN+))Y`J߿\rZ㭑_L&S: •+W6xonFG޼yCR: '|pYMNFsAQec)//H$t:]"00P.o۶͸ul]]t!ݻ7wggg7ydY555&NaüҥK_yoj൰ Xv-!Ś?> ,{R˗0KdpP]]m<툮/^Lӏ?XV)f|[ Ғ<^?Mwoc!]SC.]d\%\R'_ؼn pvv~iRRׯ͙@yk1ch43f4xs_[݆ d2|8==](ԍ7Ο?J.\]]].]* iӦ]x1??~# JPzQ^^>vؿ\&I$җ/_яO?Twho޼ٰa B_?hwN2˥|>իWg[vvv+'NxرzTZYY`w%KM4iڵ^ꫯ]Kg|С4/^ذa_mNǎb?~lٲׯ_=z̏hܹsUUմiVZ̓'OΙ3-h0 /^|U]>,<<*--fioo>?s̽{fee ꌌׯԷcǎ-_b駟޻wBBt&77#'=v… cV\lYUUՠA̜8{… Ht̙=:u;wrX,.,,|?2ڒ)[[۝;wm޼= T*ʪk׮[l1x/X,.((xɾ}ȋSW^zX,_z}F3Wjuc [lh4d N;v9ѣ7oޜKVKf0///DB޽{̕+W|RL<0gLCQJN: vd… 333]D j"d&MX|9Nmܸ_~ѝG{r/^?~JJӧO8A~,C+++ݴ:t?-[JA&Jscٳg7|s rN4] F;tڵk/;wNw9::<%l!1]N04ѣvرe PeXK.5gFI4 nٲeyyys!.PxYAVZ~̙4tUUWWOMI8jΝ;:ׯ'PA\nT 7o;v՞`mڵ۶mȷבQ+čjϞ=/NKK/7a1=jccG?~|ڵz*9JN^x1ar#-\󵵵 $&45oybfPlٳgW\~MwPe0VVV %/ x;;+W,Y$%%eϞ=۷o?ph_(yzj;v~p8vvvܹ >_YYjueeeZZZVVP(D688cǎMzID")))y9cXX~R T*Ͷrssםc h4ZcbsrrJe``_Qedddddfccء|BѰl2tsYYYjJKKE"~S0999;;[._[^^^ ؍?UVV&X,VǎXKKK233tzÝ͜=BԤ$''K${{hZۻIb  wfkjj&X""":t`Y*|hk[``=2===%%E*:99u79ךBrQBZdpoJRimm_Q/c%$$ovss37xvd[oooD"aii񜜜3V^^ŋLRӡCK~_xQYYBW^iZ6M8:;;[[[o~J={GEGG78g׮]/̙c~6?o>G  ww@[[[;;ƞ_NNNf6J}fdd-XYYY6xB _|YPP@深𰲲ѝ,@P\\T^^T*LWXXgWAuuubbbjjj}}CzzZ 5IܡCmf;GI$DRZZtm"##tbbb\]];uU `QT wQӧT*͆@?33399Y 888DEEy{{ɤP(j2(((,,<.BQZZիRd2 V P2xYVVY__pݍ7j ?^\\L;wԓsrr^|YWWح[7___e X\WWWYY]]]-ɘLKttgR%C׮]}||$W dddzJ"8::vLJ$ )))77WTx;QT[PPի|v) D|C jjjmll @KnذR.kZ20,66vmoٲeϟ6Ν;/_{}/ZXP8N4-<<رcK 6oެhc1c/{xbMMJ"A4gΜQFdt֬Y@*{'N[[[O6uΝr 0 c2wss3i>3.{9 ~{n]]F!aF۽{w^ ,X.HBh666?IO:urHY, |dT*.\ 0GGSN:Ϟ=[l|gϞ[ʕ+gΜ @KO.YBDGG=SNt:?*JR0 3Xj'MR>3fxzz(ZZZzmD]Lk"CivuA++I& :޾쯿z~[SS3k,F x+Vؠ(ZQQ޺ukǎeȑ""99s=AYKmϟ?o߾b8%%~4h1bD]]]ppj;v%tRsNݻw0y1c8888q;uꔇ~ |1o޼`{{{!H^xqS^v`2lʔ)NNN|I޽+++}zHHɆ.u=000!!ݻw U7o5kq.Ɍ4nܸZn]xxĉE"*B{/q``%K ?{,444**`Չ'¦L+>} 0D!t͠$53gvݻsrrgϞMd)z_UT~[7#{,4䘎[%Ɏ;`0va(,,l bƍfnvѢE&''xjǦ3,˕J%ñ32''͛7>>>g6!V^M8"ڴiS}}_~`jݫ4.mָN߹sGP(UAr P(&O| (w!!!uuu|# drV\\a Y,fT*R[ ZJ,s\+Hx~~cbbƍJF}g(5*!!Irtt𨮮.**_.jjjlmm&wrW(JbopEO>p‡zyy?~ĈTQQ~~~&(jp4^ xfFoܹs]\\~o9**ꫯ2V+**X,ʕ+NH$p8(SѣG7SRQTEaaZVj4\h|XYTT4a„3, 2n://]v͞=566vt:K.e0{3g{߾}̙c~?~7Ad0 iVhm!ӧOڵۻp={uHX}}=AQCJfnFQդ9rvW\ڵ_]RRF*S9999qrrرc@@q$aA\nqtttww 2?6pj5u"2LPLc#<==c9s\v>P(ǎ ۸qP(48A_}U\\\>}d2ّ#G7ol1z~l3T:p@`4T*Jh4jݝqoտ}_|˗/ׯ߯j~cmm ۧP($g&7[rP(LMMݸqן>}uh2T*ݱcGKZZZj>}|wmTh4FSjUB:tI$ȪyƍO8q3g˶mjjjo~رcǎS .\nΝ;W\I.pV9r$|0lh^8RԜ,4M1a^^^ .w_zߔb: 0,55.p7^nzyҥs*իW2bԒ8v -ߚByᚽСC/^XaÆݹsO>ſAݓ'Of6DQS.VeccceeU[[[UUeRfff$'NTw-rR}cǎl6FZvi1cH$%ݛJ^|%իZNMMmc~k2dZ>qD7ecc񪫫[Kx*M[lQ(ׯ_7WTT|B>}dXZf[Tnذᭉ{hΞ=\ZYYxxzz|||\`իW(:eg?FgE-L&399ٛe0#GRׯo8ݽM:d b0PS,:u%Yh J2+TM6Q(ѱcǎ%, _$&&Μ9S.I7x`{{1YWT^zՠ!Man:Zm۶Çlp>xik87آr.0ӳ{Bp1oy\n\\ܖ-[L _lݻc溸b|ڵ*ĉ$5͛uuu,KW 6ݻ`5W;=://o޽^^^\3>|x֬Y 111 xuuu?~~֭[Ν{ϟ4H(Ǔر㯿9rdHHX,ͽxT*󋍍ե:uŋG9qDOOOZfggb:_oݺƍC qpp(++;|eeͯ7H@0tP//>}xzz"RUUuOr81cXYY̙3_z5z]^R޿N9s{\bźu9r#G2LXzڵӧO-Zk׮-[ocƌRb >>EQ^^^3guѳˑ#G-[v7n <Ϗf .:twd#FtUє_v-++⫯"kB^^^~ н{wV[QQql߿5W:sνwU*Տ? Z3rss;ys8pe1A~t:w|ĉիWvVkccӷo_oooh;wN7;AL&>O fZ|9N?|}(?׵[tܹs۶mzjjj* A`6~9sGWZV,78$E"QM>dhA~aXHHH\\ׯuoV˖-ҥfѣ[fXI&y vȑ?ѣj:""`.(]϶kbI&/^ܳgp0,**J?>&޽?annt]v=CΟ?qrS{Iu)QypȪշoߥKɔJeddÇule˖鿞\?Wyyy3nxڵ{ݍ !-z ;;\V;99h L&ǧd2j /_llZ" %J2FR[[K%,C3SVVVUUR 5=FVUUj.C7 \_$322B! 666  YYYd2[[˕Jܹ;v=ۗ-b5BZ!4:{`\+ @XཇPiP@KPhgˇ45e4WoIKi.^keOoH]VWRa=eʱ4L즦4 4Wc<;B&qWLUM1k{N!'Ȇ5EVqÖ́%@xa8@vT?P'q|m\Š)vSpK]K8v*։\N[SUBw㍜E m4Z55e i=eWY(<Ƙsu*T1$#@@EzE@"4U+|J'¥ iHSͩ:uov9Ǹ?\nޓJ&5bf^Yx| Vgs7~f5/e'@sra/w^P7.}돫 OvXl77^5]:O?`vn`|>K_%6@.>yo~[Mg<޾/lT_'=av1:o՝ A?_ӷί*Yr֖o>nn?Qp?wK/EXOO^BW'??}Ia\z%`[>so}NPڽoè rO ٭ߌ}ǭx?۳tsRǾ>cg_;n/W& kye?O;_T*OWLV}q-\O'>zm?u~A_"ɳSb?7bRTAZO[_}vv'vG{nrAiju߻䝯y+OG `ku9=?#W=1xٿ=[c~{~{DPW]uUׇlO0??^cf➓޺\/qq?qg{q/w_ZO{'/Y=qWW2t7wZwO>_ ƣ|^󩲯4F{?]$NqɯTuDV_K)zSOß{lRUQac9Uۗgo+658/VPW~v[mү)jw}?49{4XXhQ^?6ௗtǿO?>q<.Jmq_ӏ}/X\~N_ܟxZjג=5Øͧ>)zcvp}UW]uUGh[K{g=׼1}=/2H:/R<>q75%KOܯ6wNj/Gݷ]ls/%napa{zxDŽ,H2ul6W!V>flh2O{gkY81{{/=z}=>g9՟<[ SW_{Ͻ_]{?8{ׯL~A]uUW]09R7uU`luXk?&HAi|}.s4ǧjYlgcn+ڼݺ zv/_V7zI^CzY;՟:<.IzmY6{kLfWC/O96Zƣ|wݿ7Ƿ>S6oE} 2>l=hn?ܽPPM9bR[W_xs{A Sy_uݷ~ap{|6_[>>#UW]u@+j"o"^0o?M_Ec˾|1oq}SpmK~Ϲ?<׾t֞w&=V7^dm37Dg<&g/}X~@$zS),}R;oJo_tc/8Up=rG{˿>UW]u 0jb$nY9gg ֗}Sm'`>>t(t\y^Ͻ'\y%ɿ?.}3{.yx>0|e@6;5KwZ]mݗ,)!0T:|{6o?7}_1jb]QEnr=4 8(eLf}#ADͶd>>ju{׽y_~h۾ O^}޷G頮ꪫW}5Xj]c+v#Os~nY8ߣ.ύ^jG^{Aވbj'^\}碑ؓ?j){zpoumJȽAS6/vȟ2WWGn] ?t=~znkKu? f??ןoZW]uUW]s8͚E~g_]ٞͩƓ?#i\%D>vʹG'?򏦿3,y'??ܭ/~~} /4%y7}u-C[|slx:>ίBmtX@?-寕d6/[mhܞ4Nm eߔ<'%<5;~~?[ *7?񧿖y>n?_<ƎN/۫n|_J;:)5Q+$OkCecso{X몫EAx=]|`/޺ߊ:>8m]DZ=8sX}ܖ#h_-wg7^:u'9u~ݾz>#|kTC?_ _8PcG'٭Uk gٝkⓨ h7y]O3[_Kߺ{|WZ_~[ωȕ{][֗֗N_~Zؓ}Ϊn/}w} {pGo^ ?> qJn<}?i.?r/}lؓ}~Z|%/zںȣӪzKp!}UjVFW5zK^\zXx:n>E~vyjW|W1QW]uUW]R =8yZOd>-7;ottn7|:OG<:kn .Gb[v2{2F7n|syS{B?"3WWrUڃ$yot? 2X?v 7\͏yC_ho}[UOs'?'V'>ݷ)D4ح z- |aOɾ$޺>k 4}QҘ'C`{g}.z}$sspð)_pTr8{'Lm\hO>U>XN 6^g;97^q[iEǨT7]{uC?`|=~UxeSʏ|gأ={\x`˿yzN$_?ɓ9QAӾRfǧ<>|A C\|!ONT_|֍RRwvꪫ]ƘH_ȩU{~Q7ꪫ|~*zX5룹jUW]uUW]uaZ[GVZSֺꪫhZUW]uUW]u!Uꪫ=`UW]uUW]uꪫꪫXuUW]uUW]5ꪫꪫXuUW]uUW]5ꪫV]uUW]uUW]5ꪫV]uUW]uUW ꪫꪫV]uUW]uUW ꪫꪫjUW]uUW]uUꪫꪫjUW]uUW]uUꪫꪫ`UW]uUW]uUꪫꪫ`UW]uUW]uꪫꪫ`UW]uUW]uꪫꪫXuUW]uUW]uꪫꪫXuUW]uUW]5ꪫV]uUW]uUW]5ꪫV]uUW]uUW ꪫꪫV]uUW]uU.d)2h 0`@w e- !`̟CFB"q ";0?=FDD@* " H@$"b@NB,>aFD@D ̂~;"" " qE@HA@@m9 0#݋Ws/[8!r {]`b" rw?\ʽ [D0KZNu}͎RJ ""3XkR2.~ꪫ`UGQ!-XA(raƜ |aFB"rw0K5JfmfE,rk68"(AXsGv!:@$h6&4[ogٲ"R$rFXuUh0'kيB%">w5nݵ@} c!BDkm`9p[-ٱbaႉrTrۈ8L @QE,J)1YFH #O>_ୟG76=YrjpUW]5,$ T(ܼq{`wg~w .\t  UYRRYENSmU <3V&&{N|PP$^-@29`񄔔K~U,Ե .ySP2rn LIHB/cWqAXN;ӏ9YoP5G#+BDLjƚ$I߸qOd}gq"$ύոjUW]+*  ͵k/^v}hSO=V*T*\YZXP(q9iA*pk+ #%I rP SjDN*H UdKAV} ^ rYT^4lsAXrzrʺp@A $㺰q2wZ*l1֘Pk,[< ׯ[cZRJ0 jUW]e9 di:^}~6>+7mlmxzhf-`e= ؂BR2(8JSq/%\Y?z(?ɾ"9]U}"Ë%[~"H$UpXlE<^@RuTa9rqb)Xt ]pc!iw$\ Rt(\ ̙9d {W ԡ֘RNst|q:ꈥ."ꪫXua<}k VaXyHx(E0<YIHMSh eMRL+,([aLZ7(*Z(!gcJ^/\,錪MU6DZVJ)fX0C ~:OTa~ΐ~;C_6iX`A1E',BeJ儙$M*A RJkmb!'dFCDFNu`a" ̢uu@u1]V]u}T9" 4M,Ifvq2!BBB0=Jb`^p~957uC@CD\0Z8E2VA_ r9BsveЪ[o @ɕūES xgVnfU٬ s+~po-)r\,T.wo\#82uhlf)βIBDڤjUW]œkB]."Hުp!AӘ,lCI˪ *bvߩj @ DZXH3Dq-vx"Zniq\N/Eaْ1B\Kd0 ah¹CR:O%ÆcF /)=L}(=˽,v9V=@љ223($!IPojMTdk_ꗪbuUY+T` m-¢˖ؒϑ&3 րkd݃isӂK|T& ]pذ`rU8U}A4^AmABr%|Vm_dj3 'DܐA ݕNJU} [1CP9X@ +.n;+N$[RHcBY$ N49LHHc46T7UH>UUW]5aK{є=pvE8sfabnm.TYZo̠PJ$ZdV8V\F˵P`+F茥:vN/97SF0($7H =ml,ppeCPSv}}/*y8eHK |Y~TЏ.ZHއs>pipaH͓w^惏;0) Vu%"DA"fk%$lH3x uZ*S~*_^m]u}Hh+MHnS8WuV Z3Y2T+^$=V6d+eHk(9X>vTUX)YK$wCZi91ns{4Q$YonZ ꪫf`!-5H2Y14E몫Xua_*LsuDc,$\r⥼K$) P$AD0"8ژY]|ZvjD qV2wvmǷ-؂M꺾IL Ё4_Pm# Y˓+s{ճhNH\-* ClUW Å *QZ+*.uu+eYfx<2:A-4E0cvWWՎ|زUd /@|0aVE_%T>,%@2@~ޢ\V#uUW =D^ ro2A/,rr(B9yl i:p8N'x:I2G5Y<nUzC, lB^IU^u$Z%`QֽĽ%r,R"PVD9\R,e=X},),UV@/  Fx*JgUA@BT pf60)ʻ/ӓDXp\/_uh$.aiĥxci{U>Ԫp S&۟|+af:xϣ*Rq3VWJUaXΦl6IO&d2Niii&&M8,Kbaf@ƛ7ǟl"Q- ڜZ2\SD/oU;,\Ts{ /7s- X;[e2y]93I>kX[^!1mf8uNf&g^/ ubVv_If-JYv).UFגxl*У(j4Nmw,CòŽZuL[L4"8)wڇ%DuTmmV],*'U^V}N?VX$!&'ֳ)OTLoɲ$K4M4I$xIx>NgYēt6<6@)uHQHigYvr|4&{{WG_)ZXpq ΰ-uꨥ!}*X}4̽Y0<Շ@΢-W')&xih^8yn>OXPl)A\P[?.n?̂XD5YlɪZ fS)eM#" +,*8Y:|d]Ig$;0@E;;{{{۷4y^~yFQFl4hFͨو€HUD/c<8E,3X6T.{fl V]5Af%mx߯Mj:p`EXbA>X JCί}I(!W"]52_Ifh+KD\ ~i*EH=\˔LaĐ2 R&|s/=qfӣ=v2J^{ftf $*P! #Y{We$v*T]oOUW R'Rj\ˈ,Z4zJ/+{y -6rIU0c|}A-4˒8L'4χ4h<gucb!wI5VK)vDD W7҄lmfe6NK;{{;={? †ۙ%?$w TT݃ rAn "yVFd8&65d֚Bbzn"C|tX0J9~_Gy08G`ys @sl; ;ek]|ykk+ CiۈB\,Z?^k]2rrA>'ʫ$"Da;ϲ|DN@t:hRR[7ntNtzf"%s`a&F`Qs&E8&o/8--Ize=uU>jX^]=4U4_ 3  ;_ @,xUqseqL8Il6gI&I$qI&qǩ16K,K-3Zi(ԁ @9TR*Ӫh~RcG04OOfif&}N}@$lZ6ܫV՝\5SXF Y{'N' "F!D;׮`A7Rb@97妱,wr k}@$+bW6DAr_9/QvQ'Wa);) ESq|Թ0g+ gWhZj:l:=99ID&D,,Ȣzu'ýTPx&T,s<܌ AaiNDD& Zk=qijTDaEQj6A6Ft:njM"dAD$[vr2J%?(K5sK^W Ȫ{hV9*Ӳ5A1|Nx6f4MS(lFan ZJ|ie0C`u @XAlĖ6M$K4L$6([V{VuZm5(P!)ɥ[e{yHfe#;`!4+j`pw 0Z*H1v΁kxeft:L&q<'d2F$Ҳ=v:=r")ä%?K Xk]mCWHH.@X@4F)N${kz0A8|Y[,𑔰;"lQt AC{˧&QD!CE@#"HXJ&DAf.& Kl\ >B4$bW&Ugo oFo,Pp2yo4Ƙ8C?dwIl6۹{կBc(hvdɬ|ED0#nzꖭ5fY:YXBFi  ov;ZiH"c̓Ck!,RUMߺ*"y.29*'xeCS9Y:'rYh4Nfd4Mgx64M4Kac"R0T jTZ7R{Xa62;uH[A( L S,0nP-J[{lmm>w=y|%h4a#^n%ytlU`ŷ/_$ Dp66/|[.2($,I<ЈHՔ!nԺP7"ѩa\afڙ6H+Wn+*BIXf/#bxCD^/޽{pp9;x(l-9jvшVge2[[rClTĠJX@LffIF#.^~pI{ GHJk 1 sfl-9>>? ҍftz^0xB ÐTXrQVDaP.O% ^jUW D`CdAvV pE@yC5Ʀ,˲4Yys_&8N|:qfA"OB@Ean4A >ﶅ h1*|57j̙B_ O "Vdv||QΉh8,0*(L[ jTKYpL nQG6O@1 XQA`5YC)TlG! f]?CUDvH)/]8WoLxĽZ؊5s J)(Fx"!0a~_\gFMZ+Db63Y<$d87VuMSm[Vn[fh4BZ ;<`UuO]5 `{;|R5]km1yk,@W-rHTN;sAKgE@ Lek [c1Yf$i$I:Lx:p8͒$kAu)J~NAE r{?j((O, |0ʝ rНݙ-0BfkX)%(ͲV&h8>獱{{{*УX:@YZzd᜹*_wyi*D$akR Ad66*h x i~:o_\O;a8]gܣ-C!eҎetZ9UQc F$en͓ivtrG JRJ!"_ A4Y=pdұX] b.pm6DtK.v矿v .&I FE; j&6YFlI!"fY6c<;9s$EfmnnnnnzftjH#~ycŪ!3q/(+s]ezP@q.y?&V^YrJ[HYddi6ft6Ox:M4X7N5)lP!RNwdXDL,Q`NQRƸsc+nrMGG9V T5Vh&iۈZmflz8'۩*q[CU̮,3iLL,c 2h&:3,s )-A"7GfGU& )\zSQv:2[=V "iyMiB0" J:\v3q. @Ԉ `wggtzzEбiyD,b~VY݁P7 n7WFc)5H 8=lF/ #Ǽ/ɒ{9~t`Z^ౠCp9xks)1vȝcϸ4fq2L'x4mgIcDe&2cM^) LqEPU~9E dq39kQdy2omΥT#?椸,L(_ U`6'[&qF5LxO4uo"EnJ5BΚ8[Y*ǁ  ;`gDe"JZ` W\ vJ &FgEnoi!E$@iH2jEDQlCepn*#o%[V˸KaLF#a %):HxScB~ϕVã#&,lA!'Rʽɘ 3X?R 4 DmQh,3&ֲ$;wwt0a[~vFgZK=VNNū;ɿHz_5L1Xu~k:W9I0g噼:\y"]kas[d16M4M$u.l$ifd:qt2g[TO&s|A -@@Ջ"DִeR"Hfc>>L=wrsDx<:::IӔTF/Y Mj {^[uo_`Rcz!B҄BZBb $yK˿@tDׂ,> J )EtGy큈R8̮߸u@pQ% kcR*^K8*s6܅0`Bp 2a!2̂,S*$EX"& vY2Ѳ`װBWxAE< f׭cZA,#r(JEHq,USR IYáU#Wʜ2d)&g`aaFRhy|<{g'=|ڍ+>) avd VHnoQBr{ Fˆ+Q-jC,fY靝45&Ĉ@Zs677{^ PX"D]C=`b&`>!^G"XM#q@g}V^jUG*.BVJ!-MhAeqR٬SJy2%qOx8'x2N<̦͒Z`p#6hjM:@=3EKyh x򕮎^DUFDEA x2O.9CnaC$GiW*!@ni6k Włi8K\nJhxzri,Q(r ޽swscYX9װ]kp 'RSL+RÜ*ХJFa:Xi.T̪V.T[XiKu3я;|Na:c|p[Yg uhZfk8p&*MS`FEb-&R(-BD}C,$_HR*$02plݣ.وz^ .\v;V; X;R ErD۹`Yʂ- !UsV]-F18NXD.Khe- }a0ɒt6ϧ7Gx>ĆMf4Mԩ1ZQ+pr""M> ܹkUGɻr!'c̺Y.` <dUYED).Z[BQ%jx'?Sb 0@lmonnw0HĘnGGͨ9$i{} R,t[:bX9"TI.g 6yaj4~j^ϻ=R!{׺NV -e#ᮋ:Te8w^Z1vo5d ۃ92h[8)[;NN;dichoc_9T~>[}<=0,WddY$quԼ:F#vtH-X#_zN K_%B%6Z TDa (\kR]Gva0DX*/D56f1qyx<9mn1fmPD-!-o<1 օ7~Wh?L ר\W߱gMr(R1ޟkСT* O.Q5êiʺv;Kb)J|D. ?ֺֺlfww6\qWls)#`-mkbȗ>Nq*('sjtCՀtC-q-cLƘy fA@pc nj50 Q5( E2wToX6@$wU 'ZW V87zUѥ_vaZN%"eiϳ,sj6Gd|zrzzz2Lf83l(V!Cw..N^JXx"߬QDCDP./ΥI ơKKF"gl18Tq6+NЎwYC`--K+x_%Q,"2Vd=>:ͧnw8<5af&,K{ICVk͖-[c E@TK*/v[Ҿ ֲA,WUW4gFn!Z%Hx)ni4okaqγBKcCk'ע=ߊ-I.EJi 2d\vҳ>W=x<͡d:f/+ %R*u,+APGFw+˲4M,*:ι`(ܶ~awC&w(zVa16SW a9~Dbǧ'l:h4SZck|qe̖4 8BrHH)7 wC'H@7/@4-Cij|_}̂ځ\+zb)+,L HrWa X7I!rHZqH*J,ONNEd2jD JK.z,5n;kj#oF! ݒo*؂:VBUJEV\ž%tޣvu= `^nۍfpGX1ٴl5菺6MU_ArBƇ>MvPg:5jX"k1ifNg֭DHalsmlڭR!R~A(!>kp'`+T?U~yKҪt"p=;y4M$$5=IJAH#|嚀;I)1$ Rs ;#0yEk!;%-kJUD<8χC Cd`EX3j XI.Zخ k3L,+tYPZx1fksΝ =Ykeoowow{{{pC7(uFZAZbH{΂GgCC`V~PعbTo,/YB]Km;3zPqXeV{{UA[UZYS%7MR]1!,2RQ3ݽ խnۋ /BB6}~kV۠899xSw.%YVJ0 "fa6Yk3ghۭV+j4Zn;N^Ѻ/At-צsF@XuwI{77@!ikXiZa:Dr@ݺr bXҹA'trZZ~|G]֢\m f_2{R"R"(BPe*[k1̢n( h>gYV,J5̢ʱ.-AdY‹׵.\8wKD.\f(Aasv2Kzf9V]׆ޜ%9Z/5&W m.J`Z*mVe i:XJfJ~1+P8i" kIetJU f 8y(D1A n޼EK~K/=qXD]݃>˙s㗾%X!&++Of+ VjD*A3It8?8J E4^}ncoZQ5(jFFJ*M sqSbp8%u~%*.i[n_}Qll@i' (:/YQ 0>NŨ )#"{?~"a!)p)gΊ=~` Ȃwm\|J :Hͬe$BF#hڛA'"Ƙ"j c0wI^LqFap8<<ؿrVl6ϟ?e ȃ#p#\HxdLnYb0:pĥĐUnQ8,*<}TQTqÛXKMl-X:܇jYzTuv3rҢR/>T' `pWZ 4a)\p"xK'oRA'YYk|FUaz;wN'0xƍ;;Q!Q9]UZkÜYk0f&/V-g9.I`RRE-|E ?JeC 5obdPRDd ֤g$Mtzݣ (cڦJۿKPXA "z!(\,K1PKT ʑ :"2VDtnkϿfj{޹s676QW\z,6V]vP,sA $v͛7Ij776a.HԂgy ޱ ҝ8Mw&4=cL8'jsq~"P P`<]VR J9CE̱&K,|68G|l2$k^lR P,H{#z z;3RHCBJm<43,`-  `P4s?wGH"M(,t~bI̘l2MY0EaznQiAZ ZMɝ'%- E`]V#/r@-RAܾO[DЭ [ѝw$Sr8"載(ܤ +,1 ,NH,aH帄 S}@[c3Rl4dl6O8ϧd2qbL3 aA{VnonzYfQ$ЄlMNfӃh,'@Dܗ"u! E\.!23FA0v޽rJe/BZ'If! OON7763bB/rst>Kk=Jp~Q~Ld,q}Q<Zda:W JdL.^.b?pX?wvh#1tSEڰׇ!},dAt.BhTz⣖|/ E`*`" XfbE"XE&P,[`<"ti΀0Z{61 ΀.?(,b0P&3F6Xapcpxxgw^x}5Vj $gQQ@âkDah\{isY9+b{gH1 I|x;-:vW L< 'I.m vXex`x{v; 76nn Q6B`, ehw5nǪrڹ3O#BŒypDU/U?O\Tqd2N{ݻwvvvxn.Od0{="jw7)@J)-~% ca@֭p .۸99ˏ(ZL K@[TGh6  WĀIE"xU/~IR I#),₪R`@aLP)` l( $xn@k { df  - k]9HJ6ǥJ+`k@(H08?b@QDb}[ 0*BOu 1 Yb:8NOFvQ iN D˭IV[RAiI0ldY&.X+lR()kYH0N "ZZkVb"tѮe\iV]<(<SDZfc|"izgg;Q8.;ʕz@;(>ʋrXu}<;[FDR$eoo`ZDuCVpXu1(W$A;XP*  D@l!1Qb2β44sj>Mx$p6%q:Lq&l8&a4nh4Vh5[v4fuۍ0 uDaFat68@,lȟmEd>WႬZ'Z5YT5[#!Q@IFcel6pBt>q'f5Zkϝ;hfY 蛀ȍn9\B<2ώyG^]1#KHv|A/+RM+pn)+|i!s/nay곕i QbVQ5yDՈplmq'qdiIO4|8I,KSkR@ Qjvh4n#jZͰi( @)>" 26X; @RH46*I/wI ׄ"c1-o9^zLƽaCEQ G'nw8A0 (t-(FExppE`y#ab:uV^!fa߳s*s)R@s)CZ98q8;-c\u%Z<*Y.;-8O¹$ XD4Br,FQ[kFRe2UDPWDdlIy 13RE7:$"0 "lҊT$DKEA`ʲEH`2{[kCMbd:`5%-joQJ#b&, ~zzw叿Lye/PGNiHge5Zu+Z#ҁˆf9V;l5FlFZ̒FkY$oE 0[fTHHx*V^\(ӊ?K9LIH)" YZk8>>C|vV ^OU>jYU2[0lw޸q<#j_#z{'nU Z8O,$q *R63I)DQ40[~ׁ0K|ߊdYJHJ+̹33L3 3\2莗de$21 Eˈ:""9dBzVZpa׏ /c ~x4i:`(,N7UfCw !l4]t+ׯ_٫֤،noھtv;`l5)P+ *Z61+l C*I1bY7" ŵ;a8X.ň(&$ HL~ |ZNڦXu}VbrjRN8{m>#wlU>q* Eq*$ Dj=l6{+o;ֲ1f4ݼy/97M3Ofoq3efwO?3ϾvO֞,sEn9tfuU*@ZguIhQh<VTa qےc{ɷYZuh)x kMA1ƽ#9XVq1A0(G","HKHֲp@CV$B̲R: 1h犗k,dbea'1̢hL@6@i˂ Pi "FaEa,NZx2sNoo4vxijWK_+pxtt8Ãw\}7 Tu_<ܹZN)MJp1bp, BnAPQ >2X\<[.ʬX$"b^t)Om'Rh,{wWz~{|t5V]/*@D"f1Ɗ Ͽx|rD*\ DB%p1,2YZ;O'HAfh6Vjۍ fA60t5 P1&qW"B(sHAR]*<ǞNQ%s."gUtE^ 굙P5Aj:0zoWu(dvt|8śQMa$VklD#h6^g_|:ֈaMbB˱/pc {,TT(em:QU.{48n<{T̺a@Ҭyގ`e ZSRo"b Zcܚ$ nߵ "`q,lW J+VanEHg$N@5 1 Xfcm&lKJvh-h,3AlhZn7lҨI)Dp߮_" J9@"ȑ%.)f "Ԥ`E+/&fc2f6$N, )MH̬Q:׮xK./pkqk-f;w?I'dxtx<x|st.ZV{^lnluV;h4 ؂56&2,ˌkI=jv]wv*W;yvF|}?o[ZVY^IQhK;9=y׻ޕdisF. ǕzU(+ *KrV+>G^ڝf AH3qA h] 6iaG[+)(4@p B̅RXvRpr]9yk`ua(mn;Gmmm1R4˗;ݶ1YibLT⍛*QEn RLR"p2ڨ`]b>tWC, Ѽ,SN}*4^ tԡ6saWg7QHh@luf~V q6"I#jkV8wX Ra 67FhZl:Χ3 (Β8jssS)f1la0 4 $A5BAdkDCZIPR`?{$@#+oDH( =cQJy~ Q)U f2FܽRV w6ܼyk0ul.]\Ch4M"Lzݣ>fȝ݊HJisζY@)"Tl+ۘJnN RF9N\DGy̎㬊Q,"Qmk X&Uʘr%^ُ HyXr0G&oo'667Zc۷ntwh@ ̘0 ,ۿ: ?፟.t<ahQ[-zMFJi@g1|%1}+˗oo]Ǽw?#7hD1XUhNͯ.Nӝ[w'Ο{Փzz_ozn޼?F?^Wݹ ]_oНVJ~s wrhjYF)"ٜH)MhZ<><_ѓn\{zbdρNDъв 23,(Rޜ0`0>@eqj8Mh4\FQs緶ol n:hA;2ɲ|f#wu$'82C~LHX2(nVJIq?mln\x@ƼP;5H%/P7 ^S %ރ o{X[^u$Dsʼn!\@+W:BB7aٹo ݽ.͛v8̩t.@n$Nn: ̄HZk{ %wc-AgPn.VRnx*E ۷, > o|ּ~㯸xt2cu5a1^WU33ljPQoпчONI||||p|yܕh[3K.>x,Yfjga=ֳWyzp2=أw} qin_>~]8.B9UK"*W09zDHAi2l5@lIz{t8<}7YjLkm)dCeWfS Z!#$͗#yY?vlz_.qlRJahp$Ih4OvoFlvp/ `v;s] :Tq:ciBF1^PSB݁O^HIJaah}'|KfjCOy`+gPf޺qQ xODuo,ȒT58R{s,Tq> prX~ UA[=m BU[I>[&1 ȧٵ Ŋ"@P]rwIނrвH))BZ,(n>t2oLOb- jf+0ph:nwۛOO(PA,SƘd4޻~ڭpܹO99><Zit˗/u{假 0JO&P埒ŧh!T7>5'R'@Zh|xxıRHZ!I~B`іuܽ{pJY?֝U\yTy*5tsG$ Pܛ$/P[5y'"j#AÍ ! @5fqfZZYi =u6A@Q77fN{0h7Fkks3PDޠh4rS~h S&ZRGQP6*IEa!h4Lq`YaZED6e`#'YQi0,9s_|i\To/#,ۨT櫃s]""9Nb) nn6x+Zgt:=999?^y]h49fud2ea*,oRz1Y# 7n޼rC?rK]b]5k),wnc5 ZXę^X4p/NaͻRS&07rAJ8(_(xAR P  /&1i| ( <RlmuU8Fpbf3 )[_kL-R4H;\yp4ܹ}{04 ,˲jwWJsc07Q mH<ϔ7H(0 W'8!T}ʤG2 2z r44:q>_gQ@"1E,F#=h8Xt~{>o6Ijmo|h b6iO;8:<+x8bADA%&,v`qbIl<7maaٚ;u_xUl[iBV:œ󌀤5[+%Mߕȏpl-> /\ff0 V!!YA]?~4MtOGtoa6[K/Nb*\U_Q߽s֭[?FkӚV]/UTDIw}C泌*$вâ}etRrKE[F; dN|逕;}! {sHqDu$`e"ֲ[ԣNЅ׊\kYedɲ,RIXk-s:ԁF"wN&iE^|YNO8љ['ah<ȣگ;43RJܽ˝~o2A0S2]{߾i̼wZd&C@kk̝OOS@Z^xh>&N˖OONËt2>G&Mnߺy|pԈB$APq?EJ)I籈40 dV)vnxc㩏i4=#>$ׂP;=ɌeDRAAqH$3=_M Y/$2(BEv,\$nd>Q|:g3'\Ǻgb_e4ͦFA'`w2X<~3Ϟ77$Q.m7ZUY֖:9י ֙B/UEEO\ %iK_ ܹ;0ˬMPC@Œ9S RHĖ P)e-Vdֺ?ɲԲafIYY4g,2et6s&Yt6'/wP)5á#O4xbʲ]GN'"w/j8ܼtᡣ^\#s-[l-+FiLf$YcցNj4&EU RD&+7o>cZ,kXW ,D`c=::r^̂, m>|9+gp^yZ!(HvM7WXHn HW.!qqT\i@+dbB疥&D1&ckrfyll>OygIeI|6|hbMf92#δX+Tn7ϟ:HYFQ_;{h8<99m[[[fn?"2N,r֥KΝ?@RDIAP]P$ 7FrA,.Kyw-(5gx,Dgf\@J}T9_R61 /'j$\Wd9CV&`a:Gi6MBYJ3(ҀBH5)eywgW$I b&W0L3*СFP|;77-B'C: Nfk x|: RVDX!) FTQB"Z#BY5N'8qj)4 S AefgwK4%8MOmfAbaw~ՄVZnrRi~X,])D.Y(9`f59'R.tXky:&$mͯy{_ǵ@'<˗{ UjU!|!"L`ٲ,a8ȫQViuK*; Bo!r<'*V8Ê[zHק+>~Godگ}C$t7&~D G8ㅈ@sxCRJy,YrJ`rOw13^1:ڋ@+&P:"m484MdmEJ#kM<]Ra2?°A?:'oE6,:lpfH@QA*,ck1 B1!'&sG#"K3D$IDP [EHH LpRJ;BH1f6A6BZ$T"av;wwZ[7nܸp|>apw٧nw.]nzN?h;fnG͆6bezAԪt)d`ukOjCAU$3/[X6@eny:M31bZŕe_@֭bSEX_K-| U{@t.0+N\JB_X0@!7 `kuazft* œry=\aI$3'4MSk5|Ib,N|gIgQ 2C֓e0t=ز4 z+n¹Gyt4sFw^qC)Y>wAS`g5ݹgg@~DL4"D<>:άۋ˭V+˲ .; IF:ڞ9 S` " Z"z #?Ψ $1x0N@!~,uw'Q@aE;YȲ_pTgہD/&jJ9"@<X 6DۢIEF"k0"r# % laBM p)Rb-[P.~'$a@ AB஄FQTŴXb@i!,-O  B%"aΌ8(8-a/ggĢz6D"`3c Kcv,v29=9=7 ݹtW}kf3@F/|6n5[V;v ~nwf3PkjXSɲ,;Z{}kP@AUP"H8fplO/Oczh6D4hQR.G|{="t Cu{{wyrj̹'v>K )z,\*S8lhl݅}zb]\Yֈ8wvH=iD[[Cݴȳg ?^cf>;띜IӏaPU]B’HU9c\eƅ{l$e+cn0,\,[,̖1yEey\.E\bL&7"3Ͳ,ck *E>3[1( }ϋxZh<ehIwO~Çwݽpp_Qot:]o.i(hZ[[[xJx˳Yp0899v "*k :f5d T_Ya+[ VU6᫅C|zE@DEs8Z#,œeieigE^,tiZ)iy^ EўB"+9(0j5`xzz,˗/_qcq;N}^۷d_(W_^\}[ݍO}S< {X~nmm;jP@NOma"?LKSfEAl2qv]uiAt N"*CX*tbIȭ_[=g\6ʥN[fᲇuI^tP#@((Sxz*P5oTG))$勂:NEF+k oZk8(`O)#+<2l Ԡ rq1Zc,F-ֵ<'"&">*- a"*"@BBE%$_{LnQ!(B$,եu\@IxF|/ "~ݞ/i 0Da,GG5flɱ)2 <1f<Fk ((nZݍvh$QODٺ˗횵oW^er kBZb".r6kD|{3V.Z kr㣋/e=XTH< ?0c5 BabDE|T?X/T%)htUβeXDU g[+2qXs+"R4O;/fk\&7ưlfif Yy^y- gY"XkKg AnlltDdĥDŽAO}ꓟK$z[ۍf__?~Aa(xHvʼnhU"A'>>y|||:[Y~7e'C?,Vsc{ѢH-gAdi~-0|tx[[aD8 p8l;;`EXk[yK5t(^J R"c5ea>t8H tb)G5.*ʄn+V%e%V+* T\zj+"yUw)rQJKyt_Qj;GeZ:zxx0vvd8(O(txxx;98=|(E)̅7v;[[蹰krqvk+ q(E l0 ._aj7|qluB"((L I8M7Wntwڦ*@H*]uR&Y=hShX?q!"@^) xTgZ LKS0mvuq^i*/hWi`Bgv2@8+|ljdZ!ݝ[ى,!W7p9 I[\>Ae[rE<:Pj~UW(&"d, 䖝ҴBH0 8FT|^hXZ6g>ɣ~k_tku^9<<ˆAi =: n k4Kn`NFw㥗_  ֪ej$%a]VP]J\-ףWVrUuWn֝CS_&CO" 5Ke=Uxg[=#'I5rXF?YV7K tyq44\ eZ8od7rI;k߹V9eZ&0td?z"1g٪U=Kx6y\,8S~.i._X;Bۻx֭;5YfƔ8 :hgV"+"gC ~QfjwZvi6q$Iy"jH@)eg"]tVq1fTX ܛU\~+Fկ~6(N]lJ9уvM2b:kZc .gճ><=("ec,+)-L@jieٻXyW=A)ҨjʼnJl^Ei()<<ϊȳ4˳Ⲑ\椕ʶ ~uZO J\st:ke Zje3\ Q5Vlq|`yxJ}u;]rh4r)(0 h\EP i;w^[[3_.S.m=x# BA` Sچ$^] BRJ) +,]F>*8vVlv:FǍ8 =MH,bظ)1"@e.X%RULq|^ ӴeEsUI),: R(NGdznzg.Y=zҀUf17'VY[HrSyeAA(UG A$lSLRe2a̜)/i[{;W]˳|19YNdccwJ[oh$EO3_僋{r)zd[|[A|l8Y6[.@%_в,lb0D$@D0\|B3A.+_Alm.ÇVnj_El, R|wwWDi:#")ڰ+j" MNg.HmWk:=: ?R e7ABzݱf8kS=Jk&+zCuOsSγ_ok>/\qV7MBXOփ`(4:WcOĵ9_XɟBH %^aA9UGVyv%bPJ)]e#7Q%F8x8OV3c{7^{kL,}w7A?MfI?7\в0F$#"s\,شtB$l$VoA4'f7:^RJP`ԜTL ,Og3Z{J)_p8L @G0D(Çe4<˝ .zoffCkU͂G>(zg?/tWwuG<1=ִ,9W<},cNY |""D $Ze֪(^LQEQP{~>9;m6O}ommt.?hl.n|KGiʐEyzF$"K nV A@pXav. -@\"\>b6  J)y~Qh6[Fw4q{T֦YFUZIenoS222*BDRݻ{;,DXrўm u 9GO67w|/ lrzvrR )ɲ(؏)0E"FHx-r}FP\kT*lR+ˤkV7LNN|2O'Ghxz}1Ezj,O"ӣRl9]JsM$`s`?>z8wvpju;& YSppaAE La:#w[UdT Jq0s  tQ֪K|KT6"m^VQKȪX<}X]nōƩ|6TEUhAWTMz4Ob ,s.SοJ|ʪ}ydZ +ʥ¶xrϊI>/`)c{ZiOiB( I@2xJւ[P=W^XQزkV *q-lmVl2*)$I$ F~qJAXcqfa1"~L2ޤ# ֋5ʸh![Uy"noΝo=8."_.0 A8&)Jh 0!Q5c_kϋ$I߈NGQFa҈ͰqXm "C5Pzhv6`klQaYXLU(f#"_D =BBM&HiM,\5<++*I ,]E?( aώONi% ൪VL,+'{ ^SOToXɂjBյ@"Ոb#Ku!-*ߘ֝+d5)YeJ BPF*l3!RYv1\dnQwl&o~stxy}"[D6ɖ,N'ӳ`<ggY/ͦXpQ BĄ5pAaeGDGA)Qu8:6DQw:0 t:fY6VZDOqQlVi6ZV#0<ǒ\EkѺ]eaKyZ8o.G)eJiwSZk1ѨmookY[j] "V*gnUkLF1lnax*0p0ZQAחM}f " "[G U8W*g `ZYEluKlł0F1hEą@{ioCLȏ$i48N(J8j4$ ?NFËb D:Vy5"!H3@Zr%]#S# 2$-1`0(Qecx6#ZFߟ/,zfD[<]l6Η.SkXy+Wnt˩"-()7ZB+o+4&s%Z{!$QhMQO"4jbf $\'eLuل>(B3BV@E @֖o*R D~`O(A)Ă{l W(kbͼ@8g]@=Y-v∸RhB= /h-zx <>H@Et]vߺ}/.w?퉵q9WqB*D#i?P,<1 c;y.olr96r^Q8i6f3#/~*YbsEQXgpYV6'2 1k5Vi?>::[[[bܢ Vqk쟋}#e8===;뙂}?V@?ңlV*[*DTT*UaT,RRJ@ز(T^lӠ 3(O@"P#7( AQ6 #wk[aa7Qxa셡c?(!!l)(\X#l];Mh*]o͊/+"Ƞ0[ƅjƥ[[/D| ==Mg!2O?O{Q&Gvt'''twghS…0VwˍF) ,"ͤtTRY/$@DU1" ەJOҺQR. @Vy}ӚEāeC1ws)>JiK!-dˈT @MRX.`D!R1\ 5T00fvٚVt)g 3֊uliB(#п!C 5()G5E`v {Gz b>eYqnlSe7q<9:>NQz]H1Z†A TkuWH5;\rfS3Q6NǃQ7Ɠd2Ogt3O2 d@igEJ#zfgRVT Ol)"lnmn[]AsthZݝ²R4{h^D~{- l7fC{^7fLfF~GJk?pFn]eX&!9-YѤ0?y~=L_,L X ^' Jtxw&i#Nŀg}PzxpZdsRHKS(^"(#;  *Nn"~ 25e!*`I@+pM 1a7<{^V( (8nEAGNhvh{QSC-Hֵ,'\p!E,ȈXmiFePZr*`qFe 妒r,Y؊}|v4Њ @[v Coo? |y|{NK sׂ0 qx6jM\!(e6 m*)׼J+xcʄOƠR6V\y*t?vGkY {@L(bQ&Zf ZaqFRj%€F&GhY")eɭ) @EEyX#$艧3e!@J)X_qR0"(D@ Ik]JX@X/zVUrzM݊ufQp>I[BaU9(B"+Ҟܜ + }xt&<-1Yg&PD%< d$fN$D @lɣct(n޺?lwNϊ’RyfnMӹfCD4EPGQ){F7Q5fAjZt1ԥz{R⒧V 2]JD®\1 U M@-wy4GQ,=eCPI>< z'UBi9P%‚!eD!lQR`@@HhXa1Xt8 }{z04fšnuø:RIS'M50RoXS9& ")B`])H4 S("K@)1Z[TW5z,gWhSCҭ^AzBZIe~͘ ujB bł %,Y.RAmfҍكq+/zc_'ɰyzf؈N+ ,ϛQ$ [;M㳇wF AH80jV'N0 8"i壗繱 kZx~\FɒWzhY=,eU+ۙ`0E @(DRl?<~4* \&,͒.T ʂ1`-LQ"&l) }?FEA^FEa4Ièш0nIӏ?(PbEYi@FX`)+Fk*T(RyX1f^3# ]B>Uavh WZMG â(4ŋP35Rӈ2&UJ,ˆaQDΦfΖen$ 4=(Ns%|:9!J[S[ZVI6],* ["rG@@kXqRJt1+Uk{=[ɐ`,[=?p%R8_̗ a$-x<iq& !kA< CbQt< f؊†eʰά*GGǸ.{a,[k"O~܆Q,ew{77N}ttx8ݻ =<"*"( 30R5ҊJ*]KB@.lkK&E<ԍm5b2.R\,r1't2a? fd~|>Kx2w:W*l||WVrc1øl_2B">񯥱i]m)ZU"bH&elaQke2ϒnryb;jADbRJ+ &ͲHw/]pX'NE*D eXc| S̗eܽ 'xGIQ@3#򄋂 ɳƐxO4|e8b/(U8+OY,+)y i!w.a+SJmτdm׮_/wIѬOVSEƘrfXMF[.`ת&]ӞŐ!"P\dZD VrZ!(uNj}E(H2T3Mxt>zw?y1\f4d"9$T\q?14k84l7($I궻[{wOFl4͝ݽ;w陵՗I/}?zW^ 7F͛7`08>i7Q3i$Ff+8(bF_H'p0|ŸAT @ g" nܸsa<`D}tx{`" FSb2b QhP( MI<!Tx#?L'Fj6FՈI7xP/0h$@"*JB^ E)AF.#f J ) Z$d^+QXluZ3%xD%VkU,}ylv`p+Wb!"gggtooVIAbcҵNp8<>>ܜ/fQJy ð 8>>Z,ۀlZօ Du1BekO Xק3d%;cȨiPS<^ree0_/4ϺfU^ 8]-32]{_~[ՃwA9ZE.BDR"`U"H쫟{~ǿ/Kz˿ϖ>(R-hB"XΔOs#nֿ* J@xZ/57KRHi%7b dԦOF4 ):*cմk|K{=;MzYp:f4RcScf'҄ x*d=k4{NIQ19wy\4Ao}ѝLƇLJ?_|ŗ~p…?nz[o޻ᄏglt"uv*l(v;W^xzd񅳃]Y?ADq?+_Epttt}yKQ^ڋiӖZZ5} vDaqh4VlDIl7Z8 TܑF* TCQdZV $9\@DC92DYG `E2?4*. ; eD\⍜b Y *_J[Vr\(?cu<ωH)/7n\xC0Z t¢(F~gޥK(vcRҥK[[[v8ty[[Vl T3 RV+qEV^wl-x݉U{kc_pοN^/ X=@ 5~1F+l4Q#?/M XeaiT X@" T('>}wv~OKsѿgj9_4XaQD̖w?/̩x[o*3*rƺX˨1GOuTM"X:!Uhu1H s`oDԤDܰǓP[h4zѧ?^p^zјXSJyNjnB [Ȋ~`8w*)XNpUepB6 (5tv5B ѧbn5ڨ4[HI b+/RŝN%I3iuG͸AIM#yy@@H,dsɟ*2f1[d!B@#T܌^/ 9ld9 `+g@ %nB*G,{l agǯdTsXk _uss_-fy%<j6N( ua/ \,'I9~PEg`? 5z+WApzzyM777%*PWJ"|JXK4N)1v"D]KWJrUP~\/eP4k} D`2(|+? SRNb0no?g$ 1VV$]L姾߷.n_"Z5E@en"+}[‘?Z@\5 ϻU7Rg?O_k z=StѣM̰4M](0^i*m~x|ΎTD"@%Be&ͦIDI#lѠ<"o*ŠвV OVV[|yυJxh&b2I'a F?L|<_pAa:3Gp;o==OE_? >ˁRw=P~ŗo|PW}wFl6FyÓonl/s GGW^FTX&`k@8\7~IY_‚ ONzqD ZYwܸY3(.r%921,.pfMe"q;LzBTJʥ}Ɓ X;83E Q*ڎK^AD XU Ró b_sW`\Q\Ta9Qo<B5!: M㓓0 4}/]hҹ4l}|6M&S7!yw,Kёɋ8q]zussZSJe˻{ƺ.[N(P yOkK繏tlg A ٽʫ?3?o> 3i]zJ[XQ#_Aҍ/VH䁏 ˅T(`W?_H:o~7 df׮~ֱ98 Yy6՟;O/?/JyZ.\L5B"qgů̏^olXge]]y[.-ןot55SABBh67o]v;oo0.ҩ0e*1b1I{&χI1bid J4XZmY* o#1Ԟ-V,0bJQX@Q<-ny-a*hZ6Z,Mlrt4M'ax8 !IQSj8w~7hliZhk{n$I@$4F…˳Axl`%7ww1/E"YTo\av6ek-) ;[4 c-""f6=?)"T)D21F 5!XfBpi\:f7ZfwUZeQ~, X2 ^xk(X;S}ԢG]M8T9yqYᜩyWY򶄕;5Xf j4W\so60 A>LK/Ph:0,Ey6֊V$J ߺa^{9M8MSE>w9E8O:Vq!rvya%Y3i\rJfVatZr?vlv-.u*;Tj8'ʥA,ֲA _} ?GoqIC4 1"dl-EE4e1|_~:?_ޛ?L6b9S_봻h'`/^ۻɫA?x`َJ6{or7_K @ȱ5c/گ?arvZWdb 㷱X&y.DUki;qm]pFw8 Vyo) c+<+(.ى@4l2[ܹw猱~V8) f),'Z3X6V0HB![.Angn;Qd{g{yaio:K:6iƢ0Z{Z`ưaB@,ʗT zpVaбQihQ@C)~W6.hZ$bvl:M4Yz`r֟~0-t9LN|FFƯ7F>9:!AXѣx4<xaqhhMټ?ܛ^G~4 >-? ׾*`e|LzV`Evq0XU "BJEvp6&sL^;NQJ1R 0Tƶ 6WZ)t@!D֊(R!c ncR=>3ECR`¸*ci'wǽ* ndѮ(zu7 o9"2bNkϑ0NfjOO~4KskRDFp1/4w`k7(X,Iޝ0 $]."f>=9VG{>evH"VH80"3V 9[UY\dUw=fC\"3ʼKbaBqn,Wr"2`7[ou<;mFIC PK-;֊޿__t__T&-X)`_;ek=|pwAkq'J{@{OW\luGS?J^?Kf/?/lhDM "ڲuHJҡ]dXAilX^r'<~zqo\mlD:._/[Z($:l?°?M |9ͮyb EQp_+-̃᠑4666Fx6NZ"3lky犨lmmZ-%`HHY1 EB# )@x޿F3 0=!*U"!憵0 ٨h0t26NhxȪ<_7*HO3Clټ$^(L(olK+"(nd^ySs#A pǖ,, b t:}yGZPPv7֒R"JQ9.0Ϻ}_DXiύ((f&RZSNɲ,2.=zũzר٤c3~6.vFBAOvPc( "l$/xZ#4M#(O–DrM&y?==~8fs;nHeY݋hHB{^G߸j6ArDPC[[[G,ErU ^k)^kMǬ_j~-ʶqXf"+ i*;ηK6F:&M)mFzz\efN/}/}?/WQ,`5c~owv_{ևq~0axzvr.4bDE:͊<ۿqn47G>kEy&4) c"L7a̹LSoSGִM=W,#!\|{7gyQ-pok}wdCAǞ0~F;[L8ϳt*"SC(ffk,!r ` hK z8Zk=O1<jXy9BBm5-d90#j ̪Uںiۀ|7_S;tz~8.Fpڟb/3kFeA좐!0(a lt|zte`(9|އ{;O!"J 0֓†]Zx4}1E4̢5^M&nRQ75ZeyJ)Wxo)iqB5#S z"ye0(n4;0A |'dSz#+̳c܋47!]@iΖ!xJtbmI`:޹sӧl68f":<<ΦvfX,vv7^n:G֪U;J{R!V^ɭ*J)K_ⵡPOK}@ֹe "oz~09wT*OQD g}ߧ?AW/{&"[Q +t0$|, EgH@<cX2#@|?eYlltlt?W?4@@,KQe &zE)UVk%?s‰==v?6;4s=vhX}AIHONN泩־: b j6wwv (ܕ5nX.ZwvEQEN-µ pNwrE42uv답Çyw݋/2s -O\=m|~ƧR)ue-?f{3JcVŰr8+,z1 EV #2ˍQZa޽nDѣIntR^FׯA0Lä__t~ߞ2 z 4!b-RJkkl@璩585ݝU^ɈR ;u^=zRmD\%]h=m۹~p_,3"!LMW]~ν–JM{^qel6[w>u|xlgE1(X,.sXal68}61sE@՚f7on6i,˶[VEae XqQc؂ǖzpq,(-XTLDX+bn6ЅX,h}d,7M'x8'`8nàonnw].Os?W<`=+ڰ/ڍkfs@IkB4ټ(Y#*\ drJih4:>>.MQdzd>;ggggVnzwZ-k;wvvv.]Ƙr9qss\e j~Ԃ;gMtTafU F `XDT<+-[%(l6ERr4<=-f+őX;5Mk9 M"Jn]|1r[MbkbWXq: ^YdvFk.:CN`bϡ^ ]Oú_@n֙kQP)$`j\x_ɿ%_C3)MόZkPBo|2N8e (b`.P}A eR|?[okEvu!.L)ta c 0:!{Ct:]GUin%mM|B>'8TG&@ìA [Q+;{2aѸ*Nf7HDD$ɠ?xѥW{/i\љ4[6Sgk-;.ci6\Dxz􈈮_\.{hd|kk˹ܹs֭~X, u֭`j$ p:.lc_.uF#s(j _[kP ! JkGmQV"er,igqy1 666牰d8^tG…KafӃ]sgtg~;N\+/~yN2nM\kZ.ղRx  35=WFUK*]H 7!r 4p.I?#Wڷ_|wV  V ƹmnzf7),[EmL/ THPO4Ily /t1Q5nER> Njj2x$XBlOtVG9'1㖮_h}jfZg;E˔ :3@kRZH3C dOk=O W(,=zhkkk0{ͭVCbPJё"w76.\8p>"tMW0csZ9_VT">*9RA9U@ߩ| 7z#Q'FlI(bAH~(i"o8$ra}ys׮~Ѓû{|-FQ<M> f:n4R j(P) E|WZƼ=z,NLX^N~d9w3"~2M !E+7i}˗/ݻ+B9A[XRXfڝI?X.aX=\0iPJ5 wMOglJHv;im4٬hEqΝV0@U,l Eȹr*/88E @"@"D0 ,"ϵ!yPڳN3Q)ز(vU[nnHEh4D$Mӳph4r'?`&B};YPn *% HVDpA"QĢB1N@LP[~ bnQQJ1OfM|<78Bf&+즵3ֳ/|t,+vvE!y;;۝nZ{lp8ÇmQL?v%-Ӵn֞ TC$Yp4ú9倇qVA{(6SinUXcS |^d|gYf(|4v.$U(0K$ie鲵zvvvtt/ZGf<ϲݻtR@2r}gu !XIK~:Qg=PK3S%*zHw΢*TFfqR6q`["ņZcaġ2E 7+2$%0,߽Ev?7G~G/Joׁ5EjLM/,tA/iXQ5fqa?$.?wl^rgc(A`$NS W2/?~:_ok~-],# "R$;ޜJO̅=!\&)T1s6r nQ1n 3 \zڵZ+fV֗9"Vy));wnln({_i6,<מD,k (Z;br1MS@T$s&v"ys=ѣxK*rrUJԱ5*R. d@IP<݊!XB"`&qX%F (ڂV"1VZPPln}xhrKHt0Og[.5CX  %Z7Y2R2 FH:2.|$1lM&25e|Pڤ)b4φya|kGG'}@x7Yh2OIҊ[lm&0n%~XkAu XziVʐ6VTʹf2۫ut*Նbeʝۊ;t;i֎ ןAP4V8u4,H&Ykٚ¤93MA1tD V#*cMa(HƌD4s36=nݹ9I2EvA$p\cj0Pn<|Hi'CE)cb  A /^UF B@LL=I6<99*fNL/IVsLzє1 Cl|zKFr0(ͦ5_{l;oY%.Sea5"V[[c5Aw ݥ |?5bMWZݣccV^!,.O=\ BH@Q|ǗۣQ{wKk Q-bH\iw/<| |yv;j6 `YQ "W"@ XM8Mz k9Nbk (Eȉhss(PUZ6- ,x:kxޛv=wAf4F?8==MFن~t(VyyD @wsK=9V,,H$XJ Ъۭd=@Q@X&FpcpMb-Gh@]eZD&[ f+XZ1,̘̲kAU0Z($r&2a6!~yAkY $JԍnƲy"pttN #N^jZƘ8˷w9HQʤJ<^s W&XjTJ8X+iq=֜BcլME!~Ap ,VH5?{$vwbkcy<Ý\jbVDL 7Sҋ̚lkIZMdX#pqq3{=" *EBvŹy2#"=|{o?~<3 ;Caoqqz?/b}1zbYEtoC՘08N&ho탽) o x4pio~ͷfye V%;f(DBd`4vM 1 2%PEmS )ьm%mH 5ZTF5ը%wИ̢Zc,F VKSGib Ő W&N xG s䋂 UL D2Ɔ̔UUy޽`pEQMgwy7Qʲ,}vQ 󳾾YiL5H:SԈ+Qk$AnI&i.L)eu])mu@P#֡&H =FmG*j4P$!џ3!70rcY[pg}5ESz=1gLL?}r>Gq6ؠ1Υdz{P5z\"8>~:3$ 09\;|_G?G`^#pOf!’l`׈Vغm@eJtR]T !l{g&iƔ b”#A'4xmmds}x獻<8u}9B&6FFt@JـQPD&U0D65C 4Mj'\.t!)'j(ZOG>u S~IͿ);'VLqz^3-$Z0UNOO?~wGyuB`8''go ֖AYmO:FZdX|,"J.DM?C"ְ 6o: GɘbɞM$sn}}}}scwg}cJj@Ι)9w{3YqQ A#MB+⚕`@hi&@T#Q "C)i$4 U@BA$Bf9IS+*@L1DP0 uԦ1+CS֡Ch$FaC X0 f!5&,(p MC Lp2q3 ,4̱B(6jLږ9 HQJ .A4|f`AQ ?~< ;L&GGG{{4*l,QCB\qDY 4kp>c]|wWjo BVrZVWUoդ\v }dPD4]R$*(2sg43DLps[|xk3ߌWjьg[o~GJ]b~UO7v7_ͷyOrmh9ec8:fxnè"TUS5;(]\bᮇu# _jኣ?A;-`Z%PԠpoP_ i8Hh1[''Lb˴2؁,[[O.&Ç?_]^^!XA5!1#2%!dsi)M^&[O7K?ѣG|<OÝ,eY%g|~HEOO&ؗ1MK}^ _valS_|u/ (!A y+LќE.=X""<8D (9(҄9tNj.@MhChQ@D`J= 123m$t<1վ.q!ԍ`u]æ u:U JPiT+RxeA6g `6DBd A1BB2#0gռF FꐀQE \ 'W~Qb]JLAL P03ը.yQj,8;;~wV&dm&QUmmmw_[:v/6+x݃_z?׀U?Xg WǶsV> C&~2W}p<#ALQApҜE"y5 UX*y[o?OE1<ݿ>=}v$^,jhQ;~뇟'1G4]d"RMh>}$S C4"""()Әjep;6n*һ[>76zgϟe-jTMm޷ V~4O&WMS_^^xq:_̉ i"fLd$"US71Ա2|6Ys7DFoѺ" vl>HZo:(*!F34.Ֆmp}Fs{wuuɓgώ 1ɋt68?6Xm k]Z`{JCmW%3Ὧ:9 Ԛl߾}{0M`2o 9!TU%DTWlf̞U#!%++r"( ;3&FFDiWKG AELUy4hfQ.bH]k&4ehZchC$"qY xOـ3y6䃱 q! |*ΠS0%PHƠQ3m 0AЌT0Ves4hvǒ-^=!fwpvpD8{qWvvvE/br;y^D ٳSBmUU7Ljn~GQ.Ѣ/$trYE֗tYm\c/mhԛԝNfQW.3\!i*rkC:}+xDas7LEDabPCpdjx"Z Ƥ`!X~7>O;[/>x?7_wnOx@"P@M8RB@H!l`{6h{fhQMMO[ժZ_ftU>1g7R X(LB'DG_ս/?2ϋ*DEQ;UygY>͟=\vqvw ABs[d0B2TrV6e@|4,=gs{ij.r,dz K]D%Aᶓfc ڴde[i(|J^}iOLBN|[nӧOONNFsNE}{OlNS3899;zN,b&v^Kטς:SmDX'''|oF3ںupb )g6Q)TbJQko3$"D10fGfCՉ#FE(fQX)M:Ī UScDCjX,.i(,@ EPՙ9)<e9zfv>+21 `ֲ8\> $$%T~N05/1LULj&O T Z7"bޅF28TdEdh "N./B<Drɘ 5Ad6 鷾w5OO=YQ6mdKARdjQ#Jh4uf+V"V9u$qP۝^+PZ )gMltUxqKвXz5G}7߼=m̨ ȴ%MVqmms>z?>}¼RtjB@ kFa$ QyԐ!9./FPd1#&"1hOr@dM-d8355$V1/ݭ-SD5I.DRe&3 crL(&jꈑTTDQEHA'XK]ISCSi5ElmBrQuEgM5$GPd [MUEeusciҚ`33Gg"4hF&҂t@DT"`pcl  !Pڎ7B[1CF 0crm99f\1Ȇyy3'"= eŧ_cg'WWWw4MeY@/b0moo3b26}Y V-;--}u[ HJ$zUHOоC b8iiž38}86g`@m{6 u|_d1cD\>?O[{_x]Q6ogoG_G'[+Td 2aSb k|_A4(h3ɹIFf2AO&•@$[xvٷzOg'ݫe x}TĬbB5(wv~睝O.P$GX[[N'Ϟv[exxakGLU0wQ$j:ԃȲ{s;Ǫ(E*mM uF>YQ99s!rTԥ;%hT!( jЮQx3 ijt@%և\Qqcc ; H{{/^:/\G b%}^ xP}򳟝 !PT`ggp?/CӅ bpGwزLUFA&)DE> "Q;1DPƪj~Z8[UZ! QE&6uUJR 0!X@Qe1T`sD]p<'9@2$ 5*0Ih$PD#[6B?Olqv~u⢜,~賢23dPE:?g?kfA<t1 h *2tl}޾~uQ2b{OB̤J}+jNbquqq4}7EQD"2:圓G{39smHFJfEPӰjDSY,ϝ+|6] ={CPDAPCUmIY5^]KױW[o^>DOOM&l6L35u{{{g"{_Ç>xxiX_;X,*}A-{_}(UϼDQBYg`QĠ*;3fAQ@3l1QAC)u뫓8{є%6Z5D&$DsvEUDcv-YMII"TR ƍcA\rr9'c'^]|m1tEphL- C`FD2bpN聴 43iՇC8>;yȀ <0?///'gkk;;j"}w<{HU_]MoL VTɬǤw ¶`u1AHإvS(5,(k6)zOa J%J )ZE &5[1 41H$u9n1DL HjVr7<5ps+褆̏R)iX.ApGn}vE &sibT@Plzբi G"GDcf#Q 91zL2+gI`X-|6룝Ц䀽Jrwtmܘ4#,ݶr;[ŢY+&ί|?ӟs> -w"Ebt(߸{1gnmmlUFD2b%Vt JH<(sĈlF&m1Rİb2N RryEhX/9WpV9p> l0(; IcѢ: ^҄>q]+z/5Z,²%B"}F.ڭ[?~r5A\q]Yo|]z-~VZTR6HKiNOΦWSʲj"zw67yVi$K+jꦊMS7e]EU&B DT2D!iXsHf我I c2PM\`-^ejUbkO5hЈ֢ Ĉ4h 1DQMM "1 S DA|gލxm8Fx0{<;e B=˪Y44S5cQ@0el- 1Ȉ Nl:#fL>y|xp8PS4bf3+2 f=D;wn]\AR !:ϸĝQPXFJ_na^^ Iu UhRA_?`Fv_]\]v<}h4hFՐ ;$4gTu40~6/?~*` . s؁sXF9qE%(qqw &Z%'M02 UHY|8h}c4ZFb!19b=kM_29s`%KNi5ja4$Od *q0-ú5ƫ'e xkkచ/bY#jiPH PSj[h}Ϙ hB-_{ic]Ux) sagIV"ĝ"j:$vur3,a(Gɩ#kSs@)0 -X44V2   dmQ5BvjSa.XQj`QK @ Pcmͥ22!3*Qw)*>UJ?W=bdmZ#?@Dd_iF Tb#;]gd@>~LD !F Y NjEӓt $eC"U,"4MPj**иtP +S/h^hd˽Ch"XʼhBI\Y>x3gOƂĘPB Ѵ)NG2Än$4m#%)F ;W?ƶr1ˆ{O&fvo:0}x-~ٔV>QJ"s;;; ѡJE(*@y拼AQY#d! DΣ3v<>%T$@bSbS3K6F$FA06",ZY2y泑cPG|ey,|ygYe˼|Yy9;fv@ 1QۼBĴ rrB'(X"R\nNaufK%,Q7B[«" Lury9N/.E1@ĺfٝ;w8?;8pxxs*o@qg+i%&]_3Z2\7-# #J l5o+F#1BU@EwLB f$JH#BF%Ǟua/f\DԀi"6"qVbʲG!1+$TPg@!snPd*ԍcFj}絏ЮIvpٻo5.X˧mڍ~n~~WW_A^TUM)FM9gb:RȊ{F였W|?><5 Fb4YcfvYHWrV5=p'YU@TD%$e,D:#3 ,7 DZee Y6*`>`4a1 `IWW`QA, ڲsˬ"E@ghtHh\X^wyM9+ 0ײ%BBӳFDSC  nݺ5 *AiBAq`0,ȲRe$rnvL,ZuChʦlq6sK  Pe+<$8d̂gn4EDΆy{gy#&23sSjHFD!{-oL_RCˁgJ@zY%Muȭr VՇ2#3y>Rյˋ6766NOOCh>{{~7VtW~k%`DZӍ.8'޺OfSU5QZm =vDZcZ.k9D*.R4aw·yUՌ@LgűC$BjbCtLD3W)y._\E"9r]HXElso<7;$pyvv57!\M&ZUEaf|NlDž'Q,EktY?bgvwvq\:nLtZB^5Ѐ}`[9i}uoT D7׿o?! D ,ԥ 4AVb9 r GE$ X_-Ax0*,[ƣp>,Fl8,A>,\^9rg$ FjT.E@4)1F Sx^e ?ا\F6aXD# Ɂ d/^?~X}@(0hYP0sgfŰ AyBGȪf025QP-4e,XVnDM}.3BfG.C/.+yg9Q"+Ďgf2;DTlSh) }Ƞ#@:%*^,jeZ匯MУe0"\Z[1ϋ>4>8<FAq jw|$'0JV5EA"u;w=;1QHꦾJl&z63o_8={w>}zttHU!Q5ID{QP$"DU`}*x 8j  Xb**TLkTed`:b?ylٴuDa18^_/kp~8pQ>67C3ft): K1q 1R43#@C]HDACw,[w|x4g'>?H Z`2=zQ;%Ugݻ`}}B#2p0LDHb_`l=;CTa{'D쁐ɉ"2xyqScvLh9ˈ3p>Q)AŘe 65)@if`` JODPg^'.?2\2{M/exVԈ`ټi^Ul:s/,AӓboA1GU]Ƿ$H$Sf-ԥV[=-AkH2NNm+6%4n˖WJ+σœZ1 )4{77wy,wwwFkιj޽{޺;ۇwO[>ţ{.˽8O~r/onMXՃ`}om`cp k|ɷ?Gq1s3b裛dz;V/Uɵ[NO+,pNRzE#P{Ol) ?>~O]Dd gi]p}}=|(r82yT<}6 UkD A󈉋'I۴PUU%&TJDܻlP46q}CblyXf>f1 Y 1 hEAU2@UM@EMt!B19b\*EЖmmOz/78W?|>Uxvv6LvvD7Z[[K`@389=L.޽;/^xa`;;γs~N6myfc/OQ/v^o,zE=ύ `d6]\\\M&ǏÏ> MO>n??ɳΝ;A0FyĜojQ޼^Ó=zw}%ѣǷ\n|׾B[[o?45fW5u 1wcQjI֦#UM"ndD,S+X_]_2&Ȳ{Ǐy.D؄x~y!"Ѩ( sn4B4u=)/Ng?'W`ૺIfUM!v  \rb"Br}Yۘ%bT %gi>6֥U(,yTMjZT(yYWl*q3$U!5g D΁5`)@@I!Ae& R1CGGι]fH̄$X-bFDp;;[ǣt΄@tqqox^OXd*MLi=;{q9R5bI]έ[|= TyO.Khc#2MD]ӥQ* =F03 b`jGZibY$hy20IC ŨfHҖ 9ѭw!uѦ;G#[}o ϕ[(yBw ,?WDf,BU]ש`6=~|4 KQ!EQYT<嚦)G]eƟ0XVbUls Wv˸RkV+J;kQGmϳ|6٥IG@,|o!;ᄈqq~CYX?ugggż{;p}c}P =7_x8)e9Ewٽ\^\Rݾ{76ΛFhN_8~Fh DdffD}94I_y*J=h7Wt-않f[\qlH^WƸ l|:n1q֝;3C0Qkx413i"t~kߏO~oei4ygYܵMD!l3Sኙ Ĕ["W$LH?CxDf" u1u_Ţ׳|2NjQ60F("QB2 9;oY.YfyQ0T"UAT-E$ `BpS<$0̱gtHCFD8=g3Sr"2j66]hOwvU45#u%z,=)f<_t4#[U R=HXq30ECTkL ߄~Qj0 0N)A$ RŦ$b6C3UQbbkA%VJԡ6@@YsV>ZA[O*q0OʟM d2 iFDy?yG1ʣGb; ʲdd2b!:Qև~X nj\}jp3xxuo˿\W)q2䴜.0ZXrZGeӼɃ#ou޽,ym$;88O=|7p::8ؿs'/'õjx7&'?p-'ɳc1.sv|- LHoT,T5_WP|yBXx:Xڗ^.h{"sU667RJDԂEQfZ7h4ͦGO6daNLjB!dYDwuU/f`0%٩':@=9c4 jZWh `ycFQ 1M|:Ng.㬊ESlV/ MF+g ֈ`.hAT}Ʈac>4fAM qa͜Ȣޢ 9;qFmpkQB.c1֭={D)2_.=掩!ƌX5oo:/1M'?jr5^ߊj qg'?~zqy f9%;_W{￿oտj66Ox~ -NgWFQ#:zœ;"Oӏ.gfS gEjcA5Cɀc^+kߊAz{VciWWMa/Gx6+HL%U9L`4)]ιKLhIqa1F̴F0#P"mcUɩˈ:e`LT iRRUuU7e(rzu5NEXguLCl | S9 ̆T9Q@*ʨA4eFRJ1h*AAA/75\]1ϮrQ +b}mgs֭7mm?;91}U7?_L]4|׏] W7/.w̌%ʋ/^xfh( (̠jEܾc!0G5P 2H]IH $AĊD&D$HmЙ)aEdtH,8BHQbH*-@ԘӺi`0,B]U=3ĐQTc4$ KS9W^.u Y_7-կܨB(˲(yGDbf|8|>L&n1FェaSw35 PINp>]ÐivPLSj"L ! G }tUl"b꼢bSϥW[!/wk1˓kP4&!.fs&jHvTwև!"tDE@"2SȲWU5zh<>Z˯ji2jQ]EUlm}#\6HC'LĵL$S)Hsl L4,y5Dag>kq(:Ͷb ^?^ /'l9 IL/N?3S(#DFjQpw{0,꺌QRl^k4PA="D&b1А#F!"1A ,˜vEDDЈCVE>e9\AQD ]!yE`lv5z lVecn[e.7pm #ȼ`c:@k?g/uPm٬0*/bYՙʖ `|9L.Y$NW̩0#wv{Ш,7{^ok7#Ю֍/?:½HM1x+_"-%U7ZuWtх `0d"K.1gB:'YFŊ|@$rMLYvyMH qNDp 5DQbt;ĜeE4=;>Q26vT$]d sXLoT>ޖՕ& {:#l?BjO+=Xjw4Z;\7j)JoyfRelyk~o8xT(@-.yR< "3;i#0ՃÃ]N&oSkJQD48y0loořH !h4bv"af@9]Ef4"db07aK7UI|gHP;eBĈ|P}əաCUf2_rQWb1UY:PDM@ P< 198r+Sk47 L&Ѡ Z=t bg?ŋ}]`2{^ˬ?^aZ|T(@ O|1W 0/p`-EjDjɫ_cIs7> []|a'#?Ԥ@@S~0!;GS R(QzH$s0XLZ2*Pۄm-Rp\)߸E>B{m{% yUZq>bI>s` f?gڔ4Ɖm[U=i .D}ʺl bFxɳɼj&ٳ޻dS1UE;???=y.";":O٢iQGe];õL%FNxl퀝/D:$kqj60LEO*f"ϋw۸(1u!D D^bBARLMQR`Q#"qA h1REhQlQOG#l)g6 HR~x-#QWHruvvqu5ɲ,F3vTh8}f4lfu]H"UUj,fɤiMUmbyh"/Dޭ!r""񈝫v|k`P (i͍h`jE rW`0˲ŋ"""1FfN}%<ݘD[ƷJʮ !u[J[})UE !63}`;g&""u=/b`n0,;wlq'Df7Ξ6ږ$KPoD;(VE%xcJͳ4.a떬{V\>դ ?DRbڢ*DY./ ,mni'˥e+Y63ms[6Sm x0$ɥC+.fιɱeY=-2FB3 OϏ8zÇbQ&Qc ,Kte>YYIX?{idkk㣪nݺ 0vw tTJV\}/>yZȊ4%R:,G'yn./V\-U5K! +h E%0׭!˂V.hԵTSqͺlidܙV@ԓWSS ,nj%O5ڂ_۵&\*Z8EXdh;l76pE.{M]9UWڀ5h.I]fjX͍Plϰ~op4_3S飯kri{v΅/.ee`Ps󋣣_GO}unU5M *|PhlHiwvu]]SUfvuu^;/" +~Mθ j_KԴG@wM!9 hv)%@cDZ؈141DP1 #H]6:[rD"t`JR3 @yq1<=yw 2kJNDH Mto~g #"t M=o^5M@Dfً/T<%ܥ$㳳33F "tzzz~*˲x<yYYmmo!bhDU777Á"LE$ ahB*{c`6E ]Q`4 x@Tu1sSȞߐ躯Gh81sUUw-Q* í-f>y]ݬ`KMNE vSs};mDJp5= %>k }"a;Mj j#bO9_ZZ"ޤmJ++ v/w-,zm6PЫxi˺TW&qŅ fr͊pZ]'@EW ѹwT-;̸c\>f;nݾx8AT% \ɉcγUh}}Ā.ϟ=}RUi,&d߿666( eY2z 1h{%# #*]( &&,?m_D͒mwA3`44bt,b4hblDR㴉 !sq=bM]kڨ BlBl8Ej^_M&|MkZ`PZz5v~ý{OO/Ȝi۷wfE^\\TU5 B*VU"'Olnnt3''':TyJ[s,KQ1s>A`? v6Ν;y*1[\VhhhT9ݞV7 ]V/H )Qzy/[)}@iP!"vΧn 3p!bfQ9G\̉5 O~BX_zYhÃ[kkc轟^M= yFDdHo}cgg ^۝]Z(t${[䛵߳τs+~?2 /nI_4nlN7hW_gG8B'nmRk3Ah4˲~~|f{feM*Kι 'kfޛY4Y%3Ɔ ôoDi+J0oKujLUSyS [\CeHcvD y7.d1XL/UeYOZNu u[׋kރ* v1ɋ㳳 1sYbvv6 :qMS"lFU<1^]]%nI3㪪Beh4w!i6R b Hȕ͙Z2LeB #?aUj"It߱jDD"In$rA>""WH]W!b]7MSUUUU(˪ MSMY.ʪL4MҩλclZռi[UTm>UDDl;ǃ޽Oiwww0C; 9X,NOO޾j$ޥtgwRXBOLlS5p>qAxOuuCӴ~O޺&ۑC_e%UAJB_F{ JgY?_^wEj]Ru}ڋֵ1@t ɖETmurgY`]`]Wv^d<[[[fQzxwg*6|hTōoTjUYOY+>zuj˙kwRm+1׆ ƊH̛L3Dz7Ȯ]ѤKb\[7YuIb~*zo׵Dՠ뾥WV{+OEo˖g'P4t3#03XUڋVb{ | @13jtp݃DȲl(բY,擋 F%@$G_/! kɕ*uGn=cTOz+} ˮݱUEuUxNLрAסF$vӑf}!x'RA-4'Ȏ}CG`q#Tj:=zg_, 7C* XѵN+7B\]}MӌF4Cl 2 ݄&Ev7¹[uR6f鞒*^I{Aa*TU%LE3dU=#ϫ"jH\oY5sLtͯf"@$f؄&4!4MR%b^UuS׋j4uYVr:bQ׵(1F1(|}--D6ayo3eY]~yU+˲,K4 M8~,h-jZ'OΝ;!|";ߒnfpՇ 4 VV)+l~V6hzmhW&B_ا6-JPHk*(U 22V3gZ {JPe*LH)o%JB65VQulԶMpHׁ mYT%[]`^[`}-r-tՒSt:ԟQJGf>n>Jæ:G[bHDX $JxqRVTx"SjU3cL62 R UJ=ťKY-IF&:/ X4s8̚1i[*b!F62z@bks<=u#"1DQ9;;}u-VADv(^ $v(,nB0MI2UUmjif4YYDM$rU9ǩqDQ$/ofGmB@&D Çww]>y&vwk#Tgc\,?|ss3Ƹ>mm~D~keDg7MˋߒR^wЅ)|/' |SwVtf^Zէ[6lRDaR+Ygʼqz]r$7/̋_|>MiBs .I$>sbtV%/\5iԓح@\V`;S̀cp``B ZJA"()AHA .77'IjxF}mkIR:ܓ'"_͏UeYݔ)BÃ"uSyA!^"bRޭbD6qBKaƺ*hh*MhLYT5d%vϘ̱s ً"3)hd?46ek3%bm{βg?ϏOt6bhB&!FѠbIIxƗA3bT KY[eQGDβ4DL*uR5SUU$p!0ԄTъ|p|ɓ<>z8mlnn(˲7x#!pPUE*7|㣏= FM3}sLVQ]2= [zPhpAP㒫Fk.ްC)4c趙WmȺRK/\ vw}7^T-@숹=z>{[se)>:zC KRIت4i:Rpy9#*SB[6 Z1UPm"S Af ~ 1u PSӐԒL9X(ɇ;otr6rpzr>hUv׏/H_ .$U DDϏ}ϣ\&X*S $" @2:G9ɈD$dWpp1 ģKbMR'z v'3df3\111!?~zz_>ѓhF>>>{Q,󄶨koqZ:xZeNM2&Un\IK~Қ:iS%+q4C밣h)$QNN\+Ͼv\UݥeUwu5SUBLUu1:6c^H``VG_N,Ϗ(3FUQÔdʆl *8& 2)93 K"Z Y2Dg8 Q:j (i ][%M>w2I4%h7w6J/HBP0r#FӼwwwww.*BA~iz-=X&}@5'G_}xmmyZ# e=oߺ3XGZ):05@@&hit٬ $4rlJ@dgN D! jddQk"Jb heSBQ75hlʙ%ihSJSLu;oo_}ƦS`~6j086(Fk>gbPQ< G5)̵D13fFHe&`iًKw5j2Ym-סg0"b 8H;DYː] U]M&W6d$rι|zzzd2bHBx4鳣h|a 3omnǣKK%ZjZ%~Շ8F1UM݇VntpAi-:S+ jKHZAѬMKM 34K#vvǗ"eqc,zFJJ]5vة8N⤧rZ7R0Nۺ0$#! "*Zk6m*Ox (TF;)`.EДA ؏+Cmi3V/ &)%L@[D$N,Y|[[;NGo磳ӓK"@y^H *;LsD:AdRfe>bMd#6F I*2#{l"Ć #$#I3"jN-S[f @dK*KU֌~B(җ)Y)C]be*1$NpRj  .‰DFfyQ]];ꫭDjd/*kV>1#Yf!Q&;;w46*C Fh @#hR Q`CӄE%sBPB)F5H Aj$HT5. yX4PpqدnQ@"O??apH,pl 2Ul|Ο9 LD1!DFrgOH'pe?:z4/'O,ΉDollEry91cӄwn'̄?nė8E}%]q)tLsB4d"6s*]NZt 4:-2Ůc:nfeUrw?d zRQemmW>KqSx31&M四\QfOf̼>\>>zr/WdlEH6`"DG b`aH䨙#V)#7^ !BdT#6l.eυck 6ArEжN5BL` !6*ʣ t'Ka7s} 4@ٙf=z`0HyXNkk4U Шh_ݻqQYA ,͝M`AI$4` 3]U̧uUJ B0   C6kPDŠbQ8iS` U/&3; 9iD1ỳ'hX˜TmzP,){?? 7뛻N)gcǼlnJ>PY;;sʲ;GVWy{6]WWWM C3^[[뽽lٳĞ@ޘz ؚ[QĔz0o}wwM%bA"޻w냃lmomnlPf4IH(=oaVM`u2^2GtDN[QNG}VVΦ0[垭655Ė,V9&.]{#^"C$R $\R Rj3EUIvӐNnqj=7tC@h˓㤐0'dz]V ӓn{*{ w {Vx|BuXG W5Ku፰fmx**]or2AxlfMh꺖B_ɋ>_~` *%`,)C4EF"dl6`R#d(թM HHAƯPs$ҢXia@8 RFE[8D "DD1xV  >!caSk9q:8*@MX ;Jc{i\eH4|2Volm?}$DeG`6LNN^njh]/$~Xn2TIUDg_8][[#$E1&㍵bL *p"T̴<Q#Ԉ*lMٌ@X6=3&Dc+)LilQ-sET@DU`$5 g(.ըhhNO_斡*}[ևo;h«?RхHέWDo7MB0NNߟL&|IzT5%b?W֭^̜_|ӓQ[D袩GboOZoUEoNjٳg{{{eY޽stGp~Zuuuh L暷?^[zsj+jg [7rͰʱDH D KC,E TUb!AlE u@AU v"0S2N`r@SW *-YUX /hW +wʺ~ԒDBۍ< .oҮ4Vs۷6ʪvaӇM[OԲW1U%ĵ/CWӫtዜ*"e990D&Ɂb:"Kfdo<"&)&fXL/6.؀|`3\.GbEHj f-*@Q Q@ ӕ)¼mWp,jdB{= Y078ż`Pnooo??>EG;;;{5B׏wt.OPRGOeDjw wwPA!&'W!x g6i '8`D"G鳆ME%󤉢!FBLji;PXK8 pM EԴr H)0 (40?&f^66?zG %Z9BIJ,_8q,vP8G*X,Rl6{{キ:{{0zSto4EK !jSzӍW,kr[Y`3]Nh?v -{9Rg\k)[Q\q[@ͬ#pRUz@0%>KCȀ!z E߮@FdLj/h'= Z MFGE!GՖןU: ?Ҋ]Zej]}}|ϼ*d>v{B2Ї0;!wv=C-̗ƥ!̈Y @ հULQ ;42RYHK\#CT%8^{V2hm,6(BLgoؼJi}oC}rW`gK)xj:28DN"'&OAr:(<{o퐊EzdB̚R(SQj!s}oQDb EU1m&CH*!^DEu]V7LsxrNUU!Ҍ?<gBr~~m1R+~?_ŠJ;p o?%72+g_@$5;u'9ٴWIeY&E$BuS?_{wx26Uπkƞ3ub8^eg`]X>\tGn3?I5u!u:-iЅ^!Cq=)YQ1Qru_:w˦ 9dRPtA뢰,&3AKy -9e#AvTV)KPloow{<ߡSt YZ04/ŗ_* #3 ftE\~w{sk X 4>^oy`);m9$,Eĸ"t]S@Ibʙ[H %0Ed@>1ՈPv;̹`-@9zPt6oĩ-ֻk;NC#//?) Ƅ\}'_ԃM +P`d Z0<3><OQfw+fv{Owt~/u/Bա {HA4vk7\"xDH)Eg15ir)jJ&-B| -}+V5 ! ԗ!^r]F`BU1YC(Q"ˌ"бF7{npBh4O2^<|{5Ȃ4_mx D:ggg}@@U̳OuêkXE7ؽUGG5tl=Kh_6/]ɭ=!%"Mc`"a7pkb:!:lZUĀ΢5c.ccRz ͗_}뿟Nw/KٚJ<Bzt=QZu6ͽ'-E(ȹD$|~~rvHH,'NB8;;k(b1(BQ"4iұxI=">bb* _,f~{4!ҷ,CLMiõǏeX,F_oViWVS[B^`8miQ⋪’˺JYr[u5$9\WzLK@׬m.|ED]F*)( fb7zg1G. $yZW=%̌;f b\Җ3Y=XFƄ@ iI{j 7@N_X9K[k஼M>3ުmX.l33ao;OϞ=܄elp8 EQ4M3NEq>>|e76˷.Xo(XK'emv55&^[ʯK(\+BJvڈU2⺖r͘eDK]+p179GҖk\F^eH`2&afݎH""/\I*ʜy@!<0Lr;-\hOJ&EU~ZRAZroollo$^xs] V rzTbV.j뵌0ܾŏxxx8L9QU XHñUNI؇)eQ>{;wIu]$w&GeS~>te@㙌g+T;ź%Հ`R颞ųyz4)?u;)%EVs#uYG7y!3"/m @EIr0P?r *7rT_(rb"TFR&ص(wVQFXJ:[̫ӓx1"BC~۾~xW`3YHLv'O9@T1Yclnmmmwo;T7s.5IsWec!S”@ @0"e9\n7`}$d ԋʗEpW1iP]i&$ZMf$ 8Fg%5=~7l~JDXx~y~}qy Rggo_΍j pzUaTLH=l$}rM-jP,˼ItD*BbeYz6M]׵s{$˾R "溮˲q&9vIjsaXS"|`mll* X[[V-\ `  KW4to#_]е;|ڟ@+wL^> )7(s L%cT8FKIOlĒdjf93  A `M aRlՊp`6 ]ՏxM|dp;oʰ^'yڂ̶w?r٤v{Op8e:O>u>&t:9;X__ <t6ΈPUEJmK%7އ} ٵәHW"t‹Ϟ=}SUQ)sNN6zw]NEeLM31Bfj24j HHj`HB+1BCf $4AkPAH~TqvǿOdGxFAlwT <&)(:oRP[Nߟ;u* wջ㞽RUUO::uP#uS7ܸI Ĭ~؞>G235QRQ!RQJEC9#d`q8'b4gDSS&JEU94Ʀ!H&bO*kiOzC3rmpy~}v~B>Y|p8_A5m4ؿs:f\0M]Xas6OfNj4Rl}znWUӜ1sNDyA3U3VHPO?9D&X]5yLIe={٫۷|rmm-JidEu||vwv>ɧl'~~e EԒKrh=K uYfo/LؼJmنN/²˙6_:+xQY\)~+[ kf9o@ , e.ln$kG(%萡@q+RFyliOk^   )z06c493jyYI?1/BMP3tESÌR7(Fx-B,H +jNU=nW ?~'LJ'''XT.9U[eRJHԤ+BDЇWwjTV˅bN~1b2 "5jB9t⊠&S5M쉝wJN$ɃFF-u1T!jWhFhOsBav!kĤ=b7ĤZJaM-AP$ fb+1BL 6ַwNΣHl|q1\2x?XW`{&;oYjLNyNY|Ӕ,!9_ͭm@ͼ @ߓɤ>|l#O1hTM\΁#dQER-Bw}pEu=~Br@1@M"vjPM@DeĎլii3;)Z'LeI1&D(i}`0K}]W5[?Io w~pyP݇_vpy'o?_|'w>\Vg㺱P8 r. |>/5M1իWfVU|FtjK3*T"&M">۷4M4=꒤|^׵!^NN{\^^޼yAJ@˲{Nӫ:ppp0vvvLy zK4.cT҇V 1]& ^cne5%үm-seΒVKzH@KkBlS(fUT sA(TRnőʅed[=*&I@F+KXjfb/ Tp˓ç˫{՟o &yM vu@(KKLD0@e"vFeR=g*lĆc2`M D2F*kd'rL>##[A"qP+𠂘ȸw ΃ccX$FBGI)P \@t#{ǃl&բ`@֧6f`_XoX R$_}Y%$@F伆[[SQY̸3@(Wohq"nbI;Џ>,0Oҿ/תMwK%k h5&, ԉCQc$\RC)g-0. U-k|MT3 `Q 5Qw~m cGW7I_<>e8zLif> /5s1LPIEϳf;3!o9yYYo_DfLIbLwu]- |>NSJļD|>;?h{wzz*"7npMl`?{dwwm}t~v4F46?]A!Pdڷv\l'k!. ƄL[EL1llPrĪ@f1y4}p .*`eR7=8H8Ei!3QDs,ֹL 4H-eM#̺(*I.42A(:’ЕygQ GCvcoK{kWcRbU-dI>hg{'!fө92PUd2nKH Q87./=z/ONN.%-b1߻}87]1|qttAu" u@ (uzUP+3"1(4FT*B;)DVB"dV0+``hLa6I"ʟ $`q= 7i&ɑw!"t 1 $@F )BȾp6`Yg[ka+ :u"@j<r>_n-fb16[{k|SWBĘXyxё@ЬzQsyVM֔}ϫ?,)bBpj.P+w1V̜_bLWUL)߼y92=/l>#f1L33zj>zƗcfFfH4i?4lv||BpUU5666Wʁi (Ж]Bx}+zMur ^KTED_\=qA$_50VQH:M Ԅ;pPHHsn >P{q(쬮lի+T6F?⓯.Ɠ?˃߄z1Y\ĊȹG6j4)DIS_N#sM>zRQQ2#`ϱF@GhҦZ6` GrHN\@E$4>9D^Z(#&5,y:|}o&~Hi(ib'zY)8!GC0p*+eOռ::_"i4߹p{+! ʫWdz9BhĜ1Mi"XP^|].ƅ֒"*j,uk| "8kp}X?3n)\|:cCcPO|$J "-nܸq]si{\׋"Ç&noo?`ˢvkbbPooozE&7./i5]Z4w#۝Ү գbd'-Û:,Tb-j;JS @`EG!h4̨6jgsc ]4j{%Ղ in-=6ADh RuwHID8tWQ1f5VAP@PB`WR6p302nQa2cr&EYtFѵꚽF7$MVBXuGCnV$# VKs\r[RP4D@E@~{wկbbvUTY]aUQM)e򥪦sw}w捲 嗛nY.˃eXOBo?s0$ ,]b,s3kК N4x6IzRQ I N%JK c@M5sr"0Qddhh-P%>.V9eeUl 8_Jd'd1zhhYGeu 񤃲7pa* w7wwO\\'1F`<"teK-$+JKG}n41%hc1z1_Pdsee:c#O?WL%%b,x.1A+d`Yn f L/S"cq,`THePH >.'Ioshz&CkFq\kZܾբij&WiIh 2}(*;| !^lGv֚5±,L5!4#QQ|sYk)HѰˋt<w:~ReӧO={VU3ukR97ΊLDcSUfMlХeY?yR>,b1u:eJ骃(")|>LbEvMbJ)eon-{+sUR],A;̙2[Ԕ2!\U>ڵr?J1< G`-3ZJ?~-`r- D'#ݎnxam*D%U4K ,}7ՈE)ơ NJHH4.vYD@3L‰3 84۬2qIQnk>hY$Q$ǐԖ:^B!T4kjzp{Eh匿oKEn,;jW֢: W[%oJ,(۹}GO&!u:(C(LP Fu9gλpt||ttt4'ݼu_jAoxQQ\h_+E_jlJ NI:]pʢvfp> p1%+.)&$ J`# TDjGO I P㵊}^Bb Y;`"8 h;a&i]w8pqltc;\a|q[ᨛLC5۷_D64{+XD'GGϟ=E;\LHIU?ns(j_<Ɨߑ(9nϩ?rfȌީASDt#08Sm]_OOSL7M5NzG}|߿w~~W9ŋd6Y13{zJ`?ZJqq0&d˸\tˋo^x,U]eyhDnw}}P~F~) K\8_+)ju5bVI|:F$&peLJZG4㯿"W%nQ'U Ll,(7UְZcXw0 qj&f*́Mx؃`)ClyB}! iE"}S@Z8*۽7G^mng=;KlaҏnS/zO3agcg䰪@+^?nmm7=X$ExZGv|GB$*s~s}ckk/fLّ6{Xw[ZoC4U(ZSLb@ ap5?R\4 : ?bU}НOb%q=CoDXׂFjf f꽉RE%IG-aNo;g'x2_8(X+:*xrw }Q>/ۛ!tc˵ڧ|ss`{=!jAzgzn>lKe5y4*L1^RQW lF<3LE?j@D -o) *-- 6#EqpgٜW ƛC juI0sQzS3*( 萕$ Cc0h%&+ Ș2+l7Zvج#R gYokq-WrYyav=RUGJzlK!ok KTusg/WP:㳳hm08Mi4ai1`mmGG|ѫ'ϟ GW铭ݓ4>9p|/ .RT'B Y!AG $U3fܧNM4ev,`j:~ L,d?!AR!<{5@!7Bi^SIJ4Ԉrt_MZMrz8ܫO_7zZYn[p؀zfxه~r% ̔C4 zռs6>{d9Zv˲  b ܓ_N_ؓoYT"DrTE\ѱ|aT-^=袗m\(VGZwь羿%76kp &?xUc M S&ЬR9fv˗ Yfӥ߈2 2-4!XQ{0W:O2##i( TX\dBRHǴdY" 3̑e *gT 1[4(&O FPĐZJHh毴0eԜ?V[pڊF?a{릮H&$,Y^r0&+8{7[Hdhö|^54woo>?>0QC|+9O!x"hmDQ%`(Ϟ_^NZNϞ6r‡7xbSX׽KqkITjЧQ#1 7jرS/LN .UٰTkEdNT%#0f˅*"IDQ* h$f(F%XAÎ\T"Z.y.D~ z0# N*!l|:ދ%ZoF@AQ!,6\/.ƛHVygk`wwﱡN~˯m՞5'>-̚&I *Y|k움T9I˞A/–c)͢fUj&##@6WÞs3~c;˪_(RJ 7"𪁦)X,$7M3l$芢\Tٳ^H1|=~1\E㮥Y薟G'/<0S ׊NL]!y-"EE 8 U˵)$qT?<948^{>q9WUvj1Zэ!=V7u{o}{2=.4V|7n/1/^noo2h%"|W`S\(ć`"޻fQ߻ē,h`EQol{3 ao3=|Ly(VU[0[F*z*FT$1TZU:UJhiu:ǃXHL3e (d*C d~yt6սG}N5 xNO/}rz!3wY"93)ɐ*yRlY.Č,n!"i+p(|jt*Lm!U+3se{O<ܬFâ,RL1tJD^oF,*bgg, ֓]|"{WZr}db¶˦F15ECkQ`*#B(3VQɈpÈ\'@HUDU@&^Y,GY sԐL2:ԭ63::&D&EfjO.dvm*犯N!Jrv&X4)d\#gaK_o'[wV8^[V؊)&o!:e/ytQ)#$by;`8fp8X~#p8~|>Y|̓RU""\8o??o vvѱozk_L'NˆPr -PH΄G0 R+# Ϭ>6ܶ59G<6F@!$JJbL8 H;@du$#CPKy$"!1)7&.L'% hcUm졻R6HTu7'04,>(JΝ;NUJ{{~ +!e^҃$B 2*0%g7vnMZ !J(G0R;c{1e.TuoV<3$\# xF d iB吀 A($Oaԃ8+-Eo<4 R0Gmnʠ%*VpWpUcj(hբ;,uyLǢ%k,˃+!]u۵  uο+<^w}-MtmC1:qO2ѮCr!@>b+v{>8@4Ud?CI5o ..ǯ^d<}BNomAQTvz]և`{ŋouǃ/\0W)CEl߀T[숇D`PHp1Le5Iv]_)v8%D"cA|joLh pEf`ʙBo*~ DB 9}TA4pA#RiYJ@l8kdFp[;A豳ù<>j F}Y?|vym=U~`+jTL 0j../&mf*/2/qkkksk5V5Eh/YE#b;&ҪhQ@A͉4z~h``tD5 XuL e{T0! x>/Bٯbjpyyqxt(5QBΗ4GsF1Z2(qTK=UzTK.yAD",f{Bnwkkkoo?>TU%"D2"Z,鴉s^4jbqwo` cdNp3b1 lfO>c-4 ce@;g?+ څ+]K(=٢\Kȹ#lE&_[q#r ķer$RlC-9[C @ 9BP L $ )%lTx`H$qȖS Wdj `d"ls;x7?VfС(2 k띀Z<+H;i2lƫc6=?]\t]rlmmo.7ww?ރ]/֎-f?ˁ43p8 >%O}(AM!e+ʒGj"yOPM )1 Ȉ1 ;#@-30€+!@~5R{Scq< ?$F,aid(3%5ASNF]LK:. (t;٘̎](;EQE(BN766nݺuX,DATꦩprrryqtQS;;c4p^9}nwowoUWn?XJ+GWKQ2(/kM"5vv0" 2lv5o-;m׹5g3,ϼ"jFCsU@(!@a@IE С T1x!@H̀d3X0O7UD"3A`95X'٩fhbf& jVBAgq,AP1YMWuXZ٢ֲ3^<_ht uDWVgU$s^(?xO?!`~}}l:i>[ t:96s^:=x["\߻ښO:<>*xR>M>}}Y X!0zhD9,ȹ}er:?8kW/?> 4fBIZ) 82rλ>TCu$e᪹Dh j*fJ& B'/PIn0 D!\egck3|[Ȝڢeql(|݁ ?YYEgᛳǝNQ@,woln $eǻMe>/_of91nS. y{\ʹB2(d@f-OTPsWO DL1TI=Z8133s޹Lg ~98("EYEYv:}etu(CUϥX+Tis!29?9V3T=5ox|zvFD^/ˋ 3a拋 p8v;j2wǿO.ג*[5^о~X; [36>@K#B}\_99[2--޹6,XQTaњ4T<@ A1vKҀʔkt@Ȑ`G-&{ bDD64@F[S"BǸd9"X}":7nE_3>p#NJ5^> H#:rOq槟~P|>z>3ǔ&qqXDuey;N_}şe\FNbO‚z_]NLko.й"!9U EՅ s3^Jhf13:bt/6tԧHͣ魉Qz&ExO>hmmHx5W e5y1;>>~IeY"̜słwwwF#"2Ct2ӓ'SN \KjBY FD3FF2!WH,ƈ#@mIQN=Hjj`l RҩԵ6 ßHwp:F_vsuHM*BE|ˤPn/\5,9w9@rnY1sLHyx\ `KiRy}wuEB(gE]7=Ĩ{髯*pppd f&"99:Mӻwv{eS7e$D2HjTlٕ:l-vJ{<*~Kv+zN+%Df*"ikFiԣ-?~FKY%TD d"BU| jm}H]mc'iQ[$9KE#1M'aL j@Ը6TZr={pTzS\ {vIX♉j#x_JDK~3 $1 rF̬,JKB*-$E #Hs-@ Zo 5kiٰ1KBf&ۉHUV)BD2FVs9ǻ뇯 R ! >qQҶs?{h6˓?7k·k]g/7F;xRcq܆+^5M#u @%1{Ԏt҅y`jXKP^,,D" :):?مR6,6 }9P35%b%<?>> ӛQj^ppPA5GFi1^0(Xc`ԆW!-I _#*ifSIfX̢1KQDl$Ƙ3!8.+2* 0S Ue"Gfjf*"C. KNB$e&Ѭ?{ }skGv֪'n lnF| G{ihפh9w,{Ye^oF$\6Ͱ]A^&U&#ў Ҫb@313$3%Y, r% YT&1M57",J7 '|<\NgDlk&55EX,RJYRզiAWF?;womgwGke:yh<\p>n,6Cp󦮫wH`ˆ8b.V@ɝ3>gzC۾a7a;;J*533$ 9Sk,}"iVV 9e6j4,/:KPۿV_6Ɉ4!kO΁ٲ`OUX,"fb!4GGGlwwo6??语&q{{;PuX(r8޽{w05uĐwQ:?,mu* hhK2O_Edr%5)Ȍ?ҶDjDF@xy;jM$h m "$PuV6B"r $ qISF9It)$ȓC 0ke!7d#@"#4ը6iTĿBY'p;ux`W0;%DAnJfYWD0C$#Y =Ԧj(eNz <e5"gReC;kVvm5m~خjz3[X~ʲDn7N2x){et6L1geQ;'F|77߭IL?ݿwxj.С/H N_-3\hH&X(CE$0k}$CSAzvGk񸐗fKHƑ$ 2(TꎇP֦k*"Ie\ T ,)8)mޭ_~n˔#S`>*YT `/.X#K?џnM./Ϸ6;2.wﻳ]^вB>? "5a777FP>4DXpj+@Ij?ke5A0Ֆ>] = ]pL\ :P:(^NXKt=:oK6BJ9u5)Jb1aJIs6 5BT bİAKJ hY2gl2_ͫ/;9 Z"1Il@-`f)ImRNhf6$,'Y^x9M^.Gb_|"|2{ynQK\4C몷ͮͬ,Mo +Y_KP`^q9G>E\48WHHh 'ŠI)U-Ɖ n٣5I $1gQ%k<KlY,@Z&)·_EEqs+O:.}ҡlie{-q>")#%D0}*ZD|ށ/?ӷJWS{so=%JO>1;f@4=;Lu-u]ԔeY%doeLq8>zx?уo𨉉bnˉ󙺊Gn7ҿB_MicY r #{"츂Щ<0{Wxm)jϹ6 @-*JXzE Mb)J R{T@є[PIU !div;{}Π&fQCM:FBH bTާwnƳ8v u?'kf&h`UUuݥ7|ׅ6+^$ϒInxFmT"/4mPU9۽wY t:TPz療1 G_@`eOVvC9t>x(]dg Ђđe$|^U:.* р=x`g  Ādd 2j'{ZxC-4Ly%']jN!y+Z&!%
9|wȂv`}:{uR b:tyx#vTêK 7XsΊyun*l F4*t&Lw1q1xboX`/'Z` >)N.n|'5Ns5@T$1$M,41$h!bܡ{{WvkMtaS'2ц045ZVb[[k}{85Џw"FJϞlmy}DW BwweyfhL"/_̧aDi&fJ9}!EC?(oާ#o u޹JA\ɕ1!{&c}yOl`Ӥjjl shI6'娟eY>a1J@lL@ȅDhl@AILp UF,"e? PͶ >d6!!sNXJ~{9s XUQ I5@,{Xk# @D$IZ̛:r6cn<x??b0kmU)h˽~OHtcs>^;/vdG_ۮZg^"̞R$[5)*O\{7\^52`25Ks#er ƜۋNShS &ZSZ.ji1#f\""b& sCVSfZ"zEL2Jf !%13-8oOq!{ׁ5Ơ6% F7`ETRZD@u/KT9CO`3T)1d|+Y؄`tliU&>6jSCZs,ӻQ͖-vãf.?"zoݶ+Қp?xBA4i:Bp!i+(U"4?b<||wpMᯟNy~] ~qR?&}O;6W_Pg9,ejI5ymP#I1UpRՉ҅rXϛf /9ٴ;։CgnE)63.9@iz6X5R]ė/we5DzX,a]@@4O_OgJIFB85Mw>_:tj1$lrO[A% 菎ONN}L%;5wֻ" 9f/{~zvTM-%թ#>[EUM DS_O-y'GnoݪB (li326:0Cc|ja=M4I @U[Z(6y}q_>* HbOXpfT((8_S%%ja%"DA2'aQBD ymM81"[]+]e#*$E(2 RߡPjPQh\-rQ ֪yT0|;'u76Z1xRIR4QUH0-"6E%nιh2ͻō<kq]\\kvwwAb|qdY޶-zW_wvpZB!/ Wz,XjJP^` "gmM*JP>j _Z!)(9ߊ@Ac!#J,MYdAF "x崋rGT *TUe%&A¦nVP!jbm,:8j(~\/@LDXc I[6o[=سd!L(Am4"7RQ@3KM%a0jMpTJԑ@&, S ~buy2\ e?ieulS r鷅]W-U _Ip>xO>~qs6ȳdsc#1IbQu׾1&N{ Ώ,~I'fϟnFǩ6lL_^ݣqhl;bZ93()e؈hQyYk]\S\  f"L i|dsk 3'OEm(.[t k nl2aWq ǖ$}L @e"t2BVzZlM1gg/Ɩ9ҩ 'gFhH y $&w7_:9Ȟ[lY;t_qYGO@ jU+)*A1@e/(`Hr֘"D`5負jRJ&7UD!Fw>jq>4UBe>&uaj'^RT4 B$5iB P <,S-]QaTi{`).p2yw&|rZ~^U@ќ>Îf!tچ6Uӫfq+ݽw2:c!dH?߾_d2IГ@yshذ AcN@7~Ë$_OO^T>:{weQ|u ΀a}pǓu#*ֲׯ>8P@dP)@ ?'dVQ F'LPYHc"[XN ]ZZT+x#R'U҆%x !εHlP$!ᢙ~ThW6:i]K]ꜸqM\Ayhi\hAW'Ռ% #vP4q$I!`^5)29PIaEgAƵkvVF4HYeU+s<ϏvvǣyekkfjT$dfUe~㢝s`wo'<>Ug3+ٿas:\V8}& "H*e]})k H"? IUĩxB&k ^!!&.S aю n$FHEB$>x=G]anll!*D%U1a1_IqI,t33/E(#@JLu*uǙL Q UU3enRUM 0{AB d 4!8 ׻@Ze5 \j!Z%4e+ud̛?K}5{W^9/*bὋcp8HDDB$RQ 1t:X|ε4M?ڰZѣ髋;;;׳QoGӝ=V쏪og0$clȐ!&b/%Ө?s}I7S>>afmxvRth?[CJ8㠣ڝ|5Cqst^Gy· !%'YBo =O54|x/N/>r}]6 Tj,j @ U'◑ ^ىrlAW٬|V@-,O珞>~MLTĮ¯ _NK"ӧ6iԸD&v:ݽ,MbW Q@ I <3!H6\^ D9!epeCP%[2صv4 -)[S!02 e6 JCƣ<;N37jUI]@Y5H NISwx8::fCasqQvYu;=d2 !}f>1Ǐ[7??x|:Q Y΍۽&^{<ݹ_l7nkoxkgɒ4a!(X6u- Pz 6* uU>}}5Ԏ->}X?=ƍ;ٌ\am]!ǟ?4$>~dgMJ`a6j|?aZ}p=21ޖ/ΩGOMgqn?6HD^UUPY Jn{^=[7Hw+'NX *ylvRPTRU믯 _AҠ ?z9u6M;;[[[HKtY@DDP#9#)} QŠ*"@eSYĆFTlH[E^Q%8ϺNM@@I*͒ /7b[T)fbs$H8AE%a$%bL\T 9 ;/*lsN24'*{Aq$b`#R'J&RKT4DTeX*!r(懇u kGGMS߽w/n/xe ιnݾph˷r ԫb[Iʘp7HVUj"xuI B-fAc |-qd*A"1Tfv 'ƘXiJ (Ki|0b.7A|^Xkb}APx\"@Q ^"7RS@UTѸn44?XL_|3j b0Za#SGH^lPI!)b㡍؍%cy{on֕1(^+2D  V>D jbeJ(zͷy?+2aؚsITDyWe4yzQ4zŢ6}-P `7ϟ񳳏+p|ͻU|sw+njGö-fۡ,IΖDI $AR8ьI=o޴o |8$H^ٵI1p;Q_ӣtnwAq<{? >,h{8Hx}xvhgsBøprx~1H`>8w?q[DHD٠ XTEBe݇O~`1I!/ϟ_ܾզ"UuyxtKN.']`}'nlk >3"8WznNZE x '7ZԋBOɢ*DDdY2DUc*0,ct/M N^`+?_, 9B(AnlaO=㓎ib3z "Ajm[;D D|2*"3kLbRAhLH"Sb!@IہK6N& K.h2@*yb2]^bQukD`&8=;f~vnnnްA$IBO&8+y{jprsXaYbD `Tdh-X?Kp@T6~^7EؤUF2_b%-R#C4 G bccAk'BB@Vjhbs@ 1'ԳA7*,M! rgH05"!UI4x@e |u9#3%;Tkd}M_BX^s[W mn^L7N7I84Mn+CbfEQ2OE /_899w:~ڬe{bTyOw|%C^>7n{>ֳu;6cBqIeIZ{49wg)@x>.Fu^Bt蘉,sٓxFoT8myM^܇C߿Gw~kM ;ufk>x?{|#iڪ{r9}݇~1~5}{ڻu߻J U!%BA 6w{o}E]Ns&)v/ XBxͬߏ0ȯK ܉:ӳ(@ RB^__HZ6CԻPյhp`T@Ř)H%ݦ0bYf:2MPbK3ƴX%l^%AƦK-b{_|_۳MNk`mn]`DP 832{ 3(x ^&3rA"AƠ$\# Lv}}|m%VU=_,\x4 ^<DN~xxX,ڠ6U%"$7nĢ*Mׯw:" a^51!r,T_v%a%jw,]F(-ZZ_/2T/B 4_Բ_!%t lТjLl YPZmsUTҺO0Q/ReiEL, }4G?1f*,KӴjj|!egI *zހɑLf'y(k bn4`A=#ZE!(H$ڨFTCH(XB&Be5ͨX((K/=WbWbf9ale~~1RpXeAIqߦTq&I.77^zߚV8v/Ms4ūd1I.?yOzw߷4}rcu~uNfY1SbNBT%,z- hօR1ⅷ:ͭӳ,E$f`2caQNtDt}mlN>y[ K'8[z^A )6"7ԗ0YUSMgy%֦Y&"E|EIBMϩofC5UUC% |x$ì98Ƶ{j3w13af}|}6|uz۝;Mh7nߢ:ޚ5}s|&iZ?x&C{ro@fޭl\YY!9@0Tыʍݝ|p|zRNɴx{DҺNNnONo)w :_S~:X,zqBD鳧 BacTa}c$!kOL$#b"d"bP b11TѶwH; T 95z| ~54l[UdmjBrÏ|AvuہHYP QpZ@Rf% ^Tk$NW\Wd{RU5M߯}szڴ? ;ϚEQ<{|gg,E$΍7*EZkR{P*ઊB+Qd Y5> Ҏ,'0 OOOCMS'զԺ{{l tGUqa[L I.=)@Cz|nLN'/UucjIN1ѩlT%)[jl谚7q_65$t7v+t}ta"}bZySy+P~[z7>"=agìm.W]6V A@ $BV{w~|z>)sBŶ5ټ;2kN ʨm_w`\NH:̦@Ą=k6vTB @cmUM0`$ch !V fЖS+ej+T\ GZv2Rq#@$";oBkWe!1H(X"d^@MnN$x,j7Ga,v4# 8H ao$V2Lt;`}zv$@.f~'*kkڱiUfإ,:Ýkrt|i3/S& BCNc}Yn (ԨM'3~E)*+"E@E檘ITX*ѕX8~VTDzD Q v RG%KFmLQ-;bJBpŚ9|;NKWq$> & `Vu'O&?qE v[bs搚 ,Y[CYVYIh`CS?1&Dol%bݪv?O?xoُG硦>PO3s伷Օwݞ~޹hn|1Lyv֔K1)2 $2(;k@UώӪϕ0A e| ,}p[Mq^kvUeRani|w>8/z/}vs`7^m~9{vw_jo}k|}ovʬDRb! ީxU HQX[_WϏ\{ Byy<:~[O Wޝ_ƧD,"tEvڬj?^(x77Ũy}Htooo8*}0q9xG{,.:W+[ƍDdƾ_. 5E(y'*) B@ DF I#T L%TtH *&IjM0+Y&`6SW"!N'ͲcVnB" V ǔlų R%g$Qfo`":E$J `؄me`١2y~Pp*6',lŋ`D\[O EZ{_ _g+cP\9gAȲNs3s4̍k[[Q  $IR'q `cwD ދJ@HDh&'gK0Z5Ԡq0/YLFPN̰^N¢&`ME1MMӜO&WT h4$ hdu!jjjMYkT0*_x[zj}}}0(h]ױ_k&P|>#ºsN[X8^+BەR+Ir%S.PNWKS>\N+QʷɲQLZPL   T~T 13ܥ0!iy|񱵶6<@CBT%Z%~yxI-w"Q5}vt_VU^ٰޫ6ٖ.Xi@)fј./͢@pcI, *aMHFiW"$ .2S AxaK<[T`ycVDUEEV4K2\Td{o[otݪk0~m=clI5" AM48?+*3IY[hA`Zi_r^uxso}_Nرh*͕ ]pϷ]ynT YNFH@Fn뛼Cʢy62ה2"$q\|m+$3E4iEuREY粹k ͫ.MTVū$A/w)y7uJxpˇp[I 1@x ґoܼ36P\2拧g@M|P%Ite/ܹz}<|5B$G`lFNРboܸkǣ.$2>I'?<;;{>w*y-kĎd2ud2IZ[ubO"0Z8Dcl "[EvϾ|?@Bʈ>x/÷oeo:]@ |mAI`Xp59OZ>zlDD\D/UR0)]:w2 4E%Dh)c3cIH^Z=nH%I/gb@zQ,z˲<;N/{iReFaÝIX,yFf_mXu2? ˒L #c)ْEcb*/"1ppV7Y81dFĶuо+`i bi9 $xOgKeb >qN`5bc/H4/K`s^VW)(<`v3!U.ݺq4*WbHS]gb0 UfJ3  &9O j!yϿ(˜$˲bQU@nw h!(Pw4}}N㏇;nE~^jǒwr9*z>rZuMp .w; Y|T޳Si9^:uݹ2MTWx2 ͬ_7yqEZJIՐD!t'c fvU$s2V0)~uļ(C!OtwT8s]긎oe>|ۿ勣g4ض(AJP" d\pO#2<4ǧ'omn0rq1>?u, _+ +g\E" $͌I"lT! -cMY,b{qom~*~*t;?/+xwww~wڠVûYk,$Isu]36r/IӴXki׋yyDbZkۑrVتD4%+)U8_x+w "92lp]&'=;}yyri@X+5 4M:N2ӎTsbH ()^7 gYUUNj㛵۷pĈG9OW v%+76Mݒ~q,N[,ٛUDY y.ʦh[Ǿ͒TU !1b+?O^۸I]hUdFYr- LC DxIE;m}`y6LYPBE (E跒 ʘ+%3 l@>Sy gxg{lgƔ~uR^\jDJϥ(ofYR9n|;(xlfqnc-Jtpkn\wi)Xȳ #gKb:2M;4߮ί'\$d¦7J:|L;\K;r^MpLyM= w| KUpSdT0z9_z1T(zTz I%gaԠaW%$뚲gT,qhҝN=)1mx~n\ƴq$9:1 g i.7NCgMa!T~|=`⩬W/_mmmlKCҲULuZ+ro "m݆ƛ˧Ϟz6yOLTEׯ]\FwEs&}dynv(ZLggISV"`֭[ׯ_gi₂Jtss9z1/"r=l]__WX9^鯆PXS a'R.B@"U2b^,):9ٔR_mRڼE$󝝝ڣʼn*6Ukm,L ?R-2= "`H$>8 >OS A Yk]o~s4ߪILEURJH+'пF `D=5'!AHYQ_J k4hqbOBzI҈y&+jZr++ K ,U/q齏ɬ![ov&95Q6P}h2 kKös4|qr6?jEA~{m㾾t2.S |yz}V :vt[bܜTD|/NUr%G:~J[ Tq! )PIS~t&"¼ `Od%!` 6@rO>ɺhZ4adcQ];h:}mĀ&}IOWg|[u)C)Q xtAi{=4PR^<>}o{{y7j̗}]`@|4,aQT\6o޺aνg&)ETUU}xp[eQmmmEݽ~{cc#VN+-kUZL^Uu(],V䜋Q, a H1q>-_ Cb,(AUέm2#P(UW#蓗󿙏!W p{ 㠘L u/^tcP, uEQ*ZCiBh<S <>>6wLCyY9,Ɓq o~Tɘڃ :Q;r֢1(I@ (Q0D)x"4!-`6,g'ER4xɋVll-ZԵsؐ5-x֝$ ͛pTc;HWWa/Ƈywݻ֠;TN@OB,gls?ٗ0B٭[i4ט_= ᕒWT zbe*;/qkcc{kK4l_E`$ĝGB!xrpxLF$Mz;o)@Yl /`؝==vb9XlD]oy"BĆ$Z2x'$`^:{7RpF u⳿8i#Oyb: zB!5>1QMI;i4UIOxmgJgsoh evN@4(y߿ovfЂ(])@֘xӲs~sk+N|BHJl4MپsmԲW/ŕK2)GWܪ到%.d]f뵵^A"U  a ŀB|UxBژ'M(x!dKD{$d%EDT[l u}tt()R,(Z-EЊefb^ yɸ|"iP{UdDZ2ֶ2mb!ĠZ@[:,;0k`|ijة41Q:3O6b%C?R6@.~W]KP@kMPe [|. [.cX](ڎ/z˃ã$$tʲ/&иVŢHXhIfW/_^߾6͟=~􍛿M6 .Iy垣EjJ}Tm=+'ӑ!q&)7N ,izʻ>)#7h2,~2>1-.V3Ҝ$FB̨I+@Ö/ HL^X'>IB2]& *(I$H})S7 &,9荹>Ox9)g5?ڹkCMۯ ,=^3\߾yr(V-f.Ѹnu&I^,wTC7"\.0Oz> Bv;[[^W{!3}PO@&[??;NguΚꃷN3\- jO.8 K$Iz\\\x,SըU+Ҩ !ij m.,_-?j&U#N}?*h|S5nVm_>UWsGN!UO1ݵawޛASMRQߨ3AC DP:X6 Be ޥk^t!k:X,eU-gL`^ͦY֭f4!b?hoo/x2/ֆabܼy*(+Z/]UA+S,`W e52.gCBKs( $B"JDZS/ *su ,Ql/Q*cs" Pb6DK2o/DLx谘_6U21s~EBD0ƣZcb38F*RB)`"$58ɸ|ɛ=bL !$Ǖ6%Jb$D)\BbR' *WlJ`*<;kb [w V\6Zmh淾icع?tx,˲(`` *.rNNz݃Ѵ7tVV~ck<0}bђAN2qLI7 $]^N9iZU!%B `ȜsZ@3WY8o(!C15&-m&ffR0,tFK4C &eÑgqDӉK'"N*UDO(DygR` ^e3$$þ s ·bQV51oD .N$>x t^ IMUIuUӥb^c(xVUbs3%*̀BPxMѨj* Ĵl~CỸDBp܌̮nqx +<\ZBcA+_]68!XL;EU<LDsA%0+ :2=&آ\o T*"W+*H &H AΗĊ׫7in0}M! * ߸qc{Z$L-$Aĺ}.ckfy'H8* k&gp'/Ϗ5d pJ dN!GJx$,vjH.@)%"1C).(ԅwI G@ ",̽,kND'!G#'z6tSsN=cõi6kD$E2 [o?+s>?ޭ,ulrL\7aPKO ԝgM(Md s$Ϧu]v:UkͿ z4Xq`X@%nnk9xwtx1;@DmվP"ӐR&I-weYΘLJ99{k1b2O[LU.+?Ijr҅/yP jYDd\qVU~^ * ;=s~~qӼ$MzE[px-Yut>->B>UNP@ rQ]4]cdM( .Ib&`2 90FD5@M'w|:r`iX=K4F?'pmc'N ͓C6@DNGNeUUۻ709H6ٶ"Ġ*|;o6qR=VFWf % miB4lF 5dF.jP '('JN( Ɖv,#yH&^R]]$Х EVf#\TAG6j", &VрAB5_k ?ƟdD 1X{ "D|@RMe"E\7Mڔ;Cs#æ .82B_,㳖g%*`E*f0'@3KiuK$р J1lPTc* 3:8\y7/zXqh}'eEF@lͽwox7s0*h6,$6~&d'ǿFo+W?z|>!ьHNS^^1@(dD,9(zѦ k\@(CwLy?8iL]&))9 j,1P_n b\40M\$U?5g2U0limf90l 0`'^!W; U7Bo>OG/|wCO{'ٴRJv۽a](뤺d٩۽) %M&^ݹw " whbP"ݘl>gu4u`<.A|Y}h[c YJgC|wKMCޘŅ1k㆛N\ٙW{dDTD LjNh;5ܗvVtꙑDBEٗVI {J^! gcDIIE@U)IPR1oTEx|P t-H\NJ V2@(  &X7`{U 0 QEEĢavɴi[ Gh2>8<ί}=|0Wmm=˲tҔfכgy~歛PEL,~#+&eo0%@pUQMg("*IU/RQBP B!8F#0@dB|l BuqES S0\vMU/F7l},#, @ZZ/^-ڛtEiA+٦/"N͢RŇ@@47>|<\ӠeR&4IYRtIa2c]P$kb4g= aQAT&!Rj-(/5XK)D􂂮1rm@NE/{ ,4X$ص-F\="1}_~1A"k< PvskMәN49 DkGwr1κ滷'fUjnf28Ŭ"5?vo FA86Ux!4 ߤUfB6yєkٴ) j hVaBQ9 Ơd|9$Fă":1ԢU366V_1pbOlec%$4o 4]aߙMa|89xk{~=?=x{o{,_˶S หCY3MRw8={#7֔e嫽7~"@H*+uƎբ/>m\uU:3{*d)yKfI:aQ]<9/mY7`^*sXDFoB=@Xy@$I,r<+Ы몓|}f&5!ak-_ЫDB kBdx^}@Ɵ_:iJMy1 -a#z2;߾ -*$) 8HD<R&HҨЏ)yHDbܢ,zwn.& Z!TMU'%HU5IŢ8:<"ĝlvttt֭^r F,>Glq-"*rm};e6~'e5q{ /$In]a[J^MnD֪T4-H@@ĤZ/ĨASXTwĤ"NSF!)v$n7gYD?kJD+yU-͚x%+åri r2˨%@˘TB_Hak w;o9Os'$50Arh\8Yu[lHZJ7 3!*s "JѳK2Zm˛PX@?_KSzJw4MmR)"Rו_61H\mL^ʍT_ET1ئ*Gb @$奀&ęM  R8F nQggg\/lⲦ*.rNeL=$dOO>i#9IRUmBzA#R$4miiC( K-wIˣ_|ԙ&iPfU)zTv(,b!ײr2I{6 ӗ뽵pYV+QteB9 Ȉd]"7Hc77om *nE'&!0S999o~?Ͳ6M,`0mbwAk_jX{nF]:eRN;ECW*-6{OWh#ë.Q,AD I%MP95;_@&F( x/i  /u+2_O胮tpS3p5x?_&BkEP'ZR,6E1 8IyUo}D@9m߉6VAAimEUĮܓU2>X<X ǯJ /ajW7{?OETUd>-@:ݜsEQ$Iy^nonܾ{oܹo~r>7\nF^G Vٳ|klDb uUki}_dZ 9n8/~n$RomYu:*_y{xj/5Ҭ M7ӺyAuQ5 *As$JAb0#2 AØdʼtx6U۷z\L0 $bcG?=O߽L&o~g޼=yꋰ I3\LBi(Jb ÍMцQIR-<}'Y޵zo4v``A *1*Wi{ϞL~[Gk[4k[n~tϋ^e%T4$ӢPmYGD14MS5"fYgZmi>MӘ)'F7t\&n|]fN9:?}jKb=U-D n.UlnA mW\!f*~ރ{ɹVR&%R檘EKaMk===;;;{ӏ^x1M1^$Iat1Ra@DM>x&9{a)UUoW JcTDe HpJJ> fBĆ /UPpycimm!xD$f@ B: /_U7WX mԴ(i*J/GU1FDBE1Eͭ»ڄ4Ԁ!$֖(ҍGEe {B^_Z 2M«u d+B o}듟~,GĦELđ[,1>II ֎NN..{|rxpgWއo}yY+5L+L]r]|X@Ħ* ( |j947bi5.1Hʭ:4ITof[0MCuoVn ^ӋzO%a(=UdP!FB@F?D6 (I2 pբO-0%ڃ+L=Nm{;F~go^nztFqtZ%!ν0$֪O>߽AHk֯؈ I5qŨ,` |pQTnc}moo # @YŗlzV??:2Gܷ7q}Ç>Nv]x`%!E'BlOgŴ0t2cEB&QhLJK3jt*I2;=u΁{^e4ugTYQ4 sPլF =AC.$:љuJʀ+yYq֥*iXϟLFp>#"M"^U~k 't:U %& ommݽ{j喏eՄ?'#* 1eF4L(JDFElؤb8a&ت2 G{'ǟA6C@F^V%*RMwDP1dQC $ /k΂9sa_z]9TAF*^$aFzzg Ζ JB/%ڼqɩı IbD4&I:.}YyKŋݼ7?zv|^}6 &dGF??޿pmKiBUTu"%΄tg;+iCKDIHPQa@A a$gYiӎ-YфovmQOgZy^Ok͛;;j3օ4ńt(u|ޜzIB"ggg{;p6T!#RU->,1U TC`QADF!OdW vĽ2u%B jz{gwl]]~ьfGas8#{Z ti:3{P=.CK~ܟ䵼'Y0booC>ol@r}x'G㯽jcq$9|DAvwGjYRhOrRz8fJXCjNDӃb$(z\aU}$ yT׀>ׄDO H&DG =! B-S5%C&КUcGѤQo"F5eĄH l:~o~!H"8`g\D9"QR~8ͮ_vpppvv6 A]BUUF<ϝseUfY{^n"9mp+0)&q).[ͨCZqV뉽J<"gk@'!!2!'*#2!HFF`6 .ɠ*!!fOi5j@D-EdWqӵb#Rʠ2ًT)PU+ba8=xIZQAd2yoGf )=YUȯΎ~?x~ "^͇l qgk0x'8g?<:;ȅLɈX bre9ġű/^޺~5ń;*|tl+8#D*OO0c8G-ųGx=]oolrŤكhEkAH&j޹HsĊ񓃃!V&Wr?o?{n|;ѹ_3-ܯ|F!Vᣍ ^~tp{{?N9:Boy `Nb" !g$~=\Nbki%s6/xP/|U@Uu لE/pp쥛a:jaM-CF\\m2DPTC&{wώNf3>x@{׈k ;ev(m\{,٤xNk7x3pbzׇ;ס?5fv~쨈ԉr/W-uQT&H1iG*'̼CًCh5D3ʔs&!>מEퟗOLn>ۏgE6h~oͯK9"J0I/zB,Ss|){ IRȢƪzEIw>dG(!&Hőf59P5  h(2\W61dIEqA4A&f:ˤJ1Fu]"ܾ߭!!!t: RԈf:NzѣiQţwΥs.İ(˺Dy뭷^}.XU/I|ҊB[6 vw,`h-8-cu @Hd1H@h<-!lPxΐfswr6Z& Ġ1 0΍~< | q>{7{nMȴ\|`P%\W!*iQ ^~x%ZP{CϴT! =Xgb^-:ۏ<~W%2Va `zY3hPPՐB J&J#.p{-i9e41\YMϏŬbH&9 Pj  ! !2Q$@-Sֺk@P) #Qcrslj F)4h3rlU\ SP</l̜T(u}~V'р`^Bx衈8O&әE70SJ m\ ^kv^y6MӶ"F?GXsem uB*"9C+9Gȡ"" wb`.=UMcF)!e!;1E$tYF 4OÊ".Cp".l/ .8Sh .`bqUe"S>{w:GuDl  < Tg;=h IUC ρI U4vd A WJe"=# s Ymb"TAo_wtrbnJ-"o^V!j Bb&O^yN?ӟ޾qcwgt묳Tozjgҵy~x|p%V՜2؀Ki1|v^{YZxy9YSr&;#h<\6rt''8GhB=99JEP}!%c@hPU!BͤGᅚi]$[w;߸ާnZ srOXROPߋTFlPq^Z&٢0rxW(\eZxmj|_7eZը4oG|jPPȽl1i#ij}C(3GxbHj}w_k4Aoooߙ.҉xJZXհwv<,N~ s.ۉHD"\J ~4rs%f5 KF\]hh f@`@%J/ 52]QiSQ/b ˲~ݛon˙IHغDdXu#"Vu~8 Obq"5ftFpvvffkkyU5ʹ \L]坱=56臰n&]Od x|gF9= 1Д G E4(*Ng1!#jU#m|`[+NEj.6k[紿<m 5Fm0BW/Z6E<7ÃTB4!j|X65 "94bd#f5%ai@S]GMt .IXIH댄\FLWV{)u2ћ_7j H90<~:==[o)ʰ .<:_Q3@Ĕ?yxxp ),`x8VTQd un^PIjDaɇ}pYѴ EC"L5oP ٥A+2N/"A2͍bYwOߏ'yZX. 06 BU@M FѐH*F"njDFzDFHHLdbfr ܣ^l<{Cܝ;~'I؇Mg"kUISG}]x<{E0D+, 9== ! CSݺskmmC!JmK&PRь[khφB,+ꢔ:d8ŏ AIPS)#( F6!@51z Ik ,5W^ 4XV0Bx`bR/]-uXbҕ5Elj s9] kR kJc2*!Glf@kEP@gY C@V I1[:$V$/ڱAӠ79!&ReuW_BIip՗sEUBw]rEbhz4YEb&1UVlLIiH5d^['V3 E#pİlbA** Gn*Ÿ z ;7kɧ` 9`f=re9zLsPTC<:=f̓c"DDI2MJĆ&kX2<3tU#v-l`ND(zEyttι|OݎD"y~rro}kskѹU\. voR^y (V G1nb ZjQRJ(*jJU jc5Zo2_K^meӥD QBUgyՍј( )@Lqn!֨hZBל)Wg 6}@5fY_@^5[N[D+Z8Łloz^2M&~0NW="u "뇇Ǜۯ'~r͗^}}91Lf3@k$ h1#_?y?:2`9xt:-G:'B+I&ТiM*UtLZYɜ/f59yēLTӘ A4@A@X{:JY y>޷{OoQ dDYfi HB'b`fjAhv_ʑt(vIi\UgϞitI k e3S@$xXJ{# h`Q @`ʑsTАE!rgnɸ~ifZM@ד}1p*=MՇLw6}pJ !,C!CJJrDDFfVS*"h+Ik`N0@WXfDhaRS@CS(Q,Dt%CT-A% 1.*"ED?|`mm}:>{G5fQq:N~QUqe{{לwu]W^}yQ<؋~GNS&cj4\r[tFٰ?Ug٬k!4Knh463UayQOG{Ipu hA0'6c@Ft kv_ٓԟW0sƝy]((cY(θX_wӳ ^[Q1ܫ"92.p9D@DecsڵkB ̙!مD@)'\qf'r DiZ:*:u韫+(BBu&u0CPjXnSP<$EfX :%{ Ns@ JMMֲI }m JE -refňS&QD1"Db>EıOfL(Ǐ?{ƍGGO< yHd21b-HT^;LL4\jVR-A2]q&-WڐRl_y0 xB$D#2C5 *!3 hE8|:[,+Y[ JWx%",-.d. kjIIDfvvGA5ER@` ) Ugj"Yh= lit55٦f hTz.XD)P7Q]sK2VjjM5dK:P_-Qg`9a# zP 3VK[`^ONNN'gvt|q@A"aꅐxH1nssFQ6, m{Ţ\P8PHYC]4kunbD2ShL( qLUfFt"BM$IPbcRJfSUlyڴ L~Gç͈RqAL5(DR e*Qw`127{*[n?[&q1IHćO=Kjo$ic`FwYfZ5Fu5Ï&gEVQ3u;;fhӒ*~3 LcEUc0GH5 16-L1 6[fsi~tWս㥝y_B0%M ժ(j %tU)`fdf Tj_4!5CF$K7)5fm瀎iB1K x 7:R`zQsl>"=~k_ڏ~k̤bFdEQDюyX}_w̍.SiFրe vt Պ]mVjLmwj@T lEQR*"5fyonmnoLΟ?B fy 5YREiFeS06lk5L4to *)ihs :.ݭ|r+H j$ZNӳY|!hWPfgp<b4qv|vvz^eWՀ;jWl{yb"pwwg4> !f# 33s9eVDf2 "P )e%c]V^0T׮si"k UP 9ςҠMAP$B@dvV*Ѯ (Jh4ئ5cF5IBQ%6-90"a:ёy~||R՝5q(^D`+zQb]/wdKKrY"0.K?NLf>VڙQP FME$sx%:u]յVvxY|gH3Y zzvV/eY =hm4$Jd4T#DE3UPGXFhfp%CvyJpFU'-s\MA9{O>}gf.'CUgYXEQbP1(ݻoeq5h`fD3f<F1M8R3e#p]$d%£bA}̕w'/:ߝ; 3&SU3{( !)&d9.Fب uՉDqvlm5Ǐ 'ʪX,e֖H *FXz>7_]Cpiy~tr|1+4?;?}kV>ҥq? HlrPh3Ġf޹Ԯ]%ºb̜6"F),""c3Ȁbb"594&Ns-MQ+\fta'K#B2(6$1`5l \b ,ݔ-ShjJ Mvդh`dH+S\!!): 3#m2|9x>cD(XUh4L&GGGp8iagg^#D5Cj7eՒ9Xfm0 biq32\)y喗g'pKͰ)i4 F&̼Fdj0H$Cb٬jqS"SӨࠡBc +8BBh¸r+$\*[uj Ar^`+'Z^" PN JZ7h2-A WUO<~ ɠ =3d=oVA~ ̄)42+m6YNY-c DSDE--HvkCzm]EғXqҊKo`תͪfD[o^~yw5gUUQ2s]דɤ+^s'^o@t|@C>}:99wv3RP b*X ́(b Z` Y "*z55#pu_2%M#V 'GD!Z{65\F(<~T3HB*;͡*P=võ{ }{4D$U&O?9U30:B"dXMy7?z@.4zgbZbZ Ǐ7z&YqZ + oA4g%Y(Z>~B1Q0RU$0ƍu]*!#ASȀ("5(b@DQY JJ!Xlj+-yniuɅ!B'Q?` F,S 9CHb%N!X!0k& ay-8GQfߓWvw]3#,D|{{_@7oKH4\/J'\[i jmƾ«Fdf`حak,ddFjh@bUDerZT$H$fM/ISl$Bn$lA*JD)x,HMF L2Dl<黍ͨQZ04XdESk3CLj DK*b3E#h@ RP@= O a8E jB5p( Ԗd0-@%oBcҝZ D6бhtH`5h_{7;?{Oϲ̭:Tu혉h4^ˊ0Q "1|O}_XO~kܹܱDaLE #0xdE $) t(`Jh 4T$agOOpVUaVʢ*jY"H$X"%@T FNAH834H`Jϱ PIpts8/g+7x^.TUJZNnTeLZ̫r>'{{7mׁDL ͝GGGG׮1c#E2Mn[4JA1XTQU%3^I$"u}#Wʶ*y舣܈fy&j]]`V7s\ASDҷ.tNܺ=sRÊv"S&SQFѠ}ni7[@dE]ihmk feY5"9%BQ=;;?9>q?yR*"t:-x6r29^JD޼u+>/`.d_]U]\ YMoQ|PN5dƔ L,MAqR3~A3@"Ǧ"` DλP˳L,ƆJ1mԻ.fvca#33h;B%d AUϙ&1?cG>KeDGYMm6*X{$3h5PU*hjCP$ⓉG^Q3pbC@ؒm$xQ]y; VcE` _}շz?(c)E D̲dBHq‚hEQGy^G={_ީ:5sNDE ER\3(!@>][vsz::j jZXVUY:謒*J,8eQZhVSCU˼Ҫ2RP b$eB3#!A(!4X4ШEo.6+Ck\J%ЖݵWK;AI(XBL#7QIef fDHb1XS@;A05b$BSKT& h $"#5"cR6!̌D!QE&$&fI)(N"Sՙa P Q $裺%JσQ1.J|G6u9&v'\s _az;9iH 7w_{Us Șh\3vL$PՕC/ՀH1;ݟxE[e 1c ̋`CQBV& `O50E5.lm`'HJ4HPAj2 u-A`lCY\g.j-XRUFQ4!Ug!p ֋ؘ 1#$O"dJ(sIjN;Hbomm3CQo^[H| K(N)VMM3F[kiW6@H,Tt+% 2Ěa6I $&)ZIn9H I/'/ "@6fA 1zph0aWh0#rdQD LEIlCA`&4œHyb{yyS(LmȭVőTM̈Ќ<Ԭ4k@g3LŸ@#8uv p"ZVGbFax_o1p,M<сZ0T/mƺvP1UyvƍݽݪUUA[!012BњO%|~tt|vz6?_o;bUU!P$`I zq3ܝ\X%AD#AK!TVTy  DP46 B0X#J\1j4**R).몌%5aG \my2AY ͤ( !Ǝ$qF^BUI99c.xkZ }y/l4zkQw߿h4N0u n޺uRT_`%{DjZuXbu]Qe`黝">X"eT\v&tZUU*KSHY Zj69鉥< R jL їK\w__}^a-ӳ󺖴%4ßk׮#'|Qs'N.~~~eT-ݽ?&RlGR v.h PdFbaPAI'5 &,`ĈeY6ι(B nPZ$0 "8bb1@cfbDT sF1@b&bFqB ŋ,ONN{^t:-:p8LncA{XU ,8{ye ʿn*Zt:5P.[Y$>zGw&?z^[ONN6dRa4Eo@pz5#vy+E6/ThP2T5n-fV2 B1Aϲlggg}}}2<,Đܲ31U rHwdB jD1r8d$`IO[bS#&CL4P:4̆Eѥpld!ׄ*YLTSex ,ovQ>wOOj̩9 hL@A3 0iuC)~%9w `tD76~?`dPt4A 7,|qϟWO<}z~zYc !ᗓ%@xYqN֡(h#$ +-րFHx[B P2SNl`͘ BeDhrё"!!22zSH\G,,s;kkTU޿`6brjOw*a,&xDsyUU?N'gKSB_wmO5&I*ٷz_ 󼺄]1_I_IЩםsu]=|0N&dӧO߿_UUQi*r0Yq{{{<ʶVUU]چ4y<ر/~gB#ʏ>k^] q:"c 8>zh秧[;;;UIBH#uNY盛?נkؗJW38ߖQbMª"*έRV- "T) 6 U!$7osC45RK_Ŵ4~}('٨7 U]UpPO%Čtj)% )!1*%;!qjQAfL"dDq" 3ꙁQLڮ*uOaAgέMqumh&#tDML LJ%Li\wXBc׬KMVvWk[r.h[ָ1K"JW^}usc.m[T }kN;tцq̲ϧ''g["?7Ὓ.iAp9غw~t4 e#M yT`0o NtkҢ@Swis4n]kTK)yAELL&1 X#9GDj" @40S]U ;;G'GUUY{_a$uڀծ)!k1g?O˲JNp4vh8(0:6pUZ g_ÇLD꺾u떈9<"5)$O;X"!ʘ$C*$jhHDwD!XMDP@2b"Vuk1JUVf1jdȌ!Uݛ9# R 1F}כg{yT4A#s4$+;~Fq ՆJ`NS• =@߬οknUf0 _yO>,KFTp|VUg>[[[[,e+$b7U'?׾zi0ϼz$Ա?"j]WDt6;'sTWW:Z^ .4ݘOtf@U],Njbgg,,n߾~͛~97NޯeyM\UUilM(IAqss3] q\Ol5{ YW'/>p6IwU fFd6(>iYoѓ'Okk^OU%F"Nϙi0qU̼eYY.7v6Sjoʹ2akin/%5Z0Rc)C pPrjCHDhȀ].!R[D5J(H*9H jG;{{O?o0TԱ ҷ&K &tXEtfDyH0wϧ{y-߹7胏{<0;:iWD΅̼:(gbkB  TA Ds؀QI b-\ԪSx\>BJҶyyY縬v^1p1/ 62O~3!{cGhlEyVUt:Qת|_{5${drm0H(~UVe^y)?:PD1L_q)Xk&T@AlBD)=א .DpG"$$ 0)8Bvb45I,Fl~RpV՘accc{{HU;W׆"hA"W֗;dJ[3``X!-AfPk9V)dtKUԽ %up8̲l0TUh{{;E3^҄\RҤ~>I(н[ V ?OOz/-[)UtX,(RF ɓ'"+/ٿϟoo4HDu]1SdVUFכ7o̲­-[;agg׮_[dŇ|*Y-H QX;U"Iiɹ)ID!FU@`M9ݽ *3,͉1CQ}4d쪭 LH*&rBƌR65\ ˦|h7߸}_g\@U$Fp̴,P`eY2aQ<;988;=lӵ5}@"B$)_ܐquX]/-_D! H0CkRRJFԬ;M&N OIpX#K+ω@AH)JfFEcGj UȩZ$ZLqĉ}զxTD9mzQ\4y.1|o^F)t;IC#1wQ`Č>+LR1Gý<ϫJHS/ >6xu4UtVgʸp}}ڏ4B W\LVJsHeN]]i鰊uOxAyUYʳKaN/T}{ҢVUUUcҌA8<:NfE|,xMD"$|QR2Co|{KW`jf*E*GnX˝~AhP hA!/"7ϊ& F` ,#%].Yew?پ{?c?y6<?6EYGk㭝lwwoܺ`_2יִKۢӏދ;w׶˺~c;;ι[{&ztzr|`Ãڝqoxmm4= ݼQ>o4jLԐa{Z"3QR5!zv]TM"8l:A@ 4FpHaQy FF|dkފpEx]#6B,mMlG _:%X[qm}|^O᮪ @y34eH 33KQsEs7>ٹvZj7zh #&n\^ڝ:Ү"[[} ,l^:4i”b DRt ). !*A2)IEt`*3kOk˭PD,͋<xr|˰tY;w`]kJ*OԞf@O<sE)Fr[[[ۛαHer^Ht]]d/Փ,c1Ƥe6NֿnJV ^ȕʰKM^t ^zx}61DODh}}ٳggg^`8"Ƙ~cs}8I%[n ZOaW6 bTn55閠pFԢHMw0բCB2eD  (C e!A}#EЎlچ__{{vr{mkڦ@ 6766c=~ݿhkg7^}߽KG?nݪMg߽nt`^'?|m>=y,n~?,fpmY1*o"evvr7O/rrv噪!"r چjcPL#A1{f0 ϒR.ipWA+H 02-(ˬبB|鱲%Ul𣣃A.C]2#2cf&z0dZ$RMn{dݔ,wĀ` *JJ] Ll+ޟUѥYjt'SIEIjJNMj9*Abtl ќ3s׽@NOO4")WH V: QDD꣣b6bv"1֕ ÝwUxTy%/ɳOl xێ=={Wz٣/ݾҿl 436+3s!s'{{A>);%'t0 3$yN(wRod- ?m{ zK*Za{"'{fQVTJǕV0B@5f&v D?ojSM^Rlb%VY^]KH/ყqNMϧ:;6 fǏ&gG'?óMB~|^{IqүNg6+cO?yxow?xzvLU;9>:=9o?qPaoggiIs,N:9E֔.Hiʬ ehz b4@EDh<% Gukk\Xlȭi4iME54'r?ad>Λo_L+8]CI)ΆN>~_ENN 2>}ٳ8 I9R?3MV2-wi꒙æ'`nZ6ƫZWyzњ ^^+Y-{[j Z9 Zݳb=ýkNO+ѣ׹Pr 4XhJ܃1~ѓ碊dĨι]fWUe1³m"yˆUU'ttgm[K}WUP_HI}I&sKH%sfωw)\غ(Ʉ.>pxxxvvv|_1!EH眨 ,w/X[K9qn捵V2nӚnXIQ A L\#n @ m!] ;u0p8 ʲ<;;#e?"ݗg/3ӧ=G=`6{E~YV~|ώCOEsKDFF3iIX,s/1'ȼug b0#" Ѧl%&|ˤuCiYM&"X=m*IG4ۗ~\[sk{HhT^NVjJ*4EaY^ONΞ<~{mѣz>/u@-lo.uۮsjhIuv"jH))3ԜZ7rV"Ib}{{?͠b8999::NUV6_i~F B}ٰ?6@1Ig{H *"|*I܉~_ f9? 4XW?1dRUu2QRŢ(0 G<2|!fRw>5lo *&ַ}dQvÿAtNzENŎhӫ ;@S Hi$q-TD6ׯ_YV?` zEgѰ8r(ka1ZyA m{. m !&bC.+|Q6·\,k\HLQz\|}0\GR3rEgsvH@U,"Q2C)~o;5::P0pE D!SC0Zyn~n'7zZ7 "+7csc{z 7{fy`~DdXt̀`onǏlml~GB5뺎\)QK|IY; ls]Zï./|.Swtg}tR/տͩҗy*`Ἃ1FyO?'UU(~CG4XJ L&!<{ y(LUTeowBg Bl(ՓrUwY/\BG_|WEv/~YUoNcISEDv<&I9~ED,;>>O_n`|SyCfBb^w{yDB,9 \Jgv m{!4y`&A8C2P&c@S;:h R̒mbQe|sldU`W@.@NT$bU;b59؎, F-ƐȰgc9Z,QԘIRd"n.SZ-&@MGue @3DS0"&Ɲ(B驙,#"@"* HS/r ؆R.>Mh}^im|bS]./Fk:^%QWyϹS/ E}pxp~6!|EEKH1Y1PX͛7@$籥0&M*j]xQ0=[VfD䷘C4dGBJSi ԈNNNfYWd~&A2E#9LaQ;& "ɚG`&L`Q$ӑb 1=O" $G|pe=jmˆUBUw>&%3@B E %]̢ybP U.KSG 0Ő`j|UQ"*u V(G Py#Kݨ R2Mb"p)0Ӯ8-E]3޻W^}e{gTDbdHOhlɒa,˲"/ѣG>>Y=\,z^8q& xrgggιh@tGHS]G?:g_([ߢ_~u.q.vvw=?=9cf~!ÃpZ_Ǽ=h29Ƞ{_U(1*R5nzdrV@@c_BV׵Um%O*W7E/./T_5EF(]ɫGCDyYmZl#,p:Onm̧>Y9^W>,p<f갫_`51m:4F-2%t,k7)hf+ FH͝E!ed耉c QQS*O@H쩪TĔ幩.CE[Lx%ˍ̌q3AP%D`2&rDj&U D(bE9#Ǣ1 A 0ƐRQ{KZF=Z:3DIUT3, (D0c"ӨHu@P.,=|m,f$m x%nqn61SFx[!A.Iz1``0^k|JfPz>Ղ-p)ҹg@0~~+z?Ox㍭AL\YUUY}"JA)2`mml׭(L@WJHչW/-W]WĎ-YMiĨPEU>,nt[l>y" "?ns^y!Z8]th߻<1f 9ݺuskss^p쿴X5| A>^ By?xHӿPl'bXw]@q>Ϧg̲`+B'!pΥ2ya*?bǓ/o|h~;/YJ;UY&EWS TDG]ѪȬa 1άR)+&1F.n4}0ǤTFQe$SSIH,!-k_SJd2AdGF yBuP,:$)U/I`b"[J0If+QMӃNyp(U<it`ĢQ2"/ \lmETUS@=Ï/"`=N&Tkg^Wl6KK `6?~fwݓgCmeYz+,ʲ|p8އ&I.FTEQ$Vg,[?p(WMTW!&.ϲx|zz&"q'ܸqm{g{eeW֯}:1a<==x 9HU̢Ƞݸq?&9d[:_̺+-z*.W%YcfV"~>c?z.\8-uZGLs/1J3lDzёwDC-"y97-ae;o~{IL k,;5M;EX'K6d)| bd UEQ( M(5E@܅ (5%9j!B,y\Z2S|J,ZmXBsl@ DADM3k[6SLyPHЌW4NFBtL;U5yj2Aj)3L!yH#96\1b9S55ϙptDEMsBDUd Z,K)ИI70TB mR$ \%=J6cB;(4%Ȧ*פ0Sk3k_{^?LΙHX,SfbWE]ԼyUU9L/Ûos&fQ$@d̔bp8<<88<< i~|dlXj}}֭[Y%kIw)Kg KҥB~ARZvf0흞jQt:LUbJߊ&RGLg` =zGfE}& ԢQa6v}(jKHĝZ{*VUŪsybJN\ZWYřV/DQx$~&_ ~(̦52w||v'OαJ0"A"ELLdY1&G Vn]zhm 3Ec]ȺXxN ms8H: iDBP "{`5m F=>;n~Oֆ#AP,SnB{ofb>_sYi,3y*L&K0 5V@ #λ ˷KljtggMlkEHYD>?<<(\~spp0_,wuONOc"OѲx"s}?L^o/ظRt,Yˆ.UrULBSB`1tI& ,e0{bH̰y4lK a.4l~‡~?ۺZ^OwF $"SCPxeIBf(M#} ^Gh}>h)+`+3 Ѓ&f4DPGƄ\nٯ}+?{dIv^߰)9FTP"t%7Y_Ⱥd|[Ln]JIq U1!3}_?lw?'"2 (2 ĉ8~ܷZ~O%)mA ox_2tD<9>N "'''ٌK9w;HǏCcgQb:Ңs| .lllyVVV6>+w:DL+l61&TH5Sm𙶘=ŤaS\|-$VĔy?L<ٛNfVf)HWaA3ԣgz")mUn1pXؼrr:lcdyu|YQ]hSU"뀈c`PUy饗^}UHQIu2gp?`-4ƪ~YS Fv"QR{SI%3DkR", 8$L)Q Ԑw:[[['''{{{,ybM+L`]ab%wnOآD7SsVMZ ;03AS^R>b~]DeQj^7׾?H@`$`LhHP0PBdr^MP puFEoPSUKwv̜<ݽM\9__Ճ.הt>!w}d65bd~op"PIT4aq);A<˲LO!5H[PTJ7h,i%$F$*2,kB'|<{|OdHHKߩް<-* Wi6;޾xoP%#]mZY &= {ftklgs]{O\6M:Uy&L1Th5$P[DRVmW3˲l0aIq#ͅh>#jUi;`|,E>ً6ɟh=/5az|LM bǏDT"I$Nc|Q-Ք.ԌV6}n>NeVa>ךfz|f7 ke%Oyl:GHLa^JKͭͤT5POUo8]RUp>.tOF 4Fhu̪CDP$EN 1^ HTǏZ~w;mCH't@6Y::<<9e"j^tDm6@wΥ NeUf7._$5bs~HAZ=!qrEU$e{60M@r=AFhqi4Ɋ4l)g'8Wj .?m (-8r]sp)3j/Q%m|f3JffN5C5ScRjd4|ooԷog߿}6R1N&dmSuƓ mlDL:2<۷+YƓ>T!Vg>α,\r̭ҶҞU!D$]/YzO1<d634^PD}"FbQ+Ǐ6:[ _88v)]1|VTJ3_#,(h>NMt<dSU$,hgg2aYV[y؉D wcMAQt:u;7\:Owܭu6RJ SK5eOSU]cIJH&)K0!b` 1Xє ҐG{i.a·MYC;%5Ϊ@ T[U=Wg&muIB% >A:(M_C,H' #4Dbbr+Ui-#C030&@ "nxk]T_iX^"(jbР>j'T{0XˏM M^z⥋BlZ.@ #]=gt:{D?q0mɊN<%%r%p6=yBD$ c; urpCW[O9Bt^8/~рhulnnyLUDx W_m_K"gUCt`fѓ?% *&W]xBp<EY"2LSxp8|aq6=z(UPqǻzt:M$ h4+++yzA2nփuzjyqw~IYyWg ϸo q)Laq!Yӆ8ie11m3SӠ׈)f&f@Bh|I AfʨL m"ӹDxlY 1j۴3t Si7j ~_QςVhy˞rUF@L7_tR3tlN'#Gi?3s AEkG>\tw;<<̳,KIeUFHDLE=)"z3к>TDձ *>ڜ{TXY>Ŷ_wf^^tYAv.AM)neueee\@V_3XgS]LF{Vxd{1WzϿ`П8ht||j0p^X]]TEeYt677WWWJUYWKWckmsyommmoo'-eùJR36珗e<|~ KϓmL;"&cK{1d̘ 97^xpa-"EUBLҨ|X5%D`q[xr>֖RY,逸P̤hsw3Tx'. ``u HBJh@z mGkρ ̴!k΢STo2'd>j вVțJ24–o<"Лm;=D!"MLdp=IYȕœE``:?.5FΌ(Dq47o`@I\$A+}"T੖J 3k.y1-K a&!ꫯV1zsD`P%32El:}kz΍7zl6XEe)'%$|(sDDڐu ^hTK[֕zGRX:fNݒnm Ywq'jmi_'H[;"Q-/IbMFpS8is0\zeW,b` w\X1ί֧<?uVsgUevKre*ɔ>=%Y{ay BH!(^x^zGt1$cF efUG|YJ9 R;{`g+zǏqx<^]_#"(LHi1J !15Cd3BHX'1lj - XEH4:"?e 6Votl=\۴Y{6De^rG's$r̈㝝o>-f3X %+Iм)?$ !|>-kkk*Hv/hW&dq{{{cccssEv{^!s#\V^ W6nBR(3wݖdg xIbp|<ϫNCQ4!TfXBEa4L.ݼo|,)qTNKgEWl>&^5dh1 ^öh0Y*3MVM *T*t$&E=@M)qucNGl*"@IdJ^TP y~`ц}z~ϒ+l7  4Q&ur-4ōڔ7'MFU"Wdo@T<1F` *Y uus0 HDuf"1Zb/'xx,yyrM@QRmO-i#50PDJ Wd`6.2`9S"jedNRB׹zL͌ "_7ՒsH Dh͔"޺/^w7EU(2<#*6zws8{T$́#D@eǖe@!32Qjp@@fL VYAFÈ qゑ EPʢAfԴ' I5ݧ3UB UT;]0&L-v7.lT1:Npx֝׮'㔫_`Y5j۾["R|p6)WWj^5V|kʍ7R=gBcLvdYJι}hZ(s…傝T Hodss3C)~ige_H=NOyL/-O-:j hp8JPT;s_xdtxtD>Q-)v:]Mq>{.ClW-jr\l]gc6$x7XKV :=oF1fDȔ(B*cH ʦqi\Qү4F'U[NfX &d6|z* pFmcKylV@ņdi:L .ʳ 1z?26xPNU1n+/A I8Vse /8/bYE0 *"Th_@ fY40jhm$gbWZEHO001CL^~36ԫ`Փ Z^a)hDM_5kW/4`Օdb!s:g=t8VW>|K7}*1RCs{AY3 >r糓'O:#l!&ZU&هjou:Q R⭭͋.9b fx`JRn7ݦYvk_nKd>)I-Y]nlc͟-+rMgO宖_۽{ϿTІ^B(9L|4{1:\Uo}AYV/GG~?Rfw(2sJ!g^'u0*nT|K̃ՎJZҒ 1i5LJ 4 BF50T 2 ؑCugcĈFZcfj62,kY l& [k!{zohXdڳm1s 3&M"*lcԭ)WTlN߿? М2 Ƞ+|WChb`@hjވ`xn?XҚ~SGH޵Ztj'V#x.&/~yK=W~[ɏvXU>+%%,ϻ '?A.+(>zt:N{~Qt48Pi<[# [ݪsFZ!J 2ʢƨb"**MZH&hJ``JJ)j 1BnSyN:_Z^)l6OysJFn6;>XE_v9\ȑgΈ<"{ DVvDd ZDC&FUVEWI)BJ*D@4OyFXU{ч"D8N߿쳪a g61!Tog^.h&v._YtsڦKr2bh!2Ӳ; KODYk~Y_h|Ym_jlYUG'GeUR"}sُŋϴqe++ѦөyvDTׯ][WmIEMWڍ4Y'$1ZHX$4y Y (a5BZp!IRǣQUUHQ13|Kce;fʢ2hqj{<PeٹV5,ˣ[y[fbbhBSssn5?DW22oV@PDI#+N$O,jonKSdZ_D_]YYy!Ux>,Udrrtt[omd2t{̓`0bt}dK0C,Š@#2e3AUA !$<)b,Rc1Τz:߻yʊT:qgчɛ'wEs]9`̘于\αGvyv JQHe=z2%(u|qͤgZqwV+[[vvyDx|Ν7n$ê/DOoLgjOPDUҥ.]2?Djmk>8Zkydݼ.k{3BޙΦ= |<ysPPwuIzT d~Q 1=(0LFS5DT<؝L.ٓ'M94UH9dys1F5fҋ/KwS] &ARɍTf FHaVpoz]ʹ&J)Lks} %N@֪ qP`0T4OF gJldmefSN=or7\mzRsT Z{aZА -Y>!2GCMQJQ"vjABWTtؐ` M-,CRL1@  $U:T,4U,<Z)I'r;ޏ*,뎏v|o{rz`P fAb=.cbGNa;`F6$6""HeĹ2C!{+:YwysVW+jVFYX[[͘<GGGx}֯֯|٤ZGwc1P3~VYUfm\,1'-k!S71Fz=&1{1)@q>w:\3[g,T ea`Z!+ 12"VPRm f(`%1w Jй%9"}\]h2C)f|Vq&/m ƀ:P!(iTBP+M*V Q&(jXE3KYN _?+/ ͂moo=zHU tÇW\)bӳ1̑9b+WxsRlXUUU%[q5qZE9?˗looE#JHtĝNөѨ$7o^kS2"o&& <7KHqBbUSmkXseaOZ -%&in 2IϣdY9ruu5Ry(&޾'4;"aUFb96W\s w>TX'Aq,DCwiCIOeJTNњ"BUeI`aTb Uѡ,c y4w;1J9@Tg L PH) c'COHF1efjr(AXSR eܗ|>+?\(efE_t[1˲l>߽vƍ`Z".SNW`>0($mCUzÒu&(^^,g8Oz|R:rښ\T5ُQZ!3qg'Gfqn )BD e5P_uD8 >|ꫯEYzTQ|:E7M"bU&޼v'r}ŕF_ +/^vF5D`4HcQMmS&;cc(`LĚDIM%kyW{jZo[>zq~V# X4hi7֞( C2U%D F*o7Z`G Vz^}͜u!SDld&s(7`Ƞ_^~~F#bM?Q/7n\zjg&R *FB.biAn[V;w^~퟿}+׮sS".Kd]*{%dփ 67/|W;qc0u{Y! ĨՐ 4t)cRd,A %I&B9T+%;UqF'l 砡ʻ Ɔ 28%E4+ 3< WT1u7_: <*B3$ n1Qnރ i`gw}? hm}usshXUeQGƘn@!_q1/AJeK]Yv<2cnO|*ZsAyFg\Sc0z+'3 }gUU5S[vjb^fϫh\ԌL-˲tjgGǏ=^[v{"j;DUT Yy7.]6ad L_ނ4g!|6s: *!%,e=5C=Z jCVg,\U52殓dc4ӕFd@Nrx>O6H]iTBӮX#%ZǬK{ò5DhD@sĪb+ۢFM𳭜Yc&r$4i,Xf  'yĦF٬s+j64P-c[[56>)`3K$3`7o>Q3@UOnxxFDno6+ݽK/ VV;w{2l:=ܙ3)wZEϣ{6?z/_}w3̱GIMS|];aRKDZټ2)w298_Ƭ;۰l:eX묃v/a:T;~t"dzgh`} NGDP|>O- FԎ2thtTIY(^K9ȓɉi"tGvwvv'ݻw˲2(XywYS{7777ommmtX9UgXS%]>)lk.ZABuM]+%N$z J$v\uRZd2 @Bbf2꘧p8`hī'-?Dv}rB+gmfUcB;bbC05JVWW=~|޽"9N| !J&wVWj! j*s@*ҢD  "@6'hH<߷vZ W^y׿Xs۝N,s0-Bͼϊdo4\;>}q"#Q RU9/'џ1_f~'-'_lJw6+cPR*-))bI-5S#9(UTQM9I\LCÓ݀hfmn|}c' A3)5 IЉ"Zi4Y3&VE:FtLB0`e|%^XzK.}4Eܓ'{o^]]ɲ"A}uLçT]tn%W;"\ظvrQUL՘]q>R`JmmL@[ K3[I<_ xFOHr6By^jU>lM4 1|ONdz2CDѪhT3 `5A" 9V+D $D @"EeeXZ rkڲ3nxkdg=sUp.ɶc/믽:LgQ}GU,0{:3B8>z`׮]JB 1&c_q@˟{Ix2m{ۛ[Nvlʸ,jsLY'%DMT@",zҠRU3Irq$ EQ\t;'? ^'`ET@@#QS4b% 2!E"PN.)X <'UR2$13c@e eB(B@vpw>׿c$յ'1\Ooz˯dYjuiS_4/\i5D@Ae{O/^J!D@Fׯ!HdY*C^xyjomXCp&ksHR{Þ }|yLARJ|꯰~P_MfD@1tLyQ+tO 9Vh@!#@EJZeGr1@w.Ҕa,Eah6z~Sv [/2#8OƣF!0N̠tNhv#bjs]y\__}酴Ka KT%SmH,}C逑!5;[J>D$K) V s2<,X%&j*`J6%*sA)ƈ{rDUtuDLʪXuBPS11d2SZO13HVAS4*LUhAjbU Ѳ,kJ5FBUʌ=+ZF$: Ϳ PMeBNiAWsN ,0e 84Ut"g\tZmҮjLuR5ڳO]-,}3vN'ϧD~J裄d|0dYt^*ޓ'd@r%Ps%$:sWRDVV^ڸU={/`a/s 2GW#Cbx;FFqUUĔy 26q*; Xś: 6@ȁ9 ~0Cpܻ %z3"@"-0tp,B*ɋO #2FD0`d$  X1`nW{?= tⅭMN%ަlh:!9$\**/B`7fu"J5Zx.\=AԵ6$u,ֵQ+9,]mPeInDM2$"tes导vQ!A< n{PTBVDe8w2ͽ}o~ /L<hN#h9ÐCI*li(P ;>6hEMv^F߸;,]~0d<ͫ* BzeYqfK`; '5;>9d>tV5@]ɲU__~ޭwxu'9W# Qh [8y÷~_;?<jAUmVwEÐr]EbbL&0TQŨ@I&[rIG3Ny!Hp7}4D`4M~?{Nw@U|>{p8|*[i͸u4쭓QKfJ$\~e}}-D9DJK*٭ əAxZBeEQ$ 9B+,<_ljXݟI4|܇_-Y|oی|>OW;3ߺu, ?~rttCSyva6fsUAl6cg`6._}{ֿqoKi 5s;ȢuFeU?/\K0ЀhT VSt$TI0Q; gC4 X=H̽5SdE A4!Z⢼H.BoAT5tWSh4JG!,ꪽ{.momͷF`eyAUSIsAtĕ8< F4Nht븠{2CHQIMIjF襓&~a!!DSUv;꽻MD8N'T!t;*"Hn?wFw}e!luU6wz;mnWPlava0VsЃffȀ,'{'nXxvVs gnz.św$ !h%%Șy\` []+`h@Zu"EA EMa#JsТ : b Pw !(rWAReY1༻p¥ٿjk<ζ(7Qgmڍ}>@͢!9D#H-@&b~'7Lb J8H TS\Vk|FPͰ_j75jr;M)աC7['8 D4}Aς`$:{FB564$$!xACQT$q/ <@؉adfVgQ1Pf\L(PUS੦@m|IStS+%V֤gSX/ d!{ ,3bfv 9\s3`;>9>Ou;3 :;UcT=lll׎wt2a[/ݾUUJU@bDc':\J\@FFsy|T٬| TLA2&ge`l,{(WZdBoO!s"1"a@>l)h%L>XC D0i9XiQCSdD0rKPW'=yK/Lb߾OD._hssB_ _woDwOsPi+,g#KVv$!ݿ[H:,\%=ztppPE,J|]UU彯9!%Zz[ϿSffeY&zEQUN)f3w:ED]BEQdYyy!FW+/MsΟxM>YSfpm LMELABdC1zC 1 m'zb;v "jĀfj|ɪP`C)FAkQ21Fl#ȆuKH WP`).n6ݿ}ﭟo瞆X8FFdJʌ4{;c29;R@ձ'E1AԀ#dh`pq,NS!Vw?n[Y*ju A_7Ͼ^fY&'nכL&n7sD3L{."~|NM;+iݥNOg^xcsm?ٹw/ol_~1G2(Ā5Trܫ^ކ>׶^\GOފ:wY *9)$率zrLHSPJ6"*'97Hpg͚:wSXXpx< !xG7ğ]}Կu.v޹7:!Htʥ,ij1hefUYZ%;c|6dRG(54NNNvvv677g|>9WUUٔ^nͪ9o]B IZ `?.xv-v?~WǏSukUӪr>t:NAUe &˯DHmE,;Jꏖl1H-o+S>3$.nx"=Rp u%X*'biO`Ux(IYs0))J eF&*"xI6%"sCDwlVKrb@VU`0Kj},?s'8!r5SQ& 2džsrlWdfFg !m" `9"ͧ$P40Ku=HV'-`Jx$Rm6Wa k:0KJH3y"H:~F9Iai)S3˲"QbDw76~\r/OFPQb|?nOd~S߾rL+bPA -~lxRM&S"9@1ZtjȎ0?ٽ4ʅk=DQ{2b3e@cw!t[vyn!Q"j [FhD$(2`Pc 9Eʙks>;m ZQ.]x$931Nn="~NH&Ty0 ( L&eY s.-fNT5_>3  \=#4,"h4JE3|Gx]|SuF1F4FIGf緷UtnS FYm/,\M&Jj&RJU0Z Kٲy I~I*h1 dGLӨjr6ϫ(F;dUU$` ("9׌9bkk{?G:"LoMˣ䡝P3FDYhtr|,8D'=v2b$HL^:OH_`unj8hXuM(*x"1trT`j4V>hlDj5RBdRG0#CDv -g,B].%~5WXjH"/7V5&&ʕ:ݮs.YI9 ^G/+O޽{e.Ī i()/d?;ytlX])`˲ل8R"9ZYHձG!c7 h ]oڽ~St+Y $4Q9PҔNB9W.Z3U׺ۯw(c !LJԋ"U %B4YED$$Eh!S4$ 3M)Qd2޻7׾ނlmm "B>ˆ᝻w766nkѩ$[AOzGydQ>)0xܹ͟sU⥋/b,V$Ճ~G?ώʕ+ۿ?=ۉ64ͬ9^JD04c8RVI*XI??mp?2.bxڜ1j4ΦS)BUn޼9L>w/N'W]ڒ tE777/^jd< ֒,|lz%ꤦ6]LT&hKZrPERji`4-נf @B$1Ę{nh0Y iڑ23jLXAbtow{=f~0"Ji9!`hH3U&go VVLutrPxnSSD4FQ0PbB0AR@{`ά87FL24DU) "**"Ď+7,PBi2)%X=sp.z@4fp)seK}. $~ ITo!^W!dygv8ϧiQ`##׭'G=yh6L!fp-E6殪kMV&]]2AH$K}Ɣ" YU2~P2{@ UȨA'Aҕ$jѻyА qKyb *0@%d ##2*- Bn &&5a:BuhiW O_{(U׶WeLN'w+$vbS? EbG]xU=b~p˗/#l6CC9*i<w4[uxV!9DQ$$LqD'/]ӧNڻ]ru/rT[pl9cQfΫa , 9AElZ|m-b4ΡLHK y`24thZ#@36(T<'$Mth E_@)?.GYzdP jөTL4ES'''Ng0.p0̮S?~|-8rrt:X ,M<UO)gEb%ĎӠUqyLDH*),"a4!'e$cvyY:l87.egDU#N!z6gytPp): `w?C*beS@0E`Oh#*# @`jDJ&h2Y"э,/Gy]h߻xaBy?5&S"04X)̊HAp.g Xo[2c/vZT]NYmiD/%$*%dz)<}VrHԼwQ d) "Rt6ټz;$N}j ?Li`)}lY34T!?h @s5Ec!Kh @P0Ui#33(„=E:G&29B"*"!,"ZJOJ767d4*CS3I1xscgqbx:3U.DTSDidtNdZ̾BL"AT`I@D;r%:S@i$yLAM|MYdS>ޖk^Ҝ0Y@<#k\d 鷜4`~M7Y#ë.=w}/HHFhڱ UMsHRb776=|xwyW(Z"~c)t*3,@HaET @ ($yM+1)i I '%ƙˊ~W`'peB>$#!8c X!Vpq2[qW޹ɛӾ*"27 */b%́C 9`:C(Bn&H!x!#r̨S 8w:|x7\ .l]dLD3n}ƚ$6Uó?m v\λd,e׋A /oZ_Vt?ow?~GG5/nIYNjCf,^GD""'''Yy2KFQI/.W .#-+vCbҙ3e۪''T#jٹW|+!' QJ>m /H.H"6SgfpE#0D7^.>ԐLM u "+ &5Ic@My.] FEv ? ŴkM[ Q%0;!@Lj4*yofQ zfUU'&i"u͵l6 (cP,*aLdoV}Uy9Ν ZP2Da@J0}J)HnBX I I04qN,K^ƹͽ dIKpXCr"Bo$o>}dhE9YcF@=|1 EÃ,8Z\d4u{1Y~|!u/L72FhƪBQ$ w~@P4@hPXIs y&h}Fhq;h*1*(sEfYYpvgYfwttYj9位ji֐GRj,hJۜژx6[% :ҟ.ZƋ`<Os`Tp8|/+++YWU1EN7)Ӂ:1`0|s.L c¬:N7q I2EA4dF%̶p .u岁**"32 e.:iYMf1`*&ff! l#6aL&`0NUcLv777˲,db"8e<H 0`Y !ppBU"g!r09Y; h!M* :@ST0gF)T D?a?W TϽB~do'L:5 fZ$4'nN-e?$d9N/;NQo}[GcgAӉy0̦ )%2|UUg.ϜH A$l:{wӝ?x he0 e//.X$q'jUC4-A G11Ec#5`%% (:h2u1Չ :P enW.lt;VE8dm(|ά %DĒPiBff(@Ks"n9!"MlG^|/e+_׾51w>xv_y_x'+%2q!٨$|3qeeU2%V4gOQ/Kj7hX(>t*kkkG'Ǔm$( "̔9KkPQ"cr`¿Ͽu~EH@S-mK<pz)J9CgP`ˀR]{ʒz@@fUU=z(Y Z ʲ<99BR$r*Zppx8:VUedRq Aef= Y"ph݌_Z6ySmŹY93Di:2sh@#qs:%5=?z ;8m/VsmԦX ܻUJ{U: /!~ivx/˻w??yۻrW@!.VRe~˲\ )8 %Fl_]O{ٷtW=oxԾe}-[nkplehB89VeՠF*rvG[ɉy 1[i S!y*.]|啗}-uAS  cWpi6amgTix }%s0_|}w:$B̼7e?*ˎtkvr6PUt,}DdQ9^Կs|2^C%P"!f4f+At)dAR!BU} _xȁ9:9:L SB:(?^]zʨ\_y a I+Bt@E$ [,֫!]>dZ8|r^ɝ{k8S3}q{uumV>8ϊvoߺt3hZ4ܻw?sYX=VV?`0*9lZy hD21O??~,ivյ|}U~^tssjkMmjZb Plْ,h Y/1&*\[L糓I );6[n3΃xg9"MDФHEs׾F5hsKR_|OaaIB$DRlPʾX;S"`1LLd\⑒DS1P2䘲ӼQ+ER3OX$p"-vMCao)R3tL9Ǭf" e`SS4aH[뺶 PPk(g[y|qEXW;q Kga<眘.Z @Q&u?ؑ qY5cUBkH ZEiU`g_WX*XS:}nmaݳF+J#~WNdo}[Ě3suQRDZ j~9u$v:"/Q-΃յ4`"d2e+*K蹫x?u@2P(` P9g+^wN⾧)BJWC24TTjb*;}ϽaY-|2)IQ*=8\^ 3@b޵߈Xg4VSe`QQBTc$CTV4I}ㆤ@`FbʚvWhhԐԀh(*u~[?{7^}d˗ Gn`ʪXӝ՞i+ɂj~>yɍncSYW:[?kKviNvO֝UY1VzQbD>6MEQ$3?_ݿwſӟ?O_|gck3E+e<677nYIJz\@OBWɧ$>֡>Mt~eHv&^iާ!1tf`)m2Ƀ_~<1`*[lW.\̊5p%l]/.NCVV %ዌ! p@hDJ`U%JFH+89;2(5KlըĔHZ%EX7Z4NCbJWMY`{y:{|[ .FV4BS"@d hi r*kC@:*éqdJ(I?{yK2~NZA_KuR5sHzbݢh 1xY`zn4;w]|>_җDD0N)QQ^Y߽u]: ,JU bv9y`$#f?]I5LD͑yzCuJRPPc&1-hx<#Pֹ-^RHP=HXkeP;c1 IaL+`@hl`LHXX ǣûw녵cp擽}E^7L{íjZkClXkicuÇ{N4IBd‹繏BT@TVb*{._p8_o{s?^/o~3^`rʇhÄ666ҭ: [[}bkΝ;''']xڵkta*.̍x6_5'LvӜ 4%iU-HfkOϜN';[O˲xz䄛NLJY^y!bLeYVU>S6 /<f"O c/h<XtBDKs5-[v4[i5cHa FuםJ\وD-z&rEL1{>cν+TeCg]Rc3`QSISpI!nEpn G&G #g\1h ٥8K\u "MFaycM,FPg &Du,QAi ƕR<6Thfm9{~+ų"〈mYZ2p1y/OG[/—[?{,<Xl(@{!";Kã^9>|8<2:Jy9D\vb#H2c*YP@MޟXeفs}ͣ1QUd(B[ ߢo& @ 4!6$H &Y,Vefnӛp{VٖfpsY{pFwev')hD,\$tGu\VT+]qQ@AL!g'nm'*V&_DSN5%VDA&,<CD" BTPPfT@R/f@CA#W:9;_{検w;ݽ'ϞG#B>L';՞#K. /}K  3*o ٳՕU,c^Wˀg;Of>DG^飯DF:Ԍё~0EQ>x~~mVX%iYwޝYF#c]6˲pbKl0R@$3)uXO-Ry]OͯKp,-"4 ->???;;SU|&/曕AϟUe+ AŚl c$裏޸} wcˆ5Rm+њGD9D%BR1FADl=#ՠZ=E $RO@R6OZKHTn<2luq$W$B;uCӼ,+&.5~Mn"uDVcX;)(#?X% x- zpʨ2JYPH1TKP4CuI+ @ZV5Q (AK I̊JDYT+J58j*f˽mQjV-7 UȮjjx(M%(./vPP@+[#x]""BH훿{?WU&,i} ֭FTaDȲ;99>==z'W_\d2][[M+ݫ۫i:/r%RLz3"Щ ,22#EF;zpAvs2aץކ* p1 B郇O<&hšƀ" *Q*@N +1zcP6$$jp+݉ \J峄g_|?!ɉw>PۻYJm~P.`,U1`!ƍE%*<@,W ƓQ䘦iŢ*5HӔ<D{wDoo?nd{ggss+F6xp)y[nݸq=2t:=??_YY:ovF?k׮dZ===nw6>n^hj) rCJ)Կ,xxHnfDs1ZP缵6!Io]?8xKR+/pTB֪IΩHIxi'Kׯ7]]fy rQ,5 9?.,vei㭹EPD DszBT jX#8t#  G"ΡK""G|Dq\p^p16oC-*4Dr Kd yNЩX V,q8Zh~&{IΩjAA]biQϝOFш:R%AUD)Kt)IT)D ievd~U [KZ`461pk-ϱD[[UO#t*~$Px8ƓVU}.$I$x<NDCLdeee4]L'<~;:McRѽn`P4`9Gd]T8zz>ΙBvFrJU#KE0I#(I&Xӓ^_^FFFT%h*v)g\zS\t~{RH !* *HAP*TAT YGZM,9LE!NO=`7ɇXwCW^Mv_nu"oK!8$>|xw4R!@U,Օ+WeKRV Yc>L}k l4t8oOytT`8O1OJ%PP*vt{9 Dqޞ^tɭӧO-ǣO?~qQUUMӖbdB0;x[QUUǭCJo oo&f0/~ZR+?Zіlکͦ+1O18/w{Yu;GD"xG.,#$U7n̺-mLY%FK>ib-$Fup`キq~gkm}}}t>b/cw?M)vƀ_kGU@^EVR%70_5 "5Ws^ |~J0=>>|,坕ͯ?BGOG|6OnjzXh)Qr'<=J0M``$IVWWCZSnk[oxB,rsk@v@} ^jZb^m˵t0k=J%|m݆,yKZ>]aIK:i(-lOŀ*EsXkcF6G$ RQQ%DrR9s2H+j]Z! J]\Ou#Ӿ^J;Tj\9*)9%td\] dFgz%YPQkE%e&N fAiP6Kb~h[WM&fɒ1TQjYTN;yH}IXK`H6_$$],M$It}},K+cdYcu䠓uw_|IO1O{ԕ`kwwoh8/.x'H*2?#uأ”H8qH@Ή%i;{>lo|\0i2r aZ0vgZ8:zqzz}{*HD+W$I2s> Eyҭ^>ӧ4z;E7 ?|_0d_?ݿ?{Ow;y进I%{YN%+Ǽo^A"*IIӤ:;=B7|\?C+)큪jX?=9g-^۶_jε vXמ#*kV)v&-|TeIEbCs䭷ޞN'NC\H;NeIX`a$N;?667~?6O!f`Ԙb[5Au<);."M/F!EL^[0oV+F0&mڮV O*WYc ]B|& fA@ ʡbX򭾮R UA2'aH(bD<"*&unGi,!a$(BZMAH48pNcJ"%?BњpUC8VKAD"WIs,[5'儾¥/Bw__HVNfYg}Pk9bpxv޽O>Z<VcT"Qe.]QBŲ,IXf|:0RƮ4\1 49EP9E' LPVBE(: М^cXC :GV"4MXG'g~_Oay^A>}{/Zh=$ )V$M͡nV'ܹ]jބNrcoл}v߷9#w|ϟCor`?;`|z+{{uż|g4|uoS:wsĩCLXȱn6@;841;6TYJQ/g\P˵K3-&[^ >Ń׈_ [;erh4Lwec*>~|իWAD"M,KE8V$Mw9nmlVN]fk_j8mRqDDʿPS7< K(0B$de#JV $0 FDK8#:z@e$U/U98ϋ 2y.bdt*G@n*25gBg% uVa*  Pţ\/eI, K^u^X^?ӏgϞf%1ũYQ.Ƭu:v>@\+f*!b1Ί(2:r L\,?tzfժĈmz-Z8UP%1KbZ~YyRA.m',wke}A. h-Bp镏ߣNGWVWퟜzh>_v-rT6 2kgȊzƵM;tCD,DrZ.ʬEk*]5 {$7A fZlTghP4 ::a[D-MīE:` gJQ/"DELMtƨZD2V\jiuKRRk`i2`m@.6G^&Ġ5R]E+4 H05'es D  Je rHβ@&\܏vjYh|ُB-~A5p$s;"JCp!-0v>t6=}BDbX|:Lվw>9c쭯^>xTUdq @Q>!7A'M+!J#"0F$E9;ov;+% Qs^PE {>98ڹ'Ue;'q(Xb(:UB.C%HЂGHA(ȃw C2Xk*9.c`v>y/>}~歈o^ɓѓ$I ;?M{f!Kʖo[S"GRxv,iQi]vmgg9,KBQQc q&n{W|_W7dudϑSW3ƓYY\ ,l{ua8/BHqtΕe)"nWU[cv26X˗%ek+oӼ2̖@BzV;%#iJeYTr[ 9v:>zVVGGG''N[e gԄ: ~?*XZ~7ZˬaӴlOEMm^M|d#Pk;.-t䠄1 r*Z4HD\TAYJ-BYԃG"G%,qIyQ{!%itZFpYy, !V"~MVnS\RGOET@uӎPԞD(\ U(S♼Gu ^K7X^# zwDo*rXzLo뗚_Po~svY횊U|I>yv>x<*b6UzUUV SA,<|6{o{!1i,KlՔ&ڂ% 6'.y?煹8rŦ!$y>ꫯzʲҔ e :N*|}{&;^&wo`T&UBe`ңQK @aVA( ֦m|J$*R4;jG 9zf@ENcёFVՉ+}O}ܫC @II{/K\4QZ7Nl4WKFJsU2a/Z%9/yr. *ߜQU'Wf"Lȅr4f+]p_}[7?#l:MJ G"tխ_CB fUQX*U:R!h|9 Ϫ:1j%LO2z"O'`ť`FQMKy>{>yz*Bv?3ZC EU@Z+d:DߒZ͖]s$O<7߹]ȼ>xz~vF.xg'ǧ;;TuD-]V̯ꩢ4//}Dk)F9ffQªP 2\d> ޛo#a}JlroO{s~' 1pŋ^aqVh*j>ˑpmmFX QYbޛM|<O}.2rT-I4ɗEV VWE=""χ|^yj%h4~׻^/ƈHleҌf~;_y~Bo\RY2PXK)ZdapA48IHt!8ケ! ׻w'Err]Si12 Z$ʨ\K -|:`cXĆﺿm#4kEs\p8*I+K9fʤʠ@uW-/^@LPπL[$M6(" U() :dB\AM祦^@[bF2:5A֝k};ˉ]''8MR Zecb4{,nw~v6`6>DAʢ|0H!ewwm y楥hD8\sa%5 kn<ޭVT1DHjl4=?;lO:Ab,.UpTrV)$e,BV?}Ɉ`D6DQY(' NTL{u[(! " SB)eC!ᝇko\ek}so{ ټxսյK#kv@~e;և`ӟl:f4vww, "P5!"i%,%UZƘ[ie3BZ8V A8MbV&:f^;_ݾγgiJDh1򓓓(,$I*h|H^"yC,DuL㞳kUY@0֥W=uۥ.2t3Gndd|zz,\]jQub|2t:D8<<N 129RXENBPወnw{k3MK٥o?8nj=M2|>Ah(ZzEVjR5p!\d2Z&׆v]MUKr%;YRj͂Sth3v 1SD*Ƴ.nWk>DT"xR"QD*}r Q$)@)@Q *@Sc]5(E _dخAEkլ pSA$XI E"#'%RZHs?bA+ŹiU/PYM%@Ed?_)<;|r>M}{ϡ@TqAԁ (x#VPVB *hA׭!̋F$ fߠjOyRX[+^dr[97R$I<dYt:Dd:+$I`ѿι~odιk׮n\^]Eq[o_S 2ŨP1,ER+J!*hЋ߸j\+FPT0Q: b(OĆXTRMb&5%l y|Ӓ Y)SDcr ngwo{mm9" <{K /H)5هpxx/cHCd-7*ˊ(X0CJ*5$kit6"`DT>G䴌}2n@s=$I$i1V6swmm, |C| "}aݬ*"ޙs s!3\9 1Viq'I\Rƣt2KATl-=zSȲ.!({'Ct:߽~󺾪 i-TT9\6NZ [DiQ&S5 lК(9rdQT;rPԔnYVToHNcN''%hM:˱!4v w1TR͛=٭Rn"Lij1D[+VDmTidL"K bJ\#D,UPD@$Q<:%:Ơ/0Rbkŵ>iX& !PK%o/B0^ Q 7?h8LT`YĐ- qoփ _Y9??{}_}1N't0tWy%O&~;ɿB/BST !VpcT>V\gV,,ɢd5-eVT]Hl~5'׳ q*^ ΢&i}|8J+w_#cA(=SAH1Z=9r>ǞSPSETB$╣C+, (zTrڡw}5]lWn_S{Yыpb$"7QdZgͳD?iYpۉ\ Ak[nmlyΙ#CWĄ_FKͼlbƢt0n#V"8ΕNcjfv;sֿ^'&ݕ4M;N+E2EXU- n2"$# EDux70"wR.# {ֳWcRU6HA$ ETǥpFOul4*5t"JD'''oVUO< Ifi\Īd$IhQ꺈2K~ᕫW^EoZRasY**-)lA {ezw0E렢u ^ɥlklɕ@ڭd^Tf1Jd!ޙ{~f)fNA#QCEM#!v|:P+S.cȎȎ,4b6+z "yĈ$EV)D" U.-4rEJЏ4}RS_~QʝJ]9,Ci_'ע6I]E% ք#4 ^Rl- ~߻sΝ;:}eC !nYtY,ӣn7ʂy:< HKNݳ幀#DQ QU;M7+,{?Da$&(I R/eE\ pbDPBR $ RTm8g5 Ô}Սv߸~s$BgGd&urRZoMmʵ.; U ?90 "@QVׯmUYjls¬(d&^U!y>9$UU9[YYvUU6 !TUBvI8[]jUUf V14VY`kna{EsD7b[3nYϲ36cM+;XjQhGm/#F;AUU[lx>_ ,ŋdccc:u$IT E 0W(ݸq3MVP.6|[dmƫuL#_;Si7iWrD͵KBhU3Uzk ۔ 7CIb&@"@(# *ա|N F*jԺA12UdUQ48*@J j(s|vpߟD2 qNR"FMBL%a1 P֦UJH*@)zWxWrvg>)X2-$R'K, .;YU_.;+bvhVG.%N|{wxm_;`mY Yu:%lm4xݽ'3'͋ā뮮v߹t2# (r(*ȬQ5E T3b™i,cYƪ's.N٩xsu}eh|Y1CIt,ZE* Hŗgp'^n*Ʃ@) b(LT"8Qvʺ {0sy (BHTO9 IË/<_NoE n%doZ${~x7TU$9Q,֛1JъhM9_5K?:rVzcgϞoQyWUUl6k] Oy^E5{8??V;xmyYv|bIL /"&ڛXSnD32Uu5@QUwSUuނ#Z[ {k.11j/AU=;;崯p8<;;CggѸm_! In,n~T$ж9b ~5_[_ ULkMTj(:g BD&PdQ4@!RF@=A[u9wVRviAᴤcx2+Bp\r1cB䀥RDWa` ^3.A+TJHyJ"q<{.L;GnT$Mkp~EN_}'O/WaL_T١.,/fě7ݼv՚ƣt:ur5o(ty丶_ l6yU֣[nmчkX\9TSXDE-\uI!FL2Otu3dzǓXUAT1fąG fGgϾ<# $׽# *S&,*:RHv3b 3cJ7 X,b'Ogd&`u++Ʒl6{koo`-жs?0?O~c"nll~s~>N67;E%eXUUbNOOͭࠪͲ, X\(UAPDREyɋ9V DcA,*(_7#^MH.tzA[_Tp].Vm,q6!>?9YmijH˞oCnQWfeN֑*>ڵy=11ϊ|>V,*ߚ_JA*`F)&*B ƪ XK6G A&,ɻQ9!I^aeɕ~_k.LNnD=IsPRv_IY1^Y}'_3NCߕEEB ĄDI=!g 'uHHH "Gk}q^ݿp/zfÇ677H\ni~[Vk.M`Fϊ;w>}+eY"0@YIF-/A_iV=GQ~ٳgƵL&z#zvvfVU᡽pXE3XquuLVFl639]eYNj2 5LZ|c/eY󳳳p'''izdb]0I}:3F#CQv,y>q%Krc[bɒDUƓx8af+\޽;Mo߾}tt4z[ED{,S4["BP$kgz Ƨ%, q@j#^1/Zd"ѹ]G mV{jm@e,>Q]A]CU6[ gdbhUvhdDh{XZn4*05`;OhVkU#$%'֧sɅ;!,,9ֽi1d@/J($v&PFQ Σ?ȓy8/ 2$Z=ڃhQM:/"pX\M˸ЪiEFJgS`}T=t)ܽ5O>ݽGrjY[oADPyr9ɓ'^wccϟ_zm6bQn;7enou߽Y,W*0*5S PbKEe+( @$; E9Ϗn! (R+pT?z{Y,* fkoǏ%tћW$ J.KPSR1gGyESuV"mujp.d_woݎf$U ӴS7|s[O.MWԐ,QINO$rė[nu]D%6@XڸAjٟ\h3˲4̱齟L&KE[TUe G\3.;)RT+N& QJR1**:%H zxoJY8- 9ª>l$i~Ƣi#my sF A/Mŷ^_9#)P޼yOB`f^JB0w+ qL7O>]o"(8ӧ__yGYB-bxv>-ҸQFzj5L\r1X")BFUJ%hbTQG\)$JJn jKVU;㧳Q*(9WRU1 V鈾~4}Bo& K"R5*Z. T5EP!s\1"Cuh.Sqmh]gPhr^>?={ee llnm2<}ECV-Z^Wn( KQ*CU:a/ O+D2Z=/gBʨծ҇`Q^):TT*8PPIq\4 &շ~H[N((8aTk!v]b u2,742ԎY[s&;ӻI%";;;7_W+Blósjg-W p1t2'F^o`7D*UβlFY,cTHtuJGIu:[ؔiYsn6ij:tޞ݊gvι$I1XFkYɤ666ʲlk>^e<ϳ,EQ^Wx <ϲ̸+BۗNv*t!f̜e-VVVsg&dhDu2f۟T7Ue'''񘐒$^'QEBH|m|h;߸~m;~ "k$.iV^aT:YfZd\ @ڪPMPUlN!+lh;l6X% ºj d ۄ UET6CE! @H裺Srg^N 6YX:D V#\1X*M#W2pZϣw) JUcj-5jtZXܿ[nxjUSnlK.,jVL'M'4"f]jۢ(,Kcv+ֲTl6wDóѳ@bKZv$*U@7=?I;zpTB%8;Ns˯{C;7=bArh:gF1 z$GF: G*`Vl ,2ΏϿ| ʕ$ͦLT$\=z5DzvKm[kVѽ{2nmmT%"Xƽݝ+W{t21 ȕeprץ"J3 aYΦmM4 ZA4+ @NQ߷s~Y,ˢ(:NeZx ,-7jʰHLob/jwu:ubǒu,aVlF *I#3y_f=~12Sʪ!<| {7~뭚lLD;6,QmjzP7Zim6.tJ~aB˩ VC%ZJ(jMȩmlVjT >6~˷ptMS΋2*"&[AHQ*ID\^l" sxjp!LƑ #+≜'XD82(@*޾YBG`Sۘ-Eډ/uL62+)z41å4%X_w7??Ӭ$ !jN {Y뛛yQ|??0W0L<<#UNqJMf8H|`B1$ > &ł%)b| ' *@;`%뭀s,>Y=O_O_O*2_bc" |>_=ps}1޵KN@ŠR1B" SZ@,Ρ4dNJ*/ W &mm$JH|Ž;X]ݝAQ޻,<{Ç~V-q:UB;)4&`y'޽ñ]ȹ$Īo\لU\x\dї.`-0ci9ָkU"ҚuO# U(Πl0Jp~gi:qs8fۓ@^bn+++m[oVRV2}֦X 9|> %tfӧW^Mݾ TUt.AeIBp"Qծ|nٕL F\hj銺zTh_`-dXpbY8S~s :~on|*.@# ֧ 5$1/L{3"sE˨AUX,"SB *(""#WΕYQzBO$ a@$Bp(\*O͢+40z1THΣ.)eT "LjkxUC 4f-E|…  W S$ԃﴜqix_qho0xw7B qVr?:'3 78y0?ϯ\n읏 Hz`/f_|||3RUHk)z%Q!SB%P ĨUK B`"s@dCTSDEEꠒ3s.I;싵=VEnݾuGGGW;YP_{'[U+)CEԹD}Esp~|:3BDeYXeYj= ʥ 34)CV\$I֧^$n |4.|rrBDV"4c}gV,-cmn)h0V4mp#m&t: !lll=}-a[eaJĬP9dlЪ.޻&x~L`+O:?=9=9~wOiQE8|;rݕ;DZB7mlH7Mj-X4mR4/{evK/.žrvi^P0qVRT]q 7SRdÂ^A#[8@r![)(R)#-7@ ,o A4+*DXkwQ3${ ~q ׭,X.v(d @DCp{ǏY"8r4znKisڅ6h泧OYų磓M Uc(__ " ŏ>8i|ҥ0S@?v l:T\t 3OTp #:kICUt h:'7|e,<ӳ٭dcœgQS%PL?sapW?pO;FI pnN)UVR՞T.FOaȩ ]" ĤCP(IHuÂ*E?z'_{uH ]v/@1ONN?쳝$qJNU ԬZH=,</vkyDBUvvw^exDaܰ~{5%5C _""~uuJwf ^cb>QՃveYsE,LϲTP?'I`<s"24RL 0u:d2GիѨ 9@iM1kh-&_ ]"&4MWǓxb$sa]ld2oly"J9*"VyvwwwZTX$}'/5ņM4GjvM qV9ZsJ/3XuEsKDXem!ыKiR.*5J ,86w<q_pQm1GɇV$DfaX8y@LVnϊN#aAM$AA&%QUK|/.QlÂlC6nci<=PaI|a9p+ӟ/?eB֝b3b,M)cD P8使wYsN&d0sRU>˺7߽^>~MJ HsY;^:V}B7- 9@\P I 3=|2w1xս:ǟ`-(o~5ux[k**DrIQA Q )E0dG8%ǎuu9zӯooiaUE8ݻ駟։#XǫVf|4e+ s(R_TGf"G٬ܾ}{ksӮx٪^JsiZ6~]pkMTQ_IefΩDYT/^xap84ʸ|nf jqʲ<99\Q֙h}ٙa&2 hUUe^Y21Uf[: 󹉺 8ZegYvrrREeVm}-, [ и1AV +|4#"χ@Y={.xxx!I^Ѣ,5 JBJIYn?߷HMP k.[ E2m{6J@4)4wKfIόV^:ؔ물h݄@R3MTj<-QV!Ϭ@KH,PB%$YTN픅5Ҋ DCu,yS HbӞ7P̴GX -F۬@B;]z]…oXau8֮eJ zb.JsabˎЗ4XEOdqyovMB>ω (ҁZ9m^JcbUp\__ϯ]us::禳p8޶~;o?z~)weU)zCA7ёO sŠHt$A!ay6*7ӕA]o$lg+2ԟُkH.*}?ʏyI+`TAj:Yɣ "8Ai!: {DF%)wD-TEHG_Oo|nUW^y4IOnmmѼ~K4X5 ,W󯿹}b~AǼE1 3[^z%j3dZ`Y31u}eښ%z= !s"~}}}<>heG,Bt>KV!s3zCGuUh`$9 ֖eYsC`GGG|:F#?mFfc5 1Ӭh|rZ+RU"\`#"㓓(;<:|<{;;̞<~iU+#VU$"t+BtZ&{J*uQ^4hD5vᅖ_jYyd` 墇.ngfmQy`e}h9 pHRk s7DIGQ"@E[PiӹGTsS}:<7pS6 Iljkap9-cj[}IQ4~P{5,k]b-{ ߸z֭nGD"D.$ M5kCo=xm*IӴ,ʇݼ~}su:A |4`@8diw8Dm,•]ph2VHɓ*#@yAt@cz{~Zxr2Noe-vGq|>yUXiL cp/T_ƪo~xxw@ (X/#AWQJ$B`@):$a!р:I'0/cEBJ<7X$-ݻ׮]+X$**şﮬlJeYA7oM0dfUq/K^\Zc^>M`0~fM^<~Yeunnn JߟNfd2cȪfj}V,ޖ<Y?Ea.Vs4'Ki%[yCEOCun7`j|-a6uimWsMs(AG4"ΑٳǓgϟG#7٨NUQfyBb^?X]]ȭGw[g|C-yy|[,)t9.*"L"#c'HA@QE. %WO'˕V]j-t9`_3vNąaWoj !w}?zl]2#׊ UU}I|8v:CU uC?~7]_̄D=5 .Kv^!uB,zSQ<(@puitqJ| %WɎa^UʥPP]̟>}%|Iޮa"߬/ˌs W,t7nڰ{. ]jR|FuLRW5MBp+5>IUQY X)B&c hd (ԺW3Xs40,pu pڛ^Pf/.e<є qm=bpY {}ɧܽw/VU%VV4h[mjhv>ן;Xq]׳BaNS4/29iUӳ?U1&iȲx<2b;]b"`.$I^"WU"",ruu7 [}.H~l.iֵB5Y ޥ$ʲBh/rIudJФMR$0~(DʪeV^' `ZXTW" o#REe{"dM_T+퓗Q__)غ+ zp]uE7s.tAf6edE8VVVNOO_}̓gO߿XH|f)$|ֵg?:"!( ,ʨJut9""KCBbЈTE Câ8$}r7Bwrf:fQI*>ꍛߗeo _WA)*c") !x"U8RUr@@\n~DũjP ,E(f`fώ^]~M15^x,¦v"*ˆ4Op8 *"*pʕ׮TUU6qg`qü3rL^~2[2P3%F-mh Leb*oic΢64h+٫[k#&"qg~B;dZILe,2# Bg1kimnnk2xh6*Vggg w" D;w׮^?8x:M݁s5^IS^W_ʍEkp.^MKٝ. /-l.46ФKj^h18EEmh] *DATUTT@M *Hn#K:cYD@.*#תeS-/9PGmۨMtZ^)zOvb-`HTaiO}wrq`ikRM[o[IN!K+,`kQ[vE֋~o688u6FeEivt, De$ͼ[M;"Ak:5*vJV(KRdV#{Bቖ$W.ÆV6^GLg"FeBѨ4MT8L4 !hDB)QK1z*pgNJc\I:hbPj`2;zzrt<}0,xWrf׮^z##Ru?~zjHqyJ$7`]Z6'?=;gYE"nqsss(VԮEN\ }Y >˟iY˪Ei-kդZƂ@@UQڡs'52Ht)w\Kŵ/x01s![i [zӱ k4 f/ 4hH":9=}y|t: *x4Jfc$O7OZ/^U 9sdKfOm'* "XdRHeHY` 5T[&pIgc6-{Wi~J:- LkΓuU@QBT(JRTdp*]7" 䜢Sv67h- 4j(y<}D}MpއӓǏO_FyT7HgOu$28GVreڵdM-;bR%^uae6'ߦ*Q@dSlR_v%ha%`x}YT-$b5@l.m=Z.k=Zh,Htx2+fi"bsce1 ;Ύ(UUeι(66*N,#XuG3Ke[H[o Gt ^,1< ytVbt_7k$ܰBh\"E^j2ckKPC-ȶF!\f]Z=6`_pZ6P!x@d H $uؙ(ؒ\L%@deE5ȆQEP6fۻZ\k]0[n%ҹ4'V j֝Zue-[jkN.sMZץmP"pTum}?ʪ> "Sqy3"ȥ!L'O+WRp8* 'f7g'G1@,R 2U\!wGBTȥhQx^=/fG0}HճȟyUHQ{brg3eq֮}v#(T *+2hZD( $,1J=:ITmCAPE.y {?|6št:7oz+Yx ,._s!7`A` /^_@/!!+r|*[CsSO9i}P34-#Ձ-hZsrQž۹Vx~x*B6k}" H]:R5F nd;V)ia\ە>hǹN7kxVc'6,^h4 ևՓOh4wOZ"2#(sUedݬ,m]wpBhƲ D %j]sƭ`5vMI mk] %Th\P5P_6AHxQLB8Z5Djvph/Eq JU&j(eV"C qE3}\BZ}xpn4 (4ŋ EB%.e|rpBd(p֍k*UaMt00B]RmdjVvz*ʃs_a8Wh48pp[r}tO,0(0pkJ; 0js8edu Ղ9i T$rdy!WsTʹ a17)\Ud<> 1H)JnRZ Dըʺ %s8pTuTB|ԓGQPq S5̈́$%H:U/:J gDxՍVp8/g:.[( V3U+"ͽ~s٬ի(r]]]}7WVtE6*&oLK.6FBtVo{v)ҪEfR_ɟ1r)n0ݥt+Zx4UY~R{K+o]~T Fl_L&p$1*dUBtOt2e)`H}:Oܻf^Ue'˚HͫgOZyD$زeiww* XDԘ:M."Xd>4bneP#/sHR%{oDeeS-!{T5 nyĆZ;|ioQ`ʪQEYUh*-F[$"v3ڷ4.H$B*&22;2R/D˪遄6g`@齄. ۻR')VaPQVaW`@9 L c8e(j"$ՃXkM?ߝ9 އxVs. YYFUd $PU:sg'Nxrr~!*`~sṜ~ϝ$8槼~wrJ#I* "((̱,ХtpUB"eQA=j%f##pHcqsɧ!M+?9NP*I=s8 s ͭ??{,Ir bU53Dؗ .tU=3=2Ö![82B O)>yi {ӜfUBb%2#2n9|8f~oD&2\{|_6>?,#4 B >0">d|,A$1 r X18!+|XѺW% cByFSk~'FXLzfzfi[ɛe1bd" M W%l#|2 ,p*b* ];1 3N : Xk]e[ke0>}4ϧ =MV#aa̢]`|?s{d(%յ^{[ gК?뒼$j@\ZZh+i*ZzPPΘUGV4FM'şВ~'hg̓Y~~ 5UZK՟CQBn< ԛ< &M4K[t6vEu{̹rqi/gb0[ooq_iؾu A'UuZ^z)>M$;`5uA`dND*tuۙ):J 2B2ɻΨ,SN{s Uei=XQXzy6O,/IV uV1 ZOĶݮ4%ͮp^}7irvvE>BC8ݻ΅Ijbı-X;_kIgl~rXL>@k7<@, Ycld! >j&rU"g">i݊I.$~Lgdzgѵ"ےP:OmC,H3@% 9B3cg`Y([&c>==yzt, !"@`@:uM8aCa%8ӏNN@B/]r*w9<PyuJ쭊~Ta),!d?_I6d% kג$h/e3/=m`>ҏZ<^ ]jf\NK5~9gus9| ]4;*L*zU[~?j'AaEC0MMk^D8Yx_:Vyqssc{kTW:dyK@'0&!KU,_ZZȟ,ʮϱ\i5X*ˮ97]iDֈh&AXK,DbE,+2Wm3 i27,ʛ0gɰ/~q(c+>5ۺ0Sj~gU*W?&[77Efo+4l7 `t(ߨ%iWs4R`R&"+W677f4s +H^u^ie k1~JE=~kׯ]&pD6\Vo^Ygy` A,FCy10(H! Hu_  S '>K$|v݋՟W}C6 䁩ggO޲~25[_]GD Z, 'gӤ7&D†а@4` 0j+4ba ŁBHnōh住·ݺuk5J%PmX"dHrrw:! !s̽^ݵ|eT=+\R)4ҧ}y>Hg9z+%pW8|_ ql:B;(uoݗRFBHJ|4w0Aҥ+I0ck-;X;,ںZJgn믯ߦ9_~!a V6K)*9a:i|ܫ̯s%6LW4\}.v)ۜe[@D*oZHxuY/2e)U3j)/mRKJ"TOpK_JT+kYSE:k f 6u'\JVZ-eEܫc[o֏~4M54LTZܨi1PφQ'^ٳgn70f,͐`x}}YnFA3.&P CJ&@sOƒ8(l-I.XHtw瞳ɏ}]dI@Sә 7cIDzVwяCKT0,ZA&4 `$d'6K9ra8B \R&ab""f $<+(!}$I"u rDqi[n 4~$"-Rb1X\>S$Eg@2y " ,kjkA -JUS;SΠ% _/ Ά'6bG>XL~L1k];^~o|?59ou AB"c+׏?sw~,'knGI#?]|x;Qގ㸈"9WD\jp"H& $ FРŐ"C 0 @@YΘ1)M;0t d:ٻeAiw=MUۆi6"N%H0h@b @ IXimq4a:ٳ'O\vM7E(~N7smF/]t"sXh[,->?k~9,ڔUte rsPD 8`۠juc4=88F{kfIaumg?&id) Hmc$VEEt~NÁ+n`6qioU-%CPaMb: ;X2\ejv§1C kQ+dS60*9GRuzH][Xm 2ܧ&*0V4*=X=y|Q:G k ?fsҎm5հ΂,ȚQ4gW9YJ.n&2A7"";;ۯ&UyMVZO&sՕգ7>$pst|:wۭi$Woo;:W>¡4Ee>3m_:ܔlomӧO>|jufY٥4M$/q9cGZD?o(!16յY0aIOtw'YZ uGv V$d)K `aAqε\d%dg)B9 F.y]8A1&M!t%DG`ṙy`vet3ؘ\< aDA"hee-$0db4322`%H&HDl)s> P11BP8_' CMÃ?}; ~~_#+޽j TDy뻻1 !Ӓ2ť a ?]|:kCg\^u@TPțk"c'dztByGS?1|dj/Ȁlbk-ҳđV3Vz;c@W5y틈`͍s-eT˵K k(u7q֦s5Jc=b :|G*<$+WJ1 oP\J~ԌvI P-u$@Do,A#->U` ɒC^ĸA/ PaRi Pyoy@`JZDe5CaE(p௕޷o}q)=iaQcxӎR@}β{ZV?oRLL\$` oY˓ZV# #DrgC4{L1)QȑqA%rdPaLn `l%, f&0עH%b$7Bd d`92c'`@ȄsF"&h H#s(rc{vvv?~VI5 _z…=f+.o2"&vu? }{Vcl+}` 8V6n޸vY{Z!tX7NJMT*Zij6΁,!v²,N8(iFW;ͦp᪌& K(ypFL Jj{˘-Λ/k XU#UtRs kԪUl Z?eK5s`A_x``"ya!-S@ G3LN:㢂XBa>B̂$bz e pLbA Q$щ >;81(, :GH΢Xk yψҽt{1Qw^,[kaWM_$ [7~"DGNu13ޥ W\"d !4MA-jcVoD/HPigpե(Οh~`b*Z9}jy a`sŹWr$ p()OKN[kOqɳV+Tx?̌uEPsYsΥKy>.d MDYPK֤ †.>ebZ#6 )@] ]Q XBE2P|w~ṪZ._.U0g𿅚`Yg,?aj{+Po ?ɏ YX4՞(Nj`DQ3ae >z:.fᬽf 9@" >MG2@Dc1ʽ 眛*IyQ'!d6FP:>#1C0$ȑcpa, 3gNmkCNч"s_CZh(2tvNgkkEɀ(0FcǠ@T ]ڽ}caevVQ`A4RC4[Y^|֫2ܵWhX^H[e<={~AQ|uJ,R{%Yقք+<:dfgcD8>z֐!2E| /(%װH#R^9+qY`Q\aTUI˩ !w1Urɦ@J VfHPk,Gԑ^Ҫs#ʋ iʼpi EeU%%Qzj EDh-* / H8l``XjLV'ĥDh¦]w#w *]KPMΙ]RC-].~8 :i33gYe!rY>yܥ_ܻ|,0MpgƑ#Z<(!2F@X"29 mH!f`!qΘ\k&SBMU_Ċꦈ$Q[[nj,u(~?ԮY4tnODq"< !ti$F_x53區(">Ev[7WVK=$/싩MAak ,lӟ=x(Nbc W)9[n%I\Vղ-;Ӵ^:zbx%?2)+>hJ@jaP]րvp:EQ8WG3[gFGO1~q;|"&(vY5 "Yk(8^z+ 9HR/"lPTJX65KJT>s4X\oS5XXRJI-Y.i_j  f+ܬ{PPP 6g}"[*iW[ú \tFq?r)b# 8wy*oT_d\opj&)-pU ʟIߔɔ_~1;/V~nܼ?~OˈUTKCJŧQEPVѓǛW.}r_!Dc(p4N'.JDGGG~{{{eee:2^e9Q"PMpVVV8gD:ˋ9p]V()@ezl![v<,"uL&5h! Q7ɲT[fYEvXstxkkky `QVZ'U( s_mzt<ȋ\yO>y_{*K@N(?7N=Oym f ,]x DSY B})ϡk8LbgΥ Ԛ뚿NR S+5\{wZÃ+Wo?~(FBZk f8<7x͛j _3Ri@VXv"/:l&wˢO?u^%P}C\if%Y86XGʶ% g3yDXY5'sṅs@RJT+-ng (.)ʁP}*h JI}=Ku7}D!oߺykcc=r[4Qs`+jw*K`%`8wdrtt)k(&`0XYYΪgsn<?~X;Ǔt2ef|i{?L4>|($I8.oyUi'5<3L]UjӥCj͠Ƽ(Oñ:qc4Gy[n[kC!x$S9Z'FB3Jc^y wه!9gaܺ0YJFTÔ=zU4]^,y>w}Ud¹6{rF ckc襗n֛ei(Vwl4\B@21qp''$tNNO=|i /`gg;W^p8Tc(NNNRVuWlGQakk͛ӧ"28>>v(, jZ}}Z__ϲL.9q맧Qi٤^jAsέZ z&nnnEl6vjQZxyw{[nHD v}xxtGkZքsC,%"ff"3NSkEY*@֘,p׮V8B-AA{_o#VHK@R ЩϥyUz[~[=gC? tO3L,ӓt:3q|rrrrrjà?(l}cY{fSM#54nll\gyAd*քKnO Phd㰆`ݷ_ ”+Q1VNjbʰ^ !PT9,$SZKm $چSkPJ_wC^7(?aI8!T73 @E J\R>i"AyK,X0UiՐ*[&yŗj4n߾ۿ?OG(JtΩ$&0M vEXcmn > 1p8GNGߤ(V3L(MpVuiZE*EQ4CI[HլkccCURU ]1R6W@$[[[J_=VKSƘvv[Q7kt[Z--$ >BuH\R,M֕+76ֆS8|_[1Ti c_|y%.(>xBy;>_ty41qɐc1DA(JgiK/^\Y*]|0EXy}+9GP+5v<7,/΋f5Up]3 U^QMKnQyX} U.q 1 w*08j,*jiYjQ^|^+ц}.jj PGU܏s՟f3 cޯowcDfŖ}Q -6gpeum4>ݼ{{?UfQ. /ZXgYFD>xlQ[{\]]__6 jK ,|g6 KJO}l%Ϻ,>2% ș/#csϾ yi䫮*Gյ?{w?NZ-+0 JB,lD f4>~Q^{׾tbh0lll+5BDJ1Dy4)7fE~<;VVVOYHű󜙵&NJ*Tqή:gYAQNguuU8͘Y)tzU4r{];vww8VJFO;>+߿o{5 ύ"@C̄qчg HCY^o^Y__+BX8r >>9>99iZNG)gu)b6QUn΢2_NnY2}UŁ֟“6C^4=&ѡFQgdY "oZio,^ ̡( Dgf!dfY[[v*PRRS7kﴶ0]PR?Zj%kfb!•koL~|8!!f`0qꆈt6v"8{$.mqi~ M&D\]]ex ]eڪjZV-Y"{EQt:JkNl>W?*]y('vTZb_4-.r53Bٺpb?,"", ۷o ϣiQ`- "]p =DdiKWP1Dd''ǧ§Vk6N^7F#}S4IBlvL(%YgO?#􋮻/.YQ<ϻhmڨL@D Kwt{|aVQPBE:8|V!y믽F-=$ۥ h¥8h/% ,(<Cmy3Vt(T VeXY/Z+  Uk2]*D /%El48SYjṪuf*NE*((," r(]Z=YX(bVbi *G*4,Yњ$VʶҴBD@JDŽy\-eym K/|p nU]JXE6rYh<N/[sO>KFӝUDcF(A-yk9Fx}:?t:5hŎF#l6[YYq},2-FMSjb~nzkTn3~,΅Odkl6;<<hLI,M;Dj|=BA[B7n|WJDQ#2wy⹊i84T9s2wPIT"aZX\is0ڳsJ[]9-?V<*n uEfU"Ns++ (e^zF}LVҭ-vҥ:O^ KGQU][Q:l_)k@7ϊxA{˩b}gcR|ZE[m4nUn_ǏZ{￿U 5{"Ay`Y!J#֭[.wTY 5 tрvFXX-XJLK_y,\ٱ"* EKTC Ugc BJTsk.+W2F҇7`^T kO R1}kQ9`ʼv8M1J Xgg':ׯ_ַs.MS]5'GE50b[k#! ?OjjZZ$)ڤ3gmZ{YC],(bLO+ҷEY5KzTztJW9j P; rzJn =^?fiFW]xqZ6v7LyPo j췰C>{0 @@^ڵkf(F' awwwՌ_ky<\?2Y1fmmM OOOU!x%5I>|id}}]iQϫ koc/49~,l `QGGGڍ$IlHz a6Ƥ鬕~뷾y ZU"T/V,M}iይY.AD\μ>]VQ㮛5d:g819s՟K^y9L^z~wof^y4eY-cmmXc#`mZׯ_}ӷ~3r. hZs#,o,ӅR_E"Iu8`]]Հt$/:whTDK[iokuԃ>˲fwl6*8á:1=VZkԸ,=?l3̞==8<<\[[]U_T<{`oɽqv{ ,"^[L )zC`Usۢ(qeekХnk?n ZvI6!666t}|v0-u. Wl=҄XߖJcaΒUrQBd6$I:'Y@Xt7WcRDn[{^#KHU.(Aee%u24T5\WG ai<^8|~NӷWXZ2* @mE/5VkZ]ޜᚆ7io&H\bh&7'9X*+jFaggtLJX $& ^ yWb8]le.2Gl."[UH'՜]>|_OO/\tҴl;$5=uc,SΜNӴVkuG^#Rϗ K7-x1oiFU&S~+L}U#2S`Z-%U(RK<ϵPK@տYh`a"?8 "`KY]]v`Wg\fT3!ȟ2Qk1fh8<::d /Ek_;$1~ʕ$}1eJLؚ DlUu04`@bT ; /8oԖceNkD渙R%WWh)7 4GH*қkeܪ fgOk MڀG`WWҼ4bY<4Nj\ 5"RXE)?Jߧ2i,j|ix4_V) qcc7xh4(@_\ޜ\uL#~?NÇVWע8Y*ON\7(Quu:PŰ8gXs8QdK7L-,Fj""(GrΪI,,C'*,DD$Ui]k6%;!XfIlcfXwA+XLg?ɏ_{핵~Szm*qOXAt,|{g^[Yycݸ"j+z1ŗ 1CC& SJ*jq ?uq셬QV.XTt`#7޲|<EJ%lMĹey-\趞,A*ꏠ/cZFçOfSDBA`DC>}kמ<~S,1e"ENꫯlmns!`*:=/a:8J@/__SDPy< ΓPfnll]σ*ђz- n¶8r>jCRJU\c Rߠs*_CF ɽVTT ob2JY-ˆ55U'dxe_$ %-e~Oe4VH ֮z{e{n.MHoنnſ;{?Е-4O*0W*m(һ|ݻ_|^ɘpx?pNj+BPkP]tR#9ED;51:^gZNiŦ_*/raCo׽E¬%A=ι$Nw%BY笵>) tl[ "GHQiIT)&a+ZoUl Lc8O=z'o޼U?Rupwoj<Ql?O^Kd9xC{[yWbԺ["bG8  mCJ8C'c֐GLm,K@Yc5a5'h.a+KG.΍=t:;88;ge]Ph ͍>?" Z^#!3$C{睯oUgQ!9%Kab/>WuUIմ\M]C9z"uEw 3`yU@=mE4g\rKi-'Ȁ@D_vw4mZ0Ҍ:^?UbUHE!|pgc}um}{%2dgE: iRc#T$Lmivx:F.2՚$.EeymC`fp 8!݋@&Д؞V"zYPUC(QvK6d("lug!"QB%1z%aP C`&2taw{k]^>֭7nlU>][ /^E 'ۻpq1v6ˌ1,- 0Ȟ!"D` #ej[~ 5{n? TmU0S8 C0 ptX@"*̕?" *Ggt2*CI\1DY0m5 14"(,"l"$x0rc0 ![C"21.r`fV[׼sX4dscwwvedu_"ai.U]mW^"YHer`"Ɲ1k4,9H=AXۗվ,K9%*d٠+ A%d(; k<d))S0H%v[ )M<#̳Puue-ETjWd]48G-kJ++l2ԡBSb>g K]ҫq)T&x*O4Peڥ:u``67'qf`ֺ4O IӚ;1v8cltEm"*̨tQ&9T8 hZn':3ĢHC#Q( X h J,9ф6`\AHzD`%r3 ѻ "Y@ V׶^}Gt$G7nDD  bCU߬ EUb)=zp옽%J<HVfpgs륗nu;]8ʦGXЬRDŽ\! A i@ TXs|YYkIT6az[%83! a Te!cTQ 堬H2Rse/4WʕUSaUсׁX-T.cP*Q@aAt#0"k4;>>&)J# p L` BCB u-&H\OϞ=ck Zc,{իW{{oM4Q՚Zc>EaAB`ɽW_ucMeMA.idq~z[򫺤jb4\͝r/R8{m-ΦI*H'{}ylXr^֛o:K5#E>cuZku;ֳ/~pG=ϋd|tttz=/DK Eh#alnVHϐVq؀I8zs F#3K1p)SNEs9BA$ςQ^퀍Iȭ7wOA<EQӟ捛ъA\`⃃O=5XCEx׺vP: "R*$"#pOWQ,3 8K%0'0X@`gw]l6Xͪknrl7DT!Tڂ 3l@.H,lͼZPJŋ $ L}$BT2hӶ7jw]汁82JAB4Ɉael 1󣣣~ODXAcŠ?ͦnWD={yct*^|,}՗yXT<>rq'%BoAE!-ⱲZ o$OBmU/u4lOO\T|b X2*H,Qe@f~ņуyg-;FݻwJh<7J ?Y\Ka{嗮\|0c$fYu+6vL %8!H9gXp^Mm#g%{dj9FjIUvR2d H˚q6 @a,3"&t(b$@Tz/# V$PɣXS8t4VJ Iе H(ht.głeD{(֐5H,rYC?i;j7 D.Ƈzh4꭮jm}uJ,լc s(7o^~02eNJV0~)³3{ _.9j4gN.51RQ5x*L5/PcɂB q) D!# q(-,"Վ,\.ORݗLMwwu^X#FꧦDQY\/P^Y\V5UXOJwݼsk,.L`)a$*+WS \{O> ++!";-۬]"߆==X_ϲ횅@ F!Ϭ~,>x Ʃ{m)$!Q2(@! q` e{ZʫS7MJ0HLdҰъ1^r  %gE.e EarAP$f!@ =X|QV7lb;r EQz>zdIH?ZQX!=oJU{߹}ղ*f޺}kww985Jou  `Ȓ18ظ!0hS.q5UuRKb)Oqh>X$aR+"3d4zzz:ν}YYD8ƠEgH9XBK`L1v@`&&a! v P'e1` bQA! H8.$p01s@a,LQ<{Lr@~f5ĵ+/yA?]f7_ цU,ˀK xn@jo~ed4_"^jlu#h?POOu µ \\*VU}3e!W\%T6+^E4fT7W#Tj)c Pjh 5/k@wp"US^U1PDs+E~U /???XY!BJ?r+W @;w뷾ypp{;NW1trrrjD(2(&8c#Lvc c"ZAU1c@0Π.=_PkYsODzEցgt` Z+YZ"l*RfE!@>˦4E6ӬͲd:Lt2l6."I.o\sĎƳ4rf,Rym[|޹ĶYQK[.l<~L$?ɵW޸4{Wmlݽ?i$q@dA{6nmm\rN "Z눌V'ιN㜛`z.| 3&rP6وϼC^ 0Wj' gMӟh eeiEhO0.x XCb@, ! 5H$D,3b -aD# Ct ad(rԊYj 5D%o :rF23 X2E@dV3+F AX$8 x@"f>:: >`W^) S5nCGD/\حW9_ 7R)rG6Zk\pΝ;EW~bR\J4{)"Y={zzzvsa9>>FkkkY6`՝( p`@D 1xt&]'b1DBˮRX7שfFI\Z2YF?{? YH}q83ثzkOfe2q:< ,l6N&tO'h:O&i:N&$k|6I<9d3Nw>|wr)s{/+|!$|Wo\_~F2Ft:zp0^]&㊼@̒eyݾq6yQiw윕<هxG9{xp(祴 JB,|E F:#%ټh/ȥnkXx?>"!AGAc׽ry>@a[rk dzY?=z(.F(ZQQvy%2P\fh[*DH"`)"q;[G>B6)9"`ƒˁȱgs# l#/(La!3, 3< -AbAd3qM, :%vra)1 # Q<>|(MS>N,atA?h%-$ue w:7onojT 6{K,=KT޴cT* ( `Ȕm(0X#ۈ>wP#+to2*Y5{7xjbCg󪽮꿂TkSm LWeX-5ʦ QV9\ @kB7S5DkcOTX9_V*P ee$|N?梐* 5]ӪjZ iMCyRRҐͭW^}…l"E12u43k5^-e裏'hgw'h4XNң ;qqY$ yw˻?>#7nZNFk`륯"oDk.Ɩ%2q'j ȶ^]뮧#޸y$vPQ ypYfYJzَt33EfЂ$0jnRw-ΊrzZݤ;J'MeؙX 6Zgct2Bgo8M3YΊ:B{qG}?xۛ);C.o^3gZ됎?x0 ֡MՉ 2֪#2O|>Kx tz啽[nX B 4Բ.b$#lEP8ōJLdZm_ٯ_IE̳u&ȏ>}dS$y3p8t:i7hj9˲|$V4" Aw'FX$D1=0*:{kn°c0BtNĖ!!SfyD F`p`bY@g# `@&^1"@@j"BB~8|ptp:mDB,(a8?~4v΅PVNDgO=3]x/WѸ閖> ksQyryqŷnWRH"T_ւun\ќ` a"h5ʎ>9XT *A$dV el $"L\EꄧyR)X&Ƌbnm#Sy}2l\m$W`e0aU LjjԯAFE *dVgy$" ;,msyy^߆,"k_~a$DzͯpI3Wi 68DD`]fY5&-o5ooOD i%\nnX ,!E02B= puׯߜfL4i%DkM,yO/\YoDx"9{BJyÿ??Jcxmmݻ ]9O{`&G'?ӿ蟞ZIڛ&̞C[n_z-0uAg&&t( b8_ӰuxoFob"4.OMW6|ɧ30!eGGG*EOT܊P?l>o Qks\p故a9 &""%oZ#М{Y{e?(@DELVB18&(`ƖC+GALRg,)¬!!˂ȥ yC1d!tD >GșqgNIJEy2?36Z'̩R f6KOFb:X딄ͦli9::ʲIH_)H֒( 1Mwz՚SoKX$3E&oژ@]PcB#}ݐ$ӧO_{O{d2cllll0 K`8"lGm,ǝٰUL&I1'S02  F1΅Wq:oO/lWK/?x}+{A:!(jax@A::MfEa,ƞ"Zw>d16L\nmt7wtꭍ01|>=zrKWmv:+cdi߂lgD$Q]<< ?W66o'ptss "FN5 _PRy:GIgG=<>T4w{{w}}Yf)K\dYD]ע6@?yO{V_[/X&I뫿:?gɣq?EƁ1Pwwwil<[jw:k>e(uz5Bd{&ͬ $d>v0;,iQB1X|=`ܐ@0lHNyM5םOSE-cgVL9d l[vb/g~SB6c[ ^%rfE'cvVp0bYπKNN>{,OK_+N'˥FYr{! ) «aע/t! U˜74ݙw/*Hz+KV,ѹ+%g]j666?Gn$*-0uB疬"qBx`vvG A_ENh{ݕcGQs5+ [~vCe[I>[o]MpnMGѩ}˯qӓg:C0 hՔ3 e >#nlnmo?)[lMnmbgeQj Ş%$kV:''>ىյյbkeekn46j bzq"8åK/G?5ӟGvm}qҥ7n|]f(޼q㥗^bvm_  g?\1 $˲'O?yɚđ`8F.rSyKݭ/>$| =v*qO/ 8Z̦6S鎦V#E˘@Ǖ'{="<ANdYY==]uNS2g̷ӵ̪EEIqfv|f {DCAX<̮oima1{c 1֜Uv6Z%t|֭ĚUkk߽{psߟfImB{z&kO44oÝD!Qo'7V-PlܝAqc+q{o=c0Q!hӌC[[qd[{M讎L 1ﯬˍ[4-"1жY7gn/kZ|~nIJ6@4W: Q¢7KR,7@O;T}Aj$=i_ XnXV|ɫ`SM/sm٤Xv. 2O "t2cP)e < 9$kAC8_{׏ol] "4tF;D`<s|>'lvcLj 3U ŞdgG{7;{F[uY=^mNmn~[?xo}lǰ?ع791" 9rlkX]pd33W_WߚmO=ؙ -c ӌ> V8^a?=ٍK9Q`urleZCciM 340g?߶iowv>+~Y/>'ܺE죏>ں~gYN;t}z? t:O߸qc柈l NھD"I`"lik.N~xatB.bnn~ H0iѨۨh4hveek5֚mD۶mɸU75c1ˌ%jmcI{ệ7숬x#]OXfOnݝ-. 0tѹܟ5w}vg86vC>0cl'?8tiB֢a&+<:ߵa?cCA{÷oߺsvm#CM߾}[mV׍I,~cvDc5ιl:}^x?jTfn΢$fbT#X%(&5xĄSUZ `Yl(< ibA<35D %- 5,;8CvIj HJE%RxONރ~n >ήJvE"d؛4 Ȍ5ﮊŇ[,Cph@RFjŸ9P1G#%F7Zquf1Ϝ|/eA8 YhHyIh*BF="Qa͛7a4߸qmq{CLD I5+S+GHӃ+N&ͻOxf'=2>yzԙSی&n~'nnܹu'kԮ%"@8<8:Ӷ+֙ӟݿw[~g_:2qs`yڭ[wɶ4v49~cM 5ΣG꽙u+c~\=~ɵDsIc,d m-(΢]q-GWgO<ҜѸGW^ΥWɸ؅'.]m>hjS/T.h_W)W!Y||5ck[@(PS#3YYHdXc 1V RFƲ`d;w'䅃Ƈ~;;n. [̵H1k@ cp<$$FHuXC3ֺ5YBrεmckuӸ:!9c\wجlG3E^ݹDzb$"yƮyb$FE#֍}A(F0蜻~։'\CDjomM>H$Dd2xQaL mDY ' ɖ0kAyG (E_Blr;*֒I1d߆yo-R˫DD!W✳)\"~#HTXyweE%8y*Jt*_Y b#'2l/5'"tI*Ѻ݋^,9=(o#jxH΁L͗ܓ#ICrɪIZѨo1 bLB<@",}.,ŘCD}moߛL&kkW\9{ӧݿ}΅ k9"38;:qrvl6Mڶ}`cm͕kܘnuy+{@sؙ泝+Q39[7cOu6)S'9}Ϯp`vVOoEʴmȀbGD=:2;>N7gɍN>fds#G+bgBOћ(exwkzeڽg?~o>,Y㚳g'>hW^F5nP~W` j X|g6 6^1F#"K!2c/؏IrqƂm0P+'d.Dђĸ.?ݣxpȐ#gQ0>rM{軞}!g,e* o"7YȐqZ紮r5M3֍(Zh6McӎF}sp#LY19cmڱ}x}w:s'GqzxD ߛW5c`1lgEAfD{AjZ/4B$+ޘ8!6SĉDH, A!j}"m}" A<l]6=8hUl nmmݾ}_ߺ}ӹm[k!p֊]YKxܹ\*fT< gɂ n|#]V%烥 }D|58<'P((CU{ep #љ=ni mZjUHWX|AuBɡnL*㔵NSe+7o1{ ]{+7W[,6=??{`[?=3}U}d7̉:{okks>ݿs?~Ow^}6vW_xG|zekgZ~c>θ ΰe`Zl"=6ܻ٧Nzsuef'J`d'd kmm l# xw>o N[ÿz{޹CD#FO>䭷zg3UG- ϯ^yK{o45S5I[-1RKHP8FL#̰qƴcݴ⸵#0Ec0@+!xCl 3Sh(1ۓ NAD#Ɗ56 ЀMSsF8P#Ŀ:jfmm9 ՝<2Rsi&}[ӿ1=qN!%hJA"a}ArZf=P"\ێZOI E`kGF{bx8 ֲ@8$ٹul֍*JdD x޽óg~g7o2֐_cDBZk,sk*vpHd.`; +Fwrs}\_Y?qxe ԙ܈}j9r,]wxN;pwge-v3؁AY_ mf~^w_ٿ5,}XЄfu޺s?yƥˏ+%mQrneR>֭=qY7-+LHEHAĘ] sDHQF@7  -T D1R[ZlN4UN7j) X Fz@(2CY"FC#qѴB foKL$Z4#k\.ލiHk"Cm `̡]a1gvZkQZKƀ0E2dJ@ I:޹֍{ k7/o>L٣l.;6gprct=32ίo޾;?W6/Cxo]Og8}Z;'6koo>ux7ϝݺ3Ɵ>hr^tma-Ȝ?w/_͕VfX&F#bE;5ˁOFY|8c猳'mDWЦ^B2X"Dh%^$!G~#E,Yc #`l8ql؆`  3~W`aF:Msx= &>)Ou; h "Xŀ x(MDd%qfbbQ%5L$2 BbDz2*~K #Jq!jJ?qQDk8 R1 Y}D[sEAB-1%@CBFh, :I&Z! @@h@d(Hg0oo!6 r[g {wo^{D,,d%4ׅy"FԶeFn@>r4ƌL+ }߯LV^|ᅕ1{j槦*J+3pC!%o͐ FJ\, 4:+31aDl2B;xT5Tys~ʵW{f8 :K;<siyaD j\$kJ*N[*~Ԗ)Px4؍tZlK!RW*<2/26osِ!9ynڂWY9XVwI7j^#^z??a׵6pDf!X4Յ(X DԶ[o|t…O>yQܻ>&#A 4Y;qnˣQ& gΞ:sb{DpǀϾǞxɧ^?Og֩ jܺyß~顗{w~_g!{ONx7SO}?Kg^ zcO<͝k<^B`h24^!xLcjW8JCj`l!Ou`$cѨE "B#b@00wѐ11r"7[69Ye1 BbfnFBbIm&`&*Z6V &rS4n !IV'vќPyK_;l|o%`Z\CH?4D"B @!ywlQA$I}ј~P(])$@ "FJ@Y6 @2j,r7θY"h[{ƭ ܎ Ƹw̑?re<<7omfБ'xlkƘZ>sԩSEvN seŕj0Jl.ZuMyFHZPv,Wf`CNV,%yiOq{Xc7UD $dn>+Â-5P [a0/y|%SL@%b@'֞l G|ZXpJn#&gϵoΦ,h5LKVY,0sLb֮,XXk$^yҥ|"DD0ƈ6 p1,iXX@Ɠ۟~rݝ+W|o)>0On\;ѬuΙvlrY7u㮵 plwܩ'ۼo=!OڶY;3?}xworA>o~3go\ۺz}kׇՍO_~~ /q /;{On{߼^x_vzءmucmkeJ0n1z@GNzF1;@4f Y[[CQ 7,"dp2ԅ$";_uKO24T`Y%@v'_#q(1VŪނ1&OX0׀/A5Xr-P/"\<sFW3k0(UCIm 0AD10GԦj%JT"M CZDΞܺf$1c1ZYbZለhp:ݼy ϝ;c2}9K ZBQonnKk֪ᛎ]-Ns2_fR. ՂTb>R˚h*Ew\-+|-G& a7gRMs-~U 1=ӛg 5ŻUb!VT `Ӵw`Cf{ggΜnQ8<ӫWO;{i㤋k7N?@1aǟ|w?{;:֞Nڿϝu?ŎWOlݺOAЃ }̪ixib$*1D1_0% Z !Ѯ.,뼪ΏLy8ʧWlf݇}O=bW"Lw ͛W51 dh@ ܐtfW,_JVq֘ҿsQ0,5BXX`~EV*G\t9p9!!$3  Pe/_;j 8",wgə.Jм@8./%WaٯFy$O'OoܾCdR3(FGJUlW c "ku}ڵQgyowޝgΝeiȾЬ]/g^ ϶h4k'Whսvz(29u7D%z8 Ox MXkx=jt\cͦYGH,kA ئ=F#Z,Qlgs 1\Z4Pr-& YB Gn&;~Ξݔ#J\`HD~Ѩm1>ʦ6 U>j^(Y~\25T:-+yb.đ|V\CeFjFHn5# Iwes 0sH0WpQP`ԎU"ih:3  3;wƓ!t7v]B<Ν|嗌IGo^ѽ4rV$GD!\- w旉CeO~o; rTty!eLS7BIBJ2@α9 x`J@.tkZ7+p>;PsvP|(ZC(B9|eFy??[?#@!AСAD|aCFm+"W\|2|O=x2km-@١Lc`='Đ1Xf,]?ݽCG@Ç^ε&v1ĩ5Ng h,Y+kcG&W((DD0!C  ()xL SMO2q%R2qshWmɖ z#kW\h2n嵶Ic~}rj!;BHZꑥ`K8/VhMY?E|!V^FDDɸ!\.#NֈGV5TzbdD!ȬVΛa\3cqB 4" LLXG渰v#"FDm2[g{C4W^=u┱O>}? Xh14Ʌ nBRLQTw[?IY⍷<<wAd (~Q9. CaK:5gixΕEX$w%sHU5 C1UGgzx>bWs eZI3x 立dEГp"7Dd6y7Ξ݌sLQ-9I}+.(̖ ןzxGmH[[7o`E гD^`bdq4SC>>1K(bkG(3CZl\/g]d$c4"E200Hh!K}>1vFlq2<Y y%2MSy`| g\D^|}3:(}(@DH|O?͂s9DD!]C_*|eրL_8+ǪR^dldcq"Nq&e hwBo[ x#G$ 0B$P}"#2Qc ">H X`ƍN6\Z7{~_ :g1Za=/skأjY1ƒqUwW6EN 3T빑4e~cp u^F8k Z-Nb`\zJ.ͺo,` O>F5c5 N'16i,KЕwn6kZ2-S n$QZ"Zfx2vHQ$dGǮ3>rCjh1qFO"֙1" hqLsa)a$Y\gKJt Bc482[dy'Y"% XIWjѕϮ! gWT?+_YYY}lW#@Q)*k Ҽo9%5 :C6/y0q[,orv>yѼj Sv %F XZFGG@,H@{0 BA yK@dluX""ꡧevwC x6޽4klX߸P٫9笳ۯ+AH.yؠ|3>e?uϮ(0UAw,,Q!vsǴ\T,)#dAxJ:^\䮦W͟PuCFU^ʘ,CnXpV^O`WR LtH5'BӏIS &U[\-Stl`pzvtJ.5<~ξ/@"3B@:T.,HiHcƍ}ߝ>}ڵwn qM4i]# AG1-bCc؇( 3cDBѨ'D19ouGF4,ը4c @0@Hb# X2DF0HD $nI$䨡M" XYD!JvaQg䪙5iڃEڦyƯi|>X?ںzL zDF"HZa< Z @UfbHBrլ9 #>KMKacRWZ4Rg$?VUG-2` $,H(jTcdY)\->q{FKxB1@}0Qʁk߿wرc"U"#QDDh*fB_|,85" ]hvvsT(m"Vxw5sm2N!H*`/"H=;[f"a ᢤYΛoogD,d=hh0+(b<_ӎF׮^8v3g.?y:@4d%,  PH! j1}`:?,H4,V8"3C!"F@bN I >ZO' 1291!'^#O`‚T.`e?6Xe,s;9ELgoܺq}}16%}=w?晓1kcAT`!IO26S MPH-72\KUN $VKB2=2 1 0Qa&ZiY}d)͂\}0hx"$  08 ANrQZD7dPiUFѝ;w߿n\7ݻkȒ1Hi(^D /]4Y[kΥ6RVeUfP,N$rM\Y\H_I+k\Yc%'Zyq&kݕ'9 |UÐdFrp,eX-9XTn >^坡$IdRWsRZ뤙H%%ê˜192d$2 `A֓HH K&S`YS!)ܬր>>,Z.^OYk 2s.p N(U /a<,֘?d:޺u`9xfhHHD!$0CX$1'cF1xAjMA#Hƹ!F7& "0@l1Zl-a G,#34LC:E FJZBg le{ag1d_fH/Dֶ_7N whE{ܿ? !؞uE"!>` b3ǪrFAޑ2Q+4=LaW|I`-0#VG.rؚ+gj6jBS- s|p\(Hm$hmƬev}) ߸y`zy;w8 "ɴ{]8$AL~Ǒk/]yaXZ3 }Ѥ=>paZDz(”[DhX5UmdE>Ãi°JdOTW؋I X!#+qs!p2dkdB 0"z#VP'^!Jd N;Ë $"E˦sB9ha: &Iu(AF D&A S lpnPRvL> p̀ȱ-<V̯TJ~.O? /!1޷dk Mɻn);cgW2- D@(b Y1 hQb2G  XdLlHǣp@ň4oI(,8 hQX(>0#PF ``D0j!QCƖe[&P->>tʕM`2Y9~ݺ}H. ܾW7~e 2x꥞WX eԕO`Β9hϰ@:kDXjN) /UR6.yE}`͵WV[9b$NCb< ~szU\: RM+plHeu 0Ov㱵#`.; !8q78yvr>q6oU9%tu RCMFc,h xL}?xmJ"N֥j0%" ,Ɉy} ,Met:UJoDAg“ړPJ ID_PiR9F'BFr礱aR~ ACbCŌpV{ KNw1g,. xȦsA*ɧﯬ7!P,F8Q95{{7n:uzoo>p3ЃP'blʕngQ2syb*+UHhf" )(=&"9Z~|0~RI' #6XR??겘H5s2Vw(z lk7x)c(_۷Wa69<6*hOdaѝg@jRL*ȁ857|ٙmʬ-ĵK ̙tH@~ \&sV"\7d%F,*s^8eXhz/O'4BF1]MXc#3Z뜫 sƘl|gcx24JO,遄Z!Tx&" !y^@Z U1),9]`̀'07C\81QDiF)!&"&L! ߫(g d Ba:B2#Q:@ԭ I>%h@!4H XuHj31ZXbB+HHg  ) E41FJfJ8! F A 3@4,Q̈'"@BR6 9'_6z G$$f_ț7W&i$ 1EKC֭"uc^dGXc 3Q/)TDܬ+ ~eI\9sV3DjOd[-9$+7Mr skhZQryesί8S/ِu[U#G~'w!jW{vvwvɌ1+ bgOoTYi5M-*:2j&'!YŜK)XQ2(* ~¼@9q;rP׌£bt(XxRFnݸBX[[;[MHXVWW_|ŕ5rojHDpyE*9cL5;>Sd-|`u_"dqj%AD))hm/yd0 `} IHK! UK&Wٙd%IK֝ r9͟1-:7)Y0bڝEV`hxL,-l$G#85|w1 SqY9݅&ݹu箱s'N&L1Ujx-1I" k2ε1H`^jr.Ӳļ\ͳWS9;Y|+Ree̦ /X</OYMӞ8}Orc-GfC(*% 7ɶ*aI~QeAi4ZPԊQ2!i7jVG0M%ܝ^ <&|>9(.4 NϞ02@C3I;ti@TQ2*Vq$E Ax@˦dF:ۻXfB@MB{TNVS'O?qWDt!R \#ŐDXkiv8{h>|>OC-|jK=%_](gY{"SZ \6eB.B |@s~hpp̌57?,X_! W TYԫʠ䳐d7Z Z#eWO\9-F¼h b|f<ka~uĐ{.sFʤ<8|o;X 6Y S%GIH+!U] )M̛4U/fCⅻ$@(Cr s!Yg.ކ]r@q0f/\}T6̚ѥT"i (톙8Q둨Y02jD;O`]7 ЃJH,W^y b=&fͣhA0 [$f<3Οa #^`n5ƘtrڥKWW?{9{:b11FM:2-(yBS-w`!bkEG=R=!{-dG [B<Z$ad蜽qƻᄏyiBν{{UƐx6˪$Ll\[c$Ġ2ܱ`!]Kj]2DDR]\Oӄ|*L#ѬD {먢xb-쵖w^(GrbٕXy3ܭ$V47oFιݝ٬S(b׺ESzf~vҥ^}M G"h %SsC37TD"ف]ǭ~\;U`2De( KfSQ$+%fU争`S'jUX{|RLzSTI )?&(XvOq"$j /Sh<b6YbƛR2K& EAbRu#UzG1 D} {S)1Q4pz   QNdP3&CE,u؝}/޹s'Pv]cU#3{֎2~SקY/[ 0*Z3B _V.ǨkG _-Zn\U eky+o̠/X᥅!l":H%W&۷oxw_`Mdv"{A  ~k k6fd3:Ҙok!C&u TU &cvHjd^2Xվ٢Y=z6YjC ]H3~cJe]S.V9#oP#jŠ ޽{IBqjs0xIQcjvt۴7`L150XٶB#dk,RTHak>p*ފh jBK)m`0X[#d!uԶN_Y~J!t[trOl@Fd6_t֐^ 'Ss.?3πYJK @3aRd502G? |z%4AV_š[ };}gq,ŵ*OdhwƩg^v_Y}ulE(B\n{b >c}"s Q#!k16uMc 㬱֥Yc31CKPd1η !0o–gdf2!' "TnœٶPR/'bZumrk&Kk@Hgȷg5m6T6)C&ՠB?87nܹ{qiӁYcZMN֘lf}[=ܳм,I{40Q*ŵPS6"+a2rd4gJ8EHW?Xh'5ZJFn hPgp:~ :荇8 d $Ҭ-L/5JY,e[a~vɂ/롰JZ>׽O4)%0ԹtdZ % ^D0LZ>U_RbL7r8/]8~6;R18t+D: [cvwoGcJ1SsZGIRCS=U8 D"+yŰĥ*  IJɮ=ܥ$4/ ;d0p/"G!DB !|>}>ƨClvUR`rُ?~۹'XgQq"Ƹ!kƣ%cg-&R#xMflއЇ.^-c ن "YKm;j۶isM۶mZg#e0s&!1P DavTmItaf60 Rʌ,]2 2 {~ hZG#H$1*̂Terv3)2e<j@QŮ뚦xrppppx6nggǏ0hP.16Ƃp71J㚽ƹ7^ɧ/Wg|E^B m07g.RUR#AOMM kΤjjl6p])ѯ8W aV3RRQEl)ܥ"HL!QOi2W2o T$_& je 9\zL_"KF]H2U˒"=#ӎHemK1źeDI| eYz Z8RzC.D:@( 1֚*$[T dJ#[T-S Ͽ7ruhw=$dE}TX2ưtv?_1ćS"1!o +Ü &ͱvd@2dEBDs#gqڑ3Z E"]C cZszA_sއf>κ4-D`ELJcERK\|cٶq:SzXO $xs:H ҏIRh35fD('p)@;ЁR#;:KWL4XO2&g%ZK?~iz8{YgC6 ۶AeTl!zObeSF5IEeܭt D*ZWb,h!"~_Q=T^%x5XΝGV;5 ` ZjoI'"t^v>O?^hDJg"y~7D IXq)H*BDpJU]ʰ(%:S o :P$=C}>,ދQA6Q;nNjZk5jJf,1ZpzOv;ѱS_Wwq29??~?tSjZ -+Azv}yԹK>:Ǡd94kq328ǧ֤N[C!W1zB H 9xU` FxNT m:CdYc8jFGk#"D26HHh\ /[34MJlRfdht 3"齜v*L.nMsfZFބt1H"4xȓo_p֍ݝxU8MxfU{o=q@W o&%Rr]`BJ\T,kQv v,*&,rrXOrePUU}y|Y7-Ҥ!t!/E3Ljsu(y{G20+/D)+S ,뛤$1Y:pDXt?5JK!֏0~f'V!&#;km5}B ԐםsuYah|٧?ʕ;O:e Ckl1(Rk^$Q$\!1Gh_R%6FE+"2&"pB >{^iEg!(C cL'MX'c"jGmֹ:\ۺh icP$ a;;Nx|g^yQuXƊVN|㏿wo߽yg3 8#B۶b+9TD\S)CC(߿/s^i^#c4zjycukdlیFks8!fX$F2ޚ>Ÿ%]~6C}?뻾ӋBzu=jB+[::-ǜkum\ӴMCdqvyg`- %Ԥ-lEXbegJݟ{ bA bV`Ō9ݰs[ D?r5bj}?E+7n^2kM ᧠wǎ{W777X?%\4N+lv$7nl>ID;;t}0'2SZJ_?c3 (}@૔~aFo¨M( _¼QÒL?ZIAazLzHMVd-(hxHG2UMƪN9l#\]D[0;B,\q.ق/hB]0sv,1s]ϡZT6Yerl~tU;jK *9EƠ|0vtOϞ=s׮^|3ggɴ- <OM:gD4 ,E'Q!"901 +%scC}nMgtz8=<~t4zS+Xj,!k:tֵm{J۞09k]4u1YB 1}u9Fk C ""|fdedcn6puվo++8ybZӌ;R`E P11ԬLN?z'o~{fwwϐzSr2I|l@|" I4ۢQ6S4?Y3D"w *HmLmC:cM*o5#۶1ƸT$"5XВqZ3 g1Dc!({ݻ#&z?EtFA9f919g;C=׸9G96䈈Hpm/w4 `۶]_[q';ۇS/RMVTlcc7=YYѼe/~,Rh@Ja{s0/{+ׅ܉O8䁱KGԵ\vʐ"A<zԶ:JnͿs氃L/=rxy4q"TQVG8r*.a6@i?'Gzk#Gadd  ?SN8q>wsj pmA,@A9 ڞPG- !}}eB뺮O8$ʔ,b]iڱZ`z45ֹ aCk:4>@x|>}NY ^ !adhe S|7^=~+3\p1t12"(D8HcVN~ۿ}wo޼;Nc"cY,ѓ! ŋڮ$ @)},Qd\jˊAdX!pZR!F&q"2dhl\kmۦIS4mGJL,XvL*^9zw}u}>f3}GC:FXFh/5:1h8c4Ykh;:, v>`Z6d|O˗pz8L*0GSj9u/Zk֚#/}㚅ifH+#ˠ؃bڇO \CT#DOϑG0JkZ.Pk ȒnhY–(2utHnR+XgU/}L$f6y_~_kYBԀ Lb*QG%*! 9LL Y1!phX$h 0b\zzűHp<8D~ +3uچ!r5m]fl%kFmFM[]o_6!RM Ow!r}u}=ua! 9ZnV,2kk'5*Q;a6yן~ Ͽ+/߹{׿U'OԀdxqi+9s橧\[]L opcPjݝ|$tUɑiQxG?|Ḩjb /yC>\ᾪ,Zn # g}'n݌!f#u$4 eؾs3g6^vG/2ˌcNDmv*E|ߧ]l6~z0,i:v}Cd5u`Bӓ)4jG]Ykƣds^wwԳώWVH*JIq4ǐV"b|n% K;ZyŗO:ꫯ~'~'}uz켒Gr ݩ"Vъ+U'_Z['U:mD]ݒJ V DHZ5*́Ū@ƶhB̆ ňP%˼j$"T1~`YgHTP&JI)Yھ*Hߤ+ukVMꔎE@h,a@>1QcZ)e,t]RCbC} E(( b5~?lM;ae{l/!R//Y{DUap7?(QJCMau-º]%\w HnC ,/Bc ckyV}<5v UE6h-ob;= p*&./e:W>BLA: 3ЇHӅ^؜*L)I oP )KeKD+|{Nh>]هÛvɧ8ڷ^?˿:}sB]X;divOl:M|w0ͺf}^3 3c5+kYomѨu34 *H$Q!g}L=DC# 0 BE ϝ;vlc7< QPdP`evdFD>٬Nþxeu'>qc_|r֭t} Wcܕ"l{D';߁X1Q"+ :4K9gŒ1@H8;WD((m HIQFD5sV?DO)蹅 . AcHyc& hcӴqsi]ismX۸kp!DC燨XmcևYbׇݬض㧞|'޾{//?Ck\dYQ$ž o3O˗xl2\y-K D k-!eehQt]wއYd(zY]d<Dii#*ƞ}baJ~1-!2&=R$#e`s!s>90c(QXE|ta涵]xɧ_y;~|C?..ѐ0є"_,f ,"`4FئmYbaooオx/?y՝tw>ƴd%}b.Ք[R2HdA "1pT!28k[kLGmآfa bb4ƀ1# Dv!XGƕJ/vL-5 `օi TvPj:N)"BfKMXiїR6;EAf4xԶmM4h4juinԎѪYgȨqvj0F ^Clܨmϯ\_?v 皖BY8!FQ"d4b=ŋi)@^</] #!c9M .e2S*hy)_ztK呭ܒv"ݨCg*OCr2^W*' Q28׏+50BN"JNUCւHJ8de<W+嫩|:9M kNOz2RR U(F2wwd%DΎÕ|Jo *.ٲ죟XP#2M.?qi<f/?1z"!``9#G}:m"R۷~ӟ8_՝;WWV͂@kH%ZhuAg~:p@+UN0ADl]7k BZ(]䐇'i\XCZuQBVmM DD& %cZwɓ'.̝/Xm2AAd AD04M3i5N>vL-fl6b W>Ҳ6ec *Ԛ{!FNV!= *1.s0jSg@ʔ,  v4Ȃ*o<ZeJe ȩTGҶFP($D^k Q(gxJJd+v2_pt ]ODK)㝵MΚQ۶ΙmGmH{{7onݸqّ c`B!Rmk c_zl4i8üs4G?:;w]w6pdFLJxJ,xSb|2V)1HBla %'@`)30,<"ĹA?44fnkJ  ueC`է♎F9i~dy6b6$B fEGйԲ2E!7Sw0iVb3-$8q:; 1k暃_gieL}l_ɏ~:k(` #Fb8h&ġqnz8կ~97|Yd4jmkk۴ֹƐՁg!îf!x+|$ K 8dhq69]`^ICS'3e[T$ mYGCv$,hFMi3Yd<^][8q̩'6X&֪9ݿ X("c!i8bļ2B]yCoAђ%А9j$6bz-ccvFH%Xb-Q;G#h bHZB@@1JAvHYk! G?4(4*}$ TiwNƃbdiQ8dhB"hА}Z?ij֒oK20 a,TZQXe4.uSp2, 2ѤZ1F ŧD$ %T2t D:c5$EP%] [;%HH) EZ)tc)5T5͂9JDv]$q9g.=ae Sa/8c+RxA]`0,$ƘQ;%I)X޷,l _}o>lccG"Pf3AQ!FCh 1ί.]| >xGwb4$Dc}XL;*+b+Z6RQe#fiMt!L)އ "fCӨ`SJIӪW}Dd6.=5mیxMU&&D8X_9+sFZFdfcLdH*z!ܔ ("zR0=9Y cQi!0XP-7g9p!WZ-ҡ@E0r YpƘnJLV+dLb*D#(@Q&"jIXDJ9! |z. ,!4{&-D$r$ Rtpn:[9q7޸xb҉)ZҭwمD` y%R#GAe]dgCx]Bū)wX:`?gCj*h %7Rx'J(Ik{[f̯ܵl5% w2jג:$ߪ3JCpPʈ k$%DJM A9 s`V?+{UhWE|t JuQ<-jbe举}N~ZkY1G13)`mvgw;w/_l뺙u}{m^0'q+c!529eROR]S~md֍F#cEvJLI6# " DLgK;e *]r){M6hf!FwL]='#umc!9auR-0ep"y_2$+ q`䲈Hnh\LY'-m͇4$ZD'"1F2?S'>JhAp /x\c}TGN ;߳bHmZ!c6z:W]ttUo3?$ejqN%IZaIHe +WDHL!D9j ӓ%Pcr]g)f ULQHbB[Tsړ96X=uZL} Cfy,j4Y/oҐ0u2sLa9Bg`J܅*TV !"Ф [z8c͹sO6 Y=sdcmq|gw~uuUhG 4Fo;㏍1>(Bb QRܑ!F4d#5dx@QD%c}H脴~l][pZ5T3Q2ֶIzrUk41@TƄ!c/( jNӷmG#FmFmJkm -}iheߔiy5,92yQ%Qy]LAӪktTxZ~zM+m8>jz^{qdac'$Ld{($~fi5E|Bd0 !AZQ=3_V D%E oA,1H%ւӧW1!(i .VuWl++OCDlblx~#o udd$Gab Ͽx+~Ơ5,1pKF09h MӶm B,Fh.MQɽ:tJﰽP͝5ꆀmۦ,@kZ 4xD2"5@OH'"%Px Mm5ֺk5MPJYkhZ)cgx˩_H9i>{d Q,'-;b_K ER*"ZOI2hk˼RoT0}l7Imːh9*2Ǥ !h&t>N8YI4o,è^\RtSʶdVxPhM UEDQ% R8@Od}}}02#ǏTMJ@8nll{^|w$e^՟šp^:(% N|>JH\H}i&ћ1~%M3fvt uj&2D& Ɣ+JeN=f n@HE{`XSD{U#BJ`(I$D,*ި„\%)yD(T'2Ϩ9& F'Scc-v ﺮizrmmm:NdW6%lT[:okͥ'/=so)HL8l\xyZ֩^Τ'̄0Qzp r! VYfbO➌A޹f< 1F kI"skd i/sV&k]ێs|&`%,x*> ,Y;ow,R9_quut6vqKJy8R*Ze是_k.JUjɥLM^g* G}IzB昌xS  9.!p0ꓻSB='S>ϔ\`ެ.%2)3E ՜I(sGfF!Hp~+}@ԡ_)T\)eDaJ7C#ܽwΎ_sdz wvwwNK|r[Kбdz>wfePVl(DT?5A 5`|fxiuiLu]n"~x'ce 6@ B1i1zäχ% ԓ%7<&1e:clh2_Xbx1l*HRLI.Br(Sdv FXZӶhԞ=9O{k?/c9D5`ɭ~}v>k97[o~qιwpIXffQ"ń1sM2Q(Dd$8- نPd+@)hZZԐ(dJ*qFiڶm7ئq9Ϸm@.sln .Q45ʶJZ4CEqTJF\tXlGҜMRDiD o+$wJ"4Tofn{IbI1B)އ%VQ >Ul!,'9YZ#|VK\_䔙/,!&/&dXsFf1T D昺Y:?wf޽2:huaF4ބ鲆胟uӃ~3W>/hߺ|=]V<~ |ƌǓ^~|l6,р5s%1 1q Q"ag  4d!c&J1HLSHK56M㬳g]ӶUi!XOҺ*cT}PtMT+l#/rYn=Dzl*\/C bǜoo}9-4F[jyV:Y+j:LAΑ@̥H9ڤ0:%3{af'sT +G%B.>FըU-iή, i >e}]PGt UD" Ő־IeaA )/jkF5#z!oAJ9a>{ܫsfh4F!KSBֽ3Clvw0_z?_^eQ ICmclY4A3а֤ݏ:,e<١~ZW®TBZmkTRdZ0R5N3p1Liό43Y*#KzF.CWJ#HH rDž#[z_Y=XsdvZ_<#TZ"a*P){$$"\b5L;TD !|d6mvLQ?uVCe5%̀ȑ%np U, R L?]bΛ}^cTC߮{C}kTpFde/78a*Ycd2:.>Gr~a6e^aN.İ{]v|it2=$\R #(jck4Ӻo>@Uj(r+Yaz[v!KVkJ%f@,?Yc9(18Qriqq9}z m*IHa'I6iTW^~ XuȚ>0MӾ;;;;]% j2'10dHu=s*sUPHU`ץ6d#*6YuoȧYrL jQ/V&9SbfʔC)i=YpʣɭwUSmZw.fu8$^(PG5r:F@q.2*&*T 1DGP|gJ*@$@냬~.⩪D7jTBZ]55m"Fc~sNbɨG՗g\AeUu٬>Cuu]O8/;w`:=aX@a㦍hNvQj{lnJO5U:2gyZ ~{!,>fR}T6q/H,[P̀^Fp|`- o͇4XrҲrϻ]fBBV,f^AuU%Ӕ.8BT~YvsT~n(D9??: }Aw#"!!b!( vwacV+Ƶ ^61{~{vYDٳs<33<;Wq>dUu_b,|$T5:ڮ- } DrR7%mAU P&c̡m&cANbe2L,HR1*1\dUDXyB brlj W+UE-Kz *Bolu??aCR5fATݑw6uBD_b%!f YU"+$yMc|wvk 禚G8GpmlzMaMfqYT2C iT+`imWkclǟFBkO&D"^o(چB^0MŲ;Xra_[ P|$2Th$ bQ) BhE(ڿQph7~XgtxQkޱAVO○'خ9H-泏 wntJ '1>qcꦪ¡a:1UUAKp:xujN(XTDP#KmߩԹ .:lnTO#Q0=d $a/hhaEB]?(x B?&jK#ؤ"O `czbz͂]F?Wo{W:D:_7VbPRTXiRڷUۭZӋn}4 6RfQ{Pj>U!":⫂ԚO}?> q&P(y9^O?6+Ƶu:_vTw@**4Go,[a:wxHlH*;xj;0>DG@=J<3UU(4A%)A-&И D-{jؤK :XV/Բc.?f (͍ TZ"VU+ʠYQlL:Uoj\4TZ_s)T \{> ;=(8Eu jjaMjOT h D[k9 kժͼaUԿE#?zB_aߕJNCP@;@ ,,P@@ ,,P@  ,PP@  ,PP@ ,,P@@ ,@+KT|>@P('NM4ٺuڔj[v)۶m300 \S|+EO<Նk_chhxq O5 KG:ԜG$Qmա!&Ml߾S 펕a97Vyj򩪪_/-\"|.A9HSjGs+׫w4?/hz6P;vꪶ{ګ-}vU˳c-G-wf\?ֆU;w~:qD6mlllcc>l###):::u^9Dm>MQͧRVmJ_cmm3ק4D$l@=ڲX,CCCbhhXg ;R)5_ܫʣ5VSԖGkkk-KH\Ԇi}yTNm |>ڴjOo b`RP?^ wBJmmljmhh/mŏqȈJ1>X(X` (X(X` (X``  (X``  (X` (X(X` (X&r׮]T*T^^>p@SNAmgR(l?E&Y,FA sqqi{{;wXϯ7dŊ-[,**+tuu ?)JT֪*=== ~Gu,`0gnn+J&~[uۆL&A@,)((8}˗:wñٳgdd!?|իWyyy|>d:;;9Gׯ_xodr&MwC~~P(quu4hPppemTWWvAVV޽[^^nccӳgh[[[(X+W΅G}+z]P(>|`0rE 0!^rGL)c0eP(ߧOaÆXb욗/_Κ5*&&ٹĉ7oޜ4iR\\G> B]]].#cǎCh(K.3fƏoff_p#GUWWT*Ν;7n{ݻ*ɓͻw?~p 6و׆իMLLT[D"={vt:=66:33ѣu+(gСAAAGetyTTThouuuHHH=9stر@zT:hР~ O%%%ǎ'ǫuyy׀B!(Ho޼qrrD pWWDnժUJJJ7nϢEzq9~SNF,// nXm̙3'00Hmͼ~y„ Dӽ{J%hVuuԩS/111Νî9tݻw/ajժUvҐ^.]8NiiF(L&Sdcc[l,N/XѣGe(H֮]ۤI[X,¯j߾}͚5[~=Ww^BPg9̙# Uw-Z$ oݺUgZڱc΂ 4RBBu$Ɇ ׭[O755)..NOOO RU*b3gGoѬY3H2226mjoo0'O҂gggkkڮ/((ܹj }}>h.L3!]GG:##C'ttt(++P鎎 BkN/H^~ ]'@,}};wZXXҩT*'?|0|pyCBQQQf% P(N>ɓ dllL_&ꚚX­T**..֠x< E,wI{yrr\t333fMxxxVUUnՆH$j@m੬/^ W|`H$:֩p( BdJ59)%%erfbL]eHRTd2r9rƦVTT( _-eeeJ6Fi0)a0 =&) |XsssX,H4RPx.H&n˺j*XaXr_~&L|D"9rCL&DR*'O\j#d29HQ VZmذE^J555AD"_r\.'PN} b*!:B! 0HҨXrbRݻ:v@ XbEӦMvk< JR#fٺ999 PԌlׯ_5\LRRRG\k>--MOOO>WڠR]fllb>~ |iee+y<J3O###X,JjggWQQ!~&&&%%%555577OII`rrr233믿?z^~AWWWggg4o111122Ҳ6$111133,4yd:>sLUo!U"ܿp<==}v xgΜ!ˑ` 4lӦMqРat)##̙3Gʪu֟>}y&m۶̞=q[L&UkΞ=~av!  OOϪ*cbbsiU+/>j:pG5\ִi߿߻wݻ2FD?{]v2{{'Oܸq#''.((xO"""Gy.p8ׯ_OJJڵ+'##ҥK)))J㥥ݸqݻwXvZl&).^Խ{w ܩSߟ={ٳgJJ>][nt:D"?y_~522>|XfMrrrvΝwG100 'HϟD111Xݻ o߾]YYd2_~bŊ/ܹxuҥK͚5CZѣGj[[no޼pϑ2WUUvя?i"O!N2L@ eÆ nnn.]ںu+ϗdL&MѣK.I$*`0 <8u *jllܪU+X6l0wO0A,S(&i``H%ɰW橲pB /n߾rabb+JT{Y|'OV\lU˱c-ZŋcNJbeee5l0RTI 5 >EPPÇ2f[ZZe es΍ T8pj`88jժGXB(* ejjkDWҐ@SRY9H$R(4‚pxM.WWWd2 `0lʲ2tN__ʊ XTTTUU%JQ>:::##&Mh(vuu5éQ(t:̌FJ%r\.yܳlCCC5JRajjOr~`5 %}Y~}aaaUU\.g2&&&joG"x<^qq !șQgTTTQ( b4`cxxx,P/(P`}TUU8`Q*  " ,PP@  ,PP@ ,,P@@ ,.hQ(iiirp8`2@$d+++2édL&JGGe2sQRU#) >}Tjmm?-Zeee$_աj񊋋r9Ͷ?UUU-Zs\zzL&8Q*T*_PMMMʹiӺw ߅ ]vҥ RSSdBh֬ٹs \{=~xyy9ŪڱcGhh(ŋlRZZ+Ξ=ۖvӦMd2yժUgJJرc>իkҥst(#BXr|>N(MMMUSS3cƌ.]4@œ'O7U DPPTeddd``?RRR?ɓH$REEo]eggݻwذa3f@M&sAdYYYVVVo޼i޼ W?M6qD"JeffJR777աb5d2-$I?<2TڳgOtt4a{,YN"~|<ჅL&BiؤT*bd7o4mTFVF' @ `ٖ;v\pᯑ_IYYYzz3vE"2?z5C߼QQQݻw߸q#D:tk ֑#G\]]Gmx6mڨ*X ;v,F;y$Zn#ٳgvE"e0ݴi&PZZ:qDP_奥ׯ A)G9~xqqΝ;a$}}}۷yI-FFFxo9;5LCCBHOBWWݝFT#^ZZZTTTT;8 BP` Nںuc޼yS( \nddTJ79XTbeeuiB\.MLV>~ f-7oMF<&Bz3nR+#OOO[[GW\.))_aoo_PP ;wFGGhbСO}R;wܢE-Zz.]444rrr2339?>W\U[c+KLL ԩׯ_ծ.\ܤI^VVӲeK776mڬZ㡟n߾ꚗG*66v֭[{xxڵkUVǏ'LW-[>>J{'jժmzxx]~ԩSϞ=@@_xqر<LNN Ln߾=}@OOOÇcݻ=<ړH$.]ѣGǎ7o\UUY6>|0rH///oo)Sŋ#J]x1JG&&?;v@~B… uݲe=z`ƛqơyxx16JT޽{7<<ãM6K,! >|>|8nݺmٲꔔ8___oonݺmݺU5LֻwaÆ߽{W&LؼysΝQG޾};aC9;wW^-Z ܹs'tttQCO2})\ׯ_^H$>إK//ݻ={VՃ0%%e>>>~~~ӦM#˗/{n:U_`m92ul:n``@nP(Xbnnnbb̙3Ν`0$IRRѣvZ]:t0j(&Y]]bŊ#F|ɞL&>}ׯ_W^mee%޾}yƍ2 MHNV^]UUa쮬D/eee˖-Z|իWߏI޽{VZeddDP^|aÆ#GHQF}M0ɓyY#F L... 6lYf͝;w_5lʕVVVYYY/~Ybbdd2ٳg >}Z`AT C__Rw޽}v;;;&=:tj ײe.]3|*ZXX)d2($$$&&{{]v ;&۷o700x-AQ?]p~J菋-JIIAD"UUU޽k׮'OfXJ޽{ ,xŋL_d={ٳ۷oj+ѣ[n2eJPkӦnVVVfͰ_xajj@,ϭZBGtuu|RRRiiMXfffn޼ԩS'N|T:s/^q|ʕwܹzjmq!C̝;WP?~|Ȑ!;vh׮&]t7oaēƍׯ0866V&-_:;;{ڵ !h߾}ׯW*EEEXo߾= ۣ7o>xܹsWZ% ׬Y3lذ'O( 4yŋ&L|XxkYYŋ>|o>.555˖-EUC? d2_|ٺuk䄆_RT*WXaiit`s gϞP(N81t۷cŋܵk>|PDR[_(((hӦ _|MJJB_므R5h8S*~~~yyyk-ZԥK- P(ZhqEsӦM{8p`mׯo۶-V,//l ()) 1iҤǏ ٳg^XǎKJJ4؅ :wr5Wٳ,XPgedd4oӧO-Z 77׷mhhQ|kyyy.]Μ9CQN.\> ޽{[pMiiiǎ\r\77tb92::r]'''Bi?SCy [jvZ,eС111kry^D/߿MJxn4t`m۶[ׯҥKUU>wދ/`Æ ۺuj̙3 oݺ#G-[tI.c)^^^wU{ϟh+WUTTΝڵ+ӧOwڵ}}# D$ޘ@6I@O9NVBD=ڵkk6l݈M2EU֯_z \1vX7n)C w.]$''3gHԯ_brMrfvڵ ׿TW|i & RQQ4u:GQ__۷oO<9!!Kwwoʳj*) N:p@|W{޸q#㓑Ańƺ>},_4k,۴iSPPm.]-Ϯ]zOYzuVH^}5kVhh@ .xgJJRo0 YTcǎm۶-v^a\$iذaEEEhDY۶m?|0ĉ9sT*~i^'vvvld#pB۶m1cR+//G{=%%%hx%%%Y̬߿?dȐ_|> [y̹mڴ X>}jggz.7$$$11{Ç =(bZ+:!122%[ҢE ]]Ǐw޽klmmݻkeeձcGdFF!;;R숇Γ'Oj!xAjz J500@12k:.J| y4Jg:)//xb`d2t*!" D шQ-FFFh^|ٴiS#8id2yȑK,4i-^!F;>?~D~ o633;w.E"ɓ֭[+YryJJX,ۘ1c9_߿D<@lLQ)>_g/V˃mmm =zشi7TWWh"558QƊ \NRΟ?_>?~ƍwttvqСgϞŜ MLLŋ<_r@u_\$ILL̅ 8r(((JT>ǫ+&  [fȨWSTCCʿAC,D2f̘;*  KRi4Zm233322X,xd21KJJ:<~^Y 2d߾}C;ȁy`H$UՄ`XjkGY&??9EhaaaooY`UUU$$$_X_`D+V:Lff&bX `TL&f#L&Y [nmݺŋjOSYB&) hzzzځ֯_iӦZsV믿LLLd2YMM Ӱޡ'˱٢^W5KKK,=zpvvްa18q"jfffggwE`]|9 ‚`?>..M`eed&&&T4 Rۃp83m4U_X,UN6sT*^3A5k&JTj&Mn:x`GG興tKRRRbb5k=x'D͛Zt]DT?xeB9ݺu )X/_F͛~ɉD"]z_~"A&(Uk5k9ڪ.E,Riggnذ!,,lѢE+W ;A ҢE#u]|}}}ͣRG בSNz=VX5***ի _G̙3?ޤIPkwL4=(:::FFF/^>a<<<ܹEUʕ+g͚suݺukΝȹfʕۼy-[?e9;;:thӦMlMǎch R2,>>>((Hy~cbbr]d4hйsf˗݋蚚cǎiX7|gJJ hff&uHN>}*ɖ.]iW$8ŋϟ?4h6lثWPHWb󄞞ѫWT8.RgK.Κ5 /ye܈# R[z"_dtN3L|`0 drxD"Yl>Nuj6x<F?Wyyk:͐]]]===--ZtĉP .]t޽? }y=p8عo-rh)c``@@Z <4''"\6mӹ\ӧOuuu6mupCCC͛E{ DСٳoooHD)J%5d?w"GڮNNNb wtttvv޶m[JJ Jż8 |}}?~L7ٳB˫k`Q݀ T*5<<СC{cUڵm۶!0R%_~MȶOzIH Q(gϞ^fvQշ9boo_wܩ S+?pM6U}GΝ;}||T]I~kk8H$KKKwww#2*M1Qlmmutt>|uҥ;! |8B$;w܀WͰXaÆ={l˖-l!-[&ǎL&gܹIRQݽ{j O>}rssw؁TaÆݿ]ZZZܹ(%///::=vٸqRRR aN?AAAVBcJ5k>|xҥPRRdF.]/3J޽{ʕ+P(\p+W֮],++ 9s7n\jo|>р~MOO߹sg@@7ٳ^fTUUU3fx%~SQ zd{i+W|Ք)S֭[]ohh`0PףR߿dlɤA?֩׎5СCVkh˗/c᯲m۶SLٻw˗I-֯_j:t{]v:thرcbað$r-OtwΜ9c/_lJS)N:tRԝ̙ի˗={}J70K.M6-<<?ZXXE'O9o޼jZcZ*+ͨ6mڴe˖cǎlmjjJpn֬ý{TN8f9r2tggSN᣼,\{E| oxGTM N8e˖4MP"^z?~<)) 5N ?8pE:uDѐNJfo۶->>Y@.]MggϞ]n*ѣ1r %%%,K"0  L]vmذɓhlgguVv%BxСCL&;v(~q?wѱcM4AA/^5{UV͞=Ei4ھ}ƎSSSd2bIͱWA]XWWܹs&MZ`ĉY,\.oҤ P͓ӧĉ;vD$HJ־?i m:rHCCý{"@`nnE"-Z,RjxGM6b1~I$Ç ApP:KKˤ 6d\nee5k,okk%>3 \nhhثW/4ĉ3gܴiLD"ӛ7o $Hm۶MHHHLLAo֬ٙ3gtrZO!C tbhhu-??˗/S(/N6m֬Yhcرch0<|rrX,fX"zĈd~…gϞjjj͛">P(8իב#G?~}GR“'O2:wss;r䈩)s=|PêOɓ?H$ggfee%%%eff6mVSSckk;p-[f~[n3L֭[wԉd_###7oܴiS_nܸqͪ* __߁֦ }%%%#GWѣ/_JSLQ;i#9ݻzjeeرc ŋ  `ffָEaaCNN:EEEiȊN:u s[z5HtgϞq\ ~߿T\\lgg7r&MhT*~/;w,رc_|100ڵk>}<߿sOFEEyzzf~/_: 2D_Jvvv ?~咒33͛GFFE%))ÇRk ϟ=z^YYfP_wF{^ d2Ç߼y`u.4Hruu>}d``aL_u/_BssAD"߾}K"lmmڷoɾ}b?Vokyy9 ZB099eeeVVV{Pۑ#GrrrLLLG0FuDIIniiٿmjx޾}Bxxx=/cٳg޽D&&&;wׯj>999ONOOرc֭N./ZHGGЦM^za)|>}^$ݻ:::=zѣ7|?,**s]׀( WWWu$qر;wH{uY0 trIR)w! N:5{wIR6I&[]vgΜp8rYYY Oiiiyyy삂Y,2d555ǎL`Dcƌٶm۷oߨT*Bym^N:E˗5ӧOL&ST~uŇn#"">~H /^~X[NLL$kNjnn#^޿qq1NۨQ-[FpSݳgϨQP~)$$ܹs _\\sl&0rHTJ_w~]*ZRRrʨ* .;|>_WWp…G޽ Ƈ3B7ԩөS( ~s&Lݻw?s B166NMM=z(^Z/^}bbb j b-ZDH:ujBBswwbj,3g.X} {^r%vT*]vm 6mڼ~Z&av&ݻ.H 0a,e֬Y111XJFFFΝ'LyӦM#d`0uT*߾}ۺuc)k׮3fjᲲ25G(wժUX ߿hhhff&VΏ=.3Z%{zz`1SNXJyyy.]?o/_`-;rݻgggc`JСC[~&kٲe~~>{N*~͛X>oӦ 2q&9-ر#:~$dΜ9\.#99s@rrrTmժÇQgܹX-7zJRXX}rqqz*#x{{8q=za$%%uԩK|ӧJܹ3]v?>:g?88$$$[pk|> †5>`:`#UYYYN.\6lM`„ jUVWƞr]vUfIp8-[ܷoFgΜdxzzb9|p߲-[n޼+BLӅ <==5F 88رcWHo>,q̙3f7Lv OOOw޽͛_e?~ w¨֭͛[SE:+˗]]]WvcǏڶmӧXJiii@@U07nĮѣGBB6dW^mvʟOF P*zzzٳsssѾDzwڅnXYYb{C(B 611Q= NTGGK칏wӡhp8߾}C)EEEX>};wodXazf.^]zaÆbhbԨQ~UBi``fdͮ9^|d25{W`{1#F w3ZaZ∈|"pqqIIIAۜ9sԩS}I䐐JrS O۶m19p8ÇWQmhzz:gAAA׮] C5wЄ+lݻWTOOO]]]7,,ǏvR{ÔoeeEH6l~Vĉ?md[jjj޿NX6mcsJH$铿?],88Px fr~yjRd-4o3ڸ{{jÇ/++|w¨~=1׷0ֆC TK.uJ`c}Rٳ0;; իKbcc UUU QRR ΏSzzzأJP 4ҤRT*P_zzz2ۨM6]jզM|}}njӿn`kmmm}066JkL] P"^,..>t͛7Q[{{{F 9ARK7OfgghIBM`qȑӧ8000p„ ͛7o3z{1cƨQgΜ٢E l߰~]/fTaaa5Y\Rjjj|~UkX__V;T*FCrw^mZR۲ B&g֭yyy/_;v_֯jرc|ľaQDd2 8sww_f a&S*j:thhhW6nܸm۶ӧ4^eSINId1bA\ Z1?&,ZHǯY=j;R,U'D ( \H8+.6\reܹG|iҤɀfΜpΞ=kkkmKӧwޝP ¢N5+6MήQ\`0QN._pN:uرa:G/}m|!a}F__&k5X5ׅB!~9kbb<==?}TYYwg eTwdJc^*||EЦV7ot &L8ȑ#u vvvUUUm~ҤIcǎ1bѣ=ZIKL&ŒH$jX,==o߾u. |{ffQȀzF={ϟjggWSS#Ԛ< <߾}S7$PYY)~6VuP]OT3666 BPj?9rYX\/b~[nٳ>>>$Hm۶yyy/^߿?fKԾ*dFzNy<\B133+..V> ƍ jx܍sAA!̕*C. 談G<$4x~0ac4j111EEEG!Ll׮ew711jݻwnm$ܼ6hU,,,zUTTT[ bHH!qkkk)E755( ǎv5idҤIQZK?;퉈|)GODNyyy 6O>ӧO#FxUzzGa2d3f 6d_Θ1# `4ݷo_Ν2_~iuyj_m 4V{yyy󦸸o{_```rC͘1Ǐ7n>|xIIImy:::.\0++ Jа{>>6kҤIaxן6mڧOF댌Ǐ1"==cǎ}pTwxYXfѹo&$$hxӧ={6)) Boߞ={6h?h4o߾}&M&Oe177vH Ӧ CBB=z 5f̘׳gb;ӦMСÏ1c ҥK>|>WEE~8//۵kwc[A$-^X,:M0L&~ (6-[br. QfϞpv܉B˗h^xJ䌌 LNZPPgRUUUEcBmL6hǎ6ju]bKBݻ?>>|X޽{:mڴ7o8p%.YҥKxݨi(Tbb"Mh willnaaa7nܸrJΝϡC*xb&&|_ׯ_y0)HBBBsccc_'acǎ#G4I&FFFhލ#A:w3m4www333LVYYֳgOlv1kkkccc.G\/zo߾ &;;cj6ۯ_~6664M m޼T*}Qtt! ix„ W~葥%B266ӢE CRԬ1c756|v>pe2YIIIII /EFF:;;޽;&&F"4o<<<mcǎ1cDEE999Œ;vϞ=JN:‚B+Ç#]]ǣLMM%Iee@ ޽{۷oBvر"2qƹs> o VzzСC]\\- كF5k֤6mp<߿GC)6qDlB@~9˖-kp={vxx!CB!ݽ{+3fiӦa5C ֭[oڴi֬YOA=Ns0F-Mǎ1bD߾}4i-[6ly< C[nΜ9qqq >|ذazzzv1bDDDz.(((**ڻw/~aCΝ;w5[[[UVV3g:e<8~MRԼ9s=ztĈkҤB\.mZ$޽{ĉ>+))ٵkvL(++sⴷ߳go߾mccc``$j ( r՛7or8z4sɓ'hܼ*---88xʧM6 ,XbŅ ,--o߾xB5/^fkkkddTUUUTTDP։ObbI^jgg_]]pB)ܼk׮@ڵk޽4iaÆSyDeeeff;zܺuP"gff:X>ϟ?3f %޸ 5!!ԩ}aa!}I,{.00]Ebx<^EEQ~t颶rZh! y<zS1;"鿱<==\.e١aaah[OO/::bȢ:tصkZЛϟ߮]H~ݻ5mdAРAR{ٳgOtD":ֻw[1d4UUUVVVQQQXA c@ҫW/վ`gg١ANNN͚5C2iddD%ʒdA@͚5k3f̈W2~L&jժO>SN5j^d|>ǫqpp2eJm^Y *--Z?~66VPp8J]]ݖ-[ksppx 0Ns\&8..5|><&&&00t\~~~+W"i!>|=‹gQJjqssۿcu?NAAA޽<h# hD8̙ӈd@ݻ5]eӦM_&Q77\.oAūW611o o D==JRkˉߊ_h,޽KXРrC>ommvsss7o\ialllccp***e3m4@$"777nٔ Ÿ],@GGG\^^^plv.]mۦ}JҊ ={*o&Md2)--eX[lW@5D6m4 t=ϟ?ÿTmՍ͙,F|@ ,,P@@K32 N r,ZYW! ˝7oWkצM G;J5"hƍ3f̆ ~t֭o߾H$RqqUMy<^|||QQяgb ׸%\bE^^ޏSYY9f̘ϟc);vسg?R>|8o޼#F̚5^#Lrr#GbWVV.]Q+X… `JKKsss4!k[nd2`ҰP$M0˗/YYYUwرe˖'On۶ĉƾxB6z233ծ###, |xFֿ4.;trڵ#G2 u!233ΩRLMM-..TT~~;wsهyN233eggK>sNrrן;w_cܾ}WBǏxcOE(^L"|A$KìeK.0CPT\\\6o燥p\[[[ l+? *]9Ǐ f֭[bb"^ըWRRRbL& A?マ?lݺ5 `UVVr\XL&uuumllvTBQYYY]]]SS#tj>bD(RT##c ŕd2B꡸X"X,KKK6M+++E"L&j3ꡨɉNYYY K,++xJROOL&1>=--}J|>_*Y0Bi֬~dP(YYYW}}}{{{B! >ˬRP(˙L5E"Qee@ Jd2fب+..R*@iiieeB`0 ɊJU+u|>J(d2 0Ho߾蠯NNN:,**P(&d|>FT2 333^ZZ:Kӛ5kV)痖"Y@H0QH$"DOOU.$憆jAUR(hLGGTWW'é `RH(R(333 #~VV$dw6mmmO勃.pttwUB #V`P~RjK7͚5cXsUU@ REEE|>Fٵ+**d2qLTr8*DBѐt===-XB)J.[YY)H(>BaiiiMM FcfffxASNDH`0 =N&IT d2 z\$uuu6m.#J )h3 :::666 fff! ڌ %''GOOcaJի € (X2}W^lLFR۴iyfl@ٵkŋPDVVV'Ng?s̴4B!ɌW҆ 6g5k֬GM__?,,lܹݿ͚5l6[,lٲS2GrE"P(411߰aZ˗/b`ر 4iJYl٭[) ޽{7OWUU5jԨI& <ȑ#g̘N"x<ތ3RSSE"۷߻w*]pa>}P Jݺu+T }K.E?]ʕ+|>JR:lذ?IIIht:](m޼ԩS333+**lvMM trr:t萾>>3g>h4ٮ ˗.]zrDya${ӦM\.Jfff999cƌxը} ڶma-Zq d ,xT*U({ `wO"N:_~ӧOG_=/_NLLe0 YfV*D"]vm۶m\H$ׯW\-i4H$ݼy3B!}Zp<f#sqqٿ?ւR(F>}[nMJJRL&}[n%y}UM:tǏ+SL۷d2Y,Č1{Qԍ7~tRvZ;.'HϞ=K"zꕜKqƥD"]]e˖aϲ{n4#:22rwѢEիWcWHOO;wnFFBA=nф,>>?_Xbzȑ3g aYnjG=~Jd2{{%Ko^G`2+V+:b\]]lbee cƌ7nСCkATdT]vNP(:tdɒfѣGO2%""D" 2ߏ= FUWW`MBY>}}~𡟟KlJBXbϟk*++ R޾};l0WWWdPСC}E ֬YP(Ə߹sgϞ+Wx{{ڵ &==UV2LTVUU92 @P RSS}||RRR\~sC *:tpq|J~~~˖-*ӧm699={Jeyyy.]N>MȹSNLR,-- WTTDDD JÇݻGFFW*r|ȑ;v믿PJqqŋn޼Y}/^ܮ]?Ky ;w ѣGSSS޼yܸqe˖;vRCCCRݗ,YұcǏΝfСZ8qbvQL& ={v]˗K,JM6=-- AAAWP*iii-[9rd^^kLLLTT^~RM0cǎW۷o$Hܹs;u7:++ w%O]P\\^zٱcǂyq)yyy;v߿?&Jݻ:up8XA?]vM69gee:u {7nmVCڵG2s…m޺uK.}yyy]~YTTT~ݲe۷oksxx'OQFyyya T*CCCmۆ}ERT*>~>t&O/婩دǎzaFL:򧧧O2 K& $'ظŋ;Μ98q"BXrÇ߿=ѣGs2dHff&6sM> Um3K\\~f!ҬY3l SXXؼy/_`ضm۸8lV***P ֭۩S5肂֭[?3T׮]>`|||Ν1 _ECI&=zN Zd2yѢE...O.Xpݻդ߲e˴`訨(F{޽]v(wޑ.\4\K.k֬A}}uQ(mxo=8eׯ߅ {^555huu֭UVa6$Gfff_ՃچٟdcYYٚ5k6m4h ҦM-[>|>Xa Jo={vFFFAA}͛CCCQъ\M>}zҥ]v BخҥKi4ڮ],qqq˗/_n> J577Ǿ̟??11PժNO}JafffggUqrrwZ۷Ǐ\.r=0իW---7$#III͛<{L<<< xXE]ziӦ*`mw9uꔣ#1wЁhiiyIM֭drN>O DHRJ'00S_xQYYK1bDfffII|rƌiii xܡCzdXr}UϞ= ƍǬǏk.!AݻwfffYY-(hB56`KKK333  ҪMN𔔔dff:H*7y_ ---Fl`X,СC=jh433_]VSNy嚛9Y[[c_ TVVjժD"n׮]BBϟ?eH,rf/X#ٶJR#jĉժ'N۷oc߾}-4mرc|;?d2><|֭[ UV N ͮ@SRRT+"W\\lllYF k۶-!]__$%%ECt:fk*`DPh8ieeU@o߾533SuFnӦ ǏyHIIINNF-H`ڵ#G 6mޱÇl6{xUJ2ROOϊ PcBGGǏb;wݾ}M6$m4Ճ>`JVVVTTK&M*++=zT@ r\&5L\.ѹqFTTPYYiaaAX/!sUUFsvv&ͭ!P*J [hq֭q1 Mnl}TWWX,@Adl4a„w޹+;wOlڟfB A^RTy^D 5_Nh87779}h4UR?nذĉnnnM4/_DgooAUҥK>|ܹSNL4 UUbVh#B̙3KKK?~HRMLL3}tY7===ls2d ɭ!zՕd?%/^?~}YYٷoԎo?:W[ǯMN5kv7^Cap4899 DfԲRTUJ ΞT*%nj?Ʌ =z6lN5 VBB1sλw J%>tP|||LLE)!G@Tj&MԧO¦M&''cj0'* BRY;͙3G˾gϞÇկNc:a0t:RDOI$t,^_mwӛ?~L/Q}[KQQFS{DpǏO8sN̟D"<̙3d7oD"6V;v$߿?|fLLLdwΜ96V@|~Ak N6UmL4)&&fԩܑؖ#GDG~&YVVZJ&Y\\E"@⚚:xy@3ĉ2C^^rœd2lB:.C *66v߿3g/_/T͛HfY}ccc ;w4k׮ 2dWSSÇիWfffMl͚5E7o\;wCտ\~k׮BGG'--6mڔhB7o]Iwҥښ0!5o޼L}TKQ0@cI::: 뛚"?X‚갰0B,zenhhhnn~]jJzss7oKJJ:w\6Yhfy100055vO7o411ؓ=`c (j`Xl6a3 77ݻwggg}(XjLA>}d?"wR}ܲ|rwN"֯_ rJͽHP\&|ÇAi(޴iN<wڕd"%''zjԨQ6kݻw5`£`pUSS9 AаܵkWB2L8xuЯ_ϟn*41Tqrr*//W=E8nܸ,™`nn^RRyBurr Xh>ɓ>>>j#UYjիW-Z˸r ;7~4ڦDzŋ5\?}+W:^z}0LLCKÇWX9::2ĉb~~~?{ϟ?͜9Qy%KRƎۮ];*ٳM6pjxb?gmm=nܸ;v¨(&yΝ}==7ziӦ2>_Z)--rJ޽ ]Tsen߾}̙Æ 0ax^~mkkۭ[7G5cƌ1c HDPUZ++iӦmذaȑFP(eee|.]v>1߉144{MTTٳE"Ν;߿o߾zaǎΝ;o<nݺ9::.X`ƍ_|:tX,~=Сƍ۵kWHHL&ӳg XYYeggYf.OnݦMv-[2LTjkko]v7nHQ((|TBlٲ6m8;;S-[XZZc'O^nѣ,--Rׯr9NSN .LIIݻ7?~yoҤܹs0($)""bݺu=zh&SLYxNJJ ׀0447oLII122"~=ydܸq#Fٳ'Ū~U۷od2!Hݻk׮S֫zzzfffGqssC'<6lѢE GL1cƼyZlIRJeڹaÆs~~~d2ӧOdȐ!&&&&M,'/>>Ņ]V 4ѐ!CO٪U+@p) usE䑑}4iR|ݻ4if4vrΝvڡ.\? ?$4gΜA }}}/\X^^~3f0L3XV>WXѪU+4l۶‚Z\۹s'zaرc D">T*%l[ NNNӧOm&Hn߾=""y̘1$i֬Y!!!0a.o۶- D"=zˇA/ f2K,INNޱcGMMuzQAll%KT{۶m{XTsN,Xm?~Ν'Nl6߅P:U...k֬Yz5Ù5kPNPHǗ3 ???,ڸ]Μ9}#GVVVeEBbddd˖-=:k֬ aiiٯ_?aÆhb޽Ǐ!!!Le˖[n۷oժUzzzAAAϟoҤ WUU=~ի%kΜ9(=..e˖0aBUU0!5@jt[;te˖M6m߾]OO[nj SN?~|eeeC 2d~SGG +ɴ6mfe0gϞݽ{ٳQdg ???p6-FPر#//O&YZZђHA?~|ܹ( 7궘nݺ/^۷ɓbXGG)..Nö ]~=t{7njCco``sUVEGG+Jooo \^Ą/0xȑիWGEEeF"NB+5kѣOfff;wV}-ƼySYY[?w1c͛׺ukHm۶{{{ ?xb777 RYYgϞu˧Obbb:ut1v[HcJ={/_BKP֓-[^vMO͛7?p_9뛚O;wn۶mIII;wrk6nZ\\\ݻ}}}o߾+EӳhooX,xbv7oO6mL>}4>QP࿎;qӦMLP3M>}(I$B999=Pok֬Y۶m۴iSAA᧏?ZXX[nt333++{i]FFT*%H>444,((#77fgddH$[[ۏ7<_L} zSNhbɒ%͚5#HbX"D""3 U]]EEENs8 aL&D"m۶]re޼y;w^d J7Udr˖-޽{}={X"22JRTH64Ft\.l *l5\f#"""""N>}sB{ hx,??9211FWTTbՋuuu^zEHrnP( r˵)/\`cc `!ڶm+jjjH$ҠA|zGNQϟ?/**:t(T* yw%YYQGe>{W\U{_=|y5ל8q{G$_~%3/-- D"H!$~>~JWlٲe˖-gޟ=7|x"H$D]PH$D"d%"eH$D"|1Xu>2*D"H$ H$D"HD"H$+D"H$ H$D"("H$D"Q`E"H$DD"H$V$D"H$ H$D"("H$DD"H$DD"H$V$D"HXH$D"Q`E"H$DD"H$+D"H$ H$D"HXH$D"Q`E"H$DD"H$+D"H$ H$D"("H$D"Q`E"H$;bwp* L~A<3jw;S˼0Ӽߴ2z=kG^H$|,˷V_1iG7F ]H$O+]+a9nL5O8Ev[ws$iB@k?y/Ӆ~?˟8L&ʮ?m;翹? 5t=y|ox#9KhS &i'r_? aĵ?1_s ]??Tm;k+zV>wr9Rsxj?0@'bZ'.?w;>{{{Jd}k>?䕏+ HPdvԏqLxIjEUTWo?n}vv-8tW~RuNO1!l߭?&qey=duïF`e y77=}{ @A>oۯ_Vs6Mg[֪^93Wg"L<_3+gRMO}@Pk_PT-r;ub?5\ɿ=Au _]n9; 4?W<5?+wגʼS^ZO#/{T?)ej{7y_P}l|o %?t楅d;q36{nՑH$&'zʧ@ uϓj' ?Wdwޤ">S 'kY(aaz3~}Ϙ /_r3'g/ru]{'Wmaq]_*n}Z=q~٣z7~bp짯műe\9xKu:}g-{˿߽%M;:+: \ p}n;f=}gnY*s[{Ƅ?u􏎾e}_-_ξ{mg/yt-{J+5~؁s%tO#e՗txrSy߾s^__~v+wu$os ~j\o# P{r;>sӣW W>ko\|1{̈um`ARUi̦NasDvOcDֆ3e&=b~g}o&k7mgxy4jՀoy島O IƦOhq$J}m<9K挒O'ѥ^^'Un~-C˟Tz~+u/bv>?tnˮw7Q;ݾoMqН=\v㒳Bl̈oCٗl4x)lan\ش 3G f՗.-}*9vlx/ܦ_CN<'.yOyH ϵS[N5D<{7NYtݻVGc0EqXCn׷ѭ%3GFiLl'U~z[mfÏpmҿL⶝'ο"98'^ccEZ_g?/=7n魄m++? ~9̖7}g4DN {~mdz%ayqNm1}Qf.^fjYIMgi$☚_7˚u_;ڧBaat><}\v! ~8SG̑GsӇ= =}9n<S/~C⫾γ7Nv3dfD"HXN<yze4&鹏p)G=O0䬇% mi3s"JFc]d͘,~ueM].,=2aW}uu^=Q쿕ZhF' JwTW X?(O2ŒO޾شOn\|-xveVyE/_o/]Q/#/,꾽m5Xaub */k_;coxX]?\dmSR' 14L|0W9I1ӒևEdj0VūN{A}۟x]ӿGow?؆Eh0.>2CA"/:G}kI]9| %hڲ_h8vpo='Ϻ=gX٭_\ΏK/i$|͛kM͝O,gƞ*Έ.y"dmu~fx,~ɚJ|C E8y8:z@1yg~MK[ᙣY+u՛7vPԖ}tg>`'6 P^|ygiz3?x#0VO;1'_4쮯[o?qv́ʨD|kah|72fOȃ*vvǾ-o~5+7g>?[{K<D6nKϺx#&Kshٴ-{]`CKxw^' #jMYm{ m_ҙy $xo0 'G Y2M[0߽UZBl-ӟsƓ.YY8KV>WkB~ X_umW>޹տ^CZO\zf]Aia:'8=|whi[~දs-~{mu|L ő{^ni=Ӊ/5C#Hsf*3GZ3 gK~U zkz}_&KxO],7}KQ%^L/>lߙ}ϝ_ 'ߟί[=܍?kM~LL\x{zc,ߵ&YX_<'0?pwjm5\N[OϷ} ^𻷾ݥdO[_{KХZd? >5M5oԞGNN?o~ϑW>lol\_m}[Ng܎}Xw4`u~~7&'fi=s~v'^z3aO^qg~&6){ xBB4?_Fuh?E/\;m=6Խ׼zʵ ~Y^jj>,yTVUWwQ{ן84>ɗ"5Z>&w WM?[&jҢ߽\[fo\ko>0ISM}=?F"ȷM`S?;PZ]R{r_?]wGd׹cWtXj?fZ ?6@D܉|.tO\|KwIGD9}4V |K!3>~֋'_~3RRK|tU󱕏~UIw~?nz;=,;w$ZgƦ=eVGb^ċsxfrhGTſm _]s7~fk=?V=}ɆWN)oy{/jO@U5~~?br67ƞã-\L{b|K%.b[gzE杻ZP=]YӊBI3 ^GOډM2n޹~a=v ro mnT{Wn~QeVweƭ?[Oޱ=K+vjkg &vs?sd]ݖ\we~:8SU;D<,,*vDEA%V*HH:8*xp咈9ɵ,7T]T"G_sgaQuToUY6w8":i5 ~yq8C*"5T;2(GgKUR vsg>J\p/*#ᴕq%0+*3]{Jk$C:pG5忠S\]k2#kFK<9WO-3|y*&ˑ=9oG.͚ppQW'3-)ݏ ^%7a1\18| ckQD+yH+xTK 0J )Sd,w3`Tߪ@*");g ܪt;-D3jX>O^[ V+K@Cۃ>K7V"އ# DRpشk P]qʆ!g-p]RrP 5gr*H8"(he&v(@PPuIuUABu^8bZlj| y C*Pbke*w׿4 "1d%V$_-J*SiPU@r "N!#5&r_"^Gt>N8=|]@Bt58Cy7lZX)F3C2RWϡGeZ7b1ϭzXCqfTZ5(%ByU 32 E:*Ƞ5dԚo(U4dFc&*K-* Jkd@ Y!s |}qͲžV]{k"l-=d,MBZcM?_("|P2b"F+SzV:uED'r8mZ]?+*в4bZOu3WR<#& ]TGP]*t ˹xF'ѓ\* G|R:rjz^LFFPqTc}NUA`Ԩ,.* WP ||kΣ#2iu+QH ":4IuU X2k751D]`Ĭ5\E2HCەDzΏ1XH5`RdW@$RёW~\O֔.ZH\T Y]ȫQ{sS Z=# !9X$КKT Ȳո YDqը$]'[aM`2`|pYW]4C`=Hqي$FYTQKk1Z z`m`?*lECPΖ!xZiO)C凈$"B=QV$Z"в"`D(ƐZNΨtt:u+Uf58r/#Q_lj֛Q/Px~=ӥZHi^Ya!;ktj`Zp֜P̍f:W@XP&ϕ;NQ-hq#ƵVuJ 2|/CXoZ'5P˴V`ᡋT̘,Ж:e*tUTUF4/zձZϢ\QX{)o ["т<-Xôv))El~Wl8,XŤuuErYf>/gh5̫10#֢3a;XgY5[CUHgp&;̓(K]O*U,cN#'1̓Ѥ vUK>̪Vyׁ8V7=0Z* \@|& h~3-_f./ %HXCAH= !ACjXw55zpuJ0*VF5ٷ2zYyTraFptMRtQOO?⨤nXՆ69X[cbh#ǦgRU:X;eՁ8MQ^aP-ox3&_2ZCC=}O" VK4 #9, HXC[`ASU!ch89GjjAAX:ugdmVa,g:qr/**ʥ%CHWjhH D|[LG0!oXwH%( 1u"$UrR s Y ="APA"j# TUBo%b LSj@B 9@$6' Ȓ(;@( vAQR P \t $jS 'AMD+A(Y¤ b 25D=b`Rd%l5 oP8 5f&*c +c`1,j,lv)׻ F)FS䚈0z6jjEPB"0w {JVjX2֦AsV‚55uY-B5J   j Rv0DD8ǧ_xE|kzڃmDDCՠ 2wLCDr/pmݱ<555k!k!C?XDdEʨD0qJj Y M< "kQUʋBI$Jddh~DBx+*a:^ooL߂*%rL>.X1IXrb]Pɪ5j(:QpĪ $MRC P BjQSgm@LDu /@%IUY@嵚!T4- aNFX` ԠdhhAl DMBWt 3+B0 Fx pSHTGVQ7NhPAB=I9&+L`FY 7 s"OD IPrB5~STpRjx$`T(TL &'%j4vjczTīHY_YQhB hPxA$g2$q\ 2'j0V:G]- BVUT:(¥᪼uQEEӴVח^SG۷oq2HXCDa"QeQ"Pӿ7}/K/t-s D(Z G8C  _MP@eH9r RLFYdcE[5Z $ ZpÃJٕ@2֥AR0D` }⼺X%Ʀ5IШEFaBrC"X"B":U[TQDe8.ޣ0 I, l@1Dj!D4k`eMPT X ^EC) EPDRLQ)b-#x@q&bҀB 9ICb\VCd6% g`?sF) UCJljXàDCK0b1Z@U 5oX4Hn %v89m4֛!c FczjQP-86Xk&]ĢN*6GdjH@n_0ؼ\#4&INI`JUQoP "lh%dD8G@_U2hHA%`X ڄ%*" z" ! kNEZpBF2 ƠATErjJ|iB$"{ @Tu P)p^5I.ybz3i85m{6^\155.}Hd9J *F)#'@hxc$&# *)K`ꪈ–LQѫ`S<@9Hj2Xc,)XY Y@4%ӓ1@`Ű昂 bOSTTU E(  !41Eɘ:@C$ H䡠L$BT˲%nrr2Oe]kȹDT}Q(Y$H1Ҭ" P}LR'9rU@ Y[RDJྱ)j[3ƂC9Qb*!#8BPd"Ґ$UDU.4Ef"2{B5(1BD!8+xC=$PUSvMԈ/J!!6B4AadjXB  56XL` ʌl41"F+()xc\_"]< М&jT8&] j}B:K/7 TXϳBcAuFUUP(&MUA$T2TA hRZKk *: zG&LPeWB,IY9EQ,V$PBanVx!'{=:kA QQc (a"0E#U;u>zjG B0(@Au~R8 UeT-+lTjl9?i +k)ꠕJ.pY?m@|68> YSj727h%(>X5)H08c,8cgԨ%77 Y:|sj7Ov:e(9{ Jb BdL|šIBrHJ*d aOJm.FN{9D`] ;Z-4, czH=Ay2S$@rB0ھE5iܵfUPE765+o:$CgjҙI \7$R_ӕdΎMcuXP,t/k;9D$Òs-v mM-wͱb`SM\?IL\Iry瞵) BXn9:7{1דЊj!b2 X,u(PAzrQ)SUe Km *U맑\ B M9g6xşHXB*ex!z}erS=PC "JmV;k)/dC ZkBfz'>OޝXK! PF @ -Pj1c"Cek "D*8: "A+ψ*v珝2~ՕC8#x4G<ښ%A(x2%%HBn@MA3͚m?Nnlt24u15/8͉Z4@I`Us.Sl[;~XqY k#x@QAk qV @XCv ]@Ն[yo(c@QCua~sKpC'0 qjmrob)]|aNoiuj>ݭB֥g[]V6v PYrɉɹ7U lAw}+Ӌ@$뗳bŕ|V|8 mZ9l4 'o>$sZ yP&e3aӞ/s+@Сі85u"5~زs(*FHd&@< cXu~"Q`E" Ix(âd EaE@H6q{ސ-_8H=GaFBjU`! $qsرNZD  h"!)"A2dsXZD(V@õ,-T%ijxfX8ړ?eۮG=v!Y6739uƃ]vYsX0DhG6ɇE [Zs%>ҙO}_ҽ۷o۰%2M4F!S2,! Urq/ 9rV 1` Ij 0$z&o@qj(3X (DDN\}[>/r,34]D󉺜&2!%S,x5HNaYb M<1#Oy;w0 I{O PD}wE3$`ZK.5Y?Zo驤U>v6J%e/Ns/g\jfleσ3VN -8Q2f4Q цU_կ& DΉ2l-+beB"! Kv>V(" CF eNP(Jeڠ 2ř !QAaOj,""HD "̌%Bb˅  0fjZ @d@8K `cMq abDHQ@XAE TH`uvi->|}ƶq5'ZnI\DIŠK1jئA27kt^m[޺hs<1}ʣ^ijG?Ǐ߸ilE-VPKCDƽK\4^0@YKYAZT$ vX *(Ll(2 g@ lؠ33xK`H7a֡lvrl:<{ໞs{ތSmpx`,&[Ԛ~<v3XT6i53aº,ܩbqqו{Η3c},sY֙nCT輇L]EvSTE3PB PUSBT TRP1f* RS6$R'0y!8p"$1֘aUAǥH$ H?Kb)H6^FJ(k`AIJ3Z"\)'(TZKd*熲FjIf-E@ݠe s6i6ӱMXf"稬YAc Ȑ1$8 c1XeGIJpV \䊰iq3,g屛ǟ&?&>-Hi%dQ9&XPzظѪX4g=l~{Ӊ):AXaf-Ya%Ik!i1j `kaQB`Q%,\J\(`мIbua7 /B CJ L!BrTo I>NݛEguS4YLn`:+~Dˈ Ei͍q-)+]ڴh RR@=鱛_O+5[Y6x:E'/{?i|H \J .ءӥ^1%HO2Z擶R>7;&7W೷o9s஼י᰾כJ|'?9{OM&d D:|$SLKk(R٠ "@A*[|RA%Q* Z)%]#(A$SRZZD V0@ MZ4fy"'$SfPAI%Qh5%. 1AQLUUA*U({ւ%xV/l1",!( bQ.}\b)vz/~Lmex u= ])! =)-1/N.v}rpr7ھG>y/<⒋g :~. bKU4,[ډeJbsa`A VQ0"^*Ze`08SP+X,d*i!&&i">[jūѐ0idۅ!pr L ;o27$$k6snC>B3_^g%%6~rFg?.# 89Z+Y*Tq_'6c+\>͐j$hCx:9?]^ LC6ى~w_;o.[B &T-7tzxQ0$ny_;=$ &'op!!%Dm Zc6!([EȔlʊgïjb d(qs3"тt P/%HTOa@VH@_^S'nesZh 7k0MSۨ'$McgLÄ>EV+ըlF#pի}%G5 O?o }F"Q`E"1q5Sj9- >`Ӵ>h%jHJ?PyYʺeEcДHA ,0T %auͪMڧX aKRCЦoS?];i_['OSI:6miJ)1rQyl'bѻ/:i7J>qV;G?1g? /Yh~fBݵd2 Zރ2Ya=XH@5)+Q @a$Ru`.Q`im)T:q A0RT VfX@ jDmg@Q  (!2'L. P8 vI)Ds<w؈P "GL!X)H+hl1\Xжg;x|v ĩ)kk_α{{/fde|u?]LآM{@=D/$&51N*;fT u"*֓44m #dex[UB\պR&`B"BRՌuvH$Z" #ipY=ꅣ3mf$>䁫+DN+S0l~t-"*TphXM%fӻX *Xb2C%1kIgL?a6m? Z9mɜ{}:OښY!d*Dº{E矷s K΍5㳟̍6nS.Y/[>8=>pIR :#됒(hH!(0D.%,B$D4ƠUB DV`BKB`LClCqI e&E jjzJ֬4T}S  * u@ѳ jU{jzW=b[;}䱤֜SgMC@c-EV1i6anBQ4,^K^'6N}_PVkǘs5sGGf + K0I`rC{m6x@ѾKŅl7[ZO5߇QXCס39Z e@aiXa2I`VUc(GDjT*VJqXq5:dF֧CU`Pδl6qPҁ;,[e !]KPVn,U&cUaĨeKkQG_I5_8m,cmoRg /xϞ]8h"ժgχ7΁ϺjmXX9;ySIЋ~*'?/mۦl͂7\!2$0XB",hYtR,=j Bee`DPR$"2*Zd@ pYW@ ^X[X$JvNXRf5SC@P02ٕWnh-!Mn7a8bz\\QC}=ZgW}=ll73<)-}֞~K}g>r}VwaG=cl\QK=7_Б"I䤙gчC0fO 7/ -tvȤ ,qAV G~ >k̑vqSk<=.z2;{w_0F6VfE$0 &Z@eR*u h͎ *jP.A+(yTMRQ2!Dފy 50qTP .b IxN9M\uѾs4բk6dY8Y>j 5M`qcS=cF߂a~" Zjz"BU{`FkYnJa.c0A0P9cc5SZRbYl&k<푾qJ6ev60@AD"8W7 }/̉٥SvwpWn O]7|^F#iR >U0Д@u@8iW`.k `, CU C- k^v( d"(րB;=D9ajQ[{ޜ?fal>]SM0yZ,>xcI6!sѣsbEza@%Er@Rwxw,ǜ'׃c8&/K6۹cW,GX H1M% ' ݞ[z{=^>Ͼ_?fކƁo .˂nì QEE$.H]:P*?"R:VÊ|#emW ۵쌶jA"\M - .`Uʠ0b B #+u5 _+ hyP΢( r'I4TRlC3Mz`tYccnNj޾c76.Uz^3VE! =/՚n*k Scis'n{zѵLs_$11 h;yݲ9 |b %)a٪oڼ0_v_xrZXW\q] !dD24}UU4L)DDBxv^TQXCBY$E+yh,y+"*XfD3 Zj*z^¡alL,t$ YiUT%TA` Fh,R rQuThJƃ?*],UD@BJ˞œF)ꋓ )~ZI)}zWsvgG:6s6o tѨeKATP,gw8]q/1pБBdjrc^O9xx wGĕ'ŭ҆ul'e֨xD ~!Pc CcLB@LL&>~5gre@zYYE2A=$)(&7&]L8'L׻[Wgf]ΗRd٦ |v;w\SւFn&{lvDҰv'pO'u[Lf?_Wory2ql50k %r\? İ%Q2E!4)Pw-P-IU ke&}/=5?sҶ~ݳ ΝiR.hZHY&L@0}UP(d( #h"ŒD9=sOh{8woO9>yrivC{ TV  ڲPYEA,)MCVv TX B(}IA@T(1 *5SЂacX >&&EkD8xƯۅN @@T5,\SeUb൞fw;ٙcg=yT9]_8.(eAUl5aV훪^ ,*#`׻DTjWCAwN!#5*s^&qbY0,Eeժո/Hʂe?a4j+S"eU>²""I$Q0RX9p!  D!"vlPD-{A_lo+8s1]a~oq܊{s{/9}BRXaY{84z;B>X}mOY\;qL"SOYW^$MfWW1P#\P$.FPEtR+R-(? @Uj ޳* ܳ VZ"IPH*8Px 5 Ůl5io5ɤvSnžC*lġ ` ^zyU};VX>xha江z/|ѥtdJ tʦi4c}]izڀcǿvm3'vo#+۶mܴ4T?=m9xsz㜳bBΨx +!clj%QeQa%2LQX8*KVh-@C&)!AC (!rPD>1ހp% T< Ad}YZMn 퍡{wg涼{x0B;ic.l;syq; B6g;+ =xĞ^p_YnO5'& mi(__Kp9;;ѰB=U =X=V߿oر#VK/ئno\FUAJ-ޫ*4~TqSxoO46DC.]!ji:)K5{@G~;[@?úXZ [ h4b2|Xzqj̠ȀDVڱGDYDYRF2**Dm?t}E~t nO1|ŏ}cf;~Ӭ)iԵ  DW;;,KlU j5/,~7./y]~q/Ɋncw}#Lk"!hdWnB; @F4Fi"â(z(Ut~Z8bED jMR`UɽX&GD[@ 0jMa5ݟ+"JF *Pv)Sp$@0(#ք(#9Lv/BXZ5[.'5%Xǩ<笼ۅZ͌:gY_$A5zl[]ܶ}i['Oc':tĖ]Nqs,nro9s 3=^1 CtUE*ic&eIJe\f" AU(b , &2%2Il"`_@ bbEA!= %Rdq=ܨuԬCG]nTUNxl3[WWMqzt#tEFE{C}"ө>lh}ģ/leeп c~36+@*Ƙ_OuǹS700: 03B^;~bni S]|o^y"|okfk,0K_: J9צ#S\-Rz p#S$V$^DTTu]oǂ qXydmԋb(QFKT[0bn5tVJlXFeY@DHW7AE%T@1]Io#|(ݳ=ݝTzmmsy%p' 5`$XZjqzv`f-:w OΝwގ{v//gǏ8ع1D5~3/0"OrN 12BRd"-+cDbyI  B&Y0(VPT0B"`l]>SPA3P-26AF-L`yFV TnZj v.n'/*56 noq@lj96V w_ȉݛY9xg~}Chq,0C@Ӱée)| 35B%aLdJ-Td\(FM%t/|>tνpMv=|Z3p^×>uMwIyi_ "T5Upa= H+y(SpZ>j> u@Z,a6a鵒*HAUT VD $4pFK@@ JZջA2 T ,2H˱)eAbt ~;͋w[ڶwmɶ|gV9%[+Nv>duqc7S&ŠdᲢ{d‡y|G]\^ظijGZ8~ȉ{<|w}L5}Ϻ?#^d(j'`"* H*2puPE4 (s@APEIUCivAFeCF,\t2WLA 9EOUc A [LSU!ԳrMƶ%x bYMӘȓgX"= vtmɩe&\x嫋Iz=i,9v6oVnj52 !h'P-zpxC6ł HLyo[,#)(X(_8x -.v{KԿ~gqqFZcΙD ySe B W*2, Jt|1Ɣ`Xh4V$S G@.U2(v-tT^5lR1(UxTmMYuPkXkTdPAV/*RC 6Tކexƈudm賣Y#폶þ;BBw•"4?so}8G^U|p噿Gfqc:kyL-v3'o<_غ<|mh=gq~q%ƍ{E+V):$ D[NP B0;_eD!X#@`ظʲ% T^) h!!>GPz* `r5>#`!;!Hbr圻--MSc2ug?dӌOl`KsN]ƯTݍS yf]:-[}|X|0Z(}-M!-/:s+H!*약CrDp  Q4TCgD^OՙXcX {dKzgq3~o@m(ED$jR)6J&z436MkFmF(JHI@؉ (ԂZol"N[h~$;n[yď~?-/="2]S6rkҞKL]|^'=c3Lomئ97^E-֢^=q,@,U3=' a[+Fqv9{ >A æ{03!-;6CzZ02.W5tNًCܬݮt% #q<ʾs==V+3rhKf/pan}hӣwZt|+q䐲ie<$8f@'9n%&l~QKg<`sD8Wf*2&jZ)"Q aӘC6Zx(,'˨e/,K{>ڮwjP9uLl0hRnG-;ɋ[߾y+3vΗ_C=''BW/hX q_Í)TNR 9fK'&t?X f,nEyY80u%smՐ Z[%ʳ{=^oxj TXHE-֢cm;1hFku B3N<5vcAɁ738S2s熍>Kp vJf n Uf01F# "^Q0*+Kv~SZO$X.ϜֲVen&UX {S[w78}z=ޣ+v쯮M6ܸzͷ{++=zӣҬ\L7>lp!r͢e!F@4,*H@"ƕ <`nSUEq :zXըͻY݂(z]v؅ x/eKn~tNWgNb̤.:]8nW<;S}|6{7WwN|ͳ_yo|#ΝNE#pauȦuk;-Dz2W;=ڼ* Ȉ5SᎲ23ձ^XF#I͌L8Emq ̓kೠ&C:ѥT͏u-EW ֢smSyBLai<1G(m"f̅yB64*jZ㋣Zފ}>q|t ~~!@(5-sSWSQcjnT+{_ftʶlș ";Cr؅O=j*&u`aa g fWkw6}SO[nzV^ ߾qڵ$X_ I$VZNԜ(&Lp?`p 5A $ו`"8qF,W5720,KBBuUר-0:V!ę# e =rY_~KG{ [e'ຯVzpo8 dNvWAw¹~rO~?776͋MC0Zp0rSzs^iv(C)(̝\: 6EEܽ42ÑL&1V+bt4OR .7ӽ<)Bl8HhqK4p,iXkEUÄ_hE-:XZԻ5?9̍ZlzbR@UܙYapM&. EM>'wg'q[dϊ9Z)7`4wj@'r׎*~` Y 4ڄL'iZxWzLZHqU9g Lo'Wz=(q;:Cy7NJF)p0K_xVy+7v͝޹gOp+_{ClsBV+HF^qU{DJ0Mu-S4%Q;pi[V #CE&9FԥfU)6ݢ&u4L )R#dr]ytOuVU޺{pk9b0h╎ y\Mvhtrh,y"z<#y_zv|ʙމu*2ԎL'eu͆ԯo_q1Έեd25)\[B+YnB g3"V!C.=\jF-9C?KC Iۗe ki6D_ZB`-jQ&99[%i/gMC {M-ë99B{,oPsd_H)gO5nnIw{[̍(n)7Wq0#;(nvUZ~t_DZymkz4kVtg%2eѵ}=wݻ{kVV7:kG/|TJ ԢQhqJpQ8Ag4^lFr#+46Y & @ 9)L̦*S1r,ˆ2bGQ2̪8>bS~ {{Pq~8T@l{[^`~7ȦWz!fS EwN\u筗/Cge} $C^EK/ЍRV:5 cuD8L>p:ni6MDf0%G"Ph ]sXZ%L+Ÿ;-i*}Su_=:A>r"\B`-jQڝvy|"01֨QDBnKc$  SSvɩ03M,0fFdfQiFg>an+63S7ZX 8kBTQ:,vhc^.\tkzxs yb#,{t|R!Z}{W;P,pl`4cUkdϲ]ss7775UJkbg59+(jԄ#6O_8V{rJPB?_OKo^֋|x?ͭӃ믽t~o.-uv"+HTݔ9u&9ݗ(q+`T4NHJ(Ш-|h0"28t`GڏD@x4Xr!&LPCl=x1gֳzfK} ~6EzL7Kn>}zIz'``to~K=#[*A+UYUy!iYMrcW+#-:Hk3NwSS ox0q:*O`.ݾs{won זo޸k?xn'T,LLգjCufg%E4;K ,!x0̠f\h1ʼn0s#cK4i}T+( 4a*rsBlyy$Pq2_=KuRɺbozww?9qv;$W^L߹u[/~7^sgu)xRІ(^oGVZHmv3'~±29ܜ$9 nB c$H>M9~ɳWZ[9uUZ3+oXDNs7GiOeNkP*m#Ԡ0gtEkQ-/Yr:ƻ7KH7oYN:{ c !Gɒ>Y\:t>,OM#YLLSj=1shS<37Y7͜[]:٩no[F~^o'i<ǶO~l2.Lb*)ݻ}{G>#w\eBo q!Uک1)pk2A0(`F q' EB@Mi<86GTRa'7xgr-8qp/%d孬~x+"Na9,;'|iqu讎y7S/}?sڟP>$d 3ջcCy1o -&nEK tRs5#,˘8ֵfyF,Ն'[x{tH bZj.T:۪pƟ)ή E-jZԢEݬp ΐM̓ɴΝ:}:흝K.ݻwn-,A\fmm㔚kmH; vIiEko*͸1gYww%!9Sd'Ȇ߸(NY?yXu-we:;#/N7oE#b `ݻ~N<>W\[;',(DT( INT)1K:!-8Z68{y-'fbMU:3`8b:Etu{VZdD!36Nt4jO=]+$^ïNt׶2~p7֪@%u rjƴ=)݉~~WQs&Ԭh`omf#P8஦ ;}_;sT)٤XQ-]խI}YidޯbBZԢ޵*Ylj;3,uϔ챪QՖեv] ӉXf]?^Ho4{2Oс^,!0[ !6L437!'iMCJ 2Ku#aj43Yf0_|WSeC{K^~NR9޵U|ʣ{;+խaֵ7W^Z)ʳ"icdF. W#"CAcNѠfT9 8 U9`nVh9L%s1 y&y!d E6j4[>',<[ ݾF-UmWv/]Ϋ9WYB "ЪHP)FC:طەX )?-iX4ϻDcBB]O76Μ9kχȺJ7)bZ8͛6 Xo Ui? "LY& f4)Uj1"\L4_ -LE$3U-˲X[U#BjjjmneLQ #:4r!hcծRyv֝iGD5o7f#2f'̙ňfHZz*1b]k4s 88[l9O GUw_>WOwWN\+ˁwH w=?GyoXr^xƕ]/RR391`npS8 *-zw$I*@Ĉy4F"%(z}`$FJp ESp'3CYYa`VshWj)]}o8=Kzo[ xo^ /H5=J6X82pgZGGȺjpes0fTp! M'ˌE1d\Nʉe!wwEL|4D9[8>Gxo.s0=2%g˶̽Ӝ6ZԢE;{#sfFKɵ^n{nwyyݽ!qE㥧64l. h~-L?DzǼq'C';/QbL1F3 BB\cVW4Q住UA9JL~QUuuwZu.gb9v['Ϝݹspo2&K{7Tz 9ǚq(lšoz]{yo%J;͘`+AL 9D(-* %wݪ@5͚FS&Ƭe5B@6Dtt.ӈCl1;?9zqx4tOj;'ox _utw 1UH;МOofߺU,pmKm,35&14Nƀp]wEd<:: Dē3;IrvxV9>M.܄)@̳~6CDؚ_`"#0Hkx3$vWsJq;]$3)J VB-7;X.o;+k*gp{2?ý KyU|gՓkǣDnѕ `LZ*;1:#}aj@j@L"HYÅ 1t8x^jWԕAyA5BNZGC$x!:#tGliE6G;rWǗFC߿Tm<9;h|KqNNR?Fպr&BR(ag¨X'?w ܼ$Dv)#q9Ș3IJJ gw,-D(TW7:N3#_>}?>~{Ec;e NTv槢fm4C\5>zPh JaDm^:>͑ZԢ4W iESr6]&P|D :,xoYy$ꖒ[C9;bs4i%pl9|W;fWQYN͔VqZ=(w󧞎)XEUU)2 B 7"1H;'w߽t^"#R`|z4|fvTky8::|KS9r:~GY  l.pO,qуxZH)#KZ $D`15BzWGx9lpBځvxexvs"Jș(N_[ӧ eP+1ke 'Jj-L6:kܪSnM4: M\kmB`Zzj}PLx?kŷߴo][7&V%M2{5M&gd,ڣl΅7쫆B ߿FPVZE-Xi IF@tS`f2BTC TD2sqsø#ufX #7\Gvr4=r2$P1j M\k 9K5L\ԃsfڅHD1A|ul{ztt[7ix.xX/]~b.?P]Y[SG\^ 'l1;Z2O +fQB`P̀)n!6&zqwGV??t#J߸cuaToI>ϼj{W߸q}qr}yekm:v#<393hL.&ଌM,Sm^Eh (e=r0r"GiPk+C~ǧ5eɖ7S/oQvly\Oo߽x6'DGRU=}mPZCW/\⼎*ʊȑ5jGߍQ'j1c__٩}ZG_>GUUU 0r$1x;dܬrQAiӏ7nDM:ceڼ`E-֢&ݙZ85n\jiPf扑hX D32 Tҽ-qqC`O4)aN$[b &:601̜OD횠A3G`b AT YP43 Tg$y$QUZP%bѕi+_}huk^ꮭO5dO>"y7Kpo?w0|șuN<#qAuG-B1]Սɉ sGD!3F@DB h%|R ) Т;LҒ%ڡjbZtNOG##6 8eHctYP"wrx/6?[?ܿ v(qyo]qmZ+l#皁d+F\Mx)ù(KJgGWX\. \! '֫j3jB*Ǔg/,Y17o;ip:=J9Rmes7Bj(mu04JfE"kQzڶf8> hR/;۷.ovN?𞵵~]\Ha0o МBɆh%, @D!dYF-]YŮTƂCRQ <EDJ Z9.,޷_{2Rrw{jr7(`eW.Tʚ6yˇÃN?z|{t]"0 `" flpQ`=%1P%2*f1dNcda/`b;+dѸ3b]gG+6z?2z wޓ Ν3KA'Jl:b48!_9վqnQ'%X('S]AsxH&u9, ^ܫAySW^y4ՓG6je,@X`pNOI 5H900LKfdGf f"6Gͭkps1eGP4@>lIbeI89A!1H8p!eeiX_^SGW/m|.?gl~'1H\|o &7^d[otPL35c GtI#iS'29MA!rf6bA:,MuUUu?#_[[y[ϾKAIJ]3&"9­ 7ReC򢜐Z"7ߔٌ .l<jZԢkn+|,q*Q Hf.L!c{͋[GãN#ms`ThLvfh4L9%iG**M ՝`s 'Rc)d#!"$<, yyXo9T,J$iٵҽE0nlTAl\C?t:ZQ+uX:;..ur*ݾӏ/O ׭bt7P``EL2882 ա f3sUw#b8 D2##1€ ;D-x= BCptK΃Ou?Z^{vzkXUvƥ|8N/˩xmq@`k:ʱZ:=x t獎堩ytY׭brq#Lf.jJDU7XEwauUW8<߾{\vC,Q sj"oD0apq iNDǸ4u׈-BO8)2kQ E4$,b_ܞn:V81s[k^RUiIAt6ʹI$$S05iq0p#0 rYYdq$ Buo?'S\}OuR"(h6D' (7'Ǒw8K{]S?QɡKӒu9lbrK%ޙ룃SˏV-TD`pe"6x 8ȝi2:gMgĄA- nP7g03aFtw(0rZ5&zN4X+uAcO>^\ -ͷʫ7JEuwQn ﷂ[ǔN ;9Ԭ&yϥߑL؍d)e/v|P(MS8b&'o\;#gB䜜6|`fw7\${92޺zO28 KUW;GZO?ͧ3oޒzSTzZ񁖑 ,Y2<;Fp&C͍*!ãY$T JLF0Hd1bZZ<[XEk-jh#d,rBU== rsZoLUatDi1fK!=8TD@ +{7tvK3y[y[oۭ`ksQJQ1%8WtɎL ۧ쁭~)~R!ȳQ,oP ܲ}1;2u}j~H-=hVg752srjZB$!5U8H4ZW7 ̪Vը&+ 01IPD'5:e+W6ƔxL1Whxy@+GKoSU\Vj>*@eZĊ}ZL.lr_jNȶzܳȔ#@F^EG\D; ^U B$乚VUmIݵ:7nݪ+ci05u YsTpP[o)ސ"i`Gb>pѼZB`-jQ^]BZ1ԺnC &QN@/ZkͺC]K46B.YhIOA9V ]N$"wM52»;_;3_կ޺,κYc`XO#S Qѫxw󿷺ZS e5ZEٱF?37ꏞ͵]T0,013&&73Ξ &7YWnݛ,`s sWk2FSHڠ3ȈJM b87(쒯QQf?ߨ6WwG|@'6|}3h9'5CkŴ1@^u{jynF?{K׻_t]DJGjRdшҾ%7kK+mO5[M"0) 0UxDTeq:V p &2w&ogً c\>U;[ukXE-֢l`$ Bp D$"-26DU#@"0w&3#KfWW1 NA""'XA[+JHqϾ9јK"3)B4Z⑂dn??g~p<4x"f'ݜY̋ڴ& 1q/Hr9 9zy޳l;3X0%U,xȬX +.VmtDL9Mt{OKufO2J3V7ehєAJNrxSKp 8"C* N/52C0*{t{Me-6n%zExDpͻz8=ӧ5dTԢ`GeV{G-*:Wd9OO4ϩlIt <[WM40 2;0 *Z$4I=2;]GvEqfb3@QbMq4鞃&~muL,</ZE-!hGO}{&j_l$5Q N|0s2gb0yZ;ͨ&lo?>kKyؤRjDqj~/rf(f1K_pb5{NՉeZ^zo|[;_<6W޺8R(,w_(r|+y؍U8%VzՇN^+oUrw}%>kM,P&Qj aɜe]-J : :LْSM#P2κEp)yrl1oƋӃbɆV;Q)%cg4qՊu,it@%sAA\A5wWTxu [\9Ĩ14 Pଣ1X2NƤk60wB89…GeQ֫wޱN[,-[k9 +*)Ӱ.Z+ovґZFUЩ{Pk8s7vg&m#@9! N0aؚ=QRv8!d bɳ\Z ;ԛ330ۼ4zǾ 9xhaEh%ZԢe5[k­imd-ze+ [JIDmn6([*\aF3:=5Q`8q5ڻ]*i;95A5D vq7N䡷vk'Wv?ӿ_[_n鵍@kPFB9;bic.go}p5:*p{/A޶yh>1=q?aC'- 0$mlE363~G?׮Μ+d i FdQeBno'3Fp4J9388Xsr^gUvnRm0VZ[q6_9@J#-=@ӘnhӲg'q2ULZ'[|2aqB9 uW`Y[ݼq;o=)OUNK;.F/ "XCk.n-x7дe0b%!}h́++s1Oju9D$!FV2pJD2'Q'5G*L;lH|nעXZԻwֵxo~?ͽY0O -?~h\  'ODǏevhE|̾J&*kt[ig4c2!,Gnfe(A&gW:oQgx?sbpG(LPt^ *֍b,o[⎞F3<4?B 1 TGdv:N3ղ8@0W78wKGw<"u`'uRwFhEԱ9IFfJ0'7"@@C˩gwN doZy9!;H05%ۢXJaGo2Ose6Q@9@Z C6c{e1]d3RXE,w nbl C 3w;6$f7SԒ $RWFyR)̭ [0Q9P(d] 4677[ݛ8ަDJ\&wug88s/j!wIj~3}juܮ8'-5{@̜U`80}vkLx[Njn(>ܲٺzTVżXh .B]<#=V_82>۽?8ğ_g?ڕWyvmj{S" 1Usj_w'߹Voo<VeTf dB%r;yanT݇M`E?yz?'~!=jk&ԄrvnƀB,A4I̔`bL >jSVgnFlfdۤm6 iNŒR|ZB`-jQȮ T`wuwr'TB<( $SV񶟷?9scY?3xCsT[â?p۴ 汍d[:A %rQ݉| b کt4 zzF++F2rc}N9Dq=&Fv笁bgb4v5P}3WT"˂G Sk50)~xg+olL v""kzɵuv98 :^L" Ŕ`.K^j〴v_ROV7~o|7'kK-񝺼;ڻ74.IYR(Y/۩T*g!UjPUS5ՐG PW3usv/.]螽Q잎?x.|2\j'8)\J F`  s@$f(# je69Ý$s=#ޭq+q0` ^M!Q2!2őo+Jd^,d&RF"xƑJEY&5>Ovg1@6 J5q`r2!~>(Y5YE$|1ܸy_v=dK!Ʋ 2Ec[Za,ҕh6ovdC.Lg-}ZB`-jQEs-BR1#F6o$SkJ)\x=|7mB1s&npi+qf`p2:wO bplq77R7M-"' Y ș3zѿGU/} ՗|O>ޕYٓJw<_qP_qy<ݍV]xpkom|{o`|SA79-"7Ü&l07'k.$֢kQz6UfSI&Q0}L;c6kt:t?iqϞJΘ|}9 %ֶG38[ c=Po Zs.|n_᛽Gyk^HkۿSݽ4WGTç'ymRBƄi^ev|y:7-;Yq:+;'/DFFZ}vaMc䪹@6A x1ritywm;N! H]%|zՔŎt?=pZb:eQza \\{~c8=Yؠ7F+φ"U]據=dpzX yyn̘圾jtm4,?_z_by(/hxD;xk/;YSnYFLd4_sM?磅ZB`-jQ0Eݐ:} '94c-79!&¢s'MpURR!Bl M=5#PK459in0ZfP 9ЄY^t$_.lgWOvܙNv&wn8ޥZ{^ykoV ZJ #jGBaTU: NM9& ^5w;s#{7/qnyp郧Oy*nd繛/>'VPCZ--G+YګLQâP., nɻqu4= 'vA!#" HXBFVᅹԽTn1KPN`Enãը)3U%k ) #K+<(̝n_o^)VٹPv|2TL p (jUDR Q#BfS7%]tSRDȈ@ܪdHkko|O}[_}nnĥ+o]q9FG>d"2]pͷ_*ݏ}쇙V۷&%i|rjXqc'{ùM=^lS1dHW:OZXZԻ@].퍈nY۴XjDb'5foyR4r6)43L'蔩\dr&#LMln[i@$$͍͛ΖFJnf ]3=jSPrW"k4%yNzTŊJ ,܍Ý{ћN?On~O ک$s'D{AtIiRNh'tan$/[z75Yw[cw=8*@u4t#))HM*"dg:%r\5a+>,G1ŸXTN*#Pi~R:8R.Ȗ{GƯm7 ^Y!z]Dh HDŽA@4,Z,& @\?>k ,-LJ5dݪ$b(?>wyŧK,__xOã+bǟ_?/.]);"+L! Tcct┧iX.j!wa[%J9szo(Y7=٨X$V.i'A0yHւ҄Ʉ>CDα[HBkFpnf#&->% LwS3jLmZ$ }iwuxtm2ٮ[wOy:yu?s/ڕo/۾ITiE#d8&"KܩGkh% .MGNO{z+c)WN]FPbu,2lLƈF,eMxdLC.Qud`˧7@:4DlZtt_p ˼m7/Ư_ݑw2,w[ץsN8.&,} 1D>[}H]YBRA @Mp@౬XNē~lџɿ3K>_]o_9{k][SO?{vdcoCNҢw`81':SH_m{9}ZԢ]4mF6Ɍ ޠ{B#Z,%w%cVI v$f$ )й̅,sbsbBdPRW &VBA8$@<5ynMq4YH"F@PV-FY Fm8uG÷_͛/'z'O?|޸#}ߌ|-d@C" 1&Gr0Oч7^98uj Cy">,XՊ1O.,Ak]{Vg139MwƾXiiYRXI]lL:՝:kd­ȆM.u"WYf~k7!X;#WfFuOiiFBYC mFDHLc'Vi9]6 % j~G_/u'byk_<Ϝ>~WzcLZWO/|{g>~2r #2bkܲA M݉Q$UNhV`-j!wM뾿H9־Ef|4m<tKԞ 3\b5Y(x4iu?P~L|>soө1 €uʡhmp4ɝH bc :xSMw$d{Ƴu|{3Z?+|dpG[k;kyFumTk>Nl4<\.\Z꽷vܙ9U#O.fS/+@̳ a`fY #-AI*A5PR^[e{}tFciTj]f2"a "$Yf+qsGk ׳dFpfjL`bkt̋bvCVcas|]6i91T=]qgw*w;U{;է^ryí<سَ]OyIr,gĬ՜Y;l,hCWh){#Gøg6>}{|}ƭ3Shck 4]#]iˏLV̺{hGdk}_nm^6K<7^uWݫXw.bɊ-2knĠ*LҀ{bqcXCݣR3QLa㨎ա|>ӣp w/%w Be4)%L!-S\C9t6񎍆gw_ȟ}n~k.Q)j2L~BfI{C0K@xxa͏Z(CŢYx:,ڗ򐏯^-_;wn}񋟟Nӕ33W׮ݿzW_}~?ć? Ý<'sO,Nsit"+Ȅq:!JLnMwT`f9iu_xQZE-]+{MMR9[?6; s>PٽHMK(Yېv{ft@t:VBmHӃ8ۭy XY*QpdiSZ9{0@n0kV68~mOF^7;o]ڻxctⱲv&t̻uUt0 K-g8;ݭrYg͎ߺ+8}yra. i_NG+xbeS*-sZ<&:rXnv @!tϘ$&Gw3)!=ja%vW.>-Zʶ*#2X0hx "aΘkq́8??' ygsHij"10)1(pi=șkGOmdA9,{?ӿV?T!uL  IܲieͨBc#!]H {~iQZE-]P YmX=4'KVc9 q_ڎ7+kJ{RZ3k|fbrQ{LJb7B8-1ԸgԹr&re[hE7q`ʦޫȜ.lDJFbymGtXH/H8>l ,nVQy0<1-ΦF|XN㘑7i@^>,OQdRNry2O䇯վ9* 1J"@@=U}5U^R7DvˏOouNJ>Lܫ|/O=۷FC A,y10D\@fz]ыVƝ^jjG!YYs3wBuZd#R2A;MB B$kbRKD&0ܙϬӌ0볶mA]tq%= M=+ue~ug{ע|;enpb!w 9r=۾kkYe%0yfNs/bj%i-C}+lQ EʀK̡t2ĵ2IO6YM1*q;F(bd䜂E h8`v'Nn n)Q"&Vn 8G 3,VZb$SLY=:&TD$M@^ uN'GDg+\h=/~zI,wLVGSt[y?f u5㌍bZ7/J5Gswz{~{0xz}oz8u}By>bոK69̢^z)Է0հ>Q^;[{R1qY{l偟|͝[j_-~pWb-ř-F#N8jDawe&LBq LRBŖ"1hm[tiZ.mB"73S7Jkf&$y%jkO׾fkOd2 zLⷾy:n_g`ļ|?M,.53p>&GXsI,2W7<3!"E-֢ܽl!)^A7YPv\!A}0'% HFcBsVkkp&wրԸ챹1[33Yu0!c'"ufldL"wrF\\C,/p\֣X:]L' >Y1p?gd¡{{@'w^vV}ԅig.so9tr:~2w;'SyP\MttDqVUɖ9Va1N|\B;WsC\.WAw-i4LJ&1w؂9IMo6>zUmzDxùjSK2ig*:`woak}gYW%䏯Gh{~O=BQ~孢?O? wYR+<}1VpMn&֢kQzש+Ifbf31-JE,13Ssu&/Mh"CR!8 #(!6 `S3aPF͖}\om5)*[BLABfffi Ru7-XBDB2N` xʒ9`Nɉq U:g:R7ѩOb^w|4񽽣7kl)b`,",2" Pq@ Ir !X rH( e&gViZ:tFvJz?ߑN4{M\UmUy?jv8u7mZ5Jywn48[<5.wSH@ XUnE<{d͍ij{UU50Nګ۝G?x{_l  ͻ~莮*y>:Q@[,BڰׇC,=7;a6 =+v@V9T _=y?u9UL߹/|7/<*{|_{~O=<~gbB|㡠 `d:6XDg`DWv8mm}jh4 0dTkc@2ej8G,3pAP[@yϜmʟHdD4 @(U+ULjsKJ@`@ D@";H6Aʖ."f L*O@X 2@4`O!espUo:tVOut<]9xw :v4rgNa y+􁙷kFv6 DfH<9np0;Gw!aqlH'h4o8R.,<ę=ƽDbyXOplݷWW>uxD_:znzm[hIOONWGȵ=P-1[V(o\x-YI׮|suBXeZ)Π*ɤq5\w|wߓY=c @lB]LY~J5C" UCCXOGw6:̳i,q ~ɿ_/_;rhG>y޿o? /?<:<_/bqW,v]/u*:L)_mmk`mk[*O ʰO#qCi#yCaCѐ!_2 ǑDHy6 "<8Al4HQק!*iv*?Ifc+1cJ $tCf4gN!;Gn~?ro?5?W?r7߹[zSDOG^:|?~x›]sys;I]_oMgzǾ^"X"@ciyr -4z]_6M+/?...vf;3mi`&P,n8,|(ˆ<"nlmmmmC$]mpPoi3. sN 8&#BC#7ɾhY`F"l*c4gpSֶ0}րМQ ߌ# BJe{~MhAaA\[3n,L(J6S1"+@D(9i5˳ ɫ&~Y_?yw7w\'pz8 sA{a=Yxx燞,p7D:ϧUf!yziɼv`8ST9Lg&>X|tkϘ]ۂ͈2Bf@vz5FErr;OCV#c)Fu#p`:L}숪su+~nٟ}9}߼8?~o\,o=wp9[-"7C$R %X1qS,'vp[k[ 'nntKow23Aa2e(6Y%' Pbq(KEY>PDBS/Ύ=~y534Ҷ˓L;???ݦ{S/ּ? [a0 ǀF”9?;}VngRY p=6Fk8^sLOPu4 f dianp6ݔQDM'~y߿{xx]jOIN/)ix"Tpգ+qZ\.NU UR!@@7(pCbagmtCٶ ֶ!4-_>Mc(!aoZ69y-1;!1"*^|tCFmRq"ΝV6U񕆪 % @D ED3nDuᩡ !!" +3A>(fbЋ:2KIՃ9o:;/߸}SwϾ'?~'?o+y㋗O:Ƀ7'nz޹?p4udWshC}ګy-IZOD@ W*I5иN޹Oy}۟hf@l9M82$}lZWo#`VAٜ*^kC7Ofl4.(hAMb@1ak*Ak!iF L;*m^Ύiݺ\k̏u;-^ =?mb,T5.YC\=<:ޭ\TE# ..: V$+f_6>>ۆ9ˀ/gV{ۀ緡zhD`FLHf2HTG##$"R흃PiU{ b4ZJ"}UET$dLjR4mUB%^.ORia2Pg~צ +{=Z.wfSnsS USC8ݩ*fw._>u;W/0ϼxr7޻\..V<\_}/_y>[ߟ=yirN^mi72/ b)@4Ch!v(a΃>Om|%&+VA]ΤV`M7{ xPo$gol6 [T:TV IHjČJ1y1[P"$SS!FBR0B༖" `b*`*Zbe&T68pcE0'L)3msR@ X6Xևp<*C16f c9|?MLf(Qrnde1l(d KG()"(f")2!):AY2sv#[`"F50q`I#$0SI.S)vd&,b2S#0&c3Dct^*&2+:bפ@S0ݛMjR$NY5s|+!Tn.CSJǫ,uɬDM?xቾY{w?|叾K<;?x܏è:j$2!D(*>"tnj;o޹zzV\ 3w-Ee(`@/jn6p22YWQD eȝɟ` !G&ydIR"#f(1o22,`&%<\U3K+{jyGhz<9,C?Ömm}H۫<#_jZ,;EF6v9D)"03hLIJf05P yƭdd TY=`y4KbZ9 zޓw+2GB 3RcMHHTLV&J09R%dS`3V%jɴeF$EEQԃ&Dإ&I 06ٕݣl=_;ۙ5uTլٝV;*u OV^zv>:MQG!R4MẆ;o}19ܦ*B]"*؝v\@c:{dIGǗsNF9J]1d, Ao݀esͻ+|5RCQQ6-6Մ$ |W^&:ozti&ՎsUtGmP3U_h/}DƃXkQdjl{v62sF*H+43ױ6iX;F+e ̈9gjsT{H)žus:!3JN!!hJHDFQc澏)EC$b{-=頻\]$)f/}$D`&"#$Ȫye}j޿0dWlֶֶB g{gZ7X0P2;T@THQ9:]t؅AzѣDȜIBb<k2]>Y\|ŽysǿG3Ͻdu޾{?y;Y,/UQ#;9b`کc6^W \]Ofu8ڝ\=rÛW_<+7'{7BIItsSSKǎlZ:ǓImw A@kO#.slR`i\VbzY`fUnnzz*-O׽{PW4F̚zg6uso!PكO.I$"jJMHDC,9{1p.cD@ sGL2#ȑ YY$ &``Čl "F"uU!Bs $5t%bn_KgØ lQhpDscm=X6X_D}k0l(1T5;\s80i2Ӎ]5~m5)SB,But@5BԄ$"3ޣX<>~>9^~YSW{΃w}'ǏOu-@j.lrJ3";GW{M798]wׯ\;4;G4|j}T}Лu-Eb+!ک*A B 33{Y GpEҮJۧeePuzXXyP>]7_S;SZq@=i6}?4^V@2^/5(ZR >xԜ*58T|6=3d,F L>3'd*"$MHbUPjsϿp r><>y|~~H yOޡ&w>{p?mYHI>E(P#I[ChfZ+ːL0B,wDU545bHd)%4S%0W=^?g?O|9{~r~~~],L v=gv|Us(p]WTH\t҂RR.DZC0Dɸb3ԩqǾ"l뒂ԫ}ڹC J'cLW&UFD Mj.mc?o*TvT[ rF*CW_e6T@W4x}:fLz`As3 )1A%@<8Cħ%"!(:(> L 5(V֑.d$"22ŔR&Qq> R׶{W^緿p~~7kGG_ڷR7$Eh 5TITS-.B12z%Fln(#~[m[k[W1XwhRj#QcJOJXRuL>}mA0}qSQ3d_'^4JGLه ^}xWnW>v_%@zs/̲tFpTD5pTjEZ]\ZaߑEW/!5Dv@b$ N % 4&ZFhSP^Yiƌ;fh)X%R^S`$iWR꼥N//] g$Itꔧ1 9CS-krn]߽%՝pa޳h ™"Yɔ' 3!)ɢ e f̈-{:p#>e!,i}i fD13 A9gF?z׮߸yx+ggff1#Bڥ]7; ,.<ےܷmv!J 6dg$@Ϟ huy^kkKm (P3DAQ5 # Ա)*Uq`ݻ8w8~㏾tf߬ݮp'0р hHWI&*1h֣Ҫ>&d-瘬U3#50"6v$&Y%}ESa(%UҁY3b@EV)cI=N]#/]KB!zs}L1!0Zt)dg]vv#wPE\6eM/:9dˇ\8&( @ 0mBic @!A:74U%hkqIDc^z,ʩݔE52 UKG>FU $׮^[?S?~l:'>[oqvz9i8ܕ )&r 48(_iдAiFÜjW6Xև˲l-.],xʍGwLww$vSbY84ʔrXG=PȀ LT&Bޠe@wk:Z1=޻{Wx= ʬ] 4VgvcY(TPvQAZ !JTP >Az2dRAЈHIrPP9Tzd&ƫϙj0VcJG[kM PH1`S5wUXϗ{$F WR :9JuMg(-?ï}{_nP B3UFr)X(u?c8XQC4kR|QfK|.d w`$]`LU>~}x#w|g'N;wMfHiu¨]Ӓ0nmo0g>FX֢Slfmk`mk[닁!0@PH=_ ]My~_.TqW*ͮf—*̧B>#8 K!5L|Z-IW P8fE!1d2N=|G_/Oԏ?yn]98ؙ齯#fTMXdHP22 >'$cЀTSL&]"ֶֶ>d"$eP`#͊ d}y`mk`mk[.? ẙFcњm3k~*ew|qV#"yV*7ɀp dC>Q$*9}ELt>oWn߸rXW;H+Upz0"9_UuS+umrd]CSc ;@%8cD8*I%@ BK*> ڦI$o8T$5c2epvWR|Izh3B9 ->9}7AN+L䅰ul̽g >i/_e;~|_35R 8YsX^rkDLXM$&D$8eKKWSZSGzn*n&vB՞j/6Eȹs#0D{ME!؇5IDS*+Sd1:rbS5HPUĊ9 %8O!9]҅પPI=m貏|5 emji%u(aೋK{v拇MckyߕnMTdNo^nl=u`?j7gzFODYҸLmb&իW@'=UdjUT <+~XO F}Ֆ侭mm}b!# ܵyB & I!oMVڬaLqЫL@LўVrVAX"HH $EU3$ĚW+;oehuz}O5ve]U_7TQ{]pA5r V5uL;53BI&IA@MR6!3";gYcFƹ$&-Dh`^(qܔft½!I0>Zb \4ⲋ+FNLb@N MX)?&wv =4K?bW޸/s{72cɲٲGgݚ"*bm: h 1_9n[=D#0;*8 'O>xp|=zD뺞/{v.xBK3DζzЁ;fHցEF"Ѿdjmm}jTc 58n2eNz ({s&!L?NLd¸;hyꡚɦ8@Hm]P!Qff?c$rTTT;GST{2???|r_7kvɯM3W!LWCv]%&igαΐD0&牂{hR(ș`eZ[SsE3K1)TicfU3F9j) $; U&8\6vmh8_b2"&$웞%ٜ}Xʛ>_?W~oGWnul8.iyƷ)'::c62-#FtS? /ܵjOGK:eJNMo߼}ʕwݽ8ǏMtww7`s $23D`#K3Tc ۇoJ75$*A#dl[k[t5hgcZa#ц?('g6|yњ^( c{7W@9q`pd TT!cDCLaJ<<[pco66V={" )1'rI>A/PU*d{r9d$*WF2F6$@6H@4JBU %QQmS43rՎ"6|v>RBC<&(!V* SՈ!ĮR'l3J/|LDξ~_okW·ZVة"Yv^f3>寴A~{{ OqnԷy1˰8$`Z2%r]Rrvѣ_^z__|\,CUĔkg&:۟P 7F)$ommmmCT9p#ׇUqظWb15,|efDdya>՚彆Gt& ] z= ґ"hbB5UaMx!itA+-w|y~:uwI|r\\ 4Sf5ߵ}&#@Td1 @i/䐀 #5#Rp#)8$֩c H@`θbSLL4 jnRcD~ػlRHˋe]]q=Mm}bV}L UMZ+V?LWٯ~/Od7B6 v7 p6T%+8ǎs1Ba@VpݢjHB5,PD-fƌ HJ;?9}?3?O|*-7.M'}ǔIXv=P ''uQ7&Y6ֶֶ>\`@q8@Sl+ ZxDۈjfO9ӭ@ &1Kl ٣6H^jFx(i3,汋AcOs)EQqnc]h޽xqxiEʡV <D1Y 09 SL1>0;d#pH5<ۗw)"#sAo`ȁ(>T4QbG<ѼT[y> Bh EJ;Mޞ]LB^[jIS%ፃk׮G__?ѝ] :SA".EhC&4X:T dV h1 f$!"* j6E!);`)%UT$pf;/|{}[ʹ MUUUjU#,D*/ 6'щV"q򿐁 c̢wj [;ֶ ֶ!Գʆiyl (mDf8՘$Xc %rHbAp?!3S { 9Gd`@1dJ)iz41<_ܽw՛n\98ؽlbU&}0Ӧjw&Z \U1#* KBPs䉉;IDDU=SK#djDhB!(4 9&pAՈ]_7;vA_Ub!+Nvvb \?Nκ [WY(JvWDbNj8=/+G>?xAw>8ZVJ,MP U+Zۭ65(tܸmSؠ=QbR.dp|$)wqp|~/~%F;6IR>{D2G:~gm ֶ ֶa+~ YC8Ȑ`J-$Y^_^=8ؙvv// JUMUԅvv'6QcrSZ(xv AL-FT4Ι'"4U"$H94`TMb`D*OJĀ(L{"JQث2WzyV?ݙURƘ4yj=A88v0)0kz]\J1]u{ӷ_W>{gUDČ= (LUd݋`e"88l sg 6Oؐ ɲ~D PL MjRb/K3uU}ͦCDe5X0+r xf:f;BC,NM Vֶֶs(e50Cr4 llѨͧ"@ ArXMAǞ[Yc N =QAle:QF0yӸhVH!R3Q fbVPSqy j<60]=:y|6??>}[G7c7׮Myۊf;m3Lf]Cͦ3$:@E#eG"vA>9"F$b"tL\X!#D?U (.ir)PU}퀐ɪfBTY۵q H}hj.&N;引vҜ5]:~/9?>_sC2)!随Ts:2t[uBkơȌ+xD +B:&.rÌKȣCն ֶarL90TA1dk1{12P͛ O`I5,PrE33sH 9BМԦPPHPx^(5 _R`cW0DfISΒ&eoÞMY\e@'o//O޺zѵ+ךK˳IRtٵr3mvY3KLw_eL2&Pl\>`u}j"`D`31WضD)UHL H kJF&u,jޛ yծ3ZJݪDNXvDӊN[ehqET7l,n=?:"\R&0@{r-ʠ]3a(Y9-ځ F JYp' p/f/0e-cvz..V|;ͪ*w{DLL!OX{}1JUOY 5wMPhI$ER}`P]p12@D=9TUX껶]#)IRĘ.W=qaktzq:?r$ 5CKD@c"*`|Մw Q5`*iam|F\pt&ތ8|׃Aǜ+SE2 h4"\J )T*9DSAAP ȲkCtMee䍦&"De #* ޶Y6XϩGO?Yݸe6$(l,@6ް"\e@5^t |#~'46bk6OD5C@""tce߭Ο\b5ww˶55S,/Vx~yrv36M]7M]f2ٙӒzjj-]eWWgEq BV點cujLFvKS$jcD$*}Ҕ!Qm۩sι TR21BFCR\y :rޡc|:WEhm5i\^DWoM\ONA}oF@̞rͨ D#R"”2͍O4 m =pSUuD9d6D313T QUvѵ Du DBFސz s!IGoKF\dWlX>rLR5cf\6ն ֶ!un ,@cM$"ʬkZԃ FF.ˉHϧY17"rٙ6ǯ όEr IV"l0{GN *L+ALꗏb8?88TWWmwy\Η]"-3>kM3ԓz6NLDThߧP;2&bvU#Dcff35Y;D13w2.!k&fu=u7@,]QN"9J8L9_P#{^,h =UiK8`νc40;PpaDByjZ("e"#0CAU72\AT7X,丏$]ںJ5MD*JH.rhLM%c܀idԙ0$6f-G0%15TM2wT!64̌Rdᶶ ֶ)-`W2mLu%`aA; iC?U r}0>mHyb8ێB+2lpd\Zƿ"]-o,/쑝S& a1?#ff&*y)Z*l"7#S'Z@̏|?{޿?ó.:B$B"96 Sk wD'FD> q=mvC17nTuKֶֶb5XÊJh6 n6ac 'V-7ig^+hw  )Z4-ݴ(VlGkva,' #4@ a2UK 26KȘt]t>ztڕ볝zf|ޮzdrΙBvuQLǴ\.//d2igάvyYE@2 6UUQ_;D&1 H.XJ뺮ꚑ ʸ)Q$uEr~ )$7ǚy5bv5{VIFzMgj:˯߼L׿;rU/ 8o %m3U0 u@˝41o®ǻE"y]wJ?s?󹝝민ڝOo|J @DȈ 1[̉M@a{q\vDHC"f.h Xﻚtum@)ۄymGSXo}E %ϤB^'܄o~% 9HUC#DDF$K\FX sã!; 5뒚m6X„OLsP510q)cb$5S9MF 36O.Nܵ+Ww&u}zX]*Py,gXecU 8_4ͥw t34 }LfDUͤ2F^bF>;0QbR+.o:BIMb jTu]SeTU{ٓ1IB-ZKq靳WwG'N[,UpD@ ؘV9̆(_@m7[҇r|ޟzHST7nܘL|]7n~ԹY3ID6lE@$ضr ].jb`zB$Ηe* 3+LMє "GD Leih65) p -glÛYUވdks@!sztiHC @dd(c-a1Y+[XZsDItj)D4T#1U3DGL}Lݿrrzoܸro_G>=]QkAibpQ.E`c!s mmk`mk[8gm Dmi!WDZ:9"bRBd$ #ER"UP$@$@dƁdcD65wFXΧ{mղ~&*Q0TF,H1;8_.\^\xݽlX]*DĞ<#Db.qr>բnꪩ꺞/M 亶SUf;3$j]SsH7CUp#YׯP!xGDD0PCU5U{33eUh56TV@FQ&^eZ{~{>Bץ΀ `#@enQY?oX*Wn/.lկ,gy01@"UwZ7|?c[_7|Olqubj.9;?wяv흽!ju}}z,%(}JmZcJ}LS&MB0>߮̀ j&hb`]UJJ"`JmL8a{BF "p`RDUpIM D -))L!jT*mJ(TMUWB'{r}S}ܹ{|%xj ,@i(>@5;rCS\ΫV{ob}1yp Sf<ژKXNC28e@3T#o)B᲏ ̖ё8MQ><|aI\! -Zـ}hh3(w+Wvv|ޮzE|pމ(3UUP1E3UBbuv9_8]r1}Lfs]7M4hىheY!&S0>ƾDU^DgB]-JITTTs;DZEk yI-AJ=۷~3Γ߁{S6B@#ҪK֬Tٲ]HEȗ M{V5*t%8YS^mնBU3qJ\$ݷo}{Wf_7{۹^x^n1!1lLnzhkZzlt83qNUĮżŽ:F01RSCbf T0#t@>uQQ Ͷ!5U\z>5QuMsr~o?_~tg:>=(@̢#9v@RQ ԣf]-$3;{o iB:"2#q03:"mLn6!ĘRRDG@1*1}7 2!%3Q%μ$b]b*3'^ԋ7-Kh{TL" ;*a2`YBC2;.<ypCB$IT,vVAB jT ) R3B z .w>_׿ݯ޼uv ;Dy,yL3BEԁ ?fhjƆheC2˜amk`mk[ԭ|;߂SAm1N d|pF6dM6#+IvRIK1H-tۘn췊d𔐰D=8#FNN./.?l|>?}\f"9'I1vT-WrVl_^s~6٩:cC Dcvyn()RݪC#FyE1xF)۹IΠLTUFDRmJJDfKobv~-f'^z/=8S@QCeb+xV*K_Q0)"0XCs`cg`ȡvUD (9 iwavHvwg?/W<>CfU.D͔H|P~mU!6R?lk[k[]jYXO-ӽ׺;a6"T "l[/[r!"|fhqev[uF9m|1oOoϟ6^/Z]ֿ :~b fٽÎ9CRClcӋ'O^+GiSUmժ/1FB{!a>"*`'l7R1L|1ڻ@D]r2u]u539q]ao!NE|YL}Ϧ#R3MN$fZžWQOΩHbzPTbuK]mv]/\.w,iDDCU10 5!*YڰWavt\-;Dduo瓟ţ)iT1kԧ>j@$$"L}\۵=9>{Ν޾\335UA1мaqj`b&ڸODH%3gAF@ѩֶֶ ֶaa2FsH2${Im4Xш ]Oxچ!޲ D{632Br <K7D!0JjH`@"BU=r8̄mȆP̏+p"1VFD&Tf|9󝷏No][W\=<ڞ[1zIRR%Ŕ934ˮnIS7-r^_M3ivBL<#sLhj0C 0L)y3.@JIz}j=I69)(:D5E M3>yڍbCʠ~Q2$Gz:<6\d g 8uh_{ޟ1Yeo)%]R5,Hp]^ƨnl>}+_G\9:&fVC19I7N>H::"(Mֶֶ>t~S?da&/Ԁ!7m>46R̊\w_Pが` Ԧ26EЌ@<#:d_5Kx~X-?y[_r4ݪ3PbfL>w|*B̜R @]-5XW:?;w̓tnjc.xmjBTLgj4 })*%yf6M]Pr\؃5  9[Gln'6X2v FXg9Ǿ-<>ܸM)KUw+3y;j%E, )fje$99>_<9ys/W?99~tڶ U>RUתX;'9>ƘjG,cL9wqf;3\I vz~ᝫft2u'rfYUfhfIk{!hbҔ4YO"靈jŢ]\N5ۗ^_~t~v!: ((Q䍣Gob/a"gN%o0$" @RJH\#ECsHTEu]Gl!v5V†pa^m!3^6=J( Q I>T: mlIҗD̋Y:/8#l84gȨl4FcfDV͈ȼ'ݏa+ ܧ QRBHB:9č1**a3̂=zу?[ݽyƍ[ѣˋjoaSբ$"clfQ+)<3EHe3Qxfv*!iHH, =lyqhq$|mZk+u0~/R+(DτX\Q70{sjJwF}N-?-K}ObxJĤ`9&][QboEW3U":*͕XUugTS)|_=|G~o+o=u>zQ+mFbl::ڎC` 3~m7f i͘YKaaG4S}\iϬcONOONNU7cJ Yhz] 4do{Ͱ92ݺ]dw^+d0!gIՅNB&1" iڍn@a66F$D\UD7b"#̕zL *ʾq|5ԇf[  ! ?G5ID" V"+je"&.,eS5whǴq'Ri.`2 ytSR*,"Nἐ-b,q=5ru[}3y%$D*  BXJǣD@b ֜ p( +IffR@`ab>_/v޹{vyjoDDʑ8&JEDI$N`q(j_U*}vpVZJQi"Bp> 0 ֧ؓ&I`IAGM}Wk_(4<;p#&t3jףHW2CJf&rk.fYkm.U%IhEc0RU7I>濤u%̀;/G-"Mˀx^Q䨵{u 8(mGG}w_};^;=qzvۏ_<㋋}PxQ(+cHaf&u6$RJQBfHDpe^, 'nz~~5PBy܊8$4way;g.02O.^7nAe)HEX)Bʈm ",`&['7L242(#*`v̉6Wc0\$3)/;gMrH+5aL2wJj9EHH$5,.ZY,*7CoaՉ[GSM*fHCEV5s"213p f.p2b"gGBC:Ol<;̣; Vqk90  ' FܴX>OX {vRPB‡t(`.BevKz'{rRU#-5B z|ZcFDDYV}VmZ,ef^m˽HѢsl?ݞlZ]<>v{rT Lvsr:dvՋ'Wg?movlheBLa~LN&FUYwb(ԆB%t YwE_/{88w0CYBi!*HHd+ ͜H9X%,! 0wCˣK&Uj-9YAl`0HE @h4%l^Jys5ETu_މ|{|rr2Pmv|JmĞ/Ę.3:DGuo_?.ag*\ȍ'& gB⼉{%f{eybgG"ѫ-[#QhpuYn g r*nx@U:#z‚g'lN5?-xzBDD$S>A0`kEۉ $*[d 6^4kdxDM0 +cqu9lQmby+XB3`,G$fe/Nb*;9oh1bkpbq> 'bp>o-8Ԣ ×?}v "ڭ}ׅ$X$XRr!㗱04U9WW#m|/pD-U"S3wbDXM nL›>K_zw!b/f ]xd#BĵS$Aw^3wHqiӔhՎȈ [WKD7V֨g0 z1fԈzwst wC2Āg7vhosHHw II"xM{&i]Š{T8Gϗg!UUU!hJ$`%/BNۼj*}x=G]s:M%);C x [D 0,…PL[[t$q b| ` *`ZjmK4_-:2tZ2jD,ڛ73g0B7j:2dgsQb*"nWWWUGSꑨy̢d s S 0R2o|Zc`52 "!vwwDVke0 ,EԔ&M20}WYjNz g(eSql~)tuꉄ4?K{7 V<\=O=,Gaҹjo S 6@ScO4֯HNBg $Xwͮhe@M+"|FZgE蚃s9TziqzX-;/ulb" V"?^ e܌u~~4Ax /JVFĬȒZr^E$ ZWzYtEj^&B<|BمDdVVT DE/pk=7hwkkC{/aADj~ZTATV7 F~ D eb)BCD*L Ca;JZe+ 1yI VSw'ref'J\] M@*"f0 |3G<`ȅ ZU:b?2&z3co`NQrJ5;e Z1Ђ9f=T"YըtR9_+}>{q`*+x b3Cr݋λ&jW @^L{wruCcJcNE8 FQJ>|V96܊br:.ATa<-eq{1>AO k4#h>RA$n*! tiq L1DA0W\ qa*`WUAk #8QJQsU qzfpgޓHh@B>nTɉlaݎdĦL0ĄyORMKˆ$^4@dߌ5:fϮz _r8zY_zMEj qxJKy6 b[S }Oqݗч,ʢ*$ܺ}{G5++xY0ì͛7q|鍳v "fMWrYڻ}Q VRHD֪CH@m[v!JkC`+ ն0<{(X <}-Ci(B{%rn?PZ# lDS6]8WnaS5 nj1QgfVV0`}Bv!M="~sJ`,R~a{Bu3sf*>]Ged^jf޸M3c`QcZ] z*R;򻫬D>cId8Ce5 h?&9JY R["L x2 >~awyj癘JjE/k9HpHT04 )0`Z 3D&fN@Dh; n1'}3-x捈PL=d+n RFQ?S m˅@ҽ0@f[PL<ۚW-e1ۺ/ u+3~3/wrwv!ѓLOɯ=Z <8ア@?`Rqvڳ0d+KBfUWY~X,_Bca5z,):fPUivW^}g?W_wyh./ED" V"*0ei(T,Gۣ7oܿSE|qk%Ǡf`Ue&sjD$Rxe0ZD"Ezo̴g =S%"̃v6{I"ZQTC(%"CG\⧿a$Bn∄|-EDbyjVaB|w .8"VQ Ygs/ePJ!"3 iں|'f6Mxm f*_Dhy&(mѩbU+ #sPP=ϻ ^PՃOdI,H s533)Eᨵ; Vȩ"8B ًkc|뜮+xk2[U!\sdU37s4uԎJ**hҫ B[isp3p+J),mDwo~ko?NoZ.aInwWUj:|rvzwˋˋqaf܅6fZ"GK&8p)#Sa󉰈EyĤQ;0uë͘ĩdJ낑Hd{Ö@ pk.nB.*Et\DRX +/QUq0D_QYY^Sfz-xQYy͠؀JV?xǓ H\ cbZ[L{|2>:h͢U(ҐZEGj3Ih4s v18݀fkrՎl^ k]QP[-Y-$}KT'جw|mxýbEh->J)G'>ggg~ͨZVB$X Gdy~u;?|W7>.a^UXM!bbEN4!4MpUNĪZʰa 6FhfԢ儗i;*I,ZyjADKRJղ)[k)צ+0g G1vznZ06Oʠ- <`&A\$J< IZW\8Z*b|0x15|'kyEԃjOFN,?ȀplכK{sF8٫a;>1dv\K䐃V#lXE0Rn'&_:P Y|B՞*+xf U9nivŏ?vޠֹV&>4Kk;s#"ai?Zyys hRq,Hi؋t,yd̀sx7@ƅwQVZV ,"E5:qbOo-h?yW?naO1|]Q\%qP`vnGSPRH@e67VM~"+JCUqAY|.;wY8nXt 늞1l;Ql3lNH"Ȼ~Me&MHvSidJ-0۵ E :x7xݻN|xvt ?>d\׏CҬӒ)C>wrאZZqb!/!Op|'q,/loɪңdFOg6NsH:RRS}~S-}m5E&SG$(oΥniJ->Q Qaa!-"a[ YTW;D`% q|`E&kx/:P^%Pu5:XD>ԝ$U @?Aryt9+܏a7",V8hFkdi6j~%`%/ūcs.}'Z?ׁ\OKjGE=iu׎h5'R;K?V#<|cWy}KxƙDD"H$HoD"H$$XD"H$ID"H$`%D"H$`%D"H$J$D"HH$D"HH$D"+H$D" V"H$D" V"H$DD"H$$XD"H$ID"H$ID"H$`%D"H$J$D"H$J$D"HH$D"+H$D"+H$D" V"H$DD"H$DD"H$$XD"H$ID"H$`%D"H$`%D"H$J$D"HH$D"HH$D"+H$D" V"H$D" V"H$DD"H$$XD"H$$XD"H$ID"H$`%D"H$J$D"H|]__ߘ=_U=}IA%\ [W~^؁zUKz9?_>Dz;df[o}~un|Ջ_˶,E繿 {-enϳ|5}'36ۿggW/?w}?vnxuuo,?S۸(^k߿OYr7ZrϳWYJ~ttG>>rսey9g{g9>Ͻ?y4~_\]]'Mg9_/z77xE=|?quvo|< gO?>~/~Vfs۷q<99uVx?ۻMg=_vk)YG+[<>< # # This program is free software; you can redistribute it # and/or modify it under the terms of the GNU General # Public License as published by the Free Software Foundation; # either version 2, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # INCLUDE(FindPackageHandleStandardArgs) FIND_PROGRAM(GCOVR_EXECUTABLE gcovr HINTS ${GCOVR_ROOT} "${GCOVR_ROOT}/bin") FIND_PACKAGE_HANDLE_STANDARD_ARGS(gcovr DEFAULT_MSG GCOVR_EXECUTABLE) # only visible in advanced view MARK_AS_ADVANCED(GCOVR_EXECUTABLE) ./cmake/ParseArguments.cmake0000644000004100000410000000340613004613604016265 0ustar www-datawww-data# Parse arguments passed to a function into several lists separated by # upper-case identifiers and options that do not have an associated list e.g.: # # SET(arguments # hello OPTION3 world # LIST3 foo bar # OPTION2 # LIST1 fuz baz # ) # PARSE_ARGUMENTS(ARG "LIST1;LIST2;LIST3" "OPTION1;OPTION2;OPTION3" ${arguments}) # # results in 7 distinct variables: # * ARG_DEFAULT_ARGS: hello;world # * ARG_LIST1: fuz;baz # * ARG_LIST2: # * ARG_LIST3: foo;bar # * ARG_OPTION1: FALSE # * ARG_OPTION2: TRUE # * ARG_OPTION3: TRUE # # taken from http://www.cmake.org/Wiki/CMakeMacroParseArguments MACRO(PARSE_ARGUMENTS prefix arg_names option_names) SET(DEFAULT_ARGS) FOREACH(arg_name ${arg_names}) SET(${prefix}_${arg_name}) ENDFOREACH(arg_name) FOREACH(option ${option_names}) SET(${prefix}_${option} FALSE) ENDFOREACH(option) SET(current_arg_name DEFAULT_ARGS) SET(current_arg_list) FOREACH(arg ${ARGN}) SET(larg_names ${arg_names}) LIST(FIND larg_names "${arg}" is_arg_name) IF (is_arg_name GREATER -1) SET(${prefix}_${current_arg_name} ${current_arg_list}) SET(current_arg_name ${arg}) SET(current_arg_list) ELSE (is_arg_name GREATER -1) SET(loption_names ${option_names}) LIST(FIND loption_names "${arg}" is_option) IF (is_option GREATER -1) SET(${prefix}_${arg} TRUE) ELSE (is_option GREATER -1) SET(current_arg_list ${current_arg_list} ${arg}) ENDIF (is_option GREATER -1) ENDIF (is_arg_name GREATER -1) ENDFOREACH(arg) SET(${prefix}_${current_arg_name} ${current_arg_list}) ENDMACRO(PARSE_ARGUMENTS) ./cmake/FindLcov.cmake0000644000004100000410000000172013004613604015026 0ustar www-datawww-data# - Find lcov # Will define: # # LCOV_EXECUTABLE - the lcov binary # GENHTML_EXECUTABLE - the genhtml executable # # Copyright (C) 2010 by Johannes Wienke # # This program is free software; you can redistribute it # and/or modify it under the terms of the GNU General # Public License as published by the Free Software Foundation; # either version 2, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # INCLUDE(FindPackageHandleStandardArgs) FIND_PROGRAM(LCOV_EXECUTABLE lcov) FIND_PROGRAM(GENHTML_EXECUTABLE genhtml) FIND_PACKAGE_HANDLE_STANDARD_ARGS(Lcov DEFAULT_MSG LCOV_EXECUTABLE GENHTML_EXECUTABLE) # only visible in advanced view MARK_AS_ADVANCED(LCOV_EXECUTABLE GENHTML_EXECUTABLE) ./cmake/qt5.cmake0000644000004100000410000000277213004613604014043 0ustar www-datawww-data# shamelessly copied over from oxide’s build system # to enable ARM cross compilation if(CMAKE_CROSSCOMPILING) # QT_MOC_EXECUTABLE is set by Qt5CoreConfigExtras, but it sets it to # the target executable rather than the host executable, which is no # use for cross-compiling. For cross-compiling, we have a guess and # override it ourselves if(NOT TARGET Qt5::moc) find_program( QT_MOC_EXECUTABLE moc PATHS /usr/lib/qt5/bin /usr/lib/${HOST_ARCHITECTURE}/qt5/bin NO_DEFAULT_PATH) if(QT_MOC_EXECUTABLE STREQUAL "QT_MOC_EXECUTABLE-NOTFOUND") message(FATAL_ERROR "Can't find a moc executable for the host arch") endif() add_executable(Qt5::moc IMPORTED) set_target_properties(Qt5::moc PROPERTIES IMPORTED_LOCATION "${QT_MOC_EXECUTABLE}") endif() # Dummy targets - not used anywhere, but this stops Qt5CoreConfigExtras.cmake # from creating them and checking if the binary exists, which is broken when # cross-building because it checks for the target system binary. We need the # host system binaries installed, because they are in the same package as the # moc in Ubuntu (qtbase5-dev-tools), which is not currently multi-arch if(NOT TARGET Qt5::qmake) add_executable(Qt5::qmake IMPORTED) endif() if(NOT TARGET Qt5::rcc) add_executable(Qt5::rcc IMPORTED) endif() if(NOT TARGET Qt5::uic) add_executable(Qt5::uic IMPORTED) endif() else() # This should be enough to initialize QT_MOC_EXECUTABLE find_package(Qt5Core) endif() ./cmake/ubuntu-arm-linux-gnueabihf.cmake0000644000004100000410000000150113004613604020501 0ustar www-datawww-data# shamelessly copied over from oxide’s build system # to enable ARM cross compilation set(CMAKE_SYSTEM_NAME Linux CACHE INTERNAL "") find_program(_DPKG_ARCH_EXECUTABLE dpkg-architecture) if(_DPKG_ARCH_EXECUTABLE STREQUAL "DPKG_ARCHITECTURE_EXECUTABLE-NOTFOUND") message(FATAL_ERROR "dpkg-architecture not found") endif() execute_process(COMMAND ${_DPKG_ARCH_EXECUTABLE} -qDEB_BUILD_GNU_TYPE RESULT_VARIABLE _RESULT OUTPUT_VARIABLE HOST_ARCHITECTURE OUTPUT_STRIP_TRAILING_WHITESPACE) if(NOT _RESULT EQUAL 0) message(FATAL_ERROR "Failed to determine host architecture") endif() set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc CACHE INTERNAL "") set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++ CACHE INTERNAL "") set(CMAKE_LIBRARY_ARCHITECTURE arm-linux-gnueabihf CACHE INTERNAL "") ./cmake/EnableCoverageReport.cmake0000644000004100000410000001535013004613604017364 0ustar www-datawww-data# - Creates a special coverage build type and target on GCC. # # Defines a function ENABLE_COVERAGE_REPORT which generates the coverage target # for selected targets. Optional arguments to this function are used to filter # unwanted results using globbing expressions. Moreover targets with tests for # the source code can be specified to trigger regenerating the report if the # test has changed # # ENABLE_COVERAGE_REPORT(TARGETS target... [FILTER filter...] [TESTS test targets...]) # # To generate a coverage report first build the project with # CMAKE_BUILD_TYPE=coverage, then call make test and afterwards make coverage. # # The coverage report is based on gcov. Depending on the availability of lcov # a HTML report will be generated and/or an XML report of gcovr is found. # The generated coverage target executes all found solutions. Special targets # exist to create e.g. only the xml report: coverage-xml. # # Copyright (C) 2010 by Johannes Wienke # # This program is free software; you can redistribute it # and/or modify it under the terms of the GNU General # Public License as published by the Free Software Foundation; # either version 2, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # INCLUDE(ParseArguments) FIND_PACKAGE(Lcov) FIND_PACKAGE(gcovr) FUNCTION(ENABLE_COVERAGE_REPORT) # argument parsing PARSE_ARGUMENTS(ARG "FILTER;TARGETS;TESTS;EXCLUDES" "" ${ARGN}) SET(COVERAGE_RAW_FILE "${CMAKE_BINARY_DIR}/coverage.raw.info") SET(COVERAGE_FILTERED_FILE "${CMAKE_BINARY_DIR}/coverage.info") SET(COVERAGE_REPORT_DIR "${CMAKE_BINARY_DIR}/coveragereport") SET(COVERAGE_XML_FILE "${CMAKE_BINARY_DIR}/coverage.xml") SET(COVERAGE_XML_COMMAND_FILE "${CMAKE_BINARY_DIR}/coverage-xml.cmake") # decide if there is any tool to create coverage data SET(TOOL_FOUND FALSE) IF(LCOV_FOUND OR GCOVR_FOUND) SET(TOOL_FOUND TRUE) ENDIF() IF(NOT TOOL_FOUND) MESSAGE(STATUS "Cannot enable coverage targets because neither lcov nor gcovr are found.") ENDIF() STRING(TOLOWER "${CMAKE_BUILD_TYPE}" COVERAGE_BUILD_TYPE) IF(CMAKE_COMPILER_IS_GNUCXX AND TOOL_FOUND AND "${COVERAGE_BUILD_TYPE}" MATCHES "coverage") MESSAGE(STATUS "Coverage support enabled for targets: ${ARG_TARGETS}") # create coverage build type SET(CMAKE_CXX_FLAGS_COVERAGE ${CMAKE_CXX_FLAGS_DEBUG} PARENT_SCOPE) SET(CMAKE_C_FLAGS_COVERAGE ${CMAKE_C_FLAGS_DEBUG} PARENT_SCOPE) SET(CMAKE_CONFIGURATION_TYPES ${CMAKE_CONFIGURATION_TYPES} coverage PARENT_SCOPE) # instrument targets SET_TARGET_PROPERTIES(${ARG_TARGETS} PROPERTIES COMPILE_FLAGS --coverage LINK_FLAGS --coverage) # html report IF (LCOV_FOUND) MESSAGE(STATUS "Enabling HTML coverage report") # set up coverage target ADD_CUSTOM_COMMAND(OUTPUT ${COVERAGE_RAW_FILE} COMMAND ${LCOV_EXECUTABLE} -c -d ${CMAKE_BINARY_DIR} -o ${COVERAGE_RAW_FILE} WORKING_DIRECTORY ${CMAKE_BINARY_DIR} COMMENT "Collecting coverage data" DEPENDS ${ARG_TARGETS} ${ARG_TESTS} VERBATIM) # filter unwanted stuff LIST(LENGTH ARG_FILTER FILTER_LENGTH) IF(${FILTER_LENGTH} GREATER 0) SET(FILTER COMMAND ${LCOV_EXECUTABLE}) FOREACH(F ${ARG_FILTER}) SET(FILTER ${FILTER} -r ${COVERAGE_FILTERED_FILE} ${F}) ENDFOREACH() SET(FILTER ${FILTER} -o ${COVERAGE_FILTERED_FILE}) ELSE() SET(FILTER "") ENDIF() ADD_CUSTOM_COMMAND(OUTPUT ${COVERAGE_FILTERED_FILE} COMMAND ${LCOV_EXECUTABLE} -e ${COVERAGE_RAW_FILE} "${CMAKE_SOURCE_DIR}*" -o ${COVERAGE_FILTERED_FILE} ${FILTER} DEPENDS ${COVERAGE_RAW_FILE} COMMENT "Filtering recorded coverage data for project-relevant entries" VERBATIM) ADD_CUSTOM_COMMAND(OUTPUT ${COVERAGE_REPORT_DIR} COMMAND ${CMAKE_COMMAND} -E make_directory ${COVERAGE_REPORT_DIR} COMMAND ${GENHTML_EXECUTABLE} --legend --show-details -t "${PROJECT_NAME} test coverage" -o ${COVERAGE_REPORT_DIR} ${COVERAGE_FILTERED_FILE} DEPENDS ${COVERAGE_FILTERED_FILE} COMMENT "Generating HTML coverage report in ${COVERAGE_REPORT_DIR}" VERBATIM) ADD_CUSTOM_TARGET(coverage-html DEPENDS ${COVERAGE_REPORT_DIR}) ENDIF() # xml coverage report IF(GCOVR_FOUND) MESSAGE(STATUS "Enabling XML coverage report") # gcovr cannot write directly to a file so the execution needs to # be wrapped in a cmake file that generates the file output FILE(WRITE ${COVERAGE_XML_COMMAND_FILE} "SET(ENV{LANG} en)\n") FILE(APPEND ${COVERAGE_XML_COMMAND_FILE} "EXECUTE_PROCESS(COMMAND \"${GCOVR_EXECUTABLE}\" -x -e \"${ARG_EXCLUDES}\" -r \"${CMAKE_SOURCE_DIR}\" OUTPUT_FILE \"${COVERAGE_XML_FILE}\" WORKING_DIRECTORY \"${CMAKE_BINARY_DIR}\")\n") ADD_CUSTOM_COMMAND(OUTPUT ${COVERAGE_XML_FILE} COMMAND ${CMAKE_COMMAND} ARGS -P ${COVERAGE_XML_COMMAND_FILE} COMMENT "Generating coverage XML report" VERBATIM) ADD_CUSTOM_TARGET(coverage-xml DEPENDS ${COVERAGE_XML_FILE}) ENDIF() # provide a global coverage target executing both steps if available SET(GLOBAL_DEPENDS "") IF(LCOV_FOUND) LIST(APPEND GLOBAL_DEPENDS ${COVERAGE_REPORT_DIR}) ENDIF() IF(GCOVR_FOUND) LIST(APPEND GLOBAL_DEPENDS ${COVERAGE_XML_FILE}) ENDIF() IF(LCOV_FOUND OR GCOVR_FOUND) ADD_CUSTOM_TARGET(coverage DEPENDS ${GLOBAL_DEPENDS}) ENDIF() ENDIF() ENDFUNCTION() ./webbrowser-app.png0000644000004100000410000004002113004613604014702 0ustar www-datawww-dataPNG  IHDR?1sBITO pHYsbb8ztEXtSoftwarewww.inkscape.org<?IDATx{d}s~NwtϛHI$KErKr6""#.vN0`A Xl'@Hq;YVezÙLtsyQVǹ[Źlpn~{~_!@ 9ox:CzC?3D\x0BNa) oDMS0 !̀wuC.j+|>n,= xqǘ~WҏTbUJ0r$x&Iaҏã?JJ?(In O\`FƝ#i@ G\CR]ҏc:~HK?hWO3aӏ%ҏa'T PcZmPa$CCVGV0?ybwmsSG?et~̟~J%Ɓ~?hWl sM`8U@D<_haBnm"ˮ_pCsM KNM4|Vz,O]J?fgk ))As(Q~́FFDF Lz>7q44K;f{m3J?$#=Tee?A6P(Pfm.ɦ{?vIx!;HmEK}ǫJNcg9r?xH|Џ#-;0Ɓ~ p,)>^v#P3\;y9S$ \9B>?GQ aGo=yfպ~NW0^:i~$[wG2Iؘ&Y];ZN;wD12Ot4Zb$;k'ĥ `$B7"yv 5̎!tamaа\S)q5<ڃCztǁ"eH晊~JG\R0Buc76(-'0K* AinDxa d?f$ .t+G2OYEdDpAgPI݇$7o\8WBO<å6à@q׊v;Q"簹[&\U Fra_J HH&]Uo6"|ʜ&?jwMI#yFtϙeIe5V0{~;Źv1]vMS\}*wĢͺ$ʽg+M41<_3s3)E΃.Pj@ O.NxzP.W<=je|y~Lf?Z,'Ҥ-W^enӏU C g" % kP1=6u~oK#?`9oL̲5걁9I2 ɜ*y<$|gi‰߱Y9LH28T4fUW4Vݻla_:]IJ?x*!$_1Y=jEӗf\hb {`X2UqA/&~+Wä?r G/ r\)D[4}x $@r'DRç $LAdc+ 3,ぢm50UӍL? AؐV( <K3 x'h'kkQqr5h=*~74CV=d<0ET7mvkmYSzq|BcV*'iяYlsl`W晎~賁Aa'u2яҏi]s+7;kCDž~έT`k ?1?훦ߺbm8f~Jdpnu35ĭY@^{w Qӌ?٧mr5J뽜R76 }¹}y*zJ1Qr(Go[) ׷Z(q?%ʰwb-CO2>7@ڍ ~]X{J`yzΟXN~>@9hԲMJ?~>]bny>?,_!G/$f| (e<<pEdH1~0%~hcM?~븿yI=$Q$ڃvWHO'zwMEcL?V(~$nڞ'2Ox_Bp~gawbhCF3>!gÐx1\9鏭O=>c8qHC1Pg #82a2qu%omo%|#Z+& \/FxO;@O\[P:1d&a448W]0Sv< H}T=W_\""9oȞ$+ !s('9-3'k+K$X~/)" "ۜ?| yBӕjyޑ!wO8wg\k׼m4m{kS\hK=nYo\ӽo=*llw-JDJx>vo&j~/δ=c?k, .ub"h{6peu:T󌢟HtZչͶ,7ߋ}4?|n;и>:vzO &AxЯdg;5?=dn:@d<''Cd=lX{w[;:4Ro|3C)e*@wcb'2Utçү3~'}|"邎kzk6?ci\Z.O42\97nM>2ֽ3'*Fm*_hlߤ3;-*:з=׌\nܠ=AXC\)+E(}Բy܌x}@!ѝV[˶Ղl׷gܱXӻ?_F]}oKMx"NJo9su`~ ;9яxYc.xCu`cװ^Bwyj rߵ6~c+\#e663۟90 k}sqw wS` ?g~K=ܮU}"ť{<4?$I*}w.ZB,&aà_M :金52'oY$oG~ݫhP9s(* yuC̕LRgaЏ ] A(W5O垓cudلx;˯eoc4ډ?u*B0B[29wZ1'<} V__<I^ky>+Wk#*gЭ/ޥC'c O#ps;åSzE t'"#owOA㏟}={\uv1gY3rDM^IW7臬S[)د0y~^pT=~zAZUHlB<"csCy:x7Гa3h% h~ZtnO_jhP ~[|vR+T._?mE3d`mlqF_Z`WgD dz B(GÕ"NW/#'y@" T..[,Ӧ*L 4ӕͶ=޽~N[vGz1~UGY :|2bH(A5ۄx,rWUnmw x^$|s7(!ѥwdNh^zҔ$g>4g/NkD$^{~!b]]oo]u[Eҿ }>.CaKǢW4 x%-+^\'Li'q$|ٙ¥3SdiBU S^ϟi}n׾ycMC8Bqf33qƬ~z \ Xk{vB?Bl<=BlKso,DRUga J멛ii/;Zw_H* ޙ>'s;zgf(@xl e?O<ӏeq⃃[Gr{g$_3'>K 阦{9u c˿UG\G9fO; Opf7? "0 ̲~b^D_vnulCpG\]o>a ~iyHH>_™*k[4f[_{kVqcn/M阚`NY^6A0ָ-?~@wK8Io'/Lfӄ)e?[G_z,Pdoy*to-[QڗLHHӾyԔW1cyt8dI"tl|Z߿+g+F'z4h4g:_1yCҤLycq"[XU= 9@Y臾mRGJ?~u{H$_䱿?]ft:0t9;=/~MHk[c_] .xGt И~E `kgJn.G$ /v /Z!wA}4W.~)@ +'J{XCӏN/饟%'2A_8[&&FH!jZwR󯟸ʹάbjz@?1Gm*͵N7\M4S{ot4V9oK6`_w`5qtxx@6rvg?QWVz\d]Sz~g2|Q/W>锔~_.zAC~' v tDtӯ DM4;輱z?}SVgהV{c_vyinAVs"]}ܠ=7ZřE;k%_@*HFAt\=4](31jld=$ihL/]46So519=w?\T;YXF"+u4ptyZV {keyACV9J&H_P$9żYCf_{O3}qMqtZ#O y*xc,A[8P:g3᠚^\5K$?DsiV?!3z>gi\\9SUG /,}F;輹zt gL靰ִ"OmqKFv~t cxtܗqn0, c= "t'\Cloj~DAͺ9 i,~o++HL4/ɹ)=/_2Oѱ!CƬ &\ .8s'c?`~ջ}6P: HguS͓7$5a}3cW)!*.zI{UƁLZlI_xyør66˄R+Wdw]F,X1+ytz~-3)g?]"Iv[=˄zKqB *دw\C͍P*Oz*gK ?n"}OqG"C_ߴw2; uڭëgánmCDwqY%%ߗ|락竚*t8ƙ϶SבJ}8 'E@8,cݽ6~,~߶vvkT{A@;N KB*Oo{{-2DRV4T0< 'ܨ1vagҔ~KVĝhf@ıלf 3"Y.x'`B{F$L lb$Wk(r 9C$]a0dN.ݞ}`6(W&Q XV,}shOƆ}ݕjmCWϐs8":S 4.z(+~7HxshH&$ ZoB:£ӕ= tG;/疽6qcjG ߍl( uD@lI5vDGS>r؝X?29W/x5jeҷ͖:Xw)f|_<ָtv*Y,`R8 |\Ɲ#˓SNW0 CRRrH>LʳbU=jɝ2ѯT4Ff~/ GTkժ.iW=wN^gN% ™N[6e~CO ,NzbkPمSU.K"{28GaAOL?鏫IoDɯ:.}>ypA.8*!'dCCy $\W J(dn۝$vB0\_lOL$s?e4SΟ[ Ӡ_b^_\\vs1ݪjvH?W ]St򥅀4 shdI}aMuk82v9^)\v;'~ N.̋~";QےG'RF1!f}dvIPƬ%HsO?3a'N@)JDLE#DBOH.miZ!6D "~#7gP J<,'=V,:r3U0,t:%>鱽|~ /Պ#s?\2џ@9 u |i$;4mooo zs;?Crw $'晻#;/7>z;b06x!Oq2X׳;0a${߮N{wt1-kJ^9ԶaJp&c>6yB q5w1\}LLr!"쌣|[uM+MӒt` r|  &"fʽg ԁ0 .%zN_Ds#c]ە cc&1B/$Iʏ?s4xk5h!=-_λȫLhc!Gd]6 s<1k-LO_]$ [ہY|sss,Ӥ6״*"Gg:&ll$c$^?y&fy "i#? ɨrt]'OoXhZqȘaoۀ{[!@YV1ӈ"GO{z@f 2o~w:ҫӜkۻamQSEC2C8,Wax+54?o{?RJ)GMSmU>"\VurCM\gBClo@iYHxOI}`ҏã_ez'i@ĪTw-T*NgccT)_WVr;2>-v0[rOӒ&vq0}4W7-9ϑ7O6Jg( `Vzg2DΞBּ/ɗ?YN81555Booo^Z1H-~Ļ,2΂2[N}{~N/a,ĆalmmMOO/--%I7~(JƅL0L?I3яb2L<3 9sf' |$1cl޹sgiiifffh}m))UЋ~yo̹)dK?+E7x$W[Ƨ}5S߷3e?uM^>%r@40_{c^[]Yzg/ՄjյolZ6˘8g#NCܰ\aN*>I czŽwPz@OGjS4Ai鏶)DX" w6Gp(ăpݣ\;IZu3qa#KD̷k\?1p-ZSҙ!8S$h.F']aUE)d*2\!"4bYzoD@WӔ^ۯqf*U@ξ{ ED҉SD0SO.A<0;Xq5"EΘ~ ## {s+yeKw76w$Z sa5ʜ: sYnahuc/;Tc?{3fm` MDkyF}}8۾?H]h}˅VM MG.c*:U#<ÀӗO5 DD6닋ءwh$Aw厖xt†Io/6[@E |IP\}t zV`:5i::eh}lQ4k̺~^28¾?Y:/Ɣ~{iʢ'i$QI߿n\S^e\0[qjoH~3bDNyVz}|ILj%?ps Ǹ[`Y̘~hVkNBT*L17 @vQK:~g="[ _%j%9+ҏB)@wsE;Ϙ@闼4ZG7[77Lj"U_}D$-Ѷ\_JeVZ4{~? LzQ<N,'MJw?&*i!tfD"yolt_n[ n0'ES핝~ 1 Zj+6U׆@;9,Fk[wnuh΅`{-%mm!1xЋGdү&V}8hudwLJHPs0Sݜx/,3ı#;{yZ9,zѷ>h}h - Ld|臼,Ձ.ɗ~LL?7O 3Q owGsQVͽҰ>HZOG6!ӟpۈrwc迷cf? e4-}O>a\5#%0~tcHW,w +Q yԖ~aP~ȕ~LNp>2~&9t2;NU )oAkbZљ؋з-eB! ?r<1 [2S)vРjf+ZUk!Cз<-izDaeIOe {yE{!яYo[/eN}v oRVk:VUd>%aZ7+̏wL?~8lQ.'PNR>P~,9'A/|pueBL B@oa+2`}[o;?TaRh7A#&K.tSB)ZFzBշRUܷq"Z2&įm)~,~P% y3T?ډHNB75uO?Ѣ4nl2Rp.^sgH$׷ Hy2I?$?}DhF/|unUo54I…ƹַG|tT<01! 1,!G[39ۨbmKhsMWf< з| QKw<藒١mdq#@ e1}? >! #ác臠c/KmӟQ\ o2O% ?32F[>4PK/LÈdE@y*~)}CE?2OPTB~L"rVaP橜aȕ~z[䌪 zXVyH?/q{1! \$,-P)d#a5Z$TJD? ,)̳H9\+T b-LO? ݒ*rN&C$L&st.(s8?ye/DXdøHY-~Sk$2|s,Dz?!2O!l",/3etdwOEΐ0l,T'dq~zDW/H晌~Xشg$<3qdПx$ %) ,Rg,="G2/JN=J?~LmwH)OYRGɖyuC晈~Mnot-n5 ]HLo0e2O%EaA2O|$)+L0["g(c/T'$d!D. """webapp_container autopilot tests and emulators - top level package.""" ./tests/autopilot/webapp_container/tests/0000755000004100000410000000000013004613623021105 5ustar www-datawww-data./tests/autopilot/webapp_container/tests/test_app_launch.py0000644000004100000410000000743613004613604024641 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # Copyright 2014-2015 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from contextlib import contextmanager import os import tempfile import shutil from testtools.matchers import Equals from autopilot.matchers import Eventually from webapp_container.tests import WebappContainerTestCaseWithLocalContentBase @contextmanager def generate_temp_webapp(): tmpdir = tempfile.mkdtemp() webapp_folder_name = '{}/unity-webapps-test'.format(tmpdir) os.mkdir(webapp_folder_name) manifest_content = """ { "includes": ["http://test.com:*/*"], "name": "test", "scripts": ["test.user.js"], "domain":"", "homepage":"http://www.test.com/" } """ manifest_file = "{}/manifest.json".format(webapp_folder_name) with open(manifest_file, "w+") as f: f.write(manifest_content) script_file = "{}/test.user.js".format(webapp_folder_name) with open(script_file, "w+") as f: f.write("") try: yield tmpdir finally: shutil.rmtree(tmpdir) class WebappContainerAppLaunchTestCase( WebappContainerTestCaseWithLocalContentBase): def test_container_does_not_load_with_no_webapp_name_and_url(self): args = [] self.launch_webcontainer_app(args) self.assertIsNone(self.get_webcontainer_proxy()) def test_loads_with_url(self): args = ['--enable-addressbar'] self.launch_webcontainer_app_with_local_http_server(args) window = self.get_webcontainer_window() self.assertThat(window.url, Eventually(Equals(self.url))) def test_local_app_with_webapps_model(self): args = ['--webappModelSearchPath=.', './index.html'] self.launch_webcontainer_app( args, {'WEBAPP_CONTAINER_SHOULD_NOT_VALIDATE_CLI_URLS': '1'}) self.assertIsNone(self.get_webcontainer_proxy()) def test_local_app_with_webapp_name(self): args = ['--webapp=DEADBEEF', './index.html'] self.launch_webcontainer_app( args, {'WEBAPP_CONTAINER_SHOULD_NOT_VALIDATE_CLI_URLS': '1'}) self.assertIsNone(self.get_webcontainer_proxy()) def test_local_app_with_urls_patterns(self): args = ['--webappUrlPatterns=https?://*.blabla.com/*', './index.html'] self.launch_webcontainer_app( args, {'WEBAPP_CONTAINER_SHOULD_NOT_VALIDATE_CLI_URLS': '1'}) self.assertIsNone(self.get_webcontainer_proxy()) def test_webapps_launch_default_search_path(self): args = ["--webapp='dGVzdA=='"] rule = 'MAP *.test.com:80 ' + self.get_base_url_hostname() with generate_temp_webapp() as webapp_install_path: env_vars = { 'UBUNTU_WEBVIEW_HOST_MAPPING_RULES': rule, 'WEBAPP_QML_DEFAULT_WEBAPPS_INSTALL_FOLDER': ( webapp_install_path) } self.launch_webcontainer_app(args, env_vars) webview = self.get_oxide_webview() webapp_url = 'http://www.test.com/' self.assertThat(webview.url, Eventually(Equals(webapp_url))) result = 'test' self.assertThat(self.get_webcontainer_window().title, Eventually(Equals(result))) ./tests/autopilot/webapp_container/tests/test_media_permission.py0000644000004100000410000000522613004613604026051 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # Copyright 2016 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import testtools from webapp_container.tests import WebappContainerTestCaseWithLocalContentBase from testtools.matchers import Equals, GreaterThan from autopilot.matchers import Eventually class TestMediaPermission(WebappContainerTestCaseWithLocalContentBase): def _click_window_open(self): webview = self.get_oxide_webview() gr = webview.globalRect self.pointing_device.move( gr.x + webview.width*3/4, gr.y + webview.height*3/4) self.pointing_device.click() @testtools.skip("Skipping due to the lack of HTTPS support in the " "test suite, see https://launchpad.net/bugs/1505995") def test_access_media_from_main_view(self): args = [] self.launch_webcontainer_app_with_local_http_server( args, '/media-access') self.get_webcontainer_window().visible.wait_for(True) self.app.wait_select_single( objectName="mediaAccessDialog") @testtools.skip("Skipping due to the lack of HTTPS support in the " "test suite, see https://launchpad.net/bugs/1505995") def test_access_media_from_overlay(self): args = [] overlay_link = "/with-overlay-link?path=media-access" self.launch_webcontainer_app_with_local_http_server( args, overlay_link) self.get_webcontainer_window().visible.wait_for(True) popup_controller = self.get_popup_controller() animation_watcher = popup_controller.watch_signal( 'windowOverlayOpenAnimationDone()') animation_signal_emission = animation_watcher.num_emissions self._click_window_open() self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(1))) self.assertThat( lambda: animation_watcher.num_emissions, Eventually(GreaterThan(animation_signal_emission))) self.app.wait_select_single( objectName="mediaAccessDialog") ./tests/autopilot/webapp_container/tests/test_url_patterns.py0000644000004100000410000000636713004613604025253 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # Copyright 2014 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from testtools.matchers import Equals, Contains from autopilot.matchers import Eventually from webapp_container.tests import WebappContainerTestCaseWithLocalContentBase class WebappContainerUrlPatternsTestCase( WebappContainerTestCaseWithLocalContentBase): def test_pattern_with_external_url(self): args = ["--webappUrlPatterns=http://www.test.com/*"] rule = 'MAP *.test.com:80 ' + self.get_base_url_hostname() self.launch_webcontainer_app_with_local_http_server( args, '', {'WEBAPP_CONTAINER_BLOCK_OPEN_URL_EXTERNALLY': '1', 'UBUNTU_WEBVIEW_HOST_MAPPING_RULES': rule}, "http://www.test.com/with-external-link") self.get_webcontainer_window().visible.wait_for(True) webview = self.get_oxide_webview() external_open_watcher = webview.watch_signal( 'openExternalUrlTriggered(QString)') self.pointing_device.click_object(webview) self.assertThat( lambda: external_open_watcher.was_emitted, Eventually(Equals(True))) def test_pattern_with_external_url_in_overlay(self): args = ["--webappUrlPatterns=http://www.test.com/*", "--open-external-url-in-overlay"] rule = 'MAP *.test.com:80 ' + self.get_base_url_hostname() self.launch_webcontainer_app_with_local_http_server( args, '', {'WEBAPP_CONTAINER_BLOCK_OPEN_URL_EXTERNALLY': '1', 'UBUNTU_WEBVIEW_HOST_MAPPING_RULES': rule}, "http://www.test.com/with-external-link") self.get_webcontainer_window().visible.wait_for(True) popup_controller = self.get_popup_controller() new_view_watcher = popup_controller.watch_signal( 'newViewCreated(QString)') views = self.get_popup_overlay_views() self.assertThat(len(views), Equals(0)) webview = self.get_oxide_webview() external_open_watcher = webview.watch_signal( 'openExternalUrlTriggered(QString)') self.pointing_device.click_object(webview) self.assertThat( lambda: new_view_watcher.was_emitted, Eventually(Equals(True))) self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(1))) views = self.get_popup_overlay_views() overlay = views[0] self.assertThat( overlay.select_single(objectName="overlayWebview").url, Contains('ubuntu')) self.assertThat( external_open_watcher.was_emitted, Equals(False)) ./tests/autopilot/webapp_container/tests/test_popup_webview_overlay.py0000644000004100000410000001553713004613623027165 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # Copyright 2015 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from testtools.matchers import Equals, Contains, GreaterThan from autopilot.matchers import Eventually from webapp_container.tests import WebappContainerTestCaseWithLocalContentBase class WebappContainerPopupWebViewOverlayTestCase( WebappContainerTestCaseWithLocalContentBase): def click_href_target_blank(self): webview = self.get_oxide_webview() self.assertThat(webview.url, Contains('/open-close-content')) gr = webview.globalRect self.pointing_device.move( gr.x + gr.width/4, gr.y + gr.height/4) self.pointing_device.click() def click_window_open(self): webview = self.get_oxide_webview() self.assertThat(webview.url.endswith('/open-close-content')) gr = webview.globalRect self.pointing_device.move( gr.x + webview.width*3/4, gr.y + webview.height*3/4) self.pointing_device.click() def test_open_close_back_to_mainview(self): args = [] self.launch_webcontainer_app_with_local_http_server( args, '/open-close-content') self.get_webcontainer_window().visible.wait_for(True) popup_controller = self.get_popup_controller() new_view_watcher = popup_controller.watch_signal( 'newViewCreated(QString)') animation_watcher = popup_controller.watch_signal( 'windowOverlayOpenAnimationDone()') animation_signal_emission = animation_watcher.num_emissions views = self.get_popup_overlay_views() self.assertThat(len(views), Equals(0)) self.click_href_target_blank() self.assertThat( lambda: new_view_watcher.was_emitted, Eventually(Equals(True))) self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(1))) views = self.get_popup_overlay_views() overlay = views[0] self.assertThat( overlay.select_single(objectName="overlayWebview").url, Contains('/open-close-content')) self.assertThat( lambda: animation_watcher.num_emissions, Eventually(GreaterThan(animation_signal_emission))) animation_signal_emission = animation_watcher.num_emissions closeButton = overlay.select_single( objectName='overlayCloseButton') self.pointing_device.click_object(closeButton) self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(0))) def test_open_overlay_in_main_browser(self): args = [] self.launch_webcontainer_app_with_local_http_server( args, '/open-close-content', {'WEBAPP_CONTAINER_BLOCK_OPEN_URL_EXTERNALLY': '1'}) self.get_webcontainer_window().visible.wait_for(True) popup_controller = self.get_popup_controller() webview = self.get_oxide_webview() self.assertThat( lambda: webview.visible, Eventually(Equals(True))) external_open_watcher = popup_controller.watch_signal( 'openExternalUrlTriggered(QString)') animation_watcher = popup_controller.watch_signal( 'windowOverlayOpenAnimationDone()') animation_signal_emission = animation_watcher.num_emissions self.click_href_target_blank() self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(1))) views = self.get_popup_overlay_views() overlay = views[0] self.assertThat( lambda: animation_watcher.num_emissions, Eventually(GreaterThan(animation_signal_emission))) animation_signal_emission = animation_watcher.num_emissions openInBrowserButton = overlay.select_single( objectName='overlayButtonOpenInBrowser') self.pointing_device.click_object(openInBrowserButton) self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(0))) self.assertThat( lambda: external_open_watcher.was_emitted, Eventually(Equals(True))) self.assertThat( lambda: webview.visible, Eventually(Equals(True))) def test_max_overlay_count_reached(self): args = [] self.launch_webcontainer_app_with_local_http_server( args, '/open-close-content', {'WEBAPP_CONTAINER_BLOCK_OPEN_URL_EXTERNALLY': '1'}) self.get_webcontainer_window().visible.wait_for(True) popup_controller = self.get_popup_controller() webview = self.get_oxide_webview() self.assertThat( lambda: webview.visible, Eventually(Equals(True))) animation_watcher = popup_controller.watch_signal( 'windowOverlayOpenAnimationDone()') animation_signal_emission = animation_watcher.num_emissions OVERLAY_MAX_COUNT = 3 for i in range(0, OVERLAY_MAX_COUNT): self.click_href_target_blank() self.assertThat( lambda: animation_watcher.num_emissions, Eventually(GreaterThan(animation_signal_emission))) animation_signal_emission = animation_watcher.num_emissions external_open_watcher = popup_controller.watch_signal( 'openExternalUrlTriggered(QString)') self.click_href_target_blank() self.assertThat( lambda: external_open_watcher.was_emitted, Eventually(Equals(True))) def test_multiple_window_open_from_webview(self): args = [] overlay_opened_from_main_view_count = 3 self.launch_webcontainer_app_with_local_http_server( args, '/timer-window-open-content?count={}'.format( overlay_opened_from_main_view_count), {'WEBAPP_CONTAINER_BLOCKER_DISABLED': '1'}) self.get_webcontainer_window().visible.wait_for(True) webview = self.get_oxide_webview() self.assertThat( lambda: webview.visible, Eventually(Equals(True))) self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(overlay_opened_from_main_view_count))) ./tests/autopilot/webapp_container/tests/__init__.py0000644000004100000410000001357613004613623023232 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # Copyright 2014-2015 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Autopilot tests for the webapp_container package """ import os import signal import subprocess import psutil import fixtures from autopilot.testcase import AutopilotTestCase from autopilot.platform import model from testtools.matchers import Equals, GreaterThan from autopilot.matchers import Eventually import ubuntuuitoolkit as uitk from webapp_container.tests import fake_servers BASE_FILE_PATH = os.path.dirname(os.path.realpath(__file__)) CONTAINER_EXEC_REL_PATH = '../../../../src/app/webcontainer/webapp-container' INSTALLED_BROWSER_CONTAINER_PATH_NAME = 'webapp-container' try: INSTALLED_BROWSER_CONTAINER_PATH_NAME = subprocess.check_output( ['which', 'webapp-container']).strip() except subprocess.CalledProcessError: pass LOCAL_BROWSER_CONTAINER_PATH_NAME = \ os.path.join(BASE_FILE_PATH, CONTAINER_EXEC_REL_PATH) class WebappContainerTestCaseBase(AutopilotTestCase): def setUp(self): self.pointing_device = uitk.get_pointing_device() super(WebappContainerTestCaseBase, self).setUp() def get_webcontainer_app_path(self): if os.path.exists(LOCAL_BROWSER_CONTAINER_PATH_NAME): return LOCAL_BROWSER_CONTAINER_PATH_NAME return INSTALLED_BROWSER_CONTAINER_PATH_NAME def launch_webcontainer_app(self, args, envvars={}): if model() != 'Desktop': args.append( '--desktop_file_hint=/usr/share/applications/' 'webbrowser-app.desktop') if next(filter(lambda e: e.startswith('--appid'), args), None) is None: args.append('--app-id=running.test') if envvars: for envvar_key in envvars: self.useFixture(fixtures.EnvironmentVariable( envvar_key, envvars[envvar_key])) try: self.app = self.launch_test_application( self.get_webcontainer_app_path(), *args, emulator_base=uitk.UbuntuUIToolkitCustomProxyObjectBase) except: self.app = None def get_webcontainer_proxy(self): return self.app def get_webcontainer_window(self): return self.app.select_single(objectName="webappContainer") def get_webcontainer_webview(self): return self.app.select_single(objectName="webappBrowserView") def get_webcontainer_chrome(self): return self.app.select_single("Chrome") def get_webview(self): return self.app.select_single(objectName="webview") def get_popup_overlay_views(self): return self.app.select_many("PopupWindowOverlay") def get_popup_controller(self): return self.app.select_single(objectName="popupController") def get_oxide_webview(self): container = self.get_webview().select_single( objectName='containerWebviewLoader') return container.wait_select_single('WebViewImplOxide') def assert_page_eventually_loaded(self, url): webview = self.get_oxide_webview() self.assertThat(webview.url, Eventually(Equals(url))) # loadProgress == 100 ensures that a page has actually loaded self.assertThat(webview.loadProgress, Eventually(Equals(100), timeout=20)) self.assertThat(webview.loading, Eventually(Equals(False))) def get_scheme_filtered_uri(self, uri): webviewContainer = self.get_webcontainer_window() watcher = webviewContainer.watch_signal( 'schemeUriHandleFilterResult(QString)') previous = watcher.num_emissions webviewContainer.slots.translateHandlerUri(uri) self.assertThat( lambda: watcher.num_emissions, Eventually(GreaterThan(previous))) result = webviewContainer.get_signal_emissions( 'schemeUriHandleFilterResult(QString)')[-1][0] return result def browse_to(self, url): webview = self.get_oxide_webview() webview.url = url self.assert_page_eventually_loaded(url) def kill_app(self, signal=signal.SIGKILL): os.kill(self.app.pid, signal) self.app.process.wait() def kill_web_processes(self, signal=signal.SIGKILL): children = psutil.Process(self.app.pid).children(True) for child in children: if child.name() == 'oxide-renderer': for arg in child.cmdline(): if '--type=renderer' in arg: os.kill(child.pid, signal) break class WebappContainerTestCaseWithLocalContentBase(WebappContainerTestCaseBase): BASE_URL_SCHEME = 'http://' def setUp(self): super(WebappContainerTestCaseWithLocalContentBase, self).setUp() self.http_server = fake_servers.WebappContainerContentHttpServer() self.addCleanup(self.http_server.shutdown) self.base_url = "{}localhost:{}".format( self.BASE_URL_SCHEME, self.http_server.port) def get_base_url_hostname(self): return self.base_url[len(self.BASE_URL_SCHEME):] def launch_webcontainer_app_with_local_http_server( self, args, path='/', envvars={}, homepage=''): self.url = self.base_url + path if len(homepage) != 0: self.url = homepage args.append(self.url) self.launch_webcontainer_app(args, envvars) ./tests/autopilot/webapp_container/tests/test_sad_tab.py0000644000004100000410000001246213004613604024117 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # Copyright 2015 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import signal import time from webapp_container.tests import WebappContainerTestCaseWithLocalContentBase from testtools.matchers import Equals, Contains, GreaterThan from autopilot.matchers import Eventually import ubuntuuitoolkit as uitk class SadPage(uitk.UbuntuUIToolkitCustomProxyObjectBase): def click_reload_button(self): button = self.select_single("Button", objectName="reloadButton") self.pointing_device.click_object(button) class TestSadTab(WebappContainerTestCaseWithLocalContentBase): def _kill_web_process(self): self.kill_web_processes() time.sleep(1) self.assert_page_eventually_loaded(self.url) self.kill_web_processes() def _click_href_target_blank(self): webview = self.get_oxide_webview() self.assertThat(webview.url, Contains('/open-close-content')) gr = webview.globalRect self.pointing_device.move( gr.x + gr.width/4, gr.y + gr.height/4) self.pointing_device.click() def _click_overlay(self): popup_controller = self.get_popup_controller() new_view_watcher = popup_controller.watch_signal( 'newViewCreated(QString)') animation_watcher = popup_controller.watch_signal( 'windowOverlayOpenAnimationDone()') animation_signal_emission = animation_watcher.num_emissions views = self.get_popup_overlay_views() self.assertThat(len(views), Equals(0)) self._click_href_target_blank() self.assertThat( lambda: new_view_watcher.was_emitted, Eventually(Equals(True))) self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(1))) views = self.get_popup_overlay_views() overlay = views[0] self.assertThat( overlay.select_single(objectName="overlayWebview").url, Contains('/open-close-content')) self.assertThat( lambda: animation_watcher.num_emissions, Eventually(GreaterThan(animation_signal_emission))) def test_reload_main_webview_killed(self): self.launch_webcontainer_app_with_local_http_server([]) self.get_webcontainer_window().visible.wait_for(True) self._kill_web_process() sad_webview = self.app.wait_select_single( SadPage, objectName="mainWebviewSadPage") sad_webview.click_reload_button() sad_webview.wait_until_destroyed() self.assert_page_eventually_loaded(self.url) def test_reload_overlay_killed(self): args = [] self.launch_webcontainer_app_with_local_http_server( args, '/open-close-content') self.get_webcontainer_window().visible.wait_for(True) self._click_overlay() self._kill_web_process() sad_webview = self.app.wait_select_single( SadPage, objectName="overlaySadPage") sad_webview.click_reload_button() sad_webview.wait_until_destroyed() views = self.get_popup_overlay_views() overlay = views[0] self.assertThat( lambda: overlay.wait_select_single( objectName="overlayWebview").url, Eventually(Contains('/open-close-content'))) def _crash_web_process(self): self.kill_web_processes(signal.SIGABRT) def test_reload_main_webview_crashed(self): self.launch_webcontainer_app_with_local_http_server([]) self.get_webcontainer_window().visible.wait_for(True) self._crash_web_process() sad_webview = self.app.wait_select_single( SadPage, objectName="mainWebviewSadPage") sad_webview.click_reload_button() sad_webview.wait_until_destroyed() self.assert_page_eventually_loaded(self.url) def test_reload_overlay_crashed(self): args = [] self.launch_webcontainer_app_with_local_http_server( args, '/open-close-content') self.get_webcontainer_window().visible.wait_for(True) self._click_overlay() self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(1))) self._crash_web_process() sad_webview = self.app.wait_select_single( SadPage, objectName="overlaySadPage") sad_webview.click_reload_button() sad_webview.wait_until_destroyed() views = self.get_popup_overlay_views() overlay = views[0] self.assertThat( lambda: overlay.wait_select_single( objectName="overlayWebview").url, Eventually(Contains('/open-close-content'))) ./tests/autopilot/webapp_container/tests/test_overlay_recovery.py0000644000004100000410000001200713004613604026114 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # Copyright 2015 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import signal from webapp_container.tests import WebappContainerTestCaseWithLocalContentBase from testtools.matchers import Equals, Contains, GreaterThan from autopilot.matchers import Eventually class TestOverlayRecovery(WebappContainerTestCaseWithLocalContentBase): def _click_href_target_blank(self): webview = self.get_oxide_webview() self.assertThat(webview.url, Contains('/open-close-content')) gr = webview.globalRect self.pointing_device.move( gr.x + gr.width/4, gr.y + gr.height/4) self.pointing_device.click() def _click_overlay(self): popup_controller = self.get_popup_controller() new_view_watcher = popup_controller.watch_signal( 'newViewCreated(QString)') animation_watcher = popup_controller.watch_signal( 'windowOverlayOpenAnimationDone()') animation_signal_emission = animation_watcher.num_emissions views = self.get_popup_overlay_views() self.assertThat(len(views), Equals(0)) self._click_href_target_blank() self.assertThat( lambda: new_view_watcher.was_emitted, Eventually(Equals(True))) self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(1))) views = self.get_popup_overlay_views() overlay = views[0] self.assertThat( overlay.select_single(objectName="overlayWebview").url, Contains('/open-close-content')) self.assertThat( lambda: animation_watcher.num_emissions, Eventually(GreaterThan(animation_signal_emission))) def test_crash_app_overlay_reloaded(self): args = [] self.launch_webcontainer_app_with_local_http_server( args, '/open-close-content') self.get_webcontainer_window().visible.wait_for(True) self._click_overlay() self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(1))) self.kill_app(signal.SIGABRT) self.launch_webcontainer_app_with_local_http_server( args, '/open-close-content') self.get_webcontainer_window().visible.wait_for(True) self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(1))) views = self.get_popup_overlay_views() overlay = views[0] self.assertThat( lambda: overlay.wait_select_single( objectName="overlayWebview").url, Eventually(Contains('/open-close-content'))) def test_crash_app_closed_overlay_not_reloaded(self): args = [] self.launch_webcontainer_app_with_local_http_server( args, '/open-close-content') self.get_webcontainer_window().visible.wait_for(True) self._click_overlay() self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(1))) views = self.get_popup_overlay_views() overlay = views[0] closeButton = overlay.select_single( objectName='overlayCloseButton') self.pointing_device.click_object(closeButton) self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(0))) self.kill_app(signal.SIGABRT) self.launch_webcontainer_app_with_local_http_server( args, '/open-close-content') self.get_webcontainer_window().visible.wait_for(True) self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(0))) def test_closed_app_overlay_not_reloaded(self): args = [] self.launch_webcontainer_app_with_local_http_server( args, '/open-close-content') self.get_webcontainer_window().visible.wait_for(True) self._click_overlay() self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(1))) self.kill_app(signal.SIGTERM) self.launch_webcontainer_app_with_local_http_server( args, '/open-close-content') self.get_webcontainer_window().visible.wait_for(True) self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(0))) ./tests/autopilot/webapp_container/tests/test_chrome.py0000644000004100000410000000600113004613604023767 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # Copyright 2014-2015 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from testtools.matchers import Equals, GreaterThan from autopilot.matchers import Eventually from webapp_container.tests import WebappContainerTestCaseWithLocalContentBase class WebappContainerChromeSetupTestCase( WebappContainerTestCaseWithLocalContentBase): def test_default_to_chromeless(self): self.launch_webcontainer_app_with_local_http_server([]) self.assertIsNotNone(self.get_webcontainer_proxy()) webview = self.get_webcontainer_webview() self.assertThat(webview.chromeless, Equals(True)) def test_enable_chrome_back_forward(self): args = ['--enable-back-forward'] self.launch_webcontainer_app_with_local_http_server(args) webview = self.get_webcontainer_webview() self.assertThat(webview.chromeless, Equals(False)) chrome = self.get_webcontainer_chrome() self.assertThat(chrome.navigationButtonsVisible, Equals(True)) self.assertThat( self.app.select_single(objectName="reloadButton").visible, Equals(True)) self.assertThat( self.app.select_single(objectName="backButton").visible, Equals(True)) def test_enable_chrome_address_bar(self): args = ['--enable-addressbar'] self.launch_webcontainer_app_with_local_http_server(args) self.assertIsNotNone(self.get_webcontainer_proxy()) webview = self.get_webcontainer_webview() self.assertThat(webview.chromeless, Equals(False)) def test_reload(self): args = ['--enable-back-forward'] self.launch_webcontainer_app_with_local_http_server(args) self.get_webcontainer_window().visible.wait_for(True) self.assert_page_eventually_loaded(self.url) container_view = self.get_webcontainer_webview() self.assertThat(container_view.chromeless, Equals(False)) reload_button = self.app.select_single(objectName="reloadButton") self.assertThat(reload_button.visible, Equals(True)) webview = self.get_oxide_webview() watcher = webview.watch_signal('loadingStateChanged()') previous = watcher.num_emissions self.pointing_device.click_object(reload_button) self.assertThat( lambda: watcher.num_emissions, Eventually(GreaterThan(previous))) self.assertThat(webview.loading, Eventually(Equals(False))) ./tests/autopilot/webapp_container/tests/test_page_meta_collector.py0000644000004100000410000000477013004613604026515 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # Copyright 2015 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from webapp_container.tests import WebappContainerTestCaseWithLocalContentBase from testtools.matchers import Equals from autopilot.matchers import Eventually class TestPageMetaCollector(WebappContainerTestCaseWithLocalContentBase): def test_update_theme_color(self): args = ['--enable-addressbar'] self.launch_webcontainer_app_with_local_http_server( args, '/theme-color/?color=red') self.get_webcontainer_window().visible.wait_for(True) chrome_base = self.app.wait_select_single( objectName="chromeBase") self.assertThat( lambda: str(chrome_base.backgroundColor), Eventually(Equals("Color(255, 0, 0, 255)"))) def test_update_theme_color_with_manifest(self): args = ['--enable-addressbar'] self.launch_webcontainer_app_with_local_http_server( args, '/theme-color/?manifest=true') self.get_webcontainer_window().visible.wait_for(True) chrome_base = self.app.wait_select_single( objectName="chromeBase") self.assertThat( lambda: str(chrome_base.backgroundColor), Eventually(Equals("Color(255, 0, 0, 255)"))) def test_track_theme_color_live_updates(self): args = ['--enable-addressbar'] self.launch_webcontainer_app_with_local_http_server( args, '/theme-color/?color=red&delaycolorupdate=black') self.get_webcontainer_window().visible.wait_for(True) chrome_base = self.app.wait_select_single( objectName="chromeBase") self.assertThat( lambda: str(chrome_base.backgroundColor), Eventually(Equals("Color(255, 0, 0, 255)"))) self.assertThat( lambda: str(chrome_base.backgroundColor), Eventually(Equals("Color(0, 0, 0, 255)"))) ./tests/autopilot/webapp_container/tests/fake_servers.py0000644000004100000410000002474113004613623024146 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # Copyright 2014 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Autopilot tests for the webapp_container package """ import http.server as http import logging import threading import urllib class RequestHandler(http.BaseHTTPRequestHandler): def serve_content(self, content, mime_type='text/html'): self.send_header('Content-type', mime_type) self.end_headers() self.wfile.write(content.encode()) def basic_html_content(self, content="basic"): return """ Some content This is some {} content """.format(content) def redirect_html_content(self): return """ Some content """ def external_click_content(self): return """ Some content """ def external_href_with_link_content(self, path="open-close-content"): return """ Some content """.format(path) def display_ua_content(self): return """ Some content """.format("'"+self.headers['user-agent']+"'") def saml(self, loopcount): return """ open-close
target blank link
""".format(loopcount) def media_access(self): return """ open-close
""" def manifest_json_content(self): return """ { "name": "Theme Color", "short_name": "Theme Color", "icons": [], "theme_color": "#FF0000" } """ def theme_color_content(self, color, with_manifest=False, delayed_color_update=''): color_content = '' if color: color_content = """ """.format(color) manifest_content = '' if with_manifest: manifest_content = "" delayed_color_code = '' if len(delayed_color_update) != 0: delayed_color_code = """ setTimeout(function() { var e=document.head.querySelector('meta[name="theme-color"]'); e.content = '%s'; }, 2000)""" % delayed_color_update return """ {} {} theme-color """.format(color_content, manifest_content, delayed_color_code) def open_close_content(self): return """ open-close
target blank link
Lorem ipsum dolor sit amet
""" def timer_based_window_open_content(self, count): return """ open-close Test """ % count base64_png_data = \ "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAACXBIWXMAAAsTAAALEwE" \ "AmpwYAAAAOUlEQVRYw+3OAQ0AAAgDoGv/zlpDN0hATS7qaGlpaWlpaWlpaWlpaWlpaW" \ "lpaWlpaWlpaWlpab1qLUGqAWNyFWTYAAAAAElFTkSuQmCC" def do_GET(self): if self.path == '/': self.send_response(200) self.serve_content(self.basic_html_content()) elif self.path == '/other': self.send_response(200) self.serve_content(self.basic_html_content("other")) elif self.path == '/get-redirect': self.send_response(200) self.serve_content(self.redirect_html_content()) elif self.path == '/with-external-link': self.send_response(200) self.serve_content(self.external_click_content()) elif self.path == "/image": self.send_response(200) html = '' html += '' html += '' self.serve_content(html) elif self.path == "/imagelink": self.send_response(200) html = '' html += '' html += '' self.serve_content(html) elif self.path == "/textarea": self.send_response(200) html = '' html += '' self.serve_content(html) elif self.path == '/with-targetted-link': self.send_response(200) self.serve_content(self.external_href_with_link_content()) elif self.path == '/show-user-agent': self.send_response(200) self.serve_content(self.display_ua_content()) elif self.path == '/open-close-content': self.send_response(200) self.serve_content(self.open_close_content()) elif self.path == '/theme-color/manifest.json': self.send_response(200) self.serve_content(self.manifest_json_content()) elif self.path.startswith('/theme-color/'): q = urllib.parse.parse_qs( urllib.parse.urlparse( self.path).query) self.send_response(200) color = '' if 'color' in q: color = q['color'][0] color_update = '' if 'delaycolorupdate' in q: color_update = q['delaycolorupdate'][0] with_manifest = False if 'manifest' in q and q['manifest'][0] == 'true': with_manifest = True self.send_response(200) self.serve_content( self.theme_color_content( color, with_manifest, color_update)) elif self.path.startswith('/saml/'): args = self.path[len('/saml/'):] loopCount = 0 if args.startswith('?loopcount='): loopCount = int(args[len('?loopcount='):].split(';')[0]) self.send_response(200) self.serve_content(self.saml(loopCount)) elif self.path.startswith('/redirect-to-saml/'): locationTarget = '/' args = self.path[len('/redirect-to-saml/'):] if args.startswith('?loopcount='): header_size = len('?loopcount=') loopCount = int( args[header_size:args.index('&')].split(';')[0]) if loopCount > 0: loopCount = loopCount - 1 locationTarget += 'redirect-to-saml\ /?loopcount=' + str(loopCount) + '&SAMLRequest=1' self.send_response(302) self.send_header("Location", locationTarget) self.end_headers() elif self.path == '/media-access': self.send_response(200) self.serve_content(self.media_access()) elif self.path.startswith('/with-overlay-link'): qs = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query) self.send_response(200) self.serve_content( self.external_href_with_link_content(qs['path'][0])) elif self.path.startswith('/timer-window-open-content'): qs = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query) count = 1 if 'count' in qs: count = int(qs['count'][0]) self.send_response(200) self.serve_content(self.timer_based_window_open_content(count)) else: self.send_error(404) class WebappContainerContentHttpServer(object): def __init__(self): super(WebappContainerContentHttpServer, self).__init__() self.server = http.HTTPServer(("", 0), RequestHandler) self.server.allow_reuse_address = True self.server_thread = threading.Thread(target=self.server.serve_forever) self.server_thread.start() logging.info("now serving on port {}".format(self.server.server_port)) @property def port(self): return self.server.server_port def run(self): self.server.serve_forever() def shutdown(self): self.server.shutdown() self.server.server_close() self.server_thread.join() ./tests/autopilot/webapp_container/tests/test_scheme_filter.py0000644000004100000410000001530313004613604025330 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # Copyright 2014 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from contextlib import contextmanager import os import tempfile import shutil from testtools.matchers import Equals from autopilot.matchers import Eventually from webapp_container.tests import WebappContainerTestCaseWithLocalContentBase @contextmanager def generate_webapp_with_scheme_filter(scheme_filter_content=""): tmpdir = tempfile.mkdtemp() manifest_content = """ { "includes": ["http://www.test.com/*"], "name": "test", "domain":"", "homepage":"http://www.test.com/" } """ manifest_file = "{}/webapp-properties.json".format(tmpdir) with open(manifest_file, "w+") as f: f.write(manifest_content) if len(scheme_filter_content) != 0: scheme_filter_file = "{}/local-scheme-filter.js".format(tmpdir) with open(scheme_filter_file, "w+") as f: f.write(scheme_filter_content) old_cwd = os.getcwd() try: os.chdir(tmpdir) yield tmpdir finally: os.chdir(old_cwd) shutil.rmtree(tmpdir) # Those tests rely on get_scheme_filtered_uri() which # relies on implementation detail to trigger part of the intent handling # code. This comes from the fact that the url-dispatcher is not easily # instrumentable , so a full feature flow coverage is quite tricky to get. # Those tests are not really functional in that sense. class WebappContainerSchemeFilterTestCase( WebappContainerTestCaseWithLocalContentBase): def test_basic_intent_parsing(self): rule = 'MAP *.test.com:80 ' + self.get_base_url_hostname() with generate_webapp_with_scheme_filter() as webapp_install_path: args = ['--webappModelSearchPath='+webapp_install_path] self.launch_webcontainer_app( args, {'UBUNTU_WEBVIEW_HOST_MAPPING_RULES': rule}) webview = self.get_oxide_webview() webapp_url = 'http://www.test.com/' self.assertThat(webview.url, Eventually(Equals(webapp_url))) intent_uri = 'intent://maps.google.es/maps?ie=utf-8&gl=es\ #Intent;scheme=http;package=com.google.android.apps.maps;end' self.assertThat( 'http://maps.google.es/maps?ie=utf-8&gl=es', Equals(self.get_scheme_filtered_uri(intent_uri))) def test_webapp_with_invalid_default_local_intent(self): rule = 'MAP *.test.com:80 ' + self.get_base_url_hostname() filter = "{ \"intent\": 1 }" with generate_webapp_with_scheme_filter(filter) as webapp_install_path: args = ['--webappModelSearchPath='+webapp_install_path] self.launch_webcontainer_app( args, {'UBUNTU_WEBVIEW_HOST_MAPPING_RULES': rule}) webview = self.get_oxide_webview() webapp_url = 'http://www.test.com/' self.assertThat(webview.url, Eventually(Equals(webapp_url))) intent_uri = 'intent://www.test.com/maps?ie=utf-8&gl=es\ #Intent;scheme=http;package=com.google.android.apps.maps;end' self.assertThat( 'http://www.test.com/maps?ie=utf-8&gl=es', Equals(self.get_scheme_filtered_uri(intent_uri))) def test_with_valid_default_local_intent(self): rule = 'MAP *.test.com:80 ' + self.get_base_url_hostname() filter = "{ \"intent\": \"(function(r) { \ return { \ 'scheme': 'https', \ 'host': 'maps.test.com', \ 'path': r.path }; })\" }" with generate_webapp_with_scheme_filter(filter) as webapp_install_path: args = ['--webappModelSearchPath='+webapp_install_path] self.launch_webcontainer_app( args, {'UBUNTU_WEBVIEW_HOST_MAPPING_RULES': rule}) webview = self.get_oxide_webview() webapp_url = 'http://www.test.com/' self.assertThat(webview.url, Eventually(Equals(webapp_url))) intent_uri = 'intent://www.test.com/maps?ie=utf-8&gl=es\ #Intent;scheme=http;package=com.google.android.apps.maps;end' self.assertThat( 'https://maps.test.com/maps?ie=utf-8&gl=es', Equals(self.get_scheme_filtered_uri(intent_uri))) def test_no_filter_for_http(self): rule = 'MAP *.test.com:80 ' + self.get_base_url_hostname() filter = "{ \"http\": \"(function(r) { \ return { \ 'scheme': 'https', \ 'host': 'maps.test.com', \ 'path': r.path }; })\" }" with generate_webapp_with_scheme_filter(filter) as webapp_install_path: args = ['--webappModelSearchPath='+webapp_install_path] self.launch_webcontainer_app( args, {'UBUNTU_WEBVIEW_HOST_MAPPING_RULES': rule}) webview = self.get_oxide_webview() webapp_url = 'http://www.test.com/' self.assertThat(webview.url, Eventually(Equals(webapp_url))) new_uri = 'http://www.test.com/maps?ie=utf-8&gl=es' self.assertThat( 'http://www.test.com/maps?ie=utf-8&gl=es', Equals(self.get_scheme_filtered_uri(new_uri))) def test_default_scheme_filter(self): rule = 'MAP *.test.com:80 ' + self.get_base_url_hostname() filter = "{ \"mailto\": \"(function(r) { \ return { \ 'scheme': 'https', \ 'host': 'mail.google.com', \ 'path': '?to='+encodeURIComponent(r.path) }; })\" }" with generate_webapp_with_scheme_filter(filter) as webapp_install_path: args = ['--webappModelSearchPath='+webapp_install_path] self.launch_webcontainer_app( args, {'UBUNTU_WEBVIEW_HOST_MAPPING_RULES': rule}) webview = self.get_oxide_webview() webapp_url = 'http://www.test.com/' self.assertThat(webview.url, Eventually(Equals(webapp_url))) scheme_uri = 'mailto:blabla@ubuntu.com' self.assertThat( 'https://mail.google.com/?to=blabla%40ubuntu.com', Equals(self.get_scheme_filtered_uri(scheme_uri))) ./tests/autopilot/webapp_container/tests/test_context_menu.py0000644000004100000410000002643413004613604025236 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Copyright 2015-2016 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import time from autopilot.platform import model from autopilot.matchers import Eventually import testtools from testtools.matchers import Equals, GreaterThan, StartsWith from webapp_container.tests import WebappContainerTestCaseWithLocalContentBase import ubuntuuitoolkit as uitk class ContextMenuBase(uitk.UbuntuUIToolkitCustomProxyObjectBase): def get_title_label(self): return self.select_single(objectName="titleLabel") def get_visible_actions(self): return self.select_many("Empty", visible=True) def get_action(self, objectName): name = objectName + "_item" return self.select_single("Empty", objectName=name) def click_action(self, objectName): name = objectName + "_item" action = self.select_single("Empty", visible=True, enabled=True, objectName=name) self.pointing_device.click_object(action) self.wait_until_destroyed() class ContextMenuWide(ContextMenuBase): pass class ContextMenuMobile(ContextMenuBase): def click_cancel_action(self): action = self.select_single("Empty", objectName="cancelAction") self.pointing_device.click_object(action) class TestContextMenuBase(WebappContainerTestCaseWithLocalContentBase): data_uri_prefix = "data:image/png;base64," def _get_context_menu(self): if self.get_webcontainer_webview().wide: return self.app.wait_select_single( ContextMenuWide, objectName="contextMenuWide") else: return self.app.wait_select_single( ContextMenuMobile, objectName="contextMenuMobile") def _open_context_menu(self, webview): gr = webview.globalRect x = gr.x + webview.width // 2 y = gr.y + webview.height // 2 self.pointing_device.move(x, y) if model() == 'Desktop': self.pointing_device.click(button=3) else: self.pointing_device.press() time.sleep(1.5) self.pointing_device.release() return self._get_context_menu() def _dismiss_context_menu(self, menu): if self.get_webcontainer_webview().wide: # Dismiss by clicking outside of the menu webview_rect = self.get_webview().globalRect actions = menu.get_visible_actions() outside_x = (webview_rect.x + actions[0].globalRect.x) // 2 outside_y = webview_rect.y + webview_rect.height // 2 self.pointing_device.move(outside_x, outside_y) self.pointing_device.click() else: # Dismiss by clicking the cancel action menu.click_cancel_action() menu.wait_until_destroyed() def _click_window_open(self): webview = self.get_oxide_webview() gr = webview.globalRect self.pointing_device.move( gr.x + webview.width*3/4, gr.y + webview.height*3/4) self.pointing_device.click() def _launch_application(self, path): args = [] self.launch_webcontainer_app_with_local_http_server( args, path, {'WEBAPP_CONTAINER_BLOCK_OPEN_URL_EXTERNALLY': '1'}) self.get_webcontainer_window().visible.wait_for(True) def _setup_overlay_webview_context_menu(self, path): overlay_path = "/with-overlay-link?path={}".format(path) self._launch_application(overlay_path) popup_controller = self.get_popup_controller() animation_watcher = popup_controller.watch_signal( 'windowOverlayOpenAnimationDone()') animation_signal_emission = animation_watcher.num_emissions self._click_window_open() self.assertThat( lambda: len(self.get_popup_overlay_views()), Eventually(Equals(1))) self.assertThat( lambda: animation_watcher.num_emissions, Eventually(GreaterThan(animation_signal_emission))) self.webview = self.get_popup_overlay_views()[0].select_single( objectName="overlayWebview") self.menu = self._open_context_menu(self.webview) def _setup_webview_context_menu(self, path): self._launch_application("/{}".format(path)) self.webview = self.get_oxide_webview() self.menu = self._open_context_menu(self.webview) class TestContextMenuLink(TestContextMenuBase): def _test_open_link_(self): signal = self.webview.watch_signal( 'openUrlExternallyRequested(QString)') self.assertThat(signal.was_emitted, Equals(False)) self.menu.click_action("OpenLinkInWebBrowser") self.assertThat(lambda: signal.was_emitted, Eventually(Equals(True))) self.assertThat(signal.num_emissions, Equals(1)) def _test_copy_link(self): self.menu.click_action("CopyLinkContextualAction") @testtools.skipIf(model() == "Desktop", "on devices only") def _test_share_link(self): self.menu.click_action("ShareContextualAction") self.app.wait_select_single("ContentShareDialog") class TestContextMenuLinkOverlayWebView(TestContextMenuLink): def setUp(self): super(TestContextMenuLinkOverlayWebView, self).setUp() self._setup_overlay_webview_context_menu("with-external-link") def test_open_link_(self): self._test_open_link_() def test_copy_link(self): self._test_copy_link() @testtools.skipIf(model() == "Desktop", "on devices only") def test_share_link(self): self._test_share_link() class TestContextMenuLinkMainWebView(TestContextMenuLink): def setUp(self): super(TestContextMenuLinkMainWebView, self).setUp() self._setup_webview_context_menu("with-external-link") def test_open_link_(self): self._test_open_link_() def test_copy_link(self): self._test_copy_link() @testtools.skipIf(model() == "Desktop", "on devices only") def test_share_link(self): self._test_share_link() class TestContextMenuImage(TestContextMenuBase): def _test_copy_image(self): # There is no easy way to test the contents of the clipboard, # but we can at least verify that the context menu was dismissed. self.menu.click_action("CopyImageContextualAction") class TestContextMenuImageMainWebview(TestContextMenuImage): def setUp(self): super(TestContextMenuImageMainWebview, self).setUp() self._setup_webview_context_menu("image") self.assertThat(self.menu.get_title_label().text, StartsWith(self.data_uri_prefix)) def test_copy_image(self): self._test_copy_image() class TestContextMenuImageOverlayWebView(TestContextMenuImage): def setUp(self): super(TestContextMenuImageOverlayWebView, self).setUp() self._setup_overlay_webview_context_menu("image") self.assertThat(self.menu.get_title_label().text, StartsWith(self.data_uri_prefix)) def test_copy_image(self): self._test_copy_image() class TestContextMenuImageAndLink(TestContextMenuBase): def _test_open_link_in_webbrowser(self): signal = self.webview.watch_signal( 'openUrlExternallyRequested(QString)') self.assertThat(signal.was_emitted, Equals(False)) self.menu.click_action("OpenLinkInWebBrowser") self.assertThat(lambda: signal.was_emitted, Eventually(Equals(True))) self.assertThat(signal.num_emissions, Equals(1)) def _test_share_link(self): self.menu.click_action("ShareContextualAction") self.app.wait_select_single("ContentShareDialog") def _test_copy_link(self): # There is no easy way to test the contents of the clipboard, # but we can at least verify that the context menu was dismissed. self.menu.click_action("CopyLinkContextualAction") def _test_copy_image(self): # There is no easy way to test the contents of the clipboard, # but we can at least verify that the context menu was dismissed. self.menu.click_action("CopyImageContextualAction") class TestContextMenuImageAndLinkMainWebView(TestContextMenuImageAndLink): def setUp(self): super(TestContextMenuImageAndLinkMainWebView, self).setUp() self._setup_webview_context_menu("imagelink") self.assertThat(self.menu.get_title_label().text, StartsWith(self.data_uri_prefix)) def test_open_link_in_webbrowser(self): self._test_open_link_in_webbrowser() @testtools.skipIf(model() == "Desktop", "on devices only") def test_share_link(self): self._test_share_link() def test_copy_link(self): self._test_copy_link() def test_copy_image(self): self._test_copy_image() class TestContextMenuImageAndLinkOverlayWebView(TestContextMenuImageAndLink): def setUp(self): super(TestContextMenuImageAndLinkOverlayWebView, self).setUp() self._setup_overlay_webview_context_menu("imagelink") self.assertThat(self.menu.get_title_label().text, StartsWith(self.data_uri_prefix)) def test_open_link_in_webbrowser(self): self._test_open_link_in_webbrowser() @testtools.skipIf(model() == "Desktop", "on devices only") def test_share_link(self): self._test_share_link() def test_copy_link(self): self._test_copy_link() def test_copy_image(self): self._test_copy_image() class TestContextMenuTextArea(TestContextMenuBase): def _test_actions(self): actions = ["SelectAll", "Cut", "Undo", "Redo", "Paste", "SelectAll", "Copy", "Erase"] for action in actions: self.menu.click_action("{}ContextualAction".format(action)) webview = self.get_webview() self.menu = self._open_context_menu(webview) @testtools.skipIf(model() != "Desktop", "on desktop only") class TestContextMenuTextAreaMainWebView(TestContextMenuTextArea): def setUp(self): super(TestContextMenuTextAreaMainWebView, self).setUp() self._setup_webview_context_menu("textarea") self.assertThat(self.menu.get_title_label().visible, Equals(False)) def test_actions(self): self._test_actions() @testtools.skipIf(model() != "Desktop", "on desktop only") class TestContextMenuTextAreaOverlayWebView(TestContextMenuTextArea): def setUp(self): super(TestContextMenuTextAreaOverlayWebView, self).setUp() self._setup_overlay_webview_context_menu("textarea") self.assertThat(self.menu.get_title_label().visible, Equals(False)) def test_actions(self): self._test_actions() ./tests/autopilot/webapp_container/tests/test_user_agent.py0000644000004100000410000000613613004613604024657 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # Copyright 2014 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from contextlib import contextmanager import os import tempfile import shutil from testtools.matchers import Equals from autopilot.matchers import Eventually from webapp_container.tests import WebappContainerTestCaseWithLocalContentBase @contextmanager def generate_temp_local_props_webapp(): tmpdir = tempfile.mkdtemp() webapp_folder_name = '{}/unity-webapps-test'.format(tmpdir) os.mkdir(webapp_folder_name) manifest_content = """ { "includes": ["http://test.com:*/*"], "name": "test", "domain": "", "homepage": "http://www.test.com/show-user-agent", "user-agent-override": "MyUserAgent" } """ manifest_file = "{}/webapp-properties.json".format(webapp_folder_name) with open(manifest_file, "w+") as f: f.write(manifest_content) try: yield webapp_folder_name finally: shutil.rmtree(tmpdir) class WebappUserAgentTestCase( WebappContainerTestCaseWithLocalContentBase): def test_override_user_agent(self): args = ['--user-agent-string=MyUserAgent'] self.launch_webcontainer_app_with_local_http_server( args, '/show-user-agent') self.get_webcontainer_window().visible.wait_for(True) # trick until we get e.g. selenium/chromedriver tests result = 'MyUserAgent MyUserAgent' self.assertThat(self.get_oxide_webview().title, Eventually(Equals(result))) def test_webapp_properties_override(self): rule = 'MAP *.test.com:80 ' + self.get_base_url_hostname() with generate_temp_local_props_webapp() as webapp_install_path: args = ['--webappModelSearchPath=' + webapp_install_path] self.launch_webcontainer_app( args, {'UBUNTU_WEBVIEW_HOST_MAPPING_RULES': rule}) self.get_webcontainer_window().visible.wait_for(True) webview = self.get_oxide_webview() webapp_url = 'http://www.test.com/show-user-agent' self.assertThat(webview.url, Eventually(Equals(webapp_url))) webapp_name = 'test' self.assertThat(self.get_webcontainer_window().title, Eventually(Equals(webapp_name))) # trick until we get e.g. selenium/chromedriver tests result = 'MyUserAgent MyUserAgent' self.assertThat(self.get_oxide_webview().title, Eventually(Equals(result))) ./tests/autopilot/webapp_container/tests/test_saml_url_patterns.py0000644000004100000410000000550713004613623026263 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # Copyright 2014 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from testtools.matchers import Equals, Contains from autopilot.matchers import Eventually from webapp_container.tests import WebappContainerTestCaseWithLocalContentBase class WebappContainerSAMLUrlPatternsTestCase( WebappContainerTestCaseWithLocalContentBase): def test_saml_urls_added(self): rule = 'MAP *.test.com:80 ' + self.get_base_url_hostname() args = ["--webappUrlPatterns=\ http://www.test.com/saml/*,{}/saml/*".format(self.base_url)] expectedSamlRequestRedirectPatternsCount = 1 samlRequestNavigationCount = 3 target_path = '/saml/?\ loopcount={}'.format(str(samlRequestNavigationCount)) self.launch_webcontainer_app_with_local_http_server( args, target_path, {'WEBAPP_CONTAINER_BLOCK_OPEN_URL_EXTERNALLY': '1', 'UBUNTU_WEBVIEW_HOST_MAPPING_RULES': rule}) self.get_webcontainer_window().visible.wait_for(True) self.assert_page_eventually_loaded(self.base_url+target_path) saml_url_navigations_detected_watcher = self.get_webview( ).watch_signal('samlRequestUrlPatternReceived(QString)') webcontainer_webview = self.get_webcontainer_webview() url_patterns_settings_watcher = webcontainer_webview.watch_signal( 'generatedUrlPatternsChanged()') webview = self.get_oxide_webview() gr = webview.globalRect self.pointing_device.move( gr.x + webview.width*0.5, gr.y + webview.height*0.5) self.pointing_device.click() self.assertThat( lambda: url_patterns_settings_watcher.was_emitted, Eventually(Equals(True))) self.assertThat( lambda: url_patterns_settings_watcher.num_emissions, Eventually(Equals(expectedSamlRequestRedirectPatternsCount))) self.assertThat( lambda: saml_url_navigations_detected_watcher.num_emissions, Eventually(Equals(samlRequestNavigationCount+1))) saved_patterns = webcontainer_webview.generatedUrlPatterns self.assertThat( saved_patterns, Contains("\"https?://{}/*\"".format(self.get_base_url_hostname()))) ./tests/autopilot/webapp_container/tests/test_webapp_name_precedence.py0000644000004100000410000000646613004613604027164 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # Copyright 2015 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from contextlib import contextmanager import os import tempfile import shutil from testtools.matchers import Equals from autopilot.matchers import Eventually from webapp_container.tests import WebappContainerTestCaseWithLocalContentBase @contextmanager def generate_temp_webapp(manifest_filename='manifest.json'): tmpdir = tempfile.mkdtemp() webapp_folder_name = '{}/unity-webapps-test'.format(tmpdir) os.mkdir(webapp_folder_name) manifest_content = """ { "includes": ["http://test.com:*/*"], "name": "test", "scripts": ["test.user.js"], "domain":"", "homepage":"http://www.test.com/" } """ manifest_file = "{}/{}".format(webapp_folder_name, manifest_filename) with open(manifest_file, "w+") as f: f.write(manifest_content) script_file = "{}/test.user.js".format(webapp_folder_name) with open(script_file, "w+") as f: f.write("") try: yield tmpdir finally: shutil.rmtree(tmpdir) class WebappContainerWebappNamePrecedenceTestCase( WebappContainerTestCaseWithLocalContentBase): def test_webapps_launch_default_search_path(self): args = ["--webapp='dGVzdA=='"] rule = 'MAP *.test.com:80 ' + self.get_base_url_hostname() with generate_temp_webapp() as webapp_install_path: self.launch_webcontainer_app( args, {'UBUNTU_WEBVIEW_HOST_MAPPING_RULES': rule, 'WEBAPP_QML_DEFAULT_WEBAPPS_INSTALL_FOLDER': webapp_install_path}) webview = self.get_oxide_webview() webapp_url = 'http://www.test.com/' self.assertThat(webview.url, Eventually(Equals(webapp_url))) result = 'test' self.assertThat(self.get_webcontainer_window().title, Eventually(Equals(result))) def test_webapps_launch_custom_search_path(self): args = [] rule = 'MAP *.test.com:80 ' + self.get_base_url_hostname() with generate_temp_webapp('webapp-properties.json') as install_path: args.append('--webappModelSearchPath={}'.format(install_path)) self.launch_webcontainer_app( args, {'UBUNTU_WEBVIEW_HOST_MAPPING_RULES': rule, 'WEBAPP_QML_DEFAULT_WEBAPPS_INSTALL_FOLDER': install_path}) webview = self.get_oxide_webview() webapp_url = 'http://www.test.com/' self.assertThat(webview.url, Eventually(Equals(webapp_url))) result = 'test' self.assertThat(self.get_webcontainer_window().title, Eventually(Equals(result))) ./tests/autopilot/webbrowser_app/0000755000004100000410000000000013004613604017443 5ustar www-datawww-data./tests/autopilot/webbrowser_app/emulators/0000755000004100000410000000000013004613605021457 5ustar www-datawww-data./tests/autopilot/webbrowser_app/emulators/browser.py0000644000004100000410000006511313004613604023521 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Copyright 2013-2016 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import logging import time import autopilot.logging import ubuntuuitoolkit as uitk from autopilot import exceptions from autopilot import input from autopilot.platform import model logger = logging.getLogger(__name__) class Browser(uitk.UbuntuUIToolkitCustomProxyObjectBase): def __init__(self, *args): super().__init__(*args) self.chrome = self._get_chrome() self.address_bar = self.chrome.address_bar self.keyboard = input.Keyboard.create() def _get_chrome(self): return self.select_single(Chrome) def go_to_url(self, url): self.address_bar.go_to_url(url) def wait_until_page_loaded(self, url): webview = self.wait_select_single("WebViewImpl", current=True, url=url) # loadProgress == 100 ensures that a page has actually loaded webview.loadProgress.wait_for(100, timeout=20) webview.loading.wait_for(False) def go_back(self): self.chrome.go_back() def go_forward(self): self.chrome.go_forward() @autopilot.logging.log_action(logger.info) def enter_private_mode(self): if not self.is_in_private_mode(): self.chrome.toggle_private_mode() else: logger.warning('The browser is already in private mode.') def is_in_private_mode(self): return self.get_current_webview().incognito @autopilot.logging.log_action(logger.info) def leave_private_mode(self): if self.is_in_private_mode(): self.chrome.toggle_private_mode() else: logger.warning('The browser is not in private mode.') @autopilot.logging.log_action(logger.info) def leave_private_mode_with_confirmation(self, confirm=True): if self.is_in_private_mode(): self.chrome.toggle_private_mode() dialog = self._get_leave_private_mode_dialog() if confirm: dialog.confirm() else: dialog.cancel() dialog.wait_until_destroyed() else: logger.warning('The browser is not in private mode.') def _get_leave_private_mode_dialog(self): return self.wait_select_single(LeavePrivateModeDialog, visible=True) # Since the NewPrivateTabView does not define any new QML property in its # extended file, it does not report itself to autopilot with the same name # as the extended file. (See http://pad.lv/1454394) def is_new_private_tab_view_visible(self): try: self.get_new_private_tab_view() return True except exceptions.StateNotFoundError: return False def get_window(self): return self.get_parent() def get_current_webview(self): return self.select_single("WebViewImpl", current=True) def get_webviews(self): return self.select_many("WebViewImpl", incognito=False) def get_incognito_webviews(self): return self.select_many("WebViewImpl", incognito=True) def get_error_sheet(self): return self.select_single("ErrorSheet") def get_sad_tab(self): return self.wait_select_single(SadTab) def get_suggestions(self): return self.select_single(Suggestions) def get_geolocation_dialog(self): return self.wait_select_single(GeolocationPermissionRequest) def get_http_auth_dialog(self): return self.wait_select_single(HttpAuthenticationDialog) def get_media_access_dialog(self): return self.wait_select_single(MediaAccessDialog) def get_tabs_view(self): return self.wait_select_single(TabsList, visible=True) def get_recent_view_toolbar(self): return self.wait_select_single(Toolbar, objectName="recentToolbar", state="shown") def get_new_tab_view(self): if self.wide: return self.wait_select_single("NewTabViewWide", visible=True) else: return self.wait_select_single("NewTabView", visible=True) # Since the NewPrivateTabView does not define any new QML property in its # extended file, it does not report itself to autopilot with the same name # as the extended file. (See http://pad.lv/1454394) def get_new_private_tab_view(self): return self.wait_select_single("QQuickItem", objectName="newPrivateTabView", visible=True) def get_settings_page(self): return self.wait_select_single(SettingsPage, visible=True) def get_downloads_page(self): return self.wait_select_single(DownloadsPage, visible=True) def get_content_picker_dialog(self): # only on devices return self.wait_select_single("PopupBase", objectName="contentPickerDialog") def get_download_dialog(self): return self.wait_select_single("PopupBase", objectName="downloadDialog") def get_peer_picker(self): return self.wait_select_single(objectName="contentPeerPicker") def get_download_options_dialog(self): return self.wait_select_single("Dialog", objectName="downloadOptionsDialog") def click_cancel_download_button(self): button = self.select_single("Button", objectName="cancelDownloadButton") self.pointing_device.click_object(button) def click_choose_app_button(self): button = self.select_single("Button", objectName="chooseAppButton") self.pointing_device.click_object(button) def click_download_file_button(self): button = self.select_single("Button", objectName="downloadFileButton") self.pointing_device.click_object(button) def get_bottom_edge_handle(self): return self.select_single(objectName="bottomEdgeHandle") def get_bottom_edge_bar(self): return self.select_single(objectName="bottomEdgeBar", visible=True) def get_bookmark_options(self): return self.select_single(BookmarkOptions) def get_new_bookmarks_folder_dialog(self): return self.wait_select_single("Dialog", objectName="newFolderDialog") # The bookmarks view is dynamically created, so it might or might not be # available def get_bookmarks_view(self): try: if self.wide: return self.select_single("BookmarksViewWide") else: return self.select_single("BookmarksView") except exceptions.StateNotFoundError: return None # The history view is dynamically created, so it might or might not be # available def get_history_view(self): try: if self.wide: return self.select_single(HistoryViewWide) else: return self.select_single(HistoryView) except exceptions.StateNotFoundError: return None def get_expanded_history_view(self): return self.wait_select_single(ExpandedHistoryView, visible=True) def press_key(self, key): self.keyboard.press_and_release(key) def get_context_menu(self): if self.wide: return self.wait_select_single(ContextMenuWide) else: return self.wait_select_single(ContextMenuMobile) def open_item_context_menu_on_item(self, item, menuClass): cx = item.globalRect.x + item.globalRect.width // 2 cy = item.globalRect.y + item.globalRect.height // 2 self.pointing_device.move(cx, cy) if model() == 'Desktop': self.pointing_device.click(button=3) else: self.pointing_device.press() time.sleep(1.5) self.pointing_device.release() return self.wait_select_single(menuClass) def open_context_menu(self): webview = self.get_current_webview() chrome = self.chrome x = webview.globalRect.x + webview.globalRect.width // 2 y = webview.globalRect.y + \ (webview.globalRect.height + chrome.height) // 2 self.pointing_device.move(x, y) if model() == 'Desktop': self.pointing_device.click(button=3) else: self.pointing_device.press() time.sleep(1.5) self.pointing_device.release() return self.get_context_menu() def dismiss_context_menu(self, menu): if self.wide: # Dismiss by clicking outside of the menu webview_rect = self.get_current_webview().globalRect actions = menu.get_visible_actions() outside_x = (webview_rect.x + actions[0].globalRect.x) // 2 outside_y = webview_rect.y + webview_rect.height // 2 self.pointing_device.move(outside_x, outside_y) self.pointing_device.click() else: # Dismiss by clicking the cancel action menu.click_cancel_action() menu.wait_until_destroyed() class Chrome(uitk.UbuntuUIToolkitCustomProxyObjectBase): def __init__(self, *args): super().__init__(*args) self.address_bar = self._get_address_bar() def _get_address_bar(self): return self.select_single(AddressBar) @autopilot.logging.log_action(logger.info) def go_back(self): back_button = self._get_back_button() back_button.enabled.wait_for(True) self.pointing_device.click_object(back_button) def _get_back_button(self): return self.select_single("ChromeButton", objectName="backButton") def is_back_button_enabled(self): back_button = self._get_back_button() return back_button.enabled @autopilot.logging.log_action(logger.info) def go_forward(self): forward_button = self._get_forward_button() forward_button.enabled.wait_for(True) self.pointing_device.click_object(forward_button) def _get_forward_button(self): return self.select_single("ChromeButton", objectName="forwardButton") def is_forward_button_enabled(self): forward_button = self._get_forward_button() return forward_button.enabled def toggle_private_mode(self): drawer_button = self.get_drawer_button() self.pointing_device.click_object(drawer_button) self.get_drawer() private_mode_action = self.get_drawer_action("privatemode") self.pointing_device.click_object(private_mode_action) def get_drawer_button(self): return self.select_single("ChromeButton", objectName="drawerButton") def get_drawer(self): return self.wait_select_single("QQuickItem", objectName="drawer", clip=False) def get_drawer_action(self, actionName): drawer = self.get_drawer() return drawer.select_single(objectName=actionName, visible=True) def get_tabs_bar(self): return self.select_single(TabsBar) def get_find_next_button(self): return self.select_single("ChromeButton", objectName="findNextButton") def get_find_prev_button(self): return self.select_single("ChromeButton", objectName="findPreviousButton") class AddressBar(uitk.UbuntuUIToolkitCustomProxyObjectBase): def __init__(self, *args): super().__init__(*args) self.text_field = self.select_single( uitk.TextField, objectName='addressBarTextField') @autopilot.logging.log_action(logger.debug) def focus(self): self.pointing_device.click_object(self) self.activeFocus.wait_for(True) def clear(self): self.text_field.clear() @autopilot.logging.log_action(logger.info) def go_to_url(self, url): self.write(url) self.press_key('Enter') def write(self, text, clear=True): self.text_field.write(text, clear) def press_key(self, key): self.text_field.keyboard.press_and_release(key) @autopilot.logging.log_action(logger.info) def click_action_button(self): button = self.select_single("QQuickMouseArea", objectName="actionButton") self.pointing_device.click_object(button) def get_bookmark_toggle(self): return self.select_single("QQuickMouseArea", objectName="bookmarkToggle") def get_find_in_page_counter(self): return self.select_single(objectName="findInPageCounter") class TabsBar(uitk.UbuntuUIToolkitCustomProxyObjectBase): @autopilot.logging.log_action(logger.info) def click_new_tab_button(self): button = self.select_single("QQuickMouseArea", objectName="newTabButton") self.pointing_device.click_object(button) def get_tabs(self): return self.select_many("QQuickMouseArea", objectName="tabDelegate") def get_tab(self, index): return self.select_single("QQuickMouseArea", objectName="tabDelegate", tabIndex=index) @autopilot.logging.log_action(logger.info) def select_tab(self, index): self.pointing_device.click_object(self.get_tab(index)) @autopilot.logging.log_action(logger.info) def close_tab(self, index): tab = self.get_tab(index) close_button = tab.select_single(objectName="closeButton") self.pointing_device.click_object(close_button) class Suggestions(uitk.UbuntuUIToolkitCustomProxyObjectBase): def get_ordered_entries(self): return sorted(self.select_many("Suggestion"), key=lambda item: item.globalRect.y) class SadTab(uitk.UbuntuUIToolkitCustomProxyObjectBase): @autopilot.logging.log_action(logger.info) def click_close_tab_button(self): button = self.select_single("Button", objectName="closeTabButton") self.pointing_device.click_object(button) @autopilot.logging.log_action(logger.info) def click_reload_button(self): button = self.select_single("Button", objectName="reloadButton") self.pointing_device.click_object(button) class GeolocationPermissionRequest(uitk.UbuntuUIToolkitCustomProxyObjectBase): def get_deny_button(self): return self.select_single("Button", objectName="deny") def get_allow_button(self): return self.select_single("Button", objectName="allow") class HttpAuthenticationDialog(uitk.UbuntuUIToolkitCustomProxyObjectBase): def get_deny_button(self): return self.select_single("Button", objectName="deny") def get_allow_button(self): return self.select_single("Button", objectName="allow") def get_username_field(self): return self.select_single("TextField", objectName="username") def get_password_field(self): return self.select_single("TextField", objectName="password") class MediaAccessDialog(uitk.UbuntuUIToolkitCustomProxyObjectBase): @autopilot.logging.log_action(logger.info) def click_deny_button(self): button = self.select_single("Button", objectName="mediaAccessDialog.denyButton") self.pointing_device.click_object(button) @autopilot.logging.log_action(logger.info) def click_allow_button(self): button = self.select_single("Button", objectName="mediaAccessDialog.allowButton") self.pointing_device.click_object(button) class TabPreview(uitk.UbuntuUIToolkitCustomProxyObjectBase): @autopilot.logging.log_action(logger.info) def select(self): area = self.select_single("QQuickMouseArea", objectName="selectArea") # click towards the top of the area to ensure we’re not selecting # the following preview that might be overlapping ca = area.globalRect self.pointing_device.move(ca.x + ca.width // 2, ca.y + ca.height // 4) self.pointing_device.click() @autopilot.logging.log_action(logger.info) def close(self): button = self.select_single(objectName="closeButton") self.pointing_device.click_object(button) class TabsList(uitk.UbuntuUIToolkitCustomProxyObjectBase): def get_previews(self): previews = self.select_many(TabPreview) previews.sort(key=lambda tab: tab.globalRect.y) return previews class Toolbar(uitk.UbuntuUIToolkitCustomProxyObjectBase): @autopilot.logging.log_action(logger.info) def click_button(self, name): self.isFullyShown.wait_for(True) button = self.select_single("Button", objectName=name) self.pointing_device.click_object(button) @autopilot.logging.log_action(logger.info) def click_action(self, name): self.isFullyShown.wait_for(True) action = self.select_single("ToolbarAction", objectName=name) self.pointing_device.click_object(action) class SettingsPage(uitk.UbuntuUIToolkitCustomProxyObjectBase): def get_header(self): return self.select_single(BrowserPageHeader) def get_searchengine_entry(self): return self.select_single(objectName="searchengine") def get_searchengine_page(self): return self.wait_select_single(objectName="searchEnginePage") def get_homepage_entry(self): return self.select_single(objectName="homepage") def get_restore_session_entry(self): return self.select_single(objectName="restoreSession") def get_privacy_entry(self): return self.select_single(objectName="privacy") def get_privacy_page(self): return self.wait_select_single(objectName="privacySettings") def get_reset_settings_entry(self): return self.select_single(objectName="reset") class DownloadsPage(uitk.UbuntuUIToolkitCustomProxyObjectBase): def get_header(self): return self.select_single(BrowserPageHeader) class BrowserPageHeader(uitk.UbuntuUIToolkitCustomProxyObjectBase): @autopilot.logging.log_action(logger.info) def click_back_button(self): button = self.select_single(objectName="backButton") self.pointing_device.click_object(button) class HistoryView(uitk.UbuntuUIToolkitCustomProxyObjectBase): def get_domain_entries(self): return sorted(self.select_many("UrlDelegate"), key=lambda item: item.globalRect.y) class HistoryViewWide(uitk.UbuntuUIToolkitCustomProxyObjectBase): def get_entries(self): return sorted(self.select_many("UrlDelegate"), key=lambda item: item.globalRect.y) def get_search_field(self): return self.select_single(objectName="searchQuery") class ExpandedHistoryView(uitk.UbuntuUIToolkitCustomProxyObjectBase): def get_header(self): return self.select_single(objectName="header") def get_entries(self): return sorted(self.select_many("UrlDelegate", objectName="entriesDelegate"), key=lambda item: item.globalRect.y) class LeavePrivateModeDialog(uitk.Dialog): @autopilot.logging.log_action(logger.info) def confirm(self): confirm_button = self.select_single( "Button", objectName="leavePrivateModeDialog.okButton") self.pointing_device.click_object(confirm_button) @autopilot.logging.log_action(logger.info) def cancel(self): cancel_button = self.select_single( "Button", objectName="leavePrivateModeDialog.cancelButton") self.pointing_device.click_object(cancel_button) class NewTabView(uitk.UbuntuUIToolkitCustomProxyObjectBase): def get_bookmarks_more_button(self): return self.select_single("Button", objectName="bookmarks.moreButton") def get_homepage_bookmark(self): return self.select_single(UrlDelegate, objectName="homepageBookmark") def get_bookmarks_list(self): return self.select_single(objectName="bookmarksList") def get_bookmark_delegates(self): list = self.get_bookmarks_list() return sorted(list.select_many(UrlDelegate), key=lambda delegate: delegate.globalRect.y) def get_top_sites_list(self): return self.select_single(UrlPreviewGrid, objectName="topSitesList") def get_notopsites_label(self): return self.select_single(objectName="notopsites") def get_top_site_items(self): return self.get_top_sites_list().get_delegates() def get_bookmarks_folder_list_view(self): return self.wait_select_single(BookmarksFoldersView) def get_bookmarks(self, folder_name): # assumes that the "more" button has been clicked folders = self.get_bookmarks_folder_list_view() folder_delegate = folders.get_folder_delegate(folder_name) return folders.get_urls_from_folder(folder_delegate) def get_folder_names(self): folders = self.get_bookmarks_folder_list_view().get_delegates() return [folder.folderName for folder in folders] class NewTabViewWide(uitk.UbuntuUIToolkitCustomProxyObjectBase): def go_to_section(self, section_index): sections = self.select_single(uitk.Sections) if not sections.selectedIndex == section_index: sections.click_section_button(section_index) def get_bookmarks_list(self): self.go_to_section(1) list = self.select_single(uitk.QQuickListView, objectName="bookmarksList") return sorted(list.select_many("DraggableUrlDelegateWide", objectName="bookmarkItem"), key=lambda delegate: delegate.globalRect.y) def get_top_sites_list(self): self.go_to_section(0) return self.select_single(UrlPreviewGrid, objectName="topSitesList") def get_folders_list(self): self.go_to_section(1) list = self.select_single(uitk.QQuickListView, objectName="foldersList") return sorted(list.select_many(objectName="folderItem"), key=lambda delegate: delegate.globalRect.y) def get_top_site_items(self): return self.get_top_sites_list().get_delegates() def get_bookmarks(self, folder_name): folders = self.get_folders_list() matches = [folder for folder in folders if folder.name == folder_name] if not len(matches) == 1: return [] self.pointing_device.click_object(matches[0]) return self.get_bookmarks_list() def get_folder_names(self): return [folder.name for folder in self.get_folders_list()] class UrlPreviewGrid(uitk.UbuntuUIToolkitCustomProxyObjectBase): def get_delegates(self): return sorted(self.select_many("UrlPreviewDelegate"), key=lambda delegate: delegate.globalRect.y) def get_urls(self): return [delegate.url for delegate in self.get_delegates()] class UrlDelegate(uitk.UCListItem): pass class UrlDelegateWide(uitk.UCListItem): pass class UrlPreviewDelegate(uitk.UbuntuUIToolkitCustomProxyObjectBase): def hide_from_history(self, root): menu = root.open_item_context_menu_on_item(self, "ActionSelectionPopover") # Note: we can't still use the click_action_button method of # ActionSelectionPopover's CPO, because it will crash if we delete the # menu as a reaction to the click (which is the case here). # However at least we can select the action button by objectName now. # See bug http://pad.lv/1504189 delete_item = menu.wait_select_single(objectName="delete_button") self.pointing_device.click_object(delete_item) menu.wait_until_destroyed() class DraggableUrlDelegateWide(UrlDelegateWide): def get_grip(self): return self.select_single("Icon", objectName="dragGrip") class BookmarkOptions(uitk.UbuntuUIToolkitCustomProxyObjectBase): def get_title_text_field(self): return self.select_single(uitk.TextField, objectName="titleTextField") def get_save_in_option_selector(self): return self.select_single("OptionSelector", currentlyExpanded=False) @autopilot.logging.log_action(logger.info) def click_new_folder_button(self): button = self.select_single("Button", objectName="bookmarkOptions.newButton") self.pointing_device.click_object(button) @autopilot.logging.log_action(logger.info) def click_dismiss_button(self): button = self.select_single("Button", objectName="bookmarkOptions.okButton") self.pointing_device.click_object(button) class BookmarksFoldersView(uitk.UbuntuUIToolkitCustomProxyObjectBase): def get_delegates(self): return sorted(self.select_many(objectName="bookmarkFolderDelegate"), key=lambda delegate: delegate.globalRect.y) def get_folder_delegate(self, folder): return self.select_single(objectName="bookmarkFolderDelegate", folderName=folder) def get_urls_from_folder(self, folder): return sorted(folder.select_many(UrlDelegate), key=lambda delegate: delegate.globalRect.y) def get_header_from_folder(self, folder): return folder.wait_select_single(objectName="bookmarkFolderHeader") class ContextMenuBase(uitk.UbuntuUIToolkitCustomProxyObjectBase): def get_title_label(self): return self.select_single(objectName="titleLabel") def get_visible_actions(self): return self.select_many("Empty", visible=True) def get_action(self, objectName): name = objectName + "_item" return self.select_single("Empty", objectName=name) def click_action(self, objectName): name = objectName + "_item" action = self.select_single("Empty", visible=True, enabled=True, objectName=name) self.pointing_device.click_object(action) self.wait_until_destroyed() class ContextMenuWide(ContextMenuBase): pass class ContextMenuMobile(ContextMenuBase): def click_cancel_action(self): action = self.select_single("Empty", objectName="cancelAction") self.pointing_device.click_object(action) ./tests/autopilot/webbrowser_app/emulators/__init__.py0000644000004100000410000000124313004613604023567 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Copyright 2013 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ./tests/autopilot/webbrowser_app/__init__.py0000644000004100000410000000252613004613604021561 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Copyright 2013-2015 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """webbrowser-app autopilot tests and emulators - top level package.""" import ubuntuuitoolkit as uitk from autopilot import introspection from webbrowser_app.emulators import browser class Webbrowser(uitk.UbuntuUIToolkitCustomProxyObjectBase): """Autopilot custom proxy object for the webbrowser app.""" @classmethod def validate_dbus_object(cls, path, state): name = introspection.get_classname_from_path(path) if name == b'webbrowser-app': if state['applicationName'][1] == 'webbrowser-app': return True return False @property def main_window(self): return self.select_single(browser.Browser) ./tests/autopilot/webbrowser_app/tests/0000755000004100000410000000000013004613605020606 5ustar www-datawww-data./tests/autopilot/webbrowser_app/tests/test_content_pick.py0000644000004100000410000000252713004613604024704 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Copyright 2014-2015 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from webbrowser_app.tests import StartOpenRemotePageTestCaseBase from autopilot.matchers import Eventually from autopilot.platform import model from testtools.matchers import Equals import unittest class TestContentPick(StartOpenRemotePageTestCaseBase): def setUp(self): super(TestContentPick, self).setUp(path="/uploadform") @unittest.skipIf(model() == "Desktop", "on devices only") def test_picker_dialog_shows_up(self): webview = self.main_window.get_current_webview() self.pointing_device.click_object(webview) dialog = self.main_window.get_content_picker_dialog() self.assertThat(dialog.visible, Eventually(Equals(True))) ./tests/autopilot/webbrowser_app/tests/test_basic_authentication.py0000644000004100000410000000366113004613604026404 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Copyright 2015 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from webbrowser_app.tests import StartOpenRemotePageTestCaseBase class TestBasicAuthentication(StartOpenRemotePageTestCaseBase): def setUp(self): super(TestBasicAuthentication, self).setUp() self.main_window.go_to_url(self.base_url + "/basicauth") self.dialog = self.main_window.get_http_auth_dialog() self.username = "user" self.password = "pass" def test_cancel(self): self.pointing_device.click_object(self.dialog.get_deny_button()) self.dialog.wait_until_destroyed() def test_right_credentials(self): username = self.dialog.get_username_field() username.write(self.username) password = self.dialog.get_password_field() password.write(self.password) self.pointing_device.click_object(self.dialog.get_allow_button()) self.dialog.wait_until_destroyed() def test_wrong_credentials(self): username = self.dialog.get_username_field() username.write("x") password = self.dialog.get_password_field() password.write("x") self.pointing_device.click_object(self.dialog.get_allow_button()) self.dialog.wait_until_destroyed() # verify that a new dialog has been displayed self.main_window.get_http_auth_dialog() ./tests/autopilot/webbrowser_app/tests/test_history.py0000644000004100000410000001255213004613604023724 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Copyright 2015-2016 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import time from testtools.matchers import EndsWith, Equals, StartsWith from autopilot.matchers import Eventually from webbrowser_app.tests import StartOpenRemotePageTestCaseBase class TestHistory(StartOpenRemotePageTestCaseBase): def expect_history_entries(self, ordered_urls): history = self.main_window.get_history_view() if self.main_window.wide: self.assertThat(lambda: len(history.get_entries()), Eventually(Equals(len(ordered_urls)))) entries = history.get_entries() else: self.assertThat(lambda: len(history.get_domain_entries()), Eventually(Equals(1))) self.pointing_device.click_object(history.get_domain_entries()[0]) expanded_history = self.main_window.get_expanded_history_view() self.assertThat(lambda: len(expanded_history.get_entries()), Eventually(Equals(len(ordered_urls)))) entries = expanded_history.get_entries() self.assertThat([entry.url for entry in entries], Equals(ordered_urls)) return entries def test_404_not_saved(self): url = self.base_url + "/notfound" self.main_window.go_to_url(url) self.main_window.wait_until_page_loaded(url) # A valid url to be sure the fact the 404 page isn't present in the # history view isn't a timing issue. url = self.base_url + "/test2" self.main_window.go_to_url(url) self.main_window.wait_until_page_loaded(url) self.open_history() self.expect_history_entries([url, self.url]) def test_expanded_history_view_header_swallows_clicks(self): # Regression test for https://launchpad.net/bugs/1518904 if self.main_window.wide: self.skipTest("Only on narrow form factors") history = self.open_history() self.pointing_device.click_object(history.get_domain_entries()[0]) expanded_history = self.main_window.get_expanded_history_view() hr = expanded_history.get_header().globalRect self.pointing_device.move(hr.x + hr.width // 2, hr.y + hr.height - 5) self.pointing_device.click() time.sleep(1) # There should be only one instance on the expanded history view. # If there’s more, the following call will raise an exception. self.main_window.get_expanded_history_view() def test_favicon_saved(self): # Regression test for https://launchpad.net/bugs/1549780 url = self.base_url + "/favicon" self.main_window.go_to_url(url) self.main_window.wait_until_page_loaded(url) self.open_history() first = self.expect_history_entries([url, self.url])[0] self.assertThat(first.url, Equals(url)) favicon = self.base_url + "/assets/icon1.png" self.assertThat(first.icon, Equals(favicon)) def test_favicon_updated(self): # Verify that a page dynamically updating its favicon # triggers an update in the history database. url = self.base_url + "/changingfavicon" self.main_window.go_to_url(url) self.main_window.wait_until_page_loaded(url) self.open_history() first = self.expect_history_entries([url, self.url])[0] indexes = set() while len(indexes) < 3: self.assertThat(first.url, Equals(url)) icon = first.icon self.assertThat(icon, StartsWith(self.base_url)) self.assertThat(icon, EndsWith(".png")) indexes.add(int(first.icon[(len(self.base_url)+1):-4])) def test_title_saved(self): self.open_history() entry = self.expect_history_entries([self.url])[0] self.assertThat(entry.title, Equals("test page 1")) def test_title_not_updated(self): # Verify that a page dynamically updating its title # does NOT trigger an update in the history database. url = self.base_url + "/changingtitle" self.main_window.go_to_url(url) self.main_window.wait_until_page_loaded(url) self.open_history() first = self.expect_history_entries([url, self.url])[0] for i in range(10): self.assertThat(first.title, Equals("title0")) time.sleep(0.5) def test_pushstate_updates_history(self): url = self.base_url + "/pushstate" self.main_window.go_to_url(url) self.main_window.wait_until_page_loaded(url) webview = self.main_window.get_current_webview() self.pointing_device.click_object(webview) pushed = self.base_url + "/statepushed" self.main_window.wait_until_page_loaded(pushed) self.open_history() self.expect_history_entries([pushed, url, self.url]) ./tests/autopilot/webbrowser_app/tests/test_new_tab_view.py0000644000004100000410000006007313004613604024675 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Copyright 2015-2016 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os.path import sqlite3 import time import testtools from autopilot.platform import model from autopilot.matchers import Eventually from testtools.matchers import Equals, NotEquals from webbrowser_app.tests import StartOpenRemotePageTestCaseBase class TestNewTabViewLifetime(StartOpenRemotePageTestCaseBase): def test_new_tab_view_destroyed_when_browsing(self): new_tab_view = self.open_new_tab(open_tabs_view=True) self.main_window.go_to_url(self.base_url + "/test2") new_tab_view.wait_until_destroyed() def test_new_tab_view_destroyed_when_closing_tab(self): new_tab_view = self.open_new_tab(open_tabs_view=True) if self.main_window.wide: self.main_window.chrome.get_tabs_bar().close_tab(1) else: tabs_view = self.open_tabs_view() tabs_view.get_previews()[0].close() toolbar = self.main_window.get_recent_view_toolbar() toolbar.click_button("doneButton") new_tab_view.wait_until_destroyed() def test_new_tab_view_is_shared_between_tabs(self): # Open one new tab new_tab_view = self.open_new_tab(open_tabs_view=True) # Open a second new tab new_tab_view_2 = self.open_new_tab(open_tabs_view=True) # Verify that they share the same NewTabView instance self.assertThat(new_tab_view_2.id, Equals(new_tab_view.id)) # Close the second new tab, and verify that the NewTabView instance # is still there if self.main_window.wide: self.main_window.chrome.get_tabs_bar().close_tab(2) else: tabs_view = self.open_tabs_view() tabs_view.get_previews()[0].close() toolbar = self.main_window.get_recent_view_toolbar() toolbar.click_button("doneButton") tabs_view.visible.wait_for(False) self.assertThat(new_tab_view.visible, Equals(True)) # Close the first new tab, and verify that the NewTabView instance # is destroyed if self.main_window.wide: self.main_window.chrome.get_tabs_bar().close_tab(1) else: tabs_view = self.open_tabs_view() tabs_view.get_previews()[0].close() toolbar = self.main_window.get_recent_view_toolbar() toolbar.click_button("doneButton") new_tab_view.wait_until_destroyed() @testtools.skipIf(model() == "Desktop", "Closing the last open tab on desktop quits the app") def test_new_tab_view_not_destroyed_when_closing_last_open_tab(self): if self.main_window.wide: self.main_window.chrome.get_tabs_bar().close_tab(0) else: tabs_view = self.open_tabs_view() tabs_view.get_previews()[0].close() tabs_view.visible.wait_for(False) new_tab_view = self.main_window.get_new_tab_view() # Verify that the new tab view is not destroyed and then re-created # when closing the last open tab if it was a blank one if self.main_window.wide: self.main_window.chrome.get_tabs_bar().close_tab(0) else: tabs_view = self.open_tabs_view() tabs_view.get_previews()[0].close() tabs_view.visible.wait_for(False) self.assertThat(new_tab_view.visible, Equals(True)) class TestNewPrivateTabViewLifetime(StartOpenRemotePageTestCaseBase): def test_new_private_tab_view_destroyed_when_browsing(self): self.main_window.enter_private_mode() new_private_tab_view = self.main_window.get_new_private_tab_view() self.main_window.go_to_url(self.base_url + "/test2") new_private_tab_view.wait_until_destroyed() def test_new_private_tab_view_destroyed_when_leaving_private_mode(self): self.main_window.enter_private_mode() new_private_tab_view = self.main_window.get_new_private_tab_view() self.main_window.leave_private_mode() new_private_tab_view.wait_until_destroyed() def test_new_private_tab_view_is_shared_between_tabs(self): self.main_window.enter_private_mode() new_private_tab_view = self.main_window.get_new_private_tab_view() self.main_window.go_to_url(self.base_url + "/test2") new_private_tab_view.wait_until_destroyed() # Open one new private tab new_private_tab_view = self.open_new_tab(open_tabs_view=True) # Open a second new private tab new_private_tab_view_2 = self.open_new_tab(open_tabs_view=True) # Verify that they share the same NewPrivateTabView instance self.assertThat(new_private_tab_view_2.id, Equals(new_private_tab_view.id)) # Close the second new private tab, and verify that the # NewPrivateTabView instance is still there if self.main_window.wide: self.main_window.chrome.get_tabs_bar().close_tab(2) else: tabs_view = self.open_tabs_view() tabs_view.get_previews()[0].close() toolbar = self.main_window.get_recent_view_toolbar() toolbar.click_button("doneButton") tabs_view.visible.wait_for(False) self.assertThat(new_private_tab_view.visible, Equals(True)) # Close the first new private tab, and verify that the # NewPrivateTabView instance is destroyed if self.main_window.wide: self.main_window.chrome.get_tabs_bar().close_tab(1) else: tabs_view = self.open_tabs_view() tabs_view.get_previews()[0].close() toolbar = self.main_window.get_recent_view_toolbar() toolbar.click_button("doneButton") new_private_tab_view.wait_until_destroyed() class TestNewTabViewContentsBase(StartOpenRemotePageTestCaseBase): def setUp(self): self.create_temporary_profile() self.populate_config() self.populate_bookmarks() super(TestNewTabViewContentsBase, self).setUp() self.new_tab_view = self.open_new_tab(open_tabs_view=True) def populate_config(self): self.homepage = "http://test/test2" config_file = os.path.join(self.config_location, "webbrowser-app.conf") with open(config_file, "w") as f: f.write("[General]\n") f.write("homepage={}".format(self.homepage)) def populate_bookmarks(self): db_path = os.path.join(self.data_location, "bookmarks.sqlite") connection = sqlite3.connect(db_path) connection.execute("""CREATE TABLE IF NOT EXISTS folders (folderId INTEGER PRIMARY KEY, folder VARCHAR);""") rows = [ "Actinide", "NobleGas", ] for row in rows: query = "INSERT INTO folders (folder) VALUES ('{}');" query = query.format(row) connection.execute(query) foldersId = dict(connection.execute("""SELECT folder, folderId FROM folders;""")) connection.execute("""CREATE TABLE IF NOT EXISTS bookmarks (url VARCHAR, title VARCHAR, icon VARCHAR, created INTEGER, folderId INTEGER);""") rows = [ ("http://test/periodic-table/element/24/chromium", "Chromium - Element Information", 0), ("http://test/periodic-table/element/77/iridium", "Iridium - Element Information", 0), ("http://test/periodic-table/element/31/gallium", "Gallium - Element Information", 0), ("http://test/periodic-table/element/116/livermorium", "Livermorium - Element Information", 0), ("http://test/periodic-table/element/89/actinium", "Actinium - Element Information", foldersId['Actinide']), ("http://test/periodic-table/element/2/helium", "Helium - Element Information", foldersId['NobleGas']), ] for i, row in enumerate(rows): timestamp = int(time.time()) - i * 10 query = "INSERT INTO bookmarks \ VALUES ('{}', '{}', '', {}, {});" query = query.format(row[0], row[1], timestamp, row[2]) connection.execute(query) connection.commit() connection.close() class TestNewTabViewContentsNarrow(TestNewTabViewContentsBase): def setUp(self): super(TestNewTabViewContentsNarrow, self).setUp() if self.main_window.wide: self.skipTest("Only on narrow form factors") def test_default_home_bookmark(self): homepage_bookmark = self.new_tab_view.get_homepage_bookmark() self.assertThat(homepage_bookmark.url, Equals(self.homepage)) self.pointing_device.click_object(homepage_bookmark) self.new_tab_view.wait_until_destroyed() self.main_window.wait_until_page_loaded(self.homepage) def test_open_top_site(self): top_sites = self.new_tab_view.get_top_sites_list() self.assertThat(lambda: len(top_sites.get_delegates()), Eventually(Equals(1))) top_site = top_sites.get_delegates()[0] url = top_site.url self.pointing_device.click_object(top_site) self.new_tab_view.wait_until_destroyed() self.main_window.wait_until_page_loaded(url) def test_open_bookmark(self): bookmark = self.new_tab_view.get_bookmark_delegates()[2] url = bookmark.url self.pointing_device.click_object(bookmark) self.new_tab_view.wait_until_destroyed() self.main_window.wait_until_page_loaded(url) def test_open_bookmark_when_expanded(self): more_button = self.new_tab_view.get_bookmarks_more_button() self.assertThat(more_button.visible, Equals(True)) self.pointing_device.click_object(more_button) folders = self.new_tab_view.get_bookmarks_folder_list_view() folder_delegate = folders.get_folder_delegate("") self.assertThat(lambda: len(folders.get_urls_from_folder( folder_delegate)), Eventually(Equals(5))) bookmark = folders.get_urls_from_folder(folder_delegate)[0] url = bookmark.url self.pointing_device.click_object(bookmark) self.new_tab_view.wait_until_destroyed() self.main_window.wait_until_page_loaded(url) def test_bookmarks_section_expands_and_collapses(self): bookmarks = self.new_tab_view.get_bookmarks_list() top_sites = self.new_tab_view.get_top_sites_list() self.assertThat(top_sites.visible, Equals(True)) # When the bookmarks list is collapsed, it shows a maximum of 5 entries self.assertThat(bookmarks.count, Eventually(Equals(5))) # When expanded, it shows all entries more_button = self.new_tab_view.get_bookmarks_more_button() self.assertThat(more_button.visible, Equals(True)) self.pointing_device.click_object(more_button) folders = self.new_tab_view.get_bookmarks_folder_list_view() folder_delegate = folders.get_folder_delegate("") self.assertThat(lambda: len(folders.get_urls_from_folder( folder_delegate)), Eventually(Equals(5))) self.assertThat(top_sites.visible, Eventually(Equals(False))) # Collapse again self.assertThat(more_button.visible, Equals(True)) self.pointing_device.click_object(more_button) bookmarks = self.new_tab_view.get_bookmarks_list() self.assertThat(bookmarks.count, Eventually(Equals(5))) self.assertThat(top_sites.visible, Eventually(Equals(True))) def _remove_first_bookmark(self): bookmark = self.new_tab_view.get_bookmark_delegates()[1] url = bookmark.url bookmark.trigger_leading_action("leadingAction.delete", lambda: None) self.assertThat( lambda: self.new_tab_view.get_bookmark_delegates()[1].url, Eventually(NotEquals(url))) def _remove_first_bookmark_from_folder(self, folder): folders = self.new_tab_view.get_bookmarks_folder_list_view() folder_delegate = folders.get_folder_delegate(folder) delegate = folders.get_urls_from_folder(folder_delegate)[0] url = delegate.url count = len(folders.get_urls_from_folder(folder_delegate)) delegate.trigger_leading_action("leadingAction.delete", delegate.wait_until_destroyed) if ((count - 1) > 4): self.assertThat( lambda: folders.get_urls_from_folder(folder_delegate)[0], Eventually(NotEquals(url))) def _toggle_bookmark_folder(self, folder): folders = self.new_tab_view.get_bookmarks_folder_list_view() folder_delegate = folders.get_folder_delegate(folder) self.pointing_device.click_object( folders.get_header_from_folder(folder_delegate)) def test_remove_bookmarks_when_collapsed(self): bookmarks = self.new_tab_view.get_bookmarks_list() self.assertThat(bookmarks.count, Eventually(Equals(5))) more_button = self.new_tab_view.get_bookmarks_more_button() for i in range(3): self._remove_first_bookmark() self.assertThat(more_button.visible, Eventually(Equals(i < 1))) self.assertThat(bookmarks.count, Equals(5 if (i < 2) else 4)) def test_remove_bookmarks_when_expanded(self): more_button = self.new_tab_view.get_bookmarks_more_button() self.assertThat(more_button.visible, Equals(True)) self.pointing_device.click_object(more_button) folders = self.new_tab_view.get_bookmarks_folder_list_view() folder_delegate = folders.get_folder_delegate("") self.assertThat(lambda: len(folders.get_urls_from_folder( folder_delegate)), Eventually(Equals(5))) more_button = self.new_tab_view.get_bookmarks_more_button() top_sites = self.new_tab_view.get_top_sites_list() self._toggle_bookmark_folder("Actinide") self._remove_first_bookmark_from_folder("Actinide") self._toggle_bookmark_folder("NobleGas") self._remove_first_bookmark_from_folder("NobleGas") self.assertThat(more_button.visible, Eventually(Equals(False))) self.assertThat(top_sites.visible, Eventually(Equals(True))) def test_show_bookmarks_folders_when_expanded(self): more_button = self.new_tab_view.get_bookmarks_more_button() self.assertThat(more_button.visible, Equals(True)) self.pointing_device.click_object(more_button) folders = self.new_tab_view.get_bookmarks_folder_list_view() self.assertThat(lambda: len(folders.get_delegates()), Eventually(Equals(3))) folder_delegate = folders.get_folder_delegate("") self.assertThat(lambda: len(folders.get_urls_from_folder( folder_delegate)), Eventually(Equals(5))) self._toggle_bookmark_folder("Actinide") folder_delegate = folders.get_folder_delegate("Actinide") self.assertThat(lambda: len(folders.get_urls_from_folder( folder_delegate)), Eventually(Equals(1))) self._toggle_bookmark_folder("NobleGas") folder_delegate = folders.get_folder_delegate("NobleGas") self.assertThat(lambda: len(folders.get_urls_from_folder( folder_delegate)), Eventually(Equals(1))) def test_collapsed_bookmarks_folders_when_expanded(self): more_button = self.new_tab_view.get_bookmarks_more_button() self.assertThat(more_button.visible, Equals(True)) self.pointing_device.click_object(more_button) folders = self.new_tab_view.get_bookmarks_folder_list_view() self.assertThat(lambda: len(folders.get_delegates()), Eventually(Equals(3))) folder_delegate = folders.get_folder_delegate("") self.assertThat(lambda: len(folders.get_urls_from_folder( folder_delegate)), Eventually(Equals(5))) folder_delegate = folders.get_folder_delegate("Actinide") self.assertThat(lambda: len(folders.get_urls_from_folder( folder_delegate)), Eventually(Equals(0))) folder_delegate = folders.get_folder_delegate("NobleGas") self.assertThat(lambda: len(folders.get_urls_from_folder( folder_delegate)), Eventually(Equals(0))) def test_hide_empty_bookmarks_folders_when_expanded(self): more_button = self.new_tab_view.get_bookmarks_more_button() self.assertThat(more_button.visible, Equals(True)) self.pointing_device.click_object(more_button) folders = self.new_tab_view.get_bookmarks_folder_list_view() self.assertThat(lambda: len(folders.get_delegates()), Eventually(Equals(3))) self._toggle_bookmark_folder("Actinide") folder_delegate = folders.get_folder_delegate("Actinide") self.assertThat(lambda: len(folders.get_urls_from_folder( folder_delegate)), Eventually(Equals(1))) self._remove_first_bookmark_from_folder("Actinide") self.assertThat(lambda: len(folders.get_delegates()), Eventually(Equals(2))) folder_delegate = folders.get_folder_delegate("") self.assertThat(lambda: len(folders.get_urls_from_folder( folder_delegate)), Eventually(Equals(5))) self._toggle_bookmark_folder("NobleGas") folder_delegate = folders.get_folder_delegate("NobleGas") self.assertThat(lambda: len(folders.get_urls_from_folder( folder_delegate)), Eventually(Equals(1))) def test_bookmarks_folder_expands_and_collapses(self): more_button = self.new_tab_view.get_bookmarks_more_button() self.assertThat(more_button.visible, Equals(True)) self.pointing_device.click_object(more_button) folders = self.new_tab_view.get_bookmarks_folder_list_view() self.assertThat(lambda: len(folders.get_delegates()), Eventually(Equals(3))) folder_delegate = folders.get_folder_delegate("") self.assertThat(lambda: len(folders.get_urls_from_folder( folder_delegate)), Eventually(Equals(5))) self.pointing_device.click_object( folders.get_header_from_folder(folder_delegate)) self.assertThat(lambda: len(folders.get_urls_from_folder( folder_delegate)), Eventually(Equals(0))) self.pointing_device.click_object( folders.get_header_from_folder(folder_delegate)) self.assertThat(lambda: len(folders.get_urls_from_folder( folder_delegate)), Eventually(Equals(5))) def test_remove_top_sites(self): top_sites = self.new_tab_view.get_top_sites_list() self.assertThat(lambda: len(top_sites.get_delegates()), Eventually(Equals(1))) notopsites_label = self.new_tab_view.get_notopsites_label() self.assertThat(notopsites_label.visible, Eventually(Equals(False))) delegate = top_sites.get_delegates()[0] delegate.hide_from_history(self.main_window) self.assertThat(lambda: len(top_sites.get_delegates()), Eventually(Equals(0))) self.assertThat(notopsites_label.visible, Eventually(Equals(True))) class TestNewTabViewContentsWide(TestNewTabViewContentsBase): def setUp(self): super(TestNewTabViewContentsWide, self).setUp() if not self.main_window.wide: self.skipTest("Only on wide form factors") def test_remove_bookmark(self): view = self.new_tab_view bookmarks = view.get_bookmarks_list() previous_count = len(bookmarks) bookmark = bookmarks[1] bookmark.trigger_leading_action("leadingAction.delete", bookmark.wait_until_destroyed) bookmarks = view.get_bookmarks_list() self.assertThat(len(bookmarks), Equals(previous_count - 1)) def test_remove_top_sites(self): view = self.new_tab_view topsites = view.get_top_site_items() previous_count = len(topsites) topsites[0].hide_from_history(self.main_window) self.assertThat(len(view.get_top_site_items()), Equals(previous_count - 1)) def test_drag_bookmarks(self): view = self.new_tab_view folders = view.get_folders_list() bookmarks = view.get_bookmarks_list() previous_count = len(bookmarks) bookmark = bookmarks[1] title = bookmark.title grip = bookmark.get_grip() rect = grip.globalRect # Test that when hovering normal bookmarks item the grip appears self.assertThat(grip.opacity, Equals(0)) self.pointing_device.move_to_object(bookmark) self.assertThat(grip.opacity, Eventually(Equals(1.0))) # Test that an item bounces back when dragged within the list itself self.pointing_device.drag(rect.x, rect.y, rect.x, rect.y + 200) self.assertThat(grip.globalRect, Eventually(Equals(rect))) # Test that an item bounces back when dragged to the same folder folder = folders[0] folder_cx = folder.globalRect.x + folder.width / 2 folder_cy = folder.globalRect.y + folder.height / 2 # Work around https://launchpad.net/bugs/1499437 by dragging downwards # a little bit first, then to the target folder. self.pointing_device.move_to_object(grip) pos = self.pointing_device.position() self.pointing_device.press() self.pointing_device.move(pos[0], pos[1] + 20) self.pointing_device.move(folder_cx, folder_cy) self.pointing_device.release() self.assertThat(grip.globalRect, Eventually(Equals(rect))) # Test that dragging an item to another folder removes it from this one # and adds it to the target folder folder = folders[2] folder_cx = folder.globalRect.x + folder.width / 2 folder_cy = folder.globalRect.y + folder.height / 2 # Work around https://launchpad.net/bugs/1499437 by dragging downwards # a little bit first, then to the target folder. self.pointing_device.move_to_object(grip) pos = self.pointing_device.position() self.pointing_device.press() self.pointing_device.move(pos[0], pos[1] + 20) # Move the cursor to a few pixels below the vertical center of the # folder to ensure that the folder above doesn’t get targetted instead. self.pointing_device.move(folder_cx, folder_cy + 5) self.pointing_device.release() self.assertThat(lambda: len(view.get_bookmarks_list()), Eventually(NotEquals(previous_count))) # Verify that the item has been added to the top of the target folder self.pointing_device.click_object(folder) self.assertThat(lambda: len(view.get_bookmarks_list()), Eventually(Equals(2))) self.assertThat(view.get_bookmarks_list()[0].title, Equals(title)) ./tests/autopilot/webbrowser_app/tests/test_errorsheet.py0000644000004100000410000000465413004613604024411 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Copyright 2013-2014 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from testtools.matchers import Equals from autopilot.matchers import Eventually from webbrowser_app.tests import StartOpenRemotePageTestCaseBase INVALID_URL = "http://invalid/" class TestErrorSheet(StartOpenRemotePageTestCaseBase): """Tests the error message functionality.""" def test_invalid_url_triggers_error_message(self): error = self.main_window.get_error_sheet() self.assertThat(error.visible, Equals(False)) self.main_window.go_to_url(INVALID_URL) self.assertThat(error.visible, Eventually(Equals(True))) def test_navigating_away_discards_error_message(self): error = self.main_window.get_error_sheet() self.main_window.go_to_url(INVALID_URL) self.assertThat(error.visible, Eventually(Equals(True))) self.main_window.go_to_url(self.base_url + "/test2") self.assertThat(error.visible, Eventually(Equals(False))) def test_navigating_back_discards_error_message(self): error = self.main_window.get_error_sheet() self.main_window.go_to_url(INVALID_URL) self.assertThat(error.visible, Eventually(Equals(True))) self.main_window.go_back() self.assertThat(error.visible, Eventually(Equals(False))) def test_navigating_forward_discards_error_message(self): error = self.main_window.get_error_sheet() self.main_window.go_to_url(INVALID_URL) self.main_window.wait_until_page_loaded(INVALID_URL) url = self.base_url + "/test2" self.main_window.go_to_url(url) self.main_window.wait_until_page_loaded(url) self.main_window.go_back() self.assertThat(error.visible, Eventually(Equals(True))) self.main_window.go_forward() self.assertThat(error.visible, Eventually(Equals(False))) ./tests/autopilot/webbrowser_app/tests/test_addressbar_bookmark.py0000644000004100000410000000630513004613604026221 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Copyright 2014-2015 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from testtools.matchers import Equals from autopilot.matchers import Eventually from webbrowser_app.tests import StartOpenRemotePageTestCaseBase class TestAddressBarBookmark(StartOpenRemotePageTestCaseBase): def test_switching_tabs_updates_bookmark_toggle(self): chrome = self.main_window.chrome address_bar = self.main_window.address_bar bookmark_toggle = address_bar.get_bookmark_toggle() self.pointing_device.click_object(bookmark_toggle) bookmark_options = self.main_window.get_bookmark_options() bookmark_options.click_dismiss_button() bookmark_options.wait_until_destroyed() self.assertThat(chrome.bookmarked, Eventually(Equals(True))) self.open_new_tab(open_tabs_view=True) url = self.base_url + "/test2" self.main_window.go_to_url(url) self.main_window.wait_until_page_loaded(url) self.assertThat(chrome.bookmarked, Eventually(Equals(False))) if self.main_window.wide: self.main_window.chrome.get_tabs_bar().select_tab(0) else: tabs_view = self.open_tabs_view() tabs_view.get_previews()[1].select() tabs_view.visible.wait_for(False) self.assertThat(chrome.bookmarked, Eventually(Equals(True))) if self.main_window.wide: self.main_window.chrome.get_tabs_bar().select_tab(1) else: tabs_view = self.open_tabs_view() tabs_view.get_previews()[1].select() tabs_view.visible.wait_for(False) self.assertThat(chrome.bookmarked, Eventually(Equals(False))) def test_cannot_bookmark_empty_page(self): self.open_new_tab(open_tabs_view=True) if self.main_window.wide: self.main_window.chrome.get_tabs_bar().select_tab(0) else: tabs_view = self.open_tabs_view() tabs_view.get_previews()[1].select() tabs_view.visible.wait_for(False) webview = self.main_window.get_current_webview() self.pointing_device.click_object(webview) address_bar = self.main_window.address_bar bookmark_toggle = address_bar.get_bookmark_toggle() self.assertThat(bookmark_toggle.visible, Eventually(Equals(True))) if self.main_window.wide: self.main_window.chrome.get_tabs_bar().select_tab(1) else: tabs_view = self.open_tabs_view() tabs_view.get_previews()[1].select() tabs_view.visible.wait_for(False) self.assertThat(bookmark_toggle.visible, Eventually(Equals(False))) ./tests/autopilot/webbrowser_app/tests/test_addressbar_states.py0000644000004100000410000000676013004613604025724 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Copyright 2013-2015 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import testtools from testtools.matchers import Equals from autopilot.matchers import Eventually from autopilot.platform import model from webbrowser_app.tests import StartOpenRemotePageTestCaseBase class TestAddressBarStates(StartOpenRemotePageTestCaseBase): def test_cancel_state_loading(self): address_bar = self.main_window.address_bar url = self.base_url + "/wait/5" self.main_window.go_to_url(url) address_bar.loading.wait_for(True) address_bar.click_action_button() address_bar.loading.wait_for(False) def test_state_editing(self): address_bar = self.main_window.address_bar self.pointing_device.click_object(address_bar) address_bar.activeFocus.wait_for(True) self.keyboard.press_and_release("Enter") address_bar.activeFocus.wait_for(False) def test_looses_focus_when_loading_starts(self): address_bar = self.main_window.address_bar self.pointing_device.click_object(address_bar) address_bar.activeFocus.wait_for(True) url = self.base_url + "/test2" self.main_window.go_to_url(url) address_bar.activeFocus.wait_for(False) def test_looses_focus_when_reloading(self): address_bar = self.main_window.address_bar self.pointing_device.click_object(address_bar) address_bar.activeFocus.wait_for(True) # Work around https://launchpad.net/bugs/1417118 by clearing the # address bar and typing again the current URL to enable the reload # button. address_bar.clear() address_bar.write(self.url) address_bar.click_action_button() address_bar.activeFocus.wait_for(False) # http://pad.lv/1456199 @testtools.skipIf(model() != "Desktop", "on desktop only") def test_clears_when_actual_url_changed(self): address_bar = self.main_window.address_bar self.pointing_device.click_object(address_bar) address_bar.activeFocus.wait_for(True) url = self.base_url + "/test1" self.main_window.go_to_url(url) self.main_window.wait_until_page_loaded(url) self.pointing_device.click_object(address_bar) address_bar.activeFocus.wait_for(True) self.new_tab_view = self.open_new_tab(open_tabs_view=True) self.assertThat(address_bar.text, Eventually(Equals(""))) # http://pad.lv/1487713 def test_does_not_clear_when_typing_while_loading(self): address_bar = self.main_window.address_bar self.pointing_device.click_object(address_bar) address_bar.activeFocus.wait_for(True) url = self.base_url + "/wait/3" self.main_window.go_to_url(url) self.pointing_device.click_object(address_bar) address_bar.write("x") self.main_window.wait_until_page_loaded(url) self.assertThat(address_bar.text, Equals("x")) ./tests/autopilot/webbrowser_app/tests/test_backforward.py0000644000004100000410000000410013004613604024476 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Copyright 2013-2014 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from testtools.matchers import Equals from webbrowser_app.tests import StartOpenRemotePageTestCaseBase class TestBackForward(StartOpenRemotePageTestCaseBase): """Tests the back and forward functionality.""" def setUp(self): super().setUp() self.chrome = self.main_window.chrome def test_homepage_no_history(self): self.assertThat(self.chrome.is_back_button_enabled(), Equals(False)) self.assertThat(self.chrome.is_forward_button_enabled(), Equals(False)) def test_go_back_after_opening_a_new_page(self): """Test that the back button must open the previous page.""" self.assertThat(self.chrome.is_back_button_enabled(), Equals(False)) url = self.base_url + "/test2" self.main_window.go_to_url(url) self.main_window.wait_until_page_loaded(url) self.main_window.go_back() self.assert_home_page_eventually_loaded() def test_go_forward_after_going_back(self): """Test that the forward button must open the previous page.""" url = self.base_url + "/test2" self.main_window.go_to_url(url) self.main_window.wait_until_page_loaded(url) self.assertThat(self.chrome.is_forward_button_enabled(), Equals(False)) self.main_window.go_back() self.assert_home_page_eventually_loaded() self.main_window.go_forward() self.main_window.wait_until_page_loaded(url) ./tests/autopilot/webbrowser_app/tests/__init__.py0000644000004100000410000002412413004613604022721 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Copyright 2013-2016 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """webbrowser-app autopilot tests.""" import os import shutil import signal import tempfile import time import urllib.request import fixtures import psutil from testtools.matchers import Equals, NotEquals from autopilot.matchers import Eventually from autopilot.platform import model from autopilot.testcase import AutopilotTestCase from . import http_server import ubuntuuitoolkit as uitk class BrowserTestCaseBase(AutopilotTestCase): """ A common test case class that provides several useful methods for webbrowser-app tests. """ local_location = "../../src/app/webbrowser/webbrowser-app" d_f = "--desktop_file_hint=/usr/share/applications/webbrowser-app.desktop" ARGS = ["--new-session"] def create_temporary_profile(self): # This method is meant to be called exactly once, in setUp(). # Tests that need to pre-populate the profile may call it earlier. if hasattr(self, '_temp_xdg_dir'): return self._temp_xdg_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, self._temp_xdg_dir) appname = 'webbrowser-app' xdg_data = os.path.join(self._temp_xdg_dir, 'data') self.useFixture(fixtures.EnvironmentVariable( 'XDG_DATA_HOME', xdg_data)) self.data_location = os.path.join(xdg_data, appname) if not os.path.exists(self.data_location): os.makedirs(self.data_location) xdg_config = os.path.join(self._temp_xdg_dir, 'config') self.useFixture(fixtures.EnvironmentVariable( 'XDG_CONFIG_HOME', xdg_config)) self.config_location = os.path.join(xdg_config, appname) if not os.path.exists(self.config_location): os.makedirs(self.config_location) xdg_cache = os.path.join(self._temp_xdg_dir, 'cache') self.useFixture(fixtures.EnvironmentVariable( 'XDG_CACHE_HOME', xdg_cache)) self.cache_location = os.path.join(xdg_cache, appname) if not os.path.exists(self.cache_location): os.makedirs(self.cache_location) def setUp(self, launch=True): self.create_temporary_profile() self.pointing_device = uitk.get_pointing_device() super(BrowserTestCaseBase, self).setUp() if (launch): self.launch_app() def launch_app(self): if os.path.exists(self.local_location): self.app = self.launch_test_local() else: self.app = self.launch_test_installed() self.main_window.visible.wait_for(True) def launch_test_local(self): return self.launch_test_application( self.local_location, *self.ARGS, emulator_base=uitk.UbuntuUIToolkitCustomProxyObjectBase) def launch_test_installed(self): if model() == 'Desktop': return self.launch_test_application( "webbrowser-app", *self.ARGS, emulator_base=uitk.UbuntuUIToolkitCustomProxyObjectBase) else: return self.launch_test_application( "webbrowser-app", self.d_f, *self.ARGS, app_type='qt', emulator_base=uitk.UbuntuUIToolkitCustomProxyObjectBase) @property def main_window(self): return self.app.main_window def drag_bottom_edge_upwards(self, fraction): self.assertThat(model(), NotEquals('Desktop')) handleRect = self.main_window.get_bottom_edge_handle().globalRect x = handleRect.x + handleRect.width // 2 y0 = handleRect.y + handleRect.height // 2 y1 = y0 - int(self.main_window.height * fraction) self.pointing_device.drag(x, y0, x, y1) def open_tabs_view(self): self.assertFalse(self.main_window.wide) if model() == 'Desktop': bar = self.main_window.get_bottom_edge_bar() self.pointing_device.click_object(bar) else: self.drag_bottom_edge_upwards(0.75) tabs_view = self.main_window.get_tabs_view() # Give some time for the view to settle so that all previews reached # their initial position (the animation has a duration of # UbuntuAnimation.BriskDuration, i.e. 333ms, so 1s should be plenty). time.sleep(1) return tabs_view def open_new_tab(self, open_tabs_view=False, expand_view=False): if (self.main_window.incognito): count = len(self.main_window.get_incognito_webviews()) else: count = len(self.main_window.get_webviews()) if self.main_window.wide: self.main_window.chrome.get_tabs_bar().click_new_tab_button() else: if open_tabs_view: self.open_tabs_view() tabs_view = self.main_window.get_tabs_view() toolbar = self.main_window.get_recent_view_toolbar() toolbar.click_action("newTabButton") tabs_view.visible.wait_for(False) if (self.main_window.incognito): self.assert_number_incognito_webviews_eventually(count + 1) new_tab_view = self.main_window.get_new_private_tab_view() else: self.assert_number_webviews_eventually(count + 1) new_tab_view = self.main_window.get_new_tab_view() if self.main_window.wide: self.assertThat(self.main_window.address_bar.activeFocus, Eventually(Equals(True))) if not self.main_window.wide and expand_view: more_button = new_tab_view.get_bookmarks_more_button() self.assertThat(more_button.visible, Equals(True)) self.pointing_device.click_object(more_button) return new_tab_view def open_settings(self): chrome = self.main_window.chrome drawer_button = chrome.get_drawer_button() self.pointing_device.click_object(drawer_button) chrome.get_drawer() settings_action = chrome.get_drawer_action("settings") self.pointing_device.click_object(settings_action) return self.main_window.get_settings_page() def open_bookmarks(self): chrome = self.main_window.chrome drawer_button = chrome.get_drawer_button() self.pointing_device.click_object(drawer_button) chrome.get_drawer() bookmarks_action = chrome.get_drawer_action("bookmarks") self.pointing_device.click_object(bookmarks_action) return self.main_window.get_bookmarks_view() def open_history(self): chrome = self.main_window.chrome drawer_button = chrome.get_drawer_button() self.pointing_device.click_object(drawer_button) chrome.get_drawer() history_action = chrome.get_drawer_action("history") self.pointing_device.click_object(history_action) return self.main_window.get_history_view() def open_downloads(self): chrome = self.main_window.chrome drawer_button = chrome.get_drawer_button() self.pointing_device.click_object(drawer_button) chrome.get_drawer() downloads_action = chrome.get_drawer_action("downloads") self.pointing_device.click_object(downloads_action) return self.main_window.get_downloads_page() def assert_number_webviews_eventually(self, count): self.assertThat(lambda: len(self.main_window.get_webviews()), Eventually(Equals(count))) def assert_number_incognito_webviews_eventually(self, count): self.assertThat(lambda: len(self.main_window.get_incognito_webviews()), Eventually(Equals(count))) def ping_server(self, server): url = "http://localhost:{}/ping".format(server.port) ping = urllib.request.urlopen(url) self.assertThat(ping.read(), Equals(b"pong")) def kill_web_processes(self, signal=signal.SIGKILL): children = psutil.Process(self.app.pid).children(True) for child in children: if child.name() == 'oxide-renderer': for arg in child.cmdline(): if '--type=renderer' in arg: os.kill(child.pid, signal) break class StartOpenRemotePageTestCaseBase(BrowserTestCaseBase): """ Helper test class that opens the browser at a remote URL instead of defaulting to the homepage. This class should be preferred to the base test case class, as it doesn’t rely on a connection to the outside world (to open the default homepage), and because it ensures the initial page is fully loaded before the tests are executed, thus making them more robust. """ def setUp(self, path="/test1", launch=True): self.http_server = http_server.HTTPServerInAThread() self.ping_server(self.http_server) self.addCleanup(self.http_server.cleanup) self.useFixture(fixtures.EnvironmentVariable( 'UBUNTU_WEBVIEW_HOST_MAPPING_RULES', "MAP test:80 localhost:{}".format(self.http_server.port))) self.base_domain = "test" self.base_url = "http://" + self.base_domain self.url = self.base_url + path self.ARGS = self.ARGS + [self.url] super(StartOpenRemotePageTestCaseBase, self).setUp(launch) if (launch): self.assert_home_page_eventually_loaded() def launch_and_wait_for_page_loaded(self): self.launch_app() self.assert_home_page_eventually_loaded() def assert_home_page_eventually_loaded(self): self.main_window.wait_until_page_loaded(self.url) ./tests/autopilot/webbrowser_app/tests/test_sad_tab.py0000644000004100000410000000525713004613604023624 0ustar www-datawww-data# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Copyright 2015-2016 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import signal import time from webbrowser_app.tests import StartOpenRemotePageTestCaseBase class TestSadTab(StartOpenRemotePageTestCaseBase): def _kill_web_process(self): self.kill_web_processes() # The first time the web process is killed, the browser attempts to # reload the page gracefully (after a short delay), hoping the process # won’t be killed again. time.sleep(1) self.main_window.wait_until_page_loaded(self.url) self.kill_web_processes() # The second time around, the browser displays a sad tab. return self.main_window.get_sad_tab() def test_reload_web_process_killed(self): sad_tab = self._kill_web_process() sad_tab.click_reload_button() sad_tab.wait_until_destroyed() self.assert_home_page_eventually_loaded() def test_close_tab_web_process_killed(self): wide = self.main_window.wide sad_tab = self._kill_web_process() sad_tab.click_close_tab_button() if wide: # closing the last open tab exits the application self.app.process.wait() return sad_tab.wait_until_destroyed() self.main_window.get_new_tab_view() def _crash_web_process(self): self.kill_web_processes(signal.SIGABRT) # A crash of the web process displays the sad tab right away return self.main_window.get_sad_tab() def test_reload_web_process_crashed(self): sad_tab = self._crash_web_process() sad_tab.click_reload_button() sad_tab.wait_until_destroyed() self.assert_home_page_eventually_loaded() def test_close_tab_web_process_crashed(self): wide = self.main_window.wide sad_tab = self._crash_web_process() sad_tab.click_close_tab_button() if wide: # On desktop, closing the last open tab exits the application self.app.process.wait() return sad_tab.wait_until_destroyed() self.main_window.get_new_tab_view() ./tests/autopilot/webbrowser_app/tests/http_server.py0000644000004100000410000003056013004613604023530 0ustar www-datawww-data# -*- coding: utf-8 -*- # # Copyright 2013-2016 Canonical # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. from base64 import b64decode import http.server as http import json import logging import threading import time logger = logging.getLogger(__name__) class HTTPRequestHandler(http.BaseHTTPRequestHandler): """ A custom HTTP request handler that serves GET resources. """ suggestions_data = {} base64_png_data = \ "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAACXBIWXMAAAsTAAALEwE" \ "AmpwYAAAAOUlEQVRYw+3OAQ0AAAgDoGv/zlpDN0hATS7qaGlpaWlpaWlpaWlpaWlpaW" \ "lpaWlpaWlpaWlpab1qLUGqAWNyFWTYAAAAAElFTkSuQmCC" def make_html(self, title, body): html = "{}{}" return html.format(title, body) def send_html(self, html): self.send_header("Content-Type", "text/html") self.end_headers() self.wfile.write(html.encode()) def send_auth_request(self): self.send_response(401) self.send_header("WWW-Authenticate", "Basic realm=\"Enter Password\"") self.end_headers() self.send_html("Not Authorized") def do_GET(self): if self.path == "/ping": self.send_response(200) self.send_header("Content-Type", "text/plain") self.end_headers() self.wfile.write(b"pong") elif self.path == "/test1": self.send_response(200) title = "test page 1" body = "

test page 1

" html = self.make_html(title, body) self.send_html(html) elif self.path == "/test2": self.send_response(200) title = "test page 2" body = "

test page 2

" html = self.make_html(title, body) self.send_html(html) elif self.path == "/link": self.send_response(200) html = '' html += '
' html += '' self.send_html(html) elif self.path.startswith("/wait/"): delay = int(self.path[6:]) self.send_response(200) title = "waiting {} seconds".format(delay) body = "

this page took {} seconds to load

".format(delay) html = self.make_html(title, body) time.sleep(delay) self.send_html(html) elif self.path == "/blanktargetlink": # craft a page that accepts clicks anywhere inside its window # and that requests opening another page in a new tab self.send_response(200) html = '' html += '' html += '
' html += '' self.send_html(html) elif self.path == "/fulliframewithblanktargetlink": # iframe that takes up the whole page and that contains # the page above self.send_response(200) html = '' html += '