Commit 67afeef4 authored by Emmanuel Promayon's avatar Emmanuel Promayon

NEW viewer extension + examples

- NEW Imp show all central viewer
    The show viewer actions are not needed anymore
- NEW introducing the dual panel viewer
    Very simple viewer that relies on MedicalImageViewer to
    do the heavy work (for instance the synchronization between the viewers
    is already taken into account by what the MedicalImageViewer set in place).
    Shows how to simply re-use the default axial and 3D viewer
    in a dual panel viewer...
- NEW Visibility menu to set the visibility of a component in a viewer
- FIXED simplified Viewer API
  No need for the numberOfViewedComponent method
- FIXED BitmapViewer and InteractiveSliceViewer can only see SingleImageComponent
- FIXED image component management of visibility in 3D viewer
    and some override warnings
    Thanks to Maxime that prepared most of then visibility menu code
- FIXED add viwer count test + fix numbers of actions and extensions
- FIXED broken tests
    Note that the sleepingwhileworking automatic test are
    now disabled until it is possible to specify a dependency
    of the action with one ore more viewer extensions
- FIXED bug: do not record null viewer is the component viewer's list
    When testcomponents is ran, no viewer extensions are loaded.
    Application::getViewer(..) returns nullptr. As MeshComponent set its
    visibility in the 3DViewer, and there is no 3DViewer, nullptr was registered
    in the list. This caused a crash later on, when refresh() was called.
