mirror of
https://github.com/wesnoth/wesnoth
synced 2025-05-03 19:27:49 +00:00

CVideo::set_clip -> draw::set_clip CVideo::set_render_target -> draw::set_render_target etc.
1608 lines
45 KiB
C++
1608 lines
45 KiB
C++
/*
|
|
Copyright (C) 2007 - 2022
|
|
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 "cursor.hpp"
|
|
#include "draw.hpp"
|
|
#include "events.hpp"
|
|
#include "floating_label.hpp"
|
|
#include "formula/callable.hpp"
|
|
#include "gettext.hpp"
|
|
#include "log.hpp"
|
|
#include "gui/auxiliary/typed_formula.hpp"
|
|
#include "gui/auxiliary/find_widget.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/title_screen.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 "preferences/general.hpp"
|
|
#include "preferences/display.hpp"
|
|
#include "sdl/rect.hpp"
|
|
#include "sdl/surface.hpp"
|
|
#include "sdl/texture.hpp"
|
|
#include "formula/variant.hpp"
|
|
#include "video.hpp"
|
|
#include "wml_exception.hpp"
|
|
#include "sdl/userevent.hpp"
|
|
#include "sdl/input.hpp" // get_mouse_button_mask
|
|
|
|
#include <functional>
|
|
|
|
#include <algorithm>
|
|
#include <iterator>
|
|
#include <stdexcept>
|
|
|
|
namespace wfl { class function_symbol_table; }
|
|
namespace gui2 { class button; }
|
|
|
|
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 + ':'
|
|
|
|
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
|
|
|
|
/**
|
|
* Pushes a single draw event to the queue. To be used before calling
|
|
* events::pump when drawing windows.
|
|
*
|
|
* @todo: in the future we should simply call draw functions directly
|
|
* from events::pump and do away with the custom drawing events, but
|
|
* that's a 1.15 target. For now, this will have to do.
|
|
*/
|
|
static void push_draw_event()
|
|
{
|
|
// DBG_GUI_E << "Pushing draw event in queue.\n";
|
|
|
|
SDL_Event event;
|
|
sdl::UserEvent data(DRAW_EVENT);
|
|
|
|
event.type = DRAW_EVENT;
|
|
event.user = data;
|
|
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
/**
|
|
* 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.\n";
|
|
|
|
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(std::map<unsigned, window*>::iterator itor = windows_.begin();
|
|
itor != windows_.end();
|
|
++itor) {
|
|
|
|
if(itor->second == &win) {
|
|
windows_.erase(itor);
|
|
return;
|
|
}
|
|
}
|
|
assert(false);
|
|
}
|
|
|
|
unsigned manager::get_id(window& win)
|
|
{
|
|
for(std::map<unsigned, window*>::iterator itor = windows_.begin();
|
|
itor != windows_.end();
|
|
++itor) {
|
|
|
|
if(itor->second == &win) {
|
|
return itor->first;
|
|
}
|
|
}
|
|
assert(false);
|
|
|
|
return 0;
|
|
}
|
|
|
|
window* manager::get_window(const unsigned id)
|
|
{
|
|
std::map<unsigned, window*>::iterator itor = windows_.find(id);
|
|
|
|
if(itor == windows_.end()) {
|
|
return nullptr;
|
|
} else {
|
|
return itor->second;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
window::window(const builder_window::window_resolution& definition)
|
|
: panel(implementation::builder_window(::config {"definition", definition.definition}), type())
|
|
, video_(CVideo::get_singleton())
|
|
, status_(status::NEW)
|
|
, show_mode_(show_mode::none)
|
|
, retval_(retval::NONE)
|
|
, owner_(nullptr)
|
|
, need_layout_(true)
|
|
, variables_()
|
|
, invalidate_layout_blocked_(false)
|
|
, suspend_drawing_(true)
|
|
, restore_(true)
|
|
, is_toplevel_(!is_in_dialog())
|
|
, restorer_()
|
|
, 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_(false)
|
|
, enter_disabled_(false)
|
|
, escape_disabled_(false)
|
|
, linked_size_()
|
|
, mouse_button_state_(0) /**< Needs to be initialized in @ref show. */
|
|
, dirty_list_()
|
|
#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_([](window&)->bool { return true; })
|
|
, callback_next_draw_(nullptr)
|
|
{
|
|
manager::instance().add(*this);
|
|
|
|
connect();
|
|
|
|
if (!video_.faked())
|
|
{
|
|
connect_signal<event::DRAW>(std::bind(&window::draw, 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);
|
|
|
|
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),
|
|
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, false));
|
|
|
|
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(&CVideo::toggle_fullscreen, std::ref(video_)));
|
|
}
|
|
|
|
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);
|
|
|
|
#ifdef DEBUG_WINDOW_LAYOUT_GRAPHS
|
|
|
|
delete debug_layout_;
|
|
|
|
#endif
|
|
}
|
|
|
|
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*/)
|
|
{
|
|
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();
|
|
suspend_drawing_ = false;
|
|
}
|
|
|
|
void window::show_non_modal(/*const unsigned auto_close_timeout*/)
|
|
{
|
|
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();
|
|
suspend_drawing_ = false;
|
|
|
|
push_draw_event();
|
|
|
|
events::pump();
|
|
}
|
|
|
|
int window::show(const bool restore, const unsigned auto_close_timeout)
|
|
{
|
|
/*
|
|
* 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;
|
|
restore_ = restore;
|
|
|
|
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();
|
|
suspend_drawing_ = false;
|
|
|
|
if(auto_close_timeout) {
|
|
// Make sure we're drawn before we try to close ourselves, which can
|
|
// happen if the timeout is small.
|
|
draw();
|
|
|
|
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
|
|
{
|
|
// Start our loop drawing will happen here as well.
|
|
bool mouse_button_state_initialized = false;
|
|
for(status_ = status::SHOWING; status_ != status::CLOSED;) {
|
|
push_draw_event();
|
|
|
|
// process installed callback if valid, to allow e.g. network
|
|
// polling
|
|
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;
|
|
}
|
|
|
|
if(status_ == status::REQUEST_CLOSE) {
|
|
status_ = exit_hook_(*this) ? status::CLOSED : status::SHOWING;
|
|
}
|
|
|
|
// Add a delay so we don't keep spinning if there's no event.
|
|
if(status_ != status::CLOSED) {
|
|
SDL_Delay(10);
|
|
}
|
|
}
|
|
}
|
|
catch(...)
|
|
{
|
|
/**
|
|
* @todo Clean up the code duplication.
|
|
*
|
|
* In the future the restoring shouldn't be needed so the duplication
|
|
* doesn't hurt too much but keep this todo as a reminder.
|
|
*/
|
|
suspend_drawing_ = true;
|
|
|
|
// restore area
|
|
if(restore_) {
|
|
draw::blit(restorer_, get_rectangle());
|
|
font::undraw_floating_labels();
|
|
}
|
|
throw;
|
|
}
|
|
|
|
suspend_drawing_ = true;
|
|
|
|
// restore area
|
|
if(restore_) {
|
|
draw::blit(restorer_, get_rectangle());
|
|
font::undraw_floating_labels();
|
|
}
|
|
|
|
if(text_box_base* tb = dynamic_cast<text_box_base*>(event_distributor_->keyboard_focus())) {
|
|
tb->interrupt_composition();
|
|
}
|
|
|
|
return retval_;
|
|
}
|
|
|
|
void window::draw()
|
|
{
|
|
/***** ***** ***** ***** Init ***** ***** ***** *****/
|
|
// Prohibited from drawing?
|
|
if(suspend_drawing_) {
|
|
return;
|
|
}
|
|
|
|
/***** ***** Layout and get dirty list ***** *****/
|
|
if(need_layout_) {
|
|
// Restore old surface. In the future this phase will not be needed
|
|
// since all will be redrawn when needed with dirty rects. Since that
|
|
// doesn't work yet we need to undraw the window.
|
|
if(restore_ && restorer_) {
|
|
draw::blit(restorer_, get_rectangle());
|
|
}
|
|
|
|
layout();
|
|
|
|
// Get new surface for restoring
|
|
SDL_Rect rect = get_rectangle();
|
|
|
|
// We want the labels underneath the window so draw them and use them
|
|
// as restore point.
|
|
if(is_toplevel_) {
|
|
font::draw_floating_labels();
|
|
}
|
|
|
|
if(restore_) {
|
|
restorer_ = video_.read_texture(&rect);
|
|
}
|
|
|
|
// Need full redraw so only set ourselves dirty.
|
|
dirty_list_.emplace_back(1, this);
|
|
} else {
|
|
|
|
// Let widgets update themselves, which might dirty some things.
|
|
layout_children();
|
|
|
|
// Now find the widgets that are dirty.
|
|
std::vector<widget*> call_stack;
|
|
if(!new_widgets) {
|
|
populate_dirty_list(*this, call_stack);
|
|
} else {
|
|
/* Force to update and redraw the entire screen */
|
|
dirty_list_.clear();
|
|
dirty_list_.emplace_back(1, this);
|
|
}
|
|
}
|
|
|
|
if (dirty_list_.empty()) {
|
|
consecutive_changed_frames_ = 0u;
|
|
return;
|
|
}
|
|
|
|
++consecutive_changed_frames_;
|
|
if(consecutive_changed_frames_ >= 100u && id_ == "title_screen") {
|
|
/* The title screen has changed in 100 consecutive frames, i.e. every
|
|
frame for two seconds. It looks like the screen is constantly changing
|
|
or at least marking widgets as dirty.
|
|
|
|
That's a severe problem. Every time the title screen changes, all
|
|
other GUI windows need to be fully redrawn, with huge CPU usage cost.
|
|
For that reason, this situation is a hard error. */
|
|
throw std::logic_error("The title screen is constantly changing, "
|
|
"which has a huge CPU usage cost. See the code comment.");
|
|
}
|
|
|
|
for(auto & item : dirty_list_)
|
|
{
|
|
|
|
assert(!item.empty());
|
|
|
|
const SDL_Rect dirty_rect
|
|
= new_widgets ? video_.draw_area()
|
|
: item.back()->get_dirty_rectangle();
|
|
|
|
// For testing we disable the clipping rect and force the entire screen to
|
|
// update. This way an item rendered at the wrong place is directly visible.
|
|
#if 0
|
|
dirty_list_.clear();
|
|
dirty_list_.emplace_back(1, this);
|
|
#else
|
|
auto clipper = draw::set_clip(dirty_rect);
|
|
#endif
|
|
|
|
/*
|
|
* The actual update routine does the following:
|
|
* - Restore the background.
|
|
*
|
|
* - draw [begin, end) the back ground of all widgets.
|
|
*
|
|
* - draw the children of the last item in the list, if this item is
|
|
* a container it's children get a full redraw. If it's not a
|
|
* container nothing happens.
|
|
*
|
|
* - draw [rbegin, rend) the fore ground of all widgets. For items
|
|
* which have two layers eg window or panel it draws the foreground
|
|
* layer. For other widgets it's a nop.
|
|
*
|
|
* Before drawing there needs to be determined whether a dirty widget
|
|
* really needs to be redrawn. If the widget doesn't need to be
|
|
* redrawing either being not visibility::visible or has status
|
|
* widget::redraw_action::none. If it's not drawn it's still set not
|
|
* dirty to avoid it keep getting on the dirty list.
|
|
*/
|
|
|
|
for(std::vector<widget*>::iterator itor = item.begin();
|
|
itor != item.end();
|
|
++itor) {
|
|
|
|
if((**itor).get_visible() != widget::visibility::visible
|
|
|| (**itor).get_drawing_action()
|
|
== widget::redraw_action::none) {
|
|
|
|
for(std::vector<widget*>::iterator citor = itor;
|
|
citor != item.end();
|
|
++citor) {
|
|
|
|
(**citor).set_is_dirty(false);
|
|
}
|
|
|
|
item.erase(itor, item.end());
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Restore.
|
|
if(restore_) {
|
|
draw::blit(restorer_, get_rectangle());
|
|
}
|
|
|
|
// Background.
|
|
for(std::vector<widget*>::iterator itor = item.begin();
|
|
itor != item.end();
|
|
++itor) {
|
|
|
|
(**itor).draw_background(0, 0);
|
|
}
|
|
|
|
// Children.
|
|
if(!item.empty()) {
|
|
item.back()->draw_children(0, 0);
|
|
}
|
|
|
|
// Foreground.
|
|
for(std::vector<widget*>::reverse_iterator ritor = item.rbegin();
|
|
ritor != item.rend();
|
|
++ritor) {
|
|
|
|
(**ritor).draw_foreground(0, 0);
|
|
(**ritor).set_is_dirty(false);
|
|
}
|
|
}
|
|
|
|
dirty_list_.clear();
|
|
|
|
redraw_windows_on_top();
|
|
|
|
std::vector<widget*> call_stack;
|
|
populate_dirty_list(*this, call_stack);
|
|
assert(dirty_list_.empty());
|
|
|
|
if(callback_next_draw_ != nullptr) {
|
|
callback_next_draw_();
|
|
callback_next_draw_ = nullptr;
|
|
}
|
|
}
|
|
|
|
void window::undraw()
|
|
{
|
|
if(restore_ && restorer_) {
|
|
draw::blit(restorer_, 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& id, const bool must_be_active)
|
|
{
|
|
return container_base::find(id, must_be_active);
|
|
}
|
|
|
|
const widget* window::find(const std::string& id, const bool must_be_active)
|
|
const
|
|
{
|
|
return container_base::find(id, must_be_active);
|
|
}
|
|
|
|
void window::init_linked_size_group(const std::string& id,
|
|
const bool fixed_width,
|
|
const bool fixed_height)
|
|
{
|
|
assert(fixed_width || fixed_height);
|
|
assert(!has_linked_size_group(id));
|
|
|
|
linked_size_[id] = linked_size(fixed_width, fixed_height);
|
|
}
|
|
|
|
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\n";
|
|
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;
|
|
|
|
std::vector<widget*>::iterator 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()
|
|
{
|
|
/***** 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 = nullptr;
|
|
if((click_dismiss_button
|
|
= find_widget<button>(this, "click_dismiss", false, false))) {
|
|
|
|
click_dismiss_button->set_visible(widget::visibility::invisible);
|
|
}
|
|
if(click_dismiss_) {
|
|
button* btn = find_widget<button>(this, "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 doesn't 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 doesn't 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::redraw_windows_on_top() const
|
|
{
|
|
std::vector<dispatcher*>& dispatchers = event::get_all_dispatchers();
|
|
auto me = std::find(dispatchers.begin(), dispatchers.end(), this);
|
|
|
|
for(auto it = std::next(me); it != dispatchers.end(); ++it) {
|
|
// Note that setting an entire window dirty like this is expensive.
|
|
dynamic_cast<widget&>(**it).set_is_dirty(true);
|
|
}
|
|
}
|
|
|
|
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 = find_widget<grid>(&get_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 << ".\n";
|
|
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.\n";
|
|
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 << ".\n";
|
|
throw layout_exception_width_resize_failed();
|
|
}
|
|
DBG_GUI_L << LOG_IMPL_HEADER
|
|
<< " Status: Resize width succeeded.\n";
|
|
}
|
|
|
|
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 << ".\n";
|
|
throw layout_exception_height_resize_failed();
|
|
}
|
|
DBG_GUI_L << LOG_IMPL_HEADER
|
|
<< " Status: Resize height succeeded.\n";
|
|
}
|
|
|
|
assert(size.x <= static_cast<int>(maximum_width)
|
|
&& size.y <= static_cast<int>(maximum_height));
|
|
|
|
|
|
DBG_GUI_L << LOG_IMPL_HEADER << " Result: Resizing succeeded.\n";
|
|
return;
|
|
}
|
|
catch(const layout_exception_width_modified&)
|
|
{
|
|
DBG_GUI_L << LOG_IMPL_HEADER
|
|
<< " Status: Width has been modified, rerun.\n";
|
|
|
|
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 << ".\n";
|
|
|
|
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) << ".\n";
|
|
|
|
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 << ".\n";
|
|
|
|
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(!enter_disabled_ && (key == SDLK_KP_ENTER || key == SDLK_RETURN)) {
|
|
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 << ".\n";
|
|
|
|
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 << ".\n";
|
|
|
|
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 << ".\n";
|
|
|
|
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 << '\n';
|
|
|
|
load_resolutions<resolution>(cfg);
|
|
}
|
|
|
|
window_definition::resolution::resolution(const config& cfg)
|
|
: panel_definition::resolution(cfg), grid(nullptr)
|
|
{
|
|
const config& child = cfg.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.
|
|
*/
|