Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/server/src/qml/launcher-window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,16 @@ void LauncherWindow::popToRoot() {
emit viewNavigatedBack();
}

void LauncherWindow::removeCurrentFilter() {
auto *nav = m_ctx.navigation.get();
if (nav->isRootSearch()) return;

QString const query = nav->searchText();
nav->popCurrentView();
nav->setSearchText(query);
emit viewNavigatedBack();
}

bool LauncherWindow::popOnBackspace() { return m_ctx.services->config()->value().popOnBackspace; }

void LauncherWindow::setCompleterValue(int index, const QString &value) {
Expand Down
2 changes: 2 additions & 0 deletions src/server/src/qml/launcher-window.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ class LauncherWindow : public QObject {
Q_INVOKABLE void handleEscape();
Q_INVOKABLE void goBack();
Q_INVOKABLE void popToRoot();
// Pop the current scoped view (a filter) but preserve the query text (Shift+Tab).
Q_INVOKABLE void removeCurrentFilter();
Q_INVOKABLE void setCompleterValue(int index, const QString &value);
Q_INVOKABLE QRect cursorScreenGeometry() const;
Q_INVOKABLE void positionOnCursorScreen();
Expand Down
4 changes: 4 additions & 0 deletions src/server/src/qml/qml/CommandListView.qml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ GenericListView {
itemAlias: ""
itemIsActive: false
itemAccessory: delegateLoader.itemAccessory
quickNumber: {
commandListView.listModel.selectedIndex;
return commandListView.listModel.quickSelectNumber(delegateLoader.index);
}
selected: commandListView.currentIndex === delegateLoader.index
onClicked: commandListView.currentIndex = delegateLoader.index
onActivated: commandListView.itemActivated(delegateLoader.index)
Expand Down
21 changes: 21 additions & 0 deletions src/server/src/qml/qml/ExtensionView.qml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ Item {
}
return false;
}
function moveBy(delta) {
if (contentLoader.item && typeof contentLoader.item.moveBy === "function") {
return contentLoader.item.moveBy(delta);
}
return false;
}
function activateRelative(offset) {
if (contentLoader.item && typeof contentLoader.item.activateRelative === "function") {
return contentLoader.item.activateRelative(offset);
}
}

Loader {
id: contentLoader
Expand Down Expand Up @@ -83,6 +94,12 @@ Item {
function moveSectionDown() {
return listView.moveSectionDown();
}
function moveBy(delta) {
return listView.moveBy(delta);
}
function activateRelative(offset) {
return listView.activateRelative(offset);
}

GenericListView {
id: listView
Expand Down Expand Up @@ -133,6 +150,10 @@ Item {
itemAlias: ""
itemIsActive: false
itemAccessory: delegateLoader.itemAccessory
quickNumber: {
listView.listModel.selectedIndex;
return listView.listModel.quickSelectNumber(delegateLoader.index);
}
selected: listView.currentIndex === delegateLoader.index
onClicked: listView.currentIndex = delegateLoader.index
onActivated: listView.itemActivated(delegateLoader.index)
Expand Down
40 changes: 40 additions & 0 deletions src/server/src/qml/qml/GenericListView.qml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,46 @@ Item {
listView.currentIndex = root.listModel.nextSelectableIndex(-1, 1);
}

// Move the selection by `delta` selectable rows (negative = up), clamping at
// the list ends instead of wrapping. Skips section headers.
function moveBy(delta) {
if (delta === 0)
return true;
const step = delta > 0 ? 1 : -1;
let idx = listView.currentIndex;
for (let i = 0; i < Math.abs(delta); i++) {
const next = root.listModel.nextSelectableIndex(idx, step);
// nextSelectableIndex wraps around; treat a wrap as reaching the end.
if (next === idx || (step > 0 && next < idx) || (step < 0 && next > idx))
break;
idx = next;
}
if (idx !== listView.currentIndex) {
listView.currentIndex = idx;
listView.positionViewAtIndex(sectionScrollTarget(idx, -1), ListView.Contain);
}
return true;
}

// Activate the item `offset` selectable rows below the current selection
// (offset 0 = current). Clamps to the last item; never wraps.
function activateRelative(offset) {
let idx = listView.currentIndex;
if (idx < 0)
idx = root.listModel.nextSelectableIndex(-1, 1);
if (idx < 0)
return;
for (let i = 0; i < offset; i++) {
const next = root.listModel.nextSelectableIndex(idx, 1);
if (next <= idx)
break; // reached end (no wrap)
idx = next;
}
listView.currentIndex = idx;
root.listModel.setSelectedIndex(idx);
root.listModel.activateSelected();
}

onListModelChanged: {
if (root.autoWireModel && root.listModel && listView.count > 0) {
root.selectFirst();
Expand Down
14 changes: 13 additions & 1 deletion src/server/src/qml/qml/LauncherWindow.qml
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,19 @@ Window {
Shortcut {
sequence: "Escape"
enabled: !launcher.alertModel.visible && !actionPanel.open && !footerPanel.open && !launcher.hasOverlay
onActivated: launcher.handleEscape()
onActivated: {
// If the selection isn't on the first item, Escape resets it to the top
// instead of closing; a second Escape (already at top) closes as usual.
const view = commandStack.currentItem;
if (view && view.listModel && typeof view.selectFirst === "function" && typeof view.currentIndex !== "undefined") {
const first = view.listModel.nextSelectableIndex(-1, 1);
if (first >= 0 && view.currentIndex !== first) {
view.selectFirst();
return;
}
}
launcher.handleEscape();
}
}

Shortcut {
Expand Down
10 changes: 10 additions & 0 deletions src/server/src/qml/qml/ListItemDelegate.qml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ SelectableDelegate {
property string filePath: ""
property string fileUrl: ""

// 1..5 position from the current selection, for the Alt+N shortcut (0 = hide).
property int quickNumber: 0

readonly property bool _isDraggable: root.filePath !== ""

Component.onCompleted: {
Expand Down Expand Up @@ -147,5 +150,12 @@ SelectableDelegate {
Layout.alignment: Qt.AlignVCenter
clip: true
}

TextBadge {
visible: root.quickNumber > 0
text: root.quickNumber.toString()
contentColor: root.selected ? Theme.listItemSelectionFg : Theme.textMuted
Layout.alignment: Qt.AlignVCenter
}
}
}
4 changes: 4 additions & 0 deletions src/server/src/qml/qml/RootSearchList.qml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ GenericListView {
itemIsActive: delegateLoader.isActive
itemAccessory: delegateLoader.accessoryText
itemAccessoryColor: delegateLoader.accessoryColor
quickNumber: {
searchListView.listModel.selectedIndex;
return searchListView.listModel.quickSelectNumber(delegateLoader.index);
}
selected: searchListView.currentIndex === delegateLoader.index
onClicked: searchListView.currentIndex = delegateLoader.index
onActivated: searchListView.itemActivated(delegateLoader.index)
Expand Down
60 changes: 58 additions & 2 deletions src/server/src/qml/qml/SearchBar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,46 @@ Item {
return false;
}

// Alt+1..5 open the Nth result counting from the current selection;
// Alt+q / Alt+w move the selection up / down by 5 rows.
function _handleAltShortcuts(event) {
if ((event.modifiers & Qt.AltModifier) === 0)
return false;
if (event.modifiers & ~(Qt.AltModifier | Qt.KeypadModifier))
return false;

const view = commandStack.currentItem;
if (!view)
return false;

if (launcher.compacted) {
launcher.expand();
return true;
}

switch (event.key) {
case Qt.Key_1:
case Qt.Key_2:
case Qt.Key_3:
case Qt.Key_4:
case Qt.Key_5:
if (typeof view.activateRelative === "function") {
view.activateRelative(event.key - Qt.Key_1);
return true;
}
return false;
case Qt.Key_Q:
if (typeof view.moveBy === "function")
return view.moveBy(-5);
return false;
case Qt.Key_W:
if (typeof view.moveBy === "function")
return view.moveBy(5);
return false;
}
return false;
}

function _handleNavigation(event) {
const nav = Keyboard.matchNavigation(event.key, event.modifiers);
if (nav === 0)
Expand Down Expand Up @@ -274,11 +314,27 @@ Item {
event.accepted = launcher.forwardKey(event.key, event.modifiers);
}
}
Keys.onTabPressed: event => {
// Apply an alias typed at the end of the query as a scope filter,
// keeping the preceding words as the scoped command's query.
if (searchInput.text.length > 0 && typeof launcher.commandViewHost?.applyTrailingFilter === "function" && launcher.commandViewHost.applyTrailingFilter())
event.accepted = true;
else
event.accepted = false;
}
Keys.onBacktabPressed: event => {
event.accepted = false;
// Remove the current scope filter but keep the query text.
if (!launcher.atRoot) {
launcher.removeCurrentFilter();
event.accepted = true;
} else {
event.accepted = false;
}
}
Keys.onPressed: event => {
if (_handleEmacsEditing(event)) {
if (_handleAltShortcuts(event)) {
event.accepted = true;
} else if (_handleEmacsEditing(event)) {
event.accepted = true;
} else if (_handleNavigation(event)) {
event.accepted = true;
Expand Down
5 changes: 5 additions & 0 deletions src/server/src/qml/root-view-host.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ bool RootViewHost::tryAliasFastTrack() {
if (!m_model) return false;
return m_model->tryAliasFastTrack();
}

bool RootViewHost::applyTrailingFilter() {
if (!m_model) return false;
return m_model->applyTrailingFilter();
}
1 change: 1 addition & 0 deletions src/server/src/qml/root-view-host.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class RootViewHost : public ViewHostBase {

QObject *listModel() const;
Q_INVOKABLE bool tryAliasFastTrack();
Q_INVOKABLE bool applyTrailingFilter();

private:
RootSearchModel *m_model = nullptr;
Expand Down
15 changes: 15 additions & 0 deletions src/server/src/qml/section-list-model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,21 @@ int SectionListModel::nextSelectableIndex(int from, int direction) const {
return from;
}

int SectionListModel::quickSelectNumber(int row) const {
if (row < 0 || m_selectedIndex < 0) return 0;
if (row == m_selectedIndex) return 1;

int idx = m_selectedIndex;
for (int n = 2; n <= QUICK_SELECT_MAX; ++n) {
int const next = nextSelectableIndex(idx, 1);
if (next <= idx) break; // reached the end (nextSelectableIndex wraps)
idx = next;
if (idx == row) return n;
}

return 0;
}

int SectionListModel::nextSectionIndex(int from, int direction) const {
int const count = static_cast<int>(m_flat.size());
if (count == 0) return from;
Expand Down
5 changes: 5 additions & 0 deletions src/server/src/qml/section-list-model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ class SectionListModel : public QAbstractListModel {
Q_INVOKABLE int nextSelectableIndex(int from, int direction) const;
Q_INVOKABLE int nextSectionIndex(int from, int direction) const;
Q_INVOKABLE int scrollTargetIndex(int index, int direction) const;
// 1-based position of `row` among the selectable rows at/below the current
// selection (1 = selected, 2 = next selectable, ...), capped; 0 if not in range.
// Mirrors the Alt+1..5 "open Nth from selection" shortcut for on-screen numbering.
Q_INVOKABLE int quickSelectNumber(int row) const;
static constexpr int QUICK_SELECT_MAX = 5;

void refreshActionPanel();
void beforePop();
Expand Down