Skip to content

Commit

Permalink
gui: draw user avatar (#716)
Browse files Browse the repository at this point in the history
  • Loading branch information
cosven authored Jul 15, 2023
1 parent cc824cf commit ec77ed4
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 81 deletions.
73 changes: 39 additions & 34 deletions feeluown/gui/components/avatar.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,38 @@
from PyQt5.QtWidgets import QMenu, QAction
from PyQt5.QtGui import QPainter, QIcon, QPalette, QContextMenuEvent

from feeluown.library import UserModel
from feeluown.library import NoUserLoggedIn
from feeluown.models.uri import reverse
from feeluown.utils.aio import run_afn, run_fn
from feeluown.gui.widgets import SelfPaintAbstractSquareButton
from feeluown.gui.uimodels.provider import ProviderUiItem
from feeluown.gui.drawers import PixmapDrawer, AvatarIconDrawer

if TYPE_CHECKING:
from feeluown.app.gui_app import GuiApp


class Avatar(SelfPaintAbstractSquareButton):
"""
When no provider is selected, click this button will popup a menu,
and let user select a provider. When a provider is selected, click this
button will trigger `provider_ui_item.clicked`. If the provider has
a current user, this tries to show the user avatar.
"""

def __init__(self, app: 'GuiApp', *args, **kwargs):
super().__init__(*args, **kwargs)

self._app = app
self._provider_ui_item: Optional[ProviderUiItem] = None
self._current_user: Optional[UserModel] = None
self._avatar_drawer = None
self._icon_drawer = AvatarIconDrawer(self.width(), self._padding)
self.clicked.connect(self.on_clicked)
self.setToolTip('点击登陆资源提供方')

def on_clicked(self):
if self._provider_ui_item is not None:
self.maybe_goto_current_provider()
self._provider_ui_item.clicked.emit()
else:
pos = self.cursor().pos()
e = QContextMenuEvent(QContextMenuEvent.Mouse, pos, pos)
Expand All @@ -49,46 +59,41 @@ def contextMenuEvent(self, e) -> None:

def on_provider_selected(self, provider: ProviderUiItem):
self._provider_ui_item = provider
self.maybe_goto_current_provider()

def maybe_goto_current_provider(self):
provider = self._provider_ui_item
provider.clicked.emit()
self.setToolTip(provider.text + '\n\n' + provider.desc)
# HACK: If the provider does not update the current page,
# render the provider home page manually.
# old_page = self._app.browser.current_page
# new_page = self._app.browser.current_page
# if new_page == old_page:
# self._app.browser.goto(page=f'/providers/{provider.name}')
self._provider_ui_item.clicked.emit()
run_afn(self.show_provider_current_user)

async def show_provider_current_user(self):
self._avatar_drawer = None
try:
user = await run_fn(
self._app.library.provider_get_current_user,
self._provider_ui_item.name)
except NoUserLoggedIn:
user = None

if user is not None:
self.setToolTip(f'{user.name} ({self._provider_ui_item.text})')
if user.avatar_url:
img_data = await run_afn(self._app.img_mgr.get,
user.avatar_url,
reverse(user))
if img_data:
self._avatar_drawer = PixmapDrawer.from_img_data(
img_data, self, radius=0.5)

def paintEvent(self, _):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
self.paint_round_bg_when_hover(painter)

has_avatar = False
if self._current_user is not None and self._current_user.avatar_url:
has_avatar = True
# Draw avatar.

if not has_avatar:
pen = painter.pen()
pen.setWidthF(1.5)
painter.setPen(pen)

if self._avatar_drawer:
self._avatar_drawer.draw(painter)
else:
# If a provider is selected, draw a highlight circle.
if self._provider_ui_item is not None:
painter.setPen(self.palette().color(QPalette.Highlight))

diameter = self.width() // 3
# Draw circle.
painter.drawEllipse(diameter, self._padding, diameter, diameter)
# Draw body.
x, y = self._padding, self.height() // 2
width, height = self.width() // 2, self.height() // 2
painter.drawArc(x, y, width, height, 0, 60*16)
painter.drawArc(x, y, width, height, 120*16, 60*16)
self._icon_drawer.fg_color = self.palette().color(QPalette.Highlight)
self._icon_drawer.draw(painter)


if __name__ == '__main__':
Expand Down
93 changes: 93 additions & 0 deletions feeluown/gui/drawers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from PyQt5.QtCore import Qt, QRect
from PyQt5.QtGui import QPainter, QBrush, QPixmap, QImage, QColor
from PyQt5.QtWidgets import QWidget


class PixmapDrawer:
"""Draw pixmap on a widget with radius.
The pixmap will be scaled to the width of the widget.
"""
def __init__(self, img, widget: QWidget, radius: int = 0):
"""
:param widget: a object which has width() and height() method.
"""
self._img = img
self._widget_last_width = widget.width()

new_img = img.scaledToWidth(self._widget_last_width, Qt.SmoothTransformation)
self._pixmap = QPixmap(new_img)

self._widget = widget
self._radius = radius

@classmethod
def from_img_data(cls, img_data, *args, **kwargs):
img = QImage()
img.loadFromData(img_data)
return cls(img, *args, **kwargs)

def get_img(self) -> QImage:
return self._img

def get_pixmap(self) -> QPixmap:
return self._pixmap

def maybe_update_pixmap(self):
if self._widget.width() != self._widget_last_width:
self._widget_last_width = self._widget.width()
new_img = self._img.scaledToWidth(self._widget_last_width,
Qt.SmoothTransformation)
self._pixmap = QPixmap(new_img)

def draw(self, painter):
if self._pixmap is None:
return

self.maybe_update_pixmap()

painter.save()
painter.setRenderHint(QPainter.Antialiasing)
painter.setRenderHint(QPainter.SmoothPixmapTransform)
brush = QBrush(self._pixmap)
painter.setBrush(brush)
painter.setPen(Qt.NoPen)
radius = self._radius
size = self._pixmap.size()
y = (size.height() - self._widget.height()) // 2

painter.save()
painter.translate(0, -y)
rect = QRect(0, y, self._widget.width(), self._widget.height())
if radius == 0:
painter.drawRect(rect)
else:
radius = radius if self._radius >= 1 else self._widget.width() * self._radius
painter.drawRoundedRect(rect, radius, radius)
painter.restore()
painter.restore()


class AvatarIconDrawer:
def __init__(self, length, padding, fg_color=None):
self._length = length
self._padding = padding

self.fg_color = fg_color

def draw(self, painter):
pen = painter.pen()
pen.setWidthF(1.5)
painter.setPen(pen)

if self.fg_color:
painter.setPen(QColor(self.fg_color))

diameter = self._length // 3
# Draw circle.
painter.drawEllipse(diameter, self._padding, diameter, diameter)
# Draw body.
x, y = self._padding, self._length // 2
width, height = self._length // 2, self._length // 2
painter.drawArc(x, y, width, height, 0, 60*16)
painter.drawArc(x, y, width, height, 120*16, 60*16)
63 changes: 16 additions & 47 deletions feeluown/gui/widgets/cover_label.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import warnings

from PyQt5.QtCore import Qt, QSize, QRect
from PyQt5.QtGui import QPainter, QBrush, QImage, QPixmap
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QPainter, QImage
from PyQt5.QtWidgets import QLabel, QSizePolicy, QMenu

from feeluown.gui.drawers import PixmapDrawer
from feeluown.gui.image import open_image


Expand All @@ -12,31 +13,23 @@ def __init__(self, parent=None, pixmap=None, radius=3):
super().__init__(parent=parent)

self._radius = radius

# There is possibility that self._img is None and self._pixmap is not None.
# When self._img is not None, self._pixmap can not be None.
self._img = None
self._pixmap = pixmap
self.drawer = None
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.MinimumExpanding)

