/*========================================================================= Library: CTK Copyright (c) Kitware Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.txt Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. =========================================================================*/ // Qt includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include // CTK includes #include "ctkPopupWidget_p.h" #define LEAVE_CLOSING_DELAY 100 // we don't want to be too fast to close #define ENTER_OPENING_DELAY 20 // we want to be responsive but allow "errors" #define DEFAULT_FADING_DURATION 333 // fast enough without being too slow // ------------------------------------------------------------------------- ctkPopupWidgetPrivate::ctkPopupWidgetPrivate(ctkPopupWidget& object) :Superclass(object) { this->Active = false; this->AutoShow = true; this->AutoHide = true; } // ------------------------------------------------------------------------- ctkPopupWidgetPrivate::~ctkPopupWidgetPrivate() { } // ------------------------------------------------------------------------- void ctkPopupWidgetPrivate::init() { Q_Q(ctkPopupWidget); this->setParent(q); this->Superclass::init(); q->setActive(true); } // ------------------------------------------------------------------------- QWidget* ctkPopupWidgetPrivate::mouseOver() { Q_Q(ctkPopupWidget); QWidget* widgetUnderCursor = this->Superclass::mouseOver(); if (widgetUnderCursor && !this->focusWidgets(true).contains(widgetUnderCursor)) { widgetUnderCursor->installEventFilter(q); } return widgetUnderCursor; } // ------------------------------------------------------------------------- bool ctkPopupWidgetPrivate::eventFilter(QObject* obj, QEvent* event) { Q_Q(ctkPopupWidget); QWidget* widget = qobject_cast(obj); // Here are the application events, it's a lot of events, so we need to be // careful to be fast. if (event->type() == QEvent::ApplicationDeactivate) { // We wait to see if there is no other window being active QTimer::singleShot(0, this, SLOT(onApplicationDeactivate())); } else if (event->type() == QEvent::ApplicationActivate) { QTimer::singleShot(0, this, SLOT(updateVisibility())); } if (!this->BaseWidget) { return false; } if (event->type() == QEvent::Move && widget != this->BaseWidget) { if (widget->isAncestorOf(this->BaseWidget)) { QMoveEvent* moveEvent = dynamic_cast(event); QPoint topLeft = widget->parentWidget() ? widget->parentWidget()->mapToGlobal(moveEvent->pos()) : moveEvent->pos(); topLeft += this->BaseWidget->mapTo(widget, QPoint(0,0)); //q->move(q->pos() + moveEvent->pos() - moveEvent->oldPos()); QRect newBaseGeometry = this->baseGeometry(); newBaseGeometry.moveTopLeft(topLeft); QRect desiredGeometry = this->desiredOpenGeometry(newBaseGeometry); q->move(desiredGeometry.topLeft()); } else if (widget->isWindow() && widget->windowType() != Qt::ToolTip && widget->windowType() != Qt::Popup) { QTimer::singleShot(0, this, SLOT(updateVisibility())); } } else if (event->type() == QEvent::Resize) { if (widget->isWindow() && widget != this->BaseWidget->window() && widget->windowType() != Qt::ToolTip && widget->windowType() != Qt::Popup) { QTimer::singleShot(0, this, SLOT(updateVisibility())); } } else if (event->type() == QEvent::WindowStateChange && widget != this->BaseWidget->window() && widget->windowType() != Qt::ToolTip && widget->windowType() != Qt::Popup) { QTimer::singleShot(0, this, SLOT(updateVisibility())); } else if ((event->type() == QEvent::WindowActivate || event->type() == QEvent::WindowDeactivate) && widget == this->BaseWidget->window()) { QTimer::singleShot(0, this, SLOT(updateVisibility())); } return false; } // ------------------------------------------------------------------------- void ctkPopupWidgetPrivate::onApplicationDeactivate() { // Still no active window, that means the user now is controlling another // application, we have no control over when the other app moves over the // popup, so we hide the popup as it would show on top of the other app. if (!qApp->activeWindow()) { this->temporarilyHiddenOn(); } } // ------------------------------------------------------------------------- void ctkPopupWidgetPrivate::updateVisibility() { Q_Q(ctkPopupWidget); // If the BaseWidget window is active, then there is no reason to cover the // popup. if (!this->BaseWidget || // the popupwidget active window is not active (!this->BaseWidget->window()->isActiveWindow() && // and no other active window (!qApp->activeWindow() || // or the active window is a popup/tooltip (qApp->activeWindow()->windowType() != Qt::ToolTip && qApp->activeWindow()->windowType() != Qt::Popup)))) { foreach(QWidget* topLevelWidget, qApp->topLevelWidgets()) { // If there is at least 1 window (active or not) that covers the popup, // then we ensure the popup is hidden. // We have no way of knowing which toplevel is over (z-order) which one, // it is an OS specific information. // Of course, tooltips and popups don't count as covering windows. if (topLevelWidget->isVisible() && !(topLevelWidget->windowState() & Qt::WindowMinimized) && topLevelWidget->windowType() != Qt::ToolTip && topLevelWidget->windowType() != Qt::Popup && topLevelWidget != (this->BaseWidget ? this->BaseWidget->window() : 0) && topLevelWidget->frameGeometry().intersects(q->geometry())) { //qDebug() << "hide" << q << "because of: " << topLevelWidget // << " with windowType: " << topLevelWidget->windowType() // << topLevelWidget->isVisible() // << (this->BaseWidget ? this->BaseWidget->window() : 0) // << topLevelWidget->frameGeometry(); this->temporarilyHiddenOn(); return; } } } // If the base widget is hidden or minimized, we don't want to restore the // popup. if (this->BaseWidget && (!this->BaseWidget->isVisible() || this->BaseWidget->window()->windowState() & Qt::WindowMinimized)) { return; } // Restore the visibility of the popup if it was hidden this->temporarilyHiddenOff(); } // ------------------------------------------------------------------------- void ctkPopupWidgetPrivate::temporarilyHiddenOn() { Q_Q(ctkPopupWidget); if (!this->AutoHide && (q->isVisible() || this->isOpening()) && !(q->isHidden() || this->isClosing())) { this->setProperty("forcedClosed", this->isOpening() ? 2 : 1); } this->currentAnimation()->stop(); this->hideAll(); } // ------------------------------------------------------------------------- void ctkPopupWidgetPrivate::temporarilyHiddenOff() { Q_Q(ctkPopupWidget); int forcedClosed = this->property("forcedClosed").toInt(); if (forcedClosed > 0) { q->show(); if (forcedClosed == 2) { emit q->popupOpened(true); } this->setProperty("forcedClosed", 0); } else { q->updatePopup(); } } // ------------------------------------------------------------------------- // Qt::FramelessWindowHint is required on Windows for Translucent background // Qt::Toolip is preferred to Qt::Popup as it would close itself at the first // click outside the widget (typically a click in the BaseWidget) ctkPopupWidget::ctkPopupWidget(QWidget* parentWidget) : Superclass(new ctkPopupWidgetPrivate(*this), parentWidget) { Q_D(ctkPopupWidget); d->init(); } // ------------------------------------------------------------------------- ctkPopupWidget::~ctkPopupWidget() { } // ------------------------------------------------------------------------- bool ctkPopupWidget::isActive()const { Q_D(const ctkPopupWidget); return d->Active; } // ------------------------------------------------------------------------- void ctkPopupWidget::setActive(bool active) { Q_D(ctkPopupWidget); if (active == d->Active) { return; } d->Active = active; if (d->Active) { if (d->BaseWidget) { d->BaseWidget->installEventFilter(this); } d->PopupPixmapWidget->installEventFilter(this); qApp->installEventFilter(d); } else // not active { if (d->BaseWidget) { d->BaseWidget->removeEventFilter(this); } d->PopupPixmapWidget->removeEventFilter(this); qApp->removeEventFilter(d); } } // ------------------------------------------------------------------------- void ctkPopupWidget::setBaseWidget(QWidget* widget) { Q_D(ctkPopupWidget); if (d->BaseWidget) { d->BaseWidget->removeEventFilter(this); } this->Superclass::setBaseWidget(widget); if (d->BaseWidget && d->Active) { d->BaseWidget->installEventFilter(this); } QTimer::singleShot(ENTER_OPENING_DELAY, this, SLOT(updatePopup())); } // ------------------------------------------------------------------------- bool ctkPopupWidget::autoShow()const { Q_D(const ctkPopupWidget); return d->AutoShow; } // ------------------------------------------------------------------------- void ctkPopupWidget::setAutoShow(bool mode) { Q_D(ctkPopupWidget); d->AutoShow = mode; QTimer::singleShot(ENTER_OPENING_DELAY, this, SLOT(updatePopup())); } // ------------------------------------------------------------------------- bool ctkPopupWidget::autoHide()const { Q_D(const ctkPopupWidget); return d->AutoHide; } // ------------------------------------------------------------------------- void ctkPopupWidget::setAutoHide(bool mode) { Q_D(ctkPopupWidget); d->AutoHide = mode; QTimer::singleShot(LEAVE_CLOSING_DELAY, this, SLOT(updatePopup())); } // ------------------------------------------------------------------------- void ctkPopupWidget::onEffectFinished() { Q_D(ctkPopupWidget); bool wasClosing = d->wasClosing(); this->Superclass::onEffectFinished(); if (wasClosing) { /// restore the AutoShow if needed. if (!this->property("AutoShowOnClose").isNull()) { d->AutoShow = this->property("AutoShowOnClose").toBool(); this->setProperty("AutoShowOnClose", QVariant()); } } } // -------------------------------------------------------------------------- void ctkPopupWidget::leaveEvent(QEvent* event) { QTimer::singleShot(LEAVE_CLOSING_DELAY, this, SLOT(updatePopup())); this->Superclass::leaveEvent(event); } // -------------------------------------------------------------------------- void ctkPopupWidget::enterEvent(QEvent* event) { QTimer::singleShot(ENTER_OPENING_DELAY, this, SLOT(updatePopup())); this->Superclass::enterEvent(event); } // -------------------------------------------------------------------------- bool ctkPopupWidget::eventFilter(QObject* obj, QEvent* event) { Q_D(ctkPopupWidget); // Here we listen to PopupPixmapWidget, BaseWidget and ctkPopupWidget // children popups that were under the mouse switch(event->type()) { case QEvent::Move: { if (obj != d->BaseWidget) { break; } QMoveEvent* moveEvent = dynamic_cast(event); QRect newBaseGeometry = d->baseGeometry(); newBaseGeometry.moveTopLeft(d->mapToGlobal(moveEvent->pos())); QRect desiredGeometry = d->desiredOpenGeometry(newBaseGeometry); this->move(desiredGeometry.topLeft()); //this->move(this->pos() + moveEvent->pos() - moveEvent->oldPos()); this->update(); break; } case QEvent::Hide: case QEvent::Close: // if the mouse was in a base widget child popup, then when we leave // the popup we want to check if it needs to be closed. if (obj != d->BaseWidget) { if (obj != d->PopupPixmapWidget && qobject_cast(obj)->windowType() == Qt::Popup) { obj->removeEventFilter(this); QTimer::singleShot(LEAVE_CLOSING_DELAY, this, SLOT(updatePopup())); } break; } d->temporarilyHiddenOn(); break; case QEvent::Show: if (obj != d->BaseWidget) { break; } this->setGeometry(d->desiredOpenGeometry()); d->temporarilyHiddenOff(); break; case QEvent::Resize: if (obj != d->BaseWidget || !(d->Alignment & Qt::AlignJustify || (d->Alignment & Qt::AlignTop && d->Alignment & Qt::AlignBottom)) || !(d->isOpening() || this->isVisible())) { break; } // TODO: bug when the effect is WindowOpacityFadeEffect this->setGeometry(d->desiredOpenGeometry()); break; case QEvent::Enter: if ( d->currentAnimation()->state() == QAbstractAnimation::Stopped ) { // Maybe the user moved the mouse on the widget by mistake, don't open // the popup instantly... QTimer::singleShot(ENTER_OPENING_DELAY, this, SLOT(updatePopup())); } else { // ... except if the popup is closing, we want to reopen it as sooon as // possible. this->updatePopup(); } break; case QEvent::Leave: // Don't listen to base widget children that are popups as what // matters here is their close event instead if (obj != d->BaseWidget && obj != d->PopupPixmapWidget && qobject_cast(obj)->windowType() == Qt::Popup) { break; } // The mouse might have left the area that keeps the popup open QTimer::singleShot(LEAVE_CLOSING_DELAY, this, SLOT(updatePopup())); if (obj != d->BaseWidget && obj != d->PopupPixmapWidget) { obj->removeEventFilter(this); } break; default: break; } return this->QObject::eventFilter(obj, event); } // -------------------------------------------------------------------------- void ctkPopupWidget::updatePopup() { Q_D(ctkPopupWidget); // Querying mouseOver can be slow, don't do it if not needed. QWidget* mouseOver = (d->AutoShow || d->AutoHide) ? d->mouseOver() : 0; if (d->AutoShow && mouseOver) { this->showPopup(); } else if (d->AutoHide && !mouseOver) { this->hidePopup(); } } // -------------------------------------------------------------------------- void ctkPopupWidget::hidePopup() { Q_D(ctkPopupWidget); // just in case it was set. this->setProperty("forcedClosed", 0); this->Superclass::hidePopup(); } // -------------------------------------------------------------------------- void ctkPopupWidget::pinPopup(bool pin) { Q_D(ctkPopupWidget); this->setAutoHide(!pin); if (pin) { this->showPopup(); } else { // When closing, we don't want to inadvertently re-open the menu. this->setProperty("AutoShowOnClose", this->autoShow()); d->AutoShow = false; this->hidePopup(); } }