diff --git a/.gitignore b/.gitignore index 4e942b0..5b16403 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Build build/ +build-debug/ build-doc/ vcpkg/ vcpkg_installed/ diff --git a/README.md b/README.md index 9ab56ed..64b63c3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Tools subproject of something referred to as Arclight - Linux: The latest LTS of Ubuntu, roughly - macOS: -- Windows: Windows 10 and above, Visual Studio 2019 and above +- Windows: Windows 10 and above, Visual Studio 2019 seems to be the latest Qt supported version ## erfherder @@ -25,9 +25,9 @@ erfherder is an editor for [BioWare's ERF File Format](docs/bioware_aurora_engin ## texi -A super mid and basic texture gallery viewer. ERF's only for now. There are probably issues on account of a lack of knowledge. Screenshot is using Bill Harper's great work [HD Clubs](https://neverwintervault.org/project/nwnee/prefab/item/hd-clubs). +A super mid and basic texture gallery viewer. It can open Key, Erf, and Zip containers. Screenshot is using NWN:EE main key file circa 36-2. -![texi](screenshots/texi-2024-04-04.gif) +![texi](screenshots/texi-2024-04-09.gif) ## Acknowledgements, Credits, & Inspirations diff --git a/external/rollnw b/external/rollnw index 3db8ae7..d115c57 160000 --- a/external/rollnw +++ b/external/rollnw @@ -1 +1 @@ -Subproject commit 3db8ae746803f101b702b26eb9f420b4d7c23e72 +Subproject commit d115c57a2c8673551b9c79aeb0923679fbe3a301 diff --git a/screenshots/texi-2024-04-09.gif b/screenshots/texi-2024-04-09.gif new file mode 100644 index 0000000..2751171 Binary files /dev/null and b/screenshots/texi-2024-04-09.gif differ diff --git a/src/texi/CMakeLists.txt b/src/texi/CMakeLists.txt index 9d42bb7..e490a04 100644 --- a/src/texi/CMakeLists.txt +++ b/src/texi/CMakeLists.txt @@ -5,6 +5,8 @@ set(SRC_FILES mainwindow.h mainwindow.cpp mainwindow.ui + texuregallerymodel.h + texuregallerymodel.cpp ${CMAKE_SOURCE_DIR}/external/ZFontIcon/ZFontIcon/Fonts.qrc ) @@ -34,6 +36,7 @@ endif() target_include_directories(texi PRIVATE ../ ${CMAKE_SOURCE_DIR}/external/rollnw/external + ${CMAKE_SOURCE_DIR}/external/rollnw/external/minizip/include ${CMAKE_SOURCE_DIR}/external/rollnw/lib ${CMAKE_SOURCE_DIR}/external/ZFontIcon ${CMAKE_SOURCE_DIR}/external/ @@ -43,6 +46,7 @@ target_link_libraries(texi PRIVATE arclight-widgets nw arclight-external + minizip Qt6::Widgets ) diff --git a/src/texi/mainwindow.cpp b/src/texi/mainwindow.cpp index 4106614..224e956 100644 --- a/src/texi/mainwindow.cpp +++ b/src/texi/mainwindow.cpp @@ -1,8 +1,7 @@ #include "mainwindow.h" #include "ui_mainwindow.h" -#include -#include +#include "texuregallerymodel.h" #include #include @@ -17,51 +16,17 @@ MainWindow::MainWindow(QWidget* parent) QObject::connect(ui->actionOpen, &QAction::triggered, this, &MainWindow::onActionOpen); } -void MainWindow::loadIcons() -{ - if (!container_) { - return; - } - - auto cb = [this](const nw::Resource& resource) { - if (resource.type == nw::ResourceType::plt) { - return; - } - if (nw::ResourceType::check_category(nw::ResourceType::texture, resource.type)) { - images_.emplace_back(container_->demand(resource)); - labels_.push_back(resource.filename()); - } - }; - - container_->visit(cb); - - for (size_t i = 0; i < images_.size(); ++i) { - if (!images_[i].valid()) { - continue; - } - QImage qi(images_[i].data(), images_[i].width(), images_[i].height(), - images_[i].channels() == 4 ? QImage::Format_RGBA8888 : QImage::Format_RGB888); - - ui->imageGallery->addItem(new QListWidgetItem(QIcon(QPixmap::fromImage(qi)), - QString::fromStdString(labels_[i]))); - } -} - void MainWindow::open(const QString& path) { if (!QFileInfo::exists(path)) { return; } - std::string p = path.toStdString(); - - ui->imageGallery->clear(); - container_ = std::make_unique(p); - loadIcons(); + ui->imageGallery->setModel(new TexureGalleryModel(path)); } void MainWindow::onActionOpen() { - QString fn = QFileDialog::getOpenFileName(this, "Open Erf", "", "Erf (*.erf *.mod *.hak *.nwm *.sav)"); + QString fn = QFileDialog::getOpenFileName(this, "Open Container", "", "Container (*.erf *.hak *.key *.zip)"); if (fn.isEmpty()) { return; } diff --git a/src/texi/mainwindow.h b/src/texi/mainwindow.h index 75cf3cc..abf83c1 100644 --- a/src/texi/mainwindow.h +++ b/src/texi/mainwindow.h @@ -9,11 +9,6 @@ #include -struct Payload { - nw::Image image; - QImage qimage; -}; - namespace Ui { class MainWindow; } @@ -25,7 +20,6 @@ class MainWindow : public QMainWindow { explicit MainWindow(QWidget* parent = nullptr); ~MainWindow(); - void loadIcons(); void open(const QString& path); public slots: @@ -33,9 +27,6 @@ public slots: private: Ui::MainWindow* ui; - std::unique_ptr container_; - std::vector images_; - std::vector labels_; }; #endif // MAINWINDOW_H diff --git a/src/texi/mainwindow.ui b/src/texi/mainwindow.ui index 9918e4d..ecf3e7a 100644 --- a/src/texi/mainwindow.ui +++ b/src/texi/mainwindow.ui @@ -16,7 +16,13 @@ - + + + true + + + QAbstractItemView::NoSelection + 256 @@ -26,15 +32,24 @@ QListView::Adjust + + QListView::SinglePass + + + 5 + QListView::IconMode true - + false + + Qt::AlignBottom|Qt::AlignHCenter + diff --git a/src/texi/texuregallerymodel.cpp b/src/texi/texuregallerymodel.cpp new file mode 100644 index 0000000..3bc2d9b --- /dev/null +++ b/src/texi/texuregallerymodel.cpp @@ -0,0 +1,105 @@ +#include "texuregallerymodel.h" + +#include "nw/formats/Image.hpp" +#include "nw/log.hpp" +#include "nw/resources/Directory.hpp" +#include "nw/resources/Erf.hpp" +#include "nw/resources/Key.hpp" +#include "nw/resources/Zip.hpp" +#include "nw/util/platform.hpp" + +#include +#include + +inline std::unique_ptr load_container(const std::filesystem::path& p) +{ + auto ext = nw::path_to_string(p.extension()); + + if (std::filesystem::is_directory(p)) { + return std::make_unique(p); + } else if (nw::string::icmp(ext, ".hak")) { + return std::make_unique(p); + } else if (nw::string::icmp(ext, ".erf")) { + return std::make_unique(p); + } else if (nw::string::icmp(ext, ".zip")) { + return std::make_unique(p); + } else if (nw::string::icmp(ext, ".key")) { + return std::make_unique(p); + } + return nullptr; +} + +TexureGalleryModel::TexureGalleryModel(const QString& path, QObject* parent) + : QAbstractListModel(parent) +{ + container_ = load_container(path.toStdString()); + + auto cb = [this](const nw::Resource& resource) { + if (resource.type == nw::ResourceType::plt) { + return; + } + if (nw::ResourceType::check_category(nw::ResourceType::texture, resource.type)) { + labels_.push_back(resource); + } + }; + + container_->visit(cb); + std::sort(std::begin(labels_), std::end(labels_)); +} + +QVariant TexureGalleryModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + return {}; +} + +int TexureGalleryModel::rowCount(const QModelIndex& parent) const +{ + return labels_.size(); +} + +int TexureGalleryModel::columnCount(const QModelIndex& parent) const +{ + return 1; +} + +QVariant TexureGalleryModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (role == Qt::DecorationRole) { + auto it = cache_.find(labels_[index.row()]); + if (it != std::end(cache_)) { + return it->second; + } else { + nw::Image img{container_->demand(labels_[index.row()])}; + if (!img.valid()) { + QVariant(); + } + + QImage qi(img.data(), img.width(), img.height(), + img.channels() == 4 ? QImage::Format_RGBA8888 : QImage::Format_RGB888); + if (qi.height() > 128 || qi.width() > 128) { + qi = qi.scaled(128, 128, Qt::KeepAspectRatio); + } + if (img.is_bio_dds()) { + qi.mirror(); + } + auto it2 = cache_.emplace(labels_[index.row()], QPixmap::fromImage(qi)); + return it2.first->second; + } + } else if (role == Qt::SizeHintRole) { + return QSize(128, 150); + } else if (role == Qt::DisplayRole) { + return QString::fromStdString(labels_[index.row()].filename()); + } else if (role == Qt::TextAlignmentRole) { + return (Qt::AlignBottom | Qt::AlignHCenter).toInt(); + } else if (role == Qt::ToolTipRole) { + auto it = cache_.find(labels_[index.row()]); + if (it == std::end(cache_)) { + return {}; + } + return QString::fromStdString(fmt::format("{}x{}", it->second.width(), it->second.height())); + } + return QVariant(); +} diff --git a/src/texi/texuregallerymodel.h b/src/texi/texuregallerymodel.h new file mode 100644 index 0000000..b7dd613 --- /dev/null +++ b/src/texi/texuregallerymodel.h @@ -0,0 +1,34 @@ +#ifndef TEXUREGALLERYMODEL_H +#define TEXUREGALLERYMODEL_H + +#include "nw/resources/Container.hpp" + +#include "absl/container/flat_hash_map.h" + +#include +#include + +#include + +class TexureGalleryModel : public QAbstractListModel { + Q_OBJECT + +public: + explicit TexureGalleryModel(const QString& path, QObject* parent = nullptr); + + // Header: + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + // Basic functionality: + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + +private: + std::unique_ptr container_; + std::vector labels_; + mutable absl::flat_hash_map cache_; +}; + +#endif // TEXUREGALLERYMODEL_H