1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "fancymainwindow.h"
27 
28 #include "algorithm.h"
29 #include "porting.h"
30 #include "qtcassert.h"
31 #include "stringutils.h"
32 
33 #include <QAbstractButton>
34 #include <QApplication>
35 #include <QContextMenuEvent>
36 #include <QDockWidget>
37 #include <QHBoxLayout>
38 #include <QLabel>
39 #include <QMenu>
40 #include <QPainter>
41 #include <QSettings>
42 #include <QStyle>
43 #include <QStyleOption>
44 #include <QTimer>
45 #include <QToolButton>
46 
47 static const char AutoHideTitleBarsKey[] = "AutoHideTitleBars";
48 static const char ShowCentralWidgetKey[] = "ShowCentralWidget";
49 static const char StateKey[] = "State";
50 
51 static const int settingsVersion = 2;
52 static const char dockWidgetActiveState[] = "DockWidgetActiveState";
53 
54 namespace Utils {
55 
56 class TitleBarWidget;
57 
58 struct FancyMainWindowPrivate
59 {
60     FancyMainWindowPrivate(FancyMainWindow *parent);
61 
62     FancyMainWindow *q;
63 
64     bool m_handleDockVisibilityChanges;
65     QAction m_showCentralWidget;
66     QAction m_menuSeparator1;
67     QAction m_menuSeparator2;
68     QAction m_resetLayoutAction;
69     QAction m_autoHideTitleBars;
70 };
71 
72 class DockWidget : public QDockWidget
73 {
74 public:
75     DockWidget(QWidget *inner, FancyMainWindow *parent, bool immutable = false);
76 
77     bool eventFilter(QObject *, QEvent *event) override;
78     void enterEvent(EnterEvent *event) override;
79     void leaveEvent(QEvent *event) override;
80     void handleMouseTimeout();
81     void handleToplevelChanged(bool floating);
82 
83     FancyMainWindow *q;
84 
85 private:
86     QPoint m_startPos;
87     TitleBarWidget *m_titleBar;
88     QTimer m_timer;
89     bool m_immutable = false;
90 };
91 
92 // Stolen from QDockWidgetTitleButton
93 class DockWidgetTitleButton : public QAbstractButton
94 {
95 public:
DockWidgetTitleButton(QWidget * parent)96     DockWidgetTitleButton(QWidget *parent)
97         : QAbstractButton(parent)
98     {
99         setFocusPolicy(Qt::NoFocus);
100     }
101 
sizeHint() const102     QSize sizeHint() const override
103     {
104         ensurePolished();
105 
106         int size = 2*style()->pixelMetric(QStyle::PM_DockWidgetTitleBarButtonMargin, nullptr, this);
107         if (!icon().isNull()) {
108             int iconSize = style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, this);
109             QSize sz = icon().actualSize(QSize(iconSize, iconSize));
110             size += qMax(sz.width(), sz.height());
111         }
112 
113         return QSize(size, size);
114     }
115 
minimumSizeHint() const116     QSize minimumSizeHint() const override { return sizeHint(); }
117 
enterEvent(EnterEvent * event)118     void enterEvent(EnterEvent *event) override
119     {
120         if (isEnabled())
121             update();
122         QAbstractButton::enterEvent(event);
123     }
124 
leaveEvent(QEvent * event)125     void leaveEvent(QEvent *event) override
126     {
127         if (isEnabled())
128             update();
129         QAbstractButton::leaveEvent(event);
130     }
131 
132     void paintEvent(QPaintEvent *event) override;
133 };
134 
paintEvent(QPaintEvent *)135 void DockWidgetTitleButton::paintEvent(QPaintEvent *)
136 {
137     QPainter p(this);
138 
139     QStyleOptionToolButton opt;
140     opt.initFrom(this);
141     opt.state |= QStyle::State_AutoRaise;
142     opt.icon = icon();
143     opt.subControls = {};
144     opt.activeSubControls = {};
145     opt.features = QStyleOptionToolButton::None;
146     opt.arrowType = Qt::NoArrow;
147     int size = style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, this);
148     opt.iconSize = QSize(size, size);
149     style()->drawComplexControl(QStyle::CC_ToolButton, &opt, &p, this);
150 }
151 
152 class TitleBarWidget : public QWidget
153 {
154 public:
TitleBarWidget(DockWidget * parent,const QStyleOptionDockWidget & opt)155     TitleBarWidget(DockWidget *parent, const QStyleOptionDockWidget &opt)
156       : QWidget(parent), q(parent), m_active(true)
157     {
158         m_titleLabel = new QLabel(this);
159 
160         m_floatButton = new DockWidgetTitleButton(this);
161         m_floatButton->setIcon(q->style()->standardIcon(QStyle::SP_TitleBarNormalButton, &opt, q));
162 
163         m_closeButton = new DockWidgetTitleButton(this);
164         m_closeButton->setIcon(q->style()->standardIcon(QStyle::SP_TitleBarCloseButton, &opt, q));
165 
166 #ifndef QT_NO_ACCESSIBILITY
167         m_floatButton->setAccessibleName(QDockWidget::tr("Float"));
168         m_floatButton->setAccessibleDescription(QDockWidget::tr("Undocks and re-attaches the dock widget"));
169         m_closeButton->setAccessibleName(QDockWidget::tr("Close"));
170         m_closeButton->setAccessibleDescription(QDockWidget::tr("Closes the dock widget"));
171 #endif
172 
173         setActive(false);
174 
175         const int minWidth = 10;
176         const int maxWidth = 10000;
177         const int inactiveHeight = 0;
178         const int activeHeight = m_closeButton->sizeHint().height() + 2;
179 
180         m_minimumInactiveSize = QSize(minWidth, inactiveHeight);
181         m_maximumInactiveSize = QSize(maxWidth, inactiveHeight);
182         m_minimumActiveSize   = QSize(minWidth, activeHeight);
183         m_maximumActiveSize   = QSize(maxWidth, activeHeight);
184 
185         auto layout = new QHBoxLayout(this);
186         layout->setSpacing(0);
187         layout->setContentsMargins(4, 0, 0, 0);
188         layout->addWidget(m_titleLabel);
189         layout->addStretch();
190         layout->addWidget(m_floatButton);
191         layout->addWidget(m_closeButton);
192         setLayout(layout);
193 
194         setProperty("managed_titlebar", 1);
195     }
196 
enterEvent(EnterEvent * event)197     void enterEvent(EnterEvent *event) override
198     {
199         setActive(true);
200         QWidget::enterEvent(event);
201     }
202 
setActive(bool on)203     void setActive(bool on)
204     {
205         m_active = on;
206         updateChildren();
207     }
208 
updateChildren()209     void updateChildren()
210     {
211         bool clickable = isClickable();
212         m_titleLabel->setVisible(clickable);
213         m_floatButton->setVisible(clickable);
214         m_closeButton->setVisible(clickable);
215     }
216 
isClickable() const217     bool isClickable() const
218     {
219         return m_active || !q->q->autoHideTitleBars();
220     }
221 
sizeHint() const222     QSize sizeHint() const override
223     {
224         ensurePolished();
225         return isClickable() ? m_maximumActiveSize : m_maximumInactiveSize;
226     }
227 
minimumSizeHint() const228     QSize minimumSizeHint() const override
229     {
230         ensurePolished();
231         return isClickable() ? m_minimumActiveSize : m_minimumInactiveSize;
232     }
233 
234 private:
235     DockWidget *q;
236     bool m_active;
237     QSize m_minimumActiveSize;
238     QSize m_maximumActiveSize;
239     QSize m_minimumInactiveSize;
240     QSize m_maximumInactiveSize;
241 
242 public:
243     QLabel *m_titleLabel;
244     DockWidgetTitleButton *m_floatButton;
245     DockWidgetTitleButton *m_closeButton;
246 };
247 
DockWidget(QWidget * inner,FancyMainWindow * parent,bool immutable)248 DockWidget::DockWidget(QWidget *inner, FancyMainWindow *parent, bool immutable)
249     : QDockWidget(parent), q(parent), m_immutable(immutable)
250 {
251     setWidget(inner);
252     setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable);
253     setObjectName(inner->objectName() + QLatin1String("DockWidget"));
254     setMouseTracking(true);
255 
256     QString title = inner->windowTitle();
257     toggleViewAction()->setProperty("original_title", title);
258     title = Utils::stripAccelerator(title);
259     setWindowTitle(title);
260 
261     QStyleOptionDockWidget opt;
262     initStyleOption(&opt);
263     m_titleBar = new TitleBarWidget(this, opt);
264     m_titleBar->m_titleLabel->setText(title);
265     setTitleBarWidget(m_titleBar);
266 
267     if (immutable)
268         return;
269 
270     m_timer.setSingleShot(true);
271     m_timer.setInterval(500);
272 
273     connect(&m_timer, &QTimer::timeout, this, &DockWidget::handleMouseTimeout);
274 
275     connect(this, &QDockWidget::topLevelChanged, this, &DockWidget::handleToplevelChanged);
276 
277     connect(toggleViewAction(), &QAction::triggered,
278             [this]() {
279                 if (isVisible())
280                     raise();
281             });
282 
283     auto origFloatButton = findChild<QAbstractButton *>(QLatin1String("qt_dockwidget_floatbutton"));
284     connect(m_titleBar->m_floatButton, &QAbstractButton::clicked,
285             origFloatButton, &QAbstractButton::clicked);
286 
287     auto origCloseButton = findChild<QAbstractButton *>(QLatin1String("qt_dockwidget_closebutton"));
288     connect(m_titleBar->m_closeButton, &QAbstractButton::clicked,
289             origCloseButton, &QAbstractButton::clicked);
290 }
291 
eventFilter(QObject *,QEvent * event)292 bool DockWidget::eventFilter(QObject *, QEvent *event)
293 {
294     if (!m_immutable && event->type() == QEvent::MouseMove && q->autoHideTitleBars()) {
295         auto me = static_cast<QMouseEvent *>(event);
296         int y = me->pos().y();
297         int x = me->pos().x();
298         int h = qMin(8, m_titleBar->m_floatButton->height());
299         if (!isFloating() && widget() && 0 <= x && x < widget()->width() && 0 <= y && y <= h) {
300             m_timer.start();
301             m_startPos = mapToGlobal(me->pos());
302         }
303     }
304     return false;
305 }
306 
enterEvent(EnterEvent * event)307 void DockWidget::enterEvent(EnterEvent *event)
308 {
309     if (!m_immutable)
310         QApplication::instance()->installEventFilter(this);
311     QDockWidget::enterEvent(event);
312 }
313 
leaveEvent(QEvent * event)314 void DockWidget::leaveEvent(QEvent *event)
315 {
316     if (!m_immutable) {
317         if (!isFloating()) {
318             m_timer.stop();
319             m_titleBar->setActive(false);
320         }
321         QApplication::instance()->removeEventFilter(this);
322     }
323     QDockWidget::leaveEvent(event);
324 }
325 
handleMouseTimeout()326 void DockWidget::handleMouseTimeout()
327 {
328     QPoint dist = m_startPos - QCursor::pos();
329     if (!isFloating() && dist.manhattanLength() < 4)
330         m_titleBar->setActive(true);
331 }
332 
handleToplevelChanged(bool floating)333 void DockWidget::handleToplevelChanged(bool floating)
334 {
335     m_titleBar->setActive(floating);
336 }
337 
338 
339 
340 /*! \class Utils::FancyMainWindow
341 
342     \brief The FancyMainWindow class is a MainWindow with dock widgets and
343     additional "lock" functionality
344     (locking the dock widgets in place) and "reset layout" functionality.
345 
346     The dock actions and the additional actions should be accessible
347     in a Window-menu.
348 */
349 
FancyMainWindowPrivate(FancyMainWindow * parent)350 FancyMainWindowPrivate::FancyMainWindowPrivate(FancyMainWindow *parent) :
351     q(parent),
352     m_handleDockVisibilityChanges(true),
353     m_showCentralWidget(FancyMainWindow::tr("Central Widget"), nullptr),
354     m_menuSeparator1(nullptr),
355     m_menuSeparator2(nullptr),
356     m_resetLayoutAction(FancyMainWindow::tr("Reset to Default Layout"), nullptr),
357     m_autoHideTitleBars(FancyMainWindow::tr("Automatically Hide View Title Bars"), nullptr)
358 {
359     m_showCentralWidget.setCheckable(true);
360     m_showCentralWidget.setChecked(true);
361 
362     m_menuSeparator1.setSeparator(true);
363     m_menuSeparator2.setSeparator(true);
364 
365     m_autoHideTitleBars.setCheckable(true);
366     m_autoHideTitleBars.setChecked(true);
367 
368     QObject::connect(&m_autoHideTitleBars, &QAction::toggled, q, [this](bool) {
369         for (QDockWidget *dock : q->dockWidgets()) {
370             if (auto titleBar = dynamic_cast<TitleBarWidget *>(dock->titleBarWidget()))
371                 titleBar->updateChildren();
372         }
373     });
374 
375     QObject::connect(&m_showCentralWidget, &QAction::toggled, q, [this](bool visible) {
376         q->centralWidget()->setVisible(visible);
377     });
378 }
379 
FancyMainWindow(QWidget * parent)380 FancyMainWindow::FancyMainWindow(QWidget *parent) :
381     QMainWindow(parent), d(new FancyMainWindowPrivate(this))
382 {
383     connect(&d->m_resetLayoutAction, &QAction::triggered,
384             this, &FancyMainWindow::resetLayout);
385 }
386 
~FancyMainWindow()387 FancyMainWindow::~FancyMainWindow()
388 {
389     delete d;
390 }
391 
addDockForWidget(QWidget * widget,bool immutable)392 QDockWidget *FancyMainWindow::addDockForWidget(QWidget *widget, bool immutable)
393 {
394     QTC_ASSERT(widget, return nullptr);
395     QTC_CHECK(widget->objectName().size());
396     QTC_CHECK(widget->windowTitle().size());
397 
398     auto dockWidget = new DockWidget(widget, this, immutable);
399 
400     if (!immutable) {
401         connect(dockWidget, &QDockWidget::visibilityChanged,
402             [this, dockWidget](bool visible) {
403                 if (d->m_handleDockVisibilityChanges)
404                     dockWidget->setProperty(dockWidgetActiveState, visible);
405             });
406 
407         connect(dockWidget->toggleViewAction(), &QAction::triggered,
408                 this, &FancyMainWindow::onDockActionTriggered,
409                 Qt::QueuedConnection);
410 
411         dockWidget->setProperty(dockWidgetActiveState, true);
412     }
413 
414     return dockWidget;
415 }
416 
onDockActionTriggered()417 void FancyMainWindow::onDockActionTriggered()
418 {
419     auto dw = qobject_cast<QDockWidget *>(sender()->parent());
420     if (dw) {
421         if (dw->isVisible())
422             dw->raise();
423     }
424 }
425 
setTrackingEnabled(bool enabled)426 void FancyMainWindow::setTrackingEnabled(bool enabled)
427 {
428     if (enabled) {
429         d->m_handleDockVisibilityChanges = true;
430         for (QDockWidget *dockWidget : dockWidgets())
431             dockWidget->setProperty(dockWidgetActiveState, dockWidget->isVisible());
432     } else {
433         d->m_handleDockVisibilityChanges = false;
434     }
435 }
436 
hideEvent(QHideEvent * event)437 void FancyMainWindow::hideEvent(QHideEvent *event)
438 {
439     Q_UNUSED(event)
440     handleVisibilityChanged(false);
441 }
442 
showEvent(QShowEvent * event)443 void FancyMainWindow::showEvent(QShowEvent *event)
444 {
445     Q_UNUSED(event)
446     handleVisibilityChanged(true);
447 }
448 
contextMenuEvent(QContextMenuEvent * event)449 void FancyMainWindow::contextMenuEvent(QContextMenuEvent *event)
450 {
451     QMenu menu;
452     addDockActionsToMenu(&menu);
453     menu.exec(event->globalPos());
454 }
455 
handleVisibilityChanged(bool visible)456 void FancyMainWindow::handleVisibilityChanged(bool visible)
457 {
458     d->m_handleDockVisibilityChanges = false;
459     for (QDockWidget *dockWidget : dockWidgets()) {
460         if (dockWidget->isFloating()) {
461             dockWidget->setVisible(visible
462                 && dockWidget->property(dockWidgetActiveState).toBool());
463         }
464     }
465     if (visible)
466         d->m_handleDockVisibilityChanges = true;
467 }
468 
saveSettings(QSettings * settings) const469 void FancyMainWindow::saveSettings(QSettings *settings) const
470 {
471     const QHash<QString, QVariant> hash = saveSettings();
472     for (auto it = hash.cbegin(), end = hash.cend(); it != end; ++it)
473         settings->setValue(it.key(), it.value());
474 }
475 
restoreSettings(const QSettings * settings)476 void FancyMainWindow::restoreSettings(const QSettings *settings)
477 {
478     QHash<QString, QVariant> hash;
479     const QStringList childKeys = settings->childKeys();
480     for (const QString &key : childKeys)
481         hash.insert(key, settings->value(key));
482     restoreSettings(hash);
483 }
484 
saveSettings() const485 QHash<QString, QVariant> FancyMainWindow::saveSettings() const
486 {
487     QHash<QString, QVariant> settings;
488     settings.insert(QLatin1String(StateKey), saveState(settingsVersion));
489     settings.insert(QLatin1String(AutoHideTitleBarsKey),
490         d->m_autoHideTitleBars.isChecked());
491     settings.insert(ShowCentralWidgetKey, d->m_showCentralWidget.isChecked());
492     for (QDockWidget *dockWidget : dockWidgets()) {
493         settings.insert(dockWidget->objectName(),
494                 dockWidget->property(dockWidgetActiveState));
495     }
496     return settings;
497 }
498 
restoreSettings(const QHash<QString,QVariant> & settings)499 void FancyMainWindow::restoreSettings(const QHash<QString, QVariant> &settings)
500 {
501     QByteArray ba = settings.value(QLatin1String(StateKey), QByteArray()).toByteArray();
502     if (!ba.isEmpty())
503         restoreState(ba, settingsVersion);
504     bool on = settings.value(QLatin1String(AutoHideTitleBarsKey), true).toBool();
505     d->m_autoHideTitleBars.setChecked(on);
506     d->m_showCentralWidget.setChecked(settings.value(ShowCentralWidgetKey, true).toBool());
507     for (QDockWidget *widget : dockWidgets()) {
508         widget->setProperty(dockWidgetActiveState,
509             settings.value(widget->objectName(), false));
510     }
511 }
512 
dockWidgets() const513 const QList<QDockWidget *> FancyMainWindow::dockWidgets() const
514 {
515     return findChildren<QDockWidget *>();
516 }
517 
autoHideTitleBars() const518 bool FancyMainWindow::autoHideTitleBars() const
519 {
520     return d->m_autoHideTitleBars.isChecked();
521 }
522 
setAutoHideTitleBars(bool on)523 void FancyMainWindow::setAutoHideTitleBars(bool on)
524 {
525     d->m_autoHideTitleBars.setChecked(on);
526 }
527 
isCentralWidgetShown() const528 bool FancyMainWindow::isCentralWidgetShown() const
529 {
530     return d->m_showCentralWidget.isChecked();
531 }
532 
showCentralWidget(bool on)533 void FancyMainWindow::showCentralWidget(bool on)
534 {
535     d->m_showCentralWidget.setChecked(on);
536 }
537 
addDockActionsToMenu(QMenu * menu)538 void FancyMainWindow::addDockActionsToMenu(QMenu *menu)
539 {
540     QList<QAction *> actions;
541     QList<QDockWidget *> dockwidgets = findChildren<QDockWidget *>();
542     for (int i = 0; i < dockwidgets.size(); ++i) {
543         QDockWidget *dockWidget = dockwidgets.at(i);
544         if (dockWidget->property("managed_dockwidget").isNull()
545                 && dockWidget->parentWidget() == this) {
546             QAction *action = dockWidget->toggleViewAction();
547             action->setText(action->property("original_title").toString());
548             actions.append(action);
549         }
550     }
551     Utils::sort(actions, [](const QAction *action1, const QAction *action2) {
552         QTC_ASSERT(action1, return true);
553         QTC_ASSERT(action2, return false);
554         return stripAccelerator(action1->text()).toLower() < stripAccelerator(action2->text()).toLower();
555     });
556     for (QAction *action : qAsConst(actions))
557         menu->addAction(action);
558     menu->addAction(&d->m_showCentralWidget);
559     menu->addAction(&d->m_menuSeparator1);
560     menu->addAction(&d->m_autoHideTitleBars);
561     menu->addAction(&d->m_menuSeparator2);
562     menu->addAction(&d->m_resetLayoutAction);
563 }
564 
menuSeparator1() const565 QAction *FancyMainWindow::menuSeparator1() const
566 {
567     return &d->m_menuSeparator1;
568 }
569 
autoHideTitleBarsAction() const570 QAction *FancyMainWindow::autoHideTitleBarsAction() const
571 {
572     return &d->m_autoHideTitleBars;
573 }
574 
menuSeparator2() const575 QAction *FancyMainWindow::menuSeparator2() const
576 {
577     return &d->m_menuSeparator2;
578 }
579 
resetLayoutAction() const580 QAction *FancyMainWindow::resetLayoutAction() const
581 {
582     return &d->m_resetLayoutAction;
583 }
584 
showCentralWidgetAction() const585 QAction *FancyMainWindow::showCentralWidgetAction() const
586 {
587     return &d->m_showCentralWidget;
588 }
589 
setDockActionsVisible(bool v)590 void FancyMainWindow::setDockActionsVisible(bool v)
591 {
592     for (const QDockWidget *dockWidget : dockWidgets())
593         dockWidget->toggleViewAction()->setVisible(v);
594     d->m_showCentralWidget.setVisible(v);
595     d->m_autoHideTitleBars.setVisible(v);
596     d->m_menuSeparator1.setVisible(v);
597     d->m_menuSeparator2.setVisible(v);
598     d->m_resetLayoutAction.setVisible(v);
599 }
600 
601 } // namespace Utils
602