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/gtkadds'
27 require 'booh/GtkAutoTable'
31 bindtextdomain("booh")
33 require 'rexml/document'
36 require 'booh/booh-lib'
38 require 'booh/UndoHandler'
43 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
45 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
49 puts _("Usage: %s [OPTION]...") % File.basename($0)
51 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
56 parser = GetoptLong.new
57 parser.set_options(*$options.collect { |ary| ary[0..2] })
59 parser.each_option do |name, arg|
65 when '--verbose-level'
66 $verbose_level = arg.to_i
79 $config_file = File.expand_path('~/.booh-gui-rc')
80 if File.readable?($config_file)
81 $xmldoc = REXML::Document.new(File.new($config_file))
82 $xmldoc.root.elements.each { |element|
83 txt = element.get_text
85 if txt.value =~ /~~~/ || element.name == 'last-opens'
86 $config[element.name] = txt.value.split(/~~~/)
88 $config[element.name] = txt.value
91 $config[element.name] = {}
94 $config[element.name][chld.name] = txt ? txt.value : nil
99 $config['video-viewer'] ||= 'mplayer %f'
100 if !FileTest.directory?(File.expand_path('~/.booh'))
101 system("mkdir ~/.booh")
106 if $config['last-opens'] && $config['last-opens'].size > 5
107 $config['last-opens'] = $config['last-opens'][-5, 5]
110 ios = File.open($config_file, "w")
111 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
112 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
113 $config.each_pair { |key, value|
114 elem = $xmldoc.root.add_element key
116 $config[key].each_pair { |subkey, subvalue|
117 subelem = elem.add_element subkey
118 subelem.add_text subvalue.to_s
120 elsif value.is_a? Array
121 elem.add_text value.join('~~~')
123 elem.add_text value.to_s
126 $xmldoc.write(ios, 0)
130 def set_mousecursor(what, *widget)
131 if widget[0] && widget[0].window
132 widget[0].window.set_cursor(Gdk::Cursor.new(what))
134 if $main_window.window
135 $main_window.window.set_cursor(Gdk::Cursor.new(what))
137 $current_cursor = what
139 def set_mousecursor_wait(*widget)
140 set_mousecursor(Gdk::Cursor::WATCH, *widget)
141 if Thread.current == Thread.main
142 Gtk.main_iteration while Gtk.events_pending?
145 def set_mousecursor_normal(*widget)
146 set_mousecursor(Gdk::Cursor::LEFT_PTR, *widget)
148 def push_mousecursor_wait(*widget)
149 if $current_cursor != Gdk::Cursor::WATCH
150 $save_cursor = $current_cursor
151 set_mousecursor_wait(*widget)
154 def pop_mousecursor(*widget)
155 set_mousecursor($save_cursor || Gdk::Cursor::LEFT_PTR, *widget)
159 source = $xmldoc.root.attributes['source']
160 dest = $xmldoc.root.attributes['destination']
161 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
164 def build_full_dest_filename(filename)
165 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
168 def save_undo(name, closure, *params)
169 UndoHandler.save_undo(name, closure, [ *params ])
170 $undo_tb.sensitive = $undo_mb.sensitive = true
173 def view_element(filename)
174 if entry2type(filename) == 'video'
175 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'")
183 msg 3, "filename: #{filename}"
184 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
185 #- typically this file won't exist in case of videos; try with the largest thumbnail around
186 if !File.exists?(dest_img)
187 if entry2type(filename) == 'video'
188 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
189 if not alternatives.empty?
190 dest_img = alternatives[-1]
193 push_mousecursor_wait
194 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
196 if !File.exists?(dest_img)
197 msg 2, _("Could not generate fullscreen thumbnail!")
202 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)))
204 tooltips = Gtk::Tooltips.new
205 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
207 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
208 b.signal_connect('clicked') { w.destroy }
211 vb.pack_start(evt, false, false)
212 vb.pack_end(bottom, false, false)
215 w.signal_connect('delete-event') { w.destroy }
216 w.window_position = Gtk::Window::POS_CENTER
220 def create_editzone(scrolledwindow, pagenum, image)
221 frame = Gtk::Frame.new
222 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
223 frame.set_shadow_type(Gtk::SHADOW_IN)
224 textview.signal_connect('key-press-event') { |w, event|
225 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
226 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
227 scrolledwindow.signal_emit('key-press-event', event)
229 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
230 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
231 if event.keyval == Gdk::Keyval::GDK_Up
232 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
233 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
235 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
238 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
239 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
241 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
247 textview.signal_connect('focus-in-event') { |w, event|
248 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
252 candidate_undo_text = nil
253 textview.signal_connect('focus-in-event') { |w, event|
254 candidate_undo_text = textview.buffer.text
257 textview.signal_connect('key-release-event') { |w, event|
258 if candidate_undo_text && candidate_undo_text != textview.buffer.text
260 save_undo(_("text edit"),
262 save_text = textview.buffer.text
263 textview.buffer.text = text
265 $notebook.set_page(pagenum)
267 textview.buffer.text = save_text
269 $notebook.set_page(pagenum)
271 }, candidate_undo_text)
272 candidate_undo_text = nil
275 if ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
276 #- autoscroll if cursor or image is not visible
277 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
278 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
279 current_miny_visible = scrolledwindow.vadjustment.value
280 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
281 if ypos_top < current_miny_visible
282 newval = scrolledwindow.vadjustment.value -
283 ((current_miny_visible - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
284 if newval < scrolledwindow.vadjustment.lower
285 newval = scrolledwindow.vadjustment.lower
287 scrolledwindow.vadjustment.value = newval
288 elsif ypos_bottom > current_maxy_visible
289 newval = scrolledwindow.vadjustment.value +
290 ((ypos_bottom - current_maxy_visible - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
291 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
292 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
294 scrolledwindow.vadjustment.value = newval
300 return [ frame, textview ]
303 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
305 if !$modified_pixbufs[thumbnail_img]
306 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
307 elsif !$modified_pixbufs[thumbnail_img][:orig]
308 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
311 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
314 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
315 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
316 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
317 if pixbuf.height > desired_y
318 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
319 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
320 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
325 if $modified_pixbufs[thumbnail_img][:whitebalance]
326 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
329 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
332 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
335 #- update rotate attribute
336 xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
338 $modified_pixbufs[thumbnail_img] ||= {}
339 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
340 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
342 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
345 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
348 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
350 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
352 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
353 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
355 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
356 $notebook.set_page(0)
357 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
362 def color_swap(xmldir, attributes_prefix)
364 if xmldir.attributes["#{attributes_prefix}color-swap"]
365 xmldir.delete_attribute("#{attributes_prefix}color-swap")
367 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
371 def enhance(xmldir, attributes_prefix)
373 if xmldir.attributes["#{attributes_prefix}enhance"]
374 xmldir.delete_attribute("#{attributes_prefix}enhance")
376 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
380 def change_frame_offset(xmldir, attributes_prefix, value)
382 xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
385 def ask_new_frame_offset(xmldir, attributes_prefix)
386 value = xmldir.attributes["#{attributes_prefix}frame-offset"]
388 dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
390 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
391 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
392 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
396 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
397 from. There are approximately 25 frames per second in a video.
400 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
401 entry.signal_connect('key-press-event') { |w, event|
402 if event.keyval == Gdk::Keyval::GDK_Return
403 dialog.response(Gtk::Dialog::RESPONSE_OK)
405 elsif event.keyval == Gdk::Keyval::GDK_Escape
406 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
409 false #- propagate if needed
413 dialog.window_position = Gtk::Window::POS_MOUSE
416 dialog.run { |response|
419 if response == Gtk::Dialog::RESPONSE_OK
421 msg 3, "changing frame offset top #{newval}"
422 return { :old => value, :new => newval }
429 def change_whitebalance(xmlelem, attributes_prefix, value)
431 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
434 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
436 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
437 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
438 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
439 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
440 destfile = "#{thumbnail_img}-orig-whitebalance.jpg"
441 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
442 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
443 $modified_pixbufs[thumbnail_img] ||= {}
444 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
445 system("rm -f '#{destfile}'")
446 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
447 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
448 if entry2type(orig) == 'video'
449 #- cleanup temp for videos
450 system("rm -f #{current_dest_dir}/screenshot.jpg000000.jpg")
454 $modified_pixbufs[thumbnail_img] ||= {}
455 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
457 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
460 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
461 #- init $modified_pixbufs correctly
462 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
464 value = xmlelem.attributes["#{attributes_prefix}white-balance"] || "0"
466 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
468 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
469 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
470 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
474 _("You can fix the <b>white balance</b> of the image, if your image is too blue
475 or too yellow because your camera didn't detect the light correctly. Drag the
476 slider below the image to the left for more blue, to the right for more yellow.
479 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
480 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
482 dialog.window_position = Gtk::Window::POS_MOUSE
487 timeout = Gtk.timeout_add(100) {
488 if hs.value != lastval
493 thread = Thread.new {
494 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
500 dialog.run { |response|
501 Gtk.timeout_remove(timeout)
502 if response == Gtk::Dialog::RESPONSE_OK
504 newval = hs.value.to_s
505 msg 3, "changing white balance to #{newval}"
507 return { :old => value, :new => newval }
509 $modified_pixbufs[thumbnail_img] ||= {}
510 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
517 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
518 system("rm -f '#{destfile}'")
519 #- type can be 'element' or 'subdir'
521 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
523 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
527 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
529 push_mousecursor_wait
530 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
532 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
533 if entry2type(origfile) == 'video'
534 #- cleanup temp for videos
535 system("rm -f #{current_dest_dir}/screenshot.jpg000000.jpg")
541 def popup_thumbnail_menu(event, optionals, type, xmldir, attributes_prefix, closures)
543 if optionals.include?('change_image')
544 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
545 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
546 changeimg.signal_connect('activate') { closures[:change].call }
547 menu.append( Gtk::SeparatorMenuItem.new)
549 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
550 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
551 r90.signal_connect('activate') { closures[:rotate].call(90) }
552 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
553 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
554 r270.signal_connect('activate') { closures[:rotate].call(-90) }
555 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
556 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
557 whitebalance.signal_connect('activate') { closures[:whitebalance].call }
559 menu.append( Gtk::SeparatorMenuItem.new)
560 menu.append( color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
561 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
562 color_swap.signal_connect('activate') { closures[:color_swap].call }
563 menu.append( flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
564 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
565 flip.signal_connect('activate') { closures[:rotate].call(180) }
566 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
567 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
568 frame_offset.signal_connect('activate') { closures[:frame_offset].call }
570 menu.append( Gtk::SeparatorMenuItem.new)
571 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
572 _("Enhance constrast"))))
573 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
574 enhance.signal_connect('activate') { closures[:enhance].call }
575 if optionals.include?('delete')
576 menu.append( Gtk::SeparatorMenuItem.new)
577 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
578 delete_item.signal_connect('activate') { closures[:delete].call }
581 menu.popup(nil, nil, event.button, event.time)
584 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
587 frame1 = Gtk::Frame.new
589 my_gen_real_thumbnail = proc {
590 gen_real_thumbnail('element', from_utf8("#{$current_path}/#{filename}"), thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
593 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
594 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
595 frame1.add(img = Gtk::Image.new)
596 my_gen_real_thumbnail.call
598 frame1.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
600 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
602 tooltips = Gtk::Tooltips.new
603 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
604 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(from_utf8("#{$current_path}/#{filename}"))/1024)]) : tipname), nil)
606 frame2, textview = create_editzone($autotable_sw, 1, img)
607 textview.buffer.text = utf8(caption)
608 textview.set_justification(Gtk::Justification::CENTER)
610 vbox = Gtk::VBox.new(false, 5)
611 vbox.pack_start(evtbox, false, false)
612 vbox.pack_start(frame2, false, false)
613 autotable.append(vbox, filename)
615 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
616 $vbox2textview[vbox] = textview
618 #- to be able to find widgets by name
619 $name2widgets[filename] = { :textview => textview }
621 cleanup_all_thumbnails = Proc.new {
622 #- remove out of sync images
623 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
624 for sizeobj in $images_size
625 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
630 rotate_and_cleanup = Proc.new { |angle|
631 rotate(angle, thumbnail_img, img, $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
632 cleanup_all_thumbnails.call
635 color_swap_and_cleanup = Proc.new {
636 perform_color_swap_and_cleanup = Proc.new {
637 color_swap($xmldir.elements["[@filename='#{filename}']"], '')
638 my_gen_real_thumbnail.call
641 cleanup_all_thumbnails.call
642 perform_color_swap_and_cleanup.call
644 save_undo(_("color swap"),
646 perform_color_swap_and_cleanup.call
647 $notebook.set_page(1)
649 perform_color_swap_and_cleanup.call
650 $notebook.set_page(1)
655 change_frame_offset_and_cleanup = Proc.new {
656 if values = ask_new_frame_offset($xmldir.elements["[@filename='#{filename}']"], '')
657 perform_change_frame_offset_and_cleanup = Proc.new { |val|
658 change_frame_offset($xmldir.elements["[@filename='#{filename}']"], '', val)
659 my_gen_real_thumbnail.call
661 perform_change_frame_offset_and_cleanup.call(values[:new])
663 save_undo(_("specify frame offset"),
665 perform_change_frame_offset_and_cleanup.call(values[:old])
666 $notebook.set_page(1)
668 perform_change_frame_offset_and_cleanup.call(values[:new])
669 $notebook.set_page(1)
675 whitebalance_and_cleanup = Proc.new {
676 if values = ask_whitebalance(from_utf8("#{$current_path}/#{filename}"), thumbnail_img, img,
677 $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
678 perform_change_whitebalance_and_cleanup = Proc.new { |val|
679 change_whitebalance($xmldir.elements["[@filename='#{filename}']"], '', val)
680 recalc_whitebalance(val, from_utf8("#{$current_path}/#{filename}"), thumbnail_img, img,
681 $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
682 cleanup_all_thumbnails.call
684 perform_change_whitebalance_and_cleanup.call(values[:new])
686 save_undo(_("fix white balance"),
688 perform_change_whitebalance_and_cleanup.call(values[:old])
689 $notebook.set_page(1)
691 perform_change_whitebalance_and_cleanup.call(values[:new])
692 $notebook.set_page(1)
698 enhance_and_cleanup = Proc.new {
699 perform_enhance_and_cleanup = Proc.new {
700 enhance($xmldir.elements["[@filename='#{filename}']"], '')
701 my_gen_real_thumbnail.call
704 cleanup_all_thumbnails.call
705 perform_enhance_and_cleanup.call
707 save_undo(_("enhance"),
709 perform_enhance_and_cleanup.call
710 $notebook.set_page(1)
712 perform_enhance_and_cleanup.call
713 $notebook.set_page(1)
719 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed?")), { :okcancel => true })
722 perform_delete = Proc.new {
723 after = autotable.get_next_widget(vbox)
725 after = autotable.get_previous_widget(vbox)
727 autotable.remove(vbox)
729 $vbox2textview[after].grab_focus
736 if $xmldir.elements['dir']
737 $xmldir.delete_attribute('thumbnails-caption')
738 $xmldir.delete_attribute('thumbnails-captionfile')
742 save_changes('forced')
743 populate_subalbums_treeview
745 save_undo(_("delete"),
747 autotable.reinsert(pos, vbox, filename)
748 $notebook.set_page(1)
751 $notebook.set_page(1)
753 }, autotable.get_current_number(vbox))
758 textview.signal_connect('key-press-event') { |w, event|
761 x, y = autotable.get_current_pos(vbox)
762 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
763 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
764 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
765 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
767 $vbox2textview[autotable.get_widget_at_pos(x, y - 1)].grab_focus
770 autotable.move_up(vbox)
771 textview.grab_focus #- because if moving, focus is stolen
774 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
776 $vbox2textview[autotable.get_widget_at_pos(x, y + 1)].grab_focus
779 autotable.move_down(vbox)
780 textview.grab_focus #- because if moving, focus is stolen
783 if event.keyval == Gdk::Keyval::GDK_Left
784 previous = autotable.get_previous_widget(vbox)
785 if previous && autotable.get_current_pos(previous)[0] < x
787 $vbox2textview[previous].grab_focus
790 autotable.move_left(vbox)
791 textview.grab_focus #- because if moving, focus is stolen
795 rotate_and_cleanup.call(-90)
798 if event.keyval == Gdk::Keyval::GDK_Right
799 next_ = autotable.get_next_widget(vbox)
800 if next_ && autotable.get_current_pos(next_)[0] > x
802 $vbox2textview[next_].grab_focus
805 autotable.move_right(vbox)
806 textview.grab_focus #- because if moving, focus is stolen
810 rotate_and_cleanup.call(90)
813 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
816 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
817 view_element(filename)
821 !propagate #- propagate if needed
824 evtbox.signal_connect('button-press-event') { |w, event|
826 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
827 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
829 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
831 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
832 elsif $enhance.active?
833 enhance_and_cleanup.call
834 elsif $delete.active?
840 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
841 popup_thumbnail_menu(event, ['delete'], type, $xmldir.elements["[@filename='#{filename}']"], '',
842 { :rotate => rotate_and_cleanup, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
843 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup })
845 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
846 view_element(filename)
848 retval = false #- propagate
853 vbox.signal_connect('button-press-event') { |w, event|
854 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
855 $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
859 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
860 if $gesture_press && $gesture_press[:filename] == filename
861 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
862 angle = x-$gesture_press[:x] > 0 ? 90 : -90
863 msg 3, "gesture rotate: #{angle}"
864 rotate_and_cleanup.call(angle)
870 #- handle reordering with drag and drop
871 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
872 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
873 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
874 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
876 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
877 ctxt.targets.each { |target|
878 if target.name == 'reorder-elements'
879 from, to = selection_data.data.to_i, autotable.get_current_number(vbox)
882 autotable.move(from, to)
883 save_undo(_("reorder"),
884 Proc.new { |from, to|
886 autotable.move(to - 1, from)
888 autotable.move(to, from + 1)
890 $notebook.set_page(1)
892 autotable.move(from, to)
893 $notebook.set_page(1)
904 def create_auto_table
906 $autotable = Gtk::AutoTable.new(5)
908 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
909 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
910 $autotable_sw.add_with_viewport($autotable)
913 def create_subalbums_page
915 subalbums_hb = Gtk::HBox.new
916 $subalbums_vb = Gtk::VBox.new(false, 5)
917 subalbums_hb.pack_start($subalbums_vb, false, false)
918 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
919 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
920 $subalbums_sw.add_with_viewport(subalbums_hb)
923 def save_current_file
926 ios = File.open($filename, "w")
927 $xmldoc.write(ios, 0)
935 dialog = Gtk::Dialog.new(utf8(_("Save before quitting?")),
937 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
938 [Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
939 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
940 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
941 dialog.vbox.add(Gtk::Label.new(utf8(_("Do you want to save your changes before quitting?"))))
942 dialog.window_position = Gtk::Window::POS_CENTER
945 dialog.run { |response|
947 if response == Gtk::Dialog::RESPONSE_CANCEL
949 elsif response == Gtk::Dialog::RESPONSE_YES
958 def show_popup(parent, msg, *options)
959 dialog = Gtk::Dialog.new
960 dialog.title = utf8(_("Booh message"))
963 if options[0] && options[0][:centered]
964 lbl.set_justify(Gtk::Justification::CENTER)
967 if options[0] && options[0][:okcancel]
968 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
970 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
972 dialog.transient_for = parent
973 dialog.set_default_size(200, 120)
974 if options[0] && options[0][:pos_centered]
975 dialog.window_position = Gtk::Window::POS_CENTER
977 dialog.window_position = Gtk::Window::POS_MOUSE
981 dialog.run { |response|
983 if options[0] && options[0][:okcancel]
984 return response == Gtk::Dialog::RESPONSE_OK
989 def wait_message(parent, msg)
991 w.set_transient_for(parent)
994 vb = Gtk::VBox.new(false, 5)
995 vb.set_border_width(5)
996 vb.pack_start(Gtk::Label.new(msg), false, false)
997 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
998 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
999 vb.pack_start(pb = Gtk::ProgressBar.new.set_pulse_step(0.05), false, false)
1000 vb.pack_start(Gtk::HSeparator.new, false, false)
1001 vb.pack_end(bottom, false, false)
1003 timeout = Gtk.timeout_add(200) { pb.pulse }
1006 w.signal_connect('delete-event') { w.destroy }
1007 w.signal_connect('destroy') { Gtk.timeout_remove(timeout) }
1008 w.window_position = Gtk::Window::POS_CENTER
1014 def perform_in_background(cmd, waitmsg, params)
1015 button, w8 = wait_message($main_window, waitmsg)
1020 id, exitstatus = Process.waitpid2(pid)
1023 if params[:successmsg]
1024 show_popup($main_window, params[:successmsg])
1026 if params[:closure_after]
1027 params[:closure_after].call
1029 elsif exitstatus == 15
1030 #- say nothing, user aborted
1032 if params[:failuremsg]
1033 show_popup($main_window, params[:failuremsg])
1040 button.signal_connect('clicked') {
1041 Process.kill('SIGTERM', pid)
1045 def save_changes(*forced)
1046 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1050 if $xmldir.elements['dir']
1051 $xmldir.add_attribute('subdirs-caption', $subalbums_title.buffer.text)
1052 $xmldir.elements.each('dir') { |element|
1053 path = element.attributes['path']
1054 if element.attributes['subdirs-caption']
1055 element.add_attribute('subdirs-caption', $subalbums_edits[path][:editzone].buffer.text)
1056 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1058 element.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1059 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1062 if $xmldir.attributes['thumbnails-caption']
1063 path = $xmldir.attributes['path']
1064 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1068 #- remove and reinsert elements to reflect new ordering
1069 save_attributes = {}
1072 $xmldir.elements.each { |element|
1073 if element.name == 'image' || element.name == 'video'
1074 save_types[element.attributes['filename']] = element.name
1075 save_attributes[element.attributes['filename']] = element.attributes
1080 $autotable.current_order.each { |path|
1081 chld = $xmldir.add_element save_types[path], save_attributes[path]
1082 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1091 $undo_tb.sensitive = $undo_mb.sensitive = false
1092 $redo_tb.sensitive = $redo_mb.sensitive = false
1098 $subalbums_vb.children.each { |chld|
1099 $subalbums_vb.remove(chld)
1101 $subalbums = Gtk::Table.new(0, 0, true)
1102 current_y_sub_albums = 0
1104 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
1105 $subalbums_edits = {}
1107 add_subalbum = Proc.new { |xmldir|
1108 if xmldir == $xmldir
1109 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
1110 caption = xmldir.attributes['thumbnails-caption']
1111 captionfile, dummy = find_subalbum_caption_info(xmldir)
1112 infotype = 'thumbnails'
1114 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
1115 captionfile, caption = find_subalbum_caption_info(xmldir)
1116 infotype = find_subalbum_info_type(xmldir)
1118 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
1119 hbox = Gtk::HBox.new
1120 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
1122 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
1125 my_gen_real_thumbnail = proc {
1126 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
1129 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
1130 f.add(img = Gtk::Image.new)
1131 my_gen_real_thumbnail.call
1133 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
1135 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
1136 $subalbums.attach(hbox,
1137 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1139 change_image = Proc.new {
1140 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
1142 Gtk::FileChooser::ACTION_OPEN,
1144 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1145 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
1146 fc.transient_for = $main_window
1147 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))
1148 f.add(preview_img = Gtk::Image.new)
1150 fc.signal_connect('update-preview') { |w|
1152 if fc.preview_filename
1153 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
1154 fc.preview_widget_active = true
1156 rescue Gdk::PixbufError
1157 fc.preview_widget_active = false
1160 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1162 old_file = captionfile
1163 old_rotate = xmldir.attributes["#{infotype}-rotate"]
1164 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
1165 old_enhance = xmldir.attributes["#{infotype}-enhance"]
1166 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
1168 new_file = fc.filename
1169 msg 3, "new captionfile is: #{fc.filename}"
1170 perform_changefile = Proc.new {
1171 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
1172 $modified_pixbufs.delete(thumbnail_file)
1173 xmldir.delete_attribute("#{infotype}-rotate")
1174 xmldir.delete_attribute("#{infotype}-color-swap")
1175 xmldir.delete_attribute("#{infotype}-enhance")
1176 xmldir.delete_attribute("#{infotype}-frame-offset")
1177 my_gen_real_thumbnail.call
1179 perform_changefile.call
1181 save_undo(_("change caption file for sub-album"),
1183 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
1184 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
1185 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
1186 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
1187 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
1188 my_gen_real_thumbnail.call
1189 $notebook.set_page(0)
1191 perform_changefile.call
1192 $notebook.set_page(0)
1199 rotate_and_cleanup = Proc.new { |angle|
1200 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
1201 system("rm -f '#{thumbnail_file}'")
1204 color_swap_and_cleanup = Proc.new {
1205 perform_color_swap_and_cleanup = Proc.new {
1206 color_swap(xmldir, "#{infotype}-")
1207 my_gen_real_thumbnail.call
1209 perform_color_swap_and_cleanup.call
1211 save_undo(_("color swap"),
1213 perform_color_swap_and_cleanup.call
1214 $notebook.set_page(0)
1216 perform_color_swap_and_cleanup.call
1217 $notebook.set_page(0)
1222 change_frame_offset_and_cleanup = Proc.new {
1223 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
1224 perform_change_frame_offset_and_cleanup = Proc.new { |val|
1225 change_frame_offset(xmldir, "#{infotype}-", val)
1226 my_gen_real_thumbnail.call
1228 perform_change_frame_offset_and_cleanup.call(values[:new])
1230 save_undo(_("specify frame offset"),
1232 perform_change_frame_offset_and_cleanup.call(values[:old])
1233 $notebook.set_page(0)
1235 perform_change_frame_offset_and_cleanup.call(values[:new])
1236 $notebook.set_page(0)
1242 whitebalance_and_cleanup = Proc.new {
1243 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
1244 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
1245 perform_change_whitebalance_and_cleanup = Proc.new { |val|
1246 change_whitebalance(xmldir, "#{infotype}-", val)
1247 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
1248 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
1249 system("rm -f '#{thumbnail_file}'")
1251 perform_change_whitebalance_and_cleanup.call(values[:new])
1253 save_undo(_("fix white balance"),
1255 perform_change_whitebalance_and_cleanup.call(values[:old])
1256 $notebook.set_page(0)
1258 perform_change_whitebalance_and_cleanup.call(values[:new])
1259 $notebook.set_page(0)
1265 enhance_and_cleanup = Proc.new {
1266 perform_enhance_and_cleanup = Proc.new {
1267 enhance(xmldir, "#{infotype}-")
1268 my_gen_real_thumbnail.call
1271 perform_enhance_and_cleanup.call
1273 save_undo(_("enhance"),
1275 perform_enhance_and_cleanup.call
1276 $notebook.set_page(0)
1278 perform_enhance_and_cleanup.call
1279 $notebook.set_page(0)
1284 evtbox.signal_connect('button-press-event') { |w, event|
1285 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1287 rotate_and_cleanup.call(90)
1289 rotate_and_cleanup.call(-90)
1290 elsif $enhance.active?
1291 enhance_and_cleanup.call
1294 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1295 popup_thumbnail_menu(event, ['change_image'], entry2type(captionfile), xmldir, "#{infotype}-",
1296 { :change => change_image, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
1297 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
1299 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1304 evtbox.signal_connect('button-press-event') { |w, event|
1305 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
1309 evtbox.signal_connect('button-release-event') { |w, event|
1310 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
1311 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
1312 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
1313 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
1314 msg 3, "gesture rotate: #{angle}"
1315 rotate_and_cleanup.call(angle)
1318 $gesture_press = nil
1321 frame, textview = create_editzone($subalbums_sw, 0, img)
1322 textview.buffer.text = caption
1323 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1324 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1326 $subalbums_edits[xmldir.attributes['path']] = { :editzone => textview, :captionfile => captionfile }
1327 current_y_sub_albums += 1
1330 if $xmldir.elements['dir']
1332 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
1333 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
1334 $subalbums_title.set_justification(Gtk::Justification::CENTER)
1335 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1336 #- this album image/caption
1337 if $xmldir.attributes['thumbnails-caption']
1338 add_subalbum.call($xmldir)
1341 $xmldir.elements.each { |element|
1342 if element.name == 'image' || element.name == 'video'
1343 #- element (image or video) of this album
1344 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
1345 msg 3, "dest_img: #{dest_img}"
1346 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
1348 if element.name == 'dir'
1349 #- sub-album image/caption
1350 add_subalbum.call(element)
1353 $subalbums_vb.add($subalbums)
1354 $subalbums_vb.show_all
1356 if !$xmldir.elements['image'] && !$xmldir.elements['video']
1357 $notebook.get_tab_label($autotable_sw).sensitive = false
1358 $notebook.set_page(0)
1360 $notebook.get_tab_label($autotable_sw).sensitive = true
1363 if !$xmldir.elements['dir']
1364 $notebook.get_tab_label($subalbums_sw).sensitive = false
1365 $notebook.set_page(1)
1367 $notebook.get_tab_label($subalbums_sw).sensitive = true
1371 def pixbuf_or_nil(filename)
1373 return Gdk::Pixbuf.new(filename)
1379 def theme_choose(current)
1380 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
1382 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1383 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1384 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1386 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
1387 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
1388 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
1389 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
1390 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
1391 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
1392 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
1393 treeview.signal_connect('button-press-event') { |w, event|
1394 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1395 dialog.response(Gtk::Dialog::RESPONSE_OK)
1399 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
1401 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
1404 iter[0] = File.basename(dir)
1405 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
1406 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
1407 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
1408 if File.basename(dir) == current
1409 treeview.selection.select_iter(iter)
1413 dialog.set_default_size(700, 400)
1414 dialog.vbox.show_all
1415 dialog.run { |response|
1416 iter = treeview.selection.selected
1418 if response == Gtk::Dialog::RESPONSE_OK && iter
1419 return model.get_value(iter, 0)
1425 def populate_subalbums_treeview
1428 $subalbums_vb.children.each { |chld|
1429 $subalbums_vb.remove(chld)
1432 source = $xmldoc.root.attributes['source']
1433 msg 3, "source: #{source}"
1435 xmldir = $xmldoc.elements["//dir[@path='#{source}']"]
1437 msg 1, _("Corrupted booh file...")
1441 append_dir_elem = Proc.new { |parent_iter, location|
1442 child_iter = $albums_ts.append(parent_iter)
1443 child_iter[0] = File.basename(location)
1444 child_iter[1] = location
1445 msg 3, "puttin location: #{location}"
1446 $xmldoc.elements.each("//dir[@path='#{location}']/dir") { |elem|
1447 append_dir_elem.call(child_iter, elem.attributes['path'])
1450 append_dir_elem.call(nil, source)
1452 $albums_tv.expand_all
1453 $albums_tv.selection.select_iter($albums_ts.iter_first)
1456 def open_file(filename)
1460 $current_path = nil #- invalidate
1461 $modified_pixbufs = {}
1464 $subalbums_vb.children.each { |chld|
1465 $subalbums_vb.remove(chld)
1468 if !File.exists?(filename)
1469 return utf8(_("File not found."))
1473 $xmldoc = REXML::Document.new File.new(filename)
1478 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
1479 if entry2type(filename).nil?
1480 return utf8(_("Not a booh file!"))
1482 return utf8(_("Not a booh file!\n\nHint: you cannot import directly an image or video with File/Open.\nUse File/New to create a new album."))
1486 if !source = $xmldoc.root.attributes['source']
1487 return utf8(_("Corrupted booh file..."))
1490 if !dest = $xmldoc.root.attributes['destination']
1491 return utf8(_("Corrupted booh file..."))
1494 if !theme = $xmldoc.root.attributes['theme']
1495 return utf8(_("Corrupted booh file..."))
1498 limit_sizes = $xmldoc.root.attributes['limit-sizes']
1500 $filename = filename
1501 select_theme(theme, limit_sizes)
1502 $default_size['thumbnails'] =~ /(.*)x(.*)/
1503 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
1504 $albums_thumbnail_size =~ /(.*)x(.*)/
1505 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
1507 populate_subalbums_treeview
1509 $config['last-opens'] ||= []
1510 if $config['last-opens'][-1] != utf8(filename)
1511 $config['last-opens'] << utf8(filename)
1513 $save.sensitive = $save_as.sensitive = $merge.sensitive = $merge_subalbums.sensitive = $generate.sensitive = $properties.sensitive = true
1518 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
1520 Gtk::FileChooser::ACTION_OPEN,
1522 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1523 fc.add_shortcut_folder(File.expand_path("~/.booh"))
1524 fc.set_current_folder(File.expand_path("~/.booh"))
1525 fc.transient_for = $main_window
1528 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1529 push_mousecursor_wait(fc)
1530 msg = open_file(fc.filename)
1545 def additional_booh_options
1548 options += "--mproc #{$config['mproc'].to_i} "
1554 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
1556 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1557 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1558 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1560 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
1561 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
1562 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1563 tbl.attach(src = Gtk::Entry.new,
1564 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1565 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
1566 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1567 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
1568 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1569 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
1570 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1571 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
1572 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1573 tbl.attach(dest = Gtk::Entry.new,
1574 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1575 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
1576 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1577 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
1578 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1579 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
1580 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
1581 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
1582 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1584 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(tbl = Gtk::Table.new(0, 0, false))
1585 tbl.attach(Gtk::Label.new(utf8(_("Theme: "))),
1586 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1587 tbl.attach(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'),
1588 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1589 tbl.attach(Gtk::Label.new(utf8(_("Sizes of images to generate: "))),
1590 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1591 tbl.attach(sizes = Gtk::HBox.new,
1592 1, 3, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1594 src_nb_calculated_for = ''
1596 process_src_nb = Proc.new {
1597 if src.text != src_nb_calculated_for
1598 src_nb_calculated_for = src.text
1600 Thread.kill(src_nb_thread)
1603 if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
1604 if File.readable?(from_utf8(src_nb_calculated_for))
1605 src_nb_thread = Thread.new {
1606 src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
1607 total = { 'image' => 0, 'video' => 0, nil => 0 }
1608 `find '#{from_utf8(src_nb_calculated_for)}' -type d`.each { |dir|
1609 if File.basename(dir) =~ /^\./
1613 Dir.entries(dir.chomp).each { |file|
1614 total[entry2type(file)] += 1
1616 rescue Errno::EACCES, Errno::ENOENT
1620 src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ]))
1624 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
1627 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
1632 timeout_src_nb = Gtk.timeout_add(100) {
1636 src_browse.signal_connect('clicked') {
1637 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
1639 Gtk::FileChooser::ACTION_SELECT_FOLDER,
1641 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1642 fc.transient_for = $main_window
1643 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1644 src.text = utf8(fc.filename)
1646 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
1651 dest_browse.signal_connect('clicked') {
1652 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
1654 Gtk::FileChooser::ACTION_CREATE_FOLDER,
1656 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1657 fc.transient_for = $main_window
1658 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1659 dest.text = utf8(fc.filename)
1664 conf_browse.signal_connect('clicked') {
1665 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
1667 Gtk::FileChooser::ACTION_SAVE,
1669 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1670 fc.transient_for = $main_window
1671 fc.add_shortcut_folder(File.expand_path("~/.booh"))
1672 fc.set_current_folder(File.expand_path("~/.booh"))
1673 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1674 conf.text = utf8(fc.filename)
1680 recreate_theme_config = proc {
1681 theme_sizes.each { |e| sizes.remove(e[:widget]) }
1683 select_theme(theme_button.label, 'all')
1684 $images_size.each { |s|
1685 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
1689 tooltips = Gtk::Tooltips.new
1690 tooltips.set_tip(cb, utf8(s['description']), nil)
1691 theme_sizes << { :widget => cb, :value => s['name'] }
1693 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
1694 tooltips = Gtk::Tooltips.new
1695 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
1696 theme_sizes << { :widget => cb, :value => 'original' }
1699 recreate_theme_config.call
1701 theme_button.signal_connect('clicked') {
1702 if newtheme = theme_choose(theme_button.label)
1703 theme_button.label = newtheme
1704 recreate_theme_config.call
1708 dialog.vbox.add(frame1)
1709 dialog.vbox.add(frame2)
1710 dialog.window_position = Gtk::Window::POS_MOUSE
1716 dialog.run { |response|
1717 if response == Gtk::Dialog::RESPONSE_OK
1718 srcdir = from_utf8(src.text)
1719 destdir = from_utf8(dest.text)
1720 if !File.directory?(srcdir)
1721 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
1723 elsif conf.text == ''
1724 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
1726 elsif destdir != make_dest_filename(destdir)
1727 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
1729 elsif File.directory?(destdir)
1730 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
1732 elsif File.exists?(destdir)
1733 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
1735 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
1736 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
1738 system("mkdir '#{destdir}'")
1739 if !File.directory?(destdir)
1740 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
1751 srcdir = from_utf8(src.text)
1752 destdir = from_utf8(dest.text)
1753 configskel = File.expand_path(from_utf8(conf.text))
1754 theme = theme_button.label
1755 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
1758 Thread.kill(src_nb_thread)
1760 Gtk.timeout_remove(timeout_src_nb)
1763 perform_in_background("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
1764 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} #{additional_booh_options}",
1765 utf8(_("Please wait while scanning source directory...")),
1766 { :closure_after => proc { open_file(configskel) } })
1771 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
1773 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1774 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1775 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1777 source = $xmldoc.root.attributes['source']
1778 dest = $xmldoc.root.attributes['destination']
1779 theme = $xmldoc.root.attributes['theme']
1780 limit_sizes = $xmldoc.root.attributes['limit-sizes']
1782 limit_sizes = limit_sizes.split(/,/)
1785 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
1786 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
1787 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1788 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
1789 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1790 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
1791 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1792 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>')),
1793 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1794 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
1795 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1796 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $filename + '</i>')),
1797 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
1799 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(tbl = Gtk::Table.new(0, 0, false))
1800 tbl.attach(Gtk::Label.new(utf8(_("Theme: "))),
1801 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1802 tbl.attach(theme_button = Gtk::Button.new(theme),
1803 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1804 tbl.attach(Gtk::Label.new(utf8(_("Sizes of images to generate: "))),
1805 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1806 tbl.attach(sizes = Gtk::HBox.new,
1807 1, 3, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1810 recreate_theme_config = proc {
1811 theme_sizes.each { |e| sizes.remove(e[:widget]) }
1813 select_theme(theme_button.label, 'all')
1814 $images_size.each { |s|
1815 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
1817 if limit_sizes.include?(s['name'])
1825 tooltips = Gtk::Tooltips.new
1826 tooltips.set_tip(cb, utf8(s['description']), nil)
1827 theme_sizes << { :widget => cb, :value => s['name'] }
1829 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
1830 tooltips = Gtk::Tooltips.new
1831 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
1832 if limit_sizes && limit_sizes.include?('original')
1835 theme_sizes << { :widget => cb, :value => 'original' }
1838 recreate_theme_config.call
1840 theme_button.signal_connect('clicked') {
1841 if newtheme = theme_choose(theme_button.label)
1843 theme_button.label = newtheme
1844 recreate_theme_config.call
1848 dialog.vbox.add(frame1)
1849 dialog.vbox.add(frame2)
1850 dialog.window_position = Gtk::Window::POS_MOUSE
1856 dialog.run { |response|
1857 if response == Gtk::Dialog::RESPONSE_OK
1858 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
1859 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
1868 save_theme = theme_button.label
1869 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
1872 if ok && (save_theme != theme || save_limit_sizes != limit_sizes)
1874 perform_in_background("booh-backend --use-config '#{$filename}' --for-gui " +
1875 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{save_limit_sizes.join(',')} #{additional_booh_options}",
1876 utf8(_("Please wait while scanning source directory...")),
1877 { :closure_after => proc { open_file($filename) } })
1884 theme = $xmldoc.root.attributes['theme']
1885 limit_sizes = $xmldoc.root.attributes['limit-sizes']
1887 limit_sizes = "--sizes #{limit_sizes}"
1889 perform_in_background("booh-backend --merge-config '#{$filename}' --for-gui " +
1890 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
1891 utf8(_("Please wait while scanning source directory...")),
1892 { :closure_after => proc { open_file($filename) } })
1898 perform_in_background("booh-backend --merge-config-newdirs '#{$filename}' --for-gui " +
1899 "--verbose-level #{$verbose_level} #{additional_booh_options}",
1900 utf8(_("Please wait while scanning source directory...")),
1901 { :closure_after => proc { open_file($filename) } })
1905 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
1907 Gtk::FileChooser::ACTION_SAVE,
1909 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1910 fc.transient_for = $main_window
1911 fc.add_shortcut_folder(File.expand_path("~/.booh"))
1912 fc.set_current_folder(File.expand_path("~/.booh"))
1913 fc.filename = $filename
1914 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1915 $filename = fc.filename
1922 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
1924 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1925 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1926 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1928 dialog.vbox.add(notebook = Gtk::Notebook.new)
1929 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
1930 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
1931 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1932 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
1933 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1934 tooltips = Gtk::Tooltips.new
1935 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename; for example: mplayer %f")), nil)
1936 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
1937 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1938 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(smp_hbox = Gtk::HBox.new.add(smp_spin = Gtk::SpinButton.new(2, 16, 1)).add(Gtk::Label.new(utf8(_("processors")))).set_sensitive(false)),
1939 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1940 smp_check.signal_connect('toggled') {
1941 if smp_check.active?
1942 smp_hbox.sensitive = true
1944 smp_hbox.sensitive = false
1948 smp_check.active = true
1949 smp_spin.value = $config['mproc'].to_i
1952 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
1953 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
1954 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1955 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
1956 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1958 dialog.vbox.show_all
1959 dialog.run { |response|
1960 if response == Gtk::Dialog::RESPONSE_OK
1961 $config['video-viewer'] = video_viewer_entry.text
1962 if smp_check.active?
1963 $config['mproc'] = smp_spin.value.to_i
1965 $config.delete('mproc')
1968 $config['convert-enhance'] = enhance_entry.text
1974 def create_menu_and_toolbar
1977 mb = Gtk::MenuBar.new
1979 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
1980 filesubmenu = Gtk::Menu.new
1981 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
1982 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
1983 filesubmenu.append( Gtk::SeparatorMenuItem.new)
1984 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
1985 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
1986 filesubmenu.append( Gtk::SeparatorMenuItem.new)
1987 tooltips = Gtk::Tooltips.new
1988 filesubmenu.append($merge_subalbums = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums"))).set_sensitive(false))
1989 $merge_subalbums.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
1990 tooltips.set_tip($merge_subalbums, utf8(_("Take into account new/removed subalbums (subdirectories) in the source directory")), nil)
1991 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums and new/removed images/videos"))).set_sensitive(false))
1992 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
1993 tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums in the source directory")), nil)
1994 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
1995 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
1996 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
1997 filesubmenu.append( Gtk::SeparatorMenuItem.new)
1998 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
1999 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
2000 filesubmenu.append( Gtk::SeparatorMenuItem.new)
2001 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
2002 filemenu.set_submenu(filesubmenu)
2005 new.signal_connect('activate') { new_album }
2006 open.signal_connect('activate') { open_file_popup }
2007 $save.signal_connect('activate') { save_current_file }
2008 $save_as.signal_connect('activate') { save_as_do }
2009 $merge.signal_connect('activate') { merge }
2010 $merge_subalbums.signal_connect('activate') { merge_subalbums }
2011 $generate.signal_connect('activate') {
2013 perform_in_background("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
2014 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
2015 { :successmsg => utf8(_("Your web-album is now ready in directory `%s'.") % $xmldoc.root.attributes['destination']),
2016 :failuremsg => utf8(_("There was something wrong when generating the web-album, sorry.")) })
2018 $properties.signal_connect('activate') { properties }
2020 quit.signal_connect('activate') { try_quit }
2022 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
2023 editsubmenu = Gtk::Menu.new
2024 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
2025 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
2026 editsubmenu.append( Gtk::SeparatorMenuItem.new)
2027 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
2028 editmenu.set_submenu(editsubmenu)
2031 prefs.signal_connect('activate') { preferences }
2033 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
2034 helpsubmenu = Gtk::Menu.new
2035 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
2036 helpmenu.set_submenu(helpsubmenu)
2039 about.signal_connect('activate') {
2040 show_popup($main_window, utf8(_("<span size='x-large' weight='bold'>Booh %s</span>
2042 <i>``The Web-Album of choice for discriminating Linux users''</i>
2044 Copyright (c) 2005 Guillaume Cottenceau") % $VERSION), { :centered => true, :pos_centered => true })
2049 tb = Gtk::Toolbar.new
2051 tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
2052 open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
2053 open.menu = Gtk::Menu.new
2054 open.signal_connect('clicked') { open_file_popup }
2055 open.signal_connect('show-menu') {
2056 lastopens = Gtk::Menu.new
2058 if $config['last-opens']
2059 $config['last-opens'].reverse.each { |e|
2060 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
2061 item.signal_connect('activate') {
2062 push_mousecursor_wait
2063 msg = open_file(from_utf8(e))
2066 show_popup($main_window, msg)
2073 open.menu = lastopens
2076 tb.insert(-1, Gtk::SeparatorToolItem.new)
2078 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
2079 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
2080 $r90.label = utf8(_("Rotate"))
2081 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
2082 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
2083 $r270.label = utf8(_("Rotate"))
2084 tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
2085 $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
2086 $enhance.label = utf8(_("Enhance"))
2087 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
2088 $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
2089 tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
2090 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
2091 nothing.label = utf8(_("None"))
2093 tb.insert(-1, Gtk::SeparatorToolItem.new)
2095 tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
2096 tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
2098 perform_undo = Proc.new {
2099 $redo_tb.sensitive = $redo_mb.sensitive = true
2100 if not more_undoes = UndoHandler.undo($statusbar)
2101 $undo_tb.sensitive = $undo_mb.sensitive = false
2104 perform_redo = Proc.new {
2105 $undo_tb.sensitive = $undo_mb.sensitive = true
2106 if not more_redoes = UndoHandler.redo($statusbar)
2107 $redo_tb.sensitive = $redo_mb.sensitive = false
2111 $undo_tb.signal_connect('clicked') { perform_undo.call }
2112 $undo_mb.signal_connect('activate') { perform_undo.call }
2113 $redo_tb.signal_connect('clicked') { perform_redo.call }
2114 $redo_mb.signal_connect('activate') { perform_redo.call }
2116 one_click_explain_try = Proc.new {
2117 if !$config['one-click-explained']
2118 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
2120 You have just clicked on a One-Click tool. When such a tool is activated
2121 (<span foreground='darkblue'>Rotate clockwise</span>, <span foreground='darkblue'>Rotate counter-clockwise</span>, <span foreground='darkblue'>Enhance</span> or <span foreground='darkblue'>Delete</span>), clicking
2122 on a thumbnail will immediately apply the desired action.
2124 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
2126 $config['one-click-explained'] = true
2130 $r90.signal_connect('toggled') {
2132 set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
2133 one_click_explain_try.call
2134 $r270.active = false
2135 $enhance.active = false
2136 $delete.active = false
2137 nothing.sensitive = true
2139 if !$r270.active? && !$enhance.active? && !$delete.active?
2140 set_mousecursor_normal
2141 nothing.sensitive = false
2143 nothing.sensitive = true
2147 $r270.signal_connect('toggled') {
2149 set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
2150 one_click_explain_try.call
2152 $enhance.active = false
2153 $delete.active = false
2154 nothing.sensitive = true
2156 if !$r90.active? && !$enhance.active? && !$delete.active?
2157 set_mousecursor_normal
2158 nothing.sensitive = false
2160 nothing.sensitive = true
2164 $enhance.signal_connect('toggled') {
2166 set_mousecursor(Gdk::Cursor::SPRAYCAN)
2167 one_click_explain_try.call
2169 $r270.active = false
2170 $delete.active = false
2171 nothing.sensitive = true
2173 if !$r90.active? && !$r270.active? && !$delete.active?
2174 set_mousecursor_normal
2175 nothing.sensitive = false
2177 nothing.sensitive = true
2181 $delete.signal_connect('toggled') {
2183 set_mousecursor(Gdk::Cursor::PIRATE)
2184 one_click_explain_try.call
2186 $r270.active = false
2187 $enhance.active = false
2188 nothing.sensitive = true
2190 if !$r90.active? && !$r270.active? && !$enhance.active?
2191 set_mousecursor_normal
2192 nothing.sensitive = false
2194 nothing.sensitive = true
2198 nothing.signal_connect('clicked') {
2199 $r90.active = $r270.active = $enhance.active = $delete.active = false
2200 set_mousecursor_normal
2206 def create_main_window
2208 mb, tb = create_menu_and_toolbar
2210 # open_file('/home/gc/booh/foo')
2212 $albums_tv = Gtk::TreeView.new
2213 $albums_tv.set_size_request(120, -1)
2214 renderer = Gtk::CellRendererText.new
2215 column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 })
2216 $albums_tv.append_column(column)
2217 $albums_tv.set_headers_visible(false)
2218 $albums_tv.selection.signal_connect('changed') { |w|
2219 push_mousecursor_wait
2223 msg 3, "no selection"
2225 $current_path = $albums_ts.get_value(iter, 1)
2230 $albums_ts = Gtk::TreeStore.new(String, String)
2231 $albums_tv.set_model($albums_ts)
2232 $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
2234 albums_sw = Gtk::ScrolledWindow.new(nil, nil)
2235 albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
2236 albums_sw.add_with_viewport($albums_tv)
2238 $notebook = Gtk::Notebook.new
2239 create_subalbums_page
2240 $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
2242 $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
2244 $notebook.signal_connect('switch-page') { |w, page, num|
2246 $delete.active = false
2247 $delete.sensitive = false
2249 $delete.sensitive = true
2253 paned = Gtk::HPaned.new
2254 paned.pack1(albums_sw, false, false)
2255 paned.pack2($notebook, true, true)
2257 main_vbox = Gtk::VBox.new(false, 0)
2258 main_vbox.pack_start(mb, false, false)
2259 main_vbox.pack_start(tb, false, false)
2260 main_vbox.pack_start(paned, true, true)
2261 main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
2263 $main_window = Gtk::Window.new
2264 $main_window.add(main_vbox)
2265 $main_window.signal_connect('delete-event') {
2269 #- read/save size and position of window
2270 if $config['pos-x'] && $config['pos-y']
2271 $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
2273 $main_window.window_position = Gtk::Window::POS_CENTER
2275 msg 3, "size: #{$config['width']}x#{$config['height']}"
2276 $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
2277 $main_window.signal_connect('configure-event') {
2278 msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
2279 x, y = $main_window.window.root_origin
2280 width, height = $main_window.window.size
2281 $config['pos-x'] = x
2282 $config['pos-y'] = y
2283 $config['width'] = width
2284 $config['height'] = height
2288 $statusbar.push(0, utf8(_("Ready.")))
2289 $main_window.show_all
2292 Thread.abort_on_exception = true