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
27__all__ = ['main', 'panel', 'playlist']
28
29from gi.repository import Gdk
30from gi.repository import GLib
31from gi.repository import Gtk
32
33import logging
34
35logger = logging.getLogger(__name__)
36
37import os
38import sys
39
40from xl import common, player, providers, settings, version, xdg
41from xl.nls import gettext as _
42from xlgui import guiutil
43
44version.register(
45    "GTK+", "%s.%s.%s" % (Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION, Gtk.MICRO_VERSION)
46)
47version.register("GTK+ theme", Gtk.Settings.get_default().props.gtk_theme_name)
48
49
50def get_controller():
51    return Main._main
52
53
54class Main:
55    """
56    This is the main gui controller for exaile
57    """
58
59    _main = None
60
61    def __init__(self, exaile):
62        """
63        Initializes the GUI
64
65        @param exaile: The Exaile instance
66        """
67        from xlgui import icons, main, panels, tray, progress
68
69        Gdk.set_program_class("Exaile")  # For GNOME Shell
70
71        # https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/Developer/Clients/ApplicationProperties/
72        GLib.set_application_name("Exaile")
73        os.environ['PULSE_PROP_media.role'] = 'music'
74
75        self.exaile = exaile
76        self.first_removed = False
77        self.tray_icon = None
78
79        self.builder = Gtk.Builder()
80        self.builder.add_from_file(xdg.get_data_path('ui', 'main.ui'))
81        self.progress_box = self.builder.get_object('progress_box')
82        self.progress_manager = progress.ProgressManager(self.progress_box)
83
84        add_icon = icons.MANAGER.add_icon_name_from_directory
85        images_dir = xdg.get_data_path('images')
86
87        exaile_icon_path = add_icon('exaile', images_dir)
88        Gtk.Window.set_default_icon_name('exaile')
89        if xdg.local_hack:
90            # PulseAudio also attaches the above name to streams. However, if
91            # Exaile is not installed, any app trying to display the icon won't
92            # be able to find it just by name. The following is a hack to tell
93            # PA the icon file path instead of the name; this only works on
94            # some clients, e.g. pavucontrol.
95            os.environ['PULSE_PROP_application.icon_name'] = exaile_icon_path
96
97        for name in (
98            'exaile-pause',
99            'exaile-play',
100            'office-calendar',
101            'extension',
102            'music-library',
103            'artist',
104            'genre',
105        ):
106            add_icon(name, images_dir)
107        for name in ('dynamic', 'repeat', 'shuffle'):
108            add_icon('media-playlist-' + name, images_dir)
109
110        logger.info("Loading main window...")
111        self.main = main.MainWindow(self, self.builder, exaile.collection)
112
113        if self.exaile.options.StartMinimized:
114            self.main.window.iconify()
115
116        self.play_toolbar = self.builder.get_object('play_toolbar')
117
118        panel_notebook = self.builder.get_object('panel_notebook')
119        self.panel_notebook = panels.PanelNotebook(exaile, self)
120
121        self.device_panels = {}
122
123        # add the device panels
124        for device in self.exaile.devices.get_devices():
125            if device.connected:
126                self.add_device_panel(None, None, device)
127
128        logger.info("Connecting panel events...")
129        self.main._connect_panel_events()
130
131        guiutil.gtk_widget_replace(panel_notebook, self.panel_notebook)
132        self.panel_notebook.get_parent().child_set_property(
133            self.panel_notebook, 'shrink', False
134        )
135
136        if settings.get_option('gui/use_tray', False):
137            if tray.is_supported():
138                self.tray_icon = tray.TrayIcon(self.main)
139            else:
140                settings.set_option('gui/use_tray', False)
141                logger.warning(
142                    "Tray icons are not supported on your platform. Disabling tray icon."
143                )
144
145        from xl import event
146
147        event.add_ui_callback(self.add_device_panel, 'device_connected')
148        event.add_ui_callback(self.remove_device_panel, 'device_disconnected')
149        event.add_ui_callback(self.on_gui_loaded, 'gui_loaded')
150
151        logger.info("Done loading main window...")
152        Main._main = self
153
154        if sys.platform == 'darwin':
155            self._setup_osx()
156
157    def open_uris(self, uris, play=True):
158        if len(uris) > 0:
159            self.open_uri(uris[0], play=play)
160
161            for uri in uris[1:]:
162                self.open_uri(uri, play=False)
163
164    def open_uri(self, uri, play=True):
165        """
166        Determines the type of a uri, imports it into a playlist, and
167        starts playing it
168        """
169        from xl import playlist, trax
170
171        if playlist.is_valid_playlist(uri):
172            try:
173                playlist = playlist.import_playlist(uri)
174            except playlist.InvalidPlaylistTypeError:
175                pass
176            else:
177                self.main.playlist_container.create_tab_from_playlist(playlist)
178
179                if play:
180                    player.QUEUE.current_playlist = playlist
181                    player.QUEUE.current_playlist.current_position = 0
182                    player.QUEUE.play(playlist[0])
183        else:
184            page = self.main.get_selected_page()
185            column = page.view.get_sort_column()
186            reverse = False
187            sort_by = common.BASE_SORT_TAGS
188
189            if column:
190                reverse = column.get_sort_order() == Gtk.SortType.DESCENDING
191                sort_by = [column.name] + sort_by
192
193            tracks = trax.get_tracks_from_uri(uri)
194            tracks = trax.sort_tracks(sort_by, tracks, reverse=reverse)
195
196            try:
197                page.playlist.extend(tracks)
198                page.playlist.current_position = len(page.playlist) - len(tracks)
199
200                if play:
201                    player.QUEUE.current_playlist = page.playlist
202                    player.QUEUE.play(tracks[0])
203            # Catch empty directories
204            except IndexError:
205                pass
206
207    def show_cover_manager(self, *e):
208        """
209        Shows the cover manager
210        """
211        from xlgui.cover import CoverManager
212
213        CoverManager(self.main.window, self.exaile.collection)
214
215    def show_preferences(self):
216        """
217        Shows the preferences dialog
218        """
219        from xlgui.preferences import PreferencesDialog
220
221        dialog = PreferencesDialog(self.main.window, self)
222        dialog.run()
223
224    def show_devices(self):
225        from xlgui.devices import ManagerDialog
226
227        dialog = ManagerDialog(self.main.window, self)
228        dialog.run()
229
230    def queue_manager(self, *e):
231        self.main.playlist_container.show_queue()
232
233    def collection_manager(self, *e):
234        """
235        Invokes the collection manager dialog
236        """
237        from xl.collection import Library
238        from xlgui.collection import CollectionManagerDialog
239
240        dialog = CollectionManagerDialog(self.main.window, self.exaile.collection)
241        result = dialog.run()
242        dialog.hide()
243
244        if result == Gtk.ResponseType.APPLY:
245            collection = self.exaile.collection
246            collection.freeze_libraries()
247
248            collection_libraries = sorted(
249                (l.location, l.monitored, l.startup_scan)
250                for l in collection.libraries.values()
251            )
252            new_libraries = sorted(dialog.get_items())
253
254            if collection_libraries != new_libraries:
255                collection_locations = [
256                    location
257                    for location, monitored, startup_scan in collection_libraries
258                ]
259                new_locations = [
260                    location for location, monitored, startup_scan in new_libraries
261                ]
262
263                if collection_locations != new_locations:
264                    for location in new_locations:
265                        if location not in collection_locations:
266                            collection.add_library(Library(location))
267
268                    removals = []
269
270                    for location, library in collection.libraries.items():
271                        if location not in new_locations:
272                            removals.append(library)
273
274                    for removal in removals:
275                        collection.remove_library(removal)
276
277                    self.on_rescan_collection()
278
279                for location, monitored, startup_scan in new_libraries:
280                    collection.libraries[location].monitored = monitored
281                    collection.libraries[location].startup_scan = startup_scan
282
283            collection.thaw_libraries()
284
285        dialog.destroy()
286
287    def on_gui_loaded(self, event, object, nothing):
288
289        # This has to be idle_add so that plugin panels can be configured
290        GLib.idle_add(self.panel_notebook.on_gui_loaded)
291
292        # Fix track info not displaying properly when resuming after a restart.
293        self.main._update_track_information()
294
295    def on_rescan_collection(self, *e):
296        """
297        Called when the user wishes to rescan the collection
298        """
299        self.rescan_collection_with_progress()
300
301    def on_rescan_collection_forced(self, *e):
302        """
303        Called when the user wishes to rescan the collection slowly
304        """
305        self.rescan_collection_with_progress(force_update=True)
306
307    def rescan_collection_with_progress(self, startup=False, force_update=False):
308
309        libraries = self.exaile.collection.get_libraries()
310        if not self.exaile.collection._scanning and len(libraries) > 0:
311            from xl.collection import CollectionScanThread
312
313            thread = CollectionScanThread(
314                self.exaile.collection, startup_scan=startup, force_update=force_update
315            )
316            thread.connect('done', self.on_rescan_done)
317            self.progress_manager.add_monitor(
318                thread, _("Scanning collection..."), 'drive-harddisk'
319            )
320
321    def on_rescan_done(self, thread):
322        """
323        Called when the rescan has finished
324        """
325        GLib.idle_add(self.get_panel('collection').load_tree)
326
327    def on_track_properties(self, *e):
328        pl = self.main.get_selected_page()
329        pl.view.show_properties_dialog()
330
331    def get_active_panel(self):
332        """
333        Returns the provider object associated with the currently shown
334        panel in the sidebar. May return None.
335        """
336        return self.panel_notebook.get_active_panel()
337
338    def focus_panel(self, panel_name):
339        """
340        Focuses on a panel in the sidebar
341        """
342        self.panel_notebook.focus_panel(panel_name)
343
344    def get_panel(self, panel_name):
345        """
346        Returns the provider object associated with a panel in the sidebar
347        """
348        return self.panel_notebook.panels[panel_name].panel
349
350    def quit(self):
351        """
352        Quits the gui, saving anything that needs to be saved
353        """
354
355        # save open tabs
356        self.main.playlist_container.save_current_tabs()
357
358    def add_device_panel(self, type, obj, device):
359        from xl.collection import CollectionScanThread
360        from xlgui.panel.device import DevicePanel, FlatPlaylistDevicePanel
361        import xlgui.panel
362
363        paneltype = DevicePanel
364        if hasattr(device, 'panel_type'):
365            if device.panel_type == 'flatplaylist':
366                paneltype = FlatPlaylistDevicePanel
367            elif issubclass(device.panel_type, xlgui.panel.Panel):
368                paneltype = device.panel_type
369
370        panel = paneltype(self.main.window, self.main, device, device.get_name())
371
372        do_sort = True
373        panel.connect(
374            'append-items',
375            lambda _panel, items, play: self.main.on_append_items(
376                items, play, sort=do_sort
377            ),
378        )
379        panel.connect(
380            'queue-items',
381            lambda _panel, items: self.main.on_append_items(
382                items, queue=True, sort=do_sort
383            ),
384        )
385        panel.connect(
386            'replace-items',
387            lambda _panel, items: self.main.on_append_items(
388                items, replace=True, sort=do_sort
389            ),
390        )
391
392        self.device_panels[device.get_name()] = panel
393        GLib.idle_add(providers.register, 'main-panel', panel)
394        thread = CollectionScanThread(device.get_collection())
395        thread.connect('done', panel.load_tree)
396        self.progress_manager.add_monitor(
397            thread, _("Scanning %s..." % device.name), 'drive-harddisk'
398        )
399
400    def remove_device_panel(self, type, obj, device):
401        try:
402            providers.unregister('main-panel', self.device_panels[device.get_name()])
403        except ValueError:
404            logger.debug("Couldn't remove panel for %s", device.get_name())
405        del self.device_panels[device.get_name()]
406
407    def _setup_osx(self):
408        """
409        Copied from Quod Libet, GPL v2 or later
410        """
411
412        from AppKit import NSObject, NSApplication
413        import objc
414
415        try:
416            import gi
417
418            gi.require_version('GtkosxApplication', '1.0')
419            from gi.repository import GtkosxApplication
420        except (ValueError, ImportError):
421            logger.warning("importing GtkosxApplication failed, no native menus")
422        else:
423            osx_app = GtkosxApplication.Application()
424            # self.main.setup_osx(osx_app)
425            osx_app.ready()
426
427        shared_app = NSApplication.sharedApplication()
428        gtk_delegate = shared_app.delegate()
429
430        other_self = self
431
432        # TODO
433        # Instead of quitting when the main window gets closed just hide it.
434        # If the dock icon gets clicked we get
435        # applicationShouldHandleReopen_hasVisibleWindows_ and show everything.
436        class Delegate(NSObject):
437            @objc.signature('B@:#B')
438            def applicationShouldHandleReopen_hasVisibleWindows_(self, ns_app, flag):
439                logger.debug("osx: handle reopen")
440                # TODO
441                # app.present()
442                return True
443
444            def applicationShouldTerminate_(self, sender):
445                logger.debug("osx: block termination")
446                other_self.main.quit()
447                return False
448
449            def applicationDockMenu_(self, sender):
450                return gtk_delegate.applicationDockMenu_(sender)
451
452            # def application_openFile_(self, sender, filename):
453            #    return app.window.open_file(filename.encode("utf-8"))
454
455        delegate = Delegate.alloc().init()
456        delegate.retain()
457        shared_app.setDelegate_(delegate)
458
459        # QL shouldn't exit on window close, EF should
460        # if window.get_is_persistent():
461        #    window.connect(
462        #        "delete-event", lambda window, event: window.hide() or True)
463