mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-06-11 00:47:22 +00:00
Animate dynamic tiling window moves
Add a KWin JavaScript effect for compositor-side size and translation transitions, and enable it in convergence mode. Keep the tile preview effect as an outline driver so nested preview does not activate a SceneEffect surface.
This commit is contained in:
parent
495f76900f
commit
f09a1db84a
6 changed files with 221 additions and 118 deletions
|
|
@ -54,15 +54,14 @@ QMap<QString, QMap<QString, QVariant>> getKwinrcSettings(KSharedConfig::Ptr m_mo
|
|||
{"ElectricBorderTiling", convergenceModeEnabled} // drag to left/right edges to tile half-screen in convergence mode
|
||||
}},
|
||||
{"Plugins",
|
||||
{
|
||||
{"blurEnabled", false}, // disable blur for performance reasons, we could reconsider in the future for more powerful devices
|
||||
{{"blurEnabled", false}, // disable blur for performance reasons, we could reconsider in the future for more powerful devices
|
||||
{"convergentwindowsEnabled", true}, // enable our convergent window plugin
|
||||
{"mobiletaskswitcherEnabled", !convergenceModeEnabled}, // mobile task switcher on phone only; convergence uses standard Alt-Tab tabbox
|
||||
{"overviewEnabled", convergenceModeEnabled}, // enable KWin Overview effect in convergence mode for desktop-style task switching
|
||||
{"screenedgeEnabled", convergenceModeEnabled}, // enable screen edge visual feedback in convergence mode (mouse hot corners)
|
||||
{"shift-tile-animationsEnabled", convergenceModeEnabled},
|
||||
{"shift-tilingEnabled", convergenceModeEnabled},
|
||||
{"shift-tile-previewEnabled", convergenceModeEnabled}
|
||||
}},
|
||||
{"shift-tile-previewEnabled", convergenceModeEnabled}}},
|
||||
{"Wayland",
|
||||
{
|
||||
{"VirtualKeyboardEnabled", true} // enable vkbd
|
||||
|
|
@ -80,7 +79,7 @@ QMap<QString, QMap<QString, QVariant>> getKwinrcSettings(KSharedConfig::Ptr m_mo
|
|||
|
||||
// Have a separate list here because we need to trigger DBus calls to load/unload each effect/script.
|
||||
// Make sure that the effect/script is added to the kwinrc "Plugins" section above!
|
||||
const QList<QString> KWIN_EFFECTS = {"blur", "mobiletaskswitcher", "overview", "screenedge", "shift-tile-preview"};
|
||||
const QList<QString> KWIN_EFFECTS = {"blur", "mobiletaskswitcher", "overview", "screenedge", "shift-tile-animations", "shift-tile-preview"};
|
||||
const QList<QString> KWIN_SCRIPTS = {"convergentwindows", "shift-tiling"};
|
||||
|
||||
// .config/plasma-mobile/ksmserver - immutable settings:
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@ function(add_kwin_effect name source)
|
|||
endfunction()
|
||||
|
||||
add_kwin_effect(shift-snap-assist shift-snap-assist)
|
||||
add_kwin_effect(shift-tile-animations shift-tile-animations)
|
||||
add_kwin_effect(shift-tile-preview shift-tile-preview)
|
||||
|
|
|
|||
155
kwin/effects/shift-tile-animations/contents/code/main.js
Normal file
155
kwin/effects/shift-tile-animations/contents/code/main.js
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
/*global effect, effects, animate, animationTime, cancel, Effect, QEasingCurve */
|
||||
|
||||
"use strict";
|
||||
|
||||
class ShiftTileAnimationsEffect {
|
||||
constructor() {
|
||||
effect.configChanged.connect(this.loadConfig.bind(this));
|
||||
effect.animationEnded.connect(this.cleanupWindow.bind(this));
|
||||
effects.windowAdded.connect(this.manage.bind(this));
|
||||
effects.windowClosed.connect(this.cleanupWindow.bind(this));
|
||||
|
||||
this.loadConfig();
|
||||
|
||||
for (const window of effects.stackingOrder) {
|
||||
this.manage(window);
|
||||
}
|
||||
}
|
||||
|
||||
loadConfig() {
|
||||
this.baseDuration = effect.readConfig("Duration", 185) || 185;
|
||||
this.minimumDelta = effect.readConfig("MinimumDelta", 3) || 3;
|
||||
}
|
||||
|
||||
manage(window) {
|
||||
if (!window || window.shiftTileAnimationsManaged) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.shiftTileAnimationsManaged = true;
|
||||
window.shiftTileUserMoveResize = false;
|
||||
window.windowFrameGeometryChanged.connect(this.onWindowFrameGeometryChanged.bind(this));
|
||||
window.windowStartUserMovedResized.connect(this.onWindowStartUserMovedResized.bind(this));
|
||||
window.windowFinishUserMovedResized.connect(this.onWindowFinishUserMovedResized.bind(this));
|
||||
}
|
||||
|
||||
eligibleWindow(window) {
|
||||
return window
|
||||
&& !effects.hasActiveFullScreenEffect
|
||||
&& window.visible
|
||||
&& !window.deleted
|
||||
&& window.managed
|
||||
&& window.normalWindow
|
||||
&& !window.fullScreen
|
||||
&& !window.desktopWindow
|
||||
&& !window.dock
|
||||
&& !window.popup
|
||||
&& !window.popupWindow
|
||||
&& !window.outline;
|
||||
}
|
||||
|
||||
validGeometry(geometry) {
|
||||
return geometry && geometry.width > 0 && geometry.height > 0;
|
||||
}
|
||||
|
||||
movedEnough(oldGeometry, newGeometry) {
|
||||
return Math.abs(oldGeometry.x - newGeometry.x) >= this.minimumDelta
|
||||
|| Math.abs(oldGeometry.y - newGeometry.y) >= this.minimumDelta
|
||||
|| Math.abs(oldGeometry.width - newGeometry.width) >= this.minimumDelta
|
||||
|| Math.abs(oldGeometry.height - newGeometry.height) >= this.minimumDelta;
|
||||
}
|
||||
|
||||
durationFor(oldGeometry, newGeometry) {
|
||||
const oldCenterX = oldGeometry.x + oldGeometry.width / 2;
|
||||
const oldCenterY = oldGeometry.y + oldGeometry.height / 2;
|
||||
const newCenterX = newGeometry.x + newGeometry.width / 2;
|
||||
const newCenterY = newGeometry.y + newGeometry.height / 2;
|
||||
const distanceX = newCenterX - oldCenterX;
|
||||
const distanceY = newCenterY - oldCenterY;
|
||||
const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
|
||||
const resize = Math.abs(newGeometry.width - oldGeometry.width) + Math.abs(newGeometry.height - oldGeometry.height);
|
||||
const travelAllowance = Math.min(80, Math.round(Math.max(distance / 12, resize / 18)));
|
||||
|
||||
return animationTime(Math.max(135, Math.min(260, this.baseDuration + travelAllowance)));
|
||||
}
|
||||
|
||||
cancelWindowAnimation(window) {
|
||||
if (!window || window.shiftTileMoveAnimation === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
cancel(window.shiftTileMoveAnimation);
|
||||
window.shiftTileMoveAnimation = undefined;
|
||||
window.setData(Effect.WindowForceBlurRole, null);
|
||||
}
|
||||
|
||||
cleanupWindow(window) {
|
||||
if (!window) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.shiftTileMoveAnimation = undefined;
|
||||
window.setData(Effect.WindowForceBlurRole, null);
|
||||
}
|
||||
|
||||
onWindowStartUserMovedResized(window) {
|
||||
if (!window) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.shiftTileUserMoveResize = true;
|
||||
this.cancelWindowAnimation(window);
|
||||
}
|
||||
|
||||
onWindowFinishUserMovedResized(window) {
|
||||
if (window) {
|
||||
window.shiftTileUserMoveResize = false;
|
||||
}
|
||||
}
|
||||
|
||||
onWindowFrameGeometryChanged(window, oldGeometry) {
|
||||
if (!this.eligibleWindow(window) || window.shiftTileUserMoveResize) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newGeometry = window.geometry;
|
||||
if (!this.validGeometry(oldGeometry) || !this.validGeometry(newGeometry) || !this.movedEnough(oldGeometry, newGeometry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cancelWindowAnimation(window);
|
||||
window.setData(Effect.WindowForceBlurRole, true);
|
||||
window.shiftTileMoveAnimation = animate({
|
||||
window: window,
|
||||
duration: this.durationFor(oldGeometry, newGeometry),
|
||||
keepAlive: false,
|
||||
animations: [{
|
||||
type: Effect.Size,
|
||||
from: {
|
||||
value1: oldGeometry.width,
|
||||
value2: oldGeometry.height
|
||||
},
|
||||
to: {
|
||||
value1: newGeometry.width,
|
||||
value2: newGeometry.height
|
||||
},
|
||||
curve: QEasingCurve.OutCubic
|
||||
}, {
|
||||
type: Effect.Translation,
|
||||
from: {
|
||||
value1: oldGeometry.x - newGeometry.x - (newGeometry.width / 2 - oldGeometry.width / 2),
|
||||
value2: oldGeometry.y - newGeometry.y - (newGeometry.height / 2 - oldGeometry.height / 2)
|
||||
},
|
||||
to: {
|
||||
value1: 0,
|
||||
value2: 0
|
||||
},
|
||||
curve: QEasingCurve.OutCubic
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
new ShiftTileAnimationsEffect();
|
||||
20
kwin/effects/shift-tile-animations/metadata.json
Normal file
20
kwin/effects/shift-tile-animations/metadata.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"KPackageStructure": "KWin/Effect",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "marcoa@example.com",
|
||||
"Name": "Marco Allegretti"
|
||||
}
|
||||
],
|
||||
"Category": "Appearance",
|
||||
"Description": "Animates SHIFT convergence window layout moves.",
|
||||
"EnabledByDefault": false,
|
||||
"Id": "shift-tile-animations",
|
||||
"License": "EUPL-1.2",
|
||||
"Name": "SHIFT Tile Animations",
|
||||
"Version": "1.0"
|
||||
},
|
||||
"X-KDE-Ordering": 62,
|
||||
"X-Plasma-API": "javascript"
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
import QtQuick
|
||||
import org.kde.kwin as KWinComponents
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
|
||||
KWinComponents.SceneEffect {
|
||||
|
|
@ -13,14 +12,11 @@ KWinComponents.SceneEffect {
|
|||
|
||||
readonly property int outerGap: 8
|
||||
readonly property int floatEscapeMargin: 32
|
||||
readonly property int previewAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.SpatialDefault)
|
||||
readonly property int previewFadeDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsFast)
|
||||
|
||||
property var dragConnectedWindows: ({})
|
||||
property var draggingWindow: null
|
||||
property rect dragSourceGeometry: Qt.rect(0, 0, 0, 0)
|
||||
|
||||
property bool animatePreview: false
|
||||
property bool previewVisible: false
|
||||
property string previewMode: ""
|
||||
property string previewScreenName: ""
|
||||
|
|
@ -126,14 +122,26 @@ KWinComponents.SceneEffect {
|
|||
}
|
||||
|
||||
function showPreview(mode, geometry, screenName) {
|
||||
if (!isActive() || !validRect(geometry)) {
|
||||
hidePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
previewMode = mode;
|
||||
previewScreenName = screenName;
|
||||
previewGeometry = insetPreviewGeometry(geometry);
|
||||
previewVisible = true;
|
||||
KWinComponents.Workspace.showOutline(previewGeometry);
|
||||
}
|
||||
|
||||
function hidePreview() {
|
||||
previewVisible = false;
|
||||
draggingWindow = null;
|
||||
dragSourceGeometry = Qt.rect(0, 0, 0, 0);
|
||||
disableEffectTimer.restart();
|
||||
previewMode = "";
|
||||
previewScreenName = "";
|
||||
previewGeometry = Qt.rect(0, 0, 0, 0);
|
||||
KWinComponents.Workspace.hideOutline();
|
||||
}
|
||||
|
||||
function updatePreview(window, dragGeometry) {
|
||||
|
|
@ -189,44 +197,6 @@ KWinComponents.SceneEffect {
|
|||
}
|
||||
}
|
||||
|
||||
function previewFillColor(mode) {
|
||||
if (mode === "float") {
|
||||
return Qt.rgba(1.0, 0.62, 0.24, 0.18);
|
||||
}
|
||||
if (mode === "restore") {
|
||||
return Qt.rgba(1.0, 1.0, 1.0, 0.10);
|
||||
}
|
||||
return Qt.rgba(0.18, 0.72, 0.66, 0.22);
|
||||
}
|
||||
|
||||
function previewBorderColor(mode) {
|
||||
if (mode === "float") {
|
||||
return Qt.rgba(1.0, 0.72, 0.36, 0.72);
|
||||
}
|
||||
if (mode === "restore") {
|
||||
return Qt.rgba(1.0, 1.0, 1.0, 0.36);
|
||||
}
|
||||
return Qt.rgba(0.64, 0.90, 0.86, 0.82);
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: enableAnimationTimer
|
||||
interval: 1
|
||||
repeat: false
|
||||
onTriggered: effect.animatePreview = true
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: disableEffectTimer
|
||||
interval: effect.previewFadeDuration
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!effect.previewVisible) {
|
||||
effect.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: KWinComponents.Workspace
|
||||
|
||||
|
|
@ -267,65 +237,5 @@ KWinComponents.SceneEffect {
|
|||
}
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
id: screenDelegate
|
||||
|
||||
readonly property var targetScreen: KWinComponents.SceneView.screen
|
||||
readonly property bool previewOnScreen: effect.previewScreenName === targetScreen.name
|
||||
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
id: previewSurface
|
||||
|
||||
visible: opacity > 0
|
||||
x: effect.previewGeometry.x - screenDelegate.targetScreen.geometry.x
|
||||
y: effect.previewGeometry.y - screenDelegate.targetScreen.geometry.y
|
||||
width: effect.previewGeometry.width
|
||||
height: effect.previewGeometry.height
|
||||
radius: 14
|
||||
opacity: effect.previewVisible && screenDelegate.previewOnScreen ? 1 : 0
|
||||
color: effect.previewFillColor(effect.previewMode)
|
||||
border.width: 2
|
||||
border.color: effect.previewBorderColor(effect.previewMode)
|
||||
|
||||
Behavior on x {
|
||||
enabled: effect.animatePreview
|
||||
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: effect.previewAnimationDuration }
|
||||
}
|
||||
Behavior on y {
|
||||
enabled: effect.animatePreview
|
||||
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: effect.previewAnimationDuration }
|
||||
}
|
||||
Behavior on width {
|
||||
enabled: effect.animatePreview
|
||||
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: effect.previewAnimationDuration }
|
||||
}
|
||||
Behavior on height {
|
||||
enabled: effect.animatePreview
|
||||
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: effect.previewAnimationDuration }
|
||||
}
|
||||
Behavior on opacity {
|
||||
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: effect.previewFadeDuration }
|
||||
}
|
||||
Behavior on color {
|
||||
MobileShell.MotionColorAnimation { type: MobileShell.Motion.EffectsFast; duration: effect.previewFadeDuration }
|
||||
}
|
||||
Behavior on border.color {
|
||||
MobileShell.MotionColorAnimation { type: MobileShell.Motion.EffectsFast; duration: effect.previewFadeDuration }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
radius: Math.max(0, parent.radius - anchors.margins)
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(1, 1, 1, 0.14)
|
||||
opacity: effect.previewMode === "insert" ? 1 : 0.45
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: connectExistingWindows()
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|||
|
||||
effect_qml="$repo_root/kwin/effects/shift-tile-preview/contents/ui/main.qml"
|
||||
effect_metadata="$repo_root/kwin/effects/shift-tile-preview/metadata.json"
|
||||
animation_effect_js="$repo_root/kwin/effects/shift-tile-animations/contents/code/main.js"
|
||||
animation_effect_metadata="$repo_root/kwin/effects/shift-tile-animations/metadata.json"
|
||||
effects_cmake="$repo_root/kwin/effects/CMakeLists.txt"
|
||||
tiling_script="$repo_root/kwin/scripts/shift-tiling/contents/ui/main.qml"
|
||||
decoration_qml="$repo_root/kwin/decorations/org.shift.decoration/contents/ui/main.qml"
|
||||
|
|
@ -36,12 +38,17 @@ reject_line() {
|
|||
}
|
||||
|
||||
require_line "$effects_cmake" "add_kwin_effect(shift-tile-preview shift-tile-preview)"
|
||||
require_line "$effects_cmake" "add_kwin_effect(shift-tile-animations shift-tile-animations)"
|
||||
require_line "$effect_metadata" '"Id": "shift-tile-preview"'
|
||||
require_line "$effect_metadata" '"X-Plasma-API": "declarativescript"'
|
||||
require_line "$animation_effect_metadata" '"Id": "shift-tile-animations"'
|
||||
require_line "$animation_effect_metadata" '"X-Plasma-API": "javascript"'
|
||||
|
||||
require_line "$env_config" '{"shift-tilingEnabled", convergenceModeEnabled}'
|
||||
require_line "$env_config" '{"shift-tile-previewEnabled", convergenceModeEnabled}'
|
||||
require_line "$env_config" '{"shift-tile-animationsEnabled", convergenceModeEnabled}'
|
||||
require_line "$env_config" '"shift-tile-preview"'
|
||||
require_line "$env_config" '"shift-tile-animations"'
|
||||
require_line "$env_config" '"shift-tiling"'
|
||||
|
||||
require_line "$effect_qml" "KWinComponents.SceneEffect"
|
||||
|
|
@ -50,13 +57,24 @@ require_line "$effect_qml" "ShellSettings.Settings.dynamicTilingEnabled"
|
|||
require_line "$effect_qml" "interactiveMoveResizeStarted.connect"
|
||||
require_line "$effect_qml" "interactiveMoveResizeStepped.connect"
|
||||
require_line "$effect_qml" "interactiveMoveResizeFinished.connect"
|
||||
require_line "$effect_qml" "Behavior on x"
|
||||
require_line "$effect_qml" "Behavior on y"
|
||||
require_line "$effect_qml" "Behavior on width"
|
||||
require_line "$effect_qml" "Behavior on height"
|
||||
require_line "$effect_qml" "showPreview(\"swap\""
|
||||
require_line "$effect_qml" "showPreview(\"float\""
|
||||
require_line "$effect_qml" "showPreview(\"restore\""
|
||||
require_line "$effect_qml" "KWinComponents.Workspace.showOutline(previewGeometry)"
|
||||
require_line "$effect_qml" "KWinComponents.Workspace.hideOutline()"
|
||||
reject_line "$effect_qml" "MobileShell.MotionNumberAnimation"
|
||||
reject_line "$effect_qml" "MobileShell.MotionColorAnimation"
|
||||
reject_line "$effect_qml" "disableEffectTimer"
|
||||
|
||||
require_line "$animation_effect_js" "class ShiftTileAnimationsEffect"
|
||||
require_line "$animation_effect_js" "window.windowFrameGeometryChanged.connect"
|
||||
require_line "$animation_effect_js" "window.windowStartUserMovedResized.connect"
|
||||
require_line "$animation_effect_js" "window.windowFinishUserMovedResized.connect"
|
||||
require_line "$animation_effect_js" "animationTime("
|
||||
require_line "$animation_effect_js" "type: Effect.Size"
|
||||
require_line "$animation_effect_js" "type: Effect.Translation"
|
||||
require_line "$animation_effect_js" "curve: QEasingCurve.OutCubic"
|
||||
require_line "$animation_effect_js" "window.shiftTileUserMoveResize"
|
||||
|
||||
require_line "$tiling_script" "readonly property int maxWindowsPerPage: 4"
|
||||
require_line "$tiling_script" "readonly property real stablePrimaryRatio: 0.58"
|
||||
|
|
|
|||
Loading…
Reference in a new issue