diff --git a/src/plugins/logfile/filterwidget.py b/src/plugins/logfile/filterwidget.py index fc6e7ed..59e5292 100644 --- a/src/plugins/logfile/filterwidget.py +++ b/src/plugins/logfile/filterwidget.py @@ -38,7 +38,8 @@ class FilterTask(QRunnable): on_before: Callable[[], None], on_finish: Callable[[], None], show_only_matches: bool, - matches_separator: str + matches_separator: str, + zoned_plugin_registry: ZonedPluginRegistry ): super(FilterTask, self).__init__() self.source_model = source_model @@ -53,6 +54,7 @@ class FilterTask(QRunnable): self.filter_match_found_listeners = filter_match_found_listeners self.show_only_matches = show_only_matches self.matches_separator = matches_separator + self.zoned_plugin_registry = zoned_plugin_registry def only_matches(self, line: str, regex: re.Pattern): result = "" @@ -87,7 +89,10 @@ class FilterTask(QRunnable): listener(-1, -1) # notify listeners that a new search started hits_count = 0 + hits_positions: set[float] = set(()) + self.zoned_plugin_registry.execute("update_hit_positions", hits_positions) last_progress_report = time.time() + source_file_size = self.source_model.byte_count() try: with open(self.source_model.get_file(), "rb") as source: source.seek(self.source_model.range_start) @@ -121,6 +126,12 @@ class FilterTask(QRunnable): target.write(line.encode("utf8")) hits_count = hits_count + 1 + hits_positions_before = len(hits_positions) + hits_positions.add(round(source_line_offset / source_file_size, 3)) + hits_positions_after = len(hits_positions) + if hits_positions_before != hits_positions_after: + self.zoned_plugin_registry.execute("update_hit_positions", hits_positions) + # sometime buffering can hide results for a while # We force a flush periodically. if line_count % 10000 == 0: @@ -350,6 +361,7 @@ class FilterWidget(QWidget): lambda: self.search_is_running.emit(True), lambda: self.search_is_running.emit(False), show_only_matches, - self.matches_separator.text() + self.matches_separator.text(), + self._zoned_plugin_registry ) QThreadPool.globalInstance().start(self.filter_task) diff --git a/src/plugins/logfile/fulltabwidget.py b/src/plugins/logfile/fulltabwidget.py index a855a16..b274ee2 100644 --- a/src/plugins/logfile/fulltabwidget.py +++ b/src/plugins/logfile/fulltabwidget.py @@ -1,6 +1,7 @@ from PySide6.QtWidgets import * from PySide6.QtCore import * +from src.pluginbase import PluginBase from src.ui.bigtext.bigtext import BigText from src.plugins.logfile.filterviewsyncer import FilterViewSyncer from src.plugins.logfile.filterwidget import FilterWidget @@ -17,6 +18,7 @@ class FullTabWidget(Tab): self._model = model self._zoned_plugin_registry = zoned_plugin_registry self.file_view = BigText(model) + self._zoned_plugin_registry.register_plugin("TabWidgetPlugin", TabWidgetPlugin(self.file_view)) self.filter_hit_view = FilterWidget(self._model, self._zoned_plugin_registry) self.filter_view_syncer = FilterViewSyncer(self.file_view) self.filter_hit_view.add_line_click_listener(self.filter_view_syncer.click_listener) @@ -51,3 +53,12 @@ class FullTabWidget(Tab): # overriding abstract method def on_reveal(self): self.filter_hit_view.on_reveal() + + +class TabWidgetPlugin(PluginBase): + def __init__(self, file_view: BigText): + super(TabWidgetPlugin, self).__init__() + self._file_view = file_view + + def update_hit_positions(self, hit_positions: set[float]): + self._file_view.update_hit_positions(hit_positions) diff --git a/src/ui/bigtext/bigtext.py b/src/ui/bigtext/bigtext.py index 7b5ea37..d207df4 100644 --- a/src/ui/bigtext/bigtext.py +++ b/src/ui/bigtext/bigtext.py @@ -119,6 +119,10 @@ class BigText(QWidget): def get_file(self): return self.model.get_file() + def update_hit_positions(self, hit_positions: set[float]): + if self.range_limit: + self.range_limit.update_hit_positions(hit_positions) + def add_line_click_listener(self, listener: Callable[[int], None]): """ :param listener: a callable, the parameter is the byte offset of the clicked line diff --git a/src/ui/rangeslider.py b/src/ui/rangeslider.py index ab1009f..cb77b7b 100644 --- a/src/ui/rangeslider.py +++ b/src/ui/rangeslider.py @@ -4,7 +4,7 @@ from enum import Enum import PySide6 from PySide6 import QtGui from PySide6.QtCore import QRect, QPoint, Signal -from PySide6.QtGui import QPainter, Qt +from PySide6.QtGui import QPainter, Qt, QColor, QPen from PySide6.QtWidgets import QWidget from src.pluginregistry import PluginRegistry @@ -53,6 +53,7 @@ class RangeSlider(QWidget): self.selected_handle = None self.selection_drag_range = (self.min_value, self.max_value) self.drag_y_offset_in_handle = 0 + self._hit_positions: set[float] = set(()) def set_maximum(self, max: int): if self.max_value == max: @@ -65,12 +66,16 @@ class RangeSlider(QWidget): self.upper_value.value = max self._emit_value_changed() + def update_hit_positions(self, hit_positions: set[float]): + # print(f"updated hit positions in range slider:{len(hit_positions)} -> {hit_positions}") + self._hit_positions = hit_positions + def paintEvent(self, event: PySide6.QtGui.QPaintEvent) -> None: painter = QPainter(self) self._draw_background(painter) + self._draw_hits(painter) self._draw_handle(painter, self.lower_value) self._draw_handle(painter, self.upper_value, direction=-1) - painter.end() def _draw_background(self, painter: QPainter) -> None: @@ -111,6 +116,25 @@ class RangeSlider(QWidget): pixel = (self.height() - 2 * self._handle_width) * value_percent + self._handle_width return pixel + def _draw_hits(self, painter: QPainter) -> None: + color = to_qcolor("000000") + color.setAlpha(192) + pen = QPen(color) + pen.setWidth(1) + painter.setPen(pen) + + # compute where to draw a line and then deduplicate then, so that we don't draw lines multiple times + # this is for performance and because we use transparency and drawing a line multiple times would make it + # darker + paint_at_y_positions: set[int] = set(()) + for hit_position in self._hit_positions: + y = (self.height() - 2 * self._handle_width) * hit_position + self._handle_width + y = round(y) + paint_at_y_positions.add(y) + + for y in paint_at_y_positions: + painter.drawLine(2, y, 18, y) + def _pixel_to_value(self, pixel: int) -> int: pixel_percent = (pixel - self._handle_width) / (self.height() - 2 * self._handle_width) return int(math.floor(self.max_value * pixel_percent)) diff --git a/src/util/color.py b/src/util/color.py index b97f101..0ea6a95 100644 --- a/src/util/color.py +++ b/src/util/color.py @@ -7,12 +7,22 @@ def is_hex_color(color: str): return re.match("[0-9a-f]{6}", color, flags=re.IGNORECASE) +def is_hex_color_with_alpha(color: str): + return re.match("[0-9a-f]{8}", color, flags=re.IGNORECASE) + + def to_qcolor(color: str): if is_hex_color(color): red = int(color[0:2], 16) green = int(color[2:4], 16) blue = int(color[4:6], 16) return QColor(red, green, blue) + if is_hex_color_with_alpha(color): + red = int(color[0:2], 16) + green = int(color[2:4], 16) + blue = int(color[4:6], 16) + alpha = int(color[6:8], 16) + return QColor(red, green, blue, alpha) elif color in QColor().colorNames(): return QColor(color) return QColor(255, 255, 255)