diff --git a/install/menu.xml b/install/menu.xml
index a3be463e06..697fcd8c3a 100644
--- a/install/menu.xml
+++ b/install/menu.xml
@@ -4,6 +4,7 @@
+
diff --git a/radiant/ui/UserInterfaceModule.cpp b/radiant/ui/UserInterfaceModule.cpp
index 5eb7074de8..c030daae53 100644
--- a/radiant/ui/UserInterfaceModule.cpp
+++ b/radiant/ui/UserInterfaceModule.cpp
@@ -59,6 +59,7 @@
#include "ui/brush/QuerySidesDialog.h"
#include "ui/brush/FindBrush.h"
#include "ui/mousetool/RegistrationHelper.h"
+#include "ui/mapselector/MapSelector.h"
#include
@@ -408,6 +409,7 @@ void UserInterfaceModule::registerUICommands()
GlobalEventManager().addRegistryToggle("TogTexLock", RKEY_ENABLE_TEXTURE_LOCK);
GlobalCommandSystem().addCommand("LoadPrefab", ui::loadPrefabDialog);
+ GlobalCommandSystem().addCommand("OpenMapFromProject", ui::MapSelector::OpenMapFromProject);
}
void UserInterfaceModule::HandleTextureChanged(radiant::TextureChangedMessage& msg)
diff --git a/radiant/ui/mapselector/MapSelector.cpp b/radiant/ui/mapselector/MapSelector.cpp
new file mode 100644
index 0000000000..289633cbf5
--- /dev/null
+++ b/radiant/ui/mapselector/MapSelector.cpp
@@ -0,0 +1,117 @@
+#include "MapSelector.h"
+
+#include "i18n.h"
+
+#include
+#include
+
+namespace ui
+{
+
+// CONSTANTS
+namespace
+{
+ const char* const MAPSELECTOR_TITLE = N_("Choose Map File");
+}
+
+MapSelector::MapSelector() :
+ DialogBase(_(MAPSELECTOR_TITLE)),
+ _treeView(nullptr),
+ _handlingSelectionChange(false)
+{
+ SetSizer(new wxBoxSizer(wxVERTICAL));
+
+ wxBoxSizer* vbox = new wxBoxSizer(wxVERTICAL);
+ GetSizer()->Add(vbox, 1, wxEXPAND | wxALL, 12);
+
+ setupTreeView(this);
+ vbox->Add(_treeView, 1, wxEXPAND);
+
+ wxStdDialogButtonSizer* buttonSizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL);
+ wxButton* reloadButton = new wxButton(this, wxID_ANY, _("Rescan"));
+ reloadButton->Bind(wxEVT_BUTTON, &MapSelector::onRescanPath, this);
+
+ buttonSizer->Prepend(reloadButton, 0, wxRIGHT, 32);
+
+ vbox->Add(buttonSizer, 0, wxALIGN_RIGHT | wxTOP, 12);
+
+ // Set the default size of the window
+ _position.connect(this);
+ _position.readPosition();
+
+ FitToScreen(0.5f, 0.6f);
+}
+
+int MapSelector::ShowModal()
+{
+ // Populate the tree
+ populateTree();
+
+ // Enter the main loop
+ return DialogBase::ShowModal();
+}
+
+std::string MapSelector::ChooseMapFile()
+{
+ auto* dialog = new MapSelector();
+ std::string returnValue = "";
+
+ if (dialog->ShowModal() == wxID_OK)
+ {
+ returnValue = dialog->getSelectedPath();
+ }
+
+ // Use the instance to select a model.
+ return returnValue;
+}
+
+void MapSelector::OpenMapFromProject(const cmd::ArgumentList& args)
+{
+ auto mapPath = ChooseMapFile();
+
+ if (!mapPath.empty())
+ {
+ GlobalCommandSystem().executeCommand("OpenMap", mapPath);
+ }
+}
+
+// Helper function to create the TreeView
+void MapSelector::setupTreeView(wxWindow* parent)
+{
+ _treeView = wxutil::FileSystemView::Create(parent, wxBORDER_STATIC | wxDV_NO_HEADER);
+
+ // Get the extensions from all possible patterns (e.g. *.map or *.mapx)
+ FileTypePatterns patterns = GlobalFiletypes().getPatternsForType(filetype::TYPE_MAP);
+
+ std::set fileExtensions;
+
+ for (const auto& pattern : patterns)
+ {
+ fileExtensions.insert(pattern.extension);
+ }
+
+ _treeView->SetFileExtensions(fileExtensions);
+}
+
+std::string MapSelector::getBaseFolder()
+{
+ return ""; // use an empty path which resembles the VFS root
+}
+
+void MapSelector::populateTree()
+{
+ _treeView->SetBasePath(getBaseFolder());
+ _treeView->Populate();
+}
+
+void MapSelector::onRescanPath(wxCommandEvent& ev)
+{
+ populateTree();
+}
+
+std::string MapSelector::getSelectedPath()
+{
+ return _treeView->GetSelectedPath();
+}
+
+}
diff --git a/radiant/ui/mapselector/MapSelector.h b/radiant/ui/mapselector/MapSelector.h
new file mode 100644
index 0000000000..1f60ceea60
--- /dev/null
+++ b/radiant/ui/mapselector/MapSelector.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "icommandsystem.h"
+
+#include "wxutil/dialog/DialogBase.h"
+#include "wxutil/fsview/FileSystemView.h"
+#include "wxutil/WindowPosition.h"
+
+namespace ui
+{
+
+/// Dialog for browsing and selecting a map from (V)FS
+class MapSelector :
+ public wxutil::DialogBase
+{
+private:
+ wxPanel* _dialogPanel;
+
+ // Main tree view with the folder hierarchy
+ wxutil::FileSystemView* _treeView;
+
+ // The window position tracker
+ wxutil::WindowPosition _position;
+
+ bool _handlingSelectionChange;
+
+private:
+ // Private constructor, creates widgets
+ MapSelector();
+
+ // Helper functions to configure GUI components
+ void setupTreeView(wxWindow* parent);
+
+ // Populate the tree view with files
+ void populateTree();
+
+ // Get the path that should be used for map population
+ // This reflects the settings made by the user on the top of the selector window
+ std::string getBaseFolder();
+
+ // Return the selected map path
+ std::string getSelectedPath();
+
+ void onRescanPath(wxCommandEvent& ev);
+
+public:
+ int ShowModal() override;
+
+ /**
+ * Display the Selector return the path of the file selected by the user.
+ */
+ static std::string ChooseMapFile();
+
+ static void OpenMapFromProject(const cmd::ArgumentList& args);
+};
+
+}
diff --git a/radiantcore/map/Map.cpp b/radiantcore/map/Map.cpp
index d1cc4732bd..8ce5c47913 100644
--- a/radiantcore/map/Map.cpp
+++ b/radiantcore/map/Map.cpp
@@ -702,17 +702,26 @@ void Map::openMap(const cmd::ArgumentList& args)
}
else if (!candidate.empty())
{
- // Next, try to look up the map in the regular maps path
- fs::path mapsPath = GlobalGameManager().getMapPath();
- fs::path fullMapPath = mapsPath / candidate;
-
- if (os::fileOrDirExists(fullMapPath.string()))
+ // Try to open this file from the VFS (this will hit physical files
+ // in the active project as well as files in registered PK4)
+ if (GlobalFileSystem().openTextFile(candidate))
{
- mapToLoad = fullMapPath.string();
+ mapToLoad = candidate;
}
else
{
- throw cmd::ExecutionFailure(fmt::format(_("File doesn't exist: {0}"), fullMapPath.string()));
+ // Next, try to look up the map in the regular maps path
+ fs::path mapsPath = GlobalGameManager().getMapPath();
+ fs::path fullMapPath = mapsPath / candidate;
+
+ if (os::fileOrDirExists(fullMapPath.string()))
+ {
+ mapToLoad = fullMapPath.string();
+ }
+ else
+ {
+ throw cmd::ExecutionFailure(fmt::format(_("File doesn't exist: {0}"), candidate));
+ }
}
}
diff --git a/tools/msvc/DarkRadiant.vcxproj b/tools/msvc/DarkRadiant.vcxproj
index 24036097c2..92b98ccc0f 100644
--- a/tools/msvc/DarkRadiant.vcxproj
+++ b/tools/msvc/DarkRadiant.vcxproj
@@ -268,6 +268,7 @@
+
@@ -454,6 +455,7 @@
+
diff --git a/tools/msvc/DarkRadiant.vcxproj.filters b/tools/msvc/DarkRadiant.vcxproj.filters
index f9d2c61e24..135cf31a8a 100644
--- a/tools/msvc/DarkRadiant.vcxproj.filters
+++ b/tools/msvc/DarkRadiant.vcxproj.filters
@@ -173,6 +173,9 @@
{28ae583f-8545-491c-abcc-5e459c3d357b}
+
+ {6ddb8b69-4e5a-4c82-b7d4-aa1306342340}
+
@@ -667,6 +670,9 @@
src\ui
+
+ src\ui\mapselector
+
@@ -1284,6 +1290,9 @@
src\ui\common
+
+ src\ui\mapselector
+