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