The Battle for Wesnoth  1.19.10+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"
25 #include "gui/widgets/text_box.hpp"
29 #include "gui/widgets/window.hpp"
30 #include "help/help.hpp"
32 #include "utils/ci_searcher.hpp"
33 
34 static lg::log_domain log_help("help");
35 #define ERR_HP LOG_STREAM(err, log_help)
36 #define WRN_HP LOG_STREAM(warn, log_help)
37 #define DBG_HP LOG_STREAM(debug, log_help)
38 
39 namespace gui2::dialogs
40 {
41 
42 namespace
43 {
44  int win_w = 0;
45  int tree_w = 0;
46 }
47 
48 REGISTER_DIALOG(help_browser)
49 
50 help_browser::help_browser(const help::section& toplevel, const std::string& initial)
51  : modal_dialog(window_id())
52  , initial_topic_(initial.empty() ? help::default_show_topic : initial)
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 
74  next_button.set_active(false);
75  back_button.set_active(false);
76  connect_signal_mouse_left_click(back_button, std::bind(&help_browser::on_history_navigate, this, true));
77  connect_signal_mouse_left_click(next_button, std::bind(&help_browser::on_history_navigate, this, false));
78 
79  toggle_button& contents = find_widget<toggle_button>("contents");
80 
81  contents.set_value(true);
82  topic_tree.set_visible(true);
83  connect_signal_mouse_left_click(contents, [&](auto&&...) {
84  auto parent = topic_text.get_window();
85  // Cache the initial values, get_best_size() keeps changing
86  if ((parent != nullptr) && (win_w == 0)) {
87  win_w = parent->get_best_size().x;
88  }
89  if (tree_w == 0) {
90  tree_w = topic_tree.get_best_size().x;
91  }
92 
93  // Set RL's width and reshow
94  bool is_contents_visible = (topic_tree.get_visible() == widget::visibility::visible);
95  if (topic_text.get_window()) {
96  topic_text.set_width(win_w - (is_contents_visible ? 0 : tree_w) - 20 /* Padding */);
97  show_topic(history_.at(history_pos_), false);
98  }
99  topic_tree.set_visible(!is_contents_visible);
101  });
102 
103  text_box& filter = find_widget<text_box>("filter_box");
105  filter.on_modified([this](const auto& box) { update_list(box.text()); });
106 
107  topic_text.register_link_callback(std::bind(&help_browser::show_topic, this, std::placeholders::_1, true));
108 
109  connect_signal_notify_modified(topic_tree, std::bind(&help_browser::on_topic_select, this));
110 
111  keyboard_capture(&topic_tree);
112 
114 
115  tree_view_node& initial_node = topic_tree.find_widget<tree_view_node>(initial_topic_);
116  initial_node.select_node(true);
117 
118  on_topic_select();
119 }
120 
121 void help_browser::update_list(const std::string& filter_text) {
122  tree_view& topic_tree = find_widget<tree_view>("topic_tree");
123  topic_tree.clear();
124  if(!add_topics_for_section(toplevel_, topic_tree.get_root_node(), filter_text)) {
125  // Add everything if nothing matches
127  }
128 }
129 
130 bool help_browser::add_topics_for_section(const help::section& parent_section, tree_view_node& parent_node, const std::string& filter_text)
131 {
132  bool topics_added = false;
133  const auto match = translation::make_ci_matcher(filter_text);
134 
135  for(const help::section& section : parent_section.sections) {
136  tree_view_node& section_node = add_topic(section.id, section.title, true, parent_node);
137  bool subtopics_added = add_topics_for_section(section, section_node, filter_text);
138 
139  if (subtopics_added || (match(section.id) || match(section.title))) {
140  if (!filter_text.empty()) {
141  section_node.unfold();
142  }
143  topics_added = true;
144  } else {
145  find_widget<tree_view>("topic_tree").remove_node(&section_node);
146  }
147  }
148 
149  for(const help::topic& topic : parent_section.topics) {
150  if ((match(topic.id) || match(topic.title)) && (topic.id.compare(0, 2, "..") != 0)) {
151  add_topic(topic.id, topic.title, false, parent_node);
152  topics_added = true;
153  }
154  }
155 
156  return topics_added;
157 }
158 
159 tree_view_node& help_browser::add_topic(const std::string& topic_id, const std::string& topic_title,
160  bool expands, tree_view_node& parent)
161 {
163  widget_item item;
164 
165  item["label"] = topic_title;
166  data.emplace("topic_name", item);
167 
168  tree_view_node& new_node = parent.add_child(expands ? "section" : "topic", data);
169  new_node.set_id(std::string(expands ? "+" : "-") + topic_id);
170 
171  return new_node;
172 }
173 
174 void help_browser::show_topic(std::string topic_id, bool add_to_history)
175 {
176  if(topic_id.empty()) {
177  return;
178  }
179 
180  if(topic_id[0] == '+') {
181  topic_id.replace(topic_id.begin(), topic_id.begin() + 1, 2, '.');
182  }
183 
184  if(topic_id[0] == '-') {
185  topic_id.erase(topic_id.begin(), topic_id.begin() + 1);
186  }
187 
188  auto iter = parsed_pages_.find(topic_id);
189  if(iter == parsed_pages_.end()) {
190  const help::topic* topic = help::find_topic(toplevel_, topic_id);
191  if(!topic) {
192  ERR_HP << "Help browser tried to show topic with id '" << topic_id
193  << "' but that topic could not be found." << std::endl;
194  return;
195  }
196 
197  DBG_HP << "Showing topic: " << topic->id << ": " << topic->title;
198 
200  widget_item item;
201 
202  item["label"] = topic->title;
203  data.emplace("topic_title", item);
204 
205  find_widget<label>("topic_title").set_label(topic->title);
206  find_widget<rich_label>("topic_text").set_dom(topic->text.parsed_text());
207 
209  scrollbar_panel& scroll = find_widget<scrollbar_panel>("topic_scroll_panel");
211  }
212 
213  if (add_to_history) {
214  // history pos is 0 initially, so it's already at first entry
215  // no need to increment first time
216  if (!history_.empty()) {
217  // don't add duplicate entries back-to-back
218  if (history_.back() == topic_id) {
219  return;
220  }
221  history_pos_++;
222  }
223  history_.push_back(topic_id);
224 
225  find_widget<button>("back").set_active(history_pos_ != 0);
226  }
227 }
228 
230 {
231  tree_view& topic_tree = find_widget<tree_view>("topic_tree");
232 
233  if(topic_tree.empty()) {
234  return;
235  }
236 
237  tree_view_node* selected = topic_tree.selected_item();
238  assert(selected);
239 
240  show_topic(selected->id());
241 }
242 
244 {
245  if(backwards) {
246  history_pos_--;
247  } else {
248  history_pos_++;
249  }
250  find_widget<button>("back").set_active(!history_.empty() && history_pos_ != 0);
251  find_widget<button>("next").set_active(!history_.empty() && history_pos_ != (history_.size()-1));
252 
253  show_topic(history_.at(history_pos_), false);
254 }
255 
256 } // 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="")
void show_topic(std::string topic_id, bool add_to_history=true)
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)
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:835
@ BEGIN
Go to begin position.
Definition: scrollbar.hpp:53
void scroll_vertical_scrollbar(const scrollbar_base::scroll_mode scroll)
Scrolls the vertical scrollbar.
A widget that allows the user to input text in single line.
Definition: text_box.hpp:125
virtual void set_value(unsigned selected, bool fire_event=false) override
Inherited from selectable_item.
void select_node(bool expand_parents=false)
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:99
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:1201
void invalidate_layout()
Updates the size of the window.
Definition: window.cpp:760
void add_to_keyboard_chain(widget *widget)
Adds the widget to the keyboard chain.
Definition: window.cpp:1207
const config & parsed_text() const
Definition: help_impl.cpp:380
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")
std::string selected
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:178
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:1293
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:178
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