~saiko/mcrestool

02049080b23ac2bc4e07a68f320b4282b519208a — 2xsaiko a month ago 5f3969d master
Loading directory tree somewhat works
M CMakeLists.txt => CMakeLists.txt +24 -24
@@ 22,45 22,45 @@ find_package(KF5 REQUIRED
find_package(QuaZip5 REQUIRED)

set(mcrestool_HEADERS
        src/identifier.h
        src/languagetable/languagetable.h
        src/model/languagetablemodel.h
        src/project/languagetablecontainer.h
        src/result.h
        src/table.h
        src/identifier.h
        src/model/resourcetree.h
        src/ui/mainwindow.h
        src/ui/languagetablewindow.h
        src/ui/geneditorwindow.h
        src/ui/itembutton.h
        src/ui/languagetablewindow.h
        src/ui/mainwindow.h
        src/ui/recipeeditextensionwidget.h
        src/ui/recipeeditwindow.h
        src/ui/shapedcraftingwidget.h
        src/ui/recipeeditextensionwidget.h
        src/util.h
        src/result.h
        src/ui/smeltingwidget.h
        src/model/treeitem.h
        src/project/languagetablecontainer.h
        src/ui/geneditorwindow.h
        src/workspace/workspace.h
        src/util.h
        src/workspace/direntry.h
        src/workspace/fsref.h
        src/languagetable/languagetable.h
        src/workspace/direntry.h)
        src/workspace/fstree.h
        src/workspace/fstreemodel.h
        src/workspace/workspace.h)

set(mcrestool_SRC
        src/identifier.cpp
        src/languagetable/languagetable.cpp
        src/main.cpp
        src/model/languagetablemodel.cpp
        src/identifier.cpp
        src/model/resourcetree.cpp
        src/ui/mainwindow.cpp
        src/ui/languagetablewindow.cpp
        src/project/languagetablecontainer.cpp
        src/ui/geneditorwindow.cpp
        src/ui/itembutton.cpp
        src/ui/languagetablewindow.cpp
        src/ui/mainwindow.cpp
        src/ui/recipeeditextensionwidget.cpp
        src/ui/recipeeditwindow.cpp
        src/ui/shapedcraftingwidget.cpp
        src/ui/recipeeditextensionwidget.cpp
        src/ui/smeltingwidget.cpp
        src/model/treeitem.cpp
        src/project/languagetablecontainer.cpp
        src/ui/geneditorwindow.cpp
        src/workspace/workspace.cpp
        src/workspace/fsref.cpp
        src/languagetable/languagetable.cpp)
        src/workspace/fstree.cpp
        src/workspace/fstreemodel.cpp
        src/workspace/workspace.cpp)

add_executable(mcrestool ${mcrestool_SRC})



@@ 72,4 72,4 @@ target_link_libraries(mcrestool
        KF5::Archive
        ${QUAZIP_LIBRARIES})

install(TARGETS mcrestool DESTINATION bin)
\ No newline at end of file
install(TARGETS mcrestool DESTINATION bin)

M src/languagetable/languagetable.cpp => src/languagetable/languagetable.cpp +32 -8
@@ 1,33 1,57 @@
#include "languagetable.h"

void LanguageTable::insert(QString language, QString key, QString value) {

    this->add_language(language);
    this->add_key(key);
    this->table[language][key] = value;
}

void LanguageTable::add_key(QString key) {

    if (!this->keys.contains(key)) {
        this->keys += key;
    }
}

void LanguageTable::add_language(QString language) {

    if (!this->languages.contains(language)) {
        this->languages += language;
    }
}

int LanguageTable::key_count() const {
    return 0;
    return this->keys.size();
}

int LanguageTable::language_count() const {
    return 0;
    return this->languages.size();
}

QString LanguageTable::get(const QString& language, const QString& key) const {
    return QString();
    return this->table[language][key];
}

QString LanguageTable::get_language_at(int index) const {
    return QString();
    return this->languages[index];
}

QString LanguageTable::get_key_at(int index) const {
    return QString();
    return this->keys[index];
}

QStringList LanguageTable::get_keys_for(const QString& language) const {
    return this->table[language].keys();
}

QMap<QString, QString> LanguageTable::get_entries_for(const QString& language) const {
    return this->table[language];
}

bool LanguageTable::contains_language(const QString& language) const {
    return this->table.contains(language);
}

void LanguageTable::clear() {
    this->table.clear();
    this->keys.clear();
    this->languages.clear();
}

M src/languagetable/languagetable.h => src/languagetable/languagetable.h +14 -0
@@ 2,6 2,7 @@
#define MCRESTOOL_LANGUAGETABLE_H

#include <QString>
#include <QMap>

class LanguageTable {



@@ 22,6 23,19 @@ public:

    QString get_key_at(int index) const;

    QStringList get_keys_for(const QString& language) const;

    QMap<QString, QString> get_entries_for(const QString& language) const;

    bool contains_language(const QString& language) const;

    void clear();

private:
    QStringList languages;
    QStringList keys;
    QMap<QString, QMap<QString, QString>> table;

};

#endif //MCRESTOOL_LANGUAGETABLE_H

