Initial commit: macOS Window Buttons plasmoid for KDE Plasma 6
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
.directory
|
||||||
|
*~
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
@@ -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+
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user