3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 |
7 | import "../"
8 | import "../utils.js" as Utils
9 | import QtQuick
10 | import QtQuick.Controls
11 | import QtQuick.Layouts
12 | import org.kde.kcmutils as KCM
13 | import org.kde.kirigami as Kirigami
14 | import org.kde.plasma.components as PlasmaComponents
15 |
16 | KCM.SimpleKCM {
17 | id: page
18 |
19 | property alias cfg_widgetActiveTaskSource: widgetActiveTaskSource.currentIndex
20 | property alias cfg_widgetActiveTaskFilterByActivity: widgetActiveTaskFilterByActivity.checked
21 | property alias cfg_widgetActiveTaskFilterByScreen: widgetActiveTaskFilterByScreen.checked
22 | property alias cfg_widgetActiveTaskFilterByVirtualDesktop: widgetActiveTaskFilterByVirtualDesktop.checked
23 | property alias cfg_widgetActiveTaskFilterNotMaximized: widgetActiveTaskFilterNotMaximized.checked
24 | property alias cfg_disableButtonsForNotHoveredWidget: disableButtonsForNotHoveredWidget.checked
25 | property alias cfg_windowTitleDragEnabled: windowTitleDragEnabled.checked
26 | property alias cfg_widgetMouseAreaClickEnabled: widgetMouseAreaClickEnabled.checked
27 | property alias cfg_widgetMouseAreaWheelEnabled: widgetMouseAreaWheelEnabled.checked
28 | property alias cfg_windowTitleDragOnlyMaximized: windowTitleDragOnlyMaximized.checked
29 | property alias cfg_windowTitleDragThreshold: windowTitleDragThreshold.value
30 | property string cfg_widgetMouseAreaLeftDragAction
31 | property string cfg_widgetMouseAreaLeftClickAction
32 | property string cfg_widgetMouseAreaLeftDoubleClickAction
33 | property string cfg_widgetMouseAreaLeftLongPressAction
34 | property string cfg_widgetMouseAreaMiddleDragAction
35 | property string cfg_widgetMouseAreaMiddleClickAction
36 | property string cfg_widgetMouseAreaMiddleDoubleClickAction
37 | property string cfg_widgetMouseAreaMiddleLongPressAction
38 | property alias cfg_widgetMouseAreaWheelFirstEventDistance: widgetMouseAreaWheelFirstEventDistance.value
39 | property alias cfg_widgetMouseAreaWheelNextEventDistance: widgetMouseAreaWheelNextEventDistance.value
40 | property string cfg_widgetMouseAreaWheelUpAction
41 | property string cfg_widgetMouseAreaWheelDownAction
42 | property string cfg_widgetMouseAreaWheelLeftAction
43 | property string cfg_widgetMouseAreaWheelRightAction
44 |
45 | Kirigami.FormLayout {
46 | anchors.left: parent.left
47 | anchors.right: parent.right
48 |
49 | KWinConfig {
50 | id: kWinConfig
51 |
52 | onBorderlessMaximizedWindowsChanged: {
53 | borderlessMaximizedWindowsCheckBox.checked = borderlessMaximizedWindows;
54 | borderlessMaximizedWindowsCheckBox.enabled = kWinConfig.kwriteconfigCommandName !== "" && kWinConfig.kreadconfigCommandName !== "";
55 | }
56 | }
57 |
58 | Kirigami.InlineMessage {
59 | anchors.left: parent.left
60 | anchors.right: parent.right
61 | text: i18n("Some functionality is unsupported or can work unstable in X11 sessions.")
62 | type: Kirigami.MessageType.Warning
63 | visible: Utils.isX11()
64 | }
65 |
66 | Kirigami.InlineMessage {
67 | anchors.left: parent.left
68 | anchors.right: parent.right
69 | text: "QDbus command is missing in the system!"
70 | type: Kirigami.MessageType.Error
71 | visible: kWinConfig.qdbusCommandName === ""
72 | }
73 |
74 | Kirigami.InlineMessage {
75 | anchors.left: parent.left
76 | anchors.right: parent.right
77 | text: "Kwriteconfig command is missing in the system!"
78 | type: Kirigami.MessageType.Error
79 | visible: kWinConfig.kwriteconfigCommandName === ""
80 | }
81 |
82 | Kirigami.InlineMessage {
83 | anchors.left: parent.left
84 | anchors.right: parent.right
85 | text: "Kreadconfig command is missing in the system!"
86 | type: Kirigami.MessageType.Error
87 | visible: kWinConfig.kreadconfigCommandName === ""
88 | }
89 |
90 | Kirigami.InlineMessage {
91 | anchors.left: parent.left
92 | anchors.right: parent.right
93 | text: kWinConfig.lastError
94 | type: Kirigami.MessageType.Error
95 | visible: kWinConfig.lastError !== ""
96 | }
97 |
98 | RowLayout {
99 | Kirigami.FormData.label: i18n("Active task source:")
100 |
101 | ComboBox {
102 | id: widgetActiveTaskSource
103 |
104 | model: [i18n("Active task"), i18n("Last active task"), i18n("Last active maximized task")]
105 | }
106 |
107 | KCM.ContextualHelpButton {
108 | toolTipText: i18n("How to obtain the active task from tasks manager:
Active task: current active task after filtering. The widget will be disabled if the current active task is on another screen, regardless whether there are another tasks on this screen or not.
Last active task: show widget for the last active task after filters applied.
Last active maximized task: show widget for the last active maximized task after filters applied.
")
109 | }
110 | }
111 |
112 | CheckBox {
113 | id: borderlessMaximizedWindowsCheckBox
114 |
115 | Kirigami.FormData.label: i18n("Borderless maximized windows:")
116 | text: i18n("enabled (Will be applied immediately)")
117 | enabled: false
118 | onToggled: function () {
119 | enabled = false;
120 | kWinConfig.setBorderlessMaximizedWindows(checked);
121 | }
122 | }
123 |
124 | CheckBox {
125 | id: widgetActiveTaskFilterByActivity
126 |
127 | Kirigami.FormData.label: i18n("Filter active task by activity:")
128 | text: i18n("enabled")
129 | }
130 |
131 | CheckBox {
132 | id: widgetActiveTaskFilterByScreen
133 |
134 | Kirigami.FormData.label: i18n("Filter active task by screen:")
135 | text: i18n("enabled")
136 | }
137 |
138 | CheckBox {
139 | id: widgetActiveTaskFilterByVirtualDesktop
140 |
141 | Kirigami.FormData.label: i18n("Filter active task by virtual desktop:")
142 | text: i18n("enabled")
143 | }
144 |
145 | CheckBox {
146 | id: widgetActiveTaskFilterNotMaximized
147 |
148 | enabled: widgetActiveTaskSource.currentIndex !== ActiveTasksModel.ActiveTaskSource.LastActiveMaximized
149 | Kirigami.FormData.label: i18n("Disable for not maximized:")
150 | text: i18n("enabled")
151 | }
152 |
153 | CheckBox {
154 | id: disableButtonsForNotHoveredWidget
155 |
156 | Kirigami.FormData.label: i18n("Disable buttons for not hovered widget:")
157 | text: i18n("enabled")
158 | }
159 |
160 | Kirigami.Separator {
161 | Kirigami.FormData.isSection: true
162 | Kirigami.FormData.label: i18n("Mouse area drag")
163 | }
164 |
165 | CheckBox {
166 | id: windowTitleDragEnabled
167 |
168 | text: i18n("enabled")
169 | }
170 |
171 | CheckBox {
172 | id: windowTitleDragOnlyMaximized
173 |
174 | enabled: windowTitleDragEnabled.checked
175 | Kirigami.FormData.label: i18n("Only maximized:")
176 | text: i18n("enabled")
177 | }
178 |
179 | SpinBox {
180 | id: windowTitleDragThreshold
181 |
182 | enabled: windowTitleDragEnabled.checked
183 | Kirigami.FormData.label: i18n("Threshold:")
184 | from: 0
185 | to: 512
186 | }
187 |
188 | Kirigami.InlineMessage {
189 | anchors.left: parent.left
190 | anchors.right: parent.right
191 | text: i18n("Window Move action is incompatible with Mouse Drag in X11 sessions.")
192 | type: Kirigami.MessageType.Error
193 | visible: Utils.isX11() && leftButtonDragAction.currentValue == "Window Move"
194 | }
195 |
196 | KWinShortcutComboBox {
197 | id: leftButtonDragAction
198 |
199 | label: i18n("Left button drag:")
200 | enabled: windowTitleDragEnabled.checked
201 | initialValue: cfg_widgetMouseAreaLeftDragAction
202 | onActivated: cfg_widgetMouseAreaLeftDragAction = currentValue
203 | }
204 |
205 | Kirigami.InlineMessage {
206 | anchors.left: parent.left
207 | anchors.right: parent.right
208 | text: i18n("Window Move action is incompatible with Mouse Drag in X11 sessions.")
209 | type: Kirigami.MessageType.Error
210 | visible: Utils.isX11() && middleButtonDragAction.currentValue == "Window Move"
211 | }
212 |
213 | KWinShortcutComboBox {
214 | id: middleButtonDragAction
215 |
216 | label: i18n("Middle button drag:")
217 | enabled: windowTitleDragEnabled.checked
218 | initialValue: cfg_widgetMouseAreaMiddleDragAction
219 | onActivated: cfg_widgetMouseAreaMiddleDragAction = currentValue
220 | }
221 |
222 | Kirigami.Separator {
223 | Kirigami.FormData.isSection: true
224 | Kirigami.FormData.label: i18n("Mouse area click")
225 | }
226 |
227 | CheckBox {
228 | id: widgetMouseAreaClickEnabled
229 |
230 | text: i18n("enabled")
231 | }
232 |
233 | KWinShortcutComboBox {
234 | enabled: widgetMouseAreaClickEnabled.checked
235 | label: i18n("Left button click:")
236 | initialValue: cfg_widgetMouseAreaLeftClickAction
237 | onActivated: cfg_widgetMouseAreaLeftClickAction = currentValue
238 | }
239 |
240 | KWinShortcutComboBox {
241 | enabled: widgetMouseAreaClickEnabled.checked
242 | label: i18n("Left button double-click:")
243 | initialValue: cfg_widgetMouseAreaLeftDoubleClickAction
244 | onActivated: cfg_widgetMouseAreaLeftDoubleClickAction = currentValue
245 | }
246 |
247 | KWinShortcutComboBox {
248 | enabled: widgetMouseAreaClickEnabled.checked
249 | label: i18n("Left button long press:")
250 | initialValue: cfg_widgetMouseAreaLeftLongPressAction
251 | onActivated: cfg_widgetMouseAreaLeftLongPressAction = currentValue
252 | }
253 |
254 | KWinShortcutComboBox {
255 | enabled: widgetMouseAreaClickEnabled.checked
256 | label: i18n("Middle button click:")
257 | initialValue: cfg_widgetMouseAreaMiddleClickAction
258 | onActivated: cfg_widgetMouseAreaMiddleClickAction = currentValue
259 | }
260 |
261 | KWinShortcutComboBox {
262 | enabled: widgetMouseAreaClickEnabled.checked
263 | label: i18n("Middle button double-click:")
264 | initialValue: cfg_widgetMouseAreaMiddleDoubleClickAction
265 | onActivated: cfg_widgetMouseAreaMiddleDoubleClickAction = currentValue
266 | }
267 |
268 | KWinShortcutComboBox {
269 | enabled: widgetMouseAreaClickEnabled.checked
270 | label: i18n("Middle button long press:")
271 | initialValue: cfg_widgetMouseAreaMiddleLongPressAction
272 | onActivated: cfg_widgetMouseAreaMiddleLongPressAction = currentValue
273 | }
274 |
275 | Kirigami.Separator {
276 | Kirigami.FormData.isSection: true
277 | Kirigami.FormData.label: i18n("Mouse area wheel")
278 | }
279 |
280 | CheckBox {
281 | id: widgetMouseAreaWheelEnabled
282 |
283 | text: i18n("enabled")
284 | }
285 |
286 | SpinBox {
287 | id: widgetMouseAreaWheelFirstEventDistance
288 |
289 | enabled: widgetMouseAreaWheelEnabled.checked
290 | Kirigami.FormData.label: i18n("First event distance:")
291 | from: 0
292 | to: 4096
293 | }
294 |
295 | SpinBox {
296 | id: widgetMouseAreaWheelNextEventDistance
297 |
298 | enabled: widgetMouseAreaWheelEnabled.checked
299 | Kirigami.FormData.label: i18n("Next event distance:")
300 | from: 0
301 | to: 4096
302 | }
303 |
304 | KWinShortcutComboBox {
305 | enabled: widgetMouseAreaWheelEnabled.checked
306 | label: i18n("Wheel up:")
307 | initialValue: cfg_widgetMouseAreaWheelUpAction
308 | onActivated: cfg_widgetMouseAreaWheelUpAction = currentValue
309 | }
310 |
311 | KWinShortcutComboBox {
312 | enabled: widgetMouseAreaWheelEnabled.checked
313 | label: i18n("Wheel down:")
314 | initialValue: cfg_widgetMouseAreaWheelDownAction
315 | onActivated: cfg_widgetMouseAreaWheelDownAction = currentValue
316 | }
317 |
318 | KWinShortcutComboBox {
319 | enabled: widgetMouseAreaWheelEnabled.checked
320 | label: i18n("Wheel left:")
321 | initialValue: cfg_widgetMouseAreaWheelLeftAction
322 | onActivated: cfg_widgetMouseAreaWheelLeftAction = currentValue
323 | }
324 |
325 | KWinShortcutComboBox {
326 | enabled: widgetMouseAreaWheelEnabled.checked
327 | label: i18n("Wheel right:")
328 | initialValue: cfg_widgetMouseAreaWheelRightAction
329 | onActivated: cfg_widgetMouseAreaWheelRightAction = currentValue
330 | }
331 | }
332 | }
333 |
--------------------------------------------------------------------------------
/package/contents/ui/config/FormSlider.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 |
7 | import QtQuick
8 | import QtQuick.Controls
9 | import QtQuick.Layouts
10 | import org.kde.kirigami as Kirigami
11 |
12 | RowLayout {
13 | anchors.left: parent.left
14 | anchors.right: parent.right
15 |
16 | property var text: ""
17 | property var from: 0
18 | property var to: 100
19 | property var stepSize: 1
20 | property alias value: slider.value
21 |
22 | Label {
23 | Layout.alignment: Qt.AlignLeft
24 | text: parent.text
25 | }
26 |
27 | Slider {
28 | id: slider
29 | Layout.alignment: Qt.AlignHCenter
30 | Layout.fillWidth: true
31 | from: parent.from
32 | to: parent.to
33 | stepSize: parent.stepSize
34 | }
35 |
36 | SpinBox {
37 | id: spin
38 | Layout.alignment: Qt.AlignRight
39 | value: slider.value
40 | from: parent.from
41 | to: parent.to
42 | }
43 |
44 | Binding {
45 | slider.value: spin.value
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/package/contents/ui/config/KWinShortcutComboBox.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 |
7 | import "../utils.js" as Utils
8 | import QtQuick
9 | import QtQuick.Controls
10 | import org.kde.kirigami as Kirigami
11 |
12 | ComboBox {
13 | property int maxStringLength: 30
14 | property string label
15 | property var initialValue
16 |
17 | function updateCurrentIndex() {
18 | enabled = model.length > 1;
19 | currentIndex = indexOfValue(initialValue);
20 | }
21 |
22 | Component.onCompleted: updateCurrentIndex()
23 | onModelChanged: updateCurrentIndex()
24 | Kirigami.FormData.label: label
25 | model: [
26 | {
27 | "shortcut": "",
28 | "truncatedName": " "
29 | }
30 | ].concat(kWinConfig.shortcutNames.map(shortcut => {
31 | return ({
32 | "shortcut": shortcut,
33 | "truncatedName": Utils.truncateString(shortcut, 40)
34 | });
35 | }))
36 | valueRole: "shortcut"
37 | textRole: "truncatedName"
38 | }
39 |
--------------------------------------------------------------------------------
/package/contents/ui/config/TitleReplacements.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 |
7 | import "../"
8 | import "../config"
9 | import QtQuick
10 | import QtQuick.Controls
11 | import QtQuick.Layouts
12 | import org.kde.kcmutils as KCM
13 | import org.kde.kirigami as Kirigami
14 | import org.kde.plasma.components as PlasmaComponents
15 | import "../utils.js" as Utils
16 |
17 | KCM.SimpleKCM {
18 | id: page
19 |
20 | property list cfg_titleReplacementsPatterns
21 | property list cfg_titleReplacementsTemplates
22 | property list cfg_titleReplacementsTypes
23 |
24 | enum Type {
25 | String,
26 | Regex
27 | }
28 |
29 | ListModel {
30 | id: replacementsModel
31 |
32 | function updateModelFromConfig() {
33 | clear();
34 | for (let i = 0; i < cfg_titleReplacementsPatterns.length; i++) {
35 | append({
36 | "pattern": cfg_titleReplacementsPatterns[i],
37 | "template": cfg_titleReplacementsTemplates[i],
38 | "type": cfg_titleReplacementsTypes[i]
39 | });
40 | }
41 | }
42 |
43 | function updateConfigFromModel() {
44 | Qt.callLater(_updateConfigFromModel);
45 | }
46 |
47 | function _updateConfigFromModel() {
48 | const length = count;
49 | cfg_titleReplacementsPatterns.length = length;
50 | cfg_titleReplacementsTemplates.length = length;
51 | cfg_titleReplacementsTypes.length = length;
52 | for (let i = 0; i < length; i++) {
53 | const rowValue = get(i);
54 | cfg_titleReplacementsPatterns[i] = rowValue.pattern;
55 | cfg_titleReplacementsTemplates[i] = rowValue.template;
56 | cfg_titleReplacementsTypes[i] = rowValue.type;
57 | }
58 | }
59 |
60 | function pushNewReplacement() {
61 | append({
62 | "pattern": "",
63 | "template": "",
64 | "type": TitleReplacements.Type.String
65 | });
66 | updateConfigFromModel();
67 | }
68 |
69 | function deleteReplacement(index) {
70 | remove(index);
71 | updateConfigFromModel();
72 | }
73 |
74 | function setType(index, type) {
75 | set(index, {
76 | "type": type
77 | });
78 | updateConfigFromModel();
79 | }
80 |
81 | function setPattern(index, pattern) {
82 | set(index, {
83 | "pattern": pattern
84 | });
85 | updateConfigFromModel();
86 | }
87 |
88 | function setTemplate(index, template) {
89 | set(index, {
90 | "template": template
91 | });
92 | updateConfigFromModel();
93 | }
94 |
95 | function moveReplacement(from, to) {
96 | move(from, to, 1);
97 | updateConfigFromModel();
98 | }
99 |
100 | Component.onCompleted: updateModelFromConfig()
101 | }
102 |
103 | ColumnLayout {
104 | RowLayout {
105 | Button {
106 | text: i18n("Add Title Replacement")
107 | onClicked: replacementsModel.pushNewReplacement()
108 | }
109 | Label {
110 | text: i18n("JavaScript RegExp Reference")
111 | onLinkActivated: function (link) {
112 | Qt.openUrlExternally(link);
113 | }
114 | verticalAlignment: Text.AlignVCenter
115 | }
116 | }
117 | Repeater {
118 | id: replacementsRepeater
119 |
120 | model: replacementsModel
121 | delegate: RowLayout {
122 | id: replacement
123 | required property int index
124 | required property var modelData
125 |
126 | Drag.source: dragArea
127 | Drag.active: dragArea.drag.active
128 | Drag.hotSpot.y: height / 2
129 |
130 | Kirigami.Icon {
131 | Layout.maximumWidth: Kirigami.Units.gridUnit
132 |
133 | source: "transform-move-vertical"
134 | MouseArea {
135 | id: dragArea
136 | anchors.fill: parent
137 | drag.axis: Drag.YAxis
138 | drag.target: replacement
139 | cursorShape: Qt.DragMoveCursor
140 |
141 | drag {
142 | onActiveChanged: function () {
143 | if (!drag.active) {
144 | replacementsModel.updateModelFromConfig();
145 | }
146 | }
147 | }
148 |
149 | states: State {
150 | when: dragArea.drag.active
151 |
152 | PropertyChanges {
153 | target: replacement
154 | z: 1
155 | }
156 | }
157 |
158 | DropArea {
159 | anchors.fill: parent
160 | onEntered: drag => {
161 | replacementsModel.moveReplacement(drag.source.DelegateModel.itemsIndex, dragArea.DelegateModel.itemsIndex);
162 | }
163 | }
164 | }
165 | }
166 |
167 | ComboBox {
168 | id: titleReplacementsType
169 | Layout.maximumWidth: Kirigami.Units.gridUnit * 5
170 |
171 | model: [i18n("String"), i18n("RegExp")]
172 | onActivated: function () {
173 | replacementsModel.setType(index, currentIndex);
174 | }
175 | currentIndex: modelData.type
176 | }
177 |
178 | TextField {
179 | id: titleReplacementsPattern
180 |
181 | onTextEdited: function () {
182 | replacementsModel.setPattern(index, text);
183 | }
184 | Layout.alignment: Qt.AlignLeft
185 | text: modelData.pattern
186 | }
187 |
188 | Label {
189 | text: "=>"
190 | }
191 |
192 | TextField {
193 | id: titleReplacementsTemplate
194 |
195 | onTextEdited: function () {
196 | replacementsModel.setTemplate(index, text);
197 | }
198 | Layout.alignment: Qt.AlignLeft
199 | text: modelData.template
200 | }
201 |
202 | Button {
203 | icon.name: "delete"
204 | onClicked: function () {
205 | replacementsModel.deleteReplacement(replacement.index);
206 | }
207 | }
208 | }
209 | }
210 | RowLayout {
211 | visible: replacementsRepeater.count > 5
212 | Button {
213 | text: i18n("Add Title Replacement")
214 | onClicked: replacementsModel.pushNewReplacement()
215 | }
216 | Label {
217 | text: i18n("JavaScript RegExp Reference")
218 | onLinkActivated: function (link) {
219 | Qt.openUrlExternally(link);
220 | }
221 | verticalAlignment: Text.AlignVCenter
222 | }
223 | }
224 | }
225 |
226 | footer: Kirigami.FormLayout {
227 | Kirigami.Separator {
228 | Kirigami.FormData.isSection: true
229 | Kirigami.FormData.label: i18n("Title Replacements Testing")
230 | }
231 |
232 | TextField {
233 | id: testInput
234 |
235 | Kirigami.FormData.label: i18n("Test input:")
236 | Layout.alignment: Qt.AlignLeft
237 | Layout.minimumWidth: 400
238 |
239 | text: " -- 123 Application title : Freeware / buy 3 - oneone"
240 | }
241 |
242 | TextField {
243 | id: testOutput
244 |
245 | Kirigami.FormData.label: i18n("Test output:")
246 | Layout.alignment: Qt.AlignLeft
247 | readOnly: true
248 |
249 | Connections {
250 | target: testInput
251 |
252 | function onTextChanged() {
253 | testOutput.updateTestOutput();
254 | }
255 | }
256 |
257 | Connections {
258 | target: replacementsModel
259 |
260 | function onDataChanged() {
261 | testOutput.updateTestOutput();
262 | }
263 |
264 | function onRowsRemoved() {
265 | testOutput.updateTestOutput();
266 | }
267 | }
268 |
269 | function _updateTestOutput() {
270 | let outputText = testInput.text;
271 | for (let i = 0; i < replacementsModel.count; i++) {
272 | const rowValue = replacementsModel.get(i);
273 | const replacement = Utils.Replacement.createReplacement(rowValue.type, rowValue.pattern, rowValue.template);
274 | outputText = replacement.replace(outputText);
275 | }
276 | testOutput.text = outputText;
277 | }
278 |
279 | function updateTestOutput() {
280 | Qt.callLater(_updateTestOutput);
281 | }
282 |
283 | Component.onCompleted: updateTestOutput()
284 | }
285 | }
286 | }
287 |
--------------------------------------------------------------------------------
/package/contents/ui/config/WidgetElements.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 |
7 | import "../"
8 | import "../utils.js" as Utils
9 | import QtQuick
10 | import QtQuick.Controls
11 | import QtQuick.Layouts
12 | import org.kde.kcmutils as KCM
13 | import org.kde.kirigami as Kirigami
14 | import org.kde.plasma.components as PlasmaComponents
15 |
16 | RowLayout {
17 | id: widgetElements
18 |
19 | property alias model: sortableItemsRow.model
20 | property var iconSize: Kirigami.Units.iconSizes.medium
21 | property var elements: []
22 |
23 | Kirigami.Theme.colorSet: Kirigami.Theme.View
24 | Kirigami.Theme.inherit: false
25 |
26 | Layout.fillWidth: true
27 | Layout.preferredWidth: Kirigami.Units.gridUnit * 21
28 | spacing: 0
29 |
30 | Component.onCompleted: function () {
31 | model.ignoreInsertEvent = true;
32 | for (var i = 0; i < elements.length; i++) {
33 | model.append({
34 | "value": elements[i]
35 | });
36 | }
37 | model.ignoreInsertEvent = false;
38 | }
39 |
40 | model: ListModel {
41 | property bool ignoreInsertEvent: false
42 |
43 | function updateConfigFromModel() {
44 | elements = [];
45 | for (var i = 0; i < count; i++) {
46 | elements.push(get(i).value);
47 | }
48 | }
49 |
50 | onRowsMoved: updateConfigFromModel()
51 | onRowsRemoved: updateConfigFromModel()
52 | onRowsInserted: ignoreInsertEvent || updateConfigFromModel()
53 | }
54 |
55 | Rectangle {
56 | z: 1
57 | border.color: Kirigami.Theme.disabledTextColor
58 | color: Kirigami.Theme.backgroundColor
59 | height: Kirigami.Units.iconSizes.medium
60 | Layout.alignment: Qt.AlignLeft
61 | Layout.fillWidth: true
62 | Layout.margins: 1
63 | radius: 2
64 |
65 | SortableItemsRow {
66 | id: sortableItemsRow
67 |
68 | anchors.fill: parent
69 | sourceComponent: widgetElementLoaderDelegate
70 |
71 | Component {
72 | id: widgetElementLoaderDelegate
73 |
74 | Loader {
75 | id: widgetElementLoader
76 |
77 | property var modelData
78 | property var elementModel: Utils.widgetElementModelFromName(modelData)
79 |
80 | onLoaded: function () {
81 | item.modelData = elementModel;
82 | }
83 | sourceComponent: {
84 | switch (elementModel.type) {
85 | case WidgetElement.Type.WindowControlButton:
86 | return windowControlButton;
87 | case WidgetElement.Type.WindowTitle:
88 | return windowTitle;
89 | case WidgetElement.Type.WindowIcon:
90 | return windowIcon;
91 | case WidgetElement.Type.Spacer:
92 | return spacerIcon;
93 | }
94 | }
95 | }
96 | }
97 |
98 | Component {
99 | id: windowControlButton
100 |
101 | WindowControlButton {
102 | id: windowControlButton
103 |
104 | property var modelData
105 |
106 | height: widgetElements.iconSize
107 | width: widgetElements.iconSize
108 | buttonType: modelData.windowControlButtonType
109 | mouseAreaEnabled: false
110 | }
111 | }
112 |
113 | Component {
114 | id: windowTitle
115 |
116 | PlasmaComponents.Label {
117 | property var modelData
118 |
119 | text: i18n("Title")
120 | font.bold: page.cfg_windowTitleFontBold
121 | maximumLineCount: 1
122 | elide: Text.ElideRight
123 | wrapMode: Text.WrapAnywhere
124 | verticalAlignment: Text.AlignVCenter
125 | height: widgetElements.iconSize
126 | width: widgetElements.iconSize
127 | }
128 | }
129 |
130 | Component {
131 | id: windowIcon
132 |
133 | Kirigami.Icon {
134 | property var modelData
135 |
136 | source: "window"
137 | height: widgetElements.iconSize
138 | width: widgetElements.iconSize
139 | }
140 | }
141 |
142 | Component {
143 | id: spacerIcon
144 |
145 | Kirigami.Icon {
146 | property var modelData
147 |
148 | source: "adjustcol"
149 | height: widgetElements.iconSize
150 | width: widgetElements.iconSize / 2
151 | }
152 | }
153 | }
154 | }
155 |
156 | Rectangle {
157 | id: elementRemoveArea
158 |
159 | z: 0
160 | border.color: Kirigami.Theme.disabledTextColor
161 | color: Kirigami.Theme.negativeBackgroundColor
162 | height: Kirigami.Units.iconSizes.medium
163 | width: Kirigami.Units.iconSizes.medium
164 | Layout.alignment: Qt.AlignLeft
165 | Layout.margins: 1
166 | radius: 2
167 |
168 | property bool dragActive: false
169 |
170 | MouseArea {
171 | id: cursorShapeMouseArea
172 | anchors.fill: parent
173 | enabled: false
174 |
175 | states: State {
176 | when: elementRemoveArea.dragActive
177 |
178 | PropertyChanges {
179 | cursorShapeMouseArea.cursorShape: Qt.DragMoveCursor
180 | }
181 | }
182 | }
183 |
184 | Kirigami.Icon {
185 | anchors.fill: parent
186 | source: "delete"
187 | }
188 |
189 | DropArea {
190 | id: dropArea
191 |
192 | property var dragSource
193 |
194 | anchors.fill: parent
195 | anchors.margins: 5
196 | onEntered: dragEvent => {
197 | dragSource = dragEvent.source;
198 | elementRemoveArea.dragActive = true;
199 | }
200 | onExited: () => {
201 | if (dragSource && !dragSource.pressed)
202 | widgetElements.model.remove(dragSource.DelegateModel.itemsIndex);
203 | dragSource = undefined;
204 | elementRemoveArea.dragActive = false;
205 | }
206 |
207 | states: State {
208 | when: dropArea.dragSource !== undefined
209 |
210 | PropertyChanges {
211 | elementRemoveArea.border.color: "red"
212 | }
213 | }
214 | }
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/package/contents/ui/config/effect/ActiveWindowFlagCondition.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2025 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 | import QtQuick
7 | import QtQuick.Controls
8 | import QtQuick.Layouts
9 |
10 | RowLayout {
11 | property var arg0
12 | property var arg1
13 |
14 | signal arg0Updated(var value)
15 | signal arg1Updated(var value)
16 |
17 | onArg0Changed: function () {
18 | if (propertyName.indexOfValue(arg0) === -1 && propertyName.count > 0) {
19 | arg0Updated(propertyName.valueAt(0));
20 | }
21 | }
22 |
23 | ComboBox {
24 | id: propertyName
25 |
26 | Layout.fillWidth: true
27 |
28 | textRole: "name"
29 | valueRole: "value"
30 | wheelEnabled: false
31 |
32 | currentIndex: indexOfValue(arg0)
33 | onActivated: arg0Updated(currentValue)
34 |
35 | model: [
36 | {
37 | name: i18n("Minimizable"),
38 | value: "minimizable"
39 | },
40 | {
41 | name: i18n("Maximizable"),
42 | value: "maximizable"
43 | },
44 | {
45 | name: i18n("Closable"),
46 | value: "closable"
47 | },
48 | {
49 | name: i18n("Movable"),
50 | value: "movable"
51 | },
52 | {
53 | name: i18n("Minimized"),
54 | value: "minimized"
55 | },
56 | {
57 | name: i18n("Maximized"),
58 | value: "maximized"
59 | },
60 | {
61 | name: i18n("Shadeable"),
62 | value: "shadeable"
63 | },
64 | {
65 | name: i18n("Shaded"),
66 | value: "shaded"
67 | },
68 | {
69 | name: i18n("Has App Menu"),
70 | value: "hasAppMenu"
71 | },
72 | {
73 | name: i18n("On all Virtual Desktops"),
74 | value: "onAllVirtualDesktops"
75 | },
76 | {
77 | name: i18n("Keep Above"),
78 | value: "keepAbove"
79 | },
80 | {
81 | name: i18n("Keep Below"),
82 | value: "keepBelow"
83 | },
84 | {
85 | name: i18n("Full Screenable"),
86 | value: "fullScreenable"
87 | },
88 | {
89 | name: i18n("Full Screen"),
90 | value: "fullScreen"
91 | },
92 | {
93 | name: i18n("Resizable"),
94 | value: "resizable"
95 | },
96 | {
97 | name: i18n("Active"),
98 | value: "active"
99 | }
100 | ]
101 | }
102 |
103 | CheckBox {
104 | id: propertyValue
105 |
106 | checked: !!arg1
107 | onToggled: arg1Updated(checked)
108 |
109 | text: i18n("value")
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/package/contents/ui/config/effect/ActiveWindowRegExpCondition.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2025 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 | import QtQuick
7 | import QtQuick.Controls
8 | import QtQuick.Layouts
9 |
10 | RowLayout {
11 | property var arg0
12 | property var arg1
13 |
14 | signal arg0Updated(var value)
15 | signal arg1Updated(var value)
16 |
17 | onArg0Changed: function () {
18 | if (propertyName.indexOfValue(arg0) === -1 && propertyName.count > 0) {
19 | arg0Updated(propertyName.valueAt(0));
20 | }
21 | }
22 |
23 | ComboBox {
24 | id: propertyName
25 |
26 | currentIndex: indexOfValue(arg0)
27 | onActivated: arg0Updated(currentValue)
28 |
29 | textRole: "name"
30 | valueRole: "value"
31 | wheelEnabled: false
32 |
33 | model: [
34 | {
35 | name: i18n("App Name"),
36 | value: "appName"
37 | },
38 | {
39 | name: i18n("Generic App Name"),
40 | value: "genericAppName"
41 | },
42 | {
43 | name: i18n("Full App Name"),
44 | value: "decoration"
45 | },
46 | {
47 | name: i18n("Icon"),
48 | value: "icon"
49 | }
50 | ]
51 | }
52 |
53 | TextField {
54 | id: propertyValue
55 |
56 | Layout.fillWidth: true
57 | text: arg1 || ""
58 | onTextEdited: arg1Updated(text)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/package/contents/ui/config/effect/ActiveWindowStringCondition.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2025 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 | import QtQuick
7 | import QtQuick.Controls
8 | import QtQuick.Layouts
9 |
10 | RowLayout {
11 | property var arg0
12 | property var arg1
13 |
14 | signal arg0Updated(var value)
15 | signal arg1Updated(var value)
16 |
17 | onArg0Changed: function () {
18 | if (propertyName.indexOfValue(arg0) === -1 && propertyName.count > 0) {
19 | arg0Updated(propertyName.valueAt(0));
20 | }
21 | }
22 |
23 | ComboBox {
24 | id: propertyName
25 |
26 | currentIndex: indexOfValue(arg0)
27 | onActivated: arg0Updated(currentValue)
28 |
29 | textRole: "name"
30 | valueRole: "value"
31 | wheelEnabled: false
32 |
33 | model: [
34 | {
35 | name: i18n("App Name"),
36 | value: "appName"
37 | },
38 | {
39 | name: i18n("Generic App Name"),
40 | value: "genericAppName"
41 | },
42 | {
43 | name: i18n("Full App Name"),
44 | value: "decoration"
45 | },
46 | {
47 | name: i18n("Icon"),
48 | value: "icon"
49 | }
50 | ]
51 | }
52 |
53 | TextField {
54 | id: propertyValue
55 |
56 | Layout.fillWidth: true
57 | text: arg1 || ""
58 | onTextEdited: arg1Updated(text)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/package/contents/ui/config/effect/AddPresetEffect.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2025 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 | import QtQuick
7 | import QtQuick.Controls
8 |
9 | ComboBox {
10 | textRole: "name"
11 | valueRole: "value"
12 | wheelEnabled: false
13 | displayText: i18n("Add Preset Effect")
14 |
15 | model: [
16 | {
17 | name: i18n("White Glow"),
18 | value: {
19 | shadowOpacity: 0.5,
20 | shadowColor: Qt.color("white"),
21 | maskEnabled: false,
22 | maskInverted: false
23 | }
24 | },
25 | {
26 | name: i18n("Blur"),
27 | value: {
28 | blur: 0.5,
29 | maskEnabled: false,
30 | maskInverted: false
31 | }
32 | },
33 | {
34 | name: i18n("Desaturate"),
35 | value: {
36 | contrast: -0.5,
37 | saturation: -1.0,
38 | maskEnabled: false,
39 | maskInverted: false
40 | }
41 | }
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/package/contents/ui/config/effect/Checkable.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 | import QtQuick
7 | import QtQuick.Controls
8 | import org.kde.kirigami as Kirigami
9 | import QtQuick.Layouts
10 |
11 | RowLayout {
12 | Layout.fillWidth: true
13 | Layout.leftMargin: Kirigami.Units.gridUnit
14 | Layout.rightMargin: Kirigami.Units.gridUnit
15 | property alias value: checkBox.checked
16 | property alias label: label.text
17 |
18 | signal valueUpdated(var updatedValue)
19 |
20 | Label {
21 | id: label
22 | text: "CheckBox"
23 | Layout.preferredWidth: 200
24 | }
25 |
26 | CheckBox {
27 | id: checkBox
28 |
29 | onToggled: function () {
30 | valueUpdated(checked);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/package/contents/ui/config/effect/CheckableColorSlider.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 |
7 | import QtQuick
8 | import QtQuick.Controls
9 | import org.kde.kirigami as Kirigami
10 | import QtQuick.Layouts
11 |
12 | RowLayout {
13 | id: root
14 |
15 | property alias label: label.text
16 | property var value: undefined
17 | property alias checked: checkBox.checked
18 | property color initValue: "black"
19 |
20 | signal valueUpdated(var updatedValue)
21 |
22 | Layout.fillWidth: true
23 | Layout.leftMargin: Kirigami.Units.gridUnit
24 | Layout.rightMargin: Kirigami.Units.gridUnit
25 |
26 | onValueChanged: function () {
27 | if (value === undefined) {
28 | checkBox.checked = false;
29 | } else {
30 | checkBox.checked = true;
31 | red.value = value.r;
32 | green.value = value.g;
33 | blue.value = value.b;
34 | alpha.value = value.a;
35 | }
36 | }
37 |
38 | function updateValue() {
39 | if (checkBox.checked) {
40 | root.value = Qt.rgba(red.value, green.value, blue.value, alpha.value);
41 | } else {
42 | root.value = undefined;
43 | }
44 | valueUpdated(root.value);
45 | }
46 |
47 | Label {
48 | id: label
49 | text: "Color"
50 | Layout.preferredWidth: 200
51 | }
52 |
53 | CheckBox {
54 | id: checkBox
55 |
56 | onToggled: updateValue()
57 | }
58 |
59 | Slider {
60 | id: red
61 | Layout.fillWidth: true
62 |
63 | enabled: checkBox.checked
64 | value: root.initValue.r
65 | from: 0
66 | to: 1
67 | stepSize: 0.01
68 |
69 | onMoved: updateValue()
70 |
71 | Rectangle {
72 | anchors.fill: parent
73 | color: "#22FF0000"
74 | }
75 | }
76 |
77 | Slider {
78 | id: green
79 | Layout.fillWidth: true
80 |
81 | enabled: checkBox.checked
82 | value: root.initValue.g
83 | from: 0
84 | to: 1
85 | stepSize: 0.01
86 |
87 | onMoved: updateValue()
88 |
89 | Rectangle {
90 | anchors.fill: parent
91 | color: "#2200FF00"
92 | }
93 | }
94 |
95 | Slider {
96 | id: blue
97 | Layout.fillWidth: true
98 |
99 | enabled: checkBox.checked
100 | value: root.initValue.b
101 | from: 0
102 | to: 1
103 | stepSize: 0.01
104 |
105 | onMoved: updateValue()
106 |
107 | Rectangle {
108 | anchors.fill: parent
109 | color: "#220000FF"
110 | }
111 | }
112 |
113 | Slider {
114 | id: alpha
115 | Layout.fillWidth: true
116 |
117 | enabled: checkBox.checked
118 | value: root.initValue.a
119 | from: 0
120 | to: 1
121 | stepSize: 0.01
122 |
123 | onMoved: updateValue()
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/package/contents/ui/config/effect/CheckableSlider.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 |
7 | import QtQuick
8 | import QtQuick.Controls
9 | import org.kde.kirigami as Kirigami
10 | import QtQuick.Layouts
11 |
12 | RowLayout {
13 | id: root
14 |
15 | property alias label: label.text
16 | property var value: undefined
17 | property alias initValue: slider.value
18 | property alias from: slider.from
19 | property alias to: slider.to
20 | property alias checked: checkBox.checked
21 | property alias stepSize: slider.stepSize
22 |
23 | signal valueUpdated(var updatedValue)
24 |
25 | Layout.fillWidth: true
26 | Layout.leftMargin: Kirigami.Units.gridUnit
27 | Layout.rightMargin: Kirigami.Units.gridUnit
28 |
29 | onValueChanged: function () {
30 | if (value === undefined) {
31 | checkBox.checked = false;
32 | } else {
33 | checkBox.checked = true;
34 | slider.value = value;
35 | }
36 | }
37 |
38 | Label {
39 | id: label
40 | text: "Slider"
41 | Layout.preferredWidth: 200
42 | }
43 |
44 | CheckBox {
45 | id: checkBox
46 |
47 | onToggled: function () {
48 | if (checked) {
49 | root.value = slider.value;
50 | } else {
51 | root.value = undefined;
52 | }
53 | valueUpdated(root.value);
54 | }
55 | }
56 |
57 | Slider {
58 | id: slider
59 | Layout.fillWidth: true
60 |
61 | enabled: checkBox.checked
62 | value: 0
63 | from: -1
64 | to: 1
65 | stepSize: 0.05
66 |
67 | onMoved: function () {
68 | if (checkBox.checked) {
69 | root.value = value;
70 | valueUpdated(root.value);
71 | }
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/package/contents/ui/config/effect/ConfigEffects.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 |
7 | import "../../"
8 | import "../../config"
9 | import "../../config/effect"
10 | import "../../common"
11 | import QtQuick
12 | import QtQuick.Controls
13 | import QtQuick.Layouts
14 | import QtQuick.Effects
15 | import org.kde.kcmutils as KCM
16 | import org.kde.kirigami as Kirigami
17 | import org.kde.plasma.components as PlasmaComponents
18 |
19 | KCM.SimpleKCM {
20 | id: page
21 |
22 | property alias cfg_effects: effectsListModel.stringListModel
23 | property alias cfg_effectRules: rulesListModel.stringListModel
24 |
25 | verticalScrollBarPolicy: ScrollBar.AlwaysOff
26 |
27 | JsonListModel {
28 | id: effectsListModel
29 | tempModel: EffectModel {}
30 | }
31 |
32 | JsonListModel {
33 | id: rulesListModel
34 | tempModel: RuleModel {}
35 |
36 | function removeConnectedRulesAndEffect(index) {
37 | for (let i = rulesListModel.count - 1; i >= 0; i--) {
38 | const rule = rulesListModel.get(i);
39 | if (rule.effectIndex === index) {
40 | rulesListModel.deleteModel(i);
41 | } else if (rule.effectIndex > index) {
42 | rulesRepeater.itemAt(i).ruleModel.effectIndex = rule.effectIndex - 1;
43 | }
44 | }
45 | effectsListModel.deleteModel(index);
46 | }
47 | }
48 |
49 | Dialog {
50 | id: removeConnectedRulesDialog
51 | title: i18n("Remove Effect and connected Rules")
52 | standardButtons: Dialog.Ok | Dialog.Cancel
53 | width: 400
54 | modal: true
55 |
56 | onAccepted: rulesListModel.removeConnectedRulesAndEffect(effectIndex)
57 |
58 | property int connectedRulesCount: 0
59 | property int effectIndex: 0
60 |
61 | contentItem: Text {
62 | wrapMode: Text.WordWrap
63 | text: i18n("%1 Rules are connected to the Effect and will be removed.", removeConnectedRulesDialog.connectedRulesCount)
64 | }
65 | }
66 |
67 | TabBar {
68 | id: bar
69 |
70 | anchors.top: parent.top
71 | width: page.width
72 | TabButton {
73 | text: i18n("Effects")
74 | contentItem: tabsLayout
75 | }
76 | TabButton {
77 | text: i18n("Rules")
78 | contentItem: tabsLayout
79 | }
80 | }
81 |
82 | Frame {
83 | anchors.top: bar.bottom
84 | width: parent.width
85 | height: page.height - bar.height - Kirigami.Units.mediumSpacing
86 |
87 | StackLayout {
88 | id: tabsLayout
89 | anchors.fill: parent
90 | currentIndex: bar.currentIndex
91 |
92 | ScrollView {
93 | id: effectsScrollView
94 |
95 | Layout.fillWidth: true
96 | Layout.fillHeight: true
97 |
98 | ColumnLayout {
99 | width: effectsScrollView.availableWidth
100 | RowLayout {
101 | Button {
102 | text: i18n("Add Effect")
103 | onClicked: function () {
104 | const effectName = effectsRepeater.getUniqueEffectName();
105 | effectsListModel.pushModel({
106 | name: effectName,
107 | maskEnabled: false,
108 | maskInverted: false
109 | });
110 | }
111 | }
112 | AddPresetEffect {
113 | Layout.preferredWidth: Kirigami.Units.gridUnit * 10
114 | onActivated: function (index) {
115 | const effectName = effectsRepeater.getUniqueEffectName(textAt(index));
116 | const effectModel = valueAt(index);
117 | effectModel.name = effectName;
118 | effectsListModel.pushModel(effectModel);
119 | }
120 | }
121 | }
122 |
123 | Repeater {
124 | id: effectsRepeater
125 |
126 | model: effectsListModel
127 |
128 | onItemAdded: function (index, item) {
129 | item.effectModel.updateFromJsonObject(model.get(index));
130 | }
131 |
132 | function getUniqueEffectName(effectPrefix) {
133 | const prefix = effectPrefix || "Effect";
134 | let name = prefix;
135 |
136 | outer: for (let i = 1; i < Number.MAX_SAFE_INTEGER; i++) {
137 | for (let index = 0; index < count; index++) {
138 | if (model.get(index).name == name) {
139 | name = prefix + " " + i;
140 | continue outer;
141 | }
142 | }
143 | return name;
144 | }
145 | }
146 |
147 | Rectangle {
148 | Layout.fillWidth: true
149 | implicitHeight: editableEffectRow.implicitHeight + Kirigami.Units.mediumSpacing * 2
150 | border.color: Kirigami.Theme.disabledTextColor
151 | radius: Kirigami.Units.cornerRadius
152 | color: Kirigami.Theme.backgroundColor
153 |
154 | property alias effectModel: editableEffectRow.effectModel
155 |
156 | required property int index
157 | required property var modelData
158 |
159 | EditableEffectRow {
160 | id: editableEffectRow
161 | anchors.left: parent.left
162 | anchors.right: parent.right
163 | anchors.top: parent.top
164 | anchors.margins: Kirigami.Units.mediumSpacing
165 |
166 | onRowRemoved: function () {
167 | let connectedRulesCount = 0;
168 | for (let i = 0; i < rulesListModel.count; i++) {
169 | const rule = rulesListModel.get(i);
170 | if (rule.effectIndex === index) {
171 | connectedRulesCount++;
172 | }
173 | }
174 | if (connectedRulesCount === 0) {
175 | rulesListModel.removeConnectedRulesAndEffect(index);
176 | } else {
177 | removeConnectedRulesDialog.effectIndex = index;
178 | removeConnectedRulesDialog.connectedRulesCount = connectedRulesCount;
179 | removeConnectedRulesDialog.open();
180 | }
181 | }
182 | }
183 |
184 | Connections {
185 | target: effectModel
186 | function onPropertyChanged(propertyName, propertyValue) {
187 | effectsListModel.setProperty(index, propertyName, propertyValue);
188 | effectsListModel.updateConfigFromModel();
189 | }
190 | }
191 | }
192 | }
193 | }
194 | }
195 | ScrollView {
196 | id: rulesScrollView
197 |
198 | Layout.fillWidth: true
199 | Layout.fillHeight: true
200 |
201 | ColumnLayout {
202 | width: rulesScrollView.availableWidth
203 | Button {
204 | text: i18n("Add Effect Rule")
205 | onClicked: function () {
206 | rulesListModel.pushModel({
207 | effectIndex: 0,
208 | conditionType: 0,
209 | arg0: "maximized",
210 | arg1: true
211 | });
212 | }
213 | }
214 |
215 | Repeater {
216 | id: rulesRepeater
217 |
218 | model: rulesListModel
219 |
220 | onItemAdded: function (index, item) {
221 | item.ruleModel.updateFromJsonObject(model.get(index));
222 | }
223 |
224 | Rectangle {
225 | Layout.fillWidth: true
226 | implicitHeight: editableRuleRow.implicitHeight + Kirigami.Units.mediumSpacing * 2
227 | border.color: Kirigami.Theme.disabledTextColor
228 | radius: Kirigami.Units.cornerRadius
229 | color: Kirigami.Theme.backgroundColor
230 |
231 | property alias ruleModel: editableRuleRow.ruleModel
232 |
233 | required property int index
234 | required property var modelData
235 |
236 | EditableRuleRow {
237 | id: editableRuleRow
238 | anchors.left: parent.left
239 | anchors.right: parent.right
240 | anchors.top: parent.top
241 | anchors.margins: Kirigami.Units.mediumSpacing
242 |
243 | effects: effectsListModel
244 |
245 | onRowRemoved: function () {
246 | rulesRepeater.model.deleteModel(index);
247 | }
248 | }
249 |
250 | Connections {
251 | target: ruleModel
252 | function onPropertyChanged(propertyName, propertyValue) {
253 | rulesListModel.setProperty(index, propertyName, propertyValue);
254 | rulesListModel.updateConfigFromModel();
255 | }
256 | }
257 | }
258 | }
259 | }
260 | }
261 | }
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/package/contents/ui/config/effect/EditEffect.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 | import QtQuick
7 | import QtQuick.Controls
8 | import QtQuick.Layouts
9 | import org.kde.kirigami as Kirigami
10 |
11 | ColumnLayout {
12 | id: root
13 |
14 | property EffectModel effectModel: EffectModel {}
15 |
16 | onEffectModelChanged: function () {
17 | brightness.value = effectModel.brightness;
18 | contrast.value = effectModel.contrast;
19 | saturation.value = effectModel.saturation;
20 | colorization.value = effectModel.colorization;
21 | colorizationColor.value = effectModel.colorizationColor;
22 |
23 | blur.value = effectModel.blur;
24 | blurMax.value = effectModel.blurMax;
25 | blurMultiplier.value = effectModel.blurMultiplier;
26 |
27 | shadowOpacity.value = effectModel.shadowOpacity;
28 | shadowBlur.value = effectModel.shadowBlur;
29 | shadowHorizontalOffset.value = effectModel.shadowHorizontalOffset;
30 | shadowVerticalOffset.value = effectModel.shadowVerticalOffset;
31 | shadowScale.value = effectModel.shadowScale;
32 | shadowColor.value = effectModel.shadowColor;
33 |
34 | maskEnabled.value = effectModel.maskEnabled;
35 | maskInverted.value = effectModel.maskInverted;
36 | maskThresholdMin.value = effectModel.maskThresholdMin;
37 | maskThresholdMax.value = effectModel.maskThresholdMax;
38 | maskSpreadAtMin.value = effectModel.maskSpreadAtMin;
39 | maskSpreadAtMax.value = effectModel.maskSpreadAtMax;
40 | }
41 |
42 | spacing: 0
43 |
44 | TabBar {
45 | id: bar
46 | Layout.fillWidth: true
47 | Layout.bottomMargin: 0
48 | TabButton {
49 | text: i18n("Color")
50 | contentItem: tabsLayout
51 | }
52 | TabButton {
53 | text: i18n("Blur")
54 | contentItem: tabsLayout
55 | }
56 | TabButton {
57 | text: i18n("Shadow")
58 | contentItem: tabsLayout
59 | }
60 | TabButton {
61 | text: i18n("Mask")
62 | contentItem: tabsLayout
63 | }
64 | }
65 |
66 | Frame {
67 | Layout.fillWidth: true
68 | Layout.topMargin: 0
69 | implicitHeight: tabsLayout.implicitHeight
70 |
71 | StackLayout {
72 | id: tabsLayout
73 | width: parent.width
74 | currentIndex: bar.currentIndex
75 |
76 | ColumnLayout {
77 | CheckableSlider {
78 | id: brightness
79 | Layout.topMargin: Kirigami.Units.gridUnit
80 | label: i18n("Brightness")
81 | onValueUpdated: function (v) {
82 | root.effectModel.brightness = v;
83 | }
84 | }
85 |
86 | CheckableSlider {
87 | id: contrast
88 | label: i18n("Contrast")
89 | onValueUpdated: function (v) {
90 | root.effectModel.contrast = v;
91 | }
92 | }
93 |
94 | CheckableSlider {
95 | id: saturation
96 | label: i18n("Saturation")
97 | onValueUpdated: function (v) {
98 | root.effectModel.saturation = v;
99 | }
100 | }
101 |
102 | CheckableSlider {
103 | id: colorization
104 | label: i18n("Colorization")
105 | from: 0
106 | stepSize: 0.01
107 | onValueUpdated: function (v) {
108 | root.effectModel.colorization = v;
109 | }
110 | }
111 |
112 | CheckableColorSlider {
113 | id: colorizationColor
114 | Layout.bottomMargin: Kirigami.Units.gridUnit
115 | label: i18n("Colorization Color")
116 | initValue: "red"
117 | onValueUpdated: function (v) {
118 | root.effectModel.colorizationColor = v;
119 | }
120 | }
121 | }
122 | ColumnLayout {
123 | CheckableSlider {
124 | id: blur
125 | Layout.topMargin: Kirigami.Units.gridUnit
126 | label: i18n("Blur")
127 | from: 0
128 | stepSize: 0.01
129 | onValueUpdated: function (v) {
130 | root.effectModel.blur = v;
131 | }
132 | }
133 |
134 | CheckableSlider {
135 | id: blurMax
136 | label: i18n("Blur Max")
137 | from: 2
138 | to: 64
139 | initValue: 32
140 | stepSize: 1
141 | onValueUpdated: function (v) {
142 | root.effectModel.blurMax = v;
143 | }
144 | }
145 |
146 | CheckableSlider {
147 | id: blurMultiplier
148 | Layout.bottomMargin: Kirigami.Units.gridUnit
149 | label: i18n("Blur Multiplier")
150 | from: 0
151 | to: 16
152 | stepSize: 0.1
153 | onValueUpdated: function (v) {
154 | root.effectModel.blurMultiplier = v;
155 | }
156 | }
157 | }
158 | ColumnLayout {
159 | CheckableSlider {
160 | id: shadowOpacity
161 | Layout.topMargin: Kirigami.Units.gridUnit
162 | label: i18n("Shadow")
163 | from: 0
164 | stepSize: 0.01
165 | initValue: 1.0
166 | onValueUpdated: function (v) {
167 | root.effectModel.shadowOpacity = v;
168 | }
169 | }
170 |
171 | CheckableSlider {
172 | id: shadowBlur
173 | label: i18n("Shadow Blur")
174 | from: 0
175 | stepSize: 0.01
176 | initValue: 1.0
177 | onValueUpdated: function (v) {
178 | root.effectModel.shadowBlur = v;
179 | }
180 | }
181 |
182 | CheckableSlider {
183 | id: shadowHorizontalOffset
184 | label: i18n("Shadow Horizontal Offset")
185 | from: -20
186 | to: 20
187 | stepSize: 0.1
188 | onValueUpdated: function (v) {
189 | root.effectModel.shadowHorizontalOffset = v;
190 | }
191 | }
192 |
193 | CheckableSlider {
194 | id: shadowVerticalOffset
195 | label: i18n("Shadow Vertical Offset")
196 | from: -20
197 | to: 20
198 | stepSize: 0.1
199 | onValueUpdated: function (v) {
200 | root.effectModel.shadowVerticalOffset = v;
201 | }
202 | }
203 |
204 | CheckableSlider {
205 | id: shadowScale
206 | label: i18n("Shadow Scale")
207 | from: 0.7
208 | to: 1.3
209 | stepSize: 0.01
210 | initValue: 1.0
211 | onValueUpdated: function (v) {
212 | root.effectModel.shadowScale = v;
213 | }
214 | }
215 |
216 | CheckableColorSlider {
217 | id: shadowColor
218 | Layout.bottomMargin: Kirigami.Units.gridUnit
219 | label: i18n("Shadow Color")
220 | onValueUpdated: function (v) {
221 | root.effectModel.shadowColor = v;
222 | }
223 | }
224 | }
225 | ColumnLayout {
226 | Checkable {
227 | id: maskEnabled
228 | Layout.topMargin: Kirigami.Units.gridUnit
229 | label: i18n("Mask Enabled")
230 | onValueUpdated: function (v) {
231 | root.effectModel.maskEnabled = v;
232 | }
233 | }
234 |
235 | Checkable {
236 | id: maskInverted
237 | label: i18n("Mask Inverted")
238 | onValueUpdated: function (v) {
239 | root.effectModel.maskInverted = v;
240 | }
241 | }
242 |
243 | CheckableSlider {
244 | id: maskThresholdMin
245 | label: i18n("Mask Threshold Min")
246 | from: 0
247 | stepSize: 0.01
248 | onValueUpdated: function (v) {
249 | root.effectModel.maskThresholdMin = v;
250 | }
251 | }
252 |
253 | CheckableSlider {
254 | id: maskThresholdMax
255 | label: i18n("Mask Threshold Max")
256 | from: 0
257 | stepSize: 0.01
258 | initValue: 1.0
259 | onValueUpdated: function (v) {
260 | root.effectModel.maskThresholdMax = v;
261 | }
262 | }
263 |
264 | CheckableSlider {
265 | id: maskSpreadAtMin
266 | label: i18n("Mask Spread At Min")
267 | from: 0
268 | stepSize: 0.01
269 | onValueUpdated: function (v) {
270 | root.effectModel.maskSpreadAtMin = v;
271 | }
272 | }
273 |
274 | CheckableSlider {
275 | id: maskSpreadAtMax
276 | Layout.bottomMargin: Kirigami.Units.gridUnit
277 | label: i18n("Mask Spread At Max")
278 | from: 0
279 | stepSize: 0.01
280 | onValueUpdated: function (v) {
281 | root.effectModel.maskSpreadAtMax = v;
282 | }
283 | }
284 | }
285 | }
286 | }
287 | }
288 |
--------------------------------------------------------------------------------
/package/contents/ui/config/effect/EditableEffectRow.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 |
7 | import "../../"
8 | import "../../config"
9 | import "../../config/effect"
10 | import QtQuick
11 | import QtQuick.Controls
12 | import QtQuick.Layouts
13 | import org.kde.kirigami as Kirigami
14 |
15 | ColumnLayout {
16 | id: root
17 |
18 | Layout.fillWidth: true
19 |
20 | property EffectModel effectModel: EffectModel {}
21 | signal rowRemoved
22 |
23 | RowLayout {
24 | Layout.fillWidth: true
25 |
26 | Item {
27 | Layout.fillWidth: true
28 | height: Kirigami.Units.iconSizes.medium
29 |
30 | TextField {
31 | id: effectTitle
32 | anchors.fill: parent
33 | visible: editEffectButton.checked
34 | text: effectModel.name
35 |
36 | onTextEdited: function () {
37 | effectModel.name = text;
38 | }
39 | }
40 | Text {
41 | id: editEffectTitle
42 | anchors.fill: parent
43 | anchors.leftMargin: Kirigami.Units.smallSpacing
44 | visible: !editEffectButton.checked
45 | text: effectTitle.text
46 | verticalAlignment: Text.AlignVCenter
47 | wrapMode: Text.WrapAnywhere
48 | elide: Text.ElideRight
49 | maximumLineCount: 1
50 | }
51 | }
52 |
53 | Rectangle {
54 | id: effectPreview
55 |
56 | width: Kirigami.Units.gridUnit * 15
57 |
58 | gradient: Gradient {
59 | GradientStop {
60 | position: 0.0
61 | color: Kirigami.Theme.backgroundColor
62 | }
63 | GradientStop {
64 | position: 1.0
65 | color: Kirigami.Theme.alternateBackgroundColor
66 | }
67 | }
68 | border.color: Kirigami.Theme.disabledTextColor
69 | height: Kirigami.Units.iconSizes.medium
70 | radius: Kirigami.Units.cornerRadius
71 |
72 | RowLayout {
73 | id: effectPreviewContent
74 | anchors.fill: parent
75 |
76 | Repeater {
77 | model: ["window-close", "window-maximize", "window-minimize"]
78 | Kirigami.Icon {
79 | height: Kirigami.Units.iconSizes.medium
80 | width: height
81 | source: modelData
82 | }
83 | }
84 | Text {
85 | text: i18n("Preview")
86 | color: Kirigami.Theme.textColor
87 | }
88 | Text {
89 | text: i18n("Preview")
90 | color: Kirigami.Theme.disabledTextColor
91 | }
92 | }
93 |
94 | MergedMultiEffect {
95 | source: effectPreviewContent
96 | anchors.fill: parent
97 | effectModel: root.effectModel
98 | }
99 | }
100 |
101 | Button {
102 | id: editEffectButton
103 | checkable: true
104 | icon.name: "editor"
105 | }
106 |
107 | Button {
108 | id: deleteEffectButton
109 | icon.name: "delete"
110 | onClicked: function () {
111 | root.rowRemoved();
112 | }
113 | }
114 | }
115 |
116 | Loader {
117 | Layout.fillWidth: true
118 | Layout.preferredHeight: item ? item.Layout.preferredHeight : 0
119 | active: editEffectButton.checked
120 | visible: active
121 |
122 | sourceComponent: Component {
123 | EditEffect {
124 | Layout.fillWidth: true
125 | effectModel: root.effectModel
126 | }
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/package/contents/ui/config/effect/EditableRuleRow.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 |
7 | import "../../"
8 | import "../../config"
9 | import "../../config/effect"
10 | import "../../common"
11 | import QtQuick
12 | import QtQuick.Controls
13 | import QtQuick.Layouts
14 | import org.kde.kirigami as Kirigami
15 |
16 | RowLayout {
17 | id: root
18 |
19 | readonly property list argumentFragments: ["ActiveWindowFlagCondition", "ActiveWindowStringCondition", "ActiveWindowRegExpCondition"]
20 |
21 | property RuleModel ruleModel: RuleModel {}
22 | required property JsonListModel effects
23 | signal rowRemoved
24 |
25 | ComboBox {
26 | Kirigami.FormData.label: i18n("Effect:")
27 | model: effects
28 | currentIndex: ruleModel.effectIndex
29 | textRole: "name"
30 | wheelEnabled: false
31 | onActivated: function (index) {
32 | ruleModel.effectIndex = index;
33 | }
34 | }
35 |
36 | ComboBox {
37 | Kirigami.FormData.label: i18n("Type:")
38 | Layout.preferredWidth: Kirigami.Units.gridUnit * 4
39 | wheelEnabled: false
40 | model: [i18n("Flag"), i18n("Value"), i18n("Regex")]
41 | currentIndex: ruleModel.conditionType
42 | onActivated: function (index) {
43 | ruleModel.conditionType = index;
44 | }
45 | }
46 |
47 | Loader {
48 | id: argumentFragmentLoader
49 | Layout.fillWidth: true
50 | source: argumentFragments[ruleModel.conditionType] + ".qml"
51 |
52 | Binding {
53 | when: argumentFragmentLoaderConnections.enabled
54 | target: argumentFragmentLoader.item
55 | property: "arg0"
56 | value: ruleModel.arg0
57 | delayed: true
58 | }
59 |
60 | Binding {
61 | when: argumentFragmentLoaderConnections.enabled
62 | target: argumentFragmentLoader.item
63 | property: "arg1"
64 | value: ruleModel.arg1
65 | delayed: true
66 | }
67 |
68 | Connections {
69 | id: argumentFragmentLoaderConnections
70 |
71 | enabled: argumentFragmentLoader.status === Loader.Ready
72 | target: argumentFragmentLoader.item
73 | function onArg0Updated(val) {
74 | ruleModel.arg0 = val;
75 | }
76 | function onArg1Updated(val) {
77 | ruleModel.arg1 = val;
78 | }
79 | }
80 | }
81 |
82 | Button {
83 | icon.name: "delete"
84 | onClicked: function () {
85 | root.rowRemoved();
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/package/contents/ui/config/effect/EffectModel.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 | import "../../common"
7 | import QtQuick
8 |
9 | JsonModel {
10 | readonly property list propertyNames: ["name", "brightness", "contrast", "saturation", "colorization", "colorizationColor", "blur", "blurMax", "blurMultiplier", "shadowOpacity", "shadowBlur", "shadowHorizontalOffset", "shadowVerticalOffset", "shadowScale", "shadowColor", "maskEnabled", "maskInverted", "maskThresholdMin", "maskThresholdMax", "maskSpreadAtMin", "maskSpreadAtMax"]
11 | _propertyNames: propertyNames
12 |
13 | property string name: "Effect"
14 |
15 | property var brightness: undefined
16 | property var contrast: undefined
17 | property var saturation: undefined
18 | property var colorization: undefined
19 | property var colorizationColor: undefined
20 |
21 | property var blur: undefined
22 | property var blurMax: undefined
23 | property var blurMultiplier: undefined
24 |
25 | property var shadowOpacity: undefined
26 | property var shadowBlur: undefined
27 | property var shadowHorizontalOffset: undefined
28 | property var shadowVerticalOffset: undefined
29 | property var shadowScale: undefined
30 | property var shadowColor: undefined
31 |
32 | property bool maskEnabled: false
33 | property bool maskInverted: false
34 | property var maskThresholdMin: undefined
35 | property var maskThresholdMax: undefined
36 | property var maskSpreadAtMin: undefined
37 | property var maskSpreadAtMax: undefined
38 | }
39 |
--------------------------------------------------------------------------------
/package/contents/ui/config/effect/MergedMultiEffect.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 | import QtQuick.Effects
7 |
8 | MultiEffect {
9 | readonly property MultiEffect defaultEffectModel: MultiEffect {
10 | visible: false
11 | enabled: false
12 | }
13 | property EffectModel effectModel: EffectModel {}
14 |
15 | brightness: effectModel.brightness !== undefined ? effectModel.brightness : defaultEffectModel.brightness
16 | contrast: effectModel.contrast !== undefined ? effectModel.contrast : defaultEffectModel.contrast
17 | saturation: effectModel.saturation !== undefined ? effectModel.saturation : defaultEffectModel.saturation
18 | colorization: effectModel.colorization !== undefined ? effectModel.colorization : defaultEffectModel.colorization
19 | colorizationColor: effectModel.colorizationColor !== undefined ? effectModel.colorizationColor : defaultEffectModel.colorizationColor
20 |
21 | blurEnabled: effectModel.blur !== undefined
22 | blur: effectModel.blur !== undefined ? effectModel.blur : defaultEffectModel.blur
23 | blurMax: effectModel.blurMax !== undefined ? effectModel.blurMax : defaultEffectModel.blurMax
24 | blurMultiplier: effectModel.blurMultiplier !== undefined ? effectModel.blurMultiplier : defaultEffectModel.blurMultiplier
25 |
26 | shadowEnabled: effectModel.shadowOpacity !== undefined
27 | shadowOpacity: effectModel.shadowOpacity !== undefined ? effectModel.shadowOpacity : defaultEffectModel.shadowOpacity
28 | shadowBlur: effectModel.shadowBlur !== undefined ? effectModel.shadowBlur : defaultEffectModel.shadowBlur
29 | shadowHorizontalOffset: effectModel.shadowHorizontalOffset !== undefined ? effectModel.shadowHorizontalOffset : defaultEffectModel.shadowHorizontalOffset
30 | shadowVerticalOffset: effectModel.shadowVerticalOffset !== undefined ? effectModel.shadowVerticalOffset : defaultEffectModel.shadowVerticalOffset
31 | shadowScale: effectModel.shadowScale !== undefined ? effectModel.shadowScale : defaultEffectModel.shadowScale
32 | shadowColor: effectModel.shadowColor !== undefined ? effectModel.shadowColor : defaultEffectModel.shadowColor
33 |
34 | maskEnabled: effectModel.maskEnabled
35 | maskInverted: effectModel.maskInverted
36 | maskThresholdMin: effectModel.maskThresholdMin !== undefined ? effectModel.maskThresholdMin : defaultEffectModel.maskThresholdMin
37 | maskThresholdMax: effectModel.maskThresholdMax !== undefined ? effectModel.maskThresholdMax : defaultEffectModel.maskThresholdMax
38 | maskSpreadAtMin: effectModel.maskSpreadAtMin !== undefined ? effectModel.maskSpreadAtMin : defaultEffectModel.maskSpreadAtMin
39 | maskSpreadAtMax: effectModel.maskSpreadAtMax !== undefined ? effectModel.maskSpreadAtMax : defaultEffectModel.maskSpreadAtMax
40 | }
41 |
--------------------------------------------------------------------------------
/package/contents/ui/config/effect/RuleModel.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 | import "../../common"
7 | import QtQuick
8 |
9 | JsonModel {
10 | readonly property list propertyNames: ["effectIndex", "conditionType", "arg0", "arg1"]
11 | _propertyNames: propertyNames
12 |
13 | property int effectIndex: 0
14 | property int conditionType: 0
15 | property var arg0: undefined
16 | property var arg1: undefined
17 | }
18 |
--------------------------------------------------------------------------------
/package/contents/ui/config/effect/effect.js:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 |
7 | const CONDITION_ACTIVE_WINDOW_FLAG = 0;
8 | const CONDITION_ACTIVE_WINDOW_STRING = 1;
9 | const CONDITION_ACTIVE_WINDOW_REGEX = 2;
10 |
11 | class Condition {
12 | constructor(effectIndex) {
13 | this.effectIndex = effectIndex;
14 | }
15 |
16 | checkCondition(_widget) {
17 | throw new Error("Not implemented");
18 | }
19 |
20 | static fromModel(model) {
21 | switch (model.conditionType) {
22 | case CONDITION_ACTIVE_WINDOW_FLAG:
23 | return new ActiveWindowFlagCondition(model.effectIndex, model.arg0, !!model.arg1);
24 | case CONDITION_ACTIVE_WINDOW_STRING:
25 | return new ActiveWindowStringCondition(model.effectIndex, model.arg0, model.arg1);
26 | case CONDITION_ACTIVE_WINDOW_REGEX:
27 | return new ActiveWindowRegexpCondition(model.effectIndex, model.arg0, model.arg1);
28 | default:
29 | throw new Error("Unsupported condition type: " + model.conditionType);
30 | }
31 | }
32 | }
33 |
34 | class ActiveWindowCondition extends Condition {
35 | constructor(effectIndex) {
36 | super(effectIndex);
37 | }
38 |
39 | checkCondition(widget) {
40 | return widget.tasksModel.hasActiveWindow && this.checkActiveWindowCondition(widget.tasksModel.activeWindow);
41 | }
42 |
43 | checkActiveWindowCondition(_activeWindow) {
44 | throw new Error("Not implemented");
45 | }
46 | }
47 |
48 | class ActiveWindowFlagCondition extends ActiveWindowCondition {
49 | constructor(effectIndex, propertyName, propertyValue) {
50 | super(effectIndex);
51 | this.propertyName = propertyName;
52 | this.propertyValue = propertyValue;
53 | }
54 |
55 | checkActiveWindowCondition(activeWindow) {
56 | return activeWindow[this.propertyName] === this.propertyValue;
57 | }
58 | }
59 |
60 | class ActiveWindowStringCondition extends ActiveWindowCondition {
61 | constructor(effectIndex, propertyName, propertyValue) {
62 | super(effectIndex);
63 | this.propertyName = propertyName;
64 | this.propertyValue = propertyValue;
65 | }
66 |
67 | checkActiveWindowCondition(activeWindow) {
68 | return activeWindow[this.propertyName] === this.propertyValue;
69 | }
70 | }
71 |
72 | class ActiveWindowRegexpCondition extends ActiveWindowCondition {
73 | constructor(effectIndex, propertyName, regExp) {
74 | super(effectIndex);
75 | this.propertyName = propertyName;
76 | this.regExp = new RegExp(regExp, "g");
77 | }
78 |
79 | checkActiveWindowCondition(activeWindow) {
80 | const value = activeWindow[this.propertyName];
81 | return (value === undefined || value === null) ? false : value.match(this.regExp);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/package/contents/ui/theme/AuroraeWindowControlButton.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 | import QtCore
7 | import QtQuick
8 | import QtQuick.Controls
9 | import QtQuick.Effects
10 | import org.kde.kirigami as Kirigami
11 | import org.kde.ksvg 1.0 as KSvg
12 | import ".."
13 | import "../windowControlButton.js" as WCB
14 |
15 | Item {
16 | id: auroraeControlButton
17 |
18 | anchors.fill: parent
19 |
20 | readonly property var iconStatesPrefixes: ["active", "hover", "pressed", "pressed", "pressed", "deactivated", "deactivated", "inactive", "hover-inactive", "pressed-inactive", "pressed-inactive", "pressed-inactive", "deactivated-inactive", "deactivated-inactive"]
21 | readonly property string themesPath: "aurorae/themes/"
22 | readonly property string svgExt: "svg"
23 | readonly property string svgzExt: "svgz"
24 |
25 | property string themeName
26 | property int buttonType: WindowControlButton.Type.CloseButton
27 | property int iconState: calculateAuroraeIconState(this)
28 | property bool hovered: false
29 | property bool active: false
30 | property bool pressed: false
31 | property bool checked: false
32 | property bool disabled: false
33 | property int animationDuration: 0
34 | property var iconPath: buttonIconPath(buttonType)
35 |
36 | property bool hasActiveHover: iconPath === undefined || iconInfo.hasElementPrefix("hover")
37 | property bool hasActivePressed: iconPath === undefined || iconInfo.hasElementPrefix("pressed")
38 | property bool hasActiveChecked: iconPath === undefined || iconInfo.hasElementPrefix("toggled")
39 | property bool hasActiveDisabled: iconPath === undefined || iconInfo.hasElementPrefix("deactivated")
40 | property bool hasInactive: iconPath === undefined || iconInfo.hasElementPrefix("inactive")
41 | property bool hasInactiveHover: iconPath === undefined || iconInfo.hasElementPrefix("hover-inactive")
42 | property bool hasInactivePressed: iconPath === undefined || iconInfo.hasElementPrefix("pressed-inactive")
43 | property bool hasInactiveChecked: iconPath === undefined || iconInfo.hasElementPrefix("toggled-inactive")
44 | property bool hasInactiveDisabled: iconPath === undefined || iconInfo.hasElementPrefix("deactivated-inactive")
45 |
46 | Behavior on opacity {
47 | NumberAnimation {
48 | duration: animationDuration
49 | }
50 | }
51 |
52 | KSvg.FrameSvg {
53 | id: iconInfo
54 |
55 | imagePath: iconPath || ""
56 | }
57 |
58 | Loader {
59 | id: auroraeIconSvg
60 | active: !!iconPath
61 | anchors.fill: parent
62 |
63 | sourceComponent: KSvg.FrameSvgItem {
64 | anchors.fill: parent
65 | prefix: iconStatesPrefixes[iconState]
66 | imagePath: iconPath
67 | enabledBorders: KSvg.FrameSvgItem.NoBorder
68 | }
69 | }
70 | Loader {
71 | anchors.fill: parent
72 | active: !auroraeIconSvg.active
73 | sourceComponent: PlasmaWindowControlButtonIcon {
74 | anchors.fill: parent
75 | buttonType: auroraeControlButton.buttonType
76 | hovered: auroraeControlButton.hovered
77 | checked: auroraeControlButton.checked
78 | pressed: auroraeControlButton.pressed
79 | active: auroraeControlButton.active
80 | enabled: !auroraeControlButton.disabled
81 | }
82 | }
83 |
84 | function buttonIconPath(type) {
85 | let buttonName = mapButtonToName(type);
86 | let relativeIconPath = themesPath + themeName + "/" + buttonName;
87 | let path = locateAtDataLocations(relativeIconPath + "." + svgExt);
88 | if (!path)
89 | path = locateAtDataLocations(relativeIconPath + "." + svgzExt);
90 | return path;
91 | }
92 |
93 | function locateAtDataLocations(file) {
94 | let path = StandardPaths.locate(StandardPaths.GenericDataLocation, file, StandardPaths.LocateFile);
95 | return path && path.toString() !== "" ? path : undefined;
96 | }
97 |
98 | // https://invent.kde.org/plasma/kwin/-/blob/master/src/plugins/kdecorations/aurorae/src/qml/AuroraeButton.qml
99 | function calculateAuroraeIconState(button) {
100 | if (!button.active) {
101 | if (button.pressed && button.hasInactivePressed)
102 | return WindowControlButton.IconState.InactivePressed;
103 | else if (button.checked && button.hovered && button.hasInactiveChecked && button.hasInactiveHover)
104 | return WindowControlButton.IconState.InactiveHoverChecked;
105 | else if (button.checked && button.hasInactiveChecked)
106 | return WindowControlButton.IconState.InactiveChecked;
107 | else if (button.hovered && button.hasInactiveHover)
108 | return WindowControlButton.IconState.InactiveHover;
109 | else if (!button.enabled && button.hasInactiveDisabled)
110 | return WindowControlButton.IconState.InactiveDisabled;
111 | else if (button.hasInactive && !button.pressed && !button.checked && !button.hovered && button.enabled)
112 | return WindowControlButton.IconState.Inactive;
113 | }
114 | if (button.pressed && button.hasActivePressed && (button.active || !button.hasInactivePressed))
115 | return WindowControlButton.IconState.ActivePressed;
116 | else if (button.checked && button.hover && button.hasActiveChecked && button.hasActiveHover && (button.active || !button.hasInactiveChecked || !button.hasInactiveHover))
117 | return WindowControlButton.IconState.ActiveHoverChecked;
118 | else if (button.checked && button.hasActiveChecked && (button.active || !button.hasInactiveChecked))
119 | return WindowControlButton.IconState.ActiveChecked;
120 | else if (button.hovered && button.hasActiveHover && (button.active || !button.hasInactiveHover))
121 | return WindowControlButton.IconState.ActiveHover;
122 | else if (!button.enabled && button.hasActiveDisabled && (button.active || !button.hasInactiveDisabled))
123 | return WindowControlButton.IconState.ActiveDisabled;
124 | else
125 | return WindowControlButton.IconState.Active;
126 | }
127 |
128 | function mapButtonToName(type) {
129 | switch (type) {
130 | case WindowControlButton.Type.MinimizeButton:
131 | return "minimize";
132 | case WindowControlButton.Type.MaximizeButton:
133 | return "maximize";
134 | case WindowControlButton.Type.RestoreButton:
135 | return "restore";
136 | case WindowControlButton.Type.CloseButton:
137 | return "close";
138 | case WindowControlButton.Type.AllDesktopsButton:
139 | return "alldesktops";
140 | case WindowControlButton.Type.KeepAboveButton:
141 | return "keepabove";
142 | case WindowControlButton.Type.KeepBelowButton:
143 | return "keepbelow";
144 | case WindowControlButton.Type.ShadeButton:
145 | return "shade";
146 | case WindowControlButton.Type.HelpButton:
147 | return "help";
148 | case WindowControlButton.Type.MenuButton:
149 | return "menu";
150 | case WindowControlButton.Type.AppMenuButton:
151 | return "appmenu";
152 | default:
153 | return "";
154 | }
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/package/contents/ui/theme/BreezeWindowControlButton.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 |
7 | import QtQuick
8 | import org.kde.kirigami as Kirigami
9 |
10 | BreezeWindowControlButtonIcon {
11 | property bool disabled: false
12 | property int animationDuration: 0
13 |
14 | titleBarColor: disabled ? Kirigami.Theme.alternateBackgroundColor : Kirigami.Theme.backgroundColor
15 | fontColor: disabled ? Kirigami.Theme.disabledTextColor : Kirigami.Theme.textColor
16 | foregroundWarningColor: disabled ? Kirigami.Theme.alternateBackgroundColor : Kirigami.Theme.negativeTextColor
17 | anchors.fill: parent
18 | anchors.margins: 3
19 | opacity: 0
20 |
21 | Behavior on opacity {
22 | NumberAnimation {
23 | duration: animationDuration
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/package/contents/ui/theme/BreezeWindowControlButtonIcon.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 | import QtQuick
7 | import "../"
8 |
9 | Canvas {
10 | property color titleBarColor: "gray"
11 | property color fontColor: "white"
12 | property color foregroundWarningColor: "red"
13 | property bool outlineCloseButton: false
14 | property color backgroundColor: calculateBackgroundColor()
15 | property color foregroundColor: calculateForegroundColor()
16 | property int buttonType: WindowControlButton.Type.CloseButton
17 | property bool hovered: false
18 | property bool active: false
19 | property bool pressed: false
20 | property bool checked: false
21 |
22 | function drawIcon(ctx) {
23 | ctx.reset();
24 | ctx.scale(width / 20, height / 20);
25 | ctx.translate(1, 1);
26 | drawBackground(ctx);
27 | drawForeground(ctx);
28 | }
29 |
30 | function drawBackground(ctx) {
31 | ctx.fillStyle = backgroundColor;
32 | ctx.ellipse(0, 0, 18, 18);
33 | ctx.fill();
34 | }
35 |
36 | function drawForeground(ctx) {
37 | ctx.beginPath();
38 | ctx.strokeStyle = foregroundColor;
39 | ctx.fillStyle = foregroundColor;
40 | ctx.lineJoin = "miter";
41 | ctx.lineCap = "round";
42 | ctx.lineWidth = Math.max(1, 20 / width);
43 | switch (buttonType) {
44 | case WindowControlButton.Type.MinimizeButton:
45 | ctx.moveTo(4, 7);
46 | ctx.lineTo(9, 12);
47 | ctx.lineTo(14, 7);
48 | ctx.stroke();
49 | break;
50 | case WindowControlButton.Type.RestoreButton:
51 | case WindowControlButton.Type.MaximizeButton:
52 | if (checked) {
53 | ctx.lineJoin = "round";
54 | ctx.moveTo(4, 9);
55 | ctx.lineTo(9, 4);
56 | ctx.lineTo(14, 9);
57 | ctx.lineTo(9, 14);
58 | ctx.closePath();
59 | ctx.stroke();
60 | } else {
61 | ctx.moveTo(4, 11);
62 | ctx.lineTo(9, 6);
63 | ctx.lineTo(14, 11);
64 | ctx.stroke();
65 | }
66 | break;
67 | case WindowControlButton.Type.CloseButton:
68 | ctx.moveTo(5, 5);
69 | ctx.lineTo(13, 13);
70 | ctx.moveTo(13, 5);
71 | ctx.lineTo(5, 13);
72 | ctx.stroke();
73 | break;
74 | case WindowControlButton.Type.AllDesktopsButton:
75 | if (checked) {
76 | ctx.ellipse(3, 3, 12, 12);
77 | ctx.fill();
78 | ctx.beginPath();
79 | ctx.fillStyle = backgroundColor;
80 | ctx.ellipse(8, 8, 2, 2);
81 | ctx.fill();
82 | } else {
83 | ctx.moveTo(6.5, 8.5);
84 | ctx.lineTo(12, 3);
85 | ctx.lineTo(15, 6);
86 | ctx.lineTo(9.5, 11.5);
87 | ctx.closePath();
88 | ctx.fill();
89 | ctx.beginPath();
90 | ctx.moveTo(5.5, 7.5);
91 | ctx.lineTo(10.5, 12.5);
92 | ctx.moveTo(12, 6);
93 | ctx.lineTo(4.5, 13.5);
94 | ctx.stroke();
95 | }
96 | break;
97 | case WindowControlButton.Type.KeepAboveButton:
98 | ctx.moveTo(4, 9);
99 | ctx.lineTo(9, 4);
100 | ctx.lineTo(14, 9);
101 | ctx.moveTo(4, 13);
102 | ctx.lineTo(9, 8);
103 | ctx.lineTo(14, 13);
104 | ctx.stroke();
105 | break;
106 | case WindowControlButton.Type.KeepBelowButton:
107 | ctx.moveTo(4, 5);
108 | ctx.lineTo(9, 10);
109 | ctx.lineTo(14, 5);
110 | ctx.moveTo(4, 9);
111 | ctx.lineTo(9, 14);
112 | ctx.lineTo(14, 9);
113 | ctx.stroke();
114 | break;
115 | case WindowControlButton.Type.ShadeButton:
116 | ctx.moveTo(4, 5.5);
117 | ctx.lineTo(14, 5.5);
118 | if (checked) {
119 | ctx.moveTo(4, 8);
120 | ctx.lineTo(9, 13);
121 | ctx.lineTo(14, 8);
122 | ctx.stroke();
123 | } else {
124 | ctx.moveTo(4, 13);
125 | ctx.lineTo(9, 8);
126 | ctx.lineTo(14, 13);
127 | ctx.stroke();
128 | }
129 | break;
130 | case WindowControlButton.Type.HelpButton:
131 | ctx.moveTo(6, 6);
132 | ctx.bezierCurveTo(7, 1.5, 16, 4, 12, 8.5);
133 | ctx.bezierCurveTo(12, 8.5, 9, 8.5, 9, 11.5);
134 | ctx.rect(9, 15, 0.5, 0.5);
135 | ctx.stroke();
136 | break;
137 | case WindowControlButton.Type.MenuButton:
138 | case WindowControlButton.Type.AppMenuButton:
139 | ctx.rect(3.5, 4.5, 11, 1);
140 | ctx.rect(3.5, 8.5, 11, 1);
141 | ctx.rect(3.5, 12.5, 11, 1);
142 | ctx.stroke();
143 | break;
144 | }
145 | }
146 |
147 | function mix(a, b, bias) {
148 | return a + (b - a) * bias;
149 | }
150 |
151 | function bound(min, val, max) {
152 | return Math.max(min, Math.min(val, max));
153 | }
154 |
155 | function mixColors(c1, c2, bias) {
156 | if (bias <= 0)
157 | return c1;
158 | if (bias >= 1)
159 | return c2;
160 | let a = mix(c1.a, c2.a, bias);
161 | if (a <= 0)
162 | return Qt.rgba(0, 0, 0, 0);
163 | let r = bound(0, mix(c1.r * c1.a, c2.r * c2.a, bias), 1) / a;
164 | let g = bound(0, mix(c1.g * c1.a, c2.g * c2.a, bias), 1) / a;
165 | let b = bound(0, mix(c1.b * c1.a, c2.b * c2.a, bias), 1) / a;
166 | return Qt.rgba(r, g, b, a);
167 | }
168 |
169 | function calculateBackgroundColor() {
170 | if (pressed) {
171 | if (buttonType == WindowControlButton.Type.CloseButton)
172 | return foregroundWarningColor.darker();
173 | else
174 | return mixColors(titleBarColor, fontColor, 0.3);
175 | } else if ((buttonType == WindowControlButton.Type.KeepAboveButton || buttonType == WindowControlButton.Type.KeepBelowButton || buttonType == WindowControlButton.Type.ShadeButton) && checked) {
176 | return fontColor;
177 | } else if (hovered) {
178 | if (buttonType == WindowControlButton.Type.CloseButton)
179 | return active ? foregroundWarningColor.lighter() : foregroundWarningColor;
180 | else
181 | return fontColor;
182 | } else if (buttonType == WindowControlButton.Type.CloseButton && outlineCloseButton) {
183 | return active ? foregroundWarningColor : fontColor;
184 | }
185 | return Qt.rgba(0, 0, 0, 0);
186 | }
187 |
188 | function calculateForegroundColor() {
189 | if (pressed)
190 | return titleBarColor;
191 | else if (buttonType == WindowControlButton.Type.CloseButton && outlineCloseButton)
192 | return titleBarColor;
193 | else if ((buttonType == WindowControlButton.Type.KeepAboveButton || buttonType == WindowControlButton.Type.KeepBelowButton || buttonType == WindowControlButton.Type.ShadeButton) && checked)
194 | return titleBarColor;
195 | else if (hovered)
196 | return titleBarColor;
197 | return fontColor;
198 | }
199 |
200 | onHoveredChanged: Qt.callLater(requestPaint)
201 | onActiveChanged: Qt.callLater(requestPaint)
202 | onPressedChanged: Qt.callLater(requestPaint)
203 | onCheckedChanged: Qt.callLater(requestPaint)
204 | onWidthChanged: Qt.callLater(requestPaint)
205 | onHeightChanged: Qt.callLater(requestPaint)
206 | onTitleBarColorChanged: Qt.callLater(requestPaint)
207 | onFontColorChanged: Qt.callLater(requestPaint)
208 | onForegroundWarningColorChanged: Qt.callLater(requestPaint)
209 | onButtonTypeChanged: Qt.callLater(requestPaint)
210 | anchors.fill: parent
211 | antialiasing: true
212 | onPaint: function () {
213 | var ctx = getContext("2d");
214 | drawIcon(ctx);
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/package/contents/ui/theme/OxygenWindowControlButton.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 |
7 | import QtQuick
8 |
9 | OxygenWindowControlButtonIcon {
10 | property int animationDuration: 0
11 |
12 | anchors.fill: parent
13 | opacity: 0
14 |
15 | Behavior on opacity {
16 | NumberAnimation {
17 | duration: animationDuration
18 | easing.type: Easing.OutQuint
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/package/contents/ui/theme/OxygenWindowControlButtonIcon.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 | import QtQuick
7 | import org.kde.kirigami as Kirigami
8 | import "../"
9 |
10 | Canvas {
11 | antialiasing: true
12 |
13 | property bool hovered: false
14 | property bool active: false
15 | property bool pressed: false
16 | property bool checked: false
17 | property bool disabled: false
18 | property int buttonType: 0
19 |
20 | property color inactiveForegroundColor: palette.inactive.buttonText
21 | property var colors: disabled ? palette.disabled : active ? palette.active : palette.inactive
22 | property color backgroundColor: colors.button
23 | property color foregroundColor: colors.buttonText
24 | property color lightBackgroundColor: colors.light
25 | property color midDarkBackgroundColor: colors.mid
26 | property color midLightBackgroundColor: colors.midlight
27 | property color shadowColor: Qt.alpha(colors.shadow, 0.5)
28 | property color negativeForegroundColor: Kirigami.Theme.negativeTextColor
29 | property color hoverForegroundColor: Kirigami.Theme.hoverColor
30 | property color checkedForegroundColor: Kirigami.Theme.focusColor
31 | property bool toggleButton: isToggleButton()
32 |
33 | onPaint: function () {
34 | let ctx = getContext("2d");
35 | let color = foregroundColor;
36 | ctx.save();
37 | ctx.reset();
38 | ctx.translate(0, 0);
39 | ctx.lineWidth = 1.2;
40 | ctx.lineCap = "round";
41 | ctx.lineJoin = "round";
42 | if (hovered || (toggleButton && checked)) {
43 | let glowColor = calcGlowColor();
44 | if (hovered !== (toggleButton && checked)) {
45 | color = glowColor;
46 | } else if (toggleButton && checked) {
47 | color = Kirigami.ColorUtils.linearInterpolation(inactiveForegroundColor, color, 0.7);
48 | }
49 | drawButtonGlow(ctx, glowColor);
50 | }
51 | drawButton(ctx, color, pressed || checked);
52 | ctx.strokeStyle = midLightBackgroundColor;
53 | ctx.translate(0, 1.5);
54 | drawIcon(ctx);
55 | ctx.translate(0, -1.5);
56 | ctx.strokeStyle = color;
57 | drawIcon(ctx);
58 | ctx.restore();
59 | }
60 |
61 | function drawIcon(ctx) {
62 | ctx.save();
63 | ctx.beginPath();
64 | scaleCtx(ctx, 21);
65 | ctx.lineWidth = Math.max(ctx.lineWidth, 1.1 / width / 21);
66 | ctx.fillStyle = ctx.strokeStyle;
67 | switch (buttonType) {
68 | case WindowControlButton.Type.MinimizeButton:
69 | ctx.lineTo(7.5, 9.5);
70 | ctx.lineTo(10.5, 12.5);
71 | ctx.lineTo(13.5, 9.5);
72 | ctx.stroke();
73 | break;
74 | case WindowControlButton.Type.MaximizeButton:
75 | case WindowControlButton.Type.RestoreButton:
76 | if (checked) {
77 | ctx.lineTo(7.5, 10.5);
78 | ctx.lineTo(10.5, 7.5);
79 | ctx.lineTo(13.5, 10.5);
80 | ctx.lineTo(10.5, 13.5);
81 | ctx.closePath();
82 | ctx.stroke();
83 | } else {
84 | ctx.lineTo(7.5, 11.5);
85 | ctx.lineTo(10.5, 8.5);
86 | ctx.lineTo(13.5, 11.5);
87 | ctx.stroke();
88 | }
89 | break;
90 | case WindowControlButton.Type.CloseButton:
91 | ctx.moveTo(7.5, 7.5);
92 | ctx.lineTo(13.5, 13.5);
93 | ctx.moveTo(13.5, 7.5);
94 | ctx.lineTo(7.5, 13.5);
95 | ctx.stroke();
96 | break;
97 | case WindowControlButton.Type.AllDesktopsButton:
98 | drawPoint(ctx, 10.5, 10.5);
99 | break;
100 | case WindowControlButton.Type.KeepAboveButton:
101 | ctx.moveTo(7.5, 14);
102 | ctx.lineTo(10.5, 11);
103 | ctx.lineTo(13.5, 14);
104 | ctx.moveTo(7.5, 10);
105 | ctx.lineTo(10.5, 7);
106 | ctx.lineTo(13.5, 10);
107 | ctx.stroke();
108 | break;
109 | case WindowControlButton.Type.KeepBelowButton:
110 | ctx.moveTo(7.5, 11);
111 | ctx.lineTo(10.5, 14);
112 | ctx.lineTo(13.5, 11);
113 | ctx.moveTo(7.5, 7);
114 | ctx.lineTo(10.5, 10);
115 | ctx.lineTo(13.5, 7);
116 | ctx.stroke();
117 | break;
118 | case WindowControlButton.Type.ShadeButton:
119 | if (checked) {
120 | ctx.moveTo(7.5, 10.5);
121 | ctx.lineTo(10.5, 7.5);
122 | ctx.lineTo(13.5, 10.5);
123 | } else {
124 | ctx.moveTo(7.5, 7.5);
125 | ctx.lineTo(10.5, 10.5);
126 | ctx.lineTo(13.5, 7.5);
127 | }
128 | ctx.moveTo(7.5, 13);
129 | ctx.lineTo(13.5, 13);
130 | ctx.stroke();
131 | break;
132 | case WindowControlButton.Type.HelpButton:
133 | const radsInDegree = Math.PI / 180;
134 | ctx.translate(1.5, 1.5);
135 | arc(ctx, 7, 5, 4, 135, -180);
136 | ctx.stroke();
137 | ctx.beginPath();
138 | arc(ctx, 9, 8, 4, 135, 45);
139 | ctx.stroke();
140 | drawPoint(ctx, 9, 12);
141 | ctx.translate(-1.5, -1.5);
142 | break;
143 | case WindowControlButton.Type.MenuButton:
144 | case WindowControlButton.Type.AppMenuButton:
145 | ctx.moveTo(7.5, 7.5);
146 | ctx.lineTo(13.5, 7.5);
147 | ctx.moveTo(7.5, 10.5);
148 | ctx.lineTo(13.5, 10.5);
149 | ctx.moveTo(7.5, 13.5);
150 | ctx.lineTo(13.5, 13.5);
151 | ctx.stroke();
152 | break;
153 | default:
154 | break;
155 | }
156 | descaleCtx(ctx, 21);
157 | ctx.restore();
158 | }
159 |
160 | function drawButton(ctx, glowColor, sunken) {
161 | scaleCtx(ctx, 21);
162 | drawButtonShadow(ctx, shadowColor, 21);
163 | descaleCtx(ctx, 21);
164 | scaleCtx(ctx, 18);
165 | drawButtonSlab(ctx, 18, sunken);
166 | descaleCtx(ctx, 18);
167 | }
168 |
169 | function drawButtonShadow(ctx, color, size) {
170 | let m = (size - 2) / 2;
171 | let offset = 0.8;
172 | let k0 = (m - 4) / m;
173 | let x = m + 1.0;
174 | let y = m + offset + 1.0;
175 | let shadowGradient = ctx.createRadialGradient(x, y, 0, x, y, m);
176 | for (let i = 0; i < 8; i++) {
177 | let k1 = (k0 * (8 - i) + i) / 8;
178 | let a = (Math.cos(Math.PI * i / 8) + 1) * 0.3;
179 | let colorStop = Qt.alpha(color, color.a * a * 1.5);
180 | shadowGradient.addColorStop(k1, colorStop);
181 | }
182 | shadowGradient.addColorStop(1, "transparent");
183 | ctx.save();
184 | ctx.translate(0, -0.2);
185 | ctx.fillStyle = shadowGradient;
186 | ctx.beginPath();
187 | ctx.ellipse(0, 0, size, size);
188 | ctx.fill();
189 | ctx.restore();
190 | }
191 |
192 | function drawButtonGlow(ctx, color) {
193 | let size = 21;
194 | let m = size / 2;
195 | let glowWidth = 3;
196 | let glowBias = 0.6;
197 | let bias = glowBias * 14 / size;
198 | let gm = m + bias - 0.9;
199 | let k0 = (m - glowWidth + bias) / gm;
200 | let glowGradient = ctx.createRadialGradient(m, m, 0, m, m, gm);
201 | for (let i = 0; i < 8; i++) {
202 | let k1 = k0 + i * (1 - k0) / 8;
203 | let a = 1 - Math.sqrt(i / 8);
204 | let colorStop = Qt.alpha(color, color.a * a);
205 | glowGradient.addColorStop(k1, colorStop);
206 | }
207 | glowGradient.addColorStop(1, "transparent");
208 | ctx.save();
209 | scaleCtx(ctx, 21);
210 | ctx.translate(0, -0.2);
211 | ctx.fillStyle = glowGradient;
212 | ctx.beginPath();
213 | ctx.ellipse(0, 0, size, size);
214 | ctx.fill();
215 | ctx.globalCompositeOperation = "destination-out";
216 | ctx.fillStyle = "black";
217 | ctx.beginPath();
218 | ctx.ellipse(width + 0.5, width + 0.5, size - width - 1, size - width - 1);
219 | ctx.fill();
220 | descaleCtx(ctx, 21);
221 | ctx.restore();
222 | }
223 |
224 | function drawButtonSlab(ctx, size, sunken) {
225 | ctx.save();
226 | ctx.translate(0, 1);
227 | let lighter = midLightBackgroundColor;
228 | let darker = midDarkBackgroundColor;
229 | let backgroundGradient = ctx.createLinearGradient(0, 1.665, 0, (12.33 + 1.665));
230 | if (sunken) {
231 | backgroundGradient.addColorStop(1, lighter);
232 | backgroundGradient.addColorStop(0, darker);
233 | } else {
234 | backgroundGradient.addColorStop(1, darker);
235 | backgroundGradient.addColorStop(0, lighter);
236 | }
237 | let contourGradient = ctx.createLinearGradient(0, 1.665, 0, (2.0 * 12.33 + 1.665));
238 | contourGradient.addColorStop(1, lighter);
239 | contourGradient.addColorStop(0, darker);
240 | ctx.beginPath();
241 | ctx.ellipse(0.5 * (18 - 12.33), 1.665, 12.33, 12.33);
242 | ctx.fillStyle = backgroundGradient;
243 | ctx.lineStyle = contourGradient;
244 | ctx.lineWidth = 0.7 / size / width;
245 | ctx.fill();
246 | ctx.stroke();
247 | ctx.restore();
248 | }
249 |
250 | function scaleCtx(ctx, scale) {
251 | ctx.scale(width / scale, width / scale);
252 | }
253 |
254 | function descaleCtx(ctx, scale) {
255 | ctx.scale(scale / width, scale / width);
256 | }
257 |
258 | function normalize(val) {
259 | return Math.min(1, Math.max(0, val));
260 | }
261 |
262 | function calcGlowColor() {
263 | if (buttonType === WindowControlButton.Type.CloseButton) {
264 | return negativeForegroundColor;
265 | } else if (hovered && toggleButton && checked) {
266 | return inactiveForegroundColor;
267 | } else if (toggleButton && checked) {
268 | return checkedForegroundColor;
269 | } else {
270 | return hoverForegroundColor;
271 | }
272 | }
273 |
274 | function arc(ctx, x, y, size, startAngle, spanAngle) {
275 | let r = size / 2;
276 | const radsInDegree = Math.PI / 180;
277 | let start = -startAngle * radsInDegree;
278 | let end = -(startAngle + spanAngle) * radsInDegree;
279 | ctx.arc(x + r, y + r, r, start, end, spanAngle > 0);
280 | }
281 |
282 | function drawPoint(ctx, x, y) {
283 | let radius = ctx.lineWidth / 2;
284 | ctx.beginPath();
285 | ctx.ellipse(x - radius, y - radius, ctx.lineWidth, ctx.lineWidth);
286 | ctx.fill();
287 | }
288 |
289 | function isToggleButton() {
290 | switch (buttonType) {
291 | case WindowControlButton.Type.AllDesktopsButton:
292 | case WindowControlButton.Type.KeepAboveButton:
293 | case WindowControlButton.Type.KeepBelowButton:
294 | return true;
295 | default:
296 | return false;
297 | }
298 | }
299 |
300 | onHoveredChanged: Qt.callLater(requestPaint)
301 | onActiveChanged: Qt.callLater(requestPaint)
302 | onPressedChanged: Qt.callLater(requestPaint)
303 | onCheckedChanged: Qt.callLater(requestPaint)
304 | onWidthChanged: Qt.callLater(requestPaint)
305 | onHeightChanged: Qt.callLater(requestPaint)
306 | onColorsChanged: Qt.callLater(requestPaint)
307 | onButtonTypeChanged: Qt.callLater(requestPaint)
308 | }
309 |
--------------------------------------------------------------------------------
/package/contents/ui/theme/PlasmaWindowControlButton.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 | import QtQuick
7 |
8 | PlasmaWindowControlButtonIcon {
9 | anchors.fill: parent
10 |
11 | property bool disabled: false
12 | property int animationDuration: 0
13 |
14 | opacity: 0
15 |
16 | Behavior on opacity {
17 | NumberAnimation {
18 | duration: animationDuration
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/package/contents/ui/theme/PlasmaWindowControlButtonIcon.qml:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 |
7 | import QtCore
8 | import QtQuick
9 | import QtQuick.Controls
10 | import QtQuick.Effects
11 | import QtQuick.Shapes
12 | import org.kde.kirigami as Kirigami
13 | import "../"
14 |
15 | Item {
16 | id: iconRoot
17 |
18 | property bool active: true
19 | property bool hovered: false
20 | property bool pressed: false
21 | property bool checked: false
22 | property int buttonType: WindowControlButton.Type.MinimizeButton
23 |
24 | anchors.fill: parent
25 |
26 | Shape {
27 | id: iconBackground
28 |
29 | layer.enabled: true
30 | anchors.fill: parent
31 | opacity: hovered ? 0.25 : pressed || checked ? 0.15 : 0
32 |
33 | ShapePath {
34 | startX: 0
35 | startY: height / 2
36 | fillGradient: radialGradient
37 | strokeColor: "transparent"
38 |
39 | PathArc {
40 | x: width
41 | y: height / 2
42 | radiusX: iconBackground.width / 2
43 | radiusY: height / 2
44 | useLargeArc: true
45 | }
46 |
47 | PathArc {
48 | x: 0
49 | y: height / 2
50 | radiusX: iconBackground.width / 2
51 | radiusY: height / 2
52 | useLargeArc: true
53 | }
54 | }
55 |
56 | RadialGradient {
57 | id: radialGradient
58 |
59 | centerX: iconBackground.width / 2
60 | centerY: iconBackground.height / 2
61 | centerRadius: iconBackground.width / 2
62 | focalX: centerX
63 | focalY: centerY
64 |
65 | GradientStop {
66 | position: 0
67 | color: Kirigami.Theme.textColor
68 | }
69 |
70 | GradientStop {
71 | position: 1
72 | color: "transparent"
73 | }
74 | }
75 |
76 | Behavior on opacity {
77 | NumberAnimation {
78 | duration: button.animationDuration
79 | }
80 | }
81 | }
82 |
83 | Kirigami.Icon {
84 | id: icon
85 |
86 | anchors.fill: parent
87 | source: plasmaIconName(iconRoot.buttonType)
88 | }
89 |
90 | MultiEffect {
91 | anchors.fill: source
92 | source: icon
93 | blurEnabled: true
94 | blurMax: 8
95 | brightness: hovered ? 0.6 : pressed || checked ? 0.3 : 0
96 | blur: hovered ? 4 : pressed || checked ? 2 : 0
97 | saturation: active ? 0 : -0.5
98 |
99 | Behavior on blur {
100 | NumberAnimation {
101 | duration: button.animationDuration
102 | }
103 | }
104 |
105 | Behavior on brightness {
106 | NumberAnimation {
107 | duration: button.animationDuration
108 | }
109 | }
110 | }
111 |
112 | function plasmaIconName(type) {
113 | switch (type) {
114 | case WindowControlButton.Type.MinimizeButton:
115 | return "window-minimize";
116 | case WindowControlButton.Type.MaximizeButton:
117 | return "window-maximize";
118 | case WindowControlButton.Type.RestoreButton:
119 | return "window-restore";
120 | case WindowControlButton.Type.CloseButton:
121 | return "window-close";
122 | case WindowControlButton.Type.AllDesktopsButton:
123 | return "window-pin";
124 | case WindowControlButton.Type.KeepAboveButton:
125 | return "window-keep-above";
126 | case WindowControlButton.Type.KeepBelowButton:
127 | return "window-keep-below";
128 | case WindowControlButton.Type.ShadeButton:
129 | return "window-shade";
130 | case WindowControlButton.Type.HelpButton:
131 | return "question";
132 | case WindowControlButton.Type.MenuButton:
133 | return "plasma-symbolic";
134 | case WindowControlButton.Type.AppMenuButton:
135 | return "application-menu";
136 | default:
137 | return "";
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/package/contents/ui/utils.js:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 |
7 | function isX11() {
8 | const isX11Plugin = Qt.platform.pluginName == "xcb";
9 | return isX11Plugin;
10 | }
11 |
12 | function copyLayoutConstraint(from, to) {
13 | Object.assign(to.Layout, {
14 | "alignment": Qt.binding(function () { return from.Layout.alignment }),
15 | "bottomMargin": Qt.binding(function () { return from.Layout.bottomMargin }),
16 | "column": Qt.binding(function () { return from.Layout.column }),
17 | "columnSpan": Qt.binding(function () { return from.Layout.columnSpan }),
18 | "fillHeight": Qt.binding(function () { return from.Layout.fillHeight }),
19 | "fillWidth": Qt.binding(function () { return from.Layout.fillWidth }),
20 | "horizontalStretchFactor": Qt.binding(function () { return from.Layout.horizontalStretchFactor }),
21 | "leftMargin": Qt.binding(function () { return from.Layout.leftMargin }),
22 | "maximumHeight": Qt.binding(function () { return from.Layout.maximumHeight }),
23 | "maximumWidth": Qt.binding(function () { return from.Layout.maximumWidth }),
24 | "minimumHeight": Qt.binding(function () { return from.Layout.minimumHeight }),
25 | "minimumWidth": Qt.binding(function () { return from.Layout.minimumWidth }),
26 | "preferredHeight": Qt.binding(function () { return from.Layout.preferredHeight }),
27 | "preferredWidth": Qt.binding(function () { return from.Layout.preferredWidth }),
28 | "rightMargin": Qt.binding(function () { return from.Layout.rightMargin }),
29 | "row": Qt.binding(function () { return from.Layout.row }),
30 | "rowSpan": Qt.binding(function () { return from.Layout.rowSpan }),
31 | "topMargin": Qt.binding(function () { return from.Layout.topMargin }),
32 | "verticalStretchFactor": Qt.binding(function () { return from.Layout.verticalStretchFactor })
33 | })
34 | }
35 |
36 | function calculateItemPreferredWidth(item) {
37 | var preferredWidth = 0;
38 |
39 | if (item && item.Layout) {
40 | preferredWidth += item.Layout.preferredWidth || 0;
41 | preferredWidth += item.Layout.leftMargin || 0;
42 | preferredWidth += item.Layout.rightMargin || 0;
43 | }
44 |
45 | return preferredWidth;
46 | }
47 |
48 | function widgetElementModelFromName(name) {
49 | switch (name) {
50 | case "windowCloseButton":
51 | return {
52 | "type": WidgetElement.Type.WindowControlButton,
53 | "windowControlButtonType": WindowControlButton.Type.CloseButton
54 | };
55 | case "windowMinimizeButton":
56 | return {
57 | "type": WidgetElement.Type.WindowControlButton,
58 | "windowControlButtonType": WindowControlButton.Type.MinimizeButton
59 | };
60 | case "windowMaximizeButton":
61 | return {
62 | "type": WidgetElement.Type.WindowControlButton,
63 | "windowControlButtonType": WindowControlButton.Type.MaximizeButton
64 | };
65 | case "windowAllDesktopsButton":
66 | return {
67 | "type": WidgetElement.Type.WindowControlButton,
68 | "windowControlButtonType": WindowControlButton.Type.AllDesktopsButton
69 | };
70 | case "windowKeepAboveButton":
71 | return {
72 | "type": WidgetElement.Type.WindowControlButton,
73 | "windowControlButtonType": WindowControlButton.Type.KeepAboveButton
74 | };
75 | case "windowKeepBelowButton":
76 | return {
77 | "type": WidgetElement.Type.WindowControlButton,
78 | "windowControlButtonType": WindowControlButton.Type.KeepBelowButton
79 | };
80 | case "windowShadeButton":
81 | return {
82 | "type": WidgetElement.Type.WindowControlButton,
83 | "windowControlButtonType": WindowControlButton.Type.ShadeButton
84 | };
85 | case "windowHelpButton":
86 | return {
87 | "type": WidgetElement.Type.WindowControlButton,
88 | "windowControlButtonType": WindowControlButton.Type.HelpButton
89 | };
90 | case "windowMenuButton":
91 | return {
92 | "type": WidgetElement.Type.WindowControlButton,
93 | "windowControlButtonType": WindowControlButton.Type.MenuButton
94 | };
95 | case "windowAppMenuButton":
96 | return {
97 | "type": WidgetElement.Type.WindowControlButton,
98 | "windowControlButtonType": WindowControlButton.Type.AppMenuButton
99 | };
100 | case "windowTitle":
101 | return {
102 | "type": WidgetElement.Type.WindowTitle
103 | };
104 | case "windowIcon":
105 | return {
106 | "type": WidgetElement.Type.WindowIcon
107 | };
108 | case "spacer":
109 | return {
110 | "type": WidgetElement.Type.Spacer
111 | };
112 | }
113 | }
114 |
115 | function truncateString(str, n) {
116 | return (str.length > n) ? str.slice(0, n - 1) + '\u2026' : str;
117 | };
118 |
119 | class Replacement {
120 | replace(_title) {
121 | throw new Error("Not implemented");
122 | }
123 |
124 | static createReplacement(type, pattern, template) {
125 | switch (type) {
126 | case 0: return new StringReplacement(pattern, template);
127 | case 1: return new RegExpReplacement(pattern, template);
128 | default:
129 | throw new Error("Unknown replacement type: " + type);
130 | }
131 | }
132 |
133 | static createReplacementList(types, patterns, templates) {
134 | const length = types.length;
135 | let result = new Array();
136 | for (let i = 0; i < length; i++) {
137 | const type = types[i];
138 | const pattern = patterns[i];
139 | const template = templates[i];
140 | if (type === undefined || pattern === undefined || template === undefined) {
141 | break; // Inconsistent state
142 | } else {
143 | result.push(Replacement.createReplacement(type, pattern, template));
144 | }
145 | }
146 | return result;
147 | }
148 |
149 | static applyReplacementList(title, replacements) {
150 | return replacements.reduce((acc, cur) => cur.replace(acc), title);
151 | }
152 | }
153 |
154 | class StringReplacement extends Replacement {
155 | constructor(stringToReplace, stringReplacement) {
156 | super();
157 | this.stringToReplace = stringToReplace;
158 | this.stringReplacement = stringReplacement;
159 | }
160 |
161 | replace(title) {
162 | let replaced = title.replace(this.stringToReplace, this.stringReplacement);
163 | while (replaced !== title) {
164 | title = replaced;
165 | replaced = title.replace(this.stringToReplace, this.stringReplacement);
166 | }
167 | return replaced;
168 | }
169 | }
170 |
171 | class RegExpReplacement extends Replacement {
172 | constructor(regExp, replacement) {
173 | super();
174 | this.regExp = new RegExp(regExp, "g");
175 | this.replacement = replacement;
176 | }
177 |
178 | replace(title) {
179 | return title.replace(this.regExp, this.replacement);
180 | }
181 | }
--------------------------------------------------------------------------------
/package/contents/ui/windowControlButton.js:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: 2024 Anton Kharuzhy
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-or-later
5 | */
6 |
7 | const statePropertiesMap = [
8 | { active: true, pressed: false, checked: false, hovered: false, disabled: false }, // Active
9 | { active: true, pressed: false, checked: false, hovered: true, disabled: false }, // ActiveHover
10 | { active: true, pressed: true, checked: false, hovered: false, disabled: false }, // ActivePressed
11 | { active: true, pressed: false, checked: true, hovered: false, disabled: false }, // ActiveChecked
12 | { active: true, pressed: false, checked: true, hovered: true, disabled: false }, // ActiveHoverChecked
13 | { active: true, pressed: false, checked: true, hovered: false, disabled: true }, // ActiveCheckedDisabled
14 | { active: true, pressed: false, checked: false, hovered: false, disabled: true }, // ActiveDisabled
15 | { active: false, pressed: false, checked: false, hovered: false, disabled: false }, // Inactive
16 | { active: false, pressed: false, checked: false, hovered: true, disabled: false }, // InactiveHover
17 | { active: false, pressed: true, checked: false, hovered: false, disabled: false }, // InactivePressed
18 | { active: false, pressed: false, checked: true, hovered: false, disabled: false }, // InactiveChecked
19 | { active: false, pressed: false, checked: true, hovered: true, disabled: false }, // InactiveHoverChecked
20 | { active: false, pressed: false, checked: true, hovered: false, disabled: true }, // InactiveCheckedDisabled
21 | { active: false, pressed: false, checked: false, hovered: false, disabled: true }, // InactiveDisabled
22 | ];
23 |
24 | function calculateIconState(button) {
25 | if (button.active) {
26 | if (button.pressed)
27 | return WindowControlButton.IconState.ActivePressed;
28 | else if (button.checked && !button.enabled)
29 | return WindowControlButton.IconState.ActiveCheckedDisabled;
30 | else if (button.checked && button.hovered)
31 | return WindowControlButton.IconState.ActiveHoverChecked;
32 | else if (button.checked)
33 | return WindowControlButton.IconState.ActiveChecked;
34 | else if (button.hovered)
35 | return WindowControlButton.IconState.ActiveHover;
36 | else if (!button.enabled)
37 | return WindowControlButton.IconState.ActiveDisabled;
38 | else
39 | return WindowControlButton.IconState.Active;
40 | } else {
41 | if (button.pressed)
42 | return WindowControlButton.IconState.InactivePressed;
43 | else if (button.checked && !button.enabled)
44 | return WindowControlButton.IconState.InactiveCheckedDisabled;
45 | else if (button.checked && button.hovered)
46 | return WindowControlButton.IconState.InactiveHoverChecked;
47 | else if (button.checked)
48 | return WindowControlButton.IconState.InactiveChecked;
49 | else if (button.hovered)
50 | return WindowControlButton.IconState.InactiveHover;
51 | else if (!button.enabled)
52 | return WindowControlButton.IconState.InactiveDisabled;
53 | else
54 | return WindowControlButton.IconState.Inactive;
55 | }
56 | }
57 |
58 | function getAction(buttonType) {
59 | switch (buttonType) {
60 | case WindowControlButton.Type.MinimizeButton:
61 | return ActiveWindow.Action.Minimize;
62 | case WindowControlButton.Type.MaximizeButton:
63 | return ActiveWindow.Action.Maximize;
64 | case WindowControlButton.Type.RestoreButton:
65 | return ActiveWindow.Action.Maximize;
66 | case WindowControlButton.Type.CloseButton:
67 | return ActiveWindow.Action.Close;
68 | case WindowControlButton.Type.AllDesktopsButton:
69 | return ActiveWindow.Action.AllDesktops;
70 | case WindowControlButton.Type.KeepAboveButton:
71 | return ActiveWindow.Action.KeepAbove;
72 | case WindowControlButton.Type.KeepBelowButton:
73 | return ActiveWindow.Action.KeepBelow;
74 | case WindowControlButton.Type.ShadeButton:
75 | return ActiveWindow.Action.Shade;
76 | case WindowControlButton.Type.HelpButton:
77 | return ActiveWindow.Action.Help;
78 | case WindowControlButton.Type.MenuButton:
79 | return ActiveWindow.Action.Menu;
80 | case WindowControlButton.Type.AppMenuButton:
81 | return ActiveWindow.Action.AppMenu;
82 | default:
83 | return "";
84 | }
85 | }
86 |
87 | function getButtonComponentSourcePath(iconTheme) {
88 | switch (iconTheme) {
89 | case WindowControlButton.IconTheme.Breeze:
90 | return "theme/BreezeWindowControlButton.qml";
91 | case WindowControlButton.IconTheme.Oxygen:
92 | return "theme/OxygenWindowControlButton.qml";
93 | case WindowControlButton.IconTheme.Aurorae:
94 | return "theme/AuroraeWindowControlButton.qml";
95 | default:
96 | return "theme/PlasmaWindowControlButton.qml";
97 | }
98 | }
--------------------------------------------------------------------------------
/package/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "KPlugin": {
3 | "Authors": [
4 | {
5 | "Email": "publicantroids@gmail.com",
6 | "Name": "Anton Kharuzhy"
7 | }
8 | ],
9 | "Category": "Windows and Tasks",
10 | "Description": "A widget with buttons to control active task",
11 | "Icon": "window-close",
12 | "Id": "com.github.antroids.application-title-bar",
13 | "Name": "Application Title Bar",
14 | "License": "GPL-3.0+",
15 | "Version": "0.8.5",
16 | "Website": "https://github.com/antroids/application-title-bar",
17 | "FormFactors": [
18 | "desktop"
19 | ]
20 | },
21 | "X-Plasma-API": "declarativeappletscript",
22 | "KPackageStructure": "Plasma/Applet",
23 | "X-Plasma-API-Minimum-Version": "6.0"
24 | }
--------------------------------------------------------------------------------
/test/functional-test.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | #
3 | # SPDX-FileCopyrightText: 2024 Anton Kharuzhy
4 | #
5 | # SPDX-License-Identifier: GPL-3.0-or-later
6 | #
7 |
8 | import unittest
9 | from appium import webdriver
10 | from appium.webdriver.common.appiumby import AppiumBy
11 | from appium.options.common.base import AppiumOptions
12 | from selenium.webdriver.support.ui import WebDriverWait
13 | from selenium.webdriver.common.action_chains import ActionChains
14 | from selenium.webdriver.common.by import By
15 | import time
16 | from subprocess import check_output
17 | import subprocess
18 |
19 |
20 | class SimpleCalculatorTests(unittest.TestCase):
21 |
22 | @classmethod
23 |
24 | def setUpClass(self):
25 | options = AppiumOptions()
26 | # The app capability may be a command line or a desktop file id.
27 | options.set_capability("app", "plasmashell --replace")
28 | # Boilerplate, always the same
29 | self.driver = webdriver.Remote(
30 | command_executor='http://127.0.0.1:4723',
31 | options=options)
32 | # Set a timeout for waiting to find elements. If elements cannot be found
33 | # in time we'll get a test failure. This should be somewhat long so as to
34 | # not fall over when the system is under load, but also not too long that
35 | # the test takes forever.
36 | self.driver.implicitly_wait = 10
37 | time.sleep(30);
38 | print("elements: " + self.driver.find_elements(by=By.XPATH, value="//*"));
39 |
40 | def setUp(self):
41 | self.driver.find_element("//*[@name='Application title bar']//*[@name='Close']").click()
42 | widget = self.driver.find_element("//*[@name='Application title bar']")
43 | ActionChains(self.driver).context_click(widget).perform()
44 |
45 | @classmethod
46 | def tearDownClass(self):
47 | self.driver.quit()
48 | subprocess.run("systemctl --user restart plasma-plasmashell.service");
49 |
50 | def test_initialize(self):
51 | self.driver.find_element(by=AppiumBy.NAME, value="AC").click()
52 | self.driver.find_element(by=AppiumBy.NAME, value="7").click()
53 |
54 |
55 | if __name__ == '__main__':
56 | unittest.main()
57 |
--------------------------------------------------------------------------------