1 /***************************************************************************
2     qgsmapcanvasdockwidget.cpp
3     --------------------------
4     begin                : February 2017
5     copyright            : (C) 2017 by Nyall Dawson
6     email                : nyall dot dawson at gmail dot com
7  ***************************************************************************
8  *                                                                         *
9  *   This program is free software; you can redistribute it and/or modify  *
10  *   it under the terms of the GNU General Public License as published by  *
11  *   the Free Software Foundation; either version 2 of the License, or     *
12  *   (at your option) any later version.                                   *
13  *                                                                         *
14  ***************************************************************************/
15 #include "qgsmapcanvasdockwidget.h"
16 #include "qgsmapcanvas.h"
17 #include "qgsexception.h"
18 #include "qgsprojectionselectiondialog.h"
19 #include "qgsscalecombobox.h"
20 #include "qgsdoublespinbox.h"
21 #include "qgssettings.h"
22 #include "qgsmaptoolpan.h"
23 #include "qgsmapthemecollection.h"
24 #include "qgsproject.h"
25 #include "qgsmapthemes.h"
26 #include "qgslayertreeview.h"
27 #include "qgslayertreeviewdefaultactions.h"
28 #include "qgisapp.h"
29 #include "qgsvertexmarker.h"
30 #include "qgsrubberband.h"
31 #include "qgsvectorlayer.h"
32 #include "qgsapplication.h"
33 #include <QMessageBox>
34 #include <QMenu>
35 #include <QToolBar>
36 #include <QToolButton>
37 #include <QRadioButton>
38 
39 
QgsMapCanvasDockWidget(const QString & name,QWidget * parent)40 QgsMapCanvasDockWidget::QgsMapCanvasDockWidget( const QString &name, QWidget *parent )
41   : QgsDockWidget( parent )
42 {
43   setupUi( this );
44   setAttribute( Qt::WA_DeleteOnClose );
45 
46   mContents->layout()->setContentsMargins( 0, 0, 0, 0 );
47   static_cast< QVBoxLayout * >( mContents->layout() )->setSpacing( 0 );
48 
49   setWindowTitle( name );
50   mToolbar->setIconSize( QgisApp::instance()->iconSize( true ) );
51 
52   mMapCanvas = new QgsMapCanvas( this );
53   mXyMarker = new QgsVertexMarker( mMapCanvas );
54   mXyMarker->setIconType( QgsVertexMarker::ICON_CIRCLE );
55   mXyMarker->setIconSize( 6 );
56   mXyMarker->setColor( QColor( 30, 30, 30, 225 ) );
57   mXyMarker->setFillColor( QColor( 255, 255, 255, 225 ) );
58 
59   mExtentRubberBand = new QgsRubberBand( mMapCanvas, QgsWkbTypes::PolygonGeometry );
60   mExtentRubberBand->setStrokeColor( Qt::red );
61   mExtentRubberBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 225 ) );
62   mExtentRubberBand->setFillColor( Qt::transparent );
63 
64   mPanTool = new QgsMapToolPan( mMapCanvas );
65   mMapCanvas->setMapTool( mPanTool );
66 
67   mMainWidget->setLayout( new QVBoxLayout() );
68   mMainWidget->layout()->setContentsMargins( 0, 0, 0, 0 );
69 
70   mMainWidget->layout()->addWidget( mMapCanvas );
71 
72   mMenu = new QMenu();
73   connect( mMenu, &QMenu::aboutToShow, this, &QgsMapCanvasDockWidget::menuAboutToShow );
74 
75   mToolbar->addSeparator();
76 
77   QToolButton *btnMapThemes = new QToolButton;
78   btnMapThemes->setAutoRaise( true );
79   btnMapThemes->setToolTip( tr( "Set View Theme" ) );
80   btnMapThemes->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionShowAllLayers.svg" ) ) );
81   btnMapThemes->setPopupMode( QToolButton::InstantPopup );
82   btnMapThemes->setMenu( mMenu );
83   mToolbar->addWidget( btnMapThemes );
84 
85   QMenu *settingsMenu = new QMenu();
86   QToolButton *settingsButton = new QToolButton();
87   settingsButton->setAutoRaise( true );
88   settingsButton->setToolTip( tr( "View Settings" ) );
89   settingsButton->setMenu( settingsMenu );
90   settingsButton->setPopupMode( QToolButton::InstantPopup );
91   settingsButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionOptions.svg" ) ) );
92   mToolbar->addWidget( settingsButton );
93 
94   connect( mActionSetCrs, &QAction::triggered, this, &QgsMapCanvasDockWidget::setMapCrs );
95   connect( mMapCanvas, &QgsMapCanvas::destinationCrsChanged, this, &QgsMapCanvasDockWidget::mapCrsChanged );
96   connect( mMapCanvas, &QgsMapCanvas::destinationCrsChanged, this, &QgsMapCanvasDockWidget::updateExtentRect );
97   connect( mActionZoomFullExtent, &QAction::triggered, mMapCanvas, &QgsMapCanvas::zoomToProjectExtent );
98   connect( mActionZoomToLayers, &QAction::triggered, mMapCanvas, [ = ] { QgisApp::instance()->layerTreeView()->defaultActions()->zoomToLayers( mMapCanvas ); } );
99   connect( mActionZoomToSelected, &QAction::triggered, mMapCanvas, [ = ] { mMapCanvas->zoomToSelected(); } );
100   mapCrsChanged();
101 
102   QgsMapSettingsAction *settingsAction = new QgsMapSettingsAction( settingsMenu );
103   settingsMenu->addAction( settingsAction );
104 
105   settingsMenu->addSeparator();
106   settingsMenu->addAction( mActionShowAnnotations );
107   settingsMenu->addAction( mActionShowCursor );
108   settingsMenu->addAction( mActionShowExtent );
109   settingsMenu->addAction( mActionShowLabels );
110   settingsMenu->addSeparator();
111   settingsMenu->addAction( mActionSetCrs );
112   settingsMenu->addAction( mActionRename );
113 
114   connect( settingsMenu, &QMenu::aboutToShow, this, &QgsMapCanvasDockWidget::settingsMenuAboutToShow );
115 
116   connect( mActionRename, &QAction::triggered, this, &QgsMapCanvasDockWidget::renameTriggered );
117   mActionShowAnnotations->setChecked( mMapCanvas->annotationsVisible() );
118   connect( mActionShowAnnotations, &QAction::toggled, this, [ = ]( bool checked ) { mMapCanvas->setAnnotationsVisible( checked ); } );
119   mActionShowCursor->setChecked( true );
120   connect( mActionShowCursor, &QAction::toggled, this, [ = ]( bool checked ) { mXyMarker->setVisible( checked ); } );
121   mActionShowExtent->setChecked( false );
122   connect( mActionShowExtent, &QAction::toggled, this, [ = ]( bool checked ) { mExtentRubberBand->setVisible( checked ); updateExtentRect(); } );
123   mActionShowLabels->setChecked( true );
124   connect( mActionShowLabels, &QAction::toggled, this, &QgsMapCanvasDockWidget::showLabels );
125 
126 
127   mSyncExtentRadio = settingsAction->syncExtentRadio();
128   mSyncSelectionRadio = settingsAction->syncSelectionRadio();
129   mScaleCombo = settingsAction->scaleCombo();
130   mRotationEdit = settingsAction->rotationSpinBox();
131   mMagnificationEdit = settingsAction->magnifierSpinBox();
132   mSyncScaleCheckBox = settingsAction->syncScaleCheckBox();
133   mScaleFactorWidget = settingsAction->scaleFactorSpinBox();
134 
135   connect( mSyncSelectionRadio, &QRadioButton::toggled, this, [ = ]( bool checked )
136   {
137     autoZoomToSelection( checked );
138     if ( checked )
139     {
140       syncSelection();
141     }
142   } );
143 
144   connect( mSyncExtentRadio, &QRadioButton::toggled, this, [ = ]( bool checked )
145   {
146     if ( checked )
147     {
148       syncViewCenter( mMainCanvas );
149     }
150   } );
151 
152   connect( mScaleCombo, &QgsScaleComboBox::scaleChanged, this, [ = ]( double scale )
153   {
154     if ( !mBlockScaleUpdate )
155     {
156       mBlockScaleUpdate = true;
157       mMapCanvas->zoomScale( scale );
158       mBlockScaleUpdate = false;
159     }
160   } );
161   connect( mMapCanvas, &QgsMapCanvas::scaleChanged, this, [ = ]( double scale )
162   {
163     if ( !mBlockScaleUpdate )
164     {
165       mBlockScaleUpdate = true;
166       mScaleCombo->setScale( scale );
167       mBlockScaleUpdate = false;
168     }
169   } );
170 
171   connect( mRotationEdit, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, [ = ]( double value )
172   {
173     if ( !mBlockRotationUpdate )
174     {
175       mBlockRotationUpdate = true;
176       mMapCanvas->setRotation( value );
177       mMapCanvas->refresh();
178       mBlockRotationUpdate = false;
179     }
180   } );
181 
182   connect( mMapCanvas, &QgsMapCanvas::rotationChanged, this, [ = ]( double rotation )
183   {
184     if ( !mBlockRotationUpdate )
185     {
186       mBlockRotationUpdate = true;
187       mRotationEdit->setValue( rotation );
188       mBlockRotationUpdate = false;
189     }
190   } );
191 
192   connect( mMagnificationEdit, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, [ = ]( double value )
193   {
194     if ( !mBlockMagnificationUpdate )
195     {
196       mBlockMagnificationUpdate = true;
197       mMapCanvas->setMagnificationFactor( value / 100 );
198       mMapCanvas->refresh();
199       mBlockMagnificationUpdate = false;
200     }
201   } );
202 
203   connect( mMapCanvas, &QgsMapCanvas::magnificationChanged, this, [ = ]( double factor )
204   {
205     if ( !mBlockMagnificationUpdate )
206     {
207       mBlockMagnificationUpdate = true;
208       mMagnificationEdit->setValue( factor * 100 );
209       mBlockMagnificationUpdate = false;
210     }
211   } );
212 
213   connect( mScaleFactorWidget, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, &QgsMapCanvasDockWidget::mapScaleChanged );
214   connect( mSyncScaleCheckBox, &QCheckBox::toggled, this, [ = ]( bool checked )
215   {
216     if ( checked )
217       mapScaleChanged();
218   }
219          );
220 
221   mResizeTimer.setSingleShot( true );
222   connect( &mResizeTimer, &QTimer::timeout, this, [ = ]
223   {
224     mBlockExtentSync = false;
225     if ( mSyncExtentRadio->isChecked() )
226       syncViewCenter( mMainCanvas );
227   } );
228 
229   connect( QgsProject::instance()->mapThemeCollection(), &QgsMapThemeCollection::mapThemeRenamed, this, &QgsMapCanvasDockWidget::currentMapThemeRenamed );
230 }
231 
setMainCanvas(QgsMapCanvas * canvas)232 void QgsMapCanvasDockWidget::setMainCanvas( QgsMapCanvas *canvas )
233 {
234   if ( mMainCanvas )
235   {
236     disconnect( mMainCanvas, &QgsMapCanvas::xyCoordinates, this, &QgsMapCanvasDockWidget::syncMarker );
237     disconnect( mMainCanvas, &QgsMapCanvas::scaleChanged, this, &QgsMapCanvasDockWidget::mapScaleChanged );
238     disconnect( mMainCanvas, &QgsMapCanvas::extentsChanged, this, &QgsMapCanvasDockWidget::mapExtentChanged );
239     disconnect( mMainCanvas, &QgsMapCanvas::extentsChanged, this, &QgsMapCanvasDockWidget::updateExtentRect );
240     disconnect( mMainCanvas, &QgsMapCanvas::destinationCrsChanged, this, &QgsMapCanvasDockWidget::updateExtentRect );
241   }
242 
243   mMainCanvas = canvas;
244   connect( mMainCanvas, &QgsMapCanvas::xyCoordinates, this, &QgsMapCanvasDockWidget::syncMarker );
245   connect( mMainCanvas, &QgsMapCanvas::scaleChanged, this, &QgsMapCanvasDockWidget::mapScaleChanged );
246   connect( mMainCanvas, &QgsMapCanvas::extentsChanged, this, &QgsMapCanvasDockWidget::mapExtentChanged );
247   connect( mMapCanvas, &QgsMapCanvas::extentsChanged, this, &QgsMapCanvasDockWidget::mapExtentChanged, Qt::UniqueConnection );
248   connect( mMainCanvas, &QgsMapCanvas::extentsChanged, this, &QgsMapCanvasDockWidget::updateExtentRect );
249   connect( mMainCanvas, &QgsMapCanvas::destinationCrsChanged, this, &QgsMapCanvasDockWidget::updateExtentRect );
250   updateExtentRect();
251 }
252 
mapCanvas()253 QgsMapCanvas *QgsMapCanvasDockWidget::mapCanvas()
254 {
255   return mMapCanvas;
256 }
257 
setViewCenterSynchronized(bool enabled)258 void QgsMapCanvasDockWidget::setViewCenterSynchronized( bool enabled )
259 {
260   mSyncExtentRadio->setChecked( enabled );
261 }
262 
isViewCenterSynchronized() const263 bool QgsMapCanvasDockWidget::isViewCenterSynchronized() const
264 {
265   return mSyncExtentRadio->isChecked();
266 }
267 
isAutoZoomToSelected() const268 bool QgsMapCanvasDockWidget::isAutoZoomToSelected() const
269 {
270   return mSyncSelectionRadio->isChecked();
271 }
272 
setAutoZoomToSelected(bool autoZoom)273 void QgsMapCanvasDockWidget::setAutoZoomToSelected( bool autoZoom )
274 {
275   mSyncSelectionRadio->setChecked( autoZoom );
276 }
277 
setCursorMarkerVisible(bool visible)278 void QgsMapCanvasDockWidget::setCursorMarkerVisible( bool visible )
279 {
280   mActionShowCursor->setChecked( visible );
281 }
282 
isCursorMarkerVisible() const283 bool QgsMapCanvasDockWidget::isCursorMarkerVisible() const
284 {
285   return mXyMarker->isVisible();
286 }
287 
setMainCanvasExtentVisible(bool visible)288 void QgsMapCanvasDockWidget::setMainCanvasExtentVisible( bool visible )
289 {
290   mActionShowExtent->setChecked( visible );
291 }
292 
isMainCanvasExtentVisible() const293 bool QgsMapCanvasDockWidget::isMainCanvasExtentVisible() const
294 {
295   return mExtentRubberBand->isVisible();
296 }
297 
setScaleFactor(double factor)298 void QgsMapCanvasDockWidget::setScaleFactor( double factor )
299 {
300   mScaleFactorWidget->setValue( factor );
301 }
302 
setViewScaleSynchronized(bool enabled)303 void QgsMapCanvasDockWidget::setViewScaleSynchronized( bool enabled )
304 {
305   mSyncScaleCheckBox->setChecked( enabled );
306 }
307 
isViewScaleSynchronized() const308 bool QgsMapCanvasDockWidget::isViewScaleSynchronized() const
309 {
310   return mSyncScaleCheckBox->isChecked();
311 }
312 
setLabelsVisible(bool enabled)313 void QgsMapCanvasDockWidget::setLabelsVisible( bool enabled )
314 {
315   mActionShowLabels->setChecked( enabled );
316 }
317 
labelsVisible() const318 bool QgsMapCanvasDockWidget::labelsVisible() const
319 {
320   return mActionShowLabels->isChecked();
321 }
322 
scaleFactor() const323 double QgsMapCanvasDockWidget::scaleFactor() const
324 {
325   return mScaleFactorWidget->value();
326 }
327 
resizeEvent(QResizeEvent *)328 void QgsMapCanvasDockWidget::resizeEvent( QResizeEvent * )
329 {
330   mBlockExtentSync = true;
331   mResizeTimer.start( 500 );
332 }
333 
setMapCrs()334 void QgsMapCanvasDockWidget::setMapCrs()
335 {
336   QgsProjectionSelectionDialog dlg;
337   dlg.setShowNoProjection( true );
338   dlg.setCrs( mMapCanvas->mapSettings().destinationCrs() );
339 
340   if ( dlg.exec() )
341   {
342     mMapCanvas->setDestinationCrs( dlg.crs() );
343   }
344 }
345 
syncViewCenter(QgsMapCanvas * sourceCanvas)346 void QgsMapCanvasDockWidget::syncViewCenter( QgsMapCanvas *sourceCanvas )
347 {
348   // avoid infinite recursion
349   mBlockExtentSync = true;
350 
351   QgsMapCanvas *destCanvas = sourceCanvas == mMapCanvas ? mMainCanvas : mMapCanvas;
352 
353   // reproject extent
354   const QgsCoordinateTransform ct( sourceCanvas->mapSettings().destinationCrs(),
355                                    destCanvas->mapSettings().destinationCrs(), QgsProject::instance() );
356   try
357   {
358     destCanvas->setCenter( ct.transform( sourceCanvas->center() ) );
359   }
360   catch ( QgsCsException & )
361   {
362     destCanvas->setCenter( sourceCanvas->center() );
363   }
364   destCanvas->refresh();
365 
366   mBlockExtentSync = false;
367 }
368 
syncSelection()369 void QgsMapCanvasDockWidget::syncSelection()
370 {
371   QgsVectorLayer *layer = qobject_cast<QgsVectorLayer *>( mMapCanvas->currentLayer() );
372 
373   if ( !layer )
374     return;
375 
376   mMapCanvas->zoomToSelected( layer );
377 }
378 
mapExtentChanged()379 void QgsMapCanvasDockWidget::mapExtentChanged()
380 {
381   if ( mBlockExtentSync )
382     return;
383 
384   QgsMapCanvas *sourceCanvas = qobject_cast< QgsMapCanvas * >( sender() );
385   if ( !sourceCanvas )
386     return;
387 
388   if ( sourceCanvas == mMapCanvas && mSyncScaleCheckBox->isChecked() )
389   {
390     const double newScaleFactor = mMainCanvas->scale() / mMapCanvas->scale();
391     mScaleFactorWidget->setValue( newScaleFactor );
392   }
393 
394   if ( mSyncExtentRadio->isChecked() )
395     syncViewCenter( sourceCanvas );
396 }
397 
mapCrsChanged()398 void QgsMapCanvasDockWidget::mapCrsChanged()
399 {
400   mActionSetCrs->setText( tr( "Change Map CRS (%1)…" ).arg( mMapCanvas->mapSettings().destinationCrs().isValid() ?
401                           mMapCanvas->mapSettings().destinationCrs().authid() :
402                           tr( "No projection" ) ) );
403 }
404 
menuAboutToShow()405 void QgsMapCanvasDockWidget::menuAboutToShow()
406 {
407   qDeleteAll( mMenuPresetActions );
408   mMenuPresetActions.clear();
409 
410   const QString currentTheme = mMapCanvas->theme();
411 
412   QAction *actionFollowMain = new QAction( tr( "(none)" ), mMenu );
413   actionFollowMain->setCheckable( true );
414   if ( currentTheme.isEmpty() || !QgsProject::instance()->mapThemeCollection()->hasMapTheme( currentTheme ) )
415   {
416     actionFollowMain->setChecked( true );
417   }
418   connect( actionFollowMain, &QAction::triggered, this, [ = ]
419   {
420     mMapCanvas->setTheme( QString() );
421     mMapCanvas->refresh();
422   } );
423   mMenuPresetActions.append( actionFollowMain );
424 
425   const auto constMapThemes = QgsProject::instance()->mapThemeCollection()->mapThemes();
426   for ( const QString &grpName : constMapThemes )
427   {
428     QAction *a = new QAction( grpName, mMenu );
429     a->setCheckable( true );
430     if ( grpName == currentTheme )
431     {
432       a->setChecked( true );
433     }
434     connect( a, &QAction::triggered, this, [a, this]
435     {
436       mMapCanvas->setTheme( a->text() );
437       mMapCanvas->refresh();
438     } );
439     mMenuPresetActions.append( a );
440   }
441   mMenu->addActions( mMenuPresetActions );
442 }
443 
currentMapThemeRenamed(const QString & theme,const QString & newTheme)444 void QgsMapCanvasDockWidget::currentMapThemeRenamed( const QString &theme, const QString &newTheme )
445 {
446   if ( theme == mMapCanvas->theme() )
447   {
448     mMapCanvas->setTheme( newTheme );
449   }
450 }
451 
settingsMenuAboutToShow()452 void QgsMapCanvasDockWidget::settingsMenuAboutToShow()
453 {
454   whileBlocking( mActionShowAnnotations )->setChecked( mMapCanvas->annotationsVisible() );
455 }
456 
syncMarker(const QgsPointXY & p)457 void QgsMapCanvasDockWidget::syncMarker( const QgsPointXY &p )
458 {
459   if ( !mXyMarker->isVisible() )
460     return;
461 
462   // reproject point
463   const QgsCoordinateTransform ct( mMainCanvas->mapSettings().destinationCrs(),
464                                    mMapCanvas->mapSettings().destinationCrs(), QgsProject::instance() );
465   QgsPointXY t = p;
466   try
467   {
468     t = ct.transform( p );
469   }
470   catch ( QgsCsException & )
471   {}
472 
473   mXyMarker->setCenter( t );
474 }
475 
mapScaleChanged()476 void QgsMapCanvasDockWidget::mapScaleChanged()
477 {
478   if ( !mSyncScaleCheckBox->isChecked() )
479     return;
480 
481   const double newScale = mMainCanvas->scale() / mScaleFactorWidget->value();
482   const bool prev = mBlockExtentSync;
483   mBlockExtentSync = true;
484   mMapCanvas->zoomScale( newScale );
485   mBlockExtentSync = prev;
486 }
487 
updateExtentRect()488 void QgsMapCanvasDockWidget::updateExtentRect()
489 {
490   if ( !mExtentRubberBand->isVisible() )
491     return;
492 
493   QPolygonF mainCanvasPoly = mMainCanvas->mapSettings().visiblePolygon();
494   // close polygon
495   mainCanvasPoly << mainCanvasPoly.at( 0 );
496   QgsGeometry g = QgsGeometry::fromQPolygonF( mainCanvasPoly );
497   if ( mMainCanvas->mapSettings().destinationCrs() !=
498        mMapCanvas->mapSettings().destinationCrs() )
499   {
500     // reproject extent
501     const QgsCoordinateTransform ct( mMainCanvas->mapSettings().destinationCrs(),
502                                      mMapCanvas->mapSettings().destinationCrs(), QgsProject::instance() );
503     g = g.densifyByCount( 5 );
504     try
505     {
506       g.transform( ct );
507     }
508     catch ( QgsCsException & )
509     {
510     }
511   }
512   mExtentRubberBand->setToGeometry( g, nullptr );
513 }
514 
showLabels(bool show)515 void QgsMapCanvasDockWidget::showLabels( bool show )
516 {
517   Qgis::MapSettingsFlags flags = mMapCanvas->mapSettings().flags();
518   if ( show )
519     flags = flags | Qgis::MapSettingsFlag::DrawLabeling;
520   else
521     flags = flags & ~( static_cast< int >( Qgis::MapSettingsFlag::DrawLabeling ) );
522   mMapCanvas->setMapSettingsFlags( flags );
523 }
524 
autoZoomToSelection(bool autoZoom)525 void QgsMapCanvasDockWidget::autoZoomToSelection( bool autoZoom )
526 {
527   if ( autoZoom )
528     connect( mMapCanvas, &QgsMapCanvas::selectionChanged, mMapCanvas, qOverload<QgsVectorLayer *>( &QgsMapCanvas::zoomToSelected ) );
529   else
530     disconnect( mMapCanvas, &QgsMapCanvas::selectionChanged, mMapCanvas, qOverload<QgsVectorLayer *>( &QgsMapCanvas::zoomToSelected ) );
531 }
532 
QgsMapSettingsAction(QWidget * parent)533 QgsMapSettingsAction::QgsMapSettingsAction( QWidget *parent )
534   : QWidgetAction( parent )
535 {
536   QGridLayout *gLayout = new QGridLayout();
537   gLayout->setContentsMargins( 3, 2, 3, 2 );
538 
539   mSyncExtentRadio = new QRadioButton( tr( "Synchronize View Center with Main Map" ) );
540   gLayout->addWidget( mSyncExtentRadio, 0, 0, 1, 2 );
541 
542   mSyncSelectionRadio = new QRadioButton( tr( "Synchronize View to Selection" ) );
543   gLayout->addWidget( mSyncSelectionRadio, 1, 0, 1, 2 );
544 
545   QLabel *label = new QLabel( tr( "Scale" ) );
546   gLayout->addWidget( label, 2, 0 );
547 
548   mScaleCombo = new QgsScaleComboBox();
549   gLayout->addWidget( mScaleCombo, 2, 1 );
550 
551   mRotationWidget = new QgsDoubleSpinBox();
552   mRotationWidget->setClearValue( 0.0 );
553   mRotationWidget->setKeyboardTracking( false );
554   mRotationWidget->setMaximumWidth( 120 );
555   mRotationWidget->setDecimals( 1 );
556   mRotationWidget->setRange( -180.0, 180.0 );
557   mRotationWidget->setWrapping( true );
558   mRotationWidget->setSingleStep( 5.0 );
559   mRotationWidget->setSuffix( tr( " °" ) );
560   mRotationWidget->setToolTip( tr( "Current clockwise map rotation in degrees" ) );
561 
562   label = new QLabel( tr( "Rotation" ) );
563   gLayout->addWidget( label, 3, 0 );
564   gLayout->addWidget( mRotationWidget, 3, 1 );
565 
566   const QgsSettings settings;
567   const int minimumFactor = 100 * QgsGuiUtils::CANVAS_MAGNIFICATION_MIN;
568   const int maximumFactor = 100 * QgsGuiUtils::CANVAS_MAGNIFICATION_MAX;
569   const int defaultFactor = 100 * settings.value( QStringLiteral( "/qgis/magnifier_factor_default" ), 1.0 ).toDouble();
570 
571   mMagnifierWidget = new QgsDoubleSpinBox();
572   mMagnifierWidget->setSuffix( QStringLiteral( "%" ) );
573   mMagnifierWidget->setKeyboardTracking( false );
574   mMagnifierWidget->setDecimals( 0 );
575   mMagnifierWidget->setRange( minimumFactor, maximumFactor );
576   mMagnifierWidget->setWrapping( false );
577   mMagnifierWidget->setSingleStep( 50 );
578   mMagnifierWidget->setToolTip( tr( "Magnifier level" ) );
579   mMagnifierWidget->setClearValueMode( QgsDoubleSpinBox::CustomValue );
580   mMagnifierWidget->setClearValue( defaultFactor );
581   mMagnifierWidget->setValue( defaultFactor );
582 
583   label = new QLabel( tr( "Magnification" ) );
584   gLayout->addWidget( label, 4, 0 );
585   gLayout->addWidget( mMagnifierWidget, 4, 1 );
586 
587   mSyncScaleCheckBox = new QCheckBox( tr( "Synchronize scale" ) );
588   gLayout->addWidget( mSyncScaleCheckBox, 5, 0, 1, 2 );
589 
590   mScaleFactorWidget = new QgsDoubleSpinBox();
591   mScaleFactorWidget->setSuffix( tr( "×" ) );
592   mScaleFactorWidget->setDecimals( 2 );
593   mScaleFactorWidget->setRange( 0.01, 100000 );
594   mScaleFactorWidget->setWrapping( false );
595   mScaleFactorWidget->setSingleStep( 0.1 );
596   mScaleFactorWidget->setToolTip( tr( "Multiplication factor for main canvas scale to view scale" ) );
597   mScaleFactorWidget->setClearValueMode( QgsDoubleSpinBox::CustomValue );
598   mScaleFactorWidget->setClearValue( 1.0 );
599   mScaleFactorWidget->setValue( 1.0 );
600   mScaleFactorWidget->setEnabled( false );
601 
602   connect( mSyncScaleCheckBox, &QCheckBox::toggled, mScaleFactorWidget, &QgsDoubleSpinBox::setEnabled );
603 
604   label = new QLabel( tr( "Scale Factor" ) );
605   gLayout->addWidget( label, 6, 0 );
606   gLayout->addWidget( mScaleFactorWidget, 6, 1 );
607 
608   QWidget *w = new QWidget();
609   w->setLayout( gLayout );
610   setDefaultWidget( w );
611 }
612