D src/model/resourcetree.cpp => src/model/resourcetree.cpp +0 -117
@@ 1,117 0,0 @@
#include "resourcetree.h"

ResourceTree::ResourceTree(QObject* parent):
        QAbstractItemModel(parent) {
    root_item = new TreeItem(QString());

    {
        auto mcjar = new TreeItem("minecraft.jar", root_item);
        auto mcnamespace = new TreeItem("minecraft", mcjar);

        auto mcassets = new TreeItem("Assets", mcnamespace);

        mcassets->append_child(new TreeItem("Localization", mcassets));
        mcnamespace->append_child(mcassets);

        auto mcdata = new TreeItem("Data", mcnamespace);

        auto recipes = new TreeItem("Recipes", mcdata);
        recipes->append_child(new TreeItem("recipe_a", recipes));
        recipes->append_child(new TreeItem("recipe_b", recipes));
        recipes->append_child(new TreeItem("recipe_c", recipes));
        recipes->append_child(new TreeItem("recipe_d", recipes));
        mcdata->append_child(recipes);
        mcnamespace->append_child(mcdata);

        mcjar->append_child(mcnamespace);
        root_item->append_child(mcjar);
    }

    {
        auto mcjar = new TreeItem("resources", root_item);
        auto mcnamespace = new TreeItem("rswires", mcjar);

        auto mcassets = new TreeItem("Assets", mcnamespace);

        mcassets->append_child(new TreeItem("Localization", mcassets));
        mcnamespace->append_child(mcassets);

        auto mcdata = new TreeItem("Data", mcnamespace);

        auto recipes = new TreeItem("Recipes", mcdata);
        recipes->append_child(new TreeItem("bundled_cable", recipes));
        recipes->append_child(new TreeItem("red_alloy_wire", recipes));
        recipes->append_child(new TreeItem("white_insulated_wire", recipes));
        mcdata->append_child(recipes);
        mcnamespace->append_child(mcdata);

        mcjar->append_child(mcnamespace);
        root_item->append_child(mcjar);
    }

}

ResourceTree::~ResourceTree() {
    delete root_item;
}

QModelIndex ResourceTree::index(int row, int column, const QModelIndex& parent) const {
    if (!hasIndex(row, column, parent)) return QModelIndex();

    TreeItem* parent_item;

    if (!parent.isValid()) {
        parent_item = root_item;
    } else {
        parent_item = static_cast<TreeItem*>(parent.internalPointer());
    }

    TreeItem* child_item = parent_item->child(row);

    if (!child_item) return QModelIndex();

    return createIndex(row, column, child_item);
}

QModelIndex ResourceTree::parent(const QModelIndex& child) const {
    if (!child.isValid()) return QModelIndex();

    auto* item = static_cast<TreeItem*>(child.internalPointer());
    TreeItem* parent = item->parent_item();

    if (parent == root_item) return QModelIndex();

    return createIndex(parent->row(), 0, parent);
}

QVariant ResourceTree::data(const QModelIndex& index, int role) const {
    if (!index.isValid()) return QVariant();
    if (role != Qt::DisplayRole) return QVariant();

    auto* item = static_cast<TreeItem*>(index.internalPointer());

    return item->text();
}

Qt::ItemFlags ResourceTree::flags(const QModelIndex& index) const {
    if (!index.isValid()) return Qt::NoItemFlags;

    return QAbstractItemModel::flags(index);
}

QVariant ResourceTree::headerData(int, Qt::Orientation, int) const {
    return QVariant();
}

int ResourceTree::rowCount(const QModelIndex& parent) const {
    if (parent.column() > 0) return 0;

    if (!parent.isValid()) return root_item->child_count();

    auto* parent_item = static_cast<TreeItem*>(parent.internalPointer());
    return parent_item->child_count();
}

int ResourceTree::columnCount(const QModelIndex& parent) const {
    return 1;
}

D src/model/treeitem.cpp => src/model/treeitem.cpp +0 -34
@@ 1,34 0,0 @@
#include "treeitem.h"

TreeItem::TreeItem(const QString& data, TreeItem* parentItem) :
    _item_data(data), _parent_item(parentItem) {}

TreeItem::~TreeItem() {
    qDeleteAll(_child_items);
}

void TreeItem::append_child(TreeItem* child) {
    _child_items.append(child);
}

TreeItem* TreeItem::child(int row) {
    if (row < 0 || row >= _child_items.size()) return nullptr;
    return _child_items.at(row);
}

int TreeItem::child_count() const {
    return _child_items.size();
}

QString TreeItem::text() const {
    return _item_data;
}

int TreeItem::row() const {
    if (!_parent_item) return 0;
    return _parent_item->_child_items.indexOf(const_cast<TreeItem*>(this));
}

TreeItem* TreeItem::parent_item() {
    return _parent_item;
}

D src/model/treeitem.h => src/model/treeitem.h +0 -30
@@ 1,30 0,0 @@
#ifndef MCRESTOOL_TREEITEM_H
#define MCRESTOOL_TREEITEM_H

#include <QVariant>

