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
19enum Direction {
20 LeftToRight = 0,
21 RightToLeft,
22 TopToBottom,
23 BottomToTop,
24};
25
26const static int TIMELINE_DURATION = 500;
27
28const static qreal MINIMUM_OPACITY = 0.3;
29
30static const struct {
31 Qt::ArrowType arrowVisible;
32 Qt::ArrowType notArrowVisible;
33} s_arrowDirection[] = {{.arrowVisible: Qt::LeftArrow, .notArrowVisible: Qt::RightArrow}, {.arrowVisible: Qt::RightArrow, .notArrowVisible: Qt::LeftArrow}, {.arrowVisible: Qt::UpArrow, .notArrowVisible: Qt::DownArrow}, {.arrowVisible: Qt::DownArrow, .notArrowVisible: Qt::UpArrow}};
34
35class KSplitterCollapserButtonPrivate
36{
37public:
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
62KSplitterCollapserButtonPrivate::KSplitterCollapserButtonPrivate(KSplitterCollapserButton *qq)
63 : q(qq)
64 , splitter(nullptr)
65 , childWidget(nullptr)
66 , opacityTimeLine(nullptr)
67{
68}
69
70bool KSplitterCollapserButtonPrivate::isVertical() const
71{
72 return (splitter->orientation() == Qt::Vertical);
73}
74
75bool 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
85void 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(ax: x, ay: y);
129}
130
131void KSplitterCollapserButtonPrivate::updateArrow()
132{
133 q->setArrowType(isWidgetCollapsed() ? s_arrowDirection[direction].notArrowVisible : s_arrowDirection[direction].arrowVisible);
134}
135
136void 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
153void KSplitterCollapserButtonPrivate::updateOpacity()
154{
155 const QPoint pos = q->parentWidget()->mapFromGlobal(QCursor::pos());
156 const QRect opaqueRect = q->geometry();
157 const bool opaqueCollapser = opaqueRect.contains(p: pos);
158 if (opaqueCollapser) {
159 opacityTimeLine->setDirection(QTimeLine::Forward);
160 startTimeLine();
161 } else {
162 opacityTimeLine->setDirection(QTimeLine::Backward);
163 startTimeLine();
164 }
165}
166
167void KSplitterCollapserButtonPrivate::startTimeLine()
168{
169 if (opacityTimeLine->state() == QTimeLine::Running) {
170 opacityTimeLine->stop();
171 }
172 opacityTimeLine->start();
173}
174
175KSplitterCollapserButton::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(startFrame: int(MINIMUM_OPACITY * 1000), endFrame: 1000);
186 connect(sender: d->opacityTimeLine, signal: &QTimeLine::valueChanged, context: this, slot: qOverload<>(&QWidget::update));
187
188 d->childWidget = childWidget;
189 d->childWidget->installEventFilter(filterObj: this);
190
191 d->splitter = splitter;
192 setParent(d->splitter);
193
194 switch (splitter->orientation()) {
195 case Qt::Horizontal:
196 if (splitter->indexOf(w: 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(w: childWidget) < splitter->count() / 2) {
204 d->direction = TopToBottom;
205 } else {
206 d->direction = BottomToTop;
207 }
208 break;
209 }
210
211 connect(sender: this, signal: &KSplitterCollapserButton::clicked, context: this, slot: &KSplitterCollapserButton::slotClicked);
212}
213
214KSplitterCollapserButton::~KSplitterCollapserButton() = default;
215
216bool KSplitterCollapserButton::isWidgetCollapsed() const
217{
218 return d->isWidgetCollapsed();
219}
220
221bool KSplitterCollapserButton::eventFilter(QObject *object, QEvent *event)
222{
223 if (object == d->childWidget) {
224 d->widgetEventFilter(event);
225 }
226 return QToolButton::eventFilter(watched: object, event);
227}
228
229void KSplitterCollapserButton::enterEvent(QEnterEvent *event)
230{
231 Q_UNUSED(event)
232 d->updateOpacity();
233}
234
235void KSplitterCollapserButton::leaveEvent(QEvent *event)
236{
237 Q_UNUSED(event)
238 d->updateOpacity();
239}
240
241void KSplitterCollapserButton::showEvent(QShowEvent *event)
242{
243 Q_UNUSED(event)
244 d->updateOpacity();
245}
246
247QSize KSplitterCollapserButton::sizeHint() const
248{
249 QStyleOption opt;
250 opt.initFrom(w: this);
251 const int extent = style()->pixelMetric(metric: QStyle::PM_ScrollBarExtent, option: &opt);
252 QSize sh(extent * 3 / 4, extent * 240 / 100);
253 if (d->isVertical()) {
254 sh.transpose();
255 }
256 return sh;
257}
258
259void KSplitterCollapserButton::slotClicked()
260{
261 QList<int> sizes = d->splitter->sizes();
262 const int index = d->splitter->indexOf(w: 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
282void KSplitterCollapserButton::collapse()
283{
284 if (!d->isWidgetCollapsed()) {
285 slotClicked();
286 }
287 // else do nothing
288}
289
290void KSplitterCollapserButton::restore()
291{
292 if (d->isWidgetCollapsed()) {
293 slotClicked();
294 }
295 // else do nothing
296}
297
298void KSplitterCollapserButton::setCollapsed(bool collapse)
299{
300 if (collapse == d->isWidgetCollapsed()) {
301 slotClicked();
302 }
303 // else do nothing
304}
305
306void 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(option: &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(pe: QStyle::PE_PanelButtonTool, opt);
329
330 QStyleOptionToolButton opt2;
331 initStyleOption(option: &opt2);
332 painter.drawControl(ce: QStyle::CE_ToolButtonLabel, opt: opt2);
333}
334
335#include "moc_ksplittercollapserbutton.cpp"
336

source code of kwidgetsaddons/src/ksplittercollapserbutton.cpp