Exemple #1
0
class MainWindow(GObject.GObject):
    """
        Main Exaile Window
    """

    __gproperties__ = {
        'is-fullscreen': (
            bool,
            'Fullscreen',
            'Whether the window is fullscreen.',
            False,  # Default
            GObject.PARAM_READWRITE,
        )
    }

    __gsignals__ = {
        'main-visible-toggle': (GObject.SignalFlags.RUN_LAST, bool, ())
    }

    _mainwindow = None

    def __init__(self, controller, builder, collection):
        """
            Initializes the main window

            @param controller: the main gui controller
        """
        GObject.GObject.__init__(self)

        self.controller = controller
        self.collection = collection
        self.playlist_manager = controller.exaile.playlists
        self.current_page = -1
        self._fullscreen = False
        self.resuming = False

        self.window_state = 0
        self.minimized = False

        self.builder = builder

        self.window = self.builder.get_object('ExaileWindow')
        self.window.set_title('Exaile')
        self.title_formatter = formatter.TrackFormatter(
            settings.get_option('gui/main_window_title_format',
                                _('$title (by $artist)') + ' - Exaile'))

        self.accel_group = Gtk.AccelGroup()
        self.window.add_accel_group(self.accel_group)
        self.accel_manager = AcceleratorManager('mainwindow-accelerators',
                                                self.accel_group)
        self.menubar = self.builder.get_object("mainmenu")

        fileitem = self.builder.get_object("file_menu_item")
        filemenu = menu.ProviderMenu('menubar-file-menu', self)
        fileitem.set_submenu(filemenu)

        edititem = self.builder.get_object("edit_menu_item")
        editmenu = menu.ProviderMenu('menubar-edit-menu', self)
        edititem.set_submenu(editmenu)

        viewitem = self.builder.get_object("view_menu_item")
        viewmenu = menu.ProviderMenu('menubar-view-menu', self)
        viewitem.set_submenu(viewmenu)

        toolsitem = self.builder.get_object("tools_menu_item")
        toolsmenu = menu.ProviderMenu('menubar-tools-menu', self)
        toolsitem.set_submenu(toolsmenu)

        helpitem = self.builder.get_object("help_menu_item")
        helpmenu = menu.ProviderMenu('menubar-help-menu', self)
        helpitem.set_submenu(helpmenu)

        self._setup_widgets()
        self._setup_position()
        self._setup_hotkeys()
        logger.info("Connecting main window events...")
        self._connect_events()
        MainWindow._mainwindow = self

        mainmenu._create_menus()

    def _setup_hotkeys(self):
        """
            Sets up accelerators that haven't been set up in UI designer
        """
        def factory(integer, description):
            """ Generate key bindings for Alt keys """
            keybinding = '<Alt>%s' % str(integer)
            callback = lambda *_e: self._on_focus_playlist_tab(integer - 1)
            return (keybinding, description, callback)

        hotkeys = (
            (
                '<Primary>S',
                _('Save currently selected playlist'),
                lambda *_e: self.on_save_playlist(),
            ),
            (
                '<Shift><Primary>S',
                _('Save currently selected playlist under a custom name'),
                lambda *_e: self.on_save_playlist_as(),
            ),
            (
                '<Primary>F',
                _('Focus filter in currently focused panel'),
                lambda *_e: self.on_panel_filter_focus(),
            ),
            (
                '<Primary>G',
                _('Focus playlist search'),
                lambda *_e: self.on_search_playlist_focus(),
            ),  # FIXME
            (
                '<Primary><Alt>l',
                _('Clear queue'),
                lambda *_e: player.QUEUE.clear(),
            ),  # FIXME
            (
                '<Primary>P',
                _('Start, pause or resume the playback'),
                self._on_playpause_button,
            ),
            (
                '<Primary>Right',
                _('Seek to the right'),
                lambda *_e: self._on_seek_key(True),
            ),
            (
                '<Primary>Left',
                _('Seek to the left'),
                lambda *_e: self._on_seek_key(False),
            ),
            (
                '<Primary>plus',
                _('Increase the volume'),
                lambda *_e: self._on_volume_key(True),
            ),
            (
                '<Primary>equal',
                _('Increase the volume'),
                lambda *_e: self._on_volume_key(True),
            ),
            (
                '<Primary>minus',
                _('Decrease the volume'),
                lambda *_e: self._on_volume_key(False),
            ),
            ('<Primary>Page_Up', _('Switch to previous tab'),
             self._on_prev_tab_key),
            ('<Primary>Page_Down', _('Switch to next tab'),
             self._on_next_tab_key),
            (
                '<Alt>N',
                _('Focus the playlist container'),
                self._on_focus_playlist_container,
            ),
            # These 4 are subject to change.. probably should do this
            # via a different mechanism too...
            (
                '<Alt>I',
                _('Focus the files panel'),
                lambda *_e: self.controller.focus_panel('files'),
            ),
            # ('<Alt>C', _('Focus the collection panel'),  # TODO: Does not work, why?
            # lambda *_e: self.controller.focus_panel('collection')),
            (
                '<Alt>R',
                _('Focus the radio panel'),
                lambda *_e: self.controller.focus_panel('radio'),
            ),
            (
                '<Alt>L',
                _('Focus the playlists panel'),
                lambda *_e: self.controller.focus_panel('playlists'),
            ),
            factory(1, _('Focus the first tab')),
            factory(2, _('Focus the second tab')),
            factory(3, _('Focus the third tab')),
            factory(4, _('Focus the fourth tab')),
            factory(5, _('Focus the fifth tab')),
            factory(6, _('Focus the sixth tab')),
            factory(7, _('Focus the seventh tab')),
            factory(8, _('Focus the eighth tab')),
            factory(9, _('Focus the ninth tab')),
            factory(0, _('Focus the tenth tab')),
        )

        for keys, helptext, function in hotkeys:
            accelerator = Accelerator(keys, helptext, function)
            providers.register('mainwindow-accelerators', accelerator)

    def _setup_widgets(self):
        """
            Sets up the various widgets
        """
        # TODO: Maybe make this stackable
        self.message = dialogs.MessageBar(
            parent=self.builder.get_object('player_box'),
            buttons=Gtk.ButtonsType.CLOSE)

        self.info_area = MainWindowTrackInfoPane(player.PLAYER)
        self.info_area.set_auto_update(True)
        self.info_area.set_border_width(3)
        self.info_area.hide()
        self.info_area.set_no_show_all(True)
        guiutil.gtk_widget_replace(self.builder.get_object('info_area'),
                                   self.info_area)

        self.volume_control = playback.VolumeControl(player.PLAYER)
        self.info_area.get_action_area().pack_end(self.volume_control, False,
                                                  False, 0)

        if settings.get_option('gui/use_alpha', False):
            screen = self.window.get_screen()
            visual = screen.get_rgba_visual()
            self.window.set_visual(visual)
            self.window.connect('screen-changed', self.on_screen_changed)
            self._update_alpha()

        self._update_dark_hint()

        playlist_area = self.builder.get_object('playlist_area')
        self.playlist_container = PlaylistContainer('saved_tabs',
                                                    player.PLAYER)
        for notebook in self.playlist_container.notebooks:
            notebook.connect_after('switch-page',
                                   self.on_playlist_container_switch_page)
            page = notebook.get_current_tab()
            if page is not None:
                selection = page.view.get_selection()
                selection.connect('changed',
                                  self.on_playlist_view_selection_changed)

        playlist_area.pack_start(self.playlist_container, True, True, 3)

        self.splitter = self.builder.get_object('splitter')

        # In most (all?) RTL locales, the playback controls should still be LTR.
        # Just in case that's not always the case, we provide a hidden option to
        # force RTL layout instead. This can be removed once we're more certain
        # that the default behavior (always LTR) is correct.
        controls_direction = (Gtk.TextDirection.RTL if
                              settings.get_option('gui/rtl_playback_controls')
                              else Gtk.TextDirection.LTR)

        self.play_image = Gtk.Image.new_from_icon_name(
            'media-playback-start', Gtk.IconSize.SMALL_TOOLBAR)
        self.play_image.set_direction(controls_direction)
        self.pause_image = Gtk.Image.new_from_icon_name(
            'media-playback-pause', Gtk.IconSize.SMALL_TOOLBAR)
        self.pause_image.set_direction(controls_direction)

        play_toolbar = self.builder.get_object('play_toolbar')
        play_toolbar.set_direction(controls_direction)
        for button in ('playpause', 'next', 'prev', 'stop'):
            widget = self.builder.get_object('%s_button' % button)
            setattr(self, '%s_button' % button, widget)
            widget.get_child().set_direction(controls_direction)

        self.progress_bar = playback.SeekProgressBar(player.PLAYER)
        self.progress_bar.get_child().set_direction(controls_direction)
        # Don't expand vertically; looks awful on Adwaita.
        self.progress_bar.set_valign(Gtk.Align.CENTER)
        guiutil.gtk_widget_replace(
            self.builder.get_object('playback_progressbar_dummy'),
            self.progress_bar)

        self.stop_button.toggle_spat = False
        self.stop_button.add_events(Gdk.EventMask.POINTER_MOTION_MASK)
        self.stop_button.connect('motion-notify-event',
                                 self.on_stop_button_motion_notify_event)
        self.stop_button.connect('leave-notify-event',
                                 self.on_stop_button_leave_notify_event)
        self.stop_button.connect('key-press-event',
                                 self.on_stop_button_key_press_event)
        self.stop_button.connect('key-release-event',
                                 self.on_stop_button_key_release_event)
        self.stop_button.connect('focus-out-event',
                                 self.on_stop_button_focus_out_event)
        self.stop_button.connect('button-press-event',
                                 self.on_stop_button_press_event)
        self.stop_button.connect('button-release-event',
                                 self.on_stop_button_release_event)
        self.stop_button.drag_dest_set(
            Gtk.DestDefaults.ALL,
            [
                Gtk.TargetEntry.new("exaile-index-list",
                                    Gtk.TargetFlags.SAME_APP, 0)
            ],
            Gdk.DragAction.COPY,
        )
        self.stop_button.connect('drag-motion',
                                 self.on_stop_button_drag_motion)
        self.stop_button.connect('drag-leave', self.on_stop_button_drag_leave)
        self.stop_button.connect('drag-data-received',
                                 self.on_stop_button_drag_data_received)

        self.statusbar = info.Statusbar(self.builder.get_object('status_bar'))
        event.add_ui_callback(self.on_exaile_loaded, 'exaile_loaded')

    def _connect_events(self):
        """
            Connects the various events to their handlers
        """
        self.builder.connect_signals({
            'on_configure_event':
            self.configure_event,
            'on_window_state_event':
            self.window_state_change_event,
            'on_delete_event':
            self.on_delete_event,
            'on_playpause_button_clicked':
            self._on_playpause_button,
            'on_next_button_clicked':
            lambda *e: player.QUEUE.next(),
            'on_prev_button_clicked':
            lambda *e: player.QUEUE.prev(),
            'on_about_item_activate':
            self.on_about_item_activate,
            # Controller
            #            'on_scan_collection_item_activate': self.controller.on_rescan_collection,
            #            'on_device_manager_item_activate': lambda *e: self.controller.show_devices(),
            #            'on_track_properties_activate':self.controller.on_track_properties,
        })

        event.add_ui_callback(self.on_playback_resume,
                              'playback_player_resume', player.PLAYER)
        event.add_ui_callback(self.on_playback_end, 'playback_player_end',
                              player.PLAYER)
        event.add_ui_callback(self.on_playback_end, 'playback_error',
                              player.PLAYER)
        event.add_ui_callback(self.on_playback_start, 'playback_track_start',
                              player.PLAYER)
        event.add_ui_callback(self.on_toggle_pause, 'playback_toggle_pause',
                              player.PLAYER)
        event.add_ui_callback(self.on_track_tags_changed, 'track_tags_changed')
        event.add_ui_callback(self.on_buffering, 'playback_buffering',
                              player.PLAYER)
        event.add_ui_callback(self.on_playback_error, 'playback_error',
                              player.PLAYER)

        event.add_ui_callback(self.on_playlist_tracks_added,
                              'playlist_tracks_added')
        event.add_ui_callback(self.on_playlist_tracks_removed,
                              'playlist_tracks_removed')

        # Settings
        self._on_option_set('gui_option_set', settings, 'gui/show_info_area')
        self._on_option_set('gui_option_set', settings,
                            'gui/show_info_area_covers')
        event.add_ui_callback(self._on_option_set, 'option_set')

    def _connect_panel_events(self):
        """
            Sets up panel events
        """

        # When there's nothing in the notebook, hide it
        self.controller.panel_notebook.connect('page-added',
                                               self.on_panel_notebook_add_page)
        self.controller.panel_notebook.connect(
            'page-removed', self.on_panel_notebook_remove_page)

        # panels
        panels = self.controller.panel_notebook.panels

        for panel_name in ('playlists', 'radio', 'files', 'collection'):
            panel = panels[panel_name].panel
            do_sort = False

            if panel_name in ('files', 'collection'):
                do_sort = True

            panel.connect(
                'append-items',
                lambda panel, items, force_play: self.on_append_items(
                    items, force_play, sort=do_sort),
            )
            panel.connect(
                'queue-items',
                lambda panel, items: self.on_append_items(
                    items, queue=True, sort=do_sort),
            )
            panel.connect(
                'replace-items',
                lambda panel, items: self.on_append_items(
                    items, replace=True, sort=do_sort),
            )

        ## Collection Panel
        panel = panels['collection'].panel
        panel.connect('collection-tree-loaded', self.on_collection_tree_loaded)

        ## Playlist Panel
        panel = panels['playlists'].panel
        panel.connect(
            'playlist-selected',
            lambda panel, playlist: self.playlist_container.
            create_tab_from_playlist(playlist),
        )

        ## Radio Panel
        panel = panels['radio'].panel
        panel.connect(
            'playlist-selected',
            lambda panel, playlist: self.playlist_container.
            create_tab_from_playlist(playlist),
        )

        ## Files Panel
        # panel = panels['files']

    def _update_alpha(self):
        if not settings.get_option('gui/use_alpha', False):
            return
        opac = 1.0 - float(settings.get_option('gui/transparency', 0.3))
        Gtk.Widget.set_opacity(self.window, opac)

    def _update_dark_hint(self):
        gs = Gtk.Settings.get_default()

        # We should use reset_property, but that's only available in > 3.20...
        if not hasattr(self, '_default_dark_hint'):
            self._default_dark_hint = gs.props.gtk_application_prefer_dark_theme

        if settings.get_option('gui/gtk_dark_hint', False):
            gs.props.gtk_application_prefer_dark_theme = True

        elif gs.props.gtk_application_prefer_dark_theme != self._default_dark_hint:
            # don't set it explicitly otherwise the app will revert to a light
            # theme -- what we actually want is to leave it up to the OS
            gs.props.gtk_application_prefer_dark_theme = self._default_dark_hint

    def do_get_property(self, prop):
        if prop.name == 'is-fullscreen':
            return self._fullscreen
        else:
            return GObject.GObject.do_get_property(self, prop)

    def do_set_property(self, prop, value):
        if prop.name == 'is-fullscreen':
            if value:
                self.window.fullscreen()
            else:
                self.window.unfullscreen()
        else:
            GObject.GObject.do_set_property(self, prop, value)

    def on_screen_changed(self, widget, event):
        """
            Updates the colormap on screen change
        """
        screen = widget.get_screen()
        visual = screen.get_rgba_visual() or screen.get_rgb_visual()
        self.window.set_visual(visual)

    def on_panel_notebook_add_page(self, notebook, page, page_num):
        if self.splitter.get_child1() is None:
            self.splitter.pack1(self.controller.panel_notebook)
            self.controller.panel_notebook.get_parent().child_set_property(
                self.controller.panel_notebook, 'shrink', False)

    def on_panel_notebook_remove_page(self, notebook, page, page_num):
        if notebook.get_n_pages() == 0:
            self.splitter.remove(self.controller.panel_notebook)

    def on_stop_button_motion_notify_event(self, widget, event):
        """
            Sets the hover state and shows SPAT icon
        """
        widget.__hovered = True
        if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
            widget.set_image(
                Gtk.Image.new_from_icon_name('process-stop',
                                             Gtk.IconSize.BUTTON))
        else:
            widget.set_image(
                Gtk.Image.new_from_icon_name('media-playback-stop',
                                             Gtk.IconSize.BUTTON))

    def on_stop_button_leave_notify_event(self, widget, event):
        """
            Unsets the hover state and resets the button icon
        """
        widget.__hovered = False
        if not widget.is_focus() and ~(event.get_state()
                                       & Gdk.ModifierType.SHIFT_MASK):
            widget.set_image(
                Gtk.Image.new_from_icon_name('media-playback-stop',
                                             Gtk.IconSize.BUTTON))

    def on_stop_button_key_press_event(self, widget, event):
        """
            Shows SPAT icon on Shift key press
        """
        if event.keyval in (Gdk.KEY_Shift_L, Gdk.KEY_Shift_R):
            widget.set_image(
                Gtk.Image.new_from_icon_name('process-stop',
                                             Gtk.IconSize.BUTTON))
            widget.toggle_spat = True

        if event.keyval in (Gdk.KEY_space, Gdk.KEY_Return):
            if widget.toggle_spat:
                self.on_spat_clicked()
            else:
                player.PLAYER.stop()

    def on_stop_button_key_release_event(self, widget, event):
        """
            Resets the button icon
        """
        if event.keyval in (Gdk.KEY_Shift_L, Gdk.KEY_Shift_R):
            widget.set_image(
                Gtk.Image.new_from_icon_name('media-playback-stop',
                                             Gtk.IconSize.BUTTON))
            widget.toggle_spat = False

    def on_stop_button_focus_out_event(self, widget, event):
        """
            Resets the button icon unless
            the button is still hovered
        """
        if not getattr(widget, '__hovered', False):
            widget.set_image(
                Gtk.Image.new_from_icon_name('media-playback-stop',
                                             Gtk.IconSize.BUTTON))

    def on_stop_button_press_event(self, widget, event):
        """
            Called when the user clicks on the stop button
        """
        if event.button == Gdk.BUTTON_PRIMARY:
            if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
                self.on_spat_clicked()
        elif event.triggers_context_menu():
            m = menu.Menu(self)
            m.attach_to_widget(widget)
            m.add_simple(
                _("Toggle: Stop after Selected Track"),
                self.on_spat_clicked,
                'process-stop',
            )
            m.popup(event)

    def on_stop_button_release_event(self, widget, event):
        """
            Called when the user releases the mouse from the stop button
        """
        rect = widget.get_allocation()
        if 0 <= event.x < rect.width and 0 <= event.y < rect.height:
            player.PLAYER.stop()

    def on_stop_button_drag_motion(self, widget, context, x, y, time):
        """
            Indicates possible SPAT during drag motion of tracks
        """
        target = widget.drag_dest_find_target(context, None).name()
        if target == 'exaile-index-list':
            widget.set_image(
                Gtk.Image.new_from_icon_name('process-stop',
                                             Gtk.IconSize.BUTTON))

    def on_stop_button_drag_leave(self, widget, context, time):
        """
            Resets the stop button
        """
        widget.set_image(
            Gtk.Image.new_from_icon_name('media-playback-stop',
                                         Gtk.IconSize.BUTTON))

    def on_stop_button_drag_data_received(self, widget, context, x, y,
                                          selection, info, time):
        """
            Allows for triggering the SPAT feature
            by dropping tracks on the stop button
        """
        source_widget = Gtk.drag_get_source_widget(context)

        if selection.target.name() == 'exaile-index-list' and isinstance(
                source_widget, PlaylistView):
            position = int(selection.data.split(',')[0])

            if position == source_widget.playlist.spat_position:
                position = -1

            source_widget.playlist.spat_position = position
            source_widget.queue_draw()

    def on_spat_clicked(self, *e):
        """
            Called when the user clicks on the SPAT item
        """
        trs = self.get_selected_page().view.get_selected_items()
        if not trs:
            return

        # TODO: this works, but implement this some other way in the future
        if player.QUEUE.current_playlist.spat_position == -1:
            player.QUEUE.current_playlist.spat_position = trs[0][0]
        else:
            player.QUEUE.current_playlist.spat_position = -1

        self.get_selected_page().view.queue_draw()

    def on_append_items(self,
                        tracks,
                        force_play=False,
                        queue=False,
                        sort=False,
                        replace=False):
        """
            Called when a panel (or other component)
            has tracks to append and possibly queue

            :param tracks: The tracks to append
            :param force_play: Force playing the first track if there
                                is no track currently playing. Otherwise
                                check a setting to determine whether the
                                track should be played
            :param queue: Additionally queue tracks
            :param sort: Sort before adding
            :param replace: Clear playlist before adding
        """
        if len(tracks) == 0:
            return

        page = self.get_selected_page()

        if sort:
            tracks = trax.sort_tracks(common.BASE_SORT_TAGS, tracks)

        if replace:
            page.playlist.clear()

        offset = len(page.playlist)
        page.playlist.extend(tracks)

        # extending the queue automatically starts playback
        if queue:
            if player.QUEUE is not page.playlist:
                player.QUEUE.extend(tracks)

        elif (force_play
              or settings.get_option('playlist/append_menu_starts_playback',
                                     False)) and not player.PLAYER.current:
            page.view.play_track_at(offset, tracks[0])

    def on_playback_error(self, type, player, message):
        """
            Called when there has been a playback error
        """
        self.message.show_error(_('Playback error encountered!'), message)

    def on_buffering(self, type, player, percent):
        """
            Called when a stream is buffering
        """
        percent = min(percent, 100)
        self.statusbar.set_status(_("Buffering: %d%%...") % percent, 1)

    def on_track_tags_changed(self, type, track, tags):
        """
            Called when tags are changed
        """
        if track is player.PLAYER.current:
            self._update_track_information()

    def on_collection_tree_loaded(self, tree):
        """
            Updates information on collection tree load
        """
        self.statusbar.update_info()

    def on_exaile_loaded(self, event_type, exaile, nothing):
        """
            Updates information on exaile load
        """
        self.statusbar.update_info()
        event.remove_callback(self.on_exaile_loaded, 'exaile_loaded')

    def on_playlist_tracks_added(self, type, playlist, tracks):
        """
            Updates information on track add
        """
        self.statusbar.update_info()

    def on_playlist_tracks_removed(self, type, playlist, tracks):
        """
            Updates information on track removal
        """
        self.statusbar.update_info()

    def on_toggle_pause(self, type, player, object):
        """
            Called when the user clicks the play button after playback has
            already begun
        """
        if player.is_paused():
            image = self.play_image
            tooltip = _('Continue Playback')
        else:
            image = self.pause_image
            tooltip = _('Pause Playback')

        self.playpause_button.set_image(image)
        self.playpause_button.set_tooltip_text(tooltip)
        self._update_track_information()

    def on_playlist_container_switch_page(self, notebook, page, page_num):
        """
            Updates info after notebook page switch
        """
        page = notebook.get_nth_page(page_num)
        selection = page.view.get_selection()
        selection.connect('changed', self.on_playlist_view_selection_changed)
        self.statusbar.update_info()

    def on_playlist_view_selection_changed(self, selection):
        """
            Updates info after playlist page selection change
        """
        self.statusbar.update_info()

    def on_panel_filter_focus(self, *e):
        """
            Gives focus to the filter field of the current panel
        """
        try:
            self.controller.get_active_panel().filter.grab_focus()
        except (AttributeError, KeyError):
            pass

    def on_search_playlist_focus(self, *e):
        """
            Gives focus to the playlist search bar
        """
        plpage = get_selected_playlist()
        if plpage:
            plpage.get_search_entry().grab_focus()

    def on_save_playlist(self, *e):
        """
            Called when the user presses Ctrl+S
        """
        page = self.get_selected_playlist()
        if page:
            page.on_save()

    def on_save_playlist_as(self, *e):
        """
            Called when the user presses Ctrl+S
            Spawns the save as dialog of the current playlist tab
        """
        page = self.get_selected_playlist()
        if page:
            page.on_saveas()

    def on_clear_playlist(self, *e):
        """
            Clears the current playlist tab
        """
        page = self.get_selected_page()
        if page:
            page.playlist.clear()

    def on_open_item_activate(self, menuitem):
        """
            Shows a dialog to open media
        """
        def on_uris_selected(dialog, uris):
            uris.reverse()

            if len(uris) > 0:
                self.controller.open_uri(uris.pop(), play=True)

            for uri in uris:
                self.controller.open_uri(uri, play=False)

        dialog = dialogs.MediaOpenDialog(self.window)
        dialog.connect('uris-selected', on_uris_selected)
        dialog.show()

    def on_open_url_item_activate(self, menuitem):
        """
            Shows a dialog to open an URI
        """
        def on_uri_selected(dialog, uri):
            self.controller.open_uri(uri, play=False)

        dialog = dialogs.URIOpenDialog(self.window)
        dialog.connect('uri-selected', on_uri_selected)
        dialog.show()

    def on_open_directories_item_activate(self, menuitem):
        """
            Shows a dialog to open directories
        """
        def on_uris_selected(dialog, uris):
            uris.reverse()

            if len(uris) > 0:
                self.controller.open_uri(uris.pop(), play=True)

            for uri in uris:
                self.controller.open_uri(uri, play=False)

        dialog = dialogs.DirectoryOpenDialog(self.window)
        # Selecting empty folders is useless
        dialog.props.create_folders = False
        dialog.connect('uris-selected', on_uris_selected)
        dialog.show()

    def on_export_current_playlist_activate(self, menuitem):
        """
            Shows a dialog to export the current playlist
        """
        page = self.get_selected_page()

        if not page or not isinstance(page, PlaylistPage):
            return

        def on_message(dialog, message_type, message):
            """
                Show messages in the main window message area
            """
            if message_type == Gtk.MessageType.INFO:
                self.message.show_info(markup=message)
            elif message_type == Gtk.MessageType.ERROR:
                self.message.show_error(_('Playlist export failed!'), message)

            return True

        dialog = dialogs.PlaylistExportDialog(page.playlist, self.window)
        dialog.connect('message', on_message)
        dialog.show()

    def on_playlist_utilities_bar_visible_toggled(self, checkmenuitem):
        """
            Shows or hides the playlist utilities bar
        """
        settings.set_option('gui/playlist_utilities_bar_visible',
                            checkmenuitem.get_active())

    def on_show_playing_track_item_activate(self, menuitem):
        """
            Tries to show the currently playing track
        """
        self.playlist_container.show_current_track()

    def on_about_item_activate(self, menuitem):
        """
            Shows the about dialog
        """
        dialog = dialogs.AboutDialog(self.window)
        dialog.show()

    def on_playback_resume(self, type, player, data):
        self.resuming = True

    def on_playback_start(self, type, player, object):
        """
            Called when playback starts
            Sets the currently playing track visible in the currently selected
            playlist if the user has chosen this setting
        """
        if self.resuming:
            self.resuming = False
            return

        self._update_track_information()
        self.playpause_button.set_image(self.pause_image)
        self.playpause_button.set_tooltip_text(_('Pause Playback'))

    def on_playback_end(self, type, player, object):
        """
            Called when playback ends
        """
        self.window.set_title('Exaile')

        self.playpause_button.set_image(self.play_image)
        self.playpause_button.set_tooltip_text(_('Start Playback'))

    def _on_option_set(self, name, object, option):
        """
           Handles changes of settings
        """
        if option == 'gui/main_window_title_format':
            self.title_formatter.props.format = settings.get_option(
                option, self.title_formatter.props.format)

        elif option == 'gui/use_tray':
            usetray = settings.get_option(option, False)
            if self.controller.tray_icon and not usetray:
                self.controller.tray_icon.destroy()
                self.controller.tray_icon = None
            elif not self.controller.tray_icon and usetray:
                self.controller.tray_icon = tray.TrayIcon(self)

        elif option == 'gui/show_info_area':
            self.info_area.set_no_show_all(False)
            if settings.get_option(option, True):
                self.info_area.show_all()
            else:
                self.info_area.hide()
            self.info_area.set_no_show_all(True)

        elif option == 'gui/show_info_area_covers':
            cover = self.info_area.cover
            cover.set_no_show_all(False)
            if settings.get_option(option, True):
                cover.show_all()
            else:
                cover.hide()
            cover.set_no_show_all(True)

        elif option == 'gui/transparency':
            self._update_alpha()

        elif option == 'gui/gtk_dark_hint':
            self._update_dark_hint()

    def _on_volume_key(self, is_up):
        diff = int(100 * settings.get_option('gui/volue_key_step_size',
                                             VOLUME_STEP_DEFAULT))
        if not is_up:
            diff = -diff

        player.PLAYER.modify_volume(diff)
        return True

    def _on_seek_key(self, is_forward):
        diff = settings.get_option('gui/seek_key_step_size', SEEK_STEP_DEFAULT)
        if not is_forward:
            diff = -diff

        if player.PLAYER.current:
            player.PLAYER.modify_time(diff)
            self.progress_bar.update_progress()

        return True

    def _on_prev_tab_key(self, *e):
        self.playlist_container.get_current_notebook().select_prev_tab()
        return True

    def _on_next_tab_key(self, *e):
        self.playlist_container.get_current_notebook().select_next_tab()
        return True

    def _on_playpause_button(self, *e):
        self.playpause()
        return True

    def _on_focus_playlist_tab(self, tab_nr):
        self.playlist_container.get_current_notebook().focus_tab(tab_nr)
        return True

    def _on_focus_playlist_container(self, *_e):
        self.playlist_container.focus()
        return True

    def _update_track_information(self):
        """
            Sets track information
        """
        track = player.PLAYER.current

        if not track:
            return

        self.window.set_title(self.title_formatter.format(track))

    def playpause(self):
        """
            Pauses the playlist if it is playing, starts playing if it is
            paused. If stopped, try to start playing the next suitable track.
        """
        if player.PLAYER.is_paused() or player.PLAYER.is_playing():
            player.PLAYER.toggle_pause()
        else:
            pl = self.get_selected_page()
            player.QUEUE.set_current_playlist(pl.playlist)
            try:
                trackpath = pl.view.get_selected_paths()[0]
                pl.playlist.current_position = trackpath[0]
            except IndexError:
                pass
            player.QUEUE.play(track=pl.playlist.current)

    def _setup_position(self):
        """
            Sets up the position and sized based on the size the window was
            when it was last moved or resized
        """
        if settings.get_option('gui/mainw_maximized', False):
            self.window.maximize()

        width = settings.get_option('gui/mainw_width', 500)
        height = settings.get_option('gui/mainw_height', 475)
        x = settings.get_option('gui/mainw_x', 10)
        y = settings.get_option('gui/mainw_y', 10)

        self.window.move(x, y)
        self.window.resize(width, height)

        pos = settings.get_option('gui/mainw_sash_pos', 200)
        self.splitter.set_position(pos)

    def on_delete_event(self, *e):
        """
            Called when the user attempts to close the window
        """
        sash_pos = self.splitter.get_position()
        if sash_pos > 10:
            settings.set_option('gui/mainw_sash_pos', sash_pos)

        if settings.get_option('gui/use_tray', False) and settings.get_option(
                'gui/close_to_tray', False):
            self.window.hide()
        else:
            self.quit()
        return True

    def quit(self, *e):
        """
            Quits Exaile
        """
        self.window.hide()
        GLib.idle_add(self.controller.exaile.quit)
        return True

    def on_restart_item_activate(self, menuitem):
        """
            Restarts Exaile
        """
        self.window.hide()
        GLib.idle_add(self.controller.exaile.quit, True)

    def toggle_visible(self, bringtofront=False):
        """
            Toggles visibility of the main window
        """
        toggle_handled = self.emit('main-visible-toggle')

        if not toggle_handled:
            if (bringtofront and self.window.is_active() or not bringtofront
                    and self.window.get_property('visible')):
                self.window.hide()
            else:
                # the ordering for deiconify/show matters -- if this gets
                # switched, then the minimization detection breaks
                self.window.deiconify()
                self.window.show()

    def configure_event(self, *e):
        """
            Called when the window is resized or moved
        """
        # Don't save window size if it is maximized or fullscreen.
        if settings.get_option('gui/mainw_maximized',
                               False) or self._fullscreen:
            return False

        (width, height) = self.window.get_size()
        if [width, height] != [
                settings.get_option("gui/mainw_" + key, -1)
                for key in ["width", "height"]
        ]:
            settings.set_option('gui/mainw_height', height, save=False)
            settings.set_option('gui/mainw_width', width, save=False)
        (x, y) = self.window.get_position()
        if [x, y] != [
                settings.get_option("gui/mainw_" + key, -1)
                for key in ["x", "y"]
        ]:
            settings.set_option('gui/mainw_x', x, save=False)
            settings.set_option('gui/mainw_y', y, save=False)

        return False

    def window_state_change_event(self, window, event):
        """
            Saves the current maximized and fullscreen
            states and minimizes to tray if requested
        """
        if event.changed_mask & Gdk.WindowState.MAXIMIZED:
            settings.set_option(
                'gui/mainw_maximized',
                bool(event.new_window_state & Gdk.WindowState.MAXIMIZED),
            )
        if event.changed_mask & Gdk.WindowState.FULLSCREEN:
            self._fullscreen = bool(event.new_window_state
                                    & Gdk.WindowState.FULLSCREEN)
            self.notify('is-fullscreen')

        # detect minimization state changes
        prev_minimized = self.minimized

        if not self.minimized:

            if (event.changed_mask & Gdk.WindowState.ICONIFIED
                    and not event.changed_mask & Gdk.WindowState.WITHDRAWN
                    and event.new_window_state & Gdk.WindowState.ICONIFIED
                    and not event.new_window_state & Gdk.WindowState.WITHDRAWN
                    and not self.window_state & Gdk.WindowState.ICONIFIED):
                self.minimized = True
        else:
            if (event.changed_mask & Gdk.WindowState.WITHDRAWN
                    and not event.new_window_state &
                (Gdk.WindowState.WITHDRAWN)):  # and \
                self.minimized = False

        # track this
        self.window_state = event.new_window_state

        if settings.get_option('gui/minimize_to_tray', False):

            # old code to detect minimization
            # -> it must have worked at some point, perhaps this is a GTK version
            # specific set of behaviors? Current code works now on 2.24.17

            # if wm_state is not None:
            #    if '_NET_WM_STATE_HIDDEN' in wm_state[2]:
            #        show tray
            #        window.hide
            # else
            #    destroy tray

            if self.minimized != prev_minimized and self.minimized is True:
                if (not settings.get_option('gui/use_tray', False)
                        and self.controller.tray_icon is None):
                    self.controller.tray_icon = tray.TrayIcon(self)

                window.hide()
            elif (not settings.get_option('gui/use_tray', False)
                  and self.controller.tray_icon is not None):
                self.controller.tray_icon.destroy()
                self.controller.tray_icon = None

        return False

    def get_selected_page(self):
        """
            Returns the currently displayed playlist notebook page
        """
        return self.playlist_container.get_current_tab()

    def get_selected_playlist(self):
        try:
            page = self.get_selected_page()
        except AttributeError:
            return None
        if not isinstance(page, PlaylistPage):
            return None
        return page