class TreeItem {
public:
    explicit TreeItem(const QString& text, TreeItem* parentItem = nullptr);

    ~TreeItem();

    void append_child(TreeItem* child);

    TreeItem* child(int row);

    int child_count() const;

    QString text() const;

    int row() const;

    TreeItem* parent_item();

private:
    QVector<TreeItem*> _child_items;
    QString _item_data;
    TreeItem* _parent_item;
};

#endif //MCRESTOOL_TREEITEM_H

M src/project/languagetablecontainer.cpp => src/project/languagetablecontainer.cpp +91 -33
@@ 1,14 1,18 @@
#include "src/workspace/direntry.h"
#include "languagetablecontainer.h"

#include <QJsonDocument>
#include <QJsonObject>

LanguageTableContainer::LanguageTableContainer(
    FsRef fs_ref,
    QObject* parent
) : QObject(parent),
    fs_ref(fs_ref),
    lt(new LanguageTableModel(LanguageTable(), this)) {
    _persistent = false;
    _changed = false;
    _deleted = false;
    this->_persistent = false;
    this->_changed = false;
    this->_deleted = false;

    connect(lt, SIGNAL(changed(const QString&, const QString&, const QString&)), this, SLOT(on_changed()));
}


@@ 18,52 22,106 @@ LanguageTableContainer::~LanguageTableContainer() {
}

LanguageTableModel* LanguageTableContainer::language_table() {
    return lt;
    return this->lt;
}

bool LanguageTableContainer::persistent() const {
    return _persistent;
    return this->_persistent;
}

bool LanguageTableContainer::changed() const {
    return _changed;
    return this->_changed;
}

bool LanguageTableContainer::read_only() const {
    return fs_ref.read_only();
}

void LanguageTableContainer::delete_file() {
//    if (read_only()) return;
//
//    if (persistent()) {
//        QList<DirEntryW> files = src->data_source()->list_dir("/assets/" + domain + "/lang/");
//        for (const auto& entry: files) {
//            src->data_source()->delete_file(entry.name);
//        }
//    }
//    _deleted = true;
//    _persistent = false;
    return this->fs_ref.read_only();
}

void LanguageTableContainer::save() {
//    if (read_only()) return;
//
//    languagetable_write_to(lt->data(), src->data_source()->inner(), ("/assets/" + domain + "/lang/").toLocal8Bit());
//
//    _persistent = true;
//    _changed = false;
    if (read_only()) return;

    for (auto entry: this->fs_ref.read_dir()) {
        qDebug() << entry.file_name;
        if (entry.file_name.endsWith(".json") && entry.is_file) {
            QString lang = entry.file_name.left(entry.file_name.length() - 5);
            if (!this->lt->data().contains_language(lang)) {
                entry.real_path.remove(false);
            }
        }
    }

    for (int i = 0; i < this->lt->data().language_count(); i++) {
        QString lang = this->lt->data().get_language_at(i);
        QJsonObject obj;
        QMap<QString, QString> map = this->lt->data().get_entries_for(lang);
        if (!map.isEmpty()) {
            for (auto key: map.keys()) {
                QString value = map[key];
                if (!value.isEmpty()) {
                    obj.insert(key, value);
                }
            }
        }
        QJsonDocument d;
        d.setObject(obj);
        FsRef lang_file = this->fs_ref.join(lang + ".json");

        QSharedPointer<QIODevice> dev = lang_file.open();
        dev->open(QIODevice::ReadWrite | QIODevice::Truncate | QIODevice::Text);
        dev->write(d.toJson(QJsonDocument::Compact));
        dev->close();
    }

    _persistent = true;
    _changed = false;
}

void LanguageTableContainer::load() {
//    languagetable_load_into(lt->data(), src->data_source()->inner(), ("/assets/" + domain + "/lang/").toLocal8Bit());
//
//    _persistent = true;
//    _changed = false;
//    emit lt->layoutChanged();
    this->lt->data().clear();

    QList<DirEntry> list = this->fs_ref.read_dir();

    // move en_us to the beginning
    for (int i = 0; i < list.size(); i++) {
        DirEntry entry = list[i];
        if (entry.file_name.endsWith(".json") && entry.is_file) {
            QString lang = entry.file_name.left(entry.file_name.length() - 5);
            if (lang == "en_us") {
                list.removeAt(i);
                list.insert(0, entry);
                break;
            }
        }
    }

    for (auto entry: list) {
        if (entry.file_name.endsWith(".json") && entry.is_file) {
            QString lang = entry.file_name.left(entry.file_name.length() - 5);
            this->lt->data().add_language(lang);
            QSharedPointer<QIODevice> dev = entry.real_path.open();
            dev->open(QIODevice::ReadOnly | QIODevice::Text);
            QJsonParseError err;
            auto doc = QJsonDocument::fromJson(dev->readAll(), &err);

            // TODO actually show errors
            if (err.error != QJsonParseError::NoError) continue;
            if (!doc.isObject()) continue;

            QJsonObject object = doc.object();
            for (QString key: object.keys()) {
                QJsonValueRef value = object[key];
                if (!value.isString()) continue;
                this->lt->data().insert(lang, key, value.toString());
            }
        }
    }

    _persistent = true;
    _changed = false;
    emit lt->layoutChanged();
}

