[LyX features/breakrows] Break multi-row strings in one pass

Jean-Marc Lasgouttes lasgouttes at lyx.org
Mon Sep 20 15:38:05 UTC 2021


The branch, breakrows, has been updated.

- Log -----------------------------------------------------------------

commit 8006aa710f2bc52082538f1c37252cc98b2b5c46
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Mon Sep 20 17:42:45 2021 +0200

    Break multi-row strings in one pass
    
    Replace FontMetrics::breakAt, which returned the next break point,
    with FontMetrics::breakString, which returns a vector of break points.
    To this end, an additional parameter gives the available width for
    next rows.
    
    Rename various variables and methods accordingly. Factor the code in
    breakString_helper to be more manageable.
    
    Adapt Row::Element::splitAt to return a vector of Element. Note that,
    beyond the two widthes noted above, a new parameter wrap_any has beeen
    added in addition to force.
    
    In Row::finalize, remove the code that computed inconditionnally the
    current element size, and make sure that this width will be compute
    din all code paths of Row::Element::splitAt.

diff --git a/src/Row.cpp b/src/Row.cpp
index fe829b5..2b27f74 100644
--- a/src/Row.cpp
+++ b/src/Row.cpp
@@ -129,38 +129,72 @@ pos_type Row::Element::x2pos(int &x) const
 }
 
 
-Row::Elements Row::Element::splitAt(int w, bool force)
+Row::Elements Row::Element::splitAt(int const width, int next_width, bool wrap_any, bool force)
 {
-	if (type != STRING || !(row_flags & CanBreakInside))
+	// Not a string or already OK.
+	if (type != STRING || (dim.wid > 0 && dim.wid < width))
 		return Elements();
 
 	FontMetrics const & fm = theFontMetrics(font);
-	dim.wid = w;
-	int const i = fm.breakAt(str, dim.wid, isRTL(), force);
 
-	// No need to break
-	if (i == -1)
+	// A a string that is not breakable
+	if (!(row_flags & CanBreakInside)) {
+		// has width been computed yet?
+		if (dim.wid == 0)
+			dim.wid = fm.width(str);
 		return Elements();
+	}
+
+	FontMetrics::Breaks breaks = fm.breakString(str, width, next_width, isRTL(), wrap_any | force);
+
+	// if breaking did not really work, give up
+	if (!force && breaks.front().wid > width) {
+		if (dim.wid == 0)
+			dim.wid = fm.width(str);
+		return Elements();
+	}
 
+	Element first_e(STRING, pos, font, change);
+	// should next element eventually replace *this?
+	bool first = true;
 	Elements tail;
-	// Is there a second element to return?
-	if (i < int(str.length())) {
-		Element ret(STRING, pos + i, font, change);
-		ret.str = str.substr(i);
-		ret.endpos = ret.pos + ret.str.length();
-		// Copy the after flags of the original element to the second one.
-		ret.row_flags = row_flags & (CanBreakInside | AfterFlags);
-		tail.push_back(ret);
-
-		// Now update ourselves
-		str.erase(i);
-		endpos = pos + i;
+	docstring::size_type i = 0;
+	for (FontMetrics::Break const & brk : breaks) {
+		Element e(STRING, pos + i, font, change);
+		e.str = str.substr(i, brk.len);
+		e.endpos = e.pos + brk.len;
+		e.dim.wid = brk.wid;
+		e.row_flags = CanBreakInside | BreakAfter;
+		if (first) {
+			// this element eventually goes to *this
+			e.row_flags |= row_flags & ~AfterFlags;
+			first_e = e;
+			first = false;
+		} else
+			tail.push_back(e);
+		i += brk.len;
 	}
 
-	// Row should be broken after the original element
-	row_flags = (row_flags & ~AfterFlags) | BreakAfter;
-	//LYXERR0("splitAt(" << w << ")  Row element Broken at " << w << "(w(str)=" << fm.width(str) << "): e=" << *this);
+	if (!tail.empty()) {
+		// Avoid having a last empty element. This happens when
+		// breaking at the trailing space of string
+		if (tail.back().str.empty())
+			tail.pop_back();
+		else {
+			// Copy the after flags of the original element to the last one.
+			tail.back().row_flags &= ~BreakAfter;
+			tail.back().row_flags |= row_flags & AfterFlags;
+		}
+		// first_e row should be broken after the original element
+		first_e.row_flags |= BreakAfter;
+	} else {
+		// Restore the after flags of the original element.
+		first_e.row_flags &= ~BreakAfter;
+		first_e.row_flags |= row_flags & AfterFlags;
+	}
 
+	// update ourselves
+	swap(first_e, *this);
 	return tail;
 }
 
@@ -393,11 +427,6 @@ void Row::finalizeLast()
 	elt.final = true;
 	if (elt.change.changed())
 		changebar_ = true;
-
-	if (elt.type == STRING && elt.dim.wid == 0) {
-		elt.dim.wid = theFontMetrics(elt.font).width(elt.str);
-		dim_.wid += elt.dim.wid;
-	}
 }
 
 
