Fix Qt indexing in `GapAnnotations.reposition()`
`prepareGeometryChange()` ran after the gap rects and arrow specs were mutated. Qt could retain the item's old scene index even though `boundingRect()` returned its new prepended-history position. Invalidate cached bounds and notify Qt before changing any geometry. Cover both scene lookup positions with a real offscreen item. (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))chart_local_gapper
parent
78982d8280
commit
711666f9c8
|
|
@ -670,6 +670,11 @@ class GapAnnotations(GraphicsObject):
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Notify Qt before mutating anything used by
|
||||||
|
# `boundingRect()`.
|
||||||
|
self.prepareGeometryChange()
|
||||||
|
self._br = None
|
||||||
|
|
||||||
# rebuild rect array from gap specs with new indices
|
# rebuild rect array from gap specs with new indices
|
||||||
rect_memory = self._rectarray.ndarray()
|
rect_memory = self._rectarray.ndarray()
|
||||||
|
|
||||||
|
|
@ -777,7 +782,4 @@ class GapAnnotations(GraphicsObject):
|
||||||
self._arrow_path.addPolygon(arrow_poly)
|
self._arrow_path.addPolygon(arrow_poly)
|
||||||
self._arrow_path.closeSubpath()
|
self._arrow_path.closeSubpath()
|
||||||
|
|
||||||
# invalidate bounding rect cache
|
|
||||||
self._br = None
|
|
||||||
self.prepareGeometryChange()
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ os.environ['QT_QPA_PLATFORM'] = 'offscreen'
|
||||||
|
|
||||||
import msgspec
|
import msgspec
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from PyQt6.QtWidgets import QGraphicsScene
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
@ -22,7 +23,11 @@ from piker.ui._gaps import (
|
||||||
SetGapOverlay,
|
SetGapOverlay,
|
||||||
gap_specs_from_ohlcv,
|
gap_specs_from_ohlcv,
|
||||||
)
|
)
|
||||||
from piker.ui.qt import QApplication
|
from piker.ui.qt import (
|
||||||
|
QApplication,
|
||||||
|
QPointF,
|
||||||
|
QRectF,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
FQME: str = 'gap.test'
|
FQME: str = 'gap.test'
|
||||||
|
|
@ -387,6 +392,79 @@ def test_gap_manager_real_qt_lifecycle(
|
||||||
qapp.processEvents()
|
qapp.processEvents()
|
||||||
|
|
||||||
|
|
||||||
|
def test_gap_annotations_reposition_after_prepend(
|
||||||
|
qapp: QApplication,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Reposition live gap geometry after history index offsets change.
|
||||||
|
|
||||||
|
A history prepend preserves timestamps but shifts every absolute
|
||||||
|
chart index. Gap rectangles and arrows otherwise remain attached
|
||||||
|
to their pre-prepend x coordinates. Render one real gap item,
|
||||||
|
shift its source indexes by ten, call `reposition()`, and prove
|
||||||
|
its rectangle bounds and arrow spec move together while the item
|
||||||
|
stays attached to its real Qt scene.
|
||||||
|
|
||||||
|
'''
|
||||||
|
ds: SimpleNamespace
|
||||||
|
charts: tuple[_ChartStub, _ChartStub]
|
||||||
|
ds, charts = _display_state(FQME)
|
||||||
|
annots: dict[int, GapAnnotations] = {}
|
||||||
|
gapman: GapOverlayMngr = GapOverlayMngr(
|
||||||
|
dss={FQME: ds},
|
||||||
|
annots=annots,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
gap: GapOverlay = gapman.refresh(
|
||||||
|
ds=ds,
|
||||||
|
timeframe=60,
|
||||||
|
)
|
||||||
|
assert gap.aid is not None
|
||||||
|
item: GapAnnotations = annots[gap.aid]
|
||||||
|
old_br: QRectF = item.boundingRect()
|
||||||
|
scene: QGraphicsScene = charts[1].widget.scene()
|
||||||
|
old_scene_point: QPointF = item.mapToScene(
|
||||||
|
old_br.center()
|
||||||
|
)
|
||||||
|
qapp.processEvents()
|
||||||
|
assert item in scene.items(old_scene_point)
|
||||||
|
old_rects: np.ndarray = (
|
||||||
|
item._rectarray.ndarray().copy()
|
||||||
|
)
|
||||||
|
shifted: np.ndarray = ds.hist_viz.shm.array.copy()
|
||||||
|
shifted['index'] += 10
|
||||||
|
|
||||||
|
item.reposition(
|
||||||
|
array=shifted,
|
||||||
|
fqme=FQME,
|
||||||
|
timeframe=60,
|
||||||
|
)
|
||||||
|
qapp.processEvents()
|
||||||
|
|
||||||
|
new_rects: np.ndarray = item._rectarray.ndarray()
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
new_rects[:, 0],
|
||||||
|
old_rects[:, 0] + 10,
|
||||||
|
)
|
||||||
|
assert item._gap_specs[0]['arrow_x'] == 12
|
||||||
|
new_br: QRectF = item.boundingRect()
|
||||||
|
new_scene_point: QPointF = item.mapToScene(
|
||||||
|
new_br.center()
|
||||||
|
)
|
||||||
|
assert new_br.left() == old_br.left() + 10
|
||||||
|
assert item in scene.items(new_scene_point)
|
||||||
|
assert item not in scene.items(old_scene_point)
|
||||||
|
assert item.scene() is charts[1].widget.scene()
|
||||||
|
finally:
|
||||||
|
gapman.remove_owner('chart-local')
|
||||||
|
chart: _ChartStub
|
||||||
|
for chart in charts:
|
||||||
|
chart.close()
|
||||||
|
qapp.processEvents()
|
||||||
|
|
||||||
|
|
||||||
def test_duplicate_fqme_layers_use_local_chart_identity(
|
def test_duplicate_fqme_layers_use_local_chart_identity(
|
||||||
qapp: QApplication,
|
qapp: QApplication,
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue