New QgsFloatingWidget widget for easy creation of widgets which "float"

above a layout.

Supports setting another widget as a anchor point for the widget, eg
the floating widget could be set so that it's always placed to the
top-right of the anchor widget.
This commit is contained in:
Nyall Dawson 2016-04-26 12:31:37 +10:00
parent f85d24e9c7
commit 5da25136a6
6 changed files with 514 additions and 0 deletions

View File

@ -79,6 +79,7 @@
%Include qgsfilewidget.sip
%Include qgsfiledownloader.sip
%Include qgsfilterlineedit.sip
%Include qgsfloatingwidget.sip
%Include qgsfocuswatcher.sip
%Include qgsformannotationitem.sip
%Include qgsgenericprojectionselector.sip

View File

@ -226,6 +226,7 @@ SET(QGIS_GUI_SRCS
qgsfiledropedit.cpp
qgsfilewidget.cpp
qgsfilterlineedit.cpp
qgsfloatingwidget.cpp
qgsfocuswatcher.cpp
qgsformannotationitem.cpp
qgsgenericprojectionselector.cpp
@ -393,6 +394,7 @@ SET(QGIS_GUI_MOC_HDRS
qgsfiledropedit.h
qgsfilewidget.h
qgsfilterlineedit.h
qgsfloatingwidget.h
qgsfocuswatcher.h
qgsformannotationitem.h
qgsgenericprojectionselector.h

View File

@ -0,0 +1,179 @@
/***************************************************************************
qgsfloatingwidget.cpp
---------------------
begin : April 2016
copyright : (C) Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgsfloatingwidget.h"
#include <QEvent>
//
// QgsFloatingWidget
//
QgsFloatingWidget::QgsFloatingWidget( QWidget *parent )
: QWidget( parent )
, mAnchorWidget( nullptr )
, mParentEventFilter( nullptr )
, mAnchorEventFilter( nullptr )
, mFloatAnchorPoint( BottomMiddle )
, mAnchorWidgetAnchorPoint( TopMiddle )
{
if ( parent )
{
mParentEventFilter = new QgsFloatingWidgetEventFilter( parent );
parent->installEventFilter( mParentEventFilter );
connect( mParentEventFilter, SIGNAL( anchorPointChanged() ), this, SLOT( anchorPointChanged() ) );
}
}
void QgsFloatingWidget::setAnchorWidget( QWidget *widget )
{
// remove existing event filter
if ( mAnchorWidget )
{
mAnchorWidget->removeEventFilter( mAnchorEventFilter );
delete mAnchorEventFilter;
mAnchorEventFilter = nullptr;
}
mAnchorWidget = widget;
if ( mAnchorWidget )
{
mAnchorEventFilter = new QgsFloatingWidgetEventFilter( mAnchorWidget );
mAnchorWidget->installEventFilter( mAnchorEventFilter );
connect( mAnchorEventFilter, SIGNAL( anchorPointChanged() ), this, SLOT( anchorPointChanged() ) );
}
anchorPointChanged();
}
QWidget *QgsFloatingWidget::anchorWidget()
{
return mAnchorWidget;
}
void QgsFloatingWidget::showEvent( QShowEvent *e )
{
anchorPointChanged();
QWidget::showEvent( e );
}
void QgsFloatingWidget::anchorPointChanged()
{
if ( mAnchorWidget )
{
QPoint anchorWidgetOrigin;
switch ( mAnchorWidgetAnchorPoint )
{
case TopLeft:
anchorWidgetOrigin = QPoint( 0, 0 );
break;
case TopMiddle:
anchorWidgetOrigin = QPoint( mAnchorWidget->width() / 2, 0 );
break;
case TopRight:
anchorWidgetOrigin = QPoint( mAnchorWidget->width(), 0 );
break;
case MiddleLeft:
anchorWidgetOrigin = QPoint( 0, mAnchorWidget->height() / 2 );
break;
case Middle:
anchorWidgetOrigin = QPoint( mAnchorWidget->width() / 2, mAnchorWidget->height() / 2 );
break;
case MiddleRight:
anchorWidgetOrigin = QPoint( mAnchorWidget->width(), mAnchorWidget->height() / 2 );
break;
case BottomLeft:
anchorWidgetOrigin = QPoint( 0, mAnchorWidget->height() );
break;
case BottomMiddle:
anchorWidgetOrigin = QPoint( mAnchorWidget->width() / 2, mAnchorWidget->height() );
break;
case BottomRight:
anchorWidgetOrigin = QPoint( mAnchorWidget->width(), mAnchorWidget->height() );
break;
}
anchorWidgetOrigin = mAnchorWidget->mapTo( parentWidget(), anchorWidgetOrigin );
int anchorX = anchorWidgetOrigin.x();
int anchorY = anchorWidgetOrigin.y();
switch ( mFloatAnchorPoint )
{
case TopLeft:
break;
case TopMiddle:
anchorX = anchorX - width() / 2;
break;
case TopRight:
anchorX = anchorX - width();
break;
case MiddleLeft:
anchorY = anchorY - height() / 2;
break;
case Middle:
anchorY = anchorY - height() / 2;
anchorX = anchorX - width() / 2;
break;
case MiddleRight:
anchorX = anchorX - width();
anchorY = anchorY - height() / 2;
break;
case BottomLeft:
anchorY = anchorY - height();
break;
case BottomMiddle:
anchorX = anchorX - width() / 2;
anchorY = anchorY - height();
break;
case BottomRight:
anchorX = anchorX - width();
anchorY = anchorY - height();
break;
}
// constrain x so that widget floats within parent widget
anchorX = qBound( 0, anchorX, parentWidget()->width() - width() );
move( anchorX, anchorY );
}
}
//
// QgsFloatingWidgetEventFilter
//
/// @cond PRIVATE
QgsFloatingWidgetEventFilter::QgsFloatingWidgetEventFilter( QWidget *parent )
: QObject( parent )
{
}
bool QgsFloatingWidgetEventFilter::eventFilter( QObject *object, QEvent *event )
{
Q_UNUSED( object );
switch ( event->type() )
{
case QEvent::Move:
case QEvent::Resize:
emit anchorPointChanged();
return true;
default:
return false;
}
}
///@endcond

130
src/gui/qgsfloatingwidget.h Normal file
View File

@ -0,0 +1,130 @@
/***************************************************************************
qgsfloatingwidget.h
-------------------
begin : April 2016
copyright : (C) Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSFLOATINGWIDGET_H
#define QGSFLOATINGWIDGET_H
#include <QWidget>
/** \ingroup gui
* \class QgsFloatingWidget
* A QWidget subclass for creating widgets which float outside of the normal Qt layout
* system. Floating widgets use an "anchor widget" to determine how they are anchored
* within their parent widget.
* \note Added in version 2.16
*/
class GUI_EXPORT QgsFloatingWidget: public QWidget
{
Q_OBJECT
public:
//! Reference points for anchoring widget position
enum AnchorPoint
{
TopLeft, //!< Top-left of widget
TopMiddle, //!< Top center of widget
TopRight, //!< Top-right of widget
MiddleLeft, //!< Middle left of widget
Middle, //!< Middle of widget
MiddleRight, //!< Middle right of widget
BottomLeft, //!< Bottom-left of widget
BottomMiddle, //!< Bottom center of widget
BottomRight, //!< Bottom-right of widget
};
QgsFloatingWidget( QWidget* parent = nullptr );
/** Sets the widget to "anchor" the floating widget to. The floating widget will be repositioned whenever the
* anchor widget moves or is resized so that it maintains the same relative position to the anchor widget.
* @param widget anchor widget. Both the floating widget and the anchor widget must share some common parent.
* @see anchorWidget()
*/
void setAnchorWidget( QWidget* widget );
/** Returns the widget that the floating widget is "anchored" tto. The floating widget will be repositioned whenever the
* anchor widget moves or is resized so that it maintains the same relative position to the anchor widget.
* @see setAnchorWidget()
*/
QWidget* anchorWidget();
/** Returns the floating widget's anchor point, which corresponds to the point on the widget which should remain
* fixed in the same relative position whenever the widget's parent is resized or moved.
* @see setAnchorPoint()
*/
AnchorPoint anchorPoint() const { return mFloatAnchorPoint; }
/** Sets the floating widget's anchor point, which corresponds to the point on the widget which should remain
* fixed in the same relative position whenever the widget's parent is resized or moved.
* @param point anchor point
* @see anchorPoint()
*/
void setAnchorPoint( AnchorPoint point ) { mFloatAnchorPoint = point; anchorPointChanged(); }
/** Returns the anchor widget's anchor point, which corresponds to the point on the anchor widget which
* the floating widget should "attach" to. The floating widget should remain fixed in the same relative position
* to this anchor widget whenever the widget's parent is resized or moved.
* @see setAnchorWidgetPoint()
*/
AnchorPoint anchorWidgetPoint() const { return mAnchorWidgetAnchorPoint; }
/** Returns the anchor widget's anchor point, which corresponds to the point on the anchor widget which
* the floating widget should "attach" to. The floating widget should remain fixed in the same relative position
* to this anchor widget whenever the widget's parent is resized or moved.
* @see setAnchorWidgetPoint()
*/
void setAnchorWidgetPoint( AnchorPoint point ) { mAnchorWidgetAnchorPoint = point; anchorPointChanged(); }
protected:
void showEvent( QShowEvent* e ) override;
private slots:
//! Repositions the floating widget to a changed anchor point
void anchorPointChanged();
private:
QWidget* mAnchorWidget;
QObject* mParentEventFilter;
QObject* mAnchorEventFilter;
AnchorPoint mFloatAnchorPoint;
AnchorPoint mAnchorWidgetAnchorPoint;
};
/// @cond PRIVATE
class QgsFloatingWidgetEventFilter: public QObject
{
Q_OBJECT
public:
QgsFloatingWidgetEventFilter( QWidget* parent = nullptr );
virtual bool eventFilter( QObject* object, QEvent* event ) override;
signals:
//! Emitted when the filter's parent is moved or resized
void anchorPointChanged();
};
/// @endcond
#endif // QGSFLOATINGWIDGET_H

View File

@ -49,6 +49,7 @@ ADD_PYTHON_TEST(PyQgsFeatureIterator test_qgsfeatureiterator.py)
ADD_PYTHON_TEST(PyQgsField test_qgsfield.py)
ADD_PYTHON_TEST(PyQgsFieldModel test_qgsfieldmodel.py)
ADD_PYTHON_TEST(PyQgsFilterLineEdit test_qgsfilterlineedit.py)
ADD_PYTHON_TEST(PyQgsFloatingWidget test_qgsfloatingwidget.py)
ADD_PYTHON_TEST(PyQgsFontUtils test_qgsfontutils.py)
ADD_PYTHON_TEST(PyQgsGeometryAvoidIntersections test_qgsgeometry_avoid_intersections.py)
ADD_PYTHON_TEST(PyQgsGeometryGeneratorSymbolLayer test_qgsgeometrygeneratorsymbollayer.py)

View File

@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsFloatingWidget.
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
"""
__author__ = 'Nyall Dawson'
__date__ = '26/04/2016'
__copyright__ = 'Copyright 2016, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
import qgis # NOQA
from PyQt.QtGui import QWidget, QGridLayout
import os
from qgis.gui import (
QgsFloatingWidget
)
from qgis.testing import start_app, unittest
start_app()
class TestQgsFloatingWidget(unittest.TestCase):
def testAnchor(self):
""" test setting anchor point for widget """
main_frame = QWidget()
gl = QGridLayout()
main_frame.setLayout(gl)
main_frame.setMinimumSize(800, 600)
anchor_widget = QWidget(main_frame)
anchor_widget.setMinimumSize(300, 200)
main_frame.layout().addWidget(anchor_widget, 1, 1)
gl.setColumnStretch(0, 1)
gl.setColumnStretch(1, 0)
gl.setColumnStretch(2, 1)
gl.setRowStretch(0, 1)
gl.setRowStretch(1, 0)
gl.setRowStretch(2, 1)
# 103 = WA_DontShowOnScreen (not available in PyQt)
main_frame.setAttribute(103)
main_frame.show()
fw = qgis.gui.QgsFloatingWidget(main_frame)
fw.setMinimumSize(100, 50)
fw.setAnchorWidget(anchor_widget)
tests = [{'anchorPoint': QgsFloatingWidget.TopLeft, 'widgetAnchorPoint': QgsFloatingWidget.TopLeft, 'x': 250, 'y': 200},
{'anchorPoint': QgsFloatingWidget.TopMiddle, 'widgetAnchorPoint': QgsFloatingWidget.TopLeft, 'x': 200, 'y': 200},
{'anchorPoint': QgsFloatingWidget.TopRight, 'widgetAnchorPoint': QgsFloatingWidget.TopLeft, 'x': 150, 'y': 200},
{'anchorPoint': QgsFloatingWidget.MiddleLeft, 'widgetAnchorPoint': QgsFloatingWidget.TopLeft, 'x': 250, 'y': 175},
{'anchorPoint': QgsFloatingWidget.Middle, 'widgetAnchorPoint': QgsFloatingWidget.TopLeft, 'x': 200, 'y': 175},
{'anchorPoint': QgsFloatingWidget.MiddleRight, 'widgetAnchorPoint': QgsFloatingWidget.TopLeft, 'x': 150, 'y': 175},
{'anchorPoint': QgsFloatingWidget.BottomLeft, 'widgetAnchorPoint': QgsFloatingWidget.TopLeft, 'x': 250, 'y': 150},
{'anchorPoint': QgsFloatingWidget.BottomMiddle, 'widgetAnchorPoint': QgsFloatingWidget.TopLeft, 'x': 200, 'y': 150},
{'anchorPoint': QgsFloatingWidget.BottomRight, 'widgetAnchorPoint': QgsFloatingWidget.TopLeft, 'x': 150, 'y': 150},
{'anchorPoint': QgsFloatingWidget.TopLeft, 'widgetAnchorPoint': QgsFloatingWidget.TopMiddle, 'x': 400, 'y': 200},
{'anchorPoint': QgsFloatingWidget.TopLeft, 'widgetAnchorPoint': QgsFloatingWidget.TopRight, 'x': 550, 'y': 200},
{'anchorPoint': QgsFloatingWidget.TopLeft, 'widgetAnchorPoint': QgsFloatingWidget.MiddleLeft, 'x': 250, 'y': 300},
{'anchorPoint': QgsFloatingWidget.TopLeft, 'widgetAnchorPoint': QgsFloatingWidget.Middle, 'x': 400, 'y': 300},
{'anchorPoint': QgsFloatingWidget.TopLeft, 'widgetAnchorPoint': QgsFloatingWidget.MiddleRight, 'x': 550, 'y': 300},
{'anchorPoint': QgsFloatingWidget.TopLeft, 'widgetAnchorPoint': QgsFloatingWidget.BottomLeft, 'x': 250, 'y': 400},
{'anchorPoint': QgsFloatingWidget.TopLeft, 'widgetAnchorPoint': QgsFloatingWidget.BottomMiddle, 'x': 400, 'y': 400},
{'anchorPoint': QgsFloatingWidget.TopLeft, 'widgetAnchorPoint': QgsFloatingWidget.BottomRight, 'x': 550, 'y': 400}]
for t in tests:
fw.setAnchorPoint(t['anchorPoint'])
fw.setAnchorWidgetPoint(t['widgetAnchorPoint'])
self.assertEqual(fw.pos().x(), t['x'])
self.assertEqual(fw.pos().y(), t['y'])
def testMovingResizingAnchorWidget(self):
""" test that moving or resizing the anchor widget updates the floating widget position """
main_frame = QWidget()
gl = QGridLayout()
main_frame.setLayout(gl)
main_frame.setMinimumSize(800, 600)
anchor_widget = QWidget(main_frame)
anchor_widget.setFixedSize(300, 200)
main_frame.layout().addWidget(anchor_widget, 1, 1)
gl.setColumnStretch(0, 1)
gl.setColumnStretch(1, 0)
gl.setColumnStretch(2, 1)
gl.setRowStretch(0, 1)
gl.setRowStretch(1, 0)
gl.setRowStretch(2, 1)
# 103 = WA_DontShowOnScreen (not available in PyQt)
main_frame.setAttribute(103)
main_frame.show()
fw = qgis.gui.QgsFloatingWidget(main_frame)
fw.setMinimumSize(100, 50)
fw.setAnchorWidget(anchor_widget)
fw.setAnchorPoint(QgsFloatingWidget.TopLeft)
fw.setAnchorWidgetPoint(QgsFloatingWidget.TopLeft)
self.assertEqual(fw.pos().x(), 250)
self.assertEqual(fw.pos().y(), 200)
# now resize anchor widget
anchor_widget.setFixedSize(400, 300)
# force layout recalculation
main_frame.layout().invalidate()
main_frame.layout().activate()
self.assertEqual(fw.pos().x(), 200)
self.assertEqual(fw.pos().y(), 150)
# now move anchor widget
anchor_widget.move(100, 110)
self.assertEqual(fw.pos().x(), 100)
self.assertEqual(fw.pos().y(), 110)
def testResizingParentWidget(self):
""" test resizing parent widget correctly repositions floating widget"""
main_frame = QWidget()
gl = QGridLayout()
main_frame.setLayout(gl)
main_frame.setMinimumSize(800, 600)
anchor_widget = QWidget(main_frame)
anchor_widget.setFixedSize(300, 200)
main_frame.layout().addWidget(anchor_widget, 1, 1)
gl.setColumnStretch(0, 1)
gl.setColumnStretch(1, 0)
gl.setColumnStretch(2, 1)
gl.setRowStretch(0, 1)
gl.setRowStretch(1, 0)
gl.setRowStretch(2, 1)
# 103 = WA_DontShowOnScreen (not available in PyQt)
main_frame.setAttribute(103)
main_frame.show()
fw = qgis.gui.QgsFloatingWidget(main_frame)
fw.setMinimumSize(100, 50)
fw.setAnchorWidget(anchor_widget)
fw.setAnchorPoint(QgsFloatingWidget.TopLeft)
fw.setAnchorWidgetPoint(QgsFloatingWidget.TopLeft)
self.assertEqual(fw.pos().x(), 250)
self.assertEqual(fw.pos().y(), 200)
# now resize parent widget
main_frame.setFixedSize(1000, 800)
# force layout recalculation
main_frame.layout().invalidate()
main_frame.layout().activate()
self.assertEqual(fw.pos().x(), 350)
self.assertEqual(fw.pos().y(), 300)
def testPositionConstrainedToParent(self):
""" test that floating widget will be placed inside parent when possible """
main_frame = QWidget()
gl = QGridLayout()
main_frame.setLayout(gl)
main_frame.setMinimumSize(800, 600)
anchor_widget = QWidget(main_frame)
anchor_widget.setFixedSize(300, 200)
main_frame.layout().addWidget(anchor_widget, 1, 1)
gl.setColumnStretch(0, 1)
gl.setColumnStretch(1, 0)
gl.setColumnStretch(2, 1)
gl.setRowStretch(0, 1)
gl.setRowStretch(1, 0)
gl.setRowStretch(2, 1)
main_frame.setAttribute(103)
main_frame.show()
fw = qgis.gui.QgsFloatingWidget(main_frame)
fw.setMinimumSize(300, 50)
fw.setAnchorWidget(anchor_widget)
fw.setAnchorPoint(QgsFloatingWidget.TopRight)
fw.setAnchorWidgetPoint(QgsFloatingWidget.TopLeft)
# x-position should be 0, not -50
self.assertEqual(fw.pos().x(), 0)
fw.setAnchorPoint(QgsFloatingWidget.TopLeft)
fw.setAnchorWidgetPoint(QgsFloatingWidget.TopRight)
# x-position should be 500, not 600
self.assertEqual(fw.pos().x(), 500)
if __name__ == '__main__':
unittest.main()