5 # A.k.a `Best web-album Of the world, Or your money back, Humerus'.
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.
13 # Copyright (c) 2004 Guillaume Cottenceau <gc3 at bluewin.ch>
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
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.
26 require 'booh/GtkAutoTable'
30 bindtextdomain("booh")
32 require 'rexml/document'
35 require 'booh/booh-lib'
41 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
43 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
47 puts _("Usage: %s [OPTION]...") % File.basename($0)
49 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
54 parser = GetoptLong.new
55 parser.set_options(*$options.collect { |ary| ary[0..2] })
57 parser.each_option do |name, arg|
63 when '--verbose-level'
64 $verbose_level = arg.to_i
77 $config_file = File.expand_path('~/.booh-gui-rc')
78 if File.readable?($config_file)
79 $xmldoc = REXML::Document.new(File.new($config_file))
80 $xmldoc.root.elements.each { |element|
81 txt = element.get_text
82 $config[element.name] = txt ? txt.value : nil
85 if !FileTest.directory?(File.expand_path('~/.booh-gui-files'))
86 system("mkdir ~/.booh-gui-files")
91 ios = File.open($config_file, "w")
92 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
93 $xmldoc << XMLDecl.new( XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET )
94 $config.each_pair { |key, value|
95 elem = $xmldoc.root.add_element key
96 elem.add_text value.to_s
102 def set_mousecursor(widget, what)
106 widget.window.set_cursor(Gdk::Cursor.new(what))
108 def set_mousecursor_wait(widget)
109 set_mousecursor(widget, Gdk::Cursor::WATCH)
110 Gtk.main_iteration while Gtk.events_pending?
112 def set_mousecursor_normal(widget)
113 set_mousecursor(widget, Gdk::Cursor::LEFT_PTR)
117 source = $xmldoc.root.attributes['source']
118 dest = $xmldoc.root.attributes['destination']
119 return make_dest_filename(from_utf8($current_path).sub(/^#{Regexp.quote(source)}/, dest))
122 def build_full_dest_filename(filename)
123 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
126 def view_element(filename)
127 if entry2type(filename) == 'video'
128 system("mplayer #{$current_path + '/' + filename}")
134 msg 3, "filename: #{filename}"
135 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
136 #- typically this file won't exist in case of videos; try with the largest thumbnail around
137 if !File.exists?(dest_img)
138 if entry2type(filename) == 'video'
139 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
140 if not alternatives.empty?
141 dest_img = alternatives[-1]
144 set_mousecursor_wait($main_window)
145 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
146 set_mousecursor_normal($main_window)
147 if !File.exists?(dest_img)
148 msg 2, _("Could not generate fullscreen thumbnail!")
153 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)))
155 tooltips = Gtk::Tooltips.new
156 tooltips.set_tip(evt, utf8(File.basename(filename).gsub(/\.jpg/, '')), nil)
158 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
159 b.signal_connect('clicked') { w.destroy }
162 vb.pack_start(evt, false, false)
163 vb.pack_end(bottom, false, false)
166 w.signal_connect('delete-event') { w.destroy }
167 w.window_position = Gtk::Window::POS_CENTER
171 def create_editzone(scrolledwindow)
172 frame = Gtk::Frame.new
173 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
174 frame.set_shadow_type(Gtk::SHADOW_IN)
175 textview.signal_connect('key-press-event') { |w, event|
176 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
177 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
178 scrolledwindow.signal_emit('key-press-event', event)
182 textview.signal_connect('focus-in-event') { |w, event|
183 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
187 return [ frame, textview ]
190 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
191 #- update rotate attribute
192 xmlelem.add_attribute("#{attributes_prefix}rotate", current_angle = ( xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle ) % 360)
194 if !$rotated_pixbufs[thumbnail_img]
195 $rotated_pixbufs[thumbnail_img] = { :orig => img.pixbuf, :angle_to_orig => angle % 360 }
197 $rotated_pixbufs[thumbnail_img][:angle_to_orig] = ( $rotated_pixbufs[thumbnail_img][:angle_to_orig] + angle ) % 360
199 msg 3, "angle: #{angle}, angle to orig: #{$rotated_pixbufs[thumbnail_img][:angle_to_orig]}"
201 #- rotate shown thumbnail
202 pixbuf = rotate_pixbuf($rotated_pixbufs[thumbnail_img][:orig], $rotated_pixbufs[thumbnail_img][:angle_to_orig])
203 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
204 if pixbuf.height > desired_y
205 img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y,
206 Gdk::Pixbuf::INTERP_BILINEAR)
207 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
208 img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width),
209 Gdk::Pixbuf::INTERP_BILINEAR)
211 img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf
215 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
218 gen_real_thumbnail = Proc.new {
220 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => thumbnail_img, 'size' => $default_size['thumbnails'] } ])
221 img.set(thumbnail_img)
222 $rotated_pixbufs[thumbnail_img] = { :orig => img.pixbuf, :angle_to_orig => 0 }
224 #- cleanup temp for videos
225 system("rm -f #{current_dest_dir}/screenshot.jpg00000*")
230 frame1 = Gtk::Frame.new
231 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
232 if !$rotated_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
233 frame1.add(img = Gtk::Image.new)
234 gen_real_thumbnail.call
236 frame1.add(img = Gtk::Image.new($rotated_pixbufs[thumbnail_img] ? $rotated_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
238 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
240 tooltips = Gtk::Tooltips.new
241 tipname = File.basename(thumbnail_img).gsub(/-\d+x\d+\.jpg/, '')
242 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(File.size(from_utf8("#{$current_path}/#{filename}"))/1024)]) : tipname), nil)
244 frame2, textview = create_editzone($autotable_sw)
245 textview.buffer.text = utf8(caption)
246 textview.set_justification(Gtk::Justification::CENTER)
248 vbox = Gtk::VBox.new(false, 5)
249 vbox.pack_start(evtbox, false, false)
250 vbox.pack_start(frame2, false, false)
251 autotable.append(vbox, filename)
253 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
254 $vbox2textview[vbox] = textview
256 #- to be able to find widgets by name
257 $name2widgets[filename] = { :textview => textview }
259 rotate_and_cleanup = Proc.new { |angle|
260 rotate(angle, thumbnail_img, img, $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
262 #- remove out of sync images
263 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
264 for sizeobj in $images_size
265 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
269 color_swap_and_cleanup = Proc.new {
270 xmlelem = $xmldir.elements["[@filename='#{filename}']"]
271 if xmlelem.attributes['color-swap']
272 xmlelem.delete_attribute('color-swap')
274 xmlelem.add_attribute('color-swap', '1')
276 system("rm -f '#{thumbnail_img}'")
278 #- remove out of sync images
279 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
280 for sizeobj in $images_size
281 system("rm -f #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
284 gen_real_thumbnail.call
288 after = autotable.get_next_widget(vbox)
290 after = autotable.get_previous_widget(vbox)
292 autotable.remove(vbox)
294 $vbox2textview[after].grab_focus
298 textview.signal_connect('key-press-event') { |w, event|
300 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
301 $autotable_sw.signal_emit('key-press-event', event)
304 x, y = autotable.get_current_pos(vbox)
305 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
306 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
307 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
308 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
310 $vbox2textview[autotable.get_widget_at_pos(x, y - 1)].grab_focus
313 autotable.move_up(vbox)
314 textview.grab_focus #- because if moving, focus is stolen
317 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
319 $vbox2textview[autotable.get_widget_at_pos(x, y + 1)].grab_focus
322 autotable.move_down(vbox)
323 textview.grab_focus #- because if moving, focus is stolen
326 if event.keyval == Gdk::Keyval::GDK_Left
327 previous = autotable.get_previous_widget(vbox)
328 if previous && autotable.get_current_pos(previous)[0] < x
330 $vbox2textview[previous].grab_focus
333 autotable.move_left(vbox)
334 textview.grab_focus #- because if moving, focus is stolen
338 rotate_and_cleanup.call(-90)
341 if event.keyval == Gdk::Keyval::GDK_Right
342 next_ = autotable.get_next_widget(vbox)
343 if next_ && autotable.get_current_pos(next_)[0] > x
345 $vbox2textview[next_].grab_focus
348 autotable.move_right(vbox)
349 textview.grab_focus #- because if moving, focus is stolen
353 rotate_and_cleanup.call(90)
356 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
359 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
360 view_element(filename)
364 !propagate #- propagate if needed
367 evtbox.signal_connect('button-press-event') { |w, event|
369 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
373 rotate_and_cleanup.call(90)
375 rotate_and_cleanup.call(270)
380 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
382 menu.append( r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
383 menu.append( r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
384 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
385 r90.signal_connect('activate') { rotate_and_cleanup.call(90) }
386 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
387 r270.signal_connect('activate') { rotate_and_cleanup.call(270) }
389 menu.append( Gtk::SeparatorMenuItem.new)
390 menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
391 menu.append( flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
392 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
393 color_swap.signal_connect('activate') { color_swap_and_cleanup.call }
394 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
395 flip.signal_connect('activate') { rotate_and_cleanup.call(180) }
397 menu.append( Gtk::SeparatorMenuItem.new)
398 menu.append(delete = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
399 delete.signal_connect('activate') { delete.call }
401 menu.popup(nil, nil, event.button, event.time)
403 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
404 view_element(filename)
406 retval = false #- propagate
411 vbox.signal_connect('button-press-event') { |w, event|
412 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
413 $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
418 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
419 if $gesture_press && $gesture_press[:filename] == filename
420 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
421 angle = x-$gesture_press[:x] > 0 ? 90 : -90
422 msg 3, "gesture rotate: #{angle}"
423 rotate_and_cleanup.call(90)
432 def create_auto_table
434 $autotable = Gtk::AutoTable.new(5)
436 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
437 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
438 $autotable_sw.add_with_viewport($autotable)
441 def create_subalbums_page
443 subalbums_hb = Gtk::HBox.new
444 # subalbums_hb.pack_start(Gtk::Label.new, true, true)
445 $subalbums_vb = Gtk::VBox.new(false, 5)
446 subalbums_hb.pack_start($subalbums_vb, false, false)
447 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
448 $subalbums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
449 $subalbums_sw.add_with_viewport(subalbums_hb)
455 ios = File.open($filename, "w")
456 $xmldoc.write(ios, 0)
462 def show_popup(parent, msg)
463 dialog = Gtk::Dialog.new
464 dialog.title = utf8(_("Booh message"))
468 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
470 dialog.transient_for = parent
471 dialog.set_default_size(200, 120)
472 dialog.window_position = Gtk::Window::POS_CENTER_ON_PARENT
484 if $xmldir.elements['dir']
485 $xmldir.add_attribute('subdirs-caption', $subalbums_title.buffer.text)
486 $xmldir.elements.each('dir') { |element|
487 path = element.attributes['path']
488 if element.attributes['subdirs-caption']
489 element.add_attribute('subdirs-caption', $subalbums_edits[path][:editzone].buffer.text)
490 element.add_attribute('subdirs-captionfile', $subalbums_edits[path][:captionfile])
492 element.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
493 element.add_attribute('thumbnails-captionfile', $subalbums_edits[path][:captionfile])
496 if $xmldir.attributes['thumbnails-caption']
497 path = $xmldir.attributes['path']
498 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
502 #- remove and reinsert elements to reflect new ordering
505 $xmldir.elements.each { |element|
506 if element.name == 'image' || element.name == 'video'
507 save_types[element.attributes['filename']] = element.name
508 save_attributes[element.attributes['filename']] = element.attributes
512 $autotable.current_order.each { |path|
513 chld = $xmldir.add_element save_types[path], save_attributes[path]
514 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
523 $subalbums_vb.children.each { |chld|
524 $subalbums_vb.remove(chld)
526 $subalbums = Gtk::Table.new(0, 0, true)
527 current_y_sub_albums = 0
529 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
530 $subalbums_edits = {}
532 find_infotype = Proc.new { |xmldir|
533 xmldir == $xmldir ? 'thumbnails' : find_subalbum_info_type(xmldir)
536 add_subalbum = Proc.new { |xmldir|
538 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
539 caption = xmldir.attributes['thumbnails-caption']
540 captionfile, dummy = find_subalbum_caption_info(xmldir)
542 thumbnail_file = "#{current_dest_dir}/thumbnails-#{from_utf8(File.basename(xmldir.attributes['path']))}.jpg"
543 captionfile, caption = find_subalbum_caption_info(xmldir)
545 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
547 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
549 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
552 gen_real_thumbnail = Proc.new {
554 system("rm -f '#{thumbnail_file}'")
555 gen_thumbnails_subdir(from_utf8(captionfile), xmldir, false,
556 [ { 'filename' => thumbnail_file, 'size' => $albums_thumbnail_size } ], find_infotype.call(xmldir))
557 img.set(thumbnail_file)
558 $rotated_pixbufs[thumbnail_file] = { :orig => img.pixbuf, :angle_to_orig => 0 }
559 if entry2type(captionfile) == 'video'
560 system("rm -f #{current_dest_dir}/screenshot.jpg00000*")
565 if !$rotated_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
566 f.add(img = Gtk::Image.new)
567 gen_real_thumbnail.call
569 f.add(img = Gtk::Image.new($rotated_pixbufs[thumbnail_file] ? $rotated_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
571 hbox.pack_end(evtbox = Gtk::EventBox.new.add(f), false, false)
572 $subalbums.attach(hbox,
573 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
575 rotate_and_cleanup = Proc.new { |angle|
576 rotate(angle, thumbnail_file, img, xmldir, find_infotype.call(xmldir) + '-',
577 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
578 system("rm -f '#{thumbnail_file}'")
581 color_swap_and_cleanup = Proc.new {
582 infotype = find_infotype.call(xmldir)
583 if xmldir.attributes["#{infotype}-color-swap"]
584 xmldir.delete_attribute("#{infotype}-color-swap")
586 xmldir.add_attribute("#{infotype}-color-swap", '1')
588 gen_real_thumbnail.call
591 evtbox.signal_connect('button-press-event') { |w, event|
592 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
594 rotate_and_cleanup.call(90)
597 rotate_and_cleanup.call(270)
600 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
602 menu.append( r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
603 menu.append( r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
604 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
605 r90.signal_connect('activate') { rotate_and_cleanup.call(90) }
606 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
607 r270.signal_connect('activate') { rotate_and_cleanup.call(270) }
608 if entry2type(captionfile) == 'video'
609 menu.append( Gtk::SeparatorMenuItem.new)
610 menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
611 menu.append( flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
612 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
613 color_swap.signal_connect('activate') { color_swap_and_cleanup.call }
614 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
615 flip.signal_connect('activate') { rotate_and_cleanup.call(180) }
618 menu.popup(nil, nil, event.button, event.time)
620 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
621 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
623 Gtk::FileChooser::ACTION_OPEN,
625 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
626 fc.set_current_folder(xmldir.attributes['path'])
627 fc.transient_for = $main_window
628 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))
629 f.add(preview_img = Gtk::Image.new)
631 fc.signal_connect('update-preview') { |w|
633 if fc.preview_filename
634 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
635 fc.preview_widget_active = true
637 rescue Gdk::PixbufError
638 fc.preview_widget_active = false
641 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
642 msg 3, "new captionfile is: #{fc.filename}"
643 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = utf8(fc.filename)
644 $rotated_pixbufs.delete(thumbnail_file)
645 infotype = find_infotype.call(xmldir)
646 xmldir.delete_attribute("#{infotype}-rotate")
647 xmldir.delete_attribute("#{infotype}-color-swap")
648 gen_real_thumbnail.call
654 evtbox.signal_connect('button-press-event') { |w, event|
655 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
659 evtbox.signal_connect('button-release-event') { |w, event|
660 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
661 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
662 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
663 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
664 msg 3, "gesture rotate: #{angle}"
665 rotate_and_cleanup.call(angle)
671 frame, textview = create_editzone($subalbums_sw)
672 textview.buffer.text = caption
673 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
674 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
676 $subalbums_edits[xmldir.attributes['path']] = { :editzone => textview, :captionfile => captionfile }
677 current_y_sub_albums += 1
680 if $xmldir.elements['dir']
682 frame, $subalbums_title = create_editzone($subalbums_sw)
683 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
684 $subalbums_title.set_justification(Gtk::Justification::CENTER)
685 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
686 #- this album image/caption
687 if $xmldir.attributes['thumbnails-caption']
688 add_subalbum.call($xmldir)
691 $xmldir.elements.each { |element|
692 if element.name == 'image' || element.name == 'video'
693 #- element (image or video) of this album
694 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
695 msg 3, "dest_img: #{dest_img}"
696 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
698 if element.name == 'dir'
699 #- sub-album image/caption
700 add_subalbum.call(element)
703 $subalbums_vb.add($subalbums)
704 $subalbums_vb.show_all
706 if !$xmldir.elements['image'] && !$xmldir.elements['video']
707 $notebook.get_tab_label($autotable_sw).sensitive = false
708 $notebook.set_page(0)
710 $notebook.get_tab_label($autotable_sw).sensitive = true
713 if !$xmldir.elements['dir']
714 $notebook.get_tab_label($subalbums_sw).sensitive = false
715 $notebook.set_page(1)
717 $notebook.get_tab_label($subalbums_sw).sensitive = true
721 def open_file(filename)
723 $current_path = nil #- invalidate
724 $rotated_pixbufs = {}
727 $xmldoc = REXML::Document.new File.new(filename)
732 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
733 return utf8(_("Not a booh file!"))
736 if !source = $xmldoc.root.attributes['source']
737 return utf8(_("Corrupted booh file..."))
740 if !dest = $xmldoc.root.attributes['destination']
741 return utf8(_("Corrupted booh file..."))
744 if !theme = $xmldoc.root.attributes['theme']
745 return utf8(_("Corrupted booh file..."))
750 $default_size = $images_size.detect { |sizeobj| sizeobj['default'] }
751 $default_size['thumbnails'] =~ /(.*)x(.*)/
752 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
753 $albums_thumbnail_size =~ /(.*)x(.*)/
754 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
756 msg 3, "source: #{source}"
758 xmldir = $xmldoc.elements["//dir[@path='#{source}']"]
760 return utf8(_("Corrupted booh file..."))
765 append_dir_elem = Proc.new { |parent_iter, location|
766 child_iter = $albums_ts.append(parent_iter)
767 child_iter[0] = File.basename(location)
768 child_iter[1] = location
769 msg 3, "puttin location: #{location}"
770 $xmldoc.elements.each("//dir[@path='#{location}']/dir") { |elem|
771 append_dir_elem.call(child_iter, elem.attributes['path'])
774 append_dir_elem.call(nil, source)
776 $albums_tv.expand_all
777 $albums_tv.selection.select_iter($albums_ts.iter_first)
783 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
785 Gtk::FileChooser::ACTION_OPEN,
787 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
788 fc.add_shortcut_folder(File.expand_path("~/.booh-gui-files"))
789 fc.set_current_folder(File.expand_path("~/.booh-gui-files"))
790 fc.transient_for = $main_window
793 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
794 set_mousecursor_wait(fc)
795 msg = open_file(fc.filename)
796 set_mousecursor_normal(fc)
811 mb = Gtk::MenuBar.new
813 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
814 filesubmenu = Gtk::Menu.new
815 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
816 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
817 filesubmenu.append( Gtk::SeparatorMenuItem.new)
818 filesubmenu.append(save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE))
819 filesubmenu.append(save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS))
820 filesubmenu.append( Gtk::SeparatorMenuItem.new)
821 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
822 filemenu.set_submenu(filesubmenu)
825 open.signal_connect('activate') { open_file_popup }
826 quit.signal_connect('activate') { try_quit }
832 tb = Gtk::Toolbar.new
834 tb.insert(-1, open = Gtk::ToolButton.new(Gtk::Stock::OPEN))
835 open.label = utf8(_("Open"))
837 tb.insert(-1, Gtk::SeparatorToolItem.new)
839 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
840 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
841 $r90.label = utf8(_("Rotate"))
842 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
843 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
844 $r270.label = utf8(_("Rotate"))
845 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
846 $delete.label = utf8(_("Delete"))
847 tb.insert(-1, nothing = Gtk::ToolButton.new(''))
848 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
849 nothing.label = utf8(_("None"))
851 open.signal_connect('clicked') { open_file_popup }
853 one_click_explain_try = Proc.new {
854 if !$config['one-click-explained']
855 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
857 You have just clicked on a One-Click tool. When such a tool is activated
858 (<span foreground=\"darkblue\">Rotate clockwise</span>, <span foreground=\"darkblue\">Rotate counter-clockwise</span>, or <span foreground=\"darkblue\">Delete</span>), clicking on a
859 thumbnail will immediately apply the desired action.
861 Click the <span foreground=\"darkblue\">None</span> icon when you're finished with One-Click tools.
863 $config['one-click-explained'] = true
867 $r90.signal_connect('toggled') {
869 set_mousecursor($autotable, Gdk::Cursor::SB_RIGHT_ARROW)
870 set_mousecursor($subalbums, Gdk::Cursor::SB_RIGHT_ARROW)
871 one_click_explain_try.call
873 $delete.active = false
875 if !$r270.active? && !$delete.active?
876 set_mousecursor_normal($autotable)
877 set_mousecursor_normal($subalbums)
881 $r270.signal_connect('toggled') {
883 set_mousecursor($autotable, Gdk::Cursor::SB_LEFT_ARROW)
884 set_mousecursor($subalbums, Gdk::Cursor::SB_LEFT_ARROW)
885 one_click_explain_try.call
887 $delete.active = false
889 if !$r90.active? && !$delete.active?
890 set_mousecursor_normal($autotable)
891 set_mousecursor_normal($subalbums)
895 $delete.signal_connect('toggled') {
897 set_mousecursor($autotable, Gdk::Cursor::PIRATE)
898 one_click_explain_try.call
902 if !$r90.active? && !$r270.active?
903 set_mousecursor_normal($autotable)
904 set_mousecursor_normal($subalbums)
908 nothing.signal_connect('clicked') {
909 $r90.active = $r270.active = $delete.active = false
910 set_mousecursor_normal($autotable)
911 set_mousecursor_normal($subalbums)
917 def create_main_window
923 # open_file('/home/gc/booh/foo')
925 $albums_tv = Gtk::TreeView.new
926 $albums_tv.set_size_request(120, -1)
927 renderer = Gtk::CellRendererText.new
928 column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 })
929 $albums_tv.append_column(column)
930 $albums_tv.set_headers_visible(false)
931 $albums_tv.selection.signal_connect('changed') { |w|
932 set_mousecursor_wait($main_window)
936 msg 3, "no selection"
938 $current_path = $albums_ts.get_value(iter, 1)
941 set_mousecursor_normal($main_window)
943 $albums_ts = Gtk::TreeStore.new(String, String)
944 $albums_tv.set_model($albums_ts)
945 $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
947 $notebook = Gtk::Notebook.new
948 create_subalbums_page
949 $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
951 $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
953 $notebook.signal_connect('switch-page') { |w, page, num|
955 $delete.active = false
956 $delete.sensitive = false
958 $delete.sensitive = true
962 paned = Gtk::HPaned.new
963 paned.pack1($albums_tv, false, false)
964 paned.pack2($notebook, true, true)
966 main_vbox = Gtk::VBox.new(false, 0)
967 main_vbox.pack_start(mb, false, false)
968 main_vbox.pack_start(tb, false, false)
969 main_vbox.pack_start(paned, true, true)
970 main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
972 $main_window = Gtk::Window.new
973 $main_window.add(main_vbox)
974 $main_window.signal_connect('delete-event') {
978 #- read/save size and position of window
979 if $config['pos-x'] && $config['pos-y']
980 $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
982 $main_window.window_position = Gtk::Window::POS_CENTER
984 msg 3, "size: #{$config['width']}x#{$config['height']}"
985 $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
986 $main_window.signal_connect('configure-event') {
987 msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
988 x, y = $main_window.window.root_origin
989 width, height = $main_window.window.size
992 $config['width'] = width
993 $config['height'] = height
997 $statusbar.push(0, utf8(_("Ready.")))
998 $main_window.show_all