Browse Source

ENH: Add implementation of crosshair and magnification widgets.

- Crosshair: paints a crosshair on top of a QLabel's pixmap, allowing display of a crosshair on top of a small image.
- Magnify: observes QVTKWidget(s), and displays a magnification of the area around the mouse cursor.

This is a combination of 63 commits.
ENH: Begin adding ctkCursorPixmapWidget

- A widget that displays a cursor (ex. crosshair) on top of a QLabel's pixmap

ENH: Begin adding ctkVTKMagnifyWidget

- zooms in on a VTKWidget as triggered by mouse enter/leave/move events

ENH: Expose the QVTKWidget within ctkVTKAbstractView

ENH: Move implementation from magnifyWidget to cursorPixmapWidget

- Move blankColor and updateWidget() to cursorPixmapWidget from magnifyWidget
- Private implementation of magnifyWidget inherits from privateImplementation of cursorPixmapWidget

Associated export directive with ctkCursorPixmapWidgetPrivate

That's solving linking issue when deriving the pimpl from other library.

STYLE: Remove unnecessary #include and #define

ENH: Add magnifification (zoom)

ENH: Draw crosshair cursor

STYLE: comments

ENH: Specify background color when cursor close to window border

ENH: Read from vtkRenderWindow properly, handle alignment near borders

- Works when not scaling
- Do computations with doubles to prep for accurate scaling

ENH: Handle when label widget larger than magnified pixmap

ENH: Implement CrossHair and BullsEye cursors

- CrossHair: simple cross (reimplement with floats)
- BullsEye: central rectangle with four lines (as in Slicer3)

STYLE: casting

ENH: Scale image for magnification

STYLE: Comments

STYLE: ctkCursorPixmapWidget does not need updatePixmap function

ENH: Blank out pixmap for ctkVTKMagnifyWidget on LeaveEvent

STYLE: No need to ctkVTKMagnifyWidget EnterEvent

STYLE: Default ctkVTKMagnifyWidget background color and magnification

ENH: Add ctkVTKMagnifyWidget plugin

STYLE: Qt includes before CTK includes

STYLE: BullsEyeWidth property should be lower case

STYLE: Redeclare virtual when overloading functions

ENH: Add MarginColor in ctkCursorPixmapWidget = background in ctkVTKMagnifyWidget

- Default value is the widget's palette's Window color

ENH: Support BullsEyeWidth of 0

STYLE: Error checking and clean up

ENH: Add test for ctkCursorPixmapWidget

ENH: Link ctkCursorPixmapWidget cursorColor to Highlight role

STYLE: Remove unused reference to d

STYLE: Move blankOutPixmap from CursorPixmapWidget->MagnifyWidget

ENH: ctkVTKMagnifyWidget emits signals when entering/leaving observed widget

ENH: Add ctkCursorPixmapWidget Plugin

ENH: Update pixmap on enter events

- Required for when the widget is created with the mouse already within it

ENH: Test ctkCursorPixmapWidget with transparent colors

ENH: Default ctkCursorPixmapWidget alignment is centered

ENH: ctkCursorPixmapWidgetTest compares output to baseline images

ENH: Split ctkCursorPixmapWidget tests into two (with and without CTKData)

ENH: ctkCTKMagnifyWidget returns number of observed widgets

STYLE: Small code cleanup in ctkCursorPixmapWidget tests

PERF: Do not need to fill pixmap with solid color, just remove it

STYLE: bullsEyeWidth READ function should be lowercase

BUG: Must call init() in ctkCursorPixmapWidget ctor with pimpl parameter

STYLE: ctkCursorPixmapWidgetTest2 error message clarified

ENH: Add tests for ctkVTKMagnifyWidget

STYLE: Update comment

BUG: ctkVTKMagnifyWidget typedef of Superclass had typo

STYLE: Indentation

ENH: Add error checking for bad mouse positions

ENH: Fix partial pixels by cropping scaled image before display

- Correct cropping deals with label size, window size, and magnification values that do not nicely divide each other
- Correct cropping prevents need for centering pixmap in label
- Mouse position corresponds to a pixel in the renderWindow

ENH: Tweak ctkVTKMagnifyWidgetTest2

- Use a non-standard magnification
- Label size does not equal observed widget sizes

ENH: ctkCursorPixmapWidgetTest2 relies on input Data/computerIcon.png

- Using the icon from the cursor's style gave a different image on different machines, so use a defined input image instead

ENH: ctkVTKMagnifyWidgetTest2 uses Data/computerIcon.png as input

ENH: ctkVTKMagnifyWidgetTest2: not necessary to move widget to a certain position

BUG: Initial widget positioning required for ctkVTKMagnifyWidgetTest2 on some machines

STYLE: ctkVTKMagnifyWidgetPrivate no longer needs to inherit ctkCursorPixmapWidgetPrivate

BUG: Fix un-centered cursor

ENH: ctkVTKMagnifyWidgetTest2, on error: output to std::cerr and remember return value

ENH: ctkCursorPixmapWidgetTest2 tests even/odd parameter specifications

- i.e. size of the cursor widget and the bulls-eye width

ENH: Abort ctkCursorPixmapWidgetTest2 if running a non-interactive test

STYLE: Clarify error message

ENH: Clean up ctkVTKMagnifyWidgetTest2

- Ensure magnify widget size and bulls-eye width are both odd
- Simplify code by adding "runBaselineTest" function

ENH: Add sizeHints to ctkCursorPixmapWidget
Danielle Pace 14 years ago
parent
commit
4cc9a20dc3

+ 3 - 0
Libs/Visualization/VTK/Widgets/CMakeLists.txt

