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(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 unlocLengthString(QString const & str)
221 return res.replace(loc.decimalPoint(), QString("."));
225 bool SortLocaleAware(QString const & lhs, QString const & rhs)
227 return QString::localeAwareCompare(lhs, rhs) < 0;
231 bool ColorSorter(ColorCode lhs, ColorCode rhs)
233 return compare_no_case(lcolor.getGUIName(lhs), lcolor.getGUIName(rhs)) < 0;
237 void setValid(QWidget * widget, bool valid)
240 if (qobject_cast<QCheckBox*>(widget) != nullptr)
241 // Check boxes need to be treated differenty, see
242 // https://forum.qt.io/topic/93253/
243 widget->setStyleSheet("");
245 widget->setPalette(QPalette());
247 if (qobject_cast<QCheckBox*>(widget) != nullptr) {
248 // Check boxes need to be treated differenty, see
249 // https://forum.qt.io/topic/93253/
250 widget->setStyleSheet("QCheckBox:unchecked{ color: red; }QCheckBox:checked{ color: red; }");
252 QPalette pal = widget->palette();
253 pal.setColor(QPalette::Active, QPalette::WindowText, QColor(255, 0, 0));
254 widget->setPalette(pal);
260 void focusAndHighlight(QAbstractItemView * w)
263 w->setCurrentIndex(w->currentIndex());
264 w->scrollTo(w->currentIndex());
268 void setMessageColour(list<QWidget *> highlighted, list<QWidget *> plain)
270 QPalette pal = QApplication::palette();
271 QPalette newpal(pal.color(QPalette::Active, QPalette::HighlightedText),
272 pal.color(QPalette::Active, QPalette::Highlight));
273 for (QWidget * w : highlighted)
274 w->setPalette(newpal);
275 for (QWidget * w : plain)
280 /// wrapper to hide the change of method name to setSectionResizeMode
281 void setSectionResizeMode(QHeaderView * view,
282 int logicalIndex, QHeaderView::ResizeMode mode) {
283 #if (QT_VERSION >= 0x050000)
284 view->setSectionResizeMode(logicalIndex, mode);
286 view->setResizeMode(logicalIndex, mode);
290 void setSectionResizeMode(QHeaderView * view, QHeaderView::ResizeMode mode) {
291 #if (QT_VERSION >= 0x050000)
292 view->setSectionResizeMode(mode);
294 view->setResizeMode(mode);
298 void showDirectory(FileName const & directory)
300 if (!directory.exists())
302 QUrl qurl(QUrl::fromLocalFile(QDir::toNativeSeparators(toqstr(directory.absFileName()))));
303 // Give hints in case of bugs
304 if (!qurl.isValid()) {
305 frontend::Alert::error(_("Invalid URL"),
306 bformat(_("The URL `%1$s' could not be resolved."),
307 qstring_to_ucs4(qurl.toString())));
311 if (!QDesktopServices::openUrl(qurl))
312 frontend::Alert::error(_("URL could not be accessed"),
313 bformat(_("The URL `%1$s' could not be opened although it exists!"),
314 qstring_to_ucs4(qurl.toString())));
317 void showTarget(string const & target, string const & pdfv, string const & psv)
319 LYXERR(Debug::INSETS, "Showtarget:" << target << "\n");
320 if (prefixIs(target, "EXTERNAL ")) {
321 if (!lyxrc.citation_search)
323 string tmp, tar, opts;
324 tar = split(target, tmp, ' ');
326 opts = " -v " + pdfv;
328 opts += " -w " + psv;
332 string const command = lyxrc.citation_search_view + " " + opts + tar;
333 int const result = one.startscript(Systemcall::Wait, command);
336 frontend::Alert::error(_("Could not open file"),
337 _("The lyxpaperview script failed."));
338 else if (result == 2)
339 frontend::Alert::error(_("Could not open file"),
340 bformat(_("No file was found using the pattern `%1$s'."),
344 if (!QDesktopServices::openUrl(QUrl(toqstr(target), QUrl::TolerantMode)))
345 frontend::Alert::error(_("Could not open file"),
346 bformat(_("The target `%1$s' could not be resolved."),
349 } // namespace frontend
351 QString const qt_(char const * str, const char *)
353 return toqstr(_(str));
357 QString const qt_(string const & str)
359 return toqstr(_(str));
363 QString const qt_(QString const & qstr)
365 return toqstr(_(fromqstr(qstr)));
369 void rescanTexStyles(string const & arg)
371 // Run rescan in user lyx directory
372 PathChanger p(package().user_support());
373 FileName const prog = support::libFileSearch("scripts", "TeXFiles.py");
375 string const command = os::python() + ' ' +
376 quoteName(prog.toFilesystemEncoding()) + ' ' +
378 int const status = one.startscript(Systemcall::Wait, command);
382 frontend::Alert::error(_("Could not update TeX information"),
383 bformat(_("The script `%1$s' failed."), from_utf8(prog.absFileName())));
387 QStringList texFileList(QString const & filename)
390 FileName const file = libFileSearch(QString(), filename);
395 vector<docstring> doclist =
396 getVectorFromString(file.fileContents("UTF-8"), from_ascii("\n"));
398 // Normalise paths like /foo//bar ==> /foo/bar
400 for (size_t i = 0; i != doclist.size(); ++i) {
401 QString qfile = toqstr(doclist[i]);
402 qfile.replace("\r", "");
403 while (qfile.contains("//"))
404 qfile.replace("//", "/");
405 if (!qfile.isEmpty())
410 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
411 return QList<QString>(set.begin(), set.end());
413 return QList<QString>::fromSet(set);
417 QString const externalLineEnding(docstring const & str)
420 // The MAC clipboard uses \r for lineendings, and we use \n
421 return toqstr(subst(str, '\n', '\r'));
422 #elif defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN)
423 // Windows clipboard uses \r\n for lineendings, and we use \n
424 return toqstr(subst(str, from_ascii("\n"), from_ascii("\r\n")));
431 docstring const internalLineEnding(QString const & str)
433 docstring const s = subst(qstring_to_ucs4(str),
434 from_ascii("\r\n"), from_ascii("\n"));
435 return subst(s, '\r', '\n');
439 QString internalPath(const QString & str)
441 return toqstr(os::internal_path(fromqstr(str)));
445 QString onlyFileName(const QString & str)
447 return toqstr(support::onlyFileName(fromqstr(str)));
451 QString onlyPath(const QString & str)
453 return toqstr(support::onlyPath(fromqstr(str)));
457 QString changeExtension(QString const & oldname, QString const & ext)
459 return toqstr(support::changeExtension(fromqstr(oldname), fromqstr(ext)));
462 /// Remove the extension from \p name
463 QString removeExtension(QString const & name)
465 return toqstr(support::removeExtension(fromqstr(name)));
468 /** Add the extension \p ext to \p name.
469 Use this instead of changeExtension if you know that \p name is without
470 extension, because changeExtension would wrongly interpret \p name if it
473 QString addExtension(QString const & name, QString const & ext)
475 return toqstr(support::addExtension(fromqstr(name), fromqstr(ext)));
478 /// Return the extension of the file (not including the .)
479 QString getExtension(QString const & name)
481 return toqstr(support::getExtension(fromqstr(name)));
485 /** Convert relative path into absolute path based on a basepath.
486 If relpath is absolute, just use that.
487 If basepath doesn't exist use CWD.
489 QString makeAbsPath(QString const & relpath, QString const & base)
491 return toqstr(support::makeAbsPath(fromqstr(relpath),
492 fromqstr(base)).absFileName());
496 /////////////////////////////////////////////////////////////////////////
500 /////////////////////////////////////////////////////////////////////////
502 /** Given a string such as
503 * "<glob> <glob> ... *.{abc,def} <glob>",
504 * convert the csh-style brace expressions:
505 * "<glob> <glob> ... *.abc *.def <glob>".
506 * Requires no system support, so should work equally on Unix, Mac, Win32.
508 static string const convert_brace_glob(string const & glob)
510 // Matches " *.{abc,def,ghi}", storing "*." as group 1 and
511 // "abc,def,ghi" as group 2, while allowing spaces in group 2.
512 static regex const glob_re(" *([^ {]*)\\{([^}]+)\\}");
513 // Matches "abc" and "abc,", storing "abc" as group 1,
514 // while ignoring surrounding spaces.
515 static regex const block_re(" *([^ ,}]+) *,? *");
519 string::const_iterator it = glob.begin();
520 string::const_iterator const end = glob.end();
522 match_results<string::const_iterator> what;
523 if (!regex_search(it, end, what, glob_re)) {
524 // Ensure that no information is lost.
525 pattern += string(it, end);
529 // Everything from the start of the input to
530 // the start of the match.
531 pattern += string(what[-1].first, what[-1].second);
533 // Given " *.{abc,def}", head == "*." and tail == "abc,def".
534 string const head = string(what[1].first, what[1].second);
535 string const tail = string(what[2].first, what[2].second);
537 // Split the ','-separated chunks of tail so that
538 // $head{$chunk1,$chunk2} becomes "$head$chunk1 $head$chunk2".
539 string const fmt = " " + head + "$1";
540 pattern += regex_replace(tail, block_re, fmt);
542 // Increment the iterator to the end of the match.
543 it += distance(it, what[0].second);
552 /* \param description text describing the filters.
553 * \param one or more wildcard patterns, separated by
556 Filter(docstring const & description, std::string const & globs);
558 docstring const & description() const { return desc_; }
560 QString toString() const;
563 std::vector<std::string> globs_;
567 Filter::Filter(docstring const & description, string const & globs)
570 // Given "<glob> <glob> ... *.{abc,def} <glob>", expand to
571 // "<glob> <glob> ... *.abc *.def <glob>"
572 string const expanded_globs = convert_brace_glob(globs);
574 // Split into individual globs.
575 globs_ = getVectorFromString(expanded_globs, " ");
579 QString Filter::toString() const
583 bool const has_description = !desc_.empty();
585 if (has_description) {
590 s += toqstr(getStringFromVector(globs_, " "));
598 /** \c FileFilterList parses a Qt-style list of available file filters
599 * to generate the corresponding vector.
600 * For example "TeX documents (*.tex);;LyX Documents (*.lyx)"
601 * will be parsed to fill a vector of size 2, whilst "*.{p[bgp]m} *.pdf"
602 * will result in a vector of size 1 in which the description field is empty.
604 struct FileFilterList
606 // FIXME UNICODE: globs_ should be unicode...
607 /** \param qt_style_filter a list of available file filters.
608 * Eg. "TeX documents (*.tex);;LyX Documents (*.lyx)".
609 * The "All files (*)" filter is always added to the list.
611 explicit FileFilterList(docstring const & qt_style_filter =
614 typedef std::vector<Filter>::size_type size_type;
616 bool empty() const { return filters_.empty(); }
617 size_type size() const { return filters_.size(); }
618 Filter & operator[](size_type i) { return filters_[i]; }
619 Filter const & operator[](size_type i) const { return filters_[i]; }
621 void parse_filter(std::string const & filter);
622 std::vector<Filter> filters_;
626 FileFilterList::FileFilterList(docstring const & qt_style_filter)
629 string const filter = to_utf8(qt_style_filter)
630 + (qt_style_filter.empty() ? string() : ";;")
631 + to_utf8(_("All Files "))
638 // Split data such as "TeX documents (*.tex);;LyX Documents (*.lyx)"
639 // into individual filters.
640 static regex const separator_re(";;");
642 string::const_iterator it = filter.begin();
643 string::const_iterator const end = filter.end();
645 match_results<string::const_iterator> what;
647 if (!regex_search(it, end, what, separator_re)) {
648 parse_filter(string(it, end));
652 // Everything from the start of the input to
653 // the start of the match.
654 parse_filter(string(it, what[0].first));
656 // Increment the iterator to the end of the match.
657 it += distance(it, what[0].second);
662 void FileFilterList::parse_filter(string const & filter)
664 // Matches "TeX documents (plain) (*.tex)",
665 // storing "TeX documents (plain) " as group 1 and "*.tex" as group 2.
666 static regex const filter_re("(.*)\\(([^()]+)\\) *$");
668 match_results<string::const_iterator> what;
669 if (!regex_search(filter, what, filter_re)) {
670 // Just a glob, no description.
671 filters_.push_back(Filter(docstring(), trim(filter)));
674 docstring const desc = from_utf8(string(what[1].first, what[1].second));
675 string const globs = string(what[2].first, what[2].second);
676 filters_.push_back(Filter(trim(desc), trim(globs)));
681 /** \returns the equivalent of the string passed in
682 * although any brace expressions are expanded.
683 * (E.g. "*.{png,jpg}" -> "*.png *.jpg")
685 QStringList fileFilters(QString const & desc)
687 // we have: "*.{gif,png,jpg,bmp,pbm,ppm,tga,tif,xpm,xbm}"
688 // but need: "*.cpp;*.cc;*.C;*.cxx;*.c++"
689 FileFilterList filters(qstring_to_ucs4(desc));
690 //LYXERR0("DESC: " << desc);
692 for (size_t i = 0; i != filters.filters_.size(); ++i) {
693 QString f = filters.filters_[i].toString();
694 //LYXERR0("FILTER: " << f);
701 QString formatToolTip(QString text, int width)
703 // 1. QTooltip activates word wrapping only if mightBeRichText()
704 // is true. So we convert the text to rich text.
706 // 2. The default width is way too small. Setting the width is tricky; first
707 // one has to compute the ideal width, and then force it with special
710 // do nothing if empty or already formatted
711 if (text.isEmpty() || text.startsWith(QString("<html>")))
713 // Convert to rich text if it is not already
714 if (!Qt::mightBeRichText(text))
715 text = Qt::convertFromPlainText(text, Qt::WhiteSpaceNormal);
716 // Compute desired width in pixels
717 QFont const font = QToolTip::font();
718 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
719 int const px_width = width * QFontMetrics(font).horizontalAdvance("M");
721 int const px_width = width * QFontMetrics(font).width("M");
723 // Determine the ideal width of the tooltip
724 QTextDocument td("");
726 td.setDefaultFont(QToolTip::font());
727 td.setDocumentMargin(0);
728 td.setTextWidth(px_width);
729 double best_width = td.idealWidth();
730 // Set the line wrapping with appropriate width
731 return QString("<html><body><table><tr>"
732 "<td align=justify width=%1>%2</td>"
733 "</tr></table></body></html>")
734 .arg(QString::number(int(best_width) + 1), text);
738 QString qtHtmlToPlainText(QString const & text)
740 if (!Qt::mightBeRichText(text))
744 return td.toPlainText();