diff --git a/YACReader/YACReader.pro b/YACReader/YACReader.pro index 06ef1432..f2bdd8f0 100644 --- a/YACReader/YACReader.pro +++ b/YACReader/YACReader.pro @@ -62,6 +62,7 @@ HEADERS += ../common/comic.h \ goto_dialog.h \ magnifying_glass.h \ main_window_viewer.h \ + continuous_page_widget.h \ mouse_handler.h \ viewer.h \ options_dialog.h \ @@ -98,6 +99,7 @@ SOURCES += ../common/comic.cpp \ goto_dialog.cpp \ magnifying_glass.cpp \ main_window_viewer.cpp \ + continuous_page_widget.cpp \ mouse_handler.cpp \ viewer.cpp \ options_dialog.cpp \ diff --git a/YACReader/configuration.h b/YACReader/configuration.h index b70594bf..23cc5348 100644 --- a/YACReader/configuration.h +++ b/YACReader/configuration.h @@ -82,6 +82,8 @@ public: void setDoublePage(bool b) { settings->setValue(DOUBLE_PAGE, b); } bool getDoubleMangaPage() { return settings->value(DOUBLE_MANGA_PAGE).toBool(); } void setDoubleMangaPage(bool b) { settings->setValue(DOUBLE_MANGA_PAGE, b); } + bool getContinuousScroll() { return settings->value(CONTINUOUS_SCROLL, false).toBool(); } + void setContinuousScroll(bool b) { settings->setValue(CONTINUOUS_SCROLL, b); } bool getEnlargeImages() { return settings->value(ENLARGE_IMAGES, true).toBool(); } void setEnlargeImages(bool b) { settings->setValue(ENLARGE_IMAGES, b); } diff --git a/YACReader/continuous_page_widget.cpp b/YACReader/continuous_page_widget.cpp new file mode 100644 index 00000000..59a148ae --- /dev/null +++ b/YACReader/continuous_page_widget.cpp @@ -0,0 +1,393 @@ +#include "continuous_page_widget.h" +#include "render.h" + +#include +#include +#include +#include + +ContinuousPageWidget::ContinuousPageWidget(QWidget *parent) + : QWidget(parent) +{ + QSizePolicy sp(QSizePolicy::Preferred, QSizePolicy::Preferred); + sp.setHeightForWidth(true); + setSizePolicy(sp); + setMouseTracking(true); + initTheme(this); +} + +void ContinuousPageWidget::applyTheme(const Theme &) +{ + update(); +} + +void ContinuousPageWidget::setRender(Render *r) +{ + render = r; +} + +void ContinuousPageWidget::setNumPages(int count) +{ + numPages = count; + defaultPageSize = QSize(800, 1200); + pageSizes.fill(QSize(0, 0), count); + relayout(false); +} + +void ContinuousPageWidget::setZoomFactor(int zoom) +{ + if (zoomFactor == zoom) { + return; + } + zoomFactor = zoom; + relayout(true); + update(); +} + +void ContinuousPageWidget::probeBufferedPages() +{ + if (!render || numPages == 0) { + return; + } + + bool changed = false; + for (int i = 0; i < numPages; ++i) { + const QImage *img = render->bufferedImage(i); + bool hasKnownSize = pageSizes[i].width() > 0 && pageSizes[i].height() > 0; + if (img && !img->isNull() && !hasKnownSize) { + pageSizes[i] = img->size(); + if (defaultPageSize == QSize(800, 1200)) { + defaultPageSize = img->size(); + } + changed = true; + } + } + + if (changed) { + relayout(true); + update(); + } +} + +void ContinuousPageWidget::reset() +{ + numPages = 0; + pageSizes.clear(); + yPositions.clear(); + currentTotalHeight = 0; + layoutSnapshot = LayoutSnapshot(); + defaultPageSize = QSize(800, 1200); + setMinimumHeight(0); + setMaximumHeight(QWIDGETSIZE_MAX); + updateGeometry(); + update(); +} + +int ContinuousPageWidget::centerPage(int scrollY, int viewportHeight) const +{ + const int centerY = scrollY + std::max(0, viewportHeight / 2); + return pageAtY(centerY); +} + +int ContinuousPageWidget::yPositionForPage(int pageIndex) const +{ + if (pageIndex < 0 || pageIndex >= yPositions.size()) { + return 0; + } + return yPositions[pageIndex]; +} + +int ContinuousPageWidget::totalHeight() const +{ + return currentTotalHeight; +} + +bool ContinuousPageWidget::hasHeightForWidth() const +{ + return true; +} + +int ContinuousPageWidget::heightForWidth(int w) const +{ + if (numPages == 0 || w <= 0) { + return 0; + } + + int h = 0; + for (int i = 0; i < numPages; ++i) { + QSize scaled = scaledPageSize(i, w); + h += scaled.height(); + } + return h; +} + +QSize ContinuousPageWidget::sizeHint() const +{ + return QSize(defaultPageSize.width(), currentTotalHeight > 0 ? currentTotalHeight : 0); +} + +void ContinuousPageWidget::onPageAvailable(int absolutePageIndex) +{ + if (!render || absolutePageIndex < 0 || absolutePageIndex >= numPages) { + return; + } + + const QImage *img = render->bufferedImage(absolutePageIndex); + if (!img || img->isNull()) { + return; + } + + QSize naturalSize = img->size(); + + // update default page size from the first real page we see + if (defaultPageSize == QSize(800, 1200) && !naturalSize.isNull()) { + defaultPageSize = naturalSize; + } + + bool sizeChanged = (pageSizes[absolutePageIndex] != naturalSize); + pageSizes[absolutePageIndex] = naturalSize; + + if (sizeChanged) { + // keep anchor page visually stable while refined page sizes arrive + relayout(true); + } + + // repaint the region where this page lives + if (absolutePageIndex < yPositions.size()) { + QSize scaled = scaledPageSize(absolutePageIndex, width()); + QRect pageRect(0, yPositions[absolutePageIndex], scaled.width(), scaled.height()); + update(pageRect); + } +} + +void ContinuousPageWidget::paintEvent(QPaintEvent *event) +{ + if (numPages == 0 || !render) { + return; + } + + QPainter painter(this); + + QRect visibleRect = event->rect(); + int firstPage = pageAtY(visibleRect.top()); + int lastPage = pageAtY(visibleRect.bottom()); + + int w = width(); + for (int i = firstPage; i <= lastPage && i < numPages; ++i) { + int y = yPositions[i]; + QSize scaled = scaledPageSize(i, w); + // center horizontally if page is narrower than widget + int x = (w - scaled.width()) / 2; + if (x < 0) { + x = 0; + } + QRect pageRect(x, y, scaled.width(), scaled.height()); + + const QImage *img = render->bufferedImage(i); + if (img && !img->isNull()) { + if (img->size() != scaled) { + painter.drawImage(pageRect, img->scaled(scaled, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + } else { + painter.drawImage(pageRect, *img); + } + } else { + // placeholder + painter.fillRect(pageRect, QColor(45, 45, 45)); + painter.setPen(theme.viewer.defaultTextColor); + painter.drawText(pageRect, Qt::AlignCenter, tr("Loading page %1").arg(i + 1)); + } + } +} + +void ContinuousPageWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + relayout(true); +} + +void ContinuousPageWidget::setAnchorPage(int page) +{ + anchorPage = page; +} + +void ContinuousPageWidget::setViewportState(int scrollY, int viewportHeight) +{ + viewportScrollY = std::max(0, scrollY); + currentViewportHeight = std::max(0, viewportHeight); + hasViewportState = true; +} + +void ContinuousPageWidget::updateLayout() +{ + relayout(false); +} + +void ContinuousPageWidget::updateLayoutWithAnchor() +{ + relayout(true); +} + +void ContinuousPageWidget::relayout(bool preserveAnchor) +{ + int w = width(); + if (w <= 0) { + w = parentWidget() ? parentWidget()->width() : 0; + } + if (w <= 0) { + w = defaultPageSize.width(); + } + + const LayoutSnapshot oldSnapshot = layoutSnapshot; + + ViewportAnchor anchor; + if (preserveAnchor && hasViewportState && !oldSnapshot.yPositions.isEmpty()) { + anchor = anchorFromViewport(oldSnapshot, viewportScrollY, currentViewportHeight); + } else if (preserveAnchor && anchorPage >= 0) { + anchor.pageIndex = anchorPage; + anchor.offsetRatio = 0.5f; + anchor.valid = true; + } + + layoutSnapshot = buildLayoutSnapshot(w); + +#ifndef NDEBUG + Q_ASSERT(layoutSnapshot.yPositions.size() == numPages); + Q_ASSERT(layoutSnapshot.scaledSizes.size() == numPages); + for (int i = 0; i < layoutSnapshot.scaledSizes.size(); ++i) { + Q_ASSERT(layoutSnapshot.scaledSizes[i].width() > 0); + Q_ASSERT(layoutSnapshot.scaledSizes[i].height() > 0); + if (i > 0) { + Q_ASSERT(layoutSnapshot.yPositions[i] >= layoutSnapshot.yPositions[i - 1]); + } + } +#endif + + yPositions = layoutSnapshot.yPositions; + currentTotalHeight = layoutSnapshot.totalHeight; + + setFixedHeight(currentTotalHeight); + updateGeometry(); + + if (!preserveAnchor || !anchor.valid || currentViewportHeight <= 0) { + return; + } + + const int newScrollForAnchor = resolveAnchorToScrollY(layoutSnapshot, anchor, currentViewportHeight); + emit layoutScrollPositionRequested(newScrollForAnchor); +} + +ContinuousPageWidget::LayoutSnapshot ContinuousPageWidget::buildLayoutSnapshot(int w) const +{ + LayoutSnapshot snapshot; + + if (numPages <= 0 || w <= 0) { + return snapshot; + } + + snapshot.yPositions.resize(numPages); + snapshot.scaledSizes.resize(numPages); + + qint64 y = 0; + for (int i = 0; i < numPages; ++i) { + snapshot.yPositions[i] = static_cast(std::min(y, std::numeric_limits::max())); + QSize scaled = scaledPageSize(i, w); + scaled.setWidth(std::max(1, scaled.width())); + scaled.setHeight(std::max(1, scaled.height())); + snapshot.scaledSizes[i] = scaled; + y += scaled.height(); + } + + snapshot.totalHeight = static_cast(std::min(y, static_cast(QWIDGETSIZE_MAX))); + return snapshot; +} + +int ContinuousPageWidget::pageAtY(const LayoutSnapshot &snapshot, int y) const +{ + if (snapshot.yPositions.isEmpty()) { + return 0; + } + + auto it = std::upper_bound(snapshot.yPositions.constBegin(), snapshot.yPositions.constEnd(), y); + if (it == snapshot.yPositions.constBegin()) { + return 0; + } + --it; + return static_cast(it - snapshot.yPositions.constBegin()); +} + +ContinuousPageWidget::ViewportAnchor ContinuousPageWidget::anchorFromViewport(const LayoutSnapshot &snapshot, int scrollY, int viewportHeight) const +{ + ViewportAnchor anchor; + + if (snapshot.yPositions.isEmpty() || viewportHeight <= 0) { + return anchor; + } + + const int maxScroll = std::max(0, snapshot.totalHeight - viewportHeight); + const int clampedScroll = qBound(0, scrollY, maxScroll); + const int anchorY = clampedScroll + viewportHeight / 2; + const int page = pageAtY(snapshot, anchorY); + + if (page < 0 || page >= snapshot.scaledSizes.size()) { + return anchor; + } + + const int pageTop = snapshot.yPositions[page]; + const int pageHeight = std::max(1, snapshot.scaledSizes[page].height()); + const float ratio = static_cast(anchorY - pageTop) / static_cast(pageHeight); + + anchor.pageIndex = page; + anchor.offsetRatio = qBound(0.0f, ratio, 1.0f); + anchor.valid = true; + return anchor; +} + +int ContinuousPageWidget::resolveAnchorToScrollY(const LayoutSnapshot &snapshot, const ViewportAnchor &anchor, int viewportHeight) const +{ + if (!anchor.valid || viewportHeight <= 0 || snapshot.yPositions.isEmpty()) { + return 0; + } + + if (anchor.pageIndex < 0 || anchor.pageIndex >= snapshot.yPositions.size() || anchor.pageIndex >= snapshot.scaledSizes.size()) { + return 0; + } + + const int pageTop = snapshot.yPositions[anchor.pageIndex]; + const int pageHeight = std::max(1, snapshot.scaledSizes[anchor.pageIndex].height()); + const int anchorY = pageTop + qRound(anchor.offsetRatio * pageHeight); + const int maxScroll = std::max(0, snapshot.totalHeight - viewportHeight); + const int target = anchorY - viewportHeight / 2; + return qBound(0, target, maxScroll); +} + +int ContinuousPageWidget::pageAtY(int y) const +{ + return pageAtY(layoutSnapshot, y); +} + +QSize ContinuousPageWidget::scaledPageSize(int pageIndex, int forWidth) const +{ + QSize natural = (pageIndex < pageSizes.size() && pageSizes[pageIndex].width() > 0 && pageSizes[pageIndex].height() > 0) + ? pageSizes[pageIndex] + : defaultPageSize; + + float scale = scaleForPage(pageIndex, forWidth); + int scaledW = std::max(1, qRound(natural.width() * scale)); + int scaledH = std::max(1, qRound(natural.height() * scale)); + return QSize(scaledW, scaledH); +} + +float ContinuousPageWidget::scaleForPage(int pageIndex, int forWidth) const +{ + QSize natural = (pageIndex < pageSizes.size() && pageSizes[pageIndex].width() > 0 && pageSizes[pageIndex].height() > 0) + ? pageSizes[pageIndex] + : defaultPageSize; + + if (natural.width() <= 0 || forWidth <= 0) { + return 1.0f; + } + + float baseScale = static_cast(forWidth) / natural.width(); + float zoomMultiplier = zoomFactor / 100.0f; + return baseScale * zoomMultiplier; +} diff --git a/YACReader/continuous_page_widget.h b/YACReader/continuous_page_widget.h new file mode 100644 index 00000000..55393a5b --- /dev/null +++ b/YACReader/continuous_page_widget.h @@ -0,0 +1,86 @@ +#ifndef CONTINUOUS_PAGE_WIDGET_H +#define CONTINUOUS_PAGE_WIDGET_H + +#include +#include +#include + +#include "themable.h" + +class Render; + +class ContinuousPageWidget : public QWidget, protected Themable +{ + Q_OBJECT +public: + explicit ContinuousPageWidget(QWidget *parent = nullptr); + + void setRender(Render *r); + void setNumPages(int count); + void setZoomFactor(int zoom); + void probeBufferedPages(); + void reset(); + + int centerPage(int scrollY, int viewportHeight) const; + int yPositionForPage(int pageIndex) const; + int totalHeight() const; + + bool hasHeightForWidth() const override; + int heightForWidth(int w) const override; + QSize sizeHint() const override; + + void setAnchorPage(int page); + void setViewportState(int scrollY, int viewportHeight); + +signals: + // emitted after layout recomputation when the preserved viewport anchor + // resolves to an absolute scroll position + void layoutScrollPositionRequested(int scrollY); + +public slots: + void onPageAvailable(int absolutePageIndex); + +protected: + void paintEvent(QPaintEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void applyTheme(const Theme &theme) override; + +private: + struct LayoutSnapshot { + QVector yPositions; + QVector scaledSizes; + int totalHeight = 0; + }; + + struct ViewportAnchor { + int pageIndex = -1; + float offsetRatio = 0.0f; + bool valid = false; + }; + + void updateLayout(); + void updateLayoutWithAnchor(); + void relayout(bool preserveAnchor); + LayoutSnapshot buildLayoutSnapshot(int w) const; + int pageAtY(const LayoutSnapshot &snapshot, int y) const; + ViewportAnchor anchorFromViewport(const LayoutSnapshot &snapshot, int scrollY, int viewportHeight) const; + int resolveAnchorToScrollY(const LayoutSnapshot &snapshot, const ViewportAnchor &anchor, int viewportHeight) const; + int pageAtY(int y) const; + QSize scaledPageSize(int pageIndex, int forWidth) const; + float scaleForPage(int pageIndex, int forWidth) const; + + Render *render = nullptr; + int numPages = 0; + QVector pageSizes; + QVector yPositions; + int currentTotalHeight = 0; + LayoutSnapshot layoutSnapshot; + QSize defaultPageSize { 800, 1200 }; + int zoomFactor = 100; + int anchorPage = -1; + int viewportScrollY = 0; + int currentViewportHeight = 0; + bool hasViewportState = false; +}; + +#endif // CONTINUOUS_PAGE_WIDGET_H diff --git a/YACReader/main.cpp b/YACReader/main.cpp index 813674cf..e71f6791 100644 --- a/YACReader/main.cpp +++ b/YACReader/main.cpp @@ -144,7 +144,7 @@ int main(int argc, char *argv[]) QDir().mkpath(YACReader::getSettingsPath()); Logger &logger = Logger::instance(); - logger.setLoggingLevel(QsLogging::InfoLevel); + logger.setLoggingLevel(QsLogging::TraceLevel); if (parser.isSet("loglevel")) { if (parser.value("loglevel") == "trace") { diff --git a/YACReader/main_window_viewer.cpp b/YACReader/main_window_viewer.cpp index 2018b25f..449c198f 100644 --- a/YACReader/main_window_viewer.cpp +++ b/YACReader/main_window_viewer.cpp @@ -77,6 +77,7 @@ MainWindowViewer::~MainWindowViewer() delete rightRotationAction; delete doublePageAction; delete doubleMangaPageAction; + delete continuousScrollAction; delete increasePageZoomAction; delete decreasePageZoomAction; delete resetZoomAction; @@ -309,28 +310,38 @@ void MainWindowViewer::createActions() fitToPageAction->setCheckable(true); connect(fitToPageAction, &QAction::triggered, this, &MainWindowViewer::fitToPageSwitch); + continuousScrollAction = new QAction(tr("Continuous scroll"), this); + continuousScrollAction->setToolTip(tr("Switch to continuous scroll mode")); + continuousScrollAction->setIcon(QIcon(":/images/viewer_toolbar/toContinuousScroll.svg")); + continuousScrollAction->setCheckable(true); + continuousScrollAction->setChecked(Configuration::getConfiguration().getContinuousScroll()); + connect(continuousScrollAction, &QAction::toggled, viewer, &Viewer::setContinuousScroll); + // fit modes have to be exclusive and checkable auto fitModes = new QActionGroup(this); fitModes->addAction(adjustHeightAction); fitModes->addAction(adjustWidthAction); fitModes->addAction(adjustToFullSizeAction); fitModes->addAction(fitToPageAction); + fitModes->addAction(continuousScrollAction); - switch (Configuration::getConfiguration().getFitMode()) { - case YACReader::FitMode::ToWidth: - adjustWidthAction->setChecked(true); - break; - case YACReader::FitMode::ToHeight: - adjustHeightAction->setChecked(true); - break; - case YACReader::FitMode::FullRes: - adjustToFullSizeAction->setChecked(true); - break; - case YACReader::FitMode::FullPage: - fitToPageAction->setChecked(true); - break; - default: - fitToPageAction->setChecked(true); + if (Configuration::getConfiguration().getContinuousScroll()) { + continuousScrollAction->setChecked(true); + } else { + switch (Configuration::getConfiguration().getFitMode()) { + case YACReader::FitMode::ToWidth: + adjustWidthAction->setChecked(true); + break; + case YACReader::FitMode::ToHeight: + adjustHeightAction->setChecked(true); + break; + case YACReader::FitMode::FullRes: + adjustToFullSizeAction->setChecked(true); + break; + case YACReader::FitMode::FullPage: + default: + fitToPageAction->setChecked(true); + } } resetZoomAction = new QAction(tr("Reset zoom"), this); @@ -536,11 +547,14 @@ void MainWindowViewer::createToolBars() auto fitToPageTBAction = actionWithCustomIcon(QIcon(addExtensionToIconPathInToolbar(":/images/viewer_toolbar/fitToPage")), fitToPageAction); comicToolBar->addAction(fitToPageTBAction); + auto continuousScroollTBAction = actionWithCustomIcon(QIcon(addExtensionToIconPathInToolbar(":/images/viewer_toolbar/toContinuousScroll")), continuousScrollAction); + auto fitModes = new QActionGroup(this); fitModes->addAction(adjustToWidthTBAction); fitModes->addAction(adjustToHeightTBAction); fitModes->addAction(adjustToFullSizeTBAction); fitModes->addAction(fitToPageTBAction); + fitModes->addAction(continuousScroollTBAction); zoomSliderAction = new YACReaderSlider(this); zoomSliderAction->hide(); @@ -555,6 +569,7 @@ void MainWindowViewer::createToolBars() comicToolBar->addAction(actionWithCustomIcon(QIcon(addExtensionToIconPathInToolbar(":/images/viewer_toolbar/rotateR")), rightRotationAction)); comicToolBar->addAction(actionWithCustomIcon(QIcon(addExtensionToIconPathInToolbar(":/images/viewer_toolbar/doublePage")), doublePageAction)); comicToolBar->addAction(actionWithCustomIcon(QIcon(addExtensionToIconPathInToolbar(":/images/viewer_toolbar/doubleMangaPage")), doubleMangaPageAction)); + comicToolBar->addAction(continuousScroollTBAction); comicToolBar->addSeparator(); @@ -603,6 +618,7 @@ void MainWindowViewer::createToolBars() viewer->addAction(rightRotationAction); viewer->addAction(doublePageAction); viewer->addAction(doubleMangaPageAction); + viewer->addAction(continuousScrollAction); YACReader::addSperator(viewer); viewer->addAction(showMagnifyingGlassAction); @@ -675,6 +691,7 @@ void MainWindowViewer::createToolBars() viewMenu->addSeparator(); viewMenu->addAction(doublePageAction); viewMenu->addAction(doubleMangaPageAction); + viewMenu->addAction(continuousScrollAction); viewMenu->addSeparator(); viewMenu->addAction(showMagnifyingGlassAction); @@ -1247,6 +1264,7 @@ void MainWindowViewer::setUpShortcutsManagement() << rightRotationAction << doublePageAction << doubleMangaPageAction + << continuousScrollAction << adjustToFullSizeAction << fitToPageAction << increasePageZoomAction @@ -1540,6 +1558,7 @@ void MainWindowViewer::setActionsEnabled(bool enabled) showMagnifyingGlassAction, doublePageAction, doubleMangaPageAction, + continuousScrollAction, adjustToFullSizeAction, fitToPageAction, showZoomSliderlAction, @@ -1613,6 +1632,7 @@ void MainWindowViewer::applyTheme(const Theme &theme) setIcon(showDictionaryAction, toolbarTheme.showDictionaryAction, toolbarTheme.showDictionaryAction18x18); setIcon(adjustToFullSizeAction, toolbarTheme.adjustToFullSizeAction, toolbarTheme.adjustToFullSizeAction18x18); setIcon(fitToPageAction, toolbarTheme.fitToPageAction, toolbarTheme.fitToPageAction18x18); + setIcon(continuousScrollAction, toolbarTheme.continuousScrollAction, toolbarTheme.continuousScrollAction18x18); setIcon(showFlowAction, toolbarTheme.showFlowAction, toolbarTheme.showFlowAction18x18); } diff --git a/YACReader/main_window_viewer.h b/YACReader/main_window_viewer.h index f480ef28..561c54c8 100644 --- a/YACReader/main_window_viewer.h +++ b/YACReader/main_window_viewer.h @@ -140,6 +140,7 @@ private: QAction *closeAction; QAction *doublePageAction; QAction *doubleMangaPageAction; + QAction *continuousScrollAction; QAction *showShorcutsAction; QAction *showDictionaryAction; QAction *adjustToFullSizeAction; diff --git a/YACReader/render.cpp b/YACReader/render.cpp index fee12b11..595df27c 100644 --- a/YACReader/render.cpp +++ b/YACReader/render.cpp @@ -546,6 +546,17 @@ QPixmap *Render::getCurrentDoubleMangaPage() } } +const QImage *Render::bufferedImage(int absolutePageIndex) const +{ + int offset = absolutePageIndex - currentIndex; + int pos = currentPageBufferedIndex + offset; + if (pos < 0 || pos >= buffer.size()) { + return nullptr; + } + const QImage *img = buffer[pos]; + return (img && !img->isNull()) ? img : nullptr; +} + bool Render::currentPageIsDoublePage() { if (currentIndex == 0 && Configuration::getConfiguration().getSettings()->value(COVER_IS_SP, true).toBool()) { @@ -627,6 +638,8 @@ void Render::setComic(Comic *c) void Render::prepareAvailablePage(int page) { + emit pageRendered(page); + if (!doublePage) { if (currentIndex == page) { emit currentPageReady(); diff --git a/YACReader/render.h b/YACReader/render.h index bfeb81af..25ad7166 100644 --- a/YACReader/render.h +++ b/YACReader/render.h @@ -142,6 +142,7 @@ public slots: bool currentPageIsDoublePage(); bool nextPageIsDoublePage(); bool previousPageIsDoublePage(); + const QImage *bufferedImage(int absolutePageIndex) const; void goTo(int index); void doublePageSwitch(); void setManga(bool manga); @@ -197,6 +198,7 @@ signals: void isLast(); void isCover(); + void pageRendered(int absolutePageIndex); void bookmarksUpdated(); private: diff --git a/YACReader/themes/theme.h b/YACReader/themes/theme.h index 50994209..2cc5e8cf 100644 --- a/YACReader/themes/theme.h +++ b/YACReader/themes/theme.h @@ -102,6 +102,8 @@ struct ToolbarTheme { QIcon adjustToFullSizeAction18x18; QIcon fitToPageAction; QIcon fitToPageAction18x18; + QIcon continuousScrollAction; + QIcon continuousScrollAction18x18; QIcon showFlowAction; QIcon showFlowAction18x18; }; diff --git a/YACReader/themes/theme_factory.cpp b/YACReader/themes/theme_factory.cpp index 7fd7d1f7..d798a5a4 100644 --- a/YACReader/themes/theme_factory.cpp +++ b/YACReader/themes/theme_factory.cpp @@ -139,6 +139,7 @@ Theme makeTheme(const ThemeParams ¶ms) setToolbarIconPairT(theme.toolbar.showDictionaryAction, theme.toolbar.showDictionaryAction18x18, ":/images/viewer_toolbar/translator.svg"); setToolbarIconPairT(theme.toolbar.adjustToFullSizeAction, theme.toolbar.adjustToFullSizeAction18x18, ":/images/viewer_toolbar/full.svg"); setToolbarIconPairT(theme.toolbar.fitToPageAction, theme.toolbar.fitToPageAction18x18, ":/images/viewer_toolbar/fitToPage.svg"); + setToolbarIconPairT(theme.toolbar.continuousScrollAction, theme.toolbar.continuousScrollAction18x18, ":/images/viewer_toolbar/toContinuousScroll.svg"); setToolbarIconPairT(theme.toolbar.showFlowAction, theme.toolbar.showFlowAction18x18, ":/images/viewer_toolbar/flow.svg"); // end Toolbar & actions diff --git a/YACReader/viewer.cpp b/YACReader/viewer.cpp index 7187f77b..32c9911b 100644 --- a/YACReader/viewer.cpp +++ b/YACReader/viewer.cpp @@ -1,4 +1,5 @@ #include "viewer.h" +#include "continuous_page_widget.h" #include "configuration.h" #include "magnifying_glass.h" #include "goto_flow_widget.h" @@ -23,6 +24,7 @@ Viewer::Viewer(QWidget *parent) information(false), doublePage(false), doubleMangaPage(false), + continuousScroll(false), zoom(100), currentPage(nullptr), wheelStop(false), @@ -40,15 +42,30 @@ Viewer::Viewer(QWidget *parent) translatorAnimation->setDuration(150); translatorXPos = -10000; translator->move(-translator->width(), 10); - // current comic page + // current comic page (used in non-continuous mode when a comic is open) content = new QLabel(this); - configureContent(tr("Press 'O' to open comic.")); + content->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + if (!(devicePixelRatioF() > 1)) + content->setScaledContents(true); + content->setAlignment(Qt::AlignTop | Qt::AlignHCenter); + content->setMouseTracking(true); - setWidget(content); + // dedicated widget for status messages ("Press 'O' to open comic.", "Loading...", etc.) + messageLabel = new QLabel(this); + messageLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + messageLabel->setAlignment(Qt::AlignTop | Qt::AlignHCenter); + messageLabel->setText(tr("Press 'O' to open comic.")); + messageLabel->setFont(QFont("courier new", 12)); + messageLabel->setMouseTracking(true); + + setWidget(messageLabel); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setFrameStyle(QFrame::NoFrame); setAlignment(Qt::AlignCenter); + + continuousWidget = new ContinuousPageWidget(); + continuousWidget->installEventFilter(this); //--------------------------------------- mglass = new MagnifyingGlass( Configuration::getConfiguration().getMagnifyingGlassSize(), @@ -63,7 +80,6 @@ Viewer::Viewer(QWidget *parent) }); mglass->hide(); - content->setMouseTracking(true); setMouseTracking(true); showCursor(); @@ -81,6 +97,7 @@ Viewer::Viewer(QWidget *parent) bd = new BookmarksDialog(this->parentWidget()); render = new Render(); + continuousWidget->setRender(render); hideCursorTimer = new QTimer(); hideCursorTimer->setSingleShot(true); @@ -91,6 +108,9 @@ Viewer::Viewer(QWidget *parent) if (Configuration::getConfiguration().getDoubleMangaPage()) doubleMangaPageSwitch(); + if (Configuration::getConfiguration().getContinuousScroll()) + setContinuousScroll(true); + createConnections(); hideCursorTimer->start(2500); @@ -122,7 +142,17 @@ Viewer::~Viewer() delete goToFlow; delete translator; delete translatorAnimation; - delete content; + // messageLabel, content or continuousWidget may not be owned by the scroll area + // (after takeWidget), so delete whichever ones are not currently set + if (widget() != messageLabel) { + delete messageLabel; + } + if (widget() != content) { + delete content; + } + if (widget() != continuousWidget) { + delete continuousWidget; + } delete hideCursorTimer; delete informationLabel; delete verticalScroller; @@ -173,9 +203,14 @@ void Viewer::createConnections() connect(render, qOverload(&Render::numPages), this, &Viewer::comicLoaded); connect(render, QOverload::of(&Render::imageLoaded), goToFlow, &GoToFlowWidget::setImageReady); connect(render, &Render::currentPageReady, this, &Viewer::updatePage); + connect(render, &Render::pageRendered, continuousWidget, &ContinuousPageWidget::onPageAvailable); + connect(continuousWidget, &ContinuousPageWidget::layoutScrollPositionRequested, this, &Viewer::onContinuousLayoutScrollRequested); + connect(render, qOverload(&Render::numPages), this, &Viewer::onNumPagesReady); + connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &Viewer::onContinuousScroll); connect(render, &Render::processingPage, this, &Viewer::setLoadingMessage); connect(render, &Render::currentPageIsBookmark, this, &Viewer::pageIsBookmark); connect(render, &Render::pageChanged, this, &Viewer::updateInformation); + connect(render, &Render::pageChanged, this, &Viewer::onRenderPageChanged); connect(render, &Render::isLast, this, &Viewer::showIsLastMessage); connect(render, &Render::isCover, this, &Viewer::showIsCoverMessage); @@ -304,11 +339,26 @@ void Viewer::goToLastPage() void Viewer::goTo(unsigned int page) { direction = 1; // in "go to" direction is always fordward + + if (continuousScroll) { + lastCenterPage = page; + continuousWidget->setAnchorPage(page); + render->goTo(page); + scrollToCurrentContinuousPage(); + return; + } + render->goTo(page); } void Viewer::updatePage() { + if (continuousScroll) { + return; + } + + setActiveWidget(content); + QPixmap *previousPage = currentPage; if (doublePage) { if (!doubleMangaPage) @@ -397,7 +447,11 @@ void Viewer::increaseZoomFactor() { zoom = std::min(zoom + 10, 500); - updateContentSize(); + if (continuousScroll) { + continuousWidget->setZoomFactor(zoom); + } else { + updateContentSize(); + } notificationsLabel->setText(QString::number(getZoomFactor()) + "%"); notificationsLabel->flash(); @@ -407,7 +461,11 @@ void Viewer::decreaseZoomFactor() { zoom = std::max(zoom - 10, 30); - updateContentSize(); + if (continuousScroll) { + continuousWidget->setZoomFactor(zoom); + } else { + updateContentSize(); + } notificationsLabel->setText(QString::number(getZoomFactor()) + "%"); notificationsLabel->flash(); @@ -435,16 +493,22 @@ void Viewer::setZoomFactor(int z) void Viewer::updateVerticalScrollBar() { - if (direction > 0) + if (direction > 0) { verticalScrollBar()->setSliderPosition(verticalScrollBar()->minimum()); - else + } else { verticalScrollBar()->setSliderPosition(verticalScrollBar()->maximum()); + } } void Viewer::scrollDown() { if (verticalScrollBar()->sliderPosition() == verticalScrollBar()->maximum()) { - next(); + if (continuousScroll) { + shouldOpenNext = true; + emit openNextComic(); + } else { + next(); + } } else { int currentPos = verticalScrollBar()->sliderPosition(); verticalScroller->setDuration(animationDuration()); @@ -460,7 +524,12 @@ void Viewer::scrollDown() void Viewer::scrollUp() { if (verticalScrollBar()->sliderPosition() == verticalScrollBar()->minimum()) { - prev(); + if (continuousScroll) { + shouldOpenPrevious = true; + emit openPreviousComic(); + } else { + prev(); + } } else { int currentPos = verticalScrollBar()->sliderPosition(); verticalScroller->setDuration(animationDuration()); @@ -670,6 +739,11 @@ void Viewer::wheelEventMouse(QWheelEvent *event) return; } + if (continuousScroll) { + animateScroll(*verticalScroller, *verticalScrollBar(), delta.y()); + return; + } + auto turnPageOnScroll = !Configuration::getConfiguration().getDoNotTurnPageOnScroll(); auto getUseSingleScrollStepToTurnPage = Configuration::getConfiguration().getUseSingleScrollStepToTurnPage(); @@ -718,6 +792,10 @@ void Viewer::wheelEventTrackpad(QWheelEvent *event) verticalScrollBar()->setValue(newVerticalValue); } + if (continuousScroll) { + return; + } + auto turnPageOnScroll = !Configuration::getConfiguration().getDoNotTurnPageOnScroll(); auto getUseSingleScrollStepToTurnPage = Configuration::getConfiguration().getUseSingleScrollStepToTurnPage(); @@ -750,11 +828,16 @@ void Viewer::wheelEventTrackpad(QWheelEvent *event) void Viewer::resizeEvent(QResizeEvent *event) { + QScrollArea::resizeEvent(event); + + if (continuousScroll) { + continuousWidget->setViewportState(verticalScrollBar()->value(), viewport()->height()); + } + updateContentSize(); goToFlow->updateSize(); goToFlow->move((width() - goToFlow->width()) / 2, height() - goToFlow->height()); informationLabel->updatePosition(); - QScrollArea::resizeEvent(event); } QPixmap Viewer::pixmap() const @@ -927,6 +1010,132 @@ void Viewer::doublePageSwitch() Configuration::getConfiguration().setDoublePage(doublePage); } +void Viewer::setContinuousScroll(bool enabled) +{ + if (continuousScroll == enabled) { + return; + } + continuousScroll = enabled; + Configuration::getConfiguration().setContinuousScroll(continuousScroll); + + if (continuousScroll) { + continuousWidget->setZoomFactor(zoom); + if (render->hasLoadedComic()) { + continuousWidget->setViewportState(verticalScrollBar()->value(), viewport()->height()); + continuousWidget->setNumPages(render->numPages()); + // set the current page as model state before any layout/scroll happens + lastCenterPage = render->getIndex(); + continuousWidget->setAnchorPage(lastCenterPage); + // pick up sizes of pages already in the buffer + continuousWidget->probeBufferedPages(); + // trigger a render cycle so new pages arrive via pageRendered signal + render->update(); + setActiveWidget(continuousWidget); + scrollToCurrentContinuousPage(); + continuousWidget->update(); + viewport()->update(); + } + // if no comic is loaded, messageLabel stays as the active widget + } else { + lastCenterPage = -1; + if (render->hasLoadedComic()) { + updatePage(); + } + // if no comic is loaded, messageLabel stays as the active widget + } +} + +void Viewer::onContinuousScroll(int value) +{ + if (!continuousScroll || !render->hasLoadedComic()) { + return; + } + + continuousWidget->setViewportState(value, viewport()->height()); + + int center = continuousWidget->centerPage(value, viewport()->height()); + + if (center != lastCenterPage && center >= 0) { + lastCenterPage = center; + continuousWidget->setAnchorPage(center); + syncingRenderFromContinuousScroll = true; + render->goTo(center); + syncingRenderFromContinuousScroll = false; + emit pageAvailable(true); + } +} + +void Viewer::onContinuousLayoutScrollRequested(int scrollY) +{ + if (!continuousScroll) { + return; + } + + auto *sb = verticalScrollBar(); + const int target = qBound(sb->minimum(), scrollY, sb->maximum()); + + sb->blockSignals(true); + sb->setValue(target); + sb->blockSignals(false); + + continuousWidget->setViewportState(target, viewport()->height()); +} + +void Viewer::scrollToCurrentContinuousPage() +{ + if (lastCenterPage < 0) { + return; + } + + auto applyPosition = [this]() { + auto *sb = verticalScrollBar(); + int targetY = continuousWidget->yPositionForPage(lastCenterPage); + targetY = qBound(sb->minimum(), targetY, sb->maximum()); + + sb->blockSignals(true); + sb->setValue(targetY); + sb->blockSignals(false); + + continuousWidget->setViewportState(targetY, viewport()->height()); + + continuousWidget->update(); + viewport()->update(); + }; + + applyPosition(); +} + +void Viewer::onNumPagesReady(unsigned int numPages) +{ + if (continuousScroll && numPages > 0) { + setActiveWidget(continuousWidget); + + continuousWidget->setViewportState(verticalScrollBar()->value(), viewport()->height()); + continuousWidget->setNumPages(numPages); + + int page = lastCenterPage; + if (page < 0) { + page = render->getIndex(); + } + page = qBound(0, page, static_cast(numPages) - 1); + lastCenterPage = page; + continuousWidget->setAnchorPage(page); + + scrollToCurrentContinuousPage(); + } +} + +void Viewer::onRenderPageChanged(int page) +{ + if (!continuousScroll || page < 0 || page == lastCenterPage || syncingRenderFromContinuousScroll) { + return; + } + + lastCenterPage = page; + continuousWidget->setAnchorPage(page); + scrollToCurrentContinuousPage(); +} + void Viewer::setMangaWithoutStoringSetting(bool manga) { doubleMangaPage = manga; @@ -948,6 +1157,8 @@ void Viewer::resetContent() { configureContent(tr("Press 'O' to open comic.")); goToFlow->reset(); + continuousWidget->reset(); + lastCenterPage = -1; emit reset(); } @@ -958,7 +1169,9 @@ void Viewer::setLoadingMessage() restoreMagnifyingGlass = true; } emit pageAvailable(false); - configureContent(tr("Loading...please wait!")); + if (!continuousScroll) { + configureContent(tr("Loading...please wait!")); + } } void Viewer::setPageUnavailableMessage() @@ -973,15 +1186,9 @@ void Viewer::setPageUnavailableMessage() void Viewer::configureContent(QString msg) { - content->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); - if (!(devicePixelRatioF() > 1)) - content->setScaledContents(true); - content->setAlignment(Qt::AlignTop | Qt::AlignHCenter); - content->setText(msg); - content->setFont(QFont("courier new", 12)); - content->adjustSize(); + messageLabel->setText(msg); + setActiveWidget(messageLabel); setFocus(Qt::ShortcutFocusReason); - // emit showingText(); } void Viewer::hideCursor() @@ -1022,7 +1229,8 @@ void Viewer::applyTheme(const Theme &theme) updateBackgroundColor(Configuration::getConfiguration().getBackgroundColor(viewerTheme.defaultBackgroundColor)); const QString textColor = viewerTheme.defaultTextColor.name(QColor::HexArgb); - content->setStyleSheet(QStringLiteral("QLabel { color : %1; background: transparent; }").arg(textColor)); + messageLabel->setStyleSheet(QStringLiteral("QLabel { color : %1; background: transparent; }").arg(textColor)); + content->setStyleSheet(QStringLiteral("QLabel { background: transparent; }")); } void Viewer::animateShowTranslator() @@ -1072,10 +1280,46 @@ void Viewer::mouseMoveEvent(QMouseEvent *event) mouseHandler->mouseMoveEvent(event); } +bool Viewer::eventFilter(QObject *obj, QEvent *event) +{ + if (obj == continuousWidget && event->type() == QEvent::MouseMove) { + auto *mouseEvent = static_cast(event); + // Map position from continuousWidget coords to Viewer coords so the + // go-to-flow proximity check and cursor management work correctly. + QPointF viewerPos = mapFromGlobal(mouseEvent->globalPosition().toPoint()); + QMouseEvent mappedEvent(mouseEvent->type(), + viewerPos, + mouseEvent->globalPosition(), + mouseEvent->button(), + mouseEvent->buttons(), + mouseEvent->modifiers()); + mouseHandler->mouseMoveEvent(&mappedEvent); + } + return QScrollArea::eventFilter(obj, event); +} + +void Viewer::setActiveWidget(QWidget *w) +{ + if (widget() == w) { + return; + } + verticalScrollBar()->blockSignals(true); + takeWidget(); + const bool isContinuous = (w == continuousWidget); + setWidgetResizable(isContinuous); + setVerticalScrollBarPolicy(isContinuous ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff); + setWidget(w); + verticalScrollBar()->blockSignals(false); +} + void Viewer::updateZoomRatio(int ratio) { zoom = ratio; - updateContentSize(); + if (continuousScroll) { + continuousWidget->setZoomFactor(zoom); + } else { + updateContentSize(); + } } bool Viewer::getIsMangaMode() diff --git a/YACReader/viewer.h b/YACReader/viewer.h index 0684e501..d5b852f5 100644 --- a/YACReader/viewer.h +++ b/YACReader/viewer.h @@ -30,6 +30,7 @@ class GoToDialog; class YACReaderTranslator; class GoToFlowWidget; class Bookmarks; +class ContinuousPageWidget; class PageLabelWidget; class NotificationsLabelWidget; @@ -113,11 +114,13 @@ public slots: int getCurrentPageNumber(); void updateZoomRatio(int ratio); bool getIsMangaMode(); + void setContinuousScroll(bool enabled); private: bool information; bool doublePage; bool doubleMangaPage; + bool continuousScroll; int zoom; @@ -144,6 +147,10 @@ private: //! Widgets QLabel *content; + QLabel *messageLabel; + ContinuousPageWidget *continuousWidget; + int lastCenterPage = -1; + bool syncingRenderFromContinuousScroll = false; YACReaderTranslator *translator; int translatorXPos; @@ -183,12 +190,19 @@ private: // Zero when animations are disabled int animationDuration() const; void animateScroll(QPropertyAnimation &scroller, const QScrollBar &scrollBar, int delta); + void onContinuousScroll(int value); + void onContinuousLayoutScrollRequested(int scrollY); + void scrollToCurrentContinuousPage(); + void onNumPagesReady(unsigned int numPages); + void onRenderPageChanged(int page); + void setActiveWidget(QWidget *w); //! Mouse handler std::unique_ptr mouseHandler; protected: void applyTheme(const Theme &theme) override; + bool eventFilter(QObject *obj, QEvent *event) override; public: Viewer(QWidget *parent = nullptr); diff --git a/YACReader/yacreader_images.qrc b/YACReader/yacreader_images.qrc index ed7b9a8e..df996b67 100644 --- a/YACReader/yacreader_images.qrc +++ b/YACReader/yacreader_images.qrc @@ -55,6 +55,7 @@ ../images/viewer_toolbar/shortcuts.svg ../images/viewer_toolbar/showBookmarks.svg ../images/viewer_toolbar/toHeight.svg + ../images/viewer_toolbar/toContinuousScroll.svg ../images/viewer_toolbar/toWidth.svg ../images/viewer_toolbar/translator.svg ../images/viewer_toolbar/zoom.svg @@ -82,6 +83,7 @@ ../images/viewer_toolbar/shortcuts_18x18.svg ../images/viewer_toolbar/showBookmarks_18x18.svg ../images/viewer_toolbar/toHeight_18x18.svg + ../images/viewer_toolbar/toContinuousScroll_18x18.svg ../images/viewer_toolbar/toWidth_18x18.svg ../images/viewer_toolbar/translator_18x18.svg ../images/viewer_toolbar/zoom_18x18.svg diff --git a/common/yacreader_global_gui.h b/common/yacreader_global_gui.h index 7f25ad99..2738e346 100644 --- a/common/yacreader_global_gui.h +++ b/common/yacreader_global_gui.h @@ -23,6 +23,7 @@ #define START_TO_TRAY "START_TO_TRAY" #define DOUBLE_PAGE "DOUBLE_PAGE" #define DOUBLE_MANGA_PAGE "DOUBLE_MANGA_PAGE" +#define CONTINUOUS_SCROLL "CONTINUOUS_SCROLL" #define COVER_IS_SP "COVER_IS_SP" #define BACKGROUND_COLOR "BACKGROUND_COLOR_10" #define SHOW_TOOLBARS "SHOW_TOOLBARS" diff --git a/images/viewer_toolbar/toContinuousScroll.svg b/images/viewer_toolbar/toContinuousScroll.svg new file mode 100644 index 00000000..17ded9c2 --- /dev/null +++ b/images/viewer_toolbar/toContinuousScroll.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/images/viewer_toolbar/toContinuousScroll_18x18.svg b/images/viewer_toolbar/toContinuousScroll_18x18.svg new file mode 100644 index 00000000..76040304 --- /dev/null +++ b/images/viewer_toolbar/toContinuousScroll_18x18.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file