Port floating textbox/command console to GUI2

The old floating textbox was extremely entwined with the controller_base, play_controller, and menu_handler
classes. controller_base::have_keyboard_focus essentially controlled whether some events were executed based
on whether the floating textbox was open or not. Additionally, those events weren't even reached if a UI dialog
was open at all.

The new design features a singleton console class that can be called from anywhere, not just the game. I've also
decoupled the execution object from play_controller. The relevant functions in menu_handler are now passed to
the console as callbacks.

To work around map events such as clicking not being available if the console was open, I removed the exclusionary
is-in-dialog check from controller_base::handle_event and instead exit early out certain types of events using
controller_base::have_keyboard_focus. As mentioned in the accompanying comment, this isn't the best solution, but
it will do for now.

The new console also isn't fully feature-comparable with the old GUI1 one. The following are still missing:
* The checkbox, for use when sending messages.
* Tab completion.
* A crash occurs when existing the app if a game was exited with the console open.

I'm leaving the old floating_textbox code around for now for reference.
This commit is contained in:
Charles Dang 2017-06-23 13:20:50 +11:00
parent 770664d1f3
commit ed27b67f1e
12 changed files with 362 additions and 80 deletions

View File

@ -0,0 +1,120 @@
#textdomain wesnoth-lib
[window_definition]
id = "command_console_window"
description = "The window definition for the command console."
[resolution]
# NOTE: we don't specify borders like most definitions since we want
# widgets to fully reach the edge of the window.
[background]
[draw]
[rectangle]
x = 0
y = 0
w = "(width)"
h = "(height)"
fill_color = "0, 0, 0, 150"
[/rectangle]
[/draw]
[/background]
[foreground]
[draw]
[/draw]
[/foreground]
[/resolution]
[/window_definition]
#
# TODO: improve visual design. Hard to see!
#
[window]
id = "command_console"
description = "Command console"
[resolution]
definition = "command_console_window"
automatic_placement = false
x = 0
y = "(gamemap_height - 25)"
width = "(gamemap_width)"
height = "25"
[tooltip]
id = "tooltip_large"
[/tooltip]
[helptip]
id = "tooltip_large"
[/helptip]
[grid]
[row]
grow_factor = 0
[column]
grow_factor = 0
border = "left,right"
border_size = 5
horizontal_alignment = "left"
[label]
id = "prompt"
definition = "default_small"
use_markup = true
[/label]
[/column]
[column]
grow_factor = 1
border = "left,right"
border_size = 5
horizontal_grow = true
[text_box]
id = "input"
definition = "transparent"
[/text_box]
[/column]
# TODO!
[column]
grow_factor = 0
border = "left,right"
border_size = 5
[toggle_button]
id = "toggle"
definition = "default"
label = "Implement Me!"
[/toggle_button]
[/column]
[/row]
[/grid]
[/resolution]
[/window]

View File

@ -546,6 +546,8 @@
<Unit filename="../../src/gui/dialogs/campaign_selection.hpp" />
<Unit filename="../../src/gui/dialogs/chat_log.cpp" />
<Unit filename="../../src/gui/dialogs/chat_log.hpp" />
<Unit filename="../../src/gui/dialogs/command_console.cpp" />
<Unit filename="../../src/gui/dialogs/command_console.hpp" />
<Unit filename="../../src/gui/dialogs/core_selection.cpp" />
<Unit filename="../../src/gui/dialogs/debug_clock.cpp" />
<Unit filename="../../src/gui/dialogs/debug_clock.hpp" />

View File

@ -170,6 +170,7 @@ gui/dialogs/attack_predictions.cpp
gui/dialogs/campaign_difficulty.cpp
gui/dialogs/campaign_selection.cpp
gui/dialogs/chat_log.cpp
gui/dialogs/command_console.cpp
gui/dialogs/core_selection.cpp
gui/dialogs/debug_clock.cpp
gui/dialogs/depcheck_confirm_change.cpp

