]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/qt_helpers.cpp
Add message dialog to LFUN_CITATION_OPEN chain
[lyx.git] / src / frontends / qt / qt_helpers.cpp
1 /**
2  * \file qt_helpers.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Dekel Tsur
7  * \author Jürgen Spitzmüller
8  * \author Richard Kimberly Heck
9  *
10  * Full author contact details are available in file CREDITS.
11  */
12
13 #include <config.h>
14
15 #include "qt_helpers.h"
16
17 #include "LengthCombo.h"
18 #include "LyXRC.h"
19
20 #include "frontends/alert.h"
21
22 #include "support/convert.h"
23 #include "support/debug.h"
24 #include "support/gettext.h"
25 #include "support/lstrings.h"
26 #include "support/Package.h"
27 #include "support/PathChanger.h"
28 #include "support/Systemcall.h"
29
30 #include <QApplication>
31 #include <QCheckBox>
32 #include <QComboBox>
33 #include <QDesktopServices>
34 #include <QDir>
35 #include <QLineEdit>
36 #include <QMessageBox>
37 #include <QLocale>
38 #include <QPalette>
39 #include <QPushButton>
40 #include <QSet>
41 #include <QSettings>
42 #include <QTextLayout>
43 #include <QTextDocument>
44 #include <QToolTip>
45 #include <QUrl>
46
47 #include <algorithm>
48 #include <fstream>
49 #include <locale>
50
51 // for FileFilter.
52 // FIXME: Remove
53 #include <regex>
54
55 using namespace std;
56 using namespace lyx::support;
57
58 namespace lyx {
59
60 FileName libFileSearch(QString const & dir, QString const & name,
61                                 QString const & ext, search_mode mode)
62 {
63         return support::libFileSearch(fromqstr(dir), fromqstr(name), fromqstr(ext), mode);
64 }
65
66
67 FileName imageLibFileSearch(QString & dir, QString const & name,
68                                 QString const & ext, search_mode mode)
69 {
70         string tmp = fromqstr(dir);
71         FileName fn = support::imageLibFileSearch(tmp, fromqstr(name), fromqstr(ext), mode);
72         dir = toqstr(tmp);
73         return fn;
74 }
75
76 namespace {
77
78 double locstringToDouble(QString const & str)
79 {
80         QLocale loc;
81         bool ok;
82         double res = loc.toDouble(str, &ok);
83         if (!ok) {
84                 // Fall back to C
85                 QLocale c(QLocale::C);
86                 res = c.toDouble(str);
87         }
88         return res;
89 }
90
91 } // namespace
92
93
94 namespace frontend {
95
96 string widgetsToLength(QLineEdit const * input, LengthCombo const * combo)
97 {
98         QString const length = input->text();
99         if (length.isEmpty())
100                 return string();
101
102         // Don't return unit-from-choice if the input(field) contains a unit
103         if (isValidGlueLength(fromqstr(length)))
104                 return fromqstr(length);
105         // Also try with localized version
106         if (isValidGlueLength(fromqstr(unlocLengthString(length))))
107                 return fromqstr(unlocLengthString(length));
108
109         Length::UNIT const unit = combo->currentLengthItem();
110
111         return Length(locstringToDouble(length.trimmed()), unit).asString();
112 }
113
114
115 Length widgetsToLength(QLineEdit const * input, QComboBox const * combo)
116 {
117         QString const length = input->text();
118         if (length.isEmpty())
119                 return Length();
120
121         // don't return unit-from-choice if the input(field) contains a unit
122         if (isValidGlueLength(fromqstr(length)))
123                 return Length(fromqstr(length));
124         // Also try with localized version
125         if (isValidGlueLength(fromqstr(unlocLengthString(length))))
126                 return Length(fromqstr(unlocLengthString(length)));
127
128         Length::UNIT unit = Length::UNIT_NONE;
129         QString const item = combo->currentText();
130         for (int i = 0; i < num_units; i++) {
131                 if (qt_(lyx::unit_name_gui[i]) == item) {
132                         unit = unitFromString(unit_name[i]);
133                         break;
134                 }
135         }
136
137         return Length(locstringToDouble(length.trimmed()), unit);
138 }
139
140
141 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
142         Length const & len, Length::UNIT /*defaultUnit*/)
143 {
144         if (len.empty()) {
145                 // no length (UNIT_NONE)
146                 combo->setCurrentItem(Length::defaultUnit());
147                 input->setText("");
148         } else {
149                 combo->setCurrentItem(len.unit());
150                 QLocale loc;
151                 loc.setNumberOptions(QLocale::OmitGroupSeparator);
152                 input->setText(formatLocFPNumber(Length(len).value()));
153         }
154 }
155
156
157 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
158         string const & len, Length::UNIT defaultUnit)
159 {
160         if (len.empty()) {
161                 // no length (UNIT_NONE)
162                 combo->setCurrentItem(defaultUnit);
163                 input->setText("");
164         } else if (!isValidLength(len) && !isStrDbl(len)) {
165                 // use input field only for gluelengths
166                 combo->setCurrentItem(defaultUnit);
167                 input->setText(locLengthString(toqstr(len)));
168         } else {
169                 lengthToWidgets(input, combo, Length(len), defaultUnit);
170         }
171 }
172
173
174 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
175         docstring const & len, Length::UNIT defaultUnit)
176 {
177         lengthToWidgets(input, combo, to_utf8(len), defaultUnit);
178 }
179
180
181 double widgetToDouble(QLineEdit const * input)
182 {
183         QString const text = input->text();
184         if (text.isEmpty())
185                 return 0.0;
186
187         return locstringToDouble(text.trimmed());
188 }
189
190
191 string widgetToDoubleStr(QLineEdit const * input)
192 {
193         return convert<string>(widgetToDouble(input));
194 }
195
196
197 void doubleToWidget(QLineEdit * input, double value, char f, int prec)
198 {
199         QLocale loc;
200         loc.setNumberOptions(QLocale::OmitGroupSeparator);
201         input->setText(loc.toString(value, f, prec));
202 }
203
204
205 void doubleToWidget(QLineEdit * input, string const & value, char f, int prec)
206 {
207         doubleToWidget(input, convert<double>(value), f, prec);
208 }
209
210
211 QString formatLocFPNumber(double d)
212 {
213         QString result = toqstr(formatFPNumber(d));
214         QLocale loc;
215         result.replace('.', loc.decimalPoint());
216         return result;
217 }
218
219
220 bool SortLocaleAware(QString const & lhs, QString const & rhs)
221 {
222         return QString::localeAwareCompare(lhs, rhs) < 0;
223 }
224
225
226 bool ColorSorter(ColorCode lhs, ColorCode rhs)
227 {
228         return compare_no_case(lcolor.getGUIName(lhs), lcolor.getGUIName(rhs)) < 0;
229 }
230
231
232 void setValid(QWidget * widget, bool valid)
233 {
234         if (valid) {
235                 if (qobject_cast<QCheckBox*>(widget) != nullptr)
236                         // Check boxes need to be treated differenty, see
237                         // https://forum.qt.io/topic/93253/
238                         widget->setStyleSheet("");
239                 else
240                         widget->setPalette(QPalette());
241         } else {
242                 if (qobject_cast<QCheckBox*>(widget) != nullptr) {
243                         // Check boxes need to be treated differenty, see
244                         // https://forum.qt.io/topic/93253/
245                         if (qobject_cast<QCheckBox*>(widget)->isChecked())
246                                 widget->setStyleSheet("QCheckBox:unchecked{ color: red; }QCheckBox:checked{ color: red; }");
247                 } else {
248                         QPalette pal = widget->palette();
249                         pal.setColor(QPalette::Active, QPalette::WindowText, QColor(255, 0, 0));
250                         pal.setColor(QPalette::Active, QPalette::Text, QColor(255, 0, 0));
251                         widget->setPalette(pal);
252                 }
253         }
254 }
255
256
257 void focusAndHighlight(QAbstractItemView * w)
258 {
259         w->setFocus();
260         w->setCurrentIndex(w->currentIndex());
261         w->scrollTo(w->currentIndex());
262 }
263
264
265 void setMessageColour(list<QWidget *> highlighted, list<QWidget *> plain)
266 {
267         QPalette pal = QApplication::palette();
268         QPalette newpal(pal.color(QPalette::Active, QPalette::HighlightedText),
269                         pal.color(QPalette::Active, QPalette::Highlight));
270         newpal.setColor(QPalette::WindowText,
271                         pal.color(QPalette::Active, QPalette::HighlightedText));
272         for (QWidget * w : highlighted)
273                 w->setPalette(newpal);
274         for (QWidget * w : plain)
275                 w->setPalette(pal);
276 }
277
278
279 void showDirectory(FileName const & directory)
280 {
281         if (!directory.exists())
282                 return;
283         QUrl qurl(QUrl::fromLocalFile(QDir::toNativeSeparators(toqstr(directory.absFileName()))));
284         // Give hints in case of bugs
285         if (!qurl.isValid()) {
286                 frontend::Alert::error(_("Invalid URL"),
287                         bformat(_("The URL `%1$s' could not be resolved."),
288                                 qstring_to_ucs4(qurl.toString())));
289                 return;
290
291         }
292         if (!QDesktopServices::openUrl(qurl))
293                 frontend::Alert::error(_("URL could not be accessed"),
294                         bformat(_("The URL `%1$s' could not be opened although it exists!"),
295                                 qstring_to_ucs4(qurl.toString())));
296 }
297
298 void showTarget(string const & target, string const & docpath,
299                 string const & pdfv, string const & psv)
300 {
301         LYXERR(Debug::INSETS, "Showtarget:" << target << "\n");
302
303         // security measure: ask user before opening if document is not marked trusted.
304         QSettings settings;
305         if (!settings.value("trusted documents/" + toqstr(docpath), false).toBool()) {
306                 QCheckBox * dontShowAgainCB = new QCheckBox();
307                 dontShowAgainCB->setText(qt_("&Trust this document and do not ask me again!"));
308                 dontShowAgainCB->setToolTip(qt_("If you check this, LyX will open all targets without asking for the given document in the future."));
309                 docstring const warn =
310                         prefixIs(target, "EXTERNAL ") ?
311                                         bformat(_("LyX will search your directory for files with the following keywords in their name "
312                                                   "and then open it in an external application, if a file is found:\n"
313                                                   "'%1$s'\n"
314                                                   "Be aware that this might entail security infringements!\n"
315                                                   "Only do this if you trust origin of the document and the keywords used!\n"
316                                                   "How do you want to proceed?"), from_utf8(target).substr(9, docstring::npos))
317                                       : bformat(_("LyX wants to open the following link in an external application:\n"
318                                                   "%1$s\n"
319                                                   "Be aware that this might entail security infringements!\n"
320                                                   "Only do this if you trust origin of the document and the target of the link!\n"
321                                                   "How do you want to proceed?"), from_utf8(target));
322                 QMessageBox box(QMessageBox::Warning, qt_("Open external target?"), toqstr(warn),
323                                 QMessageBox::NoButton, qApp->focusWidget());
324                 QPushButton * openButton = box.addButton(qt_("&Open Target"), QMessageBox::ActionRole);
325                 box.addButton(QMessageBox::Abort);
326                 box.setCheckBox(dontShowAgainCB);
327                 box.setDefaultButton(QMessageBox::Abort);
328                 box.exec();
329                 if (box.clickedButton() != openButton)
330                         return;
331                 if (dontShowAgainCB->isChecked())
332                         settings.setValue("trusted documents/"
333                                 + toqstr(docpath), true);
334         }
335         
336         if (prefixIs(target, "EXTERNAL ")) {
337                 if (!lyxrc.citation_search)
338                         return;
339                 string tmp, tar, opts;
340                 tar = split(target, tmp, ' ');
341                 if (!pdfv.empty())
342                         opts = " -v \"" + pdfv + "\"";
343                 if (!psv.empty())
344                         opts += " -w \"" + psv + "\"";
345                 if (!opts.empty())
346                         opts += " ";
347                 Systemcall one;
348                 string const viewer = subst(lyxrc.citation_search_view, "$${python}", os::python());
349                 string const command = viewer + " " + opts + tar;
350                 int const result = one.startscript(Systemcall::Wait, command);
351                 if (result == 1)
352                         // Script failed
353                         frontend::Alert::error(_("Could not open file"),
354                                 _("The lyxpaperview script failed."));
355                 else if (result == 2)
356                         frontend::Alert::error(_("Could not open file"),
357                                 bformat(_("No file was found using the pattern `%1$s'."),
358                                         from_utf8(tar)));
359                 return;
360         }
361         if (!QDesktopServices::openUrl(QUrl(toqstr(target), QUrl::TolerantMode)))
362                 frontend::Alert::error(_("Could not open file"),
363                         bformat(_("The target `%1$s' could not be resolved."),
364                                 from_utf8(target)));
365 }
366 } // namespace frontend
367
368 QString const qt_(char const * str, const char *)
369 {
370         return toqstr(_(str));
371 }
372
373
374 QString const qt_(string const & str)
375 {
376         return toqstr(_(str));
377 }
378
379
380 QString const qt_(QString const & qstr)
381 {
382         return toqstr(_(fromqstr(qstr)));
383 }
384
385
386 void rescanTexStyles(string const & arg)
387 {
388         // Run rescan in user lyx directory
389         PathChanger p(package().user_support());
390         FileName const prog = support::libFileSearch("scripts", "TeXFiles.py");
391         Systemcall one;
392         string const command = os::python() + ' ' +
393             quoteName(prog.toFilesystemEncoding()) + ' ' +
394             arg;
395         int const status = one.startscript(Systemcall::Wait, command);
396         if (status == 0)
397                 return;
398         // FIXME UNICODE
399         frontend::Alert::error(_("Could not update TeX information"),
400                 bformat(_("The script `%1$s' failed."), from_utf8(prog.absFileName())));
401 }
402
403
404 QStringList texFileList(QString const & filename)
405 {
406         QStringList list;
407         FileName const file = libFileSearch(QString(), filename);
408         if (file.empty())
409                 return list;
410
411         // FIXME Unicode.
412         vector<docstring> doclist =
413                 getVectorFromString(file.fileContents("UTF-8"), from_ascii("\n"));
414
415         // Normalise paths like /foo//bar ==> /foo/bar
416         QSet<QString> set;
417         for (size_t i = 0; i != doclist.size(); ++i) {
418                 QString qfile = toqstr(doclist[i]);
419                 qfile.replace("\r", "");
420                 while (qfile.contains("//"))
421                         qfile.replace("//", "/");
422                 if (!qfile.isEmpty())
423                         set.insert(qfile);
424         }
425
426         // remove duplicates
427 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
428         return QList<QString>(set.begin(), set.end());
429 #else
430         return QList<QString>::fromSet(set);
431 #endif
432 }
433
434 QString const externalLineEnding(docstring const & str)
435 {
436 #ifdef Q_OS_MAC
437         // The MAC clipboard uses \r for lineendings, and we use \n
438         return toqstr(subst(str, '\n', '\r'));
439 #elif defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN)
440         // Windows clipboard uses \r\n for lineendings, and we use \n
441         return toqstr(subst(str, from_ascii("\n"), from_ascii("\r\n")));
442 #else
443         return toqstr(str);
444 #endif
445 }
446
447
448 docstring const internalLineEnding(QString const & str)
449 {
450         docstring const s = subst(qstring_to_ucs4(str),
451                                   from_ascii("\r\n"), from_ascii("\n"));
452         return subst(s, '\r', '\n');
453 }
454
455
456 QString internalPath(const QString & str)
457 {
458         return toqstr(os::internal_path(fromqstr(str)));
459 }
460
461
462 QString onlyFileName(const QString & str)
463 {
464         return toqstr(support::onlyFileName(fromqstr(str)));
465 }
466
467
468 QString onlyPath(const QString & str)
469 {
470         return toqstr(support::onlyPath(fromqstr(str)));
471 }
472
473
474 QString changeExtension(QString const & oldname, QString const & ext)
475 {
476         return toqstr(support::changeExtension(fromqstr(oldname), fromqstr(ext)));
477 }
478
479 /// Remove the extension from \p name
480 QString removeExtension(QString const & name)
481 {
482         return toqstr(support::removeExtension(fromqstr(name)));
483 }
484
485 /** Add the extension \p ext to \p name.
486  Use this instead of changeExtension if you know that \p name is without
487  extension, because changeExtension would wrongly interpret \p name if it
488  contains a dot.
489  */
490 QString addExtension(QString const & name, QString const & ext)
491 {
492         return toqstr(support::addExtension(fromqstr(name), fromqstr(ext)));
493 }
494
495 /// Return the extension of the file (not including the .)
496 QString getExtension(QString const & name)
497 {
498         return toqstr(support::getExtension(fromqstr(name)));
499 }
500
501
502 /** Convert relative path into absolute path based on a basepath.
503   If relpath is absolute, just use that.
504   If basepath doesn't exist use CWD.
505   */
506 QString makeAbsPath(QString const & relpath, QString const & base)
507 {
508         return toqstr(support::makeAbsPath(fromqstr(relpath),
509                 fromqstr(base)).absFileName());
510 }
511
512
513 /////////////////////////////////////////////////////////////////////////
514 //
515 // FileFilterList
516 //
517 /////////////////////////////////////////////////////////////////////////
518
519 /** Given a string such as
520  *      "<glob> <glob> ... *.{abc,def} <glob>",
521  *  convert the csh-style brace expressions:
522  *      "<glob> <glob> ... *.abc *.def <glob>".
523  *  Requires no system support, so should work equally on Unix, Mac, Win32.
524  */
525 static string const convert_brace_glob(string const & glob)
526 {
527         // Matches " *.{abc,def,ghi}", storing "*." as group 1 and
528         // "abc,def,ghi" as group 2, while allowing spaces in group 2.
529         static regex const glob_re(" *([^ {]*)\\{([^}]+)\\}");
530         // Matches "abc" and "abc,", storing "abc" as group 1,
531         // while ignoring surrounding spaces.
532         static regex const block_re(" *([^ ,}]+) *,? *");
533
534         string pattern;
535
536         string::const_iterator it = glob.begin();
537         string::const_iterator const end = glob.end();
538         while (true) {
539                 match_results<string::const_iterator> what;
540                 if (!regex_search(it, end, what, glob_re)) {
541                         // Ensure that no information is lost.
542                         pattern += string(it, end);
543                         break;
544                 }
545
546                 // Everything from the start of the input to
547                 // the start of the match.
548                 pattern += string(what[-1].first, what[-1].second);
549
550                 // Given " *.{abc,def}", head == "*." and tail == "abc,def".
551                 string const head = string(what[1].first, what[1].second);
552                 string const tail = string(what[2].first, what[2].second);
553
554                 // Split the ','-separated chunks of tail so that
555                 // $head{$chunk1,$chunk2} becomes "$head$chunk1 $head$chunk2".
556                 string const fmt = " " + head + "$1";
557                 pattern += regex_replace(tail, block_re, fmt);
558
559                 // Increment the iterator to the end of the match.
560                 it += distance(it, what[0].second);
561         }
562
563         return pattern;
564 }
565
566
567 struct Filter
568 {
569         /* \param description text describing the filters.
570          * \param one or more wildcard patterns, separated by
571          * whitespace.
572          */
573         Filter(docstring const & description, std::string const & globs);
574
575         docstring const & description() const { return desc_; }
576
577         QString toString() const;
578
579         docstring desc_;
580         std::vector<std::string> globs_;
581 };
582
583
584 Filter::Filter(docstring const & description, string const & globs)
585         : desc_(description)
586 {
587         // Given "<glob> <glob> ... *.{abc,def} <glob>", expand to
588         //       "<glob> <glob> ... *.abc *.def <glob>"
589         string const expanded_globs = convert_brace_glob(globs);
590
591         // Split into individual globs.
592         globs_ = getVectorFromString(expanded_globs, " ");
593 }
594
595
596 QString Filter::toString() const
597 {
598         QString s;
599
600         bool const has_description = !desc_.empty();
601
602         if (has_description) {
603                 s += toqstr(desc_);
604                 s += " (";
605         }
606
607         s += toqstr(getStringFromVector(globs_, " "));
608
609         if (has_description)
610                 s += ')';
611         return s;
612 }
613
614
615 /** \c FileFilterList parses a Qt-style list of available file filters
616  *  to generate the corresponding vector.
617  *  For example "TeX documents (*.tex);;LyX Documents (*.lyx)"
618  *  will be parsed to fill a vector of size 2, whilst "*.{p[bgp]m} *.pdf"
619  *  will result in a vector of size 1 in which the description field is empty.
620  */
621 struct FileFilterList
622 {
623         // FIXME UNICODE: globs_ should be unicode...
624         /** \param qt_style_filter a list of available file filters.
625          *  Eg. "TeX documents (*.tex);;LyX Documents (*.lyx)".
626          *  The "All files (*)" filter is always added to the list.
627          */
628         explicit FileFilterList(docstring const & qt_style_filter =
629                                 docstring());
630
631         typedef std::vector<Filter>::size_type size_type;
632
633         bool empty() const { return filters_.empty(); }
634         size_type size() const { return filters_.size(); }
635         Filter & operator[](size_type i) { return filters_[i]; }
636         Filter const & operator[](size_type i) const { return filters_[i]; }
637
638         void parse_filter(std::string const & filter);
639         std::vector<Filter> filters_;
640 };
641
642 FileFilterList::FileFilterList(docstring const & qt_style_filter)
643 {
644         // FIXME UNICODE
645         string const filter = to_utf8(qt_style_filter)
646                 + (qt_style_filter.empty() ? string() : ";;")
647                 + to_utf8(_("All Files")) + " " + fromqstr(wildcardAllFiles());
648
649         // Split data such as "TeX documents (*.tex);;LyX Documents (*.lyx)"
650         // into individual filters.
651         static regex const separator_re(";;");
652
653         string::const_iterator it = filter.begin();
654         string::const_iterator const end = filter.end();
655         while (true) {
656                 match_results<string::const_iterator> what;
657
658                 if (!regex_search(it, end, what, separator_re)) {
659                         parse_filter(string(it, end));
660                         break;
661                 }
662
663                 // Everything from the start of the input to
664                 // the start of the match.
665                 parse_filter(string(it, what[0].first));
666
667                 // Increment the iterator to the end of the match.
668                 it += distance(it, what[0].second);
669         }
670 }
671
672
673 void FileFilterList::parse_filter(string const & filter)
674 {
675         // Matches "TeX documents (plain) (*.tex)",
676         // storing "TeX documents (plain) " as group 1 and "*.tex" as group 2.
677         static regex const filter_re("(.*)\\(([^()]+)\\) *$");
678
679         match_results<string::const_iterator> what;
680         if (!regex_search(filter, what, filter_re)) {
681                 // Just a glob, no description.
682                 filters_.push_back(Filter(docstring(), trim(filter)));
683         } else {
684                 // FIXME UNICODE
685                 docstring const desc = from_utf8(string(what[1].first, what[1].second));
686                 string const globs = string(what[2].first, what[2].second);
687                 filters_.push_back(Filter(trim(desc), trim(globs)));
688         }
689 }
690
691
692 QString wildcardAllFiles()
693 {
694 #if defined(_WIN32)
695                 return "(*.*)";
696 #else
697                 return "(*)";
698 #endif
699 }
700
701
702 /** \returns the equivalent of the string passed in
703  *  although any brace expressions are expanded.
704  *  (E.g. "*.{png,jpg}" -> "*.png *.jpg")
705  */
706 QStringList fileFilters(QString const & desc)
707 {
708         // we have: "*.{gif,png,jpg,bmp,pbm,ppm,tga,tif,xpm,xbm}"
709         // but need:  "*.cpp;*.cc;*.C;*.cxx;*.c++"
710         FileFilterList filters(qstring_to_ucs4(desc));
711         //LYXERR0("DESC: " << desc);
712         QStringList list;
713         for (size_t i = 0; i != filters.filters_.size(); ++i) {
714                 QString f = filters.filters_[i].toString();
715                 //LYXERR0("FILTER: " << f);
716                 list.append(f);
717         }
718         return list;
719 }
720
721
722 QString formatToolTip(QString text, int width)
723 {
724         // 1. QTooltip activates word wrapping only if mightBeRichText()
725         //    is true. So we convert the text to rich text.
726         //
727         // 2. The default width is way too small. Setting the width is tricky; first
728         //    one has to compute the ideal width, and then force it with special
729         //    html markup.
730
731         // do nothing if empty or already formatted
732         if (text.isEmpty() || text.startsWith(QString("<html>")))
733                 return text;
734         // Convert to rich text if it is not already
735         if (!Qt::mightBeRichText(text))
736                 text = Qt::convertFromPlainText(text, Qt::WhiteSpaceNormal);
737         // Compute desired width in pixels
738         QFont const font = QToolTip::font();
739 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
740         int const px_width = width * QFontMetrics(font).horizontalAdvance("M");
741 #else
742         int const px_width = width * QFontMetrics(font).width("M");
743 #endif
744         // Determine the ideal width of the tooltip
745         QTextDocument td("");
746         td.setHtml(text);
747         td.setDefaultFont(QToolTip::font());
748         td.setDocumentMargin(0);
749         td.setTextWidth(px_width);
750         double best_width = td.idealWidth();
751         // Set the line wrapping with appropriate width
752         return QString("<html><body><table><tr>"
753                        "<td align=justify width=%1>%2</td>"
754                        "</tr></table></body></html>")
755                 .arg(QString::number(int(best_width) + 1), text);
756 }
757
758
759 QString qtHtmlToPlainText(QString const & text)
760 {
761         if (!Qt::mightBeRichText(text))
762                 return text;
763         QTextDocument td;
764         td.setHtml(text);
765         return td.toPlainText();
766 }
767
768
769 } // namespace lyx