Exemple #1
0
class BasicSongCard(QWidget):
    """ 歌曲卡基类 """

    clicked = pyqtSignal(int)
    doubleClicked = pyqtSignal(int)
    playButtonClicked = pyqtSignal(int)
    addSongToPlayingSig = pyqtSignal(dict)
    checkedStateChanged = pyqtSignal(int, bool)
    addSongsToCustomPlaylistSig = pyqtSignal(str, list)

    def __init__(self, songInfo: dict, songCardType, parent=None):
        """ 实例化歌曲卡

        Parameters
        ----------
        songInfo: dict
            歌曲信息字典

        songCardType: `~SongCardType`
            歌曲卡类型

        parent:
            父级
        """
        super().__init__(parent)
        self._getInfo(songInfo)
        self.__resizeTime = 0
        self.__songCardType = songCardType
        # 歌曲卡类型
        self.__SongNameCard = [SongTabSongNameCard,
                               TrackNumSongNameCard][songCardType.value]
        # 初始化各标志位
        self.isSongExist = True
        self.isPlaying = False
        self.isSelected = False
        self.isChecked = False
        self.isInSelectionMode = False
        self.isDoubleClicked = False
        # 记录songCard对应的item的下标
        self.itemIndex = None
        # 创建小部件
        if self.__songCardType == SongCardType.SONG_TAB_SONG_CARD:
            self.songNameCard = self.__SongNameCard(self.songName, self)
        elif self.__songCardType == SongCardType.ALBUM_INTERFACE_SONG_CARD:
            self.songNameCard = self.__SongNameCard(self.songName,
                                                    self.tracknumber, self)
        self.__referenceWidgets()
        # 初始化小部件列表
        self.__scaleableLabelTextWidth_list = []  # 可拉伸的标签的文本的宽度列表
        self.__scaleableWidgetMaxWidth_list = []  # 可拉伸部件的最大宽度列表
        self.__dynamicStyleLabel_list = []
        self.__scaleableWidget_list = []
        self.__clickableLabel_list = []
        self.__labelSpacing_list = []
        self.__label_list = []
        self.__widget_list = []  # 存放所有的小部件
        # 创建动画组和动画列表
        self.aniGroup = QParallelAnimationGroup(self)
        self.__aniWidget_list = []
        self.__deltaX_list = []
        self.ani_list = []
        # 安装事件过滤器
        self.installEventFilter(self)
        # 信号连接到槽
        self.playButton.clicked.connect(self.playButtonSlot)
        self.addToButton.clicked.connect(self.__showAddToMenu)
        self.checkBox.stateChanged.connect(self.checkedStateChangedSlot)

    def _getInfo(self, songInfo: dict):
        """ 从歌曲信息字典中获取信息 """
        self.songInfo = songInfo
        self.songPath = songInfo.get("songPath", "")  # type:str
        self.songName = songInfo.get("songName", "未知歌曲")  # type:str
        self.songer = songInfo.get("songer", "未知歌手")  # type:str
        self.album = songInfo.get("album", "未知专辑")  # type:str
        self.year = songInfo.get("year", "未知年份")  # type:str
        self.tcon = songInfo.get("tcon", "未知流派")  # type:str
        self.duration = songInfo.get("duration", "0:00")  # type:str
        self.tracknumber = songInfo.get("tracknumber", "0")  # type:str

    def setScalableWidgets(self,
                           scaleableWidget_list: list,
                           scalebaleWidgetWidth_list: list,
                           fixedWidth=0):
        """ 设置可随着歌曲卡的伸缩而伸缩的标签

        Parameters
        ----------
        scaleableWidget_list : 随着歌曲卡的伸缩而伸缩的小部件列表,要求第一个元素为歌名卡,后面的元素都是label
        scaleableWidgetWidth_list : 与可伸缩小部件相对应的小部件初始长度列表
        fixedWidth : 为其他不可拉伸的小部件保留的宽度 """
        if self.__scaleableWidgetMaxWidth_list:
            return
        self.__checkIsLengthEqual(scaleableWidget_list,
                                  scalebaleWidgetWidth_list)
        # 必须先将所有标签添加到列表中后才能调用这个函数
        if not self.__label_list:
            raise Exception("必须先调用addLabels函数将标签添加到窗口中")
        # 歌名卡默认可拉伸
        self.__scaleableWidget_list = scaleableWidget_list
        self.__scaleableWidgetMaxWidth_list = scalebaleWidgetWidth_list
        # 计算初始宽度
        initWidth = (sum(self.__scaleableWidgetMaxWidth_list) +
                     sum(self.__labelSpacing_list) + fixedWidth)
        self.resize(initWidth, 60)
        self.setFixedHeight(60)

    def addLabels(self, label_list: list, labelSpacing_list: list):
        """ 往歌曲卡中添加除了歌曲名卡之外的标签,只能初始化一次标签列表

        Paramerter
        ----------
        label_list : 歌名卡后的标签列表
        labelSpacing_list : 每个标签的前置空白 """
        if self.__label_list:
            return
        self.__checkIsLengthEqual(label_list, labelSpacing_list)
        self.__label_list = label_list
        self.__labelSpacing_list = labelSpacing_list
        self.__widget_list = [self.songNameCard] + self.__label_list
        # 移动小部件
        for i in range(len(label_list)):
            label_list[i]

    def setDynamicStyleLabels(self, label_list: list):
        """ 设置需要动态更新样式的标签列表 """
        self.__dynamicStyleLabel_list = label_list

    def setClickableLabels(self, clickableLabel_list: list):
        """ 设置可点击的标签列表 """
        self.__clickableLabel_list = clickableLabel_list
        # 分配ID
        for label in self.__clickableLabel_list:
            label.setObjectName("clickableLabel")

    def setSelected(self, isSelected: bool):
        """ 设置选中状态 """
        self.isSelected = isSelected
        if isSelected:
            self.setWidgetState("selected-leave")
            self.setCheckBoxBtLabelState("selected")
        else:
            self.songNameCard.setWidgetHidden(True)
            self.setWidgetState("notSelected-leave")
            state = "notSelected-play" if self.isPlaying else "notSelected-notPlay"
            self.setCheckBoxBtLabelState(state)
        self.setStyle(QApplication.style())

    def setPlay(self, isPlay: bool):
        """ 设置播放状态并更新样式 """
        self.isPlaying = isPlay
        self.isSelected = isPlay
        # 判断歌曲文件是否存在
        self.isSongExist = os.path.exists(self.songPath)
        if isPlay:
            self.isSelected = True
            self.setCheckBoxBtLabelState("selected")
            self.setWidgetState("selected-leave")
        else:
            self.setCheckBoxBtLabelState("notSelected-notPlay")
            self.setWidgetState("notSelected-leave")
        self.songNameCard.setPlay(isPlay, self.isSongExist)
        self.setStyle(QApplication.style())

    def setCheckBoxBtLabelState(self, state: str):
        """ 设置复选框、按钮和标签动态属性

        Parameters
        ----------
        state: str
            复选框、按钮和标签的状态,可以是:
            * `notSelected-notPlay`
            * `notSelected-play`
            * `selected`
        """
        self.songNameCard.setCheckBoxBtLabelState(state, self.isSongExist)
        for label in self.__dynamicStyleLabel_list:
            label.setProperty("state", state)

    def setWidgetState(self, state: str):
        """ 设置按钮组窗口和自己的状态

        Parameters
        ----------
        state: str
            窗口状态,可以是:
            * `notSelected-leave`
            * `notSelected-enter`
            * `notSelected-pressed`
            * `selected-leave`
            * `selected-enter`
            * `selected-pressed`
        """
        self.songNameCard.setButtonGroupState(state)
        self.setProperty("state", state)

    def setAnimation(self, aniWidget_list: list, deltaX_list: list):
        """ 设置小部件的动画

        Parameters
        ----------
        aniWidget_list : 需要设置动画的小部件列表
        deltaX_list : 和aniWidget_list相对应的动画位置偏移量列表 """
        self.__checkIsLengthEqual(aniWidget_list, deltaX_list)
        self.__aniWidget_list = aniWidget_list
        self.__deltaX_list = deltaX_list
        # 清空动画组的内容
        self.ani_list.clear()
        self.aniGroup.clear()
        self.__ani_list = [
            QPropertyAnimation(widget, b"geometry")
            for widget in self.__aniWidget_list
        ]
        # 初始化动画
        for ani in self.__ani_list:
            ani.setDuration(400)
            ani.setEasingCurve(QEasingCurve.OutQuad)
            self.aniGroup.addAnimation(ani)
        self.getAniTargetX_list()

    def getAniTargetX_list(self):
        """ 计算动画的初始值 """
        self.__aniTargetX_list = []
        for deltaX, widget in zip(self.__deltaX_list, self.__aniWidget_list):
            self.__aniTargetX_list.append(deltaX + widget.x())

    def eventFilter(self, obj, e: QEvent):
        """ 安装监听 """
        if obj == self:
            if e.type() == QEvent.Enter:
                self.songNameCard.checkBox.show()
                self.songNameCard.buttonGroup.setHidden(self.isInSelectionMode)
                state = "selected-enter" if self.isSelected else "notSelected-enter"
                self.setWidgetState(state)
                self.setStyle(QApplication.style())
            elif e.type() == QEvent.Leave:
                # 不处于选择模式下时,如果歌曲卡没被选中而鼠标离开窗口就隐藏复选框和按钮组窗口
                if not self.isSelected:
                    self.songNameCard.buttonGroup.hide()
                    self.songNameCard.checkBox.setHidden(
                        not self.isInSelectionMode)
                state = "selected-leave" if self.isSelected else "notSelected-leave"
                self.setWidgetState(state)
                self.setStyle(QApplication.style())
            elif e.type() == QEvent.MouseButtonPress:
                state = "selected-pressed" if self.isSelected else "notSelected-pressed"
                if e.button() == Qt.LeftButton:
                    self.isSelected = True
                self.setWidgetState(state)
                self.setStyle(QApplication.style())
            elif e.type() == QEvent.MouseButtonRelease and e.button(
            ) == Qt.LeftButton:
                self.setWidgetState("selected-leave")
                self.setCheckBoxBtLabelState("selected")  # 鼠标松开时将设置标签为白色
                self.setStyle(QApplication.style())
            elif e.type() == QEvent.MouseButtonDblClick:
                self.isDoubleClicked = True
        return super().eventFilter(obj, e)

    def mousePressEvent(self, e):
        """ 鼠标按下时移动小部件 """
        super().mousePressEvent(e)
        # 移动小部件
        if self.aniGroup.state() == QAbstractAnimation.Stopped:
            for deltaX, widget in zip(self.__deltaX_list,
                                      self.__aniWidget_list):
                widget.move(widget.x() + deltaX, widget.y())
        else:
            self.aniGroup.stop()  # 强制停止还未结束的动画
            for targetX, widget in zip(self.__aniTargetX_list,
                                       self.__aniWidget_list):
                widget.move(targetX, widget.y())

    def mouseReleaseEvent(self, e: QMouseEvent):
        """ 鼠标松开时开始动画 """
        for ani, widget, deltaX in zip(self.__ani_list, self.__aniWidget_list,
                                       self.__deltaX_list):
            ani.setStartValue(
                QRect(widget.x(), widget.y(), widget.width(), widget.height()))
            ani.setEndValue(
                QRect(widget.x() - deltaX, widget.y(), widget.width(),
                      widget.height()))
        self.aniGroup.start()
        if e.button() == Qt.LeftButton:
            self.clicked.emit(self.itemIndex)
        # 左键点击时才发送信号
        if self.isDoubleClicked and e.button() == Qt.LeftButton:
            self.isDoubleClicked = False
            if not self.isPlaying:
                # 发送点击信号
                self.aniGroup.finished.connect(self.__aniFinishedSlot)

    def __aniFinishedSlot(self):
        """ 动画完成时发出双击信号 """
        self.doubleClicked.emit(self.itemIndex)
        self.aniGroup.disconnect()

    def setClickableLabelCursor(self, cursor):
        """ 设置可点击标签的光标样式 """
        for label in self.__clickableLabel_list:
            label.setCursor(cursor)

    def __referenceWidgets(self):
        """ 引用小部件 """
        self.buttonGroup = self.songNameCard.buttonGroup
        self.playButton = self.songNameCard.playButton
        self.addToButton = self.songNameCard.addToButton
        self.checkBox = self.songNameCard.checkBox

    def playButtonSlot(self):
        """ 播放按钮按下时更新样式 """
        self.playButtonClicked.emit(self.itemIndex)

    def checkedStateChangedSlot(self):
        """ 复选框选中状态改变对应的槽函数 """
        self.isChecked = self.checkBox.isChecked()
        self.setSelected(self.isChecked)
        # 只要点击了复选框就进入选择模式,由父级控制退出选择模式
        self.checkBox.show()
        self.setSelectionModeOpen(True)
        # 发出选中状态改变信号
        self.checkedStateChanged.emit(self.itemIndex, self.isChecked)

    def setSelectionModeOpen(self, isOpenSelectionMode: bool):
        """ 设置是否进入选择模式, 处于选择模式下复选框一直可见,按钮不管是否处于选择模式都不可见 """
        if self.isInSelectionMode == isOpenSelectionMode:
            return
        # 更新标志位
        self.isInSelectionMode = isOpenSelectionMode
        # 设置按钮和复选框的可见性
        self.checkBox.setHidden(not isOpenSelectionMode)
        self.buttonGroup.setHidden(True)

    def setChecked(self, isChecked: bool):
        """ 设置歌曲卡选中状态 """
        self.checkBox.setChecked(isChecked)

    def updateSongCard(self):
        """ 更新歌曲卡 """
        # 必须被子类重写
        raise NotImplementedError

    def __checkIsLengthEqual(self, list_1: list, list_2: list):
        """ 检查输入的两个列表的长度是否相等,不相等则引发错误 """
        if len(list_1) != len(list_2):
            raise Exception("两个列表的长度必须一样")

    def resizeEvent(self, e):
        """ 改变窗口大小时移动标签 """
        self.__resizeTime += 1
        if self.__resizeTime > 1:
            # 分配多出来的宽度
            deltaWidth = self.width() - self.__originalWidth
            self.__originalWidth = self.width()
            equalWidth = int(deltaWidth / len(self.__scaleableWidget_list))
            self.__scaleableWidgetMaxWidth_list = [
                i + equalWidth for i in self.__scaleableWidgetMaxWidth_list
            ]
        else:
            self.__originalWidth = self.width()
        # 调整小部件宽度
        self.adjustWidgetWidth()
        # 移动标签
        x = self.songNameCard.width()
        for label, spacing in zip(self.__label_list, self.__labelSpacing_list):
            # 如果标签是可变长度的,就将width设置为其最大可变宽度
            if label in self.__scaleableWidget_list:
                index = self.__scaleableWidget_list.index(label)
                width = self.__scaleableWidgetMaxWidth_list[index]
            else:
                width = label.width()
            label.move(x + spacing, 20)
            x = width + label.x()
        # 更新动画目标移动位置
        self.getAniTargetX_list()

    def adjustWidgetWidth(self):
        """ 调整小部件宽度 """
        # 计算标签的宽度
        self.__getScaleableLabelTextWidth()
        self.songNameCard.resize(self.__scaleableWidgetMaxWidth_list[0], 60)
        for i in range(1, len(self.__scaleableWidget_list)):
            label = self.__scaleableWidget_list[i]
            textWidth = self.__scaleableLabelTextWidth_list[i - 1]
            maxLabelWidth = self.__scaleableWidgetMaxWidth_list[i]
            width = maxLabelWidth if textWidth > maxLabelWidth else textWidth
            label.setFixedWidth(width)

    def __getScaleableLabelTextWidth(self):
        """ 计算可拉伸的标签的文本宽度 """
        fontMetrics = QFontMetrics(QFont("Microsoft YaHei", 9))
        self.__scaleableLabelTextWidth_list = [
            fontMetrics.width(label.text())
            for label in self.__scaleableWidget_list[1:]
        ]

    def __showAddToMenu(self):
        """ 显示添加到菜单 """
        addToMenu = AddToMenu(parent=self)
        addToGlobalPos = self.mapToGlobal(QPoint(0, 0)) + QPoint(
            self.addToButton.x() + self.buttonGroup.x(), 0)
        x = addToGlobalPos.x() + self.addToButton.width() + 5
        y = addToGlobalPos.y() + int(self.addToButton.height() / 2 -
                                     (13 + 38 * addToMenu.actionCount()) / 2)
        addToMenu.playingAct.triggered.connect(
            lambda: self.addSongToPlayingSig.emit(self.songInfo))
        addToMenu.addSongsToPlaylistSig.connect(
            lambda name: self.addSongsToCustomPlaylistSig.emit(
                name, [self.songInfo]))
        addToMenu.exec(QPoint(x, y))

    @property
    def widget_list(self) -> list:
        """ 返回窗口内的所有小部件组成的列表 """
        return self.__widget_list

    @property
    def label_list(self) -> list:
        """ 返回窗口内所有标签组成的列表 """
        return self.__label_list