diff --git a/CMakeLists.txt b/CMakeLists.txt index 412888b..946f8f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,16 +5,15 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) -find_package(Qt6 REQUIRED COMPONENTS Widgets Concurrent) +find_package(Qt6 REQUIRED COMPONENTS Widgets Concurrent DBus) qt_add_executable(searchmyfiles src/main.cpp ) -target_link_libraries(searchmyfiles PRIVATE Qt6::Widgets Qt6::Concurrent) +target_link_libraries(searchmyfiles PRIVATE Qt6::Widgets Qt6::Concurrent Qt6::DBus) target_compile_options(searchmyfiles PRIVATE -Wall -Wextra -Wpedantic) install(TARGETS searchmyfiles RUNTIME DESTINATION bin) install(FILES packaging/searchmyfiles.desktop DESTINATION share/applications) - diff --git a/src/main.cpp b/src/main.cpp index f3c3960..2c4ac1a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,10 +1,15 @@ #include +#include #include #include +#include #include #include #include #include +#include +#include +#include #include #include #include @@ -20,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -48,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -86,6 +93,8 @@ struct SearchOptions { QString mode = QStringLiteral("Standard search"); QString duplicateNameMode = QStringLiteral("All files and folders"); bool duplicateNameWithoutExtension = false; + bool showOnlyDuplicateFiles = true; + bool includeSubfoldersInSummary = false; QString hidden = QStringLiteral("Any"); QString readonly = QStringLiteral("Any"); QString executable = QStringLiteral("Any"); @@ -550,6 +559,10 @@ public slots: for (const auto &record : candidates) if (record.type == QStringLiteral("File") && !duplicatedPaths.contains(record.path)) results.push_back(record); + } else if (!o.showOnlyDuplicateFiles) { + for (const auto &record : candidates) + if (record.type == QStringLiteral("File") && !duplicatedPaths.contains(record.path)) + results.push_back(record); } } else if (o.mode == QStringLiteral("Duplicate names search")) { QHash> byName; @@ -592,8 +605,25 @@ public slots: } } else if (o.mode == QStringLiteral("Summary")) { QHash> byFolder; - for (const auto &record : candidates) + for (const auto &record : candidates) { byFolder[record.folder].push_back(record); + if (o.includeSubfoldersInSummary) { + QDir parent(record.folder); + while (parent.cdUp()) { + const QString ancestor = parent.absolutePath(); + bool belongsToRoot = false; + for (const QString &root : patterns(o.roots)) { + if (ancestor == QDir(root).absolutePath()) { + belongsToRoot = true; + break; + } + } + byFolder[ancestor].push_back(record); + if (belongsToRoot) + break; + } + } + } for (auto it = byFolder.cbegin(); it != byFolder.cend(); ++it) { qint64 total = 0; qint64 totalOnDisk = 0; @@ -982,6 +1012,7 @@ public: setWindowTitle(tr("SearchMyFiles for Linux")); resize(1280, 760); model_ = new ResultsModel(this); + model_->setDisplayOptions(sizeUnit_, showGmt_, markDuplicates_, duplicateColorSet_); proxy_ = new QSortFilterProxyModel(this); proxy_->setSourceModel(model_); proxy_->setSortRole(Qt::EditRole); @@ -995,10 +1026,18 @@ public: table_->horizontalHeader()->setSectionResizeMode(ResultsModel::Name, QHeaderView::ResizeToContents); table_->horizontalHeader()->setSectionResizeMode(ResultsModel::Folder, QHeaderView::Stretch); setCentralWidget(table_); + { + QSettings settings; + restoreGeometry(settings.value(QStringLiteral("ui/windowGeometry")).toByteArray()); + table_->horizontalHeader()->restoreState( + settings.value(QStringLiteral("ui/headerState")).toByteArray()); + table_->setShowGrid(showGrid_); + table_->viewport()->setAttribute(Qt::WA_AlwaysShowToolTips, showTooltips_); + } auto *toolbar = addToolBar(tr("Main")); searchAction_ = toolbar->addAction(QIcon::fromTheme(QStringLiteral("system-search")), tr("Search options…")); - searchAction_->setShortcut(Qt::Key_F9); + searchAction_->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_F9)); stopAction_ = toolbar->addAction(QIcon::fromTheme(QStringLiteral("process-stop")), tr("Stop")); stopAction_->setShortcut(Qt::Key_Escape); stopAction_->setEnabled(false); @@ -1008,8 +1047,23 @@ public: auto *exportAction = toolbar->addAction(QIcon::fromTheme(QStringLiteral("document-save")), tr("Export…")); exportAction->setShortcut(QKeySequence::Save); auto *openAction = toolbar->addAction(QIcon::fromTheme(QStringLiteral("document-open")), tr("Open")); - openAction->setShortcut(Qt::Key_Return); + openAction->setShortcut(Qt::Key_F9); auto *folderAction = toolbar->addAction(QIcon::fromTheme(QStringLiteral("folder-open")), tr("Open folder")); + folderAction->setShortcut(Qt::Key_F8); + auto *openWithAction = new QAction(tr("Open With…"), this); + openWithAction->setShortcut(Qt::Key_F7); + auto *selectInFolderAction = new QAction(tr("Select File In File Manager"), this); + auto *trashAction = new QAction(tr("Move To Trash"), this); + trashAction->setShortcut(Qt::Key_Delete); + auto *deleteAction = new QAction(tr("Delete Selected Files"), this); + deleteAction->setShortcut(QKeySequence(Qt::SHIFT | Qt::Key_Delete)); + auto *renameAction = new QAction(tr("Rename"), this); + renameAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_R)); + auto *propertiesAction = new QAction(tr("Properties"), this); + propertiesAction->setShortcut(QKeySequence(Qt::ALT | Qt::Key_Return)); + for (QAction *action : {openWithAction, selectInFolderAction, trashAction, + deleteAction, renameAction, propertiesAction}) + addAction(action); auto *fileMenu = menuBar()->addMenu(tr("&File")); fileMenu->addAction(searchAction_); @@ -1018,8 +1072,15 @@ public: fileMenu->addSeparator(); fileMenu->addAction(openAction); fileMenu->addAction(folderAction); + fileMenu->addAction(openWithAction); + fileMenu->addAction(selectInFolderAction); + fileMenu->addSeparator(); + fileMenu->addAction(trashAction); + fileMenu->addAction(deleteAction); + fileMenu->addAction(renameAction); fileMenu->addSeparator(); fileMenu->addAction(exportAction); + fileMenu->addAction(propertiesAction); fileMenu->addSeparator(); fileMenu->addAction(tr("Exit"), QKeySequence::Quit, this, &QWidget::close); @@ -1035,6 +1096,8 @@ public: copyPathsAction->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_C)); auto *explorerCopyAction = editMenu->addAction(tr("Explorer copy")); explorerCopyAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_E)); + auto *explorerCutAction = editMenu->addAction(tr("Explorer cut")); + explorerCutAction->setShortcut(QKeySequence::Cut); editMenu->addSeparator(); editMenu->addAction(tr("Select all"), QKeySequence::SelectAll, table_, &QTableView::selectAll); auto *deselectAction = editMenu->addAction(tr("Deselect all")); @@ -1043,26 +1106,129 @@ public: auto *viewMenu = menuBar()->addMenu(tr("&View")); auto *gridAction = viewMenu->addAction(tr("Show grid lines")); gridAction->setCheckable(true); - gridAction->setChecked(true); + gridAction->setChecked(showGrid_); auto *tooltipsAction = viewMenu->addAction(tr("Show tooltips")); tooltipsAction->setCheckable(true); - tooltipsAction->setChecked(true); + tooltipsAction->setChecked(showTooltips_); viewMenu->addSeparator(); - viewMenu->addAction(tr("Auto size columns"), this, [this] { + auto *autoSizeAction = viewMenu->addAction(tr("Auto size columns"), this, [this] { table_->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); table_->resizeColumnsToContents(); + saveUiSettings(); }); + autoSizeAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Plus)); auto *columnsMenu = viewMenu->addMenu(tr("Choose columns")); for (int column = 0; column < ResultsModel::ColumnCount; ++column) { auto *action = columnsMenu->addAction( model_->headerData(column, Qt::Horizontal, Qt::DisplayRole).toString()); action->setCheckable(true); - action->setChecked(true); + action->setChecked(!table_->isColumnHidden(column)); connect(action, &QAction::toggled, this, [this, column](bool visible) { table_->setColumnHidden(column, !visible); + saveUiSettings(); }); } + auto *optionsMenu = menuBar()->addMenu(tr("&Options")); + const auto addChoiceMenu = [this, optionsMenu]( + const QString &title, + const QStringList &choices, + const QString &selected, + const std::function &changed) { + auto *menu = optionsMenu->addMenu(title); + auto *group = new QActionGroup(menu); + group->setExclusive(true); + for (const QString &choice : choices) { + auto *action = menu->addAction(choice); + action->setCheckable(true); + action->setChecked(choice == selected); + group->addAction(action); + connect(action, &QAction::triggered, this, [this, choice, changed] { + changed(choice); + saveUiSettings(); + }); + } + return menu; + }; + + addChoiceMenu(tr("File Size Unit"), + {tr("Automatic"), tr("Bytes"), tr("KB"), tr("MB"), tr("GB")}, + sizeUnitName(), [this](const QString &choice) { + sizeUnit_ = sizeUnitFromName(choice); + applyDisplayOptions(); + }); + addChoiceMenu(tr("Summary File Size Unit"), + {tr("Automatic"), tr("Bytes"), tr("KB"), tr("MB"), tr("GB")}, + summarySizeUnitName_, [this](const QString &choice) { + summarySizeUnitName_ = choice; + if (options_.mode == QStringLiteral("Summary")) + applyDisplayOptions(); + }); + + auto *duplicateOptions = optionsMenu->addMenu(tr("Duplicate Search Options")); + auto *duplicateOptionsGroup = new QActionGroup(duplicateOptions); + auto *showAllDuplicates = duplicateOptions->addAction(tr("Show All Files")); + auto *showOnlyDuplicates = duplicateOptions->addAction(tr("Show Only Duplicate Files")); + for (QAction *action : {showAllDuplicates, showOnlyDuplicates}) { + action->setCheckable(true); + duplicateOptionsGroup->addAction(action); + } + showOnlyDuplicates->setChecked(options_.showOnlyDuplicateFiles); + showAllDuplicates->setChecked(!options_.showOnlyDuplicateFiles); + connect(showOnlyDuplicates, &QAction::triggered, this, [this] { + options_.showOnlyDuplicateFiles = true; + saveOptions(); + }); + connect(showAllDuplicates, &QAction::triggered, this, [this] { + options_.showOnlyDuplicateFiles = false; + saveOptions(); + }); + + addChoiceMenu(tr("Duplicate Mark Color Set"), + {tr("Color Set 1"), tr("Color Set 2")}, + duplicateColorSet_ == 1 ? tr("Color Set 1") : tr("Color Set 2"), + [this](const QString &choice) { + duplicateColorSet_ = choice.endsWith(u'1') ? 1 : 2; + applyDisplayOptions(); + }); + addChoiceMenu(tr("Double-Click Action"), actionChoices(), doubleClickAction_, + [this](const QString &choice) { doubleClickAction_ = choice; }); + addChoiceMenu(tr("Enter Key Action"), actionChoices(), enterKeyAction_, + [this](const QString &choice) { enterKeyAction_ = choice; }); + + auto addToggle = [this, optionsMenu](const QString &label, bool checked, + const std::function &changed) { + auto *action = optionsMenu->addAction(label); + action->setCheckable(true); + action->setChecked(checked); + connect(action, &QAction::toggled, this, [this, changed](bool value) { + changed(value); + saveUiSettings(); + }); + return action; + }; + addToggle(tr("Include Subfolders in Summary Totals"), options_.includeSubfoldersInSummary, + [this](bool value) { options_.includeSubfoldersInSummary = value; saveOptions(); }); + addToggle(tr("Show Time In GMT"), showGmt_, [this](bool value) { + showGmt_ = value; applyDisplayOptions(); + }); + optionsMenu->addSeparator(); + addToggle(tr("Mark Duplicate Files"), markDuplicates_, [this](bool value) { + markDuplicates_ = value; applyDisplayOptions(); + }); + addToggle(tr("Add Header Line To CSV/Tab-Delimited File"), addExportHeader_, + [this](bool value) { addExportHeader_ = value; }); + addToggle(tr("Retrieve File Owner"), options_.retrieveOwner, + [this](bool value) { options_.retrieveOwner = value; saveOptions(); }); + addToggle(tr("Accurate Progress (Count Files First)"), options_.accurateProgress, + [this](bool value) { options_.accurateProgress = value; saveOptions(); }); + addToggle(tr("Set Focus On Search Start"), focusOnSearchStart_, + [this](bool value) { focusOnSearchStart_ = value; }); + addToggle(tr("Set Focus On Search End"), focusOnSearchEnd_, + [this](bool value) { focusOnSearchEnd_ = value; }); + addToggle(tr("Auto Size Columns On Search End"), autoSizeOnSearchEnd_, + [this](bool value) { autoSizeOnSearchEnd_ = value; }); + menuBar()->addMenu(tr("&Help"))->addAction(tr("About"), this, [this] { QMessageBox::about(this, tr("About"), tr("SearchMyFiles for Linux\n\nFast unindexed file search.\nC++20 + Qt 6.")); @@ -1075,7 +1241,7 @@ public: progress_->setTextVisible(true); progress_->hide(); statusBar()->addPermanentWidget(progress_); - statusBar()->showMessage(tr("Press F9 to configure and start a search")); + statusBar()->showMessage(tr("Press Ctrl+F9 to configure and start a search")); engine_ = new SearchEngine; engine_->moveToThread(&workerThread_); @@ -1115,17 +1281,32 @@ public: connect(exportAction, &QAction::triggered, this, &MainWindow::exportResults); connect(openAction, &QAction::triggered, this, &MainWindow::openSelected); connect(folderAction, &QAction::triggered, this, &MainWindow::openFolder); + connect(openWithAction, &QAction::triggered, this, &MainWindow::openWith); + connect(selectInFolderAction, &QAction::triggered, this, &MainWindow::selectInFileManager); + connect(trashAction, &QAction::triggered, this, &MainWindow::moveToTrash); + connect(deleteAction, &QAction::triggered, this, &MainWindow::deleteSelected); + connect(renameAction, &QAction::triggered, this, &MainWindow::renameSelected); + connect(propertiesAction, &QAction::triggered, this, &MainWindow::showProperties); connect(findAction, &QAction::triggered, this, &MainWindow::findText); connect(findNextAction, &QAction::triggered, this, &MainWindow::findNext); connect(copyInfoAction, &QAction::triggered, this, &MainWindow::copyInformation); connect(copyPathsAction, &QAction::triggered, this, &MainWindow::copyPaths); connect(explorerCopyAction, &QAction::triggered, this, &MainWindow::explorerCopy); + connect(explorerCutAction, &QAction::triggered, this, &MainWindow::explorerCut); connect(deselectAction, &QAction::triggered, table_, &QTableView::clearSelection); - connect(gridAction, &QAction::toggled, table_, &QTableView::setShowGrid); - connect(tooltipsAction, &QAction::toggled, this, [this](bool enabled) { - table_->viewport()->setAttribute(Qt::WA_AlwaysShowToolTips, enabled); + connect(gridAction, &QAction::toggled, this, [this](bool enabled) { + showGrid_ = enabled; + table_->setShowGrid(enabled); + saveUiSettings(); + }); + connect(tooltipsAction, &QAction::toggled, this, [this](bool enabled) { + showTooltips_ = enabled; + table_->viewport()->setAttribute(Qt::WA_AlwaysShowToolTips, enabled); + saveUiSettings(); + }); + connect(table_, &QTableView::doubleClicked, this, [this] { + executeConfiguredAction(doubleClickAction_); }); - connect(table_, &QTableView::doubleClicked, this, &MainWindow::openSelected); connect(table_, &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu); } @@ -1136,6 +1317,24 @@ public: workerThread_.wait(); } +protected: + void closeEvent(QCloseEvent *event) override + { + saveUiSettings(); + QMainWindow::closeEvent(event); + } + + void keyPressEvent(QKeyEvent *event) override + { + if ((event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) + && event->modifiers() == Qt::NoModifier) { + executeConfiguredAction(enterKeyAction_); + event->accept(); + return; + } + QMainWindow::keyPressEvent(event); + } + signals: void startRequested(const SearchOptions &options); void stopRequested(); @@ -1232,11 +1431,31 @@ private slots: QApplication::clipboard()->setMimeData(mime); } + void explorerCut() + { + const QStringList paths = selectedPaths(); + if (paths.isEmpty()) + return; + auto *mime = new QMimeData; + QList urls; + QByteArray gnomeData("cut\n"); + for (const QString &path : paths) { + const QUrl url = QUrl::fromLocalFile(path); + urls.push_back(url); + gnomeData += url.toEncoded() + '\n'; + } + mime->setUrls(urls); + mime->setText(paths.join(u'\n')); + mime->setData(QStringLiteral("x-special/gnome-copied-files"), gnomeData); + QApplication::clipboard()->setMimeData(mime); + } + void startCurrentSearch() { if (isSearching_ || options_.roots.trimmed().isEmpty()) return; isSearching_ = true; + applyDisplayOptions(); model_->setRows({}); searchAction_->setEnabled(false); stopAction_->setEnabled(true); @@ -1246,6 +1465,8 @@ private slots: progress_->setFormat(tr("Scanning…")); setWindowTitle(tr("%1 — %2 — SearchMyFiles for Linux") .arg(options_.roots, options_.mode)); + if (focusOnSearchStart_) + table_->setFocus(); emit startRequested(options_); } @@ -1258,6 +1479,12 @@ private slots: refreshAction_->setEnabled(true); progress_->hide(); statusBar()->showMessage(message); + if (autoSizeOnSearchEnd_) { + table_->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + table_->resizeColumnsToContents(); + } + if (focusOnSearchEnd_) + table_->setFocus(); } void openSelected() @@ -1273,6 +1500,137 @@ private slots: QDesktopServices::openUrl(QUrl::fromLocalFile(QFileInfo(paths.front()).absolutePath())); } + void openWith() + { + const QStringList paths = selectedPaths(); + if (!paths.isEmpty()) + QProcess::startDetached(QStringLiteral("gio"), {QStringLiteral("open"), paths.front()}); + } + + void selectInFileManager() + { + const QStringList paths = selectedPaths(); + if (paths.isEmpty()) + return; + QStringList urls; + for (const QString &path : paths) + urls.push_back(QUrl::fromLocalFile(path).toString()); + QDBusInterface fileManager( + QStringLiteral("org.freedesktop.FileManager1"), + QStringLiteral("/org/freedesktop/FileManager1"), + QStringLiteral("org.freedesktop.FileManager1"), + QDBusConnection::sessionBus()); + const QDBusMessage reply = fileManager.call(QStringLiteral("ShowItems"), urls, QString{}); + if (!fileManager.isValid() || reply.type() == QDBusMessage::ErrorMessage) + openFolder(); + } + + void moveToTrash() + { + const QStringList paths = selectedPaths(); + if (paths.isEmpty() + || QMessageBox::question( + this, tr("Move To Trash"), + tr("Move %1 selected item(s) to Trash?").arg(paths.size()), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) + return; + int failed = 0; + for (const QString &path : paths) + if (!QFile::moveToTrash(path)) + ++failed; + if (failed) + QMessageBox::warning(this, tr("Move To Trash"), + tr("Failed to move %1 item(s) to Trash.").arg(failed)); + refreshSearch(); + } + + void deleteSelected() + { + const QStringList paths = selectedPaths(); + if (paths.isEmpty() + || QMessageBox::warning( + this, tr("Delete Selected Files"), + tr("Permanently delete %1 selected item(s)?\n\nThis cannot be undone.") + .arg(paths.size()), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) + return; + int failed = 0; + for (const QString &path : paths) { + const QFileInfo info(path); + const bool removed = info.isDir() ? QDir().rmdir(path) : QFile::remove(path); + if (!removed) + ++failed; + } + if (failed) + QMessageBox::warning(this, tr("Delete Selected Files"), + tr("Failed to delete %1 item(s).").arg(failed)); + refreshSearch(); + } + + void renameSelected() + { + const auto record = currentRecord(); + if (!record) + return; + bool accepted = false; + const QString newName = QInputDialog::getText( + this, tr("Rename"), tr("New filename:"), QLineEdit::Normal, + record->name, &accepted); + if (!accepted || newName.isEmpty() || newName == record->name) + return; + const QString newPath = QDir(record->folder).filePath(newName); + if (!QFile::rename(record->path, newPath)) + QMessageBox::critical(this, tr("Rename"), tr("Failed to rename this item.")); + else + refreshSearch(); + } + + void showProperties() + { + const auto record = currentRecord(); + if (!record) + return; + QDialog dialog(this); + dialog.setWindowTitle(tr("Properties")); + dialog.resize(780, 540); + auto *layout = new QVBoxLayout(&dialog); + auto *form = new QFormLayout; + const auto addValue = [form](const QString &label, const QString &value) { + auto *edit = new QLineEdit(value); + edit->setReadOnly(true); + edit->setCursorPosition(0); + form->addRow(label, edit); + }; + const auto date = [this](qint64 timestamp) { + if (!timestamp) + return QString{}; + QDateTime value = QDateTime::fromSecsSinceEpoch(timestamp); + if (showGmt_) + value = value.toUTC(); + return value.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss")); + }; + addValue(tr("Filename:"), record->name); + addValue(tr("Folder:"), record->folder); + addValue(tr("Full Path:"), record->path); + addValue(tr("Size:"), QString::number(record->size)); + addValue(tr("Size On Disk:"), QString::number(record->sizeOnDisk)); + addValue(tr("Modified Time:"), date(record->modified)); + addValue(tr("Created Time:"), date(record->created)); + addValue(tr("Last Accessed Time:"), date(record->accessed)); + addValue(tr("Entry Modified Time:"), date(record->changed)); + addValue(tr("Attributes:"), record->attributes); + addValue(tr("Extension:"), QFileInfo(record->name).suffix()); + addValue(tr("Duplicate Number:"), record->group ? QString::number(record->group) : QString{}); + addValue(tr("Duplicate Group:"), + record->duplicateCopy ? QString::number(record->duplicateCopy) : QString{}); + addValue(tr("File Owner:"), record->owner); + layout->addLayout(form); + auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok); + connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + layout->addWidget(buttons); + dialog.exec(); + } + void copyPaths() { QApplication::clipboard()->setText(selectedPaths().join(u'\n')); @@ -1281,11 +1639,45 @@ private slots: void contextMenu(const QPoint &point) { QMenu menu(this); - menu.addAction(tr("Open"), this, &MainWindow::openSelected); - menu.addAction(tr("Open containing folder"), this, &MainWindow::openFolder); + menu.addAction(tr("Open Selected File"), QKeySequence(Qt::Key_F9), this, &MainWindow::openSelected); + menu.addAction(tr("Open Folder"), QKeySequence(Qt::Key_F8), this, &MainWindow::openFolder); + menu.addAction(tr("Open With…"), QKeySequence(Qt::Key_F7), this, &MainWindow::openWith); + menu.addAction(tr("Select File In File Manager"), this, &MainWindow::selectInFileManager); menu.addSeparator(); - menu.addAction(tr("Copy paths"), this, &MainWindow::copyPaths); - menu.addAction(tr("Export selected…"), this, &MainWindow::exportResults); + menu.addAction(tr("Explorer Copy"), QKeySequence(Qt::CTRL | Qt::Key_E), + this, &MainWindow::explorerCopy); + menu.addAction(tr("Explorer Cut"), QKeySequence::Cut, this, &MainWindow::explorerCut); + menu.addSeparator(); + menu.addAction(tr("Move To Trash"), QKeySequence(Qt::Key_Delete), + this, &MainWindow::moveToTrash); + menu.addAction(tr("Delete Selected Files"), QKeySequence(Qt::SHIFT | Qt::Key_Delete), + this, &MainWindow::deleteSelected); + menu.addAction(tr("Rename"), QKeySequence(Qt::CTRL | Qt::Key_R), + this, &MainWindow::renameSelected); + menu.addSeparator(); + menu.addAction(tr("Save Files Information"), QKeySequence::Save, + this, &MainWindow::exportResults); + menu.addAction(tr("Copy Files Information"), QKeySequence::Copy, + this, &MainWindow::copyInformation); + menu.addSeparator(); + auto *columns = menu.addMenu(tr("Choose Columns")); + for (int column = 0; column < ResultsModel::ColumnCount; ++column) { + auto *action = columns->addAction( + model_->headerData(column, Qt::Horizontal, Qt::DisplayRole).toString()); + action->setCheckable(true); + action->setChecked(!table_->isColumnHidden(column)); + connect(action, &QAction::toggled, this, [this, column](bool visible) { + table_->setColumnHidden(column, !visible); + saveUiSettings(); + }); + } + menu.addAction(tr("Auto Size Columns"), QKeySequence(Qt::CTRL | Qt::Key_Plus), this, [this] { + table_->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + table_->resizeColumnsToContents(); + }); + menu.addSeparator(); + menu.addAction(tr("Properties"), QKeySequence(Qt::ALT | Qt::Key_Return), + this, &MainWindow::showProperties); menu.addSeparator(); menu.addAction(tr("Refresh"), QKeySequence(Qt::Key_F5), this, &MainWindow::refreshSearch); menu.exec(table_->viewport()->mapToGlobal(point)); @@ -1317,13 +1709,21 @@ private slots: }; const auto values = [](const FileRecord &r) { const auto date = [](qint64 t) { return t ? QDateTime::fromSecsSinceEpoch(t).toString(Qt::ISODate) : QString{}; }; - return QStringList{r.name, r.folder, QString::number(r.size), QString::number(r.sizeOnDisk), date(r.modified), - date(r.accessed), date(r.changed), r.type, r.owner, r.attributes, - r.group ? QString::number(r.group) : QString{}, r.path}; + return QStringList{ + r.name, r.folder, QString::number(r.size), QString::number(r.sizeOnDisk), + date(r.modified), date(r.created), date(r.accessed), date(r.changed), + r.attributes, QFileInfo(r.name).suffix(), + r.group ? QString::number(r.group) : QString{}, + r.duplicateCopy ? QString::number(r.duplicateCopy) : QString{}, + r.type, r.owner, r.path + }; + }; + const QStringList headers{ + tr("Filename"), tr("Folder"), tr("Size"), tr("Size On Disk"), tr("Modified Time"), + tr("Created Time"), tr("Last Accessed Time"), tr("Entry Modified Time"), + tr("Attributes"), tr("Extension"), tr("Duplicate Number"), tr("Duplicate Group"), + tr("Type"), tr("File Owner"), tr("Full Path") }; - const QStringList headers{tr("Name"), tr("Folder"), tr("Size"), tr("Size On Disk"), tr("Modified Time"), - tr("Accessed Time"), tr("Changed Time"), tr("Type"), - tr("Owner"), tr("Attributes"), tr("Duplicate Number"), tr("Full Path")}; QTextStream out(&file); const QString suffix = QFileInfo(path).suffix().toLower(); if (suffix == QStringLiteral("json")) { @@ -1362,7 +1762,8 @@ private slots: out << ""; } else { const QChar separator = suffix == QStringLiteral("csv") ? u',' : u'\t'; - out << headers.join(separator) << u'\n'; + if (addExportHeader_) + out << headers.join(separator) << u'\n'; for (const auto &record : rows) { QStringList row = values(record); if (separator == u',') @@ -1374,6 +1775,77 @@ private slots: } private: + std::optional currentRecord() const + { + const QModelIndex current = table_->currentIndex(); + if (!current.isValid()) + return std::nullopt; + const int row = proxy_->mapToSource(current).row(); + if (row < 0 || row >= model_->rows().size()) + return std::nullopt; + return model_->rows().at(row); + } + + QStringList actionChoices() const + { + return {tr("No Action"), tr("Open Properties Window"), tr("Open Selected File"), + tr("Open With…"), tr("Open Folder"), tr("Select File In File Manager")}; + } + + void executeConfiguredAction(const QString &action) + { + if (action == tr("Open Properties Window")) + showProperties(); + else if (action == tr("Open Selected File")) + openSelected(); + else if (action == tr("Open With…")) + openWith(); + else if (action == tr("Open Folder")) + openFolder(); + else if (action == tr("Select File In File Manager")) + selectInFileManager(); + } + + QString sizeUnitName() const + { + static const QStringList units{tr("Automatic"), tr("Bytes"), tr("KB"), tr("MB"), tr("GB")}; + return units.value(sizeUnit_, units.front()); + } + + int sizeUnitFromName(const QString &name) const + { + const QStringList units{tr("Automatic"), tr("Bytes"), tr("KB"), tr("MB"), tr("GB")}; + return std::max(0, int(units.indexOf(name))); + } + + void applyDisplayOptions() + { + const int unit = options_.mode == QStringLiteral("Summary") + ? sizeUnitFromName(summarySizeUnitName_) : sizeUnit_; + model_->setDisplayOptions(unit, showGmt_, markDuplicates_, duplicateColorSet_); + } + + void saveUiSettings() + { + QSettings settings; + settings.setValue(QStringLiteral("ui/sizeUnit"), sizeUnit_); + settings.setValue(QStringLiteral("ui/summarySizeUnit"), summarySizeUnitName_); + settings.setValue(QStringLiteral("ui/showGmt"), showGmt_); + settings.setValue(QStringLiteral("ui/markDuplicates"), markDuplicates_); + settings.setValue(QStringLiteral("ui/duplicateColorSet"), duplicateColorSet_); + settings.setValue(QStringLiteral("ui/doubleClickAction"), doubleClickAction_); + settings.setValue(QStringLiteral("ui/enterKeyAction"), enterKeyAction_); + settings.setValue(QStringLiteral("ui/addExportHeader"), addExportHeader_); + settings.setValue(QStringLiteral("ui/focusOnSearchStart"), focusOnSearchStart_); + settings.setValue(QStringLiteral("ui/focusOnSearchEnd"), focusOnSearchEnd_); + settings.setValue(QStringLiteral("ui/autoSizeOnSearchEnd"), autoSizeOnSearchEnd_); + settings.setValue(QStringLiteral("ui/showGrid"), showGrid_); + settings.setValue(QStringLiteral("ui/showTooltips"), showTooltips_); + settings.setValue(QStringLiteral("ui/windowGeometry"), saveGeometry()); + if (table_) + settings.setValue(QStringLiteral("ui/headerState"), table_->horizontalHeader()->saveState()); + } + QStringList selectedPaths() const { QStringList paths; @@ -1414,6 +1886,8 @@ private: settings.setValue(QStringLiteral("mode"), options_.mode); settings.setValue(QStringLiteral("duplicateNameMode"), options_.duplicateNameMode); settings.setValue(QStringLiteral("duplicateNameWithoutExtension"), options_.duplicateNameWithoutExtension); + settings.setValue(QStringLiteral("showOnlyDuplicateFiles"), options_.showOnlyDuplicateFiles); + settings.setValue(QStringLiteral("includeSubfoldersInSummary"), options_.includeSubfoldersInSummary); settings.setValue(QStringLiteral("hidden"), options_.hidden); settings.setValue(QStringLiteral("readonly"), options_.readonly); settings.setValue(QStringLiteral("executable"), options_.executable); @@ -1448,9 +1922,26 @@ private: options_.mode = s.value(QStringLiteral("mode"), options_.mode).toString(); options_.duplicateNameMode = s.value(QStringLiteral("duplicateNameMode"), options_.duplicateNameMode).toString(); options_.duplicateNameWithoutExtension = s.value(QStringLiteral("duplicateNameWithoutExtension"), false).toBool(); + options_.showOnlyDuplicateFiles = s.value(QStringLiteral("showOnlyDuplicateFiles"), true).toBool(); + options_.includeSubfoldersInSummary = s.value(QStringLiteral("includeSubfoldersInSummary"), false).toBool(); options_.hidden = s.value(QStringLiteral("hidden"), options_.hidden).toString(); options_.readonly = s.value(QStringLiteral("readonly"), options_.readonly).toString(); options_.executable = s.value(QStringLiteral("executable"), options_.executable).toString(); + sizeUnit_ = s.value(QStringLiteral("ui/sizeUnit"), 0).toInt(); + summarySizeUnitName_ = s.value(QStringLiteral("ui/summarySizeUnit"), tr("Automatic")).toString(); + showGmt_ = s.value(QStringLiteral("ui/showGmt"), false).toBool(); + markDuplicates_ = s.value(QStringLiteral("ui/markDuplicates"), true).toBool(); + duplicateColorSet_ = s.value(QStringLiteral("ui/duplicateColorSet"), 1).toInt(); + doubleClickAction_ = s.value( + QStringLiteral("ui/doubleClickAction"), tr("Open Properties Window")).toString(); + enterKeyAction_ = s.value( + QStringLiteral("ui/enterKeyAction"), tr("Open Selected File")).toString(); + addExportHeader_ = s.value(QStringLiteral("ui/addExportHeader"), true).toBool(); + focusOnSearchStart_ = s.value(QStringLiteral("ui/focusOnSearchStart"), true).toBool(); + focusOnSearchEnd_ = s.value(QStringLiteral("ui/focusOnSearchEnd"), true).toBool(); + autoSizeOnSearchEnd_ = s.value(QStringLiteral("ui/autoSizeOnSearchEnd"), false).toBool(); + showGrid_ = s.value(QStringLiteral("ui/showGrid"), true).toBool(); + showTooltips_ = s.value(QStringLiteral("ui/showTooltips"), true).toBool(); } SearchOptions options_; @@ -1464,6 +1955,19 @@ private: QThread workerThread_; SearchEngine *engine_{}; QString findQuery_; + QString summarySizeUnitName_ = QStringLiteral("Automatic"); + QString doubleClickAction_ = QStringLiteral("Open Properties Window"); + QString enterKeyAction_ = QStringLiteral("Open Selected File"); + int sizeUnit_ = 0; + int duplicateColorSet_ = 1; + bool showGmt_ = false; + bool markDuplicates_ = true; + bool addExportHeader_ = true; + bool focusOnSearchStart_ = true; + bool focusOnSearchEnd_ = true; + bool autoSizeOnSearchEnd_ = false; + bool showGrid_ = true; + bool showTooltips_ = true; bool isSearching_ = false; };