Skip to content

Commit 8692e48

Browse files
feat: display progress bar when extracting multiple files from a vpk, add better way to toggle user action triggerability
1 parent 25cd382 commit 8692e48

File tree

3 files changed

+125
-37
lines changed

3 files changed

+125
-37
lines changed

CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
cmake_minimum_required(VERSION 3.25 FATAL_ERROR)
22
project(vpkedit
33
DESCRIPTION "A tool to read, preview, and write to VPK files."
4-
VERSION 3.3.1
4+
VERSION 3.3.2
55
HOMEPAGE_URL "https://github.com/craftablescience/VPKEdit")
66
set(PROJECT_NAME_PRETTY "VPKEdit" CACHE STRING "" FORCE)
77
set(PROJECT_HOMEPAGE_URL_API "https://api.github.com/repos/craftablescience/VPKEdit" CACHE STRING "" FORCE)

src/gui/Window.cpp

+98-36
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <QStatusBar>
2626
#include <QStyle>
2727
#include <QStyleFactory>
28+
#include <QThread>
2829

2930
#include <sapp/FilesystemSearchProvider.h>
3031

@@ -42,24 +43,25 @@ constexpr auto VPK_SAVE_FILTER = "Valve PacK (*.vpk);;All files (*.*)";
4243

4344
Window::Window(QSettings& options, QWidget* parent)
4445
: QMainWindow(parent)
46+
, extractWorkerThread(nullptr)
4547
, modified(false) {
4648
this->setWindowIcon(QIcon(":/icon.png"));
4749
this->setMinimumSize(900, 500);
4850

4951
// File menu
5052
auto* fileMenu = this->menuBar()->addMenu(tr("&File"));
51-
fileMenu->addAction(this->style()->standardIcon(QStyle::SP_FileIcon), tr("&Create Empty..."), Qt::CTRL | Qt::Key_N, [=] {
53+
this->createEmptyVPKAction = fileMenu->addAction(this->style()->standardIcon(QStyle::SP_FileIcon), tr("&Create Empty..."), Qt::CTRL | Qt::Key_N, [=] {
5254
this->newVPK(false);
5355
});
54-
fileMenu->addAction(this->style()->standardIcon(QStyle::SP_FileIcon), tr("Create From &Folder..."), Qt::CTRL | Qt::Key_N, [=] {
56+
this->createVPKFromDirAction = fileMenu->addAction(this->style()->standardIcon(QStyle::SP_FileIcon), tr("Create From &Folder..."), Qt::CTRL | Qt::Key_N, [=] {
5557
this->newVPK(true);
5658
});
57-
fileMenu->addAction(this->style()->standardIcon(QStyle::SP_DirIcon), tr("&Open..."), Qt::CTRL | Qt::Key_O, [=] {
59+
this->openVPKAction = fileMenu->addAction(this->style()->standardIcon(QStyle::SP_DirIcon), tr("&Open..."), Qt::CTRL | Qt::Key_O, [=] {
5860
this->openVPK();
5961
});
6062

6163
if (CFileSystemSearchProvider provider; provider.Available()) {
62-
auto* openRelativeToMenu = fileMenu->addMenu(this->style()->standardIcon(QStyle::SP_DirLinkIcon), tr("Open &In..."));
64+
this->openVPKRelativeToMenu = fileMenu->addMenu(this->style()->standardIcon(QStyle::SP_DirLinkIcon), tr("Open &In..."));
6365

6466
QList<std::tuple<QString, QString, QDir>> sourceGames;
6567
auto installedSteamAppCount = provider.GetNumInstalledApps();
@@ -83,10 +85,12 @@ Window::Window(QSettings& options, QWidget* parent)
8385

8486
for (const auto& [gameName, iconPath, relativeDirectoryPath] : sourceGames) {
8587
const auto relativeDirectory = relativeDirectoryPath.path();
86-
openRelativeToMenu->addAction(QIcon(iconPath), gameName, [=] {
88+
this->openVPKRelativeToMenu->addAction(QIcon(iconPath), gameName, [=] {
8789
this->openVPK(relativeDirectory);
8890
});
8991
}
92+
} else {
93+
this->openVPKRelativeToMenu = nullptr;
9094
}
9195

9296
this->saveVPKAction = fileMenu->addAction(this->style()->standardIcon(QStyle::SP_DialogSaveButton), tr("&Save"), Qt::CTRL | Qt::Key_S, [=] {
@@ -410,19 +414,19 @@ void Window::aboutQt() {
410414
}
411415

412416
std::optional<std::vector<std::byte>> Window::readBinaryEntry(const QString& path) {
413-
auto entry = (*this->vpk).findEntry(path.toStdString());
417+
auto entry = this->vpk->findEntry(path.toStdString());
414418
if (!entry) {
415419
return std::nullopt;
416420
}
417-
return (*this->vpk).readBinaryEntry(*entry);
421+
return this->vpk->readBinaryEntry(*entry);
418422
}
419423

420424
std::optional<QString> Window::readTextEntry(const QString& path) {
421-
auto entry = (*this->vpk).findEntry(path.toStdString());
425+
auto entry = this->vpk->findEntry(path.toStdString());
422426
if (!entry) {
423427
return std::nullopt;
424428
}
425-
auto textData = (*this->vpk).readTextEntry(*entry);
429+
auto textData = this->vpk->readTextEntry(*entry);
426430
if (!textData) {
427431
return std::nullopt;
428432
}
@@ -442,7 +446,7 @@ void Window::selectSubItemInDir(const QString& name) {
442446
}
443447

444448
void Window::extractFile(const QString& path, QString savePath) {
445-
auto entry = (*this->vpk).findEntry(path.toStdString());
449+
auto entry = this->vpk->findEntry(path.toStdString());
446450
if (!entry) {
447451
QMessageBox::critical(this, tr("Error"), "Failed to find file in VPK.");
448452
return;
@@ -466,23 +470,48 @@ void Window::extractFile(const QString& path, QString savePath) {
466470
}
467471

468472
void Window::extractFilesIf(const QString& saveDir, const std::function<bool(const QString&)>& predicate) {
469-
for (const auto& [directory, entries] : (*this->vpk).getEntries()) {
470-
QString dir(directory.c_str());
471-
if (!predicate(dir)) {
473+
// Set up progress bar
474+
this->statusText->hide();
475+
this->statusProgressBar->show();
476+
477+
// Get progress bar maximum
478+
int progressBarMax = 0;
479+
for (const auto& [directory, entries] : this->vpk->getEntries()) {
480+
if (!predicate(QString(directory.c_str()))) {
472481
continue;
473482
}
483+
progressBarMax += static_cast<int>(entries.size());
484+
}
474485

475-
QDir qDir;
476-
if (!qDir.mkpath(saveDir + '/' + dir)) {
477-
QMessageBox::critical(this, tr("Error"), "Failed to create directory.");
478-
return;
479-
}
486+
this->statusProgressBar->setMinimum(0);
487+
this->statusProgressBar->setMaximum(progressBarMax);
488+
this->statusProgressBar->setValue(0);
480489

481-
for (const auto& entry : entries) {
482-
auto filePath = saveDir + '/' + dir + '/' + entry.filename.c_str();
483-
this->writeEntryToFile(filePath, entry);
484-
}
485-
}
490+
this->freezeActions(true);
491+
492+
// Set up thread
493+
this->extractWorkerThread = new QThread(this);
494+
auto* worker = new ExtractVPKWorker();
495+
worker->moveToThread(this->extractWorkerThread);
496+
QObject::connect(this->extractWorkerThread, &QThread::started, worker, [=] {
497+
worker->run(this, saveDir, predicate);
498+
});
499+
QObject::connect(worker, &ExtractVPKWorker::progressUpdated, this, [=] {
500+
this->statusProgressBar->setValue(this->statusProgressBar->value() + 1);
501+
});
502+
QObject::connect(worker, &ExtractVPKWorker::taskFinished, this, [=] {
503+
// Kill thread
504+
this->extractWorkerThread->quit();
505+
this->extractWorkerThread->wait();
506+
delete this->extractWorkerThread;
507+
this->extractWorkerThread = nullptr;
508+
509+
this->freezeActions(false);
510+
511+
this->statusText->show();
512+
this->statusProgressBar->hide();
513+
});
514+
this->extractWorkerThread->start();
486515
}
487516

488517
void Window::extractDir(const QString& path, QString saveDir) {
@@ -503,7 +532,7 @@ void Window::extractAll(QString saveDir) {
503532
return;
504533
}
505534
saveDir += '/';
506-
saveDir += (*this->vpk).getPrettyFileName();
535+
saveDir += this->vpk->getRealFileName();
507536

508537
this->extractFilesIf(saveDir, [](const QString&) { return true; });
509538
}
@@ -547,15 +576,12 @@ void Window::clearContents() {
547576
this->searchBar->setDisabled(true);
548577

549578
this->entryTree->clearContents();
579+
this->entryTree->setDisabled(true);
550580

551581
this->fileViewer->clearContents();
552582

553-
this->saveAsVPKAction->setDisabled(true);
554-
this->closeFileAction->setDisabled(true);
555-
this->addFileAction->setDisabled(true);
556-
this->extractAllAction->setDisabled(true);
557-
558583
this->markModified(false);
584+
this->freezeActions(true, false); // Leave create/open unfrozen
559585
}
560586

561587
void Window::closeEvent(QCloseEvent* event) {
@@ -566,11 +592,29 @@ void Window::closeEvent(QCloseEvent* event) {
566592
event->accept();
567593
}
568594

595+
void Window::freezeActions(bool freeze, bool freezeCreationActions) {
596+
this->createEmptyVPKAction->setDisabled(freeze && freezeCreationActions);
597+
this->createVPKFromDirAction->setDisabled(freeze && freezeCreationActions);
598+
this->openVPKAction->setDisabled(freeze && freezeCreationActions);
599+
if (this->openVPKRelativeToMenu) this->openVPKRelativeToMenu->setDisabled(freeze && freezeCreationActions);
600+
this->saveVPKAction->setDisabled(freeze || !this->modified);
601+
this->saveAsVPKAction->setDisabled(freeze);
602+
this->closeFileAction->setDisabled(freeze);
603+
this->addFileAction->setDisabled(freeze);
604+
this->extractAllAction->setDisabled(freeze);
605+
606+
this->searchBar->setDisabled(freeze);
607+
this->entryTree->setDisabled(freeze);
608+
this->fileViewer->setDisabled(freeze);
609+
}
610+
569611
bool Window::loadVPK(const QString& path) {
570612
QString fixedPath(path);
571613
fixedPath.replace('\\', '/');
572614

573615
this->clearContents();
616+
this->freezeActions(true);
617+
574618
this->vpk = VPK::open(fixedPath.toStdString());
575619
if (!this->vpk) {
576620
QMessageBox::critical(this, tr("Error"), "Unable to load given VPK. Please ensure you are loading a "
@@ -584,13 +628,8 @@ bool Window::loadVPK(const QString& path) {
584628
this->statusText->hide();
585629
this->statusProgressBar->show();
586630

587-
this->searchBar->setDisabled(false);
588-
589631
this->entryTree->loadVPK(this->vpk.value(), this->statusProgressBar, [=] {
590-
this->saveAsVPKAction->setDisabled(false);
591-
this->closeFileAction->setDisabled(false);
592-
this->addFileAction->setDisabled(false);
593-
this->extractAllAction->setDisabled(false);
632+
this->freezeActions(false);
594633

595634
this->statusText->setText(' ' + QString("Loaded \"") + path + '\"');
596635
this->statusText->show();
@@ -601,7 +640,7 @@ bool Window::loadVPK(const QString& path) {
601640
}
602641

603642
void Window::writeEntryToFile(const QString& path, const VPKEntry& entry) {
604-
auto data = (*this->vpk).readBinaryEntry(entry);
643+
auto data = this->vpk->readBinaryEntry(entry);
605644
if (!data) {
606645
QMessageBox::critical(this, tr("Error"), QString("Failed to read data from the VPK for \"") + entry.filename.c_str() + "\". Please ensure that a game or another application is not using the VPK.");
607646
return;
@@ -617,3 +656,26 @@ void Window::writeEntryToFile(const QString& path, const VPKEntry& entry) {
617656
}
618657
file.close();
619658
}
659+
660+
void ExtractVPKWorker::run(Window* window, const QString& saveDir, const std::function<bool(const QString&)>& predicate) {
661+
int currentEntry = 0;
662+
for (const auto& [directory, entries] : window->vpk->getEntries()) {
663+
QString dir(directory.c_str());
664+
if (!predicate(dir)) {
665+
continue;
666+
}
667+
668+
QDir qDir;
669+
if (!qDir.mkpath(saveDir + '/' + dir)) {
670+
QMessageBox::critical(window, tr("Error"), "Failed to create directory.");
671+
return;
672+
}
673+
674+
for (const auto& entry : entries) {
675+
auto filePath = saveDir + '/' + dir + '/' + entry.filename.c_str();
676+
window->writeEntryToFile(filePath, entry);
677+
emit progressUpdated(++currentEntry);
678+
}
679+
}
680+
emit taskFinished();
681+
}

src/gui/Window.h

+26
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,24 @@
66
#include <QMainWindow>
77
#include <vpkedit/VPK.h>
88

9+
class QAction;
910
class QLabel;
1011
class QLineEdit;
12+
class QMenu;
1113
class QNetworkAccessManager;
1214
class QNetworkReply;
1315
class QProgressBar;
1416
class QSettings;
17+
class QThread;
1518

1619
class EntryTree;
1720
class FileViewer;
1821

1922
class Window : public QMainWindow {
2023
Q_OBJECT;
2124

25+
friend class ExtractVPKWorker;
26+
2227
public:
2328
explicit Window(QSettings& options, QWidget* parent = nullptr);
2429

@@ -76,6 +81,10 @@ class Window : public QMainWindow {
7681
EntryTree* entryTree;
7782
FileViewer* fileViewer;
7883

84+
QAction* createEmptyVPKAction;
85+
QAction* createVPKFromDirAction;
86+
QAction* openVPKAction;
87+
QMenu* openVPKRelativeToMenu;
7988
QAction* saveVPKAction;
8089
QAction* saveAsVPKAction;
8190
QAction* closeFileAction;
@@ -84,12 +93,29 @@ class Window : public QMainWindow {
8493

8594
QNetworkAccessManager* checkForUpdatesNetworkManager;
8695

96+
QThread* extractWorkerThread;
97+
8798
std::optional<vpkedit::VPK> vpk;
8899
bool modified;
89100

101+
void freezeActions(bool freeze, bool freezeCreationActions = true);
102+
90103
bool loadVPK(const QString& path);
91104

92105
void checkForUpdatesReply(QNetworkReply* reply);
93106

94107
void writeEntryToFile(const QString& path, const vpkedit::VPKEntry& entry);
95108
};
109+
110+
class ExtractVPKWorker : public QObject {
111+
Q_OBJECT;
112+
113+
public:
114+
ExtractVPKWorker() = default;
115+
116+
void run(Window* window, const QString& saveDir, const std::function<bool(const QString&)>& predicate);
117+
118+
signals:
119+
void progressUpdated(int value);
120+
void taskFinished();
121+
};

0 commit comments

Comments
 (0)