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