mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
5c3b3a2185
Colorization now activates automatically when icon or text color is set, simplifying the UX by removing an extra toggle step.
709 lines
27 KiB
QML
709 lines
27 KiB
QML
import QtQuick
|
|
import QtQuick.Layouts
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import qs.Commons
|
|
import qs.Modules.Bar.Extras
|
|
import qs.Modules.Panels.Settings
|
|
import qs.Services.Control
|
|
import qs.Services.UI
|
|
import qs.Widgets
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property ShellScreen screen
|
|
|
|
// Widget properties passed from Bar.qml for per-instance settings
|
|
property string widgetId: ""
|
|
property string section: ""
|
|
property int sectionWidgetIndex: -1
|
|
property int sectionWidgetsCount: 0
|
|
|
|
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
|
// Explicit screenName property ensures reactive binding when screen changes
|
|
readonly property string screenName: screen ? screen.name : ""
|
|
property var widgetSettings: {
|
|
if (section && sectionWidgetIndex >= 0 && screenName) {
|
|
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
|
if (widgets && sectionWidgetIndex < widgets.length) {
|
|
return widgets[sectionWidgetIndex];
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
|
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
|
|
|
readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon
|
|
readonly property string iconPosition: widgetSettings.iconPosition || widgetMetadata.iconPosition
|
|
readonly property string leftClickExec: widgetSettings.leftClickExec || widgetMetadata.leftClickExec
|
|
readonly property bool leftClickUpdateText: widgetSettings.leftClickUpdateText ?? widgetMetadata.leftClickUpdateText
|
|
readonly property string rightClickExec: widgetSettings.rightClickExec || widgetMetadata.rightClickExec
|
|
readonly property bool rightClickUpdateText: widgetSettings.rightClickUpdateText ?? widgetMetadata.rightClickUpdateText
|
|
readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec
|
|
readonly property bool middleClickUpdateText: widgetSettings.middleClickUpdateText ?? widgetMetadata.middleClickUpdateText
|
|
readonly property string ipcIdentifier: widgetSettings.ipcIdentifier !== undefined ? widgetSettings.ipcIdentifier : (widgetMetadata.ipcIdentifier || "")
|
|
readonly property string wheelExec: widgetSettings.wheelExec || widgetMetadata.wheelExec
|
|
readonly property string wheelUpExec: widgetSettings.wheelUpExec || widgetMetadata.wheelUpExec
|
|
readonly property string wheelDownExec: widgetSettings.wheelDownExec || widgetMetadata.wheelDownExec
|
|
readonly property string wheelMode: widgetSettings.wheelMode || widgetMetadata.wheelMode
|
|
readonly property bool wheelUpdateText: widgetSettings.wheelUpdateText ?? widgetMetadata.wheelUpdateText
|
|
readonly property bool wheelUpUpdateText: widgetSettings.wheelUpUpdateText ?? widgetMetadata.wheelUpUpdateText
|
|
readonly property bool wheelDownUpdateText: widgetSettings.wheelDownUpdateText ?? widgetMetadata.wheelDownUpdateText
|
|
readonly property string textCommand: widgetSettings.textCommand !== undefined ? widgetSettings.textCommand : (widgetMetadata.textCommand || "")
|
|
readonly property bool textStream: widgetSettings.textStream !== undefined ? widgetSettings.textStream : (widgetMetadata.textStream || false)
|
|
readonly property int textIntervalMs: widgetSettings.textIntervalMs !== undefined ? widgetSettings.textIntervalMs : (widgetMetadata.textIntervalMs || 3000)
|
|
readonly property string textCollapse: widgetSettings.textCollapse !== undefined ? widgetSettings.textCollapse : (widgetMetadata.textCollapse || "")
|
|
readonly property bool parseJson: widgetSettings.parseJson !== undefined ? widgetSettings.parseJson : (widgetMetadata.parseJson || false)
|
|
readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec || (wheelMode === "unified" && wheelExec) || (wheelMode === "separate" && (wheelUpExec || wheelDownExec)))
|
|
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : true
|
|
readonly property bool showExecTooltip: widgetSettings.showExecTooltip !== undefined ? widgetSettings.showExecTooltip : (widgetMetadata.showExecTooltip !== undefined ? widgetMetadata.showExecTooltip : true)
|
|
readonly property bool showTextTooltip: widgetSettings.showTextTooltip !== undefined ? widgetSettings.showTextTooltip : (widgetMetadata.showTextTooltip !== undefined ? widgetMetadata.showTextTooltip : true)
|
|
readonly property string generalTooltipText: widgetSettings.generalTooltipText !== undefined ? widgetSettings.generalTooltipText : (widgetMetadata.generalTooltipText || "")
|
|
readonly property bool _hasCustomTooltip: generalTooltipText.trim() !== ""
|
|
readonly property string hideMode: widgetSettings.hideMode || "alwaysExpanded"
|
|
readonly property bool hasOutput: _dynamicText !== ""
|
|
readonly property bool shouldForceOpen: textStream && (hideMode === "alwaysExpanded" || hideMode === "maxTransparent")
|
|
|
|
readonly property bool _useNewHideLogic: textCommand && textCommand.length > 0 && textStream
|
|
readonly property bool _useTextCommandLogic: textCommand && textCommand.length > 0 && textStream
|
|
|
|
readonly property bool _pillVisible: {
|
|
if (!_useTextCommandLogic) {
|
|
return true;
|
|
}
|
|
if (hideMode === "alwaysExpanded" || hideMode === "maxTransparent") {
|
|
return true;
|
|
}
|
|
var hasActualIcon = (_dynamicIcon !== "" || customIcon !== "");
|
|
return hasOutput || (showIcon && hasActualIcon);
|
|
}
|
|
|
|
readonly property real _pillOpacity: {
|
|
if (!_useTextCommandLogic) {
|
|
return 1.0;
|
|
}
|
|
if (hideMode === "maxTransparent" && !hasOutput) {
|
|
return 0.0;
|
|
}
|
|
return 1.0;
|
|
}
|
|
|
|
readonly property bool _pillForceOpen: {
|
|
if (!_useTextCommandLogic) {
|
|
return _dynamicText !== "" || (textStream && currentMaxTextLength > 0);
|
|
}
|
|
if (currentMaxTextLength <= 0) {
|
|
return false;
|
|
}
|
|
|
|
if (hideMode === "alwaysExpanded" || hideMode === "maxTransparent") {
|
|
return true;
|
|
}
|
|
return hasOutput;
|
|
}
|
|
|
|
readonly property string _pillIcon: {
|
|
if (!_useTextCommandLogic) {
|
|
if (textCommand && textCommand.length > 0 && showIcon) {
|
|
return _dynamicIcon !== "" ? _dynamicIcon : customIcon;
|
|
} else if (!(textCommand && textCommand.length > 0)) {
|
|
return _dynamicIcon !== "" ? _dynamicIcon : customIcon;
|
|
} else {
|
|
return "";
|
|
}
|
|
}
|
|
if (!showIcon)
|
|
return "";
|
|
var actualIcon = _dynamicIcon !== "" ? _dynamicIcon : customIcon;
|
|
if (hideMode === "expandWithOutput" && actualIcon === "") {
|
|
return "question-mark";
|
|
}
|
|
return actualIcon;
|
|
}
|
|
|
|
readonly property string _pillText: {
|
|
if (!_useTextCommandLogic) {
|
|
return (!isVerticalBar || currentMaxTextLength > 0) ? _dynamicText : "";
|
|
}
|
|
if (currentMaxTextLength <= 0) {
|
|
return "";
|
|
}
|
|
if (hasOutput) {
|
|
return _dynamicText;
|
|
}
|
|
if (hideMode === "expandWithOutput") {
|
|
return "";
|
|
}
|
|
return " ".repeat(currentMaxTextLength);
|
|
}
|
|
|
|
readonly property string colorizeSystemIcon: {
|
|
if (widgetSettings.colorizeSystemIcon !== undefined)
|
|
return widgetSettings.colorizeSystemIcon;
|
|
return widgetMetadata.colorizeSystemIcon !== undefined ? widgetMetadata.colorizeSystemIcon : "none";
|
|
}
|
|
readonly property string colorizeSystemText: {
|
|
if (widgetSettings.colorizeSystemText !== undefined)
|
|
return widgetSettings.colorizeSystemText;
|
|
return widgetMetadata.colorizeSystemText !== undefined ? widgetMetadata.colorizeSystemText : "none";
|
|
}
|
|
|
|
// Colorization is active if either icon or text has a color set
|
|
readonly property bool isColorizing: colorizeSystemIcon !== "none" || colorizeSystemText !== "none"
|
|
|
|
// Get color value from color name (returns null for invalid names)
|
|
function _getColorValue(colorName, forHover) {
|
|
const baseColor = (function () {
|
|
switch (colorName) {
|
|
case "primary":
|
|
return Color.mPrimary;
|
|
case "secondary":
|
|
return Color.mSecondary;
|
|
case "tertiary":
|
|
return Color.mTertiary;
|
|
case "error":
|
|
return Color.mError;
|
|
default:
|
|
return null;
|
|
}
|
|
})();
|
|
return baseColor !== null ? (forHover ? Qt.darker(baseColor, 1.2) : baseColor) : null;
|
|
}
|
|
|
|
// Resolve icon color with priority: dynamic > static > default
|
|
function _resolveIconColor(dynamicColorName, staticColorName, isHover) {
|
|
if (dynamicColorName && dynamicColorName !== "") {
|
|
if (dynamicColorName === "none") {
|
|
return isHover ? Color.mOnHover : Color.mOnSurface;
|
|
}
|
|
const color = _getColorValue(dynamicColorName, isHover);
|
|
if (color !== null)
|
|
return color;
|
|
}
|
|
|
|
if (staticColorName && staticColorName !== "" && isColorizing) {
|
|
const color = _getColorValue(staticColorName, isHover);
|
|
if (color !== null)
|
|
return color;
|
|
}
|
|
|
|
return isHover ? Color.mOnHover : Color.mOnSurface;
|
|
}
|
|
|
|
readonly property color iconColor: _resolveIconColor(_dynamicIconColor || _dynamicColor, colorizeSystemIcon, false)
|
|
readonly property color iconHoverColor: _resolveIconColor(_dynamicIconColor || _dynamicColor, colorizeSystemIcon, true)
|
|
readonly property color textColor: _resolveIconColor(_dynamicTextColor || _dynamicColor, colorizeSystemText, false)
|
|
readonly property color textHoverColor: _resolveIconColor(_dynamicTextColor || _dynamicColor, colorizeSystemText, true)
|
|
|
|
implicitWidth: pill.width
|
|
implicitHeight: pill.height
|
|
|
|
BarPill {
|
|
id: pill
|
|
|
|
visible: _pillVisible
|
|
opacity: _pillOpacity
|
|
screen: root.screen
|
|
oppositeDirection: BarService.getPillDirection(root)
|
|
iconPosition: root.iconPosition
|
|
icon: _pillIcon
|
|
text: _pillText
|
|
rotateText: isVerticalBar && currentMaxTextLength > 0
|
|
autoHide: false
|
|
forceOpen: _pillForceOpen
|
|
forceClose: !_pillForceOpen
|
|
customIconColor: iconColor
|
|
customTextColor: textColor
|
|
|
|
// Helper function to build tooltip content
|
|
function _buildTooltipContent() {
|
|
var lines = [];
|
|
|
|
// Add custom tooltip if set
|
|
if (_hasCustomTooltip) {
|
|
lines.push(generalTooltipText);
|
|
}
|
|
|
|
// Add command details if enabled and available
|
|
if (showExecTooltip && hasExec) {
|
|
if (leftClickExec !== "") {
|
|
lines.push(I18n.tr("bar.custom-button.left-click-label") + `: ${leftClickExec}`);
|
|
} else if (!leftClickUpdateText) {
|
|
lines.push(I18n.tr("bar.custom-button.left-click-label") + ": " + I18n.tr("actions.widget-settings"));
|
|
}
|
|
|
|
if (rightClickExec !== "") {
|
|
lines.push(I18n.tr("bar.custom-button.right-click-label") + `: ${rightClickExec}`);
|
|
} else if (!rightClickUpdateText) {
|
|
lines.push(I18n.tr("bar.custom-button.right-click-label") + ": " + I18n.tr("actions.widget-settings"));
|
|
}
|
|
|
|
if (middleClickExec !== "") {
|
|
lines.push(I18n.tr("bar.custom-button.middle-click-label") + `: ${middleClickExec}`);
|
|
} else if (!middleClickUpdateText) {
|
|
lines.push(I18n.tr("bar.custom-button.middle-click-label") + ": " + I18n.tr("actions.widget-settings"));
|
|
}
|
|
|
|
if (wheelMode === "unified" && wheelExec !== "") {
|
|
lines.push(I18n.tr("bar.custom-button.wheel-label") + `: ${wheelExec}`);
|
|
} else if (wheelMode === "separate") {
|
|
if (wheelUpExec !== "") {
|
|
lines.push(I18n.tr("bar.custom-button.wheel-up") + `: ${wheelUpExec}`);
|
|
}
|
|
if (wheelDownExec !== "") {
|
|
lines.push(I18n.tr("bar.custom-button.wheel-down") + `Wheel down: ${wheelDownExec}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add dynamic text tooltip if enabled and available
|
|
if (showTextTooltip && _dynamicTooltip !== "") {
|
|
lines.push(_dynamicTooltip);
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
tooltipText: {
|
|
var lines = _buildTooltipContent();
|
|
|
|
// If no custom tooltip and both switches are off, show default tooltip
|
|
if (!_hasCustomTooltip && !showExecTooltip && !showTextTooltip) {
|
|
return I18n.tr("bar.custom-button.default-tooltip");
|
|
}
|
|
|
|
// If there's content, join with newlines
|
|
if (lines.length > 0) {
|
|
return lines.join("\n");
|
|
}
|
|
|
|
// Fallback (shouldn't reach here normally)
|
|
return I18n.tr("bar.custom-button.default-tooltip");
|
|
}
|
|
|
|
onClicked: root.clicked()
|
|
onRightClicked: root.rightClicked()
|
|
onMiddleClicked: root.middleClicked()
|
|
onWheel: delta => root.wheeled(delta)
|
|
}
|
|
|
|
// Internal state for dynamic text
|
|
property string _dynamicText: ""
|
|
property string _dynamicIcon: ""
|
|
property string _dynamicTooltip: ""
|
|
property string _dynamicColor: ""
|
|
property string _dynamicIconColor: ""
|
|
property string _dynamicTextColor: ""
|
|
|
|
// Maximum length for text display before scrolling (different values for horizontal and vertical)
|
|
readonly property var maxTextLength: {
|
|
"horizontal": ((widgetSettings && widgetSettings.maxTextLength && widgetSettings.maxTextLength.horizontal !== undefined) ? widgetSettings.maxTextLength.horizontal : ((widgetMetadata && widgetMetadata.maxTextLength && widgetMetadata.maxTextLength.horizontal !== undefined) ? widgetMetadata.maxTextLength.horizontal : 10)),
|
|
"vertical": ((widgetSettings && widgetSettings.maxTextLength && widgetSettings.maxTextLength.vertical !== undefined) ? widgetSettings.maxTextLength.vertical : ((widgetMetadata && widgetMetadata.maxTextLength && widgetMetadata.maxTextLength.vertical !== undefined) ? widgetMetadata.maxTextLength.vertical : 10))
|
|
}
|
|
readonly property int _staticDuration: 6 // How many cycles to stay static at start/end
|
|
|
|
// Encapsulated state for scrolling text implementation
|
|
property var _scrollState: {
|
|
"originalText": "",
|
|
"needsScrolling": false,
|
|
"offset": 0,
|
|
"phase": 0 // 0=static start, 1=scrolling, 2=static end
|
|
,
|
|
"phaseCounter": 0
|
|
}
|
|
|
|
// Current max text length based on bar orientation
|
|
readonly property int currentMaxTextLength: isVerticalBar ? maxTextLength.vertical : maxTextLength.horizontal
|
|
|
|
// Periodically run the text command (if set)
|
|
Timer {
|
|
id: refreshTimer
|
|
interval: Math.max(250, textIntervalMs)
|
|
repeat: true
|
|
running: (!isVerticalBar || currentMaxTextLength > 0) && !textStream && textCommand && textCommand.length > 0
|
|
triggeredOnStart: true
|
|
onTriggered: root.runTextCommand()
|
|
}
|
|
|
|
// Restart exited text stream commands after a delay
|
|
Timer {
|
|
id: restartTimer
|
|
interval: 1000
|
|
running: (!isVerticalBar || currentMaxTextLength > 0) && textStream && !textProc.running
|
|
onTriggered: root.runTextCommand()
|
|
}
|
|
|
|
// Timer for scrolling text display
|
|
Timer {
|
|
id: scrollTimer
|
|
interval: 300
|
|
repeat: true
|
|
running: false
|
|
onTriggered: {
|
|
if (_scrollState.needsScrolling && _scrollState.originalText.length > currentMaxTextLength) {
|
|
// Traditional marquee with pause at beginning and end
|
|
if (_scrollState.phase === 0) {
|
|
// Static at beginning
|
|
_dynamicText = _scrollState.originalText.substring(0, Math.min(currentMaxTextLength, _scrollState.originalText.length));
|
|
_scrollState.phaseCounter++;
|
|
if (_scrollState.phaseCounter >= _staticDuration) {
|
|
_scrollState.phaseCounter = 0;
|
|
_scrollState.phase = 1; // Move to scrolling
|
|
}
|
|
} else if (_scrollState.phase === 1) {
|
|
// Scrolling
|
|
_scrollState.offset++;
|
|
var start = _scrollState.offset;
|
|
var end = start + currentMaxTextLength;
|
|
|
|
if (start >= _scrollState.originalText.length - currentMaxTextLength) {
|
|
// Reached or passed the end, ensure we show the last part
|
|
var textEnd = _scrollState.originalText.length;
|
|
var textStart = Math.max(0, textEnd - currentMaxTextLength);
|
|
_dynamicText = _scrollState.originalText.substring(textStart, textEnd);
|
|
_scrollState.phase = 2; // Move to static end phase
|
|
_scrollState.phaseCounter = 0;
|
|
} else {
|
|
_dynamicText = _scrollState.originalText.substring(start, end);
|
|
}
|
|
} else if (_scrollState.phase === 2) {
|
|
// Static at end
|
|
// Ensure end text is displayed correctly
|
|
var textEnd = _scrollState.originalText.length;
|
|
var textStart = Math.max(0, textEnd - currentMaxTextLength);
|
|
_dynamicText = _scrollState.originalText.substring(textStart, textEnd);
|
|
_scrollState.phaseCounter++;
|
|
if (_scrollState.phaseCounter >= _staticDuration) {
|
|
// Do NOT loop back to start, just stop scrolling
|
|
scrollTimer.stop();
|
|
}
|
|
}
|
|
} else {
|
|
scrollTimer.stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
SplitParser {
|
|
id: textStdoutSplit
|
|
onRead: line => root.parseDynamicContent(line)
|
|
}
|
|
|
|
StdioCollector {
|
|
id: textStdoutCollect
|
|
onStreamFinished: () => root.parseDynamicContent(this.text)
|
|
}
|
|
|
|
Process {
|
|
id: textProc
|
|
stdout: textStream ? textStdoutSplit : textStdoutCollect
|
|
stderr: StdioCollector {}
|
|
onExited: (exitCode, exitStatus) => {
|
|
if (textStream) {
|
|
Logger.w("CustomButton", `Streaming text command exited (code: ${exitCode}), restarting...`);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
function parseDynamicContent(content) {
|
|
var contentStr = String(content || "").trim();
|
|
|
|
if (parseJson && contentStr) {
|
|
// Handle multi-line JSON by filtering empty lines and joining
|
|
var jsonStr = contentStr;
|
|
if (contentStr.includes('\n')) {
|
|
const lines = contentStr.split('\n').filter(line => line.trim() !== '');
|
|
jsonStr = lines.join('');
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(jsonStr);
|
|
const text = parsed.text || "";
|
|
const icon = parsed.icon || "";
|
|
let tooltip = parsed.tooltip || "";
|
|
|
|
// Support both "color" (legacy) and "iconColor"/"textColor" (new)
|
|
const legacyColor = parsed.color || "";
|
|
const iconColorKey = parsed.iconColor || "";
|
|
const textColorKey = parsed.textColor || "";
|
|
|
|
const validColors = ["primary", "secondary", "tertiary", "error", "none"];
|
|
|
|
// Helper to resolve color: legacy > specific > none
|
|
function resolveColor(legacy, specific) {
|
|
if (legacy && validColors.includes(legacy)) return legacy;
|
|
if (specific && validColors.includes(specific)) return specific;
|
|
return "";
|
|
}
|
|
|
|
const resolvedIconColor = resolveColor(legacyColor, iconColorKey);
|
|
const resolvedTextColor = resolveColor(legacyColor, textColorKey);
|
|
|
|
if (checkCollapse(text)) {
|
|
_scrollState.originalText = "";
|
|
_dynamicText = "";
|
|
_dynamicIcon = "";
|
|
_dynamicTooltip = "";
|
|
_dynamicColor = "";
|
|
_dynamicIconColor = "";
|
|
_dynamicTextColor = "";
|
|
_scrollState.needsScrolling = false;
|
|
_scrollState.phase = 0;
|
|
_scrollState.phaseCounter = 0;
|
|
return;
|
|
}
|
|
|
|
_scrollState.originalText = text;
|
|
_scrollState.needsScrolling = text.length > currentMaxTextLength && currentMaxTextLength > 0;
|
|
if (_scrollState.needsScrolling) {
|
|
_dynamicText = text.substring(0, currentMaxTextLength);
|
|
_scrollState.phase = 0;
|
|
_scrollState.phaseCounter = 0;
|
|
_scrollState.offset = 0;
|
|
scrollTimer.start();
|
|
} else {
|
|
_dynamicText = text;
|
|
scrollTimer.stop();
|
|
}
|
|
_dynamicIcon = icon;
|
|
_dynamicColor = legacyColor; // Keep legacy color for fallback
|
|
_dynamicIconColor = resolvedIconColor;
|
|
_dynamicTextColor = resolvedTextColor;
|
|
|
|
_dynamicTooltip = toHtml(tooltip);
|
|
_scrollState.offset = 0;
|
|
return;
|
|
} catch (e) {
|
|
Logger.w("CustomButton", `Failed to parse JSON. Content: "${contentStr}"`);
|
|
}
|
|
}
|
|
|
|
if (checkCollapse(contentStr)) {
|
|
_scrollState.originalText = "";
|
|
_dynamicText = "";
|
|
_dynamicIcon = "";
|
|
_dynamicTooltip = "";
|
|
_dynamicColor = "";
|
|
_dynamicIconColor = "";
|
|
_dynamicTextColor = "";
|
|
_scrollState.needsScrolling = false;
|
|
_scrollState.phase = 0;
|
|
_scrollState.phaseCounter = 0;
|
|
return;
|
|
}
|
|
|
|
_scrollState.originalText = contentStr;
|
|
_scrollState.needsScrolling = contentStr.length > currentMaxTextLength && currentMaxTextLength > 0;
|
|
if (_scrollState.needsScrolling) {
|
|
_dynamicText = contentStr.substring(0, currentMaxTextLength);
|
|
_scrollState.phase = 0;
|
|
_scrollState.phaseCounter = 0;
|
|
_scrollState.offset = 0;
|
|
scrollTimer.start();
|
|
} else {
|
|
_dynamicText = contentStr;
|
|
scrollTimer.stop();
|
|
}
|
|
_dynamicIcon = "";
|
|
_dynamicColor = "";
|
|
_dynamicIconColor = "";
|
|
_dynamicTextColor = "";
|
|
_dynamicTooltip = toHtml(contentStr);
|
|
_scrollState.offset = 0;
|
|
}
|
|
|
|
function checkCollapse(text) {
|
|
if (!textCollapse || textCollapse.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
if (textCollapse.startsWith("/") && textCollapse.endsWith("/") && textCollapse.length > 1) {
|
|
// Treat as regex
|
|
var pattern = textCollapse.substring(1, textCollapse.length - 1);
|
|
try {
|
|
var regex = new RegExp(pattern);
|
|
return regex.test(text);
|
|
} catch (e) {
|
|
Logger.w("CustomButton", `Invalid regex for textCollapse: ${textCollapse} - ${e.message}`);
|
|
return (textCollapse === text); // Fallback to exact match on invalid regex
|
|
}
|
|
} else {
|
|
// Treat as plain string
|
|
return (textCollapse === text);
|
|
}
|
|
}
|
|
|
|
function clicked() {
|
|
if (leftClickExec) {
|
|
Quickshell.execDetached(["sh", "-lc", leftClickExec]);
|
|
Logger.i("CustomButton", `Executing command: ${leftClickExec}`);
|
|
} else if (!leftClickUpdateText) {
|
|
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
|
}
|
|
if (!textStream && leftClickUpdateText) {
|
|
runTextCommand();
|
|
}
|
|
}
|
|
|
|
function rightClicked() {
|
|
if (rightClickExec) {
|
|
Quickshell.execDetached(["sh", "-lc", rightClickExec]);
|
|
Logger.i("CustomButton", `Executing command: ${rightClickExec}`);
|
|
} else if (!rightClickUpdateText) {
|
|
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
|
}
|
|
if (!textStream && rightClickUpdateText) {
|
|
runTextCommand();
|
|
}
|
|
}
|
|
|
|
function middleClicked() {
|
|
if (middleClickExec) {
|
|
Quickshell.execDetached(["sh", "-lc", middleClickExec]);
|
|
Logger.i("CustomButton", `Executing command: ${middleClickExec}`);
|
|
} else if (!middleClickUpdateText) {
|
|
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
|
}
|
|
if (!textStream && middleClickUpdateText) {
|
|
runTextCommand();
|
|
}
|
|
}
|
|
|
|
function toHtml(str) {
|
|
const htmlTagRegex = /<\/?[a-zA-Z][^>]*>/g;
|
|
const placeholders = [];
|
|
let i = 0;
|
|
const protectedStr = str.replace(htmlTagRegex, tag => {
|
|
placeholders.push(tag);
|
|
return `___HTML_TAG_${i++}___`;
|
|
});
|
|
|
|
let escaped = protectedStr.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/\r\n|\r|\n/g, "<br/>");
|
|
|
|
escaped = escaped.replace(/___HTML_TAG_(\d+)___/g, (_, index) => placeholders[Number(index)]);
|
|
|
|
return escaped;
|
|
}
|
|
|
|
function runTextCommand() {
|
|
if (!textCommand || textCommand.length === 0)
|
|
return;
|
|
if (textProc.running)
|
|
return;
|
|
textProc.command = ["sh", "-lc", textCommand];
|
|
textProc.running = true;
|
|
}
|
|
|
|
function wheeled(delta) {
|
|
if (wheelMode === "unified" && wheelExec) {
|
|
let normalizedDelta = delta > 0 ? 1 : -1;
|
|
|
|
let command = wheelExec.replace(/\$delta([+\-*/]\d+)?/g, function (match, operation) {
|
|
if (operation) {
|
|
try {
|
|
let operator = operation.charAt(0);
|
|
let operand = parseInt(operation.substring(1));
|
|
|
|
let result;
|
|
switch (operator) {
|
|
case '+':
|
|
result = normalizedDelta + operand;
|
|
break;
|
|
case '-':
|
|
result = normalizedDelta - operand;
|
|
break;
|
|
case '*':
|
|
result = normalizedDelta * operand;
|
|
break;
|
|
case '/':
|
|
result = Math.floor(normalizedDelta / operand);
|
|
break;
|
|
default:
|
|
result = normalizedDelta;
|
|
}
|
|
|
|
return result.toString();
|
|
} catch (e) {
|
|
Logger.w("CustomButton", `Error evaluating expression: ${match}, using normalized value ${normalizedDelta}`);
|
|
return normalizedDelta.toString();
|
|
}
|
|
} else {
|
|
return normalizedDelta.toString();
|
|
}
|
|
});
|
|
|
|
Quickshell.execDetached(["sh", "-lc", command]);
|
|
Logger.i("CustomButton", `Executing command: ${command}`);
|
|
} else if (wheelMode === "separate") {
|
|
if ((delta > 0 && wheelUpExec) || (delta < 0 && wheelDownExec)) {
|
|
let commandExec = delta > 0 ? wheelUpExec : wheelDownExec;
|
|
let normalizedDelta = delta > 0 ? 1 : -1;
|
|
|
|
let command = commandExec.replace(/\$delta([+\-*/]\d+)?/g, function (match, operation) {
|
|
if (operation) {
|
|
try {
|
|
let operator = operation.charAt(0);
|
|
let operand = parseInt(operation.substring(1));
|
|
|
|
let result;
|
|
switch (operator) {
|
|
case '+':
|
|
result = normalizedDelta + operand;
|
|
break;
|
|
case '-':
|
|
result = normalizedDelta - operand;
|
|
break;
|
|
case '*':
|
|
result = normalizedDelta * operand;
|
|
break;
|
|
case '/':
|
|
result = Math.floor(normalizedDelta / operand);
|
|
break;
|
|
default:
|
|
result = normalizedDelta;
|
|
}
|
|
|
|
return result.toString();
|
|
} catch (e) {
|
|
Logger.w("CustomButton", `Error evaluating expression: ${match}, using normalized value ${normalizedDelta}`);
|
|
return normalizedDelta.toString();
|
|
}
|
|
} else {
|
|
return normalizedDelta.toString();
|
|
}
|
|
});
|
|
|
|
Quickshell.execDetached(["sh", "-lc", command]);
|
|
Logger.i("CustomButton", `Executing command: ${command}`);
|
|
}
|
|
}
|
|
|
|
if (!textStream) {
|
|
if (wheelMode === "unified" && wheelUpdateText) {
|
|
runTextCommand();
|
|
} else if (wheelMode === "separate") {
|
|
if ((delta > 0 && wheelUpUpdateText) || (delta < 0 && wheelDownUpdateText)) {
|
|
runTextCommand();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Register IPC button in onLoaded (called by BarWidgetLoader after properties are set).
|
|
// Component.onCompleted is too early — widgetSettings depends on section/index/screen
|
|
// which are set by BarWidgetLoader.onLoaded after the component is created.
|
|
function onLoaded() {
|
|
if (ipcIdentifier && ipcIdentifier.trim() !== "")
|
|
CustomButtonIPCService.registerButton(root);
|
|
}
|
|
|
|
Component.onDestruction: {
|
|
if (ipcIdentifier && ipcIdentifier.trim() !== "")
|
|
CustomButtonIPCService.unregisterButton(root);
|
|
}
|
|
}
|