@@ -545,12 +574,12 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
 		*/
 		bool const word_wrap = brk.font.language()->wordWrap();
 		/* We have found a suitable separable element. This is the common case.
-		 * Try to break it cleanly (at word boundary) at a length that is both
+		 * Try to break it cleanly at a length that is both
 		 * - less than the available space on the row
 		 * - shorter than the natural width of the element, in order to enforce
 		 *   break-up.
 		 */
-		tail = brk.splitAt(min(w - wid_brk, brk.dim.wid - 2), !word_wrap);
+		tail = brk.splitAt(min(w - wid_brk, brk.dim.wid - 2), next_width, !word_wrap, false);
 		if (brk.row_flags & BreakAfter) {
 			/* if this element originally did not cause a row overflow
 			 * in itself, and the remainder of the row would still be
@@ -587,10 +616,10 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
 	}
 
 	/* If we are here, it means that we have not found a separator to
-	 * shorten the row. Let's try to break it again, but not at word
-	 * boundary this time.
+	 * shorten the row. Let's try to break it again, but force
+	 * splitting this time.
 	 */
-	tail = cit->splitAt(w - wid, true);
+	tail = cit->splitAt(w - wid, next_width, !cit->font.language()->wordWrap(), true);
 	if (cit->row_flags & BreakAfter) {
 		end_ = cit->endpos;
 		dim_.wid = wid + cit->dim.wid;
diff --git a/src/Row.h b/src/Row.h
index 0cc4879..7d8caba 100644
--- a/src/Row.h
+++ b/src/Row.h
@@ -92,12 +92,16 @@ public:
 		 * than \param w.
 		 * \return a vector of elements containing the remainder of
 		 *   the text (empty if nothing happened).
-		 * \param w: the desired maximum width
-		 * \param force: if true, the string is cut at any place, otherwise it
-		 *   respects the row breaking rules of characters.
+		 * \param width maximum width of the row.
+		 * \param next_width available width on next row.
+		 * \param wrap_any: if true, the string is cut at any place,
+		 *   otherwise it respects the row breaking rules of
+		 *   characters.
+		 * \param force: if false, do not break at all if first
+		 *   element would larger than \c width.
 		 */
 		// FIXME: ideally return type should be Elements, but it is not possible.
-		std::vector<Element> splitAt(int w, bool force);
+		std::vector<Element> splitAt(int width, int next_width, bool wrap_any, bool force);
 		// remove trailing spaces (useful for end of row)
 		void rtrim();
 
@@ -284,8 +288,8 @@ public:
 	 * separator and update endpos if necessary. If all that
 	 * remains is a large word, cut it to \param width.
 	 * \param width maximum width of the row.
-	 * \param available width on next row.
-	 * \return true if the row has been shortened.
+	 * \param next_width available width on next row.
+	 * \return list of elements remaining after breaking.
 	 */
 	Elements shortenIfNeeded(int const width, int const next_width);
 
diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index f70f11a..610f03b 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -1098,6 +1098,8 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 	RowList rows;
 	bool const is_rtl = text_->isRTL(bigrow.pit());
 	bool const end_label = text_->getEndLabel(bigrow.pit()) != END_LABEL_NO_LABEL;
+	int const next_width = max_width_ - leftMargin(bigrow.pit(), bigrow.endpos())
+		- rightMargin(bigrow.pit());
 
 	int width = 0;
 	flexible_const_iterator<Row> fcit = flexible_begin(bigrow);
@@ -1130,18 +1132,14 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 		// Next element to consider is either the top of the temporary
 		// pile, or the place when we were in main row
 		Row::Element elt = *fcit;
-		Row::Elements tail = elt.splitAt(width - rows.back().width(),
-		                                 !elt.font.language()->wordWrap());
+		Row::Elements tail = elt.splitAt(width - rows.back().width(), next_width,
+		                                 !elt.font.language()->wordWrap(), false);
 		Row & rb = rows.back();
 		rb.push_back(elt);
 		rb.finalizeLast();
-		if (rows.back().width() > width) {
+		if (rb.width() > width) {
 			LATTEST(tail.empty());
-			// if the row is too large, try to cut at last separator. In case
-			// of success, reset indication that the row was broken abruptly.
-			int const next_width = max_width_ - leftMargin(rb.pit(), rb.endpos())
-				- rightMargin(rb.pit());
-
+			// if the row is too large, try to cut at last separator.
 			tail = rb.shortenIfNeeded(width, next_width);
 		}
 
diff --git a/src/frontends/FontMetrics.h b/src/frontends/FontMetrics.h
index b562ebf..b78bc2d 100644
--- a/src/frontends/FontMetrics.h
+++ b/src/frontends/FontMetrics.h
@@ -16,6 +16,8 @@
 
 #include "support/strfwd.h"
 
+#include <vector>
+
 /**
  * A class holding helper functions for determining
  * the screen dimensions of fonts.
@@ -121,15 +123,27 @@ public:
 	 * \param ws is the amount of extra inter-word space applied text justification.
 	 */
 	virtual int x2pos(docstring const & s, int & x, bool rtl, double ws) const = 0;
+
+	// The places where to break a string and the width of the resulting lines.
+	struct Break {
+		Break(int l, int w) : len(l), wid(w) {}
+		int len = 0;
+		int wid = 0;
+	};
+	typedef std::vector<Break> Breaks;
 	/**
-	 * Break string s at width at most x.
-	 * \return break position (-1 if not successful)
-	 * \param position x is updated to real width
-	 * \param rtl is true for right-to-left layout
+	 * Break a string in multiple fragments according to width limits.
+	 * \return a sequence of Break elements.
+	 * \param s is the string to break.
+	 * \param first_wid is the available width for first line.
+	 * \param wid is the available width for the next lines.
+	 * \param rtl is true for right-to-left layout.
 	 * \param force is false for breaking at word separator, true for
 	 *   arbitrary position.
 	 */
-	virtual int breakAt(docstring const & s, int & x, bool rtl, bool force) const = 0;
+	virtual Breaks
+	breakString(docstring const & s, int first_wid, int wid, bool rtl, bool force) const = 0;
+
 	/// return char dimension for the font.
 	virtual Dimension const dimension(char_type c) const = 0;
 	/**
diff --git a/src/frontends/qt/GuiFontMetrics.cpp b/src/frontends/qt/GuiFontMetrics.cpp
index 83c2c0a..f7f41b1 100644
--- a/src/frontends/qt/GuiFontMetrics.cpp
+++ b/src/frontends/qt/GuiFontMetrics.cpp
@@ -19,6 +19,7 @@
 
 #include "support/convert.h"
 #include "support/lassert.h"
+#include "support/lstrings.h"
 #include "support/lyxlib.h"
 #include "support/debug.h"
 
@@ -77,14 +78,14 @@ namespace frontend {
 
 
 /*
- * Limit (strwidth|breakat)_cache_ size to 512kB of string data.
+ * Limit (strwidth|breakstr)_cache_ size to 512kB of string data.
  * Limit qtextlayout_cache_ size to 500 elements (we do not know the
  * size of the QTextLayout objects anyway).
  * Note that all these numbers are arbitrary.
  * Also, setting size to 0 is tantamount to disabling the cache.
  */
 int cache_metrics_width_size = 1 << 19;
-int cache_metrics_breakat_size = 1 << 19;
+int cache_metrics_breakstr_size = 1 << 19;
 // Qt 5.x already has its own caching of QTextLayout objects
 // but it does not seem to work well on MacOS X.
 #if (QT_VERSION < 0x050000) || defined(Q_OS_MAC)
@@ -119,7 +120,7 @@ inline QChar const ucs4_to_qchar(char_type const ucs4)
 GuiFontMetrics::GuiFontMetrics(QFont const & font)
 	: font_(font), metrics_(font, 0),
 	  strwidth_cache_(cache_metrics_width_size),
-	  breakat_cache_(cache_metrics_breakat_size),
+	  breakstr_cache_(cache_metrics_breakstr_size),
 	  qtextlayout_cache_(cache_metrics_qtextlayout_size)
 {
 	// Determine italic slope
@@ -471,11 +472,13 @@ int GuiFontMetrics::countExpanders(docstring const & str) const
 }
 
 
-pair<int, int>
-GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
-                               bool const rtl, bool const force) const
+namespace {
+
+const int brkStrOffset = 1 + BIDI_OFFSET;
+
+
+QString createBreakableString(docstring const & s, bool rtl, QTextLayout & tl)
 {
-	QTextLayout tl;
 	/* Qt will not break at a leading or trailing space, and we need
 	 * that sometimes, see http://www.lyx.org/trac/ticket/9921.
 	 *
@@ -504,34 +507,23 @@ GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
 		// Left-to-right override: forces to draw text left-to-right
 		qs =  QChar(0x202D) + qs;
 #endif
-	int const offset = 1 + BIDI_OFFSET;
+	return qs;
+}
 
-	tl.setText(qs);
-	tl.setFont(font_);
-	QTextOption to;
-	to.setWrapMode(force ? QTextOption::WrapAtWordBoundaryOrAnywhere
-	                     : QTextOption::WordWrap);
-	// Let QTextLine::naturalTextWidth() account for trailing spaces
-	// (horizontalAdvance() still does not).
-	to.setFlags(QTextOption::IncludeTrailingSpaces);
-	tl.setTextOption(to);
-	tl.beginLayout();
-	QTextLine line = tl.createLine();
-	line.setLineWidth(x);
-	tl.createLine();
-	tl.endLayout();
-	int line_wid = iround(line.horizontalAdvance());
-	if ((force && line.textLength() == offset) || line_wid > x)
-		return {-1, line_wid};
+
+docstring::size_type brkstr2str_pos(QString brkstr, docstring const & str, int pos)
+{
 	/* Since QString is UTF-16 and docstring is UCS-4, the offsets may
 	 * not be the same when there are high-plan unicode characters
 	 * (bug #10443).
 	 */
-	// The variable `offset' is here to account for the extra leading characters.
+	// The variable `brkStrOffset' is here to account for the extra leading characters.
 	// The ending character zerow_nbsp has to be ignored if the line is complete.
-	int const qlen = line.textLength() - offset - (line.textLength() == qs.length());
+	int const qlen = pos - brkStrOffset - (pos == brkstr.length());
 #if QT_VERSION < 0x040801 || QT_VERSION >= 0x050100
-	int len = qstring_to_ucs4(qs.mid(offset, qlen)).length();
+	auto const len = qstring_to_ucs4(brkstr.mid(brkStrOffset, qlen)).length();
+	// Avoid warning
+	(void)str;
 #else
 	/* Due to QTBUG-25536 in 4.8.1 <= Qt < 5.1.0, the string returned
 	 * by QString::toUcs4 (used by qstring_to_ucs4) may have wrong
@@ -541,47 +533,94 @@ GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
 	 * worthwhile to implement a dichotomy search if this shows up
 	 * under a profiler.
 	 */
-	int len = min(qlen, static_cast<int>(s.length()));
-	while (len >= 0 && toqstr(s.substr(0, len)).length() != qlen)
+	int len = min(qlen, static_cast<int>(str.length()));
+	while (len >= 0 && toqstr(str.substr(0, len)).length() != qlen)
 		--len;
 	LASSERT(len > 0 || qlen == 0, /**/);
 #endif
-	// Do not cut is the string is already short enough. We rely on
-	// naturalTextWidth() to catch the case where we cut at the trailing
-	// space.
-	if (len == static_cast<int>(s.length())
-		&& line.naturalTextWidth() <= x) {
-		len = -1;
-#if QT_VERSION < 0x050000
+	return len;
+}
+
+}
+
+FontMetrics::Breaks
+GuiFontMetrics::breakString_helper(docstring const & s, int first_wid, int wid,
+                                   bool rtl, bool force) const
+{
+	QTextLayout tl;
+	QString qs = createBreakableString(s, rtl, tl);
+	tl.setText(qs);
+	tl.setFont(font_);
+	QTextOption to;
+	to.setWrapMode(force ? QTextOption::WrapAtWordBoundaryOrAnywhere
+	                     : QTextOption::WordWrap);
+	// Let QTextLine::naturalTextWidth() account for trailing spaces
+	// (horizontalAdvance() still does not).
+	to.setFlags(QTextOption::IncludeTrailingSpaces);
+	tl.setTextOption(to);
+
+	bool first = true;
+	tl.beginLayout();
+	while(true) {
+		QTextLine line = tl.createLine();
+		if (!line.isValid())
+			break;
+		line.setLineWidth(first ? first_wid : wid);
+		tl.createLine();
+		first = false;
+	}
+	tl.endLayout();
+
+	Breaks breaks;
+	int pos = 0;
+	for (int i = 0 ; i < tl.lineCount() ; ++i) {
+		QTextLine const & line = tl.lineAt(i);
+		int const epos = brkstr2str_pos(qs, s, line.textStart() + line.textLength());
+#if QT_VERSION >= 0x050000
+		int const wid = i + 1 < tl.lineCount() ? iround(line.horizontalAdvance())
+		                                       : iround(line.naturalTextWidth());
+#else
 		// With some monospace fonts, the value of horizontalAdvance()
 		// can be wrong with Qt4. One hypothesis is that the invisible
 		// characters that we use are given a non-null width.
-		line_wid = width(s);
+		// FIXME: this is slower than it could be but we'll get rid of Qt4 anyway
+		int const wid = i + 1 < tl.lineCount() ? width(rtrim(s.substr(pos, epos - pos)))
+		                                       : width(s.substr(pos, epos - pos));
+#endif
+		breaks.emplace_back(epos - pos, wid);
+		pos = epos;
+#if 0
+		// FIXME: should it be kept in some form?
+		if ((force && line.textLength() == brkStrOffset) || line_wid > x)
+			return {-1, line_wid};
 #endif
+
 	}
-	return {len, line_wid};
+
+	return breaks;
 }
 
 
-int GuiFontMetrics::breakAt(docstring const & s, int & x, bool const rtl, bool const force) const
+FontMetrics::Breaks GuiFontMetrics::breakString(docstring const & s, int first_wid, int wid,
+                                                bool rtl, bool force) const
 {
-	PROFILE_THIS_BLOCK(breakAt);
+	PROFILE_THIS_BLOCK(breakString);
 	if (s.empty())
-		return false;
+		return Breaks();
 
 	docstring const s_cache =
-		s + convert<docstring>(x) + (rtl ? "r" : "l") + (force ? "f" : "w");
-	pair<int, int> pp;
+		s + ":" + convert<docstring>(first_wid) + ":" + convert<docstring>(wid)
+		  + (rtl ? "r" : "l") + (force ? "f" : "w");
+	Breaks brks;
 
-	if (breakat_cache_.contains(s_cache))
-		pp = breakat_cache_[s_cache];
+	if (breakstr_cache_.contains(s_cache))
+		 brks = breakstr_cache_[s_cache];
 	else {
-		PROFILE_CACHE_MISS(breakAt);
-		pp = breakAt_helper(s, x, rtl, force);
-		breakat_cache_.insert(s_cache, pp, s_cache.size() * sizeof(char_type));
+		PROFILE_CACHE_MISS(breakString);
+		brks = breakString_helper(s, first_wid, wid, rtl, force);
+		breakstr_cache_.insert(s_cache, brks, s_cache.size() * sizeof(char_type));
 	}
-	x = pp.second;
-	return pp.first;
+	return brks;
 }
 
 
diff --git a/src/frontends/qt/GuiFontMetrics.h b/src/frontends/qt/GuiFontMetrics.h
index 70bb7f6..6999df6 100644
--- a/src/frontends/qt/GuiFontMetrics.h
+++ b/src/frontends/qt/GuiFontMetrics.h
@@ -53,7 +53,7 @@ public:
 	int signedWidth(docstring const & s) const override;
 	int pos2x(docstring const & s, int pos, bool rtl, double ws) const override;
 	int x2pos(docstring const & s, int & x, bool rtl, double ws) const override;
-	int breakAt(docstring const & s, int & x, bool rtl, bool force) const override;
+	Breaks breakString(docstring const & s, int first_wid, int wid, bool rtl, bool force) const override;
 	Dimension const dimension(char_type c) const override;
 
 	void rectText(docstring const & str,
@@ -77,8 +77,8 @@ public:
 
 private:
 
-	std::pair<int, int> breakAt_helper(docstring const & s, int const x,
-	                                   bool const rtl, bool const force) const;
+	Breaks breakString_helper(docstring const & s, int first_wid, int wid,
+	                          bool rtl, bool force) const;
 
 	/// The font
 	QFont font_;
@@ -93,8 +93,8 @@ private:
 	mutable QHash<char_type, int> width_cache_;
 	/// Cache of string widths
 	mutable Cache<docstring, int> strwidth_cache_;
-	/// Cache for breakAt
-	mutable Cache<docstring, std::pair<int, int>> breakat_cache_;
+	/// Cache for breakString
+	mutable Cache<docstring, Breaks> breakstr_cache_;
 	/// Cache for QTextLayout
 	mutable Cache<docstring, std::shared_ptr<QTextLayout>> qtextlayout_cache_;
 

commit 68c4867ba2f1de9489b9e20e2f708ede7111701e
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Mon Sep 20 17:32:18 2021 +0200

    Add operator<< for Row::Elements
    
    This is useful for debugging.

diff --git a/src/Row.cpp b/src/Row.cpp
index 093f280..fe829b5 100644
--- a/src/Row.cpp
+++ b/src/Row.cpp
@@ -275,6 +275,17 @@ ostream & operator<<(ostream & os, Row::Element const & e)
 }
 
 
+ostream & operator<<(ostream & os, Row::Elements const & elts)
+{
+	double x = 0;
+	for (Row::Element const & e : elts) {
+		os << "x=" << x << " => " << e << endl;
+		x += e.full_width();
+	}
+	return os;
+}
+
+
 ostream & operator<<(ostream & os, Row const & row)
 {
 	os << " pos: " << row.pos_ << " end: " << row.end_
@@ -286,11 +297,11 @@ ostream & operator<<(ostream & os, Row const & row)
 	   << " separator: " << row.separator
 	   << " label_hfill: " << row.label_hfill
 	   << " row_boundary: " << row.right_boundary() << "\n";
+	// We cannot use the operator above, unfortunately
 	double x = row.left_margin;
-	Row::Elements::const_iterator it = row.elements_.begin();
-	for ( ; it != row.elements_.end() ; ++it) {
-		os << "x=" << x << " => " << *it << endl;
-		x += it->full_width();
+	for (Row::Element const & e : row.elements_) {
+		os << "x=" << x << " => " << e << endl;
+		x += e.full_width();
 	}
 	return os;
 }
diff --git a/src/Row.h b/src/Row.h
index 552c2fe..0cc4879 100644
--- a/src/Row.h
+++ b/src/Row.h
@@ -373,6 +373,8 @@ private:
 	bool changebar_ = false;
 };
 
+std::ostream & operator<<(std::ostream & os, Row::Elements const & elts);
+
 
 /**
  * Each paragraph is broken up into a number of rows on the screen.

-----------------------------------------------------------------------

Summary of changes:
 src/Row.cpp                         |  110 ++++++++++++++++++---------
 src/Row.h                           |   18 +++--
 src/TextMetrics.cpp                 |   14 ++--
 src/frontends/FontMetrics.h         |   24 +++++-
 src/frontends/qt/GuiFontMetrics.cpp |  141 ++++++++++++++++++++++-------------
 src/frontends/qt/GuiFontMetrics.h   |   10 +-
 6 files changed, 207 insertions(+), 110 deletions(-)


hooks/post-receive
-- 
Repository for new features


More information about the lyx-cvs mailing list