mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-26 14:23:09 +00:00
Merge branch plasma-mobile:master into master
This commit is contained in:
commit
c4d2238f36
1936 changed files with 75158 additions and 102013 deletions
|
|
@ -6,7 +6,11 @@ include:
|
|||
file:
|
||||
- /gitlab-templates/reuse-lint.yml
|
||||
- /gitlab-templates/linux-qt6.yml
|
||||
- /gitlab-templates/linux-qt6-next.yml
|
||||
- /gitlab-templates/alpine-qt6.yml
|
||||
- /gitlab-templates/xml-lint.yml
|
||||
- /gitlab-templates/yaml-lint.yml
|
||||
- /gitlab-templates/qml-lint.yml
|
||||
|
||||
posix-script-compliance:
|
||||
image:
|
||||
|
|
|
|||
18
.kde-ci.yml
18
.kde-ci.yml
|
|
@ -2,9 +2,10 @@
|
|||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
Dependencies:
|
||||
- 'on': ['@all']
|
||||
'require':
|
||||
- 'on': ['@all']
|
||||
'require':
|
||||
'frameworks/extra-cmake-modules': '@latest-kf6'
|
||||
'frameworks/kauth': '@latest-kf6'
|
||||
'frameworks/kconfig': '@latest-kf6'
|
||||
'frameworks/kcoreaddons': '@latest-kf6'
|
||||
'frameworks/ki18n': '@latest-kf6'
|
||||
|
|
@ -24,17 +25,18 @@ Dependencies:
|
|||
'plasma/plasma-workspace': '@same'
|
||||
'libraries/kirigami-addons': '@latest-kf6'
|
||||
Options:
|
||||
require-passing-tests-on: [ 'Linux', 'FreeBSD']
|
||||
require-passing-tests-on: ['Linux', 'FreeBSD']
|
||||
run-qmllint: true
|
||||
|
||||
RuntimeDependencies:
|
||||
- 'on': ['Linux']
|
||||
'require':
|
||||
- 'on': ['Linux']
|
||||
'require':
|
||||
'plasma/plasma-nm': '@same'
|
||||
'plasma/plasma-pa': '@same'
|
||||
'plasma/bluedevil': '@same'
|
||||
'plasma/milou': '@same'
|
||||
'plasma/qqc2-breeze-style': '@same'
|
||||
'frameworks/bluez-qt': '@latest-kf6'
|
||||
- 'on': ['Linux', 'FreeBSD']
|
||||
'require':
|
||||
'plasma/milou' : '@same'
|
||||
- 'on': ['Linux', 'FreeBSD']
|
||||
'require':
|
||||
'plasma/milou': '@same'
|
||||
|
|
|
|||
1
.well-known/funding-manifest-urls
Normal file
1
.well-known/funding-manifest-urls
Normal file
|
|
@ -0,0 +1 @@
|
|||
https://ev.kde.org/.well-known/funding.json
|
||||
2
.well-known/funding-manifest-urls.license
Normal file
2
.well-known/funding-manifest-urls.license
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Copyright None
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
|
|
@ -4,15 +4,14 @@
|
|||
# SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
|
||||
project(plasma-mobile)
|
||||
set(PROJECT_VERSION "6.2.80")
|
||||
set(PROJECT_VERSION_MAJOR 6)
|
||||
set(PROJECT_VERSION "6.6.80")
|
||||
|
||||
set(PROJECT_DEP_VERSION "6.2.0")
|
||||
set(QT_MIN_VERSION "6.7.0")
|
||||
set(KF6_MIN_VERSION "6.5.0")
|
||||
set(PROJECT_DEP_VERSION "6.5.90")
|
||||
set(QT_MIN_VERSION "6.10.0")
|
||||
set(KF6_MIN_VERSION "6.22.0")
|
||||
set(KDE_COMPILERSETTINGS_LEVEL "5.82")
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
|
@ -35,7 +34,9 @@ include(ECMMarkNonGuiExecutable)
|
|||
include(ECMGenerateHeaders)
|
||||
include(ECMFindQmlModule)
|
||||
include(ECMGenerateQmlTypes)
|
||||
include(ECMConfiguredInstall)
|
||||
include(ECMFindQmlModule)
|
||||
include(ECMQtDeclareLoggingCategory)
|
||||
include(GenerateExportHeader)
|
||||
include(KDEGitCommitHooks)
|
||||
include(KDEClangFormat)
|
||||
|
|
@ -57,6 +58,10 @@ find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED
|
|||
WaylandClient
|
||||
)
|
||||
|
||||
if(Qt6WaylandClient_VERSION VERSION_GREATER_EQUAL "6.10.0")
|
||||
find_package(Qt6 REQUIRED COMPONENTS WaylandClientPrivate)
|
||||
endif()
|
||||
|
||||
find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS
|
||||
I18n
|
||||
GlobalAccel
|
||||
|
|
@ -71,6 +76,7 @@ find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS
|
|||
KCMUtils
|
||||
Package
|
||||
JobWidgets
|
||||
WindowSystem
|
||||
)
|
||||
|
||||
find_package(Plasma CONFIG REQUIRED)
|
||||
|
|
@ -78,16 +84,14 @@ find_package(PlasmaQuick CONFIG REQUIRED)
|
|||
find_package(PlasmaActivities CONFIG REQUIRED)
|
||||
find_package(KF6Screen CONFIG REQUIRED)
|
||||
find_package(KWayland CONFIG REQUIRED)
|
||||
find_package(KPipeWire ${PROJECT_DEP_VERSION} REQUIRED)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
|
||||
find_package(QCoro6 REQUIRED COMPONENTS DBus)
|
||||
find_package(QCoro6 REQUIRED COMPONENTS DBus Qml)
|
||||
qcoro_enable_coroutines()
|
||||
kde_enable_exceptions()
|
||||
|
||||
pkg_check_modules(GOBJECT gobject-2.0 REQUIRED IMPORTED_TARGET)
|
||||
pkg_check_modules(GIO gio-2.0 REQUIRED IMPORTED_TARGET)
|
||||
|
||||
find_package(KF6KirigamiAddons 0.6 REQUIRED)
|
||||
find_package(epoxy REQUIRED)
|
||||
find_package(XCB REQUIRED COMPONENTS XCB)
|
||||
|
|
@ -96,6 +100,14 @@ find_package(KWin ${PROJECT_DEP_VERSION} REQUIRED COMPONENTS
|
|||
)
|
||||
|
||||
find_package(LayerShellQt REQUIRED)
|
||||
find_package(Wayland REQUIRED)
|
||||
find_package(PlasmaWaylandProtocols 1.8 CONFIG)
|
||||
set_package_properties(PlasmaWaylandProtocols PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Collection of Plasma-specific Wayland protocols"
|
||||
URL "https://invent.kde.org/libraries/plasma-wayland-protocols/"
|
||||
)
|
||||
|
||||
|
||||
find_package(LibKWorkspace CONFIG REQUIRED)
|
||||
find_package(Libudev REQUIRED)
|
||||
|
|
@ -106,6 +118,12 @@ set_package_properties(KWinDBusInterface PROPERTIES DESCRIPTION "KWin DBus inter
|
|||
PURPOSE "Needed for virtual keyboard toggle button"
|
||||
)
|
||||
|
||||
find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS Auth)
|
||||
set_package_properties(KF6::AuthCore PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Allows Plasma Mobile to configure Waydroid"
|
||||
)
|
||||
|
||||
include(CheckIncludeFiles)
|
||||
|
||||
ecm_find_qmlmodule(org.kde.pipewire 0.1)
|
||||
|
|
@ -116,6 +134,7 @@ plasma_install_package(shell org.kde.plasma.mobileshell shells)
|
|||
add_subdirectory(bin)
|
||||
add_subdirectory(components)
|
||||
add_subdirectory(containments)
|
||||
add_subdirectory(devices)
|
||||
add_subdirectory(quicksettings)
|
||||
add_subdirectory(kcms)
|
||||
add_subdirectory(kded)
|
||||
|
|
@ -127,6 +146,8 @@ if(BUILD_TESTING)
|
|||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
install(FILES org.kde.plasma.mobileshell.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
|
||||
|
||||
find_program(PlasmaOpenSettings plasma-open-settings)
|
||||
set_package_properties(PlasmaOpenSettings PROPERTIES
|
||||
URL https://invent.kde.org/plasma/kde-cli-tools/
|
||||
|
|
@ -146,6 +167,6 @@ ki18n_install(po)
|
|||
if (INSTALL_SYSTEMD_SERVICE)
|
||||
install(FILES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/plasma-mobile.service
|
||||
DESTINATION ${KDE_INSTALL_SYSTEMDUNITDIR}
|
||||
DESTINATION ${KDE_INSTALL_SYSTEMDUNITDIR}/system
|
||||
)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
Copyright (c) <year> <owner>.
|
||||
Copyright (c) <year> <owner>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
|
|
@ -6,6 +6,4 @@ Redistribution and use in source and binary forms, with or without modification,
|
|||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
170
LICENSES/CC-BY-SA-4.0.txt
Normal file
170
LICENSES/CC-BY-SA-4.0.txt
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
Creative Commons Attribution-ShareAlike 4.0 International
|
||||
|
||||
Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.
|
||||
|
||||
Using Creative Commons Public Licenses
|
||||
|
||||
Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses.
|
||||
|
||||
Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors.
|
||||
|
||||
Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described.
|
||||
|
||||
Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public.
|
||||
|
||||
Creative Commons Attribution-ShareAlike 4.0 International Public License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
|
||||
|
||||
Section 1 – Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License.
|
||||
|
||||
d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
|
||||
|
||||
e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
|
||||
|
||||
f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
|
||||
|
||||
g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike.
|
||||
|
||||
h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
|
||||
|
||||
i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
j. Licensor means the individual(s) or entity(ies) granting rights under this Public License.
|
||||
|
||||
k. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
|
||||
|
||||
l. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
|
||||
|
||||
m. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
|
||||
|
||||
Section 2 – Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
A. reproduce and Share the Licensed Material, in whole or in part; and
|
||||
|
||||
B. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section 6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
|
||||
|
||||
B. Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply.
|
||||
|
||||
C. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties.
|
||||
|
||||
Section 3 – License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified form), You must:
|
||||
|
||||
A. retain the following if it is supplied by the Licensor with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
|
||||
|
||||
B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
|
||||
|
||||
C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
|
||||
|
||||
b. ShareAlike.In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply.
|
||||
|
||||
Section 4 – Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
|
||||
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
|
||||
|
||||
Section 5 – Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.
|
||||
|
||||
b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
|
||||
|
||||
Section 6 – Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
|
||||
|
||||
d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
|
||||
|
||||
e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
|
||||
|
||||
Section 7 – Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
|
||||
|
||||
Section 8 – Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
|
||||
|
||||
Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
|
|
@ -45,7 +45,7 @@ Dependencies:
|
|||
* Milou (for search)
|
||||
* Kirigami
|
||||
* Kirigami Addons
|
||||
* hfd-service (optional: for vibrations)
|
||||
* feedbackd (optional: for vibrations)
|
||||
|
||||
To start the shell in a window, run:
|
||||
|
||||
|
|
@ -61,6 +61,10 @@ Useful options:
|
|||
QT_QPA_PLATFORM=wayland dbus-run-session kwin_wayland --xwayland "plasmashell -p org.kde.plasma.mobileshell" --output-count 2 --width 360 --height 720
|
||||
```
|
||||
|
||||
### Device specific configuration
|
||||
|
||||
See [/devices/README.md](/devices/README.md) for more details on setting device specific configuration (ex. notches, screen curves).
|
||||
|
||||
---
|
||||
|
||||
<img src="https://invent.kde.org/plasma/plasma-mobile/-/wikis/uploads/19a607bb68faa76bbc9f888e33a3aa9a/konqi-calling.png" width=200px>
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ Exec=@CMAKE_INSTALL_FULL_LIBEXECDIR@/plasma-dbus-run-session-if-needed @CMAKE_IN
|
|||
Type=Application
|
||||
DesktopNames=KDE
|
||||
Name=Plasma Mobile (Development)
|
||||
Name[ar]=بلازما الجوال (التطوير)
|
||||
Name[az]=Plasma Mobil (Tərtibat)
|
||||
Name[ca]=Plasma Mobile (Desenvolupament)
|
||||
Name[ca@valencia]=Plasma Mobile (Desenvolupament)
|
||||
Name[ca@valencia]=Plasma Mobile (desenvolupament)
|
||||
Name[cs]=Plasma Mobile (Vývojová)
|
||||
Name[de]=Plasma Mobile (Entwicklung)
|
||||
Name[en_GB]=Plasma Mobile (Development)
|
||||
|
|
@ -25,7 +26,7 @@ Name[is]=Plasma Mobile (þróunarútgáfa)
|
|||
Name[it]=Plasma Mobile (sviluppo)
|
||||
Name[ka]=Plasma Mobile (ბეტა)
|
||||
Name[ko]=Plasma 모바일(개발)
|
||||
Name[lv]=„Plasma Mobile“ (izstrādē)
|
||||
Name[lv]=Plasma Mobile (izstrādē)
|
||||
Name[nl]=Plasma Mobile (ontwikkeling)
|
||||
Name[nn]=Plasma Mobile (utvikling)
|
||||
Name[pa]=ਪਲਾਜ਼ਮਾ ਮੋਬਾਈਲ (ਵਿਕਾਸ)
|
||||
|
|
@ -33,16 +34,17 @@ Name[pl]=Przenośna Plazma (Rozwój)
|
|||
Name[pt]=Plasma Mobile (Desenvolvimento)
|
||||
Name[pt_BR]=Plasma Mobile (desenvolvimento)
|
||||
Name[ru]=Plasma Mobile (разработка)
|
||||
Name[sa]=प्लाज्मा मोबाईल (विकास)
|
||||
Name[sk]=Plasma Mobile (Vývoj)
|
||||
Name[sl]=Plasma Mobile (Razvoj)
|
||||
Name[sv]=Plasma mobil (utvecklingsversion)
|
||||
Name[ta]=பிளாஸ்மா கைபேசி (நிரலாக்கம்)
|
||||
Name[tr]=Plasma Cep (Geliştirme)
|
||||
Name[uk]=Мобільна Плазма (версія у розробці)
|
||||
Name[x-test]=xxPlasma Mobile (Development)xx
|
||||
Name[zh_CN]=Plasma Mobile (开发)
|
||||
Name[zh_TW]=Plasma 行動(開發)
|
||||
Comment=Plasma Mobile by KDE
|
||||
Comment[ar]=بلازما الجوال من كيدي
|
||||
Comment[az]=KDE Plasma Mobil
|
||||
Comment[ca]=Plasma Mobile, creat per la comunitat KDE
|
||||
Comment[ca@valencia]=Plasma Mobile, creat per la comunitat KDE
|
||||
|
|
@ -76,13 +78,13 @@ Comment[pt]=Plasma Mobile do KDE
|
|||
Comment[pt_BR]=Plasma Mobile do KDE
|
||||
Comment[ro]=Plasma Mobile, de către KDE
|
||||
Comment[ru]=KDE Plasma Mobile
|
||||
Comment[sa]=केडीई द्वारा प्लाज्मा मोबाईल
|
||||
Comment[sk]=Plasma Mobile od KDE
|
||||
Comment[sl]=Plasma Mobile od KDE
|
||||
Comment[sv]=Plasma mobil av KDE
|
||||
Comment[ta]=கே.டீ.யீ. வழங்கும் பிளாஸ்மா கைபேசி
|
||||
Comment[tr]=KDE tarafından yapılan Plasma Cep
|
||||
Comment[uk]=Мобільна Плазма від KDE
|
||||
Comment[x-test]=xxPlasma Mobile by KDExx
|
||||
Comment[zh_CN]=KDE Plasma Mobile
|
||||
Comment[zh_TW]=來自 KDE 的 Plasma 行動作業系統
|
||||
X-KDE-PluginInfo-Version=${PROJECT_VERSION}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ Name[it]=Plasma Mobile
|
|||
Name[ka]=Plasma Mobile
|
||||
Name[ko]=Plasma 모바일
|
||||
Name[lt]=Plasma mobilioji sąsaja
|
||||
Name[lv]=„Plasma Mobile“
|
||||
Name[lv]=Plasma Mobile
|
||||
Name[nl]=Plasma Mobile
|
||||
Name[nn]=Plasma Mobile
|
||||
Name[pa]=ਪਲਾਜ਼ਮਾ ਮੋਬਾਈਲ
|
||||
|
|
@ -41,16 +41,17 @@ Name[pt]=Plasma Mobile
|
|||
Name[pt_BR]=Plasma Mobile
|
||||
Name[ro]=Plasma Mobile
|
||||
Name[ru]=Plasma Mobile
|
||||
Name[sa]=प्लाज्मा मोबाईल
|
||||
Name[sk]=Plasma Mobile
|
||||
Name[sl]=Plasma Mobile
|
||||
Name[sv]=Plasma mobil
|
||||
Name[ta]=பிளாஸ்மா கைபேசி
|
||||
Name[tr]=Plasma Cep
|
||||
Name[uk]=Мобільна Плазма
|
||||
Name[x-test]=xxPlasma Mobilexx
|
||||
Name[zh_CN]=Plasma Mobile
|
||||
Name[zh_TW]=Plasma 行動
|
||||
Comment=Plasma Mobile by KDE
|
||||
Comment[ar]=بلازما الجوال من كيدي
|
||||
Comment[az]=KDE Plasma Mobil
|
||||
Comment[ca]=Plasma Mobile, creat per la comunitat KDE
|
||||
Comment[ca@valencia]=Plasma Mobile, creat per la comunitat KDE
|
||||
|
|
@ -84,13 +85,13 @@ Comment[pt]=Plasma Mobile do KDE
|
|||
Comment[pt_BR]=Plasma Mobile do KDE
|
||||
Comment[ro]=Plasma Mobile, de către KDE
|
||||
Comment[ru]=KDE Plasma Mobile
|
||||
Comment[sa]=केडीई द्वारा प्लाज्मा मोबाईल
|
||||
Comment[sk]=Plasma Mobile od KDE
|
||||
Comment[sl]=Plasma Mobile od KDE
|
||||
Comment[sv]=Plasma mobil av KDE
|
||||
Comment[ta]=கே.டீ.யீ. வழங்கும் பிளாஸ்மா கைபேசி
|
||||
Comment[tr]=KDE tarafından yapılan Plasma Cep
|
||||
Comment[uk]=Мобільна Плазма від KDE
|
||||
Comment[x-test]=xxPlasma Mobile by KDExx
|
||||
Comment[zh_CN]=KDE Plasma Mobile
|
||||
Comment[zh_TW]=來自 KDE 的 Plasma 行動作業系統
|
||||
X-KDE-PluginInfo-Version=${PROJECT_VERSION}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
[ -f /etc/profile ] && . /etc/profile
|
||||
|
||||
export QT_QPA_PLATFORM=wayland
|
||||
export QT_QPA_PLATFORMTHEME=KDE
|
||||
export EGL_PLATFORM=wayland
|
||||
|
||||
|
|
@ -17,6 +16,9 @@ export QT_QUICK_CONTROLS_MOBILE=true
|
|||
export PLASMA_INTEGRATION_USE_PORTAL=1
|
||||
export PLASMA_PLATFORM=phone:handset
|
||||
|
||||
# Set ~/.config/plasma-mobile/... as location for default mobile configs (i.e. envmanager generated)
|
||||
export XDG_CONFIG_DIRS="$HOME/.config/plasma-mobile:/etc/xdg:$XDG_CONFIG_DIRS"
|
||||
|
||||
# if coredumpd knows about the dumps, make sure drkonqi catches them
|
||||
if grep -q '/systemd-coredump' /proc/sys/kernel/core_pattern
|
||||
then
|
||||
|
|
@ -28,4 +30,4 @@ QT_QPA_PLATFORM=offscreen plasma-mobile-envmanager --apply-settings
|
|||
|
||||
# start the shell
|
||||
export PLASMA_DEFAULT_SHELL=org.kde.plasma.mobileshell
|
||||
startplasma-wayland --xwayland --inputmethod maliit-keyboard
|
||||
startplasma-wayland
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@
|
|||
# SPDX-FileCopyrightText: 2017-2021 Marco Martin <mart@kde.org>
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
add_subdirectory(waydroidintegrationplugin)
|
||||
add_subdirectory(hapticsplugin)
|
||||
add_subdirectory(mmplugin)
|
||||
add_subdirectory(mobileshell)
|
||||
add_subdirectory(mobileshellstate)
|
||||
add_subdirectory(quicksettingsplugin)
|
||||
add_subdirectory(rotationplugin)
|
||||
add_subdirectory(windowplugin)
|
||||
add_subdirectory(shellsettingsplugin)
|
||||
add_subdirectory(wallpaperimageplugin)
|
||||
add_subdirectory(dpmsplugin)
|
||||
add_subdirectory(screenbrightnessplugin)
|
||||
add_subdirectory(raiselockscreenplugin)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
qt_add_dbus_interfaces(DBUS_SRCS dbus/com.lomiri.hfd.xml)
|
||||
set_source_files_properties(dbus/org.sigxcpu.Feedback.Haptic.xml PROPERTIES INCLUDE vibrationevent.h)
|
||||
qt_add_dbus_interfaces(dbusinterface_SRCS
|
||||
dbus/org.sigxcpu.Feedback.Haptic.xml)
|
||||
|
||||
ecm_add_qml_module(hapticsplugin URI org.kde.plasma.private.mobileshell.hapticsplugin GENERATE_PLUGIN_SOURCE)
|
||||
target_sources(hapticsplugin PRIVATE
|
||||
vibrationevent.h
|
||||
vibrationmanager.cpp
|
||||
${DBUS_SRCS}
|
||||
${dbusinterface_SRCS}
|
||||
)
|
||||
|
||||
target_link_libraries(hapticsplugin PRIVATE
|
||||
|
|
@ -14,6 +17,8 @@ target_link_libraries(hapticsplugin PRIVATE
|
|||
Qt::DBus
|
||||
KF6::CoreAddons
|
||||
KF6::I18n
|
||||
QCoro::DBus
|
||||
QCoro::Qml
|
||||
)
|
||||
|
||||
ecm_finalize_qml_module(hapticsplugin)
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2020 Marius Gripsgard <marius@ubports.com>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
<node name="/com/lomiri/hfd">
|
||||
<interface name="com.lomiri.hfd.Vibrator">
|
||||
<method name="vibrate"/>
|
||||
<method name="vibrate">
|
||||
<arg name="durationMs" type="i" direction="in" />
|
||||
</method>
|
||||
<method name="rumble">
|
||||
<arg name="durationMs" type="i" direction="in" />
|
||||
<arg name="repeat" type="i" direction="in" />
|
||||
</method>
|
||||
</interface>
|
||||
<interface name="com.lomiri.hfd.Leds">
|
||||
<method name="setState">
|
||||
<arg name="state" type="i" direction="in" />
|
||||
</method>
|
||||
<method name="setColor">
|
||||
<arg name="color" type="u" direction="in" />
|
||||
</method>
|
||||
<method name="setOnMs">
|
||||
<arg name="onMs" type="i" direction="in" />
|
||||
</method>
|
||||
<method name="setOffMs">
|
||||
<arg name="offMs" type="i" direction="in" />
|
||||
</method>
|
||||
</interface>
|
||||
</node>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Guido Günther <agx@sigxcpu.org>
|
||||
- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
<node>
|
||||
<!-- org.sigxcpu.Feedback.Haptic
|
||||
@short_description: Interface to make a device vibrate
|
||||
|
||||
This D-Bus interface is used to make a device's haptic motor
|
||||
vibrate. This is can be useful e.g. for games.
|
||||
|
||||
To provider user feedback the event based interface should be
|
||||
preferred.
|
||||
-->
|
||||
<interface name="org.sigxcpu.Feedback.Haptic">
|
||||
<!--
|
||||
Vibrate:
|
||||
@app_id: The application id usually in "reverse DNS" format
|
||||
@pattern: The vibration pattern.
|
||||
@success: Whether vibration was triggered
|
||||
|
||||
Triggers the given vibration pattern on the haptic device. The
|
||||
pattern is a sequence of relative amplitude and duration pairs.
|
||||
The amplitude must be between 0.0 and 1.0, durations are in
|
||||
milliseconds.
|
||||
-->
|
||||
<method name="Vibrate">
|
||||
<arg direction="in" name="app_id" type="s"/>
|
||||
<arg direction="in" name="pattern" type="a(du)"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="VibrationEventList" />
|
||||
<arg direction="out" name="success" type="b"/>
|
||||
</method>
|
||||
|
||||
</interface>
|
||||
|
||||
</node>
|
||||
80
components/hapticsplugin/dbus/org.sigxcpu.Feedback.xml
Normal file
80
components/hapticsplugin/dbus/org.sigxcpu.Feedback.xml
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Guido Günther <agx@sigxcpu.org>
|
||||
- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
<node>
|
||||
<!-- org.sigxcpu.Feedback
|
||||
@short_description: haptic/audio/visual feedback interface
|
||||
|
||||
This D-Bus interface is used to get the current feedback theme
|
||||
and to give feedback on events.
|
||||
-->
|
||||
<interface name="org.sigxcpu.Feedback">
|
||||
<!--
|
||||
Profile: The currently used profile.
|
||||
|
||||
The currently used feedback profile name. Applications should
|
||||
usually not change this value.
|
||||
-->
|
||||
<property name="Profile" type="s" access="readwrite" />
|
||||
|
||||
<!--
|
||||
TriggerFeedback:
|
||||
@app_id: The application id usually in "reverse DNS" format
|
||||
@event: The event name from the Event naming spec
|
||||
@hints: Additional hints. Currently known hints
|
||||
- profile: Override the profile used for this event with the given profile name
|
||||
- important: Override the current global feedback level.
|
||||
Together with the 'profile' hint this allows to trigger feedback for events
|
||||
that would otherwise be disabled. A typical use case is an alarm clock. Note
|
||||
that the feedback daemon (depending on it's configuration) might ignore this flag.
|
||||
- sound-file: A custom sound file to play. This file will be used instead of any
|
||||
sound event specified in the "full" profile. The sound will only be played if
|
||||
appropriate for the feedback level of the event.
|
||||
@timeout: When the feedbacks for this event should end latest in seconds. The special
|
||||
values '-1' (just run each feedback once) and '0' (endless loop) are also supported.
|
||||
@id: Event id for future reference
|
||||
|
||||
Give user feedback for an event by triggering feedbacks
|
||||
defined in the daemon. The method call returns an event id
|
||||
that can be used later on to e.g. cancel the triggered
|
||||
feedbacks early.
|
||||
|
||||
Depending on the event, theme and profile several forms of
|
||||
feedback will be triggered such as an audio ring tone and a
|
||||
haptic motor.
|
||||
-->
|
||||
<method name="TriggerFeedback">
|
||||
<arg direction="in" name="app_id" type="s"/>
|
||||
<arg direction="in" name="event" type="s"/>
|
||||
<arg direction="in" name="hints" type="a{sv}"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QVariantMap" />
|
||||
<arg direction="in" name="timeout" type="i"/>
|
||||
<arg direction="out" name="id" type="u"/>
|
||||
</method>
|
||||
|
||||
<!--
|
||||
EndFeedback:
|
||||
@id: The id of the event
|
||||
|
||||
End all feedbacks triggered by the event with the given id.
|
||||
-->
|
||||
<method name="EndFeedback">
|
||||
<arg direction="in" name="id" type="u"/>
|
||||
</method>
|
||||
|
||||
<!--
|
||||
FeedbackEnded:
|
||||
@id: The id of the event
|
||||
@reason: The reason why feedback was ended (currently unused).
|
||||
|
||||
Emitted when all feedbacks for an event have ended.
|
||||
-->
|
||||
<signal name="FeedbackEnded">
|
||||
<arg name="id" type="u"/>
|
||||
<arg name="reason" type="u"/>
|
||||
</signal>
|
||||
</interface>
|
||||
|
||||
</node>
|
||||
37
components/hapticsplugin/vibrationevent.h
Normal file
37
components/hapticsplugin/vibrationevent.h
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDBusArgument>
|
||||
#include <QList>
|
||||
|
||||
class VibrationEvent
|
||||
{
|
||||
public:
|
||||
double amplitude;
|
||||
quint32 duration;
|
||||
};
|
||||
|
||||
using VibrationEventList = QList<VibrationEvent>;
|
||||
|
||||
Q_DECLARE_METATYPE(VibrationEvent)
|
||||
Q_DECLARE_METATYPE(VibrationEventList)
|
||||
|
||||
inline QDBusArgument &operator<<(QDBusArgument &argument, const VibrationEvent &e)
|
||||
{
|
||||
argument.beginStructure();
|
||||
argument << e.amplitude;
|
||||
argument << e.duration;
|
||||
argument.endStructure();
|
||||
return argument;
|
||||
}
|
||||
|
||||
inline const QDBusArgument &operator>>(const QDBusArgument &argument, VibrationEvent &e)
|
||||
{
|
||||
argument.beginStructure();
|
||||
argument >> e.amplitude;
|
||||
argument >> e.duration;
|
||||
argument.endStructure();
|
||||
return argument;
|
||||
}
|
||||
|
|
@ -1,19 +1,42 @@
|
|||
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Devin Lin <devin@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "vibrationmanager.h"
|
||||
|
||||
#include <QPointer>
|
||||
|
||||
VibrationManager::VibrationManager(QObject *parent)
|
||||
: QObject{parent}
|
||||
{
|
||||
qDBusRegisterMetaType<VibrationEvent>();
|
||||
qDBusRegisterMetaType<VibrationEventList>();
|
||||
}
|
||||
|
||||
void VibrationManager::vibrate(int durationMs)
|
||||
QCoro::Task<void> VibrationManager::vibrateTask(int durationMs)
|
||||
{
|
||||
// Only create interface when needed.
|
||||
if (!m_interface) {
|
||||
const auto objectPath = QStringLiteral("/com/lomiri/hfd");
|
||||
m_interface = new com::lomiri::hfd::Vibrator("com.lomiri.hfd", objectPath, QDBusConnection::systemBus(), this);
|
||||
const auto objectPath = QStringLiteral("/org/sigxcpu/Feedback");
|
||||
m_interface = new OrgSigxcpuFeedbackHapticInterface("org.sigxcpu.Feedback", objectPath, QDBusConnection::sessionBus(), this);
|
||||
}
|
||||
|
||||
const QString appId = QStringLiteral("org.kde.plasmashell");
|
||||
const VibrationEvent event{1.0, static_cast<quint32>(durationMs)};
|
||||
const VibrationEventList pattern = {event};
|
||||
|
||||
QPointer<VibrationManager> guard(this);
|
||||
QDBusPendingReply<bool> reply = co_await m_interface->Vibrate(appId, pattern);
|
||||
|
||||
if (!guard) {
|
||||
co_return;
|
||||
}
|
||||
|
||||
if (!reply.isValid() || !reply.value()) {
|
||||
qWarning() << "feedbackd vibration failed";
|
||||
}
|
||||
m_interface->vibrate(durationMs);
|
||||
}
|
||||
|
||||
QCoro::QmlTask VibrationManager::vibrate(int durationMs)
|
||||
{
|
||||
return vibrateTask(durationMs);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,17 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <qqmlregistration.h>
|
||||
|
||||
#include "hfdinterface.h"
|
||||
#include "hapticinterface.h"
|
||||
#include "vibrationevent.h"
|
||||
|
||||
#include <QCoroCore>
|
||||
#include <QCoroDBusPendingReply>
|
||||
#include <QCoroQml>
|
||||
#include <QCoroQmlTask>
|
||||
|
||||
class VibrationManager : public QObject
|
||||
{
|
||||
|
|
@ -17,8 +24,9 @@ class VibrationManager : public QObject
|
|||
public:
|
||||
VibrationManager(QObject *parent = nullptr);
|
||||
|
||||
Q_INVOKABLE void vibrate(int durationMs);
|
||||
QCoro::Task<void> vibrateTask(int durationMs);
|
||||
Q_INVOKABLE QCoro::QmlTask vibrate(int durationMs);
|
||||
|
||||
private:
|
||||
com::lomiri::hfd::Vibrator *m_interface{nullptr};
|
||||
};
|
||||
OrgSigxcpuFeedbackHapticInterface *m_interface{nullptr};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ target_link_libraries(ppc-mmqmlplugin PRIVATE
|
|||
KF6::NetworkManagerQt
|
||||
KF6::CoreAddons
|
||||
KF6::I18n
|
||||
QCoro::DBus
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
#include <NetworkManagerQt/Manager>
|
||||
#include <NetworkManagerQt/Settings>
|
||||
#include <NetworkManagerQt/Utils>
|
||||
#include <QDBusReply>
|
||||
#include <QPointer>
|
||||
|
||||
#include <KUser>
|
||||
|
||||
|
|
@ -197,11 +199,11 @@ void SignalIndicator::refreshProfiles()
|
|||
Q_EMIT profileListChanged();
|
||||
}
|
||||
|
||||
void SignalIndicator::activateProfile(const QString &connectionUni)
|
||||
QCoro::Task<void> SignalIndicator::activateProfile(const QString &connectionUni)
|
||||
{
|
||||
if (!m_nmModem) {
|
||||
qWarning() << "Cannot activate profile since there is no NetworkManager modem";
|
||||
return;
|
||||
co_return;
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("Activating profile on modem") << m_nmModem->uni() << QStringLiteral("for connection") << connectionUni << ".";
|
||||
|
|
@ -220,24 +222,29 @@ void SignalIndicator::activateProfile(const QString &connectionUni)
|
|||
|
||||
if (!con) {
|
||||
qDebug() << QStringLiteral("Connection") << connectionUni << QStringLiteral("not found.");
|
||||
return;
|
||||
co_return;
|
||||
}
|
||||
|
||||
// activate connection manually
|
||||
// despite the documentation saying otherwise, activateConnection seems to need the DBus path, not uuid of the connection
|
||||
QDBusPendingReply<QDBusObjectPath> reply = NetworkManager::activateConnection(con->path(), m_nmModem->uni(), {});
|
||||
reply.waitForFinished();
|
||||
if (reply.isError()) {
|
||||
QPointer<SignalIndicator> guard(this);
|
||||
QDBusReply<QDBusObjectPath> reply = co_await NetworkManager::activateConnection(con->path(), m_nmModem->uni(), {});
|
||||
|
||||
if (!guard) {
|
||||
co_return;
|
||||
}
|
||||
|
||||
if (!reply.isValid()) {
|
||||
qWarning() << QStringLiteral("Error activating connection:") << reply.error().message();
|
||||
return;
|
||||
co_return;
|
||||
}
|
||||
}
|
||||
|
||||
void SignalIndicator::addProfile(const QString &name, const QString &apn, const QString &username, const QString &password, const QString &networkType)
|
||||
QCoro::Task<void> SignalIndicator::addProfile(const QString &name, const QString &apn, const QString &username, const QString &password, const QString &networkType)
|
||||
{
|
||||
if (!m_nmModem) {
|
||||
qWarning() << "Cannot add profile since there is no NetworkManager modem";
|
||||
return;
|
||||
co_return;
|
||||
}
|
||||
|
||||
NetworkManager::ConnectionSettings::Ptr settings{new NetworkManager::ConnectionSettings(NetworkManager::ConnectionSettings::Gsm)};
|
||||
|
|
@ -255,47 +262,57 @@ void SignalIndicator::addProfile(const QString &name, const QString &apn, const
|
|||
|
||||
gsmSetting->setInitialized(true);
|
||||
|
||||
QDBusPendingReply<QDBusObjectPath> reply = NetworkManager::addAndActivateConnection(settings->toMap(), m_nmModem->uni(), {});
|
||||
reply.waitForFinished();
|
||||
if (reply.isError()) {
|
||||
QPointer<SignalIndicator> guard(this);
|
||||
QDBusReply<QDBusObjectPath> reply = co_await NetworkManager::addAndActivateConnection(settings->toMap(), m_nmModem->uni(), {});
|
||||
|
||||
if (!guard) {
|
||||
co_return;
|
||||
}
|
||||
|
||||
if (!reply.isValid()) {
|
||||
qWarning() << "Error adding connection:" << reply.error().message();
|
||||
} else {
|
||||
qDebug() << "Successfully added a new connection" << name << "with APN" << apn << ".";
|
||||
}
|
||||
}
|
||||
|
||||
void SignalIndicator::removeProfile(const QString &connectionUni)
|
||||
QCoro::Task<void> SignalIndicator::removeProfile(const QString &connectionUni)
|
||||
{
|
||||
NetworkManager::Connection::Ptr con = NetworkManager::findConnectionByUuid(connectionUni);
|
||||
if (!con) {
|
||||
qWarning() << "Could not find connection" << connectionUni << "to update!";
|
||||
return;
|
||||
co_return;
|
||||
}
|
||||
|
||||
QDBusPendingReply reply = con->remove();
|
||||
reply.waitForFinished();
|
||||
if (reply.isError()) {
|
||||
QPointer<SignalIndicator> guard(this);
|
||||
QDBusPendingReply reply = co_await con->remove();
|
||||
|
||||
if (!guard) {
|
||||
co_return;
|
||||
}
|
||||
|
||||
if (!reply.isValid()) {
|
||||
qWarning() << "Error removing connection" << reply.error().message();
|
||||
}
|
||||
}
|
||||
|
||||
void SignalIndicator::updateProfile(const QString &connectionUni,
|
||||
const QString &name,
|
||||
const QString &apn,
|
||||
const QString &username,
|
||||
const QString &password,
|
||||
const QString &networkType)
|
||||
QCoro::Task<void> SignalIndicator::updateProfile(const QString &connectionUni,
|
||||
const QString &name,
|
||||
const QString &apn,
|
||||
const QString &username,
|
||||
const QString &password,
|
||||
const QString &networkType)
|
||||
{
|
||||
NetworkManager::Connection::Ptr con = NetworkManager::findConnectionByUuid(connectionUni);
|
||||
if (!con) {
|
||||
qWarning() << "Could not find connection" << connectionUni << "to update!";
|
||||
return;
|
||||
co_return;
|
||||
}
|
||||
|
||||
NetworkManager::ConnectionSettings::Ptr conSettings = con->settings();
|
||||
if (!conSettings) {
|
||||
qWarning() << "Could not find connection settings for" << connectionUni << "to update!";
|
||||
return;
|
||||
co_return;
|
||||
}
|
||||
|
||||
conSettings->setId(name);
|
||||
|
|
@ -309,9 +326,14 @@ void SignalIndicator::updateProfile(const QString &connectionUni,
|
|||
|
||||
gsmSetting->setInitialized(true);
|
||||
|
||||
QDBusPendingReply reply = con->update(conSettings->toMap());
|
||||
reply.waitForFinished();
|
||||
if (reply.isError()) {
|
||||
QPointer<SignalIndicator> guard(this);
|
||||
QDBusPendingReply reply = co_await con->update(conSettings->toMap());
|
||||
|
||||
if (!guard) {
|
||||
co_return;
|
||||
}
|
||||
|
||||
if (!reply.isValid()) {
|
||||
qWarning() << "Error updating connection settings for" << connectionUni << ":" << reply.error().message() << ".";
|
||||
} else {
|
||||
qDebug() << "Successfully updated connection settings" << connectionUni << ".";
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
#include <NetworkManagerQt/Connection>
|
||||
#include <NetworkManagerQt/ModemDevice>
|
||||
#include <QCoroDBusPendingReply>
|
||||
|
||||
#include <QObject>
|
||||
#include <qqmlregistration.h>
|
||||
|
|
@ -53,10 +54,10 @@ public:
|
|||
// connection profiles
|
||||
QList<ProfileSettings *> &profileList();
|
||||
void refreshProfiles();
|
||||
Q_INVOKABLE void activateProfile(const QString &connectionUni);
|
||||
Q_INVOKABLE void addProfile(const QString &name, const QString &apn, const QString &username, const QString &password, const QString &networkType);
|
||||
Q_INVOKABLE void removeProfile(const QString &connectionUni);
|
||||
Q_INVOKABLE void updateProfile(const QString &connectionUni,
|
||||
Q_INVOKABLE QCoro::Task<void> activateProfile(const QString &connectionUni);
|
||||
Q_INVOKABLE QCoro::Task<void> addProfile(const QString &name, const QString &apn, const QString &username, const QString &password, const QString &networkType);
|
||||
Q_INVOKABLE QCoro::Task<void> removeProfile(const QString &connectionUni);
|
||||
Q_INVOKABLE QCoro::Task<void> updateProfile(const QString &connectionUni,
|
||||
const QString &name,
|
||||
const QString &apn,
|
||||
const QString &username,
|
||||
|
|
|
|||
|
|
@ -11,27 +11,136 @@ set(mobileshellplugin_SRCS
|
|||
notifications/notificationthumbnailer.cpp
|
||||
notifications/notificationfilemenu.cpp
|
||||
notifications/notificationfileinfo.cpp
|
||||
masklayer/masklayer.cpp
|
||||
masklayer/maskmanager.cpp
|
||||
)
|
||||
target_include_directories(mobileshellplugin PRIVATE components)
|
||||
target_include_directories(mobileshellplugin PRIVATE notifications)
|
||||
target_include_directories(mobileshellplugin PRIVATE masklayer)
|
||||
target_sources(mobileshellplugin PRIVATE ${mobileshellplugin_SRCS})
|
||||
|
||||
# Singleton declarations
|
||||
set_source_files_properties(qml/components/AppLaunch.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
|
||||
set_source_files_properties(qml/components/Constants.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
|
||||
set_source_files_properties(qml/dataproviders/AudioInfo.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
|
||||
set_source_files_properties(qml/dataproviders/BatteryInfo.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
|
||||
set_source_files_properties(qml/dataproviders/BluetoothInfo.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
|
||||
set_source_files_properties(qml/dataproviders/SignalStrengthInfo.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
|
||||
set_source_files_properties(qml/notificationpopup/NotificationPopupProviderLoader.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
|
||||
set_source_files_properties(qml/volumeosd/VolumeOSDProviderLoader.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
|
||||
|
||||
# Include qml and js files within ./qml/
|
||||
file(GLOB_RECURSE _qml_sources
|
||||
"qml/*.qml"
|
||||
"qml/*.js"
|
||||
set_source_files_properties(
|
||||
qml/components/AppLaunch.qml
|
||||
qml/components/Constants.qml
|
||||
qml/dataproviders/AudioInfo.qml
|
||||
qml/dataproviders/BatteryInfo.qml
|
||||
qml/dataproviders/BluetoothInfo.qml
|
||||
qml/dataproviders/NetworkInfo.qml
|
||||
qml/dataproviders/SignalStrengthInfo.qml
|
||||
qml/popups/PopupProviderLoader.qml
|
||||
PROPERTIES
|
||||
QT_QML_SINGLETON_TYPE TRUE
|
||||
)
|
||||
|
||||
# QML files meant to be used publicly
|
||||
# TODO: move files gradually out of main namespace into sub namespaces
|
||||
ecm_target_qml_sources(mobileshellplugin SOURCES
|
||||
qml/actiondrawer/ActionDrawer.qml
|
||||
qml/actiondrawer/ActionDrawerOpenSurface.qml
|
||||
qml/actiondrawer/ActionDrawerWindow.qml
|
||||
|
||||
qml/components/AppLaunch.qml
|
||||
qml/components/BaseItem.qml
|
||||
qml/components/Constants.qml
|
||||
qml/components/Flickable.qml
|
||||
qml/components/FlickableOpacityGradient.qml
|
||||
qml/components/GridView.qml
|
||||
qml/components/HapticsEffect.qml
|
||||
qml/components/ListView.qml
|
||||
qml/components/MarqueeLabel.qml
|
||||
qml/components/PanelBackground.qml
|
||||
qml/components/ScreenEdgeDragEffect.qml
|
||||
qml/components/StartupFeedbackPanelFill.qml
|
||||
qml/components/StartupFeedbackWindows.qml
|
||||
qml/components/TextDropShadow.qml
|
||||
qml/components/VelocityCalculator.qml
|
||||
|
||||
qml/dataproviders/AudioInfo.qml
|
||||
qml/dataproviders/BatteryInfo.qml
|
||||
qml/dataproviders/BluetoothInfo.qml
|
||||
qml/dataproviders/NetworkInfo.qml
|
||||
qml/dataproviders/SignalStrengthInfo.qml
|
||||
|
||||
qml/homescreen/BlurEffect.qml
|
||||
qml/homescreen/DeviceLock.qml
|
||||
qml/homescreen/HomeScreen.qml
|
||||
qml/homescreen/WallpaperSelector.qml
|
||||
|
||||
qml/navigationpanel/GesturePanel.qml
|
||||
qml/navigationpanel/NavigationPanel.qml
|
||||
qml/navigationpanel/NavigationPanelAction.qml
|
||||
qml/navigationpanel/NavigationPanelButton.qml
|
||||
|
||||
qml/popups/actionbuttons/ActionButton.qml
|
||||
qml/popups/actionbuttons/ActionButtonsProvider.qml
|
||||
qml/popups/actionbuttons/RotationButton.qml
|
||||
|
||||
qml/popups/notifications/NotificationPopup.qml
|
||||
qml/popups/notifications/NotificationPopupManager.qml
|
||||
qml/popups/notifications/NotificationPopupProvider.qml
|
||||
qml/popups/notifications/PulseAudio.qml
|
||||
|
||||
qml/popups/kscreenosd/KScreenOSDProvider.qml
|
||||
|
||||
qml/popups/volumeosd/AudioApplet.qml
|
||||
qml/popups/volumeosd/DeviceListItem.qml
|
||||
qml/popups/volumeosd/icon.js
|
||||
qml/popups/volumeosd/ListItemBase.qml
|
||||
qml/popups/volumeosd/PopupCard.qml
|
||||
qml/popups/volumeosd/StreamListItem.qml
|
||||
qml/popups/volumeosd/VolumeChangedPopup.qml
|
||||
qml/popups/volumeosd/VolumeOSD.qml
|
||||
qml/popups/volumeosd/VolumeOSDProvider.qml
|
||||
qml/popups/volumeosd/VolumeSlider.qml
|
||||
qml/popups/PopupProviderLoader.qml
|
||||
|
||||
qml/statusbar/indicators/BatteryIndicator.qml
|
||||
qml/statusbar/indicators/BluetoothIndicator.qml
|
||||
qml/statusbar/indicators/InternetIndicator.qml
|
||||
qml/statusbar/indicators/SignalStrengthIndicator.qml
|
||||
qml/statusbar/indicators/VolumeIndicator.qml
|
||||
qml/statusbar/ClockText.qml
|
||||
qml/statusbar/StatusBar.qml
|
||||
|
||||
qml/widgets/krunner/KRunnerScreen.qml
|
||||
qml/widgets/mediacontrols/BlurredBackground.qml
|
||||
qml/widgets/mediacontrols/MediaControlsSource.qml
|
||||
qml/widgets/mediacontrols/MediaControlsWidget.qml
|
||||
qml/widgets/notifications/BaseNotificationItem.qml
|
||||
qml/widgets/notifications/NotificationBodyLabel.qml
|
||||
qml/widgets/notifications/NotificationCard.qml
|
||||
qml/widgets/notifications/NotificationEditContextMenu.qml
|
||||
qml/widgets/notifications/NotificationFooterActions.qml
|
||||
qml/widgets/notifications/NotificationGroupHeader.qml
|
||||
qml/widgets/notifications/NotificationItem.qml
|
||||
qml/widgets/notifications/NotificationJobDetails.qml
|
||||
qml/widgets/notifications/NotificationJobItem.qml
|
||||
qml/widgets/notifications/NotificationPopupItem.qml
|
||||
qml/widgets/notifications/NotificationReplyField.qml
|
||||
qml/widgets/notifications/NotificationsModelType.qml
|
||||
qml/widgets/notifications/NotificationsUtils.js
|
||||
qml/widgets/notifications/NotificationsWidget.qml
|
||||
qml/widgets/notifications/NotificationTimeText.qml
|
||||
qml/widgets/notifications/ThumbnailStrip.qml
|
||||
)
|
||||
|
||||
# Other supporting QML files
|
||||
ecm_target_qml_sources(mobileshellplugin SOURCES
|
||||
qml/actiondrawer/private/BrightnessItem.qml
|
||||
qml/actiondrawer/private/Handle.qml
|
||||
qml/actiondrawer/private/QuickSettings.qml
|
||||
qml/actiondrawer/private/QuickSettingsDelegate.qml
|
||||
qml/actiondrawer/private/QuickSettingsDrawer.qml
|
||||
qml/actiondrawer/private/QuickSettingsFullDelegate.qml
|
||||
qml/actiondrawer/private/QuickSettingsMinimizedDelegate.qml
|
||||
qml/actiondrawer/private/QuickSettingsPanel.qml
|
||||
qml/actiondrawer/private/ContentContainer.qml
|
||||
qml/actiondrawer/private/LandscapeContentContainer.qml
|
||||
qml/actiondrawer/private/NotificationDrawer.qml
|
||||
qml/actiondrawer/private/PortraitContentContainer.qml
|
||||
PATH actiondrawer/private
|
||||
)
|
||||
ecm_target_qml_sources(mobileshellplugin SOURCES ${_qml_sources})
|
||||
|
||||
|
||||
target_link_libraries(mobileshellplugin
|
||||
|
|
|
|||
268
components/mobileshell/masklayer/masklayer.cpp
Normal file
268
components/mobileshell/masklayer/masklayer.cpp
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
// SPDX-FileCopyrightText: 2025 Micah Stanley <stanleymicah@proton.me>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "masklayer.h"
|
||||
|
||||
#include <QSGFlatColorMaterial>
|
||||
|
||||
// helper function for creating rounded rectangles
|
||||
static void createRoundedRectGeometry(QSGGeometry *geometry, const QRectF &rect, qreal radius)
|
||||
{
|
||||
geometry->setDrawingMode(QSGGeometry::DrawTriangles);
|
||||
radius = qMin(radius, qMin(rect.width(), rect.height()) / 2.0); // clamp radius
|
||||
|
||||
// if the radius is too small, draw a simple rectangle instead
|
||||
if (radius < 0.1) {
|
||||
// 4 vertices, 6 indices (2 triangles * 3 indices)
|
||||
geometry->allocate(4, 6);
|
||||
|
||||
// fill vertex data
|
||||
QSGGeometry::Point2D *vertices = geometry->vertexDataAsPoint2D();
|
||||
vertices[0].set(rect.left(), rect.top());
|
||||
vertices[1].set(rect.right(), rect.top());
|
||||
vertices[2].set(rect.left(), rect.bottom());
|
||||
vertices[3].set(rect.right(), rect.bottom());
|
||||
|
||||
// fill index data
|
||||
quint16 *indices = geometry->indexDataAsUShort();
|
||||
indices[0] = 0; indices[1] = 2; indices[2] = 1; // first triangle (TL, BL, TR)
|
||||
indices[3] = 1; indices[4] = 2; indices[5] = 3; // second triangle (TR, BL, BR)
|
||||
|
||||
geometry->markVertexDataDirty();
|
||||
geometry->markIndexDataDirty();
|
||||
return;
|
||||
}
|
||||
|
||||
const int segments_per_corner = 16;
|
||||
const int perimeter_verts = segments_per_corner * 4;
|
||||
const int vertex_count = 1 + perimeter_verts;
|
||||
const int index_count = perimeter_verts * 3;
|
||||
|
||||
geometry->allocate(vertex_count, index_count);
|
||||
|
||||
QSGGeometry::Point2D *vertices = geometry->vertexDataAsPoint2D();
|
||||
quint16 *indices = geometry->indexDataAsUShort();
|
||||
|
||||
int vertIndex = 0;
|
||||
int indexPos = 0;
|
||||
|
||||
// define the center vertex
|
||||
const quint16 center_vert_index = vertIndex;
|
||||
vertices[vertIndex++].set(rect.center().x(), rect.center().y());
|
||||
|
||||
// define the center of the corners
|
||||
const QPointF tl_c = {rect.left() + radius, rect.top() + radius};
|
||||
const QPointF tr_c = {rect.right() - radius, rect.top() + radius};
|
||||
const QPointF br_c = {rect.right() - radius, rect.bottom() - radius};
|
||||
const QPointF bl_c = {rect.left() + radius, rect.bottom() - radius};
|
||||
|
||||
// create all perimeter vertices
|
||||
// top-right
|
||||
for (int i = 0; i < segments_per_corner; ++i) {
|
||||
const qreal angle = M_PI * 1.5 + (M_PI_2 * i / segments_per_corner);
|
||||
vertices[vertIndex++].set(tr_c.x() + radius * cos(angle), tr_c.y() + radius * sin(angle));
|
||||
}
|
||||
// bottom-right
|
||||
for (int i = 0; i < segments_per_corner; ++i) {
|
||||
const qreal angle = (M_PI_2 * i / segments_per_corner);
|
||||
vertices[vertIndex++].set(br_c.x() + radius * cos(angle), br_c.y() + radius * sin(angle));
|
||||
}
|
||||
// bottom-left
|
||||
for (int i = 0; i < segments_per_corner; ++i) {
|
||||
const qreal angle = M_PI_2 + (M_PI_2 * i / segments_per_corner);
|
||||
vertices[vertIndex++].set(bl_c.x() + radius * cos(angle), bl_c.y() + radius * sin(angle));
|
||||
}
|
||||
// top-left
|
||||
for (int i = 0; i < segments_per_corner; ++i) {
|
||||
const qreal angle = M_PI + (M_PI_2 * i / segments_per_corner);
|
||||
vertices[vertIndex++].set(tl_c.x() + radius * cos(angle), tl_c.y() + radius * sin(angle));
|
||||
}
|
||||
|
||||
// create the triangles using indices
|
||||
// loop through all perimeter vertices and connect them to the center and the next vertex
|
||||
for (quint16 i = 0; i < perimeter_verts; ++i) {
|
||||
indices[indexPos++] = center_vert_index; // center vertex
|
||||
indices[indexPos++] = center_vert_index + 1 + i; // current perimeter vertex
|
||||
// the next perimeter vertex / wrapping around to the start at the end
|
||||
indices[indexPos++] = center_vert_index + 1 + ((i + 1) % perimeter_verts);
|
||||
}
|
||||
|
||||
// tell renderer to mark all the data as dirty
|
||||
geometry->markVertexDataDirty();
|
||||
geometry->markIndexDataDirty();
|
||||
}
|
||||
|
||||
MaskLayer::MaskLayer(QQuickItem *parent) : QQuickItem(parent)
|
||||
{
|
||||
setFlag(ItemHasContents, true);
|
||||
}
|
||||
|
||||
MaskLayer::~MaskLayer() = default;
|
||||
|
||||
void MaskLayer::addItem(QQuickItem* item)
|
||||
{
|
||||
if (!item || m_sourceItems.contains(item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_sourceItems.append(item);
|
||||
|
||||
// we connect these signals so that any changes that affects the item's visual representation triggers an update
|
||||
// we then store connections to be able to disconnect them later
|
||||
auto& conns = m_connections[item];
|
||||
conns.append(QObject::connect(item, &QQuickItem::xChanged, this, &MaskLayer::scheduleUpdate));
|
||||
conns.append(QObject::connect(item, &QQuickItem::yChanged, this, &MaskLayer::scheduleUpdate));
|
||||
conns.append(QObject::connect(item, &QQuickItem::visibleChanged, this, &MaskLayer::scheduleUpdate));
|
||||
conns.append(QObject::connect(item, &QQuickItem::opacityChanged, this, &MaskLayer::scheduleUpdate));
|
||||
conns.append(QObject::connect(item, &QObject::destroyed, this, [this, item]() {
|
||||
removeItem(item);
|
||||
}));
|
||||
|
||||
const QMetaObject* metaObject = item->metaObject();
|
||||
|
||||
// due to not being about to tell when the item's transform value changes
|
||||
// we check for 'scaleAmountChanged()' to use as a sort of work around
|
||||
int scaleAmountIndex = metaObject->indexOfProperty("scaleAmount");
|
||||
if (scaleAmountIndex != -1 && metaObject->property(scaleAmountIndex).hasNotifySignal()) {
|
||||
conns.append(QObject::connect(item, SIGNAL(scaleAmountChanged()), this, SLOT(scheduleUpdate())));
|
||||
}
|
||||
|
||||
// connect the parents signal changes, as this affects the final visible outcome
|
||||
QQuickItem* currentParent = item->parentItem();
|
||||
while (currentParent) {
|
||||
conns.append(QObject::connect(currentParent, &QQuickItem::xChanged, this, &MaskLayer::scheduleUpdate));
|
||||
conns.append(QObject::connect(currentParent, &QQuickItem::yChanged, this, &MaskLayer::scheduleUpdate));
|
||||
conns.append(QObject::connect(currentParent, &QQuickItem::opacityChanged, this, &MaskLayer::scheduleUpdate));
|
||||
|
||||
const QMetaObject* metaObject = currentParent->metaObject();
|
||||
|
||||
// check for 'scaleAmountChanged()'
|
||||
int scaleAmountIndex = metaObject->indexOfProperty("scaleAmount");
|
||||
if (scaleAmountIndex != -1 && metaObject->property(scaleAmountIndex).hasNotifySignal()) {
|
||||
conns.append(QObject::connect(currentParent, SIGNAL(scaleAmountChanged()), this, SLOT(scheduleUpdate())));
|
||||
}
|
||||
|
||||
currentParent = currentParent->parentItem();
|
||||
}
|
||||
|
||||
scheduleUpdate();
|
||||
}
|
||||
|
||||
void MaskLayer::removeItem(QQuickItem* item)
|
||||
{
|
||||
if (!item) return;
|
||||
|
||||
disconnectItemSignals(item);
|
||||
m_connections.remove(item);
|
||||
m_sourceItems.removeAll(item);
|
||||
scheduleUpdate();
|
||||
}
|
||||
|
||||
void MaskLayer::disconnectItemSignals(QQuickItem* item)
|
||||
{
|
||||
if (m_connections.contains(item)) {
|
||||
for (const auto &conn : m_connections.value(item)) {
|
||||
QObject::disconnect(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MaskLayer::scheduleUpdate()
|
||||
{
|
||||
// marks this item for an update.
|
||||
// the renderer will call updatePaintNode before the next frame
|
||||
update();
|
||||
}
|
||||
|
||||
QSGNode *MaskLayer::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
|
||||
{
|
||||
// if oldNode is null, we need to create a new root node for our content
|
||||
// otherwise, we can reuse it and manage its children
|
||||
QSGNode *rootNode = oldNode;
|
||||
if (!rootNode) {
|
||||
rootNode = new QSGNode();
|
||||
}
|
||||
|
||||
int currentChildIndex = 0;
|
||||
|
||||
for (const QPointer<QQuickItem>& itemPtr : m_sourceItems) {
|
||||
QQuickItem* item = itemPtr.data();
|
||||
// item was deleted
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// calculate opacity and visibility
|
||||
qreal accumulatedOpacity = item->opacity();
|
||||
bool isVisible = item->isVisible();
|
||||
QQuickItem* currentParent = item->parentItem();
|
||||
while (currentParent) {
|
||||
if (!currentParent->isVisible()) {
|
||||
isVisible = false;
|
||||
break;
|
||||
}
|
||||
accumulatedOpacity *= currentParent->opacity();
|
||||
if (currentParent == this) break;
|
||||
currentParent = currentParent->parentItem();
|
||||
}
|
||||
|
||||
// skip this item if it is invisible or fully transparent
|
||||
if (!isVisible || qFuzzyCompare(accumulatedOpacity, 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// calculate position and size
|
||||
bool transformOk = false;
|
||||
const QTransform transform = item->itemTransform(this, &transformOk);
|
||||
if (!transformOk) continue;
|
||||
|
||||
qreal radius = item->property("radius").toReal();
|
||||
|
||||
QSGTransformNode *transformNode = nullptr;
|
||||
QSGGeometryNode *geometryNode = nullptr;
|
||||
|
||||
if (currentChildIndex < rootNode->childCount()) {
|
||||
transformNode = static_cast<QSGTransformNode*>(rootNode->childAtIndex(currentChildIndex));
|
||||
geometryNode = static_cast<QSGGeometryNode*>(transformNode->firstChild());
|
||||
} else {
|
||||
transformNode = new QSGTransformNode();
|
||||
geometryNode = new QSGGeometryNode();
|
||||
|
||||
QSGGeometry *geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0);
|
||||
|
||||
geometryNode->setGeometry(geometry);
|
||||
|
||||
QSGFlatColorMaterial *material = new QSGFlatColorMaterial();
|
||||
geometryNode->setMaterial(material);
|
||||
geometryNode->setFlags(QSGNode::OwnsMaterial);
|
||||
|
||||
transformNode->appendChildNode(geometryNode);
|
||||
rootNode->appendChildNode(transformNode);
|
||||
}
|
||||
|
||||
transformNode->setMatrix(QMatrix4x4(transform));
|
||||
|
||||
QSGFlatColorMaterial *material = static_cast<QSGFlatColorMaterial*>(geometryNode->material());
|
||||
QColor color = Qt::white;
|
||||
color.setAlphaF(accumulatedOpacity);
|
||||
if (material->color() != color) material->setColor(color);
|
||||
|
||||
QRectF rect(0, 0, item->width(), item->height());
|
||||
createRoundedRectGeometry(geometryNode->geometry(), rect, radius);
|
||||
geometryNode->markDirty(QSGNode::DirtyGeometry);
|
||||
|
||||
|
||||
currentChildIndex++;
|
||||
}
|
||||
|
||||
// if we have more nodes than items this frame, remove the extras
|
||||
if (currentChildIndex < rootNode->childCount()) {
|
||||
for (int i = rootNode->childCount() - 1; i >= currentChildIndex; --i) {
|
||||
QSGNode *nodeToRemove = rootNode->childAtIndex(i);
|
||||
rootNode->removeChildNode(nodeToRemove);
|
||||
delete nodeToRemove;
|
||||
}
|
||||
}
|
||||
|
||||
return rootNode;
|
||||
}
|
||||
33
components/mobileshell/masklayer/masklayer.h
Normal file
33
components/mobileshell/masklayer/masklayer.h
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// SPDX-FileCopyrightText: 2025 Micah Stanley <stanleymicah@proton.me>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQuickItem>
|
||||
|
||||
class QSGNode;
|
||||
|
||||
class MaskLayer : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
explicit MaskLayer(QQuickItem *parent = nullptr);
|
||||
~MaskLayer() override;
|
||||
|
||||
Q_INVOKABLE void addItem(QQuickItem* item);
|
||||
Q_INVOKABLE void removeItem(QQuickItem* item);
|
||||
|
||||
protected:
|
||||
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData) override;
|
||||
|
||||
private slots:
|
||||
void scheduleUpdate();
|
||||
|
||||
private:
|
||||
void disconnectItemSignals(QQuickItem* item);
|
||||
|
||||
QVector<QPointer<QQuickItem>> m_sourceItems;
|
||||
QHash<QQuickItem*, QVector<QMetaObject::Connection>> m_connections;
|
||||
};
|
||||
43
components/mobileshell/masklayer/maskmanager.cpp
Normal file
43
components/mobileshell/masklayer/maskmanager.cpp
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// SPDX-FileCopyrightText: 2025 Micah Stanley <stanleymicah@proton.me>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "maskmanager.h"
|
||||
#include "masklayer.h"
|
||||
|
||||
MaskManager::MaskManager(QQuickItem *parent)
|
||||
: QQuickItem(parent),
|
||||
m_maskLayer(new MaskLayer(this))
|
||||
{
|
||||
}
|
||||
|
||||
MaskManager::~MaskManager() = default;
|
||||
|
||||
void MaskManager::componentComplete() {
|
||||
QQuickItem::componentComplete();
|
||||
// ensure the mask layers fill the dimensions
|
||||
m_maskLayer->setX(0);
|
||||
m_maskLayer->setY(0);
|
||||
m_maskLayer->setWidth(width());
|
||||
m_maskLayer->setHeight(height());
|
||||
m_maskLayer->setZ(z());
|
||||
|
||||
connect(this, &QQuickItem::widthChanged, this, [this]() {
|
||||
m_maskLayer->setWidth(width());
|
||||
});
|
||||
connect(this, &QQuickItem::heightChanged, this, [this]() {
|
||||
m_maskLayer->setHeight(height());
|
||||
});
|
||||
}
|
||||
|
||||
QQuickItem* MaskManager::maskLayer() const {
|
||||
return m_maskLayer;
|
||||
}
|
||||
|
||||
void MaskManager::assignToMask(QQuickItem* item) {
|
||||
if (!item) {
|
||||
qWarning() << "Cannot assign a null item to a mask.";
|
||||
return;
|
||||
}
|
||||
|
||||
m_maskLayer->addItem(item);
|
||||
}
|
||||
29
components/mobileshell/masklayer/maskmanager.h
Normal file
29
components/mobileshell/masklayer/maskmanager.h
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// SPDX-FileCopyrightText: 2025 Micah Stanley <stanleymicah@proton.me>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQuickItem>
|
||||
|
||||
class MaskLayer;
|
||||
|
||||
class MaskManager : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
Q_PROPERTY(QQuickItem* maskLayer READ maskLayer CONSTANT)
|
||||
|
||||
public:
|
||||
explicit MaskManager(QQuickItem *parent = nullptr);
|
||||
~MaskManager() override;
|
||||
|
||||
QQuickItem* maskLayer() const;
|
||||
|
||||
Q_INVOKABLE void assignToMask(QQuickItem* item);
|
||||
|
||||
protected:
|
||||
void componentComplete() override;
|
||||
|
||||
private:
|
||||
MaskLayer* m_maskLayer;
|
||||
};
|
||||
|
|
@ -12,18 +12,20 @@ import QtQuick.Window 2.2
|
|||
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.private.nanoshell 2.0 as NanoShell
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
|
||||
import 'actiondrawer/private'
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
/*
|
||||
* The intended visiblity of the action drawer.
|
||||
* The intended visibility of the action drawer.
|
||||
*
|
||||
* This is separate from "visible" in order to avoid having to set
|
||||
* item visiblity when its on its own window (wasteful since the window itself can be shown/hidden).
|
||||
* item visibility when it's on its own window (wasteful since the window itself can be shown/hidden).
|
||||
*/
|
||||
property bool intendedToBeVisible: false
|
||||
|
||||
|
|
@ -59,6 +61,19 @@ Item {
|
|||
*/
|
||||
property real offset: 0
|
||||
|
||||
/**
|
||||
* Same as offset value except this adds resistance when passing the open position of the current drawer state.
|
||||
*/
|
||||
readonly property real offsetResistance: {
|
||||
if (!openToPinnedMode) {
|
||||
return root.calculateResistance(offset, contentContainer.maximizedQuickSettingsOffset);
|
||||
} else if (!opened) {
|
||||
return root.calculateResistance(offset, contentContainer.minimizedQuickSettingsOffset);
|
||||
} else {
|
||||
return root.calculateResistance(offset, contentContainer.maximizedQuickSettingsOffset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the panel is being dragged.
|
||||
*/
|
||||
|
|
@ -80,15 +95,10 @@ Item {
|
|||
*/
|
||||
property int direction: MobileShell.Direction.None
|
||||
|
||||
/**
|
||||
* The notifications widget being shown. May be null.
|
||||
*/
|
||||
property var notificationsWidget: contentContainer.notificationsWidget
|
||||
|
||||
/**
|
||||
* The mode of the action drawer (portrait or landscape).
|
||||
*/
|
||||
property int mode: (height > width && width <= largePortraitThreshold) ? ActionDrawer.Portrait : ActionDrawer.Landscape
|
||||
property int mode: (height > width && width <= largePortraitThreshold) ? MobileShell.ActionDrawer.Portrait : MobileShell.ActionDrawer.Landscape
|
||||
|
||||
/**
|
||||
* At some point, even if the screen is technically portrait, if we have a ton of width it'd be best to just show the landscape mode.
|
||||
|
|
@ -124,10 +134,6 @@ Item {
|
|||
*/
|
||||
signal runPendingNotificationAction()
|
||||
|
||||
onOpenedChanged: {
|
||||
if (opened) swipeArea.focus = true;
|
||||
}
|
||||
|
||||
property real oldOffset
|
||||
onOffsetChanged: {
|
||||
if (offset < 0) {
|
||||
|
|
@ -135,13 +141,13 @@ Item {
|
|||
}
|
||||
|
||||
root.direction = (oldOffset === offset)
|
||||
? MobileShell.Direction.None
|
||||
: (offset > oldOffset ? MobileShell.Direction.Down : MobileShell.Direction.Up);
|
||||
? MobileShell.Direction.None
|
||||
: (offset > oldOffset ? MobileShell.Direction.Down : MobileShell.Direction.Up);
|
||||
|
||||
oldOffset = offset;
|
||||
|
||||
// close panel immediately after panel is not shown, and the flickable is not being dragged
|
||||
if (opened && root.offset <= 0 && !swipeArea.moving && !drawerAnimation.running) {
|
||||
if (opened && root.offset <= 0 && !contentContainer.swipeAreaMoving && !drawerAnimation.running) {
|
||||
root.state = "";
|
||||
offset = 0;
|
||||
focus = false;
|
||||
|
|
@ -150,6 +156,15 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
// calculates offset resistance for the action drawer overshoots it's open position
|
||||
function calculateResistance(value : double, threshold : int) : double {
|
||||
if (value > threshold) {
|
||||
return threshold + Math.pow(value - threshold + 1, Math.max(0.8 - (value - threshold) / ((root.height - threshold) * 15), 0.35));
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelAnimations() {
|
||||
root.state = "";
|
||||
}
|
||||
|
|
@ -249,7 +264,9 @@ Item {
|
|||
SequentialAnimation {
|
||||
PropertyAnimation {
|
||||
id: drawerAnimation
|
||||
properties: "offset"; easing.type: Easing.OutExpo; duration: root.state != "" ? Kirigami.Units.veryLongDuration : 0
|
||||
properties: "offset"
|
||||
easing.type: Easing.OutExpo
|
||||
duration: root.state != "" ? Kirigami.Units.veryLongDuration : 0
|
||||
}
|
||||
ScriptAction {
|
||||
script: {
|
||||
|
|
@ -267,45 +284,12 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
MobileShell.SwipeArea {
|
||||
id: swipeArea
|
||||
mode: MobileShell.SwipeArea.VerticalOnly
|
||||
// action drawer ui content
|
||||
ContentContainer {
|
||||
id: contentContainer
|
||||
anchors.fill: parent
|
||||
|
||||
function startSwipe() {
|
||||
root.cancelAnimations();
|
||||
root.dragging = true;
|
||||
|
||||
// Immediately open action drawer if we interact with it and it's already open
|
||||
// This allows us to have 2 quick flicks from minimized -> expanded
|
||||
if (root.visible && !root.opened) {
|
||||
root.opened = true;
|
||||
}
|
||||
}
|
||||
|
||||
function endSwipe() {
|
||||
root.dragging = false;
|
||||
root.updateState();
|
||||
}
|
||||
|
||||
function moveSwipe(totalDeltaX, totalDeltaY, deltaX, deltaY) {
|
||||
root.offset += deltaY;
|
||||
}
|
||||
|
||||
onSwipeStarted: startSwipe()
|
||||
onSwipeEnded: endSwipe()
|
||||
onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => moveSwipe(totalDeltaX, totalDeltaY, deltaX, deltaY)
|
||||
|
||||
onTouchpadScrollStarted: startSwipe()
|
||||
onTouchpadScrollEnded: endSwipe()
|
||||
onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => moveSwipe(totalDeltaX, totalDeltaY, deltaX, deltaY)
|
||||
|
||||
ContentContainer {
|
||||
id: contentContainer
|
||||
anchors.fill: parent
|
||||
|
||||
actionDrawer: root
|
||||
quickSettingsModel: root.quickSettingsModel
|
||||
}
|
||||
actionDrawer: root
|
||||
quickSettingsModel: root.quickSettingsModel
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ Window {
|
|||
color: "transparent"
|
||||
|
||||
// set input to transparent when closing to prevent window from taking unwanted touch inputs
|
||||
onStateChanged: ShellUtil.setInputTransparent(window, state == "close")
|
||||
onStateChanged: MobileShell.ShellUtil.setInputTransparent(window, state === "close")
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
|
|
|
|||
|
|
@ -1,144 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Devin Lin <devin@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.components 3.0 as PC3
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
|
||||
|
||||
/**
|
||||
* Root element that contains all the ActionDrawer's contents, and is anchored to the screen.
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var actionDrawer
|
||||
required property QS.QuickSettingsModel quickSettingsModel
|
||||
|
||||
readonly property real minimizedQuickSettingsOffset: contentContainerLoader.minimizedQuickSettingsOffset
|
||||
readonly property real maximizedQuickSettingsOffset: contentContainerLoader.maximizedQuickSettingsOffset
|
||||
|
||||
function applyMinMax(val) {
|
||||
return Math.max(0, Math.min(1, val));
|
||||
}
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
readonly property alias brightnessPressedValue: quickSettings.brightnessPressedValue
|
||||
|
||||
// Background color
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Kirigami.Theme.backgroundColor.r,
|
||||
Kirigami.Theme.backgroundColor.g,
|
||||
Kirigami.Theme.backgroundColor.b,
|
||||
(root.actionDrawer.mode == ActionDrawer.Portrait || notificationWidget.hasNotifications) ? 0.95 : 0.9)
|
||||
Behavior on color { ColorAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.OutQuad } }
|
||||
opacity: Math.max(0, Math.min(brightnessPressedValue, actionDrawer.offset / root.minimizedQuickSettingsOffset))
|
||||
}
|
||||
|
||||
// Layout that switches between landscape and portrait mode
|
||||
Loader {
|
||||
id: contentContainerLoader
|
||||
anchors.fill: parent
|
||||
|
||||
readonly property real minimizedQuickSettingsOffset: item ? item.minimizedQuickSettingsOffset : 0
|
||||
readonly property real maximizedQuickSettingsOffset: item ? item.maximizedQuickSettingsOffset : 0
|
||||
|
||||
readonly property real offsetDist: root.actionDrawer.offset - minimizedQuickSettingsOffset
|
||||
readonly property real totalOffsetDist: maximizedQuickSettingsOffset - minimizedQuickSettingsOffset
|
||||
readonly property real minimizedToFullProgress: root.actionDrawer.openToPinnedMode ? (root.actionDrawer.opened ? applyMinMax(offsetDist / totalOffsetDist) : 0) : 1
|
||||
|
||||
asynchronous: true
|
||||
sourceComponent: root.actionDrawer.mode == ActionDrawer.Portrait ? portraitContentContainer : landscapeContentContainer
|
||||
}
|
||||
|
||||
Component {
|
||||
id: portraitContentContainer
|
||||
PortraitContentContainer {
|
||||
actionDrawer: root.actionDrawer
|
||||
width: root.width
|
||||
height: root.height
|
||||
|
||||
quickSettings: root.quickSettings
|
||||
statusBar: root.statusBar
|
||||
mediaControlsWidget: root.mediaControlsWidget
|
||||
notificationsWidget: root.notificationsWidget
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: landscapeContentContainer
|
||||
LandscapeContentContainer {
|
||||
actionDrawer: root.actionDrawer
|
||||
width: root.width
|
||||
height: root.height
|
||||
|
||||
quickSettings: root.quickSettings
|
||||
statusBar: root.statusBar
|
||||
mediaControlsWidget: root.mediaControlsWidget
|
||||
notificationsWidget: root.notificationsWidget
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Components shared between the two layouts.
|
||||
// This allows us to avoid having to reload the components every time the screen size changes.
|
||||
|
||||
property MobileShell.QuickSettings quickSettings: MobileShell.QuickSettings {
|
||||
id: quickSettings
|
||||
actionDrawer: root.actionDrawer
|
||||
quickSettingsModel: root.quickSettingsModel
|
||||
fullViewProgress: (root.actionDrawer.mode == ActionDrawer.Portrait) ? contentContainerLoader.minimizedToFullProgress : 1.0
|
||||
}
|
||||
|
||||
property MobileShell.StatusBar statusBar: MobileShell.StatusBar {
|
||||
id: statusBar
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
backgroundColor: "transparent"
|
||||
showSecondRow: root.actionDrawer.mode == ActionDrawer.Portrait
|
||||
showDropShadow: false
|
||||
showTime: root.actionDrawer.mode == ActionDrawer.Portrait
|
||||
|
||||
// security reasons, system tray also doesn't work on lockscreen
|
||||
disableSystemTray: root.actionDrawer.restrictedPermissions
|
||||
|
||||
opacity: brightnessPressedValue
|
||||
}
|
||||
|
||||
property MobileShell.MediaControlsWidget mediaControlsWidget: MobileShell.MediaControlsWidget {
|
||||
id: mediaWidget
|
||||
inActionDrawer: true
|
||||
|
||||
opacity: brightnessPressedValue
|
||||
}
|
||||
|
||||
property MobileShell.NotificationsWidget notificationsWidget: MobileShell.NotificationsWidget {
|
||||
id: notificationWidget
|
||||
historyModel: root.actionDrawer.notificationModel
|
||||
historyModelType: root.actionDrawer.notificationModelType
|
||||
notificationSettings: root.actionDrawer.notificationSettings
|
||||
actionsRequireUnlock: root.actionDrawer.restrictedPermissions
|
||||
onUnlockRequested: root.actionDrawer.permissionsRequested()
|
||||
|
||||
opacity: brightnessPressedValue
|
||||
|
||||
Connections {
|
||||
target: root.actionDrawer
|
||||
|
||||
function onRunPendingNotificationAction() {
|
||||
notificationWidget.runPendingAction();
|
||||
}
|
||||
}
|
||||
|
||||
onBackgroundClicked: root.actionDrawer.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Devin Lin <devin@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
|
||||
import org.kde.kirigami 2.12 as Kirigami
|
||||
|
||||
import org.kde.plasma.core as PlasmaCore
|
||||
import org.kde.plasma.plasma5support 2.0 as P5Support
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
|
||||
|
||||
/**
|
||||
* Root element that contains all the ActionDrawer's contents, and is anchored to the screen.
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var actionDrawer
|
||||
|
||||
property alias quickSettings: quickSettingsPanel.quickSettings
|
||||
property alias statusBar: quickSettingsPanel.statusBar
|
||||
property alias mediaControlsWidget: mediaControlsWidgetProxy.contentItem
|
||||
property alias notificationsWidget: notificationWidgetProxy.contentItem
|
||||
|
||||
readonly property real minimizedQuickSettingsOffset: height
|
||||
readonly property real maximizedQuickSettingsOffset: height
|
||||
readonly property bool isOnLargeScreen: width > quickSettingsPanel.width * 2.5
|
||||
readonly property real minWidthHeight: Math.min(root.width, root.height)
|
||||
readonly property real opacityValue: Math.max(0, Math.min(1, actionDrawer.offset / root.minimizedQuickSettingsOffset))
|
||||
readonly property double brightnessPressedValue: quickSettings.brightnessPressedValue
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
P5Support.DataSource {
|
||||
id: timeSource
|
||||
engine: "time"
|
||||
connectedSources: ["Local"]
|
||||
interval: 60 * 1000
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
// dismiss drawer when background is clicked
|
||||
onClicked: root.actionDrawer.close();
|
||||
|
||||
// left side
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
|
||||
opacity: opacityValue
|
||||
spacing: 0
|
||||
|
||||
anchors {
|
||||
top: mediaControlsWidgetProxy.bottom
|
||||
topMargin: 0
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 0
|
||||
right: quickSettingsPanel.left
|
||||
left: parent.left
|
||||
}
|
||||
anchors.margins: minWidthHeight * 0.06
|
||||
|
||||
MobileShell.BaseItem {
|
||||
id: notificationWidgetProxy
|
||||
|
||||
// don't allow notifications widget to get too wide
|
||||
Layout.maximumWidth: Kirigami.Units.gridUnit * 25
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: minWidthHeight * 0.02
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaComponents.Label {
|
||||
id: clock
|
||||
text: Qt.formatTime(timeSource.data.Local.DateTime, MobileShell.ShellUtil.isSystem24HourFormat ? "h:mm" : "h:mm ap")
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
opacity: Math.min(brightnessPressedValue, columnLayout.opacity)
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
topMargin: columnLayout.anchors.margins / 2
|
||||
leftMargin: columnLayout.anchors.margins
|
||||
}
|
||||
|
||||
font.pixelSize: Math.min(40, minWidthHeight * 0.1)
|
||||
font.weight: Font.ExtraLight
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
PlasmaComponents.Label {
|
||||
id: date
|
||||
text: Qt.formatDate(timeSource.data.Local.DateTime, "ddd MMMM d")
|
||||
verticalAlignment: Qt.AlignTop
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
opacity: Math.min(brightnessPressedValue, columnLayout.opacity)
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: clock.bottom
|
||||
bottom: isOnLargeScreen ? columnLayout.top : mediaControlsWidgetProxy.top
|
||||
topMargin: Kirigami.Units.smallSpacing
|
||||
leftMargin: columnLayout.anchors.margins
|
||||
}
|
||||
|
||||
font.pixelSize: Math.min(20, minWidthHeight * 0.05)
|
||||
font.weight: Font.Light
|
||||
}
|
||||
|
||||
MobileShell.BaseItem {
|
||||
id: mediaControlsWidgetProxy
|
||||
property real fullHeight: visible ? height + Kirigami.Units.smallSpacing * 6 : 0
|
||||
|
||||
y: isOnLargeScreen ? date.y - height + date.implicitHeight : date.y + date.implicitHeight + columnLayout.anchors.margins / 2
|
||||
opacity: columnLayout.opacity
|
||||
|
||||
anchors {
|
||||
right: quickSettingsPanel.left
|
||||
left: isOnLargeScreen ? date.right : parent.left
|
||||
leftMargin: columnLayout.anchors.margins
|
||||
rightMargin: columnLayout.anchors.margins - quickSettingsPanel.leftPadding
|
||||
}
|
||||
}
|
||||
|
||||
// right sidebar
|
||||
MobileShell.QuickSettingsPanel {
|
||||
id: quickSettingsPanel
|
||||
height: quickSettingsPanel.contentImplicitHeight + quickSettingsPanel.topPadding + quickSettingsPanel.bottomPadding
|
||||
width: intendedWidth
|
||||
|
||||
readonly property real intendedWidth: 360
|
||||
|
||||
property real offsetRatio: quickSettingsPanel.height / root.height
|
||||
anchors.topMargin: Math.min(root.actionDrawer.offset * offsetRatio - quickSettingsPanel.height, 0)
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
|
||||
actionDrawer: root.actionDrawer
|
||||
fullScreenHeight: root.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import org.kde.kirigami 2.20 as Kirigami
|
|||
import org.kde.plasma.core as PlasmaCore
|
||||
import org.kde.plasma.components 3.0 as PC3
|
||||
import org.kde.plasma.private.mobileshell.screenbrightnessplugin as ScreenBrightness
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
|
@ -22,8 +23,8 @@ Item {
|
|||
property double brightnessPressedValue: 1
|
||||
Behavior on brightnessPressedValue {
|
||||
NumberAnimation {
|
||||
duration: Kirigami.Units.longDuration
|
||||
easing.type: Easing.OutQuad
|
||||
duration: Kirigami.Units.longDuration * 2
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -31,15 +32,18 @@ Item {
|
|||
id: screenBrightness
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
MobileShell.PanelBackground {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: -Kirigami.Units.smallSpacing
|
||||
anchors.rightMargin: -Kirigami.Units.smallSpacing
|
||||
anchors.topMargin: -Kirigami.Units.smallSpacing * 2
|
||||
anchors.bottomMargin: -Kirigami.Units.smallSpacing * 2
|
||||
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
panelType: MobileShell.PanelBackground.PanelType.Base
|
||||
flatten: root.brightnessPressedValue
|
||||
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
|
@ -77,7 +81,7 @@ Item {
|
|||
|
||||
Timer {
|
||||
id: brightnessPressedTimer
|
||||
interval: 200
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (brightnessSlider.pressed) {
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Devin Lin <devin@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.components 3.0 as PC3
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
|
||||
|
||||
/**
|
||||
* Root element that contains all the ActionDrawer's contents, and is anchored to the screen.
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var actionDrawer
|
||||
required property QS.QuickSettingsModel quickSettingsModel
|
||||
|
||||
readonly property real minimizedQuickSettingsOffset: contentContainerLoader.minimizedQuickSettingsOffset
|
||||
readonly property real maximizedQuickSettingsOffset: contentContainerLoader.maximizedQuickSettingsOffset
|
||||
|
||||
readonly property bool swipeAreaMoving: swipeAreaBase.moving || swipeAreaPortrait.moving
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
readonly property alias brightnessPressedValue: quickSettings.brightnessPressedValue
|
||||
|
||||
function applyMinMax(val) {
|
||||
return Math.max(0, Math.min(1, val));
|
||||
}
|
||||
|
||||
function startSwipe() {
|
||||
actionDrawer.cancelAnimations();
|
||||
actionDrawer.dragging = true;
|
||||
// Immediately open action drawer if we interact with it and it's already open
|
||||
// This allows us to have 2 quick flicks from minimized -> expanded
|
||||
if (actionDrawer.visible && !actionDrawer.opened) {
|
||||
actionDrawer.opened = true;
|
||||
}
|
||||
}
|
||||
|
||||
function endSwipe() {
|
||||
actionDrawer.dragging = false;
|
||||
actionDrawer.updateState();
|
||||
}
|
||||
|
||||
function moveSwipe(totalDeltaX, totalDeltaY, deltaX, deltaY) {
|
||||
actionDrawer.offset += deltaY;
|
||||
}
|
||||
|
||||
// Background color
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Kirigami.Theme.backgroundColor.r,
|
||||
Kirigami.Theme.backgroundColor.g,
|
||||
Kirigami.Theme.backgroundColor.b,
|
||||
0.9)
|
||||
Behavior on color { ColorAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.OutQuad } }
|
||||
opacity: Math.max(0, Math.min(brightnessPressedValue, actionDrawer.offset / root.minimizedQuickSettingsOffset))
|
||||
}
|
||||
|
||||
// The base swipe area.
|
||||
// Used to cover the full surface of the drawer to allow dismissing or expanding it.
|
||||
MobileShell.SwipeArea {
|
||||
id: swipeAreaBase
|
||||
mode: MobileShell.SwipeArea.VerticalOnly
|
||||
anchors.fill: parent
|
||||
|
||||
onSwipeStarted: root.startSwipe()
|
||||
onSwipeEnded: root.endSwipe()
|
||||
onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => root.moveSwipe(totalDeltaX, totalDeltaY, deltaX, deltaY)
|
||||
|
||||
onTouchpadScrollStarted: root.startSwipe()
|
||||
onTouchpadScrollEnded: root.endSwipe()
|
||||
onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => root.moveSwipe(totalDeltaX, totalDeltaY, deltaX, deltaY)
|
||||
|
||||
// Proxy in the layout that switches between landscape and portrait mode.
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
visible: root.actionDrawer.mode != MobileShell.ActionDrawer.Portrait
|
||||
LayoutItemProxy { target: contentContainerLoader }
|
||||
}
|
||||
|
||||
// Mouse area for dismissing action drawer in portrait mode when background is clicked.
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
visible: root.actionDrawer.mode == MobileShell.ActionDrawer.Portrait
|
||||
|
||||
// dismiss drawer when background is clicked
|
||||
onClicked: root.actionDrawer.close();
|
||||
}
|
||||
|
||||
// The clear all notification history button.
|
||||
Item {
|
||||
id: toolButtons
|
||||
height: visible ? spacer.height + toolLayout.height + toolLayout.anchors.topMargin + toolLayout.anchors.bottomMargin : 0
|
||||
|
||||
visible: actionDrawer.intendedToBeVisible
|
||||
opacity: Math.max(0, Math.min(root.brightnessPressedValue, actionDrawer.offsetResistance / root.minimizedQuickSettingsOffset))
|
||||
|
||||
anchors {
|
||||
topMargin: notificationDrawer.height + 1
|
||||
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : 10
|
||||
rightMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : notificationDrawer.notificationWidget.anchors.rightMargin + Kirigami.Units.gridUnit - notificationDrawer.anchors.leftMargin + 370
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: spacer
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
visible: notificationDrawer.listOverflowing
|
||||
height: 1
|
||||
opacity: 0.25
|
||||
color: Kirigami.Theme.textColor
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: toolLayout
|
||||
|
||||
anchors {
|
||||
top: spacer.bottom
|
||||
right: parent.right
|
||||
left: parent.left
|
||||
leftMargin: Kirigami.Units.largeSpacing
|
||||
rightMargin: Kirigami.Units.largeSpacing
|
||||
topMargin: Kirigami.Units.largeSpacing
|
||||
bottomMargin: Kirigami.Units.largeSpacing
|
||||
}
|
||||
|
||||
PlasmaComponents.ToolButton {
|
||||
id: clearButton
|
||||
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
visible: notificationDrawer.hasNotifications
|
||||
|
||||
font.bold: true
|
||||
font.pointSize: Kirigami.Theme.smallFont.pointSize
|
||||
|
||||
icon.name: "edit-clear-history"
|
||||
text: i18n("Clear All Notifications")
|
||||
onClicked: notificationDrawer.notificationWidget.clearHistory()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// notification drawer ui
|
||||
// separated from the main drawer ui swipe area to prevent scrolling conflicts
|
||||
NotificationDrawer {
|
||||
id: notificationDrawer
|
||||
|
||||
swipeArea: swipeAreaPortrait
|
||||
actionDrawer: root.actionDrawer
|
||||
mediaControlsWidget: root.mediaControlsWidget
|
||||
contentContainer: root
|
||||
opacity: Math.max(0, Math.min(root.brightnessPressedValue, actionDrawer.offsetResistance / root.minimizedQuickSettingsOffset))
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
rightMargin: root.actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : 360
|
||||
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : notificationDrawer.minWidthHeight * 0.06
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary swipe area for uses in portrait.
|
||||
// Covers the surface area of the quick settings panel to allow dismissing or expanding the drawer while also having it over top of the notification list.
|
||||
MobileShell.SwipeArea {
|
||||
id: swipeAreaPortrait
|
||||
mode: MobileShell.SwipeArea.VerticalOnly
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
height: root.actionDrawer.mode === MobileShell.ActionDrawer.Portrait ? actionDrawer.offsetResistance : root.height
|
||||
interactive: root.actionDrawer.mode === MobileShell.ActionDrawer.Portrait
|
||||
|
||||
onSwipeStarted: root.startSwipe()
|
||||
onSwipeEnded: root.endSwipe()
|
||||
onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => root.moveSwipe(totalDeltaX, totalDeltaY, deltaX, deltaY)
|
||||
|
||||
onTouchpadScrollStarted: root.startSwipe()
|
||||
onTouchpadScrollEnded: root.endSwipe()
|
||||
onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => root.moveSwipe(totalDeltaX, totalDeltaY, deltaX, deltaY)
|
||||
|
||||
// Proxy in the layout that switches between landscape and portrait mode.
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
visible: root.actionDrawer.mode == MobileShell.ActionDrawer.Portrait
|
||||
LayoutItemProxy { target: contentContainerLoader }
|
||||
}
|
||||
}
|
||||
|
||||
// Layout that switches between landscape and portrait mode
|
||||
Loader {
|
||||
id: contentContainerLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
readonly property real minimizedQuickSettingsOffset: item ? item.minimizedQuickSettingsOffset : 0
|
||||
readonly property real maximizedQuickSettingsOffset: item ? item.maximizedQuickSettingsOffset : 0
|
||||
|
||||
readonly property real offsetDist: root.actionDrawer.offset - minimizedQuickSettingsOffset
|
||||
readonly property real totalOffsetDist: maximizedQuickSettingsOffset - minimizedQuickSettingsOffset
|
||||
readonly property real minimizedToFullProgress: root.actionDrawer.openToPinnedMode ? (root.actionDrawer.opened ? applyMinMax(offsetDist / totalOffsetDist) : 0) : 1
|
||||
|
||||
asynchronous: true
|
||||
sourceComponent: root.actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? portraitContentContainer : landscapeContentContainer
|
||||
}
|
||||
|
||||
// The portrait content container.
|
||||
Component {
|
||||
id: portraitContentContainer
|
||||
PortraitContentContainer {
|
||||
actionDrawer: root.actionDrawer
|
||||
width: root.width
|
||||
height: root.height
|
||||
|
||||
quickSettings: root.quickSettings
|
||||
statusBar: root.statusBar
|
||||
mediaControlsWidget: root.mediaControlsWidget
|
||||
}
|
||||
}
|
||||
|
||||
// The landscape content container.
|
||||
Component {
|
||||
id: landscapeContentContainer
|
||||
LandscapeContentContainer {
|
||||
actionDrawer: root.actionDrawer
|
||||
width: root.width
|
||||
height: root.height
|
||||
|
||||
quickSettings: root.quickSettings
|
||||
statusBar: root.statusBar
|
||||
}
|
||||
}
|
||||
|
||||
// Components shared between the two layouts.
|
||||
// This allows us to avoid having to reload the components every time the screen size changes.
|
||||
|
||||
property QuickSettings quickSettings: QuickSettings {
|
||||
id: quickSettings
|
||||
actionDrawer: root.actionDrawer
|
||||
quickSettingsModel: root.quickSettingsModel
|
||||
fullViewProgress: (root.actionDrawer.mode == MobileShell.ActionDrawer.Portrait) ? contentContainerLoader.minimizedToFullProgress : 1.0
|
||||
}
|
||||
|
||||
property MobileShell.StatusBar statusBar: MobileShell.StatusBar {
|
||||
id: statusBar
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
backgroundColor: "transparent"
|
||||
showSecondRow: root.actionDrawer.mode == MobileShell.ActionDrawer.Portrait
|
||||
showDropShadow: false
|
||||
showTime: root.actionDrawer.mode == MobileShell.ActionDrawer.Portrait
|
||||
|
||||
opacity: brightnessPressedValue
|
||||
}
|
||||
|
||||
property MobileShell.MediaControlsWidget mediaControlsWidget: MobileShell.MediaControlsWidget {
|
||||
id: mediaWidget
|
||||
opacity: brightnessPressedValue
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Devin Lin <devin@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
|
||||
import org.kde.kirigami 2.12 as Kirigami
|
||||
|
||||
import org.kde.plasma.core as PlasmaCore
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
|
||||
|
||||
/**
|
||||
* Root element that contains all the ActionDrawer's contents, and is anchored to the screen.
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var actionDrawer
|
||||
|
||||
property alias quickSettings: quickSettingsPanel.quickSettings
|
||||
property alias statusBar: quickSettingsPanel.statusBar
|
||||
|
||||
readonly property real minimizedQuickSettingsOffset: height
|
||||
readonly property real maximizedQuickSettingsOffset: height
|
||||
readonly property bool isOnLargeScreen: width > quickSettingsPanel.width * 2.5
|
||||
readonly property real minWidthHeight: Math.min(root.width, root.height)
|
||||
readonly property real opacityValue: Math.max(0, Math.min(1, actionDrawer.offsetResistance / root.minimizedQuickSettingsOffset))
|
||||
readonly property double brightnessPressedValue: quickSettings.brightnessPressedValue
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
// dismiss drawer when background is clicked
|
||||
onClicked: root.actionDrawer.close();
|
||||
|
||||
// right sidebar
|
||||
MobileShell.QuickSettingsPanel {
|
||||
id: quickSettingsPanel
|
||||
height: quickSettingsPanel.contentImplicitHeight + quickSettingsPanel.topPadding + quickSettingsPanel.bottomPadding
|
||||
width: intendedWidth
|
||||
|
||||
readonly property real columnWidth: 6 * Kirigami.Units.gridUnit
|
||||
readonly property real intendedWidth: (columnWidth * ShellSettings.Settings.quickSettingsColumns) + Kirigami.Units.gridUnit
|
||||
|
||||
property real offsetRatio: quickSettingsPanel.height / root.height
|
||||
anchors.topMargin: Math.min(root.actionDrawer.offsetResistance * offsetRatio - quickSettingsPanel.height, 0)
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
|
||||
actionDrawer: root.actionDrawer
|
||||
fullScreenHeight: root.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Micah Stanley <stanleymicah@proton.me>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.1
|
||||
|
||||
import org.kde.plasma.clock
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var actionDrawer
|
||||
required property var contentContainer
|
||||
required property var swipeArea
|
||||
|
||||
property alias mediaControlsWidget: notificationWidget.header
|
||||
property alias notificationWidget: notificationWidget
|
||||
property real contentY: notificationWidget.listView.contentY
|
||||
|
||||
property real topPadding: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? Kirigami.Units.largeSpacing : date.y + date.height + Kirigami.Units.smallSpacing * 6
|
||||
property real topMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? actionDrawer.offsetResistance + 1 : 0
|
||||
|
||||
readonly property real minWidthHeight: Math.min(actionDrawer.width, actionDrawer.height)
|
||||
readonly property bool hasNotifications: notificationWidget.hasNotifications
|
||||
readonly property bool listOverflowing: notificationWidget.listView.listOverflowing
|
||||
|
||||
height: Math.min(actionDrawer.height - toolButtons.height, notificationWidget.listView.contentHeight + 10 + topMargin)
|
||||
|
||||
// time source for the time and date whenin landscape mode
|
||||
Clock {
|
||||
id: clockSource
|
||||
}
|
||||
|
||||
MobileShell.VelocityCalculator {
|
||||
id: velocityCalculator
|
||||
}
|
||||
|
||||
// notification list widget
|
||||
// margin adjusted to fit and position into the action drawer
|
||||
MobileShell.NotificationsWidget {
|
||||
id: notificationWidget
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: root.topMargin
|
||||
anchors.rightMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : Math.max(root.width - Kirigami.Units.gridUnit * 25, 0)
|
||||
anchors.leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : -Kirigami.Units.gridUnit
|
||||
|
||||
historyModel: actionDrawer.notificationModel
|
||||
historyModelType: actionDrawer.notificationModelType
|
||||
notificationSettings: actionDrawer.notificationSettings
|
||||
actionsRequireUnlock: actionDrawer.restrictedPermissions
|
||||
onUnlockRequested: actionDrawer.permissionsRequested()
|
||||
topPadding: root.topPadding
|
||||
showHeader: actionDrawer.mode != MobileShell.ActionDrawer.Portrait
|
||||
listView.interactive: !actionDrawer.dragging && root.listOverflowing
|
||||
|
||||
cardColorScheme: Kirigami.Theme.View
|
||||
|
||||
Connections {
|
||||
target: actionDrawer
|
||||
|
||||
function onRunPendingNotificationAction() {
|
||||
notificationWidget.runPendingAction();
|
||||
}
|
||||
}
|
||||
|
||||
// the first swipe when at the top of the notification list is handled using a MouseArea, not the flickable
|
||||
// this is so one can swipe down from the top of the notification drawer to expand the action drawer
|
||||
DragHandler {
|
||||
id: dragHandler
|
||||
// disable the draghandler when we are not at the top of the notification list as it can interfere with the notification scrolling
|
||||
yAxis.enabled: notificationWidget.listView.atYBeginning || active
|
||||
xAxis.enabled: false
|
||||
|
||||
property bool startActive: false
|
||||
|
||||
property real startOffset: 0
|
||||
property real startMouseY: 0
|
||||
property real lastMouseY: 0
|
||||
|
||||
property bool startedAtYBeginning: false
|
||||
property bool startedAtYEnd: false
|
||||
property bool drawerDrag: true
|
||||
|
||||
property string currentState
|
||||
|
||||
onTranslationChanged: {
|
||||
if (startActive) {
|
||||
dragHandler.startedAtYBeginning = notificationWidget.listView.atYBeginning;
|
||||
dragHandler.startedAtYEnd = notificationWidget.listView.atYEnd;
|
||||
startActive = false;
|
||||
|
||||
if (notificationWidget.listView.atYBeginning) {
|
||||
currentState = actionDrawer.state;
|
||||
actionDrawer.cancelAnimations();
|
||||
actionDrawer.dragging = true;
|
||||
actionDrawer.opened = true;
|
||||
dragHandler.startOffset = actionDrawer.offset;
|
||||
dragHandler.startMouseY = translation.y;
|
||||
dragHandler.lastMouseY = dragHandler.startMouseY;
|
||||
dragHandler.drawerDrag = true;
|
||||
|
||||
velocityCalculator.startMeasure();
|
||||
velocityCalculator.changePosition(notificationWidget.listView.contentY);
|
||||
}
|
||||
}
|
||||
|
||||
if (!actionDrawer.dragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(dragHandler.startedAtYBeginning && dragHandler.startedAtYEnd) && ((dragHandler.startedAtYBeginning && (dragHandler.startMouseY - translation.y) > 0) || (dragHandler.startedAtYEnd && (translation.y - dragHandler.startMouseY) > 0))) {
|
||||
actionDrawer.state = currentState;
|
||||
dragHandler.drawerDrag = false;
|
||||
}
|
||||
|
||||
if (dragHandler.drawerDrag) {
|
||||
actionDrawer.offset = dragHandler.startOffset - (dragHandler.startMouseY - translation.y);
|
||||
} else {
|
||||
let contentY = notificationWidget.listView.contentY - (translation.y - dragHandler.lastMouseY);
|
||||
|
||||
notificationWidget.listView.contentY = contentY;
|
||||
velocityCalculator.changePosition(notificationWidget.listView.contentY);
|
||||
dragHandler.lastMouseY = translation.y;
|
||||
}
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
startActive = active;
|
||||
|
||||
if (!active) { // release event
|
||||
if (actionDrawer.dragging) {
|
||||
if (dragHandler.drawerDrag) {
|
||||
actionDrawer.updateState();
|
||||
} else {
|
||||
notificationWidget.listView.flick(0, -velocityCalculator.velocity);
|
||||
}
|
||||
}
|
||||
actionDrawer.dragging = false;
|
||||
dragHandler.drawerDrag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// time and date displayed in landscape mode
|
||||
Item {
|
||||
id: landscapeModeHeader
|
||||
anchors.fill: parent
|
||||
visible: actionDrawer.mode != MobileShell.ActionDrawer.Portrait
|
||||
|
||||
transform: [
|
||||
Translate {
|
||||
y: -notificationWidget.listView.contentY + notificationWidget.listView.originY
|
||||
}
|
||||
]
|
||||
|
||||
PlasmaComponents.Label {
|
||||
id: clock
|
||||
text: Qt.formatTime(clockSource.dateTime, MobileShell.ShellUtil.isSystem24HourFormat ? "h:mm" : "h:mm ap")
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
topMargin: minWidthHeight * 0.03
|
||||
}
|
||||
|
||||
font.pixelSize: Math.min(40, minWidthHeight * 0.1)
|
||||
font.weight: Font.ExtraLight
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
PlasmaComponents.Label {
|
||||
id: date
|
||||
text: Qt.formatDate(clockSource.dateTime, "ddd MMMM d")
|
||||
verticalAlignment: Qt.AlignTop
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: clock.bottom
|
||||
topMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
font.pixelSize: Math.min(20, minWidthHeight * 0.05)
|
||||
font.weight: Font.Light
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,6 @@ Item {
|
|||
property alias quickSettings: quickSettingsDrawer.quickSettings
|
||||
property alias statusBar: quickSettingsDrawer.statusBar
|
||||
property alias mediaControlsWidget: quickSettingsDrawer.mediaControlsWidget
|
||||
property alias notificationsWidget: notificationWidgetProxy.contentItem
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
|
@ -39,19 +38,20 @@ Item {
|
|||
|
||||
MobileShell.QuickSettingsDrawer {
|
||||
id: quickSettingsDrawer
|
||||
z: 1 // ensure it's above notifications
|
||||
|
||||
// physically move the drawer when between closed <-> pinned mode
|
||||
readonly property real offsetHeight: actionDrawer.openToPinnedMode ? minimizedQuickSettingsOffset : maximizedQuickSettingsOffset
|
||||
anchors.topMargin: Math.min(root.actionDrawer.offset - offsetHeight, 0)
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors {
|
||||
topMargin: Math.min(root.actionDrawer.offsetResistance - offsetHeight, 0)
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
|
||||
actionDrawer: root.actionDrawer
|
||||
|
||||
// opacity and move animation (disabled when openToPinnedMode is false)
|
||||
property real offsetDist: actionDrawer.offset - minimizedQuickSettingsOffset
|
||||
property real offsetDist: actionDrawer.offsetResistance - minimizedQuickSettingsOffset
|
||||
property real totalOffsetDist: maximizedQuickSettingsOffset - minimizedQuickSettingsOffset
|
||||
minimizedToFullProgress: actionDrawer.openToPinnedMode ? (actionDrawer.opened ? applyMinMax(offsetDist / totalOffsetDist) : 0) : 1
|
||||
|
||||
|
|
@ -65,33 +65,12 @@ Item {
|
|||
addedHeight: {
|
||||
if (!actionDrawer.openToPinnedMode) {
|
||||
// if pinned mode disabled, just go to full height
|
||||
let progress = (root.actionDrawer.offset - maximizedQuickSettingsOffset) / (quickSettingsDrawer.maxAddedHeight * 4);
|
||||
let effectProgress = Math.atan(Math.max(0, progress));
|
||||
return (quickSettingsDrawer.maxAddedHeight * effectProgress) + quickSettingsDrawer.maxAddedHeight;
|
||||
return Math.max(maximizedQuickSettingsOffset - minimizedQuickSettingsOffset, root.actionDrawer.offsetResistance - minimizedQuickSettingsOffset)
|
||||
} else if (!actionDrawer.opened) {
|
||||
// over-scroll effect for initial opening
|
||||
let progress = (root.actionDrawer.offset - minimizedQuickSettingsOffset) / quickSettingsDrawer.maxAddedHeight;
|
||||
let effectProgress = Math.atan(Math.max(0, progress));
|
||||
return quickSettingsDrawer.maxAddedHeight * 0.25 * effectProgress;
|
||||
return Math.max(0, root.actionDrawer.offsetResistance - minimizedQuickSettingsOffset)
|
||||
} else {
|
||||
// over-scroll effect for full drawer
|
||||
let progress = (root.actionDrawer.offset - maximizedQuickSettingsOffset) / (quickSettingsDrawer.maxAddedHeight * 4);
|
||||
let effectProgress = Math.atan(Math.max(0, progress));
|
||||
// as the drawer opens, add height to the rectangle, revealing content
|
||||
return (quickSettingsDrawer.maxAddedHeight * effectProgress) + Math.max(0, Math.min(quickSettingsDrawer.maxAddedHeight, root.actionDrawer.offset - minimizedQuickSettingsOffset));
|
||||
return Math.max(0, root.actionDrawer.offsetResistance - minimizedQuickSettingsOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MobileShell.BaseItem {
|
||||
id: notificationWidgetProxy
|
||||
|
||||
anchors {
|
||||
top: quickSettingsDrawer.bottom
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
opacity: applyMinMax(root.actionDrawer.offset / root.minimizedQuickSettingsOffset)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import QtQuick.Layouts 1.1
|
|||
import QtQuick.Window 2.2
|
||||
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
|
|
@ -20,13 +21,15 @@ import org.kde.kirigami 2.20 as Kirigami
|
|||
*/
|
||||
Item {
|
||||
id: root
|
||||
clip: true
|
||||
// to prevent clipping off the shadows form the BrightnessItem when the rest of the action panel view
|
||||
// is transparent, we stop clipping the base view when fullViewProgress is not less then 1
|
||||
clip: fullViewProgress < 1
|
||||
|
||||
required property var actionDrawer
|
||||
|
||||
required property QS.QuickSettingsModel quickSettingsModel
|
||||
|
||||
readonly property real columns: Math.round(Math.min(6, Math.max(3, width / intendedColumnWidth)))
|
||||
readonly property real columns: Math.round(Math.min(6, Math.max(ShellSettings.Settings.quickSettingsColumns, width / intendedColumnWidth)))
|
||||
readonly property real columnWidth: Math.floor(width / columns)
|
||||
readonly property int minimizedColumns: Math.round(Math.min(8, Math.max(5, width / intendedMinimizedColumnWidth)))
|
||||
readonly property real minimizedColumnWidth: Math.floor(width / minimizedColumns)
|
||||
|
|
@ -96,28 +99,12 @@ Item {
|
|||
sourceModel: root.quickSettingsModel
|
||||
pageSize: Math.min(root.pageSize, root.minimizedColumns) // HACK: just root.minimizedColumns appears to end up with an empty model?
|
||||
}
|
||||
delegate: MobileShell.BaseItem {
|
||||
delegate: Loader {
|
||||
required property var modelData
|
||||
|
||||
implicitHeight: root.minimizedRowHeight
|
||||
implicitWidth: root.minimizedColumnWidth
|
||||
horizontalPadding: (width - Kirigami.Units.gridUnit * 3) / 2
|
||||
verticalPadding: (height - Kirigami.Units.gridUnit * 3) / 2
|
||||
asynchronous: true
|
||||
|
||||
contentItem: QuickSettingsMinimizedDelegate {
|
||||
restrictedPermissions: actionDrawer.restrictedPermissions
|
||||
|
||||
text: modelData.text
|
||||
status: modelData.status
|
||||
icon: modelData.icon
|
||||
enabled: modelData.enabled
|
||||
settingsCommand: modelData.settingsCommand
|
||||
toggleFunction: modelData.toggle
|
||||
|
||||
onCloseRequested: {
|
||||
actionDrawer.close();
|
||||
}
|
||||
}
|
||||
sourceComponent: quickSettingComponentMinimized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -140,6 +127,9 @@ Item {
|
|||
|
||||
SwipeView {
|
||||
id: swipeView
|
||||
// we need to clip this view here to prevent the other quick settings pages from being visible
|
||||
// when fullViewProgress is not less then 1 and the base view is no longer being clipped
|
||||
clip: true
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: rowCount * rowHeight
|
||||
|
|
@ -163,7 +153,7 @@ Item {
|
|||
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: quickSettingComponent
|
||||
sourceComponent: quickSettingComponentFull
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -211,9 +201,36 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
// Quick setting component
|
||||
// Quick setting component minimized
|
||||
Component {
|
||||
id: quickSettingComponent
|
||||
id: quickSettingComponentMinimized
|
||||
|
||||
MobileShell.BaseItem {
|
||||
implicitHeight: root.minimizedRowHeight
|
||||
implicitWidth: root.minimizedColumnWidth
|
||||
horizontalPadding: (width - Kirigami.Units.gridUnit * 3) / 2
|
||||
verticalPadding: (height - Kirigami.Units.gridUnit * 3) / 2
|
||||
|
||||
contentItem: QuickSettingsMinimizedDelegate {
|
||||
restrictedPermissions: actionDrawer.restrictedPermissions
|
||||
|
||||
text: modelData.text
|
||||
status: modelData.status
|
||||
icon: modelData.icon
|
||||
enabled: modelData.enabled
|
||||
settingsCommand: modelData.settingsCommand
|
||||
toggleFunction: modelData.toggle
|
||||
|
||||
onCloseRequested: {
|
||||
actionDrawer.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Quick setting component full
|
||||
Component {
|
||||
id: quickSettingComponentFull
|
||||
|
||||
MobileShell.BaseItem {
|
||||
height: root.rowHeight
|
||||
|
|
@ -34,18 +34,29 @@ MobileShell.BaseItem {
|
|||
// set by children
|
||||
property var iconItem
|
||||
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Button
|
||||
|
||||
readonly property color enabledButtonBorderColor: Qt.darker(Kirigami.Theme.highlightColor, 1.25)
|
||||
readonly property color disabledButtonBorderColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.textColor, Kirigami.Theme.backgroundColor, 0.75)
|
||||
readonly property color disabledButtonBorderColor: separatorColorHelper(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.2)
|
||||
readonly property color enabledButtonColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.6)
|
||||
readonly property color enabledButtonPressedColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.4);
|
||||
readonly property color disabledButtonColor: Kirigami.Theme.backgroundColor
|
||||
readonly property color disabledButtonPressedColor: Qt.darker(disabledButtonColor, 1.1)
|
||||
|
||||
function separatorColorHelper(bg, fg, baseRatio) {
|
||||
if (Kirigami.ColorUtils.brightnessForColor(bg) === Kirigami.ColorUtils.Light) {
|
||||
return Kirigami.ColorUtils.linearInterpolation(bg, fg, baseRatio);
|
||||
} else {
|
||||
return Kirigami.ColorUtils.linearInterpolation(bg, fg, baseRatio / 2);
|
||||
}
|
||||
}
|
||||
|
||||
// scale animation on press
|
||||
property real zoomScale: 1
|
||||
Behavior on zoomScale {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
duration: Kirigami.Units.longDuration
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ MobileShell.BaseItem {
|
|||
/**
|
||||
* Height of just the QuickSettings component in minimized mode.
|
||||
*/
|
||||
readonly property real minimizedQuickSettingsHeight: quickSettings.minimizedRowHeight + Kirigami.Units.gridUnit
|
||||
readonly property real minimizedQuickSettingsHeight: quickSettings.minimizedRowHeight + Kirigami.Units.gridUnit - Kirigami.Units.largeSpacing
|
||||
|
||||
/**
|
||||
* Progress of showing the full quick settings view from pinned.
|
||||
|
|
@ -57,10 +57,50 @@ MobileShell.BaseItem {
|
|||
rightPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing * 4
|
||||
|
||||
background: KSvg.FrameSvgItem {
|
||||
enabledBorders: KSvg.FrameSvgItem.BottomBorder
|
||||
imagePath: "widgets/background"
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
|
||||
background: Item {
|
||||
opacity: brightnessPressedValue
|
||||
|
||||
MobileShell.PanelBackground {
|
||||
id: background
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: shadow.height
|
||||
panelType: MobileShell.PanelBackground.PanelType.Flat
|
||||
radius: 0
|
||||
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: separator
|
||||
anchors.bottom: background.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
antialiasing: true
|
||||
|
||||
// Only show separator on dark background
|
||||
visible: (Kirigami.ColorUtils.brightnessForColor(background.panelColor)) === Kirigami.ColorUtils.Dark ? 1 : 0
|
||||
height: 1
|
||||
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.textColor, Kirigami.Theme.backgroundColor, 0.9)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: shadow
|
||||
anchors.top: background.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: Kirigami.Units.largeSpacing
|
||||
opacity: 0.1
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Vertical
|
||||
GradientStop { position: 0.0; color: 'black' }
|
||||
GradientStop { position: 1.0; color: 'transparent' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
|
|
@ -41,6 +41,7 @@ QuickSettingsDelegate {
|
|||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
border.pixelAligned: false
|
||||
border.width: 1
|
||||
border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor
|
||||
color: {
|
||||
|
|
@ -40,6 +40,8 @@ QuickSettingsDelegate {
|
|||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
border.pixelAligned: false
|
||||
border.width: 1
|
||||
border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor
|
||||
color: {
|
||||
if (root.enabled) {
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
|
||||
import org.kde.kirigami 2.12 as Kirigami
|
||||
import org.kde.ksvg 1.0 as KSvg
|
||||
|
|
@ -43,10 +44,17 @@ MobileShell.BaseItem {
|
|||
rightPadding: Kirigami.Units.smallSpacing * 4
|
||||
bottomPadding: Kirigami.Units.smallSpacing * 4
|
||||
|
||||
background: KSvg.FrameSvgItem {
|
||||
enabledBorders: KSvg.FrameSvgItem.AllBorders
|
||||
imagePath: "widgets/background"
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
|
||||
background: MobileShell.PanelBackground {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.largeSpacing
|
||||
panelType: MobileShell.PanelBackground.PanelType.Base
|
||||
opacity: brightnessPressedValue
|
||||
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
|
|
@ -84,7 +92,8 @@ MobileShell.BaseItem {
|
|||
Layout.maximumWidth: column.width
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
// add extra space here to prevent the bottom handle from overlapping with the BrightnessItem
|
||||
Item { Layout.fillHeight: true; height: Kirigami.Units.largeSpacing }
|
||||
}
|
||||
|
||||
Handle {
|
||||
|
|
@ -5,6 +5,7 @@ import QtQuick
|
|||
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
||||
|
||||
// NOTE: This is a singleton in the mobileshell library, so we need to be careful to
|
||||
// make this load as fast as possible (since it may be loaded in other processes ex. lockscreen).
|
||||
|
|
@ -12,8 +13,39 @@ import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
|||
pragma Singleton
|
||||
|
||||
QtObject {
|
||||
readonly property real topPanelHeight: Math.round(Kirigami.Units.gridUnit * ShellSettings.Settings.statusBarScaleFactor / 2) * 2 + Kirigami.Units.smallSpacing
|
||||
readonly property real navigationPanelThickness: ShellSettings.Settings.navigationPanelEnabled ? Kirigami.Units.gridUnit * 2 : 0
|
||||
id: root
|
||||
readonly property var panelSettings: MobileShellState.PanelSettingsDBusClient {
|
||||
screenName: Screen.name
|
||||
}
|
||||
|
||||
readonly property real defaultTopPanelHeight: Math.round(Kirigami.Units.gridUnit * ShellSettings.Settings.statusBarScaleFactor / 2) * 2 + Kirigami.Units.smallSpacing
|
||||
|
||||
readonly property real topPanelHeight: {
|
||||
if (root.panelSettings.statusBarHeight <= 0) {
|
||||
return defaultTopPanelHeight;
|
||||
}
|
||||
return root.panelSettings.statusBarHeight;
|
||||
}
|
||||
|
||||
readonly property real defaultNavigationPanelThickness: Kirigami.Units.gridUnit * 2
|
||||
readonly property real defaultGesturePanelThickness: Kirigami.Units.gridUnit
|
||||
|
||||
readonly property real navigationPanelThickness: {
|
||||
if (!ShellSettings.Settings.navigationPanelEnabled) {
|
||||
return ShellSettings.Settings.gesturePanelEnabled ? defaultGesturePanelThickness : 0;
|
||||
}
|
||||
if (root.panelSettings.navigationPanelHeight <= 0) {
|
||||
return defaultNavigationPanelThickness;
|
||||
}
|
||||
return root.panelSettings.navigationPanelHeight;
|
||||
}
|
||||
|
||||
readonly property real screenEdgeTouchTarget: (ShellSettings.Settings.gesturePanelEnabled && !ShellSettings.Settings.navigationPanelEnabled) ? defaultGesturePanelThickness : 8
|
||||
onScreenEdgeTouchTargetChanged: {
|
||||
if (ShellSettings.KWinSettings.screenEdgeTouchTarget != screenEdgeTouchTarget) {
|
||||
ShellSettings.KWinSettings.screenEdgeTouchTarget = screenEdgeTouchTarget;
|
||||
}
|
||||
}
|
||||
|
||||
function navigationPanelOnSide(screenWidth: real, screenHeight: real): bool {
|
||||
// TODO: we have this disabled for now, we might consider just removing this feature entirely due to it causing several issues:
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
|
||||
/**
|
||||
* This component is an AbstractButton with some added functionality to simulate a MouseArea.
|
||||
*
|
||||
* The hovered property of AbstractButton is much more accurate than the containsMouse property of MouseArea,
|
||||
* and so this is useful for creating custom buttons.
|
||||
*/
|
||||
|
||||
QQC2.AbstractButton {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* The cursor shape when the mouse is over the button.
|
||||
*/
|
||||
property alias cursorShape: hoverHandler.cursorShape
|
||||
|
||||
/**
|
||||
* Alias to MouseArea used in the button.
|
||||
*/
|
||||
property alias mouseArea: mouseArea
|
||||
|
||||
/**
|
||||
* Whether a mouse is hovering over the button (not touch).
|
||||
*/
|
||||
readonly property bool mouseHovered: hoverHandler.hovered
|
||||
|
||||
/**
|
||||
* Signal that is emitted when the right click button is pressed.
|
||||
*/
|
||||
signal rightClickPressed()
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onPressed: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
root.rightClickPressed();
|
||||
} else {
|
||||
mouse.accepted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
acceptedDevices: PointerDevice.Mouse
|
||||
acceptedPointerTypes: PointerDevice.Generic
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +1,119 @@
|
|||
// SPDX-FileCopyrightText: 2022 Yari Polla <skilvingr@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2025 Micah Stanley <stanleymicah@proton.me>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
/**
|
||||
* This is a simple marquee (flowing) label based on PlasmaComponents Label.
|
||||
*/
|
||||
|
||||
PlasmaComponents.Label {
|
||||
OpacityMask {
|
||||
id: root
|
||||
height: row.height
|
||||
|
||||
// label values
|
||||
required property string inputText
|
||||
property font font
|
||||
property var textFormat: Text.RichText
|
||||
// properties for the marquee label scroll speed and wait duration
|
||||
readonly property real scrollSpeed: 0.025
|
||||
readonly property int waitDuration: 2000
|
||||
|
||||
readonly property string filteredText: inputText.replace(/\n/g, ' ') // remove new line characters
|
||||
readonly property bool charactersOverflowing: txtMeter.advanceWidth > root.width // true when text is overflowing
|
||||
|
||||
readonly property int interval: 200 // update position every 200 ms
|
||||
readonly property int longDuration: 300
|
||||
readonly property int waitDuration: 900
|
||||
// update animation values and text positions whenever the label overflows or changes
|
||||
onFilteredTextChanged: if (root.charactersOverflowing) { textAnimationLoop.restart() }
|
||||
onCharactersOverflowingChanged: if (charactersOverflowing) { row.scrollPosition = 0 }
|
||||
|
||||
readonly property int charactersOverflow: Math.ceil((txtMeter.advanceWidth - root.width) / (txtMeter.advanceWidth / filteredText.length))
|
||||
property int step: 0
|
||||
Item {
|
||||
id: rowContaner
|
||||
anchors.fill: parent
|
||||
height: row.height
|
||||
opacity: 0 // we display with the opacity gradient below
|
||||
|
||||
TextMetrics {
|
||||
id: txtMeter
|
||||
font: root.font
|
||||
text: filteredText
|
||||
}
|
||||
// use two identical labels for scrolling so we can give the illusion of infinite scrolling
|
||||
RowLayout {
|
||||
id: row
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
Timer {
|
||||
property bool paused: false
|
||||
property real scrollPosition: 0
|
||||
|
||||
interval: root.interval
|
||||
running: visible && charactersOverflow > 0
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (paused) {
|
||||
if (step != 0) {
|
||||
interval = root.longDuration;
|
||||
step = 0;
|
||||
} else {
|
||||
interval = root.interval;
|
||||
paused = false;
|
||||
transform: [
|
||||
Translate {
|
||||
x: row.scrollPosition
|
||||
}
|
||||
} else {
|
||||
step = (step + 1) % filteredText.length;
|
||||
if (step === charactersOverflow) {
|
||||
interval = root.waitDuration;
|
||||
paused = true;
|
||||
]
|
||||
|
||||
spacing: 32
|
||||
|
||||
PlasmaComponents.Label {
|
||||
id: label
|
||||
font: root.font
|
||||
textFormat: root.textFormat
|
||||
text: filteredText
|
||||
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
|
||||
TextMetrics {
|
||||
id: txtMeter
|
||||
font: root.font
|
||||
text: filteredText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
step = 0;
|
||||
PlasmaComponents.Label {
|
||||
// hide this label when the text is not overflowing so the user never sees both labels
|
||||
visible: textAnimationLoop.running
|
||||
font: root.font
|
||||
textFormat: root.textFormat
|
||||
text: filteredText
|
||||
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text: filteredText.substring(step, step + filteredText.length - charactersOverflow)
|
||||
// setting the gradient mask source
|
||||
source: rowContaner
|
||||
|
||||
// if the label is overflowing, this animation in a loop smoothly scrolling thought the text
|
||||
SequentialAnimation {
|
||||
id: textAnimationLoop
|
||||
running: root.charactersOverflowing && root.visible
|
||||
onRunningChanged: row.scrollPosition = 0
|
||||
loops: Animation.Infinite
|
||||
PauseAnimation { duration: root.waitDuration }
|
||||
NumberAnimation { target: row; property: "scrollPosition"; from: 0; to: -txtMeter.advanceWidth - row.spacing; duration: (txtMeter.advanceWidth + row.spacing) / root.scrollSpeed }
|
||||
}
|
||||
|
||||
// gradient mask to smoothly fade the ends of the label when it is scrolling
|
||||
maskSource: Rectangle {
|
||||
id: mask
|
||||
width: root.width
|
||||
height: root.height
|
||||
|
||||
property real gradientPct: (Kirigami.Units.gridUnit * 0.35) / root.width
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
|
||||
GradientStop { position: 0; color: row.scrollPosition == 0 || row.scrollPosition < -txtMeter.advanceWidth ? 'white' : 'transparent' } // remove the beginning of the gradient when at the start of the label so the front text is fully visible
|
||||
GradientStop { position: 0 + mask.gradientPct; color: 'white' }
|
||||
GradientStop { position: 1.0 - mask.gradientPct; color: 'white' }
|
||||
GradientStop { position: 1.0; color: 'transparent' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
119
components/mobileshell/qml/components/PanelBackground.qml
Normal file
119
components/mobileshell/qml/components/PanelBackground.qml
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// SPDX-FileCopyrightText: 2025 Micah Stanley <stanleymicah@proton.me>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import org.kde.kirigami 2.12 as Kirigami
|
||||
import QtQuick.Effects
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property int panelType: PanelBackground.PanelType.Base
|
||||
property real flatten: 0 // flattens out the border and shadow effects
|
||||
property bool pressed: false // darkens the panel when true
|
||||
property bool animate: false // animate panel color changes
|
||||
|
||||
enum PanelType {
|
||||
Flat, // the base rectangle with no effects
|
||||
Base, // the base panel with some effects
|
||||
Stacked, // for being stacked on top of the base panel
|
||||
Drawer, // for uses in drawers, scroll containers, typically when a translucent component is behind them
|
||||
Wallpaper, // for uses when the panel is right on top of the user's wallpaper
|
||||
Popup // for uses as a popup, like for the volume and notifition popups.
|
||||
}
|
||||
|
||||
// whether to use a shadow effect
|
||||
readonly property bool shadow: panelType === PanelBackground.PanelType.Base ||
|
||||
panelType === PanelBackground.PanelType.Stacked ||
|
||||
panelType === PanelBackground.PanelType.Drawer ||
|
||||
panelType === PanelBackground.PanelType.Popup ||
|
||||
panelType === PanelBackground.PanelType.Wallpaper
|
||||
// whether to use the complex shadow effect - note that this uses more performance
|
||||
readonly property bool complexShadow: shadow &&
|
||||
(panelType === PanelBackground.PanelType.Base ||
|
||||
panelType === PanelBackground.PanelType.Drawer ||
|
||||
panelType === PanelBackground.PanelType.Popup ||
|
||||
panelType === PanelBackground.PanelType.Wallpaper)
|
||||
// whether the panel should have a border when using a dark theme
|
||||
readonly property bool border: panelType === PanelBackground.PanelType.Base ||
|
||||
panelType === PanelBackground.PanelType.Stacked ||
|
||||
panelType === PanelBackground.PanelType.Popup
|
||||
// whether to force the panel to have a border even when using a light theme
|
||||
readonly property bool forceBorder: border &&
|
||||
(panelType === PanelBackground.PanelType.Stacked)
|
||||
// whether the panel is translucent border - note that border cannot be used when translucent
|
||||
readonly property bool translucent: panelType === PanelBackground.PanelType.Popup ||
|
||||
panelType === PanelBackground.PanelType.Wallpaper
|
||||
// adjust color depending on panel type
|
||||
property color panelColor: {
|
||||
let tintPercent
|
||||
if (panelType === PanelBackground.PanelType.Popup) {
|
||||
tintPercent = 0.035
|
||||
} else if (panelType === PanelBackground.PanelType.Base || panelType === PanelBackground.PanelType.Stacked || panelType === PanelBackground.PanelType.Flat) {
|
||||
tintPercent = 0
|
||||
} else {
|
||||
tintPercent = 0.06
|
||||
}
|
||||
|
||||
return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, "white", tintPercent)
|
||||
}
|
||||
// in some circumstances, panels can change there type
|
||||
// for example, popup notifition when opening the popup notifition drawer
|
||||
// in these incidents, we animate the color to prevent harsh transitions
|
||||
Behavior on panelColor {
|
||||
ColorAnimation {
|
||||
duration: animate ? Kirigami.Units.veryLongDuration * 1.5 : 0
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
}
|
||||
|
||||
// corner radius of the panel
|
||||
property int radius: Kirigami.Units.cornerRadius
|
||||
|
||||
Kirigami.Theme.colorSet: panelType === PanelBackground.PanelType.Popup ? Kirigami.Theme.Window : Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
// very simple shadow for performance
|
||||
Rectangle {
|
||||
id: simpleShadow
|
||||
anchors.top: root.top
|
||||
anchors.topMargin: 1
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
height: root.height
|
||||
|
||||
visible: root.shadow && root.flatten < 1
|
||||
radius: root.radius
|
||||
|
||||
color: Qt.rgba(0, 0, 0, (root.complexShadow ? 0.025 : 0.15) * (1 - root.flatten))
|
||||
opacity: root.complexShadow ? 0 : 1
|
||||
}
|
||||
|
||||
// simple-ish expanded shadow for performance
|
||||
MultiEffect {
|
||||
anchors.fill: background
|
||||
source: background
|
||||
visible: root.complexShadow && root.flatten < 1
|
||||
blurMax: 16
|
||||
|
||||
shadowEnabled: root.complexShadow
|
||||
shadowVerticalOffset: 1
|
||||
shadowOpacity: (panelType === PanelBackground.PanelType.Base ? 0.5 : 0.2) * (1 - root.flatten)
|
||||
shadowColor: "black"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: background
|
||||
anchors.fill: root
|
||||
|
||||
color: Qt.darker(Qt.rgba(root.panelColor.r, root.panelColor.g, root.panelColor.b, root.translucent ? 0.9 : 1), root.pressed ? 3.5 : 1)
|
||||
radius: root.radius
|
||||
|
||||
// Only show border when using a dark background and when the border property is set to true
|
||||
readonly property color borderColor: Qt.darker(Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.textColor, root.panelColor, 0.9), root.pressed ? 3.5 : 1)
|
||||
border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 1 - root.flatten)
|
||||
border.width: root.border && root.flatten < 1 && ((Kirigami.ColorUtils.brightnessForColor(color)) === Kirigami.ColorUtils.Dark || root.forceBorder) ? 1 : 0
|
||||
border.pixelAligned: false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Yari Polla <skilvingr@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.private.nanoshell as NanoShell
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
/*
|
||||
* A context popup menu closable by tapping outside it.
|
||||
* Being it a FullScreenOverlay, no event is delivered to underlying components until it's closed.
|
||||
*
|
||||
* - property relatedTo: Item to which the popup is related; the popup will spawn either above or below it, depending on its y value.
|
||||
* If no item is supplied, the popup will spawn at the center of the screen.
|
||||
* - property title: The title for the menu.
|
||||
* - property menuActions: The menu will be composed of these actions.
|
||||
* - function showOverlay(): Spawns the popup.
|
||||
*/
|
||||
NanoShell.FullScreenOverlay {
|
||||
id: overlay
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
property point mappedGlobalCoordinates
|
||||
property Item relatedTo: null
|
||||
property string title
|
||||
property list<Kirigami.Action> menuActions
|
||||
|
||||
function showOverlay() {
|
||||
if (!overlay.visible) {
|
||||
overlay.visible = true;
|
||||
menu.open();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Item {
|
||||
id: containerItem
|
||||
height: menu.implicitHeight
|
||||
width: menu.implicitWidth
|
||||
|
||||
readonly property point coordinates: {
|
||||
if (relatedTo) { // Place next to Item
|
||||
return mapFromGlobal(mappedGlobalCoordinates.x, mappedGlobalCoordinates.y);
|
||||
} else { // Place at the center of the screen
|
||||
return Qt.point((overlay.width - width) / 2, (overlay.height - height) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
x: coordinates.x
|
||||
y: coordinates.y
|
||||
|
||||
transform: Translate {
|
||||
x: 0
|
||||
y: (containerItem.coordinates.y <= overlay.height/2 ? relatedTo.height : -containerItem.height) - Constants.topPanelHeight
|
||||
}
|
||||
|
||||
PlasmaComponents.Menu {
|
||||
id: menu
|
||||
title: overlay.title
|
||||
closePolicy: PlasmaComponents.Menu.CloseOnReleaseOutside | PlasmaComponents.Menu.CloseOnEscape
|
||||
|
||||
onClosed: overlay.close()
|
||||
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < menuActions.length; i++) {
|
||||
appendItem(menuActions[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function appendItem(button) {
|
||||
menu.addItem(menuItem.createObject(
|
||||
menu,
|
||||
{
|
||||
iconName: button.iconName,
|
||||
text: i18n(button.text),
|
||||
callback: button.triggered
|
||||
}));
|
||||
}
|
||||
Component {
|
||||
id: menuItem
|
||||
|
||||
PlasmaComponents.MenuItem {
|
||||
property string iconName: ""
|
||||
property var callback: () => {}
|
||||
|
||||
icon.name: iconName
|
||||
onClicked: callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -14,6 +14,8 @@ Rectangle {
|
|||
property int screen
|
||||
property var maximizedTracker
|
||||
|
||||
readonly property bool isShowing: height > 0
|
||||
|
||||
// Smooth animation for colored rectangle
|
||||
NumberAnimation on height {
|
||||
id: heightAnim
|
||||
|
|
@ -29,6 +31,7 @@ Rectangle {
|
|||
|
||||
function onShowingWindowChanged() {
|
||||
root.color = 'transparent';
|
||||
root.height = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,8 +73,8 @@ Item {
|
|||
SequentialAnimation {
|
||||
id: openAnimComplex
|
||||
|
||||
// slight pause to give slower devices time to catch up when the item becomes visible
|
||||
PauseAnimation { duration: 20 }
|
||||
// pause for background color to catch up
|
||||
PauseAnimation { duration: 1 }
|
||||
|
||||
ParallelAnimation {
|
||||
id: parallelAnim
|
||||
|
|
@ -167,10 +167,9 @@ Item {
|
|||
id: background
|
||||
anchors.fill: parent
|
||||
|
||||
// Tint the background color if a dark theme is being used
|
||||
color: Kirigami.ColorUtils.brightnessForColor(Kirigami.Theme.backgroundColor) === Kirigami.ColorUtils.Dark ?
|
||||
Kirigami.ColorUtils.tintWithAlpha(colorGenerator.dominant, Kirigami.Theme.backgroundColor, 0.7) :
|
||||
colorGenerator.dominant
|
||||
// Tint the background color so that it is less prominent
|
||||
// This avoids flashing the user all of a sudden with bright colors
|
||||
color: Kirigami.ColorUtils.tintWithAlpha(colorGenerator.dominant, Kirigami.Theme.backgroundColor, 0.7)
|
||||
|
||||
Kirigami.ImageColors {
|
||||
id: colorGenerator
|
||||
|
|
@ -225,4 +224,4 @@ Item {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ pragma Singleton
|
|||
import QtQuick
|
||||
|
||||
import org.kde.plasma.private.volume
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
|
@ -31,57 +32,13 @@ QtObject {
|
|||
// the maximum volume amount
|
||||
readonly property int maxVolumeValue: maxVolumePercent * PulseAudio.NormalVolume / 100
|
||||
|
||||
// step that increments when adjusting the volume
|
||||
readonly property int volumeStep: Math.round(5 * PulseAudio.NormalVolume / 100.0)
|
||||
|
||||
// The current audio volume (updated by connecting to sinks)
|
||||
property int volumeValue
|
||||
readonly property int volumeValue: PreferredDevice.sink ? (PreferredDevice.sink.volume / PulseAudio.NormalVolume) * 100 : 0
|
||||
|
||||
function isDummyOutput(output) {
|
||||
return output && output.name === dummyOutputName;
|
||||
}
|
||||
|
||||
function boundVolume(volume) {
|
||||
return Math.max(PulseAudio.MinimalVolume, Math.min(volume, maxVolumeValue));
|
||||
}
|
||||
|
||||
function volumePercent(volume, max){
|
||||
if (!max) {
|
||||
max = PulseAudio.NormalVolume;
|
||||
}
|
||||
return Math.round(volume / max * maxVolumePercent);
|
||||
}
|
||||
|
||||
function increaseVolume() {
|
||||
if (!PreferredDevice.sink || isDummyOutput(PreferredDevice.sink)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var volume = boundVolume(PreferredDevice.sink.volume + volumeStep);
|
||||
var percent = volumePercent(volume, maxVolumeValue);
|
||||
PreferredDevice.sink.muted = percent == 0;
|
||||
PreferredDevice.sink.volume = volume;
|
||||
}
|
||||
|
||||
function decreaseVolume() {
|
||||
if (!PreferredDevice.sink || isDummyOutput(PreferredDevice.sink)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var volume = boundVolume(PreferredDevice.sink.volume - volumeStep);
|
||||
var percent = volumePercent(volume, maxVolumeValue);
|
||||
PreferredDevice.sink.muted = percent == 0;
|
||||
PreferredDevice.sink.volume = volume;
|
||||
}
|
||||
|
||||
function muteVolume() {
|
||||
if (!PreferredDevice.sink || isDummyOutput(PreferredDevice.sink)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PreferredDevice.sink.muted = !PreferredDevice.sink.muted;
|
||||
}
|
||||
|
||||
function iconName(volume, muted, prefix) {
|
||||
if (!prefix) {
|
||||
prefix = "audio-volume";
|
||||
|
|
@ -99,33 +56,4 @@ QtObject {
|
|||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
// emitted when the volume changed, but not due to sink switching
|
||||
signal volumeChanged()
|
||||
|
||||
property var updateVolume: Connections {
|
||||
target: root.paSinkModel ? (PreferredDevice.sink ? PreferredDevice.sink : null) : null
|
||||
enabled: target !== null
|
||||
|
||||
function onVolumeChanged() {
|
||||
root.volumeValue = root.volumePercent(PreferredDevice.sink.volume, root.maxVolumeValue);
|
||||
root.volumeChanged();
|
||||
}
|
||||
|
||||
function onMutedChanged() {
|
||||
root.volumeValue = PreferredDevice.sink.muted ? 0 : root.volumePercent(PreferredDevice.sink.volume, root.maxVolumeValue);
|
||||
root.volumeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
property var updateVolumeOnSinkChange: Connections {
|
||||
target: root.paSinkModel ? root.paSinkModel : null
|
||||
enabled: target !== null
|
||||
|
||||
function onPreferredSinkChanged() {
|
||||
if (PreferredDevice.sink) {
|
||||
root.volumeValue = root.volumePercent(PreferredDevice.sink.volume, root.maxVolumeValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
components/mobileshell/qml/dataproviders/NetworkInfo.qml
Normal file
15
components/mobileshell/qml/dataproviders/NetworkInfo.qml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
|
||||
import org.kde.plasma.networkmanagement as PlasmaNM
|
||||
|
||||
QtObject {
|
||||
// Initialization of PlasmaNM.Handler is quite heavy, initialize it once for the shell as a singleton.
|
||||
readonly property PlasmaNM.Handler handler: PlasmaNM.Handler {}
|
||||
|
||||
readonly property PlasmaNM.WirelessStatus wirelessStatus: PlasmaNM.WirelessStatus {}
|
||||
}
|
||||
90
components/mobileshell/qml/homescreen/BlurEffect.qml
Normal file
90
components/mobileshell/qml/homescreen/BlurEffect.qml
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
// SPDX-FileCopyrightText: 2023-2025 Devin Lin <devin@kde.org>
|
||||
// SPDX-FileCopyrightText: 2025 Micah Stanley <stanleymicah@proton.me>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
Loader {
|
||||
id: root
|
||||
|
||||
property Item sourceLayer
|
||||
property Item maskSourceLayer
|
||||
// this value is used to switch between blurring the whole wallpaper or just behind the mask areas
|
||||
property real fullBlur: 1
|
||||
// gets multiplied against the screen size to set the texture size
|
||||
readonly property real blurTextureQuality: 0.5
|
||||
readonly property var textureSize: Qt.size(Math.round(root.width * root.blurTextureQuality), Math.round(root.height * root.blurTextureQuality))
|
||||
readonly property int fastBlurRadius: 42
|
||||
|
||||
sourceComponent: Item {
|
||||
// only take samples from wallpaper when we need the blur for performance
|
||||
ShaderEffectSource {
|
||||
id: controlledWallpaperSource
|
||||
anchors.fill: parent
|
||||
|
||||
// this layer will be blurred, so it looks fine to have a lower texture quality to help with performance
|
||||
textureSize: root.textureSize
|
||||
|
||||
hideSource: false
|
||||
opacity: root.fullBlur
|
||||
visible: opacity > 0
|
||||
|
||||
// wallpaper blur
|
||||
// we attempted to use MultiEffect in the past, but it had very poor performance on the PinePhone
|
||||
sourceItem: FastBlur {
|
||||
height: controlledWallpaperSource.textureSize.height
|
||||
width: controlledWallpaperSource.textureSize.width
|
||||
|
||||
cached: true
|
||||
radius: root.fastBlurRadius
|
||||
|
||||
source: ShaderEffectSource {
|
||||
anchors.fill: parent
|
||||
|
||||
textureSize: controlledWallpaperSource.textureSize
|
||||
|
||||
sourceItem: root.sourceLayer
|
||||
hideSource: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// load in the layer mask so we can utilize it with the OpacityMask
|
||||
Item {
|
||||
id: blurMask
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
opacity: 0
|
||||
|
||||
Loader {
|
||||
asynchronous: true
|
||||
active: root.maskSourceLayer != null && root.fullBlur != 1
|
||||
anchors.fill: parent
|
||||
|
||||
sourceComponent: maskSource
|
||||
|
||||
property Component maskSource: Item {
|
||||
ShaderEffectSource {
|
||||
anchors.fill: parent
|
||||
|
||||
sourceItem: root.maskSourceLayer
|
||||
hideSource: false
|
||||
live: true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// here we utilize the mask on the blur layer so we can blur behind the some homescreen items
|
||||
OpacityMask {
|
||||
anchors.fill: parent
|
||||
source: controlledWallpaperSource
|
||||
maskSource: blurMask
|
||||
visible: opacity > 0 && root.maskSourceLayer != null
|
||||
}
|
||||
}
|
||||
}
|
||||
21
components/mobileshell/qml/homescreen/DeviceLock.qml
Normal file
21
components/mobileshell/qml/homescreen/DeviceLock.qml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
|
||||
import org.kde.plasma.private.mobileshell.dpmsplugin as DPMS
|
||||
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
function triggerLock() {
|
||||
MobileShellState.LockscreenDBusClient.lockScreen();
|
||||
__dpms.turnDpmsOff();
|
||||
}
|
||||
|
||||
property DPMS.DPMSUtil __dpms: DPMS.DPMSUtil {
|
||||
id: dpms
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,9 +103,14 @@ Item {
|
|||
itemContainer.zoomIn();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Set initial item container state
|
||||
onIsTaskSwitcherVisibleChanged();
|
||||
}
|
||||
}
|
||||
|
||||
//END API implementation
|
||||
//END API implementation
|
||||
|
||||
Component.onCompleted: {
|
||||
// determine the margins used
|
||||
|
|
@ -128,7 +133,7 @@ Item {
|
|||
|
||||
// animations
|
||||
opacity: 0
|
||||
property real zoomScale: 1
|
||||
property real scaleAmount: 1
|
||||
|
||||
readonly property real zoomScaleOut: 0.8
|
||||
|
||||
|
|
@ -150,7 +155,7 @@ Item {
|
|||
function zoomOutImmediately() {
|
||||
scaleAnim.stop();
|
||||
opacityAnim.stop();
|
||||
zoomScale = zoomScaleOut;
|
||||
scaleAmount = zoomScaleOut;
|
||||
opacity = 0;
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +165,7 @@ Item {
|
|||
running: false
|
||||
}
|
||||
|
||||
NumberAnimation on zoomScale {
|
||||
NumberAnimation on scaleAmount {
|
||||
id: scaleAnim
|
||||
duration: 600
|
||||
running: false
|
||||
|
|
@ -179,8 +184,8 @@ Item {
|
|||
transform: Scale {
|
||||
origin.x: itemContainer.width / 2;
|
||||
origin.y: itemContainer.height / 2;
|
||||
xScale: itemContainer.zoomScale
|
||||
yScale: itemContainer.zoomScale
|
||||
xScale: itemContainer.scaleAmount
|
||||
yScale: itemContainer.scaleAmount
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,15 @@ import QtQuick.Controls as Controls
|
|||
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
import org.kde.plasma.wallpapers.image 2.0 as Wallpaper
|
||||
import org.kde.kquickcontrolsaddons 2.0 as Addons
|
||||
import org.kde.plasma.private.mobileshell.wallpaperimageplugin as WallpaperImagePlugin
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
|
||||
Controls.Drawer {
|
||||
id: imageWallpaperDrawer
|
||||
dragMargin: 0
|
||||
|
||||
property MobileShell.MaskManager maskManager
|
||||
|
||||
required property bool horizontal
|
||||
|
||||
signal wallpaperSettingsRequested()
|
||||
|
|
@ -41,18 +43,18 @@ Controls.Drawer {
|
|||
anchors.leftMargin: imageWallpaperDrawer.leftMargin
|
||||
anchors.rightMargin: imageWallpaperDrawer.rightMargin
|
||||
anchors.bottomMargin: imageWallpaperDrawer.bottomMargin
|
||||
|
||||
orientation: imageWallpaperDrawer.horizontal ? ListView.Vertical : ListView.Horizontal
|
||||
keyNavigationEnabled: true
|
||||
highlightFollowsCurrentItem: true
|
||||
snapMode: ListView.SnapToItem
|
||||
model: imageWallpaper.wallpaperModel
|
||||
// onCountChanged: currentIndex = Math.min(model.indexOf(configDialog.wallpaperConfiguration["Image"]), model.rowCount()-1)
|
||||
headerPositioning: ListView.InlineHeader
|
||||
|
||||
header: Controls.ItemDelegate {
|
||||
id: openSettings
|
||||
width: imageWallpaperDrawer.horizontal ? parent.width : height * (imageWallpaperDrawer.width / imageWallpaperDrawer.Screen.height)
|
||||
height: imageWallpaperDrawer.horizontal ? width / (imageWallpaperDrawer.Screen.width / imageWallpaperDrawer.Screen.height) : parent.height
|
||||
width: imageWallpaperDrawer.horizontal ? wallpapersView.width : height * (imageWallpaperDrawer.width / imageWallpaperDrawer.Screen.height)
|
||||
height: imageWallpaperDrawer.horizontal ? width / (imageWallpaperDrawer.Screen.width / imageWallpaperDrawer.Screen.height) : wallpapersView.height
|
||||
padding: Kirigami.Units.gridUnit / 2
|
||||
leftPadding: padding
|
||||
topPadding: padding
|
||||
|
|
@ -60,10 +62,14 @@ Controls.Drawer {
|
|||
bottomPadding: padding
|
||||
|
||||
background: Rectangle {
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: Qt.rgba(255, 255, 255, (openSettings.down || openSettings.highlighted) ? 0.3 : 0.2)
|
||||
radius: Kirigami.Units.gridUnit / 4
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.gridUnit / 4
|
||||
|
||||
Component.onCompleted: {
|
||||
if (maskManager) {
|
||||
maskManager.assignToMask(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
|
|
@ -72,6 +78,7 @@ Controls.Drawer {
|
|||
implicitHeight: Kirigami.Units.iconSizes.large
|
||||
implicitWidth: Kirigami.Units.iconSizes.large
|
||||
source: 'list-add'
|
||||
color: 'white'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,13 +87,18 @@ Controls.Drawer {
|
|||
}
|
||||
|
||||
delegate: Controls.ItemDelegate {
|
||||
width: imageWallpaperDrawer.horizontal ? parent.width : height * (imageWallpaperDrawer.width / imageWallpaperDrawer.Screen.height)
|
||||
height: imageWallpaperDrawer.horizontal ? width / (imageWallpaperDrawer.Screen.width / imageWallpaperDrawer.Screen.height) : parent.height
|
||||
padding: wallpapersView.currentIndex === index ? Kirigami.Units.gridUnit / 4 : Kirigami.Units.gridUnit / 2
|
||||
leftPadding: padding
|
||||
topPadding: padding
|
||||
rightPadding: padding
|
||||
bottomPadding: padding
|
||||
id: delegate
|
||||
|
||||
width: imageWallpaperDrawer.horizontal ? wallpapersView.width : height * (imageWallpaperDrawer.width / imageWallpaperDrawer.Screen.height)
|
||||
height: imageWallpaperDrawer.horizontal ? width / (imageWallpaperDrawer.Screen.width / imageWallpaperDrawer.Screen.height) : (wallpapersView ? wallpapersView.height : 0)
|
||||
padding: Kirigami.Units.largeSpacing - (wallpapersView.currentIndex === index ? Kirigami.Units.smallSpacing : 0)
|
||||
property real scaleAmount: wallpapersView.currentIndex === index ? 0 : Kirigami.Units.smallSpacing
|
||||
Behavior on scaleAmount {
|
||||
NumberAnimation {
|
||||
duration: Kirigami.Units.longDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
Behavior on padding {
|
||||
NumberAnimation {
|
||||
duration: Kirigami.Units.longDuration
|
||||
|
|
@ -94,6 +106,15 @@ Controls.Drawer {
|
|||
}
|
||||
}
|
||||
|
||||
leftPadding: padding
|
||||
topPadding: padding
|
||||
rightPadding: padding
|
||||
bottomPadding: padding
|
||||
topInset: scaleAmount
|
||||
bottomInset: scaleAmount
|
||||
leftInset: scaleAmount
|
||||
rightInset: scaleAmount
|
||||
|
||||
property bool isCurrent: WallpaperImagePlugin.WallpaperPlugin.homescreenWallpaperPath == model.path
|
||||
onIsCurrentChanged: {
|
||||
if (isCurrent) {
|
||||
|
|
@ -111,34 +132,32 @@ Controls.Drawer {
|
|||
visible: !walliePreview.visible
|
||||
}
|
||||
|
||||
Addons.QPixmapItem {
|
||||
Image {
|
||||
id: walliePreview
|
||||
visible: model.screenshot != null
|
||||
anchors.fill: parent
|
||||
smooth: true
|
||||
pixmap: model.screenshot
|
||||
visible: model.source != null
|
||||
asynchronous: true
|
||||
cache: false
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: model.preview
|
||||
sourceSize: Qt.size(width * 3, height * 3)
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
WallpaperImagePlugin.WallpaperPlugin.setHomescreenWallpaper(model.path);
|
||||
WallpaperImagePlugin.WallpaperPlugin.setHomescreenWallpaper(model.source);
|
||||
}
|
||||
Keys.onReturnPressed: {
|
||||
clicked();
|
||||
}
|
||||
background: Item {
|
||||
Rectangle {
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: wallpapersView.currentIndex === index ? 0 : Kirigami.Units.gridUnit / 4
|
||||
Behavior on margins {
|
||||
NumberAnimation {
|
||||
duration: Kirigami.Units.longDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Qt.rgba(255, 255, 255, (delegate.down || delegate.highlighted) ? 0.4 : 0.2)
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
|
||||
Component.onCompleted: {
|
||||
if (maskManager) {
|
||||
maskManager.assignToMask(this)
|
||||
}
|
||||
radius: Kirigami.Units.gridUnit / 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
61
components/mobileshell/qml/navigationpanel/GesturePanel.qml
Normal file
61
components/mobileshell/qml/navigationpanel/GesturePanel.qml
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
|
||||
import org.kde.plasma.plasmoid
|
||||
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
// Whether the bar background should be opaque
|
||||
required property bool opaqueBar
|
||||
|
||||
signal handleClicked()
|
||||
signal handlePressedAndHeld()
|
||||
|
||||
color: opaqueBar ? Kirigami.Theme.backgroundColor : "transparent"
|
||||
|
||||
// Handle
|
||||
MouseArea {
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(root.width * 0.2, Kirigami.Units.gridUnit * 12)
|
||||
height: parent.height
|
||||
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: root.handleClicked()
|
||||
onPressAndHold: root.handlePressedAndHeld()
|
||||
|
||||
property real startX
|
||||
property real startY
|
||||
onPressed: {
|
||||
startX = mouseX;
|
||||
startY = mouseY;
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
// Trigger gesture after threshold is crossed (root.height)
|
||||
if (startY - mouse.y > root.height) {
|
||||
root.handleClicked();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 4
|
||||
radius: height / 2
|
||||
|
||||
opacity: 0.8
|
||||
color: Kirigami.Theme.textColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,11 +29,14 @@ Item {
|
|||
property NavigationPanelAction leftCornerAction
|
||||
property NavigationPanelAction rightCornerAction
|
||||
|
||||
property real leftPadding: 0
|
||||
property real rightPadding: 0
|
||||
|
||||
property bool isVertical: false
|
||||
|
||||
// drop shadow for icons
|
||||
MultiEffect {
|
||||
anchors.fill: root
|
||||
anchors.fill: icons
|
||||
visible: shadow
|
||||
source: icons
|
||||
blurMax: 16
|
||||
|
|
@ -42,25 +45,25 @@ Item {
|
|||
shadowOpacity: 0.8
|
||||
}
|
||||
|
||||
// background colour
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.backgroundColor
|
||||
}
|
||||
|
||||
Item {
|
||||
id: icons
|
||||
anchors.fill: parent
|
||||
|
||||
property real buttonLength: 0
|
||||
|
||||
// background colour
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.backgroundColor
|
||||
}
|
||||
|
||||
NavigationPanelButton {
|
||||
id: leftCornerButton
|
||||
visible: root.leftCornerAction.visible
|
||||
Kirigami.Theme.colorSet: root.foregroundColorGroup
|
||||
Kirigami.Theme.inherit: false
|
||||
enabled: root.leftCornerAction.enabled
|
||||
iconSizeFactor: root.leftCornerAction.iconSizeFactor
|
||||
shrinkSize: root.leftCornerAction.shrinkSize
|
||||
iconSource: root.leftCornerAction.iconSource
|
||||
onClicked: {
|
||||
if (enabled) {
|
||||
|
|
@ -76,7 +79,7 @@ Item {
|
|||
Kirigami.Theme.colorSet: root.foregroundColorGroup
|
||||
Kirigami.Theme.inherit: false
|
||||
enabled: root.leftAction.enabled
|
||||
iconSizeFactor: root.leftAction.iconSizeFactor
|
||||
shrinkSize: root.leftAction.shrinkSize
|
||||
iconSource: root.leftAction.iconSource
|
||||
onClicked: {
|
||||
if (enabled) {
|
||||
|
|
@ -92,7 +95,7 @@ Item {
|
|||
Kirigami.Theme.colorSet: root.foregroundColorGroup
|
||||
Kirigami.Theme.inherit: false
|
||||
enabled: root.middleAction.enabled
|
||||
iconSizeFactor: root.middleAction.iconSizeFactor
|
||||
shrinkSize: root.middleAction.shrinkSize
|
||||
iconSource: root.middleAction.iconSource
|
||||
onClicked: {
|
||||
if (enabled) {
|
||||
|
|
@ -107,7 +110,7 @@ Item {
|
|||
Kirigami.Theme.colorSet: root.foregroundColorGroup
|
||||
Kirigami.Theme.inherit: false
|
||||
enabled: root.rightAction.enabled
|
||||
iconSizeFactor: root.rightAction.iconSizeFactor
|
||||
shrinkSize: root.rightAction.shrinkSize
|
||||
iconSource: root.rightAction.iconSource
|
||||
onClicked: {
|
||||
if (enabled) {
|
||||
|
|
@ -122,7 +125,7 @@ Item {
|
|||
Kirigami.Theme.colorSet: root.foregroundColorGroup
|
||||
Kirigami.Theme.inherit: false
|
||||
enabled: root.rightCornerAction.enabled
|
||||
iconSizeFactor: root.rightCornerAction.iconSizeFactor
|
||||
shrinkSize: root.rightCornerAction.shrinkSize
|
||||
iconSource: root.rightCornerAction.iconSource
|
||||
onClicked: {
|
||||
if (enabled) {
|
||||
|
|
@ -138,6 +141,10 @@ Item {
|
|||
when: root.isVertical
|
||||
PropertyChanges {
|
||||
target: icons
|
||||
anchors {
|
||||
topMargin: root.leftPadding
|
||||
bottomMargin: root.rightPadding
|
||||
}
|
||||
buttonLength: Math.min(Kirigami.Units.gridUnit * 10, icons.height * 0.7 / 3)
|
||||
}
|
||||
AnchorChanges {
|
||||
|
|
@ -198,6 +205,10 @@ Item {
|
|||
when: !root.isVertical
|
||||
PropertyChanges {
|
||||
target: icons
|
||||
anchors {
|
||||
leftMargin: root.leftPadding
|
||||
rightMargin: root.rightPadding
|
||||
}
|
||||
buttonLength: Math.min(Kirigami.Units.gridUnit * 8, icons.width * 0.7 / 3)
|
||||
}
|
||||
AnchorChanges {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ QtObject {
|
|||
property bool enabled
|
||||
property bool visible: true
|
||||
property string iconSource
|
||||
property real iconSizeFactor
|
||||
property real shrinkSize
|
||||
|
||||
signal triggered()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ Controls.AbstractButton {
|
|||
width: Math.min(parent.width, parent.height)
|
||||
height: width
|
||||
|
||||
property double iconSizeFactor: 1
|
||||
property int shrinkSize: 0
|
||||
property alias iconSource: icon.source
|
||||
|
||||
MobileShell.HapticsEffect {
|
||||
|
|
@ -79,9 +79,11 @@ Controls.AbstractButton {
|
|||
Kirigami.Theme.colorSet: button.Kirigami.Theme.colorSet
|
||||
|
||||
readonly property real side: Math.min(button.width, button.height)
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: Math.round((side - side * iconSizeFactor * 0.6) / 2)
|
||||
}
|
||||
anchors.centerIn: parent
|
||||
|
||||
implicitHeight: Kirigami.Units.iconSizes.smallMedium - shrinkSize
|
||||
implicitWidth: Kirigami.Units.iconSizes.smallMedium - shrinkSize
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Micah Stanley <stanleymicah@proton.me>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
|
||||
pragma Singleton
|
||||
|
||||
/**
|
||||
* This wraps the NotificationPopupProvider component so that we can avoid loading side
|
||||
* effects from imports (since this is a singleton and initialized immediately on import).
|
||||
*/
|
||||
Loader {
|
||||
id: root
|
||||
sourceComponent: Component {
|
||||
MobileShell.NotificationPopupProvider {}
|
||||
}
|
||||
|
||||
function load() {
|
||||
root.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
64
components/mobileshell/qml/popups/PopupProviderLoader.qml
Normal file
64
components/mobileshell/qml/popups/PopupProviderLoader.qml
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
|
||||
* SPDX-FileCopyrightText: 2024-2025 Micah Stanley <stanleymicah@proton.me>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
|
||||
pragma Singleton
|
||||
|
||||
/**
|
||||
* This wraps the popup components so that we can avoid loading side
|
||||
* effects from imports (since this is a singleton and initialized immediately on import).
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// WARNING: only call this load from within the plasmashell process, because
|
||||
// multiple bindings of the shortcut may break it entirely (hardware volume keys)
|
||||
function load() {
|
||||
if (!volumeOSD.active) {
|
||||
volumeOSD.active = true;
|
||||
}
|
||||
if (!kscreenOSD.active) {
|
||||
kscreenOSD.active = true;
|
||||
}
|
||||
if (!notifications.active) {
|
||||
notifications.active = true;
|
||||
}
|
||||
if (!actionButtons.active) {
|
||||
actionButtons.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: volumeOSD
|
||||
sourceComponent: Component {
|
||||
MobileShell.VolumeOSDProvider {}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: kscreenOSD
|
||||
sourceComponent: Component {
|
||||
MobileShell.KScreenOSDProvider {}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: notifications
|
||||
sourceComponent: Component {
|
||||
MobileShell.NotificationPopupProvider {}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: actionButtons
|
||||
sourceComponent: Component {
|
||||
MobileShell.ActionButtonsProvider {}
|
||||
}
|
||||
}
|
||||
}
|
||||
161
components/mobileshell/qml/popups/actionbuttons/ActionButton.qml
Normal file
161
components/mobileshell/qml/popups/actionbuttons/ActionButton.qml
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 Micah Stanley <stanleymicah@proton.me>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
|
||||
import org.kde.layershell 1.0 as LayerShell
|
||||
|
||||
|
||||
Window {
|
||||
id: root
|
||||
|
||||
readonly property int size: Kirigami.Units.gridUnit * 2
|
||||
readonly property int margins: Math.round(Kirigami.Units.largeSpace * 0.5)
|
||||
|
||||
property int screenCorner: ActionButton.ScreenCorner.BottomRight
|
||||
property int angle: 0
|
||||
property string iconSource
|
||||
property bool active: false
|
||||
|
||||
signal triggered()
|
||||
|
||||
enum ScreenCorner {
|
||||
BottomRight,
|
||||
BottomLeft,
|
||||
TopLeft,
|
||||
TopRight
|
||||
}
|
||||
|
||||
// When the button is animating its disappearance, make sure it is transparent to inputs.
|
||||
onActiveChanged: {
|
||||
ShellUtil.setInputTransparent(root, !active)
|
||||
if (active) {
|
||||
root.visible = true;
|
||||
root.raise();
|
||||
hideButton.stop();
|
||||
return;
|
||||
}
|
||||
hideButton.restart();
|
||||
}
|
||||
|
||||
LayerShell.Window.scope: "overlay"
|
||||
LayerShell.Window.margins.top: margins
|
||||
LayerShell.Window.margins.bottom: margins
|
||||
LayerShell.Window.margins.left: margins
|
||||
LayerShell.Window.margins.right: margins
|
||||
LayerShell.Window.layer: LayerShell.Window.LayerOverlay
|
||||
LayerShell.Window.exclusionZone: -1
|
||||
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
||||
LayerShell.Window.anchors: {
|
||||
if (screenCorner === ActionButton.ScreenCorner.TopLeft) {
|
||||
return LayerShell.Window.AnchorTop | LayerShell.Window.AnchorLeft
|
||||
} else if (screenCorner === ActionButton.ScreenCorner.BottomRight) {
|
||||
return LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorRight
|
||||
} else if (screenCorner === ActionButton.ScreenCorner.BottomLeft) {
|
||||
return LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft
|
||||
} else {
|
||||
return LayerShell.Window.AnchorTop | LayerShell.Window.AnchorRight
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
// Double the set button size to leave room for button scale animation.
|
||||
width: size * 2
|
||||
height: size * 2
|
||||
|
||||
visible: active
|
||||
|
||||
color: "transparent"
|
||||
|
||||
// Hide the root window after the button disappearing animation finishes.
|
||||
Timer {
|
||||
id: hideButton
|
||||
interval: Kirigami.Units.longDuration
|
||||
repeat: false
|
||||
onTriggered: if (!active) root.visible = false;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Because the window surface area had to be made larger to accommodate the button scale animation,
|
||||
// set the input region to the size of the actual button.
|
||||
ShellUtil.setInputRegion(root, Qt.rect((root.width - size) / 2, (root.height - size) / 2, size, size));
|
||||
ShellUtil.setInputTransparent(root, !active);
|
||||
}
|
||||
|
||||
Controls.Control {
|
||||
id: content
|
||||
anchors.centerIn: parent
|
||||
width: root.size
|
||||
height: root.size
|
||||
opacity: root.active ? 1 : 0
|
||||
|
||||
property double scale: !root.active ? 0.5 : (button.pressed ? 1.5 : 1)
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Kirigami.Units.longDuration
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Kirigami.Units.longDuration
|
||||
easing.type: Easing.OutCirc
|
||||
}
|
||||
}
|
||||
|
||||
transform: Scale {
|
||||
origin.x: root.size / 2
|
||||
origin.y: root.size / 2
|
||||
xScale: content.scale
|
||||
yScale: content.scale
|
||||
}
|
||||
|
||||
MobileShell.PanelBackground {
|
||||
anchors.fill: parent
|
||||
panelType: MobileShell.PanelBackground.PanelType.Popup
|
||||
radius: root.size
|
||||
}
|
||||
|
||||
Controls.AbstractButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
|
||||
MobileShell.HapticsEffect {
|
||||
id: haptics
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
Kirigami.Icon {
|
||||
anchors.centerIn: parent
|
||||
width: Kirigami.Units.iconSizes.small
|
||||
height: Kirigami.Units.iconSizes.small
|
||||
transformOrigin: Item.Center
|
||||
rotation: root.angle
|
||||
source: root.iconSource
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: {
|
||||
haptics.buttonVibrate();
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
if (active) root.triggered();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 Micah Stanley <stanleymicah@proton.me>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
|
||||
/**
|
||||
* This sets up the popup action buttons.
|
||||
*/
|
||||
QtObject {
|
||||
id: component
|
||||
|
||||
property var rotationButton: RotationButton {}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 Micah Stanley <stanleymicah@proton.me>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.plasma.private.mobileshell.rotationplugin as RotationPlugin
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
|
||||
ActionButton {
|
||||
id: root
|
||||
|
||||
readonly property int deviceRotation: RotationPlugin.RotationUtil.deviceRotation
|
||||
readonly property int currentRotation: RotationPlugin.RotationUtil.currentRotation
|
||||
|
||||
iconSource: "rotation-allowed-symbolic"
|
||||
|
||||
// Update button position and timeout when device rotation changes.
|
||||
onDeviceRotationChanged: {
|
||||
if (ShellSettings.Settings.navigationPanelEnabled) return;
|
||||
// reset button if visible
|
||||
if (root.visible) {
|
||||
root.active = false;
|
||||
timeout.stop();
|
||||
}
|
||||
if (!RotationPlugin.RotationUtil.showRotationButton) return;
|
||||
// Position at the bottom left edge of actual device, regardless of current rotation.
|
||||
root.screenCorner = (currentRotation + 1) % 4;
|
||||
// match angle to physical device rotation.
|
||||
root.angle = ((4 + currentRotation - deviceRotation) % 4) * 90;
|
||||
root.active = true;
|
||||
}
|
||||
|
||||
// Rotate to suggested rotation if button is pressed.
|
||||
onTriggered: {
|
||||
root.visible = false;
|
||||
root.active = false;
|
||||
timeout.stop();
|
||||
rotate.restart();
|
||||
}
|
||||
|
||||
// rotate on timeout to give time to hide the button before rotation happens
|
||||
Timer {
|
||||
id: rotate
|
||||
interval: 0
|
||||
repeat: false
|
||||
onTriggered: RotationPlugin.RotationUtil.rotateToSuggestedRotation();
|
||||
}
|
||||
|
||||
// When the button is active, hide it after a certain amount of time has passed.
|
||||
// This is to prevent the button form bothering the user when they do not wish to rotate.
|
||||
onActiveChanged: if (active) timeout.restart();
|
||||
|
||||
Timer {
|
||||
id: timeout
|
||||
interval: 10000
|
||||
repeat: false
|
||||
onTriggered: active = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// SPDX-FileCopyrightText: 2025 Sebastian Kügler <sebas@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQml
|
||||
|
||||
import org.kde.plasma.quicksetting.kscreenosd 1.0
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
|
||||
|
||||
/**
|
||||
* This switches between docked / convergence mode and normal when a monitor
|
||||
* is plugged in or unplugged.
|
||||
*/
|
||||
QtObject {
|
||||
id: component
|
||||
|
||||
property bool initialConvergenceMode: false
|
||||
|
||||
property var apiListener: Connections {
|
||||
target: KScreenOSDUtil
|
||||
function onOutputsChanged() {
|
||||
if (KScreenOSDUtil.outputs > 1) {
|
||||
initialConvergenceMode = ShellSettings.Settings.convergenceModeEnabled;
|
||||
}
|
||||
- console.log("KScreenOSDProvider convergenceModeEnabled: "
|
||||
- + (KScreenOSDUtil.outputs > 1 ? "true" : (initialConvergenceMode ? "TRUE" : "FALSE")));
|
||||
ShellSettings.Settings.convergenceModeEnabled = KScreenOSDUtil.outputs > 1 ? true : initialConvergenceMode;
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (KScreenOSDUtil.outputs < 2) {
|
||||
initialConvergenceMode = ShellSettings.Settings.convergenceModeEnabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Micah Stanley <stanleymicah@proton.me>
|
||||
* SPDX-FileCopyrightText: 2024-2025 Micah Stanley <stanleymicah@proton.me>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
|
@ -16,7 +16,7 @@ import org.kde.plasma.private.mobileshell.state as MobileShellState
|
|||
import org.kde.layershell 1.0 as LayerShell
|
||||
|
||||
import org.kde.notificationmanager as NotificationManager
|
||||
import org.kde.plasma.plasma5support 2.0 as P5Support
|
||||
import org.kde.plasma.clock
|
||||
|
||||
Item {
|
||||
id: notificationPopup
|
||||
|
|
@ -29,10 +29,13 @@ Item {
|
|||
property real openOffset
|
||||
|
||||
// calculate the position needed to at when the expanded drawer is active
|
||||
readonly property real fullOpenOffset: popupDrawerOpened ? aboveNotificationFullOffset + aboveNotificationHeight + Kirigami.Units.gridUnit : 0
|
||||
readonly property real fullOpenOffset: popupDrawerOpened ? aboveNotificationFullOffset + aboveNotificationHeight + Kirigami.Units.largeSpacing : 0
|
||||
property real aboveNotificationFullOffset: 0
|
||||
property int aboveNotificationHeight: 0
|
||||
|
||||
// set to true when notification is swiped up by user
|
||||
property bool closedWithSwipe: false
|
||||
|
||||
// the drag offset on the current popup notification - used to position notification when stacked underneath
|
||||
property real currentDragOffset: {
|
||||
let current = popupNotifications.currentPopupIndex == notificationPopup.popupIndex;
|
||||
|
|
@ -47,7 +50,6 @@ Item {
|
|||
onTriggered: {
|
||||
visible = true;
|
||||
updateNotificationPopups();
|
||||
checkActionDrawerOpened();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +72,7 @@ Item {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
onTriggered: notificationPopup.closePopup()
|
||||
onTriggered: notificationPopup.closePopup(popupIndex);
|
||||
}
|
||||
|
||||
// the value of how much time is left, normalized from 1 to 0
|
||||
|
|
@ -131,7 +133,7 @@ Item {
|
|||
easing.type: Easing.OutExpo
|
||||
}
|
||||
|
||||
// if the popup height ever changes, update the notification below wiht new height
|
||||
// if the popup height ever changes, update the notification below with new height
|
||||
// also update the allowed touch area for the main window
|
||||
onPopupHeightChanged: {
|
||||
let abovePopup = popupNotifications.objectAt(popupIndex + 1)
|
||||
|
|
@ -143,14 +145,14 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
// if the offset position need in the expanded drawer changes, update the notification below wiht new offset
|
||||
// if the offset position need in the expanded drawer changes, update the notification below with new offset
|
||||
onFullOpenOffsetChanged: {
|
||||
let abovePopup = popupNotifications.objectAt(popupIndex + 1)
|
||||
if (popupIndex + 1 < popupCount && abovePopup) {
|
||||
abovePopup.aboveNotificationFullOffset = fullOpenOffset;
|
||||
}
|
||||
}
|
||||
// if the notification is being draged and is the current one
|
||||
// if the notification is being dragged and is the current one
|
||||
// update 'currentDragOffset' so all notifications can easily access this value
|
||||
onDragOffsetChanged: {
|
||||
let abovePopup = popupNotifications.objectAt(popupIndex + 1)
|
||||
|
|
@ -161,7 +163,7 @@ Item {
|
|||
// if a new notification is added, update the above notification values need for the expanded drawer
|
||||
onPopupCountChanged: {
|
||||
let abovePopup = popupNotifications.objectAt(popupIndex + 1)
|
||||
if (popupIndex + 1 < abovePopup) {
|
||||
if (popupIndex + 1 < popupCount && abovePopup) {
|
||||
abovePopup.aboveNotificationHeight = popupHeight;
|
||||
abovePopup.aboveNotificationFullOffset = fullOpenOffset;
|
||||
}
|
||||
|
|
@ -169,12 +171,14 @@ Item {
|
|||
// update the current popup index value if the index ever changes.
|
||||
onPopupIndexChanged: {
|
||||
if (!isClosing && !inPopupDrawer && !waiting) {
|
||||
// if index goes below zero, assume it is being closed externally and move over to the next popup
|
||||
if (popupIndex < 0 ) {
|
||||
closePopup(0);
|
||||
return;
|
||||
}
|
||||
popupNotifications.currentPopupIndex = popupIndex;
|
||||
}
|
||||
}
|
||||
// if the action drawer opens, it is best to dismiss all popup notifications
|
||||
onIsActionDrawerOpenChanged: checkActionDrawerOpened()
|
||||
|
||||
property bool isActionDrawerOpen: MobileShellState.ShellDBusClient.isActionDrawerOpen
|
||||
|
||||
property bool waiting: true
|
||||
|
|
@ -206,16 +210,6 @@ Item {
|
|||
return model.timeout;
|
||||
}
|
||||
|
||||
// check if the action drawer is opened and the popup is fully created
|
||||
// if so, close the popup with a scale effect
|
||||
function checkActionDrawerOpened() {
|
||||
if (isActionDrawerOpen && popupNotifications.objectAt(popupIndex)) {
|
||||
notificationPopup.expired();
|
||||
keyboardInteractivity = LayerShell.Window.KeyboardInteractivityNone;
|
||||
notificationItem.state = "closeWithScale";
|
||||
}
|
||||
}
|
||||
|
||||
// show the top most notification in the list and move the rest to the popup drawer
|
||||
function updateNotificationPopups() {
|
||||
if (popupCount != 1) {
|
||||
|
|
@ -229,7 +223,7 @@ Item {
|
|||
|
||||
function showNotificationPopup() {
|
||||
if (isClosing) {
|
||||
closePopup();
|
||||
closePopup(popupIndex);
|
||||
return;
|
||||
}
|
||||
if (notificationItem.state != "open") {
|
||||
|
|
@ -252,7 +246,7 @@ Item {
|
|||
if (notificationPopup.popupDrawerOpened && notificationItem.state != "inDrawerClosed" && notificationItem.state != "open") {
|
||||
notificationItem.offset = openOffset;
|
||||
notificationItem.scale = 0.75;
|
||||
notificationItem.opacity = 0.0;
|
||||
notificationItem.popupOpacity = 0.0;
|
||||
}
|
||||
notificationItem.state = "inDrawerClosed";
|
||||
notificationPopup.removeKeyboardFocus();
|
||||
|
|
@ -263,7 +257,7 @@ Item {
|
|||
if (notificationPopup.popupDrawerOpened && notificationItem.state != "open" && notificationItem.state != "inDrawerClosed") {
|
||||
notificationItem.offset = openOffset;
|
||||
notificationItem.scale = 0.75;
|
||||
notificationItem.opacity = 0.0;
|
||||
notificationItem.popupOpacity = 0.0;
|
||||
}
|
||||
notificationItem.state = "open";
|
||||
notificationPopup.removeKeyboardFocus();
|
||||
|
|
@ -271,19 +265,19 @@ Item {
|
|||
|
||||
// if the notification ever expires, close it and move on to the next one in the list.
|
||||
property bool isExpired: model.expired
|
||||
onIsExpiredChanged: closePopup()
|
||||
onIsExpiredChanged: closePopup(popupIndex)
|
||||
|
||||
// this closes the popup notification with the relvent animation while updating the popup below to show, if any exist
|
||||
function closePopup() {
|
||||
function closePopup(index: int) {
|
||||
notificationPopup.removeKeyboardFocus();
|
||||
notificationPopup.setInputTransparent();
|
||||
if (popupIndex + 1 < popupCount) {
|
||||
popupNotifications.objectAt(popupIndex + 1).aboveNotificationHeight = 0;
|
||||
popupNotifications.objectAt(popupIndex + 1).aboveNotificationFullOffset = 0;
|
||||
if (index + 1 < popupCount) {
|
||||
popupNotifications.objectAt(index + 1).aboveNotificationHeight = 0;
|
||||
popupNotifications.objectAt(index + 1).aboveNotificationFullOffset = 0;
|
||||
}
|
||||
|
||||
if (popupCount > 1) {
|
||||
let nextNotificationIdx = popupIndex + (popupIndex < popupCount - 1 ? 1 : -1);
|
||||
let nextNotificationIdx = index + (index < popupCount - 1 ? 1 : -1);
|
||||
let nextNotification = popupNotifications.objectAt(nextNotificationIdx);
|
||||
|
||||
if (nextNotification != null) {
|
||||
|
|
@ -329,7 +323,10 @@ Item {
|
|||
modelIndex: notificationPopup.popupIndex
|
||||
notificationsModel: notificationPopup.notificationsModel
|
||||
notificationsModelType: notificationPopup.notificationsModelType
|
||||
timeSource: notificationPopup.timeDataSource
|
||||
clockSource: notificationPopup.timeDataSource
|
||||
panelType: notificationPopup.popupDrawerOpened ?
|
||||
MobileShell.PanelBackground.PanelType.Drawer :
|
||||
MobileShell.PanelBackground.PanelType.Popup
|
||||
|
||||
currentPopupHeight: popupNotifications.currentPopupHeight
|
||||
|
||||
|
|
@ -344,33 +341,57 @@ Item {
|
|||
preventDismissTimeout = true;
|
||||
}
|
||||
|
||||
onDismissRequested: closePopup()
|
||||
onDismissRequested: closePopup(popupIndex)
|
||||
|
||||
property real offset: closedOffset
|
||||
property real scale: 1.0
|
||||
property real drawerScale: 1 - Math.max(notificationPopup.popupIndex - popupNotifications.currentPopupIndex, 1) * 0.075
|
||||
property real popupOpacity: 1.0 // controls the opacity of the notification popup when outside the popup drawer
|
||||
property real drawerScale: {
|
||||
if (notificationPopup.popupDrawerOpened) {
|
||||
return 0; // when popup drawer is opened, reset scale to 0
|
||||
}
|
||||
let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex;
|
||||
// clamp the index value to avoid scaling too much with animations
|
||||
let indexClamped = Math.max(Math.min(index, 2), 0);
|
||||
return indexClamped * 0.075;
|
||||
}
|
||||
property real drawerAddedOffset: {
|
||||
if (notificationPopup.popupDrawerOpened) {
|
||||
return 0; // when popup drawer is opened, reset any added height to 0
|
||||
}
|
||||
let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex;
|
||||
// clamp the index value to avoid moving too much with animations
|
||||
let indexClamped = Math.max(Math.min(index, 2), -1);
|
||||
return Kirigami.Units.gridUnit * 0.5 * indexClamped;
|
||||
}
|
||||
property real drawerOpacity: {
|
||||
let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex;
|
||||
if (index > 2 && !notificationPopup.popupDrawerOpened) {
|
||||
return 0; // make this popup invisible if it is below 3 other popups
|
||||
} else {
|
||||
return 1; // when popup drawer is opened, reset opacity to 1
|
||||
}
|
||||
}
|
||||
Behavior on drawerScale {
|
||||
NumberAnimation {
|
||||
duration: Kirigami.Units.veryLongDuration
|
||||
easing.type: Easing.OutExpo
|
||||
duration: Kirigami.Units.veryLongDuration * 1.25
|
||||
easing.type: Easing.OutQuint
|
||||
}
|
||||
}
|
||||
property real drawerAddedOffset: Kirigami.Units.gridUnit * 0.5 * Math.max(notificationPopup.popupIndex - popupNotifications.currentPopupIndex, 1)
|
||||
Behavior on drawerAddedOffset {
|
||||
NumberAnimation {
|
||||
duration: Kirigami.Units.veryLongDuration
|
||||
easing.type: Easing.OutExpo
|
||||
duration: Kirigami.Units.veryLongDuration * 1.25
|
||||
easing.type: Easing.OutQuint
|
||||
}
|
||||
}
|
||||
property real drawerOpacity: (Math.max(notificationPopup.popupIndex - popupNotifications.currentPopupIndex, 1) > 2) ? 0 : 1
|
||||
Behavior on drawerOpacity {
|
||||
NumberAnimation {
|
||||
duration: Kirigami.Units.veryLongDuration
|
||||
easing.type: Easing.OutExpo
|
||||
duration: Kirigami.Units.veryLongDuration * 1.25
|
||||
easing.type: Easing.OutQuint
|
||||
}
|
||||
}
|
||||
|
||||
opacity: 1.0
|
||||
opacity: Math.min(popupOpacity, drawerOpacity)
|
||||
|
||||
state: ""
|
||||
|
||||
|
|
@ -384,7 +405,7 @@ Item {
|
|||
target: notificationItem; scale: 1.0
|
||||
}
|
||||
PropertyChanges {
|
||||
target: notificationItem; opacity: 1.0
|
||||
target: notificationItem; popupOpacity: 1.0
|
||||
}
|
||||
},
|
||||
State {
|
||||
|
|
@ -396,7 +417,7 @@ Item {
|
|||
target: notificationItem; scale: 1.0
|
||||
}
|
||||
PropertyChanges {
|
||||
target: notificationItem; opacity: 1.0
|
||||
target: notificationItem; popupOpacity: 1.0
|
||||
}
|
||||
},
|
||||
State {
|
||||
|
|
@ -408,47 +429,88 @@ Item {
|
|||
target: notificationItem; scale: 0.75
|
||||
}
|
||||
PropertyChanges {
|
||||
target: notificationItem; opacity: 0.0
|
||||
target: notificationItem; popupOpacity: 0.0
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "inDrawerClosed"
|
||||
PropertyChanges {
|
||||
target: notificationItem; offset: notificationPopup.openOffset + (notificationPopup.popupDrawerOpened ? 0 : drawerAddedOffset)
|
||||
target: notificationItem; offset: notificationPopup.openOffset
|
||||
}
|
||||
PropertyChanges {
|
||||
target: notificationItem; scale: notificationPopup.popupDrawerOpened ? 1 : drawerScale
|
||||
target: notificationItem; scale: 1
|
||||
}
|
||||
PropertyChanges {
|
||||
target: notificationItem; opacity: notificationPopup.popupDrawerOpened ? 1 : drawerOpacity
|
||||
target: notificationItem; popupOpacity: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
readonly property int notificationEasing: {
|
||||
// check whether the popup is the current one or above it
|
||||
let topPopup = popupNotifications.currentPopupIndex >= notificationPopup.popupIndex;
|
||||
// check whether the popup has any popups below it
|
||||
let popupBelow = notificationPopup.popupCount - notificationPopup.popupIndex > 1;
|
||||
let popupOpening = notificationItem.state == "open" || notificationItem.state == "inDrawerClosed";
|
||||
let popupClosing = notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale"
|
||||
if (notificationPopup.closedWithSwipe || (topPopup && popupClosing && popupBelow)) {
|
||||
// set the easing type to linear when closed with a swipe or if a popup is below when closing
|
||||
// as to make sure the popup feels like it is keeping it's momentum
|
||||
return Easing.Linear;
|
||||
} else if (popupOpening) {
|
||||
// set the easing type to 'Out' when opening so the popup will have a gentle landing
|
||||
return Easing.OutQuint;
|
||||
} else {
|
||||
// if above conditions fail, set the easing type to 'In' so the popup will build up speed for it's exit
|
||||
return Easing.InQuint;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real notificationDuration: {
|
||||
// check whether the popup is the current one or above it
|
||||
let topPopup = popupNotifications.currentPopupIndex >= notificationPopup.popupIndex;
|
||||
// check whether the popup has any popups below it
|
||||
let popupBelow = notificationPopup.popupCount - notificationPopup.popupIndex > 1;
|
||||
let popupClosing = notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale"
|
||||
if (notificationPopup.closedWithSwipe || (topPopup && popupClosing && popupBelow)) {
|
||||
// make sure the speed it faster when closed with a swipe or if there is a popup below when closing
|
||||
// as to make sure the speed feels comparable with the easing type is set to linear
|
||||
return Kirigami.Units.veryLongDuration * 0.5;
|
||||
} else {
|
||||
return Kirigami.Units.veryLongDuration * 1.25;
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
SequentialAnimation {
|
||||
ParallelAnimation {
|
||||
PropertyAnimation {
|
||||
properties: "offset"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.5
|
||||
properties: "offset"
|
||||
easing.type: notificationItem.notificationEasing
|
||||
duration: notificationItem.notificationDuration
|
||||
}
|
||||
PropertyAnimation {
|
||||
properties: "scale"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.5
|
||||
properties: "scale"
|
||||
easing.type: notificationItem.notificationEasing
|
||||
duration: notificationItem.notificationDuration
|
||||
}
|
||||
PropertyAnimation {
|
||||
properties: "opacity"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.5
|
||||
properties: "popupOpacity"
|
||||
easing.type: notificationItem.notificationEasing
|
||||
duration: notificationItem.notificationDuration
|
||||
}
|
||||
}
|
||||
ScriptAction {
|
||||
script: {
|
||||
if (notificationItem.state == "open") {
|
||||
preventDismissTimeout = false;
|
||||
notificationPopup.preventDismissTimeout = false;
|
||||
notificationPopup.updateTouchArea();
|
||||
} else if (notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale") {
|
||||
preventDismissTimeout = true;
|
||||
if (dismissTimeout) {
|
||||
notificationPopup.preventDismissTimeout = true;
|
||||
if (notificationPopup.dismissTimeout) {
|
||||
notificationPopup.dismissClicked();
|
||||
} else if (!isActionDrawerOpen) {
|
||||
notificationPopup.expired();
|
||||
} else {
|
||||
notificationPopup.expired();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -460,15 +522,15 @@ Item {
|
|||
Scale {
|
||||
origin.x: Math.round(notificationPopup.popupWidth / 2)
|
||||
origin.y: notificationPopup.scaleOriginY
|
||||
xScale: notificationItem.scale
|
||||
yScale: notificationItem.scale
|
||||
xScale: notificationItem.scale - notificationItem.drawerScale
|
||||
yScale: notificationItem.scale - notificationItem.drawerScale
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
transform: [
|
||||
Translate {
|
||||
y: notificationItem.offset + notificationPopup.fullOffsetAn + notificationPopup.dragOffset + notificationPopup.currentDragOffset
|
||||
y: notificationItem.offset + notificationPopup.fullOffsetAn + notificationPopup.dragOffset + notificationPopup.currentDragOffset + notificationItem.drawerAddedOffset
|
||||
}
|
||||
]
|
||||
|
||||
|
|
@ -501,13 +563,15 @@ Item {
|
|||
startActive = active;
|
||||
notificationPopup.preventDismissTimeout = true;
|
||||
if (!active && !(notificationItem.state == "closeWithScale" || notificationItem.state == "closeWithMove")) {
|
||||
dragOffsetAn.running = true;
|
||||
if ((lastOffset - notificationPopup.dragOffset > 1.0 && notificationPopup.dragOffset < 0) || (-(notificationPopup.openOffset - notificationPopup.closedOffset) / 4 > notificationPopup.dragOffset)) {
|
||||
// this code is called when the notifition is swiped or draged to the top.
|
||||
notificationPopup.closePopup();
|
||||
// this code is called when the notification is swiped or dragged to the top.
|
||||
notificationPopup.closedWithSwipe = true;
|
||||
notificationPopup.closePopup(popupIndex);
|
||||
return;
|
||||
} else if (notificationPopup.dragOffset - lastOffset > 1.0 || Kirigami.Units.gridUnit * 3 < notificationPopup.dragOffset) {
|
||||
// this code is called when the notifition is swiped or draged down.
|
||||
}
|
||||
dragOffsetAn.running = true;
|
||||
if (notificationPopup.dragOffset - lastOffset > 1.0 || Kirigami.Units.gridUnit * 3 < notificationPopup.dragOffset) {
|
||||
// this code is called when the notification is swiped or dragged down.
|
||||
}
|
||||
notificationPopup.preventDismissTimeout = (keyboardInteractivity == LayerShell.Window.KeyboardInteractivityOnDemand);
|
||||
} else {
|
||||
|
|
@ -15,7 +15,7 @@ import org.kde.plasma.private.mobileshell.state as MobileShellState
|
|||
import org.kde.layershell 1.0 as LayerShell
|
||||
|
||||
import org.kde.notificationmanager as NotificationManager
|
||||
import org.kde.plasma.plasma5support 2.0 as P5Support
|
||||
import org.kde.plasma.clock
|
||||
|
||||
import QtQuick.Controls as Controls
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
|
|
@ -46,10 +46,10 @@ Window {
|
|||
property QtObject notificationSettings
|
||||
property QtObject popupNotificationsModel
|
||||
property QtObject tasksModel
|
||||
property QtObject timeSource
|
||||
property Clock clockSource
|
||||
property bool inhibited
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
readonly property color backgroundColor: Qt.darker(Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.95), 1.05)
|
||||
|
|
@ -68,13 +68,27 @@ Window {
|
|||
|
||||
Component.onCompleted: ShellUtil.setInputTransparent(notificationPopupManager, true)
|
||||
|
||||
Binding {
|
||||
target: MobileShellState.ShellDBusClient
|
||||
property: "isNotificationPopupDrawerOpen"
|
||||
value: popupDrawerOpened
|
||||
}
|
||||
|
||||
// hide on timeout to give time to finish animations
|
||||
Timer {
|
||||
id: hideTimeout
|
||||
interval: Kirigami.Units.veryLongDuration * 1.5
|
||||
repeat: false
|
||||
onTriggered: if (notifications.count == 0) notificationPopupManager.visible = false;
|
||||
}
|
||||
|
||||
// Update the window touch region to encapsulate the notification area or the whole screen depending on the 'popupDrawerOpened' state
|
||||
function updateTouchArea() {
|
||||
ShellUtil.setInputTransparent(notificationPopupManager, false);
|
||||
if (popupDrawerOpened) {
|
||||
ShellUtil.setInputRegion(notificationPopupManager, Qt.rect(0, 0, 0, 0));
|
||||
} else {
|
||||
// get the height of the popup directly to ensure we get the lastest version
|
||||
// get the height of the popup directly to ensure we get the latest version
|
||||
let popupHeight = Kirigami.Units.gridUnit * 6;
|
||||
let currentPopup = notifications.objectAt(notifications.currentPopupIndex);
|
||||
if (currentPopup) {
|
||||
|
|
@ -160,7 +174,7 @@ Window {
|
|||
onCountChanged: {
|
||||
if (count == 0) {
|
||||
ShellUtil.setInputTransparent(notificationPopupManager, true);
|
||||
notificationPopupManager.visible = false;
|
||||
hideTimeout.restart();
|
||||
notificationPopupManager.popupDrawerOpened = false;
|
||||
fullHeight = 0;
|
||||
return;
|
||||
|
|
@ -195,7 +209,7 @@ Window {
|
|||
popupModel: model
|
||||
notificationsModel: popupNotificationsModel
|
||||
notificationsModelType: notificationModelType
|
||||
timeDataSource: timeSource
|
||||
timeDataSource: clockSource
|
||||
|
||||
timeout: model.timeout
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ import org.kde.plasma.private.mobileshell.state as MobileShellState
|
|||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
|
||||
import org.kde.notificationmanager as NotificationManager
|
||||
import org.kde.plasma.plasma5support 2.0 as P5Support
|
||||
import org.kde.plasma.clock
|
||||
|
||||
import org.kde.taskmanager 0.1 as TaskManager
|
||||
|
||||
|
|
@ -62,10 +62,15 @@ QtObject {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO use pulseaudio-qt for this once it becomes a framework
|
||||
property QtObject __pulseAudio: Loader {
|
||||
source: "PulseAudio.qml"
|
||||
}
|
||||
|
||||
property bool inhibited: false
|
||||
|
||||
onInhibitedChanged: {
|
||||
var pa = pulseAudio.item;
|
||||
var pa = __pulseAudio.item;
|
||||
if (!pa) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -121,13 +126,9 @@ QtObject {
|
|||
groupInline: false
|
||||
}
|
||||
|
||||
|
||||
property QtObject timeSource: P5Support.DataSource {
|
||||
engine: "time"
|
||||
connectedSources: ["Local"]
|
||||
interval: 60000 // 1 min
|
||||
intervalAlignment: P5Support.Types.AlignToMinute
|
||||
onDataChanged: {
|
||||
property Clock clockSource: Clock {
|
||||
id: clockSource
|
||||
onTimeChanged: {
|
||||
checkInhibition();
|
||||
npm.timeChanged();
|
||||
}
|
||||
|
|
@ -137,7 +138,7 @@ QtObject {
|
|||
notificationModelType: notificationProvider.notificationModelType
|
||||
notificationSettings: notificationProvider.notificationSettings
|
||||
popupNotificationsModel: notificationProvider.popupNotificationsModel
|
||||
timeSource: notificationProvider.timeSource
|
||||
clockSource: notificationProvider.clockSource
|
||||
inhibited: notificationProvider.inhibited
|
||||
tasksModel: notificationProvider.tasksModel
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2017, 2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
import QtQuick 2.2
|
||||
|
||||
import org.kde.plasma.private.volume 0.1
|
||||
|
||||
QtObject {
|
||||
id: pulseAudio
|
||||
|
||||
readonly property string notificationStreamId: "sink-input-by-media-role:event"
|
||||
|
||||
property QtObject notificationStream
|
||||
|
||||
property QtObject instantiator: Instantiator {
|
||||
model: StreamRestoreModel {}
|
||||
|
||||
delegate: QtObject {
|
||||
readonly property string name: Name
|
||||
readonly property bool muted: Muted
|
||||
|
||||
function mute() {
|
||||
Muted = true
|
||||
}
|
||||
function unmute() {
|
||||
Muted = false
|
||||
}
|
||||
}
|
||||
|
||||
onObjectAdded: (index, object) => {
|
||||
if (object.name === notificationStreamId) {
|
||||
notificationStream = object;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,53 +27,68 @@ ColumnLayout {
|
|||
|
||||
property real scale: 1.0
|
||||
|
||||
PulseObjectFilterModel {
|
||||
id: paSinkFilterModel
|
||||
sortRoleName: "SortByDefault"
|
||||
sortOrder: Qt.DescendingOrder
|
||||
filterOutInactiveDevices: true
|
||||
sourceModel: MobileShell.AudioInfo.paSinkModel
|
||||
}
|
||||
// Input devices
|
||||
readonly property SourceModel paSourceModel: SourceModel { id: paSourceModel }
|
||||
|
||||
SourceModel {
|
||||
id: paSourceModel
|
||||
}
|
||||
// Output devices
|
||||
readonly property SinkModel paSinkModel: SinkModel { id: paSinkModel }
|
||||
|
||||
PulseObjectFilterModel {
|
||||
// Confusingly, Sink Input is what PulseAudio calls streams that send audio to an output device
|
||||
readonly property SinkInputModel paSinkInputModel: SinkInputModel { id: paSinkInputModel }
|
||||
|
||||
// Confusingly, Source Output is what PulseAudio calls streams that take audio from an input device
|
||||
readonly property SourceOutputModel paSourceOutputModel: SourceOutputModel { id: paSourceOutputModel }
|
||||
|
||||
// Active input devices
|
||||
readonly property PulseObjectFilterModel paSourceFilterModel: PulseObjectFilterModel {
|
||||
id: paSourceFilterModel
|
||||
sortRoleName: "SortByDefault"
|
||||
sortOrder: Qt.DescendingOrder
|
||||
filterOutInactiveDevices: true
|
||||
filterVirtualDevices: true
|
||||
sourceModel: paSourceModel
|
||||
}
|
||||
|
||||
CardModel {
|
||||
id: paCardModel
|
||||
// Active output devices
|
||||
readonly property PulseObjectFilterModel paSinkFilterModel: PulseObjectFilterModel {
|
||||
id: paSinkFilterModel
|
||||
filterOutInactiveDevices: true
|
||||
filterVirtualDevices: true
|
||||
sourceModel: paSinkModel
|
||||
}
|
||||
|
||||
// ui elements
|
||||
// non-virtual streams going to output devices
|
||||
readonly property PulseObjectFilterModel paSinkInputFilterModel: PulseObjectFilterModel {
|
||||
id: paSinkInputFilterModel
|
||||
filters: [
|
||||
{ role: "VirtualStream", value: false },
|
||||
{ role: "Client", value: (client) => client.name !== "libcanberra" },
|
||||
]
|
||||
sourceModel: paSinkInputModel
|
||||
}
|
||||
|
||||
// non-virtual streams coming from input devices
|
||||
readonly property PulseObjectFilterModel paSourceOutputFilterModel: PulseObjectFilterModel {
|
||||
id: paSourceOutputFilterModel
|
||||
filters: [ { role: "VirtualStream", value: false } ]
|
||||
sourceModel: paSourceOutputModel
|
||||
}
|
||||
|
||||
readonly property CardModel paCardModel: CardModel { id: paCardModel }
|
||||
|
||||
// UI elements
|
||||
|
||||
PopupCard {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: Kirigami.Units.gridUnit
|
||||
|
||||
transform: Scale {
|
||||
origin.x: Math.round(implicitWidth / 2)
|
||||
origin.y: Math.round(height / 2)
|
||||
xScale: audioApplet.scale
|
||||
yScale: audioApplet.scale
|
||||
}
|
||||
scaleFactor: audioApplet.scale
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
anchors.rightMargin: Kirigami.Units.smallSpacing
|
||||
anchors.leftMargin: Kirigami.Units.smallSpacing
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Heading {
|
||||
level: 2
|
||||
text: i18n("Outputs")
|
||||
text: i18n("Output Devices")
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.leftMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
Repeater {
|
||||
|
|
@ -83,9 +98,8 @@ ColumnLayout {
|
|||
model: paSinkFilterModel
|
||||
delegate: DeviceListItem {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.smallSpacing
|
||||
type: "sink"
|
||||
onlyone: sinkView.count === 1
|
||||
onlyOne: sinkView.count === 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -94,24 +108,16 @@ ColumnLayout {
|
|||
PopupCard {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: Kirigami.Units.gridUnit
|
||||
|
||||
transform: Scale {
|
||||
origin.x: Math.round(implicitWidth / 2)
|
||||
origin.y: Math.round(height / 2)
|
||||
xScale: audioApplet.scale
|
||||
yScale: audioApplet.scale
|
||||
}
|
||||
scaleFactor: audioApplet.scale
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
anchors.rightMargin: Kirigami.Units.smallSpacing
|
||||
anchors.leftMargin: Kirigami.Units.smallSpacing
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Heading {
|
||||
level: 2
|
||||
text: i18n("Inputs")
|
||||
text: i18n("Input Devices")
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.leftMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
Repeater {
|
||||
|
|
@ -121,38 +127,30 @@ ColumnLayout {
|
|||
model: paSourceFilterModel
|
||||
delegate: DeviceListItem {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.smallSpacing
|
||||
type: "source"
|
||||
onlyone: sinkView.count === 1
|
||||
onlyOne: sourceView.count === 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PopupCard {
|
||||
visible: sourceInputView.model.count + sourceMediaInputView.model.count !== 0
|
||||
visible: (sourceMediaInputView.count + sourceInputView.count) > 0
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: Kirigami.Units.gridUnit
|
||||
|
||||
transform: Scale {
|
||||
origin.x: Math.round(implicitWidth / 2)
|
||||
origin.y: Math.round(height / 2)
|
||||
xScale: audioApplet.scale
|
||||
yScale: audioApplet.scale
|
||||
}
|
||||
scaleFactor: audioApplet.scale
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
anchors.rightMargin: Kirigami.Units.smallSpacing
|
||||
anchors.leftMargin: Kirigami.Units.smallSpacing
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Heading {
|
||||
level: 2
|
||||
text: i18n("Playback Streams")
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.leftMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
// "Grouped" media sources (ex. Notifications)
|
||||
Repeater {
|
||||
id: sourceMediaInputView
|
||||
Layout.fillWidth: true
|
||||
|
|
@ -166,25 +164,22 @@ ColumnLayout {
|
|||
Layout.margins: Kirigami.Units.smallSpacing
|
||||
width: sourceOutputView.width
|
||||
type: "sink-input"
|
||||
devicesModel: sourceView.model
|
||||
devicesModel: paSinkFilterModel
|
||||
}
|
||||
}
|
||||
|
||||
// Regular playback streams
|
||||
Repeater {
|
||||
id: sourceInputView
|
||||
Layout.fillWidth: true
|
||||
|
||||
model: PulseObjectFilterModel {
|
||||
filters: [ { role: "VirtualStream", value: false } ]
|
||||
sourceModel: SinkInputModel {}
|
||||
}
|
||||
model: paSinkInputFilterModel
|
||||
|
||||
delegate: StreamListItem {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.smallSpacing
|
||||
width: sourceOutputView.width
|
||||
type: "sink-input"
|
||||
devicesModel: sourceView.model
|
||||
devicesModel: paSinkFilterModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -194,40 +189,29 @@ ColumnLayout {
|
|||
visible: sourceOutputView.model.count !== 0
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: Kirigami.Units.gridUnit
|
||||
|
||||
transform: Scale {
|
||||
origin.x: Math.round(implicitWidth / 2)
|
||||
origin.y: Math.round(height / 2)
|
||||
xScale: audioApplet.scale
|
||||
yScale: audioApplet.scale
|
||||
}
|
||||
scaleFactor: audioApplet.scale
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
anchors.rightMargin: Kirigami.Units.smallSpacing
|
||||
anchors.leftMargin: Kirigami.Units.smallSpacing
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Heading {
|
||||
level: 2
|
||||
text: i18n("Recording Streams")
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.leftMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: sourceOutputView
|
||||
Layout.fillWidth: true
|
||||
|
||||
model: PulseObjectFilterModel {
|
||||
filters: [ { role: "VirtualStream", value: false } ]
|
||||
sourceModel: SourceOutputModel {}
|
||||
}
|
||||
model: paSourceOutputFilterModel
|
||||
|
||||
delegate: StreamListItem {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.smallSpacing
|
||||
width: sourceOutputView.width
|
||||
type: "source-output"
|
||||
devicesModel: sourceView.model
|
||||
devicesModel: paSourceFilterModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,25 +11,24 @@ import org.kde.plasma.private.volume 0.1
|
|||
|
||||
// adapted from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/DeviceListItem.qml
|
||||
ListItemBase {
|
||||
readonly property var currentPort: Ports[ActivePortIndex]
|
||||
readonly property var currentActivePortIndex: ActivePortIndex
|
||||
readonly property var currentMuted: Muted
|
||||
readonly property var activePortIndex: ActivePortIndex
|
||||
property bool onlyone: false
|
||||
readonly property var currentPort: model.Ports[model.ActivePortIndex]
|
||||
readonly property var currentActivePortIndex: model.ActivePortIndex
|
||||
readonly property var currentMuted: model.Muted
|
||||
readonly property var activePortIndex: model.ActivePortIndex
|
||||
|
||||
label: {
|
||||
if (currentPort && currentPort.description) {
|
||||
if (onlyone || !Description) {
|
||||
if (onlyOne || !model.Description) {
|
||||
return currentPort.description;
|
||||
} else {
|
||||
return i18nc("label of device items", "%1 (%2)", currentPort.description, Description);
|
||||
return i18nc("label of device items", "%1 (%2)", currentPort.description, model.Description);
|
||||
}
|
||||
}
|
||||
if (Description) {
|
||||
return Description;
|
||||
if (model.Description) {
|
||||
return model.Description;
|
||||
}
|
||||
if (Name) {
|
||||
return Name;
|
||||
if (model.Name) {
|
||||
return model.Name;
|
||||
}
|
||||
return i18n("Device name not found");
|
||||
}
|
||||
|
|
@ -6,52 +6,55 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick
|
||||
import QtQuick.Controls as Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
import org.kde.ksvg 1.0 as KSvg
|
||||
import org.kde.kquickcontrolsaddons 2.0
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.ksvg as KSvg
|
||||
import org.kde.kquickcontrolsaddons
|
||||
import org.kde.plasma.core as PlasmaCore
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.private.volume 0.1
|
||||
import org.kde.plasma.components as PlasmaComponents
|
||||
import org.kde.plasma.extras as PlasmaExtras
|
||||
import org.kde.plasma.private.volume
|
||||
|
||||
import "icon.js" as Icon
|
||||
|
||||
// adapted from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/ListItemBase.qml
|
||||
Controls.Control {
|
||||
Controls.AbstractButton {
|
||||
id: baseItem
|
||||
|
||||
property string label
|
||||
property alias listIcon: clientIcon.source
|
||||
property string type // sink, source, source-output
|
||||
|
||||
MouseArea {
|
||||
id: clickArea
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
onClicked: {
|
||||
if (selectButton.visible) {
|
||||
model.PulseObject.default = true;
|
||||
}
|
||||
}
|
||||
property bool onlyOne: false
|
||||
|
||||
// Whether this item is selected
|
||||
readonly property bool supportsSelection: (baseItem.type == "sink" || baseItem.type == "source")
|
||||
readonly property bool selected: supportsSelection && (model.PulseObject.hasOwnProperty("default") ? model.PulseObject.default : false)
|
||||
|
||||
onClicked: {
|
||||
// Set as the default audio device
|
||||
model.PulseObject.default = true
|
||||
}
|
||||
|
||||
topPadding: Kirigami.Units.mediumSpacing
|
||||
bottomPadding: Kirigami.Units.mediumSpacing
|
||||
leftPadding: Kirigami.Units.mediumSpacing
|
||||
rightPadding: Kirigami.Units.mediumSpacing
|
||||
|
||||
background: Rectangle {
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: (baseItem.down)
|
||||
? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.3)
|
||||
: 'transparent'
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
id: row
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
PlasmaComponents.RadioButton {
|
||||
id: selectButton
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.topMargin: Math.round(row.height / 2 - implicitHeight - Kirigami.Units.smallSpacing / 2) // align with text
|
||||
checked: model.PulseObject.hasOwnProperty("default") ? model.PulseObject.default : false
|
||||
visible: (baseItem.type == "sink" && sinkView.model.count > 1) || (baseItem.type == "source" && sourceView.model.count > 1)
|
||||
onClicked: model.PulseObject.default = true
|
||||
}
|
||||
|
||||
// application icon
|
||||
Kirigami.Icon {
|
||||
id: clientIcon
|
||||
|
|
@ -68,6 +71,24 @@ Controls.Control {
|
|||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
Layout.maximumWidth: Infinity // Ignore maximum width of children
|
||||
visible: (baseItem.type === "sink" || baseItem.type === "source") && !baseItem.onlyOne
|
||||
|
||||
PlasmaComponents.RadioButton {
|
||||
id: defaultButton
|
||||
Accessible.ignored: true // read out from delegate
|
||||
activeFocusOnTab: false // toggle from delegate
|
||||
checked: model.PulseObject?.default ?? false
|
||||
onToggled: {
|
||||
if (checked) {
|
||||
baseItem.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
|
@ -87,9 +108,10 @@ Controls.Control {
|
|||
}
|
||||
|
||||
PlasmaComponents.ToolButton {
|
||||
id: viewButton
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
Layout.bottomMargin: -Kirigami.Units.smallSpacing
|
||||
icon.name: "application-menu"
|
||||
icon.name: "view-more-symbolic"
|
||||
checkable: true
|
||||
checked: contextMenu.visible && contextMenu.visualParent === this
|
||||
visible: contextMenu.hasContent
|
||||
|
|
@ -132,93 +154,26 @@ Controls.Control {
|
|||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
PlasmaComponents.ToolButton {
|
||||
icon.name: Icon.name(Volume / PulseAudio.NormalVolume * 100.0, Muted)
|
||||
text: Muted ? i18n("Unmute") : i18n("Mute")
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
onClicked: {
|
||||
Muted = !Muted
|
||||
}
|
||||
}
|
||||
|
||||
// this slider was effectively copied from the source (linked at the top of the file)
|
||||
PlasmaComponents.Slider {
|
||||
VolumeSlider {
|
||||
id: slider
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
// Helper properties to allow async slider updates.
|
||||
// While we are sliding we must not react to value updates
|
||||
// as otherwise we can easily end up in a loop where value
|
||||
// changes trigger volume changes trigger value changes.
|
||||
property int volume: Volume
|
||||
property bool ignoreValueChange: true
|
||||
readonly property bool forceRaiseMaxVolume: volume >= PulseAudio.NormalVolume * 1.01
|
||||
|
||||
from: PulseAudio.MinimalVolume
|
||||
to: PulseAudio.NormalVolume
|
||||
stepSize: to / (to / PulseAudio.NormalVolume * 100.0)
|
||||
visible: HasVolume
|
||||
enabled: VolumeWritable
|
||||
opacity: Muted ? 0.5 : 1
|
||||
to: model.Volume >= PulseAudio.NormalVolume * 1.01 ? PulseAudio.MaximalVolume : PulseAudio.NormalVolume
|
||||
stepSize: PulseAudio.NormalVolume / 100.0
|
||||
property real myStepSize: PulseAudio.NormalVolume / 100.0
|
||||
visible: model.HasVolume !== false // Devices always have volume but Streams don't necessarily
|
||||
enabled: model.VolumeWritable
|
||||
muted: model.Muted
|
||||
volumeObject: model.PulseObject
|
||||
activeFocusOnTab: false // access from delegate
|
||||
|
||||
Accessible.name: i18nc("Accessibility data on volume slider", "Adjust volume for %1", baseItem.label)
|
||||
|
||||
background: KSvg.FrameSvgItem {
|
||||
imagePath: "widgets/slider"
|
||||
prefix: "groove"
|
||||
width: parent.availableWidth
|
||||
height: margins.top + margins.bottom
|
||||
anchors.centerIn: parent
|
||||
scale: parent.mirrored ? -1 : 1
|
||||
|
||||
KSvg.FrameSvgItem {
|
||||
imagePath: "widgets/slider"
|
||||
prefix: "groove-highlight"
|
||||
anchors.left: parent.left
|
||||
y: (parent.height - height) / 2
|
||||
width: Math.max(margins.left + margins.right, slider.handle.x * meter.volume)
|
||||
height: Math.max(margins.top + margins.bottom, parent.height)
|
||||
opacity: meter.available && (meter.volume > 0 || animation.running)
|
||||
VolumeMonitor {
|
||||
id: meter
|
||||
target: parent.visible ? model.PulseObject : null
|
||||
}
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
id: animation
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
value: to, model.Volume
|
||||
onMoved: {
|
||||
model.Volume = value;
|
||||
model.Muted = value === 0;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
ignoreValueChange = false;
|
||||
}
|
||||
|
||||
onVolumeChanged: {
|
||||
var oldIgnoreValueChange = ignoreValueChange;
|
||||
ignoreValueChange = true;
|
||||
value = Volume;
|
||||
ignoreValueChange = oldIgnoreValueChange;
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
if (!ignoreValueChange) {
|
||||
Volume = value;
|
||||
Muted = value == 0;
|
||||
|
||||
if (!pressed) {
|
||||
updateTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPressedChanged: {
|
||||
if (!pressed) {
|
||||
// Make sure to sync the volume once the button was
|
||||
|
|
@ -226,14 +181,28 @@ Controls.Control {
|
|||
// Otherwise it might be that the slider is at v10
|
||||
// whereas PA rejected the volume change and is
|
||||
// still at v15 (e.g.).
|
||||
updateTimer.restart();
|
||||
value = Qt.binding(() => model.Volume);
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: 200
|
||||
onTriggered: slider.value = Volume
|
||||
function updateVolume() {
|
||||
if (model.Volume > PulseAudio.NormalVolume) {
|
||||
model.Volume = PulseAudio.NormalVolume;
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: seqAnimation
|
||||
NumberAnimation {
|
||||
id: toAnimation
|
||||
target: slider
|
||||
property: "to"
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
ScriptAction {
|
||||
script: slider.updateVolume()
|
||||
}
|
||||
}
|
||||
}
|
||||
PlasmaComponents.Label {
|
||||
|
|
@ -263,8 +232,4 @@ Controls.Control {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setVolumeByPercent(targetPercent) {
|
||||
model.PulseObject.volume = Math.round(PulseAudio.NormalVolume * (targetPercent/100));
|
||||
}
|
||||
}
|
||||
43
components/mobileshell/qml/popups/volumeosd/PopupCard.qml
Normal file
43
components/mobileshell/qml/popups/volumeosd/PopupCard.qml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2024 Micah Stanley <stanleymicah@proton.me>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
|
||||
import QtQuick.Effects
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
import org.kde.ksvg 1.0 as KSvg
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
|
||||
Controls.Control {
|
||||
id: content
|
||||
|
||||
property real scaleFactor: 1.0
|
||||
|
||||
implicitWidth: Math.min(Kirigami.Units.gridUnit * 20, Screen.width - Kirigami.Units.gridUnit * 2)
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
|
||||
transform: Scale {
|
||||
origin.x: Math.round(implicitWidth / 2)
|
||||
origin.y: Math.round(height / 2)
|
||||
xScale: scaleFactor
|
||||
yScale: scaleFactor
|
||||
}
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
MobileShell.PanelBackground {
|
||||
anchors.fill: parent
|
||||
panelType: MobileShell.PanelBackground.PanelType.Popup
|
||||
}
|
||||
}
|
||||
|
|
@ -23,17 +23,16 @@ Window {
|
|||
id: window
|
||||
|
||||
width: osd.width + 6
|
||||
height: cards.implicitHeight + 6
|
||||
height: cards.implicitHeight + 6 + cards.openOffset
|
||||
|
||||
onWidthChanged: if (visible) window.updateTouchRegion()
|
||||
|
||||
visible: false
|
||||
|
||||
readonly property real offsetMargins: Math.max(cards.offset, 0)
|
||||
|
||||
LayerShell.Window.scope: "overlay"
|
||||
LayerShell.Window.anchors: LayerShell.Window.AnchorTop
|
||||
LayerShell.Window.layer: LayerShell.Window.LayerOverlay
|
||||
LayerShell.Window.exclusionZone: -1
|
||||
LayerShell.Window.margins.top: offsetMargins
|
||||
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
|
|
@ -45,16 +44,17 @@ Window {
|
|||
if (cards.state == "closed") {
|
||||
hideTimer.stop();
|
||||
window.open();
|
||||
} else if (!volumeSlider.isPressed) {
|
||||
} else if (!volumeSlider.pressed) {
|
||||
hideTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
function open() {
|
||||
// set window input transparency to allow touches to pass through while the opening animation is playing
|
||||
ShellUtil.setInputTransparent(window, true);
|
||||
|
||||
window.visible = true;
|
||||
cards.state = "open";
|
||||
// set window input transparency to accept touches
|
||||
ShellUtil.setInputTransparent(window, false);
|
||||
}
|
||||
|
||||
function close() {
|
||||
|
|
@ -63,12 +63,16 @@ Window {
|
|||
ShellUtil.setInputTransparent(window, true);
|
||||
}
|
||||
|
||||
function updateTouchRegion() {
|
||||
ShellUtil.setInputRegion(window, Qt.rect(0, cards.openOffset, window.width, cards.implicitHeight + 6));
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 2000
|
||||
running: false
|
||||
onTriggered: {
|
||||
window.close();
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,6 +83,10 @@ Window {
|
|||
|
||||
ColumnLayout {
|
||||
id: cards
|
||||
|
||||
// Ensure children get visibility state of window so that they don't update while closed
|
||||
visible: window.visible
|
||||
|
||||
width: parent.width
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
|
@ -87,7 +95,6 @@ Window {
|
|||
readonly property real closedOffset: -(cards.implicitHeight + Kirigami.Units.smallSpacing)
|
||||
readonly property real openOffset: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 3
|
||||
property real offset: closedOffset
|
||||
property real scale: 0.95
|
||||
|
||||
state: "closed"
|
||||
|
||||
|
|
@ -97,18 +104,12 @@ Window {
|
|||
PropertyChanges {
|
||||
target: cards; offset: openOffset
|
||||
}
|
||||
PropertyChanges {
|
||||
target: cards; scale: 1.0
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "closed"
|
||||
PropertyChanges {
|
||||
target: cards; offset: closedOffset
|
||||
}
|
||||
PropertyChanges {
|
||||
target: cards; scale: 0.95
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
|
@ -116,16 +117,16 @@ Window {
|
|||
SequentialAnimation {
|
||||
ParallelAnimation {
|
||||
PropertyAnimation {
|
||||
properties: "offset"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.25
|
||||
}
|
||||
PropertyAnimation {
|
||||
properties: "scale"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.25
|
||||
properties: "offset"; easing.type: cards.state == "open" ? Easing.OutQuint : Easing.InQuint; duration: Kirigami.Units.veryLongDuration * 1.25
|
||||
}
|
||||
}
|
||||
ScriptAction {
|
||||
script: {
|
||||
if (cards.state == "open") {
|
||||
hideTimer.restart();
|
||||
// set window input transparency to accept touches
|
||||
ShellUtil.setInputTransparent(window, false);
|
||||
window.updateTouchRegion();
|
||||
} else {
|
||||
hideTimer.stop();
|
||||
window.visible = false;
|
||||
|
|
@ -142,13 +143,7 @@ Window {
|
|||
|
||||
transform: [
|
||||
Translate {
|
||||
y: cards.offset - window.offsetMargins + 1
|
||||
},
|
||||
Scale {
|
||||
origin.x: Math.round(width / 2)
|
||||
origin.y: Math.round(height / 2)
|
||||
xScale: cards.scale
|
||||
yScale: cards.scale
|
||||
y: cards.offset + 1
|
||||
}
|
||||
]
|
||||
|
||||
|
|
@ -162,81 +157,41 @@ Window {
|
|||
property int volumePercent: PreferredDevice.sink.volume / PulseAudio.NormalVolume * 100.0
|
||||
|
||||
PlasmaComponents.ToolButton {
|
||||
icon.name: !PreferredDevice.sink || PreferredDevice.sink.muted ? "audio-volume-muted" : MobileShell.AudioInfo.icon
|
||||
text: !PreferredDevice.sink || PreferredDevice.sink.muted ? i18n("Unmute") : i18n("Mute")
|
||||
icon.name: !PreferredDevice.sink || (PreferredDevice.sink.muted ? "audio-volume-muted" : MobileShell.AudioInfo.icon)
|
||||
text: !PreferredDevice.sink || (PreferredDevice.sink.muted ? i18n("Unmute") : i18n("Mute"))
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||
|
||||
onClicked: {
|
||||
hideTimer.restart();
|
||||
PreferredDevice.sink.muted = !PreferredDevice.sink.muted;
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaComponents.Slider {
|
||||
VolumeSlider {
|
||||
id: volumeSlider
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.rightMargin: Kirigami.Units.smallSpacing * 2
|
||||
|
||||
property real volume: PreferredDevice.sink.volume
|
||||
property bool muted: PreferredDevice.sink.muted
|
||||
property bool ignoreValueChange: false
|
||||
property bool isPressed: false
|
||||
|
||||
from: PulseAudio.MinimalVolume
|
||||
to: PulseAudio.NormalVolume
|
||||
stepSize: to / (to / PulseAudio.NormalVolume * 100.0)
|
||||
opacity: muted ? 0.5 : 1.0
|
||||
|
||||
Component.onCompleted: {
|
||||
ignoreValueChange = false;
|
||||
volumeObject: PreferredDevice.sink
|
||||
muted: PreferredDevice.sink.muted
|
||||
value: PreferredDevice.sink.volume
|
||||
|
||||
onMoved: {
|
||||
PreferredDevice.sink.volume = value;
|
||||
PreferredDevice.sink.muted = value === 0;
|
||||
}
|
||||
|
||||
onVolumeChanged: {
|
||||
if (!window.visible) {
|
||||
return;
|
||||
}
|
||||
var oldIgnoreValueChange = ignoreValueChange;
|
||||
ignoreValueChange = true;
|
||||
value = muted ? 0 : PreferredDevice.sink.volume;
|
||||
ignoreValueChange = oldIgnoreValueChange;
|
||||
if (volumeSlider.isPressed) {
|
||||
return;
|
||||
}
|
||||
window.open();
|
||||
hideTimer.restart();
|
||||
}
|
||||
|
||||
onMutedChanged: {
|
||||
var oldIgnoreValueChange = ignoreValueChange;
|
||||
ignoreValueChange = true;
|
||||
value = muted ? 0 : PreferredDevice.sink.volume;
|
||||
ignoreValueChange = oldIgnoreValueChange;
|
||||
if (!window.visible || volumeSlider.isPressed) {
|
||||
return;
|
||||
}
|
||||
window.open();
|
||||
hideTimer.restart();
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
if (!ignoreValueChange) {
|
||||
PreferredDevice.sink.muted = false;
|
||||
PreferredDevice.sink.volume = value;
|
||||
if (!volumeSlider.isPressed) {
|
||||
updateTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPressedChanged: {
|
||||
volumeSlider.isPressed = pressed;
|
||||
if (pressed) {
|
||||
window.open();
|
||||
hideTimer.stop();
|
||||
} else {
|
||||
// Make sure to sync the volume once the button was
|
||||
|
|
@ -244,16 +199,10 @@ Window {
|
|||
// Otherwise it might be that the slider is at v10
|
||||
// whereas PA rejected the volume change and is
|
||||
// still at v15 (e.g.).
|
||||
value = Qt.binding(() => PreferredDevice.sink.volume);
|
||||
hideTimer.restart();
|
||||
updateTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: 200
|
||||
onTriggered: volumeSlider.value = PreferredDevice.sink.volume
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaComponents.ToolButton {
|
||||
|
|
@ -31,12 +31,12 @@ Window {
|
|||
|
||||
LayerShell.Window.scope: "overlay"
|
||||
LayerShell.Window.anchors: LayerShell.Window.AnchorTop
|
||||
LayerShell.Window.layer: LayerShell.Window.LayerTop
|
||||
LayerShell.Window.layer: LayerShell.Window.LayerOverlay
|
||||
LayerShell.Window.exclusionZone: -1
|
||||
|
||||
readonly property color backgroundColor: Qt.darker(Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.95), 1.05)
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
color: backgroundColor
|
||||
|
|
@ -65,12 +65,21 @@ Window {
|
|||
visible = false;
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: MobileShellState.ShellDBusClient
|
||||
property: "isVolumeOSDOpen"
|
||||
value: window.visible
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: flickable
|
||||
anchors.fill: parent
|
||||
contentHeight: cards.implicitHeight
|
||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
||||
|
||||
// Ensure children get visibility state of window so that they don't update while closed
|
||||
visible: window.visible
|
||||
|
||||
pressDelay: 50
|
||||
|
||||
property real offset: -Kirigami.Units.gridUnit
|
||||
|
|
@ -115,16 +124,16 @@ Window {
|
|||
SequentialAnimation {
|
||||
ParallelAnimation {
|
||||
PropertyAnimation {
|
||||
properties: "offset"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.25
|
||||
properties: "offset"; easing.type: Easing.OutQuint; duration: Kirigami.Units.veryLongDuration * 1.25
|
||||
}
|
||||
PropertyAnimation {
|
||||
properties: "scale"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.25
|
||||
properties: "scale"; easing.type: Easing.OutQuint; duration: Kirigami.Units.veryLongDuration * 1.25
|
||||
}
|
||||
PropertyAnimation {
|
||||
properties: "opacity"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.25
|
||||
properties: "opacity"; easing.type: Easing.OutQuint; duration: Kirigami.Units.veryLongDuration * 1.25
|
||||
}
|
||||
PropertyAnimation {
|
||||
properties: "color"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.25
|
||||
properties: "color"; easing.type: Easing.OutQuint; duration: Kirigami.Units.veryLongDuration * 1.25
|
||||
}
|
||||
}
|
||||
ScriptAction {
|
||||
|
|
@ -171,44 +180,32 @@ Window {
|
|||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: Kirigami.Units.gridUnit
|
||||
|
||||
transform: Scale {
|
||||
origin.x: Math.round(implicitWidth / 2)
|
||||
origin.y: Math.round(height / 2)
|
||||
xScale: flickable.scale
|
||||
yScale: flickable.scale
|
||||
}
|
||||
scaleFactor: flickable.scale
|
||||
|
||||
contentItem: RowLayout {
|
||||
contentItem: PlasmaComponents.ToolButton {
|
||||
id: audioSettingsButton
|
||||
|
||||
PlasmaComponents.ToolButton {
|
||||
property int addedPadding: Kirigami.Units.smallSpacing * 2
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
Layout.preferredWidth: parent.width - addedPadding * 2
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||
Layout.margins: addedPadding
|
||||
|
||||
contentItem: Item {
|
||||
anchors.fill: parent
|
||||
RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
anchors.centerIn: parent
|
||||
Kirigami.Icon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
source: "settings-configure"
|
||||
}
|
||||
PlasmaComponents.Label {
|
||||
text: i18n("Open audio settings")
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
contentItem: Item {
|
||||
anchors.fill: parent
|
||||
RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
anchors.centerIn: parent
|
||||
Kirigami.Icon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
source: "settings-configure"
|
||||
}
|
||||
PlasmaComponents.Label {
|
||||
text: i18n("Open audio settings")
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_pulseaudio");
|
||||
window.close();
|
||||
}
|
||||
onClicked: {
|
||||
MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_pulseaudio");
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,14 +19,14 @@ import org.kde.plasma.private.mobileshell as MobileShell
|
|||
QtObject {
|
||||
id: component
|
||||
|
||||
function showVolumeOverlay() {
|
||||
if (!osd.visible) {
|
||||
vcp.showOverlay();
|
||||
}
|
||||
}
|
||||
property var osdListener: Connections {
|
||||
target: MobileShellState.VolumeOSDListener
|
||||
|
||||
Component.onCompleted: {
|
||||
MobileShell.AudioInfo.volumeChanged.connect(showVolumeOverlay);
|
||||
function onShowOSD(icon, volume, maxVolume) {
|
||||
if (!osd.visible) {
|
||||
vcp.showOverlay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property var apiListener: Connections {
|
||||
131
components/mobileshell/qml/popups/volumeosd/VolumeSlider.qml
Normal file
131
components/mobileshell/qml/popups/volumeosd/VolumeSlider.qml
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
|
||||
SPDX-FileCopyrightText: 2019 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
SPDX-FileCopyrightText: 2022 ivan (@ratijas) tkachenko <me@ratijas.tk>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kquickcontrolsaddons
|
||||
import org.kde.plasma.components as PC3
|
||||
import org.kde.ksvg as KSvg
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.plasma.private.volume
|
||||
|
||||
// Audio volume slider. Value represents desired volume level in
|
||||
// device-specific units, while volume property reports current volume level
|
||||
// normalized to 0..1 range.
|
||||
PC3.Slider {
|
||||
id: control
|
||||
|
||||
property VolumeObject volumeObject
|
||||
|
||||
// When muted, the whole slider will appear slightly faded, but remain
|
||||
// functional and interactive.
|
||||
property bool muted: false
|
||||
|
||||
// Current (monitored) volume. To be animated. Do not update too fast
|
||||
// (i.e. faster or close to screen refresh rate), otherwise it won't
|
||||
// animate smoothly.
|
||||
property real volume: meter.volume
|
||||
|
||||
VolumeMonitor {
|
||||
id: meter
|
||||
target: control.visible ? control.volumeObject : null
|
||||
}
|
||||
|
||||
Behavior on volume {
|
||||
NumberAnimation {
|
||||
id: animate
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
// When a maximum volume limit is raised/lower, animate the change.
|
||||
Behavior on to {
|
||||
NumberAnimation {
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
opacity: muted ? 0.5 : 1
|
||||
// Prevents the groove from showing through the handle
|
||||
layer.enabled: opacity < 1
|
||||
|
||||
wheelEnabled: false
|
||||
// `wheelEnabled: true` doesn't work we can't both respect stepsize
|
||||
// on scroll and allow fine-tuning on drag.
|
||||
// So we have to implement the scroll handling ourselves. See
|
||||
// https://bugreports.qt.io/browse/QTBUG-93081
|
||||
WheelHandler {
|
||||
orientation: Qt.Vertical | Qt.Horizontal
|
||||
property int wheelDelta: 0
|
||||
acceptedButtons: Qt.NoButton
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onWheel: wheel => {
|
||||
const lastValue = control.value
|
||||
// We want a positive delta to increase the slider for up/right scrolling,
|
||||
// independently of the scrolling inversion setting
|
||||
// The x-axis is also inverted (scrolling right produce negative values)
|
||||
const delta = (wheel.angleDelta.y || -wheel.angleDelta.x) * (wheel.inverted ? -1 : 1)
|
||||
wheelDelta += delta;
|
||||
// magic number 120 for common "one click"
|
||||
// See: https://doc.qt.io/qt-6/qml-qtquick-wheelevent.html#angleDelta-prop
|
||||
while (wheelDelta >= 120) {
|
||||
wheelDelta -= 120;
|
||||
control.increase();
|
||||
}
|
||||
while (wheelDelta <= -120) {
|
||||
wheelDelta += 120;
|
||||
control.decrease();
|
||||
}
|
||||
if (lastValue !== control.value) {
|
||||
control.moved();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: KSvg.FrameSvgItem {
|
||||
imagePath: "widgets/slider"
|
||||
prefix: "groove"
|
||||
|
||||
implicitWidth: control.horizontal ? Kirigami.Units.gridUnit * 12 : fixedMargins.left + fixedMargins.right
|
||||
implicitHeight: control.vertical ? Kirigami.Units.gridUnit * 12 : fixedMargins.top + fixedMargins.bottom
|
||||
|
||||
width: control.horizontal ? Math.max(fixedMargins.left + fixedMargins.right, control.availableWidth) : implicitWidth
|
||||
height: control.vertical ? Math.max(fixedMargins.top + fixedMargins.bottom, control.availableHeight) : implicitHeight
|
||||
|
||||
x: control.leftPadding + (control.horizontal ? 0 : Math.round((control.availableWidth - width) / 2))
|
||||
y: control.topPadding + (control.vertical ? 0 : Math.round((control.availableHeight - height) / 2))
|
||||
|
||||
KSvg.FrameSvgItem {
|
||||
imagePath: "widgets/slider"
|
||||
prefix: "groove-highlight"
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: parent.bottom
|
||||
LayoutMirroring.enabled: control.mirrored
|
||||
|
||||
width: control.horizontal ? Math.max(fixedMargins.left + fixedMargins.right, Math.round(control.position * (control.availableWidth - control.handle.width / 2) + (control.handle.width / 2))) : parent.width
|
||||
height: control.vertical ? Math.max(fixedMargins.top + fixedMargins.bottom, Math.round(control.position * (control.availableHeight - control.handle.height / 2) + (control.handle.height / 2))) : parent.height
|
||||
}
|
||||
|
||||
KSvg.FrameSvgItem {
|
||||
imagePath: "widgets/slider"
|
||||
prefix: "groove-highlight"
|
||||
status: KSvg.FrameSvgItem.Selected
|
||||
visible: meter.available && control.volume > 0
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: parent.bottom
|
||||
LayoutMirroring.enabled: control.mirrored
|
||||
|
||||
width: control.horizontal ? Math.max(fixedMargins.left + fixedMargins.right, Math.round(control.volume * control.position * control.availableWidth)) : parent.width
|
||||
height: control.vertical ? Math.max(fixedMargins.top + fixedMargins.bottom, Math.round(control.volume * control.position * control.availableHeight)) : parent.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.plasma.plasma5support 2.0 as P5Support
|
||||
import org.kde.plasma.clock
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
|
|
@ -19,14 +19,14 @@ RowLayout {
|
|||
id: clockText
|
||||
|
||||
required property int fontPixelSize
|
||||
required property P5Support.DataSource source
|
||||
required property Clock clockSource
|
||||
|
||||
PlasmaComponents.Label {
|
||||
id: clock
|
||||
|
||||
property bool is24HourTime: MobileShell.ShellUtil.isSystem24HourFormat
|
||||
|
||||
text: Qt.formatTime(source.data.Local.DateTime, is24HourTime ? "h:mm" : "h:mm ap")
|
||||
text: Qt.formatTime(clockSource.dateTime, is24HourTime ? "h:mm" : "h:mm ap")
|
||||
color: Kirigami.Theme.textColor
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
font.pixelSize: fontPixelSize
|
||||
|
|
@ -36,7 +36,7 @@ RowLayout {
|
|||
id: date
|
||||
visible: ShellSettings.Settings.dateInStatusBar && !root.showSecondRow
|
||||
|
||||
text: Qt.formatDate(source.data.Local.DateTime, "ddd. MMMM d")
|
||||
text: Qt.formatDate(clockSource.dateTime, "ddd. MMMM d")
|
||||
color: Kirigami.Theme.textColor
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
font.pixelSize: fontPixelSize
|
||||
|
|
|
|||
|
|
@ -15,12 +15,13 @@ import QtQml.Models
|
|||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.plasma.core as PlasmaCore
|
||||
import org.kde.plasma.plasma5support 2.0 as P5Support
|
||||
import org.kde.plasma.clock
|
||||
import org.kde.plasma.private.systemtray as SystemTray
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.kitemmodels as KItemModels
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
|
@ -45,31 +46,17 @@ Item {
|
|||
*/
|
||||
property bool showTime: true
|
||||
|
||||
/**
|
||||
* Disables showing system tray indicators, preventing SIGABRT when used on the lockscreen.
|
||||
*/
|
||||
property bool disableSystemTray: false
|
||||
|
||||
property color colorScopeColor: Kirigami.Theme.backgroundColor
|
||||
|
||||
readonly property real textPixelSize: Math.round(11 * ShellSettings.Settings.statusBarScaleFactor)
|
||||
readonly property real smallerTextPixelSize: Math.round(9 * ShellSettings.Settings.statusBarScaleFactor)
|
||||
readonly property real elementSpacing: Math.round(Kirigami.Units.smallSpacing * 1.5)
|
||||
|
||||
P5Support.DataSource {
|
||||
id: timeSource
|
||||
engine: "time"
|
||||
connectedSources: ["Local"]
|
||||
interval: 1000
|
||||
intervalAlignment: P5Support.Types.AlignToMinute
|
||||
Clock {
|
||||
id: clockSource
|
||||
}
|
||||
|
||||
property alias statusNotifierSource: statusNotifierSourceLoader.item
|
||||
|
||||
Loader {
|
||||
id: statusNotifierSourceLoader
|
||||
active: !disableSystemTray
|
||||
sourceComponent: SystemTray.StatusNotifierModel { }
|
||||
MobileShellState.PanelSettingsDBusClient {
|
||||
id: panelSettings
|
||||
screenName: Screen.name
|
||||
}
|
||||
|
||||
// drop shadow for icons
|
||||
|
|
@ -89,8 +76,8 @@ Item {
|
|||
z: 1
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
rightPadding: Kirigami.Units.smallSpacing * 3
|
||||
leftPadding: Kirigami.Units.smallSpacing * 3
|
||||
rightPadding: Kirigami.Units.smallSpacing * 3 + panelSettings.statusBarLeftPadding
|
||||
leftPadding: Kirigami.Units.smallSpacing * 3 + panelSettings.statusBarRightPadding
|
||||
|
||||
anchors.fill: parent
|
||||
background: Rectangle {
|
||||
|
|
@ -103,9 +90,10 @@ Item {
|
|||
|
||||
RowLayout {
|
||||
id: mainRow
|
||||
readonly property real rowHeight: MobileShell.Constants.topPanelHeight - Kirigami.Units.smallSpacing * 2
|
||||
readonly property real rowHeight: MobileShell.Constants.defaultTopPanelHeight - Kirigami.Units.smallSpacing * 2
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredHeight: rowHeight
|
||||
|
||||
spacing: 0
|
||||
|
|
@ -115,7 +103,7 @@ Item {
|
|||
visible: root.showTime
|
||||
Layout.fillHeight: true
|
||||
fontPixelSize: textPixelSize
|
||||
source: timeSource
|
||||
clockSource: clockSource
|
||||
}
|
||||
|
||||
MobileShell.SignalStrengthIndicator {
|
||||
|
|
@ -130,16 +118,6 @@ Item {
|
|||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// system tray
|
||||
Repeater {
|
||||
id: statusNotifierRepeater
|
||||
model: root.statusNotifierSource
|
||||
|
||||
delegate: TaskWidget {
|
||||
Layout.leftMargin: root.elementSpacing
|
||||
}
|
||||
}
|
||||
|
||||
// system indicators
|
||||
// using Layout.fillHeight here seems to cause polish loops, instead just define the height of the row
|
||||
RowLayout {
|
||||
|
|
@ -185,7 +163,7 @@ Item {
|
|||
Layout.fillWidth: true
|
||||
|
||||
PlasmaComponents.Label {
|
||||
text: Qt.formatDate(timeSource.data.Local.DateTime, "ddd. MMMM d")
|
||||
text: Qt.formatDate(clockSource.dateTime, "ddd. MMMM d")
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
font.pixelSize: root.smallerTextPixelSize
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2011 Marco Martin <mart@kde.org>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
Item {
|
||||
id: taskIcon
|
||||
width: parent.height
|
||||
height: width
|
||||
//hide application status icons
|
||||
opacity: (Category != "ApplicationStatus" && Status != "Passive") ? 1 : 0
|
||||
onOpacityChanged: visible = opacity
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Icon {
|
||||
id: icon
|
||||
source: IconName ? IconName : Icon
|
||||
width: Math.min(parent.width, parent.height)
|
||||
height: width
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: taskIcon
|
||||
onClicked: {
|
||||
//print(iconSvg.hasElement(IconName))
|
||||
var service = statusNotifierSource.serviceForSource(DataEngineSource)
|
||||
var operation = service.operationDescription("Activate")
|
||||
operation.x = parent.x
|
||||
|
||||
// kmix shows main window instead of volume popup if (parent.x, parent.y) == (0, 0), which is the case here.
|
||||
// I am passing a position right below the panel (assuming panel is at screen's top).
|
||||
// Plasmoids' popups are already shown below the panel, so this make kmix's popup more consistent
|
||||
// to them.
|
||||
operation.y = parent.y + parent.height + 6
|
||||
service.startOperationCall(operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import org.kde.kirigami 2.20 as Kirigami
|
|||
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.workspace.components 2.0 as PW
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.private.battery // needed for charging state
|
||||
|
||||
|
|
@ -24,30 +25,31 @@ RowLayout {
|
|||
ListView {
|
||||
id: batteryRepeater
|
||||
|
||||
property int batteryWidth: 0
|
||||
|
||||
spacing: root.elementSpacing
|
||||
spacing: 0
|
||||
model: MobileShell.BatteryInfo.batteries
|
||||
orientation: ListView.Horizontal
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: (batteryRepeater.batteryWidth + root.elementSpacing) * batteryRepeater.count
|
||||
Layout.preferredWidth: contentItem.childrenRect.width
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: false
|
||||
|
||||
delegate: RowLayout {
|
||||
id: batteryBase
|
||||
|
||||
Layout.preferredWidth: batteryRepeater.batteryWidth
|
||||
width: (batteryBase.visible ? ((batteryLabel.visible ? batteryLabel.width : 0) + battery.width) + (ShellSettings.Settings.showBatteryPercentage ? root.elementSpacing : 0) : 0)
|
||||
Layout.fillHeight: false
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
height: batteryRepeater.height
|
||||
|
||||
visible: Type === "Battery" // only show the internal battery
|
||||
|
||||
PW.BatteryIcon {
|
||||
id: battery
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
height: batteryLabel.height
|
||||
Layout.fillHeight: true
|
||||
width: batteryLabel.height
|
||||
|
||||
hasBattery: PluggedIn
|
||||
|
|
@ -59,16 +61,12 @@ RowLayout {
|
|||
id: batteryLabel
|
||||
text: i18n("%1%", Percent)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillHeight: true
|
||||
|
||||
color: Kirigami.Theme.textColor
|
||||
visible: ShellSettings.Settings.showBatteryPercentage
|
||||
font.pixelSize: textPixelSize
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// ListView & RowLayout have problems with childrenRect size,
|
||||
// set it here so it propagates up nicely
|
||||
batteryRepeater.batteryWidth = batteryLabel.width + battery.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,26 +12,12 @@ import QtQuick.Controls as QQC2
|
|||
import org.kde.plasma.networkmanagement as PlasmaNM
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
Kirigami.Icon {
|
||||
Item {
|
||||
id: connectionIcon
|
||||
|
||||
// data
|
||||
|
||||
readonly property string icon: wirelessStatus.hotspotSSID.length !== 0 ? "network-wireless-hotspot" : connectionIconProvider.connectionIcon
|
||||
readonly property bool indicatorRunning: connectionIconProvider.connecting
|
||||
|
||||
readonly property var networkStatus: PlasmaNM.NetworkStatus {
|
||||
id: networkStatus
|
||||
}
|
||||
|
||||
readonly property var networkModel: PlasmaNM.NetworkModel {
|
||||
id: connectionModel
|
||||
}
|
||||
|
||||
readonly property var handler: PlasmaNM.Handler {
|
||||
id: handler
|
||||
}
|
||||
|
||||
readonly property var wirelessStatus: PlasmaNM.WirelessStatus {
|
||||
id: wirelessStatus
|
||||
}
|
||||
|
|
@ -40,9 +26,16 @@ Kirigami.Icon {
|
|||
id: connectionIconProvider
|
||||
}
|
||||
|
||||
// implementation
|
||||
source: icon
|
||||
// Internet icon, only show while visible
|
||||
Kirigami.Icon {
|
||||
id: internetIcon
|
||||
|
||||
anchors.fill: parent
|
||||
visible: !connectingIndicator.visible
|
||||
source: connectionIcon.icon
|
||||
}
|
||||
|
||||
// Connecting indicator
|
||||
QQC2.BusyIndicator {
|
||||
id: connectingIndicator
|
||||
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2024 Micah Stanley <stanleymicah@proton.me>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
|
||||
import QtQuick.Effects
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
import org.kde.ksvg 1.0 as KSvg
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
|
||||
// capture presses on the audio applet so it doesn't close the overlay
|
||||
Controls.Control {
|
||||
id: content
|
||||
implicitWidth: Math.min(Kirigami.Units.gridUnit * 20, Screen.width - Kirigami.Units.gridUnit * 2)
|
||||
padding: Kirigami.Units.smallSpacing * 2
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
source: simpleShadow
|
||||
blurMax: 16
|
||||
shadowEnabled: true
|
||||
shadowVerticalOffset: 1
|
||||
shadowOpacity: 0.85
|
||||
shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.2)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: simpleShadow
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: -1
|
||||
anchors.rightMargin: -1
|
||||
anchors.bottomMargin: -1
|
||||
|
||||
color: {
|
||||
let darkerBackgroundColor = Qt.darker(Kirigami.Theme.backgroundColor, 1.3);
|
||||
return Qt.rgba(darkerBackgroundColor.r, darkerBackgroundColor.g, darkerBackgroundColor.b, 0.5)
|
||||
}
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.lighter(Kirigami.Theme.backgroundColor, 1.5)
|
||||
opacity: 0.85
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
|
||||
import QtQuick
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
|
||||
pragma Singleton
|
||||
|
||||
/**
|
||||
* This wraps the VolumeOSDProvider component so that we can avoid loading side
|
||||
* effects from imports (since this is a singleton and initialized immediately on import).
|
||||
*/
|
||||
Loader {
|
||||
id: root
|
||||
sourceComponent: Component {
|
||||
MobileShell.VolumeOSDProvider {}
|
||||
}
|
||||
|
||||
// WARNING: only call this load from within the plasmashell process, because
|
||||
// multiple bindings of the shortcut may break it entirely (hardware volume keys)
|
||||
function load() {
|
||||
root.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ import org.kde.kirigami 2.19 as Kirigami
|
|||
|
||||
MouseArea {
|
||||
id: root
|
||||
onClicked: root.requestedClose()
|
||||
onClicked: root.requestedClose(false)
|
||||
|
||||
function requestFocus() {
|
||||
queryField.forceActiveFocus();
|
||||
|
|
@ -30,11 +30,11 @@ MouseArea {
|
|||
queryField.text = "";
|
||||
}
|
||||
|
||||
signal requestedClose()
|
||||
signal requestedClose(triggeredByKeyEvent: bool)
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.requestedClose();
|
||||
root.requestedClose(true);
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -72,7 +72,21 @@ MouseArea {
|
|||
|
||||
font.weight: Font.Bold
|
||||
|
||||
KeyNavigation.down: listView
|
||||
// Keyboard navigation
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Down) {
|
||||
if (listView.count === 0) {
|
||||
// Close if listview has no elements
|
||||
root.requestedClose(true);
|
||||
} else {
|
||||
// Focus on listview if there are elements
|
||||
listView.forceActiveFocus();
|
||||
listView.currentIndex = 0;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QQC2.ScrollView {
|
||||
|
|
@ -84,7 +98,7 @@ MouseArea {
|
|||
Layout.rightMargin: Kirigami.Units.gridUnit
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Milou.ResultsListView {
|
||||
Milou.ResultsView {
|
||||
id: listView
|
||||
queryString: queryField.text
|
||||
clip: true
|
||||
|
|
@ -97,7 +111,7 @@ MouseArea {
|
|||
}
|
||||
|
||||
onActivated: {
|
||||
root.requestedClose();
|
||||
root.requestedClose(false);
|
||||
}
|
||||
onUpdateQueryString: {
|
||||
queryField.text = text
|
||||
|
|
@ -139,12 +153,12 @@ MouseArea {
|
|||
// Close search view if we press down with last item selected
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Down && (model.index === listView.count - 1)) {
|
||||
root.requestedClose();
|
||||
root.requestedClose(true);
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Used by ResultsListView to determine next tab action
|
||||
// Used by ResultsView to determine next tab action
|
||||
function activateNextAction() {
|
||||
queryField.forceActiveFocus();
|
||||
queryField.selectAll();
|
||||
|
|
@ -155,7 +169,7 @@ MouseArea {
|
|||
listView.currentIndex = model.index;
|
||||
listView.runCurrentIndex();
|
||||
|
||||
root.requestedClose();
|
||||
root.requestedClose(false);
|
||||
}
|
||||
hoverEnabled: true
|
||||
|
||||
|
|
|
|||
|
|
@ -1,324 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2014 Aaron Seigo <aseigo@kde.org>
|
||||
* SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Controls as Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.plasma.core as PlasmaCore
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.extras 2.0 as PlasmaExtras
|
||||
|
||||
import org.kde.milou as Milou
|
||||
import org.kde.kirigami 2.19 as Kirigami
|
||||
|
||||
/**
|
||||
* Search widget that is embedded into the homescreen. The dimensions of
|
||||
* the root item is assumed to be the available screen area for applications.
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// content margins (background ignores this)
|
||||
property real topMargin: 0
|
||||
property real bottomMargin: 0
|
||||
property real leftMargin: 0
|
||||
property real rightMargin: 0
|
||||
|
||||
function startGesture() {
|
||||
queryField.text = "";
|
||||
flickable.contentY = closedContentY;
|
||||
}
|
||||
|
||||
function updateGestureOffset(yOffset) {
|
||||
flickable.contentY = Math.max(0, Math.min(closedContentY, flickable.contentY + yOffset));
|
||||
}
|
||||
|
||||
// call when the touch gesture has let go
|
||||
function endGesture() {
|
||||
flickable.opening ? open() : close();
|
||||
}
|
||||
|
||||
// open the search widget (animated)
|
||||
function open() {
|
||||
anim.to = openedContentY;
|
||||
anim.restart();
|
||||
}
|
||||
|
||||
// close the search widget (animated)
|
||||
function close() {
|
||||
anim.to = closedContentY;
|
||||
anim.restart();
|
||||
}
|
||||
|
||||
// emitted when an item on the ListView is triggered
|
||||
signal actionTriggered()
|
||||
|
||||
readonly property real closedContentY: Kirigami.Units.gridUnit * 5
|
||||
readonly property real openedContentY: 0
|
||||
readonly property real openFactor: Math.max(0, Math.min(1, 1 - flickable.contentY / closedContentY))
|
||||
readonly property bool isOpen: openFactor != 0
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.3)
|
||||
opacity: root.openFactor
|
||||
}
|
||||
|
||||
onOpacityChanged: {
|
||||
if (opacity === 0) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Down) {
|
||||
listView.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: flickable
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: root.topMargin
|
||||
anchors.bottomMargin: root.bottomMargin
|
||||
anchors.leftMargin: root.leftMargin
|
||||
anchors.rightMargin: root.rightMargin
|
||||
|
||||
contentHeight: flickable.height + root.closedContentY
|
||||
contentY: root.closedContentY
|
||||
property real oldContentY: contentY
|
||||
property bool opening: false
|
||||
|
||||
onContentYChanged: {
|
||||
opening = contentY < oldContentY;
|
||||
oldContentY = contentY;
|
||||
|
||||
if (contentY !== root.openedContentY) {
|
||||
queryField.focus = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMovementEnded: root.endGesture()
|
||||
|
||||
onDraggingChanged: {
|
||||
if (!dragging) {
|
||||
root.endGesture();
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation on contentY {
|
||||
id: anim
|
||||
duration: Kirigami.Units.longDuration * 2
|
||||
easing.type: Easing.OutQuad
|
||||
running: false
|
||||
onFinished: {
|
||||
if (anim.to === root.openedContentY) {
|
||||
queryField.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
height: flickable.height
|
||||
width: flickable.width
|
||||
|
||||
Controls.Control {
|
||||
opacity: root.openFactor
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Kirigami.Units.gridUnit * 30
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: Kirigami.Units.gridUnit
|
||||
Layout.leftMargin: Kirigami.Units.gridUnit
|
||||
Layout.rightMargin: Kirigami.Units.gridUnit
|
||||
|
||||
leftPadding: Kirigami.Units.smallSpacing
|
||||
rightPadding: Kirigami.Units.smallSpacing
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
|
||||
background: Item {
|
||||
|
||||
// shadow for search window
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
source: rectBackground
|
||||
blurMax: 16
|
||||
shadowEnabled: true
|
||||
shadowVerticalOffset: 1
|
||||
shadowOpacity: 0.15
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: rectBackground
|
||||
anchors.fill: parent
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
Item {
|
||||
implicitHeight: queryField.height
|
||||
implicitWidth: height
|
||||
Kirigami.Icon {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Math.round(Kirigami.Units.smallSpacing)
|
||||
source: "start-here-symbolic"
|
||||
}
|
||||
}
|
||||
PlasmaComponents.TextField {
|
||||
id: queryField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: i18n("Search…")
|
||||
inputMethodHints: Qt.ImhNoPredictiveText // don't need to press "enter" to update text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Controls.ScrollView {
|
||||
opacity: root.openFactor === 1 ? 1 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: listView.contentHeight > availableHeight
|
||||
|
||||
Milou.ResultsListView {
|
||||
id: listView
|
||||
queryString: queryField.text
|
||||
clip: true
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
|
||||
highlight: activeFocus ? highlightComponent : null
|
||||
Component {
|
||||
id: highlightComponent
|
||||
|
||||
PlasmaExtras.Highlight {}
|
||||
}
|
||||
|
||||
onActivated: {
|
||||
root.close();
|
||||
}
|
||||
onUpdateQueryString: {
|
||||
queryField.text = text
|
||||
queryField.cursorPosition = cursorPosition
|
||||
}
|
||||
|
||||
delegate: MouseArea {
|
||||
id: delegate
|
||||
height: rowLayout.height
|
||||
width: listView.width
|
||||
|
||||
onClicked: {
|
||||
listView.currentIndex = model.index;
|
||||
listView.runCurrentIndex();
|
||||
|
||||
root.actionTriggered();
|
||||
}
|
||||
hoverEnabled: true
|
||||
|
||||
function activateNextAction() {
|
||||
queryField.forceActiveFocus();
|
||||
queryField.selectAll();
|
||||
listView.currentIndex = -1;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: delegate.pressed ? Qt.rgba(255, 255, 255, 0.2) : (delegate.containsMouse ? Qt.rgba(255, 255, 255, 0.05) : "transparent")
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
height: Kirigami.Units.gridUnit * 3
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: Kirigami.Units.gridUnit
|
||||
anchors.rightMargin: Kirigami.Units.gridUnit
|
||||
|
||||
Kirigami.Icon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
source: model.decoration
|
||||
implicitWidth: Kirigami.Units.iconSizes.medium
|
||||
implicitHeight: Kirigami.Units.iconSizes.medium
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
PlasmaComponents.Label {
|
||||
id: title
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.smallSpacing * 2
|
||||
Layout.rightMargin: Kirigami.Units.gridUnit
|
||||
|
||||
maximumLineCount: 1
|
||||
elide: Text.ElideRight
|
||||
text: typeof modelData !== "undefined" ? modelData : model.display
|
||||
color: "white"
|
||||
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize
|
||||
}
|
||||
PlasmaComponents.Label {
|
||||
id: subtitle
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.smallSpacing * 2
|
||||
Layout.rightMargin: Kirigami.Units.gridUnit
|
||||
|
||||
maximumLineCount: 1
|
||||
elide: Text.ElideRight
|
||||
text: model.subtext || ""
|
||||
color: "white"
|
||||
opacity: 0.8
|
||||
|
||||
font.pointSize: Math.round(Kirigami.Theme.defaultFont.pointSize * 0.8)
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: actionsRepeater
|
||||
model: typeof actions !== "undefined" ? actions : []
|
||||
|
||||
Controls.ToolButton {
|
||||
icon: modelData.icon || ""
|
||||
visible: modelData.visible || true
|
||||
enabled: modelData.enabled || true
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: modelData.text
|
||||
checkable: checked
|
||||
checked: delegate.activeAction === index
|
||||
focus: delegate.activeAction === index
|
||||
onClicked: delegate.ListView.view.runAction(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
onClicked: close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,13 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
// darken background when pressed
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: root.darken ? 0.05 : 0
|
||||
}
|
||||
|
||||
Image {
|
||||
id: img
|
||||
source: root.imageSource
|
||||
|
|
@ -41,9 +48,12 @@ Item {
|
|||
// ensure text is readable
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Kirigami.Theme.backgroundColor.r * (inActionDrawer ? 0.85 : 0.95), Kirigami.Theme.backgroundColor.g * (inActionDrawer ? 0.85 : 0.95), Kirigami.Theme.backgroundColor.b * (inActionDrawer ? 0.85 : 0.95), root.darken ? 0.95 : 0.85)
|
||||
color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.2)
|
||||
visible: img.progress
|
||||
}
|
||||
|
||||
opacity: 0.1
|
||||
|
||||
// apply lighten, saturate and blur effect
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
|
|
|
|||
|
|
@ -21,8 +21,7 @@ Item {
|
|||
id: root
|
||||
visible: sourceRepeater.count > 0
|
||||
|
||||
property bool inActionDrawer: false
|
||||
|
||||
property int panelType: MobileShell.PanelBackground.PanelType.Drawer
|
||||
property bool detailledView: false
|
||||
|
||||
readonly property real padding: Kirigami.Units.gridUnit
|
||||
|
|
@ -34,8 +33,8 @@ Item {
|
|||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: implicitHeight == 0 ? 0 : Kirigami.Units.shortDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
duration: implicitHeight == 0 ? 0 : Kirigami.Units.longDuration
|
||||
easing.type: Easing.OutQuart
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,30 +62,9 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
// shadow
|
||||
MultiEffect {
|
||||
anchors.fill: root
|
||||
visible: !inActionDrawer
|
||||
source: simpleShadow
|
||||
blurMax: 32
|
||||
shadowEnabled: true
|
||||
shadowVerticalOffset: 1
|
||||
shadowOpacity: 0.5
|
||||
shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.2)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: simpleShadow
|
||||
anchors.fill: root
|
||||
anchors.leftMargin: -1
|
||||
anchors.rightMargin: -1
|
||||
anchors.bottomMargin: -1
|
||||
|
||||
color: {
|
||||
let darkerBackgroundColor = Qt.darker(Kirigami.Theme.backgroundColor, 1.3);
|
||||
return Qt.rgba(darkerBackgroundColor.r, darkerBackgroundColor.g, darkerBackgroundColor.b, 0.3)
|
||||
}
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
MobileShell.PanelBackground {
|
||||
anchors.fill: parent
|
||||
panelType: root.panelType
|
||||
}
|
||||
|
||||
// list of app media widgets
|
||||
|
|
@ -160,7 +138,6 @@ Item {
|
|||
padding: root.padding
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
Kirigami.Theme.inherit: true
|
||||
width: playerItem.width - playerItem.leftPadding - playerItem.rightPadding
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import QtQuick
|
|||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
|
||||
import org.kde.plasma.plasma5support 2.0 as P5Support
|
||||
import org.kde.plasma.clock
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents3
|
||||
|
||||
import org.kde.notificationmanager as NotificationManager
|
||||
|
|
@ -35,7 +35,7 @@ Item {
|
|||
property var model
|
||||
property int modelIndex
|
||||
|
||||
property P5Support.DataSource timeSource
|
||||
property Clock clockSource
|
||||
|
||||
readonly property int notificationType: model.type
|
||||
|
||||
|
|
@ -158,7 +158,6 @@ Item {
|
|||
notificationsModel.invokeAction(notificationsModel.index(modelIndex, 0), actionName, NotificationManager.Close); // notification closes
|
||||
}
|
||||
}
|
||||
expire();
|
||||
}
|
||||
|
||||
if (notificationItem.requestToInvoke) {
|
||||
|
|
|
|||
|
|
@ -9,26 +9,64 @@ import Qt5Compat.GraphicalEffects
|
|||
import org.kde.kirigami 2.12 as Kirigami
|
||||
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* The content that goes inside the notification card
|
||||
*/
|
||||
default property Item contentItem
|
||||
|
||||
/**
|
||||
* The panel background type for this notification.
|
||||
*/
|
||||
property int panelType: MobileShell.PanelBackground.PanelType.Drawer
|
||||
|
||||
/**
|
||||
* Whether this is a popup notification.
|
||||
*/
|
||||
property bool popupNotification: false
|
||||
|
||||
/**
|
||||
* Whether this popup notification is tucked underneath the current popup.
|
||||
*/
|
||||
property bool inPopupDrawer: false
|
||||
|
||||
/**
|
||||
* Whether this notification is within the lockscreen.
|
||||
*/
|
||||
property bool inLockScreen: false
|
||||
|
||||
/**
|
||||
* The current notification popup height.
|
||||
*/
|
||||
property int currentPopupHeight: 0
|
||||
|
||||
/**
|
||||
* The remaining time before the notification popup is dismissed.
|
||||
*/
|
||||
property real remainingTimeProgress: 1
|
||||
|
||||
/**
|
||||
* Whether the timer for dismissing the notification popup is running.
|
||||
*/
|
||||
property bool closeTimerRunning: false
|
||||
|
||||
/**
|
||||
* Whether tapping on this notification is enabled.
|
||||
*/
|
||||
property bool tapEnabled: false
|
||||
|
||||
/**
|
||||
* Whether swipping on this notification is enabled.
|
||||
*/
|
||||
property bool swipeGestureEnabled: false
|
||||
|
||||
/**
|
||||
* The current drag offset for this notification.
|
||||
*/
|
||||
property real dragOffset: 0
|
||||
|
||||
signal tapped()
|
||||
|
|
@ -57,35 +95,12 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
// shadow
|
||||
MultiEffect {
|
||||
MobileShell.PanelBackground {
|
||||
anchors.fill: mainCard
|
||||
visible: Math.abs(dragOffset) !== root.width
|
||||
source: simpleShadow
|
||||
blurMax: 16
|
||||
shadowEnabled: true
|
||||
shadowVerticalOffset: 1
|
||||
shadowOpacity: popupNotification ? 0.85 : 0.5
|
||||
shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.2)
|
||||
animate: true
|
||||
panelType: root.panelType
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: simpleShadow
|
||||
visible: Math.abs(dragOffset) !== root.width
|
||||
anchors.fill: mainCard
|
||||
anchors.leftMargin: -1
|
||||
anchors.rightMargin: -1
|
||||
anchors.bottomMargin: -1
|
||||
|
||||
color: {
|
||||
let darkerBackgroundColor = Qt.darker(Kirigami.Theme.backgroundColor, 1.3);
|
||||
return Qt.rgba(darkerBackgroundColor.r, darkerBackgroundColor.g, darkerBackgroundColor.b, popupNotification ? 0.5 : 0.3)
|
||||
}
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
}
|
||||
|
||||
|
||||
|
||||
// card
|
||||
Item {
|
||||
id: mainCard
|
||||
|
|
@ -103,17 +118,6 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: popupNotification ? Qt.lighter(Kirigami.Theme.backgroundColor, 1.5) : Qt.rgba(Kirigami.Theme.backgroundColor.r * 0.95, Kirigami.Theme.backgroundColor.g * 0.95, Kirigami.Theme.backgroundColor.b * 0.95, (root.tapEnabled && mouseArea.pressed) ? 0.95 : 0.85)
|
||||
opacity: popupNotification ? 0.85 : 1
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
layer.enabled: popupNotification ? false : true
|
||||
layer.effect: MultiEffect {
|
||||
brightness: 0.075
|
||||
}
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
id: progress
|
||||
anchors.left: parent.left
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import QtQuick.Window 2.2
|
|||
import org.kde.kirigami 2.12 as Kirigami
|
||||
|
||||
import org.kde.plasma.core as PlasmaCore
|
||||
import org.kde.plasma.plasma5support 2.0 as P5Support
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.notificationmanager as NotificationManager
|
||||
|
||||
|
|
@ -21,7 +20,6 @@ import org.kde.coreaddons 1.0 as KCoreAddons
|
|||
RowLayout {
|
||||
id: notificationHeading
|
||||
|
||||
property bool inLockscreen: false
|
||||
property var applicationIconSource
|
||||
property string applicationName
|
||||
property string originName
|
||||
|
|
@ -44,7 +42,7 @@ RowLayout {
|
|||
Layout.leftMargin: Kirigami.Units.smallSpacing
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: inLockscreen ? "white" : Kirigami.Theme.textColor
|
||||
color: Kirigami.Theme.textColor
|
||||
|
||||
opacity: 0.75
|
||||
elide: Text.ElideLeft
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ BaseNotificationItem {
|
|||
id: notificationItem
|
||||
implicitHeight: mainCard.implicitHeight + mainCard.anchors.topMargin + notificationHeading.height
|
||||
|
||||
property bool inLockscreen: false
|
||||
property bool inLockScreen: false
|
||||
property int panelType: MobileShell.PanelBackground.PanelType.Drawer
|
||||
|
||||
property var cardColorScheme: Kirigami.Theme.View
|
||||
|
||||
signal dragStart()
|
||||
signal dragEnd()
|
||||
|
|
@ -37,11 +40,6 @@ BaseNotificationItem {
|
|||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
inLockscreen: root.inLockscreen
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Header
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
visible: !notificationItem.inGroup
|
||||
height: visible ? implicitHeight : 0
|
||||
|
||||
|
|
@ -62,10 +60,15 @@ BaseNotificationItem {
|
|||
onTapped: notificationItem.actionInvoked("default");
|
||||
swipeGestureEnabled: notificationItem.closable
|
||||
onDismissRequested: notificationItem.close();
|
||||
inLockScreen: notificationItem.inLockScreen
|
||||
panelType: notificationItem.panelType
|
||||
|
||||
onDragStart: notificationItem.dragStart()
|
||||
onDragEnd: notificationItem.dragEnd()
|
||||
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: notificationItem.cardColorScheme
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
spacing: 0
|
||||
|
|
@ -98,7 +101,7 @@ BaseNotificationItem {
|
|||
jobDetails: notificationItem.jobDetails
|
||||
|
||||
time: notificationItem.time
|
||||
timeSource: notificationItem.timeSource
|
||||
clockSource: notificationItem.clockSource
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ BaseNotificationItem {
|
|||
property real remainingTimeProgress: 1
|
||||
property bool closeTimerRunning: false
|
||||
|
||||
property bool inLockscreen: false
|
||||
property bool inLockScreen: false
|
||||
property int panelType: MobileShell.PanelBackground.PanelType.Drawer
|
||||
|
||||
signal dragStart()
|
||||
signal dragEnd()
|
||||
|
|
@ -48,6 +49,8 @@ BaseNotificationItem {
|
|||
currentPopupHeight: notificationItem.currentPopupHeight
|
||||
remainingTimeProgress: notificationItem.remainingTimeProgress
|
||||
closeTimerRunning: notificationItem.closeTimerRunning
|
||||
inLockScreen: notificationItem.inLockScreen
|
||||
panelType: notificationItem.panelType
|
||||
tapEnabled: notificationItem.hasDefaultAction
|
||||
onTapped: notificationItem.actionInvoked("default");
|
||||
swipeGestureEnabled: notificationItem.closable
|
||||
|
|
@ -110,7 +113,7 @@ BaseNotificationItem {
|
|||
jobDetails: notificationItem.jobDetails
|
||||
|
||||
time: notificationItem.time
|
||||
timeSource: notificationItem.timeSource
|
||||
clockSource: notificationItem.clockSource
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import QtQuick.Layouts 1.1
|
|||
import QtQuick.Window 2.2
|
||||
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.plasma5support 2.0 as P5Support
|
||||
import org.kde.plasma.clock
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
|
@ -26,15 +26,15 @@ PlasmaComponents.Label {
|
|||
property QtObject jobDetails
|
||||
|
||||
property var time
|
||||
property P5Support.DataSource timeSource
|
||||
property Clock clockSource
|
||||
|
||||
// notification created/updated time changed
|
||||
onTimeChanged: updateAgoText()
|
||||
|
||||
Connections {
|
||||
target: timeSource
|
||||
target: clockSource
|
||||
// clock time changed
|
||||
function onDataChanged() {
|
||||
function onTimeChanged() {
|
||||
ageLabel.updateAgoText()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
.import org.kde.notificationmanager as NotificationManager
|
||||
.import org.kde.coreaddons 1.0 as KCoreAddons
|
||||
.import QtQml as QtQml
|
||||
|
||||
function determineNotificationHeadingText(notificationItem) {
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ import Qt5Compat.GraphicalEffects
|
|||
|
||||
import org.kde.kirigami 2.12 as Kirigami
|
||||
|
||||
import org.kde.plasma.plasma5support 2.0 as P5Support
|
||||
import org.kde.plasma.clock
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
||||
import org.kde.plasma.extras 2.0 as PlasmaExtras
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents3
|
||||
|
||||
|
|
@ -27,7 +28,20 @@ import org.kde.notificationmanager as NotificationManager
|
|||
Item {
|
||||
id: root
|
||||
|
||||
property bool inLockscreen: false
|
||||
/**
|
||||
* If the notification is in the lockscreen.
|
||||
*/
|
||||
property bool inLockScreen: false
|
||||
|
||||
/**
|
||||
* The panel background type for all the notifications within the widget.
|
||||
*/
|
||||
property int panelType: MobileShell.PanelBackground.PanelType.Drawer
|
||||
|
||||
/**
|
||||
* The color scheme of the foreground cards (but not notification headers, which are outside of it).
|
||||
*/
|
||||
property var cardColorScheme: Kirigami.Theme.View
|
||||
|
||||
/**
|
||||
* The notification model for the widget.
|
||||
|
|
@ -53,6 +67,31 @@ Item {
|
|||
*/
|
||||
property bool actionsRequireUnlock: false
|
||||
|
||||
/**
|
||||
* Top padding of the notification list.
|
||||
*/
|
||||
property int topPadding: 0
|
||||
|
||||
/**
|
||||
* Bottom padding of the notification list.
|
||||
*/
|
||||
property int bottomPadding: 0
|
||||
|
||||
/**
|
||||
* Header component for notification list.
|
||||
*/
|
||||
property var header
|
||||
|
||||
/**
|
||||
* Whether to show the header component.
|
||||
*/
|
||||
property bool showHeader: false
|
||||
|
||||
/**
|
||||
* Gives access to the notification list view outside of the notification widget.
|
||||
*/
|
||||
property alias listView: list
|
||||
|
||||
/**
|
||||
* Whether the widget has notifications.
|
||||
*/
|
||||
|
|
@ -73,16 +112,14 @@ Item {
|
|||
*/
|
||||
signal unlockRequested()
|
||||
|
||||
/**
|
||||
* Emitted when the background is clicked (not a notification or other element).
|
||||
*/
|
||||
signal backgroundClicked()
|
||||
|
||||
/**
|
||||
* Run pending action that was pending for authentication when unlockRequested() was emitted.
|
||||
*/
|
||||
function runPendingAction() {
|
||||
list.pendingNotificationWithAction.runPendingAction();
|
||||
if (list.pendingNotificationWithAction) {
|
||||
list.pendingNotificationWithAction.runPendingAction();
|
||||
list.pendingNotificationWithAction = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -97,19 +134,27 @@ Item {
|
|||
}
|
||||
|
||||
/**
|
||||
* Toggles Do Not Disturb mode.
|
||||
* Sets Do Not Disturb mode to the intended setting.
|
||||
* Note: The state may not change to the desired setting, always read doNotDisturbEnabled for the current state.
|
||||
*/
|
||||
function toggleDoNotDisturbMode() {
|
||||
if (doNotDisturbModeEnabled) {
|
||||
notificationSettings.defaults();
|
||||
} else {
|
||||
var until = new Date();
|
||||
function setDoNotDisturbMode(doNotDisturb: bool) {
|
||||
if (!doNotDisturb) {
|
||||
// Turn off do not disturb
|
||||
|
||||
notificationSettings.notificationsInhibitedUntil = undefined;
|
||||
notificationSettings.revokeApplicationInhibitions();
|
||||
notificationSettings.fullscreenFocused = false;
|
||||
// overrules current mirrored screen setup, updates again when screen configuration changes
|
||||
notificationSettings.screensMirrored = false;
|
||||
|
||||
} else {
|
||||
// Turn on do not disturb
|
||||
// We just have a global toggle, so set it to a really long time (in this case, a year)
|
||||
var until = new Date();
|
||||
until.setFullYear(until.getFullYear() + 1);
|
||||
|
||||
notificationSettings.notificationsInhibitedUntil = until;
|
||||
}
|
||||
|
||||
notificationSettings.save();
|
||||
}
|
||||
|
||||
|
|
@ -120,19 +165,19 @@ Item {
|
|||
MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_notifications");
|
||||
}
|
||||
|
||||
P5Support.DataSource {
|
||||
id: timeDataSource
|
||||
engine: "time"
|
||||
connectedSources: ["Local"]
|
||||
interval: 60000 // 1 min
|
||||
intervalAlignment: P5Support.Types.AlignToMinute
|
||||
// Implement listening to system "do not disturb" requests
|
||||
Connections {
|
||||
target: MobileShellState.ShellDBusClient
|
||||
|
||||
function onDoNotDisturbChanged() {
|
||||
if (root.doNotDisturbModeEnabled !== MobileShellState.ShellDBusClient.doNotDisturb) {
|
||||
root.setDoNotDisturbMode(MobileShellState.ShellDBusClient.doNotDisturb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// implement background clicking signal
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: backgroundClicked()
|
||||
z: -1 // ensure that this is below notification items so we don't steal button clicks
|
||||
Clock {
|
||||
id: clock
|
||||
}
|
||||
|
||||
ListView {
|
||||
|
|
@ -143,15 +188,16 @@ Item {
|
|||
|
||||
currentIndex: 0
|
||||
|
||||
property var pendingNotificationWithAction
|
||||
property NotificationItem pendingNotificationWithAction: null
|
||||
|
||||
readonly property int animationDuration: ShellSettings.Settings.animationsEnabled ? Kirigami.Units.longDuration : 0
|
||||
|
||||
// If a screen overflow occurs, fix height in order to maintain tool buttons in place.
|
||||
readonly property bool listOverflowing: contentItem.childrenRect.height + toolButtons.height + spacing >= root.height
|
||||
readonly property bool listOverflowing: listHeight + spacing >= root.height
|
||||
readonly property int listHeight: contentItem.childrenRect.height
|
||||
|
||||
bottomMargin: spacing
|
||||
height: count === 0 ? 0 : (listOverflowing ? root.height - toolButtons.height : contentItem.childrenRect.height + bottomMargin)
|
||||
height: count === 0 ? (root.topPadding + (showHeader ? root.header.height + listHeight + Kirigami.Units.largeSpacing * 2 : 0)) : (listOverflowing ? root.height : listHeight + bottomMargin)
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
|
|
@ -159,7 +205,7 @@ Item {
|
|||
right: parent.right
|
||||
}
|
||||
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
// TODO keyboard focus
|
||||
|
|
@ -167,6 +213,39 @@ Item {
|
|||
highlightResizeDuration: 0
|
||||
highlight: Item {}
|
||||
|
||||
// media control widget
|
||||
// added to the notification list when in landscape mode
|
||||
Component {
|
||||
id: headerComponent
|
||||
Item {
|
||||
width: parent.width
|
||||
|
||||
MobileShell.BaseItem {
|
||||
id: headerComponentProxy
|
||||
|
||||
contentItem: showHeader ? root.header : null
|
||||
y: root.topPadding + Kirigami.Units.largeSpacing
|
||||
|
||||
width: parent.width - Kirigami.Units.gridUnit * 2
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Kirigami.Units.gridUnit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set bottom padding for the notification list
|
||||
Component {
|
||||
id: footerComponent
|
||||
Item {
|
||||
width: parent.width
|
||||
height: root.bottomPadding
|
||||
}
|
||||
}
|
||||
|
||||
header: headerComponent
|
||||
|
||||
footer: footerComponent
|
||||
|
||||
section {
|
||||
property: "isGroup"
|
||||
criteria: ViewSection.FullString
|
||||
|
|
@ -250,12 +329,28 @@ Item {
|
|||
PropertyAction { target: delegateLoader; property: "ListView.delayRemove"; value: false }
|
||||
}
|
||||
|
||||
// adjust top paddding for media control widget
|
||||
Component {
|
||||
id: groupDelegate
|
||||
NotificationGroupHeader {
|
||||
applicationName: model.applicationName
|
||||
applicationIconSource: model.applicationIconName
|
||||
originName: model.originName || ""
|
||||
|
||||
Column {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
height: headerSpace.height + groupHeader.height
|
||||
|
||||
Item {
|
||||
id: headerSpace
|
||||
width: parent.width
|
||||
height: index == 0 ? root.topPadding + (showHeader && root.header.visible ? root.header.height + Kirigami.Units.largeSpacing * 2 : 0) : 0
|
||||
visible: index == 0
|
||||
}
|
||||
|
||||
NotificationGroupHeader {
|
||||
id: groupHeader
|
||||
applicationName: model.applicationName
|
||||
applicationIconSource: model.applicationIconName
|
||||
originName: model.originName || ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -265,20 +360,29 @@ Item {
|
|||
Column {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
height: notificationItem.height + showMoreLoader.height
|
||||
height: headerSpace.height + notificationItem.height + showMoreLoader.height
|
||||
|
||||
Item {
|
||||
id: headerSpace
|
||||
width: parent.width
|
||||
height: index == 0 ? root.topPadding + (showHeader && root.header.visible ? root.header.height + Kirigami.Units.largeSpacing * 2 : 0) : 0
|
||||
visible: index == 0
|
||||
}
|
||||
|
||||
NotificationItem {
|
||||
id: notificationItem
|
||||
width: parent.width
|
||||
height: implicitHeight
|
||||
|
||||
inLockscreen: root.inLockscreen
|
||||
inLockScreen: root.inLockScreen
|
||||
panelType: root.panelType
|
||||
cardColorScheme: root.cardColorScheme
|
||||
|
||||
model: delegateLoader.model
|
||||
modelIndex: delegateLoader.index
|
||||
notificationsModel: root.historyModel
|
||||
notificationsModelType: root.historyModelType
|
||||
timeSource: timeDataSource
|
||||
clockSource: clock
|
||||
|
||||
requestToInvoke: root.actionsRequireUnlock
|
||||
onRunActionRequested: {
|
||||
|
|
@ -303,7 +407,7 @@ Item {
|
|||
return false;
|
||||
|
||||
return (model.groupChildrenCount > model.expandedGroupChildrenCount || model.isGroupExpanded)
|
||||
&& delegateLoader.ListView.nextSection != delegateLoader.ListView.section;
|
||||
&& delegateLoader.ListView.nextSection != delegateLoader.ListView.section;
|
||||
}
|
||||
|
||||
// state + transition: animates the item when it becomes visible. Fade off is handled by above ListView.onRemove.
|
||||
|
|
@ -323,8 +427,8 @@ Item {
|
|||
sourceComponent: PlasmaComponents3.ToolButton {
|
||||
icon.name: model.isGroupExpanded ? "arrow-up" : "arrow-down"
|
||||
text: model.isGroupExpanded ? i18n("Show Fewer")
|
||||
: i18nc("Expand to show n more notifications",
|
||||
"Show %1 More", (model.groupChildrenCount - model.expandedGroupChildrenCount))
|
||||
: i18nc("Expand to show n more notifications",
|
||||
"Show %1 More", (model.groupChildrenCount - model.expandedGroupChildrenCount))
|
||||
onClicked: {
|
||||
list.setGroupExpanded(model.index, !model.isGroupExpanded)
|
||||
}
|
||||
|
|
@ -334,58 +438,4 @@ Item {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: toolButtons
|
||||
height: visible ? spacer.height + toolLayout.height + toolLayout.anchors.topMargin + toolLayout.anchors.bottomMargin : 0
|
||||
|
||||
// do not show on lockscreen
|
||||
visible: !root.actionsRequireUnlock
|
||||
|
||||
anchors {
|
||||
top: list.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: spacer
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
visible: list.listOverflowing
|
||||
height: 1
|
||||
opacity: 0.25
|
||||
color: Kirigami.Theme.textColor
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: toolLayout
|
||||
|
||||
anchors {
|
||||
top: spacer.bottom
|
||||
right: parent.right
|
||||
left: parent.left
|
||||
leftMargin: Kirigami.Units.largeSpacing
|
||||
rightMargin: Kirigami.Units.largeSpacing
|
||||
topMargin: list.spacing
|
||||
bottomMargin: list.spacing
|
||||
}
|
||||
|
||||
PlasmaComponents3.ToolButton {
|
||||
id: clearButton
|
||||
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
visible: hasNotifications
|
||||
|
||||
font.bold: true
|
||||
font.pointSize: Kirigami.Theme.smallFont.pointSize
|
||||
|
||||
icon.name: "edit-clear-history"
|
||||
text: i18n("Clear All Notifications")
|
||||
onClicked: clearHistory()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue