Custom canvas keyboard input

Started by InvisibleShoe, 19 December 2024, 06:02:32

InvisibleShoe

Hi again,
I'm trying to hookup a controller to a custom CanvasSFML widget and can't get key events to register. Mouse move and click work fine.

Header:
class GameCanvas : public tgui::CanvasSFML{
public:
    using Ptr = std::shared_ptr<GameCanvas>;

    GameCanvas(flecs::world& w);
    static GameCanvas::Ptr create(flecs::world& w);

    bool canHandleKeyPress(const tgui::Event::KeyEvent& event) override;
    void keyPressed (const tgui::Event::KeyEvent& event) override;
    bool leftMousePressed(tgui::Vector2f pos) override;
    void leftMouseReleased(tgui::Vector2f pos) override;
    void rightMousePressed(tgui::Vector2f pos) override;
    void rightMouseReleased(tgui::Vector2f pos) override;
    void mouseMoved(tgui::Vector2f pos) override;

    bool isFocused() const{
        bool f = tgui::CanvasSFML::isFocused();
        std::cout << "GameCanvas::isFocused(): " << (f ? "True" : "False") << std::endl;

        return f;
    }

    tgui::SignalTyped<tgui::Event::KeyEvent> onKeyPress = {"onKeyPress"};
protected:
    tgui::Signal& getSignal(tgui::String signalName) override;
private:
    flecs::world world;
};

src:
#include "gameCanvas.hpp"

GameCanvas::GameCanvas(flecs::world& w) : tgui::CanvasSFML(), world(w) {
    onKeyPress(
        [this](const tgui::Event::KeyEvent& event){
            Tag::InputCtrl* ctrl = world.lookup("Tag::GameScene").get_mut<Tag::InputCtrl>();
            ctrl->ctrl->onKeyEvent(event);
        }
    );
}

GameCanvas::Ptr GameCanvas::create(flecs::world& w){
    auto canvas = std::make_shared<GameCanvas>(w);
    return canvas;
}

void GameCanvas::keyPressed (const tgui::Event::KeyEvent& event){
    std::cout << "GameCanvas::keyPressed()" << std::endl;
    tgui::CanvasSFML::keyPressed(event);

    onKeyPress.emit(this, event);
}
bool GameCanvas::leftMousePressed(tgui::Vector2f pos){
    tgui::CanvasSFML::leftMousePressed(pos);

    return false;
}
void GameCanvas::leftMouseReleased(tgui::Vector2f pos){
    tgui::CanvasSFML::leftMouseReleased(pos);

    std::cout << "GameCanvas::leftMouseReleased()" << std::endl;
}
void GameCanvas::rightMousePressed(tgui::Vector2f pos){
    tgui::CanvasSFML::rightMousePressed(pos);
}
void GameCanvas::rightMouseReleased(tgui::Vector2f pos){
    tgui::CanvasSFML::rightMouseReleased(pos);
}
void GameCanvas::mouseMoved(tgui::Vector2f pos){
    tgui::CanvasSFML::mouseMoved(pos);

    Tag::InputCtrl* ctrl = world.lookup("Tag::GameScene").get_mut<Tag::InputCtrl>();
    ctrl->ctrl->onMouseMoveEvent(pos - getPosition());
}

bool GameCanvas::canHandleKeyPress(const tgui::Event::KeyEvent& event){
    std::cout << "GameCanvas::canHandleKeyPress()" << std::endl;

    return true;
}

tgui::Signal& GameCanvas::getSignal(tgui::String signalName) {
    std::cout << "GameCanvas::getSignal()" << std::endl;

    if (signalName == onKeyPress.getName())
        return onKeyPress;
    else
        return tgui::CanvasSFML::getSignal(std::move(signalName));
}


The log messages from isFocused, keyPressed, canHandleKeyPress and getSignal never get called but the messages/actions in mouseMoved and leftMouseReleased do.

The canvas hierarchy is: GameCanvas > VerticalLayout["LeftPanel"] > HorizontalLayout["HudRoot"] > RootContainer.



I've looked through the forums, the tutorial pages on signals and custom widgets, and TGUI source code, especially EditBox, but I just can't seem to figure out how to get the canvas to receive keyboard input.

If someone could point out what I'm doing wrong in the above code or provide me some basic code showing how to create a custom canvas with keyPress functionality, I'd greatly appreciate it.
Cheers 

texus

#1
Key events are only send to the focused widget (and its chain of parents). There were a handful of widgets (like Picture and Canvas) that I considered "unfocusable". Since they never get focused, they never receive the key events. This isn't really documented properly anywhere, I never expected someone to inherit from these classes and turn them into functional widgets instead of just drawings.

You can fix your code by adding the following function to your class:
Code (cpp) Select
bool GameCanvas::canGainFocus() const override
{
    return true;
}

Of course you will only receive the key presses after you clicked the canvas or called canvas->setFocused(true), and you will no longer receive them once the user clicks on a button and the button receives focus. If this isn't what you want, then you should process the key events somewhere else. Either in your event loop, or make a Group/Panel widget that contains all other widgets and already handle key presses in that container.

