From 1372b66d058422beb5e043d2bfd73479b64a75fb Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Tue, 28 Apr 2020 21:31:52 +0100 Subject: [PATCH] Add moved files missing from previous commit --- libs/wxutil/EntityClassChooser.cpp | 446 +++++++++++++++++++++++++++++ libs/wxutil/EntityClassChooser.h | 115 ++++++++ 2 files changed, 561 insertions(+) create mode 100644 libs/wxutil/EntityClassChooser.cpp create mode 100644 libs/wxutil/EntityClassChooser.h diff --git a/libs/wxutil/EntityClassChooser.cpp b/libs/wxutil/EntityClassChooser.cpp new file mode 100644 index 0000000000..fc5535f012 --- /dev/null +++ b/libs/wxutil/EntityClassChooser.cpp @@ -0,0 +1,446 @@ +#include "EntityClassChooser.h" +#include "TreeModel.h" +#include "VFSTreePopulator.h" + +#include "i18n.h" +#include "imainframe.h" +#include "iuimanager.h" +#include "gamelib.h" + +#include + +#include +#include +#include + +#include "string/string.h" +#include "eclass.h" + +#include "debugging/ScopedDebugTimer.h" + +namespace wxutil +{ + +namespace +{ + const char* const ECLASS_CHOOSER_TITLE = N_("Create entity"); + const std::string RKEY_SPLIT_POS = "user/ui/entityClassChooser/splitPos"; + + const char* const FOLDER_ICON = "folder16.png"; + const char* const ENTITY_ICON = "cmenu_add_entity.png"; + + // Registry XPath to lookup key that specifies the display folder + const char* const FOLDER_KEY_PATH = "/entityChooser/displayFolderKey"; +} + +/* + * EntityClassVisitor which populates a Gtk::TreeStore with entity classnames + * taking account of display folders and mod names. + */ +class EntityClassTreePopulator: + public wxutil::VFSTreePopulator, + public EntityClassVisitor +{ + // TreeStore to populate + wxutil::TreeModel::Ptr _store; + + // Column definition + const wxutil::EntityClassChooser::TreeColumns& _columns; + + // Key that specifies the display folder + std::string _folderKey; + + wxIcon _folderIcon; + wxIcon _entityIcon; + +public: + + // Constructor + EntityClassTreePopulator(wxutil::TreeModel::Ptr store, + const EntityClassChooser::TreeColumns& columns) + : wxutil::VFSTreePopulator(store), + _store(store), + _columns(columns), + _folderKey(game::current::getValue(FOLDER_KEY_PATH)) + { + _folderIcon.CopyFromBitmap(wxArtProvider::GetBitmap(GlobalUIManager().ArtIdPrefix() + FOLDER_ICON)); + _entityIcon.CopyFromBitmap(wxArtProvider::GetBitmap(GlobalUIManager().ArtIdPrefix() + ENTITY_ICON)); + } + + // EntityClassVisitor implementation + void visit(const IEntityClassPtr& eclass) + { + std::string folderPath = eclass->getAttribute(_folderKey).getValue(); + + if (!folderPath.empty()) + { + folderPath = "/" + folderPath; + } + + // Create the folder to put this EntityClass in, depending on the value + // of the DISPLAY_FOLDER_KEY. + addPath( + eclass->getModName() + folderPath + "/" + eclass->getName(), + [this](wxutil::TreeModel::Row& row, const std::string& path, + bool isFolder) + { + // Get the display name by stripping off everything before the + // last slash + row[_columns.name] = wxVariant( + wxDataViewIconText(path.substr(path.rfind("/") + 1), + !isFolder ? _entityIcon : _folderIcon) + ); + row[_columns.isFolder] = isFolder; + row.SendItemAdded(); + } + ); + } +}; + +// Local class for loading entity class definitions in a separate thread +class EntityClassChooser::ThreadedEntityClassLoader : + public wxThread +{ + // Column specification struct + const EntityClassChooser::TreeColumns& _columns; + + // The tree store to populate. We must operate on our own tree store, since + // updating the EntityClassChooser's tree store from a different thread + // wouldn't be safe + wxutil::TreeModel::Ptr _treeStore; + + // The class to be notified on finish + wxEvtHandler* _finishedHandler; + +public: + + // Construct and initialise variables + ThreadedEntityClassLoader(const EntityClassChooser::TreeColumns& cols, + wxEvtHandler* finishedHandler) : + wxThread(wxTHREAD_JOINABLE), + _columns(cols), + _finishedHandler(finishedHandler) + {} + + ~ThreadedEntityClassLoader() + { + if (IsRunning()) + { + Delete(); + } + } + + // The worker function that will execute in the thread + ExitCode Entry() + { + ScopedDebugTimer timer("ThreadedEntityClassLoader::run()"); + + // Create new treestoree + _treeStore = new wxutil::TreeModel(_columns); + + // Populate it with the list of entity classes by using a visitor class. + EntityClassTreePopulator visitor(_treeStore, _columns); + GlobalEntityClassManager().forEachEntityClass(visitor); + + if (TestDestroy()) return static_cast(0); + + // Ensure model is sorted before giving it to the tree view + _treeStore->SortModelFoldersFirst(_columns.name, _columns.isFolder); + + if (!TestDestroy()) + { + wxQueueEvent(_finishedHandler, new wxutil::TreeModel::PopulationFinishedEvent(_treeStore)); + } + + return static_cast(0); + } +}; + +// Main constructor +EntityClassChooser::EntityClassChooser() +: wxutil::DialogBase(_(ECLASS_CHOOSER_TITLE)), + _treeStore(NULL), + _treeView(NULL), + _selectedName("") +{ + // Connect the finish callback to load the treestore + Connect(wxutil::EV_TREEMODEL_POPULATION_FINISHED, + TreeModelPopulationFinishedHandler(EntityClassChooser::onTreeStorePopulationFinished), NULL, this); + + loadNamedPanel(this, "EntityClassChooserMainPanel"); + + // Connect button signals + findNamedObject(this, "EntityClassChooserAddButton")->Connect( + wxEVT_BUTTON, wxCommandEventHandler(EntityClassChooser::onOK), NULL, this); + findNamedObject(this, "EntityClassChooserAddButton")->SetBitmap( + wxArtProvider::GetBitmap(wxART_PLUS)); + + findNamedObject(this, "EntityClassChooserCancelButton")->Connect( + wxEVT_BUTTON, wxCommandEventHandler(EntityClassChooser::onCancel), NULL, this); + + // Add model preview to right-hand-side of main container + wxPanel* rightPanel = findNamedObject(this, "EntityClassChooserRightPane"); + + _modelPreview.reset(new wxutil::ModelPreview(rightPanel)); + + rightPanel->GetSizer()->Add(_modelPreview->getWidget(), 1, wxEXPAND); + + // Listen for defs-reloaded signal (cannot bind directly to + // ThreadedEntityClassLoader method because it is not sigc::trackable) + GlobalEntityClassManager().defsReloadedSignal().connect( + sigc::mem_fun(this, &EntityClassChooser::loadEntityClasses) + ); + + // Setup the tree view and invoke threaded loader to get the entity classes + setupTreeView(); + loadEntityClasses(); + + FitToScreen(0.7f, 0.6f); + + wxSplitterWindow* splitter = findNamedObject(this, "EntityClassChooserSplitter"); + + // Disallow unsplitting + splitter->SetMinimumPaneSize(200); + splitter->SetSashPosition(static_cast(GetSize().GetWidth() * 0.2f)); + + // Persist layout to registry + _panedPosition.connect(splitter); + _panedPosition.loadFromPath(RKEY_SPLIT_POS); + + Connect(wxEVT_CLOSE_WINDOW, wxCloseEventHandler(EntityClassChooser::onDeleteEvent), NULL, this); + + // Set the model preview height to something significantly smaller than the + // window's height to allow shrinking + _modelPreview->getWidget()->SetMinClientSize( + wxSize(GetSize().GetWidth() * 0.4f, GetSize().GetHeight() * 0.2f)); +} + +// Display the singleton instance +std::string EntityClassChooser::chooseEntityClass(const std::string& preselectEclass) +{ + if (!preselectEclass.empty()) + { + Instance().setSelectedEntityClass(preselectEclass); + } + + if (Instance().ShowModal() == wxID_OK) + { + return Instance().getSelectedEntityClass(); + } + else + { + return ""; // Empty selection on cancel + } +} + +EntityClassChooser& EntityClassChooser::Instance() +{ + EntityClassChooserPtr& instancePtr = InstancePtr(); + + if (instancePtr == NULL) + { + // Not yet instantiated, do it now + instancePtr.reset(new EntityClassChooser); + + // Register this instance with GlobalRadiant() at once + GlobalRadiant().signal_radiantShutdown().connect( + sigc::mem_fun(*instancePtr, &EntityClassChooser::onRadiantShutdown) + ); + } + + return *instancePtr; +} + +EntityClassChooserPtr& EntityClassChooser::InstancePtr() +{ + static EntityClassChooserPtr _instancePtr; + return _instancePtr; +} + +void EntityClassChooser::onRadiantShutdown() +{ + rMessage() << "EntityClassChooser shutting down." << std::endl; + + _modelPreview.reset(); + + // Final step at shutdown, release the shared ptr + Instance().SendDestroyEvent(); + InstancePtr().reset(); +} + +void EntityClassChooser::loadEntityClasses() +{ + _eclassLoader.reset(new ThreadedEntityClassLoader(_columns, this)); + _eclassLoader->Run(); +} + +void EntityClassChooser::setSelectedEntityClass(const std::string& eclass) +{ + // Select immediately if possible, otherwise remember class name for later + // selection + if (_treeStore != NULL) + { + wxDataViewItem item = _treeStore->FindString(eclass, _columns.name); + + if (item.IsOk()) + { + _treeView->Select(item); + _classToHighlight.clear(); + + return; + } + } + + _classToHighlight = eclass; +} + +const std::string& EntityClassChooser::getSelectedEntityClass() const +{ + return _selectedName; +} + +void EntityClassChooser::onDeleteEvent(wxCloseEvent& ev) +{ + // greebo: Clear the selected name on hide, we don't want to create another entity when + // the user clicks on the X in the upper right corner. + _selectedName.clear(); + + EndModal(wxID_CANCEL); // break main loop + Hide(); +} + +int EntityClassChooser::ShowModal() +{ + // Update the member variables + updateSelection(); + + // Focus on the treeview + _treeView->SetFocus(); + + int returnCode = DialogBase::ShowModal(); + + _panedPosition.saveToPath(RKEY_SPLIT_POS); + + return returnCode; +} + +void EntityClassChooser::setTreeViewModel() +{ + _treeView->AssociateModel(_treeStore.get()); + + // Expand the first layer + _treeView->ExpandTopLevelItems(); + + // Pre-select the given class if requested by setSelectedEntityClass() + if (!_classToHighlight.empty()) + { + assert(_treeStore); + setSelectedEntityClass(_classToHighlight); + } +} + +void EntityClassChooser::setupTreeView() +{ + // Use the TreeModel's full string search function + _treeStore = new wxutil::TreeModel(_columns); + wxutil::TreeModel::Row row = _treeStore->AddItem(); + + row[_columns.name] = wxVariant(wxDataViewIconText(_("Loading..."))); + + wxPanel* parent = findNamedObject(this, "EntityClassChooserLeftPane"); + + _treeView = wxutil::TreeView::CreateWithModel(parent, _treeStore); + _treeView->AddSearchColumn(_columns.name); + + _treeView->Connect(wxEVT_DATAVIEW_SELECTION_CHANGED, + wxDataViewEventHandler(EntityClassChooser::onSelectionChanged), NULL, this); + + // Single column with icon and name + _treeView->AppendIconTextColumn(_("Classname"), _columns.name.getColumnIndex(), + wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_NOT, wxDATAVIEW_COL_SORTABLE); + + parent->GetSizer()->Prepend(_treeView, 1, wxEXPAND | wxBOTTOM, 6); +} + +// Update the usage information +void EntityClassChooser::updateUsageInfo(const std::string& eclass) +{ + // Lookup the IEntityClass instance + IEntityClassPtr e = GlobalEntityClassManager().findOrInsert(eclass, true); + + // Set the usage panel to the IEntityClass' usage information string + wxTextCtrl* usageText = findNamedObject(this, "EntityClassChooserUsageText"); + usageText->SetValue(eclass::getUsage(*e)); +} + +void EntityClassChooser::updateSelection() +{ + wxDataViewItem item = _treeView->GetSelection(); + + if (item.IsOk()) + { + wxutil::TreeModel::Row row(item, *_treeStore); + + if (!row[_columns.isFolder].getBool()) + { + // Make the OK button active + findNamedObject(this, "EntityClassChooserAddButton")->Enable(true); + + // Set the panel text with the usage information + wxDataViewIconText iconAndName = static_cast(row[_columns.name]); + std::string selName = iconAndName.GetText().ToStdString(); + + updateUsageInfo(selName); + + // Lookup the IEntityClass instance + IEntityClassPtr eclass = GlobalEntityClassManager().findClass(selName); + + if (eclass != NULL) + { + _modelPreview->setModel(eclass->getAttribute("model").getValue()); + _modelPreview->setSkin(eclass->getAttribute("skin").getValue()); + } + + // Update the _selectionName field + _selectedName = selName; + + return; // success + } + } + + // Nothing selected + _modelPreview->setModel(""); + _modelPreview->setSkin(""); + + findNamedObject(this, "EntityClassChooserAddButton")->Enable(false); +} + +void EntityClassChooser::onCancel(wxCommandEvent& ev) +{ + _selectedName.clear(); + + EndModal(wxID_CANCEL); // break main loop + Hide(); +} + +void EntityClassChooser::onOK(wxCommandEvent& ev) +{ + EndModal(wxID_OK); // break main loop + Hide(); +} + +void EntityClassChooser::onSelectionChanged(wxDataViewEvent& ev) +{ + updateSelection(); +} + +void EntityClassChooser::onTreeStorePopulationFinished(wxutil::TreeModel::PopulationFinishedEvent& ev) +{ + _treeView->UnselectAll(); + + _treeStore = ev.GetTreeModel(); + setTreeViewModel(); +} + + +} // namespace ui diff --git a/libs/wxutil/EntityClassChooser.h b/libs/wxutil/EntityClassChooser.h new file mode 100644 index 0000000000..eafdaf9b62 --- /dev/null +++ b/libs/wxutil/EntityClassChooser.h @@ -0,0 +1,115 @@ +#pragma once + +#include "iradiant.h" +#include "ieclass.h" + +#include "wxutil/preview/ModelPreview.h" +#include "wxutil/dialog/DialogBase.h" +#include "wxutil/TreeModel.h" +#include "wxutil/TreeView.h" +#include "wxutil/XmlResourceBasedWidget.h" +#include "wxutil/PanedPosition.h" + +#include + +namespace wxutil +{ + +class EntityClassChooser; +typedef std::shared_ptr EntityClassChooserPtr; + +/** + * Dialog window displaying a tree of Entity Classes, allowing the selection + * of a class to create at the current location. + */ +class EntityClassChooser : + public wxutil::DialogBase, + private wxutil::XmlResourceBasedWidget +{ +public: + // Treemodel definition + struct TreeColumns : + public wxutil::TreeModel::ColumnRecord + { + TreeColumns() : + name(add(wxutil::TreeModel::Column::IconText)), + isFolder(add(wxutil::TreeModel::Column::Boolean)) + {} + + wxutil::TreeModel::Column name; + wxutil::TreeModel::Column isFolder; + }; + +private: + TreeColumns _columns; + + // Tree model holding the classnames + wxutil::TreeModel::Ptr _treeStore; + wxutil::TreeView* _treeView; + + // Delegated object for loading entity classes in a separate thread + class ThreadedEntityClassLoader; + std::unique_ptr _eclassLoader; // PIMPL idiom + + // Last selected classname + std::string _selectedName; + + // Class we should select when the treemodel is populated + std::string _classToHighlight; + + // Model preview widget + wxutil::ModelPreviewPtr _modelPreview; + + wxutil::PanedPosition _panedPosition; + +private: + // Constructor. Creates the GTK widgets. + EntityClassChooser(); + + void setTreeViewModel(); + void getEntityClassesFromLoader(); + void loadEntityClasses(); + + // Widget construction helpers + void setupTreeView(); + + // Update the usage panel with information from the provided entityclass + void updateUsageInfo(const std::string& eclass); + + // Updates the member variables based on the current tree selection + void updateSelection(); + + // Button callbacks + void onCancel(wxCommandEvent& ev); + void onOK(wxCommandEvent& ev); + void onSelectionChanged(wxDataViewEvent& ev); + void onDeleteEvent(wxCloseEvent& ev); + void onTreeStorePopulationFinished(wxutil::TreeModel::PopulationFinishedEvent& ev); + + // This is where the static shared_ptr of the singleton instance is held. + static EntityClassChooserPtr& InstancePtr(); + +public: + // Public accessor to the singleton instance + static EntityClassChooser& Instance(); + + // Sets the tree selection to the given entity class + void setSelectedEntityClass(const std::string& eclass); + + // Sets the tree selection to the given entity class + const std::string& getSelectedEntityClass() const; + + virtual int ShowModal(); + + /** + * Convenience function: + * Display the dialog and block awaiting the selection of an entity class, + * which is returned to the caller. If the dialog is cancelled or no + * selection is made, and empty string will be returned. + */ + static std::string chooseEntityClass(const std::string& preselectEclass = std::string()); + + void onRadiantShutdown(); +}; + +} // namespace ui