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