CoverArt Browser  v2.0
Browse your cover-art albums in Rhythmbox
/home/foss/Downloads/coverart-browser/coverart_widgets.py
00001 # -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
00002 #
00003 # Copyright (C) 2012 - fossfreedom
00004 # Copyright (C) 2012 - Agustin Carrasco
00005 #
00006 # This program is free software; you can redistribute it and/or modify
00007 # it under the terms of the GNU General Public License as published by
00008 # the Free Software Foundation; either version 2, or (at your option)
00009 # any later version.
00010 #
00011 # This program is distributed in the hope that it will be useful,
00012 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00013 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00014 # GNU General Public License for more details.
00015 #
00016 # You should have received a copy of the GNU General Public License
00017 # along with this program; if not, write to the Free Software
00018 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
00019 
00020 from gi.repository import RB
00021 from gi.repository import Gtk
00022 from gi.repository import Gdk
00023 from gi.repository import GLib
00024 from gi.repository import GObject
00025 from gi.repository import Gio
00026 from gi.repository import Notify
00027 import cairo
00028 
00029 from coverart_browser_prefs import GSetting
00030 from coverart_external_plugins import ExternalPlugin
00031 import rb
00032 
00033 
00034 def enum(**enums):
00035     return type('Enum', (object,), enums)
00036 
00037 
00038 class OptionsWidget(Gtk.Widget):
00039     def __init__(self, *args, **kwargs):
00040         super(OptionsWidget, self).__init__(*args, **kwargs)
00041         self._controller = None
00042 
00043     @property
00044     def controller(self):
00045         return self._controller
00046 
00047     @controller.setter
00048     def controller(self, controller):
00049         if self._controller:
00050             # disconnect signals
00051             self._controller.disconnect(self._options_changed_id)
00052             self._controller.disconnect(self._current_key_changed_id)
00053             self._controller.disconnect(self._update_image_changed_id)
00054 
00055         self._controller = controller
00056 
00057         # connect signals
00058         self._options_changed_id = self._controller.connect('notify::options',
00059                                                             self._update_options)
00060         self._current_key_changed_id = self._controller.connect(
00061             'notify::current-key', self._update_current_key)
00062         self._update_image_changed_id = self._controller.connect(
00063             'notify::update-image', self._update_image)
00064         self._visible_changed_id = self._controller.connect(
00065             'notify::enabled', self._update_visibility)
00066 
00067         # update the menu and current key
00068         self.update_options()
00069         self.update_current_key()
00070 
00071     def _update_visibility(self, *args):
00072         self.set_visible(self._controller.enabled)
00073 
00074     def _update_options(self, *args):
00075         self.update_options()
00076 
00077     def update_options(self):
00078         pass
00079 
00080     def _update_current_key(self, *args):
00081         self.update_current_key()
00082 
00083     def update_current_key():
00084         pass
00085 
00086     def _update_image(self, *args):
00087         self.update_image()
00088 
00089     def update_image(self):
00090         pass
00091 
00092     def calc_popup_position(self, widget):
00093         # this calculates the popup positioning - algorithm taken
00094         # from Gtk3.8 gtk/gtkmenubutton.c
00095 
00096         toplevel = self.get_toplevel()
00097         toplevel.set_type_hint(Gdk.WindowTypeHint.DROPDOWN_MENU)
00098 
00099         menu_req, pref_req = widget.get_preferred_size()
00100         align = widget.get_halign()
00101         direction = self.get_direction()
00102         window = self.get_window()
00103 
00104         screen = widget.get_screen()
00105         monitor_num = screen.get_monitor_at_window(window)
00106         if (monitor_num < 0):
00107             monitor_num = 0
00108         monitor = screen.get_monitor_workarea(monitor_num)
00109 
00110         allocation = self.get_allocation()
00111 
00112         ret, x, y = window.get_origin()
00113         x += allocation.x
00114         y += allocation.y
00115 
00116         if allocation.width - menu_req.width > 0:
00117             x += allocation.width - menu_req.width
00118 
00119         if ((y + allocation.height + menu_req.height) <= monitor.y + monitor.height):
00120             y += allocation.height
00121         elif ((y - menu_req.height) >= monitor.y):
00122             y -= menu_req.height
00123         else:
00124             y -= menu_req.height
00125 
00126         return x, y
00127 
00128 
00129 class OptionsPopupWidget(OptionsWidget):
00130     # signals
00131     __gsignals__ = {
00132         'item-clicked': (GObject.SIGNAL_RUN_LAST, None, (str,))
00133     }
00134 
00135     def __init__(self, *args, **kwargs):
00136         OptionsWidget.__init__(self, *args, **kwargs)
00137 
00138         self._popup_menu = Gtk.Menu()
00139 
00140     def update_options(self):
00141         self.clear_popupmenu()
00142 
00143         for key in self._controller.options:
00144             self.add_menuitem(key)
00145 
00146     def update_current_key(self):
00147         # select the item if it isn't already
00148         item = self.get_menuitems()[self._controller.get_current_key_index()]
00149 
00150         if not item.get_active():
00151             item.set_active(True)
00152 
00153     def add_menuitem(self, label):
00154         '''
00155         add a new menu item to the popup
00156         '''
00157         if not self._first_menu_item:
00158             new_menu_item = Gtk.RadioMenuItem(label=label)
00159             self._first_menu_item = new_menu_item
00160         else:
00161             new_menu_item = Gtk.RadioMenuItem.new_with_label_from_widget(
00162                 group=self._first_menu_item, label=label)
00163 
00164         new_menu_item.connect('toggled', self._fire_item_clicked)
00165         new_menu_item.show()
00166 
00167         self._popup_menu.append(new_menu_item)
00168 
00169     def get_menuitems(self):
00170         return self._popup_menu.get_children()
00171 
00172     def clear_popupmenu(self):
00173         '''
00174         reinitialises/clears the current popup menu and associated actions
00175         '''
00176         for menu_item in self._popup_menu:
00177             self._popup_menu.remove(menu_item)
00178 
00179         self._first_menu_item = None
00180 
00181     def _fire_item_clicked(self, menu_item):
00182         '''
00183         Fires the item-clicked signal if the item is selected, passing the
00184         given value as a parameter. Also updates the current value with the
00185         value of the selected item.
00186         '''
00187         if menu_item.get_active():
00188             self.emit('item-clicked', menu_item.get_label())
00189 
00190     def do_item_clicked(self, key):
00191         if self._controller:
00192             # inform the controller
00193             self._controller.option_selected(key)
00194 
00195     def _popup_callback(self, *args):
00196         x, y = self.calc_popup_position(self._popup_menu)
00197 
00198         return x, y, False, None
00199 
00200     def show_popup(self, align=True):
00201         '''
00202         show the current popup menu
00203         '''
00204 
00205         if align:
00206             self._popup_menu.popup(None, None, self._popup_callback, self, 0,
00207                                    Gtk.get_current_event_time())
00208         else:
00209             self._popup_menu.popup(None, None, None, None, 0,
00210                                    Gtk.get_current_event_time())
00211 
00212     def do_delete_thyself(self):
00213         self.clear_popupmenu()
00214         del self._popupmenu
00215 
00216 
00217 class EnhancedButton(Gtk.ToggleButton):
00218     button_relief = GObject.property(type=bool, default=False)
00219 
00220     def __init__(self, *args, **kwargs):
00221         super(EnhancedButton, self).__init__(*args, **kwargs)
00222 
00223         gs = GSetting()
00224         setting = gs.get_setting(gs.Path.PLUGIN)
00225         setting.bind(gs.PluginKey.BUTTON_RELIEF, self,
00226                      'button_relief', Gio.SettingsBindFlags.GET)
00227 
00228         self.connect('notify::button-relief',
00229                      self.on_notify_button_relief)
00230 
00231     def on_notify_button_relief(self, *arg):
00232         if self.button_relief:
00233             self.set_relief(Gtk.ReliefStyle.NONE)
00234         else:
00235             self.set_relief(Gtk.ReliefStyle.HALF)
00236 
00237 
00238 class PixbufButton(EnhancedButton):
00239     button_relief = GObject.property(type=bool, default=False)
00240 
00241     def __init__(self, *args, **kwargs):
00242         super(PixbufButton, self).__init__(*args, **kwargs)
00243 
00244     def set_image(self, pixbuf):
00245         image = self.get_image()
00246 
00247         if not image:
00248             image = Gtk.Image()
00249             super(PixbufButton, self).set_image(image)
00250 
00251         if hasattr(self, "controller.enabled") and not self.controller.enabled:
00252             pixbuf = self._getBlendedPixbuf(pixbuf)
00253 
00254         self.get_image().set_from_pixbuf(pixbuf)
00255 
00256         self.on_notify_button_relief()
00257 
00258     def _getBlendedPixbuf(self, pixbuf):
00259         """Turn a pixbuf into a blended version of the pixbuf by drawing a
00260         transparent alpha blend on it."""
00261         pixbuf = pixbuf.copy()
00262 
00263         w, h = pixbuf.get_width(), pixbuf.get_height()
00264         surface = cairo.ImageSurface(
00265             cairo.FORMAT_ARGB32, pixbuf.get_width(), pixbuf.get_height())
00266         context = cairo.Context(surface)
00267 
00268         Gdk.cairo_set_source_pixbuf(context, pixbuf, 0, 0)
00269         context.paint()
00270 
00271         context.set_source_rgba(32, 32, 32, 0.4)
00272         context.set_line_width(0)
00273         context.rectangle(0, 0, w, h)
00274         context.fill()
00275 
00276         pixbuf = Gdk.pixbuf_get_from_surface(surface, 0, 0, w, h)
00277 
00278         return pixbuf
00279 
00280 
00281 class PopupButton(PixbufButton, OptionsPopupWidget):
00282     __gtype_name__ = "PopupButton"
00283 
00284     # signals
00285     __gsignals__ = {
00286         'item-clicked': (GObject.SIGNAL_RUN_LAST, None, (str,))
00287     }
00288 
00289     def __init__(self, *args, **kwargs):
00290         '''
00291         Initializes the button.
00292         '''
00293         PixbufButton.__init__(self, *args, **kwargs)
00294         OptionsPopupWidget.__init__(self, *args, **kwargs)
00295 
00296         self._popup_menu.attach_to_widget(self, None)  #critical to ensure theming works
00297         self._popup_menu.connect('deactivate', self.popup_deactivate)
00298 
00299         # initialise some variables
00300         self._first_menu_item = None
00301 
00302     def popup_deactivate(self, *args):
00303         self.set_active(False)
00304 
00305     def update_image(self):
00306         super(PopupButton, self).update_image()
00307         self.set_image(self._controller.get_current_image())
00308 
00309     def update_current_key(self):
00310         super(PopupButton, self).update_current_key()
00311 
00312         # update the current image and tooltip
00313         self.set_image(self._controller.get_current_image())
00314         self.set_tooltip_text(self._controller.get_current_description())
00315 
00316     def do_button_press_event(self, event):
00317         '''
00318         when button is clicked, update the popup with the sorting options
00319         before displaying the popup
00320         '''
00321         if (event.button == Gdk.BUTTON_PRIMARY):
00322             self.show_popup()
00323             self.set_active(True)
00324 
00325 
00326 class TextPopupButton(EnhancedButton, OptionsPopupWidget):
00327     __gtype_name__ = "TextPopupButton"
00328 
00329     # signals
00330     __gsignals__ = {
00331         'item-clicked': (GObject.SIGNAL_RUN_LAST, None, (str,))
00332     }
00333 
00334     def __init__(self, *args, **kwargs):
00335         '''
00336         Initializes the button.
00337         '''
00338         EnhancedButton.__init__(self, *args, **kwargs)
00339         OptionsPopupWidget.__init__(self, *args, **kwargs)
00340 
00341         self._popup_menu.attach_to_widget(self, None)  #critical to ensure theming works
00342         self._popup_menu.connect('deactivate', self.popup_deactivate)
00343 
00344         # initialise some variables
00345         self._first_menu_item = None
00346 
00347     def popup_deactivate(self, *args):
00348         self.set_active(False)
00349 
00350     def do_button_press_event(self, event):
00351         '''
00352         when button is clicked, update the popup with the sorting options
00353         before displaying the popup
00354         '''
00355         if (event.button == Gdk.BUTTON_PRIMARY):
00356             self.show_popup()
00357             self.set_active(True)
00358 
00359 
00360 class MenuButton(PixbufButton, OptionsPopupWidget):
00361     __gtype_name__ = "MenuButton"
00362 
00363     # signals
00364     __gsignals__ = {
00365         'item-clicked': (GObject.SIGNAL_RUN_LAST, None, (str,))
00366     }
00367 
00368     def __init__(self, *args, **kwargs):
00369         '''
00370         Initializes the button.
00371         '''
00372         PixbufButton.__init__(self, *args, **kwargs)
00373         OptionsPopupWidget.__init__(self, *args, **kwargs)
00374 
00375         self._popup_menu.attach_to_widget(self, None)  #critical to ensure theming works
00376         self._popup_menu.connect('deactivate', self.popup_deactivate)
00377         self._states = {}
00378 
00379     def popup_deactivate(self, *args):
00380         self.set_active(False)
00381 
00382     def add_menuitem(self, key):
00383         '''
00384         add a new menu item to the popup
00385         '''
00386 
00387         label = key.label
00388         menutype = key.menutype
00389         typevalue = key.typevalue
00390 
00391         if menutype and menutype == 'separator':
00392             new_menu_item = Gtk.SeparatorMenuItem().new()
00393         elif menutype and menutype == 'check':
00394             new_menu_item = Gtk.CheckMenuItem(label=label)
00395             new_menu_item.set_active(typevalue)
00396             new_menu_item.connect('toggled', self._fire_item_clicked)
00397         else:
00398             new_menu_item = Gtk.MenuItem(label=label)
00399             new_menu_item.connect('activate', self._fire_item_clicked)
00400 
00401         new_menu_item.show()
00402         self._popup_menu.append(new_menu_item)
00403 
00404     def clear_popupmenu(self):
00405         '''
00406         reinitialises/clears the current popup menu and associated actions
00407         '''
00408         for menu_item in self._popup_menu:
00409             if isinstance(menu_item, Gtk.CheckMenuItem):
00410                 self._states[menu_item.get_label()] = menu_item.get_active()
00411             self._popup_menu.remove(menu_item)
00412 
00413         self._first_menu_item = None
00414 
00415     def update_options(self):
00416         self.clear_popupmenu()
00417 
00418         for key in self._controller.options:
00419             self.add_menuitem(key)
00420 
00421         self._states = {}
00422 
00423     def _fire_item_clicked(self, menu_item):
00424         '''
00425         Fires the item-clicked signal if the item is selected, passing the
00426         given value as a parameter. Also updates the current value with the
00427         value of the selected item.
00428         '''
00429         self.emit('item-clicked', menu_item.get_label())
00430 
00431     def update_image(self):
00432         super(MenuButton, self).update_image()
00433         self.set_image(self._controller.get_current_image())
00434 
00435     def update_current_key(self):
00436         # select the item if it isn't already
00437         #item = self.get_menuitems()[self._controller.get_current_key_index()]
00438 
00439         # update the current image and tooltip
00440         self.set_image(self._controller.get_current_image())
00441         self.set_tooltip_text(self._controller.get_current_description())
00442 
00443     def do_button_press_event(self, event):
00444         '''
00445         when button is clicked, update the popup with the sorting options
00446         before displaying the popup
00447         '''
00448         if (event.button == Gdk.BUTTON_PRIMARY):
00449             self.show_popup()
00450             self.set_active(True)
00451 
00452 
00453 class ImageToggleButton(PixbufButton, OptionsWidget):
00454     __gtype_name__ = "ImageToggleButton"
00455 
00456     def __init__(self, *args, **kwargs):
00457         '''
00458         Initializes the button.
00459         '''
00460         PixbufButton.__init__(self, *args, **kwargs)
00461         OptionsWidget.__init__(self, *args, **kwargs)
00462 
00463         # initialise some variables
00464         self.image_display = False
00465         self.initialised = False
00466 
00467     def update_image(self):
00468         super(ImageToggleButton, self).update_image()
00469         self.set_image(self._controller.get_current_image())
00470 
00471 
00472     def update_current_key(self):
00473         # update the current image and tooltip
00474         self.set_image(self._controller.get_current_image())
00475         self.set_tooltip_text(self._controller.get_current_description())
00476 
00477     def do_clicked(self):
00478         if self._controller:
00479             index = self._controller.get_current_key_index()
00480             index = (index + 1) % len(self._controller.options)
00481 
00482             # inform the controller
00483             self._controller.option_selected(
00484                 self._controller.options[index])
00485 
00486 
00487 class ImageRadioButton(Gtk.RadioButton, OptionsWidget):
00488     # this is legacy code that will not as yet work with
00489     # the new toolbar - consider removing this later
00490 
00491     __gtype_name__ = "ImageRadioButton"
00492 
00493     button_relief = GObject.property(type=bool, default=False)
00494 
00495     def __init__(self, *args, **kwargs):
00496         '''
00497         Initializes the button.
00498         '''
00499         Gtk.RadioButton.__init__(self, *args, **kwargs)
00500         OptionsWidget.__init__(self, *args, **kwargs)
00501 
00502         gs = GSetting()
00503         setting = gs.get_setting(gs.Path.PLUGIN)
00504         setting.bind(gs.PluginKey.BUTTON_RELIEF, self,
00505                      'button_relief', Gio.SettingsBindFlags.GET)
00506 
00507         self.connect('notify::button-relief',
00508                      self.on_notify_button_relief)
00509 
00510         # initialise some variables
00511         self.image_display = False
00512         self.initialised = False
00513 
00514         #ensure button appearance rather than standard radio toggle
00515         self.set_mode(False)
00516 
00517         #label colours
00518         self._not_active_colour = None
00519         self._active_colour = None
00520 
00521     def update_image(self):
00522         super(ImageRadioButton, self).update_image()
00523         #self.set_image(self._controller.get_current_image(Gtk.Buildable.get_name(self)))
00524 
00525     def do_toggled(self):
00526         if self.get_active():
00527             self.controller.option_selected(Gtk.Buildable.get_name(self))
00528 
00529     def set_image(self, pixbuf):
00530         image = self.get_image()
00531 
00532         if not image:
00533             image = Gtk.Image()
00534             super(ImageRadioButton, self).set_image(image)
00535 
00536         self.get_image().set_from_pixbuf(pixbuf)
00537 
00538         self.on_notify_button_relief()
00539 
00540     def on_notify_button_relief(self, *arg):
00541         if self.button_relief:
00542             self.set_relief(Gtk.ReliefStyle.NONE)
00543         else:
00544             self.set_relief(Gtk.ReliefStyle.HALF)
00545 
00546     def update_current_key(self):
00547         # update the current image and tooltip
00548         #self.set_image(self._controller.get_current_image(Gtk.Buildable.get_name(self)))
00549         self.set_tooltip_text("")  #self._controller.get_current_description())
00550 
00551         if self.controller.current_key == Gtk.Buildable.get_name(self):
00552             self.set_active(True)
00553             self._set_colour(Gtk.StateFlags.NORMAL)
00554         else:
00555             self._set_colour(Gtk.StateFlags.INSENSITIVE)
00556 
00557     def _set_colour(self, state_flag):
00558 
00559         if len(self.get_children()) == 0:
00560             return
00561 
00562         def get_standard_colour(label, state_flag):
00563             context = label.get_style_context()
00564             return context.get_color(state_flag)
00565 
00566         label0 = self.get_children()[0]
00567 
00568         if not self._not_active_colour:
00569             self._not_active_colour = get_standard_colour(label0, Gtk.StateFlags.INSENSITIVE)
00570 
00571         if not self._active_colour:
00572             self._active_colour = get_standard_colour(label0, Gtk.StateFlags.NORMAL)
00573 
00574         if state_flag == Gtk.StateFlags.INSENSITIVE:
00575             label0.override_color(Gtk.StateType.NORMAL, self._not_active_colour)
00576         else:
00577             label0.override_color(Gtk.StateType.NORMAL, self._active_colour)
00578 
00579 
00580 class SearchEntry(RB.SearchEntry, OptionsPopupWidget):
00581     __gtype_name__ = "SearchEntry"
00582 
00583     # signals
00584     __gsignals__ = {
00585         'item-clicked': (GObject.SIGNAL_RUN_LAST, None, (str,))
00586     }
00587 
00588     def __init__(self, *args, **kwargs):
00589         RB.SearchEntry.__init__(self, *args, **kwargs)
00590         OptionsPopupWidget.__init__(self)
00591         #self.props.explicit_mode = True
00592 
00593     @OptionsPopupWidget.controller.setter
00594     def controller(self, controller):
00595         if self._controller:
00596             # disconnect signals
00597             self._controller.disconnect(self._search_text_changed_id)
00598 
00599         OptionsPopupWidget.controller.fset(self, controller)
00600 
00601         # connect signals
00602         self._search_text_changed_id = self._controller.connect(
00603             'notify::search-text', self._update_search_text)
00604 
00605         # update the current text
00606         self._update_search_text()
00607 
00608     def _update_search_text(self, *args):
00609         if not self.searching():
00610             self.grab_focus()
00611         self.set_text(self._controller.search_text)
00612 
00613     def update_current_key(self):
00614         super(SearchEntry, self).update_current_key()
00615 
00616         self.set_placeholder(self._controller.get_current_description())
00617 
00618     def do_show_popup(self):
00619         '''
00620         Callback called by the search entry when the magnifier is clicked.
00621         It prompts the user through a popup to select a filter type.
00622         '''
00623         self.show_popup(False)
00624 
00625     def do_search(self, text):
00626         '''
00627         Callback called by the search entry when a new search must
00628         be performed.
00629         '''
00630         if self._controller:
00631             self._controller.do_search(text)
00632 
00633 
00634 class QuickSearchEntry(Gtk.Frame):
00635     __gtype_name__ = "QuickSearchEntry"
00636 
00637     # signals
00638     __gsignals__ = {
00639         'quick-search': (GObject.SIGNAL_RUN_LAST, None, (str,)),
00640         'arrow-pressed': (GObject.SIGNAL_RUN_LAST, None, (object,))
00641     }
00642 
00643     def __init__(self, *args, **kwargs):
00644         super(QuickSearchEntry, self).__init__(*args, **kwargs)
00645         self._idle = 0
00646 
00647         # text entry for the quick search input
00648         text_entry = Gtk.Entry(halign='center', valign='center',
00649                                margin=5)
00650 
00651         self.add(text_entry)
00652 
00653         self.connect_signals(text_entry)
00654 
00655     def get_text(self):
00656         return self.get_child().get_text()
00657 
00658     def set_text(self, text):
00659         self.get_child().set_text(text)
00660 
00661     def connect_signals(self, text_entry):
00662         text_entry.connect('changed', self._on_quick_search)
00663         text_entry.connect('focus-out-event', self._on_focus_lost)
00664         text_entry.connect('key-press-event', self._on_key_pressed)
00665 
00666     def _hide_quick_search(self):
00667         self.hide()
00668 
00669     def _add_hide_on_timeout(self):
00670         self._idle += 1
00671 
00672         def hide_on_timeout(*args):
00673             self._idle -= 1
00674 
00675             if not self._idle:
00676                 self._hide_quick_search()
00677 
00678             return False
00679 
00680         Gdk.threads_add_timeout_seconds(GLib.PRIORITY_DEFAULT_IDLE, 4,
00681                                         hide_on_timeout, None)
00682 
00683     def do_parent_set(self, old_parent, *args):
00684         if old_parent:
00685             old_parent.disconnect(self._on_parent_key_press_id)
00686 
00687         parent = self.get_parent()
00688         self._on_parent_key_press_id = parent.connect('key-press-event',
00689                                                       self._on_parent_key_press, self.get_child())
00690 
00691     def _on_parent_key_press(self, parent, event, entry):
00692         if not self.get_visible() and \
00693                         event.keyval not in [Gdk.KEY_Shift_L,
00694                                              Gdk.KEY_Shift_R,
00695                                              Gdk.KEY_Control_L,
00696                                              Gdk.KEY_Control_R,
00697                                              Gdk.KEY_Escape,
00698                                              Gdk.KEY_Alt_L,
00699                                              Gdk.KEY_Super_L,
00700                                              Gdk.KEY_Super_R]:
00701             # grab focus, redirect the pressed key and make the quick search
00702             # entry visible
00703             entry.set_text('')
00704             entry.grab_focus()
00705             self.show_all()
00706             entry.im_context_filter_keypress(event)
00707 
00708         elif self.get_visible() and event.keyval == Gdk.KEY_Escape:
00709             self._hide_quick_search()
00710 
00711         return False
00712 
00713     def _on_quick_search(self, entry, *args):
00714         if entry.get_visible():
00715             # emit the quick-search signal
00716             search_text = entry.get_text()
00717             self.emit('quick-search', search_text)
00718 
00719             # add a timeout to hide the search entry
00720             self._add_hide_on_timeout()
00721 
00722     def _on_focus_lost(self, entry, *args):
00723         self._hide_quick_search()
00724 
00725         return False
00726 
00727     def _on_key_pressed(self, entry, event, *args):
00728         arrow = event.keyval in [Gdk.KEY_Up, Gdk.KEY_Down]
00729 
00730         if arrow:
00731             self.emit('arrow-pressed', event.keyval)
00732             self._add_hide_on_timeout()
00733 
00734         return arrow
00735 
00736 
00737 class ProxyPopupButton(Gtk.Frame):
00738     __gtype_name__ = "ProxyPopupButton"
00739 
00740     def __init__(self, *args, **kwargs):
00741         super(ProxyPopupButton, self).__init__(*args, **kwargs)
00742         self._delegate = None
00743 
00744     @property
00745     def controller(self):
00746         if self._delegate:
00747             return self._delegate.controller
00748 
00749     @controller.setter
00750     def controller(self, controller):
00751         if self._delegate:
00752             self.remove(self._delegate)
00753 
00754         if len(controller.options) < 25:
00755             self._delegate = PopupButton()
00756         else:
00757             self._delegate = ListViewButton()
00758 
00759         self._delegate.set_visible(True)
00760         self._delegate.set_has_tooltip(True)
00761         self._delegate.set_can_focus(False)
00762 
00763         self._delegate.controller = controller
00764         self.add(self._delegate)
00765 
00766 
00767 class OptionsListViewWidget(OptionsWidget):
00768     # signals
00769     __gsignals__ = {
00770         'item-clicked': (GObject.SIGNAL_RUN_LAST, None, (str,)),
00771         'deactivate': (GObject.SIGNAL_RUN_LAST, None, ())
00772     }
00773 
00774     def __init__(self, *args, **kwargs):
00775         OptionsWidget.__init__(self, *args, **kwargs)
00776         self._popup = self
00777 
00778     @OptionsWidget.controller.setter
00779     def controller(self, controller):
00780         ui = Gtk.Builder()
00781         ui.add_from_file(rb.find_plugin_file(controller.plugin,
00782                                              'ui/coverart_listwindow.ui'))
00783         ui.connect_signals(self)
00784         self._listwindow = ui.get_object('listwindow')
00785         self._liststore = ui.get_object('liststore')
00786         self._listwindow.set_size_request(200, 300)
00787         self._treeview = ui.get_object('treeview')
00788         self._scrollwindow = ui.get_object('scrolledwindow')
00789         self._scrolldown_button = ui.get_object('scrolldown_button')
00790         self._increment = False
00791 
00792         OptionsWidget.controller.fset(self, controller)
00793 
00794     def update_options(self):
00795         self.clear_options()
00796         self.add_options(self._controller.options)
00797 
00798     def update_current_key(self):
00799         self.select(self.controller.get_current_key_index())
00800 
00801     def do_item_clicked(self, key):
00802         if self._controller:
00803             # inform the controller
00804             self._controller.option_selected(key)
00805 
00806     def show_popup(self):
00807         '''
00808         show the listview window either above or below the controlling
00809         widget depending upon where the cursor position is relative to the
00810         screen
00811         params - x & y is the cursor position
00812         '''
00813         pos_x, pos_y = self.calc_popup_position(self._listwindow)
00814 
00815         self._listwindow.move(pos_x, pos_y)
00816         self._listwindow.show_all()
00817 
00818     def clear_options(self):
00819         self._liststore.clear()
00820 
00821     def add_options(self, iterable):
00822         for label in iterable:
00823             self._liststore.append((label,))
00824 
00825     def select(self, index):
00826         self._treeview.get_selection().select_iter(self._liststore[index].iter)
00827         self._treeview.scroll_to_cell(self._liststore[index].path)
00828 
00829     def on_button_click(self, view, arg):
00830         try:
00831             liststore, viewiter = view.get_selection().get_selected()
00832             label = liststore.get_value(viewiter, 0)
00833             self.emit('item-clicked', label)
00834         except:
00835             pass
00836 
00837         self._treeview.set_hover_selection(False)
00838         self._listwindow.hide()
00839         self.emit('deactivate')
00840 
00841     def on_scroll_button_enter(self, button):
00842 
00843         def scroll(*args):
00844             if self._increment:
00845                 if button is self._scrolldown_button:
00846                     adjustment.set_value(adjustment.get_value()
00847                                          + self._step)
00848                 else:
00849                     adjustment.set_value(adjustment.get_value()
00850                                          - self._step)
00851 
00852             return self._increment
00853 
00854         self._increment = True
00855 
00856         adjustment = self._scrollwindow.get_vadjustment()
00857         self.on_scroll_button_released()
00858 
00859         Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 50,
00860                                 scroll, None)
00861 
00862     def on_scroll_button_leave(self, *args):
00863         self._increment = False
00864 
00865     def on_scroll_button_pressed(self, *args):
00866         adjustment = self._scrollwindow.get_vadjustment()
00867         self._step = adjustment.get_page_increment()
00868 
00869     def on_scroll_button_released(self, *args):
00870         adjustment = self._scrollwindow.get_vadjustment()
00871         self._step = adjustment.get_step_increment()
00872 
00873     def on_treeview_enter_notify_event(self, *args):
00874         self._treeview.set_hover_selection(True)
00875 
00876     def on_cancel(self, *args):
00877         self._listwindow.hide()
00878         self.emit('deactivate')
00879         return True
00880 
00881     def do_delete_thyself(self):
00882         self.clear_list()
00883         del self._listwindow
00884 
00885 
00886 class ListViewButton(PixbufButton, OptionsListViewWidget):
00887     __gtype_name__ = "ListViewButton"
00888 
00889     # signals
00890     __gsignals__ = {
00891         'item-clicked': (GObject.SIGNAL_RUN_LAST, None, (str,)),
00892         'deactivate': (GObject.SIGNAL_RUN_LAST, None, ())
00893     }
00894 
00895     def __init__(self, *args, **kwargs):
00896         '''
00897         Initializes the button.
00898         '''
00899         PixbufButton.__init__(self, *args, **kwargs)
00900         OptionsListViewWidget.__init__(self, *args, **kwargs)
00901 
00902         self._popup.connect('deactivate', self.popup_deactivate)
00903 
00904     def popup_deactivate(self, *args):
00905         # add a slight delay to allow the click of button to occur
00906         # before the deactivation of the button - this will allow
00907         # us to toggle the popup via the button correctly
00908 
00909         def deactivate(*args):
00910             self.set_active(False)
00911 
00912         Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 50, deactivate, None)
00913 
00914     def update_image(self):
00915         super(ListViewButton, self).update_image()
00916         self.set_image(self._controller.get_current_image())
00917 
00918     def update_current_key(self):
00919         super(ListViewButton, self).update_current_key()
00920 
00921         # update the current image and tooltip
00922         self.set_image(self._controller.get_current_image())
00923         self.set_tooltip_text(self._controller.get_current_description())
00924 
00925     def do_button_press_event(self, event):
00926         '''
00927         when button is clicked, update the popup with the sorting options
00928         before displaying the popup
00929         '''
00930         if (event.button == Gdk.BUTTON_PRIMARY and not self.get_active()):
00931             self.show_popup()
00932             self.set_active(True)
00933         else:
00934             self.set_active(False)
00935 
00936 
00937 class EnhancedIconView(Gtk.IconView):
00938     __gtype_name__ = "EnhancedIconView"
00939 
00940     # signals
00941     __gsignals__ = {
00942         'item-clicked': (GObject.SIGNAL_RUN_LAST, None, (object, object))
00943     }
00944 
00945     object_column = GObject.property(type=int, default=-1)
00946 
00947     def __init__(self, *args, **kwargs):
00948         super(EnhancedIconView, self).__init__(*args, **kwargs)
00949 
00950         self._reallocate_count = 0
00951         self.view_name = None
00952         self.source = None
00953         self.ext_menu_pos = 0
00954 
00955     def do_size_allocate(self, allocation):
00956         '''
00957         Forces the reallocation of the IconView columns when the width of the
00958         widgets changes. Neverthless, it takes into account that multiple
00959         reallocations could happen in a short amount of time, so it avoids
00960         trying to refresh until the user has stopped resizing the component.
00961         '''
00962         if self.get_allocated_width() != allocation.width:
00963             # don't need to reaccommodate if it's a vertical change
00964             self._reallocate_count += 1
00965             Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 500,
00966                                     self._reallocate_columns, None)
00967 
00968         Gtk.IconView.do_size_allocate(self, allocation)
00969 
00970     def _reallocate_columns(self, *args):
00971         self._reallocate_count -= 1
00972 
00973         if not self._reallocate_count:
00974             self.set_columns(0)
00975             self.set_columns(-1)
00976 
00977     def do_button_press_event(self, event):
00978         '''
00979         Other than the default behavior, adds an event firing when the mouse
00980         has clicked on top of a current item, informing the listeners of the
00981         path of the clicked item.
00982         '''
00983         x = int(event.x)
00984         y = int(event.y)
00985         current_path = self.get_path_at_pos(x, y)
00986 
00987         if event.type is Gdk.EventType.BUTTON_PRESS and current_path:
00988             if event.triggers_context_menu():
00989                 # if the item being clicked isn't selected, we should clear
00990                 # the current selection
00991                 if len(self.get_selected_objects()) > 0 and \
00992                         not self.path_is_selected(current_path):
00993                     self.unselect_all()
00994 
00995                 self.select_path(current_path)
00996                 self.set_cursor(current_path, None, False)
00997 
00998                 if self.popup:
00999                     self.popup.popup(self.source, 'popup_menu', event.button, event.time)
01000             else:
01001                 self.emit('item-clicked', event, current_path)
01002 
01003         Gtk.IconView.do_button_press_event(self, event)
01004 
01005     def get_selected_objects(self):
01006         '''
01007         Helper method that simplifies getting the objects stored on the
01008         selected items, givent that the object_column property is setted.
01009         This way there's no need for the client class to repeateadly access the
01010         correct column to retrieve the object from the raw rows.
01011         '''
01012         selected_items = self.get_selected_items()
01013 
01014         if not self.object_column:
01015             # if no object_column is setted, return the selected rows
01016             return selected_items
01017 
01018         model = self.get_model()
01019         selected_objects = list(reversed([model[selected][self.object_column]
01020                                           for selected in selected_items]))
01021 
01022         return selected_objects
01023 
01024     def select_and_scroll_to_path(self, path):
01025         '''
01026         Helper method to select and scroll to a given path on the IconView.
01027         '''
01028         self.unselect_all()
01029         self.select_path(path)
01030         self.set_cursor(path, None, False)
01031         self.scroll_to_path(path, True, 0.5, 0.5)
01032 
01033 
01034 class PanedCollapsible(Gtk.Paned):
01035     __gtype_name__ = "PanedCollapsible"
01036 
01037     # properties
01038     # this two properties indicate which one of the Paned childs is collapsible
01039     # only one can be True at a time, the widget takes care of keeping this
01040     # restriction consistent.
01041     collapsible1 = GObject.property(type=bool, default=False)
01042     collapsible2 = GObject.property(type=bool, default=False)
01043 
01044     # values for expand method
01045     Paned = enum(DEFAULT=1, EXPAND=2, COLLAPSE=3)
01046 
01047     # this indicates the latest position for the handle before a child was
01048     # collapsed
01049     collapsible_y = GObject.property(type=int, default=0)
01050 
01051     # label for the Expander used on the collapsible child
01052     collapsible_label = GObject.property(type=str, default='')
01053 
01054     # signals
01055     __gsignals__ = {
01056         'expanded': (GObject.SIGNAL_RUN_LAST, None, (bool,))
01057     }
01058 
01059     def __init__(self, *args, **kwargs):
01060         super(PanedCollapsible, self).__init__(*args, **kwargs)
01061 
01062         self._connect_properties()
01063 
01064     def _connect_properties(self):
01065         self.connect('notify::collapsible1', self._on_collapsible1_changed)
01066         self.connect('notify::collapsible2', self._on_collapsible2_changed)
01067         self.connect('notify::collapsible_label',
01068                      self._on_collapsible_label_changed)
01069 
01070     def _on_collapsible1_changed(self, *args):
01071         if self.collapsible1 and self.collapsible2:
01072             # check consistency, only one collapsible at a time
01073             self.collapsible2 = False
01074 
01075         child = self.get_child1()
01076 
01077         self._wrap_unwrap_child(child, self.collapsible1, self.add1)
01078 
01079     def _on_collapsible2_changed(self, *args):
01080         if self.collapsible1 and self.collapsible2:
01081             # check consistency, only one collapsible at a time
01082             self.collapsible1 = False
01083 
01084         child = self.get_child2()
01085 
01086         self._wrap_unwrap_child(child, self.collapsible2, self.add2)
01087 
01088     def _wrap_unwrap_child(self, child, wrap, add):
01089         if child:
01090             self.remove(child)
01091 
01092             if not wrap:
01093                 inner_child = child.get_child()
01094                 child.remove(inner_child)
01095                 child = inner_child
01096 
01097             add(child)
01098 
01099     def _on_collapsible_label_changed(self, *args):
01100         if self._expander:
01101             self._expander.set_label(self.collapsible_label)
01102 
01103     def _on_collapsible_expanded(self, *args):
01104         expand = self._expander.get_expanded()
01105 
01106         if not expand:
01107             self.collapsible_y = self.get_position()
01108 
01109             # move the lower pane to the bottom since it's collapsed
01110             self._collapse()
01111         else:
01112             # reinstate the lower pane to it's expanded size
01113             if not self.collapsible_y:
01114                 # if there isn't a saved size, use half of the space
01115                 new_y = self.get_allocated_height() / 2
01116                 self.collapsible_y = new_y
01117 
01118             self.set_position(self.collapsible_y)
01119 
01120         self.emit('expanded', expand)
01121 
01122     def do_button_press_event(self, *args):
01123         '''
01124         This callback allows or denies the paned handle to move depending on
01125         the expanded expander
01126         '''
01127         if not self._expander or self._expander.get_expanded():
01128             Gtk.Paned.do_button_press_event(self, *args)
01129 
01130     def do_button_release_event(self, *args):
01131         '''
01132         Callback when the paned handle is released from its mouse click.
01133         '''
01134         if not self._expander or self._expander.get_expanded():
01135             Gtk.Paned.do_button_release_event(self, *args)
01136             self.collapsible_y = self.get_position()
01137 
01138     def do_remove(self, widget):
01139         '''
01140         Overwrites the super class remove method, taking care of removing the
01141         child even if it's wrapped inside an Expander.
01142         '''
01143         if self.collapsible1 and self.get_child1().get_child() is widget:
01144             expander = self.get_child1()
01145             expander.remove(widget)
01146             widget = expander
01147         elif self.collapsible2 and self.get_child2().get_child() is widget:
01148             expander = self.get_child2()
01149             expander.remove(widget)
01150             widget = expander
01151 
01152         self._expander = None
01153 
01154         Gtk.Paned.remove(self, widget)
01155 
01156     def do_add(self, widget):
01157         '''
01158         This method had to be overridden to allow the add and packs method to
01159         work with Glade.
01160         '''
01161         if not self.get_child1():
01162             self.do_add1(widget)
01163         elif not self.get_child2():
01164             self.do_add2(widget)
01165         else:
01166             print("GtkPaned cannot have more than 2 children")
01167 
01168     def do_add1(self, widget):
01169         '''
01170         Overrides the add1 superclass' method for pack1 to work correctly.
01171         '''
01172         self.do_pack1(widget, True, True)
01173 
01174     def do_pack1(self, widget, *args, **kwargs):
01175         '''
01176         Packs the widget into the first paned child, adding a GtkExpander
01177         around the packed widget if the collapsible1 property is True.
01178         '''
01179         if self.collapsible1:
01180             widget = self._create_expander(widget)
01181 
01182         Gtk.Paned.pack1(self, widget, *args, **kwargs)
01183 
01184     def do_add2(self, widget):
01185         '''
01186         Overrides the add2 superclass' method for pack2 to work correctly.
01187         '''
01188         self.do_pack2(widget, True, True)
01189 
01190     def do_pack2(self, widget, *args, **kwargs):
01191         '''
01192         Packs the widget into the second paned child, adding a GtkExpander
01193         around the packed widget if the collapsible2 property is True.
01194         '''
01195         if self.collapsible2:
01196             widget = self._create_expander(widget)
01197 
01198         Gtk.Paned.pack2(self, widget, *args, **kwargs)
01199 
01200     def _create_expander(self, widget):
01201         self._expander = Gtk.Expander(label=self.collapsible_label,
01202                                       visible=True)
01203         self._expander.add(widget)
01204 
01205         # connect the expanded signal
01206         self._expander.connect('notify::expanded',
01207                                self._on_collapsible_expanded)
01208 
01209         # connect the initial collapse
01210         self._allocate_id = self._expander.connect('size-allocate',
01211                                                    self._initial_collapse)
01212 
01213         return self._expander
01214 
01215     def _initial_collapse(self, *args):
01216         self._collapse()
01217         self._expander.disconnect(self._allocate_id)
01218         del self._allocate_id
01219 
01220     def _collapse(self):
01221         new_y = self.get_allocated_height() - \
01222                 self.get_handle_window().get_height() - \
01223                 self._expander.get_label_widget().get_allocated_height()
01224 
01225         self.set_position(new_y)
01226 
01227     def expand(self, force):
01228         '''
01229         Toggles the expanded property of the collapsible children.
01230         unless requested to force expansion
01231         '''
01232         if self._expander:
01233             if force == PanedCollapsible.Paned.EXPAND:
01234                 self._expander.set_expanded(True)
01235             elif force == PanedCollapsible.Paned.COLLAPSE:
01236                 self._expander.set_expanded(False)
01237             elif force == PanedCollapsible.Paned.DEFAULT:
01238                 self._expander.set_expanded(not self._expander.get_expanded())
01239 
01240     def get_expansion_status(self):
01241         '''
01242         returns the position of the expander i.e. expanded or not
01243         '''
01244         value = PanedCollapsible.Paned.COLLAPSE
01245         if self._expander and self._expander.get_expanded():
01246             value = PanedCollapsible.Paned.EXPAND
01247 
01248         return value
01249 
01250 
01251 class AbstractView(GObject.Object):
01252     '''
01253     intention is to document 'the must have' methods all views should define
01254     N.B. this is preliminary and will change as and when
01255     coverflow view is added with lessons learned
01256     '''
01257     view = None
01258     panedposition = PanedCollapsible.Paned.DEFAULT
01259     use_plugin_window = True
01260     # signals - note - pygobject doesnt appear to support signal declaration
01261     # in multiple inheritance - so these signals need to be defined in all view classes
01262     # where abstractview is part of multiple inheritance
01263     __gsignals__ = {
01264         'update-toolbar': (GObject.SIGNAL_RUN_LAST, None, ())
01265     }
01266 
01267     def __init__(self):
01268         super(AbstractView, self).__init__()
01269 
01270     def initialise(self, source):
01271         self.source = source
01272         self.plugin = source.plugin
01273 
01274         self._notification_displayed = 0
01275         Notify.init("coverart_browser")
01276 
01277         self.connect('update-toolbar', self.do_update_toolbar)
01278 
01279     def do_update_toolbar(self, *args):
01280         '''
01281             called when update-toolbar signal is emitted
01282             by default the toolbar objects are made visible
01283         '''
01284         from coverart_toolbar import ToolbarObject
01285 
01286         self.source.toolbar_manager.set_enabled(True, ToolbarObject.SORT_BY)
01287         self.source.toolbar_manager.set_enabled(True, ToolbarObject.SORT_ORDER)
01288         self.source.toolbar_manager.set_enabled(False, ToolbarObject.SORT_BY_ARTIST)
01289         self.source.toolbar_manager.set_enabled(False, ToolbarObject.SORT_ORDER_ARTIST)
01290 
01291     def display_notification(self, title, text, file):
01292 
01293         # first see if the notification plugin is enabled
01294         # if it is, we use standard notifications
01295         # if it is not, we use the infobar
01296 
01297         def hide_notification(*args):
01298             if self._notification_displayed > 7:
01299                 self.source.notification_infobar.response(0)
01300                 self._notification_displayed = 0
01301                 return False
01302 
01303             self._notification_displayed = self._notification_displayed + 1
01304             return True
01305 
01306         notifyext = ExternalPlugin()
01307         notifyext.appendattribute('plugin_name', 'notification')
01308 
01309         if notifyext.is_activated():
01310             n = Notify.Notification.new(title, text, file)
01311             n.show()
01312         else:
01313             self.source.notification_text.set_text(title + " : " + text)
01314             #self.source.notification_infobar.set_visible(True)#reveal_notification.set_reveal_child(True)
01315             self.source.notification_infobar.show()#reveal_notification.set_reveal_child(True)
01316 
01317             if self._notification_displayed == 0:
01318                 Gdk.threads_add_timeout_seconds(GLib.PRIORITY_DEFAULT_IDLE, 1,
01319                                             hide_notification, None)
01320             else:
01321                 self._notification_displayed = 1 # reset notification for new label
01322 
01323 
01324     def resize_icon(self, cover_size):
01325         '''
01326         resize the view main picture icon
01327 
01328         :param cover_size: `int` icon size
01329         '''
01330         pass
01331 
01332     def get_selected_objects(self):
01333         '''
01334         finds what has been selected
01335 
01336         returns an array of `Album`
01337         '''
01338         pass
01339 
01340     def selectionchanged_callback(self, *args):
01341         '''
01342         callback when a selection has changed
01343         '''
01344         self.source.update_with_selection()
01345 
01346     def select_and_scroll_to_path(self, path):
01347         '''
01348         find a path and highlight (select) that object
01349         '''
01350         pass
01351 
01352     def scroll_to_album(self, album):
01353         '''
01354         scroll to the album in the view
01355         '''
01356         if album:
01357             path = self.source.album_manager.model.get_path(album)
01358             if path:
01359                 self.select_and_scroll_to_path(path)
01360 
01361     def set_popup_menu(self, popup):
01362         '''
01363         define the popup menu (right click) used for the view
01364         '''
01365         self.popup = popup
01366 
01367     def grab_focus(self):
01368         '''
01369         ensures main view object retains the focus
01370         '''
01371         pass
01372 
01373     def switch_to_view(self, source, album):
01374         '''
01375         ensures that when the user toggles to a view stuff remains
01376         consistent
01377         '''
01378         pass
01379 
01380     def get_view_icon_name(self):
01381         '''
01382         every view should have an icon - subject to removal
01383         since we'll probably just have text buttons for the view
01384         '''
01385         return ""
01386 
01387     def get_default_manager(self):
01388         '''
01389         every view should have a default manager
01390         for example an AlbumManager or ArtistManager
01391         by default - use the AlbumManager from the source
01392         '''
01393 
01394         return self.source.album_manager
01395 
01396     def switch_to_coverpane(self, cover_search_pane):
01397         '''
01398         called from the source to update the coverpane when
01399         it is switched from the track pane
01400         '''
01401 
01402         selected = self.get_selected_objects()
01403 
01404         if selected:
01405             manager = self.get_default_manager()
01406             cover_search_pane.do_search(selected[0],
01407                                         manager.cover_man.update_cover)
01408 
 All Classes Functions