diff --git a/app/fullscreencontent.cpp b/app/fullscreencontent.cpp index c7c6a270..4768f49f 100644 --- a/app/fullscreencontent.cpp +++ b/app/fullscreencontent.cpp @@ -1,572 +1,572 @@ // 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 "fullscreencontent.h" // Qt #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include // Local #include #include #include #include #include #include #include #include #include #include namespace Gwenview { /** * A widget which behaves more or less like a QToolBar, but which uses real * widgets for the toolbar items. We need a real widget to be able to position * the option menu. */ class FullScreenToolBar : public QWidget { public: explicit FullScreenToolBar(QWidget* parent = nullptr) : QWidget(parent) , mLayout(new QHBoxLayout(this)) { setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); mLayout->setSpacing(0); mLayout->setContentsMargins(0, 0, 0, 0); } void addAction(QAction* action) { QToolButton* button = new QToolButton; button->setDefaultAction(action); button->setToolButtonStyle(Qt::ToolButtonIconOnly); button->setAutoRaise(true); const int extent = KIconLoader::SizeMedium; button->setIconSize(QSize(extent, extent)); mLayout->addWidget(button); } void addSeparator() { mLayout->addSpacing(QApplication::style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); } void addStretch() { mLayout->addStretch(); } void setDirection(QBoxLayout::Direction direction) { mLayout->setDirection(direction); } private: QBoxLayout* mLayout; }; FullScreenContent::FullScreenContent(QObject* parent, GvCore* gvCore) : QObject(parent) { mGvCore = gvCore; mViewPageVisible = false; } void FullScreenContent::init(KActionCollection* actionCollection, QWidget* autoHideParentWidget, SlideShow* slideShow) { mSlideShow = slideShow; mActionCollection = actionCollection; connect(actionCollection->action("view"), &QAction::toggled, this, &FullScreenContent::slotViewModeActionToggled); // mAutoHideContainer mAutoHideContainer = new FullScreenBar(autoHideParentWidget); mAutoHideContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); QVBoxLayout* layout = new QVBoxLayout(mAutoHideContainer); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); EventWatcher::install(autoHideParentWidget, QEvent::Resize, this, SLOT(adjustSize())); // mContent mContent = new QWidget; mContent->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); mContent->setAutoFillBackground(true); EventWatcher::install(mContent, QEvent::Show, this, SLOT(updateCurrentUrlWidgets())); layout->addWidget(mContent); createOptionsAction(); // mToolBar mToolBar = new FullScreenToolBar(mContent); #define addAction(name) mToolBar->addAction(actionCollection->action(name)) addAction("browse"); addAction("view"); mToolBar->addSeparator(); addAction("go_previous"); addAction("toggle_slideshow"); addAction("go_next"); mToolBar->addSeparator(); addAction("rotate_left"); addAction("rotate_right"); #undef addAction mToolBarShadow = new ShadowFilter(mToolBar); // mInformationLabel mInformationLabel = new QLabel; mInformationLabel->setWordWrap(true); mInformationLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); // mDocumentCountLabel mDocumentCountLabel = new QLabel; mInformationContainer = new QWidget; const int infoContainerTopMargin = 6; const int infoContainerBottomMargin = 2; mInformationContainer->setContentsMargins(6, infoContainerTopMargin, 6, infoContainerBottomMargin); mInformationContainer->setAutoFillBackground(true); mInformationContainer->setBackgroundRole(QPalette::Mid); mInformationContainerShadow = new ShadowFilter(mInformationContainer); QHBoxLayout* hLayout = new QHBoxLayout(mInformationContainer); hLayout->setContentsMargins(0, 0, 0, 0); hLayout->addWidget(mInformationLabel); hLayout->addWidget(mDocumentCountLabel); // Thumbnail bar mThumbnailBar = new ThumbnailBarView(mContent); mThumbnailBar->setThumbnailScaleMode(ThumbnailView::ScaleToSquare); ThumbnailBarItemDelegate* delegate = new ThumbnailBarItemDelegate(mThumbnailBar); mThumbnailBar->setItemDelegate(delegate); mThumbnailBar->setSelectionMode(QAbstractItemView::ExtendedSelection); // Calculate minimum bar height to give mInformationLabel exactly two lines height const int lineHeight = mInformationLabel->fontMetrics().lineSpacing(); mMinimumThumbnailBarHeight = mToolBar->sizeHint().height() + lineHeight * 2 + infoContainerTopMargin + infoContainerBottomMargin; // Ensure document count is updated when items added/removed from folder connect(mThumbnailBar, &ThumbnailBarView::rowsInsertedSignal, this, &FullScreenContent::updateDocumentCountLabel); connect(mThumbnailBar, &ThumbnailBarView::rowsRemovedSignal, this, &FullScreenContent::updateDocumentCountLabel); connect(mThumbnailBar, &ThumbnailBarView::indexActivated, this, &FullScreenContent::updateDocumentCountLabel); // Right bar mRightToolBar = new FullScreenToolBar(mContent); mRightToolBar->addAction(mActionCollection->action("leave_fullscreen")); mRightToolBar->addAction(mOptionsAction); mRightToolBar->addStretch(); mRightToolBar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); mRightToolBarShadow = new ShadowFilter(mRightToolBar); updateLayout(); updateContainerAppearance(); } ThumbnailBarView* FullScreenContent::thumbnailBar() const { return mThumbnailBar; } void FullScreenContent::setCurrentUrl(const QUrl &url) { if (url.isEmpty()) { mCurrentDocument = Document::Ptr(); } else { mCurrentDocument = DocumentFactory::instance()->load(url); connect(mCurrentDocument.data(), &Document::metaInfoUpdated, this, &FullScreenContent::updateCurrentUrlWidgets); } updateCurrentUrlWidgets(); // Give the thumbnail view time to update its "current index" QTimer::singleShot(0, this, &FullScreenContent::updateDocumentCountLabel); } void FullScreenContent::updateInformationLabel() { if (!mCurrentDocument) { return; } if (!mInformationLabel->isVisible()) { return; } ImageMetaInfoModel* model = mCurrentDocument->metaInfo(); QStringList valueList; Q_FOREACH(const QString & key, GwenviewConfig::fullScreenPreferredMetaInfoKeyList()) { const QString value = model->getValueForKey(key); if (value.length() > 0) { valueList << value; } } QString text = valueList.join(i18nc("@item:intext fullscreen meta info separator", ", ")); mInformationLabel->setText(text); } void FullScreenContent::updateCurrentUrlWidgets() { updateInformationLabel(); updateMetaInfoDialog(); } void FullScreenContent::showImageMetaInfoDialog() { if (!mImageMetaInfoDialog) { mImageMetaInfoDialog = new ImageMetaInfoDialog(mInformationLabel); // Do not let the fullscreen theme propagate to this dialog for now, // it's already quite complicated to create a theme mImageMetaInfoDialog->setStyle(QApplication::style()); mImageMetaInfoDialog->setAttribute(Qt::WA_DeleteOnClose, true); connect(mImageMetaInfoDialog.data(), &ImageMetaInfoDialog::preferredMetaInfoKeyListChanged, this, &FullScreenContent::slotPreferredMetaInfoKeyListChanged); connect(mImageMetaInfoDialog.data(), &QObject::destroyed, this, &FullScreenContent::slotImageMetaInfoDialogClosed); } if (mCurrentDocument) { mImageMetaInfoDialog->setMetaInfo(mCurrentDocument->metaInfo(), GwenviewConfig::fullScreenPreferredMetaInfoKeyList()); } mAutoHideContainer->setAutoHidingEnabled(false); mImageMetaInfoDialog->show(); } void FullScreenContent::slotPreferredMetaInfoKeyListChanged(const QStringList& list) { GwenviewConfig::setFullScreenPreferredMetaInfoKeyList(list); GwenviewConfig::self()->save(); updateInformationLabel(); } void FullScreenContent::updateMetaInfoDialog() { if (!mImageMetaInfoDialog) { return; } ImageMetaInfoModel* model = mCurrentDocument ? mCurrentDocument->metaInfo() : nullptr; mImageMetaInfoDialog->setMetaInfo(model, GwenviewConfig::fullScreenPreferredMetaInfoKeyList()); } static QString formatSlideShowIntervalText(int value) { return i18ncp("Slideshow interval in seconds", "%1 sec", "%1 secs", value); } void FullScreenContent::updateLayout() { delete mContent->layout(); if (GwenviewConfig::showFullScreenThumbnails()) { mRightToolBar->setDirection(QBoxLayout::TopToBottom); QHBoxLayout* layout = new QHBoxLayout(mContent); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); QVBoxLayout* vLayout; // First column vLayout = new QVBoxLayout; vLayout->addWidget(mToolBar); vLayout->addWidget(mInformationContainer); layout->addLayout(vLayout); // Second column layout->addSpacing(2); layout->addWidget(mThumbnailBar); layout->addSpacing(2); // Third column vLayout = new QVBoxLayout; vLayout->addWidget(mRightToolBar); layout->addLayout(vLayout); mInformationContainer->setMaximumWidth(mToolBar->sizeHint().width()); const int barHeight = qMax(GwenviewConfig::fullScreenBarHeight(), mMinimumThumbnailBarHeight); mThumbnailBar->setFixedHeight(barHeight); mAutoHideContainer->setFixedHeight(barHeight); mInformationLabel->setAlignment(Qt::AlignTop); mDocumentCountLabel->setAlignment(Qt::AlignBottom | Qt::AlignRight); // Shadows mToolBarShadow->reset(); mToolBarShadow->setShadow(ShadowFilter::RightEdge, QColor(0, 0, 0, 64)); mToolBarShadow->setShadow(ShadowFilter::BottomEdge, QColor(255, 255, 255, 8)); mInformationContainerShadow->reset(); mInformationContainerShadow->setShadow(ShadowFilter::LeftEdge, QColor(0, 0, 0, 64)); mInformationContainerShadow->setShadow(ShadowFilter::TopEdge, QColor(0, 0, 0, 64)); mInformationContainerShadow->setShadow(ShadowFilter::RightEdge, QColor(0, 0, 0, 128)); mInformationContainerShadow->setShadow(ShadowFilter::BottomEdge, QColor(255, 255, 255, 8)); mRightToolBarShadow->reset(); mRightToolBarShadow->setShadow(ShadowFilter::LeftEdge, QColor(0, 0, 0, 64)); mRightToolBarShadow->setShadow(ShadowFilter::BottomEdge, QColor(255, 255, 255, 8)); } else { mRightToolBar->setDirection(QBoxLayout::RightToLeft); QHBoxLayout* layout = new QHBoxLayout(mContent); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); layout->addWidget(mToolBar); layout->addWidget(mInformationContainer); layout->addWidget(mRightToolBar); mInformationContainer->setMaximumWidth(QWIDGETSIZE_MAX); mAutoHideContainer->setFixedHeight(mToolBar->sizeHint().height()); mInformationLabel->setAlignment(Qt::AlignVCenter); mDocumentCountLabel->setAlignment(Qt::AlignVCenter | Qt::AlignRight); // Shadows mToolBarShadow->reset(); mInformationContainerShadow->reset(); mInformationContainerShadow->setShadow(ShadowFilter::LeftEdge, QColor(0, 0, 0, 64)); mInformationContainerShadow->setShadow(ShadowFilter::RightEdge, QColor(0, 0, 0, 32)); mRightToolBarShadow->reset(); mRightToolBarShadow->setShadow(ShadowFilter::LeftEdge, QColor(255, 255, 255, 8)); } } void FullScreenContent::updateContainerAppearance() { if (!mContent->window()->isFullScreen() || !mViewPageVisible) { mAutoHideContainer->setActivated(false); return; } mThumbnailBar->setVisible(GwenviewConfig::showFullScreenThumbnails()); mAutoHideContainer->adjustSize(); mAutoHideContainer->setActivated(true); } void FullScreenContent::adjustSize() { if (mContent->window()->isFullScreen() && mViewPageVisible) { mAutoHideContainer->adjustSize(); } } void FullScreenContent::createOptionsAction() { // We do not use a KActionMenu because: // // - It causes the button to show a small down arrow on its right, // which makes it wider // // - We can't control where the menu shows: in no-thumbnail-mode, the // menu should not be aligned to the right edge of the screen because // if the mode is changed to thumbnail-mode, we want the option button // to remain visible mOptionsAction = new QAction(this); mOptionsAction->setPriority(QAction::LowPriority); mOptionsAction->setIcon(QIcon::fromTheme("configure")); mOptionsAction->setToolTip(i18nc("@info:tooltip", "Configure full screen mode")); QObject::connect(mOptionsAction, &QAction::triggered, this, &FullScreenContent::showOptionsMenu); } void FullScreenContent::updateSlideShowIntervalLabel() { Q_ASSERT(mConfigWidget); int value = mConfigWidget->mSlideShowIntervalSlider->value(); QString text = formatSlideShowIntervalText(value); mConfigWidget->mSlideShowIntervalLabel->setText(text); } void FullScreenContent::setFullScreenBarHeight(int value) { mThumbnailBar->setFixedHeight(value); mAutoHideContainer->setFixedHeight(value); GwenviewConfig::setFullScreenBarHeight(value); } void FullScreenContent::showOptionsMenu() { Q_ASSERT(!mConfigWidget); mConfigWidget = new FullScreenConfigWidget; FullScreenConfigWidget* widget = mConfigWidget; // Put widget in a menu QMenu menu; QWidgetAction* action = new QWidgetAction(&menu); action->setDefaultWidget(widget); menu.addAction(action); // Slideshow checkboxes widget->mSlideShowLoopCheckBox->setChecked(mSlideShow->loopAction()->isChecked()); connect(widget->mSlideShowLoopCheckBox, &QAbstractButton::toggled, mSlideShow->loopAction(), &QAction::trigger); widget->mSlideShowRandomCheckBox->setChecked(mSlideShow->randomAction()->isChecked()); connect(widget->mSlideShowRandomCheckBox, &QAbstractButton::toggled, mSlideShow->randomAction(), &QAction::trigger); // Interval slider widget->mSlideShowIntervalSlider->setValue(int(GwenviewConfig::interval())); connect(widget->mSlideShowIntervalSlider, &QAbstractSlider::valueChanged, mSlideShow, &SlideShow::setInterval); connect(widget->mSlideShowIntervalSlider, &QAbstractSlider::valueChanged, this, &FullScreenContent::updateSlideShowIntervalLabel); // Interval label QString text = formatSlideShowIntervalText(88); - int width = widget->mSlideShowIntervalLabel->fontMetrics().width(text); + int width = widget->mSlideShowIntervalLabel->fontMetrics().boundingRect(text).width(); widget->mSlideShowIntervalLabel->setFixedWidth(width); updateSlideShowIntervalLabel(); // Image information connect(widget->mConfigureDisplayedInformationButton, &QAbstractButton::clicked, this, &FullScreenContent::showImageMetaInfoDialog); // Thumbnails widget->mThumbnailGroupBox->setVisible(mViewPageVisible); if (mViewPageVisible) { widget->mShowThumbnailsCheckBox->setChecked(GwenviewConfig::showFullScreenThumbnails()); widget->mHeightSlider->setMinimum(mMinimumThumbnailBarHeight); widget->mHeightSlider->setValue(mThumbnailBar->height()); connect(widget->mShowThumbnailsCheckBox, &QAbstractButton::toggled, this, &FullScreenContent::slotShowThumbnailsToggled); connect(widget->mHeightSlider, &QAbstractSlider::valueChanged, this, &FullScreenContent::setFullScreenBarHeight); } // Show menu below its button QPoint pos; QWidget* button = mOptionsAction->associatedWidgets().constFirst(); Q_ASSERT(button); qWarning() << button << button->geometry(); if (QApplication::isRightToLeft()) { pos = button->mapToGlobal(button->rect().bottomLeft()); } else { pos = button->mapToGlobal(button->rect().bottomRight()); pos.rx() -= menu.sizeHint().width(); } qWarning() << pos; menu.exec(pos); } void FullScreenContent::setFullScreenMode(bool fullScreenMode) { Q_UNUSED(fullScreenMode); updateContainerAppearance(); setupThumbnailBarStyleSheet(); } void FullScreenContent::setDistractionFreeMode(bool distractionFreeMode) { mAutoHideContainer->setEdgeTriggerEnabled(!distractionFreeMode); } void FullScreenContent::slotImageMetaInfoDialogClosed() { mAutoHideContainer->setAutoHidingEnabled(true); } void FullScreenContent::slotShowThumbnailsToggled(bool value) { GwenviewConfig::setShowFullScreenThumbnails(value); GwenviewConfig::self()->save(); mThumbnailBar->setVisible(value); updateLayout(); mContent->adjustSize(); mAutoHideContainer->adjustSize(); } void FullScreenContent::slotViewModeActionToggled(bool value) { mViewPageVisible = value; updateContainerAppearance(); } void FullScreenContent::setupThumbnailBarStyleSheet() { const QPalette pal = mGvCore->palette(GvCore::NormalPalette); const QPalette fsPal = mGvCore->palette(GvCore::FullScreenPalette); QColor bgColor = fsPal.color(QPalette::Normal, QPalette::Base); QColor bgSelColor = pal.color(QPalette::Normal, QPalette::Highlight); QColor bgHovColor = pal.color(QPalette::Normal, QPalette::Highlight); // Darken the select color a little to suit dark theme of fullscreen mode bgSelColor.setHsv(bgSelColor.hue(), bgSelColor.saturation(), (bgSelColor.value() * 0.8)); // Calculate hover color based on background color in case it changes (matches ViewMainPage thumbnail bar) bgHovColor.setHsv(bgHovColor.hue(), (bgHovColor.saturation() / 2), ((bgHovColor.value() + bgColor.value()) / 2)); QString genCss = "QListView {" " background-color: %1;" "}"; genCss = genCss.arg(StyleSheetUtils::rgba(bgColor)); QString itemSelCss = "QListView::item:selected {" " background-color: %1;" "}"; itemSelCss = itemSelCss.arg(StyleSheetUtils::rgba(bgSelColor)); QString itemHovCss = "QListView::item:hover:!selected {" " background-color: %1;" "}"; itemHovCss = itemHovCss.arg(StyleSheetUtils::rgba(bgHovColor)); QString css = genCss + itemSelCss + itemHovCss; mThumbnailBar->setStyleSheet(css); } void FullScreenContent::updateDocumentCountLabel() { const int current = mThumbnailBar->currentIndex().row() + 1; const int total = mThumbnailBar->model()->rowCount(); const QString text = i18nc("@info:status %1 current document index, %2 total documents", "%1 of %2", current, total); mDocumentCountLabel->setText(text); } } // namespace diff --git a/app/infocontextmanageritem.cpp b/app/infocontextmanageritem.cpp index c6bb7e0f..9a01fea9 100644 --- a/app/infocontextmanageritem.cpp +++ b/app/infocontextmanageritem.cpp @@ -1,383 +1,383 @@ /* 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. */ #include "infocontextmanageritem.h" // Qt #include #include #include #include #include // KDE #include #include // Local #include "imagemetainfodialog.h" #include "sidebar.h" #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 /** * This widget is capable of showing multiple lines of key/value pairs. */ class KeyValueWidget : public QWidget { struct Row { Row(QWidget* parent) : keyLabel(new QLabel(parent)) , valueLabel(new QLabel(parent)) { initLabel(keyLabel); initLabel(valueLabel); QPalette pal = keyLabel->palette(); QColor color = pal.color(QPalette::WindowText); color.setAlphaF(0.65); pal.setColor(QPalette::WindowText, color); keyLabel->setPalette(pal); valueLabel->setContentsMargins(6, 0, 0, 6); } ~Row() { delete keyLabel; delete valueLabel; } int setLabelGeometries(int rowY, int labelWidth) { int labelHeight = keyLabel->heightForWidth(labelWidth); keyLabel->setGeometry(0, rowY, labelWidth, labelHeight); rowY += labelHeight; labelHeight = valueLabel->heightForWidth(labelWidth); valueLabel->setGeometry(0, rowY, labelWidth, labelHeight); rowY += labelHeight; return rowY; } int heightForWidth(int width) const { return keyLabel->heightForWidth(width) + valueLabel->heightForWidth(width); } static void initLabel(QLabel* label) { label->setWordWrap(true); label->show(); label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse); } QLabel* keyLabel; QLabel* valueLabel; }; public: explicit KeyValueWidget(QWidget* parent = nullptr) : QWidget(parent) { QSizePolicy policy(QSizePolicy::Preferred, QSizePolicy::Fixed); policy.setHeightForWidth(true); setSizePolicy(policy); } QSize sizeHint() const override { int width = 150; int height = heightForWidth(width); return QSize(width, height); } int heightForWidth(int w) const override { int height = 0; Q_FOREACH(Row* row, mRows) { height += row->heightForWidth(w); } return height; } void clear() { qDeleteAll(mRows); mRows.clear(); updateGeometry(); } void addRow(const QString& key, const QString& value) { Row* row = new Row(this); row->keyLabel->setText(i18nc( "@item:intext %1 is a key, we append a colon to it. A value is displayed after", "%1:", key)); row->valueLabel->setText(value); mRows << row; } static bool rowsLessThan(const Row* row1, const Row* row2) { return row1->keyLabel->text() < row2->keyLabel->text(); } void finishAddRows() { - qSort(mRows.begin(), mRows.end(), KeyValueWidget::rowsLessThan); + std::sort(mRows.begin(), mRows.end(), KeyValueWidget::rowsLessThan); updateGeometry(); } void layoutRows() { // Layout labels manually: I tried to use a QVBoxLayout but for some // reason when using software animations the widget blinks when going // from one image to another int rowY = 0; const int labelWidth = width(); Q_FOREACH(Row* row, mRows) { rowY = row->setLabelGeometries(rowY, labelWidth); } } protected: void showEvent(QShowEvent* event) override { QWidget::showEvent(event); layoutRows(); } void resizeEvent(QResizeEvent* event) override { QWidget::resizeEvent(event); layoutRows(); } private: QVector mRows; }; struct InfoContextManagerItemPrivate { InfoContextManagerItem* q; SideBarGroup* mGroup; // One selection fields QScrollArea* mOneFileWidget; KeyValueWidget* mKeyValueWidget; Document::Ptr mDocument; // Multiple selection fields QLabel* mMultipleFilesLabel; QPointer mImageMetaInfoDialog; void updateMetaInfoDialog() { if (!mImageMetaInfoDialog) { return; } ImageMetaInfoModel* model = mDocument ? mDocument->metaInfo() : nullptr; mImageMetaInfoDialog->setMetaInfo(model, GwenviewConfig::preferredMetaInfoKeyList()); } void setupGroup() { mOneFileWidget = new QScrollArea(); mOneFileWidget->setFrameStyle(QFrame::NoFrame); mOneFileWidget->setWidgetResizable(true); mKeyValueWidget = new KeyValueWidget; QLabel* moreLabel = new QLabel(mOneFileWidget); moreLabel->setText(QStringLiteral("%1").arg(i18nc("@action show more image meta info", "More..."))); moreLabel->setAlignment(Qt::AlignRight); QWidget* content = new QWidget; QVBoxLayout* layout = new QVBoxLayout(content); layout->setMargin(2); layout->setSpacing(2); layout->addWidget(mKeyValueWidget); layout->addWidget(moreLabel); mOneFileWidget->setWidget(content); mMultipleFilesLabel = new QLabel(); mGroup = new SideBarGroup(i18nc("@title:group", "Meta Information")); q->setWidget(mGroup); mGroup->addWidget(mOneFileWidget); mGroup->addWidget(mMultipleFilesLabel); EventWatcher::install(mGroup, QEvent::Show, q, SLOT(updateSideBarContent())); QObject::connect(moreLabel, &QLabel::linkActivated, q, &InfoContextManagerItem::showMetaInfoDialog); } void forgetCurrentDocument() { if (mDocument) { QObject::disconnect(mDocument.data(), nullptr, q, nullptr); // "Garbage collect" document mDocument = nullptr; } } }; InfoContextManagerItem::InfoContextManagerItem(ContextManager* manager) : AbstractContextManagerItem(manager) , d(new InfoContextManagerItemPrivate) { d->q = this; d->setupGroup(); connect(contextManager(), &ContextManager::selectionChanged, this, &InfoContextManagerItem::updateSideBarContent); connect(contextManager(), &ContextManager::selectionDataChanged, this, &InfoContextManagerItem::updateSideBarContent); } InfoContextManagerItem::~InfoContextManagerItem() { delete d; } void InfoContextManagerItem::updateSideBarContent() { LOG("updateSideBarContent"); if (!d->mGroup->isVisible()) { LOG("updateSideBarContent: not visible, not updating"); return; } LOG("updateSideBarContent: really updating"); KFileItemList itemList = contextManager()->selectedFileItemList(); if (itemList.count() == 0) { d->forgetCurrentDocument(); d->mOneFileWidget->hide(); d->mMultipleFilesLabel->hide(); d->updateMetaInfoDialog(); return; } KFileItem item = itemList.first(); if (itemList.count() == 1 && !ArchiveUtils::fileItemIsDirOrArchive(item)) { fillOneFileGroup(item); } else { fillMultipleItemsGroup(itemList); } d->updateMetaInfoDialog(); } void InfoContextManagerItem::fillOneFileGroup(const KFileItem& item) { d->mOneFileWidget->show(); d->mMultipleFilesLabel->hide(); d->forgetCurrentDocument(); d->mDocument = DocumentFactory::instance()->load(item.url()); connect(d->mDocument.data(), &Document::metaInfoUpdated, this, &InfoContextManagerItem::updateOneFileInfo); d->updateMetaInfoDialog(); updateOneFileInfo(); } void InfoContextManagerItem::fillMultipleItemsGroup(const KFileItemList& itemList) { d->forgetCurrentDocument(); int folderCount = 0, fileCount = 0; Q_FOREACH(const KFileItem & item, itemList) { if (item.isDir()) { folderCount++; } else { fileCount++; } } if (folderCount == 0) { d->mMultipleFilesLabel->setText(i18ncp("@label", "%1 file selected", "%1 files selected", fileCount)); } else if (fileCount == 0) { d->mMultipleFilesLabel->setText(i18ncp("@label", "%1 folder selected", "%1 folders selected", folderCount)); } else { d->mMultipleFilesLabel->setText(i18nc("@label. The two parameters are strings like '2 folders' and '1 file'.", "%1 and %2 selected", i18np("%1 folder", "%1 folders", folderCount), i18np("%1 file", "%1 files", fileCount))); } d->mOneFileWidget->hide(); d->mMultipleFilesLabel->show(); } void InfoContextManagerItem::updateOneFileInfo() { if (!d->mDocument) { return; } ImageMetaInfoModel* metaInfoModel = d->mDocument->metaInfo(); d->mKeyValueWidget->clear(); Q_FOREACH(const QString & key, GwenviewConfig::preferredMetaInfoKeyList()) { QString label; QString value; metaInfoModel->getInfoForKey(key, &label, &value); if (!label.isEmpty() && !value.isEmpty()) { d->mKeyValueWidget->addRow(label, value); } } d->mKeyValueWidget->finishAddRows(); d->mKeyValueWidget->layoutRows(); } void InfoContextManagerItem::showMetaInfoDialog() { if (!d->mImageMetaInfoDialog) { d->mImageMetaInfoDialog = new ImageMetaInfoDialog(d->mOneFileWidget); d->mImageMetaInfoDialog->setAttribute(Qt::WA_DeleteOnClose, true); connect(d->mImageMetaInfoDialog.data(), &ImageMetaInfoDialog::preferredMetaInfoKeyListChanged, this, &InfoContextManagerItem::slotPreferredMetaInfoKeyListChanged); } d->mImageMetaInfoDialog->setMetaInfo(d->mDocument ? d->mDocument->metaInfo() : nullptr, GwenviewConfig::preferredMetaInfoKeyList()); d->mImageMetaInfoDialog->show(); } void InfoContextManagerItem::slotPreferredMetaInfoKeyListChanged(const QStringList& list) { GwenviewConfig::setPreferredMetaInfoKeyList(list); GwenviewConfig::self()->save(); updateOneFileInfo(); } } // namespace diff --git a/app/kipiinterface.cpp b/app/kipiinterface.cpp index 1c422674..f8eb6431 100644 --- a/app/kipiinterface.cpp +++ b/app/kipiinterface.cpp @@ -1,518 +1,518 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2000-2008 Aurélien Gâteau Copyright 2008 Angelo Naselli 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. */ #include "kipiinterface.h" // Qt #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include #include // KIPI #include #include #include #include #include // local #include "mainwindow.h" #include "kipiimagecollectionselector.h" #include "kipiuploadwidget.h" #include #include #include #include #include #define KIPI_PLUGINS_URL QStringLiteral("appstream://photolayoutseditor.desktop") namespace Gwenview { #undef ENABLE_LOG #undef LOG //#define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) qDebug() << x #else #define LOG(x) ; #endif class KIPIImageInfo : public KIPI::ImageInfoShared { static const QRegExp sExtensionRE; public: KIPIImageInfo(KIPI::Interface* interface, const QUrl &url) : KIPI::ImageInfoShared(interface, url) { KFileItem item(url); mAttributes.insert("name", url.fileName()); mAttributes.insert("comment", comment()); mAttributes.insert("date", TimeUtils::dateTimeForFileItem(item)); mAttributes.insert("orientation", orientation()); mAttributes.insert("title", prettyFileName()); int size = item.size(); if (size > 0) { mAttributes.insert("filesize", size); } } QMap attributes() override { return mAttributes; } void delAttributes(const QStringList& attributeNames) override { Q_FOREACH(const QString& name, attributeNames) { mAttributes.remove(name); } } void clearAttributes() override { mAttributes.clear(); } void addAttributes(const QVariantMap& attributes) override { QVariantMap::ConstIterator it = attributes.constBegin(), end = attributes.constEnd(); for (; it != end; ++it) { mAttributes.insert(it.key(), it.value()); } } private: QString prettyFileName() const { QString txt = _url.fileName(); txt.replace('_', ' '); txt.remove(sExtensionRE); return txt; } QString comment() const { if (!_url.isLocalFile()) return QString(); JpegContent content; bool ok = content.load(_url.toLocalFile()); if (!ok) return QString(); return content.comment(); } int orientation() const { #if 0 //PORT QT5 KFileMetaInfo metaInfo(_url); if (!metaInfo.isValid()) { return 0; } const KFileMetaInfoItem& mii = metaInfo.item("http://freedesktop.org/standards/xesam/1.0/core#orientation"); bool ok = false; const Orientation orientation = (Orientation)mii.value().toInt(&ok); if (!ok) { return 0; } switch (orientation) { case NOT_AVAILABLE: case NORMAL: return 0; case ROT_90: return 90; case ROT_180: return 180; case ROT_270: return 270; case HFLIP: case VFLIP: case TRANSPOSE: case TRANSVERSE: qWarning() << "Can't represent an orientation value of" << orientation << "as an angle (" << _url << ')'; return 0; } #endif //qWarning() << "Don't know how to handle an orientation value of" << orientation << '(' << _url << ')'; return 0; } QVariantMap mAttributes; }; const QRegExp KIPIImageInfo::sExtensionRE("\\.[a-z0-9]+$", Qt::CaseInsensitive); struct MenuInfo { QString mName; QList mActions; QString mIconName; MenuInfo() {} MenuInfo(const QString& name, QString iconName) : mName(name) , mIconName(iconName) {} }; typedef QMap MenuInfoMap; struct KIPIInterfacePrivate { KIPIInterface* q; MainWindow* mMainWindow; QMenu* mPluginMenu; KIPI::PluginLoader* mPluginLoader; KIPI::PluginLoader::PluginList mPluginQueue; MenuInfoMap mMenuInfoMap; QAction * mLoadingAction; QAction * mNoPluginAction; QAction * mInstallPluginAction; QFileSystemWatcher mPluginWatcher; QTimer mPluginLoadTimer; void setupPluginsMenu() { mPluginMenu = static_cast( mMainWindow->factory()->container("plugins", mMainWindow)); QObject::connect(mPluginMenu, &QMenu::aboutToShow, q, &KIPIInterface::loadPlugins); } QAction * createDummyPluginAction(const QString& text) { QAction * action = new QAction(q); action->setText(text); //PORT QT5 action->setShortcutConfigurable(false); action->setEnabled(false); return action; } }; KIPIInterface::KIPIInterface(MainWindow* mainWindow) : KIPI::Interface(mainWindow) , d(new KIPIInterfacePrivate) { d->q = this; d->mMainWindow = mainWindow; d->mPluginLoader = nullptr; d->mLoadingAction = d->createDummyPluginAction(i18n("Loading...")); d->mNoPluginAction = d->createDummyPluginAction(i18n("No Plugin Found")); d->mInstallPluginAction = d->createDummyPluginAction(i18nc("@item:inmenu", "Install Plugins")); connect(&d->mPluginLoadTimer, &QTimer::timeout, this, &KIPIInterface::loadPlugins); d->setupPluginsMenu(); QObject::connect(d->mMainWindow->contextManager(), &ContextManager::selectionChanged, this, &KIPIInterface::slotSelectionChanged); QObject::connect(d->mMainWindow->contextManager(), &ContextManager::currentDirUrlChanged, this, &KIPIInterface::slotDirectoryChanged); #if 0 //TODO instead of delaying can we load them all at start-up to use actions somewhere else? // delay a bit, so that it's called after loadPlugins() QTimer::singleShot(0, this, SLOT(init())); #endif } KIPIInterface::~KIPIInterface() { delete d; } static bool actionLessThan(QAction* a1, QAction* a2) { QString a1Text = a1->text().replace('&', QString()); QString a2Text = a2->text().replace('&', QString()); return QString::compare(a1Text, a2Text, Qt::CaseInsensitive) < 0; } void KIPIInterface::loadPlugins() { // Already done if (d->mPluginLoader) { return; } d->mMenuInfoMap[KIPI::ImagesPlugin] = MenuInfo(i18nc("@title:menu", "Images"), QStringLiteral("viewimage")); d->mMenuInfoMap[KIPI::ToolsPlugin] = MenuInfo(i18nc("@title:menu", "Tools"), QStringLiteral("tools")); d->mMenuInfoMap[KIPI::ImportPlugin] = MenuInfo(i18nc("@title:menu", "Import"), QStringLiteral("document-import")); d->mMenuInfoMap[KIPI::ExportPlugin] = MenuInfo(i18nc("@title:menu", "Export"), QStringLiteral("document-export")); d->mMenuInfoMap[KIPI::BatchPlugin] = MenuInfo(i18nc("@title:menu", "Batch Processing"), QStringLiteral("editimage")); d->mMenuInfoMap[KIPI::CollectionsPlugin] = MenuInfo(i18nc("@title:menu", "Collections"), QStringLiteral("view-list-symbolic")); d->mPluginLoader = new KIPI::PluginLoader(); d->mPluginLoader->setInterface(this); d->mPluginLoader->init(); d->mPluginQueue = d->mPluginLoader->pluginList(); d->mPluginMenu->addAction(d->mLoadingAction); loadOnePlugin(); } void KIPIInterface::loadOnePlugin() { while (!d->mPluginQueue.isEmpty()) { KIPI::PluginLoader::Info* pluginInfo = d->mPluginQueue.takeFirst(); if (!pluginInfo->shouldLoad()) { continue; } KIPI::Plugin* plugin = pluginInfo->plugin(); if (!plugin) { qWarning() << "Plugin from library" << pluginInfo->library() << "failed to load"; continue; } plugin->setup(d->mMainWindow); QList actions = plugin->actions(); Q_FOREACH(QAction * action, actions) { KIPI::Category category = plugin->category(action); if (!d->mMenuInfoMap.contains(category)) { qWarning() << "Unknown category '" << category; continue; } d->mMenuInfoMap[category].mActions << action; } // FIXME: Port //plugin->actionCollection()->readShortcutSettings(); // If we reach this point, we just loaded one plugin. Go back to the // event loop. We will come back to load the remaining plugins or create // the menu later QMetaObject::invokeMethod(this, "loadOnePlugin", Qt::QueuedConnection); return; } // If we reach this point, all plugins have been loaded. We can fill the // menu MenuInfoMap::Iterator it = d->mMenuInfoMap.begin(), end = d->mMenuInfoMap.end(); for (; it != end; ++it) { MenuInfo& info = it.value(); if (!info.mActions.isEmpty()) { QMenu* menu = d->mPluginMenu->addMenu(info.mName); menu->setIcon(QIcon::fromTheme(info.mIconName)); - qSort(info.mActions.begin(), info.mActions.end(), actionLessThan); + std::sort(info.mActions.begin(), info.mActions.end(), actionLessThan); Q_FOREACH(QAction * action, info.mActions) { menu->addAction(action); } } } d->mPluginMenu->removeAction(d->mLoadingAction); if (d->mPluginMenu->isEmpty()) { if (KIO::DesktopExecParser::hasSchemeHandler(QUrl(KIPI_PLUGINS_URL))) { d->mPluginMenu->addAction(d->mInstallPluginAction); d->mInstallPluginAction->setEnabled(true); QObject::connect(d->mInstallPluginAction, &QAction::triggered, this, [&](){ QDesktopServices::openUrl(QUrl(KIPI_PLUGINS_URL)); d->mPluginWatcher.addPaths(QCoreApplication::libraryPaths()); connect(&d->mPluginWatcher, &QFileSystemWatcher::directoryChanged, this, &KIPIInterface::packageFinished); }); } else { d->mPluginMenu->addAction(d->mNoPluginAction); } } loadingFinished(); } void KIPIInterface::packageFinished() { if (d->mPluginLoader) { delete d->mPluginLoader; d->mPluginLoader = nullptr; } d->mPluginMenu->removeAction(d->mInstallPluginAction); d->mPluginMenu->removeAction(d->mNoPluginAction); d->mPluginLoadTimer.start(1000); } QList KIPIInterface::pluginActions(KIPI::Category category) const { const_cast(this)->loadPlugins(); if (isLoadingFinished()) { QList list = d->mMenuInfoMap.value(category).mActions; if (list.isEmpty()) { if (KIO::DesktopExecParser::hasSchemeHandler(QUrl(KIPI_PLUGINS_URL))) { list << d->mInstallPluginAction; } else { list << d->mNoPluginAction; } } return list; } else { return QList() << d->mLoadingAction; } } bool KIPIInterface::isLoadingFinished() const { if (!d->mPluginLoader) { // Not even started return false; } return d->mPluginQueue.isEmpty(); } void KIPIInterface::init() { slotDirectoryChanged(); slotSelectionChanged(); } KIPI::ImageCollection KIPIInterface::currentAlbum() { LOG(""); const ContextManager* contextManager = d->mMainWindow->contextManager(); const QUrl url = contextManager->currentDirUrl(); const SortedDirModel* model = contextManager->dirModel(); QList list; const int count = model->rowCount(); for (int row = 0; row < count; ++row) { const QModelIndex& index = model->index(row, 0); const KFileItem item = model->itemForIndex(index); if (MimeTypeUtils::fileItemKind(item) == MimeTypeUtils::KIND_RASTER_IMAGE) { list << item.targetUrl(); } } return KIPI::ImageCollection(new ImageCollection(url, url.fileName(), list)); } KIPI::ImageCollection KIPIInterface::currentSelection() { LOG(""); KFileItemList fileList = d->mMainWindow->contextManager()->selectedFileItemList(); QList list = fileList.urlList(); QUrl url = d->mMainWindow->contextManager()->currentUrl(); return KIPI::ImageCollection(new ImageCollection(url, url.fileName(), list)); } QList KIPIInterface::allAlbums() { LOG(""); QList list; list << currentAlbum() << currentSelection(); return list; } KIPI::ImageInfo KIPIInterface::info(const QUrl &url) { LOG(""); return KIPI::ImageInfo(new KIPIImageInfo(this, url)); } int KIPIInterface::features() const { return KIPI::HostAcceptNewImages; } /** * KDirLister will pick up the image if necessary, so no updating is needed * here, it is however necessary to discard caches if the plugin preserves timestamp */ bool KIPIInterface::addImage(const QUrl&, QString&) { //TODO setContext(const QUrl ¤tUrl, const KFileItemList& selection)? //Cache::instance()->invalidate( url ); return true; } void KIPIInterface::delImage(const QUrl&) { //TODO } void KIPIInterface::refreshImages(const QList&) { // TODO } KIPI::ImageCollectionSelector* KIPIInterface::imageCollectionSelector(QWidget *parent) { return new KIPIImageCollectionSelector(this, parent); } KIPI::UploadWidget* KIPIInterface::uploadWidget(QWidget *parent) { return (new KIPIUploadWidget(this, parent)); } void KIPIInterface::slotSelectionChanged() { emit selectionChanged(!d->mMainWindow->contextManager()->selectedFileItemList().isEmpty()); } void KIPIInterface::slotDirectoryChanged() { emit currentAlbumChanged(true); } #ifdef GWENVIEW_KIPI_WITH_CREATE_METHODS KIPI::FileReadWriteLock* KIPIInterface::createReadWriteLock(const QUrl& url) const { Q_UNUSED(url); return NULL; } KIPI::MetadataProcessor* KIPIInterface::createMetadataProcessor() const { return NULL; } #ifdef GWENVIEW_KIPI_WITH_CREATE_RAW_PROCESSOR KIPI::RawProcessor* KIPIInterface::createRawProcessor() const { return NULL; } #endif #endif } //namespace diff --git a/lib/crop/croptool.cpp b/lib/crop/croptool.cpp index e208a10d..bd2e841e 100644 --- a/lib/crop/croptool.cpp +++ b/lib/crop/croptool.cpp @@ -1,490 +1,490 @@ // 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 "croptool.h" // Qt #include #include #include #include #include #include #include // KDE // Local #include #include "cropimageoperation.h" #include "cropwidget.h" #include "gwenviewconfig.h" static const int HANDLE_SIZE = 15; namespace Gwenview { enum CropHandleFlag { CH_None, CH_Top = 1, CH_Left = 2, CH_Right = 4, CH_Bottom = 8, CH_TopLeft = CH_Top | CH_Left, CH_BottomLeft = CH_Bottom | CH_Left, CH_TopRight = CH_Top | CH_Right, CH_BottomRight = CH_Bottom | CH_Right, CH_Content = 16 }; Q_DECLARE_FLAGS(CropHandle, CropHandleFlag) } // namespace inline QPoint boundPointX(const QPoint& point, const QRect& rect) { return QPoint( qBound(rect.left(), point.x(), rect.right()), point.y() ); } inline QPoint boundPointXY(const QPoint& point, const QRect& rect) { return QPoint( qBound(rect.left(), point.x(), rect.right()), qBound(rect.top(), point.y(), rect.bottom()) ); } Q_DECLARE_OPERATORS_FOR_FLAGS(Gwenview::CropHandle) namespace Gwenview { struct CropToolPrivate { CropTool* q; QRect mRect; QList mCropHandleList; CropHandle mMovingHandle; QPoint mLastMouseMovePos; double mCropRatio; double mLockedCropRatio; CropWidget* mCropWidget; QRect viewportCropRect() const { return q->imageView()->mapToView(mRect); } QRect handleViewportRect(CropHandle handle) { QSize viewportSize = q->imageView()->size().toSize(); QRect rect = viewportCropRect(); int left, top; if (handle & CH_Top) { top = rect.top(); } else if (handle & CH_Bottom) { top = rect.bottom() + 1 - HANDLE_SIZE; } else { top = rect.top() + (rect.height() - HANDLE_SIZE) / 2; top = qBound(0, top, viewportSize.height() - HANDLE_SIZE); top = qBound(rect.top() + HANDLE_SIZE, top, rect.bottom() - 2 * HANDLE_SIZE); } if (handle & CH_Left) { left = rect.left(); } else if (handle & CH_Right) { left = rect.right() + 1 - HANDLE_SIZE; } else { left = rect.left() + (rect.width() - HANDLE_SIZE) / 2; left = qBound(0, left, viewportSize.width() - HANDLE_SIZE); left = qBound(rect.left() + HANDLE_SIZE, left, rect.right() - 2 * HANDLE_SIZE); } return QRect(left, top, HANDLE_SIZE, HANDLE_SIZE); } CropHandle handleAt(const QPointF& pos) { Q_FOREACH(const CropHandle & handle, mCropHandleList) { QRectF rect = handleViewportRect(handle); if (rect.contains(pos)) { return handle; } } QRectF rect = viewportCropRect(); if (rect.contains(pos)) { return CH_Content; } return CH_None; } void updateCursor(CropHandle handle, bool buttonDown) { Qt::CursorShape shape; switch (handle) { case CH_TopLeft: case CH_BottomRight: shape = Qt::SizeFDiagCursor; break; case CH_TopRight: case CH_BottomLeft: shape = Qt::SizeBDiagCursor; break; case CH_Left: case CH_Right: shape = Qt::SizeHorCursor; break; case CH_Top: case CH_Bottom: shape = Qt::SizeVerCursor; break; case CH_Content: shape = buttonDown ? Qt::ClosedHandCursor : Qt::OpenHandCursor; break; default: shape = Qt::ArrowCursor; break; } q->imageView()->setCursor(shape); } void keepRectInsideImage() { const QSize imageSize = q->imageView()->documentSize().toSize(); if (mRect.width() > imageSize.width() || mRect.height() > imageSize.height()) { // This can happen when the crop ratio changes QSize rectSize = mRect.size(); rectSize.scale(imageSize, Qt::KeepAspectRatio); mRect.setSize(rectSize); } if (mRect.right() >= imageSize.width()) { mRect.moveRight(imageSize.width() - 1); } else if (mRect.left() < 0) { mRect.moveLeft(0); } if (mRect.bottom() >= imageSize.height()) { mRect.moveBottom(imageSize.height() - 1); } else if (mRect.top() < 0) { mRect.moveTop(0); } } void setupWidget() { RasterImageView* view = q->imageView(); mCropWidget = new CropWidget(nullptr, view, q); QObject::connect(mCropWidget, SIGNAL(cropRequested()), q, SLOT(slotCropRequested())); QObject::connect(mCropWidget, &CropWidget::done, q, &CropTool::done); // This is needed when crop ratio set to Current Image, and the image is rotated QObject::connect(view, &RasterImageView::imageRectUpdated, mCropWidget, &CropWidget::updateCropRatio); } QRect computeVisibleImageRect() const { RasterImageView* view = q->imageView(); const QRect imageRect = QRect(QPoint(0, 0), view->documentSize().toSize()); const QRect viewportRect = view->mapToImage(view->rect().toRect()); return imageRect & viewportRect; } }; CropTool::CropTool(RasterImageView* view) : AbstractRasterImageViewTool(view) , d(new CropToolPrivate) { d->q = this; d->mCropHandleList << CH_Left << CH_Right << CH_Top << CH_Bottom << CH_TopLeft << CH_TopRight << CH_BottomLeft << CH_BottomRight; d->mMovingHandle = CH_None; d->mCropRatio = 0.; d->mLockedCropRatio = 0.; d->mRect = d->computeVisibleImageRect(); d->setupWidget(); } CropTool::~CropTool() { // mCropWidget is a child of its container not of us, so it is not deleted automatically delete d->mCropWidget; delete d; } void CropTool::setCropRatio(double ratio) { d->mCropRatio = ratio; } void CropTool::setRect(const QRect& rect) { QRect oldRect = d->mRect; d->mRect = rect; d->keepRectInsideImage(); if (d->mRect != oldRect) { emit rectUpdated(d->mRect); } imageView()->update(); } QRect CropTool::rect() const { return d->mRect; } void CropTool::paint(QPainter* painter) { QRect rect = d->viewportCropRect(); QRect imageRect = imageView()->rect().toRect(); static const QColor outerColor = QColor::fromHsvF(0, 0, 0, 0.5); // For some reason nothing gets drawn if borderColor is not fully opaque! //static const QColor borderColor = QColor::fromHsvF(0, 0, 1.0, 0.66); static const QColor borderColor = QColor::fromHsvF(0, 0, 1.0); static const QColor fillColor = QColor::fromHsvF(0, 0, 0.75, 0.66); - QRegion outerRegion = QRegion(imageRect) - QRegion(rect); - Q_FOREACH(const QRect & outerRect, outerRegion.rects()) { + const QRegion outerRegion = QRegion(imageRect) - QRegion(rect); + for (const QRect &outerRect : outerRegion) { painter->fillRect(outerRect, outerColor); } painter->setPen(borderColor); painter->drawRect(rect); if (d->mMovingHandle == CH_None) { // Only draw handles when user is not resizing painter->setBrush(fillColor); Q_FOREACH(const CropHandle & handle, d->mCropHandleList) { rect = d->handleViewportRect(handle); painter->drawRect(rect); } } } void CropTool::mousePressEvent(QGraphicsSceneMouseEvent* event) { if (event->buttons() != Qt::LeftButton) { event->ignore(); return; } const CropHandle newMovingHandle = d->handleAt(event->pos()); if (event->modifiers() & Qt::ControlModifier && !(newMovingHandle & (CH_Top | CH_Left | CH_Right | CH_Bottom))) { event->ignore(); return; } event->accept(); d->mMovingHandle = newMovingHandle; d->updateCursor(d->mMovingHandle, true /* down */); if (d->mMovingHandle == CH_Content) { d->mLastMouseMovePos = imageView()->mapToImage(event->pos().toPoint()); } // Update to hide handles imageView()->update(); } void CropTool::mouseMoveEvent(QGraphicsSceneMouseEvent* event) { event->accept(); if (event->buttons() != Qt::LeftButton) { return; } const QSize imageSize = imageView()->document()->size(); QPoint point = imageView()->mapToImage(event->pos().toPoint()); int posX = qBound(0, point.x(), imageSize.width() - 1); int posY = qBound(0, point.y(), imageSize.height() - 1); if (d->mMovingHandle == CH_None) { return; } // Adjust edge if (d->mMovingHandle & CH_Top) { d->mRect.setTop(posY); } else if (d->mMovingHandle & CH_Bottom) { d->mRect.setBottom(posY); } if (d->mMovingHandle & CH_Left) { d->mRect.setLeft(posX); } else if (d->mMovingHandle & CH_Right) { d->mRect.setRight(posX); } // Normalize rect and handles (this is useful when user drag the right side // of the crop rect to the left of the left side) if (d->mRect.height() < 0) { d->mMovingHandle = d->mMovingHandle ^(CH_Top | CH_Bottom); } if (d->mRect.width() < 0) { d->mMovingHandle = d->mMovingHandle ^(CH_Left | CH_Right); } d->mRect = d->mRect.normalized(); // Enforce ratio: double ratioToEnforce = d->mCropRatio; // - if user is holding down Ctrl/Shift when resizing rect, lock to current rect ratio if (event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier) && d->mLockedCropRatio > 0) { ratioToEnforce = d->mLockedCropRatio; } // - if user has restricted the ratio via the GUI if (ratioToEnforce > 0.) { if (d->mMovingHandle == CH_Top || d->mMovingHandle == CH_Bottom) { // Top or bottom int width = int(d->mRect.height() / ratioToEnforce); d->mRect.setWidth(width); } else if (d->mMovingHandle == CH_Left || d->mMovingHandle == CH_Right) { // Left or right int height = int(d->mRect.width() * ratioToEnforce); d->mRect.setHeight(height); } else if (d->mMovingHandle & CH_Top) { // Top left or top right int height = int(d->mRect.width() * ratioToEnforce); d->mRect.setTop(d->mRect.bottom() - height); } else if (d->mMovingHandle & CH_Bottom) { // Bottom left or bottom right int height = int(d->mRect.width() * ratioToEnforce); d->mRect.setHeight(height); } } if (d->mMovingHandle == CH_Content) { d->mRect.translate(point - d->mLastMouseMovePos); d->mLastMouseMovePos = point; } d->keepRectInsideImage(); imageView()->update(); emit rectUpdated(d->mRect); } void CropTool::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) { event->accept(); d->mMovingHandle = CH_None; d->updateCursor(d->handleAt(event->lastPos()), false); // Update to show handles imageView()->update(); } void CropTool::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event) { if (event->buttons() != Qt::LeftButton || d->handleAt(event->pos()) == CH_None) { event->ignore(); return; } event->accept(); emit d->mCropWidget->findChild()->accepted(); } void CropTool::hoverMoveEvent(QGraphicsSceneHoverEvent* event) { event->accept(); // Make sure cursor is updated when moving over handles CropHandle handle = d->handleAt(event->lastPos()); d->updateCursor(handle, false /* buttonDown */); } void CropTool::keyPressEvent(QKeyEvent* event) { // Lock crop ratio to current rect when user presses Control or Shift if (event->key() == Qt::Key_Control || event->key() == Qt::Key_Shift) { d->mLockedCropRatio = 1. * d->mRect.height() / d->mRect.width(); } QDialogButtonBox *buttons = d->mCropWidget->findChild(); switch (event->key()) { case Qt::Key_Escape: event->accept(); emit buttons->rejected(); break; case Qt::Key_Return: case Qt::Key_Enter: { event->accept(); auto focusButton = static_cast(buttons->focusWidget()); if (focusButton && buttons->buttonRole(focusButton) == QDialogButtonBox::RejectRole) { emit buttons->rejected(); } else { emit buttons->accepted(); } break; } default: break; } } void CropTool::toolActivated() { d->mCropWidget->setAdvancedSettingsEnabled(GwenviewConfig::cropAdvancedSettingsEnabled()); d->mCropWidget->setPreserveAspectRatio(GwenviewConfig::cropPreserveAspectRatio()); const int index = GwenviewConfig::cropRatioIndex(); if (index >= 0) { // Preset ratio d->mCropWidget->setCropRatioIndex(index); } else { // Must be a custom ratio, or blank const QSizeF ratio = QSizeF(GwenviewConfig::cropRatioWidth(), GwenviewConfig::cropRatioHeight()); d->mCropWidget->setCropRatio(ratio); } } void CropTool::toolDeactivated() { GwenviewConfig::setCropAdvancedSettingsEnabled(d->mCropWidget->advancedSettingsEnabled()); GwenviewConfig::setCropPreserveAspectRatio(d->mCropWidget->preserveAspectRatio()); GwenviewConfig::setCropRatioIndex(d->mCropWidget->cropRatioIndex()); const QSizeF ratio = d->mCropWidget->cropRatio(); GwenviewConfig::setCropRatioWidth(ratio.width()); GwenviewConfig::setCropRatioHeight(ratio.height()); } void CropTool::slotCropRequested() { CropImageOperation* op = new CropImageOperation(d->mRect); emit imageOperationRequested(op); emit done(); } QWidget* CropTool::widget() const { return d->mCropWidget; } } // namespace diff --git a/lib/crop/cropwidget.cpp b/lib/crop/cropwidget.cpp index edf4fa57..d27a4c1b 100644 --- a/lib/crop/cropwidget.cpp +++ b/lib/crop/cropwidget.cpp @@ -1,572 +1,575 @@ // 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 // Qt #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include // Local #include #include #include "croptool.h" #include "cropwidget.h" #include "flowlayout.h" namespace Gwenview { // Euclidean algorithm to compute the greatest common divisor of two integers. // Found at: // http://en.wikipedia.org/wiki/Euclidean_algorithm static int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } static QSize ratio(const QSize &size) { const int divisor = gcd(size.width(), size.height()); return size / divisor; } struct CropWidgetPrivate : public QWidget { CropWidget* q; QList mAdvancedWidgets; QWidget* mPreserveAspectRatioWidget; QCheckBox* advancedCheckBox; QComboBox* ratioComboBox; QSpinBox* widthSpinBox; QSpinBox* heightSpinBox; QSpinBox* leftSpinBox; QSpinBox* topSpinBox; QCheckBox* preserveAspectRatioCheckBox; QDialogButtonBox* dialogButtonBox; Document::Ptr mDocument; CropTool* mCropTool; bool mUpdatingFromCropTool; int mCurrentImageComboBoxIndex; int mCropRatioComboBoxCurrentIndex; bool ratioIsConstrained() const { return cropRatio() > 0; } QSizeF chosenRatio() const { // A size of 0 represents no ratio, i.e. the combobox is empty if (ratioComboBox->currentText().isEmpty()) { return QSizeF(0, 0); } // A preset ratio is selected const int index = ratioComboBox->currentIndex(); if (index != -1 && ratioComboBox->currentText() == ratioComboBox->itemText(index)) { return ratioComboBox->currentData().toSizeF(); } // A custom ratio has been entered, extract ratio from the text // If invalid, return zero size instead const QStringList lst = ratioComboBox->currentText().split(QLatin1Char(':')); if (lst.size() != 2) { return QSizeF(0, 0); } bool ok; const double width = lst[0].toDouble(&ok); if (!ok) { return QSizeF(0, 0); } const double height = lst[1].toDouble(&ok); if (!ok) { return QSizeF(0, 0); } // Valid custom value return QSizeF(width, height); } void setChosenRatio(QSizeF size) const { // Size matches preset ratio, let's set the combobox to that const int index = ratioComboBox->findData(size); if (index >= 0) { ratioComboBox->setCurrentIndex(index); return; } // Deselect whatever was selected if anything ratioComboBox->setCurrentIndex(-1); // If size is 0 (represents blank combobox, i.e., unrestricted) if (size.isEmpty()) { ratioComboBox->clearEditText(); return; } // Size must be custom ratio, convert to text and add to combobox QString ratioString = QStringLiteral("%1:%2").arg(size.width()).arg(size.height()); ratioComboBox->setCurrentText(ratioString); } double cropRatio() const { if (q->advancedSettingsEnabled()) { QSizeF size = chosenRatio(); if (size.isEmpty()) { return 0; } return size.height() / size.width(); } if (q->preserveAspectRatio()) { QSizeF size = ratio(mDocument->size()); return size.height() / size.width(); } return 0; } void addRatioToComboBox(const QSizeF& size, const QString& label = QString()) { QString text = label.isEmpty() ? QStringLiteral("%1:%2").arg(size.width()).arg(size.height()) : label; ratioComboBox->addItem(text, QVariant(size)); } void addSectionHeaderToComboBox(const QString& title) { // Insert a line ratioComboBox->insertSeparator(ratioComboBox->count()); // Insert our section header // This header is made of a separator with a text. We reset // Qt::AccessibleDescriptionRole to the header text otherwise QComboBox // delegate will draw a separator line instead of our text. int index = ratioComboBox->count(); ratioComboBox->insertSeparator(index); ratioComboBox->setItemText(index, title); ratioComboBox->setItemData(index, title, Qt::AccessibleDescriptionRole); ratioComboBox->setItemData(index, Qt::AlignHCenter, Qt::TextAlignmentRole); } void initRatioComboBox() { QList ratioList; const qreal sqrt2 = qSqrt(2.); ratioList << QSizeF(16, 9) << QSizeF(7, 5) << QSizeF(3, 2) << QSizeF(4, 3) << QSizeF(5, 4); addRatioToComboBox(ratio(mDocument->size()), i18n("Current Image")); mCurrentImageComboBoxIndex = ratioComboBox->count() - 1; // We need to refer to this ratio later addRatioToComboBox(QSizeF(1, 1), i18n("Square")); addRatioToComboBox(ratio(QApplication::desktop()->screenGeometry().size()), i18n("This Screen")); + // The previous string should be changed to + // addRatioToComboBox(ratio(QGuiApplication::screenAt(QCursor::pos())->geometry().size()), i18n("This Screen")); + // after switching to Qt > 5.9 addSectionHeaderToComboBox(i18n("Landscape")); Q_FOREACH(const QSizeF& size, ratioList) { addRatioToComboBox(size); } addRatioToComboBox(QSizeF(sqrt2, 1), i18n("ISO (A4, A3...)")); addRatioToComboBox(QSizeF(11, 8.5), i18n("US Letter")); addSectionHeaderToComboBox(i18n("Portrait")); Q_FOREACH(QSizeF size, ratioList) { size.transpose(); addRatioToComboBox(size); } addRatioToComboBox(QSizeF(1, sqrt2), i18n("ISO (A4, A3...)")); addRatioToComboBox(QSizeF(8.5, 11), i18n("US Letter")); ratioComboBox->setMaxVisibleItems(ratioComboBox->count()); ratioComboBox->clearEditText(); QLineEdit* edit = qobject_cast(ratioComboBox->lineEdit()); Q_ASSERT(edit); // Do not use i18n("%1:%2") because ':' should not be translated, it is // used to parse the ratio string. edit->setPlaceholderText(QStringLiteral("%1:%2").arg(i18n("Width"), i18n("Height"))); // Enable clear button edit->setClearButtonEnabled(true); // Must manually adjust minimum width because the auto size adjustment doesn't take the // clear button into account const int width = ratioComboBox->minimumSizeHint().width(); ratioComboBox->setMinimumWidth(width + 24); mCropRatioComboBoxCurrentIndex = -1; ratioComboBox->setCurrentIndex(mCropRatioComboBoxCurrentIndex); } QRect cropRect() const { QRect rect( leftSpinBox->value(), topSpinBox->value(), widthSpinBox->value(), heightSpinBox->value() ); return rect; } void initSpinBoxes() { QSize size = mDocument->size(); leftSpinBox->setMaximum(size.width()); widthSpinBox->setMaximum(size.width()); topSpinBox->setMaximum(size.height()); heightSpinBox->setMaximum(size.height()); // When users change the crop rectangle, QSpinBox::setMaximum will be called // again, which then adapts the sizeHint due to a different maximum number // of digits, leading to horizontal movement in the layout. This can be // avoided by setting the minimum width so it fits the largest value possible. leftSpinBox->setMinimumWidth(leftSpinBox->sizeHint().width()); widthSpinBox->setMinimumWidth(widthSpinBox->sizeHint().width()); topSpinBox->setMinimumWidth(topSpinBox->sizeHint().width()); heightSpinBox->setMinimumWidth(heightSpinBox->sizeHint().width()); } void initDialogButtonBox() { QPushButton* cropButton = dialogButtonBox->button(QDialogButtonBox::Ok); cropButton->setIcon(QIcon::fromTheme(QStringLiteral("transform-crop-and-resize"))); cropButton->setText(i18n("Crop")); QObject::connect(dialogButtonBox, &QDialogButtonBox::accepted, q, &CropWidget::cropRequested); QObject::connect(dialogButtonBox, &QDialogButtonBox::rejected, q, &CropWidget::done); } QWidget* boxWidget(QWidget* parent = nullptr) { QWidget* widget = new QWidget(parent); QHBoxLayout* layout = new QHBoxLayout(widget); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(2); return widget; } void setupUi(QWidget* cropWidget) { cropWidget->setObjectName(QStringLiteral("CropWidget")); FlowLayout* flowLayout = new FlowLayout(cropWidget, 6, 0); flowLayout->setObjectName(QStringLiteral("CropWidgetFlowLayout")); flowLayout->setAlignment(Qt::AlignCenter); flowLayout->setVerticalSpacing(6); // (1) Checkbox QWidget* box = boxWidget(cropWidget); advancedCheckBox = new QCheckBox(i18nc("@option:check", "Advanced settings"), box); advancedCheckBox->setFocusPolicy(Qt::NoFocus); box->layout()->addWidget(advancedCheckBox); flowLayout->addWidget(box); flowLayout->addSpacing(14); // (2) Ratio combobox (Advanced settings) box = boxWidget(cropWidget); mAdvancedWidgets << box; QLabel* label = new QLabel(i18nc("@label:listbox", "Aspect ratio:"), box); label->setMargin(4); box->layout()->addWidget(label); ratioComboBox = new QComboBox(box); ratioComboBox->setEditable(true); ratioComboBox->setInsertPolicy(QComboBox::NoInsert); box->layout()->addWidget(ratioComboBox); flowLayout->addWidget(box); flowLayout->addSpacing(8); // (3) Size spinboxes (Advanced settings) box = boxWidget(cropWidget); mAdvancedWidgets << box; label = new QLabel(i18nc("@label:spinbox", "Size:"), box); label->setMargin(4); box->layout()->addWidget(label); QHBoxLayout* innerLayout = new QHBoxLayout(); innerLayout->setSpacing(3); widthSpinBox = new QSpinBox(box); widthSpinBox->setAlignment(Qt::AlignCenter); innerLayout->addWidget(widthSpinBox); heightSpinBox = new QSpinBox(box); heightSpinBox->setAlignment(Qt::AlignCenter); innerLayout->addWidget(heightSpinBox); box->layout()->addItem(innerLayout); flowLayout->addWidget(box); flowLayout->addSpacing(8); // (4) Position spinboxes (Advanced settings) box = boxWidget(cropWidget); mAdvancedWidgets << box; label = new QLabel(i18nc("@label:spinbox", "Position:"), box); label->setMargin(4); box->layout()->addWidget(label); innerLayout = new QHBoxLayout(); innerLayout->setSpacing(3); leftSpinBox = new QSpinBox(box); leftSpinBox->setAlignment(Qt::AlignCenter); innerLayout->addWidget(leftSpinBox); topSpinBox = new QSpinBox(box); topSpinBox->setAlignment(Qt::AlignCenter); innerLayout->addWidget(topSpinBox); box->layout()->addItem(innerLayout); flowLayout->addWidget(box); flowLayout->addSpacing(18); // (5) Preserve ratio checkbox mPreserveAspectRatioWidget = boxWidget(cropWidget); preserveAspectRatioCheckBox = new QCheckBox(i18nc("@option:check", "Preserve aspect ratio"), mPreserveAspectRatioWidget); mPreserveAspectRatioWidget->layout()->addWidget(preserveAspectRatioCheckBox); flowLayout->addWidget(mPreserveAspectRatioWidget); flowLayout->addSpacing(18); // (6) Dialog buttons box = boxWidget(cropWidget); dialogButtonBox = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok, box); box->layout()->addWidget(dialogButtonBox); flowLayout->addWidget(box); } }; CropWidget::CropWidget(QWidget* parent, RasterImageView* imageView, CropTool* cropTool) : QWidget(parent) , d(new CropWidgetPrivate) { setWindowFlags(Qt::Tool); d->q = this; d->mDocument = imageView->document(); d->mUpdatingFromCropTool = false; d->mCropTool = cropTool; d->setupUi(this); setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); connect(d->advancedCheckBox, &QCheckBox::toggled, this, &CropWidget::slotAdvancedCheckBoxToggled); for (auto w : d->mAdvancedWidgets) { w->setVisible(false); } connect(d->preserveAspectRatioCheckBox, &QCheckBox::toggled, this, &CropWidget::applyRatioConstraint); d->initRatioComboBox(); connect(d->mCropTool, &CropTool::rectUpdated, this, &CropWidget::setCropRect); connect(d->leftSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &CropWidget::slotPositionChanged); connect(d->topSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &CropWidget::slotPositionChanged); connect(d->widthSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &CropWidget::slotWidthChanged); connect(d->heightSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &CropWidget::slotHeightChanged); d->initDialogButtonBox(); // We need to listen for both signals because the combobox is multi-function: // Text Changed: required so that manual ratio entry is detected (index doesn't change) // Index Changed: required so that choosing an item with the same text is detected (e.g. going from US Letter portrait // to US Letter landscape) connect(d->ratioComboBox, &QComboBox::editTextChanged, this, &CropWidget::slotRatioComboBoxChanged); connect(d->ratioComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &CropWidget::slotRatioComboBoxChanged); // Don't do this before signals are connected, otherwise the tool won't get // initialized d->initSpinBoxes(); setCropRect(d->mCropTool->rect()); } CropWidget::~CropWidget() { delete d; } void CropWidget::setAdvancedSettingsEnabled(bool enable) { d->advancedCheckBox->setChecked(enable); } bool CropWidget::advancedSettingsEnabled() const { return d->advancedCheckBox->isChecked(); } void CropWidget::setPreserveAspectRatio(bool preserve) { d->preserveAspectRatioCheckBox->setChecked(preserve); } bool CropWidget::preserveAspectRatio() const { return d->preserveAspectRatioCheckBox->isChecked(); } void CropWidget::setCropRatio(QSizeF size) { d->setChosenRatio(size); } QSizeF CropWidget::cropRatio() const { return d->chosenRatio(); } void CropWidget::setCropRatioIndex(int index) { d->ratioComboBox->setCurrentIndex(index); } int CropWidget::cropRatioIndex() const { return d->mCropRatioComboBoxCurrentIndex; } void CropWidget::setCropRect(const QRect& rect) { d->mUpdatingFromCropTool = true; d->leftSpinBox->setValue(rect.left()); d->topSpinBox->setValue(rect.top()); d->widthSpinBox->setValue(rect.width()); d->heightSpinBox->setValue(rect.height()); d->mUpdatingFromCropTool = false; } void CropWidget::slotPositionChanged() { const QSize size = d->mDocument->size(); d->widthSpinBox->setMaximum(size.width() - d->leftSpinBox->value()); d->heightSpinBox->setMaximum(size.height() - d->topSpinBox->value()); if (d->mUpdatingFromCropTool) { return; } d->mCropTool->setRect(d->cropRect()); } void CropWidget::slotWidthChanged() { d->leftSpinBox->setMaximum(d->mDocument->width() - d->widthSpinBox->value()); if (d->mUpdatingFromCropTool) { return; } if (d->ratioIsConstrained()) { int height = int(d->widthSpinBox->value() * d->cropRatio()); d->heightSpinBox->setValue(height); } d->mCropTool->setRect(d->cropRect()); } void CropWidget::slotHeightChanged() { d->topSpinBox->setMaximum(d->mDocument->height() - d->heightSpinBox->value()); if (d->mUpdatingFromCropTool) { return; } if (d->ratioIsConstrained()) { int width = int(d->heightSpinBox->value() / d->cropRatio()); d->widthSpinBox->setValue(width); } d->mCropTool->setRect(d->cropRect()); } void CropWidget::applyRatioConstraint() { double ratio = d->cropRatio(); d->mCropTool->setCropRatio(ratio); if (!d->ratioIsConstrained()) { return; } QRect rect = d->cropRect(); rect.setHeight(int(rect.width() * ratio)); d->mCropTool->setRect(rect); } void CropWidget::slotAdvancedCheckBoxToggled(bool checked) { for (auto w : d->mAdvancedWidgets) { w->setVisible(checked); } d->mPreserveAspectRatioWidget->setVisible(!checked); applyRatioConstraint(); } void CropWidget::slotRatioComboBoxChanged() { const QString text = d->ratioComboBox->currentText(); // If text cleared, clear the current item as well if (text.isEmpty()) { d->ratioComboBox->setCurrentIndex(-1); } // We want to keep track of the selected ratio, including when the user has entered a custom ratio // or cleared the text. We can't simply use currentIndex() because this stays >= 0 when the user manually // enters text. We also can't set the current index to -1 when there is no match like above because that // interferes when manually entering text. // Furthermore, since there can be duplicate text items, we can't rely on findText() as it will stop on // the first match it finds. Therefore we must check if there's a match, and if so, get the index directly. if (d->ratioComboBox->findText(text) >= 0) { d->mCropRatioComboBoxCurrentIndex = d->ratioComboBox->currentIndex(); } else { d->mCropRatioComboBoxCurrentIndex = -1; } applyRatioConstraint(); } void CropWidget::updateCropRatio() { // First we need to re-calculate the "Current Image" ratio in case the user rotated the image d->ratioComboBox->setItemData(d->mCurrentImageComboBoxIndex, QVariant(ratio(d->mDocument->size()))); // Always re-apply the constraint, even though we only need to when the user has "Current Image" // selected or the "Preserve aspect ratio" checked, since there's no harm applyRatioConstraint(); // If the ratio is unrestricted, calling applyRatioConstraint doesn't update the rect, so we call // this manually to make sure the rect is adjusted to fit within the image d->mCropTool->setRect(d->mCropTool->rect()); } } // namespace diff --git a/lib/documentview/documentviewcontainer.cpp b/lib/documentview/documentviewcontainer.cpp index 17822fea..d680e3a7 100644 --- a/lib/documentview/documentviewcontainer.cpp +++ b/lib/documentview/documentviewcontainer.cpp @@ -1,341 +1,341 @@ // 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 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); + std::sort(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, &QObject::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 9cd0e4e0..8b72f49e 100644 --- a/lib/fullscreenbar.cpp +++ b/lib/fullscreenbar.cpp @@ -1,279 +1,283 @@ // 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 // 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())); + // The previous string should be changed to + // QRect rect = QGuiApplication::screenAt(QCursor::pos())->geometry(); + // after switching to Qt > 5.9 + // 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(QStringLiteral("fullScreenBar")); d->mTimeLine = new QTimeLine(SLIDE_DURATION, 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, &FullScreenBar::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/imagescaler.cpp b/lib/imagescaler.cpp index 7936cb13..c7562290 100644 --- a/lib/imagescaler.cpp +++ b/lib/imagescaler.cpp @@ -1,228 +1,228 @@ /* 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. */ #include "imagescaler.h" // Qt #include #include #include #include // KDE // Local #include #include #undef ENABLE_LOG #undef LOG //#define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) qDebug() << x #else #define LOG(x) ; #endif namespace Gwenview { // Amount of pixels to keep so that smooth scale is correct static const int SMOOTH_MARGIN = 3; static inline QRectF scaledRect(const QRectF& rect, qreal factor) { return QRectF(rect.x() * factor, rect.y() * factor, rect.width() * factor, rect.height() * factor); } static inline QRect scaledRect(const QRect& rect, qreal factor) { return scaledRect(QRectF(rect), factor).toAlignedRect(); } struct ImageScalerPrivate { Qt::TransformationMode mTransformationMode; Document::Ptr mDocument; qreal mZoom; QRegion mRegion; }; ImageScaler::ImageScaler(QObject* parent) : QObject(parent) , d(new ImageScalerPrivate) { d->mTransformationMode = Qt::FastTransformation; d->mZoom = 0; } ImageScaler::~ImageScaler() { delete d; } void ImageScaler::setDocument(const Document::Ptr &document) { if (d->mDocument) { disconnect(d->mDocument.data(), nullptr, this, nullptr); } d->mDocument = document; // Used when scaler asked for a down-sampled image connect(d->mDocument.data(), &Document::downSampledImageReady, this, &ImageScaler::doScale); // Used when scaler asked for a full image connect(d->mDocument.data(), &Document::loaded, this, &ImageScaler::doScale); } void ImageScaler::setZoom(qreal zoom) { // If we zoom to 400% or more, then assume the user wants to see the real // pixels, for example to fine tune a crop operation d->mTransformationMode = zoom < 4. ? Qt::SmoothTransformation : Qt::FastTransformation; d->mZoom = zoom; } void ImageScaler::setDestinationRegion(const QRegion& region) { LOG(region); d->mRegion = region; if (d->mRegion.isEmpty()) { return; } if (d->mDocument && d->mZoom > 0) { doScale(); } } void ImageScaler::doScale() { if (d->mZoom < Document::maxDownSampledZoom()) { if (!d->mDocument->prepareDownSampledImageForZoom(d->mZoom)) { LOG("Asked for a down sampled image"); return; } } else if (d->mDocument->image().isNull()) { LOG("Asked for the full image"); d->mDocument->startLoadingFullImage(); return; } LOG("Starting"); - Q_FOREACH(const QRect & rect, d->mRegion.rects()) { + for (const QRect &rect : qAsConst(d->mRegion)) { LOG(rect); scaleRect(rect); } LOG("Done"); } void ImageScaler::scaleRect(const QRect& rect) { const qreal dpr = qApp->devicePixelRatio(); // variables prefixed with dp are in device pixels const QRect dpRect = Gwenview::scaledRect(rect, dpr); const qreal REAL_DELTA = 0.001; if (qAbs(d->mZoom - 1.0) < REAL_DELTA) { QImage tmp = d->mDocument->image().copy(dpRect); tmp.setDevicePixelRatio(dpr); emit scaledRect(rect.left(), rect.top(), tmp); return; } QImage image; qreal zoom; if (d->mZoom < Document::maxDownSampledZoom()) { image = d->mDocument->downSampledImageForZoom(d->mZoom); Q_ASSERT(!image.isNull()); qreal zoom1 = qreal(image.width()) / d->mDocument->width(); zoom = d->mZoom / zoom1; } else { image = d->mDocument->image(); zoom = d->mZoom; } const QRect imageRect = Gwenview::scaledRect(image.rect(), 1.0 / dpr); // If rect contains "half" pixels, make sure sourceRect includes them QRectF sourceRectF = Gwenview::scaledRect(QRectF(rect), 1.0 / zoom); sourceRectF = sourceRectF.intersected(imageRect); QRect sourceRect = sourceRectF.toAlignedRect(); if (sourceRect.isEmpty()) { return; } // Compute smooth margin bool needsSmoothMargins = d->mTransformationMode == Qt::SmoothTransformation; int sourceLeftMargin, sourceRightMargin, sourceTopMargin, sourceBottomMargin; int destLeftMargin, destRightMargin, destTopMargin, destBottomMargin; if (needsSmoothMargins) { sourceLeftMargin = qMin(sourceRect.left(), SMOOTH_MARGIN); sourceTopMargin = qMin(sourceRect.top(), SMOOTH_MARGIN); sourceRightMargin = qMin(imageRect.right() - sourceRect.right(), SMOOTH_MARGIN); sourceBottomMargin = qMin(imageRect.bottom() - sourceRect.bottom(), SMOOTH_MARGIN); sourceRect.adjust( -sourceLeftMargin, -sourceTopMargin, sourceRightMargin, sourceBottomMargin); destLeftMargin = int(sourceLeftMargin * zoom); destTopMargin = int(sourceTopMargin * zoom); destRightMargin = int(sourceRightMargin * zoom); destBottomMargin = int(sourceBottomMargin * zoom); } else { sourceLeftMargin = sourceRightMargin = sourceTopMargin = sourceBottomMargin = 0; destLeftMargin = destRightMargin = destTopMargin = destBottomMargin = 0; } // destRect is almost like rect, but it contains only "full" pixels QRect destRect = Gwenview::scaledRect(sourceRect, zoom); QRect dpSourceRect = Gwenview::scaledRect(sourceRect, dpr); QRect dpDestRect = Gwenview::scaledRect(dpSourceRect, zoom); QImage tmp; tmp = image.copy(dpSourceRect); tmp = tmp.scaled( dpDestRect.width(), dpDestRect.height(), Qt::IgnoreAspectRatio, // Do not use KeepAspectRatio, it can lead to skipped rows or columns d->mTransformationMode); if (needsSmoothMargins) { tmp = tmp.copy( destLeftMargin * dpr, destTopMargin * dpr, dpDestRect.width() - (destLeftMargin + destRightMargin) * dpr, dpDestRect.height() - (destTopMargin + destBottomMargin) * dpr ); } tmp.setDevicePixelRatio(dpr); emit scaledRect(destRect.left() + destLeftMargin, destRect.top() + destTopMargin, tmp); } } // namespace diff --git a/lib/semanticinfo/tagitemdelegate.cpp b/lib/semanticinfo/tagitemdelegate.cpp index b54609f4..764b1faf 100644 --- a/lib/semanticinfo/tagitemdelegate.cpp +++ b/lib/semanticinfo/tagitemdelegate.cpp @@ -1,152 +1,152 @@ // 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 "tagitemdelegate.h" // Qt #include #include #include #include // KDE #include #include // Local #include namespace Gwenview { TagItemDelegate::TagItemDelegate(QAbstractItemView* view) : KWidgetItemDelegate(view, view) { #define pm(x) view->style()->pixelMetric(QStyle::x) mMargin = pm(PM_ToolBarItemMargin); mSpacing = pm(PM_ToolBarItemSpacing); #undef pm const int iconSize = KIconLoader::global()->currentSize(KIconLoader::Toolbar); const QSize sz = view->style()->sizeFromContents(QStyle::CT_ToolButton, nullptr, QSize(iconSize, iconSize)); mButtonSize = qMax(sz.width(), sz.height()); } QList TagItemDelegate::createItemWidgets(const QModelIndex &index) const { #define initButton(x) \ (x)->setAutoRaise(true); \ setBlockedEventTypes((x), QList() \ << QEvent::MouseButtonPress \ << QEvent::MouseButtonRelease \ << QEvent::MouseButtonDblClick); Q_UNUSED(index); QToolButton* assignToAllButton = new QToolButton; initButton(assignToAllButton); assignToAllButton->setIcon(QIcon::fromTheme("fill-color")); /* FIXME: Probably not the appropriate icon */ assignToAllButton->setToolTip(i18nc("@info:tooltip", "Assign this tag to all selected images")); connect(assignToAllButton, &QToolButton::clicked, this, &TagItemDelegate::slotAssignToAllButtonClicked); QToolButton* removeButton = new QToolButton; initButton(removeButton); removeButton->setIcon(QIcon::fromTheme("list-remove")); connect(removeButton, &QToolButton::clicked, this, &TagItemDelegate::slotRemoveButtonClicked); #undef initButton return QList() << removeButton << assignToAllButton; } void TagItemDelegate::updateItemWidgets(const QList widgets, const QStyleOptionViewItem& option, const QPersistentModelIndex& index) const { const bool fullyAssigned = index.data(TagModel::AssignmentStatusRole).toInt() == int(TagModel::FullyAssigned); QToolButton* removeButton = static_cast(widgets[0]); QToolButton* assignToAllButton = static_cast(widgets[1]); QSize buttonSize(mButtonSize, option.rect.height() - 2 * mMargin); removeButton->resize(buttonSize); assignToAllButton->resize(buttonSize); removeButton->move(option.rect.width() - mButtonSize - mMargin, mMargin); if (fullyAssigned) { assignToAllButton->hide(); } else { assignToAllButton->move(removeButton->x() - mButtonSize - mSpacing, mMargin); } } void TagItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { if (!index.isValid()) { return; } const bool selected = option.state & QStyle::State_Selected; const bool fullyAssigned = index.data(TagModel::AssignmentStatusRole).toInt() == int(TagModel::FullyAssigned); itemView()->style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, nullptr); QRect textRect = option.rect; textRect.setLeft(textRect.left() + mMargin); textRect.setWidth(textRect.width() - mButtonSize - mMargin - mSpacing); if (!fullyAssigned) { textRect.setWidth(textRect.width() - mButtonSize - mSpacing); } painter->setPen(option.palette.color(QPalette::Normal, selected ? QPalette::HighlightedText : QPalette::Text)); painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, index.data().toString()); } QSize TagItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { - const int width = option.fontMetrics.width(index.data().toString()); + const int width = option.fontMetrics.boundingRect(index.data().toString()).width(); const int height = qMax(mButtonSize, option.fontMetrics.height()); return QSize(width + 2 * mMargin, height + 2 * mMargin); } void TagItemDelegate::slotRemoveButtonClicked() { const QModelIndex index = focusedIndex(); if (!index.isValid()) { qWarning() << "!index.isValid()"; return; } emit removeTagRequested(index.data(TagModel::TagRole).toString()); } void TagItemDelegate::slotAssignToAllButtonClicked() { const QModelIndex index = focusedIndex(); if (!index.isValid()) { qWarning() << "!index.isValid()"; return; } emit assignTagToAllRequested(index.data(TagModel::TagRole).toString()); } } // namespace diff --git a/lib/slideshow.cpp b/lib/slideshow.cpp index 79262b1a..39050a4e 100644 --- a/lib/slideshow.cpp +++ b/lib/slideshow.cpp @@ -1,327 +1,327 @@ /* 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. */ #include "slideshow.h" // libc #include // STL #include // Qt #include #include #include // KDE #include // Local #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 enum State { Paused, Started, WaitForEndOfUrl }; /** * This class generate random numbers which are not the same between two runs * of Gwenview. See bug #132334 */ class RandomNumberGenerator { public: RandomNumberGenerator() : mSeed(time(0)) { } int operator()(int n) { return rand_r(&mSeed) % n; } private: unsigned int mSeed; }; struct SlideShowPrivate { QTimer* mTimer; State mState; QVector mUrls; QVector mShuffledUrls; QVector::ConstIterator mStartIt; QUrl mCurrentUrl; QUrl mLastShuffledUrl; QAction* mLoopAction; QAction* mRandomAction; QUrl findNextUrl() { if (GwenviewConfig::random()) { return findNextRandomUrl(); } else { return findNextOrderedUrl(); } } QUrl findNextOrderedUrl() { - QVector::ConstIterator it = qFind(mUrls.constBegin(), mUrls.constEnd(), mCurrentUrl); + QVector::ConstIterator it = std::find(mUrls.constBegin(), mUrls.constEnd(), mCurrentUrl); GV_RETURN_VALUE_IF_FAIL2(it != mUrls.constEnd(), QUrl(), "Current url not found in list."); ++it; if (GwenviewConfig::loop()) { // Looping, if we reach the end, start again if (it == mUrls.constEnd()) { it = mUrls.constBegin(); } } else { // Not looping, have we reached the end? // FIXME: stopAtEnd if (/*(it==mUrls.end() && GwenviewConfig::stopAtEnd()) ||*/ it == mStartIt) { it = mUrls.constEnd(); } } if (it != mUrls.constEnd()) { return *it; } else { return QUrl(); } } void initShuffledUrls() { mShuffledUrls = mUrls; RandomNumberGenerator generator; std::random_shuffle(mShuffledUrls.begin(), mShuffledUrls.end(), generator); // Ensure the first url is different from the previous last one, so that // last url does not stay visible twice longer than usual if (mLastShuffledUrl == mShuffledUrls.first() && mShuffledUrls.count() > 1) { qSwap(mShuffledUrls[0], mShuffledUrls[1]); } mLastShuffledUrl = mShuffledUrls.last(); } QUrl findNextRandomUrl() { if (mShuffledUrls.empty()) { if (GwenviewConfig::loop()) { initShuffledUrls(); } else { return QUrl(); } } QUrl url = mShuffledUrls.last(); mShuffledUrls.pop_back(); return url; } void updateTimerInterval() { mTimer->setInterval(int(GwenviewConfig::interval() * 1000)); } void doStart() { if (MimeTypeUtils::urlKind(mCurrentUrl) == MimeTypeUtils::KIND_VIDEO) { LOG("mState = WaitForEndOfUrl"); // Just in case mTimer->stop(); mState = WaitForEndOfUrl; } else { LOG("mState = Started"); mTimer->start(); mState = Started; } } }; SlideShow::SlideShow(QObject* parent) : QObject(parent) , d(new SlideShowPrivate) { d->mState = Paused; d->mTimer = new QTimer(this); connect(d->mTimer, &QTimer::timeout, this, &SlideShow::goToNextUrl); d->mLoopAction = new QAction(this); d->mLoopAction->setText(i18nc("@item:inmenu toggle loop in slideshow", "Loop")); d->mLoopAction->setCheckable(true); connect(d->mLoopAction, &QAction::triggered, this, &SlideShow::updateConfig); d->mRandomAction = new QAction(this); d->mRandomAction->setText(i18nc("@item:inmenu toggle random order in slideshow", "Random")); d->mRandomAction->setCheckable(true); connect(d->mRandomAction, &QAction::toggled, this, &SlideShow::slotRandomActionToggled); connect(d->mRandomAction, &QAction::triggered, this, &SlideShow::updateConfig); d->mLoopAction->setChecked(GwenviewConfig::loop()); d->mRandomAction->setChecked(GwenviewConfig::random()); } SlideShow::~SlideShow() { GwenviewConfig::self()->save(); delete d; } QAction* SlideShow::loopAction() const { return d->mLoopAction; } QAction* SlideShow::randomAction() const { return d->mRandomAction; } void SlideShow::start(const QList& urls) { d->mUrls.resize(urls.size()); - qCopy(urls.begin(), urls.end(), d->mUrls.begin()); + std::copy(urls.begin(), urls.end(), d->mUrls.begin()); - d->mStartIt = qFind(d->mUrls.constBegin(), d->mUrls.constEnd(), d->mCurrentUrl); + d->mStartIt = std::find(d->mUrls.constBegin(), d->mUrls.constEnd(), d->mCurrentUrl); if (d->mStartIt == d->mUrls.constEnd()) { qWarning() << "Current url not found in list, aborting.\n"; return; } if (GwenviewConfig::random()) { d->initShuffledUrls(); } d->updateTimerInterval(); d->mTimer->setSingleShot(false); d->doStart(); emit stateChanged(true); } void SlideShow::setInterval(int intervalInSeconds) { GwenviewConfig::setInterval(double(intervalInSeconds)); d->updateTimerInterval(); emit intervalChanged(intervalInSeconds); } int SlideShow::interval() const { return GwenviewConfig::interval(); } int SlideShow::position() const { // TODO: also support videos // QTimer::remainingTime() returns -1 if inactive // and there are moments where mState == Started but timer already done but not yet next url reached // so handle that if (d->mState == Started) { if (d->mTimer->isActive()) { return interval() * 1000 - d->mTimer->remainingTime(); } // already timeout reached, but not yet progressed to next url return interval(); } return 0; } void SlideShow::pause() { LOG("Stopping timer"); d->mTimer->stop(); d->mState = Paused; emit stateChanged(false); } void SlideShow::resumeAndGoToNextUrl() { LOG(""); if (d->mState == WaitForEndOfUrl) { goToNextUrl(); } } void SlideShow::goToNextUrl() { LOG(""); QUrl url = d->findNextUrl(); LOG("url:" << url); if (!url.isValid()) { pause(); return; } emit goToUrl(url); } void SlideShow::setCurrentUrl(const QUrl &url) { LOG(url); if (d->mCurrentUrl == url) { return; } d->mCurrentUrl = url; // Restart timer to avoid showing new url for the remaining time of the old // url if (d->mState != Paused) { d->doStart(); } } bool SlideShow::isRunning() const { return d->mState != Paused; } void SlideShow::updateConfig() { GwenviewConfig::setLoop(d->mLoopAction->isChecked()); GwenviewConfig::setRandom(d->mRandomAction->isChecked()); } void SlideShow::slotRandomActionToggled(bool on) { if (on && d->mState != Paused) { d->initShuffledUrls(); } } } // namespace diff --git a/lib/thumbnailview/itemeditor.cpp b/lib/thumbnailview/itemeditor.cpp index fc5ce75e..2e187294 100644 --- a/lib/thumbnailview/itemeditor.cpp +++ b/lib/thumbnailview/itemeditor.cpp @@ -1,89 +1,89 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2009 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 "itemeditor.h" // Qt #include #include #include #include // KDE // Local namespace Gwenview { struct ItemEditorPrivate { QPoint mCenter; }; ItemEditor::ItemEditor(QWidget* parent) : KLineEdit(parent) , d(new ItemEditorPrivate) { setPalette(QApplication::palette()); connect(this, &ItemEditor::textChanged, this, &ItemEditor::resizeToContents); setTrapReturnKey(true); } ItemEditor::~ItemEditor() { delete d; } void ItemEditor::showEvent(QShowEvent* event) { // We can't do this in PreviewItemDelegate::updateEditorGeometry() because QAbstractItemView outsmarts us by calling selectAll() on the editor if it is a QLineEdit QMimeDatabase db; const QString extension = db.suffixForFileName(text()); if (!extension.isEmpty()) { // The filename contains an extension. Assure that only the filename // gets selected. const int selectionLength = text().length() - extension.length() - 1; setSelection(0, selectionLength); } KLineEdit::showEvent(event); } void ItemEditor::resizeToContents() { if (d->mCenter.isNull()) { d->mCenter = geometry().center(); } - int textWidth = fontMetrics().width(" " + text() + " "); + int textWidth = fontMetrics().boundingRect(" " + text() + " ").width(); QRect rect = geometry(); rect.setWidth(textWidth); rect.moveCenter(d->mCenter); if (rect.right() > parentWidget()->width()) { rect.setRight(parentWidget()->width()); } if (rect.left() < 0) { rect.setLeft(0); } setGeometry(rect); } } // namespace diff --git a/lib/thumbnailview/previewitemdelegate.cpp b/lib/thumbnailview/previewitemdelegate.cpp index f910b0c1..a699dd05 100644 --- a/lib/thumbnailview/previewitemdelegate.cpp +++ b/lib/thumbnailview/previewitemdelegate.cpp @@ -1,985 +1,985 @@ // 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 "previewitemdelegate.h" #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE #include #endif // Local #include "archiveutils.h" #include "itemeditor.h" #include "paintutils.h" #include "thumbnailview.h" #include "timeutils.h" #include "tooltipwidget.h" #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE #include "../semanticinfo/semanticinfodirmodel.h" #endif // Define this to be able to fine tune the rendering of the selection // background through a config file //#define FINETUNE_SELECTION_BACKGROUND #ifdef FINETUNE_SELECTION_BACKGROUND #include #include #endif //#define DEBUG_DRAW_BORDER //#define DEBUG_DRAW_CURRENT namespace Gwenview { /** * Space between the item outer rect and the content, and between the * thumbnail and the caption */ const int ITEM_MARGIN = 5; /** How darker is the border line around selection */ const int SELECTION_BORDER_DARKNESS = 140; const int FOCUS_BORDER_DARKNESS = 200; /** Radius of the selection rounded corners, in pixels */ const int SELECTION_RADIUS = 5; /** Space between the item outer rect and the context bar */ const int CONTEXTBAR_MARGIN = 1; /** How dark is the shadow, 0 is invisible, 255 is as dark as possible */ const int SHADOW_STRENGTH = 128; /** How many pixels around the thumbnail are shadowed */ const int SHADOW_SIZE = 4; static KFileItem fileItemForIndex(const QModelIndex& index) { Q_ASSERT(index.isValid()); QVariant data = index.data(KDirModel::FileItemRole); return qvariant_cast(data); } static QUrl urlForIndex(const QModelIndex& index) { KFileItem item = fileItemForIndex(index); return item.url(); } struct PreviewItemDelegatePrivate { /** * Maps full text to elided text. */ mutable QHash mElidedTextCache; // Key is height * 1000 + width typedef QHash ShadowCache; mutable ShadowCache mShadowCache; PreviewItemDelegate* q; ThumbnailView* mView; QWidget* mContextBar; QToolButton* mSaveButton; QPixmap mSaveButtonPixmap; QToolButton* mToggleSelectionButton; QToolButton* mFullScreenButton; QToolButton* mRotateLeftButton; QToolButton* mRotateRightButton; #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE KRatingPainter mRatingPainter; #endif QPersistentModelIndex mIndexUnderCursor; QSize mThumbnailSize; PreviewItemDelegate::ThumbnailDetails mDetails; PreviewItemDelegate::ContextBarActions mContextBarActions; Qt::TextElideMode mTextElideMode; QPointer mToolTip; QScopedPointer mToolTipAnimation; void initSaveButtonPixmap() { if (!mSaveButtonPixmap.isNull()) { return; } // Necessary otherwise we won't see the save button itself mSaveButton->adjustSize(); mSaveButtonPixmap = QPixmap(mSaveButton->sizeHint()); mSaveButtonPixmap.fill(Qt::transparent); mSaveButton->render(&mSaveButtonPixmap, QPoint(), QRegion(), QWidget::DrawChildren); } void showContextBar(const QRect& rect, const QPixmap& thumbnailPix) { if (mContextBarActions == PreviewItemDelegate::NoAction) { return; } mContextBar->adjustSize(); // Center bar, except if only showing SelectionAction. const int posX = mContextBarActions == PreviewItemDelegate::SelectionAction ? 0 : (rect.width() - mContextBar->width()) / 2; const int thumbnailPixHeight = qRound(thumbnailPix.height() / thumbnailPix.devicePixelRatio()); const int posY = qMax(CONTEXTBAR_MARGIN, mThumbnailSize.height() - thumbnailPixHeight - mContextBar->height()); mContextBar->move(rect.topLeft() + QPoint(posX, posY)); mContextBar->show(); } void initToolTip() { mToolTip = new ToolTipWidget(mView->viewport()); mToolTip->setOpacity(0); mToolTip->show(); } bool hoverEventFilter(QHoverEvent* event) { QModelIndex index = mView->indexAt(event->pos()); if (index != mIndexUnderCursor) { updateHoverUi(index); } else { // Same index, nothing to do, but repaint anyway in case we are // over the rating row mView->update(mIndexUnderCursor); } return false; } void updateHoverUi(const QModelIndex& index) { QModelIndex oldIndex = mIndexUnderCursor; mIndexUnderCursor = index; mView->update(oldIndex); if (QApplication::style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, mView)) { mView->setCursor(mIndexUnderCursor.isValid() ? Qt::PointingHandCursor : Qt::ArrowCursor); } if (mIndexUnderCursor.isValid()) { updateToggleSelectionButton(); updateImageButtons(); const QRect rect = mView->visualRect(mIndexUnderCursor); const QPixmap thumbnailPix = mView->thumbnailForIndex(index); showContextBar(rect, thumbnailPix); if (mView->isModified(mIndexUnderCursor)) { showSaveButton(rect); } else { mSaveButton->hide(); } showToolTip(index); mView->update(mIndexUnderCursor); } else { mContextBar->hide(); mSaveButton->hide(); hideToolTip(); } } QRect ratingRectFromIndexRect(const QRect& rect) const { return QRect( rect.left(), rect.bottom() - ratingRowHeight() - ITEM_MARGIN, rect.width(), ratingRowHeight()); } #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE int ratingFromCursorPosition(const QRect& ratingRect) const { const QPoint pos = mView->viewport()->mapFromGlobal(QCursor::pos()); return mRatingPainter.ratingFromPosition(ratingRect, pos); } #endif bool mouseButtonEventFilter(QEvent::Type type) { #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE const QRect rect = ratingRectFromIndexRect(mView->visualRect(mIndexUnderCursor)); const int rating = ratingFromCursorPosition(rect); if (rating == -1) { return false; } if (type == QEvent::MouseButtonRelease) { q->setDocumentRatingRequested(urlForIndex(mIndexUnderCursor) , rating); } return true; #else return false; #endif } QPoint saveButtonPosition(const QRect& itemRect) const { QSize buttonSize = mSaveButton->sizeHint(); int posX = itemRect.right() - buttonSize.width(); int posY = itemRect.top() + mThumbnailSize.height() + 2 * ITEM_MARGIN - buttonSize.height(); return QPoint(posX, posY); } void showSaveButton(const QRect& itemRect) const { mSaveButton->move(saveButtonPosition(itemRect)); mSaveButton->show(); } void drawBackground(QPainter* painter, const QRect& rect, const QColor& bgColor, const QColor& borderColor) const { int bgH, bgS, bgV; int borderH, borderS, borderV, borderMargin; #ifdef FINETUNE_SELECTION_BACKGROUND QSettings settings(QDir::homePath() + "/colors.ini", QSettings::IniFormat); bgH = settings.value("bg/h").toInt(); bgS = settings.value("bg/s").toInt(); bgV = settings.value("bg/v").toInt(); borderH = settings.value("border/h").toInt(); borderS = settings.value("border/s").toInt(); borderV = settings.value("border/v").toInt(); borderMargin = settings.value("border/margin").toInt(); #else bgH = 0; bgS = -20; bgV = 43; borderH = 0; borderS = -100; borderV = 60; borderMargin = 1; #endif painter->setRenderHint(QPainter::Antialiasing); QRectF rectF = QRectF(rect).adjusted(0.5, 0.5, -0.5, -0.5); QPainterPath path = PaintUtils::roundedRectangle(rectF, SELECTION_RADIUS); QLinearGradient gradient(rectF.topLeft(), rectF.bottomLeft()); gradient.setColorAt(0, PaintUtils::adjustedHsv(bgColor, bgH, bgS, bgV)); gradient.setColorAt(1, bgColor); painter->fillPath(path, gradient); painter->setPen(borderColor); painter->drawPath(path); painter->setPen(PaintUtils::adjustedHsv(borderColor, borderH, borderS, borderV)); rectF = rectF.adjusted(borderMargin, borderMargin, -borderMargin, -borderMargin); path = PaintUtils::roundedRectangle(rectF, SELECTION_RADIUS); painter->drawPath(path); } void drawShadow(QPainter* painter, const QRect& rect) const { const QPoint shadowOffset(-SHADOW_SIZE, -SHADOW_SIZE + 1); const auto dpr = painter->device()->devicePixelRatioF(); int key = qRound((rect.height() * 1000 + rect.width()) * dpr); 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 * dpr, color, qRound(SHADOW_SIZE * dpr)); shadow.setDevicePixelRatio(dpr); it = mShadowCache.insert(key, shadow); } painter->drawPixmap(rect.topLeft() + shadowOffset, it.value()); } void drawText(QPainter* painter, const QRect& rect, const QColor& fgColor, const QString& fullText) const { QFontMetrics fm = mView->fontMetrics(); // Elide text QString text; QHash::const_iterator it = mElidedTextCache.constFind(fullText); if (it == mElidedTextCache.constEnd()) { text = fm.elidedText(fullText, mTextElideMode, rect.width()); mElidedTextCache[fullText] = text; } else { text = it.value(); } // Compute x pos int posX; if (text.length() == fullText.length()) { // Not elided, center text - posX = (rect.width() - fm.width(text)) / 2; + posX = (rect.width() - fm.boundingRect(text).width()) / 2; } else { // Elided, left align posX = 0; } // Draw text painter->setPen(fgColor); painter->drawText(rect.left() + posX, rect.top() + fm.ascent(), text); } void drawRating(QPainter* painter, const QRect& rect, const QVariant& value) { #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE const int rating = value.toInt(); const QRect ratingRect = ratingRectFromIndexRect(rect); const int hoverRating = ratingFromCursorPosition(ratingRect); mRatingPainter.paint(painter, ratingRect, rating, hoverRating); #endif } bool isTextElided(const QString& text) const { QHash::const_iterator it = mElidedTextCache.constFind(text); if (it == mElidedTextCache.constEnd()) { return false; } return it.value().length() < text.length(); } /** * Show a tooltip only if the item has been elided. * This function places the tooltip over the item text. */ void showToolTip(const QModelIndex& index) { if (mDetails == 0 || mDetails == PreviewItemDelegate::RatingDetail) { // No text to display return; } // Gather tip text QStringList textList; bool elided = false; if (mDetails & PreviewItemDelegate::FileNameDetail) { const QString text = index.data().toString(); elided |= isTextElided(text); textList << text; } // FIXME: Duplicated from drawText const KFileItem fileItem = fileItemForIndex(index); const bool isDirOrArchive = ArchiveUtils::fileItemIsDirOrArchive(fileItem); if (mDetails & PreviewItemDelegate::DateDetail) { if (!ArchiveUtils::fileItemIsDirOrArchive(fileItem)) { const QDateTime dt = TimeUtils::dateTimeForFileItem(fileItem); const QString text = QLocale().toString(dt, QLocale::ShortFormat); elided |= isTextElided(text); textList << text; } } if (!isDirOrArchive && (mDetails & PreviewItemDelegate::ImageSizeDetail)) { QSize fullSize; QPixmap thumbnailPix = mView->thumbnailForIndex(index, &fullSize); if (fullSize.isValid()) { const QString text = QStringLiteral("%1x%2").arg(fullSize.width()).arg(fullSize.height()); elided |= isTextElided(text); textList << text; } } if (!isDirOrArchive && (mDetails & PreviewItemDelegate::FileSizeDetail)) { const KIO::filesize_t size = fileItem.size(); if (size > 0) { const QString text = KIO::convertSize(size); elided |= isTextElided(text); textList << text; } } if (!elided) { hideToolTip(); return; } bool newTipLabel = !mToolTip; if (!mToolTip) { initToolTip(); } mToolTip->setText(textList.join(QLatin1Char('\n'))); QSize tipSize = mToolTip->sizeHint(); // Compute tip position QRect rect = mView->visualRect(index); const int textY = ITEM_MARGIN + mThumbnailSize.height() + ITEM_MARGIN; const int spacing = 1; QRect geometry( QPoint(rect.topLeft() + QPoint((rect.width() - tipSize.width()) / 2, textY + spacing)), tipSize ); if (geometry.left() < 0) { geometry.moveLeft(0); } else if (geometry.right() > mView->viewport()->width()) { geometry.moveRight(mView->viewport()->width()); } // Show tip QParallelAnimationGroup* anim = new QParallelAnimationGroup(); QPropertyAnimation* fadeIn = new QPropertyAnimation(mToolTip, "opacity"); fadeIn->setStartValue(mToolTip->opacity()); fadeIn->setEndValue(1.); anim->addAnimation(fadeIn); if (newTipLabel) { mToolTip->setGeometry(geometry); } else { QPropertyAnimation* move = new QPropertyAnimation(mToolTip, "geometry"); move->setStartValue(mToolTip->geometry()); move->setEndValue(geometry); anim->addAnimation(move); } mToolTipAnimation.reset(anim); mToolTipAnimation->start(); } void hideToolTip() { if (!mToolTip) { return; } QSequentialAnimationGroup* anim = new QSequentialAnimationGroup(); if (mToolTipAnimation->state() == QPropertyAnimation::Stopped) { anim->addPause(500); } QPropertyAnimation* fadeOut = new QPropertyAnimation(mToolTip, "opacity"); fadeOut->setStartValue(mToolTip->opacity()); fadeOut->setEndValue(0.); anim->addAnimation(fadeOut); mToolTipAnimation.reset(anim); mToolTipAnimation->start(); QObject::connect(anim, &QSequentialAnimationGroup::finished, mToolTip.data(), &ToolTipWidget::deleteLater); } int itemWidth() const { return mThumbnailSize.width() + 2 * ITEM_MARGIN; } int ratingRowHeight() const { #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE return qMax(mView->fontMetrics().ascent(), int(KIconLoader::SizeSmall)); #else return 0; #endif } int itemHeight() const { const int lineHeight = mView->fontMetrics().height(); int textHeight = 0; if (mDetails & PreviewItemDelegate::FileNameDetail) { textHeight += lineHeight; } if (mDetails & PreviewItemDelegate::DateDetail) { textHeight += lineHeight; } if (mDetails & PreviewItemDelegate::ImageSizeDetail) { textHeight += lineHeight; } if (mDetails & PreviewItemDelegate::FileSizeDetail) { textHeight += lineHeight; } if (mDetails & PreviewItemDelegate::RatingDetail) { textHeight += ratingRowHeight(); } if (textHeight == 0) { // Keep at least one row of text, so that we can show folder names textHeight = lineHeight; } return mThumbnailSize.height() + textHeight + 3 * ITEM_MARGIN; } void selectIndexUnderCursorIfNoMultiSelection() { if (mView->selectionModel()->selectedIndexes().size() <= 1) { mView->setCurrentIndex(mIndexUnderCursor); } } void updateToggleSelectionButton() { mToggleSelectionButton->setIcon(QIcon::fromTheme( mView->selectionModel()->isSelected(mIndexUnderCursor) ? QStringLiteral("list-remove") : QStringLiteral("list-add") )); } void updateImageButtons() { const KFileItem item = fileItemForIndex(mIndexUnderCursor); const bool isImage = !ArchiveUtils::fileItemIsDirOrArchive(item); mFullScreenButton->setEnabled(isImage); mRotateLeftButton->setEnabled(isImage); mRotateRightButton->setEnabled(isImage); } void updateContextBar() { if (mContextBarActions == PreviewItemDelegate::NoAction) { mContextBar->hide(); return; } const int width = itemWidth(); const int buttonWidth = mRotateRightButton->sizeHint().width(); mFullScreenButton->setVisible(mContextBarActions & PreviewItemDelegate::FullScreenAction); bool rotate = mContextBarActions & PreviewItemDelegate::RotateAction; mRotateLeftButton->setVisible(rotate && width >= 3 * buttonWidth); mRotateRightButton->setVisible(rotate && width >= 4 * buttonWidth); mContextBar->adjustSize(); } void updateViewGridSize() { mView->setGridSize(QSize(itemWidth(), itemHeight())); } }; PreviewItemDelegate::PreviewItemDelegate(ThumbnailView* view) : QItemDelegate(view) , d(new PreviewItemDelegatePrivate) { d->q = this; d->mView = view; 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->mThumbnailSize = view->thumbnailSize(); d->mDetails = FileNameDetail; d->mContextBarActions = SelectionAction | FullScreenAction | RotateAction; d->mTextElideMode = Qt::ElideRight; connect(view, &ThumbnailView::rowsRemovedSignal, this, &PreviewItemDelegate::slotRowsChanged); connect(view, &ThumbnailView::rowsInsertedSignal, this, &PreviewItemDelegate::slotRowsChanged); connect(view, &ThumbnailView::selectionChangedSignal, [this]() { d->updateToggleSelectionButton(); }); #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE d->mRatingPainter.setAlignment(Qt::AlignHCenter | Qt::AlignBottom); d->mRatingPainter.setLayoutDirection(view->layoutDirection()); d->mRatingPainter.setMaxRating(10); #endif connect(view, &ThumbnailView::thumbnailSizeChanged, this, &PreviewItemDelegate::setThumbnailSize); // Button frame d->mContextBar = new QWidget(d->mView->viewport()); d->mContextBar->hide(); d->mToggleSelectionButton = new QToolButton; d->mToggleSelectionButton->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); connect(d->mToggleSelectionButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotToggleSelectionClicked); d->mFullScreenButton = new QToolButton; d->mFullScreenButton->setIcon(QIcon::fromTheme(QStringLiteral("view-fullscreen"))); connect(d->mFullScreenButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotFullScreenClicked); d->mRotateLeftButton = new QToolButton; d->mRotateLeftButton->setIcon(QIcon::fromTheme(QStringLiteral("object-rotate-left"))); connect(d->mRotateLeftButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotRotateLeftClicked); d->mRotateRightButton = new QToolButton; d->mRotateRightButton->setIcon(QIcon::fromTheme(QStringLiteral("object-rotate-right"))); connect(d->mRotateRightButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotRotateRightClicked); QHBoxLayout* layout = new QHBoxLayout(d->mContextBar); layout->setMargin(2); layout->setSpacing(2); layout->addWidget(d->mToggleSelectionButton); layout->addWidget(d->mFullScreenButton); layout->addWidget(d->mRotateLeftButton); layout->addWidget(d->mRotateRightButton); // Save button d->mSaveButton = new QToolButton(d->mView->viewport()); d->mSaveButton->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); d->mSaveButton->hide(); connect(d->mSaveButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotSaveClicked); } PreviewItemDelegate::~PreviewItemDelegate() { delete d; } QSize PreviewItemDelegate::sizeHint(const QStyleOptionViewItem & /*option*/, const QModelIndex & /*index*/) const { return d->mView->gridSize(); } bool PreviewItemDelegate::eventFilter(QObject* object, QEvent* event) { if (object == d->mView->viewport()) { switch (event->type()) { case QEvent::ToolTip: return true; case QEvent::HoverMove: case QEvent::HoverLeave: return d->hoverEventFilter(static_cast(event)); case QEvent::MouseButtonPress: case QEvent::MouseButtonRelease: return d->mouseButtonEventFilter(event->type()); default: return false; } } else { // Necessary for the item editor to work correctly (especially closing // the editor with the Escape key) return QItemDelegate::eventFilter(object, event); } } void PreviewItemDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const { int thumbnailHeight = d->mThumbnailSize.height(); QSize fullSize; QPixmap thumbnailPix = d->mView->thumbnailForIndex(index, &fullSize); QSize thumbnailSize = thumbnailPix.size() / thumbnailPix.devicePixelRatio(); const KFileItem fileItem = fileItemForIndex(index); const bool opaque = !thumbnailPix.hasAlphaChannel(); const bool isDirOrArchive = ArchiveUtils::fileItemIsDirOrArchive(fileItem); QRect rect = option.rect; const bool selected = option.state & QStyle::State_Selected; const bool underMouse = option.state & QStyle::State_MouseOver; const bool hasFocus = option.state & QStyle::State_HasFocus; const QWidget* viewport = d->mView->viewport(); #ifdef DEBUG_DRAW_BORDER painter->setPen(Qt::red); painter->setBrush(Qt::NoBrush); painter->drawRect(rect); #endif // Select color group QPalette::ColorGroup cg; if ((option.state & QStyle::State_Enabled) && (option.state & QStyle::State_Active)) { cg = QPalette::Normal; } else if ((option.state & QStyle::State_Enabled)) { cg = QPalette::Inactive; } else { cg = QPalette::Disabled; } // Select colors QColor bgColor, borderColor, fgColor; fgColor = viewport->palette().color(viewport->foregroundRole()); if (selected || underMouse) { bgColor = option.palette.color(cg, QPalette::Highlight); if (hasFocus) { borderColor = bgColor.darker(FOCUS_BORDER_DARKNESS); } else { borderColor = bgColor.darker(SELECTION_BORDER_DARKNESS); } } else { bgColor = viewport->palette().color(viewport->backgroundRole()); if (hasFocus) { borderColor = fgColor; } else { borderColor = bgColor.lighter(200); } } // Compute thumbnailRect QRect thumbnailRect = QRect( rect.left() + (rect.width() - thumbnailSize.width()) / 2, rect.top() + (thumbnailHeight - thumbnailSize.height()) + ITEM_MARGIN, thumbnailSize.width(), thumbnailSize.height()); // Draw background const QRect backgroundRect = thumbnailRect.adjusted(-ITEM_MARGIN, -ITEM_MARGIN, ITEM_MARGIN, ITEM_MARGIN); if (selected) { d->drawBackground(painter, backgroundRect, bgColor, borderColor); } else if (underMouse) { painter->setOpacity(0.2); d->drawBackground(painter, backgroundRect, bgColor, borderColor); painter->setOpacity(1.); } else if (opaque) { d->drawShadow(painter, thumbnailRect); } // Draw thumbnail if (opaque) { painter->setPen(borderColor); painter->setRenderHint(QPainter::Antialiasing, false); QRect borderRect = thumbnailRect.adjusted(-1, -1, 0, 0); painter->drawRect(borderRect); } else if (hasFocus && !selected) { painter->setPen(option.palette.color(cg, QPalette::Highlight)); painter->setRenderHint(QPainter::Antialiasing, false); QLine underLine = QLine(thumbnailRect.bottomLeft(), thumbnailRect.bottomRight()); underLine.translate(0, 3); painter->drawLine(underLine); } painter->drawPixmap(thumbnailRect.left(), thumbnailRect.top(), thumbnailPix); // Draw modified indicator bool isModified = d->mView->isModified(index); if (isModified) { // Draws a pixmap of the save button frame, as an indicator that // the image has been modified QPoint framePosition = d->saveButtonPosition(rect); d->initSaveButtonPixmap(); painter->drawPixmap(framePosition, d->mSaveButtonPixmap); } // 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); } if (index == d->mIndexUnderCursor) { // Show bar again: if the thumbnail has changed, we may need to update // its position. Don't do it if we are over rotate buttons, though: it // would not be nice to move the button now, the user may want to // rotate the image one more time. // The button will get moved when the mouse leaves. if (!d->mRotateLeftButton->underMouse() && !d->mRotateRightButton->underMouse()) { d->showContextBar(rect, thumbnailPix); } if (isModified) { // If we just rotated the image with the buttons from the // button frame, we need to show the save button frame right now. d->showSaveButton(rect); } else { d->mSaveButton->hide(); } } QRect textRect( rect.left() + ITEM_MARGIN, rect.top() + 2 * ITEM_MARGIN + thumbnailHeight, rect.width() - 2 * ITEM_MARGIN, d->mView->fontMetrics().height()); if (isDirOrArchive || (d->mDetails & PreviewItemDelegate::FileNameDetail)) { d->drawText(painter, textRect, fgColor, index.data().toString()); textRect.moveTop(textRect.bottom()); } if (!isDirOrArchive && (d->mDetails & PreviewItemDelegate::DateDetail)) { const QDateTime dt = TimeUtils::dateTimeForFileItem(fileItem); d->drawText(painter, textRect, fgColor, QLocale().toString(dt, QLocale::ShortFormat)); textRect.moveTop(textRect.bottom()); } if (!isDirOrArchive && (d->mDetails & PreviewItemDelegate::ImageSizeDetail)) { if (fullSize.isValid()) { const QString text = QStringLiteral("%1x%2").arg(fullSize.width()).arg(fullSize.height()); d->drawText(painter, textRect, fgColor, text); textRect.moveTop(textRect.bottom()); } } if (!isDirOrArchive && (d->mDetails & PreviewItemDelegate::FileSizeDetail)) { const KIO::filesize_t size = fileItem.size(); if (size > 0) { const QString st = KIO::convertSize(size); d->drawText(painter, textRect, fgColor, st); textRect.moveTop(textRect.bottom()); } } if (!isDirOrArchive && (d->mDetails & PreviewItemDelegate::RatingDetail)) { #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE d->drawRating(painter, rect, index.data(SemanticInfoDirModel::RatingRole)); #endif } #ifdef DEBUG_DRAW_CURRENT if (d->mView->currentIndex() == index) { painter->fillRect(rect.left(), rect.top(), 12, 12, Qt::red); } #endif } void PreviewItemDelegate::setThumbnailSize(const QSize& value) { d->mThumbnailSize = value; d->updateViewGridSize(); d->updateContextBar(); d->mElidedTextCache.clear(); } void PreviewItemDelegate::slotSaveClicked() { emit saveDocumentRequested(urlForIndex(d->mIndexUnderCursor)); } void PreviewItemDelegate::slotRotateLeftClicked() { d->selectIndexUnderCursorIfNoMultiSelection(); emit rotateDocumentLeftRequested(urlForIndex(d->mIndexUnderCursor)); } void PreviewItemDelegate::slotRotateRightClicked() { d->selectIndexUnderCursorIfNoMultiSelection(); emit rotateDocumentRightRequested(urlForIndex(d->mIndexUnderCursor)); } void PreviewItemDelegate::slotFullScreenClicked() { emit showDocumentInFullScreenRequested(urlForIndex(d->mIndexUnderCursor)); } void PreviewItemDelegate::slotToggleSelectionClicked() { d->mView->selectionModel()->select(d->mIndexUnderCursor, QItemSelectionModel::Toggle); } PreviewItemDelegate::ThumbnailDetails PreviewItemDelegate::thumbnailDetails() const { return d->mDetails; } void PreviewItemDelegate::setThumbnailDetails(PreviewItemDelegate::ThumbnailDetails details) { d->mDetails = details; d->updateViewGridSize(); d->mView->scheduleDelayedItemsLayout(); } PreviewItemDelegate::ContextBarActions PreviewItemDelegate::contextBarActions() const { return d->mContextBarActions; } void PreviewItemDelegate::setContextBarActions(PreviewItemDelegate::ContextBarActions actions) { d->mContextBarActions = actions; d->updateContextBar(); } Qt::TextElideMode PreviewItemDelegate::textElideMode() const { return d->mTextElideMode; } void PreviewItemDelegate::setTextElideMode(Qt::TextElideMode mode) { if (d->mTextElideMode == mode) { return; } d->mTextElideMode = mode; d->mElidedTextCache.clear(); d->mView->viewport()->update(); } void PreviewItemDelegate::slotRowsChanged() { // We need to update hover ui because the current index may have // disappeared: for example if the current image is removed with "del". QPoint pos = d->mView->viewport()->mapFromGlobal(QCursor::pos()); QModelIndex index = d->mView->indexAt(pos); d->updateHoverUi(index); } QWidget * PreviewItemDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& /*option*/, const QModelIndex& /*index*/) const { return new ItemEditor(parent); } void PreviewItemDelegate::setEditorData(QWidget* widget, const QModelIndex& index) const { ItemEditor* edit = qobject_cast(widget); if (!edit) { return; } edit->setText(index.data().toString()); } void PreviewItemDelegate::updateEditorGeometry(QWidget* widget, const QStyleOptionViewItem& option, const QModelIndex& index) const { ItemEditor* edit = qobject_cast(widget); if (!edit) { return; } QString text = index.data().toString(); - int textWidth = edit->fontMetrics().width(QStringLiteral(" ") + text + QStringLiteral(" ")); + int textWidth = edit->fontMetrics().boundingRect(QStringLiteral(" ") + text + QStringLiteral(" ")).width(); QRect textRect( option.rect.left() + (option.rect.width() - textWidth) / 2, option.rect.top() + 2 * ITEM_MARGIN + d->mThumbnailSize.height(), textWidth, edit->sizeHint().height()); edit->setGeometry(textRect); } void PreviewItemDelegate::setModelData(QWidget* widget, QAbstractItemModel* model, const QModelIndex& index) const { ItemEditor* edit = qobject_cast(widget); if (!edit) { return; } if (index.data().toString() != edit->text()) { model->setData(index, edit->text(), Qt::EditRole); } } } // namespace diff --git a/lib/zoomwidget.cpp b/lib/zoomwidget.cpp index 56f5c7b2..68e49f37 100644 --- a/lib/zoomwidget.cpp +++ b/lib/zoomwidget.cpp @@ -1,201 +1,201 @@ // 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 "zoomwidget.h" // stdc++ #include // Qt #include #include #include #include #include // KDE // Local #include "zoomslider.h" #include "signalblocker.h" #include "statusbartoolbutton.h" namespace Gwenview { static const qreal MAGIC_K = 1.04; static const qreal MAGIC_OFFSET = 16.; static const qreal PRECISION = 100.; inline int sliderValueForZoom(qreal zoom) { return int(PRECISION * (log(zoom) / log(MAGIC_K) + MAGIC_OFFSET)); } inline qreal zoomForSliderValue(int sliderValue) { return pow(MAGIC_K, sliderValue / PRECISION - MAGIC_OFFSET); } struct ZoomWidgetPrivate { ZoomWidget* q; StatusBarToolButton* mZoomToFitButton; StatusBarToolButton* mActualSizeButton; StatusBarToolButton* mZoomToFillButton; QLabel* mZoomLabel; ZoomSlider* mZoomSlider; QAction* mZoomToFitAction; QAction* mActualSizeAction; QAction* mZoomToFillAction; bool mZoomUpdatedBySlider; void emitZoomChanged() { // Use QSlider::sliderPosition(), not QSlider::value() because when we are // called from slotZoomSliderActionTriggered(), QSlider::value() has not // been updated yet. qreal zoom = zoomForSliderValue(mZoomSlider->slider()->sliderPosition()); mZoomUpdatedBySlider = true; emit q->zoomChanged(zoom); mZoomUpdatedBySlider = false; } }; ZoomWidget::ZoomWidget(QWidget* parent) : QFrame(parent) , d(new ZoomWidgetPrivate) { d->q = this; d->mZoomUpdatedBySlider = false; setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Minimum); d->mZoomToFitButton = new StatusBarToolButton; d->mActualSizeButton = new StatusBarToolButton; d->mZoomToFillButton = new StatusBarToolButton; d->mZoomToFitButton->setCheckable(true); d->mActualSizeButton->setCheckable(true); d->mZoomToFillButton->setCheckable(true); d->mZoomToFitButton->setChecked(true); if (QApplication::isLeftToRight()) { d->mZoomToFitButton->setGroupPosition(StatusBarToolButton::GroupLeft); d->mZoomToFillButton->setGroupPosition(StatusBarToolButton::GroupCenter); d->mActualSizeButton->setGroupPosition(StatusBarToolButton::GroupRight); } else { d->mActualSizeButton->setGroupPosition(StatusBarToolButton::GroupLeft); d->mZoomToFillButton->setGroupPosition(StatusBarToolButton::GroupCenter); d->mZoomToFitButton->setGroupPosition(StatusBarToolButton::GroupRight); } d->mZoomLabel = new QLabel; - d->mZoomLabel->setFixedWidth(d->mZoomLabel->fontMetrics().width(QStringLiteral(" 1000% "))); + d->mZoomLabel->setFixedWidth(d->mZoomLabel->fontMetrics().boundingRect(QStringLiteral(" 1000% ")).width()); d->mZoomLabel->setAlignment(Qt::AlignCenter); d->mZoomSlider = new ZoomSlider; d->mZoomSlider->setMinimumWidth(150); d->mZoomSlider->slider()->setSingleStep(int(PRECISION)); d->mZoomSlider->slider()->setPageStep(3 * int(PRECISION)); connect(d->mZoomSlider->slider(), &QAbstractSlider::actionTriggered, this, &ZoomWidget::slotZoomSliderActionTriggered); // Layout QHBoxLayout* layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); layout->addWidget(d->mZoomToFitButton); layout->addWidget(d->mZoomToFillButton); layout->addWidget(d->mActualSizeButton); layout->addWidget(d->mZoomSlider); layout->addWidget(d->mZoomLabel); } ZoomWidget::~ZoomWidget() { delete d; } void ZoomWidget::setActions(QAction* zoomToFitAction, QAction* actualSizeAction, QAction* zoomInAction, QAction* zoomOutAction, QAction* zoomToFillAction) { d->mZoomToFitAction = zoomToFitAction; d->mActualSizeAction = actualSizeAction; d->mZoomToFillAction = zoomToFillAction; d->mZoomToFitButton->setDefaultAction(zoomToFitAction); d->mActualSizeButton->setDefaultAction(actualSizeAction); d->mZoomToFillButton->setDefaultAction(zoomToFillAction); d->mZoomSlider->setZoomInAction(zoomInAction); d->mZoomSlider->setZoomOutAction(zoomOutAction); QActionGroup *actionGroup = new QActionGroup(d->q); actionGroup->addAction(d->mZoomToFitAction); actionGroup->addAction(d->mZoomToFillAction); actionGroup->addAction(d->mActualSizeAction); actionGroup->setExclusive(true); // Adjust sizes int width = std::max({d->mZoomToFitButton->sizeHint().width(), d->mActualSizeButton->sizeHint().width(), d->mZoomToFillButton->sizeHint().width()}); d->mZoomToFitButton->setFixedWidth(width); d->mActualSizeButton->setFixedWidth(width); d->mZoomToFillButton->setFixedWidth(width); } void ZoomWidget::slotZoomSliderActionTriggered() { // The slider value changed because of the user (not because of range // changes). In this case disable zoom and apply slider value. d->emitZoomChanged(); } void ZoomWidget::setZoom(qreal zoom) { int intZoom = qRound(zoom * 100); d->mZoomLabel->setText(QStringLiteral("%1%").arg(intZoom)); // Don't change slider value if we come here because the slider change, // avoids choppy sliding scroll. if (!d->mZoomUpdatedBySlider) { QSlider* slider = d->mZoomSlider->slider(); SignalBlocker blocker(slider); int value = sliderValueForZoom(zoom); if (value < slider->minimum()) { // It is possible that we are called *before* setMinimumZoom() as // been called. In this case, define the minimum ourself. d->mZoomSlider->setMinimum(value); } d->mZoomSlider->setValue(value); } } void ZoomWidget::setMinimumZoom(qreal minimumZoom) { d->mZoomSlider->setMinimum(sliderValueForZoom(minimumZoom)); } void ZoomWidget::setMaximumZoom(qreal zoom) { d->mZoomSlider->setMaximum(sliderValueForZoom(zoom)); } } // namespace diff --git a/tests/auto/recursivedirmodeltest.cpp b/tests/auto/recursivedirmodeltest.cpp index 34f893f1..464d4ce6 100644 --- a/tests/auto/recursivedirmodeltest.cpp +++ b/tests/auto/recursivedirmodeltest.cpp @@ -1,205 +1,205 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2012 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 "recursivedirmodeltest.h" // Local #include #include // Qt // KDE #include #include #include using namespace Gwenview; QTEST_MAIN(RecursiveDirModelTest) void RecursiveDirModelTest::testBasic_data() { QTest::addColumn("initialFiles"); QTest::addColumn("addedFiles"); QTest::addColumn("removedFiles"); #define NEW_ROW(name, initialFiles, addedFiles, removedFiles) QTest::newRow(name) << (initialFiles) << (addedFiles) << (removedFiles) NEW_ROW("empty_dir", QStringList(), QStringList() << "new.jpg", QStringList() << "new.jpg" ); NEW_ROW("images_only", QStringList() << "pict01.jpg" << "pict02.jpg" << "pict03.jpg", QStringList() << "pict04.jpg", QStringList() << "pict02.jpg" ); NEW_ROW("images_in_two_dirs", QStringList() << "d1/pict101.jpg" << "d1/pict102.jpg" << "d2/pict201.jpg", QStringList() << "d1/pict103.jpg" << "d2/pict202.jpg", QStringList() << "d2/pict202.jpg" ); NEW_ROW("images_in_two_dirs_w_same_names", QStringList() << "d1/a.jpg" << "d1/b.jpg" << "d2/a.jpg" << "d2/b.jpg", QStringList() << "d3/a.jpg" << "d3/b.jpg", QStringList() << "d1/a.jpg" << "d2/a.jpg" << "d3/a.jpg" ); #undef NEW_ROW } static QList listModelUrls(QAbstractItemModel* model) { QList out; for (int row = 0; row < model->rowCount(QModelIndex()); ++row) { QModelIndex index = model->index(row, 0); KFileItem item = index.data(KDirModel::FileItemRole).value(); out << item.url(); } - qSort(out); + std::sort(out.begin(), out.end()); return out; } static QList listExpectedUrls(const QDir& dir, const QStringList& files) { QList lst; Q_FOREACH(const QString &name, files) { lst << QUrl::fromLocalFile(dir.absoluteFilePath(name)); } - qSort(lst); + std::sort(lst.begin(), lst.end()); return lst; } void logLst(const QList& lst) { Q_FOREACH(const QUrl &url, lst) { qWarning() << url.fileName(); } } void RecursiveDirModelTest::testBasic() { QFETCH(QStringList, initialFiles); QFETCH(QStringList, addedFiles); QFETCH(QStringList, removedFiles); TestUtils::SandBoxDir sandBoxDir; RecursiveDirModel model; TestUtils::TimedEventLoop loop; connect(&model, &RecursiveDirModel::completed, &loop, &QEventLoop::quit); // Test initial files sandBoxDir.fill(initialFiles); model.setUrl(QUrl::fromLocalFile(sandBoxDir.absolutePath())); QList out, expected; do { loop.exec(); out = listModelUrls(&model); expected = listExpectedUrls(sandBoxDir, initialFiles); } while (out.size() != expected.size()); QCOMPARE(out, expected); // Test adding new files sandBoxDir.fill(addedFiles); do { loop.exec(); out = listModelUrls(&model); expected = listExpectedUrls(sandBoxDir, initialFiles + addedFiles); } while (out.size() != expected.size()); QCOMPARE(out, expected); # if 0 /* FIXME: This part of the test is not reliable :/ Sometimes some tests pass, * sometimes they don't. It feels like KDirLister::itemsDeleted() is not * always emitted. */ // Test removing files Q_FOREACH(const QString &name, removedFiles) { bool ok = sandBoxDir.remove(name); Q_ASSERT(ok); expected.removeOne(QUrl(sandBoxDir.absoluteFilePath(name))); } QTime chrono; chrono.start(); while (chrono.elapsed() < 2000) { waitForDeferredDeletes(); } out = listModelUrls(&model); if (out != expected) { qWarning() << "out:"; logLst(out); qWarning() << "expected:"; logLst(expected); } QCOMPARE(out, expected); #endif } void RecursiveDirModelTest::testSetNewUrl() { TestUtils::SandBoxDir sandBoxDir; sandBoxDir.fill( QStringList() << "d1/a.jpg" << "d1/b.jpg" << "d1/c.jpg" << "d1/d.jpg" << "d2/e.jpg" << "d2/f.jpg" ); RecursiveDirModel model; TestUtils::TimedEventLoop loop; connect(&model, &RecursiveDirModel::completed, &loop, &QEventLoop::quit); model.setUrl(QUrl::fromLocalFile(sandBoxDir.absoluteFilePath("d1"))); loop.exec(); QCOMPARE(model.rowCount(QModelIndex()), 4); model.setUrl(QUrl::fromLocalFile(sandBoxDir.absoluteFilePath("d2"))); loop.exec(); QCOMPARE(model.rowCount(QModelIndex()), 2); }