summaryrefslogtreecommitdiff
path: root/src/dialogs/ExportDialog.vala
blob: 5a61dc4a3ddfb189bfabb856250007baf41df5ec (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
/* Copyright 2016 Software Freedom Conservancy Inc.
 * Copyright 2017 Jens Georg <mail@jensge.org>
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
 * See the COPYING file in this distribution.
 */

public class ExportDialog : Gtk.Dialog {
    public const int DEFAULT_SCALE = 1200;

    // "Unmodified" and "Current," though they appear in the "Format:" popup menu, really
    // aren't formats so much as they are operating modes that determine specific formats.
    // Hereafter we'll refer to these as "special formats."
    public const int NUM_SPECIAL_FORMATS = 2;
    public const string UNMODIFIED_FORMAT_LABEL = _("Unmodified");
    public const string CURRENT_FORMAT_LABEL = _("Current");

    public const ScaleConstraint[] CONSTRAINT_ARRAY = { ScaleConstraint.ORIGINAL,
        ScaleConstraint.DIMENSIONS, ScaleConstraint.WIDTH, ScaleConstraint.HEIGHT };

    public const Jpeg.Quality[] QUALITY_ARRAY = { Jpeg.Quality.LOW, Jpeg.Quality.MEDIUM,
        Jpeg.Quality.HIGH, Jpeg.Quality.MAXIMUM };

    private static ScaleConstraint current_constraint = ScaleConstraint.ORIGINAL;
    private static ExportFormatParameters current_parameters = ExportFormatParameters.current();
    private static int current_scale = DEFAULT_SCALE;

    private Gtk.Grid table = new Gtk.Grid();
    private Gtk.ComboBoxText quality_combo;
    private Gtk.ComboBoxText constraint_combo;
    private Gtk.ComboBoxText format_combo;
    private Gtk.Switch export_metadata;
    private Gee.ArrayList<string> format_options = new Gee.ArrayList<string>();
    private Gtk.Entry pixels_entry;
    private Gtk.Widget ok_button;
    private bool in_insert = false;

    public ExportDialog(string title) {
        Object (use_header_bar: Resources.use_header_bar());

        this.title = title;
        resizable = false;

        //get information about the export settings out of our config backend
        Config.Facade config = Config.Facade.get_instance();
        current_parameters.mode = config.get_export_export_format_mode(); //ExportFormatMode
        current_parameters.specified_format = config.get_export_photo_file_format(); //PhotoFileFormat
        current_parameters.quality = config.get_export_quality(); //quality
        current_parameters.export_metadata = config.get_export_export_metadata(); //export metadata
        current_constraint = config.get_export_constraint(); //constraint
        current_scale = config.get_export_scale(); //scale

        quality_combo = new Gtk.ComboBoxText();
        int ctr = 0;
        foreach (Jpeg.Quality quality in QUALITY_ARRAY) {
            quality_combo.append_text(quality.to_string());
            if (quality == current_parameters.quality)
                quality_combo.set_active(ctr);
            ctr++;
        }

        constraint_combo = new Gtk.ComboBoxText();
        ctr = 0;
        foreach (ScaleConstraint constraint in CONSTRAINT_ARRAY) {
            constraint_combo.append_text(constraint.to_string());
            if (constraint == current_constraint)
                constraint_combo.set_active(ctr);
            ctr++;
        }

        format_combo = new Gtk.ComboBoxText();
        format_add_option(UNMODIFIED_FORMAT_LABEL);
        format_add_option(CURRENT_FORMAT_LABEL);
        foreach (PhotoFileFormat format in PhotoFileFormat.get_writeable()) {
            format_add_option(format.get_properties().get_user_visible_name());
        }

        pixels_entry = new Gtk.Entry();
        pixels_entry.set_max_length(6);
        pixels_entry.set_text("%d".printf(current_scale));

        // register after preparation to avoid signals during init
        constraint_combo.changed.connect(on_constraint_changed);
        format_combo.changed.connect(on_format_changed);
        pixels_entry.changed.connect(on_pixels_changed);
        pixels_entry.insert_text.connect(on_pixels_insert_text);
        pixels_entry.activate.connect(on_activate);

        // layout controls
        add_label(_("_Format:"), 0, 0, format_combo);
        add_control(format_combo, 1, 0);

        add_label(_("_Quality:"), 0, 1, quality_combo);
        add_control(quality_combo, 1, 1);

        add_label(_("_Scaling constraint:"), 0, 2, constraint_combo);
        add_control(constraint_combo, 1, 2);

        add_label(_("_Pixels:"), 0, 3, pixels_entry);
        add_control(pixels_entry, 1, 3);

        export_metadata = new Gtk.Switch ();
        add_label(_("Export _metadata:"), 0, 4, export_metadata);
        add_control(export_metadata, 1, 4);
        export_metadata.active = true;
        export_metadata.halign = Gtk.Align.START;

        table.set_row_spacing(6);
        table.set_column_spacing(12);
        table.set_border_width(18);

        ((Gtk.Box) get_content_area()).add(table);

        // add buttons to action area
        add_button(Resources.CANCEL_LABEL, Gtk.ResponseType.CANCEL);
        ok_button = add_button(Resources.OK_LABEL, Gtk.ResponseType.OK);
        set_default_response(Gtk.ResponseType.OK);

        ok_button.set_can_default(true);
        ok_button.has_default = true;
        set_default(ok_button);

        if (current_constraint == ScaleConstraint.ORIGINAL) {
            pixels_entry.sensitive = false;
            quality_combo.sensitive = false;
        }

        ok_button.grab_focus();
    }

    private void format_add_option(string format_name) {
        format_options.add(format_name);
        format_combo.append_text(format_name);
    }

    private void format_set_active_text(string text) {
        int selection_ticker = 0;

        foreach (string current_text in format_options) {
            if (current_text == text) {
                format_combo.set_active(selection_ticker);
                return;
            }
            selection_ticker++;
        }

        error("format_set_active_text: text '%s' isn't in combo box", text);
    }

    private PhotoFileFormat get_specified_format() {
        int index = format_combo.get_active();
        if (index < NUM_SPECIAL_FORMATS)
            index = NUM_SPECIAL_FORMATS;

        index -= NUM_SPECIAL_FORMATS;
        PhotoFileFormat[] writeable_formats = PhotoFileFormat.get_writeable();
        return writeable_formats[index];
    }

    private string get_label_for_parameters(ExportFormatParameters params) {
        switch(params.mode) {
            case ExportFormatMode.UNMODIFIED:
                return UNMODIFIED_FORMAT_LABEL;

            case ExportFormatMode.CURRENT:
                return CURRENT_FORMAT_LABEL;

            case ExportFormatMode.SPECIFIED:
                return params.specified_format.get_properties().get_user_visible_name();

            default:
                error("get_label_for_parameters: unrecognized export format mode");
        }
    }

    // unlike other parameters, which should be persisted across dialog executions, the
    // format parameters must be set each time the dialog is executed -- this is why
    // it's passed qualified as ref and not as out
    public bool execute(out int scale, out ScaleConstraint constraint,
        ref ExportFormatParameters parameters) {
        show_all();

        // if the export format mode isn't set to last (i.e., don't use the persisted settings),
        // reset the scale constraint to original size
        if (parameters.mode != ExportFormatMode.LAST) {
            current_constraint = constraint = ScaleConstraint.ORIGINAL;
            constraint_combo.set_active(0);
        }

        if (parameters.mode == ExportFormatMode.LAST)
            parameters = current_parameters;
        else if (parameters.mode == ExportFormatMode.SPECIFIED && !parameters.specified_format.can_write())
            parameters.specified_format = PhotoFileFormat.get_system_default_format();

        format_set_active_text(get_label_for_parameters(parameters));
        on_format_changed();

        bool ok = (run() == Gtk.ResponseType.OK);
        if (ok) {
            int index = constraint_combo.get_active();
            assert(index >= 0);
            constraint = CONSTRAINT_ARRAY[index];
            current_constraint = constraint;

            scale = int.parse(pixels_entry.get_text());
            if (constraint != ScaleConstraint.ORIGINAL)
                assert(scale > 0);
            current_scale = scale;

            parameters.export_metadata = export_metadata.sensitive ? export_metadata.active : false;

            if (format_combo.get_active_text() == UNMODIFIED_FORMAT_LABEL) {
                parameters.mode = current_parameters.mode = ExportFormatMode.UNMODIFIED;
            } else if (format_combo.get_active_text() == CURRENT_FORMAT_LABEL) {
                parameters.mode = current_parameters.mode = ExportFormatMode.CURRENT;
            } else {
                parameters.mode = current_parameters.mode = ExportFormatMode.SPECIFIED;
                parameters.specified_format = current_parameters.specified_format = get_specified_format();
                if (current_parameters.specified_format == PhotoFileFormat.JFIF)
                    parameters.quality = current_parameters.quality = QUALITY_ARRAY[quality_combo.get_active()];
            }

            //save current settings in config backend for reusing later
            Config.Facade config = Config.Facade.get_instance();
            config.set_export_export_format_mode(current_parameters.mode); //ExportFormatMode
            config.set_export_photo_file_format(current_parameters.specified_format); //PhotoFileFormat
            config.set_export_quality(current_parameters.quality); //quality
            config.set_export_export_metadata(current_parameters.export_metadata); //export metadata
            config.set_export_constraint(current_constraint); //constraint
            config.set_export_scale(current_scale); //scale
        } else {
            scale = 0;
            constraint = ScaleConstraint.ORIGINAL;
        }

        destroy();

        return ok;
    }

    private void add_label(string text, int x, int y, Gtk.Widget? widget = null) {
        Gtk.Label new_label = new Gtk.Label.with_mnemonic(text);
        new_label.halign = Gtk.Align.END;
        new_label.valign = Gtk.Align.CENTER;
        new_label.set_use_underline(true);

        if (widget != null)
            new_label.set_mnemonic_widget(widget);

        table.attach(new_label, x, y, 1, 1);
    }

    private void add_control(Gtk.Widget widget, int x, int y) {
        widget.halign = Gtk.Align.FILL;
        widget.valign = Gtk.Align.CENTER;
        widget.hexpand = true;
        widget.vexpand = true;

        table.attach(widget, x, y, 1, 1);
    }

    private void on_constraint_changed() {
        bool original = CONSTRAINT_ARRAY[constraint_combo.get_active()] == ScaleConstraint.ORIGINAL;
        bool jpeg = format_combo.get_active_text() ==
            PhotoFileFormat.JFIF.get_properties().get_user_visible_name();
        pixels_entry.sensitive = !original;
        quality_combo.sensitive = !original && jpeg;
        if (original)
            ok_button.sensitive = true;
        else
            on_pixels_changed();
    }

    private void on_format_changed() {
        bool original = CONSTRAINT_ARRAY[constraint_combo.get_active()] == ScaleConstraint.ORIGINAL;

        if (format_combo.get_active_text() == UNMODIFIED_FORMAT_LABEL) {
            // if the user wishes to export the media unmodified, then we just copy the original
            // files, so parameterizing size, quality, etc. is impossible -- these are all
            // just as they are in the original file. In this case, we set the scale constraint to
            // original and lock out all the controls
            constraint_combo.set_active(0); /* 0 == original size */
            constraint_combo.set_sensitive(false);
            quality_combo.set_sensitive(false);
            pixels_entry.sensitive = false;
            export_metadata.active = false;
            export_metadata.sensitive = false;
        } else if (format_combo.get_active_text() == CURRENT_FORMAT_LABEL) {
            // if the user wishes to export the media in its current format, we allow sizing but
            // not JPEG quality customization, because in a batch of many photos, it's not
            // guaranteed that all of them will be JPEGs or RAWs that get converted to JPEGs. Some
            // could be PNGs, and PNG has no notion of quality. So lock out the quality control.
            // If the user wants to set JPEG quality, he or she can explicitly specify the JPEG
            // format.
            constraint_combo.set_sensitive(true);
            quality_combo.set_sensitive(false);
            pixels_entry.sensitive = !original;
            export_metadata.sensitive = true;
        } else {
            // if the user has chosen a specific format, then allow JPEG quality customization if
            // the format is JPEG and the user is re-sizing the image, otherwise, disallow JPEG
            // quality customization; always allow scaling.
            constraint_combo.set_sensitive(true);
            bool jpeg = get_specified_format() == PhotoFileFormat.JFIF;
            quality_combo.sensitive = !original && jpeg;
            export_metadata.sensitive = true;
        }
    }

    private void on_activate() {
        response(Gtk.ResponseType.OK);
    }

    private void on_pixels_changed() {
        ok_button.sensitive = (pixels_entry.get_text_length() > 0) && (int.parse(pixels_entry.get_text()) > 0);
    }

    private void on_pixels_insert_text(string text, int length, ref int position) {
        // This is necessary because SignalHandler.block_by_func() is not properly bound
        if (in_insert)
            return;

        in_insert = true;

        if (length == -1)
            length = (int) text.length;

        // only permit numeric text
        string new_text = "";
        for (int ctr = 0; ctr < length; ctr++) {
            if (text[ctr].isdigit()) {
                new_text += ((char) text[ctr]).to_string();
            }
        }

        if (new_text.length > 0)
            pixels_entry.insert_text(new_text, (int) new_text.length, ref position);

        Signal.stop_emission_by_name(pixels_entry, "insert-text");

        in_insert = false;
    }
}