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