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