Exemple #2
0
class MainWindow(gobject.GObject):
    """
        Main Exaile Window
    """
    __gsignals__ = {'main-visible-toggle': (gobject.SIGNAL_RUN_LAST, bool, ())}
    _mainwindow = None

    def __init__(self, controller, builder, collection):
        """
            Initializes the main window

            @param controller: the main gui controller
        """
        gobject.GObject.__init__(self)

        self.controller = controller
        self.collection = collection
        self.playlist_manager = controller.exaile.playlists
        self.current_page = -1
        self._fullscreen = False
        self.resuming = False

        self.window_state = 0
        self.minimized = False

        self.builder = builder

        self.window = self.builder.get_object('ExaileWindow')
        self.window.set_title('Exaile')
        self.title_formatter = formatter.TrackFormatter(
            settings.get_option('gui/main_window_title_format',
                                _('$title (by $artist)') + ' - Exaile'))

        self.accelgroup = gtk.AccelGroup()
        self.window.add_accel_group(self.accelgroup)
        self.accel_manager = AcceleratorManager('mainwindow-accelerators',
                                                self.accelgroup)
        self.menubar = self.builder.get_object("mainmenu")

        fileitem = self.builder.get_object("file_menu_item")
        filemenu = menu.ProviderMenu('menubar-file-menu', self)
        fileitem.set_submenu(filemenu)

        edititem = self.builder.get_object("edit_menu_item")
        editmenu = menu.ProviderMenu('menubar-edit-menu', self)
        edititem.set_submenu(editmenu)

        viewitem = self.builder.get_object("view_menu_item")
        viewmenu = menu.ProviderMenu('menubar-view-menu', self)
        viewitem.set_submenu(viewmenu)

        toolsitem = self.builder.get_object("tools_menu_item")
        toolsmenu = menu.ProviderMenu('menubar-tools-menu', self)
        toolsitem.set_submenu(toolsmenu)

        helpitem = self.builder.get_object("help_menu_item")
        helpmenu = menu.ProviderMenu('menubar-help-menu', self)
        helpitem.set_submenu(helpmenu)

        self._setup_widgets()
        self._setup_position()
        self._setup_hotkeys()
        logger.info("Connecting main window events...")
        self._connect_events()
        MainWindow._mainwindow = self

        mainmenu._create_menus()

    def _setup_hotkeys(self):
        """
            Sets up accelerators that haven't been set up in UI designer
        """
        hotkeys = (
            ('<Control>S', lambda *e: self.on_save_playlist()),
            ('<Shift><Control>S', lambda *e: self.on_save_playlist_as()),
            ('<Control>F', lambda *e: self.on_panel_filter_focus()),
            ('<Control>G',
             lambda *e: self.on_search_playlist_focus()),  # FIXME
            ('<Control><Alt>l', lambda *e: player.QUEUE.clear()),  # FIXME
            ('<Control>P', self._on_playpause_button),
            ('<Control>Right', lambda *e: self._on_seek_key(True)),
            ('<Control>Left', lambda *e: self._on_seek_key(False)),
            ('<Control>plus', lambda *e: self._on_volume_key(True)),
            ('<Control>minus', lambda *e: self._on_volume_key(False)),
            ('<Control>Page_Up', self._on_prev_tab_key),
            ('<Control>Page_Down', self._on_next_tab_key),
            ('<Alt>N', self._on_focus_playlist_container),

            # These 4 are subject to change.. probably should do this
            # via a different mechanism too...
            ('<Alt>I', lambda *e: self.controller.focus_panel('files')),
            #('<Alt>C', lambda *e: self.controller.focus_panel('collection')),
            ('<Alt>R', lambda *e: self.controller.focus_panel('radio')),
            ('<Alt>L', lambda *e: self.controller.focus_panel('playlists')),
            ('<Alt>1', lambda *e: self._on_focus_playlist_tab(0)),
            ('<Alt>2', lambda *e: self._on_focus_playlist_tab(1)),
            ('<Alt>3', lambda *e: self._on_focus_playlist_tab(2)),
            ('<Alt>4', lambda *e: self._on_focus_playlist_tab(3)),
            ('<Alt>5', lambda *e: self._on_focus_playlist_tab(4)),
            ('<Alt>6', lambda *e: self._on_focus_playlist_tab(5)),
            ('<Alt>7', lambda *e: self._on_focus_playlist_tab(6)),
            ('<Alt>8', lambda *e: self._on_focus_playlist_tab(7)),
            ('<Alt>9', lambda *e: self._on_focus_playlist_tab(8)),
            ('<Alt>0', lambda *e: self._on_focus_playlist_tab(9)),
        )

        self.accel_group = gtk.AccelGroup()
        for key, function in hotkeys:
            key, mod = gtk.accelerator_parse(key)
            self.accel_group.connect_group(key, mod, gtk.ACCEL_VISIBLE,
                                           function)
        self.window.add_accel_group(self.accel_group)

    def _setup_widgets(self):
        """
            Sets up the various widgets
        """
        # TODO: Maybe make this stackable
        self.message = dialogs.MessageBar(
            parent=self.builder.get_object('player_box'),
            buttons=gtk.BUTTONS_CLOSE)
        self.message.connect('response', self.on_messagebar_response)

        self.info_area = MainWindowTrackInfoPane(player.PLAYER)
        self.info_area.set_auto_update(True)
        self.info_area.set_padding(3, 3, 3, 3)
        self.info_area.hide_all()
        self.info_area.set_no_show_all(True)
        guiutil.gtk_widget_replace(self.builder.get_object('info_area'),
                                   self.info_area)

        self.volume_control = playback.VolumeControl(player.PLAYER)
        self.info_area.get_action_area().pack_start(self.volume_control)

        if settings.get_option('gui/use_alpha', False):
            screen = self.window.get_screen()
            colormap = screen.get_rgba_colormap()

            if colormap is not None:
                self.window.set_app_paintable(True)
                self.window.set_colormap(colormap)

                self.window.connect('expose-event', self.on_expose_event)
                self.window.connect('screen-changed', self.on_screen_changed)

        playlist_area = self.builder.get_object('playlist_area')
        self.playlist_container = PlaylistContainer('saved_tabs',
                                                    player.PLAYER)
        for notebook in self.playlist_container.notebooks:
            notebook.connect_after('switch-page',
                                   self.on_playlist_container_switch_page)
            page = notebook.get_current_tab()
            if page is not None:
                selection = page.view.get_selection()
                selection.connect('changed',
                                  self.on_playlist_view_selection_changed)

        playlist_area.pack_start(self.playlist_container, padding=3)

        self.splitter = self.builder.get_object('splitter')

        self.progress_bar = playback.SeekProgressBar(player.PLAYER)
        guiutil.gtk_widget_replace(
            self.builder.get_object('playback_progressbar'), self.progress_bar)

        for button in ('playpause', 'next', 'prev', 'stop'):
            setattr(self, '%s_button' % button,
                    self.builder.get_object('%s_button' % button))

        self.stop_button.add_events(gtk.gdk.POINTER_MOTION_MASK)
        self.stop_button.connect('motion-notify-event',
                                 self.on_stop_button_motion_notify_event)
        self.stop_button.connect('leave-notify-event',
                                 self.on_stop_button_leave_notify_event)
        self.stop_button.connect('key-press-event',
                                 self.on_stop_button_key_press_event)
        self.stop_button.connect('key-release-event',
                                 self.on_stop_button_key_release_event)
        self.stop_button.connect('focus-out-event',
                                 self.on_stop_button_focus_out_event)
        self.stop_button.connect('button-press-event',
                                 self.on_stop_button_press_event)
        self.stop_button.connect('button-release-event',
                                 self.on_stop_button_release_event)
        self.stop_button.drag_dest_set(
            gtk.DEST_DEFAULT_ALL,
            [("exaile-index-list", gtk.TARGET_SAME_APP, 0)],
            gtk.gdk.ACTION_COPY)
        self.stop_button.connect('drag-motion',
                                 self.on_stop_button_drag_motion)
        self.stop_button.connect('drag-leave', self.on_stop_button_drag_leave)
        self.stop_button.connect('drag-data-received',
                                 self.on_stop_button_drag_data_received)

        self.statusbar = info.Statusbar(self.builder.get_object('status_bar'))
        event.add_callback(self.on_exaile_loaded, 'exaile_loaded')

    def _connect_events(self):
        """
            Connects the various events to their handlers
        """
        self.builder.connect_signals({
            'on_configure_event':
            self.configure_event,
            'on_window_state_event':
            self.window_state_change_event,
            'on_delete_event':
            self.on_delete_event,
            'on_playpause_button_clicked':
            self._on_playpause_button,
            'on_next_button_clicked':
            lambda *e: player.QUEUE.next(),
            'on_prev_button_clicked':
            lambda *e: player.QUEUE.prev(),
            'on_about_item_activate':
            self.on_about_item_activate,
            # Controller
            #            'on_scan_collection_item_activate': self.controller.on_rescan_collection,
            #            'on_device_manager_item_activate': lambda *e: self.controller.show_devices(),
            #            'on_track_properties_activate':self.controller.on_track_properties,
        })

        event.add_callback(self.on_playback_resume, 'playback_player_resume',
                           player.PLAYER)
        event.add_callback(self.on_playback_end, 'playback_player_end',
                           player.PLAYER)
        event.add_callback(self.on_playback_end, 'playback_error',
                           player.PLAYER)
        event.add_callback(self.on_playback_start, 'playback_track_start',
                           player.PLAYER)
        event.add_callback(self.on_toggle_pause, 'playback_toggle_pause',
                           player.PLAYER)
        event.add_callback(self.on_tags_parsed, 'tags_parsed', player.PLAYER)
        event.add_callback(self.on_track_tags_changed, 'track_tags_changed')
        event.add_callback(self.on_buffering, 'playback_buffering',
                           player.PLAYER)
        event.add_callback(self.on_playback_error, 'playback_error',
                           player.PLAYER)

        event.add_callback(self.on_playlist_tracks_added,
                           'playlist_tracks_added')
        event.add_callback(self.on_playlist_tracks_removed,
                           'playlist_tracks_removed')

        # Settings
        self._on_option_set('gui_option_set', settings, 'gui/show_info_area')
        self._on_option_set('gui_option_set', settings,
                            'gui/show_info_area_covers')
        event.add_callback(self._on_option_set, 'option_set')

    def _connect_panel_events(self):
        """
            Sets up panel events
        """

        self.panel_box = self.builder.get_object('panel_box')

        # When there's nothing in the notebook, hide it
        self.controller.panel_notebook.connect('page-added',
                                               self.on_panel_notebook_add_page)
        self.controller.panel_notebook.connect(
            'page-removed', self.on_panel_notebook_remove_page)

        # panels
        panels = self.controller.panel_notebook.panels

        for panel_name in ('playlists', 'radio', 'files', 'collection'):
            panel = panels[panel_name].panel
            sort = False

            if panel_name in ('files', 'collection'):
                sort = True

            panel.connect('append-items',
                          lambda panel, items, force_play, sort=sort: self.
                          on_append_items(items, force_play, sort=sort))
            panel.connect('queue-items',
                          lambda panel, items, sort=sort: self.on_append_items(
                              items, queue=True, sort=sort))
            panel.connect('replace-items',
                          lambda panel, items, sort=sort: self.on_append_items(
                              items, replace=True, sort=sort))

        ## Collection Panel
        panel = panels['collection'].panel
        panel.connect('collection-tree-loaded', self.on_collection_tree_loaded)

        ## Playlist Panel
        panel = panels['playlists'].panel
        panel.connect(
            'playlist-selected', lambda panel, playlist: self.
            playlist_container.create_tab_from_playlist(playlist))

        ## Radio Panel
        panel = panels['radio'].panel
        panel.connect(
            'playlist-selected', lambda panel, playlist: self.
            playlist_container.create_tab_from_playlist(playlist))

        ## Files Panel
        #panel = panels['files']

    def on_expose_event(self, widget, event):
        """
            Paints the window alpha transparency
        """
        opacity = 1 - settings.get_option('gui/transparency', 0.3)
        context = widget.props.window.cairo_create()
        background = widget.style.bg[gtk.STATE_NORMAL]
        context.set_source_rgba(
            float(background.red) / 256**2,
            float(background.green) / 256**2,
            float(background.blue) / 256**2, opacity)
        context.set_operator(cairo.OPERATOR_SOURCE)
        context.paint()

    def on_screen_changed(self, widget, event):
        """
            Updates the colormap on screen change
        """
        screen = widget.get_screen()
        colormap = screen.get_rgba_colormap() or screen.get_rgb_colormap()
        self.window.set_colormap(colormap)

    def on_messagebar_response(self, widget, response):
        """
            Hides the messagebar if requested
        """
        if response == gtk.RESPONSE_CLOSE:
            widget.hide()

    def on_panel_notebook_add_page(self, notebook, page, page_num):
        if self.splitter.get_child1() is None:
            self.splitter.pack1(self.panel_box)

    def on_panel_notebook_remove_page(self, notebook, page, page_num):
        if notebook.get_n_pages() == 0:
            self.splitter.remove(self.panel_box)

    def on_stop_button_motion_notify_event(self, widget, event):
        """
            Sets the hover state and shows SPAT icon
        """
        widget.set_data('hovered', True)
        if event.state & gtk.gdk.SHIFT_MASK:
            widget.set_image(
                gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
        else:
            widget.set_image(
                gtk.image_new_from_stock(gtk.STOCK_MEDIA_STOP,
                                         gtk.ICON_SIZE_BUTTON))

    def on_stop_button_leave_notify_event(self, widget, event):
        """
            Unsets the hover state and resets the button icon
        """
        widget.set_data('hovered', False)
        if not widget.is_focus() and \
           ~(event.state & gtk.gdk.SHIFT_MASK):
            widget.set_image(
                gtk.image_new_from_stock(gtk.STOCK_MEDIA_STOP,
                                         gtk.ICON_SIZE_BUTTON))

    def on_stop_button_key_press_event(self, widget, event):
        """
            Shows SPAT icon on Shift key press
        """
        if event.keyval in (gtk.keysyms.Shift_L, gtk.keysyms.Shift_R):
            widget.set_image(
                gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
            widget.set_data('toggle_spat', True)

        if event.keyval in (gtk.keysyms.space, gtk.keysyms.Return):
            if widget.get_data('toggle_spat'):
                self.on_spat_clicked()
            else:
                player.PLAYER.stop()

    def on_stop_button_key_release_event(self, widget, event):
        """
            Resets the button icon
        """
        if event.keyval in (gtk.keysyms.Shift_L, gtk.keysyms.Shift_R):
            widget.set_image(
                gtk.image_new_from_stock(gtk.STOCK_MEDIA_STOP,
                                         gtk.ICON_SIZE_BUTTON))
            widget.set_data('toggle_spat', False)

    def on_stop_button_focus_out_event(self, widget, event):
        """
            Resets the button icon unless
            the button is still hovered
        """
        if not widget.get_data('hovered'):
            widget.set_image(
                gtk.image_new_from_stock(gtk.STOCK_MEDIA_STOP,
                                         gtk.ICON_SIZE_BUTTON))

    def on_stop_button_press_event(self, widget, event):
        """
            Called when the user clicks on the stop button
        """
        if event.button == 1:
            if event.state & gtk.gdk.SHIFT_MASK:
                self.on_spat_clicked()
        elif event.button == 3:
            menu = guiutil.Menu()
            menu.append(_("Toggle: Stop after Selected Track"),
                        self.on_spat_clicked, gtk.STOCK_STOP)
            menu.popup(None, None, None, event.button, event.time)

    def on_stop_button_release_event(self, widget, event):
        """
            Called when the user releases the mouse from the stop button
        """
        rect = widget.get_allocation()
        if 0 <= event.x < rect.width and 0 <= event.y < rect.height:
            player.PLAYER.stop()

    def on_stop_button_drag_motion(self, widget, context, x, y, time):
        """
            Indicates possible SPAT during drag motion of tracks
        """
        target = widget.drag_dest_find_target(
            context, widget.drag_dest_get_target_list())
        if target == 'exaile-index-list':
            widget.set_image(
                gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))

    def on_stop_button_drag_leave(self, widget, context, time):
        """
            Resets the stop button
        """
        widget.set_image(
            gtk.image_new_from_stock(gtk.STOCK_MEDIA_STOP,
                                     gtk.ICON_SIZE_BUTTON))

    def on_stop_button_drag_data_received(self, widget, context, x, y,
                                          selection, info, time):
        """
            Allows for triggering the SPAT feature
            by dropping tracks on the stop button
        """
        source_widget = context.get_source_widget()

        if selection.target == 'exaile-index-list' and isinstance(
                source_widget, PlaylistView):
            position = int(selection.data.split(',')[0])

            if position == source_widget.playlist.spat_position:
                position = -1

            source_widget.playlist.spat_position = position
            source_widget.queue_draw()

    def on_spat_clicked(self, *e):
        """
            Called when the user clicks on the SPAT item
        """
        trs = self.get_selected_page().view.get_selected_items()
        if not trs: return

        # TODO: this works, but implement this some other way in the future
        if player.QUEUE.current_playlist.spat_position == -1:
            player.QUEUE.current_playlist.spat_position = trs[0][0]
        else:
            player.QUEUE.current_playlist.spat_position = -1

        self.get_selected_page().view.queue_draw()

    def on_append_items(self,
                        tracks,
                        force_play=False,
                        queue=False,
                        sort=False,
                        replace=False):
        """
            Called when a panel (or other component)
            has tracks to append and possibly queue

            :param tracks: The tracks to append
            :param force_play: Force playing the first track if there
                                is no track currently playing. Otherwise
                                check a setting to determine whether the
                                track should be played
            :param queue: Additionally queue tracks
            :param sort: Sort before adding
            :param replace: Clear playlist before adding
        """
        if len(tracks) == 0:
            return

        page = self.get_selected_page()

        if sort:
            tracks = trax.sort_tracks(
                ('artist', 'date', 'album', 'discnumber', 'tracknumber'),
                tracks)

        if replace:
            page.playlist.clear()

        offset = len(page.playlist)
        page.playlist.extend(tracks)

        # extending the queue automatically starts playback
        if queue:
            if player.QUEUE is not page.playlist:
                player.QUEUE.extend(tracks)

        elif (force_play or settings.get_option( 'playlist/append_menu_starts_playback', False )) and \
                not player.PLAYER.current:
            page.view.play_track_at(offset, tracks[0])

    def on_playback_error(self, type, player, message):
        """
            Called when there has been a playback error
        """
        glib.idle_add(self.message.show_error,
                      _('Playback error encountered!'), message)

    def on_buffering(self, type, player, percent):
        """
            Called when a stream is buffering
        """
        percent = min(percent, 100)
        glib.idle_add(self.statusbar.set_status,
                      _("Buffering: %d%%...") % percent, 1)

    def on_tags_parsed(self, type, player, args):
        """
            Called when tags are parsed from a stream/track
        """
        (tr, args) = args
        if not tr or tr.is_local():
            return
        if player.parse_stream_tags(tr, args):
            self._update_track_information()

    def on_track_tags_changed(self, type, track, tag):
        """
            Called when tags are changed
        """
        if track is player.PLAYER.current:
            self._update_track_information()

    def on_collection_tree_loaded(self, tree):
        """
            Updates information on collection tree load
        """
        self.statusbar.update_info()

    def on_exaile_loaded(self, event_type, exaile, nothing):
        """
            Updates information on exaile load
        """
        glib.idle_add(self.statusbar.update_info)
        event.remove_callback(self.on_exaile_loaded, 'exaile_loaded')

    def on_playlist_tracks_added(self, type, playlist, tracks):
        """
            Updates information on track add
        """
        glib.idle_add(self.statusbar.update_info)

    def on_playlist_tracks_removed(self, type, playlist, tracks):
        """
            Updates information on track removal
        """
        glib.idle_add(self.statusbar.update_info)

    def on_toggle_pause(self, type, player, object):
        """
            Called when the user clicks the play button after playback has
            already begun
        """
        if player.is_paused():
            image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY,
                                             gtk.ICON_SIZE_SMALL_TOOLBAR)
            tooltip = _('Continue Playback')
        else:
            image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE,
                                             gtk.ICON_SIZE_SMALL_TOOLBAR)
            tooltip = _('Pause Playback')

        glib.idle_add(self.playpause_button.set_image, image)
        glib.idle_add(self.playpause_button.set_tooltip_text, tooltip)
        self._update_track_information()

        # refresh the current playlist
        pl = self.get_selected_page()

    def on_playlist_container_switch_page(self, notebook, page, page_num):
        """
            Updates info after notebook page switch
        """
        page = notebook.get_nth_page(page_num)
        selection = page.view.get_selection()
        selection.connect('changed', self.on_playlist_view_selection_changed)
        self.statusbar.update_info()

    def on_playlist_view_selection_changed(self, selection):
        """
            Updates info after playlist page selection change
        """
        self.statusbar.update_info()

    def on_panel_filter_focus(self, *e):
        """
            Gives focus to the filter field of the current panel
        """
        try:
            self.controller.get_active_panel().filter.grab_focus()
        except (AttributeError, KeyError):
            pass

    def on_search_playlist_focus(self, *e):
        """
            Gives focus to the playlist search bar
        """
        plpage = get_selected_playlist()
        if plpage:
            plpage.get_search_entry().grab_focus()

    def on_save_playlist(self, *e):
        """
            Called when the user presses Ctrl+S
            Spawns the save dialog of the currently selected playlist tab if
            not custom, saves changes directly if custom
        """
        tab = self.get_selected_tab()
        if not tab: return
        if tab.page.playlist.get_is_custom():
            tab.do_save_changes_to_custom()
        else:
            tab.do_save_custom()

    def on_save_playlist_as(self, *e):
        """
            Called when the user presses Ctrl+S
            Spawns the save as dialog of the current playlist tab
        """
        tab = self.get_selected_tab()
        if not tab: return
        tab.do_save_custom()

    def on_clear_playlist(self, *e):
        """
            Clears the current playlist tab
        """
        page = self.get_selected_page()
        if page:
            page.playlist.clear()

    def on_open_item_activate(self, menuitem):
        """
            Shows a dialog to open media
        """
        def on_uris_selected(dialog, uris):
            uris.reverse()

            if len(uris) > 0:
                self.controller.open_uri(uris.pop(), play=True)

            for uri in uris:
                self.controller.open_uri(uri, play=False)

        dialog = dialogs.MediaOpenDialog(self.window)
        dialog.connect('uris-selected', on_uris_selected)
        dialog.show()

    def on_open_url_item_activate(self, menuitem):
        """
            Shows a dialog to open an URI
        """
        def on_uri_selected(dialog, uri):
            self.controller.open_uri(uri, play=False)

        dialog = dialogs.URIOpenDialog(self.window)
        dialog.connect('uri-selected', on_uri_selected)
        dialog.show()

    def on_open_directories_item_activate(self, menuitem):
        """
            Shows a dialog to open directories
        """
        def on_uris_selected(dialog, uris):
            uris.reverse()

            if len(uris) > 0:
                self.controller.open_uri(uris.pop(), play=True)

            for uri in uris:
                self.controller.open_uri(uri, play=False)

        dialog = dialogs.DirectoryOpenDialog(self.window)
        # Selecting empty folders is useless
        dialog.props.create_folders = False
        dialog.connect('uris-selected', on_uris_selected)
        dialog.show()

    def on_export_current_playlist_activate(self, menuitem):
        """
            Shows a dialog to export the current playlist
        """
        page = self.get_selected_page()

        if not page or not isinstance(page, PlaylistPage):
            return

        def on_message(dialog, message_type, message):
            """
                Show messages in the main window message area
            """
            if message_type == gtk.MESSAGE_INFO:
                self.message.show_info(markup=message)
            elif message_type == gtk.MESSAGE_ERROR:
                self.message.show_error(_('Playlist export failed!'), message)

            return True

        dialog = dialogs.PlaylistExportDialog(page.playlist, self.window)
        dialog.connect('message', on_message)
        dialog.show()

    def on_playlist_utilities_bar_visible_toggled(self, checkmenuitem):
        """
            Shows or hides the playlist utilities bar
        """
        settings.set_option('gui/playlist_utilities_bar_visible',
                            checkmenuitem.get_active())

    def on_show_playing_track_item_activate(self, menuitem):
        """
            Tries to show the currently playing track
        """
        self.playlist_container.show_current_track()

    def on_about_item_activate(self, menuitem):
        """
            Shows the about dialog
        """
        dialog = dialogs.AboutDialog(self.window)
        dialog.show()

    def on_playback_resume(self, type, player, data):
        self.resuming = True

    def on_playback_start(self, type, player, object):
        """
            Called when playback starts
            Sets the currently playing track visible in the currently selected
            playlist if the user has chosen this setting
        """
        if self.resuming:
            self.resuming = False
            return

        self._update_track_information()
        glib.idle_add(
            self.playpause_button.set_image,
            gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE,
                                     gtk.ICON_SIZE_SMALL_TOOLBAR))
        glib.idle_add(self.playpause_button.set_tooltip_text,
                      _('Pause Playback'))

    def on_playback_end(self, type, player, object):
        """
            Called when playback ends
        """
        glib.idle_add(self.window.set_title, 'Exaile')

        glib.idle_add(
            self.playpause_button.set_image,
            gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY,
                                     gtk.ICON_SIZE_SMALL_TOOLBAR))
        glib.idle_add(self.playpause_button.set_tooltip_text,
                      _('Start Playback'))

    def _on_option_set(self, name, object, option):
        """
           Handles changes of settings
        """
        if option == 'gui/main_window_title_format':
            self.title_formatter.props.format = settings.get_option(
                option, self.title_formatter.props.format)

        if option == 'gui/use_tray':
            usetray = settings.get_option(option, False)
            if self.controller.tray_icon and not usetray:
                glib.idle_add(self.controller.tray_icon.destroy)
                self.controller.tray_icon = None
            elif not self.controller.tray_icon and usetray:
                self.controller.tray_icon = tray.TrayIcon(self)

        if option == 'gui/show_info_area':
            glib.idle_add(self.info_area.set_no_show_all, False)
            if settings.get_option(option, True):
                glib.idle_add(self.info_area.show_all)
            else:
                glib.idle_add(self.info_area.hide_all)
            glib.idle_add(self.info_area.set_no_show_all, True)

        if option == 'gui/show_info_area_covers':

            def _setup_info_covers():
                cover = self.info_area.cover
                cover.set_no_show_all(False)
                if settings.get_option(option, True):
                    cover.show_all()
                else:
                    cover.hide_all()
                cover.set_no_show_all(True)

            glib.idle_add(_setup_info_covers)

    def _on_volume_key(self, is_up):
        diff = int(100 * settings.get_option('gui/volue_key_step_size',
                                             VOLUME_STEP_DEFAULT))
        if not is_up: diff = -diff

        player.PLAYER.modify_volume(diff)
        return True

    def _on_seek_key(self, is_forward):
        diff = settings.get_option('gui/seek_key_step_size', SEEK_STEP_DEFAULT)
        if not is_forward: diff = -diff

        if player.PLAYER.current:
            player.PLAYER.modify_time(diff)
            self.progress_bar.update_progress()

        return True

    def _on_prev_tab_key(self, *e):
        self.playlist_container.get_current_notebook().select_prev_tab()
        return True

    def _on_next_tab_key(self, *e):
        self.playlist_container.get_current_notebook().select_next_tab()
        return True

    def _on_playpause_button(self, *e):
        self.playpause()
        return True

    def _on_focus_playlist_tab(self, tab_nr):
        self.playlist_container.get_current_notebook().focus_tab(tab_nr)
        return True

    def _on_focus_playlist_container(self, *_e):
        self.playlist_container.focus()
        return True

    def _update_track_information(self):
        """
            Sets track information
        """
        track = player.PLAYER.current

        if not track:
            return

        glib.idle_add(self.window.set_title,
                      self.title_formatter.format(track))

    def playpause(self):
        """
            Pauses the playlist if it is playing, starts playing if it is
            paused. If stopped, try to start playing the next suitable track.
        """
        if player.PLAYER.is_paused() or player.PLAYER.is_playing():
            player.PLAYER.toggle_pause()
        else:
            pl = self.get_selected_page()
            player.QUEUE.set_current_playlist(pl.playlist)
            try:
                trackpath = pl.view.get_selected_paths()[0]
                pl.playlist.current_position = trackpath[0]
            except IndexError:
                pass
            player.QUEUE.play(track=pl.playlist.current)

    def _setup_position(self):
        """
            Sets up the position and sized based on the size the window was
            when it was last moved or resized
        """
        if settings.get_option('gui/mainw_maximized', False):
            self.window.maximize()

        width = settings.get_option('gui/mainw_width', 500)
        height = settings.get_option('gui/mainw_height', 475)
        x = settings.get_option('gui/mainw_x', 10)
        y = settings.get_option('gui/mainw_y', 10)

        self.window.move(x, y)
        self.window.resize(width, height)

        pos = settings.get_option('gui/mainw_sash_pos', 200)
        self.splitter.set_position(pos)

    def on_delete_event(self, *e):
        """
            Called when the user attempts to close the window
        """
        sash_pos = self.splitter.get_position()
        if sash_pos > 10:
            settings.set_option('gui/mainw_sash_pos', sash_pos)

        if settings.get_option('gui/use_tray', False) and \
           settings.get_option('gui/close_to_tray', False):
            self.window.hide()
        else:
            self.quit()
        return True

    def quit(self, *e):
        """
            Quits Exaile
        """
        self.window.hide()
        glib.idle_add(self.controller.exaile.quit)
        return True

    def on_restart_item_activate(self, menuitem):
        """
            Restarts Exaile
        """
        self.window.hide()
        glib.idle_add(self.controller.exaile.quit, True)

    def toggle_visible(self, bringtofront=False):
        """
            Toggles visibility of the main window
        """
        toggle_handled = self.emit('main-visible-toggle')

        if not toggle_handled:
            if bringtofront and self.window.is_active() or \
               not bringtofront and self.window.get_property('visible'):
                self.window.hide()
            else:
                # the ordering for deiconify/show matters -- if this gets
                # switched, then the minimization detection breaks
                self.window.deiconify()
                self.window.show()

    def configure_event(self, *e):
        """
            Called when the window is resized or moved
        """
        # Don't save window size if it is maximized or fullscreen.
        if settings.get_option('gui/mainw_maximized', False) or \
                self._fullscreen:
            return False

        (width, height) = self.window.get_size()
        if [width, height] != [ settings.get_option("gui/mainw_"+key, -1) for \
                key in ["width", "height"] ]:
            settings.set_option('gui/mainw_height', height)
            settings.set_option('gui/mainw_width', width)
        (x, y) = self.window.get_position()
        if [x, y] != [ settings.get_option("gui/mainw_"+key, -1) for \
                key in ["x", "y"] ]:
            settings.set_option('gui/mainw_x', x)
            settings.set_option('gui/mainw_y', y)

        return False

    def window_state_change_event(self, window, event):
        """
            Saves the current maximized and fullscreen
            states and minimizes to tray if requested
        """
        if event.changed_mask & gtk.gdk.WINDOW_STATE_MAXIMIZED:
            settings.set_option(
                'gui/mainw_maximized',
                bool(event.new_window_state & gtk.gdk.WINDOW_STATE_MAXIMIZED))
        if event.changed_mask & gtk.gdk.WINDOW_STATE_FULLSCREEN:
            self._fullscreen = bool(event.new_window_state
                                    & gtk.gdk.WINDOW_STATE_FULLSCREEN)

        # detect minimization state changes
        prev_minimized = self.minimized

        if not self.minimized:

            if event.changed_mask & gtk.gdk.WINDOW_STATE_ICONIFIED and \
               not event.changed_mask & gtk.gdk.WINDOW_STATE_WITHDRAWN and \
               event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED and \
               not event.new_window_state & gtk.gdk.WINDOW_STATE_WITHDRAWN and \
               not self.window_state & gtk.gdk.WINDOW_STATE_ICONIFIED:

                self.minimized = True
        else:
            if event.changed_mask & gtk.gdk.WINDOW_STATE_WITHDRAWN and \
               not event.new_window_state & (gtk.gdk.WINDOW_STATE_WITHDRAWN): #and \

                self.minimized = False

        # track this
        self.window_state = event.new_window_state

        if settings.get_option('gui/minimize_to_tray', False):

            # old code to detect minimization
            # -> it must have worked at some point, perhaps this is a GTK version
            # specific set of behaviors? Current code works now on 2.24.17

            #if wm_state is not None:
            #    if '_NET_WM_STATE_HIDDEN' in wm_state[2]:
            #        show tray
            #        window.hide
            #else
            #    destroy tray

            if self.minimized != prev_minimized and self.minimized == True:
                if not settings.get_option('gui/use_tray', False) and \
                    self.controller.tray_icon is None:
                    self.controller.tray_icon = tray.TrayIcon(self)

                window.hide()
            elif window.window.property_get('_NET_WM_STATE') is None:
                if not settings.get_option('gui/use_tray', False) and \
                    self.controller.tray_icon is not None:
                    self.controller.tray_icon.destroy()
                    self.controller.tray_icon = None

        return False

    def get_selected_page(self):
        """
            Returns the currentry displayed playlist notebook page
        """
        return self.playlist_container.get_current_tab()

    def get_selected_playlist(self):
        try:
            page = self.get_selected_page()
        except AttributeError:
            return None
        if not isinstance(page, PlaylistPage):
            return None
        return page
