1/*
2 * This file is part of GNOME LaTeX.
3 *
4 * Copyright © 2011, 2012 Sébastien Wilmet
5 *
6 * GNOME LaTeX is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * GNOME LaTeX is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with GNOME LaTeX.  If not, see <http://www.gnu.org/licenses/>.
18 */
19
20using Gtk;
21
22public class CleanBuildFiles : GLib.Object
23{
24    private enum CleanFileColumn
25    {
26        DELETE,
27        NAME,
28        FILE,
29        N_COLUMNS
30    }
31
32    private unowned MainWindow  _main_window;
33    private Document _doc;
34    private GLib.Settings _settings;
35
36    public CleanBuildFiles (MainWindow main_window, Document doc)
37    {
38        _main_window = main_window;
39        _doc = doc;
40        _settings = new GLib.Settings ("org.gnome.gnome-latex.preferences.latex");
41    }
42
43    public void clean ()
44    {
45        if (!_doc.is_main_file_a_tex_file ())
46            return;
47
48        Gee.ArrayList<File> files_to_delete;
49        File directory;
50
51        Project? project = _doc.get_project ();
52
53        // the document is part of a project
54        if (project != null)
55        {
56            directory = project.directory;
57            files_to_delete = get_build_files_in_directory (directory);
58        }
59        else
60        {
61            directory = _doc.location.get_parent ();
62            files_to_delete = get_build_files_simple ();
63        }
64
65        bool no_confirm = _settings.get_boolean ("no-confirm-clean");
66
67        if (files_to_delete.size == 0)
68        {
69            if (!no_confirm)
70                show_info_no_file ();
71            return;
72        }
73
74        if (no_confirm)
75        {
76            foreach (File file_to_delete in files_to_delete)
77                Utils.delete_file (file_to_delete);
78        }
79        else
80            confirm_cleanup (files_to_delete, directory);
81    }
82
83    // Get the list of build files for a simple document (not part of a project).
84    private Gee.ArrayList<File> get_build_files_simple ()
85    {
86        File location = _doc.location;
87        File directory = location.get_parent ();
88
89        string exts = _settings.get_string ("clean-extensions");
90        string[] extensions = exts.split (" ");
91
92        string shortname = Tepl.utils_get_file_shortname (location.get_basename ());
93
94        Gee.ArrayList<File> files_to_delete = new Gee.ArrayList<File> ();
95
96        foreach (string extension in extensions)
97        {
98            string basename = shortname + extension;
99            File file = directory.get_child (basename);
100            if (file.query_exists ())
101                files_to_delete.add (file);
102        }
103
104        return files_to_delete;
105    }
106
107    // Get the list of build files of a project.
108    private Gee.ArrayList<File> get_build_files_in_directory (File directory)
109    {
110        string exts = _settings.get_string ("clean-extensions");
111        string[] extensions = exts.split (" ");
112
113        Gee.ArrayList<File> files_to_delete = new Gee.ArrayList<File> ();
114        FileEnumerator enumerator;
115
116        try
117        {
118            enumerator = directory.enumerate_children ("standard::type,standard::name",
119                FileQueryInfoFlags.NONE);
120        }
121        catch (Error e)
122        {
123            warning ("Clean build files: %s", e.message);
124            return files_to_delete;
125        }
126
127        while (true)
128        {
129            FileInfo? info = null;
130
131            try
132            {
133                info = enumerator.next_file ();
134            }
135            catch (Error e)
136            {
137                warning ("Clean build files: %s", e.message);
138                break;
139            }
140
141            if (info == null)
142                break;
143
144            string name = info.get_name ();
145
146            // don't take into account hidden files and directories
147            // example: Git have a *.idx file in the .git/ directory
148            if (name[0] == '.')
149                continue;
150
151            File file = directory.get_child (name);
152
153            FileType type = info.get_file_type ();
154            if (type == FileType.DIRECTORY)
155            {
156                var files_to_delete_in_dir = get_build_files_in_directory (file);
157                files_to_delete.add_all (files_to_delete_in_dir);
158                continue;
159            }
160
161            foreach (string ext in extensions)
162            {
163                if (name.has_suffix (ext))
164                {
165                    files_to_delete.add (file);
166                    break;
167                }
168            }
169        }
170
171        return files_to_delete;
172    }
173
174    private void confirm_cleanup (Gee.ArrayList<File> files_to_delete, File directory)
175    {
176        return_if_fail (0 < files_to_delete.size);
177
178        TreeView list_files = get_list_files (files_to_delete, directory);
179        Dialog dialog = get_dialog (list_files);
180
181        run_dialog (dialog, list_files.get_model ());
182    }
183
184    private TreeView get_list_files (Gee.ArrayList<File> files_to_delete, File directory)
185    {
186        TreeView treeview = new TreeView ();
187        treeview.set_size_request (260, 120);
188        treeview.headers_visible = false;
189        treeview.enable_search = false;
190
191        Gtk.ListStore store = new Gtk.ListStore (CleanFileColumn.N_COLUMNS,
192            typeof (bool), typeof (string), typeof (File));
193
194        store.set_sort_func (0, on_sort_list_files);
195        store.set_sort_column_id (0, SortType.ASCENDING);
196
197        // fill the list
198        foreach (File file_to_delete in files_to_delete)
199        {
200            string relative_path = Utils.get_relative_path (directory, file_to_delete,
201                directory);
202            TreeIter iter;
203            store.append (out iter);
204            store.set (iter,
205                CleanFileColumn.DELETE, true,
206                CleanFileColumn.NAME, relative_path,
207                CleanFileColumn.FILE, file_to_delete
208            );
209        }
210
211        treeview.set_model (store);
212        CellRendererToggle toggle_renderer = new CellRendererToggle ();
213
214        toggle_renderer.toggled.connect ((path_str) =>
215        {
216            TreePath path = new TreePath.from_string (path_str);
217            TreeIter iter;
218            bool active;
219            store.get_iter (out iter, path);
220            store.get (iter, CleanFileColumn.DELETE, out active);
221            // inverse the value
222            store.set (iter, CleanFileColumn.DELETE, !active);
223        });
224
225        TreeViewColumn column = new TreeViewColumn.with_attributes ("Delete?",
226            toggle_renderer, "active", CleanFileColumn.DELETE, null);
227        treeview.append_column (column);
228
229        CellRendererText text_renderer = new CellRendererText ();
230        column = new TreeViewColumn.with_attributes ("Name", text_renderer,
231            "text", CleanFileColumn.NAME);
232        treeview.append_column (column);
233
234        return treeview;
235    }
236
237    private Dialog get_dialog (TreeView list_files)
238    {
239        Dialog dialog = new Dialog.with_buttons (null,
240            _main_window,
241            DialogFlags.DESTROY_WITH_PARENT,
242            _("_Cancel"), ResponseType.CANCEL,
243            _("_Delete"), ResponseType.ACCEPT
244        );
245
246        Grid grid = new Grid ();
247        grid.set_column_spacing (12);
248        grid.set_row_spacing (8);
249        grid.border_width = 5;
250
251        Box content_area = dialog.get_content_area () as Box;
252        content_area.pack_start (grid);
253
254        /* image */
255        Image image = new Image.from_icon_name ("dialog-warning", IconSize.DIALOG);
256        image.set_valign (Align.START);
257        grid.attach (image, 0, 0, 1, 3);
258
259        /* primary label */
260        Label primary_label = new Label (null);
261        primary_label.set_line_wrap (true);
262        primary_label.set_use_markup (true);
263        primary_label.set_halign (Align.START);
264        primary_label.set_selectable (true);
265        primary_label.margin_bottom = 4;
266        primary_label.set_markup ("<span weight=\"bold\" size=\"larger\">"
267            + _("Do you really want to delete these files?") + "</span>");
268
269        grid.attach (primary_label, 1, 0, 1, 1);
270
271        /* secondary label */
272        Label select_label = new Label (_("Select the files you want to delete:"));
273        select_label.set_line_wrap (true);
274        select_label.set_halign (Align.START);
275        grid.attach (select_label, 1, 1, 1, 1);
276
277        /* list of files with a scrollbar */
278        ScrolledWindow sw = Utils.add_scrollbar (list_files);
279        sw.set_shadow_type (ShadowType.IN);
280        sw.expand = true;
281        grid.attach (sw, 1, 2, 1, 1);
282
283        grid.show_all ();
284
285        return dialog;
286    }
287
288    private void run_dialog (Dialog dialog, TreeModel list_store)
289    {
290        if (dialog.run () == ResponseType.ACCEPT)
291        {
292            // get files to delete
293            File[] selected_files = {};
294            TreeIter iter;
295            bool valid = list_store.get_iter_first (out iter);
296            while (valid)
297            {
298                bool selected;
299                File file_to_delete;
300
301                list_store.get (iter,
302                    CleanFileColumn.DELETE, out selected,
303                    CleanFileColumn.FILE, out file_to_delete
304                );
305
306                if (selected)
307                    selected_files += file_to_delete;
308
309                valid = list_store.iter_next (ref iter);
310            }
311
312            foreach (File file_to_delete in selected_files)
313                Utils.delete_file (file_to_delete);
314        }
315
316        dialog.destroy ();
317    }
318
319    private int on_sort_list_files (TreeModel model, TreeIter a, TreeIter b)
320    {
321        string name_a;
322        string name_b;
323
324        model.get (a, CleanFileColumn.NAME, out name_a);
325        model.get (b, CleanFileColumn.NAME, out name_b);
326
327        return name_a.collate (name_b);
328    }
329
330    private void show_info_no_file ()
331    {
332        Dialog dialog = new MessageDialog (_main_window,
333            DialogFlags.DESTROY_WITH_PARENT,
334            MessageType.INFO,
335            ButtonsType.OK,
336            "%s", _("No build file to clean up."));
337
338        dialog.run ();
339        dialog.destroy ();
340    }
341}
342