Merge branch plasma-mobile:master into master

This commit is contained in:
kai weaver 2026-02-12 19:32:19 +00:00
commit c4d2238f36
1936 changed files with 75158 additions and 102013 deletions

View file

@ -6,7 +6,11 @@ include:
file: file:
- /gitlab-templates/reuse-lint.yml - /gitlab-templates/reuse-lint.yml
- /gitlab-templates/linux-qt6.yml - /gitlab-templates/linux-qt6.yml
- /gitlab-templates/linux-qt6-next.yml
- /gitlab-templates/alpine-qt6.yml - /gitlab-templates/alpine-qt6.yml
- /gitlab-templates/xml-lint.yml
- /gitlab-templates/yaml-lint.yml
- /gitlab-templates/qml-lint.yml
posix-script-compliance: posix-script-compliance:
image: image:

View file

@ -2,9 +2,10 @@
# SPDX-License-Identifier: CC0-1.0 # SPDX-License-Identifier: CC0-1.0
Dependencies: Dependencies:
- 'on': ['@all'] - 'on': ['@all']
'require': 'require':
'frameworks/extra-cmake-modules': '@latest-kf6' 'frameworks/extra-cmake-modules': '@latest-kf6'
'frameworks/kauth': '@latest-kf6'
'frameworks/kconfig': '@latest-kf6' 'frameworks/kconfig': '@latest-kf6'
'frameworks/kcoreaddons': '@latest-kf6' 'frameworks/kcoreaddons': '@latest-kf6'
'frameworks/ki18n': '@latest-kf6' 'frameworks/ki18n': '@latest-kf6'
@ -24,17 +25,18 @@ Dependencies:
'plasma/plasma-workspace': '@same' 'plasma/plasma-workspace': '@same'
'libraries/kirigami-addons': '@latest-kf6' 'libraries/kirigami-addons': '@latest-kf6'
Options: Options:
require-passing-tests-on: [ 'Linux', 'FreeBSD'] require-passing-tests-on: ['Linux', 'FreeBSD']
run-qmllint: true
RuntimeDependencies: RuntimeDependencies:
- 'on': ['Linux'] - 'on': ['Linux']
'require': 'require':
'plasma/plasma-nm': '@same' 'plasma/plasma-nm': '@same'
'plasma/plasma-pa': '@same' 'plasma/plasma-pa': '@same'
'plasma/bluedevil': '@same' 'plasma/bluedevil': '@same'
'plasma/milou': '@same' 'plasma/milou': '@same'
'plasma/qqc2-breeze-style': '@same' 'plasma/qqc2-breeze-style': '@same'
'frameworks/bluez-qt': '@latest-kf6' 'frameworks/bluez-qt': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD'] - 'on': ['Linux', 'FreeBSD']
'require': 'require':
'plasma/milou' : '@same' 'plasma/milou': '@same'

View file

@ -0,0 +1 @@
https://ev.kde.org/.well-known/funding.json

View file

@ -0,0 +1,2 @@
Copyright None
SPDX-License-Identifier: CC0-1.0

View file

