Skip to content

Commit 6b5fa3d

Browse files
iwahageclaude
andcommitted
Add two-finger rotation and configurable gesture uncovered area rendering
- Add two-finger rotation support during pinch gestures when auto-rotation (compass) is OFF. Rotation is applied around the pinch center with correct view center compensation on release. - Render map/templates in uncovered regions during pinch rotation (not just zoom-out), using the same composite transform for perfect alignment with the scaled/rotated cache. - Add "Gesture uncovered area" setting with 3 levels: Off (gray), Templates only (default), Full (templates + map). This allows lower-end devices to trade visual quality for smoother gesture performance. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cad3d01 commit 6b5fa3d

7 files changed

Lines changed: 163 additions & 85 deletions

File tree

src/gui/map/map_editor.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4083,6 +4083,8 @@ void MapEditorController::enableCompassDisplay(bool enable)
40834083

40844084
void MapEditorController::alignMapWithNorth(bool enable)
40854085
{
4086+
map_widget->setAutoRotationActive(enable);
4087+
40864088
const int update_interval = 1000; // milliseconds
40874089

40884090
if (enable)

src/gui/map/map_widget.cpp

Lines changed: 129 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <stdexcept>
2626

2727
#include <QApplication>
28+
#include <QtMath>
2829
#include <QColor>
2930
#include <QContextMenuEvent>
3031
#include <QEvent>
@@ -108,6 +109,8 @@ MapWidget::MapWidget(bool show_help, bool force_antialiasing, QWidget* parent)
108109
, dragging(false)
109110
, pinching(false)
110111
, pinching_factor(1.0)
112+
, pinching_angle(0.0)
113+
, auto_rotation_active(false)
111114
, below_template_cache_dirty_rect(rect())
112115
, above_template_cache_dirty_rect(rect())
113116
, map_cache_dirty_rect(rect())
@@ -192,6 +195,11 @@ void MapWidget::setActivity(MapEditorActivity* activity)
192195
}
193196

194197

198+
void MapWidget::setAutoRotationActive(bool active)
199+
{
200+
auto_rotation_active = active;
201+
}
202+
195203
void MapWidget::setGesturesEnabled(bool enabled)
196204
{
197205
gestures_enabled = enabled;
@@ -396,29 +404,50 @@ qreal MapWidget::startPinching(const QPoint& center)
396404
drag_start_pos = center;
397405
pinching_center = center;
398406
pinching_factor = 1.0;
407+
pinching_angle = 0.0;
399408
return pinching_factor;
400409
}
401410

402-
void MapWidget::updatePinching(const QPoint& center, qreal factor)
411+
void MapWidget::updatePinching(const QPoint& center, qreal factor, qreal angle)
403412
{
404413
Q_ASSERT(pinching);
405414
pinching_center = center;
406415
pinching_factor = factor;
416+
pinching_angle = auto_rotation_active ? 0.0 : angle;
407417
updateZoomDisplay();
408418
update();
409419
}
410420

411-
void MapWidget::finishPinching(const QPoint& center, qreal factor)
421+
void MapWidget::finishPinching(const QPoint& center, qreal factor, qreal angle)
412422
{
413423
pinching = false;
414424
view->finishPanning(center - drag_start_pos);
415425
view->setZoom(factor * view->getZoom(), viewportToView(center));
426+
if (!auto_rotation_active && angle != 0.0)
427+
{
428+
double delta_rad = qDegreesToRadians(angle);
429+
QPointF rot_center_view = viewportToView(center);
430+
auto rot_center_map = MapCoordF(view->viewToMap(rot_center_view));
431+
auto old_center = MapCoordF(view->center());
432+
433+
view->setRotation(view->getRotation() + delta_rad);
434+
435+
// Adjust center so the pinch center stays fixed on screen
436+
auto offset = old_center - rot_center_map;
437+
auto cos_d = cos(delta_rad);
438+
auto sin_d = sin(delta_rad);
439+
auto new_center = rot_center_map + MapCoordF(
440+
offset.x() * cos_d + offset.y() * sin_d,
441+
-offset.x() * sin_d + offset.y() * cos_d);
442+
view->setCenter(MapCoord(new_center));
443+
}
416444
}
417445

418446
void MapWidget::cancelPinching()
419447
{
420448
pinching = false;
421449
pinching_factor = 1.0;
450+
pinching_angle = 0.0;
422451
update();
423452
}
424453

