Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GraphicsView overhaul #699

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/logicalpixelfitter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#include "logicalpixelfitter.h"
#include <QtMath>

LogicalPixelFitter::LogicalPixelFitter(const qreal logicalScale, const QPoint offset)
: logicalScale(logicalScale), offset(offset)
{
}

int LogicalPixelFitter::snapWidth(const qreal value) const
{
return snap(value + offset.x(), logicalScale) - offset.x();
}

int LogicalPixelFitter::snapHeight(const qreal value) const
{
return snap(value + offset.y(), logicalScale) - offset.y();
}

QSize LogicalPixelFitter::snapSize(const QSizeF size) const
{
return QSize(snapWidth(size.width()), snapHeight(size.height()));
}

qreal LogicalPixelFitter::unsnapWidth(const int value) const
{
return unsnap(value + offset.x(), logicalScale) - offset.x();
}

qreal LogicalPixelFitter::unsnapHeight(const int value) const
{
return unsnap(value + offset.y(), logicalScale) - offset.y();
}

QSizeF LogicalPixelFitter::unsnapSize(const QSize size) const
{
return QSizeF(unsnapWidth(size.width()), unsnapHeight(size.height()));
}

int LogicalPixelFitter::snap(const qreal value, const qreal logicalScale)
{
const int valueRoundedDown = qFloor(value);
const int valueRoundedUp = valueRoundedDown + 1;
const int physicalPixelsDrawn = qRound(value * logicalScale);
const int physicalPixelsShownIfRoundingUp = qRound(valueRoundedUp * logicalScale);
return physicalPixelsDrawn >= physicalPixelsShownIfRoundingUp ? valueRoundedUp : valueRoundedDown;
}

qreal LogicalPixelFitter::unsnap(const int value, const qreal logicalScale)
{
// For a given input value, its physical pixels fall within [value-0.5,value+0.5), so
// calculate the first physical pixel of the next value (rounding up if between pixels),
// and the pixel prior to that is the last one within the current value.
const int maxPhysicalPixelForValue = qCeil((value + 0.5) * logicalScale) - 1;
return maxPhysicalPixelForValue / logicalScale;
}
33 changes: 33 additions & 0 deletions src/logicalpixelfitter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#ifndef LOGICALPIXELFITTER_H
#define LOGICALPIXELFITTER_H

#include <QPoint>
#include <QSize>

class LogicalPixelFitter
{
public:
LogicalPixelFitter(const qreal logicalScale, const QPoint offset);

int snapWidth(const qreal value) const;

int snapHeight(const qreal value) const;

QSize snapSize(const QSizeF size) const;

qreal unsnapWidth(const int value) const;

qreal unsnapHeight(const int value) const;

QSizeF unsnapSize(const QSize size) const;

static int snap(const qreal value, const qreal logicalScale);

static qreal unsnap(const int value, const qreal logicalScale);

private:
const qreal logicalScale;
const QPoint offset;
};

#endif // LOGICALPIXELFITTER_H
6 changes: 6 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@

int main(int argc, char *argv[])
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
#endif
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
#endif
QCoreApplication::setOrganizationName("qView");
QCoreApplication::setApplicationName("qView");
QCoreApplication::setApplicationVersion(QString::number(VERSION));
Expand Down
91 changes: 55 additions & 36 deletions src/mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ MainWindow::MainWindow(QWidget *parent) :

// Connect graphicsview signals
connect(graphicsView, &QVGraphicsView::fileChanged, this, &MainWindow::fileChanged);
connect(graphicsView, &QVGraphicsView::updatedLoadedPixmapItem, this, &MainWindow::setWindowSize);
connect(graphicsView, &QVGraphicsView::zoomLevelChanged, this, &MainWindow::zoomLevelChanged);
connect(graphicsView, &QVGraphicsView::cancelSlideshow, this, &MainWindow::cancelSlideshow);

// Initialize escape shortcut
Expand All @@ -80,6 +80,12 @@ MainWindow::MainWindow(QWidget *parent) :
slideshowTimer = new QTimer(this);
connect(slideshowTimer, &QTimer::timeout, this, &MainWindow::slideshowAction);

// Timer for updating titlebar after zoom change
zoomTitlebarUpdateTimer = new QTimer(this);
zoomTitlebarUpdateTimer->setSingleShot(true);
zoomTitlebarUpdateTimer->setInterval(50);
connect(zoomTitlebarUpdateTimer, &QTimer::timeout, this, &MainWindow::buildWindowTitle);

// Context menu
auto &actionManager = qvApp->getActionManager();

Expand Down Expand Up @@ -280,30 +286,24 @@ void MainWindow::paintEvent(QPaintEvent *event)
QPainter painter(this);

const QColor &backgroundColor = customBackgroundColor.isValid() ? customBackgroundColor : painter.background().color();

