312 lines
12 KiB
Python
312 lines
12 KiB
Python
import math
|
|
import os
|
|
import re
|
|
import time
|
|
from typing import Optional, List
|
|
import PyQt6.QtGui
|
|
from PyQt6 import QtGui
|
|
|
|
from PyQt6.QtCore import *
|
|
from PyQt6.QtGui import *
|
|
from PyQt6.QtGui import QMouseEvent
|
|
from PyQt6.QtWidgets import *
|
|
|
|
from ScaledScrollBar import ScaledScrollBar
|
|
from highlight import Highlight
|
|
from highlight_regex import HighlightRegex
|
|
from highlight_selection import HighlightSelection
|
|
from highlighted_range import HighlightedRange
|
|
from line import Line
|
|
from logFileModel import LogFileModel
|
|
import re
|
|
|
|
from ravenui import RavenUI
|
|
from settings import Settings
|
|
from watchdog.observers import Observer
|
|
from watchdog.events import FileSystemEventHandler
|
|
import threading
|
|
|
|
|
|
class FileObserver(FileSystemEventHandler):
|
|
|
|
def __init__(self, big_text):
|
|
super(FileObserver, self).__init__()
|
|
self.big_text = big_text
|
|
|
|
def on_modified(self, event):
|
|
# slow down the updates. This is needed, because the file is modified
|
|
# constantly, which would lead to constant re-rendering, which would
|
|
# block the UI thread an make the UI unresponsive.
|
|
# Note: we don't miss events, because they are queued and de-duplicated
|
|
time.sleep(0.5)
|
|
self.big_text.update()
|
|
|
|
|
|
class FileWatchdogThread(QRunnable):
|
|
|
|
def __init__(self, big_text, file: str):
|
|
super(FileWatchdogThread, self).__init__()
|
|
self.file = file
|
|
self.big_text = big_text
|
|
self.observer = Observer()
|
|
|
|
def run(self) -> None:
|
|
self.observer.schedule(FileObserver(self.big_text), self.file)
|
|
self.observer.start()
|
|
|
|
def destruct(self):
|
|
self.observer.stop()
|
|
|
|
|
|
class BigText(QWidget):
|
|
|
|
def __init__(self, model: LogFileModel):
|
|
super(BigText, self).__init__()
|
|
|
|
self.model = model
|
|
|
|
self.watchdog = FileWatchdogThread(self, model.get_file())
|
|
QThreadPool.globalInstance().start(self.watchdog)
|
|
|
|
self.grid = QGridLayout()
|
|
self.grid.setContentsMargins(0, 0, 0, 0)
|
|
self.grid.setHorizontalSpacing(0)
|
|
self.grid.setVerticalSpacing(0)
|
|
self.setLayout(self.grid)
|
|
|
|
big_text = InnerBigText(self, model)
|
|
big_text.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding))
|
|
|
|
self.h_scroll_bar = QScrollBar(Qt.Orientation.Horizontal)
|
|
self.h_scroll_bar.setMinimum(0)
|
|
self.h_scroll_bar.setMaximum(1)
|
|
self.h_scroll_bar.valueChanged.connect(big_text.h_scroll_event)
|
|
|
|
self.v_scroll_bar = ScaledScrollBar()
|
|
self.v_scroll_bar.setPageStep(1)
|
|
self.v_scroll_bar.valueChanged.connect(big_text.v_scroll_event)
|
|
|
|
self.grid.addWidget(big_text, 0, 0)
|
|
self.grid.addWidget(self.h_scroll_bar, 1, 0)
|
|
self.grid.addWidget(self.v_scroll_bar, 0, 1)
|
|
|
|
def get_file(self):
|
|
return self.model.get_file()
|
|
|
|
def destruct(self):
|
|
self.watchdog.destruct()
|
|
|
|
|
|
class InnerBigText(QWidget):
|
|
_byte_offset = 0
|
|
_left_offset = 0
|
|
scroll_lines = 0
|
|
longest_line = 0
|
|
|
|
def __init__(self, parent: BigText, model: LogFileModel):
|
|
super(InnerBigText, self).__init__()
|
|
self.model = model
|
|
self.parent = parent
|
|
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
|
self.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
|
|
|
|
self.update_font_metrics(QPainter(self))
|
|
self.lines = []
|
|
self.selection_highlight = HighlightSelection()
|
|
|
|
def keyPressEvent(self, e: QKeyEvent) -> None:
|
|
|
|
# print("%s + %s" % (e.keyCombination().keyboardModifiers(), e.key()))
|
|
if e.modifiers() == Qt.KeyboardModifier.NoModifier:
|
|
lines_to_scroll = math.floor(self.lines_shown()) - 1
|
|
if e.key() == Qt.Key.Key_PageUp:
|
|
self.scroll_by_lines(-lines_to_scroll)
|
|
if e.key() == Qt.Key.Key_PageDown:
|
|
self.scroll_by_lines(lines_to_scroll)
|
|
if e.key() == 16777235: # page up
|
|
self.scroll_by_lines(-3)
|
|
if e.key() == 16777237: # page down
|
|
self.scroll_by_lines(3)
|
|
if e.modifiers() == Qt.KeyboardModifier.ControlModifier and e.key() == 67: # ctrl + c
|
|
self.copy_selection()
|
|
if e.modifiers() == Qt.KeyboardModifier.ControlModifier and e.key() == 65: # ctrl + a
|
|
self.selection_highlight.start_byte = 0
|
|
self.selection_highlight.end_byte = self.model.byte_count()
|
|
self.update()
|
|
|
|
def wheelEvent(self, event: QWheelEvent):
|
|
direction = 1 if event.angleDelta().y() < 0 else -1
|
|
if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
|
# self.model.settings.update_font_size(-direction)
|
|
old_font_size = self.model.settings.getint('general', 'font_size')
|
|
new_font_size = old_font_size - direction
|
|
self.model.settings.set('general', 'font_size', str(new_font_size))
|
|
RavenUI.update_ui()
|
|
self.update()
|
|
else:
|
|
# print("wheel event fired :) %s" % (direction))
|
|
self.scroll_by_lines(direction * 3)
|
|
|
|
def scroll_by_lines(self, scroll_lines: int):
|
|
self.scroll_lines = scroll_lines
|
|
self.update()
|
|
self.parent.v_scroll_bar.setValue(self._byte_offset)
|
|
|
|
def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
|
|
if e.buttons() == Qt.MouseButton.LeftButton:
|
|
offset = self.to_byte_offset(e)
|
|
self.selection_highlight.set_start(offset)
|
|
self.selection_highlight.set_end_byte(offset)
|
|
self.update()
|
|
|
|
def mouseMoveEvent(self, e: QMouseEvent):
|
|
current_byte = self.to_byte_offset(e)
|
|
|
|
if self.selection_highlight.end_byte != current_byte:
|
|
self.selection_highlight.set_end_byte(current_byte)
|
|
self.update()
|
|
# print("-> %s,%s" %(self._selection_start_byte, self._selection_end_byte))
|
|
line_number = self.y_pos_to_line(e.pos().y())
|
|
column_in_line = self.x_pos_to_column(e.pos().x())
|
|
if line_number == 0:
|
|
self.scroll_by_lines(-1)
|
|
if line_number + 1 >= int(self.lines_shown()):
|
|
self.scroll_by_lines(1)
|
|
if column_in_line <= 1:
|
|
self._left_offset = max(0, self._left_offset - 2)
|
|
self.update()
|
|
if column_in_line + 1 >= self.columns_shown():
|
|
self._left_offset = self._left_offset + 2
|
|
self.update()
|
|
|
|
def h_scroll_event(self, left_offset: int):
|
|
self._left_offset = left_offset
|
|
# print("left_offset: %d" % left_offset)
|
|
self.update()
|
|
|
|
def v_scroll_event(self, byte_offset: int):
|
|
self._byte_offset = byte_offset
|
|
self.update()
|
|
|
|
def update_longest_line(self, length: int):
|
|
width_in_chars = self.width() / self.char_width
|
|
# print("width_in_chars: %d" % width_in_chars)
|
|
if self.longest_line < length:
|
|
self.longest_line = length
|
|
maximum = max(0, length - width_in_chars + 1)
|
|
self.parent.h_scroll_bar.setMaximum(maximum)
|
|
|
|
def y_pos_to_line(self, y: int) -> int:
|
|
return int(y / self.char_height)
|
|
|
|
def x_pos_to_column(self, x: int) -> int:
|
|
return round(x / self.char_width)
|
|
|
|
def lines_shown(self) -> float:
|
|
return self.height() / float(self.char_height)
|
|
|
|
def columns_shown(self) -> float:
|
|
return self.width() / float(self.char_width)
|
|
|
|
def to_byte_offset(self, e: QMouseEvent) -> int:
|
|
line_number = self.y_pos_to_line(e.pos().y())
|
|
|
|
if line_number < len(self.lines):
|
|
line = self.lines[line_number]
|
|
column_in_line = self.x_pos_to_column(e.pos().x()) + self._left_offset
|
|
char_in_line = min(column_in_line, line.length())
|
|
# print("%s in line %s" % (char_in_line, line_number))
|
|
byte_in_line = line.char_index_to_byte(char_in_line)
|
|
current_byte = line.byte_offset() + byte_in_line
|
|
# print("%s + %s = %s" % (line.byte_offset(), char_in_line, current_byte))
|
|
else:
|
|
current_byte = self.model.byte_count()
|
|
return current_byte
|
|
|
|
def copy_selection(self):
|
|
if self.selection_highlight.start_byte != self.selection_highlight.end_byte:
|
|
start = min(self.selection_highlight.start_byte, self.selection_highlight.end_byte)
|
|
end = max(self.selection_highlight.start_byte, self.selection_highlight.end_byte)
|
|
selected_text = self.model.read_range(start, end)
|
|
cb = QApplication.clipboard()
|
|
cb.setText(selected_text)
|
|
|
|
def paintEvent(self, event: QPaintEvent) -> None:
|
|
# print("paintEvent")
|
|
painter = QPainter(self)
|
|
# painter.setFont(self.model.settings.font())
|
|
painter.setFont(QFont("monospace", self.model.settings.getint('general', "font_size")))
|
|
painter.setPen(QColor(0, 0, 0))
|
|
self.update_font_metrics(painter)
|
|
|
|
lines_to_show = self.lines_shown()
|
|
# print("%s / %s = %s" %(self.height(), float(self.char_height), lines_to_show))
|
|
|
|
self.lines = self.model.data(self._byte_offset, self.scroll_lines, lines_to_show)
|
|
# print("lines_to_show: %d returned: %d" % (lines_to_show, len(self.lines)))
|
|
self.scroll_lines = 0
|
|
self._byte_offset = self.lines[0].byte_offset() if len(self.lines) > 0 else 0
|
|
# document length == maximum + pageStep + aFewBytesSoThatTheLastLineIsShown
|
|
self.parent.v_scroll_bar.setMaximum(self.model.byte_count() - 1)
|
|
|
|
for l in self.lines:
|
|
self.update_longest_line(len(l.line()))
|
|
|
|
highlighters = self.model.highlights
|
|
if self.model.get_query_highlight():
|
|
highlighters = highlighters + [self.model.get_query_highlight()]
|
|
highlighters = highlighters + [self.selection_highlight] # selection highlight should be last
|
|
|
|
# draw hightlights first - some characters may overlap to the next line
|
|
# by drawing the background hightlights first we prevent that the hightlight
|
|
# draws over a character
|
|
y_line_offset = self.char_height;
|
|
for l in self.lines:
|
|
for h in highlighters:
|
|
optional_highlight_range = h.compute_highlight(l)
|
|
if optional_highlight_range:
|
|
for highlight in optional_highlight_range:
|
|
self.draw_highlight(highlight, painter, y_line_offset)
|
|
y_line_offset = y_line_offset + self.char_height
|
|
|
|
left_offset = -1 * self._left_offset * self.char_width
|
|
y_line_offset = self.char_height;
|
|
for l in self.lines:
|
|
painter.drawText(left_offset, y_line_offset, l.line())
|
|
y_line_offset = y_line_offset + self.char_height
|
|
|
|
painter.end()
|
|
|
|
def draw_highlight(self, highlight: HighlightedRange, painter: QPainter, y_line_offset: int):
|
|
left_offset = -1 * self._left_offset * self.char_width
|
|
x1 = highlight.get_start() * self.char_width
|
|
width = highlight.get_width() * self.char_width
|
|
|
|
y1 = y_line_offset - self.char_height + self.char_height / 7
|
|
height = self.char_height
|
|
|
|
if highlight.is_highlight_full_line():
|
|
full_width = Settings.max_line_length() * self.char_width
|
|
rect = QRect(left_offset, y1, full_width, height)
|
|
self.highlight_background(painter, rect, highlight.get_brush_full_line())
|
|
|
|
rect = QRect(left_offset + x1, y1, width, height)
|
|
self.highlight_background(painter, rect, highlight.get_brush())
|
|
|
|
def highlight_background(self, painter: QPainter, rect: QRect, brush: QBrush):
|
|
old_brush = painter.brush()
|
|
old_pen = painter.pen()
|
|
painter.setBrush(brush)
|
|
painter.setPen(Qt.PenStyle.NoPen)
|
|
painter.drawRoundedRect(rect, 3.0, 3.0)
|
|
painter.setBrush(old_brush)
|
|
painter.setPen(old_pen)
|
|
|
|
def update_font_metrics(self, painter: QPainter):
|
|
fm: QFontMetrics = painter.fontMetrics()
|
|
self.char_height = fm.height()
|
|
self.char_width = fm.averageCharWidth() # all chars have same width for monospace font
|
|
text = "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
|
|
self.char_width = fm.horizontalAdvance(text) / float(len(text))
|
|
# print("font width=%s height=%s" % (self.char_width, self.char_height))
|