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