maxLibQt
ScrollableMessageBox.cpp
Go to the documentation of this file.
1 /*
2  ScrollableMessageBox
3 
4  COPYRIGHT: (c)2017 Maxim Paperno; All Right Reserved.
5  Contact: http://www.WorldDesign.com/contact
6 
7  LICENSE:
8 
9  Commercial License Usage
10  Licensees holding valid commercial licenses may use this file in
11  accordance with the terms contained in a written agreement between
12  you and the copyright holder.
13 
14  GNU General Public License Usage
15  Alternatively, this file may be used under the terms of the GNU
16  General Public License as published by the Free Software Foundation,
17  either version 3 of the License, or (at your option) any later version.
18 
19  This program is distributed in the hope that it will be useful,
20  but WITHOUT ANY WARRANTY; without even the implied warranty of
21  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22  GNU General Public License for more details.
23 
24  A copy of the GNU General Public License is available at <http://www.gnu.org/licenses/>.
25 */
26 
27 #include "ScrollableMessageBox.h"
28 
29 #include <QApplication>
30 #include <QBoxLayout>
31 #include <QScreen>
32 #include <QScrollBar>
33 #include <QStyle>
34 #include <QStyleOptionButton>
35 #include <QWindow>
36 
37 class TextEdit : public QTextEdit
38 {
39  public:
41  QSize sizeHint() const override
42  {
43  // stupid trick to get an idea of necessary size to contain all the text, works for html and plain
44  QLabel tmp;
45  tmp.setFont(font());
46  tmp.setText(toHtml());
49  }
50 };
51 
53 
54 // This is a close copy of the DetailsButton from QMessageBox code and uses the same translation strings for the button text.
55 class DetailButton : public QPushButton
56 {
57  public:
58  DetailButton(QWidget *parent) : QPushButton(label(false), parent)
59  {
61  setCheckable(true);
62  connect(this, &QPushButton::toggled, [this](bool on) {
63  setText(label(on));
64  });
65  }
66 
67  inline static const QString label(bool on)
68  {
69  return on ? QMessageBox::tr("Hide Details...") : QMessageBox::tr("Show Details...");
70  }
71 
72  QSize sizeHint() const override
73  {
76  initStyleOption(&opt);
77  const QFontMetrics fm = fontMetrics();
78  opt.text = label(false);
79  QSize sz = fm.size(Qt::TextShowMnemonic, opt.text);
80  QSize ret = style()->sizeFromContents(QStyle::CT_PushButton, &opt, sz, this);
81  opt.text = label(true);
82  sz = fm.size(Qt::TextShowMnemonic, opt.text);
83  ret = ret.expandedTo(style()->sizeFromContents(QStyle::CT_PushButton, &opt, sz, this));
85  }
86 };
87 
89 
91 {
92  const int extent = w->style()->pixelMetric(QStyle::PM_MessageBoxIconSize, nullptr, w);
93  return QSize(extent, extent);
94 }
95 
96 static QPixmap standardIcon(QMessageBox::Icon icon, QWidget *w, const QSize &size = QSize())
97 {
98  QIcon tmpIcon;
99  switch (icon) {
101  tmpIcon = w->style()->standardIcon(QStyle::SP_MessageBoxInformation, nullptr, w);
102  break;
104  tmpIcon = w->style()->standardIcon(QStyle::SP_MessageBoxWarning, nullptr, w);
105  break;
107  tmpIcon = w->style()->standardIcon(QStyle::SP_MessageBoxCritical, nullptr, w);
108  break;
110  tmpIcon = w->style()->standardIcon(QStyle::SP_MessageBoxQuestion, nullptr, w);
111  break;
112  default:
113  break;
114  }
115  if (tmpIcon.isNull())
116  return QPixmap();
117  return tmpIcon.pixmap(w->windowHandle(), (size.isEmpty() ? defaultIconSize(w) : size));
118 }
119 
121 
123 
125  QDialog(parent, f | defaultFlags())
126 {
127  init();
128 }
129 
130 ScrollableMessageBox::ScrollableMessageBox(const QString &title, const QString &text, const QString &details, QWidget *parent, Qt::WindowFlags f) :
131  QDialog(parent, f | defaultFlags())
132 {
133  init(title, text, details);
134 }
135 
136 ScrollableMessageBox::ScrollableMessageBox(const QString &title, QMessageBox::Icon icon, const QString &text, const QString &details, QWidget *parent, Qt::WindowFlags f) :
137  QDialog(parent, f | defaultFlags())
138 {
139  init(title, text, details, icon);
140 }
141 
142 
143 ScrollableMessageBox::ScrollableMessageBox(QWidget *parent, const QString &title, const QString &text, const QString &details, int buttons, int defaultButtton, Qt::WindowFlags f) :
144  QDialog(parent, f | defaultFlags())
145 {
146  init(title, text, details, QMessageBox::NoIcon, buttons, defaultButtton);
147 }
148 
149 ScrollableMessageBox::ScrollableMessageBox(QWidget *parent, const QString &title, QMessageBox::Icon icon, const QString &text, const QString &details, int buttons, int defaultButtton, Qt::WindowFlags f) :
150  QDialog(parent, f | defaultFlags())
151 {
152  init(title, text, details, icon, buttons, defaultButtton);
153 }
154 
156 {
157  if (!m_textLabel)
158  return;
159  m_textLabel->setText(text);
160  updateSize();
161 }
162 
164 {
165  if (!m_textEdit)
166  return;
167  const bool haveDeets = !details.isEmpty();
168  if (format == Qt::PlainText || !haveDeets)
169  m_textEdit->setPlainText(details);
170  else if (format == Qt::RichText)
171  m_textEdit->setHtml(details);
172  else
173  m_textEdit->setText(details);
174 
175  if (haveDeets || !showDetailsExpanded(false))
176  updateSize();
177 }
178 
180 {
181  if (!m_promptLabel) {
182  m_promptLabel = new QLabel(this);
183  const int idx = m_btnBox ? layout()->indexOf(m_btnBox) : layout()->count();
184  qobject_cast<QVBoxLayout*>(layout())->insertWidget(idx, m_promptLabel, 0, Qt::AlignLeft | Qt::AlignBottom);
185  updateSize();
186  }
187  return m_promptLabel.data();
188 }
189 
191 {
192  if (text.isEmpty()) {
193  if (m_promptLabel) {
194  layout()->removeWidget(m_promptLabel);
195  m_promptLabel->setParent(nullptr);
196  m_promptLabel->deleteLater();
197  m_promptLabel.clear();
198  updateSize();
199  }
200  return;
201  }
202  promptLabel()->setText(text);
203  updateSize();
204 }
205 
207 {
208  if (!m_iconLabel)
209  return;
210  m_iconLabel->setPixmap(pixmap);
211  static_cast<QHBoxLayout*>(layout()->itemAt(0))->setSpacing(pixmap.isNull() ? 0 : 12);
212  if (pixmap.isNull())
214  else
215  setWindowIcon(QIcon(pixmap));
216  updateSize();
217 }
218 
220 {
221  setIcon(standardIcon(icon, this, size));
222 }
223 
224 void ScrollableMessageBox::setIcon(const QIcon &icon, const QSize &size)
225 {
226  setIcon(icon.isNull() ? QPixmap() : icon.pixmap(size.isEmpty() ? defaultIconSize(this) : size));
227 }
228 
230 {
231  if (!m_iconLabel)
232  return;
233  static_cast<QHBoxLayout*>(layout()->itemAt(0))->setAlignment(m_iconLabel, Qt::AlignLeft | valign);
234 }
235 
237 {
238  if (!m_textEdit)
239  return;
240  QFont newFont(m_textEdit->font());
241  if (fixed) {
242  newFont.setFamily("Courier");
243  newFont.setStyleHint(QFont::TypeWriter);
244  }
245  else {
246  newFont.setFamily("Helvetica");
247  newFont.setStyleHint(QFont::SansSerif);
248  }
249 #ifdef Q_OS_MACOS
250  newFont.setPointSize(13);
252 #elif defined Q_OS_WIN
253  newFont.setPointSize(10);
254 #endif
255  m_textEdit->setFont(newFont);
256 }
257 
259 {
260  if (!m_textEdit)
261  return;
262  m_textEdit->setWordWrapMode(wrap ? QTextOption::WrapAtWordBoundaryOrAnywhere : QTextOption::NoWrap);
263  updateSize();
264 }
265 
267 {
268  if (!m_textLabel)
269  return;
270  if (m_textLabel->wordWrap() == wrap)
271  return;
272  m_textLabel->setWordWrap(wrap);
273  updateSize();
274 }
275 
277 {
278  return m_textEdit && m_textEdit->isVisibleTo(this);
279 }
280 
282 {
283  if (!m_textEdit || detailsExpanded() == on)
284  return false;
285  m_textEdit->setVisible(on);
286  QSizePolicy sp = m_textEdit->sizePolicy();
288  //sp.setVerticalStretch(int(on));
289  m_textEdit->setSizePolicy(sp);
290  if (m_detailsBtn)
291  m_detailsBtn->setChecked(on);
292  updateSize();
293  //adjustSize();
294  if (isVisible())
295  resize(width(), sizeHint().height());
296  return true;
297 }
298 
300 {
301  if (!m_textEdit)
302  return false;
304 }
305 
307 {
308  return !m_detailsBtn.isNull();
309 }
310 
311 void ScrollableMessageBox::setDetailsButtonVisible(bool visible, bool toggleDetails, bool toggleDetailPosition)
312 {
313  if (!m_textEdit)
314  visible = false;
315  if (m_detailsBtn.isNull() != visible)
316  return;
317  if (visible) {
318  m_detailsBtn = new DetailButton(this);
319  m_detailsBtn->setChecked(!toggleDetails && detailsExpanded());
320  m_btnBox->addButton(m_detailsBtn, QDialogButtonBox::ActionRole);
322  }
323  else {
325  m_btnBox->removeButton(m_detailsBtn);
326  m_detailsBtn->deleteLater();
327  m_detailsBtn.clear();
328  }
329  m_updatesSuspended = true;
330  if (toggleDetails)
332  if (toggleDetailPosition)
334  m_updatesSuspended = false;
335  updateSize();
336 }
337 
339 {
340  if (!m_textEdit)
341  return;
342  QVBoxLayout *l = qobject_cast<QVBoxLayout *>(layout());
343  int newIdx = on ? 2 : 1;
344  if (on && m_promptLabel)
345  ++newIdx;
346  if (l->indexOf(m_textEdit) == newIdx)
347  return;
348  //const bool wasVisible = detailsExpanded();
349  l->removeWidget(m_textEdit);
350  l->insertWidget(newIdx, m_textEdit);
351  //m_textEdit->setVisible(wasVisible);
352  updateSize();
353 }
354 
355 void ScrollableMessageBox::init(const QString &title, const QString &text, const QString &details, QMessageBox::Icon icon, int buttons, int defaultBtn)
356 {
357  m_textLabel = new QLabel(this);
358  m_textLabel->setTextInteractionFlags(Qt::TextInteractionFlags(style()->styleHint(QStyle::SH_MessageBox_TextInteractionFlags, nullptr, this)));
359  m_textLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
360  m_textLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
361  m_textLabel->setOpenExternalLinks(true);
362  //m_textLabel->setWordWrap(true);
363 
364  m_iconLabel = new QLabel(this);
365  m_iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
366 
367  m_textEdit = new TextEdit(this);
368  m_textEdit->setReadOnly(true);
369  m_textEdit->setTextInteractionFlags(Qt::TextBrowserInteraction | Qt::TextSelectableByKeyboard);
370  m_textEdit->setFocusPolicy(Qt::NoFocus);
371  QSizePolicy sp(m_textEdit->sizePolicy());
373  sp.setVerticalPolicy(QSizePolicy::Expanding);
374  sp.setVerticalStretch(1);
375  m_textEdit->setSizePolicy(sp);
376 
377  m_btnBox = new QDialogButtonBox(QDialogButtonBox::StandardButtons(buttons & ~ShowDetails), this);
378  m_btnBox->setCenterButtons(style()->styleHint(QStyle::SH_MessageBox_CenterButtons, nullptr, this));
379 
380  QHBoxLayout *labelLo = new QHBoxLayout();
381  labelLo->setSpacing(0);
382  labelLo->addWidget(m_iconLabel, 0, Qt::AlignLeft | Qt::AlignTop);
383  labelLo->addWidget(m_textLabel, 1);
384 
385  QVBoxLayout *lo = new QVBoxLayout(this);
386  lo->setSpacing(8);
387  lo->addLayout(labelLo);
388  lo->addWidget(m_textEdit, 1);
389  lo->addWidget(m_btnBox);
390 
391  setSizeGripEnabled(true);
392  setFontFixedWidth(false);
393  setWordWrap(true);
394 
395  if (!title.isEmpty())
396  setWindowTitle(title);
397  if (!text.isEmpty())
398  setText(text);
399  if (icon != QMessageBox::NoIcon)
400  setIcon(icon);
401  setDetailedText(details);
402 
403  if (buttons & ShowDetails)
405 
406  if (defaultBtn != QDialogButtonBox::NoButton && (buttons & defaultBtn)) {
407  if (defaultBtn == ShowDetails)
408  m_detailsBtn->setDefault(true);
409  else
410  m_btnBox->button(QDialogButtonBox::StandardButton(defaultBtn))->setDefault(true);
411  }
412 
415 
416  m_updatesSuspended = false;
417  updateSize();
418 }
419 
420 static inline int layoutMinWidth(QLayout *l)
421 {
422  l->update();
423  return l->totalMinimumSize().width();
424 }
425 
426 // Calculate an a resonable minimum size for the dialog.
427 // Also prevents a warning from QWindowsWindow::setGeometry about window being too small on initial show.
428 // Some of this code is from QMessageBoxPrivate::updateSize()
430 {
431  if (m_updatesSuspended)
432  return;
434  const int maxWidth = screenSz.width() <= 1024 ? screenSz.width() : qMin(int(screenSz.width() * 0.66f), 1000); // largest minimum width
435  const int maxHeight = qMin(int(screenSz.height() * 0.66f), 750); // largest minimum height
436 
437  int width = layoutMinWidth(layout());
438  if (width > maxWidth && m_textLabel && !m_textLabel->wordWrap()) {
439  m_textLabel->setWordWrap(true);
441  }
442  if (width > maxWidth && m_promptLabel) {
443  m_promptLabel->setWordWrap(true);
445  }
446  width = qMax(QFontMetrics(QApplication::font("QMdiSubWindowTitleBar")).horizontalAdvance(windowTitle()) + 50, width);
447  if (m_textEdit)
448  width = qMax(m_textEdit->sizeHint().width(), width);
449 
450  const int height = (layout()->hasHeightForWidth() ? layout()->totalHeightForWidth(width) : layout()->totalMinimumSize().height());
451  //if (m_textEdit && m_textLabel && m_textLabel->wordWrap() && !detailsExpanded())
452  // height -= layout()->spacing() * 3;
453 
454  const QSize minSize = QSize(maxWidth, maxHeight).boundedTo(QSize(width, height).expandedTo(QSize(200, 100))); // 200x100 is minimum top-level widget size as per QWidget::adjustSize()
455  setMinimumSize(minSize);
457 }
458 
460 {
461  updateSize();
463  adjustSize();
464 }
QLayout * layout() const const
void resize(int w, int h)
QSize boundedTo(const QSize &otherSize) const const
virtual void reject()
int width() const const
T * data() const const
QTextEdit(QWidget *parent)
virtual QSize sizeHint() const const override
void setDetailedText(const QString &details=QString(), Qt::TextFormat format=Qt::AutoText)
Set the longer details text shown in the scrollable message pane area.
QScreen * screenAt(const QPoint &point)
static QPixmap standardIcon(QMessageBox::Icon icon, QWidget *w, const QSize &size=QSize())
bool isEmpty() const const
QString toHtml() const const
QStyle * style() const const
TextFormat
virtual int pixelMetric(QStyle::PixelMetric metric, const QStyleOption *option, const QWidget *widget) const const=0
A "Show Details" button which toggles the details text on and off (defined with QDialogButtonBox::Act...
bool isVisible() const const
SH_MessageBox_TextInteractionFlags
void ensurePolished() const const
void clear()
void setAttribute(Qt::WidgetAttribute attribute, bool on)
void setWordWrap(bool wrap=true)
Enable or disable word wrapping for the detailed text display.
void setIcon(const QPixmap &pixmap=QPixmap())
Set the image displayed next to the message box text label to pixmap. An invalid pixmap value will re...
void adjustSize()
bool disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *method)
AlignLeft
virtual QSize sizeHint() const const override
bool showDetailsExpanded(bool on=true)
Sets the details text visibility to on.
QSize globalStrut()
void initStyleOption(QStyleOptionButton *option) const const
QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) const const
void setWindowIcon(const QIcon &icon)
static int layoutMinWidth(QLayout *l)
void setFontFixedWidth(bool fixed=true)
Changes the font used to display the detailed text to a fixed-width or a proportional font based on f...
int width() const const
virtual int indexOf(QWidget *widget) const const
QSize size() const const
void setMinimumSize(const QSize &)
void removeWidget(QWidget *widget)
static Qt::WindowFlags defaultFlags()
void removePostedEvents(QObject *receiver, int eventType)
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
QFont font()
QTextOption::WrapMode wordWrapMode() const const
bool isNull() const const
QTextEdit * textEdit() const
The QTextEdit which holds the detailed text. It is recommended to use setDetailedText() instead of se...
bool isEmpty() const const
void clicked(bool checked)
QString tr(const char *s, const char *c, int n)
ScrollableMessageBox(QWidget *parent=nullptr, Qt::WindowFlags f=Qt::Dialog)
void setText(const QString &)
virtual QSize sizeHint() const const override
void setCheckable(bool)
typedef TextInteractionFlags
void setIconAlignment(Qt::AlignmentFlag valign)
Set the vertical alignment of the icon to valign.
QScrollBar * verticalScrollBar() const const
void setSizePolicy(QSizePolicy)
virtual void accept()
void setPromptText(const QString &text=QString())
Set a message to be shown just above the dialog button(s), below the details text.
static QSize defaultIconSize(QWidget *w)
void update()
void setVerticalPolicy(QSizePolicy::Policy policy)
void setHorizontalPolicy(QSizePolicy::Policy policy)
virtual QIcon standardIcon(QStyle::StandardPixmap standardIcon, const QStyleOption *option, const QWidget *widget) const const=0
bool isNull() const const
void setFont(const QFont &)
void setText(const QString &text=QString())
Set the text shown at the top of the dialog, before the details text.
QWindow * windowHandle() const const
WA_MacNormalSize
typedef StandardButtons
PM_MessageBoxIconSize
QFontMetrics fontMetrics() const const
QPoint pos()
QSize expandedTo(const QSize &otherSize) const const
bool isNull() const const
void updateSize()
Recalculates the dialog minimum size after UI element changes like adding/removing buttons.
QIcon windowIcon()
void toggled(bool checked)
void setWindowTitle(const QString &)
void setFamily(const QString &family)
virtual QSize sizeHint() const const override
int height() const const
TextShowMnemonic
virtual int count() const const=0
QString tr(const char *s, const char *c, int n)
void setDetailsButtonVisible(bool visible=true, bool toggleDetails=true, bool toggleDetailPosition=true)
Enables or disables showing a "show details" button which toggles visibility of the details text.
virtual void showEvent(QShowEvent *event) override
void setShowDetailsBelowButtons(bool on=true)
Toggles where the details text is displayed.
void setText(const QString &text)
QScrollBar * horizontalScrollBar() const const
SP_MessageBoxInformation
void setSizeGripEnabled(bool)
void insertWidget(int index, QWidget *widget, int stretch, Qt::Alignment alignment)
QSize size(int flags, const QString &text, int tabStops, int *tabArray) const const
void showEvent(QShowEvent *e) override
QScreen * screen() const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
virtual bool hasHeightForWidth() const const
QLabel * promptLabel()
The QLabel which holds a prompt shown just above the buttons. This is created lazily if needed.
bool toggleDetailsExpanded()
Shows the details text if it is currently hidden, and hides it otherwise.
typedef WindowFlags
bool detailsButtonVisible() const
Returns true if the "show details" button is shown, false otherwise.
virtual QSize sizeFromContents(QStyle::ContentsType type, const QStyleOption *option, const QSize &contentsSize, const QWidget *widget) const const=0
void setWordWrap(bool on)
void setTextWordWrap(bool wrap=true)
Enable or disable word wrapping for the text message (shorthand for textLabel()->setWordWrap(wrap) ).
void setSpacing(int spacing)
int height() const const
bool detailsExpanded() const
Returns true if the details text is currently visible, false otherwise.
void addLayout(QLayout *layout, int stretch)