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 if (qobject_cast<QCheckBox*>(widget)->isChecked())
243 widget->setStyleSheet("QCheckBox:unchecked{ color: red; }QCheckBox:checked{ color: red; }");
245 QPalette pal = widget->palette();
246 pal.setColor(QPalette::Active, QPalette::WindowText, QColor(255, 0, 0));
247 pal.setColor(QPalette::Active, QPalette::Text, QColor(255, 0, 0));
248 widget->setPalette(pal);
254 void focusAndHighlight(QAbstractItemView * w)
257 w->setCurrentIndex(w->currentIndex());
258 w->scrollTo(w->currentIndex());
262 void setMessageColour(list<QWidget *> highlighted, list<QWidget *> plain)
264 QPalette pal = QApplication::palette();
265 QPalette newpal(pal.color(QPalette::Active, QPalette::HighlightedText),
266 pal.color(QPalette::Active, QPalette::Highlight));
267 newpal.setColor(QPalette::WindowText,
268 pal.color(QPalette::Active, QPalette::HighlightedText));
269 for (QWidget * w : highlighted)
270 w->setPalette(newpal);
271 for (QWidget * w : plain)
276 void showDirectory(FileName const & directory)
278 if (!directory.exists())
280 QUrl qurl(QUrl::fromLocalFile(QDir::toNativeSeparators(toqstr(directory.absFileName()))));
281 // Give hints in case of bugs
282 if (!qurl.isValid()) {
283 frontend::Alert::error(_("Invalid URL"),
284 bformat(_("The URL `%1$s' could not be resolved."),
285 qstring_to_ucs4(qurl.toString())));
289 if (!QDesktopServices::openUrl(qurl))
290 frontend::Alert::error(_("URL could not be accessed"),
291 bformat(_("The URL `%1$s' could not be opened although it exists!"),
292 qstring_to_ucs4(qurl.toString())));
295 void showTarget(string const & target, string const & pdfv, string const & psv)
297 LYXERR(Debug::INSETS, "Showtarget:" << target << "\n");
298 if (prefixIs(target, "EXTERNAL ")) {
299 if (!lyxrc.citation_search)
301 string tmp, tar, opts;
302 tar = split(target, tmp, ' ');
304 opts = " -v \"" + pdfv + "\"";
306 opts += " -w \"" + psv + "\"";
310 string const viewer = subst(lyxrc.citation_search_view, "$${python}", os::python());
311 string const command = viewer + " " + opts + tar;
312 int const result = one.startscript(Systemcall::Wait, command);
315 frontend::Alert::error(_("Could not open file"),
316 _("The lyxpaperview script failed."));
317 else if (result == 2)
318 frontend::Alert::error(_("Could not open file"),
319 bformat(_("No file was found using the pattern `%1$s'."),
323 if (!QDesktopServices::openUrl(QUrl(toqstr(target), QUrl::TolerantMode)))
324 frontend::Alert::error(_("Could not open file"),
325 bformat(_("The target `%1$s' could not be resolved."),
328 } // namespace frontend
330 QString const qt_(char const * str, const char *)
332 return toqstr(_(str));
336 QString const qt_(string const & str)
338 return toqstr(_(str));
342 QString const qt_(QString const & qstr)
344 return toqstr(_(fromqstr(qstr)));
348 void rescanTexStyles(string const & arg)
350 // Run rescan in user lyx directory
351 PathChanger p(package().user_support());
352 FileName const prog = support::libFileSearch("scripts", "TeXFiles.py");
354 string const command = os::python() + ' ' +
355 quoteName(prog.toFilesystemEncoding()) + ' ' +
357 int const status = one.startscript(Systemcall::Wait, command);
361 frontend::Alert::error(_("Could not update TeX information"),
362 bformat(_("The script `%1$s' failed."), from_utf8(prog.absFileName())));
366 QStringList texFileList(QString const & filename)
369 FileName const file = libFileSearch(QString(), filename);
374 vector<docstring> doclist =
375 getVectorFromString(file.fileContents("UTF-8"), from_ascii("\n"));
377 // Normalise paths like /foo//bar ==> /foo/bar
379 for (size_t i = 0; i != doclist.size(); ++i) {
380 QString qfile = toqstr(doclist[i]);
381 qfile.replace("\r", "");
382 while (qfile.contains("//"))
383 qfile.replace("//", "/");
384 if (!qfile.isEmpty())
389 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
390 return QList<QString>(set.begin(), set.end());
392 return QList<QString>::fromSet(set);
396 QString const externalLineEnding(docstring const & str)
399 // The MAC clipboard uses \r for lineendings, and we use \n
400 return toqstr(subst(str, '\n', '\r'));
401 #elif defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN)
402 // Windows clipboard uses \r\n for lineendings, and we use \n
403 return toqstr(subst(str, from_ascii("\n"), from_ascii("\r\n")));
410 docstring const internalLineEnding(QString const & str)
412 docstring const s = subst(qstring_to_ucs4(str),
413 from_ascii("\r\n"), from_ascii("\n"));
414 return subst(s, '\r', '\n');
418 QString internalPath(const QString & str)
420 return toqstr(os::internal_path(fromqstr(str)));
424 QString onlyFileName(const QString & str)
426 return toqstr(support::onlyFileName(fromqstr(str)));
430 QString onlyPath(const QString & str)
432 return toqstr(support::onlyPath(fromqstr(str)));
436 QString changeExtension(QString const & oldname, QString const & ext)
438 return toqstr(support::changeExtension(fromqstr(oldname), fromqstr(ext)));
441 /// Remove the extension from \p name
442 QString removeExtension(QString const & name)
444 return toqstr(support::removeExtension(fromqstr(name)));
447 /** Add the extension \p ext to \p name.
448 Use this instead of changeExtension if you know that \p name is without
449 extension, because changeExtension would wrongly interpret \p name if it
452 QString addExtension(QString const & name, QString const & ext)
454 return toqstr(support::addExtension(fromqstr(name), fromqstr(ext)));
457 /// Return the extension of the file (not including the .)
458 QString getExtension(QString const & name)
460 return toqstr(support::getExtension(fromqstr(name)));
464 /** Convert relative path into absolute path based on a basepath.
465 If relpath is absolute, just use that.
466 If basepath doesn't exist use CWD.
468 QString makeAbsPath(QString const & relpath, QString const & base)
470 return toqstr(support::makeAbsPath(fromqstr(relpath),
471 fromqstr(base)).absFileName());
475 /////////////////////////////////////////////////////////////////////////
479 /////////////////////////////////////////////////////////////////////////
481 /** Given a string such as
482 * "<glob> <glob> ... *.{abc,def} <glob>",
483 * convert the csh-style brace expressions:
484 * "<glob> <glob> ... *.abc *.def <glob>".
485 * Requires no system support, so should work equally on Unix, Mac, Win32.
487 static string const convert_brace_glob(string const & glob)
489 // Matches " *.{abc,def,ghi}", storing "*." as group 1 and
490 // "abc,def,ghi" as group 2, while allowing spaces in group 2.
491 static regex const glob_re(" *([^ {]*)\\{([^}]+)\\}");
492 // Matches "abc" and "abc,", storing "abc" as group 1,
493 // while ignoring surrounding spaces.
494 static regex const block_re(" *([^ ,}]+) *,? *");
498 string::const_iterator it = glob.begin();
499 string::const_iterator const end = glob.end();
501 match_results<string::const_iterator> what;
502 if (!regex_search(it, end, what, glob_re)) {
503 // Ensure that no information is lost.
504 pattern += string(it, end);
508 // Everything from the start of the input to
509 // the start of the match.
510 pattern += string(what[-1].first, what[-1].second);
512 // Given " *.{abc,def}", head == "*." and tail == "abc,def".
513 string const head = string(what[1].first, what[1].second);
514 string const tail = string(what[2].first, what[2].second);
516 // Split the ','-separated chunks of tail so that
517 // $head{$chunk1,$chunk2} becomes "$head$chunk1 $head$chunk2".
518 string const fmt = " " + head + "$1";
519 pattern += regex_replace(tail, block_re, fmt);
521 // Increment the iterator to the end of the match.
522 it += distance(it, what[0].second);
531 /* \param description text describing the filters.
532 * \param one or more wildcard patterns, separated by
535 Filter(docstring const & description, std::string const & globs);
537 docstring const & description() const { return desc_; }
539 QString toString() const;
542 std::vector<std::string> globs_;
546 Filter::Filter(docstring const & description, string const & globs)
549 // Given "<glob> <glob> ... *.{abc,def} <glob>", expand to
550 // "<glob> <glob> ... *.abc *.def <glob>"
551 string const expanded_globs = convert_brace_glob(globs);
553 // Split into individual globs.
554 globs_ = getVectorFromString(expanded_globs, " ");
558 QString Filter::toString() const
562 bool const has_description = !desc_.empty();
564 if (has_description) {
569 s += toqstr(getStringFromVector(globs_, " "));
577 /** \c FileFilterList parses a Qt-style list of available file filters
578 * to generate the corresponding vector.
579 * For example "TeX documents (*.tex);;LyX Documents (*.lyx)"
580 * will be parsed to fill a vector of size 2, whilst "*.{p[bgp]m} *.pdf"
581 * will result in a vector of size 1 in which the description field is empty.
583 struct FileFilterList
585 // FIXME UNICODE: globs_ should be unicode...
586 /** \param qt_style_filter a list of available file filters.
587 * Eg. "TeX documents (*.tex);;LyX Documents (*.lyx)".
588 * The "All files (*)" filter is always added to the list.
590 explicit FileFilterList(docstring const & qt_style_filter =
593 typedef std::vector<Filter>::size_type size_type;
595 bool empty() const { return filters_.empty(); }
596 size_type size() const { return filters_.size(); }
597 Filter & operator[](size_type i) { return filters_[i]; }
598 Filter const & operator[](size_type i) const { return filters_[i]; }
600 void parse_filter(std::string const & filter);
601 std::vector<Filter> filters_;
605 FileFilterList::FileFilterList(docstring const & qt_style_filter)
608 string const filter = to_utf8(qt_style_filter)
609 + (qt_style_filter.empty() ? string() : ";;")
610 + to_utf8(_("All Files "))
617 // Split data such as "TeX documents (*.tex);;LyX Documents (*.lyx)"
618 // into individual filters.
619 static regex const separator_re(";;");
621 string::const_iterator it = filter.begin();
622 string::const_iterator const end = filter.end();
624 match_results<string::const_iterator> what;
626 if (!regex_search(it, end, what, separator_re)) {
627 parse_filter(string(it, end));
631 // Everything from the start of the input to
632 // the start of the match.
633 parse_filter(string(it, what[0].first));
635 // Increment the iterator to the end of the match.
636 it += distance(it, what[0].second);
641 void FileFilterList::parse_filter(string const & filter)
643 // Matches "TeX documents (plain) (*.tex)",
644 // storing "TeX documents (plain) " as group 1 and "*.tex" as group 2.
645 static regex const filter_re("(.*)\\(([^()]+)\\) *$");
647 match_results<string::const_iterator> what;
648 if (!regex_search(filter, what, filter_re)) {
649 // Just a glob, no description.
650 filters_.push_back(Filter(docstring(), trim(filter)));
653 docstring const desc = from_utf8(string(what[1].first, what[1].second));
654 string const globs = string(what[2].first, what[2].second);
655 filters_.push_back(Filter(trim(desc), trim(globs)));
660 /** \returns the equivalent of the string passed in
661 * although any brace expressions are expanded.
662 * (E.g. "*.{png,jpg}" -> "*.png *.jpg")
664 QStringList fileFilters(QString const & desc)
666 // we have: "*.{gif,png,jpg,bmp,pbm,ppm,tga,tif,xpm,xbm}"
667 // but need: "*.cpp;*.cc;*.C;*.cxx;*.c++"
668 FileFilterList filters(qstring_to_ucs4(desc));
669 //LYXERR0("DESC: " << desc);
671 for (size_t i = 0; i != filters.filters_.size(); ++i) {
672 QString f = filters.filters_[i].toString();
673 //LYXERR0("FILTER: " << f);
680 QString formatToolTip(QString text, int width)
682 // 1. QTooltip activates word wrapping only if mightBeRichText()
683 // is true. So we convert the text to rich text.
685 // 2. The default width is way too small. Setting the width is tricky; first
686 // one has to compute the ideal width, and then force it with special
689 // do nothing if empty or already formatted
690 if (text.isEmpty() || text.startsWith(QString("<html>")))
692 // Convert to rich text if it is not already
693 if (!Qt::mightBeRichText(text))
694 text = Qt::convertFromPlainText(text, Qt::WhiteSpaceNormal);
695 // Compute desired width in pixels
696 QFont const font = QToolTip::font();
697 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
698 int const px_width = width * QFontMetrics(font).horizontalAdvance("M");
700 int const px_width = width * QFontMetrics(font).width("M");
702 // Determine the ideal width of the tooltip
703 QTextDocument td("");
705 td.setDefaultFont(QToolTip::font());
706 td.setDocumentMargin(0);
707 td.setTextWidth(px_width);
708 double best_width = td.idealWidth();
709 // Set the line wrapping with appropriate width
710 return QString("<html><body><table><tr>"
711 "<td align=justify width=%1>%2</td>"
712 "</tr></table></body></html>")
713 .arg(QString::number(int(best_width) + 1), text);
717 QString qtHtmlToPlainText(QString const & text)
719 if (!Qt::mightBeRichText(text))
723 return td.toPlainText();