// Find the top of the viewport to account for the menu bar if it's inside the window
// and/or the label that displays titlebar text in full screen mode.
const int viewportY = graphicsView->mapTo(this, QPoint()).y();
// On macOS, part of the viewport may be additionally covered with the window's translucent
// titlebar due to full size content view.
const int unobscuredViewportY = qMax(getTitlebarOverlap(), viewportY);
const ViewportPosition viewportPos = getViewportPosition();

// Erase the area above the viewport, i.e. fill it with the painter's default color.
const QRect headerRect = QRect(0, 0, width(), viewportY);
const QRect headerRect = QRect(0, 0, width(), viewportPos.widgetY);
if (headerRect.isValid())
{
painter.eraseRect(headerRect);
}

// Fill the viewport with the background color.
const QRect viewportRect = rect().adjusted(0, viewportY, 0, 0);
const QRect viewportRect = rect().adjusted(0, viewportPos.widgetY, 0, 0);
if (viewportRect.isValid())
{
painter.fillRect(viewportRect, backgroundColor);
}

// If there's an error message, draw it centered inside the unobscured area of the viewport.
const QRect unobscuredViewportRect = rect().adjusted(0, unobscuredViewportY, 0, 0);
const QRect unobscuredViewportRect = rect().adjusted(0, viewportPos.widgetY + viewportPos.obscuredHeight, 0, 0);
if (getCurrentFileDetails().errorData.hasError && unobscuredViewportRect.isValid())
{
const QVImageCore::ErrorData &errorData = getCurrentFileDetails().errorData;
Expand Down Expand Up @@ -387,11 +387,18 @@ void MainWindow::fileChanged()
if (info->isVisible())
refreshProperties();
buildWindowTitle();
setWindowSize();

// repaint to handle error message
update();
}

void MainWindow::zoomLevelChanged()
{
if (!zoomTitlebarUpdateTimer->isActive())
zoomTitlebarUpdateTimer->start();
}