@ -4,15 +4,14 @@
# SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org> # SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.24)
project(plasma-mobile) project(plasma-mobile)
set(PROJECT_VERSION "6.2.80") set(PROJECT_VERSION "6.6.80")
set(PROJECT_VERSION_MAJOR 6)
set(PROJECT_DEP_VERSION "6.2.0") set(PROJECT_DEP_VERSION "6.5.90")
set(QT_MIN_VERSION "6.7.0") set(QT_MIN_VERSION "6.10.0")
set(KF6_MIN_VERSION "6.5.0") set(KF6_MIN_VERSION "6.22.0")
set(KDE_COMPILERSETTINGS_LEVEL "5.82") set(KDE_COMPILERSETTINGS_LEVEL "5.82")
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
@ -35,7 +34,9 @@ include(ECMMarkNonGuiExecutable)
include(ECMGenerateHeaders) include(ECMGenerateHeaders)
include(ECMFindQmlModule) include(ECMFindQmlModule)
include(ECMGenerateQmlTypes) include(ECMGenerateQmlTypes)
include(ECMConfiguredInstall)
include(ECMFindQmlModule) include(ECMFindQmlModule)
include(ECMQtDeclareLoggingCategory)
include(GenerateExportHeader) include(GenerateExportHeader)
include(KDEGitCommitHooks) include(KDEGitCommitHooks)
include(KDEClangFormat) include(KDEClangFormat)
@ -57,6 +58,10 @@ find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED
WaylandClient 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 find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS
I18n I18n
GlobalAccel GlobalAccel
@ -71,6 +76,7 @@ find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS
KCMUtils KCMUtils
Package Package
JobWidgets JobWidgets
WindowSystem
) )
find_package(Plasma CONFIG REQUIRED) find_package(Plasma CONFIG REQUIRED)
@ -78,16 +84,14 @@ find_package(PlasmaQuick CONFIG REQUIRED)
find_package(PlasmaActivities CONFIG REQUIRED) find_package(PlasmaActivities CONFIG REQUIRED)
find_package(KF6Screen CONFIG REQUIRED) find_package(KF6Screen CONFIG REQUIRED)
find_package(KWayland CONFIG REQUIRED) find_package(KWayland CONFIG REQUIRED)
find_package(KPipeWire ${PROJECT_DEP_VERSION} REQUIRED)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
find_package(QCoro6 REQUIRED COMPONENTS DBus) find_package(QCoro6 REQUIRED COMPONENTS DBus Qml)
qcoro_enable_coroutines() qcoro_enable_coroutines()
kde_enable_exceptions() 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(KF6KirigamiAddons 0.6 REQUIRED)
find_package(epoxy REQUIRED) find_package(epoxy REQUIRED)
find_package(XCB REQUIRED COMPONENTS XCB) find_package(XCB REQUIRED COMPONENTS XCB)
@ -96,6 +100,14 @@ find_package(KWin ${PROJECT_DEP_VERSION} REQUIRED COMPONENTS
) )
find_package(LayerShellQt REQUIRED) 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(LibKWorkspace CONFIG REQUIRED)
find_package(Libudev 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" 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) include(CheckIncludeFiles)
ecm_find_qmlmodule(org.kde.pipewire 0.1) 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(bin)
add_subdirectory(components) add_subdirectory(components)
add_subdirectory(containments) add_subdirectory(containments)
add_subdirectory(devices)
add_subdirectory(quicksettings) add_subdirectory(quicksettings)
add_subdirectory(kcms) add_subdirectory(kcms)
add_subdirectory(kded) add_subdirectory(kded)
@ -127,6 +146,8 @@ if(BUILD_TESTING)
add_subdirectory(tests) add_subdirectory(tests)
endif() endif()
install(FILES org.kde.plasma.mobileshell.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
find_program(PlasmaOpenSettings plasma-open-settings) find_program(PlasmaOpenSettings plasma-open-settings)
set_package_properties(PlasmaOpenSettings PROPERTIES set_package_properties(PlasmaOpenSettings PROPERTIES
URL https://invent.kde.org/plasma/kde-cli-tools/ URL https://invent.kde.org/plasma/kde-cli-tools/
@ -146,6 +167,6 @@ ki18n_install(po)
if (INSTALL_SYSTEMD_SERVICE) if (INSTALL_SYSTEMD_SERVICE)
install(FILES install(FILES
${CMAKE_CURRENT_SOURCE_DIR}/plasma-mobile.service ${CMAKE_CURRENT_SOURCE_DIR}/plasma-mobile.service
DESTINATION ${KDE_INSTALL_SYSTEMDUNITDIR} DESTINATION ${KDE_INSTALL_SYSTEMDUNITDIR}/system
) )
endif() endif()

View file

@ -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: 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. 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. 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
View 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 licensors permission is not necessary for any reasonfor example, because of any applicable exception or limitation to copyrightthen 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 Adapters 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 Adapters 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.

View file

@ -45,7 +45,7 @@ Dependencies:
* Milou (for search) * Milou (for search)
* Kirigami * Kirigami
* Kirigami Addons * Kirigami Addons
* hfd-service (optional: for vibrations) * feedbackd (optional: for vibrations)
To start the shell in a window, run: 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 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> <img src="https://invent.kde.org/plasma/plasma-mobile/-/wikis/uploads/19a607bb68faa76bbc9f888e33a3aa9a/konqi-calling.png" width=200px>

View file

@ -6,9 +6,10 @@ Exec=@CMAKE_INSTALL_FULL_LIBEXECDIR@/plasma-dbus-run-session-if-needed @CMAKE_IN
Type=Application Type=Application
DesktopNames=KDE DesktopNames=KDE
Name=Plasma Mobile (Development) Name=Plasma Mobile (Development)
Name[ar]=بلازما الجوال (التطوير)
Name[az]=Plasma Mobil (Tərtibat) Name[az]=Plasma Mobil (Tərtibat)
Name[ca]=Plasma Mobile (Desenvolupament) Name[ca]=Plasma Mobile (Desenvolupament)
Name[ca@valencia]=Plasma Mobile (Desenvolupament) Name[ca@valencia]=Plasma Mobile (desenvolupament)
Name[cs]=Plasma Mobile (Vývojová) Name[cs]=Plasma Mobile (Vývojová)
Name[de]=Plasma Mobile (Entwicklung) Name[de]=Plasma Mobile (Entwicklung)
Name[en_GB]=Plasma Mobile (Development) Name[en_GB]=Plasma Mobile (Development)
@ -25,7 +26,7 @@ Name[is]=Plasma Mobile (þróunarútgáfa)
Name[it]=Plasma Mobile (sviluppo) Name[it]=Plasma Mobile (sviluppo)
Name[ka]=Plasma Mobile (ბეტა) Name[ka]=Plasma Mobile (ბეტა)
Name[ko]=Plasma 모바일(개발) Name[ko]=Plasma 모바일(개발)
Name[lv]=Plasma Mobile (izstrādē) Name[lv]=Plasma Mobile (izstrādē)
Name[nl]=Plasma Mobile (ontwikkeling) Name[nl]=Plasma Mobile (ontwikkeling)
Name[nn]=Plasma Mobile (utvikling) Name[nn]=Plasma Mobile (utvikling)
Name[pa]= ਈਲ (ਵਿਕਾਸ) Name[pa]= ਈਲ (ਵਿਕਾਸ)
@ -33,16 +34,17 @@ Name[pl]=Przenośna Plazma (Rozwój)
Name[pt]=Plasma Mobile (Desenvolvimento) Name[pt]=Plasma Mobile (Desenvolvimento)
Name[pt_BR]=Plasma Mobile (desenvolvimento) Name[pt_BR]=Plasma Mobile (desenvolvimento)
Name[ru]=Plasma Mobile (разработка) Name[ru]=Plasma Mobile (разработка)
Name[sa]= ईल (विकास)
Name[sk]=Plasma Mobile (Vývoj) Name[sk]=Plasma Mobile (Vývoj)
Name[sl]=Plasma Mobile (Razvoj) Name[sl]=Plasma Mobile (Razvoj)
Name[sv]=Plasma mobil (utvecklingsversion) Name[sv]=Plasma mobil (utvecklingsversion)
Name[ta]=ி ி (ி) Name[ta]=ி ி (ி)
Name[tr]=Plasma Cep (Geliştirme) Name[tr]=Plasma Cep (Geliştirme)
Name[uk]=Мобільна Плазма (версія у розробці) Name[uk]=Мобільна Плазма (версія у розробці)
Name[x-test]=xxPlasma Mobile (Development)xx
Name[zh_CN]=Plasma Mobile (开发) Name[zh_CN]=Plasma Mobile (开发)
Name[zh_TW]=Plasma Name[zh_TW]=Plasma
Comment=Plasma Mobile by KDE Comment=Plasma Mobile by KDE
Comment[ar]=بلازما الجوال من كيدي
Comment[az]=KDE Plasma Mobil Comment[az]=KDE Plasma Mobil
Comment[ca]=Plasma Mobile, creat per la comunitat KDE Comment[ca]=Plasma Mobile, creat per la comunitat KDE
Comment[ca@valencia]=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[pt_BR]=Plasma Mobile do KDE
Comment[ro]=Plasma Mobile, de către KDE Comment[ro]=Plasma Mobile, de către KDE
Comment[ru]=KDE Plasma Mobile Comment[ru]=KDE Plasma Mobile
Comment[sa]=
Comment[sk]=Plasma Mobile od KDE Comment[sk]=Plasma Mobile od KDE
Comment[sl]=Plasma Mobile od KDE Comment[sl]=Plasma Mobile od KDE
Comment[sv]=Plasma mobil av KDE Comment[sv]=Plasma mobil av KDE
Comment[ta]=... ி ி Comment[ta]=... ி ி
Comment[tr]=KDE tarafından yapılan Plasma Cep Comment[tr]=KDE tarafından yapılan Plasma Cep
Comment[uk]=Мобільна Плазма від KDE Comment[uk]=Мобільна Плазма від KDE
Comment[x-test]=xxPlasma Mobile by KDExx
Comment[zh_CN]=KDE Plasma Mobile Comment[zh_CN]=KDE Plasma Mobile
Comment[zh_TW]= KDE Plasma Comment[zh_TW]= KDE Plasma
X-KDE-PluginInfo-Version=${PROJECT_VERSION} X-KDE-PluginInfo-Version=${PROJECT_VERSION}

View file

@ -32,7 +32,7 @@ Name[it]=Plasma Mobile
Name[ka]=Plasma Mobile Name[ka]=Plasma Mobile
Name[ko]=Plasma Name[ko]=Plasma
Name[lt]=Plasma mobilioji sąsaja Name[lt]=Plasma mobilioji sąsaja
Name[lv]=Plasma Mobile Name[lv]=Plasma Mobile
Name[nl]=Plasma Mobile Name[nl]=Plasma Mobile
Name[nn]=Plasma Mobile Name[nn]=Plasma Mobile
Name[pa]= Name[pa]=
@ -41,16 +41,17 @@ Name[pt]=Plasma Mobile
Name[pt_BR]=Plasma Mobile Name[pt_BR]=Plasma Mobile
Name[ro]=Plasma Mobile Name[ro]=Plasma Mobile
Name[ru]=Plasma Mobile Name[ru]=Plasma Mobile
Name[sa]=
Name[sk]=Plasma Mobile Name[sk]=Plasma Mobile
Name[sl]=Plasma Mobile Name[sl]=Plasma Mobile
Name[sv]=Plasma mobil Name[sv]=Plasma mobil
Name[ta]=ி ி Name[ta]=ி ி
Name[tr]=Plasma Cep Name[tr]=Plasma Cep
Name[uk]=Мобільна Плазма Name[uk]=Мобільна Плазма
Name[x-test]=xxPlasma Mobilexx
Name[zh_CN]=Plasma Mobile Name[zh_CN]=Plasma Mobile
Name[zh_TW]=Plasma Name[zh_TW]=Plasma
Comment=Plasma Mobile by KDE Comment=Plasma Mobile by KDE
Comment[ar]=بلازما الجوال من كيدي
Comment[az]=KDE Plasma Mobil Comment[az]=KDE Plasma Mobil
Comment[ca]=Plasma Mobile, creat per la comunitat KDE Comment[ca]=Plasma Mobile, creat per la comunitat KDE
Comment[ca@valencia]=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[pt_BR]=Plasma Mobile do KDE
Comment[ro]=Plasma Mobile, de către KDE Comment[ro]=Plasma Mobile, de către KDE
Comment[ru]=KDE Plasma Mobile Comment[ru]=KDE Plasma Mobile
Comment[sa]=
Comment[sk]=Plasma Mobile od KDE Comment[sk]=Plasma Mobile od KDE
Comment[sl]=Plasma Mobile od KDE Comment[sl]=Plasma Mobile od KDE
Comment[sv]=Plasma mobil av KDE Comment[sv]=Plasma mobil av KDE
Comment[ta]=... ி ி Comment[ta]=... ி ி
Comment[tr]=KDE tarafından yapılan Plasma Cep Comment[tr]=KDE tarafından yapılan Plasma Cep
Comment[uk]=Мобільна Плазма від KDE Comment[uk]=Мобільна Плазма від KDE
Comment[x-test]=xxPlasma Mobile by KDExx
Comment[zh_CN]=KDE Plasma Mobile Comment[zh_CN]=KDE Plasma Mobile
Comment[zh_TW]= KDE Plasma Comment[zh_TW]= KDE Plasma
X-KDE-PluginInfo-Version=${PROJECT_VERSION} X-KDE-PluginInfo-Version=${PROJECT_VERSION}

View file

@ -7,7 +7,6 @@
[ -f /etc/profile ] && . /etc/profile [ -f /etc/profile ] && . /etc/profile
export QT_QPA_PLATFORM=wayland
export QT_QPA_PLATFORMTHEME=KDE export QT_QPA_PLATFORMTHEME=KDE
export EGL_PLATFORM=wayland export EGL_PLATFORM=wayland
@ -17,6 +16,9 @@ export QT_QUICK_CONTROLS_MOBILE=true
export PLASMA_INTEGRATION_USE_PORTAL=1 export PLASMA_INTEGRATION_USE_PORTAL=1
export PLASMA_PLATFORM=phone:handset 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 coredumpd knows about the dumps, make sure drkonqi catches them
if grep -q '/systemd-coredump' /proc/sys/kernel/core_pattern if grep -q '/systemd-coredump' /proc/sys/kernel/core_pattern
then then
@ -28,4 +30,4 @@ QT_QPA_PLATFORM=offscreen plasma-mobile-envmanager --apply-settings
# start the shell # start the shell
export PLASMA_DEFAULT_SHELL=org.kde.plasma.mobileshell export PLASMA_DEFAULT_SHELL=org.kde.plasma.mobileshell
startplasma-wayland --xwayland --inputmethod maliit-keyboard startplasma-wayland

View file

@ -2,13 +2,16 @@
# SPDX-FileCopyrightText: 2017-2021 Marco Martin <mart@kde.org> # SPDX-FileCopyrightText: 2017-2021 Marco Martin <mart@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
add_subdirectory(waydroidintegrationplugin)
add_subdirectory(hapticsplugin) add_subdirectory(hapticsplugin)
add_subdirectory(mmplugin) add_subdirectory(mmplugin)
add_subdirectory(mobileshell) add_subdirectory(mobileshell)
add_subdirectory(mobileshellstate) add_subdirectory(mobileshellstate)
add_subdirectory(quicksettingsplugin) add_subdirectory(quicksettingsplugin)
add_subdirectory(rotationplugin)
add_subdirectory(windowplugin) add_subdirectory(windowplugin)
add_subdirectory(shellsettingsplugin) add_subdirectory(shellsettingsplugin)
add_subdirectory(wallpaperimageplugin) add_subdirectory(wallpaperimageplugin)
add_subdirectory(dpmsplugin) add_subdirectory(dpmsplugin)
add_subdirectory(screenbrightnessplugin) add_subdirectory(screenbrightnessplugin)
add_subdirectory(raiselockscreenplugin)

View file

@ -1,12 +1,15 @@
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org> # SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later # 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) ecm_add_qml_module(hapticsplugin URI org.kde.plasma.private.mobileshell.hapticsplugin GENERATE_PLUGIN_SOURCE)
target_sources(hapticsplugin PRIVATE target_sources(hapticsplugin PRIVATE
vibrationevent.h
vibrationmanager.cpp vibrationmanager.cpp
${DBUS_SRCS} ${dbusinterface_SRCS}
) )
target_link_libraries(hapticsplugin PRIVATE target_link_libraries(hapticsplugin PRIVATE
@ -14,6 +17,8 @@ target_link_libraries(hapticsplugin PRIVATE
Qt::DBus Qt::DBus
KF6::CoreAddons KF6::CoreAddons
KF6::I18n KF6::I18n
QCoro::DBus
QCoro::Qml
) )
ecm_finalize_qml_module(hapticsplugin) ecm_finalize_qml_module(hapticsplugin)

View file

@ -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>

View file

@ -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>

View 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>

View 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;
}

View file

@ -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 // SPDX-License-Identifier: GPL-2.0-or-later
#include "vibrationmanager.h" #include "vibrationmanager.h"
#include <QPointer>
VibrationManager::VibrationManager(QObject *parent) VibrationManager::VibrationManager(QObject *parent)
: 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. // Only create interface when needed.
if (!m_interface) { if (!m_interface) {
const auto objectPath = QStringLiteral("/com/lomiri/hfd"); const auto objectPath = QStringLiteral("/org/sigxcpu/Feedback");
m_interface = new com::lomiri::hfd::Vibrator("com.lomiri.hfd", objectPath, QDBusConnection::systemBus(), this); 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);
} }

View file

@ -3,10 +3,17 @@
#pragma once #pragma once
#include <QList>
#include <QObject> #include <QObject>
#include <qqmlregistration.h> #include <qqmlregistration.h>
#include "hfdinterface.h" #include "hapticinterface.h"
#include "vibrationevent.h"
#include <QCoroCore>
#include <QCoroDBusPendingReply>
#include <QCoroQml>
#include <QCoroQmlTask>
class VibrationManager : public QObject class VibrationManager : public QObject
{ {
@ -17,8 +24,9 @@ class VibrationManager : public QObject
public: public:
VibrationManager(QObject *parent = nullptr); VibrationManager(QObject *parent = nullptr);
Q_INVOKABLE void vibrate(int durationMs); QCoro::Task<void> vibrateTask(int durationMs);
Q_INVOKABLE QCoro::QmlTask vibrate(int durationMs);
private: private:
com::lomiri::hfd::Vibrator *m_interface{nullptr}; OrgSigxcpuFeedbackHapticInterface *m_interface{nullptr};
}; };

View file

@ -14,6 +14,7 @@ target_link_libraries(ppc-mmqmlplugin PRIVATE
KF6::NetworkManagerQt KF6::NetworkManagerQt
KF6::CoreAddons KF6::CoreAddons
KF6::I18n KF6::I18n
QCoro::DBus
) )

View file

@ -8,6 +8,8 @@
#include <NetworkManagerQt/Manager> #include <NetworkManagerQt/Manager>
#include <NetworkManagerQt/Settings> #include <NetworkManagerQt/Settings>
#include <NetworkManagerQt/Utils> #include <NetworkManagerQt/Utils>
#include <QDBusReply>
#include <QPointer>
#include <KUser> #include <KUser>
@ -197,11 +199,11 @@ void SignalIndicator::refreshProfiles()
Q_EMIT profileListChanged(); Q_EMIT profileListChanged();
} }
void SignalIndicator::activateProfile(const QString &connectionUni) QCoro::Task<void> SignalIndicator::activateProfile(const QString &connectionUni)
{ {
if (!m_nmModem) { if (!m_nmModem) {
qWarning() << "Cannot activate profile since there is no NetworkManager modem"; 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 << "."; 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) { if (!con) {
qDebug() << QStringLiteral("Connection") << connectionUni << QStringLiteral("not found."); qDebug() << QStringLiteral("Connection") << connectionUni << QStringLiteral("not found.");
return; co_return;
} }
// activate connection manually // activate connection manually
// despite the documentation saying otherwise, activateConnection seems to need the DBus path, not uuid of the connection // 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(), {}); QPointer<SignalIndicator> guard(this);
reply.waitForFinished(); QDBusReply<QDBusObjectPath> reply = co_await NetworkManager::activateConnection(con->path(), m_nmModem->uni(), {});
if (reply.isError()) {
if (!guard) {
co_return;
}
if (!reply.isValid()) {
qWarning() << QStringLiteral("Error activating connection:") << reply.error().message(); 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) { if (!m_nmModem) {
qWarning() << "Cannot add profile since there is no NetworkManager modem"; qWarning() << "Cannot add profile since there is no NetworkManager modem";
return; co_return;
} }
NetworkManager::ConnectionSettings::Ptr settings{new NetworkManager::ConnectionSettings(NetworkManager::ConnectionSettings::Gsm)}; 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); gsmSetting->setInitialized(true);
QDBusPendingReply<QDBusObjectPath> reply = NetworkManager::addAndActivateConnection(settings->toMap(), m_nmModem->uni(), {}); QPointer<SignalIndicator> guard(this);
reply.waitForFinished(); QDBusReply<QDBusObjectPath> reply = co_await NetworkManager::addAndActivateConnection(settings->toMap(), m_nmModem->uni(), {});
if (reply.isError()) {
if (!guard) {
co_return;
}
if (!reply.isValid()) {
qWarning() << "Error adding connection:" << reply.error().message(); qWarning() << "Error adding connection:" << reply.error().message();
} else { } else {
qDebug() << "Successfully added a new connection" << name << "with APN" << apn << "."; 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); NetworkManager::Connection::Ptr con = NetworkManager::findConnectionByUuid(connectionUni);
if (!con) { if (!con) {
qWarning() << "Could not find connection" << connectionUni << "to update!"; qWarning() << "Could not find connection" << connectionUni << "to update!";
return; co_return;
} }
QDBusPendingReply reply = con->remove(); QPointer<SignalIndicator> guard(this);
reply.waitForFinished(); QDBusPendingReply reply = co_await con->remove();
if (reply.isError()) {
if (!guard) {
co_return;
}
if (!reply.isValid()) {
qWarning() << "Error removing connection" << reply.error().message(); qWarning() << "Error removing connection" << reply.error().message();
} }
} }
void SignalIndicator::updateProfile(const QString &connectionUni, QCoro::Task<void> SignalIndicator::updateProfile(const QString &connectionUni,
const QString &name, const QString &name,
const QString &apn, const QString &apn,
const QString &username, const QString &username,
const QString &password, const QString &password,
const QString &networkType) const QString &networkType)
{ {
NetworkManager::Connection::Ptr con = NetworkManager::findConnectionByUuid(connectionUni); NetworkManager::Connection::Ptr con = NetworkManager::findConnectionByUuid(connectionUni);
if (!con) { if (!con) {
qWarning() << "Could not find connection" << connectionUni << "to update!"; qWarning() << "Could not find connection" << connectionUni << "to update!";
return; co_return;
} }
NetworkManager::ConnectionSettings::Ptr conSettings = con->settings(); NetworkManager::ConnectionSettings::Ptr conSettings = con->settings();
if (!conSettings) { if (!conSettings) {
qWarning() << "Could not find connection settings for" << connectionUni << "to update!"; qWarning() << "Could not find connection settings for" << connectionUni << "to update!";
return; co_return;
} }
conSettings->setId(name); conSettings->setId(name);
@ -309,9 +326,14 @@ void SignalIndicator::updateProfile(const QString &connectionUni,
gsmSetting->setInitialized(true); gsmSetting->setInitialized(true);
QDBusPendingReply reply = con->update(conSettings->toMap()); QPointer<SignalIndicator> guard(this);
reply.waitForFinished(); QDBusPendingReply reply = co_await con->update(conSettings->toMap());
if (reply.isError()) {
if (!guard) {
co_return;
}
if (!reply.isValid()) {
qWarning() << "Error updating connection settings for" << connectionUni << ":" << reply.error().message() << "."; qWarning() << "Error updating connection settings for" << connectionUni << ":" << reply.error().message() << ".";
} else { } else {
qDebug() << "Successfully updated connection settings" << connectionUni << "."; qDebug() << "Successfully updated connection settings" << connectionUni << ".";

View file

@ -9,6 +9,7 @@
#include <NetworkManagerQt/Connection> #include <NetworkManagerQt/Connection>
#include <NetworkManagerQt/ModemDevice> #include <NetworkManagerQt/ModemDevice>
#include <QCoroDBusPendingReply>
#include <QObject> #include <QObject>
#include <qqmlregistration.h> #include <qqmlregistration.h>
@ -53,10 +54,10 @@ public:
// connection profiles // connection profiles
QList<ProfileSettings *> &profileList(); QList<ProfileSettings *> &profileList();
void refreshProfiles(); void refreshProfiles();
Q_INVOKABLE void activateProfile(const QString &connectionUni); Q_INVOKABLE QCoro::Task<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 QCoro::Task<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 QCoro::Task<void> removeProfile(const QString &connectionUni);
Q_INVOKABLE void updateProfile(const QString &connectionUni, Q_INVOKABLE QCoro::Task<void> updateProfile(const QString &connectionUni,
const QString &name, const QString &name,
const QString &apn, const QString &apn,
const QString &username, const QString &username,

View file

@ -11,27 +11,136 @@ set(mobileshellplugin_SRCS
notifications/notificationthumbnailer.cpp notifications/notificationthumbnailer.cpp
notifications/notificationfilemenu.cpp notifications/notificationfilemenu.cpp
notifications/notificationfileinfo.cpp notifications/notificationfileinfo.cpp
masklayer/masklayer.cpp
masklayer/maskmanager.cpp
) )
target_include_directories(mobileshellplugin PRIVATE components) target_include_directories(mobileshellplugin PRIVATE components)
target_include_directories(mobileshellplugin PRIVATE notifications) target_include_directories(mobileshellplugin PRIVATE notifications)
target_include_directories(mobileshellplugin PRIVATE masklayer)
target_sources(mobileshellplugin PRIVATE ${mobileshellplugin_SRCS}) target_sources(mobileshellplugin PRIVATE ${mobileshellplugin_SRCS})
# Singleton declarations # Singleton declarations
set_source_files_properties(qml/components/AppLaunch.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE) set_source_files_properties(
set_source_files_properties(qml/components/Constants.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE) qml/components/AppLaunch.qml
set_source_files_properties(qml/dataproviders/AudioInfo.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE) qml/components/Constants.qml
set_source_files_properties(qml/dataproviders/BatteryInfo.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE) qml/dataproviders/AudioInfo.qml
set_source_files_properties(qml/dataproviders/BluetoothInfo.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE) qml/dataproviders/BatteryInfo.qml
set_source_files_properties(qml/dataproviders/SignalStrengthInfo.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE) qml/dataproviders/BluetoothInfo.qml
set_source_files_properties(qml/notificationpopup/NotificationPopupProviderLoader.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE) qml/dataproviders/NetworkInfo.qml
set_source_files_properties(qml/volumeosd/VolumeOSDProviderLoader.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE) qml/dataproviders/SignalStrengthInfo.qml
qml/popups/PopupProviderLoader.qml
# Include qml and js files within ./qml/ PROPERTIES
file(GLOB_RECURSE _qml_sources QT_QML_SINGLETON_TYPE TRUE
"qml/*.qml" )
"qml/*.js"
# 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 target_link_libraries(mobileshellplugin

View 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;
}

View 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;
};

View 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);
}

View 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;
};

View file

@ -12,18 +12,20 @@ import QtQuick.Window 2.2
import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.nanoshell 2.0 as NanoShell 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.kirigami 2.20 as Kirigami
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
import org.kde.plasma.private.mobileshell as MobileShell
import 'actiondrawer/private'
Item { Item {
id: root 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 * 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 property bool intendedToBeVisible: false
@ -59,6 +61,19 @@ Item {
*/ */
property real offset: 0 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. * Whether the panel is being dragged.
*/ */
@ -80,15 +95,10 @@ Item {
*/ */
property int direction: MobileShell.Direction.None 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). * 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. * 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() signal runPendingNotificationAction()
onOpenedChanged: {
if (opened) swipeArea.focus = true;
}
property real oldOffset property real oldOffset
onOffsetChanged: { onOffsetChanged: {
if (offset < 0) { if (offset < 0) {
@ -135,13 +141,13 @@ Item {
} }
root.direction = (oldOffset === offset) root.direction = (oldOffset === offset)
? MobileShell.Direction.None ? MobileShell.Direction.None
: (offset > oldOffset ? MobileShell.Direction.Down : MobileShell.Direction.Up); : (offset > oldOffset ? MobileShell.Direction.Down : MobileShell.Direction.Up);
oldOffset = offset; oldOffset = offset;
// close panel immediately after panel is not shown, and the flickable is not being dragged // 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 = ""; root.state = "";
offset = 0; offset = 0;
focus = false; 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() { function cancelAnimations() {
root.state = ""; root.state = "";
} }
@ -249,7 +264,9 @@ Item {
SequentialAnimation { SequentialAnimation {
PropertyAnimation { PropertyAnimation {
id: drawerAnimation 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 { ScriptAction {
script: { script: {
@ -267,45 +284,12 @@ Item {
} }
} }
MobileShell.SwipeArea { // action drawer ui content
id: swipeArea ContentContainer {
mode: MobileShell.SwipeArea.VerticalOnly id: contentContainer
anchors.fill: parent anchors.fill: parent
function startSwipe() { actionDrawer: root
root.cancelAnimations(); quickSettingsModel: root.quickSettingsModel
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
}
} }
} }

View file

@ -40,7 +40,7 @@ Window {
color: "transparent" color: "transparent"
// set input to transparent when closing to prevent window from taking unwanted touch inputs // 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: { onVisibleChanged: {
if (visible) { if (visible) {

View file

@ -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();
}
}

View file

@ -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
}
}
}

View file

@ -13,6 +13,7 @@ import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.core as PlasmaCore import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.components 3.0 as PC3 import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.private.mobileshell.screenbrightnessplugin as ScreenBrightness import org.kde.plasma.private.mobileshell.screenbrightnessplugin as ScreenBrightness
import org.kde.plasma.private.mobileshell as MobileShell
Item { Item {
id: root id: root
@ -22,8 +23,8 @@ Item {
property double brightnessPressedValue: 1 property double brightnessPressedValue: 1
Behavior on brightnessPressedValue { Behavior on brightnessPressedValue {
NumberAnimation { NumberAnimation {
duration: Kirigami.Units.longDuration duration: Kirigami.Units.longDuration * 2
easing.type: Easing.OutQuad easing.type: Easing.InOutQuad
} }
} }
@ -31,15 +32,18 @@ Item {
id: screenBrightness id: screenBrightness
} }
Rectangle { MobileShell.PanelBackground {
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: -Kirigami.Units.smallSpacing anchors.leftMargin: -Kirigami.Units.smallSpacing
anchors.rightMargin: -Kirigami.Units.smallSpacing anchors.rightMargin: -Kirigami.Units.smallSpacing
anchors.topMargin: -Kirigami.Units.smallSpacing * 2 anchors.topMargin: -Kirigami.Units.smallSpacing * 2
anchors.bottomMargin: -Kirigami.Units.smallSpacing * 2 anchors.bottomMargin: -Kirigami.Units.smallSpacing * 2
color: Kirigami.Theme.backgroundColor panelType: MobileShell.PanelBackground.PanelType.Base
radius: Kirigami.Units.cornerRadius flatten: root.brightnessPressedValue
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
} }
RowLayout { RowLayout {
@ -77,7 +81,7 @@ Item {
Timer { Timer {
id: brightnessPressedTimer id: brightnessPressedTimer
interval: 200 interval: 100
repeat: false repeat: false
onTriggered: { onTriggered: {
if (brightnessSlider.pressed) { if (brightnessSlider.pressed) {

View file

@ -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
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -28,7 +28,6 @@ Item {
property alias quickSettings: quickSettingsDrawer.quickSettings property alias quickSettings: quickSettingsDrawer.quickSettings
property alias statusBar: quickSettingsDrawer.statusBar property alias statusBar: quickSettingsDrawer.statusBar
property alias mediaControlsWidget: quickSettingsDrawer.mediaControlsWidget property alias mediaControlsWidget: quickSettingsDrawer.mediaControlsWidget
property alias notificationsWidget: notificationWidgetProxy.contentItem
Kirigami.Theme.colorSet: Kirigami.Theme.View Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
@ -39,19 +38,20 @@ Item {
MobileShell.QuickSettingsDrawer { MobileShell.QuickSettingsDrawer {
id: quickSettingsDrawer id: quickSettingsDrawer
z: 1 // ensure it's above notifications
// physically move the drawer when between closed <-> pinned mode // physically move the drawer when between closed <-> pinned mode
readonly property real offsetHeight: actionDrawer.openToPinnedMode ? minimizedQuickSettingsOffset : maximizedQuickSettingsOffset readonly property real offsetHeight: actionDrawer.openToPinnedMode ? minimizedQuickSettingsOffset : maximizedQuickSettingsOffset
anchors.topMargin: Math.min(root.actionDrawer.offset - offsetHeight, 0) anchors {
anchors.top: parent.top topMargin: Math.min(root.actionDrawer.offsetResistance - offsetHeight, 0)
anchors.left: parent.left top: parent.top
anchors.right: parent.right left: parent.left
right: parent.right
}
actionDrawer: root.actionDrawer actionDrawer: root.actionDrawer
// opacity and move animation (disabled when openToPinnedMode is false) // 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 property real totalOffsetDist: maximizedQuickSettingsOffset - minimizedQuickSettingsOffset
minimizedToFullProgress: actionDrawer.openToPinnedMode ? (actionDrawer.opened ? applyMinMax(offsetDist / totalOffsetDist) : 0) : 1 minimizedToFullProgress: actionDrawer.openToPinnedMode ? (actionDrawer.opened ? applyMinMax(offsetDist / totalOffsetDist) : 0) : 1
@ -65,33 +65,12 @@ Item {
addedHeight: { addedHeight: {
if (!actionDrawer.openToPinnedMode) { if (!actionDrawer.openToPinnedMode) {
// if pinned mode disabled, just go to full height // if pinned mode disabled, just go to full height
let progress = (root.actionDrawer.offset - maximizedQuickSettingsOffset) / (quickSettingsDrawer.maxAddedHeight * 4); return Math.max(maximizedQuickSettingsOffset - minimizedQuickSettingsOffset, root.actionDrawer.offsetResistance - minimizedQuickSettingsOffset)
let effectProgress = Math.atan(Math.max(0, progress));
return (quickSettingsDrawer.maxAddedHeight * effectProgress) + quickSettingsDrawer.maxAddedHeight;
} else if (!actionDrawer.opened) { } else if (!actionDrawer.opened) {
// over-scroll effect for initial opening return Math.max(0, root.actionDrawer.offsetResistance - minimizedQuickSettingsOffset)
let progress = (root.actionDrawer.offset - minimizedQuickSettingsOffset) / quickSettingsDrawer.maxAddedHeight;
let effectProgress = Math.atan(Math.max(0, progress));
return quickSettingsDrawer.maxAddedHeight * 0.25 * effectProgress;
} else { } else {
// over-scroll effect for full drawer return Math.max(0, root.actionDrawer.offsetResistance - minimizedQuickSettingsOffset)
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));
} }
} }
} }
MobileShell.BaseItem {
id: notificationWidgetProxy
anchors {
top: quickSettingsDrawer.bottom
bottom: parent.bottom
left: parent.left
right: parent.right
}
opacity: applyMinMax(root.actionDrawer.offset / root.minimizedQuickSettingsOffset)
}
} }

View file

@ -11,6 +11,7 @@ import QtQuick.Layouts 1.1
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import org.kde.plasma.private.mobileshell as MobileShell 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.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
import org.kde.kirigami 2.20 as Kirigami import org.kde.kirigami 2.20 as Kirigami
@ -20,13 +21,15 @@ import org.kde.kirigami 2.20 as Kirigami
*/ */
Item { Item {
id: root 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 var actionDrawer
required property QS.QuickSettingsModel quickSettingsModel 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 real columnWidth: Math.floor(width / columns)
readonly property int minimizedColumns: Math.round(Math.min(8, Math.max(5, width / intendedMinimizedColumnWidth))) readonly property int minimizedColumns: Math.round(Math.min(8, Math.max(5, width / intendedMinimizedColumnWidth)))
readonly property real minimizedColumnWidth: Math.floor(width / minimizedColumns) readonly property real minimizedColumnWidth: Math.floor(width / minimizedColumns)
@ -96,28 +99,12 @@ Item {
sourceModel: root.quickSettingsModel sourceModel: root.quickSettingsModel
pageSize: Math.min(root.pageSize, root.minimizedColumns) // HACK: just root.minimizedColumns appears to end up with an empty model? 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 required property var modelData
implicitHeight: root.minimizedRowHeight asynchronous: true
implicitWidth: root.minimizedColumnWidth
horizontalPadding: (width - Kirigami.Units.gridUnit * 3) / 2
verticalPadding: (height - Kirigami.Units.gridUnit * 3) / 2
contentItem: QuickSettingsMinimizedDelegate { sourceComponent: quickSettingComponentMinimized
restrictedPermissions: actionDrawer.restrictedPermissions
text: modelData.text
status: modelData.status
icon: modelData.icon
enabled: modelData.enabled
settingsCommand: modelData.settingsCommand
toggleFunction: modelData.toggle
onCloseRequested: {
actionDrawer.close();
}
}
} }
} }
} }
@ -140,6 +127,9 @@ Item {
SwipeView { SwipeView {
id: 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.fillWidth: true
Layout.preferredHeight: rowCount * rowHeight Layout.preferredHeight: rowCount * rowHeight
@ -163,7 +153,7 @@ Item {
asynchronous: true asynchronous: true
sourceComponent: quickSettingComponent sourceComponent: quickSettingComponentFull
} }
} }
} }
@ -211,9 +201,36 @@ Item {
} }
} }
// Quick setting component // Quick setting component minimized
Component { 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 { MobileShell.BaseItem {
height: root.rowHeight height: root.rowHeight

View file

@ -34,18 +34,29 @@ MobileShell.BaseItem {
// set by children // set by children
property var iconItem 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 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 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 enabledButtonPressedColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.4);
readonly property color disabledButtonColor: Kirigami.Theme.backgroundColor readonly property color disabledButtonColor: Kirigami.Theme.backgroundColor
readonly property color disabledButtonPressedColor: Qt.darker(disabledButtonColor, 1.1) 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 // scale animation on press
property real zoomScale: 1 property real zoomScale: 1
Behavior on zoomScale { Behavior on zoomScale {
NumberAnimation { NumberAnimation {
duration: 200 duration: Kirigami.Units.longDuration
easing.type: Easing.OutExpo easing.type: Easing.OutExpo
} }
} }

View file

@ -39,7 +39,7 @@ MobileShell.BaseItem {
/** /**
* Height of just the QuickSettings component in minimized mode. * 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. * Progress of showing the full quick settings view from pinned.
@ -57,10 +57,50 @@ MobileShell.BaseItem {
rightPadding: Kirigami.Units.smallSpacing rightPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing * 4 bottomPadding: Kirigami.Units.smallSpacing * 4
background: KSvg.FrameSvgItem { Kirigami.Theme.inherit: false
enabledBorders: KSvg.FrameSvgItem.BottomBorder Kirigami.Theme.colorSet: Kirigami.Theme.Window
imagePath: "widgets/background"
background: Item {
opacity: brightnessPressedValue 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 { contentItem: Item {

View file

@ -41,6 +41,7 @@ QuickSettingsDelegate {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: Kirigami.Units.cornerRadius radius: Kirigami.Units.cornerRadius
border.pixelAligned: false
border.width: 1 border.width: 1
border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor
color: { color: {

View file

@ -40,6 +40,8 @@ QuickSettingsDelegate {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: Kirigami.Units.cornerRadius radius: Kirigami.Units.cornerRadius
border.pixelAligned: false
border.width: 1
border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor
color: { color: {
if (root.enabled) { if (root.enabled) {

View file

@ -4,6 +4,7 @@
import QtQuick import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Effects
import org.kde.kirigami 2.12 as Kirigami import org.kde.kirigami 2.12 as Kirigami
import org.kde.ksvg 1.0 as KSvg import org.kde.ksvg 1.0 as KSvg
@ -43,10 +44,17 @@ MobileShell.BaseItem {
rightPadding: Kirigami.Units.smallSpacing * 4 rightPadding: Kirigami.Units.smallSpacing * 4
bottomPadding: Kirigami.Units.smallSpacing * 4 bottomPadding: Kirigami.Units.smallSpacing * 4
background: KSvg.FrameSvgItem { Kirigami.Theme.inherit: false
enabledBorders: KSvg.FrameSvgItem.AllBorders Kirigami.Theme.colorSet: Kirigami.Theme.Window
imagePath: "widgets/background"
background: MobileShell.PanelBackground {
anchors.fill: parent
anchors.margins: Kirigami.Units.largeSpacing
panelType: MobileShell.PanelBackground.PanelType.Base
opacity: brightnessPressedValue opacity: brightnessPressedValue
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
} }
contentItem: Item { contentItem: Item {
@ -84,7 +92,8 @@ MobileShell.BaseItem {
Layout.maximumWidth: column.width 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 { Handle {

View file

@ -5,6 +5,7 @@ import QtQuick
import org.kde.kirigami 2.20 as Kirigami import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings 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 // 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). // 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 pragma Singleton
QtObject { QtObject {
readonly property real topPanelHeight: Math.round(Kirigami.Units.gridUnit * ShellSettings.Settings.statusBarScaleFactor / 2) * 2 + Kirigami.Units.smallSpacing id: root
readonly property real navigationPanelThickness: ShellSettings.Settings.navigationPanelEnabled ? Kirigami.Units.gridUnit * 2 : 0 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 { 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: // TODO: we have this disabled for now, we might consider just removing this feature entirely due to it causing several issues:

View file

@ -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
}
}

View file

@ -1,64 +1,119 @@
// SPDX-FileCopyrightText: 2022 Yari Polla <skilvingr@gmail.com> // SPDX-FileCopyrightText: 2022 Yari Polla <skilvingr@gmail.com>
// SPDX-FileCopyrightText: 2025 Micah Stanley <stanleymicah@proton.me>
// SPDX-License-Identifier: LGPL-2.0-or-later // SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import org.kde.kirigami 2.20 as Kirigami import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.components 3.0 as PlasmaComponents
import Qt5Compat.GraphicalEffects
/** /**
* This is a simple marquee (flowing) label based on PlasmaComponents Label. * This is a simple marquee (flowing) label based on PlasmaComponents Label.
*/ */
PlasmaComponents.Label { OpacityMask {
id: root id: root
height: row.height
// label values
required property string inputText 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 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 // update animation values and text positions whenever the label overflows or changes
readonly property int longDuration: 300 onFilteredTextChanged: if (root.charactersOverflowing) { textAnimationLoop.restart() }
readonly property int waitDuration: 900 onCharactersOverflowingChanged: if (charactersOverflowing) { row.scrollPosition = 0 }
readonly property int charactersOverflow: Math.ceil((txtMeter.advanceWidth - root.width) / (txtMeter.advanceWidth / filteredText.length)) Item {
property int step: 0 id: rowContaner
anchors.fill: parent
height: row.height
opacity: 0 // we display with the opacity gradient below
TextMetrics { // use two identical labels for scrolling so we can give the illusion of infinite scrolling
id: txtMeter RowLayout {
font: root.font id: row
text: filteredText anchors.left: parent.left
} anchors.right: parent.right
anchors.top: parent.top
Timer { property real scrollPosition: 0
property bool paused: false
interval: root.interval transform: [
running: visible && charactersOverflow > 0 Translate {
repeat: true x: row.scrollPosition
onTriggered: {
if (paused) {
if (step != 0) {
interval = root.longDuration;
step = 0;
} else {
interval = root.interval;
paused = false;
} }
} else { ]
step = (step + 1) % filteredText.length;
if (step === charactersOverflow) { spacing: 32
interval = root.waitDuration;
paused = true; 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: { PlasmaComponents.Label {
if (!running) { // hide this label when the text is not overflowing so the user never sees both labels
step = 0; 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' }
}
}
} }

View 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
}
}

View file

@ -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()
}
}
}
}
}

View file

@ -14,6 +14,8 @@ Rectangle {
property int screen property int screen
property var maximizedTracker property var maximizedTracker
readonly property bool isShowing: height > 0
// Smooth animation for colored rectangle // Smooth animation for colored rectangle
NumberAnimation on height { NumberAnimation on height {
id: heightAnim id: heightAnim
@ -29,6 +31,7 @@ Rectangle {
function onShowingWindowChanged() { function onShowingWindowChanged() {
root.color = 'transparent'; root.color = 'transparent';
root.height = 0;
} }
} }

View file

@ -73,8 +73,8 @@ Item {
SequentialAnimation { SequentialAnimation {
id: openAnimComplex id: openAnimComplex
// slight pause to give slower devices time to catch up when the item becomes visible // pause for background color to catch up
PauseAnimation { duration: 20 } PauseAnimation { duration: 1 }
ParallelAnimation { ParallelAnimation {
id: parallelAnim id: parallelAnim
@ -167,10 +167,9 @@ Item {
id: background id: background
anchors.fill: parent anchors.fill: parent
// Tint the background color if a dark theme is being used // Tint the background color so that it is less prominent
color: Kirigami.ColorUtils.brightnessForColor(Kirigami.Theme.backgroundColor) === Kirigami.ColorUtils.Dark ? // This avoids flashing the user all of a sudden with bright colors
Kirigami.ColorUtils.tintWithAlpha(colorGenerator.dominant, Kirigami.Theme.backgroundColor, 0.7) : color: Kirigami.ColorUtils.tintWithAlpha(colorGenerator.dominant, Kirigami.Theme.backgroundColor, 0.7)
colorGenerator.dominant
Kirigami.ImageColors { Kirigami.ImageColors {
id: colorGenerator id: colorGenerator
@ -225,4 +224,4 @@ Item {
} }
} }
} }
} }

View file

@ -6,6 +6,7 @@ pragma Singleton
import QtQuick import QtQuick
import org.kde.plasma.private.volume import org.kde.plasma.private.volume
import org.kde.plasma.private.mobileshell as MobileShell
QtObject { QtObject {
id: root id: root
@ -31,57 +32,13 @@ QtObject {
// the maximum volume amount // the maximum volume amount
readonly property int maxVolumeValue: maxVolumePercent * PulseAudio.NormalVolume / 100 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) // 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) { function isDummyOutput(output) {
return output && output.name === dummyOutputName; 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) { function iconName(volume, muted, prefix) {
if (!prefix) { if (!prefix) {
prefix = "audio-volume"; prefix = "audio-volume";
@ -99,33 +56,4 @@ QtObject {
} }
return icon; 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);
}
}
}
} }

View 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 {}
}

View 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
}
}
}

View 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
}
}

View file

@ -103,9 +103,14 @@ Item {
itemContainer.zoomIn(); itemContainer.zoomIn();
} }
} }
Component.onCompleted: {
// Set initial item container state
onIsTaskSwitcherVisibleChanged();
}
} }
//END API implementation //END API implementation
Component.onCompleted: { Component.onCompleted: {
// determine the margins used // determine the margins used
@ -128,7 +133,7 @@ Item {
// animations // animations
opacity: 0 opacity: 0
property real zoomScale: 1 property real scaleAmount: 1
readonly property real zoomScaleOut: 0.8 readonly property real zoomScaleOut: 0.8
@ -150,7 +155,7 @@ Item {
function zoomOutImmediately() { function zoomOutImmediately() {
scaleAnim.stop(); scaleAnim.stop();
opacityAnim.stop(); opacityAnim.stop();
zoomScale = zoomScaleOut; scaleAmount = zoomScaleOut;
opacity = 0; opacity = 0;
} }
@ -160,7 +165,7 @@ Item {
running: false running: false
} }
NumberAnimation on zoomScale { NumberAnimation on scaleAmount {
id: scaleAnim id: scaleAnim
duration: 600 duration: 600
running: false running: false
@ -179,8 +184,8 @@ Item {
transform: Scale { transform: Scale {
origin.x: itemContainer.width / 2; origin.x: itemContainer.width / 2;
origin.y: itemContainer.height / 2; origin.y: itemContainer.height / 2;
xScale: itemContainer.zoomScale xScale: itemContainer.scaleAmount
yScale: itemContainer.zoomScale yScale: itemContainer.scaleAmount
} }
} }

View file

@ -9,13 +9,15 @@ import QtQuick.Controls as Controls
import org.kde.kirigami 2.20 as Kirigami import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.wallpapers.image 2.0 as Wallpaper 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.wallpaperimageplugin as WallpaperImagePlugin
import org.kde.plasma.private.mobileshell as MobileShell
Controls.Drawer { Controls.Drawer {
id: imageWallpaperDrawer id: imageWallpaperDrawer
dragMargin: 0 dragMargin: 0
property MobileShell.MaskManager maskManager
required property bool horizontal required property bool horizontal
signal wallpaperSettingsRequested() signal wallpaperSettingsRequested()
@ -41,18 +43,18 @@ Controls.Drawer {
anchors.leftMargin: imageWallpaperDrawer.leftMargin anchors.leftMargin: imageWallpaperDrawer.leftMargin
anchors.rightMargin: imageWallpaperDrawer.rightMargin anchors.rightMargin: imageWallpaperDrawer.rightMargin
anchors.bottomMargin: imageWallpaperDrawer.bottomMargin anchors.bottomMargin: imageWallpaperDrawer.bottomMargin
orientation: imageWallpaperDrawer.horizontal ? ListView.Vertical : ListView.Horizontal orientation: imageWallpaperDrawer.horizontal ? ListView.Vertical : ListView.Horizontal
keyNavigationEnabled: true keyNavigationEnabled: true
highlightFollowsCurrentItem: true highlightFollowsCurrentItem: true
snapMode: ListView.SnapToItem snapMode: ListView.SnapToItem
model: imageWallpaper.wallpaperModel model: imageWallpaper.wallpaperModel
// onCountChanged: currentIndex = Math.min(model.indexOf(configDialog.wallpaperConfiguration["Image"]), model.rowCount()-1)
headerPositioning: ListView.InlineHeader headerPositioning: ListView.InlineHeader
header: Controls.ItemDelegate { header: Controls.ItemDelegate {
id: openSettings id: openSettings
width: imageWallpaperDrawer.horizontal ? parent.width : height * (imageWallpaperDrawer.width / imageWallpaperDrawer.Screen.height) width: imageWallpaperDrawer.horizontal ? wallpapersView.width : height * (imageWallpaperDrawer.width / imageWallpaperDrawer.Screen.height)
height: imageWallpaperDrawer.horizontal ? width / (imageWallpaperDrawer.Screen.width / imageWallpaperDrawer.Screen.height) : parent.height height: imageWallpaperDrawer.horizontal ? width / (imageWallpaperDrawer.Screen.width / imageWallpaperDrawer.Screen.height) : wallpapersView.height
padding: Kirigami.Units.gridUnit / 2 padding: Kirigami.Units.gridUnit / 2
leftPadding: padding leftPadding: padding
topPadding: padding topPadding: padding
@ -60,10 +62,14 @@ Controls.Drawer {
bottomPadding: padding bottomPadding: padding
background: Rectangle { background: Rectangle {
radius: Kirigami.Units.cornerRadius
color: Qt.rgba(255, 255, 255, (openSettings.down || openSettings.highlighted) ? 0.3 : 0.2) color: Qt.rgba(255, 255, 255, (openSettings.down || openSettings.highlighted) ? 0.3 : 0.2)
radius: Kirigami.Units.gridUnit / 4
anchors.fill: parent Component.onCompleted: {
anchors.margins: Kirigami.Units.gridUnit / 4 if (maskManager) {
maskManager.assignToMask(this)
}
}
} }
contentItem: Item { contentItem: Item {
@ -72,6 +78,7 @@ Controls.Drawer {
implicitHeight: Kirigami.Units.iconSizes.large implicitHeight: Kirigami.Units.iconSizes.large
implicitWidth: Kirigami.Units.iconSizes.large implicitWidth: Kirigami.Units.iconSizes.large
source: 'list-add' source: 'list-add'
color: 'white'
} }
} }
@ -80,13 +87,18 @@ Controls.Drawer {
} }
delegate: Controls.ItemDelegate { delegate: Controls.ItemDelegate {
width: imageWallpaperDrawer.horizontal ? parent.width : height * (imageWallpaperDrawer.width / imageWallpaperDrawer.Screen.height) id: delegate
height: imageWallpaperDrawer.horizontal ? width / (imageWallpaperDrawer.Screen.width / imageWallpaperDrawer.Screen.height) : parent.height
padding: wallpapersView.currentIndex === index ? Kirigami.Units.gridUnit / 4 : Kirigami.Units.gridUnit / 2 width: imageWallpaperDrawer.horizontal ? wallpapersView.width : height * (imageWallpaperDrawer.width / imageWallpaperDrawer.Screen.height)
leftPadding: padding height: imageWallpaperDrawer.horizontal ? width / (imageWallpaperDrawer.Screen.width / imageWallpaperDrawer.Screen.height) : (wallpapersView ? wallpapersView.height : 0)
topPadding: padding padding: Kirigami.Units.largeSpacing - (wallpapersView.currentIndex === index ? Kirigami.Units.smallSpacing : 0)
rightPadding: padding property real scaleAmount: wallpapersView.currentIndex === index ? 0 : Kirigami.Units.smallSpacing
bottomPadding: padding Behavior on scaleAmount {
NumberAnimation {
duration: Kirigami.Units.longDuration
easing.type: Easing.InOutQuad
}
}
Behavior on padding { Behavior on padding {
NumberAnimation { NumberAnimation {
duration: Kirigami.Units.longDuration 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 property bool isCurrent: WallpaperImagePlugin.WallpaperPlugin.homescreenWallpaperPath == model.path
onIsCurrentChanged: { onIsCurrentChanged: {
if (isCurrent) { if (isCurrent) {
@ -111,34 +132,32 @@ Controls.Drawer {
visible: !walliePreview.visible visible: !walliePreview.visible
} }
Addons.QPixmapItem { Image {
id: walliePreview id: walliePreview
visible: model.screenshot != null
anchors.fill: parent anchors.fill: parent
smooth: true visible: model.source != null
pixmap: model.screenshot asynchronous: true
cache: false
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
source: model.preview
sourceSize: Qt.size(width * 3, height * 3)
} }
} }
onClicked: { onClicked: {
WallpaperImagePlugin.WallpaperPlugin.setHomescreenWallpaper(model.path); WallpaperImagePlugin.WallpaperPlugin.setHomescreenWallpaper(model.source);
} }
Keys.onReturnPressed: { Keys.onReturnPressed: {
clicked(); clicked();
} }
background: Item {
Rectangle { background: Rectangle {
anchors { color: Qt.rgba(255, 255, 255, (delegate.down || delegate.highlighted) ? 0.4 : 0.2)
fill: parent radius: Kirigami.Units.cornerRadius
margins: wallpapersView.currentIndex === index ? 0 : Kirigami.Units.gridUnit / 4
Behavior on margins { Component.onCompleted: {
NumberAnimation { if (maskManager) {
duration: Kirigami.Units.longDuration maskManager.assignToMask(this)
easing.type: Easing.InOutQuad
}
}
} }
radius: Kirigami.Units.gridUnit / 4
} }
} }
} }

View 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
}
}
}

View file

@ -29,11 +29,14 @@ Item {
property NavigationPanelAction leftCornerAction property NavigationPanelAction leftCornerAction
property NavigationPanelAction rightCornerAction property NavigationPanelAction rightCornerAction
property real leftPadding: 0
property real rightPadding: 0
property bool isVertical: false property bool isVertical: false
// drop shadow for icons // drop shadow for icons
MultiEffect { MultiEffect {
anchors.fill: root anchors.fill: icons
visible: shadow visible: shadow
source: icons source: icons
blurMax: 16 blurMax: 16
@ -42,25 +45,25 @@ Item {
shadowOpacity: 0.8 shadowOpacity: 0.8
} }
// background colour
Rectangle {
anchors.fill: parent
color: root.backgroundColor
}
Item { Item {
id: icons id: icons
anchors.fill: parent anchors.fill: parent
property real buttonLength: 0 property real buttonLength: 0
// background colour
Rectangle {
anchors.fill: parent
color: root.backgroundColor
}
NavigationPanelButton { NavigationPanelButton {
id: leftCornerButton id: leftCornerButton
visible: root.leftCornerAction.visible visible: root.leftCornerAction.visible
Kirigami.Theme.colorSet: root.foregroundColorGroup Kirigami.Theme.colorSet: root.foregroundColorGroup
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
enabled: root.leftCornerAction.enabled enabled: root.leftCornerAction.enabled
iconSizeFactor: root.leftCornerAction.iconSizeFactor shrinkSize: root.leftCornerAction.shrinkSize
iconSource: root.leftCornerAction.iconSource iconSource: root.leftCornerAction.iconSource
onClicked: { onClicked: {
if (enabled) { if (enabled) {
@ -76,7 +79,7 @@ Item {
Kirigami.Theme.colorSet: root.foregroundColorGroup Kirigami.Theme.colorSet: root.foregroundColorGroup
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
enabled: root.leftAction.enabled enabled: root.leftAction.enabled
iconSizeFactor: root.leftAction.iconSizeFactor shrinkSize: root.leftAction.shrinkSize
iconSource: root.leftAction.iconSource iconSource: root.leftAction.iconSource
onClicked: { onClicked: {
if (enabled) { if (enabled) {
@ -92,7 +95,7 @@ Item {
Kirigami.Theme.colorSet: root.foregroundColorGroup Kirigami.Theme.colorSet: root.foregroundColorGroup
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
enabled: root.middleAction.enabled enabled: root.middleAction.enabled
iconSizeFactor: root.middleAction.iconSizeFactor shrinkSize: root.middleAction.shrinkSize
iconSource: root.middleAction.iconSource iconSource: root.middleAction.iconSource
onClicked: { onClicked: {
if (enabled) { if (enabled) {
@ -107,7 +110,7 @@ Item {
Kirigami.Theme.colorSet: root.foregroundColorGroup Kirigami.Theme.colorSet: root.foregroundColorGroup
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
enabled: root.rightAction.enabled enabled: root.rightAction.enabled
iconSizeFactor: root.rightAction.iconSizeFactor shrinkSize: root.rightAction.shrinkSize
iconSource: root.rightAction.iconSource iconSource: root.rightAction.iconSource
onClicked: { onClicked: {
if (enabled) { if (enabled) {
@ -122,7 +125,7 @@ Item {
Kirigami.Theme.colorSet: root.foregroundColorGroup Kirigami.Theme.colorSet: root.foregroundColorGroup
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
enabled: root.rightCornerAction.enabled enabled: root.rightCornerAction.enabled
iconSizeFactor: root.rightCornerAction.iconSizeFactor shrinkSize: root.rightCornerAction.shrinkSize
iconSource: root.rightCornerAction.iconSource iconSource: root.rightCornerAction.iconSource
onClicked: { onClicked: {
if (enabled) { if (enabled) {
@ -138,6 +141,10 @@ Item {
when: root.isVertical when: root.isVertical
PropertyChanges { PropertyChanges {
target: icons target: icons
anchors {
topMargin: root.leftPadding
bottomMargin: root.rightPadding
}
buttonLength: Math.min(Kirigami.Units.gridUnit * 10, icons.height * 0.7 / 3) buttonLength: Math.min(Kirigami.Units.gridUnit * 10, icons.height * 0.7 / 3)
} }
AnchorChanges { AnchorChanges {
@ -198,6 +205,10 @@ Item {
when: !root.isVertical when: !root.isVertical
PropertyChanges { PropertyChanges {
target: icons target: icons
anchors {
leftMargin: root.leftPadding
rightMargin: root.rightPadding
}
buttonLength: Math.min(Kirigami.Units.gridUnit * 8, icons.width * 0.7 / 3) buttonLength: Math.min(Kirigami.Units.gridUnit * 8, icons.width * 0.7 / 3)
} }
AnchorChanges { AnchorChanges {

View file

@ -10,7 +10,7 @@ QtObject {
property bool enabled property bool enabled
property bool visible: true property bool visible: true
property string iconSource property string iconSource
property real iconSizeFactor property real shrinkSize
signal triggered() signal triggered()
} }

View file

@ -20,7 +20,7 @@ Controls.AbstractButton {
width: Math.min(parent.width, parent.height) width: Math.min(parent.width, parent.height)
height: width height: width
property double iconSizeFactor: 1 property int shrinkSize: 0
property alias iconSource: icon.source property alias iconSource: icon.source
MobileShell.HapticsEffect { MobileShell.HapticsEffect {
@ -79,9 +79,11 @@ Controls.AbstractButton {
Kirigami.Theme.colorSet: button.Kirigami.Theme.colorSet Kirigami.Theme.colorSet: button.Kirigami.Theme.colorSet
readonly property real side: Math.min(button.width, button.height) readonly property real side: Math.min(button.width, button.height)
anchors { anchors.centerIn: parent
fill: parent
margins: Math.round((side - side * iconSizeFactor * 0.6) / 2) implicitHeight: Kirigami.Units.iconSizes.smallMedium - shrinkSize
} implicitWidth: Kirigami.Units.iconSizes.smallMedium - shrinkSize
width: implicitWidth
height: implicitHeight
} }
} }

View file

@ -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;
}
}

View 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 {}
}
}
}

View 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();
}
}
}
}

View file

@ -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 {}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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 * 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.layershell 1.0 as LayerShell
import org.kde.notificationmanager as NotificationManager import org.kde.notificationmanager as NotificationManager
import org.kde.plasma.plasma5support 2.0 as P5Support import org.kde.plasma.clock
Item { Item {
id: notificationPopup id: notificationPopup
@ -29,10 +29,13 @@ Item {
property real openOffset property real openOffset
// calculate the position needed to at when the expanded drawer is active // 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 real aboveNotificationFullOffset: 0
property int aboveNotificationHeight: 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 // the drag offset on the current popup notification - used to position notification when stacked underneath
property real currentDragOffset: { property real currentDragOffset: {
let current = popupNotifications.currentPopupIndex == notificationPopup.popupIndex; let current = popupNotifications.currentPopupIndex == notificationPopup.popupIndex;
@ -47,7 +50,6 @@ Item {
onTriggered: { onTriggered: {
visible = true; visible = true;
updateNotificationPopups(); updateNotificationPopups();
checkActionDrawerOpened();
} }
} }
@ -70,7 +72,7 @@ Item {
} }
return true; return true;
} }
onTriggered: notificationPopup.closePopup() onTriggered: notificationPopup.closePopup(popupIndex);
} }
// the value of how much time is left, normalized from 1 to 0 // the value of how much time is left, normalized from 1 to 0
@ -131,7 +133,7 @@ Item {
easing.type: Easing.OutExpo 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 // also update the allowed touch area for the main window
onPopupHeightChanged: { onPopupHeightChanged: {
let abovePopup = popupNotifications.objectAt(popupIndex + 1) 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: { onFullOpenOffsetChanged: {
let abovePopup = popupNotifications.objectAt(popupIndex + 1) let abovePopup = popupNotifications.objectAt(popupIndex + 1)
if (popupIndex + 1 < popupCount && abovePopup) { if (popupIndex + 1 < popupCount && abovePopup) {
abovePopup.aboveNotificationFullOffset = fullOpenOffset; 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 // update 'currentDragOffset' so all notifications can easily access this value
onDragOffsetChanged: { onDragOffsetChanged: {
let abovePopup = popupNotifications.objectAt(popupIndex + 1) 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 // if a new notification is added, update the above notification values need for the expanded drawer
onPopupCountChanged: { onPopupCountChanged: {
let abovePopup = popupNotifications.objectAt(popupIndex + 1) let abovePopup = popupNotifications.objectAt(popupIndex + 1)
if (popupIndex + 1 < abovePopup) { if (popupIndex + 1 < popupCount && abovePopup) {
abovePopup.aboveNotificationHeight = popupHeight; abovePopup.aboveNotificationHeight = popupHeight;
abovePopup.aboveNotificationFullOffset = fullOpenOffset; abovePopup.aboveNotificationFullOffset = fullOpenOffset;
} }
@ -169,12 +171,14 @@ Item {
// update the current popup index value if the index ever changes. // update the current popup index value if the index ever changes.
onPopupIndexChanged: { onPopupIndexChanged: {
if (!isClosing && !inPopupDrawer && !waiting) { 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; 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 isActionDrawerOpen: MobileShellState.ShellDBusClient.isActionDrawerOpen
property bool waiting: true property bool waiting: true
@ -206,16 +210,6 @@ Item {
return model.timeout; 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 // show the top most notification in the list and move the rest to the popup drawer
function updateNotificationPopups() { function updateNotificationPopups() {
if (popupCount != 1) { if (popupCount != 1) {
@ -229,7 +223,7 @@ Item {
function showNotificationPopup() { function showNotificationPopup() {
if (isClosing) { if (isClosing) {
closePopup(); closePopup(popupIndex);
return; return;
} }
if (notificationItem.state != "open") { if (notificationItem.state != "open") {
@ -252,7 +246,7 @@ Item {
if (notificationPopup.popupDrawerOpened && notificationItem.state != "inDrawerClosed" && notificationItem.state != "open") { if (notificationPopup.popupDrawerOpened && notificationItem.state != "inDrawerClosed" && notificationItem.state != "open") {
notificationItem.offset = openOffset; notificationItem.offset = openOffset;
notificationItem.scale = 0.75; notificationItem.scale = 0.75;
notificationItem.opacity = 0.0; notificationItem.popupOpacity = 0.0;
} }
notificationItem.state = "inDrawerClosed"; notificationItem.state = "inDrawerClosed";
notificationPopup.removeKeyboardFocus(); notificationPopup.removeKeyboardFocus();
@ -263,7 +257,7 @@ Item {
if (notificationPopup.popupDrawerOpened && notificationItem.state != "open" && notificationItem.state != "inDrawerClosed") { if (notificationPopup.popupDrawerOpened && notificationItem.state != "open" && notificationItem.state != "inDrawerClosed") {
notificationItem.offset = openOffset; notificationItem.offset = openOffset;
notificationItem.scale = 0.75; notificationItem.scale = 0.75;
notificationItem.opacity = 0.0; notificationItem.popupOpacity = 0.0;
} }
notificationItem.state = "open"; notificationItem.state = "open";
notificationPopup.removeKeyboardFocus(); notificationPopup.removeKeyboardFocus();
@ -271,19 +265,19 @@ Item {
// if the notification ever expires, close it and move on to the next one in the list. // if the notification ever expires, close it and move on to the next one in the list.
property bool isExpired: model.expired 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 // 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.removeKeyboardFocus();
notificationPopup.setInputTransparent(); notificationPopup.setInputTransparent();
if (popupIndex + 1 < popupCount) { if (index + 1 < popupCount) {
popupNotifications.objectAt(popupIndex + 1).aboveNotificationHeight = 0; popupNotifications.objectAt(index + 1).aboveNotificationHeight = 0;
popupNotifications.objectAt(popupIndex + 1).aboveNotificationFullOffset = 0; popupNotifications.objectAt(index + 1).aboveNotificationFullOffset = 0;
} }
if (popupCount > 1) { if (popupCount > 1) {
let nextNotificationIdx = popupIndex + (popupIndex < popupCount - 1 ? 1 : -1); let nextNotificationIdx = index + (index < popupCount - 1 ? 1 : -1);
let nextNotification = popupNotifications.objectAt(nextNotificationIdx); let nextNotification = popupNotifications.objectAt(nextNotificationIdx);
if (nextNotification != null) { if (nextNotification != null) {
@ -329,7 +323,10 @@ Item {
modelIndex: notificationPopup.popupIndex modelIndex: notificationPopup.popupIndex
notificationsModel: notificationPopup.notificationsModel notificationsModel: notificationPopup.notificationsModel
notificationsModelType: notificationPopup.notificationsModelType notificationsModelType: notificationPopup.notificationsModelType
timeSource: notificationPopup.timeDataSource clockSource: notificationPopup.timeDataSource
panelType: notificationPopup.popupDrawerOpened ?
MobileShell.PanelBackground.PanelType.Drawer :
MobileShell.PanelBackground.PanelType.Popup
currentPopupHeight: popupNotifications.currentPopupHeight currentPopupHeight: popupNotifications.currentPopupHeight
@ -344,33 +341,57 @@ Item {
preventDismissTimeout = true; preventDismissTimeout = true;
} }
onDismissRequested: closePopup() onDismissRequested: closePopup(popupIndex)
property real offset: closedOffset property real offset: closedOffset
property real scale: 1.0 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 { Behavior on drawerScale {
NumberAnimation { NumberAnimation {
duration: Kirigami.Units.veryLongDuration duration: Kirigami.Units.veryLongDuration * 1.25
easing.type: Easing.OutExpo easing.type: Easing.OutQuint
} }
} }
property real drawerAddedOffset: Kirigami.Units.gridUnit * 0.5 * Math.max(notificationPopup.popupIndex - popupNotifications.currentPopupIndex, 1)
Behavior on drawerAddedOffset { Behavior on drawerAddedOffset {
NumberAnimation { NumberAnimation {
duration: Kirigami.Units.veryLongDuration duration: Kirigami.Units.veryLongDuration * 1.25
easing.type: Easing.OutExpo easing.type: Easing.OutQuint
} }
} }
property real drawerOpacity: (Math.max(notificationPopup.popupIndex - popupNotifications.currentPopupIndex, 1) > 2) ? 0 : 1
Behavior on drawerOpacity { Behavior on drawerOpacity {
NumberAnimation { NumberAnimation {
duration: Kirigami.Units.veryLongDuration duration: Kirigami.Units.veryLongDuration * 1.25
easing.type: Easing.OutExpo easing.type: Easing.OutQuint
} }
} }
opacity: 1.0 opacity: Math.min(popupOpacity, drawerOpacity)
state: "" state: ""
@ -384,7 +405,7 @@ Item {
target: notificationItem; scale: 1.0 target: notificationItem; scale: 1.0
} }
PropertyChanges { PropertyChanges {
target: notificationItem; opacity: 1.0 target: notificationItem; popupOpacity: 1.0
} }
}, },
State { State {
@ -396,7 +417,7 @@ Item {
target: notificationItem; scale: 1.0 target: notificationItem; scale: 1.0
} }
PropertyChanges { PropertyChanges {
target: notificationItem; opacity: 1.0 target: notificationItem; popupOpacity: 1.0
} }
}, },
State { State {
@ -408,47 +429,88 @@ Item {
target: notificationItem; scale: 0.75 target: notificationItem; scale: 0.75
} }
PropertyChanges { PropertyChanges {
target: notificationItem; opacity: 0.0 target: notificationItem; popupOpacity: 0.0
} }
}, },
State { State {
name: "inDrawerClosed" name: "inDrawerClosed"
PropertyChanges { PropertyChanges {
target: notificationItem; offset: notificationPopup.openOffset + (notificationPopup.popupDrawerOpened ? 0 : drawerAddedOffset) target: notificationItem; offset: notificationPopup.openOffset
} }
PropertyChanges { PropertyChanges {
target: notificationItem; scale: notificationPopup.popupDrawerOpened ? 1 : drawerScale target: notificationItem; scale: 1
} }
PropertyChanges { 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 { transitions: Transition {
SequentialAnimation { SequentialAnimation {
ParallelAnimation { ParallelAnimation {
PropertyAnimation { PropertyAnimation {
properties: "offset"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.5 properties: "offset"
easing.type: notificationItem.notificationEasing
duration: notificationItem.notificationDuration
} }
PropertyAnimation { PropertyAnimation {
properties: "scale"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.5 properties: "scale"
easing.type: notificationItem.notificationEasing
duration: notificationItem.notificationDuration
} }
PropertyAnimation { PropertyAnimation {
properties: "opacity"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.5 properties: "popupOpacity"
easing.type: notificationItem.notificationEasing
duration: notificationItem.notificationDuration
} }
} }
ScriptAction { ScriptAction {
script: { script: {
if (notificationItem.state == "open") { if (notificationItem.state == "open") {
preventDismissTimeout = false; notificationPopup.preventDismissTimeout = false;
notificationPopup.updateTouchArea(); notificationPopup.updateTouchArea();
} else if (notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale") { } else if (notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale") {
preventDismissTimeout = true; notificationPopup.preventDismissTimeout = true;
if (dismissTimeout) { if (notificationPopup.dismissTimeout) {
notificationPopup.dismissClicked(); notificationPopup.dismissClicked();
} else if (!isActionDrawerOpen) { } else {
notificationPopup.expired(); notificationPopup.expired();
} }
} }
} }
@ -460,15 +522,15 @@ Item {
Scale { Scale {
origin.x: Math.round(notificationPopup.popupWidth / 2) origin.x: Math.round(notificationPopup.popupWidth / 2)
origin.y: notificationPopup.scaleOriginY origin.y: notificationPopup.scaleOriginY
xScale: notificationItem.scale xScale: notificationItem.scale - notificationItem.drawerScale
yScale: notificationItem.scale yScale: notificationItem.scale - notificationItem.drawerScale
} }
] ]
} }
transform: [ transform: [
Translate { 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; startActive = active;
notificationPopup.preventDismissTimeout = true; notificationPopup.preventDismissTimeout = true;
if (!active && !(notificationItem.state == "closeWithScale" || notificationItem.state == "closeWithMove")) { 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)) { 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. // this code is called when the notification is swiped or dragged to the top.
notificationPopup.closePopup(); notificationPopup.closedWithSwipe = true;
notificationPopup.closePopup(popupIndex);
return; 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); notificationPopup.preventDismissTimeout = (keyboardInteractivity == LayerShell.Window.KeyboardInteractivityOnDemand);
} else { } else {

View file

@ -15,7 +15,7 @@ import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.layershell 1.0 as LayerShell import org.kde.layershell 1.0 as LayerShell
import org.kde.notificationmanager as NotificationManager 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 QtQuick.Controls as Controls
import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.components 3.0 as PlasmaComponents
@ -46,10 +46,10 @@ Window {
property QtObject notificationSettings property QtObject notificationSettings
property QtObject popupNotificationsModel property QtObject popupNotificationsModel
property QtObject tasksModel property QtObject tasksModel
property QtObject timeSource property Clock clockSource
property bool inhibited property bool inhibited
Kirigami.Theme.colorSet: Kirigami.Theme.View Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false 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) 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) 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 // Update the window touch region to encapsulate the notification area or the whole screen depending on the 'popupDrawerOpened' state
function updateTouchArea() { function updateTouchArea() {
ShellUtil.setInputTransparent(notificationPopupManager, false); ShellUtil.setInputTransparent(notificationPopupManager, false);
if (popupDrawerOpened) { if (popupDrawerOpened) {
ShellUtil.setInputRegion(notificationPopupManager, Qt.rect(0, 0, 0, 0)); ShellUtil.setInputRegion(notificationPopupManager, Qt.rect(0, 0, 0, 0));
} else { } 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 popupHeight = Kirigami.Units.gridUnit * 6;
let currentPopup = notifications.objectAt(notifications.currentPopupIndex); let currentPopup = notifications.objectAt(notifications.currentPopupIndex);
if (currentPopup) { if (currentPopup) {
@ -160,7 +174,7 @@ Window {
onCountChanged: { onCountChanged: {
if (count == 0) { if (count == 0) {
ShellUtil.setInputTransparent(notificationPopupManager, true); ShellUtil.setInputTransparent(notificationPopupManager, true);
notificationPopupManager.visible = false; hideTimeout.restart();
notificationPopupManager.popupDrawerOpened = false; notificationPopupManager.popupDrawerOpened = false;
fullHeight = 0; fullHeight = 0;
return; return;
@ -195,7 +209,7 @@ Window {
popupModel: model popupModel: model
notificationsModel: popupNotificationsModel notificationsModel: popupNotificationsModel
notificationsModelType: notificationModelType notificationsModelType: notificationModelType
timeDataSource: timeSource timeDataSource: clockSource
timeout: model.timeout timeout: model.timeout

View file

@ -12,7 +12,7 @@ import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.notificationmanager as NotificationManager 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 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 property bool inhibited: false
onInhibitedChanged: { onInhibitedChanged: {
var pa = pulseAudio.item; var pa = __pulseAudio.item;
if (!pa) { if (!pa) {
return; return;
} }
@ -121,13 +126,9 @@ QtObject {
groupInline: false groupInline: false
} }
property Clock clockSource: Clock {
property QtObject timeSource: P5Support.DataSource { id: clockSource
engine: "time" onTimeChanged: {
connectedSources: ["Local"]
interval: 60000 // 1 min
intervalAlignment: P5Support.Types.AlignToMinute
onDataChanged: {
checkInhibition(); checkInhibition();
npm.timeChanged(); npm.timeChanged();
} }
@ -137,7 +138,7 @@ QtObject {
notificationModelType: notificationProvider.notificationModelType notificationModelType: notificationProvider.notificationModelType
notificationSettings: notificationProvider.notificationSettings notificationSettings: notificationProvider.notificationSettings
popupNotificationsModel: notificationProvider.popupNotificationsModel popupNotificationsModel: notificationProvider.popupNotificationsModel
timeSource: notificationProvider.timeSource clockSource: notificationProvider.clockSource
inhibited: notificationProvider.inhibited inhibited: notificationProvider.inhibited
tasksModel: notificationProvider.tasksModel tasksModel: notificationProvider.tasksModel
} }

View file

@ -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;
}
}
}
}

View file

@ -27,53 +27,68 @@ ColumnLayout {
property real scale: 1.0 property real scale: 1.0
PulseObjectFilterModel { // Input devices
id: paSinkFilterModel readonly property SourceModel paSourceModel: SourceModel { id: paSourceModel }
sortRoleName: "SortByDefault"
sortOrder: Qt.DescendingOrder
filterOutInactiveDevices: true
sourceModel: MobileShell.AudioInfo.paSinkModel
}
SourceModel { // Output devices
id: paSourceModel 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 id: paSourceFilterModel
sortRoleName: "SortByDefault"
sortOrder: Qt.DescendingOrder
filterOutInactiveDevices: true filterOutInactiveDevices: true
filterVirtualDevices: true
sourceModel: paSourceModel sourceModel: paSourceModel
} }
CardModel { // Active output devices
id: paCardModel 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 { PopupCard {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: Kirigami.Units.gridUnit Layout.bottomMargin: Kirigami.Units.gridUnit
scaleFactor: audioApplet.scale
transform: Scale {
origin.x: Math.round(implicitWidth / 2)
origin.y: Math.round(height / 2)
xScale: audioApplet.scale
yScale: audioApplet.scale
}
contentItem: ColumnLayout { contentItem: ColumnLayout {
anchors.rightMargin: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing
anchors.leftMargin: Kirigami.Units.smallSpacing
Kirigami.Heading { Kirigami.Heading {
level: 2 level: 2
text: i18n("Outputs") text: i18n("Output Devices")
wrapMode: Text.Wrap
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.leftMargin: Kirigami.Units.smallSpacing
} }
Repeater { Repeater {
@ -83,9 +98,8 @@ ColumnLayout {
model: paSinkFilterModel model: paSinkFilterModel
delegate: DeviceListItem { delegate: DeviceListItem {
Layout.fillWidth: true Layout.fillWidth: true
Layout.margins: Kirigami.Units.smallSpacing
type: "sink" type: "sink"
onlyone: sinkView.count === 1 onlyOne: sinkView.count === 1
} }
} }
} }
@ -94,24 +108,16 @@ ColumnLayout {
PopupCard { PopupCard {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: Kirigami.Units.gridUnit Layout.bottomMargin: Kirigami.Units.gridUnit
scaleFactor: audioApplet.scale
transform: Scale {
origin.x: Math.round(implicitWidth / 2)
origin.y: Math.round(height / 2)
xScale: audioApplet.scale
yScale: audioApplet.scale
}
contentItem: ColumnLayout { contentItem: ColumnLayout {
anchors.rightMargin: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing
anchors.leftMargin: Kirigami.Units.smallSpacing
Kirigami.Heading { Kirigami.Heading {
level: 2 level: 2
text: i18n("Inputs") text: i18n("Input Devices")
wrapMode: Text.Wrap
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.leftMargin: Kirigami.Units.smallSpacing
} }
Repeater { Repeater {
@ -121,38 +127,30 @@ ColumnLayout {
model: paSourceFilterModel model: paSourceFilterModel
delegate: DeviceListItem { delegate: DeviceListItem {
Layout.fillWidth: true Layout.fillWidth: true
Layout.margins: Kirigami.Units.smallSpacing
type: "source" type: "source"
onlyone: sinkView.count === 1 onlyOne: sourceView.count === 1
} }
} }
} }
} }
PopupCard { PopupCard {
visible: sourceInputView.model.count + sourceMediaInputView.model.count !== 0 visible: (sourceMediaInputView.count + sourceInputView.count) > 0
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: Kirigami.Units.gridUnit Layout.bottomMargin: Kirigami.Units.gridUnit
scaleFactor: audioApplet.scale
transform: Scale {
origin.x: Math.round(implicitWidth / 2)
origin.y: Math.round(height / 2)
xScale: audioApplet.scale
yScale: audioApplet.scale
}
contentItem: ColumnLayout { contentItem: ColumnLayout {
anchors.rightMargin: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing
anchors.leftMargin: Kirigami.Units.smallSpacing
Kirigami.Heading { Kirigami.Heading {
level: 2 level: 2
text: i18n("Playback Streams") text: i18n("Playback Streams")
wrapMode: Text.Wrap
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.leftMargin: Kirigami.Units.smallSpacing
} }
// "Grouped" media sources (ex. Notifications)
Repeater { Repeater {
id: sourceMediaInputView id: sourceMediaInputView
Layout.fillWidth: true Layout.fillWidth: true
@ -166,25 +164,22 @@ ColumnLayout {
Layout.margins: Kirigami.Units.smallSpacing Layout.margins: Kirigami.Units.smallSpacing
width: sourceOutputView.width width: sourceOutputView.width
type: "sink-input" type: "sink-input"
devicesModel: sourceView.model devicesModel: paSinkFilterModel
} }
} }
// Regular playback streams
Repeater { Repeater {
id: sourceInputView id: sourceInputView
Layout.fillWidth: true Layout.fillWidth: true
model: PulseObjectFilterModel { model: paSinkInputFilterModel
filters: [ { role: "VirtualStream", value: false } ]
sourceModel: SinkInputModel {}
}
delegate: StreamListItem { delegate: StreamListItem {
Layout.fillWidth: true Layout.fillWidth: true
Layout.margins: Kirigami.Units.smallSpacing
width: sourceOutputView.width width: sourceOutputView.width
type: "sink-input" type: "sink-input"
devicesModel: sourceView.model devicesModel: paSinkFilterModel
} }
} }
} }
@ -194,40 +189,29 @@ ColumnLayout {
visible: sourceOutputView.model.count !== 0 visible: sourceOutputView.model.count !== 0
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: Kirigami.Units.gridUnit Layout.bottomMargin: Kirigami.Units.gridUnit
scaleFactor: audioApplet.scale
transform: Scale {
origin.x: Math.round(implicitWidth / 2)
origin.y: Math.round(height / 2)
xScale: audioApplet.scale
yScale: audioApplet.scale
}
contentItem: ColumnLayout { contentItem: ColumnLayout {
anchors.rightMargin: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing
anchors.leftMargin: Kirigami.Units.smallSpacing
Kirigami.Heading { Kirigami.Heading {
level: 2 level: 2
text: i18n("Recording Streams") text: i18n("Recording Streams")
wrapMode: Text.Wrap
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.leftMargin: Kirigami.Units.smallSpacing
} }
Repeater { Repeater {
id: sourceOutputView id: sourceOutputView
Layout.fillWidth: true Layout.fillWidth: true
model: PulseObjectFilterModel { model: paSourceOutputFilterModel
filters: [ { role: "VirtualStream", value: false } ]
sourceModel: SourceOutputModel {}
}
delegate: StreamListItem { delegate: StreamListItem {
Layout.fillWidth: true Layout.fillWidth: true
Layout.margins: Kirigami.Units.smallSpacing
width: sourceOutputView.width width: sourceOutputView.width
type: "source-output" type: "source-output"
devicesModel: sourceView.model devicesModel: paSourceFilterModel
} }
} }
} }

View file

@ -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 // adapted from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/DeviceListItem.qml
ListItemBase { ListItemBase {
readonly property var currentPort: Ports[ActivePortIndex] readonly property var currentPort: model.Ports[model.ActivePortIndex]
readonly property var currentActivePortIndex: ActivePortIndex readonly property var currentActivePortIndex: model.ActivePortIndex
readonly property var currentMuted: Muted readonly property var currentMuted: model.Muted
readonly property var activePortIndex: ActivePortIndex readonly property var activePortIndex: model.ActivePortIndex
property bool onlyone: false
label: { label: {
if (currentPort && currentPort.description) { if (currentPort && currentPort.description) {
if (onlyone || !Description) { if (onlyOne || !model.Description) {
return currentPort.description; return currentPort.description;
} else { } 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) { if (model.Description) {
return Description; return model.Description;
} }
if (Name) { if (model.Name) {
return Name; return model.Name;
} }
return i18n("Device name not found"); return i18n("Device name not found");
} }

View file

@ -6,52 +6,55 @@
* SPDX-License-Identifier: GPL-2.0-or-later * SPDX-License-Identifier: GPL-2.0-or-later
*/ */
import QtQuick 2.15 import QtQuick
import QtQuick.Controls 2.15 as Controls import QtQuick.Controls as Controls
import QtQuick.Layouts 1.1 import QtQuick.Layouts
import QtQuick.Window 2.2
import org.kde.kirigami 2.20 as Kirigami import org.kde.kirigami as Kirigami
import org.kde.ksvg 1.0 as KSvg import org.kde.ksvg as KSvg
import org.kde.kquickcontrolsaddons 2.0 import org.kde.kquickcontrolsaddons
import org.kde.plasma.core as PlasmaCore import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.components as PlasmaComponents
import org.kde.plasma.private.volume 0.1 import org.kde.plasma.extras as PlasmaExtras
import org.kde.plasma.private.volume
import "icon.js" as Icon import "icon.js" as Icon
// adapted from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/ListItemBase.qml // adapted from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/ListItemBase.qml
Controls.Control { Controls.AbstractButton {
id: baseItem id: baseItem
property string label property string label
property alias listIcon: clientIcon.source property alias listIcon: clientIcon.source
property string type // sink, source, source-output property string type // sink, source, source-output
MouseArea { property bool onlyOne: false
id: clickArea
anchors.fill: parent // Whether this item is selected
z: -1 readonly property bool supportsSelection: (baseItem.type == "sink" || baseItem.type == "source")
onClicked: { readonly property bool selected: supportsSelection && (model.PulseObject.hasOwnProperty("default") ? model.PulseObject.default : false)
if (selectButton.visible) {
model.PulseObject.default = true; 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 { contentItem: RowLayout {
id: row id: row
spacing: Kirigami.Units.smallSpacing 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 // application icon
Kirigami.Icon { Kirigami.Icon {
id: clientIcon 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 { ColumnLayout {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true Layout.fillWidth: true
@ -87,9 +108,10 @@ Controls.Control {
} }
PlasmaComponents.ToolButton { PlasmaComponents.ToolButton {
id: viewButton
Layout.alignment: Qt.AlignBottom Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: -Kirigami.Units.smallSpacing Layout.bottomMargin: -Kirigami.Units.smallSpacing
icon.name: "application-menu" icon.name: "view-more-symbolic"
checkable: true checkable: true
checked: contextMenu.visible && contextMenu.visualParent === this checked: contextMenu.visible && contextMenu.visualParent === this
visible: contextMenu.hasContent visible: contextMenu.hasContent
@ -132,93 +154,26 @@ Controls.Control {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing 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) // this slider was effectively copied from the source (linked at the top of the file)
PlasmaComponents.Slider { VolumeSlider {
id: slider id: slider
Layout.fillWidth: true 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 from: PulseAudio.MinimalVolume
to: PulseAudio.NormalVolume to: model.Volume >= PulseAudio.NormalVolume * 1.01 ? PulseAudio.MaximalVolume : PulseAudio.NormalVolume
stepSize: to / (to / PulseAudio.NormalVolume * 100.0) stepSize: PulseAudio.NormalVolume / 100.0
visible: HasVolume property real myStepSize: PulseAudio.NormalVolume / 100.0
enabled: VolumeWritable visible: model.HasVolume !== false // Devices always have volume but Streams don't necessarily
opacity: Muted ? 0.5 : 1 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) value: to, model.Volume
onMoved: {
background: KSvg.FrameSvgItem { model.Volume = value;
imagePath: "widgets/slider" model.Muted = value === 0;
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
}
}
}
} }
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: { onPressedChanged: {
if (!pressed) { if (!pressed) {
// Make sure to sync the volume once the button was // 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 // Otherwise it might be that the slider is at v10
// whereas PA rejected the volume change and is // whereas PA rejected the volume change and is
// still at v15 (e.g.). // still at v15 (e.g.).
updateTimer.restart(); value = Qt.binding(() => model.Volume);
} }
} }
Timer { function updateVolume() {
id: updateTimer if (model.Volume > PulseAudio.NormalVolume) {
interval: 200 model.Volume = PulseAudio.NormalVolume;
onTriggered: slider.value = Volume }
}
SequentialAnimation {
id: seqAnimation
NumberAnimation {
id: toAnimation
target: slider
property: "to"
duration: Kirigami.Units.shortDuration
easing.type: Easing.InOutQuad
}
ScriptAction {
script: slider.updateVolume()
}
} }
} }
PlasmaComponents.Label { PlasmaComponents.Label {
@ -263,8 +232,4 @@ Controls.Control {
} }
} }
} }
function setVolumeByPercent(targetPercent) {
model.PulseObject.volume = Math.round(PulseAudio.NormalVolume * (targetPercent/100));
}
} }

View 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
}
}

View file

@ -23,17 +23,16 @@ Window {
id: window id: window
width: osd.width + 6 width: osd.width + 6
height: cards.implicitHeight + 6 height: cards.implicitHeight + 6 + cards.openOffset
onWidthChanged: if (visible) window.updateTouchRegion()
visible: false visible: false
readonly property real offsetMargins: Math.max(cards.offset, 0)
LayerShell.Window.scope: "overlay" LayerShell.Window.scope: "overlay"
LayerShell.Window.anchors: LayerShell.Window.AnchorTop LayerShell.Window.anchors: LayerShell.Window.AnchorTop
LayerShell.Window.layer: LayerShell.Window.LayerOverlay LayerShell.Window.layer: LayerShell.Window.LayerOverlay
LayerShell.Window.exclusionZone: -1 LayerShell.Window.exclusionZone: -1
LayerShell.Window.margins.top: offsetMargins
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
Kirigami.Theme.colorSet: Kirigami.Theme.View Kirigami.Theme.colorSet: Kirigami.Theme.View
@ -45,16 +44,17 @@ Window {
if (cards.state == "closed") { if (cards.state == "closed") {
hideTimer.stop(); hideTimer.stop();
window.open(); window.open();
} else if (!volumeSlider.isPressed) { } else if (!volumeSlider.pressed) {
hideTimer.restart(); hideTimer.restart();
} }
} }
function open() { 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; window.visible = true;
cards.state = "open"; cards.state = "open";
// set window input transparency to accept touches
ShellUtil.setInputTransparent(window, false);
} }
function close() { function close() {
@ -63,12 +63,16 @@ Window {
ShellUtil.setInputTransparent(window, true); ShellUtil.setInputTransparent(window, true);
} }
function updateTouchRegion() {
ShellUtil.setInputRegion(window, Qt.rect(0, cards.openOffset, window.width, cards.implicitHeight + 6));
}
Timer { Timer {
id: hideTimer id: hideTimer
interval: 2000 interval: 2000
running: false running: false
onTriggered: { onTriggered: {
window.close(); window.close();
} }
} }
@ -79,6 +83,10 @@ Window {
ColumnLayout { ColumnLayout {
id: cards id: cards
// Ensure children get visibility state of window so that they don't update while closed
visible: window.visible
width: parent.width width: parent.width
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@ -87,7 +95,6 @@ Window {
readonly property real closedOffset: -(cards.implicitHeight + Kirigami.Units.smallSpacing) readonly property real closedOffset: -(cards.implicitHeight + Kirigami.Units.smallSpacing)
readonly property real openOffset: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 3 readonly property real openOffset: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 3
property real offset: closedOffset property real offset: closedOffset
property real scale: 0.95
state: "closed" state: "closed"
@ -97,18 +104,12 @@ Window {
PropertyChanges { PropertyChanges {
target: cards; offset: openOffset target: cards; offset: openOffset
} }
PropertyChanges {
target: cards; scale: 1.0
}
}, },
State { State {
name: "closed" name: "closed"
PropertyChanges { PropertyChanges {
target: cards; offset: closedOffset target: cards; offset: closedOffset
} }
PropertyChanges {
target: cards; scale: 0.95
}
} }
] ]
@ -116,16 +117,16 @@ Window {
SequentialAnimation { SequentialAnimation {
ParallelAnimation { ParallelAnimation {
PropertyAnimation { PropertyAnimation {
properties: "offset"; 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
}
PropertyAnimation {
properties: "scale"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.25
} }
} }
ScriptAction { ScriptAction {
script: { script: {
if (cards.state == "open") { if (cards.state == "open") {
hideTimer.restart(); hideTimer.restart();
// set window input transparency to accept touches
ShellUtil.setInputTransparent(window, false);
window.updateTouchRegion();
} else { } else {
hideTimer.stop(); hideTimer.stop();
window.visible = false; window.visible = false;
@ -142,13 +143,7 @@ Window {
transform: [ transform: [
Translate { Translate {
y: cards.offset - window.offsetMargins + 1 y: cards.offset + 1
},
Scale {
origin.x: Math.round(width / 2)
origin.y: Math.round(height / 2)
xScale: cards.scale
yScale: cards.scale
} }
] ]
@ -162,81 +157,41 @@ Window {
property int volumePercent: PreferredDevice.sink.volume / PulseAudio.NormalVolume * 100.0 property int volumePercent: PreferredDevice.sink.volume / PulseAudio.NormalVolume * 100.0
PlasmaComponents.ToolButton { PlasmaComponents.ToolButton {
icon.name: !PreferredDevice.sink || PreferredDevice.sink.muted ? "audio-volume-muted" : MobileShell.AudioInfo.icon icon.name: !PreferredDevice.sink || (PreferredDevice.sink.muted ? "audio-volume-muted" : MobileShell.AudioInfo.icon)
text: !PreferredDevice.sink || PreferredDevice.sink.muted ? i18n("Unmute") : i18n("Mute") text: !PreferredDevice.sink || (PreferredDevice.sink.muted ? i18n("Unmute") : i18n("Mute"))
display: Controls.AbstractButton.IconOnly display: Controls.AbstractButton.IconOnly
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Kirigami.Units.iconSizes.medium Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium Layout.preferredHeight: Kirigami.Units.iconSizes.medium
Layout.rightMargin: Kirigami.Units.smallSpacing Layout.rightMargin: Kirigami.Units.smallSpacing
onClicked: { onClicked: {
hideTimer.restart(); hideTimer.restart();
PreferredDevice.sink.muted = !PreferredDevice.sink.muted; PreferredDevice.sink.muted = !PreferredDevice.sink.muted;
} }
} }
PlasmaComponents.Slider { VolumeSlider {
id: volumeSlider id: volumeSlider
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: Kirigami.Units.smallSpacing * 2 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 from: PulseAudio.MinimalVolume
to: PulseAudio.NormalVolume to: PulseAudio.NormalVolume
stepSize: to / (to / PulseAudio.NormalVolume * 100.0) stepSize: to / (to / PulseAudio.NormalVolume * 100.0)
opacity: muted ? 0.5 : 1.0
Component.onCompleted: { volumeObject: PreferredDevice.sink
ignoreValueChange = false; 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: { onPressedChanged: {
volumeSlider.isPressed = pressed;
if (pressed) { if (pressed) {
window.open();
hideTimer.stop(); hideTimer.stop();
} else { } else {
// Make sure to sync the volume once the button was // 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 // Otherwise it might be that the slider is at v10
// whereas PA rejected the volume change and is // whereas PA rejected the volume change and is
// still at v15 (e.g.). // still at v15 (e.g.).
value = Qt.binding(() => PreferredDevice.sink.volume);
hideTimer.restart(); hideTimer.restart();
updateTimer.restart();
} }
} }
Timer {
id: updateTimer
interval: 200
onTriggered: volumeSlider.value = PreferredDevice.sink.volume
}
} }
PlasmaComponents.ToolButton { PlasmaComponents.ToolButton {

View file

@ -31,12 +31,12 @@ Window {
LayerShell.Window.scope: "overlay" LayerShell.Window.scope: "overlay"
LayerShell.Window.anchors: LayerShell.Window.AnchorTop LayerShell.Window.anchors: LayerShell.Window.AnchorTop
LayerShell.Window.layer: LayerShell.Window.LayerTop LayerShell.Window.layer: LayerShell.Window.LayerOverlay
LayerShell.Window.exclusionZone: -1 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) 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 Kirigami.Theme.inherit: false
color: backgroundColor color: backgroundColor
@ -65,12 +65,21 @@ Window {
visible = false; visible = false;
} }
Binding {
target: MobileShellState.ShellDBusClient
property: "isVolumeOSDOpen"
value: window.visible
}
Flickable { Flickable {
id: flickable id: flickable
anchors.fill: parent anchors.fill: parent
contentHeight: cards.implicitHeight contentHeight: cards.implicitHeight
boundsBehavior: Flickable.DragAndOvershootBounds boundsBehavior: Flickable.DragAndOvershootBounds
// Ensure children get visibility state of window so that they don't update while closed
visible: window.visible
pressDelay: 50 pressDelay: 50
property real offset: -Kirigami.Units.gridUnit property real offset: -Kirigami.Units.gridUnit
@ -115,16 +124,16 @@ Window {
SequentialAnimation { SequentialAnimation {
ParallelAnimation { ParallelAnimation {
PropertyAnimation { 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 { 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 { 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 { 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 { ScriptAction {
@ -171,44 +180,32 @@ Window {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: Kirigami.Units.gridUnit Layout.bottomMargin: Kirigami.Units.gridUnit
transform: Scale { scaleFactor: flickable.scale
origin.x: Math.round(implicitWidth / 2)
origin.y: Math.round(height / 2)
xScale: flickable.scale
yScale: flickable.scale
}
contentItem: RowLayout { contentItem: PlasmaComponents.ToolButton {
id: audioSettingsButton
PlasmaComponents.ToolButton { contentItem: Item {
property int addedPadding: Kirigami.Units.smallSpacing * 2 anchors.fill: parent
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter RowLayout {
Layout.preferredWidth: parent.width - addedPadding * 2 spacing: Kirigami.Units.largeSpacing
Layout.preferredHeight: Kirigami.Units.iconSizes.medium anchors.centerIn: parent
Layout.margins: addedPadding Kirigami.Icon {
Layout.alignment: Qt.AlignVCenter
contentItem: Item { Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
anchors.fill: parent Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
RowLayout { source: "settings-configure"
spacing: Kirigami.Units.largeSpacing }
anchors.centerIn: parent PlasmaComponents.Label {
Kirigami.Icon { text: i18n("Open audio settings")
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
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: { onClicked: {
MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_pulseaudio"); MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_pulseaudio");
window.close(); window.close();
}
} }
} }
} }

View file

@ -19,14 +19,14 @@ import org.kde.plasma.private.mobileshell as MobileShell
QtObject { QtObject {
id: component id: component
function showVolumeOverlay() { property var osdListener: Connections {
if (!osd.visible) { target: MobileShellState.VolumeOSDListener
vcp.showOverlay();
}
}
Component.onCompleted: { function onShowOSD(icon, volume, maxVolume) {
MobileShell.AudioInfo.volumeChanged.connect(showVolumeOverlay); if (!osd.visible) {
vcp.showOverlay();
}
}
} }
property var apiListener: Connections { property var apiListener: Connections {

View 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
}
}
}

View file

@ -8,7 +8,7 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.15 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.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
@ -19,14 +19,14 @@ RowLayout {
id: clockText id: clockText
required property int fontPixelSize required property int fontPixelSize
required property P5Support.DataSource source required property Clock clockSource
PlasmaComponents.Label { PlasmaComponents.Label {
id: clock id: clock
property bool is24HourTime: MobileShell.ShellUtil.isSystem24HourFormat 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 color: Kirigami.Theme.textColor
verticalAlignment: Qt.AlignVCenter verticalAlignment: Qt.AlignVCenter
font.pixelSize: fontPixelSize font.pixelSize: fontPixelSize
@ -36,7 +36,7 @@ RowLayout {
id: date id: date
visible: ShellSettings.Settings.dateInStatusBar && !root.showSecondRow 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 color: Kirigami.Theme.textColor
verticalAlignment: Qt.AlignVCenter verticalAlignment: Qt.AlignVCenter
font.pixelSize: fontPixelSize font.pixelSize: fontPixelSize

View file

@ -15,12 +15,13 @@ import QtQml.Models
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.plasma.core as PlasmaCore 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.private.systemtray as SystemTray
import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.kitemmodels as KItemModels import org.kde.kitemmodels as KItemModels
import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.private.mobileshell.state as MobileShellState
Item { Item {
id: root id: root
@ -45,31 +46,17 @@ Item {
*/ */
property bool showTime: true 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 textPixelSize: Math.round(11 * ShellSettings.Settings.statusBarScaleFactor)
readonly property real smallerTextPixelSize: Math.round(9 * ShellSettings.Settings.statusBarScaleFactor) readonly property real smallerTextPixelSize: Math.round(9 * ShellSettings.Settings.statusBarScaleFactor)
readonly property real elementSpacing: Math.round(Kirigami.Units.smallSpacing * 1.5) readonly property real elementSpacing: Math.round(Kirigami.Units.smallSpacing * 1.5)
P5Support.DataSource { Clock {
id: timeSource id: clockSource
engine: "time"
connectedSources: ["Local"]
interval: 1000
intervalAlignment: P5Support.Types.AlignToMinute
} }
property alias statusNotifierSource: statusNotifierSourceLoader.item MobileShellState.PanelSettingsDBusClient {
id: panelSettings
Loader { screenName: Screen.name
id: statusNotifierSourceLoader
active: !disableSystemTray
sourceComponent: SystemTray.StatusNotifierModel { }
} }
// drop shadow for icons // drop shadow for icons
@ -89,8 +76,8 @@ Item {
z: 1 z: 1
topPadding: Kirigami.Units.smallSpacing topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing bottomPadding: Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.smallSpacing * 3 rightPadding: Kirigami.Units.smallSpacing * 3 + panelSettings.statusBarLeftPadding
leftPadding: Kirigami.Units.smallSpacing * 3 leftPadding: Kirigami.Units.smallSpacing * 3 + panelSettings.statusBarRightPadding
anchors.fill: parent anchors.fill: parent
background: Rectangle { background: Rectangle {
@ -103,9 +90,10 @@ Item {
RowLayout { RowLayout {
id: mainRow 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.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: rowHeight Layout.preferredHeight: rowHeight
spacing: 0 spacing: 0
@ -115,7 +103,7 @@ Item {
visible: root.showTime visible: root.showTime
Layout.fillHeight: true Layout.fillHeight: true
fontPixelSize: textPixelSize fontPixelSize: textPixelSize
source: timeSource clockSource: clockSource
} }
MobileShell.SignalStrengthIndicator { MobileShell.SignalStrengthIndicator {
@ -130,16 +118,6 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
} }
// system tray
Repeater {
id: statusNotifierRepeater
model: root.statusNotifierSource
delegate: TaskWidget {
Layout.leftMargin: root.elementSpacing
}
}
// system indicators // system indicators
// using Layout.fillHeight here seems to cause polish loops, instead just define the height of the row // using Layout.fillHeight here seems to cause polish loops, instead just define the height of the row
RowLayout { RowLayout {
@ -185,7 +163,7 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
PlasmaComponents.Label { PlasmaComponents.Label {
text: Qt.formatDate(timeSource.data.Local.DateTime, "ddd. MMMM d") text: Qt.formatDate(clockSource.dateTime, "ddd. MMMM d")
color: Kirigami.Theme.disabledTextColor color: Kirigami.Theme.disabledTextColor
font.pixelSize: root.smallerTextPixelSize font.pixelSize: root.smallerTextPixelSize
} }

View file

@ -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)
}
}
}

View file

@ -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.components 3.0 as PlasmaComponents
import org.kde.plasma.workspace.components 2.0 as PW 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.mobileshell as MobileShell
import org.kde.plasma.private.battery // needed for charging state import org.kde.plasma.private.battery // needed for charging state
@ -24,30 +25,31 @@ RowLayout {
ListView { ListView {
id: batteryRepeater id: batteryRepeater
property int batteryWidth: 0 spacing: 0
spacing: root.elementSpacing
model: MobileShell.BatteryInfo.batteries model: MobileShell.BatteryInfo.batteries
orientation: ListView.Horizontal orientation: ListView.Horizontal
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: (batteryRepeater.batteryWidth + root.elementSpacing) * batteryRepeater.count Layout.preferredWidth: contentItem.childrenRect.width
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: false Layout.fillWidth: false
delegate: RowLayout { 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.fillHeight: false
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
height: batteryRepeater.height height: batteryRepeater.height
visible: Type === "Battery" // only show the internal battery
PW.BatteryIcon { PW.BatteryIcon {
id: battery id: battery
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
height: batteryLabel.height Layout.fillHeight: true
width: batteryLabel.height width: batteryLabel.height
hasBattery: PluggedIn hasBattery: PluggedIn
@ -59,16 +61,12 @@ RowLayout {
id: batteryLabel id: batteryLabel
text: i18n("%1%", Percent) text: i18n("%1%", Percent)
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.fillHeight: true
color: Kirigami.Theme.textColor color: Kirigami.Theme.textColor
visible: ShellSettings.Settings.showBatteryPercentage
font.pixelSize: textPixelSize 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
}
} }
} }
} }

View file

@ -12,26 +12,12 @@ import QtQuick.Controls as QQC2
import org.kde.plasma.networkmanagement as PlasmaNM import org.kde.plasma.networkmanagement as PlasmaNM
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
Kirigami.Icon { Item {
id: connectionIcon id: connectionIcon
// data
readonly property string icon: wirelessStatus.hotspotSSID.length !== 0 ? "network-wireless-hotspot" : connectionIconProvider.connectionIcon readonly property string icon: wirelessStatus.hotspotSSID.length !== 0 ? "network-wireless-hotspot" : connectionIconProvider.connectionIcon
readonly property bool indicatorRunning: connectionIconProvider.connecting 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 { readonly property var wirelessStatus: PlasmaNM.WirelessStatus {
id: wirelessStatus id: wirelessStatus
} }
@ -40,9 +26,16 @@ Kirigami.Icon {
id: connectionIconProvider id: connectionIconProvider
} }
// implementation // Internet icon, only show while visible
source: icon Kirigami.Icon {
id: internetIcon
anchors.fill: parent
visible: !connectingIndicator.visible
source: connectionIcon.icon
}
// Connecting indicator
QQC2.BusyIndicator { QQC2.BusyIndicator {
id: connectingIndicator id: connectingIndicator

View file

@ -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
}
}

View file

@ -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;
}
}

View file

@ -20,7 +20,7 @@ import org.kde.kirigami 2.19 as Kirigami
MouseArea { MouseArea {
id: root id: root
onClicked: root.requestedClose() onClicked: root.requestedClose(false)
function requestFocus() { function requestFocus() {
queryField.forceActiveFocus(); queryField.forceActiveFocus();
@ -30,11 +30,11 @@ MouseArea {
queryField.text = ""; queryField.text = "";
} }
signal requestedClose() signal requestedClose(triggeredByKeyEvent: bool)
Keys.onPressed: (event) => { Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
root.requestedClose(); root.requestedClose(true);
event.accepted = true; event.accepted = true;
} }
} }
@ -72,7 +72,21 @@ MouseArea {
font.weight: Font.Bold 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 { QQC2.ScrollView {
@ -84,7 +98,7 @@ MouseArea {
Layout.rightMargin: Kirigami.Units.gridUnit Layout.rightMargin: Kirigami.Units.gridUnit
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Milou.ResultsListView { Milou.ResultsView {
id: listView id: listView
queryString: queryField.text queryString: queryField.text
clip: true clip: true
@ -97,7 +111,7 @@ MouseArea {
} }
onActivated: { onActivated: {
root.requestedClose(); root.requestedClose(false);
} }
onUpdateQueryString: { onUpdateQueryString: {
queryField.text = text queryField.text = text
@ -139,12 +153,12 @@ MouseArea {
// Close search view if we press down with last item selected // Close search view if we press down with last item selected
Keys.onPressed: (event) => { Keys.onPressed: (event) => {
if (event.key === Qt.Key_Down && (model.index === listView.count - 1)) { if (event.key === Qt.Key_Down && (model.index === listView.count - 1)) {
root.requestedClose(); root.requestedClose(true);
event.accepted = true; event.accepted = true;
} }
} }
// Used by ResultsListView to determine next tab action // Used by ResultsView to determine next tab action
function activateNextAction() { function activateNextAction() {
queryField.forceActiveFocus(); queryField.forceActiveFocus();
queryField.selectAll(); queryField.selectAll();
@ -155,7 +169,7 @@ MouseArea {
listView.currentIndex = model.index; listView.currentIndex = model.index;
listView.runCurrentIndex(); listView.runCurrentIndex();
root.requestedClose(); root.requestedClose(false);
} }
hoverEnabled: true hoverEnabled: true

View file

@ -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()
}
}
}
}

View file

@ -30,6 +30,13 @@ Item {
} }
} }
// darken background when pressed
Rectangle {
anchors.fill: parent
color: "black"
opacity: root.darken ? 0.05 : 0
}
Image { Image {
id: img id: img
source: root.imageSource source: root.imageSource
@ -41,9 +48,12 @@ Item {
// ensure text is readable // ensure text is readable
Rectangle { Rectangle {
anchors.fill: parent 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 // apply lighten, saturate and blur effect
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { layer.effect: MultiEffect {

View file

@ -21,8 +21,7 @@ Item {
id: root id: root
visible: sourceRepeater.count > 0 visible: sourceRepeater.count > 0
property bool inActionDrawer: false property int panelType: MobileShell.PanelBackground.PanelType.Drawer
property bool detailledView: false property bool detailledView: false
readonly property real padding: Kirigami.Units.gridUnit readonly property real padding: Kirigami.Units.gridUnit
@ -34,8 +33,8 @@ Item {
Behavior on implicitHeight { Behavior on implicitHeight {
NumberAnimation { NumberAnimation {
duration: implicitHeight == 0 ? 0 : Kirigami.Units.shortDuration duration: implicitHeight == 0 ? 0 : Kirigami.Units.longDuration
easing.type: Easing.InOutQuad easing.type: Easing.OutQuart
} }
} }
@ -63,30 +62,9 @@ Item {
} }
} }
// shadow MobileShell.PanelBackground {
MultiEffect { anchors.fill: parent
anchors.fill: root panelType: root.panelType
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
} }
// list of app media widgets // list of app media widgets
@ -160,7 +138,6 @@ Item {
padding: root.padding padding: root.padding
contentItem: ColumnLayout { contentItem: ColumnLayout {
Kirigami.Theme.inherit: true
width: playerItem.width - playerItem.leftPadding - playerItem.rightPadding width: playerItem.width - playerItem.leftPadding - playerItem.rightPadding
spacing: Kirigami.Units.largeSpacing spacing: Kirigami.Units.largeSpacing

View file

@ -9,7 +9,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Window 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.plasma.components 3.0 as PlasmaComponents3
import org.kde.notificationmanager as NotificationManager import org.kde.notificationmanager as NotificationManager
@ -35,7 +35,7 @@ Item {
property var model property var model
property int modelIndex property int modelIndex
property P5Support.DataSource timeSource property Clock clockSource
readonly property int notificationType: model.type readonly property int notificationType: model.type
@ -158,7 +158,6 @@ Item {
notificationsModel.invokeAction(notificationsModel.index(modelIndex, 0), actionName, NotificationManager.Close); // notification closes notificationsModel.invokeAction(notificationsModel.index(modelIndex, 0), actionName, NotificationManager.Close); // notification closes
} }
} }
expire();
} }
if (notificationItem.requestToInvoke) { if (notificationItem.requestToInvoke) {

View file

@ -9,26 +9,64 @@ import Qt5Compat.GraphicalEffects
import org.kde.kirigami 2.12 as Kirigami import org.kde.kirigami 2.12 as Kirigami
import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell as MobileShell
Item { Item {
id: root id: root
/**
* The content that goes inside the notification card
*/
default property Item contentItem 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 property bool popupNotification: false
/**
* Whether this popup notification is tucked underneath the current popup.
*/
property bool inPopupDrawer: false property bool inPopupDrawer: false
/**
* Whether this notification is within the lockscreen.
*/
property bool inLockScreen: false
/**
* The current notification popup height.
*/
property int currentPopupHeight: 0 property int currentPopupHeight: 0
/**
* The remaining time before the notification popup is dismissed.
*/
property real remainingTimeProgress: 1 property real remainingTimeProgress: 1
/**
* Whether the timer for dismissing the notification popup is running.
*/
property bool closeTimerRunning: false property bool closeTimerRunning: false
/**
* Whether tapping on this notification is enabled.
*/
property bool tapEnabled: false property bool tapEnabled: false
/**
* Whether swipping on this notification is enabled.
*/
property bool swipeGestureEnabled: false property bool swipeGestureEnabled: false
/**
* The current drag offset for this notification.
*/
property real dragOffset: 0 property real dragOffset: 0
signal tapped() signal tapped()
@ -57,35 +95,12 @@ Item {
} }
} }
// shadow MobileShell.PanelBackground {
MultiEffect {
anchors.fill: mainCard anchors.fill: mainCard
visible: Math.abs(dragOffset) !== root.width animate: true
source: simpleShadow panelType: root.panelType
blurMax: 16
shadowEnabled: true
shadowVerticalOffset: 1
shadowOpacity: popupNotification ? 0.85 : 0.5
shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.2)
} }
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 // card
Item { Item {
id: mainCard 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 { ProgressBar {
id: progress id: progress
anchors.left: parent.left anchors.left: parent.left

View file

@ -12,7 +12,6 @@ import QtQuick.Window 2.2
import org.kde.kirigami 2.12 as Kirigami import org.kde.kirigami 2.12 as Kirigami
import org.kde.plasma.core as PlasmaCore 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.components 3.0 as PlasmaComponents
import org.kde.notificationmanager as NotificationManager import org.kde.notificationmanager as NotificationManager
@ -21,7 +20,6 @@ import org.kde.coreaddons 1.0 as KCoreAddons
RowLayout { RowLayout {
id: notificationHeading id: notificationHeading
property bool inLockscreen: false
property var applicationIconSource property var applicationIconSource
property string applicationName property string applicationName
property string originName property string originName
@ -44,7 +42,7 @@ RowLayout {
Layout.leftMargin: Kirigami.Units.smallSpacing Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.fillWidth: true Layout.fillWidth: true
color: inLockscreen ? "white" : Kirigami.Theme.textColor color: Kirigami.Theme.textColor
opacity: 0.75 opacity: 0.75
elide: Text.ElideLeft elide: Text.ElideLeft

View file

@ -24,7 +24,10 @@ BaseNotificationItem {
id: notificationItem id: notificationItem
implicitHeight: mainCard.implicitHeight + mainCard.anchors.topMargin + notificationHeading.height 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 dragStart()
signal dragEnd() signal dragEnd()
@ -37,11 +40,6 @@ BaseNotificationItem {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
inLockscreen: root.inLockscreen
Kirigami.Theme.colorSet: Kirigami.Theme.Header
Kirigami.Theme.inherit: false
visible: !notificationItem.inGroup visible: !notificationItem.inGroup
height: visible ? implicitHeight : 0 height: visible ? implicitHeight : 0
@ -62,10 +60,15 @@ BaseNotificationItem {
onTapped: notificationItem.actionInvoked("default"); onTapped: notificationItem.actionInvoked("default");
swipeGestureEnabled: notificationItem.closable swipeGestureEnabled: notificationItem.closable
onDismissRequested: notificationItem.close(); onDismissRequested: notificationItem.close();
inLockScreen: notificationItem.inLockScreen
panelType: notificationItem.panelType
onDragStart: notificationItem.dragStart() onDragStart: notificationItem.dragStart()
onDragEnd: notificationItem.dragEnd() onDragEnd: notificationItem.dragEnd()
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: notificationItem.cardColorScheme
ColumnLayout { ColumnLayout {
id: column id: column
spacing: 0 spacing: 0
@ -98,7 +101,7 @@ BaseNotificationItem {
jobDetails: notificationItem.jobDetails jobDetails: notificationItem.jobDetails
time: notificationItem.time time: notificationItem.time
timeSource: notificationItem.timeSource clockSource: notificationItem.clockSource
} }
} }

View file

@ -29,7 +29,8 @@ BaseNotificationItem {
property real remainingTimeProgress: 1 property real remainingTimeProgress: 1
property bool closeTimerRunning: false property bool closeTimerRunning: false
property bool inLockscreen: false property bool inLockScreen: false
property int panelType: MobileShell.PanelBackground.PanelType.Drawer
signal dragStart() signal dragStart()
signal dragEnd() signal dragEnd()
@ -48,6 +49,8 @@ BaseNotificationItem {
currentPopupHeight: notificationItem.currentPopupHeight currentPopupHeight: notificationItem.currentPopupHeight
remainingTimeProgress: notificationItem.remainingTimeProgress remainingTimeProgress: notificationItem.remainingTimeProgress
closeTimerRunning: notificationItem.closeTimerRunning closeTimerRunning: notificationItem.closeTimerRunning
inLockScreen: notificationItem.inLockScreen
panelType: notificationItem.panelType
tapEnabled: notificationItem.hasDefaultAction tapEnabled: notificationItem.hasDefaultAction
onTapped: notificationItem.actionInvoked("default"); onTapped: notificationItem.actionInvoked("default");
swipeGestureEnabled: notificationItem.closable swipeGestureEnabled: notificationItem.closable
@ -110,7 +113,7 @@ BaseNotificationItem {
jobDetails: notificationItem.jobDetails jobDetails: notificationItem.jobDetails
time: notificationItem.time time: notificationItem.time
timeSource: notificationItem.timeSource clockSource: notificationItem.clockSource
} }
} }

View file

@ -10,7 +10,7 @@ import QtQuick.Layouts 1.1
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import org.kde.plasma.private.mobileshell as MobileShell 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.plasma.components 3.0 as PlasmaComponents
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
@ -26,15 +26,15 @@ PlasmaComponents.Label {
property QtObject jobDetails property QtObject jobDetails
property var time property var time
property P5Support.DataSource timeSource property Clock clockSource
// notification created/updated time changed // notification created/updated time changed
onTimeChanged: updateAgoText() onTimeChanged: updateAgoText()
Connections { Connections {
target: timeSource target: clockSource
// clock time changed // clock time changed
function onDataChanged() { function onTimeChanged() {
ageLabel.updateAgoText() ageLabel.updateAgoText()
} }
} }

View file

@ -5,6 +5,7 @@
*/ */
.import org.kde.notificationmanager as NotificationManager .import org.kde.notificationmanager as NotificationManager
.import org.kde.coreaddons 1.0 as KCoreAddons
.import QtQml as QtQml .import QtQml as QtQml
function determineNotificationHeadingText(notificationItem) { function determineNotificationHeadingText(notificationItem) {

View file

@ -12,9 +12,10 @@ import Qt5Compat.GraphicalEffects
import org.kde.kirigami 2.12 as Kirigami 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 as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings 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.extras 2.0 as PlasmaExtras
import org.kde.plasma.components 3.0 as PlasmaComponents3 import org.kde.plasma.components 3.0 as PlasmaComponents3
@ -27,7 +28,20 @@ import org.kde.notificationmanager as NotificationManager
Item { Item {
id: root 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. * The notification model for the widget.
@ -53,6 +67,31 @@ Item {
*/ */
property bool actionsRequireUnlock: false 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. * Whether the widget has notifications.
*/ */
@ -73,16 +112,14 @@ Item {
*/ */
signal unlockRequested() 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. * Run pending action that was pending for authentication when unlockRequested() was emitted.
*/ */
function runPendingAction() { 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() { function setDoNotDisturbMode(doNotDisturb: bool) {
if (doNotDisturbModeEnabled) { if (!doNotDisturb) {
notificationSettings.defaults(); // Turn off do not disturb
} else {
var until = new Date();
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); until.setFullYear(until.getFullYear() + 1);
notificationSettings.notificationsInhibitedUntil = until; notificationSettings.notificationsInhibitedUntil = until;
} }
notificationSettings.save(); notificationSettings.save();
} }
@ -120,19 +165,19 @@ Item {
MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_notifications"); MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_notifications");
} }
P5Support.DataSource { // Implement listening to system "do not disturb" requests
id: timeDataSource Connections {
engine: "time" target: MobileShellState.ShellDBusClient
connectedSources: ["Local"]
interval: 60000 // 1 min function onDoNotDisturbChanged() {
intervalAlignment: P5Support.Types.AlignToMinute if (root.doNotDisturbModeEnabled !== MobileShellState.ShellDBusClient.doNotDisturb) {
root.setDoNotDisturbMode(MobileShellState.ShellDBusClient.doNotDisturb);
}
}
} }
// implement background clicking signal Clock {
MouseArea { id: clock
anchors.fill: parent
onClicked: backgroundClicked()
z: -1 // ensure that this is below notification items so we don't steal button clicks
} }
ListView { ListView {
@ -143,15 +188,16 @@ Item {
currentIndex: 0 currentIndex: 0
property var pendingNotificationWithAction property NotificationItem pendingNotificationWithAction: null
readonly property int animationDuration: ShellSettings.Settings.animationsEnabled ? Kirigami.Units.longDuration : 0 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. // 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 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 { anchors {
top: parent.top top: parent.top
@ -159,7 +205,7 @@ Item {
right: parent.right right: parent.right
} }
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.DragAndOvershootBounds
spacing: Kirigami.Units.largeSpacing spacing: Kirigami.Units.largeSpacing
// TODO keyboard focus // TODO keyboard focus
@ -167,6 +213,39 @@ Item {
highlightResizeDuration: 0 highlightResizeDuration: 0
highlight: Item {} 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 { section {
property: "isGroup" property: "isGroup"
criteria: ViewSection.FullString criteria: ViewSection.FullString
@ -250,12 +329,28 @@ Item {
PropertyAction { target: delegateLoader; property: "ListView.delayRemove"; value: false } PropertyAction { target: delegateLoader; property: "ListView.delayRemove"; value: false }
} }
// adjust top paddding for media control widget
Component { Component {
id: groupDelegate id: groupDelegate
NotificationGroupHeader {
applicationName: model.applicationName Column {
applicationIconSource: model.applicationIconName spacing: Kirigami.Units.smallSpacing
originName: model.originName || ""
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 { Column {
spacing: Kirigami.Units.smallSpacing 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 { NotificationItem {
id: notificationItem id: notificationItem
width: parent.width width: parent.width
height: implicitHeight height: implicitHeight
inLockscreen: root.inLockscreen inLockScreen: root.inLockScreen
panelType: root.panelType
cardColorScheme: root.cardColorScheme
model: delegateLoader.model model: delegateLoader.model
modelIndex: delegateLoader.index modelIndex: delegateLoader.index
notificationsModel: root.historyModel notificationsModel: root.historyModel
notificationsModelType: root.historyModelType notificationsModelType: root.historyModelType
timeSource: timeDataSource clockSource: clock
requestToInvoke: root.actionsRequireUnlock requestToInvoke: root.actionsRequireUnlock
onRunActionRequested: { onRunActionRequested: {
@ -303,7 +407,7 @@ Item {
return false; return false;
return (model.groupChildrenCount > model.expandedGroupChildrenCount || model.isGroupExpanded) 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. // 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 { sourceComponent: PlasmaComponents3.ToolButton {
icon.name: model.isGroupExpanded ? "arrow-up" : "arrow-down" icon.name: model.isGroupExpanded ? "arrow-up" : "arrow-down"
text: model.isGroupExpanded ? i18n("Show Fewer") text: model.isGroupExpanded ? i18n("Show Fewer")
: i18nc("Expand to show n more notifications", : i18nc("Expand to show n more notifications",
"Show %1 More", (model.groupChildrenCount - model.expandedGroupChildrenCount)) "Show %1 More", (model.groupChildrenCount - model.expandedGroupChildrenCount))
onClicked: { onClicked: {
list.setGroupExpanded(model.index, !model.isGroupExpanded) 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