diff --git a/ThumbnailView/ThumbnailWidget.cpp b/ThumbnailView/ThumbnailWidget.cpp index 7a46c1ff..53d89e25 100644 --- a/ThumbnailView/ThumbnailWidget.cpp +++ b/ThumbnailView/ThumbnailWidget.cpp @@ -1,456 +1,451 @@ /* Copyright (C) 2003-2020 The KPhotoAlbum Development Team 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ThumbnailWidget.h" #include "CellGeometry.h" #include "Delegate.h" #include "KeyboardEventHandler.h" #include "SelectionMaintainer.h" #include "ThumbnailDND.h" #include "ThumbnailFactory.h" #include "ThumbnailModel.h" #include #include #include #include +#include #include +#include +#include +#include #include #include +#include #include #include #include -#include -#include -#include - -namespace -{ -QColor contrastColor(const QColor &color) -{ - if (color.red() < 127 && color.green() < 127 && color.blue() < 127) - return Qt::white; - else - return Qt::black; -} -} /** * \class ThumbnailView::ThumbnailWidget * This is the widget which shows the thumbnails. * * In previous versions this was implemented using a QIconView, but there * simply was too many problems, so after years of tears and pains I * rewrote it. */ ThumbnailView::ThumbnailWidget::ThumbnailWidget(ThumbnailFactory *factory) : QListView() , ThumbnailComponent(factory) , m_isSettingDate(false) , m_gridResizeInteraction(factory) , m_wheelResizing(false) , m_externallyResizing(false) , m_selectionInteraction(factory) , m_mouseTrackingHandler(factory) , m_mouseHandler(&m_mouseTrackingHandler) , m_dndHandler(new ThumbnailDND(factory)) , m_pressOnStackIndicator(false) , m_keyboardHandler(new KeyboardEventHandler(factory)) , m_videoThumbnailCycler(new VideoThumbnailCycler(model())) { setModel(ThumbnailComponent::model()); setResizeMode(QListView::Adjust); setViewMode(QListView::IconMode); setUniformItemSizes(true); setSelectionMode(QAbstractItemView::ExtendedSelection); // It beats me why I need to set mouse tracking on both, but without it doesn't work. viewport()->setMouseTracking(true); setMouseTracking(true); connect(selectionModel(), SIGNAL(currentChanged(QModelIndex, QModelIndex)), this, SLOT(scheduleDateChangeSignal())); viewport()->setAcceptDrops(true); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); connect(&m_mouseTrackingHandler, &MouseTrackingInteraction::fileIdUnderCursorChanged, this, &ThumbnailWidget::fileIdUnderCursorChanged); connect(m_keyboardHandler, &KeyboardEventHandler::showSelection, this, &ThumbnailWidget::showSelection); updatePalette(); + connect(Settings::SettingsData::instance(), &Settings::SettingsData::colorSchemeChanged, this, &ThumbnailWidget::updatePalette); setItemDelegate(new Delegate(factory, this)); connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(emitSelectionChangedSignal())); setDragEnabled(false); // We run our own dragging, so disable QListView's version. connect(verticalScrollBar(), SIGNAL(valueChanged(int)), model(), SLOT(updateVisibleRowInfo())); setupDateChangeTimer(); } bool ThumbnailView::ThumbnailWidget::isGridResizing() const { return m_mouseHandler->isResizingGrid() || m_wheelResizing || m_externallyResizing; } void ThumbnailView::ThumbnailWidget::keyPressEvent(QKeyEvent *event) { if (!m_keyboardHandler->keyPressEvent(event)) QListView::keyPressEvent(event); } void ThumbnailView::ThumbnailWidget::keyReleaseEvent(QKeyEvent *event) { const bool propagate = m_keyboardHandler->keyReleaseEvent(event); if (propagate) QListView::keyReleaseEvent(event); } bool ThumbnailView::ThumbnailWidget::isMouseOverStackIndicator(const QPoint &point) { // first check if image is stack, if not return. DB::ImageInfoPtr imageInfo = mediaIdUnderCursor().info(); if (!imageInfo) return false; if (!imageInfo->isStacked()) return false; const QModelIndex index = indexUnderCursor(); const QRect itemRect = visualRect(index); const QPixmap pixmap = index.data(Qt::DecorationRole).value(); if (pixmap.isNull()) return false; const QRect pixmapRect = cellGeometryInfo()->iconGeometry(pixmap).translated(itemRect.topLeft()); const QRect blackOutRect = pixmapRect.adjusted(0, 0, -10, -10); return pixmapRect.contains(point) && !blackOutRect.contains(point); } static bool isMouseResizeGesture(QMouseEvent *event) { return (event->button() & Qt::MidButton) || ((event->modifiers() & Qt::ControlModifier) && (event->modifiers() & Qt::AltModifier)); } void ThumbnailView::ThumbnailWidget::mousePressEvent(QMouseEvent *event) { if ((!(event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier))) && isMouseOverStackIndicator(event->pos())) { model()->toggleStackExpansion(mediaIdUnderCursor()); m_pressOnStackIndicator = true; return; } if (isMouseResizeGesture(event)) m_mouseHandler = &m_gridResizeInteraction; else m_mouseHandler = &m_selectionInteraction; if (!m_mouseHandler->mousePressEvent(event)) QListView::mousePressEvent(event); if (event->button() & Qt::RightButton) //get out of selection mode if this is a right click m_mouseHandler = &m_mouseTrackingHandler; } void ThumbnailView::ThumbnailWidget::mouseMoveEvent(QMouseEvent *event) { if (m_pressOnStackIndicator) return; if (!m_mouseHandler->mouseMoveEvent(event)) QListView::mouseMoveEvent(event); } void ThumbnailView::ThumbnailWidget::mouseReleaseEvent(QMouseEvent *event) { if (m_pressOnStackIndicator) { m_pressOnStackIndicator = false; return; } if (!m_mouseHandler->mouseReleaseEvent(event)) QListView::mouseReleaseEvent(event); m_mouseHandler = &m_mouseTrackingHandler; } void ThumbnailView::ThumbnailWidget::mouseDoubleClickEvent(QMouseEvent *event) { if (isMouseOverStackIndicator(event->pos())) { model()->toggleStackExpansion(mediaIdUnderCursor()); m_pressOnStackIndicator = true; } else if (!(event->modifiers() & Qt::ControlModifier)) { DB::FileName id = mediaIdUnderCursor(); if (!id.isNull()) emit showImage(id); } } void ThumbnailView::ThumbnailWidget::wheelEvent(QWheelEvent *event) { if (event->modifiers() & Qt::ControlModifier) { event->setAccepted(true); if (!m_wheelResizing) m_gridResizeInteraction.enterGridResizingMode(); m_wheelResizing = true; model()->beginResetModel(); const int delta = -event->delta() / 20; static int _minimum_ = Settings::SettingsData::instance()->minimumThumbnailSize(); Settings::SettingsData::instance()->setActualThumbnailSize(qMax(_minimum_, Settings::SettingsData::instance()->actualThumbnailSize() + delta)); cellGeometryInfo()->calculateCellSize(); model()->endResetModel(); } else { int delta = event->delta() / 5; QWheelEvent newevent = QWheelEvent(event->pos(), delta, event->buttons(), nullptr); QListView::wheelEvent(&newevent); } } void ThumbnailView::ThumbnailWidget::emitDateChange() { if (m_isSettingDate) return; int row = currentIndex().row(); if (row == -1) return; DB::FileName fileName = model()->imageAt(row); if (fileName.isNull()) return; static QDateTime lastDate; QDateTime date = fileName.info()->date().start(); if (date != lastDate) { lastDate = date; if (date.date().year() != 1900) emit currentDateChanged(date); } } /** * scroll to the date specified with the parameter date. * The boolean includeRanges tells whether we accept range matches or not. */ void ThumbnailView::ThumbnailWidget::gotoDate(const DB::ImageDate &date, bool includeRanges) { m_isSettingDate = true; DB::FileName candidate = DB::ImageDB::instance() ->findFirstItemInRange(model()->imageList(ViewOrder), date, includeRanges); if (!candidate.isNull()) setCurrentItem(candidate); m_isSettingDate = false; } void ThumbnailView::ThumbnailWidget::setExternallyResizing(bool state) { m_externallyResizing = state; } void ThumbnailView::ThumbnailWidget::reload(SelectionUpdateMethod method) { SelectionMaintainer maintainer(this, model()); ThumbnailComponent::model()->beginResetModel(); cellGeometryInfo()->flushCache(); updatePalette(); ThumbnailComponent::model()->endResetModel(); if (method == ClearSelection) maintainer.disable(); } DB::FileName ThumbnailView::ThumbnailWidget::mediaIdUnderCursor() const { const QModelIndex index = indexUnderCursor(); if (index.isValid()) return model()->imageAt(index.row()); else return DB::FileName(); } QModelIndex ThumbnailView::ThumbnailWidget::indexUnderCursor() const { return indexAt(mapFromGlobal(QCursor::pos())); } void ThumbnailView::ThumbnailWidget::dragMoveEvent(QDragMoveEvent *event) { m_dndHandler->contentsDragMoveEvent(event); } void ThumbnailView::ThumbnailWidget::dragLeaveEvent(QDragLeaveEvent *event) { m_dndHandler->contentsDragLeaveEvent(event); } void ThumbnailView::ThumbnailWidget::dropEvent(QDropEvent *event) { m_dndHandler->contentsDropEvent(event); } void ThumbnailView::ThumbnailWidget::dragEnterEvent(QDragEnterEvent *event) { m_dndHandler->contentsDragEnterEvent(event); } void ThumbnailView::ThumbnailWidget::setCurrentItem(const DB::FileName &fileName) { if (fileName.isNull()) return; const int row = model()->indexOf(fileName); setCurrentIndex(QListView::model()->index(row, 0)); } DB::FileName ThumbnailView::ThumbnailWidget::currentItem() const { if (!currentIndex().isValid()) return DB::FileName(); return model()->imageAt(currentIndex().row()); } void ThumbnailView::ThumbnailWidget::updatePalette() { QPalette pal = palette(); - pal.setBrush(QPalette::Base, QColor(Settings::SettingsData::instance()->backgroundColor())); - pal.setBrush(QPalette::Text, contrastColor(QColor(Settings::SettingsData::instance()->backgroundColor()))); + // if the scheme was set at startup from the scheme path (and not afterwards through KColorSchemeManager), + // then KColorScheme would use the standard system scheme if we don't explicitly give a config: + const auto schemeCfg = KSharedConfig::openConfig(Settings::SettingsData::instance()->colorScheme()); + KColorScheme::adjustBackground(pal, KColorScheme::NormalBackground, QPalette::Base, KColorScheme::Complementary, schemeCfg); + KColorScheme::adjustForeground(pal, KColorScheme::NormalText, QPalette::Text, KColorScheme::Complementary, schemeCfg); setPalette(pal); } int ThumbnailView::ThumbnailWidget::cellWidth() const { return visualRect(QListView::model()->index(0, 0)).size().width(); } void ThumbnailView::ThumbnailWidget::emitSelectionChangedSignal() { emit selectionCountChanged(selection(ExpandCollapsedStacks).size()); } void ThumbnailView::ThumbnailWidget::scheduleDateChangeSignal() { m_dateChangedTimer->start(200); } /** * During profiling, I found that emitting the dateChanged signal was * rather expensive, so now I delay that signal, so it is only emitted 200 * msec after the scroll, which means it will not be emitted when the user * holds down, say the page down key for scrolling. */ void ThumbnailView::ThumbnailWidget::setupDateChangeTimer() { m_dateChangedTimer = new QTimer(this); m_dateChangedTimer->setSingleShot(true); connect(m_dateChangedTimer, &QTimer::timeout, this, &ThumbnailWidget::emitDateChange); } void ThumbnailView::ThumbnailWidget::showEvent(QShowEvent *event) { model()->updateVisibleRowInfo(); QListView::showEvent(event); } DB::FileNameList ThumbnailView::ThumbnailWidget::selection(ThumbnailView::SelectionMode mode) const { DB::FileNameList res; const auto indexSelection = selectedIndexes(); for (const QModelIndex &index : indexSelection) { const DB::FileName currFileName = model()->imageAt(index.row()); bool includeAllStacks = false; switch (mode) { case IncludeAllStacks: includeAllStacks = true; /* FALLTHROUGH */ case ExpandCollapsedStacks: { // if the selected image belongs to a collapsed thread, // imply that all images in the stack are selected: DB::ImageInfoPtr imageInfo = currFileName.info(); if (imageInfo && imageInfo->isStacked() && (includeAllStacks || !model()->isItemInExpandedStack(imageInfo->stackId()))) { // add all images in the same stack res.append(DB::ImageDB::instance()->getStackFor(currFileName)); } else res.append(currFileName); } break; case NoExpandCollapsedStacks: res.append(currFileName); break; } } return res; } bool ThumbnailView::ThumbnailWidget::isSelected(const DB::FileName &fileName) const { return selection(NoExpandCollapsedStacks).indexOf(fileName) != -1; } /** This very specific method will make the item specified by id selected, if there only are one item selected. This is used from the Viewer when you start it without a selection, and are going forward or backward. */ void ThumbnailView::ThumbnailWidget::changeSingleSelection(const DB::FileName &fileName) { if (selection(NoExpandCollapsedStacks).size() == 1) { QItemSelectionModel *selection = selectionModel(); selection->select(model()->fileNameToIndex(fileName), QItemSelectionModel::ClearAndSelect); setCurrentItem(fileName); } } void ThumbnailView::ThumbnailWidget::select(const DB::FileNameList &items) { QItemSelection selection; QModelIndex start; QModelIndex end; int count = 0; for (const DB::FileName &fileName : items) { QModelIndex index = model()->fileNameToIndex(fileName); if (count == 0) { start = index; end = index; } else if (index.row() == end.row() + 1) { end = index; } else { selection.merge(QItemSelection(start, end), QItemSelectionModel::Select); start = index; end = index; } count++; } if (count > 0) { selection.merge(QItemSelection(start, end), QItemSelectionModel::Select); } selectionModel()->select(selection, QItemSelectionModel::Select); } bool ThumbnailView::ThumbnailWidget::isItemUnderCursorSelected() const { return widget()->selection(ExpandCollapsedStacks).contains(mediaIdUnderCursor()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ImageDisplay.cpp b/Viewer/ImageDisplay.cpp index eb3283a5..1d631ba4 100644 --- a/Viewer/ImageDisplay.cpp +++ b/Viewer/ImageDisplay.cpp @@ -1,757 +1,758 @@ /* Copyright (C) 2003-2020 Jesper K. Pedersen 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ImageDisplay.h" #include "Logging.h" #include "ViewHandler.h" #include #include #include #include #include #include #include #include #include #include #include #include #include /** Area displaying the actual image in the viewer. The purpose of this class is to display the actual image in the viewer. This involves controlling zooming and drawing on the images. This class is quite complicated as it had to both be fast and memory efficient. The following are dead end tried: 1) Initially QPainter::setWindow was used for zooming the images, but this had the effect that if you zoom to 100x100 from a 2300x1700 image on a 800x600 display, then Qt would internally create a pixmap with the size (2300/100)*800, (1700/100)*600, which takes up 1.4Gb of memory! 2) I tried doing all scaling and cropping using QPixmap's as that would allow me to keep all transformations on the X Server site (making resizing fast - or I beleived so). Unfortunately it showed up that this was much slower than doing it using QImage, and the result was thus that the looking at a series of images was slow. The process is as follows: - The image loaded from disk is rotated and stored in _loadedImage. Initially this image is as large as the view, until the user starts zooming, at which time the image is reloaded to the size as it is on disk. - Then _loadedImage is cropped and scaled to _croppedAndScaledImg. This image is the size of the display. Resizing the window thus needs to redo step. - Finally in paintEvent _croppedAndScaledImg is drawn to the screen. The above might very likely be simplified. Back in the old days it needed to be that complex to allow drawing on images. To propagate the cache, we need to know which direction the images are viewed in, which is the job of the instance variable _forward. */ Viewer::ImageDisplay::ImageDisplay(QWidget *parent) : AbstractDisplay(parent) , m_reloadImageInProgress(false) , m_forward(true) , m_curIndex(0) , m_busy(false) , m_cursorHiding(true) { m_viewHandler = new ViewHandler(this); setMouseTracking(true); m_cursorTimer = new QTimer(this); m_cursorTimer->setSingleShot(true); connect(m_cursorTimer, &QTimer::timeout, this, &ImageDisplay::hideCursor); showCursor(); } /** * If mouse cursor hiding is enabled, hide the cursor right now */ void Viewer::ImageDisplay::hideCursor() { if (m_cursorHiding) setCursor(Qt::BlankCursor); } /** * If mouse cursor hiding is enabled, show normal cursor and start a timer that will hide it later */ void Viewer::ImageDisplay::showCursor() { if (m_cursorHiding) { unsetCursor(); m_cursorTimer->start(1500); } } /** * Prevent hideCursor() and showCursor() from altering cursor state */ void Viewer::ImageDisplay::disableCursorHiding() { m_cursorHiding = false; } /** * Enable automatic mouse cursor hiding */ void Viewer::ImageDisplay::enableCursorHiding() { m_cursorHiding = true; } void Viewer::ImageDisplay::mousePressEvent(QMouseEvent *event) { // disable cursor hiding till button release disableCursorHiding(); QMouseEvent e(event->type(), mapPos(event->pos()), event->button(), event->buttons(), event->modifiers()); double ratio = sizeRatio(QSize(m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()), size()); bool block = m_viewHandler->mousePressEvent(&e, event->pos(), ratio); if (!block) QWidget::mousePressEvent(event); update(); } void Viewer::ImageDisplay::mouseMoveEvent(QMouseEvent *event) { // just reset the timer showCursor(); QMouseEvent e(event->type(), mapPos(event->pos()), event->button(), event->buttons(), event->modifiers()); double ratio = sizeRatio(QSize(m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()), size()); bool block = m_viewHandler->mouseMoveEvent(&e, event->pos(), ratio); if (!block) QWidget::mouseMoveEvent(event); update(); } void Viewer::ImageDisplay::mouseReleaseEvent(QMouseEvent *event) { // enable cursor hiding and reset timer enableCursorHiding(); showCursor(); m_cache.remove(m_curIndex); QMouseEvent e(event->type(), mapPos(event->pos()), event->button(), event->buttons(), event->modifiers()); double ratio = sizeRatio(QSize(m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()), size()); bool block = m_viewHandler->mouseReleaseEvent(&e, event->pos(), ratio); if (!block) { QWidget::mouseReleaseEvent(event); } emit possibleChange(); update(); } bool Viewer::ImageDisplay::setImage(DB::ImageInfoPtr info, bool forward) { qCDebug(ViewerLog) << "setImage(" << info->fileName().relative() << "," << forward << ")"; m_info = info; m_loadedImage = QImage(); // Find the index of the current image m_curIndex = 0; for (const DB::FileName &filename : qAsConst(m_imageList)) { if (filename == info->fileName()) break; ++m_curIndex; } if (m_cache.contains(m_curIndex) && m_cache[m_curIndex].angle == info->angle()) { const ViewPreloadInfo &found = m_cache[m_curIndex]; m_loadedImage = found.img; updateZoomPoints(Settings::SettingsData::instance()->viewerStandardSize(), found.img.size()); cropAndScale(); info->setSize(found.size); emit imageReady(); } else { requestImage(info, true); busy(); } m_forward = forward; updatePreload(); return true; } void Viewer::ImageDisplay::resizeEvent(QResizeEvent *event) { ImageManager::AsyncLoader::instance()->stop(this, ImageManager::StopOnlyNonPriorityLoads); m_cache.clear(); if (m_info) { cropAndScale(); if (event->size().width() > 1.5 * this->m_loadedImage.size().width() || event->size().height() > 1.5 * this->m_loadedImage.size().height()) potentiallyLoadFullSize(); // Only do if we scale much bigger. } updatePreload(); } void Viewer::ImageDisplay::paintEvent(QPaintEvent *) { int x = (width() - m_croppedAndScaledImg.width()) / 2; int y = (height() - m_croppedAndScaledImg.height()) / 2; QPainter painter(this); + Q_ASSERT(painter.isActive()); painter.fillRect(0, 0, width(), height(), palette().base()); painter.drawImage(x, y, m_croppedAndScaledImg); } QPoint Viewer::ImageDisplay::offset(int logicalWidth, int logicalHeight, int physicalWidth, int physicalHeight, double *ratio) { double rat = sizeRatio(QSize(logicalWidth, logicalHeight), QSize(physicalWidth, physicalHeight)); int ox = (int)(physicalWidth - logicalWidth * rat) / 2; int oy = (int)(physicalHeight - logicalHeight * rat) / 2; if (ratio) *ratio = rat; return QPoint(ox, oy); } void Viewer::ImageDisplay::zoom(QPoint p1, QPoint p2) { qCDebug(ViewerLog, "zoom(%d,%d, %d,%d)", p1.x(), p1.y(), p2.x(), p2.y()); m_cache.remove(m_curIndex); normalize(p1, p2); double ratio; QPoint off = offset((p2 - p1).x(), (p2 - p1).y(), width(), height(), &ratio); off = off / ratio; p1.setX(p1.x() - off.x()); p1.setY(p1.y() - off.y()); p2.setX(p2.x() + off.x()); p2.setY(p2.y() + off.y()); m_zStart = p1; m_zEnd = p2; potentiallyLoadFullSize(); cropAndScale(); } QPoint Viewer::ImageDisplay::mapPos(QPoint p) { QPoint off = offset(qAbs(m_zEnd.x() - m_zStart.x()), qAbs(m_zEnd.y() - m_zStart.y()), width(), height(), 0); p -= off; int x = (int)(m_zStart.x() + (m_zEnd.x() - m_zStart.x()) * ((double)p.x() / (width() - 2 * off.x()))); int y = (int)(m_zStart.y() + (m_zEnd.y() - m_zStart.y()) * ((double)p.y() / (height() - 2 * off.y()))); return QPoint(x, y); } void Viewer::ImageDisplay::xformPainter(QPainter *p) { QPoint off = offset(qAbs(m_zEnd.x() - m_zStart.x()), qAbs(m_zEnd.y() - m_zStart.y()), width(), height(), 0); double s = (width() - 2 * off.x()) / qAbs((double)m_zEnd.x() - m_zStart.x()); p->scale(s, s); p->translate(-m_zStart.x(), -m_zStart.y()); } void Viewer::ImageDisplay::zoomIn() { qCDebug(ViewerLog, "zoomIn()"); QPoint size = (m_zEnd - m_zStart); QPoint p1 = m_zStart + size * (0.2 / 2); QPoint p2 = m_zEnd - size * (0.2 / 2); zoom(p1, p2); } void Viewer::ImageDisplay::zoomOut() { qCDebug(ViewerLog, "zoomOut()"); QPoint size = (m_zEnd - m_zStart); //Bug 150971, Qt tries to render bigger and bigger images (10000x10000), hence running out of memory. if ((size.x() * size.y() > 25 * 1024 * 1024)) return; QPoint p1 = m_zStart - size * (0.25 / 2); QPoint p2 = m_zEnd + size * (0.25 / 2); zoom(p1, p2); } void Viewer::ImageDisplay::zoomFull() { qCDebug(ViewerLog, "zoomFull()"); m_zStart = QPoint(0, 0); m_zEnd = QPoint(m_loadedImage.width(), m_loadedImage.height()); zoom(QPoint(0, 0), QPoint(m_loadedImage.width(), m_loadedImage.height())); } void Viewer::ImageDisplay::normalize(QPoint &p1, QPoint &p2) { int minx = qMin(p1.x(), p2.x()); int miny = qMin(p1.y(), p2.y()); int maxx = qMax(p1.x(), p2.x()); int maxy = qMax(p1.y(), p2.y()); p1 = QPoint(minx, miny); p2 = QPoint(maxx, maxy); } void Viewer::ImageDisplay::pan(const QPoint &point) { m_zStart += point; m_zEnd += point; cropAndScale(); } void Viewer::ImageDisplay::cropAndScale() { if (m_loadedImage.isNull()) { return; } if (m_zStart != QPoint(0, 0) || m_zEnd != QPoint(m_loadedImage.width(), m_loadedImage.height())) { qCDebug(ViewerLog) << "cropAndScale(): using cropped image" << m_zStart << "-" << m_zEnd; m_croppedAndScaledImg = m_loadedImage.copy(m_zStart.x(), m_zStart.y(), m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()); } else { qCDebug(ViewerLog) << "cropAndScale(): using full image."; m_croppedAndScaledImg = m_loadedImage; } updateZoomCaption(); if (!m_croppedAndScaledImg.isNull()) // I don't know how this can happen, but it seems not to be dangerous. { qCDebug(ViewerLog) << "cropAndScale(): scaling image to" << width() << "x" << height(); m_croppedAndScaledImg = m_croppedAndScaledImg.scaled(width(), height(), Qt::KeepAspectRatio, Qt::SmoothTransformation); } else { qCDebug(ViewerLog) << "cropAndScale(): image is null."; } update(); emit viewGeometryChanged(m_croppedAndScaledImg.size(), QRect(m_zStart, m_zEnd), sizeRatio(m_loadedImage.size(), m_info->size())); } void Viewer::ImageDisplay::filterNone() { cropAndScale(); update(); } bool Viewer::ImageDisplay::filterMono() { m_croppedAndScaledImg = m_croppedAndScaledImg.convertToFormat(m_croppedAndScaledImg.Format_Mono); update(); return true; } // I can't believe there isn't a standard conversion for this??? -- WH bool Viewer::ImageDisplay::filterBW() { if (m_croppedAndScaledImg.depth() < 32) { KMessageBox::error(this, i18n("Insufficient color depth for this filter")); return false; } for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) { for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); int gray = qGray(pixel); int alpha = qAlpha(pixel); m_croppedAndScaledImg.setPixel(x, y, qRgba(gray, gray, gray, alpha)); } } update(); return true; } bool Viewer::ImageDisplay::filterContrastStretch() { int redMin, redMax, greenMin, greenMax, blueMin, blueMax; redMin = greenMin = blueMin = 255; redMax = greenMax = blueMax = 0; if (m_croppedAndScaledImg.depth() < 32) { KMessageBox::error(this, i18n("Insufficient color depth for this filter")); return false; } // Look for minimum and maximum intensities within each color channel for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) { for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); int red = qRed(pixel); int green = qGreen(pixel); int blue = qBlue(pixel); redMin = redMin < red ? redMin : red; redMax = redMax > red ? redMax : red; greenMin = greenMin < green ? greenMin : green; greenMax = greenMax > green ? greenMax : green; blueMin = blueMin < blue ? blueMin : blue; blueMax = blueMax > blue ? blueMax : blue; } } // Calculate factor for stretching each color intensity throughout the // whole range float redFactor, greenFactor, blueFactor; redFactor = ((float)(255) / (float)(redMax - redMin)); greenFactor = ((float)(255) / (float)(greenMax - greenMin)); blueFactor = ((float)(255) / (float)(blueMax - blueMin)); // Perform the contrast stretching for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) { for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); int red = qRed(pixel); int green = qGreen(pixel); int blue = qBlue(pixel); int alpha = qAlpha(pixel); red = (red - redMin) * redFactor; red = red < 255 ? red : 255; red = red > 0 ? red : 0; green = (green - greenMin) * greenFactor; green = green < 255 ? green : 255; green = green > 0 ? green : 0; blue = (blue - blueMin) * blueFactor; blue = blue < 255 ? blue : 255; blue = blue > 0 ? blue : 0; m_croppedAndScaledImg.setPixel(x, y, qRgba(red, green, blue, alpha)); } } update(); return true; } bool Viewer::ImageDisplay::filterHistogramEqualization() { int width, height; float R_histogram[256]; float G_histogram[256]; float B_histogram[256]; float d; if (m_croppedAndScaledImg.depth() < 32) { KMessageBox::error(this, i18n("Insufficient color depth for this filter")); return false; } memset(R_histogram, 0, sizeof(R_histogram)); memset(G_histogram, 0, sizeof(G_histogram)); memset(B_histogram, 0, sizeof(B_histogram)); width = m_croppedAndScaledImg.width(); height = m_croppedAndScaledImg.height(); d = 1.0 / width / height; // Populate histogram for each color channel for (int y = 0; y < height; ++y) { for (int x = 1; x < width; ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); R_histogram[qRed(pixel)] += d; G_histogram[qGreen(pixel)] += d; B_histogram[qBlue(pixel)] += d; } } // Transfer histogram table to cumulative distribution table float R_sum = 0.0; float G_sum = 0.0; float B_sum = 0.0; for (int i = 0; i < 256; ++i) { R_sum += R_histogram[i]; G_sum += G_histogram[i]; B_sum += B_histogram[i]; R_histogram[i] = R_sum * 255 + 0.5; G_histogram[i] = G_sum * 255 + 0.5; B_histogram[i] = B_sum * 255 + 0.5; } // Equalize the image for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); m_croppedAndScaledImg.setPixel( x, y, qRgba(R_histogram[qRed(pixel)], G_histogram[qGreen(pixel)], B_histogram[qBlue(pixel)], qAlpha(pixel))); } } update(); return true; } void Viewer::ImageDisplay::updateZoomCaption() { const QSize imgSize = m_loadedImage.size(); // similar to sizeRatio(), but we take the _highest_ factor. double ratio = ((double)imgSize.width()) / (m_zEnd.x() - m_zStart.x()); if (ratio * (m_zEnd.y() - m_zStart.y()) < imgSize.height()) { ratio = ((double)imgSize.height()) / (m_zEnd.y() - m_zStart.y()); } emit setCaptionInfo((ratio > 1.05) ? ki18n("[ zoom x%1 ]").subs(ratio, 0, 'f', 1).toString() : QString()); } QImage Viewer::ImageDisplay::currentViewAsThumbnail() const { if (m_croppedAndScaledImg.isNull()) return QImage(); else return m_croppedAndScaledImg.scaled(512, 512, Qt::KeepAspectRatio, Qt::SmoothTransformation); } bool Viewer::ImageDisplay::isImageZoomed(const Settings::StandardViewSize type, const QSize &imgSize) { if (type == Settings::FullSize) return true; if (type == Settings::NaturalSizeIfFits) return !(imgSize.width() < width() && imgSize.height() < height()); return false; } void Viewer::ImageDisplay::pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) { const DB::FileName fileName = request->databaseFileName(); const QSize imgSize = request->size(); const QSize fullSize = request->fullSize(); const int angle = request->angle(); const bool loadedOK = request->loadedOK(); if (loadedOK && fileName == m_info->fileName()) { if (fullSize.isValid() && !m_info->size().isValid()) m_info->setSize(fullSize); if (!m_reloadImageInProgress) updateZoomPoints(Settings::SettingsData::instance()->viewerStandardSize(), image.size()); else { // See documentation for zoomPixelForPixel for details. // We just loaded a likely much larger image, so the zoom points // need to be scaled. Notice m_loadedImage is the size of the // old image. // when using raw images, the decoded image may be a preview // and have a size different from m_info->size(). Therefore, use fullSize here: double ratio = sizeRatio(m_loadedImage.size(), fullSize); qCDebug(ViewerLog) << "Old size:" << m_loadedImage.size() << "; new size:" << m_info->size(); qCDebug(ViewerLog) << "Req size:" << imgSize << "fullsize:" << fullSize; qCDebug(ViewerLog) << "pixmapLoaded(): Zoom region was" << m_zStart << "-" << m_zEnd; m_zStart *= ratio; m_zEnd *= ratio; qCDebug(ViewerLog) << "pixmapLoaded(): Zoom region changed to" << m_zStart << "-" << m_zEnd; m_reloadImageInProgress = false; } m_loadedImage = image; cropAndScale(); emit imageReady(); } else { if (imgSize != size()) return; // Might be an old preload version, or a loaded version that never made it in time ViewPreloadInfo info(image, fullSize, angle); m_cache.insert(indexOf(fileName), info); updatePreload(); } unbusy(); emit possibleChange(); } void Viewer::ImageDisplay::setImageList(const DB::FileNameList &list) { m_imageList = list; m_cache.clear(); } void Viewer::ImageDisplay::updatePreload() { // cacheSize: number of images at current window dimensions (at 4 byte per pixel) const int cacheSize = (int)((long long)(Settings::SettingsData::instance()->viewerCacheSize() * 1024LL * 1024LL) / (width() * height() * 4)); bool cacheFull = (m_cache.count() > cacheSize); int incr = (m_forward ? 1 : -1); int nextOnesInCache = 0; // Iterate from the current image in the direction of the viewing for (int i = m_curIndex + incr; cacheSize; i += incr) { if (m_forward ? (i >= (int)m_imageList.count()) : (i < 0)) break; DB::ImageInfoPtr info = DB::ImageDB::instance()->info(m_imageList[i]); if (!info) { qCWarning(ViewerLog, "Info was null for index %d!", i); return; } if (m_cache.contains(i)) { nextOnesInCache++; if (nextOnesInCache >= ceil(cacheSize / 2.0) && cacheFull) { // Ok enough images in cache return; } } else { requestImage(info); if (cacheFull) { // The cache was full, we need to delete an item from the cache. // First try to find an item from the direction we came from for (int j = (m_forward ? 0 : m_imageList.count() - 1); j != m_curIndex; j += (m_forward ? 1 : -1)) { if (m_cache.contains(j)) { m_cache.remove(j); return; } } // OK We found no item in the direction we came from (think of home/end keys) for (int j = (m_forward ? m_imageList.count() - 1 : 0); j != m_curIndex; j += (m_forward ? -1 : 1)) { if (m_cache.contains(j)) { m_cache.remove(j); return; } } Q_ASSERT(false); // We should never get here. } return; } } } int Viewer::ImageDisplay::indexOf(const DB::FileName &fileName) { int i = 0; for (const DB::FileName &name : qAsConst(m_imageList)) { if (name == fileName) break; ++i; } return i; } void Viewer::ImageDisplay::busy() { if (!m_busy) qApp->setOverrideCursor(Qt::WaitCursor); m_busy = true; } void Viewer::ImageDisplay::unbusy() { if (m_busy) qApp->restoreOverrideCursor(); m_busy = false; } void Viewer::ImageDisplay::zoomPixelForPixel() { qCDebug(ViewerLog, "zoomPixelForPixel()"); // This is rather tricky. // We want to zoom to a pixel level for the real image, which we might // or might not have loaded yet. // // First we ask for zoom points as they would look like had we had the // real image loaded now. (We need to ask for them, for the real image, // otherwise we would just zoom to the pixel level of the view size // image) updateZoomPoints(Settings::NaturalSize, m_info->size()); // The points now, however might not match the current visible image - // as this image might be be only view size large. We therefore need // to scale the coordinates. double ratio = sizeRatio(m_loadedImage.size(), m_info->size()); qCDebug(ViewerLog) << "zoomPixelForPixel(): Zoom region was" << m_zStart << "-" << m_zEnd; m_zStart /= ratio; m_zEnd /= ratio; qCDebug(ViewerLog) << "zoomPixelForPixel(): Zoom region changed to" << m_zStart << "-" << m_zEnd; cropAndScale(); potentiallyLoadFullSize(); } void Viewer::ImageDisplay::updateZoomPoints(const Settings::StandardViewSize type, const QSize &imgSize) { const int iw = imgSize.width(); const int ih = imgSize.height(); if (isImageZoomed(type, imgSize)) { m_zStart = QPoint(0, 0); m_zEnd = QPoint(iw, ih); qCDebug(ViewerLog) << "updateZoomPoints(): Zoom region reset to" << m_zStart << "-" << m_zEnd; } else { m_zStart = QPoint(-(width() - iw) / 2, -(height() - ih) / 2); m_zEnd = QPoint(iw + (width() - iw) / 2, ih + (height() - ih) / 2); qCDebug(ViewerLog) << "updateZoomPoints(): Zoom region set to" << m_zStart << "-" << m_zEnd; } } void Viewer::ImageDisplay::potentiallyLoadFullSize() { if (m_info->size() != m_loadedImage.size()) { qCDebug(ViewerLog) << "Loading full size image for " << m_info->fileName().relative(); ImageManager::ImageRequest *request = new ImageManager::ImageRequest(m_info->fileName(), QSize(-1, -1), m_info->angle(), this); request->setPriority(ImageManager::Viewer); ImageManager::AsyncLoader::instance()->load(request); busy(); m_reloadImageInProgress = true; } } /** * return the ratio of the two sizes. That is newSize/baseSize. */ double Viewer::ImageDisplay::sizeRatio(const QSize &baseSize, const QSize &newSize) const { double res = ((double)newSize.width()) / baseSize.width(); if (res * baseSize.height() > newSize.height()) { res = ((double)newSize.height()) / baseSize.height(); } return res; } void Viewer::ImageDisplay::requestImage(const DB::ImageInfoPtr &info, bool priority) { Settings::StandardViewSize viewSize = Settings::SettingsData::instance()->viewerStandardSize(); QSize s = size(); if (viewSize == Settings::NaturalSize) s = QSize(-1, -1); ImageManager::ImageRequest *request = new ImageManager::ImageRequest(info->fileName(), s, info->angle(), this); request->setUpScale(viewSize == Settings::FullSize); request->setPriority(priority ? ImageManager::Viewer : ImageManager::ViewerPreload); ImageManager::AsyncLoader::instance()->load(request); } void Viewer::ImageDisplay::hideEvent(QHideEvent *) { m_viewHandler->hideEvent(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ViewerWidget.cpp b/Viewer/ViewerWidget.cpp index a98045f7..7941d469 100644 --- a/Viewer/ViewerWidget.cpp +++ b/Viewer/ViewerWidget.cpp @@ -1,1459 +1,1468 @@ /* Copyright (C) 2003-2020 The KPhotoAlbum Development Team 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ViewerWidget.h" #include "CategoryImageConfig.h" #include "ImageDisplay.h" #include "InfoBox.h" #include "SpeedDisplay.h" #include "TaggedArea.h" #include "TextDisplay.h" #include "VideoDisplay.h" #include "VideoShooter.h" #include "VisibleOptionsMenu.h" #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Viewer::ViewerWidget *Viewer::ViewerWidget::s_latest = nullptr; Viewer::ViewerWidget *Viewer::ViewerWidget::latest() { return s_latest; } // Notice the parent is zero to allow other windows to come on top of it. Viewer::ViewerWidget::ViewerWidget(UsageType type, QMap> *macroStore) : QStackedWidget(nullptr) , m_current(0) , m_popup(nullptr) , m_showingFullScreen(false) , m_forward(true) , m_isRunningSlideShow(false) , m_videoPlayerStoppedManually(false) , m_type(type) , m_currentCategory(DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name()) , m_inputMacros(macroStore) , m_myInputMacros(nullptr) { if (type == ViewerWindow) { setWindowFlags(Qt::Window); setAttribute(Qt::WA_DeleteOnClose); s_latest = this; } - // change the palette for the background - this is an intermediate step until we use a color scheme like gwenview does - QPalette pal = palette(); - pal.setBrush(QPalette::Base, palette().shadow()); - pal.setBrush(QPalette::Text, palette().brightText()); - setPalette(pal); + updatePalette(); + connect(Settings::SettingsData::instance(), &Settings::SettingsData::colorSchemeChanged, this, &ViewerWidget::updatePalette); if (!m_inputMacros) { m_myInputMacros = m_inputMacros = new QMap>; } m_screenSaverCookie = -1; m_currentInputMode = InACategory; m_display = m_imageDisplay = new ImageDisplay(this); addWidget(m_imageDisplay); m_textDisplay = new TextDisplay(this); addWidget(m_textDisplay); createVideoViewer(); connect(m_imageDisplay, &ImageDisplay::possibleChange, this, &ViewerWidget::updateCategoryConfig); connect(m_imageDisplay, &ImageDisplay::imageReady, this, &ViewerWidget::updateInfoBox); connect(m_imageDisplay, &ImageDisplay::setCaptionInfo, this, &ViewerWidget::setCaptionWithDetail); connect(m_imageDisplay, &ImageDisplay::viewGeometryChanged, this, &ViewerWidget::remapAreas); // This must not be added to the layout, as it is standing on top of // the ImageDisplay m_infoBox = new InfoBox(this); m_infoBox->hide(); setupContextMenu(); m_slideShowTimer = new QTimer(this); m_slideShowTimer->setSingleShot(true); m_slideShowPause = Settings::SettingsData::instance()->slideShowInterval() * 1000; connect(m_slideShowTimer, &QTimer::timeout, this, &ViewerWidget::slotSlideShowNextFromTimer); m_speedDisplay = new SpeedDisplay(this); m_speedDisplay->hide(); setFocusPolicy(Qt::StrongFocus); QTimer::singleShot(2000, this, SLOT(test())); connect(DB::ImageDB::instance(), &DB::ImageDB::imagesDeleted, this, &ViewerWidget::slotRemoveDeletedImages); } void Viewer::ViewerWidget::setupContextMenu() { m_popup = new QMenu(this); m_actions = new KActionCollection(this); createSlideShowMenu(); createZoomMenu(); createRotateMenu(); createSkipMenu(); createShowContextMenu(); createInvokeExternalMenu(); createVideoMenu(); createCategoryImageMenu(); createFilterMenu(); QAction *action = m_actions->addAction(QString::fromLatin1("viewer-edit-image-properties"), this, SLOT(editImage())); action->setText(i18nc("@action:inmenu", "Annotate...")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_1); m_popup->addAction(action); m_setStackHead = m_actions->addAction(QString::fromLatin1("viewer-set-stack-head"), this, SLOT(slotSetStackHead())); m_setStackHead->setText(i18nc("@action:inmenu", "Set as First Image in Stack")); m_actions->setDefaultShortcut(m_setStackHead, Qt::CTRL + Qt::Key_4); m_popup->addAction(m_setStackHead); m_showExifViewer = m_actions->addAction(QString::fromLatin1("viewer-show-exif-viewer"), this, SLOT(showExifViewer())); m_showExifViewer->setText(i18nc("@action:inmenu", "Show Exif Viewer")); m_popup->addAction(m_showExifViewer); m_copyTo = m_actions->addAction(QString::fromLatin1("viewer-copy-to"), this, SLOT(copyTo())); m_copyTo->setText(i18nc("@action:inmenu", "Copy Image to...")); m_actions->setDefaultShortcut(m_copyTo, Qt::Key_F7); m_popup->addAction(m_copyTo); if (m_type == ViewerWindow) { action = m_actions->addAction(QString::fromLatin1("viewer-close"), this, SLOT(close())); action->setText(i18nc("@action:inmenu", "Close")); action->setShortcut(Qt::Key_Escape); m_actions->setShortcutsConfigurable(action, false); } m_popup->addAction(action); m_actions->readSettings(); const auto actions = m_actions->actions(); for (QAction *action : actions) { action->setShortcutContext(Qt::WindowShortcut); addAction(action); } } void Viewer::ViewerWidget::createShowContextMenu() { VisibleOptionsMenu *menu = new VisibleOptionsMenu(this, m_actions); connect(menu, &VisibleOptionsMenu::visibleOptionsChanged, this, &ViewerWidget::updateInfoBox); m_popup->addMenu(menu); } void Viewer::ViewerWidget::inhibitScreenSaver(bool inhibit) { QDBusMessage message; if (inhibit) { message = QDBusMessage::createMethodCall(QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("/ScreenSaver"), QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("Inhibit")); message << QString(QString::fromLatin1("KPhotoAlbum")); message << QString(QString::fromLatin1("Giving a slideshow")); QDBusMessage reply = QDBusConnection::sessionBus().call(message); if (reply.type() == QDBusMessage::ReplyMessage) m_screenSaverCookie = reply.arguments().first().toInt(); } else { if (m_screenSaverCookie != -1) { message = QDBusMessage::createMethodCall(QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("/ScreenSaver"), QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("UnInhibit")); message << (uint)m_screenSaverCookie; QDBusConnection::sessionBus().send(message); m_screenSaverCookie = -1; } } } void Viewer::ViewerWidget::createInvokeExternalMenu() { m_externalPopup = new MainWindow::ExternalPopup(m_popup); m_popup->addMenu(m_externalPopup); connect(m_externalPopup, &MainWindow::ExternalPopup::aboutToShow, this, &ViewerWidget::populateExternalPopup); } void Viewer::ViewerWidget::createRotateMenu() { m_rotateMenu = new QMenu(m_popup); m_rotateMenu->setTitle(i18nc("@title:inmenu", "Rotate")); QAction *action = m_actions->addAction(QString::fromLatin1("viewer-rotate90"), this, SLOT(rotate90())); action->setText(i18nc("@action:inmenu", "Rotate clockwise")); action->setShortcut(Qt::Key_9); m_actions->setShortcutsConfigurable(action, false); m_rotateMenu->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-rotate180"), this, SLOT(rotate180())); action->setText(i18nc("@action:inmenu", "Flip Over")); action->setShortcut(Qt::Key_8); m_actions->setShortcutsConfigurable(action, false); m_rotateMenu->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-rotare270"), this, SLOT(rotate270())); // ^ this is a typo, isn't it?! action->setText(i18nc("@action:inmenu", "Rotate counterclockwise")); action->setShortcut(Qt::Key_7); m_actions->setShortcutsConfigurable(action, false); m_rotateMenu->addAction(action); m_popup->addMenu(m_rotateMenu); } void Viewer::ViewerWidget::createSkipMenu() { QMenu *popup = new QMenu(m_popup); popup->setTitle(i18nc("@title:inmenu As in 'skip 2 images'", "Skip")); QAction *action = m_actions->addAction(QString::fromLatin1("viewer-home"), this, SLOT(showFirst())); action->setText(i18nc("@action:inmenu Go to first image", "First")); action->setShortcut(Qt::Key_Home); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-end"), this, SLOT(showLast())); action->setText(i18nc("@action:inmenu Go to last image", "Last")); action->setShortcut(Qt::Key_End); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-next"), this, SLOT(showNext())); action->setText(i18nc("@action:inmenu", "Show Next")); action->setShortcuts(QList() << Qt::Key_PageDown << Qt::Key_Space); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-next-10"), this, SLOT(showNext10())); action->setText(i18nc("@action:inmenu", "Skip 10 Forward")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_PageDown); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-next-100"), this, SLOT(showNext100())); action->setText(i18nc("@action:inmenu", "Skip 100 Forward")); m_actions->setDefaultShortcut(action, Qt::SHIFT + Qt::Key_PageDown); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-next-1000"), this, SLOT(showNext1000())); action->setText(i18nc("@action:inmenu", "Skip 1000 Forward")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_PageDown); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-prev"), this, SLOT(showPrev())); action->setText(i18nc("@action:inmenu", "Show Previous")); action->setShortcuts(QList() << Qt::Key_PageUp << Qt::Key_Backspace); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-prev-10"), this, SLOT(showPrev10())); action->setText(i18nc("@action:inmenu", "Skip 10 Backward")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_PageUp); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-prev-100"), this, SLOT(showPrev100())); action->setText(i18nc("@action:inmenu", "Skip 100 Backward")); m_actions->setDefaultShortcut(action, Qt::SHIFT + Qt::Key_PageUp); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-prev-1000"), this, SLOT(showPrev1000())); action->setText(i18nc("@action:inmenu", "Skip 1000 Backward")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_PageUp); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-delete-current"), this, SLOT(deleteCurrent())); action->setText(i18nc("@action:inmenu", "Delete Image")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Delete); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-remove-current"), this, SLOT(removeCurrent())); action->setText(i18nc("@action:inmenu", "Remove Image from Display List")); action->setShortcut(Qt::Key_Delete); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); m_popup->addMenu(popup); } void Viewer::ViewerWidget::createZoomMenu() { QMenu *popup = new QMenu(m_popup); popup->setTitle(i18nc("@action:inmenu", "Zoom")); // PENDING(blackie) Only for image display? QAction *action = m_actions->addAction(QString::fromLatin1("viewer-zoom-in"), this, SLOT(zoomIn())); action->setText(i18nc("@action:inmenu", "Zoom In")); action->setShortcut(Qt::Key_Plus); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-zoom-out"), this, SLOT(zoomOut())); action->setText(i18nc("@action:inmenu", "Zoom Out")); action->setShortcut(Qt::Key_Minus); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-zoom-full"), this, SLOT(zoomFull())); action->setText(i18nc("@action:inmenu", "Full View")); action->setShortcut(Qt::Key_Period); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-zoom-pixel"), this, SLOT(zoomPixelForPixel())); action->setText(i18nc("@action:inmenu", "Pixel for Pixel View")); action->setShortcut(Qt::Key_Equal); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-toggle-fullscreen"), this, SLOT(toggleFullScreen())); action->setText(i18nc("@action:inmenu", "Toggle Full Screen")); action->setShortcuts(QList() << Qt::Key_F11 << Qt::Key_Return); popup->addAction(action); m_popup->addMenu(popup); } void Viewer::ViewerWidget::createSlideShowMenu() { QMenu *popup = new QMenu(m_popup); popup->setTitle(i18nc("@title:inmenu", "Slideshow")); m_startStopSlideShow = m_actions->addAction(QString::fromLatin1("viewer-start-stop-slideshow"), this, SLOT(slotStartStopSlideShow())); m_startStopSlideShow->setText(i18nc("@action:inmenu", "Run Slideshow")); m_actions->setDefaultShortcut(m_startStopSlideShow, Qt::CTRL + Qt::Key_R); popup->addAction(m_startStopSlideShow); m_slideShowRunFaster = m_actions->addAction(QString::fromLatin1("viewer-run-faster"), this, SLOT(slotSlideShowFaster())); m_slideShowRunFaster->setText(i18nc("@action:inmenu", "Run Faster")); m_actions->setDefaultShortcut(m_slideShowRunFaster, Qt::CTRL + Qt::Key_Plus); // if you change this, please update the info in Viewer::SpeedDisplay popup->addAction(m_slideShowRunFaster); m_slideShowRunSlower = m_actions->addAction(QString::fromLatin1("viewer-run-slower"), this, SLOT(slotSlideShowSlower())); m_slideShowRunSlower->setText(i18nc("@action:inmenu", "Run Slower")); m_actions->setDefaultShortcut(m_slideShowRunSlower, Qt::CTRL + Qt::Key_Minus); // if you change this, please update the info in Viewer::SpeedDisplay popup->addAction(m_slideShowRunSlower); m_popup->addMenu(popup); } void Viewer::ViewerWidget::load(const DB::FileNameList &list, int index) { m_list = list; m_imageDisplay->setImageList(list); m_current = index; load(); bool on = (list.count() > 1); m_startStopSlideShow->setEnabled(on); m_slideShowRunFaster->setEnabled(on); m_slideShowRunSlower->setEnabled(on); } void Viewer::ViewerWidget::load() { const bool isReadable = QFileInfo(m_list[m_current].absolute()).isReadable(); const bool isVideo = isReadable && Utilities::isVideo(m_list[m_current]); if (isReadable) { if (isVideo) { m_display = m_videoDisplay; } else m_display = m_imageDisplay; } else { m_display = m_textDisplay; m_textDisplay->setText(i18n("File not available")); updateInfoBox(); } setCurrentWidget(m_display); m_infoBox->raise(); m_rotateMenu->setEnabled(!isVideo); m_categoryImagePopup->setEnabled(!isVideo); m_filterMenu->setEnabled(!isVideo); m_showExifViewer->setEnabled(!isVideo); if (m_exifViewer) m_exifViewer->setImage(m_list[m_current]); for (QAction *videoAction : qAsConst(m_videoActions)) { videoAction->setVisible(isVideo); } emit soughtTo(m_list[m_current]); bool ok = m_display->setImage(currentInfo(), m_forward); if (!ok) { close(); return; } setCaptionWithDetail(QString()); // PENDING(blackie) This needs to be improved, so that it shows the actions only if there are that many images to jump. for (QList::const_iterator it = m_forwardActions.constBegin(); it != m_forwardActions.constEnd(); ++it) (*it)->setEnabled(m_current + 1 < (int)m_list.count()); for (QList::const_iterator it = m_backwardActions.constBegin(); it != m_backwardActions.constEnd(); ++it) (*it)->setEnabled(m_current > 0); m_setStackHead->setEnabled(currentInfo()->isStacked()); if (isVideo) updateCategoryConfig(); if (m_isRunningSlideShow) m_slideShowTimer->start(m_slideShowPause); if (m_display == m_textDisplay) updateInfoBox(); // Add all tagged areas setTaggedAreasFromImage(); } void Viewer::ViewerWidget::setCaptionWithDetail(const QString &detail) { setWindowTitle(i18nc("@title:window %1 is the filename, %2 its detail info", "%1 %2", m_list[m_current].absolute(), detail)); } void Viewer::ViewerWidget::slotRemoveDeletedImages(const DB::FileNameList &imageList) { for (auto filename : imageList) { m_list.removeAll(filename); } } void Viewer::ViewerWidget::contextMenuEvent(QContextMenuEvent *e) { if (m_videoDisplay) { if (m_videoDisplay->isPaused()) m_playPause->setText(i18nc("@action:inmenu Start video playback", "Play")); else m_playPause->setText(i18nc("@action:inmenu Pause video playback", "Pause")); m_stop->setEnabled(m_videoDisplay->isPlaying()); } m_popup->exec(e->globalPos()); e->setAccepted(true); } void Viewer::ViewerWidget::showNextN(int n) { filterNone(); if (m_display == m_videoDisplay) { m_videoPlayerStoppedManually = true; m_videoDisplay->stop(); } if (m_current + n < (int)m_list.count()) { m_current += n; if (m_current >= (int)m_list.count()) m_current = (int)m_list.count() - 1; m_forward = true; load(); } } void Viewer::ViewerWidget::showNext() { showNextN(1); } void Viewer::ViewerWidget::removeCurrent() { removeOrDeleteCurrent(OnlyRemoveFromViewer); } void Viewer::ViewerWidget::deleteCurrent() { removeOrDeleteCurrent(RemoveImageFromDatabase); } void Viewer::ViewerWidget::removeOrDeleteCurrent(RemoveAction action) { const DB::FileName fileName = m_list[m_current]; if (action == RemoveImageFromDatabase) m_removed.append(fileName); m_list.removeAll(fileName); if (m_list.isEmpty()) close(); if (m_current == m_list.count()) showPrev(); else showNextN(0); } void Viewer::ViewerWidget::showNext10() { showNextN(10); } void Viewer::ViewerWidget::showNext100() { showNextN(100); } void Viewer::ViewerWidget::showNext1000() { showNextN(1000); } void Viewer::ViewerWidget::showPrevN(int n) { if (m_display == m_videoDisplay) m_videoDisplay->stop(); if (m_current > 0) { m_current -= n; if (m_current < 0) m_current = 0; m_forward = false; load(); } } void Viewer::ViewerWidget::showPrev() { showPrevN(1); } void Viewer::ViewerWidget::showPrev10() { showPrevN(10); } void Viewer::ViewerWidget::showPrev100() { showPrevN(100); } void Viewer::ViewerWidget::showPrev1000() { showPrevN(1000); } void Viewer::ViewerWidget::rotate90() { currentInfo()->rotate(90); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[m_current]); } void Viewer::ViewerWidget::rotate180() { currentInfo()->rotate(180); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[m_current]); } void Viewer::ViewerWidget::rotate270() { currentInfo()->rotate(270); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[m_current]); } void Viewer::ViewerWidget::showFirst() { showPrevN(m_list.count()); } void Viewer::ViewerWidget::showLast() { showNextN(m_list.count()); } void Viewer::ViewerWidget::closeEvent(QCloseEvent *event) { if (!m_removed.isEmpty()) { MainWindow::DeleteDialog dialog(this); dialog.exec(m_removed); } m_slideShowTimer->stop(); m_isRunningSlideShow = false; event->accept(); } DB::ImageInfoPtr Viewer::ViewerWidget::currentInfo() const { return m_list[m_current].info(); } +void Viewer::ViewerWidget::updatePalette() +{ + QPalette pal = palette(); + // if the scheme was set at startup from the scheme path (and not afterwards through KColorSchemeManager), + // then KColorScheme would use the standard system scheme if we don't explicitly give a config: + const auto schemeCfg = KSharedConfig::openConfig(Settings::SettingsData::instance()->colorScheme()); + KColorScheme::adjustBackground(pal, KColorScheme::NormalBackground, QPalette::Base, KColorScheme::Complementary, schemeCfg); + KColorScheme::adjustForeground(pal, KColorScheme::NormalText, QPalette::Text, KColorScheme::Complementary, schemeCfg); + setPalette(pal); +} + void Viewer::ViewerWidget::infoBoxMove() { QPoint p = mapFromGlobal(QCursor::pos()); Settings::Position oldPos = Settings::SettingsData::instance()->infoBoxPosition(); Settings::Position pos = oldPos; int x = m_display->mapFromParent(p).x(); int y = m_display->mapFromParent(p).y(); int w = m_display->width(); int h = m_display->height(); if (x < w / 3) { if (y < h / 3) pos = Settings::TopLeft; else if (y > h * 2 / 3) pos = Settings::BottomLeft; else pos = Settings::Left; } else if (x > w * 2 / 3) { if (y < h / 3) pos = Settings::TopRight; else if (y > h * 2 / 3) pos = Settings::BottomRight; else pos = Settings::Right; } else { if (y < h / 3) pos = Settings::Top; else if (y > h * 2 / 3) pos = Settings::Bottom; } if (pos != oldPos) { Settings::SettingsData::instance()->setInfoBoxPosition(pos); updateInfoBox(); } } void Viewer::ViewerWidget::moveInfoBox() { m_infoBox->setSize(); Settings::Position pos = Settings::SettingsData::instance()->infoBoxPosition(); int lx = m_display->pos().x(); int ly = m_display->pos().y(); int lw = m_display->width(); int lh = m_display->height(); int bw = m_infoBox->width(); int bh = m_infoBox->height(); int bx, by; // x-coordinate if (pos == Settings::TopRight || pos == Settings::BottomRight || pos == Settings::Right) bx = lx + lw - 5 - bw; else if (pos == Settings::TopLeft || pos == Settings::BottomLeft || pos == Settings::Left) bx = lx + 5; else bx = lx + lw / 2 - bw / 2; // Y-coordinate if (pos == Settings::TopLeft || pos == Settings::TopRight || pos == Settings::Top) by = ly + 5; else if (pos == Settings::BottomLeft || pos == Settings::BottomRight || pos == Settings::Bottom) by = ly + lh - 5 - bh; else by = ly + lh / 2 - bh / 2; m_infoBox->move(bx, by); } void Viewer::ViewerWidget::resizeEvent(QResizeEvent *e) { moveInfoBox(); QWidget::resizeEvent(e); } void Viewer::ViewerWidget::updateInfoBox() { QString tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name(); if (currentInfo() || !m_currentInput.isEmpty() || (!m_currentCategory.isEmpty() && m_currentCategory != tokensCategory)) { QMap> map; QString text = Utilities::createInfoText(currentInfo(), &map); QString selecttext = QString::fromLatin1(""); if (m_currentCategory.isEmpty()) { selecttext = i18nc("Basically 'enter a category name'", "Setting Category: ") + m_currentInput; if (m_currentInputList.length() > 0) { selecttext += QString::fromLatin1("{") + m_currentInputList + QString::fromLatin1("}"); } } else if ((!m_currentInput.isEmpty() && m_currentCategory != tokensCategory)) { selecttext = i18nc("Basically 'enter a tag name'", "Assigning: ") + m_currentCategory + QString::fromLatin1("/") + m_currentInput; if (m_currentInputList.length() > 0) { selecttext += QString::fromLatin1("{") + m_currentInputList + QString::fromLatin1("}"); } } else if (!m_currentInput.isEmpty() && m_currentCategory == tokensCategory) { m_currentInput = QString::fromLatin1(""); } if (!selecttext.isEmpty()) text = selecttext + QString::fromLatin1("
") + text; if (Settings::SettingsData::instance()->showInfoBox() && !text.isNull() && (m_type != InlineViewer)) { m_infoBox->setInfo(text, map); m_infoBox->show(); } else m_infoBox->hide(); moveInfoBox(); } } Viewer::ViewerWidget::~ViewerWidget() { inhibitScreenSaver(false); if (s_latest == this) s_latest = nullptr; if (m_myInputMacros) delete m_myInputMacros; } void Viewer::ViewerWidget::toggleFullScreen() { setShowFullScreen(!m_showingFullScreen); } void Viewer::ViewerWidget::slotStartStopSlideShow() { bool wasRunningSlideShow = m_isRunningSlideShow; m_isRunningSlideShow = !m_isRunningSlideShow && m_list.count() != 1; if (wasRunningSlideShow) { m_startStopSlideShow->setText(i18nc("@action:inmenu", "Run Slideshow")); m_slideShowTimer->stop(); if (m_list.count() != 1) m_speedDisplay->end(); inhibitScreenSaver(false); } else { m_startStopSlideShow->setText(i18nc("@action:inmenu", "Stop Slideshow")); if (currentInfo()->mediaType() != DB::Video) m_slideShowTimer->start(m_slideShowPause); m_speedDisplay->start(); inhibitScreenSaver(true); } } void Viewer::ViewerWidget::slotSlideShowNextFromTimer() { // Load the next images. QTime timer; timer.start(); if (m_display == m_imageDisplay) slotSlideShowNext(); // ensure that there is a few milliseconds pause, so that an end slideshow keypress // can get through immediately, we don't want it to queue up behind a bunch of timer events, // which loaded a number of new images before the slideshow stops int ms = qMax(200, m_slideShowPause - timer.elapsed()); m_slideShowTimer->start(ms); } void Viewer::ViewerWidget::slotSlideShowNext() { m_forward = true; if (m_current + 1 < (int)m_list.count()) m_current++; else m_current = 0; load(); } void Viewer::ViewerWidget::slotSlideShowFaster() { changeSlideShowInterval(-500); } void Viewer::ViewerWidget::slotSlideShowSlower() { changeSlideShowInterval(+500); } void Viewer::ViewerWidget::changeSlideShowInterval(int delta) { if (m_list.count() == 1) return; m_slideShowPause += delta; m_slideShowPause = qMax(m_slideShowPause, 500); m_speedDisplay->display(m_slideShowPause); if (m_slideShowTimer->isActive()) m_slideShowTimer->start(m_slideShowPause); } void Viewer::ViewerWidget::editImage() { DB::ImageInfoList list; list.append(currentInfo()); MainWindow::Window::configureImages(list, true); } void Viewer::ViewerWidget::filterNone() { if (m_display == m_imageDisplay) { m_imageDisplay->filterNone(); m_filterMono->setChecked(false); m_filterBW->setChecked(false); m_filterContrastStretch->setChecked(false); m_filterHistogramEqualization->setChecked(false); } } void Viewer::ViewerWidget::filterSelected() { // The filters that drop bit depth below 32 should be the last ones // so that filters requiring more bit depth are processed first if (m_display == m_imageDisplay) { m_imageDisplay->filterNone(); if (m_filterBW->isChecked()) m_imageDisplay->filterBW(); if (m_filterContrastStretch->isChecked()) m_imageDisplay->filterContrastStretch(); if (m_filterHistogramEqualization->isChecked()) m_imageDisplay->filterHistogramEqualization(); if (m_filterMono->isChecked()) m_imageDisplay->filterMono(); } } void Viewer::ViewerWidget::filterBW() { if (m_display == m_imageDisplay) { if (m_filterBW->isChecked()) m_filterBW->setChecked(m_imageDisplay->filterBW()); else filterSelected(); } } void Viewer::ViewerWidget::filterContrastStretch() { if (m_display == m_imageDisplay) { if (m_filterContrastStretch->isChecked()) m_filterContrastStretch->setChecked(m_imageDisplay->filterContrastStretch()); else filterSelected(); } } void Viewer::ViewerWidget::filterHistogramEqualization() { if (m_display == m_imageDisplay) { if (m_filterHistogramEqualization->isChecked()) m_filterHistogramEqualization->setChecked(m_imageDisplay->filterHistogramEqualization()); else filterSelected(); } } void Viewer::ViewerWidget::filterMono() { if (m_display == m_imageDisplay) { if (m_filterMono->isChecked()) m_filterMono->setChecked(m_imageDisplay->filterMono()); else filterSelected(); } } void Viewer::ViewerWidget::slotSetStackHead() { MainWindow::Window::theMainWindow()->setStackHead(m_list[m_current]); } bool Viewer::ViewerWidget::showingFullScreen() const { return m_showingFullScreen; } void Viewer::ViewerWidget::setShowFullScreen(bool on) { if (on) { setWindowState(windowState() | Qt::WindowFullScreen); // set moveInfoBox(); } else { // We need to size the image when going out of full screen, in case we started directly in full screen // setWindowState(windowState() & ~Qt::WindowFullScreen); // reset resize(Settings::SettingsData::instance()->viewerSize()); } m_showingFullScreen = on; } void Viewer::ViewerWidget::updateCategoryConfig() { if (!CategoryImageConfig::instance()->isVisible()) return; CategoryImageConfig::instance()->setCurrentImage(m_imageDisplay->currentViewAsThumbnail(), currentInfo()); } void Viewer::ViewerWidget::populateExternalPopup() { m_externalPopup->populate(currentInfo(), m_list); } void Viewer::ViewerWidget::populateCategoryImagePopup() { m_categoryImagePopup->populate(m_imageDisplay->currentViewAsThumbnail(), m_list[m_current]); } void Viewer::ViewerWidget::show(bool slideShow) { QSize size; bool fullScreen; if (slideShow) { fullScreen = Settings::SettingsData::instance()->launchSlideShowFullScreen(); size = Settings::SettingsData::instance()->slideShowSize(); } else { fullScreen = Settings::SettingsData::instance()->launchViewerFullScreen(); size = Settings::SettingsData::instance()->viewerSize(); } if (fullScreen) setShowFullScreen(true); else resize(size); QWidget::show(); if (slideShow != m_isRunningSlideShow) { // The info dialog will show up at the wrong place if we call this function directly // don't ask me why - 4 Sep. 2004 15:13 -- Jesper K. Pedersen QTimer::singleShot(0, this, SLOT(slotStartStopSlideShow())); } } KActionCollection *Viewer::ViewerWidget::actions() { return m_actions; } int Viewer::ViewerWidget::find_tag_in_list(const QStringList &list, QString &namefound) { int found = 0; m_currentInputList = QString::fromLatin1(""); for (QStringList::ConstIterator listIter = list.constBegin(); listIter != list.constEnd(); ++listIter) { if (listIter->startsWith(m_currentInput, Qt::CaseInsensitive)) { found++; if (m_currentInputList.length() > 0) m_currentInputList = m_currentInputList + QString::fromLatin1(","); m_currentInputList = m_currentInputList + listIter->right(listIter->length() - m_currentInput.length()); if (found > 1 && m_currentInputList.length() > 20) { // already found more than we want to display // bail here for now // XXX: non-ideal? display more? certainly config 20 return found; } else { namefound = *listIter; } } } return found; } void Viewer::ViewerWidget::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Backspace) { // remove stuff from the current input string m_currentInput.remove(m_currentInput.length() - 1, 1); updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); m_currentInputList = QString::fromLatin1(""); // } else if (event->modifier & (Qt::AltModifier | Qt::MetaModifier) && // event->key() == Qt::Key_Enter) { return; // we've handled it } else if (event->key() == Qt::Key_Comma) { // force set the "new" token if (!m_currentCategory.isEmpty()) { if (m_currentInput.left(1) == QString::fromLatin1("\"") || // allow a starting ' or " to signal a brand new category // this bypasses the auto-selection of matching characters m_currentInput.left(1) == QString::fromLatin1("\'")) { m_currentInput = m_currentInput.right(m_currentInput.length() - 1); } if (m_currentInput.isEmpty()) return; currentInfo()->addCategoryInfo(m_currentCategory, m_currentInput); DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(m_currentCategory); category->addItem(m_currentInput); } m_currentInput = QString::fromLatin1(""); updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); return; // we've handled it } else if (event->modifiers() == 0 && event->key() >= Qt::Key_0 && event->key() <= Qt::Key_5) { bool ok; short rating = event->text().left(1).toShort(&ok, 10); if (ok) { currentInfo()->setRating(rating * 2); updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); } } else if (event->modifiers() == 0 || event->modifiers() == Qt::ShiftModifier) { // search the category for matches QString namefound; QString incomingKey = event->text().left(1); // start searching for a new category name if (incomingKey == QString::fromLatin1("/")) { if (m_currentInput.isEmpty() && m_currentCategory.isEmpty()) { if (m_currentInputMode == InACategory) { m_currentInputMode = AlwaysStartWithCategory; } else { m_currentInputMode = InACategory; } } else { // reset the category to search through m_currentInput = QString::fromLatin1(""); m_currentCategory = QString::fromLatin1(""); } // use an assigned key or map to a given key for future reference } else if (m_currentInput.isEmpty() && // can map to function keys event->key() >= Qt::Key_F1 && event->key() <= Qt::Key_F35) { // we have a request to assign a macro key or use one Qt::Key key = (Qt::Key)event->key(); if (m_inputMacros->contains(key)) { // Use the requested toggle if (event->modifiers() == Qt::ShiftModifier) { if (currentInfo()->hasCategoryInfo((*m_inputMacros)[key].first, (*m_inputMacros)[key].second)) { currentInfo()->removeCategoryInfo((*m_inputMacros)[key].first, (*m_inputMacros)[key].second); } } else { currentInfo()->addCategoryInfo((*m_inputMacros)[key].first, (*m_inputMacros)[key].second); } } else { (*m_inputMacros)[key] = qMakePair(m_lastCategory, m_lastFound); } updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); // handled it return; } else if (m_currentCategory.isEmpty()) { // still searching for a category to lock to m_currentInput += incomingKey; QStringList categorynames = DB::ImageDB::instance()->categoryCollection()->categoryTexts(); if (find_tag_in_list(categorynames, namefound) == 1) { // yay, we have exactly one! m_currentCategory = namefound; m_currentInput = QString::fromLatin1(""); m_currentInputList = QString::fromLatin1(""); } } else { m_currentInput += incomingKey; DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(m_currentCategory); QStringList items = category->items(); if (find_tag_in_list(items, namefound) == 1) { // yay, we have exactly one! if (currentInfo()->hasCategoryInfo(category->name(), namefound)) currentInfo()->removeCategoryInfo(category->name(), namefound); else currentInfo()->addCategoryInfo(category->name(), namefound); m_lastFound = namefound; m_lastCategory = m_currentCategory; m_currentInput = QString::fromLatin1(""); m_currentInputList = QString::fromLatin1(""); if (m_currentInputMode == AlwaysStartWithCategory) m_currentCategory = QString::fromLatin1(""); } } updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); } QWidget::keyPressEvent(event); return; } void Viewer::ViewerWidget::videoStopped() { if (!m_videoPlayerStoppedManually && m_isRunningSlideShow) slotSlideShowNext(); m_videoPlayerStoppedManually = false; } void Viewer::ViewerWidget::wheelEvent(QWheelEvent *event) { if (event->delta() < 0) { showNext(); } else { showPrev(); } } void Viewer::ViewerWidget::showExifViewer() { m_exifViewer = new Exif::InfoDialog(m_list[m_current], this); m_exifViewer->show(); } void Viewer::ViewerWidget::zoomIn() { m_display->zoomIn(); } void Viewer::ViewerWidget::zoomOut() { m_display->zoomOut(); } void Viewer::ViewerWidget::zoomFull() { m_display->zoomFull(); } void Viewer::ViewerWidget::zoomPixelForPixel() { m_display->zoomPixelForPixel(); } void Viewer::ViewerWidget::makeThumbnailImage() { VideoShooter::go(currentInfo(), this); } struct SeekInfo { SeekInfo(const QString &title, const char *name, int value, const QKeySequence &key) : title(title) , name(name) , value(value) , key(key) { } QString title; const char *name; int value; QKeySequence key; }; void Viewer::ViewerWidget::createVideoMenu() { QMenu *menu = new QMenu(m_popup); menu->setTitle(i18nc("@title:inmenu", "Seek")); m_videoActions.append(m_popup->addMenu(menu)); const QList list = { SeekInfo(i18nc("@action:inmenu", "10 minutes backward"), "seek-10-minute", -600000, QKeySequence(QString::fromLatin1("Ctrl+Left"))), SeekInfo(i18nc("@action:inmenu", "1 minute backward"), "seek-1-minute", -60000, QKeySequence(QString::fromLatin1("Shift+Left"))), SeekInfo(i18nc("@action:inmenu", "10 seconds backward"), "seek-10-second", -10000, QKeySequence(QString::fromLatin1("Left"))), SeekInfo(i18nc("@action:inmenu", "1 seconds backward"), "seek-1-second", -1000, QKeySequence(QString::fromLatin1("Up"))), SeekInfo(i18nc("@action:inmenu", "100 milliseconds backward"), "seek-100-millisecond", -100, QKeySequence(QString::fromLatin1("Shift+Up"))), SeekInfo(i18nc("@action:inmenu", "100 milliseconds forward"), "seek+100-millisecond", 100, QKeySequence(QString::fromLatin1("Shift+Down"))), SeekInfo(i18nc("@action:inmenu", "1 seconds forward"), "seek+1-second", 1000, QKeySequence(QString::fromLatin1("Down"))), SeekInfo(i18nc("@action:inmenu", "10 seconds forward"), "seek+10-second", 10000, QKeySequence(QString::fromLatin1("Right"))), SeekInfo(i18nc("@action:inmenu", "1 minute forward"), "seek+1-minute", 60000, QKeySequence(QString::fromLatin1("Shift+Right"))), SeekInfo(i18nc("@action:inmenu", "10 minutes forward"), "seek+10-minute", 600000, QKeySequence(QString::fromLatin1("Ctrl+Right"))) }; int count = 0; for (const SeekInfo &info : list) { if (count++ == 5) { QAction *sep = new QAction(menu); sep->setSeparator(true); menu->addAction(sep); } QAction *seek = m_actions->addAction(QString::fromLatin1(info.name), m_videoDisplay, SLOT(seek())); seek->setText(info.title); seek->setData(info.value); seek->setShortcut(info.key); m_actions->setShortcutsConfigurable(seek, false); menu->addAction(seek); } QAction *sep = new QAction(m_popup); sep->setSeparator(true); m_popup->addAction(sep); m_videoActions.append(sep); m_stop = m_actions->addAction(QString::fromLatin1("viewer-video-stop"), m_videoDisplay, SLOT(stop())); m_stop->setText(i18nc("@action:inmenu Stop video playback", "Stop")); m_popup->addAction(m_stop); m_videoActions.append(m_stop); m_playPause = m_actions->addAction(QString::fromLatin1("viewer-video-pause"), m_videoDisplay, SLOT(playPause())); // text set in contextMenuEvent() m_playPause->setShortcut(Qt::Key_P); m_actions->setShortcutsConfigurable(m_playPause, false); m_popup->addAction(m_playPause); m_videoActions.append(m_playPause); m_makeThumbnailImage = m_actions->addAction(QString::fromLatin1("make-thumbnail-image"), this, SLOT(makeThumbnailImage())); m_actions->setDefaultShortcut(m_makeThumbnailImage, Qt::ControlModifier + Qt::Key_S); m_makeThumbnailImage->setText(i18nc("@action:inmenu", "Use current frame in thumbnail view")); m_popup->addAction(m_makeThumbnailImage); m_videoActions.append(m_makeThumbnailImage); QAction *restart = m_actions->addAction(QString::fromLatin1("viewer-video-restart"), m_videoDisplay, SLOT(restart())); restart->setText(i18nc("@action:inmenu Restart video playback.", "Restart")); m_popup->addAction(restart); m_videoActions.append(restart); } void Viewer::ViewerWidget::createCategoryImageMenu() { m_categoryImagePopup = new MainWindow::CategoryImagePopup(m_popup); m_popup->addMenu(m_categoryImagePopup); connect(m_categoryImagePopup, &MainWindow::CategoryImagePopup::aboutToShow, this, &ViewerWidget::populateCategoryImagePopup); } void Viewer::ViewerWidget::createFilterMenu() { m_filterMenu = new QMenu(m_popup); m_filterMenu->setTitle(i18nc("@title:inmenu", "Filters")); m_filterNone = m_actions->addAction(QString::fromLatin1("filter-empty"), this, SLOT(filterNone())); m_filterNone->setText(i18nc("@action:inmenu", "Remove All Filters")); m_filterMenu->addAction(m_filterNone); m_filterBW = m_actions->addAction(QString::fromLatin1("filter-bw"), this, SLOT(filterBW())); m_filterBW->setText(i18nc("@action:inmenu", "Apply Grayscale Filter")); m_filterBW->setCheckable(true); m_filterMenu->addAction(m_filterBW); m_filterContrastStretch = m_actions->addAction(QString::fromLatin1("filter-cs"), this, SLOT(filterContrastStretch())); m_filterContrastStretch->setText(i18nc("@action:inmenu", "Apply Contrast Stretching Filter")); m_filterContrastStretch->setCheckable(true); m_filterMenu->addAction(m_filterContrastStretch); m_filterHistogramEqualization = m_actions->addAction(QString::fromLatin1("filter-he"), this, SLOT(filterHistogramEqualization())); m_filterHistogramEqualization->setText(i18nc("@action:inmenu", "Apply Histogram Equalization Filter")); m_filterHistogramEqualization->setCheckable(true); m_filterMenu->addAction(m_filterHistogramEqualization); m_filterMono = m_actions->addAction(QString::fromLatin1("filter-mono"), this, SLOT(filterMono())); m_filterMono->setText(i18nc("@action:inmenu", "Apply Monochrome Filter")); m_filterMono->setCheckable(true); m_filterMenu->addAction(m_filterMono); m_popup->addMenu(m_filterMenu); } void Viewer::ViewerWidget::test() { #ifdef TESTING QTimeLine *timeline = new QTimeLine; timeline->setStartFrame(_infoBox->y()); timeline->setEndFrame(height()); connect(timeline, &QTimeLine::frameChanged, this, &ViewerWidget::moveInfoBox); timeline->start(); #endif // TESTING } void Viewer::ViewerWidget::moveInfoBox(int y) { m_infoBox->move(m_infoBox->x(), y); } void Viewer::ViewerWidget::createVideoViewer() { m_videoDisplay = new VideoDisplay(this); addWidget(m_videoDisplay); connect(m_videoDisplay, &VideoDisplay::stopped, this, &ViewerWidget::videoStopped); } void Viewer::ViewerWidget::stopPlayback() { m_videoDisplay->stop(); } void Viewer::ViewerWidget::invalidateThumbnail() const { ImageManager::ThumbnailCache::instance()->removeThumbnail(m_list[m_current]); } void Viewer::ViewerWidget::setTaggedAreasFromImage() { // Clean all areas we probably already have const auto allAreas = findChildren(); for (TaggedArea *area : allAreas) { area->deleteLater(); } DB::TaggedAreas taggedAreas = currentInfo()->taggedAreas(); addTaggedAreas(taggedAreas, AreaType::Standard); } void Viewer::ViewerWidget::addAdditionalTaggedAreas(DB::TaggedAreas taggedAreas) { addTaggedAreas(taggedAreas, AreaType::Highlighted); } void Viewer::ViewerWidget::addTaggedAreas(DB::TaggedAreas taggedAreas, AreaType type) { DB::TaggedAreasIterator areasInCategory(taggedAreas); QString category; QString tag; while (areasInCategory.hasNext()) { areasInCategory.next(); category = areasInCategory.key(); DB::PositionTagsIterator areaData(areasInCategory.value()); while (areaData.hasNext()) { areaData.next(); tag = areaData.key(); // Add a new frame for the area TaggedArea *newArea = new TaggedArea(this); newArea->setTagInfo(category, category, tag); newArea->setActualGeometry(areaData.value()); newArea->setHighlighted(type == AreaType::Highlighted); newArea->show(); connect(m_infoBox, &InfoBox::tagHovered, newArea, &TaggedArea::checkIsSelected); connect(m_infoBox, &InfoBox::noTagHovered, newArea, &TaggedArea::deselect); } } // Be sure to display the areas, as viewGeometryChanged is not always emitted on load QSize imageSize = currentInfo()->size(); QSize windowSize = this->size(); // On load, the image is never zoomed, so it's a bit easier ;-) double scaleWidth = double(imageSize.width()) / windowSize.width(); double scaleHeight = double(imageSize.height()) / windowSize.height(); int offsetTop = 0; int offsetLeft = 0; if (scaleWidth > scaleHeight) { offsetTop = (windowSize.height() - imageSize.height() / scaleWidth); } else { offsetLeft = (windowSize.width() - imageSize.width() / scaleHeight); } remapAreas( QSize(windowSize.width() - offsetLeft, windowSize.height() - offsetTop), QRect(QPoint(0, 0), QPoint(imageSize.width(), imageSize.height())), 1); } void Viewer::ViewerWidget::remapAreas(QSize viewSize, QRect zoomWindow, double sizeRatio) { QSize currentWindowSize = this->size(); int outerOffsetLeft = (currentWindowSize.width() - viewSize.width()) / 2; int outerOffsetTop = (currentWindowSize.height() - viewSize.height()) / 2; if (sizeRatio != 1) { zoomWindow = QRect( QPoint( double(zoomWindow.left()) * sizeRatio, double(zoomWindow.top()) * sizeRatio), QPoint( double(zoomWindow.left() + zoomWindow.width()) * sizeRatio, double(zoomWindow.top() + zoomWindow.height()) * sizeRatio)); } double scaleHeight = double(viewSize.height()) / zoomWindow.height(); double scaleWidth = double(viewSize.width()) / zoomWindow.width(); int innerOffsetLeft = -zoomWindow.left() * scaleWidth; int innerOffsetTop = -zoomWindow.top() * scaleHeight; const auto areas = findChildren(); for (TaggedArea *area : areas) { const QRect actualGeometry = area->actualGeometry(); QRect screenGeometry; screenGeometry.setWidth(actualGeometry.width() * scaleWidth); screenGeometry.setHeight(actualGeometry.height() * scaleHeight); screenGeometry.moveTo( actualGeometry.left() * scaleWidth + outerOffsetLeft + innerOffsetLeft, actualGeometry.top() * scaleHeight + outerOffsetTop + innerOffsetTop); area->setGeometry(screenGeometry); } } void Viewer::ViewerWidget::copyTo() { QUrl src = QUrl::fromLocalFile(m_list[m_current].absolute()); if (m_lastCopyToTarget.isNull()) { // get directory of src file m_lastCopyToTarget = QFileInfo(src.path()).path(); } QFileDialog dialog(this); dialog.setWindowTitle(i18nc("@title:window", "Copy Image to...")); // use directory of src as start-location: dialog.setDirectory(m_lastCopyToTarget); dialog.selectFile(src.fileName()); dialog.setAcceptMode(QFileDialog::AcceptSave); dialog.setLabelText(QFileDialog::Accept, i18nc("@action:button", "Copy")); if (dialog.exec()) { QUrl dst = dialog.selectedUrls().first(); KIO::CopyJob *job = KIO::copy(src, dst); connect(job, &KIO::CopyJob::finished, job, &QObject::deleteLater); // get directory of dst file m_lastCopyToTarget = QFileInfo(dst.path()).path(); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ViewerWidget.h b/Viewer/ViewerWidget.h index fa6e6eab..70fbbe11 100644 --- a/Viewer/ViewerWidget.h +++ b/Viewer/ViewerWidget.h @@ -1,280 +1,282 @@ /* Copyright (C) 2003-2020 The KPhotoAlbum Development Team 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef VIEWER_H #define VIEWER_H #include #include #include #include #include #include #include class KActionCollection; class QAction; class QContextMenuEvent; class QKeyEvent; class QMenu; class QResizeEvent; class QStackedWidget; class QWheelEvent; namespace DB { class ImageInfo; class Id; } namespace MainWindow { class ExternalPopup; class CategoryImagePopup; } namespace Exif { class InfoDialog; } namespace Viewer { class AbstractDisplay; class ImageDisplay; class InfoBox; class SpeedDisplay; class TextDisplay; class VideoDisplay; class VideoShooter; class ViewerWidget : public QStackedWidget { Q_OBJECT public: enum UsageType { InlineViewer, ViewerWindow }; ViewerWidget(UsageType type = ViewerWindow, QMap> *macroStore = nullptr); ~ViewerWidget() override; static ViewerWidget *latest(); void load(const DB::FileNameList &list, int index = 0); void infoBoxMove(); bool showingFullScreen() const; void setShowFullScreen(bool on); void show(bool slideShow); KActionCollection *actions(); /** * @brief setTaggedAreasFromImage * Clear existing areas and set them based on the currentInfo(). */ void setTaggedAreasFromImage(); /** * @brief addAdditionalTaggedAreas adds additional areas and marks them as highlighted. * @param taggedAreas */ void addAdditionalTaggedAreas(DB::TaggedAreas taggedAreas); public slots: void updateInfoBox(); void test(); void moveInfoBox(int); void stopPlayback(); void remapAreas(QSize viewSize, QRect zoomWindow, double sizeRatio); void copyTo(); signals: void soughtTo(const DB::FileName &id); void imageRotated(const DB::FileName &id); protected: void closeEvent(QCloseEvent *event) override; void contextMenuEvent(QContextMenuEvent *e) override; void resizeEvent(QResizeEvent *) override; void keyPressEvent(QKeyEvent *) override; void wheelEvent(QWheelEvent *event) override; void moveInfoBox(); enum class AreaType { Standard, Highlighted }; /** * @brief addTaggedAreas adds tagged areas to the viewer. * @param taggedAreas Map(category -> Map(tagname, area)) * @param type AreaType::Standard is for areas that are part of the Image; AreaType::Highlight is for additional areas */ void addTaggedAreas(DB::TaggedAreas taggedAreas, AreaType type); void load(); void setupContextMenu(); void createShowContextMenu(); void createInvokeExternalMenu(); void createRotateMenu(); void createSkipMenu(); void createZoomMenu(); void createSlideShowMenu(); void createVideoMenu(); void createCategoryImageMenu(); void createFilterMenu(); void changeSlideShowInterval(int delta); void createVideoViewer(); void inhibitScreenSaver(bool inhibit); DB::ImageInfoPtr currentInfo() const; friend class InfoBox; + void updatePalette(); + private: void showNextN(int); void showPrevN(int); int find_tag_in_list(const QStringList &list, QString &namefound); void invalidateThumbnail() const; enum RemoveAction { RemoveImageFromDatabase, OnlyRemoveFromViewer }; void removeOrDeleteCurrent(RemoveAction); protected slots: void showNext(); void showNext10(); void showNext100(); void showNext1000(); void showPrev(); void showPrev10(); void showPrev100(); void showPrev1000(); void showFirst(); void showLast(); void deleteCurrent(); void removeCurrent(); void rotate90(); void rotate180(); void rotate270(); void toggleFullScreen(); void slotStartStopSlideShow(); void slotSlideShowNext(); void slotSlideShowNextFromTimer(); void slotSlideShowFaster(); void slotSlideShowSlower(); void editImage(); void filterNone(); void filterSelected(); void filterBW(); void filterContrastStretch(); void filterHistogramEqualization(); void filterMono(); void slotSetStackHead(); void updateCategoryConfig(); void populateExternalPopup(); void populateCategoryImagePopup(); void videoStopped(); void showExifViewer(); void zoomIn(); void zoomOut(); void zoomFull(); void zoomPixelForPixel(); void makeThumbnailImage(); /** Set the current window title (filename) and add the given detail */ void setCaptionWithDetail(const QString &detail); /** * @brief slotRemoveDeletedImages removes all deleted images from the viewer playback list. * @param imageList */ void slotRemoveDeletedImages(const DB::FileNameList &imageList); private: static ViewerWidget *s_latest; friend class VideoShooter; QList m_forwardActions; QList m_backwardActions; QAction *m_startStopSlideShow; QAction *m_slideShowRunFaster; QAction *m_slideShowRunSlower; QAction *m_setStackHead; QAction *m_filterNone; QAction *m_filterSelected; QAction *m_filterBW; QAction *m_filterContrastStretch; QAction *m_filterHistogramEqualization; QAction *m_filterMono; AbstractDisplay *m_display; ImageDisplay *m_imageDisplay; VideoDisplay *m_videoDisplay; TextDisplay *m_textDisplay; int m_screenSaverCookie; DB::FileNameList m_list; DB::FileNameList m_removed; int m_current; QRect m_textRect; QMenu *m_popup; QMenu *m_rotateMenu; QMenu *m_filterMenu; MainWindow::ExternalPopup *m_externalPopup; MainWindow::CategoryImagePopup *m_categoryImagePopup; int m_width; int m_height; QPixmap m_pixmap; QAction *m_delete; QAction *m_showExifViewer; QPointer m_exifViewer; QAction *m_copyTo; InfoBox *m_infoBox; QImage m_currentImage; bool m_showingFullScreen; int m_slideShowPause; SpeedDisplay *m_speedDisplay; KActionCollection *m_actions; bool m_forward; QTimer *m_slideShowTimer; bool m_isRunningSlideShow; QList m_videoActions; QAction *m_stop; QAction *m_playPause; QAction *m_makeThumbnailImage; bool m_videoPlayerStoppedManually; UsageType m_type; enum InputMode { InACategory, AlwaysStartWithCategory }; InputMode m_currentInputMode; QString m_currentInput; QString m_currentCategory; QString m_currentInputList; QString m_lastFound; QString m_lastCategory; QMap> *m_inputMacros; QMap> *m_myInputMacros; QString m_lastCopyToTarget; }; } #endif /* VIEWER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: