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:
- /gitlab-templates/reuse-lint.yml
- /gitlab-templates/linux-qt6.yml
- /gitlab-templates/linux-qt6-next.yml
- /gitlab-templates/alpine-qt6.yml
- /gitlab-templates/xml-lint.yml
- /gitlab-templates/yaml-lint.yml
- /gitlab-templates/qml-lint.yml
posix-script-compliance:
image:

View file

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

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-License-Identifier: GPL-2.0-or-later
cmake_minimum_required(VERSION 3.16)
cmake_minimum_required(VERSION 3.24)
project(plasma-mobile)
set(PROJECT_VERSION "6.2.80")
set(PROJECT_VERSION_MAJOR 6)
set(PROJECT_VERSION "6.6.80")
set(PROJECT_DEP_VERSION "6.2.0")
set(QT_MIN_VERSION "6.7.0")
set(KF6_MIN_VERSION "6.5.0")
set(PROJECT_DEP_VERSION "6.5.90")
set(QT_MIN_VERSION "6.10.0")
set(KF6_MIN_VERSION "6.22.0")
set(KDE_COMPILERSETTINGS_LEVEL "5.82")
set(CMAKE_CXX_STANDARD 20)
@ -35,7 +34,9 @@ include(ECMMarkNonGuiExecutable)
include(ECMGenerateHeaders)
include(ECMFindQmlModule)
include(ECMGenerateQmlTypes)
include(ECMConfiguredInstall)
include(ECMFindQmlModule)
include(ECMQtDeclareLoggingCategory)
include(GenerateExportHeader)
include(KDEGitCommitHooks)
include(KDEClangFormat)
@ -57,6 +58,10 @@ find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED
WaylandClient
)
if(Qt6WaylandClient_VERSION VERSION_GREATER_EQUAL "6.10.0")
find_package(Qt6 REQUIRED COMPONENTS WaylandClientPrivate)
endif()
find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS
I18n
GlobalAccel
@ -71,6 +76,7 @@ find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS
KCMUtils
Package
JobWidgets
WindowSystem
)
find_package(Plasma CONFIG REQUIRED)
@ -78,16 +84,14 @@ find_package(PlasmaQuick CONFIG REQUIRED)
find_package(PlasmaActivities CONFIG REQUIRED)
find_package(KF6Screen CONFIG REQUIRED)
find_package(KWayland CONFIG REQUIRED)
find_package(KPipeWire ${PROJECT_DEP_VERSION} REQUIRED)
find_package(PkgConfig REQUIRED)
find_package(QCoro6 REQUIRED COMPONENTS DBus)
find_package(QCoro6 REQUIRED COMPONENTS DBus Qml)
qcoro_enable_coroutines()
kde_enable_exceptions()
pkg_check_modules(GOBJECT gobject-2.0 REQUIRED IMPORTED_TARGET)
pkg_check_modules(GIO gio-2.0 REQUIRED IMPORTED_TARGET)
find_package(KF6KirigamiAddons 0.6 REQUIRED)
find_package(epoxy REQUIRED)
find_package(XCB REQUIRED COMPONENTS XCB)
@ -96,6 +100,14 @@ find_package(KWin ${PROJECT_DEP_VERSION} REQUIRED COMPONENTS
)
find_package(LayerShellQt REQUIRED)
find_package(Wayland REQUIRED)
find_package(PlasmaWaylandProtocols 1.8 CONFIG)
set_package_properties(PlasmaWaylandProtocols PROPERTIES
TYPE REQUIRED
PURPOSE "Collection of Plasma-specific Wayland protocols"
URL "https://invent.kde.org/libraries/plasma-wayland-protocols/"
)
find_package(LibKWorkspace CONFIG REQUIRED)
find_package(Libudev REQUIRED)
@ -106,6 +118,12 @@ set_package_properties(KWinDBusInterface PROPERTIES DESCRIPTION "KWin DBus inter
PURPOSE "Needed for virtual keyboard toggle button"
)
find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS Auth)
set_package_properties(KF6::AuthCore PROPERTIES
TYPE REQUIRED
PURPOSE "Allows Plasma Mobile to configure Waydroid"
)
include(CheckIncludeFiles)
ecm_find_qmlmodule(org.kde.pipewire 0.1)
@ -116,6 +134,7 @@ plasma_install_package(shell org.kde.plasma.mobileshell shells)
add_subdirectory(bin)
add_subdirectory(components)
add_subdirectory(containments)
add_subdirectory(devices)
add_subdirectory(quicksettings)
add_subdirectory(kcms)
add_subdirectory(kded)
@ -127,6 +146,8 @@ if(BUILD_TESTING)
add_subdirectory(tests)
endif()
install(FILES org.kde.plasma.mobileshell.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
find_program(PlasmaOpenSettings plasma-open-settings)
set_package_properties(PlasmaOpenSettings PROPERTIES
URL https://invent.kde.org/plasma/kde-cli-tools/
@ -146,6 +167,6 @@ ki18n_install(po)
if (INSTALL_SYSTEMD_SERVICE)
install(FILES
${CMAKE_CURRENT_SOURCE_DIR}/plasma-mobile.service
DESTINATION ${KDE_INSTALL_SYSTEMDUNITDIR}
DESTINATION ${KDE_INSTALL_SYSTEMDUNITDIR}/system
)
endif()

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:
@ -6,6 +6,4 @@ Redistribution and use in source and binary forms, with or without modification,
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

170
LICENSES/CC-BY-SA-4.0.txt Normal file
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)
* Kirigami
* Kirigami Addons
* hfd-service (optional: for vibrations)
* feedbackd (optional: for vibrations)
To start the shell in a window, run:
@ -61,6 +61,10 @@ Useful options:
QT_QPA_PLATFORM=wayland dbus-run-session kwin_wayland --xwayland "plasmashell -p org.kde.plasma.mobileshell" --output-count 2 --width 360 --height 720
```
### Device specific configuration
See [/devices/README.md](/devices/README.md) for more details on setting device specific configuration (ex. notches, screen curves).
---
<img src="https://invent.kde.org/plasma/plasma-mobile/-/wikis/uploads/19a607bb68faa76bbc9f888e33a3aa9a/konqi-calling.png" width=200px>

View file

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

View file

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

View file

@ -7,7 +7,6 @@
[ -f /etc/profile ] && . /etc/profile
export QT_QPA_PLATFORM=wayland
export QT_QPA_PLATFORMTHEME=KDE
export EGL_PLATFORM=wayland
@ -17,6 +16,9 @@ export QT_QUICK_CONTROLS_MOBILE=true
export PLASMA_INTEGRATION_USE_PORTAL=1
export PLASMA_PLATFORM=phone:handset
# Set ~/.config/plasma-mobile/... as location for default mobile configs (i.e. envmanager generated)
export XDG_CONFIG_DIRS="$HOME/.config/plasma-mobile:/etc/xdg:$XDG_CONFIG_DIRS"
# if coredumpd knows about the dumps, make sure drkonqi catches them
if grep -q '/systemd-coredump' /proc/sys/kernel/core_pattern
then
@ -28,4 +30,4 @@ QT_QPA_PLATFORM=offscreen plasma-mobile-envmanager --apply-settings
# start the shell
export PLASMA_DEFAULT_SHELL=org.kde.plasma.mobileshell
startplasma-wayland --xwayland --inputmethod maliit-keyboard
startplasma-wayland

View file

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

View file

@ -1,12 +1,15 @@
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later
qt_add_dbus_interfaces(DBUS_SRCS dbus/com.lomiri.hfd.xml)
set_source_files_properties(dbus/org.sigxcpu.Feedback.Haptic.xml PROPERTIES INCLUDE vibrationevent.h)
qt_add_dbus_interfaces(dbusinterface_SRCS
dbus/org.sigxcpu.Feedback.Haptic.xml)
ecm_add_qml_module(hapticsplugin URI org.kde.plasma.private.mobileshell.hapticsplugin GENERATE_PLUGIN_SOURCE)
target_sources(hapticsplugin PRIVATE
vibrationevent.h
vibrationmanager.cpp
${DBUS_SRCS}
${dbusinterface_SRCS}
)
target_link_libraries(hapticsplugin PRIVATE
@ -14,6 +17,8 @@ target_link_libraries(hapticsplugin PRIVATE
Qt::DBus
KF6::CoreAddons
KF6::I18n
QCoro::DBus
QCoro::Qml
)
ecm_finalize_qml_module(hapticsplugin)

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
#include "vibrationmanager.h"
#include <QPointer>
VibrationManager::VibrationManager(QObject *parent)
: QObject{parent}
{
qDBusRegisterMetaType<VibrationEvent>();
qDBusRegisterMetaType<VibrationEventList>();
}
void VibrationManager::vibrate(int durationMs)
QCoro::Task<void> VibrationManager::vibrateTask(int durationMs)
{
// Only create interface when needed.
if (!m_interface) {
const auto objectPath = QStringLiteral("/com/lomiri/hfd");
m_interface = new com::lomiri::hfd::Vibrator("com.lomiri.hfd", objectPath, QDBusConnection::systemBus(), this);
const auto objectPath = QStringLiteral("/org/sigxcpu/Feedback");
m_interface = new OrgSigxcpuFeedbackHapticInterface("org.sigxcpu.Feedback", objectPath, QDBusConnection::sessionBus(), this);
}
const QString appId = QStringLiteral("org.kde.plasmashell");
const VibrationEvent event{1.0, static_cast<quint32>(durationMs)};
const VibrationEventList pattern = {event};
QPointer<VibrationManager> guard(this);
QDBusPendingReply<bool> reply = co_await m_interface->Vibrate(appId, pattern);
if (!guard) {
co_return;
}
if (!reply.isValid() || !reply.value()) {
qWarning() << "feedbackd vibration failed";
}
m_interface->vibrate(durationMs);
}
QCoro::QmlTask VibrationManager::vibrate(int durationMs)
{
return vibrateTask(durationMs);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -11,27 +11,136 @@ set(mobileshellplugin_SRCS
notifications/notificationthumbnailer.cpp
notifications/notificationfilemenu.cpp
notifications/notificationfileinfo.cpp
masklayer/masklayer.cpp
masklayer/maskmanager.cpp
)
target_include_directories(mobileshellplugin PRIVATE components)
target_include_directories(mobileshellplugin PRIVATE notifications)
target_include_directories(mobileshellplugin PRIVATE masklayer)
target_sources(mobileshellplugin PRIVATE ${mobileshellplugin_SRCS})
# Singleton declarations
set_source_files_properties(qml/components/AppLaunch.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
set_source_files_properties(qml/components/Constants.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
set_source_files_properties(qml/dataproviders/AudioInfo.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
set_source_files_properties(qml/dataproviders/BatteryInfo.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
set_source_files_properties(qml/dataproviders/BluetoothInfo.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
set_source_files_properties(qml/dataproviders/SignalStrengthInfo.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
set_source_files_properties(qml/notificationpopup/NotificationPopupProviderLoader.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
set_source_files_properties(qml/volumeosd/VolumeOSDProviderLoader.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
# Include qml and js files within ./qml/
file(GLOB_RECURSE _qml_sources
"qml/*.qml"
"qml/*.js"
set_source_files_properties(
qml/components/AppLaunch.qml
qml/components/Constants.qml
qml/dataproviders/AudioInfo.qml
qml/dataproviders/BatteryInfo.qml
qml/dataproviders/BluetoothInfo.qml
qml/dataproviders/NetworkInfo.qml
qml/dataproviders/SignalStrengthInfo.qml
qml/popups/PopupProviderLoader.qml
PROPERTIES
QT_QML_SINGLETON_TYPE TRUE
)
# QML files meant to be used publicly
# TODO: move files gradually out of main namespace into sub namespaces
ecm_target_qml_sources(mobileshellplugin SOURCES
qml/actiondrawer/ActionDrawer.qml
qml/actiondrawer/ActionDrawerOpenSurface.qml
qml/actiondrawer/ActionDrawerWindow.qml
qml/components/AppLaunch.qml
qml/components/BaseItem.qml
qml/components/Constants.qml
qml/components/Flickable.qml
qml/components/FlickableOpacityGradient.qml
qml/components/GridView.qml
qml/components/HapticsEffect.qml
qml/components/ListView.qml
qml/components/MarqueeLabel.qml
qml/components/PanelBackground.qml
qml/components/ScreenEdgeDragEffect.qml
qml/components/StartupFeedbackPanelFill.qml
qml/components/StartupFeedbackWindows.qml
qml/components/TextDropShadow.qml
qml/components/VelocityCalculator.qml
qml/dataproviders/AudioInfo.qml
qml/dataproviders/BatteryInfo.qml
qml/dataproviders/BluetoothInfo.qml
qml/dataproviders/NetworkInfo.qml
qml/dataproviders/SignalStrengthInfo.qml
qml/homescreen/BlurEffect.qml
qml/homescreen/DeviceLock.qml
qml/homescreen/HomeScreen.qml
qml/homescreen/WallpaperSelector.qml
qml/navigationpanel/GesturePanel.qml
qml/navigationpanel/NavigationPanel.qml
qml/navigationpanel/NavigationPanelAction.qml
qml/navigationpanel/NavigationPanelButton.qml
qml/popups/actionbuttons/ActionButton.qml
qml/popups/actionbuttons/ActionButtonsProvider.qml
qml/popups/actionbuttons/RotationButton.qml
qml/popups/notifications/NotificationPopup.qml
qml/popups/notifications/NotificationPopupManager.qml
qml/popups/notifications/NotificationPopupProvider.qml
qml/popups/notifications/PulseAudio.qml
qml/popups/kscreenosd/KScreenOSDProvider.qml
qml/popups/volumeosd/AudioApplet.qml
qml/popups/volumeosd/DeviceListItem.qml
qml/popups/volumeosd/icon.js
qml/popups/volumeosd/ListItemBase.qml
qml/popups/volumeosd/PopupCard.qml
qml/popups/volumeosd/StreamListItem.qml
qml/popups/volumeosd/VolumeChangedPopup.qml
qml/popups/volumeosd/VolumeOSD.qml
qml/popups/volumeosd/VolumeOSDProvider.qml
qml/popups/volumeosd/VolumeSlider.qml
qml/popups/PopupProviderLoader.qml
qml/statusbar/indicators/BatteryIndicator.qml
qml/statusbar/indicators/BluetoothIndicator.qml
qml/statusbar/indicators/InternetIndicator.qml
qml/statusbar/indicators/SignalStrengthIndicator.qml
qml/statusbar/indicators/VolumeIndicator.qml
qml/statusbar/ClockText.qml
qml/statusbar/StatusBar.qml
qml/widgets/krunner/KRunnerScreen.qml
qml/widgets/mediacontrols/BlurredBackground.qml
qml/widgets/mediacontrols/MediaControlsSource.qml
qml/widgets/mediacontrols/MediaControlsWidget.qml
qml/widgets/notifications/BaseNotificationItem.qml
qml/widgets/notifications/NotificationBodyLabel.qml
qml/widgets/notifications/NotificationCard.qml
qml/widgets/notifications/NotificationEditContextMenu.qml
qml/widgets/notifications/NotificationFooterActions.qml
qml/widgets/notifications/NotificationGroupHeader.qml
qml/widgets/notifications/NotificationItem.qml
qml/widgets/notifications/NotificationJobDetails.qml
qml/widgets/notifications/NotificationJobItem.qml
qml/widgets/notifications/NotificationPopupItem.qml
qml/widgets/notifications/NotificationReplyField.qml
qml/widgets/notifications/NotificationsModelType.qml
qml/widgets/notifications/NotificationsUtils.js
qml/widgets/notifications/NotificationsWidget.qml
qml/widgets/notifications/NotificationTimeText.qml
qml/widgets/notifications/ThumbnailStrip.qml
)
# Other supporting QML files
ecm_target_qml_sources(mobileshellplugin SOURCES
qml/actiondrawer/private/BrightnessItem.qml
qml/actiondrawer/private/Handle.qml
qml/actiondrawer/private/QuickSettings.qml
qml/actiondrawer/private/QuickSettingsDelegate.qml
qml/actiondrawer/private/QuickSettingsDrawer.qml
qml/actiondrawer/private/QuickSettingsFullDelegate.qml
qml/actiondrawer/private/QuickSettingsMinimizedDelegate.qml
qml/actiondrawer/private/QuickSettingsPanel.qml
qml/actiondrawer/private/ContentContainer.qml
qml/actiondrawer/private/LandscapeContentContainer.qml
qml/actiondrawer/private/NotificationDrawer.qml
qml/actiondrawer/private/PortraitContentContainer.qml
PATH actiondrawer/private
)
ecm_target_qml_sources(mobileshellplugin SOURCES ${_qml_sources})
target_link_libraries(mobileshellplugin

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.private.nanoshell 2.0 as NanoShell
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
import org.kde.plasma.private.mobileshell as MobileShell
import 'actiondrawer/private'
Item {
id: root
/*
* The intended visiblity of the action drawer.
* The intended visibility of the action drawer.
*
* This is separate from "visible" in order to avoid having to set
* item visiblity when its on its own window (wasteful since the window itself can be shown/hidden).
* item visibility when it's on its own window (wasteful since the window itself can be shown/hidden).
*/
property bool intendedToBeVisible: false
@ -59,6 +61,19 @@ Item {
*/
property real offset: 0
/**
* Same as offset value except this adds resistance when passing the open position of the current drawer state.
*/
readonly property real offsetResistance: {
if (!openToPinnedMode) {
return root.calculateResistance(offset, contentContainer.maximizedQuickSettingsOffset);
} else if (!opened) {
return root.calculateResistance(offset, contentContainer.minimizedQuickSettingsOffset);
} else {
return root.calculateResistance(offset, contentContainer.maximizedQuickSettingsOffset);
}
}
/**
* Whether the panel is being dragged.
*/
@ -80,15 +95,10 @@ Item {
*/
property int direction: MobileShell.Direction.None
/**
* The notifications widget being shown. May be null.
*/
property var notificationsWidget: contentContainer.notificationsWidget
/**
* The mode of the action drawer (portrait or landscape).
*/
property int mode: (height > width && width <= largePortraitThreshold) ? ActionDrawer.Portrait : ActionDrawer.Landscape
property int mode: (height > width && width <= largePortraitThreshold) ? MobileShell.ActionDrawer.Portrait : MobileShell.ActionDrawer.Landscape
/**
* At some point, even if the screen is technically portrait, if we have a ton of width it'd be best to just show the landscape mode.
@ -124,10 +134,6 @@ Item {
*/
signal runPendingNotificationAction()
onOpenedChanged: {
if (opened) swipeArea.focus = true;
}
property real oldOffset
onOffsetChanged: {
if (offset < 0) {
@ -135,13 +141,13 @@ Item {
}
root.direction = (oldOffset === offset)
? MobileShell.Direction.None
: (offset > oldOffset ? MobileShell.Direction.Down : MobileShell.Direction.Up);
? MobileShell.Direction.None
: (offset > oldOffset ? MobileShell.Direction.Down : MobileShell.Direction.Up);
oldOffset = offset;
// close panel immediately after panel is not shown, and the flickable is not being dragged
if (opened && root.offset <= 0 && !swipeArea.moving && !drawerAnimation.running) {
if (opened && root.offset <= 0 && !contentContainer.swipeAreaMoving && !drawerAnimation.running) {
root.state = "";
offset = 0;
focus = false;
@ -150,6 +156,15 @@ Item {
}
}
// calculates offset resistance for the action drawer overshoots it's open position
function calculateResistance(value : double, threshold : int) : double {
if (value > threshold) {
return threshold + Math.pow(value - threshold + 1, Math.max(0.8 - (value - threshold) / ((root.height - threshold) * 15), 0.35));
} else {
return value;
}
}
function cancelAnimations() {
root.state = "";
}
@ -249,7 +264,9 @@ Item {
SequentialAnimation {
PropertyAnimation {
id: drawerAnimation
properties: "offset"; easing.type: Easing.OutExpo; duration: root.state != "" ? Kirigami.Units.veryLongDuration : 0
properties: "offset"
easing.type: Easing.OutExpo
duration: root.state != "" ? Kirigami.Units.veryLongDuration : 0
}
ScriptAction {
script: {
@ -267,45 +284,12 @@ Item {
}
}
MobileShell.SwipeArea {
id: swipeArea
mode: MobileShell.SwipeArea.VerticalOnly
// action drawer ui content
ContentContainer {
id: contentContainer
anchors.fill: parent
function startSwipe() {
root.cancelAnimations();
root.dragging = true;
// Immediately open action drawer if we interact with it and it's already open
// This allows us to have 2 quick flicks from minimized -> expanded
if (root.visible && !root.opened) {
root.opened = true;
}
}
function endSwipe() {
root.dragging = false;
root.updateState();
}
function moveSwipe(totalDeltaX, totalDeltaY, deltaX, deltaY) {
root.offset += deltaY;
}
onSwipeStarted: startSwipe()
onSwipeEnded: endSwipe()
onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => moveSwipe(totalDeltaX, totalDeltaY, deltaX, deltaY)
onTouchpadScrollStarted: startSwipe()
onTouchpadScrollEnded: endSwipe()
onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => moveSwipe(totalDeltaX, totalDeltaY, deltaX, deltaY)
ContentContainer {
id: contentContainer
anchors.fill: parent
actionDrawer: root
quickSettingsModel: root.quickSettingsModel
}
actionDrawer: root
quickSettingsModel: root.quickSettingsModel
}
}

View file

@ -40,7 +40,7 @@ Window {
color: "transparent"
// set input to transparent when closing to prevent window from taking unwanted touch inputs
onStateChanged: ShellUtil.setInputTransparent(window, state == "close")
onStateChanged: MobileShell.ShellUtil.setInputTransparent(window, state === "close")
onVisibleChanged: {
if (visible) {

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

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

View file

@ -11,6 +11,7 @@ import QtQuick.Layouts 1.1
import QtQuick.Window 2.2
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
import org.kde.kirigami 2.20 as Kirigami
@ -20,13 +21,15 @@ import org.kde.kirigami 2.20 as Kirigami
*/
Item {
id: root
clip: true
// to prevent clipping off the shadows form the BrightnessItem when the rest of the action panel view
// is transparent, we stop clipping the base view when fullViewProgress is not less then 1
clip: fullViewProgress < 1
required property var actionDrawer
required property QS.QuickSettingsModel quickSettingsModel
readonly property real columns: Math.round(Math.min(6, Math.max(3, width / intendedColumnWidth)))
readonly property real columns: Math.round(Math.min(6, Math.max(ShellSettings.Settings.quickSettingsColumns, width / intendedColumnWidth)))
readonly property real columnWidth: Math.floor(width / columns)
readonly property int minimizedColumns: Math.round(Math.min(8, Math.max(5, width / intendedMinimizedColumnWidth)))
readonly property real minimizedColumnWidth: Math.floor(width / minimizedColumns)
@ -96,28 +99,12 @@ Item {
sourceModel: root.quickSettingsModel
pageSize: Math.min(root.pageSize, root.minimizedColumns) // HACK: just root.minimizedColumns appears to end up with an empty model?
}
delegate: MobileShell.BaseItem {
delegate: Loader {
required property var modelData
implicitHeight: root.minimizedRowHeight
implicitWidth: root.minimizedColumnWidth
horizontalPadding: (width - Kirigami.Units.gridUnit * 3) / 2
verticalPadding: (height - Kirigami.Units.gridUnit * 3) / 2
asynchronous: true
contentItem: QuickSettingsMinimizedDelegate {
restrictedPermissions: actionDrawer.restrictedPermissions
text: modelData.text
status: modelData.status
icon: modelData.icon
enabled: modelData.enabled
settingsCommand: modelData.settingsCommand
toggleFunction: modelData.toggle
onCloseRequested: {
actionDrawer.close();
}
}
sourceComponent: quickSettingComponentMinimized
}
}
}
@ -140,6 +127,9 @@ Item {
SwipeView {
id: swipeView
// we need to clip this view here to prevent the other quick settings pages from being visible
// when fullViewProgress is not less then 1 and the base view is no longer being clipped
clip: true
Layout.fillWidth: true
Layout.preferredHeight: rowCount * rowHeight
@ -163,7 +153,7 @@ Item {
asynchronous: true
sourceComponent: quickSettingComponent
sourceComponent: quickSettingComponentFull
}
}
}
@ -211,9 +201,36 @@ Item {
}
}
// Quick setting component
// Quick setting component minimized
Component {
id: quickSettingComponent
id: quickSettingComponentMinimized
MobileShell.BaseItem {
implicitHeight: root.minimizedRowHeight
implicitWidth: root.minimizedColumnWidth
horizontalPadding: (width - Kirigami.Units.gridUnit * 3) / 2
verticalPadding: (height - Kirigami.Units.gridUnit * 3) / 2
contentItem: QuickSettingsMinimizedDelegate {
restrictedPermissions: actionDrawer.restrictedPermissions
text: modelData.text
status: modelData.status
icon: modelData.icon
enabled: modelData.enabled
settingsCommand: modelData.settingsCommand
toggleFunction: modelData.toggle
onCloseRequested: {
actionDrawer.close();
}
}
}
}
// Quick setting component full
Component {
id: quickSettingComponentFull
MobileShell.BaseItem {
height: root.rowHeight

View file

@ -34,18 +34,29 @@ MobileShell.BaseItem {
// set by children
property var iconItem
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Button
readonly property color enabledButtonBorderColor: Qt.darker(Kirigami.Theme.highlightColor, 1.25)
readonly property color disabledButtonBorderColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.textColor, Kirigami.Theme.backgroundColor, 0.75)
readonly property color disabledButtonBorderColor: separatorColorHelper(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.2)
readonly property color enabledButtonColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.6)
readonly property color enabledButtonPressedColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.4);
readonly property color disabledButtonColor: Kirigami.Theme.backgroundColor
readonly property color disabledButtonPressedColor: Qt.darker(disabledButtonColor, 1.1)
function separatorColorHelper(bg, fg, baseRatio) {
if (Kirigami.ColorUtils.brightnessForColor(bg) === Kirigami.ColorUtils.Light) {
return Kirigami.ColorUtils.linearInterpolation(bg, fg, baseRatio);
} else {
return Kirigami.ColorUtils.linearInterpolation(bg, fg, baseRatio / 2);
}
}
// scale animation on press
property real zoomScale: 1
Behavior on zoomScale {
NumberAnimation {
duration: 200
duration: Kirigami.Units.longDuration
easing.type: Easing.OutExpo
}
}

View file

@ -39,7 +39,7 @@ MobileShell.BaseItem {
/**
* Height of just the QuickSettings component in minimized mode.
*/
readonly property real minimizedQuickSettingsHeight: quickSettings.minimizedRowHeight + Kirigami.Units.gridUnit
readonly property real minimizedQuickSettingsHeight: quickSettings.minimizedRowHeight + Kirigami.Units.gridUnit - Kirigami.Units.largeSpacing
/**
* Progress of showing the full quick settings view from pinned.
@ -57,10 +57,50 @@ MobileShell.BaseItem {
rightPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing * 4
background: KSvg.FrameSvgItem {
enabledBorders: KSvg.FrameSvgItem.BottomBorder
imagePath: "widgets/background"
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
background: Item {
opacity: brightnessPressedValue
MobileShell.PanelBackground {
id: background
anchors.fill: parent
anchors.bottomMargin: shadow.height
panelType: MobileShell.PanelBackground.PanelType.Flat
radius: 0
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
}
Rectangle {
id: separator
anchors.bottom: background.bottom
anchors.left: parent.left
anchors.right: parent.right
antialiasing: true
// Only show separator on dark background
visible: (Kirigami.ColorUtils.brightnessForColor(background.panelColor)) === Kirigami.ColorUtils.Dark ? 1 : 0
height: 1
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.textColor, Kirigami.Theme.backgroundColor, 0.9)
}
Rectangle {
id: shadow
anchors.top: background.bottom
anchors.left: parent.left
anchors.right: parent.right
height: Kirigami.Units.largeSpacing
opacity: 0.1
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop { position: 0.0; color: 'black' }
GradientStop { position: 1.0; color: 'transparent' }
}
}
}
contentItem: Item {

View file

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

View file

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

View file

@ -4,6 +4,7 @@
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtQuick.Effects
import org.kde.kirigami 2.12 as Kirigami
import org.kde.ksvg 1.0 as KSvg
@ -43,10 +44,17 @@ MobileShell.BaseItem {
rightPadding: Kirigami.Units.smallSpacing * 4
bottomPadding: Kirigami.Units.smallSpacing * 4
background: KSvg.FrameSvgItem {
enabledBorders: KSvg.FrameSvgItem.AllBorders
imagePath: "widgets/background"
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
background: MobileShell.PanelBackground {
anchors.fill: parent
anchors.margins: Kirigami.Units.largeSpacing
panelType: MobileShell.PanelBackground.PanelType.Base
opacity: brightnessPressedValue
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
}
contentItem: Item {
@ -84,7 +92,8 @@ MobileShell.BaseItem {
Layout.maximumWidth: column.width
}
Item { Layout.fillHeight: true }
// add extra space here to prevent the bottom handle from overlapping with the BrightnessItem
Item { Layout.fillHeight: true; height: Kirigami.Units.largeSpacing }
}
Handle {

View file

@ -5,6 +5,7 @@ import QtQuick
import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.private.mobileshell.state as MobileShellState
// NOTE: This is a singleton in the mobileshell library, so we need to be careful to
// make this load as fast as possible (since it may be loaded in other processes ex. lockscreen).
@ -12,8 +13,39 @@ import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
pragma Singleton
QtObject {
readonly property real topPanelHeight: Math.round(Kirigami.Units.gridUnit * ShellSettings.Settings.statusBarScaleFactor / 2) * 2 + Kirigami.Units.smallSpacing
readonly property real navigationPanelThickness: ShellSettings.Settings.navigationPanelEnabled ? Kirigami.Units.gridUnit * 2 : 0
id: root
readonly property var panelSettings: MobileShellState.PanelSettingsDBusClient {
screenName: Screen.name
}
readonly property real defaultTopPanelHeight: Math.round(Kirigami.Units.gridUnit * ShellSettings.Settings.statusBarScaleFactor / 2) * 2 + Kirigami.Units.smallSpacing
readonly property real topPanelHeight: {
if (root.panelSettings.statusBarHeight <= 0) {
return defaultTopPanelHeight;
}
return root.panelSettings.statusBarHeight;
}
readonly property real defaultNavigationPanelThickness: Kirigami.Units.gridUnit * 2
readonly property real defaultGesturePanelThickness: Kirigami.Units.gridUnit
readonly property real navigationPanelThickness: {
if (!ShellSettings.Settings.navigationPanelEnabled) {
return ShellSettings.Settings.gesturePanelEnabled ? defaultGesturePanelThickness : 0;
}
if (root.panelSettings.navigationPanelHeight <= 0) {
return defaultNavigationPanelThickness;
}
return root.panelSettings.navigationPanelHeight;
}
readonly property real screenEdgeTouchTarget: (ShellSettings.Settings.gesturePanelEnabled && !ShellSettings.Settings.navigationPanelEnabled) ? defaultGesturePanelThickness : 8
onScreenEdgeTouchTargetChanged: {
if (ShellSettings.KWinSettings.screenEdgeTouchTarget != screenEdgeTouchTarget) {
ShellSettings.KWinSettings.screenEdgeTouchTarget = screenEdgeTouchTarget;
}
}
function navigationPanelOnSide(screenWidth: real, screenHeight: real): bool {
// TODO: we have this disabled for now, we might consider just removing this feature entirely due to it causing several issues:

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: 2025 Micah Stanley <stanleymicah@proton.me>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.components 3.0 as PlasmaComponents
import Qt5Compat.GraphicalEffects
/**
* This is a simple marquee (flowing) label based on PlasmaComponents Label.
*/
PlasmaComponents.Label {
OpacityMask {
id: root
height: row.height
// label values
required property string inputText
property font font
property var textFormat: Text.RichText
// properties for the marquee label scroll speed and wait duration
readonly property real scrollSpeed: 0.025
readonly property int waitDuration: 2000
readonly property string filteredText: inputText.replace(/\n/g, ' ') // remove new line characters
readonly property bool charactersOverflowing: txtMeter.advanceWidth > root.width // true when text is overflowing
readonly property int interval: 200 // update position every 200 ms
readonly property int longDuration: 300
readonly property int waitDuration: 900
// update animation values and text positions whenever the label overflows or changes
onFilteredTextChanged: if (root.charactersOverflowing) { textAnimationLoop.restart() }
onCharactersOverflowingChanged: if (charactersOverflowing) { row.scrollPosition = 0 }
readonly property int charactersOverflow: Math.ceil((txtMeter.advanceWidth - root.width) / (txtMeter.advanceWidth / filteredText.length))
property int step: 0
Item {
id: rowContaner
anchors.fill: parent
height: row.height
opacity: 0 // we display with the opacity gradient below
TextMetrics {
id: txtMeter
font: root.font
text: filteredText
}
// use two identical labels for scrolling so we can give the illusion of infinite scrolling
RowLayout {
id: row
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
Timer {
property bool paused: false
property real scrollPosition: 0
interval: root.interval
running: visible && charactersOverflow > 0
repeat: true
onTriggered: {
if (paused) {
if (step != 0) {
interval = root.longDuration;
step = 0;
} else {
interval = root.interval;
paused = false;
transform: [
Translate {
x: row.scrollPosition
}
} else {
step = (step + 1) % filteredText.length;
if (step === charactersOverflow) {
interval = root.waitDuration;
paused = true;
]
spacing: 32
PlasmaComponents.Label {
id: label
font: root.font
textFormat: root.textFormat
text: filteredText
Layout.alignment: Qt.AlignLeft
TextMetrics {
id: txtMeter
font: root.font
text: filteredText
}
}
}
onRunningChanged: {
if (!running) {
step = 0;
PlasmaComponents.Label {
// hide this label when the text is not overflowing so the user never sees both labels
visible: textAnimationLoop.running
font: root.font
textFormat: root.textFormat
text: filteredText
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
}
}
}
text: filteredText.substring(step, step + filteredText.length - charactersOverflow)
// setting the gradient mask source
source: rowContaner
// if the label is overflowing, this animation in a loop smoothly scrolling thought the text
SequentialAnimation {
id: textAnimationLoop
running: root.charactersOverflowing && root.visible
onRunningChanged: row.scrollPosition = 0
loops: Animation.Infinite
PauseAnimation { duration: root.waitDuration }
NumberAnimation { target: row; property: "scrollPosition"; from: 0; to: -txtMeter.advanceWidth - row.spacing; duration: (txtMeter.advanceWidth + row.spacing) / root.scrollSpeed }
}
// gradient mask to smoothly fade the ends of the label when it is scrolling
maskSource: Rectangle {
id: mask
width: root.width
height: root.height
property real gradientPct: (Kirigami.Units.gridUnit * 0.35) / root.width
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0; color: row.scrollPosition == 0 || row.scrollPosition < -txtMeter.advanceWidth ? 'white' : 'transparent' } // remove the beginning of the gradient when at the start of the label so the front text is fully visible
GradientStop { position: 0 + mask.gradientPct; color: 'white' }
GradientStop { position: 1.0 - mask.gradientPct; color: 'white' }
GradientStop { position: 1.0; color: 'transparent' }
}
}
}

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

View file

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

View file

@ -6,6 +6,7 @@ pragma Singleton
import QtQuick
import org.kde.plasma.private.volume
import org.kde.plasma.private.mobileshell as MobileShell
QtObject {
id: root
@ -31,57 +32,13 @@ QtObject {
// the maximum volume amount
readonly property int maxVolumeValue: maxVolumePercent * PulseAudio.NormalVolume / 100
// step that increments when adjusting the volume
readonly property int volumeStep: Math.round(5 * PulseAudio.NormalVolume / 100.0)
// The current audio volume (updated by connecting to sinks)
property int volumeValue
readonly property int volumeValue: PreferredDevice.sink ? (PreferredDevice.sink.volume / PulseAudio.NormalVolume) * 100 : 0
function isDummyOutput(output) {
return output && output.name === dummyOutputName;
}
function boundVolume(volume) {
return Math.max(PulseAudio.MinimalVolume, Math.min(volume, maxVolumeValue));
}
function volumePercent(volume, max){
if (!max) {
max = PulseAudio.NormalVolume;
}
return Math.round(volume / max * maxVolumePercent);
}
function increaseVolume() {
if (!PreferredDevice.sink || isDummyOutput(PreferredDevice.sink)) {
return;
}
var volume = boundVolume(PreferredDevice.sink.volume + volumeStep);
var percent = volumePercent(volume, maxVolumeValue);
PreferredDevice.sink.muted = percent == 0;
PreferredDevice.sink.volume = volume;
}
function decreaseVolume() {
if (!PreferredDevice.sink || isDummyOutput(PreferredDevice.sink)) {
return;
}
var volume = boundVolume(PreferredDevice.sink.volume - volumeStep);
var percent = volumePercent(volume, maxVolumeValue);
PreferredDevice.sink.muted = percent == 0;
PreferredDevice.sink.volume = volume;
}
function muteVolume() {
if (!PreferredDevice.sink || isDummyOutput(PreferredDevice.sink)) {
return;
}
PreferredDevice.sink.muted = !PreferredDevice.sink.muted;
}
function iconName(volume, muted, prefix) {
if (!prefix) {
prefix = "audio-volume";
@ -99,33 +56,4 @@ QtObject {
}
return icon;
}
// emitted when the volume changed, but not due to sink switching
signal volumeChanged()
property var updateVolume: Connections {
target: root.paSinkModel ? (PreferredDevice.sink ? PreferredDevice.sink : null) : null
enabled: target !== null
function onVolumeChanged() {
root.volumeValue = root.volumePercent(PreferredDevice.sink.volume, root.maxVolumeValue);
root.volumeChanged();
}
function onMutedChanged() {
root.volumeValue = PreferredDevice.sink.muted ? 0 : root.volumePercent(PreferredDevice.sink.volume, root.maxVolumeValue);
root.volumeChanged();
}
}
property var updateVolumeOnSinkChange: Connections {
target: root.paSinkModel ? root.paSinkModel : null
enabled: target !== null
function onPreferredSinkChanged() {
if (PreferredDevice.sink) {
root.volumeValue = root.volumePercent(PreferredDevice.sink.volume, root.maxVolumeValue);
}
}
}
}

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

View file

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

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

View file

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

View file

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

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
*/
@ -16,7 +16,7 @@ import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.layershell 1.0 as LayerShell
import org.kde.notificationmanager as NotificationManager
import org.kde.plasma.plasma5support 2.0 as P5Support
import org.kde.plasma.clock
Item {
id: notificationPopup
@ -29,10 +29,13 @@ Item {
property real openOffset
// calculate the position needed to at when the expanded drawer is active
readonly property real fullOpenOffset: popupDrawerOpened ? aboveNotificationFullOffset + aboveNotificationHeight + Kirigami.Units.gridUnit : 0
readonly property real fullOpenOffset: popupDrawerOpened ? aboveNotificationFullOffset + aboveNotificationHeight + Kirigami.Units.largeSpacing : 0
property real aboveNotificationFullOffset: 0
property int aboveNotificationHeight: 0
// set to true when notification is swiped up by user
property bool closedWithSwipe: false
// the drag offset on the current popup notification - used to position notification when stacked underneath
property real currentDragOffset: {
let current = popupNotifications.currentPopupIndex == notificationPopup.popupIndex;
@ -47,7 +50,6 @@ Item {
onTriggered: {
visible = true;
updateNotificationPopups();
checkActionDrawerOpened();
}
}
@ -70,7 +72,7 @@ Item {
}
return true;
}
onTriggered: notificationPopup.closePopup()
onTriggered: notificationPopup.closePopup(popupIndex);
}
// the value of how much time is left, normalized from 1 to 0
@ -131,7 +133,7 @@ Item {
easing.type: Easing.OutExpo
}
// if the popup height ever changes, update the notification below wiht new height
// if the popup height ever changes, update the notification below with new height
// also update the allowed touch area for the main window
onPopupHeightChanged: {
let abovePopup = popupNotifications.objectAt(popupIndex + 1)
@ -143,14 +145,14 @@ Item {
}
}
// if the offset position need in the expanded drawer changes, update the notification below wiht new offset
// if the offset position need in the expanded drawer changes, update the notification below with new offset
onFullOpenOffsetChanged: {
let abovePopup = popupNotifications.objectAt(popupIndex + 1)
if (popupIndex + 1 < popupCount && abovePopup) {
abovePopup.aboveNotificationFullOffset = fullOpenOffset;
}
}
// if the notification is being draged and is the current one
// if the notification is being dragged and is the current one
// update 'currentDragOffset' so all notifications can easily access this value
onDragOffsetChanged: {
let abovePopup = popupNotifications.objectAt(popupIndex + 1)
@ -161,7 +163,7 @@ Item {
// if a new notification is added, update the above notification values need for the expanded drawer
onPopupCountChanged: {
let abovePopup = popupNotifications.objectAt(popupIndex + 1)
if (popupIndex + 1 < abovePopup) {
if (popupIndex + 1 < popupCount && abovePopup) {
abovePopup.aboveNotificationHeight = popupHeight;
abovePopup.aboveNotificationFullOffset = fullOpenOffset;
}
@ -169,12 +171,14 @@ Item {
// update the current popup index value if the index ever changes.
onPopupIndexChanged: {
if (!isClosing && !inPopupDrawer && !waiting) {
// if index goes below zero, assume it is being closed externally and move over to the next popup
if (popupIndex < 0 ) {
closePopup(0);
return;
}
popupNotifications.currentPopupIndex = popupIndex;
}
}
// if the action drawer opens, it is best to dismiss all popup notifications
onIsActionDrawerOpenChanged: checkActionDrawerOpened()
property bool isActionDrawerOpen: MobileShellState.ShellDBusClient.isActionDrawerOpen
property bool waiting: true
@ -206,16 +210,6 @@ Item {
return model.timeout;
}
// check if the action drawer is opened and the popup is fully created
// if so, close the popup with a scale effect
function checkActionDrawerOpened() {
if (isActionDrawerOpen && popupNotifications.objectAt(popupIndex)) {
notificationPopup.expired();
keyboardInteractivity = LayerShell.Window.KeyboardInteractivityNone;
notificationItem.state = "closeWithScale";
}
}
// show the top most notification in the list and move the rest to the popup drawer
function updateNotificationPopups() {
if (popupCount != 1) {
@ -229,7 +223,7 @@ Item {
function showNotificationPopup() {
if (isClosing) {
closePopup();
closePopup(popupIndex);
return;
}
if (notificationItem.state != "open") {
@ -252,7 +246,7 @@ Item {
if (notificationPopup.popupDrawerOpened && notificationItem.state != "inDrawerClosed" && notificationItem.state != "open") {
notificationItem.offset = openOffset;
notificationItem.scale = 0.75;
notificationItem.opacity = 0.0;
notificationItem.popupOpacity = 0.0;
}
notificationItem.state = "inDrawerClosed";
notificationPopup.removeKeyboardFocus();
@ -263,7 +257,7 @@ Item {
if (notificationPopup.popupDrawerOpened && notificationItem.state != "open" && notificationItem.state != "inDrawerClosed") {
notificationItem.offset = openOffset;
notificationItem.scale = 0.75;
notificationItem.opacity = 0.0;
notificationItem.popupOpacity = 0.0;
}
notificationItem.state = "open";
notificationPopup.removeKeyboardFocus();
@ -271,19 +265,19 @@ Item {
// if the notification ever expires, close it and move on to the next one in the list.
property bool isExpired: model.expired
onIsExpiredChanged: closePopup()
onIsExpiredChanged: closePopup(popupIndex)
// this closes the popup notification with the relvent animation while updating the popup below to show, if any exist
function closePopup() {
function closePopup(index: int) {
notificationPopup.removeKeyboardFocus();
notificationPopup.setInputTransparent();
if (popupIndex + 1 < popupCount) {
popupNotifications.objectAt(popupIndex + 1).aboveNotificationHeight = 0;
popupNotifications.objectAt(popupIndex + 1).aboveNotificationFullOffset = 0;
if (index + 1 < popupCount) {
popupNotifications.objectAt(index + 1).aboveNotificationHeight = 0;
popupNotifications.objectAt(index + 1).aboveNotificationFullOffset = 0;
}
if (popupCount > 1) {
let nextNotificationIdx = popupIndex + (popupIndex < popupCount - 1 ? 1 : -1);
let nextNotificationIdx = index + (index < popupCount - 1 ? 1 : -1);
let nextNotification = popupNotifications.objectAt(nextNotificationIdx);
if (nextNotification != null) {
@ -329,7 +323,10 @@ Item {
modelIndex: notificationPopup.popupIndex
notificationsModel: notificationPopup.notificationsModel
notificationsModelType: notificationPopup.notificationsModelType
timeSource: notificationPopup.timeDataSource
clockSource: notificationPopup.timeDataSource
panelType: notificationPopup.popupDrawerOpened ?
MobileShell.PanelBackground.PanelType.Drawer :
MobileShell.PanelBackground.PanelType.Popup
currentPopupHeight: popupNotifications.currentPopupHeight
@ -344,33 +341,57 @@ Item {
preventDismissTimeout = true;
}
onDismissRequested: closePopup()
onDismissRequested: closePopup(popupIndex)
property real offset: closedOffset
property real scale: 1.0
property real drawerScale: 1 - Math.max(notificationPopup.popupIndex - popupNotifications.currentPopupIndex, 1) * 0.075
property real popupOpacity: 1.0 // controls the opacity of the notification popup when outside the popup drawer
property real drawerScale: {
if (notificationPopup.popupDrawerOpened) {
return 0; // when popup drawer is opened, reset scale to 0
}
let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex;
// clamp the index value to avoid scaling too much with animations
let indexClamped = Math.max(Math.min(index, 2), 0);
return indexClamped * 0.075;
}
property real drawerAddedOffset: {
if (notificationPopup.popupDrawerOpened) {
return 0; // when popup drawer is opened, reset any added height to 0
}
let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex;
// clamp the index value to avoid moving too much with animations
let indexClamped = Math.max(Math.min(index, 2), -1);
return Kirigami.Units.gridUnit * 0.5 * indexClamped;
}
property real drawerOpacity: {
let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex;
if (index > 2 && !notificationPopup.popupDrawerOpened) {
return 0; // make this popup invisible if it is below 3 other popups
} else {
return 1; // when popup drawer is opened, reset opacity to 1
}
}
Behavior on drawerScale {
NumberAnimation {
duration: Kirigami.Units.veryLongDuration
easing.type: Easing.OutExpo
duration: Kirigami.Units.veryLongDuration * 1.25
easing.type: Easing.OutQuint
}
}
property real drawerAddedOffset: Kirigami.Units.gridUnit * 0.5 * Math.max(notificationPopup.popupIndex - popupNotifications.currentPopupIndex, 1)
Behavior on drawerAddedOffset {
NumberAnimation {
duration: Kirigami.Units.veryLongDuration
easing.type: Easing.OutExpo
duration: Kirigami.Units.veryLongDuration * 1.25
easing.type: Easing.OutQuint
}
}
property real drawerOpacity: (Math.max(notificationPopup.popupIndex - popupNotifications.currentPopupIndex, 1) > 2) ? 0 : 1
Behavior on drawerOpacity {
NumberAnimation {
duration: Kirigami.Units.veryLongDuration
easing.type: Easing.OutExpo
duration: Kirigami.Units.veryLongDuration * 1.25
easing.type: Easing.OutQuint
}
}
opacity: 1.0
opacity: Math.min(popupOpacity, drawerOpacity)
state: ""
@ -384,7 +405,7 @@ Item {
target: notificationItem; scale: 1.0
}
PropertyChanges {
target: notificationItem; opacity: 1.0
target: notificationItem; popupOpacity: 1.0
}
},
State {
@ -396,7 +417,7 @@ Item {
target: notificationItem; scale: 1.0
}
PropertyChanges {
target: notificationItem; opacity: 1.0
target: notificationItem; popupOpacity: 1.0
}
},
State {
@ -408,47 +429,88 @@ Item {
target: notificationItem; scale: 0.75
}
PropertyChanges {
target: notificationItem; opacity: 0.0
target: notificationItem; popupOpacity: 0.0
}
},
State {
name: "inDrawerClosed"
PropertyChanges {
target: notificationItem; offset: notificationPopup.openOffset + (notificationPopup.popupDrawerOpened ? 0 : drawerAddedOffset)
target: notificationItem; offset: notificationPopup.openOffset
}
PropertyChanges {
target: notificationItem; scale: notificationPopup.popupDrawerOpened ? 1 : drawerScale
target: notificationItem; scale: 1
}
PropertyChanges {
target: notificationItem; opacity: notificationPopup.popupDrawerOpened ? 1 : drawerOpacity
target: notificationItem; popupOpacity: 1
}
}
]
readonly property int notificationEasing: {
// check whether the popup is the current one or above it
let topPopup = popupNotifications.currentPopupIndex >= notificationPopup.popupIndex;
// check whether the popup has any popups below it
let popupBelow = notificationPopup.popupCount - notificationPopup.popupIndex > 1;
let popupOpening = notificationItem.state == "open" || notificationItem.state == "inDrawerClosed";
let popupClosing = notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale"
if (notificationPopup.closedWithSwipe || (topPopup && popupClosing && popupBelow)) {
// set the easing type to linear when closed with a swipe or if a popup is below when closing
// as to make sure the popup feels like it is keeping it's momentum
return Easing.Linear;
} else if (popupOpening) {
// set the easing type to 'Out' when opening so the popup will have a gentle landing
return Easing.OutQuint;
} else {
// if above conditions fail, set the easing type to 'In' so the popup will build up speed for it's exit
return Easing.InQuint;
}
}
readonly property real notificationDuration: {
// check whether the popup is the current one or above it
let topPopup = popupNotifications.currentPopupIndex >= notificationPopup.popupIndex;
// check whether the popup has any popups below it
let popupBelow = notificationPopup.popupCount - notificationPopup.popupIndex > 1;
let popupClosing = notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale"
if (notificationPopup.closedWithSwipe || (topPopup && popupClosing && popupBelow)) {
// make sure the speed it faster when closed with a swipe or if there is a popup below when closing
// as to make sure the speed feels comparable with the easing type is set to linear
return Kirigami.Units.veryLongDuration * 0.5;
} else {
return Kirigami.Units.veryLongDuration * 1.25;
}
}
transitions: Transition {
SequentialAnimation {
ParallelAnimation {
PropertyAnimation {
properties: "offset"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.5
properties: "offset"
easing.type: notificationItem.notificationEasing
duration: notificationItem.notificationDuration
}
PropertyAnimation {
properties: "scale"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.5
properties: "scale"
easing.type: notificationItem.notificationEasing
duration: notificationItem.notificationDuration
}
PropertyAnimation {
properties: "opacity"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.5
properties: "popupOpacity"
easing.type: notificationItem.notificationEasing
duration: notificationItem.notificationDuration
}
}
ScriptAction {
script: {
if (notificationItem.state == "open") {
preventDismissTimeout = false;
notificationPopup.preventDismissTimeout = false;
notificationPopup.updateTouchArea();
} else if (notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale") {
preventDismissTimeout = true;
if (dismissTimeout) {
notificationPopup.preventDismissTimeout = true;
if (notificationPopup.dismissTimeout) {
notificationPopup.dismissClicked();
} else if (!isActionDrawerOpen) {
notificationPopup.expired();
} else {
notificationPopup.expired();
}
}
}
@ -460,15 +522,15 @@ Item {
Scale {
origin.x: Math.round(notificationPopup.popupWidth / 2)
origin.y: notificationPopup.scaleOriginY
xScale: notificationItem.scale
yScale: notificationItem.scale
xScale: notificationItem.scale - notificationItem.drawerScale
yScale: notificationItem.scale - notificationItem.drawerScale
}
]
}
transform: [
Translate {
y: notificationItem.offset + notificationPopup.fullOffsetAn + notificationPopup.dragOffset + notificationPopup.currentDragOffset
y: notificationItem.offset + notificationPopup.fullOffsetAn + notificationPopup.dragOffset + notificationPopup.currentDragOffset + notificationItem.drawerAddedOffset
}
]
@ -501,13 +563,15 @@ Item {
startActive = active;
notificationPopup.preventDismissTimeout = true;
if (!active && !(notificationItem.state == "closeWithScale" || notificationItem.state == "closeWithMove")) {
dragOffsetAn.running = true;
if ((lastOffset - notificationPopup.dragOffset > 1.0 && notificationPopup.dragOffset < 0) || (-(notificationPopup.openOffset - notificationPopup.closedOffset) / 4 > notificationPopup.dragOffset)) {
// this code is called when the notifition is swiped or draged to the top.
notificationPopup.closePopup();
// this code is called when the notification is swiped or dragged to the top.
notificationPopup.closedWithSwipe = true;
notificationPopup.closePopup(popupIndex);
return;
} else if (notificationPopup.dragOffset - lastOffset > 1.0 || Kirigami.Units.gridUnit * 3 < notificationPopup.dragOffset) {
// this code is called when the notifition is swiped or draged down.
}
dragOffsetAn.running = true;
if (notificationPopup.dragOffset - lastOffset > 1.0 || Kirigami.Units.gridUnit * 3 < notificationPopup.dragOffset) {
// this code is called when the notification is swiped or dragged down.
}
notificationPopup.preventDismissTimeout = (keyboardInteractivity == LayerShell.Window.KeyboardInteractivityOnDemand);
} else {

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.notificationmanager as NotificationManager
import org.kde.plasma.plasma5support 2.0 as P5Support
import org.kde.plasma.clock
import QtQuick.Controls as Controls
import org.kde.plasma.components 3.0 as PlasmaComponents
@ -46,10 +46,10 @@ Window {
property QtObject notificationSettings
property QtObject popupNotificationsModel
property QtObject tasksModel
property QtObject timeSource
property Clock clockSource
property bool inhibited
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false
readonly property color backgroundColor: Qt.darker(Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.95), 1.05)
@ -68,13 +68,27 @@ Window {
Component.onCompleted: ShellUtil.setInputTransparent(notificationPopupManager, true)
Binding {
target: MobileShellState.ShellDBusClient
property: "isNotificationPopupDrawerOpen"
value: popupDrawerOpened
}
// hide on timeout to give time to finish animations
Timer {
id: hideTimeout
interval: Kirigami.Units.veryLongDuration * 1.5
repeat: false
onTriggered: if (notifications.count == 0) notificationPopupManager.visible = false;
}
// Update the window touch region to encapsulate the notification area or the whole screen depending on the 'popupDrawerOpened' state
function updateTouchArea() {
ShellUtil.setInputTransparent(notificationPopupManager, false);
if (popupDrawerOpened) {
ShellUtil.setInputRegion(notificationPopupManager, Qt.rect(0, 0, 0, 0));
} else {
// get the height of the popup directly to ensure we get the lastest version
// get the height of the popup directly to ensure we get the latest version
let popupHeight = Kirigami.Units.gridUnit * 6;
let currentPopup = notifications.objectAt(notifications.currentPopupIndex);
if (currentPopup) {
@ -160,7 +174,7 @@ Window {
onCountChanged: {
if (count == 0) {
ShellUtil.setInputTransparent(notificationPopupManager, true);
notificationPopupManager.visible = false;
hideTimeout.restart();
notificationPopupManager.popupDrawerOpened = false;
fullHeight = 0;
return;
@ -195,7 +209,7 @@ Window {
popupModel: model
notificationsModel: popupNotificationsModel
notificationsModelType: notificationModelType
timeDataSource: timeSource
timeDataSource: clockSource
timeout: model.timeout

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.notificationmanager as NotificationManager
import org.kde.plasma.plasma5support 2.0 as P5Support
import org.kde.plasma.clock
import org.kde.taskmanager 0.1 as TaskManager
@ -62,10 +62,15 @@ QtObject {
}
}
// TODO use pulseaudio-qt for this once it becomes a framework
property QtObject __pulseAudio: Loader {
source: "PulseAudio.qml"
}
property bool inhibited: false
onInhibitedChanged: {
var pa = pulseAudio.item;
var pa = __pulseAudio.item;
if (!pa) {
return;
}
@ -121,13 +126,9 @@ QtObject {
groupInline: false
}
property QtObject timeSource: P5Support.DataSource {
engine: "time"
connectedSources: ["Local"]
interval: 60000 // 1 min
intervalAlignment: P5Support.Types.AlignToMinute
onDataChanged: {
property Clock clockSource: Clock {
id: clockSource
onTimeChanged: {
checkInhibition();
npm.timeChanged();
}
@ -137,7 +138,7 @@ QtObject {
notificationModelType: notificationProvider.notificationModelType
notificationSettings: notificationProvider.notificationSettings
popupNotificationsModel: notificationProvider.popupNotificationsModel
timeSource: notificationProvider.timeSource
clockSource: notificationProvider.clockSource
inhibited: notificationProvider.inhibited
tasksModel: notificationProvider.tasksModel
}

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

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
ListItemBase {
readonly property var currentPort: Ports[ActivePortIndex]
readonly property var currentActivePortIndex: ActivePortIndex
readonly property var currentMuted: Muted
readonly property var activePortIndex: ActivePortIndex
property bool onlyone: false
readonly property var currentPort: model.Ports[model.ActivePortIndex]
readonly property var currentActivePortIndex: model.ActivePortIndex
readonly property var currentMuted: model.Muted
readonly property var activePortIndex: model.ActivePortIndex
label: {
if (currentPort && currentPort.description) {
if (onlyone || !Description) {
if (onlyOne || !model.Description) {
return currentPort.description;
} else {
return i18nc("label of device items", "%1 (%2)", currentPort.description, Description);
return i18nc("label of device items", "%1 (%2)", currentPort.description, model.Description);
}
}
if (Description) {
return Description;
if (model.Description) {
return model.Description;
}
if (Name) {
return Name;
if (model.Name) {
return model.Name;
}
return i18n("Device name not found");
}

View file

@ -6,52 +6,55 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.1
import QtQuick.Window 2.2
import QtQuick
import QtQuick.Controls as Controls
import QtQuick.Layouts
import org.kde.kirigami 2.20 as Kirigami
import org.kde.ksvg 1.0 as KSvg
import org.kde.kquickcontrolsaddons 2.0
import org.kde.kirigami as Kirigami
import org.kde.ksvg as KSvg
import org.kde.kquickcontrolsaddons
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.volume 0.1
import org.kde.plasma.components as PlasmaComponents
import org.kde.plasma.extras as PlasmaExtras
import org.kde.plasma.private.volume
import "icon.js" as Icon
// adapted from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/ListItemBase.qml
Controls.Control {
Controls.AbstractButton {
id: baseItem
property string label
property alias listIcon: clientIcon.source
property string type // sink, source, source-output
MouseArea {
id: clickArea
anchors.fill: parent
z: -1
onClicked: {
if (selectButton.visible) {
model.PulseObject.default = true;
}
}
property bool onlyOne: false
// Whether this item is selected
readonly property bool supportsSelection: (baseItem.type == "sink" || baseItem.type == "source")
readonly property bool selected: supportsSelection && (model.PulseObject.hasOwnProperty("default") ? model.PulseObject.default : false)
onClicked: {
// Set as the default audio device
model.PulseObject.default = true
}
topPadding: Kirigami.Units.mediumSpacing
bottomPadding: Kirigami.Units.mediumSpacing
leftPadding: Kirigami.Units.mediumSpacing
rightPadding: Kirigami.Units.mediumSpacing
background: Rectangle {
radius: Kirigami.Units.cornerRadius
color: (baseItem.down)
? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.3)
: 'transparent'
}
contentItem: RowLayout {
id: row
spacing: Kirigami.Units.smallSpacing
PlasmaComponents.RadioButton {
id: selectButton
Layout.alignment: Qt.AlignTop
Layout.topMargin: Math.round(row.height / 2 - implicitHeight - Kirigami.Units.smallSpacing / 2) // align with text
checked: model.PulseObject.hasOwnProperty("default") ? model.PulseObject.default : false
visible: (baseItem.type == "sink" && sinkView.model.count > 1) || (baseItem.type == "source" && sourceView.model.count > 1)
onClicked: model.PulseObject.default = true
}
// application icon
Kirigami.Icon {
id: clientIcon
@ -68,6 +71,24 @@ Controls.Control {
}
}
RowLayout {
spacing: 0
Layout.maximumWidth: Infinity // Ignore maximum width of children
visible: (baseItem.type === "sink" || baseItem.type === "source") && !baseItem.onlyOne
PlasmaComponents.RadioButton {
id: defaultButton
Accessible.ignored: true // read out from delegate
activeFocusOnTab: false // toggle from delegate
checked: model.PulseObject?.default ?? false
onToggled: {
if (checked) {
baseItem.click();
}
}
}
}
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
@ -87,9 +108,10 @@ Controls.Control {
}
PlasmaComponents.ToolButton {
id: viewButton
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: -Kirigami.Units.smallSpacing
icon.name: "application-menu"
icon.name: "view-more-symbolic"
checkable: true
checked: contextMenu.visible && contextMenu.visualParent === this
visible: contextMenu.hasContent
@ -132,93 +154,26 @@ Controls.Control {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
PlasmaComponents.ToolButton {
icon.name: Icon.name(Volume / PulseAudio.NormalVolume * 100.0, Muted)
text: Muted ? i18n("Unmute") : i18n("Mute")
display: Controls.AbstractButton.IconOnly
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
onClicked: {
Muted = !Muted
}
}
// this slider was effectively copied from the source (linked at the top of the file)
PlasmaComponents.Slider {
VolumeSlider {
id: slider
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
// Helper properties to allow async slider updates.
// While we are sliding we must not react to value updates
// as otherwise we can easily end up in a loop where value
// changes trigger volume changes trigger value changes.
property int volume: Volume
property bool ignoreValueChange: true
readonly property bool forceRaiseMaxVolume: volume >= PulseAudio.NormalVolume * 1.01
from: PulseAudio.MinimalVolume
to: PulseAudio.NormalVolume
stepSize: to / (to / PulseAudio.NormalVolume * 100.0)
visible: HasVolume
enabled: VolumeWritable
opacity: Muted ? 0.5 : 1
to: model.Volume >= PulseAudio.NormalVolume * 1.01 ? PulseAudio.MaximalVolume : PulseAudio.NormalVolume
stepSize: PulseAudio.NormalVolume / 100.0
property real myStepSize: PulseAudio.NormalVolume / 100.0
visible: model.HasVolume !== false // Devices always have volume but Streams don't necessarily
enabled: model.VolumeWritable
muted: model.Muted
volumeObject: model.PulseObject
activeFocusOnTab: false // access from delegate
Accessible.name: i18nc("Accessibility data on volume slider", "Adjust volume for %1", baseItem.label)
background: KSvg.FrameSvgItem {
imagePath: "widgets/slider"
prefix: "groove"
width: parent.availableWidth
height: margins.top + margins.bottom
anchors.centerIn: parent
scale: parent.mirrored ? -1 : 1
KSvg.FrameSvgItem {
imagePath: "widgets/slider"
prefix: "groove-highlight"
anchors.left: parent.left
y: (parent.height - height) / 2
width: Math.max(margins.left + margins.right, slider.handle.x * meter.volume)
height: Math.max(margins.top + margins.bottom, parent.height)
opacity: meter.available && (meter.volume > 0 || animation.running)
VolumeMonitor {
id: meter
target: parent.visible ? model.PulseObject : null
}
Behavior on width {
NumberAnimation {
id: animation
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutQuad
}
}
}
value: to, model.Volume
onMoved: {
model.Volume = value;
model.Muted = value === 0;
}
Component.onCompleted: {
ignoreValueChange = false;
}
onVolumeChanged: {
var oldIgnoreValueChange = ignoreValueChange;
ignoreValueChange = true;
value = Volume;
ignoreValueChange = oldIgnoreValueChange;
}
onValueChanged: {
if (!ignoreValueChange) {
Volume = value;
Muted = value == 0;
if (!pressed) {
updateTimer.restart();
}
}
}
onPressedChanged: {
if (!pressed) {
// Make sure to sync the volume once the button was
@ -226,14 +181,28 @@ Controls.Control {
// Otherwise it might be that the slider is at v10
// whereas PA rejected the volume change and is
// still at v15 (e.g.).
updateTimer.restart();
value = Qt.binding(() => model.Volume);
}
}
Timer {
id: updateTimer
interval: 200
onTriggered: slider.value = Volume
function updateVolume() {
if (model.Volume > PulseAudio.NormalVolume) {
model.Volume = PulseAudio.NormalVolume;
}
}
SequentialAnimation {
id: seqAnimation
NumberAnimation {
id: toAnimation
target: slider
property: "to"
duration: Kirigami.Units.shortDuration
easing.type: Easing.InOutQuad
}
ScriptAction {
script: slider.updateVolume()
}
}
}
PlasmaComponents.Label {
@ -263,8 +232,4 @@ Controls.Control {
}
}
}
function setVolumeByPercent(targetPercent) {
model.PulseObject.volume = Math.round(PulseAudio.NormalVolume * (targetPercent/100));
}
}

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

View file

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

View file

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

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.Layouts 1.15
import org.kde.plasma.plasma5support 2.0 as P5Support
import org.kde.plasma.clock
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
@ -19,14 +19,14 @@ RowLayout {
id: clockText
required property int fontPixelSize
required property P5Support.DataSource source
required property Clock clockSource
PlasmaComponents.Label {
id: clock
property bool is24HourTime: MobileShell.ShellUtil.isSystem24HourFormat
text: Qt.formatTime(source.data.Local.DateTime, is24HourTime ? "h:mm" : "h:mm ap")
text: Qt.formatTime(clockSource.dateTime, is24HourTime ? "h:mm" : "h:mm ap")
color: Kirigami.Theme.textColor
verticalAlignment: Qt.AlignVCenter
font.pixelSize: fontPixelSize
@ -36,7 +36,7 @@ RowLayout {
id: date
visible: ShellSettings.Settings.dateInStatusBar && !root.showSecondRow
text: Qt.formatDate(source.data.Local.DateTime, "ddd. MMMM d")
text: Qt.formatDate(clockSource.dateTime, "ddd. MMMM d")
color: Kirigami.Theme.textColor
verticalAlignment: Qt.AlignVCenter
font.pixelSize: fontPixelSize

View file

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

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

View file

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

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

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 {
id: img
source: root.imageSource
@ -41,9 +48,12 @@ Item {
// ensure text is readable
Rectangle {
anchors.fill: parent
color: Qt.rgba(Kirigami.Theme.backgroundColor.r * (inActionDrawer ? 0.85 : 0.95), Kirigami.Theme.backgroundColor.g * (inActionDrawer ? 0.85 : 0.95), Kirigami.Theme.backgroundColor.b * (inActionDrawer ? 0.85 : 0.95), root.darken ? 0.95 : 0.85)
color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.2)
visible: img.progress
}
opacity: 0.1
// apply lighten, saturate and blur effect
layer.enabled: true
layer.effect: MultiEffect {

View file

@ -21,8 +21,7 @@ Item {
id: root
visible: sourceRepeater.count > 0
property bool inActionDrawer: false
property int panelType: MobileShell.PanelBackground.PanelType.Drawer
property bool detailledView: false
readonly property real padding: Kirigami.Units.gridUnit
@ -34,8 +33,8 @@ Item {
Behavior on implicitHeight {
NumberAnimation {
duration: implicitHeight == 0 ? 0 : Kirigami.Units.shortDuration
easing.type: Easing.InOutQuad
duration: implicitHeight == 0 ? 0 : Kirigami.Units.longDuration
easing.type: Easing.OutQuart
}
}
@ -63,30 +62,9 @@ Item {
}
}
// shadow
MultiEffect {
anchors.fill: root
visible: !inActionDrawer
source: simpleShadow
blurMax: 32
shadowEnabled: true
shadowVerticalOffset: 1
shadowOpacity: 0.5
shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.2)
}
Rectangle {
id: simpleShadow
anchors.fill: root
anchors.leftMargin: -1
anchors.rightMargin: -1
anchors.bottomMargin: -1
color: {
let darkerBackgroundColor = Qt.darker(Kirigami.Theme.backgroundColor, 1.3);
return Qt.rgba(darkerBackgroundColor.r, darkerBackgroundColor.g, darkerBackgroundColor.b, 0.3)
}
radius: Kirigami.Units.cornerRadius
MobileShell.PanelBackground {
anchors.fill: parent
panelType: root.panelType
}
// list of app media widgets
@ -160,7 +138,6 @@ Item {
padding: root.padding
contentItem: ColumnLayout {
Kirigami.Theme.inherit: true
width: playerItem.width - playerItem.leftPadding - playerItem.rightPadding
spacing: Kirigami.Units.largeSpacing

View file

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

View file

@ -9,26 +9,64 @@ import Qt5Compat.GraphicalEffects
import org.kde.kirigami 2.12 as Kirigami
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell as MobileShell
Item {
id: root
/**
* The content that goes inside the notification card
*/
default property Item contentItem
/**
* The panel background type for this notification.
*/
property int panelType: MobileShell.PanelBackground.PanelType.Drawer
/**
* Whether this is a popup notification.
*/
property bool popupNotification: false
/**
* Whether this popup notification is tucked underneath the current popup.
*/
property bool inPopupDrawer: false
/**
* Whether this notification is within the lockscreen.
*/
property bool inLockScreen: false
/**
* The current notification popup height.
*/
property int currentPopupHeight: 0
/**
* The remaining time before the notification popup is dismissed.
*/
property real remainingTimeProgress: 1
/**
* Whether the timer for dismissing the notification popup is running.
*/
property bool closeTimerRunning: false
/**
* Whether tapping on this notification is enabled.
*/
property bool tapEnabled: false
/**
* Whether swipping on this notification is enabled.
*/
property bool swipeGestureEnabled: false
/**
* The current drag offset for this notification.
*/
property real dragOffset: 0
signal tapped()
@ -57,35 +95,12 @@ Item {
}
}
// shadow
MultiEffect {
MobileShell.PanelBackground {
anchors.fill: mainCard
visible: Math.abs(dragOffset) !== root.width
source: simpleShadow
blurMax: 16
shadowEnabled: true
shadowVerticalOffset: 1
shadowOpacity: popupNotification ? 0.85 : 0.5
shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.2)
animate: true
panelType: root.panelType
}
Rectangle {
id: simpleShadow
visible: Math.abs(dragOffset) !== root.width
anchors.fill: mainCard
anchors.leftMargin: -1
anchors.rightMargin: -1
anchors.bottomMargin: -1
color: {
let darkerBackgroundColor = Qt.darker(Kirigami.Theme.backgroundColor, 1.3);
return Qt.rgba(darkerBackgroundColor.r, darkerBackgroundColor.g, darkerBackgroundColor.b, popupNotification ? 0.5 : 0.3)
}
radius: Kirigami.Units.cornerRadius
}
// card
Item {
id: mainCard
@ -103,17 +118,6 @@ Item {
}
}
Rectangle {
anchors.fill: parent
color: popupNotification ? Qt.lighter(Kirigami.Theme.backgroundColor, 1.5) : Qt.rgba(Kirigami.Theme.backgroundColor.r * 0.95, Kirigami.Theme.backgroundColor.g * 0.95, Kirigami.Theme.backgroundColor.b * 0.95, (root.tapEnabled && mouseArea.pressed) ? 0.95 : 0.85)
opacity: popupNotification ? 0.85 : 1
radius: Kirigami.Units.cornerRadius
layer.enabled: popupNotification ? false : true
layer.effect: MultiEffect {
brightness: 0.075
}
}
ProgressBar {
id: progress
anchors.left: parent.left

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,9 +12,10 @@ import Qt5Compat.GraphicalEffects
import org.kde.kirigami 2.12 as Kirigami
import org.kde.plasma.plasma5support 2.0 as P5Support
import org.kde.plasma.clock
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.plasma.components 3.0 as PlasmaComponents3
@ -27,7 +28,20 @@ import org.kde.notificationmanager as NotificationManager
Item {
id: root
property bool inLockscreen: false
/**
* If the notification is in the lockscreen.
*/
property bool inLockScreen: false
/**
* The panel background type for all the notifications within the widget.
*/
property int panelType: MobileShell.PanelBackground.PanelType.Drawer
/**
* The color scheme of the foreground cards (but not notification headers, which are outside of it).
*/
property var cardColorScheme: Kirigami.Theme.View
/**
* The notification model for the widget.
@ -53,6 +67,31 @@ Item {
*/
property bool actionsRequireUnlock: false
/**
* Top padding of the notification list.
*/
property int topPadding: 0
/**
* Bottom padding of the notification list.
*/
property int bottomPadding: 0
/**
* Header component for notification list.
*/
property var header
/**
* Whether to show the header component.
*/
property bool showHeader: false
/**
* Gives access to the notification list view outside of the notification widget.
*/
property alias listView: list
/**
* Whether the widget has notifications.
*/
@ -73,16 +112,14 @@ Item {
*/
signal unlockRequested()
/**
* Emitted when the background is clicked (not a notification or other element).
*/
signal backgroundClicked()
/**
* Run pending action that was pending for authentication when unlockRequested() was emitted.
*/
function runPendingAction() {
list.pendingNotificationWithAction.runPendingAction();
if (list.pendingNotificationWithAction) {
list.pendingNotificationWithAction.runPendingAction();
list.pendingNotificationWithAction = null;
}
}
/**
@ -97,19 +134,27 @@ Item {
}
/**
* Toggles Do Not Disturb mode.
* Sets Do Not Disturb mode to the intended setting.
* Note: The state may not change to the desired setting, always read doNotDisturbEnabled for the current state.
*/
function toggleDoNotDisturbMode() {
if (doNotDisturbModeEnabled) {
notificationSettings.defaults();
} else {
var until = new Date();
function setDoNotDisturbMode(doNotDisturb: bool) {
if (!doNotDisturb) {
// Turn off do not disturb
notificationSettings.notificationsInhibitedUntil = undefined;
notificationSettings.revokeApplicationInhibitions();
notificationSettings.fullscreenFocused = false;
// overrules current mirrored screen setup, updates again when screen configuration changes
notificationSettings.screensMirrored = false;
} else {
// Turn on do not disturb
// We just have a global toggle, so set it to a really long time (in this case, a year)
var until = new Date();
until.setFullYear(until.getFullYear() + 1);
notificationSettings.notificationsInhibitedUntil = until;
}
notificationSettings.save();
}
@ -120,19 +165,19 @@ Item {
MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_notifications");
}
P5Support.DataSource {
id: timeDataSource
engine: "time"
connectedSources: ["Local"]
interval: 60000 // 1 min
intervalAlignment: P5Support.Types.AlignToMinute
// Implement listening to system "do not disturb" requests
Connections {
target: MobileShellState.ShellDBusClient
function onDoNotDisturbChanged() {
if (root.doNotDisturbModeEnabled !== MobileShellState.ShellDBusClient.doNotDisturb) {
root.setDoNotDisturbMode(MobileShellState.ShellDBusClient.doNotDisturb);
}
}
}
// implement background clicking signal
MouseArea {
anchors.fill: parent
onClicked: backgroundClicked()
z: -1 // ensure that this is below notification items so we don't steal button clicks
Clock {
id: clock
}
ListView {
@ -143,15 +188,16 @@ Item {
currentIndex: 0
property var pendingNotificationWithAction
property NotificationItem pendingNotificationWithAction: null
readonly property int animationDuration: ShellSettings.Settings.animationsEnabled ? Kirigami.Units.longDuration : 0
// If a screen overflow occurs, fix height in order to maintain tool buttons in place.
readonly property bool listOverflowing: contentItem.childrenRect.height + toolButtons.height + spacing >= root.height
readonly property bool listOverflowing: listHeight + spacing >= root.height
readonly property int listHeight: contentItem.childrenRect.height
bottomMargin: spacing
height: count === 0 ? 0 : (listOverflowing ? root.height - toolButtons.height : contentItem.childrenRect.height + bottomMargin)
height: count === 0 ? (root.topPadding + (showHeader ? root.header.height + listHeight + Kirigami.Units.largeSpacing * 2 : 0)) : (listOverflowing ? root.height : listHeight + bottomMargin)
anchors {
top: parent.top
@ -159,7 +205,7 @@ Item {
right: parent.right
}
boundsBehavior: Flickable.StopAtBounds
boundsBehavior: Flickable.DragAndOvershootBounds
spacing: Kirigami.Units.largeSpacing
// TODO keyboard focus
@ -167,6 +213,39 @@ Item {
highlightResizeDuration: 0
highlight: Item {}
// media control widget
// added to the notification list when in landscape mode
Component {
id: headerComponent
Item {
width: parent.width
MobileShell.BaseItem {
id: headerComponentProxy
contentItem: showHeader ? root.header : null
y: root.topPadding + Kirigami.Units.largeSpacing
width: parent.width - Kirigami.Units.gridUnit * 2
anchors.left: parent.left
anchors.leftMargin: Kirigami.Units.gridUnit
}
}
}
// set bottom padding for the notification list
Component {
id: footerComponent
Item {
width: parent.width
height: root.bottomPadding
}
}
header: headerComponent
footer: footerComponent
section {
property: "isGroup"
criteria: ViewSection.FullString
@ -250,12 +329,28 @@ Item {
PropertyAction { target: delegateLoader; property: "ListView.delayRemove"; value: false }
}
// adjust top paddding for media control widget
Component {
id: groupDelegate
NotificationGroupHeader {
applicationName: model.applicationName
applicationIconSource: model.applicationIconName
originName: model.originName || ""
Column {
spacing: Kirigami.Units.smallSpacing
height: headerSpace.height + groupHeader.height
Item {
id: headerSpace
width: parent.width
height: index == 0 ? root.topPadding + (showHeader && root.header.visible ? root.header.height + Kirigami.Units.largeSpacing * 2 : 0) : 0
visible: index == 0
}
NotificationGroupHeader {
id: groupHeader
applicationName: model.applicationName
applicationIconSource: model.applicationIconName
originName: model.originName || ""
}
}
}
@ -265,20 +360,29 @@ Item {
Column {
spacing: Kirigami.Units.smallSpacing
height: notificationItem.height + showMoreLoader.height
height: headerSpace.height + notificationItem.height + showMoreLoader.height
Item {
id: headerSpace
width: parent.width
height: index == 0 ? root.topPadding + (showHeader && root.header.visible ? root.header.height + Kirigami.Units.largeSpacing * 2 : 0) : 0
visible: index == 0
}
NotificationItem {
id: notificationItem
width: parent.width
height: implicitHeight
inLockscreen: root.inLockscreen
inLockScreen: root.inLockScreen
panelType: root.panelType
cardColorScheme: root.cardColorScheme
model: delegateLoader.model
modelIndex: delegateLoader.index
notificationsModel: root.historyModel
notificationsModelType: root.historyModelType
timeSource: timeDataSource
clockSource: clock
requestToInvoke: root.actionsRequireUnlock
onRunActionRequested: {
@ -303,7 +407,7 @@ Item {
return false;
return (model.groupChildrenCount > model.expandedGroupChildrenCount || model.isGroupExpanded)
&& delegateLoader.ListView.nextSection != delegateLoader.ListView.section;
&& delegateLoader.ListView.nextSection != delegateLoader.ListView.section;
}
// state + transition: animates the item when it becomes visible. Fade off is handled by above ListView.onRemove.
@ -323,8 +427,8 @@ Item {
sourceComponent: PlasmaComponents3.ToolButton {
icon.name: model.isGroupExpanded ? "arrow-up" : "arrow-down"
text: model.isGroupExpanded ? i18n("Show Fewer")
: i18nc("Expand to show n more notifications",
"Show %1 More", (model.groupChildrenCount - model.expandedGroupChildrenCount))
: i18nc("Expand to show n more notifications",
"Show %1 More", (model.groupChildrenCount - model.expandedGroupChildrenCount))
onClicked: {
list.setGroupExpanded(model.index, !model.isGroupExpanded)
}
@ -334,58 +438,4 @@ Item {
}
}
}
Item {
id: toolButtons
height: visible ? spacer.height + toolLayout.height + toolLayout.anchors.topMargin + toolLayout.anchors.bottomMargin : 0
// do not show on lockscreen
visible: !root.actionsRequireUnlock
anchors {
top: list.bottom
left: parent.left
right: parent.right
}
Rectangle {
id: spacer
anchors.left: parent.left
anchors.right: parent.right
visible: list.listOverflowing
height: 1
opacity: 0.25
color: Kirigami.Theme.textColor
}
RowLayout {
id: toolLayout
anchors {
top: spacer.bottom
right: parent.right
left: parent.left
leftMargin: Kirigami.Units.largeSpacing
rightMargin: Kirigami.Units.largeSpacing
topMargin: list.spacing
bottomMargin: list.spacing
}
PlasmaComponents3.ToolButton {
id: clearButton
Layout.alignment: Qt.AlignCenter
visible: hasNotifications
font.bold: true
font.pointSize: Kirigami.Theme.smallFont.pointSize
icon.name: "edit-clear-history"
text: i18n("Clear All Notifications")
onClicked: clearHistory()
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more