- FIXED arbitrary viewer in fancy
- FIXED embed the arbitrary viewer in the medical image viewer
parent b3b85c53
......@@ -3,7 +3,7 @@
# CamiTK Community Edition CEP Set
#
#--------------------------------------------
cmake_minimum_required(VERSION 3.0)
cmake_minimum_required(VERSION 3.0)
project(camitkcommunityedition)
......
......@@ -34,6 +34,7 @@
// VTK includes
#include <vtkStringArray.h>
#include <vtkPointData.h>
#include <vtkImageChangeInformation.h>
// GDCM includes
#include <vtkGDCMImageReader.h>
......
/*****************************************************************************
/*****************************************************************************
* $CAMITK_LICENCE_BEGIN$
*
* CamiTK - Computer Assisted Medical Intervention ToolKit
......@@ -59,6 +59,10 @@ DicomDialog::DicomDialog(QList<DicomDialogEntry*> elements): QDialog() {
ui.tableWidget->setItem(row, column++, new QTableWidgetItem(item->getSeriesName()));
row++;
}
// To get checkbox checked if there is only one study in list
if (elements.size() == 1) {
ui.tableWidget->itemAt(0, 0)->setCheckState(Qt::Checked);
}
ui.tableWidget->resizeColumnsToContents();
......
......@@ -85,7 +85,6 @@ MonitoringDialog::MonitoringDialog(MonitoringGuiManager* guiManager, QWidget* pa
MonitoringDialog::~MonitoringDialog() {
// unselect and clear the monitor tab's content.
// this allow not to encounter crash when closing the image
// see bug 116 : https://forge.imag.fr/tracker/index.php?func=detail&aid=116&group_id=184&atid=792
this->ui->monitorsTable->clearContents();
QWidget* simulatorWidget = ui->monitoringTabs->widget(3);
if (simulatorWidget != nullptr) {
......
......@@ -75,7 +75,6 @@ void ApplicationActionExtension::init() {
registerNewAction(ShowSagittalViewer);
registerNewAction(Show3DViewer);
registerNewAction(ShowAllViewers);
// disabled as long as this viewer is not working
// registerNewAction(ShowArbitraryViewer);
registerNewAction(ShowArbitraryViewer);
registerNewAction(LoggerParameters);
}
......@@ -57,7 +57,16 @@ QWidget* QuitAction::getWidget() {
// --------------- apply -------------------
Action::ApplyStatus QuitAction::apply() {
Application::getMainWindow()->close();
return SUCCESS;
// close all components (and therefore ask the user to savet the modified ones)
ApplyStatus closeAllStatus = Application::getAction("Close All")->apply();
if (closeAllStatus == SUCCESS) {
// Quit the app (will call Application::quitting() and therefore unload all action extensions and delete all actions)
Application::quit();
return SUCCESS;
}
else {
// or abort the operation
return ABORTED;
}
}
......@@ -25,14 +25,8 @@
#include "SaveAllAction.h"
#include <Application.h>
#include <ImageComponent.h>
#include <ImageComponentExtension.h>
#include <MeshComponent.h>
#include <MeshComponentExtension.h>
#include <Log.h>
#include <QFileDialog>
using namespace camitk;
......
/*****************************************************************************
* $CAMITK_LICENCE_BEGIN$
*
* CamiTK - Computer Assisted Medical Intervention ToolKit
* (c) 2001-2018 Univ. Grenoble Alpes, CNRS, TIMC-IMAG UMR 5525 (GMCAO)
*
* Visit http://camitk.imag.fr for more information
*
* This file is part of CamiTK.
*
* CamiTK is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License version 3
* only, as published by the Free Software Foundation.
*
* CamiTK 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 Lesser General Public License version 3 for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* version 3 along with CamiTK. If not, see <http://www.gnu.org/licenses/>.
*
* $CAMITK_LICENCE_END$
****************************************************************************/
#include "AnglesAndTranslationAction.h"
// includes from CamiTK
#include <Application.h>
#include <Property.h>
#include <MedicalImageViewer.h>
#include <InteractiveViewer.h>
#include <ImageComponent.h>
#include <Log.h>
// to get the enum as a string
#include <QMetaEnum>
#include "AnglesAndTranslationWidget.h"
using namespace camitk;
// --------------- Constructor -------------------
AnglesAndTranslationAction::AnglesAndTranslationAction(ActionExtension* extension) : Action(extension) {
// Setting name, description and input component
setName("Modify Angles And Translation");
setDescription(tr("This action allows user to manually modify the orientation and translation of the arbitrary slice."));
setComponent("ImageComponent");
// Setting classification family and tags
setFamily("Arbitrary Slice");
addTag(tr("arbitrary slice"));
addTag(tr("arbitrary"));
addTag(tr("angle"));
setEmbedded(true);
currentImageComp = nullptr;
blockEvent = true;
// the AnglesAndTranslationWidget is going to modify these parameters using the GUI
// but defining them as the action parameters also allows to set them programmatically
// (for instance using another GUI or in a pipeline)
// Setting parameters default values by using properties
addParameter(new Property(tr("Translation"), 0.0, tr("Current translation inside the image"), ""));
Property* actionProperty = actionProperty = new Property(tr("X Angle"), 0, tr("X Angle"), "degree");
actionProperty->setAttribute("minimum", 0);
actionProperty->setAttribute("maximum", 360);
addParameter(actionProperty);
actionProperty = new Property(tr("Y Angle"), 0, tr("Y Angle"), "degree");
actionProperty->setAttribute("minimum", 0);
actionProperty->setAttribute("maximum", 360);
addParameter(actionProperty);
actionProperty = new Property(tr("Z Angle"), 0, tr("Z Angle"), "degree");
actionProperty->setAttribute("minimum", 0);
actionProperty->setAttribute("maximum", 360);
addParameter(actionProperty);
actionProperty = new Property("Visible Viewer", MedicalImageViewer::VIEWER_ARBITRARY, "Visible viewer in the main window", "");
// To set the enum type, use the same name of the enum as declared in the class
actionProperty->setEnumTypeName("LayoutVisibility");
// Add the property as an action parameter
addParameter(actionProperty);
// be notified automatically when the parameters change
setAutoUpdateProperties(true);
}
// --------------- destructor -------------------
AnglesAndTranslationAction::~AnglesAndTranslationAction() {
delete actionWidget;
}
// --------------- getWidget -------------------
QWidget* AnglesAndTranslationAction::getWidget() {
if (!actionWidget) {
actionWidget = new AnglesAndTranslationWidget(this);
}
// update the pointer
currentImageComp = dynamic_cast<ImageComponent*>(getTargets().last())->getArbitrarySlices();
// make sure the arbitrary slice is visible in 3D
currentImageComp->setViewSliceIn3D(true);
// update the properties
update();
return actionWidget;
}
// ---------------------- updateParameters ----------------------------
void AnglesAndTranslationAction::update() {
// update current action properties safely (i.e. without event(..) to be doing anything on the component)
blockEvent = true;
// current translation along the z axis of the arbitrary slice
setProperty("Translation", currentImageComp->getTranslationInVolume() * 100.0);
// update widget
dynamic_cast<AnglesAndTranslationWidget*>(actionWidget)->updateGUI();
// update central viewer
MedicalImageViewer::LayoutVisibility visibleViewer = static_cast<MedicalImageViewer::LayoutVisibility>(property("Visible Viewer").toInt());
dynamic_cast<MedicalImageViewer*>(Application::getViewer("MedicalImageViewer"))->setVisibleViewer(visibleViewer);
// keep property up-to-date when the GUI will change
blockEvent = false;
}
// ---------------------- resetTransform ----------------------------
void AnglesAndTranslationAction::resetTransform() {
currentImageComp->resetTransform();
// Reset the angle to zero
blockEvent = true;
setProperty("X Angle", 0);
setProperty("Y Angle", 0);
setProperty("Z Angle", 0);
blockEvent = false;
update();
currentImageComp->refresh();
}
// ---------------------- event ----------------------------
bool AnglesAndTranslationAction::event(QEvent* e) {
if (currentImageComp == nullptr || blockEvent)
return QObject::event(e);
if (e->type() == QEvent::DynamicPropertyChange) {
e->accept();
QDynamicPropertyChangeEvent* changeEvent = dynamic_cast<QDynamicPropertyChangeEvent*>(e);
if (!changeEvent)
return false;
// do something depending of the property that has changed
if (changeEvent->propertyName() != "Visible Viewer") {
// only translation and rotation angle are of interest here (the "Visible Viewer" changes will automatically be
// taken into account in the update() method
if (changeEvent->propertyName() == "Translation") {
currentImageComp->setTransformTranslation(0.0, 0.0, property(changeEvent->propertyName()).toDouble() / 100.0);
}
else {
currentImageComp->setTransformRotation(property("X Angle").toInt(), property("Y Angle").toInt(), property("Z Angle").toInt());
}
}
// needed as rotation might change the translation value or translation might have been rejected
update();
currentImageComp->refresh();
return true;
}
// this is important to continue the process if the event is a different one
return QObject::event(e);
}
......@@ -22,14 +22,16 @@
*
* $CAMITK_LICENCE_END$
****************************************************************************/
#ifndef SETANGLESACTION_H
#define SETANGLESACTION_H
#ifndef ANGLESANDTRANSLATIONACTION_H
#define ANGLESANDTRANSLATIONACTION_H
// CamiTK stuff
#include <QObject>
#include <Action.h>
#include <ImageComponent.h>
#include <ArbitrarySingleImageComponent.h>
#include <MedicalImageViewer.h>
#include "AnglesSetterWidget.h"
#include "ArbitrarySliceAPI.h"
/**
* @ingroup group_sdk_actions_image_arbitraryslice
......@@ -37,21 +39,27 @@
* @brief
* This action simply display the widget allowing the user to select an angle to orientate the arbitrary slice.
*/
class SetAnglesAction : public camitk::Action {
class ARBITRARY_SLICE_API AnglesAndTranslationAction : public camitk::Action {
Q_OBJECT
public:
/// Default Constructor
SetAnglesAction(camitk::ActionExtension*);
AnglesAndTranslationAction(camitk::ActionExtension*);
/// Default Destructor
virtual ~SetAnglesAction();
virtual ~AnglesAndTranslationAction();
/// Return the arbitrary slice angles setter widget
virtual QWidget* getWidget();
/// manage change in the action parameters (angles and slice number)
virtual bool event(QEvent* e);
/// reset transform to identity
void resetTransform();
public slots:
/** This method returns always SUCCESS as the action aims at displaying its widget to be used in order to control the
* arbitrary slice display.
......@@ -59,8 +67,18 @@ public slots:
virtual ApplyStatus apply() {
return SUCCESS;
}
private:
/// update action's parameter using the current image state + update widget GUI
/// + update the central viewer depending on the current value of the "Visible Viewer" property
void update();
/// currently controled image
camitk::ArbitrarySingleImageComponent* currentImageComp;
/// block property changed event (temporarily)
bool blockEvent;
};
#endif // SETANGLESACTION_H
#endif // ANGLESANDTRANSLATIONACTION_H
......@@ -23,102 +23,138 @@
* $CAMITK_LICENCE_END$
****************************************************************************/
#include "AnglesSetterWidget.h"
#include "AnglesAndTranslationWidget.h"
// CamiTK stuff
#include <InteractiveViewer.h>
#include <Application.h>
#include <MainWindow.h>
#include <ImageComponent.h>
// Qt stuff
#include <QSpinBox>
#include <Property.h>
#include <AnglesAndTranslationAction.h>
using namespace camitk;
// -------------------- constructor --------------------
AnglesSetterWidget::AnglesSetterWidget(QWidget* parent): QWidget(parent) {
AnglesAndTranslationWidget::AnglesAndTranslationWidget(AnglesAndTranslationAction* a, QWidget* parent): QWidget(parent) {
myAction = a;
ui.setupUi(this);
// Ui slice number change slot connection
connect(ui.sliceSpinBox, SIGNAL(valueChanged(int)), this, SLOT(updateSlice(int)));
connect(ui.sliceSlider, SIGNAL(valueChanged(int)), this, SLOT(updateSlice(int))) ;
connect(ui.translationSpinBox, SIGNAL(valueChanged(double)), this, SLOT(translationSpinBoxChanged(double)));
connect(ui.translationSlider, SIGNAL(valueChanged(int)), this, SLOT(translationSliderChanged(int)));
// Ui angles slot connection
connect(ui.xAngleDial, SIGNAL(valueChanged(int)), this, SLOT(xAngleDialValueChanged(int)));
connect(ui.yAngleDial, SIGNAL(valueChanged(int)), this, SLOT(yAngleDialValueChanged(int)));
connect(ui.zAngleDial, SIGNAL(valueChanged(int)), this, SLOT(zAngleDialValueChanged(int)));
// Initialize the slider
// The action calling this widget must be applyied on only one IMAGE component.
// Therefore, selectedComponents are counted to 5, the top image level component and its slices (axial, coronal, sagital and arbitrary).
// selectedComponent[1] => arbitrary slice of the top level component
ComponentList selectedComponents = Application::getSelectedComponents();
if (selectedComponents.size() > 0) {
ui.sliceSlider->setMaximum(selectedComponents[1]->getNumberOfSlices() - 1);
ui.sliceSlider->setValue(selectedComponents[1]->getSlice());
ui.sliceSpinBox->setMaximum(selectedComponents[1]->getNumberOfSlices() - 1);
ui.sliceSpinBox->setValue(selectedComponents[1]->getSlice());
connect(ui.showArbitraryRadio, SIGNAL(clicked(bool)), this, SLOT(showArbitraryViewer(bool)));
connect(ui.show3DRadio, SIGNAL(clicked(bool)), this, SLOT(show3DViewer(bool)));
connect(ui.showAllRadio, SIGNAL(clicked(bool)), this, SLOT(showAllViewer(bool)));
connect(ui.resetButton, SIGNAL(clicked()), this, SLOT(resetTransform()));
}
// -------------------- destructor --------------------
AnglesAndTranslationWidget::~AnglesAndTranslationWidget() {
}
// -------------------- updateGUI --------------------
void AnglesAndTranslationWidget::updateGUI() {
ui.translationSpinBox->blockSignals(true);
ui.translationSlider->blockSignals(true);
ui.translationSpinBox->setValue(myAction->property("Translation").toDouble());
ui.translationSlider->setValue(myAction->property("Translation").toDouble()*10);
ui.translationSpinBox->blockSignals(false);
ui.translationSlider->blockSignals(false);
ui.xAngleDial->blockSignals(true);
ui.xAngleDial->setValue(myAction->property("X Angle").toInt());
ui.xAngleDial->blockSignals(false);
updateAngleSliderLabel(ui.xAngleDial);
ui.yAngleDial->blockSignals(true);
ui.yAngleDial->setValue(myAction->property("Y Angle").toInt());
ui.yAngleDial->blockSignals(false);
updateAngleSliderLabel(ui.yAngleDial);
ui.zAngleDial->blockSignals(true);
ui.zAngleDial->setValue(myAction->property("Z Angle").toInt());
ui.zAngleDial->blockSignals(false);
updateAngleSliderLabel(ui.zAngleDial);
}
// -------------------- showArbitraryViewer --------------------
void AnglesAndTranslationWidget::showArbitraryViewer(bool buttonState) {
if (buttonState) {
myAction->setProperty("Visible Viewer", MedicalImageViewer::VIEWER_ARBITRARY);
}
else {
// TODO display error message, log ...
}
// -------------------- show3DViewer --------------------
void AnglesAndTranslationWidget::show3DViewer(bool buttonState) {
if (buttonState) {
myAction->setProperty("Visible Viewer", MedicalImageViewer::VIEWER_3D);
}
}
// -------------------- destructor --------------------
AnglesSetterWidget::~AnglesSetterWidget() {
;
// -------------------- showAllViewer --------------------
void AnglesAndTranslationWidget::showAllViewer(bool buttonState) {
if (buttonState) {
myAction->setProperty("Visible Viewer", MedicalImageViewer::VIEWER_ALL);
}
}
// -------------------- updateAngleSliderLabel --------------------
void AnglesAndTranslationWidget::resetTransform() {
myAction->resetTransform();
}
// -------------------- updateSlice --------------------
void AnglesSetterWidget::updateSlice(int sliceNumber) {
// Ui slider and spin box are mapped and thus automatically updated when a change occurs.
InteractiveViewer::getArbitraryViewer()->sliderChanged(sliceNumber);
InteractiveViewer::getArbitraryViewer()->refresh();
InteractiveViewer::get3DViewer()->refresh();
// -------------------- translationSpinBoxChanged --------------------
void AnglesAndTranslationWidget::translationSpinBoxChanged(double value) {
myAction->setProperty("Translation", value);
}
// -------------------- translationSliderChanged --------------------
void AnglesAndTranslationWidget::translationSliderChanged(int value) {
// the slide is from 0 to 1000
myAction->setProperty("Translation", value/10);
}
// -------------------- xAngleDialValueChanged --------------------
void AnglesSetterWidget::xAngleDialValueChanged(int value) {
InteractiveViewer::getArbitraryViewer()->xAngleChanged(value);
updateAngleSlider(ui.xAngleDial, ui.xAngleValue);
InteractiveViewer::getArbitraryViewer()->refresh();
InteractiveViewer::get3DViewer()->refresh();
void AnglesAndTranslationWidget::xAngleDialValueChanged(int value) {
myAction->setProperty("X Angle", value);
updateAngleSliderLabel(ui.xAngleDial);
}
// -------------------- yAngleDialValueChanged --------------------
void AnglesSetterWidget::yAngleDialValueChanged(int value) {
InteractiveViewer::getArbitraryViewer()->yAngleChanged(value);
updateAngleSlider(ui.yAngleDial, ui.yAngleValue);
InteractiveViewer::getArbitraryViewer()->refresh();
InteractiveViewer::get3DViewer()->refresh();
void AnglesAndTranslationWidget::yAngleDialValueChanged(int value) {
myAction->setProperty("Y Angle", value);
updateAngleSliderLabel(ui.yAngleDial);
}
// -------------------- zAngleDialValueChanged --------------------
void AnglesSetterWidget::zAngleDialValueChanged(int value) {
InteractiveViewer::getArbitraryViewer()->zAngleChanged(value);
updateAngleSlider(ui.zAngleDial, ui.zAngleValue);
InteractiveViewer::getArbitraryViewer()->refresh();
InteractiveViewer::get3DViewer()->refresh();
void AnglesAndTranslationWidget::zAngleDialValueChanged(int value) {
myAction->setProperty("Z Angle", value);
updateAngleSliderLabel(ui.zAngleDial);
}
// -------------------- updateAngleSlider --------------------
void AnglesSetterWidget::updateAngleSlider(QDial* dial, QLabel* label) {
dial->blockSignals(true);
dial->setMinimum(0);
dial->setMaximum(360);
dial->blockSignals(false);
// -------------------- updateAngleSliderLabel --------------------
void AnglesAndTranslationWidget::updateAngleSliderLabel(QDial* dial) {
QString angleLetter;
QLabel *label;
if (label == ui.xAngleValue) {
label->setText("Angle X : <tt>" + QString("%1").arg(dial->value(), 3) + "</tt>" + QChar(0x00B0));
if (dial == ui.xAngleDial) {
angleLetter = "X";
label = ui.xAngleValue;
}
else if (label == ui.yAngleValue) {
label->setText("Angle Y : <tt>" + QString("%1").arg(dial->value(), 3) + "</tt>" + QChar(0x00B0));
else if (dial == ui.yAngleDial) {
angleLetter = "Y";
label = ui.yAngleValue;
}
else if (label == ui.zAngleValue) {
label->setText("Angle Z : <tt>" + QString("%1").arg(dial->value(), 3) + "</tt>" + QChar(0x00B0));
else {
// dial == ui.zAngleDial
angleLetter = "Z";
label = ui.zAngleValue;
}
label->setText(angleLetter + QString(" Angle: <tt>%1</tt>").arg(dial->value(), 3) + QChar(0x00B0));
label->update();
}
......@@ -23,12 +23,13 @@
* $CAMITK_LICENCE_END$
****************************************************************************/
#ifndef AnglesSetterWidget_h
#define AnglesSetterWidget_h
#ifndef ANGLESANDTRANSLATIONWIDGET_H
#define ANGLESANDTRANSLATIONWIDGET_H
#include "ui_AnglesSetterWidget.h"
#include "ui_AnglesAndTranslationWidget.h"
#include <QWidget>
class AnglesAndTranslationAction;
/**
* @ingroup group_sdk_actions_image_arbitraryslice
......@@ -42,33 +43,46 @@
*
* Use corresponding .ui file created with Qt Designer.
*/
class AnglesSetterWidget : public QWidget {
class AnglesAndTranslationWidget : public QWidget {
Q_OBJECT
public:
/// Default construtor
AnglesSetterWidget(QWidget* parent = 0);
AnglesAndTranslationWidget(AnglesAndTranslationAction *, QWidget* parent = 0);
/// Destructor
virtual ~AnglesSetterWidget();
virtual ~AnglesAndTranslationWidget();
private:
Ui::AnglesSetterWidget ui;
private:
/// update the angle dialog slider (text + value)
void updateAngleSlider(QDial* dial, QLabel* label);
/// update the UI depending on the action
void updateGUI();
private slots:
/// Update slice number to be displayed
void updateSlice(int sliceNumber);
/// Update the translation value
void translationSpinBoxChanged(double);
void translationSliderChanged(int);
/// Method that update the angle dialog slider (text + value)
void xAngleDialValueChanged(int value);
void yAngleDialValueChanged(int value);
void zAngleDialValueChanged(int value);
void xAngleDialValueChanged(int);
void yAngleDialValueChanged(int);
void zAngleDialValueChanged(int);
/// switch to a specific viewer
void showArbitraryViewer(bool);
void show3DViewer(bool);
void showAllViewer(bool);
void resetTransform();
private:
/// update the angle dialog slider label using the current dial value
void updateAngleSliderLabel(QDial*);
// the GUI class itself
Ui::AnglesAndTranslationWidget ui;
/// the action to refer to
AnglesAndTranslationAction* myAction;
};
#endif // AnglesSetterWidget_h
#endif // ANGLESANDTRANSLATIONWIDGET_H
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AnglesSetterWidget</class>
<widget class="QWidget" name="AnglesSetterWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>289</width>
<height>500</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>500</height>
</size>
</property>
<property name="maximumSize">