diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f84fe4..bd25e8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to FolderScope are documented here. The project uses [Semantic Versioning](https://semver.org/). Release tags are formatted as `vMAJOR.MINOR.PATCH`. +## [0.3.0] - 2026-07-26 + +### Added + +- Added persistent input history for folder and wildcard fields in Search + Options. + +### Changed + +- Renamed the executable, desktop entry, icon, and application identity from + SearchMyFiles to FolderScope. +- Preserved existing settings and search profiles when migrating to the new + application identity. +- Kept the base-folder browser button compact so the input field uses the + available width. + ## [0.2.5] - 2026-07-26 ### Added @@ -82,3 +98,4 @@ formatted as `vMAJOR.MINOR.PATCH`. [0.2.3]: https://gitea.forust.xyz/forust/folderscope/releases/tag/v0.2.3 [0.2.4]: https://gitea.forust.xyz/forust/folderscope/releases/tag/v0.2.4 [0.2.5]: https://gitea.forust.xyz/forust/folderscope/releases/tag/v0.2.5 +[0.3.0]: https://gitea.forust.xyz/forust/folderscope/releases/tag/v0.3.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 4eba91b..65ec4a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,7 +22,7 @@ target_link_libraries(folderscope_core PUBLIC Qt6::Core) target_compile_options(folderscope_core PRIVATE -Wall -Wextra -Wpedantic) target_include_directories(folderscope_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") -qt_add_executable(searchmyfiles +qt_add_executable(folderscope src/long_long_spin_box.cpp src/long_long_spin_box.h src/main.cpp @@ -38,14 +38,14 @@ qt_add_executable(searchmyfiles src/options_dialog.cpp ) -target_link_libraries(searchmyfiles PRIVATE folderscope_core Qt6::Widgets Qt6::Concurrent Qt6::DBus) -target_compile_options(searchmyfiles PRIVATE -Wall -Wextra -Wpedantic) -target_compile_definitions(searchmyfiles PRIVATE FOLDERSCOPE_VERSION="${PROJECT_VERSION}") +target_link_libraries(folderscope PRIVATE folderscope_core Qt6::Widgets Qt6::Concurrent Qt6::DBus) +target_compile_options(folderscope PRIVATE -Wall -Wextra -Wpedantic) +target_compile_definitions(folderscope PRIVATE FOLDERSCOPE_VERSION="${PROJECT_VERSION}") -install(TARGETS searchmyfiles RUNTIME DESTINATION bin) -install(FILES packaging/searchmyfiles.desktop +install(TARGETS folderscope RUNTIME DESTINATION bin) +install(FILES packaging/folderscope.desktop DESTINATION share/applications) -install(FILES packaging/searchmyfiles.svg +install(FILES packaging/folderscope.svg DESTINATION share/icons/hicolor/scalable/apps) include(CTest) diff --git a/README.md b/README.md index b7f5cd5..95d3375 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Requirements: ```bash cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build -j -./build/searchmyfiles +./build/folderscope ``` Run the automated tests: diff --git a/packaging/searchmyfiles.desktop b/packaging/folderscope.desktop similarity index 66% rename from packaging/searchmyfiles.desktop rename to packaging/folderscope.desktop index 2f27062..726b9c0 100644 --- a/packaging/searchmyfiles.desktop +++ b/packaging/folderscope.desktop @@ -1,8 +1,8 @@ [Desktop Entry] Type=Application -Name=SearchMyFiles +Name=FolderScope Comment=Fast advanced file search -Exec=searchmyfiles -Icon=searchmyfiles +Exec=folderscope +Icon=folderscope Terminal=false Categories=Utility;FileTools; diff --git a/packaging/searchmyfiles.svg b/packaging/folderscope.svg similarity index 100% rename from packaging/searchmyfiles.svg rename to packaging/folderscope.svg diff --git a/src/main.cpp b/src/main.cpp index 62e741f..bb255f3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,15 +1,38 @@ #include #include +#include #include #include "main_window.h" #include "search_core.h" +namespace { + +void migrateLegacySettings() +{ + QSettings settings; + const QString migrationKey = + QStringLiteral("migration/searchMyFilesSettingsImported"); + if (settings.value(migrationKey, false).toBool()) + return; + + QSettings legacy(QStringLiteral("SearchMyFilesLinux"), + QStringLiteral("SearchMyFiles")); + for (const QString &key : legacy.allKeys()) { + if (!settings.contains(key)) + settings.setValue(key, legacy.value(key)); + } + settings.setValue(migrationKey, true); +} + +} + int main(int argc, char **argv) { QApplication app(argc, argv); - QCoreApplication::setOrganizationName(QStringLiteral("SearchMyFilesLinux")); - QCoreApplication::setApplicationName(QStringLiteral("SearchMyFiles")); + QCoreApplication::setOrganizationName(QStringLiteral("FolderScope")); + QCoreApplication::setApplicationName(QStringLiteral("FolderScope")); + migrateLegacySettings(); QApplication::setStyle(QStringLiteral("Fusion")); qRegisterMetaType(); qRegisterMetaType>(); diff --git a/src/main_window.cpp b/src/main_window.cpp index 20acac5..4e8425e 100644 --- a/src/main_window.cpp +++ b/src/main_window.cpp @@ -23,7 +23,7 @@ MainWindow::MainWindow() { loadOptions(); - setWindowTitle(tr("SearchMyFiles for Linux")); + setWindowTitle(tr("FolderScope")); resize(1280, 760); model_ = new ResultsModel(this); model_->setDisplayOptions(sizeUnit_, showGmt_, markDuplicates_, duplicateColorSet_); @@ -507,7 +507,7 @@ void MainWindow::startCurrentSearch() progress_->show(); progress_->setRange(0, 0); progress_->setFormat(tr("Scanning…")); - setWindowTitle(tr("%1 — %2 — SearchMyFiles for Linux") + setWindowTitle(tr("%1 — %2 — FolderScope") .arg(options_.roots, options_.mode)); if (focusOnSearchStart_) table_->setFocus(); diff --git a/src/options_dialog.cpp b/src/options_dialog.cpp index 91338f5..541d9e9 100644 --- a/src/options_dialog.cpp +++ b/src/options_dialog.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "long_long_spin_box.h" @@ -87,22 +88,29 @@ void OptionsDialog::setupUi(const SearchOptions &o) auto *filesPage = new QWidget; auto *filesForm = new QFormLayout(filesPage); - roots_ = new QLineEdit(o.roots); - roots_->setAcceptDrops(true); + roots_ = historyCombo(QStringLiteral("roots"), o.roots); + roots_->lineEdit()->setAcceptDrops(true); roots_->setToolTip(tr("Drop folders here from your file manager")); - roots_->installEventFilter(this); + roots_->lineEdit()->installEventFilter(this); auto *rootRow = new QWidget; auto *rootLayout = new QHBoxLayout(rootRow); rootLayout->setContentsMargins(0, 0, 0, 0); rootLayout->addWidget(roots_); auto *browse = new QPushButton(tr("Browse…")); + browse->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); rootLayout->addWidget(browse); + rootLayout->setStretch(0, 1); filesForm->addRow(tr("Base folders (separate with ;):"), rootRow); - fileMasks_ = addLine(filesForm, tr("Files wildcard:"), o.fileWildcards); - subfolderMasks_ = addLine(filesForm, tr("Subfolders wildcard:"), o.subfolderWildcards); - excludeFiles_ = addLine(filesForm, tr("Exclude files:"), o.excludeFiles); - excludeFolders_ = addLine(filesForm, tr("Exclude folders:"), o.excludeFolders); - includeFolders_ = addLine(filesForm, tr("Include only folders:"), o.includeFolders); + fileMasks_ = addHistoryCombo(filesForm, tr("Files wildcard:"), + QStringLiteral("fileWildcards"), o.fileWildcards); + subfolderMasks_ = addHistoryCombo(filesForm, tr("Subfolders wildcard:"), + QStringLiteral("subfolderWildcards"), o.subfolderWildcards); + excludeFiles_ = addHistoryCombo(filesForm, tr("Exclude files:"), + QStringLiteral("excludeFiles"), o.excludeFiles); + excludeFolders_ = addHistoryCombo(filesForm, tr("Exclude folders:"), + QStringLiteral("excludeFolders"), o.excludeFolders); + includeFolders_ = addHistoryCombo(filesForm, tr("Include only folders:"), + QStringLiteral("includeFolders"), o.includeFolders); recursive_ = new QCheckBox(tr("Search subfolders")); recursive_->setChecked(o.recursive); findFiles_ = new QCheckBox(tr("Find files")); findFiles_->setChecked(o.findFiles); findFolders_ = new QCheckBox(tr("Find folders")); findFolders_->setChecked(o.findFolders); @@ -183,7 +191,7 @@ void OptionsDialog::setupUi(const SearchOptions &o) connect(buttons, &QDialogButtonBox::accepted, this, &OptionsDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(browse, &QPushButton::clicked, this, [this] { - const QStringList existingFolders = patterns(roots_->text()); + const QStringList existingFolders = patterns(roots_->currentText()); const QString startFolder = existingFolders.isEmpty() ? QDir::homePath() : existingFolders.constLast(); const QString folder = QFileDialog::getExistingDirectory( @@ -195,7 +203,7 @@ void OptionsDialog::setupUi(const SearchOptions &o) const QString cleanPath = QDir::cleanPath(folder); if (!folders.contains(cleanPath)) folders.push_back(cleanPath); - roots_->setText(folders.join(u';')); + roots_->setEditText(folders.join(u';')); }); layout->addWidget(buttons); } @@ -203,12 +211,12 @@ void OptionsDialog::setupUi(const SearchOptions &o) SearchOptions OptionsDialog::options() const { SearchOptions o; - o.roots = roots_->text(); - o.fileWildcards = fileMasks_->text(); - o.subfolderWildcards = subfolderMasks_->text(); - o.excludeFiles = excludeFiles_->text(); - o.excludeFolders = excludeFolders_->text(); - o.includeFolders = includeFolders_->text(); + o.roots = roots_->currentText(); + o.fileWildcards = fileMasks_->currentText(); + o.subfolderWildcards = subfolderMasks_->currentText(); + o.excludeFiles = excludeFiles_->currentText(); + o.excludeFolders = excludeFolders_->currentText(); + o.includeFolders = includeFolders_->currentText(); o.contains = contains_->toPlainText(); o.binary = binary_->isChecked(); o.multipleValues = multiple_->isChecked(); @@ -244,14 +252,35 @@ void OptionsDialog::accept() if (!validateDate(minTime_, tr("From time")) || !validateDate(maxTime_, tr("To time"))) return; + rememberHistory(QStringLiteral("roots"), roots_->currentText()); + rememberHistory(QStringLiteral("fileWildcards"), fileMasks_->currentText()); + rememberHistory(QStringLiteral("subfolderWildcards"), subfolderMasks_->currentText()); + rememberHistory(QStringLiteral("excludeFiles"), excludeFiles_->currentText()); + rememberHistory(QStringLiteral("excludeFolders"), excludeFolders_->currentText()); + rememberHistory(QStringLiteral("includeFolders"), includeFolders_->currentText()); QDialog::accept(); } -QLineEdit *OptionsDialog::addLine(QFormLayout *layout, const QString &label, const QString &value) +QComboBox *OptionsDialog::historyCombo(const QString &key, const QString &value) { - auto *edit = new QLineEdit(value); - layout->addRow(label, edit); - return edit; + auto *combo = new QComboBox; + combo->setEditable(true); + combo->setInsertPolicy(QComboBox::NoInsert); + combo->setObjectName(QStringLiteral("history%1").arg(key)); + + QSettings settings; + const QStringList history = settings.value(QStringLiteral("inputHistory/%1").arg(key)).toStringList(); + combo->addItems(history); + combo->setEditText(value); + return combo; +} + +QComboBox *OptionsDialog::addHistoryCombo(QFormLayout *layout, const QString &label, + const QString &key, const QString &value) +{ + auto *combo = historyCombo(key, value); + layout->addRow(label, combo); + return combo; } LongLongSpinBox *OptionsDialog::sizeSpin(qint64 value) @@ -284,6 +313,22 @@ bool OptionsDialog::validateDate(QLineEdit *edit, const QString &label) return false; } +void OptionsDialog::rememberHistory(const QString &key, const QString &value) +{ + if (value.isEmpty()) + return; + + QSettings settings; + const QString settingsKey = QStringLiteral("inputHistory/%1").arg(key); + QStringList history = settings.value(settingsKey).toStringList(); + history.removeAll(value); + history.prepend(value); + constexpr qsizetype maxHistoryEntries = 20; + if (history.size() > maxHistoryEntries) + history.erase(history.begin() + maxHistoryEntries, history.end()); + settings.setValue(settingsKey, history); +} + QComboBox *OptionsDialog::attrCombo(const QString &value) { auto *combo = new QComboBox; @@ -294,7 +339,7 @@ QComboBox *OptionsDialog::attrCombo(const QString &value) bool OptionsDialog::eventFilter(QObject *obj, QEvent *event) { - if (obj == roots_ && event->type() == QEvent::DragEnter) { + if (obj == roots_->lineEdit() && event->type() == QEvent::DragEnter) { auto *dragEnter = static_cast(event); if (dragEnter->mimeData()->hasUrls()) { dragEnter->acceptProposedAction(); @@ -302,11 +347,11 @@ bool OptionsDialog::eventFilter(QObject *obj, QEvent *event) } return false; } - if (obj == roots_ && event->type() == QEvent::Drop) { + if (obj == roots_->lineEdit() && event->type() == QEvent::Drop) { auto *drop = static_cast(event); if (!drop->mimeData()->hasUrls()) return false; - QStringList folders = patterns(roots_->text()); + QStringList folders = patterns(roots_->currentText()); for (const QUrl &url : drop->mimeData()->urls()) { if (!url.isLocalFile()) continue; @@ -314,7 +359,7 @@ bool OptionsDialog::eventFilter(QObject *obj, QEvent *event) if (QFileInfo::exists(cleanPath) && !folders.contains(cleanPath)) folders.append(cleanPath); } - roots_->setText(folders.join(u';')); + roots_->setEditText(folders.join(u';')); drop->acceptProposedAction(); return true; } @@ -401,12 +446,12 @@ void OptionsDialog::loadProfile() auto loadInt = [&](const char *key, int def) { return settings.value(QString::fromLatin1(key), def).toInt(); }; auto loadLong = [&](const char *key, qint64 def) { return settings.value(QString::fromLatin1(key), def).toLongLong(); }; - roots_->setText(load("roots")); - fileMasks_->setText(load("fileWildcards")); - subfolderMasks_->setText(load("subfolderWildcards")); - excludeFiles_->setText(load("excludeFiles")); - excludeFolders_->setText(load("excludeFolders")); - includeFolders_->setText(load("includeFolders")); + roots_->setEditText(load("roots")); + fileMasks_->setEditText(load("fileWildcards")); + subfolderMasks_->setEditText(load("subfolderWildcards")); + excludeFiles_->setEditText(load("excludeFiles")); + excludeFolders_->setEditText(load("excludeFolders")); + includeFolders_->setEditText(load("includeFolders")); contains_->setText(load("contains")); binary_->setChecked(loadBool("binary", false)); multiple_->setChecked(loadBool("multipleValues", false)); diff --git a/src/options_dialog.h b/src/options_dialog.h index 8f8c70e..4d71450 100644 --- a/src/options_dialog.h +++ b/src/options_dialog.h @@ -29,10 +29,13 @@ protected: private: bool eventFilter(QObject *obj, QEvent *event) override; - static QLineEdit *addLine(QFormLayout *layout, const QString &label, const QString &value); + static QComboBox *historyCombo(const QString &key, const QString &value); + static QComboBox *addHistoryCombo(QFormLayout *layout, const QString &label, + const QString &key, const QString &value); static LongLongSpinBox *sizeSpin(qint64 value); static qint64 dateValue(const QLineEdit *edit); bool validateDate(QLineEdit *edit, const QString &label); + void rememberHistory(const QString &key, const QString &value); static QComboBox *attrCombo(const QString &value); void setupUi(const SearchOptions &o); @@ -42,12 +45,12 @@ private: QComboBox *mode_{}; QComboBox *duplicateMode_{}; QCheckBox *duplicateWithoutExtension_{}; - QLineEdit *roots_{}; - QLineEdit *fileMasks_{}; - QLineEdit *subfolderMasks_{}; - QLineEdit *excludeFiles_{}; - QLineEdit *excludeFolders_{}; - QLineEdit *includeFolders_{}; + QComboBox *roots_{}; + QComboBox *fileMasks_{}; + QComboBox *subfolderMasks_{}; + QComboBox *excludeFiles_{}; + QComboBox *excludeFolders_{}; + QComboBox *includeFolders_{}; QTextEdit *contains_{}; QCheckBox *binary_{}; QCheckBox *multiple_{}; diff --git a/tests/options_dialog_test.cpp b/tests/options_dialog_test.cpp index b1577a6..1090df3 100644 --- a/tests/options_dialog_test.cpp +++ b/tests/options_dialog_test.cpp @@ -1,6 +1,8 @@ #include "options_dialog.h" #include +#include +#include #include class OptionsDialogTest final : public QObject { @@ -23,6 +25,35 @@ private slots: strict->setChecked(false); QVERIFY(!dialog.options().strictDuplicateComparison); } + + void remembersInputHistory() + { + QSettings settings; + settings.remove(QStringLiteral("inputHistory")); + + SearchOptions options; + OptionsDialog first(options); + auto *roots = first.findChild(QStringLiteral("historyroots")); + QVERIFY(roots); + roots->setEditText(QStringLiteral("/tmp/first;/tmp/second")); + QVERIFY(QMetaObject::invokeMethod(&first, "accept")); + + QCOMPARE(settings.value(QStringLiteral("inputHistory/roots")).toStringList(), + QStringList{QStringLiteral("/tmp/first;/tmp/second")}); + + OptionsDialog second(options); + auto *history = second.findChild(QStringLiteral("historyroots")); + QVERIFY(history); + QCOMPARE(history->itemText(0), QStringLiteral("/tmp/first;/tmp/second")); + + history->setEditText(QStringLiteral("/tmp/new")); + QVERIFY(QMetaObject::invokeMethod(&second, "accept")); + QCOMPARE(settings.value(QStringLiteral("inputHistory/roots")).toStringList(), + (QStringList{QStringLiteral("/tmp/new"), + QStringLiteral("/tmp/first;/tmp/second")})); + + settings.remove(QStringLiteral("inputHistory")); + } }; QTEST_MAIN(OptionsDialogTest)