The Battle for Wesnoth  1.19.17+dev
help_browser.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2017 - 2025
3  by Charles Dang <exodia339@gmail.com>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #define GETTEXT_DOMAIN "wesnoth-lib"
17 
19 
20 #include "font/pango/escape.hpp"
21 #include "font/standard_colors.hpp"
22 #include "gui/widgets/button.hpp"
23 #include "gui/widgets/label.hpp"
26 #include "gui/widgets/text_box.hpp"
30 #include "gui/widgets/window.hpp"
31 #include "help/help.hpp"
32 #include "serialization/markup.hpp"
34 #include "utils/ci_searcher.hpp"
35 
36 static lg::log_domain log_help("help");
37 #define ERR_HP LOG_STREAM(err, log_help)
38 #define WRN_HP LOG_STREAM(warn, log_help)
39 #define DBG_HP LOG_STREAM(debug, log_help)
40 
41 namespace gui2::dialogs
42 {
43 
44 namespace
45 {
46  int win_w = 0;
47  int rl_init_w = 0;
48 }
49 
50 REGISTER_DIALOG(help_browser)
51 
52 help_browser::help_browser(const help::section& toplevel, const std::string& initial)
53  : modal_dialog(window_id())
54  , initial_topic_(initial.empty() ? help::default_show_topic : initial)
55  , current_topic_()
56  , toplevel_(toplevel)
57  , history_()
58  , history_pos_(0)
59 {
60  if(initial_topic_.compare(0, 2, "..") == 0) {
61  initial_topic_.replace(0, 2, "+");
62  } else {
63  initial_topic_.insert(0, "-");
64  }
65 }
66 
68 {
69  tree_view& topic_tree = find_widget<tree_view>("topic_tree");
70 
71  button& back_button = find_widget<button>("back");
72  button& next_button = find_widget<button>("next");
73 
74  rich_label& topic_text = find_widget<rich_label>("topic_text");
75  panel& topic_panel = find_widget<panel>("topic_panel");
76 
77  next_button.set_active(false);
78  back_button.set_active(false);
79  connect_signal_mouse_left_click(back_button, std::bind(&help_browser::on_history_navigate, this, true));
80  connect_signal_mouse_left_click(next_button, std::bind(&help_browser::on_history_navigate, this, false));
81 
82  connect_signal<event::BACK_BUTTON_CLICK>([this](auto&&...) {
83  on_history_navigate(true);
85  connect_signal<event::FORWARD_BUTTON_CLICK>([this](auto&&...) {
86  on_history_navigate(false);
88 
89  toggle_button& contents = find_widget<toggle_button>("contents");
90 
91  contents.set_value(true);
92  topic_panel.set_visible(true);
93  connect_signal_mouse_left_click(contents, [&](auto&&...) {
94  auto parent = topic_panel.get_window();
95  // Cache the initial values, get_best_size() keeps changing
96  // initial width of the window
97  if ((parent != nullptr) && (win_w == 0)) {
98  win_w = parent->get_best_size().x;
99  }
100 
101  // initial width of the rich label
102  if(rl_init_w == 0) {
103  rl_init_w = topic_text.get_width();
104  }
105 
106  // Set RL's width and reshow
107  bool is_contents_visible = (topic_panel.get_visible() == widget::visibility::visible);
108  if (parent) {
109  topic_text.set_width(is_contents_visible ? win_w : rl_init_w);
110  show_topic(history_.at(history_pos_), false, true);
111  }
112  topic_panel.set_visible(!is_contents_visible);
114  });
115 
116  text_box& filter = find_widget<text_box>("filter_box");
118  filter.on_modified([this](const auto& box) { update_list(box.text()); });
119 
120  topic_text.register_link_callback(std::bind(&help_browser::show_topic, this, std::placeholders::_1, true, false));
121 
122  connect_signal_notify_modified(topic_tree, std::bind(&help_browser::on_topic_select, this));
123 
124  keyboard_capture(&topic_tree);
125 
127 
128  tree_view_node* initial_node = topic_tree.find_widget<tree_view_node>(initial_topic_, false, false);
129  if(initial_node) {
130  initial_node->select_node(true);
131  }
132 
134 }
135 
136 void help_browser::update_list(const std::string& filter_text) {
137  tree_view& topic_tree = find_widget<tree_view>("topic_tree");
138  topic_tree.clear();
139  if(!add_topics_for_section(toplevel_, topic_tree.get_root_node(), filter_text)) {
140  // Add everything if nothing matches
142  }
143 }
144 
145 bool help_browser::add_topics_for_section(const help::section& parent_section, tree_view_node& parent_node, const std::string& filter_text)
146 {
147  bool topics_added = false;
148  const auto match = translation::make_ci_matcher(filter_text);
149 
150  for(const help::section& section : parent_section.sections) {
151  tree_view_node& section_node = add_topic(section.id, section.title, true, parent_node);
152  bool subtopics_added = add_topics_for_section(section, section_node, filter_text);
153 
154  if (subtopics_added || (match(section.id) || match(section.title))) {
155  if (!filter_text.empty()) {
156  section_node.unfold();
157  }
158  topics_added = true;
159  } else {
160  find_widget<tree_view>("topic_tree").remove_node(&section_node);
161  }
162  }
163 
164  for(const help::topic& topic : parent_section.topics) {
165  if (topic.id[0] == '.') {
166  continue;
167  }
168 
169  if ((match(topic.id) || match(topic.title)) && (topic.id.compare(0, 2, "..") != 0)) {
170  add_topic(topic.id, topic.title, false, parent_node);
171  topics_added = true;
172  }
173  }
174 
175  return topics_added;
176 }
177 
178 tree_view_node& help_browser::add_topic(const std::string& topic_id, const std::string& topic_title,
179  bool expands, tree_view_node& parent)
180 {
182  widget_item item;
183 
184  item["label"] = topic_title;
185  data.emplace("topic_name", item);
186 
187  tree_view_node& new_node = parent.add_child(expands ? "section" : "topic", data);
188  new_node.set_id(std::string(expands ? "+" : "-") + topic_id);
189 
190  return new_node;
191 }
192 
193 void help_browser::show_topic(std::string topic_id, bool add_to_history, bool reshow)
194 {
195  if(reshow) {
196  const help::topic* topic = help::find_topic(toplevel_, topic_id);
197  find_widget<rich_label>("topic_text").set_dom(topic->text.parsed_text());
199  return;
200  }
201 
202  if(topic_id.empty() || topic_id == current_topic_) {
203  return;
204  } else {
205  current_topic_ = topic_id;
206  }
207 
208  if(topic_id[0] == '+') {
209  topic_id.replace(topic_id.begin(), topic_id.begin() + 1, 2, '.');
210  }
211 
212  if(topic_id[0] == '-') {
213  topic_id.erase(topic_id.begin(), topic_id.begin() + 1);
214  }
215 
216  auto iter = parsed_pages_.find(topic_id);
217  if(iter == parsed_pages_.end()) {
218  const help::topic* topic = help::find_topic(toplevel_, topic_id);
219  if(!topic) {
220  ERR_HP << "Help browser tried to show topic with id '" << topic_id
221  << "' but that topic could not be found." << std::endl;
222  return;
223  }
224 
225  DBG_HP << "Showing topic: " << topic->id << ": " << topic->title;
226 
227  std::string topic_id_temp = topic->id;
228  if(topic_id_temp.compare(0, 2, "..") == 0) {
229  topic_id_temp.replace(0, 2, "+");
230  } else {
231  topic_id_temp.insert(0, "-");
232  }
233  tree_view& topic_tree = find_widget<tree_view>("topic_tree");
234  tree_view_node* selected_node = topic_tree.find_widget<tree_view_node>(topic_id_temp, false, false);
235  if(selected_node) {
236  selected_node->select_node(true, false);
237  }
238 
239  find_widget<label>("topic_title").set_label(topic->title);
240  try {
241  find_widget<rich_label>("topic_text").set_dom(topic->text.parsed_text());
242  } catch(const markup::parse_error& e) {
243  find_widget<rich_label>("topic_text").set_label(
245  "Error parsing markup in help page with ID: " + topic->id + "\n"
246  + font::escape_text(e.message)));
247  }
248 
250  }
251 
252  if(add_to_history) {
253  // history pos is 0 initially, so it's already at first entry
254  // no need to increment first time
255  if (!history_.empty()) {
256  // don't add duplicate entries back-to-back
257  if (history_.back() == topic_id) {
258  return;
259  }
260  history_pos_++;
261  }
262  history_.push_back(topic_id);
263 
264  find_widget<button>("back").set_active(history_pos_ != 0);
265  }
266 }
267 
269 {
270  tree_view& topic_tree = find_widget<tree_view>("topic_tree");
271 
272  if(topic_tree.empty()) {
273  return;
274  }
275 
276  tree_view_node* selected = topic_tree.selected_item();
277  assert(selected);
278 
279  show_topic(selected->id());
280 }
281 
283 {
284  if(backwards) {
285  if (history_pos_ > 0) {
286  history_pos_--;
287  } else {
288  return;
289  }
290  } else {
291  if (history_pos_ < history_.size() - 1) {
292  history_pos_++;
293  } else {
294  return;
295  }
296  }
297  find_widget<button>("back").set_active(!history_.empty() && history_pos_ != 0);
298  find_widget<button>("next").set_active(!history_.empty() && history_pos_ != (history_.size()-1));
299 
300  show_topic(history_.at(history_pos_), false);
301 }
302 
303 } // namespace dialogs
Simple push button.
Definition: button.hpp:36
virtual void set_active(const bool active) override
See styled_widget::set_active.
Definition: button.cpp:64
Help browser dialog.
const help::section & toplevel_
void update_list(const std::string &)
bool add_topics_for_section(const help::section &parent_section, tree_view_node &parent_node, const std::string &filter_text="")
std::map< std::string, int > parsed_pages_
virtual void pre_show() override
Actions to be taken before showing the window.
void on_history_navigate(bool backwards)
tree_view_node & add_topic(const std::string &topic_id, const std::string &topic_title, bool expands, tree_view_node &parent)
void show_topic(std::string topic_id, bool add_to_history=true, bool reshow=false)
std::vector< std::string > history_
Abstract base class for all modal dialogs.
A rich_label takes marked up text and shows it correctly formatted and wrapped but no scrollbars are ...
Definition: rich_label.hpp:38
void set_width(const int width)
Definition: rich_label.hpp:136
void register_link_callback(std::function< void(std::string)> link_handler)
Definition: rich_label.cpp:837
A widget that allows the user to input text in single line.
Definition: text_box.hpp:125
void select_node(bool expand_parents=false, bool fire_event=true)
void unfold(const bool recursive=false)
const tree_view_node & get_root_node() const
Definition: tree_view.hpp:53
bool empty() const
Definition: tree_view.cpp:98
tree_view_node * selected_item()
Definition: tree_view.hpp:98
point get_best_size() const
Gets the best size for the widget.
Definition: widget.cpp:203
void set_visible(const visibility visible)
Definition: widget.cpp:479
void set_id(const std::string &id)
Definition: widget.cpp:98
visibility get_visible() const
Definition: widget.cpp:506
window * get_window()
Get the parent window.
Definition: widget.cpp:117
@ visible
The user sets the widget visible, that means:
T * find_widget(const std::string_view id, const bool must_be_active, const bool must_exist)
Gets a widget with the wanted id.
Definition: widget.hpp:747
widget * parent()
Definition: widget.cpp:170
void keyboard_capture(widget *widget)
Definition: window.cpp:1197
void invalidate_layout()
Updates the size of the window.
Definition: window.cpp:759
void add_to_keyboard_chain(widget *widget)
Adds the widget to the keyboard chain.
Definition: window.cpp:1211
const config & parsed_text() const
Definition: help_impl.cpp:280
This file contains the window object, this object is a top level container which has the event manage...
#define DBG_HP
#define ERR_HP
static lg::log_domain log_help("help")
const color_t BAD_COLOR
std::string escape_text(std::string_view text)
Escapes the pango markup characters in a text.
Definition: escape.hpp:33
REGISTER_DIALOG(editor_edit_unit)
void connect_signal_notify_modified(dispatcher &dispatcher, const signal_notification &signal)
Connects a signal handler for getting a notification upon modification.
Definition: dispatcher.cpp:189
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:163
std::map< std::string, widget_item > widget_data
Definition: widget.hpp:36
std::map< std::string, t_string > widget_item
Definition: widget.hpp:33
const topic * find_topic(const section &sec, const std::string &id)
Search for the topic with the specified identifier in the section and its subsections.
Definition: help_impl.cpp:1184
const std::string default_show_topic
Definition: help_impl.cpp:61
std::string span_color(const color_t &color, Args &&... data)
Applies Pango markup to the input specifying its display color.
Definition: markup.hpp:110
auto make_ci_matcher(std::string_view filter_text)
Returns a function which performs locale-aware case-insensitive search.
Definition: ci_searcher.hpp:24
constexpr auto filter
Definition: ranges.hpp:38
std::string_view data
Definition: picture.cpp:188
A section contains topics and sections along with title and ID.
Definition: help_impl.hpp:110
section_list sections
Definition: help_impl.hpp:123
std::string id
Definition: help_impl.hpp:121
std::string title
Definition: help_impl.hpp:121
topic_list topics
Definition: help_impl.hpp:122
A topic contains a title, an id and some text.
Definition: help_impl.hpp:91
std::string id
Definition: help_impl.hpp:101
topic_text text
Definition: help_impl.hpp:102
std::string title
Definition: help_impl.hpp:101
Thrown when the help system fails to parse something.
Definition: markup.hpp:213
#define e