/* * The MIT License (MIT) * * Copyright (c) 2014 Pavel Strakhov * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * */ #include "ToolWindowManagerWrapper.h" #include "ToolWindowManager.h" #include "ToolWindowManagerArea.h" #include #include #include #include #include #include #include #include #include ToolWindowManagerWrapper::ToolWindowManagerWrapper(ToolWindowManager *manager, bool floating) : QWidget(manager) , m_manager(manager) { Qt::WindowFlags flags = Qt::Window; #if defined(Q_OS_WIN32) flags |= Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint; #endif setMouseTracking(true); setWindowFlags(flags); setWindowTitle(QStringLiteral(" ")); m_dragReady = false; m_dragActive = false; m_dragDirection = ResizeDirection::Count; m_floating = floating; QVBoxLayout* mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(0, 0, 0, 0); mainLayout->setMargin(0); mainLayout->setSpacing(0); m_manager->m_wrappers << this; m_moveTimeout = new QTimer(this); m_moveTimeout->setInterval(100); m_moveTimeout->stop(); QObject::connect(m_moveTimeout, &QTimer::timeout, this, &ToolWindowManagerWrapper::moveTimeout); m_closeButtonSize = 0; m_frameWidth = 0; m_titleHeight = 0; if (floating && (flags & Qt::FramelessWindowHint)) { m_closeButtonSize = style()->pixelMetric(QStyle::PM_SmallIconSize, 0, this); QFontMetrics titleFontMetrics = fontMetrics(); int mw = style()->pixelMetric(QStyle::PM_DockWidgetTitleMargin, 0, this); m_titleHeight = qMax(m_closeButtonSize + 2, titleFontMetrics.height() + 2*mw); m_frameWidth = style()->pixelMetric(QStyle::PM_DockWidgetFrameWidth, 0, this); mainLayout->setContentsMargins(QMargins(m_frameWidth+4, m_frameWidth+4 + m_titleHeight, m_frameWidth+4, m_frameWidth+4)); } if (floating) { installEventFilter(this); updateTitle(); } } ToolWindowManagerWrapper::~ToolWindowManagerWrapper() { m_manager->m_wrappers.removeOne(this); } void ToolWindowManagerWrapper::updateTitle() { if (!m_floating) return; // find the best candidate for a 'title' for this floating window. if (layout()->count() > 0) { QWidget *child = layout()->itemAt(0)->widget(); while (child) { // if we've found an area, use its currently selected tab's text if (ToolWindowManagerArea* area = qobject_cast(child)) { setWindowTitle(area->tabText(area->currentIndex())); return; } // otherwise we should have a splitter if (QSplitter* splitter = qobject_cast(child)) { // if it's empty, just bail if (splitter->count() == 0) break; // if it's vertical, we pick the first child and recurse if (splitter->orientation() == Qt::Vertical) { child = splitter->widget(0); continue; } // if it's horizontal there's ambiguity so we just pick the biggest one by size, with a // tie-break for the leftmost one QList sizes = splitter->sizes(); int maxIdx = 0; int maxSize = sizes[0]; for (int i=1; i < sizes.count(); i++) { if (sizes[i] > maxSize) { maxSize = sizes[i]; maxIdx = i; } } child = splitter->widget(maxIdx); continue; } // if not, use this object's window title setWindowTitle(child->windowTitle()); return; } } setWindowTitle(QStringLiteral("Tool Window")); } void ToolWindowManagerWrapper::closeEvent(QCloseEvent *event) { QList toolWindows; foreach(ToolWindowManagerArea* tabWidget, findChildren()) { if (ToolWindowManager::managerOf(tabWidget) == m_manager) { toolWindows << tabWidget->toolWindows(); } } foreach(QWidget* toolWindow, toolWindows) { if (!m_manager->allowClose(toolWindow)) { event->ignore(); return; } } foreach(QWidget* toolWindow, toolWindows) { if(m_manager->toolWindowProperties(toolWindow) & ToolWindowManager::HideOnClose) m_manager->hideToolWindow(toolWindow); else m_manager->removeToolWindow(toolWindow); } } bool ToolWindowManagerWrapper::eventFilter(QObject *object, QEvent *event) { const Qt::CursorShape shapes[(int)ResizeDirection::Count] = { Qt::SizeFDiagCursor, Qt::SizeBDiagCursor, Qt::SizeBDiagCursor, Qt::SizeFDiagCursor, Qt::SizeVerCursor, Qt::SizeHorCursor, Qt::SizeVerCursor, Qt::SizeHorCursor, }; if (object == this) { if (event->type() == QEvent::MouseButtonRelease || event->type() == QEvent::NonClientAreaMouseButtonRelease) { m_dragReady = false; m_dragDirection = ResizeDirection::Count; if (!m_dragActive && m_closeRect.contains(mapFromGlobal(QCursor::pos()))) { // catch clicks on the close button close(); } else { // if the mouse button is released, let the manager finish the drag and don't call any more // updates for any further move events m_dragActive = false; m_manager->updateDragPosition(); } } else if (event->type() == QEvent::MouseMove || event->type() == QEvent::NonClientAreaMouseMove) { // if we're ready to start a drag, check how far we've moved and start the drag if past a // certain pixel threshold. if (m_dragReady) { if ((QCursor::pos() - m_dragStartCursor).manhattanLength() > 10) { m_dragActive = true; m_dragReady = false; QList toolWindows; foreach(ToolWindowManagerArea* tabWidget, findChildren()) { if (ToolWindowManager::managerOf(tabWidget) == m_manager) { toolWindows << tabWidget->toolWindows(); } } m_manager->startDrag(toolWindows, this); } } // if the drag is active, update it in the manager. if (m_dragActive) { m_manager->updateDragPosition(); // on non-windows we have no native title bar, so we need to move the window ourselves #if !defined(Q_OS_WIN32) move(QCursor::pos() - (m_dragStartCursor - m_dragStartGeometry.topLeft())); #endif } if (titleRect().contains(mapFromGlobal(QCursor::pos()))) { // if we're in the title bar, repaint to pick up motion over the close button update(); } ResizeDirection dir = checkResize(); if (m_dragDirection != ResizeDirection::Count) { dir = m_dragDirection; QRect g = geometry(); switch (dir) { case ResizeDirection::NW: g.setTopLeft(QCursor::pos()); break; case ResizeDirection::NE: g.setTopRight(QCursor::pos()); break; case ResizeDirection::SW: g.setBottomLeft(QCursor::pos()); break; case ResizeDirection::SE: g.setBottomRight(QCursor::pos()); break; case ResizeDirection::N: g.setTop(QCursor::pos().y()); break; case ResizeDirection::E: g.setRight(QCursor::pos().x()); break; case ResizeDirection::S: g.setBottom(QCursor::pos().y()); break; case ResizeDirection::W: g.setLeft(QCursor::pos().x()); break; case ResizeDirection::Count: break; } setGeometry(g); } if (dir != ResizeDirection::Count) { setCursor(shapes[(int)dir]); QObjectList children = this->children(); for (int i = 0; i < children.size(); ++i) { if (QWidget *w = qobject_cast(children.at(i))) { if (!w->testAttribute(Qt::WA_SetCursor)) { w->setCursor(Qt::ArrowCursor); } } } } else { unsetCursor(); } } else if (event->type() == QEvent::MouseButtonPress) { ResizeDirection dir = checkResize(); m_dragStartCursor = QCursor::pos(); m_dragStartGeometry = geometry(); if (dir == ResizeDirection::Count) m_dragReady = true; else m_dragDirection = dir; } else if (event->type() == QEvent::NonClientAreaMouseButtonPress) { m_dragActive = true; m_dragReady = false; m_dragStartCursor = QCursor::pos(); m_dragStartGeometry = geometry(); QList toolWindows; foreach(ToolWindowManagerArea* tabWidget, findChildren()) { if (ToolWindowManager::managerOf(tabWidget) == m_manager) { toolWindows << tabWidget->toolWindows(); } } m_manager->startDrag(toolWindows, this); } else if (event->type() == QEvent::Move && m_dragActive) { m_manager->updateDragPosition(); m_moveTimeout->start(); } else if (event->type() == QEvent::Leave) { unsetCursor(); } else if (event->type() == QEvent::MouseButtonDblClick && titleRect().contains(mapFromGlobal(QCursor::pos()))) { if (isMaximized()) { showNormal(); } else { showMaximized(); } } } return QWidget::eventFilter(object, event); } void ToolWindowManagerWrapper::paintEvent(QPaintEvent *) { if (!m_floating || m_titleHeight == 0) return; { QStylePainter p(this); QStyleOptionFrame frameOptions; frameOptions.init(this); p.drawPrimitive(QStyle::PE_FrameDockWidget, frameOptions); // Title must be painted after the frame, since the areas overlap, and // the title may wish to extend out to all sides (eg. XP style) QStyleOptionDockWidget titlebarOptions; titlebarOptions.initFrom(this); titlebarOptions.rect = titleRect(); titlebarOptions.title = windowTitle(); titlebarOptions.closable = true; titlebarOptions.movable = true; titlebarOptions.floatable = false; titlebarOptions.verticalTitleBar = false; p.drawControl(QStyle::CE_DockWidgetTitle, titlebarOptions); QStyleOptionToolButton buttonOpt; buttonOpt.initFrom(this); buttonOpt.iconSize = QSize(m_closeButtonSize, m_closeButtonSize); buttonOpt.subControls = 0; buttonOpt.activeSubControls = 0; buttonOpt.features = QStyleOptionToolButton::None; buttonOpt.arrowType = Qt::NoArrow; buttonOpt.state = QStyle::State_Active|QStyle::State_Enabled|QStyle::State_AutoRaise; if (m_closeRect.contains(mapFromGlobal(QCursor::pos()))) { buttonOpt.state |= QStyle::State_MouseOver|QStyle::State_Raised; } buttonOpt.rect = m_closeRect; buttonOpt.icon = m_closeIcon; if (style()->styleHint(QStyle::SH_DockWidget_ButtonsHaveFrame, 0, this)) { style()->drawPrimitive(QStyle::PE_PanelButtonTool, &buttonOpt, &p, this); } style()->drawComplexControl(QStyle::CC_ToolButton, &buttonOpt, &p, this); } } void ToolWindowManagerWrapper::resizeEvent(QResizeEvent *) { QStyleOptionDockWidget option; option.initFrom(this); option.rect = titleRect(); option.closable = true; option.movable = true; option.floatable = true; m_closeRect = style()->subElementRect(QStyle::SE_DockWidgetCloseButton, &option, this); m_closeIcon = style()->standardIcon(QStyle::SP_TitleBarCloseButton, &option, this); } QRect ToolWindowManagerWrapper::titleRect() { QRect ret; ret.setTopLeft(QPoint(m_frameWidth, m_frameWidth)); ret.setSize(QSize(geometry().width() - (m_frameWidth * 2), m_titleHeight)); return ret; } QVariantMap ToolWindowManagerWrapper::saveState() { if (layout()->count() > 2) { qWarning("too many children for wrapper"); return QVariantMap(); } if (isWindow() && layout()->count() == 0) { qWarning("empty top level wrapper"); return QVariantMap(); } QVariantMap result; result[QStringLiteral("geometry")] = saveGeometry().toBase64(); QSplitter* splitter = findChild(QString(), Qt::FindDirectChildrenOnly); if (splitter) { result[QStringLiteral("splitter")] = m_manager->saveSplitterState(splitter); } else { ToolWindowManagerArea* area = findChild(); if (area) { result[QStringLiteral("area")] = area->saveState(); } else if (layout()->count() > 0) { qWarning("unknown child"); return QVariantMap(); } } return result; } void ToolWindowManagerWrapper::restoreState(const QVariantMap &savedData) { restoreGeometry(QByteArray::fromBase64(savedData[QStringLiteral("geometry")].toByteArray())); if (layout()->count() > 1) { qWarning("wrapper is not empty"); return; } if (savedData.contains(QStringLiteral("splitter"))) { layout()->addWidget(m_manager->restoreSplitterState(savedData[QStringLiteral("splitter")].toMap())); } else if (savedData.contains(QStringLiteral("area"))) { ToolWindowManagerArea* area = m_manager->createArea(); area->restoreState(savedData[QStringLiteral("area")].toMap()); layout()->addWidget(area); } } void ToolWindowManagerWrapper::moveTimeout() { m_manager->updateDragPosition(); if (!m_manager->dragInProgress()) { m_moveTimeout->stop(); } } ToolWindowManagerWrapper::ResizeDirection ToolWindowManagerWrapper::checkResize() { if (m_titleHeight == 0) return ResizeDirection::Count; // check if we should offer to resize QRect rect = this->rect(); QPoint testPos = mapFromGlobal(QCursor::pos()); if (m_closeRect.contains(testPos)) return ResizeDirection::Count; const int resizeMargin = 4; if (rect.contains(testPos)) { // check corners first, then horizontal/vertical if (testPos.x() < rect.x() + resizeMargin*4 && testPos.y() < rect.y() + resizeMargin*4) { return ResizeDirection::NW; } else if (testPos.x() > rect.width() - resizeMargin*4 && testPos.y() < rect.y() + resizeMargin*4) { return ResizeDirection::NE; } else if (testPos.x() < rect.x() + resizeMargin*4 && testPos.y() > rect.height() - resizeMargin*4) { return ResizeDirection::SW; } else if (testPos.x() > rect.width() - resizeMargin*4 && testPos.y() > rect.height() - resizeMargin*4) { return ResizeDirection::SE; } else if (testPos.x() < rect.x() + resizeMargin) { return ResizeDirection::W; } else if (testPos.x() > rect.width() - resizeMargin) { return ResizeDirection::E; } else if (testPos.y() < rect.y() + resizeMargin) { return ResizeDirection::N; } else if (testPos.y() > rect.height() - resizeMargin) { return ResizeDirection::S; } } return ResizeDirection::Count; }