Browse Source

Merge pull request #703 from lassoan/collapsible-fitted-text-browser

ENH: Made ctkFittedTextBrowser collapsible
Jean-Christophe Fillion-Robin 8 years ago
parent
commit
29b3ad8206

+ 39 - 0
Libs/Widgets/Testing/Cpp/ctkFittedTextBrowserTest1.cpp

@@ -50,6 +50,45 @@ int ctkFittedTextBrowserTest1(int argc, char * argv [] )
     "</pre>");
   layout->addWidget(&textBrowserWidget);
 
+  ctkFittedTextBrowser textBrowserWidgetCollapsibleText(&widget);
+  textBrowserWidgetCollapsibleText.setCollapsible(true);
+  textBrowserWidgetCollapsibleText.setText(
+    "This is the teaser for auto-text.\n More details are here.\n"
+    "This is a very very, very very very, very very, very very very, very very, very very very long line\n"
+    "Some more lines 1.\n"
+    "Some more lines 2.\n"
+    "Some more, some more.");
+  textBrowserWidgetCollapsibleText.setShowMoreText("&gt;&gt;&gt;");
+  textBrowserWidgetCollapsibleText.setShowLessText("&lt;&lt;&lt;");
+  layout->addWidget(&textBrowserWidgetCollapsibleText);
+
+  ctkFittedTextBrowser textBrowserWidgetCollapsibleHtml(&widget);
+  textBrowserWidgetCollapsibleHtml.setHtml(
+    "This is the teaser for html.<br>"
+    "More details are here."
+    "This is a very very, very very very, very very, very very very, very very, very very very long line\n"
+    "Some more lines 1."
+    "Some more lines 2."
+    "Some more, some more.");
+  textBrowserWidgetCollapsibleHtml.setCollapsible(true);
+  layout->addWidget(&textBrowserWidgetCollapsibleHtml);
+
+  ctkFittedTextBrowser textBrowserWidgetCollapsibleComplexHtml(&widget);
+  textBrowserWidgetCollapsibleComplexHtml.setCollapsible(true);
+  textBrowserWidgetCollapsibleComplexHtml.setHtml(
+    "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\"><html>"
+    "<head><meta name=\"qrichtext\" content=\"1\" /> <style type=\"text/css\"> p, li { white-space: pre-wrap; } </style></head>"
+    "<body style=\" font-family:'MS Shell Dlg 2'; font-size:12.25pt; font-weight:400; font-style:normal;\">"
+    "<p>This is the teaser for complex html.<br></p>"
+    "<p>More details are here.</p>"
+    "<p>This is a very very, very very very, very very, very very very, very very, very very very long line</p>"
+    "<p>Some more lines 1."
+    "Some more lines 2."
+    "Some more, some more.</p>"
+    "</body></html>");
+  layout->addWidget(&textBrowserWidgetCollapsibleComplexHtml);
+  textBrowserWidgetCollapsibleHtml.setCollapsed(false);
+
   QPushButton expandingButton(&widget);
   QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
   sizePolicy.setHorizontalStretch(1);

+ 291 - 4
Libs/Widgets/ctkFittedTextBrowser.cpp

@@ -26,14 +26,132 @@
 
 // CTK includes
 #include "ctkFittedTextBrowser.h"
