Compare commits

..

16 Commits

Author SHA1 Message Date
3e793596c2 replace ScaledScrollBar with BigScrollBar
step 4 - add repeat actions

This has probably a problem. The repeat action is triggering updates asynchronously.
Which means we do not wait until it is done. Which means we can DOS ourselves.
2024-04-14 19:12:37 +02:00
7a574f7ed4 fix typo in word minimun 2024-04-14 09:37:52 +02:00
7d20bae74d replace ScaledScrollBar with BigScrollBar
step 3 - connect wheel event
2024-04-14 09:36:50 +02:00
9b9399f120 replace ScaledScrollBar with BigScrollBar
step 2 - connect the line up/down, page up/down events
2024-04-14 09:13:06 +02:00
3d6cf84cd7 replace ScaledScrollBar with BigScrollBar
step 1 - manually moving the slider
2024-04-13 08:47:36 +02:00
2b65e61e43 remove follow tail 2024-04-11 19:06:56 +02:00
6538e85f37 add positions of hits to the range slider 2024-03-28 20:21:07 +01:00
76f7baecf3 update PySide to 6.6.3 2024-03-27 19:18:29 +01:00
7f4f6ab004 use folder of the original file in "copy to file" action 2024-03-26 18:17:52 +01:00
270b3a8683 remove some debug logging 2024-03-26 18:15:30 +01:00
b8b4b4e790 make the original file name known to the filter view 2024-03-26 18:12:42 +01:00
66d6a728cc make it possible to activate highlighter only for specific file types
In stage 1 we use a glob pattern matching the file name.
Stage 2 (which I will maybe implement some day) might use some additional magic byte sequence for file type detection.
2024-03-25 19:23:24 +01:00
56189f4094 use transparent line background by default 2024-03-25 18:40:03 +01:00
5f30862a83 keep position on handle 2024-03-24 20:06:00 +01:00
017a51a24a remove code for drawing ticks 2024-03-24 19:59:38 +01:00
442d3173c8 don't show context menu entries for ranges when range slider is disabled 2024-03-24 16:46:15 +01:00
12 changed files with 416 additions and 79 deletions

View File

@@ -1,5 +1,5 @@
pip==24.0
PySide6_Essentials==6.6.2
PySide6_Essentials==6.6.3
setuptools==69.2.0
watchdog==4.0.0
pyinstaller==6.5.0

View File

@@ -55,9 +55,9 @@ def _new_recursive(current_action_id: str, items: [MenuContribution]) -> [MenuCo
for item in items:
mc: MenuContribution = item
print("%s checking %s" % (current_action_id, mc.action_id))
# print("%s checking %s" % (current_action_id, mc.action_id))
if mc.after == current_action_id:
print("%s adding %s" % (current_action_id, mc.action_id))
#print("%s adding %s" % (current_action_id, mc.action_id))
result.append(mc)
result = result + _new_recursive(mc.action_id, items)
@@ -65,7 +65,7 @@ def _new_recursive(current_action_id: str, items: [MenuContribution]) -> [MenuCo
def _recursive_half_order_adder(result: [MenuContribution], items: [MenuContribution]):
print("%s -- %s" % ([mc.action_id for mc in result], [mc.action_id for mc in items]))
#print("%s -- %s" % ([mc.action_id for mc in result], [mc.action_id for mc in items]))
for item in items:
mc: MenuContribution = item
if mc.after:

View File

@@ -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:
@@ -225,7 +236,7 @@ class FilterWidget(QWidget):
(handle, self.tmp_filename) = tempfile.mkstemp()
os.close(handle)
self.filter_model = LogFileModel(self.tmp_filename, self.source_model.settings)
self.filter_model = LogFileModel(self.tmp_filename, self.source_model.settings, source_model.get_file())
self.hits_view = BigText(self.filter_model, show_range_slider=False)
self.layout.addWidget(filter_bar)
@@ -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)

View File

@@ -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)

View File

