Compare commits
6 Commits
6538e85f37
...
3e793596c2
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e793596c2 | |||
| 7a574f7ed4 | |||
| 7d20bae74d | |||
| 9b9399f120 | |||
| 3d6cf84cd7 | |||
| 2b65e61e43 |
249
src/ui/bigtext/BigScrollBar.py
Normal file
249
src/ui/bigtext/BigScrollBar.py
Normal 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()
|
||||||
@@ -11,6 +11,7 @@ from PySide6.QtGui import QMouseEvent
|
|||||||
from PySide6.QtWidgets import *
|
from PySide6.QtWidgets import *
|
||||||
|
|
||||||
from src.ui.ScaledScrollBar import ScaledScrollBar
|
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_regex import HighlightRegex
|
||||||
from src.ui.bigtext.highlight_selection import HighlightSelection
|
from src.ui.bigtext.highlight_selection import HighlightSelection
|
||||||
from src.ui.bigtext.highlighted_range import HighlightedRange
|
from src.ui.bigtext.highlighted_range import HighlightedRange
|
||||||
@@ -86,7 +87,7 @@ class BigText(QWidget):
|
|||||||
self.grid.setVerticalSpacing(0)
|
self.grid.setVerticalSpacing(0)
|
||||||
self.setLayout(self.grid)
|
self.setLayout(self.grid)
|
||||||
|
|
||||||
self.v_scroll_bar = ScaledScrollBar()
|
self.v_scroll_bar = BigScrollBar()
|
||||||
|
|
||||||
self.big_text = InnerBigText(self, model, self.v_scroll_bar)
|
self.big_text = InnerBigText(self, model, self.v_scroll_bar)
|
||||||
self.big_text.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding))
|
self.big_text.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding))
|
||||||
@@ -97,8 +98,8 @@ class BigText(QWidget):
|
|||||||
self.h_scroll_bar.valueChanged.connect(self.big_text.h_scroll_event)
|
self.h_scroll_bar.valueChanged.connect(self.big_text.h_scroll_event)
|
||||||
|
|
||||||
# self.v_scroll_bar.setPageStep(1)
|
# self.v_scroll_bar.setPageStep(1)
|
||||||
self.v_scroll_bar.scaledValueChanged.connect(self.big_text.v_scroll_event)
|
self.v_scroll_bar.value_changed.connect(self.big_text.v_scroll_value_changed)
|
||||||
self.v_scroll_bar.scrolled_to_end.connect(self.big_text.v_scroll_update_follow_tail)
|
self.v_scroll_bar.scroll_event.connect(self.big_text.v_scroll_event)
|
||||||
|
|
||||||
if show_range_slider:
|
if show_range_slider:
|
||||||
self.range_limit = RangeSlider()
|
self.range_limit = RangeSlider()
|
||||||
@@ -169,8 +170,6 @@ class InnerBigText(QWidget):
|
|||||||
self._last_double_click_time = 0
|
self._last_double_click_time = 0
|
||||||
self._last_double_click_line_number = -1
|
self._last_double_click_line_number = -1
|
||||||
|
|
||||||
self._follow_tail = False
|
|
||||||
|
|
||||||
self.highlight_selected_text = HighlightRegex(
|
self.highlight_selected_text = HighlightRegex(
|
||||||
"",
|
"",
|
||||||
is_regex=False,
|
is_regex=False,
|
||||||
@@ -311,12 +310,12 @@ class InnerBigText(QWidget):
|
|||||||
def scroll_by_lines(self, scroll_lines: int):
|
def scroll_by_lines(self, scroll_lines: int):
|
||||||
self.scroll_lines = scroll_lines
|
self.scroll_lines = scroll_lines
|
||||||
self.update()
|
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):
|
def scroll_to_byte(self, byte_offset: int):
|
||||||
self._byte_offset = min(byte_offset, self.model.byte_count())
|
self._byte_offset = min(byte_offset, self.model.byte_count())
|
||||||
self.update()
|
self.update()
|
||||||
self.parent.v_scroll_bar.setValue(self._byte_offset)
|
self.parent.v_scroll_bar.set_value(self._byte_offset)
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
|
def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
|
||||||
@@ -401,13 +400,21 @@ class InnerBigText(QWidget):
|
|||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
@Slot()
|
@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._byte_offset = int(byte_offset)
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def v_scroll_update_follow_tail(self, scrolled_to_end: bool):
|
def v_scroll_event(self, event: BigScrollBar.ScrollEvent):
|
||||||
self._follow_tail = scrolled_to_end
|
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):
|
def update_longest_line(self, length: int):
|
||||||
width_in_chars = self.width() / self.char_width
|
width_in_chars = self.width() / self.char_width
|
||||||
@@ -533,13 +540,10 @@ class InnerBigText(QWidget):
|
|||||||
PluginRegistry.execute("update_status_bar", "")
|
PluginRegistry.execute("update_status_bar", "")
|
||||||
|
|
||||||
def _file_changed(self):
|
def _file_changed(self):
|
||||||
if self._follow_tail:
|
|
||||||
self.scroll_to_byte(self.model.byte_count())
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def paintEvent(self, event: QPaintEvent) -> None:
|
def paintEvent(self, event: QPaintEvent) -> None:
|
||||||
start_ns = time.process_time_ns()
|
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)
|
painter = QPainter(self)
|
||||||
# font = "Courier New" if sys.platform == 'win32' or sys.platform == 'cygwin' else "Monospace"
|
# 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")))
|
painter.setFont(QFont("Courier New", self.model.settings.getint_session('general', "font_size")))
|
||||||
|
|||||||
Reference in New Issue
Block a user