The Battle for Wesnoth  1.19.15+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  }
66 }
67 
69 {
70  tree_view& topic_tree = find_widget<tree_view>("topic_tree");
71 
72  button& back_button = find_widget<button>("back");
73  button& next_button = find_widget<button>("next");
74 
75  rich_label& topic_text = find_widget<rich_label>("topic_text");
76  panel& topic_panel = find_widget<panel>("topic_panel");
77 
78  next_button.set_active(false);
79  back_button.set_active(false);
80  connect_signal_mouse_left_click(back_button, std::bind(&help_browser::on_history_navigate, this, true));
81  connect_signal_mouse_left_click(next_button, std::bind(&help_browser::on_history_navigate, this, false));
82 
83  connect_signal<event::BACK_BUTTON_CLICK>([this](auto&&...) {
84  on_history_navigate(true);
86  connect_signal<event::FORWARD_BUTTON_CLICK>([this](auto&&...) {
87  on_history_navigate(false);
89 
90  toggle_button& contents = find_widget<toggle_button>("contents");
91 
92  contents.set_value(true);
93  topic_panel.set_visible(true);
94  connect_signal_mouse_left_click(contents, [&](auto&&...) {
95  auto parent = topic_panel.get_window();
96  // Cache the initial values, get_best_size() keeps changing
97  // initial width of the window
98  if ((parent != nullptr) && (win_w == 0)) {
99  win_w = parent->get_best_size().x;
100  }
101 
102  // initial width of the rich label
103  if(rl_init_w == 0) {
104  rl_init_w = topic_text.get_width();
105  }
106 
107  // Set RL's width and reshow
108  bool is_contents_visible = (topic_panel.get_visible() == widget::visibility::visible);
109  if (parent) {
110  topic_text.set_width(is_contents_visible ? win_w : rl_init_w);
111  show_topic(history_.at(history_pos_), false, true);
112  }
113  topic_panel.set_visible(!is_contents_visible);
115  });
116 
117  text_box& filter = find_widget<text_box>("filter_box");
119  filter.on_modified([this](const auto& box) { update_list(box.text()); });
120 
121  topic_text.register_link_callback(std::bind(&help_browser::show_topic, this, std::placeholders::_1, true, false));
122 
123  connect_signal_notify_modified(topic_tree, std::bind(&help_browser::on_topic_select, this));
124 
125  keyboard_capture(&topic_tree);
126 
128 
129  tree_view_node* initial_node = topic_tree.find_widget<tree_view_node>(initial_topic_, false, false);
130  if(initial_node) {
131  initial_node->select_node(true);
132  }
133 
135 }
136 
137 void help_browser::update_list(const std::string& filter_text) {
138  tree_view& topic_tree = find_widget<tree_view>("topic_tree");
139  topic_tree.clear();
140  if(!add_topics_for_section(toplevel_, topic_tree.get_root_node(), filter_text)) {
141  // Add everything if nothing matches
143  }
144 }
145 
146 bool help_browser::add_topics_for_section(const help::section& parent_section, tree_view_node& parent_node, const std::string& filter_text)
147 {
148  bool topics_added = false;
149  const auto match = translation::make_ci_matcher(filter_text);
150 
151  for(const help::section& section : parent_section.sections) {
152  tree_view_node& section_node = add_topic(section.id, section.title, true, parent_node);
153  bool subtopics_added = add_topics_for_section(section, section_node, filter_text);
154 
155  if (subtopics_added || (match(section.id) || match(section.title))) {
156  if (!filter_text.empty()) {
157  section_node.unfold();
158  }
159  topics_added = true;
160  } else {
161  find_widget<tree_view>("topic_tree").remove_node(&section_node);
162  }
163  }
164 
165  for(const help::topic& topic : parent_section.topics) {
166  if (topic.id[0] == '.') {
167  continue;
168  }
169 
170  if ((match(topic.id) || match(topic.title)) && (topic.id.compare(0, 2, "..") != 0)) {
171  add_topic(topic.id, topic.title, false, parent_node);
172  topics_added = true;
173  }
174  }
175 
176  return topics_added;
177 }
178 
179 tree_view_node& help_browser::add_topic(const std::string& topic_id, const std::string& topic_title,
180  bool expands, tree_view_node& parent)
181 {
183  widget_item item;
184 
185  item["label"] = topic_title;
186  data.emplace("topic_name", item);
187 
188  tree_view_node& new_node = parent.add_child(expands ? "section" : "topic", data);
189  new_node.set_id(std::string(expands ? "+" : "-") + topic_id);
190 
191  return new_node;
192 }
193 
194 void help_browser::show_topic(std::string topic_id, bool add_to_history, bool reshow)
195 {
196  if(reshow) {
197  const help::topic* topic = help::find_topic(toplevel_, topic_id);
198  find_widget<rich_label>("topic_text").set_dom(topic->text.parsed_text());
200  return;
201  }
202 
203  if(topic_id.empty() || topic_id == current_topic_) {
204  return;
205  } else {
206  current_topic_ = topic_id;
207  }
208 
209  if(topic_id[0] == '+') {
210  topic_id.replace(topic_id.begin(), topic_id.begin() + 1, 2, '.');
211  }
212 
213  if(topic_id[0] == '-') {
214  topic_id.erase(topic_id.begin(), topic_id.begin() + 1);
215  }
216 
217  auto iter = parsed_pages_.find(topic_id);
218  if(iter == parsed_pages_.end()) {
219  const help::topic* topic = help::find_topic(toplevel_, topic_id);
220  if(!topic) {
221  ERR_HP << "Help browser tried to show topic with id '" << topic_id
222  << "' but that topic could not be found." << std::endl;
223  return;
224  }
225 
226  DBG_HP << "Showing topic: " << topic->id << ": " << topic->title;
227 
228  std::string topic_id_temp = topic->id;
229  if(topic_id_temp.compare(0, 2, "..") == 0) {
230  topic_id_temp.replace(0, 2, "+");
231  } else {
232  topic_id_temp.insert(0, "-");
233  }
234  tree_view& topic_tree = find_widget<tree_view>("topic_tree");
235  tree_view_node* selected_node = topic_tree.find_widget<tree_view_node>(topic_id_temp, false, false);
236  if(selected_node) {
237  selected_node->select_node(true, false);
238  }
239 
240  find_widget<label>("topic_title").set_label(topic->title);
241  try {
242  find_widget<rich_label>("topic_text").set_dom(topic->text.parsed_text());
243  } catch(const markup::parse_error& e) {
244  find_widget<rich_label>("topic_text").set_label(
246  "Error parsing markup in help page with ID: " + topic->id + "\n"
247  + font::escape_text(e.message)));
248  }
249 
251  }
252 
253  if(add_to_history) {
254  // history pos is 0 initially, so it's already at first entry
255  // no need to increment first time
256  if (!history_.empty()) {
257  // don't add duplicate entries back-to-back
258  if (history_.back() == topic_id) {
259  return;
260  }
261  history_pos_++;
262  }
263  history_.push_back(topic_id);
264 
265  find_widget<button>("back").set_active(history_pos_ != 0);
266  }
267 }
268 
270 {
271  tree_view& topic_tree = find_widget<tree_view>("topic_tree");
272 
273  if(topic_tree.empty()) {
274  return;
275  }
276 
277  tree_view_node* selected = topic_tree.selected_item();
278  assert(selected);
279 
280  show_topic(selected->id());
281 }
282 
284 {
285  if(backwards) {
286  if (history_pos_ > 0) {
287  history_pos_--;
288  } else {
289  return;
290  }
291  } else {
292  if (history_pos_ < history_.size() - 1) {
293  history_pos_++;
294  } else {
295  return;
296  }
297  }
298  find_widget<button>("back").set_active(!history_.empty() && history_pos_ != 0);
299  find_widget<button>("next").set_active(!history_.empty() && history_pos_ != (history_.size()-1));
300 
301  show_topic(history_.at(history_pos_), false);
302 }
303 
304 } // 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:365
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
void init_help()
Definition: help.cpp:145
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:1267
const std::string default_show_topic
Definition: help_impl.cpp:82
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:148
section_list sections
Definition: help_impl.hpp:168
std::string id
Definition: help_impl.hpp:166
std::string title
Definition: help_impl.hpp:166
topic_list topics
Definition: help_impl.hpp:167
A topic contains a title, an id and some text.
Definition: help_impl.hpp:115
std::string id
Definition: help_impl.hpp:139
topic_text text
Definition: help_impl.hpp:140
std::string title
Definition: help_impl.hpp:139
Thrown when the help system fails to parse something.
Definition: markup.hpp:213
#define e