1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-29 16:02:51 -05:00

dropdown: improve perf + add fuzzy search to printers

This commit is contained in:
bbedward
2025-12-22 14:18:24 -05:00
parent 4982ea53dd
commit c9331b7338
5 changed files with 164 additions and 123 deletions

View File

@@ -2,6 +2,8 @@ package cups
import ( import (
"errors" "errors"
"net"
"net/url"
"strings" "strings"
"time" "time"
@@ -156,9 +158,42 @@ func (m *Manager) PurgeJobs(printerName string) error {
return err return err
} }
func resolveIPFromURI(uri string) string {
parsed, err := url.Parse(uri)
if err != nil {
return ""
}
host := parsed.Hostname()
if host == "" {
return ""
}
if ip := net.ParseIP(host); ip != nil {
return ip.String()
}
addrs, err := net.LookupIP(host)
if err != nil || len(addrs) == 0 {
return ""
}
for _, addr := range addrs {
if v4 := addr.To4(); v4 != nil {
return v4.String()
}
}
return addrs[0].String()
}
func (m *Manager) GetDevices() ([]Device, error) { func (m *Manager) GetDevices() ([]Device, error) {
if m.pkHelper != nil { if m.pkHelper != nil {
return m.pkHelper.DevicesGet(10, 0, nil, nil) devices, err := m.pkHelper.DevicesGet(10, 0, nil, nil)
if err != nil {
return nil, err
}
for i := range devices {
if devices[i].Class == "network" {
devices[i].IP = resolveIPFromURI(devices[i].URI)
}
}
return devices, nil
} }
deviceAttrs, err := m.client.GetDevices() deviceAttrs, err := m.client.GetDevices()
@@ -176,6 +211,9 @@ func (m *Manager) GetDevices() ([]Device, error) {
ID: getStringAttr(attrs, "device-id"), ID: getStringAttr(attrs, "device-id"),
Location: getStringAttr(attrs, "device-location"), Location: getStringAttr(attrs, "device-location"),
} }
if device.Class == "network" {
device.IP = resolveIPFromURI(uri)
}
devices = append(devices, device) devices = append(devices, device)
} }

View File

