handle undo in changedir for captionfile for subalbums
[booh] / bin / booh
1 #!/usr/bin/ruby
2 #
3 #                         *  BOOH  *
4 #
5 # A.k.a `Best web-album Of the world, Or your money back, Humerus'.
6 #
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
11 #
12 #
13 # Copyright (c) 2004 Guillaume Cottenceau <gc3 at bluewin.ch>
14 #
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21
22 require 'getoptlong'
23 require 'tempfile'
24
25 require 'gtk2'
26 require 'booh/GtkAutoTable'
27
28 require 'gettext'
29 include GetText
30 bindtextdomain("booh")
31
32 require 'rexml/document'
33 include REXML
34
35 require 'booh/booh-lib'
36 include Booh
37 require 'booh/UndoHandler'
38
39
40 #- options
41 $options = [
42     [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
43
44     [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
45 ]
46
47 def usage
48     puts _("Usage: %s [OPTION]...") % File.basename($0)
49     $options.each { |ary|
50         printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
51     }
52 end
53
54 def handle_options
55     parser = GetoptLong.new
56     parser.set_options(*$options.collect { |ary| ary[0..2] })
57     begin
58         parser.each_option do |name, arg|
59             case name
60             when '--help'
61                 usage
62                 exit(0)
63
64             when '--verbose-level'
65                 $verbose_level = arg.to_i
66
67             end
68         end
69     rescue
70         puts $!
71         usage
72         exit(1)
73     end
74 end
75
76 def read_config
77     $config = {}
78     $config_file = File.expand_path('~/.booh-gui-rc')
79     if File.readable?($config_file)
80         $xmldoc = REXML::Document.new(File.new($config_file))
81         $xmldoc.root.elements.each { |element|
82             txt = element.get_text
83             if txt 
84                 if txt.value =~ /~~~/ || element.name == 'last-opens'
85                     $config[element.name] = txt.value.split(/~~~/)
86                 else
87                     $config[element.name] = txt.value
88                 end
89             else
90                 $config[element.name] = {}
91                 element.each { |chld|
92                     txt = chld.get_text
93                     $config[element.name][chld.name] = txt ? txt.value : nil
94                 }
95             end
96         }
97     end
98     if !FileTest.directory?(File.expand_path('~/.booh'))
99         system("mkdir ~/.booh")
100     end
101 end
102
103 def write_config
104     if $config['last-opens'] && $config['last-opens'].size > 5
105         $config['last-opens'] = $config['last-opens'][-5, 5]
106     end
107
108     ios = File.open($config_file, "w")
109     $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
110     $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
111     $config.each_pair { |key, value|
112         elem = $xmldoc.root.add_element key
113         if value.is_a? Hash
114             $config[key].each_pair { |subkey, subvalue|
115                 subelem = elem.add_element subkey
116                 subelem.add_text subvalue.to_s
117             }
118         elsif value.is_a? Array
119             elem.add_text value.join('~~~')
120         else
121             elem.add_text value.to_s
122         end
123     }
124     $xmldoc.write(ios, 0)
125     ios.close
126 end
127
128 def set_mousecursor(what, *widget)
129     if widget[0] && widget[0].window
130         widget[0].window.set_cursor(Gdk::Cursor.new(what))
131     end
132     if $main_window.window
133         $main_window.window.set_cursor(Gdk::Cursor.new(what))
134     end
135     $current_cursor = what
136 end
137 def set_mousecursor_wait(*widget)
138     set_mousecursor(Gdk::Cursor::WATCH, *widget)
139     if Thread.current == Thread.main
140         Gtk.main_iteration while Gtk.events_pending?
141     end
142 end
143 def set_mousecursor_normal(*widget)
144     set_mousecursor(Gdk::Cursor::LEFT_PTR, *widget)
145 end
146 def push_mousecursor_wait(*widget)
147     if $current_cursor != Gdk::Cursor::WATCH
148         $save_cursor = $current_cursor
149         set_mousecursor_wait(*widget)
150     end
151 end
152 def pop_mousecursor(*widget)
153     set_mousecursor($save_cursor || Gdk::Cursor::LEFT_PTR, *widget)
154 end
155
156 def current_dest_dir
157     source = $xmldoc.root.attributes['source']
158     dest = $xmldoc.root.attributes['destination']
159     return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
160 end
161
162 def build_full_dest_filename(filename)
163     return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
164 end
165
166 def save_undo(name, closure, *params)
167     UndoHandler.save_undo(name, closure, [ *params ])
168     $undo_tb.sensitive = $undo_mb.sensitive = true
169 end
170
171 def view_element(filename)
172     if entry2type(filename) == 'video'
173         system("mplayer '#{from_utf8($current_path + '/' + filename)}'")
174         return
175     end
176
177     w = Gtk::Window.new
178
179     msg 3, "filename: #{filename}"
180     dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
181     #- typically this file won't exist in case of videos; try with the largest thumbnail around
182     if !File.exists?(dest_img)
183         if entry2type(filename) == 'video'
184             alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
185             if not alternatives.empty?
186                 dest_img = alternatives[-1]
187             end
188         else
189             push_mousecursor_wait
190             gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
191             pop_mousecursor
192             if !File.exists?(dest_img)
193                 msg 2, _("Could not generate fullscreen thumbnail!")
194                 return
195                 end
196         end
197     end
198     evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
199
200     tooltips = Gtk::Tooltips.new
201     tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
202
203     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
204     b.signal_connect('clicked') { w.destroy }
205
206     vb = Gtk::VBox.new
207     vb.pack_start(evt, false, false)
208     vb.pack_end(bottom, false, false)
209
210     w.add(vb)
211     w.signal_connect('delete-event') { w.destroy }
212     w.window_position = Gtk::Window::POS_CENTER
213     w.show_all
214 end
215
216 def create_editzone(scrolledwindow, pagenum, image)
217     frame = Gtk::Frame.new
218     frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
219     frame.set_shadow_type(Gtk::SHADOW_IN)
220     textview.signal_connect('key-press-event') { |w, event|
221         textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
222         if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
223             scrolledwindow.signal_emit('key-press-event', event)
224         end
225         if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
226            event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
227             if event.keyval == Gdk::Keyval::GDK_Up
228                 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
229                     scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
230                 else
231                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
232                 end
233             else
234                 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
235                     scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
236                 else
237                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
238                 end
239             end
240         end
241         false  #- propagate
242     }
243     textview.signal_connect('focus-in-event') { |w, event|
244         textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
245         false  #- propagate
246     }
247
248     candidate_undo_text = nil
249     textview.signal_connect('focus-in-event') { |w, event|
250         candidate_undo_text = textview.buffer.text
251         false  #- propagate
252     }
253     textview.signal_connect('key-release-event') { |w, event|
254         if candidate_undo_text && candidate_undo_text != textview.buffer.text
255             $modified = true
256             save_undo(_("text edit"),
257                       Proc.new { |text|
258                           save_text = textview.buffer.text
259                           textview.buffer.text = text
260                           textview.grab_focus
261                           $notebook.set_page(pagenum)
262                           Proc.new {
263                               textview.buffer.text = save_text
264                               textview.grab_focus
265                               $notebook.set_page(pagenum)
266                           }
267                       }, candidate_undo_text)
268             candidate_undo_text = nil
269         end
270
271         if ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
272             #- autoscroll if cursor or image is not visible
273             ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
274             ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
275             current_miny_visible = scrolledwindow.vadjustment.value
276             current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
277             if ypos_top < current_miny_visible
278                 newval = scrolledwindow.vadjustment.value -
279                          ((current_miny_visible - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
280                 if newval < scrolledwindow.vadjustment.lower
281                     newval = scrolledwindow.vadjustment.lower
282                 end
283                 scrolledwindow.vadjustment.value = newval
284             elsif ypos_bottom > current_maxy_visible
285                 newval = scrolledwindow.vadjustment.value +
286                          ((ypos_bottom - current_maxy_visible - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
287                 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
288                     newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
289                 end
290                 scrolledwindow.vadjustment.value = newval
291             end
292         end
293         false  #- propagate
294     }
295
296     return [ frame, textview ]
297 end
298
299 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
300     $modified = true
301
302     #- update rotate attribute
303     xmlelem.add_attribute("#{attributes_prefix}rotate", current_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
304
305     if !$rotated_pixbufs[thumbnail_img]
306         $rotated_pixbufs[thumbnail_img] = { :orig => img.pixbuf, :angle_to_orig => angle % 360 }
307     else
308         $rotated_pixbufs[thumbnail_img][:angle_to_orig] = ($rotated_pixbufs[thumbnail_img][:angle_to_orig] + angle) % 360
309     end
310     msg 3, "angle: #{angle}, angle to orig: #{$rotated_pixbufs[thumbnail_img][:angle_to_orig]}"
311
312     #- rotate shown thumbnail
313     pixbuf = rotate_pixbuf($rotated_pixbufs[thumbnail_img][:orig], $rotated_pixbufs[thumbnail_img][:angle_to_orig])
314     msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
315     if pixbuf.height > desired_y
316         img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y,
317                                                                              Gdk::Pixbuf::INTERP_BILINEAR)
318     elsif pixbuf.width < desired_x && pixbuf.height < desired_y
319         img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width),
320                                                                              Gdk::Pixbuf::INTERP_BILINEAR)
321     else
322         img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf
323     end
324 end
325
326 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
327     $modified = true
328
329     rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
330
331     save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
332               Proc.new { |angle|
333                   rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
334                   $notebook.set_page(attributes_prefix != '' ? 0 : 1)
335                   Proc.new {
336                       rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
337                       $notebook.set_page(0)
338                       $notebook.set_page(attributes_prefix != '' ? 0 : 1)
339                   }
340               }, -angle)
341 end
342
343 def color_swap(xmldir, attributes_prefix)
344     $modified = true
345     if xmldir.attributes["#{attributes_prefix}color-swap"]
346         xmldir.delete_attribute("#{attributes_prefix}color-swap")
347     else
348         xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
349     end
350 end
351
352 def enhance(xmldir, attributes_prefix)
353     $modified = true
354     if xmldir.attributes["#{attributes_prefix}enhance"]
355         xmldir.delete_attribute("#{attributes_prefix}enhance")
356     else
357         xmldir.add_attribute("#{attributes_prefix}enhance", '1')
358     end
359 end
360
361 def change_frame_offset(xmldir, attributes_prefix, value)
362     $modified = true
363     xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
364 end
365
366 def ask_new_frame_offset(xmldir, attributes_prefix)
367     value = xmldir.attributes["#{attributes_prefix}frame-offset"]
368
369     dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
370                              $main_window,
371                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
372                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
373                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
374
375     lbl = Gtk::Label.new
376     lbl.markup = utf8(
377 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
378 from. There are approximately 25 frames per second in a video.
379 "))
380     dialog.vbox.add(lbl)
381     dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
382     entry.signal_connect('key-press-event') { |w, event|
383         if event.keyval == Gdk::Keyval::GDK_Return
384             dialog.response(Gtk::Dialog::RESPONSE_OK)
385             true
386         elsif event.keyval == Gdk::Keyval::GDK_Escape
387             dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
388             true
389         else
390             false  #- propagate if needed
391         end
392     }
393     
394     dialog.window_position = Gtk::Window::POS_MOUSE
395     dialog.show_all
396
397     dialog.run { |response|
398         $modified = true
399         newval = entry.text
400         dialog.destroy
401         if response == Gtk::Dialog::RESPONSE_OK
402             msg 3, "changing frame offset top #{newval}"
403             return { :old => value, :new => newval }
404         else
405             return nil
406         end
407     }
408 end
409
410 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img)
411     Thread.new {
412         push_mousecursor_wait
413         system("rm -f '#{destfile}'")
414         #- type can be 'element' or 'subdir'
415         if type == 'element'
416             gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
417         else
418             gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], find_subalbum_info_type(xmldir))
419         end
420         img.set(destfile)
421         $rotated_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
422         if entry2type(origfile) == 'video'
423             #- cleanup temp for videos
424             system("rm -f #{current_dest_dir}/screenshot.jpg000000.jpg")
425         end
426         pop_mousecursor
427     }
428 end
429
430 def popup_thumbnail_menu(event, optionals, type, xmldir, attributes_prefix, closures)
431     menu = Gtk::Menu.new
432     if optionals.include?('change_image')
433         menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
434         changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
435         changeimg.signal_connect('activate') { closures[:change].call }
436         menu.append(            Gtk::SeparatorMenuItem.new)
437     end
438     menu.append(    r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
439     r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
440     r90.signal_connect('activate') { closures[:rotate].call(90) }
441     menu.append(   r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
442     r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
443     r270.signal_connect('activate') { closures[:rotate].call(-90) }
444     if type == 'video'
445         menu.append(               Gtk::SeparatorMenuItem.new)
446         menu.append(  color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
447         color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
448         color_swap.signal_connect('activate') { closures[:color_swap].call }
449         menu.append(        flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
450         flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
451         flip.signal_connect('activate') { closures[:rotate].call(180) }
452         menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
453         frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
454         frame_offset.signal_connect('activate') { closures[:frame_offset].call }
455     end
456     menu.append(               Gtk::SeparatorMenuItem.new)
457     menu.append(enhance      = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
458                                                                                                               _("Enhance constrast"))))
459     enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
460     enhance.signal_connect('activate') { closures[:enhance].call }
461     if optionals.include?('delete')
462         menu.append(               Gtk::SeparatorMenuItem.new)
463         menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
464         delete_item.signal_connect('activate') { closures[:delete].call }
465     end
466     menu.show_all
467     menu.popup(nil, nil, event.button, event.time)
468 end
469
470 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
471
472     img = nil
473     frame1 = Gtk::Frame.new
474
475     my_gen_real_thumbnail = proc {
476         gen_real_thumbnail('element', from_utf8("#{$current_path}/#{filename}"), thumbnail_img, $xmldir, $default_size['thumbnails'], img)
477     }
478
479     #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
480     if !$rotated_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
481         frame1.add(img = Gtk::Image.new)
482         my_gen_real_thumbnail.call
483     else
484         frame1.add(img = Gtk::Image.new($rotated_pixbufs[thumbnail_img] ? $rotated_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
485     end
486     evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
487
488     tooltips = Gtk::Tooltips.new
489     tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
490     tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(from_utf8("#{$current_path}/#{filename}"))/1024)]) : tipname), nil)
491
492     frame2, textview = create_editzone($autotable_sw, 1, img)
493     textview.buffer.text = utf8(caption)
494     textview.set_justification(Gtk::Justification::CENTER)
495
496     vbox = Gtk::VBox.new(false, 5)
497     vbox.pack_start(evtbox, false, false)
498     vbox.pack_start(frame2, false, false)
499     autotable.append(vbox, filename)
500
501     #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
502     $vbox2textview[vbox] = textview
503
504     #- to be able to find widgets by name
505     $name2widgets[filename] = { :textview => textview }
506
507     cleanup_all_thumbnails = Proc.new {
508         #- remove out of sync images
509         dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
510         for sizeobj in $images_size
511             system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
512         end
513
514     }
515
516     rotate_and_cleanup = Proc.new { |angle|
517         rotate(angle, thumbnail_img, img, $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
518         cleanup_all_thumbnails.call
519     }
520
521     color_swap_and_cleanup = Proc.new {
522         perform_color_swap_and_cleanup = Proc.new {
523             color_swap($xmldir.elements["[@filename='#{filename}']"], '')
524             my_gen_real_thumbnail.call
525         }
526
527         cleanup_all_thumbnails.call
528         perform_color_swap_and_cleanup.call
529
530         save_undo(_("color swap"),
531                   Proc.new {
532                       perform_color_swap_and_cleanup.call
533                       $notebook.set_page(1)
534                       Proc.new {
535                           perform_color_swap_and_cleanup.call
536                           $notebook.set_page(1)
537                       }
538                   })
539     }
540
541     change_frame_offset_and_cleanup = Proc.new {
542         if values = ask_new_frame_offset($xmldir.elements["[@filename='#{filename}']"], '')
543             perform_change_frame_offset_and_cleanup = Proc.new { |val|
544                 change_frame_offset($xmldir.elements["[@filename='#{filename}']"], '', val)
545                 my_gen_real_thumbnail.call
546             }
547             perform_change_frame_offset_and_cleanup.call(values[:new])
548
549             save_undo(_("specify frame offset"),
550                       Proc.new {
551                           perform_change_frame_offset_and_cleanup.call(values[:old])
552                           $notebook.set_page(1)
553                           Proc.new {
554                               perform_change_frame_offset_and_cleanup.call(values[:new])
555                               $notebook.set_page(1)
556                           }
557                       })
558         end
559     }
560
561     enhance_and_cleanup = Proc.new {
562         perform_enhance_and_cleanup = Proc.new {
563             enhance($xmldir.elements["[@filename='#{filename}']"], '')
564             my_gen_real_thumbnail.call
565         }
566
567         cleanup_all_thumbnails.call
568         perform_enhance_and_cleanup.call
569
570         save_undo(_("enhance"),
571                   Proc.new {
572                       perform_enhance_and_cleanup.call
573                       $notebook.set_page(1)
574                       Proc.new {
575                           perform_enhance_and_cleanup.call
576                           $notebook.set_page(1)
577                       }
578                   })
579     }
580
581     delete = Proc.new {
582         if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed?")), { :okcancel => true })
583             $modified = true
584             after = nil
585             perform_delete = Proc.new {
586                 after = autotable.get_next_widget(vbox)
587                 if !after
588                     after = autotable.get_previous_widget(vbox)
589                 end
590                 autotable.remove(vbox)
591                 if after
592                     $vbox2textview[after].grab_focus
593                 end
594             }
595             
596             perform_delete.call
597
598             if !after
599                 if $xmldir.elements['dir']
600                     $xmldir.delete_attribute('thumbnails-caption')
601                     $xmldir.delete_attribute('thumbnails-captionfile')
602                 else
603                     $xmldir.remove
604                 end
605                 save_changes('forced')
606                 populate_subalbums_treeview
607             else
608                 save_undo(_("delete"),
609                           Proc.new { |pos|
610                               autotable.reinsert(pos, vbox, filename)
611                               $notebook.set_page(1)
612                               Proc.new {
613                                   perform_delete.call
614                                   $notebook.set_page(1)
615                               }
616                           }, autotable.get_current_number(vbox))
617             end
618         end
619     }
620
621     textview.signal_connect('key-press-event') { |w, event|
622         propagate = true
623         if event.state != 0
624             x, y = autotable.get_current_pos(vbox)
625             control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
626             shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
627             alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
628             if event.keyval == Gdk::Keyval::GDK_Up && y > 0
629                 if control_pressed
630                     $vbox2textview[autotable.get_widget_at_pos(x, y - 1)].grab_focus
631                 end
632                 if shift_pressed
633                     autotable.move_up(vbox)
634                     textview.grab_focus  #- because if moving, focus is stolen
635                 end
636             end
637             if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
638                 if control_pressed
639                     $vbox2textview[autotable.get_widget_at_pos(x, y + 1)].grab_focus
640                 end
641                 if shift_pressed
642                     autotable.move_down(vbox)
643                     textview.grab_focus  #- because if moving, focus is stolen
644                 end
645             end
646             if event.keyval == Gdk::Keyval::GDK_Left
647                 previous = autotable.get_previous_widget(vbox)
648                 if previous && autotable.get_current_pos(previous)[0] < x
649                     if control_pressed
650                         $vbox2textview[previous].grab_focus
651                     end
652                     if shift_pressed
653                         autotable.move_left(vbox)
654                         textview.grab_focus  #- because if moving, focus is stolen
655                     end
656                 end
657                 if alt_pressed
658                     rotate_and_cleanup.call(-90)
659                 end
660             end
661             if event.keyval == Gdk::Keyval::GDK_Right
662                 next_ = autotable.get_next_widget(vbox)
663                 if next_ && autotable.get_current_pos(next_)[0] > x
664                     if control_pressed
665                         $vbox2textview[next_].grab_focus
666                     end
667                     if shift_pressed
668                         autotable.move_right(vbox)
669                         textview.grab_focus  #- because if moving, focus is stolen
670                     end
671                 end
672                 if alt_pressed
673                     rotate_and_cleanup.call(90)
674                 end
675             end
676             if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
677                 delete.call
678             end
679             if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
680                 view_element(filename)
681                 propagate = false
682             end
683         end
684         !propagate  #- propagate if needed
685     }
686
687     evtbox.signal_connect('button-press-event') { |w, event|
688         retval = true
689         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
690             shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
691             if $r90.active?
692                 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
693             elsif $r270.active?
694                 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
695             elsif $enhance.active?
696                 enhance_and_cleanup.call
697             elsif $delete.active?
698                 delete.call
699             else
700                 textview.grab_focus
701             end
702         end
703         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
704             popup_thumbnail_menu(event, ['delete'], type, $xmldir.elements["[@filename='#{filename}']"], '',
705                                  { :rotate => rotate_and_cleanup, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
706                                    :frame_offset => change_frame_offset_and_cleanup, :delete => delete })
707         end
708         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
709             view_element(filename)
710         else
711             retval = false  #- propagate
712         end
713         retval
714     }
715
716     vbox.signal_connect('button-press-event') { |w, event|
717         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
718             $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
719         end
720         false
721     }
722     vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
723         if $gesture_press && $gesture_press[:filename] == filename
724             if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
725                 angle = x-$gesture_press[:x] > 0 ? 90 : -90
726                 msg 3, "gesture rotate: #{angle}"
727                 rotate_and_cleanup.call(angle)
728             end
729         end
730         $gesture_press = nil
731     }
732
733     #- handle reordering with drag and drop
734     Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
735     Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
736     vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
737         selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
738     }
739     vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
740         ctxt.targets.each { |target|
741             if target.name == 'reorder-elements'
742                 from, to = selection_data.data.to_i, autotable.get_current_number(vbox)
743                 if from != to
744                     $modified = true
745                     autotable.move(from, to)
746                     save_undo(_("reorder"),
747                               Proc.new { |from, to|
748                                   if to > from
749                                       autotable.move(to - 1, from)
750                                   else
751                                       autotable.move(to, from + 1)
752                                   end
753                                   $notebook.set_page(1)
754                                   Proc.new {
755                                       autotable.move(from, to)
756                                       $notebook.set_page(1)
757                                   }
758                               }, from, to)
759                 end
760             end
761         }
762     }
763
764     vbox.show_all
765 end
766
767 def create_auto_table
768
769     $autotable = Gtk::AutoTable.new(5)
770
771     $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
772     $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
773     $autotable_sw.add_with_viewport($autotable)
774 end
775
776 def create_subalbums_page
777
778     subalbums_hb = Gtk::HBox.new
779     $subalbums_vb = Gtk::VBox.new(false, 5)
780     subalbums_hb.pack_start($subalbums_vb, false, false)
781     $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
782     $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
783     $subalbums_sw.add_with_viewport(subalbums_hb)
784 end
785
786 def save_current_file
787     save_changes
788     if $filename
789         ios = File.open($filename, "w")
790         $xmldoc.write(ios, 0)
791         ios.close
792         $modified = false
793     end
794 end
795
796 def try_quit
797     if $modified
798         dialog = Gtk::Dialog.new(utf8(_("Save before quitting?")),
799                                  $main_window,
800                                  Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
801                                  [Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
802                                  [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
803                                  [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
804         dialog.vbox.add(Gtk::Label.new(utf8(_("Do you want to save your changes before quitting?"))))
805         dialog.window_position = Gtk::Window::POS_CENTER
806         dialog.show_all
807         
808         dialog.run { |response|
809             dialog.destroy
810             if response == Gtk::Dialog::RESPONSE_CANCEL
811                 return
812             elsif response == Gtk::Dialog::RESPONSE_YES
813                 save_current_file
814             end
815         }
816     end
817
818     Gtk.main_quit
819 end
820
821 def show_popup(parent, msg, *options)
822     dialog = Gtk::Dialog.new
823     dialog.title = utf8(_("Booh message"))
824     lbl = Gtk::Label.new
825     lbl.markup = msg
826     if options[0] && options[0][:centered]
827         lbl.set_justify(Gtk::Justification::CENTER)
828     end
829     dialog.vbox.add(lbl)
830     if options[0] && options[0][:okcancel]
831         dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
832     end
833     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
834
835     dialog.transient_for = parent
836     dialog.set_default_size(200, 120)
837     if options[0] && options[0][:pos_centered]
838         dialog.window_position = Gtk::Window::POS_CENTER
839     else
840         dialog.window_position = Gtk::Window::POS_MOUSE
841     end
842     dialog.show_all
843
844     dialog.run { |response|
845         dialog.destroy
846         if options[0] && options[0][:okcancel]
847             return response == Gtk::Dialog::RESPONSE_OK
848         end
849     }
850 end
851
852 def wait_message(parent, msg)
853     w = Gtk::Window.new
854     w.set_transient_for(parent)
855     w.modal = true
856
857     vb = Gtk::VBox.new(false, 5)
858     vb.set_border_width(5)
859     vb.pack_start(Gtk::Label.new(msg), false, false)
860     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
861     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
862     vb.pack_start(pb = Gtk::ProgressBar.new.set_pulse_step(0.05), false, false)
863     vb.pack_start(Gtk::HSeparator.new, false, false)
864     vb.pack_end(bottom, false, false)
865
866     timeout = Gtk.timeout_add(200) { pb.pulse }
867
868     w.add(vb)
869     w.signal_connect('delete-event') { w.destroy }
870     w.signal_connect('destroy') { Gtk.timeout_remove(timeout) }
871     w.window_position = Gtk::Window::POS_CENTER
872     w.show_all
873
874     return [ b, w ]
875 end
876
877 def perform_in_background(cmd, waitmsg, params)
878     button, w8 = wait_message($main_window, waitmsg)
879     pid = nil
880     Thread.new {
881         msg 2, cmd
882         if pid = fork
883             id, exitstatus = Process.waitpid2(pid)
884             w8.destroy
885             if exitstatus == 0
886                 if params[:successmsg]
887                     show_popup($main_window, params[:successmsg])
888                 end
889                 if params[:closure_after]
890                     params[:closure_after].call
891                 end
892             elsif exitstatus == 15
893                 #- say nothing, user aborted
894             else
895                 if params[:failuremsg]
896                     show_popup($main_window, params[:failuremsg])
897                 end
898             end
899         else
900             exec(cmd)
901         end
902     }
903     button.signal_connect('clicked') {
904         Process.kill('SIGTERM', pid)
905     }
906 end
907
908 def save_changes(*forced)
909     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
910         return
911     end
912
913     if $xmldir.elements['dir']
914         $xmldir.add_attribute('subdirs-caption', $subalbums_title.buffer.text)
915         $xmldir.elements.each('dir') { |element|
916             path = element.attributes['path']
917             if element.attributes['subdirs-caption']
918                 element.add_attribute('subdirs-caption',     $subalbums_edits[path][:editzone].buffer.text)
919                 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
920             else
921                 element.add_attribute('thumbnails-caption',     $subalbums_edits[path][:editzone].buffer.text)
922                 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
923             end
924         }
925         if $xmldir.attributes['thumbnails-caption']
926             path = $xmldir.attributes['path']
927             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
928         end
929     end
930
931     #- remove and reinsert elements to reflect new ordering
932     save_attributes = {}
933     save_types = {}
934     cpt = 0
935     $xmldir.elements.each { |element|
936         if element.name == 'image' || element.name == 'video'
937             save_types[element.attributes['filename']] = element.name
938             save_attributes[element.attributes['filename']] = element.attributes
939             element.remove
940             cpt += 1
941         end
942     }
943     $autotable.current_order.each { |path|
944         chld = $xmldir.add_element save_types[path], save_attributes[path]
945         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
946     }
947 end
948
949 def change_dir
950     $autotable.clear
951     $vbox2textview = {}
952     $name2widgets = {}
953     UndoHandler.cleanup
954     $undo_tb.sensitive = $undo_mb.sensitive = false
955     $redo_tb.sensitive = $redo_mb.sensitive = false
956
957     if !$current_path
958         return
959     end
960
961     $subalbums_vb.children.each { |chld|
962         $subalbums_vb.remove(chld)
963     }
964     $subalbums = Gtk::Table.new(0, 0, true)
965     current_y_sub_albums = 0
966
967     $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
968     $subalbums_edits = {}
969
970     add_subalbum = Proc.new { |xmldir|
971         if xmldir == $xmldir
972             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
973             caption = xmldir.attributes['thumbnails-caption']
974             captionfile, dummy = find_subalbum_caption_info(xmldir)
975             infotype = 'thumbnails'
976         else
977             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
978             captionfile, caption = find_subalbum_caption_info(xmldir)
979             infotype = find_subalbum_info_type(xmldir)
980         end
981         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
982         hbox = Gtk::HBox.new
983         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
984         f = Gtk::Frame.new
985         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
986
987         img = nil
988         my_gen_real_thumbnail = proc {
989             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img)
990         }
991
992         if !$rotated_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
993             f.add(img = Gtk::Image.new)
994             my_gen_real_thumbnail.call
995         else
996             f.add(img = Gtk::Image.new($rotated_pixbufs[thumbnail_file] ? $rotated_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
997         end
998         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
999         $subalbums.attach(hbox,
1000                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1001
1002         change_image = Proc.new {
1003             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
1004                                             nil,
1005                                             Gtk::FileChooser::ACTION_OPEN,
1006                                             nil,
1007                                             [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1008             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
1009             fc.transient_for = $main_window
1010             fc.preview_widget = preview = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(f = Gtk::Frame.new.set_shadow_type(Gtk::SHADOW_ETCHED_OUT))
1011             f.add(preview_img = Gtk::Image.new)
1012             preview.show_all
1013             fc.signal_connect('update-preview') { |w|
1014                 begin
1015                     if fc.preview_filename
1016                         preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
1017                         fc.preview_widget_active = true
1018                     end
1019                 rescue Gdk::PixbufError
1020                     fc.preview_widget_active = false
1021                 end
1022             }
1023             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1024                 $modified = true
1025                 old_file = captionfile
1026                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
1027                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
1028                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
1029                 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
1030
1031                 new_file = fc.filename
1032                 msg 3, "new captionfile is: #{fc.filename}"
1033                 perform_changefile = Proc.new {
1034                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
1035                     $rotated_pixbufs.delete(thumbnail_file)
1036                     xmldir.delete_attribute("#{infotype}-rotate")
1037                     xmldir.delete_attribute("#{infotype}-color-swap")
1038                     xmldir.delete_attribute("#{infotype}-enhance")
1039                     xmldir.delete_attribute("#{infotype}-frame-offset")
1040                     my_gen_real_thumbnail.call
1041                 }
1042                 perform_changefile.call
1043
1044                 save_undo(_("change caption file for sub-album"),
1045                           Proc.new {
1046                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
1047                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
1048                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
1049                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
1050                               xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
1051                               my_gen_real_thumbnail.call
1052                               $notebook.set_page(0)
1053                               Proc.new {
1054                                   perform_changefile.call
1055                                   $notebook.set_page(0)
1056                               }
1057                           })
1058             end
1059             fc.destroy
1060         }
1061
1062         rotate_and_cleanup = Proc.new { |angle|
1063             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
1064             system("rm -f '#{thumbnail_file}'")
1065         }
1066
1067         color_swap_and_cleanup = Proc.new {
1068             perform_color_swap_and_cleanup = Proc.new {
1069                 color_swap(xmldir, "#{infotype}-")
1070                 my_gen_real_thumbnail.call
1071             }
1072             perform_color_swap_and_cleanup.call
1073
1074             save_undo(_("color swap"),
1075                       Proc.new {
1076                           perform_color_swap_and_cleanup.call
1077                           $notebook.set_page(0)
1078                           Proc.new {
1079                               perform_color_swap_and_cleanup.call
1080                               $notebook.set_page(0)
1081                           }
1082                       })
1083         }
1084
1085         change_frame_offset_and_cleanup = Proc.new {
1086             if values = ask_new_frame_offset(xmldir, "#{infotype}-")
1087                 perform_change_frame_offset_and_cleanup = Proc.new { |val|
1088                     change_frame_offset(xmldir, "#{infotype}-", val)
1089                     my_gen_real_thumbnail.call
1090                 }
1091                 perform_change_frame_offset_and_cleanup.call(values[:new])
1092
1093                 save_undo(_("specify frame offset"),
1094                           Proc.new {
1095                               perform_change_frame_offset_and_cleanup.call(values[:old])
1096                               $notebook.set_page(0)
1097                               Proc.new {
1098                                   perform_change_frame_offset_and_cleanup.call(values[:new])
1099                                   $notebook.set_page(0)
1100                               }
1101                           })
1102             end
1103         }
1104
1105         enhance_and_cleanup = Proc.new {
1106             perform_enhance_and_cleanup = Proc.new {
1107                 enhance(xmldir, "#{infotype}-")
1108                 my_gen_real_thumbnail.call
1109             }
1110             
1111             perform_enhance_and_cleanup.call
1112             
1113             save_undo(_("enhance"),
1114                       Proc.new {
1115                           perform_enhance_and_cleanup.call
1116                           $notebook.set_page(0)
1117                           Proc.new {
1118                               perform_enhance_and_cleanup.call
1119                               $notebook.set_page(0)
1120                           }
1121                       })
1122         }
1123
1124         evtbox.signal_connect('button-press-event') { |w, event|
1125             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1126                 if $r90.active?
1127                     rotate_and_cleanup.call(90)
1128                 elsif $r270.active?
1129                     rotate_and_cleanup.call(-90)
1130                 elsif $enhance.active?
1131                     enhance_and_cleanup.call
1132                 end
1133             end
1134             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1135                 popup_thumbnail_menu(event, ['change_image'], entry2type(captionfile), xmldir, "#{infotype}-",
1136                                      { :change => change_image, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
1137                                        :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup })
1138             end
1139             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1140                 change_image.call
1141                 true   #- handled
1142             end
1143         }
1144         evtbox.signal_connect('button-press-event') { |w, event|
1145             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
1146             false
1147         }
1148
1149         evtbox.signal_connect('button-release-event') { |w, event|
1150             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
1151                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
1152                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
1153                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
1154                     msg 3, "gesture rotate: #{angle}"
1155                     rotate_and_cleanup.call(angle)
1156                 end
1157             end
1158             $gesture_press = nil
1159         }
1160         
1161         frame, textview = create_editzone($subalbums_sw, 0, img)
1162         textview.buffer.text = caption
1163         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1164                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1165         
1166         $subalbums_edits[xmldir.attributes['path']] = { :editzone => textview, :captionfile => captionfile }
1167         current_y_sub_albums += 1
1168     }
1169
1170     if $xmldir.elements['dir']
1171         #- title edition
1172         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
1173         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
1174         $subalbums_title.set_justification(Gtk::Justification::CENTER)
1175         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1176         #- this album image/caption
1177         if $xmldir.attributes['thumbnails-caption']
1178             add_subalbum.call($xmldir)
1179         end
1180     end
1181     $xmldir.elements.each { |element|
1182         if element.name == 'image' || element.name == 'video'
1183             #- element (image or video) of this album
1184             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
1185             msg 3, "dest_img: #{dest_img}"
1186             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
1187         end
1188         if element.name == 'dir'
1189             #- sub-album image/caption
1190             add_subalbum.call(element)
1191         end
1192     }
1193     $subalbums_vb.add($subalbums)
1194     $subalbums_vb.show_all
1195
1196     if !$xmldir.elements['image'] && !$xmldir.elements['video']
1197         $notebook.get_tab_label($autotable_sw).sensitive = false
1198         $notebook.set_page(0)
1199     else
1200         $notebook.get_tab_label($autotable_sw).sensitive = true
1201     end
1202
1203     if !$xmldir.elements['dir']
1204         $notebook.get_tab_label($subalbums_sw).sensitive = false
1205         $notebook.set_page(1)
1206     else
1207         $notebook.get_tab_label($subalbums_sw).sensitive = true
1208     end
1209 end
1210
1211 def pixbuf_or_nil(filename)
1212     begin
1213         return Gdk::Pixbuf.new(filename)
1214     rescue
1215         return nil
1216     end
1217 end
1218
1219 def theme_choose(current)
1220     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
1221                              $main_window,
1222                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1223                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1224                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1225
1226     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
1227     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
1228     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
1229     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
1230     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
1231     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
1232     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
1233     treeview.signal_connect('button-press-event') { |w, event|
1234         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1235             dialog.response(Gtk::Dialog::RESPONSE_OK)
1236         end
1237     }
1238
1239     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
1240
1241     `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
1242         dir.chomp!
1243         iter = model.append
1244         iter[0] = File.basename(dir)
1245         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
1246         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
1247         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
1248         if File.basename(dir) == current
1249             treeview.selection.select_iter(iter)
1250         end
1251     }
1252
1253     dialog.set_default_size(700, 400)
1254     dialog.vbox.show_all
1255     dialog.run { |response|
1256         iter = treeview.selection.selected
1257         dialog.destroy
1258         if response == Gtk::Dialog::RESPONSE_OK && iter
1259             return model.get_value(iter, 0)
1260         end
1261     }
1262     return nil
1263 end
1264
1265 def populate_subalbums_treeview
1266     $albums_ts.clear
1267     $autotable.clear
1268     $subalbums_vb.children.each { |chld|
1269         $subalbums_vb.remove(chld)
1270     }
1271
1272     source = $xmldoc.root.attributes['source']
1273     msg 3, "source: #{source}"
1274
1275     xmldir = $xmldoc.elements["//dir[@path='#{source}']"]
1276     if !xmldir
1277         msg 1, _("Corrupted booh file...")
1278         return
1279     end
1280
1281     append_dir_elem = Proc.new { |parent_iter, location|
1282         child_iter = $albums_ts.append(parent_iter)
1283         child_iter[0] = File.basename(location)
1284         child_iter[1] = location
1285         msg 3, "puttin location: #{location}"
1286         $xmldoc.elements.each("//dir[@path='#{location}']/dir") { |elem|
1287             append_dir_elem.call(child_iter, elem.attributes['path'])
1288         }
1289     }
1290     append_dir_elem.call(nil, source)
1291
1292     $albums_tv.expand_all
1293     $albums_tv.selection.select_iter($albums_ts.iter_first)
1294 end
1295
1296 def open_file(filename)
1297
1298     $filename = nil
1299     $modified = false
1300     $current_path = nil   #- invalidate
1301     $rotated_pixbufs = {}
1302     $albums_ts.clear
1303     $autotable.clear
1304     $subalbums_vb.children.each { |chld|
1305         $subalbums_vb.remove(chld)
1306     }
1307
1308     if !File.exists?(filename)
1309         return utf8(_("File not found."))
1310     end
1311
1312     begin
1313         $xmldoc = REXML::Document.new File.new(filename)
1314     rescue Exception
1315         $xmldoc = nil
1316     end
1317
1318     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
1319         if entry2type(filename).nil?
1320             return utf8(_("Not a booh file!"))
1321         else
1322             return utf8(_("Not a booh file!\n\nHint: you cannot import directly an image or video with File/Open.\nUse File/New to create a new album."))
1323         end
1324     end
1325
1326     if !source = $xmldoc.root.attributes['source']
1327         return utf8(_("Corrupted booh file..."))
1328     end
1329
1330     if !dest = $xmldoc.root.attributes['destination']
1331         return utf8(_("Corrupted booh file..."))
1332     end
1333
1334     if !theme = $xmldoc.root.attributes['theme']
1335         return utf8(_("Corrupted booh file..."))
1336     end
1337
1338     limit_sizes = $xmldoc.root.attributes['limit-sizes']
1339
1340     $filename = filename
1341     select_theme(theme, limit_sizes)
1342     $default_size['thumbnails'] =~ /(.*)x(.*)/
1343     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
1344     $albums_thumbnail_size =~ /(.*)x(.*)/
1345     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
1346
1347     populate_subalbums_treeview
1348
1349     $config['last-opens'] ||= []
1350     if $config['last-opens'][-1] != utf8(filename)
1351         $config['last-opens'] << utf8(filename)
1352     end
1353     $save.sensitive = $save_as.sensitive = $merge.sensitive = $merge_subalbums.sensitive = $generate.sensitive = $properties.sensitive = true
1354     return nil
1355 end
1356
1357 def open_file_popup
1358     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
1359                                     nil,
1360                                     Gtk::FileChooser::ACTION_OPEN,
1361                                     nil,
1362                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1363     fc.add_shortcut_folder(File.expand_path("~/.booh"))
1364     fc.set_current_folder(File.expand_path("~/.booh"))
1365     fc.transient_for = $main_window
1366     ok = false
1367     while !ok
1368         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1369             push_mousecursor_wait(fc)
1370             msg = open_file(fc.filename)
1371             pop_mousecursor(fc)
1372             if msg
1373                 show_popup(fc, msg)
1374                 ok = false
1375             else
1376                 ok = true
1377             end
1378         else
1379             ok = true
1380         end
1381     end
1382     fc.destroy
1383 end
1384
1385 def additional_booh_options
1386     options = ''
1387     if $config['mproc']
1388         options += "--mproc #{$config['mproc'].to_i} "
1389     end
1390     return options
1391 end
1392
1393 def new_album
1394     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
1395                              $main_window,
1396                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1397                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1398                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1399     
1400     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
1401     tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
1402                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1403     tbl.attach(src = Gtk::Entry.new,
1404                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1405     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
1406                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1407     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
1408                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1409     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
1410                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1411     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
1412                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1413     tbl.attach(dest = Gtk::Entry.new,
1414                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1415     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
1416                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1417     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
1418                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1419     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
1420                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
1421     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
1422                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1423
1424     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(tbl = Gtk::Table.new(0, 0, false))
1425     tbl.attach(Gtk::Label.new(utf8(_("Theme: "))),
1426                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1427     tbl.attach(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'),
1428                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1429     tbl.attach(Gtk::Label.new(utf8(_("Sizes of images to generate: "))),
1430                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1431     tbl.attach(sizes = Gtk::HBox.new,
1432                1, 3, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1433
1434     src_nb_calculated_for = ''
1435     src_nb_thread = nil
1436     process_src_nb = Proc.new {
1437         if src.text != src_nb_calculated_for
1438             src_nb_calculated_for = src.text
1439             if src_nb_thread
1440                 Thread.kill(src_nb_thread)
1441                 src_nb_thread = nil
1442             end
1443             if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
1444                 if File.readable?(from_utf8(src_nb_calculated_for))
1445                     src_nb_thread = Thread.new {
1446                         src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
1447                         total = { 'image' => 0, 'video' => 0, nil => 0 }
1448                         `find '#{from_utf8(src_nb_calculated_for)}' -type d`.each { |dir|
1449                             if File.basename(dir) =~ /^\./
1450                                 next
1451                             else
1452                                 begin
1453                                     Dir.entries(dir.chomp).each { |file|
1454                                         total[entry2type(file)] += 1
1455                                     }
1456                                 rescue Errno::EACCES, Errno::ENOENT
1457                                 end
1458                             end
1459                         }
1460                         src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ]))
1461                         src_nb_thread = nil
1462                     }
1463                 else
1464                     src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
1465                 end
1466             else
1467                 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
1468             end
1469         end
1470         true
1471     }
1472     timeout_src_nb = Gtk.timeout_add(100) {
1473         process_src_nb.call
1474     }
1475
1476     src_browse.signal_connect('clicked') {
1477         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
1478                                         nil,
1479                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
1480                                         nil,
1481                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1482         fc.transient_for = $main_window
1483         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1484             src.text = utf8(fc.filename)
1485             process_src_nb.call
1486             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
1487         end
1488         fc.destroy
1489     }
1490
1491     dest_browse.signal_connect('clicked') {
1492         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
1493                                         nil,
1494                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
1495                                         nil,
1496                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1497         fc.transient_for = $main_window
1498         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1499             dest.text = utf8(fc.filename)
1500         end
1501         fc.destroy
1502     }
1503
1504     conf_browse.signal_connect('clicked') {
1505         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
1506                                         nil,
1507                                         Gtk::FileChooser::ACTION_SAVE,
1508                                         nil,
1509                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1510         fc.transient_for = $main_window
1511         fc.add_shortcut_folder(File.expand_path("~/.booh"))
1512         fc.set_current_folder(File.expand_path("~/.booh"))
1513         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1514             conf.text = utf8(fc.filename)
1515         end
1516         fc.destroy
1517     }
1518
1519     theme_sizes = []
1520     recreate_theme_config = proc {
1521         theme_sizes.each { |e| sizes.remove(e[:widget]) }
1522         theme_sizes = []
1523         select_theme(theme_button.label, 'all')
1524         $images_size.each { |s|
1525             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
1526             if !s['optional']
1527                 cb.active = true
1528             end
1529             tooltips = Gtk::Tooltips.new
1530             tooltips.set_tip(cb, utf8(s['description']), nil)
1531             theme_sizes << { :widget => cb, :value => s['name'] }
1532         }
1533         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
1534         tooltips = Gtk::Tooltips.new
1535         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
1536         theme_sizes << { :widget => cb, :value => 'original' }
1537         sizes.show_all
1538     }
1539     recreate_theme_config.call
1540
1541     theme_button.signal_connect('clicked') {
1542         if newtheme = theme_choose(theme_button.label)
1543             theme_button.label = newtheme
1544             recreate_theme_config.call
1545         end
1546     }
1547
1548     dialog.vbox.add(frame1)
1549     dialog.vbox.add(frame2)
1550     dialog.window_position = Gtk::Window::POS_MOUSE
1551     dialog.show_all
1552
1553     keepon = true
1554     ok = true
1555     while keepon
1556         dialog.run { |response|
1557             if response == Gtk::Dialog::RESPONSE_OK
1558                 srcdir = from_utf8(src.text)
1559                 destdir = from_utf8(dest.text)
1560                 if !File.directory?(srcdir)
1561                     show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
1562                     src.grab_focus
1563                 elsif conf.text == ''
1564                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
1565                     conf.grab_focus
1566                 elsif destdir != make_dest_filename(destdir)
1567                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
1568                     dest.grab_focus
1569                 elsif File.directory?(destdir)
1570                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
1571                     dest.grab_focus
1572                 elsif File.exists?(destdir)
1573                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
1574                     dest.grab_focus
1575                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
1576                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
1577                 else
1578                     system("mkdir '#{destdir}'")
1579                     if !File.directory?(destdir)
1580                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
1581                         dest.grab_focus
1582                     else
1583                         keepon = false
1584                     end
1585                 end
1586             else
1587                 keepon = ok = false
1588             end
1589         }
1590     end
1591     srcdir = from_utf8(src.text)
1592     destdir = from_utf8(dest.text)
1593     configskel = File.expand_path(from_utf8(conf.text))
1594     theme = theme_button.label
1595     sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
1596     dialog.destroy
1597     if src_nb_thread
1598         Thread.kill(src_nb_thread)
1599     end
1600     Gtk.timeout_remove(timeout_src_nb)
1601
1602     if ok
1603         perform_in_background("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
1604                                   "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} #{additional_booh_options}",
1605                               utf8(_("Please wait while scanning source directory...")),
1606                               { :closure_after => proc { open_file(configskel) } })
1607     end
1608 end
1609
1610 def properties
1611     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
1612                              $main_window,
1613                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1614                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1615                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1616     
1617     source = $xmldoc.root.attributes['source']
1618     dest = $xmldoc.root.attributes['destination']
1619     theme = $xmldoc.root.attributes['theme']
1620     limit_sizes = $xmldoc.root.attributes['limit-sizes']
1621     if limit_sizes
1622         limit_sizes = limit_sizes.split(/,/)
1623     end
1624
1625     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
1626     tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
1627                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1628     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
1629                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1630     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
1631                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1632     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>')),
1633                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1634     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
1635                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1636     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $filename + '</i>')),
1637                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
1638
1639     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(tbl = Gtk::Table.new(0, 0, false))
1640     tbl.attach(Gtk::Label.new(utf8(_("Theme: "))),
1641                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1642     tbl.attach(theme_button = Gtk::Button.new(theme),
1643                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1644     tbl.attach(Gtk::Label.new(utf8(_("Sizes of images to generate: "))),
1645                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1646     tbl.attach(sizes = Gtk::HBox.new,
1647                1, 3, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1648
1649     theme_sizes = []
1650     recreate_theme_config = proc {
1651         theme_sizes.each { |e| sizes.remove(e[:widget]) }
1652         theme_sizes = []
1653         select_theme(theme_button.label, 'all')
1654         $images_size.each { |s|
1655             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
1656             if limit_sizes
1657                 if limit_sizes.include?(s['name'])
1658                     cb.active = true
1659                 end
1660             else
1661                 if !s['optional']
1662                     cb.active = true
1663                 end
1664             end
1665             tooltips = Gtk::Tooltips.new
1666             tooltips.set_tip(cb, utf8(s['description']), nil)
1667             theme_sizes << { :widget => cb, :value => s['name'] }
1668         }
1669         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
1670         tooltips = Gtk::Tooltips.new
1671         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
1672         if limit_sizes && limit_sizes.include?('original')
1673             cb.active = true
1674         end
1675         theme_sizes << { :widget => cb, :value => 'original' }
1676         sizes.show_all
1677     }
1678     recreate_theme_config.call
1679
1680     theme_button.signal_connect('clicked') {
1681         if newtheme = theme_choose(theme_button.label)
1682             limit_sizes = nil
1683             theme_button.label = newtheme
1684             recreate_theme_config.call
1685         end
1686     }
1687
1688     dialog.vbox.add(frame1)
1689     dialog.vbox.add(frame2)
1690     dialog.window_position = Gtk::Window::POS_MOUSE
1691     dialog.show_all
1692
1693     keepon = true
1694     ok = true
1695     while keepon
1696         dialog.run { |response|
1697             if response == Gtk::Dialog::RESPONSE_OK
1698                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
1699                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
1700                 else
1701                     keepon = false
1702                 end
1703             else
1704                 keepon = ok = false
1705             end
1706         }
1707     end
1708     save_theme = theme_button.label
1709     save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
1710     dialog.destroy
1711
1712     if ok && (save_theme != theme || save_limit_sizes != limit_sizes)
1713         save_current_file
1714         perform_in_background("booh-backend --use-config '#{$filename}' --for-gui " +
1715                                   "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{save_limit_sizes.join(',')} #{additional_booh_options}",
1716                               utf8(_("Please wait while scanning source directory...")),
1717                               { :closure_after => proc { open_file($filename) } })
1718     end
1719 end
1720
1721 def merge
1722     save_current_file
1723
1724     theme = $xmldoc.root.attributes['theme']
1725     limit_sizes = $xmldoc.root.attributes['limit-sizes']
1726     if limit_sizes
1727         limit_sizes = "--sizes #{limit_sizes}"
1728     end
1729     perform_in_background("booh-backend --merge-config '#{$filename}' --for-gui " +
1730                               "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
1731                           utf8(_("Please wait while scanning source directory...")),
1732                           { :closure_after => proc { open_file($filename) } })
1733 end
1734
1735 def merge_subalbums
1736     save_current_file
1737
1738     perform_in_background("booh-backend --merge-config-newdirs '#{$filename}' --for-gui " +
1739                               "--verbose-level #{$verbose_level} #{additional_booh_options}",
1740                           utf8(_("Please wait while scanning source directory...")),
1741                           { :closure_after => proc { open_file($filename) } })
1742 end
1743
1744 def save_as_do
1745     fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
1746                                     nil,
1747                                     Gtk::FileChooser::ACTION_SAVE,
1748                                     nil,
1749                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1750     fc.transient_for = $main_window
1751     fc.add_shortcut_folder(File.expand_path("~/.booh"))
1752     fc.set_current_folder(File.expand_path("~/.booh"))
1753     fc.filename = $filename
1754     if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1755         $filename = fc.filename
1756         save_current_file
1757     end
1758     fc.destroy
1759 end
1760
1761 def preferences
1762     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
1763                              $main_window,
1764                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1765                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1766                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1767
1768     dialog.vbox.add(notebook = Gtk::Notebook.new)
1769     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
1770     tbl.attach(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing"))),
1771                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1772     tbl.attach(smp_hbox = Gtk::HBox.new.add(smp_spin = Gtk::SpinButton.new(2, 16, 1)).add(Gtk::Label.new(utf8(_("processors")))).set_sensitive(false),
1773                1, 2, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1774     smp_check.signal_connect('toggled') {
1775         if smp_check.active?
1776             smp_hbox.sensitive = true
1777         else
1778             smp_hbox.sensitive = false
1779         end
1780     }
1781     if $config['mproc']
1782         smp_check.active = true
1783         smp_spin.value = $config['mproc'].to_i
1784     end
1785
1786     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
1787     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
1788                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1789     tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
1790                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1791
1792     dialog.vbox.show_all
1793     dialog.run { |response|
1794         if response == Gtk::Dialog::RESPONSE_OK
1795             if smp_check.active?
1796                 $config['mproc'] = smp_spin.value.to_i
1797             else
1798                 $config.delete('mproc')
1799             end
1800
1801             $config['convert-enhance'] = enhance_entry.text
1802         end
1803     }
1804     dialog.destroy
1805 end
1806
1807 def create_menu_and_toolbar
1808
1809     #- menu
1810     mb = Gtk::MenuBar.new
1811
1812     filemenu = Gtk::MenuItem.new(utf8(_("_File")))
1813     filesubmenu = Gtk::Menu.new
1814     filesubmenu.append(new       = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
1815     filesubmenu.append(open      = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
1816     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
1817     filesubmenu.append($save     = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
1818     filesubmenu.append($save_as  = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
1819     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
1820     tooltips = Gtk::Tooltips.new
1821     filesubmenu.append($merge_subalbums = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums"))).set_sensitive(false))
1822     $merge_subalbums.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
1823     tooltips.set_tip($merge_subalbums, utf8(_("Take into account new/removed subalbums (subdirectories) in the source directory")), nil)
1824     filesubmenu.append($merge    = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums and new/removed images/videos"))).set_sensitive(false))
1825     $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
1826     tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums in the source directory")), nil)
1827     filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
1828     $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
1829     tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
1830     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
1831     filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
1832     tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
1833     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
1834     filesubmenu.append(quit      = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
1835     filemenu.set_submenu(filesubmenu)
1836     mb.append(filemenu)
1837
1838     new.signal_connect('activate') { new_album }
1839     open.signal_connect('activate') { open_file_popup }
1840     $save.signal_connect('activate') { save_current_file }
1841     $save_as.signal_connect('activate') { save_as_do }
1842     $merge.signal_connect('activate') { merge }
1843     $merge_subalbums.signal_connect('activate') { merge_subalbums }
1844     $generate.signal_connect('activate') {
1845         save_current_file
1846         perform_in_background("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
1847                               utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
1848                               { :successmsg => utf8(_("Your web-album is now ready in directory `%s'.") % $xmldoc.root.attributes['destination']),
1849                                 :failuremsg => utf8(_("There was something wrong when generating the web-album, sorry.")) })
1850     }
1851     $properties.signal_connect('activate') { properties }
1852
1853     quit.signal_connect('activate') { try_quit }
1854
1855     editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
1856     editsubmenu = Gtk::Menu.new
1857     editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
1858     editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
1859     editsubmenu.append(           Gtk::SeparatorMenuItem.new)
1860     editsubmenu.append(prefs    = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
1861     editmenu.set_submenu(editsubmenu)
1862     mb.append(editmenu)
1863
1864     prefs.signal_connect('activate') { preferences }
1865
1866     helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
1867     helpsubmenu = Gtk::Menu.new
1868     helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
1869     helpmenu.set_submenu(helpsubmenu)
1870     mb.append(helpmenu)
1871
1872     about.signal_connect('activate') {
1873             show_popup($main_window, utf8(_("<span size='x-large' weight='bold'>Booh %s</span>
1874
1875 <i>``The Web-Album of choice for discriminating Linux users''</i>
1876
1877 Copyright (c) 2005 Guillaume Cottenceau") % $VERSION), { :centered => true, :pos_centered => true })
1878     }
1879
1880
1881     #- toolbar
1882     tb = Gtk::Toolbar.new
1883
1884     tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
1885     open.label = utf8(_("Open"))  #- to avoid missing gtk2 l10n catalogs
1886     open.menu = Gtk::Menu.new
1887     open.signal_connect('clicked') { open_file_popup }
1888     open.signal_connect('show-menu') {
1889         lastopens = Gtk::Menu.new
1890         j = 0
1891         if $config['last-opens']
1892             $config['last-opens'].reverse.each { |e|
1893                 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
1894                 item.signal_connect('activate') {
1895                     push_mousecursor_wait
1896                     msg = open_file(from_utf8(e))
1897                     pop_mousecursor
1898                     if msg
1899                         show_popup($main_window, msg)
1900                     end
1901                 }
1902                 j += 1
1903             }
1904             lastopens.show_all
1905         end
1906         open.menu = lastopens
1907     }
1908
1909     tb.insert(-1, Gtk::SeparatorToolItem.new)
1910
1911     tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
1912     $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
1913     $r90.label = utf8(_("Rotate"))
1914     tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
1915     $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
1916     $r270.label = utf8(_("Rotate"))
1917     tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
1918     $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1919     $enhance.label = utf8(_("Enhance"))
1920     tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
1921     $delete.label = utf8(_("Delete"))  #- to avoid missing gtk2 l10n catalogs
1922     tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
1923     nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
1924     nothing.label = utf8(_("None"))
1925
1926     tb.insert(-1, Gtk::SeparatorToolItem.new)
1927
1928     tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
1929     tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
1930
1931     perform_undo = Proc.new {
1932         $redo_tb.sensitive = $redo_mb.sensitive = true
1933         if not more_undoes = UndoHandler.undo($statusbar)
1934             $undo_tb.sensitive = $undo_mb.sensitive = false
1935         end
1936     }
1937     perform_redo = Proc.new {
1938         $undo_tb.sensitive = $undo_mb.sensitive = true
1939         if not more_redoes = UndoHandler.redo($statusbar)
1940             $redo_tb.sensitive = $redo_mb.sensitive = false
1941         end
1942     }
1943
1944     $undo_tb.signal_connect('clicked')  { perform_undo.call }
1945     $undo_mb.signal_connect('activate') { perform_undo.call }
1946     $redo_tb.signal_connect('clicked')  { perform_redo.call }
1947     $redo_mb.signal_connect('activate') { perform_redo.call }
1948
1949     one_click_explain_try = Proc.new {
1950         if !$config['one-click-explained']
1951             show_popup($main_window, utf8(_("<b>One-Click tools.</b>
1952
1953 You have just clicked on a One-Click tool. When such a tool is activated
1954 (<span foreground='darkblue'>Rotate clockwise</span>, <span foreground='darkblue'>Rotate counter-clockwise</span>, <span foreground='darkblue'>Enhance</span> or <span foreground='darkblue'>Delete</span>), clicking
1955 on a thumbnail will immediately apply the desired action.
1956
1957 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
1958 ")))
1959             $config['one-click-explained'] = true
1960         end
1961     }
1962
1963     $r90.signal_connect('toggled') {
1964         if $r90.active?
1965             set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
1966             one_click_explain_try.call
1967             $r270.active = false
1968             $enhance.active = false
1969             $delete.active = false
1970             nothing.sensitive = true
1971         else
1972             if !$r270.active? && !$enhance.active? && !$delete.active?
1973                 set_mousecursor_normal
1974                 nothing.sensitive = false
1975             else
1976                 nothing.sensitive = true
1977             end
1978         end
1979     }
1980     $r270.signal_connect('toggled') {
1981         if $r270.active?
1982             set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
1983             one_click_explain_try.call
1984             $r90.active = false
1985             $enhance.active = false
1986             $delete.active = false
1987             nothing.sensitive = true
1988         else
1989             if !$r90.active? && !$enhance.active? && !$delete.active?
1990                 set_mousecursor_normal
1991                 nothing.sensitive = false
1992             else
1993                 nothing.sensitive = true
1994             end
1995         end
1996     }
1997     $enhance.signal_connect('toggled') {
1998         if $enhance.active?
1999             set_mousecursor(Gdk::Cursor::SPRAYCAN)
2000             one_click_explain_try.call
2001             $r90.active = false
2002             $r270.active = false
2003             $delete.active = false
2004             nothing.sensitive = true
2005         else
2006             if !$r90.active? && !$r270.active? && !$delete.active?
2007                 set_mousecursor_normal
2008                 nothing.sensitive = false
2009             else
2010                 nothing.sensitive = true
2011             end
2012         end
2013     }
2014     $delete.signal_connect('toggled') {
2015         if $delete.active?
2016             set_mousecursor(Gdk::Cursor::PIRATE)
2017             one_click_explain_try.call
2018             $r90.active = false
2019             $r270.active = false
2020             $enhance.active = false
2021             nothing.sensitive = true
2022         else
2023             if !$r90.active? && !$r270.active? && !$enhance.active?
2024                 set_mousecursor_normal
2025                 nothing.sensitive = false
2026             else
2027                 nothing.sensitive = true
2028             end
2029         end
2030     }
2031     nothing.signal_connect('clicked') {
2032         $r90.active = $r270.active = $enhance.active = $delete.active = false
2033         set_mousecursor_normal
2034     }
2035
2036     return [ mb, tb ]
2037 end
2038
2039 def create_main_window
2040
2041     mb, tb = create_menu_and_toolbar
2042
2043 #    open_file('/home/gc/booh/foo')
2044
2045     $albums_tv = Gtk::TreeView.new
2046     $albums_tv.set_size_request(120, -1)
2047     renderer = Gtk::CellRendererText.new
2048     column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 })
2049     $albums_tv.append_column(column)
2050     $albums_tv.set_headers_visible(false)
2051     $albums_tv.selection.signal_connect('changed') { |w|
2052         push_mousecursor_wait
2053         save_changes
2054         iter = w.selected
2055         if !iter
2056             msg 3, "no selection"
2057         else
2058             $current_path = $albums_ts.get_value(iter, 1)
2059             change_dir
2060         end
2061         pop_mousecursor
2062     }
2063     $albums_ts = Gtk::TreeStore.new(String, String)
2064     $albums_tv.set_model($albums_ts)
2065     $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
2066
2067     albums_sw = Gtk::ScrolledWindow.new(nil, nil)
2068     albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
2069     albums_sw.add_with_viewport($albums_tv)
2070
2071     $notebook = Gtk::Notebook.new
2072     create_subalbums_page
2073     $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
2074     create_auto_table
2075     $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
2076     $notebook.show_all
2077     $notebook.signal_connect('switch-page') { |w, page, num|
2078         if num == 0
2079             $delete.active = false
2080             $delete.sensitive = false
2081         else
2082             $delete.sensitive = true
2083         end
2084     }
2085
2086     paned = Gtk::HPaned.new
2087     paned.pack1(albums_sw, false, false)
2088     paned.pack2($notebook, true, true)
2089
2090     main_vbox = Gtk::VBox.new(false, 0)
2091     main_vbox.pack_start(mb, false, false)
2092     main_vbox.pack_start(tb, false, false)
2093     main_vbox.pack_start(paned, true, true)
2094     main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
2095
2096     $main_window = Gtk::Window.new
2097     $main_window.add(main_vbox)
2098     $main_window.signal_connect('delete-event') {
2099         try_quit
2100     }
2101
2102     #- read/save size and position of window
2103     if $config['pos-x'] && $config['pos-y']
2104         $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
2105     else
2106         $main_window.window_position = Gtk::Window::POS_CENTER
2107     end
2108     msg 3, "size: #{$config['width']}x#{$config['height']}"
2109     $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
2110     $main_window.signal_connect('configure-event') {
2111         msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
2112         x, y = $main_window.window.root_origin
2113         width, height = $main_window.window.size
2114         $config['pos-x'] = x
2115         $config['pos-y'] = y
2116         $config['width'] = width
2117         $config['height'] = height
2118         false
2119     }
2120
2121     $statusbar.push(0, utf8(_("Ready.")))
2122     $main_window.show_all
2123 end
2124
2125 Thread.abort_on_exception = true
2126 handle_options
2127 read_config
2128
2129 Gtk.init
2130 create_main_window
2131 if ARGV[0]
2132     open_file(ARGV[0])
2133 end
2134 Gtk.main
2135
2136 write_config