Initial commit: macOS Window Buttons plasmoid for KDE Plasma 6

This commit is contained in:
RarDog
2026-09-13 18:30:37 +03:00
commit b3a976111a
4 changed files with 393 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
.directory
*~
*.swp
*.bak
.DS_Store
.vscode/
.idea/
+36
View File
@@ -0,0 +1,36 @@
# macOS Window Buttons for KDE Plasma 6
Легковесный плазмоид (виджет панели задач) для **KDE Plasma 6**, реализующий круглые кнопки управления окном в стиле macOS.
## Особенности
- **Интерфейс в стиле macOS**: аккуратные круглые кнопки (Закрыть, Свернуть, Развернуть) диаметром 13px с правильными цветами и обводкой.
- **Интерактивность**: появление миниатюрных символов (✕, −, ⤢) при наведении курсора на группу кнопок.
- **Умное автоскрытие**: виджет отображается на панели **только тогда, когда активное окно развернуто на весь экран (Maximized / FullScreen)**.
- **Схлопывание в 0px**: когда окно не развернуто, свернуто или активно другое неразвернутое окно, виджет полностью скрывается (`visible: false`) и занимает ровно 0 пикселей на панели.
- **Поддержка режима редактирования Plasma**: при входе в режим настройки панели виджет остается видимым с полупрозрачной рамкой, чтобы его можно было легко перемещать, настраивать или удалять.
## Установка
### Вариант 1: Через символическую ссылку (рекомендуется для разработки)
```bash
mkdir -p ~/.local/share/plasma/plasmoids
ln -sfn "$(pwd)" ~/.local/share/plasma/plasmoids/org.kde.plasma.macoswindowbuttons
kbuildsycoca6
systemctl --user restart plasma-plasmashell.service
```
### Вариант 2: Через kpackagetool6
```bash
kpackagetool6 -t Plasma/Applet -i .
systemctl --user restart plasma-plasmashell.service
```
## Скрытие родных рамок у развернутых окон
Чтобы у самих окон при максимизации исчезала родная верхняя полоса с заголовком и кнопками, включите опцию в KWin:
```bash
kwriteconfig6 --file kwinrc --group Windows --key BorderlessMaximizedWindows true
qdbus6 org.kde.KWin /KWin reconfigure
```
## Лицензия
GPL-2.0+
+327
View File
@@ -0,0 +1,327 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
import org.kde.plasma.plasmoid
import org.kde.plasma.core as PlasmaCore
import org.kde.taskmanager as TaskManager
PlasmoidItem {
id: root
// Константы геометрии кнопок в стиле macOS
readonly property real buttonSize: 13
readonly property real buttonSpacing: 8
readonly property real horizontalPadding: 6
readonly property real verticalPadding: 4
readonly property real contentWidth: (buttonSize * 3) + (buttonSpacing * 2) + (horizontalPadding * 2)
readonly property real contentHeight: buttonSize + (verticalPadding * 2)
// Состояние текущего активного окна
property bool isWindowMaximized: false
property bool canClose: true
property bool canMinimize: true
property bool canMaximize: true
// Режим редактирования панели/рабочего стола Plasma
readonly property bool inEditMode: (Plasmoid.containment?.corona?.editMode ?? false) || Plasmoid.userConfiguring
readonly property bool shouldBeVisible: isWindowMaximized || inEditMode
// Интеграция с панелью Plasma 6
Plasmoid.backgroundHints: PlasmaCore.Types.NoBackground
Plasmoid.status: shouldBeVisible ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.HiddenStatus
// Видимость и схлопывание виджета в 0px на панели, когда окно не развернуто и не в режиме редактирования
visible: shouldBeVisible
implicitWidth: shouldBeVisible ? contentWidth : 0
implicitHeight: shouldBeVisible ? contentHeight : 0
Layout.preferredWidth: shouldBeVisible ? contentWidth : 0
Layout.minimumWidth: shouldBeVisible ? contentWidth : 0
Layout.maximumWidth: shouldBeVisible ? contentWidth : 0
Layout.preferredHeight: shouldBeVisible ? contentHeight : 0
Layout.minimumHeight: shouldBeVisible ? contentHeight : 0
Layout.maximumHeight: shouldBeVisible ? contentHeight : 0
preferredRepresentation: fullRepresentation
compactRepresentation: fullRepresentation
// Отслеживание виртуальных рабочих столов
TaskManager.VirtualDesktopInfo {
id: virtualDesktopInfo
onCurrentDesktopChanged: root.updateWindowState()
}
// Отслеживание активностей KDE
TaskManager.ActivityInfo {
id: activityInfo
onCurrentActivityChanged: root.updateWindowState()
}
// Модель окон и задач
TaskManager.TasksModel {
id: tasksModel
virtualDesktop: virtualDesktopInfo.currentDesktop
activity: activityInfo.currentActivity
filterByVirtualDesktop: true
filterByActivity: true
groupMode: TaskManager.TasksModel.GroupDisabled
onActiveTaskChanged: root.updateWindowState()
onDataChanged: root.updateWindowState()
onRowsInserted: root.updateWindowState()
onRowsRemoved: root.updateWindowState()
onModelReset: root.updateWindowState()
onCountChanged: root.updateWindowState()
}
// Получение актуального QModelIndex активного окна
function getActiveTaskIndex() {
let idx = tasksModel.activeTask;
if (idx && idx.valid && tasksModel.data(idx, TaskManager.AbstractTasksModel.IsActive)) {
return idx;
}
for (let i = 0; i < tasksModel.rowCount(); ++i) {
let mIdx = tasksModel.index(i, 0);
if (tasksModel.data(mIdx, TaskManager.AbstractTasksModel.IsActive)) {
return mIdx;
}
}
return null;
}
// Обновление состояния развернутости и активности окна
function updateWindowState() {
let idx = getActiveTaskIndex();
if (idx && idx.valid) {
let isLauncher = Boolean(tasksModel.data(idx, TaskManager.AbstractTasksModel.IsLauncher));
let isStartup = Boolean(tasksModel.data(idx, TaskManager.AbstractTasksModel.IsStartup));
let isActive = Boolean(tasksModel.data(idx, TaskManager.AbstractTasksModel.IsActive));
let isMax = Boolean(tasksModel.data(idx, TaskManager.AbstractTasksModel.IsMaximized));
let isFull = Boolean(tasksModel.data(idx, TaskManager.AbstractTasksModel.IsFullScreen));
let isMin = Boolean(tasksModel.data(idx, TaskManager.AbstractTasksModel.IsMinimized));
let closeable = tasksModel.data(idx, TaskManager.AbstractTasksModel.IsClosable);
let minimizable = tasksModel.data(idx, TaskManager.AbstractTasksModel.IsMinimizable);
let maximizable = tasksModel.data(idx, TaskManager.AbstractTasksModel.IsMaximizable);
canClose = (closeable !== undefined && closeable !== null) ? Boolean(closeable) : true;
canMinimize = (minimizable !== undefined && minimizable !== null) ? Boolean(minimizable) : true;
canMaximize = (maximizable !== undefined && maximizable !== null) ? Boolean(maximizable) : true;
// Виджет показывается ТОЛЬКО когда активное окно развернуто на весь экран и не свернуто
isWindowMaximized = (isActive && !isLauncher && !isStartup && (isMax || isFull) && !isMin);
} else {
isWindowMaximized = false;
}
}
// Действия над окном через TasksModel
function closeWindow() {
let idx = getActiveTaskIndex();
if (idx && idx.valid) {
tasksModel.requestClose(idx);
}
}
function minimizeWindow() {
let idx = getActiveTaskIndex();
if (idx && idx.valid) {
tasksModel.requestToggleMinimized(idx);
}
}
function toggleMaximizeWindow() {
let idx = getActiveTaskIndex();
if (idx && idx.valid) {
tasksModel.requestToggleMaximized(idx);
}
}
Component.onCompleted: {
updateWindowState();
}
// Представление кнопок
fullRepresentation: Item {
id: buttonsContainer
implicitWidth: root.contentWidth
implicitHeight: root.contentHeight
visible: root.shouldBeVisible
// Подсветка-заглушка в режиме редактирования Plasma, если окно не развернуто
Rectangle {
anchors.fill: parent
visible: root.inEditMode && !root.isWindowMaximized
color: Qt.rgba(1, 1, 1, 0.08)
border.color: Qt.rgba(1, 1, 1, 0.35)
border.width: 1
radius: 4
}
// Отслеживание наведения курсора на группу кнопок
HoverHandler {
id: clusterHoverHandler
}
readonly property bool isClusterHovered: clusterHoverHandler.hovered
RowLayout {
id: buttonsRow
anchors.centerIn: parent
spacing: root.buttonSpacing
opacity: root.isWindowMaximized ? 1.0 : (root.inEditMode ? 0.6 : 0.0)
// Кнопка «Закрыть» (красная)
Rectangle {
id: closeButton
width: root.buttonSize
height: root.buttonSize
radius: root.buttonSize / 2
color: closeMouseArea.pressed ? "#bf4740" : "#ff5f56"
border.color: "#e0443e"
border.width: 1
antialiasing: true
opacity: root.canClose ? 1.0 : 0.4
enabled: root.canClose
// Символ крестика (✕)
Item {
anchors.centerIn: parent
width: 6
height: 6
opacity: buttonsContainer.isClusterHovered ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation { duration: 120 }
}
Rectangle {
anchors.centerIn: parent
width: 7
height: 1.2
color: "#4c0002"
radius: 0.6
rotation: 45
antialiasing: true
}
Rectangle {
anchors.centerIn: parent
width: 7
height: 1.2
color: "#4c0002"
radius: 0.6
rotation: -45
antialiasing: true
}
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.closeWindow()
}
}
// Кнопка «Свернуть» (желтая)
Rectangle {
id: minimizeButton
width: root.buttonSize
height: root.buttonSize
radius: root.buttonSize / 2
color: minMouseArea.pressed ? "#bfa020" : "#ffbd2e"
border.color: "#dea125"
border.width: 1
antialiasing: true
opacity: root.canMinimize ? 1.0 : 0.4
enabled: root.canMinimize
// Символ минуса (−)
Item {
anchors.centerIn: parent
width: 6
height: 6
opacity: buttonsContainer.isClusterHovered ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation { duration: 120 }
}
Rectangle {
anchors.centerIn: parent
width: 7
height: 1.2
color: "#7d4f00"
radius: 0.6
antialiasing: true
}
}
MouseArea {
id: minMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.minimizeWindow()
}
}
// Кнопка «Развернуть / Восстановить» (зеленая)
Rectangle {
id: maximizeButton
width: root.buttonSize
height: root.buttonSize
radius: root.buttonSize / 2
color: maxMouseArea.pressed ? "#1da030" : "#27c93f"
border.color: "#1aab29"
border.width: 1
antialiasing: true
opacity: root.canMaximize ? 1.0 : 0.4
enabled: root.canMaximize
// Символ стрелок macOS (два диагональных треугольника)
Shape {
anchors.centerIn: parent
width: 6
height: 6
opacity: buttonsContainer.isClusterHovered ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation { duration: 120 }
}
asynchronous: false
ShapePath {
strokeWidth: 0
strokeColor: "transparent"
fillColor: "#0d5516"
startX: 6
startY: 0
PathLine { x: 1.8; y: 0 }
PathLine { x: 6; y: 4.2 }
PathLine { x: 6; y: 0 }
}
ShapePath {
strokeWidth: 0
strokeColor: "transparent"
fillColor: "#0d5516"
startX: 0
startY: 6
PathLine { x: 4.2; y: 6 }
PathLine { x: 0; y: 1.8 }
PathLine { x: 0; y: 6 }
}
}
MouseArea {
id: maxMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.toggleMaximizeWindow()
}
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"KPackageStructure": "Plasma/Applet",
"KPlugin": {
"Authors": [
{
"Name": "Plasma Developer",
"Email": "user@example.com"
}
],
"Category": "Windows and Tasks",
"Description": "Кнопки управления окном в стиле macOS (Закрыть, Свернуть, Развернуть), отображаемые на панели только при развернутом окне.",
"Icon": "window-close",
"Id": "org.kde.plasma.macoswindowbuttons",
"License": "GPL-2.0+",
"Name": "macOS Window Buttons",
"Version": "1.0",
"Website": "https://kde.org"
},
"X-Plasma-API-Minimum-Version": "6.0",
"X-Plasma-Provides": [
"org.kde.plasma.windowmanagement"
]
}