mirror of
https://github.com/minetest/minetest.git
synced 2024-11-29 11:03:45 +01:00
Implement an editor to customize the touchscreen controls (#14933)
- The editor is accessible via the pause menu and the settings menu. - Buttons can be moved via drag & drop. - Buttons can be added/removed. The grid menu added by #14918 is used to show all buttons not included in the layout. - Custom layouts are responsive and adapt to changed screen size / DPI / hud_scaling. - The layout is saved as JSON in the "touch_layout" setting.
This commit is contained in:
parent
4faa16fe0d
commit
6a1d22b2c5
@ -123,6 +123,22 @@ local function load()
|
||||
end,
|
||||
}
|
||||
|
||||
local touchscreen_layout = {
|
||||
query_text = "Touchscreen layout",
|
||||
requires = {
|
||||
touchscreen = true,
|
||||
},
|
||||
get_formspec = function(self, avail_w)
|
||||
local btn_w = math.min(avail_w, 6)
|
||||
return ("button[0,0;%f,0.8;btn_touch_layout;%s]"):format(btn_w, fgettext("Touchscreen layout")), 0.8
|
||||
end,
|
||||
on_submit = function(self, fields)
|
||||
if fields.btn_touch_layout then
|
||||
core.show_touchscreen_layout()
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
add_page({
|
||||
id = "accessibility",
|
||||
title = fgettext_ne("Accessibility"),
|
||||
@ -151,6 +167,8 @@ local function load()
|
||||
load_settingtypes()
|
||||
|
||||
table.insert(page_by_id.controls_keyboard_and_mouse.content, 1, change_keys)
|
||||
-- insert after "touch_controls"
|
||||
table.insert(page_by_id.controls_touchscreen.content, 2, touchscreen_layout)
|
||||
do
|
||||
local content = page_by_id.graphics_and_audio_effects.content
|
||||
local idx = table.indexof(content, "enable_dynamic_shadows")
|
||||
|
@ -223,6 +223,7 @@ GUI
|
||||
* `core.set_clouds(<true/false>)`
|
||||
* `core.set_topleft_text(text)`
|
||||
* `core.show_keys_menu()`
|
||||
* `core.show_touchscreen_layout()`
|
||||
* `core.show_path_select_dialog(formname, caption, is_file_select)`
|
||||
* shows a path select dialog
|
||||
* `formname` is base name of dialog response returned in fields
|
||||
|
@ -26,6 +26,7 @@
|
||||
#include "client/event_manager.h"
|
||||
#include "fontengine.h"
|
||||
#include "gui/touchcontrols.h"
|
||||
#include "gui/touchscreeneditor.h"
|
||||
#include "itemdef.h"
|
||||
#include "log.h"
|
||||
#include "log_internal.h"
|
||||
@ -144,6 +145,11 @@ struct LocalFormspecHandler : public TextDest
|
||||
return;
|
||||
}
|
||||
|
||||
if (fields.find("btn_touchscreen_layout") != fields.end()) {
|
||||
g_gamecallback->touchscreenLayout();
|
||||
return;
|
||||
}
|
||||
|
||||
if (fields.find("btn_exit_menu") != fields.end()) {
|
||||
g_gamecallback->disconnect();
|
||||
return;
|
||||
@ -1844,6 +1850,12 @@ inline bool Game::handleCallbacks()
|
||||
g_gamecallback->keyconfig_requested = false;
|
||||
}
|
||||
|
||||
if (g_gamecallback->touchscreenlayout_requested) {
|
||||
(new GUITouchscreenLayout(guienv, guiroot, -1,
|
||||
&g_menumgr, texture_src))->drop();
|
||||
g_gamecallback->touchscreenlayout_requested = false;
|
||||
}
|
||||
|
||||
if (!g_gamecallback->show_open_url_dialog.empty()) {
|
||||
(void)make_irr<GUIOpenURLMenu>(guienv, guiroot, -1,
|
||||
&g_menumgr, texture_src, g_gamecallback->show_open_url_dialog);
|
||||
@ -4510,9 +4522,14 @@ void Game::showPauseMenu()
|
||||
<< strgettext("Sound Volume") << "]";
|
||||
}
|
||||
#endif
|
||||
os << "button_exit[4," << (ypos++) << ";3,0.5;btn_key_config;"
|
||||
<< strgettext("Controls") << "]";
|
||||
#endif
|
||||
if (g_touchcontrols) {
|
||||
os << "button_exit[4," << (ypos++) << ";3,0.5;btn_touchscreen_layout;"
|
||||
<< strgettext("Touchscreen Layout") << "]";
|
||||
} else {
|
||||
os << "button_exit[4," << (ypos++) << ";3,0.5;btn_key_config;"
|
||||
<< strgettext("Controls") << "]";
|
||||
}
|
||||
os << "button_exit[4," << (ypos++) << ";3,0.5;btn_exit_menu;"
|
||||
<< strgettext("Exit to Menu") << "]";
|
||||
os << "button_exit[4," << (ypos++) << ";3,0.5;btn_exit_os;"
|
||||
|
@ -24,6 +24,7 @@
|
||||
#include "wieldmesh.h"
|
||||
#include "client/renderingengine.h"
|
||||
#include "client/minimap.h"
|
||||
#include "client/texturesource.h"
|
||||
#include "gui/touchcontrols.h"
|
||||
#include "util/enriched_string.h"
|
||||
#include "irrlicht_changes/CGUITTFont.h"
|
||||
|
@ -542,6 +542,7 @@ void set_default_settings()
|
||||
settings->setDefault("keymap_sneak", "KEY_SHIFT");
|
||||
#endif
|
||||
|
||||
settings->setDefault("touch_layout", "");
|
||||
settings->setDefault("touchscreen_sensitivity", "0.2");
|
||||
settings->setDefault("touchscreen_threshold", "20");
|
||||
settings->setDefault("touch_long_tap_delay", "400");
|
||||
|
@ -25,5 +25,7 @@ set(gui_SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modalMenu.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/profilergraph.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/touchcontrols.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/touchscreenlayout.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/touchscreeneditor.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
@ -21,6 +21,7 @@ public:
|
||||
virtual void changeVolume() = 0;
|
||||
virtual void showOpenURLDialog(const std::string &url) = 0;
|
||||
virtual void signalKeyConfigChange() = 0;
|
||||
virtual void touchscreenLayout() = 0;
|
||||
};
|
||||
|
||||
extern gui::IGUIEnvironment *guienv;
|
||||
@ -133,6 +134,11 @@ public:
|
||||
keyconfig_changed = true;
|
||||
}
|
||||
|
||||
void touchscreenLayout() override
|
||||
{
|
||||
touchscreenlayout_requested = true;
|
||||
}
|
||||
|
||||
void showOpenURLDialog(const std::string &url) override
|
||||
{
|
||||
show_open_url_dialog = url;
|
||||
@ -142,6 +148,7 @@ public:
|
||||
bool changepassword_requested = false;
|
||||
bool changevolume_requested = false;
|
||||
bool keyconfig_requested = false;
|
||||
bool touchscreenlayout_requested = false;
|
||||
bool shutdown_requested = false;
|
||||
bool keyconfig_changed = false;
|
||||
std::string show_open_url_dialog = "";
|
||||
|
@ -5,6 +5,7 @@
|
||||
// Copyright (C) 2024 grorp, Gregor Parzefall
|
||||
|
||||
#include "touchcontrols.h"
|
||||
#include "touchscreenlayout.h"
|
||||
|
||||
#include "gettime.h"
|
||||
#include "irr_v2d.h"
|
||||
@ -14,8 +15,10 @@
|
||||
#include "client/guiscalingfilter.h"
|
||||
#include "client/keycode.h"
|
||||
#include "client/renderingengine.h"
|
||||
#include "client/texturesource.h"
|
||||
#include "util/numeric.h"
|
||||
#include "gettext.h"
|
||||
#include "irr_gui_ptr.h"
|
||||
#include "IGUIImage.h"
|
||||
#include "IGUIStaticText.h"
|
||||
#include "IGUIFont.h"
|
||||
#include <IrrlichtDevice.h>
|
||||
@ -26,59 +29,6 @@
|
||||
|
||||
TouchControls *g_touchcontrols;
|
||||
|
||||
static const char *button_image_names[] = {
|
||||
"jump_btn.png",
|
||||
"down.png",
|
||||
"zoom.png",
|
||||
"aux1_btn.png",
|
||||
"overflow_btn.png",
|
||||
|
||||
"fly_btn.png",
|
||||
"noclip_btn.png",
|
||||
"fast_btn.png",
|
||||
"debug_btn.png",
|
||||
"camera_btn.png",
|
||||
"rangeview_btn.png",
|
||||
"minimap_btn.png",
|
||||
"",
|
||||
|
||||
"chat_btn.png",
|
||||
"inventory_btn.png",
|
||||
"drop_btn.png",
|
||||
"exit_btn.png",
|
||||
|
||||
"joystick_off.png",
|
||||
"joystick_bg.png",
|
||||
"joystick_center.png",
|
||||
};
|
||||
|
||||
// compare with GUIKeyChangeMenu::init_keys
|
||||
static const char *button_titles[] = {
|
||||
N_("Jump"),
|
||||
N_("Sneak"),
|
||||
N_("Zoom"),
|
||||
N_("Aux1"),
|
||||
N_("Overflow menu"),
|
||||
|
||||
N_("Toggle fly"),
|
||||
N_("Toggle noclip"),
|
||||
N_("Toggle fast"),
|
||||
N_("Toggle debug"),
|
||||
N_("Change camera"),
|
||||
N_("Range select"),
|
||||
N_("Toggle minimap"),
|
||||
N_("Toggle chat log"),
|
||||
|
||||
N_("Chat"),
|
||||
N_("Inventory"),
|
||||
N_("Drop"),
|
||||
N_("Exit"),
|
||||
|
||||
N_("Joystick"),
|
||||
N_("Joystick"),
|
||||
N_("Joystick"),
|
||||
};
|
||||
|
||||
static void load_button_texture(IGUIImage *gui_button, const std::string &path,
|
||||
const recti &button_rect, ISimpleTextureSource *tsrc, video::IVideoDriver *driver)
|
||||
{
|
||||
@ -268,10 +218,22 @@ TouchControls::TouchControls(IrrlichtDevice *device, ISimpleTextureSource *tsrc)
|
||||
m_long_tap_delay = g_settings->getU16("touch_long_tap_delay");
|
||||
m_fixed_joystick = g_settings->getBool("fixed_virtual_joystick");
|
||||
m_joystick_triggers_aux1 = g_settings->getBool("virtual_joystick_triggers_aux1");
|
||||
|
||||
m_screensize = m_device->getVideoDriver()->getScreenSize();
|
||||
m_button_size = MYMIN(m_screensize.Y / 4.5f,
|
||||
RenderingEngine::getDisplayDensity() * 65.0f *
|
||||
g_settings->getFloat("hud_scaling"));
|
||||
m_button_size = ButtonLayout::getButtonSize(m_screensize);
|
||||
applyLayout(ButtonLayout::loadFromSettings());
|
||||
}
|
||||
|
||||
void TouchControls::applyLayout(const ButtonLayout &layout)
|
||||
{
|
||||
m_layout = layout;
|
||||
|
||||
m_buttons.clear();
|
||||
m_overflow_btn = nullptr;
|
||||
m_overflow_bg = nullptr;
|
||||
m_overflow_buttons.clear();
|
||||
m_overflow_button_titles.clear();
|
||||
m_overflow_button_rects.clear();
|
||||
|
||||
// Initialize joystick display "button".
|
||||
// Joystick is placed on the bottom left of screen.
|
||||
@ -298,47 +260,21 @@ TouchControls::TouchControls(IrrlichtDevice *device, ISimpleTextureSource *tsrc)
|
||||
m_joystick_btn_center = grab_gui_element<IGUIImage>(makeButtonDirect(joystick_center_id,
|
||||
recti(0, 0, m_button_size, m_button_size), false));
|
||||
|
||||
// init jump button
|
||||
addButton(m_buttons, jump_id, button_image_names[jump_id],
|
||||
recti(m_screensize.X - 1.75f * m_button_size,
|
||||
m_screensize.Y - m_button_size,
|
||||
m_screensize.X - 0.25f * m_button_size,
|
||||
m_screensize.Y));
|
||||
for (const auto &[id, meta] : m_layout.layout) {
|
||||
if (!mayAddButton(id))
|
||||
continue;
|
||||
|
||||
// init sneak button
|
||||
addButton(m_buttons, sneak_id, button_image_names[sneak_id],
|
||||
recti(m_screensize.X - 3.25f * m_button_size,
|
||||
m_screensize.Y - m_button_size,
|
||||
m_screensize.X - 1.75f * m_button_size,
|
||||
m_screensize.Y));
|
||||
|
||||
// init zoom button
|
||||
addButton(m_buttons, zoom_id, button_image_names[zoom_id],
|
||||
recti(m_screensize.X - 1.25f * m_button_size,
|
||||
m_screensize.Y - 4 * m_button_size,
|
||||
m_screensize.X - 0.25f * m_button_size,
|
||||
m_screensize.Y - 3 * m_button_size));
|
||||
|
||||
// init aux1 button
|
||||
if (!m_joystick_triggers_aux1)
|
||||
addButton(m_buttons, aux1_id, button_image_names[aux1_id],
|
||||
recti(m_screensize.X - 1.25f * m_button_size,
|
||||
m_screensize.Y - 2.5f * m_button_size,
|
||||
m_screensize.X - 0.25f * m_button_size,
|
||||
m_screensize.Y - 1.5f * m_button_size));
|
||||
|
||||
// init overflow button
|
||||
m_overflow_btn = grab_gui_element<IGUIImage>(makeButtonDirect(overflow_id,
|
||||
recti(m_screensize.X - 1.25f * m_button_size,
|
||||
m_screensize.Y - 5.5f * m_button_size,
|
||||
m_screensize.X - 0.25f * m_button_size,
|
||||
m_screensize.Y - 4.5f * m_button_size), true));
|
||||
|
||||
const static touch_gui_button_id overflow_buttons[] {
|
||||
chat_id, inventory_id, drop_id, exit_id,
|
||||
fly_id, noclip_id, fast_id, debug_id, camera_id, range_id, minimap_id,
|
||||
toggle_chat_id,
|
||||
};
|
||||
recti rect = m_layout.getRect(id, m_screensize, m_button_size, m_texturesource);
|
||||
if (id == toggle_chat_id)
|
||||
// Chat is shown by default, so chat_hide_btn.png is shown first.
|
||||
addToggleButton(m_buttons, id, "chat_hide_btn.png",
|
||||
"chat_show_btn.png", rect, true);
|
||||
else if (id == overflow_id)
|
||||
m_overflow_btn = grab_gui_element<IGUIImage>(
|
||||
makeButtonDirect(id, rect, true));
|
||||
else
|
||||
addButton(m_buttons, id, button_image_names[id], rect, true);
|
||||
}
|
||||
|
||||
IGUIStaticText *background = m_guienv->addStaticText(L"",
|
||||
recti(v2s32(0, 0), dimension2du(m_screensize)));
|
||||
@ -346,32 +282,17 @@ TouchControls::TouchControls(IrrlichtDevice *device, ISimpleTextureSource *tsrc)
|
||||
background->setVisible(false);
|
||||
m_overflow_bg = grab_gui_element<IGUIStaticText>(background);
|
||||
|
||||
s32 cols = 4;
|
||||
s32 rows = 3;
|
||||
f32 screen_aspect = (f32)m_screensize.X / (f32)m_screensize.Y;
|
||||
while ((s32)ARRLEN(overflow_buttons) > cols * rows) {
|
||||
f32 aspect = (f32)cols / (f32)rows;
|
||||
if (aspect > screen_aspect)
|
||||
rows++;
|
||||
else
|
||||
cols++;
|
||||
}
|
||||
|
||||
v2s32 size(m_button_size, m_button_size);
|
||||
v2s32 spacing(m_screensize.X / (cols + 1), m_screensize.Y / (rows + 1));
|
||||
v2s32 pos(spacing);
|
||||
|
||||
for (auto id : overflow_buttons) {
|
||||
if (id_to_keycode(id) == KEY_UNKNOWN)
|
||||
continue;
|
||||
|
||||
recti rect(pos - size / 2, dimension2du(size.X, size.Y));
|
||||
if (rect.LowerRightCorner.X > (s32)m_screensize.X) {
|
||||
pos.X = spacing.X;
|
||||
pos.Y += spacing.Y;
|
||||
rect = recti(pos - size / 2, dimension2du(size.X, size.Y));
|
||||
}
|
||||
auto overflow_buttons = m_layout.getMissingButtons();
|
||||
overflow_buttons.erase(std::remove_if(
|
||||
overflow_buttons.begin(), overflow_buttons.end(),
|
||||
[&](touch_gui_button_id id) {
|
||||
// There's no sense in adding the overflow button to the overflow
|
||||
// menu (also, it's impossible since it doesn't have a keycode).
|
||||
return !mayAddButton(id) || id == overflow_id;
|
||||
}), overflow_buttons.end());
|
||||
|
||||
layout_button_grid(m_screensize, m_texturesource, overflow_buttons,
|
||||
[&] (touch_gui_button_id id, v2s32 pos, recti rect) {
|
||||
if (id == toggle_chat_id)
|
||||
// Chat is shown by default, so chat_hide_btn.png is shown first.
|
||||
addToggleButton(m_overflow_buttons, id, "chat_hide_btn.png",
|
||||
@ -379,27 +300,23 @@ TouchControls::TouchControls(IrrlichtDevice *device, ISimpleTextureSource *tsrc)
|
||||
else
|
||||
addButton(m_overflow_buttons, id, button_image_names[id], rect, false);
|
||||
|
||||
std::wstring str = wstrgettext(button_titles[id]);
|
||||
IGUIStaticText *text = m_guienv->addStaticText(str.c_str(), recti());
|
||||
IGUIFont *font = text->getActiveFont();
|
||||
dimension2du dim = font->getDimension(str.c_str());
|
||||
dim = dimension2du(dim.Width * 1.25f, dim.Height * 1.25f); // avoid clipping
|
||||
text->setRelativePosition(recti(pos.X - dim.Width / 2, pos.Y + size.Y / 2,
|
||||
pos.X + dim.Width / 2, pos.Y + size.Y / 2 + dim.Height));
|
||||
text->setTextAlignment(EGUIA_CENTER, EGUIA_UPPERLEFT);
|
||||
IGUIStaticText *text = m_guienv->addStaticText(L"", recti());
|
||||
make_button_grid_title(text, id, pos, rect);
|
||||
text->setVisible(false);
|
||||
m_overflow_button_titles.push_back(grab_gui_element<IGUIStaticText>(text));
|
||||
|
||||
rect.addInternalPoint(text->getRelativePosition().UpperLeftCorner);
|
||||
rect.addInternalPoint(text->getRelativePosition().LowerRightCorner);
|
||||
m_overflow_button_rects.push_back(rect);
|
||||
|
||||
pos.X += spacing.X;
|
||||
}
|
||||
});
|
||||
|
||||
m_status_text = grab_gui_element<IGUIStaticText>(
|
||||
m_guienv->addStaticText(L"", recti(), false, false));
|
||||
m_status_text->setVisible(false);
|
||||
|
||||
// applyLayout can be called at any time, also e.g. while the overflow menu
|
||||
// is open, so this is necessary to restore correct visibility.
|
||||
updateVisibility();
|
||||
}
|
||||
|
||||
TouchControls::~TouchControls()
|
||||
@ -407,6 +324,17 @@ TouchControls::~TouchControls()
|
||||
releaseAll();
|
||||
}
|
||||
|
||||
bool TouchControls::mayAddButton(touch_gui_button_id id)
|
||||
{
|
||||
if (!ButtonLayout::isButtonAllowed(id))
|
||||
return false;
|
||||
if (id == aux1_id && m_joystick_triggers_aux1)
|
||||
return false;
|
||||
if (id != overflow_id && id_to_keycode(id) == KEY_UNKNOWN)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void TouchControls::addButton(std::vector<button_info> &buttons, touch_gui_button_id id,
|
||||
const std::string &image, const recti &rect, bool visible)
|
||||
{
|
||||
@ -701,6 +629,15 @@ void TouchControls::applyJoystickStatus()
|
||||
|
||||
void TouchControls::step(float dtime)
|
||||
{
|
||||
v2u32 screensize = m_device->getVideoDriver()->getScreenSize();
|
||||
s32 button_size = ButtonLayout::getButtonSize(screensize);
|
||||
|
||||
if (m_screensize != screensize || m_button_size != button_size) {
|
||||
m_screensize = screensize;
|
||||
m_button_size = button_size;
|
||||
applyLayout(m_layout);
|
||||
}
|
||||
|
||||
// simulate keyboard repeats
|
||||
buttons_step(m_buttons, dtime, m_device->getVideoDriver(), m_receiver, m_texturesource);
|
||||
buttons_step(m_overflow_buttons, dtime, m_device->getVideoDriver(), m_receiver, m_texturesource);
|
||||
|
@ -5,11 +5,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IGUIStaticText.h"
|
||||
#include "irrlichttypes.h"
|
||||
#include <IEventReceiver.h>
|
||||
#include <IGUIImage.h>
|
||||
#include <IGUIEnvironment.h>
|
||||
#include "IEventReceiver.h"
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
@ -17,41 +14,29 @@
|
||||
#include <vector>
|
||||
|
||||
#include "itemdef.h"
|
||||
#include "client/game.h"
|
||||
#include "touchscreenlayout.h"
|
||||
#include "util/basic_macros.h"
|
||||
#include "client/texturesource.h"
|
||||
|
||||
namespace irr
|
||||
{
|
||||
class IrrlichtDevice;
|
||||
namespace gui
|
||||
{
|
||||
class IGUIEnvironment;
|
||||
class IGUIImage;
|
||||
class IGUIStaticText;
|
||||
}
|
||||
namespace video
|
||||
{
|
||||
class IVideoDriver;
|
||||
}
|
||||
}
|
||||
class ISimpleTextureSource;
|
||||
|
||||
using namespace irr::core;
|
||||
using namespace irr::gui;
|
||||
|
||||
|
||||
// We cannot use irr_ptr for Irrlicht GUI elements we own.
|
||||
// Option 1: Pass IGUIElement* returned by IGUIEnvironment::add* into irr_ptr
|
||||
// constructor.
|
||||
// -> We steal the reference owned by IGUIEnvironment and drop it later,
|
||||
// causing the IGUIElement to be deleted while IGUIEnvironment still
|
||||
// references it.
|
||||
// Option 2: Pass IGUIElement* returned by IGUIEnvironment::add* into irr_ptr::grab.
|
||||
// -> We add another reference and drop it later, but since IGUIEnvironment
|
||||
// still references the IGUIElement, it is never deleted.
|
||||
// To make IGUIEnvironment drop its reference to the IGUIElement, we have to call
|
||||
// IGUIElement::remove, so that's what we'll do.
|
||||
template <typename T>
|
||||
std::shared_ptr<T> grab_gui_element(T *element)
|
||||
{
|
||||
static_assert(std::is_base_of_v<IGUIElement, T>,
|
||||
"grab_gui_element only works for IGUIElement");
|
||||
return std::shared_ptr<T>(element, [](T *e) {
|
||||
e->remove();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
enum class TapState
|
||||
{
|
||||
None,
|
||||
@ -59,36 +44,6 @@ enum class TapState
|
||||
LongTap,
|
||||
};
|
||||
|
||||
enum touch_gui_button_id
|
||||
{
|
||||
jump_id = 0,
|
||||
sneak_id,
|
||||
zoom_id,
|
||||
aux1_id,
|
||||
overflow_id,
|
||||
|
||||
// usually in the "settings bar"
|
||||
fly_id,
|
||||
noclip_id,
|
||||
fast_id,
|
||||
debug_id,
|
||||
camera_id,
|
||||
range_id,
|
||||
minimap_id,
|
||||
toggle_chat_id,
|
||||
|
||||
// usually in the "rare controls bar"
|
||||
chat_id,
|
||||
inventory_id,
|
||||
drop_id,
|
||||
exit_id,
|
||||
|
||||
// the joystick
|
||||
joystick_off_id,
|
||||
joystick_bg_id,
|
||||
joystick_center_id,
|
||||
};
|
||||
|
||||
|
||||
#define BUTTON_REPEAT_DELAY 0.5f
|
||||
#define BUTTON_REPEAT_INTERVAL 0.333f
|
||||
@ -168,6 +123,9 @@ public:
|
||||
bool isStatusTextOverriden() { return m_overflow_open; }
|
||||
IGUIStaticText *getStatusText() { return m_status_text.get(); }
|
||||
|
||||
ButtonLayout getLayout() { return m_layout; }
|
||||
void applyLayout(const ButtonLayout &layout);
|
||||
|
||||
private:
|
||||
IrrlichtDevice *m_device = nullptr;
|
||||
IGUIEnvironment *m_guienv = nullptr;
|
||||
@ -233,13 +191,14 @@ private:
|
||||
void releaseAll();
|
||||
|
||||
// initialize a button
|
||||
bool mayAddButton(touch_gui_button_id id);
|
||||
void addButton(std::vector<button_info> &buttons,
|
||||
touch_gui_button_id id, const std::string &image,
|
||||
const recti &rect, bool visible=true);
|
||||
const recti &rect, bool visible);
|
||||
void addToggleButton(std::vector<button_info> &buttons,
|
||||
touch_gui_button_id id,
|
||||
const std::string &image_1, const std::string &image_2,
|
||||
const recti &rect, bool visible=true);
|
||||
const recti &rect, bool visible);
|
||||
|
||||
IGUIImage *makeButtonDirect(touch_gui_button_id id,
|
||||
const recti &rect, bool visible);
|
||||
@ -268,6 +227,8 @@ private:
|
||||
|
||||
bool m_place_pressed = false;
|
||||
u64 m_place_pressed_until = 0;
|
||||
|
||||
ButtonLayout m_layout;
|
||||
};
|
||||
|
||||
extern TouchControls *g_touchcontrols;
|
||||
|
404
src/gui/touchscreeneditor.cpp
Normal file
404
src/gui/touchscreeneditor.cpp
Normal file
@ -0,0 +1,404 @@
|
||||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 grorp, Gregor Parzefall <grorp@posteo.de>
|
||||
|
||||
#include "touchscreeneditor.h"
|
||||
#include "touchcontrols.h"
|
||||
#include "touchscreenlayout.h"
|
||||
|
||||
#include "client/renderingengine.h"
|
||||
#include "gettext.h"
|
||||
#include "irr_gui_ptr.h"
|
||||
#include "settings.h"
|
||||
|
||||
#include "IGUIButton.h"
|
||||
#include "IGUIFont.h"
|
||||
#include "IGUIImage.h"
|
||||
#include "IGUIStaticText.h"
|
||||
|
||||
GUITouchscreenLayout::GUITouchscreenLayout(gui::IGUIEnvironment* env,
|
||||
gui::IGUIElement* parent, s32 id,
|
||||
IMenuManager *menumgr, ISimpleTextureSource *tsrc
|
||||
):
|
||||
GUIModalMenu(env, parent, id, menumgr),
|
||||
m_tsrc(tsrc)
|
||||
{
|
||||
if (g_touchcontrols)
|
||||
m_layout = g_touchcontrols->getLayout();
|
||||
else
|
||||
m_layout = ButtonLayout::loadFromSettings();
|
||||
|
||||
m_gui_help_text = grab_gui_element<IGUIStaticText>(Environment->addStaticText(
|
||||
L"", core::recti(), false, false, this, -1));
|
||||
m_gui_help_text->setTextAlignment(EGUIA_CENTER, EGUIA_CENTER);
|
||||
|
||||
m_gui_add_btn = grab_gui_element<IGUIButton>(Environment->addButton(
|
||||
core::recti(), this, -1, wstrgettext("Add button").c_str()));
|
||||
m_gui_reset_btn = grab_gui_element<IGUIButton>(Environment->addButton(
|
||||
core::recti(), this, -1, wstrgettext("Reset").c_str()));
|
||||
m_gui_done_btn = grab_gui_element<IGUIButton>(Environment->addButton(
|
||||
core::recti(), this, -1, wstrgettext("Done").c_str()));
|
||||
|
||||
m_gui_remove_btn = grab_gui_element<IGUIButton>(Environment->addButton(
|
||||
core::recti(), this, -1, wstrgettext("Remove").c_str()));
|
||||
}
|
||||
|
||||
GUITouchscreenLayout::~GUITouchscreenLayout()
|
||||
{
|
||||
ButtonLayout::clearTextureCache();
|
||||
}
|
||||
|
||||
void GUITouchscreenLayout::regenerateGui(v2u32 screensize)
|
||||
{
|
||||
DesiredRect = core::recti(0, 0, screensize.X, screensize.Y);
|
||||
recalculateAbsolutePosition(false);
|
||||
|
||||
s32 button_size = ButtonLayout::getButtonSize(screensize);
|
||||
if (m_last_screensize != screensize || m_button_size != button_size) {
|
||||
m_last_screensize = screensize;
|
||||
m_button_size = button_size;
|
||||
// Prevent interpolation when the layout scales.
|
||||
clearGUIImages();
|
||||
}
|
||||
|
||||
// Discard invalid selection. May happen when...
|
||||
// 1. a button is removed.
|
||||
// 2. adding a button fails and it disappears from the layout again.
|
||||
if (m_selected_btn != touch_gui_button_id_END &&
|
||||
m_layout.layout.count(m_selected_btn) == 0)
|
||||
m_selected_btn = touch_gui_button_id_END;
|
||||
|
||||
if (m_mode == Mode::Add)
|
||||
regenerateGUIImagesAddMode(screensize);
|
||||
else
|
||||
regenerateGUIImagesRegular(screensize);
|
||||
regenerateMenu(screensize);
|
||||
}
|
||||
|
||||
void GUITouchscreenLayout::clearGUIImages()
|
||||
{
|
||||
m_gui_images.clear();
|
||||
m_gui_images_target_pos.clear();
|
||||
|
||||
m_add_layout.layout.clear();
|
||||
m_add_button_titles.clear();
|
||||
}
|
||||
|
||||
void GUITouchscreenLayout::regenerateGUIImagesRegular(v2u32 screensize)
|
||||
{
|
||||
assert(m_mode != Mode::Add);
|
||||
|
||||
auto old_gui_images = m_gui_images;
|
||||
clearGUIImages();
|
||||
|
||||
for (const auto &[btn, meta] : m_layout.layout) {
|
||||
core::recti rect = m_layout.getRect(btn, screensize, m_button_size, m_tsrc);
|
||||
std::shared_ptr<IGUIImage> img;
|
||||
|
||||
if (old_gui_images.count(btn) > 0) {
|
||||
img = old_gui_images.at(btn);
|
||||
// Update size, keep position. Position is interpolated in interpolateGUIImages.
|
||||
img->setRelativePosition(core::recti(
|
||||
img->getRelativePosition().UpperLeftCorner, rect.getSize()));
|
||||
} else {
|
||||
img = grab_gui_element<IGUIImage>(Environment->addImage(rect, this, -1));
|
||||
img->setImage(ButtonLayout::getTexture(btn, m_tsrc));
|
||||
img->setScaleImage(true);
|
||||
}
|
||||
|
||||
m_gui_images[btn] = img;
|
||||
m_gui_images_target_pos[btn] = rect.UpperLeftCorner;
|
||||
}
|
||||
}
|
||||
|
||||
void GUITouchscreenLayout::regenerateGUIImagesAddMode(v2u32 screensize)
|
||||
{
|
||||
assert(m_mode == Mode::Add);
|
||||
|
||||
clearGUIImages();
|
||||
|
||||
auto missing_buttons = m_layout.getMissingButtons();
|
||||
|
||||
layout_button_grid(screensize, m_tsrc, missing_buttons,
|
||||
[&] (touch_gui_button_id btn, v2s32 pos, core::recti rect) {
|
||||
auto img = grab_gui_element<IGUIImage>(Environment->addImage(rect, this, -1));
|
||||
img->setImage(ButtonLayout::getTexture(btn, m_tsrc));
|
||||
img->setScaleImage(true);
|
||||
m_gui_images[btn] = img;
|
||||
|
||||
ButtonMeta meta;
|
||||
meta.setPos(pos, screensize, m_button_size);
|
||||
m_add_layout.layout[btn] = meta;
|
||||
|
||||
IGUIStaticText *text = Environment->addStaticText(L"", core::recti(),
|
||||
false, false,this, -1);
|
||||
make_button_grid_title(text, btn, pos, rect);
|
||||
m_add_button_titles.push_back(grab_gui_element<IGUIStaticText>(text));
|
||||
});
|
||||
}
|
||||
|
||||
void GUITouchscreenLayout::interpolateGUIImages()
|
||||
{
|
||||
if (m_mode == Mode::Add)
|
||||
return;
|
||||
|
||||
for (auto &[btn, gui_image] : m_gui_images) {
|
||||
bool interpolate = m_mode != Mode::Dragging || m_selected_btn != btn;
|
||||
|
||||
v2s32 cur_pos_int = gui_image->getRelativePosition().UpperLeftCorner;
|
||||
v2s32 tgt_pos_int = m_gui_images_target_pos.at(btn);
|
||||
v2f cur_pos(cur_pos_int.X, cur_pos_int.Y);
|
||||
v2f tgt_pos(tgt_pos_int.X, tgt_pos_int.Y);
|
||||
|
||||
if (interpolate && cur_pos.getDistanceFrom(tgt_pos) > 2.0f) {
|
||||
v2f pos = cur_pos.getInterpolated(tgt_pos, 0.5f);
|
||||
gui_image->setRelativePosition(v2s32(core::round32(pos.X), core::round32(pos.Y)));
|
||||
} else {
|
||||
gui_image->setRelativePosition(tgt_pos_int);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void layout_menu_row(v2u32 screensize,
|
||||
const std::vector<std::shared_ptr<IGUIButton>> &row,
|
||||
const std::vector<std::shared_ptr<IGUIButton>> &full_row, bool bottom)
|
||||
{
|
||||
s32 spacing = RenderingEngine::getDisplayDensity() * 4.0f;
|
||||
|
||||
s32 btn_w = 0;
|
||||
s32 btn_h = 0;
|
||||
for (const auto &btn : full_row) {
|
||||
IGUIFont *font = btn->getActiveFont();
|
||||
core::dimension2du dim = font->getDimension(btn->getText());
|
||||
btn_w = std::max(btn_w, (s32)(dim.Width * 1.5f));
|
||||
btn_h = std::max(btn_h, (s32)(dim.Height * 2.5f));
|
||||
}
|
||||
|
||||
const s32 row_width = ((btn_w + spacing) * row.size()) - spacing;
|
||||
s32 x = screensize.X / 2 - row_width / 2;
|
||||
const s32 y = bottom ? screensize.Y - spacing - btn_h : spacing;
|
||||
|
||||
for (const auto &btn : row) {
|
||||
btn->setRelativePosition(core::recti(
|
||||
v2s32(x, y), core::dimension2du(btn_w, btn_h)));
|
||||
x += btn_w + spacing;
|
||||
}
|
||||
}
|
||||
|
||||
void GUITouchscreenLayout::regenerateMenu(v2u32 screensize)
|
||||
{
|
||||
bool have_selection = m_selected_btn != touch_gui_button_id_END;
|
||||
|
||||
if (m_mode == Mode::Add)
|
||||
m_gui_help_text->setText(wstrgettext("Start dragging a button to add. Tap outside to cancel.").c_str());
|
||||
else if (!have_selection)
|
||||
m_gui_help_text->setText(wstrgettext("Tap a button to select it. Drag a button to move it.").c_str());
|
||||
else
|
||||
m_gui_help_text->setText(wstrgettext("Tap outside to deselect.").c_str());
|
||||
|
||||
IGUIFont *font = m_gui_help_text->getActiveFont();
|
||||
core::dimension2du dim = font->getDimension(m_gui_help_text->getText());
|
||||
s32 height = dim.Height * 2.5f;
|
||||
s32 pos_y = (m_mode == Mode::Add || have_selection) ? 0 : screensize.Y - height;
|
||||
m_gui_help_text->setRelativePosition(core::recti(
|
||||
v2s32(0, pos_y),
|
||||
core::dimension2du(screensize.X, height)));
|
||||
|
||||
bool normal_buttons_visible = m_mode != Mode::Add && !have_selection;
|
||||
bool add_visible = normal_buttons_visible && !m_layout.getMissingButtons().empty();
|
||||
|
||||
m_gui_add_btn->setVisible(add_visible);
|
||||
m_gui_reset_btn->setVisible(normal_buttons_visible);
|
||||
m_gui_done_btn->setVisible(normal_buttons_visible);
|
||||
|
||||
if (normal_buttons_visible) {
|
||||
std::vector row1{m_gui_add_btn, m_gui_reset_btn, m_gui_done_btn};
|
||||
if (add_visible) {
|
||||
layout_menu_row(screensize, row1, row1, false);
|
||||
} else {
|
||||
std::vector row1_reduced{m_gui_reset_btn, m_gui_done_btn};
|
||||
layout_menu_row(screensize, row1_reduced, row1, false);
|
||||
}
|
||||
}
|
||||
|
||||
bool remove_visible = m_mode != Mode::Add && have_selection &&
|
||||
!ButtonLayout::isButtonRequired(m_selected_btn);
|
||||
|
||||
m_gui_remove_btn->setVisible(remove_visible);
|
||||
|
||||
if (remove_visible) {
|
||||
std::vector row2{m_gui_remove_btn};
|
||||
layout_menu_row(screensize, row2, row2, true);
|
||||
}
|
||||
}
|
||||
|
||||
void GUITouchscreenLayout::drawMenu()
|
||||
{
|
||||
video::IVideoDriver *driver = Environment->getVideoDriver();
|
||||
|
||||
video::SColor bgcolor(140, 0, 0, 0);
|
||||
video::SColor selection_color(255, 128, 128, 128);
|
||||
video::SColor error_color(255, 255, 0, 0);
|
||||
|
||||
driver->draw2DRectangle(bgcolor, AbsoluteRect, &AbsoluteClippingRect);
|
||||
|
||||
// Done here instead of in OnPostRender to avoid drag&drop lagging behind
|
||||
// input by one frame.
|
||||
// Must be done before drawing selection rectangle.
|
||||
interpolateGUIImages();
|
||||
|
||||
bool draw_selection = m_gui_images.count(m_selected_btn) > 0;
|
||||
if (draw_selection)
|
||||
driver->draw2DRectangle(selection_color,
|
||||
m_gui_images.at(m_selected_btn)->getAbsolutePosition(),
|
||||
&AbsoluteClippingRect);
|
||||
|
||||
if (m_mode == Mode::Dragging) {
|
||||
for (const auto &rect : m_error_rects)
|
||||
driver->draw2DRectangle(error_color, rect, &AbsoluteClippingRect);
|
||||
}
|
||||
|
||||
IGUIElement::draw();
|
||||
}
|
||||
|
||||
void GUITouchscreenLayout::updateDragState(v2u32 screensize, v2s32 mouse_movement)
|
||||
{
|
||||
assert(m_mode == Mode::Dragging);
|
||||
|
||||
core::recti rect = m_layout.getRect(m_selected_btn, screensize, m_button_size, m_tsrc);
|
||||
rect += mouse_movement;
|
||||
rect.constrainTo(core::recti(v2s32(0, 0), core::dimension2du(screensize)));
|
||||
|
||||
ButtonMeta &meta = m_layout.layout.at(m_selected_btn);
|
||||
meta.setPos(rect.getCenter(), screensize, m_button_size);
|
||||
|
||||
rect = m_layout.getRect(m_selected_btn, screensize, m_button_size, m_tsrc);
|
||||
|
||||
m_error_rects.clear();
|
||||
for (const auto &[other_btn, other_meta] : m_layout.layout) {
|
||||
if (other_btn == m_selected_btn)
|
||||
continue;
|
||||
core::recti other_rect = m_layout.getRect(other_btn, screensize, m_button_size, m_tsrc);
|
||||
if (other_rect.isRectCollided(rect))
|
||||
m_error_rects.push_back(other_rect);
|
||||
}
|
||||
if (m_error_rects.empty())
|
||||
m_last_good_layout = m_layout;
|
||||
}
|
||||
|
||||
bool GUITouchscreenLayout::OnEvent(const SEvent& event)
|
||||
{
|
||||
if (event.EventType == EET_KEY_INPUT_EVENT) {
|
||||
if (event.KeyInput.Key == KEY_ESCAPE && event.KeyInput.PressedDown) {
|
||||
quitMenu();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
core::dimension2du screensize = Environment->getVideoDriver()->getScreenSize();
|
||||
|
||||
if (event.EventType == EET_MOUSE_INPUT_EVENT) {
|
||||
v2s32 mouse_pos = v2s32(event.MouseInput.X, event.MouseInput.Y);
|
||||
|
||||
switch (event.MouseInput.Event) {
|
||||
case EMIE_LMOUSE_PRESSED_DOWN: {
|
||||
m_mouse_down = true;
|
||||
m_last_mouse_pos = mouse_pos;
|
||||
|
||||
IGUIElement *el = Environment->getRootGUIElement()->getElementFromPoint(mouse_pos);
|
||||
// Clicking on nothing deselects.
|
||||
m_selected_btn = touch_gui_button_id_END;
|
||||
for (const auto &[btn, gui_image] : m_gui_images) {
|
||||
if (el == gui_image.get()) {
|
||||
m_selected_btn = btn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_mode == Mode::Add) {
|
||||
if (m_selected_btn != touch_gui_button_id_END) {
|
||||
m_mode = Mode::Dragging;
|
||||
m_last_good_layout = m_layout;
|
||||
m_layout.layout[m_selected_btn] = m_add_layout.layout.at(m_selected_btn);
|
||||
updateDragState(screensize, v2s32(0, 0));
|
||||
} else {
|
||||
// Clicking on nothing quits add mode without adding a button.
|
||||
m_mode = Mode::Default;
|
||||
}
|
||||
}
|
||||
|
||||
regenerateGui(screensize);
|
||||
return true;
|
||||
}
|
||||
case EMIE_MOUSE_MOVED: {
|
||||
if (m_mouse_down && m_selected_btn != touch_gui_button_id_END) {
|
||||
if (m_mode != Mode::Dragging) {
|
||||
m_mode = Mode::Dragging;
|
||||
m_last_good_layout = m_layout;
|
||||
}
|
||||
updateDragState(screensize, mouse_pos - m_last_mouse_pos);
|
||||
|
||||
regenerateGui(screensize);
|
||||
}
|
||||
|
||||
m_last_mouse_pos = mouse_pos;
|
||||
return true;
|
||||
}
|
||||
case EMIE_LMOUSE_LEFT_UP: {
|
||||
m_mouse_down = false;
|
||||
|
||||
if (m_mode == Mode::Dragging) {
|
||||
m_mode = Mode::Default;
|
||||
if (!m_error_rects.empty())
|
||||
m_layout = m_last_good_layout;
|
||||
|
||||
regenerateGui(screensize);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.EventType == EET_GUI_EVENT) {
|
||||
switch (event.GUIEvent.EventType) {
|
||||
case EGET_BUTTON_CLICKED: {
|
||||
if (event.GUIEvent.Caller == m_gui_add_btn.get()) {
|
||||
m_mode = Mode::Add;
|
||||
regenerateGui(screensize);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.GUIEvent.Caller == m_gui_reset_btn.get()) {
|
||||
m_layout = ButtonLayout::predefined;
|
||||
regenerateGui(screensize);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.GUIEvent.Caller == m_gui_done_btn.get()) {
|
||||
if (g_touchcontrols)
|
||||
g_touchcontrols->applyLayout(m_layout);
|
||||
std::ostringstream oss;
|
||||
m_layout.serializeJson(oss);
|
||||
g_settings->set("touch_layout", oss.str());
|
||||
quitMenu();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.GUIEvent.Caller == m_gui_remove_btn.get()) {
|
||||
m_layout.layout.erase(m_selected_btn);
|
||||
regenerateGui(screensize);
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Parent ? Parent->OnEvent(event) : false;
|
||||
}
|
81
src/gui/touchscreeneditor.h
Normal file
81
src/gui/touchscreeneditor.h
Normal file
@ -0,0 +1,81 @@
|
||||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 grorp, Gregor Parzefall <grorp@posteo.de>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "touchscreenlayout.h"
|
||||
#include "modalMenu.h"
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
class ISimpleTextureSource;
|
||||
namespace irr::gui
|
||||
{
|
||||
class IGUIImage;
|
||||
}
|
||||
|
||||
class GUITouchscreenLayout : public GUIModalMenu
|
||||
{
|
||||
public:
|
||||
GUITouchscreenLayout(gui::IGUIEnvironment* env,
|
||||
gui::IGUIElement* parent, s32 id,
|
||||
IMenuManager *menumgr, ISimpleTextureSource *tsrc);
|
||||
~GUITouchscreenLayout();
|
||||
|
||||
void regenerateGui(v2u32 screensize);
|
||||
void drawMenu();
|
||||
bool OnEvent(const SEvent& event);
|
||||
|
||||
protected:
|
||||
std::wstring getLabelByID(s32 id) { return L""; }
|
||||
std::string getNameByID(s32 id) { return ""; }
|
||||
|
||||
private:
|
||||
ISimpleTextureSource *m_tsrc;
|
||||
|
||||
ButtonLayout m_layout;
|
||||
v2u32 m_last_screensize;
|
||||
s32 m_button_size;
|
||||
|
||||
enum class Mode {
|
||||
Default,
|
||||
Dragging,
|
||||
Add,
|
||||
};
|
||||
Mode m_mode = Mode::Default;
|
||||
|
||||
std::unordered_map<touch_gui_button_id, std::shared_ptr<gui::IGUIImage>> m_gui_images;
|
||||
// unused if m_mode == Mode::Add
|
||||
std::unordered_map<touch_gui_button_id, v2s32> m_gui_images_target_pos;
|
||||
void clearGUIImages();
|
||||
void regenerateGUIImagesRegular(v2u32 screensize);
|
||||
void regenerateGUIImagesAddMode(v2u32 screensize);
|
||||
void interpolateGUIImages();
|
||||
|
||||
// interaction state
|
||||
bool m_mouse_down = false;
|
||||
v2s32 m_last_mouse_pos;
|
||||
touch_gui_button_id m_selected_btn = touch_gui_button_id_END;
|
||||
|
||||
// dragging
|
||||
ButtonLayout m_last_good_layout;
|
||||
std::vector<core::recti> m_error_rects;
|
||||
void updateDragState(v2u32 screensize, v2s32 mouse_movement);
|
||||
|
||||
// add mode
|
||||
ButtonLayout m_add_layout;
|
||||
std::vector<std::shared_ptr<gui::IGUIStaticText>> m_add_button_titles;
|
||||
|
||||
// Menu GUI elements
|
||||
std::shared_ptr<gui::IGUIStaticText> m_gui_help_text;
|
||||
|
||||
std::shared_ptr<gui::IGUIButton> m_gui_add_btn;
|
||||
std::shared_ptr<gui::IGUIButton> m_gui_reset_btn;
|
||||
std::shared_ptr<gui::IGUIButton> m_gui_done_btn;
|
||||
|
||||
std::shared_ptr<gui::IGUIButton> m_gui_remove_btn;
|
||||
|
||||
void regenerateMenu(v2u32 screensize);
|
||||
};
|
345
src/gui/touchscreenlayout.cpp
Normal file
345
src/gui/touchscreenlayout.cpp
Normal file
@ -0,0 +1,345 @@
|
||||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 grorp, Gregor Parzefall <grorp@posteo.de>
|
||||
|
||||
#include "touchscreenlayout.h"
|
||||
#include "client/renderingengine.h"
|
||||
#include "client/texturesource.h"
|
||||
#include "convert_json.h"
|
||||
#include "gettext.h"
|
||||
#include "settings.h"
|
||||
#include <json/json.h>
|
||||
|
||||
#include "IGUIFont.h"
|
||||
#include "IGUIStaticText.h"
|
||||
|
||||
const char *button_names[] = {
|
||||
"jump",
|
||||
"sneak",
|
||||
"zoom",
|
||||
"aux1",
|
||||
"overflow",
|
||||
|
||||
"chat",
|
||||
"inventory",
|
||||
"drop",
|
||||
"exit",
|
||||
|
||||
"fly",
|
||||
"fast",
|
||||
"noclip",
|
||||
"debug",
|
||||
"camera",
|
||||
"range",
|
||||
"minimap",
|
||||
"toggle_chat",
|
||||
|
||||
"joystick_off",
|
||||
"joystick_bg",
|
||||
"joystick_center",
|
||||
};
|
||||
|
||||
// compare with GUIKeyChangeMenu::init_keys
|
||||
const char *button_titles[] = {
|
||||
N_("Jump"),
|
||||
N_("Sneak"),
|
||||
N_("Zoom"),
|
||||
N_("Aux1"),
|
||||
N_("Overflow menu"),
|
||||
|
||||
N_("Chat"),
|
||||
N_("Inventory"),
|
||||
N_("Drop"),
|
||||
N_("Exit"),
|
||||
|
||||
N_("Toggle fly"),
|
||||
N_("Toggle fast"),
|
||||
N_("Toggle noclip"),
|
||||
N_("Toggle debug"),
|
||||
N_("Change camera"),
|
||||
N_("Range select"),
|
||||
N_("Toggle minimap"),
|
||||
N_("Toggle chat log"),
|
||||
|
||||
N_("Joystick"),
|
||||
N_("Joystick"),
|
||||
N_("Joystick"),
|
||||
};
|
||||
|
||||
const char *button_image_names[] = {
|
||||
"jump_btn.png",
|
||||
"down.png",
|
||||
"zoom.png",
|
||||
"aux1_btn.png",
|
||||
"overflow_btn.png",
|
||||
|
||||
"chat_btn.png",
|
||||
"inventory_btn.png",
|
||||
"drop_btn.png",
|
||||
"exit_btn.png",
|
||||
|
||||
"fly_btn.png",
|
||||
"fast_btn.png",
|
||||
"noclip_btn.png",
|
||||
"debug_btn.png",
|
||||
"camera_btn.png",
|
||||
"rangeview_btn.png",
|
||||
"minimap_btn.png",
|
||||
// toggle button: switches between "chat_hide_btn.png" and "chat_show_btn.png"
|
||||
"chat_hide_btn.png",
|
||||
|
||||
"joystick_off.png",
|
||||
"joystick_bg.png",
|
||||
"joystick_center.png",
|
||||
};
|
||||
|
||||
v2s32 ButtonMeta::getPos(v2u32 screensize, s32 button_size) const
|
||||
{
|
||||
return v2s32((position.X * screensize.X) + (offset.X * button_size),
|
||||
(position.Y * screensize.Y) + (offset.Y * button_size));
|
||||
}
|
||||
|
||||
void ButtonMeta::setPos(v2s32 pos, v2u32 screensize, s32 button_size)
|
||||
{
|
||||
v2s32 third(screensize.X / 3, screensize.Y / 3);
|
||||
|
||||
if (pos.X < third.X)
|
||||
position.X = 0.0f;
|
||||
else if (pos.X < 2 * third.X)
|
||||
position.X = 0.5f;
|
||||
else
|
||||
position.X = 1.0f;
|
||||
|
||||
if (pos.Y < third.Y)
|
||||
position.Y = 0.0f;
|
||||
else if (pos.Y < 2 * third.Y)
|
||||
position.Y = 0.5f;
|
||||
else
|
||||
position.Y = 1.0f;
|
||||
|
||||
offset.X = (pos.X - (position.X * screensize.X)) / button_size;
|
||||
offset.Y = (pos.Y - (position.Y * screensize.Y)) / button_size;
|
||||
}
|
||||
|
||||
bool ButtonLayout::isButtonAllowed(touch_gui_button_id id)
|
||||
{
|
||||
return id != joystick_off_id && id != joystick_bg_id && id != joystick_center_id &&
|
||||
id != touch_gui_button_id_END;
|
||||
}
|
||||
|
||||
bool ButtonLayout::isButtonRequired(touch_gui_button_id id)
|
||||
{
|
||||
return id == overflow_id;
|
||||
}
|
||||
|
||||
s32 ButtonLayout::getButtonSize(v2u32 screensize)
|
||||
{
|
||||
return std::min(screensize.Y / 4.5f,
|
||||
RenderingEngine::getDisplayDensity() * 65.0f *
|
||||
g_settings->getFloat("hud_scaling"));
|
||||
}
|
||||
|
||||
const ButtonLayout ButtonLayout::predefined {{
|
||||
{jump_id, {
|
||||
v2f(1.0f, 1.0f),
|
||||
v2f(-1.0f, -0.5f),
|
||||
}},
|
||||
{sneak_id, {
|
||||
v2f(1.0f, 1.0f),
|
||||
v2f(-2.5f, -0.5f),
|
||||
}},
|
||||
{zoom_id, {
|
||||
v2f(1.0f, 1.0f),
|
||||
v2f(-0.75f, -3.5f),
|
||||
}},
|
||||
{aux1_id, {
|
||||
v2f(1.0f, 1.0f),
|
||||
v2f(-0.75f, -2.0f),
|
||||
}},
|
||||
{overflow_id, {
|
||||
v2f(1.0f, 1.0f),
|
||||
v2f(-0.75f, -5.0f),
|
||||
}},
|
||||
}};
|
||||
|
||||
ButtonLayout ButtonLayout::loadFromSettings()
|
||||
{
|
||||
bool restored = false;
|
||||
ButtonLayout layout;
|
||||
|
||||
std::string str = g_settings->get("touch_layout");
|
||||
if (!str.empty()) {
|
||||
std::istringstream iss(str);
|
||||
try {
|
||||
layout.deserializeJson(iss);
|
||||
restored = true;
|
||||
} catch (const Json::Exception &e) {
|
||||
warningstream << "Could not parse touchscreen layout: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
if (!restored)
|
||||
return predefined;
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
std::unordered_map<touch_gui_button_id, irr_ptr<video::ITexture>> ButtonLayout::texture_cache;
|
||||
|
||||
video::ITexture *ButtonLayout::getTexture(touch_gui_button_id btn, ISimpleTextureSource *tsrc)
|
||||
{
|
||||
if (texture_cache.count(btn) > 0)
|
||||
return texture_cache.at(btn).get();
|
||||
|
||||
video::ITexture *tex = tsrc->getTexture(button_image_names[btn]);
|
||||
if (!tex)
|
||||
// necessary in the mainmenu
|
||||
tex = tsrc->getTexture(porting::path_share + "/textures/base/pack/" +
|
||||
button_image_names[btn]);
|
||||
irr_ptr<video::ITexture> ptr;
|
||||
ptr.grab(tex);
|
||||
texture_cache[btn] = ptr;
|
||||
return tex;
|
||||
}
|
||||
|
||||
void ButtonLayout::clearTextureCache()
|
||||
{
|
||||
texture_cache.clear();
|
||||
}
|
||||
|
||||
core::recti ButtonLayout::getRect(touch_gui_button_id btn,
|
||||
v2u32 screensize, s32 button_size, ISimpleTextureSource *tsrc)
|
||||
{
|
||||
const ButtonMeta &meta = layout.at(btn);
|
||||
v2s32 pos = meta.getPos(screensize, button_size);
|
||||
|
||||
v2u32 orig_size = getTexture(btn, tsrc)->getOriginalSize();
|
||||
v2s32 size((button_size * orig_size.X) / orig_size.Y, button_size);
|
||||
|
||||
return core::recti(pos - size / 2, core::dimension2di(size));
|
||||
}
|
||||
|
||||
std::vector<touch_gui_button_id> ButtonLayout::getMissingButtons()
|
||||
{
|
||||
std::vector<touch_gui_button_id> missing_buttons;
|
||||
for (u8 i = 0; i < touch_gui_button_id_END; i++) {
|
||||
touch_gui_button_id btn = (touch_gui_button_id)i;
|
||||
if (isButtonAllowed(btn) && layout.count(btn) == 0)
|
||||
missing_buttons.push_back(btn);
|
||||
}
|
||||
return missing_buttons;
|
||||
}
|
||||
|
||||
void ButtonLayout::serializeJson(std::ostream &os) const
|
||||
{
|
||||
Json::Value root = Json::objectValue;
|
||||
root["layout"] = Json::objectValue;
|
||||
|
||||
for (const auto &[id, meta] : layout) {
|
||||
Json::Value button = Json::objectValue;
|
||||
button["position_x"] = meta.position.X;
|
||||
button["position_y"] = meta.position.Y;
|
||||
button["offset_x"] = meta.offset.X;
|
||||
button["offset_y"] = meta.offset.Y;
|
||||
|
||||
root["layout"][button_names[id]] = button;
|
||||
}
|
||||
|
||||
fastWriteJson(root, os);
|
||||
}
|
||||
|
||||
static touch_gui_button_id button_name_to_id(const std::string &name)
|
||||
{
|
||||
for (u8 i = 0; i < touch_gui_button_id_END; i++) {
|
||||
if (name == button_names[i])
|
||||
return (touch_gui_button_id)i;
|
||||
}
|
||||
return touch_gui_button_id_END;
|
||||
}
|
||||
|
||||
void ButtonLayout::deserializeJson(std::istream &is)
|
||||
{
|
||||
layout.clear();
|
||||
|
||||
Json::Value root;
|
||||
is >> root;
|
||||
|
||||
if (!root["layout"].isObject())
|
||||
throw Json::RuntimeError("invalid type for layout");
|
||||
|
||||
Json::Value &obj = root["layout"];
|
||||
Json::ValueIterator iter;
|
||||
for (iter = obj.begin(); iter != obj.end(); iter++) {
|
||||
touch_gui_button_id id = button_name_to_id(iter.name());
|
||||
if (!isButtonAllowed(id))
|
||||
throw Json::RuntimeError("invalid button name");
|
||||
|
||||
Json::Value &value = *iter;
|
||||
if (!value.isObject())
|
||||
throw Json::RuntimeError("invalid type for button metadata");
|
||||
|
||||
ButtonMeta meta;
|
||||
|
||||
if (!value["position_x"].isNumeric() || !value["position_y"].isNumeric())
|
||||
throw Json::RuntimeError("invalid type for position_x or position_y in button metadata");
|
||||
meta.position.X = value["position_x"].asFloat();
|
||||
meta.position.Y = value["position_y"].asFloat();
|
||||
|
||||
if (!value["offset_x"].isNumeric() || !value["offset_y"].isNumeric())
|
||||
throw Json::RuntimeError("invalid type for offset_x or offset_y in button metadata");
|
||||
meta.offset.X = value["offset_x"].asFloat();
|
||||
meta.offset.Y = value["offset_y"].asFloat();
|
||||
|
||||
layout.emplace(id, meta);
|
||||
}
|
||||
}
|
||||
|
||||
void layout_button_grid(v2u32 screensize, ISimpleTextureSource *tsrc,
|
||||
const std::vector<touch_gui_button_id> &buttons,
|
||||
// pos refers to the center of the button
|
||||
const std::function<void(touch_gui_button_id btn, v2s32 pos, core::recti rect)> &callback)
|
||||
{
|
||||
s32 cols = 4;
|
||||
s32 rows = 3;
|
||||
f32 screen_aspect = (f32)screensize.X / (f32)screensize.Y;
|
||||
while ((s32)buttons.size() > cols * rows) {
|
||||
f32 aspect = (f32)cols / (f32)rows;
|
||||
if (aspect > screen_aspect)
|
||||
rows++;
|
||||
else
|
||||
cols++;
|
||||
}
|
||||
|
||||
s32 button_size = ButtonLayout::getButtonSize(screensize);
|
||||
v2s32 spacing(screensize.X / (cols + 1), screensize.Y / (rows + 1));
|
||||
v2s32 pos(spacing);
|
||||
|
||||
for (touch_gui_button_id btn : buttons) {
|
||||
v2u32 orig_size = ButtonLayout::getTexture(btn, tsrc)->getOriginalSize();
|
||||
v2s32 size((button_size * orig_size.X) / orig_size.Y, button_size);
|
||||
|
||||
core::recti rect(pos - size / 2, core::dimension2di(size));
|
||||
|
||||
if (rect.LowerRightCorner.X > (s32)screensize.X) {
|
||||
pos.X = spacing.X;
|
||||
pos.Y += spacing.Y;
|
||||
rect = core::recti(pos - size / 2, core::dimension2di(size));
|
||||
}
|
||||
|
||||
callback(btn, pos, rect);
|
||||
|
||||
pos.X += spacing.X;
|
||||
}
|
||||
}
|
||||
|
||||
void make_button_grid_title(gui::IGUIStaticText *text, touch_gui_button_id btn, v2s32 pos, core::recti rect)
|
||||
{
|
||||
std::wstring str = wstrgettext(button_titles[btn]);
|
||||
text->setText(str.c_str());
|
||||
gui::IGUIFont *font = text->getActiveFont();
|
||||
core::dimension2du dim = font->getDimension(str.c_str());
|
||||
dim = core::dimension2du(dim.Width * 1.25f, dim.Height * 1.25f); // avoid clipping
|
||||
text->setRelativePosition(core::recti(pos.X - dim.Width / 2, rect.LowerRightCorner.Y,
|
||||
pos.X + dim.Width / 2, rect.LowerRightCorner.Y + dim.Height));
|
||||
text->setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_UPPERLEFT);
|
||||
}
|
104
src/gui/touchscreenlayout.h
Normal file
104
src/gui/touchscreenlayout.h
Normal file
@ -0,0 +1,104 @@
|
||||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 grorp, Gregor Parzefall <grorp@posteo.de>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "irr_ptr.h"
|
||||
#include "irrlichttypes_bloated.h"
|
||||
#include "rect.h"
|
||||
#include <iostream>
|
||||
#include <unordered_map>
|
||||
|
||||
class ISimpleTextureSource;
|
||||
namespace irr::gui
|
||||
{
|
||||
class IGUIStaticText;
|
||||
}
|
||||
namespace irr::video
|
||||
{
|
||||
class ITexture;
|
||||
}
|
||||
|
||||
enum touch_gui_button_id : u8
|
||||
{
|
||||
jump_id = 0,
|
||||
sneak_id,
|
||||
zoom_id,
|
||||
aux1_id,
|
||||
overflow_id,
|
||||
|
||||
// formerly "rare controls bar"
|
||||
chat_id,
|
||||
inventory_id,
|
||||
drop_id,
|
||||
exit_id,
|
||||
|
||||
// formerly "settings bar"
|
||||
fly_id,
|
||||
fast_id,
|
||||
noclip_id,
|
||||
debug_id,
|
||||
camera_id,
|
||||
range_id,
|
||||
minimap_id,
|
||||
toggle_chat_id,
|
||||
|
||||
// the joystick
|
||||
joystick_off_id,
|
||||
joystick_bg_id,
|
||||
joystick_center_id,
|
||||
|
||||
touch_gui_button_id_END,
|
||||
};
|
||||
|
||||
extern const char *button_names[];
|
||||
extern const char *button_titles[];
|
||||
extern const char *button_image_names[];
|
||||
|
||||
struct ButtonMeta {
|
||||
// Position, specified as a percentage of the screensize in the range [0,1].
|
||||
// The editor currently writes the values 0, 0.5 and 1.
|
||||
v2f position;
|
||||
// Offset, multiplied by the global button size before it is applied.
|
||||
// Together, position and offset define the position of the button's center.
|
||||
v2f offset;
|
||||
|
||||
// Returns the button's effective center position in pixels.
|
||||
v2s32 getPos(v2u32 screensize, s32 button_size) const;
|
||||
// Sets the button's effective center position in pixels.
|
||||
void setPos(v2s32 pos, v2u32 screensize, s32 button_size);
|
||||
};
|
||||
|
||||
struct ButtonLayout {
|
||||
static bool isButtonAllowed(touch_gui_button_id id);
|
||||
static bool isButtonRequired(touch_gui_button_id id);
|
||||
static s32 getButtonSize(v2u32 screensize);
|
||||
static ButtonLayout loadFromSettings();
|
||||
|
||||
static video::ITexture *getTexture(touch_gui_button_id btn, ISimpleTextureSource *tsrc);
|
||||
static void clearTextureCache();
|
||||
|
||||
std::unordered_map<touch_gui_button_id, ButtonMeta> layout;
|
||||
|
||||
core::recti getRect(touch_gui_button_id btn,
|
||||
v2u32 screensize, s32 button_size, ISimpleTextureSource *tsrc);
|
||||
|
||||
std::vector<touch_gui_button_id> getMissingButtons();
|
||||
|
||||
void serializeJson(std::ostream &os) const;
|
||||
void deserializeJson(std::istream &is);
|
||||
|
||||
static const ButtonLayout predefined;
|
||||
|
||||
private:
|
||||
static std::unordered_map<touch_gui_button_id, irr_ptr<video::ITexture>> texture_cache;
|
||||
};
|
||||
|
||||
void layout_button_grid(v2u32 screensize, ISimpleTextureSource *tsrc,
|
||||
const std::vector<touch_gui_button_id> &buttons,
|
||||
// pos refers to the center of the button.
|
||||
const std::function<void(touch_gui_button_id btn, v2s32 pos, core::recti rect)> &callback);
|
||||
|
||||
void make_button_grid_title(gui::IGUIStaticText *text,
|
||||
touch_gui_button_id btn,v2s32 pos, core::recti rect);
|
28
src/irr_gui_ptr.h
Normal file
28
src/irr_gui_ptr.h
Normal file
@ -0,0 +1,28 @@
|
||||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 grorp, Gregor Parzefall <grorp@posteo.de>
|
||||
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include "IGUIElement.h"
|
||||
|
||||
// We cannot use irr_ptr for Irrlicht GUI elements we own.
|
||||
// Option 1: Pass IGUIElement* returned by IGUIEnvironment::add* into irr_ptr
|
||||
// constructor.
|
||||
// -> We steal the reference owned by IGUIEnvironment and drop it later,
|
||||
// causing the IGUIElement to be deleted while IGUIEnvironment still
|
||||
// references it.
|
||||
// Option 2: Pass IGUIElement* returned by IGUIEnvironment::add* into irr_ptr::grab.
|
||||
// -> We add another reference and drop it later, but since IGUIEnvironment
|
||||
// still references the IGUIElement, it is never deleted.
|
||||
// To make IGUIEnvironment drop its reference to the IGUIElement, we have to call
|
||||
// IGUIElement::remove, so that's what we'll do.
|
||||
template <typename T>
|
||||
std::shared_ptr<T> grab_gui_element(T *element)
|
||||
{
|
||||
static_assert(std::is_base_of_v<irr::gui::IGUIElement, T>,
|
||||
"grab_gui_element only works for IGUIElement");
|
||||
return std::shared_ptr<T>(element, [](T *e) {
|
||||
e->remove();
|
||||
});
|
||||
}
|
@ -11,6 +11,7 @@
|
||||
#include "gui/guiMainMenu.h"
|
||||
#include "gui/guiKeyChangeMenu.h"
|
||||
#include "gui/guiPathSelectMenu.h"
|
||||
#include "gui/touchscreeneditor.h"
|
||||
#include "version.h"
|
||||
#include "porting.h"
|
||||
#include "filesys.h"
|
||||
@ -535,6 +536,22 @@ int ModApiMainMenu::l_show_keys_menu(lua_State *L)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
int ModApiMainMenu::l_show_touchscreen_layout(lua_State *L)
|
||||
{
|
||||
GUIEngine *engine = getGuiEngine(L);
|
||||
sanity_check(engine != NULL);
|
||||
|
||||
GUITouchscreenLayout *gui = new GUITouchscreenLayout(
|
||||
engine->m_rendering_engine->get_gui_env(),
|
||||
engine->m_parent,
|
||||
-1,
|
||||
engine->m_menumanager,
|
||||
engine->m_texture_source.get());
|
||||
gui->drop();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
int ModApiMainMenu::l_create_world(lua_State *L)
|
||||
{
|
||||
@ -1080,6 +1097,7 @@ void ModApiMainMenu::Initialize(lua_State *L, int top)
|
||||
API_FCT(start);
|
||||
API_FCT(close);
|
||||
API_FCT(show_keys_menu);
|
||||
API_FCT(show_touchscreen_layout);
|
||||
API_FCT(create_world);
|
||||
API_FCT(delete_world);
|
||||
API_FCT(set_background);
|
||||
|
@ -69,6 +69,8 @@ private:
|
||||
|
||||
static int l_show_keys_menu(lua_State *L);
|
||||
|
||||
static int l_show_touchscreen_layout(lua_State *L);
|
||||
|
||||
static int l_show_path_select_dialog(lua_State *L);
|
||||
|
||||
static int l_set_topleft_text(lua_State *L);
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.5 KiB |
Loading…
Reference in New Issue
Block a user