@@ -42,6 +42,7 @@ type Device struct {
MakeModel string `json:"makeModel"` MakeModel string `json:"makeModel"`
ID string `json:"id"` ID string `json:"id"`
Location string `json:"location"` Location string `json:"location"`
IP string `json:"ip,omitempty"`
} }
type PPD struct { type PPD struct {

View File

@@ -287,6 +287,8 @@ Item {
id: deviceDropdown id: deviceDropdown
dropdownWidth: parent.width - 80 - scanDevicesBtn.width - Theme.spacingS * 2 dropdownWidth: parent.width - 80 - scanDevicesBtn.width - Theme.spacingS * 2
popupWidth: parent.width - 80 - scanDevicesBtn.width - Theme.spacingS * 2 popupWidth: parent.width - 80 - scanDevicesBtn.width - Theme.spacingS * 2
enableFuzzySearch: true
emptyText: I18n.tr("No devices found")
currentValue: { currentValue: {
if (CupsService.loadingDevices) if (CupsService.loadingDevices)
return I18n.tr("Scanning..."); return I18n.tr("Scanning...");
@@ -294,20 +296,12 @@ Item {
return CupsService.getDeviceDisplayName(printerTab.selectedDevice); return CupsService.getDeviceDisplayName(printerTab.selectedDevice);
return I18n.tr("Select device..."); return I18n.tr("Select device...");
} }
options: { options: CupsService.filteredDevices.map(d => CupsService.getDeviceDisplayName(d))
const filtered = CupsService.filteredDevices;
if (filtered.length === 0)
return [I18n.tr("No devices found")];
return filtered.map(d => CupsService.getDeviceDisplayName(d));
}
onValueChanged: value => { onValueChanged: value => {
if (value === I18n.tr("No devices found") || value === I18n.tr("Scanning..."))
return;
const filtered = CupsService.filteredDevices; const filtered = CupsService.filteredDevices;
const device = filtered.find(d => CupsService.getDeviceDisplayName(d) === value); const device = filtered.find(d => CupsService.getDeviceDisplayName(d) === value);
if (device) { if (device)
printerTab.selectDevice(device); printerTab.selectDevice(device);
}
} }
} }
@@ -365,6 +359,8 @@ Item {
id: ppdDropdown id: ppdDropdown
dropdownWidth: parent.width - 80 - refreshPpdsBtn.width - Theme.spacingS * 2 dropdownWidth: parent.width - 80 - refreshPpdsBtn.width - Theme.spacingS * 2
popupWidth: parent.width - 80 - refreshPpdsBtn.width - Theme.spacingS * 2 popupWidth: parent.width - 80 - refreshPpdsBtn.width - Theme.spacingS * 2
enableFuzzySearch: true
emptyText: I18n.tr("No drivers found")
currentValue: { currentValue: {
if (CupsService.loadingPPDs) if (CupsService.loadingPPDs)
return I18n.tr("Loading..."); return I18n.tr("Loading...");
@@ -379,20 +375,15 @@ Item {
return printerTab.suggestedPPDs.length > 0 ? I18n.tr("Recommended available") : I18n.tr("Select driver..."); return printerTab.suggestedPPDs.length > 0 ? I18n.tr("Recommended available") : I18n.tr("Select driver...");
} }
options: { options: {
if (CupsService.ppds.length === 0)
return [I18n.tr("No drivers found")];
const suggested = printerTab.suggestedPPDs.map(p => "★ " + (p.makeModel || p.name)); const suggested = printerTab.suggestedPPDs.map(p => "★ " + (p.makeModel || p.name));
const others = CupsService.ppds.filter(p => !printerTab.suggestedPPDs.some(s => s.name === p.name)).map(p => p.makeModel || p.name); const others = CupsService.ppds.filter(p => !printerTab.suggestedPPDs.some(s => s.name === p.name)).map(p => p.makeModel || p.name);
return suggested.concat(others); return suggested.concat(others);
} }
onValueChanged: value => { onValueChanged: value => {
if (value === I18n.tr("No drivers found") || value === I18n.tr("Loading..."))
return;
const cleanValue = value.replace(/^★ /, ""); const cleanValue = value.replace(/^★ /, "");
const ppd = CupsService.ppds.find(p => (p.makeModel || p.name) === cleanValue); const ppd = CupsService.ppds.find(p => (p.makeModel || p.name) === cleanValue);
if (ppd) { if (ppd)
printerTab.selectedPpd = ppd.name; printerTab.selectedPpd = ppd.name;
}
} }
} }

View File

@@ -109,33 +109,36 @@ Singleton {
function getDeviceDisplayName(device) { function getDeviceDisplayName(device) {
if (!device) if (!device)
return ""; return "";
let name = "";
if (device.info && device.info.length > 0) { if (device.info && device.info.length > 0) {
return decodeUri(device.info); name = decodeUri(device.info);
} else if (device.makeModel && device.makeModel.length > 0) {
name = decodeUri(device.makeModel);
} else {
return decodeUri(device.uri);
} }
if (device.makeModel && device.makeModel.length > 0) { if (device.ip)
return decodeUri(device.makeModel); return name + " (" + device.ip + ")";
} return name;
return decodeUri(device.uri);
} }
function getDeviceSubtitle(device) { function getDeviceSubtitle(device) {
if (!device) if (!device)
return ""; return "";
const parts = []; const parts = [];
if (device.class) { switch (device.class) {
switch (device.class) { case "direct":
case "direct": parts.push(I18n.tr("Local"));
parts.push(I18n.tr("Local")); break;
break; case "network":
case "network": parts.push(I18n.tr("Network"));
parts.push(I18n.tr("Network")); break;
break; case "file":
case "file": parts.push(I18n.tr("File"));
parts.push(I18n.tr("File")); break;
break; default:
default: if (device.class)
parts.push(device.class); parts.push(device.class);
}
} }
if (device.location) if (device.location)
parts.push(decodeUri(device.location)); parts.push(decodeUri(device.location));

View File

@@ -18,17 +18,20 @@ Item {
property var options: [] property var options: []
property var optionIcons: [] property var optionIcons: []
property bool enableFuzzySearch: false property bool enableFuzzySearch: false
property var optionIconMap: ({})
onOptionsChanged: { function rebuildIconMap() {
if (dropdownMenu.visible) { const map = {};
dropdownMenu.fzfFinder = new Fzf.Finder(options, { for (let i = 0; i < options.length; i++) {
"selector": option => option, if (optionIcons.length > i)
"limit": 50, map[options[i]] = optionIcons[i];
"casing": "case-insensitive"
});
dropdownMenu.updateFilteredOptions();
} }
optionIconMap = map;
} }
onOptionsChanged: rebuildIconMap()
onOptionIconsChanged: rebuildIconMap()
property int popupWidthOffset: 0 property int popupWidthOffset: 0
property int maxPopupHeight: 400 property int maxPopupHeight: 400
property bool openUpwards: false property bool openUpwards: false
@@ -37,6 +40,7 @@ Item {
property int dropdownWidth: 200 property int dropdownWidth: 200
property bool compactMode: text === "" && description === "" property bool compactMode: text === "" && description === ""
property bool addHorizontalPadding: false property bool addHorizontalPadding: false
property string emptyText: ""
signal valueChanged(string value) signal valueChanged(string value)
@@ -44,10 +48,8 @@ Item {
implicitHeight: compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM) implicitHeight: compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM)
Component.onDestruction: { Component.onDestruction: {
const popup = dropdownMenu; if (dropdownMenu.visible)
if (popup && popup.visible) { dropdownMenu.close();
popup.close();
}
} }
Column { Column {
@@ -105,36 +107,16 @@ Item {
dropdownMenu.close(); dropdownMenu.close();
return; return;
} }
dropdownMenu.searchQuery = "";
dropdownMenu.updateFilteredOptions();
dropdownMenu.open(); dropdownMenu.open();
const pos = dropdown.mapToItem(Overlay.overlay, 0, 0); const pos = dropdown.mapToItem(Overlay.overlay, 0, 0);
const popupWidth = dropdownMenu.width; const popupW = dropdownMenu.width;
const popupHeight = dropdownMenu.height; const popupH = dropdownMenu.height;
const overlayHeight = Overlay.overlay.height; const overlayH = Overlay.overlay.height;
const goUp = root.openUpwards || pos.y + dropdown.height + popupH + 4 > overlayH;
if (root.openUpwards || pos.y + dropdown.height + popupHeight + 4 > overlayHeight) { dropdownMenu.x = root.alignPopupRight ? pos.x + dropdown.width - popupW : pos.x - (root.popupWidthOffset / 2);
if (root.alignPopupRight) { dropdownMenu.y = goUp ? pos.y - popupH - 4 : pos.y + dropdown.height + 4;
dropdownMenu.x = pos.x + dropdown.width - popupWidth; if (root.enableFuzzySearch)
} else {
dropdownMenu.x = pos.x - (root.popupWidthOffset / 2);
}
dropdownMenu.y = pos.y - popupHeight - 4;
} else {
if (root.alignPopupRight) {
dropdownMenu.x = pos.x + dropdown.width - popupWidth;
} else {
dropdownMenu.x = pos.x - (root.popupWidthOffset / 2);
}
dropdownMenu.y = pos.y + dropdown.height + 4;
}
if (root.enableFuzzySearch && searchField.visible) {
searchField.forceActiveFocus(); searchField.forceActiveFocus();
}
} }
} }
@@ -149,10 +131,7 @@ Item {
spacing: Theme.spacingS spacing: Theme.spacingS
DankIcon { DankIcon {
name: { name: root.optionIconMap[root.currentValue] ?? ""
const currentIndex = root.options.indexOf(root.currentValue);
return currentIndex >= 0 && root.optionIcons.length > currentIndex ? root.optionIcons[currentIndex] : "";
}
size: 18 size: 18
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -193,51 +172,52 @@ Item {
id: dropdownMenu id: dropdownMenu
property string searchQuery: "" property string searchQuery: ""
property var filteredOptions: [] property var filteredOptions: {
if (!root.enableFuzzySearch || searchQuery.length === 0)
return root.options;
if (!fzfFinder)
return root.options;
return fzfFinder.find(searchQuery).map(r => r.item);
}
property int selectedIndex: -1 property int selectedIndex: -1
property var fzfFinder: new Fzf.Finder(root.options, { property var fzfFinder: null
"selector": option => option,
"limit": 50,
"casing": "case-insensitive"
})
function updateFilteredOptions() { function initFinder() {
if (!root.enableFuzzySearch || searchQuery.length === 0) { fzfFinder = new Fzf.Finder(root.options, {
filteredOptions = root.options; "selector": option => option,
selectedIndex = -1; "limit": 50,
return; "casing": "case-insensitive"
} });
const results = fzfFinder.find(searchQuery);
filteredOptions = results.map(result => result.item);
selectedIndex = -1;
} }
function selectNext() { function selectNext() {
if (filteredOptions.length === 0) { if (filteredOptions.length === 0)
return; return;
}
selectedIndex = (selectedIndex + 1) % filteredOptions.length; selectedIndex = (selectedIndex + 1) % filteredOptions.length;
listView.positionViewAtIndex(selectedIndex, ListView.Contain); listView.positionViewAtIndex(selectedIndex, ListView.Contain);
} }
function selectPrevious() { function selectPrevious() {
if (filteredOptions.length === 0) { if (filteredOptions.length === 0)
return; return;
}
selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1; selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1;
listView.positionViewAtIndex(selectedIndex, ListView.Contain); listView.positionViewAtIndex(selectedIndex, ListView.Contain);
} }
function selectCurrent() { function selectCurrent() {
if (selectedIndex < 0 || selectedIndex >= filteredOptions.length) { if (selectedIndex < 0 || selectedIndex >= filteredOptions.length)
return; return;
}
root.currentValue = filteredOptions[selectedIndex]; root.currentValue = filteredOptions[selectedIndex];
root.valueChanged(filteredOptions[selectedIndex]); root.valueChanged(filteredOptions[selectedIndex]);
close(); close();
} }
onOpened: {
fzfFinder = null;
searchQuery = "";
selectedIndex = -1;
}
parent: Overlay.overlay parent: Overlay.overlay
width: root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : (dropdown.width + root.popupWidthOffset)) width: root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : (dropdown.width + root.popupWidthOffset))
height: Math.min(root.maxPopupHeight, (root.enableFuzzySearch ? 54 : 0) + Math.min(filteredOptions.length, 10) * 36 + 16) height: Math.min(root.maxPopupHeight, (root.enableFuzzySearch ? 54 : 0) + Math.min(filteredOptions.length, 10) * 36 + 16)
@@ -283,30 +263,38 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: 1 anchors.margins: 1
placeholderText: I18n.tr("Search...") placeholderText: I18n.tr("Search...")
text: dropdownMenu.searchQuery
topPadding: Theme.spacingS topPadding: Theme.spacingS
bottomPadding: Theme.spacingS bottomPadding: Theme.spacingS
onTextChanged: { onTextChanged: searchDebounce.restart()
dropdownMenu.searchQuery = text;
dropdownMenu.updateFilteredOptions();
}
Keys.onDownPressed: dropdownMenu.selectNext() Keys.onDownPressed: dropdownMenu.selectNext()
Keys.onUpPressed: dropdownMenu.selectPrevious() Keys.onUpPressed: dropdownMenu.selectPrevious()
Keys.onReturnPressed: dropdownMenu.selectCurrent() Keys.onReturnPressed: dropdownMenu.selectCurrent()
Keys.onEnterPressed: dropdownMenu.selectCurrent() Keys.onEnterPressed: dropdownMenu.selectCurrent()
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) { if (!(event.modifiers & Qt.ControlModifier))
return;
switch (event.key) {
case Qt.Key_N:
case Qt.Key_J:
dropdownMenu.selectNext(); dropdownMenu.selectNext();
event.accepted = true; event.accepted = true;
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) { break;
dropdownMenu.selectPrevious(); case Qt.Key_P:
event.accepted = true; case Qt.Key_K:
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
dropdownMenu.selectNext();
event.accepted = true;
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
dropdownMenu.selectPrevious(); dropdownMenu.selectPrevious();
event.accepted = true; event.accepted = true;
break;
}
}
Timer {
id: searchDebounce
interval: 50
onTriggered: {
if (!dropdownMenu.fzfFinder)
dropdownMenu.initFinder();
dropdownMenu.searchQuery = searchField.text;
dropdownMenu.selectedIndex = -1;
} }
} }
} }
@@ -318,12 +306,28 @@ Item {
visible: root.enableFuzzySearch visible: root.enableFuzzySearch
} }
Item {
width: parent.width
height: 32
visible: root.options.length === 0 && root.emptyText !== ""
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: root.emptyText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
}
DankListView { DankListView {
id: listView id: listView
width: parent.width width: parent.width
height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0) height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0) - (root.options.length === 0 && root.emptyText !== "" ? 32 : 0)
clip: true clip: true
visible: root.options.length > 0
model: ScriptModel { model: ScriptModel {
values: dropdownMenu.filteredOptions values: dropdownMenu.filteredOptions
} }
@@ -338,9 +342,13 @@ Item {
flickableDirection: Flickable.VerticalFlick flickableDirection: Flickable.VerticalFlick
delegate: Rectangle { delegate: Rectangle {
id: delegateRoot
required property var modelData
required property int index
property bool isSelected: dropdownMenu.selectedIndex === index property bool isSelected: dropdownMenu.selectedIndex === index
property bool isCurrentValue: root.currentValue === modelData property bool isCurrentValue: root.currentValue === modelData
property int optionIndex: root.options.indexOf(modelData) property string iconName: root.optionIconMap[modelData] ?? ""
width: ListView.view.width width: ListView.view.width
height: 32 height: 32
@@ -354,19 +362,19 @@ Item {
spacing: Theme.spacingS spacing: Theme.spacingS
DankIcon { DankIcon {
name: optionIndex >= 0 && root.optionIcons.length > optionIndex ? root.optionIcons[optionIndex] : "" name: delegateRoot.iconName
size: 18 size: 18
color: isCurrentValue ? Theme.primary : Theme.surfaceText color: delegateRoot.isCurrentValue ? Theme.primary : Theme.surfaceText
visible: name !== "" visible: name !== ""
} }
StyledText { StyledText {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: modelData text: delegateRoot.modelData
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: isCurrentValue ? Theme.primary : Theme.surfaceText color: delegateRoot.isCurrentValue ? Theme.primary : Theme.surfaceText
font.weight: isCurrentValue ? Font.Medium : Font.Normal font.weight: delegateRoot.isCurrentValue ? Font.Medium : Font.Normal
width: root.popupWidth > 0 ? undefined : (parent.parent.width - parent.x - Theme.spacingS) width: root.popupWidth > 0 ? undefined : (delegateRoot.width - parent.x - Theme.spacingS)
elide: root.popupWidth > 0 ? Text.ElideNone : Text.ElideRight elide: root.popupWidth > 0 ? Text.ElideNone : Text.ElideRight
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
} }
@@ -379,8 +387,8 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
root.currentValue = modelData; root.currentValue = delegateRoot.modelData;
root.valueChanged(modelData); root.valueChanged(delegateRoot.modelData);
dropdownMenu.close(); dropdownMenu.close();
} }
} }