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
27import logging
28
29from gi.repository import Gdk
30from gi.repository import GLib
31from gi.repository import GObject
32from gi.repository import Gtk
33
34from xl.nls import gettext as _
35from xl import common, event, formatter, player, providers, settings, trax
36from xlgui.accelerators import AcceleratorManager
37from xlgui.accelerators import Accelerator
38from xlgui.playlist_container import PlaylistContainer
39from xlgui.widgets import dialogs, info, menu, playback
40from xlgui.widgets.playlist import PlaylistPage, PlaylistView
41from xlgui import guiutil, tray, menu as mainmenu
42
43logger = logging.getLogger(__name__)
44
45# Length of playback step when user presses seek key (sec)
46SEEK_STEP_DEFAULT = 10
47
48# Length of volume steps when user presses up/down key
49VOLUME_STEP_DEFAULT = 0.1
50
51
52class MainWindow(GObject.GObject):
53    """
54    Main Exaile Window
55    """
56
57    __gproperties__ = {
58        'is-fullscreen': (
59            bool,
60            'Fullscreen',
61            'Whether the window is fullscreen.',
62            False,  # Default
63            GObject.ParamFlags.READWRITE,
64        )
65    }
66
67    __gsignals__ = {'main-visible-toggle': (GObject.SignalFlags.RUN_LAST, bool, ())}
68
69    _mainwindow = None
70
71    def __init__(self, controller, builder, collection):
72        """
73        Initializes the main window
74
75        @param controller: the main gui controller
76        """
77        GObject.GObject.__init__(self)
78
79        self.controller = controller
80        self.collection = collection
81        self.playlist_manager = controller.exaile.playlists
82        self.current_page = -1
83        self._fullscreen = False
84        self.resuming = False
85
86        self.window_state = 0
87        self.minimized = False
88
89        self.builder = builder
90
91        self.window = self.builder.get_object('ExaileWindow')
92        self.window.set_title('Exaile')
93        self.title_formatter = formatter.TrackFormatter(
94            settings.get_option(
95                'gui/main_window_title_format', _('$title (by $artist)') + ' - Exaile'
96            )
97        )
98
99        self.accel_group = Gtk.AccelGroup()
100        self.window.add_accel_group(self.accel_group)
101        self.accel_manager = AcceleratorManager(
102            'mainwindow-accelerators', self.accel_group
103        )
104        self.menubar = self.builder.get_object("mainmenu")
105
106        fileitem = self.builder.get_object("file_menu_item")
107        filemenu = menu.ProviderMenu('menubar-file-menu', self)
108        fileitem.set_submenu(filemenu)
109
110        edititem = self.builder.get_object("edit_menu_item")
111        editmenu = menu.ProviderMenu('menubar-edit-menu', self)
112        edititem.set_submenu(editmenu)
113
114        viewitem = self.builder.get_object("view_menu_item")
115        viewmenu = menu.ProviderMenu('menubar-view-menu', self)
116        viewitem.set_submenu(viewmenu)
117
118        toolsitem = self.builder.get_object("tools_menu_item")
119        toolsmenu = menu.ProviderMenu('menubar-tools-menu', self)
120        toolsitem.set_submenu(toolsmenu)
121
122        helpitem = self.builder.get_object("help_menu_item")
123        helpmenu = menu.ProviderMenu('menubar-help-menu', self)
124        helpitem.set_submenu(helpmenu)
125
126        self._setup_widgets()
127        self._setup_position()
128        self._setup_hotkeys()
129        logger.info("Connecting main window events...")
130        self._connect_events()
131        MainWindow._mainwindow = self
132
133        mainmenu._create_menus()
134
135    def _setup_hotkeys(self):
136        """
137        Sets up accelerators that haven't been set up in UI designer
138        """
139
140        def factory(integer, description):
141            """ Generate key bindings for Alt keys """
142            keybinding = '<Alt>%s' % str(integer)
143            callback = lambda *_e: self._on_focus_playlist_tab(integer - 1)
144            return (keybinding, description, callback)
145
146        hotkeys = (
147            (
148                '<Primary>S',
149                _('Save currently selected playlist'),
150                lambda *_e: self.on_save_playlist(),
151            ),
152            (
153                '<Shift><Primary>S',
154                _('Save currently selected playlist under a custom name'),
155                lambda *_e: self.on_save_playlist_as(),
156            ),
157            (
158                '<Primary>F',
159                _('Focus filter in currently focused panel'),
160                lambda *_e: self.on_panel_filter_focus(),
161            ),
162            (
163                '<Primary>G',
164                _('Focus playlist search'),
165                lambda *_e: self.on_search_playlist_focus(),
166            ),  # FIXME
167            (
168                '<Primary><Alt>l',
169                _('Clear queue'),
170                lambda *_e: player.QUEUE.clear(),
171            ),  # FIXME
172            (
173                '<Primary>P',
174                _('Start, pause or resume the playback'),
175                self._on_playpause_button,
176            ),
177            (
178                '<Primary>Right',
179                _('Seek to the right'),
180                lambda *_e: self._on_seek_key(True),
181            ),
182            (
183                '<Primary>Left',
184                _('Seek to the left'),
185                lambda *_e: self._on_seek_key(False),
186            ),
187            (
188                '<Primary>plus',
189                _('Increase the volume'),
190                lambda *_e: self._on_volume_key(True),
191            ),
192            (
193                '<Primary>equal',
194                _('Increase the volume'),
195                lambda *_e: self._on_volume_key(True),
196            ),
197            (
198                '<Primary>minus',
199                _('Decrease the volume'),
200                lambda *_e: self._on_volume_key(False),
201            ),
202            ('<Primary>Page_Up', _('Switch to previous tab'), self._on_prev_tab_key),
203            ('<Primary>Page_Down', _('Switch to next tab'), self._on_next_tab_key),
204            (
205                '<Alt>N',
206                _('Focus the playlist container'),
207                self._on_focus_playlist_container,
208            ),
209            # These 4 are subject to change.. probably should do this
210            # via a different mechanism too...
211            (
212                '<Alt>I',
213                _('Focus the files panel'),
214                lambda *_e: self.controller.focus_panel('files'),
215            ),
216            # ('<Alt>C', _('Focus the collection panel'),  # TODO: Does not work, why?
217            # lambda *_e: self.controller.focus_panel('collection')),
218            (
219                '<Alt>R',
220                _('Focus the radio panel'),
221                lambda *_e: self.controller.focus_panel('radio'),
222            ),
223            (
224                '<Alt>L',
225                _('Focus the playlists panel'),
226                lambda *_e: self.controller.focus_panel('playlists'),
227            ),
228            factory(1, _('Focus the first tab')),
229            factory(2, _('Focus the second tab')),
230            factory(3, _('Focus the third tab')),
231            factory(4, _('Focus the fourth tab')),
232            factory(5, _('Focus the fifth tab')),
233            factory(6, _('Focus the sixth tab')),
234            factory(7, _('Focus the seventh tab')),
235            factory(8, _('Focus the eighth tab')),
236            factory(9, _('Focus the ninth tab')),
237            factory(0, _('Focus the tenth tab')),
238        )
239
240        for keys, helptext, function in hotkeys:
241            accelerator = Accelerator(keys, helptext, function)
242            providers.register('mainwindow-accelerators', accelerator)
243
244    def _setup_widgets(self):
245        """
246        Sets up the various widgets
247        """
248        # TODO: Maybe make this stackable
249        self.message = dialogs.MessageBar(
250            parent=self.builder.get_object('player_box'), buttons=Gtk.ButtonsType.CLOSE
251        )
252
253        self.info_area = MainWindowTrackInfoPane(player.PLAYER)
254        self.info_area.set_auto_update(True)
255        self.info_area.set_border_width(3)
256        self.info_area.hide()
257        self.info_area.set_no_show_all(True)
258        guiutil.gtk_widget_replace(self.builder.get_object('info_area'), self.info_area)
259
260        self.volume_control = playback.VolumeControl(player.PLAYER)
261        self.info_area.get_action_area().pack_end(self.volume_control, False, False, 0)
262
263        if settings.get_option('gui/use_alpha', False):
264            screen = self.window.get_screen()
265            visual = screen.get_rgba_visual()
266            self.window.set_visual(visual)
267            self.window.connect('screen-changed', self.on_screen_changed)
268            self._update_alpha()
269
270        self._update_dark_hint()
271
272        playlist_area = self.builder.get_object('playlist_area')
273        self.playlist_container = PlaylistContainer('saved_tabs', player.PLAYER)
274        for notebook in self.playlist_container.notebooks:
275            notebook.connect_after(
276                'switch-page', self.on_playlist_container_switch_page
277            )
278            page = notebook.get_current_tab()
279            if page is not None:
280                selection = page.view.get_selection()
281                selection.connect('changed', self.on_playlist_view_selection_changed)
282
283        playlist_area.pack_start(self.playlist_container, True, True, 3)
284
285        self.splitter = self.builder.get_object('splitter')
286
287        # In most (all?) RTL locales, the playback controls should still be LTR.
288        # Just in case that's not always the case, we provide a hidden option to
289        # force RTL layout instead. This can be removed once we're more certain
290        # that the default behavior (always LTR) is correct.
291        controls_direction = (
292            Gtk.TextDirection.RTL
293            if settings.get_option('gui/rtl_playback_controls')
294            else Gtk.TextDirection.LTR
295        )
296
297        self.play_image = Gtk.Image.new_from_icon_name(
298            'media-playback-start', Gtk.IconSize.SMALL_TOOLBAR
299        )
300        self.play_image.set_direction(controls_direction)
301        self.pause_image = Gtk.Image.new_from_icon_name(
302            'media-playback-pause', Gtk.IconSize.SMALL_TOOLBAR
303        )
304        self.pause_image.set_direction(controls_direction)
305
306        play_toolbar = self.builder.get_object('play_toolbar')
307        play_toolbar.set_direction(controls_direction)
308        for button in ('playpause', 'next', 'prev', 'stop'):
309            widget = self.builder.get_object('%s_button' % button)
310            setattr(self, '%s_button' % button, widget)
311            widget.get_child().set_direction(controls_direction)
312
313        self.progress_bar = playback.SeekProgressBar(player.PLAYER)
314        self.progress_bar.get_child().set_direction(controls_direction)
315        # Don't expand vertically; looks awful on Adwaita.
316        self.progress_bar.set_valign(Gtk.Align.CENTER)
317        guiutil.gtk_widget_replace(
318            self.builder.get_object('playback_progressbar_dummy'), self.progress_bar
319        )
320
321        self.stop_button.toggle_spat = False
322        self.stop_button.add_events(Gdk.EventMask.POINTER_MOTION_MASK)
323        self.stop_button.connect(
324            'motion-notify-event', self.on_stop_button_motion_notify_event
325        )
326        self.stop_button.connect(
327            'leave-notify-event', self.on_stop_button_leave_notify_event
328        )
329        self.stop_button.connect('key-press-event', self.on_stop_button_key_press_event)
330        self.stop_button.connect(
331            'key-release-event', self.on_stop_button_key_release_event
332        )
333        self.stop_button.connect('focus-out-event', self.on_stop_button_focus_out_event)
334        self.stop_button.connect('button-press-event', self.on_stop_button_press_event)
335        self.stop_button.connect(
336            'button-release-event', self.on_stop_button_release_event
337        )
338        self.stop_button.drag_dest_set(
339            Gtk.DestDefaults.ALL,
340            [Gtk.TargetEntry.new("exaile-index-list", Gtk.TargetFlags.SAME_APP, 0)],
341            Gdk.DragAction.COPY,
342        )
343        self.stop_button.connect('drag-motion', self.on_stop_button_drag_motion)
344        self.stop_button.connect('drag-leave', self.on_stop_button_drag_leave)
345        self.stop_button.connect(
346            'drag-data-received', self.on_stop_button_drag_data_received
347        )
348
349        self.statusbar = info.Statusbar(self.builder.get_object('status_bar'))
350        event.add_ui_callback(self.on_exaile_loaded, 'exaile_loaded')
351
352    def _connect_events(self):
353        """
354        Connects the various events to their handlers
355        """
356        self.builder.connect_signals(
357            {
358                'on_configure_event': self.configure_event,
359                'on_window_state_event': self.window_state_change_event,
360                'on_delete_event': self.on_delete_event,
361                'on_playpause_button_clicked': self._on_playpause_button,
362                'on_next_button_clicked': lambda *e: player.QUEUE.next(),
363                'on_prev_button_clicked': lambda *e: player.QUEUE.prev(),
364                'on_about_item_activate': self.on_about_item_activate,
365                # Controller
366                #            'on_scan_collection_item_activate': self.controller.on_rescan_collection,
367                #            'on_device_manager_item_activate': lambda *e: self.controller.show_devices(),
368                #            'on_track_properties_activate':self.controller.on_track_properties,
369            }
370        )
371
372        event.add_ui_callback(
373            self.on_playback_resume, 'playback_player_resume', player.PLAYER
374        )
375        event.add_ui_callback(
376            self.on_playback_end, 'playback_player_end', player.PLAYER
377        )
378        event.add_ui_callback(self.on_playback_end, 'playback_error', player.PLAYER)
379        event.add_ui_callback(
380            self.on_playback_start, 'playback_track_start', player.PLAYER
381        )
382        event.add_ui_callback(
383            self.on_toggle_pause, 'playback_toggle_pause', player.PLAYER
384        )
385        event.add_ui_callback(self.on_track_tags_changed, 'track_tags_changed')
386        event.add_ui_callback(self.on_buffering, 'playback_buffering', player.PLAYER)
387        event.add_ui_callback(self.on_playback_error, 'playback_error', player.PLAYER)
388
389        event.add_ui_callback(self.on_playlist_tracks_added, 'playlist_tracks_added')
390        event.add_ui_callback(
391            self.on_playlist_tracks_removed, 'playlist_tracks_removed'
392        )
393
394        # Settings
395        self._on_option_set('gui_option_set', settings, 'gui/show_info_area')
396        self._on_option_set('gui_option_set', settings, 'gui/show_info_area_covers')
397        event.add_ui_callback(self._on_option_set, 'option_set')
398
399    def _connect_panel_events(self):
400        """
401        Sets up panel events
402        """
403
404        # When there's nothing in the notebook, hide it
405        self.controller.panel_notebook.connect(
406            'page-added', self.on_panel_notebook_add_page
407        )
408        self.controller.panel_notebook.connect(
409            'page-removed', self.on_panel_notebook_remove_page
410        )
411
412        # panels
413        panels = self.controller.panel_notebook.panels
414
415        for panel_name in ('playlists', 'radio', 'files', 'collection'):
416            panel = panels[panel_name].panel
417            do_sort = False
418
419            if panel_name in ('files', 'collection'):
420                do_sort = True
421
422            panel.connect(
423                'append-items',
424                lambda panel, items, force_play: self.on_append_items(
425                    items, force_play, sort=do_sort
426                ),
427            )
428            panel.connect(
429                'queue-items',
430                lambda panel, items: self.on_append_items(
431                    items, queue=True, sort=do_sort
432                ),
433            )
434            panel.connect(
435                'replace-items',
436                lambda panel, items: self.on_append_items(
437                    items, replace=True, sort=do_sort
438                ),
439            )
440
441        ## Collection Panel
442        panel = panels['collection'].panel
443        panel.connect('collection-tree-loaded', self.on_collection_tree_loaded)
444
445        ## Playlist Panel
446        panel = panels['playlists'].panel
447        panel.connect(
448            'playlist-selected',
449            lambda panel, playlist: self.playlist_container.create_tab_from_playlist(
450                playlist
451            ),
452        )
453
454        ## Radio Panel
455        panel = panels['radio'].panel
456        panel.connect(
457            'playlist-selected',
458            lambda panel, playlist: self.playlist_container.create_tab_from_playlist(
459                playlist
460            ),
461        )
462
463        ## Files Panel
464        # panel = panels['files']
465
466    def _update_alpha(self):
467        if not settings.get_option('gui/use_alpha', False):
468            return
469        opac = 1.0 - float(settings.get_option('gui/transparency', 0.3))
470        Gtk.Widget.set_opacity(self.window, opac)
471
472    def _update_dark_hint(self):
473        gs = Gtk.Settings.get_default()
474
475        # We should use reset_property, but that's only available in > 3.20...
476        if not hasattr(self, '_default_dark_hint'):
477            self._default_dark_hint = gs.props.gtk_application_prefer_dark_theme
478
479        if settings.get_option('gui/gtk_dark_hint', False):
480            gs.props.gtk_application_prefer_dark_theme = True
481
482        elif gs.props.gtk_application_prefer_dark_theme != self._default_dark_hint:
483            # don't set it explicitly otherwise the app will revert to a light
484            # theme -- what we actually want is to leave it up to the OS
485            gs.props.gtk_application_prefer_dark_theme = self._default_dark_hint
486
487    def do_get_property(self, prop):
488        if prop.name == 'is-fullscreen':
489            return self._fullscreen
490        else:
491            return GObject.GObject.do_get_property(self, prop)
492
493    def do_set_property(self, prop, value):
494        if prop.name == 'is-fullscreen':
495            if value:
496                self.window.fullscreen()
497            else:
498                self.window.unfullscreen()
499        else:
500            GObject.GObject.do_set_property(self, prop, value)
501
502    def on_screen_changed(self, widget, event):
503        """
504        Updates the colormap on screen change
505        """
506        screen = widget.get_screen()
507        visual = screen.get_rgba_visual() or screen.get_rgb_visual()
508        self.window.set_visual(visual)
509
510    def on_panel_notebook_add_page(self, notebook, page, page_num):
511        if self.splitter.get_child1() is None:
512            self.splitter.pack1(self.controller.panel_notebook)
513            self.controller.panel_notebook.get_parent().child_set_property(
514                self.controller.panel_notebook, 'shrink', False
515            )
516
517    def on_panel_notebook_remove_page(self, notebook, page, page_num):
518        if notebook.get_n_pages() == 0:
519            self.splitter.remove(self.controller.panel_notebook)
520
521    def on_stop_button_motion_notify_event(self, widget, event):
522        """
523        Sets the hover state and shows SPAT icon
524        """
525        widget.__hovered = True
526        if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
527            widget.set_image(
528                Gtk.Image.new_from_icon_name('process-stop', Gtk.IconSize.BUTTON)
529            )
530        else:
531            widget.set_image(
532                Gtk.Image.new_from_icon_name('media-playback-stop', Gtk.IconSize.BUTTON)
533            )
534
535    def on_stop_button_leave_notify_event(self, widget, event):
536        """
537        Unsets the hover state and resets the button icon
538        """
539        widget.__hovered = False
540        if not widget.is_focus() and ~(event.get_state() & Gdk.ModifierType.SHIFT_MASK):
541            widget.set_image(
542                Gtk.Image.new_from_icon_name('media-playback-stop', Gtk.IconSize.BUTTON)
543            )
544
545    def on_stop_button_key_press_event(self, widget, event):
546        """
547        Shows SPAT icon on Shift key press
548        """
549        if event.keyval in (Gdk.KEY_Shift_L, Gdk.KEY_Shift_R):
550            widget.set_image(
551                Gtk.Image.new_from_icon_name('process-stop', Gtk.IconSize.BUTTON)
552            )
553            widget.toggle_spat = True
554
555        if event.keyval in (Gdk.KEY_space, Gdk.KEY_Return):
556            if widget.toggle_spat:
557                self.on_spat_clicked()
558            else:
559                player.PLAYER.stop()
560
561    def on_stop_button_key_release_event(self, widget, event):
562        """
563        Resets the button icon
564        """
565        if event.keyval in (Gdk.KEY_Shift_L, Gdk.KEY_Shift_R):
566            widget.set_image(
567                Gtk.Image.new_from_icon_name('media-playback-stop', Gtk.IconSize.BUTTON)
568            )
569            widget.toggle_spat = False
570
571    def on_stop_button_focus_out_event(self, widget, event):
572        """
573        Resets the button icon unless
574        the button is still hovered
575        """
576        if not getattr(widget, '__hovered', False):
577            widget.set_image(
578                Gtk.Image.new_from_icon_name('media-playback-stop', Gtk.IconSize.BUTTON)
579            )
580
581    def on_stop_button_press_event(self, widget, event):
582        """
583        Called when the user clicks on the stop button
584        """
585        if event.button == Gdk.BUTTON_PRIMARY:
586            if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
587                self.on_spat_clicked()
588        elif event.triggers_context_menu():
589            m = menu.Menu(self)
590            m.attach_to_widget(widget)
591            m.add_simple(
592                _("Toggle: Stop after Selected Track"),
593                self.on_spat_clicked,
594                'process-stop',
595            )
596            m.popup(event)
597
598    def on_stop_button_release_event(self, widget, event):
599        """
600        Called when the user releases the mouse from the stop button
601        """
602        rect = widget.get_allocation()
603        if 0 <= event.x < rect.width and 0 <= event.y < rect.height:
604            player.PLAYER.stop()
605
606    def on_stop_button_drag_motion(self, widget, context, x, y, time):
607        """
608        Indicates possible SPAT during drag motion of tracks
609        """
610        target = widget.drag_dest_find_target(context, None).name()
611        if target == 'exaile-index-list':
612            widget.set_image(
613                Gtk.Image.new_from_icon_name('process-stop', Gtk.IconSize.BUTTON)
614            )
615
616    def on_stop_button_drag_leave(self, widget, context, time):
617        """
618        Resets the stop button
619        """
620        widget.set_image(
621            Gtk.Image.new_from_icon_name('media-playback-stop', Gtk.IconSize.BUTTON)
622        )
623
624    def on_stop_button_drag_data_received(
625        self, widget, context, x, y, selection, info, time
626    ):
627        """
628        Allows for triggering the SPAT feature
629        by dropping tracks on the stop button
630        """
631        source_widget = Gtk.drag_get_source_widget(context)
632
633        if selection.target.name() == 'exaile-index-list' and isinstance(
634            source_widget, PlaylistView
635        ):
636            position = int(selection.data.split(',')[0])
637
638            if position == source_widget.playlist.spat_position:
639                position = -1
640
641            source_widget.playlist.spat_position = position
642            source_widget.queue_draw()
643
644    def on_spat_clicked(self, *e):
645        """
646        Called when the user clicks on the SPAT item
647        """
648        trs = self.get_selected_page().view.get_selected_items()
649        if not trs:
650            return
651
652        # TODO: this works, but implement this some other way in the future
653        if player.QUEUE.current_playlist.spat_position == -1:
654            player.QUEUE.current_playlist.spat_position = trs[0][0]
655        else:
656            player.QUEUE.current_playlist.spat_position = -1
657
658        self.get_selected_page().view.queue_draw()
659
660    def on_append_items(
661        self, tracks, force_play=False, queue=False, sort=False, replace=False
662    ):
663        """
664        Called when a panel (or other component)
665        has tracks to append and possibly queue
666
667        :param tracks: The tracks to append
668        :param force_play: Force playing the first track if there
669                            is no track currently playing. Otherwise
670                            check a setting to determine whether the
671                            track should be played
672        :param queue: Additionally queue tracks
673        :param sort: Sort before adding
674        :param replace: Clear playlist before adding
675        """
676        if len(tracks) == 0:
677            return
678
679        page = self.get_selected_page()
680
681        if sort:
682            tracks = trax.sort_tracks(common.BASE_SORT_TAGS, tracks)
683
684        if replace:
685            page.playlist.clear()
686
687        offset = len(page.playlist)
688        page.playlist.extend(tracks)
689
690        # extending the queue automatically starts playback
691        if queue:
692            if player.QUEUE is not page.playlist:
693                player.QUEUE.extend(tracks)
694
695        elif (
696            force_play
697            or settings.get_option('playlist/append_menu_starts_playback', False)
698        ) and not player.PLAYER.current:
699            page.view.play_track_at(offset, tracks[0])
700
701    def on_playback_error(self, type, player, message):
702        """
703        Called when there has been a playback error
704        """
705        self.message.show_error(_('Playback error encountered!'), message)
706
707    def on_buffering(self, type, player, percent):
708        """
709        Called when a stream is buffering
710        """
711        percent = min(percent, 100)
712        self.statusbar.set_status(_("Buffering: %d%%...") % percent, 1)
713
714    def on_track_tags_changed(self, type, track, tags):
715        """
716        Called when tags are changed
717        """
718        if track is player.PLAYER.current:
719            self._update_track_information()
720
721    def on_collection_tree_loaded(self, tree):
722        """
723        Updates information on collection tree load
724        """
725        self.statusbar.update_info()
726
727    def on_exaile_loaded(self, event_type, exaile, nothing):
728        """
729        Updates information on exaile load
730        """
731        self.statusbar.update_info()
732        event.remove_callback(self.on_exaile_loaded, 'exaile_loaded')
733
734    def on_playlist_tracks_added(self, type, playlist, tracks):
735        """
736        Updates information on track add
737        """
738        self.statusbar.update_info()
739
740    def on_playlist_tracks_removed(self, type, playlist, tracks):
741        """
742        Updates information on track removal
743        """
744        self.statusbar.update_info()
745
746    def on_toggle_pause(self, type, player, object):
747        """
748        Called when the user clicks the play button after playback has
749        already begun
750        """
751        if player.is_paused():
752            image = self.play_image
753            tooltip = _('Continue Playback')
754        else:
755            image = self.pause_image
756            tooltip = _('Pause Playback')
757
758        self.playpause_button.set_image(image)
759        self.playpause_button.set_tooltip_text(tooltip)
760        self._update_track_information()
761
762    def on_playlist_container_switch_page(self, notebook, page, page_num):
763        """
764        Updates info after notebook page switch
765        """
766        page = notebook.get_nth_page(page_num)
767        selection = page.view.get_selection()
768        selection.connect('changed', self.on_playlist_view_selection_changed)
769        self.statusbar.update_info()
770
771    def on_playlist_view_selection_changed(self, selection):
772        """
773        Updates info after playlist page selection change
774        """
775        self.statusbar.update_info()
776
777    def on_panel_filter_focus(self, *e):
778        """
779        Gives focus to the filter field of the current panel
780        """
781        try:
782            self.controller.get_active_panel().filter.grab_focus()
783        except (AttributeError, KeyError):
784            pass
785
786    def on_search_playlist_focus(self, *e):
787        """
788        Gives focus to the playlist search bar
789        """
790        plpage = get_selected_playlist()
791        if plpage:
792            plpage.get_search_entry().grab_focus()
793
794    def on_save_playlist(self, *e):
795        """
796        Called when the user presses Ctrl+S
797        """
798        page = self.get_selected_playlist()
799        if page:
800            page.on_save()
801
802    def on_save_playlist_as(self, *e):
803        """
804        Called when the user presses Ctrl+S
805        Spawns the save as dialog of the current playlist tab
806        """
807        page = self.get_selected_playlist()
808        if page:
809            page.on_saveas()
810
811    def on_clear_playlist(self, *e):
812        """
813        Clears the current playlist tab
814        """
815        page = self.get_selected_page()
816        if page:
817            page.playlist.clear()
818
819    def on_open_item_activate(self, menuitem):
820        """
821        Shows a dialog to open media
822        """
823
824        def on_uris_selected(dialog, uris):
825            uris.reverse()
826
827            if len(uris) > 0:
828                self.controller.open_uri(uris.pop(), play=True)
829
830            for uri in uris:
831                self.controller.open_uri(uri, play=False)
832
833        dialog = dialogs.MediaOpenDialog(self.window)
834        dialog.connect('uris-selected', on_uris_selected)
835        dialog.show()
836
837    def on_open_url_item_activate(self, menuitem):
838        """
839        Shows a dialog to open an URI
840        """
841
842        def on_uri_selected(dialog, uri):
843            self.controller.open_uri(uri, play=False)
844
845        dialog = dialogs.URIOpenDialog(self.window)
846        dialog.connect('uri-selected', on_uri_selected)
847        dialog.show()
848
849    def on_open_directories_item_activate(self, menuitem):
850        """
851        Shows a dialog to open directories
852        """
853
854        def on_uris_selected(dialog, uris):
855            uris.reverse()
856
857            if len(uris) > 0:
858                self.controller.open_uri(uris.pop(), play=True)
859
860            for uri in uris:
861                self.controller.open_uri(uri, play=False)
862
863        dialog = dialogs.DirectoryOpenDialog(self.window)
864        # Selecting empty folders is useless
865        dialog.props.create_folders = False
866        dialog.connect('uris-selected', on_uris_selected)
867        dialog.show()
868
869    def on_export_current_playlist_activate(self, menuitem):
870        """
871        Shows a dialog to export the current playlist
872        """
873        page = self.get_selected_page()
874
875        if not page or not isinstance(page, PlaylistPage):
876            return
877
878        def on_message(dialog, message_type, message):
879            """
880            Show messages in the main window message area
881            """
882            if message_type == Gtk.MessageType.INFO:
883                self.message.show_info(markup=message)
884            elif message_type == Gtk.MessageType.ERROR:
885                self.message.show_error(_('Playlist export failed!'), message)
886
887            return True
888
889        dialog = dialogs.PlaylistExportDialog(page.playlist, self.window)
890        dialog.connect('message', on_message)
891        dialog.show()
892
893    def on_playlist_utilities_bar_visible_toggled(self, checkmenuitem):
894        """
895        Shows or hides the playlist utilities bar
896        """
897        settings.set_option(
898            'gui/playlist_utilities_bar_visible', checkmenuitem.get_active()
899        )
900
901    def on_show_playing_track_item_activate(self, menuitem):
902        """
903        Tries to show the currently playing track
904        """
905        self.playlist_container.show_current_track()
906
907    def on_about_item_activate(self, menuitem):
908        """
909        Shows the about dialog
910        """
911        dialog = dialogs.AboutDialog(self.window)
912        dialog.show()
913
914    def on_playback_resume(self, type, player, data):
915        self.resuming = True
916
917    def on_playback_start(self, type, player, object):
918        """
919        Called when playback starts
920        Sets the currently playing track visible in the currently selected
921        playlist if the user has chosen this setting
922        """
923        if self.resuming:
924            self.resuming = False
925            return
926
927        self._update_track_information()
928        self.playpause_button.set_image(self.pause_image)
929        self.playpause_button.set_tooltip_text(_('Pause Playback'))
930
931    def on_playback_end(self, type, player, object):
932        """
933        Called when playback ends
934        """
935        self.window.set_title('Exaile')
936
937        self.playpause_button.set_image(self.play_image)
938        self.playpause_button.set_tooltip_text(_('Start Playback'))
939
940    def _on_option_set(self, name, object, option):
941        """
942        Handles changes of settings
943        """
944        if option == 'gui/main_window_title_format':
945            self.title_formatter.props.format = settings.get_option(
946                option, self.title_formatter.props.format
947            )
948
949        elif option == 'gui/use_tray':
950            usetray = settings.get_option(option, False)
951            if self.controller.tray_icon and not usetray:
952                self.controller.tray_icon.destroy()
953                self.controller.tray_icon = None
954            elif not self.controller.tray_icon and usetray:
955                self.controller.tray_icon = tray.TrayIcon(self)
956
957        elif option == 'gui/show_info_area':
958            self.info_area.set_no_show_all(False)
959            if settings.get_option(option, True):
960                self.info_area.show_all()
961            else:
962                self.info_area.hide()
963            self.info_area.set_no_show_all(True)
964
965        elif option == 'gui/show_info_area_covers':
966            cover = self.info_area.cover
967            cover.set_no_show_all(False)
968            if settings.get_option(option, True):
969                cover.show_all()
970            else:
971                cover.hide()
972            cover.set_no_show_all(True)
973
974        elif option == 'gui/transparency':
975            self._update_alpha()
976
977        elif option == 'gui/gtk_dark_hint':
978            self._update_dark_hint()
979
980    def _on_volume_key(self, is_up):
981        diff = int(
982            100 * settings.get_option('gui/volue_key_step_size', VOLUME_STEP_DEFAULT)
983        )
984        if not is_up:
985            diff = -diff
986
987        player.PLAYER.modify_volume(diff)
988        return True
989
990    def _on_seek_key(self, is_forward):
991        diff = settings.get_option('gui/seek_key_step_size', SEEK_STEP_DEFAULT)
992        if not is_forward:
993            diff = -diff
994
995        if player.PLAYER.current:
996            player.PLAYER.modify_time(diff)
997            self.progress_bar.update_progress()
998
999        return True
1000
1001    def _on_prev_tab_key(self, *e):
1002        self.playlist_container.get_current_notebook().select_prev_tab()
1003        return True
1004
1005    def _on_next_tab_key(self, *e):
1006        self.playlist_container.get_current_notebook().select_next_tab()
1007        return True
1008
1009    def _on_playpause_button(self, *e):
1010        self.playpause()
1011        return True
1012
1013    def _on_focus_playlist_tab(self, tab_nr):
1014        self.playlist_container.get_current_notebook().focus_tab(tab_nr)
1015        return True
1016
1017    def _on_focus_playlist_container(self, *_e):
1018        self.playlist_container.focus()
1019        return True
1020
1021    def _update_track_information(self):
1022        """
1023        Sets track information
1024        """
1025        track = player.PLAYER.current
1026
1027        if not track:
1028            return
1029
1030        self.window.set_title(self.title_formatter.format(track))
1031
1032    def playpause(self):
1033        """
1034        Pauses the playlist if it is playing, starts playing if it is
1035        paused. If stopped, try to start playing the next suitable track.
1036        """
1037        if player.PLAYER.is_paused() or player.PLAYER.is_playing():
1038            player.PLAYER.toggle_pause()
1039        else:
1040            pl = self.get_selected_page()
1041            player.QUEUE.set_current_playlist(pl.playlist)
1042            try:
1043                trackpath = pl.view.get_selected_paths()[0]
1044                pl.playlist.current_position = trackpath[0]
1045            except IndexError:
1046                pass
1047            player.QUEUE.play(track=pl.playlist.current)
1048
1049    def _setup_position(self):
1050        """
1051        Sets up the position and sized based on the size the window was
1052        when it was last moved or resized
1053        """
1054        if settings.get_option('gui/mainw_maximized', False):
1055            self.window.maximize()
1056
1057        width = settings.get_option('gui/mainw_width', 500)
1058        height = settings.get_option('gui/mainw_height', 475)
1059        x = settings.get_option('gui/mainw_x', 10)
1060        y = settings.get_option('gui/mainw_y', 10)
1061
1062        self.window.move(x, y)
1063        self.window.resize(width, height)
1064
1065        pos = settings.get_option('gui/mainw_sash_pos', 200)
1066        self.splitter.set_position(pos)
1067
1068    def on_delete_event(self, *e):
1069        """
1070        Called when the user attempts to close the window
1071        """
1072        sash_pos = self.splitter.get_position()
1073        if sash_pos > 10:
1074            settings.set_option('gui/mainw_sash_pos', sash_pos)
1075
1076        if settings.get_option('gui/use_tray', False) and settings.get_option(
1077            'gui/close_to_tray', False
1078        ):
1079            self.window.hide()
1080        else:
1081            self.quit()
1082        return True
1083
1084    def quit(self, *e):
1085        """
1086        Quits Exaile
1087        """
1088        self.window.hide()
1089        GLib.idle_add(self.controller.exaile.quit)
1090        return True
1091
1092    def on_restart_item_activate(self, menuitem):
1093        """
1094        Restarts Exaile
1095        """
1096        self.window.hide()
1097        GLib.idle_add(self.controller.exaile.quit, True)
1098
1099    def toggle_visible(self, bringtofront=False):
1100        """
1101        Toggles visibility of the main window
1102        """
1103        toggle_handled = self.emit('main-visible-toggle')
1104
1105        if not toggle_handled:
1106            if (
1107                bringtofront
1108                and self.window.is_active()
1109                or not bringtofront
1110                and self.window.get_property('visible')
1111            ):
1112                self.window.hide()
1113            else:
1114                # the ordering for deiconify/show matters -- if this gets
1115                # switched, then the minimization detection breaks
1116                self.window.deiconify()
1117                self.window.show()
1118
1119    def configure_event(self, *e):
1120        """
1121        Called when the window is resized or moved
1122        """
1123        # Don't save window size if it is maximized or fullscreen.
1124        if settings.get_option('gui/mainw_maximized', False) or self._fullscreen:
1125            return False
1126
1127        (width, height) = self.window.get_size()
1128        if [width, height] != [
1129            settings.get_option("gui/mainw_" + key, -1) for key in ["width", "height"]
1130        ]:
1131            settings.set_option('gui/mainw_height', height, save=False)
1132            settings.set_option('gui/mainw_width', width, save=False)
1133        (x, y) = self.window.get_position()
1134        if [x, y] != [
1135            settings.get_option("gui/mainw_" + key, -1) for key in ["x", "y"]
1136        ]:
1137            settings.set_option('gui/mainw_x', x, save=False)
1138            settings.set_option('gui/mainw_y', y, save=False)
1139
1140        return False
1141
1142    def window_state_change_event(self, window, event):
1143        """
1144        Saves the current maximized and fullscreen
1145        states and minimizes to tray if requested
1146        """
1147        if event.changed_mask & Gdk.WindowState.MAXIMIZED:
1148            settings.set_option(
1149                'gui/mainw_maximized',
1150                bool(event.new_window_state & Gdk.WindowState.MAXIMIZED),
1151            )
1152        if event.changed_mask & Gdk.WindowState.FULLSCREEN:
1153            self._fullscreen = bool(event.new_window_state & Gdk.WindowState.FULLSCREEN)
1154            self.notify('is-fullscreen')
1155
1156        # detect minimization state changes
1157        prev_minimized = self.minimized
1158
1159        if not self.minimized:
1160
1161            if (
1162                event.changed_mask & Gdk.WindowState.ICONIFIED
1163                and not event.changed_mask & Gdk.WindowState.WITHDRAWN
1164                and event.new_window_state & Gdk.WindowState.ICONIFIED
1165                and not event.new_window_state & Gdk.WindowState.WITHDRAWN
1166                and not self.window_state & Gdk.WindowState.ICONIFIED
1167            ):
1168                self.minimized = True
1169        else:
1170            if (
1171                event.changed_mask & Gdk.WindowState.WITHDRAWN
1172                and not event.new_window_state & (Gdk.WindowState.WITHDRAWN)
1173            ):  # and \
1174                self.minimized = False
1175
1176        # track this
1177        self.window_state = event.new_window_state
1178
1179        if settings.get_option('gui/minimize_to_tray', False):
1180
1181            # old code to detect minimization
1182            # -> it must have worked at some point, perhaps this is a GTK version
1183            # specific set of behaviors? Current code works now on 2.24.17
1184
1185            # if wm_state is not None:
1186            #    if '_NET_WM_STATE_HIDDEN' in wm_state[2]:
1187            #        show tray
1188            #        window.hide
1189            # else
1190            #    destroy tray
1191
1192            if self.minimized != prev_minimized and self.minimized is True:
1193                if (
1194                    not settings.get_option('gui/use_tray', False)
1195                    and self.controller.tray_icon is None
1196                ):
1197                    self.controller.tray_icon = tray.TrayIcon(self)
1198
1199                window.hide()
1200            elif (
1201                not settings.get_option('gui/use_tray', False)
1202                and self.controller.tray_icon is not None
1203            ):
1204                self.controller.tray_icon.destroy()
1205                self.controller.tray_icon = None
1206
1207        return False
1208
1209    def get_selected_page(self):
1210        """
1211        Returns the currently displayed playlist notebook page
1212        """
1213        return self.playlist_container.get_current_tab()
1214
1215    def get_selected_playlist(self):
1216        try:
1217            page = self.get_selected_page()
1218        except AttributeError:
1219            return None
1220        if not isinstance(page, PlaylistPage):
1221            return None
1222        return page
1223
1224
1225class MainWindowTrackInfoPane(info.TrackInfoPane, providers.ProviderHandler):
1226    """
1227    Extends the regular track info pane by an area for custom widgets
1228
1229    The mainwindow-info-area-widget provider is used to show widgets
1230    on the right of the info area. They should be small. The registered
1231    provider should provide a method 'create_widget' that takes the info
1232    area instance as a parameter, and that returns a Gtk.Widget to be
1233    inserted into the widget_area of the info area, and an attribute
1234    'name' that will be used when removing the provider.
1235    """
1236
1237    def __init__(self, player):
1238        info.TrackInfoPane.__init__(self, player)
1239
1240        self.__player = player
1241        self.widget_area = Gtk.Box()
1242
1243        self.get_child().pack_start(self.widget_area, False, False, 0)
1244
1245        self.__widget_area_widgets = {}
1246
1247        # call this last if we're using simple_init=True
1248        providers.ProviderHandler.__init__(
1249            self, 'mainwindow-info-area-widget', target=player, simple_init=True
1250        )
1251
1252    def get_player(self):
1253        """
1254        Retrieves the player object that this info area
1255        is associated with
1256        """
1257        return self._TrackInfoPane__player
1258
1259    def on_provider_added(self, provider):
1260        name = provider.name
1261        widget = provider.create_widget(self)
1262
1263        old_widget = self.__widget_area_widgets.get(name)
1264        if old_widget is not None:
1265            self.widget_area.remove(old_widget)
1266            old_widget.destroy()
1267
1268        self.__widget_area_widgets[name] = widget
1269        self.widget_area.pack_start(widget, False, False, 0)
1270        widget.show_all()
1271
1272    def on_provider_removed(self, provider):
1273        widget = self.__widget_area_widgets.pop(provider.name, None)
1274        if widget is not None:
1275            self.widget_area.remove(widget)
1276            widget.destroy()
1277
1278
1279def get_playlist_container():
1280    return MainWindow._mainwindow.playlist_container
1281
1282
1283def get_playlist_notebook():
1284    '''Retrieves the primary playlist notebook'''
1285    return MainWindow._mainwindow.playlist_container.notebooks[0]
1286
1287
1288def get_selected_page():
1289    return MainWindow._mainwindow.get_selected_page()
1290
1291
1292def get_selected_playlist():
1293    return MainWindow._mainwindow.get_selected_playlist()
1294
1295
1296def mainwindow():
1297    return MainWindow._mainwindow
1298
1299
1300# vim: et sts=4 sw=4
1301