#include "MetaCoreFoundation/MetaCoreProject.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { struct LauncherProject { QString Name{}; std::filesystem::path RootPath{}; QString UpdatedDate{}; }; constexpr const char* OrganizationName = "MetaCore"; constexpr const char* ApplicationName = "MetaCore Hub"; constexpr const char* NewProjectSettingsGroup = "NewProjectDialog"; constexpr const char* LastProjectPathKey = "lastProjectPath"; std::filesystem::path ToPath(const QString& text) { return std::filesystem::path(text.toStdWString()); } QString ToQString(const std::filesystem::path& path) { return QString::fromStdWString(path.wstring()); } std::filesystem::path GetExecutableDirectory() { return ToPath(QCoreApplication::applicationDirPath()); } std::filesystem::path GetDatabasePath() { QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); if (dataLocation.isEmpty()) { dataLocation = QDir::homePath() + "/AppData/Local/MetaCore/Hub"; } return ToPath(dataLocation) / "projects.tsv"; } std::filesystem::path GetDefaultProjectLocation() { QString documents = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); if (documents.isEmpty()) { documents = QDir::homePath(); } return ToPath(documents) / "MetaCore Projects"; } bool IsProjectRoot(const std::filesystem::path& path) { return std::filesystem::is_regular_file(MetaCore::MetaCoreGetProjectFilePath(path)); } bool WriteTextFile(const std::filesystem::path& path, const std::string& text, QString& errorMessage) { std::error_code error; if (!path.parent_path().empty()) { std::filesystem::create_directories(path.parent_path(), error); if (error) { errorMessage = QString("Unable to create folder for %1: %2") .arg(ToQString(path)) .arg(QString::fromStdString(error.message())); return false; } } std::ofstream output(path, std::ios::binary | std::ios::trunc); if (!output.is_open()) { errorMessage = QString("Unable to write %1.").arg(ToQString(path)); return false; } output << text; return true; } QPushButton* CreateButton(const QString& text, const QString& objectName = {}) { auto* button = new QPushButton(text); button->setCursor(Qt::PointingHandCursor); if (!objectName.isEmpty()) { button->setObjectName(objectName); } return button; } QLabel* CreateMutedLabel(const QString& text) { auto* label = new QLabel(text); label->setObjectName("mutedText"); return label; } void ClearLayout(QLayout* layout) { if (layout == nullptr) { return; } while (QLayoutItem* item = layout->takeAt(0)) { if (QWidget* widget = item->widget()) { widget->deleteLater(); } if (QLayout* childLayout = item->layout()) { ClearLayout(childLayout); } delete item; } } bool CreateProjectOnDisk(const std::filesystem::path& rootPath, const QString& name, QString& errorMessage) { if (rootPath.empty()) { errorMessage = "Project location is required."; return false; } std::error_code error; if (std::filesystem::exists(rootPath, error) && !std::filesystem::is_empty(rootPath, error)) { errorMessage = "Project folder already exists and is not empty."; return false; } const std::vector directories = { rootPath / "ProjectSettings", rootPath / "Logs", rootPath / "Library", rootPath / "Assets", rootPath / "Assets" / "Models", rootPath / "Assets" / "Materials", rootPath / "Assets" / "Textures", rootPath / "Assets" / "Prefabs", rootPath / "Scenes", rootPath / "Runtime", rootPath / "Ui", rootPath / "Build", rootPath / ".vscode" }; for (const std::filesystem::path& directory : directories) { std::filesystem::create_directories(directory, error); if (error) { errorMessage = QString("Unable to create project folder %1: %2") .arg(ToQString(directory)) .arg(QString::fromStdString(error.message())); return false; } } MetaCore::MetaCoreProjectFileDocument document; document.Name = name.toStdString(); document.Version = "0.1.0"; document.RuntimeDirectory = "Runtime"; document.UiDirectory = "Ui"; document.BuildDirectory = "Build"; document.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; if (!MetaCore::MetaCoreWriteProjectFile(MetaCore::MetaCoreGetProjectFilePath(rootPath), document)) { errorMessage = "Unable to write MetaCore.project.json."; return false; } if (!WriteTextFile( rootPath / "Assets" / "README.md", "# Project Assets\n\nThis folder contains all assets for the MetaCore project.\n", errorMessage )) { return false; } const std::string createdAt = QDateTime::currentDateTime().toString(Qt::ISODate).toStdString(); std::ostringstream ini; ini << "[Project]\n"; ini << "name = " << name.toStdString() << "\n"; ini << "path = " << ToQString(rootPath).toStdString() << "\n"; ini << "created_at = " << createdAt << "\n"; ini << "changed_at = " << createdAt << "\n"; if (!WriteTextFile(rootPath / (name.toStdString() + ".ini"), ini.str(), errorMessage)) { return false; } if (!WriteTextFile( rootPath / ".vscode" / "settings.json", "{\n" " \"editor.formatOnSave\": true,\n" " \"files.exclude\": {\n" " \"Library\": true,\n" " \"Logs\": true,\n" " \"Build\": true\n" " }\n" "}\n", errorMessage )) { return false; } return true; } class ProjectCard final : public QFrame { public: explicit ProjectCard(LauncherProject project, QWidget* parent = nullptr) : QFrame(parent), Project_(std::move(project)) { setObjectName("projectCard"); setCursor(Qt::PointingHandCursor); setMinimumHeight(72); setMaximumHeight(72); auto* rootLayout = new QHBoxLayout(this); rootLayout->setContentsMargins(16, 10, 12, 10); rootLayout->setSpacing(14); Avatar_ = new QLabel(Project_.Name.left(1).toUpper()); Avatar_->setObjectName("projectAvatar"); Avatar_->setAlignment(Qt::AlignCenter); Avatar_->setFixedSize(44, 44); rootLayout->addWidget(Avatar_); auto* textLayout = new QVBoxLayout(); textLayout->setContentsMargins(0, 0, 0, 0); textLayout->setSpacing(2); auto* nameLabel = new QLabel(Project_.Name); nameLabel->setObjectName("projectName"); auto* pathLabel = CreateMutedLabel(ToQString(Project_.RootPath)); pathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); pathLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); textLayout->addWidget(nameLabel); textLayout->addWidget(pathLabel); rootLayout->addLayout(textLayout, 1); auto* dateLabel = CreateMutedLabel(Project_.UpdatedDate); dateLabel->setMinimumWidth(88); rootLayout->addWidget(dateLabel); auto* openButton = CreateButton("Open"); openButton->setObjectName("smallButton"); openButton->setFixedWidth(64); connect(openButton, &QPushButton::clicked, this, [this]() { if (OnOpen) { OnOpen(); } }); rootLayout->addWidget(openButton); } void SetSelected(bool selected) { setProperty("selected", selected); style()->unpolish(this); style()->polish(this); } const LauncherProject& Project() const { return Project_; } protected: void mousePressEvent(QMouseEvent* event) override { emitSelected(); QFrame::mousePressEvent(event); } private: void emitSelected() { if (OnSelected) { OnSelected(); } } public: std::function OnSelected{}; std::function OnOpen{}; private: LauncherProject Project_{}; QLabel* Avatar_ = nullptr; }; class NewProjectDialog final : public QDialog { public: explicit NewProjectDialog(QWidget* parent = nullptr) : QDialog(parent) { setWindowTitle("Create New Project"); setMinimumWidth(520); setModal(true); auto* rootLayout = new QVBoxLayout(this); rootLayout->setContentsMargins(22, 20, 22, 18); rootLayout->setSpacing(10); rootLayout->addWidget(new QLabel("Project Name:")); NameEdit_ = new QLineEdit(); NameEdit_->setPlaceholderText("Enter a name for your project"); rootLayout->addWidget(NameEdit_); rootLayout->addWidget(new QLabel("Project Location:")); auto* locationLayout = new QHBoxLayout(); LocationEdit_ = new QLineEdit(); LocationEdit_->setReadOnly(true); LocationEdit_->setPlaceholderText("No path selected"); auto* browseButton = CreateButton("Browse..."); browseButton->setFixedWidth(90); locationLayout->addWidget(LocationEdit_, 1); locationLayout->addWidget(browseButton); rootLayout->addLayout(locationLayout); QSettings settings(OrganizationName, ApplicationName); const QString lastPath = settings.value( QString("%1/%2").arg(NewProjectSettingsGroup, LastProjectPathKey), QString() ).toString(); if (!lastPath.isEmpty()) { LocationEdit_->setText(lastPath); } connect(browseButton, &QPushButton::clicked, this, [this]() { const QString currentPath = !LocationEdit_->text().isEmpty() ? LocationEdit_->text() : ToQString(GetDefaultProjectLocation()); const QString selectedPath = QFileDialog::getExistingDirectory( this, "Project Location", currentPath ); if (!selectedPath.isEmpty()) { LocationEdit_->setText(selectedPath); QSettings settings(OrganizationName, ApplicationName); settings.setValue(QString("%1/%2").arg(NewProjectSettingsGroup, LastProjectPathKey), selectedPath); UpdateCreateButtonState(); } }); rootLayout->addWidget(new QLabel("Engine Version:")); VersionCombo_ = new QComboBox(); VersionCombo_->setFixedHeight(32); VersionCombo_->addItem("dev (current environment)", "dev-current"); rootLayout->addWidget(VersionCombo_); ErrorLabel_ = new QLabel(); ErrorLabel_->setObjectName("errorText"); ErrorLabel_->setWordWrap(true); ErrorLabel_->hide(); rootLayout->addWidget(ErrorLabel_); auto* buttonsLayout = new QHBoxLayout(); buttonsLayout->addStretch(1); auto* cancelButton = CreateButton("Cancel"); CreateButton_ = CreateButton("Create", "primaryButton"); cancelButton->setFixedWidth(90); CreateButton_->setFixedWidth(90); buttonsLayout->addWidget(cancelButton); buttonsLayout->addWidget(CreateButton_); rootLayout->addLayout(buttonsLayout); connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); connect(CreateButton_, &QPushButton::clicked, this, [this]() { accept(); }); connect(NameEdit_, &QLineEdit::textChanged, this, [this]() { UpdateCreateButtonState(); }); connect(VersionCombo_, &QComboBox::currentTextChanged, this, [this]() { UpdateCreateButtonState(); }); UpdateCreateButtonState(); } QString ProjectName() const { return NameEdit_->text().trimmed(); } std::filesystem::path ProjectRootPath() const { return ToPath(LocationEdit_->text()) / ToPath(ProjectName()); } void accept() override { UpdateCreateButtonState(); if (!CreateButton_->isEnabled()) { ShowError("Project name, location, and engine version are required."); return; } if (ProjectName().contains('/') || ProjectName().contains('\\')) { ShowError("Project name cannot contain path separators."); return; } QDialog::accept(); } private: void ShowError(const QString& text) { ErrorLabel_->setText(text); ErrorLabel_->show(); } void UpdateCreateButtonState() { if (CreateButton_ == nullptr) { return; } const bool valid = !ProjectName().isEmpty() && !LocationEdit_->text().trimmed().isEmpty() && VersionCombo_->currentIndex() >= 0; CreateButton_->setEnabled(valid); if (valid) { ErrorLabel_->hide(); } } QLineEdit* NameEdit_ = nullptr; QLineEdit* LocationEdit_ = nullptr; QComboBox* VersionCombo_ = nullptr; QLabel* ErrorLabel_ = nullptr; QPushButton* CreateButton_ = nullptr; }; class MetaCoreLauncherWindow final : public QMainWindow { public: explicit MetaCoreLauncherWindow(QWidget* parent = nullptr) : QMainWindow(parent), ExecutableDirectory_(GetExecutableDirectory()), DatabasePath_(GetDatabasePath()) { setWindowTitle("MetaCore Hub"); resize(1080, 720); setMinimumSize(960, 640); setObjectName("launcherWindow"); LoadProjects(); BuildUi(); RefreshProjectList(); } private: void BuildUi() { ApplyStyleSheet(true); auto* central = new QWidget(this); central->setObjectName("central"); setCentralWidget(central); auto* rootLayout = new QHBoxLayout(central); rootLayout->setContentsMargins(0, 0, 0, 0); rootLayout->setSpacing(0); rootLayout->addWidget(BuildSidebar()); Pages_ = new QStackedWidget(); Pages_->addWidget(BuildProjectsPage()); Pages_->addWidget(BuildInstallsPage()); rootLayout->addWidget(Pages_, 1); } QWidget* BuildSidebar() { auto* sidebar = new QWidget(); sidebar->setObjectName("sidebar"); sidebar->setFixedWidth(220); auto* layout = new QVBoxLayout(sidebar); layout->setContentsMargins(18, 24, 18, 18); layout->setSpacing(8); auto* titleLabel = new QLabel("MetaCore"); titleLabel->setObjectName("hubTitle"); auto* subtitleLabel = CreateMutedLabel("Hub"); layout->addWidget(titleLabel); layout->addWidget(subtitleLabel); layout->addSpacing(28); ProjectsNavButton_ = CreateButton("Projects", "navButton"); InstallsNavButton_ = CreateButton("Installs", "navButton"); ProjectsNavButton_->setProperty("selected", true); layout->addWidget(ProjectsNavButton_); layout->addWidget(InstallsNavButton_); layout->addStretch(1); auto* darkMode = new QCheckBox("Dark Mode"); darkMode->setChecked(true); layout->addWidget(darkMode); connect(ProjectsNavButton_, &QPushButton::clicked, this, [this]() { SetPage(0); }); connect(InstallsNavButton_, &QPushButton::clicked, this, [this]() { SetPage(1); }); connect(darkMode, &QCheckBox::toggled, this, [this](bool checked) { ApplyStyleSheet(checked); }); return sidebar; } QWidget* BuildProjectsPage() { auto* page = new QWidget(); auto* layout = new QVBoxLayout(page); layout->setContentsMargins(28, 24, 28, 24); layout->setSpacing(14); auto* headerLayout = new QHBoxLayout(); auto* titleLabel = new QLabel("Projects"); titleLabel->setObjectName("pageTitle"); headerLayout->addWidget(titleLabel); headerLayout->addStretch(1); auto* deleteButton = CreateButton("Delete", "dangerButton"); auto* newButton = CreateButton("+ New Project"); auto* launchButton = CreateButton("Launch", "primaryButton"); headerLayout->addWidget(deleteButton); headerLayout->addWidget(newButton); headerLayout->addWidget(launchButton); layout->addLayout(headerLayout); SearchEdit_ = new QLineEdit(); SearchEdit_->setPlaceholderText("Search projects..."); SearchEdit_->setFixedHeight(36); layout->addWidget(SearchEdit_); StatusLabel_ = CreateMutedLabel(""); StatusLabel_->hide(); layout->addWidget(StatusLabel_); auto* scrollArea = new QScrollArea(); scrollArea->setObjectName("projectScroll"); scrollArea->setWidgetResizable(true); scrollArea->setFrameShape(QFrame::NoFrame); ProjectListContainer_ = new QWidget(); ProjectListLayout_ = new QVBoxLayout(ProjectListContainer_); ProjectListLayout_->setContentsMargins(0, 0, 0, 0); ProjectListLayout_->setSpacing(10); ProjectListLayout_->addStretch(1); scrollArea->setWidget(ProjectListContainer_); layout->addWidget(scrollArea, 1); connect(SearchEdit_, &QLineEdit::textChanged, this, [this]() { RefreshProjectList(); }); connect(newButton, &QPushButton::clicked, this, [this]() { CreateNewProject(); }); connect(deleteButton, &QPushButton::clicked, this, [this]() { RemoveSelectedProject(); }); connect(launchButton, &QPushButton::clicked, this, [this]() { LaunchSelectedProject(); }); return page; } QWidget* BuildInstallsPage() { auto* page = new QWidget(); auto* layout = new QVBoxLayout(page); layout->setContentsMargins(28, 24, 28, 24); layout->setSpacing(14); auto* headerLayout = new QHBoxLayout(); auto* titleLabel = new QLabel("Installs"); titleLabel->setObjectName("pageTitle"); headerLayout->addWidget(titleLabel); headerLayout->addStretch(1); auto* refreshButton = CreateButton("Refresh"); headerLayout->addWidget(refreshButton); layout->addLayout(headerLayout); layout->addWidget(BuildInstallCard("MetaCore Editor", "", "Locate", [this]() { QDesktopServices::openUrl(QUrl::fromLocalFile(ToQString(ExecutableDirectory_))); }, &EditorInstallStatusLabel_)); layout->addWidget(BuildInstallCard("MetaCore Player", "", "Locate", [this]() { QDesktopServices::openUrl(QUrl::fromLocalFile(ToQString(ExecutableDirectory_))); }, &PlayerInstallStatusLabel_)); layout->addWidget(BuildInstallCard("Cook / Package Tool", "", "Locate", [this]() { QDesktopServices::openUrl(QUrl::fromLocalFile(ToQString(ExecutableDirectory_))); }, &CookToolInstallStatusLabel_)); layout->addWidget(BuildInstallCard("Runtime Stack", "C++20 / Filament / ImGui / RmlUi / Cook", "Refresh", [this]() { RefreshInstallsPage(); })); layout->addStretch(1); connect(refreshButton, &QPushButton::clicked, this, [this]() { RefreshInstallsPage(); }); RefreshInstallsPage(); return page; } QWidget* BuildInstallCard( const QString& title, const QString& subtitle, const QString& action, std::function callback, QLabel** subtitleLabelOut = nullptr ) { auto* card = new QFrame(); card->setObjectName("installCard"); card->setMinimumHeight(78); card->setMaximumHeight(78); auto* layout = new QHBoxLayout(card); layout->setContentsMargins(16, 12, 16, 12); layout->setSpacing(12); auto* textLayout = new QVBoxLayout(); textLayout->setSpacing(3); auto* titleLabel = new QLabel(title); titleLabel->setObjectName("projectName"); textLayout->addWidget(titleLabel); QLabel* subtitleLabel = CreateMutedLabel(subtitle); subtitleLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); textLayout->addWidget(subtitleLabel); if (subtitleLabelOut != nullptr) { *subtitleLabelOut = subtitleLabel; } layout->addLayout(textLayout, 1); auto* button = CreateButton(action); button->setFixedWidth(90); connect(button, &QPushButton::clicked, this, std::move(callback)); layout->addWidget(button); return card; } void RefreshInstallsPage() { auto describeExecutable = [this](const QString& label, const std::filesystem::path& executablePath) { if (std::filesystem::is_regular_file(executablePath)) { return QString("Installed: %1").arg(ToQString(executablePath)); } return QString("Missing: %1 was not found next to the launcher. Build the %2 target first.") .arg(ToQString(executablePath)) .arg(label); }; if (EditorInstallStatusLabel_ != nullptr) { const QString status = describeExecutable("MetaCoreEditorApp", ExecutableDirectory_ / "MetaCoreEditorApp.exe"); EditorInstallStatusLabel_->setText(status); EditorInstallStatusLabel_->setToolTip(status); } if (PlayerInstallStatusLabel_ != nullptr) { const QString status = describeExecutable("MetaCorePlayer", ExecutableDirectory_ / "MetaCorePlayer.exe"); PlayerInstallStatusLabel_->setText(status); PlayerInstallStatusLabel_->setToolTip(status); } if (CookToolInstallStatusLabel_ != nullptr) { const QString status = describeExecutable("MetaCoreBuildPackageTool", ExecutableDirectory_ / "MetaCoreBuildPackageTool.exe"); CookToolInstallStatusLabel_->setText(status); CookToolInstallStatusLabel_->setToolTip(status); } } void SetPage(int index) { Pages_->setCurrentIndex(index); ProjectsNavButton_->setProperty("selected", index == 0); InstallsNavButton_->setProperty("selected", index == 1); ProjectsNavButton_->style()->unpolish(ProjectsNavButton_); ProjectsNavButton_->style()->polish(ProjectsNavButton_); InstallsNavButton_->style()->unpolish(InstallsNavButton_); InstallsNavButton_->style()->polish(InstallsNavButton_); if (index == 1) { RefreshInstallsPage(); } } void LoadProjects() { Projects_.clear(); std::ifstream stream(DatabasePath_, std::ios::binary); if (!stream.is_open()) { return; } std::string line; while (std::getline(stream, line)) { std::stringstream lineStream(line); std::string name; std::string path; std::string date; if (!std::getline(lineStream, name, '\t') || !std::getline(lineStream, path, '\t') || !std::getline(lineStream, date, '\t')) { continue; } LauncherProject project; project.Name = QString::fromStdString(name); project.RootPath = std::filesystem::path(path); project.UpdatedDate = QString::fromStdString(date); if (IsProjectRoot(project.RootPath)) { Projects_.push_back(std::move(project)); } } if (!Projects_.empty()) { SelectedProjectIndex_ = 0; } } void SaveProjects() const { std::error_code error; std::filesystem::create_directories(DatabasePath_.parent_path(), error); std::ofstream stream(DatabasePath_, std::ios::binary | std::ios::trunc); if (!stream.is_open()) { return; } for (const LauncherProject& project : Projects_) { stream << project.Name.toStdString() << '\t' << project.RootPath.string() << '\t' << project.UpdatedDate.toStdString() << '\n'; } } void RefreshProjectList() { ClearLayout(ProjectListLayout_); const QString searchText = SearchEdit_ != nullptr ? SearchEdit_->text().trimmed().toLower() : QString(); for (int index = 0; index < static_cast(Projects_.size()); ++index) { const LauncherProject& project = Projects_[static_cast(index)]; if (!searchText.isEmpty() && !project.Name.toLower().contains(searchText) && !ToQString(project.RootPath).toLower().contains(searchText)) { continue; } auto* card = new ProjectCard(project); card->SetSelected(index == SelectedProjectIndex_); card->OnSelected = [this, index]() { SelectedProjectIndex_ = index; RefreshProjectList(); }; card->OnOpen = [this, index]() { SelectedProjectIndex_ = index; LaunchSelectedProject(); }; ProjectListLayout_->addWidget(card); } ProjectListLayout_->addStretch(1); } void CreateNewProject() { NewProjectDialog dialog(this); if (dialog.exec() != QDialog::Accepted) { return; } if (FindProjectIndexByName(dialog.ProjectName()) >= 0) { QMessageBox::critical(this, "Duplicate Name", QString("Project '%1' already exists.").arg(dialog.ProjectName())); return; } QString errorMessage; const std::filesystem::path projectRoot = dialog.ProjectRootPath(); if (!CreateProjectOnDisk(projectRoot, dialog.ProjectName(), errorMessage)) { QMessageBox::warning(this, "Create New Project", errorMessage); return; } LauncherProject project; project.Name = dialog.ProjectName(); project.RootPath = projectRoot; project.UpdatedDate = QDate::currentDate().toString(Qt::ISODate); Projects_.insert(Projects_.begin(), std::move(project)); SelectedProjectIndex_ = 0; SaveProjects(); RefreshProjectList(); LaunchSelectedProject(); } void RemoveSelectedProject() { if (!HasSelectedProject()) { QMessageBox::warning(this, "No Selection", "Please select a project to delete."); return; } const LauncherProject& project = Projects_[static_cast(SelectedProjectIndex_)]; QString deleteSafetyError; const bool projectFolderExists = std::filesystem::exists(project.RootPath); if (projectFolderExists && !IsSafeProjectRootForDeletion(project.RootPath, deleteSafetyError)) { QMessageBox::warning(this, "Delete Project", deleteSafetyError); return; } const QMessageBox::StandardButton result = QMessageBox::question( this, "Confirm Deletion", QString("Delete project '%1' and remove its folder from disk?\n\n%2") .arg(project.Name, ToQString(project.RootPath)) ); if (result != QMessageBox::Yes) { return; } if (projectFolderExists) { std::error_code error; std::filesystem::remove_all(project.RootPath, error); if (error) { QMessageBox::critical( this, "Project Deletion Failed", QString("Failed to remove the project folder:\n%1\n\n%2") .arg(ToQString(project.RootPath)) .arg(QString::fromStdString(error.message())) ); return; } } const QString deletedName = project.Name; Projects_.erase(Projects_.begin() + SelectedProjectIndex_); SelectedProjectIndex_ = std::min(SelectedProjectIndex_, static_cast(Projects_.size()) - 1); SaveProjects(); RefreshProjectList(); ShowStatus(projectFolderExists ? "Deleted " + deletedName : "Removed missing project from Hub."); } void LaunchSelectedProject() { if (!HasSelectedProject()) { ShowStatus("Select a project first."); return; } LauncherProject& project = Projects_[static_cast(SelectedProjectIndex_)]; const std::filesystem::path editorPath = ExecutableDirectory_ / "MetaCoreEditorApp.exe"; if (!std::filesystem::is_regular_file(editorPath)) { QMessageBox::warning(this, "Launch", "MetaCoreEditorApp.exe was not found next to the launcher."); return; } QProcess process; process.setProgram(ToQString(editorPath)); process.setArguments(QStringList{ "--project", ToQString(project.RootPath) }); process.setWorkingDirectory(ToQString(ExecutableDirectory_)); QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); environment.insert("METACORE_PROJECT_PATH", ToQString(project.RootPath)); process.setProcessEnvironment(environment); if (!process.startDetached()) { QMessageBox::warning(this, "Launch", "Unable to launch MetaCoreEditorApp.exe."); return; } project.UpdatedDate = QDate::currentDate().toString(Qt::ISODate); SaveProjects(); RefreshProjectList(); ShowStatus("Launched " + project.Name); } bool HasSelectedProject() const { return SelectedProjectIndex_ >= 0 && SelectedProjectIndex_ < static_cast(Projects_.size()); } int FindProjectIndexByName(const QString& name) const { for (int index = 0; index < static_cast(Projects_.size()); ++index) { if (Projects_[static_cast(index)].Name.compare(name, Qt::CaseInsensitive) == 0) { return index; } } return -1; } bool IsSafeProjectRootForDeletion(const std::filesystem::path& rootPath, QString& errorMessage) const { std::error_code error; const std::filesystem::path canonical = std::filesystem::weakly_canonical(rootPath, error); const std::filesystem::path checkedPath = error ? rootPath.lexically_normal() : canonical; if (!std::filesystem::is_directory(rootPath)) { errorMessage = "Selected project path is not a folder."; return false; } if (!IsProjectRoot(rootPath)) { errorMessage = "Selected folder does not contain MetaCore.project.json, so it will not be deleted."; return false; } if (checkedPath == checkedPath.root_path()) { errorMessage = "Refusing to delete a filesystem root."; return false; } if (checkedPath.parent_path() == checkedPath) { errorMessage = "Refusing to delete an invalid project path."; return false; } return true; } void ShowStatus(const QString& text) { if (StatusLabel_ == nullptr) { return; } StatusLabel_->setText(text); StatusLabel_->show(); } void ApplyStyleSheet(bool darkMode) { if (!darkMode) { qApp->setStyleSheet({}); return; } qApp->setStyleSheet(R"( QMainWindow#launcherWindow, QWidget#central { background: #191919; color: #cfcfcf; font-size: 13px; } QWidget#sidebar { background: #141414; border-right: 1px solid #222222; } QLabel#hubTitle { color: #f0f0f0; font-size: 22px; font-weight: 700; } QLabel#pageTitle { color: #f0f0f0; font-size: 24px; font-weight: 700; } QLabel#mutedText { color: #707070; } QLabel#errorText { color: #eb5757; } QLabel#projectName { color: #e5e5e5; font-weight: 600; } QLabel#projectAvatar { background: #333333; color: #f1f1f1; border-radius: 6px; font-size: 16px; font-weight: 700; } QPushButton { background: #202020; color: #cfcfcf; border: 1px solid #2f2f2f; border-radius: 4px; padding: 7px 12px; } QPushButton:hover { background: #2a2a2a; } QPushButton:pressed, QPushButton[selected="true"] { background: #333333; } QPushButton#primaryButton { background: #f1f1f1; color: #151515; border-color: #f1f1f1; } QPushButton#primaryButton:hover { background: #ffffff; } QPushButton#primaryButton:disabled { background: #555555; color: #9a9a9a; border-color: #555555; } QPushButton#dangerButton { background: #2a2020; color: #f2b7b7; border-color: #4a2d2d; } QPushButton#dangerButton:hover { background: #3a2424; } QPushButton#navButton { text-align: left; min-height: 36px; border: none; padding-left: 12px; background: #141414; } QPushButton#navButton:hover { background: #1e1e1e; } QPushButton#navButton[selected="true"] { background: #252525; } QPushButton#smallButton { padding: 5px 8px; } QLineEdit, QComboBox { background: #202020; color: #d8d8d8; border: 1px solid #2f2f2f; border-radius: 4px; padding: 8px 10px; } QLineEdit:focus, QComboBox:focus { border-color: #555555; } QLineEdit:read-only { color: #b8b8b8; } QFrame#projectCard, QFrame#installCard { background: #202020; border: 1px solid #2f2f2f; border-radius: 6px; } QFrame#projectCard:hover { background: #2a2a2a; } QFrame#projectCard[selected="true"] { background: #333333; border-color: #4a4a4a; } QScrollArea#projectScroll { background: transparent; } QScrollArea#projectScroll > QWidget > QWidget { background: transparent; } QDialog { background: #191919; color: #cfcfcf; } QMessageBox { background: #191919; color: #cfcfcf; } QCheckBox { color: #cfcfcf; } )"); } std::vector Projects_{}; int SelectedProjectIndex_ = -1; std::filesystem::path ExecutableDirectory_{}; std::filesystem::path DatabasePath_{}; QStackedWidget* Pages_ = nullptr; QPushButton* ProjectsNavButton_ = nullptr; QPushButton* InstallsNavButton_ = nullptr; QLineEdit* SearchEdit_ = nullptr; QLabel* StatusLabel_ = nullptr; QLabel* EditorInstallStatusLabel_ = nullptr; QLabel* PlayerInstallStatusLabel_ = nullptr; QLabel* CookToolInstallStatusLabel_ = nullptr; QWidget* ProjectListContainer_ = nullptr; QVBoxLayout* ProjectListLayout_ = nullptr; }; } // namespace int main(int argc, char* argv[]) { QApplication app(argc, argv); QApplication::setApplicationName(ApplicationName); QApplication::setOrganizationName(OrganizationName); QApplication::setApplicationVersion("1.0.0"); MetaCoreLauncherWindow window; window.show(); return app.exec(); }