Freeciv21
Develop your civilization from humble roots to a global empire
multi_slider.cpp
Go to the documentation of this file.
1 // SPDX-License-Identifier: GPLv3-or-later
2 // SPDX-FileCopyrightText: Louis Moureaux <m_louis30@yahoo.com>
3 
4 #include "widgets/multi_slider.h"
5 
6 #include "log.h"
7 
8 #include <QFocusEvent>
9 #include <QKeyEvent>
10 #include <QMouseEvent>
11 #include <QPainter>
12 
13 #include <algorithm>
14 #include <cmath>
15 
16 namespace {
18 namespace colors {
20 const QColor focus_indicator = Qt::gray;
22 const QColor handle_background = Qt::lightGray;
24 const QColor handle_indicator = Qt::gray;
26 const QColor handle_hover = Qt::darkGray;
28 const QColor handle_dragged = handle_hover;
29 } // namespace colors
31 namespace metrics {
33 const double focus_bar_gap = 3;
35 const double focus_bar_height = 1;
37 const double handle_bar_gap = 2;
39 const double handle_bar_width = 4;
41 const double handle_gap = 1;
43 const double handle_radius = 8;
45 const double handle_indicator_radius = 4;
47 const double handle_active_indicator_radius = handle_indicator_radius + 1;
49 const double extra_height = std::max(focus_bar_gap + focus_bar_height,
50  handle_gap + handle_radius * 2);
51 } // namespace metrics
52 } // anonymous namespace
53 
54 namespace freeciv {
55 
106 multi_slider::multi_slider(QWidget *parent) : QWidget(parent)
107 {
108  setFocusPolicy(Qt::StrongFocus);
109  setMouseTracking(true);
110  setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed,
111  QSizePolicy::Slider));
112 }
113 
120 std::size_t multi_slider::add_category(const QPixmap &icon)
121 {
122  m_categories.push_back({icon});
123  m_values.push_back(0);
124 
125  if (icon.size() != m_categories.front().icon.size()) {
126  qWarning() << "Inconsistent icon sizes:" << icon.size() << "and"
127  << m_categories.front().icon.size();
128  }
129 
130  return m_categories.size() - 1;
131 }
132 
142 void multi_slider::set_range(std::size_t category, int min, int max)
143 {
145  fc_assert_ret(min >= 0);
146  fc_assert_ret(min <= max);
147 
148  m_categories[category].minimum = min;
149  m_categories[category].maximum = max;
150 
152  updateGeometry();
153 }
154 
161 void multi_slider::set_values(const std::vector<int> &values)
162 {
163  fc_assert_ret(values.size() == m_categories.size());
164 
165  m_values = values;
167  updateGeometry();
168 
169  emit values_changed(values);
170 }
171 
175 std::size_t multi_slider::total() const
176 {
177  return std::accumulate(m_values.begin(), m_values.end(), 0);
178 }
179 
187 {
188  if (m_categories.empty()) {
189  return QSize();
190  }
191 
192  auto icon_size = m_categories.front().icon.size();
193  return QSize(total() * icon_size.width() + 2 * metrics::handle_radius,
194  icon_size.height() + metrics::extra_height);
195 }
196 
204 {
205  if (m_categories.empty()) {
206  return QSize();
207  }
208 
209  auto icon_size = m_categories.front().icon.size();
210  return QSize(total() * 5 + 2 * metrics::handle_radius,
211  icon_size.height() + metrics::extra_height);
212 }
213 
218 {
219  // Allow using Tab and Backtab to move between visible categories
220  // We need to trap those early to override the default behaviour
221  if (event->type() == QEvent::KeyPress) {
222  auto kevt = dynamic_cast<QKeyEvent *>(event);
223  // Check if focus can be moved to the next visible category
224  if (kevt->key() == Qt::Key_Tab && move_focus(true)) {
225  event->accept();
226  return true;
227  } else if (kevt->key() == Qt::Key_Backtab && move_focus(false)) {
228  event->accept();
229  return true;
230  }
231  }
232  return QWidget::event(event);
233 }
234 
240 {
241  if (!m_categories.empty()) {
242  if (event->reason() == Qt::BacktabFocusReason) {
243  m_focused_category = m_categories.size() - 1;
244  } else if (event->reason() == Qt::TabFocusReason) {
245  m_focused_category = 0;
246  } else {
247  // Keep old category alive if still present, making sure it's >= 0
248  m_focused_category = std::max(m_focused_category, 0);
249  }
250  }
251  QWidget::focusInEvent(event);
252 }
253 
259 {
260  if (m_categories.empty()) {
261  return;
262  }
263 
264  if (event->modifiers() == Qt::NoModifier) {
265  switch (event->key()) {
266  case Qt::Key_Up:
267  if (grab_item(m_focused_category, 1)) {
268  emit values_changed(values());
269  event->accept();
270  return;
271  }
272  break;
273  case Qt::Key_Down:
274  if (grab_item(m_focused_category, -1)) {
275  emit values_changed(values());
276  event->accept();
277  return;
278  }
279  break;
280  case Qt::Key_Left:
281  if (move_focus(false)) {
282  event->accept();
283  return;
284  }
285  break;
286  case Qt::Key_Right:
287  if (move_focus(true)) {
288  event->accept();
289  return;
290  }
291  break;
292  default:
293  break;
294  }
295  }
296  QWidget::keyPressEvent(event);
297 }
298 
303 {
304  m_closest_handle = -1;
305  update();
306  QWidget::leaveEvent(event);
307 }
308 
313 {
314  if (event->buttons() == Qt::LeftButton
315  && event->modifiers() == Qt::NoModifier) {
316  // Double click
317  move_handle(handle_near(event->pos()), event->pos());
318  }
319  QWidget::mouseMoveEvent(event);
320 }
321 
326 {
327  if (m_dragged_handle >= 0 && event->buttons() == Qt::LeftButton
328  && event->modifiers() == Qt::NoModifier) {
329  // Drag
331  }
332 
333  // Update the closest handle
334  auto new_handle = handle_near(event->pos());
335  if (new_handle != m_closest_handle) {
336  m_closest_handle = new_handle;
337  update();
338  }
339 
340  QWidget::mouseMoveEvent(event);
341 }
342 
347 {
348  if (event->buttons() == Qt::LeftButton
349  && event->modifiers() == Qt::NoModifier) {
351  update();
352  }
353  QWidget::mousePressEvent(event);
354 }
355 
360 {
361  m_dragged_handle = -1;
362  update();
363  QWidget::mouseReleaseEvent(event);
364 }
365 
369 void multi_slider::paintEvent(QPaintEvent *event)
370 {
371  if (m_categories.empty() || total() <= 0) {
372  return;
373  }
374 
375  // Assume all icons have the same width
376  const auto iheight = m_categories.front().icon.height();
377 
378  // Center everything
379  QPainter p(this);
380  p.translate(m_geom.left_margin, 0);
381 
382  // Draw icons
383  p.setPen(Qt::NoPen);
384  p.setRenderHint(QPainter::Antialiasing);
385  double xmin = 0, xmax = 0;
386  for (std::size_t i = 0; i < m_values.size(); ++i) {
387  xmax += m_values[i] * m_geom.item_width;
388  p.drawTiledPixmap(QRectF(xmin, 0, xmax - xmin, iheight),
389  m_categories[i].icon, QPointF(xmin, 0));
390 
391  // Focus indicator
392  if (hasFocus() && i == m_focused_category) {
393  p.setBrush(colors::focus_indicator);
394  p.drawRect(QRectF(xmin, iheight + metrics::focus_bar_gap, xmax - xmin,
395  metrics::focus_bar_height));
396  }
397  xmin = xmax;
398  }
399 
400  // Draw handles (skipping the dummy last one)
401  auto handles = visible_handles();
402  for (auto h : handles) {
403  auto x = m_geom.item_width * h.location;
404 
405  // Background
406  p.setBrush(colors::handle_background);
407  p.drawRect(QRectF(x - metrics::handle_bar_width / 2,
408  metrics::handle_bar_gap, metrics::handle_bar_width,
409  iheight + metrics::handle_gap));
410  p.drawEllipse(QPointF(x, iheight + metrics::handle_gap
411  + metrics::handle_radius - 1),
412  metrics::handle_radius, metrics::handle_radius);
413 
414  // Active handle indicator
415  bool is_closest = h.index == m_closest_handle;
416  bool is_dragged = h.index == m_dragged_handle;
417  double inner_radius = is_dragged
418  ? metrics::handle_active_indicator_radius
419  : metrics::handle_indicator_radius;
420  p.setBrush(is_dragged ? colors::handle_dragged
421  : is_closest ? colors::handle_hover
422  : colors::handle_indicator);
423  p.drawEllipse(QPointF(x, iheight + metrics::handle_gap
424  + metrics::handle_radius - 1),
425  inner_radius, inner_radius);
426  }
427 }
428 
432 void multi_slider::resizeEvent(QResizeEvent *event)
433 {
435 }
436 
441 void multi_slider::exchange(std::size_t giver, std::size_t taker, int amount)
442 {
443  m_values[giver] -= amount;
444  m_values[taker] += amount;
446  update();
447 }
448 
458 bool multi_slider::grab_item(std::size_t taker, int amount, bool from_left,
459  bool from_right)
460 {
461  fc_assert_ret_val(taker < m_categories.size(), false);
462 
463  const auto &category = m_categories[taker];
464  if (!category.allowed(m_values[taker] + amount)) {
465  return false;
466  }
467 
468  // Find category to exchange with. First look to the right...
469  if (from_right) {
470  for (int i = taker + 1; i < m_categories.size(); ++i) {
471  if (m_categories[i].allowed(m_values[i] - amount)) {
472  exchange(i, taker, amount);
473  return true;
474  }
475  }
476  }
477 
478  // No luck to the right. Try on the other side
479  if (from_left) {
480  for (int i = taker - 1; i >= 0; --i) {
481  if (m_categories[i].allowed(m_values[i] - amount)) {
482  exchange(i, taker, amount);
483  return true;
484  }
485  }
486  }
487 
488  // No luck
489  return false;
490 }
491 
496 {
497  if (m_categories.empty() || m_values[m_focused_category] > 0) {
498  // Already good
499  return;
500  }
501 
502  // One of them will always succeed
503  if (!move_focus(true)) {
504  move_focus(false);
505  }
506 }
507 
514 bool multi_slider::move_focus(bool forward)
515 {
516  int step = forward ? 1 : -1;
517  // Check if focus can be moved to the next visible category
518  for (int i = m_focused_category + step; i >= 0 && i < m_categories.size();
519  i += step) {
520  if (m_values[i] > 0) {
521  m_focused_category = i;
522  update();
523  return true;
524  }
525  }
526  return false;
527 }
528 
532 int multi_slider::handle_near(const QPoint &where)
533 {
534  const auto handles = visible_handles();
535  const auto handle_x = [this](const handle &h) {
536  return m_geom.left_margin + h.location * m_geom.item_width;
537  };
538  const auto best_handle =
539  std::min_element(handles.begin(), handles.end(),
540  [where, handle_x](const handle &a, const handle &b) {
541  return std::abs(where.x() - handle_x(a))
542  < std::abs(where.x() - handle_x(b));
543  });
544  return best_handle->index;
545 }
546 
553 bool multi_slider::move_handle(int handle, const QPoint &where)
554 {
555  // Target location of the handle
556  int target =
557  std::round((where.x() - m_geom.left_margin) / m_geom.item_width);
558 
559  // Current location of the handle
560  int current =
561  std::accumulate(m_values.begin(), m_values.begin() + handle + 1, 0);
562 
563  // Direction in which we move the handle
564  bool moving_left = current > target;
565 
566  // Category gaining items
567  int taker = moving_left ? handle + 1 : handle;
568 
569  // Try to transfer items to the taker
570  for (int i = 0; i < std::abs(current - target); ++i) {
571  // grab_item works in units of 1 item
572  if (!grab_item(taker, 1, moving_left, !moving_left)) {
573  // Nothing more we can do to move the handle in this direction
574  return false;
575  }
576  }
577  emit values_changed(values());
578  return true;
579 }
580 
585 {
586  const auto icon_size = m_categories.front().icon.size();
587  const auto items = total();
588 
589  // Safety - we shouldn't be used this way...
590  if (items <= 0) {
591  m_geom = {1, 0, 1};
592  return;
593  }
594 
595  m_geom.icons_width = items * icon_size.width();
596  int total_width = m_geom.icons_width + 2 * metrics::handle_radius;
597 
598  // Adjust if we don't have enough space
599  if (total_width > width()) {
600  int available_width = width() - 2 * metrics::handle_radius;
601  int icon_count =
602  available_width / icon_size.width(); // Note we round down
603  m_geom.icons_width = icon_count * icon_size.width();
604  }
605 
606  m_geom.left_margin = (width() - m_geom.icons_width) / 2;
607 
608  // If we have enough space, this is equal to the width of one icon
609  m_geom.item_width = static_cast<double>(m_geom.icons_width) / items;
610 }
611 
615 std::vector<multi_slider::handle> multi_slider::visible_handles() const
616 {
617  std::vector<handle> handles;
618  bool first = true;
619  int location = 0;
620  for (int i = 0; i < m_values.size() - 1; ++i) {
621  location += m_values[i];
622  if (first || m_values[i] > 0) {
623  handles.push_back({i, location});
624  }
625  first = false;
626  }
627  return handles;
628 }
629 
630 } // namespace freeciv
std::vector< int > m_values
Number of items in each category.
Definition: multi_slider.h:83
void mouseMoveEvent(QMouseEvent *event) override
Moves the current handle when dragging the mouse.
void paintEvent(QPaintEvent *event) override
Draws the widget.
void resizeEvent(QResizeEvent *event) override
Updates cached geometry information.
int m_closest_handle
Index of the handle being dragged with the mouse.
Definition: multi_slider.h:89
void set_range(std::size_t category, int min, int max)
Sets the minimum and maximum number of items a category can have.
void focus_some_category()
Makes sure the focused category is a visible one.
std::vector< int > values() const
Retrieves the number of items in each category.
Definition: multi_slider.h:40
std::vector< category > m_categories
Category data.
Definition: multi_slider.h:79
struct freeciv::multi_slider::@154 m_geom
Cached geometry information.
void mouseDoubleClickEvent(QMouseEvent *event) override
Moves the closest handle when double-clicking.
bool move_focus(bool forward)
Moves focus to the next or previous visible category.
multi_slider(QWidget *parent=nullptr)
Constructor.
int handle_near(const QPoint &where)
Finds the index of the handle closest to the given position.
void set_values(const std::vector< int > &values)
Sets the contents of all item categories.
void mousePressEvent(QMouseEvent *event) override
Sets the current handle when pressing a mouse button.
bool event(QEvent *event) override
Overrides tab handling to also cycle through visible categories.
void leaveEvent(QEvent *event) override
Sopts highlighting the closest handle.
bool grab_item(std::size_t taker, int amount, bool from_left=true, bool from_right=true)
Grab an item from elsewhere and adds it to the taker category.
void mouseReleaseEvent(QMouseEvent *event) override
Unsets the current handle when releasing a mouse button.
void update_cached_geometry()
Updates cached geometry information.
std::size_t total() const
Returns the total number of items controlled by this widget.
void keyPressEvent(QKeyEvent *event) override
Handles arrow keys: left/right to change the focused category, up/down to add or remove items.
bool move_handle(int handle, const QPoint &where)
Tries to move a handle closer to a given position.
std::vector< handle > visible_handles() const
Returns the list of all visible handles.
int m_focused_category
Index of the category receiving keyboard input.
Definition: multi_slider.h:86
std::size_t add_category(const QPixmap &icon)
Adds a category.
void values_changed(const std::vector< int > &values) const
QSize minimumSizeHint() const override
Minimum size of the widget.
QSize sizeHint() const override
Preferred size of the widget.
void focusInEvent(QFocusEvent *event) override
Focuses the first or last category when focus is gained with the keyboard.
void exchange(std::size_t giver, std::size_t taker, int amount)
Exchange items between two categories.
enum event_type event
Definition: events.cpp:68
#define fc_assert_ret(condition)
Definition: log.h:112
#define fc_assert_ret_val(condition, val)
Definition: log.h:114
Colors.
Definition: path.cpp:10
Constants used when drawing the button.
int step
Definition: specpq.h:83
bool allowed(int value) const
Checks if the category could take some value.
Definition: multi_slider.h:21