void LanguageTableContainer::on_changed() {
//    _changed = true;
//    emit changed();
    _changed = true;
    emit changed();
}

M src/project/languagetablecontainer.h => src/project/languagetablecontainer.h +0 -2
@@ 22,8 22,6 @@ public:

    bool read_only() const;

    void delete_file();

    void save();

    void load();

M src/ui/mainwindow.cpp => src/ui/mainwindow.cpp +12 -4
@@ 2,13 2,12 @@
#include "ui_mainwindow.h"
#include "languagetablewindow.h"
#include "recipeeditwindow.h"
#include "src/util.h"
#include <QApplication>
#include "src/workspace/fstreemodel.h"

#include <QDesktopWidget>
#include <QScreen>
#include <QFileDialog>
#include <QInputDialog>
#include <iostream>
#include <QDebug>

MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow), ws(new Workspace(this)) {


@@ 29,10 28,14 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi

    connect(ui->mdi_area, SIGNAL(subWindowActivated(QMdiSubWindow * )), this, SLOT(sub_window_focus_change(QMdiSubWindow * )));

    auto* ltw = new LanguageTableWindow(new LanguageTableContainer(FsRef("testres/assets/testmod/lang"), this), this);
    auto* ltw = new LanguageTableWindow(new LanguageTableContainer(FsRef("../testres/assets/testmod/lang"), this), this);
    ltw->reload();
    ui->mdi_area->addSubWindow(ltw);

    // auto* ltw1 = new LanguageTableWindow(new LanguageTableContainer(FsRef("../testres/assets/minecraft/lang"), this), this);
    // ltw1->reload();
    // ui->mdi_area->addSubWindow(ltw1);

    auto* crw = new RecipeEditWindow(this);
    ui->mdi_area->addSubWindow(crw);



@@ 42,6 45,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi
    connect(ui->res_tree_view, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(show_restree_context_menu(QPoint)));

    // ui->res_tree_view->setModel(new ResourceTree(this));
    ui->res_tree_view->setModel(new FsTreeModel(this->ws, this));
}

