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 /// wrapper to hide the change of method name to setSectionResizeMode
273 void setSectionResizeMode(QHeaderView * view,
274 int logicalIndex, QHeaderView::ResizeMode mode) {
275 #if (QT_VERSION >= 0x050000)
276 view->setSectionResizeMode(logicalIndex, mode);
278 view->setResizeMode(logicalIndex, mode);
282 void setSectionResizeMode(QHeaderView * view, QHeaderView::ResizeMode mode) {
283 #if (QT_VERSION >= 0x050000)
284 view->setSectionResizeMode(mode);
286 view->setResizeMode(mode);
290 void showDirectory(FileName const & directory)
292 if (!directory.exists())
294 QUrl qurl(QUrl::fromLocalFile(QDir::toNativeSeparators(toqstr(directory.absFileName()))));
295 // Give hints in case of bugs
296 if (!qurl.isValid()) {
297 frontend::Alert::error(_("Invalid URL"),
298 bformat(_("The URL `%1$s' could not be resolved."),
299 qstring_to_ucs4(qurl.toString())));
303 if (!QDesktopServices::openUrl(qurl))
304 frontend::Alert::error(_("URL could not be accessed"),
305 bformat(_("The URL `%1$s' could not be opened although it exists!"),
306 qstring_to_ucs4(qurl.toString())));
309 void showTarget(string const & target, string const & pdfv, string const & psv)
311 LYXERR(Debug::INSETS, "Showtarget:" << target << "\n");
312 if (prefixIs(target, "EXTERNAL ")) {
313 if (!lyxrc.citation_search)
315 string tmp, tar, opts;
316 tar = split(target, tmp, ' ');
318 opts = " -v " + pdfv;
320 opts += " -w " + psv;
324 string const command = lyxrc.citation_search_view + " " + opts + tar;
325 int const result = one.startscript(Systemcall::Wait, command);
328 frontend::Alert::error(_("Could not open file"),
329 _("The lyxpaperview script failed."));
330 else if (result == 2)
331 frontend::Alert::error(_("Could not open file"),
332 bformat(_("No file was found using the pattern `%1$s'."),
336 if (!QDesktopServices::openUrl(QUrl(toqstr(target), QUrl::TolerantMode)))
337 frontend::Alert::error(_("Could not open file"),
338 bformat(_("The target `%1$s' could not be resolved."),
341 } // namespace frontend
343 QString const qt_(char const * str, const char *)
345 return toqstr(_(str));
349 QString const qt_(string const & str)
351 return toqstr(_(str));
355 QString const qt_(QString const & qstr)
357 return toqstr(_(fromqstr(qstr)));
361 void rescanTexStyles(string const & arg)
363 // Run rescan in user lyx directory
364 PathChanger p(package().user_support());
365 FileName const prog = support::libFileSearch("scripts", "TeXFiles.py");
367 string const command = os::python() + ' ' +
368 quoteName(prog.toFilesystemEncoding()) + ' ' +
370 int const status = one.startscript(Systemcall::Wait, command);
374 frontend::Alert::error(_("Could not update TeX information"),
375 bformat(_("The script `%1$s' failed."), from_utf8(prog.absFileName())));
379 QStringList texFileList(QString const & filename)
382 FileName const file = libFileSearch(QString(), filename);
387 vector<docstring> doclist =
388 getVectorFromString(file.fileContents("UTF-8"), from_ascii("\n"));
390 // Normalise paths like /foo//bar ==> /foo/bar
392 for (size_t i = 0; i != doclist.size(); ++i) {
393 QString qfile = toqstr(doclist[i]);
394 qfile.replace("\r", "");
395 while (qfile.contains("//"))
396 qfile.replace("//", "/");
397 if (!qfile.isEmpty())
402 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
403 return QList<QString>(set.begin(), set.end());
405 return QList<QString>::fromSet(set);
409 QString const externalLineEnding(docstring const & str)
412 // The MAC clipboard uses \r for lineendings, and we use \n
413 return toqstr(subst(str, '\n', '\r'));
414 #elif defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN)
415 // Windows clipboard uses \r\n for lineendings, and we use \n
416 return toqstr(subst(str, from_ascii("\n"), from_ascii("\r\n")));
423 docstring const internalLineEnding(QString const & str)
425 docstring const s = subst(qstring_to_ucs4(str),
426 from_ascii("\r\n"), from_ascii("\n"));
427 return subst(s, '\r', '\n');
431 QString internalPath(const QString & str)
433 return toqstr(os::internal_path(fromqstr(str)));
437 QString onlyFileName(const QString & str)
439 return toqstr(support::onlyFileName(fromqstr(str)));
443 QString onlyPath(const QString & str)
445 return toqstr(support::onlyPath(fromqstr(str)));
449 QString changeExtension(QString const & oldname, QString const & ext)
451 return toqstr(support::changeExtension(fromqstr(oldname), fromqstr(ext)));
454 /// Remove the extension from \p name
455 QString removeExtension(QString const & name)
457 return toqstr(support::removeExtension(fromqstr(name)));
460 /** Add the extension \p ext to \p name.
461 Use this instead of changeExtension if you know that \p name is without
462 extension, because changeExtension would wrongly interpret \p name if it
465 QString addExtension(QString const & name, QString const & ext)
467 return toqstr(support::addExtension(fromqstr(name), fromqstr(ext)));
470 /// Return the extension of the file (not including the .)
471 QString getExtension(QString const & name)
473 return toqstr(support::getExtension(fromqstr(name)));
477 /** Convert relative path into absolute path based on a basepath.
478 If relpath is absolute, just use that.
479 If basepath doesn't exist use CWD.
481 QString makeAbsPath(QString const & relpath, QString const & base)
483 return toqstr(support::makeAbsPath(fromqstr(relpath),
484 fromqstr(base)).absFileName());
488 /////////////////////////////////////////////////////////////////////////
492 /////////////////////////////////////////////////////////////////////////
494 /** Given a string such as
495 * "<glob> <glob> ... *.{abc,def} <glob>",
496 * convert the csh-style brace expressions:
497 * "<glob> <glob> ... *.abc *.def <glob>".
498 * Requires no system support, so should work equally on Unix, Mac, Win32.
500 static string const convert_brace_glob(string const & glob)
502 // Matches " *.{abc,def,ghi}", storing "*." as group 1 and
503 // "abc,def,ghi" as group 2, while allowing spaces in group 2.
504 static regex const glob_re(" *([^ {]*)\\{([^}]+)\\}");
505 // Matches "abc" and "abc,", storing "abc" as group 1,
506 // while ignoring surrounding spaces.
507 static regex const block_re(" *([^ ,}]+) *,? *");
511 string::const_iterator it = glob.begin();
512 string::const_iterator const end = glob.end();
514 match_results<string::const_iterator> what;
515 if (!regex_search(it, end, what, glob_re)) {
516 // Ensure that no information is lost.
517 pattern += string(it, end);
521 // Everything from the start of the input to
522 // the start of the match.
523 pattern += string(what[-1].first, what[-1].second);
525 // Given " *.{abc,def}", head == "*." and tail == "abc,def".
526 string const head = string(what[1].first, what[1].second);
527 string const tail = string(what[2].first, what[2].second);
529 // Split the ','-separated chunks of tail so that
530 // $head{$chunk1,$chunk2} becomes "$head$chunk1 $head$chunk2".
531 string const fmt = " " + head + "$1";
532 pattern += regex_replace(tail, block_re, fmt);
534 // Increment the iterator to the end of the match.
535 it += distance(it, what[0].second);
544 /* \param description text describing the filters.
545 * \param one or more wildcard patterns, separated by
548 Filter(docstring const & description, std::string const & globs);
550 docstring const & description() const { return desc_; }
552 QString toString() const;
555 std::vector<std::string> globs_;
559 Filter::Filter(docstring const & description, string const & globs)
562 // Given "<glob> <glob> ... *.{abc,def} <glob>", expand to
563 // "<glob> <glob> ... *.abc *.def <glob>"
564 string const expanded_globs = convert_brace_glob(globs);
566 // Split into individual globs.
567 globs_ = getVectorFromString(expanded_globs, " ");
571 QString Filter::toString() const
575 bool const has_description = !desc_.empty();
577 if (has_description) {
582 s += toqstr(getStringFromVector(globs_, " "));
590 /** \c FileFilterList parses a Qt-style list of available file filters
591 * to generate the corresponding vector.
592 * For example "TeX documents (*.tex);;LyX Documents (*.lyx)"
593 * will be parsed to fill a vector of size 2, whilst "*.{p[bgp]m} *.pdf"
594 * will result in a vector of size 1 in which the description field is empty.
596 struct FileFilterList
598 // FIXME UNICODE: globs_ should be unicode...
599 /** \param qt_style_filter a list of available file filters.
600 * Eg. "TeX documents (*.tex);;LyX Documents (*.lyx)".
601 * The "All files (*)" filter is always added to the list.
603 explicit FileFilterList(docstring const & qt_style_filter =
606 typedef std::vector<Filter>::size_type size_type;
608 bool empty() const { return filters_.empty(); }
609 size_type size() const { return filters_.size(); }
610 Filter & operator[](size_type i) { return filters_[i]; }
611 Filter const & operator[](size_type i) const { return filters_[i]; }
613 void parse_filter(std::string const & filter);
614 std::vector<Filter> filters_;
618 FileFilterList::FileFilterList(docstring const & qt_style_filter)
621 string const filter = to_utf8(qt_style_filter)
622 + (qt_style_filter.empty() ? string() : ";;")
623 + to_utf8(_("All Files "))
630 // Split data such as "TeX documents (*.tex);;LyX Documents (*.lyx)"
631 // into individual filters.
632 static regex const separator_re(";;");
634 string::const_iterator it = filter.begin();
635 string::const_iterator const end = filter.end();
637 match_results<string::const_iterator> what;
639 if (!regex_search(it, end, what, separator_re)) {
640 parse_filter(string(it, end));
644 // Everything from the start of the input to
645 // the start of the match.
646 parse_filter(string(it, what[0].first));
648 // Increment the iterator to the end of the match.
649 it += distance(it, what[0].second);
654 void FileFilterList::parse_filter(string const & filter)
656 // Matches "TeX documents (plain) (*.tex)",
657 // storing "TeX documents (plain) " as group 1 and "*.tex" as group 2.
658 static regex const filter_re("(.*)\\(([^()]+)\\) *$");
660 match_results<string::const_iterator> what;
661 if (!regex_search(filter, what, filter_re)) {
662 // Just a glob, no description.
663 filters_.push_back(Filter(docstring(), trim(filter)));
666 docstring const desc = from_utf8(string(what[1].first, what[1].second));
667 string const globs = string(what[2].first, what[2].second);
668 filters_.push_back(Filter(trim(desc), trim(globs)));
673 /** \returns the equivalent of the string passed in
674 * although any brace expressions are expanded.
675 * (E.g. "*.{png,jpg}" -> "*.png *.jpg")
677 QStringList fileFilters(QString const & desc)
679 // we have: "*.{gif,png,jpg,bmp,pbm,ppm,tga,tif,xpm,xbm}"
680 // but need: "*.cpp;*.cc;*.C;*.cxx;*.c++"
681 FileFilterList filters(qstring_to_ucs4(desc));
682 //LYXERR0("DESC: " << desc);
684 for (size_t i = 0; i != filters.filters_.size(); ++i) {
685 QString f = filters.filters_[i].toString();
686 //LYXERR0("FILTER: " << f);
693 QString formatToolTip(QString text, int width)
695 // 1. QTooltip activates word wrapping only if mightBeRichText()
696 // is true. So we convert the text to rich text.
698 // 2. The default width is way too small. Setting the width is tricky; first
699 // one has to compute the ideal width, and then force it with special
702 // do nothing if empty or already formatted
703 if (text.isEmpty() || text.startsWith(QString("<html>")))
705 // Convert to rich text if it is not already
706 if (!Qt::mightBeRichText(text))
707 text = Qt::convertFromPlainText(text, Qt::WhiteSpaceNormal);
708 // Compute desired width in pixels
709 QFont const font = QToolTip::font();
710 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
711 int const px_width = width * QFontMetrics(font).horizontalAdvance("M");
713 int const px_width = width * QFontMetrics(font).width("M");
715 // Determine the ideal width of the tooltip
716 QTextDocument td("");
718 td.setDefaultFont(QToolTip::font());
719 td.setDocumentMargin(0);
720 td.setTextWidth(px_width);
721 double best_width = td.idealWidth();
722 // Set the line wrapping with appropriate width
723 return QString("<html><body><table><tr>"
724 "<td align=justify width=%1>%2</td>"
725 "</tr></table></body></html>")
726 .arg(QString::number(int(best_width) + 1), text);
730 QString qtHtmlToPlainText(QString const & text)
732 if (!Qt::mightBeRichText(text))
736 return td.toPlainText();