-
-
Notifications
You must be signed in to change notification settings - Fork 379
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5625 from Web-eWorks/csys-editor
Implement a graphical custom system editor
- Loading branch information
Showing
41 changed files
with
4,163 additions
and
180 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details | ||
// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt | ||
|
||
#include "ActionBinder.h" | ||
#include "core/StringUtils.h" | ||
#include "fmt/core.h" | ||
|
||
#include <imgui/imgui.h> | ||
#include <imgui/imgui_internal.h> | ||
#include <algorithm> | ||
|
||
using namespace Editor; | ||
|
||
// wrap double-dereference when working with variant of pointer types | ||
template<typename T, typename ...Types> | ||
auto get_if(std::variant<Types...> &pv) | ||
{ | ||
T *ptr = std::get_if<T>(&pv); | ||
return ptr ? *ptr : nullptr; | ||
} | ||
|
||
ActionBinder::ActionBinder() | ||
{ | ||
} | ||
|
||
ActionBinder::~ActionBinder() | ||
{ | ||
} | ||
|
||
// static | ||
std::string ActionBinder::FormatShortcut(ImGuiKeyChord shortcut) | ||
{ | ||
char name[24]; | ||
ImGui::GetKeyChordName(shortcut, name, sizeof(name)); | ||
|
||
return std::string(name); | ||
} | ||
|
||
void ActionBinder::Update() | ||
{ | ||
// Don't process shortcuts while a popup is open | ||
if (ImGui::IsPopupOpen(ImGuiID(0), ImGuiPopupFlags_AnyPopupId)) { | ||
for (auto &popup : ImGui::GetCurrentContext()->OpenPopupStack) { | ||
if (popup.Window && popup.Window->Flags & ImGuiWindowFlags_Modal) | ||
return; | ||
} | ||
} | ||
|
||
for (auto &[id, action] : m_actionStorage) { | ||
if (!action.shortcut) | ||
continue; | ||
|
||
if (ImGui::Shortcut(action.shortcut, 0, ImGuiInputFlags_RouteGlobal)) { | ||
if (action.predicate.empty() || action.predicate()) | ||
action.action(); | ||
} | ||
} | ||
} | ||
|
||
void ActionBinder::DrawGroupOverviewEntry(Group &group) | ||
{ | ||
if (group.isMenu) | ||
if (!ImGui::TreeNodeEx(group.label.c_str(), ImGuiTreeNodeFlags_FramePadding)) | ||
return; | ||
|
||
for (auto &entry : group.entries) { | ||
if (auto *action = get_if<ActionEntry *>(entry)) { | ||
ImGui::TextUnformatted(action->label.c_str()); | ||
|
||
if (action->shortcut) { | ||
ImGui::SameLine(ImGui::CalcItemWidth()); | ||
ImGui::TextUnformatted(FormatShortcut(action->shortcut).c_str()); | ||
} | ||
} else if (auto *group = get_if<Group *>(entry)) { | ||
DrawGroupOverviewEntry(*group); | ||
} | ||
} | ||
|
||
if (group.isMenu) | ||
ImGui::TreePop(); | ||
} | ||
|
||
void ActionBinder::DrawOverview(const char *title, bool *pOpen) | ||
{ | ||
if (ImGui::Begin(title, pOpen)) { | ||
for (auto &groupId : m_topLevelGroups) { | ||
DrawGroupOverviewEntry(m_groupStorage.at(groupId)); | ||
} | ||
} | ||
ImGui::End(); | ||
} | ||
|
||
void ActionBinder::DrawMenuBar() | ||
{ | ||
for (auto &groupId : m_topLevelGroups) { | ||
Group &group = m_groupStorage.at(groupId); | ||
|
||
if (group.isMenu) | ||
DrawMenuInternal(group, true); | ||
} | ||
} | ||
|
||
void ActionBinder::DrawMenuInternal(Group &group, bool submitMenuItem) | ||
{ | ||
if (group.entries.empty()) | ||
return; | ||
|
||
if (submitMenuItem && !ImGui::BeginMenu(group.label.c_str())) | ||
return; | ||
|
||
ImGui::PushID(group.label.c_str()); | ||
|
||
size_t numActions = 0; | ||
for (auto &entry : group.entries) { | ||
|
||
if (ActionEntry *action = get_if<ActionEntry *>(entry)) { | ||
|
||
numActions++; | ||
|
||
bool enabled = action->predicate.empty() || action->predicate(); | ||
ImGui::BeginDisabled(!enabled); | ||
|
||
std::string shortcut = action->shortcut ? FormatShortcut(action->shortcut) : ""; | ||
if (ImGui::MenuItem(action->label.c_str(), shortcut.c_str())) { | ||
action->action(); | ||
} | ||
|
||
ImGui::EndDisabled(); | ||
|
||
} else if (Group *subGroup = get_if<Group *>(entry)) { | ||
|
||
if (numActions) { | ||
numActions = 0; | ||
ImGui::Separator(); | ||
} | ||
|
||
DrawMenuInternal(*subGroup, subGroup->isMenu); | ||
|
||
} | ||
} | ||
|
||
ImGui::PopID(); | ||
|
||
if (submitMenuItem) | ||
ImGui::EndMenu(); | ||
} | ||
|
||
void ActionBinder::BeginInternal(std::string id, bool menu) | ||
{ | ||
for (std::string_view name : SplitString(id, ".")) { | ||
|
||
std::string lookupId = !m_activeGroupStack.empty() ? | ||
fmt::format("{}.{}", m_activeGroupStack.back().first, name) : | ||
std::string(name); | ||
|
||
// Create the new group if it doesn't exist | ||
if (!m_groupStorage.count(lookupId)) { | ||
m_groupStorage.try_emplace(lookupId, name, menu); | ||
|
||
// nothing on the stack, could be a top-level entry | ||
if (m_activeGroupStack.empty()) | ||
m_topLevelGroups.push_back(lookupId); | ||
// add a group entry to our previous group | ||
else { | ||
Group *group = m_activeGroupStack.back().second; | ||
group->entries.emplace_back(&m_groupStorage.at(lookupId)); | ||
} | ||
} | ||
|
||
m_activeGroupStack.push_back({ lookupId, &m_groupStorage.at(lookupId) }); | ||
} | ||
} | ||
|
||
void ActionBinder::EndInternal() | ||
{ | ||
assert(!m_activeGroupStack.empty()); | ||
|
||
m_activeGroupStack.pop_back(); | ||
} | ||
|
||
void ActionBinder::AddInternal(std::string id, ActionEntry &&entry) | ||
{ | ||
if (m_activeGroupStack.empty()) | ||
return; | ||
|
||
Group *group = m_activeGroupStack.back().second; | ||
|
||
std::string qualified_id = fmt::format("{}.{}", m_activeGroupStack.back().first, id); | ||
auto iter = m_actionStorage.emplace(qualified_id, std::move(entry)).first; | ||
|
||
group->entries.push_back(&iter->second); | ||
} | ||
|
||
ActionEntry *ActionBinder::GetAction(std::string id) | ||
{ | ||
if (!m_actionStorage.count(id)) | ||
return nullptr; | ||
|
||
return &m_actionStorage.at(id); | ||
} | ||
|
||
void ActionBinder::TriggerAction(std::string id) | ||
{ | ||
ActionEntry *entry = GetAction(id); | ||
|
||
if (entry && (entry->predicate.empty() || entry->predicate())) | ||
entry->action(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details | ||
// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt | ||
|
||
#pragma once | ||
|
||
#include "imgui/imgui.h" | ||
|
||
#include <sigc++/sigc++.h> | ||
|
||
#include <map> | ||
#include <string> | ||
#include <variant> | ||
|
||
namespace Editor { | ||
|
||
// Represents an action the user can execute within an editor context | ||
// that should be associated with some window-global shortcut. | ||
// The predicate will be evaluated to determine if the entry is enabled. | ||
struct ActionEntry { | ||
template<typename Functor> | ||
ActionEntry(std::string_view label, ImGuiKeyChord shortcut, Functor f) : | ||
label(label), | ||
shortcut(shortcut), | ||
action(f) | ||
{} | ||
|
||
template<typename Predicate, typename Functor> | ||
ActionEntry(std::string_view label, ImGuiKeyChord shortcut, Predicate p, Functor f) : | ||
label(label), | ||
shortcut(shortcut), | ||
predicate(p), | ||
action(f) | ||
{} | ||
|
||
template<typename Functor> | ||
ActionEntry(std::string_view label, const char *icon, ImGuiKeyChord shortcut, Functor f) : | ||
label(label), | ||
fontIcon(icon), | ||
shortcut(shortcut), | ||
action(f) | ||
{} | ||
|
||
template<typename Predicate, typename Functor> | ||
ActionEntry(std::string_view label, const char *icon, ImGuiKeyChord shortcut, Predicate p, Functor f) : | ||
label(label), | ||
fontIcon(icon), | ||
shortcut(shortcut), | ||
predicate(p), | ||
action(f) | ||
{} | ||
|
||
std::string label; | ||
const char *fontIcon; | ||
ImGuiKeyChord shortcut; | ||
|
||
sigc::slot<bool ()> predicate; | ||
sigc::slot<void ()> action; | ||
}; | ||
|
||
class ActionBinder { | ||
public: | ||
ActionBinder(); | ||
~ActionBinder(); | ||
|
||
struct Group; | ||
|
||
using GroupEntry = std::variant<Group *, ActionEntry *>; | ||
struct Group { | ||
Group(std::string_view name, bool menu) : | ||
label(std::string(name)), | ||
isMenu(menu) | ||
{} | ||
|
||
std::string label; | ||
std::vector<GroupEntry> entries; | ||
bool isMenu; | ||
}; | ||
|
||
// process all actions and determine if their shortcuts are activated | ||
void Update(); | ||
|
||
// Draw debug window displaying all registered actions | ||
void DrawOverview(const char *title, bool *pOpen = nullptr); | ||
|
||
// Draw all groups registered in this ActionBinder as a main menu bar | ||
void DrawMenuBar(); | ||
|
||
// draw the GroupEntry named by 'id' in the context of an existing | ||
// dropdown menu (i.e. do not submit a top-level BeginMenu) | ||
void DrawGroup(std::string id) { DrawMenuInternal(m_groupStorage.at(id), false); } | ||
|
||
// draw the GroupEntry named by 'id' as a submenu (with a top-level BeginMenu) | ||
void DrawMenu(std::string id) { DrawMenuInternal(m_groupStorage.at(id), true); } | ||
|
||
// draw the GroupEntry named by 'id' in the context of a button toolbar | ||
// void DrawToolbar(std::string id) | ||
|
||
static std::string FormatShortcut(ImGuiKeyChord shortcut); | ||
|
||
// Begin a group or menu with the given ID. ID is a qualified domain | ||
// name relative to the current ID stack. | ||
ActionBinder &BeginGroup(std::string id) { BeginInternal(id, false); return *this; } | ||
ActionBinder &BeginMenu(std::string id) { BeginInternal(id, true); return *this; } | ||
void EndGroup() { EndInternal(); } | ||
void EndMenu() { EndInternal(); } | ||
|
||
// Add the given action to the currently open group. Fails if no group | ||
// is open. The action can be referenced by the fully-qualified id | ||
// 'group-id[.group-id[...]].action-id'. | ||
ActionBinder &AddAction(std::string id, ActionEntry &&entry) { AddInternal(id, std::move(entry)); return *this; } | ||
|
||
std::vector<std::string> &GetGroups() { return m_topLevelGroups; } | ||
|
||
ActionEntry *GetAction(std::string id); | ||
void TriggerAction(std::string id); | ||
|
||
private: | ||
void BeginInternal(std::string id, bool menu); | ||
void EndInternal(); | ||
|
||
void AddInternal(std::string id, ActionEntry &&entry); | ||
|
||
void DrawMenuInternal(Group &group, bool menuHeading); | ||
|
||
void DrawGroupOverviewEntry(Group &group); | ||
|
||
std::map<std::string, ActionEntry> m_actionStorage; | ||
std::map<std::string, Group> m_groupStorage; | ||
|
||
std::vector<std::string> m_topLevelGroups; | ||
|
||
std::vector<std::pair<std::string, Group *>> m_activeGroupStack; | ||
}; | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.