1# Copyright (C) 2008-2010 Adam Olsen 2# 3# This program is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; either version 2, or (at your option) 6# any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software 15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16# 17# 18# The developers of the Exaile media player hereby grant permission 19# for non-GPL compatible GStreamer and Exaile plugins to be used and 20# distributed together with GStreamer and Exaile. This permission is 21# above and beyond the permissions granted by the GPL license by which 22# Exaile is covered. If you modify this code, you may extend this 23# exception to your version of the code, but you are not obligated to 24# do so. If you do not wish to do so, delete this exception statement 25# from your version. 26 27from gi.repository import Gdk 28from gi.repository import GdkPixbuf 29from gi.repository import GLib 30from gi.repository import GObject 31from gi.repository import Gtk 32import itertools 33import logging 34 35from xl.nls import gettext as _ 36from xl import common, event, formatter, settings, trax 37import xlgui 38from xlgui import guiutil, icons, panel 39from xlgui.panel import menus 40from xlgui.widgets import menu 41from xlgui.widgets.common import DragTreeView 42 43logger = logging.getLogger(__name__) 44 45# TODO: come up with a more customizable way to handle this 46SEARCH_TAGS = ("artist", "albumartist", "album", "title") 47 48 49def first_meaningful_char(s): 50 # Keep explicit str() conversion in case we ever end up receiving 51 # a non-string sort tag (e.g. an int for track number) 52 for c in str(s): 53 if c.isdigit(): 54 return '0' 55 elif c.isalpha(): 56 return c 57 else: 58 return '_' 59 60 61class Order: 62 """ 63 An Order represents a structure for arranging Tracks into the 64 Collection tree. 65 66 It is based on a list of levels, which each take the form (("sort1", 67 "sort2"), "$displaytag - $displaytag", ("search1", "search2")) wherin 68 the first entry is a tuple of tags to use for sorting, the second a 69 format string for xl.formatter, and the third a tuple of tags to use 70 for searching. 71 72 When passed in the parameters, a level can also be a single string 73 instead of a tuple, and it will be treated equivalently to (("foo",), 74 "$foo", ("foo",)) for some string "foo". 75 """ 76 77 def __init__(self, name, levels, use_compilations=True): 78 self.__name = name 79 self.__levels = [self.__parse_level(l) for l in levels] 80 self.__formatters = [formatter.TrackFormatter(l[1]) for l in self.__levels] 81 self.__use_compilations = use_compilations 82 83 @staticmethod 84 def __parse_level(val): 85 if isinstance(val, str): 86 val = ((val,), "$%s" % val, (val,)) 87 return tuple(val) 88 89 @property 90 def name(self): 91 return self.__name 92 93 @property 94 def use_compilations(self): 95 return self.__use_compilations 96 97 def get_levels(self): 98 return self.__levels[:] 99 100 def __len__(self): 101 return len(self.__levels) 102 103 def __eq__(self, other): 104 return self.__levels == other.get_levels() 105 106 def all_sort_tags(self): 107 return set(itertools.chain(*[l[0] for l in self.__levels])) 108 109 def get_sort_tags(self, level): 110 return list(self.__levels[level][0]) 111 112 def all_search_tags(self): 113 return list(itertools.chain(*[l[2] for l in self.__levels])) 114 115 def get_search_tags(self, level): 116 return list(set(self.__levels[level][2])) 117 118 def format_track(self, level, track): 119 return self.__formatters[level].format(track) 120 121 122DEFAULT_ORDERS = [ 123 # fmt: off 124 Order(_("Artist"), 125 ("artist", "album", 126 (("discnumber", "tracknumber", "title"), "$title", ("title",)))), 127 Order(_("Album Artist"), 128 ("albumartist", "album", 129 (("discnumber", "tracknumber", "title"), "$title", ("title",)))), 130 Order(_("Album"), 131 ("album", 132 (("discnumber", "tracknumber", "title"), "$title", ("title",)))), 133 Order(_("Genre - Artist"), 134 ('genre', 'artist', 'album', 135 (("discnumber", "tracknumber", "title"), "$title", ("title",)))), 136 Order(_("Genre - Album Artist"), 137 ('genre', 'albumartist', 'album', 138 (("discnumber", "tracknumber", "title"), "$title", ("title",)))), 139 Order(_("Genre - Album"), 140 ('genre', 'album', 'artist', 141 (("discnumber", "tracknumber", "title"), "$title", ("title",)))), 142 Order(_("Date - Artist"), 143 ('date', 'artist', 'album', 144 (("discnumber", "tracknumber", "title"), "$title", ("title",)))), 145 Order(_("Date - Album Artist"), 146 ('date', 'albumartist', 'album', 147 (("discnumber", "tracknumber", "title"), "$title", ("title",)))), 148 Order(_("Date - Album"), 149 ('date', 'album', 'artist', 150 (("discnumber", "tracknumber", "title"), "$title", ("title",)))), 151 Order(_("Artist - (Date - Album)"), 152 ('artist', 153 (('date', 'album'), "$date - $album", ('date', 'album')), 154 (("discnumber", "tracknumber", "title"), "$title", ("title",)))), 155 Order(_("Album Artist - (Date - Album)"), 156 ('albumartist', 157 (('date', 'album'), "$date - $album", ('date', 'album')), 158 (("discnumber", "tracknumber", "title"), "$title", ("title",)))), 159 # fmt: on 160] 161 162 163class CollectionPanel(panel.Panel): 164 """ 165 The collection panel 166 """ 167 168 __gsignals__ = { 169 'append-items': (GObject.SignalFlags.RUN_LAST, None, (object, bool)), 170 'replace-items': (GObject.SignalFlags.RUN_LAST, None, (object,)), 171 'queue-items': (GObject.SignalFlags.RUN_LAST, None, (object,)), 172 'collection-tree-loaded': (GObject.SignalFlags.RUN_LAST, None, ()), 173 } 174 175 ui_info = ('collection.ui', 'CollectionPanel') 176 177 def __init__( 178 self, 179 parent, 180 collection, 181 name=None, 182 _show_collection_empty_message=False, 183 label=_('Collection'), 184 ): 185 """ 186 Initializes the collection panel 187 188 @param parent: the parent dialog 189 @param collection: the xl.collection.Collection instance 190 @param name: an optional name for this panel 191 """ 192 panel.Panel.__init__(self, parent, name, label) 193 194 self._show_collection_empty_message = _show_collection_empty_message 195 self.collection = collection 196 self.use_alphabet = settings.get_option('gui/use_alphabet', True) 197 self.panel_stack = self.builder.get_object('CollectionPanel') 198 self.panel_content = self.builder.get_object('CollectionPanelContent') 199 self.panel_empty = self.builder.get_object('CollectionPanelEmpty') 200 self.choice = self.builder.get_object('collection_combo_box') 201 self._search_num = 0 202 self._refresh_id = 0 203 self.start_count = 0 204 self.keyword = '' 205 self.orders = DEFAULT_ORDERS[:] 206 self._setup_tree() 207 self._setup_widgets() 208 self._check_collection_empty() 209 self._setup_images() 210 self._connect_events() 211 self.order = None 212 self.tracks = [] 213 self.sorted_tracks = [] 214 215 event.add_ui_callback( 216 self._check_collection_empty, 'libraries_modified', collection 217 ) 218 219 self.menu = menus.CollectionContextMenu(self) 220 221 self.load_tree() 222 223 def _setup_widgets(self): 224 """ 225 Sets up the various widgets to be used in this panel 226 """ 227 self.choice = self.builder.get_object('collection_combo_box') 228 self.choicemodel = self.builder.get_object('collection_combo_model') 229 self.repopulate_choices() 230 231 self.filter = guiutil.SearchEntry( 232 self.builder.get_object('collection_search_entry') 233 ) 234 235 def repopulate_choices(self): 236 self.choice.set_model(None) 237 self.choicemodel.clear() 238 for order in self.orders: 239 self.choicemodel.append([order.name]) 240 self.choice.set_model(self.choicemodel) 241 # FIXME: use something other than index here, since index 242 # doesn't deal well with dynamic lists... 243 active = settings.get_option('gui/collection_active_view', 0) 244 self.choice.set_active(active) 245 246 def _check_collection_empty(self, *e): 247 if self._show_collection_empty_message and not self.collection.libraries: 248 # should show empty panel 249 if self.panel_stack.get_visible_child() == self.panel_content: 250 self.panel_stack.set_visible_child(self.panel_empty) 251 else: 252 # should show content panel 253 if self.panel_stack.get_visible_child() == self.panel_empty: 254 self.panel_stack.set_visible_child(self.panel_content) 255 256 def _connect_events(self): 257 """ 258 Uses signal_autoconnect to connect the various events 259 """ 260 self.builder.connect_signals( 261 { 262 'on_collection_combo_box_changed': lambda *e: self.load_tree(), 263 'on_refresh_button_press_event': self.on_refresh_button_press_event, 264 'on_refresh_button_key_press_event': self.on_refresh_button_key_press_event, 265 'on_collection_search_entry_activate': self.on_collection_search_entry_activate, 266 'on_add_music_button_clicked': self.on_add_music_button_clicked, 267 } 268 ) 269 self.tree.connect('key-release-event', self.on_key_released) 270 event.add_ui_callback(self.refresh_tags_in_tree, 'track_tags_changed') 271 event.add_ui_callback( 272 self.refresh_tracks_in_tree, 'tracks_added', self.collection 273 ) 274 event.add_ui_callback( 275 self.refresh_tracks_in_tree, 'tracks_removed', self.collection 276 ) 277 278 def on_refresh_button_press_event(self, button, event): 279 """ 280 Called on mouse activation of the refresh button 281 """ 282 if event.triggers_context_menu(): 283 m = menu.Menu(None) 284 m.attach_to_widget(button) 285 m.add_simple( 286 _('Rescan Collection'), 287 xlgui.get_controller().on_rescan_collection, 288 Gtk.STOCK_REFRESH, 289 ) 290 m.popup(event) 291 return 292 293 if event.get_state() & Gdk.ModifierType.SHIFT_MASK: 294 xlgui.get_controller().on_rescan_collection(None) 295 else: 296 self.load_tree() 297 298 def on_refresh_button_key_press_event(self, widget, event): 299 """ 300 Called on key presses on the refresh button 301 """ 302 if event.keyval != Gdk.KEY_Return: 303 return False 304 305 if event.get_state() & Gdk.ModifierType.SHIFT_MASK: 306 xlgui.get_controller().on_rescan_collection(None) 307 else: 308 self.load_tree() 309 310 def on_key_released(self, widget, event): 311 """ 312 Called when a key is released in the tree 313 """ 314 if event.keyval == Gdk.KEY_Menu: 315 Gtk.Menu.popup(self.menu, None, None, None, None, 0, event.time) 316 return True 317 318 if event.keyval == Gdk.KEY_Left: 319 (mods, paths) = self.tree.get_selection().get_selected_rows() 320 for path in paths: 321 self.tree.collapse_row(path) 322 return True 323 324 if event.keyval == Gdk.KEY_Right: 325 (mods, paths) = self.tree.get_selection().get_selected_rows() 326 for path in paths: 327 self.tree.expand_row(path, False) 328 return True 329 330 if event.keyval == Gdk.KEY_Return: 331 self.append_to_playlist() 332 return True 333 return False 334 335 def on_collection_search_entry_activate(self, entry): 336 """ 337 Searches tracks and reloads the tree 338 """ 339 self.keyword = entry.get_text() 340 self.start_count += 1 341 self.load_tree() 342 343 def on_add_music_button_clicked(self, button): 344 xlgui.get_controller().collection_manager() 345 346 def _setup_images(self): 347 """ 348 Sets up the various images that will be used in the tree 349 """ 350 self.artist_image = icons.MANAGER.pixbuf_from_icon_name( 351 'artist', Gtk.IconSize.SMALL_TOOLBAR 352 ) 353 self.albumartist_image = icons.MANAGER.pixbuf_from_icon_name( 354 'artist', Gtk.IconSize.SMALL_TOOLBAR 355 ) 356 self.date_image = icons.MANAGER.pixbuf_from_icon_name( 357 'office-calendar', Gtk.IconSize.SMALL_TOOLBAR 358 ) 359 self.album_image = icons.MANAGER.pixbuf_from_icon_name( 360 'image-x-generic', Gtk.IconSize.SMALL_TOOLBAR 361 ) 362 self.title_image = icons.MANAGER.pixbuf_from_icon_name( 363 'audio-x-generic', Gtk.IconSize.SMALL_TOOLBAR 364 ) 365 self.genre_image = icons.MANAGER.pixbuf_from_icon_name( 366 'genre', Gtk.IconSize.SMALL_TOOLBAR 367 ) 368 369 def drag_data_received(self, *e): 370 """ 371 stub 372 """ 373 pass 374 375 def drag_data_delete(self, *e): 376 """ 377 stub 378 """ 379 pass 380 381 def drag_get_data(self, treeview, context, selection, target_id, etime): 382 """ 383 Called when a drag source wants data for this drag operation 384 """ 385 tracks = treeview.get_selected_tracks() 386 387 for track in tracks: 388 DragTreeView.dragged_data[track.get_loc_for_io()] = track 389 390 uris = trax.util.get_uris_from_tracks(tracks) 391 selection.set_uris(uris) 392 393 def _setup_tree(self): 394 """ 395 Sets up the tree widget 396 """ 397 self.tree = CollectionDragTreeView(self) 398 self.tree.set_headers_visible(False) 399 scroll = Gtk.ScrolledWindow() 400 scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) 401 scroll.add(self.tree) 402 scroll.set_shadow_type(Gtk.ShadowType.IN) 403 self.panel_content.pack_start(scroll, True, True, 0) 404 self.panel_content.show_all() 405 406 selection = self.tree.get_selection() 407 selection.set_mode(Gtk.SelectionMode.MULTIPLE) 408 pb = Gtk.CellRendererPixbuf() 409 cell = Gtk.CellRendererText() 410 col = Gtk.TreeViewColumn('Text') 411 col.pack_start(pb, False) 412 col.pack_start(cell, True) 413 col.set_attributes(pb, pixbuf=0) 414 col.set_attributes(cell, text=1) 415 self.tree.append_column(col) 416 417 if settings.get_option('gui/ellipsize_text_in_panels', False): 418 from gi.repository import Pango 419 420 cell.set_property('ellipsize-set', True) 421 cell.set_property('ellipsize', Pango.EllipsizeMode.END) 422 423 self.tree.set_row_separator_func( 424 (lambda m, i, d: m.get_value(i, 1) is None), None 425 ) 426 427 self.model = Gtk.TreeStore(GdkPixbuf.Pixbuf, str, object) 428 429 self.tree.connect("row-expanded", self.on_expanded) 430 431 def _find_tracks(self, iter): 432 """ 433 finds tracks matching a given iter. 434 """ 435 self.load_subtree(iter) 436 search = self.get_node_search_terms(iter) 437 matcher = trax.TracksMatcher(search) 438 srtrs = trax.search_tracks(self.tracks, [matcher]) 439 return [x.track for x in srtrs] 440 441 def append_to_playlist(self, item=None, event=None, replace=False): 442 """ 443 Adds items to the current playlist 444 """ 445 if replace: 446 self.emit('replace-items', self.tree.get_selected_tracks()) 447 else: 448 self.emit('append-items', self.tree.get_selected_tracks(), True) 449 450 def button_press(self, widget, event): 451 """ 452 Called when the user clicks on the tree 453 """ 454 # selection = self.tree.get_selection() 455 (x, y) = [int(v) for v in event.get_coords()] 456 # path = self.tree.get_path_at_pos(x, y) 457 if event.type == Gdk.EventType._2BUTTON_PRESS: 458 replace = settings.get_option('playlist/replace_content', False) 459 self.append_to_playlist(replace=replace) 460 return False 461 elif event.button == Gdk.BUTTON_MIDDLE: 462 self.append_to_playlist(replace=True) 463 return False 464 465 def on_expanded(self, tree, iter, path): 466 """ 467 Called when a user expands a tree item. 468 469 Loads the various nodes that belong under this node. 470 """ 471 self.load_subtree(iter) 472 473 def get_node_search_terms(self, node): 474 """ 475 Finds all the related search terms for a particular node 476 @param node: the node you wish to create search terms 477 """ 478 if not node: 479 return "" 480 481 queries = [] 482 while node: 483 queries.append(self.model.get_value(node, 2)) 484 node = self.model.iter_parent(node) 485 486 return " ".join(queries) 487 488 def refresh_tags_in_tree(self, type, track, tags): 489 if ( 490 settings.get_option('gui/sync_on_tag_change', True) 491 and bool(tags & self.order.all_sort_tags()) 492 and self.collection.loc_is_member(track.get_loc_for_io()) 493 ): 494 self._refresh_tags_in_tree() 495 496 def refresh_tracks_in_tree(self, type, obj, loc): 497 self._refresh_tags_in_tree() 498 499 @common.glib_wait(500) 500 def _refresh_tags_in_tree(self): 501 """ 502 Callback for when tags have changed and the tree 503 needs reloading. 504 """ 505 # Trying to reload while we're rescanning is really inefficient, 506 # so we delay it until we're done scanning. 507 if self.collection._scanning: 508 return True 509 self.resort_tracks() 510 self.load_tree() 511 return False 512 513 def resort_tracks(self): 514 # import time 515 # print("sorting...", time.clock()) 516 self.sorted_tracks = trax.sort_tracks( 517 self.order.get_sort_tags(0), self.collection.get_tracks() 518 ) 519 # print("sorted.", time.clock()) 520 521 def load_tree(self): 522 """ 523 Loads the Gtk.TreeView for this collection panel. 524 525 Loads tracks based on the current keyword, or all the tracks in 526 the collection associated with this panel 527 """ 528 logger.debug("Reloading collection tree") 529 self.current_start_count = self.start_count 530 self.tree.set_model(None) 531 self.model.clear() 532 533 self.root = None 534 oldorder = self.order 535 self.order = self.orders[self.choice.get_active()] 536 537 if not oldorder or oldorder != self.order: 538 self.resort_tracks() 539 540 # save the active view setting 541 settings.set_option('gui/collection_active_view', self.choice.get_active()) 542 543 keyword = self.keyword.strip() 544 tags = list(SEARCH_TAGS) 545 tags += self.order.all_search_tags() 546 tags = list(set(tags)) # uniquify list to speed up search 547 548 self.tracks = list( 549 trax.search_tracks_from_string( 550 self.sorted_tracks, keyword, case_sensitive=False, keyword_tags=tags 551 ) 552 ) 553 554 self.load_subtree(None) 555 556 self.tree.set_model(self.model) 557 558 self.emit('collection-tree-loaded') 559 560 def _expand_node_by_name(self, search_num, parent, name, rest=None): 561 """ 562 Recursive function to expand all nodes in a hierarchical list of 563 names. 564 565 @param search_num: the current search number 566 @param parent: the parent node 567 @param name: the name of the node to expand 568 @param rest: the list of the nodes to expand after this one 569 """ 570 iter = self.model.iter_children(parent) 571 572 while iter: 573 if search_num != self._search_num: 574 return 575 value = self.model.get_value(iter, 1) 576 if not value: 577 value = self.model.get_value(iter, 2) 578 579 if value == name: 580 self.tree.expand_row(self.model.get_path(iter), False) 581 parent = iter 582 break 583 584 iter = self.model.iter_next(iter) 585 586 if rest: 587 item = rest.pop(0) 588 GLib.idle_add(self._expand_node_by_name, search_num, parent, item, rest) 589 590 def load_subtree(self, parent): 591 """ 592 Loads all the sub nodes for a specified node 593 594 @param node: the node 595 """ 596 previously_loaded = False # was the subtree already loaded 597 iter_sep = None 598 if parent is None: 599 depth = 0 600 else: 601 if ( 602 self.model.iter_n_children(parent) != 1 603 or self.model.get_value(self.model.iter_children(parent), 1) is not None 604 ): 605 previously_loaded = True 606 iter_sep = self.model.iter_children(parent) 607 depth = self.model.iter_depth(parent) + 1 608 if previously_loaded: 609 return 610 611 search = self.get_node_search_terms(parent) 612 613 try: 614 tags = self.order.get_sort_tags(depth) 615 matchers = [trax.TracksMatcher(search)] 616 srtrs = trax.search_tracks(self.tracks, matchers) 617 # sort only if we are not on top level, because tracks are 618 # already sorted by fist order 619 if depth > 0: 620 srtrs = trax.sort_result_tracks(tags, srtrs) 621 except IndexError: 622 return # at the bottom of the tree 623 try: 624 image = getattr(self, "%s_image" % tags[-1]) 625 except Exception: 626 image = None 627 bottom = False 628 if depth == len(self.order) - 1: 629 bottom = True 630 631 display_counts = settings.get_option('gui/display_track_counts', True) 632 draw_seps = settings.get_option('gui/draw_separators', True) 633 last_char = '' 634 last_val = '' 635 last_dval = '' 636 last_matchq = '' 637 count = 0 638 first = True 639 path = None 640 expanded = False 641 to_expand = [] 642 643 for srtr in srtrs: 644 # The value returned by get_tag_sort() may be of other 645 # typa than str (e.g., an int for track number), hence 646 # explicit conversion via str() is necessary. 647 stagvals = [str(srtr.track.get_tag_sort(x)) for x in tags] 648 stagval = " ".join(stagvals) 649 if last_val != stagval or bottom: 650 tagval = self.order.format_track(depth, srtr.track) 651 match_query = " ".join( 652 [srtr.track.get_tag_search(t, format=True) for t in tags] 653 ) 654 if bottom: 655 match_query += " " + srtr.track.get_tag_search("__loc", format=True) 656 657 # Different *sort tags can cause stagval to not match 658 # but the below code will produce identical entries in 659 # the displayed tree. This condition checks to ensure 660 # that new entries are added if and only if they will 661 # display different results, avoiding that problem. 662 if match_query != last_matchq or tagval != last_dval or bottom: 663 if display_counts and path and not bottom: 664 iter = self.model.get_iter(path) 665 val = self.model.get_value(iter, 1) 666 val = "%s (%s)" % (val, count) 667 self.model.set_value(iter, 1, val) 668 count = 0 669 670 last_val = stagval 671 last_dval = tagval 672 if depth == 0 and draw_seps: 673 val = srtr.track.get_tag_sort(tags[0]) 674 char = first_meaningful_char(val) 675 if first: 676 last_char = char 677 else: 678 if char != last_char and last_char != '': 679 self.model.append(parent, [None, None, None]) 680 last_char = char 681 first = False 682 683 last_matchq = match_query 684 iter = self.model.append(parent, [image, tagval, match_query]) 685 path = self.model.get_path(iter) 686 expanded = False 687 if not bottom: 688 self.model.append(iter, [None, None, None]) 689 count += 1 690 if not expanded: 691 alltags = [] 692 for i in range(depth + 1, len(self.order)): 693 alltags.extend(self.order.get_sort_tags(i)) 694 for t in alltags: 695 if t in srtr.on_tags: 696 # keep original path intact for following block 697 newpath = path 698 if depth > 0: 699 # for some reason, nested iters are always 700 # off by one in the terminal entry. 701 newpath = Gtk.TreePath.new_from_indices( 702 newpath[:-1] + [newpath[-1] - 1] 703 ) 704 to_expand.append(newpath) 705 expanded = True 706 707 if display_counts and path and not bottom: 708 iter = self.model.get_iter(path) 709 val = self.model.get_value(iter, 1) 710 val = "%s (%s)" % (val, count) 711 self.model.set_value(iter, 1, val) 712 count = 0 713 714 if ( 715 settings.get_option("gui/expand_enabled", True) 716 and len(to_expand) < settings.get_option("gui/expand_maximum_results", 100) 717 and len(self.keyword.strip()) 718 >= settings.get_option("gui/expand_minimum_term_length", 2) 719 ): 720 for row in to_expand: 721 GLib.idle_add(self.tree.expand_row, row, False) 722 723 if iter_sep is not None: 724 self.model.remove(iter_sep) 725 726 727class CollectionDragTreeView(DragTreeView): 728 """ 729 Custom DragTreeView to retrieve data 730 from collection tracks 731 """ 732 733 def __init__(self, container, receive=False, source=True): 734 """ 735 :param container: The container to place the TreeView into 736 :param receive: True if the TreeView should receive drag events 737 :param source: True if the TreeView should send drag events 738 """ 739 DragTreeView.__init__(self, container, receive, source) 740 741 self.set_has_tooltip(True) 742 self.connect('query-tooltip', self.on_query_tooltip) 743 744 def get_selection_empty(self): 745 '''Returns True if there are no selected items''' 746 return self.get_selection().count_selected_rows() == 0 747 748 def get_selected_tracks(self): 749 """ 750 Returns the currently selected tracks 751 """ 752 model, paths = self.get_selection().get_selected_rows() 753 754 if len(paths) == 0: 755 return [] 756 757 tracks = set() 758 for path in paths: 759 iter = model.get_iter(path) 760 newset = self.container._find_tracks(iter) 761 tracks.update(newset) 762 763 tracks = list(tracks) 764 765 return trax.sort_tracks(common.BASE_SORT_TAGS, tracks) 766 767 def on_query_tooltip(self, widget, x, y, keyboard_mode, tooltip): 768 """ 769 Sets up a basic tooltip 770 Required to have "&" in tooltips working 771 """ 772 if not widget.get_tooltip_context(x, y, keyboard_mode): 773 return False 774 775 result = widget.get_path_at_pos(x, y) 776 if not result: 777 return False 778 779 path = result[0] 780 781 model = widget.get_model() 782 tooltip.set_text(model[path][1]) # 1: title 783 widget.set_tooltip_row(tooltip, path) 784 785 return True 786 787 def get_tracks_for_path(self, path): 788 """ 789 Get tracks for a path from model (expand item) 790 :param path: Gtk.TreePath 791 :return: list of tracks [xl.trax.Track] 792 """ 793 it = self.get_model().get_iter(path) 794 search = self.container.get_node_search_terms(it) 795 matcher = trax.TracksMatcher(search) 796 for i in trax.search_tracks(self.container.tracks, [matcher]): 797 yield i.track 798 799 800# vim: et sts=4 sw=4 801