Exemple #3
0
class MainWindow(GObject.GObject):
    """
        Main Exaile Window
    """

    __gproperties__ = {
        'is-fullscreen': (
            bool,
            'Fullscreen',
            'Whether the window is fullscreen.',
            False,  # Default
            GObject.PARAM_READWRITE,
        )
    }

    __gsignals__ = {'main-visible-toggle': (GObject.SignalFlags.RUN_LAST, bool, ())}

    _mainwindow = None

    def __init__(self, controller, builder, collection):
        """
            Initializes the main window

            @param controller: the main gui controller
        """
        GObject.GObject.__init__(self)

        self.controller = controller
        self.collection = collection
        self.playlist_manager = controller.exaile.playlists
        self.current_page = -1
        self._fullscreen = False
        self.resuming = False

        self.window_state = 0
        self.minimized = False

        self.builder = builder

        self.window = self.builder.get_object('ExaileWindow')
        self.window.set_title('Exaile')
        self.title_formatter = formatter.TrackFormatter(
            settings.get_option(
                'gui/main_window_title_format', _('$title (by $artist)') + ' - Exaile'
            )
        )

        self.accel_group = Gtk.AccelGroup()
        self.window.add_accel_group(self.accel_group)
        self.accel_manager = AcceleratorManager(
            'mainwindow-accelerators', self.accel_group
        )
        self.menubar = self.builder.get_object("mainmenu")

        fileitem = self.builder.get_object("file_menu_item")
        filemenu = menu.ProviderMenu('menubar-file-menu', self)
        fileitem.set_submenu(filemenu)

        edititem = self.builder.get_object("edit_menu_item")
        editmenu = menu.ProviderMenu('menubar-edit-menu', self)
        edititem.set_submenu(editmenu)

        viewitem = self.builder.get_object("view_menu_item")
        viewmenu = menu.ProviderMenu('menubar-view-menu', self)
        viewitem.set_submenu(viewmenu)

        toolsitem = self.builder.get_object("tools_menu_item")
        toolsmenu = menu.ProviderMenu('menubar-tools-menu', self)
        toolsitem.set_submenu(toolsmenu)

        helpitem = self.builder.get_object("help_menu_item")
        helpmenu = menu.ProviderMenu('menubar-help-menu', self)
        helpitem.set_submenu(helpmenu)

        self._setup_widgets()
        self._setup_position()
        self._setup_hotkeys()
        logger.info("Connecting main window events...")
        self._connect_events()
        MainWindow._mainwindow = self

        mainmenu._create_menus()

    def _setup_hotkeys(self):
        """
            Sets up accelerators that haven't been set up in UI designer
        """

        def factory(integer, description):
            """ Generate key bindings for Alt keys """
            keybinding = '<Alt>%s' % str(integer)
            callback = lambda *_e: self._on_focus_playlist_tab(integer - 1)
            return (keybinding, description, callback)

        hotkeys = (
            (
                '<Primary>S',
                _('Save currently selected playlist'),
                lambda *_e: self.on_save_playlist(),
            ),
            (
                '<Shift><Primary>S',
                _('Save currently selected playlist under a custom name'),
                lambda *_e: self.on_save_playlist_as(),
            ),
            (
                '<Primary>F',
                _('Focus filter in currently focused panel'),
                lambda *_e: self.on_panel_filter_focus(),
            ),
            (
                '<Primary>G',
                _('Focus playlist search'),
                lambda *_e: self.on_search_playlist_focus(),
            ),  # FIXME
            (
                '<Primary><Alt>l',
                _('Clear queue'),
                lambda *_e: player.QUEUE.clear(),
            ),  # FIXME
            (
                '<Primary>P',
                _('Start, pause or resume the playback'),
                self._on_playpause_button,
            ),
            (
                '<Primary>Right',
                _('Seek to the right'),
                lambda *_e: self._on_seek_key(True),
            ),
            (
                '<Primary>Left',
                _('Seek to the left'),
                lambda *_e: self._on_seek_key(False),
            ),
            (
                '<Primary>plus',
                _('Increase the volume'),
                lambda *_e: self._on_volume_key(True),
            ),
            (
                '<Primary>equal',
                _('Increase the volume'),
                lambda *_e: self._on_volume_key(True),
            ),
            (
                '<Primary>minus',
                _('Decrease the volume'),
                lambda *_e: self._on_volume_key(False),
            ),
            ('<Primary>Page_Up', _('Switch to previous tab'), self._on_prev_tab_key),
            ('<Primary>Page_Down', _('Switch to next tab'), self._on_next_tab_key),
            (
                '<Alt>N',
                _('Focus the playlist container'),
                self._on_focus_playlist_container,
            ),
            # These 4 are subject to change.. probably should do this
            # via a different mechanism too...
            (
                '<Alt>I',
                _('Focus the files panel'),
                lambda *_e: self.controller.focus_panel('files'),
            ),
            # ('<Alt>C', _('Focus the collection panel'),  # TODO: Does not work, why?
            # lambda *_e: self.controller.focus_panel('collection')),
            (
                '<Alt>R',
                _('Focus the radio panel'),
                lambda *_e: self.controller.focus_panel('radio'),
            ),
            (
                '<Alt>L',
                _('Focus the playlists panel'),
                lambda *_e: self.controller.focus_panel('playlists'),
            ),
            factory(1, _('Focus the first tab')),
            factory(2, _('Focus the second tab')),
            factory(3, _('Focus the third tab')),
            factory(4, _('Focus the fourth tab')),
            factory(5, _('Focus the fifth tab')),
            factory(6, _('Focus the sixth tab')),
            factory(7, _('Focus the seventh tab')),
            factory(8, _('Focus the eighth tab')),
            factory(9, _('Focus the ninth tab')),
            factory(0, _('Focus the tenth tab')),
        )

        for keys, helptext, function in hotkeys:
            accelerator = Accelerator(keys, helptext, function)
            providers.register('mainwindow-accelerators', accelerator)

    def _setup_widgets(self):
        """
            Sets up the various widgets
        """
        # TODO: Maybe make this stackable
        self.message = dialogs.MessageBar(
            parent=self.builder.get_object('player_box'), buttons=Gtk.ButtonsType.CLOSE
        )

        self.info_area = MainWindowTrackInfoPane(player.PLAYER)
        self.info_area.set_auto_update(True)
        self.info_area.set_border_width(3)
        self.info_area.hide()
        self.info_area.set_no_show_all(True)
        guiutil.gtk_widget_replace(self.builder.get_object('info_area'), self.info_area)

        self.volume_control = playback.VolumeControl(player.PLAYER)
        self.info_area.get_action_area().pack_end(self.volume_control, False, False, 0)

        if settings.get_option('gui/use_alpha', False):
            screen = self.window.get_screen()
            visual = screen.get_rgba_visual()
            self.window.set_visual(visual)
            self.window.connect('screen-changed', self.on_screen_changed)
            self._update_alpha()

        self._update_dark_hint()

        playlist_area = self.builder.get_object('playlist_area')
        self.playlist_container = PlaylistContainer('saved_tabs', player.PLAYER)
        for notebook in self.playlist_container.notebooks:
            notebook.connect_after(
                'switch-page', self.on_playlist_container_switch_page
            )
            page = notebook.get_current_tab()
            if page is not None:
                selection = page.view.get_selection()
                selection.connect('changed', self.on_playlist_view_selection_changed)

        playlist_area.pack_start(self.playlist_container, True, True, 3)

        self.splitter = self.builder.get_object('splitter')

        # In most (all?) RTL locales, the playback controls should still be LTR.
        # Just in case that's not always the case, we provide a hidden option to
        # force RTL layout instead. This can be removed once we're more certain
        # that the default behavior (always LTR) is correct.
        controls_direction = (
            Gtk.TextDirection.RTL
            if settings.get_option('gui/rtl_playback_controls')
            else Gtk.TextDirection.LTR
        )

        self.play_image = Gtk.Image.new_from_icon_name(
            'media-playback-start', Gtk.IconSize.SMALL_TOOLBAR
        )
        self.play_image.set_direction(controls_direction)
        self.pause_image = Gtk.Image.new_from_icon_name(
            'media-playback-pause', Gtk.IconSize.SMALL_TOOLBAR
        )
        self.pause_image.set_direction(controls_direction)

        play_toolbar = self.builder.get_object('play_toolbar')
        play_toolbar.set_direction(controls_direction)
        for button in ('playpause', 'next', 'prev', 'stop'):
            widget = self.builder.get_object('%s_button' % button)
            setattr(self, '%s_button' % button, widget)
            widget.get_child().set_direction(controls_direction)

        self.progress_bar = playback.SeekProgressBar(player.PLAYER)
        self.progress_bar.get_child().set_direction(controls_direction)
        # Don't expand vertically; looks awful on Adwaita.
        self.progress_bar.set_valign(Gtk.Align.CENTER)
        guiutil.gtk_widget_replace(
            self.builder.get_object('playback_progressbar_dummy'), self.progress_bar
        )

        self.stop_button.toggle_spat = False
        self.stop_button.add_events(Gdk.EventMask.POINTER_MOTION_MASK)
        self.stop_button.connect(
            'motion-notify-event', self.on_stop_button_motion_notify_event
        )
        self.stop_button.connect(
            'leave-notify-event', self.on_stop_button_leave_notify_event
        )
        self.stop_button.connect('key-press-event', self.on_stop_button_key_press_event)
        self.stop_button.connect(
            'key-release-event', self.on_stop_button_key_release_event
        )
        self.stop_button.connect('focus-out-event', self.on_stop_button_focus_out_event)
        self.stop_button.connect('button-press-event', self.on_stop_button_press_event)
        self.stop_button.connect(
            'button-release-event', self.on_stop_button_release_event
        )
        self.stop_button.drag_dest_set(
            Gtk.DestDefaults.ALL,
            [Gtk.TargetEntry.new("exaile-index-list", Gtk.TargetFlags.SAME_APP, 0)],
            Gdk.DragAction.COPY,
        )
        self.stop_button.connect('drag-motion', self.on_stop_button_drag_motion)
        self.stop_button.connect('drag-leave', self.on_stop_button_drag_leave)
        self.stop_button.connect(
            'drag-data-received', self.on_stop_button_drag_data_received
        )

        self.statusbar = info.Statusbar(self.builder.get_object('status_bar'))
        event.add_ui_callback(self.on_exaile_loaded, 'exaile_loaded')

    def _connect_events(self):
        """
            Connects the various events to their handlers
        """
        self.builder.connect_signals(
            {
                'on_configure_event': self.configure_event,
                'on_window_state_event': self.window_state_change_event,
                'on_delete_event': self.on_delete_event,
                'on_playpause_button_clicked': self._on_playpause_button,
                'on_next_button_clicked': lambda *e: player.QUEUE.next(),
                'on_prev_button_clicked': lambda *e: player.QUEUE.prev(),
                'on_about_item_activate': self.on_about_item_activate,
                # Controller
                #            'on_scan_collection_item_activate': self.controller.on_rescan_collection,
                #            'on_device_manager_item_activate': lambda *e: self.controller.show_devices(),
                #            'on_track_properties_activate':self.controller.on_track_properties,
            }
        )

        event.add_ui_callback(
            self.on_playback_resume, 'playback_player_resume', player.PLAYER
        )
        event.add_ui_callback(
            self.on_playback_end, 'playback_player_end', player.PLAYER
        )
        event.add_ui_callback(self.on_playback_end, 'playback_error', player.PLAYER)
        event.add_ui_callback(
            self.on_playback_start, 'playback_track_start', player.PLAYER
        )
        event.add_ui_callback(
            self.on_toggle_pause, 'playback_toggle_pause', player.PLAYER
        )
        event.add_ui_callback(self.on_track_tags_changed, 'track_tags_changed')
        event.add_ui_callback(self.on_buffering, 'playback_buffering', player.PLAYER)
        event.add_ui_callback(self.on_playback_error, 'playback_error', player.PLAYER)

        event.add_ui_callback(self.on_playlist_tracks_added, 'playlist_tracks_added')
        event.add_ui_callback(
            self.on_playlist_tracks_removed, 'playlist_tracks_removed'
        )

        # Settings
        self._on_option_set('gui_option_set', settings, 'gui/show_info_area')
        self._on_option_set('gui_option_set', settings, 'gui/show_info_area_covers')
        event.add_ui_callback(self._on_option_set, 'option_set')

    def _connect_panel_events(self):
        """
            Sets up panel events
        """

        # When there's nothing in the notebook, hide it
        self.controller.panel_notebook.connect(
            'page-added', self.on_panel_notebook_add_page
        )
        self.controller.panel_notebook.connect(
            'page-removed', self.on_panel_notebook_remove_page
        )

        # panels
        panels = self.controller.panel_notebook.panels

        for panel_name in ('playlists', 'radio', 'files', 'collection'):
            panel = panels[panel_name].panel
            do_sort = False

            if panel_name in ('files', 'collection'):
                do_sort = True

            panel.connect(
                'append-items',
                lambda panel, items, force_play: self.on_append_items(
                    items, force_play, sort=do_sort
                ),
            )
            panel.connect(
                'queue-items',
                lambda panel, items: self.on_append_items(
                    items, queue=True, sort=do_sort
                ),
            )
            panel.connect(
                'replace-items',
                lambda panel, items: self.on_append_items(
                    items, replace=True, sort=do_sort
                ),
            )

        ## Collection Panel
        panel = panels['collection'].panel
        panel.connect('collection-tree-loaded', self.on_collection_tree_loaded)

        ## Playlist Panel
        panel = panels['playlists'].panel
        panel.connect(
            'playlist-selected',
            lambda panel, playlist: self.playlist_container.create_tab_from_playlist(
                playlist
            ),
        )

        ## Radio Panel
        panel = panels['radio'].panel
        panel.connect(
            'playlist-selected',
            lambda panel, playlist: self.playlist_container.create_tab_from_playlist(
                playlist
            ),
        )

        ## Files Panel
        # panel = panels['files']

    def _update_alpha(self):
        if not settings.get_option('gui/use_alpha', False):
            return
        opac = 1.0 - float(settings.get_option('gui/transparency', 0.3))
        Gtk.Widget.set_opacity(self.window, opac)

    def _update_dark_hint(self):
        gs = Gtk.Settings.get_default()

        # We should use reset_property, but that's only available in > 3.20...
        if not hasattr(self, '_default_dark_hint'):
            self._default_dark_hint = gs.props.gtk_application_prefer_dark_theme

        if settings.get_option('gui/gtk_dark_hint', False):
            gs.props.gtk_application_prefer_dark_theme = True

        elif gs.props.gtk_application_prefer_dark_theme != self._default_dark_hint:
            # don't set it explicitly otherwise the app will revert to a light
            # theme -- what we actually want is to leave it up to the OS
            gs.props.gtk_application_prefer_dark_theme = self._default_dark_hint

    def do_get_property(self, prop):
        if prop.name == 'is-fullscreen':
            return self._fullscreen
        else:
            return GObject.GObject.do_get_property(self, prop)

    def do_set_property(self, prop, value):
        if prop.name == 'is-fullscreen':
            if value:
                self.window.fullscreen()
            else:
                self.window.unfullscreen()
        else:
            GObject.GObject.do_set_property(self, prop, value)

    def on_screen_changed(self, widget, event):
        """
            Updates the colormap on screen change
        """
        screen = widget.get_screen()
        visual = screen.get_rgba_visual() or screen.get_rgb_visual()
        self.window.set_visual(visual)

    def on_panel_notebook_add_page(self, notebook, page, page_num):
        if self.splitter.get_child1() is None:
            self.splitter.pack1(self.controller.panel_notebook)
            self.controller.panel_notebook.get_parent().child_set_property(
                self.controller.panel_notebook, 'shrink', False
            )

    def on_panel_notebook_remove_page(self, notebook, page, page_num):
        if notebook.get_n_pages() == 0:
            self.splitter.remove(self.controller.panel_notebook)

    def on_stop_button_motion_notify_event(self, widget, event):
        """
            Sets the hover state and shows SPAT icon
        """
        widget.__hovered = True
        if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
            widget.set_image(
                Gtk.Image.new_from_icon_name('process-stop', Gtk.IconSize.BUTTON)
            )
        else:
            widget.set_image(
                Gtk.Image.new_from_icon_name('media-playback-stop', Gtk.IconSize.BUTTON)
            )

    def on_stop_button_leave_notify_event(self, widget, event):
        """
            Unsets the hover state and resets the button icon
        """
        widget.__hovered = False
        if not widget.is_focus() and ~(event.get_state() & Gdk.ModifierType.SHIFT_MASK):
            widget.set_image(
                Gtk.Image.new_from_icon_name('media-playback-stop', Gtk.IconSize.BUTTON)
            )

    def on_stop_button_key_press_event(self, widget, event):
        """
            Shows SPAT icon on Shift key press
        """
        if event.keyval in (Gdk.KEY_Shift_L, Gdk.KEY_Shift_R):
            widget.set_image(
                Gtk.Image.new_from_icon_name('process-stop', Gtk.IconSize.BUTTON)
            )
            widget.toggle_spat = True

        if event.keyval in (Gdk.KEY_space, Gdk.KEY_Return):
            if widget.toggle_spat:
                self.on_spat_clicked()
            else:
                player.PLAYER.stop()

    def on_stop_button_key_release_event(self, widget, event):
        """
            Resets the button icon
        """
        if event.keyval in (Gdk.KEY_Shift_L, Gdk.KEY_Shift_R):
            widget.set_image(
                Gtk.Image.new_from_icon_name('media-playback-stop', Gtk.IconSize.BUTTON)
            )
            widget.toggle_spat = False

    def on_stop_button_focus_out_event(self, widget, event):
        """
            Resets the button icon unless
            the button is still hovered
        """
        if not getattr(widget, '__hovered', False):
            widget.set_image(
                Gtk.Image.new_from_icon_name('media-playback-stop', Gtk.IconSize.BUTTON)
            )

    def on_stop_button_press_event(self, widget, event):
        """
            Called when the user clicks on the stop button
        """
        if event.button == Gdk.BUTTON_PRIMARY:
            if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
                self.on_spat_clicked()
        elif event.triggers_context_menu():
            m = menu.Menu(self)
            m.attach_to_widget(widget)
            m.add_simple(
                _("Toggle: Stop after Selected Track"),
                self.on_spat_clicked,
                'process-stop',
            )
            m.popup(event)

    def on_stop_button_release_event(self, widget, event):
        """
            Called when the user releases the mouse from the stop button
        """
        rect = widget.get_allocation()
        if 0 <= event.x < rect.width and 0 <= event.y < rect.height:
            player.PLAYER.stop()

    def on_stop_button_drag_motion(self, widget, context, x, y, time):
        """
            Indicates possible SPAT during drag motion of tracks
        """
        target = widget.drag_dest_find_target(context, None).name()
        if target == 'exaile-index-list':
            widget.set_image(
                Gtk.Image.new_from_icon_name('process-stop', Gtk.IconSize.BUTTON)
            )

    def on_stop_button_drag_leave(self, widget, context, time):
        """
            Resets the stop button
        """
        widget.set_image(
            Gtk.Image.new_from_icon_name('media-playback-stop', Gtk.IconSize.BUTTON)
        )

    def on_stop_button_drag_data_received(
        self, widget, context, x, y, selection, info, time
    ):
        """
            Allows for triggering the SPAT feature
            by dropping tracks on the stop button
        """
        source_widget = Gtk.drag_get_source_widget(context)

        if selection.target.name() == 'exaile-index-list' and isinstance(
            source_widget, PlaylistView
        ):
            position = int(selection.data.split(',')[0])

            if position == source_widget.playlist.spat_position:
                position = -1

            source_widget.playlist.spat_position = position
            source_widget.queue_draw()

    def on_spat_clicked(self, *e):
        """
            Called when the user clicks on the SPAT item
        """
        trs = self.get_selected_page().view.get_selected_items()
        if not trs:
            return

        # TODO: this works, but implement this some other way in the future
        if player.QUEUE.current_playlist.spat_position == -1:
            player.QUEUE.current_playlist.spat_position = trs[0][0]
        else:
            player.QUEUE.current_playlist.spat_position = -1

        self.get_selected_page().view.queue_draw()

    def on_append_items(
        self, tracks, force_play=False, queue=False, sort=False, replace=False
    ):
        """
            Called when a panel (or other component)
            has tracks to append and possibly queue

            :param tracks: The tracks to append
            :param force_play: Force playing the first track if there
                                is no track currently playing. Otherwise
                                check a setting to determine whether the
                                track should be played
            :param queue: Additionally queue tracks
            :param sort: Sort before adding
            :param replace: Clear playlist before adding
        """
        if len(tracks) == 0:
            return

        page = self.get_selected_page()

        if sort:
            tracks = trax.sort_tracks(common.BASE_SORT_TAGS, tracks)

        if replace:
            page.playlist.clear()

        offset = len(page.playlist)
        page.playlist.extend(tracks)

        # extending the queue automatically starts playback
        if queue:
            if player.QUEUE is not page.playlist:
                player.QUEUE.extend(tracks)

        elif (
            force_play
            or settings.get_option('playlist/append_menu_starts_playback', False)
        ) and not player.PLAYER.current:
            page.view.play_track_at(offset, tracks[0])

    def on_playback_error(self, type, player, message):
        """
            Called when there has been a playback error
        """
        self.message.show_error(_('Playback error encountered!'), message)

    def on_buffering(self, type, player, percent):
        """
            Called when a stream is buffering
        """
        percent = min(percent, 100)
        self.statusbar.set_status(_("Buffering: %d%%...") % percent, 1)

    def on_track_tags_changed(self, type, track, tags):
        """
            Called when tags are changed
        """
        if track is player.PLAYER.current:
            self._update_track_information()

    def on_collection_tree_loaded(self, tree):
        """
            Updates information on collection tree load
        """
        self.statusbar.update_info()

    def on_exaile_loaded(self, event_type, exaile, nothing):
        """
            Updates information on exaile load
        """
        self.statusbar.update_info()
        event.remove_callback(self.on_exaile_loaded, 'exaile_loaded')

    def on_playlist_tracks_added(self, type, playlist, tracks):
        """
            Updates information on track add
        """
        self.statusbar.update_info()

    def on_playlist_tracks_removed(self, type, playlist, tracks):
        """
            Updates information on track removal
        """
        self.statusbar.update_info()

    def on_toggle_pause(self, type, player, object):
        """
            Called when the user clicks the play button after playback has
            already begun
        """
        if player.is_paused():
            image = self.play_image
            tooltip = _('Continue Playback')
        else:
            image = self.pause_image
            tooltip = _('Pause Playback')

        self.playpause_button.set_image(image)
        self.playpause_button.set_tooltip_text(tooltip)
        self._update_track_information()

    def on_playlist_container_switch_page(self, notebook, page, page_num):
        """
            Updates info after notebook page switch
        """
        page = notebook.get_nth_page(page_num)
        selection = page.view.get_selection()
        selection.connect('changed', self.on_playlist_view_selection_changed)
        self.statusbar.update_info()

    def on_playlist_view_selection_changed(self, selection):
        """
            Updates info after playlist page selection change
        """
        self.statusbar.update_info()

    def on_panel_filter_focus(self, *e):
        """
            Gives focus to the filter field of the current panel
        """
        try:
            self.controller.get_active_panel().filter.grab_focus()
        except (AttributeError, KeyError):
            pass

    def on_search_playlist_focus(self, *e):
        """
            Gives focus to the playlist search bar
        """
        plpage = get_selected_playlist()
        if plpage:
            plpage.get_search_entry().grab_focus()

    def on_save_playlist(self, *e):
        """
            Called when the user presses Ctrl+S
        """
        page = self.get_selected_playlist()
        if page:
            page.on_save()

    def on_save_playlist_as(self, *e):
        """
            Called when the user presses Ctrl+S
            Spawns the save as dialog of the current playlist tab
        """
        page = self.get_selected_playlist()
        if page:
            page.on_saveas()

    def on_clear_playlist(self, *e):
        """
            Clears the current playlist tab
        """
        page = self.get_selected_page()
        if page:
            page.playlist.clear()

    def on_open_item_activate(self, menuitem):
        """
            Shows a dialog to open media
        """

        def on_uris_selected(dialog, uris):
            uris.reverse()

            if len(uris) > 0:
                self.controller.open_uri(uris.pop(), play=True)

            for uri in uris:
                self.controller.open_uri(uri, play=False)

        dialog = dialogs.MediaOpenDialog(self.window)
        dialog.connect('uris-selected', on_uris_selected)
        dialog.show()

    def on_open_url_item_activate(self, menuitem):
        """
            Shows a dialog to open an URI
        """

        def on_uri_selected(dialog, uri):
            self.controller.open_uri(uri, play=False)

        dialog = dialogs.URIOpenDialog(self.window)
        dialog.connect('uri-selected', on_uri_selected)
        dialog.show()

    def on_open_directories_item_activate(self, menuitem):
        """
            Shows a dialog to open directories
        """

        def on_uris_selected(dialog, uris):
            uris.reverse()

            if len(uris) > 0:
                self.controller.open_uri(uris.pop(), play=True)

            for uri in uris:
                self.controller.open_uri(uri, play=False)

        dialog = dialogs.DirectoryOpenDialog(self.window)
        # Selecting empty folders is useless
        dialog.props.create_folders = False
        dialog.connect('uris-selected', on_uris_selected)
        dialog.show()

    def on_export_current_playlist_activate(self, menuitem):
        """
            Shows a dialog to export the current playlist
        """
        page = self.get_selected_page()

        if not page or not isinstance(page, PlaylistPage):
            return

        def on_message(dialog, message_type, message):
            """
                Show messages in the main window message area
            """
            if message_type == Gtk.MessageType.INFO:
                self.message.show_info(markup=message)
            elif message_type == Gtk.MessageType.ERROR:
                self.message.show_error(_('Playlist export failed!'), message)

            return True

        dialog = dialogs.PlaylistExportDialog(page.playlist, self.window)
        dialog.connect('message', on_message)
        dialog.show()

    def on_playlist_utilities_bar_visible_toggled(self, checkmenuitem):
        """
            Shows or hides the playlist utilities bar
        """
        settings.set_option(
            'gui/playlist_utilities_bar_visible', checkmenuitem.get_active()
        )

    def on_show_playing_track_item_activate(self, menuitem):
        """
            Tries to show the currently playing track
        """
        self.playlist_container.show_current_track()

    def on_about_item_activate(self, menuitem):
        """
            Shows the about dialog
        """
        dialog = dialogs.AboutDialog(self.window)
        dialog.show()

    def on_playback_resume(self, type, player, data):
        self.resuming = True

    def on_playback_start(self, type, player, object):
        """
            Called when playback starts
            Sets the currently playing track visible in the currently selected
            playlist if the user has chosen this setting
        """
        if self.resuming:
            self.resuming = False
            return

        self._update_track_information()
        self.playpause_button.set_image(self.pause_image)
        self.playpause_button.set_tooltip_text(_('Pause Playback'))

    def on_playback_end(self, type, player, object):
        """
            Called when playback ends
        """
        self.window.set_title('Exaile')

        self.playpause_button.set_image(self.play_image)
        self.playpause_button.set_tooltip_text(_('Start Playback'))

    def _on_option_set(self, name, object, option):
        """
           Handles changes of settings
        """
        if option == 'gui/main_window_title_format':
            self.title_formatter.props.format = settings.get_option(
                option, self.title_formatter.props.format
            )

        elif option == 'gui/use_tray':
            usetray = settings.get_option(option, False)
            if self.controller.tray_icon and not usetray:
                self.controller.tray_icon.destroy()
                self.controller.tray_icon = None
            elif not self.controller.tray_icon and usetray:
                self.controller.tray_icon = tray.TrayIcon(self)

        elif option == 'gui/show_info_area':
            self.info_area.set_no_show_all(False)
            if settings.get_option(option, True):
                self.info_area.show_all()
            else:
                self.info_area.hide()
            self.info_area.set_no_show_all(True)

        elif option == 'gui/show_info_area_covers':
            cover = self.info_area.cover
            cover.set_no_show_all(False)
            if settings.get_option(option, True):
                cover.show_all()
            else:
                cover.hide()
            cover.set_no_show_all(True)

        elif option == 'gui/transparency':
            self._update_alpha()

        elif option == 'gui/gtk_dark_hint':
            self._update_dark_hint()

    def _on_volume_key(self, is_up):
        diff = int(
            100 * settings.get_option('gui/volue_key_step_size', VOLUME_STEP_DEFAULT)
        )
        if not is_up:
            diff = -diff

        player.PLAYER.modify_volume(diff)
        return True

    def _on_seek_key(self, is_forward):
        diff = settings.get_option('gui/seek_key_step_size', SEEK_STEP_DEFAULT)
        if not is_forward:
            diff = -diff

        if player.PLAYER.current:
            player.PLAYER.modify_time(diff)
            self.progress_bar.update_progress()

        return True

    def _on_prev_tab_key(self, *e):
        self.playlist_container.get_current_notebook().select_prev_tab()
        return True

    def _on_next_tab_key(self, *e):
        self.playlist_container.get_current_notebook().select_next_tab()
        return True

    def _on_playpause_button(self, *e):
        self.playpause()
        return True

    def _on_focus_playlist_tab(self, tab_nr):
        self.playlist_container.get_current_notebook().focus_tab(tab_nr)
        return True

    def _on_focus_playlist_container(self, *_e):
        self.playlist_container.focus()
        return True

    def _update_track_information(self):
        """
            Sets track information
        """
        track = player.PLAYER.current

        if not track:
            return

        self.window.set_title(self.title_formatter.format(track))

    def playpause(self):
        """
            Pauses the playlist if it is playing, starts playing if it is
            paused. If stopped, try to start playing the next suitable track.
        """
        if player.PLAYER.is_paused() or player.PLAYER.is_playing():
            player.PLAYER.toggle_pause()
        else:
            pl = self.get_selected_page()
            player.QUEUE.set_current_playlist(pl.playlist)
            try:
                trackpath = pl.view.get_selected_paths()[0]
                pl.playlist.current_position = trackpath[0]
            except IndexError:
                pass
            player.QUEUE.play(track=pl.playlist.current)

    def _setup_position(self):
        """
            Sets up the position and sized based on the size the window was
            when it was last moved or resized
        """
        if settings.get_option('gui/mainw_maximized', False):
            self.window.maximize()

        width = settings.get_option('gui/mainw_width', 500)
        height = settings.get_option('gui/mainw_height', 475)
        x = settings.get_option('gui/mainw_x', 10)
        y = settings.get_option('gui/mainw_y', 10)

        self.window.move(x, y)
        self.window.resize(width, height)

        pos = settings.get_option('gui/mainw_sash_pos', 200)
        self.splitter.set_position(pos)

    def on_delete_event(self, *e):
        """
            Called when the user attempts to close the window
        """
        sash_pos = self.splitter.get_position()
        if sash_pos > 10:
            settings.set_option('gui/mainw_sash_pos', sash_pos)

        if settings.get_option('gui/use_tray', False) and settings.get_option(
            'gui/close_to_tray', False
        ):
            self.window.hide()
        else:
            self.quit()
        return True

    def quit(self, *e):
        """
            Quits Exaile
        """
        self.window.hide()
        GLib.idle_add(self.controller.exaile.quit)
        return True

    def on_restart_item_activate(self, menuitem):
        """
            Restarts Exaile
        """
        self.window.hide()
        GLib.idle_add(self.controller.exaile.quit, True)

    def toggle_visible(self, bringtofront=False):
        """
            Toggles visibility of the main window
        """
        toggle_handled = self.emit('main-visible-toggle')

        if not toggle_handled:
            if (
                bringtofront
                and self.window.is_active()
                or not bringtofront
                and self.window.get_property('visible')
            ):
                self.window.hide()
            else:
                # the ordering for deiconify/show matters -- if this gets
                # switched, then the minimization detection breaks
                self.window.deiconify()
                self.window.show()

    def configure_event(self, *e):
        """
            Called when the window is resized or moved
        """
        # Don't save window size if it is maximized or fullscreen.
        if settings.get_option('gui/mainw_maximized', False) or self._fullscreen:
            return False

        (width, height) = self.window.get_size()
        if [width, height] != [
            settings.get_option("gui/mainw_" + key, -1) for key in ["width", "height"]
        ]:
            settings.set_option('gui/mainw_height', height, save=False)
            settings.set_option('gui/mainw_width', width, save=False)
        (x, y) = self.window.get_position()
        if [x, y] != [
            settings.get_option("gui/mainw_" + key, -1) for key in ["x", "y"]
        ]:
            settings.set_option('gui/mainw_x', x, save=False)
            settings.set_option('gui/mainw_y', y, save=False)

        return False

    def window_state_change_event(self, window, event):
        """
            Saves the current maximized and fullscreen
            states and minimizes to tray if requested
        """
        if event.changed_mask & Gdk.WindowState.MAXIMIZED:
            settings.set_option(
                'gui/mainw_maximized',
                bool(event.new_window_state & Gdk.WindowState.MAXIMIZED),
            )
        if event.changed_mask & Gdk.WindowState.FULLSCREEN:
            self._fullscreen = bool(event.new_window_state & Gdk.WindowState.FULLSCREEN)
            self.notify('is-fullscreen')

        # detect minimization state changes
        prev_minimized = self.minimized

        if not self.minimized:

            if (
                event.changed_mask & Gdk.WindowState.ICONIFIED
                and not event.changed_mask & Gdk.WindowState.WITHDRAWN
                and event.new_window_state & Gdk.WindowState.ICONIFIED
                and not event.new_window_state & Gdk.WindowState.WITHDRAWN
                and not self.window_state & Gdk.WindowState.ICONIFIED
            ):
                self.minimized = True
        else:
            if (
                event.changed_mask & Gdk.WindowState.WITHDRAWN
                and not event.new_window_state & (Gdk.WindowState.WITHDRAWN)
            ):  # and \
                self.minimized = False

        # track this
        self.window_state = event.new_window_state

        if settings.get_option('gui/minimize_to_tray', False):

            # old code to detect minimization
            # -> it must have worked at some point, perhaps this is a GTK version
            # specific set of behaviors? Current code works now on 2.24.17

            # if wm_state is not None:
            #    if '_NET_WM_STATE_HIDDEN' in wm_state[2]:
            #        show tray
            #        window.hide
            # else
            #    destroy tray

            if self.minimized != prev_minimized and self.minimized is True:
                if (
                    not settings.get_option('gui/use_tray', False)
                    and self.controller.tray_icon is None
                ):
                    self.controller.tray_icon = tray.TrayIcon(self)

                window.hide()
            elif (
                not settings.get_option('gui/use_tray', False)
                and self.controller.tray_icon is not None
            ):
                self.controller.tray_icon.destroy()
                self.controller.tray_icon = None

        return False

    def get_selected_page(self):
        """
            Returns the currently displayed playlist notebook page
        """
        return self.playlist_container.get_current_tab()

    def get_selected_playlist(self):
        try:
            page = self.get_selected_page()
        except AttributeError:
            return None
        if not isinstance(page, PlaylistPage):
            return None
        return page