View File

@ -17,6 +17,7 @@
#include "display.hpp"
#include "events.hpp"
#include "gui/dialogs/loading_screen.hpp"
#include "hotkey/command_executor.hpp"
#include "hotkey/hotkey_command.hpp"
#include "log.hpp"
@ -26,6 +27,7 @@
#include "scripting/plugins/context.hpp"
#include "show_dialog.hpp" //gui::in_dialog
#include "soundsource.hpp"
static lg::log_domain log_display("display");
#define ERR_DP LOG_STREAM(err, log_display)
@ -49,7 +51,17 @@ controller_base::~controller_base()
void controller_base::handle_event(const SDL_Event& event)
{
if(gui::in_dialog()) {
/* TODO: since GUI2 and the main game are now part of the same event context, there is some conflict
* between the GUI2 and event handlers such as these. By design, the GUI2 sdl handler is always on top
* of the handler queue, so its events are handled last. This means events here have a chance to fire
* first. have_keyboard_focus currently returns false if a dialog open, but this is just as stopgap
* measure. We need to figure out a better way to filter out events.
*/
//if(gui::in_dialog()) {
// return;
//}
if(gui2::dialogs::loading_screen::displaying()) {
return;
}
@ -152,7 +164,7 @@ void controller_base::keyup_listener::handle_event(const SDL_Event& event)
bool controller_base::have_keyboard_focus()
{
return true;
return !gui::in_dialog();
}
bool controller_base::handle_scroll(int mousex, int mousey, int mouse_flags, double x_axis, double y_axis)

View File

@ -0,0 +1,89 @@
/*
Copyright (C) 2017 by the Battle for Wesnoth Project http://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.
*/
#define GETTEXT_DOMAIN "wesnoth-lib"
#include "gui/dialogs/command_console.hpp"
#include "gui/auxiliary/find_widget.hpp"
#include "gui/dialogs/modal_dialog.hpp"
#include "gui/widgets/label.hpp"
#include "gui/widgets/settings.hpp"
#include "gui/widgets/text_box.hpp"
#include "gui/widgets/window.hpp"
namespace gui2
{
namespace dialogs
{
REGISTER_DIALOG(command_console)
std::unique_ptr<command_console> command_console::singleton_ = nullptr;
command_console::command_console(const std::string& prompt, callback_t callback)
: input_(nullptr)
, prompt_(prompt)
, command_callback_(callback)
{
}
void command_console::post_build(window& window)
{
// Allow ESC to dismiss the console.
connect_signal_pre_key_press(window,
std::bind(&command_console::window_key_press_callback, this, _5));
input_ = find_widget<text_box>(&window, "input", false, true);
// Execute provided callback on ENTER press.
connect_signal_pre_key_press(*input_,
std::bind(&command_console::input_key_press_callback, this, _5));
}
void command_console::pre_show(window& window)
{
find_widget<label>(&window, "prompt", false).set_label(prompt_);
window.keyboard_capture(input_);
}
void command_console::window_key_press_callback(const SDL_Keycode key)
{
if(key == SDLK_ESCAPE) {
close();
}
}
void command_console::input_key_press_callback(const SDL_Keycode key)
{
if(key == SDLK_RETURN || key == SDLK_KP_ENTER) {
// Execute callback.
if(command_callback_ != nullptr) {
command_callback_(input_->get_value());
}
// Dismiss dialog.
close();
} else if(key == SDLK_TAB) {
// TODO: implement
}
}
void command_console::close()
{
// The modeless_dialog dtor will close the dialog.
singleton_.reset();
}
} // namespace dialogs
} // namespace gui2

View File

@ -0,0 +1,102 @@
/*
Copyright (C) 2017 by the Battle for Wesnoth Project http://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.
*/
#pragma once
#include "gui/dialogs/modeless_dialog.hpp"
#include <SDL_keycode.h>
#include <functional>
#include <memory>
namespace gui2
{
class text_box;
namespace dialogs
{
/**
* A simple command console class.
*
* This class doesn't explicitly handle any processing of commands itself. Instead, it consists
* of a singleton modeless dialog that can be invoked from any area of the game using the static
* @display function. A callback function should be passed in that will handle processing of the
* command input.
*
* @todo This dialog's design definitely needs to be improved (multiline input area, for one).
* Also might be worth looking if more actual command handling could be merged in here.
*/
class command_console : public modeless_dialog
{
private:
/** Callback function signature. */
using callback_t = std::function<void(std::string)>;
public:
command_console(const command_console&) = delete;
command_console& operator=(const command_console&) = delete;
command_console(const std::string& prompt, callback_t callback);
//enum CONSOLE_MODE { MODE_NONE, MODE_SEARCH, MODE_MESSAGE, MODE_COMMAND, MODE_AI };
/**
* Shows the command console. This resets the singleton instance and displays the
* dialog until dismissed by user action.
*
* Since this is a modeless dialog, we can't use the usual method of creating a temporary
* dialog object here. Modal dialogs have a display loop that keeps that open; modeless
* dialogs, however, don't, meaning the object would immediately be destroyed and hidden.
*/
static void display(const std::string& prompt, callback_t callback = nullptr)
{
if(!singleton_) {
singleton_.reset(new command_console(prompt, callback));
singleton_->show(true); // allow interaction
}
}
private:
/** Inherited from modeless_dialog, implemented by REGISTER_DIALOG. */
virtual const std::string& window_id() const override;
/** Inherited from modeless_dialog. */
virtual void post_build(window& window) override;
/** Inherited from modeless_dialog. */
virtual void pre_show(window& window) override;
void window_key_press_callback(const SDL_Keycode key);
void input_key_press_callback(const SDL_Keycode key);
void close();
/** The input text box. */
text_box* input_;
/** Text action prompt. */
std::string prompt_;
/** The callback to process the inputted command. */
callback_t command_callback_;
/** Dialog singleton. */
static std::unique_ptr<command_console> singleton_;
};
} // namespace dialogs
} // namespace gui2

View File

@ -40,6 +40,7 @@
#include "game_state.hpp"
#include "gettext.hpp"
#include "gui/dialogs/chat_log.hpp"
#include "gui/dialogs/command_console.hpp"
#include "gui/dialogs/edit_label.hpp"
#include "gui/dialogs/edit_text.hpp"
#include "gui/dialogs/file_dialog.hpp"
@ -92,7 +93,6 @@ menu_handler::menu_handler(game_display* gui, play_controller& pc, const config&
: gui_(gui)
, pc_(pc)
, game_config_(game_config)
, textbox_info_()
, last_search_()
, last_search_hit_()
{
@ -132,11 +132,6 @@ const gamemap& menu_handler::map() const
return gamestate().board_.map();
}
gui::floating_textbox& menu_handler::get_textbox()
{
return textbox_info_;
}
void menu_handler::objectives()
{
if(!gamestate().lua_kernel_) {
@ -220,11 +215,25 @@ void menu_handler::show_help()
void menu_handler::speak()
{
#if 0
// TODO
const std::string check_prompt = has_friends()
? board().is_observer()
? _("Send to observers only")
: _("Send to allies only")
: "";
#endif
gui2::dialogs::command_console::display(_("Message:"),
std::bind(&menu_handler::do_speak, this, _1));
#if 0
textbox_info_.show(gui::TEXTBOX_MESSAGE, _("Message:"), has_friends()
? board().is_observer()
? _("Send to observers only")
: _("Send to allies only")
: "", preferences::message_private(), *gui_);
#endif
}
void menu_handler::whisper()
@ -999,15 +1008,17 @@ void menu_handler::search()
msg << " [" << last_search_ << "]";
}
msg << ':';
textbox_info_.show(gui::TEXTBOX_SEARCH, msg.str(), "", false, *gui_);
gui2::dialogs::command_console::display("", std::bind(&menu_handler::do_search, this, _1));
}
void menu_handler::do_speak()
void menu_handler::do_speak(const std::string& message)
{
// None of the two parameters really needs to be passed since the information belong to members of the class.
// But since it makes the called method more generic, it is done anyway.
chat_handler::do_speak(
textbox_info_.box()->text(), textbox_info_.check() != nullptr ? textbox_info_.check()->checked() : false);
// TODO: handle checkbox
chat_handler::do_speak(message, false);
}
void menu_handler::add_chat_message(const time_t& time,
@ -1438,7 +1449,6 @@ void console_handler::do_droid()
symbols["side"] = side_s;
command_failed(vgettext("Can't droid a local ai side: '$side'.", symbols));
}
menu_handler_.textbox_info_.close(*menu_handler_.gui_);
}
void console_handler::do_idle()
@ -1476,7 +1486,6 @@ void console_handler::do_idle()
}
}
}
menu_handler_.textbox_info_.close(*menu_handler_.gui_);
}
void console_handler::do_theme()
@ -1537,7 +1546,6 @@ void console_handler::do_control()
}
menu_handler_.request_control_change(side_num, player);
menu_handler_.textbox_info_.close(*(menu_handler_.gui_));
}
void console_handler::do_controller()
@ -1951,7 +1959,7 @@ void console_handler::do_whiteboard_options()
}
}
void menu_handler::do_ai_formula(const std::string& str, int side_num, mouse_handler& /*mousehandler*/)
void menu_handler::do_ai_formula(const std::string& str, int side_num)
{
try {
add_chat_message(time(nullptr), "wfl", 0, ai::manager::get_singleton().evaluate_command(side_num, str));
@ -1963,7 +1971,8 @@ void menu_handler::do_ai_formula(const std::string& str, int side_num, mouse_han
void menu_handler::user_command()
{
textbox_info_.show(gui::TEXTBOX_COMMAND, translation::sgettext("prompt^Command:"), "", false, *gui_);
gui2::dialogs::command_console::display(translation::sgettext("prompt^Command:"),
std::bind(&menu_handler::do_command, this, _1));
}
void menu_handler::request_control_change(int side_num, const std::string& player)
@ -1989,7 +1998,8 @@ void menu_handler::custom_command()
void menu_handler::ai_formula()
{
if(!pc_.is_networked_mp()) {
textbox_info_.show(gui::TEXTBOX_AI, translation::sgettext("prompt^Command:"), "", false, *gui_);
gui2::dialogs::command_console::display(translation::sgettext("prompt^Command:"),
std::bind(&menu_handler::do_ai_formula, this, _1, pc_.current_side()));
}
}

View File

@ -16,7 +16,7 @@
#pragma once
#include "chat_events.hpp"
#include "floating_textbox.hpp"
#include "game_display.hpp"
#include "lua_jailbreak_exception.hpp"
#include "units/map.hpp"
@ -48,7 +48,6 @@ public:
menu_handler(game_display* gui, play_controller& pc, const config& game_config);
virtual ~menu_handler();
gui::floating_textbox& get_textbox();
void set_gui(game_display* gui)
{
gui_ = gui;
@ -110,10 +109,10 @@ public:
///@return Whether or not the recruit was successful
bool do_recruit(const std::string& name, int side_num, const map_location& last_hex);
void do_speak();
void do_speak(const std::string& message);
void do_search(const std::string& new_search);
void do_command(const std::string& str);
void do_ai_formula(const std::string& str, int side_num, mouse_handler& mousehandler);
void do_ai_formula(const std::string& str, int side_num);
void send_to_server(const config& cfg) override;
game_state& gamestate() const;
@ -145,7 +144,6 @@ private:
const config& game_config_;
gui::floating_textbox textbox_info_;
std::string last_search_;
map_location last_search_hit_;
};

View File

@ -52,6 +52,7 @@
#include "save_blocker.hpp"
#include "scripting/game_lua_kernel.hpp"
#include "scripting/plugins/context.hpp"
#include "show_dialog.hpp" //gui::in_dialog
#include "sound.hpp"
#include "soundsource.hpp"
#include "statistics.hpp"
@ -584,39 +585,8 @@ bool play_controller::enemies_visible() const
return false;
}
void play_controller::enter_textbox()
{
if(menu_handler_.get_textbox().active() == false) {
return;
}
const std::string str = menu_handler_.get_textbox().box()->text();
const unsigned int team_num = current_side();
events::mouse_handler& mousehandler = mouse_handler_;
switch(menu_handler_.get_textbox().mode()) {
case gui::TEXTBOX_SEARCH:
menu_handler_.do_search(str);
menu_handler_.get_textbox().close(*gui_);
break;
case gui::TEXTBOX_MESSAGE:
menu_handler_.do_speak();
menu_handler_.get_textbox().close(*gui_); //need to close that one after executing do_speak() !
break;
case gui::TEXTBOX_COMMAND:
menu_handler_.get_textbox().close(*gui_);
menu_handler_.do_command(str);
break;
case gui::TEXTBOX_AI:
menu_handler_.get_textbox().close(*gui_);
menu_handler_.do_ai_formula(str, team_num, mousehandler);
break;
default:
menu_handler_.get_textbox().close(*gui_);
ERR_DP << "unknown textbox mode" << std::endl;
}
}
// TODO: implement in GUI2 command console dialog
#if 0
void play_controller::tab()
{
gui::TEXTBOX_MODE mode = menu_handler_.get_textbox().mode();
@ -676,6 +646,7 @@ void play_controller::tab()
menu_handler_.get_textbox().tab(dictionary);
}
#endif
team& play_controller::current_team()
{
@ -755,22 +726,6 @@ game_display& play_controller::get_display()
return *gui_;
}
bool play_controller::have_keyboard_focus()
{
return !menu_handler_.get_textbox().active();
}
void play_controller::process_focus_keydown_event(const SDL_Event& event)
{
if(event.key.keysym.sym == SDLK_ESCAPE) {
menu_handler_.get_textbox().close(*gui_);
} else if(event.key.keysym.sym == SDLK_TAB) {
tab();
} else if(event.key.keysym.sym == SDLK_RETURN || event.key.keysym.sym == SDLK_KP_ENTER) {
enter_textbox();
}
}
void play_controller::process_keydown_event(const SDL_Event& event)
{
if (event.key.keysym.sym == SDLK_TAB) {

View File

@ -275,8 +275,7 @@ protected:
};
friend struct scoped_savegame_snapshot;
void play_slice_catch();
bool have_keyboard_focus() override;
void process_focus_keydown_event(const SDL_Event& event) override;
void process_keydown_event(const SDL_Event& event) override;
void process_keyup_event(const SDL_Event& event) override;
@ -291,10 +290,6 @@ protected:
void finish_turn(); //this should not throw an end turn or end level exception
bool enemies_visible() const;
void enter_textbox();
void tab();
bool is_team_visible(int team_num, bool observer) const;
/// returns 0 if no such team was found.
int find_last_visible_team() const;

View File

@ -71,7 +71,6 @@ playsingle_controller::playsingle_controller(const config& level,
const config& game_config, const ter_data_cache & tdata, bool skip_replay)
: play_controller(level, state_of_game, game_config, tdata, skip_replay)
, cursor_setter_(cursor::NORMAL)
, textbox_info_()
, replay_sender_(*resources::recorder)
, network_reader_([this](config& cfg) {return receive_from_wesnothd(cfg);})
, turn_data_(replay_sender_, network_reader_)

View File

@ -77,7 +77,6 @@ protected:
virtual void init_gui() override;
const cursor::setter cursor_setter_;
gui::floating_textbox textbox_info_;
replay_network_sender replay_sender_;
playturn_network_adapter network_reader_;