1/* Copyright 2016 Software Freedom Conservancy Inc.
2 *
3 * This software is licensed under the GNU LGPL (version 2.1 or later).
4 * See the COPYING file in this distribution.
5 */
6
7public enum ContentLayout {
8    STANDARD_SIZE,
9    CUSTOM_SIZE,
10    IMAGE_PER_PAGE
11}
12
13public class PrintSettings {
14    public const int MIN_CONTENT_PPI = 72;    /* 72 ppi is the pixel resolution of a 14" VGA
15                                                 display -- it's standard for historical reasons */
16    public const int MAX_CONTENT_PPI = 1200;  /* 1200 ppi is appropriate for a 3600 dpi imagesetter
17                                                 used to produce photographic plates for commercial
18                                                 printing -- it's the highest pixel resolution
19                                                 commonly used */
20    private ContentLayout content_layout;
21    private Measurement content_width;
22    private Measurement content_height;
23    private int content_ppi;
24    private int image_per_page_selection;
25    private int size_selection;
26    private bool match_aspect_ratio;
27    private bool print_titles;
28    private string print_titles_font;
29
30    public PrintSettings() {
31        Config.Facade config = Config.Facade.get_instance();
32
33        MeasurementUnit units = (MeasurementUnit) config.get_printing_content_units();
34
35        content_width = Measurement(config.get_printing_content_width(), units);
36        content_height = Measurement(config.get_printing_content_height(), units);
37        size_selection = config.get_printing_size_selection();
38        content_layout = (ContentLayout) config.get_printing_content_layout();
39        match_aspect_ratio = config.get_printing_match_aspect_ratio();
40        print_titles = config.get_printing_print_titles();
41        print_titles_font = config.get_printing_titles_font();
42        image_per_page_selection = config.get_printing_images_per_page();
43        content_ppi = config.get_printing_content_ppi();
44    }
45
46    public void save() {
47        Config.Facade config = Config.Facade.get_instance();
48
49        config.set_printing_content_units(content_width.unit);
50        config.set_printing_content_width(content_width.value);
51        config.set_printing_content_height(content_height.value);
52        config.set_printing_size_selection(size_selection);
53        config.set_printing_content_layout(content_layout);
54        config.set_printing_match_aspect_ratio(match_aspect_ratio);
55        config.set_printing_print_titles(print_titles);
56        config.set_printing_titles_font(print_titles_font);
57        config.set_printing_images_per_page(image_per_page_selection);
58        config.set_printing_content_ppi(content_ppi);
59    }
60
61
62    public Measurement get_content_width() {
63        switch (get_content_layout()) {
64            case ContentLayout.STANDARD_SIZE:
65            case ContentLayout.IMAGE_PER_PAGE:
66                return (PrintManager.get_instance().get_standard_sizes()[
67                    get_size_selection()]).width;
68
69            case ContentLayout.CUSTOM_SIZE:
70                return content_width;
71
72            default:
73                error("unknown ContentLayout enumeration value");
74        }
75    }
76
77    public Measurement get_content_height() {
78        switch (get_content_layout()) {
79            case ContentLayout.STANDARD_SIZE:
80            case ContentLayout.IMAGE_PER_PAGE:
81                return (PrintManager.get_instance().get_standard_sizes()[
82                    get_size_selection()]).height;
83
84            case ContentLayout.CUSTOM_SIZE:
85                return content_height;
86
87            default:
88                error("unknown ContentLayout enumeration value");
89        }
90    }
91
92    public Measurement get_minimum_content_dimension() {
93        return Measurement(0.5, MeasurementUnit.INCHES);
94    }
95
96    public Measurement get_maximum_content_dimension() {
97        return Measurement(30, MeasurementUnit.INCHES);
98    }
99
100    public bool is_match_aspect_ratio_enabled() {
101        return match_aspect_ratio;
102    }
103
104    public bool is_print_titles_enabled() {
105        return print_titles;
106    }
107
108    public int get_content_ppi() {
109        return content_ppi;
110    }
111
112    public int get_image_per_page_selection() {
113        return image_per_page_selection;
114    }
115
116    public int get_size_selection() {
117        return size_selection;
118    }
119
120    public ContentLayout get_content_layout() {
121        return content_layout;
122    }
123
124    public void set_content_layout(ContentLayout content_layout) {
125        this.content_layout = content_layout;
126    }
127
128    public void set_content_width(Measurement content_width) {
129        this.content_width = content_width;
130    }
131
132    public void set_content_height(Measurement content_height) {
133        this.content_height = content_height;
134    }
135
136    public void set_content_ppi(int content_ppi) {
137        this.content_ppi = content_ppi;
138    }
139
140    public void set_image_per_page_selection(int image_per_page_selection) {
141        this.image_per_page_selection = image_per_page_selection;
142    }
143
144    public void set_size_selection(int size_selection) {
145        this.size_selection = size_selection;
146    }
147
148    public void set_match_aspect_ratio_enabled(bool enable_state) {
149        this.match_aspect_ratio = enable_state;
150    }
151
152    public void set_print_titles_enabled(bool print_titles) {
153        this.print_titles = print_titles;
154    }
155
156    public void set_print_titles_font(string fontname) {
157        this.print_titles_font = fontname;
158    }
159
160    public string get_print_titles_font() {
161        return this.print_titles_font;
162    }
163}
164
165/* we define our own measurement enum instead of using the Gtk.Unit enum
166   provided by Gtk+ 2.0 because Gtk.Unit doesn't define a CENTIMETERS
167   constant (though it does define an MM for millimeters). This is
168   unfortunate, because in metric countries people like to think about
169   paper sizes for printing in CM not MM. so, to avoid having to
170   multiply and divide everything by 10 (which is error prone) to convert
171   from CM to MM and vice-versa whenever we read or write measurements, we
172   eschew Gtk.Unit and substitute our own */
173public enum MeasurementUnit {
174    INCHES,
175    CENTIMETERS
176}
177
178public struct Measurement {
179    private const double CENTIMETERS_PER_INCH = 2.54;
180    private const double INCHES_PER_CENTIMETER = (1.0 / 2.54);
181
182    public double value;
183    public MeasurementUnit unit;
184
185    public Measurement(double value, MeasurementUnit unit) {
186        this.value = value;
187        this.unit = unit;
188    }
189
190    public Measurement convert_to(MeasurementUnit to_unit) {
191        if (unit == to_unit)
192            return this;
193
194        if (to_unit == MeasurementUnit.INCHES) {
195            return Measurement(value * INCHES_PER_CENTIMETER, MeasurementUnit.INCHES);
196        } else if (to_unit == MeasurementUnit.CENTIMETERS) {
197            return Measurement(value * CENTIMETERS_PER_INCH, MeasurementUnit.CENTIMETERS);
198        } else {
199            error("unrecognized unit");
200        }
201    }
202
203    public bool is_less_than(Measurement rhs) {
204        Measurement converted_rhs = (unit == rhs.unit) ? rhs : rhs.convert_to(unit);
205        return (value < converted_rhs.value);
206    }
207
208    public bool is_greater_than(Measurement rhs) {
209        Measurement converted_rhs = (unit == rhs.unit) ? rhs : rhs.convert_to(unit);
210        return (value > converted_rhs.value);
211    }
212}
213
214private enum PrintLayout {
215    ENTIRE_PAGE,
216    TWO_PER_PAGE,
217    FOUR_PER_PAGE,
218    SIX_PER_PAGE,
219    EIGHT_PER_PAGE,
220    SIXTEEN_PER_PAGE,
221    THIRTY_TWO_PER_PAGE;
222
223    public static PrintLayout[] get_all() {
224        return {
225            ENTIRE_PAGE,
226            TWO_PER_PAGE,
227            FOUR_PER_PAGE,
228            SIX_PER_PAGE,
229            EIGHT_PER_PAGE,
230            SIXTEEN_PER_PAGE,
231            THIRTY_TWO_PER_PAGE
232        };
233    }
234
235    public int get_per_page() {
236        int[] per_page = { 1, 2, 4, 6, 8, 16, 32 };
237
238        return per_page[this];
239    }
240
241    public int get_x() {
242        int[] x = { 1, 1, 2, 2, 2, 4, 4 };
243
244        return x[this];
245     }
246
247    public int get_y() {
248        int[] y = { 1, 2, 2, 3, 4, 4, 8 };
249
250        return y[this];
251    }
252
253    public string to_string() {
254        string[] labels = {
255            _("Fill the entire page"),
256            _("2 images per page"),
257            _("4 images per page"),
258            _("6 images per page"),
259            _("8 images per page"),
260            _("16 images per page"),
261            _("32 images per page")
262        };
263
264        return labels[this];
265    }
266}
267
268[GtkTemplate (ui = "/org/gnome/Shotwell/ui/printing_widget.ui")]
269public class CustomPrintTab : Gtk.Box {
270    private const int INCHES_COMBO_CHOICE = 0;
271    private const int CENTIMETERS_COMBO_CHOICE = 1;
272
273    [GtkChild]
274    private Gtk.RadioButton standard_size_radio;
275    [GtkChild]
276    private Gtk.RadioButton custom_size_radio;
277    [GtkChild]
278    private Gtk.RadioButton image_per_page_radio;
279    [GtkChild]
280    private Gtk.ComboBoxText image_per_page_combo;
281    [GtkChild]
282    private Gtk.ComboBoxText standard_sizes_combo;
283    [GtkChild]
284    private Gtk.ComboBoxText units_combo;
285    [GtkChild]
286    private Gtk.Entry custom_width_entry;
287    [GtkChild]
288    private Gtk.Entry custom_height_entry;
289    [GtkChild]
290    private Gtk.Entry ppi_entry;
291    [GtkChild]
292    private Gtk.CheckButton aspect_ratio_check;
293    [GtkChild]
294    private Gtk.CheckButton title_print_check;
295    [GtkChild]
296    private Gtk.FontButton title_print_font;
297
298    private Measurement local_content_width = Measurement(5.0, MeasurementUnit.INCHES);
299    private Measurement local_content_height = Measurement(5.0, MeasurementUnit.INCHES);
300    private int local_content_ppi;
301    private bool is_text_insertion_in_progress = false;
302    private PrintJob source_job;
303
304    public CustomPrintTab(PrintJob source_job) {
305        this.source_job = source_job;
306
307        standard_size_radio.clicked.connect(on_radio_group_click);
308        custom_size_radio.clicked.connect(on_radio_group_click);
309        image_per_page_radio.clicked.connect(on_radio_group_click);
310
311        foreach (PrintLayout layout in PrintLayout.get_all()) {
312            image_per_page_combo.append_text(layout.to_string());
313        }
314
315        unowned StandardPrintSize[] standard_sizes = PrintManager.get_instance().get_standard_sizes();
316        standard_sizes_combo.set_row_separator_func(standard_sizes_combo_separator_func);
317        foreach (StandardPrintSize size in standard_sizes) {
318            standard_sizes_combo.append_text(size.name);
319        }
320
321        standard_sizes_combo.set_active(9 * Resources.get_default_measurement_unit());
322
323        custom_width_entry.insert_text.connect(on_entry_insert_text);
324        custom_width_entry.focus_out_event.connect(on_width_entry_focus_out);
325
326        custom_height_entry.insert_text.connect(on_entry_insert_text);
327        custom_height_entry.focus_out_event.connect(on_height_entry_focus_out);
328
329        units_combo.changed.connect(on_units_combo_changed);
330        units_combo.set_active(Resources.get_default_measurement_unit());
331
332        ppi_entry.insert_text.connect(on_ppi_entry_insert_text);
333        ppi_entry.focus_out_event.connect(on_ppi_entry_focus_out);
334
335        sync_state_from_job(source_job);
336
337        show_all();
338
339        /* connect this signal after state is sync'd */
340        aspect_ratio_check.clicked.connect(on_aspect_ratio_check_clicked);
341    }
342
343    private void on_aspect_ratio_check_clicked() {
344        if (aspect_ratio_check.get_active()) {
345            local_content_width =
346                Measurement(local_content_height.value * source_job.get_source_aspect_ratio(),
347                local_content_height.unit);
348            custom_width_entry.set_text(format_measurement(local_content_width));
349        }
350    }
351
352    private bool on_width_entry_focus_out(Gdk.EventFocus event) {
353        if (custom_width_entry.get_text() == (format_measurement_as(local_content_width,
354            get_user_unit_choice())))
355            return false;
356
357        Measurement new_width = get_width_entry_value();
358        Measurement min_width = source_job.get_local_settings().get_minimum_content_dimension();
359        Measurement max_width = source_job.get_local_settings().get_maximum_content_dimension();
360
361        if (new_width.is_less_than(min_width) || new_width.is_greater_than(max_width)) {
362            custom_width_entry.set_text(format_measurement(local_content_width));
363            return false;
364        }
365
366        if (is_match_aspect_ratio_enabled()) {
367            Measurement new_height =
368                Measurement(new_width.value / source_job.get_source_aspect_ratio(),
369                new_width.unit);
370            local_content_height = new_height;
371            custom_height_entry.set_text(format_measurement(new_height));
372        }
373
374        local_content_width = new_width;
375        custom_width_entry.set_text(format_measurement(new_width));
376        return false;
377    }
378
379    private string format_measurement(Measurement measurement) {
380        return "%.2f".printf(measurement.value);
381    }
382
383    private string format_measurement_as(Measurement measurement, MeasurementUnit to_unit) {
384        Measurement converted_measurement = (measurement.unit == to_unit) ? measurement :
385            measurement.convert_to(to_unit);
386        return format_measurement(converted_measurement);
387    }
388
389    private bool on_ppi_entry_focus_out(Gdk.EventFocus event) {
390        set_content_ppi(int.parse(ppi_entry.get_text()));
391        return false;
392    }
393
394    private void on_ppi_entry_insert_text(Gtk.Editable editable, string text, int length,
395        ref int position) {
396        Gtk.Entry sender = (Gtk.Entry) editable;
397
398        if (is_text_insertion_in_progress)
399            return;
400
401        is_text_insertion_in_progress = true;
402
403        if (length == -1)
404            length = (int) text.length;
405
406        string new_text = "";
407        for (int ctr = 0; ctr < length; ctr++) {
408            if (text[ctr].isdigit())
409                new_text += ((char) text[ctr]).to_string();
410        }
411
412        if (new_text.length > 0)
413            sender.insert_text(new_text, (int) new_text.length, ref position);
414
415        Signal.stop_emission_by_name(sender, "insert-text");
416
417        is_text_insertion_in_progress = false;
418    }
419
420    private bool on_height_entry_focus_out(Gdk.EventFocus event) {
421        if (custom_height_entry.get_text() == (format_measurement_as(local_content_height,
422            get_user_unit_choice())))
423            return false;
424
425        Measurement new_height = get_height_entry_value();
426        Measurement min_height = source_job.get_local_settings().get_minimum_content_dimension();
427        Measurement max_height = source_job.get_local_settings().get_maximum_content_dimension();
428
429        if (new_height.is_less_than(min_height) || new_height.is_greater_than(max_height)) {
430            custom_height_entry.set_text(format_measurement(local_content_height));
431            return false;
432        }
433
434        if (is_match_aspect_ratio_enabled()) {
435            Measurement new_width =
436                Measurement(new_height.value * source_job.get_source_aspect_ratio(),
437                new_height.unit);
438            local_content_width = new_width;
439            custom_width_entry.set_text(format_measurement(new_width));
440        }
441
442        local_content_height = new_height;
443        custom_height_entry.set_text(format_measurement(new_height));
444        return false;
445    }
446
447    private MeasurementUnit get_user_unit_choice() {
448        if (units_combo.get_active() == INCHES_COMBO_CHOICE) {
449            return MeasurementUnit.INCHES;
450        } else if (units_combo.get_active() == CENTIMETERS_COMBO_CHOICE) {
451            return MeasurementUnit.CENTIMETERS;
452        } else {
453            error("unknown unit combo box choice");
454        }
455    }
456
457    private void set_user_unit_choice(MeasurementUnit unit) {
458        if (unit == MeasurementUnit.INCHES) {
459            units_combo.set_active(INCHES_COMBO_CHOICE);
460        } else if (unit == MeasurementUnit.CENTIMETERS) {
461            units_combo.set_active(CENTIMETERS_COMBO_CHOICE);
462        } else {
463            error("unknown MeasurementUnit enumeration");
464        }
465    }
466
467    private Measurement get_width_entry_value() {
468        return Measurement(double.parse(custom_width_entry.get_text()), get_user_unit_choice());
469    }
470
471    private Measurement get_height_entry_value() {
472        return Measurement(double.parse(custom_height_entry.get_text()), get_user_unit_choice());
473    }
474
475    private void on_entry_insert_text(Gtk.Editable editable, string text, int length,
476        ref int position) {
477
478        Gtk.Entry sender = (Gtk.Entry) editable;
479
480        if (is_text_insertion_in_progress)
481            return;
482
483        is_text_insertion_in_progress = true;
484
485        if (length == -1)
486            length = (int) text.length;
487
488        unowned string decimal_point = Posix.nl_langinfo (Posix.NLItem.RADIXCHAR);
489
490        bool contains_decimal_point = sender.get_text().contains(decimal_point);
491
492        string new_text = "";
493        for (int ctr = 0; ctr < length; ctr++) {
494            if (text[ctr].isdigit()) {
495                new_text += ((char) text[ctr]).to_string();
496            } else if ((!contains_decimal_point) && (text[ctr] == decimal_point[0])) {
497                new_text += ((char) text[ctr]).to_string();
498            }
499        }
500
501        if (new_text.length > 0)
502            sender.insert_text(new_text, (int) new_text.length, ref position);
503
504        Signal.stop_emission_by_name(sender, "insert-text");
505
506        is_text_insertion_in_progress = false;
507    }
508
509    private void sync_state_from_job(PrintJob job) {
510        assert(job.get_local_settings().get_content_width().unit ==
511            job.get_local_settings().get_content_height().unit);
512
513        Measurement constrained_width = job.get_local_settings().get_content_width();
514        if (job.get_local_settings().is_match_aspect_ratio_enabled())
515            constrained_width = Measurement(job.get_local_settings().get_content_height().value *
516                job.get_source_aspect_ratio(), job.get_local_settings().get_content_height().unit);
517        set_content_width(constrained_width);
518        set_content_height(job.get_local_settings().get_content_height());
519        set_content_layout(job.get_local_settings().get_content_layout());
520        set_content_ppi(job.get_local_settings().get_content_ppi());
521        set_image_per_page_selection(job.get_local_settings().get_image_per_page_selection());
522        set_size_selection(job.get_local_settings().get_size_selection());
523        set_match_aspect_ratio_enabled(job.get_local_settings().is_match_aspect_ratio_enabled());
524        set_print_titles_enabled(job.get_local_settings().is_print_titles_enabled());
525        set_print_titles_font(job.get_local_settings().get_print_titles_font());
526    }
527
528    private void on_radio_group_click(Gtk.Button b) {
529        Gtk.RadioButton sender = (Gtk.RadioButton) b;
530
531        if (sender == standard_size_radio) {
532            set_content_layout_control_state(ContentLayout.STANDARD_SIZE);
533            standard_sizes_combo.grab_focus();
534        } else if (sender == custom_size_radio) {
535            set_content_layout_control_state(ContentLayout.CUSTOM_SIZE);
536            custom_height_entry.grab_focus();
537        } else if (sender == image_per_page_radio) {
538            set_content_layout_control_state(ContentLayout.IMAGE_PER_PAGE);
539        }
540    }
541
542    private void on_units_combo_changed() {
543        custom_height_entry.set_text(format_measurement_as(local_content_height,
544            get_user_unit_choice()));
545        custom_width_entry.set_text(format_measurement_as(local_content_width,
546            get_user_unit_choice()));
547    }
548
549    private void set_content_layout_control_state(ContentLayout layout) {
550        switch (layout) {
551            case ContentLayout.STANDARD_SIZE:
552                standard_sizes_combo.set_sensitive(true);
553                units_combo.set_sensitive(false);
554                custom_width_entry.set_sensitive(false);
555                custom_height_entry.set_sensitive(false);
556                aspect_ratio_check.set_sensitive(false);
557                image_per_page_combo.set_sensitive(false);
558            break;
559
560            case ContentLayout.CUSTOM_SIZE:
561                standard_sizes_combo.set_sensitive(false);
562                units_combo.set_sensitive(true);
563                custom_width_entry.set_sensitive(true);
564                custom_height_entry.set_sensitive(true);
565                aspect_ratio_check.set_sensitive(true);
566                image_per_page_combo.set_sensitive(false);
567            break;
568
569            case ContentLayout.IMAGE_PER_PAGE:
570                standard_sizes_combo.set_sensitive(false);
571                units_combo.set_sensitive(false);
572                custom_width_entry.set_sensitive(false);
573                custom_height_entry.set_sensitive(false);
574                aspect_ratio_check.set_sensitive(false);
575                image_per_page_combo.set_sensitive(true);
576            break;
577
578            default:
579                error("unknown ContentLayout enumeration value");
580        }
581    }
582
583    private static bool standard_sizes_combo_separator_func(Gtk.TreeModel model,
584        Gtk.TreeIter iter) {
585        Value val;
586        model.get_value(iter, 0, out val);
587
588        return (val.get_string() == "-");
589    }
590
591    private void set_content_layout(ContentLayout content_layout) {
592        set_content_layout_control_state(content_layout);
593        switch (content_layout) {
594            case ContentLayout.STANDARD_SIZE:
595                standard_size_radio.set_active(true);
596            break;
597
598            case ContentLayout.CUSTOM_SIZE:
599                custom_size_radio.set_active(true);
600            break;
601
602            case ContentLayout.IMAGE_PER_PAGE:
603                image_per_page_radio.set_active(true);
604            break;
605
606            default:
607                error("unknown ContentLayout enumeration value");
608        }
609    }
610
611    private ContentLayout get_content_layout() {
612        if (standard_size_radio.get_active())
613            return ContentLayout.STANDARD_SIZE;
614        if (custom_size_radio.get_active())
615            return ContentLayout.CUSTOM_SIZE;
616        if (image_per_page_radio.get_active())
617            return ContentLayout.IMAGE_PER_PAGE;
618
619        error("inconsistent content layout radio button group state");
620    }
621
622    private void set_content_width(Measurement content_width) {
623        if (content_width.unit != local_content_height.unit) {
624            set_user_unit_choice(content_width.unit);
625            local_content_height = local_content_height.convert_to(content_width.unit);
626            custom_height_entry.set_text(format_measurement(local_content_height));
627        }
628        local_content_width = content_width;
629        custom_width_entry.set_text(format_measurement(content_width));
630    }
631
632    private Measurement get_content_width() {
633        return local_content_width;
634    }
635
636    private void set_content_height(Measurement content_height) {
637        if (content_height.unit != local_content_width.unit) {
638            set_user_unit_choice(content_height.unit);
639            local_content_width = local_content_width.convert_to(content_height.unit);
640            custom_width_entry.set_text(format_measurement(local_content_width));
641        }
642        local_content_height = content_height;
643        custom_height_entry.set_text(format_measurement(content_height));
644    }
645
646    private Measurement get_content_height() {
647        return local_content_height;
648    }
649
650    private void set_content_ppi(int content_ppi) {
651        local_content_ppi = content_ppi.clamp(PrintSettings.MIN_CONTENT_PPI,
652            PrintSettings.MAX_CONTENT_PPI);
653
654        ppi_entry.set_text("%d".printf(local_content_ppi));
655    }
656
657    private int get_content_ppi() {
658        return local_content_ppi;
659    }
660
661    private void set_image_per_page_selection(int image_per_page) {
662        image_per_page_combo.set_active(image_per_page);
663    }
664
665    private int get_image_per_page_selection() {
666        return image_per_page_combo.get_active();
667    }
668
669    private void set_size_selection(int size_selection) {
670        standard_sizes_combo.set_active(size_selection);
671    }
672
673    private int get_size_selection() {
674        return standard_sizes_combo.get_active();
675    }
676
677    private void set_match_aspect_ratio_enabled(bool enable_state) {
678        aspect_ratio_check.set_active(enable_state);
679    }
680
681    private void set_print_titles_enabled(bool print_titles) {
682        title_print_check.set_active(print_titles);
683    }
684
685    private void set_print_titles_font(string fontname) {
686        title_print_font.set_font_name(fontname);
687    }
688
689
690    private bool is_match_aspect_ratio_enabled() {
691        return aspect_ratio_check.get_active();
692    }
693
694    private bool is_print_titles_enabled() {
695        return title_print_check.get_active();
696    }
697
698    private string get_print_titles_font() {
699        return title_print_font.get_font_name();
700    }
701
702    public PrintJob get_source_job() {
703        return source_job;
704    }
705
706    public PrintSettings get_local_settings() {
707        PrintSettings result = new PrintSettings();
708
709        result.set_content_width(get_content_width());
710        result.set_content_height(get_content_height());
711        result.set_content_layout(get_content_layout());
712        result.set_content_ppi(get_content_ppi());
713        result.set_image_per_page_selection(get_image_per_page_selection());
714        result.set_size_selection(get_size_selection());
715        result.set_match_aspect_ratio_enabled(is_match_aspect_ratio_enabled());
716        result.set_print_titles_enabled(is_print_titles_enabled());
717        result.set_print_titles_font(get_print_titles_font());
718
719        return result;
720    }
721}
722
723public class PrintJob : Gtk.PrintOperation {
724    private PrintSettings settings;
725    private Gee.ArrayList<Photo> photos = new Gee.ArrayList<Photo>();
726
727    public PrintJob(Gee.Collection<Photo> to_print) {
728        this.settings = PrintManager.get_instance().get_global_settings();
729        photos.add_all(to_print);
730
731        set_embed_page_setup (true);
732        double photo_aspect_ratio =  photos[0].get_dimensions().get_aspect_ratio();
733        if (photo_aspect_ratio < 1.0)
734            photo_aspect_ratio = 1.0 / photo_aspect_ratio;
735    }
736
737    public Gee.List<Photo> get_photos() {
738        return photos;
739    }
740
741    public Photo get_source_photo() {
742        return photos[0];
743    }
744
745    public double get_source_aspect_ratio() {
746        double aspect_ratio = photos[0].get_dimensions().get_aspect_ratio();
747        return (aspect_ratio < 1.0) ? (1.0 / aspect_ratio) : aspect_ratio;
748    }
749
750    public PrintSettings get_local_settings() {
751        return settings;
752    }
753
754    public void set_local_settings(PrintSettings settings) {
755        this.settings = settings;
756    }
757}
758
759public class StandardPrintSize {
760    public StandardPrintSize(string name, Measurement width, Measurement height) {
761        this.name = name;
762        this.width = width;
763        this.height = height;
764    }
765
766    public string name;
767    public Measurement width;
768    public Measurement height;
769}
770
771public class PrintManager {
772    private const double IMAGE_DISTANCE = 0.24;
773
774    private static PrintManager instance = null;
775
776    private PrintSettings settings;
777    private Gtk.PageSetup user_page_setup;
778    private CustomPrintTab custom_tab;
779    private ProgressDialog? progress_dialog = null;
780    private Cancellable? cancellable = null;
781    private StandardPrintSize[] standard_sizes = null;
782
783    private PrintManager() {
784        user_page_setup = new Gtk.PageSetup();
785        settings = new PrintSettings();
786    }
787
788    public unowned StandardPrintSize[] get_standard_sizes() {
789        if (standard_sizes == null) {
790            standard_sizes = new StandardPrintSize[0];
791
792            standard_sizes += new StandardPrintSize(_("Wallet (2 × 3 in.)"),
793                    Measurement(3, MeasurementUnit.INCHES),
794                    Measurement(2, MeasurementUnit.INCHES));
795            standard_sizes += new StandardPrintSize(_("Notecard (3 × 5 in.)"),
796                    Measurement(5, MeasurementUnit.INCHES),
797                    Measurement(3, MeasurementUnit.INCHES));
798            standard_sizes += new StandardPrintSize(_("4 × 6 in."),
799                    Measurement(6, MeasurementUnit.INCHES),
800                    Measurement(4, MeasurementUnit.INCHES));
801            standard_sizes += new StandardPrintSize(_("5 × 7 in."),
802                    Measurement(7, MeasurementUnit.INCHES),
803                    Measurement(5, MeasurementUnit.INCHES));
804            standard_sizes += new StandardPrintSize(_("8 × 10 in."),
805                    Measurement(10, MeasurementUnit.INCHES),
806                    Measurement(8, MeasurementUnit.INCHES));
807            standard_sizes += new StandardPrintSize(_("11 × 14 in."),
808                    Measurement(14, MeasurementUnit.INCHES),
809                    Measurement(11, MeasurementUnit.INCHES));
810            standard_sizes += new StandardPrintSize(_("16 × 20 in."),
811                    Measurement(20, MeasurementUnit.INCHES),
812                    Measurement(16, MeasurementUnit.INCHES));
813            standard_sizes += new StandardPrintSize(("-"),
814                    Measurement(0, MeasurementUnit.INCHES),
815                    Measurement(0, MeasurementUnit.INCHES));
816            standard_sizes += new StandardPrintSize(_("Metric Wallet (9 × 13 cm)"),
817                    Measurement(13, MeasurementUnit.CENTIMETERS),
818                    Measurement(9, MeasurementUnit.CENTIMETERS));
819            standard_sizes += new StandardPrintSize(_("Postcard (10 × 15 cm)"),
820                    Measurement(15, MeasurementUnit.CENTIMETERS),
821                    Measurement(10, MeasurementUnit.CENTIMETERS));
822            standard_sizes += new StandardPrintSize(_("13 × 18 cm"),
823                    Measurement(18, MeasurementUnit.CENTIMETERS),
824                    Measurement(13, MeasurementUnit.CENTIMETERS));
825            standard_sizes += new StandardPrintSize(_("18 × 24 cm"),
826                    Measurement(24, MeasurementUnit.CENTIMETERS),
827                    Measurement(18, MeasurementUnit.CENTIMETERS));
828            standard_sizes += new StandardPrintSize(_("20 × 30 cm"),
829                    Measurement(30, MeasurementUnit.CENTIMETERS),
830                    Measurement(20, MeasurementUnit.CENTIMETERS));
831            standard_sizes += new StandardPrintSize(_("24 × 40 cm"),
832                    Measurement(40, MeasurementUnit.CENTIMETERS),
833                    Measurement(24, MeasurementUnit.CENTIMETERS));
834            standard_sizes += new StandardPrintSize(_("30 × 40 cm"),
835                    Measurement(40, MeasurementUnit.CENTIMETERS),
836                    Measurement(30, MeasurementUnit.CENTIMETERS));
837        }
838
839        return standard_sizes;
840    }
841
842    public static PrintManager get_instance() {
843        if (instance == null)
844            instance = new PrintManager();
845
846        return instance;
847    }
848
849    public void spool_photo(Gee.Collection<Photo> to_print) {
850        PrintJob job = new PrintJob(to_print);
851        job.set_custom_tab_label(_("Image Settings"));
852        job.set_unit(Gtk.Unit.INCH);
853        job.set_n_pages(1);
854        job.set_job_name(job.get_source_photo().get_name());
855        job.set_default_page_setup(user_page_setup);
856        job.begin_print.connect(on_begin_print);
857        job.draw_page.connect(on_draw_page);
858        job.create_custom_widget.connect(on_create_custom_widget);
859        job.status_changed.connect(on_status_changed);
860
861        AppWindow.get_instance().set_busy_cursor();
862
863        cancellable = new Cancellable();
864        progress_dialog = new ProgressDialog(AppWindow.get_instance(), _("Printing…"), cancellable);
865
866        string? err_msg = null;
867        try {
868            Gtk.PrintOperationResult result = job.run(Gtk.PrintOperationAction.PRINT_DIALOG,
869                AppWindow.get_instance());
870            if (result == Gtk.PrintOperationResult.APPLY)
871                user_page_setup = job.get_default_page_setup();
872        } catch (Error e) {
873            job.cancel();
874            err_msg = e.message;
875        }
876
877        progress_dialog.close();
878        progress_dialog = null;
879        cancellable = null;
880
881        AppWindow.get_instance().set_normal_cursor();
882
883        if (err_msg != null)
884            AppWindow.error_message(_("Unable to print photo:\n\n%s").printf(err_msg));
885    }
886
887    private void on_begin_print(Gtk.PrintOperation emitting_object, Gtk.PrintContext job_context) {
888        debug("on_begin_print");
889
890        PrintJob job = (PrintJob) emitting_object;
891
892        // cancel() can only be called from "begin-print", "paginate", or "draw-page"
893        if (cancellable != null && cancellable.is_cancelled()) {
894            job.cancel();
895
896            return;
897        }
898
899        Gee.List<Photo> photos = job.get_photos();
900        if (job.get_local_settings().get_content_layout() == ContentLayout.IMAGE_PER_PAGE){
901            PrintLayout layout = (PrintLayout) job.get_local_settings().get_image_per_page_selection();
902            job.set_n_pages((int) Math.ceil((double) photos.size / (double) layout.get_per_page()));
903        } else {
904            job.set_n_pages(photos.size);
905        }
906
907        spin_event_loop();
908    }
909
910    private void on_status_changed(Gtk.PrintOperation job) {
911        debug("on_status_changed: %s", job.get_status_string());
912
913        if (progress_dialog != null) {
914            progress_dialog.set_status(job.get_status_string());
915            spin_event_loop();
916        }
917    }
918
919    private void on_draw_page(Gtk.PrintOperation emitting_object, Gtk.PrintContext job_context,
920        int page_num) {
921        debug("on_draw_page");
922
923        PrintJob job = (PrintJob) emitting_object;
924
925        // cancel() can only be called from "begin-print", "paginate", or "draw-page"
926        if (cancellable != null && cancellable.is_cancelled()) {
927            job.cancel();
928
929            return;
930        }
931
932        spin_event_loop();
933
934        Gtk.PageSetup page_setup = job_context.get_page_setup();
935        double page_width = page_setup.get_page_width(Gtk.Unit.INCH);
936        double page_height = page_setup.get_page_height(Gtk.Unit.INCH);
937
938        double dpi = job.get_local_settings().get_content_ppi();
939        double inv_dpi = 1.0 / dpi;
940        Cairo.Context dc = job_context.get_cairo_context();
941        dc.scale(inv_dpi, inv_dpi);
942        Gee.List<Photo> photos = job.get_photos();
943
944        ContentLayout content_layout = job.get_local_settings().get_content_layout();
945        switch (content_layout) {
946            case ContentLayout.STANDARD_SIZE:
947            case ContentLayout.CUSTOM_SIZE:
948                double canvas_width, canvas_height;
949                if (content_layout == ContentLayout.STANDARD_SIZE) {
950                    canvas_width = get_standard_sizes()[job.get_local_settings().get_size_selection()].width.convert_to(
951                        MeasurementUnit.INCHES).value;
952                    canvas_height = get_standard_sizes()[job.get_local_settings().get_size_selection()].height.convert_to(
953                        MeasurementUnit.INCHES).value;
954                } else {
955                    assert(content_layout == ContentLayout.CUSTOM_SIZE);
956                    canvas_width = job.get_local_settings().get_content_width().convert_to(
957                        MeasurementUnit.INCHES).value;
958                    canvas_height = job.get_local_settings().get_content_height().convert_to(
959                        MeasurementUnit.INCHES).value;
960                }
961
962                if (page_num < photos.size) {
963                    Dimensions photo_dimensions = photos[page_num].get_dimensions();
964                    double photo_aspect_ratio = photo_dimensions.get_aspect_ratio();
965                    double canvas_aspect_ratio = ((double) canvas_width) / canvas_height;
966                    if (Math.floor(canvas_aspect_ratio) != Math.floor(photo_aspect_ratio)) {
967                        double canvas_tmp = canvas_width;
968                        canvas_width = canvas_height;
969                        canvas_height = canvas_tmp;
970                    }
971
972                    double dx = (page_width - canvas_width) / 2.0;
973                    double dy = (page_height - canvas_height) / 2.0;
974                    fit_image_to_canvas(photos[page_num], dx, dy, canvas_width, canvas_height, true,
975                        job, job_context);
976                    if (job.get_local_settings().is_print_titles_enabled()) {
977                        add_title_to_canvas(page_width / 2, page_height, photos[page_num].get_name(),
978                            job, job_context);
979                    }
980                }
981
982                if (progress_dialog != null)
983                    progress_dialog.monitor(page_num, photos.size);
984            break;
985
986            case ContentLayout.IMAGE_PER_PAGE:
987                PrintLayout layout = (PrintLayout) job.get_local_settings().get_image_per_page_selection();
988                int nx = layout.get_x();
989                int ny = layout.get_y();
990                int start = page_num * layout.get_per_page();
991                double canvas_width = (double) (page_width - IMAGE_DISTANCE * (nx - 1)) / nx;
992                double canvas_height = (double) (page_height - IMAGE_DISTANCE * (ny - 1)) / ny;
993                for (int y = 0; y < ny; y++){
994                    for (int x = 0; x < nx; x++){
995                        int i = start + y * nx + x;
996                        if (i < photos.size) {
997                            double dx = x * (canvas_width) + x * IMAGE_DISTANCE;
998                            double dy = y * (canvas_height) + y * IMAGE_DISTANCE;
999                            fit_image_to_canvas(photos[i], dx, dy, canvas_width, canvas_height, false,
1000                                job, job_context);
1001                            if (job.get_local_settings().is_print_titles_enabled()) {
1002                                add_title_to_canvas(dx + canvas_width / 2, dy + canvas_height,
1003                                    photos[i].get_name(), job, job_context);
1004                            }
1005                        }
1006
1007                        if (progress_dialog != null)
1008                            progress_dialog.monitor(i, photos.size);
1009                    }
1010                }
1011            break;
1012
1013            default:
1014                error("unknown or unsupported layout mode");
1015        }
1016    }
1017
1018    private unowned Object on_create_custom_widget(Gtk.PrintOperation emitting_object) {
1019        custom_tab = new CustomPrintTab((PrintJob) emitting_object);
1020        ((PrintJob) emitting_object).custom_widget_apply.connect(on_custom_widget_apply);
1021        return custom_tab;
1022    }
1023
1024    private void on_custom_widget_apply(Gtk.Widget custom_widget) {
1025        CustomPrintTab tab = (CustomPrintTab) custom_widget;
1026        tab.get_source_job().set_local_settings(tab.get_local_settings());
1027        set_global_settings(tab.get_local_settings());
1028    }
1029
1030    private void fit_image_to_canvas(Photo photo, double x, double y, double canvas_width, double canvas_height, bool crop, PrintJob job, Gtk.PrintContext job_context) {
1031        Cairo.Context dc = job_context.get_cairo_context();
1032        Dimensions photo_dimensions = photo.get_dimensions();
1033        double photo_aspect_ratio = photo_dimensions.get_aspect_ratio();
1034        double canvas_aspect_ratio = ((double) canvas_width) / canvas_height;
1035
1036        double target_width = 0.0;
1037        double target_height = 0.0;
1038        double dpi = job.get_local_settings().get_content_ppi();
1039
1040        if (!crop) {
1041            if (canvas_aspect_ratio < photo_aspect_ratio) {
1042                target_width = canvas_width;
1043                target_height = target_width * (1.0 / photo_aspect_ratio);
1044            } else {
1045                target_height = canvas_height;
1046                target_width = target_height * photo_aspect_ratio;
1047            }
1048            x += (canvas_width - target_width) / 2.0;
1049            y += (canvas_height - target_height) / 2.0;
1050        }
1051
1052        double x_offset = dpi * x;
1053        double y_offset = dpi * y;
1054        dc.save();
1055        dc.translate(x_offset, y_offset);
1056
1057        int w = (int) (dpi * canvas_width);
1058        int h = (int) (dpi * canvas_height);
1059        Dimensions viewport = Dimensions(w, h);
1060
1061        try {
1062            if (crop && !are_approximately_equal(canvas_aspect_ratio, photo_aspect_ratio)) {
1063                Scaling pixbuf_scaling = Scaling.to_fill_viewport(viewport);
1064                Gdk.Pixbuf photo_pixbuf = photo.get_pixbuf(pixbuf_scaling);
1065                Dimensions scaled_photo_dimensions = Dimensions.for_pixbuf(photo_pixbuf);
1066                int shave_vertical = 0;
1067                int shave_horizontal = 0;
1068                if (canvas_aspect_ratio < photo_aspect_ratio) {
1069                    shave_vertical = (int) ((scaled_photo_dimensions.width - (scaled_photo_dimensions.height * canvas_aspect_ratio)) / 2.0);
1070                } else {
1071                    shave_horizontal = (int) ((scaled_photo_dimensions.height - (scaled_photo_dimensions.width * (1.0 / canvas_aspect_ratio))) / 2.0);
1072                }
1073                Gdk.Pixbuf shaved_pixbuf = new Gdk.Pixbuf.subpixbuf(photo_pixbuf, shave_vertical,shave_horizontal, scaled_photo_dimensions.width - (2 * shave_vertical), scaled_photo_dimensions.height - (2 * shave_horizontal));
1074
1075                photo_pixbuf = pixbuf_scaling.perform_on_pixbuf(shaved_pixbuf, Gdk.InterpType.HYPER, true);
1076                Gdk.cairo_set_source_pixbuf(dc, photo_pixbuf, 0.0, 0.0);
1077            } else {
1078                Scaling pixbuf_scaling = Scaling.for_viewport(viewport, true);
1079                Gdk.Pixbuf photo_pixbuf = photo.get_pixbuf(pixbuf_scaling);
1080                photo_pixbuf = pixbuf_scaling.perform_on_pixbuf(photo_pixbuf, Gdk.InterpType.HYPER, true);
1081                Gdk.cairo_set_source_pixbuf(dc, photo_pixbuf, 0.0, 0.0);
1082            }
1083            dc.paint();
1084
1085        } catch (Error e) {
1086            job.cancel();
1087            AppWindow.error_message(_("Unable to print photo:\n\n%s").printf(e.message));
1088        }
1089        dc.restore();
1090    }
1091
1092    private void add_title_to_canvas(double x, double y, string title, PrintJob job, Gtk.PrintContext job_context) {
1093        Cairo.Context dc = job_context.get_cairo_context();
1094        double dpi = job.get_local_settings().get_content_ppi();
1095        var title_font_description = Pango.FontDescription.from_string(job.get_local_settings().get_print_titles_font());
1096        var title_layout = Pango.cairo_create_layout(dc);
1097        Pango.Context context = title_layout.get_context();
1098        Pango.cairo_context_set_resolution (context, dpi);
1099        title_layout.set_font_description(title_font_description);
1100        title_layout.set_text(title, -1);
1101        int title_width, title_height;
1102        title_layout.get_pixel_size(out title_width, out title_height);
1103        double tx = dpi * x - title_width / 2;
1104        double ty = dpi * y - title_height;
1105
1106        // Transparent title text background
1107        dc.rectangle(tx - 10, ty + 2, title_width + 20, title_height);
1108        dc.set_source_rgba(1, 1, 1, 1);
1109        dc.set_line_width(2);
1110        dc.stroke_preserve();
1111        dc.set_source_rgba(1, 1, 1, 0.5);
1112        dc.fill();
1113        dc.set_source_rgba(0, 0, 0, 1);
1114
1115        dc.move_to(tx, ty + 2);
1116        Pango.cairo_show_layout(dc, title_layout);
1117    }
1118
1119    private bool are_approximately_equal(double val1, double val2) {
1120        double accept_err = 0.005;
1121        return (Math.fabs(val1 - val2) <= accept_err);
1122    }
1123
1124    public PrintSettings get_global_settings() {
1125        return settings;
1126    }
1127
1128    public void set_global_settings(PrintSettings settings) {
1129        this.settings = settings;
1130        settings.save();
1131    }
1132}
1133