Some of our search pages (such as the room and user search) has a list header item. Due to how this works, it's not actually a part of the list view keyboard navigation and a whole separate item. So in the tab order, it comes *after* the list view which makes no sense. And it's part of the list view, so users must expect it to be selectable with the up and down arrows like other items. This simple change makes it so it behaves as expected. The first actual list item is selected by default, but it's possible to navigate to the list header item via the up arrow key and then return to the list view using the down arrow. The list header item is also removed from the tab order and the whole page is much nicer to use now.
188 lines
5.5 KiB
QML
188 lines
5.5 KiB
QML
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
|
|
|
import QtQuick
|
|
import QtQuick.Controls as QQC2
|
|
import QtQuick.Layouts
|
|
|
|
import org.kde.kirigami as Kirigami
|
|
|
|
/**
|
|
* @brief Component for a generic search page.
|
|
*
|
|
* This component provides a header with the search field and a ListView to visualise
|
|
* search results from the given model.
|
|
*/
|
|
Kirigami.ScrollablePage {
|
|
id: root
|
|
|
|
/**
|
|
* @brief Any additional controls after the search button.
|
|
*/
|
|
property alias headerTrailing: headerContent.children
|
|
|
|
/**
|
|
* @brief The model that provides the search results.
|
|
*
|
|
* The model needs to provide the following properties:
|
|
* - searchText
|
|
* - searching
|
|
* Where searchText is the text from the searchField and is used to match results
|
|
* and searching is true while the model is finding results.
|
|
*
|
|
* The model must also provide a search() function to start the search if
|
|
* it doesn't do so when the searchText is changed.
|
|
*/
|
|
property alias model: listView.model
|
|
|
|
/**
|
|
* @brief The number of delegates currently in the view.
|
|
*/
|
|
property alias count: listView.count
|
|
|
|
/**
|
|
* @brief The delegate to use to visualize the model data.
|
|
*/
|
|
property alias modelDelegate: listView.delegate
|
|
|
|
/**
|
|
* @brief The delegate to appear as the header of the list.
|
|
*/
|
|
property alias listHeaderDelegate: listView.header
|
|
|
|
/**
|
|
* @brief The delegate to appear as the footer of the list.
|
|
*/
|
|
property alias listFooterDelegate: listView.footer
|
|
|
|
/**
|
|
* @brief The placeholder text in the search field.
|
|
*/
|
|
property alias searchFieldPlaceholder: searchField.placeholderText
|
|
|
|
/**
|
|
* @brief The text to show when no search term has been entered.
|
|
*/
|
|
property alias noSearchPlaceholderMessage: noSearchMessage.text
|
|
|
|
/**
|
|
* @brief The text to show when no results have been found.
|
|
*/
|
|
property alias noResultPlaceholderMessage: noResultMessage.text
|
|
|
|
/**
|
|
* @brief The verticalLayoutDirection property of the internal ListView.
|
|
*/
|
|
property alias listVerticalLayoutDirection: listView.verticalLayoutDirection
|
|
|
|
/**
|
|
* @brief Force the search field to be focussed.
|
|
*/
|
|
function focusSearch() {
|
|
searchField.forceActiveFocus();
|
|
}
|
|
|
|
/**
|
|
* @brief Force the search to be updated if the model has a valid search function.
|
|
*/
|
|
function updateSearch() {
|
|
searchTimer.restart();
|
|
}
|
|
|
|
header: QQC2.Control {
|
|
padding: Kirigami.Units.largeSpacing
|
|
|
|
background: Rectangle {
|
|
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
|
Kirigami.Theme.inherit: false
|
|
color: Kirigami.Theme.backgroundColor
|
|
|
|
Kirigami.Separator {
|
|
anchors {
|
|
left: parent.left
|
|
bottom: parent.bottom
|
|
right: parent.right
|
|
}
|
|
}
|
|
}
|
|
|
|
contentItem: RowLayout {
|
|
id: headerContent
|
|
spacing: Kirigami.Units.largeSpacing
|
|
|
|
Kirigami.SearchField {
|
|
id: searchField
|
|
focus: true
|
|
Layout.fillWidth: true
|
|
Keys.onEnterPressed: searchButton.clicked()
|
|
Keys.onReturnPressed: searchButton.clicked()
|
|
onTextChanged: {
|
|
searchTimer.restart();
|
|
if (model) {
|
|
model.searchText = text;
|
|
}
|
|
}
|
|
}
|
|
QQC2.Button {
|
|
id: searchButton
|
|
icon.name: "search"
|
|
display: QQC2.Button.IconOnly
|
|
text: i18nc("@action:button", "Search")
|
|
|
|
onClicked: {
|
|
if (typeof model.search === 'function') {
|
|
model.search();
|
|
}
|
|
}
|
|
|
|
QQC2.ToolTip.visible: hovered
|
|
QQC2.ToolTip.text: text
|
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
|
}
|
|
Timer {
|
|
id: searchTimer
|
|
interval: 500
|
|
running: true
|
|
onTriggered: if (typeof model.search === 'function') {
|
|
model.search();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ListView {
|
|
id: listView
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
spacing: 0
|
|
|
|
section.property: "section"
|
|
|
|
Kirigami.PlaceholderMessage {
|
|
id: noSearchMessage
|
|
anchors.centerIn: parent
|
|
visible: searchField.text.length === 0 && listView.count === 0
|
|
}
|
|
|
|
Kirigami.PlaceholderMessage {
|
|
id: noResultMessage
|
|
anchors.centerIn: parent
|
|
visible: searchField.text.length > 0 && listView.count === 0 && !root.model.searching
|
|
}
|
|
|
|
Kirigami.LoadingPlaceholder {
|
|
anchors.centerIn: parent
|
|
visible: searchField.text.length > 0 && listView.count === 0 && root.model.searching
|
|
}
|
|
|
|
Keys.onUpPressed: {
|
|
if (listView.currentIndex > 0) {
|
|
listView.decrementCurrentIndex();
|
|
} else {
|
|
listView.currentIndex = -1; // This is so the list view doesn't appear to have two selected items
|
|
listView.headerItem.forceActiveFocus(Qt.TabFocusReason);
|
|
}
|
|
}
|
|
}
|
|
}
|