QGIS API Documentation 3.39.0-Master (d85f3c2a281)
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"
22#include "qgsfilterlineedit.h"
23#include "qgsmapcanvas.h"
24#include "qgsapplication.h"
25#include "qgslogger.h"
26#include "qgsguiutils.h"
27
28#include <QLayout>
29#include <QCompleter>
30#include <QMenu>
31#include <QTextLayout>
32#include <QTextLine>
33
35 : QWidget( parent )
36 , mModelBridge( new QgsLocatorModelBridge( this ) )
37 , mLineEdit( new QgsLocatorLineEdit( this ) )
38 , mResultsView( new QgsLocatorResultsView() )
39{
40 setObjectName( QStringLiteral( "LocatorWidget" ) );
41 mLineEdit->setShowClearButton( true );
42#ifdef Q_OS_MACX
43 mLineEdit->setPlaceholderText( tr( "Type to locate (⌘K)" ) );
44#else
45 mLineEdit->setPlaceholderText( tr( "Type to locate (Ctrl+K)" ) );
46#endif
47
48 int placeholderMinWidth = mLineEdit->fontMetrics().boundingRect( mLineEdit->placeholderText() ).width();
49 int minWidth = std::max( 200, static_cast< int >( placeholderMinWidth * 1.8 ) );
50 resize( minWidth, 30 );
51 QSizePolicy sizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Preferred );
52 sizePolicy.setHorizontalStretch( 0 );
53 sizePolicy.setVerticalStretch( 0 );
54 setSizePolicy( sizePolicy );
55 setMinimumSize( QSize( minWidth, 0 ) );
56
57 QHBoxLayout *layout = new QHBoxLayout();
58 layout->setContentsMargins( 0, 0, 0, 0 );
59 layout->addWidget( mLineEdit );
60 setLayout( layout );
61
62 setFocusProxy( mLineEdit );
63
64 // setup floating container widget
65 mResultsContainer = new QgsFloatingWidget( parent ? parent->window() : nullptr );
66 mResultsContainer->setAnchorWidget( mLineEdit );
69
70 QHBoxLayout *containerLayout = new QHBoxLayout();
71 containerLayout->setContentsMargins( 0, 0, 0, 0 );
72 containerLayout->addWidget( mResultsView );
73 mResultsContainer->setLayout( containerLayout );
74 mResultsContainer->hide();
75
76 mResultsView->setModel( mModelBridge->proxyModel() );
77 mResultsView->setUniformRowHeights( true );
78
79 int iconSize = QgsGuiUtils::scaleIconSize( 16 );
80 mResultsView->setIconSize( QSize( iconSize, iconSize ) );
81 mResultsView->recalculateSize();
82 mResultsView->setContextMenuPolicy( Qt::CustomContextMenu );
83
84 connect( mLineEdit, &QLineEdit::textChanged, this, &QgsLocatorWidget::scheduleDelayedPopup );
85 connect( mResultsView, &QAbstractItemView::activated, this, &QgsLocatorWidget::acceptCurrentEntry );
86 connect( mResultsView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &QgsLocatorWidget::selectionChanged );
87 connect( mResultsView, &QAbstractItemView::customContextMenuRequested, this, &QgsLocatorWidget::showContextMenu );
88
89 connect( mModelBridge, &QgsLocatorModelBridge::resultAdded, this, &QgsLocatorWidget::resultAdded );
90 connect( mModelBridge, &QgsLocatorModelBridge::isRunningChanged, this, [ = ]() {mLineEdit->setShowSpinner( mModelBridge->isRunning() );} );
91 connect( mModelBridge, &QgsLocatorModelBridge::resultsCleared, this, [ = ]() {mHasSelectedResult = false;} );
92
93 // have a tiny delay between typing text in line edit and showing the window
94 mPopupTimer.setInterval( 100 );
95 mPopupTimer.setSingleShot( true );
96 connect( &mPopupTimer, &QTimer::timeout, this, &QgsLocatorWidget::performSearch );
97 mFocusTimer.setInterval( 110 );
98 mFocusTimer.setSingleShot( true );
99 connect( &mFocusTimer, &QTimer::timeout, this, &QgsLocatorWidget::triggerSearchAndShowList );
100
101 mLineEdit->installEventFilter( this );
102 mResultsContainer->installEventFilter( this );
103 mResultsView->installEventFilter( this );
104 installEventFilter( this );
105 window()->installEventFilter( this );
106
107 mModelBridge->locator()->registerFilter( new QgsLocatorFilterFilter( this, this ) );
108
109 mMenu = new QMenu( this );
110 QAction *menuAction = mLineEdit->addAction( QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) ), QLineEdit::LeadingPosition );
111 connect( menuAction, &QAction::triggered, this, [ = ]
112 {
113 mFocusTimer.stop();
114 mResultsContainer->hide();
115 mMenu->exec( QCursor::pos() );
116 } );
117 connect( mMenu, &QMenu::aboutToShow, this, &QgsLocatorWidget::configMenuAboutToShow );
118
119 mModelBridge->setTransformContext( QgsProject::instance()->transformContext() );
121 this, [ = ]
122 {
123 mModelBridge->setTransformContext( QgsProject::instance()->transformContext() );
124 } );
125}
126
128{
129 return mModelBridge->locator();
130}
131
133{
134 if ( mMapCanvas == canvas )
135 return;
136
137 for ( const QMetaObject::Connection &conn : std::as_const( mCanvasConnections ) )
138 {
139 disconnect( conn );
140 }
141 mCanvasConnections.clear();
142
143 mMapCanvas = canvas;
144 if ( mMapCanvas )
145 {
146 mModelBridge->updateCanvasExtent( mMapCanvas->mapSettings().visibleExtent() );
147 mModelBridge->updateCanvasCrs( mMapCanvas->mapSettings().destinationCrs() );
148 mCanvasConnections
149 << connect( mMapCanvas, &QgsMapCanvas::extentsChanged, this, [ = ]() {mModelBridge->updateCanvasExtent( mMapCanvas->mapSettings().visibleExtent() );} )
150 << connect( mMapCanvas, &QgsMapCanvas::destinationCrsChanged, this, [ = ]() {mModelBridge->updateCanvasCrs( mMapCanvas->mapSettings().destinationCrs() );} ) ;
151 }
152}
153
154void QgsLocatorWidget::setPlaceholderText( const QString &text )
155{
156 mLineEdit->setPlaceholderText( text );
157}
158
160{
161 mResultsContainer->setAnchorPoint( anchorPoint );
162 mResultsContainer->setAnchorWidgetPoint( anchorWidgetPoint );
163}
164
165void QgsLocatorWidget::search( const QString &string )
166{
167 window()->activateWindow(); // window must also be active - otherwise floating docks can steal keystrokes
168 if ( string.isEmpty() )
169 {
170 mLineEdit->setFocus();
171 mLineEdit->selectAll();
172 }
173 else
174 {
175 scheduleDelayedPopup();
176 mLineEdit->setFocus();
177 mLineEdit->setText( string );
178 performSearch();
179 }
180}
181
183{
184 mModelBridge->invalidateResults();
185 mResultsContainer->hide();
186}
187
188void QgsLocatorWidget::scheduleDelayedPopup()
189{
190 mPopupTimer.start();
191}
192
193void QgsLocatorWidget::resultAdded()
194{
195 bool selectFirst = !mHasSelectedResult || mModelBridge->proxyModel()->rowCount() == 0;
196 if ( selectFirst )
197 {
198 int row = -1;
199 bool selectable = false;
200 while ( !selectable && row < mModelBridge->proxyModel()->rowCount() )
201 {
202 row++;
203 selectable = mModelBridge->proxyModel()->flags( mModelBridge->proxyModel()->index( row, 0 ) ).testFlag( Qt::ItemIsSelectable );
204 }
205 if ( selectable )
206 mResultsView->setCurrentIndex( mModelBridge->proxyModel()->index( row, 0 ) );
207 }
208}
209
210void QgsLocatorWidget::showContextMenu( const QPoint &point )
211{
212 QModelIndex index = mResultsView->indexAt( point );
213 if ( !index.isValid() )
214 return;
215
216 const QList<QgsLocatorResult::ResultAction> actions = mResultsView->model()->data( index, static_cast< int >( QgsLocatorModel::CustomRole::ResultActions ) ).value<QList<QgsLocatorResult::ResultAction>>();
217 QMenu *contextMenu = new QMenu( mResultsView );
218 for ( auto resultAction : actions )
219 {
220 QAction *menuAction = new QAction( resultAction.text, contextMenu );
221 if ( !resultAction.iconPath.isEmpty() )
222 menuAction->setIcon( QIcon( resultAction.iconPath ) );
223 connect( menuAction, &QAction::triggered, this, [ = ]() {mModelBridge->triggerResult( index, resultAction.id );} );
224 contextMenu->addAction( menuAction );
225 }
226 contextMenu->exec( mResultsView->viewport()->mapToGlobal( point ) );
227}
228
229void QgsLocatorWidget::performSearch()
230{
231 mPopupTimer.stop();
232 mModelBridge->performSearch( mLineEdit->text() );
233 showList();
234}
235
236void QgsLocatorWidget::showList()
237{
238 mResultsContainer->show();
239 mResultsContainer->raise();
240}
241
242void QgsLocatorWidget::triggerSearchAndShowList()
243{
244 if ( mModelBridge->proxyModel()->rowCount() == 0 )
245 performSearch();
246 else
247 showList();
248}
249
250bool QgsLocatorWidget::eventFilter( QObject *obj, QEvent *event )
251{
252 if ( obj == mLineEdit && event->type() == QEvent::KeyPress )
253 {
254 QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
255 switch ( keyEvent->key() )
256 {
257 case Qt::Key_Up:
258 case Qt::Key_Down:
259 case Qt::Key_PageUp:
260 case Qt::Key_PageDown:
261 triggerSearchAndShowList();
262 mHasSelectedResult = true;
263 QgsApplication::sendEvent( mResultsView, event );
264 return true;
265 case Qt::Key_Home:
266 case Qt::Key_End:
267 if ( keyEvent->modifiers() & Qt::ControlModifier )
268 {
269 triggerSearchAndShowList();
270 mHasSelectedResult = true;
271 QgsApplication::sendEvent( mResultsView, event );
272 return true;
273 }
274 break;
275 case Qt::Key_Enter:
276 case Qt::Key_Return:
277 acceptCurrentEntry();
278 return true;
279 case Qt::Key_Escape:
280 mResultsContainer->hide();
281 return true;
282 case Qt::Key_Tab:
283 if ( !mLineEdit->performCompletion() )
284 {
285 mHasSelectedResult = true;
286 mResultsView->selectNextResult();
287 }
288 return true;
289 case Qt::Key_Backtab:
290 mHasSelectedResult = true;
291 mResultsView->selectPreviousResult();
292 return true;
293 default:
294 break;
295 }
296 }
297 else if ( obj == mResultsView && event->type() == QEvent::MouseButtonPress )
298 {
299 mHasSelectedResult = true;
300 }
301 else if ( event->type() == QEvent::FocusOut && ( obj == mLineEdit || obj == mResultsContainer || obj == mResultsView ) )
302 {
303 if ( !mLineEdit->hasFocus() && !mResultsContainer->hasFocus() && !mResultsView->hasFocus() )
304 {
305 mFocusTimer.stop();
306 mResultsContainer->hide();
307 }
308 }
309 else if ( event->type() == QEvent::FocusIn && obj == mLineEdit )
310 {
311 mFocusTimer.start();
312 }
313 else if ( obj == window() && event->type() == QEvent::Resize )
314 {
315 mResultsView->recalculateSize();
316 }
317 return QWidget::eventFilter( obj, event );
318}
319
320void QgsLocatorWidget::configMenuAboutToShow()
321{
322 mMenu->clear();
323 for ( QgsLocatorFilter *filter : mModelBridge->locator()->filters() )
324 {
325 if ( !filter->enabled() )
326 continue;
327
328 QAction *action = new QAction( filter->displayName(), mMenu );
329 connect( action, &QAction::triggered, this, [ = ]
330 {
331 QString currentText = mLineEdit->text();
332 if ( currentText.isEmpty() )
333 currentText = tr( "<type here>" );
334 else
335 {
336 QStringList parts = currentText.split( ' ' );
337 if ( parts.count() > 1 && mModelBridge->locator()->filters( parts.at( 0 ) ).count() > 0 )
338 {
339 parts.pop_front();
340 currentText = parts.join( ' ' );
341 }
342 }
343
344 mLineEdit->setText( filter->activePrefix() + ' ' + currentText );
345 mLineEdit->setSelection( filter->activePrefix().length() + 1, currentText.length() );
346 } );
347 mMenu->addAction( action );
348 }
349 mMenu->addSeparator();
350 QAction *configAction = new QAction( tr( "Configure…" ), mMenu );
351 connect( configAction, &QAction::triggered, this, &QgsLocatorWidget::configTriggered );
352 mMenu->addAction( configAction );
353}
354
355
356void QgsLocatorWidget::acceptCurrentEntry()
357{
358 if ( mModelBridge->hasQueueRequested() )
359 {
360 return;
361 }
362 else
363 {
364 if ( !mResultsView->isVisible() )
365 return;
366
367 QModelIndex index = mResultsView->currentIndex();
368 if ( !index.isValid() )
369 return;
370
371 mResultsContainer->hide();
372 mLineEdit->clearFocus();
373 mModelBridge->triggerResult( index );
374 }
375}
376
377void QgsLocatorWidget::selectionChanged( const QItemSelection &selected, const QItemSelection &deselected )
378{
379 if ( !mResultsView->isVisible() )
380 return;
381
382 mModelBridge->selectionChanged( selected, deselected );
383}
384
386
387//
388// QgsLocatorResultsView
389//
390
391QgsLocatorResultsView::QgsLocatorResultsView( QWidget *parent )
392 : QTreeView( parent )
393{
394 setRootIsDecorated( false );
395 setUniformRowHeights( true );
396 header()->hide();
397 header()->setStretchLastSection( true );
398}
399
400void QgsLocatorResultsView::recalculateSize()
401{
402 QStyleOptionViewItem optView;
403#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
404 optView.init( this );
405#else
406 optView.initFrom( this );
407#endif
408
409 // try to show about 20 rows
410 int rowSize = 20 * itemDelegate()->sizeHint( optView, model()->index( 0, 0 ) ).height();
411
412 // try to take up a sensible portion of window width (about half)
413 int width = std::max( 300, window()->size().width() / 2 );
414 QSize newSize( width, rowSize + frameWidth() * 2 );
415 // resize the floating widget this is contained within
416 parentWidget()->resize( newSize );
417 QTreeView::resize( newSize );
418
419 header()->resizeSection( 0, width / 2 );
420 header()->resizeSection( 1, 0 );
421}
422
423void QgsLocatorResultsView::selectNextResult()
424{
425 const int rowCount = model()->rowCount( QModelIndex() );
426 if ( rowCount == 0 )
427 return;
428
429 int nextRow = currentIndex().row() + 1;
430 nextRow = nextRow % rowCount;
431 setCurrentIndex( model()->index( nextRow, 0 ) );
432}
433
434void QgsLocatorResultsView::selectPreviousResult()
435{
436 const int rowCount = model()->rowCount( QModelIndex() );
437 if ( rowCount == 0 )
438 return;
439
440 int previousRow = currentIndex().row() - 1;
441 if ( previousRow < 0 )
442 previousRow = rowCount - 1;
443 setCurrentIndex( model()->index( previousRow, 0 ) );
444}
445
446//
447// QgsLocatorFilterFilter
448//
449
450QgsLocatorFilterFilter::QgsLocatorFilterFilter( QgsLocatorWidget *locator, QObject *parent )
451 : QgsLocatorFilter( parent )
452 , mLocator( locator )
453{}
454
455QgsLocatorFilterFilter *QgsLocatorFilterFilter::clone() const
456{
457 return new QgsLocatorFilterFilter( mLocator );
458}
459
460QgsLocatorFilter::Flags QgsLocatorFilterFilter::flags() const
461{
463}
464
465void QgsLocatorFilterFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
466{
467 if ( !string.isEmpty() )
468 {
469 //only shows results when nothing typed
470 return;
471 }
472
473 for ( QgsLocatorFilter *filter : mLocator->locator()->filters() )
474 {
475 if ( feedback->isCanceled() )
476 return;
477
478 if ( filter == this || !filter || !filter->enabled() )
479 continue;
480
481 QgsLocatorResult result;
482 result.displayString = filter->activePrefix();
483 result.description = filter->displayName();
484 result.setUserData( QString( filter->activePrefix() + ' ' ) );
485 result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) );
486 emit resultFetched( result );
487 }
488}
489
490void QgsLocatorFilterFilter::triggerResult( const QgsLocatorResult &result )
491{
492 mLocator->search( result.userData().toString() );
493}
494
495QgsLocatorLineEdit::QgsLocatorLineEdit( QgsLocatorWidget *locator, QWidget *parent )
496 : QgsFilterLineEdit( parent )
497 , mLocatorWidget( locator )
498{
499 connect( mLocatorWidget->locator(), &QgsLocator::searchPrepared, this, [&] { update(); } );
500}
501
502void QgsLocatorLineEdit::paintEvent( QPaintEvent *event )
503{
504 // this adds the completion as grey text at the right of the cursor
505 // see https://stackoverflow.com/a/50425331/1548052
506 // this is possible that the completion might be badly rendered if the cursor is larger than the line edit
507 // this sounds acceptable as it is not very likely to use completion for super long texts
508 // for more details see https://stackoverflow.com/a/54218192/1548052
509
510 QLineEdit::paintEvent( event );
511
512 if ( !hasFocus() )
513 return;
514
515 QString currentText = text();
516
517 if ( currentText.length() == 0 || cursorPosition() < currentText.length() )
518 return;
519
520 const QStringList completionList = mLocatorWidget->locator()->completionList();
521
522 mCompletionText.clear();
523 QString completion;
524 for ( const QString &candidate : completionList )
525 {
526 if ( candidate.startsWith( currentText ) )
527 {
528 completion = candidate.right( candidate.length() - currentText.length() );
529 mCompletionText = candidate;
530 break;
531 }
532 }
533
534 if ( completion.isEmpty() )
535 return;
536
537 ensurePolished(); // ensure font() is up to date
538
539 QRect cr = cursorRect();
540 QPoint pos = cr.topRight() - QPoint( cr.width() / 2, 0 );
541
542 QTextLayout l( completion, font() );
543 l.beginLayout();
544 QTextLine line = l.createLine();
545 line.setLineWidth( width() - pos.x() );
546 line.setPosition( pos );
547 l.endLayout();
548
549 QPainter p( this );
550 p.setPen( QPen( Qt::gray, 1 ) );
551 l.draw( &p, QPoint( 0, 0 ) );
552}
553
554bool QgsLocatorLineEdit::performCompletion()
555{
556 if ( !mCompletionText.isEmpty() )
557 {
558 setText( mCompletionText );
559 mCompletionText.clear();
560 return true;
561 }
562 else
563 return false;
564}
565
566
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.
QVariant userData() const
Returns the userData.
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,...