Exemple #4
0
class MainWindow(gobject.GObject):
    """
        Main Exaile Window
    """

    __gsignals__ = {"main-visible-toggle": (gobject.SIGNAL_RUN_LAST, bool, ())}
    _mainwindow = None

    def __init__(self, controller, builder, collection):
        """
            Initializes the main window

            @param controller: the main gui controller
        """
        gobject.GObject.__init__(self)

        self.controller = controller
        self.collection = collection
        self.playlist_manager = controller.exaile.playlists
        self.current_page = -1
        self._fullscreen = False
        self.resuming = False

        self.window_state = 0
        self.minimized = False

        self.builder = builder

        self.window = self.builder.get_object("ExaileWindow")
        self.window.set_title("Exaile")
        self.title_formatter = formatter.TrackFormatter(
            settings.get_option("gui/main_window_title_format", _("$title (by $artist)") + " - Exaile")
        )

        self.accelgroup = gtk.AccelGroup()
        self.window.add_accel_group(self.accelgroup)
        self.accel_manager = AcceleratorManager("mainwindow-accelerators", self.accelgroup)
        self.menubar = self.builder.get_object("mainmenu")

        fileitem = self.builder.get_object("file_menu_item")
        filemenu = menu.ProviderMenu("menubar-file-menu", self)
        fileitem.set_submenu(filemenu)

        edititem = self.builder.get_object("edit_menu_item")
        editmenu = menu.ProviderMenu("menubar-edit-menu", self)
        edititem.set_submenu(editmenu)

        viewitem = self.builder.get_object("view_menu_item")
        viewmenu = menu.ProviderMenu("menubar-view-menu", self)
        viewitem.set_submenu(viewmenu)

        toolsitem = self.builder.get_object("tools_menu_item")
        toolsmenu = menu.ProviderMenu("menubar-tools-menu", self)
        toolsitem.set_submenu(toolsmenu)

        helpitem = self.builder.get_object("help_menu_item")
        helpmenu = menu.ProviderMenu("menubar-help-menu", self)
        helpitem.set_submenu(helpmenu)

        self._setup_widgets()
        self._setup_position()
        self._setup_hotkeys()
        logger.info("Connecting main window events...")
        self._connect_events()
        MainWindow._mainwindow = self

        mainmenu._create_menus()

    def _setup_hotkeys(self):
        """
            Sets up accelerators that haven't been set up in UI designer
        """
        hotkeys = (
            ("<Control>S", lambda *e: self.on_save_playlist()),
            ("<Shift><Control>S", lambda *e: self.on_save_playlist_as()),
            ("<Control>F", lambda *e: self.on_panel_filter_focus()),
            ("<Control>G", lambda *e: self.on_search_playlist_focus()),  # FIXME
            ("<Control><Alt>l", lambda *e: player.QUEUE.clear()),  # FIXME
            ("<Control>P", self._on_playpause_button),
            ("<Control>Right", lambda *e: self._on_seek_key(True)),
            ("<Control>Left", lambda *e: self._on_seek_key(False)),
            ("<Control>plus", lambda *e: self._on_volume_key(True)),
            ("<Control>minus", lambda *e: self._on_volume_key(False)),
            ("<Control>Page_Up", self._on_prev_tab_key),
            ("<Control>Page_Down", self._on_next_tab_key),
            ("<Alt>N", self._on_focus_playlist_container),
            # These 4 are subject to change.. probably should do this
            # via a different mechanism too...
            ("<Alt>I", lambda *e: self.controller.focus_panel("files")),
            # ('<Alt>C', lambda *e: self.controller.focus_panel('collection')),
            ("<Alt>R", lambda *e: self.controller.focus_panel("radio")),
            ("<Alt>L", lambda *e: self.controller.focus_panel("playlists")),
            ("<Alt>1", lambda *e: self._on_focus_playlist_tab(0)),
            ("<Alt>2", lambda *e: self._on_focus_playlist_tab(1)),
            ("<Alt>3", lambda *e: self._on_focus_playlist_tab(2)),
            ("<Alt>4", lambda *e: self._on_focus_playlist_tab(3)),
            ("<Alt>5", lambda *e: self._on_focus_playlist_tab(4)),
            ("<Alt>6", lambda *e: self._on_focus_playlist_tab(5)),
            ("<Alt>7", lambda *e: self._on_focus_playlist_tab(6)),
            ("<Alt>8", lambda *e: self._on_focus_playlist_tab(7)),
            ("<Alt>9", lambda *e: self._on_focus_playlist_tab(8)),
            ("<Alt>0", lambda *e: self._on_focus_playlist_tab(9)),
        )

        self.accel_group = gtk.AccelGroup()
        for key, function in hotkeys:
            key, mod = gtk.accelerator_parse(key)
            self.accel_group.connect_group(key, mod, gtk.ACCEL_VISIBLE, function)
        self.window.add_accel_group(self.accel_group)

    def _setup_widgets(self):
        """
            Sets up the various widgets
        """
        # TODO: Maybe make this stackable
        self.message = dialogs.MessageBar(parent=self.builder.get_object("player_box"), buttons=gtk.BUTTONS_CLOSE)
        self.message.connect("response", self.on_messagebar_response)

        self.info_area = MainWindowTrackInfoPane(player.PLAYER)
        self.info_area.set_auto_update(True)
        self.info_area.set_padding(3, 3, 3, 3)
        self.info_area.hide_all()
        self.info_area.set_no_show_all(True)
        guiutil.gtk_widget_replace(self.builder.get_object("info_area"), self.info_area)

        self.volume_control = playback.VolumeControl(player.PLAYER)
        self.info_area.get_action_area().pack_start(self.volume_control)

        if settings.get_option("gui/use_alpha", False):
            screen = self.window.get_screen()
            colormap = screen.get_rgba_colormap()

            if colormap is not None:
                self.window.set_app_paintable(True)
                self.window.set_colormap(colormap)

                self.window.connect("expose-event", self.on_expose_event)
                self.window.connect("screen-changed", self.on_screen_changed)

        playlist_area = self.builder.get_object("playlist_area")
        self.playlist_container = PlaylistContainer("saved_tabs", player.PLAYER)
        for notebook in self.playlist_container.notebooks:
            notebook.connect_after("switch-page", self.on_playlist_container_switch_page)
            page = notebook.get_current_tab()
            if page is not None:
                selection = page.view.get_selection()
                selection.connect("changed", self.on_playlist_view_selection_changed)

        playlist_area.pack_start(self.playlist_container, padding=3)

        self.splitter = self.builder.get_object("splitter")

        self.progress_bar = playback.SeekProgressBar(player.PLAYER)
        guiutil.gtk_widget_replace(self.builder.get_object("playback_progressbar"), self.progress_bar)

        for button in ("playpause", "next", "prev", "stop"):
            setattr(self, "%s_button" % button, self.builder.get_object("%s_button" % button))

        self.stop_button.add_events(gtk.gdk.POINTER_MOTION_MASK)
        self.stop_button.connect("motion-notify-event", self.on_stop_button_motion_notify_event)
        self.stop_button.connect("leave-notify-event", self.on_stop_button_leave_notify_event)
        self.stop_button.connect("key-press-event", self.on_stop_button_key_press_event)
        self.stop_button.connect("key-release-event", self.on_stop_button_key_release_event)
        self.stop_button.connect("focus-out-event", self.on_stop_button_focus_out_event)
        self.stop_button.connect("button-press-event", self.on_stop_button_press_event)
        self.stop_button.connect("button-release-event", self.on_stop_button_release_event)
        self.stop_button.drag_dest_set(
            gtk.DEST_DEFAULT_ALL, [("exaile-index-list", gtk.TARGET_SAME_APP, 0)], gtk.gdk.ACTION_COPY
        )
        self.stop_button.connect("drag-motion", self.on_stop_button_drag_motion)
        self.stop_button.connect("drag-leave", self.on_stop_button_drag_leave)
        self.stop_button.connect("drag-data-received", self.on_stop_button_drag_data_received)

        self.statusbar = info.Statusbar(self.builder.get_object("status_bar"))
        event.add_callback(self.on_exaile_loaded, "exaile_loaded")

    def _connect_events(self):
        """
            Connects the various events to their handlers
        """
        self.builder.connect_signals(
            {
                "on_configure_event": self.configure_event,
                "on_window_state_event": self.window_state_change_event,
                "on_delete_event": self.on_delete_event,
                "on_playpause_button_clicked": self._on_playpause_button,
                "on_next_button_clicked": lambda *e: player.QUEUE.next(),
                "on_prev_button_clicked": lambda *e: player.QUEUE.prev(),
                "on_about_item_activate": self.on_about_item_activate,
                # Controller
                #            'on_scan_collection_item_activate': self.controller.on_rescan_collection,
                #            'on_device_manager_item_activate': lambda *e: self.controller.show_devices(),
                #            'on_track_properties_activate':self.controller.on_track_properties,
            }
        )

        event.add_callback(self.on_playback_resume, "playback_player_resume", player.PLAYER)
        event.add_callback(self.on_playback_end, "playback_player_end", player.PLAYER)
        event.add_callback(self.on_playback_end, "playback_error", player.PLAYER)
        event.add_callback(self.on_playback_start, "playback_track_start", player.PLAYER)
        event.add_callback(self.on_toggle_pause, "playback_toggle_pause", player.PLAYER)
        event.add_callback(self.on_tags_parsed, "tags_parsed", player.PLAYER)
        event.add_callback(self.on_track_tags_changed, "track_tags_changed")
        event.add_callback(self.on_buffering, "playback_buffering", player.PLAYER)
        event.add_callback(self.on_playback_error, "playback_error", player.PLAYER)

        event.add_callback(self.on_playlist_tracks_added, "playlist_tracks_added")
        event.add_callback(self.on_playlist_tracks_removed, "playlist_tracks_removed")

        # Settings
        self._on_option_set("gui_option_set", settings, "gui/show_info_area")
        self._on_option_set("gui_option_set", settings, "gui/show_info_area_covers")
        event.add_callback(self._on_option_set, "option_set")

    def _connect_panel_events(self):
        """
            Sets up panel events
        """

        self.panel_box = self.builder.get_object("panel_box")

        # When there's nothing in the notebook, hide it
        self.controller.panel_notebook.connect("page-added", self.on_panel_notebook_add_page)
        self.controller.panel_notebook.connect("page-removed", self.on_panel_notebook_remove_page)

        # panels
        panels = self.controller.panel_notebook.panels

        for panel_name in ("playlists", "radio", "files", "collection"):
            panel = panels[panel_name].panel
            sort = False

            if panel_name in ("files", "collection"):
                sort = True

            panel.connect(
                "append-items",
                lambda panel, items, force_play, sort=sort: self.on_append_items(items, force_play, sort=sort),
            )
            panel.connect(
                "queue-items", lambda panel, items, sort=sort: self.on_append_items(items, queue=True, sort=sort)
            )
            panel.connect(
                "replace-items", lambda panel, items, sort=sort: self.on_append_items(items, replace=True, sort=sort)
            )

        ## Collection Panel
        panel = panels["collection"].panel
        panel.connect("collection-tree-loaded", self.on_collection_tree_loaded)

        ## Playlist Panel
        panel = panels["playlists"].panel
        panel.connect(
            "playlist-selected", lambda panel, playlist: self.playlist_container.create_tab_from_playlist(playlist)
        )

        ## Radio Panel
        panel = panels["radio"].panel
        panel.connect(
            "playlist-selected", lambda panel, playlist: self.playlist_container.create_tab_from_playlist(playlist)
        )

        ## Files Panel
        # panel = panels['files']

    def on_expose_event(self, widget, event):
        """
            Paints the window alpha transparency
        """
        opacity = 1 - settings.get_option("gui/transparency", 0.3)
        context = widget.props.window.cairo_create()
        background = widget.style.bg[gtk.STATE_NORMAL]
        context.set_source_rgba(
            float(background.red) / 256 ** 2,
            float(background.green) / 256 ** 2,
            float(background.blue) / 256 ** 2,
            opacity,
        )
        context.set_operator(cairo.OPERATOR_SOURCE)
        context.paint()

    def on_screen_changed(self, widget, event):
        """
            Updates the colormap on screen change
        """
        screen = widget.get_screen()
        colormap = screen.get_rgba_colormap() or screen.get_rgb_colormap()
        self.window.set_colormap(rgbamap)

    def on_messagebar_response(self, widget, response):
        """
            Hides the messagebar if requested
        """
        if response == gtk.RESPONSE_CLOSE:
            widget.hide()

    def on_panel_notebook_add_page(self, notebook, page, page_num):
        if self.splitter.get_child1() is None:
            self.splitter.pack1(self.panel_box)

    def on_panel_notebook_remove_page(self, notebook, page, page_num):
        if notebook.get_n_pages() == 0:
            self.splitter.remove(self.panel_box)

    def on_stop_button_motion_notify_event(self, widget, event):
        """
            Sets the hover state and shows SPAT icon
        """
        widget.set_data("hovered", True)
        if event.state & gtk.gdk.SHIFT_MASK:
            widget.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
        else:
            widget.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_STOP, gtk.ICON_SIZE_BUTTON))

    def on_stop_button_leave_notify_event(self, widget, event):
        """
            Unsets the hover state and resets the button icon
        """
        widget.set_data("hovered", False)
        if not widget.is_focus() and ~(event.state & gtk.gdk.SHIFT_MASK):
            widget.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_STOP, gtk.ICON_SIZE_BUTTON))

    def on_stop_button_key_press_event(self, widget, event):
        """
            Shows SPAT icon on Shift key press
        """
        if event.keyval in (gtk.keysyms.Shift_L, gtk.keysyms.Shift_R):
            widget.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
            widget.set_data("toggle_spat", True)

        if event.keyval in (gtk.keysyms.space, gtk.keysyms.Return):
            if widget.get_data("toggle_spat"):
                self.on_spat_clicked()
            else:
                player.PLAYER.stop()

    def on_stop_button_key_release_event(self, widget, event):
        """
            Resets the button icon
        """
        if event.keyval in (gtk.keysyms.Shift_L, gtk.keysyms.Shift_R):
            widget.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_STOP, gtk.ICON_SIZE_BUTTON))
            widget.set_data("toggle_spat", False)

    def on_stop_button_focus_out_event(self, widget, event):
        """
            Resets the button icon unless
            the button is still hovered
        """
        if not widget.get_data("hovered"):
            widget.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_STOP, gtk.ICON_SIZE_BUTTON))

    def on_stop_button_press_event(self, widget, event):
        """
            Called when the user clicks on the stop button
        """
        if event.button == 1:
            if event.state & gtk.gdk.SHIFT_MASK:
                self.on_spat_clicked()
        elif event.button == 3:
            menu = guiutil.Menu()
            menu.append(_("Toggle: Stop after Selected Track"), self.on_spat_clicked, gtk.STOCK_STOP)
            menu.popup(None, None, None, event.button, event.time)

    def on_stop_button_release_event(self, widget, event):
        """
            Called when the user releases the mouse from the stop button
        """
        rect = widget.get_allocation()
        if 0 <= event.x < rect.width and 0 <= event.y < rect.height:
            player.PLAYER.stop()

    def on_stop_button_drag_motion(self, widget, context, x, y, time):
        """
            Indicates possible SPAT during drag motion of tracks
        """
        target = widget.drag_dest_find_target(context, widget.drag_dest_get_target_list())
        if target == "exaile-index-list":
            widget.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))

    def on_stop_button_drag_leave(self, widget, context, time):
        """
            Resets the stop button
        """
        widget.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_STOP, gtk.ICON_SIZE_BUTTON))

    def on_stop_button_drag_data_received(self, widget, context, x, y, selection, info, time):
        """
            Allows for triggering the SPAT feature
            by dropping tracks on the stop button
        """
        source_widget = context.get_source_widget()

        if selection.target == "exaile-index-list" and isinstance(source_widget, PlaylistView):
            position = int(selection.data.split(",")[0])

            if position == source_widget.playlist.spat_position:
                position = -1

            source_widget.playlist.spat_position = position
            source_widget.queue_draw()

    def on_spat_clicked(self, *e):
        """
            Called when the user clicks on the SPAT item
        """
        trs = self.get_selected_page().view.get_selected_items()
        if not trs:
            return

        # TODO: this works, but implement this some other way in the future
        if player.QUEUE.current_playlist.spat_position == -1:
            player.QUEUE.current_playlist.spat_position = trs[0][0]
        else:
            player.QUEUE.current_playlist.spat_position = -1

        self.get_selected_page().view.queue_draw()

    def on_append_items(self, tracks, force_play=False, queue=False, sort=False, replace=False):
        """
            Called when a panel (or other component)
            has tracks to append and possibly queue

            :param tracks: The tracks to append
            :param force_play: Force playing the first track if there
                                is no track currently playing. Otherwise
                                check a setting to determine whether the
                                track should be played
            :param queue: Additionally queue tracks
            :param sort: Sort before adding
            :param replace: Clear playlist before adding
        """
        if len(tracks) == 0:
            return

        page = self.get_selected_page()

        if sort:
            tracks = trax.sort_tracks(("artist", "date", "album", "discnumber", "tracknumber"), tracks)

        if replace:
            page.playlist.clear()

        offset = len(page.playlist)
        page.playlist.extend(tracks)

        # extending the queue automatically starts playback
        if queue:
            if player.QUEUE is not page.playlist:
                player.QUEUE.extend(tracks)

        elif (
            force_play or settings.get_option("playlist/append_menu_starts_playback", False)
        ) and not player.PLAYER.current:
            page.view.play_track_at(offset, tracks[0])

    def on_playback_error(self, type, player, message):
        """
            Called when there has been a playback error
        """
        glib.idle_add(self.message.show_error, _("Playback error encountered!"), message)

    def on_buffering(self, type, player, percent):
        """
            Called when a stream is buffering
        """
        percent = min(percent, 100)
        glib.idle_add(self.statusbar.set_status, _("Buffering: %d%%...") % percent, 1)

    def on_tags_parsed(self, type, player, args):
        """
            Called when tags are parsed from a stream/track
        """
        (tr, args) = args
        if not tr or tr.is_local():
            return
        if player.parse_stream_tags(tr, args):
            self._update_track_information()

    def on_track_tags_changed(self, type, track, tag):
        """
            Called when tags are changed
        """
        if track is player.PLAYER.current:
            self._update_track_information()

    def on_collection_tree_loaded(self, tree):
        """
            Updates information on collection tree load
        """
        self.statusbar.update_info()

    def on_exaile_loaded(self, event_type, exaile, nothing):
        """
            Updates information on exaile load
        """
        glib.idle_add(self.statusbar.update_info)
        event.remove_callback(self.on_exaile_loaded, "exaile_loaded")

    def on_playlist_tracks_added(self, type, playlist, tracks):
        """
            Updates information on track add
        """
        glib.idle_add(self.statusbar.update_info)

    def on_playlist_tracks_removed(self, type, playlist, tracks):
        """
            Updates information on track removal
        """
        glib.idle_add(self.statusbar.update_info)

    def on_toggle_pause(self, type, player, object):
        """
            Called when the user clicks the play button after playback has
            already begun
        """
        if player.is_paused():
            image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_SMALL_TOOLBAR)
            tooltip = _("Continue Playback")
        else:
            image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_SMALL_TOOLBAR)
            tooltip = _("Pause Playback")

        glib.idle_add(self.playpause_button.set_image, image)
        glib.idle_add(self.playpause_button.set_tooltip_text, tooltip)
        self._update_track_information()

        # refresh the current playlist
        pl = self.get_selected_page()

    def on_playlist_container_switch_page(self, notebook, page, page_num):
        """
            Updates info after notebook page switch
        """
        page = notebook.get_nth_page(page_num)
        selection = page.view.get_selection()
        selection.connect("changed", self.on_playlist_view_selection_changed)
        self.statusbar.update_info()

    def on_playlist_view_selection_changed(self, selection):
        """
            Updates info after playlist page selection change
        """
        self.statusbar.update_info()

    def on_panel_filter_focus(self, *e):
        """
            Gives focus to the filter field of the current panel
        """
        try:
            self.controller.get_active_panel().filter.grab_focus()
        except (AttributeError, KeyError):
            pass

    def on_search_playlist_focus(self, *e):
        """
            Gives focus to the playlist search bar
        """
        plpage = get_selected_playlist()
        if plpage:
            plpage.get_search_entry().grab_focus()

    def on_save_playlist(self, *e):
        """
            Called when the user presses Ctrl+S
            Spawns the save dialog of the currently selected playlist tab if
            not custom, saves changes directly if custom
        """
        tab = self.get_selected_tab()
        if not tab:
            return
        if tab.page.playlist.get_is_custom():
            tab.do_save_changes_to_custom()
        else:
            tab.do_save_custom()

    def on_save_playlist_as(self, *e):
        """
            Called when the user presses Ctrl+S
            Spawns the save as dialog of the current playlist tab
        """
        tab = self.get_selected_tab()
        if not tab:
            return
        tab.do_save_custom()

    def on_clear_playlist(self, *e):
        """
            Clears the current playlist tab
        """
        page = self.get_selected_page()
        if page:
            page.playlist.clear()

    def on_open_item_activate(self, menuitem):
        """
            Shows a dialog to open media
        """

        def on_uris_selected(dialog, uris):
            uris.reverse()

            if len(uris) > 0:
                self.controller.open_uri(uris.pop(), play=True)

            for uri in uris:
                self.controller.open_uri(uri, play=False)

        dialog = dialogs.MediaOpenDialog(self.window)
        dialog.connect("uris-selected", on_uris_selected)
        dialog.show()

    def on_open_url_item_activate(self, menuitem):
        """
            Shows a dialog to open an URI
        """

        def on_uri_selected(dialog, uri):
            self.controller.open_uri(uri, play=False)

        dialog = dialogs.URIOpenDialog(self.window)
        dialog.connect("uri-selected", on_uri_selected)
        dialog.show()

    def on_open_directories_item_activate(self, menuitem):
        """
            Shows a dialog to open directories
        """

        def on_uris_selected(dialog, uris):
            uris.reverse()

            if len(uris) > 0:
                self.controller.open_uri(uris.pop(), play=True)

            for uri in uris:
                self.controller.open_uri(uri, play=False)

        dialog = dialogs.DirectoryOpenDialog(self.window)
        # Selecting empty folders is useless
        dialog.props.create_folders = False
        dialog.connect("uris-selected", on_uris_selected)
        dialog.show()

    def on_export_current_playlist_activate(self, menuitem):
        """
            Shows a dialog to export the current playlist
        """
        page = self.get_selected_page()

        if not page or not isinstance(page, PlaylistPage):
            return

        def on_message(dialog, message_type, message):
            """
                Show messages in the main window message area
            """
            if message_type == gtk.MESSAGE_INFO:
                self.message.show_info(markup=message)
            elif message_type == gtk.MESSAGE_ERROR:
                self.message.show_error(_("Playlist export failed!"), message)

            return True

        dialog = dialogs.PlaylistExportDialog(page.playlist, self.window)
        dialog.connect("message", on_message)
        dialog.show()

    def on_playlist_utilities_bar_visible_toggled(self, checkmenuitem):
        """
            Shows or hides the playlist utilities bar
        """
        settings.set_option("gui/playlist_utilities_bar_visible", checkmenuitem.get_active())

    def on_show_playing_track_item_activate(self, menuitem):
        """
            Tries to show the currently playing track
        """
        self.playlist_container.show_current_track()

    def on_about_item_activate(self, menuitem):
        """
            Shows the about dialog
        """
        dialog = dialogs.AboutDialog(self.window)
        dialog.show()

    def on_playback_resume(self, type, player, data):
        self.resuming = True

    def on_playback_start(self, type, player, object):
        """
            Called when playback starts
            Sets the currently playing track visible in the currently selected
            playlist if the user has chosen this setting
        """
        if self.resuming:
            self.resuming = False
            return

        self._update_track_information()
        glib.idle_add(
            self.playpause_button.set_image,
            gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_SMALL_TOOLBAR),
        )
        glib.idle_add(self.playpause_button.set_tooltip_text, _("Pause Playback"))

    def on_playback_end(self, type, player, object):
        """
            Called when playback ends
        """
        glib.idle_add(self.window.set_title, "Exaile")

        glib.idle_add(
            self.playpause_button.set_image, gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_SMALL_TOOLBAR)
        )
        glib.idle_add(self.playpause_button.set_tooltip_text, _("Start Playback"))

    def _on_option_set(self, name, object, option):
        """
           Handles changes of settings
        """
        if option == "gui/main_window_title_format":
            self.title_formatter.props.format = settings.get_option(option, self.title_formatter.props.format)

        if option == "gui/use_tray":
            usetray = settings.get_option(option, False)
            if self.controller.tray_icon and not usetray:
                glib.idle_add(self.controller.tray_icon.destroy)
                self.controller.tray_icon = None
            elif not self.controller.tray_icon and usetray:
                self.controller.tray_icon = tray.TrayIcon(self)

        if option == "gui/show_info_area":
            glib.idle_add(self.info_area.set_no_show_all, False)
            if settings.get_option(option, True):
                glib.idle_add(self.info_area.show_all)
            else:
                glib.idle_add(self.info_area.hide_all)
            glib.idle_add(self.info_area.set_no_show_all, True)

        if option == "gui/show_info_area_covers":

            def _setup_info_covers():
                cover = self.info_area.cover
                cover.set_no_show_all(False)
                if settings.get_option(option, True):
                    cover.show_all()
                else:
                    cover.hide_all()
                cover.set_no_show_all(True)

            glib.idle_add(_setup_info_covers)

    def _on_volume_key(self, is_up):
        diff = int(100 * settings.get_option("gui/volue_key_step_size", VOLUME_STEP_DEFAULT))
        if not is_up:
            diff = -diff

        player.PLAYER.modify_volume(diff)
        return True

    def _on_seek_key(self, is_forward):
        diff = settings.get_option("gui/seek_key_step_size", SEEK_STEP_DEFAULT)
        if not is_forward:
            diff = -diff

        if player.PLAYER.current:
            player.PLAYER.modify_time(diff)
            self.progress_bar.update_progress()

        return True

    def _on_prev_tab_key(self, *e):
        self.playlist_container.get_current_notebook().select_prev_tab()
        return True

    def _on_next_tab_key(self, *e):
        self.playlist_container.get_current_notebook().select_next_tab()
        return True

    def _on_playpause_button(self, *e):
        self.playpause()
        return True

    def _on_focus_playlist_tab(self, tab_nr):
        self.playlist_container.get_current_notebook().focus_tab(tab_nr)
        return True

    def _on_focus_playlist_container(self, *_e):
        self.playlist_container.focus()
        return True

    def _update_track_information(self):
        """
            Sets track information
        """
        track = player.PLAYER.current

        if not track:
            return

        glib.idle_add(self.window.set_title, self.title_formatter.format(track))

    def playpause(self):
        """
            Pauses the playlist if it is playing, starts playing if it is
            paused. If stopped, try to start playing the next suitable track.
        """
        if player.PLAYER.is_paused() or player.PLAYER.is_playing():
            player.PLAYER.toggle_pause()
        else:
            pl = self.get_selected_page()
            player.QUEUE.set_current_playlist(pl.playlist)
            try:
                trackpath = pl.view.get_selected_paths()[0]
                pl.playlist.current_position = trackpath[0]
            except IndexError:
                pass
            player.QUEUE.play(track=pl.playlist.current)

    def _setup_position(self):
        """
            Sets up the position and sized based on the size the window was
            when it was last moved or resized
        """
        if settings.get_option("gui/mainw_maximized", False):
            self.window.maximize()

        width = settings.get_option("gui/mainw_width", 500)
        height = settings.get_option("gui/mainw_height", 475)
        x = settings.get_option("gui/mainw_x", 10)
        y = settings.get_option("gui/mainw_y", 10)

        self.window.move(x, y)
        self.window.resize(width, height)

        pos = settings.get_option("gui/mainw_sash_pos", 200)
        self.splitter.set_position(pos)

    def on_delete_event(self, *e):
        """
            Called when the user attempts to close the window
        """
        sash_pos = self.splitter.get_position()
        if sash_pos > 10:
            settings.set_option("gui/mainw_sash_pos", sash_pos)

        if settings.get_option("gui/use_tray", False) and settings.get_option("gui/close_to_tray", False):
            self.window.hide()
        else:
            self.quit()
        return True

    def quit(self, *e):
        """
            Quits Exaile
        """
        self.window.hide()
        glib.idle_add(self.controller.exaile.quit)
        return True

    def on_restart_item_activate(self, menuitem):
        """
            Restarts Exaile
        """
        self.window.hide()
        glib.idle_add(self.controller.exaile.quit, True)

    def toggle_visible(self, bringtofront=False):
        """
            Toggles visibility of the main window
        """
        toggle_handled = self.emit("main-visible-toggle")

        if not toggle_handled:
            if bringtofront and self.window.is_active() or not bringtofront and self.window.get_property("visible"):
                self.window.hide()
            else:
                # the ordering for deiconify/show matters -- if this gets
                # switched, then the minimization detection breaks
                self.window.deiconify()
                self.window.show()

    def configure_event(self, *e):
        """
            Called when the window is resized or moved
        """
        # Don't save window size if it is maximized or fullscreen.
        if settings.get_option("gui/mainw_maximized", False) or self._fullscreen:
            return False

        (width, height) = self.window.get_size()
        if [width, height] != [settings.get_option("gui/mainw_" + key, -1) for key in ["width", "height"]]:
            settings.set_option("gui/mainw_height", height)
            settings.set_option("gui/mainw_width", width)
        (x, y) = self.window.get_position()
        if [x, y] != [settings.get_option("gui/mainw_" + key, -1) for key in ["x", "y"]]:
            settings.set_option("gui/mainw_x", x)
            settings.set_option("gui/mainw_y", y)

        return False

    def window_state_change_event(self, window, event):
        """
            Saves the current maximized and fullscreen
            states and minimizes to tray if requested
        """
        if event.changed_mask & gtk.gdk.WINDOW_STATE_MAXIMIZED:
            settings.set_option("gui/mainw_maximized", bool(event.new_window_state & gtk.gdk.WINDOW_STATE_MAXIMIZED))
        if event.changed_mask & gtk.gdk.WINDOW_STATE_FULLSCREEN:
            self._fullscreen = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN)

        # detect minimization state changes
        prev_minimized = self.minimized

        if not self.minimized:

            if (
                event.changed_mask & gtk.gdk.WINDOW_STATE_ICONIFIED
                and not event.changed_mask & gtk.gdk.WINDOW_STATE_WITHDRAWN
                and event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED
                and not event.new_window_state & gtk.gdk.WINDOW_STATE_WITHDRAWN
                and not self.window_state & gtk.gdk.WINDOW_STATE_ICONIFIED
            ):

                self.minimized = True
        else:
            if event.changed_mask & gtk.gdk.WINDOW_STATE_WITHDRAWN and not event.new_window_state & (
                gtk.gdk.WINDOW_STATE_WITHDRAWN
            ):  # and \

                self.minimized = False

        # track this
        self.window_state = event.new_window_state

        if settings.get_option("gui/minimize_to_tray", False):

            # old code to detect minimization
            # -> it must have worked at some point, perhaps this is a GTK version
            # specific set of behaviors? Current code works now on 2.24.17

            # if wm_state is not None:
            #    if '_NET_WM_STATE_HIDDEN' in wm_state[2]:
            #        show tray
            #        window.hide
            # else
            #    destroy tray

            if self.minimized != prev_minimized and self.minimized == True:
                if not settings.get_option("gui/use_tray", False) and self.controller.tray_icon is None:
                    self.controller.tray_icon = tray.TrayIcon(self)

                window.hide()
            elif window.window.property_get("_NET_WM_STATE") is None:
                if not settings.get_option("gui/use_tray", False) and self.controller.tray_icon is not None:
                    self.controller.tray_icon.destroy()
                    self.controller.tray_icon = None

        return False

    def get_selected_page(self):
        """
            Returns the currentry displayed playlist notebook page
        """
        return self.playlist_container.get_current_tab()

    def get_selected_playlist(self):
        try:
            page = self.get_selected_page()
        except AttributeError:
            return None
        if not isinstance(page, PlaylistPage):
            return None
        return page