@@ -19,6 +19,8 @@ SET(KIT_SRCS
   ctkVTKDataSetModel.h
   ctkVTKDataSetArrayComboBox.cpp
   ctkVTKDataSetArrayComboBox.h
+  ctkVTKMagnifyWidget.cpp
+  ctkVTKMagnifyWidget.h
   ctkVTKMatrixWidget.cpp
   ctkVTKMatrixWidget.h
   ctkVTKRenderView.cpp
@@ -44,6 +46,7 @@ SET(KIT_MOC_SRCS
   ctkVTKAbstractView_p.h
   ctkVTKDataSetArrayComboBox.h
   ctkVTKDataSetModel.h
+  ctkVTKMagnifyWidget.h
   ctkVTKMatrixWidget.h
   ctkVTKRenderView.h
   ctkVTKRenderView_p.h

+ 3 - 0
Libs/Visualization/VTK/Widgets/Plugins/CMakeLists.txt

@@ -15,6 +15,8 @@ SET(PLUGIN_SRCS
 
   ctkVTKDataSetArrayComboBoxPlugin.cpp
   ctkVTKDataSetArrayComboBoxPlugin.h
+  ctkVTKMagnifyWidgetPlugin.cpp
+  ctkVTKMagnifyWidgetPlugin.h
   ctkVTKRenderViewPlugin.cpp
   ctkVTKRenderViewPlugin.h
   ctkVTKScalarBarWidgetPlugin.cpp
@@ -32,6 +34,7 @@ SET(PLUGIN_MOC_SRCS
   ctkVTKWidgetsPlugins.h
 
   ctkVTKDataSetArrayComboBoxPlugin.h
+  ctkVTKMagnifyWidgetPlugin.h
   ctkVTKRenderViewPlugin.h
   ctkVTKScalarBarWidgetPlugin.h
   ctkVTKSliceViewPlugin.h

+ 69 - 0
Libs/Visualization/VTK/Widgets/Plugins/ctkVTKMagnifyWidgetPlugin.cpp

@@ -0,0 +1,69 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// CTK includes
+#include "ctkVTKMagnifyWidgetPlugin.h"
+#include "ctkVTKMagnifyWidget.h"
+
+//-----------------------------------------------------------------------------
+ctkVTKMagnifyWidgetPlugin::ctkVTKMagnifyWidgetPlugin(QObject *_parent):QObject(_parent)
+{
+}
+
+//-----------------------------------------------------------------------------
+QWidget *ctkVTKMagnifyWidgetPlugin::createWidget(QWidget *_parent)
+{
+  ctkVTKMagnifyWidget* _widget = new ctkVTKMagnifyWidget(_parent);
+  return _widget;
+}
+
+//-----------------------------------------------------------------------------
+QString ctkVTKMagnifyWidgetPlugin::domXml() const
+{
+  return "<widget class=\"ctkVTKMagnifyWidget\" \
+          name=\"VTKMagnify\">\n"
+          " <property name=\"geometry\">\n"
+          "  <rect>\n"
+          "   <x>0</x>\n"
+          "   <y>0</y>\n"
+          "   <width>150</width>\n"
+          "   <height>150</height>\n"
+          "  </rect>\n"
+          " </property>\n"
+          "</widget>\n";
+}
+
+//-----------------------------------------------------------------------------
+QString ctkVTKMagnifyWidgetPlugin::includeFile() const
+{
+  return "ctkVTKMagnifyWidget.h";
+}
+
+//-----------------------------------------------------------------------------
+bool ctkVTKMagnifyWidgetPlugin::isContainer() const
+{
+  return false;
+}
+
+//-----------------------------------------------------------------------------
+QString ctkVTKMagnifyWidgetPlugin::name() const
+{
+  return "ctkVTKMagnifyWidget";
+}

+ 44 - 0
Libs/Visualization/VTK/Widgets/Plugins/ctkVTKMagnifyWidgetPlugin.h

@@ -0,0 +1,44 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+#ifndef __ctkVTKMagnifyWidgetPlugin_h
+#define __ctkVTKMagnifyWidgetPlugin_h
+
+// CTK includes
+#include "ctkVTKWidgetsAbstractPlugin.h"
+
+class CTK_VISUALIZATION_VTK_WIDGETS_PLUGINS_EXPORT ctkVTKMagnifyWidgetPlugin :
+  public QObject,
+  public ctkVTKWidgetsAbstractPlugin
+{
+  Q_OBJECT
+
+public:
+  ctkVTKMagnifyWidgetPlugin(QObject *_parent = 0);
+  
+  QWidget *createWidget(QWidget *_parent);
+  QString domXml() const;
+  QString includeFile() const;
+  bool isContainer() const;
+  QString name() const;
+  
+};
+
+#endif

+ 2 - 0
Libs/Visualization/VTK/Widgets/Plugins/ctkVTKWidgetsPlugins.h

@@ -27,6 +27,7 @@
 // CTK includes
 #include "ctkVisualizationVTKWidgetsPluginsExport.h"
 #include "ctkVTKDataSetArrayComboBoxPlugin.h"
+#include "ctkVTKMagnifyWidgetPlugin.h"
 #include "ctkVTKRenderViewPlugin.h"
 #include "ctkVTKScalarBarWidgetPlugin.h"
 #ifdef CTK_USE_CHARTS
@@ -49,6 +50,7 @@ public:
     {
     QList<QDesignerCustomWidgetInterface *> plugins;
     plugins << new ctkVTKDataSetArrayComboBoxPlugin;
+    plugins << new ctkVTKMagnifyWidgetPlugin;
     plugins << new ctkVTKRenderViewPlugin;
     plugins << new ctkVTKScalarBarWidgetPlugin;
 #ifdef CTK_USE_CHARTS

+ 8 - 1
Libs/Visualization/VTK/Widgets/Testing/Cpp/CMakeLists.txt

@@ -7,6 +7,7 @@ SET(TEST_SOURCES
   ctkVTKDataSetArrayComboBoxTest1.cpp
   ctkVTKDataSetModelTest1.cpp
   ctkVTKMatrixWidgetTest1.cpp
+  ctkVTKMagnifyWidgetTest1.cpp
   ctkVTKScalarBarWidgetTest1.cpp
   ctkTransferFunctionBarsItemTest1.cpp
   ctkTransferFunctionViewTest1.cpp
@@ -37,6 +38,7 @@ ENDIF(CTK_USE_CHARTS)
 #
 IF(EXISTS "${CTKData_DIR}")
   LIST(APPEND TEST_SOURCES
+    ctkVTKMagnifyWidgetTest2.cpp
     ctkVTKSliceViewTest2.cpp
     ctkVTKRenderViewTest2.cpp
     )
@@ -58,7 +60,10 @@ TARGET_LINK_LIBRARIES(${KIT}CppTests ${LIBRARY_NAME} vtkCharts ${CTK_BASE_LIBRAR
 SET( KIT_TESTS ${CPP_TEST_PATH}/${KIT}CppTests)
 
 MACRO( SIMPLE_TEST  TESTNAME )
-  ADD_TEST( ${TESTNAME} ${KIT_TESTS} ${TESTNAME} -D "${CTKData_DIR}/Data")
+  ADD_TEST( ${TESTNAME} ${KIT_TESTS} ${TESTNAME}
+            -D "${CTKData_DIR}/Data"
+            -V "${CTKData_DIR}/Baseline/Libs/Visualization/VTK/Widgets"
+            )
   SET_PROPERTY(TEST ${TESTNAME} PROPERTY LABELS ${PROJECT_NAME})
 ENDMACRO( SIMPLE_TEST  )
 
@@ -68,6 +73,7 @@ ENDMACRO( SIMPLE_TEST  )
 
 SIMPLE_TEST( ctkVTKDataSetArrayComboBoxTest1 )
 SIMPLE_TEST( ctkVTKDataSetModelTest1 )
+SIMPLE_TEST( ctkVTKMagnifyWidgetTest1 )
 SIMPLE_TEST( ctkVTKMatrixWidgetTest1 )
 SIMPLE_TEST( ctkVTKScalarBarWidgetTest1 )
 SIMPLE_TEST( ctkTransferFunctionBarsItemTest1 )
@@ -95,6 +101,7 @@ SIMPLE_TEST( ctkVTKThumbnailViewTest1 )
 #
 IF(EXISTS "${CTKData_DIR}")
   #
+  SIMPLE_TEST( ctkVTKMagnifyWidgetTest2 )
   SIMPLE_TEST( ctkVTKSliceViewTest2 )
   SIMPLE_TEST( ctkVTKRenderViewTest2 )
 ENDIF()

+ 171 - 0
Libs/Visualization/VTK/Widgets/Testing/Cpp/ctkVTKMagnifyWidgetTest1.cpp

@@ -0,0 +1,171 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QApplication>
+
+// CTK includes
+#include "ctkVTKMagnifyWidget.h"
+
+// VTK includes
+#include <QVTKWidget.h>
+
+// STD includes
+#include <cstdlib>
+#include <iostream>
+
+//-----------------------------------------------------------------------------
+int ctkVTKMagnifyWidgetTest1(int argc, char * argv [] )
+{
+  QApplication app(argc, argv);
+
+  ctkVTKMagnifyWidget magnify;
+
+  // check default values
+  if (!magnify.showCursor() ||
+      magnify.cursorColor() != magnify.palette().color(QPalette::Highlight) ||
+      magnify.cursorType() != ctkCursorPixmapWidget::CrossHairCursor ||
+      magnify.marginColor() != magnify.palette().color(QPalette::Window) ||
+      magnify.bullsEyeWidth() != 15 ||
+      magnify.magnification() != 1.0 ||
+      magnify.numberObserved() != 0)
+    {
+    std::cerr << "ctkVTKMagnifyWidget: Wrong default values. " << std::endl
+              << " " << magnify.showCursor()
+              << " " << qPrintable(magnify.cursorColor().name())
+              << " " << magnify.cursorType()
+              << " " << qPrintable(magnify.marginColor().name())
+              << " " << magnify.bullsEyeWidth()
+              << " " << magnify.magnification()
+              << " " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Magnification
+  magnify.setMagnification(10.5);
+  if (magnify.magnification() != 10.5)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:setMagnification failed. "
+              << magnify.magnification() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Adding / removing observed QVTKWidgets
+  QList<QVTKWidget *> allVTKWidgets;
+  int numVTKWidgets = 3;
+  for (int i = 0; i < numVTKWidgets; i++)
+    {
+    allVTKWidgets.append(new QVTKWidget());
+    }
+
+  // Observe one widget
+  magnify.observe(allVTKWidgets[0]);
+  if (!magnify.isObserved(allVTKWidgets[0]) || magnify.isObserved(allVTKWidgets[1]) ||
+      magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 1)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:observe(QVTKWidget*) failed. "
+              << "Number observed = " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Observe two more widgets
+  magnify.observe(allVTKWidgets.mid(1,2));
+  if (!magnify.isObserved(allVTKWidgets[0]) || !magnify.isObserved(allVTKWidgets[1]) ||
+      !magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 3)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:observe(QList<QVTKWidget*>) failed. "
+              << "Number observed = " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Re-observe a widget
+  magnify.observe(allVTKWidgets[2]);
+  if (!magnify.isObserved(allVTKWidgets[0]) || !magnify.isObserved(allVTKWidgets[1]) ||
+      !magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 3)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:observe(QVTKWidget*) failed on re-observe. "
+              << "Number observed = " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Re-observe a list of widgets
+  magnify.observe(allVTKWidgets.mid(0,2));
+  if (!magnify.isObserved(allVTKWidgets[0]) || !magnify.isObserved(allVTKWidgets[1]) ||
+      !magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 3)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:observe(QList<QVTKWidget*>) failed on re-observe. "
+              << "Number observed = " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Remove a widget
+  magnify.remove(allVTKWidgets[2]);
+  if (!magnify.isObserved(allVTKWidgets[0]) || !magnify.isObserved(allVTKWidgets[1]) ||
+      magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 2)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:remove(QVTKWidget*) failed. "
+              << "Number observed = " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Re-remove a widget
+  magnify.remove(allVTKWidgets[2]);
+  if (!magnify.isObserved(allVTKWidgets[0]) || !magnify.isObserved(allVTKWidgets[1]) ||
+      magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 2)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:remove(QVTKWidget*) failed on re-remove. "
+              << "Number observed = " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Remove a list of widgets
+  magnify.remove(allVTKWidgets.mid(0,2));
+  if (magnify.isObserved(allVTKWidgets[0]) || magnify.isObserved(allVTKWidgets[1]) ||
+      magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 0)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:remove(QList<QVTKWidget*>) failed. "
+              << "Number observed = " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Re-remove a list of widgets
+  magnify.remove(allVTKWidgets.mid(1,2));
+  if (magnify.isObserved(allVTKWidgets[0]) || magnify.isObserved(allVTKWidgets[1]) ||
+      magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 0)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:remove(QList<QVTKWidget*>) failed on re-remove. "
+              << "Number observed = " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Observe a list of widgets of length one
+  magnify.observe(allVTKWidgets.mid(1,1));
+  if (magnify.isObserved(allVTKWidgets[0]) || !magnify.isObserved(allVTKWidgets[1]) ||
+      magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 1)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:observe(QList<QVTKWidget*>) failed on lists of "
+              << "length 1. Number observed = " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  return EXIT_SUCCESS;
+
+}
+

+ 335 - 0
Libs/Visualization/VTK/Widgets/Testing/Cpp/ctkVTKMagnifyWidgetTest2.cpp

@@ -0,0 +1,335 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QApplication>
+#include <QCursor>
+#include <QHBoxLayout>
+#include <QIcon>
+#include <QSignalSpy>
+#include <QStyle>
+#include <QTimer>
+
+// CTK includes
+#include "ctkVTKMagnifyWidget.h"
+#include "ctkCommandLineParser.h"
+#include "ctkVTKSliceView.h"
+
+// VTK includes
+#include <vtkImageReader2Factory.h>
+#include <vtkImageReader2.h>
+#include <vtkImageData.h>
+#include <vtkSmartPointer.h>
+
+// STD includes
+#include <cstdlib>
+#include <iostream>
+
+//-----------------------------------------------------------------------------
+bool imageCompare(ctkVTKMagnifyWidget * magnify, QString baselineDirectory,
+                  QString baselineFilename)
+  {
+  QImage output = QPixmap::grabWidget(magnify).toImage();
+  QImage baseline(baselineDirectory + "/" + baselineFilename);
+  return output == baseline;
+  }
+
+//-----------------------------------------------------------------------------
+// (Used to create baselines, not during testing).
+void imageSave(ctkVTKMagnifyWidget * magnify, QString baselineDirectory,
+               QString baselineFilename)
+  {
+  QImage output = QPixmap::grabWidget(magnify).toImage();
+  output.save(baselineDirectory + "/" + baselineFilename);
+  }
+
+//-----------------------------------------------------------------------------
+bool runBaselineTest(int time, QApplication& app, ctkVTKMagnifyWidget * magnify,
+                     QWidget * underWidget, bool shouldBeUnder,
+                     QString baselineDirectory, QString baselineFilename,
+                     QString errorMessage)
+{
+  QTimer::singleShot(time, &app, SLOT(quit()));
+  if (app.exec() == EXIT_FAILURE)
+    {
+    std::cerr << "ctkVTKMagnifyWidget exec failed when "
+        << qPrintable(errorMessage) << std::endl;
+    return false;
+    }
+  if (underWidget->underMouse() != shouldBeUnder)
+    {
+    std::cerr << "ctkMagnifyWidget mouse position failed when "
+        << qPrintable(errorMessage) << std::endl;
+    return false;
+    }
+  if (!imageCompare(magnify, baselineDirectory, baselineFilename))
+    {
+    std::cerr << "ctkVTKMagnifyWidget baseline comparison failed when "
+              << qPrintable(errorMessage) << "." << std::endl;
+
+    return false;
+    }
+  return true;
+}
+
+//-----------------------------------------------------------------------------
+int ctkVTKMagnifyWidgetTest2(int argc, char * argv [] )
+{
+  QApplication app(argc, argv);
+
+  // Command line parser
+  ctkCommandLineParser parser;
+  parser.addArgument("", "-D", QVariant::String);
+  parser.addArgument("", "-V", QVariant::String);
+  parser.addArgument("", "-I", QVariant::String);
+  bool ok = false;
+  QHash<QString, QVariant> parsedArgs = parser.parseArguments(app.arguments(), &ok);
+  if (!ok)
+    {
+    std::cerr << qPrintable(parser.errorString()) << std::endl;
+    return EXIT_FAILURE;
+    }
+  QString dataDirectory = parsedArgs["-D"].toString();
+  QString baselineDirectory = parsedArgs["-V"].toString();
+  bool interactive = parsedArgs["-I"].toBool();
+
+  // Create the parent widget
+  QWidget parentWidget;
+  QHBoxLayout layout(&parentWidget);
+
+  // Magnify widget parameters (we want an odd widget size and odd bullsEye)
+  int size = 341;
+  bool showCursor = true;
+  QColor cursorColor = Qt::yellow;
+  ctkCursorPixmapWidget::CursorType cursorType
+      = ctkCursorPixmapWidget::BullsEyeCursor;
+  double magnification = 17;
+  QColor marginColor = Qt::magenta;
+
+  // Create the magnify widget
+  ctkVTKMagnifyWidget * magnify = new ctkVTKMagnifyWidget(&parentWidget);
+  magnify->setMinimumSize(size,size);
+  magnify->setMaximumSize(size,size);
+  magnify->setShowCursor(showCursor);
+  magnify->setCursorColor(cursorColor);
+  magnify->setCursorType(cursorType);
+  magnify->setBullsEyeWidth(magnification);
+  magnify->setMarginColor(marginColor);
+  magnify->setMagnification(magnification);
+  layout.addWidget(magnify);
+
+  // Test magnify widget parameters
+  if (magnify->showCursor() != showCursor)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:setShowCursor failed. "
+              << magnify->showCursor() << std::endl;
+    return EXIT_FAILURE;
+    }
+  if (magnify->cursorColor() != cursorColor)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:setCursorColor failed. "
+              << qPrintable(magnify->cursorColor().name()) << std::endl;
+    return EXIT_FAILURE;
+    }
+  if (magnify->cursorType() != cursorType)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:setCursorType failed. "
+              << magnify->cursorType() << std::endl;
+    return EXIT_FAILURE;
+    }
+  if (magnify->bullsEyeWidth() != magnification)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:setBullsEyeWidth failed. "
+              << magnify->bullsEyeWidth() << std::endl;
+    return EXIT_FAILURE;
+    }
+  if (magnify->marginColor() != marginColor)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:setMarginColor failed. "
+              << qPrintable(magnify->marginColor().name()) << std::endl;
+    return EXIT_FAILURE;
+    }
+  if (magnify->magnification() != magnification)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:setMagnification failed. "
+              << magnify->magnification() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // The remainder is interactive, so abort now if command line args specify otherwise
+  if (interactive)
+    {
+    return EXIT_SUCCESS;
+    }
+
+  // Add observed ctkVTKSliceViews (there are three, and the first two are observed)
+  QList<ctkVTKSliceView *> allSliceViews;
+  int numSliceViews = 3;
+  for (int i = 0; i < numSliceViews; i++)
+    {
+    allSliceViews.append(new ctkVTKSliceView(&parentWidget));
+    layout.addWidget(allSliceViews[i]);
+    }
+
+  // Observe the first two widgets
+  magnify->observe(allSliceViews[0]->VTKWidget());
+  magnify->observe(allSliceViews[1]->VTKWidget());
+  if (!magnify->isObserved(allSliceViews[0]->VTKWidget()) ||
+      !magnify->isObserved(allSliceViews[1]->VTKWidget()) ||
+      magnify->isObserved(allSliceViews[2]->VTKWidget()) ||
+      magnify->numberObserved() != 2)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:observe(QVTKWidget*) failed. "
+              << "Number observed = " << magnify->numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  QString imageFilename = dataDirectory + "/" + "computerIcon.png";
+
+  // Instanciate the reader factory
+  vtkSmartPointer<vtkImageReader2Factory> imageFactory =
+      vtkSmartPointer<vtkImageReader2Factory>::New();
+
+  // Instanciate an image reader
+  vtkSmartPointer<vtkImageReader2> imageReader;
+  imageReader.TakeReference(imageFactory->CreateImageReader2(imageFilename.toLatin1()));
+  if (!imageReader)
+    {
+    std::cerr << "Failed to instanciate image reader using: "
+              << qPrintable(imageFilename) << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Read image
+  imageReader->SetFileName(imageFilename.toLatin1());
+  imageReader->Update();
+  vtkSmartPointer<vtkImageData> image = imageReader->GetOutput();
+
+  // Setup the slice views
+  for (int i = 0; i < numSliceViews; i++)
+    {
+    allSliceViews[i]->setRenderEnabled(true);
+    allSliceViews[i]->setMinimumSize(350,350);
+    allSliceViews[i]->setImageData(image);
+    allSliceViews[i]->setHighlightedBoxColor(Qt::yellow);
+    allSliceViews[i]->scheduleRender();
+    }
+
+  int time = 200;
+
+  // Get cursor points of interest, used in the following tests
+  parentWidget.move(0,0);
+  parentWidget.show();
+  QTimer::singleShot(time, &app, SLOT(quit()));
+  if (app.exec() == EXIT_FAILURE)
+    {
+    std::cerr << "ctkVTKMagnifyWidget:show failed the first time." << std::endl;
+    return EXIT_FAILURE;
+    }
+  QPoint insideSlice0 = allSliceViews[0]->mapToGlobal(
+        QPoint(allSliceViews[0]->width()/2 + 100, allSliceViews[0]->height()/2 + 100));
+  QPoint outside = parentWidget.mapToGlobal(
+        QPoint(parentWidget.width(), parentWidget.height()));
+  QPoint insideSlice1 = allSliceViews[1]->mapToGlobal(
+        QPoint(allSliceViews[1]->width()/2 - 50, allSliceViews[1]->height() - 50));
+  QPoint insideSlice1edge = allSliceViews[1]->mapToGlobal(
+        QPoint(allSliceViews[1]->width() - 5, allSliceViews[1]->height() - 100));
+  QPoint insideSlice2 = allSliceViews[2]->mapToGlobal(
+        QPoint(allSliceViews[2]->width()/2, allSliceViews[2]->height()/2));
+  QPoint insideSlice0bottomRightCorner = allSliceViews[0]->mapToGlobal(
+        QPoint(allSliceViews[0]->width()-1, allSliceViews[0]->height()-1));
+  QPoint insideSlice0topLeftCorner = allSliceViews[0]->mapToGlobal(
+        QPoint(0,0));
+  parentWidget.hide();
+
+  // Make sure the magnify widget magnifies right away when shown with the cursor inside
+  // an observed QVTKWidget
+  QCursor::setPos(insideSlice0);
+  parentWidget.show();
+  if (!runBaselineTest(time, app, magnify, allSliceViews[0], true,
+                       baselineDirectory, "ctkVTKMagnifyWidgetTest2a.png",
+                       "magnify widget first shown with cursor inside observed widget"))
+    {
+    return EXIT_FAILURE;
+    }
+  parentWidget.hide();
+
+  // Make sure the magnify widget shows blank right away when shown with the cursor
+  // outside the observed QVTKWidgets
+  QCursor::setPos(outside);
+  parentWidget.show();
+  if (!runBaselineTest(time, app, magnify, &parentWidget, false,
+                       baselineDirectory, "ctkVTKMagnifyWidgetTest2b.png",
+                       "magnify widget first shown with cursor outside observed widget"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Test magnification after move to allSliceViews[1]
+  QCursor::setPos(insideSlice1);
+  if (!runBaselineTest(time, app, magnify, allSliceViews[1], true,
+                       baselineDirectory, "ctkVTKMagnifyWidgetTest2c.png",
+                       "cursor moved inside 2nd observed widget the first time"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Test magnification after move within allSliceViews[1] close to border
+  QCursor::setPos(insideSlice1edge);
+  if (!runBaselineTest(time, app, magnify, allSliceViews[1], true,
+                       baselineDirectory, "ctkVTKMagnifyWidgetTest2d.png",
+                       "cursor moved inside 2nd observed widget the second time"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Test magnification after move outside an observed widget (should be blank)
+  QCursor::setPos(insideSlice2);
+  if (!runBaselineTest(time, app, magnify, allSliceViews[2], true,
+                       baselineDirectory, "ctkVTKMagnifyWidgetTest2e.png",
+                       "cursor moved inside unobserved widget"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Test magnification after move back inside an observed widget (at extreme bottom
+  // right corner)
+  QCursor::setPos(insideSlice0bottomRightCorner);
+  if (!runBaselineTest(time, app, magnify, allSliceViews[0], true,
+                       baselineDirectory, "ctkVTKMagnifyWidgetTest2f.png",
+                       "cursor moved to bottom-right corner of observed widget"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Test magnification after move back inside an observed widget (at extreme top left
+  // corner)
+  QCursor::setPos(insideSlice0topLeftCorner);
+  if (!runBaselineTest(time, app, magnify, allSliceViews[0], true,
+                       baselineDirectory, "ctkVTKMagnifyWidgetTest2g.png",
+                       "cursor moved to top-left corner of observed widget"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  return EXIT_SUCCESS;
+
+}
+

+ 7 - 0
Libs/Visualization/VTK/Widgets/ctkVTKAbstractView.cpp

@@ -201,6 +201,13 @@ vtkCornerAnnotation* ctkVTKAbstractView::cornerAnnotation() const
 }
 
 //----------------------------------------------------------------------------
+QVTKWidget * ctkVTKAbstractView::VTKWidget() const
+{
+  Q_D(const ctkVTKAbstractView);
+  return d->VTKWidget;
+}
+
+//----------------------------------------------------------------------------
 CTK_SET_CPP(ctkVTKAbstractView, bool, setRenderEnabled, RenderEnabled);
 CTK_GET_CPP(ctkVTKAbstractView, bool, renderEnabled, RenderEnabled);
 

+ 6 - 0
Libs/Visualization/VTK/Widgets/ctkVTKAbstractView.h

@@ -24,6 +24,9 @@
 // Qt includes
 #include <QWidget>
 
+// VTK includes
+#include <QVTKWidget.h>
+
 // CTK includes
 #include "ctkVisualizationVTKWidgetsExport.h"
 class ctkVTKAbstractViewPrivate;
@@ -77,6 +80,9 @@ public:
   QString cornerAnnotationText() const;
   vtkCornerAnnotation* cornerAnnotation()const;
 
+  /// Get the underlying QVTKWidget
+  QVTKWidget * VTKWidget() const;
+
   /// Get background color
   virtual QColor backgroundColor() const = 0;
 

+ 388 - 0
Libs/Visualization/VTK/Widgets/ctkVTKMagnifyWidget.cpp

@@ -0,0 +1,388 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QEvent>
+#include <QMouseEvent>
+#include <QPointF>
+
+// CTK includes
+#include "ctkVTKMagnifyWidget.h"
+#include "ctkLogger.h"
+
+// VTK includes
+#include <QVTKWidget.h>
+#include <vtkRenderWindow.h>
+#include <vtkUnsignedCharArray.h>
+
+// Convenient macro
+#define VTK_CREATE(type, name) \
+  vtkSmartPointer<type> name = vtkSmartPointer<type>::New()
+
+//--------------------------------------------------------------------------
+static ctkLogger logger("org.commontk.visualization.vtk.widgets.ctkVTKMagnifyWidget");
+//--------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+class ctkVTKMagnifyWidgetPrivate
+{
+  Q_DECLARE_PUBLIC(ctkVTKMagnifyWidget);
+protected:
+  ctkVTKMagnifyWidget* const q_ptr;
+public:
+  ctkVTKMagnifyWidgetPrivate(ctkVTKMagnifyWidget& object);
+
+  void init();
+  void removePixmap();
+  void observe(QVTKWidget * widget);
+  void remove(QVTKWidget * widget);
+  void updatePixmap(QVTKWidget * widget, QPointF pos);
+
+  QList<QVTKWidget *> ObservedQVTKWidgets;
+  double Magnification;
+};
+
+// --------------------------------------------------------------------------
+// ctkVTKMagnifyWidgetPrivate methods
+
+// --------------------------------------------------------------------------
+ctkVTKMagnifyWidgetPrivate::ctkVTKMagnifyWidgetPrivate(ctkVTKMagnifyWidget& object)
+  :q_ptr(&object)
+{
+  this->ObservedQVTKWidgets = QList<QVTKWidget *>();
+  this->Magnification = 1.0;
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyWidgetPrivate::init()
+{
+  this->removePixmap();
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyWidgetPrivate::removePixmap()
+{
+  Q_Q(ctkVTKMagnifyWidget);
+  QPixmap nullPixmap;
+  q->setPixmap(nullPixmap);
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyWidgetPrivate::observe(QVTKWidget * widget)
+{
+  Q_ASSERT(widget);
+
+  // If we are not already observing the widget, add it to the list and install
+  // the public implementation as the event filter to handle mousing
+  if (!this->ObservedQVTKWidgets.contains(widget))
+    {
+    this->ObservedQVTKWidgets.append(widget);
+    Q_Q(ctkVTKMagnifyWidget);
+    widget->installEventFilter(q);
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyWidgetPrivate::remove(QVTKWidget * widget)
+{
+  Q_ASSERT(widget);
+
+  // If we are observing the widget, remove it from the list and remove the
+  // public implementations event filtering
+  if (this->ObservedQVTKWidgets.contains(widget))
+    {
+    Q_ASSERT(this->ObservedQVTKWidgets.count(widget) == 1);
+    this->ObservedQVTKWidgets.removeOne(widget);
+    Q_Q(ctkVTKMagnifyWidget);
+    widget->removeEventFilter(q);
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyWidgetPrivate::updatePixmap(QVTKWidget * widget, QPointF pos)
+{
+  Q_ASSERT(widget);
+  Q_Q(ctkVTKMagnifyWidget);
+
+  // Retrieve buffer of given QVTKWidget from its render window
+  vtkRenderWindow * renderWindow = widget->GetRenderWindow();
+  if (!renderWindow)
+    {
+    this->removePixmap();
+    return;
+    }
+
+  // Get the window size and mouse position, and do error checking
+  int * windowSize = renderWindow->GetSize();
+  QPointF mouseWindowPos(pos.x(), static_cast<double>(windowSize[1]-1)-pos.y());
+  if (mouseWindowPos.x() < 0 || mouseWindowPos.x() >= windowSize[0] ||
+      mouseWindowPos.y() < 0 || mouseWindowPos.y() >= windowSize[1])
+    {
+    this->removePixmap();
+    return;
+    }
+
+  // Compute indices into the render window's data array
+  // Given a magnification and the label's widget size, compute the number of
+  // pixels we can show.  We should round to get a larger integer extent, since
+  // we will later adjust the pixmap's location in paintEvent().
+  QSizeF sizeToMagnify = QSizeF(q->size()) / this->Magnification;
+  double dx0 = (mouseWindowPos.x() - ((sizeToMagnify.width()-1.0) / 2.0));
+  double dx1 = (mouseWindowPos.x() + ((sizeToMagnify.width()-1.0) / 2.0));
+  double dy0 = (mouseWindowPos.y() - ((sizeToMagnify.height()-1.0) / 2.0));
+  double dy1 = (mouseWindowPos.y() + ((sizeToMagnify.height()-1.0) / 2.0));
+
+  // Round to ints, for indexing into the pixel array
+  int ix0 = floor(dx0);
+  int ix1 = ceil(dx1);
+  int iy0 = floor(dy0);
+  int iy1 = ceil(dy1);
+
+  // Handle when mouse is near the border
+  int min_x0 = 0;
+  int max_x1 = windowSize[0]-1;
+  int min_y0 = 0;
+  int max_y1 = windowSize[1]-1;
+
+  bool overLeft = ix0 < min_x0;
+  bool overRight = ix1 > max_x1;
+  bool overBottom = iy0 < min_y0;
+  bool overTop = iy1 > max_y1;
+
+  // Ensure we don't access nonexistant indices
+  if (overLeft)
+    {
+    ix0 = min_x0;
+    dx0 = min_x0;
+    }
+  if (overRight)
+    {
+    ix1 = max_x1;
+    dx1 = max_x1;
+    }
+  if (overBottom)
+    {
+    iy0 = min_y0;
+    dy0 = min_y0;
+    }
+  if (overTop)
+    {
+    iy1 = max_y1;
+    dy1 = max_y1;
+    }
+
+  // Error case
+  if (ix0 > ix1 || iy0 > iy1)
+    {
+    this->removePixmap();
+    return;
+    }
+
+  // Setup the pixelmap's position in the label
+  Qt::Alignment alignment;
+  if (overLeft && !overRight)
+    {
+    alignment = Qt::AlignRight;
+    }
+  else if (overRight && !overLeft)
+    {
+    alignment = Qt::AlignLeft;
+    }
+  else
+    {
+    alignment = Qt::AlignLeft;
+    }
+  if (overBottom && !overTop)
+    {
+    alignment = alignment | Qt::AlignTop;
+    }
+  else if (overTop && !overBottom)
+    {
+    alignment = alignment | Qt::AlignBottom;
+    }
+  else
+    {
+    alignment = alignment | Qt::AlignTop;
+    }
+  q->setAlignment(alignment);
+
+  // Retrieve the pixel data into a QImage
+  QSize actualSize(ix1-ix0+1, iy1-iy0+1);
+  QImage image(actualSize.width(), actualSize.height(), QImage::Format_RGB32);
+  vtkUnsignedCharArray * pixelData = vtkUnsignedCharArray::New();
+  pixelData->SetArray(image.bits(), actualSize.width() * actualSize.height() * 4, 1);
+  int front = renderWindow->GetDoubleBuffer();
+  int success = renderWindow->GetRGBACharPixelData(ix0, iy0, ix1, iy1, front, pixelData);
+  if (!success)
+    {
+    this->removePixmap();
+    return;
+    }
+  pixelData->Delete();
+  image = image.rgbSwapped();
+  image = image.mirrored();
+
+  // Scale the image to zoom, using FastTransformation to prevent smoothing
+  QSize imageSize = actualSize * this->Magnification;
+  image = image.scaled(imageSize, Qt::KeepAspectRatioByExpanding, Qt::FastTransformation);
+
+  // Crop the magnified image to solve the problem of magnified partial pixels
+  int x0diff = round((dx0 - static_cast<double>(ix0)) * this->Magnification);
+  int x1diff = imageSize.width()
+      - round((static_cast<double>(ix1) - dx1) * this->Magnification);
+  int y0diff = round((dy0 - static_cast<double>(iy0)) * this->Magnification);
+  int y1diff = imageSize.height()
+      - round((static_cast<double>(iy1) - dy1) * this->Magnification);
+  QRect cropRect(QPoint(x0diff, y0diff), QPoint(x1diff, y1diff));
+  image = image.copy(cropRect);
+
+  // Finally, set the pixelmap to the new one we have created
+  q->setPixmap(QPixmap::fromImage(image));
+}
+
+//---------------------------------------------------------------------------
+// ctkVTKMagnifyWidget methods
+
+// --------------------------------------------------------------------------
+ctkVTKMagnifyWidget::ctkVTKMagnifyWidget(QWidget* parentWidget)
+  : Superclass(parentWidget)
+  , d_ptr(new ctkVTKMagnifyWidgetPrivate(*this))
+{
+  Q_D(ctkVTKMagnifyWidget);
+  d->init();
+}
+
+// --------------------------------------------------------------------------
+ctkVTKMagnifyWidget::~ctkVTKMagnifyWidget()
+{
+}
+
+// --------------------------------------------------------------------------
+CTK_GET_CPP(ctkVTKMagnifyWidget, double, magnification, Magnification);
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyWidget::setMagnification(double newMagnification)
+{
+  if (newMagnification > 0)
+    {
+    Q_D(ctkVTKMagnifyWidget);
+    d->Magnification = newMagnification;
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyWidget::observe(QVTKWidget * widget)
+{
+  Q_D(ctkVTKMagnifyWidget);
+  if (widget)
+    {
+    d->observe(widget);
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyWidget::observe(QList<QVTKWidget *> widgets)
+{
+  foreach(QVTKWidget * widget, widgets)
+    {
+    this->observe(widget);
+    }
+}
+
+// --------------------------------------------------------------------------
+bool ctkVTKMagnifyWidget::isObserved(QVTKWidget * widget) const
+{
+  if (!widget)
+    {
+    return false;
+    }
+  Q_D(const ctkVTKMagnifyWidget);
+  return d->ObservedQVTKWidgets.contains(widget);
+}
+
+// --------------------------------------------------------------------------
+int ctkVTKMagnifyWidget::numberObserved() const
+  {
+  Q_D(const ctkVTKMagnifyWidget);
+  return d->ObservedQVTKWidgets.length();
+  }
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyWidget::remove(QVTKWidget * widget)
+{
+  Q_D(ctkVTKMagnifyWidget);
+  if (widget)
+    {
+    d->remove(widget);
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyWidget::remove(QList<QVTKWidget *> widgets)
+{
+  foreach(QVTKWidget * widget, widgets)
+    {
+    this->remove(widget);
+    }
+}
+
+// --------------------------------------------------------------------------
+bool ctkVTKMagnifyWidget::eventFilter(QObject * obj, QEvent * event)
+{
+  // The given object should be a QVTKWidget in our list
+  QVTKWidget * widget = static_cast<QVTKWidget *>(obj);
+  Q_ASSERT(widget);
+  Q_D(ctkVTKMagnifyWidget);
+  Q_ASSERT(d->ObservedQVTKWidgets.contains(widget));
+
+  QEvent::Type eventType = event->type();
+  // On mouse move, update the pixmap with the zoomed image
+  if (eventType == QEvent::MouseMove)
+    {
+    QMouseEvent * mouseEvent = static_cast<QMouseEvent *>(event);
+    Q_ASSERT(mouseEvent);
+    d->updatePixmap(widget, mouseEvent->posF());
+    this->update();
+    return false;
+    }
+  // On enter, update the pixmap with the zoomed image (required for zooming when widget
+  // is created with mouse already within it), and emit signal of enter event
+  else if (eventType == QEvent::Enter)
+    {
+    QPointF posF = widget->mapFromGlobal(QCursor::pos());
+    d->updatePixmap(widget, posF);
+    this->update();
+    emit enteredObservedWidget(widget);
+    return false;
+    }
+  // On leave, fill the pixmap with a solid color and emit signal of leave event
+  else if (eventType == QEvent::Leave)
+    {
+    d->removePixmap();
+    this->update();
+    emit leftObservedWidget(widget);
+    return false;
+    }
+  // For other event types, use standard event processing
+  else
+    {
+    return QObject::eventFilter(obj, event);
+    }
+}

+ 94 - 0
Libs/Visualization/VTK/Widgets/ctkVTKMagnifyWidget.h

@@ -0,0 +1,94 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+#ifndef __ctkVTKMagnifyWidget_h
+#define __ctkVTKMagnifyWidget_h
+
+// QT includes
+#include <QList>
+
+// CTK includes
+#include "ctkCursorPixmapWidget.h"
+#include "ctkVisualizationVTKWidgetsExport.h"
+
+// VTK includes
+class QVTKWidget;
+
+class ctkVTKMagnifyWidgetPrivate;
+
+class CTK_VISUALIZATION_VTK_WIDGETS_EXPORT ctkVTKMagnifyWidget
+  : public ctkCursorPixmapWidget
+{
+  Q_OBJECT
+  Q_PROPERTY(double magnification READ magnification WRITE setMagnification);
+
+public:
+  /// Constructors
+  typedef ctkCursorPixmapWidget Superclass;
+  explicit ctkVTKMagnifyWidget(QWidget* parent = 0);
+  virtual ~ctkVTKMagnifyWidget();
+
+  /// Set/get the magnification (zoom).  Default 1.0.
+  double magnification() const;
+  void setMagnification(double newMagnification);
+
+  /// Add a QVTKWidget to observe mouse events on.  You can call this function
+  /// multiple times to observe multiple QVTKWidgets.
+  /// \sa observe
+  void observe(QVTKWidget * widget);
+
+  /// Add multiple QVTKWidgets at once to observe mouse events on.  You can
+  /// call this function multiple times to observe multiple QVTKWidgets.
+  /// \sa observe
+  void observe(QList<QVTKWidget *> widgets);
+
+  /// Remove a QVTKWidget to observe mouse events on.  You can call this
+  /// function multiple times to remove multiple QVTKWidgets.
+  /// \sa remove
+  void remove(QVTKWidget * widget);
+
+  /// Remove multiple QVTKWidgets at once to observe mouse events on.  You can
+  /// call this function multiple times to remove multiple QVTKWidgets.
+  /// \sa unobserve
+  void remove(QList<QVTKWidget *> widgets);
+
+  /// Returns whether a QVTKWidget is observed
+  bool isObserved(QVTKWidget * widget) const;
+
+  /// Returns the number of observed QVTKWidgets
+  int numberObserved()const;
+
+protected:
+  QScopedPointer<ctkVTKMagnifyWidgetPrivate> d_ptr;
+
+  /// Handles mouse events on the observed QVTKWidgets (specifically,
+  /// enterEvent, leaveEvent and mouseMoveEvent).
+  virtual bool eventFilter(QObject *obj, QEvent *event);
+
+signals:
+  void enteredObservedWidget(QVTKWidget * widget);
+  void leftObservedWidget(QVTKWidget * widget);
+
+private:
+  Q_DECLARE_PRIVATE(ctkVTKMagnifyWidget);
+  Q_DISABLE_COPY(ctkVTKMagnifyWidget);
+}; 
+
+#endif

+ 3 - 0
Libs/Widgets/CMakeLists.txt

@@ -40,6 +40,8 @@ SET(KIT_SRCS
   ctkConfirmExitDialog.h
   ctkCoordinatesWidget.cpp
   ctkCoordinatesWidget.h
+  ctkCursorPixmapWidget.cpp
+  ctkCursorPixmapWidget.h
   ctkDirectoryButton.cpp
   ctkDirectoryButton.h
   ctkDoubleRangeSlider.cpp
@@ -164,6 +166,7 @@ SET(KIT_MOC_SRCS
   ctkConsole.h
   ctkConsole_p.h
   ctkCoordinatesWidget.h
+  ctkCursorPixmapWidget.h
   ctkDirectoryButton.h
   ctkDoubleRangeSlider.h
   ctkDoubleSlider.h

+ 3 - 0
Libs/Widgets/Plugins/CMakeLists.txt

@@ -27,6 +27,8 @@ SET(PLUGIN_SRCS
   ctkComboBoxPlugin.h
   ctkCoordinatesWidgetPlugin.cpp
   ctkCoordinatesWidgetPlugin.h
+  ctkCursorPixmapWidgetPlugin.cpp
+  ctkCursorPixmapWidgetPlugin.h
   ctkDirectoryButtonPlugin.cpp
   ctkDirectoryButtonPlugin.h
   ctkDoubleRangeSliderPlugin.cpp
@@ -84,6 +86,7 @@ SET(PLUGIN_MOC_SRCS
   ctkColorPickerButtonPlugin.h
   ctkComboBoxPlugin.h
   ctkCoordinatesWidgetPlugin.h
+  ctkCursorPixmapWidgetPlugin.h
   ctkDirectoryButtonPlugin.h
   ctkDoubleRangeSliderPlugin.h
   ctkDoubleSliderPlugin.h

+ 77 - 0
Libs/Widgets/Plugins/ctkCursorPixmapWidgetPlugin.cpp

@@ -0,0 +1,77 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// CTK includes
+#include "ctkCursorPixmapWidgetPlugin.h"
+#include "ctkCursorPixmapWidget.h"
+
+//-----------------------------------------------------------------------------
+ctkCursorPixmapWidgetPlugin::ctkCursorPixmapWidgetPlugin(QObject *_parent)
+        : QObject(_parent)
+{
+
+}
+
+//-----------------------------------------------------------------------------
+QWidget *ctkCursorPixmapWidgetPlugin::createWidget(QWidget *_parent)
+{
+  ctkCursorPixmapWidget* _widget = new ctkCursorPixmapWidget(_parent);
+  return _widget;
+}
+
+//-----------------------------------------------------------------------------
+QString ctkCursorPixmapWidgetPlugin::domXml() const
+{
+  return "<widget class=\"ctkCursorPixmapWidget\" \
+          name=\"CursorPixmapWidget\">\n"
+      " <property name=\"geometry\">\n"
+      "  <rect>\n"
+      "   <x>0</x>\n"
+      "   <y>0</y>\n"
+      "   <width>150</width>\n"
+      "   <height>150</height>\n"
+      "  </rect>\n"
+      " </property>\n"
+      "</widget>\n";
+}
+
+// --------------------------------------------------------------------------
+ QIcon ctkCursorPixmapWidgetPlugin::icon() const
+ {
+   return QIcon();
+ }
+
+//-----------------------------------------------------------------------------
+QString ctkCursorPixmapWidgetPlugin::includeFile() const
+{
+  return "ctkCursorPixmapWidget.h";
+}
+
+//-----------------------------------------------------------------------------
+bool ctkCursorPixmapWidgetPlugin::isContainer() const
+{
+  return false;
+}
+
+//-----------------------------------------------------------------------------
+QString ctkCursorPixmapWidgetPlugin::name() const
+{
+  return "ctkCursorPixmapWidget";
+}

+ 45 - 0
Libs/Widgets/Plugins/ctkCursorPixmapWidgetPlugin.h

@@ -0,0 +1,45 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+#ifndef __ctkCursorPixmapWidgetPlugin_h
+#define __ctkCursorPixmapWidgetPlugin_h
+
+// CTK includes
+#include "ctkWidgetsAbstractPlugin.h"
+
+class CTK_WIDGETS_PLUGINS_EXPORT ctkCursorPixmapWidgetPlugin :
+  public QObject,
+  public ctkWidgetsAbstractPlugin
+{
+  Q_OBJECT
+
+public:
+  ctkCursorPixmapWidgetPlugin(QObject *_parent = 0);
+
+  QWidget *createWidget(QWidget *_parent);
+  QString  domXml() const;
+  QIcon    icon() const;
+  QString  includeFile() const;
+  bool     isContainer() const;
+  QString  name() const;
+
+};
+
+#endif

+ 2 - 0
Libs/Widgets/Plugins/ctkWidgetsPlugins.h

@@ -33,6 +33,7 @@
 #include "ctkColorPickerButtonPlugin.h"
 #include "ctkComboBoxPlugin.h"
 #include "ctkCoordinatesWidgetPlugin.h"
+#include "ctkCursorPixmapWidgetPlugin.h"
 #include "ctkDirectoryButtonPlugin.h"
 #include "ctkDoubleRangeSliderPlugin.h"
 #include "ctkDoubleSliderPlugin.h"
@@ -73,6 +74,7 @@ public:
             << new ctkColorPickerButtonPlugin
             << new ctkComboBoxPlugin
             << new ctkCoordinatesWidgetPlugin
+            << new ctkCursorPixmapWidgetPlugin
             << new ctkDirectoryButtonPlugin
             << new ctkDoubleRangeSliderPlugin
             << new ctkDoubleSliderPlugin

+ 27 - 2
Libs/Widgets/Testing/Cpp/CMakeLists.txt

@@ -1,6 +1,6 @@
 SET(KIT ${PROJECT_NAME})
 
-CREATE_TEST_SOURCELIST(Tests ${KIT}CppTests.cxx
+SET(TEST_SOURCES
   ctkActionsWidgetTest1.cpp
   ctkAddRemoveComboBoxTest1.cpp
   ctkAxesWidgetTest1.cpp
@@ -19,6 +19,7 @@ CREATE_TEST_SOURCELIST(Tests ${KIT}CppTests.cxx
   ctkConfirmExitDialogTest1.cpp
   ctkConsoleTest1.cpp
   ctkCoordinatesWidgetTest1.cpp
+  ctkCursorPixmapWidgetTest1.cpp
   ctkDirectoryButtonTest1.cpp
   ctkDoubleRangeSliderTest1.cpp
   ctkDoubleRangeSliderTest2.cpp
@@ -55,6 +56,19 @@ CREATE_TEST_SOURCELIST(Tests ${KIT}CppTests.cxx
   ctkExampleUseOfWorkflowWidgetUsingSignalsAndSlots.cpp
   )
 
+#
+# Tests expecting CTKData to be set
+#
+IF(EXISTS "${CTKData_DIR}")
+  LIST(APPEND TEST_SOURCES
+    ctkCursorPixmapWidgetTest2.cpp
+    )
+ENDIF()
+
+CREATE_TEST_SOURCELIST(Tests ${KIT}CppTests.cxx
+  ${TEST_SOURCES}
+  )
+
 SET (TestsToRun ${Tests})
 REMOVE (TestsToRun ${KIT}CppTests.cxx)
 
@@ -81,7 +95,10 @@ TARGET_LINK_LIBRARIES(${KIT}CppTests ${LIBRARY_NAME})
 SET( KIT_TESTS ${CPP_TEST_PATH}/${KIT}CppTests)
 
 MACRO( SIMPLE_TEST  TESTNAME )
-  ADD_TEST( ${TESTNAME} ${KIT_TESTS} ${TESTNAME} )
+  ADD_TEST( ${TESTNAME} ${KIT_TESTS} ${TESTNAME}
+            -D "${CTKData_DIR}/Data"
+            -V "${CTKData_DIR}/Baseline/Libs/Widgets"
+            )
   SET_PROPERTY(TEST ${TESTNAME} PROPERTY LABELS ${PROJECT_NAME})
 ENDMACRO( SIMPLE_TEST  )
 
@@ -107,6 +124,7 @@ SIMPLE_TEST( ctkComboBoxTest1 )
 SIMPLE_TEST( ctkConfirmExitDialogTest1 )
 SIMPLE_TEST( ctkConsoleTest1 )
 SIMPLE_TEST( ctkCoordinatesWidgetTest1 )
+SIMPLE_TEST( ctkCursorPixmapWidgetTest1 )
 SIMPLE_TEST( ctkDateRangeWidgetTest1 )
 SIMPLE_TEST( ctkDirectoryButtonTest1 )
 SIMPLE_TEST( ctkDoubleRangeSliderTest1 )
@@ -141,3 +159,10 @@ SIMPLE_TEST( ctkToolTipTrapperTest1 )
 SIMPLE_TEST( ctkTreeComboBoxTest1 )
 SIMPLE_TEST( ctkWorkflowWidgetTest1 )
 SIMPLE_TEST( ctkWorkflowWidgetTest2 )
+
+#
+# Add Tests expecting CTKData to be set
+#
+IF(EXISTS "${CTKData_DIR}")
+  SIMPLE_TEST( ctkCursorPixmapWidgetTest2 )
+ENDIF()

+ 116 - 0
Libs/Widgets/Testing/Cpp/ctkCursorPixmapWidgetTest1.cpp

@@ -0,0 +1,116 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QApplication>
+
+// CTK includes
+#include "ctkCursorPixmapWidget.h"
+
+// STD includes
+#include <cstdlib>
+#include <iostream>
+
+//-----------------------------------------------------------------------------
+int ctkCursorPixmapWidgetTest1(int argc, char * argv [] )
+{
+  QApplication app(argc, argv);
+
+  ctkCursorPixmapWidget cursor;
+
+  // check default values
+  if (!cursor.showCursor() ||
+      cursor.cursorColor() != cursor.palette().color(QPalette::Highlight) ||
+      cursor.cursorType() != ctkCursorPixmapWidget::CrossHairCursor ||
+      cursor.marginColor() != cursor.palette().color(QPalette::Window) ||
+      cursor.bullsEyeWidth() != 15)
+    {
+    std::cerr << "ctkCursorPixmapWidget: Wrong default values. " << std::endl
+              << " " << cursor.showCursor()
+              << " " << qPrintable(cursor.cursorColor().name())
+              << " " << cursor.cursorType()
+              << " " << qPrintable(cursor.marginColor().name())
+              << " " << cursor.bullsEyeWidth() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Show cursor
+  cursor.setShowCursor(false);
+  if (cursor.showCursor())
+    {
+    std::cerr << "ctkCursorPixmapWidget:setShowCursor failed. "
+              << cursor.showCursor() << std::endl;
+    return EXIT_FAILURE;
+    }
+  cursor.setShowCursor(true);
+
+  // Cursor color
+  QColor transparentBlue(Qt::blue);
+  transparentBlue.setAlphaF(0.25);
+  cursor.setCursorColor(transparentBlue);
+  if (cursor.cursorColor() != transparentBlue)
+    {
+    std::cerr << "ctkCursorPixmapWidget:setCursorColor failed. "
+              << qPrintable(cursor.cursorColor().name()) << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Cursor type
+  cursor.setCursorType(ctkCursorPixmapWidget::BullsEyeCursor);
+  if (cursor.cursorType() != ctkCursorPixmapWidget::BullsEyeCursor)
+    {
+    std::cerr << "ctkCursorPixmapWidget:setCursorType failed. "
+              << cursor.cursorType() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Margin color - invalid input
+  QColor origColor = cursor.marginColor();
+  cursor.setMarginColor(QColor());
+  if (cursor.marginColor() != origColor)
+    {
+    std::cerr << "ctkCursorPixmapWidget:setMarginColor failed - invalid input. "
+              << qPrintable(cursor.marginColor().name()) << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Margin color - valid input
+  cursor.setMarginColor(transparentBlue);
+  if (cursor.marginColor() != transparentBlue)
+    {
+      {
+      std::cerr << "ctkCursorPixmapWidget:setMarginColor failed - valid input. "
+                << qPrintable(cursor.marginColor().name()) << std::endl;
+      return EXIT_FAILURE;
+      }
+    }
+
+  // Bulls eye width
+  cursor.setBullsEyeWidth(0);
+  if (cursor.bullsEyeWidth() != 0)
+    {
+    std::cerr << "ctkCursorPixmapWidget:setBullsEyeWidth failed. "
+              << cursor.bullsEyeWidth() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  return EXIT_SUCCESS;
+}
+

+ 183 - 0
Libs/Widgets/Testing/Cpp/ctkCursorPixmapWidgetTest2.cpp

@@ -0,0 +1,183 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QApplication>
+#include <QIcon>
+#include <QSignalSpy>
+#include <QStyle>
+#include <QTimer>
+
+// CTK includes
+#include "ctkCursorPixmapWidget.h"
+#include "ctkCommandLineParser.h"
+
+// STD includes
+#include <cstdlib>
+#include <iostream>
+
+//-----------------------------------------------------------------------------
+bool imageCompare(ctkCursorPixmapWidget& cursor, QString baselineDirectory,
+                  QString baselineFilename)
+{
+  QImage output = QPixmap::grabWidget(&cursor).toImage();
+  QImage baseline(baselineDirectory + "/" + baselineFilename);
+  return output == baseline;
+}
+
+//-----------------------------------------------------------------------------
+// (Used to create baselines, not during testing).
+void imageSave(ctkCursorPixmapWidget& cursor, QString baselineDirectory,
+               QString baselineFilename)
+{
+  QImage output = QPixmap::grabWidget(&cursor).toImage();
+  output.save(baselineDirectory + "/" + baselineFilename);
+}
+
+//-----------------------------------------------------------------------------
+bool runBaselineTest(int time, QApplication& app, ctkCursorPixmapWidget& cursor,
+                     QString baselineDirectory, QString baselineFilename,
+                     QString errorMessage)
+{
+  QTimer::singleShot(time, &app, SLOT(quit()));
+  if (app.exec() == EXIT_FAILURE)
+    {
+    std::cerr << "ctkCursorPixmapWidget exec failed "
+        << qPrintable(errorMessage) << std::endl;
+    return false;
+    }
+  if (!imageCompare(cursor, baselineDirectory, baselineFilename))
+    {
+    std::cerr << "ctkCursorPixmapWidget baseline comparison failed when "
+              << qPrintable(errorMessage) << "." << std::endl;
+    return false;
+    }
+  return true;
+}
+
+//-----------------------------------------------------------------------------
+int ctkCursorPixmapWidgetTest2(int argc, char * argv [] )
+{
+  QApplication app(argc, argv);
+
+  // Command line parser
+  ctkCommandLineParser parser;
+  parser.addArgument("", "-D", QVariant::String);
+  parser.addArgument("", "-V", QVariant::String);
+  parser.addArgument("", "-I", QVariant::String);
+  bool ok = false;
+  QHash<QString, QVariant> parsedArgs = parser.parseArguments(app.arguments(), &ok);
+  if (!ok)
+    {
+    std::cerr << qPrintable(parser.errorString()) << std::endl;
+    return EXIT_FAILURE;
+    }
+  QString dataDirectory = parsedArgs["-D"].toString();
+  QString baselineDirectory = parsedArgs["-V"].toString();
+  bool interactive = parsedArgs["-I"].toBool();
+
+  // The remainder is interactive, so abort now if command line args specify otherwise
+  if (interactive)
+    {
+    return EXIT_SUCCESS;
+    }
+
+  // Create the cursor widget
+  ctkCursorPixmapWidget cursor;
+  cursor.setCursorColor(Qt::yellow);
+  cursor.setMarginColor(Qt::blue);
+
+  int time = 200;
+  QPixmap pixmap(dataDirectory + "/" + "computerIcon.png");
+  // Basesize is always odd
+  QSize baseSize = pixmap.size();
+  if (pixmap.width() % 2 == 0)
+    {
+    baseSize.setWidth(baseSize.width()+1);
+    }
+  if (pixmap.height() % 2 == 0)
+    {
+    baseSize.setHeight(baseSize.height()+1);
+    }
+  cursor.setMinimumSize(baseSize);
+  cursor.setPixmap(pixmap.scaled(baseSize));
+  cursor.show();
+
+  // Bullseye cursor
+  cursor.setCursorType(ctkCursorPixmapWidget::BullsEyeCursor);
+  cursor.setBullsEyeWidth(15);
+  if (!runBaselineTest(time, app, cursor, baselineDirectory,
+                       "ctkCursorPixmapWidgetTest2a.png",
+                       "using bulls-eye cursor (odd size, odd bullsEye)"))
+    {
+    return EXIT_FAILURE;
+    }
+  cursor.setBullsEyeWidth(14);
+  if (!runBaselineTest(time, app, cursor, baselineDirectory,
+                       "ctkCursorPixmapWidgetTest2b.png",
+                       "using bulls-eye cursor (odd size, even bullsEye)"))
+    {
+    return EXIT_FAILURE;
+    }
+  cursor.resize(baseSize.width()+1, baseSize.height()+1);
+  if (!runBaselineTest(time, app, cursor, baselineDirectory,
+                       "ctkCursorPixmapWidgetTest2c.png",
+                       "using bulls-eye cursor (even size, even bullsEye)"))
+    {
+    return EXIT_FAILURE;
+    }
+  cursor.setBullsEyeWidth(15);
+  if (!runBaselineTest(time, app, cursor, baselineDirectory,
+                       "ctkCursorPixmapWidgetTest2d.png",
+                       "using bulls-eye cursor (even size, odd bullsEye)"))
+    {
+    return EXIT_FAILURE;
+    }
+  cursor.resize(baseSize);
+
+  // Cursor not shown
+  cursor.setShowCursor(false);
+  if (!runBaselineTest(time, app, cursor, baselineDirectory,
+                       "ctkCursorPixmapWidgetTest2e.png",
+                       "show cursor false"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Crosshair cursor
+  cursor.setShowCursor(true);
+  cursor.setCursorType(ctkCursorPixmapWidget::CrossHairCursor);
+  if (!runBaselineTest(time, app, cursor, baselineDirectory,
+                       "ctkCursorPixmapWidgetTest2f.png",
+                       "using cross-hair cursor (odd size)"))
+    {
+    return EXIT_FAILURE;
+    }
+  cursor.resize(baseSize.width()+1, baseSize.height()+1);
+  if (!runBaselineTest(time, app, cursor, baselineDirectory,
+                       "ctkCursorPixmapWidgetTest2g.png",
+                       "using cross-hair cursor (even size)"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  return EXIT_SUCCESS;
+}
+

+ 275 - 0
Libs/Widgets/ctkCursorPixmapWidget.cpp

@@ -0,0 +1,275 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QColor>
+#include <QDebug>
+#include <QEvent>
+#include <QPainter>
+#include <QSize>
+
+// CTK includes
+#include "ctkCursorPixmapWidget.h"
+#include "ctkLogger.h"
+
+// STD includes
+#include <math.h>
+
+//--------------------------------------------------------------------------
+static ctkLogger logger("org.commontk.visualization.vtk.widgets.ctkCursorPixmapWidget");
+//--------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+class ctkCursorPixmapWidgetPrivate
+{
+  Q_DECLARE_PUBLIC(ctkCursorPixmapWidget);
+protected:
+  ctkCursorPixmapWidget* const q_ptr;
+public:
+  ctkCursorPixmapWidgetPrivate(ctkCursorPixmapWidget& object);
+
+  void init();
+  void drawCursor();
+  void drawCrossHairCursor(QPainter& painter);
+  void drawBullsEyeCursor(QPainter& painter);
+
+  bool      ShowCursor;
+  ctkCursorPixmapWidget::CursorTypes CursorType;
+  int       BullsEyeWidth;
+
+  static const double BULLS_EYE_BLANK_FRACTION = 0.1;
+};
+
+// --------------------------------------------------------------------------
+// ctkCursorPixmapWidgetPrivate methods
+
+// --------------------------------------------------------------------------
+ctkCursorPixmapWidgetPrivate::ctkCursorPixmapWidgetPrivate(ctkCursorPixmapWidget& object)
+  :q_ptr(&object)
+{
+  this->ShowCursor = true;
+  this->CursorType = ctkCursorPixmapWidget::CrossHairCursor;
+  this->BullsEyeWidth = 15;
+}
+
+//---------------------------------------------------------------------------
+void ctkCursorPixmapWidgetPrivate::init()
+{
+  Q_Q(ctkCursorPixmapWidget);
+  q->setAutoFillBackground(true);
+  q->setAlignment(Qt::AlignCenter);
+}
+
+//---------------------------------------------------------------------------
+void ctkCursorPixmapWidgetPrivate::drawCursor()
+{
+  // Abort if we are not to draw the cursor
+  if (!this->ShowCursor)
+    {
+    return;
+    }
+
+  // Setup the painter object to paint on the label
+  Q_Q(ctkCursorPixmapWidget);
+  QPainter painter(q);
+  painter.setPen(q->cursorColor());
+
+  // Draw cursor (based on current parameters) onto the label
+  switch (this->CursorType)
+    {
+    case ctkCursorPixmapWidget::CrossHairCursor:
+      this->drawCrossHairCursor(painter);
+      break;
+    case ctkCursorPixmapWidget::BullsEyeCursor:
+      this->drawBullsEyeCursor(painter);
+      break;
+    default:
+      qDebug() << "Unsupported cursor type" << this->CursorType;
+      break;
+    }
+}
+
+//---------------------------------------------------------------------------
+void ctkCursorPixmapWidgetPrivate::drawCrossHairCursor(QPainter& painter)
+{
+  Q_Q(ctkCursorPixmapWidget);
+  QSize size = q->size();
+  double halfWidth = (size.width()-1.0) / 2.0;
+  double halfHeight = (size.height()-1.0) / 2.0;
+  painter.drawLine(QPointF(0, halfHeight), QPointF(size.width(), halfHeight));
+  painter.drawLine(QPointF(halfWidth, 0), QPointF(halfWidth, size.height()));
+}
+
+// --------------------------------------------------------------------------
+void ctkCursorPixmapWidgetPrivate::drawBullsEyeCursor(QPainter& painter)
+{
+  Q_Q(ctkCursorPixmapWidget);
+  QSize size = q->size();
+
+  // Draw rectangle
+  double bullsEye = this->BullsEyeWidth;
+  double x = (size.width()-bullsEye) / 2.0;
+  double y = (size.height()-bullsEye) / 2.0;
+  painter.drawRect(QRectF(x, y, bullsEye-1.0, bullsEye-1.0));
+
+  // Draw the lines
+  double halfWidth = (size.width()-1.0) / 2.0;
+  double halfHeight = (size.height()-1.0) / 2.0;
+  double blank = round(std::min(halfWidth, halfHeight) * this->BULLS_EYE_BLANK_FRACTION);
+
+  painter.drawLine(QPointF(0, halfHeight), QPointF(x-blank-1.0, halfHeight));
+  painter.drawLine(QPointF(x+bullsEye+blank, halfHeight), QPointF(size.width(), halfHeight));
+  painter.drawLine(QPointF(halfWidth, 0), QPointF(halfWidth, y-blank-1.0));
+  painter.drawLine(QPointF(halfWidth, y+bullsEye+blank), QPointF(halfWidth, size.height()));
+}
+
+//---------------------------------------------------------------------------
+// ctkCursorPixmapWidget methods
+
+// --------------------------------------------------------------------------
+ctkCursorPixmapWidget::ctkCursorPixmapWidget(QWidget* parent)
+  : Superclass(parent)
+  , d_ptr(new ctkCursorPixmapWidgetPrivate(*this))
+{
+  Q_D(ctkCursorPixmapWidget);
+  d->init();
+}
+
+// --------------------------------------------------------------------------
+ctkCursorPixmapWidget::~ctkCursorPixmapWidget()
+{
+}
+
+// --------------------------------------------------------------------------
+CTK_GET_CPP(ctkCursorPixmapWidget, bool, showCursor, ShowCursor);
+
+// --------------------------------------------------------------------------
+void ctkCursorPixmapWidget::setShowCursor(bool newShow)
+{
+  Q_D(ctkCursorPixmapWidget);
+  if (newShow == d->ShowCursor)
+    {
+    return;
+    }
+
+  d->ShowCursor = newShow;
+  this->update();
+}
+
+// --------------------------------------------------------------------------
+QColor ctkCursorPixmapWidget::cursorColor() const
+  {
+  return this->palette().color(QPalette::Highlight);
+  }
+
+// --------------------------------------------------------------------------
+void ctkCursorPixmapWidget::setCursorColor(const QColor& newColor)
+{
+  if (newColor.isValid())
+    {
+    QPalette palette(this->palette());
+    palette.setColor(QPalette::Highlight, newColor);
+    this->setPalette(palette);
+    this->update();
+    }
+}
+
+// --------------------------------------------------------------------------
+CTK_GET_CPP(ctkCursorPixmapWidget, const ctkCursorPixmapWidget::CursorTypes&, cursorType, CursorType);
+
+// --------------------------------------------------------------------------
+void ctkCursorPixmapWidget::setCursorType(const CursorTypes& newType)
+{
+  Q_D(ctkCursorPixmapWidget);
+  if (newType == d->CursorType)
+    {
+    return;
+    }
+
+  d->CursorType = newType;
+  this->update();
+}
+
+// --------------------------------------------------------------------------
+QColor ctkCursorPixmapWidget::marginColor() const
+  {
+  return this->palette().color(QPalette::Window);
+  }
+
+// --------------------------------------------------------------------------
+void ctkCursorPixmapWidget::setMarginColor(const QColor& newColor)
+{
+  if (newColor.isValid())
+    {
+    QPalette palette(this->palette());
+    palette.setColor(QPalette::Window, newColor);
+    this->setPalette(palette);
+    this->update();
+    }
+}
+
+// --------------------------------------------------------------------------
+CTK_GET_CPP(ctkCursorPixmapWidget, int, bullsEyeWidth, BullsEyeWidth);
+
+// --------------------------------------------------------------------------
+void ctkCursorPixmapWidget::setBullsEyeWidth(int newWidth)
+{
+  if (newWidth >= 0)
+    {
+    Q_D(ctkCursorPixmapWidget);
+    d->BullsEyeWidth = newWidth;
+    this->update();
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkCursorPixmapWidget::paintEvent(QPaintEvent * event)
+{
+  Superclass::paintEvent(event);
+  Q_D(ctkCursorPixmapWidget);
+  d->drawCursor();
+}
+
+// --------------------------------------------------------------------------
+QSize ctkCursorPixmapWidget::minimumSizeHint()const
+{
+  // Pretty arbitrary size (matches ctkVTKAbstractView)
+  return QSize(50, 50);
+}
+
+// --------------------------------------------------------------------------
+QSize ctkCursorPixmapWidget::sizeHint()const
+{
+  // Pretty arbitrary size (matches ctkVTKAbstractView)
+  return QSize(300, 300);
+}
+
+//----------------------------------------------------------------------------
+bool ctkCursorPixmapWidget::hasHeightForWidth()const
+{
+  return true;
+}
+
+//----------------------------------------------------------------------------
+int ctkCursorPixmapWidget::heightForWidth(int width)const
+{
+  // Tends to be square
+  return width;
+}

+ 100 - 0
Libs/Widgets/ctkCursorPixmapWidget.h

@@ -0,0 +1,100 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+#ifndef __ctkCursorPixmapWidget_h
+#define __ctkCursorPixmapWidget_h
+
+// QT includes
+#include <QLabel>
+class QColor;
+class QSize;
+
+// CTK includes
+#include "ctkWidgetsExport.h"
+class ctkCursorPixmapWidgetPrivate;
+
+// Draws a cursor onto a pixmap
+
+class CTK_WIDGETS_EXPORT ctkCursorPixmapWidget : public QLabel
+{
+  Q_OBJECT
+  Q_FLAGS(CursorType CursorTypes)
+  Q_PROPERTY(bool showCursor READ showCursor WRITE setShowCursor)
+  Q_PROPERTY(QColor cursorColor READ cursorColor WRITE setCursorColor)
+  Q_PROPERTY(CursorTypes cursorType READ cursorType WRITE setCursorType)
+  Q_PROPERTY(QColor marginColor READ marginColor WRITE setMarginColor);
+  Q_PROPERTY(int bullsEyeWidth READ bullsEyeWidth WRITE setBullsEyeWidth)
+
+public:
+  /// Constructors
+  typedef QLabel Superclass;
+  explicit ctkCursorPixmapWidget(QWidget* parent = 0);
+  virtual ~ctkCursorPixmapWidget();
+
+  /// Enumeration over types of cursors
+  enum CursorType {
+    CrossHairCursor = 0,
+    BullsEyeCursor
+  };
+  Q_DECLARE_FLAGS(CursorTypes, CursorType)
+
+  /// Set/get whether or not to draw the cursor (default true)
+  bool showCursor() const;
+  void setShowCursor(bool newShow);
+
+  /// Set/get the color of the cursor.  Default is the color from the widget's
+  /// palette Highlight role.  Note that the Highlight role is overridden if
+  /// you set this property.
+  QColor cursorColor() const;
+  void setCursorColor(const QColor& newColor);
+
+  /// Set/get the cursor type (default CursorType_CrossHair)
+  const CursorTypes& cursorType() const;
+  void setCursorType(const CursorTypes& newType);
+
+  /// Set/get color to set the widget to when not magnifying or when label
+  /// size is larger than pixmap size.  Default is the color from the widget's
+  /// palette Window role.  Note that the Window role is overridden if you
+  /// set this property.
+  QColor marginColor() const;
+  void setMarginColor(const QColor& newColor);
+
+  /// Set/get the width of the crosshair when in BullsEye mode (default 15)
+  int bullsEyeWidth() const;
+  void setBullsEyeWidth(int newWidth);
+
+  /// Size hints
+  virtual QSize minimumSizeHint()const;
+  virtual QSize sizeHint()const;
+  virtual bool hasHeightForWidth()const;
+  virtual int heightForWidth(int width)const;
+
+protected:
+  QScopedPointer<ctkCursorPixmapWidgetPrivate> d_ptr;
+
+  // Draws the cursor on top of the widget.
+  virtual void paintEvent(QPaintEvent * event);
+
+private:
+  Q_DECLARE_PRIVATE(ctkCursorPixmapWidget);
+  Q_DISABLE_COPY(ctkCursorPixmapWidget);
+}; 
+
+#endif