mcg/mcgGtk.py

1248 lines
39 KiB
Python
Executable file

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Author: coderkun <olli@coderkun.de>
from gi.repository import Gtk, Gdk, GdkPixbuf, GObject
import mcg
import urllib
from threading import Thread
import os
class MCGGtk(Gtk.Window):
TITLE = "MPDCoverGrid (Gtk)"
def __init__(self):
Gtk.Window.__init__(self, title=MCGGtk.TITLE)
self._mcg = mcg.MCGClient()
self._config = Configuration()
self._maximized = False
self._fullscreened = False
# Widgets
#self._main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self._main_box = Gtk.VBox()
self.add(self._main_box)
self._toolbar = Toolbar(self._config.list_mode, self._config.item_size)
self._main_box.pack_start(self._toolbar, False, False, 0)
self._connection_panel = ConnectionPanel(self._config)
self._main_box.pack_end(self._connection_panel, True, True, 0)
self._cover_panel = CoverPanel(self._config)
# Properties
self.set_hide_titlebar_when_maximized(True)
# Actions
self.resize(self._config.window_width, self._config.window_height)
if self._config.window_maximized:
self.maximize()
# Signals
self.connect('size-allocate', self.on_resize)
self.connect('window-state-event', self.on_state)
self.connect('delete-event', self.on_destroy)
self._toolbar.connect_signal(Toolbar.SIGNAL_CONNECT, self.on_toolbar_connect)
self._toolbar.connect_signal(Toolbar.SIGNAL_UPDATE, self.on_toolbar_update)
self._toolbar.connect_signal(Toolbar.SIGNAL_PLAYPAUSE, self.on_toolbar_playpause)
self._toolbar.connect_signal(Toolbar.SIGNAL_LIST_MODE, self.on_toolbar_list_mode)
self._toolbar.connect_signal(Toolbar.SIGNAL_FILTER, self.on_toolbar_filter)
self._toolbar.connect_signal(Toolbar.SIGNAL_SORT, self.on_toolbar_sort)
self._toolbar.connect_signal(Toolbar.SIGNAL_SORT_TYPE, self.on_toolbar_sort_type)
self._toolbar.connect_signal(Toolbar.SIGNAL_GRID_SIZE_CHANGE, self.on_toolbar_grid_size_change)
self._toolbar.connect_signal(Toolbar.SIGNAL_GRID_SIZE_CHANGED, self.on_toolbar_grid_size_changed)
self._connection_panel.connect_signal(ConnectionPanel.SIGNAL_PROFILE_CHANGED, self.on_connection_profile_changed)
self._cover_panel.connect_signal(CoverPanel.SIGNAL_ALBUMS_SET, self.on_albums_set)
self._cover_panel.connect_signal(CoverPanel.SIGNAL_TOGGLE_FULLSCREEN, self.on_cover_panel_toggle_fullscreen)
self._cover_panel.connect_signal(CoverPanel.SIGNAL_PLAY, self.on_cover_panel_play)
self._mcg.connect_signal(mcg.MCGClient.SIGNAL_CONNECT, self.on_mcg_connect)
self._mcg.connect_signal(mcg.MCGClient.SIGNAL_STATUS, self.on_mcg_status)
self._mcg.connect_signal(mcg.MCGClient.SIGNAL_LOAD_PLAYLIST, self.on_mcg_load_playlist)
self._mcg.connect_signal(mcg.MCGClient.SIGNAL_LOAD_ALBUMS, self.on_mcg_load_albums)
def on_resize(self, widget, event):
self._save_size()
def on_state(self, widget, state):
self._fullscreen((state.new_window_state & Gdk.WindowState.FULLSCREEN > 0))
self._save_state(state)
def on_destroy(self, widget, state):
self._mcg.disconnect_signal(mcg.MCGClient.SIGNAL_CONNECT)
self._mcg.disconnect_signal(mcg.MCGClient.SIGNAL_STATUS)
if self._mcg.is_connected():
self._mcg.disconnect()
self._mcg.join()
self._config.save()
self._connection_panel.save_profiles()
GObject.idle_add(Gtk.main_quit)
# Toolbar callbacks
def on_toolbar_connect(self):
self._connect()
def on_toolbar_update(self):
self._mcg.update()
def on_toolbar_playpause(self):
self._mcg.playpause()
def on_toolbar_list_mode(self):
self._config.list_mode = self._toolbar.get_list_mode()
self._cover_panel.set_list_mode(self._toolbar.get_list_mode())
def on_toolbar_filter(self, filter_string):
self._cover_panel.filter(filter_string)
def on_toolbar_sort(self, sort_order):
self._cover_panel.set_sort_order(sort_order)
def on_toolbar_sort_type(self, sort_type):
self._cover_panel.set_sort_type(sort_type)
def on_toolbar_grid_size_change(self, size):
self._cover_panel.set_grid_size(size)
def on_toolbar_grid_size_changed(self, size):
#self._cover_panel.set_grid_size(size)
self._cover_panel.redraw()
# Connection Panel callbacks
def on_connection_profile_changed(self, index, profile):
self._config.last_profile = index
if ConnectionPanel.TAG_AUTOCONNECT in profile.get_tags():
self._connect()
# Cover Panel callbacks
def on_albums_set(self):
GObject.idle_add(self._toolbar.set_sensitive, True)
def on_cover_panel_toggle_fullscreen(self):
self._toggle_fullscreen()
def on_cover_panel_play(self, album):
self._mcg.play_album(album)
# MCG callbacks
def on_mcg_connect(self, connected, error):
if connected:
GObject.idle_add(self._connect_connected)
GObject.idle_add(self._load_albums)
GObject.idle_add(self._load_playlist)
GObject.idle_add(self._mcg.get_status)
else:
if error:
dialog = ErrorDialog(self, error)
dialog.show_dialog()
GObject.idle_add(self._connect_disconnected)
def on_mcg_status(self, state, album, pos, error):
if state == 'play':
GObject.idle_add(self._toolbar.set_pause)
elif state == 'pause' or state == 'stop':
GObject.idle_add(self._toolbar.set_play)
if album:
GObject.idle_add(self._cover_panel.set_album, album)
def on_mcg_load_playlist(self, playlist, error):
self._cover_panel.set_playlist(self._connection_panel.get_host(), playlist)
def on_mcg_load_albums(self, albums, error):
self._cover_panel.set_albums(self._connection_panel.get_host(), albums)
# Private methods
def _connect(self):
self._connection_panel.set_sensitive(False)
self._toolbar.set_sensitive(False)
if self._mcg.is_connected():
self._mcg.disconnect()
else:
host = self._connection_panel.get_host()
port = self._connection_panel.get_port()
password = self._connection_panel.get_password()
image_dir = self._connection_panel.get_image_dir()
self._mcg.connect(host, port, password, image_dir)
def _connect_connected(self):
self._toolbar.connected()
self._toolbar.set_sensitive(True)
self._connection_panel.set_sensitive(True)
self._main_box.remove(self._connection_panel)
self._main_box.pack_start(self._cover_panel, True, True, 0)
self._main_box.show_all()
def _connect_disconnected(self):
self._toolbar.disconnected()
self._toolbar.set_sensitive(True)
self._connection_panel.set_sensitive(True)
self._main_box.remove(self._main_box.get_children()[1])
self._main_box.pack_end(self._connection_panel, True, True, 0)
self._main_box.show_all()
def _load_playlist(self):
self._mcg.load_playlist()
def _load_albums(self):
self._toolbar.set_sensitive(False)
self._mcg.load_albums()
def _save_size(self):
if not self._maximized:
self._config.window_width = self.get_allocation().width
self._config.window_height = self.get_allocation().height
def _save_state(self, state):
self._config.window_maximized = (state.new_window_state & Gdk.WindowState.MAXIMIZED > 0)
self._maximized = (state.new_window_state & Gdk.WindowState.MAXIMIZED > 0)
def _toggle_fullscreen(self):
if not self._fullscreened:
self.fullscreen()
else:
self.unfullscreen()
def _fullscreen(self, fullscreened_new):
if fullscreened_new != self._fullscreened:
self._fullscreened = fullscreened_new
if self._fullscreened:
self._toolbar.hide()
self._cover_panel.set_fullscreen_mode(True);
else:
self._toolbar.show()
self._cover_panel.set_fullscreen_mode(False);
class ErrorDialog(Gtk.MessageDialog):
def __init__(self, parent, error):
Gtk.MessageDialog.__init__(
self,
parent,
0,
Gtk.MessageType.ERROR,
Gtk.ButtonsType.OK,
type(error).__name__
)
self.format_secondary_text(error)
self.set_modal(True)
self.connect('response', self._handle_response)
def show_dialog(self):
GObject.idle_add(self._show_dialog)
def _show_dialog(self):
self.show_all()
def _handle_response(self, *args):
self.destroy()
class Toolbar(mcg.MCGBase, Gtk.Toolbar):
SIGNAL_CONNECT = 'connect'
SIGNAL_UPDATE = 'update'
SIGNAL_PLAYPAUSE = 'playpause'
SIGNAL_LIST_MODE = 'mode'
SIGNAL_FILTER = 'filter'
SIGNAL_SORT = 'sort'
SIGNAL_SORT_TYPE = 'sort-type'
SIGNAL_GRID_SIZE_CHANGE = 'grid-size-temp'
SIGNAL_GRID_SIZE_CHANGED = 'grid-size'
def __init__(self, list_mode, item_size):
mcg.MCGBase.__init__(self)
Gtk.Toolbar.__init__(self)
# Widgets
self._connection_button = Gtk.ToolButton(Gtk.STOCK_DISCONNECT)
self.add(self._connection_button)
self.add(Gtk.SeparatorToolItem())
self._update_button = Gtk.ToolButton(Gtk.STOCK_REFRESH)
self._update_button.set_sensitive(False)
self.add(self._update_button)
self._playpause_button = Gtk.ToolButton(Gtk.STOCK_MEDIA_PLAY)
self._playpause_button.set_sensitive(False)
self.add(self._playpause_button)
self._list_mode_button = Gtk.ToggleToolButton(Gtk.STOCK_PAGE_SETUP)
self._list_mode_button.set_sensitive(False)
self.add(self._list_mode_button)
separator = Gtk.SeparatorToolItem()
separator.set_draw(False)
separator.set_expand(True)
self.add(separator)
self._filter_item = Gtk.ToolItem()
self._filter_entry = Gtk.SearchEntry()
self._filter_entry.set_sensitive(False)
self._filter_item.add(self._filter_entry)
self.add(self._filter_item)
self._grid_size_item = Gtk.ToolItem()
self._grid_size_scale = Gtk.HScale()
self._grid_size_scale.set_range(100,600)
self._grid_size_scale.set_round_digits(0)
self._grid_size_scale.set_value(item_size)
self._grid_size_scale.set_size_request(100, -1)
self._grid_size_scale.set_draw_value(False)
self._grid_size_scale.set_sensitive(False)
self._grid_size_item.add(self._grid_size_scale)
self.add(self._grid_size_item)
# Library grid menu
self._library_grid_menu = Gtk.Menu()
self._library_grid_menu.show()
menu_item = Gtk.RadioMenuItem(label="sort by artist")
menu_item.connect('activate', self.on_library_grid_menu_sort, mcg.MCGAlbum.SORT_BY_ARTIST)
menu_item.show()
library_grid_menu_group_sort = menu_item
self._library_grid_menu.add(menu_item)
menu_item = Gtk.RadioMenuItem(label="by title", group=library_grid_menu_group_sort)
menu_item.set_active(True)
menu_item.connect('activate', self.on_library_grid_menu_sort, mcg.MCGAlbum.SORT_BY_TITLE)
menu_item.show()
self._library_grid_menu.add(menu_item)
menu_item = Gtk.RadioMenuItem(label="by year", group=library_grid_menu_group_sort)
menu_item.connect('activate', self.on_library_grid_menu_sort, mcg.MCGAlbum.SORT_BY_YEAR)
menu_item.show()
self._library_grid_menu.add(menu_item)
menu_item_separator = Gtk.SeparatorMenuItem()
menu_item_separator.show()
self._library_grid_menu.add(menu_item_separator)
menu_item = Gtk.CheckMenuItem("Descending")
menu_item.connect('activate', self.on_library_grid_menu_descending)
menu_item.show()
self._library_grid_menu.add(menu_item)
self._menu_button = Gtk.MenuToolButton()
self._menu_button.set_menu(self._library_grid_menu)
self._menu_button.set_direction(Gtk.ArrowType.DOWN)
self._menu_button.set_halign(Gtk.Align.END)
self._menu_button.set_sensitive(False)
self.add(self._menu_button)
# Properties
self.get_style_context().add_class(Gtk.STYLE_CLASS_PRIMARY_TOOLBAR)
# Actions
self.set_list_mode(list_mode)
# Signals
self._connection_button.connect('clicked', self.callback_with_function, self.SIGNAL_CONNECT)
self._update_button.connect('clicked', self.callback_with_function, self.SIGNAL_UPDATE)
self._playpause_button.connect('clicked', self.callback_with_function, self.SIGNAL_PLAYPAUSE)
self._list_mode_button.connect('clicked', self.callback_with_function, self.SIGNAL_LIST_MODE)
self._filter_entry.connect('changed', self.callback_with_function, self.SIGNAL_FILTER, self._filter_entry.get_text)
self._grid_size_scale.connect('change-value', self.on_grid_size_change)
self._grid_size_scale.connect('button-release-event', self.on_grid_size_changed)
def on_grid_size_change(self, widget, scroll, value):
value = round(value)
range = self._grid_size_scale.get_adjustment()
if value < range.get_lower() or value > range.get_upper():
return
self._callback(self.SIGNAL_GRID_SIZE_CHANGE, value)
def on_grid_size_changed(self, widget, event):
value = round(self._grid_size_scale.get_value())
range = self._grid_size_scale.get_adjustment()
if value < range.get_lower() or value > range.get_upper():
return
self._callback(self.SIGNAL_GRID_SIZE_CHANGED, value)
def on_library_grid_menu_sort(self, widget, sort_order):
self._callback(self.SIGNAL_SORT, sort_order)
def on_library_grid_menu_descending(self, widget):
if widget.get_active():
self._callback(self.SIGNAL_SORT_TYPE, Gtk.SortType.DESCENDING)
else:
self._callback(self.SIGNAL_SORT_TYPE, Gtk.SortType.ASCENDING)
def connected(self):
self._connection_button.set_stock_id(Gtk.STOCK_CONNECT)
self._update_button.set_sensitive(True)
self._playpause_button.set_sensitive(True)
self._list_mode_button.set_sensitive(True)
self._filter_entry.set_sensitive(True)
self._grid_size_scale.set_sensitive(True)
self._menu_button.set_sensitive(True)
def disconnected(self):
self._connection_button.set_stock_id(Gtk.STOCK_DISCONNECT)
self._update_button.set_sensitive(False)
self._playpause_button.set_sensitive(False)
self._list_mode_button.set_sensitive(False)
self._filter_entry.set_sensitive(False)
self._grid_size_scale.set_sensitive(False)
self._menu_button.set_sensitive(False)
def set_play(self):
self._playpause_button.set_stock_id(Gtk.STOCK_MEDIA_PLAY)
def set_pause(self):
self._playpause_button.set_stock_id(Gtk.STOCK_MEDIA_PAUSE)
def set_list_mode(self, active):
self._list_mode_button.set_active(active)
def get_list_mode(self):
return self._list_mode_button.get_active()
def callback_with_function(self, widget, signal, data_function=None):
data = []
if data_function is not None:
data = {data_function()}
self._callback(signal, *data)
class ConnectionPanel(mcg.MCGBase, Gtk.Box):
SIGNAL_PROFILE_CHANGED = 'change-profile'
TAG_AUTOCONNECT = 'autoconnect'
def __init__(self, config):
mcg.MCGBase.__init__(self)
Gtk.HBox.__init__(self)
self._config = mcg.MCGProfileConfig()
self._profiles = Gtk.ListStore(str)
self._profile = None
# Widgets
vbox = Gtk.VBox()
self.pack_start(vbox, True, False, 0)
self._table = Gtk.Table(6, 2, False)
vbox.pack_start(self._table, True, False, 0)
# Profile
profile_box = Gtk.HBox()
self._table.attach(profile_box, 0, 2, 0, 1)
# Profile Selection
self._profile_combo = Gtk.ComboBox.new_with_model(self._profiles)
self._profile_combo.set_entry_text_column(0)
self._profile_combo.connect("changed", self.on_profile_combo_changed)
renderer = Gtk.CellRendererText()
self._profile_combo.pack_start(renderer, True)
self._profile_combo.add_attribute(renderer, "text", 0)
profile_box.pack_start(self._profile_combo, True, True, 0)
# Profile Management
profile_button_box = Gtk.HBox()
profile_box.pack_end(profile_button_box, False, True, 0)
# New Profile
self._profile_new_button = Gtk.Button()
self._profile_new_button.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.BUTTON))
self._profile_new_button.connect('clicked', self.on_profile_new_clicked)
profile_button_box.add(self._profile_new_button)
# Delete Profile
self._profile_delete_button = Gtk.Button()
self._profile_delete_button.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_DELETE, Gtk.IconSize.BUTTON))
self._profile_delete_button.connect('clicked', self.on_profile_delete_clicked)
profile_button_box.add(self._profile_delete_button)
# Host
host_label = Gtk.Label("Host:")
host_label.set_alignment(0, 0.5)
self._table.attach(host_label, 0, 1, 1, 2)
self._host_entry = Gtk.Entry()
self._host_entry.set_text("localhost")
self._table.attach(self._host_entry, 1, 2, 1, 2)
# Port
port_label = Gtk.Label("Port:")
port_label.set_alignment(0, 0.5)
self._table.attach(port_label, 0, 1, 2, 3)
adjustment = Gtk.Adjustment(6600, 1024, 9999, 1, 10, 10)
self._port_spinner = Gtk.SpinButton()
self._port_spinner.set_adjustment(adjustment)
self._table.attach(self._port_spinner, 1, 2, 2, 3)
# Passwort
password_label = Gtk.Label("Password:")
password_label.set_alignment(0, 0.5)
self._table.attach(password_label, 0, 1, 3, 4)
self._password_entry = Gtk.Entry()
self._table.attach(self._password_entry, 1, 2, 3, 4)
# Image dir
image_dir_label = Gtk.Label("Image Dir:")
image_dir_label.set_alignment(0, 0.5)
self._table.attach(image_dir_label, 0, 1, 4, 5)
self._image_dir_entry = Gtk.Entry()
self._table.attach(self._image_dir_entry, 1, 2, 4, 5)
# Autoconnect
self._autoconnect_button = Gtk.CheckButton("Autoconnect")
self._table.attach(self._autoconnect_button, 1, 2, 5, 6)
# Signals
self._host_entry.connect('focus-out-event', self.on_host_entry_outfocused)
self._port_spinner.connect('value-changed', self.on_port_spinner_value_changed)
self._password_entry.connect('focus-out-event', self.on_password_entry_outfocused)
self._image_dir_entry.connect('focus-out-event', self.on_image_dir_entry_outfocused)
self._autoconnect_button.connect('toggled', self.on_autoconnect_button_toggled)
# Actions
self._load_config()
GObject.idle_add(self._select_last_profile, config.last_profile)
def on_profile_combo_changed(self, combo):
(index, profile) = self._get_selected_profile()
if profile is not None:
self._profile = profile
self.set_host(self._profile.get('host'))
self.set_port(int(self._profile.get('port')))
self.set_password(self._profile.get('password'))
self.set_image_dir(self._profile.get('image_dir'))
self._autoconnect_button.set_active(ConnectionPanel.TAG_AUTOCONNECT in self._profile.get_tags())
self._callback(ConnectionPanel.SIGNAL_PROFILE_CHANGED, index, self._profile)
def on_profile_new_clicked(self, widget):
profile = mcg.MCGProfile()
self._config.add_profile(profile)
self._reload_config()
self._profile_combo.set_active(len(self._profiles)-1)
def on_profile_delete_clicked(self, widget):
(index, profile) = self._get_selected_profile()
if profile is not None:
self._config.delete_profile(profile)
self._reload_config()
self._profile_combo.set_active(0)
def on_host_entry_outfocused(self, widget, event):
self._profile.set('host', widget.get_text())
self._profiles.set(self._profile_combo.get_active_iter(), 0, widget.get_text())
def on_port_spinner_value_changed(self, widget):
self._profile.set('port', self.get_port())
def on_password_entry_outfocused(self, widget, event):
self._profile.set('password', widget.get_text())
def on_image_dir_entry_outfocused(self, widget, event):
self._profile.set('image_dir', widget.get_text())
def on_autoconnect_button_toggled(self, widget):
tags = self._profile.get_tags()
if widget.get_active():
if ConnectionPanel.TAG_AUTOCONNECT not in tags:
tags.append(ConnectionPanel.TAG_AUTOCONNECT)
else:
if ConnectionPanel.TAG_AUTOCONNECT in tags:
tags.remove(ConnectionPanel.TAG_AUTOCONNECT)
self._profile.set_tags(tags)
def save_profiles(self):
self._config.save()
def set_host(self, host):
self._host_entry.set_text(host)
def get_host(self):
return self._host_entry.get_text()
def set_port(self, port):
self._port_spinner.set_value(port)
def get_port(self):
return self._port_spinner.get_value_as_int()
def set_password(self, password):
if password is None:
password = ""
self._password_entry.set_text(password)
def get_password(self):
if self._password_entry.get_text() == "":
return None
else:
return self._password_entry.get_text()
def set_image_dir(self, image_dir):
self._image_dir_entry.set_text(image_dir)
def get_image_dir(self):
return self._image_dir_entry.get_text()
def _load_config(self):
self._config.load()
for profile in self._config.get_profiles():
self._profiles.append([profile.__str__()])
def _reload_config(self):
self._profiles.clear()
for profile in self._config.get_profiles():
self._profiles.append([profile.__str__()])
def set_sensitive(self, sensitive):
self._table.set_sensitive(sensitive)
def _get_selected_profile(self):
index = self._profile_combo.get_active()
if index >= 0:
profiles = self._config.get_profiles()
if index < len(profiles):
return (index, profiles[index])
return (-1, None)
def _select_last_profile(self, index):
if len(self._profiles) <= index:
index = 0
self._profile_combo.set_active(index)
class CoverPanel(mcg.MCGBase, Gtk.HPaned):
SIGNAL_ALBUMS_SET = 'albums-set'
SIGNAL_TOGGLE_FULLSCREEN = 'toggle-fullscreen'
SIGNAL_PLAY = 'play'
MODE_GRID = 'grid'
MODE_LIST = 'list'
MODE_PROGRESS = 'progress'
MODE_FULLSCREEN = 'fullscreen'
def __init__(self, config):
mcg.MCGBase.__init__(self)
Gtk.HPaned.__init__(self)
self._config = config
self._mode = None
self._cache = None
self._current_album = None
self._cover_pixbuf = None
self._host = None
self._albums = []
self._playlist = []
self._grid_pixbufs = {}
self._filter_string = ""
self._old_ranges = {}
self._cover_background_color = None
# Widgets
self._current = Gtk.VPaned()
# Cover
self._cover_image = Gtk.Image()
self._cover_box = Gtk.EventBox()
self._cover_background_color = self._cover_box.get_style_context().get_background_color(Gtk.StateFlags.NORMAL)
self._cover_box.add(self._cover_image)
self._cover_scroll = Gtk.ScrolledWindow()
self._cover_scroll.add_with_viewport(self._cover_box)
# Playlist
self._playlist_scroll = Gtk.ScrolledWindow()
# Playlist: GridModel
self._playlist_grid_model = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str, str)
self._playlist_grid_filter = self._playlist_grid_model.filter_new()
# Playlist: GridView
self._playlist_grid = Gtk.IconView(self._playlist_grid_filter)
self._playlist_grid.set_pixbuf_column(0)
self._playlist_grid.set_text_column(-1)
self._playlist_grid.set_tooltip_column(2)
self._playlist_grid.set_columns(-1)
self._playlist_grid.set_margin(0)
self._playlist_grid.set_spacing( 0)
self._playlist_grid.set_row_spacing(0)
self._playlist_grid.set_column_spacing(0)
self._playlist_grid.set_item_padding(0)
self._playlist_grid.set_reorderable(False)
self._playlist_grid.set_selection_mode(Gtk.SelectionMode.SINGLE)
# Playlist: ListModel
self._playlist_list_model = Gtk.ListStore(str, str, str, str, str, str)
# Playlist: ListView
self._playlist_list = Gtk.TreeView(self._playlist_list_model)
renderer = Gtk.CellRendererText()
column_artist = Gtk.TreeViewColumn("Artist", renderer, text=0)
column_album = Gtk.TreeViewColumn("Album", renderer, text=1)
column_track = Gtk.TreeViewColumn("Track", renderer, text=2)
column_title = Gtk.TreeViewColumn("Title", renderer, text=3)
column_date = Gtk.TreeViewColumn("Year", renderer, text=4)
self._playlist_list.append_column(column_artist)
self._playlist_list.append_column(column_album)
self._playlist_list.append_column(column_track)
self._playlist_list.append_column(column_title)
self._playlist_list.append_column(column_date)
# Library
self._library_scroll = Gtk.ScrolledWindow()
# Library: GridModel
self._library_grid_model = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str, str)
self._library_grid_model.set_sort_func(3, self.compare_albums, mcg.MCGAlbum.SORT_BY_TITLE)
self._library_grid_model.set_sort_column_id(3, Gtk.SortType.ASCENDING)
self._library_grid_filter = self._library_grid_model.filter_new()
self._library_grid_filter.set_visible_func(self.on_filter_visible)
# Library: GridView
self._library_grid = Gtk.IconView(self._library_grid_filter)
self._library_grid.set_pixbuf_column(0)
self._library_grid.set_text_column(-1)
self._library_grid.set_tooltip_column(2)
self._library_grid.set_columns(-1)
self._library_grid.set_margin(0)
self._library_grid.set_spacing( 0)
self._library_grid.set_row_spacing(0)
self._library_grid.set_column_spacing(0)
self._library_grid.set_item_padding(0)
self._library_grid.set_reorderable(False)
self._library_grid.set_selection_mode(Gtk.SelectionMode.SINGLE)
# Library: ListModel
self._library_list_model = Gtk.ListStore(str, str, str, str, str, str)
self._library_list_filter = self._library_list_model.filter_new()
self._library_list_filter.set_visible_func(self.on_filter_visible)
# Library: ListView
self._library_list = Gtk.TreeView(self._library_list_filter)
renderer = Gtk.CellRendererText()
column_artist = Gtk.TreeViewColumn("Artist", renderer, text=0)
column_album = Gtk.TreeViewColumn("Album", renderer, text=1)
column_track = Gtk.TreeViewColumn("Track", renderer, text=2)
column_title = Gtk.TreeViewColumn("Title", renderer, text=3)
column_date = Gtk.TreeViewColumn("Year", renderer, text=4)
self._library_list.append_column(column_artist)
self._library_list.append_column(column_album)
self._library_list.append_column(column_track)
self._library_list.append_column(column_title)
self._library_list.append_column(column_date)
# Progress Bar
self._progress_bar = Gtk.ProgressBar()
# Layout
self.pack1(self._current, True, True)
self._current.pack1(self._cover_scroll, True, True)
self._current.pack2(self._playlist_scroll, False, False)
self.pack2(self._library_scroll, False, False)
# Actions
self.set_list_mode(self._config.list_mode)
self.set_position(self._config.library_position)
self._current.set_position(self._config.playlist_position)
# Signals
self.connect('size-allocate', self.on_size_allocate)
self._current.connect('size-allocate', self.on_size_allocate)
self._cover_scroll.connect('size-allocate', self.on_cover_size_allocate)
self._cover_box.connect('button-press-event', self.on_cover_box_pressed)
self._library_grid.connect('item-activated', self.on_library_grid_clicked)
def on_size_allocate(self, widget, allocation):
if widget is self:
self._config.library_position = self.get_position()
elif widget is self._current:
self._config.playlist_position = self._current.get_position()
def on_cover_size_allocate(self, widget, allocation):
self._resize_image()
def on_cover_box_pressed(self, widget, event):
if event.type == Gdk.EventType._2BUTTON_PRESS:
self._callback(self.SIGNAL_TOGGLE_FULLSCREEN)
def on_library_grid_clicked(self, widget, path):
path = self._library_grid_filter.convert_path_to_child_path(path)
iter = self._library_grid_model.get_iter(path)
self._callback(self.SIGNAL_PLAY, self._library_grid_model.get_value(iter, 3))
def on_filter_visible(self, model, iter, data):
if model is self._library_grid_model:
hash = model.get_value(iter, 3)
elif model is self._library_list_model:
hash = model.get_value(iter, 5)
if not hash in self._albums.keys():
return
album = self._albums[hash]
return album.filter(self._filter_string)
def set_list_mode(self, active):
mode = CoverPanel.MODE_GRID
if active:
mode = CoverPanel.MODE_LIST
self.set_mode(mode)
def set_fullscreen_mode(self, active):
mode = CoverPanel.MODE_FULLSCREEN
if not active:
mode = self._mode
self._set_mode(mode)
def set_mode(self, mode):
if mode != self._mode:
self._mode = mode
GObject.idle_add(self._set_mode, mode)
def set_album(self, album):
if self._current_album is not None and album.get_hash() == self._current_album.get_hash():
return
self._current_album = album
url = album.get_cover()
if url is not None and url is not "":
# Load image and draw it
self._cover_pixbuf = self._load_cover(url)
self._resize_image()
else:
# Reset image
self._cover_pixbuf = None
self._cover_image.clear()
def set_playlist(self, host, playlist):
self._host = host
self._playlist = playlist
Thread(target=self._set_playlist, args=(host, playlist, self._config.item_size,)).start()
def set_albums(self, host, albums):
self._host = host
self._albums = albums
Thread(target=self._set_albums, args=(host, albums, self._config.item_size,)).start()
def filter(self, filter_string):
self._filter_string = filter_string
GObject.idle_add(self._library_grid_filter.refilter)
GObject.idle_add(self._library_list_filter.refilter)
def set_sort_order(self, sort_order):
self._library_grid_model.set_sort_func(3, self.compare_albums, sort_order)
def set_sort_type(self, sort_type):
self._library_grid_model.set_sort_column_id(3, sort_type)
def set_grid_size(self, size):
self._config.item_width = size
GObject.idle_add(self._set_grid_size, size)
def _set_grid_size(self, size):
self._set_widget_grid_size(self._playlist_grid, size, False)
self._set_widget_grid_size(self._library_grid, size, True)
def _set_widget_grid_size(self, grid_widget, size, vertical):
grid_filter = grid_widget.get_model()
grid_model = grid_filter.get_model()
# get old_range
grid_widget_id = id(grid_widget)
if grid_widget_id not in self._old_ranges or self._old_ranges[grid_widget_id] is None:
self._old_ranges[grid_widget_id] = range(0, len(grid_filter))
old_range = self._old_ranges[grid_widget_id]
old_start = len(old_range) > 0 and old_range[0] or 0
old_end = len(old_range) > 0 and old_range[len(old_range)-1] + 1 or 0
# calculate visible range
w = (grid_widget.get_allocation().width // size) + (vertical and 0 or 1)
h = (grid_widget.get_allocation().height // size) + (vertical and 1 or 0)
c = w * h
vis_range = grid_widget.get_visible_range()
if vis_range is None:
return
(vis_start,), (vis_end,) = vis_range
vis_end = min(vis_start + c, len(grid_filter))
vis_range = range(vis_start, vis_end)
# set pixbuf
cur_start = min(old_start, vis_start)
cur_end = max(old_end, vis_end)
cur_range = range(cur_start, cur_end)
for index in cur_range:
iter = grid_filter.convert_iter_to_child_iter(grid_filter[index].iter)
if index in vis_range:
hash = grid_model.get_value(iter, 3)
pixbuf = self._grid_pixbufs[hash]
pixbuf = pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.NEAREST)
else:
pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, False, 8, 1, 1)
grid_model.set_value(iter, 0, pixbuf)
self._old_ranges[grid_widget_id] = vis_range
grid_widget.set_item_width(size)
self._config.item_size = size
def redraw(self):
Thread(target=self._set_playlist_and_albums, args=(self._host, self._playlist, self._albums, self._config.item_size,)).start()
def compare_albums(self, model, row1, row2, criterion):
hash1 = model.get_value(row1, 3)
hash2 = model.get_value(row2, 3)
if hash1 == "" or hash2 == "":
return
return mcg.MCGAlbum.compare(self._albums[hash1], self._albums[hash2], criterion)
def _set_mode(self, mode):
# Layout
if len(self.get_children()) > 1:
self.remove(self.get_children()[1])
if len(self._current.get_children()) > 1:
self._current.remove(self._current.get_children()[1])
if mode != CoverPanel.MODE_FULLSCREEN:
self._current.pack2(self._playlist_scroll, False, False)
self.pack2(self._library_scroll, False, False)
# Scroll content
if self._playlist_scroll.get_child() is not None:
self._playlist_scroll.remove(self._playlist_scroll.get_child())
if self._library_scroll.get_child() is not None:
self._library_scroll.remove(self._library_scroll.get_child())
if mode == CoverPanel.MODE_GRID:
self._playlist_scroll.add(self._playlist_grid)
self._playlist_scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.NEVER)
self._library_scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
self._library_scroll.add(self._library_grid)
elif mode == CoverPanel.MODE_LIST:
self._playlist_scroll.add(self._playlist_list)
self._playlist_scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
self._library_scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
self._library_scroll.add(self._library_list)
elif mode == CoverPanel.MODE_PROGRESS:
self._playlist_scroll.add(self._playlist_grid)
self._library_scroll.add_with_viewport(self._progress_bar)
elif mode == CoverPanel.MODE_FULLSCREEN:
self._library_scroll.hide()
# Cover background
if mode == CoverPanel.MODE_FULLSCREEN:
self._cover_box.override_background_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 1))
else:
self._cover_box.override_background_color(Gtk.StateFlags.NORMAL, self._cover_background_color)
self.show_all()
def _set_playlist_and_albums(self, host, playlist, albums, size):
self._set_playlist(host, playlist, size)
self._set_albums(host, albums, size)
def _set_playlist(self, host, playlist, size):
Gdk.threads_enter()
self._playlist_grid.set_model(None)
self._playlist_list.set_model(None)
self._playlist_grid.freeze_child_notify()
self._playlist_list.freeze_child_notify()
self._playlist_grid_model.clear()
self._playlist_list_model.clear()
Gdk.threads_leave()
cache = mcg.MCGCache(host, size)
for album in playlist:
for track in album.get_tracks():
self._playlist_list_model.append([
', '.join(track.get_artists()),
album.get_title(),
track.get_track(),
track.get_title(),
album.get_date(),
album.get_hash()
])
pixbuf = None
if album.get_cover() is not None:
try:
pixbuf = self._load_thumbnail(cache, album, size)
except Exception as e:
print(e)
if pixbuf is None:
pixbuf = self._playlist_grid.render_icon_pixbuf(Gtk.STOCK_MISSING_IMAGE, Gtk.IconSize.DIALOG)
if pixbuf is not None:
self._playlist_grid_model.append([
pixbuf,
album.get_title(),
GObject.markup_escape_text("\n".join([
album.get_title(),
album.get_date(),
', '.join(album.get_artists())
])),
album.get_hash()
])
Gdk.threads_enter()
self._playlist_grid.set_model(self._playlist_grid_filter)
self._playlist_list.set_model(self._playlist_list_model)
self._playlist_grid.thaw_child_notify()
self._playlist_list.thaw_child_notify()
self._playlist_grid.set_columns(len(playlist))
Gdk.threads_leave()
def _set_albums(self, host, albums, size):
Gdk.threads_enter()
self._library_grid.set_model(None)
self._library_list.set_model(None)
self._library_grid.freeze_child_notify()
self._library_list.freeze_child_notify()
self._library_grid_model.clear()
self._library_list_model.clear()
self._progress_bar.set_fraction(0.0)
self._set_mode(CoverPanel.MODE_PROGRESS)
Gdk.threads_leave()
i = 0
n = len(albums)
cache = mcg.MCGCache(host, size)
self._grid_pixbufs.clear()
for hash in albums.keys():
album = albums[hash]
pixbuf = None
for track in album.get_tracks():
self._library_list_model.append([
', '.join(track.get_artists()),
album.get_title(),
track.get_track(),
track.get_title(),
album.get_date(),
hash
])
try:
pixbuf = self._load_thumbnail(cache, album, size)
except Exception as e:
print(e)
if pixbuf is None:
pixbuf = self._library_grid.render_icon_pixbuf(Gtk.STOCK_MISSING_IMAGE, Gtk.IconSize.DIALOG)
if pixbuf is not None:
self._grid_pixbufs[album.get_hash()] = pixbuf
self._library_grid_model.append([
pixbuf,
album.get_title(),
GObject.markup_escape_text("\n".join([
album.get_title(),
album.get_date(),
', '.join(album.get_artists())
])),
hash
])
i += 1
GObject.idle_add(self._progress_bar.set_fraction, i/n)
Gdk.threads_enter()
self._library_grid.set_model(self._library_grid_filter)
self._library_list.set_model(self._library_list_filter)
self._library_grid.thaw_child_notify()
self._library_list.thaw_child_notify()
self._set_mode(self._mode)
Gdk.threads_leave()
self._callback(self.SIGNAL_ALBUMS_SET)
def _load_cover(self, url):
if url.startswith('/'):
try:
return GdkPixbuf.Pixbuf.new_from_file(url)
except Exception as e:
print(e)
return None
else:
try:
response = urllib.request.urlopen(url)
loader = GdkPixbuf.PixbufLoader()
loader.write(response.read())
loader.close()
return loader.get_pixbuf()
except Exception as e:
print(e)
return None
def _load_thumbnail(self, cache, album, size):
cache_url = cache.create_filename(album)
pixbuf = None
if os.path.isfile(cache_url):
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_url)
except Exception as e:
print(e)
else:
url = album.get_cover()
if url is not None:
if url.startswith('/'):
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(url, size, size)
except Exception as e:
print(e)
else:
try:
response = urllib.request.urlopen(url)
loader = GdkPixbuf.PixbufLoader()
loader.write(response.read())
loader.close()
pixbuf = loader.get_pixbuf().scale_simple(size, size, GdkPixbuf.InterpType.HYPER)
except Exception as e:
print(e)
if pixbuf is not None:
filetype = os.path.splitext(url)[1][1:]
if filetype == 'jpg':
filetype = 'jpeg'
pixbuf.savev(cache.create_filename(album), filetype, [], [])
return pixbuf
def _resize_image(self):
"""Diese Methode skaliert das geladene Bild aus dem Pixelpuffer
auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse
"""
pixbuf = self._cover_pixbuf
size = self._cover_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(round(pixbuf.get_width()*ratio))
height = int(round(pixbuf.get_height()*ratio))
# Pixelpuffer auf Oberfläche zeichnen
self._cover_image.set_from_pixbuf(pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
class Configuration(mcg.MCGConfig):
CONFIG_FILE = 'mcggtk.conf'
def __init__(self):
mcg.MCGConfig.__init__(self, Configuration.CONFIG_FILE)
self._setup()
self.load()
def load(self):
super().load()
self.last_profile = self.getint('default', 'last-profile')
self.window_width = self.getint('gui', 'window-width')
self.window_height = self.getint('gui', 'window-height')
self.window_maximized = self.getboolean('gui', 'window-maximized')
self.item_size = self.getint('gui', 'item-size')
self.list_mode = self.getboolean('gui', 'list-mode')
self.library_position = self.getint('gui', 'library-position')
self.playlist_position = self.getint('gui', 'playlist-position')
# TODO sort order
# TODO sort type
def save(self):
self.set('default', 'last-profile', str(self.last_profile))
self.set('gui', 'window-width', str(self.window_width))
self.set('gui', 'window-height', str(self.window_height))
self.set('gui', 'window-maximized', str(self.window_maximized))
self.set('gui', 'item-size', str(self.item_size))
self.set('gui', 'list-mode', str(self.list_mode))
self.set('gui', 'library-position', str(self.library_position))
self.set('gui', 'playlist-position', str(self.playlist_position))
super().save()
def _setup(self):
if not self.has_section('default'):
self.add_section('default')
self.set('default', 'last-profile', str(0))
if not self.has_section('gui'):
self.add_section('gui')
self.set('gui', 'window-width', str(800))
self.set('gui', 'window-height', str(600))
self.set('gui', 'window-maximized', str(False))
self.set('gui', 'item-size', str(100))
self.set('gui', 'list-mode', str(False))
self.set('gui', 'library-position', str(450))
self.set('gui', 'playlist-position', str(450))
if __name__ == "__main__":
GObject.threads_init()
Gdk.threads_init()
mcgg = MCGGtk()
mcgg.show_all()
try:
Gtk.main()
except (KeyboardInterrupt, SystemExit):
pass