Commit 2406510c authored by James Goppert's avatar James Goppert

Added a more generic osgqtwidget for map3d.

Still need to add uas connections. New osgwidget inherits from
qgraphicsview to expose more functionality to qt. Map3d.ui contains
the graphics view and some buttons. Currently runs yahoo_heightfield
by default. Make sure you run qgroundcontrol fromt the top dir or
it will seg fault, :-). I'll add some error handling soon.
parent 8e9c932c
<!--
osgEarth Sample
This example pulls satellite/aerial map tiles from the Yahoo! Maps service.
This is for educational purposes only - it is your responsibility to abide by
the provider's terms of service when using their data.
-->
<map name="Yahoo! Maps" type="geocentric">
<cache type="tms">
<path>/tmp/OsgEarthCache</path>
</cache>
<image name="yahoo_aerial" driver="yahoo">
<dataset>satellite</dataset>
</image>
<loading_policy mode="sequential"/>
<lighting>true</lighting>
</map>
<!--
osgEarth Sample
This example pulls satellite/aerial map tiles from the Yahoo! Maps service.
This is for educational purposes only - it is your responsibility to abide by
the provider's terms of service when using their data.
-->
<map name="Yahoo! Maps" type="geocentric">
<image name="yahoo aerial" driver="yahoo">
<dataset>satellite</dataset>
</image>
<loading_policy mode="sequential"/>
<lighting>false</lighting>
<heightfield name="WorldWind DEM" driver="wms">
<url>http://onearth.jpl.nasa.gov/wms.cgi</url>
<layers>worldwind_dem</layers>
<style>feet_short_int</style>
<format>tiff</format>
<elevation_unit>ft</elevation_unit>
</heightfield>
<normalize_edges>true</normalize_edges>
<vertical_scale>50</vertical_scale>
<skirt_ratio>0.01</skirt_ratio>
</map>
<!--
osgEarth Sample
This example pulls road map tiles from the Yahoo! Maps service.
This is for educational purposes only - it is your responsibility to abide by
the provider's terms of service when using their data.
-->
<map name="Yahoo maps" type="geocentric">
<image name="roads map" driver="yahoo">
<dataset>roads</dataset>
</image>
<loading_policy mode="preemptive"/>
<lighting>true</lighting>
</map>
......@@ -82,13 +82,14 @@ macx {
# Include OpenSceneGraph and osgEarth libraries
LIBS += -losg \
-losgViewer \
-losgEarth
-losgEarth \
-losgEarthUtil
DEFINES += QGC_OSG_ENABLED
}
}
# GNU/Linux
linux-g++-32 {
linux-g++ {
CONFIG += debug
......@@ -127,17 +128,18 @@ linux-g++-32 {
-lSDL \
-lSDLmain
exists(/usr/lib/osg):exists(/usr/lib/osgEarth) {
exists(/usr/include/osgEarth) | exists(/usr/local/include/osgEarth) {
message("Building support for OSGEARTH")
DEPENDENCIES_PRESENT += osgearth
# Include OpenSceneGraph and osgEarth libraries
LIBS += -losg \
-losgViewer \
-losgEarth
-losgEarth \
-losgEarthUtil
DEFINES += QGC_OSG_ENABLED
}
QMAKE_CXXFLAGS += -Wl,-E
QMAKE_CXXFLAGS += -Wl,-E, -DUSE_QT4
#-lflite_cmu_us_rms \
#-lflite_cmu_us_slt \
......
......@@ -47,7 +47,6 @@ MAVLINK_CONF = ""
# if the variable MAVLINK_CONF contains the name of an
# additional project, QGroundControl includes the support
# of custom MAVLink messages of this project
exists(user_config.pri) {
include(user_config.pri)
message("----- USING CUSTOM USER QGROUNDCONTROL CONFIG FROM user_config.pri -----")
......@@ -79,10 +78,10 @@ contains(MAVLINK_CONF, ualberta) {
INCLUDEPATH += $$BASEDIR/../mavlink/include/ualberta
DEFINES += QGC_USE_UALBERTA_MESSAGES
}
contains(MAVLINK_CONF, ardupilotmega) {
contains(MAVLINK_CONF, ardupilotmega) {
# Remove the default set - it is included anyway
INCLUDEPATH -= $$BASEDIR/../mavlink/include/common
# UALBERTA SPECIAL MESSAGES
INCLUDEPATH += $$BASEDIR/../mavlink/include/ardupilotmega
DEFINES += QGC_USE_ARDUPILOTMEGA_MESSAGES
......@@ -138,8 +137,10 @@ FORMS += src/ui/MainWindow.ui \
src/ui/QGCFirmwareUpdate.ui \
src/ui/QGCPxImuFirmwareUpdate.ui \
src/ui/QGCDataPlot2D.ui \
src/ui/QGCRemoteControlView.ui
#src/ui/WaypointGlobalView.ui
src/ui/QGCRemoteControlView.ui \
src/ui/QMap3D.ui
# src/ui/WaypointGlobalView.ui
INCLUDEPATH += src \
src/ui \
src/ui/linechart \
......@@ -221,8 +222,7 @@ HEADERS += src/MG.h \
src/ui/linechart/IncrementalPlot.h \
src/ui/map/Waypoint2DIcon.h \
src/ui/map/MAV2DIcon.h \
src/ui/QGCRemoteControlView.h \
#src/ui/WaypointGlobalView.h \
src/ui/QGCRemoteControlView.h \ # src/ui/WaypointGlobalView.h \
src/ui/RadioCalibration/RadioCalibrationData.h \
src/ui/RadioCalibration/RadioCalibrationWindow.h \
src/ui/RadioCalibration/AirfoilServoCalibrator.h \
......@@ -230,17 +230,18 @@ HEADERS += src/MG.h \
src/ui/RadioCalibration/CurveCalibrator.h \
src/ui/RadioCalibration/AbstractCalibrator.h \
src/comm/QGCMAVLink.h
contains(DEPENDENCIES_PRESENT, osgearth) {
message("Including headers for OSGEARTH")
# Enable only if OpenSceneGraph is available
HEADERS += src/ui/map3D/Q3DWidget.h \
src/ui/map3D/PixhawkCheetahGeode.h \
src/ui/map3D/Pixhawk3DWidget.h \
src/ui/map3D/Q3DWidgetFactory.h \
src/ui/map3D/GCManipulator.h
contains(DEPENDENCIES_PRESENT, osgearth) {
message("Including headers for OSGEARTH")
# Enable only if OpenSceneGraph is available
HEADERS += src/ui/map3D/Q3DWidget.h \
src/ui/map3D/QOSGWidget.h \
src/ui/map3D/QMap3D.h \
src/ui/map3D/PixhawkCheetahGeode.h \
src/ui/map3D/Pixhawk3DWidget.h \
src/ui/map3D/Q3DWidgetFactory.h \
src/ui/map3D/GCManipulator.h
}
SOURCES += src/main.cc \
src/Core.cc \
src/uas/UASManager.cc \
......@@ -309,17 +310,18 @@ SOURCES += src/main.cc \
src/ui/RadioCalibration/SwitchCalibrator.cc \
src/ui/RadioCalibration/CurveCalibrator.cc \
src/ui/RadioCalibration/AbstractCalibrator.cc \
src/ui/RadioCalibration/RadioCalibrationData.cc \
#src/ui/WaypointGlobalView.cc \
contains(DEPENDENCIES_PRESENT, osgearth) {
message("Including sources for OSGEARTH")
# Enable only if OpenSceneGraph is available
SOURCES += src/ui/map3D/Q3DWidget.cc \
src/ui/map3D/PixhawkCheetahGeode.cc \
src/ui/map3D/Pixhawk3DWidget.cc \
src/ui/map3D/Q3DWidgetFactory.cc \
src/ui/map3D/GCManipulator.cc
src/ui/RadioCalibration/RadioCalibrationData.cc # src/ui/WaypointGlobalView.cc \
contains(DEPENDENCIES_PRESENT, osgearth) {
message("Including sources for OSGEARTH")
# Enable only if OpenSceneGraph is available
SOURCES += src/ui/map3D/Q3DWidget.cc \
src/ui/map3D/QOSGWidget.cc \
src/ui/map3D/QMap3D.cc \
src/ui/map3D/PixhawkCheetahGeode.cc \
src/ui/map3D/Pixhawk3DWidget.cc \
src/ui/map3D/Q3DWidgetFactory.cc \
src/ui/map3D/GCManipulator.cc
}
RESOURCES += mavground.qrc
......
......@@ -357,6 +357,11 @@ void UAS::receiveMessage(LinkInterface* link, mavlink_message_t message)
GAudioOutput::instance()->notifyPositive();
}
positionLock = true;
// Send to patch antenna
mavlink_message_t msg;
mavlink_msg_global_position_pack(MG::SYSTEM::ID, MG::SYSTEM::COMPID, &msg, pos.usec, pos.lat, pos.lon, pos.alt, pos.vx, pos.vy, pos.vz);
sendMessage(msg);
}
break;
case MAVLINK_MSG_ID_GPS_RAW:
......
......@@ -55,6 +55,10 @@ CommConfigurationWindow::CommConfigurationWindow(LinkInterface* link, ProtocolIn
// Setup the user interface according to link type
ui.setupUi(this);
// add link types
ui.linkType->addItem("Serial",QGC_LINK_SERIAL);
ui.linkType->addItem("UDP",QGC_LINK_UDP);
// Create action to open this menu
// Create configuration action for this link
// Connect the current UAS
......
......@@ -39,6 +39,12 @@ This file is part of the QGROUNDCONTROL project
#include "ProtocolInterface.h"
#include "ui_CommSettings.h"
enum qgc_link_t
{
QGC_LINK_SERIAL,
QGC_LINK_UDP
};
#ifdef OPAL_RT
#include "OpalLink.h"
#endif
......
......@@ -46,8 +46,8 @@ This file is part of the QGROUNDCONTROL project
#include "JoystickWidget.h"
#include "GAudioOutput.h"
#ifdef QGC_OSG_ENABLED
#include "Q3DWidgetFactory.h"
#ifdef QGC_OSG_ENABLED
#include "QMap3D.h"
#endif
// FIXME Move
......@@ -136,8 +136,9 @@ void MainWindow::buildWidgets()
protocolWidget = new XMLCommProtocolWidget(this);
dataplotWidget = new QGCDataPlot2D(this);
#ifdef QGC_OSG_ENABLED
_3DWidget = Q3DWidgetFactory::get("PIXHAWK");
#endif
//_3DWidget = Q3DWidgetFactory::get(QGC_MAP3D_OSGEARTH);
_3DWidget = new QMap3D(this);
#endif
// Dock widgets
controlDockWidget = new QDockWidget(tr("Control"), this);
......@@ -229,9 +230,9 @@ void MainWindow::arrangeCenterStack()
if (linechartWidget) centerStack->addWidget(linechartWidget);
if (protocolWidget) centerStack->addWidget(protocolWidget);
if (mapWidget) centerStack->addWidget(mapWidget);
#ifdef QGC_OSG_ENABLED
#ifdef QGC_OSG_ENABLED
if (_3DWidget) centerStack->addWidget(_3DWidget);
#endif
#endif
if (hudWidget) centerStack->addWidget(hudWidget);
if (dataplotWidget) centerStack->addWidget(dataplotWidget);
......
......@@ -64,7 +64,8 @@ This file is part of the QGROUNDCONTROL project
#include "QGCDataPlot2D.h"
#include "QGCRemoteControlView.h"
#ifdef QGC_OSG_ENABLED
#include "Q3DWidget.h"
//#include "Q3DWidget.h"
#include "QMap3D.h"
#endif
#include "LogCompressor.h"
......@@ -166,8 +167,8 @@ protected:
QPointer<XMLCommProtocolWidget> protocolWidget;
QPointer<QGCDataPlot2D> dataplotWidget;
#ifdef QGC_OSG_ENABLED
QPointer<Q3DWidget> _3DWidget;
#endif
QPointer<QMap3D> _3DWidget;
#endif
// Dock widgets
QPointer<QDockWidget> controlDockWidget;
QPointer<QDockWidget> infoDockWidget;
......
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QMap3D</class>
<widget class="QWidget" name="QMap3D">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>548</width>
<height>211</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QPushButton" name="pushButton_map">
<property name="text">
<string>Map</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="ViewerQOSG" name="graphicsView"/>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="pushButton_vehicle">
<property name="text">
<string>Vehicle</string>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ViewerQOSG</class>
<extends>QGraphicsView</extends>
<header>QOSGWidget.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
File mode changed from 100755 to 100644
/*=====================================================================
QGroundControl Open Source Ground Control Station
(c) 2009, 2010 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
This file is part of the QGROUNDCONTROL project
QGROUNDCONTROL 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 3 of the License, or
(at your option) any later version.
QGROUNDCONTROL is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QGROUNDCONTROL. If not, see <http://www.gnu.org/licenses/>.
======================================================================*/
/**
* @file
* @brief Definition of the class QMap3D.
*
* @author James Goppert <james.goppert@gmail.com>
*
*/
#include "QMap3D.h"
#include <osgEarthUtil/EarthManipulator>
#include <stdexcept>
QMap3D::QMap3D(QWidget * parent, const char * name, WindowFlags f) :
QWidget(parent,f)
{
setupUi(this);
osg::ref_ptr<osg::Node> map = osgDB::readNodeFile("data/yahoo_heightfield.earth");
if (!map) throw std::runtime_error("unable to load file");
graphicsView->updateCamera();
graphicsView->setCameraManipulator(new osgEarthUtil::EarthManipulator);
graphicsView->setSceneData(map);
show();
}
QMap3D::~QMap3D()
{
}
/*=====================================================================
QGroundControl Open Source Ground Control Station
(c) 2009, 2010 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
This file is part of the QGROUNDCONTROL project
QGROUNDCONTROL 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 3 of the License, or
(at your option) any later version.
QGROUNDCONTROL is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QGROUNDCONTROL. If not, see <http://www.gnu.org/licenses/>.
======================================================================*/
/**
* @file
* @brief Definition of the class QMap3D.h
*
* @author James Goppert <james.goppert@gmail.com>
*
*/
#ifndef QMAP3D_H
#define QMAP3D_H
#include "QOSGWidget.h"
#include "ui_QMap3D.h"
namespace Ui {
class QMap3D;
}
class QMap3D : public QWidget, private Ui::QMap3D
{
Q_OBJECT
public:
QMap3D(QWidget * parent = 0, const char * name = 0, WindowFlags f = 0);
~QMap3D();
};
#endif // QMAP3D_H
/* OpenSceneGraph example, osganimate.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "QOSGWidget.h"
QOSGWidget::QOSGWidget( QWidget * parent, const char * name, WindowFlags f, bool overrideTraits):
#if USE_QT4
QWidget(parent, f), _overrideTraits (overrideTraits)
#else
QWidget(parent, name, f), _overrideTraits (overrideTraits)
#endif
{
createContext();
#if USE_QT4
setAttribute(Qt::WA_PaintOnScreen);
setAttribute(Qt::WA_NoSystemBackground);
setFocusPolicy(Qt::ClickFocus);
#else
setBackgroundMode(Qt::NoBackground);
#endif
}
void QOSGWidget::createContext()
{
osg::DisplaySettings* ds = osg::DisplaySettings::instance();
osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits;
traits->readDISPLAY();
if (traits->displayNum<0) traits->displayNum = 0;
traits->windowName = "osgViewerQt";
traits->screenNum = 0;
traits->x = x();
traits->y = y();
traits->width = width();
traits->height = height();
traits->alpha = ds->getMinimumNumAlphaBits();
traits->stencil = ds->getMinimumNumStencilBits();
traits->windowDecoration = false;
traits->doubleBuffer = true;
traits->sharedContext = 0;
traits->sampleBuffers = ds->getMultiSamples();
traits->samples = ds->getNumMultiSamples();
#if defined(__APPLE__)
// Extract a WindowPtr from the HIViewRef that QWidget::winId() returns.
// Without this change, the peer tries to call GetWindowPort on the HIViewRef
// which returns 0 and we only render white.
traits->inheritedWindowData = new WindowData(HIViewGetWindow((HIViewRef)winId()));
#else // all others
traits->inheritedWindowData = new WindowData(winId());
#endif
if (ds->getStereo())
{
switch(ds->getStereoMode())
{
case(osg::DisplaySettings::QUAD_BUFFER): traits->quadBufferStereo = true; break;
case(osg::DisplaySettings::VERTICAL_INTERLACE):
case(osg::DisplaySettings::CHECKERBOARD):
case(osg::DisplaySettings::HORIZONTAL_INTERLACE): traits->stencil = 8; break;
default: break;
}
}
osg::ref_ptr<osg::GraphicsContext> gc = osg::GraphicsContext::createGraphicsContext(traits.get());
_gw = dynamic_cast<osgViewer::GraphicsWindow*>(gc.get());
// get around dearanged traits on X11 (MTCompositeViewer only)
if (_overrideTraits)
{
traits->x = x();
traits->y = y();
traits->width = width();
traits->height = height();
}
}
#ifndef WIN32
void QOSGWidget::destroyEvent(bool destroyWindow, bool destroySubWindows)
{
_gw->getEventQueue()->closeWindow();
}
void QOSGWidget::closeEvent( QCloseEvent * event )
{
#ifndef USE_QT4
event->accept();
#endif
_gw->getEventQueue()->closeWindow();
}
void QOSGWidget::resizeEvent( QResizeEvent * event )
{
const QSize & size = event->size();
_gw->getEventQueue()->windowResize(0, 0, size.width(), size.height() );
_gw->resized(0, 0, size.width(), size.height());
}
void QOSGWidget::keyPressEvent( QKeyEvent* event )
{
#if USE_QT4
_gw->getEventQueue()->keyPress( (osgGA::GUIEventAdapter::KeySymbol) *(event->text().toAscii().data() ) );
#else
_gw->getEventQueue()->keyPress( (osgGA::GUIEventAdapter::KeySymbol) event->ascii() );
#endif
}
void QOSGWidget::keyReleaseEvent( QKeyEvent* event )
{
#if USE_QT4
int c = *event->text().toAscii().data();
#else
int c = event->ascii();
#endif
_gw->getEventQueue()->keyRelease( (osgGA::GUIEventAdapter::KeySymbol) (c) );
}
void QOSGWidget::mousePressEvent( QMouseEvent* event )
{
int button = 0;
switch(event->button())
{
case(Qt::LeftButton): button = 1; break;
case(Qt::MidButton): button = 2; break;
case(Qt::RightButton): button = 3; break;
case(Qt::NoButton): button = 0; break;
default: button = 0; break;
}
_gw->getEventQueue()->mouseButtonPress(event->x(), event->y(), button);
}
void QOSGWidget::mouseDoubleClickEvent ( QMouseEvent * event )
{
int button = 0;
switch(event->button())
{
case(Qt::LeftButton): button = 1; break;
case(Qt::MidButton): button = 2; break;
case(Qt::RightButton): button = 3; break;
case(Qt::NoButton): button = 0; break;
default: button = 0; break;
}
_gw->getEventQueue()->mouseDoubleButtonPress(event->x(), event->y(), button);
}
void QOSGWidget::mouseReleaseEvent( QMouseEvent* event )
{
int button = 0;
switch(event->button())
{
case(Qt::LeftButton): button = 1; break;
case(Qt::MidButton): button = 2; break;
case(Qt::RightButton): button = 3; break;
case(Qt::NoButton): button = 0; break;
default: button = 0; break;
}
_gw->getEventQueue()->mouseButtonRelease(event->x(), event->y(), button);
}
void QOSGWidget::mouseMoveEvent( QMouseEvent* event )
{
_gw->getEventQueue()->mouseMotion(event->x(), event->y());
}
#endif
void CompositeViewerQOSG::Tile()
{
int n = getNumViews() - 1; // -1 to account for dummy view
for ( int i = 0; i < n; ++i )
{
osgViewer::View * view = getView(i+1); // +1 to account for dummy view
view->getCamera()->setViewport( new osg::Viewport( 0, i*height()/n , width(), height()/n ) );
view->getCamera()->setProjectionMatrixAsPerspective( 30.0f, double( width() ) / double( height()/n ), 1.0f, 10000.0f );
}
}
void CompositeViewerQOSG::AddView( osg::Node * scene )
{
osgViewer::View* view = new osgViewer::View;
addView(view);
view->setSceneData( scene );
view->setCameraManipulator(new osgGA::TrackballManipulator);
// add the state manipulator
osg::ref_ptr<osgGA::StateSetManipulator> statesetManipulator = new osgGA::StateSetManipulator;
statesetManipulator->setStateSet(view->getCamera()->getOrCreateStateSet());
view->getCamera()->setGraphicsContext( getGraphicsWindow() );
view->getCamera()->setClearColor( osg::Vec4( 0.08, 0.08, 0.5, 1.0 ) );
Tile();
}
void CompositeViewerQOSG::RemoveView()
{
if ( getNumViews() > 1 )
{
removeView( getView( getNumViews() - 1 ) );
}
Tile();
}
#if USE_QT4
// we use this wrapper for CompositeViewer ONLY because of the timer
// NOTE: this is a workaround because we're not using QT's moc precompiler here.
//
class QViewerTimer : public QWidget
{
public:
QViewerTimer (int fps = 20, QWidget * parent = 0, WindowFlags f = 0):
QWidget (parent, f)
{
_viewer = new osgViewer::CompositeViewer ();
_viewer->setThreadingModel(osgViewer::CompositeViewer::DrawThreadPerContext);
connect(&_timer, SIGNAL(timeout()), this, SLOT(repaint()));
_timer.start(1000.0f/fps);
}
~QViewerTimer ()
{
_timer.stop ();
}
virtual void paintEvent (QPaintEvent * event) { _viewer->frame(); }
osg::ref_ptr <osgViewer::CompositeViewer> _viewer;
QTimer _timer;
};
#endif
void setupHandlers(osgViewer::View * viewer)
{
// add the state manipulator
viewer->addEventHandler( new osgGA::StateSetManipulator(viewer->getCamera()->getOrCreateStateSet()) );
// add the thread model handler
viewer->addEventHandler(new osgViewer::ThreadingHandler);
// add the window size toggle handler