3 * This file is part of LyX, the document processor.
4 * Licence details can be found in the file COPYING.
7 * \author Jürgen Spitzmüller
8 * \author Richard Kimberly Heck
10 * Full author contact details are available in file CREDITS.
15 #include "qt_helpers.h"
17 #include "LengthCombo.h"
20 #include "frontends/alert.h"
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"
30 #include <QApplication>
33 #include <QDesktopServices>
39 #include <QTextLayout>
40 #include <QTextDocument>
53 using namespace lyx::support;
57 FileName libFileSearch(QString const & dir, QString const & name,
58 QString const & ext, search_mode mode)
60 return support::libFileSearch(fromqstr(dir), fromqstr(name), fromqstr(ext), mode);
64 FileName imageLibFileSearch(QString & dir, QString const & name,
65 QString const & ext, search_mode mode)
67 string tmp = fromqstr(dir);
68 FileName fn = support::imageLibFileSearch(tmp, fromqstr(name), fromqstr(ext), mode);
75 double locstringToDouble(QString const & str)
79 double res = loc.toDouble(str, &ok);
82 QLocale c(QLocale::C);
83 res = c.toDouble(str);
93 string widgetsToLength(QLineEdit const * input, LengthCombo const * combo)
95 QString const length = input->text();
99 // Don't return unit-from-choice if the input(field) contains a unit
100 if (isValidGlueLength(fromqstr(length)))
101 return fromqstr(length);
102 // Also try with localized version
103 if (isValidGlueLength(fromqstr(unlocLengthString(length))))
104 return fromqstr(unlocLengthString(length));
106 Length::UNIT const unit = combo->currentLengthItem();
108 return Length(locstringToDouble(length.trimmed()), unit).asString();
112 Length widgetsToLength(QLineEdit const * input, QComboBox const * combo)
114 QString const length = input->text();
115 if (length.isEmpty())
118 // don't return unit-from-choice if the input(field) contains a unit
119 if (isValidGlueLength(fromqstr(length)))
120 return Length(fromqstr(length));
121 // Also try with localized version
122 if (isValidGlueLength(fromqstr(unlocLengthString(length))))
123 return Length(fromqstr(unlocLengthString(length)));
125 Length::UNIT unit = Length::UNIT_NONE;
126 QString const item = combo->currentText();
127 for (int i = 0; i < num_units; i++) {
128 if (qt_(lyx::unit_name_gui[i]) == item) {
129 unit = unitFromString(unit_name[i]);
134 return Length(locstringToDouble(length.trimmed()), unit);
138 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
139 Length const & len, Length::UNIT /*defaultUnit*/)
142 // no length (UNIT_NONE)
143 combo->setCurrentItem(Length::defaultUnit());
146 combo->setCurrentItem(len.unit());
148 loc.setNumberOptions(QLocale::OmitGroupSeparator);
149 input->setText(formatLocFPNumber(Length(len).value()));
154 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
155 string const & len, Length::UNIT defaultUnit)
158 // no length (UNIT_NONE)
159 combo->setCurrentItem(defaultUnit);
161 } else if (!isValidLength(len) && !isStrDbl(len)) {
162 // use input field only for gluelengths
163 combo->setCurrentItem(defaultUnit);
164 input->setText(locLengthString(toqstr(len)));
166 lengthToWidgets(input, combo, Length(len), defaultUnit);
171 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
172 docstring const & len, Length::UNIT defaultUnit)
174 lengthToWidgets(input, combo, to_utf8(len), defaultUnit);
178 double widgetToDouble(QLineEdit const * input)
180 QString const text = input->text();
184 return locstringToDouble(text.trimmed());
188 string widgetToDoubleStr(QLineEdit const * input)
190 return convert<string>(widgetToDouble(input));
194 void doubleToWidget(QLineEdit * input, double value, char f, int prec)
197 loc.setNumberOptions(QLocale::OmitGroupSeparator);
198 input->setText(loc.toString(value, f, prec));
202 void doubleToWidget(QLineEdit * input, string const & value, char f, int prec)
204 doubleToWidget(input, convert<double>(value), f, prec);
208 QString formatLocFPNumber(double d)
210 QString result = toqstr(formatFPNumber(d));
212 result.replace('.', loc.decimalPoint());
217 bool SortLocaleAware(QString const & lhs, QString const & rhs)
219 return QString::localeAwareCompare(lhs, rhs) < 0;
223 bool ColorSorter(ColorCode lhs, ColorCode rhs)
225 return compare_no_case(lcolor.getGUIName(lhs), lcolor.getGUIName(rhs)) < 0;
229 void setValid(QWidget * widget, bool valid)
232 if (qobject_cast<QCheckBox*>(widget) != nullptr)
233 // Check boxes need to be treated differenty, see
234 // https://forum.qt.io/topic/93253/
235 widget->setStyleSheet("");
237 widget->setPalette(QPalette());
239 if (qobject_cast<QCheckBox*>(widget) != nullptr) {
240 // Check boxes need to be treated differenty, see
241 // https://forum.qt.io/topic/93253/
242 widget->setStyleSheet("QCheckBox:unchecked{ color: red; }QCheckBox:checked{ color: red; }");
244 QPalette pal = widget->palette();
245 pal.setColor(QPalette::Active, QPalette::WindowText, QColor(255, 0, 0));
246 widget->setPalette(pal);
252 void focusAndHighlight(QAbstractItemView * w)
255 w->setCurrentIndex(w->currentIndex());
256 w->scrollTo(w->currentIndex());
260 void setMessageColour(list<QWidget *> highlighted, list<QWidget *> plain)
262 QPalette pal = QApplication::palette();
263 QPalette newpal(pal.color(QPalette::Active, QPalette::HighlightedText),
264 pal.color(QPalette::Active, QPalette::Highlight));
265 for (QWidget * w : highlighted)
266 w->setPalette(newpal);
267 for (QWidget * w : plain)
272 void showDirectory(FileName const & directory)
274 if (!directory.exists())
276 QUrl qurl(QUrl::fromLocalFile(QDir::toNativeSeparators(toqstr(directory.absFileName()))));
277 // Give hints in case of bugs
278 if (!qurl.isValid()) {
279 frontend::Alert::error(_("Invalid URL"),
280 bformat(_("The URL `%1$s' could not be resolved."),
281 qstring_to_ucs4(qurl.toString())));
285 if (!QDesktopServices::openUrl(qurl))
286 frontend::Alert::error(_("URL could not be accessed"),
287 bformat(_("The URL `%1$s' could not be opened although it exists!"),
288 qstring_to_ucs4(qurl.toString())));
291 void showTarget(string const & target, string const & pdfv, string const & psv)
293 LYXERR(Debug::INSETS, "Showtarget:" << target << "\n");
294 if (prefixIs(target, "EXTERNAL ")) {
295 if (!lyxrc.citation_search)
297 string tmp, tar, opts;
298 tar = split(target, tmp, ' ');
300 opts = " -v " + pdfv;
302 opts += " -w " + psv;
306 string const command = lyxrc.citation_search_view + " " + opts + tar;
307 int const result = one.startscript(Systemcall::Wait, command);
310 frontend::Alert::error(_("Could not open file"),
311 _("The lyxpaperview script failed."));
312 else if (result == 2)
313 frontend::Alert::error(_("Could not open file"),
314 bformat(_("No file was found using the pattern `%1$s'."),
318 if (!QDesktopServices::openUrl(QUrl(toqstr(target), QUrl::TolerantMode)))
319 frontend::Alert::error(_("Could not open file"),
320 bformat(_("The target `%1$s' could not be resolved."),
323 } // namespace frontend
325 QString const qt_(char const * str, const char *)
327 return toqstr(_(str));
331 QString const qt_(string const & str)
333 return toqstr(_(str));
337 QString const qt_(QString const & qstr)
339 return toqstr(_(fromqstr(qstr)));
343 void rescanTexStyles(string const & arg)
345 // Run rescan in user lyx directory
346 PathChanger p(package().user_support());
347 FileName const prog = support::libFileSearch("scripts", "TeXFiles.py");
349 string const command = os::python() + ' ' +
350 quoteName(prog.toFilesystemEncoding()) + ' ' +
352 int const status = one.startscript(Systemcall::Wait, command);
356 frontend::Alert::error(_("Could not update TeX information"),
357 bformat(_("The script `%1$s' failed."), from_utf8(prog.absFileName())));
361 QStringList texFileList(QString const & filename)
364 FileName const file = libFileSearch(QString(), filename);
369 vector<docstring> doclist =
370 getVectorFromString(file.fileContents("UTF-8"), from_ascii("\n"));
372 // Normalise paths like /foo//bar ==> /foo/bar
374 for (size_t i = 0; i != doclist.size(); ++i) {
375 QString qfile = toqstr(doclist[i]);
376 qfile.replace("\r", "");
377 while (qfile.contains("//"))
378 qfile.replace("//", "/");
379 if (!qfile.isEmpty())
384 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
385 return QList<QString>(set.begin(), set.end());
387 return QList<QString>::fromSet(set);
391 QString const externalLineEnding(docstring const & str)
394 // The MAC clipboard uses \r for lineendings, and we use \n
395 return toqstr(subst(str, '\n', '\r'));
396 #elif defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN)
397 // Windows clipboard uses \r\n for lineendings, and we use \n
398 return toqstr(subst(str, from_ascii("\n"), from_ascii("\r\n")));
405 docstring const internalLineEnding(QString const & str)
407 docstring const s = subst(qstring_to_ucs4(str),
408 from_ascii("\r\n"), from_ascii("\n"));
409 return subst(s, '\r', '\n');
413 QString internalPath(const QString & str)
415 return toqstr(os::internal_path(fromqstr(str)));
419 QString onlyFileName(const QString & str)
421 return toqstr(support::onlyFileName(fromqstr(str)));
425 QString onlyPath(const QString & str)
427 return toqstr(support::onlyPath(fromqstr(str)));
431 QString changeExtension(QString const & oldname, QString const & ext)
433 return toqstr(support::changeExtension(fromqstr(oldname), fromqstr(ext)));
436 /// Remove the extension from \p name
437 QString removeExtension(QString const & name)
439 return toqstr(support::removeExtension(fromqstr(name)));
442 /** Add the extension \p ext to \p name.
443 Use this instead of changeExtension if you know that \p name is without
444 extension, because changeExtension would wrongly interpret \p name if it
447 QString addExtension(QString const & name, QString const & ext)
449 return toqstr(support::addExtension(fromqstr(name), fromqstr(ext)));
452 /// Return the extension of the file (not including the .)
453 QString getExtension(QString const & name)
455 return toqstr(support::getExtension(fromqstr(name)));
459 /** Convert relative path into absolute path based on a basepath.
460 If relpath is absolute, just use that.
461 If basepath doesn't exist use CWD.
463 QString makeAbsPath(QString const & relpath, QString const & base)
465 return toqstr(support::makeAbsPath(fromqstr(relpath),
466 fromqstr(base)).absFileName());
470 /////////////////////////////////////////////////////////////////////////
474 /////////////////////////////////////////////////////////////////////////
476 /** Given a string such as
477 * "<glob> <glob> ... *.{abc,def} <glob>",
478 * convert the csh-style brace expressions:
479 * "<glob> <glob> ... *.abc *.def <glob>".
480 * Requires no system support, so should work equally on Unix, Mac, Win32.
482 static string const convert_brace_glob(string const & glob)
484 // Matches " *.{abc,def,ghi}", storing "*." as group 1 and
485 // "abc,def,ghi" as group 2, while allowing spaces in group 2.
486 static regex const glob_re(" *([^ {]*)\\{([^}]+)\\}");
487 // Matches "abc" and "abc,", storing "abc" as group 1,
488 // while ignoring surrounding spaces.
489 static regex const block_re(" *([^ ,}]+) *,? *");
493 string::const_iterator it = glob.begin();
494 string::const_iterator const end = glob.end();
496 match_results<string::const_iterator> what;
497 if (!regex_search(it, end, what, glob_re)) {
498 // Ensure that no information is lost.
499 pattern += string(it, end);
503 // Everything from the start of the input to
504 // the start of the match.
505 pattern += string(what[-1].first, what[-1].second);
507 // Given " *.{abc,def}", head == "*." and tail == "abc,def".
508 string const head = string(what[1].first, what[1].second);
509 string const tail = string(what[2].first, what[2].second);
511 // Split the ','-separated chunks of tail so that
512 // $head{$chunk1,$chunk2} becomes "$head$chunk1 $head$chunk2".
513 string const fmt = " " + head + "$1";
514 pattern += regex_replace(tail, block_re, fmt);
516 // Increment the iterator to the end of the match.
517 it += distance(it, what[0].second);
526 /* \param description text describing the filters.
527 * \param one or more wildcard patterns, separated by
530 Filter(docstring const & description, std::string const & globs);
532 docstring const & description() const { return desc_; }
534 QString toString() const;
537 std::vector<std::string> globs_;
541 Filter::Filter(docstring const & description, string const & globs)
544 // Given "<glob> <glob> ... *.{abc,def} <glob>", expand to
545 // "<glob> <glob> ... *.abc *.def <glob>"
546 string const expanded_globs = convert_brace_glob(globs);
548 // Split into individual globs.
549 globs_ = getVectorFromString(expanded_globs, " ");
553 QString Filter::toString() const
557 bool const has_description = !desc_.empty();
559 if (has_description) {
564 s += toqstr(getStringFromVector(globs_, " "));
572 /** \c FileFilterList parses a Qt-style list of available file filters
573 * to generate the corresponding vector.
574 * For example "TeX documents (*.tex);;LyX Documents (*.lyx)"
575 * will be parsed to fill a vector of size 2, whilst "*.{p[bgp]m} *.pdf"
576 * will result in a vector of size 1 in which the description field is empty.
578 struct FileFilterList
580 // FIXME UNICODE: globs_ should be unicode...
581 /** \param qt_style_filter a list of available file filters.
582 * Eg. "TeX documents (*.tex);;LyX Documents (*.lyx)".
583 * The "All files (*)" filter is always added to the list.
585 explicit FileFilterList(docstring const & qt_style_filter =
588 typedef std::vector<Filter>::size_type size_type;
590 bool empty() const { return filters_.empty(); }
591 size_type size() const { return filters_.size(); }
592 Filter & operator[](size_type i) { return filters_[i]; }
593 Filter const & operator[](size_type i) const { return filters_[i]; }
595 void parse_filter(std::string const & filter);
596 std::vector<Filter> filters_;
600 FileFilterList::FileFilterList(docstring const & qt_style_filter)
603 string const filter = to_utf8(qt_style_filter)
604 + (qt_style_filter.empty() ? string() : ";;")
605 + to_utf8(_("All Files "))
612 // Split data such as "TeX documents (*.tex);;LyX Documents (*.lyx)"
613 // into individual filters.
614 static regex const separator_re(";;");
616 string::const_iterator it = filter.begin();
617 string::const_iterator const end = filter.end();
619 match_results<string::const_iterator> what;
621 if (!regex_search(it, end, what, separator_re)) {
622 parse_filter(string(it, end));
626 // Everything from the start of the input to
627 // the start of the match.
628 parse_filter(string(it, what[0].first));
630 // Increment the iterator to the end of the match.
631 it += distance(it, what[0].second);
636 void FileFilterList::parse_filter(string const & filter)
638 // Matches "TeX documents (plain) (*.tex)",
639 // storing "TeX documents (plain) " as group 1 and "*.tex" as group 2.
640 static regex const filter_re("(.*)\\(([^()]+)\\) *$");
642 match_results<string::const_iterator> what;
643 if (!regex_search(filter, what, filter_re)) {
644 // Just a glob, no description.
645 filters_.push_back(Filter(docstring(), trim(filter)));
648 docstring const desc = from_utf8(string(what[1].first, what[1].second));
649 string const globs = string(what[2].first, what[2].second);
650 filters_.push_back(Filter(trim(desc), trim(globs)));
655 /** \returns the equivalent of the string passed in
656 * although any brace expressions are expanded.
657 * (E.g. "*.{png,jpg}" -> "*.png *.jpg")
659 QStringList fileFilters(QString const & desc)
661 // we have: "*.{gif,png,jpg,bmp,pbm,ppm,tga,tif,xpm,xbm}"
662 // but need: "*.cpp;*.cc;*.C;*.cxx;*.c++"
663 FileFilterList filters(qstring_to_ucs4(desc));
664 //LYXERR0("DESC: " << desc);
666 for (size_t i = 0; i != filters.filters_.size(); ++i) {
667 QString f = filters.filters_[i].toString();
668 //LYXERR0("FILTER: " << f);
675 QString formatToolTip(QString text, int width)
677 // 1. QTooltip activates word wrapping only if mightBeRichText()
678 // is true. So we convert the text to rich text.
680 // 2. The default width is way too small. Setting the width is tricky; first
681 // one has to compute the ideal width, and then force it with special
684 // do nothing if empty or already formatted
685 if (text.isEmpty() || text.startsWith(QString("<html>")))
687 // Convert to rich text if it is not already
688 if (!Qt::mightBeRichText(text))
689 text = Qt::convertFromPlainText(text, Qt::WhiteSpaceNormal);
690 // Compute desired width in pixels
691 QFont const font = QToolTip::font();
692 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
693 int const px_width = width * QFontMetrics(font).horizontalAdvance("M");
695 int const px_width = width * QFontMetrics(font).width("M");
697 // Determine the ideal width of the tooltip
698 QTextDocument td("");
700 td.setDefaultFont(QToolTip::font());
701 td.setDocumentMargin(0);
702 td.setTextWidth(px_width);
703 double best_width = td.idealWidth();
704 // Set the line wrapping with appropriate width
705 return QString("<html><body><table><tr>"
706 "<td align=justify width=%1>%2</td>"
707 "</tr></table></body></html>")
708 .arg(QString::number(int(best_width) + 1), text);
712 QString qtHtmlToPlainText(QString const & text)
714 if (!Qt::mightBeRichText(text))
718 return td.toPlainText();