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