def show_pixmap(self, pixmap):
"""
.. versiondeprecated:: 3.8.11
"""
warnings.warn('You should use show_img', DeprecationWarning)
self._img = None
self._pixmap = pixmap
self.updateGeometry()
self.update() # Schedule a repaint to refresh the UI ASAP.

def show_img(self, img: QImage):
if img is None or img.isNull():
if not img or img.isNull():
self.drawer = None
return

self._img = img
new_img = img.scaledToWidth(self.width())
pixmap = QPixmap(new_img)
self._pixmap = pixmap
self.drawer = PixmapDrawer(img, self, self._radius)
self.updateGeometry()
self.update()

Expand All @@ -48,53 +41,29 @@ def paintEvent(self, e):
one is as follow, the other way is using bitmap mask,
but in our practice, the mask way has poor render effects
"""
if self._pixmap is None:
return

painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setRenderHint(QPainter.SmoothPixmapTransform)
brush = QBrush(self._pixmap)
painter.setBrush(brush)
painter.setPen(Qt.NoPen)
radius = self._radius
size = self._pixmap.size()
y = (size.height() - self.height()) // 2

painter.save()
painter.translate(0, -y)
rect = QRect(0, y, self.width(), self.height())
painter.drawRoundedRect(rect, radius, radius)
painter.restore()
painter.end()
if self.drawer:
painter = QPainter(self)
self.drawer.draw(painter)

def contextMenuEvent(self, e):
if self._img is None:
if self.drawer is None:
return
menu = QMenu()
action = menu.addAction('查看原图')
action.triggered.connect(lambda: open_image(self._img)) # type: ignore
action.triggered.connect(
lambda: open_image(self.drawer.get_img())) # type: ignore
menu.exec(e.globalPos())

def resizeEvent(self, e):
super().resizeEvent(e)
self.updateGeometry()

if self._img is not None:
# Resize pixmap.
img = self._img.scaledToWidth(self.width(), Qt.SmoothTransformation)
self._pixmap = QPixmap(img)
elif self._pixmap is not None:
self._pixmap = self._pixmap.scaledToWidth(
self.width(),
mode=Qt.SmoothTransformation
)

def sizeHint(self):
super_size = super().sizeHint()
if self._pixmap is None:
if self.drawer is None:
return super_size
h = (self.width() * self._pixmap.height()) // self._pixmap.width()
h = (self.width() * self.drawer.get_pixmap().height()) \
// self.drawer.get_pixmap().width()
# cover label height hint can be as large as possible, since the
# parent width has been set maximumHeigh
w = self.width()
Expand Down

0 comments on commit ec77ed4

Please sign in to comment.