+#include "ctkFittedTextBrowser_p.h"
+
+static const char moreAnchor[] = "more";
+static const char lessAnchor[] = "less";
+
+//-----------------------------------------------------------------------------
+ctkFittedTextBrowserPrivate::ctkFittedTextBrowserPrivate(ctkFittedTextBrowser& object)
+  :q_ptr(&object)
+{
+  this->Collapsible = false;
+  this->Collapsed = true;
+  this->FullTextSetter = ctkFittedTextBrowserPrivate::Text;
+  this->ShowMoreText = object.tr("More...");
+  this->ShowLessText = object.tr("Hide details.");
+  QString ShowLessText;
+}
+
+//-----------------------------------------------------------------------------
+ctkFittedTextBrowserPrivate::~ctkFittedTextBrowserPrivate()
+{
+}
+
+//-----------------------------------------------------------------------------
+QString ctkFittedTextBrowserPrivate::collapsibleText()
+{
+  Q_Q(ctkFittedTextBrowser);
+  bool html = (this->FullTextSetter == ctkFittedTextBrowserPrivate::Html || this->FullText.indexOf("<html>") >= 0);
+  if (html)
+  {
+    return this->collapsibleHtml();
+  }
+  else
+  {
+    return this->collapsiblePlainText();
+  }
+}
+
+//-----------------------------------------------------------------------------
+QString ctkFittedTextBrowserPrivate::collapseLinkText()
+{
+  Q_Q(ctkFittedTextBrowser);
+  if (this->Collapsed)
+  {
+    return QString(" <a href=\"#") + moreAnchor + "\">" + this->ShowMoreText + "</a>";
+  }
+  else
+  {
+    return QString(" <a href=\"#") + lessAnchor + "\">" + this->ShowLessText + "</a>";
+  }
+}
+
+//-----------------------------------------------------------------------------
+QString ctkFittedTextBrowserPrivate::collapsiblePlainText()
+{
+  Q_Q(ctkFittedTextBrowser);
+  int teaserEndPosition = this->FullText.indexOf("\n");
+  if (teaserEndPosition < 0)
+  {
+    return this->FullText;
+  }
+  QString finalText;
+  finalText.append("<html>");
+  finalText.append(this->Collapsed ? this->FullText.left(teaserEndPosition) : this->FullText);
+  finalText.append(this->collapseLinkText());
+  finalText.append("</html>");
+  // Remove line break to allow continuation of line.
+  finalText.replace(finalText.indexOf('\n'), 1, ' ');
+  // In plain text line breaks were indicated by newline, but we now use html,
+  // so line breaks must use <br>
+  finalText.replace("\n", "<br>");
+  return finalText;
+}
+
+//-----------------------------------------------------------------------------
+QString ctkFittedTextBrowserPrivate::collapsibleHtml()
+{
+  Q_Q(ctkFittedTextBrowser);
+  const QString lineBreak("<br>");
+  int teaserEndPosition = this->FullText.indexOf(lineBreak);
+  if (teaserEndPosition < 0)
+  {
+    return this->FullText;
+  }
+
+  QString finalText = this->FullText;
+  if (this->Collapsed)
+  {
+    finalText = finalText.left(teaserEndPosition) + this->collapseLinkText();
+    // By truncating the full text we might have deleted the closing </html> tag
+    // restore it now.
+    if (finalText.contains("<html") && !finalText.contains("</html"))
+    {
+      finalText.append("</html>");
+    }
+  }
+  else
+  {
+    // Remove <br> to allow continuation of line and avoid extra space
+    // when <p> element is used as well.
+    finalText.replace(finalText.indexOf(lineBreak), lineBreak.size(), " ");
+    // Add link text before closing </body> or </html> tag
+    if (finalText.contains("</body>"))
+    {
+      finalText.replace("</body>", this->collapseLinkText() + "</body>");
+    }
+    else if (finalText.contains("</html>"))
+    {
+      finalText.replace("</html>", this->collapseLinkText() + "</html>");
+    }
+    else
+    {
+      finalText.append(this->collapseLinkText());
+    }
+  }
+  return finalText;
+}
 
 //-----------------------------------------------------------------------------
 ctkFittedTextBrowser::ctkFittedTextBrowser(QWidget* _parent)
   : QTextBrowser(_parent)
+  , d_ptr(new ctkFittedTextBrowserPrivate(*this))
 {
   this->connect(this, SIGNAL(textChanged()), SLOT(heightForWidthMayHaveChanged()));
   QSizePolicy newSizePolicy = QSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
   this->setSizePolicy(newSizePolicy);
+  this->connect(this, SIGNAL(anchorClicked(QUrl)), SLOT(anchorClicked(QUrl)));
 }
 
 //-----------------------------------------------------------------------------
