Explorar o código

Merge remote-tracking branch 'vovythevov/ctkworkflow-tweaks'

* vovythevov/ctkworkflow-tweaks:
  Improve look&feel of ctkWorkflowGroupBox
  Add ctkWorkflowStep* ctkWorkflow::step(QString)
  Add ctkWorkflowGroupBox::titleFormat, subTitleFormat and errorTextFormat
  Add ctkWorkflowButtonBoxWidget::[back|next|goTo]ButtonFormat
  Fix workflow next button arrow and text
  Add ctkPushButton, an advanced QPushButton
  Fix ctkWorkflowButtonBoxWidget button size policy
  Add ctkWorkflowButtonBoxWidget::HideGoToButtons
  Cleanup ctkWorkflowButtonBoxWidget
Jean-Christophe Fillion-Robin %!s(int64=11) %!d(string=hai) anos
pai
achega
17d66f5e46

+ 8 - 0
Libs/Core/Testing/Cpp/ctkWorkflowTest3.cpp

@@ -286,6 +286,14 @@ int ctkWorkflowTest3(int argc, char * argv [] )
     }
 
   workflow->stop();
+
+  int d = workflow->backwardDistanceToStep(s7);
+  if (d != 5)
+    {
+    std::cerr << "error distance between s7->s0, got"<< d << std::endl;
+    return EXIT_FAILURE;
+    }
+
   QTimer::singleShot(defaultTime, &app, SLOT(quit()));
   app.exec();
 

+ 53 - 0
Libs/Core/ctkWorkflow.cpp

@@ -23,6 +23,8 @@
 #include <QStateMachine>
 #include <QState>
 
+#include <QQueue>
+
 // CTK includes
 #include "ctkWorkflow.h"
 #include "ctkWorkflowStep.h"
@@ -772,6 +774,14 @@ bool ctkWorkflow::hasStep(const QString& id)const
 }
 
 // --------------------------------------------------------------------------
+ctkWorkflowStep* ctkWorkflow::step(const QString& id)const
+{
+  Q_D(const ctkWorkflow);
+  return d->stepFromId(id);
+}
+
+
+// --------------------------------------------------------------------------
 // Convenience method to set the QStateMachine's initialState to a
 // specific step's processing state.
 CTK_GET_CPP(ctkWorkflow, ctkWorkflowStep*, initialStep, InitialStep);
@@ -1121,3 +1131,46 @@ void ctkWorkflow::goToStepFailed()
   //   }
 
 }
+
+// --------------------------------------------------------------------------
+int ctkWorkflow::backwardDistanceToStep(ctkWorkflowStep* fromStep,
+                                        ctkWorkflowStep* origin) const
+{
+  if (!fromStep)
+    {
+    fromStep = this->currentStep();
+    }
+  if (!origin)
+    {
+    origin = this->initialStep();
+    }
+
+  if (!fromStep || !origin)
+    {
+    return -1;
+    }
+
+  QQueue< std::pair<ctkWorkflowStep*, int> > queue;
+  queue.append(std::make_pair(fromStep, 0));
+  while (! queue.isEmpty())
+    {
+    std::pair<ctkWorkflowStep*, int> p = queue.dequeue();
+    ctkWorkflowStep* step = p.first;
+    if (! step)
+      {
+      return -1;
+      }
+
+    if (step->id() == origin->id())
+      {
+      return p.second;
+      }
+
+    foreach(ctkWorkflowStep* previousStep, this->backwardSteps(step))
+      {
+      queue.append(std::make_pair(previousStep, p.second + 1));
+      }
+    }
+
+  return -1;
+}

+ 15 - 0
Libs/Core/ctkWorkflow.h

@@ -107,15 +107,22 @@ public:
 
   /// \brief Set/get the initial step.
   /// \note In not specified, the first step added will be considered as the initialStep
+  /// \sa currentStep(), step(), hasStep(), steps()
   Q_INVOKABLE ctkWorkflowStep* initialStep()const;
   Q_INVOKABLE virtual void setInitialStep(ctkWorkflowStep* step);
 
   /// Get the current step of the state machine
+  /// \sa initialStep(), step(), hasStep(), steps()
   Q_INVOKABLE ctkWorkflowStep* currentStep()const;
 
   /// Check to see if there is a step with a given id in the workflow.
+  /// \sa step(), currentStep(), steps()
   Q_INVOKABLE bool hasStep(const QString& id)const;
 
+  /// Return the step with matching \a id if any, 0 otherwise.
+  /// \sa hasStep(), currentStep(), steps()
+  Q_INVOKABLE ctkWorkflowStep* step(const QString& id)const;
+
   /// Returns whether or not we can go forward: i.e. there exists a step that directly follows the
   /// given step.
   ///
@@ -160,6 +167,14 @@ public:
   /// Returns list of steps managed by the workflow
   Q_INVOKABLE QList<ctkWorkflowStep*> steps()const;
 
+  // Returns the distance of a given to step to another step.
+  // The directionality used here is ctkWorkflow::Bidirectional or ctkWorkflow::Backward.
+  // By default, step is the current step and origin the initial step.
+  //
+  // This is different from the other method as it's not limited to the backward or forward steps
+  // but actually performs a recursive search.
+  Q_INVOKABLE int backwardDistanceToStep(ctkWorkflowStep* fromStep = 0, ctkWorkflowStep* origin = 0)const;
+
   /// Configures the behavior of goToStep(targetId).
   ///
   /// If set to true, goToStep(targetId) goes back to the origin step after

+ 4 - 0
Libs/Widgets/CMakeLists.txt

