mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2025-12-06 06:36:15 +00:00
LauncherTab: add grid view option
Launcher: force clipboard history to list view NGridView: created
This commit is contained in:
@@ -1450,6 +1450,10 @@
|
||||
"description": "Wählen Sie, wo das Starter-Panel erscheint.",
|
||||
"label": "Position"
|
||||
},
|
||||
"grid-view": {
|
||||
"description": "Elemente in einem Raster statt in einer Liste anzeigen.",
|
||||
"label": "Rasteransicht"
|
||||
},
|
||||
"section": {
|
||||
"description": "Verhalten und Erscheinungsbild des Starters anpassen.",
|
||||
"label": "Erscheinungsbild"
|
||||
|
||||
@@ -1450,6 +1450,10 @@
|
||||
"description": "Choose where the launcher panel appears.",
|
||||
"label": "Position"
|
||||
},
|
||||
"grid-view": {
|
||||
"description": "Display items in a grid layout instead of a list.",
|
||||
"label": "Grid view"
|
||||
},
|
||||
"section": {
|
||||
"description": "Customize the launcher's behavior and appearance.",
|
||||
"label": "Appearance"
|
||||
|
||||
@@ -1450,6 +1450,10 @@
|
||||
"description": "Elige dónde aparece el panel del lanzador.",
|
||||
"label": "Posición"
|
||||
},
|
||||
"grid-view": {
|
||||
"description": "Mostrar elementos en una cuadrícula en lugar de una lista.",
|
||||
"label": "Vista de cuadrícula"
|
||||
},
|
||||
"section": {
|
||||
"description": "Personaliza el comportamiento y la apariencia del lanzador.",
|
||||
"label": "Apariencia"
|
||||
|
||||
@@ -1450,6 +1450,10 @@
|
||||
"description": "Choisissez où le panneau du lanceur apparaît.",
|
||||
"label": "Position"
|
||||
},
|
||||
"grid-view": {
|
||||
"description": "Afficher les éléments dans une grille au lieu d'une liste.",
|
||||
"label": "Vue grille"
|
||||
},
|
||||
"section": {
|
||||
"description": "Personnalisez le comportement et l'apparence du lanceur.",
|
||||
"label": "Apparence"
|
||||
|
||||
@@ -1450,6 +1450,10 @@
|
||||
"description": "Kies waar het launcher-paneel verschijnt.",
|
||||
"label": "Positie"
|
||||
},
|
||||
"grid-view": {
|
||||
"description": "Items in een raster weergeven in plaats van een lijst.",
|
||||
"label": "Rasterweergave"
|
||||
},
|
||||
"section": {
|
||||
"description": "Pas het gedrag en uiterlijk van de launcher aan.",
|
||||
"label": "Uiterlijk"
|
||||
|
||||
@@ -1450,6 +1450,10 @@
|
||||
"description": "Escolha onde o painel do lançador aparece.",
|
||||
"label": "Posição"
|
||||
},
|
||||
"grid-view": {
|
||||
"description": "Exibir itens em uma grade em vez de uma lista.",
|
||||
"label": "Visualização em grade"
|
||||
},
|
||||
"section": {
|
||||
"description": "Personalize o comportamento e a aparência do lançador.",
|
||||
"label": "Aparência"
|
||||
|
||||
@@ -1450,6 +1450,10 @@
|
||||
"description": "Выберите, где появляется панель запуска.",
|
||||
"label": "Положение"
|
||||
},
|
||||
"grid-view": {
|
||||
"description": "Показывать элементы в виде сетки вместо списка.",
|
||||
"label": "Вид сетки"
|
||||
},
|
||||
"section": {
|
||||
"description": "Настройка поведения и внешнего вида запуска.",
|
||||
"label": "Внешний вид"
|
||||
|
||||
@@ -1450,6 +1450,10 @@
|
||||
"description": "Başlatıcı panelinin nerede görüneceğini seçin.",
|
||||
"label": "Konum"
|
||||
},
|
||||
"grid-view": {
|
||||
"description": "Öğeleri liste yerine ızgara düzeninde görüntüle.",
|
||||
"label": "Izgara görünümü"
|
||||
},
|
||||
"section": {
|
||||
"description": "Başlatıcının davranışını ve görünümünü özelleştirin.",
|
||||
"label": "Görünüm"
|
||||
|
||||
@@ -1450,6 +1450,10 @@
|
||||
"description": "Виберіть, де з'являється панель запускача.",
|
||||
"label": "Положення"
|
||||
},
|
||||
"grid-view": {
|
||||
"description": "Показувати елементи у вигляді сітки замість списку.",
|
||||
"label": "Вигляд сітки"
|
||||
},
|
||||
"section": {
|
||||
"description": "Налаштуйте поведінку та зовнішній вигляд запускача.",
|
||||
"label": "Зовнішній вигляд"
|
||||
|
||||
@@ -1450,6 +1450,10 @@
|
||||
"description": "选择启动器面板出现的位置。",
|
||||
"label": "位置"
|
||||
},
|
||||
"grid-view": {
|
||||
"description": "以网格布局而非列表显示项目。",
|
||||
"label": "网格视图"
|
||||
},
|
||||
"section": {
|
||||
"description": "自定义启动器的行为和外观。",
|
||||
"label": "外观"
|
||||
|
||||
@@ -148,7 +148,8 @@
|
||||
"sortByMostUsed": true,
|
||||
"terminalCommand": "xterm -e",
|
||||
"customLaunchPrefixEnabled": false,
|
||||
"customLaunchPrefix": ""
|
||||
"customLaunchPrefix": "",
|
||||
"viewMode": "list"
|
||||
},
|
||||
"controlCenter": {
|
||||
"position": "close_to_bar_button",
|
||||
|
||||
@@ -306,6 +306,8 @@ Singleton {
|
||||
property string terminalCommand: "xterm -e"
|
||||
property bool customLaunchPrefixEnabled: false
|
||||
property string customLaunchPrefix: ""
|
||||
// View mode: "list" or "grid"
|
||||
property string viewMode: "list"
|
||||
}
|
||||
|
||||
// control center
|
||||
|
||||
@@ -55,6 +55,21 @@ SmartPanel {
|
||||
|
||||
readonly property int badgeSize: Math.round(Style.baseWidgetSize * 1.6)
|
||||
readonly property int entryHeight: Math.round(badgeSize + Style.marginM * 2)
|
||||
readonly property bool isGridView: {
|
||||
// Always use list view for clipboard to better display text content and previews
|
||||
if (searchText.startsWith(">clip")) {
|
||||
return false;
|
||||
}
|
||||
return Settings.data.appLauncher.viewMode === "grid";
|
||||
}
|
||||
|
||||
// Target columns, but actual columns may vary based on available width
|
||||
readonly property int targetGridColumns: 5
|
||||
readonly property int gridCellSize: Math.floor((listPanelWidth - Style.marginS - (targetGridColumns * Style.marginXXS)) / targetGridColumns)
|
||||
|
||||
// Actual columns that fit in the GridView
|
||||
// This gets updated dynamically by the GridView when its actual width is known
|
||||
property int gridColumns: 5
|
||||
|
||||
// Override keyboard handlers from SmartPanel for navigation.
|
||||
// Launcher specific: onTabPressed() and onBackTabPressed() are special here.
|
||||
@@ -69,11 +84,43 @@ SmartPanel {
|
||||
}
|
||||
|
||||
function onUpPressed() {
|
||||
selectPreviousWrapped();
|
||||
if (isGridView) {
|
||||
// Force update to prevent GridView interference
|
||||
Qt.callLater(() => {
|
||||
selectPreviousRow();
|
||||
});
|
||||
} else {
|
||||
selectPreviousWrapped();
|
||||
}
|
||||
}
|
||||
|
||||
function onDownPressed() {
|
||||
selectNextWrapped();
|
||||
if (isGridView) {
|
||||
// Force update to prevent GridView interference
|
||||
Qt.callLater(() => {
|
||||
selectNextRow();
|
||||
});
|
||||
} else {
|
||||
selectNextWrapped();
|
||||
}
|
||||
}
|
||||
|
||||
function onLeftPressed() {
|
||||
if (isGridView) {
|
||||
selectPreviousColumn();
|
||||
} else {
|
||||
// In list view, left = previous item
|
||||
selectPreviousWrapped();
|
||||
}
|
||||
}
|
||||
|
||||
function onRightPressed() {
|
||||
if (isGridView) {
|
||||
selectNextColumn();
|
||||
} else {
|
||||
// In list view, right = next item
|
||||
selectNextWrapped();
|
||||
}
|
||||
}
|
||||
|
||||
function onReturnPressed() {
|
||||
@@ -267,6 +314,112 @@ SmartPanel {
|
||||
}
|
||||
}
|
||||
|
||||
// Grid view navigation functions
|
||||
function selectPreviousRow() {
|
||||
if (results.length > 0 && isGridView) {
|
||||
const currentRow = Math.floor(selectedIndex / gridColumns);
|
||||
const currentCol = selectedIndex % gridColumns;
|
||||
|
||||
if (currentRow > 0) {
|
||||
// Move to previous row, same column
|
||||
const targetRow = currentRow - 1;
|
||||
const targetIndex = targetRow * gridColumns + currentCol;
|
||||
// Check if target column exists in target row
|
||||
const itemsInTargetRow = Math.min(gridColumns, results.length - targetRow * gridColumns);
|
||||
if (currentCol < itemsInTargetRow) {
|
||||
selectedIndex = targetIndex;
|
||||
} else {
|
||||
// Target column doesn't exist, go to last item in target row
|
||||
selectedIndex = targetRow * gridColumns + itemsInTargetRow - 1;
|
||||
}
|
||||
} else {
|
||||
// Wrap to last row, same column
|
||||
const totalRows = Math.ceil(results.length / gridColumns);
|
||||
const lastRow = totalRows - 1;
|
||||
const itemsInLastRow = Math.min(gridColumns, results.length - lastRow * gridColumns);
|
||||
if (currentCol < itemsInLastRow) {
|
||||
selectedIndex = lastRow * gridColumns + currentCol;
|
||||
} else {
|
||||
selectedIndex = results.length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectNextRow() {
|
||||
if (results.length > 0 && isGridView) {
|
||||
const currentRow = Math.floor(selectedIndex / gridColumns);
|
||||
const currentCol = selectedIndex % gridColumns;
|
||||
const totalRows = Math.ceil(results.length / gridColumns);
|
||||
|
||||
if (currentRow < totalRows - 1) {
|
||||
// Move to next row, same column
|
||||
const targetRow = currentRow + 1;
|
||||
const targetIndex = targetRow * gridColumns + currentCol;
|
||||
|
||||
// Check if target index is valid
|
||||
if (targetIndex < results.length) {
|
||||
selectedIndex = targetIndex;
|
||||
} else {
|
||||
// Target column doesn't exist in target row, go to last item in target row
|
||||
const itemsInTargetRow = results.length - targetRow * gridColumns;
|
||||
if (itemsInTargetRow > 0) {
|
||||
selectedIndex = targetRow * gridColumns + itemsInTargetRow - 1;
|
||||
} else {
|
||||
// Target row is empty, wrap to first row
|
||||
selectedIndex = Math.min(currentCol, results.length - 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Wrap to first row, same column
|
||||
selectedIndex = Math.min(currentCol, results.length - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectPreviousColumn() {
|
||||
if (results.length > 0 && isGridView) {
|
||||
const currentRow = Math.floor(selectedIndex / gridColumns);
|
||||
const currentCol = selectedIndex % gridColumns;
|
||||
if (currentCol > 0) {
|
||||
// Move left in same row
|
||||
selectedIndex = currentRow * gridColumns + (currentCol - 1);
|
||||
} else {
|
||||
// Wrap to last column of previous row
|
||||
if (currentRow > 0) {
|
||||
selectedIndex = (currentRow - 1) * gridColumns + (gridColumns - 1);
|
||||
} else {
|
||||
// Wrap to last column of last row
|
||||
const totalRows = Math.ceil(results.length / gridColumns);
|
||||
const lastRowIndex = (totalRows - 1) * gridColumns + (gridColumns - 1);
|
||||
selectedIndex = Math.min(lastRowIndex, results.length - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectNextColumn() {
|
||||
if (results.length > 0 && isGridView) {
|
||||
const currentRow = Math.floor(selectedIndex / gridColumns);
|
||||
const currentCol = selectedIndex % gridColumns;
|
||||
const itemsInCurrentRow = Math.min(gridColumns, results.length - currentRow * gridColumns);
|
||||
|
||||
if (currentCol < itemsInCurrentRow - 1) {
|
||||
// Move right in same row
|
||||
selectedIndex = currentRow * gridColumns + (currentCol + 1);
|
||||
} else {
|
||||
// Wrap to first column of next row
|
||||
const totalRows = Math.ceil(results.length / gridColumns);
|
||||
if (currentRow < totalRows - 1) {
|
||||
selectedIndex = (currentRow + 1) * gridColumns;
|
||||
} else {
|
||||
// Wrap to first item
|
||||
selectedIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function activate() {
|
||||
if (results.length > 0 && results[selectedIndex]) {
|
||||
const item = results[selectedIndex];
|
||||
@@ -288,9 +441,16 @@ SmartPanel {
|
||||
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
|
||||
))
|
||||
y: {
|
||||
if (!resultsViewLoader.item)
|
||||
return Style.marginL;
|
||||
const view = resultsViewLoader.item;
|
||||
const row = root.isGridView ? Math.floor(root.selectedIndex / root.gridColumns) : root.selectedIndex;
|
||||
const itemHeight = root.isGridView ? (root.gridCellSize + Style.marginXXS) : (root.entryHeight + view.spacing);
|
||||
const yPos = row * itemHeight - view.contentY;
|
||||
const mapped = view.mapToItem(ui, 0, yPos);
|
||||
return Math.max(Style.marginL, Math.min(mapped.y, ui.height - previewBox.height - Style.marginL));
|
||||
}
|
||||
z: -1 // Draw behind main panel content if it ever overlaps
|
||||
|
||||
opacity: visible ? 1.0 : 0.0
|
||||
@@ -404,7 +564,7 @@ SmartPanel {
|
||||
Component.onCompleted: {
|
||||
if (searchInput.inputItem) {
|
||||
searchInput.inputItem.forceActiveFocus();
|
||||
// Intercept Tab keys before TextField handles them
|
||||
// Intercept keys before TextField handles them
|
||||
searchInput.inputItem.Keys.onPressed.connect(function (event) {
|
||||
if (event.key === Qt.Key_Tab) {
|
||||
root.onTabPressed();
|
||||
@@ -412,113 +572,365 @@ SmartPanel {
|
||||
} else if (event.key === Qt.Key_Backtab) {
|
||||
root.onBackTabPressed();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Left && root.isGridView) {
|
||||
// In grid view, left arrow navigates the grid
|
||||
root.onLeftPressed();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Right && root.isGridView) {
|
||||
// In grid view, right arrow navigates the grid
|
||||
root.onRightPressed();
|
||||
event.accepted = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NListView {
|
||||
id: resultsList
|
||||
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
|
||||
Loader {
|
||||
id: resultsViewLoader
|
||||
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);
|
||||
sourceComponent: root.isGridView ? gridViewComponent : listViewComponent
|
||||
}
|
||||
|
||||
Component {
|
||||
id: listViewComponent
|
||||
NListView {
|
||||
id: resultsList
|
||||
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (clipboardPreviewLoader.item) {
|
||||
clipboardPreviewLoader.item.currentItem = results[currentIndex] || null;
|
||||
onModelChanged: {}
|
||||
|
||||
delegate: Rectangle {
|
||||
id: entry
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// Top row - Main entry content with pin button
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
// Icon badge or Image preview or Emoji
|
||||
Rectangle {
|
||||
Layout.preferredWidth: badgeSize
|
||||
Layout.preferredHeight: badgeSize
|
||||
radius: Style.radiusM
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
// Image preview for clipboard images
|
||||
NImageRounded {
|
||||
id: imagePreview
|
||||
anchors.fill: parent
|
||||
visible: modelData.isImage && !modelData.emojiChar
|
||||
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 && !modelData.emojiChar || (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 !== "" && !modelData.emojiChar
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emoji display - takes precedence when emojiChar is present
|
||||
NText {
|
||||
id: emojiDisplay
|
||||
anchors.centerIn: parent
|
||||
visible: modelData.emojiChar ? true : (!imagePreview.visible && !iconLoader.visible)
|
||||
text: modelData.emojiChar ? modelData.emojiChar : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?")
|
||||
pointSize: modelData.emojiChar ? Style.fontSizeXXXL : Style.fontSizeXXL // Larger font for emojis
|
||||
font.weight: Style.fontWeightBold
|
||||
color: modelData.emojiChar ? Color.mOnSurface : Color.mOnPrimary // Different color for emojis
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 !== ""
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
selectedIndex = index;
|
||||
root.activate();
|
||||
mouse.accepted = true;
|
||||
}
|
||||
}
|
||||
acceptedButtons: Qt.LeftButton
|
||||
}
|
||||
}
|
||||
}
|
||||
onModelChanged: {}
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
id: entry
|
||||
Component {
|
||||
id: gridViewComponent
|
||||
NGridView {
|
||||
id: resultsGrid
|
||||
|
||||
property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === selectedIndex)
|
||||
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;
|
||||
}
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
cellWidth: gridCellSize + Style.marginXXS
|
||||
cellHeight: gridCellSize + Style.marginXXS
|
||||
model: results
|
||||
cacheBuffer: resultsGrid.height * 2
|
||||
keyNavigationEnabled: false
|
||||
focus: false
|
||||
interactive: true
|
||||
|
||||
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);
|
||||
onWidthChanged: {
|
||||
// Update gridColumns based on actual GridView width
|
||||
// This ensures navigation works correctly regardless of panel size
|
||||
const actualCols = Math.floor(width / cellWidth);
|
||||
if (actualCols > 0 && actualCols !== root.gridColumns) {
|
||||
root.gridColumns = actualCols;
|
||||
}
|
||||
}
|
||||
|
||||
width: resultsList.width - Style.marginS
|
||||
implicitHeight: entryHeight
|
||||
radius: Style.radiusM
|
||||
color: entry.isSelected ? Color.mHover : Color.mSurface
|
||||
// Completely disable GridView key handling
|
||||
Keys.enabled: false
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCirc
|
||||
// Don't sync selectedIndex to GridView's currentIndex
|
||||
// The visual selection is handled by the delegate based on selectedIndex
|
||||
// We only need to position the view to show the selected item
|
||||
|
||||
onModelChanged: {}
|
||||
|
||||
// Handle scrolling to show selected item when it changes
|
||||
Connections {
|
||||
target: root
|
||||
function onSelectedIndexChanged() {
|
||||
if (root.selectedIndex >= 0) {
|
||||
Qt.callLater(() => {
|
||||
resultsGrid.cancelFlick();
|
||||
resultsGrid.positionViewAtIndex(root.selectedIndex, GridView.Contain);
|
||||
});
|
||||
}
|
||||
|
||||
// Update preview
|
||||
if (clipboardPreviewLoader.item && root.selectedIndex >= 0) {
|
||||
clipboardPreviewLoader.item.currentItem = results[root.selectedIndex] || null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
delegate: Rectangle {
|
||||
id: gridEntry
|
||||
|
||||
// Top row - Main entry content with pin button
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === selectedIndex)
|
||||
property string appId: (modelData && modelData.appId) ? String(modelData.appId) : ""
|
||||
|
||||
width: gridCellSize
|
||||
height: gridCellSize
|
||||
radius: Style.radiusM
|
||||
color: gridEntry.isSelected ? Color.mHover : Color.mSurface
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCirc
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
// Icon badge or Image preview or Emoji
|
||||
Rectangle {
|
||||
Layout.preferredWidth: badgeSize
|
||||
Layout.preferredHeight: badgeSize
|
||||
Layout.preferredWidth: badgeSize * 1.5
|
||||
Layout.preferredHeight: badgeSize * 1.5
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
radius: Style.radiusM
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
// Image preview for clipboard images
|
||||
NImageRounded {
|
||||
id: imagePreview
|
||||
id: gridImagePreview
|
||||
anchors.fill: parent
|
||||
visible: modelData.isImage && !modelData.emojiChar
|
||||
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) || "";
|
||||
@@ -539,18 +951,18 @@ SmartPanel {
|
||||
|
||||
onStatusChanged: status => {
|
||||
if (status === Image.Error) {
|
||||
iconLoader.visible = true;
|
||||
imagePreview.visible = false;
|
||||
gridIconLoader.visible = true;
|
||||
gridImagePreview.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: iconLoader
|
||||
id: gridIconLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginXS
|
||||
|
||||
visible: !modelData.isImage && !modelData.emojiChar || (modelData.isImage && imagePreview.status === Image.Error)
|
||||
visible: !modelData.isImage && !modelData.emojiChar || (modelData.isImage && gridImagePreview.status === Image.Error)
|
||||
active: visible
|
||||
|
||||
sourceComponent: Component {
|
||||
@@ -563,98 +975,51 @@ SmartPanel {
|
||||
}
|
||||
}
|
||||
|
||||
// Emoji display - takes precedence when emojiChar is present
|
||||
// Emoji display
|
||||
NText {
|
||||
id: emojiDisplay
|
||||
id: gridEmojiDisplay
|
||||
anchors.centerIn: parent
|
||||
visible: modelData.emojiChar ? true : (!imagePreview.visible && !iconLoader.visible)
|
||||
visible: modelData.emojiChar ? true : (!gridImagePreview.visible && !gridIconLoader.visible)
|
||||
text: modelData.emojiChar ? modelData.emojiChar : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?")
|
||||
pointSize: modelData.emojiChar ? Style.fontSizeXXXL : Style.fontSizeXXL // Larger font for emojis
|
||||
pointSize: modelData.emojiChar ? Style.fontSizeXXL : Style.fontSizeXL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: modelData.emojiChar ? Color.mOnSurface : Color.mOnPrimary // Different color for emojis
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
color: modelData.emojiChar ? Color.mOnSurface : Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
|
||||
// Text content
|
||||
ColumnLayout {
|
||||
NText {
|
||||
text: modelData.name || "Unknown"
|
||||
pointSize: Style.fontSizeS
|
||||
font.weight: Style.fontWeightSemiBold
|
||||
color: gridEntry.isSelected ? Color.mOnHover : Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
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 !== ""
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
Layout.maximumWidth: gridCellSize - Style.marginM * 2
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,13 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("settings.launcher.settings.grid-view.label")
|
||||
description: I18n.tr("settings.launcher.settings.grid-view.description")
|
||||
checked: Settings.data.appLauncher.viewMode === "grid"
|
||||
onToggled: checked => Settings.data.appLauncher.viewMode = checked ? "grid" : "list"
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("settings.launcher.settings.clipboard-history.label")
|
||||
description: I18n.tr("settings.launcher.settings.clipboard-history.description")
|
||||
|
||||
186
Widgets/NGridView.qml
Normal file
186
Widgets/NGridView.qml
Normal file
@@ -0,0 +1,186 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Templates as T
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Intercept all key events at the root level to prevent GridView from handling them
|
||||
Keys.onPressed: event => {
|
||||
// Don't let this event reach the GridView
|
||||
event.accepted = false;
|
||||
}
|
||||
|
||||
Keys.onReleased: event => {
|
||||
event.accepted = false;
|
||||
}
|
||||
|
||||
property color handleColor: Qt.alpha(Color.mHover, 0.8)
|
||||
property color handleHoverColor: handleColor
|
||||
property color handlePressedColor: handleColor
|
||||
property color trackColor: Color.transparent
|
||||
property real handleWidth: 6
|
||||
property real handleRadius: Style.radiusM
|
||||
property int verticalPolicy: ScrollBar.AsNeeded
|
||||
property int horizontalPolicy: ScrollBar.AlwaysOff
|
||||
readonly property bool verticalScrollBarActive: {
|
||||
if (gridView.ScrollBar.vertical.policy === ScrollBar.AlwaysOff)
|
||||
return false;
|
||||
return gridView.contentHeight > gridView.height;
|
||||
}
|
||||
readonly property real scrollBarWidth: verticalScrollBarActive ? handleWidth : 0
|
||||
|
||||
// Forward GridView properties
|
||||
property alias model: gridView.model
|
||||
property alias delegate: gridView.delegate
|
||||
property alias cellWidth: gridView.cellWidth
|
||||
property alias cellHeight: gridView.cellHeight
|
||||
property alias currentIndex: gridView.currentIndex
|
||||
property alias count: gridView.count
|
||||
property alias contentHeight: gridView.contentHeight
|
||||
property alias contentWidth: gridView.contentWidth
|
||||
property alias contentY: gridView.contentY
|
||||
property alias contentX: gridView.contentX
|
||||
property alias currentItem: gridView.currentItem
|
||||
property alias highlightItem: gridView.highlightItem
|
||||
property alias highlightFollowsCurrentItem: gridView.highlightFollowsCurrentItem
|
||||
property alias preferredHighlightBegin: gridView.preferredHighlightBegin
|
||||
property alias preferredHighlightEnd: gridView.preferredHighlightEnd
|
||||
property alias highlightRangeMode: gridView.highlightRangeMode
|
||||
property alias snapMode: gridView.snapMode
|
||||
property alias keyNavigationEnabled: gridView.keyNavigationEnabled
|
||||
property alias keyNavigationWraps: gridView.keyNavigationWraps
|
||||
property alias cacheBuffer: gridView.cacheBuffer
|
||||
property alias displayMarginBeginning: gridView.displayMarginBeginning
|
||||
property alias displayMarginEnd: gridView.displayMarginEnd
|
||||
property alias layoutDirection: gridView.layoutDirection
|
||||
property alias effectiveLayoutDirection: gridView.effectiveLayoutDirection
|
||||
property alias flow: gridView.flow
|
||||
property alias boundsBehavior: gridView.boundsBehavior
|
||||
property alias flickableDirection: gridView.flickableDirection
|
||||
property alias interactive: gridView.interactive
|
||||
property alias moving: gridView.moving
|
||||
property alias flicking: gridView.flicking
|
||||
property alias dragging: gridView.dragging
|
||||
property alias horizontalVelocity: gridView.horizontalVelocity
|
||||
property alias verticalVelocity: gridView.verticalVelocity
|
||||
|
||||
// Forward GridView methods
|
||||
function positionViewAtIndex(index, mode) {
|
||||
gridView.positionViewAtIndex(index, mode);
|
||||
}
|
||||
|
||||
function positionViewAtBeginning() {
|
||||
gridView.positionViewAtBeginning();
|
||||
}
|
||||
|
||||
function positionViewAtEnd() {
|
||||
gridView.positionViewAtEnd();
|
||||
}
|
||||
|
||||
function forceLayout() {
|
||||
gridView.forceLayout();
|
||||
}
|
||||
|
||||
function cancelFlick() {
|
||||
gridView.cancelFlick();
|
||||
}
|
||||
|
||||
function flick(xVelocity, yVelocity) {
|
||||
gridView.flick(xVelocity, yVelocity);
|
||||
}
|
||||
|
||||
function incrementCurrentIndex() {
|
||||
gridView.incrementCurrentIndex();
|
||||
}
|
||||
|
||||
function decrementCurrentIndex() {
|
||||
gridView.decrementCurrentIndex();
|
||||
}
|
||||
|
||||
function indexAt(x, y) {
|
||||
return gridView.indexAt(x, y);
|
||||
}
|
||||
|
||||
function itemAt(x, y) {
|
||||
return gridView.itemAt(x, y);
|
||||
}
|
||||
|
||||
function itemAtIndex(index) {
|
||||
return gridView.itemAtIndex(index);
|
||||
}
|
||||
|
||||
// Set reasonable implicit sizes for Layout usage
|
||||
implicitWidth: 200
|
||||
implicitHeight: 200
|
||||
|
||||
GridView {
|
||||
id: gridView
|
||||
anchors.fill: parent
|
||||
|
||||
// Enable clipping to keep content within bounds
|
||||
clip: true
|
||||
|
||||
// Enable flickable for smooth scrolling
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
// Completely disable focus to prevent any keyboard interaction
|
||||
focus: false
|
||||
activeFocusOnTab: false
|
||||
enabled: true // Still enabled for mouse interaction
|
||||
|
||||
// Override key navigation - do nothing
|
||||
Keys.onPressed: event => {
|
||||
// Consume the event here so GridView doesn't process it
|
||||
// but don't actually do anything
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Keys.onReleased: event => {
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
parent: gridView
|
||||
x: gridView.mirrored ? 0 : gridView.width - width
|
||||
y: 0
|
||||
height: gridView.height
|
||||
policy: root.verticalPolicy
|
||||
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: root.handleWidth
|
||||
implicitHeight: 100
|
||||
radius: root.handleRadius
|
||||
color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
|
||||
opacity: parent.policy === ScrollBar.AlwaysOn ? 1.0 : root.verticalScrollBarActive ? (parent.active ? 1.0 : 0.0) : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: root.handleWidth
|
||||
implicitHeight: 100
|
||||
color: root.trackColor
|
||||
opacity: parent.policy === ScrollBar.AlwaysOn ? 0.3 : root.verticalScrollBarActive ? (parent.active ? 0.3 : 0.0) : 0.0
|
||||
radius: root.handleRadius / 2
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user