imageviewer_moc.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. /*
  2. * imageviewer_moc.cpp, part of VCMI engine
  3. *
  4. * Authors: listed in file AUTHORS in main folder
  5. *
  6. * License: GNU General Public License v2.0 or later
  7. * Full text of license available in license.txt file, in main folder
  8. *
  9. */
  10. #include "StdInc.h"
  11. #include <QGuiApplication>
  12. #include <QGestureEvent>
  13. #include <QPainter>
  14. #include <QPinchGesture>
  15. #include <QScreen>
  16. #include <QShortcut>
  17. #include <QSwipeGesture>
  18. #include <QTouchEvent>
  19. #include "imageviewer_moc.h"
  20. #include "ui_imageviewer_moc.h"
  21. namespace
  22. {
  23. int touchPointsCount(const QTouchEvent * touchEvent)
  24. {
  25. #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
  26. return static_cast<int>(touchEvent->points().size());
  27. #else
  28. return touchEvent->touchPoints().size();
  29. #endif
  30. }
  31. int touchEventX(const QTouchEvent * touchEvent)
  32. {
  33. #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
  34. if(touchEvent->points().empty())
  35. return 0;
  36. return static_cast<int>(touchEvent->points().first().position().x());
  37. #else
  38. if(touchEvent->touchPoints().empty())
  39. return 0;
  40. return static_cast<int>(touchEvent->touchPoints().first().pos().x());
  41. #endif
  42. }
  43. QPointF touchEventPos(const QTouchEvent * touchEvent)
  44. {
  45. #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
  46. if(touchEvent->points().empty())
  47. return QPointF{};
  48. return touchEvent->points().first().position();
  49. #else
  50. if(touchEvent->touchPoints().empty())
  51. return QPointF{};
  52. return touchEvent->touchPoints().first().pos();
  53. #endif
  54. }
  55. constexpr int sideButtonSize = 48;
  56. int mouseEventX(const QMouseEvent * mouseEvent)
  57. {
  58. #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
  59. return static_cast<int>(mouseEvent->position().x());
  60. #else
  61. return static_cast<int>(mouseEvent->localPos().x());
  62. #endif
  63. }
  64. }
  65. ImageViewer::ImageViewer(QWidget * parent)
  66. : QDialog(parent), ui(new Ui::ImageViewer)
  67. {
  68. ui->setupUi(this);
  69. setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
  70. setFocusPolicy(Qt::StrongFocus);
  71. setAttribute(Qt::WA_AcceptTouchEvents, true);
  72. ui->imageLabel->setAttribute(Qt::WA_AcceptTouchEvents, true);
  73. grabGesture(Qt::SwipeGesture);
  74. grabGesture(Qt::PinchGesture);
  75. shortcutPrevious = new QShortcut(QKeySequence(Qt::Key_Left), this);
  76. shortcutNext = new QShortcut(QKeySequence(Qt::Key_Right), this);
  77. shortcutClose = new QShortcut(QKeySequence(Qt::Key_Escape), this);
  78. connect(shortcutPrevious, &QShortcut::activated, this, &ImageViewer::showPreviousImage);
  79. connect(shortcutNext, &QShortcut::activated, this, &ImageViewer::showNextImage);
  80. connect(shortcutClose, &QShortcut::activated, this, &ImageViewer::close);
  81. connect(ui->buttonPrevious, &QPushButton::clicked, this, &ImageViewer::showPreviousImage);
  82. connect(ui->buttonNext, &QPushButton::clicked, this, &ImageViewer::showNextImage);
  83. connect(ui->buttonClose, &QPushButton::clicked, this, &ImageViewer::close);
  84. updateResponsiveLayout(size());
  85. }
  86. void ImageViewer::changeEvent(QEvent *event)
  87. {
  88. if(event->type() == QEvent::LanguageChange)
  89. {
  90. ui->retranslateUi(this);
  91. }
  92. QDialog::changeEvent(event);
  93. }
  94. bool ImageViewer::event(QEvent * event)
  95. {
  96. if(event->type() == QEvent::Gesture)
  97. {
  98. auto * gestureEvent = static_cast<QGestureEvent *>(event);
  99. if(auto * pinch = static_cast<QPinchGesture *>(gestureEvent->gesture(Qt::PinchGesture)))
  100. {
  101. if(pinch->state() == Qt::GestureStarted || pinch->state() == Qt::GestureUpdated)
  102. {
  103. touchSwipeSuppressed = true;
  104. touchSwipeActive = false;
  105. }
  106. if(pinch->state() == Qt::GestureUpdated)
  107. applyZoomMultiplier(pinch->scaleFactor());
  108. return true;
  109. }
  110. if(auto * swipe = static_cast<QSwipeGesture *>(gestureEvent->gesture(Qt::SwipeGesture)))
  111. {
  112. if(swipe->state() == Qt::GestureFinished)
  113. {
  114. if(swipe->horizontalDirection() == QSwipeGesture::Left)
  115. showNextImage();
  116. else if(swipe->horizontalDirection() == QSwipeGesture::Right)
  117. showPreviousImage();
  118. }
  119. return true;
  120. }
  121. }
  122. if(event->type() == QEvent::TouchBegin)
  123. {
  124. auto * touchEvent = static_cast<QTouchEvent *>(event);
  125. if(touchPointsCount(touchEvent) > 1)
  126. {
  127. touchSwipeSuppressed = true;
  128. touchSwipeActive = false;
  129. touchPanActive = false;
  130. return true;
  131. }
  132. const auto pos = touchEventPos(touchEvent);
  133. if(pos.isNull())
  134. return QDialog::event(event);
  135. auto * touched = childAt(pos.toPoint());
  136. if(touched == ui->buttonPrevious || touched == ui->buttonNext || touched == ui->buttonClose)
  137. return QDialog::event(event);
  138. lastDragPosition = pos;
  139. touchStartPositionX = static_cast<int>(pos.x());
  140. touchPanActive = zoomFactor > 1.0;
  141. touchSwipeActive = !touchPanActive;
  142. return true;
  143. }
  144. if(event->type() == QEvent::TouchUpdate)
  145. {
  146. auto * touchEvent = static_cast<QTouchEvent *>(event);
  147. if(!touchPanActive)
  148. return QDialog::event(event);
  149. const auto pos = touchEventPos(touchEvent);
  150. if(pos.isNull())
  151. return true;
  152. panImage(pos - lastDragPosition);
  153. lastDragPosition = pos;
  154. return true;
  155. }
  156. if(event->type() == QEvent::TouchEnd)
  157. {
  158. auto * touchEvent = static_cast<QTouchEvent *>(event);
  159. if(touchSwipeSuppressed)
  160. {
  161. touchSwipeSuppressed = false;
  162. touchSwipeActive = false;
  163. touchPanActive = false;
  164. return true;
  165. }
  166. if(touchPanActive)
  167. {
  168. touchPanActive = false;
  169. return true;
  170. }
  171. if(!touchSwipeActive)
  172. return QDialog::event(event);
  173. const int touchEndX = touchEventX(touchEvent);
  174. handleHorizontalSwipe(touchEndX - touchStartPositionX);
  175. touchSwipeActive = false;
  176. return true;
  177. }
  178. return QDialog::event(event);
  179. }
  180. ImageViewer::~ImageViewer()
  181. {
  182. delete ui;
  183. }
  184. QSize ImageViewer::calculateWindowSize()
  185. {
  186. #ifdef VCMI_MOBILE
  187. return QGuiApplication::primaryScreen()->availableGeometry().size();
  188. #else
  189. return QGuiApplication::primaryScreen()->availableGeometry().size() * 0.8;
  190. #endif
  191. }
  192. void ImageViewer::showImages(const QStringList & imagePaths, int startIndex, QWidget * parent)
  193. {
  194. if(imagePaths.empty())
  195. return;
  196. auto * viewer = new ImageViewer(parent);
  197. viewer->setImages(imagePaths, startIndex);
  198. viewer->setAttribute(Qt::WA_DeleteOnClose, true);
  199. viewer->setModal(Qt::WindowModal);
  200. #ifdef VCMI_MOBILE
  201. viewer->setGeometry(QGuiApplication::primaryScreen()->availableGeometry());
  202. #endif
  203. viewer->show();
  204. viewer->setFocus();
  205. }
  206. void ImageViewer::setImages(const QStringList & imagePaths, int startIndex)
  207. {
  208. assert(!imagePaths.empty());
  209. this->imagePaths = imagePaths;
  210. const int lastImageIndex = static_cast<int>(imagePaths.size() - 1);
  211. currentImageIndex = std::clamp(startIndex, 0, lastImageIndex);
  212. showCurrentImage();
  213. }
  214. void ImageViewer::showCurrentImage()
  215. {
  216. assert(!imagePaths.empty());
  217. QPixmap pixmap(imagePaths.at(currentImageIndex));
  218. if(pixmap.isNull())
  219. return;
  220. currentPixmap = pixmap;
  221. zoomFactor = 1.0;
  222. panOffset = QPointF{};
  223. #ifndef VCMI_MOBILE
  224. if(!desktopWindowInitialized)
  225. {
  226. const QSize availableWindowSize = calculateWindowSize();
  227. const int desktopMargin = 12;
  228. const int desktopSpacing = 8;
  229. const int reservedWidth = (sideButtonSize * 2) + (desktopMargin * 2) + desktopSpacing;
  230. const int reservedHeight = desktopMargin * 2;
  231. const int maxLabelWidth = std::max(1, std::min(availableWindowSize.width() - reservedWidth, availableWindowSize.height()));
  232. const int maxLabelHeight = std::max(1, availableWindowSize.height() - reservedHeight);
  233. QSize labelTargetSize = currentPixmap.size();
  234. labelTargetSize.scale(QSize(maxLabelWidth, maxLabelHeight), Qt::KeepAspectRatio);
  235. resize(labelTargetSize.width() + reservedWidth, labelTargetSize.height() + reservedHeight);
  236. desktopWindowInitialized = true;
  237. }
  238. #endif
  239. updateDisplayedPixmap();
  240. ui->buttonPrevious->setVisible(imagePaths.size() > 1);
  241. ui->buttonNext->setVisible(imagePaths.size() > 1);
  242. #ifdef VCMI_MOBILE
  243. ui->buttonClose->setVisible(true);
  244. #else
  245. ui->buttonClose->setVisible(false);
  246. #endif
  247. }
  248. void ImageViewer::showPreviousImage()
  249. {
  250. if(imagePaths.size() <= 1)
  251. return;
  252. currentImageIndex = (currentImageIndex - 1 + imagePaths.size()) % imagePaths.size();
  253. showCurrentImage();
  254. }
  255. void ImageViewer::showNextImage()
  256. {
  257. if(imagePaths.size() <= 1)
  258. return;
  259. currentImageIndex = (currentImageIndex + 1) % imagePaths.size();
  260. showCurrentImage();
  261. }
  262. void ImageViewer::updateDisplayedPixmap()
  263. {
  264. if(currentPixmap.isNull())
  265. return;
  266. const QSize labelSize = ui->imageLabel->size();
  267. if(labelSize.isEmpty())
  268. return;
  269. QSize targetSize = currentPixmap.size();
  270. targetSize.scale(labelSize, Qt::KeepAspectRatio);
  271. targetSize = targetSize * zoomFactor;
  272. targetSize = targetSize.boundedTo(calculateWindowSize() * 2);
  273. const auto scaledPixmap = currentPixmap.scaled(targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
  274. const int maxPanX = std::max(0, (scaledPixmap.width() - labelSize.width()) / 2);
  275. const int maxPanY = std::max(0, (scaledPixmap.height() - labelSize.height()) / 2);
  276. panOffset.setX(std::clamp(panOffset.x(), static_cast<qreal>(-maxPanX), static_cast<qreal>(maxPanX)));
  277. panOffset.setY(std::clamp(panOffset.y(), static_cast<qreal>(-maxPanY), static_cast<qreal>(maxPanY)));
  278. QPixmap canvas(labelSize);
  279. canvas.fill(palette().color(QPalette::Window));
  280. QPainter painter(&canvas);
  281. const QPoint drawPos((labelSize.width() - scaledPixmap.width()) / 2 + static_cast<int>(panOffset.x()), (labelSize.height() - scaledPixmap.height()) / 2 + static_cast<int>(panOffset.y()));
  282. painter.drawPixmap(drawPos, scaledPixmap);
  283. painter.end();
  284. ui->imageLabel->setPixmap(canvas);
  285. }
  286. void ImageViewer::applyZoomMultiplier(qreal multiplier)
  287. {
  288. if(multiplier <= 0.0)
  289. return;
  290. zoomFactor *= multiplier;
  291. constexpr qreal minZoomFactor = 1.0;
  292. zoomFactor = std::clamp(zoomFactor, minZoomFactor, 3.0);
  293. if(zoomFactor <= 1.0)
  294. panOffset = QPointF{};
  295. updateDisplayedPixmap();
  296. }
  297. void ImageViewer::panImage(const QPointF & delta)
  298. {
  299. if(zoomFactor <= 1.0)
  300. return;
  301. panOffset += delta;
  302. updateDisplayedPixmap();
  303. }
  304. void ImageViewer::handleHorizontalSwipe(int deltaX)
  305. {
  306. constexpr int swipeThreshold = 40;
  307. if(deltaX > swipeThreshold)
  308. showPreviousImage();
  309. else if(deltaX < -swipeThreshold)
  310. showNextImage();
  311. }
  312. void ImageViewer::resizeEvent(QResizeEvent * event)
  313. {
  314. QDialog::resizeEvent(event);
  315. updateResponsiveLayout(event->size());
  316. updateDisplayedPixmap();
  317. }
  318. void ImageViewer::setButtonSize(int size)
  319. {
  320. ui->buttonPrevious->setMinimumSize(size, size);
  321. ui->buttonPrevious->setMaximumSize(size, size);
  322. ui->buttonNext->setMinimumSize(size, size);
  323. ui->buttonNext->setMaximumSize(size, size);
  324. ui->buttonClose->setMinimumSize(size, size);
  325. ui->buttonClose->setMaximumSize(size, size);
  326. }
  327. void ImageViewer::setButtonStyle(int size)
  328. {
  329. const int buttonRadius = size / 2;
  330. const int buttonFontSize = std::max(14, size / 2);
  331. const QString buttonStyle = QStringLiteral(
  332. "QPushButton#buttonPrevious, QPushButton#buttonNext, QPushButton#buttonClose {"
  333. " border: 1px solid palette(mid);"
  334. " border-radius: %1px;"
  335. " padding: 3px;"
  336. " background: palette(button);"
  337. " font-size: %2px;"
  338. " font-weight: 700;"
  339. " }"
  340. "QPushButton#buttonPrevious:hover, QPushButton#buttonNext:hover, QPushButton#buttonClose:hover {"
  341. " background: palette(light);"
  342. " }").arg(buttonRadius).arg(buttonFontSize);
  343. ui->buttonPrevious->setStyleSheet(buttonStyle);
  344. ui->buttonNext->setStyleSheet(buttonStyle);
  345. ui->buttonClose->setStyleSheet(buttonStyle);
  346. }
  347. void ImageViewer::updateResponsiveLayout(const QSize & windowSize)
  348. {
  349. #ifdef VCMI_MOBILE
  350. const int shortestSide = std::min(windowSize.width(), windowSize.height());
  351. const bool compactLayout = shortestSide < 560;
  352. const int spacing = compactLayout ? 2 : 4;
  353. const int margin = compactLayout ? 2 : 4;
  354. const int buttonSize = compactLayout ? 40 : sideButtonSize;
  355. #else
  356. Q_UNUSED(windowSize);
  357. const int spacing = 8;
  358. const int margin = 12;
  359. const int buttonSize = sideButtonSize;
  360. #endif
  361. ui->gridLayout->setContentsMargins(margin, margin, margin, margin);
  362. ui->gridLayout->setHorizontalSpacing(spacing);
  363. ui->gridLayout->setVerticalSpacing(spacing);
  364. setButtonSize(buttonSize);
  365. setButtonStyle(buttonSize);
  366. ui->gridLayout->setColumnStretch(0, 0);
  367. ui->gridLayout->setColumnStretch(1, 1);
  368. ui->gridLayout->setColumnStretch(2, 0);
  369. ui->gridLayout->setColumnMinimumWidth(0, buttonSize);
  370. ui->gridLayout->setColumnMinimumWidth(2, buttonSize);
  371. ui->imageLabel->setMaximumWidth(QWIDGETSIZE_MAX);
  372. ui->imageLabel->setMinimumWidth(0);
  373. }
  374. void ImageViewer::mousePressEvent(QMouseEvent * event)
  375. {
  376. if(event->button() == Qt::LeftButton && zoomFactor > 1.0)
  377. {
  378. #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
  379. lastDragPosition = event->position();
  380. #else
  381. lastDragPosition = event->localPos();
  382. #endif
  383. mousePanActive = true;
  384. event->accept();
  385. return;
  386. }
  387. mouseStartPositionX = mouseEventX(event);
  388. QDialog::mousePressEvent(event);
  389. }
  390. void ImageViewer::mouseMoveEvent(QMouseEvent * event)
  391. {
  392. if(!mousePanActive)
  393. {
  394. QDialog::mouseMoveEvent(event);
  395. return;
  396. }
  397. #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
  398. const QPointF currentPosition = event->position();
  399. #else
  400. const QPointF currentPosition = event->localPos();
  401. #endif
  402. panImage(currentPosition - lastDragPosition);
  403. lastDragPosition = currentPosition;
  404. event->accept();
  405. }
  406. void ImageViewer::mouseReleaseEvent(QMouseEvent * event)
  407. {
  408. if(mousePanActive && event->button() == Qt::LeftButton)
  409. {
  410. mousePanActive = false;
  411. event->accept();
  412. return;
  413. }
  414. const int mouseEndX = mouseEventX(event);
  415. handleHorizontalSwipe(mouseEndX - mouseStartPositionX);
  416. QDialog::mouseReleaseEvent(event);
  417. }
  418. void ImageViewer::wheelEvent(QWheelEvent * event)
  419. {
  420. const QPoint numDegrees = event->angleDelta() / 8;
  421. if(numDegrees.y() == 0)
  422. {
  423. QDialog::wheelEvent(event);
  424. return;
  425. }
  426. const qreal zoomStep = numDegrees.y() > 0 ? 1.1 : (1.0 / 1.1);
  427. applyZoomMultiplier(zoomStep);
  428. event->accept();
  429. }
  430. void ImageViewer::keyPressEvent(QKeyEvent * event)
  431. {
  432. switch(event->key())
  433. {
  434. case Qt::Key_Left:
  435. showPreviousImage();
  436. event->accept();
  437. break;
  438. case Qt::Key_Right:
  439. showNextImage();
  440. event->accept();
  441. break;
  442. case Qt::Key_Escape:
  443. close();
  444. event->accept();
  445. break;
  446. default:
  447. QDialog::keyPressEvent(event);
  448. break;
  449. }
  450. }