QGIS API Documentation 3.41.0-Master (3440c17df1d)
Loading...
Searching...
No Matches
qgslocatorwidget.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgslocatorwidget.cpp
3 --------------------
4 begin : May 2017
5 copyright : (C) 2017 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18#include "qgslocator.h"
19#include "qgslocatormodel.h"
20#include "qgslocatorwidget.h"
21#include "moc_qgslocatorwidget.cpp"
23#include "qgsfilterlineedit.h"
24#include "qgsmapcanvas.h"
25#include "qgsapplication.h"
26#include "qgslogger.h"
27#include "qgsguiutils.h"
28
29#include <QLayout>
30#include <QCompleter>
31#include <QMenu>
32#include <QTextLayout>
33#include <QTextLine>
34
36 : QWidget( parent )
37 , mModelBridge( new QgsLocatorModelBridge( this ) )
38 , mLineEdit( new QgsLocatorLineEdit( this ) )
39 , mResultsView( new QgsLocatorResultsView() )
40{
41 setObjectName( QStringLiteral( "LocatorWidget" ) );
42 mLineEdit->setShowClearButton( true );
43#ifdef Q_OS_MACOS
44 mLineEdit->setPlaceholderText( tr( "Type to locate (⌘K)" ) );
45#else
46 mLineEdit->setPlaceholderText( tr( "Type to locate (Ctrl+K)" ) );
47#endif
48
49 int placeholderMinWidth = mLineEdit->fontMetrics().boundingRect( mLineEdit->placeholderText() ).width();
50 int minWidth = std::max( 200, static_cast< int >( placeholderMinWidth * 1.8 ) );
51 resize( minWidth, 30 );
52 QSizePolicy sizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Preferred );
53 sizePolicy.setHorizontalStretch( 0 );
54 sizePolicy.setVerticalStretch( 0 );
55 setSizePolicy( sizePolicy );
56 setMinimumSize( QSize( minWidth, 0 ) );
57
58 QHBoxLayout *layout = new QHBoxLayout();
59 layout->setContentsMargins( 0, 0, 0, 0 );
60 layout->addWidget( mLineEdit );
61 setLayout( layout );
62
63 setFocusProxy( mLineEdit );
64
65 // setup floating container widget
66 mResultsContainer = new QgsFloatingWidget( parent ? parent->window() : nullptr );
67 mResultsContainer->setAnchorWidget( mLineEdit );
70
71 QHBoxLayout *containerLayout = new QHBoxLayout();
72 containerLayout->setContentsMargins( 0, 0, 0, 0 );
73 containerLayout->addWidget( mResultsView );
74 mResultsContainer->setLayout( containerLayout );
75 mResultsContainer->hide();
76
77 mResultsView->setModel( mModelBridge->proxyModel() );
78 mResultsView->setUniformRowHeights( true );
79
80 int iconSize = QgsGuiUtils::scaleIconSize( 16 );
81 mResultsView->setIconSize( QSize( iconSize, iconSize ) );
82 mResultsView->recalculateSize();
83 mResultsView->setContextMenuPolicy( Qt::CustomContextMenu );
84
85 connect( mLineEdit, &QLineEdit::textChanged, this, &QgsLocatorWidget::scheduleDelayedPopup );
86 connect( mResultsView, &QAbstractItemView::activated, this, &QgsLocatorWidget::acceptCurrentEntry );
87 connect( mResultsView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &QgsLocatorWidget::selectionChanged );
88 connect( mResultsView, &QAbstractItemView::customContextMenuRequested, this, &QgsLocatorWidget::showContextMenu );
89
90 connect( mModelBridge, &QgsLocatorModelBridge::resultAdded, this, &QgsLocatorWidget::resultAdded );
91 connect( mModelBridge, &QgsLocatorModelBridge::isRunningChanged, this, [ = ]() {mLineEdit->setShowSpinner( mModelBridge->isRunning() );} );
92 connect( mModelBridge, &QgsLocatorModelBridge::resultsCleared, this, [ = ]() {mHasSelectedResult = false;} );
93
94 // have a tiny delay between typing text in line edit and showing the window
95 mPopupTimer.setInterval( 100 );
96 mPopupTimer.setSingleShot( true );
97 connect( &mPopupTimer, &QTimer::timeout, this, &QgsLocatorWidget::performSearch );
98 mFocusTimer.setInterval( 110 );
99 mFocusTimer.setSingleShot( true );
100 connect( &mFocusTimer, &QTimer::timeout, this, &QgsLocatorWidget::triggerSearchAndShowList );
101
102 mLineEdit->installEventFilter( this );
103 mResultsContainer->installEventFilter( this );
104 mResultsView->installEventFilter( this );
105 installEventFilter( this );
106 window()->installEventFilter( this );
107
108 mModelBridge->locator()->registerFilter( new QgsLocatorFilterFilter( this, this ) );
109
110 mMenu = new QMenu( this );
111 QAction *menuAction = mLineEdit->addAction( QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) ), QLineEdit::LeadingPosition );
112 connect( menuAction, &QAction::triggered, this, [ = ]
113 {
114 mFocusTimer.stop();
115 mResultsContainer->hide();
116 mMenu->exec( QCursor::pos() );
117 } );
118 connect( mMenu, &QMenu::aboutToShow, this, &QgsLocatorWidget::configMenuAboutToShow );
119
120 mModelBridge->setTransformContext( QgsProject::instance()->transformContext() );
122 this, [ = ]
123 {
124 mModelBridge->setTransformContext( QgsProject::instance()->transformContext() );
125 } );
126}
127
129{
130 return mModelBridge->locator();
131}
132
134{
135 if ( mMapCanvas == canvas )
136 return;
137
138 for ( const QMetaObject::Connection &conn : std::as_const( mCanvasConnections ) )
139 {
140 disconnect( conn );
141 }
142 mCanvasConnections.clear();
143
144 mMapCanvas = canvas;
145 if ( mMapCanvas )
146 {
147 mModelBridge->updateCanvasExtent( mMapCanvas->mapSettings().visibleExtent() );
148 mModelBridge->updateCanvasCrs( mMapCanvas->mapSettings().destinationCrs() );
149 mCanvasConnections
150 << connect( mMapCanvas, &QgsMapCanvas::extentsChanged, this, [ = ]() {mModelBridge->updateCanvasExtent( mMapCanvas->mapSettings().visibleExtent() );} )
151 << connect( mMapCanvas, &QgsMapCanvas::destinationCrsChanged, this, [ = ]() {mModelBridge->updateCanvasCrs( mMapCanvas->mapSettings().destinationCrs() );} ) ;
152 }
153}
154
155void QgsLocatorWidget::setPlaceholderText( const QString &text )
156{
157 mLineEdit->setPlaceholderText( text );
158}
159
161{
162 mResultsContainer->setAnchorPoint( anchorPoint );
163 mResultsContainer->setAnchorWidgetPoint( anchorWidgetPoint );
164}
165
166void QgsLocatorWidget::search( const QString &string )
167{
168 window()->activateWindow(); // window must also be active - otherwise floating docks can steal keystrokes
169 if ( string.isEmpty() )
170 {
171 mLineEdit->setFocus();
172 mLineEdit->selectAll();
173 }
174 else
175 {
176 scheduleDelayedPopup();
177 mLineEdit->setFocus();
178 mLineEdit->setText( string );
179 performSearch();
180 }
181}
182
184{
185 mModelBridge->invalidateResults();
186 mResultsContainer->hide();
187}
188
189void QgsLocatorWidget::scheduleDelayedPopup()
190{
191 mPopupTimer.start();
192}
193
194void QgsLocatorWidget::resultAdded()
195{
196 bool selectFirst = !mHasSelectedResult || mModelBridge->proxyModel()->rowCount() == 0;
197 if ( selectFirst )
198 {
199 int row = -1;
200 bool selectable = false;
201 while ( !selectable && row < mModelBridge->proxyModel()->rowCount() )
202 {
203 row++;
204 selectable = mModelBridge->proxyModel()->flags( mModelBridge->proxyModel()->index( row, 0 ) ).testFlag( Qt::ItemIsSelectable );
205 }
206 if ( selectable )
207 mResultsView->setCurrentIndex( mModelBridge->proxyModel()->index( row, 0 ) );
208 }
209}
210
211void QgsLocatorWidget::showContextMenu( const QPoint &point )
212{
213 QModelIndex index = mResultsView->indexAt( point );
214 if ( !index.isValid() )
215 return;
216
217 const QList<QgsLocatorResult::ResultAction> actions = mResultsView->model()->data( index, static_cast< int >( QgsLocatorModel::CustomRole::ResultActions ) ).value<QList<QgsLocatorResult::ResultAction>>();
218 QMenu *contextMenu = new QMenu( mResultsView );
219 for ( auto resultAction : actions )
220 {
221 QAction *menuAction = new QAction( resultAction.text, contextMenu );
222 if ( !resultAction.iconPath.isEmpty() )
223 menuAction->setIcon( QIcon( resultAction.iconPath ) );
224 connect( menuAction, &QAction::triggered, this, [ = ]() {mModelBridge->triggerResult( index, resultAction.id );} );
225 contextMenu->addAction( menuAction );
226 }
227 contextMenu->exec( mResultsView->viewport()->mapToGlobal( point ) );
228}
229
230void QgsLocatorWidget::performSearch()
231{
232 mPopupTimer.stop();
233 mModelBridge->performSearch( mLineEdit->text() );
234 showList();
235}
236
237void QgsLocatorWidget::showList()
238{
239 mResultsContainer->show();
240 mResultsContainer->raise();
241}
242
243void QgsLocatorWidget::triggerSearchAndShowList()
244{
245 if ( mModelBridge->proxyModel()->rowCount() == 0 )
246 performSearch();
247 else
248 showList();
249}
250
251bool QgsLocatorWidget::eventFilter( QObject *obj, QEvent *event )
252{
253 if ( obj == mLineEdit && event->type() == QEvent::KeyPress )
254 {
255 QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
256 switch ( keyEvent->key() )
257 {
258 case Qt::Key_Up:
259 case Qt::Key_Down:
260 case Qt::Key_PageUp:
261 case Qt::Key_PageDown:
262 triggerSearchAndShowList();
263 mHasSelectedResult = true;
264 QgsApplication::sendEvent( mResultsView, event );
265 return true;
266 case Qt::Key_Home:
267 case Qt::Key_End:
268 if ( keyEvent->modifiers() & Qt::ControlModifier )
269 {
270 triggerSearchAndShowList();
271 mHasSelectedResult = true;
272 QgsApplication::sendEvent( mResultsView, event );
273 return true;
274 }
275 break;
276 case Qt::Key_Enter:
277 case Qt::Key_Return:
278 acceptCurrentEntry();
279 return true;
280 case Qt::Key_Escape:
281 mResultsContainer->hide();
282 return true;
283 case Qt::Key_Tab:
284 if ( !mLineEdit->performCompletion() )
285 {
286 mHasSelectedResult = true;
287 mResultsView->selectNextResult();
288 }
289 return true;
290 case Qt::Key_Backtab:
291 mHasSelectedResult = true;
292 mResultsView->selectPreviousResult();
293 return true;
294 default:
295 break;
296 }
297 }
298 else if ( obj == mResultsView && event->type() == QEvent::MouseButtonPress )
299 {
300 mHasSelectedResult = true;
301 }
302 else if ( event->type() == QEvent::FocusOut && ( obj == mLineEdit || obj == mResultsContainer || obj == mResultsView ) )
303 {
304 if ( !mLineEdit->hasFocus() && !mResultsContainer->hasFocus() && !mResultsView->hasFocus() )
305 {
306 mFocusTimer.stop();
307 mResultsContainer->hide();
308 }
309 }
310 else if ( event->type() == QEvent::FocusIn && obj == mLineEdit )
311 {
312 mFocusTimer.start();
313 }
314 else if ( obj == window() && event->type() == QEvent::Resize )
315 {
316 mResultsView->recalculateSize();
317 }
318 return QWidget::eventFilter( obj, event );
319}
320
321void QgsLocatorWidget::configMenuAboutToShow()
322{
323 mMenu->clear();
324 for ( QgsLocatorFilter *filter : mModelBridge->locator()->filters() )
325 {
326 if ( !filter->enabled() )
327 continue;
328
329 QAction *action = new QAction( filter->displayName(), mMenu );
330 connect( action, &QAction::triggered, this, [ = ]
331 {
332 QString currentText = mLineEdit->text();
333 if ( currentText.isEmpty() )
334 currentText = tr( "<type here>" );
335 else
336 {
337 QStringList parts = currentText.split( ' ' );
338 if ( parts.count() > 1 && mModelBridge->locator()->filters( parts.at( 0 ) ).count() > 0 )
339 {
340 parts.pop_front();
341 currentText = parts.join( ' ' );
342 }
343 }
344
345 mLineEdit->setText( filter->activePrefix() + ' ' + currentText );
346 mLineEdit->setSelection( filter->activePrefix().length() + 1, currentText.length() );
347 } );
348 mMenu->addAction( action );
349 }
350 mMenu->addSeparator();
351 QAction *configAction = new QAction( tr( "Configure…" ), mMenu );
352 connect( configAction, &QAction::triggered, this, &QgsLocatorWidget::configTriggered );
353 mMenu->addAction( configAction );
354}
355
356
357void QgsLocatorWidget::acceptCurrentEntry()
358{
359 if ( mModelBridge->hasQueueRequested() )
360 {
361 return;
362 }
363 else
364 {
365 if ( !mResultsView->isVisible() )
366 return;
367
368 QModelIndex index = mResultsView->currentIndex();
369 if ( !index.isValid() )
370 return;
371
372 mResultsContainer->hide();
373 mLineEdit->clearFocus();
374 mModelBridge->triggerResult( index );
375 }
376}
377
378void QgsLocatorWidget::selectionChanged( const QItemSelection &selected, const QItemSelection &deselected )
379{
380 if ( !mResultsView->isVisible() )
381 return;
382
383 mModelBridge->selectionChanged( selected, deselected );
384}
385
387
388//
389// QgsLocatorResultsView
390//
391
392QgsLocatorResultsView::QgsLocatorResultsView( QWidget *parent )
393 : QTreeView( parent )
394{
395 setRootIsDecorated( false );
396 setUniformRowHeights( true );
397 header()->hide();
398 header()->setStretchLastSection( true );
399}
400
401void QgsLocatorResultsView::recalculateSize()
402{
403 QStyleOptionViewItem optView;
404#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
405 optView.init( this );
406#else
407 optView.initFrom( this );
408#endif
409
410 // try to show about 20 rows
411 int rowSize = 20 * itemDelegate()->sizeHint( optView, model()->index( 0, 0 ) ).height();
412
413 // try to take up a sensible portion of window width (about half)
414 int width = std::max( 300, window()->size().width() / 2 );
415 QSize newSize( width, rowSize + frameWidth() * 2 );
416 // resize the floating widget this is contained within
417 parentWidget()->resize( newSize );
418 QTreeView::resize( newSize );
419
420 header()->resizeSection( 0, width / 2 );
421 header()->resizeSection( 1, 0 );
422}
423
424void QgsLocatorResultsView::selectNextResult()
425{
426 const int rowCount = model()->rowCount( QModelIndex() );
427 if ( rowCount == 0 )
428 return;
429
430 int nextRow = currentIndex().row() + 1;
431 nextRow = nextRow % rowCount;
432 setCurrentIndex( model()->index( nextRow, 0 ) );
433}
434
435void QgsLocatorResultsView::selectPreviousResult()
436{
437 const int rowCount = model()->rowCount( QModelIndex() );
438 if ( rowCount == 0 )
439 return;
440
441 int previousRow = currentIndex().row() - 1;
442 if ( previousRow < 0 )
443 previousRow = rowCount - 1;
444 setCurrentIndex( model()->index( previousRow, 0 ) );
445}
446
447//
448// QgsLocatorFilterFilter
449//
450
451QgsLocatorFilterFilter::QgsLocatorFilterFilter( QgsLocatorWidget *locator, QObject *parent )
452 : QgsLocatorFilter( parent )
453 , mLocator( locator )
454{}
455
456QgsLocatorFilterFilter *QgsLocatorFilterFilter::clone() const
457{
458 return new QgsLocatorFilterFilter( mLocator );
459}
460
461QgsLocatorFilter::Flags QgsLocatorFilterFilter::flags() const
462{
464}
465
466void QgsLocatorFilterFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
467{
468 if ( !string.isEmpty() )
469 {
470 //only shows results when nothing typed
471 return;
472 }
473
474 for ( QgsLocatorFilter *filter : mLocator->locator()->filters() )
475 {
476 if ( feedback->isCanceled() )
477 return;
478
479 if ( filter == this || !filter || !filter->enabled() )
480 continue;
481
482 QgsLocatorResult result;
483 result.displayString = filter->activePrefix();
484 result.description = filter->displayName();
485 result.setUserData( QString( filter->activePrefix() + ' ' ) );
486 result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) );
487 emit resultFetched( result );
488 }
489}
490
491void QgsLocatorFilterFilter::triggerResult( const QgsLocatorResult &result )
492{
493 mLocator->search( result.userData().toString() );
494}
495
496QgsLocatorLineEdit::QgsLocatorLineEdit( QgsLocatorWidget *locator, QWidget *parent )
497 : QgsFilterLineEdit( parent )
498 , mLocatorWidget( locator )
499{
500 connect( mLocatorWidget->locator(), &QgsLocator::searchPrepared, this, [&] { update(); } );
501}
502
503void QgsLocatorLineEdit::paintEvent( QPaintEvent *event )
504{
505 // this adds the completion as grey text at the right of the cursor
506 // see https://stackoverflow.com/a/50425331/1548052
507 // this is possible that the completion might be badly rendered if the cursor is larger than the line edit
508 // this sounds acceptable as it is not very likely to use completion for super long texts
509 // for more details see https://stackoverflow.com/a/54218192/1548052
510
511 QLineEdit::paintEvent( event );
512
513 if ( !hasFocus() )
514 return;
515
516 QString currentText = text();
517
518 if ( currentText.length() == 0 || cursorPosition() < currentText.length() )
519 return;
520
521 const QStringList completionList = mLocatorWidget->locator()->completionList();
522
523 mCompletionText.clear();
524 QString completion;
525 for ( const QString &candidate : completionList )
526 {
527 if ( candidate.startsWith( currentText ) )
528 {
529 completion = candidate.right( candidate.length() - currentText.length() );
530 mCompletionText = candidate;
531 break;
532 }
533 }
534
535 if ( completion.isEmpty() )
536 return;
537
538 ensurePolished(); // ensure font() is up to date
539
540 QRect cr = cursorRect();
541 QPoint pos = cr.topRight() - QPoint( cr.width() / 2, 0 );
542
543 QTextLayout l( completion, font() );
544 l.beginLayout();
545 QTextLine line = l.createLine();
546 line.setLineWidth( width() - pos.x() );
547 line.setPosition( pos );
548 l.endLayout();
549
550 QPainter p( this );
551 p.setPen( QPen( Qt::gray, 1 ) );
552 l.draw( &p, QPoint( 0, 0 ) );
553}
554
555bool QgsLocatorLineEdit::performCompletion()
556{
557 if ( !mCompletionText.isEmpty() )
558 {
559 setText( mCompletionText );
560 mCompletionText.clear();
561 return true;
562 }
563 else
564 return false;
565}
566
567
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition qgsfeedback.h:44
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:53
QLineEdit subclass with built in support for clearing the widget's value and handling custom null val...
A QWidget subclass for creating widgets which float outside of the normal Qt layout system.
void setAnchorWidget(QWidget *widget)
Sets the widget to "anchor" the floating widget to.
void setAnchorWidgetPoint(AnchorPoint point)
Returns the anchor widget's anchor point, which corresponds to the point on the anchor widget which t...
AnchorPoint
Reference points for anchoring widget position.
@ BottomLeft
Bottom-left of widget.
@ TopLeft
Top-left of widget.
void setAnchorPoint(AnchorPoint point)
Sets the floating widget's anchor point, which corresponds to the point on the widget which should re...
Encapsulates the properties relating to the context of a locator search.
Abstract base class for filters which collect locator results.
QFlags< Flag > Flags
@ FlagFast
Filter finds results quickly and can be safely run in the main thread.
The QgsLocatorModelBridge class provides the core functionality to be used in a locator widget.
Q_INVOKABLE QgsLocatorProxyModel * proxyModel() const
Returns the proxy model.
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
This will call filters implementation of selection/deselection of results.
void isRunningChanged()
Emitted when the running status changes.
void resultAdded()
Emitted when a result is added.
void triggerResult(const QModelIndex &index, const int actionId=-1)
Triggers the result at given index and with optional actionId if an additional action was triggered.
void setTransformContext(const QgsCoordinateTransformContext &context)
Sets the coordinate transform context, which should be used whenever the locator constructs a coordin...
QgsLocator * locator() const
Returns the locator.
bool hasQueueRequested() const
Returns true if some text to be search is pending in the queue.
Q_INVOKABLE void performSearch(const QString &text)
Perform a search.
void resultsCleared()
Emitted when the results are cleared.
void updateCanvasCrs(const QgsCoordinateReferenceSystem &crs)
Update the canvas CRS used to create search context.
void updateCanvasExtent(const QgsRectangle &extent)
Update the canvas extent used to create search context.
void invalidateResults()
This will invalidate current search results.
@ ResultActions
The actions to be shown for the given result in a context menu.
Encapsulates properties of an individual matching result found by a QgsLocatorFilter.
QString description
Descriptive text for result.
void setUserData(const QVariant &userData)
Set userData for the locator result.
QString displayString
String displayed for result.
QIcon icon
Icon for result.
A special locator widget which allows searching for matching results from a QgsLocator and presenting...
void setResultContainerAnchors(QgsFloatingWidget::AnchorPoint anchorPoint, QgsFloatingWidget::AnchorPoint anchorWidgetPoint)
Sets the result container anchorPoint and anchorWidgetPoint position.
void configTriggered()
Emitted when the configure option is triggered in the widget.
QgsLocatorWidget(QWidget *parent SIP_TRANSFERTHIS=nullptr)
Constructor for QgsLocatorWidget.
void setPlaceholderText(const QString &text)
Set placeholder text for the line edit.
void setMapCanvas(QgsMapCanvas *canvas)
Sets a map canvas to associate with the widget.
void search(const QString &string)
Triggers the locator widget to focus, open and start searching for a specified string.
bool eventFilter(QObject *obj, QEvent *event) override
void invalidateResults()
Invalidates the current search results, e.g.
QgsLocator * locator()
Returns a pointer to the locator utilized by this widget.
Handles the management of QgsLocatorFilter objects and async collection of search results from them.
Definition qgslocator.h:61
void searchPrepared()
Emitted when locator has prepared the search (.
void registerFilter(QgsLocatorFilter *filter)
Registers a filter within the locator.
QList< QgsLocatorFilter * > filters(const QString &prefix=QString())
Returns the list of filters registered in the locator.
Map canvas is a class for displaying all GIS data types on a canvas.
void extentsChanged()
Emitted when the extents of the map change.
void destinationCrsChanged()
Emitted when map CRS has changed.
const QgsMapSettings & mapSettings() const
Gets access to properties used for map rendering.
QgsRectangle visibleExtent() const
Returns the actual extent derived from requested extent that takes output image size into account.
QgsCoordinateReferenceSystem destinationCrs() const
Returns the destination coordinate reference system for the map render.
static QgsProject * instance()
Returns the QgsProject singleton instance.
void transformContextChanged()
Emitted when the project transformContext() is changed.
int scaleIconSize(int standardSize)
Scales an icon size to compensate for display pixel density, making the icon size hi-dpi friendly,...