@@ -0,0 +1,249 @@
import enum
from PySide6.QtGui import QWheelEvent
from PySide6.QtWidgets import QWidget, QStylePainter, QStyle, QStyleOptionSlider, QSlider, QAbstractSlider
from PySide6.QtCore import Qt, QSize, QEvent, QRect, QPoint, Signal, QTimer, QElapsedTimer
class BigScrollBar(QWidget):
value_changed = Signal(str)
"""Signal emitted when the range slider value changes.
**Note**: The value is a string and must be parsed into an int.
QT's signal api only supports 32bit integers. Ints larger
than 2**32-1 will overflow. Probably because there is some C/C++
code involved. We work around this by converting the python int
into a string."""
class ScrollEvent(enum.IntEnum):
PageUp = 1
PageDown = 2
LinesUp = 3
LinesDown = 4
scroll_event = Signal(ScrollEvent)
pressedControl = QStyle.SubControl.SC_None
click_offset = 0
scale = 10000
minimum = 0
value = 0
maximum = 100
def __init__(self):
super(BigScrollBar, self).__init__()
self.repeat_action_timer = QTimer()
self.repeat_action_control = QStyle.SubControl.SC_None
self.repeat_action_timer.setSingleShot(True)
self.repeat_action_timer.timeout.connect(self.execute_control)
self.repeat_action_timer.setInterval(50)
def setMinimum(self, min: int):
self.minimum = min
if self.value < self.minimum:
self.set_value(self.minimum)
def setMaximum(self, max: int):
self.maximum = max
if self.value > self.maximum:
self.set_value(self.maximum)
def paintEvent(self, event):
p = QStylePainter(self)
o = self.style_options()
# print(f"style_options: sliderPosition: {o.sliderPosition}")
p.drawComplexControl(QStyle.ComplexControl.CC_ScrollBar, o)
def style_options(self) -> QStyleOptionSlider:
o = QStyleOptionSlider()
o.initFrom(self)
o.orientation = Qt.Orientation.Vertical
o.subControls = QStyle.SubControl.SC_All
if self.pressedControl != QStyle.SubControl.SC_None:
o.activeSubControls = self.pressedControl # QStyle.SubControl.SC_None
o.state = o.state | QStyle.StateFlag.State_Sunken
o.singleStep = 1
# scale values to 10000
t = self.scale / self.maximum
o.minimum = self.minimum * t
o.maximum = self.maximum * t
o.sliderPosition = int(self.value * t)
# print(f"t={t}")
# print(f"({self.minimun}, {self.value}, {self.maximum}) -> ({o.minimum},{o.sliderPosition},{o.maximum})")
return o
## QSize QScrollBar::sizeHint() const
# {
# ensurePolished();
# QStyleOptionSlider opt;
# initStyleOption(&opt);
# int scrollBarExtent = style()->pixelMetric(QStyle::PM_ScrollBarExtent, &opt, this);
# int scrollBarSliderMin = style()->pixelMetric(QStyle::PM_ScrollBarSliderMin, &opt, this);
# QSize size;
# if (opt.orientation == Qt::Horizontal)
# size = QSize(scrollBarExtent * 2 + scrollBarSliderMin, scrollBarExtent);
# else
# size = QSize(scrollBarExtent, scrollBarExtent * 2 + scrollBarSliderMin);
#
# return style()->sizeFromContents(QStyle::CT_ScrollBar, &opt, size, this);
# }
def sizeHint(self):
scroll_bar_extend = self.style().pixelMetric(QStyle.PixelMetric.PM_ScrollBarExtent)
scroll_bar_slider_min = self.style().pixelMetric(QStyle.PixelMetric.PM_ScrollBarSliderMin)
# print(f"scroll_bar_extend: {scroll_bar_extend}, scroll_bar_slider_min: {scroll_bar_slider_min}")
# for vertial
size = QSize(scroll_bar_extend, scroll_bar_extend * 2 + scroll_bar_slider_min)
o = self.style_options()
return self.style().sizeFromContents(QStyle.ContentsType.CT_ScrollBar, o, size, self)
def event(self, event: QEvent) -> bool:
# print(f"event type: {event.type()}")
if event.type() == QEvent.Type.MouseButtonPress:
pass
# print(f"mouse button pressed")
return super().event(event)
def mousePressEvent(self, event):
if self.repeat_action_timer.isActive():
self.stopRepeatAction()
if not (event.button() == Qt.MouseButton.LeftButton or event.button() == Qt.MouseButton.MiddleButton):
return
style_options = self.style_options()
self.pressedControl = self.style().hitTestComplexControl(QStyle.ComplexControl.CC_ScrollBar, style_options,
event.position().toPoint(), self)
# print(f"pressedControl {self.pressedControl}")
sr: QRect = self.style().subControlRect(QStyle.ComplexControl.CC_ScrollBar, style_options,
QStyle.SubControl.SC_ScrollBarSlider, self)
click: QPoint = event.position().toPoint()
# print(f"pressYValue {pressYValue}")
if self.pressedControl == QStyle.SubControl.SC_ScrollBarSlider:
self.click_offset = click.y() - sr.y()
if (self.pressedControl == QStyle.SubControl.SC_ScrollBarAddPage
or self.pressedControl == QStyle.SubControl.SC_ScrollBarSubPage) \
and event.button() == Qt.MouseButton.MiddleButton:
slider_length = sr.height()
self.set_value(self.pixelPosToRangeValue(event.position().toPoint().y()))
self.pressedControl = QStyle.SubControl.SC_ScrollBarSlider
self.click_offset = slider_length / 2
return
self.repeat_action_control = self.pressedControl
self.execute_control()
# self.repaint(self.style().subControlRect(QStyle.ComplexControl.CC_ScrollBar, style_options, self.pressedControl))
def execute_control(self):
# print(f"execute_control: {self.repeat_action_control}")
trigger_repeat_action = True
match self.repeat_action_control:
case QStyle.SubControl.SC_ScrollBarAddPage:
self.scroll_event.emit(self.ScrollEvent.PageDown)
case QStyle.SubControl.SC_ScrollBarSubPage:
self.scroll_event.emit(self.ScrollEvent.PageUp)
if self.value <= self.minimum:
trigger_repeat_action = False
case QStyle.SubControl.SC_ScrollBarAddLine:
self.scroll_event.emit(self.ScrollEvent.LinesDown)
case QStyle.SubControl.SC_ScrollBarSubLine:
self.scroll_event.emit(self.ScrollEvent.LinesUp)
if self.value <= self.minimum:
trigger_repeat_action = False
case QStyle.SubControl.SC_ScrollBarFirst:
self.set_value(self.minimum)
trigger_repeat_action = False
case QStyle.SubControl.SC_ScrollBarLast:
self.set_value(self.maximum)
trigger_repeat_action = False
case _:
trigger_repeat_action = False
if trigger_repeat_action:
# print(f"schedule repeat action: {self.repeat_action_control}")
self.repeat_action_timer.start()
def mouseReleaseEvent(self, event):
self.pressedControl = QStyle.SubControl.SC_None
self.stopRepeatAction()
def mouseMoveEvent(self, event):
if self.pressedControl == QStyle.SubControl.SC_None:
return
if self.pressedControl == QStyle.SubControl.SC_ScrollBarSlider:
click: QPoint = event.position().toPoint()
new_position = self.pixelPosToRangeValue(click.y() - self.click_offset)
m = self.style().pixelMetric(QStyle.PixelMetric.PM_MaximumDragDistance, self.style_options(), self)
if m >= 0:
r: QRect = self.rect()
r.adjust(-m, -m, m, m)
if not r.contains(event.position().toPoint()):
new_position = self.snap_back_position
# print(f"move to value: {new_position}")
self.set_value(new_position)
# stop repeat action when pointer leaves control
pr: QRect = self.style().subControlRect(QStyle.ComplexControl.CC_ScrollBar, self.style_options(),
self.pressedControl, self)
if not pr.contains(event.position().toPoint()):
self.stopRepeatAction()
def wheelEvent(self, event: QWheelEvent):
event.ignore()
# when using a touchpad we can have simultaneous horizontal and vertical movement
horizontal = abs(event.angleDelta().x()) > abs(event.angleDelta().y())
if horizontal:
return
scroll_event = self.ScrollEvent.LinesDown if event.angleDelta().y() < 0 else self.ScrollEvent.LinesUp
self.scroll_event.emit(scroll_event)
def pixelPosToRangeValue(self, pos: int):
opt: QStyleOptionSlider = self.style_options()
gr: QRect = self.style().subControlRect(QStyle.ComplexControl.CC_ScrollBar, opt,
QStyle.SubControl.SC_ScrollBarGroove, self)
sr: QRect = self.style().subControlRect(QStyle.ComplexControl.CC_ScrollBar, opt,
QStyle.SubControl.SC_ScrollBarSlider, self)
# only for vertical scrollbars
slider_length = sr.height();
slider_min = gr.y();
slider_max = gr.bottom() - slider_length + 1;
val = QStyle.sliderValueFromPosition(opt.minimum, opt.maximum, pos - slider_min,
slider_max - slider_min, opt.upsideDown)
t = self.scale / self.maximum
val = int(val / t)
# print(f"pixelPosToRangeValue({pos}) -> {val}")
return val
def stopRepeatAction(self):
self.repeat_action_control = QStyle.SubControl.SC_None
# print(f"stop repeat action: {self.repeat_action_control}")
self.repeat_action_timer.stop()
#self.update()
def set_value(self, value: int):
self.value = value
self.value_changed.emit(str(self.value))
self.repaint()

