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