Browse Source

Options: Extract padmapper handling from options

Options now only contain the padmapper settings, not the padmapping
handling code.
pull/7670/head
Gleb Mazovetskiy 1 year ago
parent
commit
7afdbe8fdc
  1. 9
      Source/CMakeLists.txt
  2. 11
      Source/controls/controller.h
  3. 11
      Source/controls/controller_buttons.h
  4. 28
      Source/controls/controller_motion.cpp
  5. 37
      Source/controls/game_controls.cpp
  6. 66
      Source/controls/padmapper.cpp
  7. 16
      Source/controls/padmapper.hpp
  8. 3
      Source/engine/events.cpp
  9. 88
      Source/options.cpp
  10. 27
      Source/options.h

9
Source/CMakeLists.txt

@ -219,6 +219,14 @@ target_link_dependencies(libdevilutionx_controller_buttons
DevilutionX::SDL DevilutionX::SDL
) )
add_devilutionx_object_library(libdevilutionx_padmapper
controls/padmapper.cpp
)
target_link_dependencies(libdevilutionx_padmapper PUBLIC
libdevilutionx_controller_buttons
libdevilutionx_options
)
add_devilutionx_object_library(libdevilutionx_crawl add_devilutionx_object_library(libdevilutionx_crawl
crawl.cpp crawl.cpp
) )
@ -662,6 +670,7 @@ target_link_dependencies(libdevilutionx PUBLIC
libdevilutionx_mpq libdevilutionx_mpq
libdevilutionx_multiplayer libdevilutionx_multiplayer
libdevilutionx_options libdevilutionx_options
libdevilutionx_padmapper
libdevilutionx_parse_int libdevilutionx_parse_int
libdevilutionx_pathfinding libdevilutionx_pathfinding
libdevilutionx_pkware_encrypt libdevilutionx_pkware_encrypt

11
Source/controls/controller.h

@ -7,17 +7,6 @@
namespace devilution { namespace devilution {
struct ControllerButtonEvent {
ControllerButtonEvent(ControllerButton button, bool up)
: button(button)
, up(up)
{
}
ControllerButton button;
bool up;
};
// Must be called exactly once at the start of each SDL input event. // Must be called exactly once at the start of each SDL input event.
void UnlockControllerState(const SDL_Event &event); void UnlockControllerState(const SDL_Event &event);

11
Source/controls/controller_buttons.h

@ -55,6 +55,17 @@ struct ControllerButtonCombo {
ControllerButton button; ControllerButton button;
}; };
struct ControllerButtonEvent {
ControllerButtonEvent(ControllerButton button, bool up)
: button(button)
, up(up)
{
}
ControllerButton button;
bool up;
};
inline bool IsDPadButton(ControllerButton button) inline bool IsDPadButton(ControllerButton button)
{ {
return button == ControllerButton_BUTTON_DPAD_UP return button == ControllerButton_BUTTON_DPAD_UP

28
Source/controls/controller_motion.cpp

@ -9,6 +9,7 @@
#endif #endif
#include "controls/devices/joystick.h" #include "controls/devices/joystick.h"
#include "controls/game_controls.h" #include "controls/game_controls.h"
#include "controls/padmapper.hpp"
#include "controls/plrctrls.h" #include "controls/plrctrls.h"
#include "controls/touch/gamepad.h" #include "controls/touch/gamepad.h"
#include "engine/demomode.h" #include "engine/demomode.h"
@ -71,16 +72,15 @@ void ScaleJoystickAxes(float *x, float *y, float deadzone)
bool IsMovementOverriddenByPadmapper(ControllerButton button) bool IsMovementOverriddenByPadmapper(ControllerButton button)
{ {
ControllerButtonEvent releaseEvent { button, true }; ControllerButtonEvent releaseEvent { button, true };
const Options &options = GetOptions(); std::string_view actionName = PadmapperActionNameTriggeredByButtonEvent(releaseEvent);
std::string_view actionName = options.Padmapper.ActionNameTriggeredByButtonEvent(releaseEvent); ControllerButtonCombo buttonCombo = GetOptions().Padmapper.ButtonComboForAction(actionName);
ControllerButtonCombo buttonCombo = options.Padmapper.ButtonComboForAction(actionName);
return buttonCombo.modifier != ControllerButton_NONE; return buttonCombo.modifier != ControllerButton_NONE;
} }
bool TriggersQuickSpellAction(ControllerButton button) bool TriggersQuickSpellAction(ControllerButton button)
{ {
ControllerButtonEvent releaseEvent { button, true }; ControllerButtonEvent releaseEvent { button, true };
std::string_view actionName = GetOptions().Padmapper.ActionNameTriggeredByButtonEvent(releaseEvent); std::string_view actionName = PadmapperActionNameTriggeredByButtonEvent(releaseEvent);
std::string_view prefix { "QuickSpell" }; std::string_view prefix { "QuickSpell" };
if (actionName.size() < prefix.size()) if (actionName.size() < prefix.size())
@ -211,12 +211,11 @@ AxisDirection GetLeftStickOrDpadDirection(bool usePadmapper)
bool isLeftPressed = stickX <= -0.5; bool isLeftPressed = stickX <= -0.5;
bool isRightPressed = stickX >= 0.5; bool isRightPressed = stickX >= 0.5;
const Options &options = GetOptions();
if (usePadmapper) { if (usePadmapper) {
isUpPressed |= options.Padmapper.IsActive("MoveUp"); isUpPressed |= PadmapperIsActionActive("MoveUp");
isDownPressed |= options.Padmapper.IsActive("MoveDown"); isDownPressed |= PadmapperIsActionActive("MoveDown");
isLeftPressed |= options.Padmapper.IsActive("MoveLeft"); isLeftPressed |= PadmapperIsActionActive("MoveLeft");
isRightPressed |= options.Padmapper.IsActive("MoveRight"); isRightPressed |= PadmapperIsActionActive("MoveRight");
} else if (!SimulatingMouseWithPadmapper) { } else if (!SimulatingMouseWithPadmapper) {
isUpPressed |= IsPressedForMovement(ControllerButton_BUTTON_DPAD_UP); isUpPressed |= IsPressedForMovement(ControllerButton_BUTTON_DPAD_UP);
isDownPressed |= IsPressedForMovement(ControllerButton_BUTTON_DPAD_DOWN); isDownPressed |= IsPressedForMovement(ControllerButton_BUTTON_DPAD_DOWN);
@ -255,8 +254,7 @@ void SimulateRightStickWithPadmapper(ControllerButtonEvent ctrlEvent)
if (!ctrlEvent.up && ctrlEvent.button == SuppressedButton) if (!ctrlEvent.up && ctrlEvent.button == SuppressedButton)
return; return;
const Options &options = GetOptions(); std::string_view actionName = PadmapperActionNameTriggeredByButtonEvent(ctrlEvent);
std::string_view actionName = options.Padmapper.ActionNameTriggeredByButtonEvent(ctrlEvent);
bool upTriggered = actionName == "MouseUp"; bool upTriggered = actionName == "MouseUp";
bool downTriggered = actionName == "MouseDown"; bool downTriggered = actionName == "MouseDown";
bool leftTriggered = actionName == "MouseLeft"; bool leftTriggered = actionName == "MouseLeft";
@ -267,10 +265,10 @@ void SimulateRightStickWithPadmapper(ControllerButtonEvent ctrlEvent)
return; return;
} }
bool upActive = (upTriggered && !ctrlEvent.up) || (!upTriggered && options.Padmapper.IsActive("MouseUp")); bool upActive = (upTriggered && !ctrlEvent.up) || (!upTriggered && PadmapperIsActionActive("MouseUp"));
bool downActive = (downTriggered && !ctrlEvent.up) || (!downTriggered && options.Padmapper.IsActive("MouseDown")); bool downActive = (downTriggered && !ctrlEvent.up) || (!downTriggered && PadmapperIsActionActive("MouseDown"));
bool leftActive = (leftTriggered && !ctrlEvent.up) || (!leftTriggered && options.Padmapper.IsActive("MouseLeft")); bool leftActive = (leftTriggered && !ctrlEvent.up) || (!leftTriggered && PadmapperIsActionActive("MouseLeft"));
bool rightActive = (rightTriggered && !ctrlEvent.up) || (!rightTriggered && options.Padmapper.IsActive("MouseRight")); bool rightActive = (rightTriggered && !ctrlEvent.up) || (!rightTriggered && PadmapperIsActionActive("MouseRight"));
rightStickX = 0; rightStickX = 0;
rightStickY = 0; rightStickY = 0;

37
Source/controls/game_controls.cpp

@ -7,6 +7,7 @@
#include "controls/devices/game_controller.h" #include "controls/devices/game_controller.h"
#endif #endif
#include "controls/devices/joystick.h" #include "controls/devices/joystick.h"
#include "controls/padmapper.hpp"
#include "controls/plrctrls.h" #include "controls/plrctrls.h"
#include "controls/touch/gamepad.h" #include "controls/touch/gamepad.h"
#include "doom.h" #include "doom.h"
@ -214,6 +215,28 @@ bool GetGameAction(const SDL_Event &event, ControllerButtonEvent ctrlEvent, Game
return false; return false;
} }
bool CanDeferToMovementHandler(const PadmapperOptions::Action &action)
{
if (action.boundInput.modifier != ControllerButton_NONE)
return false;
if (SpellSelectFlag) {
const std::string_view prefix { "QuickSpell" };
const std::string_view key { action.key };
if (key.size() >= prefix.size()) {
const std::string_view truncated { key.data(), prefix.size() };
if (truncated == prefix)
return false;
}
}
return IsAnyOf(action.boundInput.button,
ControllerButton_BUTTON_DPAD_UP,
ControllerButton_BUTTON_DPAD_DOWN,
ControllerButton_BUTTON_DPAD_LEFT,
ControllerButton_BUTTON_DPAD_RIGHT);
}
void PressControllerButton(ControllerButton button) void PressControllerButton(ControllerButton button)
{ {
if (IsStashOpen) { if (IsStashOpen) {
@ -298,7 +321,10 @@ void PressControllerButton(ControllerButton button)
} }
} }
GetOptions().Padmapper.ButtonPressed(button); const PadmapperOptions::Action *action = GetOptions().Padmapper.findAction(button, IsControllerButtonPressed);
if (action == nullptr) return;
if (IsMovementHandlerActive() && CanDeferToMovementHandler(*action)) return;
PadmapperPress(button, *action);
} }
} // namespace } // namespace
@ -337,7 +363,7 @@ bool IsSimulatedMouseClickBinding(ControllerButtonEvent ctrlEvent)
return false; return false;
if (!ctrlEvent.up && ctrlEvent.button == SuppressedButton) if (!ctrlEvent.up && ctrlEvent.button == SuppressedButton)
return false; return false;
std::string_view actionName = GetOptions().Padmapper.ActionNameTriggeredByButtonEvent(ctrlEvent); const std::string_view actionName = PadmapperActionNameTriggeredByButtonEvent(ctrlEvent);
return IsAnyOf(actionName, "LeftMouseClick1", "LeftMouseClick2", "RightMouseClick1", "RightMouseClick2"); return IsAnyOf(actionName, "LeftMouseClick1", "LeftMouseClick2", "RightMouseClick1", "RightMouseClick2");
} }
@ -355,8 +381,7 @@ bool HandleControllerButtonEvent(const SDL_Event &event, const ControllerButtonE
struct ButtonReleaser { struct ButtonReleaser {
~ButtonReleaser() ~ButtonReleaser()
{ {
if (ctrlEvent.up) if (ctrlEvent.up) PadmapperRelease(ctrlEvent.button, /*invokeAction=*/false);
GetOptions().Padmapper.ButtonReleased(ctrlEvent.button, false);
} }
ControllerButtonEvent ctrlEvent; ControllerButtonEvent ctrlEvent;
}; };
@ -377,10 +402,10 @@ bool HandleControllerButtonEvent(const SDL_Event &event, const ControllerButtonE
SuppressedButton = ControllerButton_NONE; SuppressedButton = ControllerButton_NONE;
} }
if (ctrlEvent.up && GetOptions().Padmapper.ActionNameTriggeredByButtonEvent(ctrlEvent) != "") { if (ctrlEvent.up && !PadmapperActionNameTriggeredByButtonEvent(ctrlEvent).empty()) {
// Button press may have brought up a menu; // Button press may have brought up a menu;
// don't confuse release of that button with intent to interact with the menu // don't confuse release of that button with intent to interact with the menu
GetOptions().Padmapper.ButtonReleased(ctrlEvent.button); PadmapperRelease(ctrlEvent.button, /*invokeAction=*/true);
return true; return true;
} else if (GetGameAction(event, ctrlEvent, &action)) { } else if (GetGameAction(event, ctrlEvent, &action)) {
ProcessGameAction(action); ProcessGameAction(action);

66
Source/controls/padmapper.cpp

@ -0,0 +1,66 @@
#include "controls/padmapper.hpp"
#include <array>
#include "options.h"
namespace devilution {
namespace {
std::array<const PadmapperOptions::Action *, enum_size<ControllerButton>::value> ButtonToReleaseAction;
} // namespace
void PadmapperPress(ControllerButton button, const PadmapperOptions::Action &action)
{
if (action.actionPressed) action.actionPressed();
SuppressedButton = action.boundInput.modifier;
ButtonToReleaseAction[static_cast<size_t>(button)] = &action;
}
void PadmapperRelease(ControllerButton button, bool invokeAction)
{
if (invokeAction) {
const PadmapperOptions::Action *action = ButtonToReleaseAction[static_cast<size_t>(button)];
if (action == nullptr)
return; // Ignore unmapped buttons.
// Check that the action can be triggered.
if (action->actionReleased && action->isEnabled())
action->actionReleased();
}
ButtonToReleaseAction[static_cast<size_t>(button)] = nullptr;
}
bool PadmapperIsActionActive(std::string_view actionName)
{
for (const PadmapperOptions::Action &action : GetOptions().Padmapper.actions) {
if (action.key != actionName)
continue;
const PadmapperOptions::Action *releaseAction = ButtonToReleaseAction[static_cast<size_t>(action.boundInput.button)];
return releaseAction != nullptr && releaseAction->key == actionName;
}
return false;
}
void PadmapperReleaseAllActiveButtons()
{
for (const PadmapperOptions::Action *action : ButtonToReleaseAction) {
if (action != nullptr) {
PadmapperRelease(action->boundInput.button, /*invokeAction=*/true);
}
}
}
std::string_view PadmapperActionNameTriggeredByButtonEvent(ControllerButtonEvent ctrlEvent)
{
if (!ctrlEvent.up) {
const PadmapperOptions::Action *pressAction = GetOptions().Padmapper.findAction(ctrlEvent.button, IsControllerButtonPressed);
if (pressAction == nullptr) return {};
return pressAction->key;
}
const PadmapperOptions::Action *releaseAction = ButtonToReleaseAction[static_cast<size_t>(ctrlEvent.button)];
if (releaseAction == nullptr) return {};
return releaseAction->key;
}
} // namespace devilution

16
Source/controls/padmapper.hpp

@ -0,0 +1,16 @@
#pragma once
#include <string_view>
#include "controls/controller_buttons.h"
#include "options.h"
namespace devilution {
void PadmapperPress(ControllerButton button, const PadmapperOptions::Action &action);
void PadmapperRelease(ControllerButton button, bool invokeAction);
void PadmapperReleaseAllActiveButtons();
[[nodiscard]] bool PadmapperIsActionActive(std::string_view actionName);
[[nodiscard]] std::string_view PadmapperActionNameTriggeredByButtonEvent(ControllerButtonEvent ctrlEvent);
} // namespace devilution

3
Source/engine/events.cpp

@ -3,6 +3,7 @@
#include <cstdint> #include <cstdint>
#include "controls/input.h" #include "controls/input.h"
#include "controls/padmapper.hpp"
#include "engine/demomode.h" #include "engine/demomode.h"
#include "engine/render/primitive_render.hpp" #include "engine/render/primitive_render.hpp"
#include "interfac.h" #include "interfac.h"
@ -143,7 +144,7 @@ EventHandler CurrentEventHandler;
EventHandler SetEventHandler(EventHandler eventHandler) EventHandler SetEventHandler(EventHandler eventHandler)
{ {
GetOptions().Padmapper.ReleaseAllActiveButtons(); PadmapperReleaseAllActiveButtons();
EventHandler previousHandler = CurrentEventHandler; EventHandler previousHandler = CurrentEventHandler;
CurrentEventHandler = eventHandler; CurrentEventHandler = eventHandler;

88
Source/options.cpp

@ -1387,9 +1387,9 @@ std::vector<OptionEntryBase *> PadmapperOptions::GetEntries()
PadmapperOptions::Action::Action(std::string_view key, const char *name, const char *description, ControllerButtonCombo defaultInput, std::function<void()> actionPressed, std::function<void()> actionReleased, std::function<bool()> enable, unsigned index) PadmapperOptions::Action::Action(std::string_view key, const char *name, const char *description, ControllerButtonCombo defaultInput, std::function<void()> actionPressed, std::function<void()> actionReleased, std::function<bool()> enable, unsigned index)
: OptionEntryBase(key, OptionEntryFlags::None, name, description) : OptionEntryBase(key, OptionEntryFlags::None, name, description)
, defaultInput(defaultInput)
, actionPressed(std::move(actionPressed)) , actionPressed(std::move(actionPressed))
, actionReleased(std::move(actionReleased)) , actionReleased(std::move(actionReleased))
, defaultInput(defaultInput)
, enable(std::move(enable)) , enable(std::move(enable))
, dynamicIndex(index) , dynamicIndex(index)
{ {
@ -1548,66 +1548,6 @@ void PadmapperOptions::CommitActions()
committed = true; committed = true;
} }
void PadmapperOptions::ButtonPressed(ControllerButton button)
{
const Action *action = FindAction(button);
if (action == nullptr)
return;
if (IsMovementHandlerActive() && CanDeferToMovementHandler(*action))
return;
if (action->actionPressed)
action->actionPressed();
SuppressedButton = action->boundInput.modifier;
buttonToReleaseAction[static_cast<size_t>(button)] = action;
}
void PadmapperOptions::ButtonReleased(ControllerButton button, bool invokeAction)
{
if (invokeAction) {
const Action *action = buttonToReleaseAction[static_cast<size_t>(button)];
if (action == nullptr)
return; // Ignore unmapped buttons.
// Check that the action can be triggered.
if (action->actionReleased && (!action->enable || action->enable()))
action->actionReleased();
}
buttonToReleaseAction[static_cast<size_t>(button)] = nullptr;
}
void PadmapperOptions::ReleaseAllActiveButtons()
{
for (auto *action : buttonToReleaseAction) {
if (action == nullptr)
continue;
ControllerButton button = action->boundInput.button;
ButtonReleased(button, true);
}
}
bool PadmapperOptions::IsActive(std::string_view actionName) const
{
for (const Action &action : actions) {
if (action.key != actionName)
continue;
const Action *releaseAction = buttonToReleaseAction[static_cast<size_t>(action.boundInput.button)];
return releaseAction != nullptr && releaseAction->key == actionName;
}
return false;
}
std::string_view PadmapperOptions::ActionNameTriggeredByButtonEvent(ControllerButtonEvent ctrlEvent) const
{
if (!ctrlEvent.up) {
const Action *pressAction = FindAction(ctrlEvent.button);
return pressAction != nullptr ? pressAction->key : "";
}
const Action *releaseAction = buttonToReleaseAction[static_cast<size_t>(ctrlEvent.button)];
if (releaseAction == nullptr)
return "";
return releaseAction->key;
}
std::string_view PadmapperOptions::InputNameForAction(std::string_view actionName, bool useShortName) const std::string_view PadmapperOptions::InputNameForAction(std::string_view actionName, bool useShortName) const
{ {
for (const Action &action : actions) { for (const Action &action : actions) {
@ -1628,7 +1568,7 @@ ControllerButtonCombo PadmapperOptions::ButtonComboForAction(std::string_view ac
return ControllerButton_NONE; return ControllerButton_NONE;
} }
const PadmapperOptions::Action *PadmapperOptions::FindAction(ControllerButton button) const const PadmapperOptions::Action *PadmapperOptions::findAction(ControllerButton button, tl::function_ref<bool(ControllerButton)> isModifierPressed) const
{ {
// To give preference to button combinations, // To give preference to button combinations,
// first pass ignores mappings where no modifier is bound // first pass ignores mappings where no modifier is bound
@ -1638,7 +1578,7 @@ const PadmapperOptions::Action *PadmapperOptions::FindAction(ControllerButton bu
continue; continue;
if (button != combo.button) if (button != combo.button)
continue; continue;
if (!IsControllerButtonPressed(combo.modifier)) if (!isModifierPressed(combo.modifier))
continue; continue;
if (action.enable && !action.enable()) if (action.enable && !action.enable())
continue; continue;
@ -1659,28 +1599,6 @@ const PadmapperOptions::Action *PadmapperOptions::FindAction(ControllerButton bu
return nullptr; return nullptr;
} }
bool PadmapperOptions::CanDeferToMovementHandler(const Action &action) const
{
if (action.boundInput.modifier != ControllerButton_NONE)
return false;
if (SpellSelectFlag) {
const std::string_view prefix { "QuickSpell" };
const std::string_view key { action.key };
if (key.size() >= prefix.size()) {
const std::string_view truncated { key.data(), prefix.size() };
if (truncated == prefix)
return false;
}
}
return IsAnyOf(action.boundInput.button,
ControllerButton_BUTTON_DPAD_UP,
ControllerButton_BUTTON_DPAD_DOWN,
ControllerButton_BUTTON_DPAD_LEFT,
ControllerButton_BUTTON_DPAD_RIGHT);
}
ModOptions::ModOptions() ModOptions::ModOptions()
: OptionCategoryBase("Mods", N_("Mods"), N_("Mod Settings")) : OptionCategoryBase("Mods", N_("Mods"), N_("Mod Settings"))
{ {

27
Source/options.h

@ -131,8 +131,10 @@ public:
OptionEntryFlags flags; OptionEntryFlags flags;
protected: public:
std::string_view key; std::string_view key;
protected:
const char *name; const char *name;
const char *description; const char *description;
void NotifyValueChanged(); void NotifyValueChanged();
@ -763,12 +765,15 @@ struct PadmapperOptions : OptionCategoryBase {
bool SetValue(ControllerButtonCombo value); bool SetValue(ControllerButtonCombo value);
private: [[nodiscard]] bool isEnabled() const { return !enable || enable(); }
ControllerButtonCombo defaultInput;
std::function<void()> actionPressed; std::function<void()> actionPressed;
std::function<void()> actionReleased; std::function<void()> actionReleased;
ControllerButtonCombo boundInput;
private:
ControllerButtonCombo defaultInput;
std::function<bool()> enable; std::function<bool()> enable;
ControllerButtonCombo boundInput {};
mutable GamepadLayout boundInputDescriptionType = GamepadLayout::Generic; mutable GamepadLayout boundInputDescriptionType = GamepadLayout::Generic;
mutable std::string boundInputDescription; mutable std::string boundInputDescription;
mutable std::string boundInputShortDescription; mutable std::string boundInputShortDescription;
@ -792,23 +797,17 @@ struct PadmapperOptions : OptionCategoryBase {
std::function<bool()> enable = nullptr, std::function<bool()> enable = nullptr,
unsigned index = 0); unsigned index = 0);
void CommitActions(); void CommitActions();
void ButtonPressed(ControllerButton button);
void ButtonReleased(ControllerButton button, bool invokeAction = true);
void ReleaseAllActiveButtons();
bool IsActive(std::string_view actionName) const;
std::string_view ActionNameTriggeredByButtonEvent(ControllerButtonEvent ctrlEvent) const;
std::string_view InputNameForAction(std::string_view actionName, bool useShortName = false) const; std::string_view InputNameForAction(std::string_view actionName, bool useShortName = false) const;
ControllerButtonCombo ButtonComboForAction(std::string_view actionName) const; ControllerButtonCombo ButtonComboForAction(std::string_view actionName) const;
private: [[nodiscard]] const Action *findAction(ControllerButton button, tl::function_ref<bool(ControllerButton)> isModifierPressed) const;
std::forward_list<Action> actions; std::forward_list<Action> actions;
std::array<const Action *, enum_size<ControllerButton>::value> buttonToReleaseAction;
private:
std::array<std::string, enum_size<ControllerButton>::value> buttonToButtonName; std::array<std::string, enum_size<ControllerButton>::value> buttonToButtonName;
ankerl::unordered_dense::segmented_map<std::string, ControllerButton, StringViewHash, StringViewEquals> buttonNameToButton; ankerl::unordered_dense::segmented_map<std::string, ControllerButton, StringViewHash, StringViewEquals> buttonNameToButton;
bool committed = false; bool committed = false;
const Action *FindAction(ControllerButton button) const;
bool CanDeferToMovementHandler(const Action &action) const;
}; };
struct ModOptions : OptionCategoryBase { struct ModOptions : OptionCategoryBase {

Loading…
Cancel
Save