void MainWindow::disableActions()
{
const auto &actionLibrary = qvApp->getActionManager().getActionLibrary();
Expand Down Expand Up @@ -496,14 +503,16 @@ void MainWindow::buildWindowTitle()
}
case 2:
{
newString = QString::number(getCurrentFileDetails().loadedIndexInFolder+1);
newString = QString::number(graphicsView->getZoomLevel() * 100.0, 'f', 1) + "%";
newString += " - " + QString::number(getCurrentFileDetails().loadedIndexInFolder+1);
newString += "/" + QString::number(getCurrentFileDetails().folderFileInfoList.count());
newString += " - " + getCurrentFileDetails().fileInfo.fileName();
break;
}
case 3:
{
newString = QString::number(getCurrentFileDetails().loadedIndexInFolder+1);
newString = QString::number(graphicsView->getZoomLevel() * 100.0, 'f', 1) + "%";
newString += " - " + QString::number(getCurrentFileDetails().loadedIndexInFolder+1);
newString += "/" + QString::number(getCurrentFileDetails().folderFileInfoList.count());
newString += " - " + getCurrentFileDetails().fileInfo.fileName();
if (!getCurrentFileDetails().errorData.hasError)
Expand Down Expand Up @@ -552,11 +561,6 @@ void MainWindow::setWindowSize()
qreal minWindowResizedPercentage = qvApp->getSettingsManager().getInteger("minwindowresizedpercentage")/100.0;
qreal maxWindowResizedPercentage = qvApp->getSettingsManager().getInteger("maxwindowresizedpercentage")/100.0;


QSize imageSize = getCurrentFileDetails().loadedPixmapSize;
imageSize -= QSize(4, 4);


// Try to grab the current screen
QScreen *currentScreen = screenContaining(frameGeometry());

Expand All @@ -579,28 +583,29 @@ void MainWindow::setWindowSize()
const QSize hardLimitSize = currentScreen->availableSize() - windowFrameSize - extraWidgetsSize;
const QSize screenSize = currentScreen->size();
const QSize minWindowSize = (screenSize * minWindowResizedPercentage).boundedTo(hardLimitSize);
const QSize maxWindowSize = (screenSize * maxWindowResizedPercentage).boundedTo(hardLimitSize);
const QSize maxWindowSize = (screenSize * qMax(maxWindowResizedPercentage, minWindowResizedPercentage)).boundedTo(hardLimitSize);
const QSizeF imageSize = graphicsView->getEffectiveOriginalSize();
const LogicalPixelFitter fitter = graphicsView->getPixelFitter();
const bool enforceMinSizeBothDimensions = false;

if (imageSize.width() < minWindowSize.width() && imageSize.height() < minWindowSize.height())
{
imageSize.scale(minWindowSize, Qt::KeepAspectRatio);
}
else if (imageSize.width() > maxWindowSize.width() || imageSize.height() > maxWindowSize.height())
QSize targetSize = fitter.snapSize(imageSize);

const bool limitToMin = targetSize.width() < minWindowSize.width() && targetSize.height() < minWindowSize.height();
const bool limitToMax = targetSize.width() > maxWindowSize.width() || targetSize.height() > maxWindowSize.height();
if (limitToMin || limitToMax)
{
imageSize.scale(maxWindowSize, Qt::KeepAspectRatio);
const QSizeF enforcedSize = fitter.unsnapSize(limitToMin ? minWindowSize : maxWindowSize);
const qreal fitRatio = qMin(enforcedSize.width() / imageSize.width(), enforcedSize.height() / imageSize.height());
targetSize = fitter.snapSize(imageSize * fitRatio);
}

// Windows reports the wrong minimum width, so we constrain the image size relative to the dpi to stop weirdness with tiny images
#ifdef Q_OS_WIN
auto minimumImageSize = QSize(qRound(logicalDpiX()*1.5), logicalDpiY()/2);
if (imageSize.boundedTo(minimumImageSize) == imageSize)
imageSize = minimumImageSize;
#endif
Comment on lines -593 to -598
Copy link
Contributor Author

@jdpurcell jdpurcell Dec 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was ancient code from before qView had configurable min/max window sizes; I think the minimum window size setting makes this obsolete in a lot of cases, plus Qt/Windows seems to enforce an absolute minimum anyway (I tested on the 5.15.2 build too) so this may have been working around a Qt bug that's since resolved.

if (enforceMinSizeBothDimensions)
targetSize = targetSize.expandedTo(minWindowSize);

// Match center after new geometry
// This is smoother than a single geometry set for some reason
QRect oldRect = geometry();
resize(imageSize + extraWidgetsSize);
resize(targetSize + extraWidgetsSize);
QRect newRect = geometry();
newRect.moveCenter(oldRect.center());

Expand Down Expand Up @@ -1002,7 +1007,7 @@ void MainWindow::zoomOut()

void MainWindow::resetZoom()
{
graphicsView->resetScale();
graphicsView->zoomToFit();
}

void MainWindow::originalSize()
Expand All @@ -1014,23 +1019,25 @@ void MainWindow::rotateRight()
{
graphicsView->rotateImage(90);
resetZoom();
setWindowSize();
}

void MainWindow::rotateLeft()
{
graphicsView->rotateImage(-90);
resetZoom();
setWindowSize();
}

void MainWindow::mirror()
{
graphicsView->scale(-1, 1);
graphicsView->mirrorImage();
resetZoom();
}

void MainWindow::flip()
{
graphicsView->scale(1, -1);
graphicsView->flipImage();
resetZoom();
}

Expand Down Expand Up @@ -1078,7 +1085,7 @@ void MainWindow::saveFrameAs()
nextFrame();

graphicsView->getLoadedMovie().currentPixmap().save(fileName, nullptr, 100);
graphicsView->resetScale();
graphicsView->zoomToFit();
});
}

Expand Down Expand Up @@ -1201,3 +1208,15 @@ int MainWindow::getTitlebarOverlap() const

return 0;
}

MainWindow::ViewportPosition MainWindow::getViewportPosition() const
{
ViewportPosition result;
// This accounts for anything that may be above the viewport such as the menu bar (if it's inside
// the window) and/or the label that displays titlebar text in full screen mode.
result.widgetY = windowHandle() ? graphicsView->mapTo(this, QPoint()).y() : 0;
// On macOS, part of the viewport may be additionally covered with the window's translucent
// titlebar due to full size content view.
result.obscuredHeight = qMax(getTitlebarOverlap() - result.widgetY, 0);
return result;
}
11 changes: 11 additions & 0 deletions src/mainwindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ class MainWindow : public QMainWindow
QString previousPath;
};

struct ViewportPosition
{
int widgetY;
int obscuredHeight;
};

explicit MainWindow(QWidget *parent = nullptr);
~MainWindow() override;

Expand Down Expand Up @@ -113,6 +119,8 @@ class MainWindow : public QMainWindow

int getTitlebarOverlap() const;

ViewportPosition getViewportPosition() const;

const QVImageCore::FileDetails& getCurrentFileDetails() const { return graphicsView->getCurrentFileDetails(); }

public slots:
Expand All @@ -126,6 +134,8 @@ public slots:

void fileChanged();

void zoomLevelChanged();

void disableActions();

protected:
Expand Down Expand Up @@ -157,6 +167,7 @@ protected slots:
QMenu *virtualMenu;

QTimer *slideshowTimer;
QTimer *zoomTitlebarUpdateTimer;

QShortcut *escShortcut;

Expand Down
Loading