diff --git a/feeluown/gui/page_containers/table.py b/feeluown/gui/page_containers/table.py index 90bdd42cd6..942e07d37e 100644 --- a/feeluown/gui/page_containers/table.py +++ b/feeluown/gui/page_containers/table.py @@ -18,15 +18,13 @@ from feeluown.gui.helpers import BgTransparentMixin, \ disconnect_slots_if_has, fetch_cover_wrapper from feeluown.gui.components import SongMenuInitializer -from feeluown.gui.widgets.imglist import ImgListView -from feeluown.gui.widgets.album import AlbumListModel, AlbumListView, \ - AlbumFilterProxyModel -from feeluown.gui.widgets.artist import ArtistListModel, ArtistListView, \ - ArtistFilterProxyModel -from feeluown.gui.widgets.video_list import VideoListModel, VideoListView, \ - VideoFilterProxyModel -from feeluown.gui.widgets.playlist import PlaylistListModel, PlaylistListView, \ - PlaylistFilterProxyModel +from feeluown.gui.widgets.img_card_list import ImgCardListView +from feeluown.gui.widgets.img_card_list import ( + AlbumCardListModel, AlbumCardListView, AlbumFilterProxyModel, AlbumCardListDelegate, + ArtistCardListModel, ArtistCardListView, ArtistFilterProxyModel, ArtistCardListDelegate, + VideoCardListModel, VideoCardListView, VideoFilterProxyModel, VideoCardListDelegate, + PlaylistCardListModel, PlaylistCardListView, PlaylistFilterProxyModel, PlaylistCardListDelegate, +) from feeluown.gui.widgets.songs import ColumnsMode, SongsTableModel, SongsTableView, \ SongFilterProxyModel from feeluown.gui.widgets.comment_list import CommentListView, CommentListModel @@ -93,25 +91,25 @@ async def show_cover(self, cover, cover_uid, as_background=False): def show_albums(self, reader): self._show_model_with_cover(reader, self.albums_table, - AlbumListModel, + AlbumCardListModel, AlbumFilterProxyModel) def show_artists(self, reader): self._show_model_with_cover(reader, self.artists_table, - ArtistListModel, + ArtistCardListModel, ArtistFilterProxyModel) def show_videos(self, reader): self._show_model_with_cover(reader, self.videos_table, - VideoListModel, + VideoCardListModel, VideoFilterProxyModel) def show_playlists(self, reader): self._show_model_with_cover(reader, self.playlists_table, - PlaylistListModel, + PlaylistCardListModel, PlaylistFilterProxyModel) def _show_model_with_cover(self, reader, table, model_cls, filter_model_cls): @@ -257,10 +255,15 @@ def __init__(self, app, parent=None): self.tabbar = TableTabBarV2() self.meta_widget = TableMetaWidget(parent=self) self.songs_table = SongsTableView(app=self._app, parent=self) - self.albums_table = AlbumListView(parent=self, img_min_width=120) - self.artists_table = ArtistListView(parent=self) - self.videos_table = VideoListView(parent=self) - self.playlists_table = PlaylistListView(parent=self) + self.albums_table = AlbumCardListView(parent=self) + self.albums_table.setItemDelegate( + AlbumCardListDelegate(self.albums_table, img_min_width=120)) + self.artists_table = ArtistCardListView(parent=self) + self.artists_table.setItemDelegate(ArtistCardListDelegate(self.artists_table)) + self.videos_table = VideoCardListView(parent=self) + self.videos_table.setItemDelegate(VideoCardListDelegate(self.videos_table)) + self.playlists_table = PlaylistCardListView(parent=self) + self.playlists_table.setItemDelegate(PlaylistCardListDelegate(self.playlists_table)) self.comments_table = CommentListView(parent=self) self.desc_widget = DescLabel(parent=self) @@ -361,7 +364,7 @@ def current_table(self, table): self.toolbar.albums_mode() if table is self.songs_table: self.toolbar.songs_mode() - if isinstance(self._table, ImgListView): + if isinstance(self._table, ImgCardListView): self._table.setModel(None) self._table = table diff --git a/feeluown/gui/pages/search.py b/feeluown/gui/pages/search.py index dc2bdf5780..4ce2c88f25 100644 --- a/feeluown/gui/pages/search.py +++ b/feeluown/gui/pages/search.py @@ -3,7 +3,7 @@ from feeluown.models import SearchType from feeluown.gui.page_containers.table import TableContainer, Renderer from feeluown.gui.page_containers.scroll_area import ScrollArea -from feeluown.gui.widgets.imglist import ImgListView +from feeluown.gui.widgets.img_card_list import ImgCardListDelegate from feeluown.gui.widgets.songs import SongsTableView, ColumnsMode from feeluown.gui.base_renderer import TabBarRendererMixin from feeluown.gui.helpers import BgTransparentMixin @@ -58,9 +58,9 @@ async def render(req, **kwargs): # pylint: disable=too-many-locals,too-many-bra # HACK: set fixed row for tables. # pylint: disable=protected-access for table in table_container._tables: - if isinstance(table, ImgListView): + if isinstance(table.itemDelegate(), ImgCardListDelegate): table._fixed_row_count = 2 - table.img_min_width = 100 + table.itemDelegate().img_min_width = 100 elif isinstance(table, SongsTableView): table._fixed_row_count = 8 table._row_height = table.verticalHeader().defaultSectionSize() diff --git a/feeluown/gui/widgets/album.py b/feeluown/gui/widgets/album.py deleted file mode 100644 index 7443dab6b5..0000000000 --- a/feeluown/gui/widgets/album.py +++ /dev/null @@ -1,71 +0,0 @@ -from PyQt5.QtCore import Qt, pyqtSignal - -from feeluown.library import AlbumModel -from feeluown.models import AlbumType -from .imglist import ( - ImgListModel, ImgListDelegate, ImgListView, - ImgFilterProxyModel -) - - -class AlbumListModel(ImgListModel): - def data(self, index, role): - offset = index.row() - if not index.isValid() or offset >= len(self._items): - return None - - album = self._items[offset] - if role == Qt.WhatsThisRole: - if isinstance(album, AlbumModel): - if album.song_count >= 0: - # Like: 1991-01-01 10首 - return f'{album.released} {album.song_count}首' - return album.released - return super().data(index, role) - - -class AlbumListDelegate(ImgListDelegate): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.as_circle = False - - -class AlbumFilterProxyModel(ImgFilterProxyModel): - def __init__(self, parent=None, types=None): - super().__init__(parent) - - self.types = types - - def filter_by_types(self, types): - # if types is a empty list or None, we show all albums - if not types: - types = None - self.types = types - self.invalidateFilter() - - def filterAcceptsRow(self, source_row, source_parent): - accepted = super().filterAcceptsRow(source_row, source_parent) - source_model = self.sourceModel() - assert isinstance(source_model, AlbumListModel) - index = source_model.index(source_row, parent=source_parent) - album = index.data(Qt.UserRole) - if accepted and self.types: - accepted = AlbumType(album.type_) in self.types - return accepted - - -class AlbumListView(ImgListView): - show_album_needed = pyqtSignal([object]) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - delegate = AlbumListDelegate(self) - self.setItemDelegate(delegate) - - self.activated.connect(self._on_activated) - - def _on_activated(self, index): - album = index.data(Qt.UserRole) - self.show_album_needed.emit(album) diff --git a/feeluown/gui/widgets/artist.py b/feeluown/gui/widgets/artist.py deleted file mode 100644 index 9e1ea766d9..0000000000 --- a/feeluown/gui/widgets/artist.py +++ /dev/null @@ -1,37 +0,0 @@ -from PyQt5.QtCore import Qt, pyqtSignal - -from .imglist import ( - ImgListModel, ImgListDelegate, ImgListView, - ImgFilterProxyModel -) - - -class ArtistListModel(ImgListModel): - pass - - -class ArtistListDelegate(ImgListDelegate): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.as_circle = True - - -class ArtistFilterProxyModel(ImgFilterProxyModel): - pass - - -class ArtistListView(ImgListView): - show_artist_needed = pyqtSignal([object]) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - delegate = ArtistListDelegate(self) - self.setItemDelegate(delegate) - - self.activated.connect(self._on_activated) - - def _on_activated(self, index): - artist = index.data(Qt.UserRole) - self.show_artist_needed.emit(artist) diff --git a/feeluown/gui/widgets/imglist.py b/feeluown/gui/widgets/img_card_list.py similarity index 64% rename from feeluown/gui/widgets/imglist.py rename to feeluown/gui/widgets/img_card_list.py index 6f91b5138c..2ac7223ee6 100644 --- a/feeluown/gui/widgets/imglist.py +++ b/feeluown/gui/widgets/img_card_list.py @@ -1,8 +1,8 @@ """ ImgList model and delegate -- ImgListDelegate -- ImgListModel +- ImgCardListDelegate +- ImgCardListModel By default, we think the proper width of a cover is about 160px, the margin between two cover should be about 20px. When the view is @@ -15,11 +15,11 @@ from typing import TypeVar, Optional, List, cast from PyQt5.QtCore import ( - QAbstractListModel, QModelIndex, Qt, - QRectF, QRect, QSize, QSortFilterProxyModel + QAbstractListModel, QModelIndex, Qt, QObject, QEvent, + QRectF, QRect, QSize, QSortFilterProxyModel, pyqtSignal ) from PyQt5.QtGui import ( - QImage, QColor, + QImage, QColor, QResizeEvent, QBrush, QPainter, QTextOption, QFontMetrics, QPalette ) from PyQt5.QtWidgets import ( @@ -27,6 +27,7 @@ ) from feeluown.utils import aio +from feeluown.library import AlbumModel, AlbumType from feeluown.utils.reader import wrap from feeluown.models.uri import reverse from feeluown.gui.helpers import ( @@ -49,7 +50,7 @@ } -class ImgListModel(QAbstractListModel, ReaderFetchMoreMixin[T]): +class ImgCardListModel(QAbstractListModel, ReaderFetchMoreMixin[T]): def __init__(self, reader, fetch_image, source_name_map=None, parent=None): """ @@ -126,22 +127,30 @@ def data(self, index, role): return None -class ImgListDelegate(QAbstractItemDelegate): - def __init__(self, parent=None): +class ImgCardListDelegate(QAbstractItemDelegate): + def __init__(self, parent=None, + img_min_width=150, img_spacing=20, img_text_height=40, + **_): super().__init__(parent) - self.view: 'ImgListView' = parent - # TODO: move as_circle/w_h_ratio attribute to view + self.view: 'ImgCardListView' = parent + self.view.installEventFilter(self) self.as_circle = True self.w_h_ratio = 1.0 - self.spacing = self.view.img_spacing + self.img_min_width = img_min_width + self.img_spacing = img_spacing + self.img_text_height = img_text_height + + self.spacing = self.img_spacing self.half_spacing = self.spacing // 2 - self.text_height = self.view.img_text_height + self.text_height = self.img_text_height + + self._img_width = self._img_height = 0 + self._view_width = 0 def column_count(self): - return ((self.view.width() + self.view.img_spacing) // - (self.view.img_sizehint()[0] + self.view.img_spacing)) + return (self._view_width + self.img_spacing) // (self._img_width + self.img_spacing) def is_left_first(self, index): if self.view.isWrapping(): @@ -259,12 +268,43 @@ def draw_whats_this(self, painter, index, non_text_color, whats_this_rect): def sizeHint(self, option, index): spacing = self.get_spacing(index) - width = self.view.img_sizehint()[0] + width = self._img_width if index.isValid(): height = int(width / self.w_h_ratio) + self.text_height return QSize(width + spacing, height + self.spacing) return super().sizeHint(option, index) + def on_view_resized(self, size: QSize, old_size: QSize): + self._view_width = size.width() + self._img_width, self._img_height = self.re_calc_img_size() + self.view._row_height = self._img_height + + def re_calc_img_size(self): + # HELP: CardListView needs about 20 spacing left on macOS + width = max(0, self._view_width - 20) + img_spacing = self.img_spacing + img_min_width = self.img_min_width + img_text_height = self.img_text_height + + # according to our algorithm, when the widget width is: + # 2(img_min_width + img_spacing) + img_spacing - 1, + # the cover width can take the maximum width, it will be: + # CoverMaxWidth = 2 * img_min_width + img_spacing - 1 + + # calculate max column count + count = (width + img_spacing) // (img_min_width + img_spacing) + count = max(count, 1) + # calculate img_width when column count is the max + img_height = img_width = (width + img_spacing) // count - img_spacing + img_height = img_height + img_text_height + return img_width, img_height + + def eventFilter(self, _: QObject, event: QEvent): + if event.type() == QEvent.Resize: + event = cast(QResizeEvent, event) + self.on_view_resized(event.size(), event.oldSize()) + return False + class ImgFilterProxyModel(QSortFilterProxyModel): def __init__(self, parent=None, types=None): @@ -280,7 +320,7 @@ def filter_by_text(self, text): def filterAcceptsRow(self, source_row, source_parent): accepted = True - source_model = cast(ImgListModel, self.sourceModel()) + source_model = cast(ImgCardListModel, self.sourceModel()) index = source_model.index(source_row, parent=source_parent) artist = index.data(Qt.UserRole) if self.text: @@ -288,23 +328,21 @@ def filterAcceptsRow(self, source_row, source_parent): return accepted -class ImgListView(ItemViewNoScrollMixin, QListView): +class ImgCardListView(ItemViewNoScrollMixin, QListView): """ - .. versionadded:: 3.7.7 - The *img_min_width*, *img_spacing*, *img_text_height* parameter were added. + .. versionchanged:: 3.9 + The *img_min_width*, *img_spacing*, *img_text_height* parameter were removed. """ - def __init__(self, parent=None, - img_min_width=150, img_spacing=20, img_text_height=40, - **kwargs): + def __init__(self, parent=None, **kwargs): super().__init__(parent=parent, **kwargs) - self.img_min_width = img_min_width - self.img_spacing = img_spacing - self.img_text_height = img_text_height - - # override ItemViewNoScrollMixin variables + # Override ItemViewNoScrollMixin variables. Actually, there variables are + # not important because ItemViewNoScrollMixin use QListView.sizeHint() to + # calculate the size and it works well. + # + # self._least_row_count = 1 - self._row_height = img_min_width + img_spacing + img_text_height + self._row_height = 0 self.setViewMode(QListView.IconMode) self.setResizeMode(QListView.Adjust) @@ -312,34 +350,142 @@ def __init__(self, parent=None, self.setFrameShape(QFrame.NoFrame) self.initialize() - def resizeEvent(self, e): - super().resizeEvent(e) - - if self._no_scroll_v is True: - self._row_height = self.img_sizehint()[1] + self.img_spacing - self.adjust_height() + self.activated.connect(self.on_activated) - def img_sizehint(self) -> tuple: + def on_activated(self, _: QModelIndex): """ - - .. versionadded:: 3.7.7 + Subclass can implement this method if needed. """ - # HELP: listview needs about 20 spacing left on macOS - width = self.width() - 20 + pass - img_spacing = self.img_spacing - img_min_width = self.img_min_width - img_text_height = self.img_text_height - # according to our algorithm, when the widget width is: - # 2(img_min_width + img_spacing) + img_spacing - 1, - # the cover width can take the maximum width, it will be: - # CoverMaxWidth = 2 * img_min_width + img_spacing - 1 +class VideoCardListModel(ImgCardListModel): + def data(self, index, role): + offset = index.row() + if not index.isValid() or offset >= len(self._items): + return None + video = self._items[offset] + if role == Qt.DisplayRole: + return video.title_display + return super().data(index, role) - # calculate max column count - count = (width + img_spacing) // (img_min_width + img_spacing) - count = max(count, 1) - # calculate img_width when column count is the max - img_height = img_width = (width + img_spacing) // count - img_spacing - img_height = img_height + img_text_height - return img_width, img_height + +class VideoCardListDelegate(ImgCardListDelegate): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.as_circle = False + self.w_h_ratio = 1.618 + + +class VideoFilterProxyModel(ImgFilterProxyModel): + pass + + +class VideoCardListView(ImgCardListView): + play_video_needed = pyqtSignal([object]) + + def on_activated(self, index): + video = index.data(Qt.UserRole) + self.play_video_needed.emit(video) + + +class PlaylistCardListModel(ImgCardListModel): + pass + + +class PlaylistCardListDelegate(ImgCardListDelegate): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.as_circle = False + + +class PlaylistFilterProxyModel(ImgFilterProxyModel): + pass + + +class PlaylistCardListView(ImgCardListView): + show_playlist_needed = pyqtSignal([object]) + + def on_activated(self, index): + artist = index.data(Qt.UserRole) + self.show_playlist_needed.emit(artist) + + +class ArtistCardListModel(ImgCardListModel): + pass + + +class ArtistCardListDelegate(ImgCardListDelegate): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.as_circle = True + + +class ArtistFilterProxyModel(ImgFilterProxyModel): + pass + + +class ArtistCardListView(ImgCardListView): + show_artist_needed = pyqtSignal([object]) + + def on_activated(self, index): + artist = index.data(Qt.UserRole) + self.show_artist_needed.emit(artist) + + +class AlbumCardListModel(ImgCardListModel): + def data(self, index, role): + offset = index.row() + if not index.isValid() or offset >= len(self._items): + return None + + album = self._items[offset] + if role == Qt.WhatsThisRole: + if isinstance(album, AlbumModel): + if album.song_count >= 0: + # Like: 1991-01-01 10首 + return f'{album.released} {album.song_count}首' + return album.released + return super().data(index, role) + + +class AlbumCardListDelegate(ImgCardListDelegate): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.as_circle = False + + +class AlbumFilterProxyModel(ImgFilterProxyModel): + def __init__(self, parent=None, types=None): + super().__init__(parent) + + self.types = types + + def filter_by_types(self, types): + # if types is a empty list or None, we show all albums + if not types: + types = None + self.types = types + self.invalidateFilter() + + def filterAcceptsRow(self, source_row, source_parent): + accepted = super().filterAcceptsRow(source_row, source_parent) + source_model = self.sourceModel() + assert isinstance(source_model, AlbumCardListModel) + index = source_model.index(source_row, parent=source_parent) + album = index.data(Qt.UserRole) + if accepted and self.types: + accepted = AlbumType(album.type_) in self.types + return accepted + + +class AlbumCardListView(ImgCardListView): + show_album_needed = pyqtSignal([object]) + + def on_activated(self, index): + album = index.data(Qt.UserRole) + self.show_album_needed.emit(album) diff --git a/feeluown/gui/widgets/playlist.py b/feeluown/gui/widgets/playlist.py deleted file mode 100644 index 6762b88652..0000000000 --- a/feeluown/gui/widgets/playlist.py +++ /dev/null @@ -1,37 +0,0 @@ -from PyQt5.QtCore import Qt, pyqtSignal - -from .imglist import ( - ImgListModel, ImgListDelegate, ImgListView, - ImgFilterProxyModel -) - - -class PlaylistListModel(ImgListModel): - pass - - -class PlaylistListDelegate(ImgListDelegate): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.as_circle = False - - -class PlaylistFilterProxyModel(ImgFilterProxyModel): - pass - - -class PlaylistListView(ImgListView): - show_playlist_needed = pyqtSignal([object]) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - delegate = PlaylistListDelegate(self) - self.setItemDelegate(delegate) - - self.activated.connect(self._on_activated) - - def _on_activated(self, index): - artist = index.data(Qt.UserRole) - self.show_playlist_needed.emit(artist) diff --git a/feeluown/gui/widgets/video_list.py b/feeluown/gui/widgets/video_list.py deleted file mode 100644 index b5cd3ebf9d..0000000000 --- a/feeluown/gui/widgets/video_list.py +++ /dev/null @@ -1,45 +0,0 @@ -from PyQt5.QtCore import Qt, pyqtSignal - -from .imglist import ( - ImgListModel, ImgListDelegate, ImgListView, - ImgFilterProxyModel -) - - -class VideoListModel(ImgListModel): - def data(self, index, role): - offset = index.row() - if not index.isValid() or offset >= len(self._items): - return None - video = self._items[offset] - if role == Qt.DisplayRole: - return video.title_display - return super().data(index, role) - - -class VideoListDelegate(ImgListDelegate): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.as_circle = False - self.w_h_ratio = 1.618 - - -class VideoFilterProxyModel(ImgFilterProxyModel): - pass - - -class VideoListView(ImgListView): - play_video_needed = pyqtSignal([object]) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - delegate = VideoListDelegate(self) - self.setItemDelegate(delegate) - - self.activated.connect(self._on_activated) - - def _on_activated(self, index): - video = index.data(Qt.UserRole) - self.play_video_needed.emit(video)