We need config and session data. config is what the user changes. Only read by the app. session is what the app remembers. Read and written by the app.
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_session('general', 'font_size')
|
|
new_font_size = max(4, min(50, old_font_size - direction))
|
|
self.model.settings.set_session('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_session('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))
|