CoverArt Browser
v2.0
Browse your cover-art albums in Rhythmbox
|
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