Exemple #1
0
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)
Exemple #2
0
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)
Exemple #3
0
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)
Exemple #4
0
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)
Exemple #5
0
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())
Exemple #6
0
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())
Exemple #7
0
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)
Exemple #8
0
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)
Exemple #9
0
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)
Exemple #11
0
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)
Exemple #12
0
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()
Exemple #13
0
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()
Exemple #14
0
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])
Exemple #16
0
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()