@@ -854,6 +883,7 @@ void MapWidget::gestureEvent(QGestureEvent* event)
854883
QPinchGesture* pinch = static_cast<QPinchGesture *>(gesture);
855884
QPoint center = pinch->centerPoint().toPoint();
856885
qreal factor = pinch->totalScaleFactor();
886+
qreal rotation_angle = pinch->totalRotationAngle();
857887
switch (pinch->state())
858888
{
859889
case Qt::GestureStarted:
@@ -867,10 +897,10 @@ void MapWidget::gestureEvent(QGestureEvent* event)
867897
pinch->setTotalScaleFactor(factor);
868898
break;
869899
case Qt::GestureUpdated:
870-
updatePinching(center, factor);
900+
updatePinching(center, factor, rotation_angle);
871901
break;
872902
case Qt::GestureFinished:
873-
finishPinching(center, factor);
903+
finishPinching(center, factor, rotation_angle);
874904
break;
875905
case Qt::GestureCanceled:
876906
cancelPinching();
@@ -910,23 +940,39 @@ void MapWidget::paintEvent(QPaintEvent* event)
910940
QRect target = exposed;
911941
if (pinching)
912942
{
913-
if (pinching_factor < 1.0)
914-
{
915-
// Zoom-out: draw the uncovered region first, then overlay
916-
// the scaled cache on top.
917-
drawPinchUncoveredRegion(painter, exposed);
918-
}
919-
else
920-
{
943+
// Build pinch transform (may include rotation)
944+
QTransform pinch_xf;
945+
pinch_xf.translate(pinching_center.x(), pinching_center.y());
946+
if (pinching_angle != 0.0)
947+
pinch_xf.rotate(pinching_angle);
948+
pinch_xf.scale(pinching_factor, pinching_factor);
949+
pinch_xf.translate(-drag_start_pos.x(), -drag_start_pos.y());
950+
951+
// Check if there are uncovered corners (zoom-out or rotation)
952+
QPolygon covered = pinch_xf.mapToPolygon(exposed);
953+
QRegion uncovered = QRegion(exposed) - QRegion(covered);
954+
if (uncovered.isEmpty() || !drawPinchUncoveredRegion(painter, exposed))
921955
painter.fillRect(exposed, QColor(Qt::gray));
922-
}
956+
923957
painter.translate(pinching_center.x(), pinching_center.y());
958+
if (pinching_angle != 0.0)
959+
painter.rotate(pinching_angle);
924960
painter.scale(pinching_factor, pinching_factor);
925961
painter.translate(-drag_start_pos.x(), -drag_start_pos.y());
926962
}
927963
else if (pan_offset != QPoint())
928964
{
929-
drawPanUncoveredRegion(painter, exposed);
965+
if (!drawPanUncoveredRegion(painter, exposed))
966+
{
967+
if (pan_offset.x() > 0)
968+
painter.fillRect(QRect(0, pan_offset.y(), pan_offset.x(), height() - pan_offset.y()), QColor(Qt::gray));
969+
else if (pan_offset.x() < 0)
970+
painter.fillRect(QRect(width() + pan_offset.x(), pan_offset.y(), -pan_offset.x(), height() - pan_offset.y()), QColor(Qt::gray));
971+
if (pan_offset.y() > 0)
972+
painter.fillRect(QRect(0, 0, width(), pan_offset.y()), QColor(Qt::gray));
973+
else if (pan_offset.y() < 0)
974+
painter.fillRect(QRect(0, height() + pan_offset.y(), width(), -pan_offset.y()), QColor(Qt::gray));
975+
}
930976
target.translate(pan_offset);
931977
}
932978

@@ -1421,54 +1467,52 @@ void MapWidget::drawTemplateCache(QPainter& painter, const QImage& cache, const
14211467
painter.restore();
14221468
}
14231469

1424-
void MapWidget::drawPinchUncoveredRegion(QPainter& painter, const QRect& exposed) const
1470+
bool MapWidget::drawPinchUncoveredRegion(QPainter& painter, const QRect& exposed) const
14251471
{
14261472
Q_ASSERT(pinching);
1427-
Q_ASSERT(pinching_factor < 1.0);
14281473

1429-
// The pinch transform scales the cache around pinching_center. When
1430-
// zooming out the scaled cache is smaller than the widget, leaving an
1431-
// uncovered border. Instead of computing a separate "virtual" view
1432-
// state, we use the exact same composite transform that paintEvent
1433-
// applies to the cache:
1434-
// pinch_xf * translate(w/2, h/2) * worldTransform
1435-
// This guarantees that the uncovered region aligns perfectly with the
1436-
// scaled cache — no independent center/zoom calculation needed.
1474+
int rendering_level = Settings::getInstance().getSettingCached(Settings::MapDisplay_GestureExtraRendering).toInt();
1475+
if (rendering_level <= 0)
1476+
return false;
1477+
1478+
// The pinch transform scales (and optionally rotates) the cache around
1479+
// pinching_center. We use the exact same composite transform that
1480+
// paintEvent applies to the cache to guarantee perfect alignment.
14371481

14381482
// 1. Build the pinch-to-viewport transform (same as paintEvent sets on the painter)
14391483
QTransform pinch_xf;
14401484
pinch_xf.translate(pinching_center.x(), pinching_center.y());
1485+
if (pinching_angle != 0.0)
1486+
pinch_xf.rotate(pinching_angle);
14411487
pinch_xf.scale(pinching_factor, pinching_factor);
14421488
pinch_xf.translate(-drag_start_pos.x(), -drag_start_pos.y());
14431489

1444-
// 2. Determine the area covered by the scaled cache
1445-
QRect cache_rect = exposed; // the cache covers the full widget
1490+
// 2. Determine the area covered by the scaled/rotated cache
1491+
QRect cache_rect = exposed;
14461492
QPolygon covered = pinch_xf.mapToPolygon(cache_rect);
1447-
QRegion covered_region(covered);
1448-
QRegion uncovered = QRegion(exposed) - covered_region;
1493+
QRegion uncovered = QRegion(exposed) - QRegion(covered);
14491494
if (uncovered.isEmpty())
1450-
return; // zoom-in or fully covered — nothing to do
1495+
return true;
14511496

1452-
// 3. Set up the composite transform:
1453-
// pinch * translate(w/2, h/2) * view->worldTransform()
1454-
// This is the same transform the cache content goes through.
1497+
// 3. Set up the composite transform
14551498
auto setupTransform = [&](QPainter& p) {
14561499
p.translate(pinching_center.x(), pinching_center.y());
1500+
if (pinching_angle != 0.0)
1501+
p.rotate(pinching_angle);
14571502
p.scale(pinching_factor, pinching_factor);
14581503
p.translate(-drag_start_pos.x(), -drag_start_pos.y());
14591504
p.translate(width() / 2.0, height() / 2.0);
14601505
p.setWorldTransform(view->worldTransform(), true);
14611506
};
14621507

14631508
// Compute map_rect from the inverse of the full composite transform
1464-
// We need a temporary transform to get the inverse
14651509
QTransform composite = pinch_xf;
14661510
composite.translate(width() / 2.0, height() / 2.0);
14671511
composite = view->worldTransform() * composite;
14681512
bool invertible = false;
14691513
QTransform inv = composite.inverted(&invertible);
14701514
if (!invertible)
1471-
return;
1515+
return false;
14721516
QRectF map_rect = inv.mapRect(QRectF(exposed));
14731517

14741518
double ppm = view->calculateFinalZoomFactor() * pinching_factor;
@@ -1495,27 +1539,30 @@ void MapWidget::drawPinchUncoveredRegion(QPainter& painter, const QRect& exposed
14951539
painter.fillRect(exposed, Qt::white);
14961540
}
14971541

1498-
// Map objects
1499-
const auto map_visibility = view->effectiveMapVisibility();
1500-
if (map_visibility.visible)
1542+
// Map objects (only at full rendering level)
1543+
if (rendering_level >= 2)
15011544
{
1502-
painter.save();
1503-
qreal saved_opacity = painter.opacity();
1504-
painter.setOpacity(map_visibility.opacity);
1505-
1506-
RenderConfig::Options options(RenderConfig::Screen | RenderConfig::HelperSymbols);
1507-
bool use_antialiasing = force_antialiasing || Settings::getInstance().getSettingCached(Settings::MapDisplay_Antialiasing).toBool();
1508-
if (use_antialiasing)
1509-
painter.setRenderHint(QPainter::Antialiasing);
1510-
else
1511-
options |= RenderConfig::DisableAntialiasing | RenderConfig::ForceMinSize;
1545+
const auto map_visibility = view->effectiveMapVisibility();
1546+
if (map_visibility.visible)
1547+
{
1548+
painter.save();
1549+
qreal saved_opacity = painter.opacity();
1550+
painter.setOpacity(map_visibility.opacity);
1551+
1552+
RenderConfig::Options options(RenderConfig::Screen | RenderConfig::HelperSymbols);
1553+
bool use_antialiasing = force_antialiasing || Settings::getInstance().getSettingCached(Settings::MapDisplay_Antialiasing).toBool();
1554+
if (use_antialiasing)
1555+
painter.setRenderHint(QPainter::Antialiasing);
1556+
else
1557+
options |= RenderConfig::DisableAntialiasing | RenderConfig::ForceMinSize;
15121558

1513-
setupTransform(painter);
1514-
RenderConfig config = { *map, map_rect, ppm, options, 1.0 };
1515-
map->draw(&painter, config);
1559+
setupTransform(painter);
1560+
RenderConfig config = { *map, map_rect, ppm, options, 1.0 };
1561+
map->draw(&painter, config);
15161562

1517-
painter.setOpacity(saved_opacity);
1518-
painter.restore();
1563+
painter.setOpacity(saved_opacity);
1564+
painter.restore();
1565+
}
15191566
}
15201567

15211568
// Above templates
@@ -1529,17 +1576,22 @@ void MapWidget::drawPinchUncoveredRegion(QPainter& painter, const QRect& exposed
15291576
}
15301577

15311578
painter.restore();
1579+
return true;
15321580
}
15331581

1534-
void MapWidget::drawPanUncoveredRegion(QPainter& painter, const QRect& exposed) const
1582+
bool MapWidget::drawPanUncoveredRegion(QPainter& painter, const QRect& exposed) const
15351583
{
15361584
Q_ASSERT(pan_offset != QPoint());
15371585

1586+
int rendering_level = Settings::getInstance().getSettingCached(Settings::MapDisplay_GestureExtraRendering).toInt();
1587+
if (rendering_level <= 0)
1588+
return false;
1589+
15381590
// The cache is drawn at target = exposed.translated(pan_offset).
15391591
// The uncovered region is what's in exposed but not in the shifted target.
15401592
QRegion uncovered = QRegion(exposed) - QRegion(exposed).translated(pan_offset);
15411593
if (uncovered.isEmpty())
1542-
return;
1594+
return true;
15431595

15441596
// Use the same composite transform as the panned cache:
15451597
// translate(pan_offset) * translate(w/2, h/2) * worldTransform
@@ -1557,7 +1609,7 @@ void MapWidget::drawPanUncoveredRegion(QPainter& painter, const QRect& exposed)
15571609
bool invertible = false;
15581610
QTransform inv = composite.inverted(&invertible);
15591611
if (!invertible)
1560-
return;
1612+
return false;
15611613
QRectF map_rect = inv.mapRect(QRectF(exposed));
15621614

15631615
double ppm = view->calculateFinalZoomFactor();
@@ -1583,27 +1635,30 @@ void MapWidget::drawPanUncoveredRegion(QPainter& painter, const QRect& exposed)
15831635
painter.fillRect(exposed, Qt::white);
15841636
}
15851637

1586-
// Map objects
1587-
const auto map_visibility = view->effectiveMapVisibility();
1588-
if (map_visibility.visible)
1638+
// Map objects (only at full rendering level)
1639+
if (rendering_level >= 2)
15891640
{
1590-
painter.save();
1591-
qreal saved_opacity = painter.opacity();
1592-
painter.setOpacity(map_visibility.opacity);
1593-
1594-
RenderConfig::Options options(RenderConfig::Screen | RenderConfig::HelperSymbols);
1595-
bool use_antialiasing = force_antialiasing || Settings::getInstance().getSettingCached(Settings::MapDisplay_Antialiasing).toBool();
1596-
if (use_antialiasing)
1597-
painter.setRenderHint(QPainter::Antialiasing);
1598-
else
1599-
options |= RenderConfig::DisableAntialiasing | RenderConfig::ForceMinSize;
1641+
const auto map_visibility = view->effectiveMapVisibility();
1642+
if (map_visibility.visible)
1643+
{
1644+
painter.save();
1645+
qreal saved_opacity = painter.opacity();
1646+
painter.setOpacity(map_visibility.opacity);
1647+
1648+
RenderConfig::Options options(RenderConfig::Screen | RenderConfig::HelperSymbols);
1649+
bool use_antialiasing = force_antialiasing || Settings::getInstance().getSettingCached(Settings::MapDisplay_Antialiasing).toBool();
1650+
if (use_antialiasing)
1651+
painter.setRenderHint(QPainter::Antialiasing);
1652+
else
1653+
options |= RenderConfig::DisableAntialiasing | RenderConfig::ForceMinSize;
16001654

1601-
setupTransform(painter);
1602-
RenderConfig config = { *map, map_rect, ppm, options, 1.0 };
1603-
map->draw(&painter, config);
1655+
setupTransform(painter);
1656+
RenderConfig config = { *map, map_rect, ppm, options, 1.0 };
1657+
map->draw(&painter, config);
16041658

1605-
painter.setOpacity(saved_opacity);
1606-
painter.restore();
1659+
painter.setOpacity(saved_opacity);
1660+
painter.restore();
1661+
}
16071662
}
16081663

16091664
// Above templates
@@ -1617,6 +1672,7 @@ void MapWidget::drawPanUncoveredRegion(QPainter& painter, const QRect& exposed)
16171672
}
16181673

16191674
painter.restore();
1675+
return true;
16201676
}
16211677

16221678
void MapWidget::updateTemplateCache(QImage& cache, QRect& dirty_rect, TemplateCacheViewState& state, int first_template, int last_template, bool use_background)

0 commit comments

Comments
 (0)