Use GTK Composite Templates (close #62)
Use GTK Composite Templates for GUI elements to clean up and simplify the code for widgets and all UI elements. This includes splitting the large “gtk.glade” file into smaller .ui files and the large “widgets.py” file into smaller .py files.
This commit is contained in:
parent
f4b545369c
commit
ba373ddf4e
31 changed files with 4431 additions and 4300 deletions
349
mcg/playlistpanel.py
Normal file
349
mcg/playlistpanel.py
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
import logging
|
||||
import math
|
||||
import threading
|
||||
|
||||
from gi.repository import Gtk, GObject, GdkPixbuf
|
||||
|
||||
from mcg import client
|
||||
from mcg.albumheaderbar import AlbumHeaderbar
|
||||
from mcg.utils import Utils
|
||||
|
||||
|
||||
|
||||
|
||||
@Gtk.Template(resource_path='/de/coderkun/mcg/ui/playlist-toolbar.ui')
|
||||
class PlaylistToolbar(Gtk.ButtonBox):
|
||||
__gtype_name__ = 'McgPlaylistToolbar'
|
||||
__gsignals__ = {
|
||||
'select': (GObject.SIGNAL_RUN_FIRST, None, (bool,)),
|
||||
'clear-playlist': (GObject.SIGNAL_RUN_FIRST, None, ())
|
||||
}
|
||||
|
||||
# Widgets
|
||||
playlist_clear_button = Gtk.Template.Child()
|
||||
select_button = Gtk.Template.Child()
|
||||
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def on_select_toggled(self, widget):
|
||||
self.emit('select', widget.get_active())
|
||||
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def on_clear_clicked(self, widget):
|
||||
if widget is self.playlist_clear_button:
|
||||
self.emit('clear-playlist')
|
||||
|
||||
|
||||
def exit_selection(self):
|
||||
self.select_button.set_active(False)
|
||||
|
||||
|
||||
|
||||
|
||||
@Gtk.Template(resource_path='/de/coderkun/mcg/ui/playlist-panel.ui')
|
||||
class PlaylistPanel(Gtk.Stack):
|
||||
__gtype_name__ = 'McgPlaylistPanel'
|
||||
__gsignals__ = {
|
||||
'open-standalone': (GObject.SIGNAL_RUN_FIRST, None, ()),
|
||||
'close-standalone': (GObject.SIGNAL_RUN_FIRST, None, ()),
|
||||
'clear-playlist': (GObject.SIGNAL_RUN_FIRST, None, ()),
|
||||
'remove-album': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
||||
'remove-multiple-albums': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
||||
'play': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
||||
'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str,))
|
||||
}
|
||||
|
||||
|
||||
# Widgets
|
||||
panel_standalone = Gtk.Template.Child()
|
||||
actionbar_revealer = Gtk.Template.Child()
|
||||
# Playlist Grid
|
||||
playlist_grid = Gtk.Template.Child()
|
||||
# Action bar (normal)
|
||||
actionbar = Gtk.Template.Child()
|
||||
actionbar_standalone = Gtk.Template.Child()
|
||||
# Standalone Image
|
||||
standalone_stack = Gtk.Template.Child()
|
||||
standalone_spinner = Gtk.Template.Child()
|
||||
standalone_scroll = Gtk.Template.Child()
|
||||
standalone_image = Gtk.Template.Child()
|
||||
|
||||
|
||||
def __init__(self, client):
|
||||
GObject.GObject.__init__(self)
|
||||
self._client = client
|
||||
self._host = None
|
||||
self._item_size = 150
|
||||
self._playlist = None
|
||||
self._playlist_albums = None
|
||||
self._playlist_lock = threading.Lock()
|
||||
self._playlist_stop = threading.Event()
|
||||
self._icon_theme = Gtk.IconTheme.get_default()
|
||||
self._standalone_pixbuf = None
|
||||
self._selected_albums = []
|
||||
self._is_selected = False
|
||||
|
||||
# Widgets
|
||||
self._toolbar = PlaylistToolbar()
|
||||
self._toolbar.connect('select', self.on_toolbar_select)
|
||||
self._toolbar.connect('clear-playlist', self.on_toolbar_clear)
|
||||
# Header bar
|
||||
self._headerbar_standalone = AlbumHeaderbar()
|
||||
self._headerbar_standalone.connect('close', self.on_headerbar_close_clicked)
|
||||
# Playlist Grid: Model
|
||||
self._playlist_grid_model = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
|
||||
# Playlist Grid
|
||||
self.playlist_grid.set_model(self._playlist_grid_model)
|
||||
self.playlist_grid.set_pixbuf_column(0)
|
||||
self.playlist_grid.set_text_column(-1)
|
||||
self.playlist_grid.set_tooltip_column(1)
|
||||
|
||||
|
||||
def get_headerbar_standalone(self):
|
||||
return self._headerbar_standalone
|
||||
|
||||
|
||||
def get_toolbar(self):
|
||||
return self._toolbar
|
||||
|
||||
|
||||
def set_selected(self, selected):
|
||||
self._is_selected = selected
|
||||
|
||||
|
||||
def on_toolbar_select(self, widget, active):
|
||||
if active:
|
||||
self.actionbar_revealer.set_reveal_child(True)
|
||||
self.playlist_grid.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
|
||||
self.playlist_grid.get_style_context().add_class(Utils.CSS_SELECTION)
|
||||
else:
|
||||
self.actionbar_revealer.set_reveal_child(False)
|
||||
self.playlist_grid.set_selection_mode(Gtk.SelectionMode.SINGLE)
|
||||
self.playlist_grid.get_style_context().remove_class(Utils.CSS_SELECTION)
|
||||
|
||||
|
||||
def on_toolbar_clear(self, widget):
|
||||
self.emit('clear-playlist')
|
||||
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def on_playlist_grid_clicked(self, widget, path):
|
||||
# Get selected album
|
||||
iter = self._playlist_grid_model.get_iter(path)
|
||||
hash = self._playlist_grid_model.get_value(iter, 2)
|
||||
album = self._playlist_albums[hash]
|
||||
self._selected_albums = [album]
|
||||
self.emit('albumart', hash)
|
||||
|
||||
# Show standalone album
|
||||
if widget.get_selection_mode() == Gtk.SelectionMode.SINGLE:
|
||||
# Set labels
|
||||
self._headerbar_standalone.set_album(album)
|
||||
|
||||
# Show panel
|
||||
self._open_standalone()
|
||||
|
||||
# Set cover loading indicator
|
||||
self.standalone_stack.set_visible_child(self.standalone_spinner)
|
||||
self.standalone_spinner.start()
|
||||
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def on_playlist_grid_selection_changed(self, widget):
|
||||
self._selected_albums = []
|
||||
for path in widget.get_selected_items():
|
||||
iter = self._playlist_grid_model.get_iter(path)
|
||||
hash = self._playlist_grid_model.get_value(iter, 2)
|
||||
self._selected_albums.append(self._playlist_albums[hash])
|
||||
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def on_selection_cancel_clicked(self, widget):
|
||||
self._toolbar.exit_selection()
|
||||
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def on_selection_remove_clicked(self, widget):
|
||||
self.emit('remove-multiple-albums', self._selected_albums)
|
||||
self._toolbar.exit_selection()
|
||||
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def on_standalone_scroll_size_allocate(self, widget, allocation):
|
||||
self._resize_standalone_image()
|
||||
|
||||
|
||||
def on_headerbar_close_clicked(self, widget):
|
||||
self._close_standalone()
|
||||
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def on_standalone_remove_clicked(self, widget):
|
||||
self.emit('remove-album', self._selected_albums[0])
|
||||
self._close_standalone()
|
||||
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def on_standalone_play_clicked(self, widget):
|
||||
self.emit('play', self._selected_albums[0])
|
||||
self._close_standalone()
|
||||
|
||||
|
||||
def set_item_size(self, item_size):
|
||||
if self._item_size != item_size:
|
||||
self._item_size = item_size
|
||||
self._redraw()
|
||||
|
||||
|
||||
def get_item_size(self):
|
||||
return self._item_size
|
||||
|
||||
|
||||
def set_playlist(self, host, playlist):
|
||||
self._host = host
|
||||
self._playlist_stop.set()
|
||||
threading.Thread(target=self._set_playlist, args=(host, playlist, self._item_size,)).start()
|
||||
|
||||
|
||||
def set_albumart(self, album, data):
|
||||
if album in self._selected_albums:
|
||||
if data:
|
||||
# Load image and draw it
|
||||
try:
|
||||
self._standalone_pixbuf = Utils.load_pixbuf(data)
|
||||
except Exception as e:
|
||||
self._logger.exception("Failed to set albumart")
|
||||
self._cover_pixbuf = self._get_default_image()
|
||||
else:
|
||||
self._cover_pixbuf = self._get_default_image()
|
||||
# Show image
|
||||
self._resize_standalone_image()
|
||||
self.standalone_stack.set_visible_child(self.standalone_scroll)
|
||||
self.standalone_spinner.stop()
|
||||
|
||||
|
||||
def stop_threads(self):
|
||||
self._playlist_stop.set()
|
||||
|
||||
|
||||
def _set_playlist(self, host, playlist, size):
|
||||
if not self._is_selected and self._playlist != playlist:
|
||||
GObject.idle_add(
|
||||
self.get_parent().child_set_property,
|
||||
self,
|
||||
'needs-attention',
|
||||
True
|
||||
)
|
||||
self._playlist_lock.acquire()
|
||||
self._playlist_stop.clear()
|
||||
self._playlist = playlist
|
||||
self._playlist_albums = {}
|
||||
for album in playlist:
|
||||
self._playlist_albums[album.get_id()] = album
|
||||
self.playlist_grid.set_model(None)
|
||||
self.playlist_grid.freeze_child_notify()
|
||||
self._playlist_grid_model.clear()
|
||||
GObject.idle_add(self.playlist_grid.set_item_padding, size / 100)
|
||||
|
||||
cache = client.MCGCache(host, size)
|
||||
for album in playlist:
|
||||
pixbuf = None
|
||||
# Load albumart thumbnail
|
||||
try:
|
||||
pixbuf = Utils.load_thumbnail(cache, self._client, album, size)
|
||||
except client.CommandException:
|
||||
# Exception is handled by client
|
||||
pass
|
||||
except Exception:
|
||||
self._logger.exception("Failed to load albumart")
|
||||
if pixbuf is None:
|
||||
pixbuf = self._icon_theme.load_icon(
|
||||
Utils.STOCK_ICON_DEFAULT,
|
||||
self._item_size,
|
||||
Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
|
||||
)
|
||||
if pixbuf is not None:
|
||||
self._playlist_grid_model.append([
|
||||
pixbuf,
|
||||
GObject.markup_escape_text("\n".join([
|
||||
album.get_title(),
|
||||
', '.join(album.get_dates()),
|
||||
Utils.create_artists_label(album),
|
||||
Utils.create_length_label(album)
|
||||
])),
|
||||
album.get_id()
|
||||
])
|
||||
|
||||
if self._playlist_stop.is_set():
|
||||
self._playlist_lock.release()
|
||||
return
|
||||
|
||||
self.playlist_grid.set_model(self._playlist_grid_model)
|
||||
self.playlist_grid.thaw_child_notify()
|
||||
# TODO why set_columns()?
|
||||
#self.playlist_grid.set_columns(len(playlist))
|
||||
self._playlist_lock.release()
|
||||
|
||||
|
||||
def _redraw(self):
|
||||
if self._playlist is not None:
|
||||
self.set_playlist(self._host, self._playlist)
|
||||
|
||||
|
||||
def _open_standalone(self):
|
||||
self.set_visible_child(self.panel_standalone)
|
||||
self.emit('open-standalone')
|
||||
|
||||
|
||||
def _close_standalone(self):
|
||||
self.set_visible_child(self.get_children()[0])
|
||||
self.emit('close-standalone')
|
||||
|
||||
|
||||
def _resize_standalone_image(self):
|
||||
"""Diese Methode skaliert das geladene Bild aus dem Pixelpuffer
|
||||
auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse
|
||||
"""
|
||||
pixbuf = self._standalone_pixbuf
|
||||
size = self.standalone_scroll.get_allocation()
|
||||
# Check pixelbuffer
|
||||
if pixbuf is None:
|
||||
return
|
||||
|
||||
# Skalierungswert für Breite und Höhe ermitteln
|
||||
ratioW = float(size.width) / float(pixbuf.get_width())
|
||||
ratioH = float(size.height) / float(pixbuf.get_height())
|
||||
# Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren
|
||||
ratio = min(ratioW, ratioH)
|
||||
ratio = min(ratio, 1)
|
||||
# Neue Breite und Höhe berechnen
|
||||
width = int(math.floor(pixbuf.get_width()*ratio))
|
||||
height = int(math.floor(pixbuf.get_height()*ratio))
|
||||
if width <= 0 or height <= 0:
|
||||
return
|
||||
# Pixelpuffer auf Oberfläche zeichnen
|
||||
self.standalone_image.set_allocation(self.standalone_scroll.get_allocation())
|
||||
self.standalone_image.set_from_pixbuf(pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
|
||||
self.standalone_image.show()
|
||||
|
||||
|
||||
def _get_default_image(self):
|
||||
return self._icon_theme.load_icon(
|
||||
Utils.STOCK_ICON_DEFAULT,
|
||||
512,
|
||||
Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue