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 "gui/widgets/button.hpp"
21 #include "gui/widgets/label.hpp"
24 #include "gui/widgets/text_box.hpp"
28 #include "gui/widgets/window.hpp"
29 #include "help/help.hpp"
31 #include "utils/ci_searcher.hpp"
32 
33 static lg::log_domain log_help("help");
34 #define ERR_HP LOG_STREAM(err, log_help)
35 #define WRN_HP LOG_STREAM(warn, log_help)
36 #define DBG_HP LOG_STREAM(debug, log_help)
37 
38 namespace gui2::dialogs
39 {
40 
41 namespace
42 {
43  int win_w = 0;
44  int rl_init_w = 0;
45 }
46 
47 REGISTER_DIALOG(help_browser)
48 
49 help_browser::help_browser(const help::section& toplevel, const std::string& initial)
50  : modal_dialog(window_id())
51  , initial_topic_(initial.empty() ? help::default_show_topic : initial)
52  , current_topic_()
53  , toplevel_(toplevel)
54  , history_()
55  , history_pos_(0)
56 {
57  if(initial_topic_.compare(0, 2, "..") == 0) {
58  initial_topic_.replace(0, 2, "+");
59  } else {
60  initial_topic_.insert(0, "-");
61  }
63 }
64 
66 {
67  tree_view& topic_tree = find_widget<tree_view>("topic_tree");
68 
69  button& back_button = find_widget<button>("back");
70  button& next_button = find_widget<button>("next");
71 
72  rich_label& topic_text = find_widget<rich_label>("topic_text");
73  panel& topic_panel = find_widget<panel>("topic_panel");
74 
75  next_button.set_active(false);
76  back_button.set_active(false);
77  connect_signal_mouse_left_click(back_button, std::bind(&help_browser::on_history_navigate, this, true));
78  connect_signal_mouse_left_click(next_button, std::bind(&help_browser::on_history_navigate, this, false));
79 
80  connect_signal<event::BACK_BUTTON_CLICK>([this](auto&&...) {
81  on_history_navigate(true);
83  connect_signal<event::FORWARD_BUTTON_CLICK>([this](auto&&...) {
84  on_history_navigate(false);
86 
87  toggle_button& contents = find_widget<toggle_button>("contents");
88 
89  contents.set_value(true);
90  topic_panel.set_visible(true);
91  connect_signal_mouse_left_click(contents, [&](auto&&...) {
92  auto parent = topic_panel.get_window();
93  // Cache the initial values, get_best_size() keeps changing
94  // initial width of the window
95  if ((parent != nullptr) && (win_w == 0)) {
96  win_w = parent->get_best_size().x;
97  }
98 
99  // initial width of the rich label
100  if(rl_init_w == 0) {
101  rl_init_w = topic_text.get_width();
102  PLAIN_LOG << "RL init width: " << rl_init_w;
103  }
104 
105  // Set RL's width and reshow
106  bool is_contents_visible = (topic_panel.get_visible() == widget::visibility::visible);
107  if (parent) {
108  topic_text.set_width(is_contents_visible ? win_w : rl_init_w);
109  show_topic(history_.at(history_pos_), false, true);
110  }
111  topic_panel.set_visible(!is_contents_visible);
113  });
114 
115  text_box& filter = find_widget<text_box>("filter_box");
117  filter.on_modified([this](const auto& box) { update_list(box.text()); });
118 
119  topic_text.register_link_callback(std::bind(&help_browser::show_topic, this, std::placeholders::_1, true, false));
120 
121  connect_signal_notify_modified(topic_tree, std::bind(&help_browser::on_topic_select, this));
122 
123  keyboard_capture(&topic_tree);
124 
126 
127  tree_view_node* initial_node = topic_tree.find_widget<tree_view_node>(initial_topic_, false, false);
128  if(initial_node) {
129  initial_node->select_node(true);
130  } else {
131  ERR_HP << "Help browser tried to show topic with id '" << initial_topic_
132  << "' but that topic could not be found." << std::endl;
133  }
134 
135  on_topic_select();
136 }
137 
138 void help_browser::update_list(const std::string& filter_text) {
139  tree_view& topic_tree = find_widget<tree_view>("topic_tree");
140  topic_tree.clear();
141  if(!add_topics_for_section(toplevel_, topic_tree.get_root_node(), filter_text)) {
142  // Add everything if nothing matches
144  }
145 }
146 
147 bool help_browser::add_topics_for_section(const help::section& parent_section, tree_view_node& parent_node, const std::string& filter_text)
148 {
149  bool topics_added = false;
150  const auto match = translation::make_ci_matcher(filter_text);
151 
152  for(const help::section& section : parent_section.sections) {
153  tree_view_node& section_node = add_topic(section.id, section.title, true, parent_node);
154  bool subtopics_added = add_topics_for_section(section, section_node, filter_text);
155 
156  if (subtopics_added || (match(section.id) || match(section.title))) {
157  if (!filter_text.empty()) {
158  section_node.unfold();
159  }
160  topics_added = true;
161  } else {
162  find_widget<tree_view>("topic_tree").remove_node(&section_node);
163  }
164  }
165 
166  for(const help::topic& topic : parent_section.topics) {
167  if (topic.id[0] == '.') {
168  continue;
169  }
170 
171  if ((match(topic.id) || match(topic.title)) && (topic.id.compare(0, 2, "..") != 0)) {
172  add_topic(topic.id, topic.title, false, parent_node);
173  topics_added = true;
174  }
175  }
176 
177  return topics_added;
178 }
179 
180 tree_view_node& help_browser::add_topic(const std::string& topic_id, const std::string& topic_title,
181  bool expands, tree_view_node& parent)
182 {
184  widget_item item;
185 
186  item["label"] = topic_title;
187  data.emplace("topic_name", item);
188 
189  tree_view_node& new_node = parent.add_child(expands ? "section" : "topic", data);
190  new_node.set_id(std::string(expands ? "+" : "-") + topic_id);
191 
192  return new_node;
193 }
194 
195 void help_browser::show_topic(std::string topic_id, bool add_to_history, bool reshow)
196 {
197  if(reshow) {
198  const help::topic* topic = help::find_topic(toplevel_, topic_id);
199  find_widget<rich_label>("topic_text").set_dom(topic->text.parsed_text());
201  return;
202  }
203 
204  if(topic_id.empty() || topic_id == current_topic_) {
205  return;
206  } else {
207  current_topic_ = topic_id;
208  }
209 
210  if(topic_id[0] == '+') {
211  topic_id.replace(topic_id.begin(), topic_id.begin() + 1, 2, '.');
212  }
213 
214  if(topic_id[0] == '-') {
215  topic_id.erase(topic_id.begin(), topic_id.begin() + 1);
216  }
217 
218  auto iter = parsed_pages_.find(topic_id);
219  if(iter == parsed_pages_.end()) {
220  const help::topic* topic = help::find_topic(toplevel_, topic_id);
221  if(!topic) {
222  ERR_HP << "Help browser tried to show topic with id '" << topic_id
223  << "' but that topic could not be found." << std::endl;
224  return;
225  }
226 
227  DBG_HP << "Showing topic: " << topic->id << ": " << topic->title;
228 
229  std::string topic_id_temp = topic->id;
230  if(topic_id_temp.compare(0, 2, "..") == 0) {
231  topic_id_temp.replace(0, 2, "+");
232  } else {
233  topic_id_temp.insert(0, "-");
234  }
235  tree_view& topic_tree = find_widget<tree_view>("topic_tree");
236  tree_view_node& selected_node = topic_tree.find_widget<tree_view_node>(topic_id_temp);
237  selected_node.select_node(true, false);
238 
239  find_widget<label>("topic_title").set_label(topic->title);
240  find_widget<rich_label>("topic_text").set_dom(topic->text.parsed_text());
241 
243  }
244 
245  if(add_to_history) {
246  // history pos is 0 initially, so it's already at first entry
247  // no need to increment first time
248  if (!history_.empty()) {
249  // don't add duplicate entries back-to-back
250  if (history_.back() == topic_id) {
251  return;
252  }
253  history_pos_++;
254  }
255  history_.push_back(topic_id);
256 
257  find_widget<button>("back").set_active(history_pos_ != 0);
258  }
259 }
260 
262 {
263  tree_view& topic_tree = find_widget<tree_view>("topic_tree");
264 
265  if(topic_tree.empty()) {
266  return;
267  }
268 
269  tree_view_node* selected = topic_tree.selected_item();
270  assert(selected);
271 
272  show_topic(selected->id());
273 }
274 
276 {
277  if(backwards) {
278  if (history_pos_ > 0) {
279  history_pos_--;
280  } else {
281  return;
282  }
283  } else {
284  if (history_pos_ < history_.size() - 1) {
285  history_pos_++;
286  } else {
287  return;
288  }
289  }
290  find_widget<button>("back").set_active(!history_.empty() && history_pos_ != 0);
291  find_widget<button>("next").set_active(!history_.empty() && history_pos_ != (history_.size()-1));
292 
293  show_topic(history_.at(history_pos_), false);
294 }
295 
296 } // 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")
#define PLAIN_LOG
Definition: log.hpp:296
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
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