@@ -53,8 +171,8 @@ int ctkFittedTextBrowser::heightForWidth(int _width) const
 {
   QTextDocument* doc = this->document();
   qreal savedWidth = doc->textWidth();
-  
-  // Fudge factor. This is the difference between the frame and the 
+
+  // Fudge factor. This is the difference between the frame and the
   // viewport.
   int fudge = 2 * this->frameWidth();
 
@@ -68,12 +186,12 @@ int ctkFittedTextBrowser::heightForWidth(int _width) const
   doc->setTextWidth(_width - fudge);
   int noScrollbarHeight =
     doc->documentLayout()->documentSize().height() + fudge;
-  
+
   // (If noScrollbarHeight is greater than the maximum height we'll be
   // allowed, then there will be scrollbars, and the actual required
   // height will be even higher. But since in this case we've already
   // hit the maximum height, it doesn't matter that we underestimate.)
-  
+
   // Get minimum height (even if string is empty): one line of text
   int _minimumHeight = QFontMetrics(doc->defaultFont()).lineSpacing() + fudge;
   int ret = qMax(noScrollbarHeight, _minimumHeight) + horizontalScrollbarHeight;
@@ -108,3 +226,172 @@ void ctkFittedTextBrowser::resizeEvent(QResizeEvent* e)
     this->heightForWidthMayHaveChanged();
     }
 }
