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