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
19"""
20This module provides a user interface for changing configuration
21variables.
22
23Example usage:
24import preferences
25...
26prefs = preferences.Preferences()
27prefs.on_prefs_real(self.window, self.prefs_window_response, tab callbacks...)
28"""
29
30import gettext, hashlib
31
32from gi.repository import Gtk, Gdk, GdkPixbuf
33
34from sonata.config import Config
35from sonata.pluginsystem import pluginsystem
36from sonata import ui, misc, consts, formatting
37import os
38
39class Extras_cbs:
40    """Callbacks and data specific to the extras tab"""
41    popuptimes = []
42    notif_toggled = None
43    crossfade_changed = None
44    crossfade_toggled = None
45
46class Display_cbs:
47    """Callbacks specific to the display tab"""
48    stylized_toggled = None
49    art_toggled = None
50    playback_toggled = None
51    progress_toggled = None
52    statusbar_toggled = None
53    lyrics_toggled = None
54    trayicon_available = None
55
56class Behavior_cbs:
57    """Callbacks and data specific to the behavior tab"""
58    trayicon_toggled = None
59    trayicon_in_use = None
60    sticky_toggled = None
61    ontop_toggled = None
62    decorated_toggled = None
63    infofile_changed = None
64
65class Format_cbs:
66    """Callbacks specific to the format tab"""
67    currentoptions_changed = None
68    libraryoptions_changed = None
69    titleoptions_changed = None
70    currsongoptions1_changed = None
71    currsongoptions2_changed = None
72
73class Preferences():
74    """This class implements a preferences dialog for changing
75    configuration variables.
76
77    Many changes are applied instantly with respective
78    callbacks. Closing the dialog causes a response callback.
79    """
80    def __init__(self, config, reconnect, renotify, reinfofile,
81             settings_save, populate_profiles_for_menu):
82
83        self.config = config
84
85        # These are callbacks to Main
86        self.reconnect = reconnect
87        self.renotify = renotify
88        self.reinfofile = reinfofile
89        self.settings_save = settings_save
90        self.populate_profiles_for_menu = populate_profiles_for_menu
91
92        # Temporary flag:
93        self.updating_nameentry = False
94
95        self.prev_host = None
96        self.prev_password = None
97        self.prev_port = None
98
99        self.window = None
100        self.last_tab = 0
101        self.display_trayicon = None
102        self.direntry = None
103        self.using_mpd_env_vars = False
104
105    def on_prefs_real(self):
106        """Display the preferences dialog"""
107
108        self.builder = ui.builder('preferences')
109        self.provider = ui.css_provider('preferences')
110        self.prefswindow = self.builder.get_object('preferences_dialog')
111        self.prefswindow.set_transient_for(self.window)
112        self.prefsnotebook = self.builder.get_object('preferences_notebook')
113
114        tabs = ('mpd', 'display', 'behavior', 'format', 'extras', 'plugins')
115
116        for name in tabs:
117            func = getattr(self, '%s_tab' % name)
118            cbs = globals().get('%s_cbs' % name.capitalize())
119            func(cbs)
120
121        close_button = self.builder.get_object('preferences_closebutton')
122        self.prefswindow.show_all()
123        self.prefsnotebook.set_current_page(self.last_tab)
124        close_button.grab_focus()
125        self.prefswindow.connect('response', self._window_response)
126        # Save previous connection properties to determine if we should try to
127        # connect to MPD after prefs are closed:
128        self.prev_host = self.config.host[self.config.profile_num]
129        self.prev_port = self.config.port[self.config.profile_num]
130        self.prev_password = self.config.password[self.config.profile_num]
131
132    def mpd_tab(self, cbs=None):
133        """Construct and layout the MPD tab"""
134        #frame.set_shadow_type(Gtk.ShadowType.NONE)
135        controlbox = self.builder.get_object('connection_frame_label_widget')
136        profiles = self.builder.get_object('connection_profiles')
137        add_profile = self.builder.get_object('connection_add_profile')
138        remove_profile = self.builder.get_object('connection_remove_profile')
139        self._populate_profile_combo(profiles, self.config.profile_num,
140            remove_profile)
141        nameentry = self.builder.get_object('connection_name')
142        hostentry = self.builder.get_object('connection_host')
143        portentry = self.builder.get_object('connection_port')
144        direntry = self.builder.get_object('connection_dir')
145        self.direntry = direntry
146        direntry.connect('selection-changed', self._direntry_changed,
147            profiles)
148        passwordentry = self.builder.get_object('connection_password')
149        autoconnect = self.builder.get_object('connection_autoconnect')
150        autoconnect.set_active(self.config.autoconnect)
151        autoconnect.connect('toggled', self._config_widget_active,
152            'autoconnect')
153        # Fill in entries with current profile:
154        self._profile_chosen(profiles, nameentry, hostentry,
155            portentry, passwordentry, direntry)
156        # Update display if $MPD_HOST or $MPD_PORT is set:
157        host, port, password = misc.mpd_env_vars()
158        if host or port:
159            self.using_mpd_env_vars = True
160            if not host:
161                host = ""
162            if not port:
163                port = 0
164            if not password:
165                password = ""
166            hostentry.set_text(str(host))
167            portentry.set_value(port)
168            passwordentry.set_text(str(password))
169            nameentry.set_text(_("Using MPD_HOST/PORT"))
170            for widget in [hostentry, portentry, passwordentry,
171                       nameentry, profiles, add_profile,
172                       remove_profile]:
173                widget.set_sensitive(False)
174        else:
175            self.using_mpd_env_vars = False
176            nameentry.connect('changed', self._nameentry_changed,
177                profiles, remove_profile)
178            hostentry.connect('changed', self._hostentry_changed,
179                profiles)
180            portentry.connect('value-changed',
181                self._portentry_changed, profiles)
182            passwordentry.connect('changed',
183                self._passwordentry_changed, profiles)
184            profiles.connect('changed',
185                self._profile_chosen, nameentry, hostentry,
186                portentry, passwordentry, direntry)
187            add_profile.connect('clicked', self._add_profile,
188                nameentry, profiles, remove_profile)
189            remove_profile.connect('clicked', self._remove_profile,
190                profiles, remove_profile)
191
192    def extras_tab(self, cbs):
193        """Construct and layout the extras tab"""
194        if not self.scrobbler.imported():
195            self.config.as_enabled = False
196
197        as_checkbox = self.builder.get_object('scrobbler_check')
198        as_checkbox.set_active(self.config.as_enabled)
199        as_user_label = self.builder.get_object('scrobbler_username_label')
200        as_user_entry = self.builder.get_object('scrobbler_username_entry')
201        as_user_entry.set_text(self.config.as_username)
202        as_user_entry.connect('changed', self._as_username_changed)
203        as_pass_label = self.builder.get_object('scrobbler_password_label')
204        as_pass_entry = self.builder.get_object('scrobbler_password_entry')
205        as_pass_entry.set_text(self.config.as_password_md5)
206        as_pass_entry.connect('changed', self._as_password_changed)
207        display_notification = self.builder.get_object('notification_check')
208        display_notification.set_active(self.config.show_notification)
209
210        time_names = ["%s %s" %
211            (i , ngettext('second', 'seconds', int(i)))
212            for i in cbs.popuptimes if i != _('Entire song')]
213        notification_options = self.builder.get_object('notification_time_combo')
214        for time in time_names:
215            notification_options.append_text(time)
216        notification_options.connect('changed', self._notiftime_changed)
217        notification_options.set_active(self.config.popup_option)
218        notification_locs = self.builder.get_object('notification_loc_combo')
219        notification_locs.set_active(self.config.traytips_notifications_location)
220        notification_locs.connect('changed', self._notiflocation_changed)
221        notifhbox = self.builder.get_object('notification_box')
222        display_notification.connect('toggled', cbs.notif_toggled,
223            notifhbox)
224        if not self.config.show_notification:
225            notifhbox.set_sensitive(False)
226
227        crossfadespin = self.builder.get_object('crossfade_time')
228        crossfadespin.set_value(self.config.xfade)
229        crossfadespin.connect('value-changed', cbs.crossfade_changed)
230        crossfadelabel2 = self.builder.get_object('crossfade_label')
231        crossfadelabel3 = self.builder.get_object('crossfade_extra_label')
232        crossfadecheck = self.builder.get_object('crossfade_check')
233        crossfadecheck.connect('toggled',
234            self._crossfadecheck_toggled, crossfadespin,
235            crossfadelabel2, crossfadelabel3)
236        crossfadecheck.connect('toggled', cbs.crossfade_toggled,
237            crossfadespin)
238        crossfadecheck.set_active(self.config.xfade_enabled)
239        crossfadecheck.toggled() # Force the toggled callback
240
241        as_checkbox.connect('toggled', self._as_enabled_toggled,
242            as_user_entry, as_pass_entry, as_user_label,
243            as_pass_label)
244        if not self.config.as_enabled or not self.scrobbler.imported():
245            for widget in (as_user_entry, as_pass_entry,
246                    as_user_label, as_pass_label):
247                widget.set_sensitive(False)
248
249    def display_tab(self, cbs):
250        """Construct and layout the display tab"""
251
252        art = self.builder.get_object('art_check')
253        art.set_active(self.config.show_covers)
254        stylized_combo = self.builder.get_object('art_style_combo')
255        stylized_combo.set_active(self.config.covers_type)
256        stylized_combo.connect('changed', cbs.stylized_toggled)
257        art_prefs = self.builder.get_object('art_preferences')
258        art_prefs.set_sensitive(self.config.show_covers)
259        art_combo = self.builder.get_object('art_search_combo')
260        art_combo.set_active(self.config.covers_pref)
261        art_combo.connect('changed', self._config_widget_active, 'covers_pref')
262
263        #FIXME move into preferences_display.ui?
264        art_location = self.builder.get_object('art_save_combo')
265        for item in ["%s/%s" % (_("SONG_DIR"), item)
266            for item in ("cover.jpg", "album.jpg", "folder.jpg",
267                self.config.art_location_custom_filename or _("custom"))]:
268            art_location.append_text(item)
269        art_location.set_active(self.config.art_location)
270        art_location.connect('changed', self._art_location_changed)
271
272        art.connect('toggled', cbs.art_toggled, art_prefs)
273        playback = self.builder.get_object('playback_buttons_check')
274        playback.set_active(self.config.show_playback)
275        playback.connect('toggled', cbs.playback_toggled)
276        progress = self.builder.get_object('progressbar_check')
277        progress.set_active(self.config.show_progress)
278        progress.connect('toggled', cbs.progress_toggled)
279        statusbar = self.builder.get_object('statusbar_check')
280        statusbar.set_active(self.config.show_statusbar)
281        statusbar.connect('toggled', cbs.statusbar_toggled)
282        lyrics = self.builder.get_object('lyrics_check')
283        lyrics.set_active(self.config.show_lyrics)
284        lyrics_location = self.builder.get_object('lyrics_save_combo')
285        lyrics_location.set_active(self.config.lyrics_location)
286        lyrics_location.connect('changed', self._lyrics_location_changed)
287        lyrics_location_hbox = self.builder.get_object('lyrics_preferences')
288        lyrics_location_hbox.set_sensitive(self.config.show_lyrics)
289        lyrics.connect('toggled', cbs.lyrics_toggled, lyrics_location_hbox)
290        trayicon = self.builder.get_object('tray_icon_check')
291        self.display_trayicon = trayicon
292        trayicon.set_active(self.config.show_trayicon)
293        trayicon.set_sensitive(cbs.trayicon_available)
294
295    def behavior_tab(self, cbs):
296        """Construct and layout the behavior tab"""
297
298        frame = self.builder.get_object('behavior_frame')
299        sticky = self.builder.get_object('behavior_sticky_check')
300        sticky.set_active(self.config.sticky)
301        sticky.connect('toggled', cbs.sticky_toggled)
302        ontop = self.builder.get_object('behavior_ontop_check')
303        ontop.set_active(self.config.ontop)
304        ontop.connect('toggled', cbs.ontop_toggled)
305        decor = self.builder.get_object('behavior_decor_check')
306        decor.set_active(not self.config.decorated)
307        decor.connect('toggled', cbs.decorated_toggled, self.prefswindow)
308        minimize = self.builder.get_object('behavior_minimize_check')
309        minimize.set_active(self.config.minimize_to_systray)
310        minimize.connect('toggled', self._config_widget_active,
311            'minimize_to_systray')
312        self.display_trayicon.connect('toggled', cbs.trayicon_toggled,
313            minimize)
314        minimize.set_sensitive(cbs.trayicon_in_use)
315
316        update_start = self.builder.get_object('misc_updatestart_check')
317        update_start.set_active(self.config.update_on_start)
318        update_start.connect('toggled', self._config_widget_active,
319            'update_on_start')
320        exit_stop = self.builder.get_object('misc_exit_stop_check')
321        exit_stop.set_active(self.config.stop_on_exit)
322        exit_stop.connect('toggled', self._config_widget_active,
323            'stop_on_exit')
324        infofile_usage = self.builder.get_object('misc_infofile_usage_check')
325        infofile_usage.set_active(self.config.use_infofile)
326        infopath_options = self.builder.get_object('misc_infofile_entry')
327        infopath_options.set_text(self.config.infofile_path)
328        infopath_options.connect('focus_out_event',
329                    cbs.infofile_changed)
330        infopath_options.connect('activate', cbs.infofile_changed, None)
331        if not self.config.use_infofile:
332            infopath_options.set_sensitive(False)
333        infofile_usage.connect('toggled', self._infofile_toggled,
334            infopath_options)
335
336    def format_tab(self, cbs):
337        """Construct and layout the format tab"""
338
339        playlist_entry = self.builder.get_object('format_playlist_entry')
340        playlist_entry.set_text(self.config.currentformat)
341        library_entry = self.builder.get_object('format_library_entry')
342        library_entry.set_text(self.config.libraryformat)
343        window_entry = self.builder.get_object('format_window_entry')
344        window_entry.set_text(self.config.titleformat)
345        current1_entry = self.builder.get_object('format_current1_entry')
346        current1_entry.set_text(self.config.currsongformat1)
347        current2_entry = self.builder.get_object('format_current2_entry')
348        current2_entry.set_text(self.config.currsongformat2)
349        entries = [playlist_entry, library_entry, window_entry, current1_entry,
350                   current2_entry]
351
352        entry_cbs = (cbs.currentoptions_changed,
353                 cbs.libraryoptions_changed,
354                 cbs.titleoptions_changed,
355                 cbs.currsongoptions1_changed,
356                 cbs.currsongoptions2_changed)
357        for entry, cb, next in zip(entries, entry_cbs,
358                entries[1:] + entries[:1]):
359            entry.connect('focus_out_event', cb)
360            entry.connect('activate', lambda _, n: n.grab_focus(),
361                    next)
362
363        format_grid = self.builder.get_object('format_avail_descs')
364        codeset = formatting.formatcodes
365        codes = (codeset[:len(codeset) // 2],
366                 codeset[len(codeset) // 2:])
367
368        def make_label(content):
369            return Gtk.Label('<small>{}</small>'.format(content),
370                             use_markup=True, xalign=0, yalign=0)
371
372        for column_base, codegroup in enumerate(codes):
373            column = column_base * 2
374            for row, code in enumerate(codegroup):
375                format_code = make_label("%{}".format(code.code))
376                format_code.get_style_context().add_class('format_code')
377                format_desc = make_label(code.description)
378                format_grid.attach(format_code, column, row, 1, 1)
379                format_grid.attach(format_desc, column + 1, row, 1, 1)
380
381        additionalinfo = self.builder.get_object('format_additional_label')
382        # FIXME need to either separate markup from localized strings OR
383        # include markup in the strings and let the translators work around them
384        row = len(codes[0])
385        enclosed_code = make_label('{ }')
386        enclosed_code.get_style_context().add_class('format_code')
387        enclosed_desc = make_label(
388            _('Info displayed only if all enclosed tags are defined'))
389        enclosed_desc.set_line_wrap(True)
390        enclosed_desc.set_max_width_chars(40)
391        column_code = make_label('|')
392        column_code.get_style_context().add_class('format_code')
393        column_desc = make_label(_('Creates columns in the current playlist'))
394        column_desc.set_line_wrap(True)
395        column_desc.set_max_width_chars(40)
396
397        for widget in (enclosed_code, enclosed_desc):
398            widget.get_style_context().add_class('additional_format')
399        format_grid.attach(enclosed_code, 0, row, 1, 1)
400        format_grid.attach(enclosed_desc, 1, row, 3, 1)
401        row += 1
402        format_grid.attach(column_code, 0, row, 1, 1)
403        format_grid.attach(column_desc, 1, row, 3, 1)
404
405    def plugins_tab(self, cbs=None):
406        """Construct and layout the plugins tab"""
407
408        self.plugin_UIManager = self.builder.get_object('plugins_ui_manager')
409        menu_handlers = {
410            "plugin_configure": self.plugin_configure,
411            "plugin_about": self.plugin_about
412        }
413        self.builder.connect_signals(menu_handlers)
414
415        self.pluginview = self.builder.get_object('plugins_treeview')
416        self.pluginselection = self.pluginview.get_selection()
417        plugindata = self.builder.get_object('plugins_store')
418        self.pluginview.connect('button-press-event', self.plugin_click)
419
420        plugincheckcell = self.builder.get_object('plugins_check_renderer')
421        plugincheckcell.connect('toggled', self.plugin_toggled,
422            (plugindata, 0))
423
424        plugindata.clear()
425        for plugin in pluginsystem.get_info():
426            pb = self.plugin_get_icon_pixbuf(plugin)
427            plugin_text = "<b>" + plugin.longname + "</b> " + plugin.version_string
428            plugin_text += "\n" + plugin.description
429            enabled = plugin.get_enabled()
430            plugindata.append((enabled, pb, plugin_text))
431
432    def _window_response(self, window, response):
433        if response == Gtk.ResponseType.CLOSE:
434            self.last_tab = self.prefsnotebook.get_current_page()
435            #XXX: These two are probably never triggered
436            if self.config.show_lyrics and self.config.lyrics_location != consts.LYRICS_LOCATION_HOME:
437                if not os.path.isdir(self.config.current_musicdir):
438                    ui.show_msg(self.window, _("To save lyrics to the music file's directory, you must specify a valid music directory."), _("Music Dir Verification"), 'musicdirVerificationError', Gtk.ButtonsType.CLOSE)
439                    # Set music_dir entry focused:
440                    self.prefsnotebook.set_current_page(0)
441                    self.direntry.grab_focus()
442                    return
443            if self.config.show_covers and self.config.art_location != consts.ART_LOCATION_HOMECOVERS:
444                if not os.path.isdir(self.config.current_musicdir):
445                    ui.show_msg(self.window, _("To save artwork to the music file's directory, you must specify a valid music directory."), _("Music Dir Verification"), 'musicdirVerificationError', Gtk.ButtonsType.CLOSE)
446                    # Set music_dir entry focused:
447                    self.prefsnotebook.set_current_page(0)
448                    self.direntry.grab_focus()
449                    return
450            if not self.using_mpd_env_vars:
451                if self.prev_host != self.config.host[self.config.profile_num] or self.prev_port != self.config.port[self.config.profile_num] or self.prev_password != self.config.password[self.config.profile_num]:
452                    # Try to connect if mpd connection info has been updated:
453                    ui.change_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH))
454                    self.reconnect()
455            self.settings_save()
456            self.populate_profiles_for_menu()
457            ui.change_cursor(None)
458        window.destroy()
459
460    def _config_widget_active(self, widget, member):
461        """Sets a config attribute to the widget's active value"""
462        setattr(self.config, member, widget.get_active())
463
464    def _as_enabled_toggled(self, checkbox, *widgets):
465        if checkbox.get_active():
466            self.scrobbler.import_module(True)
467        if self.scrobbler.imported():
468            self.config.as_enabled = checkbox.get_active()
469            self.scrobbler.init()
470            for widget in widgets:
471                widget.set_sensitive(self.config.as_enabled)
472        elif checkbox.get_active():
473            checkbox.set_active(False)
474
475    def _as_username_changed(self, entry):
476        if self.scrobbler.imported():
477            self.config.as_username = entry.get_text()
478            self.scrobbler.auth_changed()
479
480    def _as_password_changed(self, entry):
481        if self.scrobbler.imported():
482            self.config.as_password_md5 = hashlib.md5(
483                entry.get_text().encode('utf-8')).hexdigest()
484            self.scrobbler.auth_changed()
485
486    def _nameentry_changed(self, entry, profile_combo, remove_profiles):
487        if not self.updating_nameentry:
488            profile_num = profile_combo.get_active()
489            self.config.profile_names[profile_num] = entry.get_text()
490            self._populate_profile_combo(profile_combo, profile_num, remove_profiles)
491
492    def _hostentry_changed(self, entry, profile_combo):
493        profile_num = profile_combo.get_active()
494        self.config.host[profile_num] = entry.get_text()
495
496    def _portentry_changed(self, entry, profile_combo):
497        profile_num = profile_combo.get_active()
498        self.config.port[profile_num] = entry.get_value_as_int()
499
500    def _passwordentry_changed(self, entry, profile_combo):
501        profile_num = profile_combo.get_active()
502        self.config.password[profile_num] = entry.get_text()
503
504    def _direntry_changed(self, entry, profile_combo):
505        profile_num = profile_combo.get_active()
506        self.config.musicdir[profile_num] = misc.sanitize_musicdir(entry.get_filename())
507
508    def _add_profile(self, _button, nameentry, profile_combo, remove_profiles):
509        self.updating_nameentry = True
510        profile_num = profile_combo.get_active()
511        self.config.profile_names.append(_("New Profile"))
512        nameentry.set_text(self.config.profile_names[-1])
513        self.updating_nameentry = False
514        self.config.host.append(self.config.host[profile_num])
515        self.config.port.append(self.config.port[profile_num])
516        self.config.password.append(self.config.password[profile_num])
517        self.config.musicdir.append(self.config.musicdir[profile_num])
518        self._populate_profile_combo(profile_combo, len(self.config.profile_names)-1, remove_profiles)
519        self.populate_profiles_for_menu()
520
521    def _remove_profile(self, _button, profile_combo, remove_profiles):
522        profile_num = profile_combo.get_active()
523        if profile_num == self.config.profile_num:
524            # Profile deleted, revert to first profile:
525            self.config.profile_num = 0
526            self.reconnect(None)
527        self.config.profile_names.pop(profile_num)
528        self.config.host.pop(profile_num)
529        self.config.port.pop(profile_num)
530        self.config.password.pop(profile_num)
531        self.config.musicdir.pop(profile_num)
532        self.populate_profiles_for_menu()
533        if profile_num > 0:
534            self._populate_profile_combo(profile_combo, profile_num-1, remove_profiles)
535        else:
536            self._populate_profile_combo(profile_combo, 0, remove_profiles)
537
538    def _profile_chosen(self, profile_combo, nameentry, hostentry, portentry, passwordentry, direntry):
539        profile_num = profile_combo.get_active()
540        self.updating_nameentry = True
541        nameentry.set_text(str(self.config.profile_names[profile_num]))
542        self.updating_nameentry = False
543        hostentry.set_text(str(self.config.host[profile_num]))
544        portentry.set_value(self.config.port[profile_num])
545        passwordentry.set_text(str(self.config.password[profile_num]))
546        direntry.set_current_folder(misc.sanitize_musicdir(self.config.musicdir[profile_num]))
547
548    def _populate_profile_combo(self, profile_combo, active_index, remove_profiles):
549        new_model = Gtk.ListStore(str)
550        new_model.clear()
551        profile_combo.set_model(new_model)
552        for i, profile_name in enumerate(self.config.profile_names):
553            combo_text = "[%s] %s" % (i+1, profile_name[:15])
554            if len(profile_name) > 15:
555                combo_text += "..."
556            profile_combo.append_text(combo_text)
557        profile_combo.set_active(active_index)
558        # Enable remove button if there is more than one profile
559        remove_profiles.set_sensitive(len(self.config.profile_names) > 1)
560
561    def _lyrics_location_changed(self, combobox):
562        self.config.lyrics_location = combobox.get_active()
563
564    def _art_location_changed(self, combobox):
565        if combobox.get_active() == consts.ART_LOCATION_CUSTOM:
566            dialog = self.builder.get_object('custom_art_dialog')
567            # Prompt user for playlist name:
568            entry = self.builder.get_object('custom_art_entry')
569            dialog.vbox.show_all()
570            response = dialog.run()
571            if response == Gtk.ResponseType.ACCEPT:
572                self.config.art_location_custom_filename = entry.get_text().replace("/", "")
573                iter = combobox.get_active_iter()
574                model = combobox.get_model()
575                model.set(iter, 0, "SONG_DIR/" + (self.config.art_location_custom_filename or _("custom")))
576            else:
577                # Revert to non-custom item in combobox:
578                combobox.set_active(self.config.art_location)
579            dialog.hide()
580        self.config.art_location = combobox.get_active()
581
582    def _crossfadecheck_toggled(self, button, *widgets):
583        button_active = button.get_active()
584        for widget in widgets:
585            widget.set_sensitive(button_active)
586
587    def _notiflocation_changed(self, combobox):
588        self.config.traytips_notifications_location = combobox.get_active()
589        self.renotify()
590
591    def _notiftime_changed(self, combobox):
592        self.config.popup_option = combobox.get_active()
593        self.renotify()
594
595    def _infofile_toggled(self, button, infofileformatbox):
596        self.config.use_infofile = button.get_active()
597        infofileformatbox.set_sensitive(self.config.use_infofile)
598        if self.config.use_infofile:
599            self.reinfofile()
600
601    def plugin_click(self, _widget, event):
602        if event.button == 3:
603            self.plugin_UIManager.get_widget('/pluginmenu').popup(None, None, None, None, event.button, event.time)
604
605    def plugin_toggled(self, _renderer, path, user_data):
606        model, column = user_data
607        enabled = not model[path][column]
608        plugin = pluginsystem.get_info()[int(path)]
609        pluginsystem.set_enabled(plugin, enabled)
610
611        if enabled:
612            # test that the plugin loads or already was loaded
613            if not plugin.force_loaded():
614                enabled = False
615                pluginsystem.set_enabled(plugin, enabled)
616
617        model[path][column] = enabled
618
619    def plugin_about(self, _widget):
620        plugin = self.plugin_get_selected()
621        iconpb = self.plugin_get_icon_pixbuf(plugin)
622
623        about_text = plugin.longname + "\n" + plugin.author + "\n"
624        if len(plugin.author_email) > 0:
625            about_text += "<" + plugin.author_email + ">"
626
627        self.about_dialog = Gtk.AboutDialog()
628        self.about_dialog.set_name(plugin.longname)
629        self.about_dialog.set_role('about')
630        self.about_dialog.set_version(plugin.version_string)
631        if len(plugin.description.strip()) > 0:
632            self.about_dialog.set_comments(plugin.description)
633        if len(plugin.author.strip()) > 0:
634            author = plugin.author
635            if len(plugin.author.strip()) > 0:
636                author += ' <' + plugin.author_email + '>'
637            self.about_dialog.set_authors([author])
638        if len(plugin.url.strip()) > 0:
639            self.about_dialog.connect("activate-link", self.plugin_show_website)
640            self.about_dialog.set_website(plugin.url)
641        self.about_dialog.set_logo(iconpb)
642
643        self.about_dialog.connect('response', self.plugin_about_close)
644        self.about_dialog.connect('delete_event', self.plugin_about_close)
645        self.about_dialog.show_all()
646
647    def plugin_about_close(self, _event, _data=None):
648        self.about_dialog.hide()
649        return True
650
651    def plugin_show_website(self, _dialog, link):
652        return misc.browser_load(link, self.config.url_browser, \
653                                 self.window)
654
655    def plugin_configure(self, _widget):
656        plugin = self.plugin_get_selected()
657        ui.show_msg(self.prefswindow, "Nothing yet implemented.", "Configure", "pluginConfigure", Gtk.ButtonsType.CLOSE)
658
659    def plugin_get_selected(self):
660        model, i = self.pluginselection.get_selected()
661        plugin_num = model.get_path(i).get_indices()[0]
662        return pluginsystem.get_info()[plugin_num]
663
664    def plugin_get_icon_pixbuf(self, plugin):
665        pb = plugin.iconurl
666        try:
667            pb = GdkPixbuf.Pixbuf.new_from_file(pb)
668        except:
669            pb = self.pluginview.render_icon(Gtk.STOCK_EXECUTE, Gtk.IconSize.LARGE_TOOLBAR)
670        return pb
671