Skip to content

Commit 4056ce8

Browse files
authored
Fix an OverflowError during rapid kinetic scrolling (#47)
This is caused by PyQt not enforcing C++ integer size restrictions on QPoint coordinates, which we correct for by implementing our own overflow-safe Point type. Fixes frescobaldi/frescobaldi#2130.
1 parent d437283 commit 4056ce8

File tree

2 files changed

+49
-20
lines changed

2 files changed

+49
-20
lines changed

qpageview/scrollarea.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525

2626
import math
2727

28-
from PyQt6.QtCore import QPoint, QRect, QSize, Qt
28+
# we use util.Point here rather than raw QPoint to prevent an overflow
29+
# during rapid kinetic scrolling (Frescobaldi issue #2130)
30+
from PyQt6.QtCore import QRect, QSize, Qt
2931
from PyQt6.QtWidgets import QAbstractScrollArea
3032

3133
from . import util
@@ -80,7 +82,7 @@ def areaPos(self):
8082
left = -self.horizontalScrollBar().value()
8183
if top < 0:
8284
top = -self.verticalScrollBar().value()
83-
return QPoint(left, top)
85+
return util.Point(left, top)
8486

8587
def visibleArea(self):
8688
"""Return a rectangle describing the part of the area that is visible."""
@@ -107,7 +109,7 @@ def offsetToEnsureVisible(self, rect):
107109
dx = rect.right() - area.right()
108110
if rect.left() < area.left() + dx:
109111
dx = rect.left() - area.left()
110-
return QPoint(dx, dy)
112+
return util.Point(dx, dy)
111113

112114
def ensureVisible(self, rect, margins=None, allowKinetic=True):
113115
"""Performs the minimal scroll to make rect visible.
@@ -152,7 +154,7 @@ def scrollOffset(self):
152154
"""Return the current scroll offset."""
153155
x = self.horizontalScrollBar().value()
154156
y = self.verticalScrollBar().value()
155-
return QPoint(x, y)
157+
return util.Point(x, y)
156158

157159
def canScrollBy(self, diff):
158160
"""Does not scroll, but return the actual distance the View would scroll.
@@ -165,7 +167,7 @@ def canScrollBy(self, diff):
165167

166168
x = min(max(0, hbar.value() + diff.x()), hbar.maximum())
167169
y = min(max(0, vbar.value() + diff.y()), vbar.maximum())
168-
return QPoint(x - hbar.value(), y - vbar.value())
170+
return util.Point(x - hbar.value(), y - vbar.value())
169171

170172
def scrollForDragging(self, pos):
171173
"""Slowly scroll the View if pos is close to the edge of the viewport.
@@ -180,7 +182,7 @@ def scrollForDragging(self, pos):
180182
dy = pos.y() - viewport.top() - 12
181183
if dy >= 0:
182184
dy = max(0, pos.y() - viewport.bottom() + 12)
183-
self.steadyScroll(QPoint(dx*10, dy*10))
185+
self.steadyScroll(util.Point(dx*10, dy*10))
184186

185187
def scrollTo(self, pos):
186188
"""Scroll the View to get pos (QPoint) in the top left corner (if possible).
@@ -204,7 +206,7 @@ def scrollBy(self, diff):
204206
y = vbar.value()
205207
vbar.setValue(vbar.value() + diff.y())
206208
y = vbar.value() - y
207-
return QPoint(x, y)
209+
return util.Point(x, y)
208210

209211
def kineticScrollTo(self, pos):
210212
"""Scroll the View to get pos (QPoint) in the top left corner (if possible).
@@ -328,7 +330,7 @@ def mouseReleaseEvent(self, ev):
328330
diffy = int(sy * (sy + 1) / 2)
329331
if speed.x() < 0: diffx = -diffx
330332
if speed.y() < 0: diffy = -diffy
331-
self.kineticScrollBy(QPoint(diffx, diffy))
333+
self.kineticScrollBy(util.Point(diffx, diffy))
332334
self._dragPos = None
333335
self._dragTime = None
334336
self._dragSpeed = None
@@ -348,23 +350,23 @@ def keyPressEvent(self, ev):
348350
# add Home and End, even in non-kinetic mode
349351
scroll = self.kineticScrollBy if self.kineticScrollingEnabled else self.scrollBy
350352
if ev.key() == Qt.Key.Key_Home:
351-
scroll(QPoint(0, -vbar.value()))
353+
scroll(util.Point(0, -vbar.value()))
352354
elif ev.key() == Qt.Key.Key_End:
353-
scroll(QPoint(0, vbar.maximum() - vbar.value()))
355+
scroll(util.Point(0, vbar.maximum() - vbar.value()))
354356
elif self.kineticScrollingEnabled:
355357
# make arrow keys and PgUp and PgDn kinetic
356358
if ev.key() == Qt.Key.Key_PageDown:
357-
self.kineticAddDelta(QPoint(0, vbar.pageStep()))
359+
self.kineticAddDelta(util.Point(0, vbar.pageStep()))
358360
elif ev.key() == Qt.Key.Key_PageUp:
359-
self.kineticAddDelta(QPoint(0, -vbar.pageStep()))
361+
self.kineticAddDelta(util.Point(0, -vbar.pageStep()))
360362
elif ev.key() == Qt.Key.Key_Down:
361-
self.kineticAddDelta(QPoint(0, vbar.singleStep()))
363+
self.kineticAddDelta(util.Point(0, vbar.singleStep()))
362364
elif ev.key() == Qt.Key.Key_Up:
363-
self.kineticAddDelta(QPoint(0, -vbar.singleStep()))
365+
self.kineticAddDelta(util.Point(0, -vbar.singleStep()))
364366
elif ev.key() == Qt.Key.Key_Left:
365-
self.kineticAddDelta(QPoint(-hbar.singleStep(), 0))
367+
self.kineticAddDelta(util.Point(-hbar.singleStep(), 0))
366368
elif ev.key() == Qt.Key.Key_Right:
367-
self.kineticAddDelta(QPoint(hbar.singleStep(), 0))
369+
self.kineticAddDelta(util.Point(hbar.singleStep(), 0))
368370
else:
369371
super().keyPressEvent(ev)
370372
else:
@@ -416,7 +418,7 @@ def step(self):
416418
dy += dy1
417419

418420
# scroll in the right direction
419-
diff = QPoint(-dx if x < 0 else dx, -dy if y < 0 else dy)
421+
diff = util.Point(-dx if x < 0 else dx, -dy if y < 0 else dy)
420422
return diff
421423

422424
def finished(self):
@@ -465,7 +467,7 @@ def scrollBy(self, diff):
465467
self._x = sx
466468
self._y = sy
467469
# the offset is accounted for in the first step
468-
self._offset = QPoint(offx, offy)
470+
self._offset = util.Point(offx, offy)
469471

470472
def remainingDistance(self):
471473
"""Return the remaining distance."""
@@ -477,15 +479,15 @@ def remainingDistance(self):
477479
dy = sy * (sy + 1) // 2
478480
if self._y < 0:
479481
dy = -dy
480-
return QPoint(dx, dy)
482+
return util.Point(dx, dy)
481483

482484
def remainingTicks(self):
483485
"""Return the remaining ticks of this scroll."""
484486
return max(abs(self._x), abs(self._y))
485487

486488
def step(self):
487489
"""Return a QPoint indicating the diff to scroll in this step."""
488-
ret = QPoint(self._x, self._y)
490+
ret = util.Point(self._x, self._y)
489491
if self._offset:
490492
ret += self._offset
491493
self._offset = None

qpageview/util.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,24 @@ def mouseReleaseEvent(self, ev):
179179
super().mouseReleaseEvent(ev)
180180

181181

182+
class Point(QPoint):
183+
"""An overflow-safe QPoint.
184+
185+
This works around an oversight in PyQt, which does not
186+
constrain integer arguments to fixed sizes as used in C++,
187+
causing an OverflowError when those limits are exceeded.
188+
189+
"""
190+
def __init__(self, x, y):
191+
super().__init__(clamp_int32(x), clamp_int32(y))
192+
193+
def setX(self, x):
194+
super().setX(clamp_int32(x))
195+
196+
def setY(self, y):
197+
super().setY(clamp_int32(y))
198+
199+
182200
def rotate(matrix, rotation, width, height, dest=False):
183201
"""Rotate matrix inside a rectangular area of width x height.
184202
@@ -243,6 +261,15 @@ def alignrect(rect, point, alignment=Qt.AlignmentFlag.AlignCenter):
243261
rect.moveBottom(point.y())
244262

245263

264+
def clamp(x, lower, upper):
265+
"""Return x bounded such that lower <= x <= upper."""
266+
return lower if x < lower else upper if x > upper else x
267+
268+
def clamp_int32(x):
269+
"""Return x bounded to the range of a 32-bit signed integer."""
270+
return clamp(x, -2**31, 2**31 - 1)
271+
272+
246273
# Found at: https://stackoverflow.com/questions/1986152/why-doesnt-python-have-a-sign-function
247274
def sign(x):
248275
"""Return the sign of x: -1 if x < 0, 0 if x == 0, or 1 if x > 0."""

0 commit comments

Comments
 (0)