1 // SPDX-License-Identifier: GPL-3.0-or-later
2 // SPDX-FileCopyrightText: 2017-2019 Alejandro Sirgo Rica & Contributors
3 
4 #include "buttonhandler.h"
5 #include "src/utils/globalvalues.h"
6 #include <QPoint>
7 #include <QScreen>
8 
9 // ButtonHandler is a habdler for every active button. It makes easier to
10 // manipulate the buttons as a unit.
11 
ButtonHandler(const QVector<CaptureToolButton * > & v,QObject * parent)12 ButtonHandler::ButtonHandler(const QVector<CaptureToolButton*>& v,
13                              QObject* parent)
14   : QObject(parent)
15 {
16     setButtons(v);
17     init();
18 }
19 
ButtonHandler(QObject * parent)20 ButtonHandler::ButtonHandler(QObject* parent)
21   : QObject(parent)
22 {
23     init();
24 }
25 
hide()26 void ButtonHandler::hide()
27 {
28     for (CaptureToolButton* b : m_vectorButtons)
29         b->hide();
30 }
31 
show()32 void ButtonHandler::show()
33 {
34     if (m_vectorButtons.isEmpty() || m_vectorButtons.first()->isVisible()) {
35         return;
36     }
37     for (CaptureToolButton* b : m_vectorButtons)
38         b->animatedShow();
39 }
40 
isVisible() const41 bool ButtonHandler::isVisible() const
42 {
43     bool ret = true;
44     for (const CaptureToolButton* b : m_vectorButtons) {
45         if (!b->isVisible()) {
46             ret = false;
47             break;
48         }
49     }
50     return ret;
51 }
52 
buttonsAreInside() const53 bool ButtonHandler::buttonsAreInside() const
54 {
55     return m_buttonsAreInside;
56 }
57 
size() const58 size_t ButtonHandler::size() const
59 {
60     return m_vectorButtons.size();
61 }
62 
63 // updatePosition updates the position of the buttons around the
64 // selection area. Ignores the sides blocked by the end of the screen.
65 // When the selection is too small it works on a virtual selection with
66 // the original in the center.
updatePosition(const QRect & selection)67 void ButtonHandler::updatePosition(const QRect& selection)
68 {
69     resetRegionTrack();
70     const int vecLength = m_vectorButtons.size();
71     if (vecLength == 0) {
72         return;
73     }
74     // Copy of the selection area for internal modifications
75     m_selection = intersectWithAreas(selection);
76     updateBlockedSides();
77     ensureSelectionMinimunSize();
78     // Indicates the actual button to be moved
79     int elemIndicator = 0;
80 
81     while (elemIndicator < vecLength) {
82 
83         // Add them inside the area when there is no more space
84         if (m_allSidesBlocked) {
85             m_selection = selection;
86             positionButtonsInside(elemIndicator);
87             break; // the while
88         }
89         // Number of buttons per row column
90         int buttonsPerRow =
91           (m_selection.width() + m_separator) / (m_buttonExtendedSize);
92         int buttonsPerCol =
93           (m_selection.height() + m_separator) / (m_buttonExtendedSize);
94         // Buttons to be placed in the corners
95         int extraButtons =
96           (vecLength - elemIndicator) - (buttonsPerRow + buttonsPerCol) * 2;
97         int elemsAtCorners = extraButtons > 4 ? 4 : extraButtons;
98         int maxExtra = 2;
99         if (m_oneHorizontalBlocked) {
100             maxExtra = 1;
101         } else if (m_horizontalyBlocked) {
102             maxExtra = 0;
103         }
104         int elemCornersTop = qBound(0, elemsAtCorners, maxExtra);
105         elemsAtCorners -= elemCornersTop;
106         int elemCornersBotton = qBound(0, elemsAtCorners, maxExtra);
107 
108         // Add buttons at the button of the selection
109         if (!m_blockedBotton) {
110             int addCounter = buttonsPerRow + elemCornersBotton;
111             // Don't add more than we have
112             addCounter = qBound(0, addCounter, vecLength - elemIndicator);
113             QPoint center = QPoint(m_selection.center().x(),
114                                    m_selection.bottom() + m_separator);
115             if (addCounter > buttonsPerRow) {
116                 adjustHorizontalCenter(center);
117             }
118             // ElemIndicator, elemsAtCorners
119             QVector<QPoint> positions =
120               horizontalPoints(center, addCounter, true);
121             moveButtonsToPoints(positions, elemIndicator);
122         }
123         // Add buttons at the right side of the selection
124         if (!m_blockedRight && elemIndicator < vecLength) {
125             int addCounter = buttonsPerCol;
126             addCounter = qBound(0, addCounter, vecLength - elemIndicator);
127 
128             QPoint center = QPoint(m_selection.right() + m_separator,
129                                    m_selection.center().y());
130             QVector<QPoint> positions =
131               verticalPoints(center, addCounter, false);
132             moveButtonsToPoints(positions, elemIndicator);
133         }
134         // Add buttons at the top of the selection
135         if (!m_blockedTop && elemIndicator < vecLength) {
136             int addCounter = buttonsPerRow + elemCornersTop;
137             addCounter = qBound(0, addCounter, vecLength - elemIndicator);
138             QPoint center = QPoint(m_selection.center().x(),
139                                    m_selection.top() - m_buttonExtendedSize);
140             if (addCounter == 1 + buttonsPerRow) {
141                 adjustHorizontalCenter(center);
142             }
143             QVector<QPoint> positions =
144               horizontalPoints(center, addCounter, false);
145             moveButtonsToPoints(positions, elemIndicator);
146         }
147         // Add buttons at the left side of the selection
148         if (!m_blockedLeft && elemIndicator < vecLength) {
149             int addCounter = buttonsPerCol;
150             addCounter = qBound(0, addCounter, vecLength - elemIndicator);
151 
152             QPoint center = QPoint(m_selection.left() - m_buttonExtendedSize,
153                                    m_selection.center().y());
154             QVector<QPoint> positions =
155               verticalPoints(center, addCounter, true);
156             moveButtonsToPoints(positions, elemIndicator);
157         }
158         // If there are elements for the next cycle, increase the size of the
159         // base area
160         if (elemIndicator < vecLength && !(m_allSidesBlocked)) {
161             expandSelection();
162         }
163         updateBlockedSides();
164     }
165 }
166 
167 // horizontalPoints is an auxiliary method for the button position computation.
168 // starts from a known center and keeps adding elements horizontally
169 // and returns the computed positions.
horizontalPoints(const QPoint & center,const int elements,const bool leftToRight) const170 QVector<QPoint> ButtonHandler::horizontalPoints(const QPoint& center,
171                                                 const int elements,
172                                                 const bool leftToRight) const
173 {
174     QVector<QPoint> res;
175     // Distance from the center to start adding buttons
176     int shift = 0;
177     if (elements % 2 == 0) {
178         shift = m_buttonExtendedSize * (elements / 2) - (m_separator / 2);
179     } else {
180         shift =
181           m_buttonExtendedSize * ((elements - 1) / 2) + m_buttonBaseSize / 2;
182     }
183     if (!leftToRight) {
184         shift -= m_buttonBaseSize;
185     }
186     int x = leftToRight ? center.x() - shift : center.x() + shift;
187     QPoint i(x, center.y());
188     while (elements > res.length()) {
189         res.append(i);
190         leftToRight ? i.setX(i.x() + m_buttonExtendedSize)
191                     : i.setX(i.x() - m_buttonExtendedSize);
192     }
193     return res;
194 }
195 
196 // verticalPoints is an auxiliary method for the button position computation.
197 // starts from a known center and keeps adding elements vertically
198 // and returns the computed positions.
verticalPoints(const QPoint & center,const int elements,const bool upToDown) const199 QVector<QPoint> ButtonHandler::verticalPoints(const QPoint& center,
200                                               const int elements,
201                                               const bool upToDown) const
202 {
203     QVector<QPoint> res;
204     // Distance from the center to start adding buttons
205     int shift = 0;
206     if (elements % 2 == 0) {
207         shift = m_buttonExtendedSize * (elements / 2) - (m_separator / 2);
208     } else {
209         shift =
210           m_buttonExtendedSize * ((elements - 1) / 2) + m_buttonBaseSize / 2;
211     }
212     if (!upToDown) {
213         shift -= m_buttonBaseSize;
214     }
215     int y = upToDown ? center.y() - shift : center.y() + shift;
216     QPoint i(center.x(), y);
217     while (elements > res.length()) {
218         res.append(i);
219         upToDown ? i.setY(i.y() + m_buttonExtendedSize)
220                  : i.setY(i.y() - m_buttonExtendedSize);
221     }
222     return res;
223 }
224 
intersectWithAreas(const QRect & rect)225 QRect ButtonHandler::intersectWithAreas(const QRect& rect)
226 {
227     QRect res;
228     for (const QRect& r : m_screenRegions) {
229         QRect temp = rect.intersected(r);
230         if (temp.height() * temp.width() > res.height() * res.width()) {
231             res = temp;
232         }
233     }
234     return res;
235 }
236 
init()237 void ButtonHandler::init()
238 {
239     m_separator = GlobalValues::buttonBaseSize() / 4;
240 }
241 
resetRegionTrack()242 void ButtonHandler::resetRegionTrack()
243 {
244     m_buttonsAreInside = false;
245 }
246 
updateBlockedSides()247 void ButtonHandler::updateBlockedSides()
248 {
249     const int EXTENSION = m_separator * 2 + m_buttonBaseSize;
250     // Right
251     QPoint pointA(m_selection.right() + EXTENSION, m_selection.bottom());
252     QPoint pointB(pointA.x(), m_selection.top());
253     m_blockedRight =
254       !(m_screenRegions.contains(pointA) && m_screenRegions.contains(pointB));
255     // Left
256     pointA.setX(m_selection.left() - EXTENSION);
257     pointB.setX(pointA.x());
258     m_blockedLeft =
259       !(m_screenRegions.contains(pointA) && m_screenRegions.contains(pointB));
260     // Bottom
261     pointA = QPoint(m_selection.left(), m_selection.bottom() + EXTENSION);
262     pointB = QPoint(m_selection.right(), pointA.y());
263     m_blockedBotton =
264       !(m_screenRegions.contains(pointA) && m_screenRegions.contains(pointB));
265     // Top
266     pointA.setY(m_selection.top() - EXTENSION);
267     pointB.setY(pointA.y());
268     m_blockedTop =
269       !(m_screenRegions.contains(pointA) && m_screenRegions.contains(pointB));
270     // Auxiliary
271     m_oneHorizontalBlocked =
272       (!m_blockedRight && m_blockedLeft) || (m_blockedRight && !m_blockedLeft);
273     m_horizontalyBlocked = (m_blockedRight && m_blockedLeft);
274     m_allSidesBlocked =
275       (m_blockedBotton && m_horizontalyBlocked && m_blockedTop);
276 }
277 
expandSelection()278 void ButtonHandler::expandSelection()
279 {
280     int& s = m_buttonExtendedSize;
281     m_selection = m_selection + QMargins(s, s, s, s);
282     m_selection = intersectWithAreas(m_selection);
283 }
284 
positionButtonsInside(int index)285 void ButtonHandler::positionButtonsInside(int index)
286 {
287     // Position the buttons in the botton-center of the main but inside of the
288     // selection.
289     QRect mainArea = m_selection;
290     mainArea = intersectWithAreas(mainArea);
291     const int buttonsPerRow = (mainArea.width()) / (m_buttonExtendedSize);
292     if (buttonsPerRow == 0) {
293         return;
294     }
295     QPoint center =
296       QPoint(mainArea.center().x(), mainArea.bottom() - m_buttonExtendedSize);
297 
298     while (m_vectorButtons.size() > index) {
299         int addCounter = buttonsPerRow;
300         addCounter = qBound(0, addCounter, m_vectorButtons.size() - index);
301         QVector<QPoint> positions = horizontalPoints(center, addCounter, true);
302         moveButtonsToPoints(positions, index);
303         center.setY(center.y() - m_buttonExtendedSize);
304     }
305 
306     m_buttonsAreInside = true;
307 }
308 
ensureSelectionMinimunSize()309 void ButtonHandler::ensureSelectionMinimunSize()
310 {
311     // Detect if a side is smaller than a button in order to prevent collision
312     // and redimension the base area the the base size of a single button per
313     // side
314     if (m_selection.width() < m_buttonBaseSize) {
315         if (!m_blockedLeft) {
316             m_selection.setX(m_selection.x() -
317                              (m_buttonBaseSize - m_selection.width()) / 2);
318         }
319         m_selection.setWidth(m_buttonBaseSize);
320     }
321     if (m_selection.height() < m_buttonBaseSize) {
322         if (!m_blockedTop) {
323             m_selection.setY(m_selection.y() -
324                              (m_buttonBaseSize - m_selection.height()) / 2);
325         }
326         m_selection.setHeight(m_buttonBaseSize);
327     }
328 }
329 
moveButtonsToPoints(const QVector<QPoint> & points,int & index)330 void ButtonHandler::moveButtonsToPoints(const QVector<QPoint>& points,
331                                         int& index)
332 {
333     for (const QPoint& p : points) {
334         auto button = m_vectorButtons[index];
335         button->move(p);
336         ++index;
337     }
338 }
339 
adjustHorizontalCenter(QPoint & center)340 void ButtonHandler::adjustHorizontalCenter(QPoint& center)
341 {
342     if (m_blockedLeft) {
343         center.setX(center.x() + m_buttonExtendedSize / 2);
344     } else if (m_blockedRight) {
345         center.setX(center.x() - m_buttonExtendedSize / 2);
346     }
347 }
348 
349 // setButtons redefines the buttons of the button handler
setButtons(const QVector<CaptureToolButton * > v)350 void ButtonHandler::setButtons(const QVector<CaptureToolButton*> v)
351 {
352     if (v.isEmpty())
353         return;
354 
355     for (CaptureToolButton* b : m_vectorButtons)
356         delete (b);
357     m_vectorButtons = v;
358     m_buttonBaseSize = GlobalValues::buttonBaseSize();
359     m_buttonExtendedSize = m_buttonBaseSize + m_separator;
360 }
361 
contains(const QPoint & p) const362 bool ButtonHandler::contains(const QPoint& p) const
363 {
364     QPoint first(m_vectorButtons.first()->pos());
365     QPoint last(m_vectorButtons.last()->pos());
366     bool firstIsTopLeft = (first.x() <= last.x() && first.y() <= last.y());
367     QPoint topLeft = firstIsTopLeft ? first : last;
368     QPoint bottonRight = firstIsTopLeft ? last : first;
369     topLeft += QPoint(-m_separator, -m_separator);
370     bottonRight += QPoint(m_buttonExtendedSize, m_buttonExtendedSize);
371     QRegion r(QRect(topLeft, bottonRight).normalized());
372     return r.contains(p);
373 }
374 
updateScreenRegions(const QVector<QRect> & rects)375 void ButtonHandler::updateScreenRegions(const QVector<QRect>& rects)
376 {
377     m_screenRegions = QRegion();
378     for (const QRect& rect : rects) {
379         m_screenRegions += rect;
380     }
381 }
382 
updateScreenRegions(const QRect & rect)383 void ButtonHandler::updateScreenRegions(const QRect& rect)
384 {
385     m_screenRegions = QRegion(rect);
386 }
387