diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,7 @@ DocTools I18n IconThemes + ItemModels KIO Service Parts diff --git a/part/CMakeLists.txt b/part/CMakeLists.txt --- a/part/CMakeLists.txt +++ b/part/CMakeLists.txt @@ -22,7 +22,7 @@ add_library(arkpart MODULE ${arkpart_PART_SRCS}) -target_link_libraries(arkpart kerfuffle KF5::Parts KF5::KIOFileWidgets) +target_link_libraries(arkpart kerfuffle KF5::Parts KF5::KIOFileWidgets KF5::ItemModels) configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/ark_part.desktop.cmake diff --git a/part/ark_part.rc b/part/ark_part.rc --- a/part/ark_part.rc +++ b/part/ark_part.rc @@ -1,5 +1,5 @@ - + &Archive @@ -7,8 +7,9 @@ - + + &File @@ -37,6 +38,7 @@ + diff --git a/part/part.h b/part/part.h --- a/part/part.h +++ b/part/part.h @@ -41,9 +41,11 @@ class KAboutData; class KAbstractWidgetJobTracker; class KJob; +class KRecursiveFilterProxyModel; class KToggleAction; class QAction; +class QLineEdit; class QSplitter; class QTreeView; class QTemporaryDir; @@ -52,6 +54,7 @@ class QFileSystemWatcher; class QGroupBox; class QPlainTextEdit; +class QPushButton; namespace Ark { @@ -156,7 +159,9 @@ void slotAddComment(); void slotCommentChanged(); void slotTestArchive(); + void slotShowFind(); void displayMsgWidget(KMessageWidget::MessageType type, const QString& msg); + void searchEdited(const QString &text); signals: void busy(); @@ -175,6 +180,8 @@ QVector filesAndRootNodesForIndexes(const QModelIndexList& list) const; QModelIndexList addChildren(const QModelIndexList &list) const; void registerJob(KJob *job); + QModelIndexList getSelectedIndexes(); + bool eventFilter(QObject *target, QEvent *event); ArchiveModel *m_model; ArchiveView *m_view; @@ -193,6 +200,7 @@ QAction *m_propertiesAction; QAction *m_editCommentAction; QAction *m_testArchiveAction; + QAction *m_searchAction; KToggleAction *m_showInfoPanelAction; InfoPanel *m_infoPanel; QSplitter *m_splitter; @@ -216,6 +224,10 @@ KMessageWidget *m_commentMsgWidget; KMessageWidget *m_messageWidget; Kerfuffle::CompressionOptions m_compressionOptions; + KRecursiveFilterProxyModel *m_filterModel; + QWidget *m_searchWidget; + QLineEdit *m_searchLineEdit; + QPushButton *m_searchCloseButton; }; } // namespace Ark diff --git a/part/part.cpp b/part/part.cpp --- a/part/part.cpp +++ b/part/part.cpp @@ -51,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -75,6 +76,7 @@ #include #include #include +#include using namespace Kerfuffle; @@ -108,6 +110,7 @@ QWidget *mainWidget = new QWidget; m_vlayout = new QVBoxLayout; m_model = new ArchiveModel(pathName, this); + m_filterModel = new KRecursiveFilterProxyModel(this); m_splitter = new QSplitter(Qt::Horizontal, parentWidget); m_view = new ArchiveView; m_infoPanel = new InfoPanel(m_model); @@ -143,6 +146,28 @@ setWidget(mainWidget); mainWidget->setLayout(m_vlayout); + // Setup search widget. + m_searchWidget = new QWidget(parentWidget); + m_searchWidget->setVisible(false); + m_searchWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); + QHBoxLayout *searchLayout = new QHBoxLayout; + searchLayout->setContentsMargins(2, 2, 2, 2); + m_vlayout->addWidget(m_searchWidget); + m_searchWidget->setLayout(searchLayout); + m_searchCloseButton = new QPushButton(QIcon::fromTheme(QStringLiteral("dialog-close")), QString(), m_searchWidget); + m_searchCloseButton->setFlat(true); + m_searchLineEdit = new QLineEdit(m_searchWidget); + m_searchLineEdit->setClearButtonEnabled(true); + m_searchLineEdit->setPlaceholderText(i18n("Type to search...")); + mainWidget->installEventFilter(this); + searchLayout->addWidget(m_searchCloseButton); + searchLayout->addWidget(m_searchLineEdit); + connect(m_searchCloseButton, &QPushButton::clicked, this, [=]() { + m_searchWidget->hide(); + m_searchLineEdit->clear(); + }); + connect(m_searchLineEdit, &QLineEdit::textChanged, this, &Part::searchEdited); + // Configure the QVBoxLayout and add widgets m_vlayout->setContentsMargins(0,0,0,0); m_vlayout->addWidget(m_messageWidget); @@ -274,7 +299,7 @@ options.setDragAndDropEnabled(true); // Create and start the ExtractJob. - ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())), destination, options); + ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(getSelectedIndexes())), destination, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); @@ -291,7 +316,10 @@ { m_view->setContextMenuPolicy(Qt::CustomContextMenu); - m_view->setModel(m_model); + m_filterModel->setSourceModel(m_model); + m_view->setModel(m_filterModel); + m_filterModel->setFilterKeyColumn(0); + m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::updateActions); @@ -428,6 +456,13 @@ m_testArchiveAction->setToolTip(i18nc("@info:tooltip", "Click to test the archive for integrity")); connect(m_testArchiveAction, &QAction::triggered, this, &Part::slotTestArchive); + m_searchAction = actionCollection()->addAction(QStringLiteral("find_in_archive")); + m_searchAction->setIcon(QIcon::fromTheme(QStringLiteral("search"))); + m_searchAction->setText(i18nc("@action:inmenu", "&Find Files")); + actionCollection()->setDefaultShortcut(m_searchAction, Qt::CTRL + Qt::Key_F); + m_searchAction->setToolTip(i18nc("@info:tooltip", "Click to search in archive")); + connect(m_searchAction, &QAction::triggered, this, &Part::slotShowFind); + connect(m_signalMapper, static_cast(&QSignalMapper::mapped), this, &Part::slotOpenEntry); @@ -439,7 +474,7 @@ void Part::updateActions() { bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly(); - const Archive::Entry *entry = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); + const Archive::Entry *entry = m_model->entryForIndex(m_filterModel->mapToSource(m_view->selectionModel()->currentIndex())); int selectedEntriesCount = m_view->selectionModel()->selectedRows().count(); // We disable adding files if the archive is encrypted but the password is @@ -508,6 +543,9 @@ (selectedEntriesCount == 0 || (selectedEntriesCount == 1 && isDir)) && (m_model->filesToMove.count() > 0 || m_model->filesToCopy.count() > 0)); + m_searchAction->setEnabled(!isBusy() && + m_model->rowCount() > 0); + m_commentView->setEnabled(!isBusy()); m_commentMsgWidget->setEnabled(!isBusy()); @@ -694,7 +732,7 @@ qCDebug(ARK) << "Extracting to:" << finalDestinationDirectory; - ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())), finalDestinationDirectory, ExtractionOptions()); + ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(getSelectedIndexes())), finalDestinationDirectory, ExtractionOptions()); registerJob(job); connect(job, &KJob::result, @@ -706,7 +744,16 @@ void Part::selectionChanged() { - m_infoPanel->setIndexes(m_view->selectionModel()->selectedRows()); + m_infoPanel->setIndexes(getSelectedIndexes()); +} + +QModelIndexList Part::getSelectedIndexes() +{ + QModelIndexList list; + foreach (const QModelIndex &i, m_view->selectionModel()->selectedRows()) { + list.append(m_filterModel->mapToSource(i)); + } + return list; } bool Part::openFile() @@ -922,7 +969,7 @@ { qCDebug(ARK) << "Opening with mode" << mode; - QModelIndex index = m_view->selectionModel()->currentIndex(); + QModelIndex index = m_filterModel->mapToSource(m_view->selectionModel()->currentIndex()); Archive::Entry *entry = m_model->entryForIndex(index); // Don't open directories. @@ -1114,7 +1161,7 @@ // If the user has chosen to extract only selected entries, fetch these // from the QTreeView. if (!dialog.data()->extractAllFiles()) { - files = filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())); + files = filesAndRootNodesForIndexes(addChildren(getSelectedIndexes())); } qCDebug(ARK) << "Selected " << files; @@ -1336,7 +1383,7 @@ QString dialogTitle = i18nc("@title:window", "Add Files"); const Archive::Entry *destination = Q_NULLPTR; if (m_view->selectionModel()->selectedRows().count() == 1) { - destination = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); + destination = m_model->entryForIndex(m_filterModel->mapToSource(m_view->selectionModel()->currentIndex())); if (destination->isDir()) { dialogTitle = i18nc("@title:window", "Add Files to %1", destination->fullPath());; } else { @@ -1382,7 +1429,7 @@ void Part::slotCutFiles() { - QModelIndexList selectedRows = addChildren(m_view->selectionModel()->selectedRows()); + QModelIndexList selectedRows = addChildren(getSelectedIndexes()); m_model->filesToMove = ArchiveModel::entryMap(filesForIndexes(selectedRows)); qCDebug(ARK) << "Entries marked to cut:" << m_model->filesToMove.values(); m_model->filesToCopy.clear(); @@ -1398,7 +1445,7 @@ void Part::slotCopyFiles() { - m_model->filesToCopy = ArchiveModel::entryMap(filesForIndexes(addChildren(m_view->selectionModel()->selectedRows()))); + m_model->filesToCopy = ArchiveModel::entryMap(filesForIndexes(addChildren(getSelectedIndexes()))); qCDebug(ARK) << "Entries marked to copy:" << m_model->filesToCopy.values(); foreach (const QModelIndex &row, m_cutIndexes) { m_view->dataChanged(row, row); @@ -1414,8 +1461,8 @@ displayMsgWidget(KMessageWidget::Error, i18n("Filename can't contain slashes and can't be equal to \".\" or \"..\"")); return; } - const Archive::Entry *entry = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); - QVector entriesToMove = filesForIndexes(addChildren(m_view->selectionModel()->selectedRows())); + const Archive::Entry *entry = m_model->entryForIndex(m_filterModel->mapToSource(m_view->selectionModel()->currentIndex())); + QVector entriesToMove = filesForIndexes(addChildren(getSelectedIndexes())); m_destination = new Archive::Entry(); const QString &entryPath = entry->fullPath(NoTrailingSlash); @@ -1432,7 +1479,7 @@ void Part::slotPasteFiles() { m_destination = (m_view->selectionModel()->selectedRows().count() > 0) - ? m_model->entryForIndex(m_view->selectionModel()->currentIndex()) + ? m_model->entryForIndex(m_filterModel->mapToSource(m_view->selectionModel()->currentIndex())) : Q_NULLPTR; if (m_destination == Q_NULLPTR) { m_destination = new Archive::Entry(Q_NULLPTR, QString()); @@ -1590,7 +1637,7 @@ return; } - DeleteJob *job = m_model->deleteFiles(filesForIndexes(addChildren(m_view->selectionModel()->selectedRows()))); + DeleteJob *job = m_model->deleteFiles(filesForIndexes(addChildren(getSelectedIndexes()))); connect(job, &KJob::result, this, &Part::slotDeleteFilesDone); registerJob(job); @@ -1673,6 +1720,47 @@ popup->popup(QCursor::pos()); } +bool Part::eventFilter(QObject *target, QEvent *event) +{ + Q_UNUSED(target) + + if (event->type() == QEvent::KeyPress) { + QKeyEvent *e = static_cast(event); + if (e->key() == Qt::Key_Escape) { + m_searchWidget->hide(); + m_searchLineEdit->clear(); + return true; + } + } + return false; +} + +void Part::slotShowFind() +{ + if (m_searchWidget->isVisible()) { + m_searchLineEdit->selectAll(); + } else { + m_searchWidget->show(); + } + m_searchLineEdit->setFocus(); +} + +void Part::searchEdited(const QString &text) +{ + m_view->collapseAll(); + + m_filterModel->setFilterFixedString(text); + + if(text.isEmpty()) { + m_view->collapseAll(); + if (m_view->model()->rowCount() == 1) { + m_view->expandToDepth(0); + } + } else { + m_view->expandAll(); + } +} + void Part::displayMsgWidget(KMessageWidget::MessageType type, const QString& msg) { // The widget could be already visible, so hide it.