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 <http://zarb.org/~gc/resource/gc_mail.png>
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.
27 require 'booh/gtkadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
44 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
46 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
50 puts _("Usage: %s [OPTION]...") % File.basename($0)
52 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
57 parser = GetoptLong.new
58 parser.set_options(*$options.collect { |ary| ary[0..2] })
60 parser.each_option do |name, arg|
66 when '--verbose-level'
67 $verbose_level = arg.to_i
80 $config_file = File.expand_path('~/.booh-gui-rc')
81 if File.readable?($config_file)
82 $xmldoc = REXML::Document.new(File.new($config_file))
83 $xmldoc.root.elements.each { |element|
84 txt = element.get_text
86 if txt.value =~ /~~~/ || element.name == 'last-opens'
87 $config[element.name] = txt.value.split(/~~~/)
89 $config[element.name] = txt.value
91 elsif element.elements.size == 0
92 $config[element.name] = ''
94 $config[element.name] = {}
97 $config[element.name][chld.name] = txt ? txt.value : nil
102 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
103 if !FileTest.directory?(File.expand_path('~/.booh'))
104 system("mkdir ~/.booh")
112 if !system("which convert >/dev/null 2>/dev/null")
113 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
114 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
117 if !system("which identify >/dev/null 2>/dev/null")
118 show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
119 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
121 missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
123 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
126 viewer_binary = $config['video-viewer'].split.first
127 if viewer_binary && !File.executable?(viewer_binary)
128 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
129 You should fix this in Edit/Preferences so that you can view videos.
131 Problem was: '%s' is not an executable file.") % viewer_binary), { :pos_centered => true, :not_transient => true })
136 if $config['last-opens'] && $config['last-opens'].size > 5
137 $config['last-opens'] = $config['last-opens'][-5, 5]
140 ios = File.open($config_file, "w")
141 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
142 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
143 $config.each_pair { |key, value|
144 elem = $xmldoc.root.add_element key
146 $config[key].each_pair { |subkey, subvalue|
147 subelem = elem.add_element subkey
148 subelem.add_text subvalue.to_s
150 elsif value.is_a? Array
151 elem.add_text value.join('~~~')
156 elem.add_text value.to_s
160 $xmldoc.write(ios, 0)
163 $tempfiles.each { |f|
168 def set_mousecursor(what, *widget)
169 if widget[0] && widget[0].window
170 widget[0].window.set_cursor(Gdk::Cursor.new(what))
172 if $main_window.window
173 $main_window.window.set_cursor(Gdk::Cursor.new(what))
175 $current_cursor = what
177 def set_mousecursor_wait(*widget)
178 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
179 if Thread.current == Thread.main
180 Gtk.main_iteration while Gtk.events_pending?
183 def set_mousecursor_normal(*widget)
184 gtk_thread_protect { set_mousecursor($save_cursor = Gdk::Cursor::LEFT_PTR, *widget) }
186 def push_mousecursor_wait(*widget)
187 if $current_cursor != Gdk::Cursor::WATCH
188 $save_cursor = $current_cursor
189 gtk_thread_protect { set_mousecursor_wait(*widget) }
192 def pop_mousecursor(*widget)
193 gtk_thread_protect { set_mousecursor($save_cursor || Gdk::Cursor::LEFT_PTR, *widget) }
197 source = $xmldoc.root.attributes['source']
198 dest = $xmldoc.root.attributes['destination']
199 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
202 def full_src_dir_to_rel(path, source)
203 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
206 def build_full_dest_filename(filename)
207 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
210 def save_undo(name, closure, *params)
211 UndoHandler.save_undo(name, closure, [ *params ])
212 $undo_tb.sensitive = $undo_mb.sensitive = true
213 $redo_tb.sensitive = $redo_mb.sensitive = false
216 def view_element(filename, closures)
217 if entry2type(filename) == 'video'
218 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
224 w = Gtk::Window.new.set_title(filename)
226 msg 3, "filename: #{filename}"
227 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
228 #- typically this file won't exist in case of videos; try with the largest thumbnail around
229 if !File.exists?(dest_img)
230 if entry2type(filename) == 'video'
231 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
232 if not alternatives.empty?
233 dest_img = alternatives[-1]
236 push_mousecursor_wait
237 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
239 if !File.exists?(dest_img)
240 msg 2, _("Could not generate fullscreen thumbnail!")
245 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)))
246 evt.signal_connect('button-press-event') { |this, event|
247 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
248 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
250 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
252 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
253 delete_item.signal_connect('activate') {
255 closures[:delete].call
258 menu.popup(nil, nil, event.button, event.time)
261 evt.signal_connect('button-release-event') { |this, event|
263 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
264 msg 3, "gesture delete: click-drag right button to the bottom"
266 closures[:delete].call
267 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
271 tooltips = Gtk::Tooltips.new
272 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
274 w.signal_connect('key-press-event') { |w,event|
275 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
277 closures[:delete].call
281 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
282 b.signal_connect('clicked') { w.destroy }
285 vb.pack_start(evt, false, false)
286 vb.pack_end(bottom, false, false)
289 w.signal_connect('delete-event') { w.destroy }
290 w.window_position = Gtk::Window::POS_CENTER
294 def scroll_upper(scrolledwindow, ypos_top)
295 newval = scrolledwindow.vadjustment.value -
296 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
297 if newval < scrolledwindow.vadjustment.lower
298 newval = scrolledwindow.vadjustment.lower
300 scrolledwindow.vadjustment.value = newval
303 def scroll_lower(scrolledwindow, ypos_bottom)
304 newval = scrolledwindow.vadjustment.value +
305 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
306 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
307 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
309 scrolledwindow.vadjustment.value = newval
312 def autoscroll_if_needed(scrolledwindow, image, textview)
313 #- autoscroll if cursor or image is not visible, if possible
314 if image && image.window || textview.window
315 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
316 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
317 current_miny_visible = scrolledwindow.vadjustment.value
318 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
319 if ypos_top < current_miny_visible
320 scroll_upper(scrolledwindow, ypos_top)
321 elsif ypos_bottom > current_maxy_visible
322 scroll_lower(scrolledwindow, ypos_bottom)
327 def create_editzone(scrolledwindow, pagenum, image)
328 frame = Gtk::Frame.new
329 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
330 frame.set_shadow_type(Gtk::SHADOW_IN)
331 textview.signal_connect('key-press-event') { |w, event|
332 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
333 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
334 scrolledwindow.signal_emit('key-press-event', event)
336 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
337 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
338 if event.keyval == Gdk::Keyval::GDK_Up
339 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
340 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
342 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
345 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
346 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
348 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
354 textview.signal_connect('focus-in-event') { |w, event|
355 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
359 candidate_undo_text = nil
360 textview.signal_connect('focus-in-event') { |w, event|
361 candidate_undo_text = textview.buffer.text
364 textview.signal_connect('key-release-event') { |w, event|
365 if candidate_undo_text && candidate_undo_text != textview.buffer.text
367 save_undo(_("text edit"),
369 save_text = textview.buffer.text
370 textview.buffer.text = text
372 $notebook.set_page(pagenum)
374 textview.buffer.text = save_text
376 $notebook.set_page(pagenum)
378 }, candidate_undo_text)
379 candidate_undo_text = nil
382 if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
383 autoscroll_if_needed(scrolledwindow, image, textview)
388 return [ frame, textview ]
391 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
393 if !$modified_pixbufs[thumbnail_img]
394 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
395 elsif !$modified_pixbufs[thumbnail_img][:orig]
396 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
399 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
402 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
403 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
404 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
405 if pixbuf.height > desired_y
406 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
407 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
408 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
413 if $modified_pixbufs[thumbnail_img][:whitebalance]
414 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
417 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
420 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
423 #- update rotate attribute
424 xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
426 $modified_pixbufs[thumbnail_img] ||= {}
427 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
428 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
430 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
433 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
436 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
438 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
440 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
441 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
443 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
444 $notebook.set_page(0)
445 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
450 def color_swap(xmldir, attributes_prefix)
452 if xmldir.attributes["#{attributes_prefix}color-swap"]
453 xmldir.delete_attribute("#{attributes_prefix}color-swap")
455 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
459 def enhance(xmldir, attributes_prefix)
461 if xmldir.attributes["#{attributes_prefix}enhance"]
462 xmldir.delete_attribute("#{attributes_prefix}enhance")
464 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
468 def change_frame_offset(xmldir, attributes_prefix, value)
470 xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
473 def ask_new_frame_offset(xmldir, attributes_prefix)
475 value = xmldir.attributes["#{attributes_prefix}frame-offset"]
480 dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
482 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
483 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
484 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
488 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
489 from. There are approximately 25 frames per second in a video.
492 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
493 entry.signal_connect('key-press-event') { |w, event|
494 if event.keyval == Gdk::Keyval::GDK_Return
495 dialog.response(Gtk::Dialog::RESPONSE_OK)
497 elsif event.keyval == Gdk::Keyval::GDK_Escape
498 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
501 false #- propagate if needed
505 dialog.window_position = Gtk::Window::POS_MOUSE
508 dialog.run { |response|
511 if response == Gtk::Dialog::RESPONSE_OK
513 msg 3, "changing frame offset to #{newval}"
514 return { :old => value, :new => newval }
521 def change_whitebalance(xmlelem, attributes_prefix, value)
523 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
526 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
528 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
529 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
530 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
531 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
532 destfile = "#{thumbnail_img}-orig-whitebalance.jpg"
533 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
534 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
535 $modified_pixbufs[thumbnail_img] ||= {}
536 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
537 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
538 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
541 $modified_pixbufs[thumbnail_img] ||= {}
542 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
544 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
547 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
548 #- init $modified_pixbufs correctly
549 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
551 value = xmlelem.attributes["#{attributes_prefix}white-balance"] || "0"
553 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
555 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
556 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
557 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
561 _("You can fix the <b>white balance</b> of the image, if your image is too blue
562 or too yellow because your camera didn't detect the light correctly. Drag the
563 slider below the image to the left for more blue, to the right for more yellow.
566 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
567 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
569 dialog.window_position = Gtk::Window::POS_MOUSE
573 timeout = Gtk.timeout_add(100) {
574 if hs.value != lastval
576 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
581 dialog.run { |response|
582 Gtk.timeout_remove(timeout)
583 if response == Gtk::Dialog::RESPONSE_OK
585 newval = hs.value.to_s
586 msg 3, "changing white balance to #{newval}"
588 return { :old => value, :new => newval }
590 $modified_pixbufs[thumbnail_img] ||= {}
591 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
592 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
599 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
600 system("rm -f '#{destfile}'")
601 #- type can be 'element' or 'subdir'
603 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
605 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
609 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
611 push_mousecursor_wait
612 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
615 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
621 def popup_thumbnail_menu(event, optionals, type, xmldir, attributes_prefix, possible_actions, closures)
622 distribute_multiple_call = Proc.new { |action, arg|
623 $selected_elements.each_key { |path|
624 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
626 if possible_actions[:can_multiple] && $selected_elements.length > 0
627 UndoHandler.begin_batch
628 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
629 UndoHandler.end_batch
631 closures[action].call(arg)
633 $selected_elements = {}
636 if optionals.include?('change_image')
637 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
638 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
639 changeimg.signal_connect('activate') { closures[:change].call }
640 menu.append( Gtk::SeparatorMenuItem.new)
642 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
643 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
644 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
645 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
646 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
647 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
648 if !possible_actions[:can_multiple] || $selected_elements.length == 0
649 menu.append( Gtk::SeparatorMenuItem.new)
650 if !possible_actions[:forbid_left]
651 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
652 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
653 moveleft.signal_connect('activate') { closures[:move].call('left') }
654 if !possible_actions[:can_left]
655 moveleft.sensitive = false
658 if !possible_actions[:forbid_right]
659 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
660 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
661 moveright.signal_connect('activate') { closures[:move].call('right') }
662 if !possible_actions[:can_right]
663 moveright.sensitive = false
666 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
667 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
668 moveup.signal_connect('activate') { closures[:move].call('up') }
669 if !possible_actions[:can_up]
670 moveup.sensitive = false
672 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
673 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
674 movedown.signal_connect('activate') { closures[:move].call('down') }
675 if !possible_actions[:can_down]
676 movedown.sensitive = false
680 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
681 menu.append( Gtk::SeparatorMenuItem.new)
682 menu.append( color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
683 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
684 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
685 menu.append( flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
686 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
687 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
688 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
689 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
690 frame_offset.signal_connect('activate') {
691 if possible_actions[:can_multiple] && $selected_elements.length > 0
692 if values = ask_new_frame_offset(nil, '')
693 distribute_multiple_call.call(:frame_offset, values)
696 closures[:frame_offset].call
701 menu.append( Gtk::SeparatorMenuItem.new)
702 if !possible_actions[:can_multiple] || $selected_elements.length == 0
703 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
704 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
705 whitebalance.signal_connect('activate') { closures[:whitebalance].call }
707 if !possible_actions[:can_multiple] || $selected_elements.length == 0
708 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
709 _("Enhance constrast"))))
711 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
713 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
714 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
715 if optionals.include?('delete')
716 menu.append( Gtk::SeparatorMenuItem.new)
717 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
718 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
719 if !possible_actions[:can_multiple] || $selected_elements.length == 0
720 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
721 paste_item.signal_connect('activate') { closures[:paste].call }
722 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
723 clear_item.signal_connect('activate') { $cuts = [] }
725 paste_item.sensitive = clear_item.sensitive = false
728 menu.append( Gtk::SeparatorMenuItem.new)
729 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
730 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
733 menu.popup(nil, nil, event.button, event.time)
736 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
739 frame1 = Gtk::Frame.new
740 fullpath = from_utf8("#{$current_path}/#{filename}")
742 my_gen_real_thumbnail = proc {
743 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
746 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
747 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
748 frame1.add(img = Gtk::Image.new)
749 my_gen_real_thumbnail.call
751 frame1.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
753 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
755 tooltips = Gtk::Tooltips.new
756 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
757 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
759 frame2, textview = create_editzone($autotable_sw, 1, img)
760 textview.buffer.text = utf8(caption)
761 textview.set_justification(Gtk::Justification::CENTER)
763 vbox = Gtk::VBox.new(false, 5)
764 vbox.pack_start(evtbox, false, false)
765 vbox.pack_start(frame2, false, false)
766 autotable.append(vbox, filename)
768 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
769 $vbox2widgets[vbox] = { :textview => textview, :image => img }
771 #- to be able to find widgets by name
772 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
774 cleanup_all_thumbnails = Proc.new {
775 #- remove out of sync images
776 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
777 for sizeobj in $images_size
778 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
783 rotate_and_cleanup = Proc.new { |angle|
784 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
785 cleanup_all_thumbnails.call
788 move = Proc.new { |direction|
789 do_method = "move_#{direction}"
790 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
792 done = autotable.method(do_method).call(vbox)
793 textview.grab_focus #- because if moving, focus is stolen
797 save_undo(_("move %s") % direction,
799 autotable.method(undo_method).call(vbox)
800 textview.grab_focus #- because if moving, focus is stolen
801 autoscroll_if_needed($autotable_sw, img, textview)
802 $notebook.set_page(1)
804 autotable.method(do_method).call(vbox)
805 textview.grab_focus #- because if moving, focus is stolen
806 autoscroll_if_needed($autotable_sw, img, textview)
807 $notebook.set_page(1)
813 color_swap_and_cleanup = Proc.new {
814 perform_color_swap_and_cleanup = Proc.new {
815 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
816 my_gen_real_thumbnail.call
819 cleanup_all_thumbnails.call
820 perform_color_swap_and_cleanup.call
822 save_undo(_("color swap"),
824 perform_color_swap_and_cleanup.call
826 autoscroll_if_needed($autotable_sw, img, textview)
827 $notebook.set_page(1)
829 perform_color_swap_and_cleanup.call
831 autoscroll_if_needed($autotable_sw, img, textview)
832 $notebook.set_page(1)
837 change_frame_offset_and_cleanup_real = Proc.new { |values|
838 perform_change_frame_offset_and_cleanup = Proc.new { |val|
839 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
840 my_gen_real_thumbnail.call
842 perform_change_frame_offset_and_cleanup.call(values[:new])
844 save_undo(_("specify frame offset"),
846 perform_change_frame_offset_and_cleanup.call(values[:old])
848 autoscroll_if_needed($autotable_sw, img, textview)
849 $notebook.set_page(1)
851 perform_change_frame_offset_and_cleanup.call(values[:new])
853 autoscroll_if_needed($autotable_sw, img, textview)
854 $notebook.set_page(1)
859 change_frame_offset_and_cleanup = Proc.new {
860 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
861 change_frame_offset_and_cleanup_real.call(values)
865 whitebalance_and_cleanup = Proc.new {
866 if values = ask_whitebalance(fullpath, thumbnail_img, img,
867 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
868 perform_change_whitebalance_and_cleanup = Proc.new { |val|
869 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
870 recalc_whitebalance(val, fullpath, thumbnail_img, img,
871 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
872 cleanup_all_thumbnails.call
874 perform_change_whitebalance_and_cleanup.call(values[:new])
876 save_undo(_("fix white balance"),
878 perform_change_whitebalance_and_cleanup.call(values[:old])
880 autoscroll_if_needed($autotable_sw, img, textview)
881 $notebook.set_page(1)
883 perform_change_whitebalance_and_cleanup.call(values[:new])
885 autoscroll_if_needed($autotable_sw, img, textview)
886 $notebook.set_page(1)
892 enhance_and_cleanup = Proc.new {
893 perform_enhance_and_cleanup = Proc.new {
894 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
895 my_gen_real_thumbnail.call
898 cleanup_all_thumbnails.call
899 perform_enhance_and_cleanup.call
901 save_undo(_("enhance"),
903 perform_enhance_and_cleanup.call
905 autoscroll_if_needed($autotable_sw, img, textview)
906 $notebook.set_page(1)
908 perform_enhance_and_cleanup.call
910 autoscroll_if_needed($autotable_sw, img, textview)
911 $notebook.set_page(1)
916 delete = Proc.new { |isacut|
917 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed?")), { :okcancel => true })
920 perform_delete = Proc.new {
921 after = autotable.get_next_widget(vbox)
923 after = autotable.get_previous_widget(vbox)
925 if $config['deleteondisk'] && !isacut
926 msg 3, "scheduling for delete: #{fullpath}"
927 $todelete << fullpath
929 autotable.remove(vbox)
931 $vbox2widgets[after][:textview].grab_focus
932 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
936 previous_pos = autotable.get_current_number(vbox)
940 if $xmldir.child_byname_notattr('dir', 'deleted')
941 $xmldir.delete_attribute('thumbnails-caption')
942 $xmldir.delete_attribute('thumbnails-captionfile')
944 $xmldir.add_attribute('deleted', 'true')
946 while moveup.parent.name == 'dir'
947 moveup = moveup.parent
948 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
949 moveup.add_attribute('deleted', 'true')
955 save_changes('forced')
956 populate_subalbums_treeview
958 save_undo(_("delete"),
960 autotable.reinsert(pos, vbox, filename)
961 $notebook.set_page(1)
962 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
964 msg 3, "removing deletion schedule of: #{fullpath}"
965 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
968 $notebook.set_page(1)
977 $cuts << { :vbox => vbox, :filename => filename }
978 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
983 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
986 autotable.queue_draws << proc {
987 $vbox2widgets[last[:vbox]][:textview].grab_focus
988 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
990 save_undo(_("paste"),
992 cuts.each { |elem| autotable.remove(elem[:vbox]) }
993 $notebook.set_page(1)
996 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
998 $notebook.set_page(1)
1001 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1006 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1007 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real }
1009 textview.signal_connect('key-press-event') { |w, event|
1012 x, y = autotable.get_current_pos(vbox)
1013 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1014 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1015 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1016 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1018 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1019 $vbox2widgets[widget_up][:textview].grab_focus
1026 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1028 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1029 $vbox2widgets[widget_down][:textview].grab_focus
1036 if event.keyval == Gdk::Keyval::GDK_Left
1039 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1046 rotate_and_cleanup.call(-90)
1049 if event.keyval == Gdk::Keyval::GDK_Right
1050 next_ = autotable.get_next_widget(vbox)
1051 if next_ && autotable.get_current_pos(next_)[0] > x
1053 $vbox2widgets[next_][:textview].grab_focus
1060 rotate_and_cleanup.call(90)
1063 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1066 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1067 view_element(filename, { :delete => delete })
1070 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1073 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1077 !propagate #- propagate if needed
1080 $ignore_next_release = false
1081 evtbox.signal_connect('button-press-event') { |w, event|
1082 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1083 if event.state & Gdk::Window::BUTTON3_MASK != 0
1084 #- gesture redo: hold right mouse button then click left mouse button
1085 $config['nogestures'] or perform_redo
1086 $ignore_next_release = true
1088 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1090 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1092 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1093 elsif $enhance.active?
1094 enhance_and_cleanup.call
1095 elsif $delete.active?
1099 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1102 $button1_pressed_autotable = true
1103 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1104 if event.state & Gdk::Window::BUTTON1_MASK != 0
1105 #- gesture undo: hold left mouse button then click right mouse button
1106 $config['nogestures'] or perform_undo
1107 $ignore_next_release = true
1109 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1110 view_element(filename, { :delete => delete })
1115 evtbox.signal_connect('button-release-event') { |w, event|
1116 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1117 if !$ignore_next_release
1118 x, y = autotable.get_current_pos(vbox)
1119 next_ = autotable.get_next_widget(vbox)
1120 popup_thumbnail_menu(event, ['delete'], type, $xmldir.elements["*[@filename='#{filename}']"], '',
1121 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1122 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true },
1123 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1124 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1125 :cut => cut, :paste => paste })
1127 $ignore_next_release = false
1128 $gesture_press = nil
1133 #- handle reordering with drag and drop
1134 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1135 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1136 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1137 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1140 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1142 #- mouse gesture first (dnd disables button-release-event)
1143 if $gesture_press && $gesture_press[:filename] == filename
1144 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1145 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1146 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1147 rotate_and_cleanup.call(angle)
1148 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1150 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1151 msg 3, "gesture delete: click-drag right button to the bottom"
1153 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1158 ctxt.targets.each { |target|
1159 if target.name == 'reorder-elements'
1160 move_dnd = Proc.new { |from,to|
1163 autotable.move(from, to)
1164 save_undo(_("reorder"),
1165 Proc.new { |from, to|
1167 autotable.move(to - 1, from)
1169 autotable.move(to, from + 1)
1171 $notebook.set_page(1)
1173 autotable.move(from, to)
1174 $notebook.set_page(1)
1179 if $multiple_dnd.size == 0
1180 move_dnd.call(selection_data.data.to_i,
1181 autotable.get_current_number(vbox))
1183 UndoHandler.begin_batch
1184 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1186 #- need to update current position between each call
1187 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1188 autotable.get_current_number(vbox))
1190 UndoHandler.end_batch
1201 def create_auto_table
1203 $autotable = Gtk::AutoTable.new(5)
1205 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1206 thumbnails_vb = Gtk::VBox.new(false, 5)
1208 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1209 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1210 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1211 thumbnails_vb.add($autotable)
1213 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1214 $autotable_sw.add_with_viewport(thumbnails_vb)
1216 #- follows stuff for handling multiple elements selection
1217 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1219 update_selected = Proc.new {
1220 $autotable.current_order.each { |path|
1221 w = $name2widgets[path][:evtbox].window
1222 xm = w.position[0] + w.size[0]/2
1223 ym = w.position[1] + w.size[1]/2
1224 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1225 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1226 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1227 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1230 if $selected_elements[path] && ! $selected_elements[path][:keep]
1231 if ((xm < press_x && xm < pos_x || xm > pos_x && xm > press_x) || (ym < press_y && ym < pos_y || ym > pos_y && ym > press_y))
1232 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1233 $selected_elements.delete(path)
1238 $autotable.signal_connect('realize') { |w,e|
1239 gc = Gdk::GC.new($autotable.window)
1240 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1241 gc.function = Gdk::GC::INVERT
1242 #- autoscroll handling for DND and multiple selections
1243 Gtk.timeout_add(100) {
1244 w, x, y, mask = $autotable.window.pointer
1245 if mask & Gdk::Window::BUTTON1_MASK != 0
1246 if y < $autotable_sw.vadjustment.value
1248 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1250 if $button1_pressed_autotable || press_x
1251 scroll_upper($autotable_sw, y)
1254 w, pos_x, pos_y = $autotable.window.pointer
1255 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1256 update_selected.call
1259 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1261 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1263 if $button1_pressed_autotable || press_x
1264 scroll_lower($autotable_sw, y)
1267 w, pos_x, pos_y = $autotable.window.pointer
1268 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1269 update_selected.call
1277 $autotable.signal_connect('button-press-event') { |w,e|
1279 if !$button1_pressed_autotable
1282 if e.state & Gdk::Window::SHIFT_MASK == 0
1283 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1284 $selected_elements = {}
1285 $statusbar.push(0, utf8(_("Nothing selected.")))
1287 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1289 set_mousecursor(Gdk::Cursor::TCROSS)
1293 $autotable.signal_connect('button-release-event') { |w,e|
1295 if $button1_pressed_autotable
1296 #- unselect all only now
1297 $multiple_dnd = $selected_elements.keys
1298 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1299 $selected_elements = {}
1300 $button1_pressed_autotable = false
1303 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1304 if $selected_elements.length > 0
1305 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1308 press_x = press_y = pos_x = pos_y = nil
1309 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1313 $autotable.signal_connect('motion-notify-event') { |w,e|
1316 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1320 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1321 update_selected.call
1327 def create_subalbums_page
1329 subalbums_hb = Gtk::HBox.new
1330 $subalbums_vb = Gtk::VBox.new(false, 5)
1331 subalbums_hb.pack_start($subalbums_vb, false, false)
1332 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1333 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1334 $subalbums_sw.add_with_viewport(subalbums_hb)
1337 def save_current_file
1341 ios = File.open($filename, "w")
1342 $xmldoc.write(ios, 0)
1347 def save_current_file_user
1348 save_tempfilename = $filename
1349 $filename = $orig_filename
1352 $generated_outofline = false
1353 $filename = save_tempfilename
1355 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1356 $todelete.each { |f|
1357 system("rm -f #{f}")
1361 def mark_document_as_dirty
1362 $xmldoc.elements.each('//dir') { |elem|
1363 elem.delete_attribute('already-generated')
1367 #- ret: true => ok false => cancel
1368 def ask_save_modifications(msg1, msg2, *options)
1370 options = options.size > 0 ? options[0] : {}
1372 if options[:disallow_cancel]
1373 dialog = Gtk::Dialog.new(msg1,
1375 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1376 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1377 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1379 dialog = Gtk::Dialog.new(msg1,
1381 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1382 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1383 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1384 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1386 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1387 dialog.vbox.add(Gtk::Label.new(msg2))
1388 dialog.window_position = Gtk::Window::POS_CENTER
1391 dialog.run { |response|
1393 if response == Gtk::Dialog::RESPONSE_YES
1394 save_current_file_user
1396 #- if we have generated an album but won't save modifications, we must remove
1397 #- already-generated markers in original file
1398 if $generated_outofline
1400 $xmldoc = REXML::Document.new File.new($orig_filename)
1401 mark_document_as_dirty
1402 ios = File.open($orig_filename, "w")
1403 $xmldoc.write(ios, 0)
1406 puts "exception: #{$!}"
1410 if response == Gtk::Dialog::RESPONSE_CANCEL
1413 $todelete = [] #- unconditionally clear the list of images/videos to delete
1419 def try_quit(*options)
1420 if ask_save_modifications(utf8(_("Save before quitting?")),
1421 utf8(_("Do you want to save your changes before quitting?")),
1427 def show_popup(parent, msg, *options)
1428 dialog = Gtk::Dialog.new
1429 dialog.title = utf8(_("Booh message"))
1430 lbl = Gtk::Label.new
1432 if options[0] && options[0][:centered]
1433 lbl.set_justify(Gtk::Justification::CENTER)
1435 if options[0] && options[0][:topwidget]
1436 dialog.vbox.add(options[0][:topwidget])
1438 dialog.vbox.add(lbl)
1439 if options[0] && options[0][:okcancel]
1440 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1442 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1444 dialog.set_default_size(200, 120)
1445 if options[0] && options[0][:pos_centered]
1446 dialog.window_position = Gtk::Window::POS_CENTER
1448 dialog.window_position = Gtk::Window::POS_MOUSE
1452 if !options[0] || !options[0][:not_transient]
1453 dialog.transient_for = parent
1454 dialog.run { |response|
1456 if options[0] && options[0][:okcancel]
1457 return response == Gtk::Dialog::RESPONSE_OK
1461 dialog.signal_connect('response') { dialog.destroy }
1465 def backend_wait_message(parent, msg, infopipe_path, mode)
1467 w.set_transient_for(parent)
1470 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1471 vb.pack_start(Gtk::Label.new(msg), false, false)
1473 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1474 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1475 if mode != 'one dir scan'
1476 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1478 if mode == 'web-album'
1479 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1480 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1482 vb.pack_start(Gtk::HSeparator.new, false, false)
1484 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1485 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1486 vb.pack_end(bottom, false, false)
1488 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1489 refresh_thread = Thread.new {
1490 directories_counter = 0
1491 while line = infopipe.gets
1492 if line =~ /^directories: (\d+), sizes: (\d+)/
1493 directories = $1.to_f + 1
1495 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1496 elements = $3.to_f + 1
1497 if mode == 'web-album'
1501 gtk_thread_protect { pb1_1.fraction = 0 }
1502 if mode != 'one dir scan'
1503 newtext = utf8(full_src_dir_to_rel($1, $2))
1504 newtext = '/' if newtext == ''
1505 gtk_thread_protect { pb1_2.text = newtext }
1506 directories_counter += 1
1507 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1509 elsif line =~ /^processing element$/
1510 element_counter += 1
1511 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1512 elsif line =~ /^processing size$/
1513 element_counter += 1
1514 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1515 elsif line =~ /^finished processing sizes$/
1516 gtk_thread_protect { pb1_1.fraction = 1 }
1517 elsif line =~ /^creating index.html$/
1518 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1519 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1520 directories_counter = 0
1521 elsif line =~ /^index.html: (.+)\|(.+)/
1522 newtext = utf8(full_src_dir_to_rel($1, $2))
1523 newtext = '/' if newtext == ''
1524 gtk_thread_protect { pb2.text = newtext }
1525 directories_counter += 1
1526 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1532 w.signal_connect('delete-event') { w.destroy }
1533 w.signal_connect('destroy') {
1534 Thread.kill(refresh_thread)
1535 gtk_thread_abandon #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1538 system("rm -f #{infopipe_path}")
1541 w.window_position = Gtk::Window::POS_CENTER
1547 def call_backend(cmd, waitmsg, mode, params)
1548 pipe = Tempfile.new("boohpipe")
1550 system("mkfifo #{pipe.path}")
1551 cmd += " --info-pipe #{pipe.path}"
1552 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1557 id, exitstatus = Process.waitpid2(pid)
1558 gtk_thread_protect { w8.destroy }
1560 if params[:successmsg]
1561 gtk_thread_protect { show_popup($main_window, params[:successmsg]) }
1563 if params[:closure_after]
1564 gtk_thread_protect(¶ms[:closure_after])
1566 elsif exitstatus == 15
1567 #- say nothing, user aborted
1569 if params[:failuremsg]
1570 gtk_thread_protect { show_popup($main_window, params[:failuremsg]) }
1577 button.signal_connect('clicked') {
1578 Process.kill('SIGTERM', pid)
1582 def save_changes(*forced)
1583 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1587 $xmldir.delete_attribute('already-generated')
1589 propagate_children = Proc.new { |xmldir|
1590 if xmldir.attributes['subdirs-caption']
1591 xmldir.delete_attribute('already-generated')
1593 xmldir.elements.each('dir') { |element|
1594 propagate_children.call(element)
1598 if $xmldir.child_byname_notattr('dir', 'deleted')
1599 new_title = $subalbums_title.buffer.text
1600 if new_title != $xmldir.attributes['subdirs-caption']
1601 parent = $xmldir.parent
1602 if parent.name == 'dir'
1603 parent.delete_attribute('already-generated')
1605 propagate_children.call($xmldir)
1607 $xmldir.add_attribute('subdirs-caption', new_title)
1608 $xmldir.elements.each('dir') { |element|
1609 if !element.attributes['deleted']
1610 path = element.attributes['path']
1611 newtext = $subalbums_edits[path][:editzone].buffer.text
1612 if element.attributes['subdirs-caption']
1613 if element.attributes['subdirs-caption'] != newtext
1614 propagate_children.call(element)
1616 element.add_attribute('subdirs-caption', newtext)
1617 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1619 if element.attributes['thumbnails-caption'] != newtext
1620 element.delete_attribute('already-generated')
1622 element.add_attribute('thumbnails-caption', newtext)
1623 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1629 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
1630 if $xmldir.attributes['thumbnails-caption']
1631 path = $xmldir.attributes['path']
1632 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1634 elsif $xmldir.attributes['thumbnails-caption']
1635 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
1638 #- remove and reinsert elements to reflect new ordering
1641 $xmldir.elements.each { |element|
1642 if element.name == 'image' || element.name == 'video'
1643 saves[element.attributes['filename']] = element.remove
1647 $autotable.current_order.each { |path|
1648 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1649 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1652 saves.each_key { |path|
1653 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1654 chld.add_attribute('deleted', 'true')
1658 def remove_all_captions
1661 $autotable.current_order.each { |path|
1662 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
1663 $name2widgets[File.basename(path)][:textview].buffer.text = ''
1665 save_undo(_("remove all captions"),
1667 texts.each_key { |key|
1668 $name2widgets[key][:textview].buffer.text = texts[key]
1670 $notebook.set_page(1)
1672 texts.each_key { |key|
1673 $name2widgets[key][:textview].buffer.text = ''
1675 $notebook.set_page(1)
1681 $selected_elements.each_key { |path|
1682 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1688 $selected_elements = {}
1692 $undo_tb.sensitive = $undo_mb.sensitive = false
1693 $redo_tb.sensitive = $redo_mb.sensitive = false
1699 $subalbums_vb.children.each { |chld|
1700 $subalbums_vb.remove(chld)
1702 $subalbums = Gtk::Table.new(0, 0, true)
1703 current_y_sub_albums = 0
1705 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
1706 $subalbums_edits = {}
1707 subalbums_counter = 0
1708 subalbums_edits_bypos = {}
1710 add_subalbum = Proc.new { |xmldir, counter|
1711 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
1712 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
1713 if xmldir == $xmldir
1714 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
1715 caption = xmldir.attributes['thumbnails-caption']
1716 captionfile, dummy = find_subalbum_caption_info(xmldir)
1717 infotype = 'thumbnails'
1719 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
1720 captionfile, caption = find_subalbum_caption_info(xmldir)
1721 infotype = find_subalbum_info_type(xmldir)
1723 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
1724 hbox = Gtk::HBox.new
1725 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
1727 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
1730 my_gen_real_thumbnail = proc {
1731 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
1734 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
1735 f.add(img = Gtk::Image.new)
1736 my_gen_real_thumbnail.call
1738 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
1740 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
1741 $subalbums.attach(hbox,
1742 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1744 frame, textview = create_editzone($subalbums_sw, 0, img)
1745 textview.buffer.text = caption
1746 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1747 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1749 change_image = Proc.new {
1750 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
1752 Gtk::FileChooser::ACTION_OPEN,
1754 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1755 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
1756 fc.transient_for = $main_window
1757 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))
1758 f.add(preview_img = Gtk::Image.new)
1760 fc.signal_connect('update-preview') { |w|
1762 if fc.preview_filename
1763 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
1764 fc.preview_widget_active = true
1766 rescue Gdk::PixbufError
1767 fc.preview_widget_active = false
1770 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1772 old_file = captionfile
1773 old_rotate = xmldir.attributes["#{infotype}-rotate"]
1774 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
1775 old_enhance = xmldir.attributes["#{infotype}-enhance"]
1776 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
1778 new_file = fc.filename
1779 msg 3, "new captionfile is: #{fc.filename}"
1780 perform_changefile = Proc.new {
1781 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
1782 $modified_pixbufs.delete(thumbnail_file)
1783 xmldir.delete_attribute("#{infotype}-rotate")
1784 xmldir.delete_attribute("#{infotype}-color-swap")
1785 xmldir.delete_attribute("#{infotype}-enhance")
1786 xmldir.delete_attribute("#{infotype}-frame-offset")
1787 my_gen_real_thumbnail.call
1789 perform_changefile.call
1791 save_undo(_("change caption file for sub-album"),
1793 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
1794 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
1795 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
1796 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
1797 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
1798 my_gen_real_thumbnail.call
1799 $notebook.set_page(0)
1801 perform_changefile.call
1802 $notebook.set_page(0)
1809 rotate_and_cleanup = Proc.new { |angle|
1810 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
1811 system("rm -f '#{thumbnail_file}'")
1814 move = Proc.new { |direction|
1817 save_changes('forced')
1818 if direction == 'up'
1819 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
1820 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
1821 subalbums_edits_bypos[oldpos - 1][:position] += 1
1823 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
1824 $subalbums_edits[xmldir.attributes['path']][:position] += 1
1825 subalbums_edits_bypos[oldpos + 1][:position] -= 1
1829 $xmldir.elements.each('dir') { |element|
1830 if (!element.attributes['deleted'])
1831 elems << [ element.attributes['path'], element.remove ]
1834 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
1835 each { |e| $xmldir.add_element(e[1]) }
1836 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
1837 $xmldir.elements.each('descendant::dir') { |elem|
1838 elem.delete_attribute('already-generated')
1843 color_swap_and_cleanup = Proc.new {
1844 perform_color_swap_and_cleanup = Proc.new {
1845 color_swap(xmldir, "#{infotype}-")
1846 my_gen_real_thumbnail.call
1848 perform_color_swap_and_cleanup.call
1850 save_undo(_("color swap"),
1852 perform_color_swap_and_cleanup.call
1853 $notebook.set_page(0)
1855 perform_color_swap_and_cleanup.call
1856 $notebook.set_page(0)
1861 change_frame_offset_and_cleanup = Proc.new {
1862 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
1863 perform_change_frame_offset_and_cleanup = Proc.new { |val|
1864 change_frame_offset(xmldir, "#{infotype}-", val)
1865 my_gen_real_thumbnail.call
1867 perform_change_frame_offset_and_cleanup.call(values[:new])
1869 save_undo(_("specify frame offset"),
1871 perform_change_frame_offset_and_cleanup.call(values[:old])
1872 $notebook.set_page(0)
1874 perform_change_frame_offset_and_cleanup.call(values[:new])
1875 $notebook.set_page(0)
1881 whitebalance_and_cleanup = Proc.new {
1882 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
1883 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
1884 perform_change_whitebalance_and_cleanup = Proc.new { |val|
1885 change_whitebalance(xmldir, "#{infotype}-", val)
1886 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
1887 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
1888 system("rm -f '#{thumbnail_file}'")
1890 perform_change_whitebalance_and_cleanup.call(values[:new])
1892 save_undo(_("fix white balance"),
1894 perform_change_whitebalance_and_cleanup.call(values[:old])
1895 $notebook.set_page(0)
1897 perform_change_whitebalance_and_cleanup.call(values[:new])
1898 $notebook.set_page(0)
1904 enhance_and_cleanup = Proc.new {
1905 perform_enhance_and_cleanup = Proc.new {
1906 enhance(xmldir, "#{infotype}-")
1907 my_gen_real_thumbnail.call
1910 perform_enhance_and_cleanup.call
1912 save_undo(_("enhance"),
1914 perform_enhance_and_cleanup.call
1915 $notebook.set_page(0)
1917 perform_enhance_and_cleanup.call
1918 $notebook.set_page(0)
1923 evtbox.signal_connect('button-press-event') { |w, event|
1924 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1926 rotate_and_cleanup.call(90)
1928 rotate_and_cleanup.call(-90)
1929 elsif $enhance.active?
1930 enhance_and_cleanup.call
1933 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1934 popup_thumbnail_menu(event, ['change_image'], entry2type(captionfile), xmldir, "#{infotype}-",
1935 { :forbid_left => true, :forbid_right => true,
1936 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter },
1937 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
1938 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
1940 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1945 evtbox.signal_connect('button-press-event') { |w, event|
1946 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
1950 evtbox.signal_connect('button-release-event') { |w, event|
1951 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
1952 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
1953 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
1954 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
1955 msg 3, "gesture rotate: #{angle}"
1956 rotate_and_cleanup.call(angle)
1959 $gesture_press = nil
1962 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
1963 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
1964 current_y_sub_albums += 1
1967 if $xmldir.child_byname_notattr('dir', 'deleted')
1969 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
1970 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
1971 $subalbums_title.set_justification(Gtk::Justification::CENTER)
1972 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1973 #- this album image/caption
1974 if $xmldir.attributes['thumbnails-caption']
1975 add_subalbum.call($xmldir, 0)
1978 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
1979 $xmldir.elements.each { |element|
1980 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
1981 #- element (image or video) of this album
1982 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
1983 msg 3, "dest_img: #{dest_img}"
1984 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
1985 total[element.name] += 1
1987 if element.name == 'dir' && !element.attributes['deleted']
1988 #- sub-album image/caption
1989 add_subalbum.call(element, subalbums_counter += 1)
1990 total[element.name] += 1
1993 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
1994 total['image'], total['video'], total['dir'] ]))
1995 $subalbums_vb.add($subalbums)
1996 $subalbums_vb.show_all
1998 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
1999 $notebook.get_tab_label($autotable_sw).sensitive = false
2000 $notebook.set_page(0)
2001 $thumbnails_title.buffer.text = ''
2003 $notebook.get_tab_label($autotable_sw).sensitive = true
2004 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2007 if !$xmldir.child_byname_notattr('dir', 'deleted')
2008 $notebook.get_tab_label($subalbums_sw).sensitive = false
2009 $notebook.set_page(1)
2011 $notebook.get_tab_label($subalbums_sw).sensitive = true
2015 def pixbuf_or_nil(filename)
2017 return Gdk::Pixbuf.new(filename)
2023 def theme_choose(current)
2024 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2026 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2027 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2028 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2030 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2031 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2032 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2033 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2034 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2035 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2036 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2037 treeview.signal_connect('button-press-event') { |w, event|
2038 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2039 dialog.response(Gtk::Dialog::RESPONSE_OK)
2043 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2045 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2048 iter[0] = File.basename(dir)
2049 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2050 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2051 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2052 if File.basename(dir) == current
2053 treeview.selection.select_iter(iter)
2057 dialog.set_default_size(700, 400)
2058 dialog.vbox.show_all
2059 dialog.run { |response|
2060 iter = treeview.selection.selected
2062 if response == Gtk::Dialog::RESPONSE_OK && iter
2063 return model.get_value(iter, 0)
2069 def populate_subalbums_treeview
2072 $subalbums_vb.children.each { |chld|
2073 $subalbums_vb.remove(chld)
2076 source = $xmldoc.root.attributes['source']
2077 msg 3, "source: #{source}"
2079 xmldir = $xmldoc.elements['//dir']
2080 if !xmldir || xmldir.attributes['path'] != source
2081 msg 1, _("Corrupted booh file...")
2085 append_dir_elem = Proc.new { |parent_iter, xmldir|
2086 child_iter = $albums_ts.append(parent_iter)
2087 child_iter[0] = File.basename(xmldir.attributes['path'])
2088 child_iter[1] = xmldir.attributes['path']
2089 msg 3, "puttin location: #{xmldir.attributes['path']}"
2090 xmldir.elements.each('dir') { |elem|
2091 if !elem.attributes['deleted']
2092 append_dir_elem.call(child_iter, elem)
2096 append_dir_elem.call(nil, xmldir)
2098 $albums_tv.expand_all
2099 $albums_tv.selection.select_iter($albums_ts.iter_first)
2102 def open_file(filename)
2106 $current_path = nil #- invalidate
2107 $modified_pixbufs = {}
2110 $subalbums_vb.children.each { |chld|
2111 $subalbums_vb.remove(chld)
2114 if !File.exists?(filename)
2115 return utf8(_("File not found."))
2119 $xmldoc = REXML::Document.new File.new(filename)
2124 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2125 if entry2type(filename).nil?
2126 return utf8(_("Not a booh file!"))
2128 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."))
2132 if !source = $xmldoc.root.attributes['source']
2133 return utf8(_("Corrupted booh file..."))
2136 if !dest = $xmldoc.root.attributes['destination']
2137 return utf8(_("Corrupted booh file..."))
2140 if !theme = $xmldoc.root.attributes['theme']
2141 return utf8(_("Corrupted booh file..."))
2144 if $xmldoc.root.attributes['version'] != $VERSION
2145 msg 2, _("File's version %s, booh version now #{$VERSION}, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2146 mark_document_as_dirty
2147 $xmldoc.root.add_attribute('version', $VERSION)
2150 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2151 optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2152 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2154 $filename = filename
2155 select_theme(theme, limit_sizes, optimizefor32, nperrow)
2156 $default_size['thumbnails'] =~ /(.*)x(.*)/
2157 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2158 $albums_thumbnail_size =~ /(.*)x(.*)/
2159 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2161 populate_subalbums_treeview
2163 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge.sensitive = $generate.sensitive = $properties.sensitive = $remove_all_captions.sensitive = true
2167 def open_file_user(filename)
2168 result = open_file(filename)
2170 $config['last-opens'] ||= []
2171 if $config['last-opens'][-1] != utf8(filename)
2172 $config['last-opens'] << utf8(filename)
2174 $orig_filename = $filename
2175 tmp = Tempfile.new("boohtemp")
2178 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2180 $tempfiles << $filename << "#{$filename}.backup"
2182 $orig_filename = nil
2188 if !ask_save_modifications(utf8(_("Save this album?")),
2189 utf8(_("Do you want to save the changes to this album?")),
2190 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2193 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2195 Gtk::FileChooser::ACTION_OPEN,
2197 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2198 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2199 fc.set_current_folder(File.expand_path("~/.booh"))
2200 fc.transient_for = $main_window
2203 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2204 push_mousecursor_wait(fc)
2205 msg = open_file_user(fc.filename)
2220 def additional_booh_options
2223 options += "--mproc #{$config['mproc'].to_i} "
2225 if $config['emptycomments']
2226 options += "--empty-comments "
2232 if !ask_save_modifications(utf8(_("Save this album?")),
2233 utf8(_("Do you want to save the changes to this album?")),
2234 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2237 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2239 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2240 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2241 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2243 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2244 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2245 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2246 tbl.attach(src = Gtk::Entry.new,
2247 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2248 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2249 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2250 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2251 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2252 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2253 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2254 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2255 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2256 tbl.attach(dest = Gtk::Entry.new,
2257 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2258 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2259 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2260 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2261 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2262 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2263 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2264 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2265 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2267 tooltips = Gtk::Tooltips.new
2268 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2269 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2270 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2271 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2272 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2273 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2274 tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)")), nil)
2275 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2276 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2278 src_nb_calculated_for = ''
2280 process_src_nb = Proc.new {
2281 if src.text != src_nb_calculated_for
2282 src_nb_calculated_for = src.text
2284 Thread.kill(src_nb_thread)
2287 if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2288 if File.readable?(from_utf8(src_nb_calculated_for))
2289 src_nb_thread = Thread.new {
2290 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2291 total = { 'image' => 0, 'video' => 0, nil => 0 }
2292 `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2293 if File.basename(dir) =~ /^\./
2297 Dir.entries(dir.chomp).each { |file|
2298 total[entry2type(file)] += 1
2300 rescue Errno::EACCES, Errno::ENOENT
2304 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2308 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2311 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2316 timeout_src_nb = Gtk.timeout_add(100) {
2320 src_browse.signal_connect('clicked') {
2321 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2323 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2325 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2326 fc.transient_for = $main_window
2327 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2328 src.text = utf8(fc.filename)
2330 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2335 dest_browse.signal_connect('clicked') {
2336 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2338 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2340 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2341 fc.transient_for = $main_window
2342 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2343 dest.text = utf8(fc.filename)
2348 conf_browse.signal_connect('clicked') {
2349 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2351 Gtk::FileChooser::ACTION_SAVE,
2353 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2354 fc.transient_for = $main_window
2355 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2356 fc.set_current_folder(File.expand_path("~/.booh"))
2357 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2358 conf.text = utf8(fc.filename)
2365 recreate_theme_config = proc {
2366 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2368 select_theme(theme_button.label, 'all', optimize432.active?, nil)
2369 $images_size.each { |s|
2370 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2374 tooltips.set_tip(cb, utf8(s['description']), nil)
2375 theme_sizes << { :widget => cb, :value => s['name'] }
2377 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2378 tooltips = Gtk::Tooltips.new
2379 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2380 theme_sizes << { :widget => cb, :value => 'original' }
2383 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2386 $allowed_N_values.each { |n|
2388 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2390 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2395 nperrows << { :widget => rb, :value => n }
2397 nperrowradios.show_all
2399 recreate_theme_config.call
2401 theme_button.signal_connect('clicked') {
2402 if newtheme = theme_choose(theme_button.label)
2403 theme_button.label = newtheme
2404 recreate_theme_config.call
2408 dialog.vbox.add(frame1)
2409 dialog.vbox.add(frame2)
2410 dialog.window_position = Gtk::Window::POS_MOUSE
2416 dialog.run { |response|
2417 if response == Gtk::Dialog::RESPONSE_OK
2418 srcdir = from_utf8(src.text)
2419 destdir = from_utf8(dest.text)
2420 if !File.directory?(srcdir)
2421 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2423 elsif conf.text == ''
2424 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2426 elsif File.directory?(from_utf8(conf.text))
2427 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2429 elsif destdir != make_dest_filename(destdir)
2430 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2432 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2433 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2435 elsif File.exists?(destdir) && !File.directory?(destdir)
2436 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2438 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2439 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2441 system("mkdir '#{destdir}'")
2442 if !File.directory?(destdir)
2443 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2454 srcdir = from_utf8(src.text)
2455 destdir = from_utf8(dest.text)
2456 configskel = File.expand_path(from_utf8(conf.text))
2457 theme = theme_button.label
2458 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2459 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2460 opt432 = optimize432.active?
2462 Thread.kill(src_nb_thread)
2463 gtk_thread_abandon #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2466 Gtk.timeout_remove(timeout_src_nb)
2469 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2470 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2471 "#{opt432 ? '--optimize-for-32' : ''} #{additional_booh_options}",
2472 utf8(_("Please wait while scanning source directory...")),
2474 { :closure_after => proc { open_file_user(configskel) } })
2479 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2481 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2482 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2483 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2485 source = $xmldoc.root.attributes['source']
2486 dest = $xmldoc.root.attributes['destination']
2487 theme = $xmldoc.root.attributes['theme']
2488 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2489 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2490 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2492 limit_sizes = limit_sizes.split(/,/)
2495 tooltips = Gtk::Tooltips.new
2496 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2497 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2498 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2499 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2500 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2501 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2502 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2503 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>')),
2504 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2505 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2506 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2507 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>')),
2508 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2510 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2511 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2512 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2513 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2514 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2515 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2516 tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)")), nil)
2517 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2518 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2522 recreate_theme_config = proc {
2523 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2525 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2527 $images_size.each { |s|
2528 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2530 if limit_sizes.include?(s['name'])
2538 tooltips.set_tip(cb, utf8(s['description']), nil)
2539 theme_sizes << { :widget => cb, :value => s['name'] }
2541 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2542 tooltips = Gtk::Tooltips.new
2543 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2544 if limit_sizes && limit_sizes.include?('original')
2547 theme_sizes << { :widget => cb, :value => 'original' }
2550 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2553 $allowed_N_values.each { |n|
2555 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2557 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2559 nperrowradios.add(Gtk::Label.new(' '))
2560 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
2563 nperrows << { :widget => rb, :value => n.to_s }
2565 nperrowradios.show_all
2567 recreate_theme_config.call
2569 theme_button.signal_connect('clicked') {
2570 if newtheme = theme_choose(theme_button.label)
2573 theme_button.label = newtheme
2574 recreate_theme_config.call
2578 dialog.vbox.add(frame1)
2579 dialog.vbox.add(frame2)
2580 dialog.window_position = Gtk::Window::POS_MOUSE
2586 dialog.run { |response|
2587 if response == Gtk::Dialog::RESPONSE_OK
2588 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2589 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2598 save_theme = theme_button.label
2599 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
2600 save_opt432 = optimize432.active?
2601 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2604 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow)
2605 mark_document_as_dirty
2607 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
2608 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
2609 "#{save_opt432 ? '--optimize-for-32' : ''} #{additional_booh_options}",
2610 utf8(_("Please wait while scanning source directory...")),
2612 { :closure_after => proc {
2613 open_file($filename)
2622 sel = $albums_tv.selection.selected_rows
2624 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2625 "--verbose-level #{$verbose_level} #{additional_booh_options}",
2626 utf8(_("Please wait while scanning source directory...")),
2628 { :closure_after => proc {
2629 open_file($filename)
2630 $albums_tv.selection.select_path(sel[0])
2638 theme = $xmldoc.root.attributes['theme']
2639 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2641 limit_sizes = "--sizes #{limit_sizes}"
2643 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
2644 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
2645 utf8(_("Please wait while scanning source directory...")),
2647 { :closure_after => proc {
2648 open_file($filename)
2654 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
2656 Gtk::FileChooser::ACTION_SAVE,
2658 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2659 fc.transient_for = $main_window
2660 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2661 fc.set_current_folder(File.expand_path("~/.booh"))
2662 fc.filename = $orig_filename
2663 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2664 $orig_filename = fc.filename
2665 save_current_file_user
2671 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
2673 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2674 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2675 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2677 dialog.vbox.add(notebook = Gtk::Notebook.new)
2678 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
2679 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
2680 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2681 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
2682 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2683 tooltips = Gtk::Tooltips.new
2684 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
2685 for example: /usr/bin/mplayer %f")), nil)
2686 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
2687 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2688 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)),
2689 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2690 tooltips.set_tip(smp_check, utf8(_("When activated, this option allows the thumbnails creation to run faster. However, if you don't have a multi-processor machine, this will only slow down processing!")), nil)
2691 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
2692 0, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2693 tooltips.set_tip(nogestures_check, utf8(_("Mouse gestures are 'unusual' mouse movements triggering special actions, and are great for speeding up your editions. Get details on available mouse gestures from the Help menu.")), nil)
2694 tbl.attach(emptycomments_check = Gtk::CheckButton.new(utf8(_("Use empty comments for new albums"))),
2695 0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2696 tooltips.set_tip(emptycomments_check, utf8(_("Normally, filenames are used as comments for new albums. Check this if you prefer empty comments.")), nil)
2697 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
2698 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
2699 tooltips.set_tip(deleteondisk_check, utf8(_("Normally, deleting an image or video in booh only removes it from the web-album. If you check this option, the original file in source directory will be removed as well. Undo is possible, since actual deletion is performed only when web-album is saved.")), nil)
2700 smp_check.signal_connect('toggled') {
2701 if smp_check.active?
2702 smp_hbox.sensitive = true
2704 smp_hbox.sensitive = false
2708 smp_check.active = true
2709 smp_spin.value = $config['mproc'].to_i
2711 nogestures_check.active = $config['nogestures']
2712 emptycomments_check.active = $config['emptycomments']
2713 deleteondisk_check.active = $config['deleteondisk']
2715 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
2716 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
2717 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2718 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
2719 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2721 dialog.vbox.show_all
2722 dialog.run { |response|
2723 if response == Gtk::Dialog::RESPONSE_OK
2724 $config['video-viewer'] = video_viewer_entry.text
2725 if smp_check.active?
2726 $config['mproc'] = smp_spin.value.to_i
2728 $config.delete('mproc')
2730 $config['nogestures'] = nogestures_check.active?
2731 $config['emptycomments'] = emptycomments_check.active?
2732 $config['deleteondisk'] = deleteondisk_check.active?
2734 $config['convert-enhance'] = enhance_entry.text
2741 if $undo_tb.sensitive?
2742 $redo_tb.sensitive = $redo_mb.sensitive = true
2743 if not more_undoes = UndoHandler.undo($statusbar)
2744 $undo_tb.sensitive = $undo_mb.sensitive = false
2750 if $redo_tb.sensitive?
2751 $undo_tb.sensitive = $undo_mb.sensitive = true
2752 if not more_redoes = UndoHandler.redo($statusbar)
2753 $redo_tb.sensitive = $redo_mb.sensitive = false
2758 def show_one_click_explanation(intro)
2759 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
2761 %s When such a tool is activated
2762 (<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
2763 on a thumbnail will immediately apply the desired action.
2765 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
2769 def create_menu_and_toolbar
2772 mb = Gtk::MenuBar.new
2774 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
2775 filesubmenu = Gtk::Menu.new
2776 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
2777 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
2778 filesubmenu.append( Gtk::SeparatorMenuItem.new)
2779 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
2780 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
2781 filesubmenu.append( Gtk::SeparatorMenuItem.new)
2782 tooltips = Gtk::Tooltips.new
2783 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
2784 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
2785 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
2786 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
2787 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
2788 tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums")), nil)
2789 filesubmenu.append( Gtk::SeparatorMenuItem.new)
2790 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
2791 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
2792 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
2793 filesubmenu.append( Gtk::SeparatorMenuItem.new)
2794 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
2795 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
2796 filesubmenu.append( Gtk::SeparatorMenuItem.new)
2797 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
2798 filemenu.set_submenu(filesubmenu)
2801 new.signal_connect('activate') { new_album }
2802 open.signal_connect('activate') { open_file_popup }
2803 $save.signal_connect('activate') { save_current_file_user }
2804 $save_as.signal_connect('activate') { save_as_do }
2805 $merge_current.signal_connect('activate') { merge_current }
2806 $merge.signal_connect('activate') { merge }
2807 $generate.signal_connect('activate') {
2809 call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
2810 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
2812 { :successmsg => utf8(_("Your web-album is now ready in directory `%s'.") % $xmldoc.root.attributes['destination']),
2813 :failuremsg => utf8(_("There was something wrong when generating the web-album, sorry.")),
2814 :closure_after => proc {
2815 $xmldoc.elements.each('//dir') { |elem|
2816 elem.add_attribute('already-generated', 'true')
2818 UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
2819 $undo_tb.sensitive = $undo_mb.sensitive = false
2820 $redo_tb.sensitive = $redo_mb.sensitive = false
2822 $generated_outofline = true
2825 $properties.signal_connect('activate') { properties }
2827 quit.signal_connect('activate') { try_quit }
2829 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
2830 editsubmenu = Gtk::Menu.new
2831 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
2832 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
2833 editsubmenu.append( Gtk::SeparatorMenuItem.new)
2834 editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
2835 $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
2836 tooltips.set_tip($remove_all_captions, utf8(_("Mainly useful when you don't want to type any caption, that will remove default captions made of filenames")), nil)
2837 editsubmenu.append( Gtk::SeparatorMenuItem.new)
2838 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
2839 editmenu.set_submenu(editsubmenu)
2842 $remove_all_captions.signal_connect('activate') { remove_all_captions }
2844 prefs.signal_connect('activate') { preferences }
2846 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
2847 helpsubmenu = Gtk::Menu.new
2848 helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
2849 one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
2850 helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
2851 speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
2852 helpsubmenu.append( Gtk::SeparatorMenuItem.new)
2853 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
2854 helpmenu.set_submenu(helpsubmenu)
2857 one_click.signal_connect('activate') {
2858 show_one_click_explanation(_("One-Click tools are available in the toolbar."))
2861 speed.signal_connect('activate') {
2862 show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
2864 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
2865 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
2866 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
2867 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
2868 <span foreground='darkblue'>Control-Delete</span>: delete image
2869 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
2870 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
2871 <span foreground='darkblue'>Control-z</span>: undo
2872 <span foreground='darkblue'>Control-r</span>: redo
2874 <span size='large' weight='bold'>Mouse gestures:</span>
2876 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
2877 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
2879 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
2880 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
2881 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
2882 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
2883 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
2884 ")), { :pos_centered => true, :not_transient => true })
2888 about.signal_connect('activate') {
2889 show_popup($main_window, utf8(_("<span size='x-large' weight='bold'>Booh %s</span>
2891 <i>``The Web-Album of choice for discriminating Linux users''</i>
2893 Copyright (c) 2005 Guillaume Cottenceau
2898 Japanese: Masao Mutoh
2899 French: Guillaume Cottenceau") % $VERSION), { :centered => true, :pos_centered => true, :topwidget => Gtk::Image.new("#{$FPATH}/images/logo.png") })
2904 tb = Gtk::Toolbar.new
2906 tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
2907 open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
2908 open.menu = Gtk::Menu.new
2909 open.signal_connect('clicked') { open_file_popup }
2910 open.signal_connect('show-menu') {
2911 lastopens = Gtk::Menu.new
2913 if $config['last-opens']
2914 $config['last-opens'].reverse.each { |e|
2915 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
2916 item.signal_connect('activate') {
2917 if ask_save_modifications(utf8(_("Save this album?")),
2918 utf8(_("Do you want to save the changes to this album?")),
2919 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2920 push_mousecursor_wait
2921 msg = open_file_user(from_utf8(e))
2924 show_popup($main_window, msg)
2932 open.menu = lastopens
2935 tb.insert(-1, Gtk::SeparatorToolItem.new)
2937 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
2938 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
2939 $r90.label = utf8(_("Rotate"))
2940 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
2941 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
2942 $r270.label = utf8(_("Rotate"))
2943 tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
2944 $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
2945 $enhance.label = utf8(_("Enhance"))
2946 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
2947 $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
2948 tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
2949 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
2950 nothing.label = utf8(_("None"))
2952 tb.insert(-1, Gtk::SeparatorToolItem.new)
2954 tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
2955 tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
2958 $undo_tb.signal_connect('clicked') { perform_undo }
2959 $undo_mb.signal_connect('activate') { perform_undo }
2960 $redo_tb.signal_connect('clicked') { perform_redo }
2961 $redo_mb.signal_connect('activate') { perform_redo }
2963 one_click_explain_try = Proc.new {
2964 if !$config['one-click-explained']
2965 show_one_click_explanation(_("You have just clicked on a One-Click tool."))
2966 $config['one-click-explained'] = true
2970 $r90.signal_connect('toggled') {
2972 set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
2973 one_click_explain_try.call
2974 $r270.active = false
2975 $enhance.active = false
2976 $delete.active = false
2977 nothing.sensitive = true
2979 if !$r270.active? && !$enhance.active? && !$delete.active?
2980 set_mousecursor_normal
2981 nothing.sensitive = false
2983 nothing.sensitive = true
2987 $r270.signal_connect('toggled') {
2989 set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
2990 one_click_explain_try.call
2992 $enhance.active = false
2993 $delete.active = false
2994 nothing.sensitive = true
2996 if !$r90.active? && !$enhance.active? && !$delete.active?
2997 set_mousecursor_normal
2998 nothing.sensitive = false
3000 nothing.sensitive = true
3004 $enhance.signal_connect('toggled') {
3006 set_mousecursor(Gdk::Cursor::SPRAYCAN)
3007 one_click_explain_try.call
3009 $r270.active = false
3010 $delete.active = false
3011 nothing.sensitive = true
3013 if !$r90.active? && !$r270.active? && !$delete.active?
3014 set_mousecursor_normal
3015 nothing.sensitive = false
3017 nothing.sensitive = true
3021 $delete.signal_connect('toggled') {
3023 set_mousecursor(Gdk::Cursor::PIRATE)
3024 one_click_explain_try.call
3026 $r270.active = false
3027 $enhance.active = false
3028 nothing.sensitive = true
3030 if !$r90.active? && !$r270.active? && !$enhance.active?
3031 set_mousecursor_normal
3032 nothing.sensitive = false
3034 nothing.sensitive = true
3038 nothing.signal_connect('clicked') {
3039 $r90.active = $r270.active = $enhance.active = $delete.active = false
3040 set_mousecursor_normal
3046 def gtk_thread_protect(&proc)
3047 if Thread.current == Thread.main
3050 $protect_gtk_pending_calls.synchronize {
3051 $gtk_pending_calls << proc
3056 def gtk_thread_abandon
3057 $protect_gtk_pending_calls.try_lock
3058 $gtk_pending_calls = []
3059 $protect_gtk_pending_calls.unlock
3062 def create_main_window
3064 mb, tb = create_menu_and_toolbar
3066 $albums_tv = Gtk::TreeView.new
3067 $albums_tv.set_size_request(120, -1)
3068 renderer = Gtk::CellRendererText.new
3069 column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 })
3070 $albums_tv.append_column(column)
3071 $albums_tv.set_headers_visible(false)
3072 $albums_tv.selection.signal_connect('changed') { |w|
3073 push_mousecursor_wait
3077 msg 3, "no selection"
3079 $current_path = $albums_ts.get_value(iter, 1)
3084 $albums_ts = Gtk::TreeStore.new(String, String)
3085 $albums_tv.set_model($albums_ts)
3086 $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
3088 albums_sw = Gtk::ScrolledWindow.new(nil, nil)
3089 albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
3090 albums_sw.add_with_viewport($albums_tv)
3092 $notebook = Gtk::Notebook.new
3093 create_subalbums_page
3094 $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
3096 $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
3098 $notebook.signal_connect('switch-page') { |w, page, num|
3100 $delete.active = false
3101 $delete.sensitive = false
3103 $delete.sensitive = true
3105 if $xmldir && $subalbums_edits[$xmldir.attributes['path']] && textview = $subalbums_edits[$xmldir.attributes['path']][:editzone]
3107 textview.buffer.text = $thumbnails_title.buffer.text
3109 if $notebook.get_tab_label($autotable_sw).sensitive?
3110 $thumbnails_title.buffer.text = textview.buffer.text
3116 paned = Gtk::HPaned.new
3117 paned.pack1(albums_sw, false, false)
3118 paned.pack2($notebook, true, true)
3120 main_vbox = Gtk::VBox.new(false, 0)
3121 main_vbox.pack_start(mb, false, false)
3122 main_vbox.pack_start(tb, false, false)
3123 main_vbox.pack_start(paned, true, true)
3124 main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
3126 $main_window = Gtk::Window.new
3127 $main_window.add(main_vbox)
3128 $main_window.signal_connect('delete-event') {
3129 try_quit({ :disallow_cancel => true })
3132 #- read/save size and position of window
3133 if $config['pos-x'] && $config['pos-y']
3134 $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
3136 $main_window.window_position = Gtk::Window::POS_CENTER
3138 msg 3, "size: #{$config['width']}x#{$config['height']}"
3139 $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
3140 $main_window.signal_connect('configure-event') {
3141 msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
3142 x, y = $main_window.window.root_origin
3143 width, height = $main_window.window.size
3144 $config['pos-x'] = x
3145 $config['pos-y'] = y
3146 $config['width'] = width
3147 $config['height'] = height
3151 $protect_gtk_pending_calls = Mutex.new
3152 $gtk_pending_calls = []
3153 Gtk.timeout_add(100) {
3154 $protect_gtk_pending_calls.synchronize {
3155 $gtk_pending_calls.each { |c| c.call }
3156 $gtk_pending_calls = []
3161 $statusbar.push(0, utf8(_("Ready.")))
3162 $main_window.show_all
3165 Thread.abort_on_exception = true
3175 open_file_user(ARGV[0])