commit fd7f51cd
Add path to the top of the browser
Changed files
| M | CMakeLists.txt before |
| M | src/mainwindow.cpp before |
| M | src/mainwindow.h before |
| A | src/pathcompletermodel.cpp |
| A | src/pathcompletermodel.h |
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b332a92..1026ab0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -25,7 +25,9 @@ qt_add_executable(SagoImageBrowser
src/preferencesdialog.cpp
src/preferencesdialog.h
src/exifreader.cpp
- src/exifreader.h)
+ src/exifreader.h
+ src/pathcompletermodel.cpp
+ src/pathcompletermodel.h)
target_include_directories(SagoImageBrowser PRIVATE ${EXIV2_INCLUDE_DIRS})
target_link_libraries(SagoImageBrowser PRIVATE Qt6::Widgets ${EXIV2_LIBRARIES})
\ No newline at end of file
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 4275c07..97b6eb5 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -5,6 +5,7 @@
#include "thumbnailcache.h"
#include "exifreader.h"
#include "fsdirmodel.h"
+#include "pathcompletermodel.h"
#include <QFile>
#include <QTreeView>
@@ -31,9 +32,13 @@
#include <QTableWidget>
#include <QDialog>
#include <QVBoxLayout>
+#include <QHBoxLayout>
#include <QDialogButtonBox>
#include <QPlainTextEdit>
#include <QMessageBox>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QCompleter>
#include <iostream>
#include <fcntl.h>
@@ -108,14 +113,38 @@ void MainWindow::setupUi()
// Single image view
m_imageView = new ImageViewWidget;
+ // Path bar: editable absolute path with ajax-style folder completion and a Go button
+ m_pathEdit = new QLineEdit;
+ m_goButton = new QPushButton(tr("Go"));
+ m_pathCompleterModel = new PathCompleterModel(this);
+ m_pathCompleter = new QCompleter(m_pathCompleterModel, this);
+ m_pathCompleter->setCaseSensitivity(Qt::CaseSensitive);
+ m_pathCompleter->setCompletionMode(QCompleter::PopupCompletion);
+ m_pathCompleter->setFilterMode(Qt::MatchStartsWith);
+ m_pathEdit->setCompleter(m_pathCompleter);
+
+ auto *pathRow = new QHBoxLayout;
+ pathRow->setContentsMargins(4, 4, 4, 0);
+ pathRow->addWidget(m_pathEdit, 1);
+ pathRow->addWidget(m_goButton);
+
+ auto *browserPage = new QWidget;
+ auto *vbox = new QVBoxLayout(browserPage);
+ vbox->setContentsMargins(0, 0, 0, 0);
+ vbox->setSpacing(0);
+ vbox->addLayout(pathRow);
+ vbox->addWidget(m_listView, 1);
+
// Stack to switch between browser and single image
m_stack = new QStackedWidget;
- m_stack->addWidget(m_listView);
+ m_stack->addWidget(browserPage);
m_stack->addWidget(m_imageView);
- m_browserPage = m_listView;
+ m_browserPage = browserPage;
setCentralWidget(m_stack);
+ updatePathField(QFile::encodeName(QDir::homePath()));
+
// Dock widgets — can be dragged, floated and stacked by the user
m_rootDock = new QDockWidget(tr("Roots"), this);
m_rootDock->setObjectName("RootDock");
@@ -181,12 +210,30 @@ void MainWindow::navigateToFolder(const QByteArray &path)
QModelIndex dirIdx = m_dirModel->indexForPath(path);
if (dirIdx.isValid())
m_treeView->setCurrentIndex(dirIdx);
+ updatePathField(path);
QTimer::singleShot(0, this, [this]() {
selectPreviousFolderIfExists();
loadVisibleThumbnails();
});
}
+void MainWindow::updatePathField(const QByteArray &path)
+{
+ if (!m_pathEdit)
+ return;
+ const QString text = QFile::decodeName(path);
+ if (m_pathEdit->text() != text)
+ m_pathEdit->setText(text);
+}
+
+void MainWindow::onPathEntered()
+{
+ const QString text = m_pathEdit->text().trimmed();
+ if (text.isEmpty())
+ return;
+ openPath(text);
+}
+
void MainWindow::openPath(const QString &path)
{
QFileInfo info(path);
@@ -236,6 +283,13 @@ void MainWindow::selectPreviousFolderIfExists()
void MainWindow::setupConnections()
{
+ connect(m_pathEdit, &QLineEdit::returnPressed,
+ this, &MainWindow::onPathEntered);
+ connect(m_goButton, &QPushButton::clicked,
+ this, &MainWindow::onPathEntered);
+ connect(m_pathEdit, &QLineEdit::textEdited,
+ m_pathCompleterModel, &PathCompleterModel::setPrefix);
+
connect(m_rootList, &QListWidget::currentItemChanged, this,
[this](QListWidgetItem *current, QListWidgetItem *) {
if (current == nullptr)
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 944db13..8edecb4 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -12,8 +12,12 @@ class QDockWidget;
class FsDirModel;
class QListWidget;
class QTableWidget;
+class QLineEdit;
+class QPushButton;
+class QCompleter;
class ImageModel;
class ImageViewWidget;
+class PathCompleterModel;
class PreferencesDialog;
class MainWindow : public QMainWindow
@@ -42,6 +46,8 @@ private:
void enterSingleImageMode(const QModelIndex &index);
void leaveSingleImageMode();
void onEditCaption();
+ void onPathEntered();
+ void updatePathField(const QByteArray &path);
QList<QByteArray> computeNeighborPaths(const QModelIndex &index) const;
void keyPressEvent(QKeyEvent *event) override;
@@ -55,6 +61,10 @@ private:
QListWidget *m_rootList;
QListView *m_listView;
QLabel *m_previewLabel;
+ QLineEdit *m_pathEdit = nullptr;
+ QPushButton *m_goButton = nullptr;
+ PathCompleterModel *m_pathCompleterModel = nullptr;
+ QCompleter *m_pathCompleter = nullptr;
QStackedWidget *m_stack;
QString m_backgroundColorPreference = "black";
diff --git a/src/pathcompletermodel.cpp b/src/pathcompletermodel.cpp
new file mode 100644
index 0000000..2afcf71
--- /dev/null
+++ b/src/pathcompletermodel.cpp
@@ -0,0 +1,85 @@
+#include "pathcompletermodel.h"
+
+#include <QFile>
+#include <QFileInfo>
+
+#include <algorithm>
+#include <filesystem>
+#include <vector>
+
+namespace fs = std::filesystem;
+
+PathCompleterModel::PathCompleterModel(QObject *parent)
+ : QAbstractListModel(parent)
+{
+}
+
+int PathCompleterModel::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid())
+ return 0;
+ return static_cast<int>(m_entries.size());
+}
+
+QVariant PathCompleterModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.size())
+ return {};
+ if (role == Qt::DisplayRole || role == Qt::EditRole)
+ return m_entries.at(index.row());
+ return {};
+}
+
+void PathCompleterModel::setPrefix(const QString &text)
+{
+ // Determine the directory whose children we should list.
+ QString parentDir;
+ if (text.isEmpty()) {
+ parentDir = QStringLiteral("/");
+ } else if (text.endsWith(QLatin1Char('/'))) {
+ // Strip the trailing slash unless we're at filesystem root.
+ parentDir = (text.size() == 1) ? text : text.left(text.size() - 1);
+ } else {
+ QFileInfo info(text);
+ parentDir = info.path();
+ if (parentDir.isEmpty())
+ parentDir = QStringLiteral("/");
+ }
+
+ if (m_hasParent && parentDir == m_currentParent)
+ return;
+
+ QStringList newEntries;
+ try {
+ const QByteArray parentNative = QFile::encodeName(parentDir);
+ std::vector<std::pair<std::string, std::string>> entries; // {nativePath, filename}
+ for (const fs::directory_entry &entry :
+ fs::directory_iterator(parentNative.toStdString(),
+ fs::directory_options::skip_permission_denied))
+ {
+ std::error_code ec;
+ const std::string fname = entry.path().filename().native();
+ if (fname.empty() || fname[0] == '.')
+ continue;
+ if (!entry.is_directory(ec))
+ continue;
+ entries.push_back({entry.path().native(), fname});
+ }
+ std::sort(entries.begin(), entries.end(),
+ [](const auto &a, const auto &b) { return a.second < b.second; });
+
+ newEntries.reserve(static_cast<qsizetype>(entries.size()));
+ for (const auto &[npath, fname] : entries) {
+ newEntries.append(QFile::decodeName(
+ QByteArray::fromStdString(npath)));
+ }
+ } catch (const fs::filesystem_error &) {
+ // Inaccessible parent — fall through with an empty list.
+ }
+
+ beginResetModel();
+ m_entries = std::move(newEntries);
+ m_currentParent = parentDir;
+ m_hasParent = true;
+ endResetModel();
+}
diff --git a/src/pathcompletermodel.h b/src/pathcompletermodel.h
new file mode 100644
index 0000000..5dda786
--- /dev/null
+++ b/src/pathcompletermodel.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <QAbstractListModel>
+#include <QString>
+#include <QStringList>
+
+// Lightweight ajax-style completion model that exposes the immediate
+// subdirectories of whatever parent directory the user is typing into the
+// path field. Refreshed on each text change via setPrefix(); we only re-scan
+// when the parent directory actually changes, so per-keystroke cost is just
+// the QCompleter filter pass over an already-loaded list.
+//
+// Directory enumeration mirrors FsDirModel::listSubdirs (std::filesystem with
+// QFile::decodeName), so non-ASCII names that round-trip through the locale
+// are listed correctly.
+class PathCompleterModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ explicit PathCompleterModel(QObject *parent = nullptr);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+public slots:
+ // Update entries based on the current text in the path field.
+ void setPrefix(const QString &text);
+
+private:
+ QStringList m_entries;
+ QString m_currentParent;
+ bool m_hasParent = false;
+};