1 /*
2     SPDX-FileCopyrightText: 2014 Montel Laurent <montel@kde.org>
3     based on code:
4     SPDX-FileCopyrightText: 2009 Aurélien Gâteau <agateau@kde.org>
5     SPDX-FileCopyrightText: 2009 Kåre Sårs <kare.sars@iki.fi>
6 
7     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
8 */
9 
10 #include "ksplittercollapserbutton.h"
11 
12 // Qt
13 #include <QEvent>
14 #include <QSplitter>
15 #include <QStyleOptionToolButton>
16 #include <QStylePainter>
17 #include <QTimeLine>
18 
19 enum Direction {
20     LeftToRight = 0,
21     RightToLeft,
22     TopToBottom,
23     BottomToTop,
24 };
25 
26 const static int TIMELINE_DURATION = 500;
27 
28 const static qreal MINIMUM_OPACITY = 0.3;
29 
30 static const struct {
31     Qt::ArrowType arrowVisible;
32     Qt::ArrowType notArrowVisible;
33 } s_arrowDirection[] = {{Qt::LeftArrow, Qt::RightArrow}, {Qt::RightArrow, Qt::LeftArrow}, {Qt::UpArrow, Qt::DownArrow}, {Qt::DownArrow, Qt::UpArrow}};
34 
35 class KSplitterCollapserButtonPrivate
36 {
37 public:
38     KSplitterCollapserButtonPrivate(KSplitterCollapserButton *qq);
39 
40     KSplitterCollapserButton *q;
41     QSplitter *splitter;
42     QWidget *childWidget;
43     Direction direction;
44     QTimeLine *opacityTimeLine;
45     QList<int> sizeAtCollapse;
46 
47     bool isVertical() const;
48 
49     bool isWidgetCollapsed() const;
50 
51     void updatePosition();
52 
53     void updateArrow();
54 
55     void widgetEventFilter(QEvent *event);
56 
57     void updateOpacity();
58 
59     void startTimeLine();
60 };
61 
KSplitterCollapserButtonPrivate(KSplitterCollapserButton * qq)62 KSplitterCollapserButtonPrivate::KSplitterCollapserButtonPrivate(KSplitterCollapserButton *qq)
63     : q(qq)
64     , splitter(nullptr)
65     , childWidget(nullptr)
66     , opacityTimeLine(nullptr)
67 {
68 }
69 
isVertical() const70 bool KSplitterCollapserButtonPrivate::isVertical() const
71 {
72     return (splitter->orientation() == Qt::Vertical);
73 }
74 
isWidgetCollapsed() const75 bool KSplitterCollapserButtonPrivate::isWidgetCollapsed() const
76 {
77     const QRect widgetRect = childWidget->geometry();
78     if ((widgetRect.height() == 0) || (widgetRect.width() == 0)) {
79         return true;
80     } else {
81         return false;
82     }
83 }
84 
updatePosition()85 void KSplitterCollapserButtonPrivate::updatePosition()
86 {
87     int x = 0;
88     int y = 0;
89     const QRect widgetRect = childWidget->geometry();
90     const int handleWidth = splitter->handleWidth();
91 
92     if (!isVertical()) {
93         const int splitterWidth = splitter->width();
94         const int width = q->sizeHint().width();
95         // FIXME: Make this configurable
96         y = 30;
97         if (direction == LeftToRight) {
98             if (!isWidgetCollapsed()) {
99                 x = widgetRect.right() + handleWidth;
100             } else {
101                 x = 0;
102             }
103         } else { // RightToLeft
104             if (!isWidgetCollapsed()) {
105                 x = widgetRect.left() - handleWidth - width;
106             } else {
107                 x = splitterWidth - handleWidth - width;
108             }
109         }
110     } else {
111         x = 30;
112         const int height = q->sizeHint().height();
113         const int splitterHeight = splitter->height();
114         if (direction == TopToBottom) {
115             if (!isWidgetCollapsed()) {
116                 y = widgetRect.bottom() + handleWidth;
117             } else {
118                 y = 0;
119             }
120         } else { // BottomToTop
121             if (!isWidgetCollapsed()) {
122                 y = widgetRect.top() - handleWidth - height;
123             } else {
124                 y = splitterHeight - handleWidth - height;
125             }
126         }
127     }
128     q->move(x, y);
129 }
130 
updateArrow()131 void KSplitterCollapserButtonPrivate::updateArrow()
132 {
133     q->setArrowType(isWidgetCollapsed() ? s_arrowDirection[direction].notArrowVisible : s_arrowDirection[direction].arrowVisible);
134 }
135 
widgetEventFilter(QEvent * event)136 void KSplitterCollapserButtonPrivate::widgetEventFilter(QEvent *event)
137 {
138     switch (event->type()) {
139     case QEvent::Resize:
140     case QEvent::Move:
141     case QEvent::Show:
142     case QEvent::Hide:
143         updatePosition();
144         updateOpacity();
145         updateArrow();
146         break;
147 
148     default:
149         break;
150     }
151 }
152 
updateOpacity()153 void KSplitterCollapserButtonPrivate::updateOpacity()
154 {
155     const QPoint pos = q->parentWidget()->mapFromGlobal(QCursor::pos());
156     const QRect opaqueRect = q->geometry();
157     const bool opaqueCollapser = opaqueRect.contains(pos);
158     if (opaqueCollapser) {
159         opacityTimeLine->setDirection(QTimeLine::Forward);
160         startTimeLine();
161     } else {
162         opacityTimeLine->setDirection(QTimeLine::Backward);
163         startTimeLine();
164     }
165 }
166 
startTimeLine()167 void KSplitterCollapserButtonPrivate::startTimeLine()
168 {
169     if (opacityTimeLine->state() == QTimeLine::Running) {
170         opacityTimeLine->stop();
171     }
172     opacityTimeLine->start();
173 }
174 
KSplitterCollapserButton(QWidget * childWidget,QSplitter * splitter)175 KSplitterCollapserButton::KSplitterCollapserButton(QWidget *childWidget, QSplitter *splitter)
176     : QToolButton()
177     , d(new KSplitterCollapserButtonPrivate(this))
178 {
179     setObjectName(QStringLiteral("splittercollapser"));
180     // We do not want our collapser to be added as a regular widget in the
181     // splitter!
182     setAttribute(Qt::WA_NoChildEventsForParent);
183 
184     d->opacityTimeLine = new QTimeLine(TIMELINE_DURATION, this);
185     d->opacityTimeLine->setFrameRange(int(MINIMUM_OPACITY * 1000), 1000);
186     connect(d->opacityTimeLine, &QTimeLine::valueChanged, this, qOverload<>(&QWidget::update));
187 
188     d->childWidget = childWidget;
189     d->childWidget->installEventFilter(this);
190 
191     d->splitter = splitter;
192     setParent(d->splitter);
193 
194     switch (splitter->orientation()) {
195     case Qt::Horizontal:
196         if (splitter->indexOf(childWidget) < splitter->count() / 2) {
197             d->direction = LeftToRight;
198         } else {
199             d->direction = RightToLeft;
200         }
201         break;
202     case Qt::Vertical:
203         if (splitter->indexOf(childWidget) < splitter->count() / 2) {
204             d->direction = TopToBottom;
205         } else {
206             d->direction = BottomToTop;
207         }
208         break;
209     }
210 
211     connect(this, &KSplitterCollapserButton::clicked, this, &KSplitterCollapserButton::slotClicked);
212 }
213 
214 KSplitterCollapserButton::~KSplitterCollapserButton() = default;
215 
isWidgetCollapsed() const216 bool KSplitterCollapserButton::isWidgetCollapsed() const
217 {
218     return d->isWidgetCollapsed();
219 }
220 
eventFilter(QObject * object,QEvent * event)221 bool KSplitterCollapserButton::eventFilter(QObject *object, QEvent *event)
222 {
223     if (object == d->childWidget) {
224         d->widgetEventFilter(event);
225     }
226     return QToolButton::eventFilter(object, event);
227 }
228 
enterEvent(QEvent * event)229 void KSplitterCollapserButton::enterEvent(QEvent *event)
230 {
231     Q_UNUSED(event)
232     d->updateOpacity();
233 }
234 
leaveEvent(QEvent * event)235 void KSplitterCollapserButton::leaveEvent(QEvent *event)
236 {
237     Q_UNUSED(event)
238     d->updateOpacity();
239 }
240 
showEvent(QShowEvent * event)241 void KSplitterCollapserButton::showEvent(QShowEvent *event)
242 {
243     Q_UNUSED(event)
244     d->updateOpacity();
245 }
246 
sizeHint() const247 QSize KSplitterCollapserButton::sizeHint() const
248 {
249     QStyleOption opt;
250     opt.initFrom(this);
251     const int extent = style()->pixelMetric(QStyle::PM_ScrollBarExtent, &opt);
252     QSize sh(extent * 3 / 4, extent * 240 / 100);
253     if (d->isVertical()) {
254         sh.transpose();
255     }
256     return sh;
257 }
258 
slotClicked()259 void KSplitterCollapserButton::slotClicked()
260 {
261     QList<int> sizes = d->splitter->sizes();
262     const int index = d->splitter->indexOf(d->childWidget);
263     if (!d->isWidgetCollapsed()) {
264         d->sizeAtCollapse = sizes;
265         sizes[index] = 0;
266     } else {
267         if (!d->sizeAtCollapse.isEmpty()) {
268             sizes = d->sizeAtCollapse;
269         } else {
270             if (d->isVertical()) {
271                 sizes[index] = d->childWidget->sizeHint().height();
272             } else {
273                 sizes[index] = d->childWidget->sizeHint().width();
274             }
275         }
276     }
277     d->splitter->setSizes(sizes);
278     d->opacityTimeLine->setDirection(QTimeLine::Backward);
279     d->startTimeLine();
280 }
281 
collapse()282 void KSplitterCollapserButton::collapse()
283 {
284     if (!d->isWidgetCollapsed()) {
285         slotClicked();
286     }
287     // else do nothing
288 }
289 
restore()290 void KSplitterCollapserButton::restore()
291 {
292     if (d->isWidgetCollapsed()) {
293         slotClicked();
294     }
295     // else do nothing
296 }
297 
setCollapsed(bool collapse)298 void KSplitterCollapserButton::setCollapsed(bool collapse)
299 {
300     if (collapse == d->isWidgetCollapsed()) {
301         slotClicked();
302     }
303     // else do nothing
304 }
305 
paintEvent(QPaintEvent *)306 void KSplitterCollapserButton::paintEvent(QPaintEvent *)
307 {
308     QStylePainter painter(this);
309     const qreal opacity = d->opacityTimeLine->currentFrame() / 1000.;
310     painter.setOpacity(opacity);
311 
312     QStyleOptionToolButton opt;
313     initStyleOption(&opt);
314 
315     if (d->isVertical()) {
316         if (d->direction == TopToBottom) {
317             opt.rect.setTop(-height());
318         } else {
319             opt.rect.setHeight(height() * 2);
320         }
321     } else {
322         if (d->direction == LeftToRight) {
323             opt.rect.setLeft(-width());
324         } else {
325             opt.rect.setWidth(width() * 2);
326         }
327     }
328     painter.drawPrimitive(QStyle::PE_PanelButtonTool, opt);
329 
330     QStyleOptionToolButton opt2;
331     initStyleOption(&opt2);
332     painter.drawControl(QStyle::CE_ToolButtonLabel, opt2);
333 }
334