diff --git a/lib/documentview/documentview.cpp b/lib/documentview/documentview.cpp index 2035df28..42e3a1fd 100644 --- a/lib/documentview/documentview.cpp +++ b/lib/documentview/documentview.cpp @@ -1,1022 +1,1022 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2008 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "documentview.h" // C++ Standard library #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include // KDE #include #include #include // Local #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Gwenview { #undef ENABLE_LOG #undef LOG //#define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) //qDebug() << x #else #define LOG(x) ; #endif static const qreal REAL_DELTA = 0.001; static const qreal MAXIMUM_ZOOM_VALUE = qreal(DocumentView::MaximumZoom); static const auto MINSTEP = sqrt(0.5); static const auto MAXSTEP = sqrt(2.0); static const int COMPARE_MARGIN = 4; const int DocumentView::MaximumZoom = 16; -const int DocumentView::AnimDuration = 250; struct DocumentViewPrivate { DocumentView* q; int mSortKey; // Used to sort views when displayed in compare mode HudWidget* mHud; BirdEyeView* mBirdEyeView; QPointer mMoveAnimation; QPointer mFadeAnimation; QGraphicsOpacityEffect* mOpacityEffect; LoadingIndicator* mLoadingIndicator; QScopedPointer mAdapter; QList mZoomSnapValues; Document::Ptr mDocument; DocumentView::Setup mSetup; bool mCurrent; bool mCompareMode; int controlWheelAccumulatedDelta; QPointF mDragStartPosition; QPointer mDragThumbnailProvider; QPointer mDrag; void setCurrentAdapter(AbstractDocumentViewAdapter* adapter) { Q_ASSERT(adapter); mAdapter.reset(adapter); adapter->widget()->setParentItem(q); resizeAdapterWidget(); if (adapter->canZoom()) { QObject::connect(adapter, SIGNAL(zoomChanged(qreal)), q, SLOT(slotZoomChanged(qreal))); QObject::connect(adapter, SIGNAL(zoomInRequested(QPointF)), q, SLOT(zoomIn(QPointF))); QObject::connect(adapter, SIGNAL(zoomOutRequested(QPointF)), q, SLOT(zoomOut(QPointF))); QObject::connect(adapter, SIGNAL(zoomToFitChanged(bool)), q, SIGNAL(zoomToFitChanged(bool))); QObject::connect(adapter, SIGNAL(zoomToFillChanged(bool)), q, SIGNAL(zoomToFillChanged(bool))); } QObject::connect(adapter, SIGNAL(scrollPosChanged()), q, SIGNAL(positionChanged())); QObject::connect(adapter, SIGNAL(previousImageRequested()), q, SIGNAL(previousImageRequested())); QObject::connect(adapter, SIGNAL(nextImageRequested()), q, SIGNAL(nextImageRequested())); QObject::connect(adapter, SIGNAL(toggleFullScreenRequested()), q, SIGNAL(toggleFullScreenRequested())); QObject::connect(adapter, SIGNAL(completed()), q, SLOT(slotCompleted())); adapter->loadConfig(); adapter->widget()->installSceneEventFilter(q); if (mCurrent) { adapter->widget()->setFocus(); } if (mSetup.valid && adapter->canZoom()) { adapter->setZoomToFit(mSetup.zoomToFit); adapter->setZoomToFill(mSetup.zoomToFill); if (!mSetup.zoomToFit && !mSetup.zoomToFill) { adapter->setZoom(mSetup.zoom); adapter->setScrollPos(mSetup.position); } } emit q->adapterChanged(); emit q->positionChanged(); if (adapter->canZoom()) { if (adapter->zoomToFit()) { emit q->zoomToFitChanged(true); } else if (adapter->zoomToFill()) { emit q->zoomToFillChanged(true); } else { emit q->zoomChanged(adapter->zoom()); } } if (adapter->rasterImageView()) { QObject::connect(adapter->rasterImageView(), SIGNAL(currentToolChanged(AbstractRasterImageViewTool*)), q, SIGNAL(currentToolChanged(AbstractRasterImageViewTool*))); } } void setupLoadingIndicator() { mLoadingIndicator = new LoadingIndicator(q); GraphicsWidgetFloater* floater = new GraphicsWidgetFloater(q); floater->setChildWidget(mLoadingIndicator); } HudButton* createHudButton(const QString& text, const QString &iconName, bool showText) { HudButton* button = new HudButton; if (showText) { button->setText(text); } else { button->setToolTip(text); } button->setIcon(QIcon::fromTheme(iconName)); return button; } void setupHud() { HudButton* trashButton = createHudButton(i18nc("@info:tooltip", "Trash"), QStringLiteral("user-trash"), false); HudButton* deselectButton = createHudButton(i18nc("@action:button", "Deselect"), QStringLiteral("list-remove"), true); QGraphicsWidget* content = new QGraphicsWidget; QGraphicsLinearLayout* layout = new QGraphicsLinearLayout(content); layout->addItem(trashButton); layout->addItem(deselectButton); mHud = new HudWidget(q); mHud->init(content, HudWidget::OptionNone); GraphicsWidgetFloater* floater = new GraphicsWidgetFloater(q); floater->setChildWidget(mHud); floater->setAlignment(Qt::AlignBottom | Qt::AlignHCenter); QObject::connect(trashButton, SIGNAL(clicked()), q, SLOT(emitHudTrashClicked())); QObject::connect(deselectButton, SIGNAL(clicked()), q, SLOT(emitHudDeselectClicked())); mHud->hide(); } void setupBirdEyeView() { if (mBirdEyeView) { delete mBirdEyeView; } mBirdEyeView = new BirdEyeView(q); mBirdEyeView->setZValue(1); } void updateCaption() { if (!mCurrent) { return; } QString caption; Document::Ptr doc = mAdapter->document(); if (!doc) { emit q->captionUpdateRequested(caption); return; } caption = doc->url().fileName(); QSize size = doc->size(); if (size.isValid()) { caption += QStringLiteral(" - %1x%2") .arg(size.width()) .arg(size.height()); if (mAdapter->canZoom()) { int intZoom = qRound(mAdapter->zoom() * 100); caption += QStringLiteral(" - %1%") .arg(intZoom); } } emit q->captionUpdateRequested(caption); } void uncheckZoomToFit() { if (mAdapter->zoomToFit()) { mAdapter->setZoomToFit(false); } } void uncheckZoomToFill() { if (mAdapter->zoomToFill()) { mAdapter->setZoomToFill(false); } } void setZoom(qreal zoom, const QPointF& center = QPointF(-1, -1)) { uncheckZoomToFit(); uncheckZoomToFill(); zoom = qBound(q->minimumZoom(), zoom, MAXIMUM_ZOOM_VALUE); mAdapter->setZoom(zoom, center); } void updateZoomSnapValues() { qreal min = q->minimumZoom(); mZoomSnapValues.clear(); for (qreal zoom = MINSTEP; zoom > min; zoom *= MINSTEP) { mZoomSnapValues << zoom; } mZoomSnapValues << min; std::reverse(mZoomSnapValues.begin(), mZoomSnapValues.end()); for (qreal zoom = 1; zoom < MAXIMUM_ZOOM_VALUE; zoom *= MAXSTEP) { mZoomSnapValues << zoom; } mZoomSnapValues << MAXIMUM_ZOOM_VALUE; emit q->minimumZoomChanged(min); } void showLoadingIndicator() { if (!mLoadingIndicator) { setupLoadingIndicator(); } mLoadingIndicator->show(); mLoadingIndicator->setZValue(1); } void hideLoadingIndicator() { if (!mLoadingIndicator) { return; } mLoadingIndicator->hide(); } void resizeAdapterWidget() { QRectF rect = QRectF(QPointF(0, 0), q->boundingRect().size()); if (mCompareMode) { rect.adjust(COMPARE_MARGIN, COMPARE_MARGIN, -COMPARE_MARGIN, -COMPARE_MARGIN); } mAdapter->widget()->setGeometry(rect); } void fadeTo(qreal value) { if (mFadeAnimation.data()) { qreal endValue = mFadeAnimation.data()->endValue().toReal(); if (qFuzzyCompare(value, endValue)) { // Same end value, don't change the actual animation return; } } // Create a new fade animation QPropertyAnimation* anim = new QPropertyAnimation(mOpacityEffect, "opacity"); anim->setStartValue(mOpacityEffect->opacity()); anim->setEndValue(value); if (qFuzzyCompare(value, 1)) { QObject::connect(anim, SIGNAL(finished()), q, SLOT(slotFadeInFinished())); } QObject::connect(anim, SIGNAL(finished()), q, SIGNAL(isAnimatedChanged())); - anim->setDuration(DocumentView::AnimDuration); + anim->setDuration(q->style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, nullptr)); mFadeAnimation = anim; emit q->isAnimatedChanged(); anim->start(QAbstractAnimation::DeleteWhenStopped); } bool canPan() const { if (!q->canZoom()) { return false; } const QSize zoomedImageSize = mDocument->size() * q->zoom(); const QSize viewPortSize = q->boundingRect().size().toSize(); const bool imageWiderThanViewport = zoomedImageSize.width() > viewPortSize.width(); const bool imageTallerThanViewport = zoomedImageSize.height() > viewPortSize.height(); return (imageWiderThanViewport || imageTallerThanViewport); } void setDragPixmap(const QPixmap& pix) { if (mDrag) { DragPixmapGenerator::DragPixmap dragPixmap = DragPixmapGenerator::generate({pix}, 1); mDrag->setPixmap(dragPixmap.pix); mDrag->setHotSpot(dragPixmap.hotSpot); } } void executeDrag() { if (mDrag) { if (mAdapter->imageView()) { mAdapter->imageView()->resetDragCursor(); } mDrag->exec(Qt::MoveAction | Qt::CopyAction | Qt::LinkAction, Qt::CopyAction); } } void initDragThumbnailProvider() { mDragThumbnailProvider = new ThumbnailProvider(); QObject::connect(mDragThumbnailProvider, &ThumbnailProvider::thumbnailLoaded, q, &DocumentView::dragThumbnailLoaded); QObject::connect(mDragThumbnailProvider, &ThumbnailProvider::thumbnailLoadingFailed, q, &DocumentView::dragThumbnailLoadingFailed); } void startDragIfSensible() { if (q->document()->loadingState() == Document::LoadingFailed) { return; } if (q->currentTool()) { return; } if (mDrag) { mDrag->deleteLater(); } mDrag = new QDrag(q); const auto itemList = KFileItemList({q->document()->url()}); mDrag->setMimeData(MimeTypeUtils::selectionMimeData(itemList, MimeTypeUtils::DropTarget)); if (q->document()->isModified()) { setDragPixmap(QPixmap::fromImage(q->document()->image())); executeDrag(); } else { // Drag is triggered on success or failure of thumbnail generation if (mDragThumbnailProvider.isNull()) { initDragThumbnailProvider(); } mDragThumbnailProvider->appendItems(itemList); } } QPointF cursorPosition() { const QGraphicsScene* sc = q->scene(); if (sc) { const auto views = sc->views(); for (const QGraphicsView* view : views) { if (view->underMouse()) { return q->mapFromScene(view->mapFromGlobal(QCursor::pos())); } } } return QPointF(-1, -1); } }; DocumentView::DocumentView(QGraphicsScene* scene) : d(new DocumentViewPrivate) { setFlag(ItemIsFocusable); setFlag(ItemIsSelectable); setFlag(ItemClipsChildrenToShape); d->q = this; d->mLoadingIndicator = nullptr; d->mBirdEyeView = nullptr; d->mCurrent = false; d->mCompareMode = false; d->controlWheelAccumulatedDelta = 0; d->mDragStartPosition = QPointF(0, 0); d->mDrag = nullptr; // We use an opacity effect instead of using the opacity property directly, because the latter operates at // the painter level, which means if you draw multiple layers in paint(), all layers get the specified // opacity, resulting in all layers being visible when 0 < opacity < 1. // QGraphicsEffects on the other hand, operate after all painting is done, therefore 'flattening' all layers. // This is important for fade effects, where we don't want any background layers visible during the fade. d->mOpacityEffect = new QGraphicsOpacityEffect(this); d->mOpacityEffect->setOpacity(0); setGraphicsEffect(d->mOpacityEffect); scene->addItem(this); d->setupHud(); d->setCurrentAdapter(new EmptyAdapter); setAcceptDrops(true); connect(DocumentFactory::instance(), &DocumentFactory::documentChanged, this, [this]() { d->updateCaption(); }); } DocumentView::~DocumentView() { delete d->mDragThumbnailProvider; delete d->mDrag; delete d; } void DocumentView::createAdapterForDocument() { const MimeTypeUtils::Kind documentKind = d->mDocument->kind(); if (d->mAdapter && documentKind == d->mAdapter->kind() && documentKind != MimeTypeUtils::KIND_UNKNOWN) { // Do not reuse for KIND_UNKNOWN: we may need to change the message LOG("Reusing current adapter"); return; } AbstractDocumentViewAdapter* adapter = nullptr; switch (documentKind) { case MimeTypeUtils::KIND_RASTER_IMAGE: adapter = new RasterImageViewAdapter; break; case MimeTypeUtils::KIND_SVG_IMAGE: adapter = new SvgViewAdapter; break; case MimeTypeUtils::KIND_VIDEO: adapter = new VideoViewAdapter; connect(adapter, SIGNAL(videoFinished()), SIGNAL(videoFinished())); break; case MimeTypeUtils::KIND_UNKNOWN: adapter = new MessageViewAdapter; static_cast(adapter)->setErrorMessage(i18n("Gwenview does not know how to display this kind of document")); break; default: qWarning() << "should not be called for documentKind=" << documentKind; adapter = new MessageViewAdapter; break; } d->setCurrentAdapter(adapter); } void DocumentView::openUrl(const QUrl &url, const DocumentView::Setup& setup) { if (d->mDocument) { if (url == d->mDocument->url()) { return; } disconnect(d->mDocument.data(), nullptr, this, nullptr); } d->mSetup = setup; d->mDocument = DocumentFactory::instance()->load(url); connect(d->mDocument.data(), SIGNAL(busyChanged(QUrl,bool)), SLOT(slotBusyChanged(QUrl,bool))); connect(d->mDocument.data(), &Document::modified, this, [this]() { d->updateZoomSnapValues(); }); if (d->mDocument->loadingState() < Document::KindDetermined) { MessageViewAdapter* messageViewAdapter = qobject_cast(d->mAdapter.data()); if (messageViewAdapter) { messageViewAdapter->setInfoMessage(QString()); } d->showLoadingIndicator(); connect(d->mDocument.data(), SIGNAL(kindDetermined(QUrl)), SLOT(finishOpenUrl())); } else { QMetaObject::invokeMethod(this, "finishOpenUrl", Qt::QueuedConnection); } d->setupBirdEyeView(); } void DocumentView::finishOpenUrl() { disconnect(d->mDocument.data(), SIGNAL(kindDetermined(QUrl)), this, SLOT(finishOpenUrl())); GV_RETURN_IF_FAIL(d->mDocument->loadingState() >= Document::KindDetermined); if (d->mDocument->loadingState() == Document::LoadingFailed) { slotLoadingFailed(); return; } createAdapterForDocument(); connect(d->mDocument.data(), SIGNAL(loadingFailed(QUrl)), SLOT(slotLoadingFailed())); d->mAdapter->setDocument(d->mDocument); d->updateCaption(); } void DocumentView::loadAdapterConfig() { d->mAdapter->loadConfig(); } RasterImageView* DocumentView::imageView() const { return d->mAdapter->rasterImageView(); } void DocumentView::slotCompleted() { d->hideLoadingIndicator(); d->updateCaption(); d->updateZoomSnapValues(); if (!d->mAdapter->zoomToFit() || !d->mAdapter->zoomToFill()) { qreal min = minimumZoom(); if (d->mAdapter->zoom() < min) { d->mAdapter->setZoom(min); } } emit completed(); } DocumentView::Setup DocumentView::setup() const { Setup setup; if (d->mAdapter->canZoom()) { setup.valid = true; setup.zoomToFit = zoomToFit(); setup.zoomToFill = zoomToFill(); if (!setup.zoomToFit && !setup.zoomToFill) { setup.zoom = zoom(); setup.position = position(); } } return setup; } void DocumentView::slotLoadingFailed() { d->hideLoadingIndicator(); MessageViewAdapter* adapter = new MessageViewAdapter; adapter->setDocument(d->mDocument); QString message = xi18n("Loading %1 failed", d->mDocument->url().fileName()); adapter->setErrorMessage(message, d->mDocument->errorString()); d->setCurrentAdapter(adapter); emit completed(); } bool DocumentView::canZoom() const { return d->mAdapter->canZoom(); } void DocumentView::setZoomToFit(bool on) { if (on == d->mAdapter->zoomToFit()) { return; } d->mAdapter->setZoomToFit(on); } void DocumentView::toggleZoomToFit() { const bool zoomToFitOn = d->mAdapter->zoomToFit(); d->mAdapter->setZoomToFit(!zoomToFitOn); if (zoomToFitOn) { d->setZoom(1., d->cursorPosition()); } } void DocumentView::setZoomToFill(bool on) { if (on == d->mAdapter->zoomToFill()) { return; } d->mAdapter->setZoomToFill(on, d->cursorPosition()); } void DocumentView::toggleZoomToFill() { const bool zoomToFillOn = d->mAdapter->zoomToFill(); d->mAdapter->setZoomToFill(!zoomToFillOn, d->cursorPosition()); if (zoomToFillOn) { d->setZoom(1., d->cursorPosition()); } } bool DocumentView::zoomToFit() const { return d->mAdapter->zoomToFit(); } bool DocumentView::zoomToFill() const { return d->mAdapter->zoomToFill(); } void DocumentView::zoomActualSize() { d->uncheckZoomToFit(); d->uncheckZoomToFill(); d->mAdapter->setZoom(1., d->cursorPosition()); } void DocumentView::zoomIn(QPointF center) { if (center == QPointF(-1, -1)) { center = d->cursorPosition(); } qreal currentZoom = d->mAdapter->zoom(); Q_FOREACH(qreal zoom, d->mZoomSnapValues) { if (zoom > currentZoom + REAL_DELTA) { d->setZoom(zoom, center); return; } } } void DocumentView::zoomOut(QPointF center) { if (center == QPointF(-1, -1)) { center = d->cursorPosition(); } qreal currentZoom = d->mAdapter->zoom(); QListIterator it(d->mZoomSnapValues); it.toBack(); while (it.hasPrevious()) { qreal zoom = it.previous(); if (zoom < currentZoom - REAL_DELTA) { d->setZoom(zoom, center); return; } } } void DocumentView::slotZoomChanged(qreal zoom) { d->updateCaption(); emit zoomChanged(zoom); } void DocumentView::setZoom(qreal zoom) { d->setZoom(zoom); } qreal DocumentView::zoom() const { return d->mAdapter->zoom(); } void DocumentView::resizeEvent(QGraphicsSceneResizeEvent *event) { d->resizeAdapterWidget(); d->updateZoomSnapValues(); QGraphicsWidget::resizeEvent(event); } void DocumentView::mousePressEvent(QGraphicsSceneMouseEvent* event) { QGraphicsWidget::mousePressEvent(event); if (d->mAdapter->canZoom() && event->button() == Qt::MiddleButton) { if (event->modifiers() == Qt::NoModifier) { toggleZoomToFit(); } else if (event->modifiers() == Qt::SHIFT) { toggleZoomToFill(); } } } void DocumentView::wheelEvent(QGraphicsSceneWheelEvent* event) { if (d->mAdapter->canZoom()) { if ((event->modifiers() & Qt::ControlModifier) || (GwenviewConfig::mouseWheelBehavior() == MouseWheelBehavior::Zoom && event->modifiers() == Qt::NoModifier)) { d->controlWheelAccumulatedDelta += event->delta(); // Ctrl + wheel => zoom in or out if (d->controlWheelAccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep) { zoomIn(event->pos()); d->controlWheelAccumulatedDelta = 0; } else if (d->controlWheelAccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep) { zoomOut(event->pos()); d->controlWheelAccumulatedDelta = 0; } return; } } if (GwenviewConfig::mouseWheelBehavior() == MouseWheelBehavior::Browse && event->modifiers() == Qt::NoModifier) { d->controlWheelAccumulatedDelta += event->delta(); // Browse with mouse wheel if (d->controlWheelAccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep) { emit previousImageRequested(); d->controlWheelAccumulatedDelta = 0; } else if (d->controlWheelAccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep) { emit nextImageRequested(); d->controlWheelAccumulatedDelta = 0; } return; } // Scroll qreal dx = 0; // 16 = pixels for one line // 120: see QWheelEvent::delta() doc qreal dy = -qApp->wheelScrollLines() * 16 * event->delta() / 120; if (event->orientation() == Qt::Horizontal) { qSwap(dx, dy); } d->mAdapter->setScrollPos(d->mAdapter->scrollPos() + QPointF(dx, dy)); } void DocumentView::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) { // Filter out context menu if Ctrl is down to avoid showing it when // zooming out with Ctrl + Right button if (event->modifiers() != Qt::ControlModifier) { emit contextMenuRequested(); } } void DocumentView::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget* /*widget*/) { // Fill background manually, because setAutoFillBackground(true) fill with QPalette::Window, // but our palettes use QPalette::Base for the background color/texture painter->fillRect(rect(), palette().base()); // Selection indicator/highlight if (d->mCompareMode && d->mCurrent) { painter->save(); painter->setBrush(Qt::NoBrush); painter->setPen(QPen(palette().highlight().color(), 2)); painter->setRenderHint(QPainter::Antialiasing); const QRectF visibleRectF = mapRectFromItem(d->mAdapter->widget(), d->mAdapter->visibleDocumentRect()); // Round the point and size independently. This is different than calling toRect(), // and is necessary to keep consistent rects, otherwise the selection rect can be // drawn 1 pixel too big or small. const QRect visibleRect = QRect(visibleRectF.topLeft().toPoint(), visibleRectF.size().toSize()); const QRect selectionRect = visibleRect.adjusted(-1, -1, 1, 1); painter->drawRoundedRect(selectionRect, 3, 3); painter->restore(); } } void DocumentView::slotBusyChanged(const QUrl&, bool busy) { if (busy) { d->showLoadingIndicator(); } else { d->hideLoadingIndicator(); } } qreal DocumentView::minimumZoom() const { // There is no point zooming out less than zoomToFit, but make sure it does // not get too small either return qBound(qreal(0.001), d->mAdapter->computeZoomToFit(), qreal(1.)); } void DocumentView::setCompareMode(bool compare) { d->mCompareMode = compare; if (compare) { d->mHud->show(); d->mHud->setZValue(1); } else { d->mHud->hide(); } } void DocumentView::setCurrent(bool value) { d->mCurrent = value; if (value) { d->mAdapter->widget()->setFocus(); d->updateCaption(); } update(); } bool DocumentView::isCurrent() const { return d->mCurrent; } QPoint DocumentView::position() const { return d->mAdapter->scrollPos().toPoint(); } void DocumentView::setPosition(const QPoint& pos) { d->mAdapter->setScrollPos(pos); } Document::Ptr DocumentView::document() const { return d->mDocument; } QUrl DocumentView::url() const { Document::Ptr doc = d->mDocument; return doc ? doc->url() : QUrl(); } void DocumentView::emitHudDeselectClicked() { emit hudDeselectClicked(this); } void DocumentView::emitHudTrashClicked() { emit hudTrashClicked(this); } void DocumentView::emitFocused() { emit focused(this); } void DocumentView::setGeometry(const QRectF& rect) { QGraphicsWidget::setGeometry(rect); if (d->mBirdEyeView) { d->mBirdEyeView->slotZoomOrSizeChanged(); } } void DocumentView::moveTo(const QRect& rect) { if (d->mMoveAnimation) { d->mMoveAnimation.data()->setEndValue(rect); } else { setGeometry(rect); } } void DocumentView::moveToAnimated(const QRect& rect) { QPropertyAnimation* anim = new QPropertyAnimation(this, "geometry"); anim->setStartValue(geometry()); anim->setEndValue(rect); - anim->setDuration(DocumentView::AnimDuration); + anim->setDuration(style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, nullptr)); connect(anim, SIGNAL(finished()), SIGNAL(isAnimatedChanged())); d->mMoveAnimation = anim; emit isAnimatedChanged(); anim->start(QAbstractAnimation::DeleteWhenStopped); } QPropertyAnimation* DocumentView::fadeIn() { d->fadeTo(1); return d->mFadeAnimation.data(); } void DocumentView::fadeOut() { d->fadeTo(0); } void DocumentView::slotFadeInFinished() { emit fadeInFinished(this); } bool DocumentView::isAnimated() const { return d->mMoveAnimation || d->mFadeAnimation; } bool DocumentView::sceneEventFilter(QGraphicsItem*, QEvent* event) { if (event->type() == QEvent::GraphicsSceneMousePress) { const QGraphicsSceneMouseEvent* mouseEvent = static_cast(event); if (mouseEvent->button() == Qt::LeftButton) { d->mDragStartPosition = mouseEvent->pos(); } QMetaObject::invokeMethod(this, "emitFocused", Qt::QueuedConnection); } else if (event->type() == QEvent::GraphicsSceneHoverMove) { if (d->mBirdEyeView) { d->mBirdEyeView->onMouseMoved(); } } else if (event->type() == QEvent::GraphicsSceneMouseMove) { const QGraphicsSceneMouseEvent* mouseEvent = static_cast(event); const qreal dragDistance = (mouseEvent->pos() - d->mDragStartPosition).manhattanLength(); const qreal minDistanceToStartDrag = QGuiApplication::styleHints()->startDragDistance(); if (!d->canPan() && dragDistance >= minDistanceToStartDrag) { d->startDragIfSensible(); } } return false; } AbstractRasterImageViewTool* DocumentView::currentTool() const { return imageView() ? imageView()->currentTool() : nullptr; } int DocumentView::sortKey() const { return d->mSortKey; } void DocumentView::setSortKey(int sortKey) { d->mSortKey = sortKey; } void DocumentView::hideAndDeleteLater() { hide(); deleteLater(); } void DocumentView::setGraphicsEffectOpacity(qreal opacity) { d->mOpacityEffect->setOpacity(opacity); } void DocumentView::dragEnterEvent(QGraphicsSceneDragDropEvent* event) { QGraphicsWidget::dragEnterEvent(event); const auto urls = KUrlMimeData::urlsFromMimeData(event->mimeData()); bool acceptDrag = !urls.isEmpty(); if (urls.size() == 1 && urls.first() == url()) { // Do not allow dragging a single image onto itself acceptDrag = false; } event->setAccepted(acceptDrag); } void DocumentView::dropEvent(QGraphicsSceneDragDropEvent* event) { QGraphicsWidget::dropEvent(event); // Since we're capturing drops in View mode, we only support one url const QUrl url = event->mimeData()->urls().first(); if (UrlUtils::urlIsDirectory(url)) { emit openDirUrlRequested(url); } else { emit openUrlRequested(url); } } void DocumentView::dragThumbnailLoaded(const KFileItem& item, const QPixmap& pix) { d->setDragPixmap(pix); d->executeDrag(); d->mDragThumbnailProvider->removeItems(KFileItemList({item})); } void DocumentView::dragThumbnailLoadingFailed(const KFileItem& item) { d->executeDrag(); d->mDragThumbnailProvider->removeItems(KFileItemList({item})); } } // namespace diff --git a/lib/documentview/documentview.h b/lib/documentview/documentview.h index a2da3d8a..4db6659e 100644 --- a/lib/documentview/documentview.h +++ b/lib/documentview/documentview.h @@ -1,246 +1,245 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2008 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ #ifndef DOCUMENTVIEW_H #define DOCUMENTVIEW_H #include // Qt #include // KDE // Local #include class QPropertyAnimation; class QUrl; namespace Gwenview { class AbstractRasterImageViewTool; class RasterImageView; struct DocumentViewPrivate; /** * This widget can display various documents, using an instance of * AbstractDocumentViewAdapter */ class GWENVIEWLIB_EXPORT DocumentView : public QGraphicsWidget { Q_OBJECT Q_PROPERTY(qreal zoom READ zoom WRITE setZoom NOTIFY zoomChanged) Q_PROPERTY(bool zoomToFit READ zoomToFit WRITE setZoomToFit NOTIFY zoomToFitChanged) Q_PROPERTY(bool zoomToFill READ zoomToFill WRITE setZoomToFill NOTIFY zoomToFillChanged) Q_PROPERTY(QPoint position READ position WRITE setPosition NOTIFY positionChanged) public: static const int MaximumZoom; - static const int AnimDuration; struct Setup { Setup() : valid(false) , zoomToFit(true) , zoomToFill(false) , zoom(0) {} bool valid:1; bool zoomToFit:1; bool zoomToFill:1; qreal zoom; QPointF position; }; enum AnimationMethod { NoAnimation, SoftwareAnimation, GLAnimation }; /** * Create a new view attached to scene. We need the scene to be able to * install scene event filters. */ explicit DocumentView(QGraphicsScene* scene); ~DocumentView() override; Document::Ptr document() const; QUrl url() const; void openUrl(const QUrl&, const Setup&); Setup setup() const; /** * Tells the current adapter to load its config. Used when the user changed * the config while the view was visible. */ void loadAdapterConfig(); bool canZoom() const; qreal minimumZoom() const; qreal zoom() const; bool isCurrent() const; void setCurrent(bool); void setCompareMode(bool); bool zoomToFit() const; bool zoomToFill() const; QPoint position() const; /** * Returns the RasterImageView of the current adapter, if it has one */ RasterImageView* imageView() const; AbstractRasterImageViewTool* currentTool() const; void moveTo(const QRect&); void moveToAnimated(const QRect&); QPropertyAnimation* fadeIn(); void fadeOut(); void fakeFadeOut(); void setGeometry(const QRectF& rect) override; int sortKey() const; void setSortKey(int sortKey); bool isAnimated() const; /** * Sets the opacity on the installed QGraphicsOpacityEffect. * Use this instead of setOpacity(). */ void setGraphicsEffectOpacity(qreal opacity); public Q_SLOTS: void setZoom(qreal); void setZoomToFit(bool); void toggleZoomToFit(); void setZoomToFill(bool); void toggleZoomToFill(); void setPosition(const QPoint&); void hideAndDeleteLater(); Q_SIGNALS: /** * Emitted when the part has finished loading */ void completed(); void previousImageRequested(); void nextImageRequested(); void openUrlRequested(const QUrl&); void openDirUrlRequested(const QUrl&); void captionUpdateRequested(const QString&); void toggleFullScreenRequested(); void videoFinished(); void minimumZoomChanged(qreal); void zoomChanged(qreal); void adapterChanged(); void focused(DocumentView*); void zoomToFitChanged(bool); void zoomToFillChanged(bool); void positionChanged(); void hudTrashClicked(DocumentView*); void hudDeselectClicked(DocumentView*); void fadeInFinished(DocumentView*); void contextMenuRequested(); void currentToolChanged(AbstractRasterImageViewTool*); void isAnimatedChanged(); protected: void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = nullptr) override; void resizeEvent(QGraphicsSceneResizeEvent* event) override; void mousePressEvent(QGraphicsSceneMouseEvent* event) override; void wheelEvent(QGraphicsSceneWheelEvent* event) override; void contextMenuEvent(QGraphicsSceneContextMenuEvent* event) override; bool sceneEventFilter(QGraphicsItem*, QEvent*) override; void dragEnterEvent(QGraphicsSceneDragDropEvent* event) override; void dropEvent(QGraphicsSceneDragDropEvent* event) override; private Q_SLOTS: void finishOpenUrl(); void slotCompleted(); void slotLoadingFailed(); void zoomActualSize(); void zoomIn(QPointF center = QPointF(-1, -1)); void zoomOut(QPointF center = QPointF(-1, -1)); void slotZoomChanged(qreal); void slotBusyChanged(const QUrl&, bool); void emitHudTrashClicked(); void emitHudDeselectClicked(); void emitFocused(); void slotFadeInFinished(); void dragThumbnailLoaded(const KFileItem&, const QPixmap&); void dragThumbnailLoadingFailed(const KFileItem&); private: friend struct DocumentViewPrivate; DocumentViewPrivate* const d; void createAdapterForDocument(); }; } // namespace #endif /* DOCUMENTVIEW_H */ diff --git a/lib/documentview/documentviewcontainer.cpp b/lib/documentview/documentviewcontainer.cpp index cba61212..69add77a 100644 --- a/lib/documentview/documentviewcontainer.cpp +++ b/lib/documentview/documentviewcontainer.cpp @@ -1,341 +1,343 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2011 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "documentviewcontainer.h" // Local #include #include #include #include // KDE // Qt #include #include #include #include #include #include +#include namespace Gwenview { typedef QSet DocumentViewSet; typedef QHash SetupForUrl; struct DocumentViewContainerPrivate { DocumentViewContainer* q; QGraphicsScene* mScene; SetupForUrl mSetupForUrl; DocumentViewSet mViews; DocumentViewSet mAddedViews; DocumentViewSet mRemovedViews; QTimer* mLayoutUpdateTimer; void scheduleLayoutUpdate() { mLayoutUpdateTimer->start(); } /** * Remove view from set, move it to mRemovedViews so that it is later * deleted. */ bool removeFromSet(DocumentView* view, DocumentViewSet* set) { DocumentViewSet::Iterator it = set->find(view); if (it == set->end()) { return false; } set->erase(it); mRemovedViews << view; scheduleLayoutUpdate(); return true; } void resetSet(DocumentViewSet* set) { qDeleteAll(*set); set->clear(); } }; DocumentViewContainer::DocumentViewContainer(QWidget* parent) : QGraphicsView(parent) , d(new DocumentViewContainerPrivate) { d->q = this; d->mScene = new QGraphicsScene(this); if (GwenviewConfig::animationMethod() == DocumentView::GLAnimation) { QGLWidget* glWidget = new QGLWidget; if (glWidget->isValid()) { setViewport(glWidget); } else { qWarning() << "Failed to initialize OpenGL support!"; delete glWidget; } } setScene(d->mScene); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setFrameStyle(QFrame::NoFrame); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); d->mLayoutUpdateTimer = new QTimer(this); d->mLayoutUpdateTimer->setInterval(0); d->mLayoutUpdateTimer->setSingleShot(true); connect(d->mLayoutUpdateTimer, &QTimer::timeout, this, &DocumentViewContainer::updateLayout); connect(GwenviewConfig::self(), &GwenviewConfig::configChanged, this, &DocumentViewContainer::slotConfigChanged); } DocumentViewContainer::~DocumentViewContainer() { delete d; } DocumentView* DocumentViewContainer::createView() { DocumentView* view = new DocumentView(d->mScene); view->setPalette(palette()); d->mAddedViews << view; view->show(); connect(view, &DocumentView::fadeInFinished, this, &DocumentViewContainer::slotFadeInFinished); d->scheduleLayoutUpdate(); return view; } void DocumentViewContainer::deleteView(DocumentView* view) { if (d->removeFromSet(view, &d->mViews)) { return; } d->removeFromSet(view, &d->mAddedViews); } DocumentView::Setup DocumentViewContainer::savedSetup(const QUrl& url) const { return d->mSetupForUrl.value(url); } void DocumentViewContainer::updateSetup(DocumentView* view) { d->mSetupForUrl[view->url()] = view->setup(); } void DocumentViewContainer::reset() { d->resetSet(&d->mViews); d->resetSet(&d->mAddedViews); d->resetSet(&d->mRemovedViews); } void DocumentViewContainer::showEvent(QShowEvent* event) { QWidget::showEvent(event); updateLayout(); } void DocumentViewContainer::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); d->mScene->setSceneRect(rect()); updateLayout(); } static bool viewLessThan(DocumentView* v1, DocumentView* v2) { return v1->sortKey() < v2->sortKey(); } void DocumentViewContainer::updateLayout() { // Stop update timer: this is useful if updateLayout() is called directly // and not through scheduleLayoutUpdate() d->mLayoutUpdateTimer->stop(); QList views = (d->mViews | d->mAddedViews).toList(); qSort(views.begin(), views.end(), viewLessThan); bool animated = GwenviewConfig::animationMethod() != DocumentView::NoAnimation; bool crossFade = d->mAddedViews.count() == 1 && d->mRemovedViews.count() == 1; if (animated && crossFade) { DocumentView* oldView = *d->mRemovedViews.begin(); DocumentView* newView = *d->mAddedViews.begin(); newView->setGeometry(rect()); QPropertyAnimation* anim = newView->fadeIn(); oldView->setZValue(-1); connect(anim, &QPropertyAnimation::finished, oldView, &DocumentView::hideAndDeleteLater); d->mRemovedViews.clear(); return; } if (!views.isEmpty()) { // Compute column count int colCount; switch (views.count()) { case 1: colCount = 1; break; case 2: colCount = 2; break; case 3: colCount = 3; break; case 4: colCount = 2; break; case 5: colCount = 3; break; case 6: colCount = 3; break; default: colCount = 3; break; } int rowCount = qCeil(views.count() / qreal(colCount)); Q_ASSERT(rowCount > 0); int viewWidth = width() / colCount; int viewHeight = height() / rowCount; int col = 0; int row = 0; Q_FOREACH(DocumentView * view, views) { QRect rect; rect.setLeft(col * viewWidth); rect.setTop(row * viewHeight); rect.setWidth(viewWidth); rect.setHeight(viewHeight); if (animated) { if (d->mViews.contains(view)) { if (rect != view->geometry()) { if (d->mAddedViews.isEmpty() && d->mRemovedViews.isEmpty()) { // View moves because of a resize view->moveTo(rect); } else { // View moves because the number of views changed, // animate the change view->moveToAnimated(rect); } } } else { view->setGeometry(rect); view->fadeIn(); } } else { // Not animated, set final geometry and opacity now view->setGeometry(rect); view->setGraphicsEffectOpacity(1); } ++col; if (col == colCount) { col = 0; ++row; } } } // Handle removed views if (animated) { Q_FOREACH(DocumentView* view, d->mRemovedViews) { view->fadeOut(); - QTimer::singleShot(DocumentView::AnimDuration, view, SLOT(deleteLater())); + QVariant durVar = style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this); + QTimer::singleShot(durVar.toInt(), view, SLOT(deleteLater())); } } else { Q_FOREACH(DocumentView* view, d->mRemovedViews) { view->deleteLater(); } QMetaObject::invokeMethod(this, "pretendFadeInFinished", Qt::QueuedConnection); } d->mRemovedViews.clear(); } void DocumentViewContainer::pretendFadeInFinished() { // Animations are disabled. Pretend all fade ins are finished so that added // views are moved to mViews Q_FOREACH(DocumentView* view, d->mAddedViews) { slotFadeInFinished(view); } } void DocumentViewContainer::slotFadeInFinished(DocumentView* view) { if (!d->mAddedViews.contains(view)) { // This can happen if user goes to next image then quickly goes to the // next one before the animation is finished. return; } d->mAddedViews.remove(view); d->mViews.insert(view); } void DocumentViewContainer::slotConfigChanged() { bool currentlyGL = qobject_cast(viewport()); bool wantGL = GwenviewConfig::animationMethod() == DocumentView::GLAnimation; if (currentlyGL != wantGL) { setViewport(wantGL ? new QGLWidget() : new QWidget()); } } void DocumentViewContainer::showMessageWidget(QGraphicsWidget* widget, Qt::Alignment align) { DocumentView* view = nullptr; if (d->mViews.isEmpty()) { GV_RETURN_IF_FAIL(!d->mAddedViews.isEmpty()); view = *d->mAddedViews.begin(); } else { view = *d->mViews.begin(); } GV_RETURN_IF_FAIL(view); widget->setParentItem(view); GraphicsWidgetFloater* floater = new GraphicsWidgetFloater(view); floater->setChildWidget(widget); floater->setAlignment(align); widget->show(); widget->setZValue(1); } void DocumentViewContainer::applyPalette(const QPalette& palette) { setPalette(palette); for (DocumentView* view : d->mViews | d->mAddedViews) { view->setPalette(palette); } } } // namespace diff --git a/lib/fullscreenbar.cpp b/lib/fullscreenbar.cpp index 7084a034..ddc02983 100644 --- a/lib/fullscreenbar.cpp +++ b/lib/fullscreenbar.cpp @@ -1,279 +1,279 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Self #include "fullscreenbar.h" // Qt #include #include #include #include #include #include #include #include #include #include +#include // KDE #include // Local namespace Gwenview { -static const int SLIDE_DURATION = 150; static const int AUTO_HIDE_CURSOR_TIMEOUT = 1000; // How long before the bar slide out after switching to fullscreen static const int INITIAL_HIDE_TIMEOUT = 2000; // Do not slide bar out if mouse is less than this amount of pixels below bar, to // prevent accidental slide outs static const int EXTRA_BAR_HEIGHT = 20; struct FullScreenBarPrivate { FullScreenBar* q; QTimeLine* mTimeLine; QTimer* mAutoHideCursorTimer; bool mAutoHidingEnabled; bool mEdgeTriggerEnabled; QTimer* mInitialHideTimer; void startTimeLine() { if (mTimeLine->state() != QTimeLine::Running) { mTimeLine->start(); } } void hideCursor() { QBitmap empty(32, 32); empty.clear(); QCursor blankCursor(empty, empty); QApplication::setOverrideCursor(blankCursor); } /** * Returns the rectangle in which the mouse must enter to trigger bar * sliding. The rectangle is in global coords. */ QRect slideInTriggerRect() const { QRect rect = QApplication::desktop()->screenGeometry(QApplication::desktop()->screenNumber(q->parentWidget())); // Take parent widget position into account because it may not be at // the top of the screen, for example when the save bar warning is // shown. rect.setHeight(q->parentWidget()->y() + q->height() + EXTRA_BAR_HEIGHT); return rect; } bool shouldHide() const { Q_ASSERT(q->parentWidget()); if (!mAutoHidingEnabled) { return false; } if (slideInTriggerRect().contains(QCursor::pos())) { return false; } if (QApplication::activePopupWidget()) { return false; } // Do not hide if a button is down, which can happen when we are // using a scroll bar. if (QApplication::mouseButtons() != Qt::NoButton) { return false; } return true; } }; FullScreenBar::FullScreenBar(QWidget* parent) : QFrame(parent) , d(new FullScreenBarPrivate) { d->q = this; d->mAutoHidingEnabled = true; d->mEdgeTriggerEnabled = true; setObjectName(QLatin1String("fullScreenBar")); - d->mTimeLine = new QTimeLine(SLIDE_DURATION, this); + d->mTimeLine = new QTimeLine(style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this), this); connect(d->mTimeLine, &QTimeLine::valueChanged, this, &FullScreenBar::moveBar); d->mAutoHideCursorTimer = new QTimer(this); d->mAutoHideCursorTimer->setInterval(AUTO_HIDE_CURSOR_TIMEOUT); d->mAutoHideCursorTimer->setSingleShot(true); connect(d->mAutoHideCursorTimer, &QTimer::timeout, this, &FullScreenBar::slotAutoHideCursorTimeout); d->mInitialHideTimer = new QTimer(this); d->mInitialHideTimer->setInterval(INITIAL_HIDE_TIMEOUT); d->mInitialHideTimer->setSingleShot(true); connect(d->mInitialHideTimer, &QTimer::timeout, this, &FullScreenBar::slideOut); hide(); } FullScreenBar::~FullScreenBar() { delete d; } QSize FullScreenBar::sizeHint() const { QSize sh = QFrame::sizeHint(); if (!layout()) { return sh; } if (layout()->expandingDirections() & Qt::Horizontal) { sh.setWidth(parentWidget()->width()); } return sh; } void FullScreenBar::moveBar(qreal value) { move(0, -height() + int(value * height())); // For some reason, if Gwenview is started with command line options to // start a slideshow, the bar might end up below the view. Calling raise() // here fixes it. raise(); } void FullScreenBar::setActivated(bool activated) { if (activated) { // Delay installation of event filter because switching to fullscreen // cause a few window adjustments, which seems to generate unwanted // mouse events, which cause the bar to slide in. QTimer::singleShot(500, this, SLOT(delayedInstallEventFilter())); adjustSize(); // Make sure the widget is visible on start move(0, 0); raise(); show(); } else { qApp->removeEventFilter(this); hide(); d->mAutoHideCursorTimer->stop(); QApplication::restoreOverrideCursor(); } } void FullScreenBar::delayedInstallEventFilter() { qApp->installEventFilter(this); if (d->shouldHide()) { d->mInitialHideTimer->start(); d->hideCursor(); } } void FullScreenBar::slotAutoHideCursorTimeout() { if (d->shouldHide()) { d->hideCursor(); } else { d->mAutoHideCursorTimer->start(); } } void FullScreenBar::slideOut() { d->mInitialHideTimer->stop(); d->mTimeLine->setDirection(QTimeLine::Backward); d->startTimeLine(); } void FullScreenBar::slideIn() { d->mInitialHideTimer->stop(); d->mTimeLine->setDirection(QTimeLine::Forward); d->startTimeLine(); } bool FullScreenBar::eventFilter(QObject* object, QEvent* event) { if (event->type() == QEvent::MouseMove) { QApplication::restoreOverrideCursor(); d->mAutoHideCursorTimer->start(); if (y() == 0) { if (d->shouldHide()) { slideOut(); } } else { QMouseEvent* mouseEvent = static_cast(event); if (d->mEdgeTriggerEnabled && mouseEvent->buttons() == 0 && d->slideInTriggerRect().contains(QCursor::pos())) { slideIn(); } } return false; } if (event->type() == QEvent::MouseButtonRelease) { // This can happen if user released the mouse after using a scrollbar // in the content (the bar does not hide while a button is down) if (y() == 0 && d->shouldHide()) { slideOut(); } return false; } // Filtering message on tooltip text for CJK to remove accelerators. // Quoting ktoolbar.cpp: // """ // CJK languages use more verbose accelerator marker: they add a Latin // letter in parenthesis, and put accelerator on that. Hence, the default // removal of ampersand only may not be enough there, instead the whole // parenthesis construct should be removed. Use KLocale's method to do this. // """ if (event->type() == QEvent::Show || event->type() == QEvent::Paint) { QToolButton* button = qobject_cast(object); if (button && !button->actions().isEmpty()) { QAction* action = button->actions().constFirst(); QString toolTip = KLocalizedString::removeAcceleratorMarker(action->toolTip()); // Filtering message requested by translators (scripting). button->setToolTip(i18nc("@info:tooltip of custom toolbar button", "%1", toolTip)); } } return false; } void FullScreenBar::setAutoHidingEnabled(bool value) { d->mAutoHidingEnabled = value; } void FullScreenBar::setEdgeTriggerEnabled(bool value) { d->mEdgeTriggerEnabled = value; } } // namespace diff --git a/lib/slidecontainer.cpp b/lib/slidecontainer.cpp index a6728d15..98759ff1 100644 --- a/lib/slidecontainer.cpp +++ b/lib/slidecontainer.cpp @@ -1,167 +1,167 @@ /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Self #include "slidecontainer.h" // KDE // Qt #include #include #include +#include namespace Gwenview { -static const int SLIDE_DURATION = 250; - SlideContainer::SlideContainer(QWidget* parent) : QFrame(parent) { mContent = 0; mSlidingOut = false; setFixedHeight(0); } QWidget* SlideContainer::content() const { return mContent; } void SlideContainer::setContent(QWidget* content) { if (mContent) { mContent->setParent(0); mContent->removeEventFilter(this); } mContent = content; if (mContent) { mContent->setParent(this); mContent->installEventFilter(this); mContent->hide(); } } void SlideContainer::animTo(int newHeight) { delete mAnim.data(); QPropertyAnimation* anim = new QPropertyAnimation(this, "slideHeight", this); - anim->setDuration(SLIDE_DURATION); + QVariant animVar = style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this); + anim->setDuration(animVar.toInt()); anim->setStartValue(slideHeight()); anim->setEndValue(newHeight); anim->start(QAbstractAnimation::DeleteWhenStopped); connect(anim, &QPropertyAnimation::finished, this, &SlideContainer::slotAnimFinished); mAnim = anim; } void SlideContainer::slideIn() { mSlidingOut = false; mContent->show(); mContent->adjustSize(); delete mAnim.data(); if (height() == mContent->height()) { return; } animTo(mContent->height()); } void SlideContainer::slideOut() { if (height() == 0) { return; } mSlidingOut = true; animTo(0); } QSize SlideContainer::sizeHint() const { if (mContent) { return mContent->sizeHint(); } else { return QSize(); } } QSize SlideContainer::minimumSizeHint() const { if (mContent) { return mContent->minimumSizeHint(); } else { return QSize(); } } void SlideContainer::resizeEvent(QResizeEvent* event) { if (mContent) { if (event->oldSize().width() != width()) { adjustContentGeometry(); } } } void SlideContainer::adjustContentGeometry() { if (mContent) { mContent->setGeometry(0, height() - mContent->height(), width(), mContent->height()); } } bool SlideContainer::eventFilter(QObject*, QEvent* event) { switch (event->type()) { case QEvent::Resize: if (!mSlidingOut && height() != 0) { animTo(mContent->height()); } break; case QEvent::LayoutRequest: updateGeometry(); break; default: break; } return false; } int SlideContainer::slideHeight() const { return isVisible() ? height() : 0; } void SlideContainer::setSlideHeight(int value) { setFixedHeight(value); adjustContentGeometry(); } void SlideContainer::slotAnimFinished() { if (height() == 0) { mSlidingOut = false; emit slidedOut(); } else { emit slidedIn(); } } } // namespace diff --git a/lib/thumbnailview/thumbnailbarview.cpp b/lib/thumbnailview/thumbnailbarview.cpp index 89c05c1d..9c5473f6 100644 --- a/lib/thumbnailview/thumbnailbarview.cpp +++ b/lib/thumbnailview/thumbnailbarview.cpp @@ -1,547 +1,542 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2008 Aurélien Gâteau Copyright 2008 Ilya Konkov This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "thumbnailbarview.h" // Qt #include #include #include #include #include #include #include #include #include #ifdef WINDOWS_PROXY_STYLE #include #endif // KDE #include // Local #include "lib/hud/hudtheme.h" #include "lib/paintutils.h" #include "lib/thumbnailview/abstractthumbnailviewhelper.h" #include "gwenviewconfig.h" namespace Gwenview { -/** - * Duration in ms of the smooth scroll - */ -const int SMOOTH_SCROLL_DURATION = 250; - /** * Space between the item outer rect and the content, and between the * thumbnail and the caption */ const int ITEM_MARGIN = 5; /** How dark is the shadow, 0 is invisible, 255 is as dark as possible */ const int SHADOW_STRENGTH = 127; /** How many pixels around the thumbnail are shadowed */ const int SHADOW_SIZE = 4; struct ThumbnailBarItemDelegatePrivate { // Key is height * 1000 + width typedef QMap ShadowCache; mutable ShadowCache mShadowCache; ThumbnailBarItemDelegate* q; ThumbnailView* mView; QToolButton* mToggleSelectionButton; QColor mBorderColor; QPersistentModelIndex mIndexUnderCursor; void setupToggleSelectionButton() { mToggleSelectionButton = new QToolButton(mView->viewport()); mToggleSelectionButton->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); mToggleSelectionButton->hide(); QObject::connect(mToggleSelectionButton, &QToolButton::clicked, q, &ThumbnailBarItemDelegate::toggleSelection); } void showToolTip(QHelpEvent* helpEvent) { QModelIndex index = mView->indexAt(helpEvent->pos()); if (!index.isValid()) { return; } QString fullText = index.data().toString(); QPoint pos = QCursor::pos(); QToolTip::showText(pos, fullText, mView); } void drawShadow(QPainter* painter, const QRect& rect) const { const QPoint shadowOffset(-SHADOW_SIZE, -SHADOW_SIZE + 1); int key = rect.height() * 1000 + rect.width(); ShadowCache::Iterator it = mShadowCache.find(key); if (it == mShadowCache.end()) { QSize size = QSize(rect.width() + 2 * SHADOW_SIZE, rect.height() + 2 * SHADOW_SIZE); QColor color(0, 0, 0, SHADOW_STRENGTH); QPixmap shadow = PaintUtils::generateFuzzyRect(size, color, SHADOW_SIZE); it = mShadowCache.insert(key, shadow); } painter->drawPixmap(rect.topLeft() + shadowOffset, it.value()); } bool hoverEventFilter(QHoverEvent* event) { QModelIndex index = mView->indexAt(event->pos()); if (index != mIndexUnderCursor) { updateHoverUi(index); } return false; } void updateHoverUi(const QModelIndex& index) { mIndexUnderCursor = index; if (mIndexUnderCursor.isValid() && GwenviewConfig::thumbnailActions() != ThumbnailActions::None) { updateToggleSelectionButton(); const QRect rect = mView->visualRect(mIndexUnderCursor); mToggleSelectionButton->move(rect.topLeft() + QPoint(2, 2)); mToggleSelectionButton->show(); } else { mToggleSelectionButton->hide(); } } void updateToggleSelectionButton() { bool isSelected = mView->selectionModel()->isSelected(mIndexUnderCursor); mToggleSelectionButton->setIcon(QIcon::fromTheme(isSelected ? QStringLiteral("list-remove") : QStringLiteral("list-add"))); } }; ThumbnailBarItemDelegate::ThumbnailBarItemDelegate(ThumbnailView* view) : QAbstractItemDelegate(view) , d(new ThumbnailBarItemDelegatePrivate) { d->q = this; d->mView = view; d->setupToggleSelectionButton(); view->viewport()->installEventFilter(this); // Set this attribute so that the viewport receives QEvent::HoverMove and // QEvent::HoverLeave events. We use these events in the event filter // installed on the viewport. // Some styles set this attribute themselves (Oxygen and Skulpture do) but // others do not (Plastique, Cleanlooks...) view->viewport()->setAttribute(Qt::WA_Hover); d->mBorderColor = PaintUtils::alphaAdjustedF(QColor(Qt::white), 0.65); connect(view, &ThumbnailView::selectionChangedSignal, [this]() { d->updateToggleSelectionButton(); }); } QSize ThumbnailBarItemDelegate::sizeHint(const QStyleOptionViewItem & /*option*/, const QModelIndex & index) const { QSize size; if (d->mView->thumbnailScaleMode() == ThumbnailView::ScaleToFit) { size = d->mView->gridSize(); } else { QPixmap thumbnailPix = d->mView->thumbnailForIndex(index); size = thumbnailPix.size(); size.rwidth() += ITEM_MARGIN * 2; size.rheight() += ITEM_MARGIN * 2; } return size; } bool ThumbnailBarItemDelegate::eventFilter(QObject*, QEvent* event) { switch (event->type()) { case QEvent::ToolTip: d->showToolTip(static_cast(event)); return true; case QEvent::HoverMove: case QEvent::HoverLeave: return d->hoverEventFilter(static_cast(event)); default: break; } return false; } void ThumbnailBarItemDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const { bool isSelected = option.state & QStyle::State_Selected; bool isCurrent = d->mView->selectionModel()->currentIndex() == index; QPixmap thumbnailPix = d->mView->thumbnailForIndex(index); QRect rect = option.rect; QStyleOptionViewItem opt = option; const QWidget* widget = opt.widget; QStyle* style = widget ? widget->style() : QApplication::style(); if (isSelected && !isCurrent) { // Draw selected but not current item backgrounds with some transparency // so that the current item stands out. painter->setOpacity(.33); } style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget); painter->setOpacity(1); // Draw thumbnail if (!thumbnailPix.isNull()) { QRect thumbnailRect = QRect( rect.left() + (rect.width() - thumbnailPix.width()) / 2, rect.top() + (rect.height() - thumbnailPix.height()) / 2 - 1, thumbnailPix.width(), thumbnailPix.height()); if (!thumbnailPix.hasAlphaChannel()) { d->drawShadow(painter, thumbnailRect); painter->setPen(d->mBorderColor); painter->setRenderHint(QPainter::Antialiasing, false); QRect borderRect = thumbnailRect.adjusted(-1, -1, 0, 0); painter->drawRect(borderRect); } painter->drawPixmap(thumbnailRect.left(), thumbnailRect.top(), thumbnailPix); // Draw busy indicator if (d->mView->isBusy(index)) { QPixmap pix = d->mView->busySequenceCurrentPixmap(); painter->drawPixmap( thumbnailRect.left() + (thumbnailRect.width() - pix.width()) / 2, thumbnailRect.top() + (thumbnailRect.height() - pix.height()) / 2, pix); } } } void ThumbnailBarItemDelegate::toggleSelection() { d->mView->selectionModel()->select(d->mIndexUnderCursor, QItemSelectionModel::Toggle); } ThumbnailBarItemDelegate::~ThumbnailBarItemDelegate() { delete d; } //this is disabled by David Edmundson as I can't figure out how to port it //I hope with breeze being the default we don't want to start making our own styles anyway #ifdef WINDOWS_PROXY_STYLE /** * This proxy style makes it possible to override the value returned by * styleHint() which leads to not-so-nice results with some styles. * * We cannot use QProxyStyle because it takes ownership of the base style, * which causes crash when user change styles. */ class ProxyStyle : public QWindowsStyle { public: ProxyStyle() : QWindowsStyle() { } void drawPrimitive(PrimitiveElement pe, const QStyleOption *opt, QPainter *p, const QWidget *w = 0) const { QApplication::style()->drawPrimitive(pe, opt, p, w); } void drawControl(ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w = 0) const { QApplication::style()->drawControl(element, opt, p, w); } void drawComplexControl(ComplexControl cc, const QStyleOptionComplex *opt, QPainter *p, const QWidget *w = 0) const { QApplication::style()->drawComplexControl(cc, opt, p, w); } int styleHint(StyleHint sh, const QStyleOption *opt = 0, const QWidget *w = 0, QStyleHintReturn *shret = 0) const { switch (sh) { case SH_ItemView_ShowDecorationSelected: // We want the highlight to cover our thumbnail return true; case SH_ScrollView_FrameOnlyAroundContents: // Ensure the frame does not include the scrollbar. This ensure the // scrollbar touches the edge of the window and thus can touch the // edge of the screen when maximized return false; default: return QApplication::style()->styleHint(sh, opt, w, shret); } } void polish(QApplication* application) { QApplication::style()->polish(application); } void polish(QPalette& palette) { QApplication::style()->polish(palette); } void polish(QWidget* widget) { QApplication::style()->polish(widget); } void unpolish(QWidget* widget) { QApplication::style()->unpolish(widget); } void unpolish(QApplication* application) { QApplication::style()->unpolish(application); } int pixelMetric(PixelMetric pm, const QStyleOption* opt, const QWidget* widget) const { switch (pm) { case PM_MaximumDragDistance: // Ensure the fullscreen thumbnailbar does not go away while // dragging the scrollbar if the mouse cursor is too far away from // the widget return -1; default: return QApplication::style()->pixelMetric(pm, opt, widget); } } }; #endif// WINDOWS_PROXY_STYLE typedef int (QSize::*QSizeDimension)() const; struct ThumbnailBarViewPrivate { ThumbnailBarView* q; QStyle* mStyle; QTimeLine* mTimeLine; Qt::Orientation mOrientation; int mRowCount; QScrollBar* scrollBar() const { return mOrientation == Qt::Horizontal ? q->horizontalScrollBar() : q->verticalScrollBar(); } QSizeDimension mainDimension() const { return mOrientation == Qt::Horizontal ? &QSize::width : &QSize::height; } QSizeDimension oppositeDimension() const { return mOrientation == Qt::Horizontal ? &QSize::height : &QSize::width; } void smoothScrollTo(const QModelIndex& index) { if (!index.isValid()) { return; } const QRect rect = q->visualRect(index); int oldValue = scrollBar()->value(); int newValue = scrollToValue(rect); if (mTimeLine->state() == QTimeLine::Running) { mTimeLine->stop(); } mTimeLine->setFrameRange(oldValue, newValue); mTimeLine->start(); } int scrollToValue(const QRect& rect) { // This code is a much simplified version of // QListViewPrivate::horizontalScrollToValue() const QRect area = q->viewport()->rect(); int value = scrollBar()->value(); if (mOrientation == Qt::Horizontal) { if (q->isRightToLeft()) { value += (area.width() - rect.width()) / 2 - rect.left(); } else { value += rect.left() - (area.width() - rect.width()) / 2; } } else { value += rect.top() - (area.height() - rect.height()) / 2; } return value; } void updateMinMaxSizes() { QSizeDimension dimension = oppositeDimension(); int scrollBarSize = (scrollBar()->sizeHint().*dimension)(); QSize minSize(0, mRowCount * 48 + scrollBarSize); QSize maxSize(QWIDGETSIZE_MAX, mRowCount * 256 + scrollBarSize); if (mOrientation == Qt::Vertical) { minSize.transpose(); maxSize.transpose(); } q->setMinimumSize(minSize); q->setMaximumSize(maxSize); } void updateThumbnailSize() { QSizeDimension dimension = oppositeDimension(); int scrollBarSize = (scrollBar()->sizeHint().*dimension)(); int widgetSize = (q->size().*dimension)(); if (mRowCount > 1) { // Decrease widgetSize because otherwise the view sometimes wraps at // mRowCount-1 instead of mRowCount. Probably because gridSize * // mRowCount is too close to widgetSize. --widgetSize; } int gridWidth, gridHeight; if (mOrientation == Qt::Horizontal) { gridHeight = (widgetSize - scrollBarSize - 2 * q->frameWidth()) / mRowCount; gridWidth = qRound(gridHeight * q->thumbnailAspectRatio()); } else { gridWidth = (widgetSize - scrollBarSize - 2 * q->frameWidth()) / mRowCount; gridHeight = qRound(gridWidth / q->thumbnailAspectRatio()); } if (q->thumbnailScaleMode() == ThumbnailView::ScaleToFit) { q->setGridSize(QSize(gridWidth, gridHeight)); } q->setThumbnailWidth(gridWidth - ITEM_MARGIN * 2); } }; ThumbnailBarView::ThumbnailBarView(QWidget* parent) : ThumbnailView(parent) , d(new ThumbnailBarViewPrivate) { d->q = this; - d->mTimeLine = new QTimeLine(SMOOTH_SCROLL_DURATION, this); + d->mTimeLine = new QTimeLine(style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this), this); connect(d->mTimeLine, &QTimeLine::frameChanged, this, &ThumbnailBarView::slotFrameChanged); d->mRowCount = 1; d->mOrientation = Qt::Vertical; // To pass value-has-changed check in setOrientation() setOrientation(Qt::Horizontal); setObjectName(QLatin1String("thumbnailBarView")); setWrapping(true); #ifdef WINDOWS_PROXY_STYLE d->mStyle = new ProxyStyle; setStyle(d->mStyle); #endif } ThumbnailBarView::~ThumbnailBarView() { #ifdef WINDOWS_PROXY_STYLE delete d->mStyle; #endif delete d; } Qt::Orientation ThumbnailBarView::orientation() const { return d->mOrientation; } void ThumbnailBarView::setOrientation(Qt::Orientation orientation) { if (d->mOrientation == orientation) { return; } d->mOrientation = orientation; if (d->mOrientation == Qt::Vertical) { setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setFlow(LeftToRight); } else { setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setFlow(TopToBottom); } d->updateMinMaxSizes(); } void ThumbnailBarView::slotFrameChanged(int value) { d->scrollBar()->setValue(value); } void ThumbnailBarView::resizeEvent(QResizeEvent *event) { ThumbnailView::resizeEvent(event); d->updateThumbnailSize(); } void ThumbnailBarView::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { ThumbnailView::selectionChanged(selected, deselected); QModelIndexList oldList = deselected.indexes(); QModelIndexList newList = selected.indexes(); // Only scroll the list if the user went from one image to another. If the // user just unselected one image from a set of two, he might want to // reselect it again, scrolling the thumbnails would prevent him from // reselecting it by clicking again without moving the mouse. if (oldList.count() == 1 && newList.count() == 1 && isVisible()) { d->smoothScrollTo(newList.first()); } } void ThumbnailBarView::wheelEvent(QWheelEvent* event) { d->scrollBar()->setValue(d->scrollBar()->value() - event->delta()); } int ThumbnailBarView::rowCount() const { return d->mRowCount; } void ThumbnailBarView::setRowCount(int rowCount) { Q_ASSERT(rowCount > 0); d->mRowCount = rowCount; d->updateMinMaxSizes(); d->updateThumbnailSize(); } } // namespace