+
+//-----------------------------------------------------------------------------
+void ctkFittedTextBrowser::setText(const QString &text)
+{
+  Q_D(ctkFittedTextBrowser);
+  d->FullTextSetter = ctkFittedTextBrowserPrivate::Text;
+  if (d->Collapsible)
+    {
+    d->FullText = text;
+    QTextBrowser::setHtml(d->collapsibleText());
+    }
+  else
+    {
+    QTextBrowser::setText(text);
+    }
+}
+
+//-----------------------------------------------------------------------------
+void ctkFittedTextBrowser::setPlainText(const QString &text)
+{
+  Q_D(ctkFittedTextBrowser);
+  d->FullTextSetter = ctkFittedTextBrowserPrivate::PlainText;
+  if (d->Collapsible)
+  {
+    d->FullText = text;
+    QTextBrowser::setHtml(d->collapsibleText());
+  }
+  else
+  {
+    QTextBrowser::setPlainText(text);
+  }
+}
+
+//-----------------------------------------------------------------------------
+void ctkFittedTextBrowser::setHtml(const QString &text)
+{
+  Q_D(ctkFittedTextBrowser);
+  d->FullTextSetter = ctkFittedTextBrowserPrivate::Html;
+  // always save the original text as well because use may make the widget
+  // collapsible at any time
+  d->FullText = text;
+  if (d->Collapsible)
+  {
+    QTextBrowser::setHtml(d->collapsibleText());
+  }
+  else
+  {
+    QTextBrowser::setHtml(text);
+  }
+}
+
+//-----------------------------------------------------------------------------
+void ctkFittedTextBrowser::anchorClicked(const QUrl &url)
+{
+  Q_D(ctkFittedTextBrowser);
+  if (url.path().isEmpty())
+  {
+    if (url.fragment() == moreAnchor)
+    {
+      this->setCollapsed(false);
+    }
+    else if (url.fragment() == lessAnchor)
+    {
+      this->setCollapsed(true);
+    }
+  }
+}
+
+//-----------------------------------------------------------------------------
+void ctkFittedTextBrowser::setCollapsed(bool collapsed)
+{
+  Q_D(ctkFittedTextBrowser);
+  if (d->Collapsed == collapsed)
+  {
+    // no change
+    return;
+  }
+  d->Collapsed = collapsed;
+  if (d->Collapsible)
+  {
+    QTextBrowser::setHtml(d->collapsibleText());
+  }
+}
+
+//-----------------------------------------------------------------------------
+bool ctkFittedTextBrowser::collapsed() const
+{
+  Q_D(const ctkFittedTextBrowser);
+  return d->Collapsed;
+}
+
+//-----------------------------------------------------------------------------
+void ctkFittedTextBrowser::setCollapsible(bool collapsible)
+{
+  Q_D(ctkFittedTextBrowser);
+  if (d->Collapsible == collapsible)
+  {
+    // no change
+    return;
+  }
+  d->Collapsible = collapsible;
+  if (collapsible)
+  {
+    QTextBrowser::setHtml(d->collapsibleText());
+  }
+  else
+  {
+    switch (d->FullTextSetter)
+    {
+    case ctkFittedTextBrowserPrivate::Text: QTextBrowser::setText(d->FullText); break;
+    case ctkFittedTextBrowserPrivate::PlainText: QTextBrowser::setPlainText(d->FullText); break;
+    case ctkFittedTextBrowserPrivate::Html: QTextBrowser::setHtml(d->FullText); break;
+    default: QTextBrowser::setText(d->FullText); break;
+    }
+  }
+}
+
+//-----------------------------------------------------------------------------
+bool ctkFittedTextBrowser::collapsible() const
+{
+  Q_D(const ctkFittedTextBrowser);
+  return d->Collapsible;
+}
+
+//-----------------------------------------------------------------------------
+void ctkFittedTextBrowser::setShowMoreText(const QString &text)
+{
+  Q_D(ctkFittedTextBrowser);
+  if (d->ShowMoreText == text)
+  {
+    // no change
+    return;
+  }
+  d->ShowMoreText = text;
+  if (d->Collapsible)
+  {
+    QTextBrowser::setHtml(d->collapsibleText());
+  }
+}
+
+//-----------------------------------------------------------------------------
+QString ctkFittedTextBrowser::showMoreText() const
+{
+  Q_D(const ctkFittedTextBrowser);
+  return d->ShowMoreText;
+}
+
+//-----------------------------------------------------------------------------
+void ctkFittedTextBrowser::setShowLessText(const QString &text)
+{
+  Q_D(ctkFittedTextBrowser);
+  if (d->ShowLessText == text)
+  {
+    // no change
+    return;
+  }
+  d->ShowLessText = text;
+  if (d->Collapsible)
+  {
+    QTextBrowser::setHtml(d->collapsibleText());
+  }
+}
+
+//-----------------------------------------------------------------------------
+QString ctkFittedTextBrowser::showLessText() const
+{
+  Q_D(const ctkFittedTextBrowser);
+  return d->ShowLessText;
+}

+ 51 - 0
Libs/Widgets/ctkFittedTextBrowser.h

@@ -26,6 +26,7 @@
 
 // CTK includes
 #include "ctkWidgetsExport.h"
+class ctkFittedTextBrowserPrivate;
 
 /// \ingroup Widgets
 /// ctkFittedTextBrowser is a QTextBrowser that adapts its height depending
@@ -34,14 +35,57 @@
 /// sizeHint, minimumSizeHint and heightForWidth. Here sizeHint() and 
 /// minimumSizeHint() are the same as ctkFittedTextBrowser always try to
 /// show the whole contents.
