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 for (QWidget * w : highlighted)
268 w->setPalette(newpal);
269 for (QWidget * w : plain)
274 void showDirectory(FileName const & directory)
276 if (!directory.exists())
278 QUrl qurl(QUrl::fromLocalFile(QDir::toNativeSeparators(toqstr(directory.absFileName()))));
279 // Give hints in case of bugs
280 if (!qurl.isValid()) {
281 frontend::Alert::error(_("Invalid URL"),
282 bformat(_("The URL `%1$s' could not be resolved."),
283 qstring_to_ucs4(qurl.toString())));
287 if (!QDesktopServices::openUrl(qurl))
288 frontend::Alert::error(_("URL could not be accessed"),
289 bformat(_("The URL `%1$s' could not be opened although it exists!"),
290 qstring_to_ucs4(qurl.toString())));
293 void showTarget(string const & target, string const & pdfv, string const & psv)
295 LYXERR(Debug::INSETS, "Showtarget:" << target << "\n");
296 if (prefixIs(target, "EXTERNAL ")) {
297 if (!lyxrc.citation_search)
299 string tmp, tar, opts;
300 tar = split(target, tmp, ' ');
302 opts = " -v \"" + pdfv + "\"";
304 opts += " -w \"" + psv + "\"";
308 string const viewer = subst(lyxrc.citation_search_view, "$${python}", os::python());
309 string const command = viewer + " " + opts + tar;
310 int const result = one.startscript(Systemcall::Wait, command);
313 frontend::Alert::error(_("Could not open file"),
314 _("The lyxpaperview script failed."));
315 else if (result == 2)
316 frontend::Alert::error(_("Could not open file"),
317 bformat(_("No file was found using the pattern `%1$s'."),
321 if (!QDesktopServices::openUrl(QUrl(toqstr(target), QUrl::TolerantMode)))
322 frontend::Alert::error(_("Could not open file"),
323 bformat(_("The target `%1$s' could not be resolved."),
326 } // namespace frontend
328 QString const qt_(char const * str, const char *)
330 return toqstr(_(str));
334 QString const qt_(string const & str)
336 return toqstr(_(str));
340 QString const qt_(QString const & qstr)
342 return toqstr(_(fromqstr(qstr)));
346 void rescanTexStyles(string const & arg)
348 // Run rescan in user lyx directory
349 PathChanger p(package().user_support());
350 FileName const prog = support::libFileSearch("scripts", "TeXFiles.py");
352 string const command = os::python() + ' ' +
353 quoteName(prog.toFilesystemEncoding()) + ' ' +
355 int const status = one.startscript(Systemcall::Wait, command);
359 frontend::Alert::error(_("Could not update TeX information"),
360 bformat(_("The script `%1$s' failed."), from_utf8(prog.absFileName())));
364 QStringList texFileList(QString const & filename)
367 FileName const file = libFileSearch(QString(), filename);
372 vector<docstring> doclist =
373 getVectorFromString(file.fileContents("UTF-8"), from_ascii("\n"));
375 // Normalise paths like /foo//bar ==> /foo/bar
377 for (size_t i = 0; i != doclist.size(); ++i) {
378 QString qfile = toqstr(doclist[i]);
379 qfile.replace("\r", "");
380 while (qfile.contains("//"))
381 qfile.replace("//", "/");
382 if (!qfile.isEmpty())
387 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
388 return QList<QString>(set.begin(), set.end());
390 return QList<QString>::fromSet(set);
394 QString const externalLineEnding(docstring const & str)
397 // The MAC clipboard uses \r for lineendings, and we use \n
398 return toqstr(subst(str, '\n', '\r'));
399 #elif defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN)
400 // Windows clipboard uses \r\n for lineendings, and we use \n
401 return toqstr(subst(str, from_ascii("\n"), from_ascii("\r\n")));
408 docstring const internalLineEnding(QString const & str)
410 docstring const s = subst(qstring_to_ucs4(str),
411 from_ascii("\r\n"), from_ascii("\n"));
412 return subst(s, '\r', '\n');
416 QString internalPath(const QString & str)
418 return toqstr(os::internal_path(fromqstr(str)));
422 QString onlyFileName(const QString & str)
424 return toqstr(support::onlyFileName(fromqstr(str)));
428 QString onlyPath(const QString & str)
430 return toqstr(support::onlyPath(fromqstr(str)));
434 QString changeExtension(QString const & oldname, QString const & ext)
436 return toqstr(support::changeExtension(fromqstr(oldname), fromqstr(ext)));
439 /// Remove the extension from \p name
440 QString removeExtension(QString const & name)
442 return toqstr(support::removeExtension(fromqstr(name)));
445 /** Add the extension \p ext to \p name.
446 Use this instead of changeExtension if you know that \p name is without
447 extension, because changeExtension would wrongly interpret \p name if it
450 QString addExtension(QString const & name, QString const & ext)
452 return toqstr(support::addExtension(fromqstr(name), fromqstr(ext)));
455 /// Return the extension of the file (not including the .)
456 QString getExtension(QString const & name)
458 return toqstr(support::getExtension(fromqstr(name)));
462 /** Convert relative path into absolute path based on a basepath.
463 If relpath is absolute, just use that.
464 If basepath doesn't exist use CWD.
466 QString makeAbsPath(QString const & relpath, QString const & base)
468 return toqstr(support::makeAbsPath(fromqstr(relpath),
469 fromqstr(base)).absFileName());
473 /////////////////////////////////////////////////////////////////////////
477 /////////////////////////////////////////////////////////////////////////
479 /** Given a string such as
480 * "<glob> <glob> ... *.{abc,def} <glob>",
481 * convert the csh-style brace expressions:
482 * "<glob> <glob> ... *.abc *.def <glob>".
483 * Requires no system support, so should work equally on Unix, Mac, Win32.
485 static string const convert_brace_glob(string const & glob)
487 // Matches " *.{abc,def,ghi}", storing "*." as group 1 and
488 // "abc,def,ghi" as group 2, while allowing spaces in group 2.
489 static regex const glob_re(" *([^ {]*)\\{([^}]+)\\}");
490 // Matches "abc" and "abc,", storing "abc" as group 1,
491 // while ignoring surrounding spaces.
492 static regex const block_re(" *([^ ,}]+) *,? *");
496 string::const_iterator it = glob.begin();
497 string::const_iterator const end = glob.end();
499 match_results<string::const_iterator> what;
500 if (!regex_search(it, end, what, glob_re)) {
501 // Ensure that no information is lost.
502 pattern += string(it, end);
506 // Everything from the start of the input to
507 // the start of the match.
508 pattern += string(what[-1].first, what[-1].second);
510 // Given " *.{abc,def}", head == "*." and tail == "abc,def".
511 string const head = string(what[1].first, what[1].second);
512 string const tail = string(what[2].first, what[2].second);
514 // Split the ','-separated chunks of tail so that
515 // $head{$chunk1,$chunk2} becomes "$head$chunk1 $head$chunk2".
516 string const fmt = " " + head + "$1";
517 pattern += regex_replace(tail, block_re, fmt);
519 // Increment the iterator to the end of the match.
520 it += distance(it, what[0].second);
529 /* \param description text describing the filters.
530 * \param one or more wildcard patterns, separated by
533 Filter(docstring const & description, std::string const & globs);
535 docstring const & description() const { return desc_; }
537 QString toString() const;
540 std::vector<std::string> globs_;
544 Filter::Filter(docstring const & description, string const & globs)
547 // Given "<glob> <glob> ... *.{abc,def} <glob>", expand to
548 // "<glob> <glob> ... *.abc *.def <glob>"
549 string const expanded_globs = convert_brace_glob(globs);
551 // Split into individual globs.
552 globs_ = getVectorFromString(expanded_globs, " ");
556 QString Filter::toString() const
560 bool const has_description = !desc_.empty();
562 if (has_description) {
567 s += toqstr(getStringFromVector(globs_, " "));
575 /** \c FileFilterList parses a Qt-style list of available file filters
576 * to generate the corresponding vector.
577 * For example "TeX documents (*.tex);;LyX Documents (*.lyx)"
578 * will be parsed to fill a vector of size 2, whilst "*.{p[bgp]m} *.pdf"
579 * will result in a vector of size 1 in which the description field is empty.
581 struct FileFilterList
583 // FIXME UNICODE: globs_ should be unicode...
584 /** \param qt_style_filter a list of available file filters.
585 * Eg. "TeX documents (*.tex);;LyX Documents (*.lyx)".
586 * The "All files (*)" filter is always added to the list.
588 explicit FileFilterList(docstring const & qt_style_filter =
591 typedef std::vector<Filter>::size_type size_type;
593 bool empty() const { return filters_.empty(); }
594 size_type size() const { return filters_.size(); }
595 Filter & operator[](size_type i) { return filters_[i]; }
596 Filter const & operator[](size_type i) const { return filters_[i]; }
598 void parse_filter(std::string const & filter);
599 std::vector<Filter> filters_;
603 FileFilterList::FileFilterList(docstring const & qt_style_filter)
606 string const filter = to_utf8(qt_style_filter)
607 + (qt_style_filter.empty() ? string() : ";;")
608 + to_utf8(_("All Files "))
615 // Split data such as "TeX documents (*.tex);;LyX Documents (*.lyx)"
616 // into individual filters.
617 static regex const separator_re(";;");
619 string::const_iterator it = filter.begin();
620 string::const_iterator const end = filter.end();
622 match_results<string::const_iterator> what;
624 if (!regex_search(it, end, what, separator_re)) {
625 parse_filter(string(it, end));
629 // Everything from the start of the input to
630 // the start of the match.
631 parse_filter(string(it, what[0].first));
633 // Increment the iterator to the end of the match.
634 it += distance(it, what[0].second);
639 void FileFilterList::parse_filter(string const & filter)
641 // Matches "TeX documents (plain) (*.tex)",
642 // storing "TeX documents (plain) " as group 1 and "*.tex" as group 2.
643 static regex const filter_re("(.*)\\(([^()]+)\\) *$");
645 match_results<string::const_iterator> what;
646 if (!regex_search(filter, what, filter_re)) {
647 // Just a glob, no description.
648 filters_.push_back(Filter(docstring(), trim(filter)));
651 docstring const desc = from_utf8(string(what[1].first, what[1].second));
652 string const globs = string(what[2].first, what[2].second);
653 filters_.push_back(Filter(trim(desc), trim(globs)));
658 /** \returns the equivalent of the string passed in
659 * although any brace expressions are expanded.
660 * (E.g. "*.{png,jpg}" -> "*.png *.jpg")
662 QStringList fileFilters(QString const & desc)
664 // we have: "*.{gif,png,jpg,bmp,pbm,ppm,tga,tif,xpm,xbm}"
665 // but need: "*.cpp;*.cc;*.C;*.cxx;*.c++"
666 FileFilterList filters(qstring_to_ucs4(desc));
667 //LYXERR0("DESC: " << desc);
669 for (size_t i = 0; i != filters.filters_.size(); ++i) {
670 QString f = filters.filters_[i].toString();
671 //LYXERR0("FILTER: " << f);
678 QString formatToolTip(QString text, int width)
680 // 1. QTooltip activates word wrapping only if mightBeRichText()
681 // is true. So we convert the text to rich text.
683 // 2. The default width is way too small. Setting the width is tricky; first
684 // one has to compute the ideal width, and then force it with special
687 // do nothing if empty or already formatted
688 if (text.isEmpty() || text.startsWith(QString("<html>")))
690 // Convert to rich text if it is not already
691 if (!Qt::mightBeRichText(text))
692 text = Qt::convertFromPlainText(text, Qt::WhiteSpaceNormal);
693 // Compute desired width in pixels
694 QFont const font = QToolTip::font();
695 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
696 int const px_width = width * QFontMetrics(font).horizontalAdvance("M");
698 int const px_width = width * QFontMetrics(font).width("M");
700 // Determine the ideal width of the tooltip
701 QTextDocument td("");
703 td.setDefaultFont(QToolTip::font());
704 td.setDocumentMargin(0);
705 td.setTextWidth(px_width);
706 double best_width = td.idealWidth();
707 // Set the line wrapping with appropriate width
708 return QString("<html><body><table><tr>"
709 "<td align=justify width=%1>%2</td>"
710 "</tr></table></body></html>")
711 .arg(QString::number(int(best_width) + 1), text);
715 QString qtHtmlToPlainText(QString const & text)
717 if (!Qt::mightBeRichText(text))
721 return td.toPlainText();