Commit 263bdb05 authored by Marinus Schraal's avatar Marinus Schraal

Introduce HiDPI support

Use cairo surfaces to support HiDPI artwork.
Search stil requires us to convert back to GdkPixbuf at the end of the
line, because of libgd limitations.
parent 928d0a63
......@@ -47,13 +47,14 @@ logger = logging.getLogger(__name__)
@log
def _make_icon_frame(pixbuf):
border = 3
def _make_icon_frame(pixbuf, art_size=None, scale=1):
border = 3 * scale
degrees = pi / 180
radius = 3
radius = 3 * scale
w = pixbuf.get_width()
h = pixbuf.get_height()
ratio = pixbuf.get_height() / pixbuf.get_width()
w = art_size.width * scale
h = int(art_size.height * ratio * scale)
new_pixbuf = pixbuf.scale_simple(w - border * 2,
h - border * 2,
......@@ -83,7 +84,11 @@ def _make_icon_frame(pixbuf):
border_pixbuf = Gdk.pixbuf_get_from_surface(surface, 0, 0, w, h)
return border_pixbuf
surface = Gdk.cairo_surface_create_from_pixbuf(border_pixbuf,
scale,
None)
return surface
class ArtSize(Enum):
......@@ -108,14 +113,20 @@ class DefaultIcon(GObject.GObject):
music = 'folder-music-symbolic'
_cache = {}
_scale = 1
def __repr__(self):
return '<DefaultIcon>'
@log
def _make_default_icon(self, icon_type, art_size):
width = art_size.width
height = art_size.height
def __init__(self, scale=1):
GObject.GObject.__init__(self)
self._scale = scale
@log
def _make_default_icon(self, icon_type, art_size=None):
width = art_size.width * self._scale
height = art_size.height * self._scale
icon = Gtk.IconTheme.get_default().load_icon(icon_type.value,
max(width, height) / 4,
......@@ -138,9 +149,9 @@ class DefaultIcon(GObject.GObject):
icon.get_height() * 3 / 2,
1, 1, GdkPixbuf.InterpType.HYPER, 0x33)
final_icon = _make_icon_frame(result)
icon_surface = _make_icon_frame(result, art_size, self._scale)
return final_icon
return icon_surface
@log
def get(self, icon_type, art_size):
......@@ -170,13 +181,15 @@ class AlbumArtCache(GObject.GObject):
"""
_instance = None
blacklist = {}
_scale = 1
def __repr__(self):
return '<AlbumArtCache>'
@log
def __init__(self):
def __init__(self, scale=1):
GObject.GObject.__init__(self)
self._scale = scale
self.cache_dir = os.path.join(GLib.get_user_cache_dir(), 'media-art')
if not os.path.exists(self.cache_dir):
......@@ -191,8 +204,7 @@ class AlbumArtCache(GObject.GObject):
"""Find art for the given item
:param item: Grilo media item
:param int width: Width of the icon to return
:param int height: Height of the icon to return
:param ArtSize art_size: Size of the icon
:param callback: Callback function when retrieved
:param itr: Iter to return with callback
"""
......@@ -231,21 +243,19 @@ class AlbumArtCache(GObject.GObject):
def do_callback(pixbuf):
if not pixbuf:
pixbuf = DefaultIcon().get(DefaultIcon.Type.music, art_size)
surface = DefaultIcon(self._scale).get(DefaultIcon.Type.music,
art_size)
else:
pixbuf = pixbuf.scale_simple(art_size.width,
art_size.height,
GdkPixbuf.InterpType.HYPER)
pixbuf = _make_icon_frame(pixbuf)
surface = _make_icon_frame(pixbuf, art_size, self._scale)
# Sets the thumbnail location for MPRIS to use.
item.set_thumbnail(GLib.filename_to_uri(thumb_file.get_path(),
None))
GLib.idle_add(callback, pixbuf, None, itr)
GLib.idle_add(callback, surface, None, itr)
return
[success, thumb_file] = MediaArt.get_file(artist, album, "album")
success, thumb_file = MediaArt.get_file(artist, album, "album")
if (success
and thumb_file.query_exists()):
......
......@@ -108,9 +108,11 @@ class Player(GObject.GObject):
self.currentTrack = None
self.currentTrackUri = None
self._lastState = Gst.State.PAUSED
self.cache = AlbumArtCache()
self._no_artwork_icon = DefaultIcon().get(DefaultIcon.Type.music,
ArtSize.xsmall)
scale = parent_window.get_scale_factor()
self.cache = AlbumArtCache(scale)
self._no_artwork_icon_surface = DefaultIcon(scale).get(
DefaultIcon.Type.music,
ArtSize.xsmall)
self._missingPluginMessages = []
Gst.init(None)
......@@ -605,9 +607,8 @@ class Player(GObject.GObject):
except:
self._currentAlbum = album
self.coverImg.set_from_pixbuf(self._no_artwork_icon)
self.cache.lookup(
media, ArtSize.xsmall, self._on_cache_lookup, None)
self.coverImg.set_from_surface(self._no_artwork_icon_surface)
self.cache.lookup(media, ArtSize.xsmall, self._on_cache_lookup, None)
self._currentTitle = utils.get_media_title(media)
self.titleLabel.set_label(self._currentTitle)
......@@ -662,7 +663,7 @@ class Player(GObject.GObject):
@log
def _on_cache_lookup(self, pixbuf, path, data=None):
if pixbuf is not None:
self.coverImg.set_from_pixbuf(pixbuf)
self.coverImg.set_from_surface(pixbuf)
self.emit('thumbnail-updated', path)
@log
......
......@@ -30,10 +30,10 @@
# code, but you are not obligated to do so. If you do not wish to do so,
# delete this exception statement from your version.
from gi.repository import Gtk
from gi.repository import GObject
from gi.repository import Gd
from gi.repository import Gdk
from gi.repository import Gio
from gi.repository import Grl
from gi.repository import Pango
......@@ -127,9 +127,13 @@ class ViewContainer(Gtk.Stack):
self.show_all()
self.view.hide()
self._items = []
self.cache = AlbumArtCache()
self._loading_icon = DefaultIcon().get(DefaultIcon.Type.loading,
ArtSize.medium)
scale = self.get_scale_factor()
self.cache = AlbumArtCache(scale)
self._loading_icon_surface = DefaultIcon(scale).get(
DefaultIcon.Type.loading,
ArtSize.medium)
self._init = False
grilo.connect('ready', self._on_grilo_ready)
......@@ -194,7 +198,6 @@ class ViewContainer(Gtk.Stack):
@log
def _on_view_selection_changed(self, widget):
if not self.selection_mode:
return
......@@ -241,18 +244,33 @@ class ViewContainer(Gtk.Stack):
title = utils.get_media_title(item)
_iter = self.model.append(None)
self.model.set(_iter,
[0, 1, 2, 3, 4, 5, 7, 9],
[str(item.get_id()), '', title,
artist, self._loading_icon, item,
0, False])
loading_icon = Gdk.pixbuf_get_from_surface(
self._loadin_icon_surface, 0, 0,
self._loading_icon_surface.get_width(),
self._loading_icon_surface.get_height())
self.model[_iter][0, 1, 2, 3, 4, 5, 7, 9] = [
str(item.get_id()),
'',
title,
artist,
loading_icon,
item,
0,
False
]
self.cache.lookup(item, self._iconWidth, self._iconHeight,
self._on_lookup_ready, _iter)
@log
def _on_lookup_ready(self, icon, path, _iter):
if icon:
self.model.set_value(_iter, 4, icon)
def _on_lookup_ready(self, surface, path, _iter):
if surface:
pixbuf = Gdk.pixbuf_get_from_surface(surface, 0, 0,
surface.get_width(),
surface.get_height())
self.model[_iter][4] = pixbuf
@log
def _add_list_renderers(self):
......@@ -494,7 +512,8 @@ class Albums(ViewContainer):
child.title.set_label(title)
child.subtitle.set_label(artist)
child.image.set_from_pixbuf(self._loading_icon)
child.image.set_from_surface(self._loading_icon_surface)
# In the case of off-sized icons (eg. provided in the soundfile)
# keep the size request equal to all other icons to get proper
# alignment with GtkFlowBox.
......@@ -530,7 +549,7 @@ class Albums(ViewContainer):
child.check.set_active(True)
def _on_lookup_ready(self, icon, path, child):
child.image.set_from_pixbuf(icon)
child.image.set_from_surface(icon)
@log
def _on_child_toggled(self, check, pspec, child):
......@@ -1527,10 +1546,25 @@ class Search(ViewContainer):
self._items = {}
self.isStarred = None
self.iter_to_clean = None
self._loading_icon = DefaultIcon().get(DefaultIcon.Type.loading,
ArtSize.small)
self._no_albumart_icon = DefaultIcon().get(DefaultIcon.Type.music,
ArtSize.small)
scale = self.get_scale_factor()
loading_icon_surface = DefaultIcon(scale).get(DefaultIcon.Type.loading,
ArtSize.small)
no_albumart_surface = DefaultIcon(scale).get(DefaultIcon.Type.music,
ArtSize.small)
self._loading_icon = Gdk.pixbuf_get_from_surface(
loading_icon_surface,
0,
0,
loading_icon_surface.get_width(),
loading_icon_surface.get_height())
self._no_albumart_icon = Gdk.pixbuf_get_from_surface(
no_albumart_surface,
0,
0,
no_albumart_surface.get_width(),
no_albumart_surface.get_height())
self._add_list_renderers()
self.player = player
self.head_iters = [None, None, None, None]
......@@ -1691,6 +1725,9 @@ class Search(ViewContainer):
except:
pass
# FIXME: HiDPI icon lookups return a surface that can't be
# scaled by GdkPixbuf, so it results in a * scale factor sized
# icon for the search view.
_iter = None
if category == 'album':
_iter = self.model.insert_with_values(
......@@ -1704,7 +1741,9 @@ class Search(ViewContainer):
self.head_iters[group], -1,
[0, 2, 3, 4, 5, 9, 11],
[str(item.get_id()), title, artist,
self._no_albumart_icon, item, 2 if source.get_id() != 'grl-tracker-source' else bool(item.get_lyrics()), category])
self._no_albumart_icon, item,
2 if source.get_id() != 'grl-tracker-source' \
else bool(item.get_lyrics()), category])
else:
if not artist.casefold() in self._artists:
_iter = self.model.insert_with_values(
......
......@@ -123,8 +123,6 @@ class AlbumWidget(Gtk.EventBox):
"""
_duration = 0
_loading_icon = DefaultIcon().get(DefaultIcon.Type.loading, ArtSize.small)
_no_artwork_icon = DefaultIcon().get(DefaultIcon.Type.music, ArtSize.small)
def __repr__(self):
return '<AlbumWidget>'
......@@ -137,7 +135,16 @@ class AlbumWidget(Gtk.EventBox):
:param parent_view: The view this widget is part of
"""
Gtk.EventBox.__init__(self)
self._cache = AlbumArtCache()
scale = self.get_scale_factor()
self._cache = AlbumArtCache(scale)
self._loading_icon_surface = DefaultIcon(scale).get(
DefaultIcon.Type.loading,
ArtSize.small)
self._no_artwork_icon_surface = DefaultIcon(scale).get(
DefaultIcon.Type.music,
ArtSize.small)
self._player = player
self._iter_to_clean = None
......@@ -290,7 +297,8 @@ class AlbumWidget(Gtk.EventBox):
self.selection_toolbar = selection_toolbar
self._header_bar = header_bar
self._album = album
self._ui.get_object('cover').set_from_pixbuf(self._loading_icon)
self._ui.get_object('cover').set_from_surface(
self._loading_icon_surface)
self._cache.lookup(item, ArtSize.large, self._on_look_up, None)
self._duration = 0
self._create_model()
......@@ -380,19 +388,16 @@ class AlbumWidget(Gtk.EventBox):
_("%d min") % (int(self._duration / 60) + 1))
@log
def _on_look_up(self, pixbuf, path, data=None):
def _on_look_up(self, surface, path, data=None):
"""Albumart retrieved callback.
:param pixbuf: The GtkPixbuf retrieved
:param surface: The Cairo surface retrieved
:param path: The filesystem location the pixbuf
:param data: User data
"""
_iter = self._iter_to_clean
if not pixbuf:
pixbuf = self._no_artwork_icon
self._ui.get_object('cover').set_from_pixbuf(pixbuf)
if _iter:
self.model[_iter][4] = pixbuf
if not surface:
surface = self._no_artwork_icon_surface
self._ui.get_object('cover').set_from_surface(surface)
@log
def _update_model(self, player, playlist, current_iter):
......@@ -626,16 +631,23 @@ class ArtistAlbumWidget(Gtk.Box):
'tracks-loaded': (GObject.SignalFlags.RUN_FIRST, None, ()),
}
_loading_icon = DefaultIcon().get(DefaultIcon.Type.loading, ArtSize.large)
_no_artwork_icon = DefaultIcon().get(DefaultIcon.Type.music, ArtSize.large)
def __repr__(self):
return '<ArtistAlbumWidget>'
@log
def __init__(self, artist, album, player, model, header_bar, selectionModeAllowed):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
self._cache = AlbumArtCache()
scale = self.get_scale_factor()
self._cache = AlbumArtCache(scale)
self._loading_icon_surface = DefaultIcon(scale).get(
DefaultIcon.Type.loading,
ArtSize.large)
self._no_artwork_icon_surface = DefaultIcon(scale).get(
DefaultIcon.Type.music,
ArtSize.large)
self.player = player
self.album = album
self.artist = artist
......@@ -651,7 +663,7 @@ class ArtistAlbumWidget(Gtk.Box):
GLib.idle_add(self._update_album_art)
self.cover = self.ui.get_object('cover')
self.cover.set_from_pixbuf(self._loading_icon)
self.cover.set_from_surface(self._loading_icon_surface)
self.songsGrid = self.ui.get_object('grid1')
self.ui.get_object('title').set_label(album.get_title())
if album.get_creation_date():
......@@ -719,10 +731,10 @@ class ArtistAlbumWidget(Gtk.Box):
None)
@log
def _get_album_cover(self, pixbuf, path, data=None):
if not pixbuf:
pixbuf = self._no_artwork_icon
self.cover.set_from_pixbuf(pixbuf)
def _get_album_cover(self, surface, path, data=None):
if not surface:
surface = self._no_artwork_icon_surface
self.cover.set_from_surface(surface)
@log
def track_selected(self, widget, event):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment