Add dynamic file filter to Open dialog

- Add TypeRegistry::supportedExtensions() to extract file extensions
  from loaded XScript definitions by parsing criteria blocks
- Implement File > Open action with QFileDialog
- Generate filter string dynamically from all root type definitions
- Include "All Supported Files" and "All Files" filter options

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
njohnson 2026-01-07 16:41:46 -05:00
parent bebaeff322
commit 64db5a19ed
3 changed files with 137 additions and 2 deletions

View File

@ -297,8 +297,100 @@ MainWindow::MainWindow(QWidget *parent)
// TODO: Implement New action
});
connect(actionOpen, &QAction::triggered, this, []() {
// TODO: Implement Open action
connect(actionOpen, &QAction::triggered, this, [this]() {
// Build filter string from loaded definitions
QMap<QString, QString> exts = mTypeRegistry.supportedExtensions();
QStringList filterParts;
QStringList allExts;
for (auto it = exts.begin(); it != exts.end(); ++it) {
const QString& ext = it.key();
const QString& displayName = it.value();
filterParts.append(QString("%1 (*.%2)").arg(displayName, ext));
allExts.append(QString("*.%1").arg(ext));
}
// Sort filters alphabetically by display name
filterParts.sort(Qt::CaseInsensitive);
// Add "All Supported Files" at the beginning
QString allFilter = QString("All Supported Files (%1)").arg(allExts.join(" "));
filterParts.prepend(allFilter);
// Add "All Files" at the end
filterParts.append("All Files (*)");
QString filter = filterParts.join(";;");
QStringList filePaths = QFileDialog::getOpenFileNames(
this,
"Open File",
QString(),
filter
);
for (const QString& path : filePaths) {
const QString fileName = QFileInfo(path).fileName();
QFile inputFile(path);
if (!inputFile.open(QIODevice::ReadOnly)) {
LogManager::instance().addError(QString("Failed to open: %1").arg(path));
continue;
}
LogManager::instance().addEntry(QString("[FILE] Analyzing: %1").arg(fileName));
const QString rootType = mTypeRegistry.chooseType(&inputFile, path);
LogManager::instance().addEntry(QString("[FILE] Matched type '%1' for file: %2").arg(rootType).arg(fileName));
if (rootType.isEmpty()) {
LogManager::instance().addError(QString("No matching definition for: %1").arg(path));
continue;
}
inputFile.seek(0);
LogManager::instance().addLine();
LogManager::instance().addEntry(QString("========== PARSING %1 ==========").arg(fileName));
LogManager::instance().addLine();
const qint64 fileSize = inputFile.size();
QProgressDialog progress(QString("Parsing %1...").arg(fileName), "Cancel", 0, static_cast<int>(fileSize), this);
progress.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(500);
QVariantMap rootVars;
try {
rootVars = mTypeRegistry.parse(rootType, &inputFile, path, [this, &progress](qint64 pos, qint64 size) {
progress.setValue(static_cast<int>(pos));
QApplication::processEvents();
});
} catch (const std::exception& e) {
LogManager::instance().addLine();
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
LogManager::instance().addError(QString("File position: 0x%1").arg(inputFile.pos(), 0, 16));
LogManager::instance().addLine();
QMessageBox::critical(this, "Parse Error",
QString("Failed to parse %1:\n\n%2").arg(fileName).arg(e.what()));
continue;
}
LogManager::instance().addLine();
LogManager::instance().addEntry(QString("========== FINISHED %1 ==========").arg(fileName));
LogManager::instance().addLine();
if (!mOpenedFilePaths.contains(path)) {
mOpenedFilePaths.append(path);
}
XTreeWidgetItem* cat = ensureTypeCategoryRoot(rootType);
auto* rootInst = addInstanceNode(cat, fileName, rootType, rootVars);
routeNestedObjects(rootInst, rootVars);
cat->setExpanded(true);
mTreeWidget->setCurrentItem(rootInst);
}
});
connect(actionOpenFolder, &QAction::triggered, this, []() {

View File

@ -39,6 +39,46 @@ QStringList TypeRegistry::typeNames() const {
return m_module.types.keys();
}
QMap<QString, QString> TypeRegistry::supportedExtensions() const {
QMap<QString, QString> result;
for (auto it = m_module.types.begin(); it != m_module.types.end(); ++it) {
const TypeDef& td = it.value();
if (!td.isRoot) continue;
// Scan criteria for `require _ext == "xxx"` patterns
for (const auto& stmtPtr : td.criteria) {
if (!std::holds_alternative<Stmt::Require>(stmtPtr->node)) continue;
const auto& req = std::get<Stmt::Require>(stmtPtr->node);
if (!std::holds_alternative<Expr::Binary>(req.cond->node)) continue;
const auto& bin = std::get<Expr::Binary>(req.cond->node);
if (bin.op != "==") continue;
// Check for _ext == "xxx" or "xxx" == _ext
QString ext;
bool lhsIsExt = std::holds_alternative<Expr::Var>(bin.lhs->node) &&
std::get<Expr::Var>(bin.lhs->node).name == "_ext";
bool rhsIsExt = std::holds_alternative<Expr::Var>(bin.rhs->node) &&
std::get<Expr::Var>(bin.rhs->node).name == "_ext";
if (lhsIsExt && std::holds_alternative<Expr::String>(bin.rhs->node)) {
ext = std::get<Expr::String>(bin.rhs->node).v;
} else if (rhsIsExt && std::holds_alternative<Expr::String>(bin.lhs->node)) {
ext = std::get<Expr::String>(bin.lhs->node).v;
}
if (!ext.isEmpty()) {
QString displayName = td.display.isEmpty() ? td.name : td.display;
result.insert(ext.toLower(), displayName);
}
}
}
return result;
}
void TypeRegistry::collectTypeRefsFromExpr(const Expr& expr, QSet<QString>& refs) const {
if (std::holds_alternative<Expr::Call>(expr.node)) {
const auto& call = std::get<Expr::Call>(expr.node);

View File

@ -19,6 +19,9 @@ public:
bool hasType(const QString& typeName) const;
QStringList typeNames() const;
// Returns map of extension -> display name for all root types
QMap<QString, QString> supportedExtensions() const;
// Validate all type references - returns list of errors, empty if valid
QStringList validateTypeReferences() const;