mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-29 06:27:47 -04:00
401 lines
15 KiB
QML
401 lines
15 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Widgets
|
|
import qs.Common
|
|
import qs.Modules.DBar
|
|
import qs.Services
|
|
import qs.DCommon.Widgets
|
|
import qs.Widgets
|
|
|
|
DPopout {
|
|
id: root
|
|
|
|
property var currentWindow: null
|
|
property int processId: 0
|
|
|
|
readonly property string appId: currentWindow?.appId || ""
|
|
readonly property string windowTitle: currentWindow?.title || ""
|
|
readonly property string appName: appId ? Paths.getAppName(appId, DesktopEntries.heuristicLookup(Paths.moddedAppId(appId))) : I18n.tr("Unknown")
|
|
readonly property int pid: processId
|
|
readonly property string scratchpadName: CompositorService.windowScratchpadName(currentWindow)
|
|
|
|
layerNamespace: "dms:focused-window-popout"
|
|
popupWidth: 340
|
|
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 260
|
|
triggerWidth: 40
|
|
positioning: ""
|
|
shouldBeVisible: false
|
|
|
|
property var matchingRule: null
|
|
readonly property bool hasMatchingRule: matchingRule !== null
|
|
|
|
function testPattern(pattern, value) {
|
|
if (!pattern)
|
|
return true;
|
|
try {
|
|
return new RegExp(pattern).test(value || "");
|
|
} catch (e) {
|
|
return pattern === (value || "");
|
|
}
|
|
}
|
|
|
|
function matchesCriteria(match, window) {
|
|
if (!match || (!match.appId && !match.title))
|
|
return false;
|
|
return testPattern(match.appId, window.appId) && testPattern(match.title, window.title);
|
|
}
|
|
|
|
function findMatchingDmsRule(rules, window) {
|
|
if (!rules || !window)
|
|
return null;
|
|
return rules.find(rule => {
|
|
if (!(rule.source || "").includes("dms/windowrules") || rule.id === CompositorService.dmsFloatingRuleId)
|
|
return false;
|
|
const matches = (rule.matches && rule.matches.length > 0) ? rule.matches : (rule.matchCriteria ? [rule.matchCriteria] : []);
|
|
return matches.some(m => matchesCriteria(m, window));
|
|
}) || null;
|
|
}
|
|
|
|
function checkMatchingRule() {
|
|
if (!shouldBeVisible || !currentWindow || !CompositorService.supportsWindowRules) {
|
|
matchingRule = null;
|
|
return;
|
|
}
|
|
|
|
matchingRule = null;
|
|
const targetWin = currentWindow;
|
|
const compositor = CompositorService.compositor;
|
|
Proc.runCommand("focused-window-rules", [Proc.dmsBin, "config", "windowrules", "list", compositor], (output, exitCode) => {
|
|
if (exitCode !== 0 || targetWin !== currentWindow)
|
|
return;
|
|
try {
|
|
const result = JSON.parse(output.trim());
|
|
matchingRule = findMatchingDmsRule(result.rules, currentWindow);
|
|
} catch (e) {
|
|
matchingRule = null;
|
|
}
|
|
});
|
|
}
|
|
|
|
onShouldBeVisibleChanged: {
|
|
if (shouldBeVisible)
|
|
checkMatchingRule();
|
|
}
|
|
|
|
onCurrentWindowChanged: {
|
|
if (shouldBeVisible)
|
|
checkMatchingRule();
|
|
}
|
|
|
|
function copyValue(value) {
|
|
if (!value)
|
|
return;
|
|
Quickshell.execDetached(["dms", "cl", "copy", value.toString()]);
|
|
ToastService.showInfo(I18n.tr("Copied to clipboard"));
|
|
close();
|
|
}
|
|
|
|
function killWindow() {
|
|
if (pid > 0)
|
|
Quickshell.execDetached(["kill", pid.toString()]);
|
|
close();
|
|
}
|
|
|
|
function addWindowRule() {
|
|
openWindowRule();
|
|
}
|
|
|
|
function openWindowRule() {
|
|
if (!currentWindow || !PopoutService.windowRuleModalLoader)
|
|
return;
|
|
const targetRule = root.matchingRule;
|
|
const targetWin = currentWindow;
|
|
close();
|
|
|
|
const loader = PopoutService.windowRuleModalLoader;
|
|
loader.active = true;
|
|
|
|
const openModal = () => {
|
|
if (!loader.item)
|
|
return;
|
|
if (targetRule)
|
|
loader.item.showEdit(targetRule);
|
|
else
|
|
loader.item.show(targetWin);
|
|
};
|
|
|
|
if (loader.item)
|
|
openModal();
|
|
else
|
|
Qt.callLater(openModal);
|
|
}
|
|
|
|
onBackgroundClicked: close()
|
|
|
|
content: Component {
|
|
Rectangle {
|
|
id: contentRoot
|
|
|
|
implicitWidth: 340
|
|
implicitHeight: contentColumn.implicitHeight + PopoutMetrics.contentPadding * 2
|
|
anchors.fill: parent
|
|
color: "transparent"
|
|
focus: true
|
|
|
|
Keys.onEscapePressed: event => {
|
|
root.close();
|
|
event.accepted = true;
|
|
}
|
|
|
|
Column {
|
|
id: contentColumn
|
|
anchors.fill: parent
|
|
anchors.margins: PopoutMetrics.contentPadding
|
|
spacing: Theme.spacingXXS
|
|
|
|
Row {
|
|
width: parent.width
|
|
height: BarMetrics.menuRowHeight
|
|
spacing: Theme.spacingS
|
|
|
|
IconImage {
|
|
width: Theme.iconSizeMedium
|
|
height: Theme.iconSizeMedium
|
|
source: Paths.getAppIcon(root.appId, DesktopEntries.heuristicLookup(root.appId))
|
|
asynchronous: true
|
|
mipmap: true
|
|
smooth: true
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
StyledText {
|
|
width: parent.width - Theme.iconSizeMedium - Theme.spacingS
|
|
text: root.appName
|
|
color: Theme.surfaceText
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Theme.fontWeightMedium
|
|
elide: Text.ElideRight
|
|
maximumLineCount: 2
|
|
wrapMode: Text.Wrap
|
|
clip: true
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
|
|
Repeater {
|
|
model: [
|
|
{
|
|
label: I18n.tr("App ID"),
|
|
value: root.appId,
|
|
copyable: !!root.appId
|
|
},
|
|
{
|
|
label: I18n.tr("Title"),
|
|
value: root.windowTitle || I18n.tr("Untitled"),
|
|
copyable: !!root.windowTitle
|
|
},
|
|
{
|
|
label: I18n.tr("PID", "Label for the process ID row in the focused window popout"),
|
|
value: root.pid > 0 ? root.pid.toString() : I18n.tr("Unavailable"),
|
|
copyable: root.pid > 0
|
|
}
|
|
]
|
|
|
|
delegate: DListRow {
|
|
id: copyRow
|
|
required property var modelData
|
|
required property int index
|
|
readonly property real labelWidth: Theme.iconButtonSize + Theme.iconSizeLarge
|
|
|
|
width: contentColumn.width
|
|
implicitHeight: Theme.listItemHeight
|
|
firstInGroup: index === 0
|
|
lastInGroup: index === 2
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: Theme.spacingL
|
|
anchors.rightMargin: Theme.spacingL
|
|
spacing: Theme.spacingS
|
|
|
|
StyledText {
|
|
width: copyRow.labelWidth
|
|
text: copyRow.modelData.label
|
|
color: copyRow.supportingContentColor
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
StyledText {
|
|
width: parent.width - copyRow.labelWidth - Theme.spacingS
|
|
text: copyRow.modelData.value
|
|
color: copyRow.contentColor
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
font.family: SettingsData.monoFontFamily
|
|
wrapMode: Text.Wrap
|
|
maximumLineCount: 2
|
|
elide: Text.ElideMiddle
|
|
clip: true
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
|
|
StateLayer {
|
|
topLeftRadius: copyRow.firstInGroup ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
|
|
topRightRadius: topLeftRadius
|
|
bottomLeftRadius: copyRow.lastInGroup ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
|
|
bottomRightRadius: bottomLeftRadius
|
|
enabled: copyRow.modelData.copyable
|
|
disabled: !copyRow.modelData.copyable
|
|
onClicked: root.copyValue(copyRow.modelData.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: parent.width
|
|
height: Theme.dividerWidth
|
|
color: Theme.outlineVariant
|
|
}
|
|
|
|
Item {
|
|
visible: root.scratchpadName !== ""
|
|
width: parent.width
|
|
height: BarMetrics.menuRowHeight
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: Theme.spacingS
|
|
spacing: Theme.spacingS
|
|
|
|
DIcon {
|
|
name: "outbox"
|
|
size: Theme.iconSizeSmall
|
|
color: Theme.surfaceText
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Move out of scratchpad", "window context menu action, brings the window back from the special workspace")
|
|
color: Theme.surfaceText
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
|
|
StateLayer {
|
|
cornerRadius: BarMetrics.menuItemRadius
|
|
onClicked: {
|
|
CompositorService.moveWindowOutOfSpecial(root.currentWindow);
|
|
root.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
Repeater {
|
|
model: root.scratchpadName !== "" ? [] : CompositorService.specialWorkspaceNames
|
|
|
|
Item {
|
|
required property string modelData
|
|
|
|
width: parent.width
|
|
height: BarMetrics.menuRowHeight
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: Theme.spacingS
|
|
spacing: Theme.spacingS
|
|
|
|
DIcon {
|
|
name: "inbox"
|
|
size: Theme.iconSizeSmall
|
|
color: Theme.surfaceText
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
StyledText {
|
|
text: modelData === "special" ? I18n.tr("Move to scratchpad", "window context menu action, sends the window to the unnamed special workspace") : I18n.tr("Move to scratchpad: %1", "%1 is the named special workspace").arg(modelData)
|
|
color: Theme.surfaceText
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
|
|
StateLayer {
|
|
cornerRadius: BarMetrics.menuItemRadius
|
|
onClicked: {
|
|
CompositorService.moveWindowToSpecial(root.currentWindow, modelData);
|
|
root.close();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
visible: CompositorService.supportsWindowRules
|
|
width: parent.width
|
|
height: BarMetrics.menuRowHeight
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: Theme.spacingS
|
|
spacing: Theme.spacingS
|
|
|
|
DIcon {
|
|
name: root.hasMatchingRule ? "edit" : "rule"
|
|
size: Theme.iconSizeSmall
|
|
color: Theme.surfaceText
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
StyledText {
|
|
text: root.hasMatchingRule ? I18n.tr("Edit Window Rule") : I18n.tr("Add window rule")
|
|
color: Theme.surfaceText
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
|
|
StateLayer {
|
|
cornerRadius: BarMetrics.menuItemRadius
|
|
onClicked: root.openWindowRule()
|
|
}
|
|
}
|
|
|
|
Item {
|
|
width: parent.width
|
|
height: BarMetrics.menuRowHeight
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: Theme.spacingS
|
|
spacing: Theme.spacingS
|
|
|
|
DIcon {
|
|
name: "close"
|
|
size: Theme.iconSizeSmall
|
|
color: Theme.error
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Kill Process")
|
|
color: Theme.error
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
|
|
StateLayer {
|
|
cornerRadius: BarMetrics.menuItemRadius
|
|
stateColor: Theme.error
|
|
enabled: root.pid > 0
|
|
disabled: root.pid <= 0
|
|
onClicked: root.killWindow()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|