mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-29 22:47:47 -04:00
feat(settings + keyboard): core clean up & keyboard driven overhaul in settings
This commit is contained in:
1 parent
c668ab9fdf
commit
de1c5c99fa
85 files changed
+2264
-1891
No files matched your search
+1
-1
Submodule dank-qml-common updated: fdd2dfe3f3...fd2f92dc75.
Generated
+3
-3
@@ -3,11 +3,11 @@
|
||||
"dank-qml-common": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1790818052,
|
||||
"narHash": "sha256-B4QALG/6pkJuxKOOSs7wU+qJdOXHWYUt2YK6E3Y5M84=",
|
||||
"lastModified": 1790897239,
|
||||
"narHash": "sha256-JgDpkgnLeNuLFLcxpo3oLJC7NC7uiM7+w+Jv1e4xlIM=",
|
||||
"owner": "AvengeMedia",
|
||||
"repo": "dank-qml-common",
|
||||
"rev": "fdd2dfe3f3444585bc1f44180837724abbdfe7f1",
|
||||
"rev": "fd2f92dc755aebc3ecda4b0ce3e27ce6d6ca9f92",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -276,4 +276,8 @@ Singleton {
|
||||
function hasOptions(id) {
|
||||
return optionFiles[id] !== undefined;
|
||||
}
|
||||
|
||||
function configurable(item) {
|
||||
return hasOptions(item?.id) || !!item?.pluginId;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,10 @@ import qs.Modules.Settings.Widgets
|
||||
SettingsNavRow {
|
||||
id: root
|
||||
|
||||
signal navigationRequested(bool keyboard)
|
||||
|
||||
onClicked: keyboard => navigationRequested(keyboard)
|
||||
|
||||
title: UserInfoService.fullName || I18n.tr("User")
|
||||
hint: DgopService.hostname || "DMS"
|
||||
singleLineTitle: true
|
||||
|
||||
@@ -27,17 +27,158 @@ FocusScope {
|
||||
readonly property string pageTitle: (pageInfo?.titleFrom ? SettingsUiState[pageInfo.titleFrom] : "") || (pageInfo?.text ?? "")
|
||||
readonly property real pageContentMaxWidth: currentPageItem?.contentMaxWidth ?? SettingsMetrics.contentMaxWidth
|
||||
readonly property bool animationsEnabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
|
||||
// Mouse navigation lands parked; only keyboard navigation highlights a control on the page
|
||||
property bool keyboardNavigation: true
|
||||
|
||||
focus: true
|
||||
|
||||
function rememberFocus() {
|
||||
const item = Window.activeFocusItem;
|
||||
if (pageStack.currentItem && item !== currentPageItem && _contains(currentPageItem, item))
|
||||
pageStack.currentItem.rememberedFocus = item;
|
||||
}
|
||||
|
||||
function _validRemembered() {
|
||||
const remembered = pageStack.currentItem?.rememberedFocus;
|
||||
return remembered?.visible && remembered.enabled && _contains(currentPageItem, remembered) ? remembered : null;
|
||||
}
|
||||
|
||||
function _focusPage() {
|
||||
const revision = parentModal?.modalFocusScope?.focusRevision;
|
||||
Qt.callLater(() => {
|
||||
if (!sessionVisible || !currentPageItem || parentModal?.searchFocused)
|
||||
if (revision !== parentModal?.modalFocusScope?.focusRevision)
|
||||
return;
|
||||
currentPageItem.forceActiveFocus();
|
||||
if (parentModal?.modalFocusScope?.transientOwnsFocus())
|
||||
return;
|
||||
if (!sessionVisible || !currentPageItem || !pageStack.currentItem.presented)
|
||||
return;
|
||||
if (parentModal?.focusPane === "sidebar")
|
||||
return;
|
||||
if (!keyboardNavigation) {
|
||||
if (!_contains(currentPageItem, Window.activeFocusItem))
|
||||
parkFocus();
|
||||
return;
|
||||
}
|
||||
const remembered = _validRemembered();
|
||||
if (remembered) {
|
||||
remembered.forceActiveFocus(Qt.TabFocusReason);
|
||||
_reveal(remembered);
|
||||
return;
|
||||
}
|
||||
let item = currentPageItem;
|
||||
do {
|
||||
item = item.nextItemInFocusChain(true);
|
||||
if (!item || item === currentPageItem || !_contains(currentPageItem, item)) {
|
||||
currentPageItem.forceActiveFocus(Qt.TabFocusReason);
|
||||
return;
|
||||
}
|
||||
} while (!item.visible || !item.enabled || !_inViewport(item));
|
||||
item.forceActiveFocus(Qt.TabFocusReason);
|
||||
});
|
||||
}
|
||||
|
||||
function goBack(keyboard) {
|
||||
if (!showBack)
|
||||
return false;
|
||||
if (menuHidden && !(parentModal?.canGoBack ?? false)) {
|
||||
parentModal.toggleMenu();
|
||||
return true;
|
||||
}
|
||||
parentModal?.goBack(keyboard);
|
||||
return true;
|
||||
}
|
||||
|
||||
function _contains(ancestor, item) {
|
||||
for (let p = item; p; p = p.parent) {
|
||||
if (p === ancestor)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parked focus highlights nothing until a key resumes from rememberedFocus
|
||||
function parkFocus() {
|
||||
focusAnchor.forceActiveFocus(Qt.MouseFocusReason);
|
||||
}
|
||||
|
||||
Item {
|
||||
id: focusAnchor
|
||||
}
|
||||
|
||||
function _moveFocus(forward) {
|
||||
const page = currentPageItem;
|
||||
let start = Window.activeFocusItem;
|
||||
if (!page || (start !== focusAnchor && !_contains(page, start)))
|
||||
return false;
|
||||
if (start === focusAnchor) {
|
||||
const remembered = _validRemembered();
|
||||
if (remembered) {
|
||||
remembered.forceActiveFocus(Qt.TabFocusReason);
|
||||
_reveal(remembered);
|
||||
return true;
|
||||
}
|
||||
start = page;
|
||||
}
|
||||
let item = start;
|
||||
do {
|
||||
item = item.nextItemInFocusChain(forward);
|
||||
if (!item || item === start || !_contains(page, item))
|
||||
return false;
|
||||
} while (!item.visible || !item.enabled);
|
||||
item.forceActiveFocus(forward ? Qt.TabFocusReason : Qt.BacktabFocusReason);
|
||||
_reveal(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
function _inViewport(item) {
|
||||
for (let f = item.parent; f; f = f.parent) {
|
||||
if (typeof f.flick !== "function")
|
||||
continue;
|
||||
const y = item.mapToItem(f.contentItem, 0, 0).y;
|
||||
const top = Math.max(0, y - Theme.spacingL);
|
||||
const bottom = Math.min(f.contentHeight, y + item.height + Theme.spacingL);
|
||||
if (top < f.contentY || bottom > f.contentY + f.height)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function _reveal(item) {
|
||||
for (let f = item.parent; f; f = f.parent) {
|
||||
if (typeof f.flick !== "function" || f.contentHeight <= f.height)
|
||||
continue;
|
||||
const top = item.mapToItem(f.contentItem, 0, 0).y - Theme.spacingL;
|
||||
const bottom = top + item.height + Theme.spacingL * 2;
|
||||
if (top < f.contentY)
|
||||
f.contentY = Math.max(0, top);
|
||||
else if (bottom > f.contentY + f.height)
|
||||
f.contentY = Math.min(f.contentHeight - f.height, bottom - f.height);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Keys bubble up from the focused control, so a control that uses an arrow itself (text, lists, open menus) keeps it
|
||||
Keys.onPressed: event => {
|
||||
const mods = event.modifiers & ~Qt.KeypadModifier;
|
||||
const backKey = I18n.isRtl ? Qt.Key_Right : Qt.Key_Left;
|
||||
if ((event.key === Qt.Key_Up || event.key === Qt.Key_Down) && mods === Qt.NoModifier) {
|
||||
event.accepted = _moveFocus(event.key === Qt.Key_Down);
|
||||
return;
|
||||
}
|
||||
if ((event.key === backKey && mods === Qt.AltModifier) || (event.key === Qt.Key_Backspace && mods === Qt.NoModifier)) {
|
||||
event.accepted = goBack(true);
|
||||
return;
|
||||
}
|
||||
if (event.key !== Qt.Key_Escape || mods !== Qt.NoModifier)
|
||||
return;
|
||||
const focused = Window.activeFocusItem;
|
||||
if (focused && focused !== currentPageItem && focused.cursorPosition !== undefined)
|
||||
currentPageItem?.forceActiveFocus();
|
||||
else if (!goBack(true))
|
||||
parentModal?.focusSidebar();
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
function _fileFor(page) {
|
||||
if (pageFiles[page])
|
||||
return pageFiles[page];
|
||||
@@ -114,6 +255,30 @@ FocusScope {
|
||||
}
|
||||
}
|
||||
|
||||
function _scrollerOf(item) {
|
||||
if (typeof item.flick === "function")
|
||||
return item;
|
||||
for (const child of item.children) {
|
||||
const found = _scrollerOf(child);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// A page's Flickable accepts every press inside it before the pane handler sees it, so the scroller carries its own.
|
||||
// Handlers die with their parent, so one per page.
|
||||
Component {
|
||||
id: scrollerTap
|
||||
|
||||
TapHandler {
|
||||
onPressedChanged: {
|
||||
if (pressed)
|
||||
root.parentModal?.modalFocusScope?.pointerFocus("content");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pageComponent
|
||||
|
||||
@@ -127,6 +292,7 @@ FocusScope {
|
||||
readonly property int status: loader.status
|
||||
readonly property bool settled: !pending && loader.status !== Loader.Loading && (presented || loader.status !== Loader.Ready)
|
||||
|
||||
property Item rememberedFocus: null
|
||||
property bool pending: true
|
||||
property bool presented: false
|
||||
|
||||
@@ -147,10 +313,16 @@ FocusScope {
|
||||
|
||||
anchors.fill: parent
|
||||
opacity: host.presented ? 1 : 0
|
||||
enabled: host.presented
|
||||
|
||||
onLoaded: {
|
||||
if (item.pageActive !== undefined)
|
||||
item.pageActive = Qt.binding(() => host.pageActive);
|
||||
const scroller = root._scrollerOf(item);
|
||||
if (scroller)
|
||||
scrollerTap.createObject(scroller, {
|
||||
"parent": scroller
|
||||
});
|
||||
root._focusPage();
|
||||
}
|
||||
}
|
||||
@@ -180,6 +352,11 @@ FocusScope {
|
||||
}
|
||||
}
|
||||
|
||||
onPresentedChanged: {
|
||||
if (presented && pageActive)
|
||||
root._focusPage();
|
||||
}
|
||||
|
||||
onPageActiveChanged: {
|
||||
if (pageActive)
|
||||
root._focusPage();
|
||||
@@ -281,13 +458,7 @@ FocusScope {
|
||||
iconSize: Theme.iconSize
|
||||
iconColor: Theme.surfaceText
|
||||
visible: root.showBack
|
||||
onClicked: {
|
||||
if (root.menuHidden && !(root.parentModal?.canGoBack ?? false)) {
|
||||
root.parentModal.toggleMenu();
|
||||
return;
|
||||
}
|
||||
root.parentModal?.goBack();
|
||||
}
|
||||
onClicked: root.goBack()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ DankFloatingWindow {
|
||||
}
|
||||
property alias sidebar: sidebar
|
||||
readonly property alias modalFocusScope: contentFocusScope
|
||||
readonly property alias focusPane: contentFocusScope.activePane
|
||||
property string currentPage: "personalization"
|
||||
property var pageHistory: []
|
||||
readonly property int currentTabIndex: SettingsTabs.tabIndexForPage(currentPage)
|
||||
@@ -54,6 +55,8 @@ DankFloatingWindow {
|
||||
visible = false;
|
||||
}
|
||||
CompositorService.closeNiriOverviewOnWindowFocus();
|
||||
if (!shouldBeVisible && isCompactMode)
|
||||
menuVisible = true;
|
||||
shouldBeVisible = true;
|
||||
if (readyToMap)
|
||||
visible = true;
|
||||
@@ -82,12 +85,15 @@ DankFloatingWindow {
|
||||
return true;
|
||||
}
|
||||
|
||||
function navigateTo(pageId: string): bool {
|
||||
// Rows report how they were activated; buttons and cards fall back to the focused control's reason
|
||||
function navigateTo(pageId, keyboard = contentFocusScope.keyboardDriven()): bool {
|
||||
const resolved = SettingsTabs.resolvePage(pageId);
|
||||
if (!resolved || resolved === currentPage)
|
||||
return false;
|
||||
content.rememberFocus();
|
||||
pageHistory = pageHistory.concat([currentPage]);
|
||||
currentPage = resolved;
|
||||
focusCurrentPage(keyboard);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -95,17 +101,19 @@ DankFloatingWindow {
|
||||
return setPage(name);
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
function goBack(keyboard = contentFocusScope.keyboardDriven()) {
|
||||
if (pageHistory.length > 0) {
|
||||
const history = pageHistory.slice();
|
||||
const target = history.pop();
|
||||
pageHistory = history;
|
||||
currentPage = target;
|
||||
focusCurrentPage(keyboard);
|
||||
return;
|
||||
}
|
||||
if (!currentParentId)
|
||||
return;
|
||||
currentPage = currentParentId;
|
||||
focusCurrentPage(keyboard);
|
||||
}
|
||||
|
||||
function setTabIndex(tabIndex: int) {
|
||||
@@ -140,20 +148,22 @@ DankFloatingWindow {
|
||||
show();
|
||||
}
|
||||
|
||||
function focusCurrentPage() {
|
||||
content._focusPage();
|
||||
function focusCurrentPage(keyboard = true) {
|
||||
contentFocusScope.focusContent(keyboard);
|
||||
}
|
||||
|
||||
function focusSidebar() {
|
||||
contentFocusScope.focusSidebar();
|
||||
}
|
||||
|
||||
function focusSearch() {
|
||||
if (isCompactMode)
|
||||
menuVisible = true;
|
||||
Qt.callLater(sidebar.focusSearch);
|
||||
contentFocusScope.focusSearch();
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
menuVisible = !menuVisible;
|
||||
if (menuVisible)
|
||||
Qt.callLater(sidebar.focusSearch);
|
||||
focusSearch();
|
||||
}
|
||||
|
||||
objectName: "settingsModal"
|
||||
@@ -184,9 +194,11 @@ DankFloatingWindow {
|
||||
if (!visible) {
|
||||
pageHistory = [];
|
||||
closingModal();
|
||||
} else if (!isCompactMode || menuVisible) {
|
||||
} else {
|
||||
const revision = contentFocusScope.focusRevision;
|
||||
Qt.callLater(() => {
|
||||
sidebar.focusSearch();
|
||||
if (settingsModal.visible && revision === contentFocusScope.focusRevision)
|
||||
settingsModal.focusSearch();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -257,22 +269,19 @@ DankFloatingWindow {
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
SettingsPaneNavigation {
|
||||
id: contentFocusScope
|
||||
|
||||
sidebar: sidebar
|
||||
content: content
|
||||
parentModal: settingsModal
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key !== Qt.Key_F || !(event.modifiers & Qt.ControlModifier))
|
||||
return;
|
||||
settingsModal.focusSearch();
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Keys.onBackPressed: event => {
|
||||
settingsModal.goBack();
|
||||
event.accepted = true;
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import QtQuick
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
required property Item sidebar
|
||||
required property Item content
|
||||
required property var parentModal
|
||||
property string activePane: "sidebar"
|
||||
property Item sidebarFocus: null
|
||||
readonly property Item focusedItem: Window.activeFocusItem
|
||||
property int focusRevision: 0
|
||||
|
||||
focus: true
|
||||
|
||||
// Controls take focus on press with the mouse reason, so visualFocus tells how the activating control was reached
|
||||
function keyboardDriven() {
|
||||
return focusedItem?.visualFocus ?? false;
|
||||
}
|
||||
|
||||
function contains(ancestor, item) {
|
||||
for (let p = item; p; p = p.parent) {
|
||||
if (p === ancestor)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function transientOwnsFocus() {
|
||||
for (let p = focusedItem; p && p !== root; p = p.parent) {
|
||||
if (p.capturing === true || p.Accessible.role === Accessible.Dialog || p.Accessible.role === Accessible.PopupMenu)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
onFocusedItemChanged: {
|
||||
if (transientOwnsFocus())
|
||||
return;
|
||||
if (contains(sidebar, focusedItem)) {
|
||||
if (focusedItem !== sidebar)
|
||||
sidebarFocus = focusedItem;
|
||||
activePane = "sidebar";
|
||||
} else if (contains(content, focusedItem)) {
|
||||
activePane = "content";
|
||||
content.rememberFocus();
|
||||
content._reveal(focusedItem);
|
||||
}
|
||||
}
|
||||
|
||||
function focusSidebar() {
|
||||
focusRevision++;
|
||||
activePane = "sidebar";
|
||||
if (parentModal.isCompactMode)
|
||||
parentModal.menuVisible = true;
|
||||
if (sidebarFocus?.visible && sidebarFocus.enabled && contains(sidebar, sidebarFocus)) {
|
||||
sidebarFocus.forceActiveFocus(Qt.TabFocusReason);
|
||||
sidebar.ensureRowVisible(sidebarFocus);
|
||||
} else {
|
||||
sidebar.focusNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
function focusContent(keyboard = true) {
|
||||
focusRevision++;
|
||||
activePane = "content";
|
||||
if (parentModal.isCompactMode)
|
||||
parentModal.menuVisible = false;
|
||||
content.keyboardNavigation = keyboard;
|
||||
content._focusPage();
|
||||
}
|
||||
|
||||
function focusSearch() {
|
||||
focusRevision++;
|
||||
activePane = "sidebar";
|
||||
if (parentModal.isCompactMode)
|
||||
parentModal.menuVisible = true;
|
||||
sidebar.focusSearch();
|
||||
}
|
||||
|
||||
function pointerFocus(pane) {
|
||||
const revision = ++focusRevision;
|
||||
activePane = pane;
|
||||
// Child controls get the press first. A background click only moves the keyboard region, highlighting nothing.
|
||||
Qt.callLater(() => {
|
||||
if (revision !== focusRevision || transientOwnsFocus())
|
||||
return;
|
||||
const region = pane === "sidebar" ? sidebar : content;
|
||||
if (contains(region, focusedItem))
|
||||
return;
|
||||
if (pane === "sidebar")
|
||||
sidebar.forceActiveFocus(Qt.MouseFocusReason);
|
||||
else
|
||||
content.parkFocus();
|
||||
});
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
parent: root.sidebar
|
||||
onPressedChanged: {
|
||||
if (pressed)
|
||||
root.pointerFocus("sidebar");
|
||||
}
|
||||
}
|
||||
|
||||
// Page scrollers accept presses before this handler sees them; SettingsContent gives each one its own
|
||||
TapHandler {
|
||||
parent: root.content
|
||||
onPressedChanged: {
|
||||
if (pressed)
|
||||
root.pointerFocus("content");
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.BackButton
|
||||
onTapped: {
|
||||
if (!transientOwnsFocus())
|
||||
root.content.goBack(false);
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
const mods = event.modifiers & ~Qt.KeypadModifier;
|
||||
if (transientOwnsFocus()) {
|
||||
if ((event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab || event.key === Qt.Key_F) && (mods & Qt.ControlModifier))
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if ((event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab) && (mods === Qt.ControlModifier || mods === (Qt.ControlModifier | Qt.ShiftModifier))) {
|
||||
if (activePane === "sidebar")
|
||||
focusContent();
|
||||
else
|
||||
focusSidebar();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_F && mods === Qt.ControlModifier) {
|
||||
focusSearch();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,12 +46,62 @@ Item {
|
||||
searchField.forceActiveFocus();
|
||||
}
|
||||
|
||||
function focusAfterNavigation() {
|
||||
if (parentModal?.isCompactMode && !parentModal.menuVisible) {
|
||||
parentModal.focusCurrentPage();
|
||||
function focusAfterNavigation(keyboard) {
|
||||
if (keyboard || (parentModal?.isCompactMode && !parentModal.menuVisible)) {
|
||||
parentModal?.focusCurrentPage(keyboard);
|
||||
return;
|
||||
}
|
||||
focusSearch();
|
||||
parentModal?.focusSidebar();
|
||||
}
|
||||
|
||||
function focusNavigation() {
|
||||
function findRow(item) {
|
||||
if (item.modelData?.id === root.activeCategoryId && item.visible && item.enabled)
|
||||
return item;
|
||||
for (const child of item.children ?? []) {
|
||||
const row = findRow(child);
|
||||
if (row)
|
||||
return row;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const row = findRow(sidebarColumn);
|
||||
if (row && !searchActive) {
|
||||
row.forceActiveFocus(Qt.TabFocusReason);
|
||||
ensureRowVisible(row);
|
||||
} else {
|
||||
focusSearch();
|
||||
}
|
||||
}
|
||||
|
||||
function moveRowFocus(forward) {
|
||||
const start = Window.activeFocusItem;
|
||||
if (start === root) {
|
||||
parentModal?.focusSidebar();
|
||||
return true;
|
||||
}
|
||||
let item = start;
|
||||
do {
|
||||
item = item?.nextItemInFocusChain(forward);
|
||||
if (!item || item === start)
|
||||
return false;
|
||||
let ancestor = item;
|
||||
while (ancestor && ancestor !== root)
|
||||
ancestor = ancestor.parent;
|
||||
if (!ancestor)
|
||||
return false;
|
||||
} while (!item.visible || !item.enabled);
|
||||
keyboardHighlightId = "";
|
||||
item.forceActiveFocus(forward ? Qt.TabFocusReason : Qt.BacktabFocusReason);
|
||||
ensureRowVisible(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if ((event.modifiers & ~Qt.KeypadModifier) !== Qt.NoModifier)
|
||||
return;
|
||||
if (event.key === Qt.Key_Up || event.key === Qt.Key_Down)
|
||||
event.accepted = moveRowFocus(event.key === Qt.Key_Down);
|
||||
}
|
||||
|
||||
function navigableIds() {
|
||||
@@ -79,7 +129,7 @@ Item {
|
||||
return;
|
||||
pageRequested(keyboardHighlightId);
|
||||
keyboardHighlightId = "";
|
||||
Qt.callLater(root.focusAfterNavigation);
|
||||
root.focusAfterNavigation(true);
|
||||
}
|
||||
|
||||
function ensureRowVisible(item) {
|
||||
@@ -120,13 +170,13 @@ Item {
|
||||
parentModal?.navigateTo("bar_widget");
|
||||
}
|
||||
|
||||
function selectSearchResult(result) {
|
||||
function selectSearchResult(result, keyboard = true) {
|
||||
if (!result)
|
||||
return;
|
||||
if (result.runtimeType === "barWidget" || result.runtimeType === "barWidgetAdd") {
|
||||
openBarWidget(result.runtimeId, result.runtimeType === "barWidgetAdd");
|
||||
keyboardHighlightId = "";
|
||||
Qt.callLater(root.focusAfterNavigation);
|
||||
root.focusAfterNavigation(keyboard);
|
||||
return;
|
||||
}
|
||||
if (result.section)
|
||||
@@ -135,7 +185,7 @@ Item {
|
||||
if (page)
|
||||
pageRequested(page);
|
||||
keyboardHighlightId = "";
|
||||
Qt.callLater(root.focusAfterNavigation);
|
||||
root.focusAfterNavigation(keyboard);
|
||||
}
|
||||
|
||||
function navigateSearchResults(delta) {
|
||||
@@ -200,7 +250,11 @@ Item {
|
||||
anchors.topMargin: SettingsMetrics.searchBarGap
|
||||
height: SettingsMetrics.searchBarHeight
|
||||
placeholderText: I18n.tr("Search settings", "settings search field placeholder")
|
||||
onFocusStateChanged: hasFocus => root.searchFocused = hasFocus
|
||||
onFocusStateChanged: hasFocus => {
|
||||
root.searchFocused = hasFocus;
|
||||
if (!hasFocus)
|
||||
root.keyboardHighlightId = "";
|
||||
}
|
||||
onTextChanged: {
|
||||
SettingsSearchService.search(text);
|
||||
root.searchSelectedIndex = 0;
|
||||
@@ -230,6 +284,10 @@ Item {
|
||||
root.selectSearchResult(SettingsSearchService.results[root.searchSelectedIndex]);
|
||||
return;
|
||||
}
|
||||
if (!root.searchActive && root.keyboardHighlightId === "") {
|
||||
root.parentModal?.focusCurrentPage();
|
||||
return;
|
||||
}
|
||||
root.selectHighlighted();
|
||||
}
|
||||
Keys.onDownPressed: event => {
|
||||
@@ -240,16 +298,19 @@ Item {
|
||||
navPrev();
|
||||
event.accepted = true;
|
||||
}
|
||||
// Specific key handlers accept by default; with nothing to cycle, Tab must reach the focus chain
|
||||
Keys.onTabPressed: event => {
|
||||
if (!root.searchActive && root.keyboardHighlightId === "")
|
||||
return;
|
||||
navNext();
|
||||
event.accepted = true;
|
||||
event.accepted = !(event.modifiers & Qt.ControlModifier) && (root.searchActive || root.keyboardHighlightId !== "");
|
||||
if (event.accepted)
|
||||
navNext();
|
||||
}
|
||||
Keys.onBacktabPressed: event => {
|
||||
if (!root.searchActive && root.keyboardHighlightId === "")
|
||||
return;
|
||||
navPrev();
|
||||
event.accepted = !(event.modifiers & Qt.ControlModifier) && (root.searchActive || root.keyboardHighlightId !== "");
|
||||
if (event.accepted)
|
||||
navPrev();
|
||||
}
|
||||
Keys.onEnterPressed: event => {
|
||||
navSelect();
|
||||
event.accepted = true;
|
||||
}
|
||||
Keys.onReturnPressed: event => {
|
||||
@@ -287,11 +348,17 @@ Item {
|
||||
spacing: SettingsMetrics.sidebarGroupGap
|
||||
|
||||
ProfileSection {
|
||||
id: profileRow
|
||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||
visible: !root.searchActive
|
||||
onClicked: {
|
||||
highlighted: activeFocus
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus)
|
||||
root.ensureRowVisible(profileRow);
|
||||
}
|
||||
onNavigationRequested: keyboard => {
|
||||
root.pageRequested("user_accounts");
|
||||
Qt.callLater(root.focusAfterNavigation);
|
||||
root.focusAfterNavigation(keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +379,10 @@ Item {
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus)
|
||||
root.ensureRowVisible(resultDelegate);
|
||||
}
|
||||
isFirstInGroup: index === 0
|
||||
isLastInGroup: index === SettingsSearchService.results.length - 1
|
||||
iconName: modelData.icon || "settings"
|
||||
@@ -319,7 +390,7 @@ Item {
|
||||
hint: modelData.category
|
||||
accent: SettingsTabs.accentFor(modelData.page || SettingsTabs.pageForTabIndex(modelData.tabIndex))
|
||||
active: root.searchSelectedIndex === index
|
||||
onClicked: root.selectSearchResult(modelData)
|
||||
onClicked: keyboard => root.selectSearchResult(modelData, keyboard)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +436,10 @@ Item {
|
||||
id: categoryRow
|
||||
required property var modelData
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus)
|
||||
root.ensureRowVisible(categoryRow);
|
||||
}
|
||||
readonly property bool isHighlighted: root.keyboardHighlightId === modelData.id
|
||||
onIsHighlightedChanged: {
|
||||
if (isHighlighted)
|
||||
@@ -381,10 +456,10 @@ Item {
|
||||
accent: SettingsTabs.accentFor(modelData.id)
|
||||
active: root.activeCategoryId === modelData.id && !SettingsTabs.isPluginPage(root.currentPage)
|
||||
highlighted: isHighlighted
|
||||
onClicked: {
|
||||
onClicked: keyboard => {
|
||||
root.keyboardHighlightId = "";
|
||||
root.pageRequested(modelData.id);
|
||||
Qt.callLater(root.focusAfterNavigation);
|
||||
root.focusAfterNavigation(keyboard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,34 +139,13 @@ CcListRow {
|
||||
Repeater {
|
||||
model: root.configFields
|
||||
|
||||
Rectangle {
|
||||
DankDetailChip {
|
||||
required property var modelData
|
||||
|
||||
width: fieldContent.width + Theme.spacingM * 2
|
||||
height: Theme.buttonHeightXS
|
||||
radius: Theme.cornerRadiusS
|
||||
label: modelData.label
|
||||
value: modelData.value
|
||||
color: Theme.chipSurface
|
||||
|
||||
Row {
|
||||
id: fieldContent
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: modelData.label + ":"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.value
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
border.width: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ Column {
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
downKeyOpens: false
|
||||
width: parent.width
|
||||
text: root.label
|
||||
description: root.description
|
||||
|
||||
@@ -58,6 +58,7 @@ Column {
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
upDownKeysStep: false
|
||||
width: parent.width
|
||||
value: root.value
|
||||
minimum: root.minimum
|
||||
|
||||
@@ -16,11 +16,8 @@ Item {
|
||||
property var outputDevices: []
|
||||
property var inputDevices: []
|
||||
property var parentModal: null
|
||||
property bool renameOpen: false
|
||||
property bool renameMounted: false
|
||||
property var editingDevice: null
|
||||
property string editingDeviceType: ""
|
||||
property string newDeviceName: ""
|
||||
property bool isReloadingAudio: false
|
||||
property var hiddenOutputDeviceNames: SessionData.hiddenOutputDeviceNames ?? []
|
||||
property var hiddenInputDeviceNames: SessionData.hiddenInputDeviceNames ?? []
|
||||
@@ -28,30 +25,16 @@ Item {
|
||||
property bool showHiddenInputDevices: false
|
||||
|
||||
function openRenameDialog(device, type) {
|
||||
if (renameOpen)
|
||||
return;
|
||||
editingDevice = device;
|
||||
editingDeviceType = type;
|
||||
newDeviceName = AudioService.displayName(device);
|
||||
renameOpen = true;
|
||||
renameMounted = true;
|
||||
if (renameLoader.item)
|
||||
renameLoader.item.show();
|
||||
renameDialog.show(AudioService.displayName(device));
|
||||
}
|
||||
|
||||
function closeRenameDialog() {
|
||||
if (!renameOpen)
|
||||
function saveDeviceName(name) {
|
||||
if (!editingDevice)
|
||||
return;
|
||||
renameOpen = false;
|
||||
if (renameLoader.item)
|
||||
renameLoader.item.opened = false;
|
||||
}
|
||||
|
||||
function saveDeviceName() {
|
||||
if (!editingDevice || newDeviceName.trim() === "")
|
||||
return;
|
||||
AudioService.setDeviceAlias(editingDevice.name, newDeviceName);
|
||||
closeRenameDialog();
|
||||
AudioService.setDeviceAlias(editingDevice.name, name);
|
||||
renameDialog.hide();
|
||||
}
|
||||
|
||||
function persistHiddenOutputDeviceNames(deviceNames) {
|
||||
@@ -216,9 +199,7 @@ Item {
|
||||
deviceType: "output"
|
||||
showHideButton: true
|
||||
|
||||
onEditRequested: device => {
|
||||
root.openRenameDialog(device, "output");
|
||||
}
|
||||
onEditRequested: device => root.openRenameDialog(device, "output")
|
||||
|
||||
onHideRequested: device => {
|
||||
root.persistHiddenOutputDeviceNames([...root.hiddenOutputDeviceNames, device.name]);
|
||||
@@ -242,6 +223,7 @@ Item {
|
||||
|
||||
DankSlider {
|
||||
id: maxVolSlider
|
||||
upDownKeysStep: false
|
||||
anchors.left: maxVolLabel.right
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
@@ -381,9 +363,7 @@ Item {
|
||||
deviceType: "input"
|
||||
showHideButton: true
|
||||
|
||||
onEditRequested: device => {
|
||||
root.openRenameDialog(device, "input");
|
||||
}
|
||||
onEditRequested: device => root.openRenameDialog(device, "input")
|
||||
|
||||
onHideRequested: device => {
|
||||
root.persistHiddenInputDeviceNames([...root.hiddenInputDeviceNames, device.name]);
|
||||
@@ -513,82 +493,32 @@ Item {
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: renameLoader
|
||||
SettingsRenameDialog {
|
||||
id: renameDialog
|
||||
parent: root.parentModal?.modalFocusScope ?? root
|
||||
anchors.fill: parent
|
||||
z: 1000
|
||||
active: root.renameMounted
|
||||
onLoaded: item.show()
|
||||
title: I18n.tr("Set custom device name")
|
||||
supportingText: root.editingDevice?.name ?? ""
|
||||
labelText: I18n.tr("Custom name")
|
||||
leftIconName: root.editingDeviceType === "input" ? "mic" : "speaker"
|
||||
onAccepted: name => root.saveDeviceName(name)
|
||||
|
||||
sourceComponent: DankDialog {
|
||||
id: renameDialog
|
||||
aboveField: StyledText {
|
||||
visible: AudioService.hasDeviceAlias(root.editingDevice?.name ?? "")
|
||||
text: I18n.tr("Original: %1", "Shows the original device name before renaming").arg(AudioService.originalName(root.editingDevice))
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
|
||||
embedded: nativeWindow
|
||||
opened: nativeWindow
|
||||
maximumWidth: SettingsMetrics.formDialogWidth
|
||||
surfaceColor: Theme.hostSurface
|
||||
title: I18n.tr("Set custom device name")
|
||||
supportingText: root.editingDevice?.name ?? ""
|
||||
acceptEnabled: root.newDeviceName.trim() !== ""
|
||||
onAccepted: root.saveDeviceName()
|
||||
onRejected: root.closeRenameDialog()
|
||||
onActiveChanged: {
|
||||
if (!active && !root.renameOpen)
|
||||
root.renameMounted = false;
|
||||
}
|
||||
|
||||
function show() {
|
||||
nameInput.text = root.newDeviceName;
|
||||
opened = true;
|
||||
forceActiveFocus();
|
||||
nameInput.forceActiveFocus();
|
||||
nameInput.selectAll();
|
||||
}
|
||||
|
||||
actions: [
|
||||
DankButton {
|
||||
text: I18n.tr("Cancel")
|
||||
backgroundColor: "transparent"
|
||||
textColor: Theme.primary
|
||||
onClicked: renameDialog.rejected()
|
||||
},
|
||||
DankButton {
|
||||
text: I18n.tr("Save")
|
||||
iconName: "check"
|
||||
enabled: renameDialog.acceptEnabled
|
||||
onClicked: root.saveDeviceName()
|
||||
}
|
||||
]
|
||||
|
||||
StyledText {
|
||||
visible: AudioService.hasDeviceAlias(root.editingDevice?.name ?? "")
|
||||
text: I18n.tr("Original: %1", "Shows the original device name before renaming").arg(AudioService.originalName(root.editingDevice))
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: nameInput
|
||||
outlined: true
|
||||
leftIconName: root.editingDeviceType === "input" ? "mic" : "speaker"
|
||||
labelText: I18n.tr("Custom name")
|
||||
width: parent.width
|
||||
showClearButton: true
|
||||
onTextChanged: root.newDeviceName = text
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Press Enter and the audio system will restart to apply the change", "Audio device rename dialog hint")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
belowField: StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Press Enter and the audio system will restart to apply the change", "Audio device rename dialog hint")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -392,110 +392,36 @@ Item {
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
id: appPickerRow
|
||||
|
||||
readonly property var selectedApp: root.desktopApps.find(a => (a.id || a.execString) === root.newEntryDesktopId) ?? null
|
||||
|
||||
visible: root.newEntryType === "desktop"
|
||||
body: Column {
|
||||
title: I18n.tr("App", "noun, application picker label in autostart add entry")
|
||||
subtitle: root.newEntryDesktopId ? (selectedApp?.name || selectedApp?.id || root.newEntryDesktopId) : I18n.tr("No application selected")
|
||||
clickable: true
|
||||
showChevron: true
|
||||
onClicked: appBrowserPopup.show()
|
||||
leading: AppIconRenderer {
|
||||
width: Theme.iconSize
|
||||
height: Theme.iconSize
|
||||
iconValue: appPickerRow.selectedApp?.icon || "application-x-executable"
|
||||
iconSize: Theme.iconSize
|
||||
fallbackText: (appPickerRow.selectedApp?.name || "?").charAt(0).toUpperCase()
|
||||
}
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
visible: root.newEntryType === "desktop"
|
||||
body: DankTextField {
|
||||
outlined: true
|
||||
leftIconName: "terminal"
|
||||
labelText: I18n.tr("Command")
|
||||
supportingText: I18n.tr("Wrap the app command. %command% is replaced with the actual executable", "autostart command field hint, keep %command% verbatim")
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: appLabelColumn.height
|
||||
|
||||
Column {
|
||||
id: appLabelColumn
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("App", "noun, application picker label in autostart add entry")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledRect {
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: root.newEntryDesktopId ? Theme.floatingWindowFieldColor : Theme.withAlpha(Theme.chipSurface, Theme.floatingWindowForegroundLayers ? Theme.floatingWindowForegroundTransparency * 0.5 : 0)
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
readonly property string selectedName: {
|
||||
if (!root.newEntryDesktopId)
|
||||
return "";
|
||||
const app = root.desktopApps.find(a => (a.id || a.execString) === root.newEntryDesktopId);
|
||||
return app ? (app.name || app.id || "") : root.newEntryDesktopId;
|
||||
}
|
||||
|
||||
width: parent.width - browseButton.width - Theme.spacingM
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
visible: root.newEntryDesktopId !== ""
|
||||
|
||||
Image {
|
||||
width: 24
|
||||
height: 24
|
||||
source: {
|
||||
const app = root.desktopApps.find(a => (a.id || a.execString) === root.newEntryDesktopId);
|
||||
return Paths.resolveIconUrl(app?.icon || "application-x-executable");
|
||||
}
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
fillMode: Image.PreserveAspectFit
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error)
|
||||
source = "image://icon/application-x-executable";
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: parent.parent.selectedName
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("No application selected")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: root.newEntryDesktopId === ""
|
||||
}
|
||||
}
|
||||
|
||||
DankButton {
|
||||
id: browseButton
|
||||
text: I18n.tr("Browse")
|
||||
iconName: "search"
|
||||
onClicked: appBrowserPopup.show()
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
outlined: true
|
||||
leftIconName: "terminal"
|
||||
labelText: I18n.tr("Command")
|
||||
supportingText: I18n.tr("Wrap the app command. %command% is replaced with the actual executable", "autostart command field hint, keep %command% verbatim")
|
||||
width: parent.width
|
||||
placeholderText: "%command%"
|
||||
text: root.newEntryCommandWrapper
|
||||
onTextChanged: root.newEntryCommandWrapper = text
|
||||
}
|
||||
placeholderText: "%command%"
|
||||
text: root.newEntryCommandWrapper
|
||||
onTextChanged: root.newEntryCommandWrapper = text
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,117 +516,46 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
body: Column {
|
||||
id: entriesList
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
Repeater {
|
||||
model: root.entries
|
||||
|
||||
Repeater {
|
||||
model: root.entries
|
||||
delegate: SettingsRow {
|
||||
id: entryRow
|
||||
|
||||
delegate: Rectangle {
|
||||
width: entriesList.width
|
||||
height: 48
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.floatingWindowFieldColor
|
||||
border.width: 0
|
||||
required property var modelData
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.right: entryToggle.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: (index + 1).toString()
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.primary
|
||||
width: 20
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Image {
|
||||
width: 24
|
||||
height: 24
|
||||
source: Paths.resolveIconUrl(modelData.icon || "application-x-executable")
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
fillMode: Image.PreserveAspectFit
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error)
|
||||
source = "image://icon/application-x-executable";
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - 20 - 24 - Theme.spacingM * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: modelData.hidden ? Theme.surfaceVariantText : Theme.surfaceText
|
||||
maximumLineCount: 1
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
opacity: modelData.hidden ? 0.6 : 1.0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: modelData.hidden ? I18n.tr("Disabled") : modelData.exec
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
maximumLineCount: 1
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: entryToggle
|
||||
anchors.right: entryRemoveButton.left
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: !modelData.hidden
|
||||
onToggled: checked => root.setHidden(modelData, !checked)
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: entryRemoveButton
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "close"
|
||||
Accessible.name: I18n.tr("Remove")
|
||||
iconSize: 16
|
||||
buttonSize: 32
|
||||
circular: true
|
||||
iconColor: Theme.error
|
||||
onClicked: root.removeEntry(modelData.filePath)
|
||||
}
|
||||
}
|
||||
title: modelData.name
|
||||
titleColor: modelData.hidden ? Theme.surfaceVariantText : Theme.surfaceText
|
||||
subtitle: modelData.hidden ? I18n.tr("Disabled") : modelData.exec
|
||||
singleLineTitle: true
|
||||
leading: AppIconRenderer {
|
||||
width: Theme.iconSize
|
||||
height: Theme.iconSize
|
||||
iconValue: entryRow.modelData.icon || "application-x-executable"
|
||||
iconSize: Theme.iconSize
|
||||
fallbackText: (entryRow.modelData.name || "?").charAt(0).toUpperCase()
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("No autostart entries")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: root.entries.length === 0
|
||||
DankToggle {
|
||||
hideText: true
|
||||
text: entryRow.title
|
||||
checked: !entryRow.modelData.hidden
|
||||
onToggled: checked => root.setHidden(entryRow.modelData, !checked)
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "delete"
|
||||
iconColor: Theme.error
|
||||
tooltipText: I18n.tr("Remove")
|
||||
onClicked: root.removeEntry(entryRow.modelData.filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
visible: root.entries.length === 0
|
||||
subtitle: I18n.tr("No autostart entries")
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
|
||||
@@ -10,7 +10,6 @@ Column {
|
||||
id: root
|
||||
|
||||
property var parentModal: null
|
||||
property string confirmingRemoveId: ""
|
||||
property string editingBarId: ""
|
||||
property string renameDraft: ""
|
||||
readonly property bool dotEnabled: SettingsData.dotBarConfig?.enabled ?? false
|
||||
@@ -19,10 +18,7 @@ Column {
|
||||
BarSelectionState {
|
||||
id: bar
|
||||
|
||||
onSelectedBarIdChanged: {
|
||||
root.editingBarId = "";
|
||||
root.confirmingRemoveId = "";
|
||||
}
|
||||
onSelectedBarIdChanged: root.editingBarId = ""
|
||||
}
|
||||
|
||||
function finishRename(value) {
|
||||
@@ -65,11 +61,6 @@ Column {
|
||||
}
|
||||
|
||||
function deleteBar(barId) {
|
||||
if (confirmingRemoveId !== barId) {
|
||||
confirmingRemoveId = barId;
|
||||
return;
|
||||
}
|
||||
confirmingRemoveId = "";
|
||||
SettingsData.deleteBarConfig(barId);
|
||||
bar.select("default");
|
||||
}
|
||||
@@ -128,7 +119,6 @@ Column {
|
||||
checked: modelData.enabled ?? false
|
||||
toggleVisible: root.canToggleBar(modelData)
|
||||
deletable: root.canDeleteBar(modelData)
|
||||
confirmingDelete: root.confirmingRemoveId === modelData.id
|
||||
onClicked: bar.select(modelData.id)
|
||||
onToggled: checked => {
|
||||
bar.select(modelData.id);
|
||||
@@ -205,46 +195,21 @@ Column {
|
||||
iconName: modelData.icon
|
||||
title: modelData.text
|
||||
hint: modelData.hint ?? ""
|
||||
onClicked: root.parentModal?.navigateTo(modelData.id)
|
||||
onClicked: keyboard => root.parentModal?.navigateTo(modelData.id, keyboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
id: dotRow
|
||||
|
||||
SettingsSplitRow {
|
||||
settingKey: "dotEnabled"
|
||||
tags: ["dot", "dankdot", "companion", "floating", "island", "enable"]
|
||||
iconName: "blur_on"
|
||||
iconColor: root.dotEnabled ? Theme.primary : Theme.onSurfaceVariant
|
||||
title: I18n.tr("Dot", "bar layout: free-floating dot that opens island activities")
|
||||
subtitle: I18n.tr("A floating companion that works alongside any bar layout", "bar settings: what the dot is")
|
||||
clickable: root.dotEnabled
|
||||
onClicked: root.parentModal?.navigateTo("dankbar_dot")
|
||||
|
||||
DankIcon {
|
||||
name: "chevron_right"
|
||||
size: Theme.iconSize
|
||||
color: Theme.onSurfaceVariant
|
||||
rotation: I18n.isRtl ? 180 : 0
|
||||
visible: root.dotEnabled
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Theme.dividerWidth
|
||||
height: SettingsMetrics.splitDividerHeight
|
||||
color: Theme.outlineVariant
|
||||
visible: root.dotEnabled
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
hideText: true
|
||||
text: dotRow.title
|
||||
checked: root.dotEnabled
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onToggled: value => SettingsData.setDotEnabled(value, bar.selectedBarId)
|
||||
}
|
||||
navigable: root.dotEnabled
|
||||
checked: root.dotEnabled
|
||||
onNavigated: keyboard => root.parentModal?.navigateTo("dankbar_dot", keyboard)
|
||||
onToggled: value => SettingsData.setDotEnabled(value, bar.selectedBarId)
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ Column {
|
||||
iconName: "schedule"
|
||||
title: I18n.tr("Time & weather")
|
||||
hint: I18n.tr("Clock format, calendar, weather location")
|
||||
onClicked: root.page.parentModal?.navigateTo("time_weather")
|
||||
onClicked: keyboard => root.page.parentModal?.navigateTo("time_weather", keyboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ Column {
|
||||
iconName: "music_note"
|
||||
title: I18n.tr("Media player")
|
||||
hint: I18n.tr("Visualizer, album art, excluded players")
|
||||
onClicked: root.page.parentModal?.navigateTo("media_player")
|
||||
onClicked: keyboard => root.page.parentModal?.navigateTo("media_player", keyboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ Column {
|
||||
iconName: "system_update_alt"
|
||||
title: I18n.tr("Software updates")
|
||||
hint: I18n.tr("DMS and system updates")
|
||||
onClicked: root.page.parentModal?.navigateTo("updater")
|
||||
onClicked: keyboard => root.page.parentModal?.navigateTo("updater", keyboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ Item {
|
||||
SettingsNavRow {
|
||||
title: I18n.tr("Weather")
|
||||
iconName: "partly_cloudy_day"
|
||||
onClicked: root.parentModal?.navigateTo("weather")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("weather", keyboard)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,17 @@ Column {
|
||||
height: Theme.listItemHeight
|
||||
radius: Theme.cornerRadiusM
|
||||
color: Theme.chipSurface
|
||||
activeFocusOnTab: root.currentMode === "custom"
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: I18n.tr("Custom color")
|
||||
Accessible.onPressAction: root.openCustomColorPicker()
|
||||
Keys.onSpacePressed: root.openCustomColorPicker()
|
||||
Keys.onReturnPressed: root.openCustomColorPicker()
|
||||
Keys.onEnterPressed: root.openCustomColorPicker()
|
||||
|
||||
FocusRing {
|
||||
id: chipRing
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
@@ -187,7 +198,12 @@ Column {
|
||||
StateLayer {
|
||||
stateColor: Theme.surfaceText
|
||||
cornerRadius: customChip.radius
|
||||
onClicked: root.openCustomColorPicker()
|
||||
focused: chipRing.visible
|
||||
onClicked: {
|
||||
chipRing.pointerFocused = true;
|
||||
customChip.forceActiveFocus(Qt.MouseFocusReason);
|
||||
root.openCustomColorPicker();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ FocusScope {
|
||||
settingKey: "dashWeatherSettings"
|
||||
title: I18n.tr("Weather")
|
||||
iconName: "partly_cloudy_day"
|
||||
onClicked: root.parentModal?.navigateTo("weather")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("weather", keyboard)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ FocusScope {
|
||||
title: I18n.tr("Media player")
|
||||
hint: I18n.tr("Lyrics providers", "Lyrics source priority settings")
|
||||
iconName: "music_note"
|
||||
onClicked: root.parentModal?.navigateTo("media_player")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("media_player", keyboard)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -11,7 +10,6 @@ SettingsReorderRow {
|
||||
id: root
|
||||
|
||||
required property var instanceData
|
||||
property bool confirmingDelete: false
|
||||
|
||||
readonly property string instanceId: instanceData?.id ?? ""
|
||||
readonly property string widgetType: instanceData?.widgetType ?? ""
|
||||
@@ -24,6 +22,8 @@ SettingsReorderRow {
|
||||
|
||||
iconName: widgetDef?.icon ?? "widgets"
|
||||
title: widgetName
|
||||
subtitle: deleteButton.confirming ? I18n.tr("Confirm Delete") : ""
|
||||
subtitleColor: Theme.error
|
||||
clickable: true
|
||||
onClicked: configureRequested()
|
||||
|
||||
@@ -35,16 +35,14 @@ SettingsReorderRow {
|
||||
color: Theme.onSurfaceVariant
|
||||
rotation: I18n.isRtl ? 180 : 0
|
||||
},
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Theme.dividerWidth
|
||||
height: SettingsMetrics.splitDividerHeight
|
||||
color: Theme.outlineVariant
|
||||
SettingsDivider {
|
||||
vertical: true
|
||||
},
|
||||
DankToggle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
hideText: true
|
||||
checked: instanceData?.enabled ?? true
|
||||
text: root.title
|
||||
checked: root.instanceData?.enabled ?? true
|
||||
onToggled: isChecked => {
|
||||
SettingsData.updateDesktopWidgetInstance(root.instanceId, {
|
||||
enabled: isChecked
|
||||
@@ -52,119 +50,15 @@ SettingsReorderRow {
|
||||
}
|
||||
},
|
||||
DankActionButton {
|
||||
id: menuButton
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "more_vert"
|
||||
Accessible.name: I18n.tr("Options")
|
||||
onClicked: {
|
||||
if (actionsMenu.visible) {
|
||||
actionsMenu.close();
|
||||
return;
|
||||
}
|
||||
actionsMenu.open();
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: actionsMenu
|
||||
x: -width + parent.width
|
||||
y: parent.height + Theme.spacingXS
|
||||
width: 160
|
||||
padding: Theme.spacingXS
|
||||
modal: false
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
||||
|
||||
onClosed: root.confirmingDelete = false
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.floatingWindowSurface
|
||||
radius: Theme.windowRadius
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: Theme.layerOutlineWidth
|
||||
}
|
||||
|
||||
contentItem: Column {
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: Theme.iconSizeLarge
|
||||
radius: Theme.cornerRadius
|
||||
color: duplicateArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "content_copy"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Duplicate", "verb, desktop widget menu action")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: duplicateArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
actionsMenu.close();
|
||||
root.duplicateRequested();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: Theme.iconSizeLarge
|
||||
radius: Theme.cornerRadius
|
||||
color: deleteArea.containsMouse ? Theme.errorHover : Theme.withAlpha(Theme.errorHover, 0)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: root.confirmingDelete ? "warning" : "delete"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.error
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.confirmingDelete ? I18n.tr("Confirm Delete") : I18n.tr("Delete")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deleteArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.confirmingDelete) {
|
||||
actionsMenu.close();
|
||||
root.deleteRequested();
|
||||
return;
|
||||
}
|
||||
root.confirmingDelete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
iconName: "content_copy"
|
||||
tooltipText: I18n.tr("Duplicate", "verb, desktop widget menu action")
|
||||
onClicked: root.duplicateRequested()
|
||||
},
|
||||
SettingsDeleteButton {
|
||||
id: deleteButton
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onDeleteRequested: root.deleteRequested()
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -15,8 +15,8 @@ Item {
|
||||
|
||||
property var groupCollapsedStates: ({})
|
||||
property var parentModal: null
|
||||
property string editingGroupId: ""
|
||||
property string newGroupName: ""
|
||||
property string renamingGroupId: ""
|
||||
|
||||
readonly property var allInstances: SettingsData.desktopWidgetInstances || []
|
||||
readonly property var allGroups: SettingsData.desktopWidgetGroups || []
|
||||
@@ -48,6 +48,18 @@ Item {
|
||||
parentModal?.navigateTo("desktop_widget");
|
||||
}
|
||||
|
||||
function openRenameDialog(group) {
|
||||
renamingGroupId = group.id;
|
||||
renameDialog.show(group.name);
|
||||
}
|
||||
|
||||
function saveGroupName(name) {
|
||||
SettingsData.updateDesktopWidgetGroup(renamingGroupId, {
|
||||
name: name
|
||||
});
|
||||
renameDialog.hide();
|
||||
}
|
||||
|
||||
function showWidgetBrowser() {
|
||||
widgetBrowserLoader.active = true;
|
||||
if (widgetBrowserLoader.item)
|
||||
@@ -95,140 +107,66 @@ Item {
|
||||
expanded: root.allGroups.length > 0
|
||||
|
||||
SettingsRow {
|
||||
body: Column {
|
||||
body: Row {
|
||||
spacing: Theme.spacingS
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
width: parent.width
|
||||
|
||||
DankTextField {
|
||||
id: newGroupField
|
||||
outlined: true
|
||||
leftIconName: "folder"
|
||||
labelText: I18n.tr("Name")
|
||||
width: parent.width - addGroupBtn.width - Theme.spacingS
|
||||
text: root.newGroupName
|
||||
onTextChanged: root.newGroupName = text
|
||||
onAccepted: {
|
||||
if (!text.trim())
|
||||
return;
|
||||
SettingsData.createDesktopWidgetGroup(text.trim());
|
||||
root.newGroupName = "";
|
||||
text = "";
|
||||
}
|
||||
}
|
||||
|
||||
DankButton {
|
||||
id: addGroupBtn
|
||||
iconName: "add"
|
||||
text: I18n.tr("Add")
|
||||
enabled: root.newGroupName.trim().length > 0
|
||||
onClicked: {
|
||||
SettingsData.createDesktopWidgetGroup(root.newGroupName.trim());
|
||||
root.newGroupName = "";
|
||||
newGroupField.text = "";
|
||||
}
|
||||
DankTextField {
|
||||
id: newGroupField
|
||||
outlined: true
|
||||
leftIconName: "folder"
|
||||
labelText: I18n.tr("Name")
|
||||
width: parent.width - addGroupBtn.width - Theme.spacingS
|
||||
text: root.newGroupName
|
||||
onTextChanged: root.newGroupName = text
|
||||
onAccepted: {
|
||||
if (!text.trim())
|
||||
return;
|
||||
SettingsData.createDesktopWidgetGroup(text.trim());
|
||||
root.newGroupName = "";
|
||||
text = "";
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
visible: root.allGroups.length > 0
|
||||
DankButton {
|
||||
id: addGroupBtn
|
||||
iconName: "add"
|
||||
text: I18n.tr("Add")
|
||||
enabled: root.newGroupName.trim().length > 0
|
||||
onClicked: {
|
||||
SettingsData.createDesktopWidgetGroup(root.newGroupName.trim());
|
||||
root.newGroupName = "";
|
||||
newGroupField.text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.allGroups
|
||||
Repeater {
|
||||
model: root.allGroups
|
||||
|
||||
Rectangle {
|
||||
id: groupItem
|
||||
required property var modelData
|
||||
required property int index
|
||||
delegate: SettingsRow {
|
||||
id: groupRow
|
||||
|
||||
width: parent.width
|
||||
height: Math.max(Theme.iconButtonSize, groupNameLoader.height + Theme.spacingS)
|
||||
radius: Theme.cornerRadius
|
||||
color: groupMouseArea.containsMouse ? Theme.surfaceHover : Theme.floatingWindowFieldColor
|
||||
required property var modelData
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
iconName: "folder"
|
||||
title: modelData.name
|
||||
singleLineTitle: true
|
||||
|
||||
DankIcon {
|
||||
name: "folder"
|
||||
visible: !groupNameLoader.active
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
DankActionButton {
|
||||
iconName: "edit"
|
||||
tooltipText: I18n.tr("Rename")
|
||||
onClicked: root.openRenameDialog(groupRow.modelData)
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: groupNameLoader
|
||||
active: root.editingGroupId === groupItem.modelData.id
|
||||
width: active ? parent.width - deleteGroupBtn.width - Theme.spacingS : 0
|
||||
height: active && item ? item.implicitHeight : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
sourceComponent: DankTextField {
|
||||
outlined: true
|
||||
leftIconName: "folder"
|
||||
labelText: I18n.tr("Name")
|
||||
text: groupItem.modelData.name
|
||||
onAccepted: {
|
||||
if (!text.trim())
|
||||
return;
|
||||
SettingsData.updateDesktopWidgetGroup(groupItem.modelData.id, {
|
||||
name: text.trim()
|
||||
});
|
||||
root.editingGroupId = "";
|
||||
}
|
||||
onEditingFinished: {
|
||||
if (!text.trim())
|
||||
return;
|
||||
SettingsData.updateDesktopWidgetGroup(groupItem.modelData.id, {
|
||||
name: text.trim()
|
||||
});
|
||||
root.editingGroupId = "";
|
||||
}
|
||||
Component.onCompleted: forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: root.editingGroupId !== groupItem.modelData.id
|
||||
text: groupItem.modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - Theme.iconSizeSmall - deleteGroupBtn.width - Theme.spacingS * 3
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: deleteGroupBtn
|
||||
iconName: "delete"
|
||||
Accessible.name: I18n.tr("Delete")
|
||||
backgroundColor: Theme.withAlpha(Theme.error, 0.15)
|
||||
iconColor: Theme.error
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: {
|
||||
SettingsData.removeDesktopWidgetGroup(groupItem.modelData.id);
|
||||
ToastService.showInfo(I18n.tr("Group removed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: groupMouseArea
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
hoverEnabled: true
|
||||
onDoubleClicked: root.editingGroupId = groupItem.modelData.id
|
||||
}
|
||||
}
|
||||
DankActionButton {
|
||||
iconName: "delete"
|
||||
iconColor: Theme.error
|
||||
tooltipText: I18n.tr("Delete")
|
||||
onClicked: {
|
||||
SettingsData.removeDesktopWidgetGroup(groupRow.modelData.id);
|
||||
ToastService.showInfo(I18n.tr("Group removed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,4 +285,12 @@ Item {
|
||||
SettingsReorderPreview {
|
||||
group: dragGroup
|
||||
}
|
||||
|
||||
SettingsRenameDialog {
|
||||
id: renameDialog
|
||||
parent: root.parentModal?.modalFocusScope ?? root
|
||||
supportingText: root.allGroups.find(g => g.id === root.renamingGroupId)?.name ?? ""
|
||||
leftIconName: "folder"
|
||||
onAccepted: name => root.saveGroupName(name)
|
||||
}
|
||||
}
|
||||
@@ -40,8 +40,9 @@ Variants {
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
duration: Theme.expressiveDurations.expressiveEffects
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -156,6 +156,7 @@ Column {
|
||||
|
||||
DankDropdown {
|
||||
id: scaleDropdown
|
||||
downKeyOpens: false
|
||||
visible: !scaleRow.customMode
|
||||
width: Math.min(dropdownWidth, scaleRow.width - SettingsMetrics.rowPaddingH * 2)
|
||||
Accessible.name: scaleRow.title
|
||||
|
||||
@@ -140,6 +140,7 @@ Item {
|
||||
|
||||
DankDropdown {
|
||||
id: profileDropdown
|
||||
downKeyOpens: false
|
||||
width: parent.width - newButton.width - editMonitorsButton.width - deleteButton.width - Theme.spacingS * 3
|
||||
compactMode: true
|
||||
dropdownWidth: width
|
||||
|
||||
@@ -10,7 +10,6 @@ Column {
|
||||
id: root
|
||||
|
||||
property var parentModal: null
|
||||
property string confirmingRemoveId: ""
|
||||
property string editingDockId: ""
|
||||
property string renameDraft: ""
|
||||
|
||||
@@ -26,18 +25,12 @@ Column {
|
||||
}
|
||||
|
||||
function addDock() {
|
||||
confirmingRemoveId = "";
|
||||
dock.selectedDockId = SettingsData.createDockConfig();
|
||||
if (!dock.config?.enabled)
|
||||
ToastService.showWarning(I18n.tr("Every edge of this display is already taken"));
|
||||
}
|
||||
|
||||
function removeDock(id) {
|
||||
if (confirmingRemoveId !== id) {
|
||||
confirmingRemoveId = id;
|
||||
return;
|
||||
}
|
||||
confirmingRemoveId = "";
|
||||
SettingsData.removeDockConfig(id);
|
||||
}
|
||||
|
||||
@@ -47,10 +40,7 @@ Column {
|
||||
DockSelectionState {
|
||||
id: dock
|
||||
|
||||
onSelectedDockIdChanged: {
|
||||
root.editingDockId = "";
|
||||
root.confirmingRemoveId = "";
|
||||
}
|
||||
onSelectedDockIdChanged: root.editingDockId = ""
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
@@ -77,7 +67,6 @@ Column {
|
||||
selected: dock.selectedDockId === modelData.id
|
||||
checked: modelData.enabled
|
||||
deletable: SettingsData.dockConfigs.length > 1
|
||||
confirmingDelete: root.confirmingRemoveId === modelData.id
|
||||
onClicked: dock.selectedDockId = modelData.id
|
||||
onToggled: checked => {
|
||||
dock.selectedDockId = modelData.id;
|
||||
|
||||
@@ -11,7 +11,6 @@ Item {
|
||||
|
||||
property var parentModal: null
|
||||
property int catalogRevision: 0
|
||||
property string confirmingRemoveId: ""
|
||||
|
||||
readonly property bool dockHosted: true
|
||||
readonly property var appsEntry: dock.config?.widgets.find(item => item.widgetId === "appsDock") ?? null
|
||||
@@ -61,8 +60,6 @@ Item {
|
||||
|
||||
DockSelectionState {
|
||||
id: dock
|
||||
|
||||
onSelectedDockIdChanged: root.confirmingRemoveId = ""
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -126,7 +123,6 @@ Item {
|
||||
}
|
||||
|
||||
function updateWidgets(widgets) {
|
||||
root.confirmingRemoveId = "";
|
||||
if (!dock.hasConfig)
|
||||
return;
|
||||
SettingsData.updateDockConfig(dock.selectedDockId, {
|
||||
@@ -173,13 +169,12 @@ Item {
|
||||
id: widgetRow
|
||||
|
||||
required property var modelData
|
||||
readonly property bool confirmingRemove: root.confirmingRemoveId === modelData.id
|
||||
readonly property string slotOption: root.slotOptions[modelData.widgetId] ?? ""
|
||||
readonly property bool shown: slotOption ? dock.config[slotOption] === true : modelData.enabled !== false
|
||||
|
||||
title: root.widgetName(modelData.widgetId)
|
||||
iconName: root.widgetIcon(modelData.widgetId)
|
||||
subtitle: confirmingRemove ? I18n.tr("Confirm Delete") : ""
|
||||
subtitle: removeButton.confirming ? I18n.tr("Confirm Delete") : ""
|
||||
subtitleColor: Theme.error
|
||||
body: SettingsSliderRow {
|
||||
width: parent.width
|
||||
@@ -218,18 +213,10 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
SettingsDeleteButton {
|
||||
id: removeButton
|
||||
visible: !widgetRow.slotOption
|
||||
iconName: widgetRow.confirmingRemove ? "warning" : "delete"
|
||||
iconColor: widgetRow.confirmingRemove ? Theme.error : Theme.surfaceVariantText
|
||||
Accessible.name: widgetRow.confirmingRemove ? I18n.tr("Confirm Delete") : I18n.tr("Remove")
|
||||
onClicked: {
|
||||
if (!widgetRow.confirmingRemove) {
|
||||
root.confirmingRemoveId = widgetRow.modelData.id;
|
||||
return;
|
||||
}
|
||||
root.updateWidgets(dock.config.widgets.filter(item => item.id !== widgetRow.modelData.id));
|
||||
}
|
||||
onDeleteRequested: root.updateWidgets(dock.config.widgets.filter(item => item.id !== widgetRow.modelData.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ Item {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: tileColumn.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.floatingWindowNestedSurface
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: Theme.layerOutlineWidth
|
||||
color: Theme.floatingWindowFieldColor
|
||||
border.color: Theme.floatingWindowFieldBorderColor
|
||||
border.width: Theme.outlineWidth
|
||||
|
||||
Column {
|
||||
id: tileColumn
|
||||
|
||||
@@ -216,31 +216,13 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
SettingsNoteRow {
|
||||
visible: root.greeterStatusOutput !== ""
|
||||
|
||||
body: Rectangle {
|
||||
width: parent.width
|
||||
height: statusTextArea.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.floatingWindowFieldColor
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: Theme.layerOutlineWidth
|
||||
|
||||
StyledText {
|
||||
id: statusTextArea
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingM
|
||||
text: root.greeterStatusOutput
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
isMonospace: true
|
||||
color: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText
|
||||
wrapMode: Text.Wrap
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
}
|
||||
noteIconName: ""
|
||||
monospace: true
|
||||
text: root.greeterStatusOutput
|
||||
tint: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText
|
||||
tintBackground: Theme.floatingWindowFieldColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +232,7 @@ Item {
|
||||
tags: ["greeter", "login", "authentication", "pam", "fingerprint", "security", "key"]
|
||||
title: I18n.tr("Authentication")
|
||||
iconName: "fingerprint"
|
||||
onClicked: root.parentModal?.navigateTo("greeter_auth")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("greeter_auth", keyboard)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,19 +248,19 @@ Item {
|
||||
SettingsNavRow {
|
||||
title: I18n.tr("Wallpaper & colors")
|
||||
iconName: "wallpaper"
|
||||
onClicked: root.parentModal?.navigateTo("personalization")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("personalization", keyboard)
|
||||
}
|
||||
|
||||
SettingsNavRow {
|
||||
title: I18n.tr("Fonts & motion")
|
||||
iconName: "text_fields"
|
||||
onClicked: root.parentModal?.navigateTo("typography")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("typography", keyboard)
|
||||
}
|
||||
|
||||
SettingsNavRow {
|
||||
title: I18n.tr("Lock screen")
|
||||
iconName: "lock"
|
||||
onClicked: root.parentModal?.navigateTo("lock_screen")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("lock_screen", keyboard)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -901,6 +901,7 @@ DankDialog {
|
||||
|
||||
SettingsRow {
|
||||
body: DankButtonGroup {
|
||||
arrowKeysSelect: false
|
||||
width: parent.width
|
||||
fillWidth: true
|
||||
maximumWidth: width
|
||||
|
||||
@@ -587,6 +587,7 @@ Item {
|
||||
|
||||
DankListView {
|
||||
id: flickable
|
||||
keyNavigationEnabled: false
|
||||
|
||||
readonly property real columnWidth: Math.min(SettingsMetrics.contentMaxWidth, width - Theme.spacingL * 2)
|
||||
property Item fabBar: null
|
||||
|
||||
@@ -355,7 +355,7 @@ Item {
|
||||
title: I18n.tr("Weather")
|
||||
checked: SettingsData.lockScreenShowWeather
|
||||
onToggled: checked => SettingsData.set("lockScreenShowWeather", checked)
|
||||
onNavigated: root.parentModal?.navigateTo("weather")
|
||||
onNavigated: keyboard => root.parentModal?.navigateTo("weather", keyboard)
|
||||
}
|
||||
|
||||
SettingsDropdownRow {
|
||||
@@ -605,7 +605,6 @@ Item {
|
||||
id: securityKeyCapture
|
||||
width: 200
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
focus: capturing
|
||||
text: capturing ? I18n.tr("Press key...", "lock screen security key shortcut key combination capture prompt") : SettingsData.lockScreenSecurityKeyShortcut
|
||||
backgroundColor: capturing ? Theme.selectedContainer : Theme.chipSurface
|
||||
textColor: Theme.surfaceText
|
||||
@@ -631,8 +630,14 @@ Item {
|
||||
startCapture();
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (!securityKeyCapture.capturing)
|
||||
Keys.onPressed: event => captureKey(event)
|
||||
// Specific Tab handlers stop Qt focus traversal while capturing.
|
||||
Keys.onTabPressed: event => captureKey(event)
|
||||
Keys.onBacktabPressed: event => captureKey(event)
|
||||
|
||||
function captureKey(event) {
|
||||
event.accepted = capturing;
|
||||
if (!capturing)
|
||||
return;
|
||||
|
||||
if (KeyUtils.isModifierKey(event.key))
|
||||
@@ -640,7 +645,6 @@ Item {
|
||||
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
securityKeyCapture.stopCapture();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -798,28 +802,14 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
SettingsNoteRow {
|
||||
visible: root.authValidateMessage !== ""
|
||||
body: Rectangle {
|
||||
width: parent.width
|
||||
height: Math.min(160, authStatusText.implicitHeight + Theme.spacingM * 2)
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.floatingWindowFieldColor
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: Theme.layerOutlineWidth
|
||||
|
||||
StyledText {
|
||||
id: authStatusText
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
text: root.authValidateMessage
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: "monospace"
|
||||
color: !root.authValidateOk ? Theme.error : (root.authValidateWarn ? Theme.warning : Theme.surfaceVariantText)
|
||||
wrapMode: Text.Wrap
|
||||
verticalAlignment: Text.AlignTop
|
||||
}
|
||||
}
|
||||
noteIconName: ""
|
||||
monospace: true
|
||||
maxHeight: SettingsMetrics.noteMaxHeight
|
||||
text: root.authValidateMessage
|
||||
tint: !root.authValidateOk ? Theme.error : (root.authValidateWarn ? Theme.warning : Theme.surfaceVariantText)
|
||||
tintBackground: Theme.floatingWindowFieldColor
|
||||
}
|
||||
|
||||
SettingsDropdownRow {
|
||||
@@ -866,28 +856,14 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
SettingsNoteRow {
|
||||
visible: !root.lockU2fControlledByPrimary && root.u2fValidateMessage !== ""
|
||||
body: Rectangle {
|
||||
width: parent.width
|
||||
height: Math.min(160, u2fStatusText.implicitHeight + Theme.spacingM * 2)
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.floatingWindowFieldColor
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: Theme.layerOutlineWidth
|
||||
|
||||
StyledText {
|
||||
id: u2fStatusText
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
text: root.u2fValidateMessage
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: "monospace"
|
||||
color: !root.u2fValidateOk ? Theme.error : (root.u2fValidateWarn ? Theme.warning : Theme.surfaceVariantText)
|
||||
wrapMode: Text.Wrap
|
||||
verticalAlignment: Text.AlignTop
|
||||
}
|
||||
}
|
||||
noteIconName: ""
|
||||
monospace: true
|
||||
maxHeight: SettingsMetrics.noteMaxHeight
|
||||
text: root.u2fValidateMessage
|
||||
tint: !root.u2fValidateOk ? Theme.error : (root.u2fValidateWarn ? Theme.warning : Theme.surfaceVariantText)
|
||||
tintBackground: Theme.floatingWindowFieldColor
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
|
||||
@@ -88,8 +88,8 @@ Item {
|
||||
title: I18n.tr("Dashboard")
|
||||
hint: I18n.tr("Media")
|
||||
iconName: "dashboard"
|
||||
onClicked: {
|
||||
root.parentModal?.navigateTo("dank_dash");
|
||||
onClicked: keyboard => {
|
||||
root.parentModal?.navigateTo("dank_dash", keyboard);
|
||||
SettingsSearchService.navigateToSection("dashOptions:media");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,8 +263,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: profileDelegate.isActive ? "link_off" : "link"
|
||||
tooltipText: profileDelegate.isActive ? I18n.tr("Disconnect") : I18n.tr("Connect")
|
||||
buttonSize: 28
|
||||
iconSize: 18
|
||||
iconSize: Theme.iconSizeSmall
|
||||
iconColor: profileDelegate.isActive ? Theme.error : Theme.primary
|
||||
onClicked: {
|
||||
if (profileDelegate.isActive)
|
||||
|
||||
@@ -261,37 +261,11 @@ Item {
|
||||
return fields;
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
delegate: DankDetailChip {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: ethFieldContent.width + Theme.spacingM * 2
|
||||
height: 32
|
||||
radius: Theme.cornerRadius - Theme.outlineWidthFocused
|
||||
color: Theme.floatingWindowFieldColor
|
||||
border.width: Theme.outlineWidth
|
||||
border.color: Theme.floatingWindowFieldBorderColor
|
||||
|
||||
Row {
|
||||
id: ethFieldContent
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: modelData.label + ":"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.value
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Theme.fontWeightMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
label: modelData.label
|
||||
value: modelData.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ Item {
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
arrowKeysSelect: false
|
||||
id: preferenceButtons
|
||||
|
||||
readonly property var preferenceValues: {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Modules.Network
|
||||
import qs.Modules.Settings.Widgets
|
||||
@@ -25,6 +24,48 @@ Item {
|
||||
NetworkService.removeRef();
|
||||
}
|
||||
|
||||
function wifiDetailFields(net) {
|
||||
const fields = [];
|
||||
if (!net || net.outOfRange)
|
||||
return fields;
|
||||
|
||||
fields.push({
|
||||
label: I18n.tr("Signal"),
|
||||
value: (net.signal || 0) + "%"
|
||||
});
|
||||
if (net.frequency)
|
||||
fields.push({
|
||||
label: I18n.tr("Frequency", "wifi network detail label, radio frequency in GHz"),
|
||||
value: (net.frequency / 1000).toFixed(1) + " GHz"
|
||||
});
|
||||
if (net.channel)
|
||||
fields.push({
|
||||
label: I18n.tr("Channel", "wifi network detail label, radio channel number"),
|
||||
value: String(net.channel)
|
||||
});
|
||||
if (net.rate)
|
||||
fields.push({
|
||||
label: I18n.tr("Rate", "noun, wifi network detail label, link speed in Mbps"),
|
||||
value: net.rate + " Mbps"
|
||||
});
|
||||
if (net.mode)
|
||||
fields.push({
|
||||
label: I18n.tr("Mode"),
|
||||
value: net.mode
|
||||
});
|
||||
if (net.bssid)
|
||||
fields.push({
|
||||
label: "BSSID",
|
||||
value: net.bssid
|
||||
});
|
||||
fields.push({
|
||||
label: I18n.tr("Security", "noun, settings page name and wifi security type label"),
|
||||
value: net.secured ? (net.enterprise ? I18n.tr("Enterprise", "wifi security type value, 802.1x enterprise network") : "WPA/WPA2") : I18n.tr("Open", "network security type", true)
|
||||
});
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
SettingsPage {
|
||||
id: mainColumn
|
||||
|
||||
@@ -278,31 +319,8 @@ Item {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
id: scanningIcon
|
||||
name: "wifi_find"
|
||||
size: Theme.iconSizeLarge
|
||||
color: Theme.surfaceVariantText
|
||||
DankLoadingIndicator {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
SequentialAnimation {
|
||||
running: networkWifiTab.visible && NetworkService.isScanning
|
||||
loops: Animation.Infinite
|
||||
OpacityAnimator {
|
||||
target: scanningIcon
|
||||
to: 0.3
|
||||
duration: 400
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
OpacityAnimator {
|
||||
target: scanningIcon
|
||||
to: 1.0
|
||||
duration: 400
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
onRunningChanged: if (!running)
|
||||
scanningIcon.opacity = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
@@ -380,7 +398,6 @@ Item {
|
||||
iconName: wifiNetworkDelegate.isExpanded ? "expand_less" : "expand_more"
|
||||
Accessible.name: wifiNetworkDelegate.isExpanded ? I18n.tr("Collapse") : I18n.tr("Expand")
|
||||
iconSize: Theme.iconSizeSmall
|
||||
buttonSize: 28
|
||||
visible: wifiNetworkDelegate.isConnected || wifiNetworkDelegate.modelData.saved
|
||||
onClicked: {
|
||||
if (wifiNetworkDelegate.isExpanded) {
|
||||
@@ -395,7 +412,6 @@ Item {
|
||||
DankActionButton {
|
||||
iconName: "qr_code"
|
||||
tooltipText: I18n.tr("Show QR Code")
|
||||
buttonSize: 28
|
||||
visible: wifiNetworkDelegate.modelData.secured && wifiNetworkDelegate.modelData.saved && !(wifiNetworkDelegate.modelData.enterprise || false)
|
||||
onClicked: {
|
||||
PopoutService.showWifiQRCodeModal(wifiNetworkDelegate.modelData.ssid);
|
||||
@@ -405,7 +421,6 @@ Item {
|
||||
DankActionButton {
|
||||
iconName: "push_pin"
|
||||
Accessible.name: wifiNetworkDelegate.isPinned ? I18n.tr("Unpin") : I18n.tr("Pin", "verb, keep an item pinned in place")
|
||||
buttonSize: 28
|
||||
iconColor: wifiNetworkDelegate.isPinned ? Theme.primary : Theme.surfaceVariantText
|
||||
onClicked: {
|
||||
root.toggleWifiPin(wifiNetworkDelegate.modelData.ssid);
|
||||
@@ -414,8 +429,7 @@ Item {
|
||||
|
||||
DankActionButton {
|
||||
iconName: "delete"
|
||||
tooltipText: I18n.tr("Forget")
|
||||
buttonSize: 28
|
||||
tooltipText: I18n.tr("Forget", "verb, remove a saved wifi network, button")
|
||||
iconColor: Theme.error
|
||||
visible: wifiNetworkDelegate.modelData.saved || wifiNetworkDelegate.isConnected
|
||||
onClicked: {
|
||||
@@ -447,80 +461,13 @@ Item {
|
||||
visible: !NetworkService.networkInfoLoading
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
const fields = [];
|
||||
const net = wifiNetworkDelegate.modelData;
|
||||
if (!net)
|
||||
return fields;
|
||||
model: wifiNetworkDelegate.isExpanded ? networkWifiTab.wifiDetailFields(wifiNetworkDelegate.modelData) : []
|
||||
|
||||
fields.push({
|
||||
label: I18n.tr("Signal"),
|
||||
value: net.signal + "%"
|
||||
});
|
||||
if (net.frequency)
|
||||
fields.push({
|
||||
label: I18n.tr("Frequency", "wifi network detail label, radio frequency in GHz"),
|
||||
value: (net.frequency / 1000).toFixed(1) + " GHz"
|
||||
});
|
||||
if (net.channel)
|
||||
fields.push({
|
||||
label: I18n.tr("Channel", "wifi network detail label, radio channel number"),
|
||||
value: String(net.channel)
|
||||
});
|
||||
if (net.rate)
|
||||
fields.push({
|
||||
label: I18n.tr("Rate", "noun, wifi network detail label, link speed in Mbps"),
|
||||
value: net.rate + " Mbps"
|
||||
});
|
||||
if (net.mode)
|
||||
fields.push({
|
||||
label: I18n.tr("Mode"),
|
||||
value: net.mode
|
||||
});
|
||||
if (net.bssid)
|
||||
fields.push({
|
||||
label: "BSSID",
|
||||
value: net.bssid
|
||||
});
|
||||
fields.push({
|
||||
label: I18n.tr("Security", "noun, settings page name and wifi security type label"),
|
||||
value: net.secured ? (net.enterprise ? I18n.tr("Enterprise", "wifi security type value, 802.1x enterprise network") : "WPA/WPA2") : I18n.tr("Open", "network security type", true)
|
||||
});
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
delegate: DankDetailChip {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: wifiFieldContent.width + Theme.spacingM * 2
|
||||
height: Theme.buttonHeightXS
|
||||
radius: Theme.cornerRadiusS
|
||||
color: Theme.floatingWindowFieldColor
|
||||
border.width: Theme.outlineWidth
|
||||
border.color: Theme.floatingWindowFieldBorderColor
|
||||
|
||||
Row {
|
||||
id: wifiFieldContent
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: modelData.label + ":"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.value
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Theme.fontWeightMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
label: modelData.label
|
||||
value: modelData.value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -574,7 +521,8 @@ Item {
|
||||
readonly property bool isConnecting: NetworkService.isWifiConnecting && NetworkService.connectingSSID === modelData.ssid
|
||||
readonly property bool isPinned: root.getPinnedWifiNetworks().includes(modelData.ssid)
|
||||
readonly property bool isOutOfRange: modelData.outOfRange || false
|
||||
readonly property bool isExpanded: !isOutOfRange && root.expandedSavedWifiSsid === modelData.ssid
|
||||
readonly property bool isExpanded: root.expandedSavedWifiSsid === modelData.ssid
|
||||
readonly property var detailFields: networkWifiTab.wifiDetailFields(modelData)
|
||||
|
||||
width: parent?.width ?? 0
|
||||
spacing: Theme.groupedListGap
|
||||
@@ -595,13 +543,10 @@ Item {
|
||||
subtitleColor: savedWifiDelegate.isConnecting ? Theme.warning : supportingContentColor
|
||||
clickable: !savedWifiDelegate.isOutOfRange && (!NetworkService.isWifiConnecting || savedWifiDelegate.isConnected)
|
||||
onClicked: {
|
||||
if (savedWifiDelegate.isOutOfRange)
|
||||
return;
|
||||
if (savedWifiDelegate.isExpanded) {
|
||||
root.expandedSavedWifiSsid = "";
|
||||
} else {
|
||||
root.expandedSavedWifiSsid = savedWifiDelegate.modelData.ssid;
|
||||
}
|
||||
WifiConnectionActions.connectToNetwork(savedWifiDelegate.modelData, {
|
||||
connected: savedWifiDelegate.isConnected,
|
||||
disconnectWhenConnected: true
|
||||
});
|
||||
}
|
||||
|
||||
leading: [
|
||||
@@ -635,8 +580,6 @@ Item {
|
||||
iconName: savedWifiDelegate.isExpanded ? "expand_less" : "expand_more"
|
||||
Accessible.name: savedWifiDelegate.isExpanded ? I18n.tr("Collapse") : I18n.tr("Expand")
|
||||
iconSize: Theme.iconSizeSmall
|
||||
buttonSize: 28
|
||||
visible: !savedWifiDelegate.isOutOfRange
|
||||
onClicked: {
|
||||
if (savedWifiDelegate.isExpanded) {
|
||||
root.expandedSavedWifiSsid = "";
|
||||
@@ -649,7 +592,6 @@ Item {
|
||||
DankActionButton {
|
||||
iconName: "qr_code"
|
||||
tooltipText: I18n.tr("Show QR Code")
|
||||
buttonSize: 28
|
||||
visible: savedWifiDelegate.modelData.secured && !(savedWifiDelegate.modelData.enterprise || false)
|
||||
onClicked: {
|
||||
PopoutService.showWifiQRCodeModal(savedWifiDelegate.modelData.ssid);
|
||||
@@ -659,7 +601,6 @@ Item {
|
||||
DankActionButton {
|
||||
iconName: "push_pin"
|
||||
Accessible.name: savedWifiDelegate.isPinned ? I18n.tr("Unpin") : I18n.tr("Pin", "verb, keep an item pinned in place")
|
||||
buttonSize: 28
|
||||
iconColor: savedWifiDelegate.isPinned ? Theme.primary : Theme.surfaceVariantText
|
||||
onClicked: {
|
||||
root.toggleWifiPin(savedWifiDelegate.modelData.ssid);
|
||||
@@ -667,188 +608,37 @@ Item {
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: savedWifiMoreButton
|
||||
iconName: "more_horiz"
|
||||
Accessible.name: I18n.tr("Options")
|
||||
buttonSize: 28
|
||||
onClicked: {
|
||||
if (savedWifiMenu.visible) {
|
||||
savedWifiMenu.close();
|
||||
return;
|
||||
}
|
||||
savedWifiMenu.popup(savedWifiMoreButton, -savedWifiMenu.width + savedWifiMoreButton.width, savedWifiMoreButton.height + Theme.spacingXS);
|
||||
}
|
||||
iconName: "delete"
|
||||
tooltipText: I18n.tr("Forget", "verb, remove a saved wifi network, button")
|
||||
iconColor: Theme.error
|
||||
onClicked: root.showForgetNetworkConfirm(savedWifiDelegate.modelData.ssid)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
visible: savedWifiDelegate.isExpanded
|
||||
visible: savedWifiDelegate.isExpanded && savedWifiDelegate.detailFields.length > 0
|
||||
body: Flow {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
const fields = [];
|
||||
const net = savedWifiDelegate.modelData;
|
||||
if (!net)
|
||||
return fields;
|
||||
model: savedWifiDelegate.isExpanded ? savedWifiDelegate.detailFields : []
|
||||
|
||||
fields.push({
|
||||
label: I18n.tr("Signal"),
|
||||
value: (net.signal || 0) + "%"
|
||||
});
|
||||
if (net.frequency)
|
||||
fields.push({
|
||||
label: I18n.tr("Frequency"),
|
||||
value: (net.frequency / 1000).toFixed(1) + " GHz"
|
||||
});
|
||||
if (net.channel)
|
||||
fields.push({
|
||||
label: I18n.tr("Channel"),
|
||||
value: String(net.channel)
|
||||
});
|
||||
if (net.rate)
|
||||
fields.push({
|
||||
label: I18n.tr("Rate"),
|
||||
value: net.rate + " Mbps"
|
||||
});
|
||||
if (net.mode)
|
||||
fields.push({
|
||||
label: I18n.tr("Mode"),
|
||||
value: net.mode
|
||||
});
|
||||
if (net.bssid)
|
||||
fields.push({
|
||||
label: "BSSID",
|
||||
value: net.bssid
|
||||
});
|
||||
fields.push({
|
||||
label: I18n.tr("Security"),
|
||||
value: net.secured ? (net.enterprise ? I18n.tr("Enterprise") : "WPA/WPA2") : I18n.tr("Open", "network security type", true)
|
||||
});
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
delegate: DankDetailChip {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: savedWifiFieldContent.width + Theme.spacingM * 2
|
||||
height: Theme.buttonHeightXS
|
||||
radius: Theme.cornerRadiusS
|
||||
color: Theme.floatingWindowFieldColor
|
||||
border.width: Theme.outlineWidth
|
||||
border.color: Theme.floatingWindowFieldBorderColor
|
||||
|
||||
Row {
|
||||
id: savedWifiFieldContent
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: modelData.label + ":"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.value
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Theme.fontWeightMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
label: modelData.label
|
||||
value: modelData.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: savedWifiMenu
|
||||
width: 170
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.floatingWindowSurface
|
||||
radius: Theme.windowRadius
|
||||
border.width: 0
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: isConnecting ? I18n.tr("Connecting...") : (isConnected ? I18n.tr("Disconnect") : I18n.tr("Connect"))
|
||||
height: isOutOfRange ? 0 : 32
|
||||
visible: !isOutOfRange
|
||||
enabled: !isConnecting
|
||||
|
||||
contentItem: StyledText {
|
||||
text: parent.text
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: parent.enabled ? Theme.surfaceText : Theme.surfaceVariantText
|
||||
leftPadding: Theme.spacingS
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.hovered ? Theme.primaryHoverLight : Theme.withAlpha(Theme.primaryHoverLight, 0)
|
||||
radius: Theme.cornerRadiusS
|
||||
}
|
||||
|
||||
onTriggered: {
|
||||
WifiConnectionActions.connectToNetwork(modelData, {
|
||||
connected: isConnected,
|
||||
disconnectWhenConnected: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: modelData.autoconnect ? I18n.tr("Disable autoconnect") : I18n.tr("Enable autoconnect")
|
||||
height: DMSService.apiVersion > 13 ? 32 : 0
|
||||
visible: DMSService.apiVersion > 13
|
||||
|
||||
contentItem: StyledText {
|
||||
text: parent.text
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
leftPadding: Theme.spacingS
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.hovered ? Theme.primaryHoverLight : Theme.withAlpha(Theme.primaryHoverLight, 0)
|
||||
radius: Theme.cornerRadiusS
|
||||
}
|
||||
|
||||
onTriggered: {
|
||||
NetworkService.setWifiAutoconnect(modelData.ssid, !(modelData.autoconnect || false));
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: I18n.tr("Forget network")
|
||||
height: Theme.buttonHeightXS
|
||||
|
||||
contentItem: StyledText {
|
||||
text: parent.text
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
leftPadding: Theme.spacingS
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.hovered ? Theme.errorHover : Theme.withAlpha(Theme.errorHover, 0)
|
||||
radius: Theme.cornerRadiusS
|
||||
}
|
||||
|
||||
onTriggered: {
|
||||
root.showForgetNetworkConfirm(modelData.ssid);
|
||||
}
|
||||
}
|
||||
SettingsToggleRow {
|
||||
visible: savedWifiDelegate.isExpanded && DMSService.apiVersion > 13
|
||||
text: I18n.tr("Autoconnect", "toggle, connect to this wifi or vpn automatically")
|
||||
checked: savedWifiDelegate.modelData.autoconnect || false
|
||||
onToggled: checked => NetworkService.setWifiAutoconnect(savedWifiDelegate.modelData.ssid, checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,56 +252,23 @@ Item {
|
||||
tags: ["notification", "mute", "unmute", "popup"]
|
||||
|
||||
SettingsRow {
|
||||
body: Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
subtitle: root.mutedRules.length > 0 ? I18n.tr("Apps with notification popups muted. Unmute or delete to remove.") : I18n.tr("No apps muted. Right-click a notification and choose \"Mute popups\" to add one here.")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: mutedRules.length > 0 ? I18n.tr("Apps with notification popups muted. Unmute or delete to remove.") : I18n.tr("No apps muted. Right-click a notification and choose \"Mute popups\" to add one here.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
bottomPadding: Theme.spacingS
|
||||
}
|
||||
Repeater {
|
||||
model: root.mutedRules
|
||||
|
||||
Repeater {
|
||||
model: mutedRules
|
||||
delegate: SettingsRow {
|
||||
required property var modelData
|
||||
|
||||
delegate: Rectangle {
|
||||
width: parent.width
|
||||
height: mutedRow.implicitHeight + Theme.spacingS * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.floatingWindowFieldColor
|
||||
title: modelData.rule?.pattern || I18n.tr("Unknown")
|
||||
singleLineTitle: true
|
||||
|
||||
Row {
|
||||
id: mutedRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
id: mutedAppLabel
|
||||
text: (modelData.rule && modelData.rule.pattern) ? modelData.rule.pattern : I18n.tr("Unknown")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Math.max(0, parent.width - mutedAppLabel.width - unmuteBtn.width - parent.spacing * 2)
|
||||
height: 1
|
||||
}
|
||||
|
||||
DankButton {
|
||||
id: unmuteBtn
|
||||
text: I18n.tr("Unmute")
|
||||
backgroundColor: Theme.chipSurface
|
||||
textColor: Theme.primary
|
||||
onClicked: SettingsData.removeNotificationRule(modelData.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
DankButton {
|
||||
text: I18n.tr("Unmute")
|
||||
backgroundColor: "transparent"
|
||||
textColor: Theme.primary
|
||||
onClicked: SettingsData.removeNotificationRule(modelData.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,59 +270,23 @@ Column {
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
body: Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
subtitle: I18n.tr("Right-click a notification and choose \"Allow in Do Not Disturb\" to add an app")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Right-click a notification and choose \"Allow in Do Not Disturb\" to add an app")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
bottomPadding: Theme.spacingS
|
||||
}
|
||||
Repeater {
|
||||
model: root.dndBypassRules
|
||||
|
||||
Repeater {
|
||||
model: root.dndBypassRules
|
||||
delegate: SettingsRow {
|
||||
required property var modelData
|
||||
|
||||
delegate: Rectangle {
|
||||
width: parent.width
|
||||
height: dndBypassRow.implicitHeight + Theme.spacingS * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.floatingWindowFieldColor
|
||||
title: modelData.rule?.pattern || I18n.tr("Unknown")
|
||||
singleLineTitle: true
|
||||
|
||||
Row {
|
||||
id: dndBypassRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
id: dndBypassLabel
|
||||
text: modelData.rule.pattern || I18n.tr("Unknown")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Math.max(0, parent.width - parent.spacing * 2 - dndBypassLabel.width - dndBypassRemoveBtn.width)
|
||||
height: 1
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: dndBypassRemoveBtn
|
||||
buttonSize: 28
|
||||
iconName: "delete"
|
||||
Accessible.name: I18n.tr("Remove")
|
||||
iconSize: 18
|
||||
iconColor: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: SettingsData.removeNotificationRule(modelData.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
DankActionButton {
|
||||
iconName: "delete"
|
||||
iconColor: Theme.error
|
||||
tooltipText: I18n.tr("Remove")
|
||||
onClicked: SettingsData.removeNotificationRule(modelData.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -335,7 +299,7 @@ Column {
|
||||
iconName: "rule_settings"
|
||||
title: I18n.tr("Rules")
|
||||
hint: SettingsTabs.page("notification_rules")?.hint ?? ""
|
||||
onClicked: root.parentModal?.navigateTo("notification_rules")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("notification_rules", keyboard)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,8 +84,6 @@ Item {
|
||||
|
||||
DankToggle {
|
||||
id: enableToggle
|
||||
width: 40
|
||||
height: 24
|
||||
hideText: true
|
||||
checked: paletteItem.modelData.enabled !== false
|
||||
onToggled: checked => PaletteInjectService.updatePalette(paletteItem.index, "enabled", checked)
|
||||
|
||||
@@ -200,7 +200,7 @@ FocusScope {
|
||||
iconName: "widgets"
|
||||
title: I18n.tr("Desktop widgets")
|
||||
hint: I18n.tr("Desktop widget plugins are configured per widget instance")
|
||||
onClicked: root.parentModal?.navigateTo("desktop_widgets")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("desktop_widgets", keyboard)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Settings.Widgets
|
||||
import qs.Services
|
||||
|
||||
StyledRect {
|
||||
SettingsCard {
|
||||
id: root
|
||||
|
||||
property var updatesList: []
|
||||
@@ -17,16 +16,18 @@ StyledRect {
|
||||
signal pluginUpdated(string pluginId)
|
||||
signal updatesRequested(var plugins)
|
||||
|
||||
width: parent.width
|
||||
height: visible ? innerColumn.implicitHeight + Theme.spacingL * 2 : 0
|
||||
radius: Theme.cornerRadiusM
|
||||
color: Theme.floatingWindowNestedSurface
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: Theme.layerOutlineWidth
|
||||
clip: true
|
||||
|
||||
iconName: "download"
|
||||
title: I18n.tr("Available Updates (%1)", "plugin updates dialog title, %1 is a count").arg(updatesList.length)
|
||||
visible: false
|
||||
|
||||
headerActions: DankActionButton {
|
||||
iconName: "close"
|
||||
tooltipText: I18n.tr("Close")
|
||||
iconColor: Theme.surfaceVariantText
|
||||
enabled: !root.isUpdating && !root.operationsBlocked
|
||||
onClicked: root.hide()
|
||||
}
|
||||
|
||||
function show(list) {
|
||||
if (isUpdating || operationsBlocked)
|
||||
return;
|
||||
@@ -47,7 +48,7 @@ StyledRect {
|
||||
}
|
||||
|
||||
function updateAll() {
|
||||
updatesRequested(updatesList.slice());
|
||||
updatesRequested(updatesList.filter(plugin => PluginService.checkPluginCompatibility(plugin.requires_dms)));
|
||||
}
|
||||
|
||||
function updatePlugins(list) {
|
||||
@@ -81,166 +82,76 @@ StyledRect {
|
||||
updateNext();
|
||||
}
|
||||
|
||||
Column {
|
||||
id: innerColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
SettingsNoteRow {
|
||||
visible: !root.isUpdating && root.updatesList.length > 0
|
||||
text: I18n.tr("Plugin updates can change the code running in your session. Review the changes before updating.", "plugin update audit reminder")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
SettingsRow {
|
||||
visible: root.isUpdating
|
||||
title: root.currentUpdatingPlugin ? I18n.tr("Updating %1...", "plugin updates dialog progress, %1 is the plugin name").arg(root.currentUpdatingPlugin) : I18n.tr("Updating plugins...")
|
||||
leading: DankSpinner {
|
||||
size: Theme.iconSize
|
||||
running: root.isUpdating
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "download"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
SettingsRow {
|
||||
visible: Object.keys(root.updateErrors).length > 0
|
||||
subtitle: Object.values(root.updateErrors).join("\n")
|
||||
subtitleColor: Theme.error
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: I18n.tr("Available Updates (%1)", "plugin updates dialog title, %1 is a count").arg(root.updatesList.length)
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.surfaceText
|
||||
wrapMode: Text.Wrap
|
||||
Repeater {
|
||||
model: root.isUpdating ? [] : root.updatesList
|
||||
|
||||
delegate: SettingsRow {
|
||||
required property var modelData
|
||||
|
||||
readonly property bool compatible: PluginService.checkPluginCompatibility(modelData.requires_dms)
|
||||
|
||||
iconName: modelData.icon || "extension"
|
||||
title: modelData.name || ""
|
||||
subtitle: !compatible ? I18n.tr("Requires DMS %1", "plugin incompatibility notice, %1 is the required DMS version").arg(modelData.requires_dms) : modelData.author ? I18n.tr("by %1", "author attribution").arg(modelData.author) : ""
|
||||
subtitleColor: compatible ? supportingContentColor : Theme.error
|
||||
|
||||
DankActionButton {
|
||||
iconName: "open_in_new"
|
||||
tooltipText: I18n.tr("View Changes", "open plugin changes before updating")
|
||||
visible: !!modelData.diffUrl || !!modelData.repo
|
||||
onClicked: Qt.openUrlExternally(modelData.diffUrl || modelData.repo)
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: collapseBtn
|
||||
iconName: "close"
|
||||
Accessible.name: I18n.tr("Close")
|
||||
iconSize: Theme.iconSizeMedium
|
||||
iconColor: Theme.outline
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
enabled: !root.isUpdating && !root.operationsBlocked
|
||||
onClicked: root.hide()
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: !root.isUpdating && root.updatesList.length > 0
|
||||
|
||||
DankIcon {
|
||||
name: "warning"
|
||||
size: Theme.iconSizeMedium
|
||||
color: Theme.warning
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: I18n.tr("Plugin updates can change the code running in your session. Review the changes before updating.", "plugin update audit reminder")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.onSurfaceVariant
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: isUpdating ? Theme.buttonHeightS : 0
|
||||
visible: isUpdating
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankSpinner {
|
||||
size: Theme.iconSize
|
||||
running: root.isUpdating
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.currentUpdatingPlugin ? I18n.tr("Updating %1...", "plugin updates dialog progress, %1 is the plugin name").arg(root.currentUpdatingPlugin) : I18n.tr("Updating plugins...")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
visible: Object.keys(root.updateErrors).length > 0
|
||||
text: Object.values(root.updateErrors).join("\n")
|
||||
color: Theme.error
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
width: parent.width
|
||||
height: Math.min(listCol.implicitHeight, Theme.smallBreakpoint)
|
||||
clip: true
|
||||
contentHeight: listCol.implicitHeight
|
||||
visible: !isUpdating
|
||||
|
||||
Column {
|
||||
id: listCol
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Repeater {
|
||||
model: root.updatesList
|
||||
|
||||
delegate: SettingsRow {
|
||||
required property var modelData
|
||||
width: listCol.width
|
||||
iconName: modelData.icon || "extension"
|
||||
title: modelData.name || ""
|
||||
subtitle: modelData.author ? I18n.tr("by %1", "author attribution").arg(modelData.author) : ""
|
||||
DankActionButton {
|
||||
iconName: "open_in_new"
|
||||
tooltipText: I18n.tr("View Changes", "open plugin changes before updating")
|
||||
visible: !!modelData.diffUrl || !!modelData.repo
|
||||
onClicked: Qt.openUrlExternally(modelData.diffUrl || modelData.repo)
|
||||
}
|
||||
DankActionButton {
|
||||
iconName: "download"
|
||||
tooltipText: I18n.tr("Update", "verb, button installing a newer plugin version")
|
||||
enabled: !root.isUpdating && !root.operationsBlocked
|
||||
onClicked: root.updateSingle(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("No updates available.")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: root.updatesList.length === 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.spacingM
|
||||
visible: !isUpdating
|
||||
|
||||
DankButton {
|
||||
text: I18n.tr("Cancel")
|
||||
iconName: "close"
|
||||
backgroundColor: Theme.chipSurface
|
||||
textColor: Theme.surfaceText
|
||||
onClicked: root.hide()
|
||||
}
|
||||
|
||||
DankButton {
|
||||
text: I18n.tr("Update All")
|
||||
iconName: "download"
|
||||
enabled: !root.operationsBlocked && root.updatesList.length > 0
|
||||
onClicked: root.updateAll()
|
||||
tooltipText: I18n.tr("Update", "verb, button installing a newer plugin version")
|
||||
enabled: !root.isUpdating && !root.operationsBlocked && compatible
|
||||
onClicked: root.updateSingle(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
visible: !root.isUpdating && root.updatesList.length === 0
|
||||
subtitle: I18n.tr("No updates available.")
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
visible: !root.isUpdating
|
||||
|
||||
DankButton {
|
||||
text: I18n.tr("Cancel")
|
||||
iconName: "close"
|
||||
backgroundColor: "transparent"
|
||||
textColor: Theme.primary
|
||||
onClicked: root.hide()
|
||||
}
|
||||
|
||||
DankButton {
|
||||
text: I18n.tr("Update All")
|
||||
iconName: "download"
|
||||
enabled: !root.operationsBlocked && root.updatesList.some(plugin => PluginService.checkPluginCompatibility(plugin.requires_dms))
|
||||
onClicked: root.updateAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,7 +418,7 @@ Column {
|
||||
wrapText: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.topMargin: (Theme.buttonHeightS - Theme.buttonHeightXS) / 2
|
||||
onClicked: root.parentModal?.navigateTo("plugins_manage")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("plugins_manage", keyboard)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,7 +482,7 @@ Column {
|
||||
textColor: storeCard.accentColor
|
||||
maximumWidth: parent.width
|
||||
wrapText: true
|
||||
onClicked: root.parentModal?.navigateTo("plugins_manage")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("plugins_manage", keyboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -611,7 +611,7 @@ Column {
|
||||
color: SettingsSearchService.highlightSection === highlightKey ? Theme.blend(surfaceColor, Theme.primary, SettingsMetrics.highlightBlend) : surfaceColor
|
||||
clickable: true
|
||||
Accessible.name: modelData.text
|
||||
onClicked: root.parentModal?.navigateTo(modelData.id)
|
||||
onClicked: keyboard => root.parentModal?.navigateTo(modelData.id, keyboard)
|
||||
|
||||
Timer {
|
||||
interval: 0
|
||||
@@ -750,7 +750,7 @@ Column {
|
||||
iconName: "settings"
|
||||
variant: "tonal"
|
||||
Accessible.name: I18n.tr("Settings")
|
||||
onClicked: root.parentModal?.navigateTo(installedCard.modelData.id)
|
||||
onClicked: keyboard => root.parentModal?.navigateTo(installedCard.modelData.id, keyboard)
|
||||
}
|
||||
Row {
|
||||
id: cardActions
|
||||
|
||||
@@ -52,7 +52,7 @@ FocusScope {
|
||||
text: PluginService.pluginDirectory
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
font.family: "monospace"
|
||||
isMonospace: true
|
||||
width: parent.width
|
||||
elide: Text.ElideMiddle
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
|
||||
@@ -301,6 +301,7 @@ Item {
|
||||
|
||||
DankDropdown {
|
||||
id: bootEntryDropdown
|
||||
downKeyOpens: false
|
||||
enabled: bootEntryPicker.options.length > 0
|
||||
Accessible.name: bootEntryPicker.title
|
||||
width: Math.min(dropdownWidth, bootEntryPicker.width - SettingsMetrics.rowPaddingH * 2)
|
||||
|
||||
@@ -573,37 +573,11 @@ Item {
|
||||
return fields;
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
delegate: DankDetailChip {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: fieldContent.width + Theme.spacingM * 2
|
||||
height: 32
|
||||
radius: Theme.cornerRadius - Theme.outlineWidthFocused
|
||||
color: Theme.floatingWindowFieldColor
|
||||
border.width: Theme.outlineWidth
|
||||
border.color: Theme.floatingWindowFieldBorderColor
|
||||
|
||||
Row {
|
||||
id: fieldContent
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: modelData.label + ":"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.value
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Theme.fontWeightMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
label: modelData.label
|
||||
value: modelData.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ FocusScope {
|
||||
id: headerLoader
|
||||
|
||||
width: parent.width
|
||||
focus: status === Loader.Ready
|
||||
source: root.headerFile ? Qt.resolvedUrl(root.headerFile + ".qml") : ""
|
||||
onLoaded: item.parentModal = Qt.binding(() => root.parentModal)
|
||||
}
|
||||
@@ -45,7 +46,7 @@ FocusScope {
|
||||
title: modelData.text
|
||||
hint: modelData.hint ?? ""
|
||||
trailingBadge: modelData.kind === "plugin" && PluginService.loadedPlugins[modelData.pluginId] === undefined ? I18n.tr("Disabled") : ""
|
||||
onClicked: root.parentModal?.navigateTo(modelData.id)
|
||||
onClicked: keyboard => root.parentModal?.navigateTo(modelData.id, keyboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,7 +64,7 @@ FocusScope {
|
||||
iconName: modelData.icon
|
||||
title: modelData.text
|
||||
hint: modelData.hint ?? ""
|
||||
onClicked: root.parentModal?.navigateTo(modelData.id)
|
||||
onClicked: keyboard => root.parentModal?.navigateTo(modelData.id, keyboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,7 +436,7 @@ Item {
|
||||
title: I18n.tr("Release notes")
|
||||
iconName: "auto_awesome"
|
||||
hint: root.notesRelease?.codename ? "v" + root.notesRelease.version + " · " + root.notesRelease.codename : "v" + (root.notesRelease?.version ?? "")
|
||||
onClicked: root.parentModal?.navigateTo("updater_changelog")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("updater_changelog", keyboard)
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
|
||||
@@ -3,6 +3,7 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Modals.FileBrowser
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -29,7 +30,18 @@ Item {
|
||||
})
|
||||
onMatugenPreviewKeyChanged: MatugenPreviewService.refresh()
|
||||
onMatugenAvailableChanged: MatugenPreviewService.refresh()
|
||||
readonly property var log: Log.scoped("ThemeColorsTab")
|
||||
property var installedRegistryThemes: []
|
||||
readonly property var activeRegistryTheme: {
|
||||
const file = SettingsData.customThemeFile;
|
||||
if (Theme.currentThemeCategory !== "registry" || Theme.currentTheme !== Theme.custom || !file)
|
||||
return null;
|
||||
return installedRegistryThemes.find(t => file.endsWith((t.sourceDir || t.id) + "/theme.json")) ?? null;
|
||||
}
|
||||
|
||||
function registryThemeDir(theme) {
|
||||
return Quickshell.env("HOME") + "/.config/DankMaterialShell/themes/" + (theme.sourceDir || theme.id);
|
||||
}
|
||||
Component.onCompleted: {
|
||||
if (DMSService.dmsAvailable)
|
||||
DMSService.listInstalledThemes();
|
||||
@@ -69,16 +81,7 @@ Item {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
property string registryThemeName: {
|
||||
if (Theme.currentThemeCategory !== "registry")
|
||||
return "";
|
||||
for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) {
|
||||
var t = themeColorsTab.installedRegistryThemes[i];
|
||||
if (SettingsData.customThemeFile && SettingsData.customThemeFile.endsWith((t.sourceDir || t.id) + "/theme.json"))
|
||||
return t.name;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
readonly property string registryThemeName: themeColorsTab.activeRegistryTheme?.name ?? ""
|
||||
text: {
|
||||
if (Theme.currentThemeCategory === "registry" && registryThemeName)
|
||||
return I18n.tr("Current Theme: %1", "current theme label").arg(registryThemeName);
|
||||
@@ -123,6 +126,7 @@ Item {
|
||||
clip: true
|
||||
|
||||
DankButtonGroup {
|
||||
arrowKeysSelect: false
|
||||
id: themeCategoryGroup
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
buttonPadding: parent.width < 420 ? Theme.spacingS : Theme.spacingL
|
||||
@@ -338,7 +342,9 @@ Item {
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
duration: Theme.expressiveDurations.expressiveFastEffects
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,181 +509,52 @@ Item {
|
||||
SettingsRow {
|
||||
visible: Theme.currentThemeCategory === "registry"
|
||||
body: Column {
|
||||
id: registrySection
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Grid {
|
||||
id: themeGrid
|
||||
property int cardWidth: registrySection.width < 350 ? 100 : 140
|
||||
property int cardHeight: registrySection.width < 350 ? 72 : 100
|
||||
columns: Math.max(1, Math.floor((registrySection.width + spacing) / (cardWidth + spacing)))
|
||||
spacing: Theme.spacingS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
SettingsSwatchGrid {
|
||||
minTileWidth: SettingsMetrics.previewTileMinWidth
|
||||
visible: themeColorsTab.installedRegistryThemes.length > 0
|
||||
|
||||
Repeater {
|
||||
model: themeColorsTab.installedRegistryThemes
|
||||
|
||||
Rectangle {
|
||||
id: themeCard
|
||||
property bool isActive: Theme.currentThemeCategory === "registry" && Theme.currentThemeName === "custom" && SettingsData.customThemeFile && SettingsData.customThemeFile.endsWith((modelData.sourceDir || modelData.id) + "/theme.json")
|
||||
property bool hasVariants: modelData.hasVariants || false
|
||||
property var variants: modelData.variants || null
|
||||
property string selectedVariant: hasVariants ? SettingsData.getRegistryThemeVariant(modelData.id, variants?.default || "") : ""
|
||||
property string previewPath: {
|
||||
const baseDir = Quickshell.env("HOME") + "/.config/DankMaterialShell/themes/" + (modelData.sourceDir || modelData.id);
|
||||
const mode = Theme.isLightMode ? "light" : "dark";
|
||||
if (hasVariants && selectedVariant)
|
||||
return baseDir + "/preview-" + selectedVariant + "-" + mode + ".svg";
|
||||
return baseDir + "/preview-" + mode + ".svg";
|
||||
}
|
||||
width: themeGrid.cardWidth
|
||||
height: themeGrid.cardHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.floatingWindowFieldColor
|
||||
border.color: isActive ? Theme.primary : Theme.outlineMedium
|
||||
border.width: isActive ? Theme.outlineWidthFocused : Theme.layerOutlineWidth
|
||||
scale: isActive ? 1.03 : 1
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: previewImage
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingXXS
|
||||
source: "file://" + themeCard.previewPath
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
mipmap: true
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "palette"
|
||||
size: themeGrid.cardWidth < 120 ? 24 : 32
|
||||
color: Theme.primary
|
||||
visible: previewImage.status === Image.Error || previewImage.status === Image.Null
|
||||
}
|
||||
|
||||
DankPaletteSwatch {
|
||||
readonly property var palette: ThemePalette.pick((Theme.isLightMode ? themeCard.modelData.light : themeCard.modelData.dark) ?? themeCard.modelData.dark)
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Theme.spacingXS
|
||||
width: Theme.iconSizeMedium
|
||||
height: Theme.iconSizeMedium
|
||||
visible: !!palette
|
||||
primaryColor: palette?.primary ?? Theme.primary
|
||||
secondaryColor: palette?.secondary ?? primaryColor
|
||||
tertiaryColor: palette?.tertiary ?? secondaryColor
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: themeGrid.cardWidth < 120 ? 18 : 22
|
||||
bottomLeftRadius: parent.radius
|
||||
bottomRightRadius: parent.radius
|
||||
color: Theme.withAlpha(Theme.scrimColor, Theme.scrimAlpha)
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.name
|
||||
font.pixelSize: themeGrid.cardWidth < 120 ? Theme.fontSizeSmall - 2 : Theme.fontSizeSmall
|
||||
color: Theme.contrastLight
|
||||
font.weight: Theme.fontWeightMedium
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - Theme.spacingXS * 2
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: themeGrid.cardWidth < 120 ? 2 : 4
|
||||
width: themeGrid.cardWidth < 120 ? 16 : 20
|
||||
height: width
|
||||
radius: Theme.fullRadius(width, height)
|
||||
color: Theme.primary
|
||||
visible: themeCard.isActive
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "check"
|
||||
size: themeGrid.cardWidth < 120 ? 10 : 14
|
||||
color: Theme.onPrimary
|
||||
}
|
||||
}
|
||||
|
||||
DankBadge {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.margins: themeGrid.cardWidth < 120 ? 2 : 4
|
||||
color: Theme.secondaryContainer
|
||||
textColor: Theme.onSecondaryContainer
|
||||
visible: themeCard.hasVariants && !deleteButton.visible
|
||||
text: {
|
||||
if (themeCard.variants?.type === "multi")
|
||||
return themeCard.variants?.accents?.length || 0;
|
||||
return themeCard.variants?.options?.length || 0;
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cardMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
const themesDir = Quickshell.env("HOME") + "/.config/DankMaterialShell/themes";
|
||||
const themePath = themesDir + "/" + (modelData.sourceDir || modelData.id) + "/theme.json";
|
||||
SettingsData.set("customThemeFile", themePath);
|
||||
Theme.switchTheme("custom", true, true);
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: deleteButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.margins: themeGrid.cardWidth < 120 ? 2 : 4
|
||||
buttonSize: themeGrid.cardWidth < 120 ? 18 : 24
|
||||
iconName: "close"
|
||||
iconSize: themeGrid.cardWidth < 120 ? 10 : 14
|
||||
iconColor: Theme.contrastLight
|
||||
backgroundColor: Theme.withAlpha(Theme.scrimColor, Theme.scrimAlpha)
|
||||
stateColor: Theme.error
|
||||
tooltipText: I18n.tr("Delete")
|
||||
opacity: cardMouseArea.containsMouse || hovered ? 1 : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
ToastService.showInfo(I18n.tr("Uninstalling: %1", "uninstallation progress").arg(modelData.name));
|
||||
DMSService.uninstallTheme(modelData.id, response => {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Uninstall failed: %1", "uninstallation error").arg(response.error));
|
||||
return;
|
||||
}
|
||||
ToastService.showInfo(I18n.tr("Uninstalled: %1", "uninstallation success").arg(modelData.name));
|
||||
DMSService.listInstalledThemes();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
currentValue: themeColorsTab.activeRegistryTheme?.id ?? ""
|
||||
options: themeColorsTab.installedRegistryThemes.map(theme => ({
|
||||
"value": theme.id,
|
||||
"label": theme.name,
|
||||
"deletable": true
|
||||
}))
|
||||
resolveOption: option => {
|
||||
const theme = themeColorsTab.installedRegistryThemes.find(t => t.id === option.value);
|
||||
if (!theme)
|
||||
return {};
|
||||
const mode = Theme.isLightMode ? "light" : "dark";
|
||||
const variant = theme.hasVariants ? SettingsData.getRegistryThemeVariant(theme.id, theme.variants?.default || "") : "";
|
||||
const palette = ThemePalette.pick((Theme.isLightMode ? theme.light : theme.dark) ?? theme.dark) ?? {};
|
||||
const variantCount = theme.variants?.type === "multi" ? theme.variants?.accents?.length : theme.variants?.options?.length;
|
||||
return {
|
||||
"preview": themeColorsTab.registryThemeDir(theme) + "/preview-" + (variant ? variant + "-" : "") + mode + ".svg",
|
||||
"primary": palette.primary ?? Theme.primary.toString(),
|
||||
"secondary": palette.secondary,
|
||||
"tertiary": palette.tertiary,
|
||||
"badge": theme.hasVariants && variantCount ? String(variantCount) : ""
|
||||
};
|
||||
}
|
||||
onSelected: value => {
|
||||
const theme = themeColorsTab.installedRegistryThemes.find(t => t.id === value);
|
||||
if (!theme)
|
||||
return;
|
||||
SettingsData.set("customThemeFile", themeColorsTab.registryThemeDir(theme) + "/theme.json");
|
||||
Theme.switchTheme("custom", true, true);
|
||||
}
|
||||
onDeleteRequested: value => {
|
||||
const theme = themeColorsTab.installedRegistryThemes.find(t => t.id === value);
|
||||
if (!theme)
|
||||
return;
|
||||
uninstallThemeConfirm.showWithOptions({
|
||||
"title": I18n.tr("Uninstall"),
|
||||
"message": I18n.tr("Uninstall %1?", "plugin removal confirmation").arg(theme.name),
|
||||
"confirmText": I18n.tr("Uninstall"),
|
||||
"confirmColor": Theme.error,
|
||||
"onConfirm": () => themeColorsTab.uninstallRegistryTheme(theme)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,14 +588,7 @@ Item {
|
||||
property string activeThemeId: {
|
||||
switch (Theme.currentThemeCategory) {
|
||||
case "registry":
|
||||
if (Theme.currentTheme !== "custom")
|
||||
return "";
|
||||
for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) {
|
||||
var t = themeColorsTab.installedRegistryThemes[i];
|
||||
if (SettingsData.customThemeFile && SettingsData.customThemeFile.endsWith((t.sourceDir || t.id) + "/theme.json"))
|
||||
return t.id;
|
||||
}
|
||||
return "";
|
||||
return themeColorsTab.activeRegistryTheme?.id ?? "";
|
||||
case "custom":
|
||||
return Theme.currentThemeId || "";
|
||||
default:
|
||||
@@ -730,12 +600,7 @@ Item {
|
||||
return null;
|
||||
switch (Theme.currentThemeCategory) {
|
||||
case "registry":
|
||||
for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) {
|
||||
var t = themeColorsTab.installedRegistryThemes[i];
|
||||
if (t.id === activeThemeId && t.hasVariants)
|
||||
return t.variants;
|
||||
}
|
||||
return null;
|
||||
return themeColorsTab.activeRegistryTheme?.hasVariants ? themeColorsTab.activeRegistryTheme.variants : null;
|
||||
case "custom":
|
||||
return Theme.currentThemeVariants || null;
|
||||
default:
|
||||
@@ -801,6 +666,7 @@ Item {
|
||||
visible: variantSelector.isMultiVariant && variantSelector.flavorOptions.length > 1
|
||||
|
||||
DankButtonGroup {
|
||||
arrowKeysSelect: false
|
||||
id: flavorButtonGroup
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
property int _count: variantSelector.flavorNames.length
|
||||
@@ -849,43 +715,21 @@ Item {
|
||||
Repeater {
|
||||
model: variantSelector.activeThemeVariants?.accents || []
|
||||
|
||||
Rectangle {
|
||||
DankColorButton {
|
||||
required property var modelData
|
||||
required property int index
|
||||
property string accentId: modelData.id
|
||||
property bool isSelected: accentId === variantSelector.selectedAccent
|
||||
readonly property string accentId: modelData.id
|
||||
|
||||
width: accentColorsGrid.dotSize
|
||||
height: accentColorsGrid.dotSize
|
||||
radius: Theme.fullRadius(width, height)
|
||||
color: modelData.color || modelData[variantSelector.selectedFlavor]?.primary || Theme.primary
|
||||
border.color: Theme.outline
|
||||
border.width: isSelected ? Theme.outlineWidthFocused : Theme.outlineWidth
|
||||
scale: isSelected ? 1.1 : 1
|
||||
|
||||
DankTooltipHost {
|
||||
text: modelData.name
|
||||
target: parent
|
||||
hoverArea: accentMouseArea
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: accentMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (parent.isSelected)
|
||||
return;
|
||||
Theme.screenTransition();
|
||||
SettingsData.setRegistryThemeMultiVariant(variantSelector.activeThemeId, variantSelector.selectedFlavor, parent.accentId, variantSelector.colorMode);
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
swatchColor: modelData.color || modelData[variantSelector.selectedFlavor]?.primary || Theme.primary
|
||||
selected: accentId === variantSelector.selectedAccent
|
||||
tooltipText: modelData.name
|
||||
onClicked: {
|
||||
if (selected)
|
||||
return;
|
||||
Theme.screenTransition();
|
||||
SettingsData.setRegistryThemeMultiVariant(variantSelector.activeThemeId, variantSelector.selectedFlavor, accentId, variantSelector.colorMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -899,6 +743,7 @@ Item {
|
||||
visible: !variantSelector.isMultiVariant && variantSelector.variantNames.length > 0
|
||||
|
||||
DankButtonGroup {
|
||||
arrowKeysSelect: false
|
||||
id: variantButtonGroup
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
property int _count: variantSelector.variantNames.length
|
||||
@@ -1035,4 +880,21 @@ Item {
|
||||
extractSaveFileView.path = outputPath;
|
||||
extractSaveFileView.setText(json);
|
||||
}
|
||||
|
||||
function uninstallRegistryTheme(theme) {
|
||||
ToastService.showInfo(I18n.tr("Uninstalling: %1", "uninstallation progress").arg(theme.name));
|
||||
DMSService.uninstallTheme(theme.id, response => {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Uninstall failed: %1", "uninstallation error").arg(response.error));
|
||||
return;
|
||||
}
|
||||
ToastService.showInfo(I18n.tr("Uninstalled: %1", "uninstallation success").arg(theme.name));
|
||||
DMSService.listInstalledThemes();
|
||||
});
|
||||
}
|
||||
|
||||
ConfirmDialogOverlay {
|
||||
id: uninstallThemeConfirm
|
||||
parent: themeColorsTab.parentModal?.modalFocusScope ?? themeColorsTab
|
||||
}
|
||||
}
|
||||
@@ -212,7 +212,7 @@ Column {
|
||||
settingKey: "blurXrayLink"
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland
|
||||
title: I18n.tr("Xray options are in Compositor → Layout")
|
||||
onClicked: root.parentModal?.navigateTo("compositor_layout")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("compositor_layout", keyboard)
|
||||
}
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
@@ -291,7 +291,7 @@ Column {
|
||||
subtitle: SettingsTabs.page("surface_shadows")?.hint ?? ""
|
||||
resetKeys: ["m3ElevationEnabled"]
|
||||
checked: SettingsData.m3ElevationEnabled ?? true
|
||||
onNavigated: root.parentModal?.navigateTo("surface_shadows")
|
||||
onNavigated: keyboard => root.parentModal?.navigateTo("surface_shadows", keyboard)
|
||||
onToggled: checked => SettingsData.set("m3ElevationEnabled", checked)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,8 +200,6 @@ Item {
|
||||
outlined: true
|
||||
leftIconName: "calendar_today"
|
||||
labelText: I18n.tr("Format")
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
x: Theme.spacingM
|
||||
visible: root.editingBarFormat || root.isCustomFormat(SettingsData.clockDateFormat)
|
||||
placeholderText: I18n.tr("Enter custom top bar format (e.g., ddd MMM d)")
|
||||
text: SettingsData.clockDateFormat
|
||||
@@ -281,8 +279,6 @@ Item {
|
||||
outlined: true
|
||||
leftIconName: "calendar_today"
|
||||
labelText: I18n.tr("Format")
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
x: Theme.spacingM
|
||||
visible: root.editingLockFormat || root.isCustomFormat(SettingsData.lockDateFormat)
|
||||
placeholderText: I18n.tr("Enter custom lock screen format (e.g., dddd, MMMM d)")
|
||||
text: SettingsData.lockDateFormat
|
||||
@@ -294,92 +290,80 @@ Item {
|
||||
|
||||
SettingsRow {
|
||||
visible: customFormatInput.visible || customLockFormatInput.visible
|
||||
body: Rectangle {
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
x: Theme.spacingM
|
||||
height: formatHelp.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.floatingWindowNestedSurface
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: Theme.layerOutlineWidth
|
||||
body: Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Column {
|
||||
id: formatHelp
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
StyledText {
|
||||
text: I18n.tr("Format legend")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
font.weight: Theme.fontWeightMedium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Format legend")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
font.weight: Theme.fontWeightMedium
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Column {
|
||||
width: (parent.width - Theme.spacingL) / 2
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("• d - Day (1-31)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• dd - Day (01-31)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• ddd - Day name (Mon)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• dddd - Day name (Monday)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• M - Month (1-12)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
Column {
|
||||
width: (parent.width - Theme.spacingL) / 2
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
Column {
|
||||
width: (parent.width - Theme.spacingL) / 2
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("• d - Day (1-31)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• dd - Day (01-31)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• ddd - Day name (Mon)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• dddd - Day name (Monday)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• M - Month (1-12)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• MM - Month (01-12)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
Column {
|
||||
width: (parent.width - Theme.spacingL) / 2
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("• MM - Month (01-12)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• MMM - Month (Jan)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• MMMM - Month (January)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• yy - Year (24)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• yyyy - Year (2024)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• MMM - Month (Jan)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• MMMM - Month (January)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• yy - Year (24)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("• yyyy - Year (2024)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -438,7 +422,7 @@ Item {
|
||||
tags: ["weather", "location", "units"]
|
||||
title: I18n.tr("Weather")
|
||||
iconName: "partly_cloudy_day"
|
||||
onClicked: root.parentModal?.navigateTo("weather")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("weather", keyboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,8 +179,9 @@ Item {
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
duration: Theme.expressiveDurations.expressiveFastEffects
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ Item {
|
||||
DankFab {
|
||||
text: I18n.tr("Add user")
|
||||
iconName: "person_add"
|
||||
onClicked: root.parentModal?.navigateTo("user_create")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("user_create", keyboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +211,7 @@ Column {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankButtonGroup {
|
||||
arrowKeysSelect: false
|
||||
id: modeGroup
|
||||
width: parent.width
|
||||
fillWidth: true
|
||||
@@ -254,7 +255,7 @@ Column {
|
||||
height: hero.stacked ? implicitHeight : Math.max(implicitHeight, side.navRowHeight)
|
||||
paddingH: Theme.spacingM
|
||||
paddingV: Theme.spacingM
|
||||
onClicked: root.parentModal?.navigateTo("theme_schedule")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("theme_schedule", keyboard)
|
||||
}
|
||||
|
||||
SettingsNavRow {
|
||||
@@ -266,7 +267,7 @@ Column {
|
||||
height: hero.stacked ? implicitHeight : Math.max(implicitHeight, side.navRowHeight)
|
||||
paddingH: Theme.spacingM
|
||||
paddingV: Theme.spacingM
|
||||
onClicked: root.parentModal?.navigateTo("theme")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("theme", keyboard)
|
||||
|
||||
leading: DankPaletteSwatch {
|
||||
width: SettingsMetrics.heroLeadingSize
|
||||
@@ -410,7 +411,7 @@ Column {
|
||||
title: I18n.tr("Automatic cycling")
|
||||
subtitle: SettingsTabs.page("wallpaper_cycling")?.hint ?? ""
|
||||
checked: root.cyclingEnabled
|
||||
onNavigated: root.parentModal?.navigateTo("wallpaper_cycling")
|
||||
onNavigated: keyboard => root.parentModal?.navigateTo("wallpaper_cycling", keyboard)
|
||||
onToggled: toggled => {
|
||||
if (root.perMonitor) {
|
||||
SessionData.setMonitorCyclingEnabled(root.selectedScreen, toggled);
|
||||
@@ -505,9 +506,9 @@ Column {
|
||||
iconName: "lock"
|
||||
title: I18n.tr("Lock screen")
|
||||
hint: SettingsData.lockScreenWallpaperPath ? SettingsData.lockScreenWallpaperPath.split("/").pop() : I18n.tr("Use desktop wallpaper")
|
||||
onClicked: {
|
||||
onClicked: keyboard => {
|
||||
SettingsSearchService.navigateToSection("lockScreenWallpaperPath");
|
||||
root.parentModal?.navigateTo("lock_screen");
|
||||
root.parentModal?.navigateTo("lock_screen", keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,13 +138,13 @@ Item {
|
||||
SettingsNavRow {
|
||||
title: I18n.tr("Dashboard")
|
||||
iconName: "space_dashboard"
|
||||
onClicked: root.parentModal?.navigateTo("dank_dash")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("dank_dash", keyboard)
|
||||
}
|
||||
|
||||
SettingsNavRow {
|
||||
title: I18n.tr("Bar widgets")
|
||||
iconName: "widgets"
|
||||
onClicked: root.parentModal?.navigateTo("dankbar_widgets")
|
||||
onClicked: keyboard => root.parentModal?.navigateTo("dankbar_widgets", keyboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,8 +180,9 @@ DankFloatingWindow {
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
duration: Theme.expressiveDurations.expressiveFastEffects
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,6 @@ FocusScope {
|
||||
|
||||
implicitHeight: loader.item ? loader.item.implicitHeight : 0
|
||||
|
||||
Keys.onPressed: event => {
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
anchors.fill: parent
|
||||
@@ -34,12 +30,6 @@ FocusScope {
|
||||
item.pluginService = root.pluginService;
|
||||
if ("popoutService" in item)
|
||||
item.popoutService = PopoutService;
|
||||
Qt.callLater(() => {
|
||||
if (!loader.item)
|
||||
return;
|
||||
root.focus = true;
|
||||
loader.item.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ SettingsRow {
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
arrowKeysSelect: false
|
||||
id: buttonGroup
|
||||
parent: root.compact ? bodyHost : trailingHost
|
||||
x: root.compact ? (parent.width - width) / 2 : 0
|
||||
|
||||
@@ -15,6 +15,7 @@ Column {
|
||||
property string settingKey: ""
|
||||
|
||||
property string title: ""
|
||||
// Not drawn; extract_settings_index.py reads it for the search result icon
|
||||
property string iconName: ""
|
||||
property bool collapsible: false
|
||||
property bool expanded: true
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankActionButton {
|
||||
id: root
|
||||
|
||||
property bool confirming: false
|
||||
|
||||
signal deleteRequested
|
||||
|
||||
iconName: confirming ? "warning" : "delete"
|
||||
iconColor: confirming ? Theme.error : Theme.surfaceVariantText
|
||||
tooltipText: confirming ? I18n.tr("Confirm Delete") : I18n.tr("Delete")
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (!activeFocus)
|
||||
confirming = false;
|
||||
}
|
||||
onClicked: {
|
||||
if (!confirming) {
|
||||
confirming = true;
|
||||
return;
|
||||
}
|
||||
confirming = false;
|
||||
deleteRequested();
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,11 @@ import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Rectangle {
|
||||
width: parent?.width ?? 0
|
||||
height: Theme.dividerWidth
|
||||
property bool vertical: false
|
||||
|
||||
width: vertical ? Theme.dividerWidth : parent?.width ?? 0
|
||||
height: vertical ? SettingsMetrics.splitDividerHeight : Theme.dividerWidth
|
||||
color: Theme.outlineVariant
|
||||
visible: !(parent?.isSettingsGroupHost ?? false)
|
||||
anchors.verticalCenter: vertical ? parent?.verticalCenter : undefined
|
||||
}
|
||||
@@ -41,6 +41,7 @@ SettingsRow {
|
||||
|
||||
DankDropdown {
|
||||
id: dropdown
|
||||
downKeyOpens: false
|
||||
enabled: root.enabled
|
||||
Accessible.name: root.text
|
||||
Accessible.description: root.description + (root.description ? " · " : "") + currentValue
|
||||
|
||||
@@ -11,7 +11,6 @@ SettingsRow {
|
||||
property bool toggleVisible: true
|
||||
property bool toggleEnabled: true
|
||||
property bool deletable: true
|
||||
property bool confirmingDelete: false
|
||||
|
||||
signal toggled(bool checked)
|
||||
signal deleteRequested
|
||||
@@ -19,8 +18,8 @@ SettingsRow {
|
||||
clickable: true
|
||||
iconName: selected ? "radio_button_checked" : "radio_button_unchecked"
|
||||
iconColor: selected ? Theme.primary : Theme.surfaceVariantText
|
||||
subtitle: confirmingDelete ? I18n.tr("Confirm Delete") : summary
|
||||
subtitleColor: confirmingDelete ? Theme.error : Theme.surfaceVariantText
|
||||
subtitle: deleteButton.confirming ? I18n.tr("Confirm Delete") : summary
|
||||
subtitleColor: deleteButton.confirming ? Theme.error : Theme.surfaceVariantText
|
||||
|
||||
DankToggle {
|
||||
visible: root.toggleVisible
|
||||
@@ -32,11 +31,9 @@ SettingsRow {
|
||||
onToggled: value => root.toggled(value)
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
SettingsDeleteButton {
|
||||
id: deleteButton
|
||||
visible: root.deletable
|
||||
iconName: root.confirmingDelete ? "warning" : "delete"
|
||||
iconColor: root.confirmingDelete ? Theme.error : Theme.surfaceVariantText
|
||||
Accessible.name: root.confirmingDelete ? I18n.tr("Confirm Delete") : I18n.tr("Remove")
|
||||
onClicked: root.deleteRequested()
|
||||
onDeleteRequested: root.deleteRequested()
|
||||
}
|
||||
}
|
||||
@@ -111,20 +111,24 @@ GridLayout {
|
||||
border.color: isActive ? Theme.primary : Theme.outlineMedium
|
||||
|
||||
activeFocusOnTab: true
|
||||
Accessible.role: Accessible.RadioButton
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: modelData.label
|
||||
Accessible.checkable: true
|
||||
Accessible.checked: isActive
|
||||
Accessible.onPressAction: root.selected(modelData.key)
|
||||
Keys.onSpacePressed: root.selected(modelData.key)
|
||||
Keys.onEnterPressed: root.selected(modelData.key)
|
||||
Keys.onReturnPressed: root.selected(modelData.key)
|
||||
|
||||
FocusRing {}
|
||||
FocusRing {
|
||||
id: cardRing
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
opacity: modeMouse.containsMouse ? Theme.stateLayerHover : 0
|
||||
opacity: cardRing.visible ? Theme.stateLayerFocus : modeMouse.containsMouse ? Theme.stateLayerHover : 0
|
||||
}
|
||||
|
||||
Column {
|
||||
@@ -252,7 +256,11 @@ GridLayout {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.selected(modeCard.modelData.key)
|
||||
onClicked: {
|
||||
cardRing.pointerFocused = true;
|
||||
modeCard.forceActiveFocus(Qt.MouseFocusReason);
|
||||
root.selected(modeCard.modelData.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,9 @@ Singleton {
|
||||
readonly property real bannerTextMinWidth: 100
|
||||
readonly property real fontMenuExtraWidth: 100
|
||||
readonly property real swatchTileMinWidth: 96
|
||||
readonly property real previewTileMinWidth: 140
|
||||
readonly property real emptyStateHeight: 100
|
||||
readonly property real noteMaxHeight: 160
|
||||
readonly property color rowColor: Theme.foregroundColor(Theme.cardSurface, true)
|
||||
readonly property color rowHighlightColor: Theme.withAlpha(Theme.primary, highlightBlend)
|
||||
readonly property color selectedRowColor: Theme.selectedContainer
|
||||
|
||||
@@ -9,33 +9,47 @@ SettingsRow {
|
||||
property string noteIconName: "warning"
|
||||
property color tint: Theme.warning
|
||||
property color tintBackground: Theme.warningHover
|
||||
property bool monospace: false
|
||||
property real maxHeight: 0
|
||||
|
||||
body: Rectangle {
|
||||
readonly property real naturalHeight: noteRow.implicitHeight + Theme.spacingS * 2
|
||||
|
||||
width: parent.width
|
||||
height: noteRow.implicitHeight + Theme.spacingS * 2
|
||||
height: root.maxHeight > 0 ? Math.min(root.maxHeight, naturalHeight) : naturalHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: root.tintBackground
|
||||
|
||||
Row {
|
||||
id: noteRow
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
contentHeight: noteRow.implicitHeight
|
||||
interactive: contentHeight > height
|
||||
wheelEnabled: interactive
|
||||
clip: interactive
|
||||
|
||||
DankIcon {
|
||||
name: root.noteIconName
|
||||
size: Theme.iconSizeSmall
|
||||
color: root.tint
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Row {
|
||||
id: noteRow
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
width: parent.width - parent.spacing - Theme.iconSizeSmall
|
||||
text: root.text
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: root.tint
|
||||
wrapMode: Text.WordWrap
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
DankIcon {
|
||||
visible: root.noteIconName !== ""
|
||||
name: root.noteIconName
|
||||
size: Theme.iconSizeSmall
|
||||
color: root.tint
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: root.noteIconName !== "" ? parent.width - parent.spacing - Theme.iconSizeSmall : parent.width
|
||||
text: root.text
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
isMonospace: root.monospace
|
||||
color: root.tint
|
||||
wrapMode: root.monospace ? Text.Wrap : Text.WordWrap
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
// Parent to the modal focus scope so it covers the window; focus returns to the opener on close
|
||||
Loader {
|
||||
id: root
|
||||
|
||||
property string title: I18n.tr("Rename")
|
||||
property string supportingText: ""
|
||||
property string labelText: I18n.tr("Name")
|
||||
property string leftIconName: "edit"
|
||||
property Component aboveField: null
|
||||
property Component belowField: null
|
||||
property string text: ""
|
||||
readonly property bool opened: _opened
|
||||
|
||||
property bool _opened: false
|
||||
property Item _returnFocusTo: null
|
||||
|
||||
signal accepted(string name)
|
||||
|
||||
anchors.fill: parent
|
||||
z: 1000
|
||||
active: false
|
||||
onLoaded: item.show()
|
||||
|
||||
function show(initialText) {
|
||||
if (_opened)
|
||||
return;
|
||||
text = initialText ?? "";
|
||||
_returnFocusTo = Window.window?.activeFocusItem ?? null;
|
||||
_opened = true;
|
||||
if (item)
|
||||
item.show();
|
||||
else
|
||||
active = true;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (!_opened)
|
||||
return;
|
||||
_opened = false;
|
||||
if (item)
|
||||
item.opened = false;
|
||||
const focusItem = _returnFocusTo;
|
||||
_returnFocusTo = null;
|
||||
if (focusItem?.visible && focusItem.enabled)
|
||||
focusItem.forceActiveFocus(Qt.OtherFocusReason);
|
||||
}
|
||||
|
||||
sourceComponent: DankDialog {
|
||||
id: dialog
|
||||
|
||||
embedded: nativeWindow
|
||||
opened: nativeWindow
|
||||
maximumWidth: SettingsMetrics.formDialogWidth
|
||||
surfaceColor: Theme.hostSurface
|
||||
title: root.title
|
||||
supportingText: root.supportingText
|
||||
acceptEnabled: root.text.trim() !== ""
|
||||
onAccepted: root.accepted(root.text.trim())
|
||||
onRejected: root.hide()
|
||||
onActiveChanged: {
|
||||
if (!active && !root._opened)
|
||||
root.active = false;
|
||||
}
|
||||
|
||||
function show() {
|
||||
nameInput.text = root.text;
|
||||
opened = true;
|
||||
forceActiveFocus();
|
||||
nameInput.forceActiveFocus();
|
||||
nameInput.selectAll();
|
||||
}
|
||||
|
||||
actions: [
|
||||
DankButton {
|
||||
text: I18n.tr("Cancel")
|
||||
backgroundColor: "transparent"
|
||||
textColor: Theme.primary
|
||||
onClicked: dialog.rejected()
|
||||
},
|
||||
DankButton {
|
||||
text: I18n.tr("Save")
|
||||
iconName: "check"
|
||||
enabled: dialog.acceptEnabled
|
||||
onClicked: dialog.accepted()
|
||||
}
|
||||
]
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
visible: status === Loader.Ready && item.visible
|
||||
sourceComponent: root.aboveField
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: nameInput
|
||||
outlined: true
|
||||
leftIconName: root.leftIconName
|
||||
labelText: root.labelText
|
||||
width: parent.width
|
||||
showClearButton: true
|
||||
onTextChanged: root.text = text
|
||||
}
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
visible: status === Loader.Ready && item.visible
|
||||
sourceComponent: root.belowField
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ SettingsRow {
|
||||
id: handle
|
||||
|
||||
coordinateItem: root.reorderList
|
||||
upDownKeysMove: false
|
||||
label: root.title
|
||||
enabled: root.reorderEnabled && (root.reorderList.externalDrag || root.reorderList.count > 1)
|
||||
dragging: root.dragging
|
||||
|
||||
@@ -52,13 +52,25 @@ T.Control {
|
||||
readonly property bool hasBody: bodySlot.height > 0
|
||||
readonly property bool hasText: title !== "" || subtitle !== ""
|
||||
readonly property bool isHighlighted: settingKey !== "" && SettingsSearchService.highlightSection === settingKey
|
||||
property bool highlighted: isHighlighted
|
||||
readonly property bool isFirstInGroup: _edge(true)
|
||||
readonly property bool isLastInGroup: _edge(false)
|
||||
property real topRadius: isFirstInGroup ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
|
||||
property real bottomRadius: isLastInGroup ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
|
||||
property real minHeight: subtitle !== "" ? Theme.listItemTwoLineHeight : Theme.listItemHeight
|
||||
// Keyboard focus on the row or any control inside it tints the row, so navigation reads without a focus ring
|
||||
readonly property Item activeItem: Window.activeFocusItem
|
||||
readonly property bool focusWithin: (activeItem?.visualFocus ?? false) && _contains(activeItem)
|
||||
|
||||
signal clicked
|
||||
function _contains(item) {
|
||||
for (let p = item; p; p = p.parent) {
|
||||
if (p === root)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
signal clicked(bool keyboard)
|
||||
signal resetRequested
|
||||
|
||||
onResetRequested: {
|
||||
@@ -72,7 +84,7 @@ T.Control {
|
||||
Accessible.description: subtitle
|
||||
Accessible.onPressAction: {
|
||||
if (clickable && enabled)
|
||||
clicked();
|
||||
clicked(true);
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
@@ -82,7 +94,7 @@ T.Control {
|
||||
case Qt.Key_Space:
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
root.clicked();
|
||||
root.clicked(true);
|
||||
event.accepted = true;
|
||||
break;
|
||||
}
|
||||
@@ -126,7 +138,7 @@ T.Control {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: root.paintBackground
|
||||
color: root.active ? Theme.selectedContainer : root.isHighlighted ? Theme.blend(root.rowColor, Theme.primary, SettingsMetrics.highlightBlend) : root.rowColor
|
||||
color: root.active ? Theme.selectedContainer : root.highlighted ? Theme.blend(root.rowColor, Theme.primary, SettingsMetrics.highlightBlend) : root.rowColor
|
||||
border.width: Theme.layerOutlineWidth
|
||||
border.color: Theme.outlineMedium
|
||||
topLeftRadius: root.topRadius
|
||||
@@ -137,7 +149,7 @@ T.Control {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !root.paintBackground && (root.active || root.isHighlighted)
|
||||
visible: !root.paintBackground && (root.active || root.highlighted)
|
||||
color: root.active ? Theme.selectedContainer : SettingsMetrics.rowHighlightColor
|
||||
topLeftRadius: root.topRadius
|
||||
topRightRadius: root.topRadius
|
||||
@@ -148,9 +160,9 @@ T.Control {
|
||||
Rectangle {
|
||||
id: stateLayer
|
||||
anchors.fill: parent
|
||||
visible: root.clickable
|
||||
visible: root.clickable || root.focusWithin
|
||||
color: root.contentColor
|
||||
opacity: !root.enabled ? 0 : (clickControl.down ? Theme.stateLayerPressed : (clickControl.hovered ? Theme.stateLayerHover : 0))
|
||||
opacity: !root.enabled ? 0 : (clickControl.down ? Theme.stateLayerPressed : root.focusWithin ? Theme.stateLayerFocus : (clickControl.hovered ? Theme.stateLayerHover : 0))
|
||||
topLeftRadius: root.topRadius
|
||||
topRightRadius: root.topRadius
|
||||
bottomLeftRadius: root.bottomRadius
|
||||
@@ -194,14 +206,16 @@ T.Control {
|
||||
anchors.fill: parent
|
||||
enabled: root.clickable && root.enabled
|
||||
hoverEnabled: root.clickable
|
||||
focusPolicy: Qt.ClickFocus
|
||||
focusPolicy: Qt.NoFocus
|
||||
background: null
|
||||
Accessible.ignored: true
|
||||
onPressedChanged: {
|
||||
if (pressed)
|
||||
if (pressed) {
|
||||
root.forceActiveFocus(Qt.MouseFocusReason);
|
||||
ripple.trigger(pressX, pressY);
|
||||
}
|
||||
}
|
||||
onClicked: root.clicked()
|
||||
onClicked: root.clicked(false)
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: root.clickable ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
|
||||
@@ -34,8 +34,9 @@ Item {
|
||||
}
|
||||
|
||||
FocusRing {
|
||||
id: labelRing
|
||||
radius: Theme.cornerRadiusS + Theme.focusRingOffset
|
||||
visible: root.activeFocus && root.collapsible
|
||||
visible: parentFocused && !pointerFocused && root.collapsible
|
||||
}
|
||||
|
||||
signal toggleRequested
|
||||
@@ -104,7 +105,12 @@ Item {
|
||||
StateLayer {
|
||||
stateColor: Theme.primary
|
||||
cornerRadius: parent.radius
|
||||
onClicked: root.toggleRequested()
|
||||
focused: labelRing.visible
|
||||
onClicked: {
|
||||
labelRing.pointerFocused = true;
|
||||
root.forceActiveFocus(Qt.MouseFocusReason);
|
||||
root.toggleRequested();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,20 +12,20 @@ Rectangle {
|
||||
property bool active: false
|
||||
property bool highlighted: false
|
||||
|
||||
signal clicked
|
||||
signal clicked(bool keyboard)
|
||||
|
||||
activeFocusOnTab: enabled
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: title
|
||||
Accessible.description: hint
|
||||
Accessible.onPressAction: clicked()
|
||||
Accessible.onPressAction: clicked(true)
|
||||
|
||||
Keys.onPressed: event => {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Space:
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
root.clicked();
|
||||
root.clicked(true);
|
||||
event.accepted = true;
|
||||
break;
|
||||
}
|
||||
@@ -83,7 +83,7 @@ Rectangle {
|
||||
bottomLeftRadius: root.bottomRadius
|
||||
bottomRightRadius: root.bottomRadius
|
||||
color: Theme.surfaceText
|
||||
opacity: mouseArea.pressed ? Theme.stateLayerPressed : (mouseArea.containsMouse || root.highlighted ? Theme.stateLayerHover : 0)
|
||||
opacity: mouseArea.pressed ? Theme.stateLayerPressed : focusRing.visible ? Theme.stateLayerFocus : (mouseArea.containsMouse || root.highlighted ? Theme.stateLayerHover : 0)
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
|
||||
@@ -169,7 +169,20 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => ripple.trigger(mouse.x, mouse.y)
|
||||
onClicked: root.clicked()
|
||||
onPressed: mouse => {
|
||||
focusRing.pointerFocused = true;
|
||||
root.forceActiveFocus(Qt.MouseFocusReason);
|
||||
ripple.trigger(mouse.x, mouse.y);
|
||||
}
|
||||
onClicked: root.clicked(false)
|
||||
}
|
||||
|
||||
FocusRing {
|
||||
id: focusRing
|
||||
anchors.margins: Theme.focusRingWidth / 2
|
||||
topLeftRadius: Math.max(0, root.topRadius - Theme.focusRingWidth / 2)
|
||||
topRightRadius: topLeftRadius
|
||||
bottomLeftRadius: Math.max(0, root.bottomRadius - Theme.focusRingWidth / 2)
|
||||
bottomRightRadius: bottomLeftRadius
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,7 @@ SettingsRow {
|
||||
|
||||
DankSlider {
|
||||
id: slider
|
||||
upDownKeysStep: false
|
||||
Accessible.name: root.text
|
||||
Accessible.description: root.description
|
||||
size: "s"
|
||||
|
||||
@@ -6,35 +6,37 @@ SettingsRow {
|
||||
id: root
|
||||
|
||||
property bool checked: false
|
||||
property bool navigable: true
|
||||
|
||||
signal navigated
|
||||
signal navigated(bool keyboard)
|
||||
signal toggled(bool checked)
|
||||
|
||||
clickable: true
|
||||
onClicked: navigated()
|
||||
clickable: navigable
|
||||
onClicked: keyboard => navigated(keyboard)
|
||||
|
||||
DankIcon {
|
||||
name: "chevron_right"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceVariantText
|
||||
rotation: I18n.isRtl ? 180 : 0
|
||||
visible: root.navigable
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Theme.dividerWidth
|
||||
height: SettingsMetrics.splitDividerHeight
|
||||
color: Theme.outlineVariant
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
SettingsDivider {
|
||||
vertical: true
|
||||
visible: root.navigable
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Theme.spacingS
|
||||
height: parent.height
|
||||
visible: root.navigable
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
hideText: true
|
||||
text: root.title
|
||||
checked: root.checked
|
||||
enabled: root.enabled
|
||||
onToggled: value => root.toggled(value)
|
||||
|
||||
@@ -13,7 +13,10 @@ Flow {
|
||||
property bool compact: false
|
||||
property real maxHeight: 0
|
||||
property real minTileWidth: compact ? Theme.minimumTouchTargetSize + Theme.spacingM : SettingsMetrics.swatchTileMinWidth
|
||||
// option => {preview, badge, primary, secondary, tertiary}, re-evaluated per tile so `options` stays stable across theme changes
|
||||
property var resolveOption: null
|
||||
signal selected(string value)
|
||||
signal deleteRequested(string value)
|
||||
|
||||
readonly property int columns: Math.max(1, Math.floor((width + spacing) / (minTileWidth + spacing)))
|
||||
readonly property real tileWidth: Math.floor((width - spacing * (columns - 1)) / columns)
|
||||
@@ -31,54 +34,114 @@ Flow {
|
||||
id: tile
|
||||
required property var modelData
|
||||
|
||||
readonly property var option: root.resolveOption ? Object.assign({}, modelData, root.resolveOption(modelData)) : modelData
|
||||
readonly property bool isActive: root.currentValue === modelData.value
|
||||
readonly property bool hasPreview: !root.compact && !!option.preview
|
||||
readonly property bool deletable: !!modelData.deletable
|
||||
|
||||
width: root.tileWidth
|
||||
height: root.compact ? root.compactTileHeight : swatch.height + label.implicitHeight + Theme.spacingS * 3
|
||||
height: root.compact ? root.compactTileHeight : media.height + label.implicitHeight + Theme.spacingS * 3
|
||||
radius: Theme.cornerRadiusM
|
||||
color: Theme.floatingWindowFieldColor
|
||||
border.width: isActive ? Theme.outlineWidthFocused : Theme.layerOutlineWidth
|
||||
border.color: isActive ? Theme.primary : Theme.outlineMedium
|
||||
|
||||
activeFocusOnTab: true
|
||||
Accessible.role: Accessible.RadioButton
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: modelData.label
|
||||
Accessible.checkable: true
|
||||
Accessible.checked: isActive
|
||||
Accessible.onPressAction: root.selected(modelData.value)
|
||||
Keys.onSpacePressed: root.selected(modelData.value)
|
||||
Keys.onEnterPressed: root.selected(modelData.value)
|
||||
Keys.onReturnPressed: root.selected(modelData.value)
|
||||
|
||||
FocusRing {}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
opacity: tileMouse.containsMouse ? Theme.stateLayerHover : 0
|
||||
Keys.onDeletePressed: event => {
|
||||
if (!tile.deletable)
|
||||
return;
|
||||
root.deleteRequested(tile.modelData.value);
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
DankPaletteSwatch {
|
||||
id: swatch
|
||||
width: Theme.minimumTouchTargetSize
|
||||
height: Theme.minimumTouchTargetSize
|
||||
FocusRing {
|
||||
id: tileRing
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: tileState
|
||||
anchors.fill: parent
|
||||
stateColor: Theme.primary
|
||||
focused: tileRing.visible
|
||||
tooltipText: root.compact ? tile.modelData.label : null
|
||||
onClicked: {
|
||||
tileRing.pointerFocused = true;
|
||||
tile.forceActiveFocus(Qt.MouseFocusReason);
|
||||
root.selected(tile.modelData.value);
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: media
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
primaryColor: tile.modelData.primary
|
||||
secondaryColor: tile.modelData.secondary ?? tile.modelData.primary
|
||||
tertiaryColor: tile.modelData.tertiary ?? tile.modelData.secondary ?? tile.modelData.primary
|
||||
width: tile.hasPreview ? parent.width - Theme.spacingS * 2 : Theme.minimumTouchTargetSize
|
||||
height: tile.hasPreview ? Math.round(width * SettingsMetrics.choiceCardPreviewRatio) : Theme.minimumTouchTargetSize
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: tile.hasPreview
|
||||
radius: Theme.cornerRadiusS
|
||||
color: Theme.foregroundColor(Theme.chipSurfaceNested, true)
|
||||
clip: true
|
||||
|
||||
Image {
|
||||
id: previewImage
|
||||
anchors.fill: parent
|
||||
source: tile.hasPreview ? "file://" + tile.option.preview : ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
mipmap: true
|
||||
}
|
||||
}
|
||||
|
||||
DankPaletteSwatch {
|
||||
anchors.centerIn: parent
|
||||
width: Theme.minimumTouchTargetSize
|
||||
height: Theme.minimumTouchTargetSize
|
||||
visible: !tile.hasPreview || previewImage.status !== Image.Ready
|
||||
primaryColor: tile.option.primary
|
||||
secondaryColor: tile.option.secondary ?? tile.option.primary
|
||||
tertiaryColor: tile.option.tertiary ?? tile.option.secondary ?? tile.option.primary
|
||||
}
|
||||
}
|
||||
|
||||
DankTooltipHost {
|
||||
text: root.compact ? tile.modelData.label : null
|
||||
target: tile
|
||||
hoverArea: tileMouse
|
||||
DankBadge {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Theme.spacingXS
|
||||
visible: !root.compact && !!tile.option.badge
|
||||
text: tile.option.badge ?? ""
|
||||
color: Theme.secondaryContainer
|
||||
textColor: Theme.onSecondaryContainer
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingXS
|
||||
visible: tile.deletable && !root.compact && (tile.activeFocus || activeFocus || hovered || tileState.containsMouse)
|
||||
iconName: "delete"
|
||||
iconColor: Theme.error
|
||||
backgroundColor: Theme.floatingWindowFieldColor
|
||||
tooltipText: I18n.tr("Delete")
|
||||
onClicked: root.deleteRequested(tile.modelData.value)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: label
|
||||
visible: !root.compact
|
||||
anchors.top: swatch.bottom
|
||||
anchors.top: media.bottom
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
@@ -93,14 +156,6 @@ Flow {
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tileMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.selected(tile.modelData.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ SettingsRow {
|
||||
Accessible.role: Accessible.CheckBox
|
||||
Accessible.checkable: true
|
||||
Accessible.checked: checked
|
||||
Accessible.onToggleAction: root.clicked()
|
||||
Accessible.onToggleAction: root.clicked(true)
|
||||
onClicked: {
|
||||
if (!enabled || toggling)
|
||||
return;
|
||||
|
||||
@@ -127,6 +127,12 @@ Item {
|
||||
return;
|
||||
var ctrl = (event.modifiers & Qt.ControlModifier) !== 0;
|
||||
if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
|
||||
var focusedRow = focusedWidgetRow();
|
||||
if (focusedRow) {
|
||||
highlightedSection = focusedRow.section;
|
||||
highlightedId = focusedRow.id;
|
||||
}
|
||||
widgetsTab.forceActiveFocus();
|
||||
var dir = event.key === Qt.Key_Down ? 1 : -1;
|
||||
if (ctrl) {
|
||||
if (highlightedId !== "")
|
||||
@@ -154,11 +160,16 @@ Item {
|
||||
if (highlightedId !== "")
|
||||
moveAcrossSections(highlightedSection, highlightedId, event.key === Qt.Key_Right ? 1 : -1);
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Space || event.key === Qt.Key_Return) {
|
||||
} else if (event.key === Qt.Key_Space) {
|
||||
if (highlightedId !== "") {
|
||||
toggleHighlighted();
|
||||
event.accepted = true;
|
||||
}
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (highlightedId !== "") {
|
||||
configureHighlighted();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,6 +391,23 @@ Item {
|
||||
SettingsData.updateBarConfig(selectedBarId, updates);
|
||||
}
|
||||
|
||||
// A row reached with Tab has focus but no cursor, so the cursor starts from it
|
||||
function focusedWidgetRow() {
|
||||
for (var item = Window.activeFocusItem; item; item = item.parent) {
|
||||
if (item.reorderList === undefined || !item.modelData?.id)
|
||||
continue;
|
||||
for (var section = item.parent; section; section = section.parent) {
|
||||
if (section.sectionId !== undefined)
|
||||
return {
|
||||
"section": section.sectionId,
|
||||
"id": item.modelData.id
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function flatList() {
|
||||
var out = [];
|
||||
["left", "center", "right"].forEach(s => {
|
||||
@@ -425,6 +453,15 @@ Item {
|
||||
handleItemEnabledChanged(highlightedSection, highlightedId, !en);
|
||||
}
|
||||
|
||||
function configureHighlighted() {
|
||||
const index = getWidgetsForSection(highlightedSection).findIndex(x => (typeof x === "string" ? x : x.id) === highlightedId);
|
||||
if (index < 0)
|
||||
return;
|
||||
if (!BarWidgetCatalog.configurable(getItemsForSection(highlightedSection)[index]))
|
||||
return;
|
||||
configureWidget(highlightedSection, index);
|
||||
}
|
||||
|
||||
function handleSpacerSizeChanged(sectionId, widgetIndex, newSize) {
|
||||
var widgets = getWidgetsForSection(sectionId).slice();
|
||||
if (widgetIndex < 0 || widgetIndex >= widgets.length)
|
||||
@@ -536,6 +573,10 @@ Item {
|
||||
SettingsPage {
|
||||
id: mainColumn
|
||||
|
||||
TapHandler {
|
||||
onTapped: widgetsTab.forceActiveFocus()
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
iconName: "toolbar"
|
||||
title: I18n.tr("Bar")
|
||||
@@ -543,6 +584,7 @@ Item {
|
||||
|
||||
SettingsRow {
|
||||
body: DankButtonGroup {
|
||||
arrowKeysSelect: false
|
||||
id: barSelectorGroup
|
||||
width: parent.width
|
||||
model: SettingsData.barConfigs.map(cfg => cfg.name || ("Bar " + (SettingsData.barConfigs.indexOf(cfg) + 1)))
|
||||
|
||||
@@ -69,10 +69,10 @@ Column {
|
||||
|
||||
required property var modelData
|
||||
|
||||
readonly property bool configurable: BarWidgetCatalog.hasOptions(modelData.id) || !!modelData.pluginId
|
||||
readonly property bool highlighted: root.highlightedId === modelData.id && root.highlightedSection === root.sectionId
|
||||
readonly property bool configurable: BarWidgetCatalog.configurable(modelData)
|
||||
|
||||
reorderList: reorderArea
|
||||
highlighted: !dragging && root.highlightedId === modelData.id && root.highlightedSection === root.sectionId
|
||||
opacity: dragging && reorderArea.crossSectionActive ? 0 : 1
|
||||
title: modelData.text
|
||||
iconName: modelData.icon
|
||||
@@ -125,12 +125,9 @@ Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Theme.dividerWidth
|
||||
height: SettingsMetrics.splitDividerHeight
|
||||
color: Theme.outlineVariant
|
||||
SettingsDivider {
|
||||
vertical: true
|
||||
visible: widgetRow.configurable
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankNumberStepper {
|
||||
@@ -158,20 +155,6 @@ Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: root.removeWidget(root.sectionId, widgetRow.index)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
parent: widgetRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.focusRingWidth / 2
|
||||
topLeftRadius: Math.max(0, widgetRow.topRadius - Theme.focusRingWidth / 2)
|
||||
topRightRadius: topLeftRadius
|
||||
bottomLeftRadius: Math.max(0, widgetRow.bottomRadius - Theme.focusRingWidth / 2)
|
||||
bottomRightRadius: bottomLeftRadius
|
||||
color: "transparent"
|
||||
border.width: Theme.focusRingWidth
|
||||
border.color: Theme.focusRingColor
|
||||
visible: widgetRow.highlighted && !widgetRow.dragging
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,16 @@ Item {
|
||||
return [rule.matchCriteria || {}];
|
||||
}
|
||||
|
||||
function actionChips(actions) {
|
||||
const a = actions || {};
|
||||
return Object.keys(a).filter(k => a[k] !== undefined && a[k] !== null && a[k] !== "").map(k => {
|
||||
const label = root.actionLabels[k] || k;
|
||||
if (typeof a[k] === "boolean")
|
||||
return a[k] ? label : label + ": " + I18n.tr("Off");
|
||||
return label + ": " + a[k];
|
||||
});
|
||||
}
|
||||
|
||||
function formatCriteria(obj, labels) {
|
||||
let out = [];
|
||||
const keys = Object.keys(obj || {});
|
||||
@@ -333,6 +343,7 @@ Item {
|
||||
|
||||
DankDropdown {
|
||||
id: windowSelector
|
||||
downKeyOpens: false
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
dropdownWidth: Math.min(400, createRuleRow.width - SettingsMetrics.rowPaddingH * 2)
|
||||
compactMode: true
|
||||
@@ -373,10 +384,9 @@ Item {
|
||||
|
||||
DankIcon {
|
||||
name: "select_window"
|
||||
size: 40
|
||||
size: Theme.iconSizeLarge
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
opacity: 0.5
|
||||
}
|
||||
|
||||
StyledText {
|
||||
@@ -442,35 +452,15 @@ Item {
|
||||
|
||||
Repeater {
|
||||
id: actionRepeater
|
||||
model: {
|
||||
const actions = ruleRow.liveRuleData.actions || {};
|
||||
const labels = root.actionLabels;
|
||||
return Object.keys(actions).filter(key => actions[key] !== undefined && actions[key] !== null && actions[key] !== "").map(key => {
|
||||
const value = actions[key];
|
||||
if (typeof value === "boolean")
|
||||
return value ? (labels[key] || key) : (labels[key] || key) + ": " + I18n.tr("Off");
|
||||
return (labels[key] || key) + ": " + value;
|
||||
});
|
||||
}
|
||||
model: root.actionChips(ruleRow.liveRuleData.actions)
|
||||
|
||||
delegate: Rectangle {
|
||||
delegate: DankBadge {
|
||||
required property string modelData
|
||||
|
||||
width: Math.min(parent?.width ?? 0, chipText.implicitWidth + Theme.spacingS * 2)
|
||||
height: chipText.height + Theme.spacingXS * 2
|
||||
radius: Theme.cornerRadiusS
|
||||
maximumWidth: parent?.width ?? 0
|
||||
text: modelData
|
||||
color: Theme.primaryContainer
|
||||
|
||||
StyledText {
|
||||
id: chipText
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(implicitWidth, parent.width - Theme.spacingS * 2)
|
||||
text: modelData
|
||||
font.weight: Theme.fontWeightMedium
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.onPrimaryContainer
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
textColor: Theme.onPrimaryContainer
|
||||
tooltipText: truncated ? modelData : null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -536,25 +526,21 @@ Item {
|
||||
visible: externalCard.sourceFile.length > 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: externalCard.sourceFile
|
||||
color: Theme.withAlpha(Theme.surfaceVariantText, 0.15)
|
||||
color: Theme.floatingWindowFieldColor
|
||||
textColor: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: externalCard.expanded ? "expand_less" : "expand_more"
|
||||
size: 20
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
buttonSize: 28
|
||||
iconName: "content_copy"
|
||||
iconSize: 16
|
||||
backgroundColor: "transparent"
|
||||
iconColor: Theme.surfaceVariantText
|
||||
enabled: !root.readOnly
|
||||
opacity: enabled ? 1 : 0.5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
tooltipText: I18n.tr("Convert to DMS")
|
||||
tooltipSide: "left"
|
||||
@@ -572,22 +558,15 @@ Item {
|
||||
visible: externalCard.hasActions
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
const a = externalCard.modelData.actions || {};
|
||||
const labels = root.actionLabels;
|
||||
return Object.keys(a).filter(k => a[k] !== undefined && a[k] !== null && a[k] !== "").map(k => {
|
||||
const val = a[k];
|
||||
if (typeof val === "boolean")
|
||||
return val ? (labels[k] || k) : (labels[k] || k) + ": " + I18n.tr("Off");
|
||||
return (labels[k] || k) + ": " + val;
|
||||
});
|
||||
}
|
||||
model: root.actionChips(externalCard.modelData.actions)
|
||||
|
||||
delegate: DankBadge {
|
||||
required property string modelData
|
||||
maximumWidth: parent?.width ?? 0
|
||||
text: modelData
|
||||
color: Theme.withAlpha(Theme.primary, 0.15)
|
||||
textColor: Theme.primary
|
||||
color: Theme.primaryContainer
|
||||
textColor: Theme.onPrimaryContainer
|
||||
tooltipText: truncated ? modelData : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,6 +469,15 @@ Singleton {
|
||||
}
|
||||
|
||||
function updatePlugin(pluginId, callback) {
|
||||
// Installed entries carry the registry's requires_dms, i.e. the version an update would fetch
|
||||
const installed = (DMSService.installedPlugins || []).find(plugin => plugin.id === pluginId);
|
||||
if (installed?.requires_dms && !checkPluginCompatibility(installed.requires_dms)) {
|
||||
if (callback)
|
||||
callback({
|
||||
error: I18n.tr("Requires DMS %1", "plugin incompatibility notice, %1 is the required DMS version").arg(installed.requires_dms)
|
||||
});
|
||||
return;
|
||||
}
|
||||
DMSService.update(pluginId, response => {
|
||||
if (!response.error)
|
||||
forceRescanPlugin(pluginId);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
DankCommon.DankColorButton {}
|
||||
@@ -0,0 +1,3 @@
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
DankCommon.DankDetailChip {}
|
||||
@@ -1,3 +1,14 @@
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
DankCommon.FocusRing {}
|
||||
DankCommon.FocusRing {
|
||||
// Plain Items have no visualFocus; a tile sets this on its own press so a mouse click draws no ring
|
||||
property bool pointerFocused: false
|
||||
readonly property bool parentFocused: parent?.activeFocus ?? false
|
||||
|
||||
visible: parentFocused && !pointerFocused
|
||||
|
||||
onParentFocusedChanged: {
|
||||
if (!parentFocused)
|
||||
pointerFocused = false;
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,8 @@ function service({ enabled = true, type = "widget" } = {}) {
|
||||
log: { error() {}, warn() {} },
|
||||
Paths: { toFileUrl: path => path.startsWith("file://") ? path : "file://" + path },
|
||||
SettingsData: { getPluginSetting: () => enabled },
|
||||
DMSService: { update: (id, callback) => requests.push(callback) },
|
||||
DMSService: { update: (id, callback) => requests.push(callback), installedPlugins: [] },
|
||||
ShellVersionService: { checkVersionRequirement: requirement => !requirement.startsWith(">=99"), getParsedShellVersion: () => ({}) },
|
||||
Component: { Error: 3, PreferSynchronous: 0 },
|
||||
I18n: { tr: text => ({ arg: value => text.replace("%1", value) }) },
|
||||
ToastService: { showError() {} },
|
||||
@@ -67,6 +68,19 @@ test("successful updates replace cached components and reread the manifest", ()
|
||||
assert.equal(context.availablePlugins.fixture.version, "2.0.0");
|
||||
});
|
||||
|
||||
test("an update whose registry entry needs a newer shell is refused before the download", () => {
|
||||
const { context, requests, pending } = service();
|
||||
context.DMSService.installedPlugins = [{ id: "fixture", requires_dms: ">=99.0.0" }];
|
||||
let response;
|
||||
context.updatePlugin("fixture", value => response = value);
|
||||
assert.equal(requests.length, 0);
|
||||
assert.equal(pending.length, 0);
|
||||
assert.match(response.error, /99\.0\.0/);
|
||||
context.DMSService.installedPlugins = [{ id: "fixture", requires_dms: ">=1.0.0" }];
|
||||
context.updatePlugin("fixture");
|
||||
assert.equal(requests.length, 1);
|
||||
});
|
||||
|
||||
test("failed updates preserve the running plugin and forward the error", () => {
|
||||
const { context, requests, pending } = service();
|
||||
const component = context.pluginWidgetComponents.fixture;
|
||||
|
||||
@@ -0,0 +1,521 @@
|
||||
import QtQuick
|
||||
import QtTest
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Modals.Settings
|
||||
import qs.DankCommon.Common as DC
|
||||
import qs.Modules.Settings.Widgets
|
||||
import qs.Widgets
|
||||
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
property bool failed: false
|
||||
property int sidebarRequests: 0
|
||||
property string sidebarRequestedPage: ""
|
||||
|
||||
function find(item, predicate) {
|
||||
if (!item)
|
||||
return null;
|
||||
if (predicate(item))
|
||||
return item;
|
||||
for (const child of item.children ?? []) {
|
||||
const found = find(child, predicate);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function inside(ancestor, item) {
|
||||
for (let p = item; p; p = p.parent) {
|
||||
if (p === ancestor)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function check(condition, label) {
|
||||
if (condition)
|
||||
return;
|
||||
failed = true;
|
||||
console.log("FIXTURE_FAIL " + label);
|
||||
}
|
||||
|
||||
function waitFor(condition, label) {
|
||||
try {
|
||||
input.tryVerify(condition, 20000);
|
||||
} catch (error) {
|
||||
throw new Error("timed out: " + label);
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: modal
|
||||
property bool shouldBeVisible: true
|
||||
property bool isCompactMode: false
|
||||
property bool menuVisible: true
|
||||
property bool canGoBack: false
|
||||
readonly property bool searchFocused: sidebar.searchFocused
|
||||
readonly property string focusPane: navigation.activePane
|
||||
readonly property Item modalFocusScope: navigation
|
||||
property var pageHistory: []
|
||||
property int backs: 0
|
||||
property int searches: 0
|
||||
property int pageFocuses: 0
|
||||
function goBack() {
|
||||
backs++;
|
||||
}
|
||||
function focusSearch() {
|
||||
searches++;
|
||||
navigation.focusSearch();
|
||||
}
|
||||
function focusSidebar() {
|
||||
searches++;
|
||||
navigation.focusSidebar();
|
||||
}
|
||||
function focusCurrentPage() {
|
||||
pageFocuses++;
|
||||
navigation.focusContent();
|
||||
}
|
||||
function toggleMenu() {
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
implicitWidth: 900
|
||||
implicitHeight: 700
|
||||
|
||||
TestCase {
|
||||
id: input
|
||||
name: "settings-keyboard"
|
||||
when: false
|
||||
}
|
||||
|
||||
SettingsPaneNavigation {
|
||||
id: navigation
|
||||
anchors.fill: parent
|
||||
sidebar: sidebar
|
||||
content: content
|
||||
parentModal: modal
|
||||
|
||||
SettingsSidebar {
|
||||
id: sidebar
|
||||
width: 300
|
||||
height: parent.height
|
||||
parentModal: modal
|
||||
onPageRequested: pageId => {
|
||||
root.sidebarRequests++;
|
||||
root.sidebarRequestedPage = pageId;
|
||||
}
|
||||
}
|
||||
|
||||
SettingsContent {
|
||||
id: content
|
||||
anchors.left: sidebar.right
|
||||
anchors.right: parent.right
|
||||
height: parent.height
|
||||
parentModal: modal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function run() {
|
||||
waitFor(() => CompositorService.compositor !== "unknown", "compositor detected");
|
||||
waitFor(() => content.Window.active, "settings window takes keyboard focus");
|
||||
SettingsData.animationDuration = 0;
|
||||
content.currentPage = "notifications";
|
||||
waitFor(() => find(content.currentPageItem, item => item.upDownKeysStep !== undefined && item.visible) !== null, "notifications page loads");
|
||||
const page = content.currentPageItem;
|
||||
const focused = () => content.Window.activeFocusItem;
|
||||
|
||||
page.forceActiveFocus();
|
||||
const before = focused();
|
||||
input.keyClick(Qt.Key_Down);
|
||||
const first = focused();
|
||||
check(first !== before && inside(page, first), "Down moves focus to a control on the page");
|
||||
input.keyClick(Qt.Key_Down);
|
||||
const second = focused();
|
||||
check(second !== first && inside(page, second), "Down moves to the next control");
|
||||
input.keyClick(Qt.Key_Up);
|
||||
check(focused() === first, "Up returns to the previous control");
|
||||
|
||||
const slider = find(page, item => item.upDownKeysStep === false && item.visible && item.enabled);
|
||||
slider.forceActiveFocus(Qt.TabFocusReason);
|
||||
const value = slider.value;
|
||||
input.keyClick(Qt.Key_Down);
|
||||
check(slider.value === value && !inside(slider, focused()), "Down leaves a settings slider without stepping it");
|
||||
|
||||
const dropdown = find(page, item => item.downKeyOpens === false && item.visible && item.enabled);
|
||||
dropdown.forceActiveFocus(Qt.TabFocusReason);
|
||||
input.keyClick(Qt.Key_Down);
|
||||
check(!dropdown.menuOpen && !inside(dropdown, focused()), "Down leaves a settings dropdown without opening it");
|
||||
|
||||
page.forceActiveFocus();
|
||||
input.keyClick(Qt.Key_Escape);
|
||||
check(modal.searches === 1 && modal.backs === 0, "Escape on a top-level page returns to the sidebar");
|
||||
modal.canGoBack = true;
|
||||
page.forceActiveFocus();
|
||||
input.keyClick(Qt.Key_Escape);
|
||||
input.keyClick(Qt.Key_Left, Qt.AltModifier);
|
||||
check(modal.backs === 2, "Escape and Alt+Left go back from a sub-page");
|
||||
|
||||
SettingsData.desktopWidgetInstances = [
|
||||
{
|
||||
id: "a",
|
||||
widgetType: "desktopClock",
|
||||
name: "A",
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: "b",
|
||||
widgetType: "desktopClock",
|
||||
name: "B",
|
||||
enabled: true
|
||||
}
|
||||
];
|
||||
content.currentPage = "desktop_widgets";
|
||||
const isHandle = item => item.upDownKeysMove === false && item.visible && item.enabled;
|
||||
waitFor(() => find(content.currentPageItem, isHandle) !== null, "desktop widgets page shows reorder handles");
|
||||
const handle = find(content.currentPageItem, isHandle);
|
||||
handle.forceActiveFocus(Qt.TabFocusReason);
|
||||
input.keyClick(Qt.Key_Down);
|
||||
check(SettingsData.desktopWidgetInstances[0].id === "a" && !handle.activeFocus, "Down leaves a reorder handle without moving its item");
|
||||
|
||||
const deleteButton = find(content.currentPageItem, item => item.confirming !== undefined && item.visible);
|
||||
deleteButton.forceActiveFocus(Qt.TabFocusReason);
|
||||
input.keyClick(Qt.Key_Space);
|
||||
check(deleteButton.confirming && SettingsData.desktopWidgetInstances.length === 2, "first Space only arms the delete button");
|
||||
input.keyClick(Qt.Key_Tab);
|
||||
check(!deleteButton.confirming, "leaving an armed delete button disarms it");
|
||||
deleteButton.forceActiveFocus(Qt.TabFocusReason);
|
||||
input.keyClick(Qt.Key_Space);
|
||||
input.keyClick(Qt.Key_Space);
|
||||
check(SettingsData.desktopWidgetInstances.length === 1, "second Space deletes exactly once");
|
||||
|
||||
SettingsData.desktopWidgetGroups = [
|
||||
{
|
||||
id: "g",
|
||||
name: "Group"
|
||||
}
|
||||
];
|
||||
const isRename = item => item.iconName === "edit" && item.tooltipText === "Rename" && item.visible;
|
||||
waitFor(() => find(content.currentPageItem, isRename) !== null, "desktop widgets page shows the group rename button");
|
||||
const renameButton = find(content.currentPageItem, isRename);
|
||||
renameButton.forceActiveFocus(Qt.TabFocusReason);
|
||||
renameButton.clicked();
|
||||
waitFor(() => focused() !== renameButton && focused()?.cursorPosition !== undefined, "rename dialog focuses its field");
|
||||
const dialogField = focused();
|
||||
let renameDialog = dialogField;
|
||||
while (renameDialog && renameDialog.Accessible.role !== Accessible.Dialog)
|
||||
renameDialog = renameDialog.parent;
|
||||
input.keyClick(Qt.Key_Tab, Qt.ControlModifier);
|
||||
check(inside(renameDialog, focused()), "pane shortcut stays inside the rename dialog");
|
||||
input.keyClick(Qt.Key_F, Qt.ControlModifier);
|
||||
check(inside(renameDialog, focused()), "Search shortcut does not leave the rename dialog");
|
||||
dialogField.forceActiveFocus();
|
||||
input.keyClick(Qt.Key_Escape);
|
||||
check(focused() === renameButton, "closing the rename dialog returns focus to the rename button");
|
||||
|
||||
SettingsData.barConfigs = [
|
||||
{
|
||||
id: "bar",
|
||||
enabled: true,
|
||||
visible: true,
|
||||
position: 0,
|
||||
leftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow", "clock"],
|
||||
centerWidgets: [],
|
||||
rightWidgets: []
|
||||
}
|
||||
];
|
||||
SettingsUiState.selectedBarId = "bar";
|
||||
content.currentPage = "dankbar_widgets";
|
||||
const isThirdRow = item => item.reorderList !== undefined && item.modelData?.id === "focusedWindow";
|
||||
waitFor(() => find(content.currentPageItem, isThirdRow) !== null, "bar widgets page lists the rows");
|
||||
const widgetsTab = find(content.currentPageItem, item => item.focusedWidgetRow !== undefined);
|
||||
find(content.currentPageItem, isThirdRow).forceActiveFocus(Qt.TabFocusReason);
|
||||
input.keyClick(Qt.Key_Down);
|
||||
check(widgetsTab.highlightedSection === "left" && widgetsTab.highlightedId === "clock", "Down from a Tab-focused widget row moves the cursor to the next row");
|
||||
|
||||
sidebar.focusSearch();
|
||||
waitFor(() => focused()?.cursorPosition !== undefined && inside(sidebar, focused()), "sidebar search takes focus");
|
||||
input.keyClick(Qt.Key_Tab);
|
||||
const afterTab = focused();
|
||||
check(inside(sidebar, afterTab) && afterTab.hint !== undefined && modal.pageFocuses === 0, "Tab from the sidebar search reaches the first sidebar row");
|
||||
|
||||
const sidebarScroll = find(sidebar, item => item.contentY !== undefined && typeof item.flick === "function");
|
||||
const categoryIds = sidebar.navigableIds();
|
||||
const originalSidebarHeight = sidebar.height;
|
||||
const originalFocusRings = SettingsData.focusRingEnabled;
|
||||
sidebar.height = 400;
|
||||
sidebar.currentPage = categoryIds[0];
|
||||
sidebar.activeCategoryId = categoryIds[0];
|
||||
|
||||
function checkCategory(id) {
|
||||
input.wait(0);
|
||||
const row = focused();
|
||||
check(inside(sidebar, row) && row.modelData?.id === id, "Tab focuses sidebar category " + id);
|
||||
const top = row.mapToItem(sidebarScroll.contentItem, 0, 0).y;
|
||||
check(top >= sidebarScroll.contentY && top + row.height <= sidebarScroll.contentY + sidebarScroll.height, "focused sidebar category stays in view: " + id);
|
||||
return row;
|
||||
}
|
||||
|
||||
// Rings off is the harder case: focus must still read through the row tint alone
|
||||
SettingsData.focusRingEnabled = false;
|
||||
sidebar.focusSearch();
|
||||
input.keyClick(Qt.Key_Tab);
|
||||
const profile = focused();
|
||||
check(profile === afterTab && profile.highlighted, "profile row shows keyboard focus");
|
||||
input.keyClick(Qt.Key_Tab);
|
||||
const firstRow = checkCategory(categoryIds[0]);
|
||||
const ring = find(firstRow, item => item.pointerFocused !== undefined);
|
||||
check(ring.visible, "sidebar category shows focus with rings off");
|
||||
for (const id of categoryIds.slice(1)) {
|
||||
input.keyClick(Qt.Key_Tab);
|
||||
checkCategory(id);
|
||||
}
|
||||
check(sidebar.currentPage === categoryIds[0] && root.sidebarRequests === 0, "Tab preserves the selected page");
|
||||
input.keyClick(Qt.Key_Tab);
|
||||
check(inside(content, focused()), "Tab leaves the sidebar for page controls");
|
||||
input.keyClick(Qt.Key_Backtab, Qt.ShiftModifier);
|
||||
checkCategory(categoryIds[categoryIds.length - 1]);
|
||||
for (let i = categoryIds.length - 2; i >= 0; i--) {
|
||||
input.keyClick(Qt.Key_Backtab, Qt.ShiftModifier);
|
||||
checkCategory(categoryIds[i]);
|
||||
}
|
||||
input.keyClick(Qt.Key_Backtab, Qt.ShiftModifier);
|
||||
check(focused() === profile && sidebarScroll.contentY === 0, "Shift+Tab reveals the profile row again");
|
||||
input.keyClick(Qt.Key_Backtab, Qt.ShiftModifier);
|
||||
check(focused()?.cursorPosition !== undefined && inside(sidebar, focused()), "Shift+Tab returns to search");
|
||||
|
||||
input.keyClick(Qt.Key_Tab);
|
||||
input.keyClick(Qt.Key_Tab);
|
||||
input.keyClick(Qt.Key_Return);
|
||||
check(root.sidebarRequests === 1 && root.sidebarRequestedPage === categoryIds[0], "Enter opens the Tab-focused category exactly once");
|
||||
waitFor(() => inside(content.currentPageItem, focused()), "category Enter actually enters the page");
|
||||
sidebar.focusSearch();
|
||||
focused().text = "bar";
|
||||
waitFor(() => SettingsSearchService.results.length > 1, "sidebar search finds several results");
|
||||
const resultRequests = root.sidebarRequests;
|
||||
input.keyClick(Qt.Key_Tab, Qt.ControlModifier);
|
||||
waitFor(() => inside(content.currentPageItem, focused()), "Ctrl+Tab leaves a populated Search without selecting a result");
|
||||
check(root.sidebarRequests === resultRequests, "pane switching does not activate Search results");
|
||||
input.keyClick(Qt.Key_Backtab, Qt.ControlModifier | Qt.ShiftModifier);
|
||||
check(sidebar.searchFocused && sidebar.searchActive, "pane switching restores the populated Search");
|
||||
input.keyClick(Qt.Key_Tab);
|
||||
check(sidebar.searchSelectedIndex === 1 && focused()?.cursorPosition !== undefined, "Tab in a populated search advances its result cursor");
|
||||
input.keyClick(Qt.Key_Escape);
|
||||
check(!sidebar.searchActive, "Escape clears search before normal Tab navigation resumes");
|
||||
input.keyClick(Qt.Key_Down);
|
||||
check(sidebar.keyboardHighlightId === categoryIds[1] && focused()?.cursorPosition !== undefined, "Down in empty search browses categories without moving focus");
|
||||
input.keyClick(Qt.Key_Tab);
|
||||
check(sidebar.keyboardHighlightId === categoryIds[2] && focused()?.cursorPosition !== undefined, "Tab advances an existing sidebar row cursor");
|
||||
input.keyClick(Qt.Key_Escape);
|
||||
input.keyClick(Qt.Key_Tab);
|
||||
check(focused() === afterTab, "clearing the sidebar cursor restores the normal Tab chain");
|
||||
sidebar.height = originalSidebarHeight;
|
||||
SettingsData.focusRingEnabled = originalFocusRings;
|
||||
|
||||
sidebar.focusSearch();
|
||||
waitFor(() => focused()?.cursorPosition !== undefined && inside(sidebar, focused()), "sidebar search takes focus again");
|
||||
input.keyClick(Qt.Key_Return);
|
||||
waitFor(() => inside(content.currentPageItem, focused()), "empty Search Enter actually focuses the page");
|
||||
check(modal.pageFocuses === 2, "Enter in the empty sidebar search requests page focus once");
|
||||
|
||||
checkPaneNavigation(categoryIds, afterTab, focused);
|
||||
}
|
||||
|
||||
function checkPaneNavigation(categoryIds, profile, focused) {
|
||||
sidebar.focusSearch();
|
||||
input.keyClick(Qt.Key_Tab);
|
||||
input.keyClick(Qt.Key_Down);
|
||||
check(focused()?.modelData?.id === categoryIds[0], "Down after Tab moves from profile to the first category");
|
||||
input.keyClick(Qt.Key_Down);
|
||||
const category = focused();
|
||||
check(category.modelData?.id === categoryIds[1], "Down moves category focus without opening it");
|
||||
const requestsBefore = sidebarRequests;
|
||||
input.keyClick(Qt.Key_Up);
|
||||
check(focused()?.modelData?.id === categoryIds[0] && sidebarRequests === requestsBefore, "Up changes focus without activating a page");
|
||||
input.keyClick(Qt.Key_Down);
|
||||
input.keyClick(Qt.Key_Tab, Qt.ControlModifier);
|
||||
waitFor(() => inside(content.currentPageItem, focused()), "Ctrl+Tab transfers actual sidebar focus into content");
|
||||
const pageControl = focused();
|
||||
input.keyClick(Qt.Key_Backtab, Qt.ControlModifier | Qt.ShiftModifier);
|
||||
check(focused() === category, "Ctrl+Shift+Tab restores the sidebar row");
|
||||
input.keyClick(Qt.Key_Tab, Qt.ControlModifier);
|
||||
waitFor(() => focused() === pageControl, "Ctrl+Tab restores the content control");
|
||||
input.waitForPolish(content.Window.window);
|
||||
input.mouseClick(category, category.width / 2, category.height / 2);
|
||||
input.wait(0);
|
||||
check(focused() === category && sidebarRequests === requestsBefore + 1, "sidebar click activates once and retains actual sidebar focus");
|
||||
input.keyClick(Qt.Key_Up);
|
||||
check(focused()?.modelData?.id === categoryIds[0], "sidebar arrows work after a mouse click");
|
||||
input.mouseClick(content, content.width - 2, 2);
|
||||
waitFor(() => inside(content, focused()) && !inside(content.currentPageItem, focused()), "content background click parks keyboard focus without highlighting a control");
|
||||
input.keyClick(Qt.Key_Tab, Qt.ControlModifier);
|
||||
check(inside(sidebar, focused()), "keyboard pane transfer works after a content background click");
|
||||
|
||||
content.currentPage = "notifications";
|
||||
waitFor(() => find(content.currentPageItem, item => item.upDownKeysStep === false) !== null, "content loads the pointer test page");
|
||||
waitFor(() => content.currentPageSettled, "pointer test page is presented");
|
||||
const slider = find(content.currentPageItem, item => item.upDownKeysStep === false && item.visible && item.enabled);
|
||||
input.waitForPolish(content.Window.window);
|
||||
content._reveal(slider);
|
||||
input.waitForPolish(content.Window.window);
|
||||
input.waitForRendering(slider);
|
||||
sidebar.focusSearch();
|
||||
input.mouseClick(slider, slider.width / 2, slider.height / 2);
|
||||
input.wait(0);
|
||||
check(inside(slider, focused()), "content click preserves clicked slider focus");
|
||||
const sliderValue = slider.value;
|
||||
input.keyClick(Qt.Key_Down);
|
||||
check(!inside(slider, focused()) && slider.value === sliderValue, "content arrows work after clicking a slider");
|
||||
input.keyClick(Qt.Key_F, Qt.ControlModifier);
|
||||
check(sidebar.searchFocused, "Ctrl+F restores actual Search focus");
|
||||
const pageScroll = find(content.currentPageItem, item => item.contentY !== undefined && typeof item.flick === "function");
|
||||
pageScroll.contentY = Math.max(0, pageScroll.contentHeight - pageScroll.height);
|
||||
const pointerScroll = pageScroll.contentY;
|
||||
input.mouseClick(content, content.width - 2, 2);
|
||||
waitFor(() => inside(content, focused()) && !inside(content.currentPageItem, focused()), "background click parks focus after scrolling");
|
||||
check(Math.abs(pageScroll.contentY - pointerScroll) < 1, "background click does not jump the page");
|
||||
input.keyClick(Qt.Key_Down);
|
||||
check(inside(content.currentPageItem, focused()) && pageScroll.contentY < pointerScroll, "the next arrow resumes at the remembered control and reveals it");
|
||||
checkPointerControls(focused);
|
||||
checkKeyCapture(focused);
|
||||
modal.focusCurrentPage();
|
||||
modal.focusSearch();
|
||||
input.wait(0);
|
||||
check(sidebar.searchFocused, "a later sidebar interaction cancels deferred content focus");
|
||||
}
|
||||
|
||||
function checkKeyCapture(focused) {
|
||||
content.currentPage = "lock_screen";
|
||||
waitFor(() => content.currentPageSettled && find(content.currentPageItem, item => item.capturing !== undefined) !== null, "security key capture loads");
|
||||
const capture = find(content.currentPageItem, item => item.capturing !== undefined);
|
||||
capture.forceActiveFocus(Qt.TabFocusReason);
|
||||
input.keyClick(Qt.Key_Return);
|
||||
check(capture.capturing, "Enter starts security key capture");
|
||||
const backs = modal.backs;
|
||||
for (const key of [Qt.Key_Up, Qt.Key_Down, Qt.Key_Backspace])
|
||||
input.keyClick(key);
|
||||
check(focused() === capture && capture.capturing && modal.backs === backs, "capture owns arrows and Backspace");
|
||||
input.keyClick(Qt.Key_Tab);
|
||||
input.keyClick(Qt.Key_Backtab, Qt.ShiftModifier);
|
||||
check(focused() === capture && capture.capturing, "capture consumes forward and reverse Tab traversal");
|
||||
content._focusPage();
|
||||
input.wait(0);
|
||||
check(focused() === capture && capture.capturing, "deferred page focus preserves key capture");
|
||||
input.keyClick(Qt.Key_Tab, Qt.ControlModifier);
|
||||
check(focused() === capture && !sidebar.searchFocused, "capture owns the pane shortcut");
|
||||
capture.startCapture();
|
||||
input.keyClick(Qt.Key_Escape);
|
||||
check(!capture.capturing && focused() === capture && modal.backs === backs, "Escape ends capture before page navigation");
|
||||
modal.focusSearch();
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pointerControls
|
||||
Column {
|
||||
objectName: "pointer-controls"
|
||||
width: 400
|
||||
z: 100
|
||||
property int toggles: 0
|
||||
property int choices: 0
|
||||
DankTextField {
|
||||
objectName: "pointer-text"
|
||||
width: parent.width
|
||||
text: "abcd"
|
||||
}
|
||||
SettingsToggleRow {
|
||||
objectName: "pointer-toggle"
|
||||
width: parent.width
|
||||
text: "Fixture"
|
||||
onToggled: value => {
|
||||
checked = value;
|
||||
parent.toggles++;
|
||||
}
|
||||
}
|
||||
SettingsSwatchGrid {
|
||||
objectName: "pointer-swatches"
|
||||
width: parent.width
|
||||
compact: true
|
||||
options: [{value: "a", label: "A", primary: Theme.primary, secondary: Theme.secondary, tertiary: Theme.tertiary}, {value: "b", label: "B", primary: Theme.primary, secondary: Theme.secondary, tertiary: Theme.tertiary}]
|
||||
onSelected: value => {
|
||||
currentValue = value;
|
||||
parent.choices++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkPointerControls(focused) {
|
||||
const panel = pointerControls.createObject(content.currentPageItem);
|
||||
input.waitForPolish(content.Window.window);
|
||||
input.waitForRendering(panel);
|
||||
const field = find(panel, item => item.objectName === "pointer-text");
|
||||
modal.focusSearch();
|
||||
input.mouseClick(field, field.width / 2, field.height / 2);
|
||||
input.wait(0);
|
||||
check(inside(field, focused()), "clicking a content text field preserves actual field focus");
|
||||
const editor = focused();
|
||||
editor.cursorPosition = editor.text.length;
|
||||
const backs = modal.backs;
|
||||
input.keyClick(Qt.Key_Left);
|
||||
check(editor.cursorPosition === 3, "text field keeps horizontal caret navigation");
|
||||
input.keyClick(Qt.Key_Backspace);
|
||||
check(editor.text === "abd" && modal.backs === backs, "Backspace edits content text without navigating back");
|
||||
const toggle = find(panel, item => item.objectName === "pointer-toggle");
|
||||
input.mouseClick(toggle, toggle.width / 2, toggle.height / 2);
|
||||
input.wait(0);
|
||||
check(focused() === toggle && panel.toggles === 1, "mouse toggle activates once and retains control focus");
|
||||
input.keyClick(Qt.Key_Space);
|
||||
check(panel.toggles === 2 && !toggle.checked, "Space toggles the clicked control once");
|
||||
const rowTint = find(toggle, item => item.parent === toggle && item.color?.toString() === toggle.contentColor.toString());
|
||||
check(rowTint.opacity === 0, "a mouse-focused row stays untinted");
|
||||
const rings = SettingsData.focusRingEnabled;
|
||||
SettingsData.focusRingEnabled = false;
|
||||
modal.focusSearch();
|
||||
toggle.forceActiveFocus(Qt.TabFocusReason);
|
||||
check(rowTint.visible && rowTint.opacity === Theme.stateLayerFocus, "a keyboard-focused row tints with rings off");
|
||||
SettingsData.focusRingEnabled = rings;
|
||||
const tile = find(panel, item => item.modelData?.value === "b");
|
||||
input.mouseClick(tile, tile.width / 2, tile.height / 2);
|
||||
input.wait(0);
|
||||
check(focused() === tile && panel.choices === 1, "swatch click focuses and applies the actual tile once");
|
||||
const tileRing = find(tile, item => item.pointerFocused !== undefined);
|
||||
const tileTint = find(tile, item => item.stateOpacity !== undefined);
|
||||
check(!tileRing.visible, "a mouse-focused swatch draws no focus ring");
|
||||
input.keyClick(Qt.Key_Return);
|
||||
check(panel.choices === 2, "Enter acts on the mouse-focused swatch");
|
||||
// Focus must actually move: forceActiveFocus keeps the old reason on an already focused item
|
||||
toggle.forceActiveFocus(Qt.TabFocusReason);
|
||||
tile.forceActiveFocus(Qt.TabFocusReason);
|
||||
check(tileRing.visible && tileTint.stateOpacity === Theme.stateLayerFocus, "a keyboard-focused swatch draws its focus ring and tint");
|
||||
panel.destroy();
|
||||
modal.focusSearch();
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 0
|
||||
running: true
|
||||
onTriggered: {
|
||||
try {
|
||||
root.run();
|
||||
} catch (error) {
|
||||
root.check(false, error.message);
|
||||
}
|
||||
console.log(root.failed ? "FIXTURE_FAIL see above" : "FIXTURE_PASS");
|
||||
Qt.quit();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Quickshell.watchFiles = false;
|
||||
DC.Style.theme = Theme;
|
||||
DC.Style.settings = SettingsData;
|
||||
DC.I18n.backend = I18n;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import QtQuick
|
||||
import QtTest
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Modals.Settings
|
||||
import qs.DankCommon.Common as DC
|
||||
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
SettingsModal {
|
||||
id: modal
|
||||
}
|
||||
|
||||
TestCase {
|
||||
id: input
|
||||
name: "settings-panes"
|
||||
when: false
|
||||
}
|
||||
|
||||
function find(item, predicate) {
|
||||
if (!item)
|
||||
return null;
|
||||
if (predicate(item))
|
||||
return item;
|
||||
for (const child of item.children ?? []) {
|
||||
const found = find(child, predicate);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function inside(ancestor, item) {
|
||||
for (let p = item; p; p = p.parent) {
|
||||
if (p === ancestor)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function check(condition, label) {
|
||||
if (!condition)
|
||||
throw new Error(label);
|
||||
}
|
||||
|
||||
function until(condition, label) {
|
||||
try {
|
||||
input.tryVerify(condition, 20000);
|
||||
} catch (error) {
|
||||
throw new Error(label);
|
||||
}
|
||||
}
|
||||
|
||||
function run() {
|
||||
until(() => CompositorService.compositor !== "unknown", "compositor detected");
|
||||
SettingsData.animationDuration = 0;
|
||||
modal.show();
|
||||
const navigation = modal.modalFocusScope;
|
||||
const content = navigation.content;
|
||||
const focused = () => navigation.Window.activeFocusItem;
|
||||
until(() => modal.visible && navigation.Window.active && modal.searchFocused, "initial Settings load focuses Search");
|
||||
input.keyClick(Qt.Key_Tab);
|
||||
input.keyClick(Qt.Key_Down);
|
||||
const row = focused();
|
||||
check(inside(modal.sidebar, row) && row.modelData?.id, "initial Tab then Down focuses a category");
|
||||
input.mouseClick(row, row.width / 2, row.height / 2);
|
||||
until(() => content.currentPageSettled, "clicked category finishes loading");
|
||||
check(focused() === row, "cold mouse category load retains sidebar focus");
|
||||
input.keyClick(Qt.Key_Return);
|
||||
until(() => inside(content.currentPageItem, focused()), "Enter on the selected category enters content");
|
||||
|
||||
const profile = find(modal.sidebar, item => item.navigationRequested !== undefined);
|
||||
input.mouseClick(profile, profile.width / 2, profile.height / 2);
|
||||
check(focused() === profile, "profile click focuses the public navigation row");
|
||||
let profileRequests = 0;
|
||||
modal.sidebar.pageRequested.connect(() => profileRequests++);
|
||||
input.keyClick(Qt.Key_Return);
|
||||
check(profileRequests === 1, "Enter on the profile row requests its page exactly once");
|
||||
until(() => inside(content.currentPageItem, focused()), "Enter after clicking profile enters its page");
|
||||
|
||||
modal.setPage("personalization");
|
||||
modal.focusCurrentPage();
|
||||
until(() => content.currentPageSettled && content.currentPageItem?.hubId === "personalization", "personalization hub loads");
|
||||
const group = find(content.currentPageItem, item => item.arrowKeysSelect === false && item.model?.[0]?.icon === "light_mode");
|
||||
group.requestFocus(false);
|
||||
const mode = SessionData.isLightMode;
|
||||
input.keyClick(Qt.Key_Right);
|
||||
check(SessionData.isLightMode === mode && focused()?.selected === false, "Light/Dark arrows focus without applying a theme");
|
||||
input.keyClick(Qt.Key_Space);
|
||||
check(SessionData.isLightMode !== mode, "Space applies the focused Light/Dark mode");
|
||||
input.keyClick(Qt.Key_Tab, Qt.ControlModifier);
|
||||
check(inside(modal.sidebar, focused()), "Ctrl+Tab returns to sidebar");
|
||||
input.keyClick(Qt.Key_Tab, Qt.ControlModifier);
|
||||
until(() => inside(group, focused()), "Ctrl+Tab restores the focused Light/Dark choice");
|
||||
|
||||
const opener = find(content.currentPageItem, item => item.settingKey === "themeNav" && item.clickable);
|
||||
opener.forceActiveFocus(Qt.TabFocusReason);
|
||||
content._reveal(opener);
|
||||
const scroll = find(content.currentPageItem, item => item.contentY !== undefined && typeof item.flick === "function");
|
||||
const position = scroll.contentY;
|
||||
input.keyClick(Qt.Key_Return);
|
||||
until(() => modal.currentPage === "theme" && content.currentPageSettled && inside(content.currentPageItem, focused()), "keyboard opens subpage and focuses a control");
|
||||
input.keyClick(Qt.Key_Left, Qt.AltModifier);
|
||||
until(() => modal.currentPage === "personalization" && focused() === opener, "Back restores the actual hub opener");
|
||||
check(Math.abs(scroll.contentY - position) < 1, "Back preserves hub scroll");
|
||||
|
||||
// Parked keyboard focus sits on the content anchor, so no control is highlighted
|
||||
const parked = () => focused()?.parent === content;
|
||||
input.mouseClick(opener, opener.width / 2, opener.height / 2);
|
||||
until(() => modal.currentPage === "theme" && content.currentPageSettled && parked(), "mouse-opened subpage parks focus instead of highlighting a control");
|
||||
input.keyClick(Qt.Key_Down);
|
||||
check(inside(content.currentPageItem, focused()), "Down after a mouse-opened subpage enters it");
|
||||
input.mouseClick(content, content.width / 2, content.height / 2, Qt.BackButton);
|
||||
until(() => modal.currentPage === "personalization" && parked(), "mouse Back button returns a page and parks focus");
|
||||
input.keyClick(Qt.Key_Down);
|
||||
check(focused() === opener, "Down after mouse Back resumes at the hub opener");
|
||||
|
||||
input.mouseClick(row, row.width / 2, row.height / 2);
|
||||
input.mouseClick(content, content.width / 2, Theme.spacingM);
|
||||
until(parked, "content background click parks keyboard focus without highlighting a control");
|
||||
input.keyClick(Qt.Key_Down);
|
||||
check(focused() === opener, "Down after a background click resumes at the last control");
|
||||
|
||||
const hubPage = content.currentPageItem;
|
||||
modal.setPage("typography");
|
||||
modal.focusSearch();
|
||||
until(() => modal.currentPage === "typography" && content.currentPageItem !== hubPage && content.currentPageSettled, "typography page loads");
|
||||
const textRow = find(content.currentPageItem, item => item.clickable === false && item.hasBody === true);
|
||||
input.waitForPolish(navigation.Window.window);
|
||||
input.mouseClick(textRow, textRow.width / 2, textRow.height / 2);
|
||||
until(parked, "click on a page body row parks keyboard focus in content");
|
||||
|
||||
SettingsData.barConfigs = [{
|
||||
id: "fixture", enabled: true, visible: true, position: 0,
|
||||
leftWidgets: ["clock"], centerWidgets: [], rightWidgets: []
|
||||
}];
|
||||
SettingsUiState.selectedBarId = "fixture";
|
||||
modal.focusSearch();
|
||||
focused().text = "clock";
|
||||
const widgetResult = () => find(modal.sidebar, item => item.modelData?.runtimeType === "barWidget" && item.modelData.runtimeId === "clock");
|
||||
until(() => widgetResult() !== null, "search finds the existing Clock widget");
|
||||
const result = widgetResult();
|
||||
modal.sidebar.ensureRowVisible(result);
|
||||
input.waitForPolish(navigation.Window.window);
|
||||
const previousPage = content.currentPageItem;
|
||||
input.mouseClick(result, result.width / 2, result.height / 2);
|
||||
until(() => modal.currentPage === "bar_widget" && content.currentPageItem !== previousPage && content.currentPageSettled, "mouse search result loads widget options");
|
||||
check(inside(modal.sidebar, focused()), "mouse search drilldown retains sidebar focus");
|
||||
input.keyClick(Qt.Key_Tab, Qt.ControlModifier);
|
||||
until(() => inside(content.currentPageItem, focused()), "pane shortcut enters searched widget options");
|
||||
modal.focusSearch();
|
||||
input.keyClick(Qt.Key_Escape);
|
||||
modal.setPage("personalization");
|
||||
modal.focusCurrentPage();
|
||||
until(() => content.currentPageItem?.hubId === "personalization" && content.currentPageSettled, "personalization returns after search drilldown");
|
||||
|
||||
// The test compositor owns tiled window width; exercise the compact layout directly.
|
||||
modal.isCompactMode = true;
|
||||
modal.focusSearch();
|
||||
input.keyClick(Qt.Key_Tab, Qt.ControlModifier);
|
||||
until(() => !modal.menuVisible && inside(content.currentPageItem, focused()), "compact pane shortcut enters visible content");
|
||||
input.keyClick(Qt.Key_Tab, Qt.ControlModifier);
|
||||
check(modal.menuVisible && inside(modal.sidebar, focused()), "compact pane shortcut reveals the sidebar");
|
||||
input.keyClick(Qt.Key_Tab);
|
||||
input.keyClick(Qt.Key_Return);
|
||||
until(() => !modal.menuVisible && inside(content.currentPageItem, focused()), "compact sidebar activation enters content");
|
||||
modal.hide();
|
||||
until(() => !content.currentPageItem, "closing Settings releases its pages");
|
||||
modal.menuVisible = false;
|
||||
navigation.activePane = "content";
|
||||
modal.show();
|
||||
until(() => modal.visible && modal.menuVisible && modal.searchFocused, "compact reopening starts in visible Search");
|
||||
modal.hide();
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 0
|
||||
running: true
|
||||
onTriggered: {
|
||||
try {
|
||||
root.run();
|
||||
console.log("FIXTURE_PASS");
|
||||
} catch (error) {
|
||||
console.log("FIXTURE_FAIL " + error.message);
|
||||
}
|
||||
Qt.quit();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Quickshell.watchFiles = false;
|
||||
DC.Style.theme = Theme;
|
||||
DC.Style.settings = SettingsData;
|
||||
DC.I18n.backend = I18n;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user