"Fossies" - the Fresh Open Source Software Archive 
Member "PhotoCollage-1.4.5/photocollage/gtkgui.py" (9 Jul 2021, 31042 Bytes) of package /linux/privat/PhotoCollage-1.4.5.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style:
standard) with prefixed line numbers.
Alternatively you can here
view or
download the uninterpreted source code file.
For more information about "gtkgui.py" see the
Fossies "Dox" file reference documentation and the latest
Fossies "Diffs" side-by-side code changes report:
1.4.4_vs_1.4.5.
1 # Copyright (C) 2013 Adrien Vergé
2 #
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16
17 import copy
18 import gettext
19 from io import BytesIO
20 import math
21 import os.path
22 import random
23 import sys
24 import urllib
25
26 import cairo
27 import gi
28
29 from photocollage import APP_NAME, artwork, collage, render
30 from photocollage.render import PIL_SUPPORTED_EXTS as EXTS
31
32 gi.require_version('Gtk', '3.0')
33 from gi.repository import Gtk, Gdk, GObject, GdkPixbuf # noqa: E402, I100
34
35
36 gettext.textdomain(APP_NAME)
37 _ = gettext.gettext
38 _n = gettext.ngettext
39 # xgettext --keyword=_n:1,2 -o po/photocollage.pot $(find . -name '*.py')
40 # cp po/photocollage.pot po/fr.po
41 # msgfmt -o po/fr.mo po/fr.po
42
43
44 def pil_image_to_cairo_surface(src):
45 # TODO: cairo.ImageSurface.create_for_data() is not yet available in
46 # Python 3, so we use PNG as an intermediate.
47 buf = BytesIO()
48 src.save(buf, "png")
49 buf.seek(0)
50 surface = cairo.ImageSurface.create_from_png(buf)
51 buf.close()
52 return surface
53
54
55 def get_all_save_image_exts():
56 all_types = dict(list(EXTS.RW.items()) + list(EXTS.WO.items()))
57 all = []
58 for type in all_types:
59 for ext in all_types[type]:
60 all.append(ext)
61
62 return all
63
64
65 def set_open_image_filters(dialog):
66 """Set our own filter because Gtk.FileFilter.add_pixbuf_formats() contains
67 formats not supported by PIL.
68
69 """
70 # Do not show the filter to the user, just limit selectable files
71 imgfilter = Gtk.FileFilter()
72 imgfilter.set_name(_("All supported image formats"))
73
74 all_types = dict(list(EXTS.RW.items()) + list(EXTS.RO.items()))
75 for type in all_types:
76 for ext in all_types[type]:
77 imgfilter.add_pattern("*." + ext)
78 imgfilter.add_pattern("*." + ext.upper())
79
80 dialog.add_filter(imgfilter)
81 dialog.set_filter(imgfilter)
82
83
84 def set_save_image_filters(dialog):
85 """Set our own filter because Gtk.FileFilter.add_pixbuf_formats() contains
86 formats not supported by PIL.
87
88 """
89 all_types = dict(list(EXTS.RW.items()) + list(EXTS.WO.items()))
90 filters = []
91
92 filters.append(Gtk.FileFilter())
93 flt = filters[-1]
94 flt.set_name(_("All supported image formats"))
95 for ext in get_all_save_image_exts():
96 flt.add_pattern("*." + ext)
97 flt.add_pattern("*." + ext.upper())
98 dialog.add_filter(flt)
99 dialog.set_filter(flt)
100
101 for type in all_types:
102 filters.append(Gtk.FileFilter())
103 flt = filters[-1]
104 name = _("%s image") % type
105 name += " (." + ", .".join(all_types[type]) + ")"
106 flt.set_name(name)
107 for ext in all_types[type]:
108 flt.add_pattern("*." + ext)
109 flt.add_pattern("*." + ext.upper())
110 dialog.add_filter(flt)
111
112
113 def gtk_run_in_main_thread(fn):
114 def my_fn(*args, **kwargs):
115 GObject.idle_add(fn, *args, **kwargs)
116 return my_fn
117
118
119 class UserCollage:
120 """Represents a user-defined collage
121
122 A UserCollage contains a list of photos (referenced by filenames) and a
123 collage.Page object describing their layout in a final poster.
124
125 """
126 def __init__(self, photolist):
127 self.photolist = photolist
128
129 def make_page(self, opts):
130 # Define the output image height / width ratio
131 ratio = 1.0 * opts.out_h / opts.out_w
132
133 # Compute a good number of columns. It depends on the ratio, the number
134 # of images and the average ratio of these images. According to my
135 # calculations, the number of column should be inversely proportional
136 # to the square root of the output image ratio, and proportional to the
137 # square root of the average input images ratio.
138 avg_ratio = (sum(1.0 * photo.h / photo.w for photo in self.photolist) /
139 len(self.photolist))
140 # Virtual number of images: since ~ 1 image over 3 is in a multi-cell
141 # (i.e. takes two columns), it takes the space of 4 images.
142 # So it's equivalent to 1/3 * 4 + 2/3 = 2 times the number of images.
143 virtual_no_imgs = 2 * len(self.photolist)
144 no_cols = int(round(math.sqrt(avg_ratio / ratio * virtual_no_imgs)))
145
146 self.page = collage.Page(1.0, ratio, no_cols)
147 random.shuffle(self.photolist)
148 for photo in self.photolist:
149 self.page.add_cell(photo)
150 self.page.adjust()
151
152 def duplicate(self):
153 return UserCollage(copy.copy(self.photolist))
154
155
156 class PhotoCollageWindow(Gtk.Window):
157 TARGET_TYPE_TEXT = 1
158 TARGET_TYPE_URI = 2
159
160 def __init__(self):
161 super().__init__(title=_("PhotoCollage"))
162 self.history = []
163 self.history_index = 0
164
165 class Options:
166 def __init__(self):
167 self.border_w = 0.01
168 self.border_c = "black"
169 self.out_w = 800
170 self.out_h = 600
171
172 self.opts = Options()
173
174 self.make_window()
175
176 def make_window(self):
177 self.set_border_width(10)
178
179 box_window = Gtk.Box(spacing=10, orientation=Gtk.Orientation.VERTICAL)
180 self.add(box_window)
181
182 # -----------------------
183 # Input and output pan
184 # -----------------------
185
186 box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.HORIZONTAL)
187 box_window.pack_start(box, False, False, 0)
188
189 self.btn_choose_images = Gtk.Button(label=_("Add images..."))
190 self.btn_choose_images.set_image(Gtk.Image.new_from_stock(
191 Gtk.STOCK_OPEN, Gtk.IconSize.LARGE_TOOLBAR))
192 self.btn_choose_images.set_always_show_image(True)
193 self.btn_choose_images.connect("clicked", self.choose_images)
194 box.pack_start(self.btn_choose_images, False, False, 0)
195
196 self.btn_save = Gtk.Button(label=_("Save poster..."))
197 self.btn_save.set_image(Gtk.Image.new_from_stock(
198 Gtk.STOCK_SAVE_AS, Gtk.IconSize.LARGE_TOOLBAR))
199 self.btn_save.set_always_show_image(True)
200 self.btn_save.connect("clicked", self.save_poster)
201 box.pack_start(self.btn_save, False, False, 0)
202
203 # -----------------------
204 # Tools pan
205 # -----------------------
206
207 box.pack_start(Gtk.SeparatorToolItem(), True, True, 0)
208
209 self.btn_undo = Gtk.Button()
210 self.btn_undo.set_image(Gtk.Image.new_from_stock(
211 Gtk.STOCK_UNDO, Gtk.IconSize.LARGE_TOOLBAR))
212 self.btn_undo.connect("clicked", self.select_prev_layout)
213 box.pack_start(self.btn_undo, False, False, 0)
214 self.lbl_history_index = Gtk.Label(" ")
215 box.pack_start(self.lbl_history_index, False, False, 0)
216 self.btn_redo = Gtk.Button()
217 self.btn_redo.set_image(Gtk.Image.new_from_stock(
218 Gtk.STOCK_REDO, Gtk.IconSize.LARGE_TOOLBAR))
219 self.btn_redo.connect("clicked", self.select_next_layout)
220 box.pack_start(self.btn_redo, False, False, 0)
221 self.btn_new_layout = Gtk.Button(label=_("Regenerate"))
222 self.btn_new_layout.set_image(Gtk.Image.new_from_stock(
223 Gtk.STOCK_REFRESH, Gtk.IconSize.LARGE_TOOLBAR))
224 self.btn_new_layout.set_always_show_image(True)
225 self.btn_new_layout.connect("clicked", self.regenerate_layout)
226 box.pack_start(self.btn_new_layout, False, False, 0)
227
228 box.pack_start(Gtk.SeparatorToolItem(), True, True, 0)
229
230 self.btn_settings = Gtk.Button()
231 self.btn_settings.set_image(Gtk.Image.new_from_stock(
232 Gtk.STOCK_PREFERENCES, Gtk.IconSize.LARGE_TOOLBAR))
233 self.btn_settings.set_always_show_image(True)
234 self.btn_settings.connect("clicked", self.set_settings)
235 box.pack_end(self.btn_settings, False, False, 0)
236
237 # -------------------
238 # Image preview pan
239 # -------------------
240
241 box = Gtk.Box(spacing=10)
242 box_window.pack_start(box, True, True, 0)
243
244 self.img_preview = ImagePreviewArea(self)
245 self.img_preview.set_size_request(600, 400)
246 self.img_preview.connect("drag-data-received", self.on_drag)
247 self.img_preview.drag_dest_set(Gtk.DestDefaults.ALL, [],
248 Gdk.DragAction.COPY)
249 targets = Gtk.TargetList.new([])
250 targets.add_text_targets(PhotoCollageWindow.TARGET_TYPE_TEXT)
251 targets.add_uri_targets(PhotoCollageWindow.TARGET_TYPE_URI)
252 self.img_preview.drag_dest_set_target_list(targets)
253
254 box.pack_start(self.img_preview, True, True, 0)
255
256 self.btn_save.set_sensitive(False)
257
258 self.btn_undo.set_sensitive(False)
259 self.btn_redo.set_sensitive(False)
260
261 self.update_photolist([])
262
263 def update_photolist(self, new_images):
264 try:
265 photolist = []
266 if self.history_index < len(self.history):
267 photolist = copy.copy(
268 self.history[self.history_index].photolist)
269 photolist.extend(render.build_photolist(new_images))
270
271 if len(photolist) > 0:
272 new_collage = UserCollage(photolist)
273 new_collage.make_page(self.opts)
274 self.render_from_new_collage(new_collage)
275 else:
276 self.update_tool_buttons()
277 except render.BadPhoto as e:
278 dialog = ErrorDialog(
279 self, _("This image could not be opened:\n\"%(imgname)s\".")
280 % {"imgname": e.photoname})
281 dialog.run()
282 dialog.destroy()
283
284 def choose_images(self, button):
285 dialog = PreviewFileChooserDialog(title=_("Choose images"),
286 parent=button.get_toplevel(),
287 action=Gtk.FileChooserAction.OPEN,
288 select_multiple=True,
289 modal=True)
290
291 if dialog.run() == Gtk.ResponseType.OK:
292 files = dialog.get_filenames()
293 dialog.destroy()
294 self.update_photolist(files)
295 else:
296 dialog.destroy()
297
298 def on_drag(self, widget, drag_context, x, y, data, info, time):
299 if info == PhotoCollageWindow.TARGET_TYPE_TEXT:
300 files = data.get_text().splitlines()
301 elif info == PhotoCollageWindow.TARGET_TYPE_URI:
302 # Can only handle local URIs
303 files = [f for f in data.get_uris() if f.startswith("file://")]
304
305 for i in range(len(files)):
306 if files[i].startswith("file://"):
307 files[i] = urllib.parse.unquote(files[i][7:])
308 self.update_photolist(files)
309
310 def render_preview(self):
311 collage = self.history[self.history_index]
312
313 # If the desired ratio changed in the meantime (e.g. from landscape to
314 # portrait), it needs to be re-updated
315 collage.page.target_ratio = 1.0 * self.opts.out_h / self.opts.out_w
316 collage.page.adjust_cols_heights()
317
318 w = self.img_preview.get_allocation().width
319 h = self.img_preview.get_allocation().height
320 collage.page.scale_to_fit(w, h)
321
322 # Display a "please wait" dialog and do the job.
323 compdialog = ComputingDialog(self)
324
325 def on_update(img, fraction_complete):
326 self.img_preview.set_collage(img, collage)
327 compdialog.update(fraction_complete)
328
329 def on_complete(img):
330 self.img_preview.set_collage(img, collage)
331 compdialog.destroy()
332 self.btn_save.set_sensitive(True)
333
334 def on_fail(exception):
335 dialog = ErrorDialog(self, "{}:\n\n{}".format(
336 _("An error occurred while rendering image:"), exception))
337 compdialog.destroy()
338 dialog.run()
339 dialog.destroy()
340 self.btn_save.set_sensitive(False)
341
342 t = render.RenderingTask(
343 collage.page,
344 border_width=self.opts.border_w * max(collage.page.w,
345 collage.page.h),
346 border_color=self.opts.border_c,
347 on_update=gtk_run_in_main_thread(on_update),
348 on_complete=gtk_run_in_main_thread(on_complete),
349 on_fail=gtk_run_in_main_thread(on_fail))
350 t.start()
351
352 response = compdialog.run()
353 if response == Gtk.ResponseType.CANCEL:
354 t.abort()
355 compdialog.destroy()
356
357 def render_from_new_collage(self, collage):
358 self.history.append(collage)
359 self.history_index = len(self.history) - 1
360 self.update_tool_buttons()
361 self.render_preview()
362
363 def regenerate_layout(self, button=None):
364 new_collage = self.history[self.history_index].duplicate()
365 new_collage.make_page(self.opts)
366 self.render_from_new_collage(new_collage)
367
368 def select_prev_layout(self, button):
369 self.history_index -= 1
370 self.update_tool_buttons()
371 self.render_preview()
372
373 def select_next_layout(self, button):
374 self.history_index += 1
375 self.update_tool_buttons()
376 self.render_preview()
377
378 def set_settings(self, button):
379 dialog = SettingsDialog(self)
380 response = dialog.run()
381 if response == Gtk.ResponseType.OK:
382 dialog.apply_opts(self.opts)
383 dialog.destroy()
384 if self.history:
385 self.render_preview()
386 else:
387 dialog.destroy()
388
389 def save_poster(self, button):
390 collage = self.history[self.history_index]
391
392 enlargement = float(self.opts.out_w) / collage.page.w
393 collage.page.scale(enlargement)
394
395 dialog = Gtk.FileChooserDialog(_("Save image"), button.get_toplevel(),
396 Gtk.FileChooserAction.SAVE)
397 dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
398 dialog.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
399 dialog.set_do_overwrite_confirmation(True)
400 set_save_image_filters(dialog)
401 if dialog.run() != Gtk.ResponseType.OK:
402 dialog.destroy()
403 return
404 savefile = dialog.get_filename()
405 base, ext = os.path.splitext(savefile)
406 if ext == "" or not ext[1:].lower() in get_all_save_image_exts():
407 savefile += ".jpg"
408 dialog.destroy()
409
410 # Display a "please wait" dialog and do the job.
411 compdialog = ComputingDialog(self)
412
413 def on_update(img, fraction_complete):
414 compdialog.update(fraction_complete)
415
416 def on_complete(img):
417 compdialog.destroy()
418
419 def on_fail(exception):
420 dialog = ErrorDialog(self, "{}:\n\n{}".format(
421 _("An error occurred while rendering image:"), exception))
422 compdialog.destroy()
423 dialog.run()
424 dialog.destroy()
425
426 t = render.RenderingTask(
427 collage.page, output_file=savefile,
428 border_width=self.opts.border_w * max(collage.page.w,
429 collage.page.h),
430 border_color=self.opts.border_c,
431 on_update=gtk_run_in_main_thread(on_update),
432 on_complete=gtk_run_in_main_thread(on_complete),
433 on_fail=gtk_run_in_main_thread(on_fail))
434 t.start()
435
436 response = compdialog.run()
437 if response == Gtk.ResponseType.CANCEL:
438 t.abort()
439 compdialog.destroy()
440
441 def update_tool_buttons(self):
442 self.btn_undo.set_sensitive(self.history_index > 0)
443 self.btn_redo.set_sensitive(self.history_index < len(self.history) - 1)
444 if self.history_index < len(self.history):
445 self.lbl_history_index.set_label(str(self.history_index + 1))
446 else:
447 self.lbl_history_index.set_label(" ")
448 self.btn_save.set_sensitive(
449 self.history_index < len(self.history))
450 self.btn_new_layout.set_sensitive(
451 self.history_index < len(self.history))
452
453
454 class ImagePreviewArea(Gtk.DrawingArea):
455 """Area to display the poster preview and react to user actions"""
456 INSENSITIVE, FLYING, SWAPPING_OR_MOVING = range(3)
457
458 def __init__(self, parent):
459 super().__init__()
460 self.parent = parent
461
462 parse, color = Gdk.Color.parse("#888888")
463 self.modify_bg(Gtk.StateType.NORMAL, color)
464
465 # http://www.pygtk.org/pygtk2tutorial/sec-EventHandling.html
466 # https://developer.gnome.org/gdk3/stable/gdk3-Events.html#GdkEventMask
467 self.connect("draw", self.draw)
468 self.connect("motion-notify-event", self.motion_notify_event)
469 self.connect("leave-notify-event", self.motion_notify_event)
470 self.connect("button-press-event", self.button_press_event)
471 self.connect("button-release-event", self.button_release_event)
472 self.set_events(Gdk.EventMask.EXPOSURE_MASK |
473 Gdk.EventMask.LEAVE_NOTIFY_MASK |
474 Gdk.EventMask.BUTTON_PRESS_MASK |
475 Gdk.EventMask.BUTTON_RELEASE_MASK |
476 Gdk.EventMask.POINTER_MOTION_MASK)
477
478 self.image = None
479 self.mode = self.INSENSITIVE
480
481 class SwapEnd:
482 def __init__(self, cell=None, x=0, y=0):
483 self.cell = cell
484 self.x = x
485 self.y = y
486
487 self.x, self.y = 0, 0
488 self.swap_origin = SwapEnd()
489 self.swap_dest = SwapEnd()
490
491 def set_collage(self, image, collage):
492 self.image = pil_image_to_cairo_surface(image)
493 # The Collage object must be deeply copied.
494 # Otherwise, SWAPPING_OR_MOVING photos in a new page would also affect
495 # the original page (in history).
496 # The deep copy is done here (not in button_release_event) because
497 # references to cells are gathered in other functions, so that making
498 # the copy at the end would invalidate these references.
499 self.collage = copy.deepcopy(collage)
500 self.mode = self.FLYING
501 self.queue_draw()
502
503 def get_image_offset(self):
504 return (round((self.get_allocation().width -
505 self.image.get_width()) / 2.0),
506 round((self.get_allocation().height -
507 self.image.get_height()) / 2.0))
508
509 def get_pos_in_image(self, x, y):
510 if self.image is not None:
511 x0, y0 = self.get_image_offset()
512 return (int(round(x - x0)), int(round(y - y0)))
513 return (int(round(x)), int(round(y)))
514
515 def paint_image_border(self, context, cell, dash=None):
516 x0, y0 = self.get_image_offset()
517
518 context.set_source_rgb(1.0, 1.0, 0.0)
519 context.set_line_width(2)
520 if dash is not None:
521 context.set_dash(dash)
522 context.rectangle(x0 + cell.x + 1, y0 + cell.y + 1,
523 cell.w - 2, cell.h - 2)
524 context.stroke()
525
526 def paint_image_delete_button(self, context, cell):
527 x0, y0 = self.get_image_offset()
528
529 x = x0 + cell.x + cell.w - 12
530 y = y0 + cell.y + 12
531
532 context.arc(x, y, 8, 0, 6.2832)
533 context.set_source_rgb(0.8, 0.0, 0.0)
534 context.fill()
535 context.arc(x, y, 8, 0, 6.2832)
536 context.set_source_rgb(0.0, 0.0, 0.0)
537 context.set_line_width(1)
538 context.move_to(x - 4, y - 4)
539 context.line_to(x + 4, y + 4)
540 context.move_to(x - 4, y + 4)
541 context.line_to(x + 4, y - 4)
542 context.stroke()
543
544 def draw(self, widget, context):
545 if self.image is not None:
546 x0, y0 = self.get_image_offset()
547 context.set_source_surface(self.image, x0, y0)
548 context.paint()
549
550 if self.mode == self.FLYING:
551 cell = self.collage.page.get_cell_at_position(self.x, self.y)
552 if cell:
553 self.paint_image_border(context, cell)
554 self.paint_image_delete_button(context, cell)
555 elif self.mode == self.SWAPPING_OR_MOVING:
556 self.paint_image_border(context, self.swap_origin.cell, (3, 3))
557 cell = self.collage.page.get_cell_at_position(self.x, self.y)
558 if cell and cell != self.swap_origin.cell:
559 self.paint_image_border(context, cell, (3, 3))
560 else:
561 # Display the drag & drop image
562 dnd_image = artwork.load_cairo_surface(artwork.ICON_DRAG_AND_DROP)
563 context.set_source_surface(
564 dnd_image,
565 round((self.get_allocation().width -
566 dnd_image.get_width()) / 2.0),
567 round((self.get_allocation().height -
568 dnd_image.get_height()) / 2.0))
569 context.paint()
570
571 return False
572
573 def motion_notify_event(self, widget, event):
574 self.x, self.y = self.get_pos_in_image(event.x, event.y)
575 widget.queue_draw()
576
577 def button_press_event(self, widget, event):
578 if self.mode == self.FLYING:
579 x, y = self.get_pos_in_image(event.x, event.y)
580 cell = self.collage.page.get_cell_at_position(x, y)
581 if not cell:
582 return
583 # Has the user clicked the delete button?
584 dist = (cell.x + cell.w - 12 - x) ** 2 + (cell.y + 12 - y) ** 2
585 if dist <= 8 * 8:
586 self.collage.photolist.remove(cell.photo)
587 if self.collage.photolist:
588 self.collage.make_page(self.parent.opts)
589 self.parent.render_from_new_collage(self.collage)
590 else:
591 self.image = None
592 self.mode = self.INSENSITIVE
593 self.parent.history_index = len(self.parent.history)
594 self.parent.update_tool_buttons()
595 # Otherwise, the user wants to swap this image with another
596 else:
597 self.swap_origin.x, self.swap_origin.y = x, y
598 self.swap_origin.cell = cell
599 self.mode = self.SWAPPING_OR_MOVING
600 widget.queue_draw()
601
602 def button_release_event(self, widget, event):
603 if self.mode == self.SWAPPING_OR_MOVING:
604 self.swap_dest.x, self.swap_dest.y = \
605 self.get_pos_in_image(event.x, event.y)
606 self.swap_dest.cell = self.collage.page.get_cell_at_position(
607 self.swap_dest.x, self.swap_dest.y)
608 if self.swap_dest.cell \
609 and self.swap_origin.cell != self.swap_dest.cell:
610 # different cell: SWAPPING
611 self.collage.page.swap_photos(self.swap_origin.cell,
612 self.swap_dest.cell)
613 self.parent.render_from_new_collage(self.collage)
614 elif self.swap_dest.cell:
615 # same cell: MOVING
616 move_x = (self.swap_origin.x - self.x) / self.swap_dest.cell.w
617 move_y = (self.swap_origin.y - self.y) / self.swap_dest.cell.h
618 self.swap_dest.cell.photo.move(move_x, move_y)
619 self.parent.render_from_new_collage(self.collage)
620 self.mode = self.FLYING
621 widget.queue_draw()
622
623
624 class SettingsDialog(Gtk.Dialog):
625 def __init__(self, parent):
626 super().__init__(
627 _("Settings"), parent, 0,
628 (Gtk.STOCK_OK, Gtk.ResponseType.OK,
629 Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL))
630 self.set_border_width(10)
631
632 self.selected_border_color = parent.opts.border_c
633
634 box = self.get_content_area()
635 vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
636 box.add(vbox)
637
638 label = Gtk.Label(xalign=0)
639 label.set_markup("<big><b>%s</b></big>" % _("Output image size"))
640 vbox.pack_start(label, False, False, 0)
641
642 box = Gtk.Box(spacing=6)
643 vbox.pack_start(box, False, False, 0)
644 self.etr_outw = Gtk.Entry(text=str(parent.opts.out_w))
645 self.etr_outw.connect("changed", self.validate_int)
646 self.etr_outw.last_valid_text = self.etr_outw.get_text()
647 box.pack_start(self.etr_outw, False, False, 0)
648 box.pack_start(Gtk.Label("×", xalign=0), False, False, 0)
649 self.etr_outh = Gtk.Entry(text=str(parent.opts.out_h))
650 self.etr_outh.connect("changed", self.validate_int)
651 self.etr_outh.last_valid_text = self.etr_outh.get_text()
652 box.pack_start(self.etr_outh, False, False, 0)
653
654 box.pack_end(Gtk.Label(_("pixels"), xalign=0), False, False, 0)
655
656 templates = (
657 ("", None),
658 ("800 × 600", (800, 600)),
659 ("1600 × 1200", (1600, 1200)),
660 ("A4 landscape (300ppi)", (3508, 2480)),
661 ("A4 portrait (300ppi)", (2480, 3508)),
662 ("A3 landscape (300ppi)", (4960, 3508)),
663 ("A3 portrait (300ppi)", (3508, 4960)),
664 ("US-Letter landscape (300ppi)", (3300, 2550)),
665 ("US-Letter portrait (300ppi)", (2550, 3300)),
666 )
667
668 def apply_template(combo):
669 t = combo.get_model()[combo.get_active_iter()][1]
670 if t:
671 dims = dict(templates)[t]
672 self.etr_outw.set_text(str(dims[0]))
673 self.etr_outh.set_text(str(dims[1]))
674 self.cmb_template.set_active(0)
675
676 box = Gtk.Box(spacing=6)
677 vbox.pack_start(box, False, False, 0)
678 box.pack_start(Gtk.Label(_("Apply a template:"), xalign=0),
679 True, True, 0)
680
681 self.cmb_template = Gtk.ComboBoxText()
682 for t, d in templates:
683 self.cmb_template.append(t, t)
684 self.cmb_template.set_active(0)
685 self.cmb_template.connect("changed", apply_template)
686 box.pack_start(self.cmb_template, False, False, 0)
687
688 vbox.pack_start(Gtk.SeparatorToolItem(), True, True, 0)
689
690 label = Gtk.Label(xalign=0)
691 label.set_markup("<big><b>%s</b></big>" % _("Border"))
692 vbox.pack_start(label, False, False, 0)
693
694 box = Gtk.Box(spacing=6)
695 vbox.pack_start(box, False, False, 0)
696 label = Gtk.Label(_("Thickness:"), xalign=0)
697 box.pack_start(label, False, False, 0)
698 self.etr_border = Gtk.Entry(text=str(100.0 * parent.opts.border_w))
699 self.etr_border.connect("changed", self.validate_float)
700 self.etr_border.last_valid_text = self.etr_border.get_text()
701 self.etr_border.set_width_chars(4)
702 self.etr_border.set_alignment(1.0)
703 box.pack_start(self.etr_border, False, False, 0)
704 label = Gtk.Label("%", xalign=0)
705 box.pack_start(label, False, False, 0)
706
707 label = Gtk.Label(_("Color:"), xalign=1)
708 box.pack_start(label, True, True, 0)
709 self.colorbutton = Gtk.ColorButton()
710 color = Gdk.RGBA()
711 color.parse(parent.opts.border_c)
712 self.colorbutton.set_rgba(color)
713 box.pack_end(self.colorbutton, False, False, 0)
714
715 vbox.pack_start(Gtk.SeparatorToolItem(), True, True, 0)
716
717 self.show_all()
718
719 def validate_int(self, entry):
720 entry_text = entry.get_text() or '0'
721 try:
722 int(entry_text)
723 entry.last_valid_text = entry_text
724 except ValueError:
725 entry.set_text(entry.last_valid_text)
726
727 def validate_float(self, entry):
728 entry_text = entry.get_text() or '0'
729 try:
730 float(entry_text)
731 entry.last_valid_text = entry_text
732 except ValueError:
733 entry.set_text(entry.last_valid_text)
734
735 def apply_opts(self, opts):
736 opts.out_w = int(self.etr_outw.get_text() or '1')
737 opts.out_h = int(self.etr_outh.get_text() or '1')
738 opts.border_w = float(self.etr_border.get_text() or '0') / 100.0
739 opts.border_c = self.colorbutton.get_rgba().to_string()
740
741
742 class ComputingDialog(Gtk.Dialog):
743 """Simple "please wait" dialog, with a "cancel" button"""
744 def __init__(self, parent):
745 super().__init__(
746 _("Please wait"), parent, 0, (Gtk.STOCK_CANCEL,
747 Gtk.ResponseType.CANCEL))
748 self.set_default_size(300, -1)
749 self.set_border_width(10)
750
751 box = self.get_content_area()
752 vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
753 box.add(vbox)
754
755 label = Gtk.Label(_("Performing image computation..."))
756 vbox.pack_start(label, True, True, 0)
757
758 self.progressbar = Gtk.ProgressBar()
759 self.progressbar.set_fraction(0)
760 vbox.pack_start(self.progressbar, True, True, 0)
761
762 self.show_all()
763
764 def update(self, fraction):
765 self.progressbar.set_fraction(fraction)
766
767
768 class ErrorDialog(Gtk.Dialog):
769 def __init__(self, parent, message):
770 super().__init__(_("Error"), parent, 0,
771 (Gtk.STOCK_OK, Gtk.ResponseType.OK))
772 self.set_border_width(10)
773 box = self.get_content_area()
774 box.add(Gtk.Label(message))
775 self.show_all()
776
777
778 class PreviewFileChooserDialog(Gtk.FileChooserDialog):
779 PREVIEW_MAX_SIZE = 256
780
781 def __init__(self, **kw):
782 super().__init__(**kw)
783
784 self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
785 self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
786
787 set_open_image_filters(self)
788
789 self._preview = Gtk.Image()
790 # Don't let preview size down horizontally for skinny images, cause
791 # that looks distracting
792 self._preview.set_size_request(
793 PreviewFileChooserDialog.PREVIEW_MAX_SIZE, -1)
794 self.set_preview_widget(self._preview)
795 self.set_use_preview_label(False)
796 self.connect("update-preview", self.update_preview_cb)
797
798 def update_preview_cb(self, file_chooser):
799 filename = self.get_preview_filename()
800 if filename is None or os.path.isdir(filename):
801 self.set_preview_widget_active(False)
802 return
803 try:
804 pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
805 filename,
806 PreviewFileChooserDialog.PREVIEW_MAX_SIZE,
807 PreviewFileChooserDialog.PREVIEW_MAX_SIZE)
808 self._preview.set_from_pixbuf(pixbuf)
809 except Exception as e:
810 print(e)
811 self.set_preview_widget_active(False)
812 return
813 self.set_preview_widget_active(True)
814
815
816 def main():
817 # Enable threading. Without that, threads hang!
818 GObject.threads_init()
819
820 win = PhotoCollageWindow()
821 win.connect("delete-event", Gtk.main_quit)
822 win.show_all()
823
824 # If arguments are given, treat them as input images
825 if len(sys.argv) > 1:
826 win.update_photolist(sys.argv[1:])
827
828 Gtk.main()