This commit is contained in:
Ly-sec
2025-11-21 13:27:13 +01:00
19 changed files with 528 additions and 280 deletions
+4
View File
@@ -1372,6 +1372,10 @@
"description": "Zugriff auf zuvor kopierte Elemente über den Launcher.",
"label": "Zwischenablageverlauf aktivieren"
},
"clip-preview": {
"description": "Zeigt eine Vorschau des Inhalts der Zwischenablage an, wenn der Befehl >clip verwendet wird.",
"label": "Clip-Vorschau aktivieren"
},
"custom-launch-prefix": {
"description": "Befehle mit einem benutzerdefinierten Launcher präfixieren (z.B. 'runapp' für systemd-Integration).",
"label": "Benutzerdefiniertes Start-Präfix"
+4
View File
@@ -1372,6 +1372,10 @@
"description": "Access previously copied items from the launcher.",
"label": "Enable clipboard history"
},
"clip-preview": {
"description": "Show a preview of the clipboard content when using the >clip command.",
"label": "Enable clip preview"
},
"custom-launch-prefix": {
"description": "Prefix commands with a custom launcher (e.g., 'runapp' for systemd integration).",
"label": "Custom launch prefix"
+4
View File
@@ -1372,6 +1372,10 @@
"description": "Accede a los elementos copiados anteriormente desde el lanzador.",
"label": "Activar historial del portapapeles"
},
"clip-preview": {
"description": "Muestra una vista previa del contenido del portapapeles al usar el comando >clip.",
"label": "Activar vista previa del portapapeles"
},
"custom-launch-prefix": {
"description": "Prefijar comandos con un lanzador personalizado (ej. 'runapp' para integración con systemd).",
"label": "Prefijo de lanzamiento personalizado"
+4
View File
@@ -1372,6 +1372,10 @@
"description": "Accédez aux éléments précédemment copiés depuis le lanceur.",
"label": "Activer l'historique du presse-papiers"
},
"clip-preview": {
"description": "Afficher un aperçu du contenu du presse-papiers lors de l'utilisation de la commande >clip.",
"label": "Activer l'aperçu du presse-papiers"
},
"custom-launch-prefix": {
"description": "Préfixer les commandes avec un lanceur personnalisé (ex. 'runapp' pour l'intégration systemd).",
"label": "Préfixe de lancement personnalisé"
+4
View File
@@ -1372,6 +1372,10 @@
"description": "Toegang tot eerder gekopieerde items vanuit de launcher.",
"label": "Klembordgeschiedenis inschakelen"
},
"clip-preview": {
"description": "Toon een voorbeeld van de inhoud van het klembord bij gebruik van het >clip-commando.",
"label": "Klembordvoorbeeld inschakelen"
},
"custom-launch-prefix": {
"description": "Voorzie commando's van een aangepaste launcher-prefix (bijv. 'runapp' voor systemd-integratie).",
"label": "Aangepaste startprefix"
+4
View File
@@ -1372,6 +1372,10 @@
"description": "Acesse itens copiados anteriormente a partir do lançador.",
"label": "Ativar histórico da área de transferência"
},
"clip-preview": {
"description": "Mostra uma pré-visualização do conteúdo da área de transferência ao usar o comando >clip.",
"label": "Ativar pré-visualização da área de transferência"
},
"custom-launch-prefix": {
"description": "Prefixar comandos com um inicializador personalizado (ex. 'runapp' para integração systemd).",
"label": "Prefixo de inicialização personalizado"
+4
View File
@@ -1372,6 +1372,10 @@
"description": "Доступ к ранее скопированным элементам из запуска.",
"label": "Включить историю буфера обмена"
},
"clip-preview": {
"description": "Показывать предварительный просмотр содержимого буфера обмена при использовании команды >clip.",
"label": "Включить предварительный просмотр буфера обмена"
},
"custom-launch-prefix": {
"description": "Добавлять префикс к командам с помощью пользовательского запуска (например, 'runapp' для интеграции с systemd).",
"label": "Пользовательский префикс запуска"
+4
View File
@@ -1372,6 +1372,10 @@
"description": "Başlatıcıdan daha önce kopyalanan öğelere erişin.",
"label": "Pano geçmişini etkinleştir"
},
"clip-preview": {
"description": ">clip komutu kullanılırken panodaki içeriğin önizlemesini gösterir.",
"label": "Panoyu önizlemeyi etkinleştir"
},
"custom-launch-prefix": {
"description": "Komutlara özel bir başlatıcı ile ön ek ekleyin (örn., systemd entegrasyonu için 'runapp').",
"label": "Özel başlatma ön eki"
+4
View File
@@ -1372,6 +1372,10 @@
"description": "Отримати доступ до раніше скопійованих елементів із запускача.",
"label": "Увімкнути історію буфера обміну"
},
"clip-preview": {
"description": "Показувати попередній перегляд вмісту буфера обміну при використанні команди >clip.",
"label": "Увімкнути попередній перегляд буфера обміну"
},
"custom-launch-prefix": {
"description": "Додати префікс до команд власним запускачем (напр., 'runapp' для інтеграції з systemd).",
"label": "Власний префікс запуску"
+4
View File
@@ -1372,6 +1372,10 @@
"description": "从启动器访问之前复制的项目。",
"label": "启用剪贴板历史记录"
},
"clip-preview": {
"description": "在使用 >clip 命令时显示剪贴板内容的预览。",
"label": "启用剪贴板预览"
},
"custom-launch-prefix": {
"description": "使用自定义启动器前缀命令(例如,'runapp'用于systemd集成)。",
"label": "自定义启动前缀"
+1
View File
@@ -143,6 +143,7 @@
},
"appLauncher": {
"enableClipboardHistory": false,
"enableClipPreview": true,
"position": "center",
"pinnedExecs": [],
"useApp2Unit": false,
+1
View File
@@ -296,6 +296,7 @@ Singleton {
// applauncher
property JsonObject appLauncher: JsonObject {
property bool enableClipboardHistory: false
property bool enableClipPreview: true
// Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center
property string position: "center"
property list<string> pinnedExecs: []
+34
View File
@@ -0,0 +1,34 @@
.pragma library
/**
* Wrap text in a nicely styled HTML container for display
* @param {string} text - The text to display
* @returns {string} HTML string
*/
function wrapTextForDisplay(text) {
// Escape HTML special characters
const escapeHtml = (s) =>
s.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
return `
<div style="
font-family: 'Fira Code', 'Courier New', monospace;
white-space: pre-wrap;
background: linear-gradient(135deg, #2c3e50, #34495e);
color: #ecf0f1;
padding: 16px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
overflow-x: auto;
line-height: 1.5;
font-size: 14px;
border: 1px solid #3d566e;
">
${escapeHtml(text)}
</div>
`;
}
@@ -0,0 +1,127 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "../../../Helpers/TextFormatter.js" as TextFormatter
import qs.Commons
import qs.Services.Keyboard
import qs.Widgets
Item {
id: previewPanel
property var currentItem: null
property string fullContent: ""
property string imageDataUrl: ""
property bool loadingFullContent: false
property bool isImageContent: false
implicitHeight: contentColumn.implicitHeight + Style.marginL * 2
Connections {
target: previewPanel
function onCurrentItemChanged() {
fullContent = "";
imageDataUrl = "";
loadingFullContent = false;
isImageContent = currentItem && currentItem.isImage;
if (currentItem && currentItem.clipboardId) {
if (isImageContent) {
imageDataUrl = ClipboardService.getImageData(currentItem.clipboardId) || "";
loadingFullContent = !imageDataUrl;
if (!imageDataUrl && currentItem.mime) {
ClipboardService.decodeToDataUrl(currentItem.clipboardId, currentItem.mime, null);
}
} else {
loadingFullContent = true;
ClipboardService.decode(currentItem.clipboardId, function (content) {
fullContent = TextFormatter.wrapTextForDisplay(content);
loadingFullContent = false;
});
}
}
}
}
readonly property int _rev: ClipboardService.revision
Timer {
id: imageUpdateTimer
interval: 200
running: currentItem && currentItem.isImage && imageDataUrl === ""
repeat: currentItem && currentItem.isImage && imageDataUrl === ""
onTriggered: {
if (currentItem && currentItem.clipboardId) {
const newData = ClipboardService.getImageData(currentItem.clipboardId) || "";
if (newData !== imageDataUrl) {
imageDataUrl = newData;
if (newData) {
loadingFullContent = false;
}
}
}
}
}
Rectangle {
anchors.fill: parent
color: Color.mSurface || "#f5f5f5"
border.color: Color.mOutlineVariant || "#cccccc"
border.width: 1
radius: Style.radiusM
ColumnLayout {
id: contentColumn
anchors.fill: parent
anchors.margins: Style.marginS
spacing: Style.marginS
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.mSurfaceVariant || "#e0e0e0"
border.color: Color.mOutline || "#aaaaaa"
border.width: 1
radius: Style.radiusS
BusyIndicator {
anchors.centerIn: parent
running: loadingFullContent
visible: loadingFullContent
width: Style.baseWidgetSize
height: width
}
Item {
anchors.fill: parent
anchors.margins: Style.marginS
NImageRounded {
anchors.fill: parent
imagePath: imageDataUrl
visible: isImageContent && !loadingFullContent && imageDataUrl !== ""
imageRadius: Style.radiusS
imageFillMode: Image.PreserveAspectFit
}
ScrollView {
anchors.fill: parent
clip: true
visible: !isImageContent && !loadingFullContent
TextArea {
text: fullContent
readOnly: true
wrapMode: Text.Wrap
textFormat: TextArea.RichText // Enable HTML rendering
font.pointSize: Style.fontSizeM // Adjust font size for readability
color: Color.mOnSurface // Consistent text color
}
}
}
}
}
}
}
+303 -259
View File
@@ -13,8 +13,14 @@ import qs.Widgets
SmartPanel {
id: root
readonly property bool previewActive: searchText.startsWith(">clip") && Settings.data.appLauncher.enableClipPreview
// Panel configuration
preferredWidth: Math.round(500 * Style.uiScaleRatio)
readonly property int listPanelWidth: Math.round(600 * Style.uiScaleRatio)
readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio)
readonly property int totalBaseWidth: listPanelWidth + (Style.marginL * 2)
preferredWidth: totalBaseWidth
preferredHeight: Math.round(600 * Style.uiScaleRatio)
preferredWidthRatio: 0.3
preferredHeightRatio: 0.5
@@ -262,13 +268,50 @@ SmartPanel {
}
}
// UI
panelContent: Rectangle {
id: ui
color: Color.transparent
opacity: resultsReady ? 1.0 : 0.0
// Global MouseArea to detect mouse movement
// Preview Panel (external)
NBox {
id: previewBox
visible: root.previewActive
width: root.previewPanelWidth
height: Math.round(400 * Style.uiScaleRatio)
x: ui.width + Style.marginM
y: Math.max(Style.marginL // Minimum y is the top margin of the content area
, Math.min(resultsList.mapToItem(ui, 0, (root.selectedIndex * (root.entryHeight + resultsList.spacing)) - resultsList.contentY).y, ui.height - previewBox.height - Style.marginL // Maximum y, considering bottom margin
))
z: -1 // Draw behind main panel content if it ever overlaps
opacity: visible ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
Loader {
id: clipboardPreviewLoader
anchors.fill: parent
active: root.previewActive
source: active ? "./ClipboardPreview.qml" : ""
onLoaded: {
if (selectedIndex >= 0 && results[selectedIndex] && item) {
item.currentItem = results[selectedIndex];
}
}
onItemChanged: {
if (item && selectedIndex >= 0 && results[selectedIndex]) {
item.currentItem = results[selectedIndex];
}
}
}
}
MouseArea {
id: mouseMovementDetector
anchors.fill: parent
@@ -282,7 +325,6 @@ SmartPanel {
property bool initialized: false
onPositionChanged: mouse => {
// Store initial position
if (!initialized) {
lastX = mouse.x;
lastY = mouse.y;
@@ -290,7 +332,6 @@ SmartPanel {
return;
}
// Check if mouse actually moved
const deltaX = Math.abs(mouse.x - lastX);
const deltaY = Math.abs(mouse.y - lastY);
if (deltaX > 1 || deltaY > 1) {
@@ -300,7 +341,6 @@ SmartPanel {
}
}
// Reset when launcher opens
Connections {
target: root
function onOpened() {
@@ -329,298 +369,302 @@ SmartPanel {
}
}
ColumnLayout {
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
anchors.margins: Style.marginL // Apply overall margins here
spacing: Style.marginM // Apply spacing between elements here
NTextInput {
id: searchInput
Layout.fillWidth: true
fontSize: Style.fontSizeL
fontWeight: Style.fontWeightSemiBold
text: searchText
placeholderText: I18n.tr("placeholders.search-launcher")
onTextChanged: searchText = text
Component.onCompleted: {
if (searchInput.inputItem) {
searchInput.inputItem.forceActiveFocus();
// Intercept Tab keys before TextField handles them
searchInput.inputItem.Keys.onPressed.connect(function (event) {
if (event.key === Qt.Key_Tab) {
root.onTabPressed();
event.accepted = true;
} else if (event.key === Qt.Key_Backtab) {
root.onBackTabPressed();
event.accepted = true;
}
});
}
}
}
// Results list
NListView {
id: resultsList
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
Layout.fillWidth: true
// Left Pane
ColumnLayout {
id: leftPane
Layout.fillHeight: true
spacing: Style.marginXXS
model: results
currentIndex: selectedIndex
cacheBuffer: resultsList.height * 2
onCurrentIndexChanged: {
cancelFlick();
if (currentIndex >= 0) {
positionViewAtIndex(currentIndex, ListView.Contain);
Layout.preferredWidth: root.listPanelWidth
spacing: Style.marginM
NTextInput {
id: searchInput
Layout.fillWidth: true
fontSize: Style.fontSizeL
fontWeight: Style.fontWeightSemiBold
text: searchText
placeholderText: I18n.tr("placeholders.search-launcher")
onTextChanged: searchText = text
Component.onCompleted: {
if (searchInput.inputItem) {
searchInput.inputItem.forceActiveFocus();
// Intercept Tab keys before TextField handles them
searchInput.inputItem.Keys.onPressed.connect(function (event) {
if (event.key === Qt.Key_Tab) {
root.onTabPressed();
event.accepted = true;
} else if (event.key === Qt.Key_Backtab) {
root.onBackTabPressed();
event.accepted = true;
}
});
}
}
}
onModelChanged: {}
delegate: Rectangle {
id: entry
NListView {
id: resultsList
property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === selectedIndex)
// Accessor for app id
property string appId: (modelData && modelData.appId) ? String(modelData.appId) : ""
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
// Pin helpers
function togglePin(appId) {
if (!appId)
return;
let arr = (Settings.data.dock.pinnedApps || []).slice();
const idx = arr.indexOf(appId);
if (idx >= 0)
arr.splice(idx, 1);
else
arr.push(appId);
Settings.data.dock.pinnedApps = arr;
}
function isPinned(appId) {
const arr = Settings.data.dock.pinnedApps || [];
return appId && arr.indexOf(appId) >= 0;
}
// Property to reliably track the current item's ID.
// This changes whenever the delegate is recycled for a new item.
property var currentClipboardId: modelData.isImage ? modelData.clipboardId : ""
// When this delegate is assigned a new image item, trigger the decode.
onCurrentClipboardIdChanged: {
// Check if it's a valid ID and if the data isn't already cached.
if (currentClipboardId && !ClipboardService.getImageData(currentClipboardId)) {
ClipboardService.decodeToDataUrl(currentClipboardId, modelData.mime, null);
Layout.fillWidth: true
Layout.fillHeight: true
spacing: Style.marginXXS
model: results
currentIndex: selectedIndex
cacheBuffer: resultsList.height * 2
onCurrentIndexChanged: {
cancelFlick();
if (currentIndex >= 0) {
positionViewAtIndex(currentIndex, ListView.Contain);
}
if (clipboardPreviewLoader.item) {
clipboardPreviewLoader.item.currentItem = results[currentIndex] || null;
}
}
onModelChanged: {}
width: resultsList.width - Style.marginS
implicitHeight: entryHeight
radius: Style.radiusM
color: entry.isSelected ? Color.mHover : Color.mSurface
delegate: Rectangle {
id: entry
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCirc
property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === selectedIndex)
property string appId: (modelData && modelData.appId) ? String(modelData.appId) : ""
// Pin helpers
function togglePin(appId) {
if (!appId)
return;
let arr = (Settings.data.dock.pinnedApps || []).slice();
const idx = arr.indexOf(appId);
if (idx >= 0)
arr.splice(idx, 1);
else
arr.push(appId);
Settings.data.dock.pinnedApps = arr;
}
}
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
function isPinned(appId) {
const arr = Settings.data.dock.pinnedApps || [];
return appId && arr.indexOf(appId) >= 0;
}
// Top row - Main entry content with pin button
RowLayout {
Layout.fillWidth: true
// Property to reliably track the current item's ID.
// This changes whenever the delegate is recycled for a new item.
property var currentClipboardId: modelData.isImage ? modelData.clipboardId : ""
// When this delegate is assigned a new image item, trigger the decode.
onCurrentClipboardIdChanged: {
// Check if it's a valid ID and if the data isn't already cached.
if (currentClipboardId && !ClipboardService.getImageData(currentClipboardId)) {
ClipboardService.decodeToDataUrl(currentClipboardId, modelData.mime, null);
}
}
width: resultsList.width - Style.marginS
implicitHeight: entryHeight
radius: Style.radiusM
color: entry.isSelected ? Color.mHover : Color.mSurface
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCirc
}
}
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
// Icon badge or Image preview
Rectangle {
Layout.preferredWidth: badgeSize
Layout.preferredHeight: badgeSize
radius: Style.radiusM
color: Color.mSurfaceVariant
// Top row - Main entry content with pin button
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
// Image preview for clipboard images
NImageRounded {
id: imagePreview
anchors.fill: parent
visible: modelData.isImage
imageRadius: Style.radiusM
// This property creates a dependency on the service's revision counter
readonly property int _rev: ClipboardService.revision
// Fetches from the service's cache.
// The dependency on `_rev` ensures this binding is re-evaluated when the cache is updated.
imagePath: {
_rev;
return ClipboardService.getImageData(modelData.clipboardId) || "";
}
// Loading indicator
Rectangle {
anchors.fill: parent
visible: parent.status === Image.Loading
color: Color.mSurfaceVariant
BusyIndicator {
anchors.centerIn: parent
running: true
width: Style.baseWidgetSize * 0.5
height: width
}
}
// Error fallback
onStatusChanged: status => {
if (status === Image.Error) {
iconLoader.visible = true;
imagePreview.visible = false;
}
}
}
// Icon fallback
Loader {
id: iconLoader
anchors.fill: parent
anchors.margins: Style.marginXS
visible: !modelData.isImage || imagePreview.status === Image.Error
active: visible
sourceComponent: Component {
IconImage {
anchors.fill: parent
source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : ""
visible: modelData.icon && source !== ""
asynchronous: true
}
}
}
// Fallback text if no icon and no image
NText {
anchors.centerIn: parent
visible: !imagePreview.visible && !iconLoader.visible
text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?"
pointSize: Style.fontSizeXXL
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
}
// Image type indicator overlay
// Icon badge or Image preview
Rectangle {
visible: modelData.isImage && imagePreview.visible
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2
width: formatLabel.width + 6
height: formatLabel.height + 2
Layout.preferredWidth: badgeSize
Layout.preferredHeight: badgeSize
radius: Style.radiusM
color: Color.mSurfaceVariant
NText {
id: formatLabel
anchors.centerIn: parent
text: {
if (!modelData.isImage)
return "";
const desc = modelData.description || "";
const parts = desc.split(" • ");
return parts[0] || "IMG";
// Image preview for clipboard images
NImageRounded {
id: imagePreview
anchors.fill: parent
visible: modelData.isImage
imageRadius: Style.radiusM
// This property creates a dependency on the service's revision counter
readonly property int _rev: ClipboardService.revision
// Fetches from the service's cache.
// The dependency on `_rev` ensures this binding is re-evaluated when the cache is updated.
imagePath: {
_rev;
return ClipboardService.getImageData(modelData.clipboardId) || "";
}
Rectangle {
anchors.fill: parent
visible: parent.status === Image.Loading
color: Color.mSurfaceVariant
BusyIndicator {
anchors.centerIn: parent
running: true
width: Style.baseWidgetSize * 0.5
height: width
}
}
onStatusChanged: status => {
if (status === Image.Error) {
iconLoader.visible = true;
imagePreview.visible = false;
}
}
}
Loader {
id: iconLoader
anchors.fill: parent
anchors.margins: Style.marginXS
visible: !modelData.isImage || imagePreview.status === Image.Error
active: visible
sourceComponent: Component {
IconImage {
anchors.fill: parent
source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : ""
visible: modelData.icon && source !== ""
asynchronous: true
}
}
}
NText {
anchors.centerIn: parent
visible: !imagePreview.visible && !iconLoader.visible
text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?"
pointSize: Style.fontSizeXXL
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
}
// Image type indicator overlay
Rectangle {
visible: modelData.isImage && imagePreview.visible
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2
width: formatLabel.width + 6
height: formatLabel.height + 2
radius: Style.radiusM
color: Color.mSurfaceVariant
NText {
id: formatLabel
anchors.centerIn: parent
text: {
if (!modelData.isImage)
return "";
const desc = modelData.description || "";
const parts = desc.split(" • ");
return parts[0] || "IMG";
}
pointSize: Style.fontSizeXXS
color: Color.mPrimary
}
pointSize: Style.fontSizeXXS
color: Color.mPrimary
}
}
}
// Text content
ColumnLayout {
Layout.fillWidth: true
spacing: 0
NText {
text: modelData.name || "Unknown"
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: entry.isSelected ? Color.mOnHover : Color.mOnSurface
elide: Text.ElideRight
// Text content
ColumnLayout {
Layout.fillWidth: true
spacing: 0
NText {
text: modelData.name || "Unknown"
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: entry.isSelected ? Color.mOnHover : Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
}
NText {
text: modelData.description || ""
pointSize: Style.fontSizeS
color: entry.isSelected ? Color.mOnHover : Color.mOnSurfaceVariant
elide: Text.ElideRight
Layout.fillWidth: true
visible: text !== ""
}
}
NText {
text: modelData.description || ""
pointSize: Style.fontSizeS
color: entry.isSelected ? Color.mOnHover : Color.mOnSurfaceVariant
elide: Text.ElideRight
Layout.fillWidth: true
visible: text !== ""
// Pin/Unpin action icon button
NIconButton {
visible: !!entry.appId && !modelData.isImage && entry.isSelected && (Settings.data.dock.monitors && Settings.data.dock.monitors.length > 0)
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
icon: entry.isPinned(entry.appId) ? "unpin" : "pin"
tooltipText: entry.isPinned(entry.appId) ? I18n.tr("launcher.unpin") : I18n.tr("launcher.pin")
onClicked: entry.togglePin(entry.appId)
}
}
// Pin/Unpin action icon button
NIconButton {
visible: !!entry.appId && !modelData.isImage && entry.isSelected && (Settings.data.dock.monitors && Settings.data.dock.monitors.length > 0)
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
icon: entry.isPinned(entry.appId) ? "unpin" : "pin"
tooltipText: entry.isPinned(entry.appId) ? I18n.tr("launcher.unpin") : I18n.tr("launcher.pin")
onClicked: entry.togglePin(entry.appId)
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
z: -1
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
if (!root.ignoreMouseHover) {
selectedIndex = index;
MouseArea {
id: mouseArea
anchors.fill: parent
z: -1
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
if (!root.ignoreMouseHover) {
selectedIndex = index;
}
}
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
selectedIndex = index;
root.activate();
mouse.accepted = true;
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
selectedIndex = index;
root.activate();
mouse.accepted = true;
}
}
}
acceptedButtons: Qt.LeftButton
acceptedButtons: Qt.LeftButton
}
}
}
}
NDivider {
Layout.fillWidth: true
}
// Status
NText {
Layout.fillWidth: true
text: {
if (results.length === 0)
return searchText ? "No results" : "";
const prefix = activePlugin?.name ? `${activePlugin.name}: ` : "";
return prefix + `${results.length} result${results.length !== 1 ? 's' : ''}`;
NDivider {
Layout.fillWidth: true
}
NText {
Layout.fillWidth: true
text: {
if (results.length === 0)
return searchText ? "No results" : "";
const prefix = activePlugin?.name ? `${activePlugin.name}: ` : "";
return prefix + `${results.length} result${results.length !== 1 ? 's' : ''}`;
}
pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
horizontalAlignment: Text.AlignCenter
}
pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
horizontalAlignment: Text.AlignCenter
}
}
}
@@ -204,12 +204,9 @@ Item {
return results;
}
// Helper: Format image clipboard entry
function formatImageEntry(item) {
const meta = parseImageMeta(item.preview);
// The launcher's delegate will now be responsible for fetching the image data.
// This function's role is to provide the necessary metadata for that request.
return {
"name": meta ? `Image ${meta.w}×${meta.h}` : "Image",
"description": meta ? `${meta.fmt} ${meta.size}` : item.mime || "Image data",
@@ -218,22 +215,20 @@ Item {
"imageWidth": meta ? meta.w : 0,
"imageHeight": meta ? meta.h : 0,
"clipboardId": item.id,
"mime": item.mime
"mime": item.mime,
"preview": item.preview
};
}
// Helper: Format text clipboard entry with preview
function formatTextEntry(item) {
const preview = (item.preview || "").trim();
const lines = preview.split('\n').filter(l => l.trim());
// Use first line as title, limit length
let title = lines[0] || "Empty text";
if (title.length > 60) {
title = title.substring(0, 57) + "...";
}
// Use second line or character count as description
let description = "";
if (lines.length > 1) {
description = lines[1];
@@ -250,11 +245,12 @@ Item {
"name": title,
"description": description,
"icon": "text-x-generic",
"isImage": false
"isImage": false,
"clipboardId": item.id,
"preview": preview
};
}
// Helper: Parse image metadata from preview string
function parseImageMeta(preview) {
const re = /\[\[\s*binary data\s+([\d\.]+\s*(?:KiB|MiB|GiB|B))\s+(\w+)\s+(\d+)x(\d+)\s*\]\]/i;
const match = (preview || "").match(re);
@@ -271,8 +267,6 @@ Item {
};
}
// Public method to get image data for a clipboard item
// This can be called by the launcher when rendering
function getImageForItem(clipboardId) {
return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null;
}
@@ -65,6 +65,13 @@ ColumnLayout {
onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked
}
NToggle {
label: I18n.tr("settings.launcher.settings.clip-preview.label")
description: I18n.tr("settings.launcher.settings.clip-preview.description")
checked: Settings.data.appLauncher.enableClipPreview
onToggled: checked => Settings.data.appLauncher.enableClipPreview = checked
}
NToggle {
label: I18n.tr("settings.launcher.settings.sort-by-usage.label")
description: I18n.tr("settings.launcher.settings.sort-by-usage.description")
+5 -3
View File
@@ -4,10 +4,12 @@ import Quickshell.Widgets
import qs.Commons
Rectangle {
width: parent.width
height: Style.borderS
property bool vertical: false
width: vertical ? Style.borderS : parent.width
height: vertical ? parent.height : Style.borderS
gradient: Gradient {
orientation: Gradient.Horizontal
orientation: vertical ? Gradient.Vertical : Gradient.Horizontal
GradientStop {
position: 0.0
color: Color.transparent
+5 -7
View File
@@ -11,6 +11,7 @@ Rectangle {
property color borderColor: Color.transparent
property real borderWidth: 0
property real imageRadius: width * 0.5
property int imageFillMode: Image.PreserveAspectCrop
property string fallbackIcon: ""
property real fallbackIconSize: Style.fontSizeXXL
@@ -30,12 +31,14 @@ Rectangle {
id: img
anchors.fill: parent
source: imagePath
visible: false // Hide since we're using it as shader source
visible: false
mipmap: true
smooth: true
asynchronous: true
antialiasing: true
fillMode: Image.PreserveAspectCrop
fillMode: root.imageFillMode
horizontalAlignment: Image.AlignHCenter
verticalAlignment: Image.AlignVCenter
onStatusChanged: root.statusChanged(status)
}
@@ -51,17 +54,14 @@ Rectangle {
format: ShaderEffectSource.RGBA
}
// Use custom property names to avoid conflicts with final properties
property real itemWidth: root.width
property real itemHeight: root.height
property real cornerRadius: root.radius
property real imageOpacity: root.opacity
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb")
// Qt6 specific properties - ensure proper blending
supportsAtlasTextures: false
blending: true
// Make sure the background is transparent
Rectangle {
id: background
anchors.fill: parent
@@ -70,7 +70,6 @@ Rectangle {
}
}
// Fallback icon
Loader {
active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
anchors.centerIn: parent
@@ -83,7 +82,6 @@ Rectangle {
}
}
// Border
Rectangle {
anchors.fill: parent
radius: parent.radius