#pragma once #include #include #include #include #include #include #include #ifdef GEODE_IS_WINDOWS #ifdef HJFOD_CUSTOM_KEYBINDS_EXPORTING #define CUSTOM_KEYBINDS_DLL __declspec(dllexport) #else #define CUSTOM_KEYBINDS_DLL __declspec(dllimport) #endif #else #define CUSTOM_KEYBINDS_DLL __attribute__((visibility("default"))) #endif struct BindSaveData; namespace keybinds { class Bind; class BindManager; using DeviceID = std::string; /** * Base class for implementing bindings for different input devices */ class CUSTOM_KEYBINDS_DLL Bind : public cocos2d::CCObject { protected: friend class BindManager; public: /** * Get the hash for this bind */ virtual size_t getHash() const = 0; /** * Check if this bind is equal to another. By default compares hashes */ virtual bool isEqual(Bind* other) const; /** * Get the bind's representation as a human-readable string */ virtual std::string toString() const = 0; virtual cocos2d::CCNode* createLabel() const; virtual DeviceID getDeviceID() const = 0; virtual matjson::Value save() const = 0; virtual ~Bind() = default; cocos2d::CCNodeRGBA* createBindSprite() const; }; enum class Modifier : unsigned int { None = 0b0000, Control = 0b0001, Shift = 0b0010, Alt = 0b0100, Command = 0b1000, #ifdef GEODE_IS_MACOS PlatformControl = Command, #else PlatformControl = Control, #endif }; CUSTOM_KEYBINDS_DLL Modifier operator|(Modifier const& a, Modifier const& b); CUSTOM_KEYBINDS_DLL Modifier operator|=(Modifier& a, Modifier const& b); CUSTOM_KEYBINDS_DLL bool operator&(Modifier const& a, Modifier const& b); CUSTOM_KEYBINDS_DLL std::string keyToString(cocos2d::enumKeyCodes key); CUSTOM_KEYBINDS_DLL bool keyIsModifier(cocos2d::enumKeyCodes key); CUSTOM_KEYBINDS_DLL bool keyIsController(cocos2d::enumKeyCodes key); enum class MouseButton { Button4 = 0, Button5 = 1, }; CUSTOM_KEYBINDS_DLL std::string mouseToString(MouseButton button); class CUSTOM_KEYBINDS_DLL Keybind final : public Bind { protected: cocos2d::enumKeyCodes m_key; Modifier m_modifiers; public: static Keybind* create(cocos2d::enumKeyCodes key, Modifier modifiers = Modifier::None); static Keybind* parse(matjson::Value const&); cocos2d::enumKeyCodes getKey() const; Modifier getModifiers() const; size_t getHash() const override; bool isEqual(Bind* other) const override; std::string toString() const override; DeviceID getDeviceID() const override; matjson::Value save() const override; }; class CUSTOM_KEYBINDS_DLL MouseBind final : public Bind { protected: MouseButton m_button; Modifier m_modifiers; public: static MouseBind* create(MouseButton button, Modifier modifiers = Modifier::None); static MouseBind* parse(matjson::Value const&); MouseButton getButton() const; Modifier getModifiers() const; size_t getHash() const override; bool isEqual(Bind* other) const override; std::string toString() const override; DeviceID getDeviceID() const override; matjson::Value save() const override; }; class CUSTOM_KEYBINDS_DLL ControllerBind final : public Bind { protected: cocos2d::enumKeyCodes m_button; public: static ControllerBind* create(cocos2d::enumKeyCodes button); static ControllerBind* parse(matjson::Value const&); cocos2d::enumKeyCodes getButton() const; size_t getHash() const override; bool isEqual(Bind* other) const override; std::string toString() const override; cocos2d::CCNode* createLabel() const override; DeviceID getDeviceID() const override; matjson::Value save() const override; }; struct CUSTOM_KEYBINDS_DLL BindHash { geode::Ref bind; BindHash(Bind* bind); bool operator==(BindHash const& other) const; }; } namespace std { template <> struct hash { CUSTOM_KEYBINDS_DLL std::size_t operator()(keybinds::BindHash const&) const; }; } namespace keybinds { class BindManager; class InvokeBindFilter; using ActionID = std::string; class CUSTOM_KEYBINDS_DLL Category final { std::string m_value; public: Category() = default; Category(const char* path); Category(std::string const& path); std::vector getPath() const; std::optional getParent() const; bool hasParent(Category const& parent) const; std::string toString() const; bool operator==(Category const&) const; static constexpr auto PLAY { "Play" }; static constexpr auto PLAY_PAUSE { "Play/Pause" }; static constexpr auto EDITOR { "Editor" }; static constexpr auto GLOBAL { "Global" }; static constexpr auto EDITOR_UI { "Editor/UI" }; static constexpr auto EDITOR_MODIFY { "Editor/Modify" }; static constexpr auto EDITOR_MOVE { "Editor/Move" }; }; class CUSTOM_KEYBINDS_DLL BindableAction { protected: ActionID m_id; std::string m_name; std::string m_description; geode::Mod* m_owner; std::vector> m_defaults; Category m_category; bool m_repeatable; public: ActionID getID() const; std::string getName() const; std::string getDescription() const; geode::Mod* getMod() const; std::vector> getDefaults() const; Category getCategory() const; bool isRepeatable() const; BindableAction() = default; BindableAction( ActionID const& id, std::string const& name, std::string const& description = "", std::vector> const& defaults = {}, Category const& category = Category(), bool repeatable = true, geode::Mod* owner = geode::Mod::get() ); }; class CUSTOM_KEYBINDS_DLL InvokeBindEvent : public geode::Event { protected: ActionID m_id; bool m_down; friend class BindManager; friend class InvokeBindFilter; public: InvokeBindEvent(ActionID const& id, bool down); ActionID getID() const; bool isDown() const; }; class CUSTOM_KEYBINDS_DLL InvokeBindFilter : public geode::EventFilter { protected: cocos2d::CCNode* m_target; ActionID m_id; public: using Callback = geode::ListenerResult(InvokeBindEvent*); geode::ListenerResult handle(std::function fn, InvokeBindEvent* event); InvokeBindFilter(cocos2d::CCNode* target, ActionID const& id); }; class CUSTOM_KEYBINDS_DLL PressBindEvent : public geode::Event { protected: Bind* m_bind; bool m_down; public: PressBindEvent(Bind* bind, bool down); Bind* getBind() const; bool isDown() const; }; class CUSTOM_KEYBINDS_DLL PressBindFilter : public geode::EventFilter { public: using Callback = geode::ListenerResult(PressBindEvent*); geode::ListenerResult handle(std::function fn, PressBindEvent* event); PressBindFilter(); }; class CUSTOM_KEYBINDS_DLL DeviceEvent : public geode::Event { protected: DeviceID m_id; bool m_attached; public: DeviceEvent(DeviceID const& id, bool attached); DeviceID getID() const; bool wasAttached() const; bool wasDetached() const; }; class CUSTOM_KEYBINDS_DLL DeviceFilter : public geode::EventFilter { protected: std::optional m_id; public: using Callback = void(DeviceEvent*); geode::ListenerResult handle(std::function fn, DeviceEvent* event); DeviceFilter(std::optional id = std::nullopt); }; struct CUSTOM_KEYBINDS_DLL RepeatOptions { bool enabled = true; size_t rate = 50; size_t delay = 500; }; using BindParser = std::function; class CUSTOM_KEYBINDS_DLL BindManager : public cocos2d::CCObject { // has to inherit from CCObject for scheduler public: using DevicelessActions = std::unordered_map>; protected: struct ActionData { BindableAction definition; RepeatOptions repeat; }; std::unordered_map> m_binds; std::unordered_map m_devicelessBinds; std::unordered_map m_devices; std::vector> m_actions; std::vector m_categories; geode::EventListener m_listener = geode::EventListener(this, &BindManager::onDispatch); std::unordered_map m_repeating; std::unordered_map> m_downActionBinds; BindManager(); geode::ListenerResult onDispatch(PressBindEvent* event); void onRepeat(float dt); void repeat(ActionID const& action); void unrepeat(ActionID const& action); bool loadActionBinds(ActionID const& action); void saveActionBinds(ActionID const& action); void fixEmptyBindsBug(); /// Checks if a specific Bind + Action combo is being held. /// @param action the action ID we are checking /// @param bind the bind we are checking /// @returns true if the action bind is being held, false otherwise bool isActionBindHeld(ActionID const& action, Bind* bind); /// Marks a bind that belongs to an action as held. No changes will be made if the action doesn't use the bind. /// @param action the action ID /// @param bind the bind we are marking as held, has to belong to the action void markActionBindHeld(ActionID const& action, Bind* bind); /// Marks a bind that belongs to an action as released. No changes will be made if the action doesn't use the bind. /// @param action the action ID /// @param bind the bind we are marking as released, has to belong to the action void unmarkActionBindHeld(ActionID const& action, Bind* bind); friend class InvokeBindFilter; friend struct matjson::Serialize; public: static BindManager* get(); void save(); void attachDevice(DeviceID const& device, BindParser parser); void detachDevice(DeviceID const& device); matjson::Value saveBind(Bind* bind) const; Bind* loadBind(matjson::Value const& json) const; bool registerBindable(BindableAction const& action, ActionID const& after = ""); void removeBindable(ActionID const& action); std::optional getBindable(ActionID const& action) const; std::vector getAllBindables() const; std::vector getBindablesIn(Category const& category, bool sub = false) const; std::vector getBindablesFor(Bind* bind) const; std::vector getAllCategories() const; /** * Add a new bindable category. If the category is a subcategory (its * ID has a slash, like "Editor/Modify"), then all its parent * categories are inserted aswell, and the subcategory is added after * its parent's last subcategory * @param category The category to add. Specify a subcategory by * including a slash in the name (like "Editor/Modify") */ void addCategory(Category const& category); /** * @note Also removes all the bindables in this category */ void removeCategory(Category const& category); void addBindTo(ActionID const& action, Bind* bind); void removeBindFrom(ActionID const& action, Bind* bind); void removeAllBindsFrom(ActionID const& action); void resetBindsToDefault(ActionID const& action); bool hasDefaultBinds(ActionID const& action) const; std::vector> getBindsFor(ActionID const& action) const; std::optional getRepeatOptionsFor(ActionID const& action); void setRepeatOptionsFor(ActionID const& action, RepeatOptions const& options); void stopAllRepeats(); }; }