wesnoth/src/gui/widgets/window.cpp
Charles Dang 4f163b2528 GUI2/Window: clear to-be-rendered area unconditionally
This fixes overpaintin issue with continually-updated to-be-drawn windows
2025-03-17 17:47:40 -04:00

1568 lines
44 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Copyright (C) 2007 - 2025
by Mark de Wever <koraq@xs4all.nl>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
/**
* @file
* Implementation of window.hpp.
*/
#define GETTEXT_DOMAIN "wesnoth-lib"
#include "gui/widgets/window_private.hpp"
#include "config.hpp"
#include "draw.hpp"
#include "events.hpp"
#include "formula/callable.hpp"
#include "formula/string_utils.hpp"
#include "gettext.hpp"
#include "log.hpp"
#include "wml_exception.hpp"
#include "gui/auxiliary/typed_formula.hpp"
#include "gui/core/event/distributor.hpp"
#include "gui/core/event/handler.hpp"
#include "gui/core/event/message.hpp"
#include "gui/core/log.hpp"
#include "gui/core/layout_exception.hpp"
#include "sdl/point.hpp"
#include "gui/core/window_builder.hpp"
#include "gui/dialogs/tooltip.hpp"
#include "gui/widgets/button.hpp"
#include "gui/widgets/container_base.hpp"
#include "gui/widgets/text_box_base.hpp"
#include "gui/core/register_widget.hpp"
#include "gui/widgets/grid.hpp"
#include "gui/widgets/helper.hpp"
#include "gui/widgets/panel.hpp"
#include "gui/widgets/settings.hpp"
#include "gui/widgets/widget.hpp"
#include "gui/widgets/window.hpp"
#ifdef DEBUG_WINDOW_LAYOUT_GRAPHS
#include "gui/widgets/debug.hpp"
#endif
#include "sdl/rect.hpp"
#include "sdl/texture.hpp"
#include "formula/variant.hpp"
#include "video.hpp" // only for toggle_fullscreen
#include "sdl/userevent.hpp"
#include "sdl/input.hpp" // get_mouse_button_mask
#include <functional>
#include <algorithm>
static lg::log_domain log_gui("gui/layout");
#define ERR_GUI LOG_STREAM(err, log_gui)
#define LOG_SCOPE_HEADER get_control_type() + " [" + id() + "] " + __func__
#define LOG_HEADER LOG_SCOPE_HEADER + ':'
#define LOG_IMPL_SCOPE_HEADER \
window.get_control_type() + " [" + window.id() + "] " + __func__
#define LOG_IMPL_HEADER LOG_IMPL_SCOPE_HEADER + ':'
static lg::log_domain log_display("display");
#define DBG_DP LOG_STREAM(debug, log_display)
#define LOG_DP LOG_STREAM(info, log_display)
#define WRN_DP LOG_STREAM(warn, log_display)
namespace gui2
{
// ------------ WIDGET -----------{
namespace implementation
{
/** @todo See whether this hack can be removed. */
// Needed to fix a compiler error in REGISTER_WIDGET.
class builder_window : public builder_styled_widget
{
public:
builder_window(const config& cfg) : builder_styled_widget(cfg)
{
}
using builder_styled_widget::build;
virtual std::unique_ptr<widget> build() const override
{
return nullptr;
}
};
} // namespace implementation
REGISTER_WIDGET(window)
namespace
{
#ifdef DEBUG_WINDOW_LAYOUT_GRAPHS
const unsigned SHOW = debug_layout_graph::SHOW;
const unsigned LAYOUT = debug_layout_graph::LAYOUT;
#else
// values are irrelavant when DEBUG_WINDOW_LAYOUT_GRAPHS is not defined.
const unsigned SHOW = 0;
const unsigned LAYOUT = 0;
#endif
/**
* SDL_AddTimer() callback for delay_event.
*
* @param event The event to push in the event queue.
*
* @return The new timer interval (always 0).
*/
static uint32_t delay_event_callback(const uint32_t, void* event)
{
SDL_PushEvent(static_cast<SDL_Event*>(event));
delete static_cast<SDL_Event*>(event);
return 0;
}
/**
* Allows an event to be delayed a certain amount of time.
*
* @note the delay is the minimum time, after the time has passed the event
* will be pushed in the SDL event queue, so it might delay more.
*
* @param event The event to delay.
* @param delay The number of ms to delay the event.
*/
static void delay_event(const SDL_Event& event, const uint32_t delay)
{
SDL_AddTimer(delay, delay_event_callback, new SDL_Event(event));
}
/**
* Adds a SHOW_HELPTIP event to the SDL event queue.
*
* The event is used to show the helptip for the currently focused widget.
*/
static void helptip()
{
DBG_GUI_E << "Pushing SHOW_HELPTIP_EVENT event in queue.";
SDL_Event event;
sdl::UserEvent data(SHOW_HELPTIP_EVENT);
event.type = SHOW_HELPTIP_EVENT;
event.user = data;
SDL_PushEvent(&event);
}
/**
* Small helper class to get an unique id for every window instance.
*
* This is used to send event to the proper window, this allows windows to post
* messages to themselves and let them delay for a certain amount of time.
*/
class manager
{
manager();
public:
static manager& instance();
void add(window& window);
void remove(window& window);
unsigned get_id(window& window);
window* get_window(const unsigned id);
private:
// The number of active window should be rather small
// so keep it simple and don't add a reverse lookup map.
std::map<unsigned, window*> windows_;
};
manager::manager() : windows_()
{
}
manager& manager::instance()
{
static manager window_manager;
return window_manager;
}
void manager::add(window& win)
{
static unsigned id;
++id;
windows_[id] = &win;
}
void manager::remove(window& win)
{
for(auto itor = windows_.begin(); itor != windows_.end(); ++itor) {
if(itor->second == &win) {
windows_.erase(itor);
return;
}
}
assert(false);
}
unsigned manager::get_id(window& win)
{
for(const auto& [id, window_ptr] : windows_) {
if(window_ptr == &win) {
return id;
}
}
assert(false);
return 0;
}
window* manager::get_window(const unsigned id)
{
if(auto itor = windows_.find(id); itor != windows_.end()) {
return itor->second;
} else {
return nullptr;
}
}
} // namespace
window::window(const builder_window::window_resolution& definition)
: panel(implementation::builder_window(::config {"definition", definition.definition}), type())
, status_(status::NEW)
, show_mode_(show_mode::none)
, retval_(retval::NONE)
, owner_(nullptr)
, need_layout_(true)
, variables_()
, invalidate_layout_blocked_(false)
, hidden_(true)
, automatic_placement_(definition.automatic_placement)
, horizontal_placement_(definition.horizontal_placement)
, vertical_placement_(definition.vertical_placement)
, maximum_width_(definition.maximum_width)
, maximum_height_(definition.maximum_height)
, x_(definition.x)
, y_(definition.y)
, w_(definition.width)
, h_(definition.height)
, reevaluate_best_size_(definition.reevaluate_best_size)
, functions_(definition.functions)
, tooltip_(definition.tooltip)
, helptip_(definition.helptip)
, click_dismiss_(definition.click_dismiss)
, enter_disabled_(false)
, escape_disabled_(false)
, linked_size_()
, mouse_button_state_(0) /**< Needs to be initialized in @ref show. */
#ifdef DEBUG_WINDOW_LAYOUT_GRAPHS
, debug_layout_(new debug_layout_graph(this))
#endif
, event_distributor_(new event::distributor(*this, event::dispatcher::front_child))
, exit_hook_([] { return true; })
{
manager::instance().add(*this);
connect();
for(const auto& [id, fixed_width, fixed_height] : definition.linked_groups) {
if(!init_linked_size_group(id, fixed_width, fixed_height)) {
FAIL(VGETTEXT("Linked $id group has multiple definitions.", {{"id", id}}));
}
}
const auto conf = cast_config_to<window_definition>();
assert(conf);
if(conf->grid) {
init_grid(*conf->grid);
finalize(*definition.grid);
} else {
init_grid(*definition.grid);
}
add_to_keyboard_chain(this);
connect_signal<event::SDL_VIDEO_RESIZE>(std::bind(
&window::signal_handler_sdl_video_resize, this, std::placeholders::_2, std::placeholders::_3, std::placeholders::_5));
connect_signal<event::SDL_ACTIVATE>(std::bind(
&event::distributor::initialize_state, event_distributor_.get()));
connect_signal<event::SDL_LEFT_BUTTON_UP>(
std::bind(&window::signal_handler_click_dismiss,
this,
std::placeholders::_2,
std::placeholders::_3,
std::placeholders::_4,
SDL_BUTTON_LMASK),
event::dispatcher::front_child);
connect_signal<event::SDL_MIDDLE_BUTTON_UP>(
std::bind(&window::signal_handler_click_dismiss,
this,
std::placeholders::_2,
std::placeholders::_3,
std::placeholders::_4,
SDL_BUTTON_MMASK),
event::dispatcher::front_child);
connect_signal<event::SDL_RIGHT_BUTTON_UP>(
std::bind(&window::signal_handler_click_dismiss,
this,
std::placeholders::_2,
std::placeholders::_3,
std::placeholders::_4,
SDL_BUTTON_RMASK),
event::dispatcher::front_child);
// FIXME investigate why this handler is being called twice and if this is actually needed
connect_signal<event::SDL_KEY_DOWN>(
std::bind(
&window::signal_handler_sdl_key_down, this, std::placeholders::_2, std::placeholders::_3, std::placeholders::_5, std::placeholders::_6, false),
event::dispatcher::back_post_child);
connect_signal<event::SDL_KEY_DOWN>(std::bind(
&window::signal_handler_sdl_key_down, this, std::placeholders::_2, std::placeholders::_3, std::placeholders::_5, std::placeholders::_6, true));
connect_signal<event::MESSAGE_SHOW_TOOLTIP>(
std::bind(&window::signal_handler_message_show_tooltip,
this,
std::placeholders::_2,
std::placeholders::_3,
std::placeholders::_5),
event::dispatcher::back_pre_child);
connect_signal<event::MESSAGE_SHOW_HELPTIP>(
std::bind(&window::signal_handler_message_show_helptip,
this,
std::placeholders::_2,
std::placeholders::_3,
std::placeholders::_5),
event::dispatcher::back_pre_child);
connect_signal<event::REQUEST_PLACEMENT>(
std::bind(
&window::signal_handler_request_placement, this, std::placeholders::_2, std::placeholders::_3),
event::dispatcher::back_pre_child);
connect_signal<event::CLOSE_WINDOW>(std::bind(&window::signal_handler_close_window, this));
register_hotkey(hotkey::GLOBAL__HELPTIP, std::bind(gui2::helptip));
/** @todo: should eventally become part of global hotkey handling. */
register_hotkey(hotkey::HOTKEY_FULLSCREEN,
std::bind(&video::toggle_fullscreen));
}
window::~window()
{
/*
* We need to delete our children here instead of waiting for the grid to
* automatically do it. The reason is when the grid deletes its children
* they will try to unregister them self from the linked widget list. At
* this point the member of window are destroyed and we enter UB. (For
* some reason the bug didn't trigger on g++ but it does on MSVC.
*/
for(unsigned row = 0; row < get_grid().get_rows(); ++row) {
for(unsigned col = 0; col < get_grid().get_cols(); ++col) {
get_grid().remove_child(row, col);
}
}
/*
* The tip needs to be closed if the window closes and the window is
* not a tip. If we don't do that the tip will unrender in the next
* window and cause drawing glitches.
* Another issue is that on smallgui and an MP game the tooltip not
* unrendered properly can capture the mouse and make playing impossible.
*/
if(show_mode_ == show_mode::modal) {
dialogs::tip::remove();
}
manager::instance().remove(*this);
// If we are currently shown, then queue an undraw.
if(!hidden_) {
queue_redraw();
}
}
window* window::window_instance(const unsigned handle)
{
return manager::instance().get_window(handle);
}
retval window::get_retval_by_id(const std::string& id)
{
// Note it might change to a map later depending on the number
// of items.
if(id == "ok") {
return retval::OK;
} else if(id == "cancel" || id == "quit") {
return retval::CANCEL;
} else {
return retval::NONE;
}
}
void window::show_tooltip(/*const unsigned auto_close_timeout*/)
{
// Unhide in any case.
hidden_ = false;
// Connect to the event handler, if not yet connected.
if(!is_connected()) {
LOG_DP << "connecting " << id() << " on show_tooltip";
connect();
}
log_scope2(log_gui_draw, "Window: show as tooltip.");
generate_dot_file("show", SHOW);
assert(status_ == status::NEW);
set_mouse_behavior(event::dispatcher::mouse_behavior::none);
set_want_keyboard_input(false);
show_mode_ = show_mode::tooltip;
/*
* Before show has been called, some functions might have done some testing
* on the window and called layout, which can give glitches. So
* reinvalidate the window to avoid those glitches.
*/
invalidate_layout();
queue_redraw();
DBG_DP << "show tooltip queued to " << get_rectangle();
}
void window::show_non_modal(/*const unsigned auto_close_timeout*/)
{
// Unhide in any case.
hidden_ = false;
// Connect to the event handler, if not yet connected.
if(!is_connected()) {
LOG_DP << "connecting " << id() << " on show_non_modal";
connect();
}
log_scope2(log_gui_draw, "Window: show non modal.");
generate_dot_file("show", SHOW);
assert(status_ == status::NEW);
set_mouse_behavior(event::dispatcher::mouse_behavior::hit);
show_mode_ = show_mode::modeless;
/*
* Before show has been called, some functions might have done some testing
* on the window and called layout, which can give glitches. So
* reinvalidate the window to avoid those glitches.
*/
invalidate_layout();
queue_redraw();
DBG_DP << "show non-modal queued to " << get_rectangle();
events::pump_and_draw();
}
int window::show(const unsigned auto_close_timeout)
{
// Unhide in any case.
hidden_ = false;
// Connect to the event handler, if not yet connected.
if(!is_connected()) {
LOG_DP << "connecting " << id() << " on show";
connect();
}
/*
* Removes the old tip if one shown. The show_tip doesn't remove
* the tip, since it's the tip.
*/
dialogs::tip::remove();
show_mode_ = show_mode::modal;
log_scope2(log_gui_draw, LOG_SCOPE_HEADER);
generate_dot_file("show", SHOW);
//assert(status_ == status::NEW);
/*
* Before show has been called, some functions might have done some testing
* on the window and called layout, which can give glitches. So
* reinvalidate the window to avoid those glitches.
*/
invalidate_layout();
queue_redraw();
// Make sure we display at least once in all cases.
events::draw();
if(auto_close_timeout) {
SDL_Event event;
sdl::UserEvent data(CLOSE_WINDOW_EVENT, manager::instance().get_id(*this));
event.type = CLOSE_WINDOW_EVENT;
event.user = data;
delay_event(event, auto_close_timeout);
}
try
{
// According to the comment in the next loop, we need to pump() once
// before we know which mouse buttons are down. Assume they're all
// down, otherwise there's a race condition when the MOUSE_UP gets
// processed in the first pump(), which immediately closes the window.
bool mouse_button_state_initialized = false;
mouse_button_state_ = std::numeric_limits<uint32_t>::max();
// Start our loop, drawing will happen here as well.
for(status_ = status::SHOWING; status_ != status::CLOSED;) {
// Process and handle all pending events.
events::pump();
if(!mouse_button_state_initialized) {
/*
* The state must be initialize when showing the dialog.
* However when initialized before this point there were random
* errors. This only happened when the 'click' was done fast; a
* slower click worked properly.
*
* So it seems the events need to be processed before SDL can
* return the proper button state. When initializing here all
* works fine.
*/
mouse_button_state_ = sdl::get_mouse_button_mask();
mouse_button_state_initialized = true;
}
// See if we should close.
if(status_ == status::REQUEST_CLOSE) {
status_ = exit_hook_() ? status::CLOSED : status::SHOWING;
}
// Update the display. This will rate limit to vsync.
events::draw();
}
}
catch(...)
{
// TODO: is this even necessary? What are we catching?
DBG_DP << "Caught general exception in show(): " << utils::get_unknown_exception_type();
hide();
throw;
}
if(text_box_base* tb = dynamic_cast<text_box_base*>(event_distributor_->keyboard_focus())) {
tb->interrupt_composition();
}
// The window may be kept around to be re-shown later. Hide it for now.
hide();
return retval_;
}
void window::draw()
{
if(hidden_) {
return;
}
// Draw background.
if(!this->draw_background()) {
// We may need to blur the background behind the window,
// but at this point it hasn't been rendered yet.
// We thus defer rendering to next frame so we can snapshot what
// is underneath the window without drawing over it.
defer_region(get_rectangle());
return;
}
// Draw children.
this->draw_children();
// Draw foreground.
if(!this->draw_foreground()) {
defer_region(get_rectangle());
}
return;
}
void window::hide()
{
// Queue a redraw of the region if we were shown.
if(!hidden_) {
queue_redraw();
}
// Disconnect from the event handler so we stop receiving events.
if(is_connected()) {
LOG_DP << "disconnecting " << id() << " on hide";
disconnect();
}
hidden_ = true;
}
void window::update_render_textures()
{
point draw = get_size();
point render = draw * video::get_pixel_scale();
// Check that the render buffer size is correct.
point buf_raw = render_buffer_.get_raw_size();
point buf_draw = render_buffer_.draw_size();
bool raw_size_changed = buf_raw.x != render.x || buf_raw.y != render.y;
bool draw_size_changed = buf_draw.x != draw.x || buf_draw.y != draw.y;
if (!raw_size_changed && !draw_size_changed) {
// buffers are fine
return;
}
if(raw_size_changed) {
LOG_DP << "regenerating window render buffer as " << render;
render_buffer_ = texture(render.x, render.y, SDL_TEXTUREACCESS_TARGET);
}
if(raw_size_changed || draw_size_changed) {
LOG_DP << "updating window render buffer draw size to " << draw;
render_buffer_.set_draw_size(draw);
}
// Clear the entire texture.
{
auto setter = draw::set_render_target(render_buffer_);
draw::clear();
}
// Rerender everything.
queue_rerender();
}
void window::queue_rerender()
{
queue_rerender(get_rectangle());
}
void window::queue_rerender(const rect& screen_region)
{
// More than one region updating per-frame should be rare.
// Just rerender the minimal area that covers everything.
rect local_region = screen_region.intersect(get_rectangle());
local_region.shift(-get_origin());
awaiting_rerender_.expand_to_cover(local_region);
}
void window::defer_region(const rect& screen_region)
{
LOG_DP << "deferring region " << screen_region;
deferred_regions_.push_back(screen_region);
}
void window::render()
{
// Ensure our render texture is correctly sized.
update_render_textures();
// Mark regions that were previously deferred for rerender and repaint.
for(auto& region : deferred_regions_) {
queue_redraw(region);
}
deferred_regions_.clear();
// Render the portion of the window awaiting rerender (if any).
if (awaiting_rerender_.empty()) {
return;
}
DBG_DP << "window::render() local " << awaiting_rerender_;
auto target_setter = draw::set_render_target(render_buffer_);
auto clip_setter = draw::override_clip(awaiting_rerender_);
// Clear the to-be-rendered area unconditionally
draw::clear();
draw();
awaiting_rerender_ = sdl::empty_rect;
}
bool window::expose(const rect& region)
{
DBG_DP << "window::expose " << region;
// Calculate the destination region we need to draw.
rect dst = get_rectangle().intersect(region);
dst.clip(draw::get_clip());
if (dst.empty()) {
return false;
}
// Blit from the pre-rendered buffer.
rect src = dst;
src.shift(-get_origin());
render_buffer_.set_src(src);
draw::blit(render_buffer_, dst);
render_buffer_.clear_src();
return true;
}
rect window::screen_location()
{
if(hidden_) {
return {0,0,0,0};
}
return get_rectangle();
}
window::invalidate_layout_blocker::invalidate_layout_blocker(window& window)
: window_(window)
{
assert(!window_.invalidate_layout_blocked_);
window_.invalidate_layout_blocked_ = true;
}
window::invalidate_layout_blocker::~invalidate_layout_blocker()
{
assert(window_.invalidate_layout_blocked_);
window_.invalidate_layout_blocked_ = false;
}
void window::invalidate_layout()
{
if(!invalidate_layout_blocked_) {
need_layout_ = true;
}
}
widget* window::find_at(const point& coordinate, const bool must_be_active)
{
return panel::find_at(coordinate, must_be_active);
}
const widget* window::find_at(const point& coordinate,
const bool must_be_active) const
{
return panel::find_at(coordinate, must_be_active);
}
widget* window::find(const std::string_view id, const bool must_be_active)
{
return container_base::find(id, must_be_active);
}
const widget* window::find(const std::string_view id, const bool must_be_active)
const
{
return container_base::find(id, must_be_active);
}
bool window::init_linked_size_group(const std::string& id, const bool fixed_width, const bool fixed_height)
{
assert(fixed_width || fixed_height);
auto [iter, success] = linked_size_.try_emplace(id, fixed_width, fixed_height);
return success;
}
bool window::has_linked_size_group(const std::string& id)
{
return linked_size_.find(id) != linked_size_.end();
}
void window::add_linked_widget(const std::string& id, widget* wgt)
{
assert(wgt);
if(!has_linked_size_group(id)) {
ERR_GUI << "Unknown linked group '" << id << "'; skipping";
return;
}
std::vector<widget*>& widgets = linked_size_[id].widgets;
if(std::find(widgets.begin(), widgets.end(), wgt) == widgets.end()) {
widgets.push_back(wgt);
}
}
void window::remove_linked_widget(const std::string& id, const widget* wgt)
{
assert(wgt);
if(!has_linked_size_group(id)) {
return;
}
std::vector<widget*>& widgets = linked_size_[id].widgets;
auto itor = std::find(widgets.begin(), widgets.end(), wgt);
if(itor != widgets.end()) {
widgets.erase(itor);
assert(std::find(widgets.begin(), widgets.end(), wgt)
== widgets.end());
}
}
void window::layout()
{
if(!need_layout_) {
return;
}
DBG_DP << "window::layout";
/***** Initialize. *****/
const auto conf = cast_config_to<window_definition>();
assert(conf);
log_scope2(log_gui_layout, LOG_SCOPE_HEADER);
point size = get_best_size();
const point mouse = get_mouse_position();
variables_.add("mouse_x", wfl::variant(mouse.x));
variables_.add("mouse_y", wfl::variant(mouse.y));
variables_.add("window_width", wfl::variant(0));
variables_.add("window_height", wfl::variant(0));
variables_.add("best_window_width", wfl::variant(size.x));
variables_.add("best_window_height", wfl::variant(size.y));
variables_.add("size_request_mode", wfl::variant("maximum"));
get_screen_size_variables(variables_);
unsigned int maximum_width = maximum_width_(variables_, &functions_);
unsigned int maximum_height = maximum_height_(variables_, &functions_);
if(automatic_placement_) {
if(maximum_width == 0 || maximum_width > settings::screen_width) {
maximum_width = settings::screen_width;
}
if(maximum_height == 0 || maximum_height > settings::screen_height) {
maximum_height = settings::screen_height;
}
} else {
maximum_width = w_(variables_, &functions_);
maximum_height = h_(variables_, &functions_);
}
/***** Handle click dismiss status. *****/
button* click_dismiss_button = find_widget<button>("click_dismiss", false, false);
if(click_dismiss_button) {
click_dismiss_button->set_visible(widget::visibility::invisible);
}
if(click_dismiss_) {
button* btn = find_widget<button>("ok", false, false);
if(btn) {
btn->set_visible(widget::visibility::invisible);
click_dismiss_button = btn;
}
VALIDATE(click_dismiss_button,
_("Click dismiss needs a click_dismiss or ok button."));
}
/***** Layout. *****/
layout_initialize(true);
generate_dot_file("layout_initialize", LAYOUT);
layout_linked_widgets();
try
{
window_implementation::layout(*this, maximum_width, maximum_height);
}
catch(const layout_exception_resize_failed&)
{
/** @todo implement the scrollbars on the window. */
std::stringstream sstr;
sstr << __FILE__ << ":" << __LINE__ << " in function '" << __func__
<< "' found the following problem: Failed to size window;"
<< " wanted size " << get_best_size() << " available size "
<< maximum_width << ',' << maximum_height << " screen size "
<< settings::screen_width << ',' << settings::screen_height << '.';
throw wml_exception(_("Failed to show a dialog, "
"which doesnt fit on the screen."),
sstr.str());
}
/****** Validate click dismiss status. *****/
if(click_dismiss_ && disable_click_dismiss()) {
assert(click_dismiss_button);
click_dismiss_button->set_visible(widget::visibility::visible);
connect_signal_mouse_left_click(
*click_dismiss_button,
std::bind(&window::set_retval, this, retval::OK, true));
layout_initialize(true);
generate_dot_file("layout_initialize", LAYOUT);
layout_linked_widgets();
try
{
window_implementation::layout(
*this, maximum_width, maximum_height);
}
catch(const layout_exception_resize_failed&)
{
/** @todo implement the scrollbars on the window. */
std::stringstream sstr;
sstr << __FILE__ << ":" << __LINE__ << " in function '" << __func__
<< "' found the following problem: Failed to size window;"
<< " wanted size " << get_best_size() << " available size "
<< maximum_width << ',' << maximum_height << " screen size "
<< settings::screen_width << ',' << settings::screen_height
<< '.';
throw wml_exception(_("Failed to show a dialog, "
"which doesnt fit on the screen."),
sstr.str());
}
}
/***** Get the best location for the window *****/
size = get_best_size();
/* Although 0-size windows might not seem valid/useful, there are
a handful of windows that request 0 size just to get a position
chosen via the code below, so at least for now allow them:
*/
assert(size.x >= 0 && static_cast<unsigned>(size.x) <= maximum_width
&& size.y >= 0 && static_cast<unsigned>(size.y) <= maximum_height);
point origin(0, 0);
if(automatic_placement_) {
switch(horizontal_placement_) {
case grid::HORIZONTAL_ALIGN_LEFT:
// Do nothing
break;
case grid::HORIZONTAL_ALIGN_CENTER:
origin.x = (settings::screen_width - size.x) / 2;
break;
case grid::HORIZONTAL_ALIGN_RIGHT:
origin.x = settings::screen_width - size.x;
break;
default:
assert(false);
}
switch(vertical_placement_) {
case grid::VERTICAL_ALIGN_TOP:
// Do nothing
break;
case grid::VERTICAL_ALIGN_CENTER:
origin.y = (settings::screen_height - size.y) / 2;
break;
case grid::VERTICAL_ALIGN_BOTTOM:
origin.y = settings::screen_height - size.y;
break;
default:
assert(false);
}
} else {
variables_.add("window_width", wfl::variant(size.x));
variables_.add("window_height", wfl::variant(size.y));
while(reevaluate_best_size_(variables_, &functions_)) {
layout_initialize(true);
window_implementation::layout(*this,
w_(variables_, &functions_),
h_(variables_, &functions_));
size = get_best_size();
variables_.add("window_width", wfl::variant(size.x));
variables_.add("window_height", wfl::variant(size.y));
}
variables_.add("size_request_mode", wfl::variant("size"));
size.x = w_(variables_, &functions_);
size.y = h_(variables_, &functions_);
variables_.add("window_width", wfl::variant(size.x));
variables_.add("window_height", wfl::variant(size.y));
origin.x = x_(variables_, &functions_);
origin.y = y_(variables_, &functions_);
}
/***** Set the window size *****/
place(origin, size);
generate_dot_file("layout_finished", LAYOUT);
need_layout_ = false;
event::init_mouse_location();
}
void window::layout_linked_widgets()
{
// evaluate the group sizes
for(auto & linked_size : linked_size_)
{
point max_size(0, 0);
// Determine the maximum size.
for(auto widget : linked_size.second.widgets)
{
const point size = widget->get_best_size();
if(size.x > max_size.x) {
max_size.x = size.x;
}
if(size.y > max_size.y) {
max_size.y = size.y;
}
}
if(linked_size.second.width != -1) {
linked_size.second.width = max_size.x;
}
if(linked_size.second.height != -1) {
linked_size.second.height = max_size.y;
}
// Set the maximum size.
for(auto widget : linked_size.second.widgets)
{
point size = widget->get_best_size();
if(linked_size.second.width != -1) {
size.x = max_size.x;
}
if(linked_size.second.height != -1) {
size.y = max_size.y;
}
widget->set_layout_size(size);
}
}
}
bool window::click_dismiss(const int mouse_button_mask)
{
if(does_click_dismiss()) {
if((mouse_button_state_ & mouse_button_mask) == 0) {
set_retval(retval::OK);
} else {
mouse_button_state_ &= ~mouse_button_mask;
}
return true;
}
return false;
}
void window::finalize(const builder_grid& content_grid)
{
auto widget = content_grid.build();
assert(widget);
static const std::string id = "_window_content_grid";
// Make sure the new child has same id.
widget->set_id(id);
auto* parent_grid = get_grid().find_widget<grid>(id, true, false);
assert(parent_grid);
if(grid* grandparent_grid = dynamic_cast<grid*>(parent_grid->parent())) {
grandparent_grid->swap_child(id, std::move(widget), false);
} else if(container_base* c = dynamic_cast<container_base*>(parent_grid->parent())) {
c->get_grid().swap_child(id, std::move(widget), true);
} else {
assert(false);
}
}
#ifdef DEBUG_WINDOW_LAYOUT_GRAPHS
void window::generate_dot_file(const std::string& generator,
const unsigned domain)
{
debug_layout_->generate_dot_file(generator, domain);
}
#endif
void window_implementation::layout(window& window,
const unsigned maximum_width,
const unsigned maximum_height)
{
log_scope2(log_gui_layout, LOG_IMPL_SCOPE_HEADER);
/*
* For now we return the status, need to test later whether this can
* entirely be converted to an exception based system as in 'promised' on
* the algorithm page.
*/
try
{
point size = window.get_best_size();
DBG_GUI_L << LOG_IMPL_HEADER << " best size : " << size
<< " maximum size : " << maximum_width << ','
<< maximum_height << ".";
if(size.x <= static_cast<int>(maximum_width)
&& size.y <= static_cast<int>(maximum_height)) {
DBG_GUI_L << LOG_IMPL_HEADER << " Result: Fits, nothing to do.";
return;
}
if(size.x > static_cast<int>(maximum_width)) {
window.reduce_width(maximum_width);
size = window.get_best_size();
if(size.x > static_cast<int>(maximum_width)) {
DBG_GUI_L << LOG_IMPL_HEADER << " Result: Resize width failed."
<< " Wanted width " << maximum_width
<< " resulting width " << size.x << ".";
throw layout_exception_width_resize_failed();
}
DBG_GUI_L << LOG_IMPL_HEADER
<< " Status: Resize width succeeded.";
}
if(size.y > static_cast<int>(maximum_height)) {
window.reduce_height(maximum_height);
size = window.get_best_size();
if(size.y > static_cast<int>(maximum_height)) {
DBG_GUI_L << LOG_IMPL_HEADER << " Result: Resize height failed."
<< " Wanted height " << maximum_height
<< " resulting height " << size.y << ".";
throw layout_exception_height_resize_failed();
}
DBG_GUI_L << LOG_IMPL_HEADER
<< " Status: Resize height succeeded.";
}
assert(size.x <= static_cast<int>(maximum_width)
&& size.y <= static_cast<int>(maximum_height));
DBG_GUI_L << LOG_IMPL_HEADER << " Result: Resizing succeeded.";
return;
}
catch(const layout_exception_width_modified&)
{
DBG_GUI_L << LOG_IMPL_HEADER
<< " Status: Width has been modified, rerun.";
window.layout_initialize(false);
window.layout_linked_widgets();
layout(window, maximum_width, maximum_height);
return;
}
}
void window::mouse_capture(const bool capture)
{
assert(event_distributor_);
event_distributor_->capture_mouse(capture);
}
void window::keyboard_capture(widget* widget)
{
assert(event_distributor_);
event_distributor_->keyboard_capture(widget);
}
void window::add_to_keyboard_chain(widget* widget)
{
assert(event_distributor_);
event_distributor_->keyboard_add_to_chain(widget);
}
void window::remove_from_keyboard_chain(widget* widget)
{
assert(event_distributor_);
event_distributor_->keyboard_remove_from_chain(widget);
}
void window::add_to_tab_order(widget* widget, int at)
{
if(std::find(tab_order.begin(), tab_order.end(), widget) != tab_order.end()) {
return;
}
assert(event_distributor_);
if(tab_order.empty() && !event_distributor_->keyboard_focus()) {
keyboard_capture(widget);
}
if(at < 0 || at >= static_cast<int>(tab_order.size())) {
tab_order.push_back(widget);
} else {
tab_order.insert(tab_order.begin() + at, widget);
}
}
void window::signal_handler_sdl_video_resize(const event::ui_event event,
bool& handled,
const point& new_size)
{
DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
settings::gamemap_width += new_size.x - settings::screen_width;
settings::gamemap_height += new_size.y - settings::screen_height;
settings::screen_width = new_size.x;
settings::screen_height = new_size.y;
invalidate_layout();
handled = true;
}
void window::signal_handler_click_dismiss(const event::ui_event event,
bool& handled,
bool& halt,
const int mouse_button_mask)
{
DBG_GUI_E << LOG_HEADER << ' ' << event << " mouse_button_mask "
<< static_cast<unsigned>(mouse_button_mask) << ".";
handled = halt = click_dismiss(mouse_button_mask);
}
static bool is_active(const widget* wgt)
{
if(const styled_widget* control = dynamic_cast<const styled_widget*>(wgt)) {
return control->get_active() && control->get_visible() == widget::visibility::visible;
}
return false;
}
void window::signal_handler_sdl_key_down(const event::ui_event event,
bool& handled,
const SDL_Keycode key,
const SDL_Keymod mod,
bool handle_tab)
{
DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
if(text_box_base* tb = dynamic_cast<text_box_base*>(event_distributor_->keyboard_focus())) {
if(tb->is_composing()) {
if(handle_tab && !tab_order.empty() && key == SDLK_TAB) {
tb->interrupt_composition();
} else {
return;
}
}
}
if(key == SDLK_KP_ENTER || key == SDLK_RETURN) {
if (mod & (KMOD_CTRL | KMOD_ALT | KMOD_GUI | KMOD_SHIFT)) {
// Don't handle if modifier is pressed
handled = false;
} else {
// Trigger window OK button only if Enter enabled,
// otherwise pass handling to widget
if (!enter_disabled_) {
set_retval(retval::OK);
handled = true;
}
}
} else if(key == SDLK_ESCAPE && !escape_disabled_) {
set_retval(retval::CANCEL);
handled = true;
} else if(key == SDLK_SPACE) {
handled = click_dismiss(0);
} else if(handle_tab && !tab_order.empty() && key == SDLK_TAB) {
assert(event_distributor_);
widget* focus = event_distributor_->keyboard_focus();
auto iter = std::find(tab_order.begin(), tab_order.end(), focus);
do {
if(mod & KMOD_SHIFT) {
if(iter == tab_order.begin()) {
iter = tab_order.end();
}
iter--;
} else {
if(iter == tab_order.end()) {
iter = tab_order.begin();
} else {
iter++;
if(iter == tab_order.end()) {
iter = tab_order.begin();
}
}
}
} while(!is_active(*iter));
keyboard_capture(*iter);
handled = true;
}
#ifdef DEBUG_WINDOW_LAYOUT_GRAPHS
if(key == SDLK_F12) {
debug_layout_->generate_dot_file("manual", debug_layout_graph::MANUAL);
handled = true;
}
#endif
}
void window::signal_handler_message_show_tooltip(const event::ui_event event,
bool& handled,
const event::message& message)
{
DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
const event::message_show_tooltip& request
= dynamic_cast<const event::message_show_tooltip&>(message);
dialogs::tip::show(tooltip_.id, request.message, request.location, request.source_rect);
handled = true;
}
void window::signal_handler_message_show_helptip(const event::ui_event event,
bool& handled,
const event::message& message)
{
DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
const event::message_show_helptip& request
= dynamic_cast<const event::message_show_helptip&>(message);
dialogs::tip::show(helptip_.id, request.message, request.location, request.source_rect);
handled = true;
}
void window::signal_handler_request_placement(const event::ui_event event,
bool& handled)
{
DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
invalidate_layout();
handled = true;
}
void window::signal_handler_close_window()
{
set_retval(retval::AUTO_CLOSE);
}
// }---------- DEFINITION ---------{
window_definition::window_definition(const config& cfg)
: styled_widget_definition(cfg)
{
DBG_GUI_P << "Parsing window " << id;
load_resolutions<resolution>(cfg);
}
window_definition::resolution::resolution(const config& cfg)
: panel_definition::resolution(cfg), grid(nullptr)
{
auto child = cfg.optional_child("grid");
// VALIDATE(child, _("No grid defined."));
/** @todo Evaluate whether the grid should become mandatory. */
if(child) {
grid = std::make_shared<builder_grid>(*child);
}
}
// }------------ END --------------
} // namespace gui2
/**
* @page layout_algorithm Layout algorithm
*
* @section introduction-layout_algorithm Introduction
*
* This page describes how the layout engine for the dialogs works. First
* a global overview of some terms used in this document.
*
* - @ref gui2::widget "Widget"; Any item which can be used in the widget
* toolkit. Not all widgets are visible. In general widgets cannot be
* sized directly, but this is controlled by a window. A widget has an
* internal size cache and if the value in the cache is not equal to 0,0
* that value is its best size. This value gets set when the widget can
* honor a resize request. It will be set with the value which honors
* the request.
*
* - @ref gui2::grid "Grid"; A grid is an invisible container which holds
* one or more widgets. Several widgets have a grid in them to hold
* multiple widgets eg panels and windows.
*
* - @ref gui2::grid::child "Grid cell"; Every widget which is in a grid is
* put in a grid cell. These cells also hold the information about the gaps
* between widgets the behavior on growing etc. All grid cells must have a
* widget inside them.
*
* - @ref gui2::window "Window"; A window is a top level item which has a
* grid with its children. The window handles the sizing of the window and
* makes sure everything fits.
*
* - @ref gui2::window::linked_size "Shared size group"; A shared size
* group is a number of widgets which share width and or height. These
* widgets are handled separately in the layout algorithm. All grid cells
* width such a widget will get the same height and or width and these
* widgets won't be resized when there's not enough size. To be sure that
* these widgets don't cause trouble for the layout algorithm, they must be
* in a container with scrollbars so there will always be a way to properly
* layout them. The engine must enforce this restriction so the shared
* layout property must be set by the engine after validation.
*
* - All visible grid cells; A grid cell is visible when the widget inside
* of it doesn't have the state visibility::invisible. Widgets which have the
* state visibility::hidden are sized properly since when they become
* visibility::visible the layout shouldn't be invalidated. A grid cell
* that's invisible has size 0,0.
*
* - All resizable grid cells; A grid cell is resizable under the following
* conditions:
* - The widget is visibility::visible.
* - The widget is not in a shared size group.
*
* There are two layout algorithms with a different purpose.
*
* - The Window algorithm; this algorithm's goal is it to make sure all grid
* cells fit in the window. Sizing the grid cells depends on the widget
* size as well, but this algorithm only sizes the grid cells and doesn't
* handle the widgets inside them.
*
* - The Grid algorithm; after the Window algorithm made sure that all grid
* cells fit this algorithm makes sure the widgets are put in the optimal
* state in their grid cell.
*
* @section layout_algorithm_window Window
*
* Here is the algorithm used to layout the window:
*
* - Perform a full initialization
* (@ref gui2::widget::layout_initialize (full_initialization = true)):
* - Clear the internal best size cache for all widgets.
* - For widgets with scrollbars hide them unless the
* @ref gui2::scrollbar_container::scrollbar_mode "scrollbar_mode" is
* ALWAYS_VISIBLE or AUTO_VISIBLE.
* - Handle shared sizes:
* - Height and width:
* - Get the best size for all widgets that share height and width.
* - Set the maximum of width and height as best size for all these
* widgets.
* - Width only:
* - Get the best width for all widgets which share their width.
* - Set the maximum width for all widgets, but keep their own height.
* - Height only:
* - Get the best height for all widgets which share their height.
* - Set the maximum height for all widgets, but keep their own width.
* - Start layout loop:
* - Get best size.
* - If width <= maximum_width && height <= maximum_height we're done.
* - If width > maximum_width, optimize the width:
* - For every grid cell in a grid row there will be a resize request
* (@ref gui2::grid::reduce_width):
* - Sort the widgets in the row on the resize priority.
* - Loop through this priority queue until the row fits
* - If priority != 0 try to share the extra width else all
* widgets are tried to reduce the full size.
* - Try to shrink the widgets by either wrapping or using a
* scrollbar (@ref gui2::widget::request_reduce_width).
* - If the row fits in the wanted width this row is done.
* - Else try the next priority.
* - All priorities done and the width still doesn't fit.
* - Loop through this priority queue until the row fits.
* - If priority != 0:
* - try to share the extra width
* -Else:
* - All widgets are tried to reduce the full size.
* - Try to shrink the widgets by sizing them smaller as really
* wanted (@ref gui2::widget::demand_reduce_width).
* For labels, buttons etc. they get ellipsized.
* - If the row fits in the wanted width this row is done.
* - Else try the next priority.
* - All priorities done and the width still doesn't fit.
* - Throw a layout width doesn't fit exception.
* - If height > maximum_height, optimize the height
* (@ref gui2::grid::reduce_height):
* - For every grid cell in a grid column there will be a resize request:
* - Sort the widgets in the column on the resize priority.
* - Loop through this priority queue until the column fits:
* - If priority != 0 try to share the extra height else all
* widgets are tried to reduce the full size.
* - Try to shrink the widgets by using a scrollbar
* (@ref gui2::widget::request_reduce_height).
* - If succeeded for a widget the width is influenced and the
* width might be invalid.
* - Throw a width modified exception.
* - If the column fits in the wanted height this column is done.
* - Else try the next priority.
* - All priorities done and the height still doesn't fit.
* - Loop through this priority queue until the column fits.
* - If priority != 0 try to share the extra height else all
* widgets are tried to reduce the full size.
* - Try to shrink the widgets by sizing them smaller as really
* wanted (@ref gui2::widget::demand_reduce_width).
* For labels, buttons etc. they get ellipsized .
* - If the column fits in the wanted height this column is done.
* - Else try the next priority.
* - All priorities done and the height still doesn't fit.
* - Throw a layout height doesn't fit exception.
* - End layout loop.
*
* - Catch @ref gui2::layout_exception_width_modified "width modified":
* - Goto relayout.
*
* - Catch
* @ref gui2::layout_exception_width_resize_failed "width resize failed":
* - If the window has a horizontal scrollbar which isn't shown but can be
* shown.
* - Show the scrollbar.
* - goto relayout.
* - Else show a layout failure message.
*
* - Catch
* @ref gui2::layout_exception_height_resize_failed "height resize failed":
* - If the window has a vertical scrollbar which isn't shown but can be
* shown:
* - Show the scrollbar.
* - goto relayout.
* - Else:
* - show a layout failure message.
*
* - Relayout:
* - Initialize all widgets
* (@ref gui2::widget::layout_initialize (full_initialization = false))
* - Handle shared sizes, since the reinitialization resets that state.
* - Goto start layout loop.
*
* @section grid Grid
*
* This section will be documented later.
*/