fix: port scripts, effects and window switcher to KWin 6

main
Reion Wong 4 weeks ago
parent 2bec103af3
commit 06bf723f3b

@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.5)
cmake_minimum_required(VERSION 3.16)
project(cutefish-kwin-plugins)

@ -1,8 +1,28 @@
# CutefishOS KWin Plugins
Window decoration, animations, window switcher and KWin configuration for
CutefishOS. The plugins target KWin 6 (Plasma 6 / KDecoration3).
## Contents
| Path | Description |
| --- | --- |
| `plugins/decoration3` | KDecoration3 window decoration (installed as `org.cutefish.decoration`) |
| `plugins/decoration` | Legacy KDecoration2 decoration for KWin 5, not built by default |
| `plugins/roundedwindow` | Legacy KWin 5 C++ effect, not built (uses private KWin 5 APIs) |
| `scripts/` | JavaScript effects and scripts (`cutefish_scale`, `cutefish_squash`, `cutefish_popups`, `cutefishlauncher`) |
| `tabbox/` | `cutefish_thumbnail` window switcher layout |
| `config/` | System-wide KWin defaults installed into `/etc/xdg` |
## Dependencies
`sudo pacman -S extra-cmake-modules qt5-base qt5-declarative kconfig kdecoration kguiaddons kcoreaddons kconfigwidgets kwindowsystem kwayland kwin`
Debian/Ubuntu:
`sudo apt install cmake extra-cmake-modules qt6-base-dev libkf6coreaddons-dev libkdecorations3-dev kwin-common`
`kwin-common` is required at runtime: it provides the KPackage structures and
the `org.kde.kwin` QML module that KWin uses to load scripted effects, scripts
and window switchers. The window switcher additionally needs FishUI.
## Build
@ -14,6 +34,16 @@ make
sudo make install
```
Build options: `CUTEFISH_BUILD_KWIN6_DECORATION` (ON), `CUTEFISH_BUILD_KWIN5_DECORATION`
(OFF) and `CUTEFISH_BUILD_KWIN5_EFFECT` (OFF).
## Enabling
The defaults in `config/kwinrc` select the decoration
(`[org.kde.kdecoration2] library=org.cutefish.decoration`), the
`cutefish_thumbnail` switcher and the CutefishOS effects. For an existing
user configuration, the same keys have to be set in `~/.config/kwinrc`.
## License
cutefish-kwin-plugins is licensed under GPLv3.

@ -1,4 +1,6 @@
[kwin]
Window Close=Alt+F4,Alt+F4,Close
Window Absolute Maximize=Meta+Up,none,Maximize
Window Unmaximize=Meta+Down,none,Unmaximize
Window Unmaximize=Meta+Down,none,Unmaximize
Walk Through Windows=Alt+Tab,Alt+Tab,Walk Through Windows
Walk Through Windows (Reverse)=Alt+Shift+Tab,Alt+Shift+Tab,Walk Through Windows (Reverse)

@ -3,16 +3,20 @@ OpenGLIsUnsafe=false
[Plugins]
blurEnabled=true
kwin4_effect_fadingpopupsEnabled=false
kwin4_effect_dialogparentEnabled=true
dialogparentEnabled=true
kwin4_effect_fadeEnabled=false
kwin4_effect_scaleEnabled=false
kwin4_effect_grayscaleEnabled = false
# The CutefishOS animations replace the KWin ones with the same purpose.
cutefish_popupsEnabled=true
cutefish_scaleEnabled=true
cutefish_squashEnabled=true
kwin4_effect_squashEnabled=false
kwin4_effect_translucencyEnabled=false
fadingpopupsEnabled=false
fadeEnabled=false
scaleEnabled=false
squashEnabled=false
glideEnabled=false
magiclampEnabled=false
translucencyEnabled=false
[Effect-Blur]
BlurStrength=15
@ -24,14 +28,6 @@ HideUtilityWindowsForInactive=false
BorderlessMaximizedWindows=false
Placement=Centered
[Effect-FlipSwitch]
TabBox=false
TabBoxAlternative=false
[Effect-CoverSwitch]
TabBox=false
TabBoxAlternative=false
[Effect-SlidingPopups]
SlideInTime=250
SlideOutTime=250
@ -43,7 +39,7 @@ BorderActivate=9
BorderAlternativeActivate=9
DesktopMode=1
HighlightWindows=true
LayoutName=present_windows
LayoutName=cutefish_thumbnail
MinimizedMode=0
MultiScreenMode=0
ShowDesktopMode=1
@ -59,8 +55,10 @@ Left=None
Right=None
Top=None
# KWin 6 still reads the decoration settings from the KDecoration2 group.
[org.kde.kdecoration2]
BorderSize=Normal
ButtonsOnLeft=
ButtonsOnRight=HIAX
library=org.cutefish.decoration
theme=

27
debian/control vendored

@ -5,29 +5,20 @@ Maintainer: CutefishOS <cutefishos@foxmail.com>
Build-Depends: cmake,
debhelper (>= 9),
extra-cmake-modules,
kwin-dev,
libxcb-util-dev,
libxcb-util0-dev,
libkf5windowsystem-dev,
libkf5globalaccel-dev,
libkf5config-dev,
libkdecorations2-dev,
libkf5coreaddons-dev,
libqt5opengl5-dev,
libqt5x11extras5-dev,
qtbase5-dev,
qtbase5-private-dev,
qtdeclarative5-dev,
qtquickcontrols2-5-dev,
qttools5-dev,
qttools5-dev-tools
libkdecorations3-dev,
libkf6coreaddons-dev,
qt6-base-dev
Standards-Version: 4.5.0
Homepage: https://cutefishos.com
Package: cutefish-kwin-plugins
Architecture: any
Depends: qml-module-qtquick-controls2,
qml-module-qtquick2,
Depends: fishui,
kwin-common,
qml6-module-qtquick,
qml6-module-qtquick-controls,
qml6-module-qtquick-layouts,
qml6-module-qtquick-window,
${misc:Depends},
${shlibs:Depends}
Description: CutefishOS KWin Plugins

5
debian/rules vendored

@ -1,9 +1,4 @@
#!/usr/bin/make -f
export QT_SELECT=5
%:
dh $@
override_dh_auto_configure:
dh_auto_configure -- -DEMBED_TRANSLATIONS=ON -DBUILD_TESTING=ON

@ -20,6 +20,13 @@ set(decoration3_SRCS
add_library(cutefishdecoration3 MODULE ${decoration3_SRCS})
# KWin looks the decoration plugin up by file name (the "library" key of the
# [org.kde.kdecoration2] group in kwinrc), so the file has to be named after the
# plugin id.
set_target_properties(cutefishdecoration3 PROPERTIES
PREFIX ""
OUTPUT_NAME "org.cutefish.decoration")
target_link_libraries(cutefishdecoration3
PRIVATE
Qt6::Core

@ -10,9 +10,14 @@
#include <KPluginFactory>
#include <KDecoration3/DecoratedWindow>
#include <KDecoration3/DecorationSettings>
#include <KDecoration3/DecorationShadow>
#include <QFontMetricsF>
#include <QImageReader>
#include <QPainter>
#include <QRadialGradient>
#include <cmath>
K_PLUGIN_FACTORY_WITH_JSON(CutefishDecorationFactory, "cutefishos.json",
registerPlugin<Cutefish::Decoration>();)
@ -20,10 +25,23 @@ K_PLUGIN_FACTORY_WITH_JSON(CutefishDecorationFactory, "cutefishos.json",
namespace Cutefish
{
static int s_decorationCount = 0;
static int s_shadowRadius = -1;
static std::shared_ptr<KDecoration3::DecorationShadow> s_shadow;
Decoration::Decoration(QObject *parent, const QVariantList &args)
: KDecoration3::Decoration(parent, args)
, m_themeSettings(QSettings::UserScope, QStringLiteral("cutefishos"), QStringLiteral("theme"))
{
++s_decorationCount;
}
Decoration::~Decoration()
{
if (--s_decorationCount == 0) {
s_shadow.reset();
s_shadowRadius = -1;
}
}
bool Decoration::init()
@ -32,14 +50,8 @@ bool Decoration::init()
return false;
}
m_devicePixelRatio = m_themeSettings.value(QStringLiteral("PixelRatio"), 1.0).toReal();
if (m_devicePixelRatio <= 0) {
m_devicePixelRatio = 1.0;
}
m_frameRadius = qRound(11 * m_devicePixelRatio);
updateColors();
updateButtonPixmaps();
m_themeSettingsFile = m_themeSettings.fileName();
reloadTheme();
m_leftButtons = new KDecoration3::DecorationButtonGroup(
KDecoration3::DecorationButtonGroup::Position::Left, this, &Button::create);
@ -49,6 +61,11 @@ bool Decoration::init()
auto window = this->window();
connect(window, &KDecoration3::DecoratedWindow::captionChanged, this, [this] { update(); });
connect(window, &KDecoration3::DecoratedWindow::activeChanged, this, [this] { update(); });
connect(window, &KDecoration3::DecoratedWindow::shadedChanged, this, [this] {
updateGeometry();
updateButtonsGeometry();
update();
});
connect(window, &KDecoration3::DecoratedWindow::maximizedChanged, this, [this] {
updateGeometry();
updateButtonsGeometry();
@ -69,13 +86,34 @@ bool Decoration::init()
updateButtonsGeometry();
update();
});
connect(settings().get(), &KDecoration3::DecorationSettings::decorationButtonsLeftChanged, this,
[this] { updateButtonsGeometry(); });
connect(settings().get(), &KDecoration3::DecorationSettings::decorationButtonsRightChanged, this,
[this] { updateButtonsGeometry(); });
connect(settings().get(), &KDecoration3::DecorationSettings::reconfigured, this, [this] {
reloadTheme();
updateGeometry();
updateButtonsGeometry();
update();
});
}
// Follow the CutefishOS theme settings (dark mode, scaling) while the window is open.
if (!m_themeSettingsFile.isEmpty()) {
m_themeWatcher.addPath(m_themeSettingsFile);
connect(&m_themeWatcher, &QFileSystemWatcher::fileChanged, this, [this] {
reloadTheme();
updateGeometry();
updateButtonsGeometry();
update();
// Editors replace the file instead of writing in place, which drops the watch.
if (!m_themeWatcher.files().contains(m_themeSettingsFile)) {
m_themeWatcher.addPath(m_themeSettingsFile);
}
});
}
updateGeometry();
updateButtonsGeometry();
return true;
@ -83,30 +121,33 @@ bool Decoration::init()
void Decoration::paint(QPainter *painter, const QRectF &repaintArea)
{
Q_UNUSED(repaintArea)
if (!window()) {
return;
}
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
painter->setPen(Qt::NoPen);
painter->setBrush(titleBarBackgroundColor());
if (!window()->isShaded() && !window()->isMaximized()) {
painter->drawRoundedRect(rect(), m_frameRadius, m_frameRadius);
} else {
painter->drawRect(rect());
if (!window()->isShaded()) {
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
painter->setPen(Qt::NoPen);
painter->setBrush(titleBarBackgroundColor());
const bool rounded = !window()->isMaximized()
&& (!settings() || settings()->isAlphaChannelSupported());
if (rounded) {
painter->drawRoundedRect(rect(), m_frameRadius, m_frameRadius);
} else {
painter->drawRect(rect());
}
painter->restore();
if (m_leftButtons) {
m_leftButtons->paint(painter, repaintArea);
}
if (m_rightButtons) {
m_rightButtons->paint(painter, repaintArea);
}
}
painter->restore();
if (m_leftButtons) {
m_leftButtons->paint(painter, repaintArea);
}
if (m_rightButtons) {
m_rightButtons->paint(painter, repaintArea);
}
paintCaption(painter);
}
@ -141,21 +182,102 @@ void Decoration::updateButtonsGeometry()
m_rightButtons->setPos(QPointF(size().width() - m_rightButtons->geometry().width() - 2, 0));
}
void Decoration::updateColors()
void Decoration::updateShadow()
{
// The theme settings are intentionally read once per decoration. KWin creates a new
// decoration when the window/theme is reconfigured, avoiding a dependency on KDE5 APIs.
if (s_shadow && s_shadowRadius == m_frameRadius) {
setShadow(s_shadow);
return;
}
const int shadowSize = 90;
const int shadowStrength = 35;
const QColor shadowColor = Qt::black;
const int shadowOverlap = m_frameRadius;
const int shadowOffset = shadowOverlap / 2;
QImage image(2 * shadowSize, 2 * shadowSize, QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::transparent);
// Gaussian delta function used to fade the shadow out.
auto alpha = [](qreal x) { return std::exp(-x * x / 0.15); };
auto gradientStopColor = [](QColor color, int alpha) {
color.setAlpha(alpha);
return color;
};
QRadialGradient radialGradient(shadowSize, shadowSize, shadowSize);
for (int i = 0; i < 10; ++i) {
const qreal x(qreal(i) / 9);
radialGradient.setColorAt(x, gradientStopColor(shadowColor, alpha(x) * shadowStrength));
}
radialGradient.setColorAt(1, gradientStopColor(shadowColor, 0));
QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.fillRect(image.rect(), radialGradient);
const QRectF innerRect(shadowSize - shadowOverlap,
shadowSize - shadowOffset - shadowOverlap,
2 * shadowOverlap,
shadowOffset + 2 * shadowOverlap);
// Contrast pixel around the window.
painter.setPen(gradientStopColor(shadowColor, shadowStrength * 0.5));
painter.setBrush(Qt::NoBrush);
painter.drawRoundedRect(innerRect, -0.5 + m_frameRadius, -0.5 + m_frameRadius);
// Mask out the area covered by the window itself.
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::black);
painter.setCompositionMode(QPainter::CompositionMode_DestinationOut);
painter.drawRoundedRect(innerRect, 0.5 + m_frameRadius, 0.5 + m_frameRadius);
painter.end();
s_shadow = std::make_shared<KDecoration3::DecorationShadow>();
s_shadow->setPadding(QMarginsF(shadowSize - shadowOverlap,
shadowSize - shadowOffset - shadowOverlap,
shadowSize - shadowOverlap,
shadowSize - shadowOverlap));
s_shadow->setInnerShadowRect(QRectF(shadowSize, shadowSize, 1, 1));
s_shadow->setShadow(image);
s_shadowRadius = m_frameRadius;
setShadow(s_shadow);
}
void Decoration::reloadTheme()
{
m_themeSettings.sync();
m_devicePixelRatio = m_themeSettings.value(QStringLiteral("PixelRatio"), 1.0).toReal();
if (m_devicePixelRatio <= 0) {
m_devicePixelRatio = 1.0;
}
m_darkMode = m_themeSettings.value(QStringLiteral("DarkMode"), false).toBool();
m_frameRadius = qRound(11 * m_devicePixelRatio);
updateButtonPixmaps();
updateShadow();
}
void Decoration::updateButtonPixmaps()
{
const auto load = [](const QString &path) {
return QPixmap(path);
};
m_closeBtnPixmap = load(pixmapPath(KDecoration3::DecorationButtonType::Close, false));
m_maximizeBtnPixmap = load(pixmapPath(KDecoration3::DecorationButtonType::Maximize, false));
m_minimizeBtnPixmap = load(pixmapPath(KDecoration3::DecorationButtonType::Minimize, false));
m_restoreBtnPixmap = load(pixmapPath(KDecoration3::DecorationButtonType::Maximize, true));
m_closeBtnPixmap = loadPixmap(pixmapPath(KDecoration3::DecorationButtonType::Close, false));
m_maximizeBtnPixmap = loadPixmap(pixmapPath(KDecoration3::DecorationButtonType::Maximize, false));
m_minimizeBtnPixmap = loadPixmap(pixmapPath(KDecoration3::DecorationButtonType::Minimize, false));
m_restoreBtnPixmap = loadPixmap(pixmapPath(KDecoration3::DecorationButtonType::Maximize, true));
}
QPixmap Decoration::loadPixmap(const QString &path) const
{
QImageReader reader(path);
if (!reader.canRead()) {
return {};
}
// The icons are SVGs, render them at the size the buttons are painted with.
reader.setScaledSize(QSize(24, 24) * m_devicePixelRatio);
return QPixmap::fromImage(reader.read());
}
QPixmap Decoration::buttonPixmap(KDecoration3::DecorationButtonType type, bool checked) const
@ -197,11 +319,6 @@ int Decoration::titleBarHeight() const
return qMax(1, qRound(m_titleBarHeight * m_devicePixelRatio));
}
bool Decoration::darkMode() const
{
return m_themeSettings.value(QStringLiteral("DarkMode"), false).toBool();
}
QColor Decoration::titleBarBackgroundColor() const
{
return darkMode() ? m_titleBarBgDarkColor : m_titleBarBgColor;

@ -9,6 +9,7 @@
#include <KDecoration3/Decoration>
#include <KDecoration3/DecorationButtonGroup>
#include <QFileSystemWatcher>
#include <QPixmap>
#include <QSettings>
@ -21,19 +22,20 @@ class Decoration : public KDecoration3::Decoration
public:
explicit Decoration(QObject *parent = nullptr, const QVariantList &args = {});
~Decoration() override = default;
~Decoration() override;
bool init() override;
void paint(QPainter *painter, const QRectF &repaintArea) override;
bool darkMode() const;
bool darkMode() const { return m_darkMode; }
qreal devicePixelRatio() const { return m_devicePixelRatio; }
QPixmap buttonPixmap(KDecoration3::DecorationButtonType type, bool checked = false) const;
private:
void updateGeometry();
void updateButtonsGeometry();
void updateColors();
void updateShadow();
void reloadTheme();
void updateButtonPixmaps();
void paintCaption(QPainter *painter) const;
@ -41,11 +43,13 @@ private:
QColor titleBarBackgroundColor() const;
QColor titleBarForegroundColor() const;
QString pixmapPath(KDecoration3::DecorationButtonType type, bool checked) const;
QPixmap loadPixmap(const QString &path) const;
KDecoration3::DecorationButtonGroup *m_leftButtons = nullptr;
KDecoration3::DecorationButtonGroup *m_rightButtons = nullptr;
qreal m_devicePixelRatio = 1.0;
bool m_darkMode = false;
int m_titleBarHeight = 30;
int m_frameRadius = 11;
QColor m_titleBarBgColor = QColor(255, 255, 255);
@ -56,6 +60,8 @@ private:
QColor m_unfocusedFgDarkColor = QColor(112, 112, 112);
QSettings m_themeSettings;
QString m_themeSettingsFile;
QFileSystemWatcher m_themeWatcher;
QPixmap m_closeBtnPixmap;
QPixmap m_maximizeBtnPixmap;
QPixmap m_minimizeBtnPixmap;

@ -10,10 +10,12 @@
"use strict";
var blocklist = [
// The logout screen has to be animated only by the logout effect.
// Ignore the black background behind the lock screen.
"ksmserver ksmserver",
// The logout screen has to be animated only by the logout effect.
"ksmserver-logout-greeter ksmserver-logout-greeter",
// The lock screen isn't a popup window.
"kscreenlocker_greet kscreenlocker_greet",
// KDE Plasma splash screen has to be animated only by the login effect.
"ksplashqml ksplashqml"
];
@ -65,7 +67,8 @@ function isPopupWindow(window) {
// was doing that.
if (window.dock || window.splash || window.toolbar
|| window.notification || window.onScreenDisplay
|| window.criticalNotification) {
|| window.criticalNotification
|| window.appletPopup) {
return true;
}
@ -87,7 +90,7 @@ var cutefishPopupsEffect = {
if (!window.visible) {
return;
}
if (!effect.grab(window, Effect.WindowAddedGrabRole)) {
if (effect.isGrabbed(window, Effect.WindowAddedGrabRole)) {
return;
}
window.fadeInAnimation = animate({
@ -106,10 +109,10 @@ var cutefishPopupsEffect = {
if (!isPopupWindow(window)) {
return;
}
if (!window.visible) {
if (!window.visible || window.skipsCloseAnimation) {
return;
}
if (!effect.grab(window, Effect.WindowClosedGrabRole)) {
if (effect.isGrabbed(window, Effect.WindowClosedGrabRole)) {
return;
}
window.fadeOutAnimation = animate({

@ -1,14 +0,0 @@
[Desktop Entry]
Name=CutefishOS Fading Popups
Type=Service
X-KDE-ServiceTypes=KWin/Effect
X-KDE-PluginInfo-Author=Vlad Zahorodnii
X-KDE-PluginInfo-Email=vlad.zahorodnii@kde.org
X-KDE-PluginInfo-Name=cutefish_popups
X-KDE-PluginInfo-Version=1.0
X-KDE-PluginInfo-Category=Appearance
X-KDE-PluginInfo-License=GPL
X-KDE-PluginInfo-EnabledByDefault=false
X-KDE-Ordering=60
X-Plasma-API=javascript
X-Plasma-MainScript=code/main.js

@ -1,4 +1,5 @@
{
"KPackageStructure": "KWin/Effect",
"KPlugin": {
"Authors": [
{
@ -8,14 +9,13 @@
],
"Category": "Appearance",
"Description": "Make popups smoothly fade in and out when they are shown or hidden",
"ServiceTypes": [
"KWin/Effect"
],
"EnabledByDefault": false,
"EnabledByDefault": true,
"Icon": "preferences-system-windows-effect-fadingpopups",
"Id": "cutefish_popups",
"License": "GPL",
"Name": "CutefishOS Fading Popups",
"Version": "1.0"
},
"X-KDE-Ordering": "60",
"X-Plasma-API": "javascript",
"X-Plasma-MainScript": "code/main.js"
"X-KDE-Ordering": 60,
"X-Plasma-API": "javascript"
}

@ -3,7 +3,7 @@
This file is part of the KDE project.
SPDX-FileCopyrightText: 2021 Reven Martin <revenmartin@gmail.com>
SPDX-FileCopyrightText: 2018 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-FileCopyrightText: 2018, 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
@ -18,13 +18,17 @@ var blocklist = [
// KDE Plasma splash screen has to be animated only by the login effect.
"ksplashqml ksplashqml",
// Screenshot tools have to stay out of their own screenshots.
"spectacle spectacle",
"spectacle org.kde.spectacle",
"cutefish-launcher cutefish-launcher",
"cutefish-statusbar cutefish-statusbar",
"cutefish-screenshot cutefish-screenshot"
];
var scaleEffect = {
loadConfig: function (window) {
loadConfig: function () {
var defaultDuration = 250;
var duration = effect.readConfig("Duration", defaultDuration) || defaultDuration;
scaleEffect.duration = animationTime(duration);
@ -58,14 +62,14 @@ var scaleEffect = {
return false;
}
// Don't animate the outline because it looks very sick.
if (window.outline) {
// Don't animate the outline and the screenlocker, it looks bad.
if (window.lockScreen || window.outline) {
return false;
}
// Override-redirect windows are usually used for user interface
// concepts that are not expected to be animated by this effect.
if (window.x11Client && !window.managed) {
if (!window.managed) {
return false;
}
@ -89,7 +93,7 @@ var scaleEffect = {
if (!window.visible) {
return;
}
if (!effect.grab(window, Effect.WindowAddedGrabRole)) {
if (effect.isGrabbed(window, Effect.WindowAddedGrabRole)) {
return;
}
scaleEffect.setupForcedRoles(window);
@ -116,10 +120,10 @@ var scaleEffect = {
if (!scaleEffect.isScaleWindow(window)) {
return;
}
if (!window.visible) {
if (!window.visible || window.skipsCloseAnimation) {
return;
}
if (!effect.grab(window, Effect.WindowClosedGrabRole)) {
if (effect.isGrabbed(window, Effect.WindowClosedGrabRole)) {
return;
}
if (window.scaleInAnimation) {

@ -1,19 +0,0 @@
[Desktop Entry]
Name=Scale
Type=Service
X-KDE-ServiceTypes=KWin/Effect,KCModule
X-KDE-PluginInfo-Author=Vlad Zahorodnii
X-KDE-PluginInfo-Email=vlad.zahorodnii@kde.org
X-KDE-PluginInfo-Name=cutefish_scale
X-KDE-PluginInfo-Version=1
X-KDE-PluginInfo-Category=Window Open/Close Animation
X-KDE-PluginInfo-License=GPL
X-KDE-PluginInfo-EnabledByDefault=false
X-KDE-Ordering=60
X-Plasma-API=javascript
X-Plasma-MainScript=code/main.js
X-KDE-PluginKeyword=cutefish_scale
X-KDE-Library=kcm_kwin4_genericscripted
X-KDE-ParentComponents=cutefish_scale
X-KWin-Config-TranslationDomain=kwin_effects
X-KWin-Exclusive-Category=toplevel-open-close-animation

@ -1,4 +1,5 @@
{
"KPackageStructure": "KWin/Effect",
"KPlugin": {
"Authors": [
{
@ -7,27 +8,15 @@
}
],
"Category": "Window Open/Close Animation",
"Dependencies": [
],
"Description": "Make windows smoothly scale in and out when they are shown or hidden",
"EnabledByDefault": false,
"EnabledByDefault": true,
"Icon": "preferences-system-windows-effect-scale",
"Id": "cutefish_scale",
"License": "GPL",
"Name": "Scale",
"ServiceTypes": [
"KWin/Effect",
"KCModule"
],
"Name": "CutefishOS Scale",
"Version": "1"
},
"X-KDE-Ordering": "60",
"X-KDE-ParentComponents": [
"cutefish_scale"
],
"X-KDE-PluginKeyword": "cutefish_scale",
"X-KWin-Config-TranslationDomain": "kwin_effects",
"X-KDE-Ordering": 60,
"X-KWin-Exclusive-Category": "toplevel-open-close-animation",
"X-Plasma-API": "javascript",
"X-Plasma-MainScript": "code/main.js"
"X-Plasma-API": "javascript"
}

@ -146,10 +146,24 @@ var squashEffect = {
]
});
},
slotWindowAdded: function (window) {
// KWin 6 no longer has effects.windowMinimized/windowUnminimized,
// every window reports its own state changes instead.
window.minimizedChanged.connect(() => {
if (window.minimized) {
squashEffect.slotWindowMinimized(window);
} else {
squashEffect.slotWindowUnminimized(window);
}
});
},
init: function () {
effect.configChanged.connect(squashEffect.loadConfig);
effects.windowMinimized.connect(squashEffect.slotWindowMinimized);
effects.windowUnminimized.connect(squashEffect.slotWindowUnminimized);
effects.windowAdded.connect(squashEffect.slotWindowAdded);
for (const window of effects.stackingOrder) {
squashEffect.slotWindowAdded(window);
}
}
};

@ -1,18 +0,0 @@
[Desktop Entry]
Icon=preferences-system-windows-effect-squash
Name=CutefishSquash
Type=Service
X-KDE-ParentApp=
X-KDE-PluginInfo-Author=Rivo Laks, Vlad Zahorodnii
X-KDE-PluginInfo-Category=Appearance
X-KDE-PluginInfo-Email=rivolaks@hot.ee, vlad.zahorodnii@kde.org
X-KDE-PluginInfo-License=GPL
X-KDE-PluginInfo-Name=cutefish_squash
X-KDE-PluginInfo-Version=1
X-KDE-PluginInfo-Website=
X-KDE-ServiceTypes=KWin/Effect
X-KDE-PluginInfo-EnabledByDefault=false
X-KDE-Ordering=60
X-Plasma-API=javascript
X-Plasma-MainScript=code/main.js
X-KWin-Exclusive-Category=minimize

@ -1,4 +1,5 @@
{
"KPackageStructure": "KWin/Effect",
"KPlugin": {
"Authors": [
{
@ -7,21 +8,15 @@
}
],
"Category": "Appearance",
"EnabledByDefault": false,
"Description": "Squash windows when they are minimized",
"EnabledByDefault": true,
"Icon": "preferences-system-windows-effect-squash",
"Id": "cutefish_squash",
"License": "GPL",
"Name": "CutefishSquash",
"ServiceTypes": [
"KWin/Effect"
],
"Version": "1",
"Website": ""
"Name": "CutefishOS Squash",
"Version": "1"
},
"X-KDE-Ordering": "60",
"X-KDE-ParentApp": "",
"X-KDE-Ordering": 60,
"X-KWin-Exclusive-Category": "minimize",
"X-KWin-Video-Url": "https://files.kde.org/plasma/kwin/effect-videos/minimize.ogv",
"X-Plasma-API": "javascript",
"X-Plasma-MainScript": "code/main.js"
"X-Plasma-API": "javascript"
}

@ -1,18 +0,0 @@
[Desktop Entry]
Name=CutefishOS Launcher
Comment=Force the cutefish-launcher window keep in screen area
Icon=preferences-system-windows-script-test
X-Plasma-API=javascript
X-Plasma-MainScript=code/main.js
X-KDE-PluginInfo-Author=revenmartin
X-KDE-PluginInfo-Email=revenmartin@gmail.com
X-KDE-PluginInfo-Name=cutefishlauncher
X-KDE-PluginInfo-Version=0.1
X-KDE-PluginInfo-EnabledByDefault=false
X-KDE-PluginInfo-Depends=
X-KDE-PluginInfo-License=GPLv3
X-KDE-ServiceTypes=KWin/Script
Type=Service

@ -1,20 +1,19 @@
{
"KPackageStructure": "KWin/Script",
"KPlugin": {
"Name": "CutefishOS Launcher",
"Description": "Force the cutefish-launcher window to fill the screen area",
"Icon": "preferences-system-windows-script-test",
"Authors": [
{
"Name": "revenmartin",
"Email": "revenmartin@gmail.com"
"Email": "revenmartin@gmail.com",
"Name": "revenmartin"
}
],
"EnabledByDefault": false,
"Description": "Force the cutefish-launcher window to fill the screen area",
"EnabledByDefault": true,
"Icon": "preferences-system-windows-script-test",
"Id": "cutefishlauncher",
"Version": "0.1",
"License": "GPL-3.0-or-later"
"License": "GPL-3.0-or-later",
"Name": "CutefishOS Launcher",
"Version": "0.1"
},
"X-Plasma-API": "javascript",
"X-Plasma-MainScript": "code/main.js",
"KPackageStructure": "KWin/Script"
"X-Plasma-API": "javascript"
}

@ -1,34 +1,39 @@
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
/*
SPDX-FileCopyrightText: 2020 Chris Holland <zrenfire@gmail.com>
SPDX-FileCopyrightText: 2021 CutefishOS Team
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as PlasmaComponents
import org.kde.kquickcontrolsaddons 2.0
import org.kde.kwin 2.0 as KWin
SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts
import org.kde.kwin 3.0 as KWin
import FishUI 1.0 as FishUI
// https://techbase.kde.org/Development/Tutorials/KWin/WindowSwitcher
KWin.Switcher {
KWin.TabBoxSwitcher {
id: tabBox
currentIndex: thumbnailGridView.currentIndex
Window {
id: dialog
visible: tabBox.visible
flags: Qt.BypassWindowManagerHint | Qt.FramelessWindowHint
color: "transparent"
property int maxWidth: tabBox.screenGeometry.width * 0.95
property int maxHeight: tabBox.screenGeometry.height * 0.7
property int optimalWidth: thumbnailGridView.cellWidth * gridColumns
property int optimalHeight: thumbnailGridView.cellHeight * gridRows
property int maxGridColumnsByWidth: Math.floor(maxWidth / thumbnailGridView.cellWidth)
readonly property int maxWidth: tabBox.screenGeometry.width * 0.95
readonly property int maxHeight: tabBox.screenGeometry.height * 0.7
readonly property int maxGridColumnsByWidth: Math.max(1, Math.floor(maxWidth / thumbnailGridView.cellWidth))
property int gridColumns: maxGridColumnsByWidth
property int gridRows: Math.ceil(thumbnailGridView.count / gridColumns)
readonly property int gridRows: Math.ceil(thumbnailGridView.count / gridColumns)
readonly property int optimalWidth: thumbnailGridView.cellWidth * gridColumns
readonly property int optimalHeight: thumbnailGridView.cellHeight * gridRows
width: Math.min(Math.max(thumbnailGridView.cellWidth, optimalWidth), maxWidth)
height: Math.min(Math.max(thumbnailGridView.cellHeight, optimalHeight), maxHeight)
@ -58,7 +63,7 @@ KWin.Switcher {
anchors.fill: parent
radius: windowHelper.compositing ? 14 : 0
color: FishUI.Theme.backgroundColor
opacity: windowHelper.compositing ? FishUI.Theme.darkMode ? 0.3 : 0.4 : 1.0
opacity: windowHelper.compositing ? (FishUI.Theme.darkMode ? 0.3 : 0.4) : 1.0
border.color: FishUI.Theme.darkMode ? "#686868" : "#D9D9D9"
border.width: windowHelper.compositing ? 0 : 1
@ -76,15 +81,17 @@ KWin.Switcher {
id: dialogMainItem
anchors.fill: parent
property real screenFactor: tabBox.screenGeometry.width / tabBox.screenGeometry.height
property bool canStretchX: false
property bool canStretchY: false
readonly property real screenFactor: tabBox.screenGeometry.width / tabBox.screenGeometry.height
clip: true
// simple greedy algorithm
// Simple greedy algorithm
function calculateColumnCount() {
if (thumbnailGridView.count === 0) {
dialog.gridColumns = 1;
return;
}
// respect screenGeometry
var c = Math.min(thumbnailGridView.count, dialog.maxGridColumnsByWidth);
@ -98,13 +105,13 @@ KWin.Switcher {
dialog.gridColumns = columnCountRecursion(c, c, c - residue);
}
// step for greedy algorithm
// Step for the greedy algorithm
function columnCountRecursion(prevC, prevBestC, prevDiff) {
var c = prevC - 1;
// don't increase vertical extent more than horizontal
// and don't exceed maxHeight
if (prevC * prevC <= thumbnailGridView.count + prevDiff ||
if (c < 1 || prevC * prevC <= thumbnailGridView.count + prevDiff ||
dialog.maxHeight < Math.ceil(thumbnailGridView.count / c) * thumbnailGridView.cellHeight) {
return prevBestC;
}
@ -116,40 +123,31 @@ KWin.Switcher {
// empty slots
var diff = c - residue;
// compare it to previous count of empty slots
// compare it to the previous count of empty slots
if (diff < prevDiff) {
return columnCountRecursion(c, c, diff);
} else if (diff == prevDiff) {
// when it's the same try again, we'll stop early enough thanks to the landscape mode condition
return columnCountRecursion(c, prevBestC, diff);
}
// when we've found a local minimum choose this one (greedy)
// when it's the same or worse keep the previous best (greedy)
return columnCountRecursion(c, prevBestC, diff);
}
property bool mouseEnabled: false
MouseArea {
id: mouseDetector
anchors.fill: parent
hoverEnabled: true
onPositionChanged: dialogMainItem.mouseEnabled = true
}
GridView {
id: thumbnailGridView
anchors.fill: parent
model: tabBox.model
// interactive: false // Disable drag to scroll
currentIndex: tabBox.currentIndex
anchors.fill: parent
clip: true
keyNavigationWraps: true
highlightMoveDuration: 0
property int captionRowHeight: 22
property int thumbnailWidth: 300
property int thumbnailHeight: thumbnailWidth * (1.0 / dialogMainItem.screenFactor)
readonly property int iconSize: 48
readonly property int captionRowHeight: 22
readonly property int thumbnailWidth: 300
readonly property int thumbnailHeight: thumbnailWidth * (1.0 / dialogMainItem.screenFactor)
cellWidth: thumbnailWidth
cellHeight: captionRowHeight + thumbnailHeight
height: cellHeight
clip: true
// allow expansion on increasing count
property int highCount: 0
@ -160,53 +158,62 @@ KWin.Switcher {
}
}
delegate: Item {
property bool isCurrent: thumbnailGridView.currentIndex === index
delegate: MouseArea {
id: thumbnailGridItem
width: thumbnailGridView.cellWidth
height: thumbnailGridView.cellHeight
hoverEnabled: true
MouseArea {
anchors.fill: parent
// hoverEnabled: dialogMainItem.mouseEnabled
// onEntered: parent.hover()
onClicked: {
parent.select()
// dialog.close() // Doesn't end the effects until you release Alt.
}
}
function select() {
readonly property bool isCurrent: thumbnailGridView.currentIndex === index
onClicked: {
thumbnailGridView.currentIndex = index;
thumbnailGridView.currentIndexChanged(thumbnailGridView.currentIndex);
tabBox.currentIndex = index;
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 4
QIconItem {
id: iconItem
// source: model.icon
icon: model.icon
width: parent.height * 0.5
height: parent.height * 0.5
state: index == thumbnailGridView.currentIndex ? QIconItem.ActiveState : QIconItem.DefaultState
Layout.alignment: Qt.AlignHCenter
// KWin.WindowThumbnail needs a container, otherwise it is
// drawn with the size of the parent layout.
Item {
Layout.fillWidth: true
Layout.fillHeight: true
KWin.WindowThumbnail {
anchors.fill: parent
wId: windowId
}
FishUI.IconItem {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.bottom
anchors.verticalCenterOffset: -Math.round(thumbnailGridView.iconSize / 4)
width: thumbnailGridView.iconSize
height: thumbnailGridView.iconSize
source: model.icon
}
}
Label {
text: model.caption
Layout.fillWidth: true
Layout.preferredHeight: thumbnailGridView.captionRowHeight
text: model.caption
elide: Text.ElideRight
textFormat: Text.PlainText
horizontalAlignment: Text.AlignHCenter
color: isCurrent ? FishUI.Theme.highlightedTextColor : FishUI.Theme.textColor
verticalAlignment: Text.AlignVCenter
font.bold: thumbnailGridItem.isCurrent
color: thumbnailGridItem.isCurrent ? FishUI.Theme.highlightedTextColor
: FishUI.Theme.textColor
}
}
} // GridView.delegate
highlight: Item {
id: highlightItem
Rectangle {
anchors.fill: parent
anchors.margins: FishUI.Units.largeSpacing
@ -216,18 +223,17 @@ KWin.Switcher {
}
}
onCurrentIndexChanged: tabBox.currentIndex = thumbnailGridView.currentIndex
Connections {
target: tabBox
function onCurrentIndexChanged() {
thumbnailGridView.currentIndex = tabBox.currentIndex
thumbnailGridView.currentIndex = tabBox.currentIndex;
}
}
} // GridView
// This doesn't work, nor does keyboard input work on any other tabbox skin (KDE 5.7.4)
// It does work in the preview however.
Keys.onPressed: {
console.log('keyPressed', event.key)
Keys.onPressed: function (event) {
if (event.key == Qt.Key_Left) {
thumbnailGridView.moveCurrentIndexLeft();
} else if (event.key == Qt.Key_Right) {
@ -240,8 +246,8 @@ KWin.Switcher {
return;
}
thumbnailGridView.currentIndexChanged(thumbnailGridView.currentIndex);
tabBox.currentIndex = thumbnailGridView.currentIndex;
}
} // Dialog.mainItem
} // Dialog
} // dialogMainItem
} // Window
}

@ -1,17 +0,0 @@
[Desktop Entry]
Name=Cutefish Thumbnail Grid
Comment=
Type=Service
X-KDE-ServiceTypes=KWin/WindowSwitcher
X-Plasma-API=declarativeappletscript
X-Plasma-MainScript=ui/main.qml
Icon=preferences-system-windows-switcher-big-icons
X-KDE-PluginInfo-Author=Chris Holland
X-KDE-PluginInfo-Email=zrenfire@gmail.com
X-KDE-PluginInfo-Name=cutefish_thumbnail
X-KDE-PluginInfo-Version=4
X-KDE-PluginInfo-Website=https://github.com/Zren/kwin-tabbox-thumbnail_grid
X-KDE-PluginInfo-Depends=

@ -0,0 +1,19 @@
{
"KPackageStructure": "KWin/WindowSwitcher",
"KPlugin": {
"Authors": [
{
"Email": "zrenfire@gmail.com",
"Name": "Chris Holland"
}
],
"Description": "A window switcher layout using a grid of thumbnails",
"Icon": "preferences-system-windows-switcher-big-icons",
"Id": "cutefish_thumbnail",
"License": "GPL",
"Name": "Cutefish Thumbnail Grid",
"Version": "4",
"Website": "https://github.com/Zren/kwin-tabbox-thumbnail_grid"
},
"X-Plasma-API": "declarativeappletscript"
}
Loading…
Cancel
Save