Kaynağa Gözat

Add ctkWorkflowButtonBoxWidget::[back|next|goTo]ButtonFormat

Give the option to dynamically configure the text, icon and tooltip
of the back, next and finish buttons.

Expose ctkWorkflowWidgetStep::name, description, statusText
into qt meta-object system to be accessible from QObject::property()

Co-authored-by: Johan Andruejol <johan.andruejol@kitware.com>
Julien Finet 12 yıl önce
ebeveyn
işleme
9aa389d2bf

+ 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();
 

+ 45 - 0
Libs/Core/ctkWorkflow.cpp

@@ -23,6 +23,8 @@
 #include <QStateMachine>
 #include <QState>
 
+#include <QQueue>
+
 // CTK includes
 #include "ctkWorkflow.h"
 #include "ctkWorkflowStep.h"
@@ -1121,3 +1123,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;
+}

+ 8 - 0
Libs/Core/ctkWorkflow.h

@@ -160,6 +160,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

+ 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"

+ 45 - 44
Libs/Widgets/ctkWorkflowButtonBoxWidget.cpp

@@ -19,17 +19,18 @@
   =========================================================================*/
 
 // Qt includes
-#include <QWidget>
-#include <QList>
 #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,14 +52,15 @@ 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)
   ctkPushButton*      BackButton;
-  QString             BackButtonDefaultText;
+  QString             BackButtonFormat;
   ctkPushButton*      NextButton;
-  QString             NextButtonDefaultText;
+  QString             NextButtonFormat;
   ButtonToStepMapType GoToButtonToStepMap;
+  QString             GoToButtonsFormat;
 
   // Direction for layout (for use with QBoxLayout only)
   QBoxLayout::Direction Direction;
@@ -82,9 +84,10 @@ 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;
@@ -104,12 +107,12 @@ void ctkWorkflowButtonBoxWidgetPrivate::setupUi(QWidget * newParent)
   QIcon nextIcon = newParent->style()->standardIcon(QStyle::SP_ArrowRight);
 
   // Setup the buttons
-  this->BackButton = new ctkPushButton(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 ctkPushButton(nextIcon, this->NextButtonDefaultText, newParent);
+  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);
@@ -127,13 +130,8 @@ 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);
 
   // Disable the back button if we can't go backward
   bool enable = currentStep && this->Workflow->canGoBackward(currentStep);
@@ -166,14 +164,8 @@ 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);
 
   // Disable the next button if we can't go backward
   bool enable = currentStep && this->Workflow->canGoForward(currentStep);
@@ -210,7 +202,7 @@ void ctkWorkflowButtonBoxWidgetPrivate::updateGoToButtons(ctkWorkflowStep* curre
   // Remove the buttons if the set of steps to have goTo buttons has changed
   if (goToStepsThatHaveButtons != goToStepsToHaveButtons)
     {
-    foreach (QPushButton* goToButton, this->GoToButtonToStepMap.keys())
+    foreach (ctkPushButton* goToButton, this->GoToButtonToStepMap.keys())
       {
       q->layout()->removeWidget(goToButton);
       goToButton->deleteLater();
@@ -225,22 +217,19 @@ 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
   ctkWorkflowWidgetStep* step = dynamic_cast<ctkWorkflowWidgetStep*>(currentStep);
-  foreach (QPushButton* goToButton, this->GoToButtonToStepMap.keys())
+  foreach (ctkPushButton* goToButton, this->GoToButtonToStepMap.keys())
     {
     // TODO enable and show the goTo button if we can go to it
     // ctkWorkflowStep* goToStep = this->GoToButtonToStepMap[goToButton];
@@ -319,32 +308,44 @@ 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;
+  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;
+  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);
@@ -353,14 +354,14 @@ CTK_SET_CPP(ctkWorkflowButtonBoxWidget, bool, setHideInvalidButtons, HideInvalid
 
 //-----------------------------------------------------------------------------
 // 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();
@@ -388,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))
       {

+ 47 - 28
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;
 
@@ -52,17 +51,27 @@ class CTK_WIDGETS_EXPORT ctkWorkflowButtonBoxWidget : public QWidget
 {
   Q_OBJECT
 
-  /// This property controls the text of the back button when the step text is empty.
-  /// "Back" by default.
-  /// \sa nextButtonDefaultText, goToButtonDefaultText
-  Q_PROPERTY(QString backButtonDefaultText
-             READ backButtonDefaultText WRITE setBackButtonDefaultText)
-
-  /// This property controls the text of the next button when the step text is empty.
-  /// "Next" by default.
-  /// \sa backButtonDefaultText, goToButtonDefaultText
-  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.
@@ -87,27 +96,37 @@ 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;
+
+  /// Set the nextButtonFormat property value.
+  /// \sa nextButtonFormat, nextButtonFormat()
+  void setNextButtonFormat(const QString& format);
 
-  /// Get 'next' button default text
-  QString nextButtonDefaultText()const;
+  /// Return the goToButtonsFormat property value.
+  /// \sa goToButtonsFormat, setGoToButtonsFormat()
+  QString goToButtonsFormat()const;
 
-  /// \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);
+  /// 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)

+ 195 - 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"
@@ -207,3 +209,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;
+}

+ 38 - 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;
 
@@ -66,6 +66,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 +110,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)