1 #include <QPainter>
2 #include <QPainterPath>
3 #include <QPaintEvent>
4 #include <QDebug>
5 #include "shellwidget.h"
6 #include "helpers.h"
7 
ShellWidget(QWidget * parent)8 ShellWidget::ShellWidget(QWidget* parent)
9 	: QWidget(parent)
10 {
11 	setAttribute(Qt::WA_OpaquePaintEvent);
12 	setAttribute(Qt::WA_KeyCompression, false);
13 	setFocusPolicy(Qt::StrongFocus);
14 	setSizePolicy(QSizePolicy::Expanding,
15 			QSizePolicy::Expanding);
16 	setMouseTracking(true);
17 
18 	setDefaultFont();
19 
20 	// Blinking Cursor Timer
21 	connect(&m_cursor, &Cursor::CursorChanged, this, &ShellWidget::handleCursorChanged);
22 }
23 
fromFile(const QString & path)24 ShellWidget* ShellWidget::fromFile(const QString& path)
25 {
26 	ShellWidget *w = new ShellWidget();
27 	w->m_contents.fromFile(path);
28 	return w;
29 }
30 
setDefaultFont()31 void ShellWidget::setDefaultFont()
32 {
33 #if defined(Q_OS_MAC)
34 #  define DEFAULT_FONT "Courier New"
35 #elif defined(Q_OS_WIN)
36 #  define DEFAULT_FONT "Consolas"
37 #else
38 #  define DEFAULT_FONT "Monospace"
39 #endif
40 	setShellFont(DEFAULT_FONT, 11, -1, false, true);
41 }
42 
setShellFont(const QString & family,qreal ptSize,int weight,bool italic,bool force)43 bool ShellWidget::setShellFont(const QString& family, qreal ptSize, int weight, bool italic, bool force)
44 {
45 	QFont f(family, -1, weight, italic);
46 	// Issue #575: Clear style name. The KDE/Plasma theme plugin may set this
47 	// but we want to match the family name with the bold/italic attributes.
48 	f.setStyleName(QStringLiteral(""));
49 
50 	f.setPointSizeF(ptSize);
51 	f.setStyleHint(QFont::TypeWriter, QFont::StyleStrategy(QFont::PreferDefault | QFont::ForceIntegerMetrics));
52 	f.setFixedPitch(true);
53 	f.setKerning(false);
54 
55 	// Issue #585 Error message "Unknown font:" for Neovim 0.4.2+.
56 	// This case has always been hit, but results in user visible error messages for recent
57 	// releases. It is safe to ignore this case, which occurs at startup time.
58 	if (f.family().isEmpty()) {
59 		return false;
60 	}
61 
62 	QFontInfo fi(f);
63 	if (fi.family().compare(f.family(), Qt::CaseInsensitive) != 0 &&
64 			f.family().compare("Monospace", Qt::CaseInsensitive) != 0) {
65 		emit fontError(QString("Unknown font: %1").arg(f.family()));
66 		return false;
67 	}
68 
69 	if (!force) {
70 		if (!fi.fixedPitch()) {
71 			emit fontError(QString("%1 is not a fixed pitch font").arg(f.family()));
72 			return false;
73 		}
74 
75 		if (isBadMonospace(f)) {
76 			emit fontError(QString("Warning: Font \"%1\" reports bad fixed pitch metrics").arg(f.family()));
77 		}
78 	}
79 
80 	setFont(f);
81 	setCellSize();
82 	emit shellFontChanged();
83 	return true;
84 }
85 
86 /// Don't used this, use setShellFont instead;
setFont(const QFont & f)87 void ShellWidget::setFont(const QFont& f)
88 {
89 	QWidget::setFont(f);
90 }
91 
setLineSpace(int height)92 void ShellWidget::setLineSpace(int height)
93 {
94 	if (height != m_lineSpace) {
95 		m_lineSpace = height;
96 		setCellSize();
97 		emit shellFontChanged();
98 	}
99 }
100 
101 /// Changed the cell size based on font metrics:
102 /// - Height is either the line spacing or the font
103 ///   height, the leading may be negative and we want the
104 ///   larger value
105 /// - Width is the width of the "W" character
setCellSize()106 void ShellWidget::setCellSize()
107 {
108 	QFontMetrics fm(font());
109 	m_ascent = fm.ascent();
110 	m_cellSize = QSize(fm.width('W'),
111 			qMax(fm.lineSpacing(), fm.height()) + m_lineSpace);
112 	setSizeIncrement(m_cellSize);
113 }
cellSize() const114 QSize ShellWidget::cellSize() const
115 {
116 	return m_cellSize;
117 }
118 
getNeovimCursorRect(QRect cellRect)119 QRect ShellWidget::getNeovimCursorRect(QRect cellRect) noexcept
120 {
121 	QRect cursorRect{ cellRect };
122 	switch (m_cursor.GetShape())
123 	{
124 		case Cursor::Shape::Block:
125 			break;
126 
127 		case Cursor::Shape::Horizontal:
128 		{
129 			const int height{ cursorRect.height() * m_cursor.GetPercentage() / 100 };
130 			const int verticalOffset{ cursorRect.height() - height };
131 			cursorRect.adjust(0, verticalOffset, 0, verticalOffset);
132 			cursorRect.setHeight(height);
133 		}
134 		break;
135 
136 		case Cursor::Shape::Vertical:
137 		{
138 			cursorRect.setWidth(cursorRect.width() * m_cursor.GetPercentage() / 100);
139 		}
140 		break;
141 	}
142 
143 	return cursorRect;
144 }
145 
paintNeovimCursorBackground(QPainter & p,QRect cellRect)146 void ShellWidget::paintNeovimCursorBackground(QPainter& p, QRect cellRect) noexcept
147 {
148 	const QRect cursorRect{ getNeovimCursorRect(cellRect) };
149 
150 	QColor cursorBackgroundColor{ m_cursor.GetBackgroundColor() };
151 	if (!cursorBackgroundColor.isValid()) {
152 		// Neovim can send QColor::Invalid, indicating the default colors with
153 		// an inverted foreground/background.
154 		cursorBackgroundColor = foreground();
155 	}
156 
157 	// If the window does not have focus, draw an outline around the cursor cell.
158 	if (!hasFocus()) {
159 		QRect noFocusCursorRect{ cellRect };
160 		noFocusCursorRect.adjust(-1, -1, -1, -1);
161 
162 		QPen pen{ cursorBackgroundColor };
163 		pen.setWidth(2);
164 
165 		p.setPen(pen);
166 		p.drawRect(cellRect);
167 		return;
168 	}
169 
170 	p.fillRect(cursorRect, cursorBackgroundColor);
171 }
172 
paintNeovimCursorForeground(QPainter & p,QRect cellRect,QPoint pos,QChar character)173 void ShellWidget::paintNeovimCursorForeground(
174 	QPainter& p,
175 	QRect cellRect,
176 	QPoint pos,
177 	QChar character) noexcept
178 {
179 	// No focus: cursor is outline with default foreground color.
180 	if (!hasFocus()) {
181 		return;
182 	}
183 
184 	const QRect cursorRect{ getNeovimCursorRect(cellRect) };
185 
186 	QColor cursorForegroundColor{ m_cursor.GetForegroundColor() };
187 	if (!cursorForegroundColor.isValid()) {
188 		// Neovim can send QColor::Invalid, indicating the default colors with
189 		// an inverted foreground/background.
190 		cursorForegroundColor = background();
191 	}
192 
193 	// Painting the cursor requires setting the clipping region. This is used
194 	// to paint only the part of text within the cursor region. Save the active
195 	// clipping settings, and restore them after the paint operation.
196 	const QRegion oldClippingRegion{ p.clipRegion() };
197 	const bool oldClippingSetting{ p.hasClipping() };
198 
199 	p.setClipping(true);
200 	p.setClipRect(cursorRect) ;
201 
202 	p.setPen(cursorForegroundColor);
203 	p.drawText(pos, character);
204 
205 	p.setClipRegion(oldClippingRegion);
206 	p.setClipping(oldClippingSetting);
207 }
208 
paintEvent(QPaintEvent * ev)209 void ShellWidget::paintEvent(QPaintEvent *ev)
210 {
211 	QPainter p(this);
212 
213 	p.setClipping(true);
214 
215 	foreach(QRect rect, ev->region().rects()) {
216 		int start_row = rect.top() / m_cellSize.height();
217 		int end_row = rect.bottom() / m_cellSize.height();
218 		int start_col = rect.left() / m_cellSize.width();
219 		int end_col = rect.right() / m_cellSize.width();
220 
221 		// Paint margins
222 		if (end_col >= m_contents.columns()) {
223 			end_col = m_contents.columns() - 1;
224 		}
225 		if (end_row >= m_contents.rows()) {
226 			end_row = m_contents.rows() - 1;
227 		}
228 
229 		// end_col/row is inclusive
230 		for (int i=start_row; i<=end_row; i++) {
231 			for (int j=end_col; j>=start_col; j--) {
232 
233 				const Cell& cell = m_contents.constValue(i,j);
234 				int chars = cell.IsDoubleWidth() ? 2 : 1;
235 				QRect r = absoluteShellRect(i, j, 1, chars);
236 				QRect ovflw = absoluteShellRect(i, j, 1, chars + 1);
237 
238 				p.setClipRegion(ovflw);
239 
240 				if (j <= 0 || !contents().constValue(i, j-1).IsDoubleWidth()) {
241 					// Only paint bg/fg if this is not the second cell
242 					// of a wide char
243 					QColor bgColor{ cell.GetBackgroundColor() };
244 					if (!bgColor.isValid()) {
245 						bgColor = (cell.IsReverse()) ? foreground() : background();
246 					}
247 					p.fillRect(r, bgColor);
248 
249 					const QPoint curPos{ j, i };
250 					const bool isCursorVisibleAtCell{ m_cursor.IsVisible() && m_cursor_pos == curPos };
251 
252 					if (isCursorVisibleAtCell) {
253 						paintNeovimCursorBackground(p, r);
254 					}
255 
256 					if (cell.GetCharacter() != ' ') {
257 						QColor fgColor{ cell.GetForegroundColor() };
258 						if (!fgColor.isValid()) {
259 							fgColor = (cell.IsReverse()) ? background() : foreground();
260 						}
261 						p.setPen(fgColor);
262 
263 						if (cell.IsBold() || cell.IsItalic()) {
264 							QFont f = p.font();
265 							f.setBold(cell.IsBold());
266 							f.setItalic(cell.IsItalic());
267 							p.setFont(f);
268 						} else {
269 							p.setFont(font());
270 						}
271 
272 						// Draw chars at the baseline
273 						const int cellTextOffset{ m_ascent + (m_lineSpace / 2) };
274 						const QPoint pos{ r.left(), r.top() + cellTextOffset};
275 						const uint character{ cell.GetCharacter() };
276 
277 						p.drawText(pos, QString::fromUcs4(&character, 1));
278 
279 						if (isCursorVisibleAtCell) {
280 							paintNeovimCursorForeground(p, r, pos, character);
281 						}
282 					}
283 				}
284 
285 				// Draw "undercurl" at the bottom of the cell
286 				if (cell.IsUnderline()|| cell.IsUndercurl()) {
287 					QPen pen = QPen();
288 					if (cell.IsUndercurl()) {
289 						if (cell.GetSpecialColor().isValid()) {
290 							pen.setColor(cell.GetSpecialColor());
291 						} else if (special().isValid()) {
292 							pen.setColor(special());
293 						} else if (cell.GetForegroundColor().isValid()) {
294 							pen.setColor(cell.GetForegroundColor());
295 						} else {
296 							pen.setColor(foreground());
297 						}
298 					} else if (cell.IsUnderline()) {
299 						if (cell.GetForegroundColor().isValid()) {
300 							pen.setColor(cell.GetForegroundColor());
301 						} else {
302 							pen.setColor(foreground());
303 						}
304 					}
305 
306 					p.setPen(pen);
307 					QPoint start = r.bottomLeft();
308 					QPoint end = r.bottomRight();
309 					start.ry()--; end.ry()--;
310 					if (cell.IsUnderline()) {
311 						p.drawLine(start, end);
312 					} else if (cell.IsUndercurl()) {
313 						static const int val[8] = {1, 0, 0, 1, 1, 2, 2, 2};
314 						QPainterPath path(start);
315 						for (int i = start.x() + 1; i <= end.x(); i++) {
316 							int offset = val[i % 8];
317 							path.lineTo(QPoint(i, start.y() - offset));
318 						}
319 						p.drawPath(path);
320 					}
321 				}
322 			}
323 		}
324 	}
325 
326 	p.setClipping(false);
327 
328 	QRect shellArea = absoluteShellRect(0, 0,
329 				m_contents.rows(), m_contents.columns());
330 	QRegion margins = QRegion(rect()).subtracted(shellArea);
331 	foreach(QRect margin, margins.intersected(ev->region()).rects()) {
332 		p.fillRect(margin, background());
333 	}
334 
335 #if 0
336 	// Draw DEBUG rulers
337 	for (int i=m_cellSize.width(); i<width(); i+=m_cellSize.width()) {
338 		p.setPen(QPen(Qt::red, 1,  Qt::DashLine));
339 		p.drawLine(i, 0, i, height());
340 	}
341 	for (int i=m_cellSize.height(); i<width(); i+=m_cellSize.height()) {
342 		p.setPen(QPen(Qt::red, 1,  Qt::DashLine));
343 		p.drawLine(0, i, width(), i);
344 	}
345 #endif
346 }
347 
resizeEvent(QResizeEvent * ev)348 void ShellWidget::resizeEvent(QResizeEvent *ev)
349 {
350 	int cols = ev->size().width() / m_cellSize.width();
351 	int rows = ev->size().height() / m_cellSize.height();
352 	resizeShell(rows, cols);
353 	QWidget::resizeEvent(ev);
354 }
355 
sizeHint() const356 QSize ShellWidget::sizeHint() const
357 {
358 	return QSize(m_cellSize.width()*m_contents.columns(),
359 				m_cellSize.height()*m_contents.rows());
360 }
361 
resizeShell(int n_rows,int n_columns)362 void ShellWidget::resizeShell(int n_rows, int n_columns)
363 {
364 	if (n_rows != rows() || n_columns != columns()) {
365 		m_contents.resize(n_rows, n_columns);
366 		updateGeometry();
367 	}
368 }
369 
setSpecial(const QColor & color)370 void ShellWidget::setSpecial(const QColor& color)
371 {
372 	m_spColor = color;
373 }
374 
special() const375 QColor ShellWidget::special() const
376 {
377 	return m_spColor;
378 }
379 
setBackground(const QColor & color)380 void ShellWidget::setBackground(const QColor& color)
381 {
382 	m_bgColor = color;
383 }
384 
background() const385 QColor ShellWidget::background() const
386 {
387 	// See 'default_colors_set' in Neovim ':help ui-linegrid'.
388 	// QColor::Invalid indicates the default color (-1), which should be
389 	// rendered as white or black based on Neovim 'background'.
390 	if (!m_bgColor.isValid())
391 	{
392 		switch (m_background)
393 		{
394 			case Background::Light:
395 				return Qt::white;
396 
397 			case Background::Dark:
398 				return Qt::black;
399 
400 			default:
401 				return Qt::black;
402 		}
403 	}
404 
405 	return m_bgColor;
406 }
407 
setForeground(const QColor & color)408 void ShellWidget::setForeground(const QColor& color)
409 {
410 	m_fgColor = color;
411 }
412 
foreground() const413 QColor ShellWidget::foreground() const
414 {
415 	// See ShellWidget::background() for more details.
416 	if (!m_bgColor.isValid())
417 	{
418 		switch (m_background)
419 		{
420 			case Background::Light:
421 				return Qt::black;
422 
423 			case Background::Dark:
424 				return Qt::white;
425 
426 			default:
427 				return Qt::white;
428 		}
429 	}
430 
431 	return m_fgColor;
432 }
433 
contents() const434 const ShellContents& ShellWidget::contents() const
435 {
436 	return m_contents;
437 }
438 
439 /// Put text in position, returns the amount of colums used
put(const QString & text,int row,int column,QColor fg,QColor bg,QColor sp,bool bold,bool italic,bool underline,bool undercurl,bool reverse)440 int ShellWidget::put(
441 	const QString& text,
442 	int row,
443 	int column,
444 	QColor fg,
445 	QColor bg,
446 	QColor sp,
447 	bool bold,
448 	bool italic,
449 	bool underline,
450 	bool undercurl,
451 	bool reverse)
452 {
453 	HighlightAttribute hl_attr = { fg, bg, sp,
454 		reverse, italic, bold, underline, undercurl };
455 	return put(text, row, column, hl_attr);
456 }
457 
put(const QString & text,int row,int column,const HighlightAttribute & hl_attr)458 int ShellWidget::put(
459 	const QString& text,
460 	int row,
461 	int column,
462 	const HighlightAttribute& hl_attr)
463 {
464 	int cols_changed = m_contents.put(text, row, column, hl_attr);
465 	if (cols_changed > 0) {
466 		QRect rect = absoluteShellRect(row, column, 1, cols_changed);
467 		update(rect);
468 	}
469 	return cols_changed;
470 }
471 
clearRow(int row)472 void ShellWidget::clearRow(int row)
473 {
474 	m_contents.clearRow(row);
475 	QRect rect = absoluteShellRect(row, 0, 1, m_contents.columns());
476 	update(rect);
477 }
clearShell(QColor bg)478 void ShellWidget::clearShell(QColor bg)
479 {
480 	m_contents.clearAll(bg);
481 	update();
482 }
483 
484 /// Clear region (row0, col0) to - but not including (row1, col1)
clearRegion(int row0,int col0,int row1,int col1)485 void ShellWidget::clearRegion(int row0, int col0, int row1, int col1)
486 {
487 	m_contents.clearRegion(row0, col0, row1, col1);
488 	// FIXME: check offset error
489 	update(absoluteShellRect(row0, col0, row1-row0, col1-col0));
490 }
491 
492 /// Scroll count rows (positive numbers move content up)
scrollShell(int rows)493 void ShellWidget::scrollShell(int rows)
494 {
495 	if (rows != 0) {
496 		m_contents.scroll(rows);
497 		// Qt's delta uses positive numbers to move down
498 		scroll(0, -rows*m_cellSize.height());
499 	}
500 }
501 /// Scroll an area, count rows (positive numbers move content up)
scrollShellRegion(int row0,int row1,int col0,int col1,int rows)502 void ShellWidget::scrollShellRegion(int row0, int row1, int col0,
503 			int col1, int rows)
504 {
505 	if (rows != 0) {
506 		m_contents.scrollRegion(row0, row1, col0, col1, rows);
507 		// Qt's delta uses positive numbers to move down
508 		QRect r = absoluteShellRect(row0, col0, row1-row0, col1-col0);
509 		scroll(0, -rows*m_cellSize.height(), r);
510 	}
511 }
512 
513 /// Convert Area in row/col coordinates into pixel coordinates
514 ///
515 /// (row0, col0) is the start position and rowcount/colcount the size
absoluteShellRect(int row0,int col0,int rowcount,int colcount)516 QRect ShellWidget::absoluteShellRect(int row0, int col0, int rowcount, int colcount)
517 {
518 	return QRect(col0*m_cellSize.width(), row0*m_cellSize.height(),
519 			colcount*m_cellSize.width(), rowcount*m_cellSize.height());
520 }
521 
fontFamily() const522 QString ShellWidget::fontFamily() const
523 {
524 	return QFontInfo(font()).family();
525 }
fontSize() const526 qreal ShellWidget::fontSize() const
527 {
528 	return font().pointSizeF();
529 }
530 
rows() const531 int ShellWidget::rows() const
532 {
533 	return m_contents.rows();
534 }
535 
columns() const536 int ShellWidget::columns() const
537 {
538 	return m_contents.columns();
539 }
540 
setNeovimCursor(uint64_t row,uint64_t col)541 void ShellWidget::setNeovimCursor(uint64_t row, uint64_t col) noexcept
542 {
543 	// Clear the stale cursor
544 	update(neovimCursorRect());
545 
546 	// Update cursor position, draw at new location
547 	m_cursor_pos = QPoint(col, row);
548 	m_cursor.ResetTimer();
549 	update(neovimCursorRect());
550 }
551 
552 /// The top left corner position (pixel) for the cursor
neovimCursorTopLeft() const553 QPoint ShellWidget::neovimCursorTopLeft() const noexcept
554 {
555 	const QSize cSize{ cellSize() };
556 	const int xPixels{ m_cursor_pos.x() * cSize.width() };
557 	const int yPixels{ m_cursor_pos.y() * cSize.height() };
558 
559 	return { xPixels, yPixels };
560 }
561 
562 /// Get the area filled by the cursor
neovimCursorRect() const563 QRect ShellWidget::neovimCursorRect() const noexcept
564 {
565 	QRect cursor{ neovimCursorTopLeft(), cellSize() };
566 
567 	const Cell& cell{ contents().constValue(m_cursor_pos.y(), m_cursor_pos.x()) };
568 	if (cell.IsDoubleWidth()) {
569 		cursor.setWidth(cursor.width() * 2);
570 	}
571 
572 	return cursor;
573 }
574 
handleCursorChanged()575 void ShellWidget::handleCursorChanged()
576 {
577 	update(neovimCursorRect());
578 }
579 
580 
581