117 lines
4.4 KiB
Python
117 lines
4.4 KiB
Python
#!/usr/bin/env python
|
|
# vim:fileencoding=utf-8
|
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
|
|
|
from typing import Tuple, Iterator, Union, Sequence
|
|
|
|
from PyQt5.QtCore import pyqtSignal, QTimer, QRect
|
|
from PyQt5.QtGui import QColor, QPainter, QFont, QFontMetrics, QRegion, QPen
|
|
from PyQt5.QtWidgets import QWidget
|
|
|
|
from .config import build_ansi_color_tables, Options
|
|
from .data_types import Line
|
|
from .utils import set_current_font_metrics
|
|
|
|
|
|
def ascii_width(fm: QFontMetrics) -> int:
|
|
ans = 0
|
|
for i in range(32, 128):
|
|
ans = max(ans, fm.widthChar(chr(i)))
|
|
return ans
|
|
|
|
|
|
class TerminalWidget(QWidget):
|
|
|
|
relayout_lines = pyqtSignal(object, object, object, object)
|
|
cells_per_line = 80
|
|
lines_per_screen = 24
|
|
|
|
def __init__(self, opts: Options, linebuf: Sequence[Line], parent: QWidget=None):
|
|
QWidget.__init__(self, parent)
|
|
self.linebuf = linebuf
|
|
self.setAutoFillBackground(True)
|
|
self.apply_opts(opts)
|
|
self.debounce_resize_timer = t = QTimer(self)
|
|
t.setSingleShot(True)
|
|
t.setInterval(50)
|
|
t.timeout.connect(self.do_layout)
|
|
|
|
def apply_opts(self, opts):
|
|
self.opts = opts
|
|
pal = self.palette()
|
|
pal.setColor(pal.Window, QColor(opts.background))
|
|
pal.setColor(pal.WindowText, QColor(opts.foreground))
|
|
self.setPalette(pal)
|
|
self.current_bg = pal.color(pal.Window)
|
|
self.current_fg = pal.color(pal.WindowText)
|
|
build_ansi_color_tables(opts)
|
|
f = QFont(opts.font_family)
|
|
f.setPointSizeF(opts.font_size)
|
|
self.setFont(f)
|
|
self.font_metrics = fm = QFontMetrics(self.font())
|
|
self.cell_height = fm.lineSpacing()
|
|
self.cell_width = ascii_width(fm)
|
|
set_current_font_metrics(fm, self.cell_width)
|
|
self.baseline_offset = fm.ascent()
|
|
self.do_layout()
|
|
|
|
def do_layout(self):
|
|
previous, self.cells_per_line = self.cells_per_line, self.width() // self.cell_width
|
|
previousl, self.lines_per_screen = self.lines_per_screen, self.height() // self.cell_height
|
|
if (previous, previousl) != (self.cells_per_line, self.lines_per_screen):
|
|
self.relayout_lines.emit(previous, self.cells_per_line, previousl, self.lines_per_screen)
|
|
self.hmargin = (self.width() - self.cells_per_line * self.cell_width) // 2
|
|
self.vmargin = (self.height() % self.cell_height) // 2
|
|
self.line_positions = tuple(self.vmargin + i * self.cell_height for i in range(self.lines_per_screen))
|
|
self.cell_positions = tuple(self.hmargin + i * self.cell_width for i in range(self.cells_per_line))
|
|
self.layout_size = self.size()
|
|
self.update()
|
|
|
|
def resizeEvent(self, ev):
|
|
self.debounce_resize_timer.start()
|
|
|
|
def dirty_lines(self, region: QRegion) -> Iterator[Tuple[int, QRegion]]:
|
|
w = self.width() - 2 * self.hmargin
|
|
for i, y in enumerate(self.line_positions):
|
|
ir = region.intersected(QRect(self.hmargin, y, w, self.cell_height))
|
|
if not ir.isEmpty():
|
|
yield i, ir
|
|
|
|
def dirty_cells(self, y: int, line_region: QRegion) -> Iterator[int]:
|
|
for i, x in enumerate(self.cell_positions):
|
|
if line_region.intersects(QRect(x, y, self.cell_width, self.cell_height)):
|
|
yield i
|
|
|
|
def line(self, screen_line: int) -> Union[Line, None]:
|
|
try:
|
|
return self.linebuf[screen_line]
|
|
except IndexError:
|
|
pass
|
|
|
|
def paintEvent(self, ev):
|
|
if self.size() != self.layout_size:
|
|
return
|
|
r = ev.region()
|
|
p = QPainter(self)
|
|
for lnum, line_region in self.dirty_lines(r):
|
|
line = self.line(lnum)
|
|
if line is not None:
|
|
ypos = self.line_positions[lnum]
|
|
for cnum in self.dirty_cells(ypos, line_region):
|
|
p.save()
|
|
self.paint_cell(p, line, cnum, ypos)
|
|
p.restore()
|
|
|
|
def paint_cell(self, painter: QPainter, line: Line, col: int, y: int) -> None:
|
|
x = self.cell_positions[col]
|
|
c = line.cursor_from(x)
|
|
fg, bg, decoration_fg = c.colors()
|
|
if fg is not None:
|
|
painter.setPen(QPen(fg))
|
|
if bg is not None:
|
|
r = QRect(x, y, self.cell_width, self.cell_height)
|
|
painter.fillRect(r, bg)
|
|
text = line.text_at(col)
|
|
if text.rstrip():
|
|
painter.drawText(x, y + self.baseline_offset, text)
|