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 if Thread.current == Thread.main
140 Gtk.main_iteration while Gtk.events_pending?
143 def set_mousecursor_normal(*widget)
144 set_mousecursor(Gdk::Cursor::LEFT_PTR, *widget)
146 def push_mousecursor_wait(*widget)
147 if $current_cursor != Gdk::Cursor::WATCH
148 $save_cursor = $current_cursor
149 set_mousecursor_wait(*widget)
152 def pop_mousecursor(*widget)
153 set_mousecursor($save_cursor || Gdk::Cursor::LEFT_PTR, *widget)
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))
162 def build_full_dest_filename(filename)
163 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
166 def save_undo(name, closure, *params)
167 UndoHandler.save_undo(name, closure, [ *params ])
168 $undo_tb.sensitive = $undo_mb.sensitive = true
171 def view_element(filename)
172 if entry2type(filename) == 'video'
173 system("mplayer #{$current_path + '/' + filename}")
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]
189 push_mousecursor_wait
190 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
192 if !File.exists?(dest_img)
193 msg 2, _("Could not generate fullscreen thumbnail!")
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)))
200 tooltips = Gtk::Tooltips.new
201 tooltips.set_tip(evt, utf8(File.basename(filename).gsub(/\.jpg/, '')), nil)
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 }
207 vb.pack_start(evt, false, false)
208 vb.pack_end(bottom, false, false)
211 w.signal_connect('delete-event') { w.destroy }
212 w.window_position = Gtk::Window::POS_CENTER
216 def create_editzone(scrolledwindow, pagenum)
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)
227 textview.signal_connect('focus-in-event') { |w, event|
228 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
232 candidate_undo_text = nil
233 textview.signal_connect('focus-in-event') { |w, event|
234 candidate_undo_text = textview.buffer.text
237 textview.signal_connect('key-release-event') { |w, event|
238 if candidate_undo_text && candidate_undo_text != textview.buffer.text
240 save_undo(_("text edit"),
242 save_text = textview.buffer.text
243 textview.buffer.text = text
245 $notebook.set_page(pagenum)
247 textview.buffer.text = save_text
249 $notebook.set_page(pagenum)
251 }, candidate_undo_text)
252 candidate_undo_text = nil
257 return [ frame, textview ]
260 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
263 #- update rotate attribute
264 xmlelem.add_attribute("#{attributes_prefix}rotate", current_angle = ( xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle ) % 360)
266 if !$rotated_pixbufs[thumbnail_img]
267 $rotated_pixbufs[thumbnail_img] = { :orig => img.pixbuf, :angle_to_orig => angle % 360 }
269 $rotated_pixbufs[thumbnail_img][:angle_to_orig] = ( $rotated_pixbufs[thumbnail_img][:angle_to_orig] + angle ) % 360
271 msg 3, "angle: #{angle}, angle to orig: #{$rotated_pixbufs[thumbnail_img][:angle_to_orig]}"
273 #- rotate shown thumbnail
274 pixbuf = rotate_pixbuf($rotated_pixbufs[thumbnail_img][:orig], $rotated_pixbufs[thumbnail_img][:angle_to_orig])
275 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
276 if pixbuf.height > desired_y
277 img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y,
278 Gdk::Pixbuf::INTERP_BILINEAR)
279 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
280 img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width),
281 Gdk::Pixbuf::INTERP_BILINEAR)
283 img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf
287 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
290 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
292 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
294 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
295 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
297 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
298 $notebook.set_page(0)
299 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
304 def color_swap(xmldir, attributes_prefix)
306 if xmldir.attributes["#{attributes_prefix}color-swap"]
307 xmldir.delete_attribute("#{attributes_prefix}color-swap")
309 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
313 def enhance(xmldir, attributes_prefix)
315 if xmldir.attributes["#{attributes_prefix}enhance"]
316 xmldir.delete_attribute("#{attributes_prefix}enhance")
318 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
322 def change_frame_offset(xmldir, attributes_prefix, value)
324 xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
327 def ask_new_frame_offset(xmldir, attributes_prefix)
328 value = xmldir.attributes["#{attributes_prefix}frame-offset"]
330 dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
332 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
333 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
334 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
338 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
339 from. There are approximately 25 frames per second in a video.
342 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
343 entry.signal_connect('key-press-event') { |w, event|
344 if event.keyval == Gdk::Keyval::GDK_Return
345 dialog.response(Gtk::Dialog::RESPONSE_OK)
347 elsif event.keyval == Gdk::Keyval::GDK_Escape
348 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
351 false #- propagate if needed
355 dialog.window_position = Gtk::Window::POS_MOUSE
358 dialog.run { |response|
362 if response == Gtk::Dialog::RESPONSE_OK
363 msg 3, "changing frame offset top #{newval}"
364 return { :old => value, :new => newval }
371 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img)
373 push_mousecursor_wait
374 system("rm -f '#{destfile}'")
375 #- type can be 'element' or 'subdir'
377 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
379 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], find_subalbum_info_type(xmldir))
382 $rotated_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
383 if entry2type(origfile) == 'video'
384 #- cleanup temp for videos
385 system("rm -f #{current_dest_dir}/screenshot.jpg000000.jpg")
391 def popup_thumbnail_menu(event, optionals, type, xmldir, attributes_prefix, closures)
393 if optionals.include?('change_image')
394 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
395 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
396 changeimg.signal_connect('activate') { closures[:change].call }
397 menu.append( Gtk::SeparatorMenuItem.new)
399 menu.append( r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
400 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
401 r90.signal_connect('activate') { closures[:rotate].call(90) }
402 menu.append( r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
403 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
404 r270.signal_connect('activate') { closures[:rotate].call(-90) }
406 menu.append( Gtk::SeparatorMenuItem.new)
407 menu.append( color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
408 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
409 color_swap.signal_connect('activate') { closures[:color_swap].call }
410 menu.append( flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
411 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
412 flip.signal_connect('activate') { closures[:rotate].call(180) }
413 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
414 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
415 frame_offset.signal_connect('activate') { closures[:frame_offset].call }
417 menu.append( Gtk::SeparatorMenuItem.new)
418 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
419 _("Enhance constrast"))))
420 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
421 enhance.signal_connect('activate') { closures[:enhance].call }
422 if optionals.include?('delete')
423 menu.append( Gtk::SeparatorMenuItem.new)
424 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
425 delete_item.signal_connect('activate') { closures[:delete].call }
428 menu.popup(nil, nil, event.button, event.time)
431 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
434 frame1 = Gtk::Frame.new
436 my_gen_real_thumbnail = proc {
437 gen_real_thumbnail('element', from_utf8("#{$current_path}/#{filename}"), thumbnail_img, $xmldir, $default_size['thumbnails'], img)
440 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
441 if !$rotated_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
442 frame1.add(img = Gtk::Image.new)
443 my_gen_real_thumbnail.call
445 frame1.add(img = Gtk::Image.new($rotated_pixbufs[thumbnail_img] ? $rotated_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
447 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
449 tooltips = Gtk::Tooltips.new
450 tipname = File.basename(thumbnail_img).gsub(/-\d+x\d+\.jpg/, '')
451 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(File.size(from_utf8("#{$current_path}/#{filename}"))/1024)]) : tipname), nil)
453 frame2, textview = create_editzone($autotable_sw, 1)
454 textview.buffer.text = utf8(caption)
455 textview.set_justification(Gtk::Justification::CENTER)
457 vbox = Gtk::VBox.new(false, 5)
458 vbox.pack_start(evtbox, false, false)
459 vbox.pack_start(frame2, false, false)
460 autotable.append(vbox, filename)
462 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
463 $vbox2textview[vbox] = textview
465 #- to be able to find widgets by name
466 $name2widgets[filename] = { :textview => textview }
468 cleanup_all_thumbnails = Proc.new {
469 #- remove out of sync images
470 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
471 for sizeobj in $images_size
472 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
477 rotate_and_cleanup = Proc.new { |angle|
478 rotate(angle, thumbnail_img, img, $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
479 cleanup_all_thumbnails.call
482 color_swap_and_cleanup = Proc.new {
483 perform_color_swap_and_cleanup = Proc.new {
484 color_swap($xmldir.elements["[@filename='#{filename}']"], '')
485 my_gen_real_thumbnail.call
488 cleanup_all_thumbnails.call
489 perform_color_swap_and_cleanup.call
491 save_undo(_("color swap"),
493 perform_color_swap_and_cleanup.call
494 $notebook.set_page(1)
496 perform_color_swap_and_cleanup.call
497 $notebook.set_page(1)
502 change_frame_offset_and_cleanup = Proc.new {
503 if values = ask_new_frame_offset($xmldir.elements["[@filename='#{filename}']"], '')
504 perform_change_frame_offset_and_cleanup = Proc.new { |val|
505 change_frame_offset($xmldir.elements["[@filename='#{filename}']"], '', val)
506 my_gen_real_thumbnail.call
508 perform_change_frame_offset_and_cleanup.call(values[:new])
510 save_undo(_("specify frame offset"),
512 perform_change_frame_offset_and_cleanup.call(values[:old])
513 $notebook.set_page(1)
515 perform_change_frame_offset_and_cleanup.call(values[:new])
516 $notebook.set_page(1)
522 enhance_and_cleanup = Proc.new {
523 perform_enhance_and_cleanup = Proc.new {
524 enhance($xmldir.elements["[@filename='#{filename}']"], '')
525 my_gen_real_thumbnail.call
528 cleanup_all_thumbnails.call
529 perform_enhance_and_cleanup.call
531 save_undo(_("enhance"),
533 perform_enhance_and_cleanup.call
534 $notebook.set_page(1)
536 perform_enhance_and_cleanup.call
537 $notebook.set_page(1)
544 perform_delete = Proc.new {
545 after = autotable.get_next_widget(vbox)
547 after = autotable.get_previous_widget(vbox)
549 autotable.remove(vbox)
551 $vbox2textview[after].grab_focus
555 save_undo(_("delete"),
557 autotable.reinsert(pos, vbox, filename)
558 $notebook.set_page(1)
561 $notebook.set_page(1)
563 }, autotable.get_current_number(vbox))
568 textview.signal_connect('key-press-event') { |w, event|
570 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
571 $autotable_sw.signal_emit('key-press-event', event)
574 x, y = autotable.get_current_pos(vbox)
575 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
576 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
577 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
578 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
580 $vbox2textview[autotable.get_widget_at_pos(x, y - 1)].grab_focus
583 autotable.move_up(vbox)
584 textview.grab_focus #- because if moving, focus is stolen
587 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
589 $vbox2textview[autotable.get_widget_at_pos(x, y + 1)].grab_focus
592 autotable.move_down(vbox)
593 textview.grab_focus #- because if moving, focus is stolen
596 if event.keyval == Gdk::Keyval::GDK_Left
597 previous = autotable.get_previous_widget(vbox)
598 if previous && autotable.get_current_pos(previous)[0] < x
600 $vbox2textview[previous].grab_focus
603 autotable.move_left(vbox)
604 textview.grab_focus #- because if moving, focus is stolen
608 rotate_and_cleanup.call(-90)
611 if event.keyval == Gdk::Keyval::GDK_Right
612 next_ = autotable.get_next_widget(vbox)
613 if next_ && autotable.get_current_pos(next_)[0] > x
615 $vbox2textview[next_].grab_focus
618 autotable.move_right(vbox)
619 textview.grab_focus #- because if moving, focus is stolen
623 rotate_and_cleanup.call(90)
626 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
629 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
630 view_element(filename)
634 !propagate #- propagate if needed
637 evtbox.signal_connect('button-press-event') { |w, event|
639 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
641 rotate_and_cleanup.call(90)
643 rotate_and_cleanup.call(-90)
644 elsif $enhance.active?
645 enhance_and_cleanup.call
646 elsif $delete.active?
652 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
653 popup_thumbnail_menu(event, ['delete'], type, $xmldir.elements["[@filename='#{filename}']"], '',
654 { :rotate => rotate_and_cleanup, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
655 :frame_offset => change_frame_offset_and_cleanup, :delete => delete })
657 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
658 view_element(filename)
660 retval = false #- propagate
665 vbox.signal_connect('button-press-event') { |w, event|
666 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
667 $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
671 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
672 if $gesture_press && $gesture_press[:filename] == filename
673 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
674 angle = x-$gesture_press[:x] > 0 ? 90 : -90
675 msg 3, "gesture rotate: #{angle}"
676 rotate_and_cleanup.call(angle)
682 #- handle reordering with drag and drop
683 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
684 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
685 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
686 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
688 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
689 ctxt.targets.each { |target|
690 if target.name == 'reorder-elements'
691 from, to = selection_data.data.to_i, autotable.get_current_number(vbox)
694 autotable.move(from, to)
695 save_undo(_("reorder"),
696 Proc.new { |from, to|
698 autotable.move(to - 1, from)
700 autotable.move(to, from + 1)
702 $notebook.set_page(1)
704 autotable.move(from, to)
705 $notebook.set_page(1)
716 def create_auto_table
718 $autotable = Gtk::AutoTable.new(5)
720 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
721 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
722 $autotable_sw.add_with_viewport($autotable)
725 def create_subalbums_page
727 subalbums_hb = Gtk::HBox.new
728 # subalbums_hb.pack_start(Gtk::Label.new, true, true)
729 $subalbums_vb = Gtk::VBox.new(false, 5)
730 subalbums_hb.pack_start($subalbums_vb, false, false)
731 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
732 $subalbums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
733 $subalbums_sw.add_with_viewport(subalbums_hb)
736 def save_current_file
739 ios = File.open($filename, "w")
740 $xmldoc.write(ios, 0)
748 dialog = Gtk::Dialog.new(utf8(_("Save before quitting?")),
750 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
751 [Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
752 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
753 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
754 dialog.vbox.add(Gtk::Label.new(utf8(_("Do you want to save your changes before quitting?"))))
755 dialog.window_position = Gtk::Window::POS_CENTER
758 dialog.run { |response|
760 if response == Gtk::Dialog::RESPONSE_CANCEL
762 elsif response == Gtk::Dialog::RESPONSE_YES
771 def show_popup(parent, msg, *options)
772 dialog = Gtk::Dialog.new
773 dialog.title = utf8(_("Booh message"))
776 if options[0] && options[0][:centered]
777 lbl.set_justify(Gtk::Justification::CENTER)
780 if options[0] && options[0][:okcancel]
781 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
783 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
785 dialog.transient_for = parent
786 dialog.set_default_size(200, 120)
787 if options[0] && options[0][:pos_centered]
788 dialog.window_position = Gtk::Window::POS_CENTER
790 dialog.window_position = Gtk::Window::POS_MOUSE
794 dialog.run { |response|
796 if options[0] && options[0][:okcancel]
797 return response == Gtk::Dialog::RESPONSE_OK
802 def wait_message(parent, msg)
804 w.set_transient_for(parent)
806 vb = Gtk::VBox.new(false, 5)
807 vb.pack_start(Gtk::Label.new(msg), false, false)
808 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
809 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
810 vb.pack_start(pb = Gtk::ProgressBar.new.set_pulse_step(0.05), false, false)
811 vb.pack_start(Gtk::HSeparator.new, false, false)
812 vb.pack_end(bottom, false, false)
814 timeout = Gtk.timeout_add(200) { pb.pulse }
815 b.signal_connect_after('clicked') { w.destroy }
818 w.signal_connect('delete-event') { w.destroy }
819 w.signal_connect('destroy') { Gtk.timeout_remove(timeout) }
820 w.window_position = Gtk::Window::POS_CENTER
831 if $xmldir.elements['dir']
832 $xmldir.add_attribute('subdirs-caption', $subalbums_title.buffer.text)
833 $xmldir.elements.each('dir') { |element|
834 path = element.attributes['path']
835 if element.attributes['subdirs-caption']
836 element.add_attribute('subdirs-caption', $subalbums_edits[path][:editzone].buffer.text)
837 element.add_attribute('subdirs-captionfile', $subalbums_edits[path][:captionfile])
839 element.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
840 element.add_attribute('thumbnails-captionfile', $subalbums_edits[path][:captionfile])
843 if $xmldir.attributes['thumbnails-caption']
844 path = $xmldir.attributes['path']
845 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
849 #- remove and reinsert elements to reflect new ordering
852 $xmldir.elements.each { |element|
853 if element.name == 'image' || element.name == 'video'
854 save_types[element.attributes['filename']] = element.name
855 save_attributes[element.attributes['filename']] = element.attributes
859 $autotable.current_order.each { |path|
860 chld = $xmldir.add_element save_types[path], save_attributes[path]
861 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
870 $undo_tb.sensitive = $undo_mb.sensitive = false
871 $redo_tb.sensitive = $redo_mb.sensitive = false
877 $subalbums_vb.children.each { |chld|
878 $subalbums_vb.remove(chld)
880 $subalbums = Gtk::Table.new(0, 0, true)
881 current_y_sub_albums = 0
883 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
884 $subalbums_edits = {}
886 add_subalbum = Proc.new { |xmldir|
888 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
889 caption = xmldir.attributes['thumbnails-caption']
890 captionfile, dummy = find_subalbum_caption_info(xmldir)
891 infotype = 'thumbnails'
893 thumbnail_file = "#{current_dest_dir}/thumbnails-#{from_utf8(File.basename(xmldir.attributes['path']))}.jpg"
894 captionfile, caption = find_subalbum_caption_info(xmldir)
895 infotype = find_subalbum_info_type(xmldir)
897 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
899 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
901 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
904 my_gen_real_thumbnail = proc {
905 gen_real_thumbnail('subdir', from_utf8(captionfile), thumbnail_file, xmldir, $albums_thumbnail_size, img)
908 if !$rotated_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
909 f.add(img = Gtk::Image.new)
910 my_gen_real_thumbnail.call
912 f.add(img = Gtk::Image.new($rotated_pixbufs[thumbnail_file] ? $rotated_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
914 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
915 $subalbums.attach(hbox,
916 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
918 change_image = Proc.new {
919 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
921 Gtk::FileChooser::ACTION_OPEN,
923 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
924 fc.set_current_folder(xmldir.attributes['path'])
925 fc.transient_for = $main_window
926 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))
927 f.add(preview_img = Gtk::Image.new)
929 fc.signal_connect('update-preview') { |w|
931 if fc.preview_filename
932 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
933 fc.preview_widget_active = true
935 rescue Gdk::PixbufError
936 fc.preview_widget_active = false
939 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
940 msg 3, "new captionfile is: #{fc.filename}"
941 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = utf8(fc.filename)
942 $rotated_pixbufs.delete(thumbnail_file)
943 xmldir.delete_attribute("#{infotype}-rotate")
944 xmldir.delete_attribute("#{infotype}-color-swap")
945 xmldir.delete_attribute("#{infotype}-enhance")
946 xmldir.delete_attribute("#{infotype}-frame-offset")
947 my_gen_real_thumbnail.call
952 rotate_and_cleanup = Proc.new { |angle|
953 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
954 system("rm -f '#{thumbnail_file}'")
957 color_swap_and_cleanup = Proc.new {
958 perform_color_swap_and_cleanup = Proc.new {
959 color_swap(xmldir, "#{infotype}-")
960 my_gen_real_thumbnail.call
962 perform_color_swap_and_cleanup.call
964 save_undo(_("color swap"),
966 perform_color_swap_and_cleanup.call
967 $notebook.set_page(0)
969 perform_color_swap_and_cleanup.call
970 $notebook.set_page(0)
975 change_frame_offset_and_cleanup = Proc.new {
976 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
977 perform_change_frame_offset_and_cleanup = Proc.new { |val|
978 change_frame_offset(xmldir, "#{infotype}-", val)
979 my_gen_real_thumbnail.call
981 perform_change_frame_offset_and_cleanup.call(values[:new])
983 save_undo(_("specify frame offset"),
985 perform_change_frame_offset_and_cleanup.call(values[:old])
986 $notebook.set_page(0)
988 perform_change_frame_offset_and_cleanup.call(values[:new])
989 $notebook.set_page(0)
995 enhance_and_cleanup = Proc.new {
996 perform_enhance_and_cleanup = Proc.new {
997 enhance(xmldir, "#{infotype}-")
998 my_gen_real_thumbnail.call
1001 perform_enhance_and_cleanup.call
1003 save_undo(_("enhance"),
1005 perform_enhance_and_cleanup.call
1006 $notebook.set_page(0)
1008 perform_enhance_and_cleanup.call
1009 $notebook.set_page(0)
1014 evtbox.signal_connect('button-press-event') { |w, event|
1015 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1017 rotate_and_cleanup.call(90)
1019 rotate_and_cleanup.call(-90)
1020 elsif $enhance.active?
1021 enhance_and_cleanup.call
1024 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1025 popup_thumbnail_menu(event, ['change_image'], entry2type(captionfile), xmldir, "#{infotype}-",
1026 { :change => change_image, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
1027 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup })
1029 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1034 evtbox.signal_connect('button-press-event') { |w, event|
1035 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
1039 evtbox.signal_connect('button-release-event') { |w, event|
1040 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
1041 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
1042 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
1043 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
1044 msg 3, "gesture rotate: #{angle}"
1045 rotate_and_cleanup.call(angle)
1048 $gesture_press = nil
1051 frame, textview = create_editzone($subalbums_sw, 0)
1052 textview.buffer.text = caption
1053 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1054 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1056 $subalbums_edits[xmldir.attributes['path']] = { :editzone => textview, :captionfile => captionfile }
1057 current_y_sub_albums += 1
1060 if $xmldir.elements['dir']
1062 frame, $subalbums_title = create_editzone($subalbums_sw, 0)
1063 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
1064 $subalbums_title.set_justification(Gtk::Justification::CENTER)
1065 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1066 #- this album image/caption
1067 if $xmldir.attributes['thumbnails-caption']
1068 add_subalbum.call($xmldir)
1071 $xmldir.elements.each { |element|
1072 if element.name == 'image' || element.name == 'video'
1073 #- element (image or video) of this album
1074 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
1075 msg 3, "dest_img: #{dest_img}"
1076 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
1078 if element.name == 'dir'
1079 #- sub-album image/caption
1080 add_subalbum.call(element)
1083 $subalbums_vb.add($subalbums)
1084 $subalbums_vb.show_all
1086 if !$xmldir.elements['image'] && !$xmldir.elements['video']
1087 $notebook.get_tab_label($autotable_sw).sensitive = false
1088 $notebook.set_page(0)
1090 $notebook.get_tab_label($autotable_sw).sensitive = true
1093 if !$xmldir.elements['dir']
1094 $notebook.get_tab_label($subalbums_sw).sensitive = false
1095 $notebook.set_page(1)
1097 $notebook.get_tab_label($subalbums_sw).sensitive = true
1101 def pixbuf_or_nil(filename)
1103 return Gdk::Pixbuf.new(filename)
1109 def theme_choose(current)
1110 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
1112 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1113 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1114 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1116 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
1117 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
1118 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
1119 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
1120 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
1121 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
1122 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
1124 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
1126 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
1129 iter[0] = File.basename(dir)
1130 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
1131 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
1132 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
1133 if File.basename(dir) == current
1134 treeview.selection.select_iter(iter)
1138 dialog.set_default_size(700, 400)
1139 dialog.vbox.show_all
1140 dialog.run { |response|
1141 iter = treeview.selection.selected
1143 if response == Gtk::Dialog::RESPONSE_OK && iter
1144 return model.get_value(iter, 0)
1150 def open_file(filename)
1154 $current_path = nil #- invalidate
1155 $rotated_pixbufs = {}
1158 $subalbums_vb.children.each { |chld|
1159 $subalbums_vb.remove(chld)
1162 if !File.exists?(filename)
1163 return utf8(_("File not found."))
1167 $xmldoc = REXML::Document.new File.new(filename)
1172 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
1173 return utf8(_("Not a booh file!"))
1176 if !source = $xmldoc.root.attributes['source']
1177 return utf8(_("Corrupted booh file..."))
1180 if !dest = $xmldoc.root.attributes['destination']
1181 return utf8(_("Corrupted booh file..."))
1184 if !theme = $xmldoc.root.attributes['theme']
1185 return utf8(_("Corrupted booh file..."))
1188 $filename = filename
1190 $default_size['thumbnails'] =~ /(.*)x(.*)/
1191 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
1192 $albums_thumbnail_size =~ /(.*)x(.*)/
1193 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
1195 msg 3, "source: #{source}"
1197 xmldir = $xmldoc.elements["//dir[@path='#{source}']"]
1199 return utf8(_("Corrupted booh file..."))
1202 append_dir_elem = Proc.new { |parent_iter, location|
1203 child_iter = $albums_ts.append(parent_iter)
1204 child_iter[0] = File.basename(location)
1205 child_iter[1] = location
1206 msg 3, "puttin location: #{location}"
1207 $xmldoc.elements.each("//dir[@path='#{location}']/dir") { |elem|
1208 append_dir_elem.call(child_iter, elem.attributes['path'])
1211 append_dir_elem.call(nil, source)
1213 $albums_tv.expand_all
1214 $albums_tv.selection.select_iter($albums_ts.iter_first)
1216 $config['last-opens'] ||= []
1217 $config['last-opens'] << filename
1218 $save.sensitive = $save_as.sensitive = $generate.sensitive = true
1223 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
1225 Gtk::FileChooser::ACTION_OPEN,
1227 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1228 fc.add_shortcut_folder(File.expand_path("~/.booh-gui-files"))
1229 fc.set_current_folder(File.expand_path("~/.booh-gui-files"))
1230 fc.transient_for = $main_window
1233 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1234 push_mousecursor_wait(fc)
1235 msg = open_file(from_utf8(fc.filename))
1250 def additional_booh_options
1253 options += "--mproc #{$config['mproc'].to_i} "
1259 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
1261 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1262 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1263 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1265 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
1266 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
1267 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1268 tbl.attach(src = Gtk::Entry.new,
1269 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1270 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
1271 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1272 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
1273 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1274 tbl.attach(dest = Gtk::Entry.new,
1275 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1276 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
1277 2, 3, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1278 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
1279 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1280 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
1281 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1282 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
1283 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1285 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(tbl = Gtk::Table.new(0, 0, false))
1286 tbl.attach(Gtk::Label.new(utf8(_("Theme: "))),
1287 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1288 tbl.attach(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'),
1289 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1291 src_browse.signal_connect('clicked') {
1292 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
1294 Gtk::FileChooser::ACTION_SELECT_FOLDER,
1296 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1297 fc.transient_for = $main_window
1298 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1299 src.text = fc.filename
1300 conf.text = File.expand_path("~/.booh-gui-files/#{File.basename(src.text)}")
1305 dest_browse.signal_connect('clicked') {
1306 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
1308 Gtk::FileChooser::ACTION_CREATE_FOLDER,
1310 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1311 fc.transient_for = $main_window
1312 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1313 dest.text = fc.filename
1318 conf_browse.signal_connect('clicked') {
1319 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
1321 Gtk::FileChooser::ACTION_SAVE,
1323 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1324 fc.transient_for = $main_window
1325 fc.add_shortcut_folder(File.expand_path("~/.booh-gui-files"))
1326 fc.set_current_folder(File.expand_path("~/.booh-gui-files"))
1327 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1328 conf.text = fc.filename
1333 theme_button.signal_connect('clicked') {
1334 if newtheme = theme_choose(theme_button.label)
1335 theme_button.label = newtheme
1339 dialog.vbox.add(frame1)
1340 dialog.vbox.add(frame2)
1341 dialog.window_position = Gtk::Window::POS_MOUSE
1347 dialog.run { |response|
1348 if response == Gtk::Dialog::RESPONSE_OK
1349 srcdir = from_utf8(src.text)
1350 destdir = from_utf8(dest.text)
1351 if !File.directory?(srcdir)
1352 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
1354 elsif conf.text == ''
1355 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
1357 elsif File.directory?(destdir)
1358 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
1360 elsif File.exists?(destdir)
1361 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
1364 system("mkdir '#{destdir}'")
1365 if !File.directory?(destdir)
1366 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
1377 srcdir = from_utf8(src.text)
1378 destdir = from_utf8(dest.text)
1379 configskel = from_utf8(conf.text)
1380 theme = theme_button.label
1384 button, w8 = wait_message($main_window, utf8(_("Please wait while scanning source directory...")))
1385 backend = Thread.new {
1386 cmd = "booh --source #{srcdir} --destination #{destdir} --config-skel #{configskel} --for-gui " +
1387 "--verbose-level #{$verbose_level} --theme #{theme} #{additional_booh_options}"
1391 open_file(configskel)
1393 button.signal_connect('clicked') {
1394 Thread.kill(backend)
1400 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
1402 Gtk::FileChooser::ACTION_SAVE,
1404 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1405 fc.transient_for = $main_window
1406 fc.add_shortcut_folder(File.expand_path("~/.booh-gui-files"))
1407 fc.set_current_folder(File.expand_path("~/.booh-gui-files"))
1408 fc.filename = $filename
1409 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1410 $filename = from_utf8(fc.filename)
1417 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
1419 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1420 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1421 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1423 dialog.vbox.add(notebook = Gtk::Notebook.new)
1424 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
1425 tbl.attach(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing"))),
1426 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1427 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),
1428 1, 2, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1429 smp_check.signal_connect('toggled') {
1430 if smp_check.active?
1431 smp_hbox.sensitive = true
1433 smp_hbox.sensitive = false
1437 smp_check.active = true
1438 smp_spin.value = $config['mproc'].to_i
1441 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
1442 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
1443 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1444 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
1445 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1447 dialog.vbox.show_all
1448 dialog.run { |response|
1449 if response == Gtk::Dialog::RESPONSE_OK
1450 if smp_check.active?
1451 $config['mproc'] = smp_spin.value.to_i
1453 $config.delete('mproc')
1456 $config['convert-enhance'] = enhance_entry.text
1462 def create_menu_and_toolbar
1465 mb = Gtk::MenuBar.new
1467 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
1468 filesubmenu = Gtk::Menu.new
1469 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
1470 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
1471 filesubmenu.append( Gtk::SeparatorMenuItem.new)
1472 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
1473 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
1474 filesubmenu.append( Gtk::SeparatorMenuItem.new)
1475 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
1476 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
1477 filesubmenu.append( Gtk::SeparatorMenuItem.new)
1478 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
1479 filemenu.set_submenu(filesubmenu)
1482 new.signal_connect('activate') { new_album }
1483 open.signal_connect('activate') { open_file_popup }
1484 $save.signal_connect('activate') { save_current_file }
1485 $save_as.signal_connect('activate') { save_as_do }
1486 $generate.signal_connect('activate') {
1487 button, w8 = wait_message($main_window, utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")))
1488 backend = Thread.new {
1490 cmd = "booh --config #{$filename} --verbose-level #{$verbose_level} #{additional_booh_options}"
1494 show_popup($main_window, utf8(_("Your web-album is now ready in directory `%s'.") % current_dest_dir ))
1496 button.signal_connect('clicked') {
1497 Thread.kill(backend)
1501 quit.signal_connect('activate') { try_quit }
1503 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
1504 editsubmenu = Gtk::Menu.new
1505 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
1506 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
1507 editsubmenu.append( Gtk::SeparatorMenuItem.new)
1508 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
1509 editmenu.set_submenu(editsubmenu)
1512 prefs.signal_connect('activate') { preferences }
1514 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
1515 helpsubmenu = Gtk::Menu.new
1516 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
1517 helpmenu.set_submenu(helpsubmenu)
1520 about.signal_connect('activate') {
1521 show_popup($main_window, utf8(_("<span size='x-large' weight='bold'>Booh %s</span>
1523 <i>``The Web-Album of choice for discriminating Linux users''</i>
1525 Copyright (c) 2005 Guillaume Cottenceau") % $VERSION), { :centered => true, :pos_centered => true })
1530 tb = Gtk::Toolbar.new
1532 tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
1533 open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
1534 open.menu = Gtk::Menu.new
1535 open.signal_connect('clicked') { open_file_popup }
1536 open.signal_connect('show-menu') {
1537 lastopens = Gtk::Menu.new
1539 if $config['last-opens']
1540 $config['last-opens'].reverse.each { |e|
1541 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
1542 item.signal_connect('activate') {
1543 push_mousecursor_wait
1547 show_popup($main_window, msg)
1554 open.menu = lastopens
1557 tb.insert(-1, Gtk::SeparatorToolItem.new)
1559 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
1560 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
1561 $r90.label = utf8(_("Rotate"))
1562 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
1563 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
1564 $r270.label = utf8(_("Rotate"))
1565 tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
1566 $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1567 $enhance.label = utf8(_("Enhance"))
1568 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
1569 $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
1570 tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
1571 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
1572 nothing.label = utf8(_("None"))
1574 tb.insert(-1, Gtk::SeparatorToolItem.new)
1576 tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
1577 tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
1579 perform_undo = Proc.new {
1580 $redo_tb.sensitive = $redo_mb.sensitive = true
1581 if not more_undoes = UndoHandler.undo($statusbar)
1582 $undo_tb.sensitive = $undo_mb.sensitive = false
1585 perform_redo = Proc.new {
1586 $undo_tb.sensitive = $undo_mb.sensitive = true
1587 if not more_redoes = UndoHandler.redo($statusbar)
1588 $redo_tb.sensitive = $redo_mb.sensitive = false
1592 $undo_tb.signal_connect('clicked') { perform_undo.call }
1593 $undo_mb.signal_connect('activate') { perform_undo.call }
1594 $redo_tb.signal_connect('clicked') { perform_redo.call }
1595 $redo_mb.signal_connect('activate') { perform_redo.call }
1597 one_click_explain_try = Proc.new {
1598 if !$config['one-click-explained']
1599 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
1601 You have just clicked on a One-Click tool. When such a tool is activated
1602 (<span foreground='darkblue'>Rotate clockwise</span>, <span foreground='darkblue'>Rotate counter-clockwise</span>, or <span foreground='darkblue'>Delete</span>), clicking on a
1603 thumbnail will immediately apply the desired action.
1605 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
1607 $config['one-click-explained'] = true
1611 $r90.signal_connect('toggled') {
1613 set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
1614 one_click_explain_try.call
1615 $r270.active = false
1616 $enhance.active = false
1617 $delete.active = false
1618 nothing.sensitive = true
1620 if !$r270.active? && !$enhance.active? && !$delete.active?
1621 set_mousecursor_normal
1622 nothing.sensitive = false
1624 nothing.sensitive = true
1628 $r270.signal_connect('toggled') {
1630 set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
1631 one_click_explain_try.call
1633 $enhance.active = false
1634 $delete.active = false
1635 nothing.sensitive = true
1637 if !$r90.active? && !$enhance.active? && !$delete.active?
1638 set_mousecursor_normal
1639 nothing.sensitive = false
1641 nothing.sensitive = true
1645 $enhance.signal_connect('toggled') {
1647 set_mousecursor(Gdk::Cursor::SPRAYCAN)
1648 one_click_explain_try.call
1650 $r270.active = false
1651 $delete.active = false
1652 nothing.sensitive = true
1654 if !$r90.active? && !$r270.active? && !$delete.active?
1655 set_mousecursor_normal
1656 nothing.sensitive = false
1658 nothing.sensitive = true
1662 $delete.signal_connect('toggled') {
1664 set_mousecursor(Gdk::Cursor::PIRATE)
1665 one_click_explain_try.call
1667 $r270.active = false
1668 $enhance.active = false
1669 nothing.sensitive = true
1671 if !$r90.active? && !$r270.active? && !$enhance.active?
1672 set_mousecursor_normal
1673 nothing.sensitive = false
1675 nothing.sensitive = true
1679 nothing.signal_connect('clicked') {
1680 $r90.active = $r270.active = $enhance.active = $delete.active = false
1681 set_mousecursor_normal
1687 def create_main_window
1689 mb, tb = create_menu_and_toolbar
1691 # open_file('/home/gc/booh/foo')
1693 $albums_tv = Gtk::TreeView.new
1694 $albums_tv.set_size_request(120, -1)
1695 renderer = Gtk::CellRendererText.new
1696 column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 })
1697 $albums_tv.append_column(column)
1698 $albums_tv.set_headers_visible(false)
1699 $albums_tv.selection.signal_connect('changed') { |w|
1700 push_mousecursor_wait
1704 msg 3, "no selection"
1706 $current_path = $albums_ts.get_value(iter, 1)
1711 $albums_ts = Gtk::TreeStore.new(String, String)
1712 $albums_tv.set_model($albums_ts)
1713 $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
1715 $notebook = Gtk::Notebook.new
1716 create_subalbums_page
1717 $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
1719 $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
1721 $notebook.signal_connect('switch-page') { |w, page, num|
1723 $delete.active = false
1724 $delete.sensitive = false
1726 $delete.sensitive = true
1730 paned = Gtk::HPaned.new
1731 paned.pack1($albums_tv, false, false)
1732 paned.pack2($notebook, true, true)
1734 main_vbox = Gtk::VBox.new(false, 0)
1735 main_vbox.pack_start(mb, false, false)
1736 main_vbox.pack_start(tb, false, false)
1737 main_vbox.pack_start(paned, true, true)
1738 main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
1740 $main_window = Gtk::Window.new
1741 $main_window.add(main_vbox)
1742 $main_window.signal_connect('delete-event') {
1746 #- read/save size and position of window
1747 if $config['pos-x'] && $config['pos-y']
1748 $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
1750 $main_window.window_position = Gtk::Window::POS_CENTER
1752 msg 3, "size: #{$config['width']}x#{$config['height']}"
1753 $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
1754 $main_window.signal_connect('configure-event') {
1755 msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
1756 x, y = $main_window.window.root_origin
1757 width, height = $main_window.window.size
1758 $config['pos-x'] = x
1759 $config['pos-y'] = y
1760 $config['width'] = width
1761 $config['height'] = height
1765 $statusbar.push(0, utf8(_("Ready.")))
1766 $main_window.show_all
1769 Thread.abort_on_exception = true