View File

@@ -11,6 +11,7 @@ from PySide6.QtGui import QMouseEvent
from PySide6.QtWidgets import *
from src.ui.ScaledScrollBar import ScaledScrollBar
from src.ui.bigtext.BigScrollBar import BigScrollBar
from src.ui.bigtext.highlight_regex import HighlightRegex
from src.ui.bigtext.highlight_selection import HighlightSelection
from src.ui.bigtext.highlighted_range import HighlightedRange
@@ -77,6 +78,7 @@ class BigText(QWidget):
def __init__(self, model: LogFileModel, show_range_slider=True):
super(BigText, self).__init__()
self.show_range_slider = show_range_slider
self.model = model
self.grid = QGridLayout()
@@ -85,12 +87,9 @@ class BigText(QWidget):
self.grid.setVerticalSpacing(0)
self.setLayout(self.grid)
self.v_scroll_bar = ScaledScrollBar()
self.v_scroll_bar = BigScrollBar()
self.range_limit = RangeSlider()
self.range_limit.value_changed.connect(self._range_limit_event)
self.big_text = InnerBigText(self, model, self.v_scroll_bar, self.range_limit)
self.big_text = InnerBigText(self, model, self.v_scroll_bar)
self.big_text.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding))
self.h_scroll_bar = QScrollBar(Qt.Orientation.Horizontal)
@@ -99,12 +98,12 @@ class BigText(QWidget):
self.h_scroll_bar.valueChanged.connect(self.big_text.h_scroll_event)
# self.v_scroll_bar.setPageStep(1)
self.v_scroll_bar.scaledValueChanged.connect(self.big_text.v_scroll_event)
self.v_scroll_bar.scrolled_to_end.connect(self.big_text.v_scroll_update_follow_tail)
self.v_scroll_bar.value_changed.connect(self.big_text.v_scroll_value_changed)
self.v_scroll_bar.scroll_event.connect(self.big_text.v_scroll_event)
if show_range_slider:
self.range_limit = RangeSlider()
self.range_limit.value_changed.connect(self._range_limit_event)
self.grid.addWidget(self.range_limit, 0, 0)
self.grid.addWidget(self.big_text, 0, 1)
self.grid.addWidget(self.h_scroll_bar, 1, 1)
@@ -121,6 +120,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
@@ -149,14 +152,12 @@ class InnerBigText(QWidget):
_range_start = 0
_range_end = -1
def __init__(self, parent: BigText, model: LogFileModel, v_scaled_scrollbar: ScaledScrollBar,
range_limit: RangeSlider):
def __init__(self, parent: BigText, model: LogFileModel, v_scaled_scrollbar: ScaledScrollBar):
super(InnerBigText, self).__init__()
self.char_height = None
self.char_width = None
self.model = model
self._v_scaled_scrollbar = v_scaled_scrollbar
self._range_limit = range_limit
self.parent = parent
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
@@ -169,8 +170,6 @@ class InnerBigText(QWidget):
self._last_double_click_time = 0
self._last_double_click_line_number = -1
self._follow_tail = False
self.highlight_selected_text = HighlightRegex(
"",
is_regex=False,
@@ -219,7 +218,6 @@ class InnerBigText(QWidget):
elif e.modifiers() == Qt.KeyboardModifier.ControlModifier and e.key() == 65: # ctrl + a
self._select_all()
def wheelEvent(self, event: QWheelEvent):
direction = 1 if event.angleDelta().y() < 0 else -1
if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
@@ -262,6 +260,7 @@ class InnerBigText(QWidget):
manage_highlighting.setShortcut("CTRL+H")
menu.addAction(manage_highlighting)
if self.parent.show_range_slider:
menu.addSeparator()
set_range_start = QAction(
@@ -311,12 +310,12 @@ class InnerBigText(QWidget):
def scroll_by_lines(self, scroll_lines: int):
self.scroll_lines = scroll_lines
self.update()
self.parent.v_scroll_bar.setValue(self._byte_offset)
self.parent.v_scroll_bar.set_value(self._byte_offset)
def scroll_to_byte(self, byte_offset: int):
self._byte_offset = min(byte_offset, self.model.byte_count())
self.update()
self.parent.v_scroll_bar.setValue(self._byte_offset)
self.parent.v_scroll_bar.set_value(self._byte_offset)
# noinspection PyTypeChecker
def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
@@ -401,13 +400,21 @@ class InnerBigText(QWidget):
self.update()
@Slot()
def v_scroll_event(self, byte_offset: str):
def v_scroll_value_changed(self, byte_offset: str):
self._byte_offset = int(byte_offset)
self.update()
@Slot()
def v_scroll_update_follow_tail(self, scrolled_to_end: bool):
self._follow_tail = scrolled_to_end
def v_scroll_event(self, event: BigScrollBar.ScrollEvent):
match event:
case BigScrollBar.ScrollEvent.LinesUp:
self.scroll_by_lines(-3)
case BigScrollBar.ScrollEvent.LinesDown:
self.scroll_by_lines(3)
case BigScrollBar.ScrollEvent.PageUp:
self.scroll_by_lines(-(int(self.lines_shown()) - 1))
case BigScrollBar.ScrollEvent.PageDown:
self.scroll_by_lines(int(self.lines_shown()) - 1)
def update_longest_line(self, length: int):
width_in_chars = self.width() / self.char_width
@@ -491,7 +498,7 @@ class InnerBigText(QWidget):
(selected_file, _filter) = dialog.getSaveFileName(
parent=self,
caption=_("Save File"),
dir=os.path.dirname(self.model.get_file())
dir=os.path.dirname(self.model.get_original_file())
)
if selected_file:
self.model.write_range(start, end, selected_file)
@@ -533,13 +540,10 @@ class InnerBigText(QWidget):
PluginRegistry.execute("update_status_bar", "")
def _file_changed(self):
if self._follow_tail:
self.scroll_to_byte(self.model.byte_count())
self.update()
def paintEvent(self, event: QPaintEvent) -> None:
start_ns = time.process_time_ns()
# print(f"paint {self.model.get_file()} at {self._byte_offset} with follow_tail={self._follow_tail}")
painter = QPainter(self)
# font = "Courier New" if sys.platform == 'win32' or sys.platform == 'cygwin' else "Monospace"
painter.setFont(QFont("Courier New", self.model.settings.getint_session('general', "font_size")))
@@ -559,6 +563,7 @@ class InnerBigText(QWidget):
byte_count = self.model.byte_count()
vmax = byte_count - 1 if self._range_end < 0 else min(self._range_end, self.model.byte_count() - 1)
self.parent.v_scroll_bar.setMaximum(vmax)
if self.parent.show_range_slider:
self.parent.range_limit.set_maximum(byte_count)
for line in self.lines:

View File

@@ -1,3 +1,5 @@
import fnmatch
import glob
from typing import Optional
from src.ui.bigtext.highlight import Highlight
@@ -12,7 +14,7 @@ import re
class HighlightRegex(Highlight):
def __init__(self, query: str, ignore_case: bool, is_regex: bool, hit_background_color: str = "None",
line_background_color: str = "None", active: bool = True):
line_background_color: str = "None", active: bool = True, activated_for_file_type: str = "*"):
self.active = active
self.query = query
self.ignore_case = ignore_case
@@ -20,6 +22,7 @@ class HighlightRegex(Highlight):
self.regex = self._get_regex()
self.hit_background_color = hit_background_color
self.line_background_color = line_background_color
self.activated_for_file_type = activated_for_file_type
self._brush_hit = self.brush(self.hit_background_color)
self._brush_line = self.brush(self.line_background_color)
@@ -99,3 +102,15 @@ class HighlightRegex(Highlight):
alpha = int(color[6:8], 16)
return QBrush(QColor(red, green, blue, alpha))
return QBrush()
def set_activated_for_file_type(self, activated_for_file_type: str):
self.activated_for_file_type = activated_for_file_type
def file_type_matches(self, file_name: str) -> bool:
if self.activated_for_file_type is None or len(self.activated_for_file_type) == 0:
return True
glob_patterns: [str] = self.activated_for_file_type.split(",") # support multiple globs like: "*.txt, *.csv"
for glob_pattern in glob_patterns:
if fnmatch.fnmatch(file_name, glob_pattern.strip()):
return True
return False

View File

@@ -25,6 +25,7 @@ class Highlighting:
is_regex = session.getboolean(section, "is-regex", fallback=False)
line_background_color = session.get(section, "line.background.color", fallback="None")
hit_background_color = session.get(section, "hit.background.color", fallback="None")
activated_for_file_type = session.get(section, "activated-for-file-type", fallback="*")
try:
highlight = HighlightRegex(
@@ -33,7 +34,8 @@ class Highlighting:
is_regex=is_regex,
hit_background_color=hit_background_color,
line_background_color=line_background_color,
active=active
active=active,
activated_for_file_type=activated_for_file_type
)
result.append(highlight)
except:
@@ -57,6 +59,7 @@ class Highlighting:
settings.session.set(section, "is-regex", str(highlighter.is_regex))
settings.session.set(section, "line.background.color", highlighter.line_background_color)
settings.session.set(section, "hit.background.color", highlighter.hit_background_color)
settings.session.set(section, "activated-for-file-type", highlighter.activated_for_file_type)
@staticmethod
def remove_highlighting_sections(settings: Settings):

View File

@@ -21,21 +21,31 @@ class LogFileModel:
range_start = 0
range_end = -1
def __init__(self, file: str, settings: Settings):
def __init__(self, file: str, settings: Settings, original_file: str = False):
"""
:param file:
:param settings:
:param original_file: used in the filter widget to denote the original file, the one being filtered, because 'file' points to the tmp file
"""
self.settings = settings
self._file = os.path.realpath(file)
self._original_file = os.path.realpath(original_file) if original_file else self._file
self._file_name = os.path.basename(self._original_file)
def highlighters(self):
all_highlighters = Highlighting.read_config(self.settings)
active_highlighters = []
for h in all_highlighters:
if h.is_active():
if h.is_active() and h.file_type_matches(self._file_name):
active_highlighters.append(h)
return active_highlighters
def get_file(self):
return self._file
def get_original_file(self):
return self._original_file
def __str__(self):
return self._file

View File

@@ -118,7 +118,7 @@ class NewHighlightingDialog(QDialog):
def _new_highlighter(self):
highlight_regex = HighlightRegex("", ignore_case=True, is_regex=True, hit_background_color="ccb400",
line_background_color="fff080")
line_background_color="None")
self._add_highlight_regex_to_list(highlight_regex, select=True)
@@ -139,7 +139,7 @@ class HighlightListItemWidget(QWidget):
self.active = QCheckBox("")
self.active.setChecked(highlight_regex.is_active())
self.active.stateChanged.connect(self._change_active_state)
self.layout.addWidget(self.active, row, 0, 3, 1, alignment=QtCore.Qt.AlignmentFlag.AlignVCenter)
self.layout.addWidget(self.active, row, 0, 4, 1, alignment=QtCore.Qt.AlignmentFlag.AlignVCenter)
query = QLineEdit(self)
query.setText(highlight_regex.query)
@@ -181,6 +181,18 @@ class HighlightListItemWidget(QWidget):
is_regex.setEnabled(highlight_regex.is_active())
self.layout.addWidget(is_regex, row, 3)
row = row + 1
activated_for_file_type_label = QLabel(_("File Type:"), self)
activated_for_file_type_label.setEnabled(highlight_regex.is_active())
self.layout.addWidget(activated_for_file_type_label, row, 1)
activated_for_file_type = QLineEdit(self)
activated_for_file_type.setEnabled(highlight_regex.is_active())
activated_for_file_type.setText(highlight_regex.activated_for_file_type)
activated_for_file_type.textChanged[str].connect(
lambda: highlight_regex.set_activated_for_file_type(activated_for_file_type.text()))
self.layout.addWidget(activated_for_file_type, row, 2)
def _change_active_state(self):
active = self.active.isChecked()
self.highlight_regex.set_active(active)