@@ -124,6 +124,9 @@ set(KIT_SRCS
   ctkPopupWidget_p.h
   ctkProxyStyle.cpp
   ctkProxyStyle.h
+  ctkPushButton.cpp
+  ctkPushButton.h
+  ctkPushButton_p.h
   ctkQImageView.cpp
   ctkQImageView.h
   ctkRangeSlider.cpp
@@ -246,6 +249,7 @@ set(KIT_MOC_SRCS
   ctkPopupWidget.h
   ctkPopupWidget_p.h
   ctkProxyStyle.h
+  ctkPushButton.h
   ctkQImageView.h
   ctkRangeSlider.h
   ctkRangeWidget.h

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

@@ -74,6 +74,8 @@ set(PLUGIN_SRCS
   ctkPathListWidgetPlugin.h
   ctkPopupWidgetPlugin.cpp
   ctkPopupWidgetPlugin.h
+  ctkPushButtonPlugin.cpp
+  ctkPushButtonPlugin.h
   ctkRangeSliderPlugin.cpp
   ctkRangeSliderPlugin.h
   ctkRangeWidgetPlugin.cpp
@@ -133,6 +135,7 @@ set(PLUGIN_MOC_SRCS
   ctkPathListButtonsWidgetPlugin.h
   ctkPathListWidgetPlugin.h
   ctkPopupWidgetPlugin.h
+  ctkPushButtonPlugin.h
   ctkRangeSliderPlugin.h
   ctkRangeWidgetPlugin.h
   ctkThumbnailLabelPlugin.h

+ 68 - 0
Libs/Widgets/Plugins/ctkPushButtonPlugin.cpp

@@ -0,0 +1,68 @@
+/*=========================================================================
+
+  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.apache.org/licenses/LICENSE-2.0.txt
+
+  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 "ctkPushButtonPlugin.h"
+#include "ctkPushButton.h"
+
+//-----------------------------------------------------------------------------
+ctkPushButtonPlugin::ctkPushButtonPlugin(QObject* pluginParent)
+  : QObject(pluginParent)
+{
+}
+
+//-----------------------------------------------------------------------------
+QWidget *ctkPushButtonPlugin::createWidget(QWidget* parentForWidget)
+{
+  ctkPushButton* newWidget =
+    new ctkPushButton(parentForWidget);
+  return newWidget;
+}
+
+//-----------------------------------------------------------------------------
+QString ctkPushButtonPlugin::domXml() const
+{
+  return "<widget class=\"ctkPushButton\" name=\"PushButton\">\n"
+         "</widget>\n";
+}
+
+// --------------------------------------------------------------------------
+QIcon ctkPushButtonPlugin::icon() const
+{
+  return QIcon(":/Icons/pushbutton.png");
+}
+
+//-----------------------------------------------------------------------------
+QString ctkPushButtonPlugin::includeFile() const
+{
+  return "ctkPushButton.h";
+}
+
+//-----------------------------------------------------------------------------
+bool ctkPushButtonPlugin::isContainer() const
+{
+  return false;
+}
+
+//-----------------------------------------------------------------------------
+QString ctkPushButtonPlugin::name() const
+{
+  return "ctkPushButton";
+}

+ 45 - 0
Libs/Widgets/Plugins/ctkPushButtonPlugin.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.apache.org/licenses/LICENSE-2.0.txt
+
+  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 __ctkPushButtonPlugin_h
+#define __ctkPushButtonPlugin_h
+
+// CTK includes
+#include "ctkWidgetsAbstractPlugin.h"
+
+class CTK_WIDGETS_PLUGINS_EXPORT ctkPushButtonPlugin
+  : public QObject
+  , public ctkWidgetsAbstractPlugin
+{
+  Q_OBJECT
+
+public:
+  ctkPushButtonPlugin(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

@@ -55,6 +55,7 @@
 #include "ctkPathListButtonsWidgetPlugin.h"
 #include "ctkPathListWidgetPlugin.h"
 #include "ctkPopupWidgetPlugin.h"
+#include "ctkPushButtonPlugin.h"
 #include "ctkRangeSliderPlugin.h"
 #include "ctkRangeWidgetPlugin.h"
 #include "ctkSearchBoxPlugin.h"
@@ -107,6 +108,7 @@ public:
             << new ctkPathListButtonsWidgetPlugin
             << new ctkPathListWidgetPlugin
             << new ctkPopupWidgetPlugin
+            << new ctkPushButtonPlugin
             << new ctkRangeSliderPlugin
             << new ctkRangeWidgetPlugin
             << new ctkSearchBoxPlugin

+ 2 - 2
Libs/Widgets/Resources/UI/ctkWorkflowGroupBox.ui

@@ -17,7 +17,7 @@
    </sizepolicy>
   </property>
   <property name="windowTitle">
-   <string>Form</string>
+   <string>Workflow</string>
   </property>
   <layout class="QVBoxLayout" name="verticalLayout">
    <property name="margin">
@@ -131,7 +131,7 @@
                 <item>
                  <layout class="QVBoxLayout" name="ClientAreaLayout">
                   <property name="margin">
-                   <number>9</number>
+                   <number>0</number>
                   </property>
                  </layout>
                 </item>

+ 1 - 0
Libs/Widgets/Testing/Cpp/ctkWorkflowWidgetTest1.cpp

@@ -31,6 +31,7 @@
 #include <QTimer>
 
 // CTK includes
+#include "ctkPushButton.h"
 #include "ctkWorkflow.h"
 #include "ctkWorkflowWidget.h"
 #include "ctkWorkflowStackedWidget.h"

+ 1 - 0
Libs/Widgets/Testing/Cpp/ctkWorkflowWidgetTest2.cpp

@@ -29,6 +29,7 @@
 #include <QDebug>
 
 // CTK includes
+#include "ctkPushButton.h"
 #include "ctkWorkflow.h"
 #include "ctkWorkflowWidget.h"
 #include "ctkWorkflowStackedWidget.h"

+ 48 - 250
Libs/Widgets/ctkCheckablePushButton.cpp

@@ -36,33 +36,29 @@
 
 // CTK includes
 #include "ctkCheckablePushButton.h"
+#include "ctkPushButton_p.h"
 
 //-----------------------------------------------------------------------------
-class ctkCheckablePushButtonPrivate
+class ctkCheckablePushButtonPrivate: public ctkPushButtonPrivate
 {
   Q_DECLARE_PUBLIC(ctkCheckablePushButton);
 protected:
   ctkCheckablePushButton* const q_ptr;
 public:
   ctkCheckablePushButtonPrivate(ctkCheckablePushButton& object);
-  void init();
-
-  QRect checkboxRect() const;
-  QSize buttonSizeHint()const;
+  virtual void init();
+  virtual QStyleOptionButton drawIcon(QPainter* p);
 
   // Tuning of the button look&feel
-  Qt::Alignment TextAlignment;
-  Qt::Alignment IndicatorAlignment;
   Qt::ItemFlags CheckBoxFlags;
   Qt::CheckState CheckState;
 };
 
 //-----------------------------------------------------------------------------
 ctkCheckablePushButtonPrivate::ctkCheckablePushButtonPrivate(ctkCheckablePushButton& object)
-  :q_ptr(&object)
+  : ctkPushButtonPrivate(object)
+  , q_ptr(&object)
 {
-  this->TextAlignment = Qt::AlignLeft | Qt::AlignVCenter;
-  this->IndicatorAlignment = Qt::AlignLeft | Qt::AlignVCenter;
   this->CheckBoxFlags = Qt::ItemIsEnabled | Qt::ItemIsUserCheckable;
   this->CheckState = Qt::Unchecked;
 }
@@ -70,110 +66,58 @@ ctkCheckablePushButtonPrivate::ctkCheckablePushButtonPrivate(ctkCheckablePushBut
 //-----------------------------------------------------------------------------
 void ctkCheckablePushButtonPrivate::init()
 {
-}
+  Q_Q(ctkCheckablePushButton);
 
-//-----------------------------------------------------------------------------
-QRect ctkCheckablePushButtonPrivate::checkboxRect()const
-{
-  Q_Q(const ctkCheckablePushButton);
-  QRect rect;
   QStyleOptionButton opt;
-  q->initStyleOption(&opt);
+  opt.initFrom(q);
 
   QSize indicatorSize = QSize(q->style()->pixelMetric(QStyle::PM_IndicatorWidth, &opt, q),
                               q->style()->pixelMetric(QStyle::PM_IndicatorHeight, &opt, q));
-  int buttonHeight = opt.rect.height();
-  uint tf = this->TextAlignment;
-  if (q->style()->styleHint(QStyle::SH_UnderlineShortcut, &opt, q))
-    {
-    tf |= Qt::TextShowMnemonic;
-    }
-  else
-    {
-    tf |= Qt::TextHideMnemonic;
-    }
-  int textWidth = opt.fontMetrics.boundingRect(opt.rect, tf, opt.text).width();
-  int indicatorSpacing = q->style()->pixelMetric(QStyle::PM_CheckBoxLabelSpacing, &opt, q);
-  int buttonMargin = q->style()->pixelMetric(QStyle::PM_ButtonMargin, &opt, q);
-  if (this->IndicatorAlignment & Qt::AlignLeft)
-    {
-    rect = QRect((buttonHeight - indicatorSize.width()) / 2,
-                 (buttonHeight - indicatorSize.height()) / 2,
-                 indicatorSize.width(), indicatorSize.height());
-    }
-  else if (this->IndicatorAlignment & Qt::AlignHCenter)
-    {
-    int w = indicatorSize.width();
-    if (!opt.text.isEmpty() && (this->TextAlignment & Qt::AlignHCenter))
-      {
-      w += textWidth + indicatorSpacing;
-      }
-    rect = QRect(opt.rect.x()+ opt.rect.width() /2 - w / 2,
-                 (buttonHeight - indicatorSize.height()) / 2,
-                 indicatorSize.width(), indicatorSize.height());
-    if (this->TextAlignment & Qt::AlignLeft &&
-        rect.left() < opt.rect.x() + buttonMargin + textWidth)
-      {
-      rect.moveLeft(opt.rect.x() + buttonMargin + textWidth);
-      }
-    else if (this->TextAlignment & Qt::AlignRight &&
-             rect.right() > opt.rect.right() - buttonMargin - textWidth)
-      {
-      rect.moveRight(opt.rect.right() - buttonMargin - textWidth);
-      }
-    }
-  else if (this->IndicatorAlignment & Qt::AlignRight)
-    {
-    rect = QRect(opt.rect.width() - (buttonHeight - indicatorSize.width()) / 2
-                                  - indicatorSize.width(),
-                 (buttonHeight - indicatorSize.height()) / 2,
-                 indicatorSize.width(), indicatorSize.height());
-    }
-  return rect;
+  q->setIconSize(indicatorSize);
+  this->IconSpacing = q->style()->pixelMetric(QStyle::PM_CheckBoxLabelSpacing, &opt, q);
 }
 
 //-----------------------------------------------------------------------------
-QSize ctkCheckablePushButtonPrivate::buttonSizeHint()const
+QStyleOptionButton ctkCheckablePushButtonPrivate::drawIcon(QPainter* p)
 {
-  Q_Q(const ctkCheckablePushButton);
-  int w = 0, h = 0;
+  Q_Q(ctkCheckablePushButton);
 
-  QStyleOptionButton opt;
-  opt.initFrom(q);
-  
-  // indicator
-  QSize indicatorSize = QSize(q->style()->pixelMetric(QStyle::PM_IndicatorWidth, &opt, q),
-                              q->style()->pixelMetric(QStyle::PM_IndicatorHeight, &opt, q));
-  int indicatorSpacing = q->style()->pixelMetric(QStyle::PM_CheckBoxLabelSpacing, &opt, q);
-  int ih = indicatorSize.height();
-  int iw = indicatorSize.width() + indicatorSpacing;
-  w += iw;
-  h = qMax(h, ih);
-  
-  // text 
-  QString string(q->text());
-  bool empty = string.isEmpty();
-  if (empty)
+  QStyleOptionButton indicatorOpt;
+
+  indicatorOpt.init(q);
+  if (!(this->CheckBoxFlags & Qt::ItemIsUserCheckable))
     {
-    string = QString::fromLatin1("XXXX");
+    indicatorOpt.state &= ~QStyle::State_Enabled;
+    }
+  if (q->checkBoxControlsButton())
+    {
+    // Hack: calling setCheckable() instead of setCheckState while being in a
+    // control button mode leads to an inconsistent state, we need to make
+    // synchronize the 2 properties.
+    q->setCheckState(q->isCheckable() ? Qt::Checked : Qt::Unchecked);
     }
-  QFontMetrics fm = q->fontMetrics();
-  QSize sz = fm.size(Qt::TextShowMnemonic, string);
-  if(!empty || !w)
+  switch (this->CheckState)
     {
-    w += sz.width();
+    case Qt::Checked:
+      indicatorOpt.state |= QStyle::State_On;
+      break;
+    case Qt::PartiallyChecked:
+      indicatorOpt.state |= QStyle::State_NoChange;
+      break;
+    default:
+    case Qt::Unchecked:
+      indicatorOpt.state |= QStyle::State_Off;
+      break;
     }
-  h = qMax(h, sz.height());
-  //opt.rect.setSize(QSize(w, h)); // PM_MenuButtonIndicator depends on the height
-  QSize buttonSize = (q->style()->sizeFromContents(QStyle::CT_PushButton, &opt, QSize(w, h), q).
-                      expandedTo(QApplication::globalStrut()));
-  return buttonSize;
+  indicatorOpt.rect = this->iconRect();
+  q->style()->drawPrimitive(QStyle::PE_IndicatorCheckBox, &indicatorOpt, p, 0);
+  return indicatorOpt;
 }
 
+
 //-----------------------------------------------------------------------------
 ctkCheckablePushButton::ctkCheckablePushButton(QWidget* _parent)
-  :QPushButton(_parent)
-  , d_ptr(new ctkCheckablePushButtonPrivate(*this))
+  : ctkPushButton(new ctkCheckablePushButtonPrivate(*this), _parent)
 {
   Q_D(ctkCheckablePushButton);
   d->init();
@@ -181,44 +125,28 @@ ctkCheckablePushButton::ctkCheckablePushButton(QWidget* _parent)
 
 //-----------------------------------------------------------------------------
 ctkCheckablePushButton::ctkCheckablePushButton(const QString& title, QWidget* _parent)
-  :QPushButton(title, _parent)
-  , d_ptr(new ctkCheckablePushButtonPrivate(*this))
-{
-}
-
-//-----------------------------------------------------------------------------
-ctkCheckablePushButton::~ctkCheckablePushButton()
-{
-}
-
-//-----------------------------------------------------------------------------
-void ctkCheckablePushButton::setButtonTextAlignment(Qt::Alignment textAlignment)
+  : ctkPushButton(new ctkCheckablePushButtonPrivate(*this), _parent)
 {
   Q_D(ctkCheckablePushButton);
-  d->TextAlignment = textAlignment;
-  this->update();
+  d->init();
+  this->setText(title);
 }
 
 //-----------------------------------------------------------------------------
-Qt::Alignment ctkCheckablePushButton::buttonTextAlignment()const
+ctkCheckablePushButton::~ctkCheckablePushButton()
 {
-  Q_D(const ctkCheckablePushButton);
-  return d->TextAlignment;
 }
 
 //-----------------------------------------------------------------------------
 void ctkCheckablePushButton::setIndicatorAlignment(Qt::Alignment indicatorAlignment)
 {
-  Q_D(ctkCheckablePushButton);
-  d->IndicatorAlignment = indicatorAlignment;
-  this->update();
+  this->setIconAlignment(indicatorAlignment);
 }
 
 //-----------------------------------------------------------------------------
 Qt::Alignment ctkCheckablePushButton::indicatorAlignment()const
 {
-  Q_D(const ctkCheckablePushButton);
-  return d->IndicatorAlignment;
+  return this->iconAlignment();
 }
 
 //-----------------------------------------------------------------------------
@@ -303,144 +231,14 @@ bool ctkCheckablePushButton::isCheckBoxUserCheckable()const
 }
 
 //-----------------------------------------------------------------------------
-QSize ctkCheckablePushButton::minimumSizeHint()const
-{
-  Q_D(const ctkCheckablePushButton);
-  return d->buttonSizeHint();
-}
-
-//-----------------------------------------------------------------------------
-QSize ctkCheckablePushButton::sizeHint()const
-{
-  return this->minimumSizeHint();
-}
-
-//-----------------------------------------------------------------------------
-void ctkCheckablePushButton::paintEvent(QPaintEvent * _event)
-{
-  Q_UNUSED(_event);
-  Q_D(ctkCheckablePushButton);
-
-  QPainter p(this);
-  // Draw Button
-  QStyleOptionButton opt;
-  this->initStyleOption(&opt);
-
-  // Checkbox size
-  QSize indicatorSize = QSize(style()->pixelMetric(QStyle::PM_IndicatorWidth, &opt, this),
-                              style()->pixelMetric(QStyle::PM_IndicatorHeight, &opt, this));
-  // Replace the icon size by the checkbox size
-  opt.iconSize = indicatorSize;
-  // Draw the panel of the button (no text, no icon)
-  style()->drawControl(QStyle::CE_PushButtonBevel, &opt, &p, this);
-  // TBD is PE_PanelButtonCommand better ?
-  //style()->drawPrimitive(QStyle::PE_PanelButtonCommand, &opt, &p, this);
-  //int buttonHeight = opt.rect.height();
-  uint tf = d->TextAlignment;
-  if (this->style()->styleHint(QStyle::SH_UnderlineShortcut, &opt, this))
-    {
-    tf |= Qt::TextShowMnemonic;
-    }
-  else
-    {
-    tf |= Qt::TextHideMnemonic;
-    }
-  int textWidth = opt.fontMetrics.boundingRect(opt.rect, tf, opt.text).width();
-  // Spacing between the text and the checkbox
-  int indicatorSpacing = this->style()->pixelMetric(QStyle::PM_CheckBoxLabelSpacing, &opt, this);
-  int buttonMargin = this->style()->pixelMetric(QStyle::PM_ButtonMargin, &opt, this);
-  // Draw Indicator
-  QStyleOptionButton indicatorOpt;
-  indicatorOpt.init(this);
-  if (!(d->CheckBoxFlags & Qt::ItemIsUserCheckable))
-    {
-    indicatorOpt.state &= ~QStyle::State_Enabled;
-    }
-  if (this->checkBoxControlsButton())
-    {
-    // Hack: calling setCheckable() instead of setCheckState while being in a
-    // control button mode leads to an inconsistent state, we need to make
-    // synchronize the 2 properties.
-    this->setCheckState(this->isCheckable() ? Qt::Checked : Qt::Unchecked);
-    }
-  switch (d->CheckState)
-    {
-    case Qt::Checked:
-      indicatorOpt.state |= QStyle::State_On;
-      break;
-    case Qt::PartiallyChecked:
-      indicatorOpt.state |= QStyle::State_NoChange;
-      break;
-    default:
-    case Qt::Unchecked:
-      indicatorOpt.state |= QStyle::State_Off;
-      break;
-    }
-  indicatorOpt.rect = d->checkboxRect();
-  this->style()->drawPrimitive(QStyle::PE_IndicatorCheckBox, &indicatorOpt, &p, 0);
-
-  // Draw Text
-  if (d->TextAlignment & Qt::AlignLeft)
-    {
-    if (d->IndicatorAlignment & Qt::AlignLeft)
-      {
-      opt.rect.setLeft(indicatorOpt.rect.right() + indicatorSpacing);
-      }
-    else
-      {
-      opt.rect.setLeft(opt.rect.x() + buttonMargin);
-      }
-    }
-  else if (d->TextAlignment & Qt::AlignHCenter)
-    {
-    if (d->IndicatorAlignment & Qt::AlignHCenter)
-      {
-      opt.rect.setLeft(indicatorOpt.rect.right() + indicatorSpacing);
-      }
-    else
-      {
-      opt.rect.setLeft(opt.rect.x() + opt.rect.width() / 2 - textWidth / 2);
-      if (d->IndicatorAlignment & Qt::AlignLeft)
-        {
-        opt.rect.setLeft( qMax(indicatorOpt.rect.right() + indicatorSpacing, opt.rect.left()) );
-        }
-      }
-    }
-  else if (d->TextAlignment & Qt::AlignRight)
-    {
-    if (d->IndicatorAlignment & Qt::AlignRight)
-      {
-      opt.rect.setLeft(indicatorOpt.rect.left() - indicatorSpacing - textWidth);
-      }
-    else
-      {
-      opt.rect.setLeft(opt.rect.right() - buttonMargin - textWidth);
-      }
-    }
-  // all the computations have been made infering the text would be left oriented
-  tf &= ~Qt::AlignHCenter & ~Qt::AlignRight;
-  tf |= Qt::AlignLeft;
-  this->style()->drawItemText(&p, opt.rect, tf, opt.palette, (opt.state & QStyle::State_Enabled),
-                        opt.text, QPalette::ButtonText);
-}
-
-//-----------------------------------------------------------------------------
 bool ctkCheckablePushButton::hitButton(const QPoint & _pos)const
 {
   Q_D(const ctkCheckablePushButton);
-  return !d->checkboxRect().contains(_pos) 
+  return !d->iconRect().contains(_pos)
     && this->QPushButton::hitButton(_pos);
 }
 
 //-----------------------------------------------------------------------------
-void ctkCheckablePushButton::initStyleOption(QStyleOptionButton* option)const
-{
-  this->QPushButton::initStyleOption(option);
-  option->iconSize = QSize(this->style()->pixelMetric(QStyle::PM_IndicatorWidth, option, this),
-                           this->style()->pixelMetric(QStyle::PM_IndicatorHeight, option, this));
-}
-
-//-----------------------------------------------------------------------------
 void ctkCheckablePushButton::mousePressEvent(QMouseEvent *e)
 {
   Q_D(ctkCheckablePushButton);
@@ -449,7 +247,7 @@ void ctkCheckablePushButton::mousePressEvent(QMouseEvent *e)
     {
     return;
     }
-  if (d->checkboxRect().contains(e->pos()) &&
+  if (d->iconRect().contains(e->pos()) &&
       (d->CheckBoxFlags & Qt::ItemIsUserCheckable))
     {
     Qt::CheckState newCheckState;

+ 4 - 22
Libs/Widgets/ctkCheckablePushButton.h

@@ -21,12 +21,9 @@
 #ifndef __ctkCheckablePushButton_h
 #define __ctkCheckablePushButton_h
 
-// Qt includes
-#include <QPushButton>
-
 // CTK includes
 #include <ctkPimpl.h>
-
+#include "ctkPushButton.h"
 #include "ctkWidgetsExport.h"
 
 class ctkCheckablePushButtonPrivate;
@@ -48,10 +45,11 @@ class ctkCheckablePushButtonPrivate;
 /// setChecked(bool) slot.
 /// \warning The checkbox is drawn in place of the pushbuton icon, any icon
 /// will then be ignored.
-class CTK_WIDGETS_EXPORT ctkCheckablePushButton : public QPushButton
+class CTK_WIDGETS_EXPORT ctkCheckablePushButton : public ctkPushButton
 {
   Q_OBJECT
-  Q_PROPERTY(Qt::Alignment buttonTextAlignment READ buttonTextAlignment WRITE setButtonTextAlignment)
+  /// This property controls the location of the checkbox with regard to the text.
+  /// Qt::AlignLeft|Qt::AlignVCenter by default
   Q_PROPERTY(Qt::Alignment indicatorAlignment READ indicatorAlignment WRITE setIndicatorAlignment)
   Q_PROPERTY(Qt::CheckState checkState READ checkState WRITE setCheckState NOTIFY checkStateChanged)
   Q_PROPERTY(bool checkBoxControlsButton READ checkBoxControlsButton WRITE setCheckBoxControlsButton)
@@ -62,21 +60,11 @@ public:
   ctkCheckablePushButton(const QString& text, QWidget *parent = 0);
   virtual ~ctkCheckablePushButton();
 
-  ///
-  /// Set the alignment of the text on the button,
-  /// Qt::AlignLeft|Qt::AlignVCenter by default.
-  void setButtonTextAlignment(Qt::Alignment textAlignment);
-  Qt::Alignment buttonTextAlignment()const;
-
-  ///
   /// Set the alignment of the indicator (arrow) on the button,
   /// Qt::AlignLeft|Qt::AlignVCenter by default.
   void setIndicatorAlignment(Qt::Alignment indicatorAlignment);
   Qt::Alignment indicatorAlignment()const;
 
-  virtual QSize minimumSizeHint()const;
-  virtual QSize sizeHint()const;
-
   virtual Qt::CheckState checkState()const;
   virtual void setCheckState(Qt::CheckState checkState);
 
@@ -94,15 +82,9 @@ Q_SIGNALS:
 
 protected:
   /// Reimplemented for internal reasons
-  virtual void paintEvent(QPaintEvent*);
-  /// Reimplemented for internal reasons
   virtual void mousePressEvent(QMouseEvent* event);
   /// Reimplemented for internal reasons
   virtual bool hitButton(const QPoint & pos) const;
-  /// Reimplemented for internal reasons
-  virtual void initStyleOption ( QStyleOptionButton * option ) const;
-protected:
-  QScopedPointer<ctkCheckablePushButtonPrivate> d_ptr;
 
 private:
   Q_DECLARE_PRIVATE(ctkCheckablePushButton);

+ 326 - 0
Libs/Widgets/ctkPushButton.cpp

@@ -0,0 +1,326 @@
+/*=========================================================================
+
+  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.apache.org/licenses/LICENSE-2.0.txt
+
+  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 <QLayout>
+#include <QPainter>
+#include <QStyle>
+#include <QStyleOptionButton>
+#include <QStylePainter>
+
+// CTK includes
+#include "ctkPushButton_p.h"
+
+//-----------------------------------------------------------------------------
+ctkPushButtonPrivate::ctkPushButtonPrivate(ctkPushButton& object)
+  :q_ptr(&object)
+{
+  this->ButtonTextAlignment = Qt::AlignHCenter | Qt::AlignVCenter;
+  this->IconAlignment = Qt::AlignLeft | Qt::AlignVCenter;
+  this->IconSpacing = 4;
+}
+
+//-----------------------------------------------------------------------------
+void ctkPushButtonPrivate::init()
+{
+}
+
+//-----------------------------------------------------------------------------
+QRect ctkPushButtonPrivate::iconRect()const
+{
+  Q_Q(const ctkPushButton);
+  QRect rect;
+  QStyleOptionButton opt;
+  q->initStyleOption(&opt);
+
+  QSize iconSize = q->iconSize();
+  int buttonHeight = opt.rect.height();
+  uint tf = this->ButtonTextAlignment;
+  if (q->style()->styleHint(QStyle::SH_UnderlineShortcut, &opt, q))
+    {
+    tf |= Qt::TextShowMnemonic;
+    }
+  else
+    {
+    tf |= Qt::TextHideMnemonic;
+    }
+  int textWidth = opt.fontMetrics.boundingRect(opt.rect, tf, opt.text).width();
+  int iconSpacing = this->IconSpacing;
+  int buttonMargin = q->style()->pixelMetric(QStyle::PM_ButtonMargin, &opt, q);
+  if (this->IconAlignment & Qt::AlignLeft)
+    {
+    rect = QRect((buttonHeight - iconSize.width()) / 2,
+                 (buttonHeight - iconSize.height()) / 2,
+                 iconSize.width(), iconSize.height());
+    }
+  else if (this->IconAlignment & Qt::AlignHCenter)
+    {
+    int w = iconSize.width();
+    if (!opt.text.isEmpty() && (this->ButtonTextAlignment & Qt::AlignHCenter))
+      {
+      w += textWidth + iconSpacing;
+      }
+    rect = QRect(opt.rect.x()+ opt.rect.width() /2 - w / 2,
+                 (buttonHeight - iconSize.height()) / 2,
+                 iconSize.width(), iconSize.height());
+    if (this->ButtonTextAlignment & Qt::AlignLeft &&
+        rect.left() < opt.rect.x() + buttonMargin + textWidth)
+      {
+      rect.moveLeft(opt.rect.x() + buttonMargin + textWidth);
+      }
+    else if (this->ButtonTextAlignment & Qt::AlignRight &&
+             rect.right() > opt.rect.right() - buttonMargin - textWidth)
+      {
+      rect.moveRight(opt.rect.right() - buttonMargin - textWidth);
+      }
+    }
+  else if (this->IconAlignment & Qt::AlignRight)
+    {
+    rect = QRect(opt.rect.width() - (buttonHeight - iconSize.width()) / 2
+                                  - iconSize.width(),
+                 (buttonHeight - iconSize.height()) / 2,
+                 iconSize.width(), iconSize.height());
+    }
+  return rect;
+}
+
+//-----------------------------------------------------------------------------
+QSize ctkPushButtonPrivate::buttonSizeHint()const
+{
+  Q_Q(const ctkPushButton);
+  int w = 0, h = 0;
+
+  QStyleOptionButton opt;
+  opt.initFrom(q);
+
+  // icon
+  QSize iconSize = q->iconSize();
+  int ih = iconSize.height();
+  int iw = iconSize.width() + this->IconSpacing;
+  w += iw;
+  h = qMax(h, ih);
+
+  // text
+  QString string(q->text());
+  bool empty = string.isEmpty();
+  if (empty)
+    {
+    string = QString::fromLatin1("XXXX");
+    }
+  QFontMetrics fm = q->fontMetrics();
+  QSize sz = fm.size(Qt::TextShowMnemonic, string);
+  if(!empty || !w)
+    {
+    w += sz.width();
+    }
+  h = qMax(h, sz.height());
+  //opt.rect.setSize(QSize(w, h)); // PM_MenuButtonIndicator depends on the height
+  QSize buttonSize = (q->style()->sizeFromContents(QStyle::CT_PushButton, &opt, QSize(w, h), q).
+                      expandedTo(QApplication::globalStrut()));
+  return buttonSize;
+}
+
+//-----------------------------------------------------------------------------
+QStyleOptionButton ctkPushButtonPrivate::drawIcon(QPainter* p)
+{
+  Q_Q(ctkPushButton);
+  QStyleOptionButton iconOpt;
+  iconOpt.init(q);
+  iconOpt.rect = this->iconRect();
+  QIcon::Mode mode = iconOpt.state & QStyle::State_Enabled ? QIcon::Normal : QIcon::Disabled;
+  if (mode == QIcon::Normal && iconOpt.state & QStyle::State_HasFocus)
+    {
+    mode = QIcon::Active;
+    }
+  QIcon::State state = QIcon::Off;
+  if (iconOpt.state & QStyle::State_On)
+    {
+    state = QIcon::On;
+    }
+
+  QPixmap pixmap = q->icon().pixmap(iconOpt.rect.size(), mode, state);
+  p->drawPixmap(iconOpt.rect, pixmap);
+  return iconOpt;
+}
+
+//-----------------------------------------------------------------------------
+ctkPushButton::ctkPushButton(QWidget* _parent)
+  : QPushButton(_parent)
+  , d_ptr(new ctkPushButtonPrivate(*this))
+{
+  Q_D(ctkPushButton);
+  d->init();
+}
+
+//-----------------------------------------------------------------------------
+ctkPushButton::ctkPushButton(const QString& title, QWidget* _parent)
+  : QPushButton(title, _parent)
+  , d_ptr(new ctkPushButtonPrivate(*this))
+{
+  Q_D(ctkPushButton);
+  d->init();
+}
+
+//-----------------------------------------------------------------------------
+ctkPushButton::ctkPushButton(const QIcon& icon, const QString& title,
+                             QWidget* _parent)
+  : QPushButton(icon, title, _parent)
+  , d_ptr(new ctkPushButtonPrivate(*this))
+{
+  Q_D(ctkPushButton);
+  d->init();
+}
+
+//-----------------------------------------------------------------------------
+ctkPushButton::ctkPushButton(ctkPushButtonPrivate* pimpl, QWidget* _parent)
+  : QPushButton(_parent)
+  , d_ptr(pimpl)
+{
+}
+
+//-----------------------------------------------------------------------------
+ctkPushButton::~ctkPushButton()
+{
+}
+
+//-----------------------------------------------------------------------------
+void ctkPushButton::setButtonTextAlignment(Qt::Alignment newButtonTextAlignment)
+{
+  Q_D(ctkPushButton);
+  d->ButtonTextAlignment = newButtonTextAlignment;
+  this->update();
+}
+
+//-----------------------------------------------------------------------------
+Qt::Alignment ctkPushButton::buttonTextAlignment()const
+{
+  Q_D(const ctkPushButton);
+  return d->ButtonTextAlignment;
+}
+
+//-----------------------------------------------------------------------------
+void ctkPushButton::setIconAlignment(Qt::Alignment newIconAlignment)
+{
+  Q_D(ctkPushButton);
+  d->IconAlignment = newIconAlignment;
+  this->update();
+}
+
+//-----------------------------------------------------------------------------
+Qt::Alignment ctkPushButton::iconAlignment()const
+{
+  Q_D(const ctkPushButton);
+  return d->IconAlignment;
+}
+
+//-----------------------------------------------------------------------------
+QSize ctkPushButton::minimumSizeHint()const
+{
+  Q_D(const ctkPushButton);
+  return d->buttonSizeHint();
+}
+
+//-----------------------------------------------------------------------------
+QSize ctkPushButton::sizeHint()const
+{
+  return this->minimumSizeHint();
+}
+
+//-----------------------------------------------------------------------------
+void ctkPushButton::paintEvent(QPaintEvent * _event)
+{
+  Q_UNUSED(_event);
+  Q_D(ctkPushButton);
+
+  QPainter p(this);
+  // Draw Button
+  QStyleOptionButton opt;
+  this->initStyleOption(&opt);
+
+  // Checkbox size
+  QSize iconSize = this->iconSize();
+  // Replace the icon size by the checkbox size
+  opt.iconSize = iconSize;
+  // Draw the panel of the button (no text, no icon)
+  style()->drawControl(QStyle::CE_PushButtonBevel, &opt, &p, this);
+  // TBD is PE_PanelButtonCommand better ?
+  //style()->drawPrimitive(QStyle::PE_PanelButtonCommand, &opt, &p, this);
+  //int buttonHeight = opt.rect.height();
+  uint tf = d->ButtonTextAlignment;
+  if (this->style()->styleHint(QStyle::SH_UnderlineShortcut, &opt, this))
+    {
+    tf |= Qt::TextShowMnemonic;
+    }
+  else
+    {
+    tf |= Qt::TextHideMnemonic;
+    }
+  int textWidth = opt.fontMetrics.boundingRect(opt.rect, tf, opt.text).width();
+  int buttonMargin = this->style()->pixelMetric(QStyle::PM_ButtonMargin, &opt, this);
+  // Draw Icon
+  QStyleOptionButton iconOpt = d->drawIcon(&p);
+  // Spacing between the text and the checkbox
+  int iconSpacing = d->IconSpacing;
+
+  // Draw Text
+  if (d->ButtonTextAlignment & Qt::AlignLeft)
+    {
+    if (d->IconAlignment & Qt::AlignLeft)
+      {
+      opt.rect.setLeft(iconOpt.rect.right() + iconSpacing);
+      }
+    else
+      {
+      opt.rect.setLeft(opt.rect.x() + buttonMargin);
+      }
+    }
+  else if (d->ButtonTextAlignment & Qt::AlignHCenter)
+    {
+    if (d->IconAlignment & Qt::AlignHCenter)
+      {
+      opt.rect.setLeft(iconOpt.rect.right() + iconSpacing);
+      }
+    else
+      {
+      opt.rect.setLeft(opt.rect.x() + opt.rect.width() / 2 - textWidth / 2);
+      if (d->IconAlignment & Qt::AlignLeft)
+        {
+        opt.rect.setLeft( qMax(iconOpt.rect.right() + iconSpacing, opt.rect.left()) );
+        }
+      }
+    }
+  else if (d->ButtonTextAlignment & Qt::AlignRight)
+    {
+    if (d->IconAlignment & Qt::AlignRight)
+      {
+      opt.rect.setLeft(iconOpt.rect.left() - iconSpacing - textWidth);
+      }
+    else
+      {
+      opt.rect.setLeft(opt.rect.right() - buttonMargin - textWidth);
+      }
+    }
+  // all the computations have been made infering the text would be left oriented
+  tf &= ~Qt::AlignHCenter & ~Qt::AlignRight;
+  tf |= Qt::AlignLeft;
+  this->style()->drawItemText(&p, opt.rect, tf, opt.palette, (opt.state & QStyle::State_Enabled),
+                              opt.text, QPalette::ButtonText);
+}

+ 85 - 0
Libs/Widgets/ctkPushButton.h

@@ -0,0 +1,85 @@
+/*=========================================================================
+
+  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.apache.org/licenses/LICENSE-2.0.txt
+
+  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 __ctkPushButton_h
+#define __ctkPushButton_h
+
+// Qt includes
+#include <QPushButton>
+
+// CTK includes
+#include <ctkPimpl.h>
+
+#include "ctkWidgetsExport.h"
+
+class ctkPushButtonPrivate;
+
+/// \ingroup Widgets
+/// Description
+/// ctkPushButton is an advanced QPushButton. It can control the alignment of text and icons.
+class CTK_WIDGETS_EXPORT ctkPushButton : public QPushButton
+{
+  Q_OBJECT
+  /// Set the alignment of the text on the button,
+  /// Qt::AlignHCenter|Qt::AlignVCenter by default.
+  /// \sa textAlignment(), setTextAlignment(), iconAlignment
+  Q_PROPERTY(Qt::Alignment buttonTextAlignment READ buttonTextAlignment WRITE setButtonTextAlignment)
+  /// Set the alignment of the icon with regard to the text.
+  /// Qt::AlignLeft|Qt::AlignVCenter by default.
+  /// \sa iconAlignment(), setIconAlignment(), textAlignment
+  Q_PROPERTY(Qt::Alignment iconAlignment READ iconAlignment WRITE setIconAlignment)
+
+public:
+  ctkPushButton(QWidget *parent = 0);
+  ctkPushButton(const QString& text, QWidget *parent = 0);
+  ctkPushButton(const QIcon& icon, const QString& text, QWidget *parent = 0);
+  virtual ~ctkPushButton();
+
+  /// Set the buttonTextAlignment property value.
+  /// \sa buttonTextAlignment
+  void setButtonTextAlignment(Qt::Alignment buttonTextAlignment);
+  /// Return the buttonTextAlignment property value.
+  /// \sa buttonTextAlignment
+  Qt::Alignment buttonTextAlignment()const;
+
+  /// Set the iconAlignment property value.
+  /// \sa iconAlignment
+  void setIconAlignment(Qt::Alignment iconAlignment);
+  /// Return the iconAlignment property value.
+  /// \sa iconAlignment
+  Qt::Alignment iconAlignment()const;
+
+  virtual QSize minimumSizeHint()const;
+  virtual QSize sizeHint()const;
+
+protected:
+  /// Reimplemented for internal reasons
+  virtual void paintEvent(QPaintEvent*);
+
+protected:
+  QScopedPointer<ctkPushButtonPrivate> d_ptr;
+  ctkPushButton(ctkPushButtonPrivate*, QWidget* parent = 0);
+
+private:
+  Q_DECLARE_PRIVATE(ctkPushButton);
+  Q_DISABLE_COPY(ctkPushButton);
+};
+
+#endif

+ 42 - 0
Libs/Widgets/ctkPushButton_p.h

@@ -0,0 +1,42 @@
+/*=========================================================================
+
+  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.apache.org/licenses/LICENSE-2.0.txt
+
+  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 "ctkPushButton.h"
+
+//-----------------------------------------------------------------------------
+class ctkPushButtonPrivate
+{
+  Q_DECLARE_PUBLIC(ctkPushButton);
+protected:
+  ctkPushButton* const q_ptr;
+public:
+  ctkPushButtonPrivate(ctkPushButton& object);
+  void init();
+
+  virtual QRect iconRect() const;
+  virtual QSize buttonSizeHint()const;
+  virtual QStyleOptionButton drawIcon(QPainter* p);
+
+  // Tuning of the button look&feel
+  Qt::Alignment ButtonTextAlignment;
+  Qt::Alignment IconAlignment;
+  int IconSpacing;
+};

+ 101 - 120
Libs/Widgets/ctkWorkflowButtonBoxWidget.cpp

@@ -19,17 +19,18 @@
   =========================================================================*/
 
 // Qt includes
-#include <QWidget>
-#include <QList>
-#include <QPushButton>
 #include <QBoxLayout>
 #include <QDebug>
+#include <QList>
 #include <QPointer>
 #include <QStyle>
+#include <QVariant>
 
 // CTK includes
+#include "ctkPushButton.h"
 #include "ctkWorkflowButtonBoxWidget.h"
 #include "ctkWorkflowStep.h"
+#include "ctkWorkflowWidget.h"
 #include "ctkWorkflowWidgetStep.h"
 #include "ctkWorkflow.h"
 
@@ -51,18 +52,21 @@ public:
   QPointer<ctkWorkflow>    Workflow;
 
   // Convenient typedefs
-  typedef QMap<QPushButton*, ctkWorkflowStep*> ButtonToStepMapType;
+  typedef QMap<ctkPushButton*, ctkWorkflowStep*> ButtonToStepMapType;
 
   // The buttons on the widget (maintain maps associating each forward/goTo button with its step)
-  QPushButton*        BackButton;
-  QString             BackButtonDefaultText;
-  QPushButton*        NextButton;
-  QString             NextButtonDefaultText;
+  ctkPushButton*      BackButton;
+  QString             BackButtonFormat;
+  ctkPushButton*      NextButton;
+  QString             NextButtonFormat;
   ButtonToStepMapType GoToButtonToStepMap;
+  QString             GoToButtonsFormat;
 
   // Direction for layout (for use with QBoxLayout only)
   QBoxLayout::Direction Direction;
+  QSizePolicy ButtonSizePolicy;
 
+  bool HideGoToButtons;
   bool HideInvalidButtons;
 
   // Functions to add/remove buttons
@@ -80,10 +84,13 @@ ctkWorkflowButtonBoxWidgetPrivate::ctkWorkflowButtonBoxWidgetPrivate(ctkWorkflow
   :q_ptr(&object)
 {
   this->BackButton = 0;
-  this->BackButtonDefaultText = "Back";
+  this->BackButtonFormat = "[<-]{backButtonText|\"Back\"}(back:description)";
   this->NextButton = 0;
-  this->NextButtonDefaultText = "Next";
+  this->NextButtonFormat = "{nextButtonText|\"Next\"}(next:description)[->]";
+  this->GoToButtonsFormat = "[icon]{stepid|\"Finish\"}(description)";
   this->Direction = QBoxLayout::LeftToRight;
+  this->ButtonSizePolicy = QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+  this->HideGoToButtons = false;
   this->HideInvalidButtons = false;
 }
 
@@ -100,11 +107,14 @@ void ctkWorkflowButtonBoxWidgetPrivate::setupUi(QWidget * newParent)
   QIcon nextIcon = newParent->style()->standardIcon(QStyle::SP_ArrowRight);
 
   // Setup the buttons
-  this->BackButton = new QPushButton(backIcon, this->BackButtonDefaultText, newParent);
+  this->BackButton = new ctkPushButton(backIcon, this->BackButtonFormat, newParent);
+  this->BackButton->setSizePolicy(this->ButtonSizePolicy);
+  this->BackButton->setIconAlignment(Qt::AlignLeft | Qt::AlignVCenter);
   newParent->layout()->addWidget(this->BackButton);
 
-  this->NextButton = new QPushButton(nextIcon, this->NextButtonDefaultText, newParent);
-  this->NextButton->setLayoutDirection(Qt::RightToLeft);
+  this->NextButton = new ctkPushButton(nextIcon, this->NextButtonFormat, newParent);
+  this->NextButton->setSizePolicy(this->ButtonSizePolicy);
+  this->NextButton->setIconAlignment(Qt::AlignRight | Qt::AlignVCenter);
   newParent->layout()->addWidget(this->NextButton);
 }
 
@@ -120,35 +130,25 @@ void ctkWorkflowButtonBoxWidgetPrivate::updateBackButton(ctkWorkflowStep* curren
 
   ctkWorkflowWidgetStep* step = dynamic_cast<ctkWorkflowWidgetStep*>(currentStep);
 
-  // Set the back button text
-  QString backButtonText = this->BackButtonDefaultText;
-  if (step && !step->backButtonText().isEmpty())
-    {
-    backButtonText = step->backButtonText();
-    }
-  this->BackButton->setText(backButtonText);
+  // Set the back button text and icon
+  ctkWorkflowWidget::formatButton(this->BackButton, this->BackButtonFormat, step);
 
-  // Enable and show the back button if we can go backward
-  if (currentStep && this->Workflow->canGoBackward(currentStep))
+  // Disable the back button if we can't go backward
+  bool enable = currentStep && this->Workflow->canGoBackward(currentStep);
+  bool visible = true;
+  // Apply the buttonBox hints if possible
+  if (enable && step)
     {
-    this->BackButton->setEnabled(true);
-    this->BackButton->show();
-
-    // Apply the buttonBox hints if possible
-    if (step)
-      {
-      this->BackButton->setDisabled(
-          step->buttonBoxHints() & ctkWorkflowWidgetStep::BackButtonDisabled);
-      this->BackButton->setHidden(
-          step->buttonBoxHints() & ctkWorkflowWidgetStep::BackButtonHidden);
-      }
+    enable = !(step->buttonBoxHints() & ctkWorkflowWidgetStep::BackButtonDisabled);
+    visible = !(step->buttonBoxHints() & (ctkWorkflowWidgetStep::BackButtonHidden |
+                                          ctkWorkflowWidgetStep::ButtonBoxHidden));
     }
-  // Disable the back button if we can't go backward, and optionally hide it
-  else
+  this->BackButton->setEnabled(enable);
+  if (!enable && this->HideInvalidButtons)
     {
-    this->BackButton->setEnabled(false);
-    this->HideInvalidButtons ? this->BackButton->hide() : this->BackButton->show();
+    visible = false;
     }
+  this->BackButton->setVisible(visible);
 }
 
 //-----------------------------------------------------------------------------
@@ -164,35 +164,25 @@ void ctkWorkflowButtonBoxWidgetPrivate::updateNextButton(ctkWorkflowStep* curren
 
   ctkWorkflowWidgetStep* step = dynamic_cast<ctkWorkflowWidgetStep*>(currentStep);
 
-  // Set the next button text
-  QString nextButtonText = this->NextButtonDefaultText;
-  if (step && !step->nextButtonText().isEmpty())
-    {
-    nextButtonText = step->nextButtonText();
-    }
-  this->NextButton->setText(nextButtonText);
+  // Set the next button text and icon
+  ctkWorkflowWidget::formatButton(this->NextButton, this->NextButtonFormat, step);
 
-  // Enable and show the next button if we can go forward
-  if (currentStep && this->Workflow->canGoForward(currentStep))
+  // Disable the next button if we can't go backward
+  bool enable = currentStep && this->Workflow->canGoForward(currentStep);
+  bool visible = true;
+  // Apply the buttonBox hints if possible
+  if (enable && step)
     {
-    this->NextButton->setEnabled(true);
-    this->NextButton->show();
-
-    // Apply the buttonBox hints if possible
-    if (step)
-      {
-      this->NextButton->setDisabled(
-          step->buttonBoxHints() & ctkWorkflowWidgetStep::NextButtonDisabled);
-      this->NextButton->setHidden(
-          step->buttonBoxHints() & ctkWorkflowWidgetStep::NextButtonHidden);
-      }
+    enable = !(step->buttonBoxHints() & ctkWorkflowWidgetStep::NextButtonDisabled);
+    visible = !(step->buttonBoxHints() & (ctkWorkflowWidgetStep::NextButtonHidden |
+                                          ctkWorkflowWidgetStep::ButtonBoxHidden));
     }
-  // Disable the next button if we can't go forward, and optionally hide it
-  else
+  this->NextButton->setEnabled(enable);
+  if (!enable && this->HideInvalidButtons)
     {
-    this->NextButton->setEnabled(false);
-    this->HideInvalidButtons ? this->NextButton->hide() : this->NextButton->show();
+    visible = false;
     }
+  this->NextButton->setVisible(visible);
 }
 
 //-----------------------------------------------------------------------------
@@ -204,16 +194,18 @@ void ctkWorkflowButtonBoxWidgetPrivate::updateGoToButtons(ctkWorkflowStep* curre
   Q_ASSERT(q->layout());
 
   // Change the buttons only if the set of steps to have goTo buttons is either empty or has changed
-  QSet<ctkWorkflowStep*> goToStepsToHaveButtons = QSet<ctkWorkflowStep*>::fromList(this->Workflow->finishSteps());
-  QSet<ctkWorkflowStep*> goToStepsThatHaveButtons = QSet<ctkWorkflowStep*>::fromList(this->GoToButtonToStepMap.values());
+  QSet<ctkWorkflowStep*> goToStepsToHaveButtons =
+    QSet<ctkWorkflowStep*>::fromList(this->Workflow->finishSteps());
+  QSet<ctkWorkflowStep*> goToStepsThatHaveButtons =
+    QSet<ctkWorkflowStep*>::fromList(this->GoToButtonToStepMap.values());
 
   // Remove the buttons if the set of steps to have goTo buttons has changed
-  if (!this->GoToButtonToStepMap.isEmpty() && goToStepsThatHaveButtons != goToStepsToHaveButtons)
+  if (goToStepsThatHaveButtons != goToStepsToHaveButtons)
     {
-    foreach (QPushButton* goToButton, this->GoToButtonToStepMap.keys())
+    foreach (ctkPushButton* goToButton, this->GoToButtonToStepMap.keys())
       {
       q->layout()->removeWidget(goToButton);
-      delete goToButton;
+      goToButton->deleteLater();
       }
     this->GoToButtonToStepMap.clear();
     }
@@ -225,39 +217,36 @@ void ctkWorkflowButtonBoxWidgetPrivate::updateGoToButtons(ctkWorkflowStep* curre
     foreach (ctkWorkflowStep* step, goToStepsToHaveButtons)
       {
       // TODO shouldn't have id here
-      QPushButton* goToButton = new QPushButton(step->id());
+      ctkPushButton* goToButton = new ctkPushButton();
+      goToButton->setSizePolicy(this->ButtonSizePolicy);
+      ctkWorkflowWidgetStep* wwStep = dynamic_cast<ctkWorkflowWidgetStep*>(step);
+      ctkWorkflowWidget::formatButton(goToButton, this->GoToButtonsFormat, wwStep);
       q->layout()->addWidget(goToButton);
       QObject::connect(goToButton, SIGNAL(clicked()), q, SLOT(prepareGoToStep()));
       this->GoToButtonToStepMap[goToButton] = step;
-      // if the goTo step has an icon associated with it, then add it to the button
-      if (ctkWorkflowWidgetStep* wwStep = dynamic_cast<ctkWorkflowWidgetStep*>(step))
-        {
-        goToButton->setIcon(wwStep->icon());
-        }
       }
     }
 
   // Show/hide the goTo buttons depending on whether they are accessible from the current step
-  ctkWorkflowStep* goToStep;
-  foreach (QPushButton* goToButton, this->GoToButtonToStepMap.keys())
+  ctkWorkflowWidgetStep* step = dynamic_cast<ctkWorkflowWidgetStep*>(currentStep);
+  foreach (ctkPushButton* goToButton, this->GoToButtonToStepMap.keys())
     {
     // TODO enable and show the goTo button if we can go to it
     // ctkWorkflowStep* goToStep = this->GoToButtonToStepMap[goToButton];
     // if (this->Workflow->canGoToStep(currentStep, goToStep))
     // for now we'll assume we can go to the step
-    goToStep = this->GoToButtonToStepMap[goToButton];
+    ctkWorkflowStep* goToStep = this->GoToButtonToStepMap[goToButton];
     Q_ASSERT(goToStep);
-    if (currentStep && this->Workflow->canGoToStep(goToStep->id(), currentStep))
+    bool enable = currentStep && this->Workflow->canGoToStep(goToStep->id(), currentStep);
+    bool visible = step ? !(step->buttonBoxHints() & ctkWorkflowWidgetStep::ButtonBoxHidden) : true;
+    if ((!enable && this->HideInvalidButtons)
+        || this->HideGoToButtons
+        )
       {
-      goToButton->setEnabled(true);
-      goToButton->show();
-      }
-    // disable the goTo button if we can't go to it, and optionally hide it
-    else
-      {
-      goToButton->setEnabled(false);
-      this->HideInvalidButtons ? goToButton->hide() : goToButton->show();
+      visible = false;
       }
+    goToButton->setEnabled(enable);
+    goToButton->setVisible(visible);
     }
 }
 
@@ -319,60 +308,60 @@ void ctkWorkflowButtonBoxWidget::setWorkflow(ctkWorkflow * newWorkflow)
 }
 
 //-----------------------------------------------------------------------------
-CTK_GET_CPP(ctkWorkflowButtonBoxWidget, QString, backButtonDefaultText,
-            BackButtonDefaultText);
+CTK_GET_CPP(ctkWorkflowButtonBoxWidget, QString, backButtonFormat,
+            BackButtonFormat);
 
 //-----------------------------------------------------------------------------
-void ctkWorkflowButtonBoxWidget::setBackButtonDefaultText(const QString& defaultText)
+void ctkWorkflowButtonBoxWidget::setBackButtonFormat(const QString& format)
 {
   Q_D(ctkWorkflowButtonBoxWidget);
-  d->BackButtonDefaultText = defaultText;
-  if (d->Workflow)
-    {
-    this->updateButtons(d->Workflow->currentStep());
-    }
-  else
-    {
-    d->BackButton->setText(d->BackButtonDefaultText);
-    }
+  d->BackButtonFormat = format;
+  d->updateBackButton(d->Workflow ? d->Workflow->currentStep() : 0);
 }
 
 //-----------------------------------------------------------------------------
-CTK_GET_CPP(ctkWorkflowButtonBoxWidget, QString, nextButtonDefaultText,
-            NextButtonDefaultText);
+CTK_GET_CPP(ctkWorkflowButtonBoxWidget, QString, nextButtonFormat,
+            NextButtonFormat);
 
 //-----------------------------------------------------------------------------
-void ctkWorkflowButtonBoxWidget::setNextButtonDefaultText(const QString& defaultText)
+void ctkWorkflowButtonBoxWidget::setNextButtonFormat(const QString& format)
 {
   Q_D(ctkWorkflowButtonBoxWidget);
-  d->NextButtonDefaultText = defaultText;
-  if (d->Workflow)
-    {
-    this->updateButtons(d->Workflow->currentStep());
-    }
-  else
-    {
-    d->NextButton->setText(d->NextButtonDefaultText);
-    }
+  d->NextButtonFormat = format;
+  d->updateNextButton(d->Workflow ? d->Workflow->currentStep() : 0);
+}
+
+//-----------------------------------------------------------------------------
+CTK_GET_CPP(ctkWorkflowButtonBoxWidget, QString, goToButtonsFormat,
+            GoToButtonsFormat);
+
+//-----------------------------------------------------------------------------
+void ctkWorkflowButtonBoxWidget::setGoToButtonsFormat(const QString& format)
+{
+  Q_D(ctkWorkflowButtonBoxWidget);
+  d->GoToButtonsFormat = format;
+  d->updateGoToButtons(d->Workflow ? d->Workflow->currentStep() : 0);
 }
 
 //-----------------------------------------------------------------------------
 CTK_GET_CPP(ctkWorkflowButtonBoxWidget, ctkWorkflow*, workflow, Workflow);
-CTK_GET_CPP(ctkWorkflowButtonBoxWidget, QPushButton*, backButton, BackButton);
+CTK_GET_CPP(ctkWorkflowButtonBoxWidget, ctkPushButton*, backButton, BackButton);
 CTK_GET_CPP(ctkWorkflowButtonBoxWidget, QBoxLayout::Direction, direction, Direction);
+CTK_GET_CPP(ctkWorkflowButtonBoxWidget, bool, hideGoToButtons, HideGoToButtons);
+CTK_SET_CPP(ctkWorkflowButtonBoxWidget, bool, setHideGoToButtons, HideGoToButtons);
 CTK_GET_CPP(ctkWorkflowButtonBoxWidget, bool, hideInvalidButtons, HideInvalidButtons);
 CTK_SET_CPP(ctkWorkflowButtonBoxWidget, bool, setHideInvalidButtons, HideInvalidButtons);
 
 //-----------------------------------------------------------------------------
 // TODO will be list of next buttons for branching workflow
-QPushButton* ctkWorkflowButtonBoxWidget::nextButton()const
+ctkPushButton* ctkWorkflowButtonBoxWidget::nextButton()const
 {
   Q_D(const ctkWorkflowButtonBoxWidget);
   return d->NextButton;
 }
 
 //-----------------------------------------------------------------------------
-QList<QPushButton*> ctkWorkflowButtonBoxWidget::goToButtons()const
+QList<ctkPushButton*> ctkWorkflowButtonBoxWidget::goToButtons()const
 {
   Q_D(const ctkWorkflowButtonBoxWidget);
   return d->GoToButtonToStepMap.keys();
@@ -391,14 +380,6 @@ void ctkWorkflowButtonBoxWidget::setDirection(const QBoxLayout::Direction& newDi
 void ctkWorkflowButtonBoxWidget::updateButtons(ctkWorkflowStep* currentStep)
 {
   Q_D(ctkWorkflowButtonBoxWidget);
-
-  // hide aspects of the button bar if specified by the current step
-  if(ctkWorkflowWidgetStep* currentWidgetStep = dynamic_cast<ctkWorkflowWidgetStep*>(currentStep))
-    {
-    bool hideButtonBar = currentWidgetStep->buttonBoxHints() & ctkWorkflowWidgetStep::ButtonBoxHidden;
-    this->setHidden(hideButtonBar);
-    }
-
   d->updateBackButton(currentStep);
   d->updateNextButton(currentStep);
   d->updateGoToButtons(currentStep);
@@ -408,7 +389,7 @@ void ctkWorkflowButtonBoxWidget::updateButtons(ctkWorkflowStep* currentStep)
 void ctkWorkflowButtonBoxWidget::prepareGoToStep()
 {
   Q_D(ctkWorkflowButtonBoxWidget);
-  if (QPushButton* button = qobject_cast<QPushButton*>(QObject::sender()))
+  if (ctkPushButton* button = qobject_cast<ctkPushButton*>(QObject::sender()))
     {
     if (ctkWorkflowStep* step = d->GoToButtonToStepMap.value(button))
       {

+ 64 - 22
Libs/Widgets/ctkWorkflowButtonBoxWidget.h

@@ -19,17 +19,16 @@
 =========================================================================*/
 
 #ifndef __ctkWorkflowButtonBoxWidget_h
-#define __ctkWorkflowButtonBoxWidget_h 
+#define __ctkWorkflowButtonBoxWidget_h
 
 // QT includes
-#include <QWidget>
-//class QList;
-class QPushButton;
 #include <QBoxLayout>
+#include <QWidget>
 
 // CTK includes
 #include "ctkPimpl.h"
 #include "ctkWidgetsExport.h"
+class ctkPushButton;
 class ctkWorkflow;
 class ctkWorkflowStep;
 
@@ -49,13 +48,39 @@ class ctkWorkflowButtonBoxWidgetPrivate;
 /// should be called whenever the workflow's current step has changed
 
 class CTK_WIDGETS_EXPORT ctkWorkflowButtonBoxWidget : public QWidget
-{ 
+{
   Q_OBJECT
 
-  Q_PROPERTY(QString backButtonDefaultText
-             READ backButtonDefaultText WRITE setBackButtonDefaultText)
-  Q_PROPERTY(QString nextButtonDefaultText
-             READ nextButtonDefaultText WRITE setNextButtonDefaultText)
+  /// This property controls the text, icon and tooltip of the back button.
+  /// "[<-]{backButtonText|\"Back\"}(back:description)" by default.
+  /// \sa backButtonFormat(), setBackButtonFormat(),
+  /// ctkWorkflow::formatButton(), nextButtonFormat, goToButtonFormat
+  Q_PROPERTY(QString backButtonFormat
+             READ backButtonFormat WRITE setBackButtonFormat)
+
+  /// This property controls the text, icon and tooltip of the next button.
+  /// "{nextButtonText|\"Next\"}(next:description)[->]" by default.
+  /// \sa nextButtonFormat(), setNextButtonFormat(),
+  /// ctkWorkflow::formatButton(), backButtonFormat, goToButtonFormat
+  Q_PROPERTY(QString nextButtonFormat
+             READ nextButtonFormat WRITE setNextButtonFormat)
+
+  /// This property controls the text, icon and tooltip of the goTo/finish
+  /// button.
+  /// "[icon]{stepid|\"Finish\"}" by default.
+  /// \sa goToButtonsFormat(), setGoToButtonsFormat(),
+  /// ctkWorkflow::formatButton(), backButtonFormat, nextButtonFormat
+  Q_PROPERTY(QString goToButtonsFormat
+             READ goToButtonsFormat WRITE setGoToButtonsFormat)
+
+  /// This property controls whether the goTo buttons are visible or hidden.
+  /// False (visible) by default.
+  /// \sa hideInvalidButtons
+  Q_PROPERTY(bool hideGoToButtons READ hideGoToButtons WRITE setHideGoToButtons)
+
+  /// This property controls whether the back, next or goTo buttons are hidden when disabled.
+  /// Note that buttons can also be hidden via ctkWorkflowWidgetStep::buttonHints.
+  /// \sa ctkWofklowWidgetStep::buttonBoxHints
   Q_PROPERTY(bool hideInvalidButtons READ hideInvalidButtons WRITE setHideInvalidButtons)
 
 public:
@@ -71,33 +96,50 @@ public:
   void setWorkflow(ctkWorkflow * newWorkflow);
 
   /// Get the 'back' button
-  QPushButton* backButton()const;
+  Q_INVOKABLE ctkPushButton* backButton()const;
 
-  /// Get 'back' button default text
-  QString backButtonDefaultText()const;
+  /// Return the backButtonFormat property value.
+  /// \sa backButtonFormat, setBackButtonFormat()
+  QString backButtonFormat()const;
 
-  /// \brief Set 'back' button \a defaultText
-  /// \a defaultText is used if the text associated with the current step is empty
-  void setBackButtonDefaultText(const QString& defaultText);
+  /// Set the backButtonFormat property value.
+  /// \sa backButtonFormat, backButtonFormat()
+  void setBackButtonFormat(const QString& format);
 
   /// Get the 'next' button
-  QPushButton* nextButton()const;
+  Q_INVOKABLE ctkPushButton* nextButton()const;
+
+  /// Return the nextButtonFormat property value.
+  /// \sa nextButtonFormat, setNextButtonFormat()
+  QString nextButtonFormat()const;
 
-  /// Get 'next' button default text
-  QString nextButtonDefaultText()const;
+  /// Set the nextButtonFormat property value.
+  /// \sa nextButtonFormat, nextButtonFormat()
+  void setNextButtonFormat(const QString& format);
 
-  /// \brief Set 'next' button \a defaultText
-  /// \a defaultText is used if the text associated with the current step is empty
-  void setNextButtonDefaultText(const QString& defaultText);
+  /// Return the goToButtonsFormat property value.
+  /// \sa goToButtonsFormat, setGoToButtonsFormat()
+  QString goToButtonsFormat()const;
+
+  /// Set the goToButtonsFormat property value.
+  /// \sa goToButtonsFormat, goToButtonsFormat()
+  void setGoToButtonsFormat(const QString& format);
 
   /// Get a list of the 'goTo' buttons
-  QList<QPushButton*> goToButtons()const;
+  QList<ctkPushButton*> goToButtons()const;
 
   /// Sets the direction of the QBoxLayout that manages this widget (default is
   /// QBoxLayout::LeftToRight)
   QBoxLayout::Direction direction()const;
   void setDirection(const QBoxLayout::Direction& newDirection);
 
+  /// Return the hideGoToButtons property value.
+  /// \sa hideGoToButtons
+  bool hideGoToButtons()const;
+  /// Set the hideGoToButtons property value.
+  /// \sa hideGoToButtons
+  void setHideGoToButtons(bool hide);
+
   /// If true, invalid buttons are hidden.  If false, invalid buttons are shown but disabled.
   /// Default is false.
   bool hideInvalidButtons()const;

+ 66 - 16
Libs/Widgets/ctkWorkflowGroupBox.cpp

@@ -26,6 +26,7 @@
 
 // CTK includes
 #include "ctkWorkflowGroupBox.h"
+#include "ctkWorkflowWidget.h"
 #include "ctkWorkflowWidgetStep.h"
 #include "ctkFittedTextBrowser.h"
 #include "ui_ctkWorkflowGroupBox.h"
@@ -37,12 +38,15 @@ static ctkLogger logger("org.commontk.libs.widgets.ctkWorkflowGroupBox");
 
 //-----------------------------------------------------------------------------
 class ctkWorkflowGroupBoxPrivate: public Ui_ctkWorkflowGroupBox
-                                  
 {
 public:
   ctkWorkflowGroupBoxPrivate();
   ~ctkWorkflowGroupBoxPrivate();
 
+  QString TitleFormat;
+  QString SubTitleFormat;
+  QString ErrorTextFormat;
+
   bool HideWidgetsOfNonCurrentSteps;
   bool ErrorTextEnabled;
 
@@ -58,6 +62,10 @@ public:
 //---------------------------------------------------------------------------
 ctkWorkflowGroupBoxPrivate::ctkWorkflowGroupBoxPrivate()
 {
+  this->TitleFormat = "{current:name}";
+  this->SubTitleFormat = "{current:description}";
+  this->ErrorTextFormat = "{current:statusText}";
+
   this->HideWidgetsOfNonCurrentSteps = false;
 
   this->ErrorTextEnabled = true;
@@ -105,14 +113,19 @@ void ctkWorkflowGroupBox::updateGroupBox(ctkWorkflowStep* currentStep)
 {
   Q_D(ctkWorkflowGroupBox);
 
-  d->StepShownPreviously = d->StepShownCurrently;
+  d->StepShownPreviously = (currentStep != d->StepShownCurrently ?
+                            d->StepShownCurrently : d->StepShownPreviously);
   d->StepShownCurrently = currentStep;
 
-  if (currentStep)
-    { 
-    this->setTitle(currentStep->name());
-    this->setSubTitle(currentStep->description());
-    this->setErrorText(currentStep->statusText());
+  ctkWorkflowWidgetStep* currentWidgetStep = dynamic_cast<ctkWorkflowWidgetStep*>(currentStep);
+
+  if (currentWidgetStep)
+    {
+    ctkWorkflowWidget::formatButton(d->CollapsibleButton, d->TitleFormat, currentWidgetStep);
+    QString subTitleText = ctkWorkflowWidget::formatText(d->SubTitleFormat, currentWidgetStep);
+    this->setSubTitle(subTitleText);
+    QString errorText = ctkWorkflowWidget::formatText(d->ErrorTextFormat, currentWidgetStep);
+    this->setErrorText(errorText);
 
     // don't show textual elements if they are empty
     d->SubTitleTextBrowser->setVisible(!this->subTitle().isEmpty());
@@ -134,8 +147,7 @@ void ctkWorkflowGroupBox::updateGroupBox(ctkWorkflowStep* currentStep)
         }
       }
     }
-  
-  ctkWorkflowWidgetStep* currentWidgetStep = dynamic_cast<ctkWorkflowWidgetStep*>(currentStep);
+
   // show/enable the current step
   if (currentWidgetStep)
     {
@@ -164,13 +176,6 @@ QString ctkWorkflowGroupBox::title()const
 }
 
 // --------------------------------------------------------------------------
-void ctkWorkflowGroupBox::setTitle(const QString& newTitleText)
-{
-  Q_D(ctkWorkflowGroupBox);
-  d->CollapsibleButton->setText(newTitleText);
-}
-
-// --------------------------------------------------------------------------
 QString ctkWorkflowGroupBox::subTitle()const
 {
   Q_D(const ctkWorkflowGroupBox);
@@ -225,3 +230,48 @@ void ctkWorkflowGroupBox::setErrorText(const QString& newErrorText)
   Q_D(ctkWorkflowGroupBox);
   d->ErrorTextBrowser->setPlainText(newErrorText);
 }
+
+// --------------------------------------------------------------------------
+QString ctkWorkflowGroupBox::titleFormat()const
+{
+  Q_D(const ctkWorkflowGroupBox);
+  return d->TitleFormat;
+}
+
+// --------------------------------------------------------------------------
+void ctkWorkflowGroupBox::setTitleFormat(const QString& format)
+{
+  Q_D(ctkWorkflowGroupBox);
+  d->TitleFormat = format;
+  this->updateGroupBox(d->StepShownCurrently);
+}
+
+// --------------------------------------------------------------------------
+QString ctkWorkflowGroupBox::subTitleFormat()const
+{
+  Q_D(const ctkWorkflowGroupBox);
+  return d->SubTitleFormat;
+}
+
+// --------------------------------------------------------------------------
+void ctkWorkflowGroupBox::setSubTitleFormat(const QString& format)
+{
+  Q_D(ctkWorkflowGroupBox);
+  d->SubTitleFormat = format;
+  this->updateGroupBox(d->StepShownCurrently);
+}
+
+// --------------------------------------------------------------------------
+QString ctkWorkflowGroupBox::errorTextFormat()const
+{
+  Q_D(const ctkWorkflowGroupBox);
+  return d->ErrorTextFormat;
+}
+
+// --------------------------------------------------------------------------
+void ctkWorkflowGroupBox::setErrorTextFormat(const QString& format)
+{
+  Q_D(ctkWorkflowGroupBox);
+  d->ErrorTextFormat = format;
+  this->updateGroupBox(d->StepShownCurrently);
+}

+ 38 - 1
Libs/Widgets/ctkWorkflowGroupBox.h

@@ -43,6 +43,20 @@ class CTK_WIDGETS_EXPORT ctkWorkflowGroupBox : public QWidget
   Q_OBJECT
   Q_PROPERTY(QString preText READ preText WRITE setPreText)
   Q_PROPERTY(QString postText READ postText WRITE setPostText)
+  /// This property controls the text, icon and tooltip of the title button.
+  /// "{current:name}" by default.
+  /// \sa titleFormat(), setTitleFormat(), ctkWorkflow::formatButton()
+  Q_PROPERTY(QString titleFormat READ titleFormat WRITE setTitleFormat)
+  /// This property controls the text of the subtitle view.
+  /// "{current:description}" by default.
+  /// \sa subTitleFormat(), setSubTitleFormat(), ctkWorkflow::formatButton(),
+  /// titleFormat, errorTextFormat
+  Q_PROPERTY(QString subTitleFormat READ subTitleFormat WRITE setSubTitleFormat)
+  /// This property controls the textof the error view.
+  /// "{current:statusText}" by default.
+  /// \sa errorTextFormat(), setErrorTextFormat(), ctkWorkflow::formatButton(),
+  /// titleFormat, subTitleFormat
+  Q_PROPERTY(QString errorTextFormat READ errorTextFormat WRITE setErrorTextFormat)
   Q_PROPERTY(bool hideWidgetsOfNonCurrentSteps READ hideWidgetsOfNonCurrentSteps WRITE setHideWidgetsOfNonCurrentSteps)
   Q_PROPERTY(bool errorTextEnabled READ errorTextEnabled WRITE setErrorTextEnabled)
 
@@ -85,6 +99,30 @@ public:
   /// \brief Get the layout onto which step specific widgets are placed.
   QLayout* clientAreaLayout()const;
 
+  /// Return the titleFormat property value.
+  /// \sa titleFormat, setTitleFormat()
+  QString titleFormat()const;
+
+  /// Set the titleFormat property value.
+  /// \sa titleFormat, titleFormat()
+  void setTitleFormat(const QString& format);
+
+  /// Return the subTitleFormat property value.
+  /// \sa subTitleFormat, setSubTitleFormat()
+  QString subTitleFormat()const;
+
+  /// Set the subTitleFormat property value.
+  /// \sa subTitleFormat, subTitleFormat()
+  void setSubTitleFormat(const QString& format);
+
+  /// Return the errorTextFormat property value.
+  /// \sa errorTextFormat, setErrorTextFormat()
+  QString errorTextFormat()const;
+
+  /// Set the errorTextFormat property value.
+  /// \sa errorTextFormat, errorTextFormat()
+  void setErrorTextFormat(const QString& format);
+
   ///
   /// If hideWidgetsOfNonCurrentSteps is turned on, then a step's
   /// widgets will be hidden when that step is not the current step.
@@ -107,7 +145,6 @@ public Q_SLOTS:
   virtual void updateGroupBox(ctkWorkflowStep* currentStep);
 
 protected:
-  virtual void setTitle(const QString& newTitle);
   virtual void setSubTitle(const QString& newSubTitle);
   virtual void setErrorText(const QString& newErrorText);
 

+ 203 - 1
Libs/Widgets/ctkWorkflowWidget.cpp

@@ -19,11 +19,13 @@
 =========================================================================*/
 
 // Qt includes
+#include <QApplication>
 #include <QDebug>
-#include <QWidget>
+#include <QStyle>
 #include <QWeakPointer>
 
 // CTK includes
+#include "ctkPushButton.h"
 #include "ctkWorkflowWidget.h"
 #include "ctkWorkflowStep.h"
 #include "ctkWorkflowWidgetStep.h"
@@ -149,6 +151,14 @@ void ctkWorkflowWidget::setWorkflow(ctkWorkflow* newWorkflow)
 }
 
 // --------------------------------------------------------------------------
+ctkWorkflowWidgetStep* ctkWorkflowWidget::widgetStep(const QString& id)const
+{
+  Q_D(const ctkWorkflowWidget);
+  return dynamic_cast<ctkWorkflowWidgetStep*>(
+    !d->Workflow.isNull() ? d->Workflow.data()->step(id) : 0);
+}
+
+// --------------------------------------------------------------------------
 void ctkWorkflowWidget::onCurrentStepChanged(ctkWorkflowStep* currentStep)
 {
   if (currentStep)
@@ -207,3 +217,195 @@ void ctkWorkflowWidget::updateButtonBoxUI(ctkWorkflowStep* currentStep)
     d->ButtonBoxWidget->updateButtons(currentStep);
     }
 }
+
+//-----------------------------------------------------------------------------
+QVariant ctkWorkflowWidget::buttonItem(QString item,
+                                       ctkWorkflowWidgetStep* step)
+{
+  QRegExp backRegExp("^[\\{\\(\\[]back:(.*)[\\}\\)\\]]$");
+  QRegExp nextRegExp("^[\\{\\(\\[]next:(.*)[\\}\\)\\]]$");
+  QRegExp currentRegExp("^[\\{\\(\\[]current:(.*)[\\}\\)\\]]$");
+  if (backRegExp.exactMatch(item))
+    {
+    QList<ctkWorkflowStep*> backs =
+      (step ? step->workflow()->backwardSteps(step) : QList<ctkWorkflowStep*>());
+    step = (backs.size() ? dynamic_cast<ctkWorkflowWidgetStep*>(backs[0]) : 0);
+    item.remove("back:");
+    return ctkWorkflowWidget::buttonItem(item, step);
+    }
+  else if (nextRegExp.exactMatch(item))
+    {
+    QList<ctkWorkflowStep*> nexts =
+      step ? step->workflow()->forwardSteps(step) : QList<ctkWorkflowStep*>();
+    step = (nexts.size() ? dynamic_cast<ctkWorkflowWidgetStep*>(nexts[0]) : 0);
+    item.remove("next:");
+    return ctkWorkflowWidget::buttonItem(item, step);
+    }
+  else if (currentRegExp.exactMatch(item))
+    {
+    item.remove("current:");
+    }
+  QVariant res;
+  QRegExp quotesRegExp("^\"(.*)\"$");
+  QRegExp propsRegExp("^[\\{\\(\\[](.*)[\\}\\)\\]]$");
+  QStyle* style = (step ? step->style() : qApp->style());
+  if (item == "[<-]")
+    {
+    res.setValue(style->standardIcon(QStyle::SP_ArrowLeft));
+    }
+  else if (item == "[->]")
+    {
+    res.setValue(style->standardIcon(QStyle::SP_ArrowRight));
+    }
+  else if (item == "{#}" || item == "(#)")
+    {
+    res = QVariant(step ? step->workflow()->backwardDistanceToStep(step) + 1 : 0);
+    }
+  else if (item == "{!#}" || item == "(!#)")
+    {
+    res = QVariant(step ? step->workflow()->steps().count() : 0);
+    }
+  else if (quotesRegExp.exactMatch(item))
+    {
+    res = quotesRegExp.cap(1);
+    }
+  else if (propsRegExp.exactMatch(item))
+    {
+    item = propsRegExp.cap(1);
+    if (quotesRegExp.exactMatch(item))
+      {
+      res = quotesRegExp.cap(1);
+      }
+    else
+      {
+      res = step ? step->property(item.toLatin1()) : QVariant();
+      if (res.isValid() && res.type() == QVariant::String && res.toString().isEmpty())
+        {
+        res = QVariant();
+        }
+      }
+    }
+  else
+    {
+    qWarning() << "Item" << item << "not supported";
+    }
+  return res;
+}
+
+//-----------------------------------------------------------------------------
+void ctkWorkflowWidget
+::formatButton(QAbstractButton* button, const QString& buttonFormat,
+               ctkWorkflowWidgetStep* step)
+{
+  QMap<QString, QVariant> formats =
+    ctkWorkflowWidget::parse(buttonFormat, step);
+  button->setIcon(formats["icon"].value<QIcon>());
+  if (qobject_cast<ctkPushButton*>(button))
+    {
+    qobject_cast<ctkPushButton*>(button)->setIconAlignment(
+      static_cast<Qt::Alignment>(formats["iconalignment"].toInt()));
+    }
+  button->setText(formats["text"].toString());
+  button->setToolTip(formats["tooltip"].toString());
+}
+
+//-----------------------------------------------------------------------------
+QString ctkWorkflowWidget
+::formatText(const QString& textFormat, ctkWorkflowWidgetStep* step)
+{
+  QMap<QString, QVariant> formats =
+    ctkWorkflowWidget::parse(textFormat, step);
+  return formats["text"].toString();
+}
+
+//-----------------------------------------------------------------------------
+QMap<QString, QVariant> ctkWorkflowWidget
+::parse(const QString& format, ctkWorkflowWidgetStep* step)
+{
+  QIcon buttonIcon;
+  Qt::Alignment buttonIconAlignment = Qt::AlignLeft | Qt::AlignVCenter;
+  QString buttonText;
+  QString buttonToolTip;
+
+  QString textRegExp("\\{[^{}]+\\}");
+  QString simpleTextRegExp("\"[^\"]+\"");
+  QString toolTipRegExp("\\([^\\(\\)]+\\)");
+  QString iconRegExp("\\[[^\\[\\]]+\\]");
+
+  //QRegExp splitBrackets("\\{([^}]+)\\}");
+  //QRegExp splitBrackets("(\\{[^{}]+\\}|\\([^\\(\\)]+\\)|\"[^\"]+\")");
+  QRegExp splitBrackets(QString("(%1|%2|%3|%4)")
+                        .arg(textRegExp).arg(simpleTextRegExp)
+                        .arg(toolTipRegExp)
+                        .arg(iconRegExp));
+  QStringList brackets;
+  int pos = 0;
+  while ((pos = splitBrackets.indexIn(format, pos)) != -1)
+    {
+    brackets << splitBrackets.cap(1);
+    pos += splitBrackets.matchedLength();
+    }
+
+  foreach(const QString& withBracket, brackets)
+    {
+    bool isSimpleText =
+      QRegExp(QString("^") + simpleTextRegExp + QString("$")).exactMatch(withBracket);
+
+    QString withoutBracket = withBracket.mid(1, withBracket.size() - 2);
+    // If the item is empty, then check the next item. For example:
+    // {next:description|next:name|next:stepid}: if 'next:description' is empty, then
+    // use 'next:name' if not empty, otherwise 'next:stepid'
+    // Don't split simple text, it is never empty.
+    QStringList tokens = isSimpleText ? QStringList(withoutBracket) : withoutBracket.split('|');
+
+    QIcon icon;
+    Qt::Alignment iconAlignment = buttonIconAlignment;
+    QString text;
+    foreach (const QString& token, tokens)
+      {
+      QString tokenWithBracket = withBracket[0] + token + withBracket[withBracket.size()-1];
+      QVariant item = ctkWorkflowWidget::buttonItem(tokenWithBracket, step);
+      if (item.isValid())
+        {
+        switch (item.type())
+          {
+          case QVariant::Icon:
+            icon = item.value<QIcon>();
+            if (!buttonText.isEmpty())
+              {
+              iconAlignment = Qt::AlignRight | Qt::AlignVCenter;
+              }
+            break;
+          case QVariant::String:
+          case QVariant::Int:
+            text += item.toString();
+            break;
+          default:
+            break;
+          }
+        // skip the other cases if the item was valid, otherwise keep on searching
+        break;
+        }
+      }
+    if (QRegExp(QString("^") + textRegExp + QString("$")).exactMatch(withBracket) ||
+        isSimpleText)
+      {
+      buttonText += text;
+      }
+    else if (QRegExp(QString("^") + iconRegExp + QString("$")).exactMatch(withBracket))
+      {
+      buttonIcon = icon;
+      buttonIconAlignment = iconAlignment;
+      }
+    else if (QRegExp(QString("^") + toolTipRegExp + QString("$")).exactMatch(withBracket))
+      {
+      buttonToolTip = text;
+      }
+    }
+  QMap<QString, QVariant> formats;
+  formats["icon"] = buttonIcon;
+  formats["iconalignment"] = static_cast<int>(buttonIconAlignment);
+  formats["text"] = buttonText;
+  formats["tooltip"] = buttonToolTip;
+  return formats;
+}

+ 40 - 3
Libs/Widgets/ctkWorkflowWidget.h

@@ -23,9 +23,8 @@
 
 // Qt includes
 #include <QWidget>
-class QPushButton;
-class QGroupBox;
-#include <QBoxLayout>
+#include <QVariant>
+class QAbstractButton;
 
 // CTK includes
 #include "ctkPimpl.h"
@@ -34,6 +33,7 @@ class ctkWorkflow;
 class ctkWorkflowStep;
 class ctkWorkflowButtonBoxWidget;
 class ctkWorkflowGroupBox;
+class ctkWorkflowWidgetStep;
 
 class ctkWorkflowWidgetPrivate;
 
@@ -55,6 +55,8 @@ public:
   Q_INVOKABLE virtual ctkWorkflow* workflow()const;
   Q_INVOKABLE virtual void setWorkflow(ctkWorkflow* newWorkflow);
 
+  Q_INVOKABLE ctkWorkflowWidgetStep* widgetStep(const QString& id)const;
+
   /// Get the widget constaining the title, subtitle, pre-text, post-text, error-text and client area
   /// layout.
   Q_INVOKABLE virtual ctkWorkflowGroupBox* workflowGroupBox()const;
@@ -66,6 +68,34 @@ public:
   /// Get the widget with the 'next', 'back' and 'goTo' buttons
   Q_INVOKABLE ctkWorkflowButtonBoxWidget* buttonBoxWidget()const;
 
+  /// Apply the text, icon and tooltip format to the button.
+  ///  * {PROP}, [prop] or (PROP): value of the PROP property (e.g. stepid,
+  /// name, description...) used as button text, icon or tooltip respectively.
+  /// PROP can be prefixed by 'back:', 'next:' or 'current:',
+  /// the property will then be the one of the previous, next or current step.
+  ///  * [<-]: Back arrow icon. If it is the first item, the icon is to the left
+  /// of the button text.
+  ///  * [->]: Next arrow icon. If it is the last item, the icon is to the right
+  ///  of the button text if the button is a ctkPushButton.
+  ///  * {#} or (#): 1-based index of the step (int)
+  ///  * {!#} or {!#} : Total number of steps (int)
+  ///  * "ABCD": text for the button
+  ///  * {PROP|"ABCD"}: Use ABCD as fallback if PROP is not a valid property or
+  /// if the text is empty.
+  ///
+  /// Examples:
+  ///   "{next:#}"/"{!#}") "{next:name}(next:description)[->]" will format the button with:
+  ///  * text="3/3) Compute Algorithm" if the next step is the last step of a
+  /// 3-step-workflow, and its name is "Compute Algorithm".
+  ///  * icon=QStyle::SP_ArrowRight
+  ///  * tooltip="This step computes the algorithm" if the next step description
+  /// is "This step...".
+  /// \sa parse(), formatText()
+  static void formatButton(QAbstractButton* button, const QString& format, ctkWorkflowWidgetStep* step);
+  /// Return the text contained in \a format.
+  /// \sa parse(), formatButton()
+  static QString formatText(const QString& format, ctkWorkflowWidgetStep* step);
+
 public Q_SLOTS:
   /// Triggers updates of the the workflowGroupBox and the buttonBoxWidget when the current workflow
   /// step has changed.
@@ -82,6 +112,13 @@ protected:
   // Triggers updates of the buttonBoxWidget when the current workflow step has changed.
   void updateButtonBoxUI(ctkWorkflowStep* currentStep);
 
+  /// Return the value of the formatItem.
+  /// \sa format()
+  static QVariant buttonItem(QString formatItem, ctkWorkflowWidgetStep* step);
+  /// Return a dictionary of formats. Keys can be 'text', 'icon', 'iconalignment' or 'tooltip'.
+  /// \sa buttonItem(), formatButton(), formatText()
+  static QMap<QString, QVariant> parse(const QString& format, ctkWorkflowWidgetStep* step);
+
 protected:
   QScopedPointer<ctkWorkflowWidgetPrivate> d_ptr;
 

+ 3 - 0
Libs/Widgets/ctkWorkflowWidgetStep.h

@@ -60,7 +60,10 @@ class CTK_WIDGETS_EXPORT ctkWorkflowWidgetStep : public QWidget, public ctkWorkf
 {
   Q_OBJECT
   Q_PROPERTY(QString stepid READ id WRITE setId)
+  Q_PROPERTY(QString name READ name WRITE setName)
+  Q_PROPERTY(QString description READ description WRITE setDescription)
   Q_PROPERTY(QIcon icon READ icon WRITE setIcon)
+  Q_PROPERTY(QString statusText READ statusText)
   Q_PROPERTY(QString backButtonText READ backButtonText WRITE setBackButtonText)
   Q_PROPERTY(QString nextButtonText READ nextButtonText WRITE setNextButtonText)
   Q_FLAGS(ButtonBoxHint ButtonBoxHints)