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 QString locLengthString(QString const & str)
221 return res.replace(QString("."), loc.decimalPoint());
225 QString unlocLengthString(QString const & str)
229 return res.replace(loc.decimalPoint(), QString("."));
233 bool SortLocaleAware(QString const & lhs, QString const & rhs)
235 return QString::localeAwareCompare(lhs, rhs) < 0;
239 bool ColorSorter(ColorCode lhs, ColorCode rhs)
241 return compare_no_case(lcolor.getGUIName(lhs), lcolor.getGUIName(rhs)) < 0;
245 void setValid(QWidget * widget, bool valid)
248 if (qobject_cast<QCheckBox*>(widget) != nullptr)
249 // Check boxes need to be treated differenty, see
250 // https://forum.qt.io/topic/93253/
251 widget->setStyleSheet("");
253 widget->setPalette(QPalette());
255 if (qobject_cast<QCheckBox*>(widget) != nullptr) {
256 // Check boxes need to be treated differenty, see
257 // https://forum.qt.io/topic/93253/
258 widget->setStyleSheet("QCheckBox:unchecked{ color: red; }QCheckBox:checked{ color: red; }");
260 QPalette pal = widget->palette();
261 pal.setColor(QPalette::Active, QPalette::WindowText, QColor(255, 0, 0));
262 widget->setPalette(pal);
268 void focusAndHighlight(QAbstractItemView * w)
271 w->setCurrentIndex(w->currentIndex());
272 w->scrollTo(w->currentIndex());
276 void setMessageColour(list<QWidget *> highlighted, list<QWidget *> plain)
278 QPalette pal = QApplication::palette();
279 QPalette newpal(pal.color(QPalette::Active, QPalette::HighlightedText),
280 pal.color(QPalette::Active, QPalette::Highlight));
281 for (QWidget * w : highlighted)
282 w->setPalette(newpal);
283 for (QWidget * w : plain)
288 /// wrapper to hide the change of method name to setSectionResizeMode
289 void setSectionResizeMode(QHeaderView * view,
290 int logicalIndex, QHeaderView::ResizeMode mode) {
291 #if (QT_VERSION >= 0x050000)
292 view->setSectionResizeMode(logicalIndex, mode);
294 view->setResizeMode(logicalIndex, mode);
298 void setSectionResizeMode(QHeaderView * view, QHeaderView::ResizeMode mode) {
299 #if (QT_VERSION >= 0x050000)
300 view->setSectionResizeMode(mode);
302 view->setResizeMode(mode);
306 void showDirectory(FileName const & directory)
308 if (!directory.exists())
310 QUrl qurl(QUrl::fromLocalFile(QDir::toNativeSeparators(toqstr(directory.absFileName()))));
311 // Give hints in case of bugs
312 if (!qurl.isValid()) {
313 frontend::Alert::error(_("Invalid URL"),
314 bformat(_("The URL `%1$s' could not be resolved."),
315 qstring_to_ucs4(qurl.toString())));
319 if (!QDesktopServices::openUrl(qurl))
320 frontend::Alert::error(_("URL could not be accessed"),
321 bformat(_("The URL `%1$s' could not be opened although it exists!"),
322 qstring_to_ucs4(qurl.toString())));
325 void showTarget(string const & target, string const & pdfv, string const & psv)
327 LYXERR(Debug::INSETS, "Showtarget:" << target << "\n");
328 if (prefixIs(target, "EXTERNAL ")) {
329 if (!lyxrc.citation_search)
331 string tmp, tar, opts;
332 tar = split(target, tmp, ' ');
334 opts = " -v " + pdfv;
336 opts += " -w " + psv;
340 string const command = lyxrc.citation_search_view + " " + opts + tar;
341 int const result = one.startscript(Systemcall::Wait, command);
344 frontend::Alert::error(_("Could not open file"),
345 _("The lyxpaperview script failed."));
346 else if (result == 2)
347 frontend::Alert::error(_("Could not open file"),
348 bformat(_("No file was found using the pattern `%1$s'."),
352 if (!QDesktopServices::openUrl(QUrl(toqstr(target), QUrl::TolerantMode)))
353 frontend::Alert::error(_("Could not open file"),
354 bformat(_("The target `%1$s' could not be resolved."),
357 } // namespace frontend
359 QString const qt_(char const * str, const char *)
361 return toqstr(_(str));
365 QString const qt_(string const & str)
367 return toqstr(_(str));
371 QString const qt_(QString const & qstr)
373 return toqstr(_(fromqstr(qstr)));
377 void rescanTexStyles(string const & arg)
379 // Run rescan in user lyx directory
380 PathChanger p(package().user_support());
381 FileName const prog = support::libFileSearch("scripts", "TeXFiles.py");
383 string const command = os::python() + ' ' +
384 quoteName(prog.toFilesystemEncoding()) + ' ' +
386 int const status = one.startscript(Systemcall::Wait, command);
390 frontend::Alert::error(_("Could not update TeX information"),
391 bformat(_("The script `%1$s' failed."), from_utf8(prog.absFileName())));
395 QStringList texFileList(QString const & filename)
398 FileName const file = libFileSearch(QString(), filename);
403 vector<docstring> doclist =
404 getVectorFromString(file.fileContents("UTF-8"), from_ascii("\n"));
406 // Normalise paths like /foo//bar ==> /foo/bar
408 for (size_t i = 0; i != doclist.size(); ++i) {
409 QString qfile = toqstr(doclist[i]);
410 qfile.replace("\r", "");
411 while (qfile.contains("//"))
412 qfile.replace("//", "/");
413 if (!qfile.isEmpty())
418 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
419 return QList<QString>(set.begin(), set.end());
421 return QList<QString>::fromSet(set);
425 QString const externalLineEnding(docstring const & str)
428 // The MAC clipboard uses \r for lineendings, and we use \n
429 return toqstr(subst(str, '\n', '\r'));
430 #elif defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN)
431 // Windows clipboard uses \r\n for lineendings, and we use \n
432 return toqstr(subst(str, from_ascii("\n"), from_ascii("\r\n")));
439 docstring const internalLineEnding(QString const & str)
441 docstring const s = subst(qstring_to_ucs4(str),
442 from_ascii("\r\n"), from_ascii("\n"));
443 return subst(s, '\r', '\n');
447 QString internalPath(const QString & str)
449 return toqstr(os::internal_path(fromqstr(str)));
453 QString onlyFileName(const QString & str)
455 return toqstr(support::onlyFileName(fromqstr(str)));
459 QString onlyPath(const QString & str)
461 return toqstr(support::onlyPath(fromqstr(str)));
465 QString changeExtension(QString const & oldname, QString const & ext)
467 return toqstr(support::changeExtension(fromqstr(oldname), fromqstr(ext)));
470 /// Remove the extension from \p name
471 QString removeExtension(QString const & name)
473 return toqstr(support::removeExtension(fromqstr(name)));
476 /** Add the extension \p ext to \p name.
477 Use this instead of changeExtension if you know that \p name is without
478 extension, because changeExtension would wrongly interpret \p name if it
481 QString addExtension(QString const & name, QString const & ext)
483 return toqstr(support::addExtension(fromqstr(name), fromqstr(ext)));
486 /// Return the extension of the file (not including the .)
487 QString getExtension(QString const & name)
489 return toqstr(support::getExtension(fromqstr(name)));
493 /** Convert relative path into absolute path based on a basepath.
494 If relpath is absolute, just use that.
495 If basepath doesn't exist use CWD.
497 QString makeAbsPath(QString const & relpath, QString const & base)
499 return toqstr(support::makeAbsPath(fromqstr(relpath),
500 fromqstr(base)).absFileName());
504 /////////////////////////////////////////////////////////////////////////
508 /////////////////////////////////////////////////////////////////////////
510 /** Given a string such as
511 * "<glob> <glob> ... *.{abc,def} <glob>",
512 * convert the csh-style brace expressions:
513 * "<glob> <glob> ... *.abc *.def <glob>".
514 * Requires no system support, so should work equally on Unix, Mac, Win32.
516 static string const convert_brace_glob(string const & glob)
518 // Matches " *.{abc,def,ghi}", storing "*." as group 1 and
519 // "abc,def,ghi" as group 2, while allowing spaces in group 2.
520 static regex const glob_re(" *([^ {]*)\\{([^}]+)\\}");
521 // Matches "abc" and "abc,", storing "abc" as group 1,
522 // while ignoring surrounding spaces.
523 static regex const block_re(" *([^ ,}]+) *,? *");
527 string::const_iterator it = glob.begin();
528 string::const_iterator const end = glob.end();
530 match_results<string::const_iterator> what;
531 if (!regex_search(it, end, what, glob_re)) {
532 // Ensure that no information is lost.
533 pattern += string(it, end);
537 // Everything from the start of the input to
538 // the start of the match.
539 pattern += string(what[-1].first, what[-1].second);
541 // Given " *.{abc,def}", head == "*." and tail == "abc,def".
542 string const head = string(what[1].first, what[1].second);
543 string const tail = string(what[2].first, what[2].second);
545 // Split the ','-separated chunks of tail so that
546 // $head{$chunk1,$chunk2} becomes "$head$chunk1 $head$chunk2".
547 string const fmt = " " + head + "$1";
548 pattern += regex_replace(tail, block_re, fmt);
550 // Increment the iterator to the end of the match.
551 it += distance(it, what[0].second);
560 /* \param description text describing the filters.
561 * \param one or more wildcard patterns, separated by
564 Filter(docstring const & description, std::string const & globs);
566 docstring const & description() const { return desc_; }
568 QString toString() const;
571 std::vector<std::string> globs_;
575 Filter::Filter(docstring const & description, string const & globs)
578 // Given "<glob> <glob> ... *.{abc,def} <glob>", expand to
579 // "<glob> <glob> ... *.abc *.def <glob>"
580 string const expanded_globs = convert_brace_glob(globs);
582 // Split into individual globs.
583 globs_ = getVectorFromString(expanded_globs, " ");
587 QString Filter::toString() const
591 bool const has_description = !desc_.empty();
593 if (has_description) {
598 s += toqstr(getStringFromVector(globs_, " "));
606 /** \c FileFilterList parses a Qt-style list of available file filters
607 * to generate the corresponding vector.
608 * For example "TeX documents (*.tex);;LyX Documents (*.lyx)"
609 * will be parsed to fill a vector of size 2, whilst "*.{p[bgp]m} *.pdf"
610 * will result in a vector of size 1 in which the description field is empty.
612 struct FileFilterList
614 // FIXME UNICODE: globs_ should be unicode...
615 /** \param qt_style_filter a list of available file filters.
616 * Eg. "TeX documents (*.tex);;LyX Documents (*.lyx)".
617 * The "All files (*)" filter is always added to the list.
619 explicit FileFilterList(docstring const & qt_style_filter =
622 typedef std::vector<Filter>::size_type size_type;
624 bool empty() const { return filters_.empty(); }
625 size_type size() const { return filters_.size(); }
626 Filter & operator[](size_type i) { return filters_[i]; }
627 Filter const & operator[](size_type i) const { return filters_[i]; }
629 void parse_filter(std::string const & filter);
630 std::vector<Filter> filters_;
634 FileFilterList::FileFilterList(docstring const & qt_style_filter)
637 string const filter = to_utf8(qt_style_filter)
638 + (qt_style_filter.empty() ? string() : ";;")
639 + to_utf8(_("All Files "))
646 // Split data such as "TeX documents (*.tex);;LyX Documents (*.lyx)"
647 // into individual filters.
648 static regex const separator_re(";;");
650 string::const_iterator it = filter.begin();
651 string::const_iterator const end = filter.end();
653 match_results<string::const_iterator> what;
655 if (!regex_search(it, end, what, separator_re)) {
656 parse_filter(string(it, end));
660 // Everything from the start of the input to
661 // the start of the match.
662 parse_filter(string(it, what[0].first));
664 // Increment the iterator to the end of the match.
665 it += distance(it, what[0].second);
670 void FileFilterList::parse_filter(string const & filter)
672 // Matches "TeX documents (plain) (*.tex)",
673 // storing "TeX documents (plain) " as group 1 and "*.tex" as group 2.
674 static regex const filter_re("(.*)\\(([^()]+)\\) *$");
676 match_results<string::const_iterator> what;
677 if (!regex_search(filter, what, filter_re)) {
678 // Just a glob, no description.
679 filters_.push_back(Filter(docstring(), trim(filter)));
682 docstring const desc = from_utf8(string(what[1].first, what[1].second));
683 string const globs = string(what[2].first, what[2].second);
684 filters_.push_back(Filter(trim(desc), trim(globs)));
689 /** \returns the equivalent of the string passed in
690 * although any brace expressions are expanded.
691 * (E.g. "*.{png,jpg}" -> "*.png *.jpg")
693 QStringList fileFilters(QString const & desc)
695 // we have: "*.{gif,png,jpg,bmp,pbm,ppm,tga,tif,xpm,xbm}"
696 // but need: "*.cpp;*.cc;*.C;*.cxx;*.c++"
697 FileFilterList filters(qstring_to_ucs4(desc));
698 //LYXERR0("DESC: " << desc);
700 for (size_t i = 0; i != filters.filters_.size(); ++i) {
701 QString f = filters.filters_[i].toString();
702 //LYXERR0("FILTER: " << f);
709 QString formatToolTip(QString text, int width)
711 // 1. QTooltip activates word wrapping only if mightBeRichText()
712 // is true. So we convert the text to rich text.
714 // 2. The default width is way too small. Setting the width is tricky; first
715 // one has to compute the ideal width, and then force it with special
718 // do nothing if empty or already formatted
719 if (text.isEmpty() || text.startsWith(QString("<html>")))
721 // Convert to rich text if it is not already
722 if (!Qt::mightBeRichText(text))
723 text = Qt::convertFromPlainText(text, Qt::WhiteSpaceNormal);
724 // Compute desired width in pixels
725 QFont const font = QToolTip::font();
726 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
727 int const px_width = width * QFontMetrics(font).horizontalAdvance("M");
729 int const px_width = width * QFontMetrics(font).width("M");
731 // Determine the ideal width of the tooltip
732 QTextDocument td("");
734 td.setDefaultFont(QToolTip::font());
735 td.setDocumentMargin(0);
736 td.setTextWidth(px_width);
737 double best_width = td.idealWidth();
738 // Set the line wrapping with appropriate width
739 return QString("<html><body><table><tr>"
740 "<td align=justify width=%1>%2</td>"
741 "</tr></table></body></html>")
742 .arg(QString::number(int(best_width) + 1), text);
746 QString qtHtmlToPlainText(QString const & text)
748 if (!Qt::mightBeRichText(text))
752 return td.toPlainText();