1# Copyright 2006-2009 Scott Horowitz <stonecrest@gmail.com> 2# Copyright 2009-2014 Jonathan Ballet <jon@multani.info> 3# 4# This file is part of Sonata. 5# 6# Sonata is free software: you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation, either version 3 of the License, or 9# (at your option) any later version. 10# 11# Sonata is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with Sonata. If not, see <http://www.gnu.org/licenses/>. 18 19import collections 20import sys 21import gettext 22import locale 23import logging 24import os 25import operator 26import random 27import warnings 28 29import urllib.parse, urllib.request 30import re 31import gc 32import shutil 33import tempfile 34import threading 35 36from gi.repository import Gtk, Gdk, GdkPixbuf, Gio, GLib, Pango 37 38import pkg_resources 39 40import sonata.mpdhelper as mpdh 41 42from sonata import misc, ui, consts, img, tray, formatting 43 44from sonata.pluginsystem import pluginsystem 45from sonata.config import Config 46 47from sonata import preferences, tagedit, \ 48 artwork, about, \ 49 scrobbler, info, \ 50 library, streams, \ 51 playlists, current, \ 52 dbus_plugin as dbus 53from sonata.song import SongRecord 54 55from sonata.version import version 56 57class Base: 58 59 ### XXX Warning, a long __init__ ahead: 60 61 def __init__(self, args): 62 self.logger = logging.getLogger(__name__) 63 64 # The following attributes were used but not defined here before: 65 self.album_current_artist = None 66 67 self.allow_art_search = None 68 self.choose_dialog = None 69 self.chooseimage_visible = None 70 71 self.imagelist = None 72 73 self.iterate_handler = None 74 self.local_dest_filename = None 75 76 self.notification_width = None 77 78 self.remote_albumentry = None 79 self.remote_artistentry = None 80 self.remote_dest_filename = None 81 self.remotefilelist = None 82 self.seekidle = None 83 self.artwork = None 84 85 self.lyrics_search_dialog = None 86 87 self.mpd = mpdh.MPDClient() 88 self.conn = False 89 # Anything != than self.conn, to actually refresh the UI at startup. 90 self.prevconn = not self.conn 91 92 # Constants 93 self.TAB_CURRENT = _("Current") 94 self.TAB_LIBRARY = _("Library") 95 self.TAB_PLAYLISTS = _("Playlists") 96 self.TAB_STREAMS = _("Streams") 97 self.TAB_INFO = _("Info") 98 99 # If the connection to MPD times out, this will cause the interface 100 # to freeze while the socket.connect() calls are repeatedly executed. 101 # Therefore, if we were not able to make a connection, slow down the 102 # iteration check to once every 15 seconds. 103 self.iterate_time_when_connected = 500 104 # Slow down polling when disconnected stopped 105 self.iterate_time_when_disconnected_or_stopped = 1000 106 107 108 self.trying_connection = False 109 110 self.traytips = tray.TrayIconTips() 111 112 # better keep a reference around 113 try: 114 self.dbus_service = dbus.SonataDBus(self.dbus_show, 115 self.dbus_toggle, 116 self.dbus_popup, 117 self.dbus_fullscreen) 118 except Exception: 119 pass 120 121 misc.create_dir('~/.covers/') 122 123 # Initialize vars for GUI 124 self.current_tab = self.TAB_CURRENT 125 126 self.prevstatus = None 127 self.prevsonginfo = None 128 129 self.popuptimes = ['2', '3', '5', '10', '15', '30', _('Entire song')] 130 131 self.exit_now = False 132 self.ignore_toggle_signal = False 133 134 self.user_connect = False 135 136 self.sonata_loaded = False 137 self.call_gc_collect = False 138 139 self.album_reset_artist() 140 141 show_prefs = False 142 self.merge_id = None 143 144 self.actionGroupProfiles = None 145 146 self.skip_on_profiles_click = False 147 self.last_playlist_repeat = None 148 self.last_song_repeat = None 149 self.last_random = None 150 self.last_consume = None 151 self.last_title = None 152 self.last_progress_frac = None 153 self.last_progress_text = None 154 155 self.last_status_text = "" 156 157 self.img_clicked = False 158 159 self.mpd_update_queued = False 160 161 # XXX get rid of all of these: 162 self.all_tab_names = [self.TAB_CURRENT, self.TAB_LIBRARY, 163 self.TAB_PLAYLISTS, self.TAB_STREAMS, 164 self.TAB_INFO] 165 self.all_tab_ids = "current library playlists streams info".split() 166 self.tabname2id = dict(zip(self.all_tab_names, self.all_tab_ids)) 167 self.tabid2name = dict(zip(self.all_tab_ids, self.all_tab_names)) 168 self.tabname2tab = dict() 169 self.tabname2focus = dict() 170 self.plugintabs = dict() 171 172 self.config = Config(_('Default Profile'), _("by %A from %B")) 173 self.preferences = preferences.Preferences(self.config, 174 self.on_connectkey_pressed, self.on_currsong_notify, 175 self.update_infofile, self.settings_save, 176 self.populate_profiles_for_menu) 177 178 self.settings_load() 179 self.setup_prefs_callbacks() 180 181 if args.start_visibility is not None: 182 self.config.withdrawn = not args.start_visibility 183 if self.config.autoconnect: 184 self.user_connect = True 185 args.apply_profile_arg(self.config) 186 187 self.builder = ui.builder('sonata') 188 self.provider = ui.css_provider('sonata') 189 190 icon_factory_src = ui.builder_string('icons') 191 icon_path = pkg_resources.resource_filename(__name__, "pixmaps") 192 icon_factory_src = icon_factory_src.format(base_path=icon_path) 193 self.builder.add_from_string(icon_factory_src) 194 icon_factory = self.builder.get_object('sonata_iconfactory') 195 Gtk.IconFactory.add_default(icon_factory) 196 197 # Main window 198 self.window = self.builder.get_object('main_window') 199 200 self.fullscreen = FullscreenApp(self.config, self.get_fullscreen_info) 201 202 if self.config.ontop: 203 self.window.set_keep_above(True) 204 if self.config.sticky: 205 self.window.stick() 206 if not self.config.decorated: 207 self.window.set_decorated(False) 208 self.preferences.window = self.window 209 210 self.notebook = self.builder.get_object('main_notebook') 211 self.album_image = self.builder.get_object('main_album_image') 212 self.tray_album_image = self.builder.get_object('tray_album_image') 213 214 # Artwork 215 self.artwork = artwork.Artwork( 216 self.config, misc.is_lang_rtl(self.window), 217 self.schedule_gc_collect, 218 self.imagelist_append, self.remotefilelist_append, 219 self.set_allow_art_search, self.status_is_play_or_pause, 220 self.album_image, self.tray_album_image) 221 222 223 # Popup menus: 224 actions = [ 225 ('sortmenu', Gtk.STOCK_SORT_ASCENDING, _('_Sort List')), 226 ('plmenu', Gtk.STOCK_SAVE, _('Sa_ve Selected to')), 227 ('profilesmenu', Gtk.STOCK_CONNECT, _('_Connection')), 228 ('playaftermenu', None, _('P_lay after')), 229 ('playmodemenu', None, _('Play _Mode')), 230 ('updatemenu', Gtk.STOCK_REFRESH, _('_Update')), 231 ('chooseimage_menu', Gtk.STOCK_CONVERT, _('Use _Remote Image...'), 232 None, None, self.image_remote), 233 ('localimage_menu', Gtk.STOCK_OPEN, _('Use _Local Image...'), 234 None, None, self.image_local), 235 ('fullscreen_window_menu', Gtk.STOCK_FULLSCREEN, 236 _('_Fullscreen Mode'), 'F11', None, self.on_fullscreen_change), 237 ('resetimage_menu', Gtk.STOCK_CLEAR, _('Reset Image'), None, None, 238 self.artwork.on_reset_image), 239 ('playmenu', Gtk.STOCK_MEDIA_PLAY, _('_Play'), None, None, 240 self.mpd_pp), 241 ('pausemenu', Gtk.STOCK_MEDIA_PAUSE, _('Pa_use'), None, None, 242 self.mpd_pp), 243 ('stopmenu', Gtk.STOCK_MEDIA_STOP, _('_Stop'), None, None, 244 self.mpd_stop), 245 ('prevmenu', Gtk.STOCK_MEDIA_PREVIOUS, _('Pre_vious'), None, None, 246 self.mpd_prev), 247 ('nextmenu', Gtk.STOCK_MEDIA_NEXT, _('_Next'), None, None, 248 self.mpd_next), 249 ('quitmenu', Gtk.STOCK_QUIT, _('_Quit'), None, None, 250 self.on_delete_event_yes), 251 ('removemenu', Gtk.STOCK_REMOVE, _('_Remove'), None, None, 252 self.on_remove), 253 ('clearmenu', Gtk.STOCK_CLEAR, _('_Clear'), '<Ctrl>Delete', None, 254 self.mpd_clear), 255 ('updatefullmenu', None, _('_Entire Library'), '<Ctrl><Shift>u', 256 None, self.on_updatedb), 257 ('updateselectedmenu', None, _('_Selected Items'), '<Ctrl>u', None, 258 self.on_updatedb_shortcut), 259 ('preferencemenu', Gtk.STOCK_PREFERENCES, _('_Preferences...'), 260 'F5', None, self.on_prefs), 261 ('aboutmenu', Gtk.STOCK_ABOUT, _('_About...'), 'F1', None, self.on_about), 262 ('tagmenu', Gtk.STOCK_EDIT, _('_Edit Tags...'), '<Ctrl>t', None, 263 self.on_tags_edit), 264 ('addmenu', Gtk.STOCK_ADD, _('_Add'), '<Ctrl>d', None, 265 self.on_add_item), 266 ('replacemenu', Gtk.STOCK_REDO, _('_Replace'), '<Ctrl>r', None, 267 self.on_replace_item), 268 ('add2menu', None, _('Add'), '<Shift><Ctrl>d', None, 269 self.on_add_item_play), 270 ('replace2menu', None, _('Replace'), '<Shift><Ctrl>r', None, 271 self.on_replace_item_play), 272 ('rmmenu', None, _('_Delete...'), None, None, self.on_remove), 273 ('sortshuffle', None, _('Shuffle'), '<Alt>r', None, 274 self.mpd_shuffle), 275 ('sortshufflealbums', None, _('Shuffle Albums'), None, None, 276 self.mpd_shuffle_albums), ] 277 278 keyactions = [ 279 ('expandkey', None, 'Expand Key', '<Alt>Down', None, 280 self.on_expand), 281 ('collapsekey', None, 'Collapse Key', '<Alt>Up', None, 282 self.on_collapse), 283 ('ppkey', None, 'Play/Pause Key', '<Ctrl>p', None, self.mpd_pp), 284 ('stopkey', None, 'Stop Key', '<Ctrl>s', None, self.mpd_stop), 285 ('prevkey', None, 'Previous Key', '<Ctrl>Left', None, 286 self.mpd_prev), 287 ('nextkey', None, 'Next Key', '<Ctrl>Right', None, self.mpd_next), 288 ('lowerkey', None, 'Lower Volume Key', '<Ctrl>minus', None, 289 self.on_volume_lower), 290 ('raisekey', None, 'Raise Volume Key', '<Ctrl>plus', None, 291 self.on_volume_raise), 292 ('raisekey2', None, 'Raise Volume Key 2', '<Ctrl>equal', None, 293 self.on_volume_raise), 294 ('quitkey', None, 'Quit Key', '<Ctrl>q', None, 295 self.on_delete_event_yes), 296 ('quitkey2', None, 'Quit Key 2', '<Ctrl>w', None, 297 self.on_delete_event), 298 ('connectkey', None, 'Connect Key', '<Alt>c', None, 299 self.on_connectkey_pressed), 300 ('disconnectkey', None, 'Disconnect Key', '<Alt>d', None, 301 self.on_disconnectkey_pressed), 302 ('searchkey', None, 'Search Key', '<Ctrl>h', None, 303 self.on_library_search_shortcut), 304 ('nexttabkey', None, 'Next Tab Key', '<Alt>Right', None, 305 self.switch_to_next_tab), 306 ('prevtabkey', None, 'Prev Tab Key', '<Alt>Left', None, 307 self.switch_to_prev_tab), ] 308 309 tabactions = [('tab%skey' % i, None, 'Tab%s Key' % i, 310 '<Alt>%s' % i, None, 311 lambda _a, i=i: self.switch_to_tab_num(i-1)) 312 for i in range(1, 10)] 313 314 toggle_actions = [ 315 ('showmenu', None, _('S_how Sonata'), None, None, 316 self.on_withdraw_app_toggle, not self.config.withdrawn), 317 ('repeatplaylistmenu', None, _('_Repeat playlist'), None, None, 318 self.on_repeat_playlist_clicked, False), 319 ('repeatsongmenu', None, _('Repeat s_ong'), None, None, 320 self.on_repeat_song_clicked, False), 321 ('randommenu', None, _('Rando_m'), None, None, 322 self.on_random_clicked, False), 323 ('consumemenu', None, _('Consume'), None, None, 324 self.on_consume_clicked, False), 325 ] 326 327 toggle_tabactions = [ 328 (self.tabname2id[self.TAB_CURRENT], None, self.TAB_CURRENT, 329 None, None, self.on_tab_toggle, self.config.current_tab_visible), 330 (self.tabname2id[self.TAB_LIBRARY], None, self.TAB_LIBRARY, 331 None, None, self.on_tab_toggle, self.config.library_tab_visible), 332 (self.tabname2id[self.TAB_PLAYLISTS], None, self.TAB_PLAYLISTS, 333 None, None, self.on_tab_toggle, self.config.playlists_tab_visible), 334 (self.tabname2id[self.TAB_STREAMS], None, self.TAB_STREAMS, 335 None, None, self.on_tab_toggle, self.config.streams_tab_visible), 336 (self.tabname2id[self.TAB_INFO], None, self.TAB_INFO, 337 None, None, self.on_tab_toggle, self.config.info_tab_visible), ] 338 339 uiDescription = """ 340 <ui> 341 <popup name="imagemenu"> 342 <menuitem action="chooseimage_menu"/> 343 <menuitem action="localimage_menu"/> 344 <menuitem action="fullscreen_window_menu"/> 345 <separator name="FM1"/> 346 <menuitem action="resetimage_menu"/> 347 </popup> 348 <popup name="traymenu"> 349 <menuitem action="showmenu"/> 350 <separator name="FM1"/> 351 <menuitem action="playmenu"/> 352 <menuitem action="pausemenu"/> 353 <menuitem action="stopmenu"/> 354 <menuitem action="prevmenu"/> 355 <menuitem action="nextmenu"/> 356 <separator name="FM2"/> 357 <menu action="playmodemenu"> 358 <menuitem action="repeatplaylistmenu"/> 359 <menuitem action="repeatsongmenu"/> 360 <menuitem action="randommenu"/> 361 <menuitem action="consumemenu"/> 362 </menu> 363 <menuitem action="fullscreen_window_menu"/> 364 <menuitem action="preferencemenu"/> 365 <separator name="FM3"/> 366 <menuitem action="quitmenu"/> 367 </popup> 368 <popup name="mainmenu"> 369 <menuitem action="addmenu"/> 370 <menuitem action="replacemenu"/> 371 <menu action="playaftermenu"> 372 <menuitem action="add2menu"/> 373 <menuitem action="replace2menu"/> 374 </menu> 375 <menuitem action="newmenu"/> 376 <menuitem action="editmenu"/> 377 <menuitem action="removemenu"/> 378 <menuitem action="clearmenu"/> 379 <menuitem action="tagmenu"/> 380 <menuitem action="renamemenu"/> 381 <menuitem action="rmmenu"/> 382 <menu action="sortmenu"> 383 <menuitem action="sortbytitle"/> 384 <menuitem action="sortbyartist"/> 385 <menuitem action="sortbyalbum"/> 386 <menuitem action="sortbyfile"/> 387 <menuitem action="sortbydirfile"/> 388 <separator name="FM3"/> 389 <menuitem action="sortshuffle"/> 390 <menuitem action="sortshufflealbums"/> 391 <menuitem action="sortreverse"/> 392 </menu> 393 <menu action="plmenu"> 394 <menuitem action="savemenu"/> 395 <separator name="FM4"/> 396 </menu> 397 <separator name="FM1"/> 398 <menuitem action="repeatplaylistmenu"/> 399 <menuitem action="repeatsongmenu"/> 400 <menuitem action="randommenu"/> 401 <menuitem action="consumemenu"/> 402 <menu action="updatemenu"> 403 <menuitem action="updateselectedmenu"/> 404 <menuitem action="updatefullmenu"/> 405 </menu> 406 <separator name="FM2"/> 407 <menu action="profilesmenu"> 408 </menu> 409 <menuitem action="preferencemenu"/> 410 <menuitem action="aboutmenu"/> 411 <menuitem action="quitmenu"/> 412 </popup> 413 <popup name="librarymenu"> 414 <menuitem action="filesystemview"/> 415 <menuitem action="artistview"/> 416 <menuitem action="genreview"/> 417 <menuitem action="albumview"/> 418 </popup> 419 <popup name="hidden"> 420 <menuitem action="centerplaylistkey"/> 421 </popup> 422 """ 423 424 uiDescription += '<popup name="notebookmenu">' 425 uiDescription += ''.join('<menuitem action="%s"/>' % name 426 for name in self.all_tab_ids)#FIXME 427 428 uiDescription += "</popup>" 429 430 uiDescription += ''.join('<accelerator action="%s"/>' % a[0] 431 for a in keyactions + tabactions) 432 433 uiDescription += "</ui>\n" 434 435 # Try to connect to MPD: 436 self.mpd_connect() 437 if self.conn: 438 self.status = self.mpd.status() 439 self.iterate_time = self.iterate_time_when_connected 440 self.songinfo = self.mpd.currentsong() 441 self.artwork.update_songinfo(self.songinfo) 442 elif self.config.initial_run: 443 show_prefs = True 444 445 # Realizing self.window will allow us to retrieve the theme's 446 # link-color; we can then apply to it various widgets: 447 try: 448 self.window.realize() 449 linkcolor = \ 450 self.window.style_get_property("link-color").to_string() 451 except: 452 linkcolor = None 453 454 # Audioscrobbler 455 self.scrobbler = scrobbler.Scrobbler(self.config) 456 self.scrobbler.import_module() 457 self.scrobbler.init() 458 self.preferences.scrobbler = self.scrobbler 459 460 # Current tab 461 self.current = current.Current( 462 self.config, self.mpd, self.TAB_CURRENT, 463 self.on_current_button_press, self.connected, 464 lambda: self.sonata_loaded, lambda: self.songinfo, 465 self.update_statusbar, self.iterate_now, self.add_tab) 466 467 self.current_treeview = self.current.get_treeview() 468 self.current_selection = self.current.get_selection() 469 470 currentactions = [ 471 ('centerplaylistkey', None, 'Center Playlist Key', '<Ctrl>i', 472 None, lambda event: self.current.center_song_in_list(True)), 473 ('sortbyartist', None, _('By Artist'), None, None, 474 self.current.on_sort_by_artist), 475 ('sortbyalbum', None, _('By Album'), None, None, 476 self.current.on_sort_by_album), 477 ('sortbytitle', None, _('By Song Title'), None, None, 478 self.current.on_sort_by_title), 479 ('sortbyfile', None, _('By File Name'), None, None, 480 self.current.on_sort_by_file), 481 ('sortbydirfile', None, _('By Dir & File Name'), None, None, 482 self.current.on_sort_by_dirfile), 483 ('sortreverse', None, _('Reverse List'), None, None, 484 self.current.on_sort_reverse), 485 ] 486 487 # Library tab 488 self.library = library.Library( 489 self.config, self.mpd, self.artwork, self.TAB_LIBRARY, 490 self.settings_save, self.current.filter_key_pressed, 491 self.on_add_item, self.connected, self.on_library_button_press, 492 self.add_tab) 493 494 self.library_treeview = self.library.get_treeview() 495 self.library_selection = self.library.get_selection() 496 497 libraryactions = self.library.get_libraryactions() 498 499 # Info tab 500 self.info = info.Info(self.config, linkcolor, self.on_link_click, 501 self.get_playing_song, 502 self.TAB_INFO, self.on_image_activate, 503 self.on_image_motion_cb, self.on_image_drop_cb, 504 self.album_return_artist_and_tracks, 505 self.add_tab) 506 self.artwork.connect('artwork-changed', 507 self.info.on_artwork_changed) 508 self.artwork.connect('artwork-reset', 509 self.info.on_artwork_reset) 510 self.info_imagebox = self.info.get_info_imagebox() 511 512 # Streams tab 513 self.streams = streams.Streams(self.config, self.window, 514 self.on_streams_button_press, 515 self.on_add_item, 516 self.settings_save, 517 self.TAB_STREAMS, 518 self.add_tab) 519 520 self.streams_treeview = self.streams.get_treeview() 521 self.streams_selection = self.streams.get_selection() 522 523 streamsactions = [ 524 ('newmenu', None, _('_New...'), '<Ctrl>n', None, 525 self.streams.on_streams_new), 526 ('editmenu', None, _('_Edit...'), None, None, 527 self.streams.on_streams_edit), ] 528 529 # Playlists tab 530 self.playlists = playlists.Playlists(self.config, self.window, 531 self.mpd, 532 lambda: self.UIManager, 533 self.update_menu_visibility, 534 self.iterate_now, 535 self.on_add_item, 536 self.on_playlists_button_press, 537 self.connected, 538 self.add_selected_to_playlist, 539 self.TAB_PLAYLISTS, 540 self.add_tab) 541 542 self.playlists_treeview = self.playlists.get_treeview() 543 self.playlists_selection = self.playlists.get_selection() 544 545 playlistsactions = [ 546 ('savemenu', None, _('_New Playlist...'), '<Ctrl><Shift>s', None, 547 self.playlists.on_playlist_save), 548 ('renamemenu', None, _('_Rename...'), None, None, 549 self.playlists.on_playlist_rename), 550 ] 551 552 # Main app: 553 self.UIManager = Gtk.UIManager() 554 accel_group = self.UIManager.get_accel_group() 555 self.fullscreen.add_accel_group(accel_group) 556 actionGroup = Gtk.ActionGroup('Actions') 557 actionGroup.add_actions(actions) 558 actionGroup.add_actions(keyactions) 559 actionGroup.add_actions(tabactions) 560 actionGroup.add_actions(currentactions) 561 actionGroup.add_actions(libraryactions) 562 actionGroup.add_actions(streamsactions) 563 actionGroup.add_actions(playlistsactions) 564 actionGroup.add_toggle_actions(toggle_actions) 565 actionGroup.add_toggle_actions(toggle_tabactions) 566 self.UIManager.insert_action_group(actionGroup, 0) 567 self.UIManager.add_ui_from_string(uiDescription) 568 self.populate_profiles_for_menu() 569 self.window.add_accel_group(self.UIManager.get_accel_group()) 570 self.mainmenu = self.UIManager.get_widget('/mainmenu') 571 self.randommenu = self.UIManager.get_widget('/mainmenu/randommenu') 572 self.consumemenu = self.UIManager.get_widget('/mainmenu/consumemenu') 573 self.repeatplaylistmenu = self.UIManager.get_widget('/mainmenu/repeatplaylistmenu') 574 self.repeatsongmenu = self.UIManager.get_widget('/mainmenu/repeatsongmenu') 575 self.imagemenu = self.UIManager.get_widget('/imagemenu') 576 self.traymenu = self.UIManager.get_widget('/traymenu') 577 self.librarymenu = self.UIManager.get_widget('/librarymenu') 578 self.library.set_librarymenu(self.librarymenu) 579 self.notebookmenu = self.UIManager.get_widget('/notebookmenu') 580 581 # Autostart plugins 582 for plugin in pluginsystem.get_info(): 583 if plugin.name in self.config.autostart_plugins: 584 pluginsystem.set_enabled(plugin, True) 585 586 # New plugins 587 for plugin in pluginsystem.get_info(): 588 if plugin.name not in self.config.known_plugins: 589 self.config.known_plugins.append(plugin.name) 590 if plugin.name in consts.DEFAULT_PLUGINS: 591 self.logger.info( 592 _("Enabling new plug-in %s..." % plugin.name)) 593 pluginsystem.set_enabled(plugin, True) 594 else: 595 self.logger.info(_("Found new plug-in %s." % plugin.name)) 596 597 self.tray_icon = tray.TrayIcon(self.window, self.traymenu, self.traytips) 598 599 self.imageeventbox = self.builder.get_object('image_event_box') 600 self.imageeventbox.drag_dest_set(Gtk.DestDefaults.HIGHLIGHT | 601 Gtk.DestDefaults.DROP, 602 [Gtk.TargetEntry.new("text/uri-list", 0, 80), 603 Gtk.TargetEntry.new("text/plain", 0, 80)], 604 Gdk.DragAction.DEFAULT) 605 if not self.config.show_covers: 606 self.imageeventbox.hide() 607 self.prevbutton = self.builder.get_object('prev_button') 608 self.ppbutton = self.builder.get_object('playpause_button') 609 self.ppbutton_image = self.builder.get_object('playpause_button_image') 610 self.stopbutton = self.builder.get_object('stop_button') 611 self.nextbutton = self.builder.get_object('next_button') 612 for mediabutton in (self.prevbutton, self.ppbutton, self.stopbutton, 613 self.nextbutton): 614 if not self.config.show_playback: 615 ui.hide(mediabutton) 616 self.progressbox = self.builder.get_object('progress_box') 617 self.progressbar = self.builder.get_object('progress_bar') 618 619 self.progresseventbox = self.builder.get_object('progress_event_box') 620 if not self.config.show_progress: 621 ui.hide(self.progressbox) 622 self.volumebutton = self.builder.get_object('volume_button') 623 if not self.config.show_playback: 624 ui.hide(self.volumebutton) 625 self.expander = self.builder.get_object('expander') 626 self.expander.set_expanded(self.config.expanded) 627 self.cursonglabel1 = self.builder.get_object('current_label_1') 628 self.cursonglabel2 = self.builder.get_object('current_label_2') 629 expanderbox = self.builder.get_object('expander_label_widget') 630 self.expander.set_label_widget(expanderbox) 631 self.statusbar = self.builder.get_object('main_statusbar') 632 if not self.config.show_statusbar or not self.config.expanded: 633 ui.hide(self.statusbar) 634 self.window.move(self.config.x, self.config.y) 635 self.window.set_size_request(270, -1) 636 songlabel1 = '<big>{}</big>'.format(_('Stopped')) 637 self.cursonglabel1.set_markup(songlabel1) 638 if not self.config.expanded: 639 ui.hide(self.notebook) 640 songlabel2 = _('Click to expand') 641 self.window.set_default_size(self.config.w, 1) 642 else: 643 songlabel2 = _('Click to collapse') 644 self.window.set_default_size(self.config.w, self.config.h) 645 songlabel2 = '<small>{}</small>'.format(songlabel2) 646 self.cursonglabel2.set_markup(songlabel2) 647 648 self.expander.set_tooltip_text(self.cursonglabel1.get_text()) 649 if not self.conn: 650 self.progressbar.set_text(_('Not Connected')) 651 elif not self.status: 652 self.progressbar.set_text(_('No Read Permission')) 653 654 # Update tab positions: 655 self.notebook.reorder_child(self.current.get_widgets(), 656 self.config.current_tab_pos) 657 self.notebook.reorder_child(self.library.get_widgets(), 658 self.config.library_tab_pos) 659 self.notebook.reorder_child(self.playlists.get_widgets(), 660 self.config.playlists_tab_pos) 661 self.notebook.reorder_child(self.streams.get_widgets(), 662 self.config.streams_tab_pos) 663 self.notebook.reorder_child(self.info.get_widgets(), 664 self.config.info_tab_pos) 665 self.last_tab = self.notebook_get_tab_text(self.notebook, 0) 666 667 # Song notification window: 668 self.tray_v_box = self.builder.get_object('tray_v_box') 669 670 if not self.config.show_covers: 671 self.tray_album_image.hide() 672 673 self.tray_current_label1 = self.builder.get_object('tray_label_1') 674 self.tray_current_label2 = self.builder.get_object('tray_label_2') 675 676 self.tray_progressbar = self.builder.get_object('tray_progressbar') 677 if not self.config.show_progress: 678 ui.hide(self.tray_progressbar) 679 680 self.tray_v_box.show_all() 681 self.traytips.add_widget(self.tray_v_box) 682 self.tooltip_set_window_width() 683 684 # Connect to signals 685 self.window.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) 686 self.traytips.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) 687 self.traytips.connect('button_press_event', self.on_traytips_press) 688 self.window.connect('delete_event', self.on_delete_event) 689 self.window.connect('configure_event', self.on_window_configure) 690 self.window.connect('key-press-event', self.on_topwindow_keypress) 691 self.imageeventbox.connect('button_press_event', 692 self.on_image_activate) 693 self.imageeventbox.connect('drag_motion', self.on_image_motion_cb) 694 self.imageeventbox.connect('drag_data_received', self.on_image_drop_cb) 695 self.ppbutton.connect('clicked', self.mpd_pp) 696 self.stopbutton.connect('clicked', self.mpd_stop) 697 self.prevbutton.connect('clicked', self.mpd_prev) 698 self.nextbutton.connect('clicked', self.mpd_next) 699 self.progresseventbox.connect('button_press_event', 700 self.on_progressbar_press) 701 self.progresseventbox.connect('scroll_event', 702 self.on_progressbar_scroll) 703 self.volumebutton.connect('value-changed', self.on_volume_change) 704 self.expander.connect('activate', self.on_expander_activate) 705 self.randommenu.connect('toggled', self.on_random_clicked) 706 self.repeatplaylistmenu.connect('toggled', self.on_repeat_playlist_clicked) 707 self.repeatsongmenu.connect('toggled', self.on_repeat_song_clicked) 708 self.cursonglabel1.connect('notify::label', self.on_currsong_notify) 709 self.cursonglabel1.connect('notify::label', self.fullscreen.on_text_changed) 710 self.progressbar.connect('notify::fraction', 711 self.on_progressbar_notify_fraction) 712 self.progressbar.connect('notify::text', 713 self.on_progressbar_notify_text) 714 self.mainwinhandler = self.window.connect('button_press_event', 715 self.on_window_click) 716 self.notebook.connect('size-allocate', self.on_notebook_resize) 717 self.notebook.connect('switch-page', self.on_notebook_page_change) 718 719 for treeview in [self.current_treeview, self.library_treeview, 720 self.playlists_treeview, self.streams_treeview]: 721 treeview.connect('popup_menu', self.on_menu_popup) 722 for treeviewsel in [self.current_selection, self.library_selection, 723 self.playlists_selection, self.streams_selection]: 724 treeviewsel.connect('changed', self.on_treeview_selection_changed) 725 for widget in [self.ppbutton, self.prevbutton, self.stopbutton, 726 self.nextbutton, self.progresseventbox, self.expander]: 727 widget.connect('button_press_event', self.menu_popup) 728 729 self.artwork.connect('artwork-changed', 730 self.fullscreen.on_artwork_changed) 731 self.artwork.connect('artwork-reset', self.fullscreen.reset) 732 733 self.systemtray_initialize() 734 735 # This will ensure that "Not connected" is shown in the systray tooltip 736 if not self.conn: 737 self.update_cursong() 738 739 # Ensure that the systemtray icon is added here. This is really only 740 # important if we're starting in hidden (minimized-to-tray) mode: 741 if self.config.withdrawn: 742 while Gtk.events_pending(): 743 Gtk.main_iteration() 744 745 dbus.init_gnome_mediakeys(self.mpd_pp, self.mpd_stop, self.mpd_prev, 746 self.mpd_next) 747 748 # XXX find new multimedia key library here, in case we don't have gnome! 749 #if not dbus.using_gnome_mediakeys(): 750 # pass 751 752 # Initialize playlist data and widget 753 self.playlistsdata = self.playlists.get_model() 754 755 # Initialize streams data and widget 756 self.streamsdata = self.streams.get_model() 757 758 # Initialize library data and widget 759 self.librarydata = self.library.get_model() 760 self.artwork.library_artwork_init(self.librarydata, 761 consts.LIB_COVER_SIZE) 762 763 icon = self.window.render_icon('sonata', Gtk.IconSize.DIALOG) 764 self.window.set_icon(icon) 765 self.streams.populate() 766 767 self.iterate_now() 768 if self.config.withdrawn and self.tray_icon.is_visible(): 769 ui.hide(self.window) 770 self.window.show_all() 771 772 # Ensure that button images are displayed despite GTK+ theme 773 self.window.get_settings().set_property("gtk-button-images", True) 774 775 if self.config.update_on_start: 776 self.on_updatedb(None) 777 778 self.notebook.set_no_show_all(False) 779 self.window.set_no_show_all(False) 780 781 if show_prefs: 782 self.on_prefs(None) 783 784 self.config.initial_run = False 785 786 # Ensure that sonata is loaded before we display the notif window 787 self.sonata_loaded = True 788 self.current.center_song_in_list(True) 789 790 gc.disable() 791 792 GLib.idle_add(self.header_save_column_widths) 793 794 pluginsystem.notify_of('tab_construct', 795 self.on_enable_tab, 796 self.on_disable_tab) 797 798 ### Tab system: 799 800 def on_enable_tab(self, _plugin, tab): 801 tab_parts = tab() 802 name = tab_parts[2] 803 self.plugintabs[name] = self.add_tab(*tab_parts) 804 805 def on_disable_tab(self, _plugin, tab): 806 name = tab()[2] 807 tab = self.plugintabs.pop(name) 808 self.notebook.remove(tab) 809 810 def add_tab(self, page, label_widget, text, focus): 811 label_widget.show_all() 812 label_widget.connect("button_press_event", self.on_tab_click) 813 814 self.notebook.append_page(page, label_widget) 815 if (text in self.tabname2id and 816 not getattr(self.config, 817 self.tabname2id[text] + '_tab_visible')): 818 ui.hide(page) 819 820 self.notebook.set_tab_reorderable(page, True) 821 if self.config.tabs_expanded: 822 self.notebook.set_tab_label_packing(page, True, True, 823 Gtk.PACK_START) 824 825 self.tabname2tab[text] = page 826 self.tabname2focus[text] = focus 827 return page 828 829 def connected(self): 830 ### "Model, logic": 831 return self.conn 832 833 def status_is_play_or_pause(self): 834 return (self.conn and self.status and 835 self.status.get('state', None) in ['play', 'pause']) 836 837 def get_playing_song(self): 838 if self.status_is_play_or_pause() and self.songinfo: 839 return self.songinfo 840 return None 841 842 def playing_song_change(self): 843 self.artwork.artwork_update() 844 for _plugin, cb in pluginsystem.get('playing_song_observers'): 845 cb(self.get_playing_song()) 846 847 def get_fullscreen_info(self): 848 return (self.status_is_play_or_pause(), self.cursonglabel1.get_text(), 849 self.cursonglabel2.get_text()) 850 851 def set_allow_art_search(self): 852 self.allow_art_search = True 853 854 def populate_profiles_for_menu(self): 855 host, port, _password = misc.mpd_env_vars() 856 if self.merge_id: 857 self.UIManager.remove_ui(self.merge_id) 858 if self.actionGroupProfiles: 859 self.UIManager.remove_action_group(self.actionGroupProfiles) 860 self.actionGroupProfiles = Gtk.ActionGroup('MPDProfiles') 861 self.UIManager.ensure_update() 862 863 profile_names = [_("MPD_HOST/PORT")] if host \ 864 or port else self.config.profile_names 865 866 actions = [ 867 (str(i), 868 None, 869 "[%d] %s" % (i + 1, ui.quote_label(name)), 870 None, 871 None, 872 i) 873 for i, name in enumerate(profile_names)] 874 actions.append(( 875 'disconnect', 876 Gtk.STOCK_DISCONNECT, 877 _('Disconnect'), 878 None, 879 None, 880 len(self.config.profile_names))) 881 882 active_radio = 0 if host or port else self.config.profile_num 883 if not self.conn: 884 active_radio = len(self.config.profile_names) 885 self.actionGroupProfiles.add_radio_actions(actions, active_radio, 886 self.on_profiles_click) 887 uiDescription = """ 888 <ui> 889 <popup name="mainmenu"> 890 <menu action="profilesmenu"> 891 """ 892 uiDescription += "".join( 893 '<menuitem action=\"%s\" position="top"/>' % action[0] 894 for action in reversed(actions)) 895 uiDescription += """</menu></popup></ui>""" 896 self.merge_id = self.UIManager.add_ui_from_string(uiDescription) 897 self.UIManager.insert_action_group(self.actionGroupProfiles, 0) 898 self.UIManager.get_widget('/hidden').set_property('visible', False) 899 900 def on_profiles_click(self, _radioaction, profile): 901 if self.skip_on_profiles_click: 902 return 903 if profile.get_name() == 'disconnect': 904 self.on_disconnectkey_pressed(None) 905 else: 906 # Clear sonata before we try to connect: 907 self.mpd_disconnect() 908 self.iterate_now() 909 # Now connect to new profile: 910 self.config.profile_num = profile.get_current_value() 911 self.on_connectkey_pressed(None) 912 913 def mpd_connect(self, force=False): 914 if self.trying_connection: 915 return 916 self.trying_connection = True 917 if self.user_connect or force: 918 host, port, password = misc.mpd_env_vars() 919 if not host: 920 host = self.config.host[self.config.profile_num] 921 if not port: 922 port = self.config.port[self.config.profile_num] 923 if not password: 924 password = self.config.password[self.config.profile_num] 925 self.mpd.connect(host, port) 926 if len(password) > 0: 927 self.mpd.password(password) 928 test = self.mpd.status() 929 if test: 930 self.conn = True 931 else: 932 self.conn = False 933 else: 934 self.conn = False 935 if not self.conn: 936 self.status = None 937 self.songinfo = None 938 if self.artwork is not None: 939 self.artwork.update_songinfo(self.songinfo) 940 941 self.iterate_time = self.iterate_time_when_disconnected_or_stopped 942 self.trying_connection = False 943 944 def mpd_disconnect(self): 945 if self.conn: 946 self.mpd.close() 947 self.mpd.disconnect() 948 self.conn = False 949 950 def on_connectkey_pressed(self, _event=None): 951 self.user_connect = True 952 # Update selected radio button in menu: 953 self.skip_on_profiles_click = True 954 host, port, _password = misc.mpd_env_vars() 955 index = str(0 if host or port else self.config.profile_num) 956 self.actionGroupProfiles.get_action(index).activate() 957 self.skip_on_profiles_click = False 958 # Connect: 959 self.mpd_connect(force=True) 960 self.iterate_now() 961 962 def on_disconnectkey_pressed(self, _event): 963 self.user_connect = False 964 # Update selected radio button in menu: 965 self.skip_on_profiles_click = True 966 self.actionGroupProfiles.get_action('disconnect').activate() 967 self.skip_on_profiles_click = False 968 # Disconnect: 969 self.mpd_disconnect() 970 971 def update_status(self): 972 try: 973 if not self.conn: 974 self.mpd_connect() 975 if self.conn: 976 self.iterate_time = self.iterate_time_when_connected 977 self.status = self.mpd.status() 978 if self.status: 979 if self.status['state'] == 'stop': 980 self.iterate_time = \ 981 self.iterate_time_when_disconnected_or_stopped 982 self.songinfo = self.mpd.currentsong() 983 self.artwork.update_songinfo(self.songinfo) 984 repeat_song = self.status['repeat'] == '1' \ 985 and self.status['single'] == '1' 986 repeat_playlist = self.status['repeat'] == '1' \ 987 and self.status['single'] == '0' 988 if not self.last_song_repeat \ 989 or self.last_song_repeat != repeat_song: 990 self.repeatsongmenu.set_active(repeat_song) 991 if not self.last_playlist_repeat \ 992 or self.last_playlist_repeat != repeat_playlist: 993 self.repeatplaylistmenu.set_active(repeat_playlist) 994 if not self.last_random \ 995 or self.last_random != self.status['random']: 996 self.randommenu.set_active( 997 self.status['random'] == '1') 998 if not self.last_consume or self.last_consume != self.status['consume']: 999 self.consumemenu.set_active(self.status['consume'] == '1') 1000 1001 self.config.xfade_enabled = False 1002 if 'xfade' in self.status: 1003 xfade = int(self.status['xfade']) 1004 1005 if xfade != 0: 1006 self.config.xfade_enabled = True 1007 self.config.xfade = xfade 1008 if self.config.xfade > 30: 1009 self.config.xfade = 30 1010 1011 self.last_song_repeat = repeat_song 1012 self.last_playlist_repeat = repeat_playlist 1013 self.last_random = self.status['random'] 1014 self.last_consume = self.status['consume'] 1015 return 1016 except: 1017 self.logger.exception("Unhandled error while updating status:") 1018 1019 self.prevstatus = self.status 1020 self.prevsonginfo = self.songinfo 1021 self.conn = False 1022 self.status = None 1023 self.songinfo = None 1024 self.artwork.update_songinfo(self.songinfo) 1025 1026 def iterate(self): 1027 self.update_status() 1028 self.info_update(False) 1029 1030 # XXX: this is subject to race condition, since self.conn can be 1031 # changed in another thread: 1032 # 1. self.conn == self.prevconn (stable state) 1033 # 2. This if is tested and self.handle_change_conn is not called 1034 # 3. The connection thread updates self.conn 1035 # 4. self.prevconn = self.conn and we never get into the connected 1036 # state (or maybe throught another way, but well). 1037 if self.conn != self.prevconn: 1038 self.handle_change_conn() 1039 if self.status != self.prevstatus: 1040 self.handle_change_status() 1041 if self.songinfo != self.prevsonginfo: 1042 self.handle_change_song() 1043 1044 self.prevconn = self.conn 1045 self.prevstatus = self.status 1046 self.prevsonginfo = self.songinfo 1047 1048 # Repeat ad infitum.. 1049 self.iterate_handler = GLib.timeout_add(self.iterate_time, self.iterate) 1050 1051 if self.config.show_trayicon: 1052 if self.tray_icon.is_available() and \ 1053 not self.tray_icon.is_visible(): 1054 # Systemtray appears, add icon 1055 self.systemtray_initialize() 1056 elif not self.tray_icon.is_available() and self.config.withdrawn: 1057 # Systemtray gone, unwithdraw app 1058 self.withdraw_app_undo() 1059 1060 if self.call_gc_collect: 1061 gc.collect() 1062 self.call_gc_collect = False 1063 1064 def schedule_gc_collect(self): 1065 self.call_gc_collect = True 1066 1067 def iterate_stop(self): 1068 try: 1069 GLib.source_remove(self.iterate_handler) 1070 except: 1071 pass 1072 1073 def iterate_now(self): 1074 # Since self.iterate_time_when_connected has been 1075 # slowed down to 500ms, we'll call self.iterate_now() 1076 # whenever the user performs an action that requires 1077 # updating the client 1078 self.iterate_stop() 1079 self.iterate() 1080 1081 def on_topwindow_keypress(self, _widget, event): 1082 shortcut = Gtk.accelerator_name(event.keyval, event.get_state()) 1083 shortcut = shortcut.replace("<Mod2>", "") 1084 # These shortcuts were moved here so that they don't interfere with 1085 # searching the library 1086 if shortcut == 'BackSpace' and self.current_tab == self.TAB_LIBRARY: 1087 return self.library.library_browse_parent(None) 1088 elif shortcut == 'Escape': 1089 if self.current_tab == self.TAB_LIBRARY \ 1090 and self.library.search_visible(): 1091 self.library.on_search_end(None) 1092 elif self.current_tab == self.TAB_CURRENT \ 1093 and self.current.filterbox_visible: 1094 self.current.searchfilter_toggle(None) 1095 elif self.config.minimize_to_systray and \ 1096 self.tray_icon.is_visible(): 1097 self.withdraw_app() 1098 return 1099 elif shortcut == 'Delete': 1100 return self.on_remove(None) 1101 if self.current_tab == self.TAB_CURRENT: 1102 if event.get_state() & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.MOD1_MASK): 1103 return 1104 1105 # XXX this isn't the right thing with GTK input methods: 1106 text = chr(Gdk.keyval_to_unicode(event.keyval)) 1107 1108 # Filter out those keys and don't toggle the filter bar if they are 1109 # pressed. 1110 filter_out = set([ 1111 '\x00', # F5, Alt, etc. 1112 '\x08', # Backspace 1113 '\x7f', # Delete 1114 ]) 1115 1116 if text not in filter_out and text.strip(): 1117 if not self.current.filterbox_visible: 1118 if text != "/": 1119 self.current.searchfilter_toggle(None, text) 1120 else: 1121 self.current.searchfilter_toggle(None) 1122 1123 def settings_load(self): 1124 self.config.settings_load_real() 1125 1126 def settings_save(self): 1127 self.header_save_column_widths() 1128 1129 self.config.current_tab_pos = self.notebook_get_tab_num( 1130 self.notebook, self.TAB_CURRENT) 1131 self.config.library_tab_pos = self.notebook_get_tab_num( 1132 self.notebook, self.TAB_LIBRARY) 1133 self.config.playlists_tab_pos = self.notebook_get_tab_num( 1134 self.notebook, self.TAB_PLAYLISTS) 1135 self.config.streams_tab_pos = self.notebook_get_tab_num( 1136 self.notebook, self.TAB_STREAMS) 1137 self.config.info_tab_pos = self.notebook_get_tab_num(self.notebook, 1138 self.TAB_INFO) 1139 1140 autostart_plugins = [] 1141 for plugin in pluginsystem.plugin_infos: 1142 if plugin._enabled: 1143 autostart_plugins.append(plugin.name) 1144 self.config.autostart_plugins = autostart_plugins 1145 1146 self.config.settings_save_real() 1147 1148 def handle_change_conn(self): 1149 if not self.conn: 1150 for mediabutton in (self.ppbutton, self.stopbutton, 1151 self.prevbutton, self.nextbutton, 1152 self.volumebutton): 1153 mediabutton.set_property('sensitive', False) 1154 self.current.clear() 1155 self.tray_icon.update_icon('sonata-disconnect') 1156 self.info_update(True) 1157 if self.current.filterbox_visible: 1158 GLib.idle_add(self.current.searchfilter_toggle, None) 1159 if self.library.search_visible(): 1160 self.library.on_search_end(None) 1161 self.handle_change_song() 1162 self.handle_change_status() 1163 else: 1164 for mediabutton in (self.ppbutton, self.stopbutton, 1165 self.prevbutton, self.nextbutton, 1166 self.volumebutton): 1167 mediabutton.set_property('sensitive', True) 1168 if self.sonata_loaded: 1169 self.library.library_browse(root=SongRecord(path="/")) 1170 self.playlists.populate() 1171 self.streams.populate() 1172 self.on_notebook_page_change(self.notebook, 0, 1173 self.notebook.get_current_page()) 1174 1175 def info_update(self, update_all): 1176 playing_or_paused = self.status_is_play_or_pause() 1177 newbitrate = None 1178 if self.status: 1179 newbitrate = self.status.get('bitrate', '') 1180 if newbitrate: 1181 newbitrate += " kbps" 1182 self.info.update(playing_or_paused, newbitrate, self.songinfo, 1183 update_all) 1184 1185 def on_treeview_selection_changed(self, treeselection): 1186 self.update_menu_visibility() 1187 if treeselection == self.current.get_selection(): 1188 # User previously clicked inside group of selected rows, re-select 1189 # rows so it doesn't look like anything changed: 1190 if self.current.sel_rows: 1191 for row in self.current.sel_rows: 1192 treeselection.select_path(row) 1193 1194 def on_library_button_press(self, widget, event): 1195 if self.on_button_press(widget, event, False): 1196 return True 1197 1198 def on_current_button_press(self, widget, event): 1199 if self.on_button_press(widget, event, True): 1200 return True 1201 1202 def on_playlists_button_press(self, widget, event): 1203 if self.on_button_press(widget, event, False): 1204 return True 1205 1206 def on_streams_button_press(self, widget, event): 1207 if self.on_button_press(widget, event, False): 1208 return True 1209 1210 def on_button_press(self, widget, event, widget_is_current): 1211 ctrl_press = (event.get_state() & Gdk.ModifierType.CONTROL_MASK) 1212 self.current.sel_rows = None 1213 if event.button == 1 and widget_is_current and not ctrl_press: 1214 # If the user clicked inside a group of rows that were already 1215 # selected, we need to retain the selected rows in case the user 1216 # wants to DND the group of rows. If they release the mouse without 1217 # first moving it, then we revert to the single selected row. 1218 # This is similar to the behavior found in thunar. 1219 try: 1220 path, _col, _x, _y = widget.get_path_at_pos(int(event.x), 1221 int(event.y)) 1222 if widget.get_selection().path_is_selected(path): 1223 self.current.sel_rows = \ 1224 widget.get_selection().get_selected_rows()[1] 1225 except: 1226 pass 1227 elif event.button == 3: 1228 self.update_menu_visibility() 1229 # Calling the popup in idle_add is important. It allows the menu 1230 # items to have been shown/hidden before the menu is popped up. 1231 # Otherwise, if the menu pops up too quickly, it can result in 1232 # automatically clicking menu items for the user! 1233 GLib.idle_add(self.mainmenu.popup, None, None, None, None, 1234 event.button, event.time) 1235 # Don't change the selection for a right-click. This 1236 # will allow the user to select multiple rows and then 1237 # right-click (instead of right-clicking and having 1238 # the current selection change to the current row) 1239 if widget.get_selection().count_selected_rows() > 1: 1240 return True 1241 1242 def on_add_item_play(self, widget): 1243 self.on_add_item(widget, True) 1244 1245 def on_add_item(self, _widget, play_after=False): 1246 if self.conn: 1247 if play_after and self.status: 1248 playid = self.status['playlistlength'] 1249 if self.current_tab == self.TAB_LIBRARY: 1250 items = self.library.get_path_child_filenames(True) 1251 self.mpd.command_list_ok_begin() 1252 for item in items: 1253 self.mpd.add(item) 1254 self.mpd.command_list_end() 1255 elif self.current_tab == self.TAB_PLAYLISTS: 1256 model, selected = self.playlists_selection.get_selected_rows() 1257 for path in selected: 1258 self.mpd.load( 1259 misc.unescape_html( 1260 model.get_value(model.get_iter(path), 1))) 1261 elif self.current_tab == self.TAB_STREAMS: 1262 model, selected = self.streams_selection.get_selected_rows() 1263 for path in selected: 1264 item = model.get_value(model.get_iter(path), 2) 1265 self.stream_parse_and_add(item) 1266 self.iterate_now() 1267 if play_after: 1268 if self.status['random'] == '1': 1269 # If we are in random mode, we want to play a random song 1270 # instead: 1271 self.mpd.play() 1272 else: 1273 self.mpd.play(int(playid)) 1274 1275 def add_selected_to_playlist(self, plname): 1276 if self.current_tab == self.TAB_LIBRARY: 1277 songs = self.library.get_path_child_filenames(True) 1278 elif self.current_tab == self.TAB_CURRENT: 1279 songs = self.current.get_selected_filenames(0) 1280 else: 1281 raise Exception("This tab doesn't support playlists") 1282 1283 self.mpd.command_list_ok_begin() 1284 for song in songs: 1285 self.mpd.playlistadd(plname, song) 1286 self.mpd.command_list_end() 1287 1288 def stream_parse_and_add(self, uri): 1289 # Try different URI, in case we don't have a good one. 1290 # XXX: we should build new URI only if it makes sense. If .open() fails 1291 # because a URI starting with "file://" doesn't exist anymore, it 1292 # doesn't really matter to add file:// in front of it to try again. 1293 for try_uri in [uri, "http://" + uri, "file://" + uri]: 1294 while Gtk.events_pending(): 1295 Gtk.main_iteration() 1296 1297 try: 1298 request = urllib.request.Request(uri) 1299 opener = urllib.request.build_opener() 1300 fp = opener.open(request) 1301 except: 1302 # We fail, maybe we'll just add the URI to MPD as it. 1303 items = [uri] 1304 continue 1305 1306 # URI openable! 1307 items = streams.parse_stream(try_uri, fp) 1308 break 1309 1310 for item in items: 1311 self.mpd.add(item) 1312 1313 def on_replace_item_play(self, widget): 1314 self.on_replace_item(widget, True) 1315 1316 def on_replace_item(self, widget, play_after=False): 1317 if self.status and self.status['state'] == 'play': 1318 play_after = True 1319 # Only clear if an item is selected: 1320 if self.current_tab == self.TAB_LIBRARY: 1321 num_selected = self.library_selection.count_selected_rows() 1322 elif self.current_tab == self.TAB_PLAYLISTS: 1323 num_selected = self.playlists_selection.count_selected_rows() 1324 elif self.current_tab == self.TAB_STREAMS: 1325 num_selected = self.streams_selection.count_selected_rows() 1326 else: 1327 return 1328 if num_selected == 0: 1329 return 1330 self.mpd_clear(None) 1331 self.on_add_item(widget, play_after) 1332 self.iterate_now() 1333 1334 def handle_change_status(self): 1335 # Called when one of the following items are changed: 1336 # 1. Current playlist (song added, removed, etc) 1337 # 2. Repeat/random/xfade/volume 1338 # 3. Currently selected song in playlist 1339 # 4. Status (playing/paused/stopped) 1340 if self.status is None: 1341 # clean up and bail out 1342 self.update_progressbar() 1343 self.update_cursong() 1344 self.update_wintitle() 1345 self.playing_song_change() 1346 self.update_statusbar() 1347 if not self.conn: 1348 self.librarydata.clear() 1349 self.playlistsdata.clear() 1350 self.streamsdata.clear() 1351 return 1352 1353 # Display current playlist 1354 if self.prevstatus is None \ 1355 or self.prevstatus['playlist'] != self.status['playlist']: 1356 prevstatus_playlist = None 1357 if self.prevstatus: 1358 prevstatus_playlist = self.prevstatus['playlist'] 1359 self.current.current_update(prevstatus_playlist, 1360 self.status['playlistlength']) 1361 1362 # Update progress frequently if we're playing 1363 if self.status_is_play_or_pause(): 1364 self.update_progressbar() 1365 1366 # If elapsed time is shown in the window title, we need to update 1367 # more often: 1368 if "%E" in self.config.titleformat: 1369 self.update_wintitle() 1370 1371 # If state changes 1372 if self.prevstatus is None \ 1373 or self.prevstatus['state'] != self.status['state']: 1374 1375 self.album_get_artist() 1376 1377 # Update progressbar if the state changes too 1378 self.update_progressbar() 1379 self.update_cursong() 1380 self.update_wintitle() 1381 self.info_update(True) 1382 if self.status['state'] == 'stop': 1383 self.ppbutton_image.set_from_stock(Gtk.STOCK_MEDIA_PLAY, 1384 Gtk.IconSize.BUTTON) 1385 self.UIManager.get_widget('/traymenu/playmenu').show() 1386 self.UIManager.get_widget('/traymenu/pausemenu').hide() 1387 icon = 'sonata' 1388 elif self.status['state'] == 'pause': 1389 self.ppbutton_image.set_from_stock(Gtk.STOCK_MEDIA_PLAY, 1390 Gtk.IconSize.BUTTON) 1391 self.UIManager.get_widget('/traymenu/playmenu').show() 1392 self.UIManager.get_widget('/traymenu/pausemenu').hide() 1393 icon = 'sonata-pause' 1394 elif self.status['state'] == 'play': 1395 self.ppbutton_image.set_from_stock(Gtk.STOCK_MEDIA_PAUSE, 1396 Gtk.IconSize.BUTTON) 1397 self.UIManager.get_widget('/traymenu/playmenu').hide() 1398 self.UIManager.get_widget('/traymenu/pausemenu').show() 1399 if self.prevstatus != None: 1400 if self.prevstatus['state'] == 'pause': 1401 # Forces the notification to popup if specified 1402 self.on_currsong_notify() 1403 icon = 'sonata-play' 1404 else: 1405 icon = 'sonata-disconnect' 1406 1407 self.tray_icon.update_icon(icon) 1408 self.playing_song_change() 1409 if self.status_is_play_or_pause(): 1410 self.current.center_song_in_list() 1411 1412 if self.prevstatus is None \ 1413 or self.status.get('volume') != self.prevstatus.get('volume'): 1414 self.volumebutton.set_value(int(self.status.get('volume', 0))) 1415 1416 if self.conn: 1417 if mpdh.mpd_is_updating(self.status): 1418 # MPD library is being updated 1419 self.update_statusbar(True) 1420 elif self.prevstatus is None \ 1421 or mpdh.mpd_is_updating(self.prevstatus) \ 1422 != mpdh.mpd_is_updating(self.status): 1423 if not mpdh.mpd_is_updating(self.status): 1424 # Done updating, refresh interface 1425 self.mpd_updated_db() 1426 elif self.mpd_update_queued: 1427 # If the update happens too quickly, we won't catch it in 1428 # our polling. So let's force an update of the interface: 1429 self.mpd_updated_db() 1430 self.mpd_update_queued = False 1431 1432 if self.config.as_enabled: 1433 if self.prevstatus: 1434 prevstate = self.prevstatus['state'] 1435 else: 1436 prevstate = 'stop' 1437 if self.status: 1438 state = self.status['state'] 1439 else: 1440 state = 'stop' 1441 1442 if state in ('play', 'pause'): 1443 mpd_time_now = self.status['time'] 1444 self.scrobbler.handle_change_status(state, prevstate, 1445 self.prevsonginfo, 1446 self.songinfo, 1447 mpd_time_now) 1448 elif state == 'stop': 1449 self.scrobbler.handle_change_status(state, prevstate, 1450 self.prevsonginfo) 1451 1452 def mpd_updated_db(self): 1453 self.library.view_caches_reset() 1454 self.update_statusbar(False) 1455 # We need to make sure that we update the artist in case tags 1456 # have changed: 1457 self.album_reset_artist() 1458 self.album_get_artist() 1459 # Now update the library and playlist tabs 1460 if self.library.search_visible(): 1461 self.library.on_library_search_combo_change() 1462 else: 1463 self.library.library_browse(root=self.config.wd) 1464 self.playlists.populate() 1465 # Update info if it's visible: 1466 self.info_update(True) 1467 return False 1468 1469 def album_get_artist(self): 1470 if self.songinfo and 'album' in self.songinfo: 1471 self.album_return_artist_name() 1472 elif self.songinfo and 'artist' in self.songinfo: 1473 self.album_current_artist = [self.songinfo, 1474 self.songinfo.artist] 1475 else: 1476 self.album_current_artist = [self.songinfo, ""] 1477 1478 def handle_change_song(self): 1479 # Called when one of the following items are changed for the current 1480 # mpd song in the playlist: 1481 # 1. Song tags or filename (e.g. if tags are edited) 1482 # 2. Position in playlist (e.g. if playlist is sorted) 1483 # Note that the song does not have to be playing; it can reflect the 1484 # next song that will be played. 1485 self.current.on_song_change(self.status) 1486 1487 self.album_get_artist() 1488 1489 self.update_cursong() 1490 self.update_wintitle() 1491 self.playing_song_change() 1492 self.info_update(True) 1493 1494 def update_progressbar(self): 1495 if self.status_is_play_or_pause(): 1496 at, length = [float(c) for c in self.status['time'].split(':')] 1497 try: 1498 newfrac = at / length 1499 except: 1500 newfrac = 0 1501 else: 1502 newfrac = 0 1503 if not self.last_progress_frac or self.last_progress_frac != newfrac: 1504 if newfrac >= 0 and newfrac <= 1: 1505 self.progressbar.set_fraction(newfrac) 1506 if self.conn: 1507 if self.status_is_play_or_pause(): 1508 at, length = [int(c) for c in self.status['time'].split(':')] 1509 at_time = misc.convert_time(at) 1510 try: 1511 time = misc.convert_time(self.songinfo.time) 1512 newtime = at_time + " / " + time 1513 except: 1514 newtime = at_time 1515 elif self.status: 1516 newtime = ' ' 1517 else: 1518 newtime = _('No Read Permission') 1519 else: 1520 newtime = _('Not Connected') 1521 if not self.last_progress_text or self.last_progress_text != newtime: 1522 self.progressbar.set_text(newtime) 1523 1524 def update_statusbar(self, updatingdb=False): 1525 if self.config.show_statusbar: 1526 if self.conn and self.status: 1527 days = None 1528 # FIXME _ is for localization, temporarily __ 1529 hours, mins, __ = misc.convert_time_raw(self.current.total_time) 1530 # Show text: 1531 songs_count = int(self.status['playlistlength']) 1532 songs_text = ngettext('{count} song', '{count} songs', 1533 songs_count).format(count=songs_count) 1534 time_parts = [] 1535 if hours >= 24: 1536 days = int(hours / 24) 1537 hours = hours - (days * 24) 1538 if days: 1539 days_text = ngettext('{count} day', '{count} days', 1540 days).format(count=days) 1541 time_parts.append(days_text) 1542 if hours: 1543 hours_text = ngettext('{count} hour', '{count} hours', 1544 hours).format(count=hours) 1545 time_parts.append(hours_text) 1546 if mins: 1547 mins_text = ngettext('{count} minute', '{count} minutes', 1548 mins).format(count=mins) 1549 time_parts.append(mins_text) 1550 time_text = ', '.join([part for part in time_parts if part]) 1551 if int(self.status['playlistlength']) > 0: 1552 status_text = "{}: {}".format(songs_text, time_text) 1553 else: 1554 status_text = '' 1555 if updatingdb: 1556 update_text = _('(updating mpd)') 1557 status_text = "{}: {}".format(status_text, update_text) 1558 else: 1559 status_text = '' 1560 if status_text != self.last_status_text: 1561 self.statusbar.push(self.statusbar.get_context_id(''), 1562 status_text) 1563 self.last_status_text = status_text 1564 1565 def update_cursong(self): 1566 if self.status_is_play_or_pause(): 1567 # We must show the trayprogressbar and trayalbumeventbox 1568 # before changing self.cursonglabel (and consequently calling 1569 # self.on_currsong_notify()) in order to ensure that the 1570 # notification popup will have the correct height when being 1571 # displayed for the first time after a stopped state. 1572 if self.config.show_progress: 1573 self.tray_progressbar.show() 1574 self.tray_current_label2.show() 1575 if self.config.show_covers: 1576 self.tray_album_image.show() 1577 1578 for label in (self.cursonglabel1, self.cursonglabel2, 1579 self.tray_current_label1, self.tray_current_label2): 1580 label.set_ellipsize(Pango.EllipsizeMode.END) 1581 1582 1583 if len(self.config.currsongformat1) > 0: 1584 newlabel1 = formatting.parse(self.config.currsongformat1, 1585 self.songinfo, True) 1586 else: 1587 newlabel1 = '' 1588 newlabel1 = '<big>{}</big>'.format(newlabel1) 1589 if len(self.config.currsongformat2) > 0: 1590 newlabel2 = formatting.parse(self.config.currsongformat2, 1591 self.songinfo, True) 1592 else: 1593 newlabel2 = '' 1594 newlabel2 = '<small>{}</small>'.format(newlabel2) 1595 if newlabel1 != self.cursonglabel1.get_label(): 1596 self.cursonglabel1.set_markup(newlabel1) 1597 if newlabel2 != self.cursonglabel2.get_label(): 1598 self.cursonglabel2.set_markup(newlabel2) 1599 if newlabel1 != self.tray_current_label1.get_label(): 1600 self.tray_current_label1.set_markup(newlabel1) 1601 if newlabel2 != self.tray_current_label2.get_label(): 1602 self.tray_current_label2.set_markup(newlabel2) 1603 self.expander.set_tooltip_text('%s\n%s' % \ 1604 (self.cursonglabel1.get_text(), 1605 self.cursonglabel2.get_text(),)) 1606 else: 1607 for label in (self.cursonglabel1, self.cursonglabel2, 1608 self.tray_current_label1, self.cursonglabel2): 1609 label.set_ellipsize(Pango.EllipsizeMode.NONE) 1610 1611 newlabel1 = '<big>{}</big>'.format(_('Stopped')) 1612 self.cursonglabel1.set_markup(newlabel1) 1613 if self.config.expanded: 1614 newlabel2 = _('Click to collapse') 1615 else: 1616 newlabel2 = _('Click to expand') 1617 newlabel2 = '<small>{}</small>'.format(newlabel2) 1618 self.cursonglabel2.set_markup(newlabel2) 1619 self.expander.set_tooltip_text(self.cursonglabel1.get_text()) 1620 if not self.conn: 1621 traylabel1 = _('Not Connected') 1622 elif not self.status: 1623 traylabel1 = _('No Read Permission') 1624 else: 1625 traylabel1 = _('Stopped') 1626 traylabel1 = '<big>{}</big>'.format(traylabel1) 1627 self.tray_current_label1.set_markup(traylabel1) 1628 self.tray_progressbar.hide() 1629 self.tray_album_image.hide() 1630 self.tray_current_label2.hide() 1631 self.update_infofile() 1632 1633 def update_wintitle(self): 1634 if self.status_is_play_or_pause(): 1635 newtitle = formatting.parse( 1636 self.config.titleformat, self.songinfo, 1637 False, True, 1638 self.status.get('time', None)) 1639 else: 1640 newtitle = '[Sonata]' 1641 if not self.last_title or self.last_title != newtitle: 1642 self.window.set_property('title', newtitle) 1643 self.last_title = newtitle 1644 1645 def tooltip_set_window_width(self): 1646 screen = self.window.get_screen() 1647 _pscreen, px, py, _mods = screen.get_display().get_pointer() 1648 monitor_num = screen.get_monitor_at_point(px, py) 1649 monitor = screen.get_monitor_geometry(monitor_num) 1650 self.notification_width = int(monitor.width * 0.30) 1651 if self.notification_width > consts.NOTIFICATION_WIDTH_MAX: 1652 self.notification_width = consts.NOTIFICATION_WIDTH_MAX 1653 elif self.notification_width < consts.NOTIFICATION_WIDTH_MIN: 1654 self.notification_width = consts.NOTIFICATION_WIDTH_MIN 1655 1656 def on_currsong_notify(self, _foo=None, _bar=None, force_popup=False): 1657 if self.fullscreen.on_fullscreen: 1658 return 1659 if self.sonata_loaded: 1660 if self.status_is_play_or_pause(): 1661 if self.config.show_covers: 1662 self.traytips.set_size_request(self.notification_width, -1) 1663 else: 1664 self.traytips.set_size_request( 1665 self.notification_width - 100, -1) 1666 else: 1667 self.traytips.set_size_request(-1, -1) 1668 if self.config.show_notification or force_popup: 1669 try: 1670 GLib.source_remove(self.traytips.notif_handler) 1671 except: 1672 pass 1673 if self.status_is_play_or_pause(): 1674 try: 1675 self.traytips.notifications_location = \ 1676 self.config.traytips_notifications_location 1677 self.traytips.use_notifications_location = True 1678 if self.tray_icon.is_visible(): 1679 self.traytips._real_display(self.tray_icon) 1680 else: 1681 self.traytips._real_display(None) 1682 if self.config.popup_option != len(self.popuptimes)-1: 1683 if force_popup and \ 1684 not self.config.show_notification: 1685 # Used -p argument and notification is disabled 1686 # in player; default to 3 seconds 1687 timeout = 3000 1688 else: 1689 timeout = \ 1690 int(self.popuptimes[ 1691 self.config.popup_option]) * 1000 1692 self.traytips.notif_handler = GLib.timeout_add( 1693 timeout, self.traytips.hide) 1694 else: 1695 # -1 indicates that the timeout should be forever. 1696 # We don't want to pass None, because then Sonata 1697 # would think that there is no current notification 1698 self.traytips.notif_handler = -1 1699 except: 1700 pass 1701 else: 1702 self.traytips.hide() 1703 elif self.traytips.get_property('visible'): 1704 self.traytips._real_display(self.tray_icon) 1705 1706 def on_progressbar_notify_fraction(self, *_args): 1707 self.tray_progressbar.set_fraction(self.progressbar.get_fraction()) 1708 1709 def on_progressbar_notify_text(self, *_args): 1710 self.tray_progressbar.set_text(self.progressbar.get_text()) 1711 1712 def update_infofile(self): 1713 if self.config.use_infofile is True: 1714 try: 1715 info_file = open(self.config.infofile_path, 'w', 1716 encoding="utf-8") 1717 1718 if self.status['state'] in ['play']: 1719 info_file.write('Status: ' + 'Playing' + '\n') 1720 elif self.status['state'] in ['pause']: 1721 info_file.write('Status: ' + 'Paused' + '\n') 1722 elif self.status['state'] in ['stop']: 1723 info_file.write('Status: ' + 'Stopped' + '\n') 1724 1725 if self.songinfo.artist: 1726 info_file.write('Title: %s - %s\n' % ( 1727 self.songinfo.artist, 1728 (self.songinfo.title or ''))) 1729 else: 1730 # No Artist in streams 1731 try: 1732 info_file.write('Title: %s\n' % (self.songinfo.title or '')) 1733 except: 1734 info_file.write('Title: No - ID Tag\n') 1735 info_file.write('Album: %s\n' % (self.songinfo.album or 'No Data')) 1736 info_file.write('Track: %s\n' % self.songinfo.track) 1737 info_file.write('File: %s\n' % (self.songinfo.file or 'No Data')) 1738 info_file.write('Time: %s\n' % self.songinfo.time) 1739 info_file.write('Volume: %s\n' % (self.status['volume'],)) 1740 info_file.write('Repeat: %s\n' % (self.status['repeat'],)) 1741 info_file.write('Single: %s\n' % (self.status['single'],)) 1742 info_file.write('Random: %s\n' % (self.status['random'],)) 1743 info_file.write('Consume: %s\n' % (self.status['consume'],)) 1744 info_file.close() 1745 except: 1746 pass 1747 1748 ################# 1749 # Gui Callbacks # 1750 ################# 1751 1752 def on_fullscreen_change(self, _widget=None): 1753 if self.fullscreen.on_fullscreen: 1754 self.traytips.show() 1755 else: 1756 self.traytips.hide() 1757 self.fullscreen.on_fullscreen_change() 1758 1759 def on_delete_event_yes(self, _widget): 1760 self.exit_now = True 1761 self.on_delete_event(None, None) 1762 1763 # This one makes sure the program exits when the window is closed 1764 def on_delete_event(self, _widget, _data=None): 1765 if not self.exit_now and self.config.minimize_to_systray: 1766 if self.tray_icon.is_visible(): 1767 self.withdraw_app() 1768 return True 1769 self.settings_save() 1770 self.artwork.cache.save() 1771 if self.config.as_enabled: 1772 self.scrobbler.save_cache() 1773 if self.conn and self.config.stop_on_exit: 1774 self.mpd_stop(None) 1775 sys.exit() 1776 1777 def on_window_configure(self, window, _event): 1778 # When withdrawing an app, extra configure events (with wrong coords) 1779 # are fired (at least on Openbox). This prevents a user from moving 1780 # the window, withdrawing it, then unwithdrawing it and finding it in 1781 # an older position 1782 if not window.props.visible: 1783 return 1784 1785 width, height = window.get_size() 1786 if self.config.expanded: 1787 self.config.w, self.config.h = width, height 1788 else: 1789 self.config.w = width 1790 self.config.x, self.config.y = window.get_position() 1791 1792 def on_notebook_resize(self, _widget, _event): 1793 if not self.current.resizing_columns: 1794 GLib.idle_add(self.header_save_column_widths) 1795 1796 def on_expand(self, _action): 1797 if not self.config.expanded: 1798 self.expander.set_expanded(False) 1799 self.on_expander_activate(None) 1800 self.expander.set_expanded(True) 1801 1802 def on_collapse(self, _action): 1803 if self.config.expanded: 1804 self.expander.set_expanded(True) 1805 self.on_expander_activate(None) 1806 self.expander.set_expanded(False) 1807 1808 def on_expander_activate(self, _expander): 1809 currheight = self.window.get_size()[1] 1810 self.config.expanded = False 1811 # Note that get_expanded() will return the state of the expander 1812 # before this current click 1813 window_about_to_be_expanded = not self.expander.get_expanded() 1814 if window_about_to_be_expanded: 1815 if self.window.get_size()[1] == self.config.h: 1816 # For WMs like ion3, the app will not actually resize 1817 # when in collapsed mode, so prevent the waiting 1818 # of the player to expand from happening: 1819 skip_size_check = True 1820 else: 1821 skip_size_check = False 1822 if self.config.show_statusbar: 1823 self.statusbar.show() 1824 self.notebook.show_all() 1825 if self.config.show_statusbar: 1826 ui.show(self.statusbar) 1827 else: 1828 ui.hide(self.statusbar) 1829 self.notebook.hide() 1830 if not self.status_is_play_or_pause(): 1831 if window_about_to_be_expanded: 1832 self.cursonglabel2.set_text(_('Click to collapse')) 1833 else: 1834 self.cursonglabel2.set_text(_('Click to expand')) 1835 # Now we wait for the height of the player to increase, so that 1836 # we know the list is visible. This is pretty hacky, but works. 1837 if window_about_to_be_expanded: 1838 if not skip_size_check: 1839 while self.window.get_size()[1] == currheight: 1840 while Gtk.events_pending(): 1841 Gtk.main_iteration() 1842 # Notebook is visible, now resize: 1843 self.window.resize(self.config.w, self.config.h) 1844 else: 1845 self.window.resize(self.config.w, 1) 1846 1847 if window_about_to_be_expanded: 1848 self.config.expanded = True 1849 if self.status_is_play_or_pause(): 1850 GLib.idle_add(self.current.center_song_in_list) 1851 1852 hints = Gdk.Geometry() 1853 hints.min_height = -1 1854 hints.max_height = -1 1855 hints.min_width = -1 1856 hints.max_width = -1 1857 self.window.set_geometry_hints(self.window, hints, Gdk.WindowHints.USER_SIZE) 1858 1859 # Put focus to the notebook: 1860 self.on_notebook_page_change(self.notebook, 0, 1861 self.notebook.get_current_page()) 1862 1863 # This callback allows the user to seek to a specific portion of the song 1864 def on_progressbar_press(self, _widget, event): 1865 if event.button == 1: 1866 if self.status_is_play_or_pause(): 1867 at, length = [int(c) for c in self.status['time'].split(':')] 1868 try: 1869 pbsize = self.progressbar.get_allocation() 1870 if misc.is_lang_rtl(self.window): 1871 seektime = int( 1872 ((pbsize.width - event.x) / pbsize.width) * length) 1873 else: 1874 seektime = int((event.x / pbsize.width) * length) 1875 self.seek(int(self.status['song']), seektime) 1876 except: 1877 pass 1878 return True 1879 1880 def on_progressbar_scroll(self, _widget, event): 1881 if self.status_is_play_or_pause(): 1882 try: 1883 GLib.source_remove(self.seekidle) 1884 except: 1885 pass 1886 self.seekidle = GLib.idle_add(self._seek_when_idle, event.direction) 1887 return True 1888 1889 def _seek_when_idle(self, direction): 1890 at, _length = [int(c) for c in self.status['time'].split(':')] 1891 try: 1892 if direction == Gdk.ScrollDirection.UP: 1893 seektime = max(0, at + 5) 1894 elif direction == Gdk.ScrollDirection.DOWN: 1895 seektime = min(self.songinfo.time, at - 5) 1896 self.seek(int(self.status['song']), seektime) 1897 except: 1898 pass 1899 1900 def on_lyrics_search(self, _event): 1901 artist = self.songinfo.artist or '' 1902 title = self.songinfo.title or '' 1903 if not self.lyrics_search_dialog: 1904 self.lyrics_search_dialog = self.builder.get_object( 1905 'lyrics_search_dialog') 1906 artist_entry = self.builder.get_object('lyrics_search_artist_entry') 1907 artist_entry.set_text(artist) 1908 title_entry = self.builder.get_object('lyrics_search_title_entry') 1909 title_entry.set_text(title) 1910 self.lyrics_search_dialog.show_all() 1911 response = self.lyrics_search_dialog.run() 1912 if response == Gtk.ResponseType.ACCEPT: 1913 # Search for new lyrics: 1914 self.info.get_lyrics_start( 1915 artist_entry.get_text(), 1916 title_entry.get_text(), 1917 artist, 1918 title, 1919 os.path.dirname(self.songinfo.file), 1920 force_fetch=True) 1921 1922 self.lyrics_search_dialog.hide() 1923 1924 def mpd_shuffle(self, _action): 1925 if self.conn: 1926 if not self.status or self.status['playlistlength'] == '0': 1927 return 1928 ui.change_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH)) 1929 while Gtk.events_pending(): 1930 Gtk.main_iteration() 1931 self.mpd.shuffle() 1932 1933 def mpd_shuffle_albums(self, _action): 1934 """Shuffle by albums. 1935 1936 Shuffle songs from the current playlist by their albums, while keeping 1937 the related songs together. 1938 """ 1939 1940 if not self.conn or \ 1941 not self.status or \ 1942 self.status['playlistlength'] == '0': 1943 return 1944 1945 albums = collections.defaultdict(lambda: []) 1946 1947 # Fetch albums 1948 for song in self.mpd.playlistinfo(): 1949 if "album" in song: 1950 k = song.album 1951 else: 1952 # We consider a song without album info to be a one song 1953 # album and use the title as album name. 1954 if 'title' in song: 1955 k = song.title 1956 else: 1957 # If there is not even a title in the song we ignore it, 1958 # which which will cause it to end up at the end of the 1959 # playlist, during shuffling. 1960 continue 1961 albums[k].append(song) 1962 1963 album_names = list(albums.keys()) 1964 random.shuffle(album_names) 1965 for album in album_names: 1966 for song in sorted(albums[album], key=operator.attrgetter('track'), 1967 reverse=True): 1968 self.mpd.moveid(song["id"], 0) 1969 1970 def on_menu_popup(self, _widget): 1971 self.update_menu_visibility() 1972 GLib.idle_add(self.mainmenu.popup, None, None, None, None, 3, 0) 1973 1974 def on_updatedb(self, _action): 1975 if self.conn: 1976 if self.library.search_visible(): 1977 self.library.on_search_end(None) 1978 self.mpd.update('/') # XXX we should pass a list here! 1979 self.mpd_update_queued = True 1980 1981 def on_updatedb_shortcut(self, _action): 1982 # If no songs selected, update view. Otherwise update 1983 # selected items. 1984 if self.library.not_parent_is_selected(): 1985 self.on_updatedb_path(True) 1986 else: 1987 self.on_updatedb_path(False) 1988 1989 def on_updatedb_path(self, selected_only): 1990 if self.conn and self.current_tab == self.TAB_LIBRARY: 1991 if self.library.search_visible(): 1992 self.library.on_search_end(None) 1993 filenames = self.library.get_path_child_filenames(True, 1994 selected_only) 1995 if len(filenames) > 0: 1996 self.mpd.update(filenames) 1997 self.mpd_update_queued = True 1998 1999 def on_image_activate(self, widget, event): 2000 self.window.handler_block(self.mainwinhandler) 2001 if event.button == 1 and widget == self.info_imagebox and \ 2002 self.artwork.have_last(): 2003 if not self.config.info_art_enlarged: 2004 self.info_imagebox.set_size_request(-1, -1) 2005 self.artwork.artwork_set_image_last() 2006 self.config.info_art_enlarged = True 2007 else: 2008 self.info_imagebox.set_size_request(152, -1) 2009 self.artwork.artwork_set_image_last() 2010 self.config.info_art_enlarged = False 2011 # Force a resize of the info labels, if needed: 2012 GLib.idle_add(self.on_notebook_resize, self.notebook, None) 2013 elif event.button == 1 and widget != self.info_imagebox: 2014 if self.config.expanded: 2015 if self.current_tab != self.TAB_INFO: 2016 self.img_clicked = True 2017 self.switch_to_tab_name(self.TAB_INFO) 2018 self.img_clicked = False 2019 else: 2020 self.switch_to_tab_name(self.last_tab) 2021 elif event.button == 3: 2022 artist = None 2023 album = None 2024 stream = None 2025 path_chooseimage = '/imagemenu/chooseimage_menu/' 2026 path_localimage = '/imagemenu/localimage_menu/' 2027 path_resetimage = '/imagemenu/resetimage_menu/' 2028 if self.status_is_play_or_pause(): 2029 self.UIManager.get_widget(path_chooseimage).show() 2030 self.UIManager.get_widget(path_localimage).show() 2031 artist = self.songinfo.artist 2032 album = self.songinfo.album 2033 stream = self.songinfo.name 2034 if not (artist or album or stream): 2035 self.UIManager.get_widget(path_localimage).hide() 2036 self.UIManager.get_widget(path_resetimage).hide() 2037 self.UIManager.get_widget(path_chooseimage).hide() 2038 self.imagemenu.popup(None, None, None, None, event.button, event.time) 2039 GLib.timeout_add(50, self.on_image_activate_after) 2040 return False 2041 2042 def on_image_motion_cb(self, _widget, context, _x, _y, time): 2043 Gdk.drag_status(context, Gdk.DragAction.COPY, time) 2044 return True 2045 2046 def on_image_drop_cb(self, _widget, _context, _x, _y, selection, 2047 _info, _time): 2048 if not self.status_is_play_or_pause(): 2049 return 2050 2051 data = selection.get_data().strip().decode('utf-8') 2052 for uri in data.rsplit('\n'): 2053 uri = uri.rstrip('\r') 2054 if not uri: 2055 continue 2056 2057 extension = os.path.splitext( 2058 urllib.parse.urlparse(uri).path)[1][1:] 2059 if extension not in img.VALID_EXTENSIONS: 2060 self.logger.debug( 2061 "Hum, the URI at '%s' doesn't look like an image...", uri) 2062 continue 2063 2064 destination = artwork.artwork_path(self.songinfo, self.config) 2065 2066 self.logger.info("Trying to download '%s' (to '%s') ...", uri, 2067 destination) 2068 2069 f = Gio.File.new_for_uri(uri) 2070 f.load_contents_async( 2071 None, self.on_image_drop_load_contents_async_end, 2072 destination) 2073 2074 def on_image_drop_load_contents_async_end(self, file, result, destination): 2075 source = result.get_source_object().get_uri() 2076 try: 2077 success, contents, etag = file.load_contents_finish(result) 2078 except GLib.GError as e: 2079 self.logger.warning("Can't retrieve %s: %s", source, e) 2080 return 2081 2082 misc.create_dir(artwork.COVERS_TEMP_DIR) 2083 with tempfile.NamedTemporaryFile(dir=artwork.COVERS_TEMP_DIR, 2084 delete=False) as t: 2085 t.write(contents) 2086 2087 if not img.valid_image(t.name): 2088 self.logger.warning("Downloaded '%s', but it's not an image :(", 2089 source) 2090 misc.remove_file(t.name) 2091 return 2092 2093 os.rename(t.name, destination) 2094 self.artwork.artwork_update(True) 2095 2096 def album_return_artist_and_tracks(self): 2097 # Includes logic for Various Artists albums to determine 2098 # the tracks. 2099 datalist = [] 2100 album = self.songinfo.album or '' 2101 songs, _playtime, _num_songs = \ 2102 self.library.library_return_search_items(album=album) 2103 for song in songs: 2104 year = song.date or '' 2105 artist = song.artist or '' 2106 path = os.path.dirname(song.file) 2107 data = SongRecord(album=album, artist=artist, year=year, path=path) 2108 datalist.append(data) 2109 if len(datalist) > 0: 2110 datalist = misc.remove_list_duplicates(datalist, case=False) 2111 datalist = library.list_mark_various_artists_albums(datalist) 2112 if len(datalist) > 0: 2113 # Multiple albums with same name and year, choose the 2114 # right one. If we have a VA album, compare paths. Otherwise, 2115 # compare artists. 2116 for dataitem in datalist: 2117 if dataitem.artist.lower() == \ 2118 str(self.songinfo.artist or '').lower() \ 2119 or dataitem.artist == library.VARIOUS_ARTISTS \ 2120 and dataitem.path == \ 2121 os.path.dirname(self.songinfo.file): 2122 2123 datalist = [dataitem] 2124 break 2125 # Find all songs in album: 2126 retsongs = [] 2127 for song in songs: 2128 if (song.album or '').lower() == datalist[0].album.lower() \ 2129 and song.date == datalist[0].year \ 2130 and (datalist[0].artist == library.VARIOUS_ARTISTS \ 2131 or datalist[0].artist.lower() == \ 2132 (song.artist or '').lower()): 2133 retsongs.append(song) 2134 2135 return artist, retsongs 2136 else: 2137 return None, None 2138 2139 def album_return_artist_name(self): 2140 # Determine if album_name is a various artists album. 2141 if self.album_current_artist[0] == self.songinfo: 2142 return 2143 artist, _tracks = self.album_return_artist_and_tracks() 2144 if artist is not None: 2145 self.album_current_artist = [self.songinfo, artist] 2146 else: 2147 self.album_current_artist = [self.songinfo, ""] 2148 2149 def album_reset_artist(self): 2150 self.album_current_artist = [None, ""] 2151 2152 def on_image_activate_after(self): 2153 self.window.handler_unblock(self.mainwinhandler) 2154 2155 def update_preview(self, file_chooser, preview): 2156 filename = file_chooser.get_preview_filename() 2157 pixbuf = None 2158 try: 2159 pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(filename, 128, 128) 2160 except: 2161 pass 2162 if pixbuf is None: 2163 try: 2164 pixbuf = GdkPixbuf.PixbufAnimation.new_from_file(filename) 2165 pixbuf = pixbuf.get_static_image() 2166 width = pixbuf.get_width() 2167 height = pixbuf.get_height() 2168 if width > height: 2169 pixbuf = pixbuf.scale_simple( 2170 128, int(float(height) / width * 128), 2171 GdkPixbuf.InterpType.HYPER) 2172 else: 2173 pixbuf = pixbuf.scale_simple( 2174 int(float(width) / height * 128), 128, 2175 GdkPixbuf.InterpType.HYPER) 2176 except: 2177 pass 2178 if pixbuf is None: 2179 pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, 1, 8, 2180 128, 128) 2181 pixbuf.fill(0x00000000) 2182 preview.set_from_pixbuf(pixbuf) 2183 have_preview = True 2184 file_chooser.set_preview_widget_active(have_preview) 2185 del pixbuf 2186 self.call_gc_collect = True 2187 2188 def image_local(self, _widget): 2189 dialog = self.builder.get_object('local_artwork_dialog') 2190 2191 filefilter = Gtk.FileFilter() 2192 filefilter.set_name(_("Images")) 2193 filefilter.add_pixbuf_formats() 2194 dialog.add_filter(filefilter) 2195 2196 filefilter = Gtk.FileFilter() 2197 filefilter.set_name(_("All files")) 2198 filefilter.add_pattern("*") 2199 dialog.add_filter(filefilter) 2200 2201 preview = self.builder.get_object('local_art_preview_image') 2202 dialog.connect("update-preview", self.update_preview, preview) 2203 2204 stream = self.songinfo.name 2205 album = (self.songinfo.album or "").replace("/", "") 2206 artist = self.album_current_artist[1].replace("/", "") 2207 songdir = os.path.dirname(self.songinfo.file) 2208 currdir = os.path.join(self.config.current_musicdir, songdir) 2209 if self.config.art_location != consts.ART_LOCATION_HOMECOVERS: 2210 dialog.set_current_folder(currdir) 2211 2212 self.local_dest_filename = artwork.artwork_path(self.songinfo, 2213 self.config) 2214 dialog.show_all() 2215 response = dialog.run() 2216 2217 if response == Gtk.ResponseType.OK: 2218 filename = dialog.get_filenames()[0] 2219 # Copy file to covers dir: 2220 if self.local_dest_filename != filename: 2221 shutil.copyfile(filename, self.local_dest_filename) 2222 # And finally, set the image in the interface: 2223 self.artwork.artwork_update(True) 2224 # Force a resize of the info labels, if needed: 2225 GLib.idle_add(self.on_notebook_resize, self.notebook, None) 2226 dialog.hide() 2227 2228 def imagelist_append(self, elem): 2229 self.imagelist.append(elem) 2230 2231 def remotefilelist_append(self, elem): 2232 self.remotefilelist.append(elem) 2233 2234 def _init_choose_dialog(self): 2235 self.choose_dialog = self.builder.get_object('artwork_dialog') 2236 self.imagelist = self.builder.get_object('artwork_liststore') 2237 self.remote_artistentry = self.builder.get_object('artwork_artist_entry') 2238 self.remote_albumentry = self.builder.get_object('artwork_album_entry') 2239 self.image_widget = self.builder.get_object('artwork_iconview') 2240 refresh_button = self.builder.get_object('artwork_update_button') 2241 refresh_button.connect('clicked', self.image_remote_refresh, 2242 self.image_widget) 2243 self.remotefilelist = [] 2244 2245 def image_remote(self, _widget): 2246 if not self.choose_dialog: 2247 self._init_choose_dialog() 2248 2249 stream = self.songinfo.name 2250 self.remote_dest_filename = artwork.artwork_path(self.songinfo, 2251 self.config) 2252 album = self.songinfo.album or '' 2253 artist = self.album_current_artist[1] 2254 self.image_widget.connect('item-activated', self.image_remote_replace_cover, 2255 artist.replace("/", ""), album.replace("/", ""), 2256 stream) 2257 self.choose_dialog.connect('response', self.image_remote_response, 2258 self.image_widget, artist, album, stream) 2259 self.remote_artistentry.set_text(artist) 2260 self.remote_albumentry.set_text(album) 2261 self.allow_art_search = True 2262 self.chooseimage_visible = True 2263 self.image_remote_refresh(None, self.image_widget) 2264 self.choose_dialog.show_all() 2265 self.choose_dialog.run() 2266 2267 def image_remote_refresh(self, _entry, imagewidget): 2268 if not self.allow_art_search: 2269 return 2270 self.allow_art_search = False 2271 self.artwork.artwork_stop_update() 2272 while self.artwork.artwork_is_downloading_image(): 2273 while Gtk.events_pending(): 2274 Gtk.main_iteration() 2275 self.imagelist.clear() 2276 imagewidget.set_text_column(-1) 2277 imagewidget.set_model(self.imagelist) 2278 imagewidget.set_pixbuf_column(1) 2279 imagewidget.grab_focus() 2280 ui.change_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH)) 2281 thread = threading.Thread(target=self._image_remote_refresh, 2282 args=(imagewidget, None)) 2283 thread.name = "ImageRemoteRefresh" 2284 thread.daemon = True 2285 thread.start() 2286 2287 def _image_remote_refresh(self, imagewidget, _ignore): 2288 self.artwork.stop_art_update = False 2289 # Retrieve all images from cover plugins 2290 artist_search = self.remote_artistentry.get_text() 2291 album_search = self.remote_albumentry.get_text() 2292 if len(artist_search) == 0 and len(album_search) == 0: 2293 GLib.idle_add(self.image_remote_no_tag_found, imagewidget) 2294 return 2295 filename = os.path.join(artwork.COVERS_TEMP_DIR, "<imagenum>.jpg") 2296 misc.remove_dir_recursive(os.path.dirname(filename)) 2297 misc.create_dir(os.path.dirname(filename)) 2298 imgfound = self.artwork.artwork_download_img_to_file(artist_search, 2299 album_search, 2300 filename, True) 2301 ui.change_cursor(None) 2302 if self.chooseimage_visible: 2303 if not imgfound: 2304 GLib.idle_add(self.image_remote_no_covers_found, imagewidget) 2305 self.call_gc_collect = True 2306 2307 def image_remote_no_tag_found(self, imagewidget): 2308 self.image_remote_warning(imagewidget, 2309 _("No artist or album name found.")) 2310 2311 def image_remote_no_covers_found(self, imagewidget): 2312 self.image_remote_warning(imagewidget, _("No cover art found.")) 2313 2314 def image_remote_warning(self, imagewidget, msgstr): 2315 liststore = Gtk.ListStore(int, str) 2316 liststore.append([0, msgstr]) 2317 imagewidget.set_pixbuf_column(-1) 2318 imagewidget.set_model(liststore) 2319 imagewidget.set_text_column(1) 2320 ui.change_cursor(None) 2321 self.allow_art_search = True 2322 2323 def image_remote_response(self, dialog, response_id, imagewidget, artist, 2324 album, stream): 2325 self.artwork.artwork_stop_update() 2326 if response_id == Gtk.ResponseType.ACCEPT: 2327 try: 2328 self.image_remote_replace_cover( 2329 imagewidget, imagewidget.get_selected_items()[0], artist, 2330 album, stream) 2331 # Force a resize of the info labels, if needed: 2332 GLib.idle_add(self.on_notebook_resize, self.notebook, None) 2333 except: 2334 dialog.hide() 2335 else: 2336 dialog.hide() 2337 ui.change_cursor(None) 2338 self.chooseimage_visible = False 2339 2340 def image_remote_replace_cover(self, _iconview, path, _artist, _album, 2341 _stream): 2342 self.artwork.artwork_stop_update() 2343 image_num = path.get_indices()[0] 2344 if len(self.remotefilelist) > 0: 2345 filename = self.remotefilelist[image_num] 2346 if os.path.exists(filename): 2347 shutil.move(filename, self.remote_dest_filename) 2348 # And finally, set the image in the interface: 2349 self.artwork.artwork_update(True) 2350 # Clean up.. 2351 misc.remove_dir_recursive(os.path.dirname(filename)) 2352 self.chooseimage_visible = False 2353 self.choose_dialog.hide() 2354 while self.artwork.artwork_is_downloading_image(): 2355 while Gtk.events_pending(): 2356 Gtk.main_iteration() 2357 2358 def header_save_column_widths(self): 2359 if not self.config.withdrawn and self.config.expanded: 2360 windowwidth = self.window.get_allocation().width 2361 if windowwidth <= 10 or self.current.columns[0].get_width() <= 10: 2362 # Make sure we only set self.config.columnwidths if 2363 # self.current has its normal allocated width: 2364 return 2365 notebookwidth = self.notebook.get_allocation().width 2366 treewidth = 0 2367 for i, column in enumerate(self.current.columns): 2368 colwidth = column.get_width() 2369 treewidth += colwidth 2370 if i == len(self.current.columns)-1 \ 2371 and treewidth <= windowwidth: 2372 self.config.columnwidths[i] = min(colwidth, 2373 column.get_fixed_width()) 2374 else: 2375 self.config.columnwidths[i] = colwidth 2376 self.current.resizing_columns = False 2377 2378 def on_tray_click(self, _widget, event): 2379 # Clicking on a system tray icon: 2380 # Left button shows/hides window(s) 2381 if event.button == 1 and not self.ignore_toggle_signal: 2382 # This prevents the user clicking twice in a row quickly 2383 # and having the second click not revert to the intial 2384 # state 2385 self.ignore_toggle_signal = True 2386 path_showmenu = '/traymenu/showmenu' 2387 prev_state = self.UIManager.get_widget(path_showmenu).get_active() 2388 self.UIManager.get_widget(path_showmenu).set_active(not prev_state) 2389 2390 # For some reason, self.window.window is not defined if 2391 # mpd is not running and sonata is started with 2392 # self.config.withdrawn = True 2393 window = self.window.get_window() 2394 if window and not (window.get_state() & Gdk.WindowState.WITHDRAWN): 2395 # Window is not withdrawn and is active (has toplevel focus): 2396 self.withdraw_app() 2397 else: 2398 self.withdraw_app_undo() 2399 # This prevents the tooltip from popping up again until the user 2400 # leaves and enters the trayicon again 2401 # if self.traytips.notif_handler is None and 2402 # self.traytips.notif_handler != -1: 2403 # self.traytips._remove_timer() 2404 GLib.timeout_add(100, self.tooltip_set_ignore_toggle_signal_false) 2405 2406 elif event.button == 2: # Middle button will play/pause 2407 if self.conn: 2408 self.mpd_pp(None) 2409 2410 elif event.button == 3: # Right button pops up menu 2411 self.traymenu.popup(None, None, None, None, event.button, event.time) 2412 2413 return False 2414 2415 def on_traytips_press(self, _widget, _event): 2416 if self.traytips.get_property('visible'): 2417 self.traytips._remove_timer() 2418 2419 def withdraw_app_undo(self): 2420 desktop = Gdk.get_default_root_window() 2421 # convert window coordinates to current workspace so sonata 2422 # will always appear on the current workspace with the same 2423 # position as it was before (may be on the other workspace) 2424 self.config.x %= desktop.get_width() 2425 self.config.y %= desktop.get_height() 2426 self.window.move(self.config.x, self.config.y) 2427 if not self.config.expanded: 2428 self.notebook.set_no_show_all(True) 2429 self.statusbar.set_no_show_all(True) 2430 self.window.show_all() 2431 self.notebook.set_no_show_all(False) 2432 self.config.withdrawn = False 2433 self.UIManager.get_widget('/traymenu/showmenu').set_active(True) 2434 self.withdraw_app_undo_present_and_focus() 2435 2436 def withdraw_app_undo_present_and_focus(self): 2437 # Helps to raise the window (useful against focus stealing prevention) 2438 self.window.present() 2439 self.window.grab_focus() 2440 if self.config.sticky: 2441 self.window.stick() 2442 if self.config.ontop: 2443 self.window.set_keep_above(True) 2444 2445 def withdraw_app(self): 2446 if self.tray_icon.is_available(): 2447 # Save the playlist column widths before withdrawing the app. 2448 # Otherwise we will not be able to correctly save the column 2449 # widths if the user quits sonata while it is withdrawn. 2450 self.header_save_column_widths() 2451 self.window.hide() 2452 self.config.withdrawn = True 2453 self.UIManager.get_widget('/traymenu/showmenu').set_active(False) 2454 2455 def on_withdraw_app_toggle(self, _action): 2456 if self.ignore_toggle_signal: 2457 return 2458 self.ignore_toggle_signal = True 2459 if self.UIManager.get_widget('/traymenu/showmenu').get_active(): 2460 self.withdraw_app_undo() 2461 else: 2462 self.withdraw_app() 2463 GLib.timeout_add(500, self.tooltip_set_ignore_toggle_signal_false) 2464 2465 def tooltip_set_ignore_toggle_signal_false(self): 2466 self.ignore_toggle_signal = False 2467 2468 # Change volume on mousewheel over systray icon: 2469 def on_tray_scroll(self, widget, event): 2470 direction = event.get_scroll_direction()[1] 2471 if self.conn: 2472 if direction == Gdk.ScrollDirection.UP: 2473 self.on_volume_raise() 2474 elif direction == Gdk.ScrollDirection.DOWN: 2475 self.on_volume_lower() 2476 2477 def switch_to_tab_name(self, tab_name): 2478 self.notebook.set_current_page(self.notebook_get_tab_num(self.notebook, 2479 tab_name)) 2480 2481 def switch_to_tab_num(self, tab_num): 2482 vis_tabnum = self.notebook_get_visible_tab_num(self.notebook, tab_num) 2483 if vis_tabnum != -1: 2484 self.notebook.set_current_page(vis_tabnum) 2485 2486 def switch_to_next_tab(self, _action): 2487 self.notebook.next_page() 2488 2489 def switch_to_prev_tab(self, _action): 2490 self.notebook.prev_page() 2491 2492 # Volume control 2493 def on_volume_lower(self, _action=None): 2494 new_volume = int(self.volumebutton.get_value()) - 5 2495 self.volumebutton.set_value(new_volume) 2496 2497 def on_volume_raise(self, _action=None): 2498 new_volume = int(self.volumebutton.get_value()) + 5 2499 self.volumebutton.set_value(new_volume) 2500 2501 def on_volume_change(self, _button, new_volume): 2502 self.mpd.setvol(int(new_volume)) 2503 2504 def mpd_pp(self, _widget, _key=None): 2505 if self.conn and self.status: 2506 if self.status['state'] in ('stop', 'pause'): 2507 self.mpd.play() 2508 elif self.status['state'] == 'play': 2509 self.mpd.pause('1') 2510 self.iterate_now() 2511 2512 def mpd_stop(self, _widget, _key=None): 2513 if self.conn: 2514 self.mpd.stop() 2515 self.iterate_now() 2516 2517 def mpd_prev(self, _widget, _key=None): 2518 if self.conn: 2519 if self.status_is_play_or_pause(): 2520 # Try to rewind the song if we are advanced enough... 2521 at, length = [int(c) for c in self.status['time'].split(':')] 2522 if at >= consts.PREV_TRACK_RESTART: 2523 self.seek(int(self.status['song']), 0) 2524 return 2525 2526 # otherwise, just go to the previous song 2527 self.mpd.previous() 2528 self.iterate_now() 2529 2530 def mpd_next(self, _widget, _key=None): 2531 if self.conn: 2532 self.mpd.next() 2533 self.iterate_now() 2534 2535 def on_remove(self, _widget): 2536 if self.conn: 2537 model = None 2538 while Gtk.events_pending(): 2539 Gtk.main_iteration() 2540 if self.current_tab == self.TAB_CURRENT: 2541 self.current.on_remove() 2542 elif self.current_tab == self.TAB_PLAYLISTS: 2543 treeviewsel = self.playlists_selection 2544 model, selected = treeviewsel.get_selected_rows() 2545 if ui.show_msg(self.window, 2546 ngettext("Delete the selected playlist?", 2547 "Delete the selected playlists?", 2548 int(len(selected))), 2549 ngettext("Delete Playlist", 2550 "Delete Playlists", 2551 int(len(selected))), 2552 'deletePlaylist', Gtk.ButtonsType.YES_NO) == \ 2553 Gtk.ResponseType.YES: 2554 iters = [model.get_iter(path) for path in selected] 2555 for i in iters: 2556 self.mpd.rm(misc.unescape_html( 2557 self.playlistsdata.get_value(i, 1))) 2558 self.playlists.populate() 2559 elif self.current_tab == self.TAB_STREAMS: 2560 treeviewsel = self.streams_selection 2561 model, selected = treeviewsel.get_selected_rows() 2562 if ui.show_msg(self.window, 2563 ngettext("Delete the selected stream?", 2564 "Delete the selected streams?", 2565 int(len(selected))), 2566 ngettext("Delete Stream", 2567 "Delete Streams", 2568 int(len(selected))), 2569 'deleteStreams', Gtk.ButtonsType.YES_NO) == \ 2570 Gtk.ResponseType.YES: 2571 iters = [model.get_iter(path) for path in selected] 2572 for i in iters: 2573 stream_removed = False 2574 for j in range(len(self.config.stream_names)): 2575 if not stream_removed: 2576 if self.streamsdata.get_value(i, 1) == \ 2577 misc.escape_html( 2578 self.config.stream_names[j]): 2579 self.config.stream_names.pop(j) 2580 self.config.stream_uris.pop(j) 2581 stream_removed = True 2582 self.streams.populate() 2583 self.iterate_now() 2584 # Attempt to retain selection in the vicinity.. 2585 if model and len(model) > 0: 2586 try: 2587 # Use top row in selection... 2588 selrow = 999999 2589 for row in selected: 2590 if row[0] < selrow: 2591 selrow = row[0] 2592 if selrow >= len(model): 2593 selrow = len(model)-1 2594 treeviewsel.select_path(selrow) 2595 except: 2596 pass 2597 2598 def mpd_clear(self, _widget): 2599 if self.conn: 2600 self.mpd.clear() 2601 self.iterate_now() 2602 2603 def on_repeat_playlist_clicked(self, widget): 2604 if self.conn: 2605 is_active = widget.get_active() 2606 self.mpd.repeat(int(is_active or self.repeatsongmenu.get_active())) 2607 self.mpd.single(1 - is_active) 2608 2609 def on_repeat_song_clicked(self, widget): 2610 if self.conn: 2611 is_active = int(widget.get_active()) 2612 is_repeat_playlist_active = int(self.repeatplaylistmenu.get_active()) 2613 self.mpd.single(is_active) 2614 self.mpd.repeat(int(is_active or is_repeat_playlist_active)) 2615 2616 def on_random_clicked(self, widget): 2617 if self.conn: 2618 self.mpd.random(int(widget.get_active())) 2619 2620 def on_consume_clicked(self, widget): 2621 if self.conn: 2622 self.mpd.consume(int(widget.get_active())) 2623 2624 def setup_prefs_callbacks(self): 2625 extras = preferences.Extras_cbs 2626 extras.popuptimes = self.popuptimes 2627 extras.notif_toggled = self.prefs_notif_toggled 2628 extras.crossfade_toggled = self.prefs_crossfade_toggled 2629 extras.crossfade_changed = self.prefs_crossfade_changed 2630 2631 display = preferences.Display_cbs 2632 display.stylized_toggled = self.prefs_stylized_toggled 2633 display.art_toggled = self.prefs_art_toggled 2634 display.playback_toggled = self.prefs_playback_toggled 2635 display.progress_toggled = self.prefs_progress_toggled 2636 display.statusbar_toggled = self.prefs_statusbar_toggled 2637 display.lyrics_toggled = self.prefs_lyrics_toggled 2638 # TODO: the tray icon object has not been build yet, so we don't know 2639 # if the tray icon will be available at this time. 2640 # We should find a way to update this when the tray icon will be 2641 # initialized. 2642 display.trayicon_available = True 2643 2644 behavior = preferences.Behavior_cbs 2645 behavior.trayicon_toggled = self.prefs_trayicon_toggled 2646 behavior.sticky_toggled = self.prefs_sticky_toggled 2647 behavior.ontop_toggled = self.prefs_ontop_toggled 2648 behavior.decorated_toggled = self.prefs_decorated_toggled 2649 behavior.infofile_changed = self.prefs_infofile_changed 2650 2651 format = preferences.Format_cbs 2652 format.currentoptions_changed = self.prefs_currentoptions_changed 2653 format.libraryoptions_changed = self.prefs_libraryoptions_changed 2654 format.titleoptions_changed = self.prefs_titleoptions_changed 2655 format.currsongoptions1_changed = self.prefs_currsongoptions1_changed 2656 format.currsongoptions2_changed = self.prefs_currsongoptions2_changed 2657 2658 def on_prefs(self, _widget): 2659 preferences.Behavior_cbs.trayicon_in_use = self.tray_icon.is_visible() 2660 self.preferences.on_prefs_real() 2661 2662 def prefs_currentoptions_changed(self, entry, _event): 2663 if self.config.currentformat != entry.get_text(): 2664 self.config.currentformat = entry.get_text() 2665 self.current.initialize_columns() 2666 2667 def prefs_libraryoptions_changed(self, entry, _event): 2668 if self.config.libraryformat != entry.get_text(): 2669 self.config.libraryformat = entry.get_text() 2670 self.library.library_browse(root=self.config.wd) 2671 2672 def prefs_titleoptions_changed(self, entry, _event): 2673 if self.config.titleformat != entry.get_text(): 2674 self.config.titleformat = entry.get_text() 2675 self.update_wintitle() 2676 2677 def prefs_currsongoptions1_changed(self, entry, _event): 2678 if self.config.currsongformat1 != entry.get_text(): 2679 self.config.currsongformat1 = entry.get_text() 2680 self.update_cursong() 2681 2682 def prefs_currsongoptions2_changed(self, entry, _event): 2683 if self.config.currsongformat2 != entry.get_text(): 2684 self.config.currsongformat2 = entry.get_text() 2685 self.update_cursong() 2686 2687 def prefs_ontop_toggled(self, button): 2688 self.config.ontop = button.get_active() 2689 self.window.set_keep_above(self.config.ontop) 2690 2691 def prefs_sticky_toggled(self, button): 2692 self.config.sticky = button.get_active() 2693 if self.config.sticky: 2694 self.window.stick() 2695 else: 2696 self.window.unstick() 2697 2698 def prefs_decorated_toggled(self, button, prefs_window): 2699 self.config.decorated = not button.get_active() 2700 if self.config.decorated != self.window.get_decorated(): 2701 self.withdraw_app() 2702 self.window.set_decorated(self.config.decorated) 2703 self.withdraw_app_undo() 2704 prefs_window.present() 2705 2706 def prefs_infofile_changed(self, entry, _event): 2707 if self.config.infofile_path != entry.get_text(): 2708 self.config.infofile_path = os.path.expanduser(entry.get_text()) 2709 if self.config.use_infofile: 2710 self.update_infofile() 2711 2712 def prefs_crossfade_changed(self, crossfade_spin): 2713 crossfade_value = crossfade_spin.get_value_as_int() 2714 self.mpd.crossfade(crossfade_value) 2715 2716 def prefs_crossfade_toggled(self, button, crossfade_spin): 2717 crossfade_value = crossfade_spin.get_value_as_int() 2718 if button.get_active(): 2719 self.mpd.crossfade(crossfade_value) 2720 else: 2721 self.mpd.crossfade(0) 2722 2723 def prefs_playback_toggled(self, button): 2724 self.config.show_playback = button.get_active() 2725 func = 'show' if self.config.show_playback else 'hide' 2726 for widget in [self.prevbutton, self.ppbutton, self.stopbutton, 2727 self.nextbutton, self.volumebutton]: 2728 getattr(ui, func)(widget) 2729 2730 def prefs_progress_toggled(self, button): 2731 self.config.show_progress = button.get_active() 2732 func = ui.show if self.config.show_progress else ui.hide 2733 for widget in [self.progressbox, self.tray_progressbar]: 2734 func(widget) 2735 2736 # FIXME move into prefs or elsewhere? 2737 def prefs_art_toggled(self, button, art_prefs): 2738 button_active = button.get_active() 2739 art_prefs.set_sensitive(button_active) 2740 2741 #art_hbox2.set_sensitive(button_active) 2742 #art_stylized.set_sensitive(button_active) 2743 if button_active: 2744 self.traytips.set_size_request(self.notification_width, -1) 2745 self.artwork.artwork_set_default_icon() 2746 for widget in [self.imageeventbox, self.info_imagebox, 2747 self.tray_album_image]: 2748 widget.set_no_show_all(False) 2749 if widget is self.tray_album_image: 2750 if self.status_is_play_or_pause(): 2751 widget.show_all() 2752 else: 2753 widget.show_all() 2754 self.config.show_covers = True 2755 self.update_cursong() 2756 self.artwork.artwork_update() 2757 else: 2758 self.traytips.set_size_request(self.notification_width-100, -1) 2759 for widget in [self.imageeventbox, self.info_imagebox, 2760 self.tray_album_image]: 2761 widget.hide() 2762 self.config.show_covers = False 2763 self.update_cursong() 2764 2765 # Force a resize of the info labels, if needed: 2766 GLib.idle_add(self.on_notebook_resize, self.notebook, None) 2767 2768 def prefs_stylized_toggled(self, button): 2769 self.config.covers_type = button.get_active() 2770 self.library.library_browse(root=self.config.wd) 2771 self.artwork.artwork_update(True) 2772 2773 def prefs_lyrics_toggled(self, button, lyrics_hbox): 2774 self.config.show_lyrics = button.get_active() 2775 lyrics_hbox.set_sensitive(self.config.show_lyrics) 2776 self.info.show_lyrics_updated() 2777 if self.config.show_lyrics: 2778 self.info_update(True) 2779 2780 def prefs_statusbar_toggled(self, button): 2781 self.config.show_statusbar = button.get_active() 2782 if self.config.show_statusbar: 2783 self.statusbar.set_no_show_all(False) 2784 if self.config.expanded: 2785 self.statusbar.show_all() 2786 else: 2787 ui.hide(self.statusbar) 2788 self.update_statusbar() 2789 2790 def prefs_notif_toggled(self, button, notifhbox): 2791 self.config.show_notification = button.get_active() 2792 notifhbox.set_sensitive(self.config.show_notification) 2793 if self.config.show_notification: 2794 self.on_currsong_notify() 2795 else: 2796 try: 2797 GLib.source_remove(self.traytips.notif_handler) 2798 except: 2799 pass 2800 self.traytips.hide() 2801 2802 def prefs_trayicon_toggled(self, button, minimize): 2803 # Note that we update the sensitivity of the minimize 2804 # CheckButton to reflect if the trayicon is visible. 2805 if button.get_active(): 2806 self.config.show_trayicon = True 2807 self.tray_icon.show() 2808 minimize.set_sensitive(True) 2809 else: 2810 self.config.show_trayicon = False 2811 minimize.set_sensitive(False) 2812 self.tray_icon.hide() 2813 2814 def seek(self, song, seektime): 2815 self.mpd.seek(song, seektime) 2816 self.iterate_now() 2817 2818 def on_link_click(self, linktype): 2819 if linktype in ['artist', 'album']: 2820 query = getattr(self.songinfo, linktype) or '' 2821 try: 2822 wikipedia_locale = locale.getdefaultlocale()[0].split('_')[0] 2823 except Exception as e: 2824 self.logger.debug("Can't find locale for Wikipedia: %s", e) 2825 wikipedia_locale = 'en' 2826 2827 url = "http://{locale}.wikipedia.org/wiki/Special:Search/{query}"\ 2828 .format(locale=wikipedia_locale, 2829 query=urllib.parse.quote(query)) 2830 browser_loaded = misc.browser_load( 2831 url, self.config.url_browser, self.window) 2832 2833 if not browser_loaded: 2834 ui.show_msg(self.window, 2835 _('Unable to launch a suitable browser.'), 2836 _('Launch Browser'), 'browserLoadError', 2837 Gtk.ButtonsType.CLOSE) 2838 2839 elif linktype == 'edit': 2840 if self.songinfo: 2841 self.on_tags_edit(None) 2842 elif linktype == 'search': 2843 self.on_lyrics_search(None) 2844 2845 def on_tab_click(self, _widget, event): 2846 if event.button == 3: 2847 self.notebookmenu.popup(None, None, None, None, event.button, event.time) 2848 return True 2849 2850 def notebook_get_tab_num(self, notebook, tabname): 2851 for tab in range(notebook.get_n_pages()): 2852 if self.notebook_get_tab_text(self.notebook, tab) == tabname: 2853 return tab 2854 2855 def notebook_tab_is_visible(self, notebook, tabname): 2856 tab = self.notebook.get_children()[self.notebook_get_tab_num(notebook, 2857 tabname)] 2858 return tab.get_property('visible') 2859 2860 def notebook_get_visible_tab_num(self, notebook, tab_num): 2861 # Get actual tab number for visible tab_num. If there is not 2862 # a visible tab for tab_num, return -1.\ 2863 curr_tab = -1 2864 for tab in range(notebook.get_n_pages()): 2865 if notebook.get_children()[tab].get_property('visible'): 2866 curr_tab += 1 2867 if curr_tab == tab_num: 2868 return tab 2869 return -1 2870 2871 def notebook_get_tab_text(self, notebook, tab_num): 2872 tab = notebook.get_children()[tab_num] 2873 2874 # FIXME when new UI is done, the top branch expression wins 2875 if notebook.get_tab_label(tab).get_children and \ 2876 len(notebook.get_tab_label(tab).get_children()) is 2: 2877 child = notebook.get_tab_label(tab).get_children()[1] 2878 else: 2879 child = notebook.get_tab_label(tab).get_child().get_children()[1] 2880 return child.get_text() 2881 2882 def on_notebook_page_change(self, _notebook, _page, page_num): 2883 self.current_tab = self.notebook_get_tab_text(self.notebook, page_num) 2884 to_focus = self.tabname2focus.get(self.current_tab, None) 2885 if to_focus: 2886 GLib.idle_add(to_focus.grab_focus) 2887 2888 GLib.idle_add(self.update_menu_visibility) 2889 if not self.img_clicked: 2890 self.last_tab = self.current_tab 2891 2892 def on_window_click(self, _widget, event): 2893 if event.button == 3: 2894 self.menu_popup(self.window, event) 2895 2896 def menu_popup(self, widget, event): 2897 if widget == self.window: 2898 # Prevent the popup from statusbar (if present) 2899 height = event.get_coords()[1] 2900 if height > self.notebook.get_allocation().height: 2901 return 2902 if event.button == 3: 2903 self.update_menu_visibility(True) 2904 GLib.idle_add(self.mainmenu.popup, None, None, None, None, 2905 event.button, event.time) 2906 2907 def on_tab_toggle(self, toggleAction): 2908 name = toggleAction.get_name() 2909 label = toggleAction.get_label() 2910 if not toggleAction.get_active(): 2911 # Make sure we aren't hiding the last visible tab: 2912 num_tabs_vis = 0 2913 for tab in self.notebook.get_children(): 2914 if tab.get_property('visible'): 2915 num_tabs_vis += 1 2916 if num_tabs_vis == 1: 2917 # Keep menu item checking and exit.. 2918 toggleAction.set_active(True) 2919 return 2920 # Store value: 2921 if label == self.TAB_CURRENT: 2922 self.config.current_tab_visible = toggleAction.get_active() 2923 elif label == self.TAB_LIBRARY: 2924 self.config.library_tab_visible = toggleAction.get_active() 2925 elif label == self.TAB_PLAYLISTS: 2926 self.config.playlists_tab_visible = toggleAction.get_active() 2927 elif label == self.TAB_STREAMS: 2928 self.config.streams_tab_visible = toggleAction.get_active() 2929 elif label == self.TAB_INFO: 2930 self.config.info_tab_visible = toggleAction.get_active() 2931 # Hide/show: 2932 tabnum = self.notebook_get_tab_num(self.notebook, self.tabid2name[name]) 2933 if toggleAction.get_active(): 2934 ui.show(self.notebook.get_children()[tabnum]) 2935 else: 2936 ui.hide(self.notebook.get_children()[tabnum]) 2937 2938 def on_library_search_shortcut(self, _event): 2939 # Ensure library tab is visible 2940 if not self.notebook_tab_is_visible(self.notebook, self.TAB_LIBRARY): 2941 return 2942 if self.current_tab != self.TAB_LIBRARY: 2943 self.switch_to_tab_name(self.TAB_LIBRARY) 2944 if self.library.search_visible(): 2945 self.library.on_search_end(None) 2946 self.library.libsearchfilter_set_focus() 2947 2948 def update_menu_visibility(self, show_songinfo_only=False): 2949 if show_songinfo_only or not self.config.expanded: 2950 for menu in ['add', 'replace', 'playafter', 'rename', 'rm', 'pl', \ 2951 'remove', 'clear', 'update', 'new', 'edit', 2952 'sort', 'tag']: 2953 self.UIManager.get_widget('/mainmenu/' + menu + 'menu/').hide() 2954 return 2955 elif self.current_tab == self.TAB_CURRENT: 2956 # XXX this should move to the current.py module 2957 if not self.current.is_empty(): 2958 if self.current_selection.count_selected_rows() > 0: 2959 for menu in ['remove', 'tag']: 2960 self.UIManager.get_widget('/mainmenu/%smenu/' % \ 2961 (menu,)).show() 2962 else: 2963 for menu in ['remove', 'tag']: 2964 self.UIManager.get_widget('/mainmenu/%smenu/' % \ 2965 (menu,)).hide() 2966 if not self.current.filterbox_visible: 2967 for menu in ['clear', 'pl', 'sort']: 2968 self.UIManager.get_widget('/mainmenu/%smenu/' % \ 2969 (menu,)).show() 2970 else: 2971 for menu in ['clear', 'pl', 'sort']: 2972 self.UIManager.get_widget('/mainmenu/%smenu/' % \ 2973 (menu,)).hide() 2974 2975 else: 2976 for menu in ['clear', 'pl', 'sort', 'remove', 'tag']: 2977 2978 self.UIManager.get_widget('/mainmenu/%smenu/' % \ 2979 (menu,)).hide() 2980 2981 for menu in ['add', 'replace', 'playafter', 'rename', 'rm', \ 2982 'update', 'new', 'edit']: 2983 self.UIManager.get_widget('/mainmenu/' + menu + 'menu/').hide() 2984 2985 elif self.current_tab == self.TAB_LIBRARY: 2986 if len(self.librarydata) > 0: 2987 path_update = '/mainmenu/updatemenu/updateselectedmenu/' 2988 if self.library_selection.count_selected_rows() > 0: 2989 for menu in ['add', 'replace', 'playafter', 'tag', 'pl']: 2990 self.UIManager.get_widget('/mainmenu/%smenu/' % \ 2991 (menu,)).show() 2992 2993 self.UIManager.get_widget(path_update).show() 2994 2995 else: 2996 for menu in ['add', 'replace', 'playafter', 'tag', 'pl']: 2997 self.UIManager.get_widget('/mainmenu/%smenu/' % \ 2998 (menu,)).hide() 2999 self.UIManager.get_widget(path_update).hide() 3000 else: 3001 for menu in ['add', 'replace', 'playafter', 3002 'tag', 'update', 'pl']: 3003 self.UIManager.get_widget('/mainmenu/%smenu/' \ 3004 % (menu,)).hide() 3005 for menu in ['remove', 'clear', 'rename', 'rm', 3006 'new', 'edit', 'sort']: 3007 self.UIManager.get_widget('/mainmenu/' + menu + 'menu/').hide() 3008 if self.library.search_visible(): 3009 self.UIManager.get_widget('/mainmenu/updatemenu/').hide() 3010 else: 3011 self.UIManager.get_widget('/mainmenu/updatemenu/').show() 3012 path_update_full = '/mainmenu/updatemenu/updatefullmenu/' 3013 self.UIManager.get_widget(path_update_full).show() 3014 elif self.current_tab == self.TAB_PLAYLISTS: 3015 if self.playlists_selection.count_selected_rows() > 0: 3016 for menu in ['add', 'replace', 'playafter', 'rm']: 3017 self.UIManager.get_widget('/mainmenu/%smenu/' % \ 3018 (menu,)).show() 3019 if self.playlists_selection.count_selected_rows() == 1: 3020 self.UIManager.get_widget('/mainmenu/renamemenu/').show() 3021 else: 3022 self.UIManager.get_widget('/mainmenu/renamemenu/').hide() 3023 else: 3024 for menu in ['add', 'replace', 'playafter', 'rm', 'rename']: 3025 self.UIManager.get_widget('/mainmenu/%smenu/' % \ 3026 (menu,)).hide() 3027 for menu in ['remove', 'clear', 'pl', 'update', 3028 'new', 'edit', 'sort', 'tag']: 3029 self.UIManager.get_widget('/mainmenu/' + menu + 'menu/').hide() 3030 elif self.current_tab == self.TAB_STREAMS: 3031 self.UIManager.get_widget('/mainmenu/newmenu/').show() 3032 if self.streams_selection.count_selected_rows() > 0: 3033 if self.streams_selection.count_selected_rows() == 1: 3034 self.UIManager.get_widget('/mainmenu/editmenu/').show() 3035 else: 3036 self.UIManager.get_widget('/mainmenu/editmenu/').hide() 3037 for menu in ['add', 'replace', 'playafter', 'rm']: 3038 self.UIManager.get_widget('/mainmenu/%smenu/' % \ 3039 (menu,)).show() 3040 3041 else: 3042 for menu in ['add', 'replace', 'playafter', 'rm']: 3043 self.UIManager.get_widget('/mainmenu/%smenu/' % \ 3044 (menu,)).hide() 3045 for menu in ['rename', 'remove', 'clear', 3046 'pl', 'update', 'sort', 'tag']: 3047 self.UIManager.get_widget('/mainmenu/' + menu + 'menu/').hide() 3048 3049 def on_tags_edit(self, _widget): 3050 ui.change_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH)) 3051 while Gtk.events_pending(): 3052 Gtk.main_iteration() 3053 3054 files = [] 3055 temp_mpdpaths = [] 3056 if self.current_tab == self.TAB_INFO: 3057 if self.status_is_play_or_pause(): 3058 # Use current file in songinfo: 3059 mpdpath = self.songinfo.file 3060 fullpath = os.path.join( 3061 self.config.current_musicdir, mpdpath) 3062 files.append(fullpath) 3063 temp_mpdpaths.append(mpdpath) 3064 elif self.current_tab == self.TAB_LIBRARY: 3065 # Populates files array with selected library items: 3066 items = self.library.get_path_child_filenames(False) 3067 for item in items: 3068 files.append(os.path.join(self.config.current_musicdir, item)) 3069 temp_mpdpaths.append(item) 3070 elif self.current_tab == self.TAB_CURRENT: 3071 # Populates files array with selected current playlist items: 3072 temp_mpdpaths = self.current.get_selected_filenames(False) 3073 files = self.current.get_selected_filenames(True) 3074 3075 tageditor = tagedit.TagEditor(self.window, 3076 self.tags_mpd_update, 3077 self.tags_set_use_mpdpath) 3078 tageditor.set_use_mpdpaths(self.config.tags_use_mpdpath) 3079 tageditor.on_tags_edit(files, temp_mpdpaths, 3080 self.config.current_musicdir) 3081 3082 def tags_set_use_mpdpath(self, use_mpdpath): 3083 self.config.tags_use_mpdpath = use_mpdpath 3084 3085 def tags_mpd_update(self, tag_paths): 3086 self.mpd.update(list(tag_paths)) 3087 self.mpd_update_queued = True 3088 3089 def on_about(self, _action): 3090 about_dialog = about.About(self.window, self.config, version) 3091 3092 stats = None 3093 if self.conn: 3094 # Extract some MPD stats: 3095 mpdstats = self.mpd.stats() 3096 stats = {'artists': mpdstats['artists'], 3097 'albums': mpdstats['albums'], 3098 'songs': mpdstats['songs'], 3099 'db_playtime': mpdstats['db_playtime'], 3100 } 3101 3102 about_dialog.about_load(stats) 3103 3104 def systemtray_initialize(self): 3105 # Make system tray 'icon' to sit in the system tray 3106 self.tray_icon.initialize( 3107 self.on_tray_click, 3108 self.on_tray_scroll, 3109 ) 3110 3111 if self.config.show_trayicon: 3112 self.tray_icon.show() 3113 else: 3114 self.tray_icon.hide() 3115 self.tray_icon.update_icon('sonata') 3116 3117 def dbus_show(self): 3118 self.window.hide() 3119 self.withdraw_app_undo() 3120 3121 def dbus_toggle(self): 3122 if self.window.get_property('visible'): 3123 self.withdraw_app() 3124 else: 3125 self.withdraw_app_undo() 3126 3127 def dbus_popup(self): 3128 self.on_currsong_notify(force_popup=True) 3129 3130 def dbus_fullscreen(self): 3131 self.on_fullscreen_change() 3132 3133 3134class FullscreenApp: 3135 def __init__(self, config, get_fullscreen_info): 3136 self.config = config 3137 self.get_fullscreen_info = get_fullscreen_info 3138 self.currentpb = None 3139 builder = ui.builder('sonata') 3140 self.window = builder.get_object("fullscreen_window") 3141 self.window.fullscreen() 3142 color = Gdk.RGBA() 3143 color.parse("black") 3144 self.window.override_background_color(Gtk.StateFlags.NORMAL, color) 3145 self.window.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) 3146 self.window.connect("button-press-event", self.on_close, False) 3147 self.window.connect("key-press-event", self.on_close, True) 3148 self.image = builder.get_object("fullscreen_image") 3149 self.album_label_1 = builder.get_object("fullscreen_label_1") 3150 self.album_label_2 = builder.get_object("fullscreen_label_2") 3151 self.reset() 3152 3153 if not config.show_covers: 3154 self.image.hide() 3155 3156 def add_accel_group(self, group): 3157 self.window.add_accel_group(group) 3158 3159 @property 3160 def on_fullscreen(self): 3161 return self.window.get_property('visible') 3162 3163 def on_fullscreen_change(self, _widget=None): 3164 if self.on_fullscreen: 3165 self.window.hide() 3166 else: 3167 self.set_image(force_update=True) 3168 self.window.show_all() 3169 # setting up invisible cursor 3170 window = self.window.get_window() 3171 window.set_cursor(Gdk.Cursor.new(Gdk.CursorType.BLANK_CURSOR)) 3172 3173 def on_close(self, _widget, event, key_press): 3174 if key_press: 3175 shortcut = Gtk.accelerator_name(event.keyval, event.get_state()) 3176 shortcut = shortcut.replace("<Mod2>", "") 3177 if shortcut != 'Escape': 3178 return 3179 self.window.hide() 3180 3181 def on_artwork_changed(self, artwork_obj, pixbuf): 3182 self.currentpb = pixbuf 3183 self.set_image() 3184 3185 def on_text_changed(self, line1, line2): 3186 self.set_text() 3187 3188 def reset(self, *args, **kwargs): 3189 self.image.set_from_icon_set(ui.icon('sonata-cd'), -1) 3190 self.currentpb = None 3191 3192 def set_image(self, force_update=False): 3193 if self.image.get_property('visible') or force_update: 3194 if self.currentpb is None: 3195 self.reset() 3196 else: 3197 # Artwork for fullscreen cover mode 3198 (pix3, w, h) = img.get_pixbuf_of_size( 3199 self.currentpb, consts.FULLSCREEN_COVER_SIZE) 3200 pix3 = img.do_style_cover(self.config, pix3, w, h) 3201 pix3 = img.pixbuf_pad(pix3, consts.FULLSCREEN_COVER_SIZE, 3202 consts.FULLSCREEN_COVER_SIZE) 3203 self.image.set_from_pixbuf(pix3) 3204 del pix3 3205 self.set_text() 3206 3207 def set_text(self): 3208 is_play_or_pause, line1, line2 = self.get_fullscreen_info() 3209 self.album_label_1.set_text(misc.escape_html(line1)) 3210 self.album_label_2.set_text(misc.escape_html(line2)) 3211 if is_play_or_pause: 3212 self.album_label_1.show() 3213 self.album_label_2.show() 3214 else: 3215 self.album_label_1.hide() 3216 self.album_label_2.hide() 3217