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 $config['video-viewer'] ||= 'mplayer %f'
99 if !FileTest.directory?(File.expand_path('~/.booh'))
100 system("mkdir ~/.booh")
105 if $config['last-opens'] && $config['last-opens'].size > 5
106 $config['last-opens'] = $config['last-opens'][-5, 5]
109 ios = File.open($config_file, "w")
110 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
111 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
112 $config.each_pair { |key, value|
113 elem = $xmldoc.root.add_element key
115 $config[key].each_pair { |subkey, subvalue|
116 subelem = elem.add_element subkey
117 subelem.add_text subvalue.to_s
119 elsif value.is_a? Array
120 elem.add_text value.join('~~~')
122 elem.add_text value.to_s
125 $xmldoc.write(ios, 0)
129 def set_mousecursor(what, *widget)
130 if widget[0] && widget[0].window
131 widget[0].window.set_cursor(Gdk::Cursor.new(what))
133 if $main_window.window
134 $main_window.window.set_cursor(Gdk::Cursor.new(what))
136 $current_cursor = what
138 def set_mousecursor_wait(*widget)
139 set_mousecursor(Gdk::Cursor::WATCH, *widget)
140 if Thread.current == Thread.main
141 Gtk.main_iteration while Gtk.events_pending?
144 def set_mousecursor_normal(*widget)
145 set_mousecursor(Gdk::Cursor::LEFT_PTR, *widget)
147 def push_mousecursor_wait(*widget)
148 if $current_cursor != Gdk::Cursor::WATCH
149 $save_cursor = $current_cursor
150 set_mousecursor_wait(*widget)
153 def pop_mousecursor(*widget)
154 set_mousecursor($save_cursor || Gdk::Cursor::LEFT_PTR, *widget)
158 source = $xmldoc.root.attributes['source']
159 dest = $xmldoc.root.attributes['destination']
160 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
163 def build_full_dest_filename(filename)
164 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
167 def save_undo(name, closure, *params)
168 UndoHandler.save_undo(name, closure, [ *params ])
169 $undo_tb.sensitive = $undo_mb.sensitive = true
172 def view_element(filename)
173 if entry2type(filename) == 'video'
174 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'")
182 msg 3, "filename: #{filename}"
183 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
184 #- typically this file won't exist in case of videos; try with the largest thumbnail around
185 if !File.exists?(dest_img)
186 if entry2type(filename) == 'video'
187 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
188 if not alternatives.empty?
189 dest_img = alternatives[-1]
192 push_mousecursor_wait
193 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
195 if !File.exists?(dest_img)
196 msg 2, _("Could not generate fullscreen thumbnail!")
201 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)))
203 tooltips = Gtk::Tooltips.new
204 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
206 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
207 b.signal_connect('clicked') { w.destroy }
210 vb.pack_start(evt, false, false)
211 vb.pack_end(bottom, false, false)
214 w.signal_connect('delete-event') { w.destroy }
215 w.window_position = Gtk::Window::POS_CENTER
219 def create_editzone(scrolledwindow, pagenum, image)
220 frame = Gtk::Frame.new
221 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
222 frame.set_shadow_type(Gtk::SHADOW_IN)
223 textview.signal_connect('key-press-event') { |w, event|
224 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
225 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
226 scrolledwindow.signal_emit('key-press-event', event)
228 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
229 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
230 if event.keyval == Gdk::Keyval::GDK_Up
231 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
232 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
234 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
237 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
238 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
240 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
246 textview.signal_connect('focus-in-event') { |w, event|
247 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
251 candidate_undo_text = nil
252 textview.signal_connect('focus-in-event') { |w, event|
253 candidate_undo_text = textview.buffer.text
256 textview.signal_connect('key-release-event') { |w, event|
257 if candidate_undo_text && candidate_undo_text != textview.buffer.text
259 save_undo(_("text edit"),
261 save_text = textview.buffer.text
262 textview.buffer.text = text
264 $notebook.set_page(pagenum)
266 textview.buffer.text = save_text
268 $notebook.set_page(pagenum)
270 }, candidate_undo_text)
271 candidate_undo_text = nil
274 if ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
275 #- autoscroll if cursor or image is not visible
276 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
277 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
278 current_miny_visible = scrolledwindow.vadjustment.value
279 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
280 if ypos_top < current_miny_visible
281 newval = scrolledwindow.vadjustment.value -
282 ((current_miny_visible - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
283 if newval < scrolledwindow.vadjustment.lower
284 newval = scrolledwindow.vadjustment.lower
286 scrolledwindow.vadjustment.value = newval
287 elsif ypos_bottom > current_maxy_visible
288 newval = scrolledwindow.vadjustment.value +
289 ((ypos_bottom - current_maxy_visible - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
290 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
291 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
293 scrolledwindow.vadjustment.value = newval
299 return [ frame, textview ]
302 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
305 #- update rotate attribute
306 xmlelem.add_attribute("#{attributes_prefix}rotate", current_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
308 if !$rotated_pixbufs[thumbnail_img]
309 $rotated_pixbufs[thumbnail_img] = { :orig => img.pixbuf, :angle_to_orig => angle % 360 }
311 $rotated_pixbufs[thumbnail_img][:angle_to_orig] = ($rotated_pixbufs[thumbnail_img][:angle_to_orig] + angle) % 360
313 msg 3, "angle: #{angle}, angle to orig: #{$rotated_pixbufs[thumbnail_img][:angle_to_orig]}"
315 #- rotate shown thumbnail
316 pixbuf = rotate_pixbuf($rotated_pixbufs[thumbnail_img][:orig], $rotated_pixbufs[thumbnail_img][:angle_to_orig])
317 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
318 if pixbuf.height > desired_y
319 img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y,
320 Gdk::Pixbuf::INTERP_BILINEAR)
321 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
322 img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width),
323 Gdk::Pixbuf::INTERP_BILINEAR)
325 img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf
329 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
332 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
334 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
336 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
337 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
339 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
340 $notebook.set_page(0)
341 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
346 def color_swap(xmldir, attributes_prefix)
348 if xmldir.attributes["#{attributes_prefix}color-swap"]
349 xmldir.delete_attribute("#{attributes_prefix}color-swap")
351 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
355 def enhance(xmldir, attributes_prefix)
357 if xmldir.attributes["#{attributes_prefix}enhance"]
358 xmldir.delete_attribute("#{attributes_prefix}enhance")
360 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
364 def change_frame_offset(xmldir, attributes_prefix, value)
366 xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
369 def ask_new_frame_offset(xmldir, attributes_prefix)
370 value = xmldir.attributes["#{attributes_prefix}frame-offset"]
372 dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
374 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
375 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
376 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
380 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
381 from. There are approximately 25 frames per second in a video.
384 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
385 entry.signal_connect('key-press-event') { |w, event|
386 if event.keyval == Gdk::Keyval::GDK_Return
387 dialog.response(Gtk::Dialog::RESPONSE_OK)
389 elsif event.keyval == Gdk::Keyval::GDK_Escape
390 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
393 false #- propagate if needed
397 dialog.window_position = Gtk::Window::POS_MOUSE
400 dialog.run { |response|
404 if response == Gtk::Dialog::RESPONSE_OK
405 msg 3, "changing frame offset top #{newval}"
406 return { :old => value, :new => newval }
413 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img)
415 push_mousecursor_wait
416 system("rm -f '#{destfile}'")
417 #- type can be 'element' or 'subdir'
419 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
421 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], find_subalbum_info_type(xmldir))
424 $rotated_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
425 if entry2type(origfile) == 'video'
426 #- cleanup temp for videos
427 system("rm -f #{current_dest_dir}/screenshot.jpg000000.jpg")
433 def popup_thumbnail_menu(event, optionals, type, xmldir, attributes_prefix, closures)
435 if optionals.include?('change_image')
436 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
437 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
438 changeimg.signal_connect('activate') { closures[:change].call }
439 menu.append( Gtk::SeparatorMenuItem.new)
441 menu.append( r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
442 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
443 r90.signal_connect('activate') { closures[:rotate].call(90) }
444 menu.append( r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
445 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
446 r270.signal_connect('activate') { closures[:rotate].call(-90) }
448 menu.append( Gtk::SeparatorMenuItem.new)
449 menu.append( color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
450 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
451 color_swap.signal_connect('activate') { closures[:color_swap].call }
452 menu.append( flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
453 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
454 flip.signal_connect('activate') { closures[:rotate].call(180) }
455 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
456 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
457 frame_offset.signal_connect('activate') { closures[:frame_offset].call }
459 menu.append( Gtk::SeparatorMenuItem.new)
460 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
461 _("Enhance constrast"))))
462 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
463 enhance.signal_connect('activate') { closures[:enhance].call }
464 if optionals.include?('delete')
465 menu.append( Gtk::SeparatorMenuItem.new)
466 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
467 delete_item.signal_connect('activate') { closures[:delete].call }
470 menu.popup(nil, nil, event.button, event.time)
473 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
476 frame1 = Gtk::Frame.new
478 my_gen_real_thumbnail = proc {
479 gen_real_thumbnail('element', from_utf8("#{$current_path}/#{filename}"), thumbnail_img, $xmldir, $default_size['thumbnails'], img)
482 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
483 if !$rotated_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
484 frame1.add(img = Gtk::Image.new)
485 my_gen_real_thumbnail.call
487 frame1.add(img = Gtk::Image.new($rotated_pixbufs[thumbnail_img] ? $rotated_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
489 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
491 tooltips = Gtk::Tooltips.new
492 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
493 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(from_utf8("#{$current_path}/#{filename}"))/1024)]) : tipname), nil)
495 frame2, textview = create_editzone($autotable_sw, 1, img)
496 textview.buffer.text = utf8(caption)
497 textview.set_justification(Gtk::Justification::CENTER)
499 vbox = Gtk::VBox.new(false, 5)
500 vbox.pack_start(evtbox, false, false)
501 vbox.pack_start(frame2, false, false)
502 autotable.append(vbox, filename)
504 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
505 $vbox2textview[vbox] = textview
507 #- to be able to find widgets by name
508 $name2widgets[filename] = { :textview => textview }
510 cleanup_all_thumbnails = Proc.new {
511 #- remove out of sync images
512 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
513 for sizeobj in $images_size
514 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
519 rotate_and_cleanup = Proc.new { |angle|
520 rotate(angle, thumbnail_img, img, $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
521 cleanup_all_thumbnails.call
524 color_swap_and_cleanup = Proc.new {
525 perform_color_swap_and_cleanup = Proc.new {
526 color_swap($xmldir.elements["[@filename='#{filename}']"], '')
527 my_gen_real_thumbnail.call
530 cleanup_all_thumbnails.call
531 perform_color_swap_and_cleanup.call
533 save_undo(_("color swap"),
535 perform_color_swap_and_cleanup.call
536 $notebook.set_page(1)
538 perform_color_swap_and_cleanup.call
539 $notebook.set_page(1)
544 change_frame_offset_and_cleanup = Proc.new {
545 if values = ask_new_frame_offset($xmldir.elements["[@filename='#{filename}']"], '')
546 perform_change_frame_offset_and_cleanup = Proc.new { |val|
547 change_frame_offset($xmldir.elements["[@filename='#{filename}']"], '', val)
548 my_gen_real_thumbnail.call
550 perform_change_frame_offset_and_cleanup.call(values[:new])
552 save_undo(_("specify frame offset"),
554 perform_change_frame_offset_and_cleanup.call(values[:old])
555 $notebook.set_page(1)
557 perform_change_frame_offset_and_cleanup.call(values[:new])
558 $notebook.set_page(1)
564 enhance_and_cleanup = Proc.new {
565 perform_enhance_and_cleanup = Proc.new {
566 enhance($xmldir.elements["[@filename='#{filename}']"], '')
567 my_gen_real_thumbnail.call
570 cleanup_all_thumbnails.call
571 perform_enhance_and_cleanup.call
573 save_undo(_("enhance"),
575 perform_enhance_and_cleanup.call
576 $notebook.set_page(1)
578 perform_enhance_and_cleanup.call
579 $notebook.set_page(1)
585 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed?")), { :okcancel => true })
588 perform_delete = Proc.new {
589 after = autotable.get_next_widget(vbox)
591 after = autotable.get_previous_widget(vbox)
593 autotable.remove(vbox)
595 $vbox2textview[after].grab_focus
602 if $xmldir.elements['dir']
603 $xmldir.delete_attribute('thumbnails-caption')
604 $xmldir.delete_attribute('thumbnails-captionfile')
608 save_changes('forced')
609 populate_subalbums_treeview
611 save_undo(_("delete"),
613 autotable.reinsert(pos, vbox, filename)
614 $notebook.set_page(1)
617 $notebook.set_page(1)
619 }, autotable.get_current_number(vbox))
624 textview.signal_connect('key-press-event') { |w, event|
627 x, y = autotable.get_current_pos(vbox)
628 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
629 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
630 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
631 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
633 $vbox2textview[autotable.get_widget_at_pos(x, y - 1)].grab_focus
636 autotable.move_up(vbox)
637 textview.grab_focus #- because if moving, focus is stolen
640 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
642 $vbox2textview[autotable.get_widget_at_pos(x, y + 1)].grab_focus
645 autotable.move_down(vbox)
646 textview.grab_focus #- because if moving, focus is stolen
649 if event.keyval == Gdk::Keyval::GDK_Left
650 previous = autotable.get_previous_widget(vbox)
651 if previous && autotable.get_current_pos(previous)[0] < x
653 $vbox2textview[previous].grab_focus
656 autotable.move_left(vbox)
657 textview.grab_focus #- because if moving, focus is stolen
661 rotate_and_cleanup.call(-90)
664 if event.keyval == Gdk::Keyval::GDK_Right
665 next_ = autotable.get_next_widget(vbox)
666 if next_ && autotable.get_current_pos(next_)[0] > x
668 $vbox2textview[next_].grab_focus
671 autotable.move_right(vbox)
672 textview.grab_focus #- because if moving, focus is stolen
676 rotate_and_cleanup.call(90)
679 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
682 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
683 view_element(filename)
687 !propagate #- propagate if needed
690 evtbox.signal_connect('button-press-event') { |w, event|
692 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
693 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
695 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
697 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
698 elsif $enhance.active?
699 enhance_and_cleanup.call
700 elsif $delete.active?
706 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
707 popup_thumbnail_menu(event, ['delete'], type, $xmldir.elements["[@filename='#{filename}']"], '',
708 { :rotate => rotate_and_cleanup, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
709 :frame_offset => change_frame_offset_and_cleanup, :delete => delete })
711 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
712 view_element(filename)
714 retval = false #- propagate
719 vbox.signal_connect('button-press-event') { |w, event|
720 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
721 $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
725 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
726 if $gesture_press && $gesture_press[:filename] == filename
727 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
728 angle = x-$gesture_press[:x] > 0 ? 90 : -90
729 msg 3, "gesture rotate: #{angle}"
730 rotate_and_cleanup.call(angle)
736 #- handle reordering with drag and drop
737 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
738 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
739 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
740 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
742 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
743 ctxt.targets.each { |target|
744 if target.name == 'reorder-elements'
745 from, to = selection_data.data.to_i, autotable.get_current_number(vbox)
748 autotable.move(from, to)
749 save_undo(_("reorder"),
750 Proc.new { |from, to|
752 autotable.move(to - 1, from)
754 autotable.move(to, from + 1)
756 $notebook.set_page(1)
758 autotable.move(from, to)
759 $notebook.set_page(1)
770 def create_auto_table
772 $autotable = Gtk::AutoTable.new(5)
774 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
775 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
776 $autotable_sw.add_with_viewport($autotable)
779 def create_subalbums_page
781 subalbums_hb = Gtk::HBox.new
782 $subalbums_vb = Gtk::VBox.new(false, 5)
783 subalbums_hb.pack_start($subalbums_vb, false, false)
784 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
785 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
786 $subalbums_sw.add_with_viewport(subalbums_hb)
789 def save_current_file
792 ios = File.open($filename, "w")
793 $xmldoc.write(ios, 0)
801 dialog = Gtk::Dialog.new(utf8(_("Save before quitting?")),
803 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
804 [Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
805 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
806 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
807 dialog.vbox.add(Gtk::Label.new(utf8(_("Do you want to save your changes before quitting?"))))
808 dialog.window_position = Gtk::Window::POS_CENTER
811 dialog.run { |response|
813 if response == Gtk::Dialog::RESPONSE_CANCEL
815 elsif response == Gtk::Dialog::RESPONSE_YES
824 def show_popup(parent, msg, *options)
825 dialog = Gtk::Dialog.new
826 dialog.title = utf8(_("Booh message"))
829 if options[0] && options[0][:centered]
830 lbl.set_justify(Gtk::Justification::CENTER)
833 if options[0] && options[0][:okcancel]
834 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
836 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
838 dialog.transient_for = parent
839 dialog.set_default_size(200, 120)
840 if options[0] && options[0][:pos_centered]
841 dialog.window_position = Gtk::Window::POS_CENTER
843 dialog.window_position = Gtk::Window::POS_MOUSE
847 dialog.run { |response|
849 if options[0] && options[0][:okcancel]
850 return response == Gtk::Dialog::RESPONSE_OK
855 def wait_message(parent, msg)
857 w.set_transient_for(parent)
860 vb = Gtk::VBox.new(false, 5)
861 vb.set_border_width(5)
862 vb.pack_start(Gtk::Label.new(msg), false, false)
863 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
864 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
865 vb.pack_start(pb = Gtk::ProgressBar.new.set_pulse_step(0.05), false, false)
866 vb.pack_start(Gtk::HSeparator.new, false, false)
867 vb.pack_end(bottom, false, false)
869 timeout = Gtk.timeout_add(200) { pb.pulse }
872 w.signal_connect('delete-event') { w.destroy }
873 w.signal_connect('destroy') { Gtk.timeout_remove(timeout) }
874 w.window_position = Gtk::Window::POS_CENTER
880 def perform_in_background(cmd, waitmsg, params)
881 button, w8 = wait_message($main_window, waitmsg)
886 id, exitstatus = Process.waitpid2(pid)
889 if params[:successmsg]
890 show_popup($main_window, params[:successmsg])
892 if params[:closure_after]
893 params[:closure_after].call
895 elsif exitstatus == 15
896 #- say nothing, user aborted
898 if params[:failuremsg]
899 show_popup($main_window, params[:failuremsg])
906 button.signal_connect('clicked') {
907 Process.kill('SIGTERM', pid)
911 def save_changes(*forced)
912 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
916 if $xmldir.elements['dir']
917 $xmldir.add_attribute('subdirs-caption', $subalbums_title.buffer.text)
918 $xmldir.elements.each('dir') { |element|
919 path = element.attributes['path']
920 if element.attributes['subdirs-caption']
921 element.add_attribute('subdirs-caption', $subalbums_edits[path][:editzone].buffer.text)
922 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
924 element.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
925 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
928 if $xmldir.attributes['thumbnails-caption']
929 path = $xmldir.attributes['path']
930 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
934 #- remove and reinsert elements to reflect new ordering
938 $xmldir.elements.each { |element|
939 if element.name == 'image' || element.name == 'video'
940 save_types[element.attributes['filename']] = element.name
941 save_attributes[element.attributes['filename']] = element.attributes
946 $autotable.current_order.each { |path|
947 chld = $xmldir.add_element save_types[path], save_attributes[path]
948 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
957 $undo_tb.sensitive = $undo_mb.sensitive = false
958 $redo_tb.sensitive = $redo_mb.sensitive = false
964 $subalbums_vb.children.each { |chld|
965 $subalbums_vb.remove(chld)
967 $subalbums = Gtk::Table.new(0, 0, true)
968 current_y_sub_albums = 0
970 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
971 $subalbums_edits = {}
973 add_subalbum = Proc.new { |xmldir|
975 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
976 caption = xmldir.attributes['thumbnails-caption']
977 captionfile, dummy = find_subalbum_caption_info(xmldir)
978 infotype = 'thumbnails'
980 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
981 captionfile, caption = find_subalbum_caption_info(xmldir)
982 infotype = find_subalbum_info_type(xmldir)
984 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
986 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
988 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
991 my_gen_real_thumbnail = proc {
992 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img)
995 if !$rotated_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
996 f.add(img = Gtk::Image.new)
997 my_gen_real_thumbnail.call
999 f.add(img = Gtk::Image.new($rotated_pixbufs[thumbnail_file] ? $rotated_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
1001 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
1002 $subalbums.attach(hbox,
1003 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1005 change_image = Proc.new {
1006 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
1008 Gtk::FileChooser::ACTION_OPEN,
1010 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1011 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
1012 fc.transient_for = $main_window
1013 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))
1014 f.add(preview_img = Gtk::Image.new)
1016 fc.signal_connect('update-preview') { |w|
1018 if fc.preview_filename
1019 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
1020 fc.preview_widget_active = true
1022 rescue Gdk::PixbufError
1023 fc.preview_widget_active = false
1026 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1028 old_file = captionfile
1029 old_rotate = xmldir.attributes["#{infotype}-rotate"]
1030 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
1031 old_enhance = xmldir.attributes["#{infotype}-enhance"]
1032 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
1034 new_file = fc.filename
1035 msg 3, "new captionfile is: #{fc.filename}"
1036 perform_changefile = Proc.new {
1037 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
1038 $rotated_pixbufs.delete(thumbnail_file)
1039 xmldir.delete_attribute("#{infotype}-rotate")
1040 xmldir.delete_attribute("#{infotype}-color-swap")
1041 xmldir.delete_attribute("#{infotype}-enhance")
1042 xmldir.delete_attribute("#{infotype}-frame-offset")
1043 my_gen_real_thumbnail.call
1045 perform_changefile.call
1047 save_undo(_("change caption file for sub-album"),
1049 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
1050 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
1051 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
1052 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
1053 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
1054 my_gen_real_thumbnail.call
1055 $notebook.set_page(0)
1057 perform_changefile.call
1058 $notebook.set_page(0)
1065 rotate_and_cleanup = Proc.new { |angle|
1066 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
1067 system("rm -f '#{thumbnail_file}'")
1070 color_swap_and_cleanup = Proc.new {
1071 perform_color_swap_and_cleanup = Proc.new {
1072 color_swap(xmldir, "#{infotype}-")
1073 my_gen_real_thumbnail.call
1075 perform_color_swap_and_cleanup.call
1077 save_undo(_("color swap"),
1079 perform_color_swap_and_cleanup.call
1080 $notebook.set_page(0)
1082 perform_color_swap_and_cleanup.call
1083 $notebook.set_page(0)
1088 change_frame_offset_and_cleanup = Proc.new {
1089 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
1090 perform_change_frame_offset_and_cleanup = Proc.new { |val|
1091 change_frame_offset(xmldir, "#{infotype}-", val)
1092 my_gen_real_thumbnail.call
1094 perform_change_frame_offset_and_cleanup.call(values[:new])
1096 save_undo(_("specify frame offset"),
1098 perform_change_frame_offset_and_cleanup.call(values[:old])
1099 $notebook.set_page(0)
1101 perform_change_frame_offset_and_cleanup.call(values[:new])
1102 $notebook.set_page(0)
1108 enhance_and_cleanup = Proc.new {
1109 perform_enhance_and_cleanup = Proc.new {
1110 enhance(xmldir, "#{infotype}-")
1111 my_gen_real_thumbnail.call
1114 perform_enhance_and_cleanup.call
1116 save_undo(_("enhance"),
1118 perform_enhance_and_cleanup.call
1119 $notebook.set_page(0)
1121 perform_enhance_and_cleanup.call
1122 $notebook.set_page(0)
1127 evtbox.signal_connect('button-press-event') { |w, event|
1128 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1130 rotate_and_cleanup.call(90)
1132 rotate_and_cleanup.call(-90)
1133 elsif $enhance.active?
1134 enhance_and_cleanup.call
1137 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1138 popup_thumbnail_menu(event, ['change_image'], entry2type(captionfile), xmldir, "#{infotype}-",
1139 { :change => change_image, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
1140 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup })
1142 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1147 evtbox.signal_connect('button-press-event') { |w, event|
1148 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
1152 evtbox.signal_connect('button-release-event') { |w, event|
1153 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
1154 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
1155 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
1156 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
1157 msg 3, "gesture rotate: #{angle}"
1158 rotate_and_cleanup.call(angle)
1161 $gesture_press = nil
1164 frame, textview = create_editzone($subalbums_sw, 0, img)
1165 textview.buffer.text = caption
1166 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1167 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1169 $subalbums_edits[xmldir.attributes['path']] = { :editzone => textview, :captionfile => captionfile }
1170 current_y_sub_albums += 1
1173 if $xmldir.elements['dir']
1175 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
1176 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
1177 $subalbums_title.set_justification(Gtk::Justification::CENTER)
1178 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1179 #- this album image/caption
1180 if $xmldir.attributes['thumbnails-caption']
1181 add_subalbum.call($xmldir)
1184 $xmldir.elements.each { |element|
1185 if element.name == 'image' || element.name == 'video'
1186 #- element (image or video) of this album
1187 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
1188 msg 3, "dest_img: #{dest_img}"
1189 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
1191 if element.name == 'dir'
1192 #- sub-album image/caption
1193 add_subalbum.call(element)
1196 $subalbums_vb.add($subalbums)
1197 $subalbums_vb.show_all
1199 if !$xmldir.elements['image'] && !$xmldir.elements['video']
1200 $notebook.get_tab_label($autotable_sw).sensitive = false
1201 $notebook.set_page(0)
1203 $notebook.get_tab_label($autotable_sw).sensitive = true
1206 if !$xmldir.elements['dir']
1207 $notebook.get_tab_label($subalbums_sw).sensitive = false
1208 $notebook.set_page(1)
1210 $notebook.get_tab_label($subalbums_sw).sensitive = true
1214 def pixbuf_or_nil(filename)
1216 return Gdk::Pixbuf.new(filename)
1222 def theme_choose(current)
1223 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
1225 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1226 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1227 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1229 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
1230 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
1231 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
1232 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
1233 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
1234 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
1235 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
1236 treeview.signal_connect('button-press-event') { |w, event|
1237 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1238 dialog.response(Gtk::Dialog::RESPONSE_OK)
1242 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
1244 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
1247 iter[0] = File.basename(dir)
1248 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
1249 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
1250 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
1251 if File.basename(dir) == current
1252 treeview.selection.select_iter(iter)
1256 dialog.set_default_size(700, 400)
1257 dialog.vbox.show_all
1258 dialog.run { |response|
1259 iter = treeview.selection.selected
1261 if response == Gtk::Dialog::RESPONSE_OK && iter
1262 return model.get_value(iter, 0)
1268 def populate_subalbums_treeview
1271 $subalbums_vb.children.each { |chld|
1272 $subalbums_vb.remove(chld)
1275 source = $xmldoc.root.attributes['source']
1276 msg 3, "source: #{source}"
1278 xmldir = $xmldoc.elements["//dir[@path='#{source}']"]
1280 msg 1, _("Corrupted booh file...")
1284 append_dir_elem = Proc.new { |parent_iter, location|
1285 child_iter = $albums_ts.append(parent_iter)
1286 child_iter[0] = File.basename(location)
1287 child_iter[1] = location
1288 msg 3, "puttin location: #{location}"
1289 $xmldoc.elements.each("//dir[@path='#{location}']/dir") { |elem|
1290 append_dir_elem.call(child_iter, elem.attributes['path'])
1293 append_dir_elem.call(nil, source)
1295 $albums_tv.expand_all
1296 $albums_tv.selection.select_iter($albums_ts.iter_first)
1299 def open_file(filename)
1303 $current_path = nil #- invalidate
1304 $rotated_pixbufs = {}
1307 $subalbums_vb.children.each { |chld|
1308 $subalbums_vb.remove(chld)
1311 if !File.exists?(filename)
1312 return utf8(_("File not found."))
1316 $xmldoc = REXML::Document.new File.new(filename)
1321 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
1322 if entry2type(filename).nil?
1323 return utf8(_("Not a booh file!"))
1325 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."))
1329 if !source = $xmldoc.root.attributes['source']
1330 return utf8(_("Corrupted booh file..."))
1333 if !dest = $xmldoc.root.attributes['destination']
1334 return utf8(_("Corrupted booh file..."))
1337 if !theme = $xmldoc.root.attributes['theme']
1338 return utf8(_("Corrupted booh file..."))
1341 limit_sizes = $xmldoc.root.attributes['limit-sizes']
1343 $filename = filename
1344 select_theme(theme, limit_sizes)
1345 $default_size['thumbnails'] =~ /(.*)x(.*)/
1346 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
1347 $albums_thumbnail_size =~ /(.*)x(.*)/
1348 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
1350 populate_subalbums_treeview
1352 $config['last-opens'] ||= []
1353 if $config['last-opens'][-1] != utf8(filename)
1354 $config['last-opens'] << utf8(filename)
1356 $save.sensitive = $save_as.sensitive = $merge.sensitive = $merge_subalbums.sensitive = $generate.sensitive = $properties.sensitive = true
1361 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
1363 Gtk::FileChooser::ACTION_OPEN,
1365 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1366 fc.add_shortcut_folder(File.expand_path("~/.booh"))
1367 fc.set_current_folder(File.expand_path("~/.booh"))
1368 fc.transient_for = $main_window
1371 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1372 push_mousecursor_wait(fc)
1373 msg = open_file(fc.filename)
1388 def additional_booh_options
1391 options += "--mproc #{$config['mproc'].to_i} "
1397 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
1399 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1400 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1401 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1403 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
1404 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
1405 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1406 tbl.attach(src = Gtk::Entry.new,
1407 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1408 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
1409 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1410 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
1411 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1412 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
1413 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1414 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
1415 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1416 tbl.attach(dest = Gtk::Entry.new,
1417 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1418 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
1419 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1420 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
1421 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1422 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
1423 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
1424 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
1425 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1427 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(tbl = Gtk::Table.new(0, 0, false))
1428 tbl.attach(Gtk::Label.new(utf8(_("Theme: "))),
1429 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1430 tbl.attach(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'),
1431 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1432 tbl.attach(Gtk::Label.new(utf8(_("Sizes of images to generate: "))),
1433 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1434 tbl.attach(sizes = Gtk::HBox.new,
1435 1, 3, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1437 src_nb_calculated_for = ''
1439 process_src_nb = Proc.new {
1440 if src.text != src_nb_calculated_for
1441 src_nb_calculated_for = src.text
1443 Thread.kill(src_nb_thread)
1446 if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
1447 if File.readable?(from_utf8(src_nb_calculated_for))
1448 src_nb_thread = Thread.new {
1449 src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
1450 total = { 'image' => 0, 'video' => 0, nil => 0 }
1451 `find '#{from_utf8(src_nb_calculated_for)}' -type d`.each { |dir|
1452 if File.basename(dir) =~ /^\./
1456 Dir.entries(dir.chomp).each { |file|
1457 total[entry2type(file)] += 1
1459 rescue Errno::EACCES, Errno::ENOENT
1463 src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ]))
1467 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
1470 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
1475 timeout_src_nb = Gtk.timeout_add(100) {
1479 src_browse.signal_connect('clicked') {
1480 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
1482 Gtk::FileChooser::ACTION_SELECT_FOLDER,
1484 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1485 fc.transient_for = $main_window
1486 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1487 src.text = utf8(fc.filename)
1489 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
1494 dest_browse.signal_connect('clicked') {
1495 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
1497 Gtk::FileChooser::ACTION_CREATE_FOLDER,
1499 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1500 fc.transient_for = $main_window
1501 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1502 dest.text = utf8(fc.filename)
1507 conf_browse.signal_connect('clicked') {
1508 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
1510 Gtk::FileChooser::ACTION_SAVE,
1512 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1513 fc.transient_for = $main_window
1514 fc.add_shortcut_folder(File.expand_path("~/.booh"))
1515 fc.set_current_folder(File.expand_path("~/.booh"))
1516 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1517 conf.text = utf8(fc.filename)
1523 recreate_theme_config = proc {
1524 theme_sizes.each { |e| sizes.remove(e[:widget]) }
1526 select_theme(theme_button.label, 'all')
1527 $images_size.each { |s|
1528 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
1532 tooltips = Gtk::Tooltips.new
1533 tooltips.set_tip(cb, utf8(s['description']), nil)
1534 theme_sizes << { :widget => cb, :value => s['name'] }
1536 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
1537 tooltips = Gtk::Tooltips.new
1538 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
1539 theme_sizes << { :widget => cb, :value => 'original' }
1542 recreate_theme_config.call
1544 theme_button.signal_connect('clicked') {
1545 if newtheme = theme_choose(theme_button.label)
1546 theme_button.label = newtheme
1547 recreate_theme_config.call
1551 dialog.vbox.add(frame1)
1552 dialog.vbox.add(frame2)
1553 dialog.window_position = Gtk::Window::POS_MOUSE
1559 dialog.run { |response|
1560 if response == Gtk::Dialog::RESPONSE_OK
1561 srcdir = from_utf8(src.text)
1562 destdir = from_utf8(dest.text)
1563 if !File.directory?(srcdir)
1564 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
1566 elsif conf.text == ''
1567 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
1569 elsif destdir != make_dest_filename(destdir)
1570 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
1572 elsif File.directory?(destdir)
1573 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
1575 elsif File.exists?(destdir)
1576 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
1578 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
1579 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
1581 system("mkdir '#{destdir}'")
1582 if !File.directory?(destdir)
1583 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
1594 srcdir = from_utf8(src.text)
1595 destdir = from_utf8(dest.text)
1596 configskel = File.expand_path(from_utf8(conf.text))
1597 theme = theme_button.label
1598 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
1601 Thread.kill(src_nb_thread)
1603 Gtk.timeout_remove(timeout_src_nb)
1606 perform_in_background("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
1607 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} #{additional_booh_options}",
1608 utf8(_("Please wait while scanning source directory...")),
1609 { :closure_after => proc { open_file(configskel) } })
1614 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
1616 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1617 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1618 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1620 source = $xmldoc.root.attributes['source']
1621 dest = $xmldoc.root.attributes['destination']
1622 theme = $xmldoc.root.attributes['theme']
1623 limit_sizes = $xmldoc.root.attributes['limit-sizes']
1625 limit_sizes = limit_sizes.split(/,/)
1628 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
1629 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
1630 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1631 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
1632 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1633 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
1634 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1635 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>')),
1636 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1637 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
1638 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1639 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $filename + '</i>')),
1640 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
1642 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(tbl = Gtk::Table.new(0, 0, false))
1643 tbl.attach(Gtk::Label.new(utf8(_("Theme: "))),
1644 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1645 tbl.attach(theme_button = Gtk::Button.new(theme),
1646 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1647 tbl.attach(Gtk::Label.new(utf8(_("Sizes of images to generate: "))),
1648 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1649 tbl.attach(sizes = Gtk::HBox.new,
1650 1, 3, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1653 recreate_theme_config = proc {
1654 theme_sizes.each { |e| sizes.remove(e[:widget]) }
1656 select_theme(theme_button.label, 'all')
1657 $images_size.each { |s|
1658 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
1660 if limit_sizes.include?(s['name'])
1668 tooltips = Gtk::Tooltips.new
1669 tooltips.set_tip(cb, utf8(s['description']), nil)
1670 theme_sizes << { :widget => cb, :value => s['name'] }
1672 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
1673 tooltips = Gtk::Tooltips.new
1674 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
1675 if limit_sizes && limit_sizes.include?('original')
1678 theme_sizes << { :widget => cb, :value => 'original' }
1681 recreate_theme_config.call
1683 theme_button.signal_connect('clicked') {
1684 if newtheme = theme_choose(theme_button.label)
1686 theme_button.label = newtheme
1687 recreate_theme_config.call
1691 dialog.vbox.add(frame1)
1692 dialog.vbox.add(frame2)
1693 dialog.window_position = Gtk::Window::POS_MOUSE
1699 dialog.run { |response|
1700 if response == Gtk::Dialog::RESPONSE_OK
1701 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
1702 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
1711 save_theme = theme_button.label
1712 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
1715 if ok && (save_theme != theme || save_limit_sizes != limit_sizes)
1717 perform_in_background("booh-backend --use-config '#{$filename}' --for-gui " +
1718 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{save_limit_sizes.join(',')} #{additional_booh_options}",
1719 utf8(_("Please wait while scanning source directory...")),
1720 { :closure_after => proc { open_file($filename) } })
1727 theme = $xmldoc.root.attributes['theme']
1728 limit_sizes = $xmldoc.root.attributes['limit-sizes']
1730 limit_sizes = "--sizes #{limit_sizes}"
1732 perform_in_background("booh-backend --merge-config '#{$filename}' --for-gui " +
1733 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
1734 utf8(_("Please wait while scanning source directory...")),
1735 { :closure_after => proc { open_file($filename) } })
1741 perform_in_background("booh-backend --merge-config-newdirs '#{$filename}' --for-gui " +
1742 "--verbose-level #{$verbose_level} #{additional_booh_options}",
1743 utf8(_("Please wait while scanning source directory...")),
1744 { :closure_after => proc { open_file($filename) } })
1748 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
1750 Gtk::FileChooser::ACTION_SAVE,
1752 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1753 fc.transient_for = $main_window
1754 fc.add_shortcut_folder(File.expand_path("~/.booh"))
1755 fc.set_current_folder(File.expand_path("~/.booh"))
1756 fc.filename = $filename
1757 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1758 $filename = fc.filename
1765 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
1767 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1768 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1769 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1771 dialog.vbox.add(notebook = Gtk::Notebook.new)
1772 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
1773 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
1774 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1775 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
1776 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1777 tooltips = Gtk::Tooltips.new
1778 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename; for example: mplayer %f")), nil)
1779 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
1780 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1781 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)),
1782 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1783 smp_check.signal_connect('toggled') {
1784 if smp_check.active?
1785 smp_hbox.sensitive = true
1787 smp_hbox.sensitive = false
1791 smp_check.active = true
1792 smp_spin.value = $config['mproc'].to_i
1795 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
1796 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
1797 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1798 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
1799 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1801 dialog.vbox.show_all
1802 dialog.run { |response|
1803 if response == Gtk::Dialog::RESPONSE_OK
1804 $config['video-viewer'] = video_viewer_entry.text
1805 if smp_check.active?
1806 $config['mproc'] = smp_spin.value.to_i
1808 $config.delete('mproc')
1811 $config['convert-enhance'] = enhance_entry.text
1817 def create_menu_and_toolbar
1820 mb = Gtk::MenuBar.new
1822 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
1823 filesubmenu = Gtk::Menu.new
1824 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
1825 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
1826 filesubmenu.append( Gtk::SeparatorMenuItem.new)
1827 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
1828 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
1829 filesubmenu.append( Gtk::SeparatorMenuItem.new)
1830 tooltips = Gtk::Tooltips.new
1831 filesubmenu.append($merge_subalbums = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums"))).set_sensitive(false))
1832 $merge_subalbums.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
1833 tooltips.set_tip($merge_subalbums, utf8(_("Take into account new/removed subalbums (subdirectories) in the source directory")), nil)
1834 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums and new/removed images/videos"))).set_sensitive(false))
1835 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
1836 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)
1837 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
1838 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
1839 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
1840 filesubmenu.append( Gtk::SeparatorMenuItem.new)
1841 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
1842 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
1843 filesubmenu.append( Gtk::SeparatorMenuItem.new)
1844 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
1845 filemenu.set_submenu(filesubmenu)
1848 new.signal_connect('activate') { new_album }
1849 open.signal_connect('activate') { open_file_popup }
1850 $save.signal_connect('activate') { save_current_file }
1851 $save_as.signal_connect('activate') { save_as_do }
1852 $merge.signal_connect('activate') { merge }
1853 $merge_subalbums.signal_connect('activate') { merge_subalbums }
1854 $generate.signal_connect('activate') {
1856 perform_in_background("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
1857 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
1858 { :successmsg => utf8(_("Your web-album is now ready in directory `%s'.") % $xmldoc.root.attributes['destination']),
1859 :failuremsg => utf8(_("There was something wrong when generating the web-album, sorry.")) })
1861 $properties.signal_connect('activate') { properties }
1863 quit.signal_connect('activate') { try_quit }
1865 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
1866 editsubmenu = Gtk::Menu.new
1867 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
1868 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
1869 editsubmenu.append( Gtk::SeparatorMenuItem.new)
1870 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
1871 editmenu.set_submenu(editsubmenu)
1874 prefs.signal_connect('activate') { preferences }
1876 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
1877 helpsubmenu = Gtk::Menu.new
1878 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
1879 helpmenu.set_submenu(helpsubmenu)
1882 about.signal_connect('activate') {
1883 show_popup($main_window, utf8(_("<span size='x-large' weight='bold'>Booh %s</span>
1885 <i>``The Web-Album of choice for discriminating Linux users''</i>
1887 Copyright (c) 2005 Guillaume Cottenceau") % $VERSION), { :centered => true, :pos_centered => true })
1892 tb = Gtk::Toolbar.new
1894 tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
1895 open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
1896 open.menu = Gtk::Menu.new
1897 open.signal_connect('clicked') { open_file_popup }
1898 open.signal_connect('show-menu') {
1899 lastopens = Gtk::Menu.new
1901 if $config['last-opens']
1902 $config['last-opens'].reverse.each { |e|
1903 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
1904 item.signal_connect('activate') {
1905 push_mousecursor_wait
1906 msg = open_file(from_utf8(e))
1909 show_popup($main_window, msg)
1916 open.menu = lastopens
1919 tb.insert(-1, Gtk::SeparatorToolItem.new)
1921 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
1922 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
1923 $r90.label = utf8(_("Rotate"))
1924 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
1925 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
1926 $r270.label = utf8(_("Rotate"))
1927 tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
1928 $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1929 $enhance.label = utf8(_("Enhance"))
1930 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
1931 $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
1932 tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
1933 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
1934 nothing.label = utf8(_("None"))
1936 tb.insert(-1, Gtk::SeparatorToolItem.new)
1938 tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
1939 tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
1941 perform_undo = Proc.new {
1942 $redo_tb.sensitive = $redo_mb.sensitive = true
1943 if not more_undoes = UndoHandler.undo($statusbar)
1944 $undo_tb.sensitive = $undo_mb.sensitive = false
1947 perform_redo = Proc.new {
1948 $undo_tb.sensitive = $undo_mb.sensitive = true
1949 if not more_redoes = UndoHandler.redo($statusbar)
1950 $redo_tb.sensitive = $redo_mb.sensitive = false
1954 $undo_tb.signal_connect('clicked') { perform_undo.call }
1955 $undo_mb.signal_connect('activate') { perform_undo.call }
1956 $redo_tb.signal_connect('clicked') { perform_redo.call }
1957 $redo_mb.signal_connect('activate') { perform_redo.call }
1959 one_click_explain_try = Proc.new {
1960 if !$config['one-click-explained']
1961 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
1963 You have just clicked on a One-Click tool. When such a tool is activated
1964 (<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
1965 on a thumbnail will immediately apply the desired action.
1967 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
1969 $config['one-click-explained'] = true
1973 $r90.signal_connect('toggled') {
1975 set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
1976 one_click_explain_try.call
1977 $r270.active = false
1978 $enhance.active = false
1979 $delete.active = false
1980 nothing.sensitive = true
1982 if !$r270.active? && !$enhance.active? && !$delete.active?
1983 set_mousecursor_normal
1984 nothing.sensitive = false
1986 nothing.sensitive = true
1990 $r270.signal_connect('toggled') {
1992 set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
1993 one_click_explain_try.call
1995 $enhance.active = false
1996 $delete.active = false
1997 nothing.sensitive = true
1999 if !$r90.active? && !$enhance.active? && !$delete.active?
2000 set_mousecursor_normal
2001 nothing.sensitive = false
2003 nothing.sensitive = true
2007 $enhance.signal_connect('toggled') {
2009 set_mousecursor(Gdk::Cursor::SPRAYCAN)
2010 one_click_explain_try.call
2012 $r270.active = false
2013 $delete.active = false
2014 nothing.sensitive = true
2016 if !$r90.active? && !$r270.active? && !$delete.active?
2017 set_mousecursor_normal
2018 nothing.sensitive = false
2020 nothing.sensitive = true
2024 $delete.signal_connect('toggled') {
2026 set_mousecursor(Gdk::Cursor::PIRATE)
2027 one_click_explain_try.call
2029 $r270.active = false
2030 $enhance.active = false
2031 nothing.sensitive = true
2033 if !$r90.active? && !$r270.active? && !$enhance.active?
2034 set_mousecursor_normal
2035 nothing.sensitive = false
2037 nothing.sensitive = true
2041 nothing.signal_connect('clicked') {
2042 $r90.active = $r270.active = $enhance.active = $delete.active = false
2043 set_mousecursor_normal
2049 def create_main_window
2051 mb, tb = create_menu_and_toolbar
2053 # open_file('/home/gc/booh/foo')
2055 $albums_tv = Gtk::TreeView.new
2056 $albums_tv.set_size_request(120, -1)
2057 renderer = Gtk::CellRendererText.new
2058 column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 })
2059 $albums_tv.append_column(column)
2060 $albums_tv.set_headers_visible(false)
2061 $albums_tv.selection.signal_connect('changed') { |w|
2062 push_mousecursor_wait
2066 msg 3, "no selection"
2068 $current_path = $albums_ts.get_value(iter, 1)
2073 $albums_ts = Gtk::TreeStore.new(String, String)
2074 $albums_tv.set_model($albums_ts)
2075 $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
2077 albums_sw = Gtk::ScrolledWindow.new(nil, nil)
2078 albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
2079 albums_sw.add_with_viewport($albums_tv)
2081 $notebook = Gtk::Notebook.new
2082 create_subalbums_page
2083 $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
2085 $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
2087 $notebook.signal_connect('switch-page') { |w, page, num|
2089 $delete.active = false
2090 $delete.sensitive = false
2092 $delete.sensitive = true
2096 paned = Gtk::HPaned.new
2097 paned.pack1(albums_sw, false, false)
2098 paned.pack2($notebook, true, true)
2100 main_vbox = Gtk::VBox.new(false, 0)
2101 main_vbox.pack_start(mb, false, false)
2102 main_vbox.pack_start(tb, false, false)
2103 main_vbox.pack_start(paned, true, true)
2104 main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
2106 $main_window = Gtk::Window.new
2107 $main_window.add(main_vbox)
2108 $main_window.signal_connect('delete-event') {
2112 #- read/save size and position of window
2113 if $config['pos-x'] && $config['pos-y']
2114 $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
2116 $main_window.window_position = Gtk::Window::POS_CENTER
2118 msg 3, "size: #{$config['width']}x#{$config['height']}"
2119 $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
2120 $main_window.signal_connect('configure-event') {
2121 msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
2122 x, y = $main_window.window.root_origin
2123 width, height = $main_window.window.size
2124 $config['pos-x'] = x
2125 $config['pos-y'] = y
2126 $config['width'] = width
2127 $config['height'] = height
2131 $statusbar.push(0, utf8(_("Ready.")))
2132 $main_window.show_all
2135 Thread.abort_on_exception = true