diff --git a/ctk/ctk.pro b/ctk/ctk.pro new file mode 100644 index 0000000..cf4d827 --- /dev/null +++ b/ctk/ctk.pro @@ -0,0 +1,48 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2017-02-26T10:51:14 +# +#------------------------------------------------- + +QT += widgets + +TARGET = ctk +TEMPLATE = lib +CONFIG += staticlib c++14 + +QMAKE_CXXFLAGS += /std:c++17 + +INCLUDEPATH += C:\prog\include \ +C:\Prog\include\pgsql \ +C:\VSproj\boost32\include\boost-1_65_1 + +DEFINES += WIN32_LEAN_AND_MEAN NOMINMAX + + +# The following define makes your compiler emit warnings if you use +# any feature of Qt which as been marked as deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS + +# You can also make your code fail to compile if you use deprecated APIs. +# In order to do so, uncomment the following line. +# You can also select to disable deprecated APIs only up to a certain version of Qt. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += ctkBasePopupWidget.cpp \ + ctkPopupWidget.cpp \ + ctkWidgetsUtils.cpp + + +HEADERS += ctkBasePopupWidget.h \ + ctkBasePopupWidget_p.h \ + ctkPopupWidget.h \ + ctkPopupWidget_p.h \ + ctkWidgetsUtils.h \ + ctkWidgetsExport.h + +unix { + target.path = /usr/lib + INSTALLS += target +} diff --git a/ctk/ctkBasePopupWidget.cpp b/ctk/ctkBasePopupWidget.cpp new file mode 100644 index 0000000..2d03489 --- /dev/null +++ b/ctk/ctkBasePopupWidget.cpp @@ -0,0 +1,843 @@ +/*========================================================================= + + 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.commontk.org/LICENSE + + 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 "ctkBasePopupWidget_p.h" +#include "ctkWidgetsUtils.h" + +// ------------------------------------------------------------------------- +QGradient* duplicateGradient(const QGradient* gradient) +{ + QGradient* newGradient = 0; + switch (gradient->type()) + { + case QGradient::LinearGradient: + { + const QLinearGradient* linearGradient = static_cast(gradient); + newGradient = new QLinearGradient(linearGradient->start(), linearGradient->finalStop()); + break; + } + case QGradient::RadialGradient: + { + const QRadialGradient* radialGradient = static_cast(gradient); + newGradient = new QRadialGradient(radialGradient->center(), radialGradient->radius()); + break; + } + case QGradient::ConicalGradient: + { + const QConicalGradient* conicalGradient = static_cast(gradient); + newGradient = new QConicalGradient(conicalGradient->center(), conicalGradient->angle()); + break; + } + default: + break; + } + if (!newGradient) + { + Q_ASSERT(gradient->type() != QGradient::NoGradient); + return newGradient; + } + newGradient->setCoordinateMode(gradient->coordinateMode()); + newGradient->setSpread(gradient->spread()); + newGradient->setStops(gradient->stops()); + return newGradient; +} + +// ------------------------------------------------------------------------- +ctkBasePopupWidgetPrivate::ctkBasePopupWidgetPrivate(ctkBasePopupWidget& object) + :q_ptr(&object) +{ + this->Effect = ctkBasePopupWidget::ScrollEffect; + this->EffectDuration = 333; // in ms + this->EffectAlpha = 1.; + this->AlphaAnimation = 0; + this->ForcedTranslucent = false; + this->ScrollAnimation = 0; + this->PopupPixmapWidget = 0; + // Geometry attributes + this->Alignment = Qt::AlignJustify | Qt::AlignBottom; + this->Orientations = Qt::Vertical; + this->VerticalDirection = ctkBasePopupWidget::TopToBottom; + this->HorizontalDirection = Qt::LeftToRight; +} + +// ------------------------------------------------------------------------- +ctkBasePopupWidgetPrivate::~ctkBasePopupWidgetPrivate() +{ +} + +// ------------------------------------------------------------------------- +void ctkBasePopupWidgetPrivate::init() +{ + Q_Q(ctkBasePopupWidget); + // By default, Tooltips are shown only on active windows. In a popup widget + // case, we sometimes aren't the active window but we still would like to + // show the children tooltips. + q->setAttribute(Qt::WA_AlwaysShowToolTips, true); + //q->setAttribute(Qt::WA_MacAlwaysShowToolWindow, true); + + this->AlphaAnimation = new QPropertyAnimation(q, "effectAlpha", q); + this->AlphaAnimation->setDuration(this->EffectDuration); + this->AlphaAnimation->setStartValue(0.); + this->AlphaAnimation->setEndValue(1.); + QObject::connect(this->AlphaAnimation, SIGNAL(finished()), + q, SLOT(onEffectFinished())); + + this->PopupPixmapWidget = new QLabel(q, Qt::ToolTip | Qt::FramelessWindowHint); + + this->ScrollAnimation = new QPropertyAnimation(q, "effectGeometry", q); + this->ScrollAnimation->setDuration(this->EffectDuration); + QObject::connect(this->ScrollAnimation, SIGNAL(finished()), + q, SLOT(onEffectFinished())); + QObject::connect(this->ScrollAnimation, SIGNAL(finished()), + this->PopupPixmapWidget, SLOT(hide())); + + q->setAnimationEffect(this->Effect); + q->setEasingCurve(QEasingCurve::OutCubic); + q->setBaseWidget(q->parentWidget()); +} + +// ------------------------------------------------------------------------- +QPropertyAnimation* ctkBasePopupWidgetPrivate::currentAnimation()const +{ + return this->Effect == ctkBasePopupWidget::ScrollEffect ? + this->ScrollAnimation : this->AlphaAnimation; +} + +// ------------------------------------------------------------------------- +bool ctkBasePopupWidgetPrivate::isOpening()const +{ + return this->currentAnimation()->state() == QAbstractAnimation::Running && + this->currentAnimation()->direction() == QAbstractAnimation::Forward; +} + +// ------------------------------------------------------------------------- +bool ctkBasePopupWidgetPrivate::isClosing()const +{ + return this->currentAnimation()->state() == QAbstractAnimation::Running && + this->currentAnimation()->direction() == QAbstractAnimation::Backward; +} + +// ------------------------------------------------------------------------- +bool ctkBasePopupWidgetPrivate::wasClosing()const +{ + Q_Q(const ctkBasePopupWidget); + return qobject_cast(q->sender())->direction() + == QAbstractAnimation::Backward; +} + +// ------------------------------------------------------------------------- +QWidgetList ctkBasePopupWidgetPrivate::focusWidgets(bool onlyVisible)const +{ + Q_Q(const ctkBasePopupWidget); + QWidgetList res; + if (!onlyVisible || q->isVisible()) + { + res << const_cast(q); + } + if (!this->BaseWidget.isNull() && (!onlyVisible || this->BaseWidget->isVisible())) + { + res << this->BaseWidget; + } + if (this->PopupPixmapWidget && (!onlyVisible || this->PopupPixmapWidget->isVisible())) + { + res << this->PopupPixmapWidget; + } + return res; +} + +// ------------------------------------------------------------------------- +QWidget* ctkBasePopupWidgetPrivate::mouseOver() +{ + QList widgets = this->focusWidgets(true); + foreach(QWidget* widget, widgets) + { + if (widget->underMouse()) + { + return widget; + } + } + // Warning QApplication::widgetAt(QCursor::pos()) can be a bit slow... + const QPoint pos = QCursor::pos(); + QWidget* widgetUnderCursor = qApp->widgetAt(pos); + foreach(const QWidget* focusWidget, widgets) + { + if (this->isAncestorOf(focusWidget, widgetUnderCursor) && + // Ignore when cursor is above a title bar of a focusWidget, underMouse + // wouldn't have return false, but QApplication::widgetAt would return + // the widget + (focusWidget != widgetUnderCursor || + QRect(QPoint(0,0), focusWidget->size()).contains( + focusWidget->mapFromGlobal(pos)))) + { + return widgetUnderCursor; + } + } + return 0; +} + +// ------------------------------------------------------------------------- +bool ctkBasePopupWidgetPrivate::isAncestorOf(const QWidget* ancestor, const QWidget* child)const +{ + while (child) + { + if (child == ancestor) + { + return true; + } + child = child->parentWidget(); + } + return false; +} + +// ------------------------------------------------------------------------- +void ctkBasePopupWidgetPrivate::setupPopupPixmapWidget() +{ + Q_Q(ctkBasePopupWidget); + this->PopupPixmapWidget->setAlignment(this->pixmapAlignment()); + QPixmap pixmap; + if (q->testAttribute(Qt::WA_TranslucentBackground)) + { + // only QImage handle transparency correctly + QImage image(q->geometry().size(), QImage::Format_ARGB32); + image.fill(0); + q->render(&image); + pixmap = QPixmap::fromImage(image); + } + else + { + pixmap = QPixmap::fromImage( + ctk::grabWidget(q, QRect(QPoint(0,0), q->geometry().size()))); + } + this->PopupPixmapWidget->setPixmap(pixmap); + this->PopupPixmapWidget->setAttribute( + Qt::WA_TranslucentBackground, q->testAttribute(Qt::WA_TranslucentBackground)); + this->PopupPixmapWidget->setWindowOpacity(q->windowOpacity()); +} + +// ------------------------------------------------------------------------- +Qt::Alignment ctkBasePopupWidgetPrivate::pixmapAlignment()const +{ + Qt::Alignment alignment; + if (this->VerticalDirection == ctkBasePopupWidget::TopToBottom) + { + alignment |= Qt::AlignBottom; + } + else// if (this->VerticalDirection == ctkBasePopupWidget::BottomToTop) + { + alignment |= Qt::AlignTop; + } + + if (this->HorizontalDirection == Qt::LeftToRight) + { + alignment |= Qt::AlignRight; + } + else// if (this->VerticalDirection == ctkBasePopupWidget::BottomToTop) + { + alignment |= Qt::AlignLeft; + } + return alignment; +} + +// ------------------------------------------------------------------------- +QRect ctkBasePopupWidgetPrivate::closedGeometry()const +{ + Q_Q(const ctkBasePopupWidget); + return this->closedGeometry(q->geometry()); +} + +// ------------------------------------------------------------------------- +QRect ctkBasePopupWidgetPrivate::closedGeometry(QRect openGeom)const +{ + if (this->Orientations & Qt::Vertical) + { + if (this->VerticalDirection == ctkBasePopupWidget::BottomToTop) + { + openGeom.moveTop(openGeom.bottom()); + } + openGeom.setHeight(0); + } + if (this->Orientations & Qt::Horizontal) + { + if (this->HorizontalDirection == Qt::RightToLeft) + { + openGeom.moveLeft(openGeom.right()); + } + openGeom.setWidth(0); + } + return openGeom; +} + +// ------------------------------------------------------------------------- +QRect ctkBasePopupWidgetPrivate::baseGeometry()const +{ + if (this->BaseWidget.isNull()) + { + return QRect(); + } + return QRect(this->mapToGlobal(this->BaseWidget->geometry().topLeft()), + this->BaseWidget->size()); +} + +// ------------------------------------------------------------------------- +QPoint ctkBasePopupWidgetPrivate::mapToGlobal(const QPoint& baseWidgetPoint)const +{ + QPoint mappedPoint = baseWidgetPoint; + if (!this->BaseWidget.isNull() && this->BaseWidget->parentWidget()) + { + mappedPoint = this->BaseWidget->parentWidget()->mapToGlobal(mappedPoint); + } + return mappedPoint; +} + +// ------------------------------------------------------------------------- +QRect ctkBasePopupWidgetPrivate::desiredOpenGeometry()const +{ + return this->desiredOpenGeometry(this->baseGeometry()); +} + +// ------------------------------------------------------------------------- +QRect ctkBasePopupWidgetPrivate::desiredOpenGeometry(QRect baseGeometry)const +{ + Q_Q(const ctkBasePopupWidget); + QSize size = q->size(); + if (!q->testAttribute(Qt::WA_WState_Created)) + { + size = q->sizeHint(); + } + + if (baseGeometry.isNull()) + { + return QRect(q->pos(), size); + } + + QRect geometry; + if (this->Alignment & Qt::AlignJustify) + { + if (this->Orientations & Qt::Vertical) + { + size.setWidth(baseGeometry.width()); + } + } + if (this->Alignment & Qt::AlignTop && + this->Alignment & Qt::AlignBottom) + { + size.setHeight(baseGeometry.height()); + } + + geometry.setSize(size); + + QPoint topLeft = baseGeometry.topLeft(); + QPoint bottomRight = baseGeometry.bottomRight(); + + if (this->Alignment & Qt::AlignLeft) + { + if (this->HorizontalDirection == Qt::LeftToRight) + { + geometry.moveLeft(topLeft.x()); + } + else + { + geometry.moveRight(topLeft.x() - 1); + } + } + else if (this->Alignment & Qt::AlignRight) + { + if (this->HorizontalDirection == Qt::LeftToRight) + { + geometry.moveLeft(bottomRight.x() + 1); + } + else + { + geometry.moveRight(bottomRight.x()); + } + } + else if (this->Alignment & Qt::AlignHCenter) + { + geometry.moveLeft((topLeft.x() + bottomRight.x()) / 2 - size.width() / 2); + } + else if (this->Alignment & Qt::AlignJustify) + { + geometry.moveLeft(topLeft.x()); + } + + if (this->Alignment & Qt::AlignTop) + { + if (this->VerticalDirection == ctkBasePopupWidget::TopToBottom) + { + geometry.moveTop(topLeft.y()); + } + else + { + geometry.moveBottom(topLeft.y() - 1); + } + } + else if (this->Alignment & Qt::AlignBottom) + { + if (this->VerticalDirection == ctkBasePopupWidget::TopToBottom) + { + geometry.moveTop(bottomRight.y() + 1); + } + else + { + geometry.moveBottom(bottomRight.y()); + } + } + else if (this->Alignment & Qt::AlignVCenter) + { + geometry.moveTop((topLeft.y() + bottomRight.y()) / 2 - size.height() / 2); + } + return geometry; +} + +// ------------------------------------------------------------------------- +void ctkBasePopupWidgetPrivate::hideAll() +{ + Q_Q(ctkBasePopupWidget); + + // It is possible to have the popup widget not being a popup but inside + // a layout: maybe the popup has been pin-down in a way that it gets parented + // In that case, there is no reason to hide the popup. + if (!(q->windowFlags() & PopupWindowType)) + { + return; + } + + // Before hiding, transfer the active window flag to its parent, this will + // prevent the application to send a ApplicationDeactivate signal that + // doesn't need to be done. +#ifndef Q_OS_MAC // See Slicer issue #3850 + if (q->isActiveWindow() && !this->BaseWidget.isNull()) + { + qApp->setActiveWindow(this->BaseWidget->window()); + } +#endif + + q->hide(); + this->PopupPixmapWidget->hide(); + + // If there is a popup open in the ctkBasePopupWidget children, then hide it + // as well so we don't have a popup open while the ctkBasePopupWidget is hidden. + QPointer activePopupWidget = qApp->activePopupWidget(); + if (activePopupWidget && this->isAncestorOf(q, activePopupWidget)) + { + activePopupWidget->close(); + } +} + +// ------------------------------------------------------------------------- +// 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) +ctkBasePopupWidget::ctkBasePopupWidget(QWidget* parentWidget) + //: Superclass(QApplication::desktop()->screen(QApplication::desktop()->screenNumber(parentWidget)), + : Superclass(parentWidget, + PopupWindowType | Qt::FramelessWindowHint) + , d_ptr(new ctkBasePopupWidgetPrivate(*this)) +{ + Q_D(ctkBasePopupWidget); + d->init(); +} + +// ------------------------------------------------------------------------- +ctkBasePopupWidget::ctkBasePopupWidget(ctkBasePopupWidgetPrivate* pimpl, QWidget* parentWidget) + //: //Superclass(QApplication::desktop()->screen(QApplication::desktop()->screenNumber(parentWidget)), + : Superclass(parentWidget, + PopupWindowType | Qt::FramelessWindowHint) + , d_ptr(pimpl) +{ +} + +// ------------------------------------------------------------------------- +ctkBasePopupWidget::~ctkBasePopupWidget() +{ +} + +// ------------------------------------------------------------------------- +QWidget* ctkBasePopupWidget::baseWidget()const +{ + Q_D(const ctkBasePopupWidget); + return d->BaseWidget; +} + +// ------------------------------------------------------------------------- +void ctkBasePopupWidget::setBaseWidget(QWidget* widget) +{ + Q_D(ctkBasePopupWidget); + if (!d->BaseWidget.isNull()) + { + //disconnect(d->BaseWidget, SIGNAL(destroyed(QObject*)), + // this, SLOT(onBaseWidgetDestroyed())); + } + d->BaseWidget = widget; + if (!d->BaseWidget.isNull()) + { + //connect(d->BaseWidget, SIGNAL(destroyed(QObject*)), + // this, SLOT(onBaseWidgetDestroyed())); + } +} + +// ------------------------------------------------------------------------- +void ctkBasePopupWidget::onBaseWidgetDestroyed() +{ + Q_D(ctkBasePopupWidget); + d->hideAll(); + this->setBaseWidget(0); + // could be a property. + this->deleteLater(); +} + +// ------------------------------------------------------------------------- +ctkBasePopupWidget::AnimationEffect ctkBasePopupWidget::animationEffect()const +{ + Q_D(const ctkBasePopupWidget); + return d->Effect; +} + +// ------------------------------------------------------------------------- +void ctkBasePopupWidget::setAnimationEffect(ctkBasePopupWidget::AnimationEffect effect) +{ + Q_D(ctkBasePopupWidget); + /// TODO: handle the case where there is an animation running + d->Effect = effect; +} + +// ------------------------------------------------------------------------- +int ctkBasePopupWidget::effectDuration()const +{ + Q_D(const ctkBasePopupWidget); + return d->EffectDuration; +} + +// ------------------------------------------------------------------------- +void ctkBasePopupWidget::setEffectDuration(int duration) +{ + Q_D(ctkBasePopupWidget); + d->EffectDuration = duration; + d->AlphaAnimation->setDuration(d->EffectDuration); + d->ScrollAnimation->setDuration(d->EffectDuration); +} + +// ------------------------------------------------------------------------- +QEasingCurve::Type ctkBasePopupWidget::easingCurve()const +{ + Q_D(const ctkBasePopupWidget); + return d->AlphaAnimation->easingCurve().type(); +} + +// ------------------------------------------------------------------------- +void ctkBasePopupWidget::setEasingCurve(QEasingCurve::Type easingCurve) +{ + Q_D(ctkBasePopupWidget); + d->AlphaAnimation->setEasingCurve(easingCurve); + d->ScrollAnimation->setEasingCurve(easingCurve); +} + +// ------------------------------------------------------------------------- +Qt::Alignment ctkBasePopupWidget::alignment()const +{ + Q_D(const ctkBasePopupWidget); + return d->Alignment; +} + +// ------------------------------------------------------------------------- +void ctkBasePopupWidget::setAlignment(Qt::Alignment alignment) +{ + Q_D(ctkBasePopupWidget); + d->Alignment = alignment; +} + +// ------------------------------------------------------------------------- +Qt::Orientations ctkBasePopupWidget::orientation()const +{ + Q_D(const ctkBasePopupWidget); + return d->Orientations; +} + +// ------------------------------------------------------------------------- +void ctkBasePopupWidget::setOrientation(Qt::Orientations orientations) +{ + Q_D(ctkBasePopupWidget); + d->Orientations = orientations; +} + +// ------------------------------------------------------------------------- +ctkBasePopupWidget::VerticalDirection ctkBasePopupWidget::verticalDirection()const +{ + Q_D(const ctkBasePopupWidget); + return d->VerticalDirection; +} + +// ------------------------------------------------------------------------- +void ctkBasePopupWidget::setVerticalDirection(ctkBasePopupWidget::VerticalDirection verticalDirection) +{ + Q_D(ctkBasePopupWidget); + d->VerticalDirection = verticalDirection; +} + +// ------------------------------------------------------------------------- +Qt::LayoutDirection ctkBasePopupWidget::horizontalDirection()const +{ + Q_D(const ctkBasePopupWidget); + return d->HorizontalDirection; +} + +// ------------------------------------------------------------------------- +void ctkBasePopupWidget::setHorizontalDirection(Qt::LayoutDirection horizontalDirection) +{ + Q_D(ctkBasePopupWidget); + d->HorizontalDirection = horizontalDirection; +} + +// ------------------------------------------------------------------------- +void ctkBasePopupWidget::onEffectFinished() +{ + Q_D(ctkBasePopupWidget); + if (d->ForcedTranslucent) + { + d->ForcedTranslucent = false; + this->setAttribute(Qt::WA_TranslucentBackground, false); + } + if (d->wasClosing()) + { + d->hideAll(); + emit this->popupOpened(false); + } + else + { + this->show(); + emit this->popupOpened(true); + } +} + +// ------------------------------------------------------------------------- +bool ctkBasePopupWidget::event(QEvent* event) +{ + switch(event->type()) + { + case QEvent::ParentChange: + // For now the base widget is the parent widget + this->setBaseWidget(this->parentWidget()); + break; + default: + break; + } + return this->Superclass::event(event); +} + +// ------------------------------------------------------------------------- +void ctkBasePopupWidget::paintEvent(QPaintEvent* event) +{ + Q_D(ctkBasePopupWidget); + Q_UNUSED(event); + + QPainter painter(this); + QBrush brush = this->palette().window(); + if (brush.style() == Qt::LinearGradientPattern || + brush.style() == Qt::ConicalGradientPattern || + brush.style() == Qt::RadialGradientPattern) + { + QGradient* newGradient = duplicateGradient(brush.gradient()); + QGradientStops stops; + foreach(QGradientStop stop, newGradient->stops()) + { + stop.second.setAlpha(stop.second.alpha() * d->EffectAlpha); + stops.push_back(stop); + } + newGradient->setStops(stops); + brush = QBrush(*newGradient); + delete newGradient; + } + else + { + QColor color = brush.color(); + color.setAlpha(color.alpha() * d->EffectAlpha); + brush.setColor(color); + } + //QColor semiTransparentColor = this->palette().window().color(); + //semiTransparentColor.setAlpha(d->CurrentAlpha); + painter.fillRect(this->rect(), brush); + painter.end(); + // Let the QFrame draw itself if needed + this->Superclass::paintEvent(event); +} + +// -------------------------------------------------------------------------- +void ctkBasePopupWidget::showPopup() +{ + Q_D(ctkBasePopupWidget); + + if ((this->isVisible() && + d->currentAnimation()->state() == QAbstractAnimation::Stopped) || + (!d->BaseWidget.isNull() && !d->BaseWidget->isVisible())) + { + return; + } + + // If the layout has never been activated, the widget doesn't know its + // minSize/maxSize and we then wouldn't know what's its true geometry. + if (this->layout() && !this->testAttribute(Qt::WA_WState_Created)) + { + this->layout()->activate(); + } + this->setGeometry(d->desiredOpenGeometry()); + /// Maybe the popup doesn't allow the desiredOpenGeometry if the widget + /// minimum size is larger than the desired size. + QRect openGeometry = this->geometry(); + QRect closedGeometry = d->closedGeometry(); + + d->currentAnimation()->setDirection(QAbstractAnimation::Forward); + + switch(d->Effect) + { + case WindowOpacityFadeEffect: + if (!this->testAttribute(Qt::WA_TranslucentBackground)) + { + d->ForcedTranslucent = true; + this->setAttribute(Qt::WA_TranslucentBackground, true); + } + this->show(); + break; + case ScrollEffect: + { + d->PopupPixmapWidget->setGeometry(closedGeometry); + d->ScrollAnimation->setStartValue(closedGeometry); + d->ScrollAnimation->setEndValue(openGeometry); + d->setupPopupPixmapWidget(); + d->PopupPixmapWidget->show(); + break; + } + default: + break; + } + switch(d->currentAnimation()->state()) + { + case QAbstractAnimation::Stopped: + d->currentAnimation()->start(); + break; + case QAbstractAnimation::Paused: + d->currentAnimation()->resume(); + break; + default: + case QAbstractAnimation::Running: + break; + } +} + +// -------------------------------------------------------------------------- +void ctkBasePopupWidget::hidePopup() +{ + Q_D(ctkBasePopupWidget); + + if (!this->isVisible() && + d->currentAnimation()->state() == QAbstractAnimation::Stopped) + { + return; + } + d->currentAnimation()->setDirection(QAbstractAnimation::Backward); + + QRect openGeometry = this->geometry(); + QRect closedGeometry = d->closedGeometry(); + + switch(d->Effect) + { + case WindowOpacityFadeEffect: + if (!this->testAttribute(Qt::WA_TranslucentBackground)) + { + d->ForcedTranslucent = true; + this->setAttribute(Qt::WA_TranslucentBackground, true); + } + break; + case ScrollEffect: + { + d->ScrollAnimation->setStartValue(closedGeometry); + d->ScrollAnimation->setEndValue(openGeometry); + d->setupPopupPixmapWidget(); + d->PopupPixmapWidget->setGeometry(this->geometry()); + d->PopupPixmapWidget->show(); + if (this->isActiveWindow()) + { + qApp->setActiveWindow(!d->BaseWidget.isNull() ? d->BaseWidget->window() : 0); + } + this->hide(); + break; + } + default: + break; + } + switch(d->currentAnimation()->state()) + { + case QAbstractAnimation::Stopped: + d->currentAnimation()->start(); + break; + case QAbstractAnimation::Paused: + d->currentAnimation()->resume(); + break; + default: + case QAbstractAnimation::Running: + break; + } +} + +// -------------------------------------------------------------------------- +double ctkBasePopupWidget::effectAlpha()const +{ + Q_D(const ctkBasePopupWidget); + return d->EffectAlpha; +} + +// -------------------------------------------------------------------------- +void ctkBasePopupWidget::setEffectAlpha(double alpha) +{ + Q_D(ctkBasePopupWidget); + d->EffectAlpha = alpha; + this->repaint(); +} + +// -------------------------------------------------------------------------- +QRect ctkBasePopupWidget::effectGeometry()const +{ + Q_D(const ctkBasePopupWidget); + return d->PopupPixmapWidget->geometry(); +} + +// -------------------------------------------------------------------------- +void ctkBasePopupWidget::setEffectGeometry(QRect newGeometry) +{ + Q_D(ctkBasePopupWidget); + d->PopupPixmapWidget->setGeometry(newGeometry); + d->PopupPixmapWidget->repaint(); +} diff --git a/ctk/ctkBasePopupWidget.h b/ctk/ctkBasePopupWidget.h new file mode 100644 index 0000000..ba86e68 --- /dev/null +++ b/ctk/ctkBasePopupWidget.h @@ -0,0 +1,225 @@ +/*========================================================================= + + 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.commontk.org/LICENSE + + 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. + +=========================================================================*/ + +#ifndef __ctkBasePopupWidget_h +#define __ctkBasePopupWidget_h + +// Qt includes +#include +#include +#include + +// CTK includes +#include "ctkWidgetsExport.h" + +class ctkBasePopupWidgetPrivate; + +/// \ingroup Widgets +/// ctkBasePopupWidget is a popup that opens under, above or on the side of +/// another widget (baseWidget() or its parent widget by default). +/// The children (widgets and layout) of the popup define of the content +/// of the popup. Different effects can be applied during the opening or +/// closing of the popup. +/// See ctkPopupWidget for an automatic control of its opening and closing. +/// \sa baseWidget(), animationEffect, ctkPopupWidget +class CTK_WIDGETS_EXPORT ctkBasePopupWidget : public QFrame +{ + Q_OBJECT + + Q_ENUMS(AnimationEffect) + Q_ENUMS(VerticalDirection) + + /// This property controls the effect to apply when the popup is being + /// opened or closed. The total duration and the easing curve of the effect + /// are controlled by \a effectDuration and \easingCurve respectively. + /// ScrollEffect by default. + /// \sa AnimationEffect, animationEffect(), setAnimationEffect(), + /// effectDuration, easingCurve + Q_PROPERTY( AnimationEffect animationEffect READ animationEffect WRITE setAnimationEffect) + + /// The property controls the \a animationEffect duration in ms. + /// If the popup state (open or close) is being changed during the animation, + /// the active animation is stopped and a new animation is being created from + /// the current state (geometry, transparency...) to the new final state. + /// Default to 333ms + /// \sa effectDuration(), setEffectDuration(), animationEffect, easingCurve + Q_PROPERTY( int effectDuration READ effectDuration WRITE setEffectDuration); + + /// The property controls the behavior of the opening or closing curve of the + /// animation effect. + /// QEasingCurve::InOutQuad by default + /// \sa easingCurve(), setEasingCurve(), animationEffect, effectDuration + Q_PROPERTY( QEasingCurve::Type easingCurve READ easingCurve WRITE setEasingCurve); + + /// Where is the popup in relation to the BaseWidget + /// To vertically justify, use Qt::AlignTop | Qt::AlignBottom. + /// Qt::AlignJustify | Qt::AlignBottom by default + Q_PROPERTY( Qt::Alignment alignment READ alignment WRITE setAlignment); + + /// Direction of the scrolling effect, can be Qt::Vertical, Qt::Horizontal or + /// both Qt::Vertical|Qt::Horizontal. + /// Vertical by default + Q_PROPERTY( Qt::Orientations orientation READ orientation WRITE setOrientation); + + /// Control where the popup opens vertically. + /// TopToBottom by default + Q_PROPERTY( ctkBasePopupWidget::VerticalDirection verticalDirection READ verticalDirection WRITE setVerticalDirection); + + /// Control where the popup opens horizontally. + /// LeftToRight by default + Q_PROPERTY( Qt::LayoutDirection horizontalDirection READ horizontalDirection WRITE setHorizontalDirection); + +public: + typedef QFrame Superclass; + /// Although a popup widget is a top-level widget, if a parent is + /// passed the popup widget will be deleted when that parent is + /// destroyed (as with any other QObject). + /// ctkBasePopupWidget is a top-level widget (Qt::ToolTip), so + /// even if a parent is passed, the popup will display outside the possible + /// parent layout. + /// \sa baseWidget(). + explicit ctkBasePopupWidget(QWidget* parent = 0); + virtual ~ctkBasePopupWidget(); + + /// Widget the popup is attached to. It opens right under \a baseWidget + /// and if the ctkBasePopupWidget sizepolicy contains the growFlag/shrinkFlag, + /// it tries to resize itself to fit the same width of \a baseWidget. + /// By default, baseWidget is the parent widget. + /// \sa setBaseWidget() + QWidget* baseWidget()const; + + enum AnimationEffect + { + WindowOpacityFadeEffect = 0, + ScrollEffect, + FadeEffect + }; + + /// Return the animationEffect property value. + /// \sa animationEffect + AnimationEffect animationEffect()const; + /// Set the animationEffect property value. + /// \sa animationEffect + void setAnimationEffect(AnimationEffect effect); + + /// Return the effectDuration property value. + /// \sa effectDuration + int effectDuration()const; + /// Set the effectDuration property value. + /// \sa effectDuration + void setEffectDuration(int duration); + + /// Return the easingCurve property value. + /// \sa easingCurve + QEasingCurve::Type easingCurve()const; + /// Set the easingCurve property value. + /// \sa easingCurve + void setEasingCurve(QEasingCurve::Type easingCurve); + + /// Return the alignment property value. + /// \sa alignment + Qt::Alignment alignment()const; + /// Set the alignment property value. + /// \sa alignment + void setAlignment(Qt::Alignment alignment); + + /// Return the orientation property value. + /// \sa orientation + Qt::Orientations orientation()const; + /// Set the orientation property value. + /// \sa orientation + void setOrientation(Qt::Orientations orientation); + + enum VerticalDirection{ + TopToBottom = 1, + BottomToTop = 2 + }; + + /// Return the verticalDirection property value. + /// \sa verticalDirection + VerticalDirection verticalDirection()const; + /// Set the verticalDirection property value. + /// \sa verticalDirection + void setVerticalDirection(VerticalDirection direction); + + /// Return the horizontalDirection property value. + /// \sa horizontalDirection + Qt::LayoutDirection horizontalDirection()const; + /// Set the horizontalDirection property value. + /// \sa horizontalDirection + void setHorizontalDirection(Qt::LayoutDirection direction); + +public Q_SLOTS: + /// Hide the popup if open or opening. It takes around 300ms + /// for the fading effect to hide the popup. + virtual void hidePopup(); + /// Open the popup if closed or closing. It takes around 300ms + /// for the fading effect to open the popup. + virtual void showPopup(); + /// Show/hide the popup. It can be conveniently linked to a QPushButton + /// signal. + inline void showPopup(bool show); + +Q_SIGNALS: + /// Fired when the popup finished its animation: opening (true) or closing (false). + /// \sa showPopup(), hidePopup() + void popupOpened(bool open); + +protected: + explicit ctkBasePopupWidget(ctkBasePopupWidgetPrivate* pimpl, QWidget* parent = 0); + QScopedPointer d_ptr; + Q_PROPERTY(double effectAlpha READ effectAlpha WRITE setEffectAlpha DESIGNABLE false) + Q_PROPERTY(QRect effectGeometry READ effectGeometry WRITE setEffectGeometry DESIGNABLE false) + + double effectAlpha()const; + QRect effectGeometry()const; + + virtual void setBaseWidget(QWidget* baseWidget); + virtual bool event(QEvent* event); + virtual void paintEvent(QPaintEvent*); + +protected Q_SLOTS: + virtual void onEffectFinished(); + void setEffectAlpha(double alpha); + void setEffectGeometry(QRect geometry); + void onBaseWidgetDestroyed(); + +private: + Q_DECLARE_PRIVATE(ctkBasePopupWidget); + Q_DISABLE_COPY(ctkBasePopupWidget); +}; + +Q_DECLARE_METATYPE(ctkBasePopupWidget::AnimationEffect) +Q_DECLARE_METATYPE(ctkBasePopupWidget::VerticalDirection) + +// ------------------------------------------------------------------------- +void ctkBasePopupWidget::showPopup(bool show) +{ + if (show) + { + this->showPopup(); + } + else + { + this->hidePopup(); + } +} + +#endif diff --git a/ctk/ctkBasePopupWidget_p.h b/ctk/ctkBasePopupWidget_p.h new file mode 100644 index 0000000..2fc7e50 --- /dev/null +++ b/ctk/ctkBasePopupWidget_p.h @@ -0,0 +1,108 @@ +/*========================================================================= + + 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. + +=========================================================================*/ + +#ifndef __ctkBasePopupWidget_p_h +#define __ctkBasePopupWidget_p_h + +// Qt includes +#include +class QLabel; +class QPropertyAnimation; + +// CTK includes +#include "ctkBasePopupWidget.h" +#define PopupWindowType Qt::Tool + +// ------------------------------------------------------------------------- +/// \ingroup Widgets +class CTK_WIDGETS_EXPORT ctkBasePopupWidgetPrivate + : public QObject +{ + Q_OBJECT + Q_DECLARE_PUBLIC(ctkBasePopupWidget); +protected: + ctkBasePopupWidget* const q_ptr; +public: + ctkBasePopupWidgetPrivate(ctkBasePopupWidget& object); + ~ctkBasePopupWidgetPrivate(); + virtual void init(); + + bool isOpening()const; + bool isClosing()const; + /// Return true if the animation was closing (direction == backward). + /// It doesn't indicate if the action is still running or finished. + /// Can only be called in a slot as it uses sender(). + bool wasClosing()const; + + bool fitBaseWidgetSize()const; + Qt::Alignment pixmapAlignment()const; + void setupPopupPixmapWidget(); + + QWidgetList focusWidgets(bool onlyVisible = false)const; + + // Return the widget if the mouse cursor is above any of the focus widgets or their + // children. + virtual QWidget* mouseOver(); + + // Same as QWidget::isAncestorOf() but don't restrain to the same window + // and apply it to all the focusWidgets + bool isAncestorOf(const QWidget* ancestor, const QWidget* child)const; + + + /// Return the closed geometry for the popup based on the current geometry + QRect closedGeometry()const; + /// Return the closed geometry for a given open geometry + QRect closedGeometry(QRect openGeom)const; + + /// Return the desired geometry, maybe it won't happen if the size is too + /// small for the popup. + QRect desiredOpenGeometry()const; + QRect desiredOpenGeometry(QRect baseGeometry)const; + QRect baseGeometry()const; + QPoint mapToGlobal(const QPoint& baseWidgetPoint)const; + + QPropertyAnimation* currentAnimation()const; + + //void temporarilyHiddenOn(); + //void temporarilyHiddenOff(); + + void hideAll(); + +protected: + QPointer BaseWidget; + + double EffectAlpha; + + ctkBasePopupWidget::AnimationEffect Effect; + int EffectDuration; + QPropertyAnimation* AlphaAnimation; + bool ForcedTranslucent; + QPropertyAnimation* ScrollAnimation; + QLabel* PopupPixmapWidget; + + // Geometry attributes + Qt::Alignment Alignment; + Qt::Orientations Orientations; + + ctkBasePopupWidget::VerticalDirection VerticalDirection; + Qt::LayoutDirection HorizontalDirection; +}; + +#endif diff --git a/ctk/ctkPopupWidget.cpp b/ctk/ctkPopupWidget.cpp new file mode 100644 index 0000000..1015547 --- /dev/null +++ b/ctk/ctkPopupWidget.cpp @@ -0,0 +1,560 @@ +/*========================================================================= + + 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" + +// ------------------------------------------------------------------------- +ctkPopupWidgetPrivate::ctkPopupWidgetPrivate(ctkPopupWidget& object) + :Superclass(object) +{ + this->Active = false; + this->AutoShow = true; + this->ShowDelay = 20; + this->AutoHide = true; + this->HideDelay = 200; +} + +// ------------------------------------------------------------------------- +ctkPopupWidgetPrivate::~ctkPopupWidgetPrivate() +{ +} + +// ------------------------------------------------------------------------- +void ctkPopupWidgetPrivate::init() +{ + Q_Q(ctkPopupWidget); + this->setParent(q); + q->setActive(true); + this->Superclass::init(); +} + +// ------------------------------------------------------------------------- +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); + if (!widget) + { + return this->Superclass::eventFilter(obj, event); + } + // 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.isNull()) + { + return false; + } + if (event->type() == QEvent::Move && widget != this->BaseWidget) + { + if (widget->isAncestorOf(this->BaseWidget)) + { + q->setGeometry(this->desiredOpenGeometry()); + } + else if (this->isHidingCandidate(widget)) + { + QTimer::singleShot(0, this, SLOT(updateVisibility())); + } + } + else if (event->type() == QEvent::Resize) + { + if (widget->isAncestorOf(this->BaseWidget)) + { + q->setGeometry(this->desiredOpenGeometry()); + } + else if (this->isHidingCandidate(widget)) + { + QTimer::singleShot(0, this, SLOT(updateVisibility())); + } + } + else if (event->type() == QEvent::WindowStateChange && + this->isHidingCandidate(widget)) + { + 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())); + } + else if (event->type() == QEvent::RequestSoftwareInputPanel) + { + qApp->setActiveWindow(widget->window()); + } + 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(); + } +} + +// ------------------------------------------------------------------------- +bool ctkPopupWidgetPrivate::isHidingCandidate(QWidget* widget)const +{ + // The mac window manager is keeping the Qt:Tool widgets always on top, + // so if a non modal dialog is moved near the popup widget, the popup will + // always appear on top of the dialog. For this reason we manually have to + // hide the popup when a dialog is intersecting with the popup. + bool canWindowsHidePopup = false; +#if defined Q_OS_MAC + canWindowsHidePopup = true; +#endif + bool isWindow = widget->isWindow(); + QDialog* dialog = qobject_cast(widget); + bool isModal = dialog ? dialog->isModal() : false; + bool isBasePopupWidget = qobject_cast(widget); + bool isToolTip = widget->windowType() == Qt::ToolTip; + bool isPopup = widget->windowType() == Qt::Popup; + bool isSelf = (widget == (this->BaseWidget ? this->BaseWidget->window() : 0)); + + return canWindowsHidePopup && isWindow && !isModal && !isBasePopupWidget && + !isToolTip && !isPopup && !isSelf; +} + +// ------------------------------------------------------------------------- +void ctkPopupWidgetPrivate::updateVisibility() +{ + Q_Q(ctkPopupWidget); + // If the BaseWidget window is active, then there is no reason to cover the + // popup. + if (this->BaseWidget.isNull() || + // 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 + (!qobject_cast(qApp->activeWindow()) && //->windowType() != PopupWindowType && + 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) && + this->isHidingCandidate(topLevelWidget) && + 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.isNull() && + (!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.isNull()) + { + d->BaseWidget->installEventFilter(this); + } + if (d->PopupPixmapWidget) + { + d->PopupPixmapWidget->installEventFilter(this); + } + qApp->installEventFilter(d); + } + else // not active + { + if (!d->BaseWidget.isNull()) + { + d->BaseWidget->removeEventFilter(this); + } + if (d->PopupPixmapWidget) + { + d->PopupPixmapWidget->removeEventFilter(this); + } + qApp->removeEventFilter(d); + } +} + +// ------------------------------------------------------------------------- +void ctkPopupWidget::setBaseWidget(QWidget* widget) +{ + Q_D(ctkPopupWidget); + if (!d->BaseWidget.isNull()) + { + d->BaseWidget->removeEventFilter(this); + } + this->Superclass::setBaseWidget(widget); + if (!d->BaseWidget.isNull() && d->Active) + { + d->BaseWidget->installEventFilter(this); + } + QTimer::singleShot(d->ShowDelay, 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(d->ShowDelay, this, SLOT(updatePopup())); +} + +// ------------------------------------------------------------------------- +int ctkPopupWidget::showDelay()const +{ + Q_D(const ctkPopupWidget); + return d->ShowDelay; +} + +// ------------------------------------------------------------------------- +void ctkPopupWidget::setShowDelay(int delay) +{ + Q_D(ctkPopupWidget); + d->ShowDelay = delay; +} + +// ------------------------------------------------------------------------- +bool ctkPopupWidget::autoHide()const +{ + Q_D(const ctkPopupWidget); + return d->AutoHide; +} + +// ------------------------------------------------------------------------- +void ctkPopupWidget::setAutoHide(bool mode) +{ + Q_D(ctkPopupWidget); + d->AutoHide = mode; + QTimer::singleShot(d->HideDelay, this, SLOT(updatePopup())); +} + +// ------------------------------------------------------------------------- +int ctkPopupWidget::hideDelay()const +{ + Q_D(const ctkPopupWidget); + return d->HideDelay; +} + +// ------------------------------------------------------------------------- +void ctkPopupWidget::setHideDelay(int delay) +{ + Q_D(ctkPopupWidget); + d->HideDelay = delay; +} + +// ------------------------------------------------------------------------- +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) +{ + Q_D(ctkPopupWidget); + QTimer::singleShot(d->HideDelay, this, SLOT(updatePopup())); + this->Superclass::leaveEvent(event); +} + +// -------------------------------------------------------------------------- +void ctkPopupWidget::enterEvent(QEvent* event) +{ + Q_D(ctkPopupWidget); + QTimer::singleShot(d->ShowDelay, 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(d->HideDelay, this, SLOT(updatePopup())); + } + break; + } + d->temporarilyHiddenOn(); + break; + case QEvent::Show: + if (obj != d->BaseWidget) + { + break; + } + this->setGeometry(d->desiredOpenGeometry()); + d->temporarilyHiddenOff(); + 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(d->ShowDelay, 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(d->HideDelay, 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 || + // Even if there is no AutoShow, we might still want to reopen the popup + // when closing it inadvertently, except if we are un-pin-ing the popup + (d->AutoHide && d->isClosing() && this->property("AutoShowOnClose").toBool())) && + // to be automatically open, the mouse has to be over a child widget + mouseOver && + // disable opening the popup when the popup is disabled + (d->BaseWidget.isNull() || d->BaseWidget->isEnabled())) + { + this->showPopup(); + } + else if (d->AutoHide && !mouseOver) + { + this->hidePopup(); + } +} + + +// -------------------------------------------------------------------------- +void ctkPopupWidget::hidePopup() +{ + // 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(); + } +} diff --git a/ctk/ctkPopupWidget.h b/ctk/ctkPopupWidget.h new file mode 100644 index 0000000..99675c5 --- /dev/null +++ b/ctk/ctkPopupWidget.h @@ -0,0 +1,133 @@ +/*========================================================================= + + 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. + +=========================================================================*/ + +#ifndef __ctkPopupWidget_h +#define __ctkPopupWidget_h + +// CTK includes +#include "ctkBasePopupWidget.h" + +class ctkPopupWidgetPrivate; + +/// \ingroup Widgets +/// ctkPopupWidget is a specialization of ctkBasePopupWidget that handles +/// the opening and closing of the popup. +/// Below is an example of a popup slider that opens and closes next to a +/// button +/// \code +/// ctkPopupWidget* popup = new ctkPopupWidget(pushButton); +/// popup->setAlignment(Qt::AlignRight | Qt::AlignTop | Qt::AlignBottom); +/// popup->setOrientation(Qt::Horizontal); +/// QHBoxLayout* popupLayout = new QHBoxLayout(popup); +/// QSlider* popupSlider = new QSlider(popup); +/// popupLayout->addWidget(popupSlider); +/// \endcode +/// \sa ctkBasePopupWidget +class CTK_WIDGETS_EXPORT ctkPopupWidget : public ctkBasePopupWidget +{ + Q_OBJECT + + /// Control whether the popup listens to the application and baseWidget + /// events and decides if it needs to be permanently or temporarily hidden. + /// You might want to setActive(false) when embedding the popup + /// into a static layout intead of having it top-level (no parent). + /// Consider also removing its windowFlags (Qt::ToolTip | + /// Qt::FramelessWindowHint) and removing the baseWidget. + /// True by default + /// \sa isActive(), setActive() + Q_PROPERTY( bool active READ isActive WRITE setActive) + + /// Control wether the popup automatically opens when the mouse + /// enter the widget. True by default + /// \sa autoShow(), setAutoShow() + Q_PROPERTY( bool autoShow READ autoShow WRITE setAutoShow) + + /// Time in ms to wait before opening the popup if autoShow is set. + /// 20ms by default + /// \sa showDelay(), setShowDelay() + Q_PROPERTY( int showDelay READ showDelay WRITE setShowDelay) + + /// Control wether the popup automatically closes when the mouse + /// leaves the widget. True by default. + /// \sa autoHide(), setAutoHide() + Q_PROPERTY( bool autoHide READ autoHide WRITE setAutoHide) + + /// Time in ms to wait before closing the popup if autoHide is set. + /// 200ms by default + /// \sa hideDelay(), setHideDelay() + Q_PROPERTY( int hideDelay READ hideDelay WRITE setHideDelay) + +public: + typedef ctkBasePopupWidget Superclass; + /// By default, the parent is the \a baseWidget. + /// \sa baseWidget() + explicit ctkPopupWidget(QWidget* parent = 0); + virtual ~ctkPopupWidget(); + + bool isActive()const; + void setActive(bool); + + bool autoShow()const; + /// Calling setAutoShow automatically updates opens the popup if the cursor + /// is above the popup or the base widget. + void setAutoShow(bool); + + int showDelay()const; + void setShowDelay(int delay); + + bool autoHide()const; + /// Don't automatically close the popup when leaving the widget. + /// Calling setAutoHide automatically updates the state close the popup + /// if the mouse is not over the popup nor the base widget. + void setAutoHide(bool autoHide); + + int hideDelay()const; + void setHideDelay(int delay); + +public Q_SLOTS: + /// Convenient function that calls setAutoHide(!pin) and opens the popup + /// if pin is true regardless of the value of \a AutoShow. + /// It is typically connected with a checkable button to anchor the popup. + void pinPopup(bool pin); + +public: + /// Reimplemented for internal reasons + virtual void hidePopup(); + +protected: + virtual void leaveEvent(QEvent* event); + virtual void enterEvent(QEvent* event); + virtual bool eventFilter(QObject* obj, QEvent* event); + + /// Widget the popup is attached to. It opens right under \a baseWidget + /// and if the ctkPopupWidget sizepolicy contains the growFlag/shrinkFlag, + /// it tries to resize itself to fit the same width of \a baseWidget. + virtual void setBaseWidget(QWidget* baseWidget); + +protected Q_SLOTS: + void updatePopup(); + virtual void onEffectFinished(); + +private: + Q_DECLARE_PRIVATE(ctkPopupWidget); + Q_DISABLE_COPY(ctkPopupWidget); +}; + +#endif diff --git a/ctk/ctkPopupWidget_p.h b/ctk/ctkPopupWidget_p.h new file mode 100644 index 0000000..c2d005a --- /dev/null +++ b/ctk/ctkPopupWidget_p.h @@ -0,0 +1,67 @@ +/*========================================================================= + + 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. + +=========================================================================*/ + +#ifndef __ctkPopupWidget_p_h +#define __ctkPopupWidget_p_h + +// CTK includes +#include "ctkBasePopupWidget_p.h" +#include "ctkPopupWidget.h" + +// ------------------------------------------------------------------------- +/// \ingroup Widgets +class CTK_WIDGETS_EXPORT ctkPopupWidgetPrivate + : public ctkBasePopupWidgetPrivate +{ + Q_OBJECT + Q_DECLARE_PUBLIC(ctkPopupWidget); +public: + typedef ctkBasePopupWidgetPrivate Superclass; + ctkPopupWidgetPrivate(ctkPopupWidget& object); + ~ctkPopupWidgetPrivate(); + + virtual void init(); + + // Return the widget if the mouse cursor is above any of the focus widgets or their + // children. + // If the cursor is above a child widget, install the event filter to listen + // when the cursor leaves the widget. + virtual QWidget* mouseOver(); + + virtual bool eventFilter(QObject* obj, QEvent* event); + + void temporarilyHiddenOn(); + void temporarilyHiddenOff(); + + bool isHidingCandidate(QWidget* widget)const; + +public Q_SLOTS: + void updateVisibility(); + void onApplicationDeactivate(); + +protected: + bool Active; + bool AutoShow; + int ShowDelay; + bool AutoHide; + int HideDelay; +}; + +#endif diff --git a/ctk/ctkWidgetsExport.h b/ctk/ctkWidgetsExport.h new file mode 100644 index 0000000..75361fe --- /dev/null +++ b/ctk/ctkWidgetsExport.h @@ -0,0 +1,6 @@ +#ifndef CTKWIDGETSEXPORT_H +#define CTKWIDGETSEXPORT_H + +#define CTK_WIDGETS_EXPORT + +#endif // CTKWIDGETSEXPORT_H diff --git a/ctk/ctkWidgetsUtils.cpp b/ctk/ctkWidgetsUtils.cpp new file mode 100644 index 0000000..3deb88d --- /dev/null +++ b/ctk/ctkWidgetsUtils.cpp @@ -0,0 +1,115 @@ +/*========================================================================= + + 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 + +// ctkWidgets includes +#include "ctkWidgetsUtils.h" + +//---------------------------------------------------------------------------- +QString ctk::base64HTMLImageTagSrc(const QImage& image) +{ + QByteArray byteArray; + QBuffer buffer(&byteArray); + buffer.open(QIODevice::WriteOnly); + image.save(&buffer, "PNG"); + return QString("data:image/png;base64,%1") + .arg(QString(buffer.data().toBase64())); +} + +//---------------------------------------------------------------------------- +//QImage ctk::grabWidget(QWidget* widget, QRect rectangle) +//{ +// if (!widget) +// { +// return QImage(); +// } +// if (!rectangle.isValid()) +// { +// // Let Qt trigger layout mechanism and compute widget size. +// rectangle = QRect(0,0,-1,-1); +// } +//#if (QT_VERSION < QT_VERSION_CHECK(5, 0, 0)) +// QPixmap widgetPixmap = QPixmap::grabWidget(widget, rectangle); +//#else +// QPixmap widgetPixmap = widget->grab(rectangle); +//#endif +// QImage widgetImage = widgetPixmap.toImage(); +// QPainter painter; +// painter.begin(&widgetImage); +// foreach(QGLWidget* glWidget, widget->findChildren()) +// { +// if (!glWidget->isVisible()) +// { +// continue; +// } +// QRect subWidgetRect = QRect(glWidget->mapTo(widget, QPoint(0,0)), glWidget->size()); +// if (!rectangle.intersects(subWidgetRect)) +// { +// continue; +// } +// QImage subImage = glWidget->grabFrameBuffer(); +// painter.drawImage(subWidgetRect, subImage); +// } +// painter.end(); +// return widgetImage; +//} + +//---------------------------------------------------------------------------- +QImage ctk::kwIconToQImage(const unsigned char *data, int width, int height, int pixelSize, unsigned int bufferLength, int options) +{ + Q_UNUSED(options); // not yet supported + + QByteArray imageData = QByteArray::fromRawData( + reinterpret_cast(data), bufferLength); + unsigned int expectedLength = width * height * pixelSize; + // Start with a zlib header ? If not, then it must be base64 encoded + if (bufferLength != expectedLength && + static_cast(imageData[0]) != 0x78 && + static_cast(imageData[1]) != 0xDA) + { + imageData = QByteArray::fromBase64(imageData); + bufferLength = imageData.size(); + } + + if (bufferLength != expectedLength && + static_cast(imageData[0]) == 0x78 && + static_cast(imageData[1]) == 0xDA) + { + imageData.prepend(static_cast((expectedLength >> 0) & 0xFF)); + imageData.prepend(static_cast((expectedLength >> 8) & 0xFF)); + imageData.prepend(static_cast((expectedLength >> 16) & 0xFF)); + imageData.prepend(static_cast((expectedLength >> 24) & 0xFF)); + imageData = qUncompress(imageData); + } + QImage image(reinterpret_cast(imageData.data()), + width, height, width * pixelSize, + pixelSize == 4 ? QImage::Format_ARGB32 : QImage::Format_RGB888); + if (pixelSize == 4) + { + return image.rgbSwapped(); + } + return image.copy(); +} diff --git a/ctk/ctkWidgetsUtils.h b/ctk/ctkWidgetsUtils.h new file mode 100644 index 0000000..bf31222 --- /dev/null +++ b/ctk/ctkWidgetsUtils.h @@ -0,0 +1,53 @@ +/*========================================================================= + + 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. + +=========================================================================*/ + +#ifndef __ctkWidgetsUtils_h +#define __ctkWidgetsUtils_h + +// Qt includes +#include +#include +class QImage; + +#include "ctkWidgetsExport.h" + +namespace ctk { +/// +/// \ingroup Widgets +/// Create a base 64 image tag. Can be used that way: +/// QString("").arg(base64HTMLImageTagSrc(myImage); +QString CTK_WIDGETS_EXPORT base64HTMLImageTagSrc(const QImage& image); + +/// +/// \ingroup Widgets +/// Grab the contents of a QWidget and all its children. +/// Handle correctly the case of QGLWidgets. +/// \sa QWidget::grab +QImage CTK_WIDGETS_EXPORT grabWidget(QWidget* widget, QRect rectangle = QRect()); + +/// +/// \ingroup Widgets +/// Convert an KWidget encoded image into a QImage +/// The data can be base64 encoded and/or zlib compressed. +QImage CTK_WIDGETS_EXPORT kwIconToQImage(const unsigned char *data, int width, int height, int pixelSize, unsigned int bufferLength, int options = 0); + +} + +#endif diff --git a/pglabAll.pro b/pglabAll.pro index 6995c21..7c43973 100644 --- a/pglabAll.pro +++ b/pglabAll.pro @@ -4,9 +4,11 @@ DEFINES += BOOST_ENABLE_ASSERT_HANDLER SUBDIRS += core \ + ctk \ pgsql \ -pglab \ - pglablib + pglablib \ + pglab + CONFIG(debug, debug|release) { SUBDIRS += tests