+///
+/// The widget can further optimize use of available space by collapsing
+/// text. If the option is enabled then only a short teaser is shown
+/// and the user has to click on "More..." to see the full text.
 class CTK_WIDGETS_EXPORT ctkFittedTextBrowser : public QTextBrowser
 {
   Q_OBJECT
+  Q_PROPERTY(bool collapsible READ collapsible WRITE setCollapsible)
+  Q_PROPERTY(bool collapsed READ collapsed WRITE setCollapsed)
+  Q_PROPERTY(QString showMoreText READ showMoreText WRITE setShowMoreText)
+  Q_PROPERTY(QString showLessText READ showLessText WRITE setShowLessText)
 
 public:
   ctkFittedTextBrowser(QWidget* parent = 0);
   virtual ~ctkFittedTextBrowser();
 
+  /// Show only first line with "More..." link to save space.
+  /// When the user clicks on the link then the full text is displayed
+  /// (and a "Less..." link).
+  /// The teaser is the beginning of the text up to the first newline character
+  /// (for plain text) or <br> tag (for html). The separator is removed when
+  /// the text is expanded so that the full text can continue on the same line
+  /// as the teaser.
+  void setCollapsible(bool collapsible);
+  /// Show only first line with "More..." link to save space.
+  bool collapsible() const;
+
+  /// Show only first line/the full text.
+  /// Only has effect if collapsible = true.
+  void setCollapsed(bool collapsed);
+  /// Show only first line/the full text.
+  bool collapsed() const;
+
+  void setPlainText(const QString &text);
+#ifndef QT_NO_TEXTHTMLPARSER
+  void setHtml(const QString &text);
+#endif
+  void setText(const QString &text);
+
+  /// Text that is displayed at the end of collapsed text.
+  /// Clicking on the text expands the widget.
+  void setShowMoreText(const QString &text);
+  /// Text that is displayed at the end of collapsed text.
+  QString showMoreText()const;
+
+  /// Text that is displayed at the end of non-collapsed text.
+  /// Clicking on the text collapses the widget.
+  void setShowLessText(const QString &text);
+  /// Text that is displayed at the end of non-collapsed text.
+  QString showLessText()const;
+
   /// Reimplemented for internal reasons
   virtual QSize sizeHint() const;
   /// Reimplemented for internal reasons
@@ -51,9 +95,16 @@ public:
 
 protected Q_SLOTS:
   void heightForWidthMayHaveChanged();
+  void anchorClicked(const QUrl &url);
 
 protected:
+  QScopedPointer<ctkFittedTextBrowserPrivate> d_ptr;
+
   virtual void resizeEvent(QResizeEvent* e);
+
+private:
+  Q_DECLARE_PRIVATE(ctkFittedTextBrowser);
+  Q_DISABLE_COPY(ctkFittedTextBrowser);
 };
 
 #endif

+ 71 - 0
Libs/Widgets/ctkFittedTextBrowser_p.h

@@ -0,0 +1,71 @@
+/*=========================================================================
+
+  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 __ctkFittedTextBrowser_p_h
+#define __ctkFittedTextBrowser_p_h
+
+// CTK includes
+#include "ctkFittedTextBrowser.h"
+
+//-----------------------------------------------------------------------------
+/// \ingroup Widgets
+class CTK_WIDGETS_EXPORT ctkFittedTextBrowserPrivate
+{
+  Q_DECLARE_PUBLIC(ctkFittedTextBrowser);
+
+protected:
+  ctkFittedTextBrowser* const q_ptr;
+
+public:
+  ctkFittedTextBrowserPrivate(ctkFittedTextBrowser& object);
+  virtual ~ctkFittedTextBrowserPrivate();
+
+  // Get collapsed/expanded text in html format.
+  // Calls collapsiblePlainText or collapsibleHtml.
+  QString collapsibleText();
+  // Get collapsed/expanded text in html format from plain text.
+  QString collapsiblePlainText();
+  // Get collapsed/expanded text in html format from html.
+  QString collapsibleHtml();
+
+  // Get more/less link in html format
+  QString collapseLinkText();
+
+  bool Collapsible;
+  bool Collapsed;
+
+  QString ShowMoreText;
+  QString ShowLessText;
+
+  // Stores the text that the user originally set.
+  QString FullText;
+  
+  enum FullTextSetMethod
+  {
+    Text,
+    PlainText,
+    Html
+  };
+
+  // Stores what method the user called to set text
+  FullTextSetMethod FullTextSetter;
+};
+
+#endif