diff --git a/data/gui/default/widget/label_default.cfg b/data/gui/default/widget/label_default.cfg index 535a530a37f4..ac68e0300f75 100644 --- a/data/gui/default/widget/label_default.cfg +++ b/data/gui/default/widget/label_default.cfg @@ -96,6 +96,7 @@ #enddef {_GUI_DEFINITION "default" "default label" DEFAULT () DEFAULT ({GUI__TEXT_VERTICALLY_CENTRED})} +{_GUI_DEFINITION "default_bold" "default label, bold font" DEFAULT "bold" DEFAULT ({GUI__TEXT_VERTICALLY_CENTRED})} {_GUI_DEFINITION "scroll_label" "scroll label" DEFAULT () DEFAULT 0} {_GUI_DEFINITION "title" "label used for titles" TITLE "bold" TITLE ({GUI__TEXT_VERTICALLY_CENTRED})} {_GUI_DEFINITION "default_large" "default, large font size" LARGE () DEFAULT ({GUI__TEXT_VERTICALLY_CENTRED})} diff --git a/data/gui/default/window/wml_error.cfg b/data/gui/default/window/wml_error.cfg new file mode 100644 index 000000000000..1e851242d46c --- /dev/null +++ b/data/gui/default/window/wml_error.cfg @@ -0,0 +1,173 @@ +#textdomain wesnoth-lib +### +### Dialog used to report WML parser or preprocessor errors. +### + +[window] + id = "wml_error" + description = "WML preprocessor/parser error report dialog." + + [resolution] + definition = "default" + + #click_dismiss = "true" + maximum_width = 600 + maximum_height = 600 + + automatic_placement = "true" + vertical_placement = "center" + horizontal_placement = "center" + + [tooltip] + id = "tooltip_large" + [/tooltip] + + [helptip] + id = "tooltip_large" + [/helptip] + + [grid] + + [row] + + [column] + border = "all" + border_size = 5 + horizontal_alignment = "left" + + [label] + definition = "title" + label = _ "Error" + [/label] + [/column] + + [/row] + + [row] + + [column] + border = "all" + border_size = 5 + horizontal_alignment = "left" + + [label] + id = "summary" + definition = "default" + wrap = true + [/label] + [/column] + + [/row] + + [row] + + [column] + border = "all" + border_size = 5 + horizontal_alignment = "left" + + [label] + id = "files" + definition = "default" + wrap = true + [/label] + [/column] + + [/row] + + [row] + + [column] + border = "all" + border_size = 5 + horizontal_alignment = "left" + + [label] + id = "post_summary" + definition = "default" + wrap = true + [/label] + [/column] + + [/row] + + [row] + + [column] + horizontal_grow = "true" + + [grid] + + [row] + + [column] + border = "all" + border_size = 5 + horizontal_alignment = "left" + vertical_alignment = "bottom" + + [label] + id = "details_heading" + definition = "default_bold" + label = _ "Details:" + [/label] + [/column] + + [column] + border = "top,left,right" + border_size = 5 + horizontal_alignment = "right" + + [button] + id = "copy" + definition = "action_copy" + label = _ "clipboard^Copy" + tooltip = _ "Copy this report to clipboard" + [/button] + [/column] + + [/row] + + [/grid] + + [/column] + + [/row] + + [row] + + [column] + border = "all" + border_size = 5 + horizontal_grow = "true" + + [scroll_label] + id = "details" + definition = "description" + [/scroll_label] + [/column] + + [/row] + + [row] + + [column] + border = "all" + border_size = 5 + horizontal_alignment = "right" + + [button] + id = "ok" + definition = "default" + label = _ "OK" + [/button] + + [/column] + + [/row] + + [/grid] + + [/resolution] + +[/window] diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c486318c6073..23bee82308bb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -769,6 +769,7 @@ set(wesnoth-main_SRC gui/dialogs/transient_message.cpp gui/dialogs/unit_attack.cpp gui/dialogs/unit_create.cpp + gui/dialogs/wml_error.cpp gui/dialogs/wml_message.cpp halo.cpp help.cpp diff --git a/src/SConscript b/src/SConscript index 07706414d141..75d943dd573f 100644 --- a/src/SConscript +++ b/src/SConscript @@ -395,6 +395,7 @@ wesnoth_sources = Split(""" gui/dialogs/transient_message.cpp gui/dialogs/unit_attack.cpp gui/dialogs/unit_create.cpp + gui/dialogs/wml_error.cpp gui/dialogs/wml_message.cpp gui/lib/types/point.cpp gui/widgets/button.cpp diff --git a/src/game.cpp b/src/game.cpp index 7f944847eaf4..94f81a3ab715 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -466,6 +466,10 @@ static int do_gameloop(int argc, char** argv) const cursor::manager cursor_manager; cursor::set(cursor::WAIT); +#if (defined(_X11) && !defined(__APPLE__)) || defined(_WIN32) + SDL_EventState(SDL_SYSWMEVENT, SDL_ENABLE); +#endif + loadscreen::global_loadscreen_manager loadscreen_manager(game->disp().video()); loadscreen::start_stage("init gui"); @@ -492,10 +496,6 @@ static int do_gameloop(int argc, char** argv) loadscreen::start_stage("refresh addons"); refresh_addon_version_info_cache(); -#if (defined(_X11) && !defined(__APPLE__)) || defined(_WIN32) - SDL_EventState(SDL_SYSWMEVENT, SDL_ENABLE); -#endif - config tips_of_day; loadscreen::start_stage("titlescreen"); diff --git a/src/game_config_manager.cpp b/src/game_config_manager.cpp index af4f9aab2451..4daebd8b8af7 100644 --- a/src/game_config_manager.cpp +++ b/src/game_config_manager.cpp @@ -20,13 +20,14 @@ #include "cursor.hpp" #include "game_config.hpp" #include "gettext.hpp" -#include "gui/dialogs/message.hpp" +#include "gui/dialogs/wml_error.hpp" #include "language.hpp" #include "loadscreen.hpp" #include "log.hpp" #include "resources.hpp" #include "scripting/lua.hpp" #include "hotkey/hotkey_item.hpp" +#include "hotkey/hotkey_command.hpp" #include @@ -174,10 +175,10 @@ void game_config_manager::load_game_config(FORCE_RELOAD_CONFIG force_reload, ::init_strings(game_config()); theme::set_known_themes(&game_config()); } catch(game::error& e) { - ERR_CONFIG << "Error loading game configuration files\n"; - gui2::show_error_message(disp_.video(), - _("Error loading game configuration files: '") + - e.message + _("' (The game will now exit)")); + ERR_CONFIG << "Error loading game configuration files\n" << e.message << '\n'; + gui2::twml_error::display( + _("Error loading game configuration files. The game will now exit."), + e.message, disp_.video()); throw; } @@ -198,7 +199,8 @@ void game_config_manager::load_addons_cfg() get_files_in_dir(user_campaign_dir, &user_files, &user_dirs, ENTIRE_FILE_PATH); - std::stringstream user_error_log; + + std::vector error_log; // Append the $user_campaign_dir/*.cfg files to addons_to_load. BOOST_FOREACH(const std::string& uc, user_files) { @@ -229,11 +231,11 @@ void game_config_manager::load_addons_cfg() ERR_CONFIG << "error reading usermade add-on '" << file << "'\n"; error_addons.push_back(file); - user_error_log << "The format '~" << file.substr(userdata_loc) - << "' is only for single-file add-ons, use '~" - << file.substr(userdata_loc, + error_log.push_back("The format '~" + file.substr(userdata_loc) + + "' is only for single-file add-ons, use '~" + + file.substr(userdata_loc, size_minus_extension - userdata_loc) - << "/_main.cfg' instead.\n"; + + "/_main.cfg' instead."); } else { addons_to_load.push_back(file); @@ -258,29 +260,34 @@ void game_config_manager::load_addons_cfg() game_config_.append(umc_cfg); } catch(config::error& err) { ERR_CONFIG << "error reading usermade add-on '" << uc << "'\n"; + ERR_CONFIG << err.message << '\n'; error_addons.push_back(uc); - user_error_log << err.message << "\n"; + error_log.push_back(err.message); } catch(preproc_config::error& err) { ERR_CONFIG << "error reading usermade add-on '" << uc << "'\n"; + ERR_CONFIG << err.message << '\n'; error_addons.push_back(uc); - user_error_log << err.message << "\n"; + error_log.push_back(err.message); } catch(io_exception&) { ERR_CONFIG << "error reading usermade add-on '" << uc << "'\n"; error_addons.push_back(uc); } } if(error_addons.empty() == false) { - std::stringstream msg; - msg << _n("The following add-on had errors and could not be loaded:", - "The following add-ons had errors and could not be loaded:", - error_addons.size()); - BOOST_FOREACH(const std::string& error_addon, error_addons) { - msg << "\n" << error_addon; - } - - msg << '\n' << _("ERROR DETAILS:") << '\n' << user_error_log.str(); - - gui2::show_error_message(disp_.video(),msg.str()); + const size_t n = error_addons.size(); + const std::string& msg1 = + _n("The following add-on had errors and could not be loaded:", + "The following add-ons had errors and could not be loaded:", + n); + const std::string& msg2 = + _n("Please report this to the author or maintainer of this add-on.", + "Please report this to the respective authors or maintainers of these add-ons.", + n); + + const std::string& report = utils::join(error_log, "\n\n"); + + gui2::twml_error::display(msg1, msg2, error_addons, report, + disp_.video()); } } diff --git a/src/gui/dialogs/wml_error.cpp b/src/gui/dialogs/wml_error.cpp new file mode 100644 index 000000000000..665b4cf88092 --- /dev/null +++ b/src/gui/dialogs/wml_error.cpp @@ -0,0 +1,236 @@ +/* + Copyright (C) 2014 by Ignacio R. Morelle + Part of 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/wml_error.hpp" + +#include "addon/info.hpp" +#include "addon/manager.hpp" +#include "clipboard.hpp" +#include "filesystem.hpp" +#include "gettext.hpp" +#include "gui/auxiliary/find_widget.tpp" +#include "gui/widgets/button.hpp" +#include "gui/widgets/control.hpp" +#include "gui/widgets/settings.hpp" +#include "gui/widgets/window.hpp" +#include "serialization/string_utils.hpp" + +#include + +namespace +{ +inline bool is_dir_separator(char c) +{ +#ifdef _WIN32 + return c == '/' || c == '\\'; +#else + return c == '/'; +#endif +} + +void strip_trailing_dir_separators(std::string& str) +{ + while(is_dir_separator(str[str.size() - 1])) { + str.erase(str.size() - 1); + } +} + +std::string format_file_list(const std::vector& files_original) +{ + if(files_original.empty()) { + return ""; + } + + const std::string& addons_path = get_addon_campaigns_dir(); + std::vector files(files_original); + + BOOST_FOREACH(std::string& file, files) + { + std::string base; + std::string filename = file_name(file); + std::string parent_path; + + const bool is_main_cfg = filename == "_main.cfg"; + + if(is_main_cfg) { + parent_path = directory_name(file) + "/.."; + } else { + parent_path = directory_name(file); + } + + // Only proceed to pretty-format the filename if it's from the add-ons + // directory. + if(normalize_path(parent_path) != normalize_path(addons_path)) { + continue; + } + + if(is_main_cfg) { + base = directory_name(file); + // HACK: fool file_name() into giving us the parent directory name + // alone by making base seem not like a directory path, + // otherwise it returns an empty string. + strip_trailing_dir_separators(base); + base = file_name(base); + } else { + base = filename; + } + + if(base.empty()) { + // We did something wrong. In the interest of not messing up the + // report, leave the original filename intact. + continue; + } + + // + // Display the name as an add-on name instead of a filename. + // + + if(!is_main_cfg) { + // Remove the file extension first. + static const std::string wml_suffix = ".cfg"; + + if(base.size() > wml_suffix.size()) { + const size_t suffix_pos = base.size() - wml_suffix.size(); + if(base.substr(suffix_pos) == wml_suffix) { + base.erase(suffix_pos); + } + } + } + + if(have_addon_install_info(base)) { + // _info.cfg may have the add-on's title starting with 1.11.7, + // if the add-on was downloaded using the revised _info.cfg writer. + config cfg; + get_addon_install_info(base, cfg); + + const config& info_cfg = cfg.child("info"); + + if(info_cfg) { + file = info_cfg["title"].str(); + continue; + } + } + + // Fallback to using a synthetic title with underscores replaced with + // whitespace. + file = make_addon_title(base); + } + + if(files.size() == 1) { + return utils::indent(files.front()); + } + + return utils::bullet_list(files); +} + +} + +namespace gui2 +{ + +/*WIKI + * @page = GUIWindowDefinitionWML + * @order = 2_wml_error + * + * == WML error == + * + * Dialog used to report WML parser or preprocessor errors. + * + * @begin{table}{dialog_widgets} + * + * summary & & control & m & + * Label used for displaying a brief summary of the error(s). $ + * + * files & & control & m & + * Label used to display the list of affected add-ons or files, if + * applicable. It is hidden otherwise. It is recommended to place it + * after the summary label. $ + * + * post_summary & & control & m & + * Label used for displaying instructions for reporting the error. + * It is recommended to place it after the file list label. It may be + * hidden if empty. $ + * + * details & & control & m & + * Full report of the parser or preprocessor error(s) found. $ + * + * copy & & button & m & + * Button that the user can click on to copy the error report to the + * system clipboard. $ + * + * @end{table} + */ + +REGISTER_DIALOG(wml_error) + +twml_error::twml_error(const std::string& summary, + const std::string& post_summary, + const std::vector& files, + const std::string& details) + : have_files_(!files.empty()) + , have_post_summary_(!post_summary.empty()) + , report_() +{ + const std::string& file_list_text = format_file_list(files); + + report_ = summary; + + if(!file_list_text.empty()) { + report_ += "\n" + file_list_text; + } + + if(!post_summary.empty()) { + report_ += "\n\n" + post_summary; + } + + report_ += "\n\n"; + report_ += _("Details:"); + report_ += "\n\n" + utils::indent(details); + // Since this is likely to be pasted into a text file, add a final line + // break for convenience, since otherwise the report ends mid-line. + report_ += "\n"; + + register_label("summary", true, summary); + register_label("post_summary", true, post_summary); + register_label("files", true, file_list_text); + register_label("details", true, details); +} + +void twml_error::pre_show(CVideo& /*video*/, twindow& window) +{ + if(!have_files_) { + tcontrol& filelist = find_widget(&window, "files", false); + filelist.set_visible(tcontrol::tvisible::invisible); + } + + if(!have_post_summary_) { + tcontrol& post_summary = find_widget(&window, + "post_summary", false); + post_summary.set_visible(tcontrol::tvisible::invisible); + } + + tbutton& copy_button = find_widget(&window, "copy", false); + + connect_signal_mouse_left_click( + copy_button, boost::bind(&twml_error::copy_report_callback, this)); +} + +void twml_error::copy_report_callback() +{ + copy_to_clipboard(report_, false); +} + +} // end namespace gui2 diff --git a/src/gui/dialogs/wml_error.hpp b/src/gui/dialogs/wml_error.hpp new file mode 100644 index 000000000000..ad2b93de3287 --- /dev/null +++ b/src/gui/dialogs/wml_error.hpp @@ -0,0 +1,74 @@ +/* + Copyright (C) 2014 by Ignacio R. Morelle + Part of 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. +*/ + +#ifndef GUI_DIALOGS_WML_ERROR_HPP_INCLUDED +#define GUI_DIALOGS_WML_ERROR_HPP_INCLUDED + +#include "gui/dialogs/dialog.hpp" + +namespace gui2 { + +/** WML preprocessor/parser error report dialog. */ +class twml_error : public tdialog +{ +public: + /** + * Constructor. + * + * @param summary Leading summary line for the report. + * @param post_summary Additional line with instructions for the user, may + * be empty. + * @param files List of WML files on which errors were detected. + * @param details Detailed WML preprocessor/parser error report. + */ + twml_error(const std::string& summary, + const std::string& post_summary, + const std::vector& files, + const std::string& details); + + /** The display function; see @ref tdialog for more information. */ + static void display(const std::string& summary, + const std::string& post_summary, + const std::vector& files, + const std::string& details, + CVideo& video) + { + twml_error(summary, post_summary, files, details).show(video); + } + + /** The display function; see @ref tdialog for more information. */ + static void display(const std::string& summary, + const std::string& details, + CVideo& video) + { + display(summary, "", std::vector(), details, video); + } + +private: + bool have_files_; + bool have_post_summary_; + std::string report_; // Plain text report for copying to clipboard. + + /** Inherited from tdialog, implemented by REGISTER_DIALOG. */ + virtual const std::string& window_id() const; + + /** Inherited from tdialog. */ + void pre_show(CVideo& video, twindow& window); + + void copy_report_callback(); +}; + +} // end namespace gui2 + +#endif diff --git a/src/serialization/parser.cpp b/src/serialization/parser.cpp index 909e797a7508..9991fce612e0 100644 --- a/src/serialization/parser.cpp +++ b/src/serialization/parser.cpp @@ -64,7 +64,7 @@ class parser void parse_element(); void parse_variable(); std::string lineno_string(utils::string_map &map, std::string const &lineno, - const char *error_string); + const char *error_string, const char *hint_string = NULL); void error(const std::string& message); config& cfg_; @@ -142,7 +142,8 @@ void parser::operator()() std::stringstream ss; ss << elements.top().start_line << " " << elements.top().file; error(lineno_string(i18n_symbols, ss.str(), - N_("Missing closing tag for tag [$tag] at $pos"))); + N_("Missing closing tag for tag [$tag]"), + N_("at $pos"))); } } @@ -205,7 +206,8 @@ void parser::parse_element() std::stringstream ss; ss << elements.top().start_line << " " << elements.top().file; error(lineno_string(i18n_symbols, ss.str(), - N_("Found invalid closing tag [/$tag2] for tag [$tag1] (opened at $pos)"))); + N_("Found invalid closing tag [/$tag2] for tag [$tag1]"), + N_("opened at $pos"))); } if(validator_){ element & el= elements.top(); @@ -339,10 +341,18 @@ void parser::parse_variable() * This function is crap. Don't use it on a string_map with prefixes. */ std::string parser::lineno_string(utils::string_map &i18n_symbols, - std::string const &lineno, const char *error_string) + std::string const &lineno, + const char *error_string, + const char *hint_string) { i18n_symbols["pos"] = ::lineno_string(lineno); std::string result = _(error_string); + + if(hint_string != NULL) { + result += "\n "; + result += hint_string; + } + BOOST_FOREACH(utils::string_map::value_type& var, i18n_symbols) boost::algorithm::replace_all(result, std::string("$") + var.first, std::string(var.second)); return result; @@ -352,18 +362,18 @@ void parser::error(const std::string& error_type) { utils::string_map i18n_symbols; i18n_symbols["error"] = error_type; - i18n_symbols["value"] = tok_->current_token().value; std::stringstream ss; ss << tok_->get_start_line() << " " << tok_->get_file(); #ifdef DEBUG + i18n_symbols["value"] = tok_->current_token().value; i18n_symbols["previous_value"] = tok_->previous_token().value; throw config::error( lineno_string(i18n_symbols, ss.str(), - N_("$error, value '$value', previous '$previous_value' at $pos"))); + N_("$error"), N_("at $pos (value '$value', previous '$previous_value')"))); #else throw config::error( lineno_string(i18n_symbols, ss.str(), - N_("$error, value '$value' at $pos"))); + N_("$error"), N_("at $pos"))); #endif } diff --git a/src/serialization/preprocessor.cpp b/src/serialization/preprocessor.cpp index 08c2d35f313d..a46de77ddcb4 100644 --- a/src/serialization/preprocessor.cpp +++ b/src/serialization/preprocessor.cpp @@ -55,6 +55,8 @@ static t_file_number_map file_number_map; static bool encode_filename = true; +static std::string preprocessor_error_detail_prefix = "\n "; + // get filename associated to this code static std::string get_filename(const std::string& file_code){ if(!encode_filename) @@ -326,7 +328,8 @@ std::string lineno_string(const std::string &lineno) { std::vector< std::string > pos = utils::quoted_split(lineno, ' '); std::vector< std::string >::const_iterator i = pos.begin(), end = pos.end(); - std::string included_from = " included from "; + std::string included_from = + preprocessor_error_detail_prefix + "included from "; std::string res; while (i != end) { std::string const &line = *(i++); @@ -348,7 +351,7 @@ void preprocessor_streambuf::error(const std::string& error_type, int l) std::ostringstream pos; pos << l << ' ' << location_; position = lineno_string(pos.str()); - error = error_type + " at " + position; + error = error_type + preprocessor_error_detail_prefix + "at " + position; ERR_CF << error << '\n'; throw preproc_config::error(error); } diff --git a/src/serialization/string_utils.cpp b/src/serialization/string_utils.cpp index f86a008256fe..7c02dab549e0 100644 --- a/src/serialization/string_utils.cpp +++ b/src/serialization/string_utils.cpp @@ -741,6 +741,35 @@ bool wildcard_string_match(const std::string& str, const std::string& match) { return matches; } +std::string indent(const std::string& string, size_t indent_size) +{ + if(string.empty() || indent_size == 0) { + return ""; + } + + const std::string indent(indent_size, ' '); + + const std::vector& lines = split(string, '\x0A', 0); + std::string res; + + for(size_t lno = 0; lno < lines.size();) { + const std::string& line = lines[lno]; + + // Lines containing only a carriage return count as empty. + if(!line.empty() && line != "\x0D") { + res += indent; + } + + res += line; + + if(++lno < lines.size()) { + res += '\x0A'; + } + } + + return res; +} + std::vector< std::string > quoted_split(std::string const &val, char c, int flags, char quote) { std::vector res; diff --git a/src/serialization/string_utils.hpp b/src/serialization/string_utils.hpp index 42e9f81f813c..abfb82e203fd 100644 --- a/src/serialization/string_utils.hpp +++ b/src/serialization/string_utils.hpp @@ -191,6 +191,17 @@ std::string bullet_list(const T& v, size_t indent = 4, const std::string& bullet return str.str(); } +/** + * Indent a block of text. + * + * Only lines with content are changed; empty lines are left intact. + * + * @param string Text to indent. + * @param indent_size Number of indentation units to use. + * @param indent_unit Indentation unit. + */ +std::string indent(const std::string& string, size_t indent_size = 4); + /** * This function is identical to split(), except it does not split * when it otherwise would if the previous character was identical to the parameter 'quote'.