Add AppLauncher
This commit is contained in:
parent
4635aec80e
commit
2b39cbfe01
6 changed files with 788 additions and 19 deletions
120
Helpers/MathHelper.js
Normal file
120
Helpers/MathHelper.js
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
// Math helper functions for calculator functionality
|
||||||
|
var MathHelper = {
|
||||||
|
// Basic arithmetic operations
|
||||||
|
add: (a, b) => a + b,
|
||||||
|
subtract: (a, b) => a - b,
|
||||||
|
multiply: (a, b) => a * b,
|
||||||
|
divide: (a, b) => b !== 0 ? a / b : NaN,
|
||||||
|
|
||||||
|
// Power and roots
|
||||||
|
pow: (base, exponent) => Math.pow(base, exponent),
|
||||||
|
sqrt: (x) => x >= 0 ? Math.sqrt(x) : NaN,
|
||||||
|
cbrt: (x) => Math.cbrt(x),
|
||||||
|
|
||||||
|
// Trigonometric functions (in radians)
|
||||||
|
sin: (x) => Math.sin(x),
|
||||||
|
cos: (x) => Math.cos(x),
|
||||||
|
tan: (x) => Math.tan(x),
|
||||||
|
asin: (x) => Math.asin(x),
|
||||||
|
acos: (x) => Math.acos(x),
|
||||||
|
atan: (x) => Math.atan(x),
|
||||||
|
|
||||||
|
// Logarithmic functions
|
||||||
|
log: (x) => x > 0 ? Math.log(x) : NaN,
|
||||||
|
log10: (x) => x > 0 ? Math.log10(x) : NaN,
|
||||||
|
log2: (x) => x > 0 ? Math.log2(x) : NaN,
|
||||||
|
|
||||||
|
// Other mathematical functions
|
||||||
|
abs: (x) => Math.abs(x),
|
||||||
|
floor: (x) => Math.floor(x),
|
||||||
|
ceil: (x) => Math.ceil(x),
|
||||||
|
round: (x) => Math.round(x),
|
||||||
|
min: (...args) => Math.min(...args),
|
||||||
|
max: (...args) => Math.max(...args),
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
PI: Math.PI,
|
||||||
|
E: Math.E,
|
||||||
|
|
||||||
|
// Factorial
|
||||||
|
factorial: (n) => {
|
||||||
|
if (n < 0 || n !== Math.floor(n)) return NaN;
|
||||||
|
if (n === 0 || n === 1) return 1;
|
||||||
|
let result = 1;
|
||||||
|
for (let i = 2; i <= n; i++) {
|
||||||
|
result *= i;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Percentage
|
||||||
|
percent: (value, total) => (value / total) * 100,
|
||||||
|
|
||||||
|
// Degrees to radians and vice versa
|
||||||
|
toRadians: (degrees) => degrees * (Math.PI / 180),
|
||||||
|
toDegrees: (radians) => radians * (180 / Math.PI),
|
||||||
|
|
||||||
|
// Safe evaluation with math functions
|
||||||
|
evaluate: (expression) => {
|
||||||
|
try {
|
||||||
|
// Replace common math functions with MathHelper equivalents
|
||||||
|
let processedExpr = expression
|
||||||
|
.replace(/\bpi\b/gi, 'MathHelper.PI')
|
||||||
|
.replace(/\be\b/gi, 'MathHelper.E')
|
||||||
|
.replace(/\bsin\b/gi, 'MathHelper.sin')
|
||||||
|
.replace(/\bcos\b/gi, 'MathHelper.cos')
|
||||||
|
.replace(/\btan\b/gi, 'MathHelper.tan')
|
||||||
|
.replace(/\basin\b/gi, 'MathHelper.asin')
|
||||||
|
.replace(/\bacos\b/gi, 'MathHelper.acos')
|
||||||
|
.replace(/\batan\b/gi, 'MathHelper.atan')
|
||||||
|
.replace(/\blog\b/gi, 'MathHelper.log')
|
||||||
|
.replace(/\blog10\b/gi, 'MathHelper.log10')
|
||||||
|
.replace(/\blog2\b/gi, 'MathHelper.log2')
|
||||||
|
.replace(/\bsqrt\b/gi, 'MathHelper.sqrt')
|
||||||
|
.replace(/\bcbrt\b/gi, 'MathHelper.cbrt')
|
||||||
|
.replace(/\bpow\b/gi, 'MathHelper.pow')
|
||||||
|
.replace(/\babs\b/gi, 'MathHelper.abs')
|
||||||
|
.replace(/\bfloor\b/gi, 'MathHelper.floor')
|
||||||
|
.replace(/\bceil\b/gi, 'MathHelper.ceil')
|
||||||
|
.replace(/\bround\b/gi, 'MathHelper.round')
|
||||||
|
.replace(/\bmin\b/gi, 'MathHelper.min')
|
||||||
|
.replace(/\bmax\b/gi, 'MathHelper.max')
|
||||||
|
.replace(/\bfactorial\b/gi, 'MathHelper.factorial')
|
||||||
|
.replace(/\bpercent\b/gi, 'MathHelper.percent')
|
||||||
|
.replace(/\btoRadians\b/gi, 'MathHelper.toRadians')
|
||||||
|
.replace(/\btoDegrees\b/gi, 'MathHelper.toDegrees');
|
||||||
|
|
||||||
|
// Evaluate the expression
|
||||||
|
const result = Function('MathHelper', 'return ' + processedExpr)(MathHelper);
|
||||||
|
|
||||||
|
// Check if result is valid
|
||||||
|
if (isNaN(result) || !isFinite(result)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Format result for display
|
||||||
|
formatResult: (result) => {
|
||||||
|
if (result === null || isNaN(result) || !isFinite(result)) {
|
||||||
|
return "Error";
|
||||||
|
}
|
||||||
|
|
||||||
|
// For very large or small numbers, use scientific notation
|
||||||
|
if (Math.abs(result) >= 1e10 || (Math.abs(result) < 1e-10 && result !== 0)) {
|
||||||
|
return result.toExponential(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For integers, don't show decimal places
|
||||||
|
if (Number.isInteger(result)) {
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For decimals, limit to 8 significant digits
|
||||||
|
return parseFloat(result.toPrecision(8)).toString();
|
||||||
|
}
|
||||||
|
};
|
||||||
519
Modules/AppLauncher/AppLauncher.qml
Normal file
519
Modules/AppLauncher/AppLauncher.qml
Normal file
|
|
@ -0,0 +1,519 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
import "../../Helpers/FuzzySort.js" as Fuzzysort
|
||||||
|
import "../../Helpers/MathHelper.js" as MathHelper
|
||||||
|
|
||||||
|
NLoader {
|
||||||
|
id: appLauncher
|
||||||
|
isLoaded: false
|
||||||
|
// Clipboard state is persisted in Services/Clipboard.qml
|
||||||
|
content: Component {
|
||||||
|
NPanel {
|
||||||
|
id: appLauncherPanel
|
||||||
|
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||||
|
|
||||||
|
// No local timer/processes; use persistent Clipboard service
|
||||||
|
|
||||||
|
// Removed local clipboard processes; handled by Clipboard service
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Copy helpers via simple exec; avoid keeping processes alive locally
|
||||||
|
function copyImageBase64(mime, base64) {
|
||||||
|
Quickshell.execDetached(["sh", "-lc", `printf %s ${base64} | base64 -d | wl-copy -t '${mime}'`])
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyText(text) {
|
||||||
|
Quickshell.execDetached(["sh", "-lc", `printf %s ${text} | wl-copy -t text/plain;charset=utf-8`])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function updateClipboardHistory() {
|
||||||
|
Clipboard.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNext() {
|
||||||
|
if (filteredEntries.length > 0) {
|
||||||
|
selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPrev() {
|
||||||
|
if (filteredEntries.length > 0) {
|
||||||
|
selectedIndex = Math.max(selectedIndex - 1, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateSelected() {
|
||||||
|
if (filteredEntries.length === 0) return;
|
||||||
|
|
||||||
|
var modelData = filteredEntries[selectedIndex];
|
||||||
|
if (modelData && modelData.execute) {
|
||||||
|
if (modelData.isCommand) {
|
||||||
|
modelData.execute();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
modelData.execute();
|
||||||
|
}
|
||||||
|
appLauncherPanel.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property var desktopEntries: DesktopEntries.applications.values
|
||||||
|
property string searchText: ""
|
||||||
|
property int selectedIndex: 0
|
||||||
|
property var filteredEntries: {
|
||||||
|
console.log("[AppLauncher] Total desktop entries:", desktopEntries ? desktopEntries.length : 0)
|
||||||
|
if (!desktopEntries || desktopEntries.length === 0) {
|
||||||
|
console.log("[AppLauncher] No desktop entries available")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out entries that shouldn't be displayed
|
||||||
|
var visibleEntries = desktopEntries.filter(entry => {
|
||||||
|
if (!entry || entry.noDisplay) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("[AppLauncher] Visible entries:", visibleEntries.length)
|
||||||
|
|
||||||
|
var query = searchText ? searchText.toLowerCase() : "";
|
||||||
|
var results = [];
|
||||||
|
|
||||||
|
// Handle special commands
|
||||||
|
if (query === ">") {
|
||||||
|
results.push({
|
||||||
|
isCommand: true,
|
||||||
|
name: ">calc",
|
||||||
|
content: "Calculator - evaluate mathematical expressions",
|
||||||
|
icon: "calculate",
|
||||||
|
execute: function() {
|
||||||
|
searchText = ">calc ";
|
||||||
|
searchInput.cursorPosition = searchText.length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
isCommand: true,
|
||||||
|
name: ">clip",
|
||||||
|
content: "Clipboard history - browse and restore clipboard items",
|
||||||
|
icon: "content_paste",
|
||||||
|
execute: function() {
|
||||||
|
searchText = ">clip ";
|
||||||
|
searchInput.cursorPosition = searchText.length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle clipboard history
|
||||||
|
if (query.startsWith(">clip")) {
|
||||||
|
if (!Clipboard.initialized) {
|
||||||
|
Clipboard.refresh();
|
||||||
|
}
|
||||||
|
const searchTerm = query.slice(5).trim();
|
||||||
|
|
||||||
|
Clipboard.history.forEach(function(clip, index) {
|
||||||
|
let searchContent = clip.type === 'image' ?
|
||||||
|
clip.mimeType :
|
||||||
|
clip.content || clip;
|
||||||
|
|
||||||
|
if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) {
|
||||||
|
let entry;
|
||||||
|
if (clip.type === 'image') {
|
||||||
|
entry = {
|
||||||
|
isClipboard: true,
|
||||||
|
name: "Image from " + new Date(clip.timestamp).toLocaleTimeString(),
|
||||||
|
content: "Image: " + clip.mimeType,
|
||||||
|
icon: "image",
|
||||||
|
type: 'image',
|
||||||
|
data: clip.data,
|
||||||
|
execute: function() {
|
||||||
|
const base64Data = clip.data.split(',')[1];
|
||||||
|
copyImageBase64(clip.mimeType, base64Data);
|
||||||
|
Quickshell.execDetached(["notify-send", "Clipboard", "Image copied: " + clip.mimeType]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const textContent = clip.content || clip;
|
||||||
|
let displayContent = textContent;
|
||||||
|
let previewContent = "";
|
||||||
|
|
||||||
|
displayContent = displayContent.replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
|
if (displayContent.length > 50) {
|
||||||
|
previewContent = displayContent;
|
||||||
|
displayContent = displayContent.split('\n')[0].substring(0, 50) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
isClipboard: true,
|
||||||
|
name: displayContent,
|
||||||
|
content: previewContent || textContent,
|
||||||
|
icon: "content_paste",
|
||||||
|
execute: function() {
|
||||||
|
Quickshell.clipboardText = String(textContent);
|
||||||
|
copyText(String(textContent));
|
||||||
|
var preview = (textContent.length > 50) ? textContent.slice(0,50) + "…" : textContent;
|
||||||
|
Quickshell.execDetached(["notify-send", "Clipboard", "Text copied: " + preview]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
results.push(entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
results.push({
|
||||||
|
isClipboard: true,
|
||||||
|
name: "No clipboard history",
|
||||||
|
content: "No matching clipboard entries found",
|
||||||
|
icon: "content_paste_off"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle calculator
|
||||||
|
if (query.startsWith(">calc")) {
|
||||||
|
var expr = searchText.slice(5).trim();
|
||||||
|
if (expr && isMathExpression(expr)) {
|
||||||
|
var value = safeEval(expr);
|
||||||
|
if (value !== null && value !== undefined && value !== "") {
|
||||||
|
var formattedResult = MathHelper.MathHelper.formatResult(value);
|
||||||
|
results.push({
|
||||||
|
isCalculator: true,
|
||||||
|
name: `Calculator: ${expr} = ${formattedResult}`,
|
||||||
|
result: value,
|
||||||
|
expr: expr,
|
||||||
|
icon: "calculate",
|
||||||
|
execute: function() {
|
||||||
|
Quickshell.clipboardText = String(formattedResult);
|
||||||
|
clipboardTextCopyProcess.copyText(String(formattedResult));
|
||||||
|
Quickshell.execDetached(["notify-send", "Calculator Result", `${expr} = ${formattedResult} (copied to clipboard)`]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular app search
|
||||||
|
if (!query) {
|
||||||
|
results = results.concat(visibleEntries.sort(function (a, b) {
|
||||||
|
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
var fuzzyResults = Fuzzysort.go(query, visibleEntries, {
|
||||||
|
keys: ["name", "comment", "genericName"]
|
||||||
|
});
|
||||||
|
results = results.concat(fuzzyResults.map(function (r) {
|
||||||
|
return r.obj;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[AppLauncher] Filtered entries:", results.length)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
console.log("[AppLauncher] Component completed")
|
||||||
|
console.log("[AppLauncher] DesktopEntries available:", typeof DesktopEntries !== 'undefined')
|
||||||
|
if (typeof DesktopEntries !== 'undefined') {
|
||||||
|
console.log("[AppLauncher] DesktopEntries.entries:", DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined')
|
||||||
|
}
|
||||||
|
// Start clipboard refresh immediately on open
|
||||||
|
updateClipboardHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMathExpression(str) {
|
||||||
|
// Allow more characters for enhanced math functions
|
||||||
|
return /^[-+*/().0-9\s\w]+$/.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeEval(expr) {
|
||||||
|
return MathHelper.MathHelper.evaluate(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main content container
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: Math.min(700 * scaling, parent.width * 0.75)
|
||||||
|
height: Math.min(550 * scaling, parent.height * 0.8)
|
||||||
|
radius: 32 * scaling
|
||||||
|
color: Colors.backgroundPrimary
|
||||||
|
border.color: Colors.outline
|
||||||
|
border.width: Style.borderThin * scaling
|
||||||
|
|
||||||
|
// Subtle gradient background
|
||||||
|
gradient: Gradient {
|
||||||
|
GradientStop { position: 0.0; color: Qt.lighter(Colors.backgroundPrimary, 1.02) }
|
||||||
|
GradientStop { position: 1.0; color: Qt.darker(Colors.backgroundPrimary, 1.1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginLarge * scaling
|
||||||
|
spacing: Style.marginMedium * scaling
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Search bar
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 40 * scaling
|
||||||
|
Layout.bottomMargin: Style.marginMedium * scaling
|
||||||
|
radius: 20 * scaling
|
||||||
|
color: Colors.backgroundSecondary
|
||||||
|
border.color: searchInput.activeFocus ? Colors.accentPrimary : Colors.outline
|
||||||
|
border.width: searchInput.activeFocus ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12 * scaling
|
||||||
|
spacing: 10 * scaling
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "search"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pointSize: 16 * scaling
|
||||||
|
color: searchInput.activeFocus ? Colors.accentPrimary : Colors.textSecondary
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: searchInput
|
||||||
|
placeholderText: "Search applications..."
|
||||||
|
color: Colors.textPrimary
|
||||||
|
placeholderTextColor: Colors.textSecondary
|
||||||
|
background: null
|
||||||
|
font.pointSize: 13 * scaling
|
||||||
|
Layout.fillWidth: true
|
||||||
|
onTextChanged: {
|
||||||
|
searchText = text;
|
||||||
|
selectedIndex = 0; // Reset selection when search changes
|
||||||
|
}
|
||||||
|
selectedTextColor: Colors.textPrimary
|
||||||
|
selectionColor: Colors.accentPrimary
|
||||||
|
padding: 0
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
leftPadding: 0
|
||||||
|
rightPadding: 0
|
||||||
|
topPadding: 0
|
||||||
|
bottomPadding: 0
|
||||||
|
font.bold: true
|
||||||
|
Component.onCompleted: {
|
||||||
|
contentItem.cursorColor = Colors.textPrimary
|
||||||
|
contentItem.verticalAlignment = TextInput.AlignVCenter
|
||||||
|
// Focus the search bar by default
|
||||||
|
Qt.callLater(() => {
|
||||||
|
searchInput.forceActiveFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onActiveFocusChanged: contentItem.cursorColor = Colors.textPrimary
|
||||||
|
|
||||||
|
Keys.onDownPressed: selectNext()
|
||||||
|
Keys.onUpPressed: selectPrev()
|
||||||
|
Keys.onEnterPressed: activateSelected()
|
||||||
|
Keys.onReturnPressed: activateSelected()
|
||||||
|
Keys.onEscapePressed: appLauncherPanel.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: 120 }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.width {
|
||||||
|
NumberAnimation { duration: 120 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applications list
|
||||||
|
ScrollView {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
clip: true
|
||||||
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: appsList
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 4 * scaling
|
||||||
|
model: filteredEntries
|
||||||
|
currentIndex: selectedIndex
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: appsList.width - Style.marginSmall * scaling
|
||||||
|
height: 56 * scaling
|
||||||
|
radius: 16 * scaling
|
||||||
|
property bool isSelected: index === selectedIndex
|
||||||
|
color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Colors.accentPrimary, 1.1) : Colors.backgroundSecondary
|
||||||
|
border.color: (appCardArea.containsMouse || isSelected) ? Colors.accentPrimary : "transparent"
|
||||||
|
border.width: (appCardArea.containsMouse || isSelected) ? 2 : 0
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: 150 }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: 150 }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.width {
|
||||||
|
NumberAnimation { duration: 150 }
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginMedium * scaling
|
||||||
|
spacing: Style.marginMedium * scaling
|
||||||
|
|
||||||
|
// App icon with background
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 40 * scaling
|
||||||
|
Layout.preferredHeight: 40 * scaling
|
||||||
|
radius: 14 * scaling
|
||||||
|
color: appCardArea.containsMouse ? Qt.darker(Colors.accentPrimary, 1.1) : Colors.backgroundTertiary
|
||||||
|
property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand) || (iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error && iconImg.source !== "")
|
||||||
|
visible: !searchText.startsWith(">calc") // Hide icon when in calculator mode
|
||||||
|
|
||||||
|
// Clipboard image display
|
||||||
|
Image {
|
||||||
|
id: clipboardImage
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 6 * scaling
|
||||||
|
visible: modelData.type === 'image'
|
||||||
|
source: modelData.data || ""
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
asynchronous: true
|
||||||
|
cache: true
|
||||||
|
}
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: iconImg
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 6 * scaling
|
||||||
|
asynchronous: true
|
||||||
|
source: modelData.isCalculator ? "calculate" :
|
||||||
|
modelData.isClipboard ? (modelData.type === 'image' ? "" : "content_paste") :
|
||||||
|
modelData.isCommand ? modelData.icon :
|
||||||
|
(modelData.icon ? Quickshell.iconPath(modelData.icon, "application-x-executable") : "")
|
||||||
|
visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand || parent.iconLoaded) && modelData.type !== 'image'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback icon container
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 6 * scaling
|
||||||
|
radius: 10 * scaling
|
||||||
|
color: Colors.accentPrimary
|
||||||
|
opacity: 0.3
|
||||||
|
visible: !parent.iconLoaded
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard || modelData.isCommand)
|
||||||
|
text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?"
|
||||||
|
font.pointSize: 18 * scaling
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Colors.accentPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: 150 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// App info
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 2 * scaling
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: modelData.name || "Unknown"
|
||||||
|
font.pointSize: 14 * scaling
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: (appCardArea.containsMouse || isSelected) ? Colors.backgroundPrimary : Colors.textPrimary
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) :
|
||||||
|
modelData.isClipboard ? modelData.content :
|
||||||
|
modelData.isCommand ? modelData.content :
|
||||||
|
(modelData.genericName || modelData.comment || "")
|
||||||
|
font.pointSize: 11 * scaling
|
||||||
|
color: (appCardArea.containsMouse || isSelected) ? Colors.backgroundPrimary : Colors.textSecondary
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: text !== ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: appCardArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
selectedIndex = index;
|
||||||
|
activateSelected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No results message
|
||||||
|
NText {
|
||||||
|
text: searchText.trim() !== "" ? "No applications found" : "No applications available"
|
||||||
|
font.pointSize: Style.fontSizeLarge * scaling
|
||||||
|
color: Colors.textSecondary
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: filteredEntries.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Results count
|
||||||
|
NText {
|
||||||
|
text: searchText.startsWith(">clip") ? `${filteredEntries.length} clipboard item${filteredEntries.length !== 1 ? 's' : ''}` :
|
||||||
|
searchText.startsWith(">calc") ? `${filteredEntries.length} result${filteredEntries.length !== 1 ? 's' : ''}` :
|
||||||
|
`${filteredEntries.length} application${filteredEntries.length !== 1 ? 's' : ''}`
|
||||||
|
font.pointSize: Style.fontSizeSmall * scaling
|
||||||
|
color: Colors.textSecondary
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: searchText.trim() !== ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -342,24 +342,6 @@ WlSessionLock {
|
||||||
anchors.margins: 12 * Scaling.scale(screen)
|
anchors.margins: 12 * Scaling.scale(screen)
|
||||||
spacing: 12 * Scaling.scale(screen)
|
spacing: 12 * Scaling.scale(screen)
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "●"
|
|
||||||
color: Colors.error
|
|
||||||
font.pixelSize: 16 * Scaling.scale(screen)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "●"
|
|
||||||
color: Colors.warning
|
|
||||||
font.pixelSize: 16 * Scaling.scale(screen)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "●"
|
|
||||||
color: Colors.accentPrimary
|
|
||||||
font.pixelSize: 16 * Scaling.scale(screen)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "SECURE TERMINAL"
|
text: "SECURE TERMINAL"
|
||||||
color: Colors.textPrimary
|
color: Colors.textPrimary
|
||||||
|
|
@ -554,6 +536,7 @@ WlSessionLock {
|
||||||
border.width: 1
|
border.width: 1
|
||||||
enabled: !lock.authenticating
|
enabled: !lock.authenticating
|
||||||
Layout.alignment: Qt.AlignRight
|
Layout.alignment: Qt.AlignRight
|
||||||
|
Layout.bottomMargin: -12 * Scaling.scale(screen)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
|
||||||
139
Services/Clipboard.qml
Normal file
139
Services/Clipboard.qml
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var history: []
|
||||||
|
property bool initialized: false
|
||||||
|
|
||||||
|
// Internal state
|
||||||
|
property bool _enabled: true
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 1000
|
||||||
|
repeat: true
|
||||||
|
running: root._enabled
|
||||||
|
onTriggered: root.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect current clipboard types (text/image)
|
||||||
|
Process {
|
||||||
|
id: typeProcess
|
||||||
|
property bool isLoading: false
|
||||||
|
property var currentTypes: []
|
||||||
|
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
currentTypes = String(stdout.text).trim().split('\n').filter(t => t)
|
||||||
|
|
||||||
|
const imageType = currentTypes.find(t => t.startsWith('image/'))
|
||||||
|
if (imageType) {
|
||||||
|
imageProcess.mimeType = imageType
|
||||||
|
imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`]
|
||||||
|
imageProcess.running = true
|
||||||
|
} else {
|
||||||
|
textProcess.command = ["wl-paste", "-n", "--type", "text/plain"]
|
||||||
|
textProcess.running = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
typeProcess.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read image data
|
||||||
|
Process {
|
||||||
|
id: imageProcess
|
||||||
|
property string mimeType: ""
|
||||||
|
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
const base64 = stdout.text.trim()
|
||||||
|
if (base64) {
|
||||||
|
const entry = {
|
||||||
|
type: 'image',
|
||||||
|
mimeType: mimeType,
|
||||||
|
data: `data:${mimeType};base64,${base64}`,
|
||||||
|
timestamp: new Date().getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = root.history.find(item => item.type === 'image' && item.data === entry.data)
|
||||||
|
if (!exists) {
|
||||||
|
root.history = [entry, ...root.history].slice(0, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!textProcess.isLoading) {
|
||||||
|
root.initialized = true
|
||||||
|
}
|
||||||
|
typeProcess.isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read text data
|
||||||
|
Process {
|
||||||
|
id: textProcess
|
||||||
|
property bool isLoading: false
|
||||||
|
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
const content = String(stdout.text).trim()
|
||||||
|
if (content) {
|
||||||
|
const entry = {
|
||||||
|
type: 'text',
|
||||||
|
content: content,
|
||||||
|
timestamp: new Date().getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = root.history.find(item => {
|
||||||
|
if (item.type === 'text') {
|
||||||
|
return item.content === content
|
||||||
|
}
|
||||||
|
return item === content
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
const newHistory = root.history.map(item => {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
return {
|
||||||
|
type: 'text',
|
||||||
|
content: item,
|
||||||
|
timestamp: new Date().getTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
|
||||||
|
root.history = [entry, ...newHistory].slice(0, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
textProcess.isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
root.initialized = true
|
||||||
|
typeProcess.isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
if (!typeProcess.isLoading && !textProcess.isLoading) {
|
||||||
|
typeProcess.isLoading = true
|
||||||
|
typeProcess.command = ["wl-paste", "-l"]
|
||||||
|
typeProcess.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -37,7 +37,8 @@ Item {
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "appLauncher"
|
target: "appLauncher"
|
||||||
|
|
||||||
function toggle() {// TODO
|
function toggle() {
|
||||||
|
appLauncherPanel.isLoaded = !appLauncherPanel.isLoaded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import qs.Modules.Calendar
|
||||||
import qs.Modules.Demo
|
import qs.Modules.Demo
|
||||||
import qs.Modules.Background
|
import qs.Modules.Background
|
||||||
import qs.Modules.SidePanel
|
import qs.Modules.SidePanel
|
||||||
|
import qs.Modules.AppLauncher
|
||||||
import qs.Modules.Notification
|
import qs.Modules.Notification
|
||||||
import qs.Modules.Settings
|
import qs.Modules.Settings
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
@ -24,6 +25,12 @@ ShellRoot {
|
||||||
Bar {}
|
Bar {}
|
||||||
Dock {}
|
Dock {}
|
||||||
|
|
||||||
|
AppLauncher {
|
||||||
|
id: appLauncherPanel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DemoPanel {
|
DemoPanel {
|
||||||
id: demoPanel
|
id: demoPanel
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue