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'
37 require 'booh/UndoHandler'
42 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
44 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
48 puts _("Usage: %s [OPTION]...") % File.basename($0)
50 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
55 parser = GetoptLong.new
56 parser.set_options(*$options.collect { |ary| ary[0..2] })
58 parser.each_option do |name, arg|
64 when '--verbose-level'
65 $verbose_level = arg.to_i
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
84 if txt.value =~ /~~~/ || element.name == 'last-opens'
85 $config[element.name] = txt.value.split(/~~~/)
87 $config[element.name] = txt.value
90 $config[element.name] = {}
93 $config[element.name][chld.name] = txt ? txt.value : nil
98 if !FileTest.directory?(File.expand_path('~/.booh-gui-files'))
99 system("mkdir ~/.booh-gui-files")
104 if $config['last-opens'].size > 5
105 $config['last-opens'] = $config['last-opens'][-5, 5]
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
114 $config[key].each_pair { |subkey, subvalue|
115 subelem = elem.add_element subkey
116 subelem.add_text subvalue.to_s
118 elsif value.is_a? Array
119 elem.add_text value.join('~~~')
121 elem.add_text value.to_s
124 $xmldoc.write(ios, 0)
128 def set_mousecursor(what, *widget)
129 if widget[0] && widget[0].window
130 widget[0].window.set_cursor(Gdk::Cursor.new(what))
132 if $main_window.window
133 $main_window.window.set_cursor(Gdk::Cursor.new(what))
135 $current_cursor = what
137 def set_mousecursor_wait(*widget)
138 set_mousecursor(Gdk::Cursor::WATCH, *widget)
139 Gtk.main_iteration while Gtk.events_pending?
141 def set_mousecursor_normal(*widget)
142 set_mousecursor(Gdk::Cursor::LEFT_PTR, *widget)
144 def push_mousecursor_wait(*widget)
145 $save_cursor = $current_cursor
146 set_mousecursor_wait(*widget)
148 def pop_mousecursor(*widget)
149 set_mousecursor($save_cursor || Gdk::Cursor::LEFT_PTR, *widget)
153 source = $xmldoc.root.attributes['source']
154 dest = $xmldoc.root.attributes['destination']
155 return make_dest_filename(from_utf8($current_path).sub(/^#{Regexp.quote(source)}/, dest))
158 def build_full_dest_filename(filename)
159 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
162 def save_undo(closure, *params)
163 UndoHandler.save_undo(closure, [ *params ])
164 $undo_tb.sensitive = $undo_mb.sensitive = true
167 def view_element(filename)
168 if entry2type(filename) == 'video'
169 system("mplayer #{$current_path + '/' + filename}")
175 msg 3, "filename: #{filename}"
176 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
177 #- typically this file won't exist in case of videos; try with the largest thumbnail around
178 if !File.exists?(dest_img)
179 if entry2type(filename) == 'video'
180 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
181 if not alternatives.empty?
182 dest_img = alternatives[-1]
185 push_mousecursor_wait
186 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
188 if !File.exists?(dest_img)
189 msg 2, _("Could not generate fullscreen thumbnail!")
194 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)))
196 tooltips = Gtk::Tooltips.new
197 tooltips.set_tip(evt, utf8(File.basename(filename).gsub(/\.jpg/, '')), nil)
199 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
200 b.signal_connect('clicked') { w.destroy }
203 vb.pack_start(evt, false, false)
204 vb.pack_end(bottom, false, false)
207 w.signal_connect('delete-event') { w.destroy }
208 w.window_position = Gtk::Window::POS_CENTER
212 def create_editzone(scrolledwindow, pagenum)
213 frame = Gtk::Frame.new
214 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
215 frame.set_shadow_type(Gtk::SHADOW_IN)
216 textview.signal_connect('key-press-event') { |w, event|
217 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
218 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
219 scrolledwindow.signal_emit('key-press-event', event)
223 textview.signal_connect('focus-in-event') { |w, event|
224 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
228 candidate_undo_text = nil
229 textview.signal_connect('focus-in-event') { |w, event|
230 candidate_undo_text = textview.buffer.text
233 textview.signal_connect('key-release-event') { |w, event|
234 if candidate_undo_text && candidate_undo_text != textview.buffer.text
235 save_undo(Proc.new { |text|
236 save_text = textview.buffer.text
237 textview.buffer.text = text
239 $notebook.set_page(pagenum)
241 textview.buffer.text = save_text
243 $notebook.set_page(pagenum)
245 }, candidate_undo_text)
246 candidate_undo_text = nil
251 return [ frame, textview ]
254 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
255 #- update rotate attribute
256 xmlelem.add_attribute("#{attributes_prefix}rotate", current_angle = ( xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle ) % 360)
258 if !$rotated_pixbufs[thumbnail_img]
259 $rotated_pixbufs[thumbnail_img] = { :orig => img.pixbuf, :angle_to_orig => angle % 360 }
261 $rotated_pixbufs[thumbnail_img][:angle_to_orig] = ( $rotated_pixbufs[thumbnail_img][:angle_to_orig] + angle ) % 360
263 msg 3, "angle: #{angle}, angle to orig: #{$rotated_pixbufs[thumbnail_img][:angle_to_orig]}"
265 #- rotate shown thumbnail
266 pixbuf = rotate_pixbuf($rotated_pixbufs[thumbnail_img][:orig], $rotated_pixbufs[thumbnail_img][:angle_to_orig])
267 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
268 if pixbuf.height > desired_y
269 img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y,
270 Gdk::Pixbuf::INTERP_BILINEAR)
271 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
272 img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width),
273 Gdk::Pixbuf::INTERP_BILINEAR)
275 img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf
279 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
282 frame1 = Gtk::Frame.new
283 gen_real_thumbnail = Proc.new {
285 push_mousecursor_wait
286 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => thumbnail_img, 'size' => $default_size['thumbnails'] } ])
287 img.set(thumbnail_img)
288 $rotated_pixbufs[thumbnail_img] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
290 #- cleanup temp for videos
291 system("rm -f #{current_dest_dir}/screenshot.jpg00000*")
297 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
298 if !$rotated_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
299 frame1.add(img = Gtk::Image.new)
300 gen_real_thumbnail.call
302 frame1.add(img = Gtk::Image.new($rotated_pixbufs[thumbnail_img] ? $rotated_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
304 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
306 tooltips = Gtk::Tooltips.new
307 tipname = File.basename(thumbnail_img).gsub(/-\d+x\d+\.jpg/, '')
308 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(File.size(from_utf8("#{$current_path}/#{filename}"))/1024)]) : tipname), nil)
310 frame2, textview = create_editzone($autotable_sw, 1)
311 textview.buffer.text = utf8(caption)
312 textview.set_justification(Gtk::Justification::CENTER)
314 vbox = Gtk::VBox.new(false, 5)
315 vbox.pack_start(evtbox, false, false)
316 vbox.pack_start(frame2, false, false)
317 autotable.append(vbox, filename)
319 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
320 $vbox2textview[vbox] = textview
322 #- to be able to find widgets by name
323 $name2widgets[filename] = { :textview => textview }
325 cleanup_all_thumbnails = Proc.new {
326 #- remove out of sync images
327 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
328 for sizeobj in $images_size
329 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
334 rotate_and_cleanup = Proc.new { |angle|
335 perform_rotate_and_cleanup = Proc.new { |angle|
336 rotate(angle, thumbnail_img, img, $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
338 perform_rotate_and_cleanup.call(angle)
340 cleanup_all_thumbnails.call
342 save_undo(Proc.new { |angle|
343 perform_rotate_and_cleanup.call(angle)
344 $notebook.set_page(1)
346 perform_rotate_and_cleanup.call(-angle)
347 $notebook.set_page(1)
352 color_swap_and_cleanup = Proc.new {
353 perform_color_swap_and_cleanup = Proc.new {
354 xmlelem = $xmldir.elements["[@filename='#{filename}']"]
355 if xmlelem.attributes['color-swap']
356 xmlelem.delete_attribute('color-swap')
358 xmlelem.add_attribute('color-swap', '1')
360 system("rm -f '#{thumbnail_img}'")
361 gen_real_thumbnail.call
364 cleanup_all_thumbnails.call
365 perform_color_swap_and_cleanup.call
368 perform_color_swap_and_cleanup.call
369 $notebook.set_page(1)
371 perform_color_swap_and_cleanup
372 $notebook.set_page(1)
377 enhance_and_cleanup = Proc.new {
378 perform_enhance_and_cleanup = Proc.new {
379 xmlelem = $xmldir.elements["[@filename='#{filename}']"]
380 if xmlelem.attributes['enhance']
381 xmlelem.delete_attribute('enhance')
383 xmlelem.add_attribute('enhance', '1')
385 system("rm -f '#{thumbnail_img}'")
386 gen_real_thumbnail.call
389 cleanup_all_thumbnails.call
390 perform_enhance_and_cleanup.call
392 #- remove out of sync images
394 perform_enhance_and_cleanup.call
395 $notebook.set_page(1)
397 perform_enhance_and_cleanup
398 $notebook.set_page(1)
404 perform_delete = Proc.new {
405 after = autotable.get_next_widget(vbox)
407 after = autotable.get_previous_widget(vbox)
409 autotable.remove(vbox)
411 $vbox2textview[after].grab_focus
415 save_undo(Proc.new { |pos|
416 autotable.reinsert(pos, vbox, filename)
417 $notebook.set_page(1)
420 $notebook.set_page(1)
422 }, autotable.get_current_number(vbox))
427 textview.signal_connect('key-press-event') { |w, event|
429 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
430 $autotable_sw.signal_emit('key-press-event', event)
433 x, y = autotable.get_current_pos(vbox)
434 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
435 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
436 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
437 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
439 $vbox2textview[autotable.get_widget_at_pos(x, y - 1)].grab_focus
442 autotable.move_up(vbox)
443 textview.grab_focus #- because if moving, focus is stolen
446 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
448 $vbox2textview[autotable.get_widget_at_pos(x, y + 1)].grab_focus
451 autotable.move_down(vbox)
452 textview.grab_focus #- because if moving, focus is stolen
455 if event.keyval == Gdk::Keyval::GDK_Left
456 previous = autotable.get_previous_widget(vbox)
457 if previous && autotable.get_current_pos(previous)[0] < x
459 $vbox2textview[previous].grab_focus
462 autotable.move_left(vbox)
463 textview.grab_focus #- because if moving, focus is stolen
467 rotate_and_cleanup.call(-90)
470 if event.keyval == Gdk::Keyval::GDK_Right
471 next_ = autotable.get_next_widget(vbox)
472 if next_ && autotable.get_current_pos(next_)[0] > x
474 $vbox2textview[next_].grab_focus
477 autotable.move_right(vbox)
478 textview.grab_focus #- because if moving, focus is stolen
482 rotate_and_cleanup.call(90)
485 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
488 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
489 view_element(filename)
493 !propagate #- propagate if needed
496 evtbox.signal_connect('button-press-event') { |w, event|
498 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
500 rotate_and_cleanup.call(90)
502 rotate_and_cleanup.call(270)
503 elsif $enhance.active?
504 enhance_and_cleanup.call
505 elsif $delete.active?
511 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
513 menu.append( r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
514 menu.append( r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
515 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
516 r90.signal_connect('activate') { rotate_and_cleanup.call(90) }
517 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
518 r270.signal_connect('activate') { rotate_and_cleanup.call(270) }
520 menu.append( Gtk::SeparatorMenuItem.new)
521 menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
522 menu.append( flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
523 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
524 color_swap.signal_connect('activate') { color_swap_and_cleanup.call }
525 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
526 flip.signal_connect('activate') { rotate_and_cleanup.call(180) }
528 menu.append( Gtk::SeparatorMenuItem.new)
529 menu.append(enhance = Gtk::ImageMenuItem.new(utf8($xmldir.elements["[@filename='#{filename}']"].attributes['enhance'] ? _("Original contrast") :
530 _("Enhance constrast"))))
531 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
532 enhance.signal_connect('activate') { enhance_and_cleanup.call }
533 menu.append( Gtk::SeparatorMenuItem.new)
534 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
535 delete_item.signal_connect('activate') { delete.call }
537 menu.popup(nil, nil, event.button, event.time)
539 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
540 view_element(filename)
542 retval = false #- propagate
547 vbox.signal_connect('button-press-event') { |w, event|
548 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
549 $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
553 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
554 if $gesture_press && $gesture_press[:filename] == filename
555 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
556 angle = x-$gesture_press[:x] > 0 ? 90 : -90
557 msg 3, "gesture rotate: #{angle}"
558 rotate_and_cleanup.call(90)
564 #- handle reordering with drag and drop
565 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
566 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
567 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
568 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
570 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
571 ctxt.targets.each { |target|
572 if target.name == 'reorder-elements'
573 from, to = selection_data.data.to_i, autotable.get_current_number(vbox)
575 autotable.move(from, to)
576 save_undo(Proc.new { |from, to|
578 autotable.move(to - 1, from)
580 autotable.move(to, from + 1)
582 $notebook.set_page(1)
584 autotable.move(from, to)
585 $notebook.set_page(1)
596 def create_auto_table
598 $autotable = Gtk::AutoTable.new(5)
600 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
601 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
602 $autotable_sw.add_with_viewport($autotable)
605 def create_subalbums_page
607 subalbums_hb = Gtk::HBox.new
608 # subalbums_hb.pack_start(Gtk::Label.new, true, true)
609 $subalbums_vb = Gtk::VBox.new(false, 5)
610 subalbums_hb.pack_start($subalbums_vb, false, false)
611 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
612 $subalbums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
613 $subalbums_sw.add_with_viewport(subalbums_hb)
619 ios = File.open($filename, "w")
620 $xmldoc.write(ios, 0)
626 def show_popup(parent, msg)
627 dialog = Gtk::Dialog.new
628 dialog.title = utf8(_("Booh message"))
632 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
634 dialog.transient_for = parent
635 dialog.set_default_size(200, 120)
636 dialog.window_position = Gtk::Window::POS_MOUSE
648 if $xmldir.elements['dir']
649 $xmldir.add_attribute('subdirs-caption', $subalbums_title.buffer.text)
650 $xmldir.elements.each('dir') { |element|
651 path = element.attributes['path']
652 if element.attributes['subdirs-caption']
653 element.add_attribute('subdirs-caption', $subalbums_edits[path][:editzone].buffer.text)
654 element.add_attribute('subdirs-captionfile', $subalbums_edits[path][:captionfile])
656 element.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
657 element.add_attribute('thumbnails-captionfile', $subalbums_edits[path][:captionfile])
660 if $xmldir.attributes['thumbnails-caption']
661 path = $xmldir.attributes['path']
662 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
666 #- remove and reinsert elements to reflect new ordering
669 $xmldir.elements.each { |element|
670 if element.name == 'image' || element.name == 'video'
671 save_types[element.attributes['filename']] = element.name
672 save_attributes[element.attributes['filename']] = element.attributes
676 $autotable.current_order.each { |path|
677 chld = $xmldir.add_element save_types[path], save_attributes[path]
678 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
687 $undo_tb.sensitive = $undo_mb.sensitive = false
688 $redo_tb.sensitive = $redo_mb.sensitive = false
694 $subalbums_vb.children.each { |chld|
695 $subalbums_vb.remove(chld)
697 $subalbums = Gtk::Table.new(0, 0, true)
698 current_y_sub_albums = 0
700 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
701 $subalbums_edits = {}
703 add_subalbum = Proc.new { |xmldir|
705 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
706 caption = xmldir.attributes['thumbnails-caption']
707 captionfile, dummy = find_subalbum_caption_info(xmldir)
708 infotype = 'thumbnails'
710 thumbnail_file = "#{current_dest_dir}/thumbnails-#{from_utf8(File.basename(xmldir.attributes['path']))}.jpg"
711 captionfile, caption = find_subalbum_caption_info(xmldir)
712 infotype = find_subalbum_info_type(xmldir)
714 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
716 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
718 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
721 gen_real_thumbnail = Proc.new {
723 system("rm -f '#{thumbnail_file}'")
724 gen_thumbnails_subdir(from_utf8(captionfile), xmldir, false,
725 [ { 'filename' => thumbnail_file, 'size' => $albums_thumbnail_size } ], infotype)
726 img.set(thumbnail_file)
727 $rotated_pixbufs[thumbnail_file] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
728 if entry2type(captionfile) == 'video'
729 system("rm -f #{current_dest_dir}/screenshot.jpg00000*")
734 if !$rotated_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
735 f.add(img = Gtk::Image.new)
736 gen_real_thumbnail.call
738 f.add(img = Gtk::Image.new($rotated_pixbufs[thumbnail_file] ? $rotated_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
740 hbox.pack_end(evtbox = Gtk::EventBox.new.add(f), false, false)
741 $subalbums.attach(hbox,
742 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
744 change_image = Proc.new {
745 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
747 Gtk::FileChooser::ACTION_OPEN,
749 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
750 fc.set_current_folder(xmldir.attributes['path'])
751 fc.transient_for = $main_window
752 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))
753 f.add(preview_img = Gtk::Image.new)
755 fc.signal_connect('update-preview') { |w|
757 if fc.preview_filename
758 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
759 fc.preview_widget_active = true
761 rescue Gdk::PixbufError
762 fc.preview_widget_active = false
765 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
766 msg 3, "new captionfile is: #{fc.filename}"
767 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = utf8(fc.filename)
768 $rotated_pixbufs.delete(thumbnail_file)
769 xmldir.delete_attribute("#{infotype}-rotate")
770 xmldir.delete_attribute("#{infotype}-color-swap")
771 xmldir.delete_attribute("#{infotype}-enhance")
772 gen_real_thumbnail.call
777 rotate_and_cleanup = Proc.new { |angle|
778 perform_rotate_and_cleanup = Proc.new { |angle|
779 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-",
780 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
782 perform_rotate_and_cleanup.call(angle)
783 system("rm -f '#{thumbnail_file}'")
785 save_undo(Proc.new { |angle|
786 perform_rotate_and_cleanup.call(angle)
787 $notebook.set_page(0)
789 perform_rotate_and_cleanup.call(-angle)
790 $notebook.set_page(0)
795 color_swap_and_cleanup = Proc.new {
796 perform_color_swap_and_cleanup = Proc.new {
797 if xmldir.attributes["#{infotype}-color-swap"]
798 xmldir.delete_attribute("#{infotype}-color-swap")
800 xmldir.add_attribute("#{infotype}-color-swap", '1')
802 gen_real_thumbnail.call
804 perform_color_swap_and_cleanup.call
807 perform_color_swap_and_cleanup.call
808 $notebook.set_page(0)
810 perform_color_swap_and_cleanup
811 $notebook.set_page(0)
816 enhance_and_cleanup = Proc.new {
817 perform_enhance_and_cleanup = Proc.new {
818 if xmldir.attributes["#{infotype}-enhance"]
819 xmldir.delete_attribute("#{infotype}-enhance")
821 xmldir.add_attribute("#{infotype}-enhance", '1')
823 gen_real_thumbnail.call
826 perform_enhance_and_cleanup.call
828 #- remove out of sync images
830 perform_enhance_and_cleanup.call
831 $notebook.set_page(1)
833 perform_enhance_and_cleanup
834 $notebook.set_page(1)
839 evtbox.signal_connect('button-press-event') { |w, event|
840 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
842 rotate_and_cleanup.call(90)
844 rotate_and_cleanup.call(270)
845 elsif $enhance.active?
846 enhance_and_cleanup.call
849 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
851 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
852 menu.append( Gtk::SeparatorMenuItem.new)
853 menu.append( r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
854 menu.append( r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
855 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
856 changeimg.signal_connect('activate') { change_image.call }
857 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
858 r90.signal_connect('activate') { rotate_and_cleanup.call(90) }
859 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
860 r270.signal_connect('activate') { rotate_and_cleanup.call(270) }
861 if entry2type(captionfile) == 'video'
862 menu.append( Gtk::SeparatorMenuItem.new)
863 menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
864 menu.append( flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
865 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
866 color_swap.signal_connect('activate') { color_swap_and_cleanup.call }
867 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
868 flip.signal_connect('activate') { rotate_and_cleanup.call(180) }
870 menu.append( Gtk::SeparatorMenuItem.new)
871 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{infotype}-enhance"] ? _("Original contrast") :
872 _("Enhance constrast"))))
873 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
874 enhance.signal_connect('activate') { enhance_and_cleanup.call }
876 menu.popup(nil, nil, event.button, event.time)
878 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
883 evtbox.signal_connect('button-press-event') { |w, event|
884 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
888 evtbox.signal_connect('button-release-event') { |w, event|
889 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
890 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
891 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
892 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
893 msg 3, "gesture rotate: #{angle}"
894 rotate_and_cleanup.call(angle)
900 frame, textview = create_editzone($subalbums_sw, 0)
901 textview.buffer.text = caption
902 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
903 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
905 $subalbums_edits[xmldir.attributes['path']] = { :editzone => textview, :captionfile => captionfile }
906 current_y_sub_albums += 1
909 if $xmldir.elements['dir']
911 frame, $subalbums_title = create_editzone($subalbums_sw, 0)
912 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
913 $subalbums_title.set_justification(Gtk::Justification::CENTER)
914 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
915 #- this album image/caption
916 if $xmldir.attributes['thumbnails-caption']
917 add_subalbum.call($xmldir)
920 $xmldir.elements.each { |element|
921 if element.name == 'image' || element.name == 'video'
922 #- element (image or video) of this album
923 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
924 msg 3, "dest_img: #{dest_img}"
925 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
927 if element.name == 'dir'
928 #- sub-album image/caption
929 add_subalbum.call(element)
932 $subalbums_vb.add($subalbums)
933 $subalbums_vb.show_all
935 if !$xmldir.elements['image'] && !$xmldir.elements['video']
936 $notebook.get_tab_label($autotable_sw).sensitive = false
937 $notebook.set_page(0)
939 $notebook.get_tab_label($autotable_sw).sensitive = true
942 if !$xmldir.elements['dir']
943 $notebook.get_tab_label($subalbums_sw).sensitive = false
944 $notebook.set_page(1)
946 $notebook.get_tab_label($subalbums_sw).sensitive = true
950 def open_file(filename)
953 $current_path = nil #- invalidate
954 $rotated_pixbufs = {}
957 $subalbums_vb.children.each { |chld|
958 $subalbums_vb.remove(chld)
961 if !File.exists?(filename)
962 return utf8(_("File not found."))
966 $xmldoc = REXML::Document.new File.new(filename)
971 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
972 return utf8(_("Not a booh file!"))
975 if !source = $xmldoc.root.attributes['source']
976 return utf8(_("Corrupted booh file..."))
979 if !dest = $xmldoc.root.attributes['destination']
980 return utf8(_("Corrupted booh file..."))
983 if !theme = $xmldoc.root.attributes['theme']
984 return utf8(_("Corrupted booh file..."))
989 $default_size = $images_size.detect { |sizeobj| sizeobj['default'] }
990 $default_size['thumbnails'] =~ /(.*)x(.*)/
991 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
992 $albums_thumbnail_size =~ /(.*)x(.*)/
993 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
995 msg 3, "source: #{source}"
997 xmldir = $xmldoc.elements["//dir[@path='#{source}']"]
999 return utf8(_("Corrupted booh file..."))
1002 append_dir_elem = Proc.new { |parent_iter, location|
1003 child_iter = $albums_ts.append(parent_iter)
1004 child_iter[0] = File.basename(location)
1005 child_iter[1] = location
1006 msg 3, "puttin location: #{location}"
1007 $xmldoc.elements.each("//dir[@path='#{location}']/dir") { |elem|
1008 append_dir_elem.call(child_iter, elem.attributes['path'])
1011 append_dir_elem.call(nil, source)
1013 $albums_tv.expand_all
1014 $albums_tv.selection.select_iter($albums_ts.iter_first)
1016 $config['last-opens'] ||= []
1017 $config['last-opens'] << filename
1022 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
1024 Gtk::FileChooser::ACTION_OPEN,
1026 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1027 fc.add_shortcut_folder(File.expand_path("~/.booh-gui-files"))
1028 fc.set_current_folder(File.expand_path("~/.booh-gui-files"))
1029 fc.transient_for = $main_window
1032 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1033 push_mousecursor_wait(fc)
1034 msg = open_file(fc.filename)
1049 def create_menu_and_toolbar
1052 mb = Gtk::MenuBar.new
1054 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
1055 filesubmenu = Gtk::Menu.new
1056 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
1057 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
1058 filesubmenu.append( Gtk::SeparatorMenuItem.new)
1059 filesubmenu.append(save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE))
1060 filesubmenu.append(save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS))
1061 filesubmenu.append( Gtk::SeparatorMenuItem.new)
1062 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
1063 filemenu.set_submenu(filesubmenu)
1066 open.signal_connect('activate') { open_file_popup }
1067 quit.signal_connect('activate') { try_quit }
1069 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
1070 editsubmenu = Gtk::Menu.new
1071 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO))
1072 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO))
1073 editsubmenu.append( Gtk::SeparatorMenuItem.new)
1074 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
1075 editmenu.set_submenu(editsubmenu)
1080 tb = Gtk::Toolbar.new
1082 tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
1083 open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
1084 open.menu = Gtk::Menu.new
1085 open.signal_connect('clicked') { open_file_popup }
1086 open.signal_connect('show-menu') {
1087 lastopens = Gtk::Menu.new
1089 if $config['last-opens']
1090 $config['last-opens'].reverse.each { |e|
1091 lastopens.attach(item = Gtk::ImageMenuItem.new(e), 0, 1, j, j + 1)
1092 item.signal_connect('activate') {
1093 push_mousecursor_wait
1097 show_popup($main_window, msg)
1104 open.menu = lastopens
1107 tb.insert(-1, Gtk::SeparatorToolItem.new)
1109 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
1110 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
1111 $r90.label = utf8(_("Rotate"))
1112 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
1113 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
1114 $r270.label = utf8(_("Rotate"))
1115 tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
1116 $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1117 $enhance.label = utf8(_("Enhance"))
1118 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
1119 $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
1120 tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
1121 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
1122 nothing.label = utf8(_("None"))
1124 tb.insert(-1, Gtk::SeparatorToolItem.new)
1126 tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO))
1127 tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO))
1129 perform_undo = Proc.new {
1130 $redo_tb.sensitive = $redo_mb.sensitive = true
1131 if not more_undoes = UndoHandler.undo
1132 $undo_tb.sensitive = $undo_mb.sensitive = false
1135 perform_redo = Proc.new {
1136 $undo_tb.sensitive = $undo_mb.sensitive = true
1137 if not more_redoes = UndoHandler.redo
1138 $redo_tb.sensitive = $redo_mb.sensitive = false
1142 $undo_tb.signal_connect('clicked') { perform_undo.call }
1143 $undo_mb.signal_connect('activate') { perform_undo.call }
1144 $redo_tb.signal_connect('clicked') { perform_redo.call }
1145 $redo_mb.signal_connect('activate') { perform_redo.call }
1147 one_click_explain_try = Proc.new {
1148 if !$config['one-click-explained']
1149 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
1151 You have just clicked on a One-Click tool. When such a tool is activated
1152 (<span foreground=\"darkblue\">Rotate clockwise</span>, <span foreground=\"darkblue\">Rotate counter-clockwise</span>, or <span foreground=\"darkblue\">Delete</span>), clicking on a
1153 thumbnail will immediately apply the desired action.
1155 Click the <span foreground=\"darkblue\">None</span> icon when you're finished with One-Click tools.
1157 $config['one-click-explained'] = true
1161 $r90.signal_connect('toggled') {
1163 set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
1164 one_click_explain_try.call
1165 $r270.active = false
1166 $enhance.active = false
1167 $delete.active = false
1168 nothing.sensitive = true
1170 if !$r270.active? && !$enhance.active? && !$delete.active?
1171 set_mousecursor_normal
1172 nothing.sensitive = false
1174 nothing.sensitive = true
1178 $r270.signal_connect('toggled') {
1180 set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
1181 one_click_explain_try.call
1183 $enhance.active = false
1184 $delete.active = false
1185 nothing.sensitive = true
1187 if !$r90.active? && !$enhance.active? && !$delete.active?
1188 set_mousecursor_normal
1189 nothing.sensitive = false
1191 nothing.sensitive = true
1195 $enhance.signal_connect('toggled') {
1197 set_mousecursor(Gdk::Cursor::SPRAYCAN)
1198 one_click_explain_try.call
1200 $r270.active = false
1201 $delete.active = false
1202 nothing.sensitive = true
1204 if !$r90.active? && !$r270.active? && !$delete.active?
1205 set_mousecursor_normal
1206 nothing.sensitive = false
1208 nothing.sensitive = true
1212 $delete.signal_connect('toggled') {
1214 set_mousecursor(Gdk::Cursor::PIRATE)
1215 one_click_explain_try.call
1217 $r270.active = false
1218 $enhance.active = false
1219 nothing.sensitive = true
1221 if !$r90.active? && !$r270.active? && !$enhance.active?
1222 set_mousecursor_normal
1223 nothing.sensitive = false
1225 nothing.sensitive = true
1229 nothing.signal_connect('clicked') {
1230 $r90.active = $r270.active = $enhance.active = $delete.active = false
1231 set_mousecursor_normal
1237 def create_main_window
1239 mb, tb = create_menu_and_toolbar
1241 # open_file('/home/gc/booh/foo')
1243 $albums_tv = Gtk::TreeView.new
1244 $albums_tv.set_size_request(120, -1)
1245 renderer = Gtk::CellRendererText.new
1246 column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 })
1247 $albums_tv.append_column(column)
1248 $albums_tv.set_headers_visible(false)
1249 $albums_tv.selection.signal_connect('changed') { |w|
1250 push_mousecursor_wait
1254 msg 3, "no selection"
1256 $current_path = $albums_ts.get_value(iter, 1)
1261 $albums_ts = Gtk::TreeStore.new(String, String)
1262 $albums_tv.set_model($albums_ts)
1263 $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
1265 $notebook = Gtk::Notebook.new
1266 create_subalbums_page
1267 $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
1269 $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
1271 $notebook.signal_connect('switch-page') { |w, page, num|
1273 $delete.active = false
1274 $delete.sensitive = false
1276 $delete.sensitive = true
1280 paned = Gtk::HPaned.new
1281 paned.pack1($albums_tv, false, false)
1282 paned.pack2($notebook, true, true)
1284 main_vbox = Gtk::VBox.new(false, 0)
1285 main_vbox.pack_start(mb, false, false)
1286 main_vbox.pack_start(tb, false, false)
1287 main_vbox.pack_start(paned, true, true)
1288 main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
1290 $main_window = Gtk::Window.new
1291 $main_window.add(main_vbox)
1292 $main_window.signal_connect('delete-event') {
1296 #- read/save size and position of window
1297 if $config['pos-x'] && $config['pos-y']
1298 $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
1300 $main_window.window_position = Gtk::Window::POS_CENTER
1302 msg 3, "size: #{$config['width']}x#{$config['height']}"
1303 $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
1304 $main_window.signal_connect('configure-event') {
1305 msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
1306 x, y = $main_window.window.root_origin
1307 width, height = $main_window.window.size
1308 $config['pos-x'] = x
1309 $config['pos-y'] = y
1310 $config['width'] = width
1311 $config['height'] = height
1315 $statusbar.push(0, utf8(_("Ready.")))
1316 $main_window.show_all