Преглед изворни кода

ENH: Made ctkFittedTextBrowser collapsible

ctkFittedTextBrowser uses available space efficiently to display text.

The widget can now further optimize use of available space by collapsing text.
If collapsible option is enabled then only a short teaser is shown (e.g., a one-line summary)
and the user has to click on "More..." to see the full text.
Andras Lasso пре 8 година
родитељ
комит
41b1e39582

+ 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