"""
Drop-in replacement for QSlider with additional features.
"""
from PySide2 import QtCore, QtWidgets, QtGui
from PySide2.QtGui import QPen, QBrush, QColor, QKeyEvent, QPolygonF, QPainterPath
from sleap.gui.color import ColorManager
import attr
import itertools
import numpy as np
from typing import Dict, Iterable, List, Optional, Tuple, Union
[docs]@attr.s(auto_attribs=True, cmp=False)
class SliderMark:
"""
Class to hold data for an individual mark on the slider.
Attributes:
type: Type of the mark, options are:
* "simple" (single value)
* "filled" (single value)
* "open" (single value)
* "predicted" (single value)
* "track" (range of values)
* "tick" (single value)
* "tick_column" (single value)
val: Beginning of mark range
end_val: End of mark range (for "track" marks)
row: The row that the mark goes in; used for tracks.
color: Color of mark, can be string or (r, g, b) tuple.
filled: Whether the mark is shown filled (solid color).
"""
type: str
val: float
end_val: float = None
row: int = None
track: "Track" = None
_color: Union[tuple, str] = "black"
@property
def color(self):
"""Returns color of mark."""
colors = dict(
simple="black",
filled="blue",
open="blue",
predicted="yellow",
tick="lightGray",
tick_column="gray",
)
if self.type in colors:
return colors[self.type]
else:
return self._color
@color.setter
def color(self, val):
"""Sets color of mark."""
self._color = val
@property
def QColor(self):
"""Returns color of mark as `QColor`."""
c = self.color
if type(c) == str:
return QColor(c)
else:
return QColor(*c)
@property
def filled(self):
"""Returns whether mark is filled or open."""
if self.type == "open":
return False
else:
return True
@property
def top_pad(self):
if self.type == "tick_column":
return 40
if self.type == "tick":
return 0
return 2
@property
def bottom_pad(self):
if self.type == "tick_column":
return 200
if self.type == "tick":
return 0
return 2
@property
def visual_width(self):
if self.type in ("open", "filled", "tick"):
return 2
if self.type in ("tick_column"):
return 1
return 0
def get_height(self, container_height):
if self.type == "track":
return 2
height = container_height
# if self.padded:
height -= self.top_pad + self.bottom_pad
return height
[docs]class VideoSlider(QtWidgets.QGraphicsView):
"""Drop-in replacement for QSlider with additional features.
Args:
orientation: ignored (here for compatibility with QSlider)
min: initial minimum value
max: initial maximum value
val: initial value
marks: initial set of values to mark on slider
this can be either
* list of values to mark
* list of (track, value)-tuples to mark
color_manager: A :class:`ColorManager` which determines the
color to use for "track"-type marks
Signals:
mousePressed: triggered on Qt event
mouseMoved: triggered on Qt event
mouseReleased: triggered on Qt event
keyPress: triggered on Qt event
keyReleased: triggered on Qt event
valueChanged: triggered when value of slider changes
selectionChanged: triggered when slider range selection changes
heightUpdated: triggered when the height of slider changes
"""
mousePressed = QtCore.Signal(float, float)
mouseMoved = QtCore.Signal(float, float)
mouseReleased = QtCore.Signal(float, float)
keyPress = QtCore.Signal(QKeyEvent)
keyRelease = QtCore.Signal(QKeyEvent)
valueChanged = QtCore.Signal(int)
selectionChanged = QtCore.Signal(int, int)
heightUpdated = QtCore.Signal()
def __init__(
self,
orientation=-1, # for compatibility with QSlider
min=0,
max=1,
val=0,
marks=None,
color_manager: Optional[ColorManager] = None,
*args,
**kwargs,
):
super(VideoSlider, self).__init__(*args, **kwargs)
self.scene = QtWidgets.QGraphicsScene()
self.setScene(self.scene)
self.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self._color_manager = color_manager
self.tick_index_offset = 1
self.zoom_factor = 1
self._track_rows = 0
self._track_height = 5
self._max_tracks_stacked = 120
self._track_stack_skip_count = 10
self._header_label_height = 20
self._header_graph_height = 30
self._header_height = self._header_label_height # room for frame labels
self._min_height = 19 + self._header_height
self._base_font = QtGui.QFont()
self._base_font.setPixelSize(10)
self._tick_marks = []
# Add border rect
outline_rect = QtCore.QRectF(0, 0, 200, self._min_height - 3)
self.setBoxRect(outline_rect)
# self.outlineBox = self.scene.addRect(outline_rect)
# self.outlineBox.setPen(QPen(QColor("black", alpha=0)))
# Add drag handle rect
self._handle_width = 6
handle_rect = QtCore.QRect(
0, self._handle_top, self._handle_width, self._handle_height
)
self.setMinimumHeight(self._min_height)
self.setMaximumHeight(self._min_height)
self.handle = self.scene.addRect(handle_rect)
self.handle.setPen(QPen(QColor(80, 80, 80)))
self.handle.setBrush(QColor(128, 128, 128, 128))
# Add (hidden) rect to highlight selection
self.select_box = self.scene.addRect(
QtCore.QRect(0, 1, 0, outline_rect.height() - 2)
)
self.select_box.setPen(QPen(QColor(80, 80, 255)))
self.select_box.setBrush(QColor(80, 80, 255, 128))
self.select_box.hide()
self.zoom_box = self.scene.addRect(
QtCore.QRect(0, 1, 0, outline_rect.height() - 2)
)
self.zoom_box.setPen(QPen(QColor(80, 80, 80, 64)))
self.zoom_box.setBrush(QColor(80, 80, 80, 64))
self.zoom_box.hide()
self.scene.setBackgroundBrush(QBrush(QColor(200, 200, 200)))
self.clearSelection()
self.setEnabled(True)
self.setMinimum(min)
self.setMaximum(max)
self.setValue(val)
self.setMarks(marks)
pen = QPen(QColor(80, 80, 255), 0.5)
pen.setCosmetic(True)
self.poly = self.scene.addPath(QPainterPath(), pen, self.select_box.brush())
self.headerSeries = dict()
self.drawHeader()
def _pointsToPath(self, points: List[QtCore.QPointF]) -> QPainterPath:
"""Converts list of `QtCore.QPointF` objects to a `QPainterPath`."""
path = QPainterPath()
path.addPolygon(QPolygonF(points))
return path
[docs] def setTracksFromLabels(self, labels: "Labels", video: "Video"):
"""Set slider marks using track information from `Labels` object.
Note that this is the only method coupled to a SLEAP object.
Args:
labels: the dataset with tracks and labeled frames
video: the video for which to show marks
Returns:
None
"""
if self._color_manager is None:
self._color_manager = ColorManager(labels=labels)
lfs = labels.find(video)
slider_marks = []
track_row = 0
# Add marks with track
track_occupancy = labels.get_track_occupancy(video)
for track in labels.tracks:
if track in track_occupancy and not track_occupancy[track].is_empty:
if track_row > 0 and self.isNewColTrack(track_row):
slider_marks.append(
SliderMark("tick_column", val=track_occupancy[track].start)
)
for occupancy_range in track_occupancy[track].list:
slider_marks.append(
SliderMark(
"track",
val=occupancy_range[0],
end_val=occupancy_range[1],
row=track_row,
color=self._color_manager.get_track_color(track),
)
)
track_row += 1
# Add marks without track
if None in track_occupancy:
for occupancy_range in track_occupancy[None].list:
for val in range(*occupancy_range):
slider_marks.append(SliderMark("simple", val=val))
# list of frame_idx for simple markers for labeled frames
labeled_marks = [lf.frame_idx for lf in lfs]
user_labeled = [lf.frame_idx for lf in lfs if len(lf.user_instances)]
for frame_idx in labels.get_video_suggestions(video):
if frame_idx in user_labeled:
mark_type = "filled"
elif frame_idx in labeled_marks:
mark_type = "predicted"
else:
mark_type = "open"
slider_marks.append(SliderMark(mark_type, val=frame_idx))
self.setTracks(track_row) # total number of tracks to show
self.setMarks(slider_marks)
[docs] def setTracks(self, track_rows):
"""Set the number of tracks to show in slider.
Args:
track_rows: the number of tracks to show
"""
self._track_rows = track_rows
self.updateHeight()
def getMinMaxHeights(self):
tracks = self._track_rows
if tracks == 0:
min_height = self._min_height
max_height = self._min_height
else:
# Start with padding height
extra_height = 8 + self._header_height
min_height = extra_height
max_height = extra_height
# Add height for tracks
min_height += self._track_height * min(tracks, 20)
max_height += self._track_height * min(tracks, self._max_tracks_stacked)
# Make sure min/max height is at least 19, even if few tracks
min_height = max(self._min_height, min_height)
max_height = max(self._min_height, max_height)
return min_height, max_height
[docs] def updateHeight(self):
"""Update the height of the slider."""
min_height, max_height = self.getMinMaxHeights()
# TODO: find the current height of the scrollbar
# self.horizontalScrollBar().height() gives the wrong value
scrollbar_height = 18
self.setMaximumHeight(max_height + scrollbar_height)
self.setMinimumHeight(min_height + scrollbar_height)
# Redraw all marks with new height and y position
marks = self.getMarks()
self.setMarks(marks)
self.resizeEvent()
self.heightUpdated.emit()
def _toPos(self, val: float, center=False) -> float:
"""
Converts slider value to x position on slider.
Args:
val: The slider value.
center: Whether to offset by half the width of drag handle,
so that plotted location will light up with center of handle.
Returns:
x position.
"""
x = val
x -= self._val_min
x /= max(1, self._val_max - self._val_min)
x *= self._sliderWidth()
if center:
x += self.handle.rect().width() / 2.0
return x
def _toVal(self, x: float, center=False) -> float:
"""Converts x position to slider value."""
val = x
val /= self._sliderWidth()
val *= max(1, self._val_max - self._val_min)
val += self._val_min
val = round(val)
return val
def _sliderWidth(self) -> float:
"""Returns visual width of slider."""
return self.getBoxRect().width() - self.handle.rect().width()
@property
def slider_visible_value_range(self) -> float:
"""Value range that's visible given current size and zoom."""
return self._toVal(self.width() - 1)
[docs] def value(self) -> float:
"""Returns value of slider."""
return self._val_main
[docs] def setValue(self, val: float) -> float:
"""Sets value of slider."""
self._val_main = val
x = self._toPos(val)
self.handle.setPos(x, 0)
self.ensureVisible(x, 0, self._handle_width, 0, 3, 0)
[docs] def setMinimum(self, min: float) -> float:
"""Sets minimum value for slider."""
self._val_min = min
[docs] def setMaximum(self, max: float) -> float:
"""Sets maximum value for slider."""
self._val_max = max
@property
def value_range(self) -> float:
return self._val_max - self._val_min
[docs] def setEnabled(self, val: float) -> float:
"""Set whether the slider is enabled."""
self._enabled = val
[docs] def enabled(self):
"""Returns whether slider is enabled."""
return self._enabled
[docs] def clearSelection(self):
"""Clears selection endpoints."""
self._selection = []
self.select_box.hide()
[docs] def startSelection(self, val):
"""Adds initial selection endpoint.
Called when user starts dragging to select range in slider.
Args:
val: value of endpoint
"""
self._selection.append(val)
[docs] def endSelection(self, val, update: bool = False):
"""Add final selection endpoint.
Called during or after the user is dragging to select range.
Args:
val: value of endpoint
update:
"""
# If we want to update endpoint and there's already one, remove it
if update and len(self._selection) % 2 == 0:
self._selection.pop()
# Add the selection endpoint
self._selection.append(val)
a, b = self._selection[-2:]
if a == b:
self.clearSelection()
else:
self.drawSelection(a, b)
# Emit signal (even if user selected same region as before)
self.selectionChanged.emit(*self.getSelection())
[docs] def setSelection(self, start_val, end_val):
"""Selects clip from start_val to end_val."""
self.startSelection(start_val)
self.endSelection(end_val, update=True)
[docs] def hasSelection(self) -> bool:
"""Returns True if a clip is selected, False otherwise."""
a, b = self.getSelection()
return a < b
[docs] def getSelection(self):
"""Returns start and end value of current selection endpoints."""
a, b = 0, 0
if len(self._selection) % 2 == 0 and len(self._selection) > 0:
a, b = self._selection[-2:]
start = min(a, b)
end = max(a, b)
return start, end
def drawSelection(self, a: float, b: float):
self.updateSelectionBoxPositions(self.select_box, a, b)
def drawZoomBox(self, a: float, b: float):
self.updateSelectionBoxPositions(self.zoom_box, a, b)
[docs] def updateSelectionBoxPositions(self, box_object, a: float, b: float):
"""Update box item on slider.
Args:
box_object: The box to update
a: one endpoint value
b: other endpoint value
Returns:
None.
"""
start = min(a, b)
end = max(a, b)
start_pos = self._toPos(start, center=True)
end_pos = self._toPos(end, center=True)
box_rect = QtCore.QRect(
start_pos,
self._header_height,
end_pos - start_pos,
self.getBoxRect().height(),
)
box_object.setRect(box_rect)
box_object.show()
def updateSelectionBoxesOnResize(self):
for box_object in (self.select_box, self.zoom_box):
rect = box_object.rect()
rect.setHeight(self._handle_height)
box_object.setRect(rect)
if self.select_box.isVisible():
self.drawSelection(*self.getSelection())
[docs] def moveSelectionAnchor(self, x: float, y: float):
"""
Moves selection anchor in response to mouse position.
Args:
x: x position of mouse
y: y position of mouse
Returns:
None.
"""
x = max(x, 0)
x = min(x, self.getBoxRect().width())
anchor_val = self._toVal(x, center=True)
if len(self._selection) % 2 == 0:
self.startSelection(anchor_val)
self.drawSelection(anchor_val, self._selection[-1])
[docs] def releaseSelectionAnchor(self, x, y):
"""
Finishes selection in response to mouse release.
Args:
x: x position of mouse
y: y position of mouse
Returns:
None.
"""
x = max(x, 0)
x = min(x, self.getBoxRect().width())
anchor_val = self._toVal(x)
self.endSelection(anchor_val)
def moveZoomDrag(self, x: float, y: float):
if getattr(self, "_zoom_start_val", None) is None:
self._zoom_start_val = self._toVal(x, center=True)
current_val = self._toVal(x, center=True)
self.drawZoomBox(current_val, self._zoom_start_val)
def releaseZoomDrag(self, x, y):
self.zoom_box.hide()
val_a = self._zoom_start_val
val_b = self._toVal(x, center=True)
val_start = min(val_a, val_b)
val_end = max(val_a, val_b)
# pad the zoom
val_range = val_end - val_start
val_start -= val_range * 0.05
val_end += val_range * 0.05
self.setZoomRange(val_start, val_end)
self._zoom_start_val = None
def setZoomRange(self, start_val: float, end_val: float):
zoom_val_range = end_val - start_val
if zoom_val_range > 0:
self.zoom_factor = self.value_range / zoom_val_range
else:
self.zoom_factor = 1
self.resizeEvent()
center_val = start_val + zoom_val_range / 2
center_pos = self._toPos(center_val)
self.centerOn(center_pos, 0)
[docs] def clearMarks(self):
"""Clears all marked values for slider."""
if hasattr(self, "_mark_items"):
for item in self._mark_items.values():
self.scene.removeItem(item)
if hasattr(self, "_mark_labels"):
for item in self._mark_labels.values():
self.scene.removeItem(item)
self._marks = set() # holds mark position
self._mark_items = dict() # holds visual Qt object for plotting mark
self._mark_labels = dict()
[docs] def setMarks(self, marks: Iterable[Union[SliderMark, int]]):
"""Sets all marked values for the slider.
Args:
marks: iterable with all values to mark
Returns:
None.
"""
self.clearMarks()
# Add tick marks first so they're behind other marks
self._add_tick_marks()
if marks is not None:
for mark in marks:
if not isinstance(mark, SliderMark):
mark = SliderMark("simple", mark)
self.addMark(mark, update=False)
self.updatePos()
[docs] def setTickMarks(self):
"""Resets which tick marks to show."""
self._clear_tick_marks()
self._add_tick_marks()
def _clear_tick_marks(self):
if not hasattr(self, "_tick_marks"):
return
for mark in self._tick_marks:
self.removeMark(mark)
def _add_tick_marks(self):
val_range = self.slider_visible_value_range
if val_range < 20:
val_order = 1
else:
val_order = 10
while val_range // val_order > 24:
val_order *= 10
self._tick_marks = []
for tick_pos in range(
self._val_min + val_order - 1, self._val_max + 1, val_order
):
self._tick_marks.append(SliderMark("tick", tick_pos))
for tick_mark in self._tick_marks:
self.addMark(tick_mark, update=False)
[docs] def removeMark(self, mark: SliderMark):
"""Removes an individual mark."""
if mark in self._mark_labels:
self.scene.removeItem(self._mark_labels[mark])
del self._mark_labels[mark]
if mark in self._mark_items:
self.scene.removeItem(self._mark_items[mark])
del self._mark_items[mark]
if mark in self._marks:
self._marks.remove(mark)
[docs] def getMarks(self, type: str = ""):
"""Returns list of marks."""
if type:
return [mark for mark in self._marks if mark.type == type]
return self._marks
[docs] def addMark(self, new_mark: SliderMark, update: bool = True):
"""Adds a marked value to the slider.
Args:
new_mark: value to mark
update: Whether to redraw slider with new mark.
Returns:
None.
"""
# check if mark is within slider range
if new_mark.val > self._val_max:
return
if new_mark.val < self._val_min:
return
self._marks.add(new_mark)
v_top_pad = self._header_height + 1
v_bottom_pad = 1
v_top_pad += new_mark.top_pad
v_bottom_pad += new_mark.bottom_pad
width = new_mark.visual_width
v_offset = v_top_pad
if new_mark.type == "track":
v_offset += self.getTrackVerticalPos(*self.getTrackColRow(new_mark.row))
height = new_mark.get_height(
container_height=self.getBoxRect().height() - self._header_height
)
color = new_mark.QColor
pen = QPen(color, 0.5)
pen.setCosmetic(True)
brush = QBrush(color) if new_mark.filled else QBrush()
line = self.scene.addRect(-width // 2, v_offset, width, height, pen, brush)
self._mark_items[new_mark] = line
if new_mark.type == "tick":
# Show tick mark behind other slider marks
self._mark_items[new_mark].setZValue(0)
# Add a text label to show in header area
mark_label_text = (
f"{new_mark.val + self.tick_index_offset:g}" # sci notation if large
)
self._mark_labels[new_mark] = self.scene.addSimpleText(
mark_label_text, self._base_font
)
else:
# Show in front of tick marks
self._mark_items[new_mark].setZValue(1)
if update:
self.updatePos()
def getTrackColRow(self, raw_row: int) -> Tuple[int, int]:
if raw_row < self._max_tracks_stacked:
return 0, raw_row
else:
rows_after_first_col = raw_row - self._max_tracks_stacked
rows_per_later_cols = (
self._max_tracks_stacked - self._track_stack_skip_count
)
rows_down = rows_after_first_col % rows_per_later_cols
col = (rows_after_first_col // rows_per_later_cols) + 1
return col, rows_down
def getTrackVerticalPos(self, col: int, row: int) -> int:
if col == 0:
return row * self._track_height
else:
return (self._track_height * self._track_stack_skip_count) + (
self._track_height * row
)
def isNewColTrack(self, row: int) -> bool:
_, row_down = self.getTrackColRow(row)
return row_down == 0
[docs] def updatePos(self):
"""Update the visual x position of handle and slider annotations."""
x = self._toPos(self.value())
self.handle.setPos(x, 0)
for mark in self._mark_items.keys():
if mark.type == "track":
width_in_frames = mark.end_val - mark.val
width = max(2, self._toPos(width_in_frames))
else:
width = mark.visual_width
x = self._toPos(mark.val, center=True)
self._mark_items[mark].setPos(x, 0)
if mark in self._mark_labels:
label_x = max(
0, x - self._mark_labels[mark].boundingRect().width() // 2
)
self._mark_labels[mark].setPos(label_x, 4)
rect = self._mark_items[mark].rect()
rect.setWidth(width)
rect.setHeight(
mark.get_height(
container_height=self.getBoxRect().height() - self._header_height
)
)
self._mark_items[mark].setRect(rect)
def _get_header_series_len(self):
if hasattr(self.headerSeries, "keys"):
series_frame_max = max(self.headerSeries.keys())
else:
series_frame_max = len(self.headerSeries)
return series_frame_max
@property
def _header_series_items(self):
"""Uields (frame idx, val) for header series items."""
if hasattr(self.headerSeries, "items"):
for key, val in self.headerSeries.items():
yield key, val
else:
for key in range(len(self.headerSeries)):
val = self.headerSeries[key]
yield key, val
[docs] def moveHandle(self, x, y):
"""Move handle in response to mouse position.
Emits valueChanged signal if value of slider changed.
Args:
x: x position of mouse
y: y position of mouse
"""
x -= self.handle.rect().width() / 2.0
x = max(x, 0)
x = min(x, self.getBoxRect().width() - self.handle.rect().width())
val = self._toVal(x)
# snap to nearby mark within handle
mark_vals = [mark.val for mark in self._marks]
handle_left = self._toVal(x - self.handle.rect().width() / 2)
handle_right = self._toVal(x + self.handle.rect().width() / 2)
marks_in_handle = [
mark for mark in mark_vals if handle_left < mark < handle_right
]
if marks_in_handle:
marks_in_handle.sort(key=lambda m: (abs(m - val), m > val))
val = marks_in_handle[0]
old = self.value()
self.setValue(val)
if old != val:
self.valueChanged.emit(self._val_main)
[docs] def contiguousSelectionMarksAroundVal(self, val):
"""Selects contiguously marked frames around value."""
if not self.isMarkedVal(val):
return
dec_val = self.getStartContiguousMark(val)
inc_val = self.getEndContiguousMark(val)
self.setSelection(dec_val, inc_val)
def getStartContiguousMark(self, val):
last_val = val
dec_val = self.decrementContiguousMarkedVal(last_val)
while dec_val < last_val and dec_val > self._val_min:
last_val = dec_val
dec_val = self.decrementContiguousMarkedVal(last_val)
return dec_val
def getEndContiguousMark(self, val):
last_val = val
inc_val = self.incrementContiguousMarkedVal(last_val)
while inc_val > last_val and inc_val < self._val_max:
last_val = inc_val
inc_val = self.incrementContiguousMarkedVal(last_val)
return inc_val
[docs] def isMarkedVal(self, val):
"""Returns whether value has mark."""
if val in [mark.val for mark in self._marks]:
return True
if any(
mark.val <= val < mark.end_val
for mark in self._marks
if mark.type == "track"
):
return True
return False
[docs] def decrementContiguousMarkedVal(self, val):
"""Decrements value within contiguously marked range if possible."""
dec_val = min(
(
mark.val
for mark in self._marks
if mark.type == "track" and mark.val < val <= mark.end_val
),
default=val,
)
if dec_val < val:
return dec_val
if val - 1 in [mark.val for mark in self._marks]:
return val - 1
# Return original value if we can't decrement it w/in contiguous range
return val
[docs] def incrementContiguousMarkedVal(self, val):
"""Increments value within contiguously marked range if possible."""
inc_val = max(
(
mark.end_val - 1
for mark in self._marks
if mark.type == "track" and mark.val <= val < mark.end_val
),
default=val,
)
if inc_val > val:
return inc_val
if val + 1 in [mark.val for mark in self._marks]:
return val + 1
# Return original value if we can't decrement it w/in contiguous range
return val
def getBoxRect(self):
# return self.outlineBox.rect()
return self._box_rect
def setBoxRect(self, rect):
# self.outlineBox.setRect(rect)
self._box_rect = rect
# Update the scene rect so that it matches how much space we
# currently want for drawing everything.
rect.setWidth(rect.width() - 1)
self.setSceneRect(rect)
def getMarkAreaHeight(self):
_, max_height = self.getMinMaxHeights()
return max_height - 3 - self._header_height
[docs] def resizeEvent(self, event=None):
"""Override method to update visual size when necessary.
Args:
event
"""
outline_rect = self.getBoxRect()
handle_rect = self.handle.rect()
outline_rect.setHeight(self.getMarkAreaHeight() + self._header_height)
if event is not None:
visual_width = event.size().width() - 1
else:
visual_width = self.width() - 1
drawn_width = visual_width * self.zoom_factor
outline_rect.setWidth(drawn_width)
self.setBoxRect(outline_rect)
handle_rect.setTop(self._handle_top)
handle_rect.setHeight(self._handle_height)
self.handle.setRect(handle_rect)
self.updateSelectionBoxesOnResize()
self.setTickMarks()
self.updatePos()
self.drawHeader()
super(VideoSlider, self).resizeEvent(event)
@property
def _handle_top(self) -> float:
"""Returns y position of top of handle (i.e., header height)."""
return 1 + self._header_height
@property
def _handle_height(self, outline_rect=None) -> float:
"""
Returns visual height of handle.
Args:
outline_rect: The rect of the outline box for the slider. This
is only required when calling during initialization (when the
outline box doesn't yet exist).
Returns:
Height of handle in pixels.
"""
return self.getMarkAreaHeight()
[docs] def mousePressEvent(self, event):
"""Override method to move handle for mouse press/drag.
Args:
event
"""
scenePos = self.mapToScene(event.pos())
# Do nothing if not enabled
if not self.enabled():
return
# Do nothing if click outside slider area
if not self.getBoxRect().contains(scenePos):
return
move_function = None
release_function = None
if event.modifiers() == QtCore.Qt.ShiftModifier:
move_function = self.moveSelectionAnchor
release_function = self.releaseSelectionAnchor
self.clearSelection()
elif event.modifiers() == QtCore.Qt.NoModifier:
move_function = self.moveHandle
release_function = None
elif event.modifiers() == QtCore.Qt.AltModifier:
move_function = self.moveZoomDrag
release_function = self.releaseZoomDrag
else:
event.accept() # mouse events shouldn't be passed to video widgets
# Connect to signals
if move_function is not None:
self.mouseMoved.connect(move_function)
def done(x, y):
if release_function is not None:
release_function(x, y)
if move_function is not None:
self.mouseMoved.disconnect(move_function)
self.mouseReleased.disconnect(done)
self.mouseReleased.connect(done)
# Emit signal
self.mouseMoved.emit(scenePos.x(), scenePos.y())
self.mousePressed.emit(scenePos.x(), scenePos.y())
[docs] def mouseMoveEvent(self, event):
"""Override method to emid mouseMoved signal on drag."""
scenePos = self.mapToScene(event.pos())
self.mouseMoved.emit(scenePos.x(), scenePos.y())
[docs] def mouseReleaseEvent(self, event):
"""Override method to emit mouseReleased signal on release."""
scenePos = self.mapToScene(event.pos())
self.mouseReleased.emit(scenePos.x(), scenePos.y())
[docs] def mouseDoubleClickEvent(self, event):
"""Override method to move handle for mouse double-click.
Args:
event
"""
scenePos = self.mapToScene(event.pos())
# Do nothing if not enabled
if not self.enabled():
return
# Do nothing if click outside slider area
if not self.getBoxRect().contains(scenePos):
return
if event.modifiers() == QtCore.Qt.ShiftModifier:
self.contiguousSelectionMarksAroundVal(self._toVal(scenePos.x()))
[docs] def keyPressEvent(self, event):
"""Catch event and emit signal so something else can handle event."""
self.keyPress.emit(event)
event.accept()
[docs] def keyReleaseEvent(self, event):
"""Catch event and emit signal so something else can handle event."""
self.keyRelease.emit(event)
event.accept()
[docs] def boundingRect(self) -> QtCore.QRectF:
"""Method required by Qt."""
return self.getBoxRect()
[docs] def paint(self, *args, **kwargs):
"""Method required by Qt."""
super(VideoSlider, self).paint(*args, **kwargs)
if __name__ == "__main__":
app = QtWidgets.QApplication([])
window = VideoSlider(
min=0,
max=20,
val=15,
marks=(10, 15), # ((0,10),(0,15),(1,10),(1,11),(2,12)), tracks=3
)
window.valueChanged.connect(lambda x: print(x))
window.show()
app.exec_()