diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cc12679e7..5e2ddb474 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -71,7 +71,7 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install binutils libgl1-mesa-dev zlib1g-dev libzstd-dev p7zip-full xorg-dev libopenal-dev + sudo apt install binutils libgl1-mesa-dev zlib1g-dev libzstd-dev p7zip-full xorg-dev libopenal-dev libgtk-3-dev - name: Install Qt run: | diff --git a/engine/engine.qbs b/engine/engine.qbs index 62fdfd1f9..8067e30ef 100644 --- a/engine/engine.qbs +++ b/engine/engine.qbs @@ -18,6 +18,7 @@ Project { "../thirdparty/next/inc/math", "../thirdparty/next/inc/core", "../thirdparty/next/inc/anim", + "../thirdparty/next/inc/os", "../thirdparty/physfs/src", "../thirdparty/glfw/include", "../thirdparty/glfm/include", diff --git a/engine/includes/editor/asseteditor.h b/engine/includes/editor/asseteditor.h index 3d9faf5c6..de0dcc461 100644 --- a/engine/includes/editor/asseteditor.h +++ b/engine/includes/editor/asseteditor.h @@ -32,6 +32,8 @@ class ENGINE_EXPORT AssetEditor : public QWidget { virtual StringList suffixes() const = 0; + virtual TString assetType() const = 0; + virtual StringList componentGroups() const; virtual QMenu *hierarchyContextMenu(Object *object); @@ -74,6 +76,8 @@ public slots: virtual void onPasteAction(); virtual void onNewAsset(); + virtual void onOpenAsset(); + virtual void onSave(); virtual void onSaveAs(); diff --git a/engine/src/editor/asseteditor.cpp b/engine/src/editor/asseteditor.cpp index 373850850..aa1a5e5a9 100644 --- a/engine/src/editor/asseteditor.cpp +++ b/engine/src/editor/asseteditor.cpp @@ -1,17 +1,19 @@ #include "editor/asseteditor.h" #include -#include #include +#include #include "editor/projectsettings.h" +#include "editor/assetmanager.h" #include "editor/undostack.h" AssetEditor::AssetEditor() : m_undoRedo(new UndoStack) { } + AssetEditor::~AssetEditor() { } @@ -21,6 +23,28 @@ void AssetEditor::onNewAsset() { m_undoRedo->clear(); } +void AssetEditor::onOpenAsset() { + FileDialog dialog; + + dialog.setMode(FileDialog::OpenFile); + dialog.setWindowTitle(TString("Save ") + assetType()); + + StringList list; + for(auto &it : suffixes()) { + list.push_back(TString("*.") + it); + } + + dialog.addFilter(assetType(), list); + dialog.setDirectory(ProjectSettings::instance()->contentPath()); + + if(dialog.exec()) { + TString path(dialog.getSelectedFile()); + if(!path.isEmpty()) { + loadAsset(AssetManager::instance()->fetchSettings(path)); + } + } +} + void AssetEditor::loadAsset(AssetConverterSettings *settings) { m_settings = { settings }; m_undoRedo->clear(); @@ -57,6 +81,10 @@ AssetEditor *AssetEditor::createInstance() { return nullptr; } +StringList AssetEditor::suffixes() const { + return StringList(); +} + std::list &AssetEditor::openedDocuments() { return m_settings; } @@ -130,30 +158,26 @@ void AssetEditor::onSaveAs() { } TString assetType(m_settings.front()->typeName()); - - std::map dictionary; + StringList list; for(auto &it : suffixes()) { - dictionary[assetType].push_back(it); + list.push_back(TString("*.") + it); } - StringList filter; - for(auto &it : dictionary) { - TString item = it.first + " ("; - for(auto &suffix : it.second) { - item += TString("*.") + suffix; - } - item += ")"; - filter.push_back(item); - } + FileDialog dialog; + + dialog.setMode(FileDialog::SaveFile); + dialog.setWindowTitle(TString("Save ") + assetType); + dialog.addFilter(assetType, list); + + dialog.setDirectory(ProjectSettings::instance()->contentPath()); - QString path(QFileDialog::getSaveFileName(nullptr, QString("Save ") + assetType.data(), - ProjectSettings::instance()->contentPath().data(), TString::join(filter, ";;").data())); - if(!path.isEmpty()) { - Url info(path.toStdString()); + if(dialog.exec()) { + TString path(dialog.getSelectedFile()); + Url info(path); if(info.suffix().isEmpty()) { - path += QString(".") + dictionary.begin()->second.front().data(); + path += TString(".") + AssetEditor::suffixes().front(); } - saveAsset(path.toStdString()); + saveAsset(path); } } diff --git a/modules/editor/motiontools/editor/animationedit.cpp b/modules/editor/motiontools/editor/animationedit.cpp index 778b8f496..45729ca0c 100644 --- a/modules/editor/motiontools/editor/animationedit.cpp +++ b/modules/editor/motiontools/editor/animationedit.cpp @@ -87,6 +87,10 @@ StringList AnimationEdit::suffixes() const { return result; } +TString AnimationEdit::assetType() const { + return "Animation"; +} + void AnimationEdit::onAddVariable(QAction *action) { TString name; Variant value; diff --git a/modules/editor/motiontools/editor/animationedit.h b/modules/editor/motiontools/editor/animationedit.h index 2b25697d3..8165b8ed4 100644 --- a/modules/editor/motiontools/editor/animationedit.h +++ b/modules/editor/motiontools/editor/animationedit.h @@ -54,6 +54,7 @@ private slots: void saveAsset(const TString &path) override; StringList suffixes() const override; + TString assetType() const override; void changeEvent(QEvent *event) override; diff --git a/modules/editor/particletools/editor/particleedit.cpp b/modules/editor/particletools/editor/particleedit.cpp index 6f64cb3b9..ffe69e7e8 100644 --- a/modules/editor/particletools/editor/particleedit.cpp +++ b/modules/editor/particletools/editor/particleedit.cpp @@ -151,6 +151,10 @@ StringList ParticleEdit::suffixes() const { return result; } +TString ParticleEdit::assetType() const { + return "Visual Effect"; +} + void ParticleEdit::onActivated() { ui->graph->reselect(); } diff --git a/modules/editor/particletools/editor/particleedit.h b/modules/editor/particletools/editor/particleedit.h index e47a21640..36dc9a0f8 100644 --- a/modules/editor/particletools/editor/particleedit.h +++ b/modules/editor/particletools/editor/particleedit.h @@ -59,6 +59,7 @@ private slots: void saveAsset(const TString &path) override; StringList suffixes() const override; + TString assetType() const override; void timerEvent(QTimerEvent *) override; void changeEvent(QEvent *event) override; diff --git a/modules/editor/pipelinetools/editor/pipelineedit.cpp b/modules/editor/pipelinetools/editor/pipelineedit.cpp index 882197bfa..4bbfe93ae 100644 --- a/modules/editor/pipelinetools/editor/pipelineedit.cpp +++ b/modules/editor/pipelinetools/editor/pipelineedit.cpp @@ -31,7 +31,7 @@ namespace { const char *gMeshRender("MeshRender"); const char *gDirectLight("DirectLight"); -}; +} class PipelineProxy : public Object { A_OBJECT(PipelineProxy, Object, Proxy) @@ -49,7 +49,7 @@ class PipelineProxy : public Object { } private: - PipelineEdit *m_editor = nullptr;; + PipelineEdit *m_editor = nullptr; }; PipelineEdit::PipelineEdit() : @@ -106,6 +106,10 @@ StringList PipelineEdit::suffixes() const { return static_cast(m_builder)->suffixes(); } +TString PipelineEdit::assetType() const { + return "Pipeline"; +} + void PipelineEdit::onActivated() { ui->schemeWidget->reselect(); } diff --git a/modules/editor/pipelinetools/editor/pipelineedit.h b/modules/editor/pipelinetools/editor/pipelineedit.h index 8fedcfa76..f56461bb7 100644 --- a/modules/editor/pipelinetools/editor/pipelineedit.h +++ b/modules/editor/pipelinetools/editor/pipelineedit.h @@ -45,6 +45,7 @@ private slots: void changeEvent(QEvent *event) override; StringList suffixes() const override; + TString assetType() const override; private: Ui::PipelineEdit *ui; diff --git a/modules/editor/shadertools/editor/materialedit.cpp b/modules/editor/shadertools/editor/materialedit.cpp index e9e15b68f..f7c7850e4 100644 --- a/modules/editor/shadertools/editor/materialedit.cpp +++ b/modules/editor/shadertools/editor/materialedit.cpp @@ -161,6 +161,10 @@ StringList MaterialEdit::suffixes() const { return {"mtl"}; // Only mtl format represented as node graph to edit } +TString MaterialEdit::assetType() const { + return "Material"; +} + void MaterialEdit::onActivated() { ui->schemeWidget->reselect(); } diff --git a/modules/editor/shadertools/editor/materialedit.h b/modules/editor/shadertools/editor/materialedit.h index 508aa92f9..379643682 100644 --- a/modules/editor/shadertools/editor/materialedit.h +++ b/modules/editor/shadertools/editor/materialedit.h @@ -55,6 +55,7 @@ private slots: void changeEvent(QEvent *event) override; StringList suffixes() const override; + TString assetType() const override; private: ShaderCodeDialog m_codeDlg; diff --git a/modules/editor/texteditor/editor/textedit.cpp b/modules/editor/texteditor/editor/textedit.cpp index 1f24a15a9..ff5e30635 100644 --- a/modules/editor/texteditor/editor/textedit.cpp +++ b/modules/editor/texteditor/editor/textedit.cpp @@ -76,6 +76,10 @@ StringList TextEdit::suffixes() const { return {"cpp", "h", "as", "txt", "json", "html", "htm", "xml", "shader", "vert", "frag"}; } +TString TextEdit::assetType() const { + return "Text Document"; +} + void TextEdit::on_actionFind_triggered() { ui->findWidget->show(); ui->lineFind->setFocus(); diff --git a/modules/editor/texteditor/editor/textedit.h b/modules/editor/texteditor/editor/textedit.h index 6a7b7a414..c63caac55 100644 --- a/modules/editor/texteditor/editor/textedit.h +++ b/modules/editor/texteditor/editor/textedit.h @@ -48,6 +48,7 @@ private slots: void cleanModified() const override; StringList suffixes() const override; + TString assetType() const override; Ui::TextEdit *ui; }; diff --git a/modules/editor/texteditor/editor/textwidget.cpp b/modules/editor/texteditor/editor/textwidget.cpp index 0d66afe37..82b75f62c 100644 --- a/modules/editor/texteditor/editor/textwidget.cpp +++ b/modules/editor/texteditor/editor/textwidget.cpp @@ -5,11 +5,7 @@ #include #include -#include -#include #include -#include -#include #include #include #include @@ -383,8 +379,6 @@ void TextWidget::paintEvent(QPaintEvent *event) { continue; } if(r.bottom() >= er.top() && r.top() <= er.bottom()) { - QTextBlockFormat blockFormat = block.blockFormat(); - QVector selections; int blpos = block.position(); diff --git a/modules/editor/texturetools/editor/spriteedit.cpp b/modules/editor/texturetools/editor/spriteedit.cpp index c5bcd5164..88f37d4b5 100644 --- a/modules/editor/texturetools/editor/spriteedit.cpp +++ b/modules/editor/texturetools/editor/spriteedit.cpp @@ -149,6 +149,10 @@ StringList SpriteEdit::suffixes() const { return static_cast(m_converter)->suffixes(); } +TString SpriteEdit::assetType() const { + return "Sprite"; +} + void SpriteEdit::onUpdateTemplate() { if(!m_settings.empty()) { m_converter->convertTexture(static_cast(m_settings.front())); diff --git a/modules/editor/texturetools/editor/spriteedit.h b/modules/editor/texturetools/editor/spriteedit.h index 54c3adcd4..7d33f9a09 100644 --- a/modules/editor/texturetools/editor/spriteedit.h +++ b/modules/editor/texturetools/editor/spriteedit.h @@ -34,6 +34,7 @@ class SpriteEdit : public AssetEditor { bool allowSaveAs() const override { return false; } StringList suffixes() const override; + TString assetType() const override; void changeEvent(QEvent *event) override; diff --git a/modules/uikit/includes/editor/uiedit.h b/modules/uikit/includes/editor/uiedit.h index 42106d423..b35f5a4f2 100644 --- a/modules/uikit/includes/editor/uiedit.h +++ b/modules/uikit/includes/editor/uiedit.h @@ -56,6 +56,7 @@ private slots: StringList suffixes() const override; StringList componentGroups() const override; + TString assetType() const override; void changeParent(const Object::ObjectList &objects, Object *parent, int position = -1) override; diff --git a/modules/uikit/src/editor/uiedit.cpp b/modules/uikit/src/editor/uiedit.cpp index 1c88a6ebb..59be0f660 100644 --- a/modules/uikit/src/editor/uiedit.cpp +++ b/modules/uikit/src/editor/uiedit.cpp @@ -105,6 +105,10 @@ StringList UiEdit::componentGroups() const { return {"Actor", "Components/UI"}; } +TString UiEdit::assetType() const { + return "UI Document"; +} + void UiEdit::changeParent(const Object::ObjectList &objects, Object *parent, int position) { m_undoRedo->push(new ParentWidget(objects, parent, position, m_controller)); } diff --git a/thirdparty/next/inc/os/filedialog.h b/thirdparty/next/inc/os/filedialog.h new file mode 100644 index 000000000..d3cc94d4f --- /dev/null +++ b/thirdparty/next/inc/os/filedialog.h @@ -0,0 +1,65 @@ +/* + This file is part of Thunder Next. + + Copyright 2008-2026 Evgeniy Prikazchikov + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#ifndef FILEDIALOG_H +#define FILEDIALOG_H + +#include +#include + +class FileDialogPrivate; + +class NEXT_LIBRARY_EXPORT FileDialog { +public: + enum Mode { + OpenFile, + SaveFile, + OpenDirectory, + OpenFiles + }; + + FileDialog(); + ~FileDialog(); + + FileDialog(const FileDialog&) = delete; + FileDialog &operator=(const FileDialog&) = delete; + + FileDialog(FileDialog&&) noexcept; + FileDialog &operator=(FileDialog&&) noexcept; + + void setMode(Mode m); + void setShowHidden(bool show); + void setDefaultSuffix(const TString &suffix); + void setWindowTitle(const TString &title); + + void addFilter(const TString &name, const StringList &extensions); + void clearFilters(); + + void setDirectory(const TString &dir); + + TString getSelectedFile() const; + StringList getSelectedFiles() const; + + bool exec(); + +private: + FileDialogPrivate *m_ptr; + +}; + +#endif // FILEDIALOG_H diff --git a/thirdparty/next/src/os/filedialog.cpp b/thirdparty/next/src/os/filedialog.cpp new file mode 100644 index 000000000..e28bcde37 --- /dev/null +++ b/thirdparty/next/src/os/filedialog.cpp @@ -0,0 +1,425 @@ +#include "os/filedialog.h" + +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +class FileDialogPrivate { +public: + FileDialogPrivate() : + m_mode(FileDialog::OpenFile), + m_showHidden(false) { + + } + + ~FileDialogPrivate() = default; + + bool exec(); + +private: + friend class FileDialog; + +#ifdef _WIN32 + bool execWindows(); + bool execWindowsFile(); + bool execWindowsDirectory(); +#endif + +#ifdef __APPLE__ + bool execMacOS(); +#endif + +#ifdef __linux__ + bool execLinux(); +#endif + + struct Filter { + TString name; + StringList extensions; + }; + + std::vector m_filters; + TString m_defaultSuffix; + TString m_windowTitle; + TString m_initialDir; + StringList m_selectedFiles; + + FileDialog::Mode m_mode; + + bool m_showHidden; + +}; + +bool FileDialogPrivate::exec() { + m_selectedFiles.clear(); + +#ifdef _WIN32 + return execWindows(); +#elif __APPLE__ + return execMacOS(); +#elif __linux__ + return execLinux(); +#else +#error "Unsupported platform" +#endif +} + +#ifdef _WIN32 +#include +#include +#include +#include +#pragma comment(lib, "shell32.lib") +#pragma comment(lib, "comdlg32.lib") +#pragma comment(lib, "User32.lib") +#pragma comment(lib, "Ole32.lib") + +bool FileDialogPrivate::execWindows() { + if(m_mode == FileDialog::OpenDirectory) { + return execWindowsDirectory(); + } + return execWindowsFile(); +} + +bool FileDialogPrivate::execWindowsFile() { + std::string filterStr; + for(const auto &f : m_filters) { + filterStr += f.name.toStdString() + '\0'; + + std::string patterns; + for(const auto& ext : f.extensions) { + if (!patterns.empty()) patterns += ";"; + patterns += "*" + ext.toStdString(); + } + if(patterns.empty()) patterns = "*.*"; + filterStr += patterns + '\0'; + } + if(m_filters.empty()) { + filterStr = "All Files\0*.*\0"; + } + + OPENFILENAMEA ofn = {}; + char fileName[MAX_PATH] = {}; + + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = GetActiveWindow(); + ofn.lpstrFile = fileName; + ofn.nMaxFile = MAX_PATH; + ofn.lpstrFilter = filterStr.c_str(); + ofn.nFilterIndex = 1; + ofn.lpstrInitialDir = m_initialDir.isEmpty() ? nullptr : m_initialDir.toStdString().c_str(); + ofn.lpstrTitle = m_windowTitle.isEmpty() ? nullptr : m_windowTitle.toStdString().c_str(); + + DWORD flags = OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR; + if(m_mode == FileDialog::SaveFile) { + flags |= OFN_OVERWRITEPROMPT; + } else { + flags |= OFN_FILEMUSTEXIST; + } + + if(m_mode == FileDialog::OpenFiles) { + flags |= OFN_ALLOWMULTISELECT | OFN_EXPLORER; + } + + ofn.Flags = flags; + + BOOL result; + if(m_mode == FileDialog::SaveFile) { + result = GetSaveFileNameA(&ofn); + } else { + result = GetOpenFileNameA(&ofn); + } + + if(!result) { + DWORD err = CommDlgExtendedError(); + if(err != 0) { + + } + return false; + } + + if(m_mode == FileDialog::OpenFiles && (ofn.Flags & OFN_ALLOWMULTISELECT)) { + char* ptr = fileName; + fs::path baseDir(ptr); + ptr += strlen(ptr) + 1; + + if(*ptr == '\0') { + m_selectedFiles.push_back(TString(baseDir.string())); + } else { + while (*ptr != '\0') { + m_selectedFiles.push_back(TString((baseDir / fs::path(ptr)).string())); + ptr += strlen(ptr) + 1; + } + } + } else { + m_selectedFiles.push_back(TString(fileName)); + } + + return true; +} + +bool FileDialogPrivate::execWindowsDirectory() { + BROWSEINFOA bi = {}; + char path[MAX_PATH] = {}; + + bi.hwndOwner = GetActiveWindow(); + bi.pszDisplayName = path; + bi.lpszTitle = m_windowTitle.isEmpty() ? "Select Directory" : m_windowTitle.toStdString().c_str(); + bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE; + bi.lpfn = nullptr; + + LPITEMIDLIST pidl = SHBrowseForFolderA(&bi); + if (pidl != nullptr) { + if (SHGetPathFromIDListA(pidl, path)) { + m_selectedFiles.push_back(TString(path)); + CoTaskMemFree(pidl); + return true; + } + CoTaskMemFree(pidl); + } + return false; +} +#endif // _WIN32 + +#ifdef __APPLE__ +#include +#include +#include +#include + +bool FileDialogPrivate::execMacOS() { + Class panelClass; + id panel; + + if(m_mode == FileDialog::SaveFile) { + panelClass = objc_getClass("NSSavePanel"); + panel = objc_msgSend(panelClass, sel_getUid("savePanel")); + } else if(m_mode == FileDialog::OpenDirectory) { + panelClass = objc_getClass("NSOpenPanel"); + panel = objc_msgSend(panelClass, sel_getUid("openPanel")); + objc_msgSend(panel, sel_getUid("setCanChooseDirectories:"), YES); + objc_msgSend(panel, sel_getUid("setCanChooseFiles:"), NO); + } else { + panelClass = objc_getClass("NSOpenPanel"); + panel = objc_msgSend(panelClass, sel_getUid("openPanel")); + objc_msgSend(panel, sel_getUid("setCanChooseDirectories:"), NO); + objc_msgSend(panel, sel_getUid("setCanChooseFiles:"), YES); + + if(m_mode == FileDialog::OpenFiles) { + objc_msgSend(panel, sel_getUid("setAllowsMultipleSelection:"), YES); + } + } + + if(!m_windowTitle.isEmpty()) { + NSString *title = objc_msgSend(objc_getClass("NSString"), sel_getUid("stringWithUTF8String:"), m_windowTitle.data()); + objc_msgSend(panel, sel_getUid("setTitle:"), title); + } + + if(!m_initialDir.isEmpty()) { + NSString* path = objc_msgSend(objc_getClass("NSString"), + sel_getUid("stringWithUTF8String:"), m_initialDir.data()); + NSURL* url = objc_msgSend(objc_getClass("NSURL"), + sel_getUid("fileURLWithPath:"), path); + objc_msgSend(panel, sel_getUid("setDirectoryURL:"), url); + } + + if(!m_filters.isEmpty()) { + NSMutableArray* allowedTypes = objc_msgSend(objc_getClass("NSMutableArray"), sel_getUid("array")); + + for(const auto& filter : m_filters) { + for(const auto& ext : filter.extensions) { + if(!ext.empty()) { + std::string extStr = ext.toStdString(); + if(!extStr.empty() && extStr[0] == '.') { + extStr = extStr.substr(1); + } + NSString* extNSString = objc_msgSend(objc_getClass("NSString"), + sel_getUid("stringWithUTF8String:"), extStr.c_str()); + objc_msgSend(allowedTypes, sel_getUid("addObject:"), extNSString); + } + } + } + + if(objc_msgSend(allowedTypes, sel_getUid("count")) > 0) { + objc_msgSend(panel, sel_getUid("setAllowedFileTypes:"), allowedTypes); + } + } + + NSInteger result = (NSInteger)objc_msgSend(panel, sel_getUid("runModal")); + + if(result == NSModalResponseOK) { + id urls = objc_msgSend(panel, sel_getUid("URLs")); + NSUInteger count = (NSUInteger)objc_msgSend(urls, sel_getUid("count")); + + for(NSUInteger i = 0; i < count; i++) { + id url = objc_msgSend(urls, sel_getUid("objectAtIndex:"), i); + id path = objc_msgSend(url, sel_getUid("path")); + const char* cpath = objc_msgSend(path, sel_getUid("UTF8String")); + selectedFiles.push_back(TString(cpath)); + } + return true; + } + + return false; +} +#endif // __APPLE__ + +#ifdef __linux__ +#include +#include +#include + +bool FileDialogPrivate::execLinux() { + static bool gtkInitialized = false; + if(!gtkInitialized) { + gtk_init(nullptr, nullptr); + gtkInitialized = true; + } + + GtkWidget *dialog; + GtkFileChooserAction action; + const char *buttonText; + + switch(m_mode) { + case SaveFile: + action = GTK_FILE_CHOOSER_ACTION_SAVE; + buttonText = "_Save"; + break; + case OpenDirectory: + action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER; + buttonText = "_Open"; + break; + default: + action = GTK_FILE_CHOOSER_ACTION_OPEN; + buttonText = "_Open"; + break; + } + + dialog = gtk_file_chooser_dialog_new( + m_windowTitle.empty() ? nullptr : m_windowTitle.data(), + nullptr, + action, + "_Cancel", GTK_RESPONSE_CANCEL, + buttonText, GTK_RESPONSE_ACCEPT, + nullptr + ); + + if(m_mode == FileDialog::OpenFiles) { + gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), TRUE); + } + + if(!m_initialDir.isEmpty()) { + gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), m_initialDir.data()); + } + + GtkFileFilter *allFilter = nullptr; + for(const auto &filter : m_filters) { + GtkFileFilter* gtkFilter = gtk_file_filter_new(); + gtk_file_filter_set_name(gtkFilter, filter.name.data()); + + for(const auto &ext : filter.extensions) { + std::string pattern = "*" + ext.toStdString(); + gtk_file_filter_add_pattern(gtkFilter, pattern.c_str()); + } + + if(filter.extensions.empty()) { + gtk_file_filter_add_pattern(gtkFilter, "*"); + } + + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), gtkFilter); + + if(filter.name == "All Files" || filter.extensions.empty()) { + allFilter = gtkFilter; + } + } + + if(m_filters.isEmpty()) { + GtkFileFilter* gtkFilter = gtk_file_filter_new(); + gtk_file_filter_set_name(gtkFilter, "All Files"); + gtk_file_filter_add_pattern(gtkFilter, "*"); + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), gtkFilter); + allFilter = gtkFilter; + } + + if(allFilter) { + gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(dialog), allFilter); + } + + int response = gtk_dialog_run(GTK_DIALOG(dialog)); + + if(response == GTK_RESPONSE_ACCEPT) { + GSList *filenames = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog)); + + for(GSList *iter = filenames; iter != nullptr; iter = iter->next) { + char *filename = (char*)iter->data; + selectedFiles.push_back(TString(filename)); + g_free(filename); + } + + g_slist_free(filenames); + gtk_widget_destroy(dialog); + return true; + } + + gtk_widget_destroy(dialog); + return false; +} +#endif // __linux__ + +FileDialog::FileDialog() : + m_ptr(new FileDialogPrivate) { + +} + +FileDialog::~FileDialog() { + delete m_ptr; +} + +FileDialog::FileDialog(FileDialog&&) noexcept = default; +FileDialog& FileDialog::operator=(FileDialog&&) noexcept = default; + +void FileDialog::setMode(Mode m) { + m_ptr->m_mode = m; +} + +void FileDialog::setShowHidden(bool show) { + m_ptr->m_showHidden = show; +} + +void FileDialog::setDefaultSuffix(const TString &suffix) { + m_ptr->m_defaultSuffix = suffix; +} + +void FileDialog::setWindowTitle(const TString &title) { + m_ptr->m_windowTitle = title; +} + +void FileDialog::addFilter(const TString &name, const StringList &extensions) { + m_ptr->m_filters.push_back({name, extensions}); +} + +void FileDialog::clearFilters() { + m_ptr->m_filters.clear(); +} + +void FileDialog::setDirectory(const TString &dir) { + if(fs::exists(dir.toStdString()) && fs::is_directory(dir.toStdString())) { + m_ptr->m_initialDir = dir; + } +} + +TString FileDialog::getSelectedFile() const { + return m_ptr->m_selectedFiles.empty() ? TString() : m_ptr->m_selectedFiles.front(); +} + +StringList FileDialog::getSelectedFiles() const { + return m_ptr->m_selectedFiles; +} + +bool FileDialog::exec() { + return m_ptr->exec(); +} diff --git a/worldeditor/src/main.cpp b/worldeditor/src/main.cpp index ebb1f33c8..3c4997f17 100644 --- a/worldeditor/src/main.cpp +++ b/worldeditor/src/main.cpp @@ -48,9 +48,9 @@ int main(int argc, char *argv[]) { Engine engine; Engine::setPlatformAdaptor(&EditorPlatform::instance()); - QString project; + TString project; if(argc > 1) { - project = QApplication::arguments().at(1); + project = QApplication::arguments().at(1).toStdString(); } else { ProjectBrowser browser; if(browser.exec() == QDialog::Accepted) { diff --git a/worldeditor/src/main/documentmodel.cpp b/worldeditor/src/main/documentmodel.cpp index e73f2f093..6fca4bd6d 100644 --- a/worldeditor/src/main/documentmodel.cpp +++ b/worldeditor/src/main/documentmodel.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include DocumentModel::DocumentModel() { for(auto &it : PluginManager::instance()->extensions("editor")) { @@ -50,6 +50,13 @@ void DocumentModel::newFile(AssetEditor *editor) { } } +void DocumentModel::openFile(AssetEditor *editor) { + if(editor->checkSave()) { + closeFile(editor); + editor->onOpenAsset(); + } +} + AssetEditor *DocumentModel::openFile(const TString &path) { QDir dir(ProjectSettings::instance()->contentPath().data()); AssetConverterSettings *settings = AssetManager::instance()->fetchSettings(dir.absoluteFilePath(path.data()).toStdString()); diff --git a/worldeditor/src/main/documentmodel.h b/worldeditor/src/main/documentmodel.h index 319d59a72..97183faa9 100644 --- a/worldeditor/src/main/documentmodel.h +++ b/worldeditor/src/main/documentmodel.h @@ -18,6 +18,7 @@ class DocumentModel : public QObject { void addEditor(AssetEditor *editor); void newFile(AssetEditor *editor); + void openFile(AssetEditor *editor); AssetEditor *openFile(const TString &path); diff --git a/worldeditor/src/main/mainwindow.cpp b/worldeditor/src/main/mainwindow.cpp index 4f2a58c8e..a8d8bd297 100644 --- a/worldeditor/src/main/mainwindow.cpp +++ b/worldeditor/src/main/mainwindow.cpp @@ -1,14 +1,15 @@ #include "mainwindow.h" #include "ui_mainwindow.h" +#include #include -#include #include #include #include #include #include +#include #include @@ -213,17 +214,11 @@ void MainWindow::closeEvent(QCloseEvent *event) { } void MainWindow::on_actionNew_triggered() { - m_documentModel->newFile(m_mainEditor); - - m_currentEditor->undoRedo()->clear(); + m_documentModel->newFile(m_currentEditor); } void MainWindow::on_actionOpen_triggered() { - QString path = QFileDialog::getOpenFileName(this, tr("Open Scene"), - m_projectSettings->contentPath().data(), "*.map"); - if(!path.isEmpty()) { - m_documentModel->openFile(path.toStdString()); - } + m_documentModel->openFile(m_currentEditor); } void MainWindow::on_actionSave_triggered() { @@ -292,9 +287,9 @@ void MainWindow::setGameMode(bool mode) { Engine::setGameMode(mode); } -void MainWindow::onOpenProject(const QString &path) { - ProjectModel::addProject(path.toStdString()); - m_projectSettings->init(path.toStdString()); +void MainWindow::onOpenProject(const TString &path) { + ProjectModel::addProject(path); + m_projectSettings->init(path); m_projectSettings->loadSettings(); PluginManager::instance()->init(m_engine); @@ -459,22 +454,27 @@ void MainWindow::onToolWindowVisibilityChanged(QWidget *toolWindow, bool visible } void MainWindow::on_actionSave_Workspace_triggered() { - QString path = QFileDialog::getSaveFileName(this, - tr("Save Workspace"), - "workspaces", - tr("Workspaces (*.ws)") ); - if(path.length() > 0) { - QFile file(path); - if(file.open(QFile::WriteOnly)) { - QVariantMap layout; - layout[gWindows] = ui->toolWidget->saveState(); - - QByteArray data; - QDataStream ds(&data, QFile::WriteOnly); - ds << layout; - - file.write(data); - file.close(); + FileDialog dialog; + dialog.setDirectory(ProjectSettings::instance()->templatePath() + "/workspaces"); + dialog.setWindowTitle("Save Workspace"); + dialog.setMode(FileDialog::SaveFile); + dialog.addFilter("Workspaces", { "*.ws" }); + + if(dialog.exec()) { + TString path = dialog.getSelectedFile(); + if(!path.isEmpty()) { + QFile file(path.data()); + if(file.open(QFile::WriteOnly)) { + QVariantMap layout; + layout[gWindows] = ui->toolWidget->saveState(); + + QByteArray data; + QDataStream ds(&data, QFile::WriteOnly); + ds << layout; + + file.write(data); + file.close(); + } } } } @@ -588,12 +588,16 @@ void MainWindow::onCopyPasteChanged() { void MainWindow::on_menuFile_aboutToShow() { QString name; - if(m_currentEditor && m_currentEditor != m_mainEditor) { + QString type; + if(m_currentEditor) { AssetConverterSettings *settings = m_currentEditor->openedDocuments().front(); name = QString(" \"%1\"").arg(settings->source().data()); + type = QString(" %1").arg(settings->typeName().data()); ui->actionSave_As->setEnabled(m_currentEditor->allowSaveAs()); } + ui->actionNew->setText(tr("New%1").arg(type)); + ui->actionOpen->setText(tr("Open%1").arg(type)); ui->actionSave->setText(tr("Save%1").arg(name)); ui->actionSave_As->setText(tr("Save%1 As...").arg(name)); } @@ -631,22 +635,25 @@ void MainWindow::on_actionExit_triggered() { } void MainWindow::build(QString platform) { - QString dir = QFileDialog::getExistingDirectory(nullptr, tr("Select Target Directory"), - "", - QFileDialog::ShowDirsOnly | - QFileDialog::DontResolveSymlinks); - - if(!dir.isEmpty()) { - QStringList args; - args << "-s" << m_projectSettings->projectPath().data() << "-t" << dir; - - if(!platform.isEmpty()) { - args << "-p" << platform; - } + FileDialog dialog; + dialog.setDirectory(""); + dialog.setWindowTitle("Select Target Directory"); + dialog.setMode(FileDialog::OpenDirectory); + + if(dialog.exec()) { + TString dir = dialog.getSelectedFile(); + if(!dir.isEmpty()) { + QStringList args; + args << "-s" << m_projectSettings->projectPath().data() << "-t" << dir.data(); + + if(!platform.isEmpty()) { + args << "-p" << platform; + } - m_builder->start("Builder", args); - if(!m_builder->waitForStarted()) { - aError() << qPrintable(m_builder->errorString()); + m_builder->start("Builder", args); + if(!m_builder->waitForStarted()) { + aError() << qPrintable(m_builder->errorString()); + } } } } diff --git a/worldeditor/src/main/mainwindow.h b/worldeditor/src/main/mainwindow.h index 8039d4d0e..efb54d0a9 100644 --- a/worldeditor/src/main/mainwindow.h +++ b/worldeditor/src/main/mainwindow.h @@ -43,7 +43,7 @@ class MainWindow : public QMainWindow { public slots: void onOpenEditor(const TString &path); - void onOpenProject(const QString &path); + void onOpenProject(const TString &path); signals: void readBuildLogs(QString log); diff --git a/worldeditor/src/main/mainwindow.ui b/worldeditor/src/main/mainwindow.ui index ffe37ff83..262114e2a 100644 --- a/worldeditor/src/main/mainwindow.ui +++ b/worldeditor/src/main/mainwindow.ui @@ -25,7 +25,7 @@ - Qt::LeftToRight + Qt::LayoutDirection::LeftToRight @@ -67,7 +67,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -108,7 +108,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -134,7 +134,7 @@ 0 0 1280 - 21 + 22 @@ -392,10 +392,10 @@ false - &New Scene + &New - New Scene + New Ctrl+N @@ -420,10 +420,10 @@ false - &Open Scene + &Open - Open Scene + Open Ctrl+O diff --git a/worldeditor/src/managers/plugindialog/plugindialog.cpp b/worldeditor/src/managers/plugindialog/plugindialog.cpp index cf2fc09dd..8c434c956 100644 --- a/worldeditor/src/managers/plugindialog/plugindialog.cpp +++ b/worldeditor/src/managers/plugindialog/plugindialog.cpp @@ -1,4 +1,5 @@ -#include +#include "plugindialog.h" + #include #include #include @@ -7,7 +8,7 @@ #include "ui_plugindialog.h" -#include "plugindialog.h" +#include #include #include @@ -168,14 +169,17 @@ PluginDialog::~PluginDialog() { } void PluginDialog::on_loadButton_clicked() { - QDir dir = QDir(QDir::currentPath()); - QString path = QFileDialog::getOpenFileName(this, - tr("Please select Thunder Engine Mod"), - dir.absolutePath(), - tr("Mods (*.dll *.mod)") ); - if(!path.isEmpty()) { - PluginManager *model = PluginManager::instance(); - model->loadPlugin(dir.relativeFilePath(path).toStdString()); + FileDialog dialog; + dialog.setDirectory(ProjectSettings::instance()->pluginsPath()); + dialog.setWindowTitle("Please select Thunder Engine Plugin"); + dialog.setMode(FileDialog::OpenFile); + dialog.addFilter("Plugins", { "*.dll" }); + + if(dialog.exec()) { + TString path = dialog.getSelectedFile(); + if(!path.isEmpty()) { + PluginManager::instance()->loadPlugin(path); + } } } diff --git a/worldeditor/src/managers/plugindialog/plugindialog.h b/worldeditor/src/managers/plugindialog/plugindialog.h index 0942ec8f2..840b27ad4 100644 --- a/worldeditor/src/managers/plugindialog/plugindialog.h +++ b/worldeditor/src/managers/plugindialog/plugindialog.h @@ -3,8 +3,6 @@ #include -#include - class PluginManager; class QSortFilterProxyModel; diff --git a/worldeditor/src/screens/contentbrowser/contentbrowser.cpp b/worldeditor/src/screens/contentbrowser/contentbrowser.cpp index 68d1a3628..b8b8e48f7 100644 --- a/worldeditor/src/screens/contentbrowser/contentbrowser.cpp +++ b/worldeditor/src/screens/contentbrowser/contentbrowser.cpp @@ -10,13 +10,13 @@ #include #include #include -#include #include #include #include "config.h" #include +#include #include "contenttree.h" #include "commitrevert.h" @@ -518,17 +518,23 @@ void ContentBrowser::on_contentList_clicked(const QModelIndex &index) { } void ContentBrowser::importAsset() { - QStringList files = QFileDialog::getOpenFileNames(this, - tr("Select files to import"), - QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), - tr("All (*.*)") ); + FileDialog dialog; + dialog.setDirectory(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation).toStdString()); + dialog.setWindowTitle("Select files to import"); + dialog.setMode(FileDialog::OpenFiles); + dialog.addFilter("All", { "*.*" }); + + StringList files; + if(dialog.exec()) { + files = dialog.getSelectedFiles(); + } const QModelIndex origin = m_listProxy->mapToSource(ui->contentList->rootIndex()); TString target = ProjectSettings::instance()->contentPath() + "/" + ContentTree::instance()->path(origin); foreach(auto &it, files) { - AssetManager::instance()->import(it.toStdString(), target); + AssetManager::instance()->import(it, target); } } diff --git a/worldeditor/src/screens/projectbrowser/projectbrowser.cpp b/worldeditor/src/screens/projectbrowser/projectbrowser.cpp index bb0ebdadd..5c10f01d5 100644 --- a/worldeditor/src/screens/projectbrowser/projectbrowser.cpp +++ b/worldeditor/src/screens/projectbrowser/projectbrowser.cpp @@ -5,10 +5,10 @@ #include "editor/projectsettings.h" #include -#include #include #include +#include #include "config.h" @@ -31,14 +31,23 @@ ProjectBrowser::~ProjectBrowser() { } void ProjectBrowser::onNewProject() { - m_projectPath = QFileDialog::getSaveFileName(this, tr("Create New Project"), - ProjectSettings::instance()->myProjectsPath().data(), (TString("*") + gProjectExt).data()); + FileDialog dialog; + dialog.setDirectory(ProjectSettings::instance()->myProjectsPath()); + dialog.setWindowTitle("Create New Project"); + dialog.setMode(FileDialog::SaveFile); + dialog.addFilter("Project Files", { TString("*") + gProjectExt }); + + m_projectPath.clear(); + if(dialog.exec()) { + m_projectPath = dialog.getSelectedFile(); + } + if(!m_projectPath.isEmpty()) { - Url info(m_projectPath.toStdString()); + Url info(m_projectPath); if(info.suffix().isEmpty()) { m_projectPath += gProjectExt; } - File file(m_projectPath.toStdString()); + File file(m_projectPath); if(file.open(File::Write)) { file.close(); @@ -48,8 +57,17 @@ void ProjectBrowser::onNewProject() { } void ProjectBrowser::onImportProject() { - m_projectPath = QFileDialog::getOpenFileName(this, tr("Import Existing Project"), - ProjectSettings::instance()->myProjectsPath().data(), (TString("*") + gProjectExt).data()); + FileDialog dialog; + dialog.setDirectory(ProjectSettings::instance()->myProjectsPath()); + dialog.setWindowTitle("Import Existing Project"); + dialog.setMode(FileDialog::OpenFile); + dialog.addFilter("Project Files", { TString("*") + gProjectExt }); + + m_projectPath.clear(); + if(dialog.exec()) { + m_projectPath = dialog.getSelectedFile(); + } + if(!m_projectPath.isEmpty()) { done(QDialog::Accepted); } @@ -63,7 +81,7 @@ void ProjectBrowser::onOpenProject() { } void ProjectBrowser::onProjectSelected(const QModelIndex &index) { - m_projectPath = ui->listView->model()->data(index, Qt::EditRole).toString(); + m_projectPath = ui->listView->model()->data(index, Qt::EditRole).toString().toStdString(); if(!m_projectPath.isEmpty()) { done(QDialog::Accepted); } diff --git a/worldeditor/src/screens/projectbrowser/projectbrowser.h b/worldeditor/src/screens/projectbrowser/projectbrowser.h index 9579631c0..c143d30f0 100644 --- a/worldeditor/src/screens/projectbrowser/projectbrowser.h +++ b/worldeditor/src/screens/projectbrowser/projectbrowser.h @@ -3,6 +3,8 @@ #include +#include + namespace Ui { class ProjectBrowser; } @@ -14,7 +16,7 @@ class ProjectBrowser : public QDialog { explicit ProjectBrowser(QWidget *parent = nullptr); ~ProjectBrowser(); - QString projectPath() const { return m_projectPath; } + TString projectPath() const { return m_projectPath; } private: void onNewProject(); @@ -28,7 +30,7 @@ class ProjectBrowser : public QDialog { private: Ui::ProjectBrowser *ui; - QString m_projectPath; + TString m_projectPath; }; #endif // PROJECTBROWSER_H diff --git a/worldeditor/src/screens/propertyedit/custom/filepath/pathedit.cpp b/worldeditor/src/screens/propertyedit/custom/filepath/pathedit.cpp index 4cc13920f..a57726141 100644 --- a/worldeditor/src/screens/propertyedit/custom/filepath/pathedit.cpp +++ b/worldeditor/src/screens/propertyedit/custom/filepath/pathedit.cpp @@ -1,10 +1,9 @@ #include "pathedit.h" #include "ui_pathedit.h" -#include - #include #include +#include #include @@ -32,23 +31,22 @@ void PathEdit::setData(const Variant &data) { void PathEdit::onFileDialog() { Url url(m_path); - QString path; - QString dir(url.dir().isEmpty() ? ProjectSettings::instance()->contentPath().data() : url.absoluteDir().data()); + FileDialog dialog; + dialog.setDirectory(url.dir().isEmpty() ? ProjectSettings::instance()->contentPath() : url.absoluteDir()); if(!m_file) { - path = QFileDialog::getExistingDirectory(dynamic_cast(parent()), - tr("Open Directory"), - dir, - QFileDialog::ShowDirsOnly); + dialog.setWindowTitle("Open Directory"); + dialog.setMode(FileDialog::OpenDirectory); } else { - path = QFileDialog::getOpenFileName(dynamic_cast(parent()), - tr("Select File"), - dir, - tr("All Files (*)")); + dialog.setWindowTitle("Select File"); + dialog.setMode(FileDialog::OpenFile); } - if(path.length() > 0) { - setData(TString(path.toStdString())); + if(dialog.exec()) { + TString path(dialog.getSelectedFile()); + if(!path.isEmpty()) { + setData(path); + } } } diff --git a/worldeditor/src/screens/scenecomposer/scenecomposer.cpp b/worldeditor/src/screens/scenecomposer/scenecomposer.cpp index 82ce00814..31e0d547c 100644 --- a/worldeditor/src/screens/scenecomposer/scenecomposer.cpp +++ b/worldeditor/src/screens/scenecomposer/scenecomposer.cpp @@ -2,7 +2,6 @@ #include "ui_scenecomposer.h" #include -#include #include #include #include @@ -13,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -493,6 +493,10 @@ StringList SceneComposer::componentGroups() const { return {"Actor", "Components"}; } +TString SceneComposer::assetType() const { + return "Scene"; +} + void SceneComposer::changeParent(const Object::ObjectList &objects, Object *parent, int position) { m_undoRedo->push(new ParentObjects(objects, parent, position, m_controller)); } @@ -856,14 +860,22 @@ void SceneComposer::saveScene(const TString &path, Scene *scene) { void SceneComposer::saveSceneAs(Scene *scene) { if(scene) { - QString path = QFileDialog::getSaveFileName(nullptr, tr("Save Scene"), - ProjectSettings::instance()->contentPath().data(), - "Map (*.map)"); + FileDialog dialog; + dialog.setDirectory(ProjectSettings::instance()->contentPath()); + dialog.setWindowTitle("Save Scene"); + dialog.setMode(FileDialog::SaveFile); + dialog.addFilter("Map", { TString("*.map") }); + + TString path; + if(dialog.exec()) { + path = dialog.getSelectedFile(); + } + if(!path.isEmpty()) { - Url info(path.toStdString()); + Url info(path); scene->setName(info.baseName()); - saveScene(path.toStdString(), scene); - AssetConverterSettings *settings = AssetManager::instance()->fetchSettings(path.toStdString()); + saveScene(path, scene); + AssetConverterSettings *settings = AssetManager::instance()->fetchSettings(path); m_sceneSettings[scene->uuid()] = settings; } } diff --git a/worldeditor/src/screens/scenecomposer/scenecomposer.h b/worldeditor/src/screens/scenecomposer/scenecomposer.h index f06ce456e..e2394f23f 100644 --- a/worldeditor/src/screens/scenecomposer/scenecomposer.h +++ b/worldeditor/src/screens/scenecomposer/scenecomposer.h @@ -105,8 +105,8 @@ private slots: void cleanModified() const override; StringList suffixes() const override; - StringList componentGroups() const override; + TString assetType() const override; void changeParent(const Object::ObjectList &objects, Object *parent, int position = -1) override; diff --git a/worldeditor/worldeditor.qbs b/worldeditor/worldeditor.qbs index 32b4a1cf8..f2d6fd69b 100644 --- a/worldeditor/worldeditor.qbs +++ b/worldeditor/worldeditor.qbs @@ -20,7 +20,8 @@ Project { "../thirdparty/next/inc", "../thirdparty/next/inc/math", "../thirdparty/next/inc/core", - "../thirdparty/next/inc/anim" + "../thirdparty/next/inc/anim", + "../thirdparty/next/inc/os", ] QtGuiApplication {