Skip to content

Commit c3cadbc

Browse files
Search files in project & replace already installed app (#204)
* Fixes #179: Search in project & open files * Fixes #168: Reinstall package if exists * Fixes #166: Set toolbar widget name * Update README
1 parent 00d143f commit c3cadbc

4 files changed

Lines changed: 199 additions & 23 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Open-source, cross platform [Qt6](https://www.qt.io/) based IDE for reverse-engi
3232
- **Automatic tool download & installation** - APK Studio can automatically download and install required tools (Java, Apktool, JADX, ADB, Uber APK Signer)
3333
- **Framework support** - Install and use manufacturer-specific framework files (e.g., HTC, LG, Samsung) with optional tagging for decompiling and recompiling APKs
3434
- **Command-line APK opening** - Open APK files directly from the file system via "Open with" context menu or command-line arguments
35+
- **Search functionality** - Quick search in open files and project tree to find items by name
3536
- Built-in code editor (\*.java; \*.smali; \*.xml; \*.yml) w/ syntax highlighting
3637
- Built-in viewer for image (\*.gif; \*.jpg; \*.jpeg; \*.png) files
3738
- Built-in hex editor for binary files

sources/adbinstallworker.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ void AdbInstallWorker::install()
1919
return;
2020
}
2121
QStringList args;
22-
args << "install" << m_Apk;
22+
args << "install" << "-r" << m_Apk;
2323
ProcessResult result = ProcessUtils::runCommand(adb, args);
2424
#ifdef QT_DEBUG
2525
qDebug() << "ADB returned code" << result.code;

sources/mainwindow.cpp

Lines changed: 188 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <QPushButton>
1616
#include <QProcess>
1717
#include <QSettings>
18+
#include <QSignalBlocker>
1819
#include <QStatusBar>
1920
#include <QTabWidget>
2021
#include <QTextDocumentFragment>
@@ -83,6 +84,10 @@ MainWindow::MainWindow(const QMap<QString, QString> &versions, QWidget *parent)
8384
m_ActionViewProject->setChecked(m_DockProject->isVisible());
8485
m_ActionViewFiles->setChecked(m_DockFiles->isVisible());
8586
m_ActionViewConsole->setChecked(m_DockConsole->isVisible());
87+
88+
// Ensure main window gets focus instead of search boxes
89+
setFocus();
90+
8691
QTimer::singleShot(100, [=] {
8792
QSettings settings;
8893
const QStringList files = settings.value("open_files").toStringList();
@@ -201,22 +206,45 @@ QDockWidget *MainWindow::buildConsoleDock()
201206
QDockWidget *MainWindow::buildFilesDock()
202207
{
203208
auto dock = new QDockWidget(tr("Files"), this);
209+
auto widget = new QWidget(this);
210+
auto layout = new QVBoxLayout(widget);
211+
layout->setContentsMargins(0, 0, 0, 0);
212+
layout->setSpacing(2);
213+
214+
m_SearchFiles = new QLineEdit(this);
215+
m_SearchFiles->setPlaceholderText(tr("Search in open files..."));
216+
m_SearchFiles->setClearButtonEnabled(true);
217+
connect(m_SearchFiles, &QLineEdit::textChanged, this, &MainWindow::handleFilesSearchChanged);
218+
layout->addWidget(m_SearchFiles);
219+
204220
m_ListOpenFiles = new QListView(this);
205221
m_ListOpenFiles->setContextMenuPolicy(Qt::CustomContextMenu);
206222
m_ListOpenFiles->setEditTriggers(QAbstractItemView::NoEditTriggers);
207223
m_ListOpenFiles->setMinimumWidth(240);
208-
m_ListOpenFiles->setModel(m_ModelOpenFiles = new QStandardItemModel(m_ListOpenFiles));
224+
m_ModelOpenFiles = new QStandardItemModel(m_ListOpenFiles);
225+
m_FilesProxyModel = new QSortFilterProxyModel(this);
226+
m_FilesProxyModel->setSourceModel(m_ModelOpenFiles);
227+
m_FilesProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
228+
m_FilesProxyModel->setFilterRole(Qt::DisplayRole);
229+
m_ListOpenFiles->setModel(m_FilesProxyModel);
209230
m_ListOpenFiles->setSelectionBehavior(QAbstractItemView::SelectItems);
210231
m_ListOpenFiles->setSelectionMode(QAbstractItemView::SingleSelection);
211-
connect(m_ListOpenFiles->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::handleFilesSelectionChanged);
232+
// Get selection model after model is set to ensure it uses proxy model indices
233+
QItemSelectionModel *selectionModel = m_ListOpenFiles->selectionModel();
234+
if (selectionModel) {
235+
connect(selectionModel, &QItemSelectionModel::selectionChanged, this, &MainWindow::handleFilesSelectionChanged);
236+
}
237+
layout->addWidget(m_ListOpenFiles);
238+
239+
widget->setLayout(layout);
212240
dock->setObjectName("FilesDock");
213-
dock->setWidget(m_ListOpenFiles);
241+
dock->setWidget(widget);
214242
return dock;
215243
}
216244

217245
QToolBar *MainWindow::buildMainToolBar()
218246
{
219-
auto toolbar = new QToolBar(this);
247+
auto toolbar = new QToolBar(tr("Sidebar"), this);
220248
toolbar->addAction(QIcon(":/icons/icons8/icons8-android-os-48.png"), tr("Open APK"), this, &MainWindow::handleActionApk);
221249
toolbar->addAction(QIcon(":/icons/icons8/icons8-folder-48.png"), tr("Open folder"), this, &MainWindow::handleActionFolder);
222250
toolbar->addSeparator();
@@ -313,6 +341,17 @@ QMenuBar *MainWindow::buildMenuBar()
313341
QDockWidget *MainWindow::buildProjectsDock()
314342
{
315343
auto dock = new QDockWidget(tr("Projects"), this);
344+
auto widget = new QWidget(this);
345+
auto layout = new QVBoxLayout(widget);
346+
layout->setContentsMargins(0, 0, 0, 0);
347+
layout->setSpacing(2);
348+
349+
m_SearchProjects = new QLineEdit(this);
350+
m_SearchProjects->setPlaceholderText(tr("Search in project..."));
351+
m_SearchProjects->setClearButtonEnabled(true);
352+
connect(m_SearchProjects, &QLineEdit::textChanged, this, &MainWindow::handleProjectsSearchChanged);
353+
layout->addWidget(m_SearchProjects);
354+
316355
m_ProjectsTree = new QTreeWidget(this);
317356
m_ProjectsTree->header()->hide();
318357
m_ProjectsTree->setContextMenuPolicy(Qt::CustomContextMenu);
@@ -324,8 +363,11 @@ QDockWidget *MainWindow::buildProjectsDock()
324363
connect(m_ProjectsTree, &QTreeWidget::customContextMenuRequested, this, &MainWindow::handleTreeContextMenu);
325364
connect(m_ProjectsTree, &QTreeWidget::doubleClicked, this, &MainWindow::handleTreeDoubleClicked);
326365
connect(m_ProjectsTree->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::handleTreeSelectionChanged);
366+
layout->addWidget(m_ProjectsTree);
367+
368+
widget->setLayout(layout);
327369
dock->setObjectName("ProjectsDock");
328-
dock->setWidget(m_ProjectsTree);
370+
dock->setWidget(widget);
329371
return dock;
330372
}
331373

@@ -834,10 +876,86 @@ void MainWindow::handleDecompileProgress(const int percent, const QString &messa
834876
void MainWindow::handleFilesSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
835877
{
836878
Q_UNUSED(deselected)
837-
if (!selected.isEmpty()) {
838-
auto index = selected.indexes().first();
839-
const QString path = index.data(Qt::UserRole + 1).toString();
840-
m_TabEditors->setCurrentIndex(findTabIndex(path));
879+
if (selected.isEmpty() || !m_FilesProxyModel || !m_ModelOpenFiles) {
880+
return;
881+
}
882+
883+
auto proxyIndex = selected.indexes().first();
884+
if (!proxyIndex.isValid()) {
885+
return;
886+
}
887+
888+
// Verify the index belongs to the proxy model - this is critical to prevent the assertion
889+
const QAbstractItemModel *indexModel = proxyIndex.model();
890+
#ifdef QT_DEBUG
891+
if (indexModel != m_FilesProxyModel) {
892+
qDebug() << "Warning: Selection index from wrong model. Expected:" << m_FilesProxyModel << "Got:" << indexModel;
893+
return;
894+
}
895+
#else
896+
if (!indexModel || indexModel != m_FilesProxyModel) {
897+
return;
898+
}
899+
#endif
900+
901+
// Additional safety check: verify the proxy model is still valid
902+
if (!m_FilesProxyModel->sourceModel() || m_FilesProxyModel->sourceModel() != m_ModelOpenFiles) {
903+
#ifdef QT_DEBUG
904+
qDebug() << "Warning: Proxy model source mismatch";
905+
#endif
906+
return;
907+
}
908+
909+
auto sourceIndex = m_FilesProxyModel->mapToSource(proxyIndex);
910+
if (sourceIndex.isValid() && sourceIndex.model() == m_ModelOpenFiles) {
911+
const QString path = m_ModelOpenFiles->data(sourceIndex, Qt::UserRole + 1).toString();
912+
if (!path.isEmpty()) {
913+
m_TabEditors->setCurrentIndex(findTabIndex(path));
914+
}
915+
}
916+
}
917+
918+
void MainWindow::handleFilesSearchChanged(const QString &text)
919+
{
920+
m_FilesProxyModel->setFilterFixedString(text);
921+
}
922+
923+
void MainWindow::handleProjectsSearchChanged(const QString &text)
924+
{
925+
for (int i = 0; i < m_ProjectsTree->topLevelItemCount(); ++i) {
926+
filterProjectTreeItems(m_ProjectsTree->topLevelItem(i), text);
927+
}
928+
}
929+
930+
void MainWindow::filterProjectTreeItems(QTreeWidgetItem *item, const QString &filter)
931+
{
932+
if (!item) {
933+
return;
934+
}
935+
936+
bool visible = false;
937+
QString itemText = item->text(0);
938+
939+
// Check if this item matches the filter
940+
if (filter.isEmpty() || itemText.contains(filter, Qt::CaseInsensitive)) {
941+
visible = true;
942+
}
943+
944+
// Check children recursively
945+
for (int i = 0; i < item->childCount(); ++i) {
946+
QTreeWidgetItem *child = item->child(i);
947+
filterProjectTreeItems(child, filter);
948+
// If any child is visible, this item should be visible too
949+
if (!child->isHidden()) {
950+
visible = true;
951+
}
952+
}
953+
954+
item->setHidden(!visible);
955+
956+
// Expand parent if this item is visible
957+
if (visible && item->parent()) {
958+
item->parent()->setExpanded(true);
841959
}
842960
}
843961

@@ -949,12 +1067,23 @@ void MainWindow::handleTabChanged(const int index)
9491067
} else if (viewer) {
9501068
path = viewer->filePath();
9511069
}
952-
const int total = m_ModelOpenFiles->rowCount();
953-
for (int i = 0; i < total; ++i) {
954-
const QModelIndex &mindex = m_ModelOpenFiles->index(i, 0);
955-
if (QString::compare(mindex.data(Qt::UserRole + 1).toString(), path) == 0) {
956-
m_ListOpenFiles->setCurrentIndex(mindex);
957-
break;
1070+
// Block signals to prevent selection change handler from firing
1071+
QSignalBlocker blocker(m_ListOpenFiles);
1072+
if (m_ListOpenFiles->selectionModel()) {
1073+
QSignalBlocker selectionBlocker(m_ListOpenFiles->selectionModel());
1074+
const int total = m_ModelOpenFiles->rowCount();
1075+
for (int i = 0; i < total; ++i) {
1076+
const QModelIndex &sourceIndex = m_ModelOpenFiles->index(i, 0);
1077+
if (QString::compare(sourceIndex.data(Qt::UserRole + 1).toString(), path) == 0) {
1078+
// Map source index to proxy index before setting selection
1079+
if (m_FilesProxyModel) {
1080+
QModelIndex proxyIndex = m_FilesProxyModel->mapFromSource(sourceIndex);
1081+
if (proxyIndex.isValid()) {
1082+
m_ListOpenFiles->setCurrentIndex(proxyIndex);
1083+
}
1084+
}
1085+
break;
1086+
}
9581087
}
9591088
}
9601089
m_ActionClose->setEnabled(index >= 0);
@@ -1009,12 +1138,26 @@ void MainWindow::handleTabCloseRequested(const int index)
10091138
} else if (viewer) {
10101139
path = viewer->filePath();
10111140
}
1012-
const int total = m_ModelOpenFiles->rowCount();
1013-
for (int i = 0; i < total; ++i) {
1014-
const QModelIndex &mindex = m_ModelOpenFiles->index(i, 0);
1015-
if (QString::compare(mindex.data(Qt::UserRole + 1).toString(), path) == 0) {
1016-
m_ModelOpenFiles->removeRow(mindex.row());
1017-
break;
1141+
// Block signals during model update to prevent selection change errors
1142+
QSignalBlocker blocker(m_ListOpenFiles);
1143+
if (m_ListOpenFiles->selectionModel()) {
1144+
QSignalBlocker selectionBlocker(m_ListOpenFiles->selectionModel());
1145+
const int total = m_ModelOpenFiles->rowCount();
1146+
for (int i = 0; i < total; ++i) {
1147+
const QModelIndex &mindex = m_ModelOpenFiles->index(i, 0);
1148+
if (QString::compare(mindex.data(Qt::UserRole + 1).toString(), path) == 0) {
1149+
m_ModelOpenFiles->removeRow(mindex.row());
1150+
break;
1151+
}
1152+
}
1153+
} else {
1154+
const int total = m_ModelOpenFiles->rowCount();
1155+
for (int i = 0; i < total; ++i) {
1156+
const QModelIndex &mindex = m_ModelOpenFiles->index(i, 0);
1157+
if (QString::compare(mindex.data(Qt::UserRole + 1).toString(), path) == 0) {
1158+
m_ModelOpenFiles->removeRow(mindex.row());
1159+
break;
1160+
}
10181161
}
10191162
}
10201163
m_ActionUndo->setEnabled(false);
@@ -1025,6 +1168,13 @@ void MainWindow::handleTabCloseRequested(const int index)
10251168
m_TabEditors->removeTab(index);
10261169
if (m_TabEditors->count() == 0) {
10271170
m_CentralStack->setCurrentIndex(0);
1171+
// Clear search boxes when all tabs are closed
1172+
if (m_SearchFiles) {
1173+
m_SearchFiles->clear();
1174+
}
1175+
if (m_SearchProjects) {
1176+
m_SearchProjects->clear();
1177+
}
10281178
}
10291179
}
10301180

@@ -1168,7 +1318,15 @@ void MainWindow::openFile(const QString &path)
11681318
const QIcon icon = m_FileIconProvider.icon(info);
11691319
auto item = new QStandardItem(icon, info.fileName());
11701320
item->setData(path, Qt::UserRole + 1);
1171-
m_ModelOpenFiles->appendRow(item);
1321+
1322+
// Block signals during model update to prevent selection change errors
1323+
QSignalBlocker blocker(m_ListOpenFiles);
1324+
if (m_ListOpenFiles->selectionModel()) {
1325+
QSignalBlocker selectionBlocker(m_ListOpenFiles->selectionModel());
1326+
m_ModelOpenFiles->appendRow(item);
1327+
} else {
1328+
m_ModelOpenFiles->appendRow(item);
1329+
}
11721330
const int i = m_TabEditors->addTab(widget, icon, info.fileName());
11731331
m_TabEditors->setCurrentIndex(i);
11741332
m_TabEditors->setTabToolTip(i, path);
@@ -1194,6 +1352,14 @@ void MainWindow::openFindReplaceDialog(QPlainTextEdit *edit, const bool replace)
11941352

11951353
void MainWindow::openProject(const QString &folder, const bool last)
11961354
{
1355+
// Clear search boxes when opening a new project
1356+
if (m_SearchFiles) {
1357+
m_SearchFiles->clear();
1358+
}
1359+
if (m_SearchProjects) {
1360+
m_SearchProjects->clear();
1361+
}
1362+
11971363
QSettings settings;
11981364
settings.setValue("open_project", folder);
11991365
settings.sync();

sources/mainwindow.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
#include <QMap>
1010
#include <QProgressDialog>
1111
#include <QStackedWidget>
12+
#include <QLineEdit>
13+
#include <QSortFilterProxyModel>
1214
#include <QStandardItemModel>
1315
#include <QTextEdit>
1416
#include <QTreeWidget>
17+
#include <QVBoxLayout>
1518
#include "findreplacedialog.h"
1619
#include "processutils.h"
1720

@@ -58,8 +61,11 @@ class MainWindow : public QMainWindow
5861
QList<QMetaObject::Connection> m_EditorConnections;
5962
QFileIconProvider m_FileIconProvider;
6063
FindReplaceDialog *m_FindReplaceDialog;
64+
QLineEdit *m_SearchFiles;
65+
QLineEdit *m_SearchProjects;
6166
QListView *m_ListOpenFiles;
6267
QStandardItemModel *m_ModelOpenFiles;
68+
QSortFilterProxyModel *m_FilesProxyModel;
6369
QProgressDialog *m_ProgressDialog;
6470
QTreeWidget *m_ProjectsTree;
6571
QLabel *m_StatusCursor;
@@ -116,13 +122,16 @@ private slots:
116122
void handleSignFinished(const QString &apk);
117123
void handleTabChanged(const int index);
118124
void handleTabCloseRequested(const int index);
125+
void handleFilesSearchChanged(const QString &text);
126+
void handleProjectsSearchChanged(const QString &text);
119127
void handleTreeContextMenu(const QPoint &point);
120128
void handleTreeDoubleClicked(const QModelIndex &index);
121129
void handleTreeSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected);
122130
void openFile(const QString &file);
123131
void openFindReplaceDialog(QPlainTextEdit *edit, const bool replace);
124132
void openProject(const QString &folder, const bool last = false);
125133
void reloadChildren(QTreeWidgetItem *item);
134+
void filterProjectTreeItems(QTreeWidgetItem *item, const QString &filter);
126135
private:
127136
bool saveTab(int index);
128137
};

0 commit comments

Comments
 (0)