mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-06-24 23:27:43 +00:00
Compare commits
34 commits
5c5d962b34
...
3d32eaf5b7
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d32eaf5b7 | |||
| 39e5a504a3 | |||
| 8aabeed02b | |||
| 2c0e329037 | |||
| ba1428851e | |||
| e8549f90cd | |||
| ed2fc8301b | |||
| 462ceddd66 | |||
| 3fa5f90ed1 | |||
| d9b2e987dc | |||
| 9364cb29db | |||
| 730385d8c8 | |||
| c52e331d31 | |||
| fe8b798cb2 | |||
| 92e7b78b5e | |||
| c2f4c8ad88 | |||
| b0438bdf48 | |||
| 9d076bf542 | |||
| 378e4830ac | |||
| e72165ec98 | |||
| 709304c99c | |||
| 9f7e1677eb | |||
| 8632d47d68 | |||
| 84a6fa8054 | |||
| 6d79f8ed05 | |||
| a0bad0507f | |||
| 160bc97621 | |||
| 6e67728f46 | |||
| 5c319d2e7e | |||
| 4077292801 | |||
| c469ad49b6 | |||
| c9f219ad60 | |||
| 1ad9fb3e3d | |||
| 566c7f69c3 |
64 changed files with 3408 additions and 270 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -15,10 +15,12 @@ build
|
||||||
/cmake-build*
|
/cmake-build*
|
||||||
.cache
|
.cache
|
||||||
.vscode/
|
.vscode/
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
.kdev4/
|
.kdev4/
|
||||||
*.kdev4
|
*.kdev4
|
||||||
/build*
|
/build*
|
||||||
.prefix/
|
.prefix/
|
||||||
.preview-config/
|
.preview-config/
|
||||||
|
.preview-data/
|
||||||
preview.sh
|
preview.sh
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,3 @@ Files: HACKING.md
|
||||||
Copyright: Plasma-Mobile contributors
|
Copyright: Plasma-Mobile contributors
|
||||||
License: CC-BY-SA-4.0
|
License: CC-BY-SA-4.0
|
||||||
|
|
||||||
Files: Nebra.code-workspace
|
|
||||||
Copyright: Shift contributors
|
|
||||||
License: CC0-1.0
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS
|
||||||
find_package(Plasma CONFIG REQUIRED)
|
find_package(Plasma CONFIG REQUIRED)
|
||||||
find_package(PlasmaQuick CONFIG REQUIRED)
|
find_package(PlasmaQuick CONFIG REQUIRED)
|
||||||
find_package(PlasmaActivities CONFIG REQUIRED)
|
find_package(PlasmaActivities CONFIG REQUIRED)
|
||||||
|
find_package(LibTaskManager CONFIG REQUIRED)
|
||||||
find_package(KF6Screen CONFIG REQUIRED)
|
find_package(KF6Screen CONFIG REQUIRED)
|
||||||
find_package(KWayland CONFIG REQUIRED)
|
find_package(KWayland CONFIG REQUIRED)
|
||||||
find_package(KPipeWire ${PROJECT_DEP_VERSION} REQUIRED)
|
find_package(KPipeWire ${PROJECT_DEP_VERSION} REQUIRED)
|
||||||
|
|
@ -137,12 +138,19 @@ include(CheckIncludeFiles)
|
||||||
|
|
||||||
ecm_find_qmlmodule(org.kde.pipewire 0.1)
|
ecm_find_qmlmodule(org.kde.pipewire 0.1)
|
||||||
|
|
||||||
plasma_install_package(lookandfeel org.kde.breeze.mobile look-and-feel lookandfeel)
|
set(SHIFT_DEFAULT_WALLPAPER_URL "file://${KDE_INSTALL_FULL_WALLPAPERDIR}/SHIFT/")
|
||||||
plasma_install_package(shell org.kde.plasma.mobileshell shells)
|
set(SHIFT_SHELL_PACKAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/shell")
|
||||||
|
file(REMOVE_RECURSE "${SHIFT_SHELL_PACKAGE_DIR}")
|
||||||
|
file(COPY shell/ DESTINATION "${SHIFT_SHELL_PACKAGE_DIR}" PATTERN "layout.js.in" EXCLUDE)
|
||||||
|
configure_file(shell/contents/layout.js.in "${SHIFT_SHELL_PACKAGE_DIR}/contents/layout.js" @ONLY)
|
||||||
|
|
||||||
|
plasma_install_package(lookandfeel org.shift.mobile look-and-feel lookandfeel)
|
||||||
|
plasma_install_package(${SHIFT_SHELL_PACKAGE_DIR} org.kde.plasma.mobileshell shells)
|
||||||
|
|
||||||
add_subdirectory(bin)
|
add_subdirectory(bin)
|
||||||
add_subdirectory(color-schemes)
|
add_subdirectory(color-schemes)
|
||||||
add_subdirectory(icons)
|
add_subdirectory(icons)
|
||||||
|
add_subdirectory(wallpapers)
|
||||||
add_subdirectory(components)
|
add_subdirectory(components)
|
||||||
add_subdirectory(containments)
|
add_subdirectory(containments)
|
||||||
add_subdirectory(devices)
|
add_subdirectory(devices)
|
||||||
|
|
|
||||||
|
|
@ -96,8 +96,7 @@ complete Plasma Mobile runtime so all QML imports resolve:
|
||||||
sudo zypper install --no-confirm \
|
sudo zypper install --no-confirm \
|
||||||
plasma6-mobile plasma6-workspace plasma6-nano plasma6-nm plasma6-pa \
|
plasma6-mobile plasma6-workspace plasma6-nano plasma6-nm plasma6-pa \
|
||||||
layer-shell-qt6-imports kf6-bluez-qt-imports \
|
layer-shell-qt6-imports kf6-bluez-qt-imports \
|
||||||
kf6-networkmanager-qt-imports \
|
kf6-networkmanager-qt-imports
|
||||||
breeze6-wallpapers plasma6-workspace-wallpapers
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"folders": [
|
|
||||||
{
|
|
||||||
"path": "."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -29,7 +29,7 @@ IntensityEffect=0
|
||||||
|
|
||||||
[Colors:Button]
|
[Colors:Button]
|
||||||
BackgroundAlternate=16,82,74
|
BackgroundAlternate=16,82,74
|
||||||
BackgroundNormal=41,44,48
|
BackgroundNormal=34,37,50
|
||||||
DecorationFocus=46,184,168
|
DecorationFocus=46,184,168
|
||||||
DecorationHover=46,184,168
|
DecorationHover=46,184,168
|
||||||
ForegroundActive=46,184,168
|
ForegroundActive=46,184,168
|
||||||
|
|
@ -43,7 +43,7 @@ ForegroundVisited=155,89,182
|
||||||
|
|
||||||
[Colors:Complementary]
|
[Colors:Complementary]
|
||||||
BackgroundAlternate=16,82,74
|
BackgroundAlternate=16,82,74
|
||||||
BackgroundNormal=32,35,38
|
BackgroundNormal=24,27,38
|
||||||
DecorationFocus=46,184,168
|
DecorationFocus=46,184,168
|
||||||
DecorationHover=46,184,168
|
DecorationHover=46,184,168
|
||||||
ForegroundActive=46,184,168
|
ForegroundActive=46,184,168
|
||||||
|
|
@ -56,8 +56,8 @@ ForegroundPositive=39,174,96
|
||||||
ForegroundVisited=155,89,182
|
ForegroundVisited=155,89,182
|
||||||
|
|
||||||
[Colors:Header]
|
[Colors:Header]
|
||||||
BackgroundAlternate=32,35,38
|
BackgroundAlternate=24,27,38
|
||||||
BackgroundNormal=41,44,48
|
BackgroundNormal=34,37,50
|
||||||
DecorationFocus=46,184,168
|
DecorationFocus=46,184,168
|
||||||
DecorationHover=46,184,168
|
DecorationHover=46,184,168
|
||||||
ForegroundActive=46,184,168
|
ForegroundActive=46,184,168
|
||||||
|
|
@ -70,8 +70,8 @@ ForegroundPositive=39,174,96
|
||||||
ForegroundVisited=155,89,182
|
ForegroundVisited=155,89,182
|
||||||
|
|
||||||
[Colors:Header][Inactive]
|
[Colors:Header][Inactive]
|
||||||
BackgroundAlternate=41,44,48
|
BackgroundAlternate=34,37,50
|
||||||
BackgroundNormal=32,35,38
|
BackgroundNormal=24,27,38
|
||||||
DecorationFocus=46,184,168
|
DecorationFocus=46,184,168
|
||||||
DecorationHover=46,184,168
|
DecorationHover=46,184,168
|
||||||
ForegroundActive=46,184,168
|
ForegroundActive=46,184,168
|
||||||
|
|
@ -98,8 +98,8 @@ ForegroundPositive=23,104,57
|
||||||
ForegroundVisited=155,89,182
|
ForegroundVisited=155,89,182
|
||||||
|
|
||||||
[Colors:Tooltip]
|
[Colors:Tooltip]
|
||||||
BackgroundAlternate=32,35,38
|
BackgroundAlternate=24,27,38
|
||||||
BackgroundNormal=41,44,48
|
BackgroundNormal=34,37,50
|
||||||
DecorationFocus=46,184,168
|
DecorationFocus=46,184,168
|
||||||
DecorationHover=46,184,168
|
DecorationHover=46,184,168
|
||||||
ForegroundActive=46,184,168
|
ForegroundActive=46,184,168
|
||||||
|
|
@ -112,8 +112,8 @@ ForegroundPositive=39,174,96
|
||||||
ForegroundVisited=155,89,182
|
ForegroundVisited=155,89,182
|
||||||
|
|
||||||
[Colors:View]
|
[Colors:View]
|
||||||
BackgroundAlternate=29,31,34
|
BackgroundAlternate=20,22,32
|
||||||
BackgroundNormal=20,22,24
|
BackgroundNormal=14,15,22
|
||||||
DecorationFocus=46,184,168
|
DecorationFocus=46,184,168
|
||||||
DecorationHover=46,184,168
|
DecorationHover=46,184,168
|
||||||
ForegroundActive=46,184,168
|
ForegroundActive=46,184,168
|
||||||
|
|
@ -126,8 +126,8 @@ ForegroundPositive=39,174,96
|
||||||
ForegroundVisited=155,89,182
|
ForegroundVisited=155,89,182
|
||||||
|
|
||||||
[Colors:Window]
|
[Colors:Window]
|
||||||
BackgroundAlternate=41,44,48
|
BackgroundAlternate=34,37,50
|
||||||
BackgroundNormal=32,35,38
|
BackgroundNormal=24,27,38
|
||||||
DecorationFocus=46,184,168
|
DecorationFocus=46,184,168
|
||||||
DecorationHover=46,184,168
|
DecorationHover=46,184,168
|
||||||
ForegroundActive=46,184,168
|
ForegroundActive=46,184,168
|
||||||
|
|
@ -148,9 +148,9 @@ shadeSortColumn=true
|
||||||
contrast=4
|
contrast=4
|
||||||
|
|
||||||
[WM]
|
[WM]
|
||||||
activeBackground=39,44,49
|
activeBackground=28,33,46
|
||||||
activeBlend=252,252,252
|
activeBlend=252,252,252
|
||||||
activeForeground=252,252,252
|
activeForeground=252,252,252
|
||||||
inactiveBackground=32,36,40
|
inactiveBackground=22,27,38
|
||||||
inactiveBlend=161,169,177
|
inactiveBlend=161,169,177
|
||||||
inactiveForeground=161,169,177
|
inactiveForeground=161,169,177
|
||||||
|
|
|
||||||
|
|
@ -357,6 +357,21 @@ void GameLauncherProvider::refresh()
|
||||||
|
|
||||||
m_allGames.clear();
|
m_allGames.clear();
|
||||||
|
|
||||||
|
// Detect which third-party launchers are installed
|
||||||
|
const bool steamWas = m_steamAvailable;
|
||||||
|
const bool lutrisWas = m_lutrisAvailable;
|
||||||
|
const bool heroicWas = m_heroicAvailable;
|
||||||
|
m_steamAvailable = detectLauncher(QStringLiteral("steam"),
|
||||||
|
{QStringLiteral("com.valvesoftware.Steam")},
|
||||||
|
{QDir::homePath() + QStringLiteral("/.steam/steam"),
|
||||||
|
QDir::homePath() + QStringLiteral("/.local/share/Steam"),
|
||||||
|
QDir::homePath() + QStringLiteral("/.var/app/com.valvesoftware.Steam/.local/share/Steam")});
|
||||||
|
m_lutrisAvailable =
|
||||||
|
detectLauncher(QStringLiteral("lutris"), {QStringLiteral("net.lutris.Lutris")}, {QDir::homePath() + QStringLiteral("/.local/share/lutris")});
|
||||||
|
m_heroicAvailable =
|
||||||
|
detectLauncher(QStringLiteral("heroic"), {QStringLiteral("com.heroicgameslauncher.hgl")}, {QDir::homePath() + QStringLiteral("/.config/heroic")});
|
||||||
|
if (m_steamAvailable != steamWas || m_lutrisAvailable != lutrisWas || m_heroicAvailable != heroicWas)
|
||||||
|
Q_EMIT launcherAvailabilityChanged();
|
||||||
loadDesktopGames();
|
loadDesktopGames();
|
||||||
loadSteamGames();
|
loadSteamGames();
|
||||||
loadLutrisGames();
|
loadLutrisGames();
|
||||||
|
|
@ -904,6 +919,37 @@ bool GameLauncherProvider::mangohudAvailable() const
|
||||||
return m_mangohudAvailable;
|
return m_mangohudAvailable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool GameLauncherProvider::steamAvailable() const
|
||||||
|
{
|
||||||
|
return m_steamAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameLauncherProvider::lutrisAvailable() const
|
||||||
|
{
|
||||||
|
return m_lutrisAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameLauncherProvider::heroicAvailable() const
|
||||||
|
{
|
||||||
|
return m_heroicAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
bool GameLauncherProvider::detectLauncher(const QString &executable, const QStringList &flatpakAppIds, const QStringList &dataDirs)
|
||||||
|
{
|
||||||
|
if (!QStandardPaths::findExecutable(executable).isEmpty())
|
||||||
|
return true;
|
||||||
|
for (const QString &appId : flatpakAppIds) {
|
||||||
|
if (QDir(QDir::homePath() + QStringLiteral("/.var/app/") + appId).exists())
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (const QString &dir : dataDirs) {
|
||||||
|
if (QDir(dir).exists())
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
int GameLauncherProvider::fpsLimit() const
|
int GameLauncherProvider::fpsLimit() const
|
||||||
{
|
{
|
||||||
return m_fpsLimit;
|
return m_fpsLimit;
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@ class GameLauncherProvider : public QAbstractListModel
|
||||||
Q_PROPERTY(QString sourceFilter READ sourceFilter WRITE setSourceFilter NOTIFY sourceFilterChanged)
|
Q_PROPERTY(QString sourceFilter READ sourceFilter WRITE setSourceFilter NOTIFY sourceFilterChanged)
|
||||||
Q_PROPERTY(bool overlayEnabled READ overlayEnabled WRITE setOverlayEnabled NOTIFY overlayEnabledChanged)
|
Q_PROPERTY(bool overlayEnabled READ overlayEnabled WRITE setOverlayEnabled NOTIFY overlayEnabledChanged)
|
||||||
Q_PROPERTY(bool mangohudAvailable READ mangohudAvailable NOTIFY mangohudAvailableChanged)
|
Q_PROPERTY(bool mangohudAvailable READ mangohudAvailable NOTIFY mangohudAvailableChanged)
|
||||||
|
Q_PROPERTY(bool steamAvailable READ steamAvailable NOTIFY launcherAvailabilityChanged)
|
||||||
|
Q_PROPERTY(bool lutrisAvailable READ lutrisAvailable NOTIFY launcherAvailabilityChanged)
|
||||||
|
Q_PROPERTY(bool heroicAvailable READ heroicAvailable NOTIFY launcherAvailabilityChanged)
|
||||||
Q_PROPERTY(int fpsLimit READ fpsLimit WRITE setFpsLimit NOTIFY fpsLimitChanged)
|
Q_PROPERTY(int fpsLimit READ fpsLimit WRITE setFpsLimit NOTIFY fpsLimitChanged)
|
||||||
Q_PROPERTY(bool launchPending READ launchPending NOTIFY launchPendingChanged)
|
Q_PROPERTY(bool launchPending READ launchPending NOTIFY launchPendingChanged)
|
||||||
Q_PROPERTY(QString pendingLaunchName READ pendingLaunchName NOTIFY launchPendingChanged)
|
Q_PROPERTY(QString pendingLaunchName READ pendingLaunchName NOTIFY launchPendingChanged)
|
||||||
|
|
@ -61,6 +64,9 @@ public:
|
||||||
bool overlayEnabled() const;
|
bool overlayEnabled() const;
|
||||||
void setOverlayEnabled(bool enabled);
|
void setOverlayEnabled(bool enabled);
|
||||||
bool mangohudAvailable() const;
|
bool mangohudAvailable() const;
|
||||||
|
bool steamAvailable() const;
|
||||||
|
bool lutrisAvailable() const;
|
||||||
|
bool heroicAvailable() const;
|
||||||
int fpsLimit() const;
|
int fpsLimit() const;
|
||||||
void setFpsLimit(int limit);
|
void setFpsLimit(int limit);
|
||||||
bool launchPending() const;
|
bool launchPending() const;
|
||||||
|
|
@ -89,6 +95,7 @@ Q_SIGNALS:
|
||||||
void sourceFilterChanged();
|
void sourceFilterChanged();
|
||||||
void overlayEnabledChanged();
|
void overlayEnabledChanged();
|
||||||
void mangohudAvailableChanged();
|
void mangohudAvailableChanged();
|
||||||
|
void launcherAvailabilityChanged();
|
||||||
void fpsLimitChanged();
|
void fpsLimitChanged();
|
||||||
void launchPendingChanged();
|
void launchPendingChanged();
|
||||||
void lastLaunchErrorChanged();
|
void lastLaunchErrorChanged();
|
||||||
|
|
@ -137,6 +144,11 @@ private:
|
||||||
int m_fpsLimit = 0;
|
int m_fpsLimit = 0;
|
||||||
bool m_mangohudAvailable = false;
|
bool m_mangohudAvailable = false;
|
||||||
QString m_mangohudPath;
|
QString m_mangohudPath;
|
||||||
|
bool m_steamAvailable = false;
|
||||||
|
bool m_lutrisAvailable = false;
|
||||||
|
bool m_heroicAvailable = false;
|
||||||
|
|
||||||
|
static bool detectLauncher(const QString &executable, const QStringList &flatpakAppIds, const QStringList &dataDirs);
|
||||||
QSet<QString> m_pinnedGames;
|
QSet<QString> m_pinnedGames;
|
||||||
bool m_launchPending = false;
|
bool m_launchPending = false;
|
||||||
QString m_pendingLaunchName;
|
QString m_pendingLaunchName;
|
||||||
|
|
|
||||||
|
|
@ -41,11 +41,21 @@ MobileShell.BaseItem {
|
||||||
|
|
||||||
readonly property color enabledButtonBorderColor: Qt.darker(Kirigami.Theme.highlightColor, 1.25)
|
readonly property color enabledButtonBorderColor: Qt.darker(Kirigami.Theme.highlightColor, 1.25)
|
||||||
readonly property color disabledButtonBorderColor: separatorColorHelper(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.2)
|
readonly property color disabledButtonBorderColor: separatorColorHelper(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.2)
|
||||||
readonly property color enabledButtonColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.6)
|
readonly property color enabledButtonColor: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.25)
|
||||||
readonly property color enabledButtonPressedColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.4);
|
readonly property color enabledButtonHoverColor: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.32)
|
||||||
readonly property color disabledButtonColor: Kirigami.Theme.backgroundColor
|
readonly property color enabledButtonPressedColor: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.12);
|
||||||
|
readonly property color disabledButtonColor: Kirigami.Theme.alternateBackgroundColor
|
||||||
|
readonly property color disabledButtonHoverColor: mixColor(Kirigami.Theme.alternateBackgroundColor, Kirigami.Theme.textColor, 0.06)
|
||||||
readonly property color disabledButtonPressedColor: Qt.darker(disabledButtonColor, 1.1)
|
readonly property color disabledButtonPressedColor: Qt.darker(disabledButtonColor, 1.1)
|
||||||
|
|
||||||
|
function mixColor(base, overlay, ratio) {
|
||||||
|
return Qt.rgba(
|
||||||
|
base.r + (overlay.r - base.r) * ratio,
|
||||||
|
base.g + (overlay.g - base.g) * ratio,
|
||||||
|
base.b + (overlay.b - base.b) * ratio,
|
||||||
|
base.a + (overlay.a - base.a) * ratio)
|
||||||
|
}
|
||||||
|
|
||||||
function separatorColorHelper(bg, fg, baseRatio) {
|
function separatorColorHelper(bg, fg, baseRatio) {
|
||||||
if (Kirigami.ColorUtils.brightnessForColor(bg) === Kirigami.ColorUtils.Light) {
|
if (Kirigami.ColorUtils.brightnessForColor(bg) === Kirigami.ColorUtils.Light) {
|
||||||
return Kirigami.ColorUtils.linearInterpolation(bg, fg, baseRatio);
|
return Kirigami.ColorUtils.linearInterpolation(bg, fg, baseRatio);
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ QuickSettingsDelegate {
|
||||||
|
|
||||||
padding: Kirigami.Units.smallSpacing * 2
|
padding: Kirigami.Units.smallSpacing * 2
|
||||||
iconItem: icon
|
iconItem: icon
|
||||||
|
readonly property int tileRadius: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
// scale animation on press
|
// scale animation on press
|
||||||
zoomScale: (ShellSettings.Settings.animationsEnabled && mouseArea.pressed) ? 0.9 : 1
|
zoomScale: (ShellSettings.Settings.animationsEnabled && mouseArea.pressed) ? 0.9 : 1
|
||||||
|
|
@ -33,24 +34,35 @@ QuickSettingsDelegate {
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
height: parent.height
|
height: parent.height
|
||||||
|
|
||||||
radius: Kirigami.Units.cornerRadius
|
radius: root.tileRadius
|
||||||
color: Qt.rgba(0, 0, 0, 0.075)
|
color: Qt.rgba(0, 0, 0, root.enabled ? 0.12 : 0.08)
|
||||||
}
|
}
|
||||||
|
|
||||||
// background color
|
// background color
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
id: tileRect
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: Kirigami.Units.cornerRadius
|
radius: root.tileRadius
|
||||||
border.pixelAligned: false
|
border.pixelAligned: false
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor
|
border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor
|
||||||
color: {
|
color: {
|
||||||
if (root.enabled) {
|
if (root.enabled) {
|
||||||
return mouseArea.pressed ? root.enabledButtonPressedColor : root.enabledButtonColor
|
if (mouseArea.pressed) {
|
||||||
|
return root.enabledButtonPressedColor
|
||||||
|
}
|
||||||
|
return mouseArea.containsMouse ? root.enabledButtonHoverColor : root.enabledButtonColor
|
||||||
} else {
|
} else {
|
||||||
return mouseArea.pressed ? root.disabledButtonPressedColor : root.disabledButtonColor
|
if (mouseArea.pressed) {
|
||||||
|
return root.disabledButtonPressedColor
|
||||||
|
}
|
||||||
|
return mouseArea.containsMouse ? root.disabledButtonHoverColor : root.disabledButtonColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: ShellSettings.Settings.animationsEnabled ? Kirigami.Units.shortDuration : 0; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,6 +72,7 @@ QuickSettingsDelegate {
|
||||||
|
|
||||||
contentItem: MouseArea {
|
contentItem: MouseArea {
|
||||||
id: mouseArea
|
id: mouseArea
|
||||||
|
hoverEnabled: true
|
||||||
|
|
||||||
onPressed: haptics.buttonVibrate()
|
onPressed: haptics.buttonVibrate()
|
||||||
onClicked: root.delegateClick()
|
onClicked: root.delegateClick()
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,21 @@ QuickSettingsDelegate {
|
||||||
border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor
|
border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor
|
||||||
color: {
|
color: {
|
||||||
if (root.enabled) {
|
if (root.enabled) {
|
||||||
return mouseArea.pressed ? root.enabledButtonPressedColor : root.enabledButtonColor
|
if (mouseArea.pressed) {
|
||||||
|
return root.enabledButtonPressedColor
|
||||||
|
}
|
||||||
|
return mouseArea.containsMouse ? root.enabledButtonHoverColor : root.enabledButtonColor
|
||||||
} else {
|
} else {
|
||||||
return mouseArea.pressed ? root.disabledButtonPressedColor : root.disabledButtonColor
|
if (mouseArea.pressed) {
|
||||||
|
return root.disabledButtonPressedColor
|
||||||
|
}
|
||||||
|
return mouseArea.containsMouse ? root.disabledButtonHoverColor : root.disabledButtonColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: ShellSettings.Settings.animationsEnabled ? Kirigami.Units.shortDuration : 0; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,6 +69,7 @@ QuickSettingsDelegate {
|
||||||
|
|
||||||
contentItem: MouseArea {
|
contentItem: MouseArea {
|
||||||
id: mouseArea
|
id: mouseArea
|
||||||
|
hoverEnabled: true
|
||||||
|
|
||||||
onPressed: haptics.buttonVibrate();
|
onPressed: haptics.buttonVibrate();
|
||||||
onClicked: root.delegateClick()
|
onClicked: root.delegateClick()
|
||||||
|
|
|
||||||
|
|
@ -30,14 +30,14 @@ Item {
|
||||||
Kirigami.Theme.inherit: false
|
Kirigami.Theme.inherit: false
|
||||||
Kirigami.Theme.colorSet: Kirigami.Theme.Button
|
Kirigami.Theme.colorSet: Kirigami.Theme.Button
|
||||||
|
|
||||||
// ── Palette (shared with tile delegates) ────────────────────────────
|
readonly property int rowRadius: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
|
||||||
readonly property color enabledBg: Kirigami.ColorUtils.tintWithAlpha(
|
readonly property color enabledBg: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.25)
|
||||||
Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.6)
|
readonly property color enabledBgHover: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.32)
|
||||||
readonly property color enabledBgPressed: Kirigami.ColorUtils.tintWithAlpha(
|
readonly property color enabledBgPressed: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.12)
|
||||||
Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.4)
|
|
||||||
readonly property color enabledBorder: Qt.darker(Kirigami.Theme.highlightColor, 1.25)
|
readonly property color enabledBorder: Qt.darker(Kirigami.Theme.highlightColor, 1.25)
|
||||||
|
|
||||||
readonly property color disabledBg: Kirigami.Theme.backgroundColor
|
readonly property color disabledBg: Kirigami.Theme.alternateBackgroundColor
|
||||||
|
readonly property color disabledBgHover: mixColor(Kirigami.Theme.alternateBackgroundColor, Kirigami.Theme.textColor, 0.06)
|
||||||
readonly property color disabledBgPressed: Qt.darker(disabledBg, 1.1)
|
readonly property color disabledBgPressed: Qt.darker(disabledBg, 1.1)
|
||||||
readonly property color disabledBorder: {
|
readonly property color disabledBorder: {
|
||||||
let bg = Kirigami.Theme.backgroundColor;
|
let bg = Kirigami.Theme.backgroundColor;
|
||||||
|
|
@ -49,6 +49,14 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mixColor(base, overlay, ratio) {
|
||||||
|
return Qt.rgba(
|
||||||
|
base.r + (overlay.r - base.r) * ratio,
|
||||||
|
base.g + (overlay.g - base.g) * ratio,
|
||||||
|
base.b + (overlay.b - base.b) * ratio,
|
||||||
|
base.a + (overlay.a - base.a) * ratio)
|
||||||
|
}
|
||||||
|
|
||||||
MobileShell.HapticsEffect { id: haptics }
|
MobileShell.HapticsEffect { id: haptics }
|
||||||
|
|
||||||
// ── Outer card ──────────────────────────────────────────────────────
|
// ── Outer card ──────────────────────────────────────────────────────
|
||||||
|
|
@ -59,8 +67,8 @@ Item {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
height: parent.height
|
height: parent.height
|
||||||
radius: Kirigami.Units.cornerRadius
|
radius: root.rowRadius
|
||||||
color: Qt.rgba(0, 0, 0, 0.075)
|
color: Qt.rgba(0, 0, 0, root.enabled ? 0.12 : 0.08)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Card background — always neutral base (the toggle pill carries the
|
// Card background — always neutral base (the toggle pill carries the
|
||||||
|
|
@ -68,7 +76,7 @@ Item {
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: cardBg
|
id: cardBg
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: Kirigami.Units.cornerRadius
|
radius: root.rowRadius
|
||||||
border.pixelAligned: false
|
border.pixelAligned: false
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: root.disabledBorder
|
border.color: root.disabledBorder
|
||||||
|
|
@ -96,9 +104,15 @@ Item {
|
||||||
border.color: root.enabled ? root.enabledBorder : root.disabledBorder
|
border.color: root.enabled ? root.enabledBorder : root.disabledBorder
|
||||||
color: {
|
color: {
|
||||||
if (root.enabled) {
|
if (root.enabled) {
|
||||||
return toggleMouse.pressed ? root.enabledBgPressed : root.enabledBg;
|
if (toggleMouse.pressed) {
|
||||||
|
return root.enabledBgPressed;
|
||||||
|
}
|
||||||
|
return toggleMouse.containsMouse ? root.enabledBgHover : root.enabledBg;
|
||||||
}
|
}
|
||||||
return toggleMouse.pressed ? root.disabledBgPressed : root.disabledBg;
|
if (toggleMouse.pressed) {
|
||||||
|
return root.disabledBgPressed;
|
||||||
|
}
|
||||||
|
return toggleMouse.containsMouse ? root.disabledBgHover : root.disabledBg;
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
|
|
@ -129,20 +143,25 @@ Item {
|
||||||
source: root.icon
|
source: root.icon
|
||||||
}
|
}
|
||||||
|
|
||||||
// Indicator dot
|
// Indicator bar
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
width: Kirigami.Units.smallSpacing * 1.5
|
width: root.enabled ? Kirigami.Units.smallSpacing * 3 : Kirigami.Units.smallSpacing * 1.5
|
||||||
height: width
|
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio))
|
||||||
radius: width / 2
|
radius: height / 2
|
||||||
color: root.enabled ? Kirigami.Theme.highlightColor : Kirigami.Theme.disabledTextColor
|
color: root.enabled ? Kirigami.Theme.highlightColor : Kirigami.Theme.disabledTextColor
|
||||||
opacity: root.enabled ? 1.0 : 0.4
|
opacity: root.enabled ? 1.0 : 0.4
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: toggleMouse
|
id: toggleMouse
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onPressed: haptics.buttonVibrate()
|
onPressed: haptics.buttonVibrate()
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ QtObject {
|
||||||
? 0
|
? 0
|
||||||
: Kirigami.Units.gridUnit * 2
|
: Kirigami.Units.gridUnit * 2
|
||||||
readonly property real defaultGesturePanelThickness: Kirigami.Units.gridUnit
|
readonly property real defaultGesturePanelThickness: Kirigami.Units.gridUnit
|
||||||
|
readonly property real convergenceDockHeight: Kirigami.Units.gridUnit * 3
|
||||||
|
readonly property real convergenceDockRevealHeight: Kirigami.Units.gridUnit
|
||||||
|
|
||||||
readonly property real navigationPanelThickness: {
|
readonly property real navigationPanelThickness: {
|
||||||
if (!ShellSettings.Settings.navigationPanelEnabled) {
|
if (!ShellSettings.Settings.navigationPanelEnabled) {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,29 @@ Rectangle {
|
||||||
easing.type: Easing.OutExpo
|
easing.type: Easing.OutExpo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-clear safety net.
|
||||||
|
//
|
||||||
|
// The colored fill is normally cleared by onShowingWindowChanged when
|
||||||
|
// the launched app's maximized state toggles. In convergence mode apps
|
||||||
|
// launch centered (kwinrc Placement=Centered), so showingWindow may
|
||||||
|
// never flip to true and the change-based cleanup never fires — the
|
||||||
|
// band would otherwise remain on the panel indefinitely.
|
||||||
|
//
|
||||||
|
// This timer runs after every panel-fill animation and clears the
|
||||||
|
// rectangle if no maximized/fullscreen window is present, restoring
|
||||||
|
// the original mobile behaviour while fixing the convergence path.
|
||||||
|
Timer {
|
||||||
|
id: autoClearTimer
|
||||||
|
interval: 600 // animation duration (200) + settle time
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
if (!root.maximizedTracker || !root.maximizedTracker.showingWindow) {
|
||||||
|
root.color = 'transparent';
|
||||||
|
root.height = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reset when maximized window state changes
|
// Reset when maximized window state changes
|
||||||
Connections {
|
Connections {
|
||||||
target: maximizedTracker
|
target: maximizedTracker
|
||||||
|
|
@ -46,6 +69,7 @@ Rectangle {
|
||||||
|
|
||||||
root.color = color;
|
root.color = color;
|
||||||
heightAnim.restart();
|
heightAnim.restart();
|
||||||
|
autoClearTimer.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,14 @@ Controls.Drawer {
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
radius: Kirigami.Units.cornerRadius
|
radius: Kirigami.Units.cornerRadius
|
||||||
color: Qt.rgba(255, 255, 255, (openSettings.down || openSettings.highlighted) ? 0.3 : 0.2)
|
Kirigami.Theme.inherit: false
|
||||||
|
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||||
|
color: Qt.rgba(
|
||||||
|
Kirigami.Theme.backgroundColor.r,
|
||||||
|
Kirigami.Theme.backgroundColor.g,
|
||||||
|
Kirigami.Theme.backgroundColor.b,
|
||||||
|
(openSettings.down || openSettings.highlighted) ? 0.9 : 0.75
|
||||||
|
)
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (maskManager) {
|
if (maskManager) {
|
||||||
|
|
@ -77,8 +84,7 @@ Controls.Drawer {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
implicitHeight: Kirigami.Units.iconSizes.large
|
implicitHeight: Kirigami.Units.iconSizes.large
|
||||||
implicitWidth: Kirigami.Units.iconSizes.large
|
implicitWidth: Kirigami.Units.iconSizes.large
|
||||||
source: 'list-add'
|
source: 'configure'
|
||||||
color: 'white'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,7 +157,14 @@ Controls.Drawer {
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: Qt.rgba(255, 255, 255, (delegate.down || delegate.highlighted) ? 0.4 : 0.2)
|
Kirigami.Theme.inherit: false
|
||||||
|
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||||
|
color: Qt.rgba(
|
||||||
|
Kirigami.Theme.backgroundColor.r,
|
||||||
|
Kirigami.Theme.backgroundColor.g,
|
||||||
|
Kirigami.Theme.backgroundColor.b,
|
||||||
|
(delegate.down || delegate.highlighted) ? 0.9 : 0.75
|
||||||
|
)
|
||||||
radius: Kirigami.Units.cornerRadius
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
|
|
||||||
|
|
@ -16,22 +16,22 @@ QtObject {
|
||||||
id: component
|
id: component
|
||||||
|
|
||||||
property bool initialConvergenceMode: false
|
property bool initialConvergenceMode: false
|
||||||
|
property bool wasDocked: false
|
||||||
|
|
||||||
property var apiListener: Connections {
|
property var apiListener: Connections {
|
||||||
target: KScreenOSDUtil
|
target: KScreenOSDUtil
|
||||||
function onOutputsChanged() {
|
function onOutputsChanged() {
|
||||||
if (KScreenOSDUtil.outputs > 1) {
|
const docked = KScreenOSDUtil.outputs > 1;
|
||||||
|
if (docked && !wasDocked) {
|
||||||
initialConvergenceMode = ShellSettings.Settings.convergenceModeEnabled;
|
initialConvergenceMode = ShellSettings.Settings.convergenceModeEnabled;
|
||||||
}
|
}
|
||||||
- console.log("KScreenOSDProvider convergenceModeEnabled: "
|
ShellSettings.Settings.convergenceModeEnabled = docked ? true : initialConvergenceMode;
|
||||||
- + (KScreenOSDUtil.outputs > 1 ? "true" : (initialConvergenceMode ? "TRUE" : "FALSE")));
|
wasDocked = docked;
|
||||||
ShellSettings.Settings.convergenceModeEnabled = KScreenOSDUtil.outputs > 1 ? true : initialConvergenceMode;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (KScreenOSDUtil.outputs < 2) {
|
wasDocked = KScreenOSDUtil.outputs > 1;
|
||||||
initialConvergenceMode = ShellSettings.Settings.convergenceModeEnabled;
|
initialConvergenceMode = ShellSettings.Settings.convergenceModeEnabled;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const QString CONFIG_FILE = QStringLiteral("kwinrc");
|
||||||
const QString OVERLAY_CONFIG_FILE = QStringLiteral("plasma-mobile/kwinrc");
|
const QString OVERLAY_CONFIG_FILE = QStringLiteral("plasma-mobile/kwinrc");
|
||||||
const QString WAYLAND_CONFIG_GROUP = QStringLiteral("Wayland");
|
const QString WAYLAND_CONFIG_GROUP = QStringLiteral("Wayland");
|
||||||
const QString SCREEN_EDGES_CONFIG_GROUP = QStringLiteral("ScreenEdges");
|
const QString SCREEN_EDGES_CONFIG_GROUP = QStringLiteral("ScreenEdges");
|
||||||
|
const QString DECORATION_CONFIG_GROUP = QStringLiteral("org.kde.kdecoration2");
|
||||||
|
|
||||||
KWinSettings::KWinSettings(QObject *parent)
|
KWinSettings::KWinSettings(QObject *parent)
|
||||||
: QObject{parent}
|
: QObject{parent}
|
||||||
|
|
@ -26,6 +27,8 @@ KWinSettings::KWinSettings(QObject *parent)
|
||||||
Q_EMIT doubleTapWakeupChanged();
|
Q_EMIT doubleTapWakeupChanged();
|
||||||
} else if (group.name() == SCREEN_EDGES_CONFIG_GROUP) {
|
} else if (group.name() == SCREEN_EDGES_CONFIG_GROUP) {
|
||||||
Q_EMIT screenEdgeTouchTargetChanged();
|
Q_EMIT screenEdgeTouchTargetChanged();
|
||||||
|
} else if (group.name() == DECORATION_CONFIG_GROUP) {
|
||||||
|
Q_EMIT titleButtonsChanged();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -64,3 +67,15 @@ void KWinSettings::setScreenEdgeTouchTarget(int target)
|
||||||
QDBusConnection::sessionBus().send(message);
|
QDBusConnection::sessionBus().send(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString KWinSettings::titleButtonsOnLeft() const
|
||||||
|
{
|
||||||
|
auto group = KConfigGroup{m_config, DECORATION_CONFIG_GROUP};
|
||||||
|
return group.readEntry("ButtonsOnLeft", QStringLiteral("MSE"));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString KWinSettings::titleButtonsOnRight() const
|
||||||
|
{
|
||||||
|
auto group = KConfigGroup{m_config, DECORATION_CONFIG_GROUP};
|
||||||
|
return group.readEntry("ButtonsOnRight", QStringLiteral("HIAX"));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ class KWinSettings : public QObject
|
||||||
|
|
||||||
Q_PROPERTY(bool doubleTapWakeup READ doubleTapWakeup WRITE setDoubleTapWakeup NOTIFY doubleTapWakeupChanged)
|
Q_PROPERTY(bool doubleTapWakeup READ doubleTapWakeup WRITE setDoubleTapWakeup NOTIFY doubleTapWakeupChanged)
|
||||||
Q_PROPERTY(int screenEdgeTouchTarget READ screenEdgeTouchTarget WRITE setScreenEdgeTouchTarget NOTIFY screenEdgeTouchTargetChanged)
|
Q_PROPERTY(int screenEdgeTouchTarget READ screenEdgeTouchTarget WRITE setScreenEdgeTouchTarget NOTIFY screenEdgeTouchTargetChanged)
|
||||||
|
Q_PROPERTY(QString titleButtonsOnLeft READ titleButtonsOnLeft NOTIFY titleButtonsChanged)
|
||||||
|
Q_PROPERTY(QString titleButtonsOnRight READ titleButtonsOnRight NOTIFY titleButtonsChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
KWinSettings(QObject *parent = nullptr);
|
KWinSettings(QObject *parent = nullptr);
|
||||||
|
|
@ -48,9 +50,20 @@ public:
|
||||||
*/
|
*/
|
||||||
void setScreenEdgeTouchTarget(int target);
|
void setScreenEdgeTouchTarget(int target);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configured KWin titlebar buttons on the left side.
|
||||||
|
*/
|
||||||
|
QString titleButtonsOnLeft() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configured KWin titlebar buttons on the right side.
|
||||||
|
*/
|
||||||
|
QString titleButtonsOnRight() const;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void doubleTapWakeupChanged();
|
void doubleTapWakeupChanged();
|
||||||
void screenEdgeTouchTargetChanged();
|
void screenEdgeTouchTargetChanged();
|
||||||
|
void titleButtonsChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
KConfigWatcher::Ptr m_configWatcher;
|
KConfigWatcher::Ptr m_configWatcher;
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ MobileShellSettings::MobileShellSettings(QObject *parent)
|
||||||
Q_EMIT autoHidePanelsEnabledChanged();
|
Q_EMIT autoHidePanelsEnabledChanged();
|
||||||
Q_EMIT gamingModeEnabledChanged();
|
Q_EMIT gamingModeEnabledChanged();
|
||||||
Q_EMIT gamingDismissHintEnabledChanged();
|
Q_EMIT gamingDismissHintEnabledChanged();
|
||||||
|
Q_EMIT dynamicTilingEnabledChanged();
|
||||||
Q_EMIT allowLogoutChanged();
|
Q_EMIT allowLogoutChanged();
|
||||||
}
|
}
|
||||||
if (group.name() == LOCKSCREEN_CONFIG_GROUP) {
|
if (group.name() == LOCKSCREEN_CONFIG_GROUP) {
|
||||||
|
|
@ -276,6 +277,19 @@ void MobileShellSettings::setGamingDismissHintEnabled(bool enabled)
|
||||||
m_config->sync();
|
m_config->sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MobileShellSettings::dynamicTilingEnabled() const
|
||||||
|
{
|
||||||
|
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
|
||||||
|
return group.readEntry("dynamicTilingEnabled", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MobileShellSettings::setDynamicTilingEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
|
||||||
|
group.writeEntry("dynamicTilingEnabled", enabled, KConfigGroup::Notify);
|
||||||
|
m_config->sync();
|
||||||
|
}
|
||||||
|
|
||||||
void MobileShellSettings::updateNavigationBarsInPlasma()
|
void MobileShellSettings::updateNavigationBarsInPlasma()
|
||||||
{
|
{
|
||||||
// Do not update panels when not in Plasma Mobile
|
// Do not update panels when not in Plasma Mobile
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,10 @@ class MobileShellSettings : public QObject
|
||||||
Q_PROPERTY(bool gamingModeEnabled READ gamingModeEnabled WRITE setGamingModeEnabled NOTIFY gamingModeEnabledChanged)
|
Q_PROPERTY(bool gamingModeEnabled READ gamingModeEnabled WRITE setGamingModeEnabled NOTIFY gamingModeEnabledChanged)
|
||||||
Q_PROPERTY(bool gamingDismissHintEnabled READ gamingDismissHintEnabled WRITE setGamingDismissHintEnabled NOTIFY gamingDismissHintEnabledChanged)
|
Q_PROPERTY(bool gamingDismissHintEnabled READ gamingDismissHintEnabled WRITE setGamingDismissHintEnabled NOTIFY gamingDismissHintEnabledChanged)
|
||||||
|
|
||||||
|
// Dynamic (BSP) window tiling — only meaningful in convergence mode.
|
||||||
|
// When false, KWin's native quick-tile behaviour is used unmodified.
|
||||||
|
Q_PROPERTY(bool dynamicTilingEnabled READ dynamicTilingEnabled WRITE setDynamicTilingEnabled NOTIFY dynamicTilingEnabledChanged)
|
||||||
|
|
||||||
// logout dialog
|
// logout dialog
|
||||||
Q_PROPERTY(bool allowLogout READ allowLogout READ allowLogout NOTIFY allowLogoutChanged)
|
Q_PROPERTY(bool allowLogout READ allowLogout READ allowLogout NOTIFY allowLogoutChanged)
|
||||||
|
|
||||||
|
|
@ -274,6 +278,14 @@ public:
|
||||||
bool gamingDismissHintEnabled() const;
|
bool gamingDismissHintEnabled() const;
|
||||||
void setGamingDismissHintEnabled(bool enabled);
|
void setGamingDismissHintEnabled(bool enabled);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the SHIFT BSP dynamic tiling script is active.
|
||||||
|
* Defaults to true; only takes effect in convergence mode (and when
|
||||||
|
* gaming mode is off).
|
||||||
|
*/
|
||||||
|
bool dynamicTilingEnabled() const;
|
||||||
|
void setDynamicTilingEnabled(bool enabled);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether logout button is shown in the logout/shutdown dialog.
|
* Whether logout button is shown in the logout/shutdown dialog.
|
||||||
*/
|
*/
|
||||||
|
|
@ -322,6 +334,7 @@ Q_SIGNALS:
|
||||||
void autoHidePanelsEnabledChanged();
|
void autoHidePanelsEnabledChanged();
|
||||||
void gamingModeEnabledChanged();
|
void gamingModeEnabledChanged();
|
||||||
void gamingDismissHintEnabledChanged();
|
void gamingDismissHintEnabledChanged();
|
||||||
|
void dynamicTilingEnabledChanged();
|
||||||
void allowLogoutChanged();
|
void allowLogoutChanged();
|
||||||
void lockscreenLeftButtonActionChanged();
|
void lockscreenLeftButtonActionChanged();
|
||||||
void lockscreenRightButtonActionChanged();
|
void lockscreenRightButtonActionChanged();
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,15 @@
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool isLegacyNextWallpaperPath(const QString &path)
|
||||||
|
{
|
||||||
|
return path == QStringLiteral("Next") || path.startsWith(QStringLiteral("/usr/share/wallpapers/Next/"))
|
||||||
|
|| path.startsWith(QStringLiteral("file:///usr/share/wallpapers/Next/"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WallpaperPlugin::WallpaperPlugin(QObject *parent)
|
WallpaperPlugin::WallpaperPlugin(QObject *parent)
|
||||||
: QObject{parent}
|
: QObject{parent}
|
||||||
, m_homescreenConfig{new QQmlPropertyMap{this}}
|
, m_homescreenConfig{new QQmlPropertyMap{this}}
|
||||||
|
|
@ -183,6 +192,11 @@ QCoro::Task<void> WallpaperPlugin::setHomescreenWallpaper(const QString &path)
|
||||||
qWarning() << "Failed to set wallpaper for screen" << screen << ":" << reply.error();
|
qWarning() << "Failed to set wallpaper for screen" << screen << ":" << reply.error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep lockscreen wallpaper aligned with homescreen wallpaper selections.
|
||||||
|
if (!path.isEmpty()) {
|
||||||
|
setLockscreenWallpaper(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WallpaperPlugin::setLockscreenWallpaper(const QString &path)
|
void WallpaperPlugin::setLockscreenWallpaper(const QString &path)
|
||||||
|
|
@ -256,6 +270,14 @@ QCoro::Task<void> WallpaperPlugin::loadHomescreenSettings()
|
||||||
// parse image configuration
|
// parse image configuration
|
||||||
if (m_homescreenWallpaperPlugin == QStringLiteral("org.kde.image")) {
|
if (m_homescreenWallpaperPlugin == QStringLiteral("org.kde.image")) {
|
||||||
m_homescreenWallpaperPath = map["Image"].toString();
|
m_homescreenWallpaperPath = map["Image"].toString();
|
||||||
|
|
||||||
|
// One-time migration for stale lockscreen wallpaper defaults.
|
||||||
|
const bool lockscreenUnset = m_lockscreenWallpaperPlugin.isEmpty() || m_lockscreenWallpaperPath.isEmpty();
|
||||||
|
const bool lockscreenLegacyNext =
|
||||||
|
m_lockscreenWallpaperPlugin == QStringLiteral("org.kde.image") && isLegacyNextWallpaperPath(m_lockscreenWallpaperPath);
|
||||||
|
if (!m_homescreenWallpaperPath.isEmpty() && m_lockscreenWallpaperPath != m_homescreenWallpaperPath && (lockscreenUnset || lockscreenLegacyNext)) {
|
||||||
|
setLockscreenWallpaper(m_homescreenWallpaperPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Q_EMIT homescreenConfigurationChanged();
|
Q_EMIT homescreenConfigurationChanged();
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ plasma_add_applet(org.kde.plasma.mobile.homescreen.folio
|
||||||
qml/main.qml
|
qml/main.qml
|
||||||
qml/PipeWireThumbnail.qml
|
qml/PipeWireThumbnail.qml
|
||||||
qml/PlaceholderDelegate.qml
|
qml/PlaceholderDelegate.qml
|
||||||
|
qml/RunningAppsPanel.qml
|
||||||
|
qml/RunningAppsPanelButton.qml
|
||||||
qml/WidgetDragItem.qml
|
qml/WidgetDragItem.qml
|
||||||
qml/config.qml
|
qml/config.qml
|
||||||
CPP_SOURCES
|
CPP_SOURCES
|
||||||
|
|
@ -95,6 +97,7 @@ target_link_libraries(org.kde.plasma.mobile.homescreen.folio PRIVATE
|
||||||
KF6::Service
|
KF6::Service
|
||||||
KF6::KIOGui
|
KF6::KIOGui
|
||||||
KF6::Notifications
|
KF6::Notifications
|
||||||
|
PW::LibTaskManager
|
||||||
Plasma::KWaylandClient
|
Plasma::KWaylandClient
|
||||||
KF6::WindowSystem
|
KF6::WindowSystem
|
||||||
KF6::JobWidgets
|
KF6::JobWidgets
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
#include "homescreen.h"
|
#include "homescreen.h"
|
||||||
|
|
||||||
|
#include <virtualdesktopinfo.h>
|
||||||
|
|
||||||
#include <KWindowSystem>
|
#include <KWindowSystem>
|
||||||
|
|
||||||
#include <QDBusConnection>
|
#include <QDBusConnection>
|
||||||
|
|
@ -96,4 +98,23 @@ void HomeScreen::triggerOverview() const
|
||||||
QDBusConnection::sessionBus().send(message);
|
QDBusConnection::sessionBus().send(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HomeScreen::activateVirtualDesktop(const QVariant &desktop) const
|
||||||
|
{
|
||||||
|
if (!desktop.isValid() || desktop.toString().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskManager::VirtualDesktopInfo virtualDesktopInfo;
|
||||||
|
virtualDesktopInfo.requestActivate(desktop);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeScreen::emptyTrash() const
|
||||||
|
{
|
||||||
|
QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kio.trash"),
|
||||||
|
QStringLiteral("/trash"),
|
||||||
|
QStringLiteral("org.kde.KIO.Trash"),
|
||||||
|
QStringLiteral("emptyTrash"));
|
||||||
|
QDBusConnection::sessionBus().send(message);
|
||||||
|
}
|
||||||
|
|
||||||
#include "homescreen.moc"
|
#include "homescreen.moc"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
#include <Plasma/Containment>
|
#include <Plasma/Containment>
|
||||||
#include <QSortFilterProxyModel>
|
#include <QSortFilterProxyModel>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
#include "applicationlistmodel.h"
|
#include "applicationlistmodel.h"
|
||||||
#include "delegatetoucharea.h"
|
#include "delegatetoucharea.h"
|
||||||
|
|
@ -33,6 +34,7 @@ class HomeScreen : public Plasma::Containment
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
QML_UNCREATABLE("HomeScreen is provided by the folio containment")
|
||||||
|
|
||||||
Q_PROPERTY(FolioSettings *FolioSettings READ folioSettings CONSTANT)
|
Q_PROPERTY(FolioSettings *FolioSettings READ folioSettings CONSTANT)
|
||||||
Q_PROPERTY(HomeScreenState *HomeScreenState READ homeScreenState CONSTANT)
|
Q_PROPERTY(HomeScreenState *HomeScreenState READ homeScreenState CONSTANT)
|
||||||
|
|
@ -49,6 +51,8 @@ public:
|
||||||
void configChanged() override;
|
void configChanged() override;
|
||||||
|
|
||||||
Q_INVOKABLE void triggerOverview() const;
|
Q_INVOKABLE void triggerOverview() const;
|
||||||
|
Q_INVOKABLE void activateVirtualDesktop(const QVariant &desktop) const;
|
||||||
|
Q_INVOKABLE void emptyTrash() const;
|
||||||
|
|
||||||
FolioSettings *folioSettings();
|
FolioSettings *folioSettings();
|
||||||
HomeScreenState *homeScreenState();
|
HomeScreenState *homeScreenState();
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Window 2.12
|
import QtQuick.Window 2.12
|
||||||
import QtQuick.Layouts 1.1
|
import QtQuick.Layouts 1.1
|
||||||
|
import QtCore
|
||||||
|
import Qt.labs.folderlistmodel
|
||||||
|
|
||||||
import org.kde.plasma.components 3.0 as PC3
|
import org.kde.plasma.components 3.0 as PC3
|
||||||
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
||||||
|
|
@ -24,12 +26,14 @@ MouseArea {
|
||||||
property MobileShell.MaskManager maskManager
|
property MobileShell.MaskManager maskManager
|
||||||
|
|
||||||
property var homeScreen
|
property var homeScreen
|
||||||
|
property bool suppressRunningTasks: false
|
||||||
|
|
||||||
signal delegateDragRequested(var item)
|
signal delegateDragRequested(var item)
|
||||||
|
|
||||||
// Convergence mode: show running apps alongside favourites
|
// Convergence mode: show running apps alongside favourites
|
||||||
readonly property bool convergenceMode: ShellSettings.Settings.convergenceModeEnabled
|
readonly property bool convergenceMode: ShellSettings.Settings.convergenceModeEnabled
|
||||||
readonly property int totalItemCount: repeater.count + (convergenceMode ? taskRepeater.count : 0)
|
readonly property bool showRunningTasks: convergenceMode && !suppressRunningTasks
|
||||||
|
readonly property int totalItemCount: repeater.count + (showRunningTasks ? taskRepeater.count : 0)
|
||||||
|
|
||||||
// In convergence mode, size icons to fit the dock bar instead of using page grid cells
|
// In convergence mode, size icons to fit the dock bar instead of using page grid cells
|
||||||
readonly property real dockCellWidth: convergenceMode ? root.height : folio.HomeScreenState.pageCellWidth
|
readonly property real dockCellWidth: convergenceMode ? root.height : folio.HomeScreenState.pageCellWidth
|
||||||
|
|
@ -37,6 +41,21 @@ MouseArea {
|
||||||
|
|
||||||
// Navigation buttons width (used to offset center positioning)
|
// Navigation buttons width (used to offset center positioning)
|
||||||
readonly property real navButtonWidth: convergenceMode ? root.height : 0
|
readonly property real navButtonWidth: convergenceMode ? root.height : 0
|
||||||
|
readonly property real dockItemInset: convergenceMode ? Math.max(2, Kirigami.Units.smallSpacing / 2) : 0
|
||||||
|
readonly property real dockIconSize: Math.min(root.height * 0.56, Kirigami.Units.iconSizes.large)
|
||||||
|
|
||||||
|
function dockItemColor(pressed, hovered, active) {
|
||||||
|
if (pressed) {
|
||||||
|
return Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.18)
|
||||||
|
}
|
||||||
|
if (active) {
|
||||||
|
return Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, hovered ? 0.18 : 0.12)
|
||||||
|
}
|
||||||
|
if (hovered) {
|
||||||
|
return Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08)
|
||||||
|
}
|
||||||
|
return "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
// Center x for dock items (offset between nav buttons in convergence mode)
|
// Center x for dock items (offset between nav buttons in convergence mode)
|
||||||
readonly property real dockCenterX: convergenceMode
|
readonly property real dockCenterX: convergenceMode
|
||||||
|
|
@ -44,7 +63,7 @@ MouseArea {
|
||||||
: root.width / 2
|
: root.width / 2
|
||||||
|
|
||||||
// Visible spacer between pinned favourites and running tasks
|
// Visible spacer between pinned favourites and running tasks
|
||||||
readonly property bool showSpacer: convergenceMode && repeater.count > 0 && taskRepeater.count > 0
|
readonly property bool showSpacer: showRunningTasks && repeater.count > 0 && taskRepeater.count > 0
|
||||||
property real spacerWidth: showSpacer ? Kirigami.Units.largeSpacing * 2 : 0
|
property real spacerWidth: showSpacer ? Kirigami.Units.largeSpacing * 2 : 0
|
||||||
Behavior on spacerWidth {
|
Behavior on spacerWidth {
|
||||||
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad }
|
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad }
|
||||||
|
|
@ -69,6 +88,39 @@ MouseArea {
|
||||||
property string taskPinStorageId: ""
|
property string taskPinStorageId: ""
|
||||||
readonly property bool taskPinCanDrop: taskPinTargetIndex !== -1 && taskPinStorageId !== ""
|
readonly property bool taskPinCanDrop: taskPinTargetIndex !== -1 && taskPinStorageId !== ""
|
||||||
|
|
||||||
|
// Virtual desktop pager (convergence mode, 2+ desktops)
|
||||||
|
readonly property bool showPager: convergenceMode && virtualDesktopInfo.numberOfDesktops > 1
|
||||||
|
readonly property real pagerButtonWidth: showPager ? Math.min(root.height, Kirigami.Units.gridUnit * 2.5) : 0
|
||||||
|
readonly property int pagerLeftCount: showPager ? Math.ceil(virtualDesktopInfo.numberOfDesktops / 2) : 0
|
||||||
|
readonly property int pagerRightCount: showPager ? virtualDesktopInfo.numberOfDesktops - pagerLeftCount : 0
|
||||||
|
readonly property real trashButtonWidth: convergenceMode ? root.height : 0
|
||||||
|
|
||||||
|
function pagerDesktopName(index) {
|
||||||
|
let names = virtualDesktopInfo.desktopNames
|
||||||
|
if (names && index < names.length && String(names[index]).length > 0)
|
||||||
|
return String(names[index])
|
||||||
|
return i18n("Desktop %1", index + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the desktop ID of the pager button under screen-space x, or ""
|
||||||
|
function pagerButtonDesktopAt(x) {
|
||||||
|
if (!showPager) return ""
|
||||||
|
let ids = virtualDesktopInfo.desktopIds
|
||||||
|
for (let i = 0; i < pagerLeftCount; ++i) {
|
||||||
|
let bx = navButtonWidth + i * pagerButtonWidth
|
||||||
|
if (x >= bx && x < bx + pagerButtonWidth)
|
||||||
|
return (ids && i < ids.length) ? String(ids[i]) : ""
|
||||||
|
}
|
||||||
|
for (let i = 0; i < pagerRightCount; ++i) {
|
||||||
|
let bx = root.width - navButtonWidth - root.trashButtonWidth - (pagerRightCount - i) * pagerButtonWidth
|
||||||
|
if (x >= bx && x < bx + pagerButtonWidth) {
|
||||||
|
let di = pagerLeftCount + i
|
||||||
|
return (ids && di < ids.length) ? String(ids[di]) : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
function runningTaskStorageId(taskModel) {
|
function runningTaskStorageId(taskModel) {
|
||||||
var id = taskModel ? taskModel.AppId || "" : ""
|
var id = taskModel ? taskModel.AppId || "" : ""
|
||||||
if (id && !id.endsWith(".desktop"))
|
if (id && !id.endsWith(".desktop"))
|
||||||
|
|
@ -130,10 +182,7 @@ MouseArea {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
width: root.navButtonWidth
|
width: root.navButtonWidth
|
||||||
color: homeMouseArea.containsPress
|
color: "transparent"
|
||||||
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
|
|
||||||
: (homeMouseArea.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) : "transparent")
|
|
||||||
radius: Kirigami.Units.cornerRadius
|
|
||||||
|
|
||||||
Accessible.role: Accessible.Button
|
Accessible.role: Accessible.Button
|
||||||
Accessible.name: i18n("Home")
|
Accessible.name: i18n("Home")
|
||||||
|
|
@ -155,9 +204,20 @@ MouseArea {
|
||||||
visible: homeButton.activeFocus
|
visible: homeButton.activeFocus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: root.dockItemInset
|
||||||
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
color: root.dockItemColor(homeMouseArea.containsPress, homeMouseArea.containsMouse, false)
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Kirigami.Icon {
|
Kirigami.Icon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: Math.min(parent.width, parent.height) * 0.75
|
width: root.dockIconSize
|
||||||
height: width
|
height: width
|
||||||
source: "start-here-shift"
|
source: "start-here-shift"
|
||||||
active: homeMouseArea.containsMouse
|
active: homeMouseArea.containsMouse
|
||||||
|
|
@ -181,10 +241,7 @@ MouseArea {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
width: root.navButtonWidth
|
width: root.navButtonWidth
|
||||||
color: overviewMouseArea.containsPress
|
color: "transparent"
|
||||||
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
|
|
||||||
: (overviewMouseArea.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) : "transparent")
|
|
||||||
radius: Kirigami.Units.cornerRadius
|
|
||||||
|
|
||||||
Accessible.role: Accessible.Button
|
Accessible.role: Accessible.Button
|
||||||
Accessible.name: i18n("Overview")
|
Accessible.name: i18n("Overview")
|
||||||
|
|
@ -206,9 +263,20 @@ MouseArea {
|
||||||
visible: overviewButton.activeFocus
|
visible: overviewButton.activeFocus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: root.dockItemInset
|
||||||
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
color: root.dockItemColor(overviewMouseArea.containsPress, overviewMouseArea.containsMouse, false)
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Kirigami.Icon {
|
Kirigami.Icon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: Math.min(parent.width, parent.height) * 0.75
|
width: root.dockIconSize
|
||||||
height: width
|
height: width
|
||||||
source: "view-grid-symbolic"
|
source: "view-grid-symbolic"
|
||||||
active: overviewMouseArea.containsMouse
|
active: overviewMouseArea.containsMouse
|
||||||
|
|
@ -223,6 +291,246 @@ MouseArea {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Virtual desktop pager: left wing (desktops 1 .. ceil(N/2)) ----
|
||||||
|
Repeater {
|
||||||
|
id: leftPagerRepeater
|
||||||
|
model: root.pagerLeftCount
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
id: leftDesktopBtn
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
readonly property string desktopId: {
|
||||||
|
let ids = virtualDesktopInfo.desktopIds
|
||||||
|
return (ids && index < ids.length) ? String(ids[index]) : ""
|
||||||
|
}
|
||||||
|
readonly property bool isCurrent: desktopId !== "" && String(desktopId) === String(virtualDesktopInfo.currentDesktop)
|
||||||
|
readonly property bool isDragTarget: {
|
||||||
|
if (root.taskPinDragIndex < 0) return false
|
||||||
|
let cx = root.taskBaseX(root.taskPinDragIndex) + root.dockCellWidth / 2 + root.taskPinDragOffset
|
||||||
|
return root.pagerButtonDesktopAt(cx) === desktopId
|
||||||
|
}
|
||||||
|
|
||||||
|
x: root.navButtonWidth + index * root.pagerButtonWidth
|
||||||
|
y: 0
|
||||||
|
width: root.pagerButtonWidth
|
||||||
|
height: root.height
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: root.dockItemInset
|
||||||
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
color: leftDesktopBtn.isCurrent || leftDesktopBtn.isDragTarget
|
||||||
|
? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b,
|
||||||
|
leftPagerHover.containsMouse || leftDesktopBtn.isDragTarget ? 0.25 : 0.18)
|
||||||
|
: root.dockItemColor(leftPagerHover.containsPress, leftPagerHover.containsMouse, false)
|
||||||
|
Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } }
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (leftDesktopBtn.index + 1).toString()
|
||||||
|
color: leftDesktopBtn.isCurrent ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor
|
||||||
|
font.pixelSize: Math.round(parent.height * 0.3)
|
||||||
|
font.bold: leftDesktopBtn.isCurrent
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.ToolTip {
|
||||||
|
visible: leftPagerHover.containsMouse
|
||||||
|
text: root.pagerDesktopName(leftDesktopBtn.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: leftPagerHover
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (leftDesktopBtn.desktopId)
|
||||||
|
root.folio.activateVirtualDesktop(leftDesktopBtn.desktopId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Virtual desktop pager: right wing (desktops ceil(N/2)+1 .. N) ----
|
||||||
|
Repeater {
|
||||||
|
id: rightPagerRepeater
|
||||||
|
model: root.pagerRightCount
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
id: rightDesktopBtn
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
readonly property int desktopIndex: root.pagerLeftCount + index
|
||||||
|
readonly property string desktopId: {
|
||||||
|
let ids = virtualDesktopInfo.desktopIds
|
||||||
|
return (ids && desktopIndex < ids.length) ? String(ids[desktopIndex]) : ""
|
||||||
|
}
|
||||||
|
readonly property bool isCurrent: desktopId !== "" && String(desktopId) === String(virtualDesktopInfo.currentDesktop)
|
||||||
|
readonly property bool isDragTarget: {
|
||||||
|
if (root.taskPinDragIndex < 0) return false
|
||||||
|
let cx = root.taskBaseX(root.taskPinDragIndex) + root.dockCellWidth / 2 + root.taskPinDragOffset
|
||||||
|
return root.pagerButtonDesktopAt(cx) === desktopId
|
||||||
|
}
|
||||||
|
|
||||||
|
x: root.width - root.navButtonWidth - root.trashButtonWidth - (root.pagerRightCount - index) * root.pagerButtonWidth
|
||||||
|
y: 0
|
||||||
|
width: root.pagerButtonWidth
|
||||||
|
height: root.height
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: root.dockItemInset
|
||||||
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
color: rightDesktopBtn.isCurrent || rightDesktopBtn.isDragTarget
|
||||||
|
? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b,
|
||||||
|
rightPagerHover.containsMouse || rightDesktopBtn.isDragTarget ? 0.25 : 0.18)
|
||||||
|
: root.dockItemColor(rightPagerHover.containsPress, rightPagerHover.containsMouse, false)
|
||||||
|
Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } }
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (rightDesktopBtn.desktopIndex + 1).toString()
|
||||||
|
color: rightDesktopBtn.isCurrent ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor
|
||||||
|
font.pixelSize: Math.round(parent.height * 0.3)
|
||||||
|
font.bold: rightDesktopBtn.isCurrent
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.ToolTip {
|
||||||
|
visible: rightPagerHover.containsMouse
|
||||||
|
text: root.pagerDesktopName(rightDesktopBtn.desktopIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: rightPagerHover
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (rightDesktopBtn.desktopId)
|
||||||
|
root.folio.activateVirtualDesktop(rightDesktopBtn.desktopId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Trash button (convergence mode, sits between the right pager wing and the Overview button) ----
|
||||||
|
|
||||||
|
// Watches ~/.local/share/Trash/files to detect empty/full state.
|
||||||
|
// FolderListModel reacts to directory changes automatically.
|
||||||
|
FolderListModel {
|
||||||
|
id: trashFilesModel
|
||||||
|
folder: StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share/Trash/files"
|
||||||
|
showFiles: true
|
||||||
|
showDirs: true
|
||||||
|
showDotAndDotDot: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirmation dialog for "Empty Trash" — parented to the homescreen so it
|
||||||
|
// is sized correctly and floats above all dock content.
|
||||||
|
Loader {
|
||||||
|
id: emptyTrashDialogLoader
|
||||||
|
parent: root.homeScreen
|
||||||
|
anchors.fill: parent
|
||||||
|
active: false
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
active = true;
|
||||||
|
item.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceComponent: Kirigami.PromptDialog {
|
||||||
|
title: i18n("Empty Trash")
|
||||||
|
subtitle: i18n("Permanently delete all items in the trash? This action cannot be undone.")
|
||||||
|
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.Cancel
|
||||||
|
onAccepted: root.folio.emptyTrash()
|
||||||
|
onClosed: emptyTrashDialogLoader.active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: trashButton
|
||||||
|
visible: root.convergenceMode
|
||||||
|
activeFocusOnTab: root.convergenceMode
|
||||||
|
x: root.width - root.navButtonWidth - root.trashButtonWidth
|
||||||
|
y: 0
|
||||||
|
width: root.trashButtonWidth
|
||||||
|
height: root.height
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Accessible.role: Accessible.Button
|
||||||
|
Accessible.name: i18n("Trash")
|
||||||
|
Accessible.onPressAction: Qt.openUrlExternally("trash:/")
|
||||||
|
|
||||||
|
Keys.onReturnPressed: Qt.openUrlExternally("trash:/")
|
||||||
|
Keys.onEnterPressed: Qt.openUrlExternally("trash:/")
|
||||||
|
Keys.onSpacePressed: Qt.openUrlExternally("trash:/")
|
||||||
|
|
||||||
|
KeyboardHighlight {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: trashButton.activeFocus
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: root.dockItemInset
|
||||||
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
color: root.dockItemColor(trashMouseArea.containsPress, trashMouseArea.containsMouse, false)
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: root.dockIconSize
|
||||||
|
height: width
|
||||||
|
source: trashFilesModel.count > 0 ? "user-trash-full" : "user-trash"
|
||||||
|
active: trashMouseArea.containsMouse
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.ToolTip {
|
||||||
|
visible: trashMouseArea.containsMouse
|
||||||
|
text: trashFilesModel.count > 0
|
||||||
|
? i18np("Trash — 1 item", "Trash — %1 items", trashFilesModel.count)
|
||||||
|
: i18n("Trash")
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: trashMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
trashContextMenu.open()
|
||||||
|
} else {
|
||||||
|
Qt.openUrlExternally("trash:/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Menu {
|
||||||
|
id: trashContextMenu
|
||||||
|
popupType: T.Popup.Window
|
||||||
|
|
||||||
|
PC3.MenuItem {
|
||||||
|
icon.name: "folder-open"
|
||||||
|
text: i18n("Open Trash")
|
||||||
|
onTriggered: Qt.openUrlExternally("trash:/")
|
||||||
|
}
|
||||||
|
PC3.MenuItem {
|
||||||
|
icon.name: "trash-empty"
|
||||||
|
text: i18n("Empty Trash")
|
||||||
|
enabled: trashFilesModel.count > 0
|
||||||
|
onTriggered: emptyTrashDialogLoader.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TaskManager.VirtualDesktopInfo {
|
TaskManager.VirtualDesktopInfo {
|
||||||
id: virtualDesktopInfo
|
id: virtualDesktopInfo
|
||||||
}
|
}
|
||||||
|
|
@ -251,9 +559,12 @@ MouseArea {
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: (mouse) => {
|
onClicked: (mouse) => {
|
||||||
// Right-click opens settings view (wallpaper/widgets), same as long-press
|
|
||||||
if (mouse.button === Qt.RightButton) {
|
if (mouse.button === Qt.RightButton) {
|
||||||
folio.HomeScreenState.openSettingsView();
|
if (convergenceMode) {
|
||||||
|
root.homeScreen.showDesktopContextMenu();
|
||||||
|
} else {
|
||||||
|
folio.HomeScreenState.openSettingsView();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -435,7 +746,7 @@ MouseArea {
|
||||||
maskManager: root.maskManager
|
maskManager: root.maskManager
|
||||||
application: delegate.delegateModel.application
|
application: delegate.delegateModel.application
|
||||||
name: folio.FolioSettings.showFavouritesAppLabels ? delegate.delegateModel.application.name : ""
|
name: folio.FolioSettings.showFavouritesAppLabels ? delegate.delegateModel.application.name : ""
|
||||||
shadow: true
|
shadow: !root.convergenceMode
|
||||||
|
|
||||||
turnToFolder: delegate.isAppHoveredOver
|
turnToFolder: delegate.isAppHoveredOver
|
||||||
turnToFolderAnimEnabled: folio.HomeScreenState.isDraggingDelegate
|
turnToFolderAnimEnabled: folio.HomeScreenState.isDraggingDelegate
|
||||||
|
|
@ -540,7 +851,7 @@ MouseArea {
|
||||||
id: appFolderDelegate
|
id: appFolderDelegate
|
||||||
folio: root.folio
|
folio: root.folio
|
||||||
maskManager: root.maskManager
|
maskManager: root.maskManager
|
||||||
shadow: true
|
shadow: !root.convergenceMode
|
||||||
folder: delegate.delegateModel.folder
|
folder: delegate.delegateModel.folder
|
||||||
name: folio.FolioSettings.showFavouritesAppLabels ? delegate.delegateModel.folder.name : ""
|
name: folio.FolioSettings.showFavouritesAppLabels ? delegate.delegateModel.folder.name : ""
|
||||||
|
|
||||||
|
|
@ -705,21 +1016,21 @@ MouseArea {
|
||||||
// Position above the hovered dock icon, in global coordinates
|
// Position above the hovered dock icon, in global coordinates
|
||||||
x: {
|
x: {
|
||||||
if (!targetDelegate) return 0
|
if (!targetDelegate) return 0
|
||||||
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
|
var win = root.Window.window
|
||||||
var win = targetDelegate.Window.window
|
|
||||||
var screenLeft = win && win.screen ? win.screen.virtualX : 0
|
var screenLeft = win && win.screen ? win.screen.virtualX : 0
|
||||||
var screenRight = screenLeft + (win && win.screen ? win.screen.width : Screen.width)
|
var screenRight = screenLeft + (win && win.screen ? win.screen.width : Screen.width)
|
||||||
var centered = delegateGlobal.x + (targetDelegate.width - width) / 2
|
// The dock window is full-width, anchored to the screen's left edge.
|
||||||
return Math.max(screenLeft, Math.min(screenRight - width, centered))
|
// targetDelegate.x is dock-local, so the global center of the icon is:
|
||||||
|
var globalCenter = screenLeft + targetDelegate.x + targetDelegate.width / 2
|
||||||
|
return Math.max(screenLeft, Math.min(screenRight - width, globalCenter - width / 2))
|
||||||
}
|
}
|
||||||
y: {
|
y: {
|
||||||
if (!targetDelegate) return 0
|
var win = root.Window.window
|
||||||
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
|
|
||||||
var win = targetDelegate.Window.window
|
|
||||||
var screenTop = win && win.screen ? win.screen.virtualY : 0
|
var screenTop = win && win.screen ? win.screen.virtualY : 0
|
||||||
var screenBottom = screenTop + (win && win.screen ? win.screen.height : Screen.height)
|
var screenBottom = screenTop + (win && win.screen ? win.screen.height : Screen.height)
|
||||||
var above = delegateGlobal.y - height - Kirigami.Units.smallSpacing
|
// Dock is bottom-anchored; its top edge is at screenBottom - dock window height.
|
||||||
return Math.max(screenTop, Math.min(screenBottom - height, above))
|
var dockTop = screenBottom - (win ? win.height : root.height)
|
||||||
|
return Math.max(screenTop, dockTop - height - Kirigami.Units.smallSpacing)
|
||||||
}
|
}
|
||||||
|
|
||||||
onShowingChanged: {
|
onShowingChanged: {
|
||||||
|
|
@ -885,11 +1196,11 @@ MouseArea {
|
||||||
id: dockSpacer
|
id: dockSpacer
|
||||||
visible: root.showSpacer
|
visible: root.showSpacer
|
||||||
x: (repeater.count - root.totalItemCount / 2) * root.dockCellWidth + root.dockCenterX - width / 2
|
x: (repeater.count - root.totalItemCount / 2) * root.dockCellWidth + root.dockCenterX - width / 2
|
||||||
y: parent.height * 0.2
|
y: parent.height * 0.28
|
||||||
width: Math.round(Kirigami.Units.devicePixelRatio)
|
width: Math.round(Kirigami.Units.devicePixelRatio)
|
||||||
height: parent.height * 0.6
|
height: parent.height * 0.44
|
||||||
color: Kirigami.Theme.textColor
|
color: Kirigami.Theme.textColor
|
||||||
opacity: 0.4
|
opacity: 0.22
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaceholderDelegate {
|
PlaceholderDelegate {
|
||||||
|
|
@ -905,7 +1216,7 @@ MouseArea {
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
id: taskRepeater
|
id: taskRepeater
|
||||||
model: root.convergenceMode ? tasksModel : null
|
model: root.showRunningTasks ? tasksModel : null
|
||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
id: taskDelegate
|
id: taskDelegate
|
||||||
|
|
@ -976,10 +1287,13 @@ MouseArea {
|
||||||
// Hover highlight background
|
// Hover highlight background
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
anchors.margins: root.dockItemInset
|
||||||
radius: Kirigami.Units.cornerRadius
|
radius: Kirigami.Units.cornerRadius
|
||||||
color: taskMouseArea.containsPress
|
color: root.dockItemColor(taskMouseArea.containsPress, taskMouseArea.containsMouse, taskDelegate.model.IsActive === true)
|
||||||
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
|
|
||||||
: (taskMouseArea.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) : "transparent")
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyboardHighlight {
|
KeyboardHighlight {
|
||||||
|
|
@ -990,7 +1304,7 @@ MouseArea {
|
||||||
// Task icon
|
// Task icon
|
||||||
Kirigami.Icon {
|
Kirigami.Icon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: Math.min(parent.width, parent.height) * 0.6
|
width: root.dockIconSize
|
||||||
height: width
|
height: width
|
||||||
source: taskDelegate.model.decoration
|
source: taskDelegate.model.decoration
|
||||||
active: taskMouseArea.containsMouse
|
active: taskMouseArea.containsMouse
|
||||||
|
|
@ -1001,7 +1315,9 @@ MouseArea {
|
||||||
target: null
|
target: null
|
||||||
xAxis.enabled: true
|
xAxis.enabled: true
|
||||||
yAxis.enabled: false
|
yAxis.enabled: false
|
||||||
enabled: root.convergenceMode && taskDelegate.isLocationBottom && !folio.FolioSettings.lockLayout && taskDelegate.taskStorageId !== "" && !folio.FavouritesModel.containsApplication(taskDelegate.taskStorageId)
|
// Enable for unpinned tasks (pin-to-dock drag) and for ALL tasks
|
||||||
|
// when the pager is showing so windows can be dragged to a desktop button.
|
||||||
|
enabled: root.convergenceMode && taskDelegate.isLocationBottom && !folio.FolioSettings.lockLayout && taskDelegate.taskStorageId !== "" && (root.showPager || !folio.FavouritesModel.containsApplication(taskDelegate.taskStorageId))
|
||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (active) {
|
if (active) {
|
||||||
|
|
@ -1014,7 +1330,12 @@ MouseArea {
|
||||||
root.taskPinTargetIndex = -1
|
root.taskPinTargetIndex = -1
|
||||||
root.taskPinStorageId = taskDelegate.taskStorageId
|
root.taskPinStorageId = taskDelegate.taskStorageId
|
||||||
} else if (root.taskPinDragIndex === taskDelegate.index) {
|
} else if (root.taskPinDragIndex === taskDelegate.index) {
|
||||||
if (root.taskPinCanDrop) {
|
// If released over a pager button, move the window to that desktop.
|
||||||
|
let finalCenterX = root.taskBaseX(taskDelegate.index) + root.dockCellWidth / 2 + root.taskPinDragOffset
|
||||||
|
let pagerDesktop = root.pagerButtonDesktopAt(finalCenterX)
|
||||||
|
if (pagerDesktop && taskDelegate.model.IsVirtualDesktopsChangeable === true) {
|
||||||
|
tasksModel.requestVirtualDesktops(tasksModel.makeModelIndex(taskDelegate.index), [pagerDesktop])
|
||||||
|
} else if (root.taskPinCanDrop && !folio.FavouritesModel.containsApplication(root.taskPinStorageId)) {
|
||||||
folio.FavouritesModel.addApplicationAt(root.taskPinTargetIndex, root.taskPinStorageId)
|
folio.FavouritesModel.addApplicationAt(root.taskPinTargetIndex, root.taskPinStorageId)
|
||||||
}
|
}
|
||||||
root.clearTaskPinDrag()
|
root.clearTaskPinDrag()
|
||||||
|
|
@ -1039,7 +1360,7 @@ MouseArea {
|
||||||
Row {
|
Row {
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.bottomMargin: Kirigami.Units.smallSpacing / 2
|
anchors.bottomMargin: Kirigami.Units.smallSpacing
|
||||||
spacing: Kirigami.Units.smallSpacing / 2
|
spacing: Kirigami.Units.smallSpacing / 2
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
|
|
@ -1049,11 +1370,15 @@ MouseArea {
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: Kirigami.Units.smallSpacing * 1.5
|
width: taskDelegate.model.IsActive === true ? Kirigami.Units.smallSpacing * 3 : Kirigami.Units.smallSpacing * 1.5
|
||||||
height: width
|
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio))
|
||||||
radius: width / 2
|
radius: height / 2
|
||||||
color: Kirigami.Theme.highlightColor
|
color: Kirigami.Theme.highlightColor
|
||||||
opacity: taskDelegate.model.IsActive === true ? 1.0 : 0.4
|
opacity: taskDelegate.model.IsActive === true ? 1.0 : 0.45
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1132,6 +1457,25 @@ MouseArea {
|
||||||
}
|
}
|
||||||
onClicked: tasksModel.requestClose(tasksModel.makeModelIndex(taskDelegate.index))
|
onClicked: tasksModel.requestClose(tasksModel.makeModelIndex(taskDelegate.index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Controls.MenuSeparator {
|
||||||
|
visible: root.showPager && taskDelegate.model.IsVirtualDesktopsChangeable === true
|
||||||
|
}
|
||||||
|
|
||||||
|
Instantiator {
|
||||||
|
model: root.showPager && taskDelegate.model.IsVirtualDesktopsChangeable === true
|
||||||
|
? virtualDesktopInfo.desktopIds : []
|
||||||
|
delegate: PC3.MenuItem {
|
||||||
|
required property int index
|
||||||
|
required property var modelData
|
||||||
|
text: i18n("Move to %1", root.pagerDesktopName(index))
|
||||||
|
enabled: String(modelData) !== String(virtualDesktopInfo.currentDesktop)
|
||||||
|
onTriggered: tasksModel.requestVirtualDesktops(
|
||||||
|
tasksModel.makeModelIndex(taskDelegate.index), [modelData])
|
||||||
|
}
|
||||||
|
onObjectAdded: (idx, obj) => taskContextMenu.insertItem(taskContextMenu.count, obj)
|
||||||
|
onObjectRemoved: (idx, obj) => taskContextMenu.removeItem(obj)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import org.kde.plasma.plasmoid 2.0
|
||||||
import org.kde.plasma.components 3.0 as PC3
|
import org.kde.plasma.components 3.0 as PC3
|
||||||
import org.kde.plasma.private.mobileshell as MobileShell
|
import org.kde.plasma.private.mobileshell as MobileShell
|
||||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||||
|
import QtQuick.Templates as T
|
||||||
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
|
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
|
||||||
|
|
||||||
import "./delegate"
|
import "./delegate"
|
||||||
|
|
@ -78,6 +79,35 @@ Item {
|
||||||
Plasmoid.internalAction("configure").trigger();
|
Plasmoid.internalAction("configure").trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showDesktopContextMenu() {
|
||||||
|
desktopContextMenu.popup();
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Menu {
|
||||||
|
id: desktopContextMenu
|
||||||
|
popupType: T.Popup.Window
|
||||||
|
|
||||||
|
PC3.MenuItem {
|
||||||
|
text: i18n("Desktop and Wallpaper...")
|
||||||
|
icon.name: "preferences-desktop-wallpaper"
|
||||||
|
onTriggered: root.wallpaperSelectorTriggered()
|
||||||
|
}
|
||||||
|
PC3.MenuItem {
|
||||||
|
text: i18n("Add Widgets...")
|
||||||
|
icon.name: "widget-alternatives"
|
||||||
|
onTriggered: {
|
||||||
|
folio.HomeScreenState.openSettingsView();
|
||||||
|
settings.openWidgetsBrowser();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PC3.MenuSeparator {}
|
||||||
|
PC3.MenuItem {
|
||||||
|
text: i18n("Configure Desktop...")
|
||||||
|
icon.name: "settings-configure"
|
||||||
|
onTriggered: root.openConfigure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onPressed: (event) => {
|
Keys.onPressed: (event) => {
|
||||||
// The root is focused when we aren't in key navigation mode
|
// The root is focused when we aren't in key navigation mode
|
||||||
// Begin key navigation when arrow keys are pressed
|
// Begin key navigation when arrow keys are pressed
|
||||||
|
|
@ -352,8 +382,8 @@ Item {
|
||||||
visible: opacity > 0 && !ShellSettings.Settings.convergenceModeEnabled
|
visible: opacity > 0 && !ShellSettings.Settings.convergenceModeEnabled
|
||||||
|
|
||||||
// one is ignored as anchors are set
|
// one is ignored as anchors are set
|
||||||
height: ShellSettings.Settings.convergenceModeEnabled ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 6
|
height: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6
|
||||||
width: ShellSettings.Settings.convergenceModeEnabled ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 6
|
width: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6
|
||||||
|
|
||||||
anchors.topMargin: root.topMargin
|
anchors.topMargin: root.topMargin
|
||||||
anchors.bottomMargin: ShellSettings.Settings.convergenceModeEnabled ? 0 : root.bottomMargin
|
anchors.bottomMargin: ShellSettings.Settings.convergenceModeEnabled ? 0 : root.bottomMargin
|
||||||
|
|
@ -409,7 +439,7 @@ Item {
|
||||||
}
|
}
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: favouritesBar
|
target: favouritesBar
|
||||||
height: ShellSettings.Settings.convergenceModeEnabled ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 6
|
height: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6
|
||||||
}
|
}
|
||||||
}, State {
|
}, State {
|
||||||
name: "left"
|
name: "left"
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import QtQuick.Layouts
|
||||||
|
|
||||||
import org.kde.plasma.components 3.0 as PC3
|
import org.kde.plasma.components 3.0 as PC3
|
||||||
import org.kde.plasma.private.mobileshell as MobileShell
|
import org.kde.plasma.private.mobileshell as MobileShell
|
||||||
|
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
|
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
|
||||||
|
|
||||||
|
|
@ -28,9 +29,12 @@ MouseArea {
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: (mouse) => {
|
onClicked: (mouse) => {
|
||||||
// Right-click opens settings view (wallpaper/widgets), same as long-press
|
|
||||||
if (mouse.button === Qt.RightButton) {
|
if (mouse.button === Qt.RightButton) {
|
||||||
folio.HomeScreenState.openSettingsView();
|
if (ShellSettings.Settings.convergenceModeEnabled) {
|
||||||
|
root.homeScreen.showDesktopContextMenu();
|
||||||
|
} else {
|
||||||
|
folio.HomeScreenState.openSettingsView();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
715
containments/homescreens/folio/qml/RunningAppsPanel.qml
Normal file
715
containments/homescreens/folio/qml/RunningAppsPanel.qml
Normal file
|
|
@ -0,0 +1,715 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.plasma.components 3.0 as PC3
|
||||||
|
import org.kde.taskmanager as TaskManager
|
||||||
|
|
||||||
|
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var folio
|
||||||
|
|
||||||
|
readonly property bool hasTasks: allTasksModel.count > 0
|
||||||
|
property bool sortByName: false
|
||||||
|
property int dragTargetDesktopIndex: -1
|
||||||
|
property string pendingMoveTaskKey: ""
|
||||||
|
property string pendingMoveTargetName: ""
|
||||||
|
|
||||||
|
signal taskActivated()
|
||||||
|
|
||||||
|
function taskStorageId(taskModel) {
|
||||||
|
var id = taskModel ? taskModel.AppId || "" : ""
|
||||||
|
if (id && !id.endsWith(".desktop")) {
|
||||||
|
id += ".desktop"
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
function taskKey(taskModel) {
|
||||||
|
const winIds = taskModel && taskModel.WinIdList ? taskModel.WinIdList : []
|
||||||
|
if (winIds.length > 0) {
|
||||||
|
var key = ""
|
||||||
|
for (var i = 0; i < winIds.length; ++i) {
|
||||||
|
key += String(winIds[i]) + "|"
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(taskModel ? taskModel.AppId || "" : "") + "|" + String(taskModel ? taskModel.display || "" : "")
|
||||||
|
}
|
||||||
|
|
||||||
|
function markTaskMove(taskKey, desktopIndex) {
|
||||||
|
pendingMoveTaskKey = taskKey
|
||||||
|
pendingMoveTargetName = desktopName(desktopIndex)
|
||||||
|
pendingMoveResetTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
function mixColor(base, overlay, ratio) {
|
||||||
|
return Qt.rgba(
|
||||||
|
base.r + (overlay.r - base.r) * ratio,
|
||||||
|
base.g + (overlay.g - base.g) * ratio,
|
||||||
|
base.b + (overlay.b - base.b) * ratio,
|
||||||
|
base.a + (overlay.a - base.a) * ratio)
|
||||||
|
}
|
||||||
|
|
||||||
|
function desktopName(index) {
|
||||||
|
const names = virtualDesktopInfo.desktopNames
|
||||||
|
if (names && names.length > index && String(names[index]).length > 0) {
|
||||||
|
return String(names[index])
|
||||||
|
}
|
||||||
|
return "Desktop " + (index + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCurrentDesktop(desktopId) {
|
||||||
|
return String(desktopId) === String(virtualDesktopInfo.currentDesktop)
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: pendingMoveResetTimer
|
||||||
|
interval: 1200
|
||||||
|
onTriggered: {
|
||||||
|
root.pendingMoveTaskKey = ""
|
||||||
|
root.pendingMoveTargetName = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskManager.VirtualDesktopInfo { id: virtualDesktopInfo }
|
||||||
|
TaskManager.ActivityInfo { id: activityInfo }
|
||||||
|
|
||||||
|
TaskManager.TasksModel {
|
||||||
|
id: allTasksModel
|
||||||
|
filterByVirtualDesktop: false
|
||||||
|
filterByActivity: true
|
||||||
|
filterNotMaximized: false
|
||||||
|
filterByScreen: true
|
||||||
|
filterHidden: false
|
||||||
|
activity: activityInfo.currentActivity
|
||||||
|
groupMode: TaskManager.TasksModel.GroupApplications
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskManager.TasksModel {
|
||||||
|
id: tasksModel
|
||||||
|
filterByVirtualDesktop: true
|
||||||
|
filterByActivity: true
|
||||||
|
filterNotMaximized: false
|
||||||
|
filterByScreen: true
|
||||||
|
filterHidden: false
|
||||||
|
virtualDesktop: virtualDesktopInfo.currentDesktop
|
||||||
|
activity: activityInfo.currentActivity
|
||||||
|
groupMode: TaskManager.TasksModel.GroupApplications
|
||||||
|
sortMode: root.sortByName ? TaskManager.TasksModel.SortAlpha : TaskManager.TasksModel.SortLastActivated
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: panelShadow
|
||||||
|
anchors.fill: panelBackground
|
||||||
|
anchors.topMargin: 2
|
||||||
|
radius: panelBackground.radius
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.35)
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: panelBackground
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
color: Kirigami.Theme.backgroundColor
|
||||||
|
border.width: 1
|
||||||
|
border.pixelAligned: false
|
||||||
|
border.color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.14)
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Kirigami.Units.smallSpacing
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: i18n("Running")
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: [
|
||||||
|
{ label: i18n("Recent"), byName: false },
|
||||||
|
{ label: i18n("Name"), byName: true }
|
||||||
|
]
|
||||||
|
|
||||||
|
delegate: MouseArea {
|
||||||
|
id: sortButton
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
readonly property bool checked: root.sortByName === modelData.byName
|
||||||
|
|
||||||
|
width: Math.max(Kirigami.Units.gridUnit * 3.5, label.implicitWidth + Kirigami.Units.smallSpacing * 3)
|
||||||
|
height: Kirigami.Units.gridUnit * 1.6
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.sortByName = modelData.byName
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
color: sortButton.checked
|
||||||
|
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, sortButton.containsMouse ? 0.28 : 0.2)
|
||||||
|
: sortButton.containsMouse
|
||||||
|
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.08)
|
||||||
|
: "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
id: label
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: sortButton.modelData.label
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85
|
||||||
|
color: sortButton.checked ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: desktopStrip
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: virtualDesktopInfo.numberOfDesktops > 1
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
text: i18n("Desktops")
|
||||||
|
opacity: 0.7
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: desktopDropSurface
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.4
|
||||||
|
|
||||||
|
function desktopIndexAt(x) {
|
||||||
|
if (virtualDesktopInfo.numberOfDesktops <= 0) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const localX = desktopRow.mapFromItem(desktopDropSurface, x, 0).x
|
||||||
|
var nearestIndex = -1
|
||||||
|
var nearestDistance = Number.MAX_VALUE
|
||||||
|
for (var i = 0; i < virtualDesktopInfo.numberOfDesktops; ++i) {
|
||||||
|
const item = desktopRepeater.itemAt(i)
|
||||||
|
if (!item) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localX >= item.x && localX <= item.x + item.width) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
const center = item.x + item.width / 2
|
||||||
|
const distance = Math.abs(localX - center)
|
||||||
|
if (distance < nearestDistance) {
|
||||||
|
nearestIndex = i
|
||||||
|
nearestDistance = distance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nearestIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: desktopRow
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: desktopRepeater
|
||||||
|
|
||||||
|
model: virtualDesktopInfo.desktopIds
|
||||||
|
|
||||||
|
delegate: MouseArea {
|
||||||
|
id: desktopButton
|
||||||
|
|
||||||
|
required property int index
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
readonly property bool checked: root.isCurrentDesktop(modelData)
|
||||||
|
readonly property string desktopLabel: root.desktopName(index)
|
||||||
|
readonly property bool dragHovered: desktopDropArea.containsDrag && root.dragTargetDesktopIndex === index
|
||||||
|
|
||||||
|
width: Math.max(Kirigami.Units.gridUnit * 5.5, (desktopRow.width / Math.max(1, virtualDesktopInfo.numberOfDesktops)) - Kirigami.Units.smallSpacing)
|
||||||
|
height: desktopRow.height
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: root.folio.activateVirtualDesktop(modelData)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
scale: desktopButton.dragHovered ? 1.03 : 1
|
||||||
|
color: desktopButton.checked
|
||||||
|
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, desktopButton.containsMouse || desktopButton.dragHovered ? 0.32 : 0.24)
|
||||||
|
: desktopButton.dragHovered
|
||||||
|
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.18)
|
||||||
|
: desktopButton.containsMouse
|
||||||
|
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.08)
|
||||||
|
: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.045)
|
||||||
|
border.width: 1
|
||||||
|
border.pixelAligned: false
|
||||||
|
border.color: desktopButton.checked || desktopButton.dragHovered
|
||||||
|
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.55)
|
||||||
|
: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.14)
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Kirigami.Units.smallSpacing / 2
|
||||||
|
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio))
|
||||||
|
radius: height / 2
|
||||||
|
visible: desktopButton.checked
|
||||||
|
color: Kirigami.Theme.highlightColor
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Kirigami.Units.smallSpacing * 2
|
||||||
|
text: desktopButton.desktopLabel
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
font.weight: desktopButton.checked || desktopButton.dragHovered ? Font.Medium : Font.Normal
|
||||||
|
font.pixelSize: Math.min(Kirigami.Theme.defaultFont.pixelSize, parent.height * 0.42)
|
||||||
|
color: desktopButton.checked || desktopButton.dragHovered ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DropArea {
|
||||||
|
id: desktopDropArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
keys: ["folio-running-task"]
|
||||||
|
|
||||||
|
onEntered: (drag) => {
|
||||||
|
root.dragTargetDesktopIndex = desktopDropSurface.desktopIndexAt(drag.x)
|
||||||
|
drag.accept(Qt.MoveAction)
|
||||||
|
}
|
||||||
|
onPositionChanged: (drag) => {
|
||||||
|
root.dragTargetDesktopIndex = desktopDropSurface.desktopIndexAt(drag.x)
|
||||||
|
drag.accept(Qt.MoveAction)
|
||||||
|
}
|
||||||
|
onExited: root.dragTargetDesktopIndex = -1
|
||||||
|
onDropped: (drop) => {
|
||||||
|
const desktopIndex = desktopDropSurface.desktopIndexAt(drop.x)
|
||||||
|
const desktopId = desktopIndex >= 0 ? virtualDesktopInfo.desktopIds[desktopIndex] : ""
|
||||||
|
if (!drop.source || !drop.source.moveToDesktop || String(desktopId).length === 0) {
|
||||||
|
root.dragTargetDesktopIndex = -1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
drop.source.moveToDesktop(desktopId, desktopIndex)
|
||||||
|
root.dragTargetDesktopIndex = -1
|
||||||
|
drop.accept(Qt.MoveAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GridView {
|
||||||
|
id: taskGrid
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
clip: true
|
||||||
|
model: tasksModel
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
interactive: contentHeight > height
|
||||||
|
|
||||||
|
readonly property int columns: Math.max(1, Math.floor(width / (Kirigami.Units.gridUnit * 14)))
|
||||||
|
cellWidth: Math.floor(width / columns)
|
||||||
|
cellHeight: Kirigami.Units.gridUnit * 10
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
id: taskCard
|
||||||
|
|
||||||
|
required property int index
|
||||||
|
required property var model
|
||||||
|
|
||||||
|
width: taskGrid.cellWidth - Kirigami.Units.smallSpacing
|
||||||
|
height: taskGrid.cellHeight - Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
readonly property var modelIndex: tasksModel.makeModelIndex(index)
|
||||||
|
readonly property var winIds: model.WinIdList ? model.WinIdList : []
|
||||||
|
readonly property int previewCount: Math.max(1, Math.min(2, winIds.length))
|
||||||
|
readonly property bool activeTask: model.IsActive === true
|
||||||
|
readonly property bool minimizedTask: model.IsMinimized === true
|
||||||
|
readonly property bool maximizedTask: model.IsMaximized === true
|
||||||
|
readonly property bool groupTask: model.IsGroupParent === true
|
||||||
|
readonly property bool desktopsChangeable: model.IsVirtualDesktopsChangeable === true
|
||||||
|
readonly property string storageId: root.taskStorageId(model)
|
||||||
|
readonly property string taskKey: root.taskKey(model)
|
||||||
|
readonly property bool pinned: storageId !== "" && root.folio.FavouritesModel.containsApplication(storageId)
|
||||||
|
readonly property bool pendingMove: root.pendingMoveTaskKey === taskKey
|
||||||
|
|
||||||
|
function taskIndexForPreview(previewIndex) {
|
||||||
|
return taskCard.groupTask
|
||||||
|
? tasksModel.makeModelIndex(taskCard.index, previewIndex)
|
||||||
|
: taskCard.modelIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
function titleForPreview(previewIndex) {
|
||||||
|
if (!taskCard.groupTask) {
|
||||||
|
return taskCard.model.display || ""
|
||||||
|
}
|
||||||
|
return tasksModel.data(tasksModel.makeModelIndex(taskCard.index, previewIndex), 0) || taskCard.model.display || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function activate(previewIndex) {
|
||||||
|
tasksModel.requestActivate(taskIndexForPreview(previewIndex || 0))
|
||||||
|
root.taskActivated()
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveToDesktop(desktopId, desktopIndex) {
|
||||||
|
if (!taskCard.desktopsChangeable || String(desktopId).length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
root.markTaskMove(taskCard.taskKey, desktopIndex)
|
||||||
|
tasksModel.requestVirtualDesktops(taskCard.modelIndex, [desktopId])
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: dragProxy
|
||||||
|
|
||||||
|
parent: root
|
||||||
|
width: taskCard.width
|
||||||
|
height: taskCard.height
|
||||||
|
z: 1000
|
||||||
|
visible: cardArea.drag.active
|
||||||
|
opacity: 0.9
|
||||||
|
|
||||||
|
Drag.active: cardArea.drag.active
|
||||||
|
Drag.hotSpot.x: cardArea.pressX
|
||||||
|
Drag.hotSpot.y: cardArea.pressY
|
||||||
|
Drag.keys: ["folio-running-task"]
|
||||||
|
Drag.proposedAction: Qt.MoveAction
|
||||||
|
Drag.source: taskCard
|
||||||
|
Drag.supportedActions: Qt.MoveAction
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.2)
|
||||||
|
border.width: 1
|
||||||
|
border.pixelAligned: false
|
||||||
|
border.color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.6)
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Kirigami.Units.smallSpacing
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
Layout.preferredHeight: Layout.preferredWidth
|
||||||
|
source: taskCard.model.decoration
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: taskCard.model.display || ""
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: cardArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: taskCard.desktopsChangeable ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
||||||
|
enabled: !taskCard.pendingMove
|
||||||
|
property real pressX: width / 2
|
||||||
|
property real pressY: height / 2
|
||||||
|
property bool wasDragged: false
|
||||||
|
drag.target: taskCard.desktopsChangeable ? dragProxy : undefined
|
||||||
|
drag.threshold: Math.max(4, Kirigami.Units.smallSpacing)
|
||||||
|
drag.smoothed: false
|
||||||
|
|
||||||
|
onPressed: (mouse) => {
|
||||||
|
wasDragged = false
|
||||||
|
pressX = mouse.x
|
||||||
|
pressY = mouse.y
|
||||||
|
const pos = taskCard.mapToItem(root, 0, 0)
|
||||||
|
dragProxy.x = pos.x
|
||||||
|
dragProxy.y = pos.y
|
||||||
|
}
|
||||||
|
|
||||||
|
onPositionChanged: {
|
||||||
|
if (drag.active) {
|
||||||
|
wasDragged = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onReleased: {
|
||||||
|
if (wasDragged) {
|
||||||
|
dragProxy.Drag.drop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (!wasDragged) {
|
||||||
|
taskCard.activate(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
color: taskCard.activeTask
|
||||||
|
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, cardArea.containsMouse ? 0.18 : 0.12)
|
||||||
|
: cardArea.containsMouse
|
||||||
|
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.08)
|
||||||
|
: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.04)
|
||||||
|
border.width: 1
|
||||||
|
border.pixelAligned: false
|
||||||
|
border.color: taskCard.activeTask
|
||||||
|
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.5)
|
||||||
|
: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.12)
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Kirigami.Units.smallSpacing
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: previewRow
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Kirigami.Units.gridUnit * 5
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: taskCard.previewCount
|
||||||
|
|
||||||
|
delegate: MouseArea {
|
||||||
|
id: previewArea
|
||||||
|
|
||||||
|
required property int index
|
||||||
|
readonly property string childUuid: taskCard.winIds.length > index ? taskCard.winIds[index] : ""
|
||||||
|
|
||||||
|
width: (previewRow.width - previewRow.spacing * (taskCard.previewCount - 1)) / taskCard.previewCount
|
||||||
|
height: previewRow.height
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: taskCard.activate(index)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, previewArea.containsMouse ? 0.1 : 0.06)
|
||||||
|
border.width: 1
|
||||||
|
border.pixelAligned: false
|
||||||
|
border.color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.14)
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: thumbnailLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 1
|
||||||
|
active: previewArea.childUuid !== "" && root.visible
|
||||||
|
sourceComponent: PipeWireThumbnail {
|
||||||
|
windowUuid: previewArea.childUuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: Kirigami.Units.iconSizes.large
|
||||||
|
height: width
|
||||||
|
source: taskCard.model.decoration
|
||||||
|
visible: !thumbnailLoader.item || !thumbnailLoader.item.hasThumbnail
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: titleLabel.implicitHeight + Kirigami.Units.smallSpacing
|
||||||
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.48)
|
||||||
|
visible: taskCard.previewCount > 1
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
id: titleLabel
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.margins: Kirigami.Units.smallSpacing
|
||||||
|
text: taskCard.titleForPreview(previewArea.index)
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
Layout.preferredHeight: Layout.preferredWidth
|
||||||
|
source: taskCard.model.decoration
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: taskCard.model.display || ""
|
||||||
|
font.weight: taskCard.activeTask ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
text: taskCard.activeTask ? i18n("Active") : taskCard.minimizedTask ? i18n("Minimized") : i18n("Open")
|
||||||
|
opacity: 0.65
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
visible: taskCard.maximizedTask
|
||||||
|
text: i18n("Maximized")
|
||||||
|
opacity: 0.65
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
visible: taskCard.winIds.length > 1
|
||||||
|
text: i18np("%1 window", "%1 windows", taskCard.winIds.length)
|
||||||
|
opacity: 0.65
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
RunningAppsPanelButton {
|
||||||
|
iconName: taskCard.pinned ? "emblem-favorite" : "window-pin"
|
||||||
|
toolTipText: taskCard.pinned ? i18n("Pinned") : i18n("Pin to Dock")
|
||||||
|
checked: taskCard.pinned
|
||||||
|
enabled: taskCard.storageId !== "" && !taskCard.pinned && !root.folio.FolioSettings.lockLayout
|
||||||
|
onTriggered: root.folio.FavouritesModel.addApplication(taskCard.storageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
RunningAppsPanelButton {
|
||||||
|
iconName: taskCard.minimizedTask ? "window-restore" : "window-minimize"
|
||||||
|
toolTipText: taskCard.minimizedTask ? i18n("Restore") : i18n("Minimize")
|
||||||
|
onTriggered: tasksModel.requestToggleMinimized(taskCard.modelIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
RunningAppsPanelButton {
|
||||||
|
iconName: taskCard.maximizedTask ? "window-restore" : "window-maximize"
|
||||||
|
toolTipText: taskCard.maximizedTask ? i18n("Restore") : i18n("Maximize")
|
||||||
|
enabled: !taskCard.groupTask
|
||||||
|
onTriggered: tasksModel.requestToggleMaximized(taskCard.modelIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
RunningAppsPanelButton {
|
||||||
|
iconName: "window-close"
|
||||||
|
toolTipText: taskCard.winIds.length > 1 ? i18n("Close All") : i18n("Close")
|
||||||
|
onTriggered: tasksModel.requestClose(taskCard.modelIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
visible: taskCard.pendingMove
|
||||||
|
color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.18)
|
||||||
|
border.width: 1
|
||||||
|
border.pixelAligned: false
|
||||||
|
border.color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.55)
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Kirigami.Units.gridUnit
|
||||||
|
text: i18n("Moving to %1", root.pendingMoveTargetName)
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Kirigami.Theme.highlightColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.ScrollBar.vertical: PC3.ScrollBar {
|
||||||
|
interactive: true
|
||||||
|
enabled: taskGrid.contentHeight > taskGrid.height
|
||||||
|
implicitWidth: Kirigami.Units.smallSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Kirigami.Units.gridUnit * 2
|
||||||
|
visible: taskGrid.count === 0
|
||||||
|
text: i18n("No windows on this desktop")
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
opacity: 0.65
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.plasma.components 3.0 as PC3
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: button
|
||||||
|
|
||||||
|
property string iconName
|
||||||
|
property string toolTipText
|
||||||
|
property bool checked: false
|
||||||
|
|
||||||
|
signal triggered()
|
||||||
|
|
||||||
|
function _mix(base, overlay, ratio) {
|
||||||
|
return Qt.rgba(
|
||||||
|
base.r + (overlay.r - base.r) * ratio,
|
||||||
|
base.g + (overlay.g - base.g) * ratio,
|
||||||
|
base.b + (overlay.b - base.b) * ratio,
|
||||||
|
base.a + (overlay.a - base.a) * ratio)
|
||||||
|
}
|
||||||
|
|
||||||
|
width: Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.smallSpacing * 2
|
||||||
|
height: width
|
||||||
|
hoverEnabled: enabled
|
||||||
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
opacity: enabled ? 1 : 0.35
|
||||||
|
|
||||||
|
onClicked: button.triggered()
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
color: button.containsPress
|
||||||
|
? button._mix(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.16)
|
||||||
|
: button.checked
|
||||||
|
? button._mix(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, button.containsMouse ? 0.22 : 0.16)
|
||||||
|
: button.containsMouse
|
||||||
|
? button._mix(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.08)
|
||||||
|
: "transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: Kirigami.Units.iconSizes.small
|
||||||
|
height: width
|
||||||
|
source: button.iconName
|
||||||
|
active: button.containsMouse || button.checked
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.ToolTip {
|
||||||
|
text: button.toolTipText
|
||||||
|
visible: button.containsMouse && button.toolTipText.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -422,8 +422,14 @@ Window {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cycle through source filter tabs.
|
// Cycle through source filter tabs (only includes installed launcher sources).
|
||||||
readonly property var _sourceFilters: ["", "steam", "desktop", "waydroid", "lutris", "heroic"]
|
readonly property var _sourceFilters: {
|
||||||
|
var filters = ["", "desktop", "waydroid"]
|
||||||
|
if (GamingShell.GameLauncherProvider.steamAvailable) filters.splice(1, 0, "steam")
|
||||||
|
if (GamingShell.GameLauncherProvider.lutrisAvailable) filters.push("lutris")
|
||||||
|
if (GamingShell.GameLauncherProvider.heroicAvailable) filters.push("heroic")
|
||||||
|
return filters
|
||||||
|
}
|
||||||
function cycleSourceFilter(direction) {
|
function cycleSourceFilter(direction) {
|
||||||
var current = _sourceFilters.indexOf(
|
var current = _sourceFilters.indexOf(
|
||||||
GamingShell.GameLauncherProvider.sourceFilter)
|
GamingShell.GameLauncherProvider.sourceFilter)
|
||||||
|
|
@ -749,35 +755,26 @@ Window {
|
||||||
id: sourceFilterBar
|
id: sourceFilterBar
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
QQC2.TabButton {
|
Repeater {
|
||||||
text: i18n("All")
|
model: {
|
||||||
width: implicitWidth
|
var tabs = [
|
||||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = ""
|
{label: i18n("All"), filter: ""},
|
||||||
}
|
{label: i18n("Desktop"), filter: "desktop"},
|
||||||
QQC2.TabButton {
|
{label: i18n("Waydroid"),filter: "waydroid"}
|
||||||
text: "Steam"
|
]
|
||||||
width: implicitWidth
|
if (GamingShell.GameLauncherProvider.steamAvailable)
|
||||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "steam"
|
tabs.splice(1, 0, {label: "Steam", filter: "steam"})
|
||||||
}
|
if (GamingShell.GameLauncherProvider.lutrisAvailable)
|
||||||
QQC2.TabButton {
|
tabs.push({label: "Lutris", filter: "lutris"})
|
||||||
text: i18n("Desktop")
|
if (GamingShell.GameLauncherProvider.heroicAvailable)
|
||||||
width: implicitWidth
|
tabs.push({label: "Heroic", filter: "heroic"})
|
||||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "desktop"
|
return tabs
|
||||||
}
|
}
|
||||||
QQC2.TabButton {
|
QQC2.TabButton {
|
||||||
text: i18n("Waydroid")
|
text: modelData.label
|
||||||
width: implicitWidth
|
width: implicitWidth
|
||||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "waydroid"
|
onClicked: GamingShell.GameLauncherProvider.sourceFilter = modelData.filter
|
||||||
}
|
}
|
||||||
QQC2.TabButton {
|
|
||||||
text: "Lutris"
|
|
||||||
width: implicitWidth
|
|
||||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "lutris"
|
|
||||||
}
|
|
||||||
QQC2.TabButton {
|
|
||||||
text: "Heroic"
|
|
||||||
width: implicitWidth
|
|
||||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "heroic"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import "./private"
|
||||||
|
|
||||||
ContainmentItem {
|
ContainmentItem {
|
||||||
id: root
|
id: root
|
||||||
property Folio.HomeScreen folio: root.plasmoid
|
property var folio: root.plasmoid
|
||||||
|
|
||||||
// Tracks whether the Game Center grid is visible within gaming mode.
|
// Tracks whether the Game Center grid is visible within gaming mode.
|
||||||
// If gaming mode is already enabled at startup, open it immediately so
|
// If gaming mode is already enabled at startup, open it immediately so
|
||||||
|
|
@ -279,7 +279,7 @@ ContainmentItem {
|
||||||
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
|
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
width: Screen.width
|
width: Screen.width
|
||||||
height: Kirigami.Units.gridUnit * 3
|
height: MobileShell.Constants.convergenceDockHeight
|
||||||
|
|
||||||
LayerShell.Window.scope: "dock-overlay"
|
LayerShell.Window.scope: "dock-overlay"
|
||||||
LayerShell.Window.layer: LayerShell.Window.LayerTop
|
LayerShell.Window.layer: LayerShell.Window.LayerTop
|
||||||
|
|
@ -290,11 +290,11 @@ ContainmentItem {
|
||||||
// Auto-hide: slide dock content off-screen when a window is
|
// Auto-hide: slide dock content off-screen when a window is
|
||||||
// maximized. The reveal strip at the screen edge brings it back.
|
// maximized. The reveal strip at the screen edge brings it back.
|
||||||
property real dockOffset: 0
|
property real dockOffset: 0
|
||||||
readonly property real dockHeight: Kirigami.Units.gridUnit * 3
|
readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight
|
||||||
|
|
||||||
// Height of the input-receive strip kept at the screen edge when
|
// Height of the input-receive strip kept at the screen edge when
|
||||||
// the dock is hidden. Matches the navigation panel convention.
|
// the dock is hidden. Matches the navigation panel convention.
|
||||||
readonly property real revealStripHeight: Kirigami.Units.gridUnit
|
readonly property real revealStripHeight: MobileShell.Constants.convergenceDockRevealHeight
|
||||||
|
|
||||||
// True once the hover-reveal timer fires; cleared on hover-exit.
|
// True once the hover-reveal timer fires; cleared on hover-exit.
|
||||||
property bool hoverRevealing: false
|
property bool hoverRevealing: false
|
||||||
|
|
@ -353,10 +353,10 @@ ContainmentItem {
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
visible: !dockOverlay.shouldHide || dockOverlay.dockOffset < dockOverlay.dockHeight
|
||||||
Kirigami.Theme.inherit: false
|
Kirigami.Theme.inherit: false
|
||||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||||
color: Kirigami.Theme.backgroundColor
|
color: Kirigami.Theme.backgroundColor
|
||||||
transform: Translate { y: dockOverlay.dockOffset }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FavouritesBar {
|
FavouritesBar {
|
||||||
|
|
@ -365,6 +365,7 @@ ContainmentItem {
|
||||||
folio: root.folio
|
folio: root.folio
|
||||||
maskManager: root.maskManager
|
maskManager: root.maskManager
|
||||||
homeScreen: folioHomeScreen
|
homeScreen: folioHomeScreen
|
||||||
|
suppressRunningTasks: runningAppsPanel.visible
|
||||||
transform: Translate { y: dockOverlay.dockOffset }
|
transform: Translate { y: dockOverlay.dockOffset }
|
||||||
// Dock is an opaque panel — use Window colorset so all content
|
// Dock is an opaque panel — use Window colorset so all content
|
||||||
// (labels, hover highlights, icon tints) follows the system theme
|
// (labels, hover highlights, icon tints) follows the system theme
|
||||||
|
|
@ -407,7 +408,7 @@ ContainmentItem {
|
||||||
|
|
||||||
readonly property real popupWidth: Math.min(Kirigami.Units.gridUnit * 28, parent.width * 0.5)
|
readonly property real popupWidth: Math.min(Kirigami.Units.gridUnit * 28, parent.width * 0.5)
|
||||||
readonly property real popupHeight: Math.min(Kirigami.Units.gridUnit * 32, parent.height * 0.7)
|
readonly property real popupHeight: Math.min(Kirigami.Units.gridUnit * 32, parent.height * 0.7)
|
||||||
readonly property real dockHeight: Kirigami.Units.gridUnit * 3
|
readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight
|
||||||
|
|
||||||
width: popupWidth
|
width: popupWidth
|
||||||
height: popupHeight
|
height: popupHeight
|
||||||
|
|
@ -519,7 +520,9 @@ ContainmentItem {
|
||||||
|
|
||||||
width: tileSize
|
width: tileSize
|
||||||
height: overlayDrawer.popupHeight
|
height: overlayDrawer.popupHeight
|
||||||
x: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing
|
x: runningAppsPanel.visible
|
||||||
|
? runningAppsPanel.x + runningAppsPanel.width + Kirigami.Units.smallSpacing
|
||||||
|
: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing
|
||||||
y: overlayDrawer.y
|
y: overlayDrawer.y
|
||||||
opacity: overlayDrawer.opacity
|
opacity: overlayDrawer.opacity
|
||||||
radius: Kirigami.Units.cornerRadius
|
radius: Kirigami.Units.cornerRadius
|
||||||
|
|
@ -739,6 +742,20 @@ ContainmentItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RunningAppsPanel {
|
||||||
|
id: runningAppsPanel
|
||||||
|
folio: root.folio
|
||||||
|
|
||||||
|
x: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing
|
||||||
|
y: overlayDrawer.y
|
||||||
|
width: Math.max(0, parent.width - x - powerPanel.width - Kirigami.Units.smallSpacing * 2)
|
||||||
|
height: overlayDrawer.popupHeight
|
||||||
|
opacity: overlayDrawer.opacity
|
||||||
|
visible: hasTasks && opacity > 0
|
||||||
|
|
||||||
|
onTaskActivated: folio.HomeScreenState.closeAppDrawer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Game Center overlay — full-screen grid of games shown when gaming mode
|
// Game Center overlay — full-screen grid of games shown when gaming mode
|
||||||
|
|
@ -907,6 +924,7 @@ ContainmentItem {
|
||||||
maskManager: root.frontMaskManager
|
maskManager: root.frontMaskManager
|
||||||
horizontal: root.width > root.height
|
horizontal: root.width > root.height
|
||||||
edge: horizontal ? Qt.LeftEdge : Qt.BottomEdge
|
edge: horizontal ? Qt.LeftEdge : Qt.BottomEdge
|
||||||
|
topMargin: horizontal ? folioHomeScreen.topMargin : 0
|
||||||
bottomMargin: horizontal ? 0 : folioHomeScreen.bottomMargin
|
bottomMargin: horizontal ? 0 : folioHomeScreen.bottomMargin
|
||||||
leftMargin: horizontal ? folioHomeScreen.leftMargin : 0
|
leftMargin: horizontal ? folioHomeScreen.leftMargin : 0
|
||||||
rightMargin: horizontal ? folioHomeScreen.rightMargin : 0
|
rightMargin: horizontal ? folioHomeScreen.rightMargin : 0
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@ Item {
|
||||||
|
|
||||||
readonly property bool homeScreenInteractive: !appletListViewer.active
|
readonly property bool homeScreenInteractive: !appletListViewer.active
|
||||||
|
|
||||||
|
function openWidgetsBrowser() {
|
||||||
|
appletListViewer.active = true;
|
||||||
|
}
|
||||||
|
|
||||||
property real bottomMargin: 0
|
property real bottomMargin: 0
|
||||||
property real leftMargin: 0
|
property real leftMargin: 0
|
||||||
property real rightMargin: 0
|
property real rightMargin: 0
|
||||||
|
|
|
||||||
|
|
@ -166,13 +166,13 @@ ContainmentItem {
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
|
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
|
||||||
// height is set by layer-shell anchoring; provide a fallback.
|
// height is set by layer-shell anchoring; provide a fallback.
|
||||||
height: Math.max(1, MobileShell.Constants.navigationPanelThickness)
|
height: MobileShell.Constants.convergenceDockHeight
|
||||||
width: 1 // layer-shell stretches it via AnchorLeft|AnchorRight
|
width: 1 // layer-shell stretches it via AnchorLeft|AnchorRight
|
||||||
|
|
||||||
LayerShell.Window.scope: "dock-space"
|
LayerShell.Window.scope: "dock-space"
|
||||||
LayerShell.Window.layer: LayerShell.Window.LayerBottom
|
LayerShell.Window.layer: LayerShell.Window.LayerBottom
|
||||||
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
|
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
|
||||||
LayerShell.Window.exclusionZone: Math.max(1, MobileShell.Constants.navigationPanelThickness)
|
LayerShell.Window.exclusionZone: MobileShell.Constants.convergenceDockHeight
|
||||||
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ const QMap<QString, QMap<QString, QVariant>> APPLICATIONS_BLACKLIST_DEFAULT_SETT
|
||||||
const QMap<QString, QMap<QString, QVariant>> KDEGLOBALS_DEFAULT_SETTINGS = {{"General", {{"BrowserApplication", "angelfish"}}}};
|
const QMap<QString, QMap<QString, QVariant>> KDEGLOBALS_DEFAULT_SETTINGS = {{"General", {{"BrowserApplication", "angelfish"}}}};
|
||||||
|
|
||||||
// .config/plasma-mobile/kdeglobals - immutable settings:
|
// .config/plasma-mobile/kdeglobals - immutable settings:
|
||||||
const QMap<QString, QMap<QString, QVariant>> KDEGLOBALS_SETTINGS = {{"KDE", {{"LookAndFeelPackage", "org.kde.breeze.mobile"}}}};
|
const QMap<QString, QMap<QString, QVariant>> KDEGLOBALS_SETTINGS = {{"KDE", {{"LookAndFeelPackage", "org.shift.mobile"}}}};
|
||||||
|
|
||||||
// .config/plasma-mobile/kwinrc - non-immutable settings:
|
// .config/plasma-mobile/kwinrc - non-immutable settings:
|
||||||
const QMap<QString, QMap<QString, QVariant>> KWINRC_DEFAULT_SETTINGS = {
|
const QMap<QString, QMap<QString, QVariant>> KWINRC_DEFAULT_SETTINGS = {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ kcoreaddons_add_plugin(kded_plasma_mobile_start SOURCES ${kded_plasma_mobile_sta
|
||||||
target_link_libraries(kded_plasma_mobile_start PRIVATE
|
target_link_libraries(kded_plasma_mobile_start PRIVATE
|
||||||
Qt::Core
|
Qt::Core
|
||||||
KF6::DBusAddons
|
KF6::DBusAddons
|
||||||
|
KF6::ConfigCore
|
||||||
KF6::KIOGui
|
KF6::KIOGui
|
||||||
KF6::JobWidgets
|
KF6::JobWidgets
|
||||||
KF6::Package
|
KF6::Package
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,77 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
|
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include <KConfigGroup>
|
||||||
#include <KIO/CommandLauncherJob>
|
#include <KIO/CommandLauncherJob>
|
||||||
#include <KNotificationJobUiDelegate>
|
#include <KNotificationJobUiDelegate>
|
||||||
#include <KPluginFactory>
|
#include <KPluginFactory>
|
||||||
|
#include <KSharedConfig>
|
||||||
|
|
||||||
#include "start.h"
|
#include "start.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool isLegacyNextWallpaperPath(const QString &path)
|
||||||
|
{
|
||||||
|
return path == QStringLiteral("Next") || path.startsWith(QStringLiteral("/usr/share/wallpapers/Next/"))
|
||||||
|
|| path.startsWith(QStringLiteral("file:///usr/share/wallpapers/Next/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString shiftWallpaperPackageUrl()
|
||||||
|
{
|
||||||
|
const QString metadataPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("wallpapers/SHIFT/metadata.json"));
|
||||||
|
if (metadataPath.isEmpty()) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString packageUrl = QUrl::fromLocalFile(QFileInfo(metadataPath).absolutePath()).toString();
|
||||||
|
if (!packageUrl.endsWith(QLatin1Char('/'))) {
|
||||||
|
packageUrl += QLatin1Char('/');
|
||||||
|
}
|
||||||
|
return packageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ensureLockscreenWallpaperDefaults()
|
||||||
|
{
|
||||||
|
auto config = KSharedConfig::openConfig(QStringLiteral("kscreenlockerrc"));
|
||||||
|
auto greeterGroup = config->group(QStringLiteral("Greeter"));
|
||||||
|
|
||||||
|
const QString wallpaperPlugin = greeterGroup.readEntry(QStringLiteral("WallpaperPlugin"), QString());
|
||||||
|
const QString wallpaperPath =
|
||||||
|
greeterGroup.group(QStringLiteral("Wallpaper")).group(wallpaperPlugin).group(QStringLiteral("General")).readEntry(QStringLiteral("Image"), QString());
|
||||||
|
|
||||||
|
const bool wallpaperUnset = wallpaperPlugin.isEmpty() || wallpaperPath.isEmpty();
|
||||||
|
const bool wallpaperLegacyNext = wallpaperPlugin == QStringLiteral("org.kde.image") && isLegacyNextWallpaperPath(wallpaperPath);
|
||||||
|
if (!wallpaperUnset && !wallpaperLegacyNext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString defaultWallpaperUrl = shiftWallpaperPackageUrl();
|
||||||
|
if (defaultWallpaperUrl.isEmpty()) {
|
||||||
|
qWarning() << "Could not locate SHIFT wallpaper package for lockscreen defaults";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
greeterGroup.group(QStringLiteral("Wallpaper"))
|
||||||
|
.group(QStringLiteral("org.kde.image"))
|
||||||
|
.group(QStringLiteral("General"))
|
||||||
|
.writeEntry(QStringLiteral("Image"), defaultWallpaperUrl, KConfigGroup::Notify);
|
||||||
|
greeterGroup.writeEntry(QStringLiteral("WallpaperPlugin"), QStringLiteral("org.kde.image"), KConfigGroup::Notify);
|
||||||
|
config->sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
K_PLUGIN_FACTORY_WITH_JSON(StartFactory, "kded_plasma_mobile_start.json", registerPlugin<Start>();)
|
K_PLUGIN_FACTORY_WITH_JSON(StartFactory, "kded_plasma_mobile_start.json", registerPlugin<Start>();)
|
||||||
|
|
||||||
Start::Start(QObject *parent, const QList<QVariant> &)
|
Start::Start(QObject *parent, const QList<QVariant> &)
|
||||||
: KDEDModule{parent}
|
: KDEDModule{parent}
|
||||||
{
|
{
|
||||||
|
ensureLockscreenWallpaperDefaults();
|
||||||
|
|
||||||
auto *envmanagerJob = new KIO::CommandLauncherJob(QStringLiteral("plasma-mobile-envmanager --apply-settings"), {});
|
auto *envmanagerJob = new KIO::CommandLauncherJob(QStringLiteral("plasma-mobile-envmanager --apply-settings"), {});
|
||||||
envmanagerJob->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled));
|
envmanagerJob->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled));
|
||||||
envmanagerJob->setDesktopName(QStringLiteral("org.kde.plasma-mobile-envmanager"));
|
envmanagerJob->setDesktopName(QStringLiteral("org.kde.plasma-mobile-envmanager"));
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,6 @@
|
||||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
|
||||||
add_subdirectory(scripts)
|
add_subdirectory(scripts)
|
||||||
|
add_subdirectory(effects)
|
||||||
|
add_subdirectory(decorations)
|
||||||
add_subdirectory(mobiletaskswitcher)
|
add_subdirectory(mobiletaskswitcher)
|
||||||
6
kwin/decorations/CMakeLists.txt
Normal file
6
kwin/decorations/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# SPDX-FileCopyrightText: 2026 Marco Allegretti <
|
||||||
|
# SPDX-License-Identifier: EUPL-1.2
|
||||||
|
install(
|
||||||
|
DIRECTORY org.shift.decoration
|
||||||
|
DESTINATION ${KDE_INSTALL_DATADIR}/kwin/decorations
|
||||||
|
)
|
||||||
211
kwin/decorations/org.shift.decoration/contents/ui/main.qml
Normal file
211
kwin/decorations/org.shift.decoration/contents/ui/main.qml
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import org.kde.kwin.decoration
|
||||||
|
|
||||||
|
Decoration {
|
||||||
|
id: root
|
||||||
|
alpha: true
|
||||||
|
|
||||||
|
// ── Palette ─────────────────────────────────────────────────────────────
|
||||||
|
readonly property color activeBar: "#1a1d2e"
|
||||||
|
readonly property color inactiveBar: "#141620"
|
||||||
|
readonly property color activeText: "#f0f0f8"
|
||||||
|
readonly property color inactiveText: "#505570"
|
||||||
|
|
||||||
|
readonly property int barHeight: 30
|
||||||
|
readonly property int btnSize: 16
|
||||||
|
readonly property int btnSpacing: 8
|
||||||
|
readonly property int btnSideMargin: 12
|
||||||
|
readonly property int cornerRadius: decoration.client.maximized ? 0 : 8
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
borders.top = barHeight;
|
||||||
|
borders.left = 0;
|
||||||
|
borders.right = 0;
|
||||||
|
borders.bottom = 0;
|
||||||
|
|
||||||
|
// Keep titlebar controls available for maximized windows in desktop
|
||||||
|
// convergence mode. Mobile mode uses noBorder=true and bypasses this.
|
||||||
|
maximizedBorders.top = barHeight;
|
||||||
|
maximizedBorders.left = 0;
|
||||||
|
maximizedBorders.right = 0;
|
||||||
|
maximizedBorders.bottom = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
DecorationOptions {
|
||||||
|
id: options
|
||||||
|
deco: decoration
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Faint window outline ─────────────────────────────────────────────────
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "transparent"
|
||||||
|
radius: root.cornerRadius
|
||||||
|
border.width: decoration.client.maximized ? 0 : 1
|
||||||
|
border.color: Qt.rgba(1, 1, 1, 0.08)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Title bar ────────────────────────────────────────────────────────────
|
||||||
|
Rectangle {
|
||||||
|
id: bar
|
||||||
|
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||||
|
height: root.barHeight
|
||||||
|
radius: root.cornerRadius
|
||||||
|
color: decoration.client.active ? root.activeBar : root.inactiveBar
|
||||||
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
|
|
||||||
|
// Square off bottom half — only top corners are rounded
|
||||||
|
Rectangle {
|
||||||
|
anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
|
||||||
|
height: root.cornerRadius
|
||||||
|
color: parent.color
|
||||||
|
visible: !decoration.client.maximized
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Title row ────────────────────────────────────────────────────────
|
||||||
|
Item {
|
||||||
|
id: titleRow
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: leftRow
|
||||||
|
spacing: root.btnSpacing
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
leftMargin: root.btnSideMargin
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
Repeater {
|
||||||
|
model: options.titleButtonsLeft
|
||||||
|
delegate: ShiftButton { btnType: modelData }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors {
|
||||||
|
left: leftRow.right; leftMargin: 6
|
||||||
|
right: rightRow.left; rightMargin: 6
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
text: decoration.client.caption
|
||||||
|
color: decoration.client.active ? root.activeText : root.inactiveText
|
||||||
|
font: options.titleFont
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: rightRow
|
||||||
|
spacing: root.btnSpacing
|
||||||
|
anchors {
|
||||||
|
right: parent.right
|
||||||
|
rightMargin: root.btnSideMargin
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
Repeater {
|
||||||
|
model: options.titleButtonsRight
|
||||||
|
delegate: ShiftButton { btnType: modelData }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: decoration.installTitleItem(titleRow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Button component ─────────────────────────────────────────────────────
|
||||||
|
component ShiftButton: DecorationButton {
|
||||||
|
property int btnType: DecorationOptions.DecorationButtonNone
|
||||||
|
readonly property bool isSpacer: btnType === DecorationOptions.DecorationButtonExplicitSpacer
|
||||||
|
readonly property bool supported: {
|
||||||
|
switch (btnType) {
|
||||||
|
case DecorationOptions.DecorationButtonExplicitSpacer:
|
||||||
|
case DecorationOptions.DecorationButtonClose:
|
||||||
|
case DecorationOptions.DecorationButtonMinimize:
|
||||||
|
case DecorationOptions.DecorationButtonMaximizeRestore:
|
||||||
|
case DecorationOptions.DecorationButtonMenu:
|
||||||
|
case DecorationOptions.DecorationButtonApplicationMenu:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buttonType: btnType
|
||||||
|
width: isSpacer ? root.btnSpacing * 2 : (supported ? root.btnSize : 0)
|
||||||
|
height: isSpacer ? 1 : (supported ? root.btnSize : 0)
|
||||||
|
visible: supported
|
||||||
|
|
||||||
|
readonly property color normalColor: {
|
||||||
|
switch (btnType) {
|
||||||
|
case DecorationOptions.DecorationButtonClose: return "#C4455D";
|
||||||
|
case DecorationOptions.DecorationButtonMenu:
|
||||||
|
case DecorationOptions.DecorationButtonApplicationMenu:
|
||||||
|
case DecorationOptions.DecorationButtonMinimize:
|
||||||
|
case DecorationOptions.DecorationButtonMaximizeRestore: return "#2b3246";
|
||||||
|
default: return "#2b3246";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readonly property color hoverColor: {
|
||||||
|
switch (btnType) {
|
||||||
|
case DecorationOptions.DecorationButtonClose: return "#E05D76";
|
||||||
|
case DecorationOptions.DecorationButtonMinimize:
|
||||||
|
case DecorationOptions.DecorationButtonMaximizeRestore:
|
||||||
|
case DecorationOptions.DecorationButtonMenu:
|
||||||
|
case DecorationOptions.DecorationButtonApplicationMenu: return "#3b435c";
|
||||||
|
default: return "#3b435c";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readonly property color symbolColor: {
|
||||||
|
switch (btnType) {
|
||||||
|
case DecorationOptions.DecorationButtonClose: return "#ffffff";
|
||||||
|
case DecorationOptions.DecorationButtonMenu:
|
||||||
|
case DecorationOptions.DecorationButtonApplicationMenu:
|
||||||
|
case DecorationOptions.DecorationButtonMinimize:
|
||||||
|
case DecorationOptions.DecorationButtonMaximizeRestore: return "#eaf2ff";
|
||||||
|
default: return "#eaf2ff";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readonly property string symbol: {
|
||||||
|
switch (btnType) {
|
||||||
|
case DecorationOptions.DecorationButtonClose: return "\u00d7";
|
||||||
|
case DecorationOptions.DecorationButtonMinimize: return "\u2212";
|
||||||
|
case DecorationOptions.DecorationButtonMaximizeRestore:
|
||||||
|
return decoration.client.maximized ? "\u25a3" : "\u25a1";
|
||||||
|
case DecorationOptions.DecorationButtonMenu:
|
||||||
|
case DecorationOptions.DecorationButtonApplicationMenu: return "\u2261";
|
||||||
|
default: return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap-layout hover activation lives in the SHIFT Snap Assist effect.
|
||||||
|
// The decoration only renders the maximize button; the effect decides
|
||||||
|
// when maximize-hover is eligible and shows the popup.
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: !isSpacer
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
antialiasing: true
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(1, 1, 1, 0.18)
|
||||||
|
color: parent.pressed ? Qt.darker(parent.hoverColor, 1.3)
|
||||||
|
: parent.hovered ? parent.hoverColor
|
||||||
|
: parent.normalColor
|
||||||
|
Behavior on color { ColorAnimation { duration: 100 } }
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: parent.parent.symbol
|
||||||
|
color: parent.parent.symbolColor
|
||||||
|
font.pixelSize: Math.round(parent.width * 0.66)
|
||||||
|
font.weight: Font.Bold
|
||||||
|
opacity: 1.0
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 100 } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
kwin/decorations/org.shift.decoration/metadata.json
Normal file
14
kwin/decorations/org.shift.decoration/metadata.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"KPackageStructure": "KWin/Decoration",
|
||||||
|
"KPlugin": {
|
||||||
|
"Authors": [
|
||||||
|
{
|
||||||
|
"Name": "SHIFT Contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Description": "Shift window decoration",
|
||||||
|
"Id": "org.shift.decoration",
|
||||||
|
"License": "GPL-2.0-or-later",
|
||||||
|
"Name": "Shift"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
kwin/effects/CMakeLists.txt
Normal file
10
kwin/effects/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
# SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
function(add_kwin_effect name source)
|
||||||
|
kpackage_install_package(${source} ${name} effects kwin)
|
||||||
|
|
||||||
|
file(COPY ${source}/contents ${source}/metadata.json DESTINATION ${CMAKE_BINARY_DIR}/bin/kwin/effects/${name})
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
add_kwin_effect(shift-snap-assist shift-snap-assist)
|
||||||
718
kwin/effects/shift-snap-assist/contents/ui/main.qml
Normal file
718
kwin/effects/shift-snap-assist/contents/ui/main.qml
Normal file
|
|
@ -0,0 +1,718 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
//
|
||||||
|
// SHIFT Snap Assist — KWin declarative SceneEffect
|
||||||
|
//
|
||||||
|
// Presents a floating panel of layout preset buttons.
|
||||||
|
// Activated by:
|
||||||
|
// 1. Meta+Shift+S keyboard shortcut
|
||||||
|
// 2. Hovering the maximize button in convergence mode while dynamic tiling is off
|
||||||
|
//
|
||||||
|
// When a preset is clicked, the active window's frameGeometry is set to the
|
||||||
|
// chosen zone (with outer gaps applied).
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import org.kde.kwin as KWinComponents
|
||||||
|
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||||
|
|
||||||
|
KWinComponents.SceneEffect {
|
||||||
|
id: effect
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
readonly property bool snapLayoutsEligible: ShellSettings.Settings.convergenceModeEnabled
|
||||||
|
&& !ShellSettings.Settings.gamingModeEnabled
|
||||||
|
&& !ShellSettings.Settings.dynamicTilingEnabled
|
||||||
|
readonly property int hoverBarHeight: 30
|
||||||
|
readonly property int decorationButtonSize: 16
|
||||||
|
readonly property int decorationButtonSpacing: 8
|
||||||
|
readonly property int decorationButtonSideMargin: 12
|
||||||
|
readonly property int maximizeButtonPadding: 4
|
||||||
|
readonly property int hoverTimerInterval: 75
|
||||||
|
readonly property int hoverDwellTicks: 14
|
||||||
|
readonly property int hoverMoveTolerance: 5
|
||||||
|
readonly property int hoverCooldownMs: 1200
|
||||||
|
readonly property int panelDismissMargin: 24
|
||||||
|
readonly property int panelScreenMargin: 8
|
||||||
|
readonly property int panelCursorGap: 12
|
||||||
|
readonly property int panelCursorRightBias: 34
|
||||||
|
property var hoverWindowId: null
|
||||||
|
property int hoverTicks: 0
|
||||||
|
property string hoverWindowStateKey: ""
|
||||||
|
property bool hoverSuppressedUntilLeave: false
|
||||||
|
property point hoverAnchorPos: Qt.point(0, 0)
|
||||||
|
property double hoverCooldownUntil: 0
|
||||||
|
property var panelAnchorPos: Qt.point(0, 0)
|
||||||
|
property string panelAnchorScreenName: ""
|
||||||
|
property bool previewVisible: false
|
||||||
|
property rect previewGeometry: Qt.rect(0, 0, 0, 0)
|
||||||
|
property rect previewArea: Qt.rect(0, 0, 0, 0)
|
||||||
|
property var previewZones: []
|
||||||
|
property int previewActiveIndex: -1
|
||||||
|
property string previewScreenName: ""
|
||||||
|
|
||||||
|
function resetHoverState() {
|
||||||
|
hoverWindowId = null;
|
||||||
|
hoverTicks = 0;
|
||||||
|
hoverWindowStateKey = "";
|
||||||
|
hoverSuppressedUntilLeave = false;
|
||||||
|
hoverAnchorPos = Qt.point(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetHoverCandidate(win) {
|
||||||
|
hoverWindowId = win ? win.internalId : null;
|
||||||
|
hoverTicks = 1;
|
||||||
|
hoverWindowStateKey = win ? windowStateKey(win) : "";
|
||||||
|
hoverSuppressedUntilLeave = false;
|
||||||
|
hoverAnchorPos = KWinComponents.Workspace.cursorPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHoverCooldown() {
|
||||||
|
hoverCooldownUntil = Date.now() + hoverCooldownMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hoverOnCooldown() {
|
||||||
|
return Date.now() < hoverCooldownUntil;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hoverMovedTooFar() {
|
||||||
|
const cursor = KWinComponents.Workspace.cursorPos;
|
||||||
|
const dx = cursor.x - hoverAnchorPos.x;
|
||||||
|
const dy = cursor.y - hoverAnchorPos.y;
|
||||||
|
return dx * dx + dy * dy > hoverMoveTolerance * hoverMoveTolerance;
|
||||||
|
}
|
||||||
|
|
||||||
|
function windowStateKey(win) {
|
||||||
|
const geometry = win.frameGeometry;
|
||||||
|
const maximized = win.maximized === undefined ? "" : win.maximized;
|
||||||
|
return maximized + ":" + geometry.x + "," + geometry.y + "," + geometry.width + "x" + geometry.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSnapLayouts() {
|
||||||
|
if (visible) {
|
||||||
|
setHoverCooldown();
|
||||||
|
}
|
||||||
|
visible = false;
|
||||||
|
resetHoverState();
|
||||||
|
panelAnchorScreenName = "";
|
||||||
|
hideSnapPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSnapLayouts() {
|
||||||
|
const win = KWinComponents.Workspace.activeWindow;
|
||||||
|
panelAnchorPos = KWinComponents.Workspace.cursorPos;
|
||||||
|
panelAnchorScreenName = win && win.output ? win.output.name : "";
|
||||||
|
visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleActiveWindowMaximized() {
|
||||||
|
const win = KWinComponents.Workspace.activeWindow;
|
||||||
|
if (!win || !win.normalWindow || win.fullScreen || !win.maximizable) {
|
||||||
|
hideSnapLayouts();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maximize = !win.maximized;
|
||||||
|
win.setMaximize(maximize, maximize);
|
||||||
|
hideSnapLayouts();
|
||||||
|
setHoverCooldown();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSnapPreview() {
|
||||||
|
previewVisible = false;
|
||||||
|
previewScreenName = "";
|
||||||
|
previewZones = [];
|
||||||
|
previewActiveIndex = -1;
|
||||||
|
KWinComponents.Workspace.hideOutline();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSnapPreview(preset, activeIndex) {
|
||||||
|
const win = KWinComponents.Workspace.activeWindow;
|
||||||
|
if (!win || !win.output) {
|
||||||
|
hideSnapPreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const desktop = win.desktops.length > 0 ? win.desktops[0] : null;
|
||||||
|
if (!desktop) {
|
||||||
|
hideSnapPreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const area = KWinComponents.Workspace.clientArea(KWinComponents.Workspace.MaximizeArea, win.output, desktop);
|
||||||
|
const zone = preset.zones[activeIndex];
|
||||||
|
const gap = effect.outerGap;
|
||||||
|
previewGeometry = Qt.rect(
|
||||||
|
area.x + Math.round(zone.x * area.width) + gap,
|
||||||
|
area.y + Math.round(zone.y * area.height) + gap,
|
||||||
|
Math.round(zone.w * area.width) - 2 * gap,
|
||||||
|
Math.round(zone.h * area.height) - 2 * gap
|
||||||
|
);
|
||||||
|
previewArea = area;
|
||||||
|
previewZones = preset.zones;
|
||||||
|
previewActiveIndex = activeIndex;
|
||||||
|
previewScreenName = win.output.name;
|
||||||
|
previewVisible = true;
|
||||||
|
KWinComponents.Workspace.hideOutline();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cursorInActiveWindowMaximizeStrip() {
|
||||||
|
const win = KWinComponents.Workspace.activeWindow;
|
||||||
|
if (!win || !win.normalWindow || win.fullScreen || !win.maximizable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cursor = KWinComponents.Workspace.cursorPos;
|
||||||
|
const button = maximizeButtonRect(win);
|
||||||
|
if (button.width <= 0 || button.height <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cursor.x >= button.x - effect.maximizeButtonPadding
|
||||||
|
&& cursor.x <= button.x + button.width + effect.maximizeButtonPadding
|
||||||
|
&& cursor.y >= button.y - effect.maximizeButtonPadding
|
||||||
|
&& cursor.y <= button.y + button.height + effect.maximizeButtonPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decorationButtonVisible(code) {
|
||||||
|
return code === "M" || code === "N" || code === "I" || code === "A" || code === "X";
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleDecorationButtons(sequence) {
|
||||||
|
const buttons = [];
|
||||||
|
for (let i = 0; i < sequence.length; i++) {
|
||||||
|
const code = sequence[i];
|
||||||
|
if (decorationButtonVisible(code)) {
|
||||||
|
buttons.push(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maximizeButtonRect(win) {
|
||||||
|
const fg = win.frameGeometry;
|
||||||
|
const buttonY = fg.y + Math.round((effect.hoverBarHeight - effect.decorationButtonSize) / 2);
|
||||||
|
const leftButtons = visibleDecorationButtons(ShellSettings.KWinSettings.titleButtonsOnLeft);
|
||||||
|
const leftIndex = leftButtons.indexOf("A");
|
||||||
|
if (leftIndex >= 0) {
|
||||||
|
return Qt.rect(
|
||||||
|
fg.x + effect.decorationButtonSideMargin + leftIndex * (effect.decorationButtonSize + effect.decorationButtonSpacing),
|
||||||
|
buttonY,
|
||||||
|
effect.decorationButtonSize,
|
||||||
|
effect.decorationButtonSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rightButtons = visibleDecorationButtons(ShellSettings.KWinSettings.titleButtonsOnRight);
|
||||||
|
const rightIndex = rightButtons.indexOf("A");
|
||||||
|
if (rightIndex >= 0) {
|
||||||
|
const rowWidth = rightButtons.length * effect.decorationButtonSize
|
||||||
|
+ Math.max(0, rightButtons.length - 1) * effect.decorationButtonSpacing;
|
||||||
|
return Qt.rect(
|
||||||
|
fg.x + fg.width - effect.decorationButtonSideMargin - rowWidth
|
||||||
|
+ rightIndex * (effect.decorationButtonSize + effect.decorationButtonSpacing),
|
||||||
|
buttonY,
|
||||||
|
effect.decorationButtonSize,
|
||||||
|
effect.decorationButtonSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Qt.rect(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cursorInPanel(screen) {
|
||||||
|
if (!screen) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (panelAnchorScreenName !== "" && panelAnchorScreenName !== screen.name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cursor = KWinComponents.Workspace.cursorPos;
|
||||||
|
const panel = panelRect(screen);
|
||||||
|
return cursor.x >= panel.x - panelDismissMargin
|
||||||
|
&& cursor.x <= panel.x + panel.width + panelDismissMargin
|
||||||
|
&& cursor.y >= panel.y - panelDismissMargin
|
||||||
|
&& cursor.y <= panel.y + panel.height + panelDismissMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
function panelRect(screen) {
|
||||||
|
const minX = screen.geometry.x + panelScreenMargin;
|
||||||
|
const maxX = screen.geometry.x + screen.geometry.width - snapPanelWidth - panelScreenMargin;
|
||||||
|
const minY = screen.geometry.y + panelScreenMargin;
|
||||||
|
const maxY = screen.geometry.y + screen.geometry.height - snapPanelHeight - panelScreenMargin;
|
||||||
|
const wantedX = panelAnchorPos.x - snapPanelWidth + panelCursorRightBias;
|
||||||
|
const wantedY = panelAnchorPos.y + panelCursorGap;
|
||||||
|
return Qt.rect(
|
||||||
|
Math.max(minX, Math.min(maxX, wantedX)),
|
||||||
|
Math.max(minY, Math.min(maxY, wantedY)),
|
||||||
|
snapPanelWidth,
|
||||||
|
snapPanelHeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Visibility ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// The effect starts invisible; toggle via shortcut.
|
||||||
|
// SceneEffect.visible controls whether delegates are painted.
|
||||||
|
|
||||||
|
KWinComponents.ShortcutHandler {
|
||||||
|
name: "SHIFT Snap Assist"
|
||||||
|
text: "SHIFT Snap Assist: Show snap layout picker"
|
||||||
|
sequence: "Meta+Shift+S"
|
||||||
|
onActivated: {
|
||||||
|
if (!effect.snapLayoutsEligible) {
|
||||||
|
effect.hideSnapLayouts();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (effect.visible) {
|
||||||
|
effect.hideSnapLayouts();
|
||||||
|
} else {
|
||||||
|
effect.showSnapLayouts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KWinComponents.ShortcutHandler {
|
||||||
|
name: "SHIFT Snap Assist Escape"
|
||||||
|
text: "SHIFT Snap Assist: Hide snap layout picker"
|
||||||
|
sequence: "Esc"
|
||||||
|
onActivated: effect.hideSnapLayouts()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: hoverTimer
|
||||||
|
interval: effect.hoverTimerInterval
|
||||||
|
repeat: true
|
||||||
|
running: effect.snapLayoutsEligible
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (effect.visible) {
|
||||||
|
const screen = KWinComponents.Workspace.activeWindow ? KWinComponents.Workspace.activeWindow.output : null;
|
||||||
|
if (!effect.cursorInActiveWindowMaximizeStrip() && !effect.cursorInPanel(screen)) {
|
||||||
|
effect.hideSnapLayouts();
|
||||||
|
} else {
|
||||||
|
effect.resetHoverState();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const win = KWinComponents.Workspace.activeWindow;
|
||||||
|
if (!win || !win.normalWindow || win.fullScreen || !win.maximizable) {
|
||||||
|
effect.resetHoverState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!effect.cursorInActiveWindowMaximizeStrip()) {
|
||||||
|
effect.resetHoverState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effect.hoverOnCooldown()) {
|
||||||
|
effect.resetHoverState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effect.hoverWindowId !== win.internalId) {
|
||||||
|
effect.resetHoverCandidate(win);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effect.hoverMovedTooFar()) {
|
||||||
|
effect.resetHoverCandidate(win);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateKey = effect.windowStateKey(win);
|
||||||
|
if (stateKey !== effect.hoverWindowStateKey) {
|
||||||
|
effect.hoverWindowStateKey = stateKey;
|
||||||
|
effect.hoverTicks = 0;
|
||||||
|
effect.hoverSuppressedUntilLeave = true;
|
||||||
|
effect.setHoverCooldown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effect.hoverSuppressedUntilLeave) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
effect.hoverTicks++;
|
||||||
|
if (effect.hoverTicks >= effect.hoverDwellTicks) {
|
||||||
|
effect.showSnapLayouts();
|
||||||
|
effect.resetHoverState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: KWinComponents.Workspace
|
||||||
|
function onActiveWindowChanged() {
|
||||||
|
effect.resetHoverState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: ShellSettings.Settings
|
||||||
|
|
||||||
|
function onConvergenceModeEnabledChanged() {
|
||||||
|
if (!effect.snapLayoutsEligible) {
|
||||||
|
effect.hideSnapLayouts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGamingModeEnabledChanged() {
|
||||||
|
if (!effect.snapLayoutsEligible) {
|
||||||
|
effect.hideSnapLayouts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDynamicTilingEnabledChanged() {
|
||||||
|
if (!effect.snapLayoutsEligible) {
|
||||||
|
effect.hideSnapLayouts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Gap constant (must match shift-tiling) ────────────────────────────
|
||||||
|
readonly property int outerGap: 8
|
||||||
|
|
||||||
|
// ── Layout presets ────────────────────────────────────────────────────
|
||||||
|
// Each preset is an array of zone descriptors:
|
||||||
|
// { x, y, w, h } in relative [0..1] coordinates (of work area).
|
||||||
|
// The first zone is where the ACTIVE window will be placed.
|
||||||
|
// Remaining zones are currently visual-only.
|
||||||
|
|
||||||
|
readonly property var presets: [
|
||||||
|
{
|
||||||
|
name: "Half left",
|
||||||
|
zones: [
|
||||||
|
{ x: 0, y: 0, w: 0.5, h: 1 },
|
||||||
|
{ x: 0.5, y: 0, w: 0.5, h: 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Thirds",
|
||||||
|
zones: [
|
||||||
|
{ x: 0, y: 0, w: 0.333, h: 1 },
|
||||||
|
{ x: 0.333, y: 0, w: 0.334, h: 1 },
|
||||||
|
{ x: 0.667, y: 0, w: 0.333, h: 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Main + side",
|
||||||
|
zones: [
|
||||||
|
{ x: 0, y: 0, w: 0.667, h: 1 },
|
||||||
|
{ x: 0.667, y: 0, w: 0.333, h: 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Side + main",
|
||||||
|
zones: [
|
||||||
|
{ x: 0.333, y: 0, w: 0.667, h: 1 },
|
||||||
|
{ x: 0, y: 0, w: 0.333, h: 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Quad",
|
||||||
|
zones: [
|
||||||
|
{ x: 0, y: 0, w: 0.5, h: 0.5 },
|
||||||
|
{ x: 0.5, y: 0, w: 0.5, h: 0.5 },
|
||||||
|
{ x: 0, y: 0.5, w: 0.5, h: 0.5 },
|
||||||
|
{ x: 0.5, y: 0.5, w: 0.5, h: 0.5 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Main + two",
|
||||||
|
zones: [
|
||||||
|
{ x: 0, y: 0, w: 0.5, h: 1 },
|
||||||
|
{ x: 0.5, y: 0, w: 0.5, h: 0.5 },
|
||||||
|
{ x: 0.5, y: 0.5, w: 0.5, h: 0.5 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
readonly property int snapButtonWidth: 58
|
||||||
|
readonly property int snapButtonHeight: 38
|
||||||
|
readonly property int snapButtonSpacing: 8
|
||||||
|
readonly property int snapPanelHorizontalPadding: 28
|
||||||
|
readonly property int snapPanelVerticalPadding: 34
|
||||||
|
readonly property int snapPanelWidth: presets.length * snapButtonWidth
|
||||||
|
+ Math.max(0, presets.length - 1) * snapButtonSpacing
|
||||||
|
+ snapPanelHorizontalPadding
|
||||||
|
readonly property int snapPanelHeight: snapButtonHeight + snapPanelVerticalPadding + 23
|
||||||
|
|
||||||
|
function accentColor(alpha) {
|
||||||
|
return Qt.rgba(46 / 255, 184 / 255, 168 / 255, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
function accentPaleColor(alpha) {
|
||||||
|
return Qt.rgba(163 / 255, 218 / 255, 212 / 255, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply a zone (in relative coords) to a window given a work area rect.
|
||||||
|
function applyZone(win, zone, area) {
|
||||||
|
const g = effect.outerGap;
|
||||||
|
win.frameGeometry = Qt.rect(
|
||||||
|
area.x + Math.round(zone.x * area.width) + g,
|
||||||
|
area.y + Math.round(zone.y * area.height) + g,
|
||||||
|
Math.round(zone.w * area.width) - 2 * g,
|
||||||
|
Math.round(zone.h * area.height) - 2 * g
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Per-screen delegate ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: screenDelegate
|
||||||
|
|
||||||
|
readonly property var targetScreen: KWinComponents.SceneView.screen
|
||||||
|
readonly property rect popupRect: effect.panelRect(targetScreen)
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
KWinComponents.DesktopBackground {
|
||||||
|
anchors.fill: parent
|
||||||
|
z: -100
|
||||||
|
activity: KWinComponents.Workspace.currentActivity
|
||||||
|
desktop: KWinComponents.Workspace.currentDesktop
|
||||||
|
outputName: screenDelegate.targetScreen.name
|
||||||
|
}
|
||||||
|
|
||||||
|
Instantiator {
|
||||||
|
model: KWinComponents.WindowFilterModel {
|
||||||
|
desktop: KWinComponents.Workspace.currentDesktop
|
||||||
|
screenName: screenDelegate.targetScreen.name
|
||||||
|
windowModel: stackModel
|
||||||
|
minimizedWindows: false
|
||||||
|
windowType: ~KWinComponents.WindowFilterModel.Desktop
|
||||||
|
& ~KWinComponents.WindowFilterModel.Notification
|
||||||
|
& ~KWinComponents.WindowFilterModel.CriticalNotification
|
||||||
|
}
|
||||||
|
|
||||||
|
KWinComponents.WindowThumbnail {
|
||||||
|
wId: model.window.internalId
|
||||||
|
x: model.window.x - screenDelegate.targetScreen.geometry.x
|
||||||
|
y: model.window.y - screenDelegate.targetScreen.geometry.y
|
||||||
|
z: model.window.stackingOrder
|
||||||
|
visible: !model.window.hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
onObjectAdded: (index, object) => {
|
||||||
|
object.parent = screenDelegate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KWinComponents.WindowModel {
|
||||||
|
id: stackModel
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: effect.previewVisible && effect.previewScreenName === screenDelegate.targetScreen.name ? effect.previewZones : []
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
readonly property bool activeZone: index === effect.previewActiveIndex
|
||||||
|
readonly property int previewGap: effect.outerGap
|
||||||
|
|
||||||
|
x: effect.previewArea.x + Math.round(modelData.x * effect.previewArea.width) + previewGap - screenDelegate.targetScreen.geometry.x
|
||||||
|
y: effect.previewArea.y + Math.round(modelData.y * effect.previewArea.height) + previewGap - screenDelegate.targetScreen.geometry.y
|
||||||
|
width: Math.max(1, Math.round(modelData.w * effect.previewArea.width) - 2 * previewGap)
|
||||||
|
height: Math.max(1, Math.round(modelData.h * effect.previewArea.height) - 2 * previewGap)
|
||||||
|
z: activeZone ? 90001 : 90000
|
||||||
|
radius: 6
|
||||||
|
color: activeZone ? effect.accentColor(0.24)
|
||||||
|
: effect.accentPaleColor(0.08)
|
||||||
|
border.width: activeZone ? 2 : 1
|
||||||
|
border.color: activeZone ? effect.accentPaleColor(0.78)
|
||||||
|
: effect.accentPaleColor(0.26)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onEscapePressed: effect.hideSnapLayouts()
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: maximizeForwarder
|
||||||
|
|
||||||
|
readonly property var activeWindow: KWinComponents.Workspace.activeWindow
|
||||||
|
readonly property rect buttonRect: activeWindow ? effect.maximizeButtonRect(activeWindow) : Qt.rect(0, 0, 0, 0)
|
||||||
|
|
||||||
|
visible: activeWindow
|
||||||
|
&& activeWindow.output
|
||||||
|
&& activeWindow.output.name === screenDelegate.targetScreen.name
|
||||||
|
&& buttonRect.width > 0
|
||||||
|
&& buttonRect.height > 0
|
||||||
|
z: 100001
|
||||||
|
x: buttonRect.x - effect.maximizeButtonPadding - screenDelegate.targetScreen.geometry.x
|
||||||
|
y: buttonRect.y - effect.maximizeButtonPadding - screenDelegate.targetScreen.geometry.y
|
||||||
|
width: buttonRect.width + 2 * effect.maximizeButtonPadding
|
||||||
|
height: buttonRect.height + 2 * effect.maximizeButtonPadding
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
onTapped: effect.toggleActiveWindowMaximized()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SnapPanel {
|
||||||
|
id: snapPanel
|
||||||
|
visible: effect.panelAnchorScreenName === "" || effect.panelAnchorScreenName === screenDelegate.targetScreen.name
|
||||||
|
z: 100000
|
||||||
|
x: screenDelegate.popupRect.x - screenDelegate.targetScreen.geometry.x
|
||||||
|
y: screenDelegate.popupRect.y - screenDelegate.targetScreen.geometry.y
|
||||||
|
|
||||||
|
screen: KWinComponents.SceneView.screen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Snap panel component ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
component SnapPanel: Rectangle {
|
||||||
|
id: panel
|
||||||
|
|
||||||
|
required property var screen
|
||||||
|
|
||||||
|
width: effect.snapPanelWidth
|
||||||
|
height: effect.snapPanelHeight
|
||||||
|
|
||||||
|
color: Qt.rgba(0.08, 0.10, 0.15, 0.82)
|
||||||
|
radius: 8
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(1, 1, 1, 0.16)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: 3
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.22)
|
||||||
|
radius: parent.radius
|
||||||
|
z: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors { fill: parent; margins: 14 }
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Snap layouts"
|
||||||
|
color: effect.accentPaleColor(0.9)
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.capitalization: Font.AllUppercase
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: presetsRow
|
||||||
|
spacing: effect.snapButtonSpacing
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: effect.presets
|
||||||
|
|
||||||
|
delegate: PresetButton {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
preset: modelData
|
||||||
|
screen: panel.screen
|
||||||
|
onClicked: {
|
||||||
|
effect.hideSnapLayouts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Preset button ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
component PresetButton: Rectangle {
|
||||||
|
id: btn
|
||||||
|
|
||||||
|
required property var preset
|
||||||
|
required property var screen
|
||||||
|
signal clicked
|
||||||
|
|
||||||
|
width: effect.snapButtonWidth
|
||||||
|
height: effect.snapButtonHeight
|
||||||
|
readonly property int previewMargin: 6
|
||||||
|
|
||||||
|
color: hovered ? Qt.rgba(1, 1, 1, 0.18) : Qt.rgba(1, 1, 1, 0.08)
|
||||||
|
radius: 6
|
||||||
|
border.width: 1
|
||||||
|
border.color: hovered ? effect.accentPaleColor(0.62) : Qt.rgba(1, 1, 1, 0.14)
|
||||||
|
|
||||||
|
property bool hovered: false
|
||||||
|
|
||||||
|
Behavior on color { ColorAnimation { duration: 80 } }
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: previewFrame
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: btn.previewMargin
|
||||||
|
color: Qt.rgba(0.06, 0.08, 0.12, 0.72)
|
||||||
|
radius: 4
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(1, 1, 1, 0.08)
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: btn.preset.zones
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
x: Math.round(modelData.x * previewFrame.width)
|
||||||
|
y: Math.round(modelData.y * previewFrame.height)
|
||||||
|
width: Math.max(4, Math.round(modelData.w * previewFrame.width) - 1)
|
||||||
|
height: Math.max(4, Math.round(modelData.h * previewFrame.height) - 1)
|
||||||
|
color: zoneHover.hovered ? effect.accentPaleColor(0.96)
|
||||||
|
: (index === 0 ? effect.accentColor(0.96)
|
||||||
|
: effect.accentPaleColor(0.48))
|
||||||
|
border.width: 0
|
||||||
|
radius: 2
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: zoneHover
|
||||||
|
onHoveredChanged: {
|
||||||
|
btn.hovered = hovered;
|
||||||
|
if (hovered) {
|
||||||
|
effect.showSnapPreview(btn.preset, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
onTapped: btn.applyZone(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
onHoveredChanged: {
|
||||||
|
if (!hovered) {
|
||||||
|
btn.hovered = false;
|
||||||
|
effect.hideSnapPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyZone(zone) {
|
||||||
|
const win = KWinComponents.Workspace.activeWindow;
|
||||||
|
if (!win || !win.output) {
|
||||||
|
btn.clicked();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const desktop = win.desktops.length > 0 ? win.desktops[0] : null;
|
||||||
|
if (!desktop) {
|
||||||
|
btn.clicked();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const area = KWinComponents.Workspace.clientArea(
|
||||||
|
KWinComponents.Workspace.MaximizeArea, win.output, desktop);
|
||||||
|
effect.hideSnapPreview();
|
||||||
|
effect.applyZone(win, zone, area);
|
||||||
|
btn.clicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
kwin/effects/shift-snap-assist/metadata.json
Normal file
20
kwin/effects/shift-snap-assist/metadata.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"KPackageStructure": "KWin/Effect",
|
||||||
|
"KPlugin": {
|
||||||
|
"Authors": [
|
||||||
|
{
|
||||||
|
"Email": "marcoa@example.com",
|
||||||
|
"Name": "Marco Allegretti"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Category": "Appearance",
|
||||||
|
"Description": "Windows 11-style snap layout picker for SHIFT convergence mode with native window placement. Activated by hovering the maximize button or pressing Meta+Shift+S when dynamic tiling is off.",
|
||||||
|
"EnabledByDefault": false,
|
||||||
|
"Id": "shift-snap-assist",
|
||||||
|
"License": "EUPL-1.2",
|
||||||
|
"Name": "SHIFT Snap Assist",
|
||||||
|
"Version": "1.0"
|
||||||
|
},
|
||||||
|
"X-KDE-Ordering": 60,
|
||||||
|
"X-Plasma-API": "declarativescript"
|
||||||
|
}
|
||||||
|
|
@ -10,4 +10,5 @@ function(add_kwin_script name source)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
add_kwin_script(convergentwindows convergentwindows)
|
add_kwin_script(convergentwindows convergentwindows)
|
||||||
|
add_kwin_script(shift-tiling shift-tiling)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,13 +53,13 @@ Loader {
|
||||||
}
|
}
|
||||||
|
|
||||||
function run(window) {
|
function run(window) {
|
||||||
// HACK: don't maximize xwaylandvideobridge
|
if (!window || window.deleted || !window.normalWindow) {
|
||||||
// see: https://invent.kde.org/plasma/plasma-mobile/-/issues/324
|
|
||||||
if (window.resourceClass === 'xwaylandvideobridge') {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!window.normalWindow) {
|
// HACK: don't maximize xwaylandvideobridge
|
||||||
|
// see: https://invent.kde.org/plasma/plasma-mobile/-/issues/324
|
||||||
|
if (window.resourceClass === 'xwaylandvideobridge') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
616
kwin/scripts/shift-tiling/contents/ui/main.qml
Normal file
616
kwin/scripts/shift-tiling/contents/ui/main.qml
Normal file
|
|
@ -0,0 +1,616 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
//
|
||||||
|
// SHIFT Dynamic Tiling — KWin declarative script
|
||||||
|
//
|
||||||
|
// Architecture:
|
||||||
|
// - One ScreenState per output, keyed by output.name
|
||||||
|
// - Each ScreenState holds an ordered list of TileNodes
|
||||||
|
// - A TileNode is { win, rect } where rect is absolute in-screen coordinates
|
||||||
|
// - On any change (add/remove/resize) the layout is recomputed from scratch
|
||||||
|
// for the affected screen using a BSP algorithm
|
||||||
|
// - Drag detection uses interactiveMoveResizeStarted/Stepped/Finished
|
||||||
|
// - Snap zones are the 6 screen-edge regions (left/right/top/corners)
|
||||||
|
// - Gap: outer 8px on screen edges, inner 8px between tiles (4px each side)
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import org.kde.kwin as KWinComponents
|
||||||
|
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// ── Configuration ───────────────────────────────────────────────────────
|
||||||
|
readonly property int outerGap: 8
|
||||||
|
readonly property int innerGap: 8 // half applied to each edge → 4px per tile
|
||||||
|
|
||||||
|
// ── State ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Per-screen tile list. Key: output.name Value: [{win, rect}]
|
||||||
|
// rect is a Qt.rect in absolute screen coordinates.
|
||||||
|
property var screenTiles: ({})
|
||||||
|
|
||||||
|
// Windows the user has manually floated (by UUID string).
|
||||||
|
property var floatedWindows: ({})
|
||||||
|
|
||||||
|
// Whether tiling is globally enabled.
|
||||||
|
property bool tilingEnabled: true
|
||||||
|
|
||||||
|
// Drag state.
|
||||||
|
//
|
||||||
|
// Behaviour: dragging a tile and dropping it onto ANOTHER tile swaps
|
||||||
|
// their positions in the BSP layout. Dropping anywhere else does
|
||||||
|
// nothing (the window will be re-tiled into its original slot on the
|
||||||
|
// next layout pass, unless KWin's native quick-tile / electric border
|
||||||
|
// takes over — which is fine; we don't fight it).
|
||||||
|
property var draggingWindow: null
|
||||||
|
property var swapOutlineActive: false
|
||||||
|
|
||||||
|
// Reorder state — kept stable while dragging so the rest of the layout
|
||||||
|
// doesn't shuffle under the cursor.
|
||||||
|
property string dragSourceScreen: ""
|
||||||
|
property int dragSourceIndex: -1
|
||||||
|
property var dragSwapTarget: null // {screen, index, rect} of tile under cursor
|
||||||
|
|
||||||
|
// Deferred retile queue.
|
||||||
|
// The dockSpaceReserver LayerShell exclusive zone needs one Wayland
|
||||||
|
// roundtrip after setMaximize() before KWin updates MaximizeArea.
|
||||||
|
// We queue output names and flush after 200 ms — same pattern as
|
||||||
|
// convergentwindows constrainAfterRestoreTimer.
|
||||||
|
property var pendingRetile: []
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: retileTimer
|
||||||
|
interval: 200
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
const queue = root.pendingRetile.slice();
|
||||||
|
root.pendingRetile = [];
|
||||||
|
const done = {};
|
||||||
|
for (let i = 0; i < queue.length; i++) {
|
||||||
|
if (!done[queue[i]]) {
|
||||||
|
done[queue[i]] = true;
|
||||||
|
root.retileScreen(queue[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleRetile(outputName) {
|
||||||
|
const q = root.pendingRetile.slice();
|
||||||
|
if (q.indexOf(outputName) < 0) q.push(outputName);
|
||||||
|
root.pendingRetile = q;
|
||||||
|
retileTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tiling guard ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Active when convergence is on, gaming is off, AND the user has
|
||||||
|
// dynamic tiling enabled in quick settings. When this returns false
|
||||||
|
// the script is fully inert and KWin's native quick-tile behaviour
|
||||||
|
// owns window placement.
|
||||||
|
function isConvergence() {
|
||||||
|
return ShellSettings.Settings.convergenceModeEnabled &&
|
||||||
|
!ShellSettings.Settings.gamingModeEnabled &&
|
||||||
|
ShellSettings.Settings.dynamicTilingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirror the same guard used by convergentwindows: only normalWindow is
|
||||||
|
// reliable in the KWin 6 declarative script API. Add maximizable to
|
||||||
|
// avoid calling setMaximize on layer-shell / panel surfaces.
|
||||||
|
function shouldIgnore(win) {
|
||||||
|
if (!win || win.deleted) return true;
|
||||||
|
if (!win.normalWindow) return true; // panels, dock, desktop, layer-shell
|
||||||
|
if (!win.maximizable) return true; // fixed/special surfaces
|
||||||
|
if (win.fullScreen) return true;
|
||||||
|
// Skip xwaylandvideobridge (same as convergentwindows)
|
||||||
|
if (win.resourceClass === "xwaylandvideobridge") return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldFloat(win) {
|
||||||
|
if (!win) return true;
|
||||||
|
// Fixed-size windows (won't tile sensibly)
|
||||||
|
const minW = win.minSize ? win.minSize.width : 0;
|
||||||
|
const maxW = win.maxSize ? win.maxSize.width : 0;
|
||||||
|
const minH = win.minSize ? win.minSize.height : 0;
|
||||||
|
const maxH = win.maxSize ? win.maxSize.height : 0;
|
||||||
|
if (minW > 0 && maxW > 0 && minW >= maxW) return true;
|
||||||
|
if (minH > 0 && maxH > 0 && minH >= maxH) return true;
|
||||||
|
// Manually floated
|
||||||
|
if (floatedWindows[win.internalId]) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTileable(win) {
|
||||||
|
if (!tilingEnabled) return false;
|
||||||
|
if (!isConvergence()) return false;
|
||||||
|
if (shouldIgnore(win)) return false;
|
||||||
|
if (shouldFloat(win)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Layout engine ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function workRect(win) {
|
||||||
|
const output = win.output;
|
||||||
|
const desktop = win.desktops[0];
|
||||||
|
if (!output || !desktop) return null;
|
||||||
|
return KWinComponents.Workspace.clientArea(
|
||||||
|
KWinComponents.Workspace.MaximizeArea, output, desktop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply outer + inner gaps to a list of rects that together tile a screen.
|
||||||
|
// outer: gap between screen edge and tile
|
||||||
|
// inner: total gap between two adjacent tiles (split equally, so 4px each side)
|
||||||
|
function applyGaps(rects, workArea) {
|
||||||
|
if (!rects || rects.length === 0) return rects;
|
||||||
|
const half = innerGap / 2;
|
||||||
|
const result = [];
|
||||||
|
for (let i = 0; i < rects.length; i++) {
|
||||||
|
let r = rects[i];
|
||||||
|
// Determine which edges touch the work area boundary
|
||||||
|
const atLeft = Math.abs(r.x - workArea.x) < 2;
|
||||||
|
const atTop = Math.abs(r.y - workArea.y) < 2;
|
||||||
|
const atRight = Math.abs((r.x + r.width) - (workArea.x + workArea.width)) < 2;
|
||||||
|
const atBottom = Math.abs((r.y + r.height) - (workArea.y + workArea.height)) < 2;
|
||||||
|
|
||||||
|
const left = atLeft ? outerGap : half;
|
||||||
|
const top = atTop ? outerGap : half;
|
||||||
|
const right = atRight ? outerGap : half;
|
||||||
|
const bottom = atBottom ? outerGap : half;
|
||||||
|
|
||||||
|
result.push(Qt.rect(
|
||||||
|
r.x + left,
|
||||||
|
r.y + top,
|
||||||
|
r.width - left - right,
|
||||||
|
r.height - top - bottom
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary-space-partition layout.
|
||||||
|
// Splits `area` recursively for `n` windows.
|
||||||
|
// Returns an ordered array of Qt.rect (without gaps applied).
|
||||||
|
function bspRects(area, n) {
|
||||||
|
if (n <= 0) return [];
|
||||||
|
if (n === 1) return [area];
|
||||||
|
|
||||||
|
// Pick split axis: split the longer dimension
|
||||||
|
const splitHorizontally = area.width >= area.height;
|
||||||
|
const rects = [];
|
||||||
|
|
||||||
|
if (splitHorizontally) {
|
||||||
|
// Left half gets one window; right half gets (n-1)
|
||||||
|
const leftW = Math.round(area.width / 2);
|
||||||
|
const left = Qt.rect(area.x, area.y, leftW, area.height);
|
||||||
|
const right = Qt.rect(area.x + leftW, area.y, area.width - leftW, area.height);
|
||||||
|
rects.push(left);
|
||||||
|
const sub = bspRects(right, n - 1);
|
||||||
|
for (let i = 0; i < sub.length; i++) rects.push(sub[i]);
|
||||||
|
} else {
|
||||||
|
// Top half gets one window; bottom half gets (n-1)
|
||||||
|
const topH = Math.round(area.height / 2);
|
||||||
|
const top = Qt.rect(area.x, area.y, area.width, topH);
|
||||||
|
const bottom = Qt.rect(area.x, area.y + topH, area.width, area.height - topH);
|
||||||
|
rects.push(top);
|
||||||
|
const sub = bspRects(bottom, n - 1);
|
||||||
|
for (let i = 0; i < sub.length; i++) rects.push(sub[i]);
|
||||||
|
}
|
||||||
|
return rects;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompute and apply layout for a single screen.
|
||||||
|
function retileScreen(outputName) {
|
||||||
|
const tiles = screenTiles[outputName];
|
||||||
|
if (!tiles || tiles.length === 0) return;
|
||||||
|
|
||||||
|
// Get work area from the first window's output
|
||||||
|
let area = null;
|
||||||
|
for (let i = 0; i < tiles.length; i++) {
|
||||||
|
const r = workRect(tiles[i].win);
|
||||||
|
if (r) { area = r; break; }
|
||||||
|
}
|
||||||
|
if (!area) return;
|
||||||
|
|
||||||
|
const n = tiles.length;
|
||||||
|
const rawRects = bspRects(area, n);
|
||||||
|
const gappedRects = applyGaps(rawRects, area);
|
||||||
|
|
||||||
|
for (let i = 0; i < tiles.length; i++) {
|
||||||
|
const win = tiles[i].win;
|
||||||
|
if (!win || win.deleted) continue;
|
||||||
|
const r = gappedRects[i];
|
||||||
|
tiles[i].rect = r;
|
||||||
|
win.frameGeometry = r;
|
||||||
|
}
|
||||||
|
// Trigger a binding update
|
||||||
|
screenTiles[outputName] = tiles.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retile all screens.
|
||||||
|
function retileAll() {
|
||||||
|
for (const name in screenTiles) {
|
||||||
|
retileScreen(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a window to its screen's tile list and retile.
|
||||||
|
function addWindow(win) {
|
||||||
|
if (!isTileable(win)) return;
|
||||||
|
|
||||||
|
const output = win.output;
|
||||||
|
if (!output) return;
|
||||||
|
const name = output.name;
|
||||||
|
|
||||||
|
if (!screenTiles[name]) {
|
||||||
|
screenTiles[name] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid duplicates
|
||||||
|
const tiles = screenTiles[name];
|
||||||
|
for (let i = 0; i < tiles.length; i++) {
|
||||||
|
if (tiles[i].win.internalId === win.internalId) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles.push({ win: win, rect: Qt.rect(0, 0, 0, 0) });
|
||||||
|
screenTiles[name] = tiles;
|
||||||
|
|
||||||
|
// Un-maximize now so the exclusive-zone Wayland roundtrip begins;
|
||||||
|
// retileScreen runs 200 ms later when MaximizeArea has settled.
|
||||||
|
// (Same pattern as convergentwindows constrainAfterRestoreTimer.)
|
||||||
|
if (win.maximizable) win.setMaximize(false, false);
|
||||||
|
win.noBorder = false;
|
||||||
|
scheduleRetile(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a window from its screen's tile list and retile.
|
||||||
|
function removeWindow(win) {
|
||||||
|
if (!win) return;
|
||||||
|
const output = win.output;
|
||||||
|
const name = output ? output.name : null;
|
||||||
|
|
||||||
|
// Search all screens (window may have been moved)
|
||||||
|
for (const sName in screenTiles) {
|
||||||
|
const tiles = screenTiles[sName];
|
||||||
|
for (let i = 0; i < tiles.length; i++) {
|
||||||
|
if (tiles[i].win.internalId === win.internalId) {
|
||||||
|
tiles.splice(i, 1);
|
||||||
|
screenTiles[sName] = tiles;
|
||||||
|
retileScreen(sName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Keyboard navigation helpers ──────────────────────────────────────────
|
||||||
|
|
||||||
|
function centreOf(rect) {
|
||||||
|
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the tile on-screen whose centre is most in `direction` from `fromRect`.
|
||||||
|
// direction: "left"|"right"|"up"|"down"
|
||||||
|
function findNeighbour(fromWin, direction) {
|
||||||
|
const outputName = fromWin.output ? fromWin.output.name : null;
|
||||||
|
if (!outputName) return null;
|
||||||
|
const tiles = screenTiles[outputName];
|
||||||
|
if (!tiles) return null;
|
||||||
|
|
||||||
|
const from = fromWin.frameGeometry;
|
||||||
|
const fc = centreOf(from);
|
||||||
|
let best = null;
|
||||||
|
let bestScore = Infinity;
|
||||||
|
|
||||||
|
for (let i = 0; i < tiles.length; i++) {
|
||||||
|
const t = tiles[i];
|
||||||
|
if (t.win.internalId === fromWin.internalId) continue;
|
||||||
|
const tc = centreOf(t.rect);
|
||||||
|
const dx = tc.x - fc.x;
|
||||||
|
const dy = tc.y - fc.y;
|
||||||
|
|
||||||
|
let inDirection = false;
|
||||||
|
let primary = 0;
|
||||||
|
let secondary = 0;
|
||||||
|
switch (direction) {
|
||||||
|
case "left": inDirection = dx < -5; primary = -dx; secondary = Math.abs(dy); break;
|
||||||
|
case "right": inDirection = dx > 5; primary = dx; secondary = Math.abs(dy); break;
|
||||||
|
case "up": inDirection = dy < -5; primary = -dy; secondary = Math.abs(dx); break;
|
||||||
|
case "down": inDirection = dy > 5; primary = dy; secondary = Math.abs(dx); break;
|
||||||
|
}
|
||||||
|
if (!inDirection) continue;
|
||||||
|
// Score: penalise perpendicular distance lightly
|
||||||
|
const score = primary + secondary * 0.3;
|
||||||
|
if (score < bestScore) { bestScore = score; best = t.win; }
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Workspace connections ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: KWinComponents.Workspace
|
||||||
|
|
||||||
|
function onWindowAdded(win) {
|
||||||
|
if (isTileable(win)) {
|
||||||
|
addWindow(win);
|
||||||
|
win.interactiveMoveResizeStarted.connect(function() { root.onDragStart(win); });
|
||||||
|
win.interactiveMoveResizeStepped.connect(function(geo) { root.onDragStep(win, geo); });
|
||||||
|
win.interactiveMoveResizeFinished.connect(function() { root.onDragEnd(win); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWindowRemoved(win) {
|
||||||
|
root.removeWindow(win);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: ShellSettings.Settings
|
||||||
|
|
||||||
|
function onConvergenceModeEnabledChanged() {
|
||||||
|
if (isConvergence()) {
|
||||||
|
// Tile all existing normal windows
|
||||||
|
const wins = KWinComponents.Workspace.windows;
|
||||||
|
for (let i = 0; i < wins.length; i++) {
|
||||||
|
addWindow(wins[i]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Clear all tiles — the convergentwindows script will re-maximize
|
||||||
|
screenTiles = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGamingModeEnabledChanged() {
|
||||||
|
if (ShellSettings.Settings.gamingModeEnabled) {
|
||||||
|
screenTiles = {};
|
||||||
|
} else if (isConvergence()) {
|
||||||
|
const wins = KWinComponents.Workspace.windows;
|
||||||
|
for (let i = 0; i < wins.length; i++) {
|
||||||
|
addWindow(wins[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDynamicTilingEnabledChanged() {
|
||||||
|
if (isConvergence()) {
|
||||||
|
const wins = KWinComponents.Workspace.windows;
|
||||||
|
for (let i = 0; i < wins.length; i++) {
|
||||||
|
addWindow(wins[i]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Tiling turned off — leave windows where they are.
|
||||||
|
screenTiles = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Drag handlers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Find the (screen, index) of an existing tile holding this window.
|
||||||
|
function findTileSlot(win) {
|
||||||
|
for (const sName in screenTiles) {
|
||||||
|
const tiles = screenTiles[sName];
|
||||||
|
for (let i = 0; i < tiles.length; i++) {
|
||||||
|
if (tiles[i].win && tiles[i].win.internalId === win.internalId) {
|
||||||
|
return { screen: sName, index: i };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the tile under a cursor position, ignoring the dragged window.
|
||||||
|
function findTileAtCursor(cursor, ignoreWin) {
|
||||||
|
for (const sName in screenTiles) {
|
||||||
|
const tiles = screenTiles[sName];
|
||||||
|
for (let i = 0; i < tiles.length; i++) {
|
||||||
|
const t = tiles[i];
|
||||||
|
if (ignoreWin && t.win && t.win.internalId === ignoreWin.internalId) continue;
|
||||||
|
const r = t.rect;
|
||||||
|
if (!r || r.width <= 0 || r.height <= 0) continue;
|
||||||
|
if (cursor.x >= r.x && cursor.x <= r.x + r.width &&
|
||||||
|
cursor.y >= r.y && cursor.y <= r.y + r.height) {
|
||||||
|
return { screen: sName, index: i, rect: r };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragStart(win) {
|
||||||
|
if (!isConvergence()) return;
|
||||||
|
draggingWindow = win;
|
||||||
|
swapOutlineActive = false;
|
||||||
|
dragSwapTarget = null;
|
||||||
|
|
||||||
|
// Remember the source slot so we can swap on drop.
|
||||||
|
// The tile stays in screenTiles[] during the drag so the rest of
|
||||||
|
// the layout doesn't shuffle.
|
||||||
|
const slot = findTileSlot(win);
|
||||||
|
if (slot) {
|
||||||
|
dragSourceScreen = slot.screen;
|
||||||
|
dragSourceIndex = slot.index;
|
||||||
|
} else {
|
||||||
|
dragSourceScreen = "";
|
||||||
|
dragSourceIndex = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragStep(win, geo) {
|
||||||
|
if (!isConvergence()) return;
|
||||||
|
if (draggingWindow !== win) return;
|
||||||
|
|
||||||
|
// Only show an outline when the cursor is over another tile —
|
||||||
|
// a clear visual hint that "drop here = swap".
|
||||||
|
const cursor = KWinComponents.Workspace.cursorPos;
|
||||||
|
const target = findTileAtCursor(cursor, win);
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
if (!dragSwapTarget ||
|
||||||
|
dragSwapTarget.screen !== target.screen ||
|
||||||
|
dragSwapTarget.index !== target.index) {
|
||||||
|
dragSwapTarget = target;
|
||||||
|
KWinComponents.Workspace.showOutline(target.rect);
|
||||||
|
swapOutlineActive = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dragSwapTarget = null;
|
||||||
|
if (swapOutlineActive) {
|
||||||
|
KWinComponents.Workspace.hideOutline();
|
||||||
|
swapOutlineActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragEnd(win) {
|
||||||
|
if (!isConvergence()) return;
|
||||||
|
if (swapOutlineActive) {
|
||||||
|
KWinComponents.Workspace.hideOutline();
|
||||||
|
swapOutlineActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropped on another tile → swap source/target slots.
|
||||||
|
if (dragSwapTarget && dragSourceScreen && dragSourceIndex >= 0) {
|
||||||
|
const sScreen = dragSourceScreen;
|
||||||
|
const sIdx = dragSourceIndex;
|
||||||
|
const tScreen = dragSwapTarget.screen;
|
||||||
|
const tIdx = dragSwapTarget.index;
|
||||||
|
|
||||||
|
const sTiles = screenTiles[sScreen];
|
||||||
|
const tTiles = screenTiles[tScreen];
|
||||||
|
if (sTiles && tTiles && sTiles[sIdx] && tTiles[tIdx]) {
|
||||||
|
const a = sTiles[sIdx];
|
||||||
|
const b = tTiles[tIdx];
|
||||||
|
if (sScreen === tScreen) {
|
||||||
|
sTiles[sIdx] = b;
|
||||||
|
sTiles[tIdx] = a;
|
||||||
|
screenTiles[sScreen] = sTiles.slice();
|
||||||
|
retileScreen(sScreen);
|
||||||
|
} else {
|
||||||
|
sTiles[sIdx] = b;
|
||||||
|
tTiles[tIdx] = a;
|
||||||
|
screenTiles[sScreen] = sTiles.slice();
|
||||||
|
screenTiles[tScreen] = tTiles.slice();
|
||||||
|
retileScreen(sScreen);
|
||||||
|
retileScreen(tScreen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Dropped elsewhere → restore the source tile to its original slot.
|
||||||
|
// (KWin's native quick-tile may have moved the window; retileScreen
|
||||||
|
// sets frameGeometry back to the BSP rect so the layout stays intact.)
|
||||||
|
else if (dragSourceScreen && dragSourceIndex >= 0) {
|
||||||
|
retileScreen(dragSourceScreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragSwapTarget = null;
|
||||||
|
dragSourceScreen = "";
|
||||||
|
dragSourceIndex = -1;
|
||||||
|
draggingWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Keyboard shortcuts ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Focus navigation
|
||||||
|
KWinComponents.ShortcutHandler {
|
||||||
|
name: "SHIFT Tiling Focus Left"
|
||||||
|
text: "SHIFT Tiling: Focus window to the left"
|
||||||
|
sequence: "Meta+H"
|
||||||
|
onActivated: {
|
||||||
|
const win = KWinComponents.Workspace.activeWindow;
|
||||||
|
if (!win) return;
|
||||||
|
const target = root.findNeighbour(win, "left");
|
||||||
|
if (target) KWinComponents.Workspace.activeWindow = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KWinComponents.ShortcutHandler {
|
||||||
|
name: "SHIFT Tiling Focus Right"
|
||||||
|
text: "SHIFT Tiling: Focus window to the right"
|
||||||
|
sequence: "Meta+L"
|
||||||
|
onActivated: {
|
||||||
|
const win = KWinComponents.Workspace.activeWindow;
|
||||||
|
if (!win) return;
|
||||||
|
const target = root.findNeighbour(win, "right");
|
||||||
|
if (target) KWinComponents.Workspace.activeWindow = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KWinComponents.ShortcutHandler {
|
||||||
|
name: "SHIFT Tiling Focus Up"
|
||||||
|
text: "SHIFT Tiling: Focus window above"
|
||||||
|
sequence: "Meta+K"
|
||||||
|
onActivated: {
|
||||||
|
const win = KWinComponents.Workspace.activeWindow;
|
||||||
|
if (!win) return;
|
||||||
|
const target = root.findNeighbour(win, "up");
|
||||||
|
if (target) KWinComponents.Workspace.activeWindow = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KWinComponents.ShortcutHandler {
|
||||||
|
name: "SHIFT Tiling Focus Down"
|
||||||
|
text: "SHIFT Tiling: Focus window below"
|
||||||
|
sequence: "Meta+J"
|
||||||
|
onActivated: {
|
||||||
|
const win = KWinComponents.Workspace.activeWindow;
|
||||||
|
if (!win) return;
|
||||||
|
const target = root.findNeighbour(win, "down");
|
||||||
|
if (target) KWinComponents.Workspace.activeWindow = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float toggle
|
||||||
|
KWinComponents.ShortcutHandler {
|
||||||
|
name: "SHIFT Tiling Float Toggle"
|
||||||
|
text: "SHIFT Tiling: Toggle float for active window"
|
||||||
|
sequence: "Meta+F"
|
||||||
|
onActivated: {
|
||||||
|
const win = KWinComponents.Workspace.activeWindow;
|
||||||
|
if (!win) return;
|
||||||
|
const id = win.internalId;
|
||||||
|
if (root.floatedWindows[id]) {
|
||||||
|
delete root.floatedWindows[id];
|
||||||
|
root.addWindow(win);
|
||||||
|
} else {
|
||||||
|
root.floatedWindows[id] = true;
|
||||||
|
root.removeWindow(win);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tiling on/off
|
||||||
|
KWinComponents.ShortcutHandler {
|
||||||
|
name: "SHIFT Tiling Toggle"
|
||||||
|
text: "SHIFT Tiling: Toggle tiling on/off"
|
||||||
|
sequence: "Meta+T"
|
||||||
|
onActivated: {
|
||||||
|
root.tilingEnabled = !root.tilingEnabled;
|
||||||
|
if (root.tilingEnabled) {
|
||||||
|
const wins = KWinComponents.Workspace.windows;
|
||||||
|
for (let i = 0; i < wins.length; i++) root.addWindow(wins[i]);
|
||||||
|
} else {
|
||||||
|
root.screenTiles = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Right-click menu ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Note: registerUserActionsMenu is a global function in KWin JS scripts.
|
||||||
|
// In declarative QML scripts it is exposed via the KWin global object.
|
||||||
|
// We wire it up after the component is complete.
|
||||||
|
Component.onCompleted: {
|
||||||
|
// Connect to existing windows
|
||||||
|
const wins = KWinComponents.Workspace.windows;
|
||||||
|
for (let i = 0; i < wins.length; i++) {
|
||||||
|
const win = wins[i];
|
||||||
|
if (isTileable(win)) {
|
||||||
|
addWindow(win);
|
||||||
|
win.interactiveMoveResizeStarted.connect(function() { root.onDragStart(win); });
|
||||||
|
win.interactiveMoveResizeStepped.connect(function(geo) { root.onDragStep(win, geo); });
|
||||||
|
win.interactiveMoveResizeFinished.connect(function() { root.onDragEnd(win); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
kwin/scripts/shift-tiling/metadata.json
Normal file
20
kwin/scripts/shift-tiling/metadata.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"KPackageStructure": "KWin/Script",
|
||||||
|
"KPlugin": {
|
||||||
|
"Authors": [
|
||||||
|
{
|
||||||
|
"Email": "marcoa@example.com",
|
||||||
|
"Name": "Marco Allegretti"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Description": "Dynamic tiling for SHIFT convergence mode. Automatically tiles windows using a BSP layout, with snap zones on drag and keyboard navigation.",
|
||||||
|
"EnabledByDefault": false,
|
||||||
|
"Id": "shift-tiling",
|
||||||
|
"License": "EUPL-1.2",
|
||||||
|
"Name": "SHIFT Dynamic Tiling",
|
||||||
|
"Version": "1.0"
|
||||||
|
},
|
||||||
|
"X-KDE-ConfigModule": "kwin/effects/configs/kcm_kwin4_genericscripted",
|
||||||
|
"X-Plasma-API": "declarativescript",
|
||||||
|
"X-Plasma-MainScript": "ui/main.qml"
|
||||||
|
}
|
||||||
|
|
@ -3,4 +3,4 @@
|
||||||
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
|
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_lookandfeel_org.kde.breeze.mobile.pot
|
$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_lookandfeel_org.shift.mobile.pot
|
||||||
|
|
|
||||||
|
|
@ -18,15 +18,15 @@ Theme=breeze-dark
|
||||||
name=default
|
name=default
|
||||||
|
|
||||||
[Wallpaper]
|
[Wallpaper]
|
||||||
Image=Next
|
Image=SHIFT
|
||||||
|
|
||||||
# Set default cursor theme
|
# Set default cursor theme
|
||||||
[kcminputrc][Mouse]
|
[kcminputrc][Mouse]
|
||||||
cursorTheme=breeze_cursors
|
cursorTheme=breeze_cursors
|
||||||
|
|
||||||
[kwinrc][org.kde.kdecoration2]
|
[kwinrc][org.kde.kdecoration2]
|
||||||
library=org.kde.breeze
|
library=org.kde.kwin.aurorae
|
||||||
theme=Shift Dark
|
theme=org.shift.decoration
|
||||||
|
|
||||||
[KSplash]
|
[KSplash]
|
||||||
Theme=org.kde.Breeze
|
Theme=org.kde.Breeze
|
||||||
|
|
|
||||||
|
|
@ -3,119 +3,14 @@
|
||||||
"KPlugin": {
|
"KPlugin": {
|
||||||
"Authors": [
|
"Authors": [
|
||||||
{
|
{
|
||||||
"Email": "plasma-mobile@kde.org",
|
"Name": "SHIFT Contributors"
|
||||||
"Name": "KDE Visual Design Group",
|
|
||||||
"Name[ar]": "مجموعة التصميم المرئي لكِيدِي",
|
|
||||||
"Name[ca@valencia]": "Grup de disseny visual de KDE",
|
|
||||||
"Name[ca]": "Grup de disseny visual de KDE",
|
|
||||||
"Name[cs]": "Skupina vizuálního návrhu KDE",
|
|
||||||
"Name[de]": "KDE Visual Design Group",
|
|
||||||
"Name[en_GB]": "KDE Visual Design Group",
|
|
||||||
"Name[eo]": "KDE Vida Dezajna Grupo",
|
|
||||||
"Name[es]": "El grupo de diseño visual de KDE",
|
|
||||||
"Name[eu]": "KDE Ikus-diseinu taldea",
|
|
||||||
"Name[fi]": "KDE:n visuaalinen suunnitteluryhmä",
|
|
||||||
"Name[fr]": "Groupe de conception graphique de KDE",
|
|
||||||
"Name[gl]": "Grupo de deseño visual de KDE",
|
|
||||||
"Name[he]": "קבוצת העיצוב החזותי של KDE",
|
|
||||||
"Name[hu]": "KDE Visual Design Group",
|
|
||||||
"Name[ia]": "KDE Visual Design Group (Gruppo de Designo Visual de KDE)",
|
|
||||||
"Name[is]": "KDE-hópurinn í myndrænni hönnun",
|
|
||||||
"Name[it]": "KDE Visual Design Group",
|
|
||||||
"Name[ka]": "KDE-ის ვიზუალური დიზაინის ჯგუფი",
|
|
||||||
"Name[ko]": "KDE 시각 디자인 그룹",
|
|
||||||
"Name[lv]": "KDE vizuālā dizaina grupa",
|
|
||||||
"Name[nl]": "KDE Visuele ontwerpgroep",
|
|
||||||
"Name[nn]": "KDE Visual Design Group",
|
|
||||||
"Name[pa]": "KDE ਦਿੱਖ ਡਿਜ਼ਾਇਨ ਗਰੁੱਪ",
|
|
||||||
"Name[pl]": "Grupa oprawy graficznej KDE",
|
|
||||||
"Name[pt_BR]": "Grupo de design visual do KDE",
|
|
||||||
"Name[ru]": "Группа по визуальному дизайну KDE",
|
|
||||||
"Name[sa]": "KDE दृश्य डिजाइन समूह",
|
|
||||||
"Name[sk]": "KDE Visual Design Group",
|
|
||||||
"Name[sl]": "KDE Visual Design Group",
|
|
||||||
"Name[sv]": "KDE:s visuella designgrupp",
|
|
||||||
"Name[tr]": "KDE Görsel Tasarım Grubu",
|
|
||||||
"Name[uk]": "Група з візуального дизайну KDE",
|
|
||||||
"Name[zh_CN]": "KDE 视觉设计团队",
|
|
||||||
"Name[zh_TW]": "KDE VDG 視覺設計組"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Category": "",
|
"Category": "",
|
||||||
"Description": "Plasma for mobile devices",
|
"Description": "SHIFT for mobile and convergent devices",
|
||||||
"Description[ar]": "بلازما للأجهزة الجوالة",
|
"Id": "org.shift.mobile",
|
||||||
"Description[ca@valencia]": "Plasma per a dispositius mòbils",
|
|
||||||
"Description[ca]": "Plasma per a dispositius mòbils",
|
|
||||||
"Description[cs]": "Plasma pro mobilní telefony",
|
|
||||||
"Description[de]": "Plasma-Shell für Mobilgeräte",
|
|
||||||
"Description[en_GB]": "Plasma for mobile devices",
|
|
||||||
"Description[eo]": "Plasmo por porteblaj aparatoj",
|
|
||||||
"Description[es]": "Plasma para dispositivos móviles",
|
|
||||||
"Description[eu]": "Plasma gailu mugikorretarako",
|
|
||||||
"Description[fi]": "Plasma mobiililaitteille",
|
|
||||||
"Description[fr]": "Plasma pour périphériques mobiles",
|
|
||||||
"Description[gl]": "Plasma para dispositivos móbiles.",
|
|
||||||
"Description[he]": "פלזמה למכשירים ניידים",
|
|
||||||
"Description[hu]": "Plasma mobileszközökhöz",
|
|
||||||
"Description[ia]": "Plasma per dispositivos mobile",
|
|
||||||
"Description[is]": "Plasma fyrir farsíma",
|
|
||||||
"Description[it]": "Plasma per dispositivi mobili",
|
|
||||||
"Description[ka]": "Plasma მობილური მოწყობილობებისთვის",
|
|
||||||
"Description[ko]": "모바일 장치용 Plasma",
|
|
||||||
"Description[lv]": "„Plasma“ mobilajām ierīcēm",
|
|
||||||
"Description[nl]": "Plasma voor mobiele apparaten",
|
|
||||||
"Description[nn]": "Plasma for telefonar og nettbrett",
|
|
||||||
"Description[pa]": "ਮੋਬਾਈਲ ਡਿਵਾਈਸ ਲਈ ਪਲਾਜ਼ਮਾ",
|
|
||||||
"Description[pl]": "Plazma dla urządzeń przenośnych",
|
|
||||||
"Description[pt_BR]": "Plasma para dispositivos móveis",
|
|
||||||
"Description[ru]": "Plasma для мобильных устройств",
|
|
||||||
"Description[sa]": "चलयन्त्राणां कृते प्लाज्मा",
|
|
||||||
"Description[sk]": "Plasma pre mobilné zariadenia",
|
|
||||||
"Description[sl]": "Plasma za mobilne naprave",
|
|
||||||
"Description[sv]": "Plasma för mobilapparater",
|
|
||||||
"Description[ta]": "கைபேசிகளுக்கான பிளாஸ்மா",
|
|
||||||
"Description[tr]": "Taşınabilir aygıtlar için Plasma",
|
|
||||||
"Description[uk]": "Плазма для мобільних пристроїв",
|
|
||||||
"Description[zh_CN]": "针对手机设备设计的 Plasma 环境",
|
|
||||||
"Description[zh_TW]": "適用於行動裝置的 Plasma",
|
|
||||||
"Id": "org.kde.breeze.mobile",
|
|
||||||
"License": "GPLv2+",
|
"License": "GPLv2+",
|
||||||
"Name": "Plasma Mobile Breeze",
|
"Name": "SHIFT Mobile"
|
||||||
"Name[ar]": "نسيم بلازما الجوال",
|
|
||||||
"Name[ca@valencia]": "Plasma Mobile Brisa",
|
|
||||||
"Name[ca]": "Plasma Mobile Brisa",
|
|
||||||
"Name[cs]": "Plasma Mobile Breeze",
|
|
||||||
"Name[de]": "Plasma Mobile Breeze",
|
|
||||||
"Name[en_GB]": "Plasma Mobile Breeze",
|
|
||||||
"Name[eo]": "Plasma-Poŝtelefona Breeze",
|
|
||||||
"Name[es]": "Brisa de Plasma Mobile",
|
|
||||||
"Name[eu]": "Plasma Mugikorra Breeze",
|
|
||||||
"Name[fi]": "Plasma Mobilen Breeze",
|
|
||||||
"Name[fr]": "Thème « Breeze » pour Plasma Mobile",
|
|
||||||
"Name[gl]": "Brisa para Plasma Mobile",
|
|
||||||
"Name[he]": "בריזה לפלזמה לניידים",
|
|
||||||
"Name[hu]": "Plasma Mobile Breeze",
|
|
||||||
"Name[ia]": "Breeze de Plasma Mobile",
|
|
||||||
"Name[is]": "Plasma Mobile Breeze",
|
|
||||||
"Name[it]": "Brezza per Plasma Mobile",
|
|
||||||
"Name[ka]": "Plasma მობილური ნიავი",
|
|
||||||
"Name[ko]": "Plasma 모바일 Breeze",
|
|
||||||
"Name[lv]": "„Plasma Mobile“ „Breeze“ motīvs",
|
|
||||||
"Name[nl]": "Plasma Mobile Breeze",
|
|
||||||
"Name[nn]": "Plasma Mobile Breeze",
|
|
||||||
"Name[pa]": "ਪਲਾਜ਼ਮਾ ਮੋਬਾਈਲ ਬਰੀਜ਼",
|
|
||||||
"Name[pl]": "Bryza Przenośnej Plazmy",
|
|
||||||
"Name[pt_BR]": "Breeze do Plasma Mobile",
|
|
||||||
"Name[ru]": "Breeze для Plasma Mobile",
|
|
||||||
"Name[sa]": "प्लाज्मा चल हवा",
|
|
||||||
"Name[sk]": "Vánok Plasma Mobile",
|
|
||||||
"Name[sl]": "Plasma Mobile Sapica",
|
|
||||||
"Name[sv]": "Plasma mobil Breeze",
|
|
||||||
"Name[tr]": "Plasma Cep Esinti",
|
|
||||||
"Name[uk]": "Breeze для мобільної Плазми",
|
|
||||||
"Name[zh_CN]": "Plasma Mobile Breeze 微风",
|
|
||||||
"Name[zh_TW]": "Plasma 行動 Breeze",
|
|
||||||
"Website": "https://www.kde.org"
|
|
||||||
},
|
},
|
||||||
"Keywords": "Phone;Mobile;Desktop;Workspace;Appearance;Look and Feel;Logout;Lock;Suspend;Shutdown;Hibernate;",
|
"Keywords": "Phone;Mobile;Desktop;Workspace;Appearance;Look and Feel;Logout;Lock;Suspend;Shutdown;Hibernate;",
|
||||||
"X-Plasma-APIVersion": "2"
|
"X-Plasma-APIVersion": "2"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ plasma_install_package(bluetooth org.kde.plasma.quicksetting.bluetooth quicksett
|
||||||
plasma_install_package(caffeine org.kde.plasma.quicksetting.caffeine quicksettings)
|
plasma_install_package(caffeine org.kde.plasma.quicksetting.caffeine quicksettings)
|
||||||
plasma_install_package(docked org.kde.plasma.quicksetting.docked quicksettings)
|
plasma_install_package(docked org.kde.plasma.quicksetting.docked quicksettings)
|
||||||
plasma_install_package(donotdisturb org.kde.plasma.quicksetting.donotdisturb quicksettings)
|
plasma_install_package(donotdisturb org.kde.plasma.quicksetting.donotdisturb quicksettings)
|
||||||
|
plasma_install_package(dynamictiling org.kde.plasma.quicksetting.dynamictiling quicksettings)
|
||||||
plasma_install_package(gaming org.kde.plasma.quicksetting.gaming quicksettings)
|
plasma_install_package(gaming org.kde.plasma.quicksetting.gaming quicksettings)
|
||||||
plasma_install_package(gaminghint org.kde.plasma.quicksetting.gaminghint quicksettings)
|
plasma_install_package(gaminghint org.kde.plasma.quicksetting.gaminghint quicksettings)
|
||||||
plasma_install_package(autohidepanels org.kde.plasma.quicksetting.autohidepanels quicksettings)
|
plasma_install_package(autohidepanels org.kde.plasma.quicksetting.autohidepanels quicksettings)
|
||||||
|
|
|
||||||
23
quicksettings/dynamictiling/contents/ui/main.qml
Normal file
23
quicksettings/dynamictiling/contents/ui/main.qml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||||
|
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
|
||||||
|
|
||||||
|
QS.QuickSetting {
|
||||||
|
text: i18n("Dynamic Tiling")
|
||||||
|
icon: "view-grid-symbolic"
|
||||||
|
|
||||||
|
// Only meaningful in convergence (desktop) mode. Hidden everywhere else.
|
||||||
|
available: ShellSettings.Settings.convergenceModeEnabled
|
||||||
|
&& !ShellSettings.Settings.gamingModeEnabled
|
||||||
|
|
||||||
|
enabled: ShellSettings.Settings.dynamicTilingEnabled
|
||||||
|
status: enabled ? i18n("On") : i18n("Off")
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
ShellSettings.Settings.dynamicTilingEnabled = !ShellSettings.Settings.dynamicTilingEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
quicksettings/dynamictiling/metadata.json
Normal file
17
quicksettings/dynamictiling/metadata.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"KPackageStructure": "KPackage/GenericQML",
|
||||||
|
"KPlugin": {
|
||||||
|
"Authors": [
|
||||||
|
{
|
||||||
|
"Email": "marco@lateuf.it",
|
||||||
|
"Name": "Marco Allegretti"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Description": "Quick setting to toggle SHIFT dynamic (BSP) window tiling in convergence mode",
|
||||||
|
"Icon": "view-grid-symbolic",
|
||||||
|
"Id": "org.kde.plasma.quicksetting.dynamictiling",
|
||||||
|
"License": "LGPL",
|
||||||
|
"Name": "Dynamic Tiling",
|
||||||
|
"Website": "https://kde.org"
|
||||||
|
}
|
||||||
|
}
|
||||||
24
shell/contents/layout.js.in
Normal file
24
shell/contents/layout.js.in
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
// SPDX-FileCopyrightText: 2014 Aaron J. Seigo
|
||||||
|
// SPDX-FileCopyrightText: 2014-2019 Marco Martin <mart@kde.org>
|
||||||
|
// SPDX-FileCopyrightText: 2015-2021 Bhushan Shah <bshah@kde.org>
|
||||||
|
// SPDX-FileCopyrightText: 2021 Aleix Pol <apol@kde.org>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
// Load panel layout
|
||||||
|
loadTemplate("org.kde.plasma.mobile.defaultNavigationPanel");
|
||||||
|
loadTemplate("org.kde.plasma.mobile.defaultStatusBar");
|
||||||
|
|
||||||
|
// Set wallpaper plugin
|
||||||
|
var shiftDefaultWallpaperUrl = "@SHIFT_DEFAULT_WALLPAPER_URL@";
|
||||||
|
var desktopsArray = desktopsForActivity(currentActivity());
|
||||||
|
for (var j = 0; j < desktopsArray.length; j++) {
|
||||||
|
desktopsArray[j].wallpaperPlugin = "org.kde.image";
|
||||||
|
desktopsArray[j].currentConfigGroup = ["Wallpaper", "org.kde.image", "General"];
|
||||||
|
desktopsArray[j].writeConfig("Image", shiftDefaultWallpaperUrl);
|
||||||
|
|
||||||
|
if (j == 0) {
|
||||||
|
// Add meta shortcut
|
||||||
|
desktopsArray[0].currentConfigGroup = ["Shortcuts"]
|
||||||
|
desktopsArray[0].writeConfig("global", "Meta+F1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,3 +2,11 @@
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
add_subdirectory(notificationtest)
|
add_subdirectory(notificationtest)
|
||||||
|
|
||||||
|
find_program(BASH_EXECUTABLE bash)
|
||||||
|
if(BASH_EXECUTABLE)
|
||||||
|
add_test(
|
||||||
|
NAME convergence-dock-invariant
|
||||||
|
COMMAND ${BASH_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/check-convergence-dock-invariant.sh
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,5 @@ Be sure to have the project installed on the system.
|
||||||
To test the power menu, the following command can be used to invoke `ksmserver-logout-greeter` with the mobile look and feel in a window:
|
To test the power menu, the following command can be used to invoke `ksmserver-logout-greeter` with the mobile look and feel in a window:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
~/kde/usr/lib/libexec/ksmserver-logout-greeter --windowed --lookandfeel org.kde.breeze.mobile
|
~/kde/usr/lib/libexec/ksmserver-logout-greeter --windowed --lookandfeel org.shift.mobile
|
||||||
```
|
```
|
||||||
|
|
|
||||||
39
tests/check-convergence-dock-invariant.sh
Normal file
39
tests/check-convergence-dock-invariant.sh
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-FileCopyrightText: 2026 Marco Allegretti <mightymarco4@gmail.com>
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
constants="$repo_root/components/mobileshell/qml/components/Constants.qml"
|
||||||
|
taskpanel="$repo_root/containments/taskpanel/qml/main.qml"
|
||||||
|
folio_main="$repo_root/containments/homescreens/folio/qml/main.qml"
|
||||||
|
folio_home="$repo_root/containments/homescreens/folio/qml/FolioHomeScreen.qml"
|
||||||
|
|
||||||
|
require_line() {
|
||||||
|
local file="$1"
|
||||||
|
local needle="$2"
|
||||||
|
|
||||||
|
if ! grep -Fq "$needle" "$file"; then
|
||||||
|
echo "Missing invariant in ${file#$repo_root/}: $needle" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_line "$constants" "readonly property real convergenceDockHeight: Kirigami.Units.gridUnit * 3"
|
||||||
|
require_line "$constants" "readonly property real convergenceDockRevealHeight: Kirigami.Units.gridUnit"
|
||||||
|
|
||||||
|
require_line "$taskpanel" "height: MobileShell.Constants.convergenceDockHeight"
|
||||||
|
require_line "$taskpanel" "LayerShell.Window.exclusionZone: MobileShell.Constants.convergenceDockHeight"
|
||||||
|
|
||||||
|
require_line "$folio_main" "height: MobileShell.Constants.convergenceDockHeight"
|
||||||
|
require_line "$folio_main" "readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight"
|
||||||
|
require_line "$folio_main" "readonly property real revealStripHeight: MobileShell.Constants.convergenceDockRevealHeight"
|
||||||
|
require_line "$folio_home" "height: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6"
|
||||||
|
|
||||||
|
dock_offset_transforms="$(grep -F "transform: Translate { y: dockOverlay.dockOffset }" "$folio_main" | wc -l)"
|
||||||
|
if [[ "$dock_offset_transforms" -ne 1 ]]; then
|
||||||
|
echo "Expected only dock contents to slide with dockOverlay.dockOffset; found $dock_offset_transforms transforms" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
7
wallpapers/CMakeLists.txt
Normal file
7
wallpapers/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# SPDX-FileCopyrightText: 2026 SHIFT Contributors
|
||||||
|
# SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
install(DIRECTORY SHIFT
|
||||||
|
DESTINATION ${KDE_INSTALL_WALLPAPERDIR}
|
||||||
|
PATTERN "*.license" EXCLUDE
|
||||||
|
)
|
||||||
BIN
wallpapers/SHIFT/contents/images/5120x2880.png
Normal file
BIN
wallpapers/SHIFT/contents/images/5120x2880.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 246 KiB |
2
wallpapers/SHIFT/contents/images/5120x2880.png.license
Normal file
2
wallpapers/SHIFT/contents/images/5120x2880.png.license
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
SPDX-FileCopyrightText: 2026 Luiza Balaur
|
||||||
|
SPDX-License-Identifier: EUPL-1.2
|
||||||
BIN
wallpapers/SHIFT/contents/images_dark/5120x2880.png
Normal file
BIN
wallpapers/SHIFT/contents/images_dark/5120x2880.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 236 KiB |
|
|
@ -0,0 +1,2 @@
|
||||||
|
SPDX-FileCopyrightText: 2026 Luiza Balaur
|
||||||
|
SPDX-License-Identifier: EUPL-1.2
|
||||||
12
wallpapers/SHIFT/metadata.json
Normal file
12
wallpapers/SHIFT/metadata.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"KPlugin": {
|
||||||
|
"Authors": [
|
||||||
|
{
|
||||||
|
"Name": "Luiza Balaur"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Id": "SHIFT",
|
||||||
|
"License": "EUPL-1.2",
|
||||||
|
"Name": "SHIFT Light / SHIFT Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
2
wallpapers/SHIFT/metadata.json.license
Normal file
2
wallpapers/SHIFT/metadata.json.license
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
SPDX-FileCopyrightText: 2026 Luiza Balaur
|
||||||
|
SPDX-License-Identifier: EUPL-1.2
|
||||||
Loading…
Reference in a new issue