1# Copyright (C) 2008-2010 Adam Olsen
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2, or (at your option)
6# any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
16#
17#
18# The developers of the Exaile media player hereby grant permission
19# for non-GPL compatible GStreamer and Exaile plugins to be used and
20# distributed together with GStreamer and Exaile. This permission is
21# above and beyond the permissions granted by the GPL license by which
22# Exaile is covered. If you modify this code, you may extend this
23# exception to your version of the code, but you are not obligated to
24# do so. If you do not wish to do so, delete this exception statement
25# from your version.
26
27from gi.repository import Gdk
28from gi.repository import GdkPixbuf
29from gi.repository import Gio
30from gi.repository import GLib
31from gi.repository import GObject
32from gi.repository import Gtk
33import logging
34from gi.repository import Pango
35import os.path
36
37from xl import metadata, providers, settings, xdg
38from xl.common import clamp
39from xl.playlist import (
40    is_valid_playlist,
41    import_playlist,
42    export_playlist,
43    InvalidPlaylistTypeError,
44    PlaylistExportOptions,
45)
46from xl.nls import gettext as _
47
48from xlgui.guiutil import GtkTemplate
49from threading import Thread
50
51logger = logging.getLogger(__name__)
52
53
54def error(parent, message=None, markup=None):
55    """
56    Shows an error dialog
57    """
58    if message is markup is None:
59        raise ValueError("message or markup must be specified")
60    dialog = Gtk.MessageDialog(
61        buttons=Gtk.ButtonsType.CLOSE,
62        message_type=Gtk.MessageType.ERROR,
63        modal=True,
64        transient_for=parent,
65    )
66    if markup is None:
67        dialog.props.text = message
68    else:
69        dialog.set_markup(markup)
70    dialog.run()
71    dialog.destroy()
72
73
74def info(parent, message=None, markup=None):
75    """
76    Shows an info dialog
77    """
78    if message is markup is None:
79        raise ValueError("message or markup must be specified")
80    dialog = Gtk.MessageDialog(
81        buttons=Gtk.ButtonsType.OK,
82        message_type=Gtk.MessageType.INFO,
83        modal=True,
84        transient_for=parent,
85    )
86    if markup is None:
87        dialog.props.text = message
88    else:
89        dialog.set_markup(markup)
90    dialog.run()
91    dialog.destroy()
92
93
94def yesno(parent, message):
95    '''Gets a Yes/No response from a user'''
96    dlg = Gtk.MessageDialog(
97        buttons=Gtk.ButtonsType.YES_NO,
98        message_type=Gtk.MessageType.QUESTION,
99        text=message,
100        transient_for=parent,
101    )
102    response = dlg.run()
103    dlg.destroy()
104    return response
105
106
107@GtkTemplate('ui', 'about_dialog.ui')
108class AboutDialog(Gtk.AboutDialog):
109    """
110    A dialog showing program info and more
111    """
112
113    __gtype_name__ = 'AboutDialog'
114
115    def __init__(self, parent=None):
116        Gtk.AboutDialog.__init__(self)
117        self.init_template()
118
119        self.set_transient_for(parent)
120        logo = GdkPixbuf.Pixbuf.new_from_file(
121            xdg.get_data_path('images', 'exailelogo.png')
122        )
123        self.set_logo(logo)
124
125        import xl.version
126
127        self.set_version(xl.version.__version__)
128
129        # The user may have changed the theme since startup.
130        theme = Gtk.Settings.get_default().props.gtk_theme_name
131        xl.version.__external_versions__["GTK+ theme"] = theme
132
133        comments = []
134        for name, version in sorted(xl.version.__external_versions__.items()):
135            comments.append('%s: %s' % (name, version))
136
137        self.set_comments('\n'.join(comments))
138
139    def on_response(self, *_):
140        self.destroy()
141
142
143@GtkTemplate('ui', 'shortcuts_dialog.ui')
144class ShortcutsDialog(Gtk.Dialog):
145    """
146    Shows information about registered shortcuts
147
148    TODO: someday upgrade to Gtk.ShortcutsWindow when we require 3.20 as
149          a minimum GTK version. This would also enable automatically
150          localized (translated) accelerator names.
151    """
152
153    # doesn't work if we don't set the treeview here too..
154    shortcuts_treeview, shortcuts_model = GtkTemplate.Child.widgets(2)
155
156    __gtype_name__ = 'ShortcutsDialog'
157
158    def __init__(self, parent=None):
159        Gtk.Dialog.__init__(self)
160        self.init_template()
161
162        self.set_transient_for(parent)
163
164        for a in sorted(
165            providers.get('mainwindow-accelerators'),
166            key=lambda a: ('%04d' % (a.key)) + a.name,
167        ):
168            self.shortcuts_model.append(
169                (Gtk.accelerator_get_label(a.key, a.mods), a.helptext.replace('_', ''))
170            )
171
172    @GtkTemplate.Callback
173    def on_close_clicked(self, widget):
174        self.destroy()
175
176
177class MultiTextEntryDialog(Gtk.Dialog):
178    """
179    Exactly like a TextEntryDialog, except it can contain multiple
180    labels/fields.
181
182    Instead of using GetValue, use GetValues.  It will return a list with
183    the contents of the fields. Each field must be filled out or the dialog
184    will not close.
185    """
186
187    def __init__(self, parent, title):
188        Gtk.Dialog.__init__(self, title=title, transient_for=parent)
189
190        self.__entry_area = Gtk.Grid()
191        self.__entry_area.set_row_spacing(3)
192        self.__entry_area.set_column_spacing(3)
193        self.__entry_area.set_border_width(3)
194        self.vbox.pack_start(self.__entry_area, True, True, 0)
195
196        self.add_buttons(
197            Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK
198        )
199
200        self.fields = []
201
202    def add_field(self, label):
203        """
204        Adds a field and corresponding label
205
206        :param label: the label to display
207        :returns: the newly created entry
208        :rtype: :class:`Gtk.Entry`
209        """
210        line_number = len(self.fields)
211
212        label = Gtk.Label(label=label)
213        label.set_xalign(0)
214        self.__entry_area.attach(label, 0, line_number, 1, 1)
215
216        entry = Gtk.Entry()
217        entry.set_width_chars(30)
218        entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, False)
219        entry.connect('activate', lambda *e: self.response(Gtk.ResponseType.OK))
220        self.__entry_area.attach(entry, 1, line_number, 1, 1)
221        label.show()
222        entry.show()
223
224        self.fields.append(entry)
225
226        return entry
227
228    def get_values(self):
229        """
230        Returns a list of the values from the added fields
231        """
232        return [a.get_text() for a in self.fields]
233
234    def run(self):
235        """
236        Shows the dialog, runs, hides, and returns
237        """
238        self.show_all()
239
240        while True:
241            response = Gtk.Dialog.run(self)
242
243            if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
244                break
245
246            if response == Gtk.ResponseType.OK:
247                # Leave loop if all fields where filled
248                if len(min([f.get_text() for f in self.fields])) > 0:
249                    break
250
251                # At least one field was not filled
252                for field in self.fields:
253                    if len(field.get_text()) > 0:
254                        # Unset possible previous marks
255                        field.set_icon_from_icon_name(
256                            Gtk.EntryIconPosition.SECONDARY, None
257                        )
258                    else:
259                        # Mark via warning
260                        field.set_icon_from_icon_name(
261                            Gtk.EntryIconPosition.SECONDARY, 'dialog-warning'
262                        )
263        self.hide()
264
265        return response
266
267
268class TextEntryDialog(Gtk.Dialog):
269    """
270    Shows a dialog with a single line of text
271    """
272
273    def __init__(
274        self,
275        message,
276        title,
277        default_text=None,
278        parent=None,
279        cancelbutton=None,
280        okbutton=None,
281    ):
282        """
283        Initializes the dialog
284        """
285        if not cancelbutton:
286            cancelbutton = Gtk.STOCK_CANCEL
287        if not okbutton:
288            okbutton = Gtk.STOCK_OK
289        Gtk.Dialog.__init__(
290            self, title=title, transient_for=parent, destroy_with_parent=True
291        )
292
293        self.add_buttons(
294            cancelbutton, Gtk.ResponseType.CANCEL, okbutton, Gtk.ResponseType.OK
295        )
296
297        label = Gtk.Label(label=message)
298        label.set_xalign(0)
299        self.vbox.set_border_width(5)
300
301        main = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
302        main.set_spacing(3)
303        main.set_border_width(5)
304        self.vbox.pack_start(main, True, True, 0)
305
306        main.pack_start(label, False, False, 0)
307
308        self.entry = Gtk.Entry()
309        self.entry.set_width_chars(35)
310        if default_text:
311            self.entry.set_text(default_text)
312        main.pack_start(self.entry, False, False, 0)
313
314        self.entry.connect('activate', lambda e: self.response(Gtk.ResponseType.OK))
315
316    def get_value(self):
317        """
318        Returns the text value
319        """
320        return self.entry.get_text()
321
322    def set_value(self, value):
323        """
324        Sets the value of the text
325        """
326        self.entry.set_text(value)
327
328    def run(self):
329        self.show_all()
330        response = Gtk.Dialog.run(self)
331        self.hide()
332        return response
333
334
335class URIOpenDialog(TextEntryDialog):
336    """
337    A dialog specialized for opening an URI
338    """
339
340    __gsignals__ = {
341        'uri-selected': (
342            GObject.SignalFlags.RUN_LAST,
343            GObject.TYPE_BOOLEAN,
344            (GObject.TYPE_PYOBJECT,),
345            GObject.signal_accumulator_true_handled,
346        )
347    }
348
349    def __init__(self, parent=None):
350        """
351        :param parent: a parent window for modal operation or None
352        :type parent: :class:`Gtk.Window`
353        """
354        TextEntryDialog.__init__(
355            self, message=_('Enter the URL to open'), title=_('Open URL'), parent=parent
356        )
357
358        self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
359
360        self.connect('response', self.on_response)
361
362    def run(self):
363        """
364        Show the dialog and block until it's closed.
365
366        The dialog will be automatically destroyed on user response.
367        To obtain the entered URI, handle the "uri-selected" signal.
368        """
369        self.show()
370        response = TextEntryDialog.run(self)
371        self.emit('response', response)
372
373    def show(self):
374        """
375        Show the dialog and return immediately.
376
377        The dialog will be automatically destroyed on user response.
378        To obtain the entered URI, handle the "uri-selected" signal.
379        """
380        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
381        text = clipboard.wait_for_text()
382
383        if text is not None:
384            f = Gio.File.new_for_uri(text)
385            if f.get_uri_scheme():
386                self.set_value(text)
387
388        TextEntryDialog.show_all(self)
389
390    def do_uri_selected(self, uri):
391        """
392        Destroys the dialog
393        """
394        self.destroy()
395
396    def on_response(self, dialog, response):
397        """
398        Notifies about the selected URI
399        """
400        self.hide()
401
402        if response == Gtk.ResponseType.OK:
403            self.emit('uri-selected', self.get_value())
404
405        # self.destroy()
406
407    '''
408        dialog = dialogs.TextEntryDialog(_('Enter the URL to open'),
409        _('Open URL'))
410        dialog.set_transient_for(self.main.window)
411        dialog.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
412
413        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
414        text = clipboard.wait_for_text()
415
416        if text is not None:
417            location = Gio.File.new_for_uri(text)
418
419            if location.get_uri_scheme() is not None:
420                dialog.set_value(text)
421
422        result = dialog.run()
423        dialog.hide()
424        if result == Gtk.ResponseType.OK:
425            url = dialog.get_value()
426            self.open_uri(url, play=False)
427    '''
428
429
430class ListDialog(Gtk.Dialog):
431    """
432    Shows a dialog with a list of specified items
433
434    Items must define a __str__ method, or be a string
435    """
436
437    def __init__(self, title, parent=None, multiple=False, write_only=False):
438        """
439        Initializes the dialog
440        """
441        Gtk.Dialog.__init__(self, title=title, transient_for=parent)
442
443        self.vbox.set_border_width(5)
444        scroll = Gtk.ScrolledWindow()
445        scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
446        self.model = Gtk.ListStore(object)
447        self.list = Gtk.TreeView(model=self.model)
448        self.list.set_headers_visible(False)
449        self.list.connect(
450            'row-activated', lambda *e: self.response(Gtk.ResponseType.OK)
451        )
452        scroll.add(self.list)
453        scroll.set_shadow_type(Gtk.ShadowType.IN)
454        self.vbox.pack_start(scroll, True, True, 0)
455
456        if write_only:
457            self.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK)
458        else:
459            self.add_buttons(
460                Gtk.STOCK_CANCEL,
461                Gtk.ResponseType.CANCEL,
462                Gtk.STOCK_OK,
463                Gtk.ResponseType.OK,
464            )
465
466        self.selection = self.list.get_selection()
467
468        if multiple:
469            self.selection.set_mode(Gtk.SelectionMode.MULTIPLE)
470        else:
471            self.selection.set_mode(Gtk.SelectionMode.SINGLE)
472
473        text = Gtk.CellRendererText()
474        col = Gtk.TreeViewColumn()
475        col.pack_start(text, True)
476
477        col.set_cell_data_func(text, self.cell_data_func)
478        self.list.append_column(col)
479        self.list.set_model(self.model)
480        self.resize(400, 240)
481
482    def get_items(self):
483        """
484        Returns the selected items
485        """
486        items = []
487        check = self.selection.get_selected_rows()
488        if not check:
489            return None
490        (model, paths) = check
491
492        for path in paths:
493            iter = self.model.get_iter(path)
494            item = self.model.get_value(iter, 0)
495            items.append(item)
496
497        return items
498
499    def run(self):
500        self.show_all()
501        result = Gtk.Dialog.run(self)
502        self.hide()
503        return result
504
505    def set_items(self, items):
506        """
507        Sets the items
508        """
509        for item in items:
510            self.model.append([item])
511
512    def cell_data_func(self, column, cell, model, iter, user_data):
513        """
514        Called when the tree needs a value for column 1
515        """
516        object = model.get_value(iter, 0)
517        cell.set_property('text', str(object))
518
519
520# TODO: combine this and list dialog
521
522
523class ListBox:
524    """
525    Represents a list box
526    """
527
528    def __init__(self, widget, rows=None):
529        """
530        Initializes the widget
531        """
532        self.list = widget
533        self.store = Gtk.ListStore(str)
534        widget.set_headers_visible(False)
535        cell = Gtk.CellRendererText()
536        col = Gtk.TreeViewColumn('', cell, text=0)
537        self.list.append_column(col)
538        self.rows = rows
539        if not rows:
540            self.rows = []
541
542        if rows:
543            for row in rows:
544                self.store.append([row])
545
546        self.list.set_model(self.store)
547
548    def connect(self, signal, func, data=None):
549        """
550        Connects a signal to the underlying treeview
551        """
552        self.list.connect(signal, func, data)
553
554    def append(self, row):
555        """
556        Appends a row to the list
557        """
558        self.rows.append(row)
559        self.set_rows(self.rows)
560
561    def remove(self, row):
562        """
563        Removes a row
564        """
565        try:
566            index = self.rows.index(row)
567        except ValueError:
568            return
569        path = (index,)
570        iter = self.store.get_iter(path)
571        self.store.remove(iter)
572        del self.rows[index]
573
574    def set_rows(self, rows):
575        """
576        Sets the rows
577        """
578        self.rows = rows
579        self.store = Gtk.ListStore(str)
580        for row in rows:
581            self.store.append([row])
582
583        self.list.set_model(self.store)
584
585    def get_selection(self):
586        """
587        Returns the selection
588        """
589        selection = self.list.get_selection()
590        (model, iter) = selection.get_selected()
591        if not iter:
592            return None
593        return model.get_value(iter, 0)
594
595
596class FileOperationDialog(Gtk.FileChooserDialog):
597    """
598    An extension of the Gtk.FileChooserDialog that
599    adds a collapsable panel to the bottom listing
600    valid file extensions that the file can be
601    saved in. (similar to the one in GIMP)
602    """
603
604    def __init__(
605        self,
606        title=None,
607        parent=None,
608        action=Gtk.FileChooserAction.OPEN,
609        buttons=None,
610        backend=None,
611    ):
612        """
613        Standard __init__ of the Gtk.FileChooserDialog.
614        Also sets up the expander and list for extensions
615        """
616        Gtk.FileChooserDialog.__init__(self, title, parent, action, buttons, backend)
617
618        self.set_do_overwrite_confirmation(True)
619
620        # Container for additional option widgets
621        self.extras_box = Gtk.Box(spacing=3, orientation=Gtk.Orientation.VERTICAL)
622        self.set_extra_widget(self.extras_box)
623        self.extras_box.show()
624
625        # Create the list that will hold the file type/extensions pair
626        self.liststore = Gtk.ListStore(str, str)
627        self.list = Gtk.TreeView(self.liststore)
628        self.list.set_headers_visible(False)
629
630        # Create the columns
631        filetype_cell = Gtk.CellRendererText()
632        extension_cell = Gtk.CellRendererText()
633        column = Gtk.TreeViewColumn()
634        column.pack_start(filetype_cell, True)
635        column.pack_start(extension_cell, False)
636        column.set_attributes(filetype_cell, text=0)
637        column.set_attributes(extension_cell, text=1)
638
639        self.list.append_column(column)
640        self.list.show_all()
641
642        self.expander = Gtk.Expander.new(_('Select File Type (by Extension)'))
643        self.expander.add(self.list)
644        self.extras_box.pack_start(self.expander, False, False, 0)
645        self.expander.show()
646
647        selection = self.list.get_selection()
648        selection.connect('changed', self.on_selection_changed)
649
650    def on_selection_changed(self, selection):
651        """
652        When the user selects an extension the filename
653        that is entered will have its extension changed
654        to the selected extension
655        """
656        model, iter = selection.get_selected()
657        (extension,) = model.get(iter, 1)
658        filename = ""
659        if self.get_filename():
660            filename = os.path.basename(self.get_filename())
661            filename, old_extension = os.path.splitext(filename)
662            filename += '.' + extension
663        else:
664            filename = '*.' + extension
665        self.set_current_name(filename)
666
667    def add_extensions(self, extensions):
668        """
669        Adds extensions to the list
670
671        @param extensions: a dictionary of extension:file type pairs
672        i.e. { 'm3u':'M3U Playlist' }
673        """
674        for key, value in extensions.items():
675            self.liststore.append((value, key))
676
677
678class MediaOpenDialog(Gtk.FileChooserDialog):
679    """
680    A dialog for opening general media
681    """
682
683    __gsignals__ = {
684        'uris-selected': (
685            GObject.SignalFlags.RUN_LAST,
686            GObject.TYPE_BOOLEAN,
687            (GObject.TYPE_PYOBJECT,),
688            GObject.signal_accumulator_true_handled,
689        )
690    }
691    _last_location = None
692
693    def __init__(self, parent=None):
694        """
695        :param parent: a parent window for modal operation or None
696        :type parent: :class:`Gtk.Window`
697        """
698        Gtk.FileChooserDialog.__init__(
699            self,
700            title=_('Choose Media to Open'),
701            parent=parent,
702            buttons=(
703                Gtk.STOCK_CANCEL,
704                Gtk.ResponseType.CANCEL,
705                Gtk.STOCK_OPEN,
706                Gtk.ResponseType.OK,
707            ),
708        )
709
710        self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
711        self.set_local_only(False)
712        self.set_select_multiple(True)
713
714        supported_filter = Gtk.FileFilter()
715        supported_filter.set_name(_('Supported Files'))
716        audio_filter = Gtk.FileFilter()
717        audio_filter.set_name(_('Music Files'))
718        playlist_filter = Gtk.FileFilter()
719        playlist_filter.set_name(_('Playlist Files'))
720        all_filter = Gtk.FileFilter()
721        all_filter.set_name(_('All Files'))
722        all_filter.add_pattern('*')
723
724        for extension in metadata.formats.keys():
725            pattern = '*.%s' % extension
726            supported_filter.add_pattern(pattern)
727            audio_filter.add_pattern(pattern)
728
729        playlist_file_extensions = (
730            ext
731            for p in providers.get('playlist-format-converter')
732            for ext in p.file_extensions
733        )
734
735        for extension in playlist_file_extensions:
736            pattern = '*.%s' % extension
737            supported_filter.add_pattern(pattern)
738            playlist_filter.add_pattern(pattern)
739
740        self.add_filter(supported_filter)
741        self.add_filter(audio_filter)
742        self.add_filter(playlist_filter)
743        self.add_filter(all_filter)
744
745        self.connect('response', self.on_response)
746
747    def run(self):
748        """
749        Override to take care of the response
750        """
751        if MediaOpenDialog._last_location is not None:
752            self.set_current_folder_uri(MediaOpenDialog._last_location)
753
754        response = Gtk.FileChooserDialog.run(self)
755        self.emit('response', response)
756
757    def show(self):
758        """
759        Override to restore last location
760        """
761        if MediaOpenDialog._last_location is not None:
762            self.set_current_folder_uri(MediaOpenDialog._last_location)
763
764        Gtk.FileChooserDialog.show(self)
765
766    def do_uris_selected(self, uris):
767        """
768        Destroys the dialog
769        """
770        self.destroy()
771
772    def on_response(self, dialog, response):
773        """
774        Notifies about selected URIs
775        """
776        self.hide()
777
778        if response == Gtk.ResponseType.OK:
779            MediaOpenDialog._last_location = self.get_current_folder_uri()
780            self.emit('uris-selected', self.get_uris())
781
782        # self.destroy()
783
784
785class DirectoryOpenDialog(Gtk.FileChooserDialog):
786    """
787    A dialog specialized for opening directories
788    """
789
790    __gsignals__ = {
791        'uris-selected': (
792            GObject.SignalFlags.RUN_LAST,
793            GObject.TYPE_BOOLEAN,
794            (GObject.TYPE_PYOBJECT,),
795            GObject.signal_accumulator_true_handled,
796        )
797    }
798    _last_location = None
799
800    def __init__(
801        self, parent=None, title=_('Choose Directory to Open'), select_multiple=True
802    ):
803        Gtk.FileChooserDialog.__init__(
804            self,
805            title,
806            parent=parent,
807            buttons=(
808                Gtk.STOCK_CANCEL,
809                Gtk.ResponseType.CANCEL,
810                Gtk.STOCK_OPEN,
811                Gtk.ResponseType.OK,
812            ),
813        )
814
815        self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
816        self.set_action(Gtk.FileChooserAction.SELECT_FOLDER)
817        self.set_local_only(False)
818        self.set_select_multiple(select_multiple)
819
820        self.connect('response', self.on_response)
821
822    def run(self):
823        """
824        Override to take care of the response
825        """
826        if DirectoryOpenDialog._last_location is not None:
827            self.set_current_folder_uri(DirectoryOpenDialog._last_location)
828
829        Gtk.FileChooserDialog.run(self)
830
831    def show(self):
832        """
833        Override to restore last location
834        """
835        if DirectoryOpenDialog._last_location is not None:
836            self.set_current_folder_uri(DirectoryOpenDialog._last_location)
837
838        Gtk.FileChooserDialog.show(self)
839
840    def do_uris_selected(self, uris):
841        """
842        Destroys the dialog
843        """
844        self.destroy()
845
846    def on_response(self, dialog, response):
847        """
848        Notifies about selected URIs
849        """
850        self.hide()
851
852        if response == Gtk.ResponseType.OK:
853            DirectoryOpenDialog._last_location = self.get_current_folder_uri()
854            self.emit('uris-selected', self.get_uris())
855
856        # self.destroy()
857
858
859class PlaylistImportDialog(Gtk.FileChooserDialog):
860    """
861    A dialog for importing a playlist
862    """
863
864    __gsignals__ = {
865        'playlists-selected': (
866            GObject.SignalFlags.RUN_LAST,
867            GObject.TYPE_BOOLEAN,
868            (GObject.TYPE_PYOBJECT,),
869            GObject.signal_accumulator_true_handled,
870        )
871    }
872    _last_location = None
873
874    def __init__(self, parent=None):
875        """
876        :param parent: a parent window for modal operation or None
877        :type parent: :class:`Gtk.Window`
878        """
879        Gtk.FileChooserDialog.__init__(
880            self,
881            title=_('Import Playlist'),
882            parent=parent,
883            buttons=(
884                Gtk.STOCK_CANCEL,
885                Gtk.ResponseType.CANCEL,
886                Gtk.STOCK_OPEN,
887                Gtk.ResponseType.OK,
888            ),
889        )
890
891        self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
892        self.set_local_only(False)
893        self.set_select_multiple(True)
894
895        playlist_filter = Gtk.FileFilter()
896        playlist_filter.set_name(_('Playlist Files'))
897        all_filter = Gtk.FileFilter()
898        all_filter.set_name(_('All Files'))
899        all_filter.add_pattern('*')
900
901        playlist_file_extensions = (
902            ext
903            for p in providers.get('playlist-format-converter')
904            for ext in p.file_extensions
905        )
906
907        for extension in playlist_file_extensions:
908            pattern = '*.%s' % extension
909            playlist_filter.add_pattern(pattern)
910
911        self.add_filter(playlist_filter)
912        self.add_filter(all_filter)
913
914        self.connect('response', self.on_response)
915
916    def run(self):
917        """
918        Override to take care of the response
919        """
920        if PlaylistImportDialog._last_location is not None:
921            self.set_current_folder_uri(PlaylistImportDialog._last_location)
922
923        response = Gtk.FileChooserDialog.run(self)
924        self.emit('response', response)
925
926    def show(self):
927        """
928        Override to restore last location
929        """
930        if PlaylistImportDialog._last_location is not None:
931            self.set_current_folder_uri(PlaylistImportDialog._last_location)
932
933        Gtk.FileChooserDialog.show(self)
934
935    def do_playlist_selected(self, uris):
936        """
937        Destroys the dialog
938        """
939        self.destroy()
940
941    def on_response(self, dialog, response):
942        """
943        Notifies about selected URIs
944        """
945        self.hide()
946
947        if response == Gtk.ResponseType.OK:
948            PlaylistImportDialog._last_location = self.get_current_folder_uri()
949
950            playlists = []
951            for uri in self.get_uris():
952                try:
953                    playlists.append(import_playlist(uri))
954                except InvalidPlaylistTypeError as e:
955                    error(
956                        self.get_transient_for(), 'Invalid playlist "%s": %s' % (uri, e)
957                    )
958                    self.destroy()
959                    return
960                except Exception as e:
961                    error(
962                        self.get_transient_for(),
963                        'Invalid playlist "%s": (internal error): %s' % (uri, e),
964                    )
965                    self.destroy()
966                    return
967
968            self.emit('playlists-selected', playlists)
969
970        # self.destroy()
971
972
973class PlaylistExportDialog(FileOperationDialog):
974    """
975    A dialog specialized for playlist export
976    """
977
978    __gsignals__ = {
979        'message': (
980            GObject.SignalFlags.RUN_LAST,
981            GObject.TYPE_BOOLEAN,
982            (Gtk.MessageType, GObject.TYPE_STRING),
983            GObject.signal_accumulator_true_handled,
984        )
985    }
986
987    def __init__(self, playlist, parent=None):
988        """
989        :param playlist: the playlist to export
990        :type playlist: :class:`xl.playlist.Playlist`
991        :param parent: a parent window for modal operation or None
992        :type parent: :class:`Gtk.Window`
993        """
994        FileOperationDialog.__init__(
995            self,
996            title=_('Export Current Playlist'),
997            parent=parent,
998            action=Gtk.FileChooserAction.SAVE,
999            buttons=(
1000                Gtk.STOCK_CANCEL,
1001                Gtk.ResponseType.CANCEL,
1002                Gtk.STOCK_SAVE,
1003                Gtk.ResponseType.OK,
1004            ),
1005        )
1006
1007        self.set_current_folder_uri(
1008            settings.get_option('gui/playlist_export_dir')
1009            or GLib.filename_to_uri(xdg.homedir, None)
1010        )
1011
1012        self.set_local_only(False)
1013
1014        self.relative_checkbox = Gtk.CheckButton(_('Use relative paths to tracks'))
1015        self.relative_checkbox.set_active(True)
1016        self.extras_box.pack_start(self.relative_checkbox, False, False, 3)
1017        self.relative_checkbox.show()
1018
1019        self.playlist = playlist
1020
1021        extensions = {}
1022
1023        for provider in providers.get('playlist-format-converter'):
1024            extensions[provider.name] = provider.title
1025
1026        self.add_extensions(extensions)
1027        self.set_current_name('%s.m3u' % playlist.name)
1028
1029        self.connect('response', self.on_response)
1030
1031    def run(self):
1032        """
1033        Override to take care of the response
1034        """
1035        response = FileOperationDialog.run(self)
1036        self.emit('response', response)
1037
1038    def do_message(self, message_type, message):
1039        """
1040        Displays simple dialogs on messages
1041        """
1042        if message_type == Gtk.MessageType.INFO:
1043            info(self.get_transient_for(), markup=message)
1044        elif message_type == Gtk.MessageType.ERROR:
1045            error(self.get_transient_for(), markup=message)
1046
1047    def on_response(self, dialog, response):
1048        """
1049        Exports the playlist if requested
1050        """
1051        self.hide()
1052
1053        if response == Gtk.ResponseType.OK:
1054            gfile = self.get_file()
1055            settings.set_option('gui/playlist_export_dir', gfile.get_parent().get_uri())
1056
1057            path = gfile.get_uri()
1058            if not is_valid_playlist(path):
1059                path = '%s.m3u' % path
1060
1061            options = PlaylistExportOptions(
1062                relative=self.relative_checkbox.get_active()
1063            )
1064
1065            try:
1066                export_playlist(self.playlist, path, options)
1067            except InvalidPlaylistTypeError as e:
1068                self.emit('message', Gtk.MessageType.ERROR, str(e))
1069            else:
1070                self.emit(
1071                    'message',
1072                    Gtk.MessageType.INFO,
1073                    _('Playlist saved as <b>%s</b>.') % path,
1074                )
1075
1076        # self.destroy()
1077
1078
1079class ConfirmCloseDialog(Gtk.MessageDialog):
1080    """
1081    Shows the dialog to confirm closing of the playlist
1082    """
1083
1084    def __init__(self, document_name):
1085        """
1086        Initializes the dialog
1087        """
1088        Gtk.MessageDialog.__init__(self, type=Gtk.MessageType.WARNING)
1089
1090        self.set_title(_('Close %s' % document_name))
1091        self.set_markup(_('<b>Save changes to %s before closing?</b>') % document_name)
1092        self.format_secondary_text(
1093            _('Your changes will be lost if you don\'t save them')
1094        )
1095
1096        self.add_buttons(
1097            _('Close Without Saving'),
1098            100,
1099            Gtk.STOCK_CANCEL,
1100            Gtk.ResponseType.CANCEL,
1101            Gtk.STOCK_SAVE,
1102            110,
1103        )
1104
1105    def run(self):
1106        self.show_all()
1107        response = Gtk.Dialog.run(self)
1108        self.hide()
1109        return response
1110
1111
1112class MessageBar(Gtk.InfoBar):
1113    type_map = {
1114        Gtk.MessageType.INFO: 'dialog-information',
1115        Gtk.MessageType.QUESTION: 'dialog-question',
1116        Gtk.MessageType.WARNING: 'dialog-warning',
1117        Gtk.MessageType.ERROR: 'dialog-error',
1118    }
1119    buttons_map = {
1120        Gtk.ButtonsType.OK: [(Gtk.STOCK_OK, Gtk.ResponseType.OK)],
1121        Gtk.ButtonsType.CLOSE: [(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE)],
1122        Gtk.ButtonsType.CANCEL: [(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)],
1123        Gtk.ButtonsType.YES_NO: [
1124            (Gtk.STOCK_NO, Gtk.ResponseType.NO),
1125            (Gtk.STOCK_YES, Gtk.ResponseType.YES),
1126        ],
1127        Gtk.ButtonsType.OK_CANCEL: [
1128            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL),
1129            (Gtk.STOCK_OK, Gtk.ResponseType.OK),
1130        ],
1131    }
1132
1133    def __init__(
1134        self,
1135        parent=None,
1136        type=Gtk.MessageType.INFO,
1137        buttons=Gtk.ButtonsType.NONE,
1138        text=None,
1139    ):
1140        """
1141        Report important messages to the user
1142
1143        :param parent: the parent container box
1144        :type parent: :class:`Gtk.Box`
1145        :param type: the type of message: Gtk.MessageType.INFO,
1146            Gtk.MessageType.WARNING, Gtk.MessageType.QUESTION or
1147            Gtk.MessageType.ERROR.
1148        :param buttons: the predefined set of buttons to
1149            use: Gtk.ButtonsType.NONE, Gtk.ButtonsType.OK,
1150            Gtk.ButtonsType.CLOSE, Gtk.ButtonsType.CANCEL,
1151            Gtk.ButtonsType.YES_NO, Gtk.ButtonsType.OK_CANCEL
1152        :param text: a string containing the message
1153            text or None
1154        """
1155        if parent is not None and not isinstance(parent, Gtk.Box):
1156            raise TypeError('Parent needs to be of type Gtk.Box')
1157
1158        Gtk.InfoBar.__init__(self)
1159        self.set_no_show_all(True)
1160
1161        if parent is not None:
1162            parent.add(self)
1163            parent.reorder_child(self, 0)
1164            parent.child_set_property(self, 'expand', False)
1165
1166        self.image = Gtk.Image()
1167        self.set_message_type(type)
1168
1169        self.primary_text = Gtk.Label(label=text)
1170        self.primary_text.set_property('xalign', 0)
1171        self.primary_text.set_line_wrap(True)
1172        self.secondary_text = Gtk.Label()
1173        self.secondary_text.set_property('xalign', 0)
1174        self.secondary_text.set_line_wrap(True)
1175        self.secondary_text.set_no_show_all(True)
1176        self.secondary_text.set_selectable(True)
1177
1178        self.message_area = Gtk.Box(spacing=12, orientation=Gtk.Orientation.VERTICAL)
1179        self.message_area.pack_start(self.primary_text, False, False, 0)
1180        self.message_area.pack_start(self.secondary_text, False, False, 0)
1181
1182        box = Gtk.Box(spacing=6)
1183        box.pack_start(self.image, False, True, 0)
1184        box.pack_start(self.message_area, True, True, 0)
1185
1186        content_area = self.get_content_area()
1187        content_area.add(box)
1188        content_area.show_all()
1189
1190        self.action_area = self.get_action_area()
1191        self.action_area.set_property('layout-style', Gtk.ButtonBoxStyle.START)
1192
1193        if buttons != Gtk.ButtonsType.NONE:
1194            for text, response in self.buttons_map[buttons]:
1195                self.add_button(text, response)
1196
1197        self.primary_text_attributes = Pango.AttrList()
1198        # TODO: GI: Pango attr
1199        # self.primary_text_attributes.insert(
1200        #    Pango.AttrWeight(Pango.Weight.NORMAL, 0, -1))
1201        # self.primary_text_attributes.insert(
1202        #    Pango.AttrScale(Pango.SCALE_MEDIUM, 0, -1))'''
1203
1204        self.primary_text_emphasized_attributes = Pango.AttrList()
1205        # TODO: GI: Pango attr
1206        # self.primary_text_emphasized_attributes.insert(
1207        #    Pango.AttrWeight(Pango.Weight.BOLD, 0, -1))
1208        # self.primary_text_emphasized_attributes.insert(
1209        #    Pango.AttrScale(Pango.SCALE_LARGE, 0, -1))'''
1210
1211        self.connect('response', self.on_response)
1212
1213        # Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=710888
1214        # -> From pitivi: https://phabricator.freedesktop.org/D1103#34aa2703
1215        def _make_sure_revealer_does_nothing(widget):
1216            if not isinstance(widget, Gtk.Revealer):
1217                return
1218            widget.set_transition_type(Gtk.RevealerTransitionType.NONE)
1219
1220        self.forall(_make_sure_revealer_does_nothing)
1221
1222    def set_text(self, text):
1223        """
1224        Sets the primary text of the message bar
1225
1226        :param markup: a regular text string
1227        :type markup: string
1228        """
1229        self.primary_text.set_text(text)
1230
1231    def set_markup(self, markup):
1232        """
1233        Sets the primary markup text of the message bar
1234
1235        :param markup: a markup string
1236        :type markup: string
1237        """
1238        self.primary_text.set_markup(markup)
1239
1240    def set_secondary_text(self, text):
1241        """
1242        Sets the secondary text to the text
1243        specified by message_format
1244
1245        :param text: The text to be displayed
1246            as the secondary text or None.
1247        :type text: string
1248        """
1249        if text is None:
1250            self.secondary_text.hide()
1251            self.primary_text.set_attributes(self.primary_text_attributes)
1252        else:
1253            self.secondary_text.set_text(text)
1254            self.secondary_text.show()
1255            self.primary_text.set_attributes(self.primary_text_emphasized_attributes)
1256
1257    def set_secondary_markup(self, markup):
1258        """
1259        Sets the secondary text to the markup text
1260        specified by text.
1261
1262        :param text: A string containing the
1263            pango markup to use as secondary text.
1264        :type text: string
1265        """
1266        if markup is None:
1267            self.secondary_text.hide()
1268            self.primary_text.set_attributes(self.primary_text_attributes)
1269        else:
1270            self.secondary_text.set_markup(markup)
1271            self.secondary_text.show()
1272            self.primary_text.set_attributes(self.primary_text_emphasized_attributes)
1273
1274    def set_image(self, image):
1275        """
1276        Sets the contained image to the :class:`Gtk.Widget`
1277        specified by image.
1278
1279        :param image: the image widget
1280        :type image: :class:`Gtk.Widget`
1281        """
1282        box = self.image.get_parent()
1283        box.remove(self.image)
1284        self.image = image
1285        box.pack_start(self.image, False, True, 0)
1286        box.reorder_child(self.image, 0)
1287        self.image.show()
1288
1289    def get_image(self):
1290        """
1291        Gets the contained image
1292        """
1293        return self.image
1294
1295    def add_button(self, button_text, response_id):
1296        """
1297        Overrides :class:`Gtk.InfoBar` to prepend
1298        instead of append to the action area
1299
1300        :param button_text: text of button, or stock ID
1301        :type button_text: string
1302        :param response_id: response ID for the button
1303        :type response_id: int
1304        """
1305        button = Gtk.InfoBar.add_button(self, button_text, response_id)
1306        self.action_area.reorder_child(button, 0)
1307
1308        return button
1309
1310    def clear_buttons(self):
1311        """
1312        Removes all buttons currently
1313        placed in the action area
1314        """
1315        for button in self.action_area:
1316            self.action_area.remove(button)
1317
1318    def set_message_type(self, type):
1319        """
1320        Sets the message type of the message area.
1321
1322        :param type: the type of message: Gtk.MessageType.INFO,
1323            Gtk.MessageType.WARNING, Gtk.MessageType.QUESTION or
1324            Gtk.MessageType.ERROR.
1325        """
1326        if type != Gtk.MessageType.OTHER:
1327            self.image.set_from_icon_name(self.type_map[type], Gtk.IconSize.DIALOG)
1328
1329        Gtk.InfoBar.set_message_type(self, type)
1330
1331    def get_message_area(self):
1332        """
1333        Retrieves the message area
1334        """
1335        return self.message_area
1336
1337    def _show_message(
1338        self, message_type, text, secondary_text, markup, secondary_markup, timeout
1339    ):
1340        """
1341        Helper for the various `show_*` methods. See `show_info` for
1342        documentation on the parameters.
1343        """
1344        if text is markup is None:
1345            raise ValueError("text or markup must be specified")
1346
1347        self.set_message_type(message_type)
1348        if markup is None:
1349            self.set_text(text)
1350        else:
1351            self.set_markup(markup)
1352        if secondary_markup is None:
1353            self.set_secondary_text(secondary_text)
1354        else:
1355            self.set_secondary_markup(secondary_markup)
1356        self.show()
1357
1358        if timeout > 0:
1359            GLib.timeout_add_seconds(timeout, self.hide)
1360
1361    def show_info(
1362        self,
1363        text=None,
1364        secondary_text=None,
1365        markup=None,
1366        secondary_markup=None,
1367        timeout=5,
1368    ):
1369        """
1370        Convenience method which sets all
1371        required flags for an info message
1372
1373        :param text: the message to display
1374        :type text: string
1375        :param secondary_text: additional information
1376        :type secondary_text: string
1377        :param markup: the message to display, in Pango markup format
1378            (overrides `text`)
1379        :type markup: string
1380        :param secondary_markup: additional information, in Pango markup
1381            format (overrides `secondary_text`)
1382        :type secondary_markup: string
1383        :param timeout: after how many seconds the
1384            message should be hidden automatically,
1385            use 0 to disable this behavior
1386        :type timeout: int
1387        """
1388        self._show_message(
1389            Gtk.MessageType.INFO,
1390            text,
1391            secondary_text,
1392            markup,
1393            secondary_markup,
1394            timeout,
1395        )
1396
1397    def show_question(
1398        self, text=None, secondary_text=None, markup=None, secondary_markup=None
1399    ):
1400        """
1401        Convenience method which sets all
1402        required flags for a question message
1403
1404        :param text: the message to display
1405        :param secondary_text: additional information
1406        :param markup: the message to display, in Pango markup format
1407        :param secondary_markup: additional information, in Pango markup format
1408        """
1409        self._show_message(
1410            Gtk.MessageType.QUESTION, text, secondary_text, markup, secondary_markup, 0
1411        )
1412
1413    def show_warning(
1414        self, text=None, secondary_text=None, markup=None, secondary_markup=None
1415    ):
1416        """
1417        Convenience method which sets all
1418        required flags for a warning message
1419
1420        :param text: the message to display
1421        :param secondary_text: additional information
1422        :param markup: the message to display, in Pango markup format
1423        :param secondary_markup: additional information, in Pango markup format
1424        """
1425        self._show_message(
1426            Gtk.MessageType.WARNING, text, secondary_text, markup, secondary_markup, 0
1427        )
1428
1429    def show_error(
1430        self, text=None, secondary_text=None, markup=None, secondary_markup=None
1431    ):
1432        """
1433        Convenience method which sets all
1434        required flags for a warning message
1435
1436        :param text: the message to display
1437        :param secondary_text: additional information
1438        :param markup: the message to display, in Pango markup format
1439        :param secondary_markup: additional information, in Pango markup format
1440        """
1441        self._show_message(
1442            Gtk.MessageType.ERROR, text, secondary_text, markup, secondary_markup, 0
1443        )
1444
1445    def on_response(self, widget, response):
1446        """
1447        Handles the response for closing
1448        """
1449        if response == Gtk.ResponseType.CLOSE:
1450            self.hide()
1451
1452
1453#
1454# Message ID's used by the XMessageDialog
1455#
1456
1457XRESPONSE_YES = Gtk.ResponseType.YES
1458XRESPONSE_YES_ALL = 8000
1459XRESPONSE_NO = Gtk.ResponseType.NO
1460XRESPONSE_NO_ALL = 8001
1461XRESPONSE_CANCEL = Gtk.ResponseType.CANCEL
1462
1463
1464class XMessageDialog(Gtk.Dialog):
1465    '''Used to show a custom message dialog with custom buttons'''
1466
1467    def __init__(
1468        self,
1469        title,
1470        text,
1471        parent=None,
1472        show_yes=True,
1473        show_yes_all=True,
1474        show_no=True,
1475        show_no_all=True,
1476        show_cancel=True,
1477    ):
1478
1479        Gtk.Dialog.__init__(self, title=title, transient_for=parent)
1480
1481        #
1482        # TODO: Make these buttons a bit prettier
1483        #
1484
1485        if show_yes:
1486            self.add_button(Gtk.STOCK_YES, XRESPONSE_YES)
1487            self.set_default_response(XRESPONSE_YES)
1488
1489        if show_yes_all:
1490            self.add_button(_('Yes to all'), XRESPONSE_YES_ALL)
1491            self.set_default_response(XRESPONSE_YES_ALL)
1492
1493        if show_no:
1494            self.add_button(Gtk.STOCK_NO, XRESPONSE_NO)
1495            self.set_default_response(XRESPONSE_NO)
1496
1497        if show_no_all:
1498            self.add_button(_('No to all'), XRESPONSE_NO_ALL)
1499            self.set_default_response(XRESPONSE_NO_ALL)
1500
1501        if show_cancel:
1502            self.add_button(Gtk.STOCK_CANCEL, XRESPONSE_CANCEL)
1503            self.set_default_response(XRESPONSE_CANCEL)
1504
1505        vbox = self.get_content_area()
1506        self._label = Gtk.Label()
1507        self._label.set_use_markup(True)
1508        self._label.set_markup(text)
1509        vbox.pack_start(self._label, True, True, 0)
1510
1511
1512class FileCopyDialog(Gtk.Dialog):
1513    """
1514    Used to copy a list of files to a single destination directory
1515
1516    Usage:
1517        dialog = FileCopyDialog( [file_uri,..], destination_uri, text, parent)
1518        dialog.do_copy()
1519
1520    Do not use run() on this dialog!
1521    """
1522
1523    class CopyThread(Thread):
1524        def __init__(self, source, dest, callback_finish_single_copy, copy_flags):
1525            Thread.__init__(self, name='CopyThread')
1526            self.__source = source
1527            self.__dest = dest
1528            self.__cb_single_copy = callback_finish_single_copy
1529            self.__copy_flags = copy_flags
1530            self.__cancel = Gio.Cancellable()
1531            self.start()
1532
1533        def run(self):
1534            try:
1535                result = self.__source.copy(
1536                    self.__dest, flags=self.__copy_flags, cancellable=self.__cancel
1537                )
1538                GLib.idle_add(self.__cb_single_copy, self.__source, result, None)
1539            except GLib.Error as err:
1540                GLib.idle_add(self.__cb_single_copy, self.__source, False, err)
1541
1542        def cancel_copy(self):
1543            self.__cancel.cancel()
1544
1545    def __init__(
1546        self,
1547        file_uris,
1548        destination_uri,
1549        title,
1550        text=_("Saved %(count)s of %(total)s."),
1551        parent=None,
1552    ):
1553
1554        self.file_uris = file_uris
1555        self.destination_uri = destination_uri
1556        self.is_copying = False
1557
1558        Gtk.Dialog.__init__(self, title=title, transient_for=parent)
1559
1560        self.parent = parent
1561        self.count = 0
1562        self.total = len(file_uris)
1563        self.text = text
1564        self.overwrite_response = None
1565
1566        # self.set_modal(True)
1567        # self.set_decorated(False)
1568        self.set_resizable(False)
1569        # self.set_focus_on_map(False)
1570
1571        vbox = self.get_content_area()
1572
1573        vbox.set_spacing(12)
1574        vbox.set_border_width(12)
1575
1576        self._label = Gtk.Label()
1577        self._label.set_use_markup(True)
1578        self._label.set_markup(self.text % {'count': 0, 'total': self.total})
1579        vbox.pack_start(self._label, True, True, 0)
1580
1581        self._progress = Gtk.ProgressBar()
1582        self._progress.set_size_request(300, -1)
1583        vbox.pack_start(self._progress, True, True, 0)
1584
1585        self.show_all()
1586
1587        # TODO: Make dialog cancelable
1588        # self.cancel_button.connect('activate', lambda *e: self.cancel.cancel() )
1589
1590        self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
1591
1592    def do_copy(self):
1593        logger.info("Copy started.")
1594        self._start_next_copy()
1595        self.show_all()
1596        self.connect('response', self._on_response)
1597
1598    def run(self):
1599        raise NotImplementedError("Don't use this")
1600
1601    def _on_response(self, widget, response):
1602        logger.info("Copy complete.")
1603        self.destroy()
1604
1605    def _step(self):
1606        '''Steps the progress bar'''
1607        self.count += 1
1608        self._progress.set_fraction(clamp(self.count / float(self.total), 0, 1))
1609        self._label.set_markup(self.text % {'count': self.count, 'total': self.total})
1610
1611    def _start_next_copy(self, overwrite=False):
1612
1613        if self.count == len(self.file_uris):
1614            self.response(Gtk.ResponseType.OK)
1615            return
1616
1617        flags = Gio.FileCopyFlags.NONE
1618
1619        src_uri = self.file_uris[self.count]
1620        dst_uri = self.destination_uri + '/' + src_uri.split('/')[-1]
1621
1622        self.source = Gio.File.new_for_uri(src_uri)
1623        self.destination = Gio.File.new_for_uri(dst_uri)
1624
1625        if not overwrite:
1626            if self.destination.query_exists(None):
1627                if self.overwrite_response == XRESPONSE_YES_ALL:
1628                    overwrite = True
1629
1630                elif (
1631                    self.overwrite_response == XRESPONSE_NO_ALL
1632                    or self.overwrite_response == XRESPONSE_NO
1633                ):
1634
1635                    # only deny the overwrite once..
1636                    if self.overwrite_response == XRESPONSE_NO:
1637                        self.overwrite_response = None
1638
1639                    logging.info("NoOverwrite: %s" % self.destination.get_uri())
1640                    self._step()
1641                    GLib.idle_add(self._start_next_copy)  # don't recurse
1642                    return
1643                else:
1644                    self._query_overwrite()
1645                    return
1646
1647        if overwrite:
1648            flags = Gio.FileCopyFlags.OVERWRITE
1649            try:
1650                # Gio.FileCopyFlags.OVERWRITE doesn't actually work
1651                logging.info("DeleteDest : %s" % self.destination.get_uri())
1652                self.destination.delete()
1653            except GLib.Error:
1654                pass
1655
1656        logging.info("CopySource : %s" % self.source.get_uri())
1657        logging.info("CopyDest   : %s" % self.destination.get_uri())
1658
1659        # TODO g_file_copy_async() isn't introspectable
1660        # see https://github.com/exaile/exaile/issues/198 for details
1661        # self.source.copy_async( self.destination, self._finish_single_copy_async, flags=flags, cancellable=self.cancel )
1662
1663        self.cpthr = self.CopyThread(
1664            self.source, self.destination, self._finish_single_copy, flags
1665        )
1666
1667    def _finish_single_copy(self, source, success, error):
1668        if error:
1669            self._on_error(
1670                _("Error occurred while copying %s: %s")
1671                % (
1672                    GLib.markup_escape_text(self.source.get_uri()),
1673                    GLib.markup_escape_text(str(error)),
1674                )
1675            )
1676        if success:
1677            self._step()
1678            self._start_next_copy()
1679
1680    def _finish_single_copy_async(self, source, async_result):
1681
1682        try:
1683            if source.copy_finish(async_result):
1684                self._step()
1685                self._start_next_copy()
1686        except GLib.Error as e:
1687            self._on_error(
1688                _("Error occurred while copying %s: %s")
1689                % (
1690                    GLib.markup_escape_text(self.source.get_uri()),
1691                    GLib.markup_escape_text(str(e)),
1692                )
1693            )
1694
1695    def _query_overwrite(self):
1696
1697        self.hide()
1698
1699        text = _('File exists, overwrite %s ?') % GLib.markup_escape_text(
1700            self.destination.get_uri()
1701        )
1702        dialog = XMessageDialog(self.parent, text)
1703        dialog.connect('response', self._on_query_overwrite_response, dialog)
1704        dialog.show_all()
1705        dialog.grab_focus()
1706        self.query_dialog = dialog
1707
1708    def _on_query_overwrite_response(self, widget, response, dialog):
1709        dialog.destroy()
1710        self.overwrite_response = response
1711
1712        if response == Gtk.ResponseType.CANCEL:
1713            self.response(response)
1714        else:
1715            if response == XRESPONSE_NO or response == XRESPONSE_NO_ALL:
1716                overwrite = False
1717            else:
1718                overwrite = True
1719
1720            self.show_all()
1721            self._start_next_copy(overwrite)
1722
1723    def _on_error(self, message):
1724
1725        self.hide()
1726
1727        dialog = Gtk.MessageDialog(
1728            buttons=Gtk.ButtonsType.CLOSE,
1729            message_type=Gtk.MessageType.ERROR,
1730            modal=True,
1731            text=message,
1732            transient_for=self.parent,
1733        )
1734        dialog.set_markup(message)
1735        dialog.connect('response', self._on_error_response, dialog)
1736        dialog.show()
1737        dialog.grab_focus()
1738        self.error_dialog = dialog
1739
1740    def _on_error_response(self, widget, response, dialog):
1741        self.response(Gtk.ResponseType.CANCEL)
1742        dialog.destroy()
1743
1744
1745def ask_for_playlist_name(parent, playlist_manager, name=None):
1746    """
1747    Returns a user-selected name that is not already used
1748        in the specified playlist manager
1749
1750    :param name: A default name to show to the user
1751    Returns None if the user hits cancel
1752    """
1753
1754    while True:
1755
1756        dialog = TextEntryDialog(
1757            _('Playlist name:'),
1758            _('Add new playlist...'),
1759            name,
1760            parent=parent,
1761            okbutton=Gtk.STOCK_ADD,
1762        )
1763
1764        result = dialog.run()
1765        if result != Gtk.ResponseType.OK:
1766            return None
1767
1768        name = dialog.get_value()
1769
1770        if name == '':
1771            error(parent, _("You did not enter a name for your playlist"))
1772        elif playlist_manager.has_playlist_name(name):
1773            # name is already in use
1774            error(parent, _("The playlist name you entered is already in use."))
1775        else:
1776            return name
1777
1778
1779def save(
1780    parent, output_fname, output_setting=None, extensions=None, title=_("Save As")
1781):
1782    """
1783    A 'save' dialog utility function, which can be used to easily
1784    remember the last location the user saved something.
1785
1786    :param parent:          Parent window
1787    :param output_fname:    Output filename
1788    :param output_setting:  Setting to store the last 'output directory' saved at
1789    :param extensions:      Valid output extensions. Dict { '.m3u': 'Description', .. }
1790    :param title:           Title of dialog
1791
1792    :returns: None if user cancels, chosen URI otherwise
1793    """
1794
1795    uri = None
1796
1797    dialog = FileOperationDialog(
1798        title,
1799        parent,
1800        Gtk.FileChooserAction.SAVE,
1801        (
1802            Gtk.STOCK_CANCEL,
1803            Gtk.ResponseType.CANCEL,
1804            Gtk.STOCK_SAVE,
1805            Gtk.ResponseType.ACCEPT,
1806        ),
1807    )
1808
1809    if extensions is not None:
1810        dialog.add_extensions(extensions)
1811
1812    dialog.set_current_name(output_fname)
1813
1814    if output_setting:
1815        output_dir = settings.get_option(output_setting)
1816        if output_dir:
1817            dialog.set_current_folder_uri(output_dir)
1818
1819    if dialog.run() == Gtk.ResponseType.ACCEPT:
1820        uri = dialog.get_uri()
1821
1822        settings.set_option(output_setting, dialog.get_current_folder_uri())
1823
1824    dialog.destroy()
1825
1826    return uri
1827
1828
1829def export_playlist_dialog(playlist, parent=None):
1830    '''Exports the playlist to a user-specified path'''
1831    if playlist is not None:
1832        dialog = PlaylistExportDialog(playlist, parent)
1833        dialog.show()
1834
1835
1836def export_playlist_files(playlist, parent=None):
1837    '''Exports the playlist files to a user-specified URI'''
1838
1839    if playlist is None:
1840        return
1841
1842    def _on_uri(uri):
1843        if hasattr(playlist, 'get_playlist'):
1844            pl = playlist.get_playlist()
1845        else:
1846            pl = playlist
1847        pl_files = [track.get_loc_for_io() for track in pl]
1848        dialog = FileCopyDialog(
1849            pl_files, uri, _('Exporting %s') % playlist.name, parent=parent
1850        )
1851        dialog.do_copy()
1852
1853    dialog = DirectoryOpenDialog(
1854        title=_('Choose directory to export files to'), parent=parent
1855    )
1856    dialog.set_select_multiple(False)
1857    dialog.connect('uris-selected', lambda widget, uris: _on_uri(uris[0]))
1858    dialog.run()
1859    dialog.destroy()
1860