View File

@@ -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
@@ -44,8 +44,6 @@ class RangeSlider(QWidget):
super(RangeSlider, self).__init__()
self.setFixedWidth(self._width)
self.draw_ticks = False
self.min_value = 0
self.max_value = 100
@@ -54,6 +52,8 @@ 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:
@@ -66,14 +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)
if self.draw_ticks:
self._draw_ticks(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:
@@ -90,19 +92,6 @@ class RangeSlider(QWidget):
self._value_to_pixel(self.upper_value.value - self.lower_value.value) - 2 * self._handle_width)
painter.drawRoundedRect(rect, 3.0, 3.0)
def _draw_ticks(self, painter: QPainter) -> None:
painter.setPen(to_qcolor("333333"))
min_tick_distance = 25
full_height = self.height() - 2 * self._handle_width
ticks = math.floor(full_height / min_tick_distance)
actual_tick_distance = full_height / ticks
print(f"ticks {ticks}")
y = actual_tick_distance + self._handle_width
while y < full_height:
painter.drawLine(8, y, 12, y)
y = y + actual_tick_distance
def _draw_handle(self, painter: QPainter, handle: RangeSliderHandle, direction=1) -> None:
y_pixel = self._value_to_pixel(handle.value)
@@ -127,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))
@@ -144,9 +152,11 @@ class RangeSlider(QWidget):
if self._is_on_handle(self.lower_value, pos.y(), direction=1):
self.selected_handle = self.lower_value
self.selection_drag_range = (self.min_value, self.upper_value.value)
self.drag_y_offset_in_handle = self.selected_handle.value - self._pixel_to_value(pos.y())
if self._is_on_handle(self.upper_value, pos.y(), direction=-1):
self.selected_handle = self.upper_value
self.selection_drag_range = (self.lower_value.value, self.max_value)
self.drag_y_offset_in_handle = self.selected_handle.value - self._pixel_to_value(pos.y())
def mouseReleaseEvent(self, event: PySide6.QtGui.QMouseEvent) -> None:
self.selected_handle = None
@@ -154,7 +164,7 @@ class RangeSlider(QWidget):
def mouseMoveEvent(self, e: PySide6.QtGui.QMouseEvent) -> None:
if self.selected_handle != None:
pos: QPoint = e.pos()
value = self._pixel_to_value(pos.y())
value = self._pixel_to_value(pos.y()) + self.drag_y_offset_in_handle
if self.selection_drag_range[0] <= value <= self.selection_drag_range[1]:
self.selected_handle.value = value
# print("%s, %s" %(self.lower_value.value, self.upper_value.value))

View File

@@ -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)