1# Copyright (C) 2008-2010 Adam Olsen
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2, or (at your option)
6# any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
16#
17#
18# The developers of the Exaile media player hereby grant permission
19# for non-GPL compatible GStreamer and Exaile plugins to be used and
20# distributed together with GStreamer and Exaile. This permission is
21# above and beyond the permissions granted by the GPL license by which
22# Exaile is covered. If you modify this code, you may extend this
23# exception to your version of the code, but you are not obligated to
24# do so. If you do not wish to do so, delete this exception statement
25# from your version.
26
27import logging
28
29from gi.repository import Gdk
30from gi.repository import Gtk
31
32from xl import event, player, providers, settings
33from xl.nls import gettext as _
34from xlgui.widgets.info import TrackToolTip
35from xlgui.widgets import menu, menuitems, playlist, playback
36from xlgui import guiutil
37
38logger = logging.getLogger(__name__)
39
40
41def is_supported():
42    """
43    On some platforms (e.g. Linux+Wayland) tray icons are not supported.
44    """
45    supported = not guiutil.platform_is_wayland()
46
47    if not supported:
48        logger.debug("No tray icon support on this platform")
49
50    return supported
51
52
53def __create_tray_context_menu():
54    sep = menu.simple_separator
55    items = []
56    # Play/Pause
57    items.append(
58        playback.PlayPauseMenuItem('playback-playpause', player.PLAYER, after=[])
59    )
60    # Next
61    items.append(
62        playback.NextMenuItem('playback-next', player.PLAYER, after=[items[-1].name])
63    )
64    # Prev
65    items.append(
66        playback.PrevMenuItem('playback-prev', player.PLAYER, after=[items[-1].name])
67    )
68    # Stop
69    items.append(
70        playback.StopMenuItem('playback-stop', player.PLAYER, after=[items[-1].name])
71    )
72    # ----
73    items.append(sep('playback-sep', [items[-1].name]))
74    # Shuffle
75    items.append(
76        playlist.ShuffleModesMenuItem('playlist-mode-shuffle', after=[items[-1].name])
77    )
78    # Repeat
79    items.append(
80        playlist.RepeatModesMenuItem('playlist-mode-repeat', after=[items[-1].name])
81    )
82    # Dynamic
83    items.append(
84        playlist.DynamicModesMenuItem('playlist-mode-dynamic', after=[items[-1].name])
85    )
86    # ----
87    items.append(sep('playlist-mode-sep', [items[-1].name]))
88    # Rating
89
90    def rating_get_tracks_func(parent, context):
91        current = player.PLAYER.current
92        if current:
93            return [current]
94        else:
95            return []
96
97    items.append(
98        menuitems.RatingMenuItem('rating', [items[-1].name], rating_get_tracks_func)
99    )
100    # Remove
101    items.append(playlist.RemoveCurrentMenuItem([items[-1].name]))
102    # ----
103    items.append(sep('misc-actions-sep', [items[-1].name]))
104    # Quit
105
106    def quit_cb(*args):
107        from xl import main
108
109        main.exaile().quit()
110
111    items.append(
112        menu.simple_menu_item(
113            'quit-application',
114            [items[-1].name],
115            _("_Quit Exaile"),
116            'application-exit',
117            callback=quit_cb,
118        )
119    )
120    for item in items:
121        providers.register('tray-icon-context', item)
122
123
124__create_tray_context_menu()
125
126
127class BaseTrayIcon:
128    """
129    Trayicon base, needs to be derived from
130    """
131
132    def __init__(self, main):
133        self.main = main
134        self.VOLUME_STEP = 0.05
135
136        self.tooltip = TrackToolTip(self, player.PLAYER)
137        self.tooltip.set_auto_update(True)
138        self.tooltip.set_display_progress(True)
139
140        self.menu = menu.ProviderMenu('tray-icon-context', self)
141        self.update_icon()
142        self.connect_events()
143        event.log_event('tray_icon_toggled', self, True)
144
145    def destroy(self):
146        """
147        Unhides the window and removes the tray icon
148        """
149        # FIXME: Allow other windows too
150        if not self.main.window.get_property('visible'):
151            self.main.window.deiconify()
152            self.main.window.present()
153
154        self.disconnect_events()
155        self.set_visible(False)
156        self.tooltip.destroy()
157        event.log_event('tray_icon_toggled', self, False)
158
159    def connect_events(self):
160        """
161        Connects various callbacks with events
162        """
163        self.connect('button-press-event', self.on_button_press_event)
164        self.connect('scroll-event', self.on_scroll_event)
165
166        event.add_ui_callback(
167            self.on_playback_change_state, 'playback_player_end', player.PLAYER
168        )
169        event.add_ui_callback(
170            self.on_playback_change_state, 'playback_track_start', player.PLAYER
171        )
172        event.add_ui_callback(
173            self.on_playback_change_state, 'playback_toggle_pause', player.PLAYER
174        )
175        event.add_ui_callback(
176            self.on_playback_change_state, 'playback_error', player.PLAYER
177        )
178
179    def disconnect_events(self):
180        """
181        Disconnects various callbacks from events
182        """
183        event.remove_callback(
184            self.on_playback_change_state, 'playback_player_end', player.PLAYER
185        )
186        event.remove_callback(
187            self.on_playback_change_state, 'playback_track_start', player.PLAYER
188        )
189        event.remove_callback(
190            self.on_playback_change_state, 'playback_toggle_pause', player.PLAYER
191        )
192        event.remove_callback(
193            self.on_playback_change_state, 'playback_error', player.PLAYER
194        )
195
196    def update_icon(self):
197        """
198        Updates icon appearance based
199        on current playback state
200        """
201        if player.PLAYER.current is None:
202            self.set_from_icon_name('exaile')
203            self.set_tooltip(_('Exaile Music Player'))
204        elif player.PLAYER.is_paused():
205            self.set_from_icon_name('exaile-pause')
206        else:
207            self.set_from_icon_name('exaile-play')
208
209    def set_from_icon_name(self, icon_name):
210        """
211        Updates the tray icon
212        """
213        pass
214
215    def set_tooltip(self, tooltip_text):
216        """
217        Updates the tray icon tooltip
218        """
219        pass
220
221    def set_visible(self, visible):
222        """
223        Shows or hides the tray icon
224        """
225        pass
226
227    def on_button_press_event(self, widget, event):
228        """
229        Toggles main window visibility and
230        pause as well as opens the context menu
231        """
232        if event.button == Gdk.BUTTON_PRIMARY:
233            self.main.toggle_visible(bringtofront=True)
234        if event.button == Gdk.BUTTON_MIDDLE:
235            playback.playpause(player.PLAYER)
236        if event.triggers_context_menu():
237            self.menu.popup_at_pointer(event)
238
239    def on_scroll_event(self, widget, event):
240        """
241        Changes volume and skips tracks on scroll
242        """
243        if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
244            if event.direction == Gdk.ScrollDirection.UP:
245                player.QUEUE.prev()
246            elif event.direction == Gdk.ScrollDirection.DOWN:
247                player.QUEUE.next()
248        else:
249            if event.direction == Gdk.ScrollDirection.UP:
250                volume = settings.get_option('player/volume', 1)
251                settings.set_option('player/volume', min(volume + self.VOLUME_STEP, 1))
252                return True
253            elif event.direction == Gdk.ScrollDirection.DOWN:
254                volume = settings.get_option('player/volume', 1)
255                settings.set_option('player/volume', max(0, volume - self.VOLUME_STEP))
256                return True
257            elif event.direction == Gdk.ScrollDirection.LEFT:
258                player.QUEUE.prev()
259            elif event.direction == Gdk.ScrollDirection.RIGHT:
260                player.QUEUE.next()
261
262    def on_playback_change_state(self, event, player, current):
263        """
264        Updates tray icon appearance
265        on playback state change
266        """
267        self.update_icon()
268
269
270class TrayIcon(Gtk.StatusIcon, BaseTrayIcon):
271    """
272    Wrapper around GtkStatusIcon
273    """
274
275    def __init__(self, main):
276        Gtk.StatusIcon.__init__(self)
277        BaseTrayIcon.__init__(self, main)
278
279    def set_tooltip(self, tooltip_text):
280        """
281        Updates the tray icon tooltip
282        """
283        self.set_tooltip_text(tooltip_text)
284