void MainWindow::center() {


@@ 94,10 98,14 @@ void MainWindow::show_game_objects(bool shown) {

void MainWindow::add_res_file() {
    QStringList sources = QFileDialog::getOpenFileNames(this, tr("Add Resource Pack/Mod"), QString(), "Minecraft Content(*.zip *.jar);;All Files(*.*)");
    for (auto source: sources) {
        this->ws->add_file(source);
    }
}

void MainWindow::add_res_dir() {
    QString source = QFileDialog::getExistingDirectory(this, tr("Add Resource Folder"));
    this->ws->add_dir(source);
}

void MainWindow::sub_window_focus_change(QMdiSubWindow* window) {

M src/util.h => src/util.h +2 -2
@@ 3,8 3,8 @@

#include <QtGlobal>

#define unimplemented() (qt_assert("unimplemented", __FILE__, __LINE__))
#define unimplemented() Q_UNIMPLEMENTED()

#define unreachable() (qt_assert("unreachable", __FILE__, __LINE__))
#define unreachable() Q_UNREACHABLE()

#endif //MCRESTOOL_UTIL_H

M src/workspace/direntry.h => src/workspace/direntry.h +1 -1
@@ 5,7 5,7 @@

#include <QString>

struct WSDirEntry {
struct DirEntry {
    bool is_file;
    bool is_dir;
    bool is_symlink;

M src/workspace/fsref.cpp => src/workspace/fsref.cpp +127 -46
@@ 4,42 4,42 @@

#include <QFileInfo>
#include <QDir>
#include <quazip5/quazip.h>
#include <QDebug>
#include <quazip5/quazipfile.h>

FsRef::FsRef(const QString& file_path) : type(FsRefType::NORMAL) {
    data.normal = NormalFsRef {
        .file_path = file_path
    };
FsRef::FsRef(const QString& file_path) : type(NORMAL), normal(new NormalFsRef) {
    this->normal->file_path = file_path;
}

FsRef::FsRef(const QString& zip_path, const QString& file_path) : type(FsRefType::ZIP) {
    data.zip = ZipFsRef {
        .zip_path = zip_path,
        .file_path = file_path,
    };
FsRef::FsRef(const QString& zip_path, const QString& file_path) : type(ZIP), zip(new ZipFsRef) {
    this->zip->zip_path = zip_path;
    this->zip->file_path = file_path;
    this->zip->qz = QSharedPointer<QuaZip>(new QuaZip(zip_path));
    this->zip->qz->open(QuaZip::mdUnzip);
}

FsRef::FsRef(const FsRef& that) : type(that.type) {
    switch(that.type) {
    switch (that.type) {
        case NORMAL:
            this->data.normal = that.data.normal;
            this->normal = new NormalFsRef;
            this->normal->file_path = that.normal->file_path;
            break;
        case ZIP:
            this->data.zip = that.data.zip;
            this->zip = new ZipFsRef;
            this->zip->zip_path = that.zip->zip_path;
            this->zip->qz = that.zip->qz;
            this->zip->file_path = that.zip->file_path;
            break;
        default:
            unreachable();
    }
}

FsRef::~FsRef() {
    switch (this->type) {
        case NORMAL:
            delete this->data.normal;
            delete this->normal;
            break;
        case ZIP:
            delete this->data.zip;
            delete this->zip;
            break;
        default:
            unreachable();


@@ 49,9 49,9 @@ FsRef::~FsRef() {
bool FsRef::read_only() const {
    switch (this->type) {
        case NORMAL:
            return !QFileInfo(this->data.normal.file_path).isWritable();
            return !QFileInfo(this->normal->file_path).isWritable();
        case ZIP:
            return !QFileInfo(this->data.zip.zip_path).isWritable();
            return !QFileInfo(this->zip->zip_path).isWritable();
        default:
            unreachable();
    }


@@ 60,9 60,9 @@ bool FsRef::read_only() const {
bool FsRef::is_file() const {
    switch (this->type) {
        case NORMAL:
            return QFileInfo(this->data.normal.file_path).isFile();
            return QFileInfo(this->normal->file_path).isFile();
        case ZIP:
            return QuaZip(this->data.zip.zip_path).getFileNameList().contains(this->data.zip.file_path);
            return QuaZip(this->zip->zip_path).getFileNameList().contains(this->zip->file_path);
        default:
            unreachable();
    }


@@ 71,10 71,9 @@ bool FsRef::is_file() const {
bool FsRef::is_dir() const {
    switch (this->type) {
        case NORMAL:
            return QFileInfo(this->data.normal.file_path).isDir();
            return QFileInfo(this->normal->file_path).isDir();
        case ZIP:
            // return !this->is_file();
            return !this->read_dir().isEmpty();
            return !this->is_file();
        default:
            unreachable();
    }


@@ 83,7 82,7 @@ bool FsRef::is_dir() const {
bool FsRef::is_link() const {
    switch (this->type) {
        case NORMAL:
            return !QFileInfo(this->data.normal.file_path).isSymbolicLink();
            return !QFileInfo(this->normal->file_path).isSymbolicLink();
        case ZIP:
            return false; // ain't no links in zip files
        default:


@@ 91,27 90,65 @@ bool FsRef::is_link() const {
    }
}

QIODevice* FsRef::open() const {
QString FsRef::file_name() const {
    // TODO cache
    QStringRef file_name;
    int from = 0;
    int idx;

    QString file_path;

    switch (this->type) {
        case NORMAL:
            return new QFile(this->data.normal.file_path);
            file_path = this->normal->file_path;
            break;
        case ZIP:
            return new QuaZipFile(this->data.zip.zip_path, this->data.zip.file_path);
            file_path = this->zip->file_path;
            break;
        default:
            unreachable();
    }

    while (from < file_path.length()) {
        idx = file_path.indexOf('/', from);
        if (idx == -1) break;
        int next = file_path.indexOf('/', idx + 1);
        if (next == -1) next = file_path.length();
        if (next - idx - 1 >= 1) {
            file_name = QStringRef(&file_path, idx + 1, next - idx - 1);
        }
        from = next;
    }

    if (file_name.isNull()) return "<???>";

    return file_name.toString();
}

QList<WSDirEntry> FsRef::read_dir() const {
QSharedPointer<QIODevice> FsRef::open() const {
    switch (this->type) {
        case NORMAL:
            return QSharedPointer<QIODevice>(new QFile(this->normal->file_path));
        case ZIP:
            return QSharedPointer<QIODevice>(new QuaZipFile(this->zip->zip_path, this->zip->zip_path));
        default:
            unreachable();
    }
}

QList<DirEntry> FsRef::read_dir() const {
    switch (this->type) {
        case NORMAL: {
            QList<WSDirEntry> list;
            QDir dir(this->data.normal.file_path);
            for (auto entry : dir.entryInfoList()) {
                list += WSDirEntry {
            QList<DirEntry> list;
            QDir dir(this->normal->file_path);
            qDebug() << this->normal->file_path;
            qDebug() << QFileInfo(this->normal->file_path).absoluteDir();
            for (auto entry : dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
                qDebug() << entry;
                list += DirEntry {
                    .is_file = entry.isFile(),
                    .is_symlink = entry.isSymbolicLink(),
                    .is_dir = entry.isDir(),
                    .is_symlink = entry.isSymbolicLink(),
                    .file_name = entry.fileName(),
                    .real_path = FsRef(entry.filePath()),
                };


@@ 119,29 156,73 @@ QList<WSDirEntry> FsRef::read_dir() const {
            return list;
        }
        case ZIP: {
            QList<WSDirEntry> list;
            QuaZipFile qzf(this->data.zip.zip_path, this->data.zip.file_path);
            QuaZip qz(this->data.zip.zip_path);
            for (auto entry : qz.getFileInfoList()) {
                QString prefix = this->data.zip.file_path;
            QList<DirEntry> list;
            QSet<QString> scanned;
            QuaZip qz(this->zip->zip_path);
            qz.open(QuaZip::mdUnzip);
            if (qz.getFileNameList().contains(this->zip->file_path)) return list;
            for (auto entry : qz.getFileInfoList64()) {
                QString prefix = this->zip->file_path;
                if (!prefix.endsWith('/')) prefix += '/';
                if (prefix == "/") prefix = "";
                qDebug() << prefix << entry.name;
                if (entry.name.startsWith(prefix)) {
                    int start = prefix.size();
                    int end = entry.name.indexOf('/', start);
                    bool is_dir = end != -1;
                    if (end == -1) end = entry.name.length();
                    QString name = entry.name.mid(start, end - start);
                    list += WSDirEntry {
                        .is_file = !is_dir,
                        .is_dir = is_dir,
                        .is_symlink = false,
                        .file_name = name,
                        .real_path = FsRef(this->data.zip.zip_path, entry.name),
                    };
                    if (!scanned.contains(name)) {
                        qDebug() << is_dir << name;
                        list += DirEntry {
                            .is_file = !is_dir,
                            .is_dir = is_dir,
                            .is_symlink = false,
                            .file_name = name,
                            .real_path = FsRef(this->zip->zip_path, entry.name),
                        };
                        scanned += name;
                    }
                }
            }
            return list;
        }
        default:
            unreachable();
    }
}

bool FsRef::remove(bool recursive) const {
    switch (this->type) {
        case NORMAL: {
            QFileInfo fi(this->normal->file_path);
            if (fi.isFile()) {
                return QFile::remove(this->normal->file_path);
            } else if (fi.isDir()) {
                if (recursive) {
                    return QDir(this->normal->file_path).removeRecursively();
                } else {
                    // TODO does this remove directories? There's no QDir::remove for one path
                    return QFile::remove(this->normal->file_path);
                }
            }
            return false;
        }
        case ZIP:
            unimplemented();
        default:
            unreachable();
    }
}

FsRef FsRef::join(const QString& rel_path) {
    switch (this->type) {
        case NORMAL:
            return FsRef(this->normal->file_path + "/" + rel_path);
        case ZIP:
            return FsRef(this->zip->zip_path, this->zip->file_path + "/" + rel_path);
        default:
            unreachable();
    }
}


M src/workspace/fsref.h => src/workspace/fsref.h +19 -7
@@ 3,8 3,10 @@

#include <QFile>
#include <QList>
#include <QSharedPointer>
#include <quazip5/quazip.h>

struct WSDirEntry;
struct DirEntry;

struct NormalFsRef {
    QString file_path;


@@ 12,6 14,8 @@ struct NormalFsRef {

struct ZipFsRef {
    QString zip_path;
    QSharedPointer<QuaZip> qz;

    QString file_path;
};



@@ 34,19 38,27 @@ public:
    bool read_only() const;

    bool is_file() const;

    bool is_dir() const;

    bool is_link() const;

    QIODevice* open() const;
    QString file_name() const;

    QSharedPointer<QIODevice> open() const;

    QList<DirEntry> read_dir() const;

    bool remove(bool recursive) const;

    QList<WSDirEntry> read_dir() const;
    FsRef join(const QString& rel_path);

private:
    FsRefType type;
    enum { NORMAL, ZIP } type;
    union {
        NormalFsRef normal;
        ZipFsRef zip;
    } data;
        NormalFsRef* normal;
        ZipFsRef* zip;
    };

};


A src/workspace/fstree.cpp => src/workspace/fstree.cpp +77 -0
@@ 0,0 1,77 @@
#include "fstree.h"
#include "workspace.h"

FsTreeEntry::FsTreeEntry(FsRef ref, WorkspaceRoot* root, FsTreeEntry* parent) : QObject(parent), ref(ref), parent(parent), root(root) {

}

void FsTreeEntry::refresh() {
    QList<DirEntry> list = this->ref.read_dir();

    bool changed = false;

    int i = 0;
    while (!list.isEmpty()) {
        DirEntry next = list[0];
        if (this->children.length() <= i) {
            this->children += new FsTreeEntry(next.real_path, this->root, this);
            changed = true;
        } else {
            QString name = this->children[i]->file_name();

            if (next.file_name != name) {
                while (this->children.length() > i && next.file_name > name) {
                    this->children.removeAt(i);
                }
                this->children.insert(i, new FsTreeEntry(next.real_path, this->root, this));
                changed = true;
            }
        }
        i++;
        list.pop_front();
    }

    while (this->children.length() > i) {
        this->children.removeLast();
    }

    if (changed) {
        emit children_changed();
    }

    for (auto c: this->children) {
        c->refresh();
    }
}

const FsRef& FsTreeEntry::fsref() const {
    return this->ref;
}

QString FsTreeEntry::file_name() const {
    return this->ref.file_name();
}

FileType FsTreeEntry::file_type() const {
    return this->type;
}

FsTreeEntry* FsTreeEntry::get_parent() {
    return this->parent;
}

WorkspaceRoot* FsTreeEntry::get_root() {
    return this->root;
}

int FsTreeEntry::children_count() const {
    return this->children.length();
}

int FsTreeEntry::index_of(FsTreeEntry* child) const {
    return this->children.indexOf(child);
}

FsTreeEntry* FsTreeEntry::by_index(int child) {
    return this->children[child];
}

A src/workspace/fstree.h => src/workspace/fstree.h +56 -0
@@ 0,0 1,56 @@
#ifndef MCRESTOOL_FSTREE_H
#define MCRESTOOL_FSTREE_H

#include "direntry.h"

#include <QList>

class WorkspaceRoot;

enum FileType {
    NONE,
    LANGUAGE,
    LANGUAGE_PART,
    RECIPE,
};

class FsTreeEntry : public QObject {
Q_OBJECT

public:
    explicit FsTreeEntry(FsRef ref, WorkspaceRoot* root, FsTreeEntry* parent = nullptr);

    void refresh();

    const FsRef& fsref() const;

    QString file_name() const;

    FileType file_type() const;

    FsTreeEntry* get_parent();

    WorkspaceRoot* get_root();

    int children_count() const;

    int index_of(FsTreeEntry* child) const;

    FsTreeEntry* by_index(int child);

signals:

    void children_changed();

private:
    FsRef ref;
    FileType type;

    FsTreeEntry* parent;
    QVector<FsTreeEntry*> children;

    WorkspaceRoot* root;

};

#endif //MCRESTOOL_FSTREE_H

A src/workspace/fstreemodel.cpp => src/workspace/fstreemodel.cpp +91 -0
@@ 0,0 1,91 @@
#include "fstreemodel.h"
#include "src/util.h"

FsTreeModel::FsTreeModel(Workspace* ws, QObject* parent) :
    QAbstractItemModel(parent),
    ws(ws) {}

QModelIndex FsTreeModel::index(int row, int column, const QModelIndex& parent) const {
    if (!hasIndex(row, column, parent)) return QModelIndex();

    void* data = nullptr;

    if (!parent.isValid()) {
        data = this->ws->by_index(row);
    } else {
        QObject* ptr = static_cast<QObject*>(parent.internalPointer());
        if (auto item = qobject_cast<WorkspaceRoot*>(ptr)) {
            data = item->get_tree()->by_index(row);
        } else if (auto item = qobject_cast<FsTreeEntry*>(ptr)) {
            data = item->by_index(row);
        }
    }

    if (!data) {
        return QModelIndex();
    }

    return createIndex(row, column, data);
}

QModelIndex FsTreeModel::parent(const QModelIndex& child) const {
    if (!child.isValid()) return QModelIndex();

    QObject* ptr = static_cast<QObject*>(child.internalPointer());
    if (auto item = qobject_cast<FsTreeEntry*>(ptr)) {
        FsTreeEntry* root = item->get_parent();
        assert(root != nullptr);

        if (root->get_parent() == nullptr) {
            // parent is top-level (WorkspaceRoot)
            return createIndex(root->index_of(item), 0, root->get_root());
        }

        return createIndex(root->index_of(item), 0, root);
    } else if (auto item = qobject_cast<WorkspaceRoot*>(ptr)) {
        return QModelIndex();
    } else {
        return QModelIndex();
    }
}

QVariant FsTreeModel::data(const QModelIndex& index, int role) const {
    if (!index.isValid()) return QVariant();
    if (role != Qt::DisplayRole) return QVariant();

    QObject* ptr = static_cast<QObject*>(index.internalPointer());
    if (auto item = qobject_cast<WorkspaceRoot*>(ptr)) {
        return item->get_name();
    } else if (auto item = qobject_cast<FsTreeEntry*>(ptr)) {
        return item->file_name();
    } else {
        return QVariant();
    }
}

Qt::ItemFlags FsTreeModel::flags(const QModelIndex& index) const {
    if (!index.isValid()) return Qt::NoItemFlags;

    return QAbstractItemModel::flags(index);
}

QVariant FsTreeModel::headerData(int section, Qt::Orientation orientation, int role) const {
    return QVariant();
}

int FsTreeModel::rowCount(const QModelIndex& parent) const {
    if (parent.column() > 0) return 0;

    QObject* ptr = static_cast<QObject*>(parent.internalPointer());
    if (auto item = qobject_cast<FsTreeEntry*>(ptr)) {
        return item->children_count();
    } else if (auto item = qobject_cast<WorkspaceRoot*>(ptr)) {
        return item->get_tree()->children_count();
    } else {
        return this->ws->root_count();
    }
}

int FsTreeModel::columnCount(const QModelIndex& parent) const {
    return 1;
}

R src/model/resourcetree.h => src/workspace/fstreemodel.h +12 -11
@@ 1,17 1,16 @@
#ifndef MCRESTOOL_RESOURCETREE_H
#define MCRESTOOL_RESOURCETREE_H
#ifndef MCRESTOOL_FSTREEMODEL_H
#define MCRESTOOL_FSTREEMODEL_H

#include "fstree.h"
#include "workspace.h"

#include <QString>
#include <QAbstractItemModel>
#include "treeitem.h"

class ResourceTree : public QAbstractItemModel {
Q_OBJECT
class FsTreeModel : public QAbstractItemModel {
    Q_OBJECT

public:
    explicit ResourceTree(QObject* parent = nullptr);

    ~ResourceTree() override;
    explicit FsTreeModel(Workspace* ws, QObject* parent = nullptr);

    QModelIndex index(int row, int column, const QModelIndex& parent) const override;



@@ 27,9 26,11 @@ public:

    int columnCount(const QModelIndex& parent) const override;

public slots:

private:
    TreeItem* root_item;
    Workspace* ws;

};

#endif //MCRESTOOL_RESOURCETREE_H
#endif //MCRESTOOL_FSTREEMODEL_H

M src/workspace/workspace.cpp => src/workspace/workspace.cpp +24 -24
@@ 1,46 1,46 @@
#include "workspace.h"
#include <QFileInfo>
#include "fstree.h"

WorkspaceRootBase::WorkspaceRootBase(const QString& name, QObject* parent) : QObject(parent) {
    this->name = name;
WorkspaceRoot::WorkspaceRoot(QString name, FsRef root, QObject* parent) :
    QObject(parent),
    name(name),
    tree(new FsTreeEntry(root, this)) {
    this->tree->refresh();
}

const QString& WorkspaceRootBase::get_name() const {
    return this->name;
FsTreeEntry* WorkspaceRoot::get_tree() {
    return this->tree;
}

void WorkspaceRootBase::set_name(QString str) {
    this->name = str;
const QString& WorkspaceRoot::get_name() const {
    return this->name;
}


Workspace::Workspace(QObject* parent) : QObject(parent), roots(QList<WorkspaceRootBase*>()) {
}
Workspace::Workspace(QObject* parent) :
    QObject(parent),
    roots(QVector<WorkspaceRoot*>()) {}

void Workspace::add_dir(QString path) {
    this->roots += new DirWorkspaceRoot(path, this);
    this->roots += new WorkspaceRoot(path, FsRef(path), this);
    emit entry_added(this->roots.last());
}

void Workspace::add_file(QString path) {
    this->roots += new ZipWorkspaceRoot(path);
}


DirWorkspaceRoot::DirWorkspaceRoot(const QString& path, QObject* parent) : WorkspaceRootBase(QFileInfo(path).fileName(), parent) {
    this->path = path;
    this->roots += new WorkspaceRoot(path, FsRef(path, "/"), this);
    emit entry_added(this->roots.last());
}

QList<WSDirEntry> DirWorkspaceRoot::list_dir_tree(const QString& path) {
    QList<WSDirEntry> v;
    return v;
int Workspace::index_of(WorkspaceRoot* root) const {
    return this->roots.indexOf(root);
}

WorkspaceRoot* Workspace::by_index(int index) {
    if (index < 0 || index >= this->roots.length()) return nullptr;

ZipWorkspaceRoot::ZipWorkspaceRoot(const QString& path, QObject* parent) : WorkspaceRootBase(QFileInfo(path).fileName(), parent) {
    this->path = path;
    return this->roots[index];
}

QList<WSDirEntry> ZipWorkspaceRoot::list_dir_tree(const QString& path) {
    QList<WSDirEntry> v;
    return v;
int Workspace::root_count() const {
    return this->roots.length();
}

M src/workspace/workspace.h => src/workspace/workspace.h +19 -32
@@ 5,61 5,48 @@

#include <QString>

class WorkspaceRootBase : public QObject {
Q_OBJECT
class FsTreeEntry;

public:
    WorkspaceRootBase(const QString& name, QObject* parent = nullptr);
class WorkspaceRoot : public QObject {
    Q_OBJECT

    virtual ~WorkspaceRootBase() = default;
public:
    WorkspaceRoot(QString name, FsRef root, QObject* parent = nullptr);

    virtual QList<WSDirEntry> list_dir_tree(const QString& path) = 0;
    FsTreeEntry* get_tree();

    const QString& get_name() const;

    void set_name(QString name);

private:
    QString name;
    FsTreeEntry* tree;

};

class DirWorkspaceRoot : public WorkspaceRootBase {
class Workspace : public QObject {
Q_OBJECT

public:
    DirWorkspaceRoot(const QString& path, QObject* parent = nullptr);

    QList<WSDirEntry> list_dir_tree(const QString& path) override;

private:
    QString path;

};

class ZipWorkspaceRoot : public WorkspaceRootBase {
    Workspace(QObject* parent = nullptr);

public:
    ZipWorkspaceRoot(const QString& path, QObject* parent = nullptr);
    void add_dir(QString path);

    QList<WSDirEntry> list_dir_tree(const QString& path) override;
    void add_file(QString path);

private:
    QString path;
    int index_of(WorkspaceRoot* root) const;

};
    WorkspaceRoot* by_index(int index);

class Workspace : public QObject {
Q_OBJECT
    int root_count() const;

public:
    Workspace(QObject* parent = nullptr);
signals:

    void add_dir(QString path);
    void entry_added(WorkspaceRoot* root);

    void add_file(QString path);
    void entry_removed(WorkspaceRoot* root);

private:
    QList<WorkspaceRootBase*> roots;
    QVector<WorkspaceRoot*> roots;

};