class PortSelector(slots.SlotContainer, QtWidgets.QComboBox): selectedPort, setSelectedPort, selectedPortChanged = slots.slot(str, 'selectedPort') def __init__(self, device_list: DeviceList, parent: QtWidgets.QWidget) -> None: super().__init__(parent=parent) self.__device_list = device_list self.setInsertPolicy(QtWidgets.QComboBox.NoInsert) self.setEditable(True) self.lineEdit().setReadOnly(True) self.__filter = FilteredDeviceList(device_list) self.setModel(self.__filter) view = QtWidgets.QTreeView() view.setHeaderHidden(True) self.setView(view) view.expandAll() self.currentIndexChanged.connect(lambda: self.setSelectedPort(self.currentData())) self.selectedPortChanged.connect(self.__updateName) def __updateName(self, uri: str) -> None: try: port_name = self.__device_list.getPort(uri).display_name except KeyError: port_name = uri self.setEditText(port_name)
class GraphView(ui_base.ProjectMixin, slots.SlotContainer, QtWidgets.QWidget): currentTrack, setCurrentTrack, currentTrackChanged = slots.slot( music.Track, 'currentTrack', allow_none=True) def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) canvas_frame = Frame(parent=self) self.__canvas = canvas.Canvas(parent=canvas_frame, context=self.context) canvas_frame.setWidget(self.__canvas) self.__toolbox = toolbox.Toolbox(parent=self, context=self.context) self.__toolbox.toolChanged.connect(self.__canvas.toolChanged) self.__toolbox.resetViewTriggered.connect(self.__canvas.resetView) layout = QtWidgets.QHBoxLayout() layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.__toolbox) layout.addWidget(canvas_frame) self.setLayout(layout) self.currentTrackChanged.connect(self.__canvas.setCurrentTrack) self.__canvas.currentTrackChanged.connect(self.setCurrentTrack)
class ControlValueConnector(ui_base.ProjectMixin, slots.SlotContainer, core.AutoCleanupMixin, QtCore.QObject): value, setValue, valueChanged = slots.slot(float, 'value') def __init__(self, *, node: music.BaseNode, name: str, **kwargs: Any) -> None: super().__init__(**kwargs) self.__node = node self.__name = name self.__listeners = core.ListenerList() self.add_cleanup_function(self.__listeners.cleanup) self.__generation = self.__node.control_value_map.generation( self.__name) self.setValue(self.__node.control_value_map.value(self.__name)) self.valueChanged.connect(self.__onValueEdited) self.__listeners.add( self.__node.control_value_map.control_value_changed.add( self.__name, self.__onValueChanged)) def __onValueEdited(self, value: float) -> None: if value != self.__node.control_value_map.value(self.__name): self.__generation += 1 with self.project.apply_mutations('%s: Change control value "%s"' % (self.__node.name, self.__name)): self.__node.set_control_value(self.__name, value, self.__generation) def __onValueChanged( self, change: music.PropertyValueChange[value_types.ControlValue] ) -> None: if change.new_value.generation < self.__generation: return self.__generation = change.new_value.generation self.setValue(change.new_value.value) def connect(self, getter: QtCore.pyqtBoundSignal, setter: Callable[[float], None]) -> None: getter.connect(self.setValue) setter(self.value()) self.valueChanged.connect(setter)
class RecordButton(slots.SlotContainer, QtWidgets.QPushButton): recordState, setRecordState, recordStateChanged = slots.slot( RecordState, 'recordState') def __init__(self) -> None: super().__init__() self.setText("Record") self.setIcon( QtGui.QIcon( os.path.join(constants.DATA_DIR, 'icons', 'media-record.svg'))) self.__default_bg = self.palette().color(QtGui.QPalette.Button) self.recordStateChanged.connect(self.__recordStateChanged) self.__timer = QtCore.QTimer() self.__timer.setInterval(250) self.__timer.timeout.connect(self.__blink) self.__blink_state = False def __recordStateChanged(self, state: RecordState) -> None: palette = self.palette() if state == RecordState.OFF: self.__timer.stop() palette.setColor(self.backgroundRole(), self.__default_bg) elif state == RecordState.WAITING: self.__timer.start() self.__blink_state = True palette.setColor(self.backgroundRole(), QtGui.QColor(0, 255, 0)) elif state == RecordState.RECORDING: self.__timer.stop() palette.setColor(self.backgroundRole(), QtGui.QColor(255, 0, 0)) self.setPalette(palette) def __blink(self) -> None: self.__blink_state = not self.__blink_state palette = self.palette() if self.__blink_state: palette.setColor(self.backgroundRole(), QtGui.QColor(0, 255, 0)) else: palette.setColor(self.backgroundRole(), self.__default_bg) self.setPalette(palette)
class ControlValueEnum(slots.SlotContainer, QtWidgets.QComboBox): value, setValue, valueChanged = slots.slot(float, 'value', default=0.0) def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) self.valueChanged.connect(lambda _: self.__valueChanged()) self.currentIndexChanged.connect(lambda _: self.__currentIndexChanged()) def __valueChanged(self) -> None: closest_idx = None # type: int closest_dist = None # type: float for idx in range(self.count()): value = self.itemData(idx) dist = abs(value - self.value()) if closest_idx is None or dist < closest_dist: closest_idx = idx closest_dist = dist if closest_idx is not None: self.setCurrentIndex(closest_idx) def __currentIndexChanged(self) -> None: self.setValue(self.currentData())
class TrackLabel(slots.SlotContainer, QtWidgets.QLabel): isCurrent, setIsCurrent, isCurrentChanged = slots.slot(bool, 'isCurrent', default=False) def __init__(self, parent: QtWidgets.QWidget) -> None: super().__init__(parent=parent) self.setAttribute(Qt.WA_TransparentForMouseEvents, True) self.setFrameStyle(QtWidgets.QFrame.Panel | QtWidgets.QFrame.Raised) self.setBackgroundRole(QtGui.QPalette.Window) self.setAutoFillBackground(True) self.isCurrentChanged.connect(lambda current: self.setBackgroundRole( QtGui.QPalette.Highlight if current else QtGui.QPalette.Window)) font = QtGui.QFont(self.font()) font.setPointSizeF(0.8 * font.pointSizeF()) self.setFont(font) def setText(self, text: str) -> None: super().setText(text) self.resize(self.sizeHint())
class GainSlider(slots.SlotContainer, QtWidgets.QWidget): orientation, setOrientation, orientationChanged = slots.slot( Qt.Orientation, 'orientation', default=Qt.Horizontal) value, setValue, valueChanged = slots.slot(float, 'value', default=0.0) default, setDefault, defaultChanged = slots.slot(float, 'default', default=0.0) minimum, setMinimum, minimumChanged = slots.slot(float, 'minimum', default=-20.0) maximum, setMaximum, maximumChanged = slots.slot(float, 'maximum', default=20.0) def __init__(self, parent: Optional[QtWidgets.QWidget]) -> None: super().__init__(parent=parent) self.orientationChanged.connect(lambda _: self.update()) self.valueChanged.connect(lambda _: self.update()) self.maximumChanged.connect(lambda _: self.update()) self.minimumChanged.connect(lambda _: self.update()) self.__display_func = lambda value: '%.2f' % value self.__dragging = False self.__drag_pos = None # type: QtCore.QPoint def sizeHint(self) -> QtCore.QSize: return QtCore.QSize(50, 50) def minimumSizeHint(self) -> QtCore.QSize: if self.orientation() == Qt.Horizontal: return QtCore.QSize(100, 24) else: return QtCore.QSize(24, 100) def setRange(self, minimum: float, maximum: float) -> None: self.setMinimum(minimum) self.setMaximum(maximum) def setDisplayFunc(self, func: Callable[[float], str]) -> None: self.__display_func = func self.update() def normalizedValue(self) -> float: return self.normalize(self.value()) def normalize(self, value: float) -> float: value = max(self.minimum(), min(value, self.maximum())) return (value - self.minimum()) / (self.maximum() - self.minimum()) def denormalize(self, value: float) -> float: value = max(0.0, min(value, 1.0)) return (self.maximum() - self.minimum()) * value + self.minimum() def normalizedValueToOffset(self, value: float) -> int: if self.orientation() == Qt.Horizontal: return int((self.width() - 5) * value) else: return int((self.height() - 5) * value) def valueToOffset(self, value: float) -> int: if self.orientation() == Qt.Horizontal: return int((self.width() - 5) * self.normalize(value)) else: return int((self.height() - 5) * self.normalize(value)) def paintEvent(self, evt: QtGui.QPaintEvent) -> None: w, h = self.width(), self.height() value = self.normalizedValue() painter = QtGui.QPainter(self) try: painter.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing) painter.fillRect(0, 0, w, h, Qt.black) if self.orientation() == Qt.Horizontal: show_ticks = (w > 200) and (h > 20) if show_ticks: m = self.minimum() while m <= self.maximum(): x = self.valueToOffset(m) painter.fillRect(x + 2, 2, 1, h - 4, QtGui.QColor(60, 60, 60)) m += 5.0 x = self.normalizedValueToOffset(value) painter.fillRect(2, 2, x, h - 4, QtGui.QColor(100, 100, 255)) painter.fillRect(x + 2, 2, 1, h - 4, QtGui.QColor(255, 100, 100)) if show_ticks: m = self.minimum() while m <= self.maximum(): x = self.valueToOffset(m) painter.fillRect(x + 2, 2, 1, 4, QtGui.QColor(255, 255, 255)) painter.fillRect(x + 2, h - 6, 1, 4, QtGui.QColor(255, 255, 255)) m += 5.0 else: show_ticks = (h > 200) and (w > 20) if show_ticks: m = self.minimum() while m <= self.maximum(): y = self.valueToOffset(m) painter.fillRect(2, h - y - 3, w - 4, 1, QtGui.QColor(60, 60, 60)) m += 5.0 y = self.normalizedValueToOffset(value) painter.fillRect(2, h - y - 2, w - 4, y, QtGui.QColor(100, 100, 255)) painter.fillRect(2, h - y - 3, w - 4, 1, QtGui.QColor(255, 100, 100)) if show_ticks: m = self.minimum() while m <= self.maximum(): y = self.valueToOffset(m) painter.fillRect(2, h - y - 3, 4, 1, QtGui.QColor(255, 255, 255)) painter.fillRect(w - 6, h - y - 3, 4, 1, QtGui.QColor(255, 255, 255)) m += 5.0 if w > 60 and h > 14: font = QtGui.QFont("Arial") font.setPixelSize(12) painter.setFont(font) label = self.__display_func(self.value()) fm = QtGui.QFontMetrics(font) r = fm.boundingRect(label) r.moveTopLeft( QtCore.QPoint( int((w - r.width()) / 2 + r.left()), int((h - r.height()) / 2 + 1.5 * fm.capHeight()))) pen = QtGui.QPen() pen.setColor(Qt.black) painter.setPen(pen) for x, y, in [(-1, 0), (1, 0), (0, -1), (0, 1)]: painter.drawText(r.topLeft() + QtCore.QPoint(x, y), label) pen = QtGui.QPen() pen.setColor(Qt.white) painter.setPen(pen) painter.drawText(r.topLeft(), label) finally: painter.end() def mouseDoubleClickEvent(self, evt: QtGui.QMouseEvent) -> None: if evt.button() == Qt.LeftButton: self.setValue(self.default()) def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None: if evt.button() == Qt.LeftButton: self.__dragging = True self.__drag_pos = self.mapToGlobal(evt.pos()) if not evt.modifiers() & Qt.ShiftModifier: if self.orientation() == Qt.Horizontal: self.setValue( self.denormalize( (evt.pos().x() - 2) / (self.width() - 5))) else: self.setValue( self.denormalize(1.0 - (evt.pos().y() - 2) / (self.height() - 5))) evt.accept() return super().mousePressEvent(evt) def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None: if self.__dragging: delta_p = (self.mapToGlobal(evt.pos()) - self.__drag_pos) self.__drag_pos = self.mapToGlobal(evt.pos()) if evt.modifiers() & Qt.ShiftModifier: if self.orientation() == Qt.Horizontal: delta = delta_p.x() else: delta = -delta_p.y() step_size = 0.0001 value = self.denormalize(self.normalizedValue() + delta * step_size) value = max(self.minimum(), min(value, self.maximum())) self.setValue(value) else: if self.orientation() == Qt.Horizontal: self.setValue( self.denormalize( (evt.pos().x() - 2) / (self.width() - 5))) else: self.setValue( self.denormalize(1.0 - (evt.pos().y() - 2) / (self.height() - 5))) evt.accept() return super().mouseMoveEvent(evt) def mouseReleasevent(self, evt: QtGui.QMouseEvent) -> None: if self.__dragging and evt.button() == Qt.LeftButton: self.__dragging = False evt.accept() return super().mouseReleaseEvent(evt)
class StepToggle(slots.SlotContainer, QtWidgets.QWidget): checked, setChecked, checkedChanged = slots.slot(bool, 'checked', default=False) def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: super().__init__(parent=parent) self.checkedChanged.connect(lambda _: self.update()) def sizeHint(self) -> QtCore.QSize: return QtCore.QSize(50, 50) def minimumSizeHint(self) -> QtCore.QSize: return QtCore.QSize(32, 32) def hasHeightForWidth(self) -> bool: return True def heightForWidth(self, w: int) -> int: return w def paintEvent(self, evt: QtGui.QPaintEvent) -> None: w = self.width() h = self.height() painter = QtGui.QPainter(self) try: painter.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing) if self.isEnabled(): border_color = QtGui.QColor(0, 0, 0) bg_color = QtGui.QColor(200, 200, 200) button_color = QtGui.QColor(100, 100, 255) else: border_color = QtGui.QColor(80, 80, 80) bg_color = QtGui.QColor(120, 120, 120) button_color = QtGui.QColor(80, 80, 80) pen = QtGui.QPen() pen.setColor(border_color) pen.setWidth(2) painter.setPen(pen) painter.setBrush(QtGui.QBrush(bg_color)) painter.drawRoundedRect(1, 1, w - 2, h - 2, 4, 4) if self.checked(): painter.fillRect(4, 4, w - 8, h - 8, button_color) finally: painter.end() def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None: if evt.button() == Qt.LeftButton: self.setChecked(not self.checked()) evt.accept() return super().mousePressEvent(evt)
class TrackListView(ui_base.ProjectMixin, slots.SlotContainer, QtWidgets.QSplitter): playingChanged = QtCore.pyqtSignal(bool) loopEnabledChanged = QtCore.pyqtSignal(bool) currentTrack, setCurrentTrack, currentTrackChanged = slots.slot( music.Track, 'currentTrack', allow_none=True) def __init__(self, *, project_view: 'project_view_lib.ProjectView', player_state: player_state_lib.PlayerState, **kwargs: Any) -> None: super().__init__(**kwargs) self.__project_view = project_view self.__player_state = player_state self.__session_prefix = 'tracklist:%s:' % self.project.id self.__session_data_last_update = {} # type: Dict[str, float] editor_frame = Frame(self) self.__editor = editor.Editor(player_state=self.__player_state, parent=editor_frame, context=self.context) editor_frame.setWidget(self.__editor) self.__editor.currentTrackChanged.connect(self.setCurrentTrack) self.currentTrackChanged.connect(self.__editor.setCurrentTrack) self.__editor.setScaleX( self.__get_session_value('scale_x', self.__editor.scaleX())) self.__editor.setXOffset(self.__get_session_value('x_offset', 0)) self.__editor.setYOffset(self.__get_session_value('y_offset', 0)) self.__editor.scaleXChanged.connect(self.__updateScaleX) time_line_frame = Frame(self) self.__time_line = time_line.TimeLine(parent=time_line_frame, player_state=self.__player_state, context=self.context) time_line_frame.setWidget(self.__time_line) self.__time_line.setScaleX(self.__editor.scaleX()) self.__time_line.setXOffset(self.__editor.xOffset()) self.__editor.scaleXChanged.connect(self.__time_line.setScaleX) self.__time_line.setAdditionalXOffset(self.__editor.sidebarWidth()) self.__editor.sidebarWidthChanged.connect( self.__time_line.setAdditionalXOffset) scroll_x = QtWidgets.QScrollBar(orientation=Qt.Horizontal, parent=self) scroll_x.setRange(0, self.__editor.maximumXOffset()) scroll_x.setSingleStep(50) scroll_x.setPageStep(self.__editor.pageWidth()) scroll_x.setValue(self.__editor.xOffset()) scroll_y = QtWidgets.QScrollBar(orientation=Qt.Vertical, parent=self) scroll_y.setRange(0, self.__editor.maximumYOffset()) scroll_y.setSingleStep(20) scroll_y.setPageStep(self.__editor.pageHeight()) scroll_y.setValue(self.__editor.yOffset()) self.__editor.maximumXOffsetChanged.connect(scroll_x.setMaximum) self.__editor.pageWidthChanged.connect(scroll_x.setPageStep) self.__editor.xOffsetChanged.connect(scroll_x.setValue) self.__time_line.xOffsetChanged.connect(scroll_x.setValue) scroll_x.valueChanged.connect(self.__editor.setXOffset) scroll_x.valueChanged.connect(self.__time_line.setXOffset) scroll_x.valueChanged.connect(self.__updateXOffset) self.__editor.maximumYOffsetChanged.connect(scroll_y.setMaximum) self.__editor.pageHeightChanged.connect(scroll_y.setPageStep) self.__editor.yOffsetChanged.connect(scroll_y.setValue) scroll_y.valueChanged.connect(self.__editor.setYOffset) scroll_y.valueChanged.connect(self.__updateYOffset) self.setMinimumHeight(time_line_frame.minimumHeight()) editor_pane = QtWidgets.QWidget(self) layout = QtWidgets.QGridLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(1) layout.addWidget(time_line_frame, 0, 0, 1, 1) layout.addWidget(editor_frame, 1, 0, 1, 1) layout.addWidget(scroll_x, 2, 0, 1, 1) layout.addWidget(scroll_y, 1, 1, 1, 1) editor_pane.setLayout(layout) self.__toolbox = toolbox.Toolbox(parent=self, context=self.context) self.__toolbox.setCurrentToolBox(self.__editor.currentToolBox()) self.__editor.currentToolBoxChanged.connect( self.__toolbox.setCurrentToolBox) self.addWidget(self.__toolbox) self.setStretchFactor(0, 0) self.addWidget(editor_pane) self.setStretchFactor(1, 1) self.setCollapsible(1, False) def __get_session_value(self, key: str, default: Any) -> Any: return self.get_session_value(self.__session_prefix + key, default) def __set_session_value(self, key: str, value: Any) -> None: self.set_session_value(self.__session_prefix + key, value) def __lazy_set_session_value(self, key: str, value: Any) -> None: # TODO: value should be stored to session 5sec after most recent change. I.e. need # some timer... last_time = self.__session_data_last_update.get(key, 0) if time_lib.time() - last_time > 5: self.__set_session_value(key, value) self.__session_data_last_update[key] = time_lib.time() def __updateScaleX(self, scale: fractions.Fraction) -> None: self.__set_session_value('scale_x', scale) def __updateXOffset(self, offset: int) -> None: self.__lazy_set_session_value('x_offset', offset) def __updateYOffset(self, offset: int) -> None: self.__lazy_set_session_value('y_offset', offset) def setPlayerID(self, player_id: str) -> None: self.__time_line.setPlayerID(player_id) def cleanup(self) -> None: self.__editor.cleanup()
class BaseTrackEditor(time_view_mixin.TimeViewMixin, ui_base.ProjectMixin, core.AutoCleanupMixin, slots.SlotContainer, QtWidgets.QWidget): sizeChanged = QtCore.pyqtSignal(QtCore.QSize) currentToolChanged = QtCore.pyqtSignal(tools.ToolType) playbackPosition, setPlaybackPosition, playbackPositionChanged = slots.slot( audioproc.MusicalTime, 'playbackPosition', default=audioproc.MusicalTime(-1, 1)) isCurrent, setIsCurrent, isCurrentChanged = slots.slot(bool, 'isCurrent', default=False) defaultHeight, setDefaultHeight, defaultHeightChanged = slots.slot( int, 'defaultHeight', default=200) zoom, setZoom, zoomChanged = slots.slot(fractions.Fraction, 'zoom', default=fractions.Fraction(1, 1)) def __init__(self, *, track: music.Track, player_state: player_state_lib.PlayerState, editor: 'editor_lib.Editor', **kwargs: Any) -> None: self.__auto_scroll = True super().__init__(parent=editor, **kwargs) self.setMouseTracking(True) self.setMinimumHeight(10) self.setMaximumHeight(1000) self.__track = track self.__player_state = player_state self.__editor = editor self.__zoom = fractions.Fraction(1, 1) self._bg_color = QtGui.QColor(255, 255, 255) self.isCurrentChanged.connect(self.__isCurrentChanged) self.__isCurrentChanged(self.isCurrent()) self.scaleXChanged.connect(lambda _: self.__scaleChanged()) self.zoomChanged.connect(lambda _: self.__scaleChanged()) self.__scaleChanged() self.__toolbox = self.createToolBox() self.currentToolChanged.emit(self.__toolbox.currentToolType()) self.__toolbox.toolTypeChanged.connect(self.currentToolChanged.emit) @property def track(self) -> music.Track: return self.__track def setAutoScroll(self, auto_scroll: bool) -> None: self.__auto_scroll = auto_scroll def setXOffset(self, offset: int) -> int: dx = super().setXOffset(offset) if self.__auto_scroll: self.scroll(dx, 0) return dx def __scaleChanged(self) -> None: self.updateSize() self.purgePaintCaches() self.update() def offset(self) -> QtCore.QPoint: return QtCore.QPoint(self.xOffset(), 0) def updateSize(self) -> None: pass def __isCurrentChanged(self, is_current: bool) -> None: if is_current: self._bg_color = QtGui.QColor(240, 240, 255) else: self._bg_color = QtGui.QColor(255, 255, 255) self.update() def purgePaintCaches(self) -> None: pass def createToolBox(self) -> tools.ToolBox: raise NotImplementedError def toolBox(self) -> tools.ToolBox: return self.__toolbox def currentTool(self) -> tools.ToolBase: return self.__toolbox.currentTool() def currentToolType(self) -> tools.ToolType: return self.__toolbox.currentToolType() def setCurrentToolType(self, tool: tools.ToolType) -> None: self.__toolbox.setCurrentToolType(tool) def playerState(self) -> player_state_lib.PlayerState: return self.__player_state def resizeEvent(self, evt: QtGui.QResizeEvent) -> None: self.sizeChanged.emit(evt.size()) super().resizeEvent(evt) def _paint(self, painter: QtGui.QPainter, rect: QtCore.QRect) -> None: painter.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing) font = QtGui.QFont("Arial") font.setPixelSize(14) painter.setFont(font) pen = QtGui.QPen() pen.setColor(Qt.black) painter.setPen(pen) painter.drawText( QtCore.QRect(0, 0, self.width(), self.height()), Qt.AlignCenter, "%s.paintEvent() not implemented" % type(self).__name__) def paintEvent(self, evt: QtGui.QPaintEvent) -> None: painter = QtGui.QPainter(self) try: painter.fillRect(evt.rect(), self._bg_color) painter.translate(-self.xOffset(), 0) self._paint(painter, evt.rect().translated(self.xOffset(), 0)) finally: painter.end() def _makeMouseEvent(self, evt: QtGui.QMouseEvent) -> QtGui.QMouseEvent: return QtGui.QMouseEvent(evt.type(), evt.localPos() + self.offset(), evt.windowPos(), evt.screenPos(), evt.button(), evt.buttons(), evt.modifiers()) def contextMenuEvent(self, evt: QtGui.QContextMenuEvent) -> None: evt = QtGui.QContextMenuEvent(evt.reason(), evt.pos() + self.offset(), evt.globalPos(), evt.modifiers()) self.__toolbox.contextMenuEvent(evt) def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None: self.__toolbox.mouseMoveEvent(self._makeMouseEvent(evt)) def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None: self.__editor.setCurrentTrack(self.track) self.__toolbox.mousePressEvent(self._makeMouseEvent(evt)) def mouseReleaseEvent(self, evt: QtGui.QMouseEvent) -> None: self.__toolbox.mouseReleaseEvent(self._makeMouseEvent(evt)) def mouseDoubleClickEvent(self, evt: QtGui.QMouseEvent) -> None: self.__toolbox.mouseDoubleClickEvent(self._makeMouseEvent(evt)) def wheelEvent(self, evt: QtGui.QWheelEvent) -> None: evt = QtGui.QWheelEvent(evt.pos() + self.offset(), evt.globalPos(), evt.pixelDelta(), evt.angleDelta(), 0, Qt.Horizontal, evt.buttons(), evt.modifiers(), evt.phase(), evt.source()) self.__toolbox.wheelEvent(evt) def keyPressEvent(self, evt: QtGui.QKeyEvent) -> None: self.__toolbox.keyPressEvent(evt) def keyReleaseEvent(self, evt: QtGui.QKeyEvent) -> None: self.__toolbox.keyReleaseEvent(evt)
class Editor(object_list_manager.ObjectListManager[music.Track, TrackContainer], time_view_mixin.TimeViewMixin, ui_base.ProjectMixin, slots.SlotContainer, core.AutoCleanupMixin, QtWidgets.QWidget): maximumYOffsetChanged = QtCore.pyqtSignal(int) yOffsetChanged = QtCore.pyqtSignal(int) pageHeightChanged = QtCore.pyqtSignal(int) currentToolBoxChanged = QtCore.pyqtSignal(tools.ToolBox) currentTrackChanged = QtCore.pyqtSignal(object) playbackPosition, setPlaybackPosition, playbackPositionChanged = slots.slot( audioproc.MusicalTime, 'playbackPosition', default=audioproc.MusicalTime(-1, 1)) sidebarWidth, setSidebarWidth, sidebarWidthChanged = slots.slot( int, 'sidebarWidth', default=12) zoom, setZoom, zoomChanged = slots.slot(fractions.Fraction, 'zoom', default=fractions.Fraction(1, 1)) MIN_ZOOM = fractions.Fraction(2, 3)**12 MAX_ZOOM = fractions.Fraction(3, 2)**2 def __init__(self, *, player_state: player_state_lib.PlayerState, **kwargs: Any) -> None: self.__player_state = player_state self.__current_tool_box = None # type: tools.ToolBox self.__current_tool = None # type: tools.ToolBase self.__y_offset = 0 super().__init__(**kwargs) self.setMouseTracking(True) self.setFocusPolicy(Qt.StrongFocus) self.setMinimumWidth(50) self.setMinimumHeight(0) self.__in_track_resize = False self.__moving_track = None # type: TrackContainer self.__moving_track_pos = None # type: int self.__moving_track_insert_index = None # type: int self.__auto_scroll_dy = 0 self.__auto_scroll_timer = QtCore.QTimer(self) self.__auto_scroll_timer.setInterval(1000 // 50) self.__auto_scroll_timer.timeout.connect(self.__autoScrollTick) self.initObjectList(self.project, 'nodes') self.__content_height = 0 self.__updateTracks() self.objectListChanged.connect(self.__updateTracks) self.sidebarWidthChanged.connect(self.__updateTracks) self.__current_track = None # type: music.Track for idx, container in enumerate(self.objectWrappers()): if idx == 0: self.__onCurrentTrackChanged(container.track) self.currentTrackChanged.connect(self.__onCurrentTrackChanged) self.__player_state.currentTimeChanged.connect( self.setPlaybackPosition) self.setZoom( self.get_session_value('tracklist:%s:zoom' % self.project.id, fractions.Fraction(1, 1))) self.zoomChanged.connect( functools.partial(self.set_session_value, 'tracklist:%s:zoom' % self.project.id)) self.zoomChanged.connect(lambda _: self.__updateTracks()) self.__increase_scale_x_action = QtWidgets.QAction(self) self.__increase_scale_x_action.setShortcut("ctrl+left") self.__increase_scale_x_action.setShortcutContext(Qt.WindowShortcut) self.__increase_scale_x_action.triggered.connect( functools.partial(self.__setScaleX, fractions.Fraction(2, 3))) self.addAction(self.__increase_scale_x_action) self.__decrease_scale_x_action = QtWidgets.QAction(self) self.__decrease_scale_x_action.setShortcut("ctrl+right") self.__decrease_scale_x_action.setShortcutContext(Qt.WindowShortcut) self.__decrease_scale_x_action.triggered.connect( functools.partial(self.__setScaleX, fractions.Fraction(3, 2))) self.addAction(self.__decrease_scale_x_action) self.__increase_zoom_action = QtWidgets.QAction(self) self.__increase_zoom_action.setShortcut("ctrl++") self.__increase_zoom_action.setShortcutContext( Qt.WidgetWithChildrenShortcut) self.__increase_zoom_action.triggered.connect( functools.partial(self.__scaleZoom, fractions.Fraction(3, 2))) self.addAction(self.__increase_zoom_action) self.__decrease_zoom_action = QtWidgets.QAction(self) self.__decrease_zoom_action.setShortcut("ctrl+-") self.__decrease_zoom_action.setShortcutContext( Qt.WidgetWithChildrenShortcut) self.__decrease_zoom_action.triggered.connect( functools.partial(self.__scaleZoom, fractions.Fraction(2, 3))) self.addAction(self.__decrease_zoom_action) self.__reset_zoom_action = QtWidgets.QAction(self) self.__reset_zoom_action.setShortcut("ctrl+0") self.__reset_zoom_action.setShortcutContext( Qt.WidgetWithChildrenShortcut) self.__reset_zoom_action.triggered.connect(self.__resetZoom) self.addAction(self.__reset_zoom_action) def __setScaleX(self, factor: fractions.Fraction) -> None: new_scale_x = self.scaleX() * factor new_scale_x = max(fractions.Fraction(5, 1), new_scale_x) new_scale_x = min(fractions.Fraction(10000, 1), new_scale_x) center_time = max( 0, self.width() // 2 - self.leftMargin() + self.xOffset()) / self.scaleX() self.setScaleX(new_scale_x) center_x = self.leftMargin() + int(self.scaleX() * center_time) self.setXOffset(max(0, center_x - self.width() // 2)) def __setZoom(self, zoom: fractions.Fraction) -> None: if zoom == self.zoom(): return center_y = (self.yOffset() + self.height() // 2) / self.zoom() self.setZoom(zoom) self.setYOffset( max( 0, min(self.maximumYOffset(), int(center_y * self.zoom()) - self.height() // 2))) def __scaleZoom(self, factor: fractions.Fraction) -> None: new_zoom = self.zoom() * factor new_zoom = max(self.MIN_ZOOM, new_zoom) new_zoom = min(self.MAX_ZOOM, new_zoom) self.__setZoom(new_zoom) self.__setScaleX(factor) def __resetZoom(self) -> None: self.__setScaleX(1 / self.zoom()) self.__setZoom(fractions.Fraction(1, 1)) def __autoScrollTick(self) -> None: self.setYOffset( max( 0, min(self.maximumYOffset(), self.yOffset() + self.__auto_scroll_dy))) def setAutoScroll(self, dy: int) -> None: self.__auto_scroll_dy = dy if self.__auto_scroll_dy and self.isVisible(): self.__auto_scroll_timer.start() else: self.__auto_scroll_timer.stop() def currentTrack(self) -> music.Track: return self.__current_track def setCurrentTrack(self, track: music.Track) -> None: if track is self.__current_track: return if self.__current_track is not None: container = self.objectWrapperById(self.__current_track.id) container.setIsCurrent(False) self.__current_track = None if track is not None: container = self.objectWrapperById(track.id) container.setIsCurrent(True) self.__current_track = track if container.track.visible and self.isVisible(): track_y = container.track_editor.y() + self.yOffset() yoffset = self.yOffset() if track_y + container.track_editor.height( ) > yoffset + self.height(): yoffset = track_y + container.track_editor.height( ) - self.height() if track_y < yoffset: yoffset = track_y self.setYOffset(yoffset) self.currentTrackChanged.emit(self.__current_track) def _filterObject(self, obj: music.ObjectBase) -> bool: return isinstance(obj, music.Track) def _createObjectWrapper(self, track: music.Track) -> TrackContainer: container = TrackContainer(editor=self, track=track, player_state=self.__player_state, context=self.context) container.visibilityChanged.connect(lambda _: self.__updateTracks()) return container def _deleteObjectWrapper(self, container: TrackContainer) -> None: if container.track is self.__current_track: self.setCurrentTrack(None) container.cleanup() def __updateTracks(self) -> None: separator_height = max(1, min(8, int(self.zoom() * 4))) tracks = [] # type: List[Tuple[TrackContainer, int]] moving_track_height = 0 content_height = 0 for container in self.objectWrappers(): if not container.track.visible: container.hide() continue if not tracks: content_height += separator_height track_height = max(5, int(self.zoom() * container.height)) track_height = min(container.track_editor.maximumHeight(), track_height) track_height = max(container.track_editor.minimumHeight(), track_height) if container is self.__moving_track: moving_track_height = track_height tracks.append((container, track_height)) content_height += track_height + separator_height if self.__in_track_resize: content_height = max(content_height, self.__content_height) if content_height != self.__content_height: self.__content_height = content_height self.maximumYOffsetChanged.emit( max(0, self.__content_height - self.height())) if self.__content_height >= self.height(): y = -self.yOffset() else: y = (self.height() - self.__content_height) // 2 y += separator_height show_top_sep = True moving_track_inserted = False for container, track_height in tracks: if container is self.__moving_track: container.setTrackGeometry( QtCore.QRect(0, self.__moving_track_pos, self.width(), track_height), self.sidebarWidth(), separator_height, True) show_top_sep = True else: if (not moving_track_inserted and self.__moving_track is not None and self.__moving_track_pos < y + track_height // 2): y += moving_track_height + separator_height if container.track.index > self.__moving_track.track.index: self.__moving_track_insert_index = container.track.index - 1 else: self.__moving_track_insert_index = container.track.index moving_track_inserted = True container.setTrackGeometry( QtCore.QRect(0, y, self.width(), track_height), self.sidebarWidth(), separator_height, show_top_sep) show_top_sep = False y += track_height + separator_height if not moving_track_inserted and self.__moving_track is not None: self.__moving_track_insert_index = len(self.project.nodes) - 1 if self.__moving_track is not None: self.__moving_track.raise_() def beginTrackResize(self) -> None: self.__in_track_resize = True def endTrackResize(self) -> None: self.__in_track_resize = False self.__updateTracks() def setTrackHeight(self, container: TrackContainer, height: int) -> None: h = fractions.Fraction(height) / self.zoom() h = min(container.track_editor.maximumHeight(), h) h = max(container.track_editor.minimumHeight(), h) if h != container.height: container.setHeight(h) self.__updateTracks() def beginTrackMove(self, container: TrackContainer) -> None: self.__moving_track = container self.__moving_track_pos = container.track_editor.pos().y() self.__moving_track_insert_index = None self.__updateTracks() def endTrackMove(self) -> None: assert self.__moving_track is not None if self.__moving_track_insert_index is not None: moving_track = self.__moving_track new_index = self.__moving_track_insert_index self.__moving_track = None self.__moving_track_insert_index = None with self.project.apply_mutations('Move track "%s"' % moving_track.track.name): self.project.nodes.move(moving_track.track.index, new_index) self.__updateTracks() def moveTrack(self, pos: int) -> None: self.__moving_track_pos = pos self.__updateTracks() def __onCurrentTrackChanged(self, track: music.Track) -> None: if track is not None: container = self.objectWrapperById(track.id) self.setCurrentToolBox(container.track_editor.toolBox()) else: self.setCurrentToolBox(None) def currentToolBox(self) -> tools.ToolBox: return self.__current_tool_box def setCurrentToolBox(self, toolbox: tools.ToolBox) -> None: if self.__current_tool_box is toolbox: return logger.debug("Switching to tool box %s", type(toolbox).__name__) if self.__current_tool_box is not None: self.__current_tool_box.currentToolChanged.disconnect( self.__onCurrentToolChanged) self.__onCurrentToolChanged(None) self.__current_tool_box = None if toolbox is not None: self.__current_tool_box = toolbox self.__onCurrentToolChanged(self.__current_tool_box.currentTool()) self.__current_tool_box.currentToolChanged.connect( self.__onCurrentToolChanged) self.currentToolBoxChanged.emit(self.__current_tool_box) def __onCurrentToolChanged(self, tool: tools.ToolBase) -> None: if tool is self.__current_tool: return logger.debug("Current tool: %s", tool) if self.__current_tool is not None: self.__current_tool.cursorChanged.disconnect( self.__onToolCursorChanged) self.__onToolCursorChanged(None) self.__current_tool = None if tool is not None: self.__current_tool = tool self.__onToolCursorChanged(self.__current_tool.cursor()) self.__current_tool.cursorChanged.connect( self.__onToolCursorChanged) def __onToolCursorChanged(self, cursor: QtGui.QCursor) -> None: logger.debug("Cursor changed: %s", cursor) if cursor is not None: self.setCursor(cursor) else: self.setCursor(QtGui.QCursor(Qt.ArrowCursor)) def maximumYOffset(self) -> int: return max(0, self.__content_height - self.height()) def pageHeight(self) -> int: return self.height() def yOffset(self) -> int: return self.__y_offset def setYOffset(self, offset: int) -> None: if offset == self.__y_offset: return self.__y_offset = offset self.yOffsetChanged.emit(self.__y_offset) self.__updateTracks() def offset(self) -> QtCore.QPoint: return QtCore.QPoint(self.xOffset(), self.__y_offset) def resizeEvent(self, evt: QtGui.QResizeEvent) -> None: super().resizeEvent(evt) self.maximumYOffsetChanged.emit( max(0, self.__content_height - self.height())) self.pageHeightChanged.emit(self.height()) self.__updateTracks() def wheelEvent(self, evt: QtGui.QWheelEvent) -> None: if evt.modifiers() == Qt.ShiftModifier: offset = self.xOffset() offset -= 2 * evt.angleDelta().y() offset = min(self.maximumXOffset(), offset) offset = max(0, offset) self.setXOffset(offset) evt.accept() return elif evt.modifiers() == Qt.ControlModifier: offset = self.yOffset() offset -= evt.angleDelta().y() offset = min(self.maximumYOffset(), offset) offset = max(0, offset) self.setYOffset(offset) evt.accept() return super().wheelEvent(evt)
class TrackHandle(slots.SlotContainer, QtWidgets.QWidget): isCurrent, setIsCurrent, isCurrentChanged = slots.slot(bool, 'isCurrent', default=False) def __init__(self, *, editor: 'Editor', container: 'TrackContainer', **kwargs: Any) -> None: super().__init__(parent=editor, **kwargs) self.__editor = editor self.__container = container self.__click_pos = None # type: QtCore.QPoint self.setCursor(Qt.OpenHandCursor) self.isCurrentChanged.connect(lambda _: self.update()) def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None: self.__editor.setCurrentTrack(self.__container.track) if evt.button() == Qt.LeftButton: self.__click_pos = evt.pos() self.__editor.beginTrackMove(self.__container) self.setCursor(Qt.ClosedHandCursor) evt.accept() def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None: if self.__click_pos is not None: mpos = self.mapTo(self.__editor, evt.pos()).y() if mpos < 20: self.__editor.setAutoScroll(-min(20, (20 - mpos) // 2)) elif mpos > self.__editor.height() - 20: self.__editor.setAutoScroll( min(20, (mpos - self.__editor.height() + 20) // 2)) else: self.__editor.setAutoScroll(0) pos = self.mapTo(self.__editor, evt.pos() - self.__click_pos) self.__editor.moveTrack(pos.y()) evt.accept() def mouseReleaseEvent(self, evt: QtGui.QMouseEvent) -> None: if evt.button() == Qt.LeftButton and self.__click_pos is not None: self.__click_pos = None self.__editor.endTrackMove() self.__editor.setAutoScroll(0) self.setCursor(Qt.OpenHandCursor) evt.accept() def paintEvent(self, evt: QtGui.QPaintEvent) -> None: painter = QtGui.QPainter(self) try: w = self.width() h = self.height() if self.isCurrent(): c_tl = QtGui.QColor(255, 255, 255) c_br = QtGui.QColor(160, 160, 255) c_body = QtGui.QColor(220, 220, 255) else: c_tl = QtGui.QColor(255, 255, 255) c_br = QtGui.QColor(160, 160, 160) c_body = QtGui.QColor(220, 220, 220) painter.fillRect(0, 0, w, 1, c_tl) painter.fillRect(0, 1, 1, h - 1, c_tl) painter.fillRect(1, h - 1, w - 1, 1, c_br) painter.fillRect(w - 1, 1, 1, h - 2, c_br) painter.fillRect(1, 1, w - 2, 1, c_tl) painter.fillRect(1, 2, 1, h - 3, c_tl) painter.fillRect(2, h - 2, w - 3, 1, c_br) painter.fillRect(w - 2, 2, 1, h - 4, c_br) painter.fillRect(2, 2, w - 4, h - 4, c_body) mx = w // 2 my = h // 2 for i in (0, -1, 1, -2, 2): hy1 = my - 3 + 8 * i hy2 = my + 3 + 8 * i if my - 8 * abs(i) >= 13: painter.fillRect(mx - 2, hy1, 1, hy2 - hy1, c_br) painter.fillRect(mx - 1, hy1, 1, hy2 - hy1 - 1, c_br) painter.fillRect(mx - 1, hy2 - 1, 1, 1, c_tl) painter.fillRect(mx, hy1, 1, 1, c_br) painter.fillRect(mx, hy1 + 1, 1, hy2 - hy1 - 1, c_tl) painter.fillRect(mx + 1, hy1, 1, hy2 - hy1, c_tl) finally: painter.end()
class Oscilloscope(slots.SlotContainer, QtWidgets.QWidget): timeScale, setTimeScale, timeScaleChanged = slots.slot(int, 'timeScale', default=-2) yScale, setYScale, yScaleChanged = slots.slot(int, 'yScale', default=0) yOffset, setYOffset, yOffsetChanged = slots.slot(float, 'yOffset', default=0.0) paused, setPaused, pausedChanged = slots.slot(bool, 'paused', default=False) holdTime, setHoldTime, holdTimeChanged = slots.slot(int, 'holdTime', default=0) def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) self.setMinimumSize(20, 20) self.__signal = [] # type: List[SignalPoint] self.__state = State.WAIT_FOR_TRIGGER self.__insert_pos = 0 self.__screen_pos = 0 self.__density = 3 self.__remainder = 0.0 self.__prev_sample = 0.0 self.__hold_begin = 0.0 self.__trigger_begin = 0.0 self.__trigger_found = False self.__timePerPixel = 1.0 self.__timePerSample = 1.0 / 44100 self.__bg_color = QtGui.QColor(0, 0, 0) self.__border_color = QtGui.QColor(100, 200, 100) self.__grid_color = QtGui.QColor(40, 60, 40) self.__center_color = QtGui.QColor(60, 100, 60) self.__plot_pen = QtGui.QPen(QtGui.QColor(255, 255, 255)) self.__plot_pen.setWidth(1) self.__plot_fill_color = QtGui.QColor(200, 255, 200, 100) self.__label_color = QtGui.QColor(100, 200, 100) self.__label_font = QtGui.QFont(self.font()) self.__label_font.setPointSizeF(0.8 * self.__label_font.pointSizeF()) self.__label_font_metrics = QtGui.QFontMetrics(self.__label_font) self.__warning_pen = QtGui.QPen(QtGui.QColor(255, 255, 255)) self.__warning_font = QtGui.QFont(self.font()) self.__warning_font.setPointSizeF(0.9 * self.__warning_font.pointSizeF()) self.__warning_font.setBold(True) self.__warning_font_metrics = QtGui.QFontMetrics(self.__warning_font) self.__warning_pixmap = QtGui.QIcon( os.path.join(constants.DATA_DIR, 'icons', 'warning.svg')).pixmap( 4 + self.__warning_font_metrics.capHeight(), 4 + self.__warning_font_metrics.capHeight()) self.__show_minor_grid = False self.__show_major_grid = False self.__show_y_labels = False self.__show_x_labels = False self.__time_step_size = 100 self.__plot_rect = None # type: QtCore.QRect self.__bg_cache = None # type: QtGui.QPixmap self.__update_timer = QtCore.QTimer(self) self.__update_timer.timeout.connect(self.update) self.__update_timer.setInterval(1000 // 20) self.timeScaleChanged.connect(self.__timeScaleChanged) self.timeScaleChanged.connect(lambda _: self.__invalidateBGCache()) self.yScaleChanged.connect(lambda _: self.__invalidateBGCache()) self.yOffsetChanged.connect(lambda _: self.__invalidateBGCache()) def __setState(self, state: State) -> None: if state == self.__state: return if state == State.RECORDING: self.__insert_pos = 0 self.__screen_pos = 0 self.__remainder = 0.0 elif state == State.HOLD: self.__hold_begin = time.time() elif state == State.WAIT_FOR_TRIGGER: self.__trigger_begin = time.time() self.__state = state def __timeScaleChanged(self, value: int) -> None: if self.__plot_rect is None: return self.__timePerPixel = self.__density * self.absTimeScale( ) / self.__time_step_size if not self.paused(): self.__setState(State.WAIT_FOR_TRIGGER) def absTimeScale(self) -> float: time_scale = self.timeScale() return [1, 2, 5][time_scale % 3] * 10.0**(time_scale // 3) def absYScale(self) -> float: y_scale = self.yScale() return [1, 2, 5][y_scale % 3] * 10.0**(y_scale // 3) def absHoldTime(self) -> float: hold_time = self.holdTime() return [1, 2, 5][hold_time % 3] * 10.0**(hold_time // 3) @classmethod def formatTimeScale(cls, time_scale: int) -> str: mul = [1, 2, 5][time_scale % 3] time_scale //= 3 if time_scale <= -4: return '%dµs' % (mul * 10**(time_scale + 6)) elif time_scale <= -1: return '%dms' % (mul * 10**(time_scale + 3)) else: return '%ds' % (mul * 10**time_scale) @classmethod def formatHoldTime(cls, hold_time: int) -> str: mul = [1, 2, 5][hold_time % 3] hold_time //= 3 if hold_time <= -4: return '%dµs' % (mul * 10**(hold_time + 6)) elif hold_time <= -1: return '%dms' % (mul * 10**(hold_time + 3)) else: return '%ds' % (mul * 10**hold_time) @classmethod def formatYScale(cls, y_scale: int) -> str: return '%g' % ([1, 2, 5][y_scale % 3] * 10.0**(y_scale // 3)) def addValues(self, samples_per_value: int, values: Iterable[float]) -> None: if self.__plot_rect is None: return trigger_value = -self.yOffset() * self.absYScale() for value in values: for _ in range(samples_per_value): if self.__state == State.HOLD and not self.paused(): if time.time() - self.__hold_begin > self.absHoldTime(): self.__setState(State.WAIT_FOR_TRIGGER) if self.__state == State.WAIT_FOR_TRIGGER: if self.__prev_sample < trigger_value and value >= trigger_value: self.__trigger_found = True self.__setState(State.RECORDING) elif time.time( ) - self.__trigger_begin > 10 * self.absTimeScale(): self.__setState(State.RECORDING) self.__trigger_found = False self.__prev_sample = value if self.__state != State.RECORDING: continue if self.__timePerPixel >= self.__timePerSample: self.__remainder += self.__timePerSample if self.__remainder >= 0.0: self.__remainder -= self.__timePerPixel pnt = SignalPoint(self.__screen_pos) pnt.add_sample(value) self.__signal.insert(self.__insert_pos, pnt) self.__insert_pos += 1 while (self.__insert_pos < len(self.__signal) and (self.__signal[self.__insert_pos].screen_pos <= self.__screen_pos)): del self.__signal[self.__insert_pos] self.__screen_pos += self.__density else: pnt = self.__signal[self.__insert_pos - 1] pnt.add_sample(value) else: pnt = SignalPoint(self.__screen_pos) pnt.add_sample(value) self.__signal.insert(self.__insert_pos, pnt) self.__insert_pos += 1 while (self.__insert_pos < len(self.__signal) and self.__signal[self.__insert_pos].screen_pos <= self.__screen_pos): del self.__signal[self.__insert_pos] self.__remainder += self.__timePerSample while self.__remainder >= 0.0: self.__remainder -= self.__timePerPixel self.__screen_pos += self.__density if self.__screen_pos >= self.__plot_rect.width() + 10: self.__setState(State.HOLD) del self.__signal[self.__insert_pos:] def step(self) -> None: if self.paused() and self.__state == State.HOLD: self.__setState(State.WAIT_FOR_TRIGGER) def sizeHint(self) -> QtCore.QSize: return QtCore.QSize(100, 100) def minimumSizeHint(self) -> QtCore.QSize: return QtCore.QSize(60, 60) def resizeEvent(self, evt: QtGui.QResizeEvent) -> None: if evt.size().width() > 20 and evt.size().height() > 20: self.__show_major_grid = True else: self.__show_major_grid = False if evt.size().width() > 100 and evt.size().height() > 100: self.__show_minor_grid = True else: self.__show_minor_grid = False y_label_width = self.__label_font_metrics.boundingRect( '500000').width() + 3 if evt.size().width() >= y_label_width + 100 and evt.size().height( ) >= 60: self.__show_y_labels = True else: self.__show_y_labels = False x_label_height = self.__label_font_metrics.capHeight() + 2 if evt.size().width() >= 100 and evt.size().height( ) >= x_label_height + 100: self.__show_x_labels = True else: self.__show_x_labels = False if evt.size().width() >= 60 and evt.size().height() >= 60: margin = 2 else: margin = 0 border_left = margin border_right = margin border_top = margin border_bottom = margin if self.__show_y_labels: border_left += y_label_width if self.__show_x_labels: border_bottom += x_label_height if (evt.size().width() >= border_left + border_right + 10 and evt.size().height() >= border_top + border_bottom + 10): self.__plot_rect = QtCore.QRect( border_left, border_right, evt.size().width() - border_left - border_right, evt.size().height() - border_top - border_bottom) self.__time_step_size = self.__plot_rect.height() // 2 else: self.__plot_rect = None self.__invalidateBGCache() self.__timeScaleChanged(self.timeScale()) super().resizeEvent(evt) def showEvent(self, evt: QtGui.QShowEvent) -> None: self.__update_timer.start() super().showEvent(evt) def hideEvent(self, evt: QtGui.QHideEvent) -> None: self.__update_timer.stop() self.__invalidateBGCache() super().hideEvent(evt) def __invalidateBGCache(self) -> None: self.__bg_cache = None def __renderBG(self) -> None: w = self.__plot_rect.width() h = self.__plot_rect.height() self.__bg_cache = QtGui.QPixmap(self.size()) painter = QtGui.QPainter(self.__bg_cache) try: painter.fillRect(self.__bg_cache.rect(), self.__bg_color) painter.save() painter.translate(self.__plot_rect.topLeft()) if self.__show_minor_grid: for g in (-4, -3, -2, -1, 1, 2, 3, 4, 6, 7, 8, 9, 11, 12, 13, 14): tick_pos = int((g / 10 - 0.5 * self.yOffset()) * (h - 1)) if not 0 <= tick_pos < h: continue painter.fillRect(0, tick_pos, w, 1, self.__grid_color) x = 0 while x < w: for g in (1, 2, 3, 4): painter.fillRect( x + int(g * self.__time_step_size / 5), 0, 1, h, self.__grid_color) x += self.__time_step_size if self.__show_major_grid: for tick in (-2.0, -1.0, 0.0, 1.0, 2.0): tick_pos = int(0.5 * (1.0 - tick - self.yOffset()) * (h - 1)) if not 0 <= tick_pos < h: continue painter.fillRect(0, tick_pos, w, 1, self.__center_color) x = self.__time_step_size while x < w: painter.fillRect(x, 0, 1, h, self.__center_color) x += self.__time_step_size painter.fillRect(0, 0, w, 1, self.__border_color) painter.fillRect(0, h - 1, w, 1, self.__border_color) painter.fillRect(0, 0, 1, h, self.__border_color) painter.fillRect(w - 1, 0, 1, h, self.__border_color) painter.restore() painter.setFont(self.__label_font) painter.setPen(self.__label_color) if self.__show_x_labels and self.__time_step_size <= w: t1 = self.formatTimeScale(self.timeScale()) t1r = self.__label_font_metrics.boundingRect(t1) painter.drawText( min( self.__plot_rect.left() + self.__time_step_size - t1r.width() // 2, self.__plot_rect.right() - t1r.width()), self.__plot_rect.bottom() + self.__label_font_metrics.capHeight() + 2, t1) if self.__show_y_labels: y_min = self.__plot_rect.top( ) + self.__label_font_metrics.capHeight() y_max = self.__plot_rect.bottom() for tick in (-2.0, -1.0, 0.0, 1.0, 2.0): tick_pos = int(0.5 * (1.0 - tick - self.yOffset()) * (h - 1)) if not 0 <= tick_pos < h: continue painter.fillRect(self.__plot_rect.left() - 3, self.__plot_rect.top() + tick_pos, 3, 1, self.__border_color) y1 = '%g' % (tick * self.absYScale()) y1r = self.__label_font_metrics.boundingRect(y1) label_pos = (self.__plot_rect.top() + tick_pos + self.__label_font_metrics.capHeight() // 2) label_pos = max(y_min, min(y_max, label_pos)) painter.drawText(self.__plot_rect.left() - y1r.width() - 4, label_pos, y1) finally: painter.end() def paintEvent(self, evt: QtGui.QPaintEvent) -> None: painter = QtGui.QPainter(self) try: if self.__bg_cache is None: self.__renderBG() painter.drawPixmap(0, 0, self.__bg_cache) w = self.__plot_rect.width() h = self.__plot_rect.height() painter.setClipRect(self.__plot_rect) painter.translate(self.__plot_rect.topLeft()) y_scale = self.absYScale() y_offset = self.yOffset() min_path = QtGui.QPolygon() max_path = QtGui.QPolygon() for pnt in self.__signal: if pnt.screen_pos >= w: break x = pnt.screen_pos min_value = pnt.min / y_scale + y_offset min_y = int((h - 1) * (1.0 - min_value) / 2.0) min_path.append(QtCore.QPoint(x, min_y)) max_value = pnt.max / y_scale + y_offset max_y = int((h - 1) * (1.0 - max_value) / 2.0) max_path.append(QtCore.QPoint(x, max_y)) if min_y > max_y + 1: painter.fillRect(x, max_y + 1, self.__density, min_y - max_y - 1, self.__plot_fill_color) painter.setPen(self.__plot_pen) painter.drawPolyline(min_path) painter.drawPolyline(max_path) if not self.__trigger_found and h > 5 * self.__warning_font_metrics.capHeight( ): x = (w - self.__warning_font_metrics.boundingRect( "No Trigger").width() - self.__warning_pixmap.width() - 8) if x > 0: painter.drawPixmap(x, 3, self.__warning_pixmap) x += self.__warning_pixmap.width() + 3 painter.setPen(self.__warning_pen) painter.setFont(self.__warning_font) painter.drawText( x, 5 + self.__warning_font_metrics.capHeight(), "No Trigger") finally: painter.end()
class ContinuousTimeMixin(ScaledTimeMixin, slots.SlotContainer): additionalXOffset, setAdditionalXOffset, additionalXOffsetChanged = slots.slot( int, 'additionalXOffset', default=0) snapToGrid, setSnapToGrid, snapToGridChanged = slots.slot(bool, 'snapToGrid', default=True) def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) self.__grid_step = audioproc.MusicalDuration(1, 1) self.scaleXChanged.connect(self.__scaleXChanged) self.__scaleXChanged(self.scaleX()) def __scaleXChanged(self, scale_x: fractions.Fraction) -> None: self.__grid_step = audioproc.MusicalDuration(1, 64) min_dist = 96 while int(self.__grid_step * scale_x) <= min_dist: self.__grid_step *= 2 if int(self.__grid_step) > 1: min_dist = 36 def durationPerPixel(self) -> audioproc.MusicalDuration: return audioproc.MusicalDuration(1 / self.scaleX()) def timeToX(self, time: audioproc.MusicalTime) -> int: return self.leftMargin() + self.additionalXOffset() + int(self.scaleX() * time.fraction) def xToTime(self, x: int) -> audioproc.MusicalTime: x -= self.leftMargin() + self.additionalXOffset() if x <= 0: return audioproc.MusicalTime(0, 1) return audioproc.MusicalTime(x / self.scaleX()) def gridStep(self) -> audioproc.MusicalDuration: return self.__grid_step def shouldSnap(self, evt: QtGui.QMouseEvent) -> bool: return self.snapToGrid() and not evt.modifiers() & Qt.ShiftModifier def snapTime(self, time: audioproc.MusicalTime) -> audioproc.MusicalTime: grid_time = ( audioproc.MusicalTime(0, 1) + self.gridStep() * int(round(float(time / self.gridStep())))) time_x = int(time * self.scaleX()) grid_x = int(grid_time * self.scaleX()) if abs(time_x - grid_x) <= 10: return grid_time return time def renderTimeGrid( self, painter: QtGui.QPainter, rect: QtCore.QRect, *, show_numbers: bool = False ) -> None: grid_step = self.gridStep() tick_num = int(self.xToTime(rect.x()) / grid_step) tick_time = (grid_step * tick_num).as_time() while tick_time < self.projectEndTime(): x = self.timeToX(tick_time) if x > rect.right(): break if tick_num == 0: painter.fillRect(x, rect.y(), 2, rect.height(), Qt.black) else: if tick_time % audioproc.MusicalTime(1, 1) == audioproc.MusicalTime(0, 1): c = QtGui.QColor(0, 0, 0) elif tick_time % audioproc.MusicalTime(1, 4) == audioproc.MusicalTime(0, 1): c = QtGui.QColor(160, 160, 160) elif tick_time % audioproc.MusicalTime(1, 8) == audioproc.MusicalTime(0, 1): c = QtGui.QColor(185, 185, 185) elif tick_time % audioproc.MusicalTime(1, 16) == audioproc.MusicalTime(0, 1): c = QtGui.QColor(210, 210, 210) elif tick_time % audioproc.MusicalTime(1, 32) == audioproc.MusicalTime(0, 1): c = QtGui.QColor(225, 225, 225) else: c = QtGui.QColor(240, 240, 240) painter.fillRect(x, rect.y(), 1, rect.height(), c) if (show_numbers and tick_time % audioproc.MusicalTime(1, 1) == audioproc.MusicalTime(0, 1)): beat_num = int(tick_time / audioproc.MusicalTime(1, 4)) painter.setPen(Qt.black) painter.drawText(x + 5, 12, '%d' % (beat_num + 1)) tick_time += grid_step tick_num += 1 x = self.timeToX(self.projectEndTime()) painter.fillRect(x, rect.y(), 2, rect.height(), Qt.black)
class MeasureEditor(core.AutoCleanupMixin, slots.SlotContainer, BaseMeasureEditor): selected, setSelected, selectedChanged = slots.slot(bool, 'selected', default=False) PLAYBACK_POS = 'playback_pos' layers = None # type: List[str] def __init__(self, measure_reference: music.MeasureReference, **kwargs: Any) -> None: super().__init__(**kwargs) self.__paint_caches = {} # type: Dict[str, QtGui.QPixmap] self.__cached_size = QtCore.QSize() self.__measure_reference = measure_reference self.__measure = self.__measure_reference.measure self.__measure_listener = self.__measure_reference.measure_changed.add( self.__measureChanged) self.__hovered = False self._measure_listeners = core.ListenerList() self.add_cleanup_function(self._measure_listeners.cleanup) if self.__measure is not None: self.addMeasureListeners() self.selectedChanged.connect( lambda _: self.rectChanged.emit(self.viewRect())) self.track_editor.hoveredMeasureChanged.connect( self.__onHoveredMeasureChanged) def cleanup(self) -> None: self.track_editor.hoveredMeasureChanged.disconnect( self.__onHoveredMeasureChanged) super().cleanup() @property def duration(self) -> audioproc.MusicalDuration: return self.__measure.duration @property def measure_reference(self) -> music.MeasureReference: return self.__measure_reference @property def measure(self) -> music.Measure: return self.__measure @property def is_first(self) -> bool: return (self.__measure_reference is not None and self.__measure_reference.index == 0) def addMeasureListeners(self) -> None: raise NotImplementedError def __measureChanged( self, change: music.PropertyValueChange[music.Measure]) -> None: self._measure_listeners.cleanup() self.purgePaintCaches() self.__measure = self.__measure_reference.measure self.addMeasureListeners() self.rectChanged.emit(self.viewRect()) def buildContextMenu(self, menu: QtWidgets.QMenu, pos: QtCore.QPoint) -> None: super().buildContextMenu(menu, pos) insert_measure_action = QtWidgets.QAction("Insert measure", menu) insert_measure_action.setStatusTip( "Insert an empty measure at this point.") insert_measure_action.triggered.connect(self.onInsertMeasure) menu.addAction(insert_measure_action) remove_measure_action = QtWidgets.QAction("Remove measure", menu) remove_measure_action.setStatusTip("Remove this measure.") remove_measure_action.triggered.connect(self.onRemoveMeasure) menu.addAction(remove_measure_action) def onInsertMeasure(self) -> None: with self.project.apply_mutations('%s: Insert measure' % self.track.name): self.track.insert_measure(self.measure_reference.index) def onRemoveMeasure(self) -> None: with self.project.apply_mutations('%s: Remove measure' % self.track.name): self.track.remove_measure(self.measure_reference.index) def __onHoveredMeasureChanged(self, measure_id: int) -> None: hovered = (measure_id == self.measure_reference.measure.id) if hovered != self.__hovered: self.__hovered = hovered self.rectChanged.emit(self.viewRect()) def purgePaintCaches(self) -> None: self.__paint_caches.clear() def invalidatePaintCache(self, *layers: str) -> None: for layer in layers: self.__paint_caches.pop(layer, None) self.rectChanged.emit(self.viewRect()) def paintPlaybackPos(self, painter: QtGui.QPainter) -> None: raise NotImplementedError def paintLayer(self, layer: str, painter: QtGui.QPainter) -> None: raise NotImplementedError def paint(self, painter: QtGui.QPainter, paint_rect: QtCore.QRect) -> None: if self.__cached_size != self.size(): self.__paint_caches.clear() for layer in self.layers: if layer == self.PLAYBACK_POS: continue if layer not in self.__paint_caches: pixmap = QtGui.QPixmap(self.size()) pixmap.fill(Qt.transparent) layer_painter = QtGui.QPainter(pixmap) try: self.paintLayer(layer, layer_painter) finally: layer_painter.end() self.__paint_caches[layer] = pixmap self.__cached_size = self.size() if self.selected(): painter.fillRect(paint_rect, QtGui.QColor(255, 200, 200)) elif self.__hovered: painter.fillRect(paint_rect, QtGui.QColor(220, 220, 255)) for layer in self.layers: if layer == self.PLAYBACK_POS: if self.playbackPos() is not None: self.paintPlaybackPos(painter) else: painter.drawPixmap(0, 0, self.__paint_caches[layer])
class VUMeter(slots.SlotContainer, QtWidgets.QWidget): orientation, setOrientation, orientationChanged = slots.slot( Qt.Orientation, 'orientation', default=Qt.Horizontal) minimum, setMinimum, minimumChanged = slots.slot(float, 'minimum', default=-52.0) maximum, setMaximum, maximumChanged = slots.slot(float, 'maximum', default=22.0) leftValue, setLeftValue, leftValueChanged = slots.slot(float, 'leftValue', default=-50.0) leftPeak, setLeftPeak, leftPeakChanged = slots.slot(float, 'leftPeak', default=-50.0) rightValue, setRightValue, rightValueChanged = slots.slot(float, 'rightValue', default=-50.0) rightPeak, setRightPeak, rightPeakChanged = slots.slot(float, 'rightPeak', default=-50.0) def __init__(self, parent: Optional[QtWidgets.QWidget]) -> None: super().__init__(parent=parent) self.minimumChanged.connect(lambda _: self.update()) self.maximumChanged.connect(lambda _: self.update()) self.leftValueChanged.connect(lambda _: self.update()) self.leftPeakChanged.connect(lambda _: self.update()) self.rightValueChanged.connect(lambda _: self.update()) self.rightPeakChanged.connect(lambda _: self.update()) self.orientationChanged.connect(lambda _: self.update()) def minimumSizeHint(self) -> QtCore.QSize: return QtCore.QSize(20, 20) def normalizeValue(self, value: float) -> float: value = max(self.minimum(), min(value, self.maximum())) value = (value - self.minimum()) / (self.maximum() - self.minimum()) return value def __drawHBar(self, painter: QtGui.QPainter, rect: QtCore.QRect, value: float, peak: float, ticks: List[Tuple[float, str]]) -> None: value = self.normalizeValue(value) peak = self.normalizeValue(peak) x, y = rect.left(), rect.top() w, h = rect.width(), rect.height() if value > 0.0: value_x = int(w * value) warn_x = int(w * self.normalizeValue(-9.0)) clip_x = int(w * self.normalizeValue(0.0)) painter.fillRect(QtCore.QRect(x, y, min(value_x, warn_x), h), Qt.green) if value_x > warn_x: painter.fillRect( QtCore.QRect(x + warn_x, y, min(value_x, clip_x) - warn_x, h), Qt.yellow) if value_x > clip_x: painter.fillRect( QtCore.QRect(x + clip_x, y, value_x - clip_x, h), Qt.red) if peak > 0.0: peak_x = int((w - 1) * peak) painter.fillRect(QtCore.QRect(x + peak_x - 1, y, 2, h), Qt.red) tick_h = int(h / 5) for tick, _ in ticks: tick_x = int(w * self.normalizeValue(tick)) painter.fillRect(QtCore.QRect(x + tick_x, y, 1, tick_h), Qt.white) painter.fillRect( QtCore.QRect(x + tick_x, y + h - tick_h, 1, tick_h), Qt.white) def __drawVBar(self, painter: QtGui.QPainter, rect: QtCore.QRect, value: float, peak: float, ticks: List[Tuple[float, str]]) -> None: value = self.normalizeValue(value) peak = self.normalizeValue(peak) x, y = rect.left(), rect.top() w, h = rect.width(), rect.height() if value > 0.0: value_y = int(h * value) warn_y = int(h * self.normalizeValue(-9.0)) clip_y = int(h * self.normalizeValue(0.0)) painter.fillRect( QtCore.QRect(x, y + h - min(value_y, warn_y), w, min(value_y, warn_y)), Qt.green) if value_y > warn_y: painter.fillRect( QtCore.QRect(x, y + h - min(value_y, clip_y), w, min(value_y, clip_y) - warn_y), Qt.yellow) if value_y > clip_y: painter.fillRect( QtCore.QRect(x, y + h - value_y, w, value_y - clip_y), Qt.red) if peak > 0.0: peak_y = int(int((h - 1) * peak)) painter.fillRect(QtCore.QRect(x, y + h - peak_y - 1, w, 2), Qt.red) tick_w = int(w / 5) for tick, _ in ticks: tick_y = int(h * self.normalizeValue(tick)) painter.fillRect(QtCore.QRect(x, y + tick_y, tick_w, 1), Qt.white) painter.fillRect( QtCore.QRect(x + w - tick_w, y + tick_y, tick_w, 1), Qt.white) def paintEvent(self, evt: QtGui.QPaintEvent) -> None: w, h = self.width(), self.height() painter = QtGui.QPainter(self) try: painter.fillRect(0, 0, w, h, Qt.black) font = QtGui.QFont("Arial") font.setPixelSize(12) painter.setFont(font) fm = QtGui.QFontMetrics(font) ticks = [] tick = math.modf(self.minimum() / 10.0)[1] * 10.0 while tick <= self.maximum(): label = '%+.0f' % tick if label == '+0': label = '0dB' ticks.append((tick, label)) tick += 10.0 if self.orientation() == Qt.Horizontal: if h > 60: label_h = max(14, min(h / 4, 30)) bar_h = int((h - 12 - label_h) / 2) else: label_h = 0 bar_h = int((h - 6) / 2) label_top = int((h - 14) / 2) label_bottom = label_top + 14 for tick, label in ticks: tick_x = int((w - 4) * self.normalizeValue(tick)) painter.fillRect(2 + tick_x, 2, 1, label_top - 2, QtGui.QColor(60, 60, 60)) painter.fillRect(2 + tick_x, 2 + label_bottom, 1, h - label_bottom - 4, QtGui.QColor(60, 60, 60)) if label_h > 0: pen = QtGui.QPen() pen.setColor(Qt.white) painter.setPen(pen) r = fm.boundingRect(label) painter.drawText( QtCore.QPoint( int(tick_x - r.width() / 2), int((h - r.height()) / 2 + 1.5 * fm.capHeight())), label) self.__drawHBar(painter, QtCore.QRect(2, 2, w - 4, bar_h), self.leftValue(), self.leftPeak(), ticks) self.__drawHBar(painter, QtCore.QRect(2, h - bar_h - 2, w - 4, bar_h), self.rightValue(), self.rightPeak(), ticks) else: if w > 80: label_w = max(30, min(w / 3, 50)) bar_w = int((w - 12 - label_w) / 2) else: label_w = 0 bar_w = int((w - 6) / 2) label_left = int((w - 30) / 2) label_right = label_left + 30 for tick, label in ticks: tick_y = h - 2 - int((h - 4) * self.normalizeValue(tick)) painter.fillRect(2, tick_y, label_left - 2, 1, QtGui.QColor(60, 60, 60)) painter.fillRect(2 + label_right, tick_y, w - label_right - 4, 1, QtGui.QColor(60, 60, 60)) if label_w > 0: pen = QtGui.QPen() pen.setColor(Qt.white) painter.setPen(pen) fm = QtGui.QFontMetrics(font) r = fm.boundingRect(label) painter.drawText( QtCore.QPoint(int((w - r.width()) / 2), int(tick_y + 0.5 * fm.capHeight())), label) self.__drawVBar(painter, QtCore.QRect(2, 2, bar_w, h - 4), self.leftValue(), self.leftPeak(), ticks) self.__drawVBar(painter, QtCore.QRect(w - bar_w - 2, 2, bar_w, h - 4), self.rightValue(), self.rightPeak(), ticks) finally: painter.end()