diff --git a/CMakeLists.txt b/CMakeLists.txt index 907417532..99cfe6510 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2500,8 +2500,13 @@ if(CLIENT) mapitems/sound.cpp mapitems/sound.h popups.cpp + prompt.cpp + prompt.h proof_mode.cpp proof_mode.h + quick_action.h + quick_actions.cpp + quick_actions.h smooth_value.cpp smooth_value.h tileart.cpp diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index ea6a8865e..978716bbf 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -1074,11 +1074,9 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) // proof button TB_Top.VSplitLeft(40.0f, &Button, &TB_Top); - static int s_ProofButton = 0; - if(DoButton_Ex(&s_ProofButton, "Proof", MapView()->ProofMode()->IsEnabled(), &Button, 0, "[ctrl+p] Toggles proof borders. These borders represent the area that a player can see with default zoom.", IGraphics::CORNER_L) || - (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(KEY_P) && ModPressed)) + if(DoButton_Ex(&m_QuickActionProof, m_QuickActionProof.Label(), m_QuickActionProof.Active(), &Button, 0, m_QuickActionProof.Description(), IGraphics::CORNER_L)) { - MapView()->ProofMode()->Toggle(); + m_QuickActionProof.Call(); } TB_Top.VSplitLeft(14.0f, &Button, &TB_Top); @@ -1254,10 +1252,9 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) // refocus button { TB_Bottom.VSplitLeft(50.0f, &Button, &TB_Bottom); - static int s_RefocusButton = 0; int FocusButtonChecked = MapView()->IsFocused() ? -1 : 1; - if(DoButton_Editor(&s_RefocusButton, "Refocus", FocusButtonChecked, &Button, 0, "[HOME] Restore map focus") || (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(KEY_HOME))) - MapView()->Focus(); + if(DoButton_Editor(&m_QuickActionRefocus, m_QuickActionRefocus.Label(), FocusButtonChecked, &Button, 0, m_QuickActionRefocus.Description()) || (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(KEY_HOME))) + m_QuickActionRefocus.Call(); TB_Bottom.VSplitLeft(5.0f, nullptr, &TB_Bottom); } @@ -4302,12 +4299,9 @@ void CEditor::RenderLayers(CUIRect LayersBox) if(s_ScrollRegion.AddRect(AddGroupButton)) { AddGroupButton.HSplitTop(RowHeight, &AddGroupButton, 0); - static int s_AddGroupButton = 0; - if(DoButton_Editor(&s_AddGroupButton, "Add group", 0, &AddGroupButton, IGraphics::CORNER_R, "Adds a new group")) + if(DoButton_Editor(&m_QuickActionAddGroup, m_QuickActionAddGroup.Label(), 0, &AddGroupButton, IGraphics::CORNER_R, m_QuickActionAddGroup.Description())) { - m_Map.NewGroup(); - m_SelectedGroup = m_Map.m_vpGroups.size() - 1; - m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, false)); + m_QuickActionAddGroup.Call(); } } @@ -4806,8 +4800,8 @@ void CEditor::RenderImagesList(CUIRect ToolBox) { AddImageButton.HSplitTop(5.0f, nullptr, &AddImageButton); AddImageButton.HSplitTop(RowHeight, &AddImageButton, nullptr); - if(DoButton_Editor(&s_AddImageButton, "Add", 0, &AddImageButton, 0, "Load a new image to use in the map")) - InvokeFileDialog(IStorage::TYPE_ALL, FILETYPE_IMG, "Add Image", "Add", "mapres", false, AddImage, this); + if(DoButton_Editor(&s_AddImageButton, m_QuickActionAddImage.Label(), 0, &AddImageButton, 0, m_QuickActionAddImage.Description())) + m_QuickActionAddImage.Call(); } s_ScrollRegion.End(); } @@ -5750,9 +5744,9 @@ void CEditor::RenderStatusbar(CUIRect View, CUIRect *pTooltipRect) CUIRect Button; View.VSplitRight(100.0f, &View, &Button); static int s_EnvelopeButton = 0; - if(DoButton_Editor(&s_EnvelopeButton, "Envelopes", ButtonsDisabled ? -1 : m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES, &Button, 0, "Toggles the envelope editor.") == 1) + if(DoButton_Editor(&s_EnvelopeButton, m_QuickActionEnvelopes.Label(), m_QuickActionEnvelopes.Color(), &Button, 0, m_QuickActionEnvelopes.Description()) == 1) { - m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES ? EXTRAEDITOR_NONE : EXTRAEDITOR_ENVELOPES; + m_QuickActionEnvelopes.Call(); } View.VSplitRight(10.0f, &View, nullptr); @@ -7919,7 +7913,7 @@ void CEditor::Render() InvokeFileDialog(IStorage::TYPE_SAVE, FILETYPE_MAP, "Save map", "Save", "maps", true, CallbackSaveCopyMap, this); // ctrl+shift+s to save as else if(Input()->KeyPress(KEY_S) && ModPressed && ShiftPressed) - InvokeFileDialog(IStorage::TYPE_SAVE, FILETYPE_MAP, "Save map", "Save", "maps", true, CallbackSaveMap, this); + m_QuickActionSaveAs.Call(); // ctrl+s to save else if(Input()->KeyPress(KEY_S) && ModPressed) { @@ -8362,6 +8356,7 @@ void CEditor::Init() m_vComponents.emplace_back(m_MapView); m_vComponents.emplace_back(m_MapSettingsBackend); m_vComponents.emplace_back(m_LayerSelector); + m_vComponents.emplace_back(m_Prompt); for(CEditorComponent &Component : m_vComponents) Component.OnInit(this); diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index bead2c217..8b5212a1a 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -38,6 +38,8 @@ #include "layer_selector.h" #include "map_view.h" #include "smooth_value.h" +#include +#include #include #include @@ -60,7 +62,8 @@ enum DIALOG_NONE = 0, DIALOG_FILE, - DIALOG_MAPSETTINGS_ERROR + DIALOG_MAPSETTINGS_ERROR, + DIALOG_QUICK_PROMPT, }; class CEditorImage; @@ -278,6 +281,7 @@ class CEditor : public IEditor std::vector> m_vComponents; CMapView m_MapView; CLayerSelector m_LayerSelector; + CPrompt m_Prompt; bool m_EditorWasUsedBefore = false; @@ -319,7 +323,16 @@ public: const CMapView *MapView() const { return &m_MapView; } CLayerSelector *LayerSelector() { return &m_LayerSelector; } + void AddGroup(); + void AddTileLayer(); +#define REGISTER_QUICK_ACTION(name, text, callback, disabled, active, button_color, description) CQuickAction m_QuickAction##name; +#include +#undef REGISTER_QUICK_ACTION + CEditor() : +#define REGISTER_QUICK_ACTION(name, text, callback, disabled, active, button_color, description) m_QuickAction##name(text, description, callback, disabled, active, button_color), +#include +#undef REGISTER_QUICK_ACTION m_ZoomEnvelopeX(1.0f, 0.1f, 600.0f), m_ZoomEnvelopeY(640.0f, 0.1f, 32000.0f), m_MapSettingsCommandContext(m_MapSettingsBackend.NewContext(&m_SettingsCommandInput)) diff --git a/src/game/editor/popups.cpp b/src/game/editor/popups.cpp index 22dc9d07f..45844b64a 100644 --- a/src/game/editor/popups.cpp +++ b/src/game/editor/popups.cpp @@ -107,9 +107,9 @@ CUi::EPopupMenuFunctionResult CEditor::PopupMenuFile(void *pContext, CUIRect Vie View.HSplitTop(2.0f, nullptr, &View); View.HSplitTop(12.0f, &Slot, &View); - if(pEditor->DoButton_MenuItem(&s_SaveAsButton, "Save As", 0, &Slot, 0, "Saves the current map under a new name (ctrl+shift+s)")) + if(pEditor->DoButton_MenuItem(&s_SaveAsButton, pEditor->m_QuickActionSaveAs.Label(), 0, &Slot, 0, pEditor->m_QuickActionSaveAs.Description())) { - pEditor->InvokeFileDialog(IStorage::TYPE_SAVE, FILETYPE_MAP, "Save map", "Save", "maps", true, CEditor::CallbackSaveMap, pEditor); + pEditor->m_QuickActionSaveAs.Call(); return CUi::POPUP_CLOSE_CURRENT; } @@ -578,16 +578,9 @@ CUi::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, // new tile layer View.HSplitBottom(5.0f, &View, nullptr); View.HSplitBottom(12.0f, &View, &Button); - static int s_NewTileLayerButton = 0; - if(pEditor->DoButton_Editor(&s_NewTileLayerButton, "Add tile layer", 0, &Button, 0, "Creates a new tile layer")) + if(pEditor->DoButton_Editor(&pEditor->m_QuickActionAddTileLayer, pEditor->m_QuickActionAddTileLayer.Label(), 0, &Button, 0, pEditor->m_QuickActionAddTileLayer.Description())) { - std::shared_ptr pTileLayer = std::make_shared(pEditor, pEditor->m_Map.m_pGameLayer->m_Width, pEditor->m_Map.m_pGameLayer->m_Height); - pTileLayer->m_pEditor = pEditor; - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pTileLayer); - int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; - pEditor->SelectLayer(LayerIndex); - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_Collapse = false; - pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); + pEditor->m_QuickActionAddTileLayer.Call(); return CUi::POPUP_CLOSE_CURRENT; } diff --git a/src/game/editor/prompt.cpp b/src/game/editor/prompt.cpp new file mode 100644 index 000000000..213466bf5 --- /dev/null +++ b/src/game/editor/prompt.cpp @@ -0,0 +1,166 @@ +#include +#include +#include + +#include "editor.h" + +#include "prompt.h" + +bool FuzzyMatch(const char *pHaystack, const char *pNeedle) +{ + if(!pNeedle || !pNeedle[0]) + return false; + char aBuf[2] = {0}; + const char *pHit = pHaystack; + int NeedleLen = str_length(pNeedle); + for(int i = 0; i < NeedleLen; i++) + { + if(!pHit) + return false; + aBuf[0] = pNeedle[i]; + pHit = str_find_nocase(pHit, aBuf); + if(pHit) + pHit++; + } + return pHit; +} + +bool CPrompt::IsActive() +{ + return CEditorComponent::IsActive() || Editor()->m_Dialog == DIALOG_QUICK_PROMPT; +} + +void CPrompt::SetActive() +{ + Editor()->m_Dialog = DIALOG_QUICK_PROMPT; + CEditorComponent::SetActive(); + + Ui()->SetActiveItem(&m_PromptInput); +} + +void CPrompt::SetInactive() +{ + m_ResetFilterResults = true; + m_PromptInput.Clear(); + if(Editor()->m_Dialog == DIALOG_QUICK_PROMPT) + Editor()->m_Dialog = DIALOG_NONE; + CEditorComponent::SetInactive(); +} + +bool CPrompt::OnInput(const IInput::CEvent &Event) +{ + if(Input()->ModifierIsPressed() && Input()->KeyIsPressed(KEY_P)) + { + SetActive(); + } + return false; +} + +void CPrompt::OnInit(CEditor *pEditor) +{ + CEditorComponent::OnInit(pEditor); + +#define REGISTER_QUICK_ACTION(name, text, callback, disabled, active, button_color, description) m_vQuickActions.emplace_back(&Editor()->m_QuickAction##name); +#include +#undef REGISTER_QUICK_ACTION +} + +void CPrompt::OnRender(CUIRect _) +{ + if(!IsActive()) + return; + + if(Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + { + SetInactive(); + return; + } + + static CListBox s_ListBox; + CUIRect Prompt, PromptBox; + CUIRect Suggestions; + + Ui()->MapScreen(); + CUIRect Overlay = *Ui()->Screen(); + + Overlay.Draw(ColorRGBA(0, 0, 0, 0.33f), IGraphics::CORNER_NONE, 0.0f); + CUIRect Background; + Overlay.VMargin(150.0f, &Background); + Background.HMargin(50.0f, &Background); + Background.Draw(ColorRGBA(0, 0, 0, 0.80f), IGraphics::CORNER_ALL, 5.0f); + + Background.Margin(10.0f, &Prompt); + + Prompt.VSplitMid(nullptr, &PromptBox); + + Prompt.HSplitTop(16.0f, &PromptBox, &Suggestions); + PromptBox.Draw(ColorRGBA(0, 0, 0, 0.75f), IGraphics::CORNER_ALL, 2.0f); + Suggestions.y += 6.0f; + + if(Ui()->DoClearableEditBox(&m_PromptInput, &PromptBox, 10.0f) || m_ResetFilterResults) + { + m_PromptSelectedIndex = 0; + m_vpFilteredPromptList.clear(); + if(m_ResetFilterResults && m_pLastAction) + { + m_vpFilteredPromptList.push_back(m_pLastAction); + } + for(auto *pQuickAction : m_vQuickActions) + { + if(m_PromptInput.IsEmpty() || FuzzyMatch(pQuickAction->Label(), m_PromptInput.GetString())) + { + bool Skip = false; + if(m_ResetFilterResults) + if(pQuickAction == m_pLastAction) + Skip = true; + if(!Skip) + m_vpFilteredPromptList.push_back(pQuickAction); + } + } + m_ResetFilterResults = false; + } + + s_ListBox.SetActive(!Ui()->IsPopupOpen()); + s_ListBox.DoStart(15.0f, m_vpFilteredPromptList.size(), 1, 5, m_PromptSelectedIndex, &Suggestions, false); + + for(size_t i = 0; i < m_vpFilteredPromptList.size(); i++) + { + const CListboxItem Item = s_ListBox.DoNextItem(m_vpFilteredPromptList[i], m_PromptSelectedIndex >= 0 && (size_t)m_PromptSelectedIndex == i); + if(!Item.m_Visible) + continue; + + CUIRect LabelColumn, DescColumn; + Item.m_Rect.VSplitLeft(5.0f, nullptr, &LabelColumn); + LabelColumn.VSplitLeft(100.0f, &LabelColumn, &DescColumn); + LabelColumn.VSplitRight(5.0f, &LabelColumn, nullptr); + + SLabelProperties Props; + Props.m_MaxWidth = LabelColumn.w; + Props.m_EllipsisAtEnd = true; + Ui()->DoLabel(&LabelColumn, m_vpFilteredPromptList[i]->Label(), 10.0f, TEXTALIGN_ML, Props); + + Props.m_MaxWidth = DescColumn.w; + ColorRGBA DescColor = TextRender()->DefaultTextColor(); + DescColor.a = Item.m_Selected ? 1.0f : 0.8f; + TextRender()->TextColor(DescColor); + Ui()->DoLabel(&DescColumn, m_vpFilteredPromptList[i]->Description(), 10.0f, TEXTALIGN_MR, Props); + TextRender()->TextColor(TextRender()->DefaultTextColor()); + } + + const int NewSelected = s_ListBox.DoEnd(); + if(m_PromptSelectedIndex != NewSelected) + { + m_PromptSelectedIndex = NewSelected; + } + + if(s_ListBox.WasItemActivated()) + { + if(m_PromptSelectedIndex >= 0) + { + const CQuickAction *pBtn = m_vpFilteredPromptList[m_PromptSelectedIndex]; + pBtn->Call(); + m_pLastAction = pBtn; + SetInactive(); + } + } +} diff --git a/src/game/editor/prompt.h b/src/game/editor/prompt.h new file mode 100644 index 000000000..4b616ec98 --- /dev/null +++ b/src/game/editor/prompt.h @@ -0,0 +1,29 @@ +#ifndef GAME_EDITOR_PROMPT_H +#define GAME_EDITOR_PROMPT_H + +#include +#include +#include + +#include "component.h" + +class CPrompt : public CEditorComponent +{ + bool m_ResetFilterResults = true; + const CQuickAction *m_pLastAction = nullptr; + int m_PromptSelectedIndex = -1; + + std::vector m_vpFilteredPromptList; + std::vector m_vQuickActions; + CLineInputBuffered<512> m_PromptInput; + +public: + void OnInit(CEditor *pEditor) override; + bool OnInput(const IInput::CEvent &Event) override; + void OnRender(CUIRect _) override; + bool IsActive(); + void SetActive(); + void SetInactive(); +}; + +#endif diff --git a/src/game/editor/quick_action.h b/src/game/editor/quick_action.h new file mode 100644 index 000000000..05efca84e --- /dev/null +++ b/src/game/editor/quick_action.h @@ -0,0 +1,58 @@ +#ifndef GAME_EDITOR_QUICK_ACTION_H +#define GAME_EDITOR_QUICK_ACTION_H + +#include +#include + +typedef std::function FButtonClickCallback; +typedef std::function FButtonDisabledCallback; +typedef std::function FButtonActiveCallback; +typedef std::function FButtonColorCallback; + +class CQuickAction +{ +private: + const char *m_pLabel; + const char *m_pDescription; + + FButtonClickCallback m_pfnCallback; + FButtonDisabledCallback m_pfnDisabledCallback; + FButtonActiveCallback m_pfnActiveCallback; + FButtonColorCallback m_pfnColorCallback; + +public: + CQuickAction( + const char *pLabel, + const char *pDescription, + FButtonClickCallback pfnCallback, + FButtonDisabledCallback pfnDisabledCallback, + FButtonActiveCallback pfnActiveCallback, + FButtonColorCallback pfnColorCallback) : + m_pLabel(pLabel), + m_pDescription(pDescription), + m_pfnCallback(std::move(pfnCallback)), + m_pfnDisabledCallback(std::move(pfnDisabledCallback)), + m_pfnActiveCallback(std::move(pfnActiveCallback)), + m_pfnColorCallback(std::move(pfnColorCallback)) + { + } + + // code to run when the action is triggered + void Call() const { m_pfnCallback(); } + + // bool that indicates if the action can be performed not or not + bool Disabled() { return m_pfnDisabledCallback(); } + + // bool that indicates if the action is currently running + // only applies to actions that can be turned on or off like proof borders + bool Active() { return m_pfnActiveCallback(); } + + // color "enum" that represents the state of the quick actions button + // used as Checked argument for DoButton_Editor() + int Color() { return m_pfnColorCallback(); } + + const char *Label() const { return m_pLabel; } + const char *Description() const { return m_pDescription; } +}; + +#endif diff --git a/src/game/editor/quick_actions.cpp b/src/game/editor/quick_actions.cpp new file mode 100644 index 000000000..b9ced6d57 --- /dev/null +++ b/src/game/editor/quick_actions.cpp @@ -0,0 +1,20 @@ +#include "editor.h" + +#include "editor_actions.h" + +void CEditor::AddGroup() +{ + m_Map.NewGroup(); + m_SelectedGroup = m_Map.m_vpGroups.size() - 1; + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, false)); +} +void CEditor::AddTileLayer() +{ + std::shared_ptr pTileLayer = std::make_shared(this, m_Map.m_pGameLayer->m_Width, m_Map.m_pGameLayer->m_Height); + pTileLayer->m_pEditor = this; + m_Map.m_vpGroups[m_SelectedGroup]->AddLayer(pTileLayer); + int LayerIndex = m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size() - 1; + SelectLayer(LayerIndex); + m_Map.m_vpGroups[m_SelectedGroup]->m_Collapse = false; + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, LayerIndex)); +} diff --git a/src/game/editor/quick_actions.h b/src/game/editor/quick_actions.h new file mode 100644 index 000000000..d581fd130 --- /dev/null +++ b/src/game/editor/quick_actions.h @@ -0,0 +1,50 @@ +// This file can be included several times. + +#ifndef REGISTER_QUICK_ACTION +#define REGISTER_QUICK_ACTION(name, text, callback, disabled, active, button_color, description) +#endif + +#define ALWAYS_FALSE []() -> bool { return false; } +#define DEFAULT_BTN []() -> int { return -1; } + +REGISTER_QUICK_ACTION( + AddGroup, "Add group", [&]() { AddGroup(); }, ALWAYS_FALSE, ALWAYS_FALSE, DEFAULT_BTN, "Adds a new group") +REGISTER_QUICK_ACTION( + Refocus, "Refocus", [&]() { MapView()->Focus(); }, ALWAYS_FALSE, ALWAYS_FALSE, DEFAULT_BTN, "[HOME] Restore map focus") +REGISTER_QUICK_ACTION( + Proof, + "Proof", + [&]() { MapView()->ProofMode()->Toggle(); }, + ALWAYS_FALSE, + [&]() -> bool { return MapView()->ProofMode()->IsEnabled(); }, + DEFAULT_BTN, + "Toggles proof borders. These borders represent the area that a player can see with default zoom.") +REGISTER_QUICK_ACTION( + AddTileLayer, "Add tile layer", [&]() { AddTileLayer(); }, ALWAYS_FALSE, ALWAYS_FALSE, DEFAULT_BTN, "Creates a new tile layer.") +REGISTER_QUICK_ACTION( + SaveAs, + "Save As", + [&]() { InvokeFileDialog(IStorage::TYPE_SAVE, FILETYPE_MAP, "Save map", "Save As", "maps", true, CEditor::CallbackSaveMap, this); }, + ALWAYS_FALSE, + ALWAYS_FALSE, + DEFAULT_BTN, + "Saves the current map under a new name (ctrl+shift+s)") +REGISTER_QUICK_ACTION( + Envelopes, + "Envelopes", + [&]() { m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES ? EXTRAEDITOR_NONE : EXTRAEDITOR_ENVELOPES; }, + ALWAYS_FALSE, + ALWAYS_FALSE, + [&]() -> int { return m_ShowPicker ? -1 : m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES; }, + "Toggles the envelope editor.") +REGISTER_QUICK_ACTION( + AddImage, + "Add Image", + [&]() { InvokeFileDialog(IStorage::TYPE_ALL, FILETYPE_IMG, "Add Image", "Add", "mapres", false, AddImage, this); }, + ALWAYS_FALSE, + ALWAYS_FALSE, + DEFAULT_BTN, + "Load a new image to use in the map") + +#undef ALWAYS_FALSE +#undef DEFAULT_BTN