Arquivos
coverart-browser/coverart_widgets.py
T

1365 linhas
43 KiB
Python

# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
#
# Copyright (C) 2012 - fossfreedom
# Copyright (C) 2012 - Agustin Carrasco
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
from gi.repository import RB
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gio
import cairo
from coverart_browser_prefs import GSetting
import rb
def enum(**enums):
return type('Enum', (object,), enums)
class OptionsWidget(Gtk.Widget):
def __init__(self, *args, **kwargs):
super(OptionsWidget, self).__init__(*args, **kwargs)
self._controller = None
@property
def controller(self):
return self._controller
@controller.setter
def controller(self, controller):
if self._controller:
# disconnect signals
self._controller.disconnect(self._options_changed_id)
self._controller.disconnect(self._current_key_changed_id)
self._controller.disconnect(self._update_image_changed_id)
self._controller = controller
# connect signals
self._options_changed_id = self._controller.connect('notify::options',
self._update_options)
self._current_key_changed_id = self._controller.connect(
'notify::current-key', self._update_current_key)
self._update_image_changed_id = self._controller.connect(
'notify::update-image', self._update_image)
self._visible_changed_id = self._controller.connect(
'notify::enabled', self._update_visibility)
# update the menu and current key
self.update_options()
self.update_current_key()
def _update_visibility(self, *args):
self.set_visible(self._controller.enabled)
def _update_options(self, *args):
self.update_options()
def update_options(self):
pass
def _update_current_key(self, *args):
self.update_current_key()
def update_current_key():
pass
def _update_image(self, *args):
self.update_image()
def update_image(self):
pass
def calc_popup_position(self, widget):
# this calculates the popup positioning - algorithm taken
# from Gtk3.8 gtk/gtkmenubutton.c
toplevel = self.get_toplevel()
toplevel.set_type_hint(Gdk.WindowTypeHint.DROPDOWN_MENU)
menu_req, pref_req = widget.get_preferred_size()
align = widget.get_halign()
direction = self.get_direction()
window = self.get_window()
screen = widget.get_screen()
monitor_num = screen.get_monitor_at_window(window)
if (monitor_num < 0):
monitor_num = 0
monitor = screen.get_monitor_workarea(monitor_num)
allocation = self.get_allocation()
ret, x, y = window.get_origin()
x += allocation.x
y += allocation.y
if allocation.width - menu_req.width > 0:
x += allocation.width - menu_req.width
if ((y + allocation.height + menu_req.height) <= monitor.y + monitor.height):
y += allocation.height
elif ((y - menu_req.height) >= monitor.y):
y -= menu_req.height
else:
y -= menu_req.height
return x, y
class OptionsPopupWidget(OptionsWidget):
# signals
__gsignals__ = {
'item-clicked': (GObject.SIGNAL_RUN_LAST, None, (str,))
}
def __init__(self, *args, **kwargs):
OptionsWidget.__init__(self, *args, **kwargs)
self._popup_menu = Gtk.Menu()
def update_options(self):
self.clear_popupmenu()
for key in self._controller.options:
self.add_menuitem(key)
def update_current_key(self):
# select the item if it isn't already
item = self.get_menuitems()[self._controller.get_current_key_index()]
if not item.get_active():
item.set_active(True)
def add_menuitem(self, label):
'''
add a new menu item to the popup
'''
if not self._first_menu_item:
new_menu_item = Gtk.RadioMenuItem(label=label)
self._first_menu_item = new_menu_item
else:
new_menu_item = Gtk.RadioMenuItem.new_with_label_from_widget(
group=self._first_menu_item, label=label)
new_menu_item.connect('toggled', self._fire_item_clicked)
new_menu_item.show()
self._popup_menu.append(new_menu_item)
def get_menuitems(self):
return self._popup_menu.get_children()
def clear_popupmenu(self):
'''
reinitialises/clears the current popup menu and associated actions
'''
for menu_item in self._popup_menu:
self._popup_menu.remove(menu_item)
self._first_menu_item = None
def _fire_item_clicked(self, menu_item):
'''
Fires the item-clicked signal if the item is selected, passing the
given value as a parameter. Also updates the current value with the
value of the selected item.
'''
if menu_item.get_active():
self.emit('item-clicked', menu_item.get_label())
def do_item_clicked(self, key):
if self._controller:
# inform the controller
self._controller.option_selected(key)
def _popup_callback(self, *args):
x, y = self.calc_popup_position(self._popup_menu)
return x, y, False, None
def show_popup(self, align=True):
'''
show the current popup menu
'''
if align:
self._popup_menu.popup(None, None, self._popup_callback, self, 0,
Gtk.get_current_event_time())
else:
self._popup_menu.popup(None, None, None, None, 0,
Gtk.get_current_event_time())
def do_delete_thyself(self):
self.clear_popupmenu()
del self._popupmenu
class EnhancedButton(Gtk.ToggleButton):
button_relief = GObject.property(type=bool, default=False)
def __init__(self, *args, **kwargs):
super(EnhancedButton, self).__init__(*args, **kwargs)
gs = GSetting()
setting = gs.get_setting(gs.Path.PLUGIN)
setting.bind(gs.PluginKey.BUTTON_RELIEF, self,
'button_relief', Gio.SettingsBindFlags.GET)
self.connect('notify::button-relief',
self.on_notify_button_relief)
def on_notify_button_relief(self, *arg):
if self.button_relief:
self.set_relief(Gtk.ReliefStyle.NONE)
else:
self.set_relief(Gtk.ReliefStyle.HALF)
class PixbufButton(EnhancedButton):
button_relief = GObject.property(type=bool, default=False)
def __init__(self, *args, **kwargs):
super(PixbufButton, self).__init__(*args, **kwargs)
def set_image(self, pixbuf):
image = self.get_image()
if not image:
image = Gtk.Image()
super(PixbufButton, self).set_image(image)
if hasattr(self, "controller.enabled") and not self.controller.enabled:
pixbuf = self._getBlendedPixbuf(pixbuf)
self.get_image().set_from_pixbuf(pixbuf)
self.on_notify_button_relief()
def _getBlendedPixbuf(self, pixbuf):
"""Turn a pixbuf into a blended version of the pixbuf by drawing a
transparent alpha blend on it."""
pixbuf = pixbuf.copy()
w, h = pixbuf.get_width(), pixbuf.get_height()
surface = cairo.ImageSurface(
cairo.FORMAT_ARGB32, pixbuf.get_width(), pixbuf.get_height())
context = cairo.Context(surface)
Gdk.cairo_set_source_pixbuf(context, pixbuf, 0, 0)
context.paint()
context.set_source_rgba(32, 32, 32, 0.4)
context.set_line_width(0)
context.rectangle(0, 0, w, h)
context.fill()
pixbuf = Gdk.pixbuf_get_from_surface(surface, 0, 0, w, h)
return pixbuf
class PopupButton(PixbufButton, OptionsPopupWidget):
__gtype_name__ = "PopupButton"
# signals
__gsignals__ = {
'item-clicked': (GObject.SIGNAL_RUN_LAST, None, (str,))
}
def __init__(self, *args, **kwargs):
'''
Initializes the button.
'''
PixbufButton.__init__(self, *args, **kwargs)
OptionsPopupWidget.__init__(self, *args, **kwargs)
self._popup_menu.attach_to_widget(self, None) #critical to ensure theming works
self._popup_menu.connect('deactivate', self.popup_deactivate)
# initialise some variables
self._first_menu_item = None
def popup_deactivate(self, *args):
self.set_active(False)
def update_image(self):
super(PopupButton, self).update_image()
self.set_image(self._controller.get_current_image())
def update_current_key(self):
super(PopupButton, self).update_current_key()
# update the current image and tooltip
self.set_image(self._controller.get_current_image())
self.set_tooltip_text(self._controller.get_current_description())
def do_button_press_event(self, event):
'''
when button is clicked, update the popup with the sorting options
before displaying the popup
'''
if (event.button == Gdk.BUTTON_PRIMARY):
self.show_popup()
self.set_active(True)
class TextPopupButton(EnhancedButton, OptionsPopupWidget):
__gtype_name__ = "TextPopupButton"
# signals
__gsignals__ = {
'item-clicked': (GObject.SIGNAL_RUN_LAST, None, (str,))
}
def __init__(self, *args, **kwargs):
'''
Initializes the button.
'''
EnhancedButton.__init__(self, *args, **kwargs)
OptionsPopupWidget.__init__(self, *args, **kwargs)
self._popup_menu.attach_to_widget(self, None) #critical to ensure theming works
self._popup_menu.connect('deactivate', self.popup_deactivate)
# initialise some variables
self._first_menu_item = None
def popup_deactivate(self, *args):
self.set_active(False)
def do_button_press_event(self, event):
'''
when button is clicked, update the popup with the sorting options
before displaying the popup
'''
if (event.button == Gdk.BUTTON_PRIMARY):
self.show_popup()
self.set_active(True)
class MenuButton(PixbufButton, OptionsPopupWidget):
__gtype_name__ = "MenuButton"
# signals
__gsignals__ = {
'item-clicked': (GObject.SIGNAL_RUN_LAST, None, (str,))
}
def __init__(self, *args, **kwargs):
'''
Initializes the button.
'''
PixbufButton.__init__(self, *args, **kwargs)
OptionsPopupWidget.__init__(self, *args, **kwargs)
self._popup_menu.attach_to_widget(self, None) #critical to ensure theming works
self._popup_menu.connect('deactivate', self.popup_deactivate)
self._states = {}
def popup_deactivate(self, *args):
self.set_active(False)
def add_menuitem(self, key):
'''
add a new menu item to the popup
'''
label = key.label
menutype = key.menutype
typevalue = key.typevalue
if menutype and menutype == 'separator':
new_menu_item = Gtk.SeparatorMenuItem().new()
elif menutype and menutype == 'check':
new_menu_item = Gtk.CheckMenuItem(label=label)
new_menu_item.set_active(typevalue)
new_menu_item.connect('toggled', self._fire_item_clicked)
else:
new_menu_item = Gtk.MenuItem(label=label)
new_menu_item.connect('activate', self._fire_item_clicked)
new_menu_item.show()
self._popup_menu.append(new_menu_item)
def clear_popupmenu(self):
'''
reinitialises/clears the current popup menu and associated actions
'''
for menu_item in self._popup_menu:
if isinstance(menu_item, Gtk.CheckMenuItem):
self._states[menu_item.get_label()] = menu_item.get_active()
self._popup_menu.remove(menu_item)
self._first_menu_item = None
def update_options(self):
self.clear_popupmenu()
for key in self._controller.options:
self.add_menuitem(key)
self._states = {}
def _fire_item_clicked(self, menu_item):
'''
Fires the item-clicked signal if the item is selected, passing the
given value as a parameter. Also updates the current value with the
value of the selected item.
'''
self.emit('item-clicked', menu_item.get_label())
def update_image(self):
super(MenuButton, self).update_image()
self.set_image(self._controller.get_current_image())
def update_current_key(self):
# select the item if it isn't already
#item = self.get_menuitems()[self._controller.get_current_key_index()]
# update the current image and tooltip
self.set_image(self._controller.get_current_image())
self.set_tooltip_text(self._controller.get_current_description())
def do_button_press_event(self, event):
'''
when button is clicked, update the popup with the sorting options
before displaying the popup
'''
if (event.button == Gdk.BUTTON_PRIMARY):
self.show_popup()
self.set_active(True)
class ImageToggleButton(PixbufButton, OptionsWidget):
__gtype_name__ = "ImageToggleButton"
def __init__(self, *args, **kwargs):
'''
Initializes the button.
'''
PixbufButton.__init__(self, *args, **kwargs)
OptionsWidget.__init__(self, *args, **kwargs)
# initialise some variables
self.image_display = False
self.initialised = False
def update_image(self):
super(ImageToggleButton, self).update_image()
self.set_image(self._controller.get_current_image())
def update_current_key(self):
# update the current image and tooltip
self.set_image(self._controller.get_current_image())
self.set_tooltip_text(self._controller.get_current_description())
def do_clicked(self):
if self._controller:
index = self._controller.get_current_key_index()
index = (index + 1) % len(self._controller.options)
# inform the controller
self._controller.option_selected(
self._controller.options[index])
class ImageRadioButton(Gtk.RadioButton, OptionsWidget):
# this is legacy code that will not as yet work with
# the new toolbar - consider removing this later
__gtype_name__ = "ImageRadioButton"
button_relief = GObject.property(type=bool, default=False)
def __init__(self, *args, **kwargs):
'''
Initializes the button.
'''
Gtk.RadioButton.__init__(self, *args, **kwargs)
OptionsWidget.__init__(self, *args, **kwargs)
gs = GSetting()
setting = gs.get_setting(gs.Path.PLUGIN)
setting.bind(gs.PluginKey.BUTTON_RELIEF, self,
'button_relief', Gio.SettingsBindFlags.GET)
self.connect('notify::button-relief',
self.on_notify_button_relief)
# initialise some variables
self.image_display = False
self.initialised = False
#ensure button appearance rather than standard radio toggle
self.set_mode(False)
#label colours
self._not_active_colour = None
self._active_colour = None
def update_image(self):
super(ImageRadioButton, self).update_image()
#self.set_image(self._controller.get_current_image(Gtk.Buildable.get_name(self)))
def do_toggled(self):
if self.get_active():
self.controller.option_selected(Gtk.Buildable.get_name(self))
def set_image(self, pixbuf):
image = self.get_image()
if not image:
image = Gtk.Image()
super(ImageRadioButton, self).set_image(image)
self.get_image().set_from_pixbuf(pixbuf)
self.on_notify_button_relief()
def on_notify_button_relief(self, *arg):
if self.button_relief:
self.set_relief(Gtk.ReliefStyle.NONE)
else:
self.set_relief(Gtk.ReliefStyle.HALF)
def update_current_key(self):
# update the current image and tooltip
#self.set_image(self._controller.get_current_image(Gtk.Buildable.get_name(self)))
self.set_tooltip_text("") #self._controller.get_current_description())
if self.controller.current_key == Gtk.Buildable.get_name(self):
self.set_active(True)
self._set_colour(Gtk.StateFlags.NORMAL)
else:
self._set_colour(Gtk.StateFlags.INSENSITIVE)
def _set_colour(self, state_flag):
if len(self.get_children()) == 0:
return
def get_standard_colour(label, state_flag):
context = label.get_style_context()
return context.get_color(state_flag)
label0 = self.get_children()[0]
if not self._not_active_colour:
self._not_active_colour = get_standard_colour(label0, Gtk.StateFlags.INSENSITIVE)
if not self._active_colour:
self._active_colour = get_standard_colour(label0, Gtk.StateFlags.NORMAL)
if state_flag == Gtk.StateFlags.INSENSITIVE:
label0.override_color(Gtk.StateType.NORMAL, self._not_active_colour)
else:
label0.override_color(Gtk.StateType.NORMAL, self._active_colour)
class SearchEntry(RB.SearchEntry, OptionsPopupWidget):
__gtype_name__ = "SearchEntry"
# signals
__gsignals__ = {
'item-clicked': (GObject.SIGNAL_RUN_LAST, None, (str,))
}
def __init__(self, *args, **kwargs):
RB.SearchEntry.__init__(self, *args, **kwargs)
OptionsPopupWidget.__init__(self)
#self.props.explicit_mode = True
@OptionsPopupWidget.controller.setter
def controller(self, controller):
if self._controller:
# disconnect signals
self._controller.disconnect(self._search_text_changed_id)
OptionsPopupWidget.controller.fset(self, controller)
# connect signals
self._search_text_changed_id = self._controller.connect(
'notify::search-text', self._update_search_text)
# update the current text
self._update_search_text()
def _update_search_text(self, *args):
if not self.searching():
self.grab_focus()
self.set_text(self._controller.search_text)
def update_current_key(self):
super(SearchEntry, self).update_current_key()
self.set_placeholder(self._controller.get_current_description())
def do_show_popup(self):
'''
Callback called by the search entry when the magnifier is clicked.
It prompts the user through a popup to select a filter type.
'''
self.show_popup(False)
def do_search(self, text):
'''
Callback called by the search entry when a new search must
be performed.
'''
if self._controller:
self._controller.do_search(text)
class QuickSearchEntry(Gtk.Frame):
__gtype_name__ = "QuickSearchEntry"
# signals
__gsignals__ = {
'quick-search': (GObject.SIGNAL_RUN_LAST, None, (str,)),
'arrow-pressed': (GObject.SIGNAL_RUN_LAST, None, (object,))
}
def __init__(self, *args, **kwargs):
super(QuickSearchEntry, self).__init__(*args, **kwargs)
self._idle = 0
# text entry for the quick search input
text_entry = Gtk.Entry(halign='center', valign='center',
margin=5)
self.add(text_entry)
self.connect_signals(text_entry)
def get_text(self):
return self.get_child().get_text()
def set_text(self, text):
self.get_child().set_text(text)
def connect_signals(self, text_entry):
text_entry.connect('changed', self._on_quick_search)
text_entry.connect('focus-out-event', self._on_focus_lost)
text_entry.connect('key-press-event', self._on_key_pressed)
def _hide_quick_search(self):
self.hide()
def _add_hide_on_timeout(self):
self._idle += 1
def hide_on_timeout(*args):
self._idle -= 1
if not self._idle:
self._hide_quick_search()
return False
Gdk.threads_add_timeout_seconds(GLib.PRIORITY_DEFAULT_IDLE, 4,
hide_on_timeout, None)
def do_parent_set(self, old_parent, *args):
if old_parent:
old_parent.disconnect(self._on_parent_key_press_id)
parent = self.get_parent()
self._on_parent_key_press_id = parent.connect('key-press-event',
self._on_parent_key_press, self.get_child())
def _on_parent_key_press(self, parent, event, entry):
if not self.get_visible() and \
event.keyval not in [Gdk.KEY_Shift_L, Gdk.KEY_Shift_R,
Gdk.KEY_Control_L, Gdk.KEY_Control_R, Gdk.KEY_Escape]:
# grab focus, redirect the pressed key and make the quick search
# entry visible
entry.set_text('')
entry.grab_focus()
entry.im_context_filter_keypress(event)
self.show_all()
elif self.get_visible() and event.keyval == Gdk.KEY_Escape:
self._hide_quick_search()
return False
def _on_quick_search(self, entry, *args):
if entry.get_visible():
# emit the quick-search signal
search_text = entry.get_text()
self.emit('quick-search', search_text)
# add a timeout to hide the search entry
self._add_hide_on_timeout()
def _on_focus_lost(self, entry, *args):
self._hide_quick_search()
return False
def _on_key_pressed(self, entry, event, *args):
arrow = event.keyval in [Gdk.KEY_Up, Gdk.KEY_Down]
if arrow:
self.emit('arrow-pressed', event.keyval)
self._add_hide_on_timeout()
return arrow
class ProxyPopupButton(Gtk.Frame):
__gtype_name__ = "ProxyPopupButton"
def __init__(self, *args, **kwargs):
super(ProxyPopupButton, self).__init__(*args, **kwargs)
self._delegate = None
@property
def controller(self):
if self._delegate:
return self._delegate.controller
@controller.setter
def controller(self, controller):
if self._delegate:
self.remove(self._delegate)
if len(controller.options) < 25:
self._delegate = PopupButton()
else:
self._delegate = ListViewButton()
self._delegate.set_visible(True)
self._delegate.set_has_tooltip(True)
self._delegate.set_can_focus(False)
self._delegate.controller = controller
self.add(self._delegate)
class OptionsListViewWidget(OptionsWidget):
# signals
__gsignals__ = {
'item-clicked': (GObject.SIGNAL_RUN_LAST, None, (str,)),
'deactivate': (GObject.SIGNAL_RUN_LAST, None, ())
}
def __init__(self, *args, **kwargs):
OptionsWidget.__init__(self, *args, **kwargs)
self._popup = self
@OptionsWidget.controller.setter
def controller(self, controller):
ui = Gtk.Builder()
ui.add_from_file(rb.find_plugin_file(controller.plugin,
'ui/coverart_listwindow.ui'))
ui.connect_signals(self)
self._listwindow = ui.get_object('listwindow')
self._liststore = ui.get_object('liststore')
self._listwindow.set_size_request(200, 300)
self._treeview = ui.get_object('treeview')
self._scrollwindow = ui.get_object('scrolledwindow')
self._scrolldown_button = ui.get_object('scrolldown_button')
self._increment = False
OptionsWidget.controller.fset(self, controller)
def update_options(self):
self.clear_options()
self.add_options(self._controller.options)
def update_current_key(self):
self.select(self.controller.get_current_key_index())
def do_item_clicked(self, key):
if self._controller:
# inform the controller
self._controller.option_selected(key)
def show_popup(self):
'''
show the listview window either above or below the controlling
widget depending upon where the cursor position is relative to the
screen
params - x & y is the cursor position
'''
pos_x, pos_y = self.calc_popup_position(self._listwindow)
self._listwindow.move(pos_x, pos_y)
self._listwindow.show_all()
def clear_options(self):
self._liststore.clear()
def add_options(self, iterable):
for label in iterable:
self._liststore.append((label,))
def select(self, index):
self._treeview.get_selection().select_iter(self._liststore[index].iter)
self._treeview.scroll_to_cell(self._liststore[index].path)
def on_button_click(self, view, arg):
try:
liststore, viewiter = view.get_selection().get_selected()
label = liststore.get_value(viewiter, 0)
self.emit('item-clicked', label)
except:
pass
self._treeview.set_hover_selection(False)
self._listwindow.hide()
self.emit('deactivate')
def on_scroll_button_enter(self, button):
def scroll(*args):
if self._increment:
if button is self._scrolldown_button:
adjustment.set_value(adjustment.get_value()
+ self._step)
else:
adjustment.set_value(adjustment.get_value()
- self._step)
return self._increment
self._increment = True
adjustment = self._scrollwindow.get_vadjustment()
self.on_scroll_button_released()
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 50,
scroll, None)
def on_scroll_button_leave(self, *args):
self._increment = False
def on_scroll_button_pressed(self, *args):
adjustment = self._scrollwindow.get_vadjustment()
self._step = adjustment.get_page_increment()
def on_scroll_button_released(self, *args):
adjustment = self._scrollwindow.get_vadjustment()
self._step = adjustment.get_step_increment()
def on_treeview_enter_notify_event(self, *args):
self._treeview.set_hover_selection(True)
def on_cancel(self, *args):
self._listwindow.hide()
self.emit('deactivate')
return True
def do_delete_thyself(self):
self.clear_list()
del self._listwindow
class ListViewButton(PixbufButton, OptionsListViewWidget):
__gtype_name__ = "ListViewButton"
# signals
__gsignals__ = {
'item-clicked': (GObject.SIGNAL_RUN_LAST, None, (str,)),
'deactivate': (GObject.SIGNAL_RUN_LAST, None, ())
}
def __init__(self, *args, **kwargs):
'''
Initializes the button.
'''
PixbufButton.__init__(self, *args, **kwargs)
OptionsListViewWidget.__init__(self, *args, **kwargs)
self._popup.connect('deactivate', self.popup_deactivate)
def popup_deactivate(self, *args):
# add a slight delay to allow the click of button to occur
# before the deactivation of the button - this will allow
# us to toggle the popup via the button correctly
def deactivate(*args):
self.set_active(False)
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 50, deactivate, None)
def update_image(self):
super(ListViewButton, self).update_image()
self.set_image(self._controller.get_current_image())
def update_current_key(self):
super(ListViewButton, self).update_current_key()
# update the current image and tooltip
self.set_image(self._controller.get_current_image())
self.set_tooltip_text(self._controller.get_current_description())
def do_button_press_event(self, event):
'''
when button is clicked, update the popup with the sorting options
before displaying the popup
'''
if (event.button == Gdk.BUTTON_PRIMARY and not self.get_active()):
self.show_popup()
self.set_active(True)
else:
self.set_active(False)
class EnhancedIconView(Gtk.IconView):
__gtype_name__ = "EnhancedIconView"
# signals
__gsignals__ = {
'item-clicked': (GObject.SIGNAL_RUN_LAST, None, (object, object))
}
object_column = GObject.property(type=int, default=-1)
def __init__(self, *args, **kwargs):
super(EnhancedIconView, self).__init__(*args, **kwargs)
self._reallocate_count = 0
self.view_name = None
self.source = None
self.ext_menu_pos = 0
def do_size_allocate(self, allocation):
'''
Forces the reallocation of the IconView columns when the width of the
widgets changes. Neverthless, it takes into account that multiple
reallocations could happen in a short amount of time, so it avoids
trying to refresh until the user has stopped resizing the component.
'''
if self.get_allocated_width() != allocation.width:
# don't need to reaccommodate if it's a vertical change
self._reallocate_count += 1
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 500,
self._reallocate_columns, None)
Gtk.IconView.do_size_allocate(self, allocation)
def _reallocate_columns(self, *args):
self._reallocate_count -= 1
if not self._reallocate_count:
self.set_columns(0)
self.set_columns(-1)
def do_button_press_event(self, event):
'''
Other than the default behavior, adds an event firing when the mouse
has clicked on top of a current item, informing the listeners of the
path of the clicked item.
'''
x = int(event.x)
y = int(event.y)
current_path = self.get_path_at_pos(x, y)
if event.type is Gdk.EventType.BUTTON_PRESS and current_path:
if event.triggers_context_menu():
# if the item being clicked isn't selected, we should clear
# the current selection
if len(self.get_selected_objects()) > 0 and \
not self.path_is_selected(current_path):
self.unselect_all()
self.select_path(current_path)
self.set_cursor(current_path, None, False)
if self.popup:
self.popup.popup(self.source, 'popup_menu', event.button, event.time)
else:
self.emit('item-clicked', event, current_path)
Gtk.IconView.do_button_press_event(self, event)
def get_selected_objects(self):
'''
Helper method that simplifies getting the objects stored on the
selected items, givent that the object_column property is setted.
This way there's no need for the client class to repeateadly access the
correct column to retrieve the object from the raw rows.
'''
selected_items = self.get_selected_items()
if not self.object_column:
# if no object_column is setted, return the selected rows
return selected_items
model = self.get_model()
selected_objects = list(reversed([model[selected][self.object_column]
for selected in selected_items]))
return selected_objects
def select_and_scroll_to_path(self, path):
'''
Helper method to select and scroll to a given path on the IconView.
'''
self.unselect_all()
self.select_path(path)
self.set_cursor(path, None, False)
self.scroll_to_path(path, True, 0.5, 0.5)
class PanedCollapsible(Gtk.Paned):
__gtype_name__ = "PanedCollapsible"
# properties
# this two properties indicate which one of the Paned childs is collapsible
# only one can be True at a time, the widget takes care of keeping this
# restriction consistent.
collapsible1 = GObject.property(type=bool, default=False)
collapsible2 = GObject.property(type=bool, default=False)
# values for expand method
Paned = enum(DEFAULT=1, EXPAND=2, COLLAPSE=3)
# this indicates the latest position for the handle before a child was
# collapsed
collapsible_y = GObject.property(type=int, default=0)
# label for the Expander used on the collapsible child
collapsible_label = GObject.property(type=str, default='')
# signals
__gsignals__ = {
'expanded': (GObject.SIGNAL_RUN_LAST, None, (bool,))
}
def __init__(self, *args, **kwargs):
super(PanedCollapsible, self).__init__(*args, **kwargs)
self._connect_properties()
def _connect_properties(self):
self.connect('notify::collapsible1', self._on_collapsible1_changed)
self.connect('notify::collapsible2', self._on_collapsible2_changed)
self.connect('notify::collapsible_label',
self._on_collapsible_label_changed)
def _on_collapsible1_changed(self, *args):
if self.collapsible1 and self.collapsible2:
# check consistency, only one collapsible at a time
self.collapsible2 = False
child = self.get_child1()
self._wrap_unwrap_child(child, self.collapsible1, self.add1)
def _on_collapsible2_changed(self, *args):
if self.collapsible1 and self.collapsible2:
# check consistency, only one collapsible at a time
self.collapsible1 = False
child = self.get_child2()
self._wrap_unwrap_child(child, self.collapsible2, self.add2)
def _wrap_unwrap_child(self, child, wrap, add):
if child:
self.remove(child)
if not wrap:
inner_child = child.get_child()
child.remove(inner_child)
child = inner_child
add(child)
def _on_collapsible_label_changed(self, *args):
if self._expander:
self._expander.set_label(self.collapsible_label)
def _on_collapsible_expanded(self, *args):
expand = self._expander.get_expanded()
if not expand:
self.collapsible_y = self.get_position()
# move the lower pane to the bottom since it's collapsed
self._collapse()
else:
# reinstate the lower pane to it's expanded size
if not self.collapsible_y:
# if there isn't a saved size, use half of the space
new_y = self.get_allocated_height() / 2
self.collapsible_y = new_y
self.set_position(self.collapsible_y)
self.emit('expanded', expand)
def do_button_press_event(self, *args):
'''
This callback allows or denies the paned handle to move depending on
the expanded expander
'''
if not self._expander or self._expander.get_expanded():
Gtk.Paned.do_button_press_event(self, *args)
def do_button_release_event(self, *args):
'''
Callback when the paned handle is released from its mouse click.
'''
if not self._expander or self._expander.get_expanded():
Gtk.Paned.do_button_release_event(self, *args)
self.collapsible_y = self.get_position()
def do_remove(self, widget):
'''
Overwrites the super class remove method, taking care of removing the
child even if it's wrapped inside an Expander.
'''
if self.collapsible1 and self.get_child1().get_child() is widget:
expander = self.get_child1()
expander.remove(widget)
widget = expander
elif self.collapsible2 and self.get_child2().get_child() is widget:
expander = self.get_child2()
expander.remove(widget)
widget = expander
self._expander = None
Gtk.Paned.remove(self, widget)
def do_add(self, widget):
'''
This method had to be overridden to allow the add and packs method to
work with Glade.
'''
if not self.get_child1():
self.do_add1(widget)
elif not self.get_child2():
self.do_add2(widget)
else:
print("GtkPaned cannot have more than 2 children")
def do_add1(self, widget):
'''
Overrides the add1 superclass' method for pack1 to work correctly.
'''
self.do_pack1(widget, True, True)
def do_pack1(self, widget, *args, **kwargs):
'''
Packs the widget into the first paned child, adding a GtkExpander
around the packed widget if the collapsible1 property is True.
'''
if self.collapsible1:
widget = self._create_expander(widget)
Gtk.Paned.pack1(self, widget, *args, **kwargs)
def do_add2(self, widget):
'''
Overrides the add2 superclass' method for pack2 to work correctly.
'''
self.do_pack2(widget, True, True)
def do_pack2(self, widget, *args, **kwargs):
'''
Packs the widget into the second paned child, adding a GtkExpander
around the packed widget if the collapsible2 property is True.
'''
if self.collapsible2:
widget = self._create_expander(widget)
Gtk.Paned.pack2(self, widget, *args, **kwargs)
def _create_expander(self, widget):
self._expander = Gtk.Expander(label=self.collapsible_label,
visible=True)
self._expander.add(widget)
# connect the expanded signal
self._expander.connect('notify::expanded',
self._on_collapsible_expanded)
# connect the initial collapse
self._allocate_id = self._expander.connect('size-allocate',
self._initial_collapse)
return self._expander
def _initial_collapse(self, *args):
self._collapse()
self._expander.disconnect(self._allocate_id)
del self._allocate_id
def _collapse(self):
new_y = self.get_allocated_height() - \
self.get_handle_window().get_height() - \
self._expander.get_label_widget().get_allocated_height()
self.set_position(new_y)
def expand(self, force):
'''
Toggles the expanded property of the collapsible children.
unless requested to force expansion
'''
if self._expander:
if force == PanedCollapsible.Paned.EXPAND:
self._expander.set_expanded(True)
elif force == PanedCollapsible.Paned.COLLAPSE:
self._expander.set_expanded(False)
elif force == PanedCollapsible.Paned.DEFAULT:
self._expander.set_expanded(not self._expander.get_expanded())
def get_expansion_status(self):
'''
returns the position of the expander i.e. expanded or not
'''
value = PanedCollapsible.Paned.COLLAPSE
if self._expander and self._expander.get_expanded():
value = PanedCollapsible.Paned.EXPAND
return value
class AbstractView(GObject.Object):
'''
intention is to document 'the must have' methods all views should define
N.B. this is preliminary and will change as and when
coverflow view is added with lessons learned
'''
view = None
panedposition = PanedCollapsible.Paned.DEFAULT
use_plugin_window = True
# signals - note - pygobject doesnt appear to support signal declaration
# in multiple inheritance - so these signals need to be defined in all view classes
# where abstractview is part of multiple inheritance
__gsignals__ = {
'update-toolbar': (GObject.SIGNAL_RUN_LAST, None, ())
}
def __init__(self):
super(AbstractView, self).__init__()
def initialise(self, source):
self.source = source
self.plugin = source.plugin
self.connect('update-toolbar', self.do_update_toolbar)
def do_update_toolbar(self, *args):
'''
called when update-toolbar signal is emitted
by default the toolbar objects are made visible
'''
from coverart_toolbar import ToolbarObject
self.source.toolbar_manager.set_enabled(True, ToolbarObject.SORT_BY)
self.source.toolbar_manager.set_enabled(True, ToolbarObject.SORT_ORDER)
self.source.toolbar_manager.set_enabled(False, ToolbarObject.SORT_BY_ARTIST)
self.source.toolbar_manager.set_enabled(False, ToolbarObject.SORT_ORDER_ARTIST)
def resize_icon(self, cover_size):
'''
resize the view main picture icon
:param cover_size: `int` icon size
'''
pass
def get_selected_objects(self):
'''
finds what has been selected
returns an array of `Album`
'''
pass
def selectionchanged_callback(self, *args):
'''
callback when a selection has changed
'''
self.source.update_with_selection()
def select_and_scroll_to_path(self, path):
'''
find a path and highlight (select) that object
'''
pass
def scroll_to_album(self, album):
'''
scroll to the album in the view
'''
if album:
path = self.source.album_manager.model.get_path(album)
if path:
self.select_and_scroll_to_path(path)
def set_popup_menu(self, popup):
'''
define the popup menu (right click) used for the view
'''
self.popup = popup
def grab_focus(self):
'''
ensures main view object retains the focus
'''
pass
def switch_to_view(self, source, album):
'''
ensures that when the user toggles to a view stuff remains
consistent
'''
pass
def get_view_icon_name(self):
'''
every view should have an icon - subject to removal
since we'll probably just have text buttons for the view
'''
return ""
def get_default_manager(self):
'''
every view should have a default manager
for example an AlbumManager or ArtistManager
by default - use the AlbumManager from the source
'''
return self.source.album_manager
def switch_to_coverpane(self, cover_search_pane):
'''
called from the source to update the coverpane when
it is switched from the track pane
'''
selected = self.get_selected_objects()
if selected:
manager = self.get_default_manager()
cover_search_pane.do_search(selected[0],
manager.cover_man.update_cover)