1# Copyright 2005 Joe Wreschnig, Michael Urman 2# 2013-2017 Nick Boultbee 3# 2013,2014 Christoph Reiter 4# 5# This program is free software; you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation; either version 2 of the License, or 8# (at your option) any later version. 9 10""" 11Functions for deleting files and songs with user interaction. 12 13Only use trash_files() or trash_songs() and TrashMenuItem(). 14""" 15 16import os 17 18from gi.repository import Gtk 19from senf import fsn2text 20 21from quodlibet import _ 22from quodlibet import print_w 23from quodlibet.util import trash 24from quodlibet.qltk import get_top_parent 25from quodlibet.qltk import Icons 26from quodlibet.qltk.msg import ErrorMessage, WarningMessage 27from quodlibet.qltk.wlw import WaitLoadWindow 28from quodlibet.qltk.x import MenuItem, Align 29from quodlibet.util.i18n import numeric_phrase 30from quodlibet.util.path import unexpand 31 32 33class FileListExpander(Gtk.Expander): 34 """A widget for showing a static list of file paths""" 35 36 def __init__(self, paths): 37 super(FileListExpander, self).__init__(label=_("Files:")) 38 self.set_resize_toplevel(True) 39 40 paths = [fsn2text(unexpand(p)) for p in paths] 41 lab = Gtk.Label(label="\n".join(paths)) 42 lab.set_alignment(0.0, 0.0) 43 lab.set_selectable(True) 44 win = Gtk.ScrolledWindow() 45 win.add_with_viewport(Align(lab, border=6)) 46 win.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) 47 win.set_shadow_type(Gtk.ShadowType.ETCHED_OUT) 48 win.set_size_request(-1, 100) 49 self.add(win) 50 win.show_all() 51 52 53class DeleteDialog(WarningMessage): 54 55 RESPONSE_DELETE = 1 56 """Return value of DeleteDialog.run() in case the passed files 57 should be deleted""" 58 59 @classmethod 60 def for_songs(cls, parent, songs): 61 """Create a delete dialog for deleting songs""" 62 63 description = _("The selected songs will be removed from the " 64 "library and their files deleted from disk.") 65 paths = [s("~filename") for s in songs] 66 return cls(parent, paths, description) 67 68 @classmethod 69 def for_files(cls, parent, paths): 70 """Create a delete dialog for deleting files""" 71 72 description = _("The selected files will be deleted from disk.") 73 return cls(parent, paths, description) 74 75 def __init__(self, parent, paths, description): 76 title = numeric_phrase("Delete %(file_count)d file permanently?", 77 "Delete %(file_count)d files permanently?", 78 len(paths), "file_count") 79 80 super(DeleteDialog, self).__init__( 81 get_top_parent(parent), 82 title, description, 83 buttons=Gtk.ButtonsType.NONE) 84 85 area = self.get_message_area() 86 exp = FileListExpander(paths) 87 exp.show() 88 area.pack_start(exp, False, True, 0) 89 90 self.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL) 91 self.add_icon_button(_("_Delete Files"), Icons.EDIT_DELETE, 92 self.RESPONSE_DELETE) 93 self.set_default_response(Gtk.ResponseType.CANCEL) 94 95 96class TrashDialog(WarningMessage): 97 98 RESPONSE_TRASH = 1 99 """Return value of TrashDialog.run() in case the passed files 100 should be moved to the trash""" 101 102 @classmethod 103 def for_songs(cls, parent, songs): 104 """Create a trash dialog for trashing songs""" 105 106 description = _("The selected songs will be removed from the " 107 "library and their files moved to the trash.") 108 paths = [s("~filename") for s in songs] 109 return cls(parent, paths, description) 110 111 @classmethod 112 def for_files(cls, parent, paths): 113 """Create a trash dialog for trashing files""" 114 115 description = _("The selected files will be moved to the trash.") 116 return cls(parent, paths, description) 117 118 def __init__(self, parent, paths, description): 119 120 title = numeric_phrase("Move %(file_count)d file to the trash?", 121 "Move %(file_count)d files to the trash?", 122 len(paths), "file_count") 123 super(TrashDialog, self).__init__( 124 get_top_parent(parent), 125 title, description, 126 buttons=Gtk.ButtonsType.NONE) 127 128 area = self.get_message_area() 129 exp = FileListExpander(paths) 130 exp.show() 131 area.pack_start(exp, False, True, 0) 132 133 self.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL) 134 self.add_icon_button(_("_Move to Trash"), Icons.USER_TRASH, 135 self.RESPONSE_TRASH) 136 self.set_default_response(Gtk.ResponseType.CANCEL) 137 138 139def TrashMenuItem(): 140 if trash.use_trash(): 141 return MenuItem(_("_Move to Trash"), Icons.USER_TRASH) 142 else: 143 return MenuItem(_("_Delete"), Icons.EDIT_DELETE) 144 145 146def _do_trash_songs(parent, songs, librarian): 147 dialog = TrashDialog.for_songs(parent, songs) 148 resp = dialog.run() 149 if resp != TrashDialog.RESPONSE_TRASH: 150 return 151 152 window_title = _("Moving %(current)d/%(total)d.") 153 154 w = WaitLoadWindow(parent, len(songs), window_title) 155 w.show() 156 157 ok = [] 158 failed = [] 159 for song in songs: 160 filename = song("~filename") 161 try: 162 trash.trash(filename) 163 except trash.TrashError as e: 164 print_w("Couldn't trash file (%s)" % e) 165 failed.append(song) 166 else: 167 ok.append(song) 168 w.step() 169 w.destroy() 170 171 if failed: 172 ErrorMessage(parent, 173 _("Unable to move to trash"), 174 _("Moving one or more files to the trash failed.") 175 ).run() 176 177 if ok: 178 librarian.remove(ok) 179 180 181def _do_trash_files(parent, paths): 182 dialog = TrashDialog.for_files(parent, paths) 183 resp = dialog.run() 184 if resp != TrashDialog.RESPONSE_TRASH: 185 return 186 187 window_title = _("Moving %(current)d/%(total)d.") 188 w = WaitLoadWindow(parent, len(paths), window_title) 189 w.show() 190 191 ok = [] 192 failed = [] 193 for path in paths: 194 try: 195 trash.trash(path) 196 except trash.TrashError: 197 failed.append(path) 198 else: 199 ok.append(path) 200 w.step() 201 w.destroy() 202 203 if failed: 204 ErrorMessage(parent, 205 _("Unable to move to trash"), 206 _("Moving one or more files to the trash failed.") 207 ).run() 208 209 210def _do_delete_songs(parent, songs, librarian): 211 dialog = DeleteDialog.for_songs(parent, songs) 212 resp = dialog.run() 213 if resp != DeleteDialog.RESPONSE_DELETE: 214 return 215 216 window_title = _("Deleting %(current)d/%(total)d.") 217 218 w = WaitLoadWindow(parent, len(songs), window_title) 219 w.show() 220 221 ok = [] 222 failed = [] 223 for song in songs: 224 filename = song("~filename") 225 try: 226 os.unlink(filename) 227 except EnvironmentError: 228 failed.append(song) 229 else: 230 ok.append(song) 231 w.step() 232 w.destroy() 233 234 if failed: 235 ErrorMessage(parent, 236 _("Unable to delete files"), 237 _("Deleting one or more files failed.") 238 ).run() 239 240 if ok: 241 librarian.remove(ok) 242 243 244def _do_delete_files(parent, paths): 245 dialog = DeleteDialog.for_files(parent, paths) 246 resp = dialog.run() 247 if resp != DeleteDialog.RESPONSE_DELETE: 248 return 249 250 window_title = _("Deleting %(current)d/%(total)d.") 251 252 w = WaitLoadWindow(parent, len(paths), window_title) 253 w.show() 254 255 ok = [] 256 failed = [] 257 for path in paths: 258 try: 259 os.unlink(path) 260 except EnvironmentError: 261 failed.append(path) 262 else: 263 ok.append(path) 264 w.step() 265 w.destroy() 266 267 if failed: 268 ErrorMessage(parent, 269 _("Unable to delete files"), 270 _("Deleting one or more files failed.") 271 ).run() 272 273 274def trash_files(parent, paths): 275 """Will try to move the files to the trash, 276 or if not possible, delete them permanently. 277 278 Will ask for confirmation in each case. 279 """ 280 281 if not paths: 282 return 283 284 # depends on the platform if we can 285 if trash.use_trash(): 286 _do_trash_files(parent, paths) 287 else: 288 _do_delete_files(parent, paths) 289 290 291def trash_songs(parent, songs, librarian): 292 """Will try to move the files associated with the songs to the trash, 293 or if not possible, delete them permanently. 294 295 Will ask for confirmation in each case. 296 297 The deleted songs will be removed from the librarian. 298 """ 299 300 if not songs: 301 return 302 303 # depends on the platform if we can 304 if trash.use_trash(): 305 _do_trash_songs(parent, songs, librarian) 306 else: 307 _do_delete_songs(parent, songs, librarian) 308