The Battle for Wesnoth  1.19.5+dev
tooltips.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2024
3  by David White <dave@whitevine.net>
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 #include "tooltips.hpp"
17 
18 #include "draw_manager.hpp"
19 #include "floating_label.hpp"
20 #include "font/standard_colors.hpp"
21 #include "game_display.hpp"
22 #include "help/help.hpp"
23 #include "log.hpp"
24 #include "video.hpp"
25 
26 #include <SDL2/SDL_rect.h>
27 
28 static lg::log_domain log_font("font");
29 #define DBG_FT LOG_STREAM(debug, log_font)
30 #define LOG_FT LOG_STREAM(info, log_font)
31 
32 namespace {
33 
34 static const int font_size = font::SIZE_SMALL;
35 static const int text_width = 400;
36 static const double height_fudge = 0.95; // An artificial "border" to keep tip text from crowding lower edge of viewing area
37 
38 struct tooltip
39 {
40  tooltip(const SDL_Rect& r, const std::string& msg, const std::string& act = "");
41  rect origin;
42  rect loc = {};
43  std::string message;
44  std::string action;
46 
47  void init_label();
48  void update_label_pos();
49 };
50 
51 tooltip::tooltip(const SDL_Rect& r, const std::string& msg, const std::string& act)
52  : origin(r), message(msg), action(act), label(msg)
53 {
54  init_label();
55  DBG_FT << "created tooltip for " << origin << " at " << loc;
56 }
57 
58 void tooltip::init_label()
59 {
60  const color_t bgcolor {0,0,0,192};
62  unsigned int border = 10;
63 
64  rect huge;
65  huge.h=1000000;
66  huge.w=1000000;
67 
68  label.set_font_size(font_size);
69  label.set_color(font::NORMAL_COLOR);
70  label.set_clip_rect(huge);
71  label.set_width(text_width); // If tooltip will be too tall for game_canvas, this could be scaled up appropriately
72  label.set_alignment(font::LEFT_ALIGN);
73  label.set_bg_color(bgcolor);
74  label.set_border_size(border);
75 
76  label.create_texture();
77 
78  point lsize = label.get_draw_size();
79  int new_text_width = text_width * static_cast<float>(lsize.y)/game_canvas.h; // If necessary, scale width to reduce height while preserving area of label
80  while((lsize.y > game_canvas.h*height_fudge) && (lsize.x < game_canvas.w)) {
81  // Scaling the tip to reduce height is hard, since making a texture wider is no guarantee that there will be fewer lines of text:
82  //
83  // This block of text is just
84  // as tall as the other one.
85  //
86  // This block of text is just as tall as the other
87  // one.
88  //
89  // Creating this over and over may not be the most efficient route, but it will work and will be quite rare (tip taller than screen).
90  bool wont_fit = false;
91  if(new_text_width>game_canvas.w) {
92  new_text_width=game_canvas.w;
93  wont_fit = true;
94  }
95  DBG_FT << "lsize.x,y = " << lsize.x << "," << lsize.y << ", new_text_width = " << new_text_width;
96 
97  label.set_width(new_text_width);
98  label.clear_texture();
99  label.create_texture();
100 
101  lsize = label.get_draw_size();
102  DBG_FT << "new label lsize.x,y = " << lsize.x << "," << lsize.y;
103  if(wont_fit) {
104  break;
105  }
106  new_text_width *= 1.3;
107  }
108  // I don't know if it's strictly necessary to create the texture yet again just to make sure the clip_rect is set to game_canvas
109  // but it seems like the safe course of action.
110  label.set_clip_rect(game_canvas);
111  label.clear_texture();
112  label.create_texture();
113 
114  update_label_pos();
115 }
116 
117 void tooltip::update_label_pos()
118 {
120 
121  point lsize = label.get_draw_size();
122  loc = {0, 0, lsize.x, lsize.y};
123 
124  DBG_FT << "\nupdate_label_pos() Start: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
125 
126  if(origin.y > loc.h) {
127  // There is enough room to fit it above the tip area
128  loc.y = origin.y - loc.h;
129  DBG_FT << "\tAbove: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
130  } else if((origin.y + origin.h + loc.h) <= game_canvas.h*height_fudge) {
131  // There is enough room to fit it below the tip area
132  loc.y = origin.y + origin.h;
133  DBG_FT << "\tBelow: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
134  } else if(((origin.y + origin.h/2 - loc.h/2) >= 0) &&
135  ((origin.y + origin.h/2 + loc.h/2) <= game_canvas.h*height_fudge)) {
136  // There is enough room to center it at the tip area
137  loc.y = origin.y + origin.h/2 - loc.h/2;
138  DBG_FT << "\tCenter: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
139  } else if(loc.h <= game_canvas.h*0.95) {
140  // There is enough room to center it
141  loc.y = game_canvas.h/2 - loc.h/2;
142  DBG_FT << "\tScreen Center: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
143  } else {
144  // It doesn't fit
145  loc.y = 0;
146  DBG_FT << "\tToo big: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
147  }
148 
149  DBG_FT << "\tBefore x adjust: loc.x,y,w,h = " << loc.x << "," << loc.y << "," << loc.w << "," << loc.h << " origin = " << origin.x << "," << origin.y;
150  // Try to keep it within the screen
151  loc.x = origin.x;
152  if(loc.x + loc.w > game_canvas.w) {
153  loc.x = game_canvas.w - loc.w;
154  }
155  if(loc.x < 0) {
156  loc.x = 0;
157  }
158 
159  DBG_FT << "\tFinal: loc.x,y,w,h = " << loc.x << "," << loc.y << "," << loc.w << "," << loc.h << " origin = " << origin.x << "," << origin.y;
160  label.set_position(loc.x, loc.y);
161 }
162 
163 
164 std::map<int, tooltip> tips;
165 int active_tooltip = 0;
166 
167 int tooltip_id = 1;
168 
169 surface current_background = nullptr;
170 
171 // Is this a freaking singleton or is it not?
172 // This is horrible, but that's how the usage elsewhere is.
173 // If you want to fix this, either make it an actual singleton,
174 // or ensure that tooltips:: functions are called on an instance.
175 tooltips::manager* current_manager = nullptr;
176 
177 } // anon namespace
178 
179 /** Clear/hide the active tooltip. */
180 static void clear_active()
181 {
182  if(!active_tooltip) {
183  return;
184  }
185  DBG_FT << "clearing active tooltip " << active_tooltip;
186  tips.at(active_tooltip).label.undraw();
187  active_tooltip = 0;
188 }
189 
190 namespace tooltips
191 {
192 
194 {
195  clear_tooltips();
196  current_manager = this;
197 }
198 
200 {
201  try {
202  clear_tooltips();
203  } catch (...) {}
204  current_manager = nullptr;
205 }
206 
208 {
209  if(!active_tooltip) {
210  return;
211  }
212  // Update the active tooltip's draw state.
213  // This will trigger redraws if necessary.
214  tips.at(active_tooltip).label.update(std::chrono::steady_clock::now());
215 }
216 
217 bool manager::expose(const rect& region)
218 {
219  // Only the active tip is shown.
220  if(!active_tooltip) {
221  return false;
222  }
223  tooltip& tip = tips.at(active_tooltip);
224  if(!tip.loc.overlaps(region)) {
225  return false;
226  }
227  tip.label.draw();
228  return true;
229 }
230 
232 {
233  // Only the active tip, if any, should be visible.
234  if(!active_tooltip) {
235  return {};
236  } else {
237  return tips.at(active_tooltip).loc;
238  }
239 }
240 
242 {
243  LOG_FT << "clearing all tooltips";
244  clear_active();
245  tips.clear();
246 }
247 
248 void clear_tooltips(const SDL_Rect& r)
249 {
250  for(auto i = tips.begin(); i != tips.end(); ) {
251  if(i->second.origin.overlaps(r)) {
252  DBG_FT << "clearing tip " << i->first << " at "
253  << i->second.origin << " overlapping " << r;
254 
255  if (i->first == active_tooltip) {
256  i->second.label.undraw();
257  active_tooltip = 0;
258  }
259 
260  i = tips.erase(i);
261  } else {
262  ++i;
263  }
264  }
265 }
266 
267 bool update_tooltip(int id, const SDL_Rect& origin, const std::string& message)
268 {
270  if (it == tips.end() ) return false;
271  tooltip& tip = it->second;
272  if(tip.message == message && tip.origin == origin) {
273  return false;
274  }
275  if(tip.message != message) {
276  LOG_FT << "updating tooltip " << id << " message";
277  tip.message = message;
278  tip.label = font::floating_label(message);
279  tip.init_label();
280  }
281  if(tip.origin != origin) {
282  DBG_FT << "updating tooltip " << id << " origin " << origin;
283  tip.origin = origin;
284  tip.update_label_pos();
285  }
286  return true;
287 }
288 
289 void remove_tooltip(int id)
290 {
291  if(!id) { return; }
292  DBG_FT << "removing tooltip " << id;
293  if(id == active_tooltip) {
294  clear_active();
295  }
296  tips.erase(id);
297 }
298 
299 int add_tooltip(const SDL_Rect& origin, const std::string& message, const std::string& action)
300 {
301  // Because some other things are braindead, we have to check we're not
302  // just adding the same tooltip over and over every time the mouse moves.
303  for(auto& [id, tip] : tips) {
304  if(tip.origin == origin && tip.message == message && tip.action == action) {
305  return id;
306  }
307  }
308  DBG_FT << "adding tooltip for " << origin;
309 
310  // Clear any existing tooltips for this origin
311  clear_tooltips(origin);
312  // Create and add a new tooltip
313  int id = tooltip_id++;
314  tips.emplace(id, tooltip(origin, message, action));
315  return id;
316 }
317 
318 static void raise_to_top()
319 {
320  // Raise the current manager so it will display on top of everything.
321  if(!current_manager) {
322  throw game::error("trying to show tooltip with no tooltip manager");
323  }
324  draw_manager::raise_drawable(current_manager);
325 }
326 
327 static void select_active(int id)
328 {
329  if(active_tooltip == id) {
330  return;
331  }
332  tooltip& tip = tips.at(id);
333  LOG_FT << "showing tip " << id << " for " << tip.origin;
334  clear_active();
335  active_tooltip = id;
336  tip.label.update(std::chrono::steady_clock::now());
337  raise_to_top();
338 }
339 
340 void process(int mousex, int mousey)
341 {
342  point mouseloc{mousex, mousey};
343  for(auto& [id, tip] : tips) {
344  if(tip.origin.contains(mouseloc)) {
345  select_active(id);
346  return;
347  }
348  }
349 
350  if(active_tooltip) {
351  LOG_FT << "clearing tooltip because none hovered";
352  clear_active();
353  }
354 }
355 
356 bool click(int mousex, int mousey)
357 {
358  for(auto& [id, tip] : tips) { (void)id;
359  if(!tip.action.empty() && tip.origin.contains(mousex, mousey)) {
360  help::show_help(tip.action);
361  return true;
362  }
363  }
364  return false;
365 }
366 
367 } // namespace tooltips
virtual bool expose(const rect &region) override
Draw the portion of the drawable intersecting region to the screen.
Definition: tooltips.cpp:217
virtual void layout() override
Finalize the size and position of the drawable and its children, and invalidate any regions requiring...
Definition: tooltips.cpp:207
virtual rect screen_location() override
The location of the TLD on the screen, in drawing coordinates.
Definition: tooltips.cpp:231
@ border
The border of the map.
std::size_t i
Definition: function.cpp:1028
std::string label
What to show in the filter's drop-down list.
Definition: manager.cpp:200
std::string tooltip
Shown when hovering over an entry in the filter's drop-down list.
Definition: manager.cpp:202
std::string id
Text to match against addon_info.tags()
Definition: manager.cpp:198
Standard logging facilities (interface).
void raise_drawable(top_level_drawable *tld)
Raise a TLD to the top of the drawing stack.
const int SIZE_SMALL
Definition: constants.cpp:24
const color_t NORMAL_COLOR
static std::unique_ptr< tooltip > tip
Definition: tooltip.cpp:62
std::vector< game_tip > tips
Definition: settings.cpp:55
void show_help(const std::string &show_topic)
Open the help browser, show topic with id show_topic.
Definition: help.cpp:140
tooltips.
Definition: tooltips.cpp:191
bool click(int mousex, int mousey)
Definition: tooltips.cpp:356
int add_tooltip(const SDL_Rect &origin, const std::string &message, const std::string &action)
Definition: tooltips.cpp:299
void clear_tooltips()
Definition: tooltips.cpp:241
void remove_tooltip(int id)
Definition: tooltips.cpp:289
static void select_active(int id)
Definition: tooltips.cpp:327
void process(int mousex, int mousey)
Definition: tooltips.cpp:340
static void raise_to_top()
Definition: tooltips.cpp:318
bool update_tooltip(int id, const SDL_Rect &origin, const std::string &message)
Definition: tooltips.cpp:267
rect game_canvas()
The game canvas area, in drawing coordinates.
Definition: video.cpp:427
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:109
The basic class for representing 8-bit RGB or RGBA colour values.
Definition: color.hpp:59
Base class for all the errors encountered by the engine.
Definition: exceptions.hpp:29
Holds a 2D point.
Definition: point.hpp:25
An abstract description of a rectangle with integer coordinates.
Definition: rect.hpp:47
#define DBG_FT
Definition: tooltips.cpp:29
static void clear_active()
Clear/hide the active tooltip.
Definition: tooltips.cpp:180
#define LOG_FT
Definition: tooltips.cpp:30
static lg::log_domain log_font("font")