InvisibleShoe

Excellent. Glad its an easy fix  ;) I'll give it a go.

I did have the game area rendered to a regular SFML view before but I was thinking that my game being fairly GUI heavy and the fact that I want interactive graphs and a minimap rendered to GUI widgets, it would be best to create a custom canvas widget that can gain focus and be interacted with.

While that info does help with creating a custom canvas for graphs and minimaps, I would prefer to have the game area rendering and input handling managed outside of GUI operations.
When I tried this before I had difficulty with TGUI consuming all input events, even when the only thing focused was an empty panel.

In the code below, I previously used insertSpace to create an area for the SFML view to be rendered and everything worked fine, input handling fell through to my controller until I click on any TGUI widget at which point TGUI starts consuming all actions and nothing falls through to my controller anymore.

Do you have any idea how to address this? I would love to go back to using a plain SFML view to render to.

GUI code:

auto gui = world.get_mut<Tag::GuiRoot>();
auto hudRoot = tgui::HorizontalLayout::create();

auto leftPanel = tgui::VerticalLayout::create();

auto topLeft = tgui::Panel::create();
topLeft->getRenderer()->setBackgroundColor(sf::Color::Green);
leftPanel->add(topLeft);

// auto canvas = GameCanvas::create(world);
// leftPanel->add(canvas, "GameCanvas");
// world.set( Tag::GameCanvas{canvas} );

// leftPanel->setRatio(canvas, 15.0f);

auto bottomLeft = tgui::Panel::create();
bottomLeft->getRenderer()->setBackgroundColor(sf::Color::Blue);
leftPanel->add(bottomLeft);

leftPanel->insertSpace(1, 80.0f);

hudRoot->add(leftPanel, "LeftPanel");

auto rightPanel = tgui::VerticalLayout::create();

auto topRight = tgui::Panel::create();
topRight->getRenderer()->setBackgroundColor(sf::Color::Green);
rightPanel->add(topRight);

auto button = tgui::Button::create("Steal Focus");
button->onMousePress(
[button](){
    button->setFocused(true);
}
);
topRight->add(button);

button = tgui::Button::create("Quit");
button->onMousePress(
[world](){
    world.add<Tag::ActiveScene, Tag::MenuScene>();
}
);
topRight->add(button);

auto txtArea = tgui::EditBox::create();
txtArea->setPosition(0, 50);
topRight->add(txtArea);

auto bottomRight = tgui::Panel::create();
bottomRight->getRenderer()->setBackgroundColor(sf::Color::Blue);
rightPanel->add(bottomRight);

hudRoot->add(rightPanel, "RightPanel");
hudRoot->setRatio(rightPanel, 0.3f);

hudRoot->setVisible(false);

gui->gui->add(hudRoot, "HudRoot");

(Previous) Input handling:

sf::Event event;

while(window.window->pollEvent(event)){
    if(event.type == sf::Event::Closed){
         e.world().set( Tag::Status{ Tag::Status::QUIT });
    }
    if(gui.gui->handleEvent(event)){
         continue;
    }
               
    ctrl.ctrl->checkInput(event);
}

Cheers and thanks for the help  :D

InvisibleShoe

I just rolled back to an earlier commit where I was rendering to SFML view and the issue appears to be "gui->handleEvent(event)" trying to handle all keyboard events once a widget is clicked.
Mouse events are handled by my controller as they should be.
But clicking on a GUI panel or button will cause the gui to consume all keyboard events until I click on the game area again.

...After some tinkering, it's definitely the "HudRoot" widget stealing focus. I hooked up "gui->unFocusAllWidgets()" to a button and that fixes the issue.

What is best practice for unfocusing widgets to allow other forms of input handling? Is there a way to detect unhandled input events so that unFocusAllWidgets can be called?

texus

#4
The thing to note it that TGUI technically isn't stealing your event. The handleEvent function just returns true, and your code is deciding not to handle the event if that is the case.

It's very difficult for me to decide what handleEvent should return. On one hand, people would only expect it to return true when the key event was actually used, but on the other hand you might not expect it to return false when an edit box is focused and you press backspace while the cursor is at the front or you try to type text in a numeric field.

The return value of handleEvent is something that can help your code decide whether it still needs to process it separately from the gui, but it doesn't has to be the only check. For example you could only execute the "continue;" line if the event wasn't a controller input, or even skip gui.handleEvent altogether for controller inputs.

So you should probably try to first think about when a key event should be handled by your code and when it should be dealt by the GUI. If you can properly define a rule about where the event needs to go to, then it might be more clear for me what you need exactly in case you are stuck implementing it.

InvisibleShoe

Understandable. I've figured out a way to make the GUI and controller work together overnight after looking at how a couple of games manage this.
But it is handy to know how to hook up keyboard input handling to CanvasSFML for other uses :)

Thanks for the help mate