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)") ],
49 #- default values for some globals
53 $ignore_videos = false
54 $button1_pressed_autotable = false
55 $generated_outofline = false
58 puts _("Usage: %s [OPTION]...") % File.basename($0)
60 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
65 parser = GetoptLong.new
66 parser.set_options(*$options.collect { |ary| ary[0..2] })
68 parser.each_option do |name, arg|
74 when '--verbose-level'
75 $verbose_level = arg.to_i
88 $config_file = File.expand_path('~/.booh-gui-rc')
89 if File.readable?($config_file)
90 $xmldoc = REXML::Document.new(File.new($config_file))
91 $xmldoc.root.elements.each { |element|
92 txt = element.get_text
94 if txt.value =~ /~~~/ || element.name == 'last-opens'
95 $config[element.name] = txt.value.split(/~~~/)
97 $config[element.name] = txt.value
99 elsif element.elements.size == 0
100 $config[element.name] = ''
102 $config[element.name] = {}
103 element.each { |chld|
105 $config[element.name][chld.name] = txt ? txt.value : nil
110 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
111 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
112 if !FileTest.directory?(File.expand_path('~/.booh'))
113 system("mkdir ~/.booh")
121 if !system("which convert >/dev/null 2>/dev/null")
122 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
123 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
126 if !system("which identify >/dev/null 2>/dev/null")
127 show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
128 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
130 missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
132 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
135 viewer_binary = $config['video-viewer'].split.first
136 if viewer_binary && !File.executable?(viewer_binary)
137 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
138 You should fix this in Edit/Preferences so that you can view videos.
140 Problem was: '%s' is not an executable file.
141 Hint: don't forget to specify the full path to the executable,
142 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
144 browser_binary = $config['browser'].split.first
145 if browser_binary && !File.executable?(browser_binary)
146 show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
147 You should fix this in Edit/Preferences so that you can open URLs.
149 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
154 if $config['last-opens'] && $config['last-opens'].size > 5
155 $config['last-opens'] = $config['last-opens'][-5, 5]
158 ios = File.open($config_file, "w")
159 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
160 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
161 $config.each_pair { |key, value|
162 elem = $xmldoc.root.add_element key
164 $config[key].each_pair { |subkey, subvalue|
165 subelem = elem.add_element subkey
166 subelem.add_text subvalue.to_s
168 elsif value.is_a? Array
169 elem.add_text value.join('~~~')
174 elem.add_text value.to_s
178 $xmldoc.write(ios, 0)
181 $tempfiles.each { |f|
186 def set_mousecursor(what, *widget)
187 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
188 if widget[0] && widget[0].window
189 widget[0].window.cursor = cursor
191 if $main_window.window
192 $main_window.window.cursor = cursor
194 $current_cursor = what
196 def set_mousecursor_wait(*widget)
197 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
198 if Thread.current == Thread.main
199 Gtk.main_iteration while Gtk.events_pending?
202 def set_mousecursor_normal(*widget)
203 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
205 def push_mousecursor_wait(*widget)
206 if $current_cursor != Gdk::Cursor::WATCH
207 $save_cursor = $current_cursor
208 gtk_thread_protect { set_mousecursor_wait(*widget) }
211 def pop_mousecursor(*widget)
212 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
216 source = $xmldoc.root.attributes['source']
217 dest = $xmldoc.root.attributes['destination']
218 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
221 def full_src_dir_to_rel(path, source)
222 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
225 def build_full_dest_filename(filename)
226 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
229 def save_undo(name, closure, *params)
230 UndoHandler.save_undo(name, closure, [ *params ])
231 $undo_tb.sensitive = $undo_mb.sensitive = true
232 $redo_tb.sensitive = $redo_mb.sensitive = false
235 def view_element(filename, closures)
236 if entry2type(filename) == 'video'
237 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
243 w = Gtk::Window.new.set_title(filename)
245 msg 3, "filename: #{filename}"
246 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
247 #- typically this file won't exist in case of videos; try with the largest thumbnail around
248 if !File.exists?(dest_img)
249 if entry2type(filename) == 'video'
250 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
251 if not alternatives.empty?
252 dest_img = alternatives[-1]
255 push_mousecursor_wait
256 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
258 if !File.exists?(dest_img)
259 msg 2, _("Could not generate fullscreen thumbnail!")
264 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)))
265 evt.signal_connect('button-press-event') { |this, event|
266 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
267 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
269 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
271 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
272 delete_item.signal_connect('activate') {
274 closures[:delete].call
277 menu.popup(nil, nil, event.button, event.time)
280 evt.signal_connect('button-release-event') { |this, event|
282 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
283 msg 3, "gesture delete: click-drag right button to the bottom"
285 closures[:delete].call
286 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
290 tooltips = Gtk::Tooltips.new
291 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
293 w.signal_connect('key-press-event') { |w,event|
294 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
296 closures[:delete].call
300 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
301 b.signal_connect('clicked') { w.destroy }
304 vb.pack_start(evt, false, false)
305 vb.pack_end(bottom, false, false)
308 w.signal_connect('delete-event') { w.destroy }
309 w.window_position = Gtk::Window::POS_CENTER
313 def scroll_upper(scrolledwindow, ypos_top)
314 newval = scrolledwindow.vadjustment.value -
315 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
316 if newval < scrolledwindow.vadjustment.lower
317 newval = scrolledwindow.vadjustment.lower
319 scrolledwindow.vadjustment.value = newval
322 def scroll_lower(scrolledwindow, ypos_bottom)
323 newval = scrolledwindow.vadjustment.value +
324 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
325 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
326 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
328 scrolledwindow.vadjustment.value = newval
331 def autoscroll_if_needed(scrolledwindow, image, textview)
332 #- autoscroll if cursor or image is not visible, if possible
333 if image && image.window || textview.window
334 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
335 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
336 current_miny_visible = scrolledwindow.vadjustment.value
337 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
338 if ypos_top < current_miny_visible
339 scroll_upper(scrolledwindow, ypos_top)
340 elsif ypos_bottom > current_maxy_visible
341 scroll_lower(scrolledwindow, ypos_bottom)
346 def create_editzone(scrolledwindow, pagenum, image)
347 frame = Gtk::Frame.new
348 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
349 frame.set_shadow_type(Gtk::SHADOW_IN)
350 textview.signal_connect('key-press-event') { |w, event|
351 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
352 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
353 scrolledwindow.signal_emit('key-press-event', event)
355 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
356 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
357 if event.keyval == Gdk::Keyval::GDK_Up
358 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
359 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
361 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
364 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
365 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
367 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
373 textview.signal_connect('focus-in-event') { |w, event|
374 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
378 candidate_undo_text = nil
379 textview.signal_connect('focus-in-event') { |w, event|
380 candidate_undo_text = textview.buffer.text
383 textview.signal_connect('key-release-event') { |w, event|
384 if candidate_undo_text && candidate_undo_text != textview.buffer.text
386 save_undo(_("text edit"),
388 save_text = textview.buffer.text
389 textview.buffer.text = text
391 $notebook.set_page(pagenum)
393 textview.buffer.text = save_text
395 $notebook.set_page(pagenum)
397 }, candidate_undo_text)
398 candidate_undo_text = nil
401 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)
402 autoscroll_if_needed(scrolledwindow, image, textview)
407 return [ frame, textview ]
410 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
412 if !$modified_pixbufs[thumbnail_img]
413 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
414 elsif !$modified_pixbufs[thumbnail_img][:orig]
415 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
418 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
421 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
422 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
423 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
424 if pixbuf.height > desired_y
425 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
426 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
427 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
432 if $modified_pixbufs[thumbnail_img][:whitebalance]
433 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
436 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
439 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
442 #- update rotate attribute
443 xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
445 $modified_pixbufs[thumbnail_img] ||= {}
446 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
447 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
449 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
452 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
455 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
457 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
459 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
460 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
462 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
463 $notebook.set_page(0)
464 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
469 def color_swap(xmldir, attributes_prefix)
471 if xmldir.attributes["#{attributes_prefix}color-swap"]
472 xmldir.delete_attribute("#{attributes_prefix}color-swap")
474 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
478 def enhance(xmldir, attributes_prefix)
480 if xmldir.attributes["#{attributes_prefix}enhance"]
481 xmldir.delete_attribute("#{attributes_prefix}enhance")
483 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
487 def change_frame_offset(xmldir, attributes_prefix, value)
489 xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
492 def ask_new_frame_offset(xmldir, attributes_prefix)
494 value = xmldir.attributes["#{attributes_prefix}frame-offset"]
499 dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
501 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
502 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
503 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
507 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
508 from. There are approximately 25 frames per second in a video.
511 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
512 entry.signal_connect('key-press-event') { |w, event|
513 if event.keyval == Gdk::Keyval::GDK_Return
514 dialog.response(Gtk::Dialog::RESPONSE_OK)
516 elsif event.keyval == Gdk::Keyval::GDK_Escape
517 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
520 false #- propagate if needed
524 dialog.window_position = Gtk::Window::POS_MOUSE
527 dialog.run { |response|
530 if response == Gtk::Dialog::RESPONSE_OK
532 msg 3, "changing frame offset to #{newval}"
533 return { :old => value, :new => newval }
540 def change_pano_amount(xmldir, attributes_prefix, value)
543 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
545 xmldir.add_attribute("#{attributes_prefix}pano-amount", value)
549 def ask_new_pano_amount(xmldir, attributes_prefix)
551 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
556 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
558 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
559 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
560 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
564 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
565 of this panorama image compared to other regular images. For example, if the panorama
566 was taken out of four photos on one row, counting the necessary overlap, the width of
567 this panorama image should probably be roughly three times the width of regular images.
569 With this information, booh will be able to generate panorama thumbnails looking
573 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
574 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
575 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
576 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
577 dialog.window_position = Gtk::Window::POS_MOUSE
580 spin.value = value.to_f
587 dialog.run { |response|
591 newval = spin.value.to_f
594 if response == Gtk::Dialog::RESPONSE_OK
596 msg 3, "changing panorama amount to #{newval}"
597 return { :old => value, :new => newval }
604 def change_whitebalance(xmlelem, attributes_prefix, value)
606 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
609 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
611 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
612 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
613 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
614 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
615 destfile = "#{thumbnail_img}-orig-whitebalance.jpg"
616 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
617 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
618 $modified_pixbufs[thumbnail_img] ||= {}
619 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
620 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
621 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
624 $modified_pixbufs[thumbnail_img] ||= {}
625 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
627 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
630 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
631 #- init $modified_pixbufs correctly
632 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
634 value = xmlelem.attributes["#{attributes_prefix}white-balance"] || "0"
636 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
638 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
639 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
640 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
644 _("You can fix the <b>white balance</b> of the image, if your image is too blue
645 or too yellow because your camera didn't detect the light correctly. Drag the
646 slider below the image to the left for more blue, to the right for more yellow.
649 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
650 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
652 dialog.window_position = Gtk::Window::POS_MOUSE
656 timeout = Gtk.timeout_add(100) {
657 if hs.value != lastval
659 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
664 dialog.run { |response|
665 Gtk.timeout_remove(timeout)
666 if response == Gtk::Dialog::RESPONSE_OK
668 newval = hs.value.to_s
669 msg 3, "changing white balance to #{newval}"
671 return { :old => value, :new => newval }
673 $modified_pixbufs[thumbnail_img] ||= {}
674 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
675 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
682 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
683 system("rm -f '#{destfile}'")
684 #- type can be 'element' or 'subdir'
686 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
688 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
692 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
694 push_mousecursor_wait
695 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
697 puts "destroyed: " + img.destroyed?.to_s
699 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
705 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
706 distribute_multiple_call = Proc.new { |action, arg|
707 $selected_elements.each_key { |path|
708 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
710 if possible_actions[:can_multiple] && $selected_elements.length > 0
711 UndoHandler.begin_batch
712 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
713 UndoHandler.end_batch
715 closures[action].call(arg)
717 $selected_elements = {}
720 if optionals.include?('change_image')
721 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
722 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
723 changeimg.signal_connect('activate') { closures[:change].call }
724 menu.append( Gtk::SeparatorMenuItem.new)
728 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
729 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
730 view.signal_connect('activate') { closures[:view].call }
732 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
733 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
734 view.signal_connect('activate') { closures[:view].call }
735 menu.append( Gtk::SeparatorMenuItem.new)
739 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
740 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
741 exif.signal_connect('activate') { show_popup($main_window,
742 utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
743 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
744 menu.append( Gtk::SeparatorMenuItem.new)
746 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
747 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
748 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
749 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
750 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
751 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
752 if !possible_actions[:can_multiple] || $selected_elements.length == 0
753 menu.append( Gtk::SeparatorMenuItem.new)
754 if !possible_actions[:forbid_left]
755 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
756 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
757 moveleft.signal_connect('activate') { closures[:move].call('left') }
758 if !possible_actions[:can_left]
759 moveleft.sensitive = false
762 if !possible_actions[:forbid_right]
763 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
764 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
765 moveright.signal_connect('activate') { closures[:move].call('right') }
766 if !possible_actions[:can_right]
767 moveright.sensitive = false
770 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
771 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
772 moveup.signal_connect('activate') { closures[:move].call('up') }
773 if !possible_actions[:can_up]
774 moveup.sensitive = false
776 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
777 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
778 movedown.signal_connect('activate') { closures[:move].call('down') }
779 if !possible_actions[:can_down]
780 movedown.sensitive = false
784 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
785 menu.append( Gtk::SeparatorMenuItem.new)
786 menu.append( color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
787 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
788 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
789 menu.append( flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
790 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
791 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
792 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
793 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
794 frame_offset.signal_connect('activate') {
795 if possible_actions[:can_multiple] && $selected_elements.length > 0
796 if values = ask_new_frame_offset(nil, '')
797 distribute_multiple_call.call(:frame_offset, values)
800 closures[:frame_offset].call
805 menu.append( Gtk::SeparatorMenuItem.new)
806 if !possible_actions[:can_multiple] || $selected_elements.length == 0
807 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
808 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
809 whitebalance.signal_connect('activate') { closures[:whitebalance].call }
811 if !possible_actions[:can_multiple] || $selected_elements.length == 0
812 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
813 _("Enhance constrast"))))
815 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
817 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
818 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
819 if type == 'image' && possible_actions[:can_panorama]
820 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
821 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
822 panorama.signal_connect('activate') { closures[:pano].call }
824 if optionals.include?('delete')
825 menu.append( Gtk::SeparatorMenuItem.new)
826 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
827 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
828 if !possible_actions[:can_multiple] || $selected_elements.length == 0
829 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
830 paste_item.signal_connect('activate') { closures[:paste].call }
831 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
832 clear_item.signal_connect('activate') { $cuts = [] }
834 paste_item.sensitive = clear_item.sensitive = false
837 menu.append( Gtk::SeparatorMenuItem.new)
838 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
839 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
842 menu.popup(nil, nil, event.button, event.time)
845 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
848 frame1 = Gtk::Frame.new
849 fullpath = from_utf8("#{$current_path}/#{filename}")
851 my_gen_real_thumbnail = proc {
852 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
855 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
856 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
857 frame1.add(img = Gtk::Image.new)
858 my_gen_real_thumbnail.call
860 frame1.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
862 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
864 tooltips = Gtk::Tooltips.new
865 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
866 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
868 frame2, textview = create_editzone($autotable_sw, 1, img)
869 textview.buffer.text = utf8(caption)
870 textview.set_justification(Gtk::Justification::CENTER)
872 vbox = Gtk::VBox.new(false, 5)
873 vbox.pack_start(evtbox, false, false)
874 vbox.pack_start(frame2, false, false)
875 autotable.append(vbox, filename)
877 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
878 $vbox2widgets[vbox] = { :textview => textview, :image => img }
880 #- to be able to find widgets by name
881 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
883 cleanup_all_thumbnails = Proc.new {
884 #- remove out of sync images
885 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
886 for sizeobj in $images_size
887 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
892 rotate_and_cleanup = Proc.new { |angle|
893 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
894 cleanup_all_thumbnails.call
897 move = Proc.new { |direction|
898 do_method = "move_#{direction}"
899 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
901 done = autotable.method(do_method).call(vbox)
902 textview.grab_focus #- because if moving, focus is stolen
906 save_undo(_("move %s") % direction,
908 autotable.method(undo_method).call(vbox)
909 textview.grab_focus #- because if moving, focus is stolen
910 autoscroll_if_needed($autotable_sw, img, textview)
911 $notebook.set_page(1)
913 autotable.method(do_method).call(vbox)
914 textview.grab_focus #- because if moving, focus is stolen
915 autoscroll_if_needed($autotable_sw, img, textview)
916 $notebook.set_page(1)
922 color_swap_and_cleanup = Proc.new {
923 perform_color_swap_and_cleanup = Proc.new {
924 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
925 my_gen_real_thumbnail.call
928 cleanup_all_thumbnails.call
929 perform_color_swap_and_cleanup.call
931 save_undo(_("color swap"),
933 perform_color_swap_and_cleanup.call
935 autoscroll_if_needed($autotable_sw, img, textview)
936 $notebook.set_page(1)
938 perform_color_swap_and_cleanup.call
940 autoscroll_if_needed($autotable_sw, img, textview)
941 $notebook.set_page(1)
946 change_frame_offset_and_cleanup_real = Proc.new { |values|
947 perform_change_frame_offset_and_cleanup = Proc.new { |val|
948 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
949 my_gen_real_thumbnail.call
951 perform_change_frame_offset_and_cleanup.call(values[:new])
953 save_undo(_("specify frame offset"),
955 perform_change_frame_offset_and_cleanup.call(values[:old])
957 autoscroll_if_needed($autotable_sw, img, textview)
958 $notebook.set_page(1)
960 perform_change_frame_offset_and_cleanup.call(values[:new])
962 autoscroll_if_needed($autotable_sw, img, textview)
963 $notebook.set_page(1)
968 change_frame_offset_and_cleanup = Proc.new {
969 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
970 change_frame_offset_and_cleanup_real.call(values)
974 change_pano_amount_and_cleanup_real = Proc.new { |values|
975 perform_change_pano_amount_and_cleanup = Proc.new { |val|
976 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
978 perform_change_pano_amount_and_cleanup.call(values[:new])
980 save_undo(_("change panorama amount"),
982 perform_change_pano_amount_and_cleanup.call(values[:old])
984 autoscroll_if_needed($autotable_sw, img, textview)
985 $notebook.set_page(1)
987 perform_change_pano_amount_and_cleanup.call(values[:new])
989 autoscroll_if_needed($autotable_sw, img, textview)
990 $notebook.set_page(1)
995 change_pano_amount_and_cleanup = Proc.new {
996 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
997 change_pano_amount_and_cleanup_real.call(values)
1001 whitebalance_and_cleanup = Proc.new {
1002 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1003 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1004 perform_change_whitebalance_and_cleanup = Proc.new { |val|
1005 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1006 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1007 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1008 cleanup_all_thumbnails.call
1010 perform_change_whitebalance_and_cleanup.call(values[:new])
1012 save_undo(_("fix white balance"),
1014 perform_change_whitebalance_and_cleanup.call(values[:old])
1016 autoscroll_if_needed($autotable_sw, img, textview)
1017 $notebook.set_page(1)
1019 perform_change_whitebalance_and_cleanup.call(values[:new])
1021 autoscroll_if_needed($autotable_sw, img, textview)
1022 $notebook.set_page(1)
1028 enhance_and_cleanup = Proc.new {
1029 perform_enhance_and_cleanup = Proc.new {
1030 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1031 my_gen_real_thumbnail.call
1034 cleanup_all_thumbnails.call
1035 perform_enhance_and_cleanup.call
1037 save_undo(_("enhance"),
1039 perform_enhance_and_cleanup.call
1041 autoscroll_if_needed($autotable_sw, img, textview)
1042 $notebook.set_page(1)
1044 perform_enhance_and_cleanup.call
1046 autoscroll_if_needed($autotable_sw, img, textview)
1047 $notebook.set_page(1)
1052 delete = Proc.new { |isacut|
1053 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed?")), { :okcancel => true })
1056 perform_delete = Proc.new {
1057 after = autotable.get_next_widget(vbox)
1059 after = autotable.get_previous_widget(vbox)
1061 if $config['deleteondisk'] && !isacut
1062 msg 3, "scheduling for delete: #{fullpath}"
1063 $todelete << fullpath
1065 autotable.remove(vbox)
1067 $vbox2widgets[after][:textview].grab_focus
1068 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1072 previous_pos = autotable.get_current_number(vbox)
1076 if $xmldir.child_byname_notattr('dir', 'deleted')
1077 $xmldir.delete_attribute('thumbnails-caption')
1078 $xmldir.delete_attribute('thumbnails-captionfile')
1080 $xmldir.add_attribute('deleted', 'true')
1082 while moveup.parent.name == 'dir'
1083 moveup = moveup.parent
1084 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1085 moveup.add_attribute('deleted', 'true')
1091 save_changes('forced')
1092 populate_subalbums_treeview
1094 save_undo(_("delete"),
1096 autotable.reinsert(pos, vbox, filename)
1097 $notebook.set_page(1)
1098 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1100 msg 3, "removing deletion schedule of: #{fullpath}"
1101 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1104 $notebook.set_page(1)
1113 $cuts << { :vbox => vbox, :filename => filename }
1114 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1119 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1122 autotable.queue_draws << proc {
1123 $vbox2widgets[last[:vbox]][:textview].grab_focus
1124 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1126 save_undo(_("paste"),
1128 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1129 $notebook.set_page(1)
1132 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1134 $notebook.set_page(1)
1137 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1142 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1143 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1144 :pano => change_pano_amount_and_cleanup }
1146 textview.signal_connect('key-press-event') { |w, event|
1149 x, y = autotable.get_current_pos(vbox)
1150 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1151 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1152 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1153 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1155 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1156 $vbox2widgets[widget_up][:textview].grab_focus
1163 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1165 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1166 $vbox2widgets[widget_down][:textview].grab_focus
1173 if event.keyval == Gdk::Keyval::GDK_Left
1176 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1183 rotate_and_cleanup.call(-90)
1186 if event.keyval == Gdk::Keyval::GDK_Right
1187 next_ = autotable.get_next_widget(vbox)
1188 if next_ && autotable.get_current_pos(next_)[0] > x
1190 $vbox2widgets[next_][:textview].grab_focus
1197 rotate_and_cleanup.call(90)
1200 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1203 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1204 view_element(filename, { :delete => delete })
1207 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1210 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1214 !propagate #- propagate if needed
1217 $ignore_next_release = false
1218 evtbox.signal_connect('button-press-event') { |w, event|
1219 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1220 if event.state & Gdk::Window::BUTTON3_MASK != 0
1221 #- gesture redo: hold right mouse button then click left mouse button
1222 $config['nogestures'] or perform_redo
1223 $ignore_next_release = true
1225 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1227 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1229 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1230 elsif $enhance.active?
1231 enhance_and_cleanup.call
1232 elsif $delete.active?
1236 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1239 $button1_pressed_autotable = true
1240 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1241 if event.state & Gdk::Window::BUTTON1_MASK != 0
1242 #- gesture undo: hold left mouse button then click right mouse button
1243 $config['nogestures'] or perform_undo
1244 $ignore_next_release = true
1246 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1247 view_element(filename, { :delete => delete })
1252 evtbox.signal_connect('button-release-event') { |w, event|
1253 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1254 if !$ignore_next_release
1255 x, y = autotable.get_current_pos(vbox)
1256 next_ = autotable.get_next_widget(vbox)
1257 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1258 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1259 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1260 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1261 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1262 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1263 :pano => change_pano_amount_and_cleanup })
1265 $ignore_next_release = false
1266 $gesture_press = nil
1271 #- handle reordering with drag and drop
1272 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1273 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1274 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1275 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1278 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1280 #- mouse gesture first (dnd disables button-release-event)
1281 if $gesture_press && $gesture_press[:filename] == filename
1282 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1283 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1284 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1285 rotate_and_cleanup.call(angle)
1286 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1288 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1289 msg 3, "gesture delete: click-drag right button to the bottom"
1291 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1296 ctxt.targets.each { |target|
1297 if target.name == 'reorder-elements'
1298 move_dnd = Proc.new { |from,to|
1301 autotable.move(from, to)
1302 save_undo(_("reorder"),
1303 Proc.new { |from, to|
1305 autotable.move(to - 1, from)
1307 autotable.move(to, from + 1)
1309 $notebook.set_page(1)
1311 autotable.move(from, to)
1312 $notebook.set_page(1)
1317 if $multiple_dnd.size == 0
1318 move_dnd.call(selection_data.data.to_i,
1319 autotable.get_current_number(vbox))
1321 UndoHandler.begin_batch
1322 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1324 #- need to update current position between each call
1325 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1326 autotable.get_current_number(vbox))
1328 UndoHandler.end_batch
1339 def create_auto_table
1341 $autotable = Gtk::AutoTable.new(5)
1343 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1344 thumbnails_vb = Gtk::VBox.new(false, 5)
1346 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1347 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1348 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1349 thumbnails_vb.add($autotable)
1351 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1352 $autotable_sw.add_with_viewport(thumbnails_vb)
1354 #- follows stuff for handling multiple elements selection
1355 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1357 update_selected = Proc.new {
1358 $autotable.current_order.each { |path|
1359 w = $name2widgets[path][:evtbox].window
1360 xm = w.position[0] + w.size[0]/2
1361 ym = w.position[1] + w.size[1]/2
1362 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1363 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1364 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1365 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1368 if $selected_elements[path] && ! $selected_elements[path][:keep]
1369 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))
1370 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1371 $selected_elements.delete(path)
1376 $autotable.signal_connect('realize') { |w,e|
1377 gc = Gdk::GC.new($autotable.window)
1378 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1379 gc.function = Gdk::GC::INVERT
1380 #- autoscroll handling for DND and multiple selections
1381 Gtk.timeout_add(100) {
1382 w, x, y, mask = $autotable.window.pointer
1383 if mask & Gdk::Window::BUTTON1_MASK != 0
1384 if y < $autotable_sw.vadjustment.value
1386 $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]])
1388 if $button1_pressed_autotable || press_x
1389 scroll_upper($autotable_sw, y)
1392 w, pos_x, pos_y = $autotable.window.pointer
1393 $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]])
1394 update_selected.call
1397 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1399 $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]])
1401 if $button1_pressed_autotable || press_x
1402 scroll_lower($autotable_sw, y)
1405 w, pos_x, pos_y = $autotable.window.pointer
1406 $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]])
1407 update_selected.call
1415 $autotable.signal_connect('button-press-event') { |w,e|
1417 if !$button1_pressed_autotable
1420 if e.state & Gdk::Window::SHIFT_MASK == 0
1421 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1422 $selected_elements = {}
1423 $statusbar.push(0, utf8(_("Nothing selected.")))
1425 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1427 set_mousecursor(Gdk::Cursor::TCROSS)
1431 $autotable.signal_connect('button-release-event') { |w,e|
1433 if $button1_pressed_autotable
1434 #- unselect all only now
1435 $multiple_dnd = $selected_elements.keys
1436 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1437 $selected_elements = {}
1438 $button1_pressed_autotable = false
1441 $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]])
1442 if $selected_elements.length > 0
1443 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1446 press_x = press_y = pos_x = pos_y = nil
1447 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1451 $autotable.signal_connect('motion-notify-event') { |w,e|
1454 $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]])
1458 $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]])
1459 update_selected.call
1465 def create_subalbums_page
1467 subalbums_hb = Gtk::HBox.new
1468 $subalbums_vb = Gtk::VBox.new(false, 5)
1469 subalbums_hb.pack_start($subalbums_vb, false, false)
1470 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1471 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1472 $subalbums_sw.add_with_viewport(subalbums_hb)
1475 def save_current_file
1479 ios = File.open($filename, "w")
1480 $xmldoc.write(ios, 0)
1485 def save_current_file_user
1486 save_tempfilename = $filename
1487 $filename = $orig_filename
1490 $generated_outofline = false
1491 $filename = save_tempfilename
1493 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1494 $todelete.each { |f|
1495 system("rm -f #{f}")
1499 def mark_document_as_dirty
1500 $xmldoc.elements.each('//dir') { |elem|
1501 elem.delete_attribute('already-generated')
1505 #- ret: true => ok false => cancel
1506 def ask_save_modifications(msg1, msg2, *options)
1508 options = options.size > 0 ? options[0] : {}
1510 if options[:disallow_cancel]
1511 dialog = Gtk::Dialog.new(msg1,
1513 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1514 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1515 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1517 dialog = Gtk::Dialog.new(msg1,
1519 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1520 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1521 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1522 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1524 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1525 dialog.vbox.add(Gtk::Label.new(msg2))
1526 dialog.window_position = Gtk::Window::POS_CENTER
1529 dialog.run { |response|
1531 if response == Gtk::Dialog::RESPONSE_YES
1532 save_current_file_user
1534 #- if we have generated an album but won't save modifications, we must remove
1535 #- already-generated markers in original file
1536 if $generated_outofline
1538 $xmldoc = REXML::Document.new File.new($orig_filename)
1539 mark_document_as_dirty
1540 ios = File.open($orig_filename, "w")
1541 $xmldoc.write(ios, 0)
1544 puts "exception: #{$!}"
1548 if response == Gtk::Dialog::RESPONSE_CANCEL
1551 $todelete = [] #- unconditionally clear the list of images/videos to delete
1557 def try_quit(*options)
1558 if ask_save_modifications(utf8(_("Save before quitting?")),
1559 utf8(_("Do you want to save your changes before quitting?")),
1565 def show_popup(parent, msg, *options)
1566 dialog = Gtk::Dialog.new
1567 if options[0] && options[0][:title]
1568 dialog.title = options[0][:title]
1570 dialog.title = utf8(_("Booh message"))
1572 lbl = Gtk::Label.new
1573 if options[0] && options[0][:nomarkup]
1578 if options[0] && options[0][:centered]
1579 lbl.set_justify(Gtk::Justification::CENTER)
1581 if options[0] && options[0][:selectable]
1582 lbl.selectable = true
1584 if options[0] && options[0][:topwidget]
1585 dialog.vbox.add(options[0][:topwidget])
1587 if options[0] && options[0][:scrolled]
1588 sw = Gtk::ScrolledWindow.new(nil, nil)
1589 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1590 sw.add_with_viewport(lbl)
1592 dialog.set_default_size(400, 500)
1594 dialog.vbox.add(lbl)
1595 dialog.set_default_size(200, 120)
1597 if options[0] && options[0][:okcancel]
1598 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1600 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1602 if options[0] && options[0][:pos_centered]
1603 dialog.window_position = Gtk::Window::POS_CENTER
1605 dialog.window_position = Gtk::Window::POS_MOUSE
1608 if options[0] && options[0][:linkurl]
1609 linkbut = Gtk::Button.new('')
1610 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1611 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1612 linkbut.relief = Gtk::RELIEF_NONE
1613 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1614 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1615 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1620 if !options[0] || !options[0][:not_transient]
1621 dialog.transient_for = parent
1622 dialog.run { |response|
1624 if options[0] && options[0][:okcancel]
1625 return response == Gtk::Dialog::RESPONSE_OK
1629 dialog.signal_connect('response') { dialog.destroy }
1633 def backend_wait_message(parent, msg, infopipe_path, mode)
1635 w.set_transient_for(parent)
1638 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1639 vb.pack_start(Gtk::Label.new(msg), false, false)
1641 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1642 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1643 if mode != 'one dir scan'
1644 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1646 if mode == 'web-album'
1647 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1648 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1650 vb.pack_start(Gtk::HSeparator.new, false, false)
1652 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1653 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1654 vb.pack_end(bottom, false, false)
1656 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1657 refresh_thread = Thread.new {
1658 directories_counter = 0
1659 while line = infopipe.gets
1660 if line =~ /^directories: (\d+), sizes: (\d+)/
1661 directories = $1.to_f + 1
1663 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1664 elements = $3.to_f + 1
1665 if mode == 'web-album'
1669 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = 0 }
1670 if mode != 'one dir scan'
1671 newtext = utf8(full_src_dir_to_rel($1, $2))
1672 newtext = '/' if newtext == ''
1673 gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.text = newtext }
1674 directories_counter += 1
1675 gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.fraction = directories_counter / directories }
1677 elsif line =~ /^processing element$/
1678 element_counter += 1
1679 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = element_counter / elements }
1680 elsif line =~ /^processing size$/
1681 element_counter += 1
1682 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = element_counter / elements }
1683 elsif line =~ /^finished processing sizes$/
1684 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = 1 }
1685 elsif line =~ /^creating index.html$/
1686 gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.text = utf8(_("finished")) }
1687 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = pb1_2.fraction = 1 }
1688 directories_counter = 0
1689 elsif line =~ /^index.html: (.+)\|(.+)/
1690 newtext = utf8(full_src_dir_to_rel($1, $2))
1691 newtext = '/' if newtext == ''
1692 gtk_thread_protect { puts "destroyed: " + pb2.destroyed?.to_s; pb2.text = newtext }
1693 directories_counter += 1
1694 gtk_thread_protect { puts "destroyed: " + pb2.destroyed?.to_s; pb2.fraction = directories_counter / directories }
1700 w.signal_connect('delete-event') { w.destroy }
1701 w.signal_connect('destroy') {
1702 Thread.kill(refresh_thread)
1703 gtk_thread_abandon #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1706 system("rm -f #{infopipe_path}")
1709 w.window_position = Gtk::Window::POS_CENTER
1715 def call_backend(cmd, waitmsg, mode, params)
1716 pipe = Tempfile.new("boohpipe")
1718 system("mkfifo #{pipe.path}")
1719 cmd += " --info-pipe #{pipe.path}"
1720 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1725 id, exitstatus = Process.waitpid2(pid)
1726 gtk_thread_protect { puts "destroyed: " + w8.destroyed?.to_s; w8.destroy }
1728 if params[:successmsg]
1729 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
1731 if params[:closure_after]
1732 gtk_thread_protect(¶ms[:closure_after])
1734 elsif exitstatus == 15
1735 #- say nothing, user aborted
1737 if params[:failuremsg]
1738 gtk_thread_protect { show_popup($main_window, params[:failuremsg]) }
1745 button.signal_connect('clicked') {
1746 Process.kill('SIGTERM', pid)
1750 def save_changes(*forced)
1751 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1755 $xmldir.delete_attribute('already-generated')
1757 propagate_children = Proc.new { |xmldir|
1758 if xmldir.attributes['subdirs-caption']
1759 xmldir.delete_attribute('already-generated')
1761 xmldir.elements.each('dir') { |element|
1762 propagate_children.call(element)
1766 if $xmldir.child_byname_notattr('dir', 'deleted')
1767 new_title = $subalbums_title.buffer.text
1768 if new_title != $xmldir.attributes['subdirs-caption']
1769 parent = $xmldir.parent
1770 if parent.name == 'dir'
1771 parent.delete_attribute('already-generated')
1773 propagate_children.call($xmldir)
1775 $xmldir.add_attribute('subdirs-caption', new_title)
1776 $xmldir.elements.each('dir') { |element|
1777 if !element.attributes['deleted']
1778 path = element.attributes['path']
1779 newtext = $subalbums_edits[path][:editzone].buffer.text
1780 if element.attributes['subdirs-caption']
1781 if element.attributes['subdirs-caption'] != newtext
1782 propagate_children.call(element)
1784 element.add_attribute('subdirs-caption', newtext)
1785 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1787 if element.attributes['thumbnails-caption'] != newtext
1788 element.delete_attribute('already-generated')
1790 element.add_attribute('thumbnails-caption', newtext)
1791 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1797 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
1798 if $xmldir.attributes['thumbnails-caption']
1799 path = $xmldir.attributes['path']
1800 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1802 elsif $xmldir.attributes['thumbnails-caption']
1803 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
1806 #- remove and reinsert elements to reflect new ordering
1809 $xmldir.elements.each { |element|
1810 if element.name == 'image' || element.name == 'video'
1811 saves[element.attributes['filename']] = element.remove
1815 $autotable.current_order.each { |path|
1816 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1817 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1820 saves.each_key { |path|
1821 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1822 chld.add_attribute('deleted', 'true')
1826 def remove_all_captions
1829 $autotable.current_order.each { |path|
1830 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
1831 $name2widgets[File.basename(path)][:textview].buffer.text = ''
1833 save_undo(_("remove all captions"),
1835 texts.each_key { |key|
1836 $name2widgets[key][:textview].buffer.text = texts[key]
1838 $notebook.set_page(1)
1840 texts.each_key { |key|
1841 $name2widgets[key][:textview].buffer.text = ''
1843 $notebook.set_page(1)
1849 $selected_elements.each_key { |path|
1850 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1856 $selected_elements = {}
1860 $undo_tb.sensitive = $undo_mb.sensitive = false
1861 $redo_tb.sensitive = $redo_mb.sensitive = false
1867 $subalbums_vb.children.each { |chld|
1868 $subalbums_vb.remove(chld)
1870 $subalbums = Gtk::Table.new(0, 0, true)
1871 current_y_sub_albums = 0
1873 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
1874 $subalbums_edits = {}
1875 subalbums_counter = 0
1876 subalbums_edits_bypos = {}
1878 add_subalbum = Proc.new { |xmldir, counter|
1879 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
1880 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
1881 if xmldir == $xmldir
1882 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
1883 caption = xmldir.attributes['thumbnails-caption']
1884 captionfile, dummy = find_subalbum_caption_info(xmldir)
1885 infotype = 'thumbnails'
1887 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
1888 captionfile, caption = find_subalbum_caption_info(xmldir)
1889 infotype = find_subalbum_info_type(xmldir)
1891 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
1892 hbox = Gtk::HBox.new
1893 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
1895 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
1898 my_gen_real_thumbnail = proc {
1899 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
1902 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
1903 f.add(img = Gtk::Image.new)
1904 my_gen_real_thumbnail.call
1906 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
1908 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
1909 $subalbums.attach(hbox,
1910 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1912 frame, textview = create_editzone($subalbums_sw, 0, img)
1913 textview.buffer.text = caption
1914 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1915 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1917 change_image = Proc.new {
1918 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
1920 Gtk::FileChooser::ACTION_OPEN,
1922 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1923 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
1924 fc.transient_for = $main_window
1925 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))
1926 f.add(preview_img = Gtk::Image.new)
1928 fc.signal_connect('update-preview') { |w|
1930 if fc.preview_filename
1931 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
1932 fc.preview_widget_active = true
1934 rescue Gdk::PixbufError
1935 fc.preview_widget_active = false
1938 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1940 old_file = captionfile
1941 old_rotate = xmldir.attributes["#{infotype}-rotate"]
1942 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
1943 old_enhance = xmldir.attributes["#{infotype}-enhance"]
1944 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
1946 new_file = fc.filename
1947 msg 3, "new captionfile is: #{fc.filename}"
1948 perform_changefile = Proc.new {
1949 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
1950 $modified_pixbufs.delete(thumbnail_file)
1951 xmldir.delete_attribute("#{infotype}-rotate")
1952 xmldir.delete_attribute("#{infotype}-color-swap")
1953 xmldir.delete_attribute("#{infotype}-enhance")
1954 xmldir.delete_attribute("#{infotype}-frame-offset")
1955 my_gen_real_thumbnail.call
1957 perform_changefile.call
1959 save_undo(_("change caption file for sub-album"),
1961 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
1962 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
1963 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
1964 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
1965 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
1966 my_gen_real_thumbnail.call
1967 $notebook.set_page(0)
1969 perform_changefile.call
1970 $notebook.set_page(0)
1977 rotate_and_cleanup = Proc.new { |angle|
1978 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
1979 system("rm -f '#{thumbnail_file}'")
1982 move = Proc.new { |direction|
1985 save_changes('forced')
1986 if direction == 'up'
1987 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
1988 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
1989 subalbums_edits_bypos[oldpos - 1][:position] += 1
1991 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
1992 $subalbums_edits[xmldir.attributes['path']][:position] += 1
1993 subalbums_edits_bypos[oldpos + 1][:position] -= 1
1997 $xmldir.elements.each('dir') { |element|
1998 if (!element.attributes['deleted'])
1999 elems << [ element.attributes['path'], element.remove ]
2002 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2003 each { |e| $xmldir.add_element(e[1]) }
2004 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2005 $xmldir.elements.each('descendant::dir') { |elem|
2006 elem.delete_attribute('already-generated')
2011 color_swap_and_cleanup = Proc.new {
2012 perform_color_swap_and_cleanup = Proc.new {
2013 color_swap(xmldir, "#{infotype}-")
2014 my_gen_real_thumbnail.call
2016 perform_color_swap_and_cleanup.call
2018 save_undo(_("color swap"),
2020 perform_color_swap_and_cleanup.call
2021 $notebook.set_page(0)
2023 perform_color_swap_and_cleanup.call
2024 $notebook.set_page(0)
2029 change_frame_offset_and_cleanup = Proc.new {
2030 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2031 perform_change_frame_offset_and_cleanup = Proc.new { |val|
2032 change_frame_offset(xmldir, "#{infotype}-", val)
2033 my_gen_real_thumbnail.call
2035 perform_change_frame_offset_and_cleanup.call(values[:new])
2037 save_undo(_("specify frame offset"),
2039 perform_change_frame_offset_and_cleanup.call(values[:old])
2040 $notebook.set_page(0)
2042 perform_change_frame_offset_and_cleanup.call(values[:new])
2043 $notebook.set_page(0)
2049 whitebalance_and_cleanup = Proc.new {
2050 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2051 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2052 perform_change_whitebalance_and_cleanup = Proc.new { |val|
2053 change_whitebalance(xmldir, "#{infotype}-", val)
2054 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2055 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2056 system("rm -f '#{thumbnail_file}'")
2058 perform_change_whitebalance_and_cleanup.call(values[:new])
2060 save_undo(_("fix white balance"),
2062 perform_change_whitebalance_and_cleanup.call(values[:old])
2063 $notebook.set_page(0)
2065 perform_change_whitebalance_and_cleanup.call(values[:new])
2066 $notebook.set_page(0)
2072 enhance_and_cleanup = Proc.new {
2073 perform_enhance_and_cleanup = Proc.new {
2074 enhance(xmldir, "#{infotype}-")
2075 my_gen_real_thumbnail.call
2078 perform_enhance_and_cleanup.call
2080 save_undo(_("enhance"),
2082 perform_enhance_and_cleanup.call
2083 $notebook.set_page(0)
2085 perform_enhance_and_cleanup.call
2086 $notebook.set_page(0)
2091 evtbox.signal_connect('button-press-event') { |w, event|
2092 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2094 rotate_and_cleanup.call(90)
2096 rotate_and_cleanup.call(-90)
2097 elsif $enhance.active?
2098 enhance_and_cleanup.call
2101 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2102 popup_thumbnail_menu(event, ['change_image'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2103 { :forbid_left => true, :forbid_right => true,
2104 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter },
2105 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2106 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
2108 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2113 evtbox.signal_connect('button-press-event') { |w, event|
2114 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2118 evtbox.signal_connect('button-release-event') { |w, event|
2119 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2120 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2121 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2122 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2123 msg 3, "gesture rotate: #{angle}"
2124 rotate_and_cleanup.call(angle)
2127 $gesture_press = nil
2130 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2131 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2132 current_y_sub_albums += 1
2135 if $xmldir.child_byname_notattr('dir', 'deleted')
2137 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2138 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2139 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2140 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2141 #- this album image/caption
2142 if $xmldir.attributes['thumbnails-caption']
2143 add_subalbum.call($xmldir, 0)
2146 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2147 $xmldir.elements.each { |element|
2148 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2149 #- element (image or video) of this album
2150 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2151 msg 3, "dest_img: #{dest_img}"
2152 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
2153 total[element.name] += 1
2155 if element.name == 'dir' && !element.attributes['deleted']
2156 #- sub-album image/caption
2157 add_subalbum.call(element, subalbums_counter += 1)
2158 total[element.name] += 1
2161 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2162 total['image'], total['video'], total['dir'] ]))
2163 $subalbums_vb.add($subalbums)
2164 $subalbums_vb.show_all
2166 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2167 $notebook.get_tab_label($autotable_sw).sensitive = false
2168 $notebook.set_page(0)
2169 $thumbnails_title.buffer.text = ''
2171 $notebook.get_tab_label($autotable_sw).sensitive = true
2172 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2175 if !$xmldir.child_byname_notattr('dir', 'deleted')
2176 $notebook.get_tab_label($subalbums_sw).sensitive = false
2177 $notebook.set_page(1)
2179 $notebook.get_tab_label($subalbums_sw).sensitive = true
2183 def pixbuf_or_nil(filename)
2185 return Gdk::Pixbuf.new(filename)
2191 def theme_choose(current)
2192 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2194 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2195 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2196 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2198 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2199 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2200 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2201 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2202 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2203 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2204 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2205 treeview.signal_connect('button-press-event') { |w, event|
2206 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2207 dialog.response(Gtk::Dialog::RESPONSE_OK)
2211 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2213 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2216 iter[0] = File.basename(dir)
2217 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2218 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2219 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2220 if File.basename(dir) == current
2221 treeview.selection.select_iter(iter)
2225 dialog.set_default_size(700, 400)
2226 dialog.vbox.show_all
2227 dialog.run { |response|
2228 iter = treeview.selection.selected
2230 if response == Gtk::Dialog::RESPONSE_OK && iter
2231 return model.get_value(iter, 0)
2237 def show_password_protections
2238 examine_dir_elem = Proc.new { |parent_iter, xmldir, already_protected|
2239 child_iter = $albums_iters[xmldir.attributes['path']]
2240 if xmldir.attributes['password-protect']
2241 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2242 already_protected = true
2243 elsif already_protected
2244 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2246 pix = pix.saturate_and_pixelate(1, true)
2252 xmldir.elements.each('dir') { |elem|
2253 if !elem.attributes['deleted']
2254 examine_dir_elem.call(child_iter, elem, already_protected)
2258 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2261 def populate_subalbums_treeview
2265 $subalbums_vb.children.each { |chld|
2266 $subalbums_vb.remove(chld)
2269 source = $xmldoc.root.attributes['source']
2270 msg 3, "source: #{source}"
2272 xmldir = $xmldoc.elements['//dir']
2273 if !xmldir || xmldir.attributes['path'] != source
2274 msg 1, _("Corrupted booh file...")
2278 append_dir_elem = Proc.new { |parent_iter, xmldir|
2279 child_iter = $albums_ts.append(parent_iter)
2280 child_iter[0] = File.basename(xmldir.attributes['path'])
2281 child_iter[1] = xmldir.attributes['path']
2282 $albums_iters[xmldir.attributes['path']] = child_iter
2283 msg 3, "puttin location: #{xmldir.attributes['path']}"
2284 xmldir.elements.each('dir') { |elem|
2285 if !elem.attributes['deleted']
2286 append_dir_elem.call(child_iter, elem)
2290 append_dir_elem.call(nil, xmldir)
2291 show_password_protections
2293 $albums_tv.expand_all
2294 $albums_tv.selection.select_iter($albums_ts.iter_first)
2297 def open_file(filename)
2301 $current_path = nil #- invalidate
2302 $modified_pixbufs = {}
2305 $subalbums_vb.children.each { |chld|
2306 $subalbums_vb.remove(chld)
2309 if !File.exists?(filename)
2310 return utf8(_("File not found."))
2314 $xmldoc = REXML::Document.new File.new(filename)
2319 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2320 if entry2type(filename).nil?
2321 return utf8(_("Not a booh file!"))
2323 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."))
2327 if !source = $xmldoc.root.attributes['source']
2328 return utf8(_("Corrupted booh file..."))
2331 if !dest = $xmldoc.root.attributes['destination']
2332 return utf8(_("Corrupted booh file..."))
2335 if !theme = $xmldoc.root.attributes['theme']
2336 return utf8(_("Corrupted booh file..."))
2339 if $xmldoc.root.attributes['version'] != $VERSION
2340 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2341 mark_document_as_dirty
2342 $xmldoc.root.add_attribute('version', $VERSION)
2345 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2346 optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2347 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2349 $filename = filename
2350 select_theme(theme, limit_sizes, optimizefor32, nperrow)
2351 $default_size['thumbnails'] =~ /(.*)x(.*)/
2352 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2353 $albums_thumbnail_size =~ /(.*)x(.*)/
2354 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2356 populate_subalbums_treeview
2358 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = true
2362 def open_file_user(filename)
2363 result = open_file(filename)
2365 $config['last-opens'] ||= []
2366 if $config['last-opens'][-1] != utf8(filename)
2367 $config['last-opens'] << utf8(filename)
2369 $orig_filename = $filename
2370 tmp = Tempfile.new("boohtemp")
2373 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2375 $tempfiles << $filename << "#{$filename}.backup"
2377 $orig_filename = nil
2383 if !ask_save_modifications(utf8(_("Save this album?")),
2384 utf8(_("Do you want to save the changes to this album?")),
2385 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2388 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2390 Gtk::FileChooser::ACTION_OPEN,
2392 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2393 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2394 fc.set_current_folder(File.expand_path("~/.booh"))
2395 fc.transient_for = $main_window
2398 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2399 push_mousecursor_wait(fc)
2400 msg = open_file_user(fc.filename)
2416 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2421 def additional_booh_options
2424 options += "--mproc #{$config['mproc'].to_i} "
2426 if $config['emptycomments']
2427 options += "--empty-comments "
2433 if !ask_save_modifications(utf8(_("Save this album?")),
2434 utf8(_("Do you want to save the changes to this album?")),
2435 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2438 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2440 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2441 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2442 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2444 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2445 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2446 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2447 tbl.attach(src = Gtk::Entry.new,
2448 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2449 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2450 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2451 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2452 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2453 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2454 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2455 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2456 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2457 tbl.attach(dest = Gtk::Entry.new,
2458 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2459 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2460 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2461 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2462 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2463 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2464 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2465 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2466 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2468 tooltips = Gtk::Tooltips.new
2469 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2470 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2471 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2472 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2473 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2474 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2475 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)
2476 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2477 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2478 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2479 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2480 tooltips.set_tip(madewithentry, utf8(_("Optional HTML markup to use on pages bottom for a small 'made with' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!")), nil)
2482 src_nb_calculated_for = ''
2484 process_src_nb = Proc.new {
2485 if src.text != src_nb_calculated_for
2486 src_nb_calculated_for = src.text
2488 Thread.kill(src_nb_thread)
2491 if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2492 if File.readable?(from_utf8(src_nb_calculated_for))
2493 src_nb_thread = Thread.new {
2494 gtk_thread_protect { puts "destroyed: " + src_nb.destroyed?.to_s; src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2495 total = { 'image' => 0, 'video' => 0, nil => 0 }
2496 `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2497 if File.basename(dir) =~ /^\./
2501 Dir.entries(dir.chomp).each { |file|
2502 total[entry2type(file)] += 1
2504 rescue Errno::EACCES, Errno::ENOENT
2508 gtk_thread_protect { puts "destroyed: " + src_nb.destroyed?.to_s; src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2512 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2515 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2520 timeout_src_nb = Gtk.timeout_add(100) {
2524 src_browse.signal_connect('clicked') {
2525 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2527 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2529 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2530 fc.transient_for = $main_window
2531 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2532 src.text = utf8(fc.filename)
2534 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2539 dest_browse.signal_connect('clicked') {
2540 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2542 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2544 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2545 fc.transient_for = $main_window
2546 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2547 dest.text = utf8(fc.filename)
2552 conf_browse.signal_connect('clicked') {
2553 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2555 Gtk::FileChooser::ACTION_SAVE,
2557 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2558 fc.transient_for = $main_window
2559 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2560 fc.set_current_folder(File.expand_path("~/.booh"))
2561 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2562 conf.text = utf8(fc.filename)
2569 recreate_theme_config = proc {
2570 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2572 select_theme(theme_button.label, 'all', optimize432.active?, nil)
2573 $images_size.each { |s|
2574 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2578 tooltips.set_tip(cb, utf8(s['description']), nil)
2579 theme_sizes << { :widget => cb, :value => s['name'] }
2581 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2582 tooltips = Gtk::Tooltips.new
2583 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2584 theme_sizes << { :widget => cb, :value => 'original' }
2587 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2590 $allowed_N_values.each { |n|
2592 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2594 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2596 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2600 nperrows << { :widget => rb, :value => n }
2602 nperrowradios.show_all
2604 recreate_theme_config.call
2606 theme_button.signal_connect('clicked') {
2607 if newtheme = theme_choose(theme_button.label)
2608 theme_button.label = newtheme
2609 recreate_theme_config.call
2613 dialog.vbox.add(frame1)
2614 dialog.vbox.add(frame2)
2615 dialog.window_position = Gtk::Window::POS_MOUSE
2621 dialog.run { |response|
2622 if response == Gtk::Dialog::RESPONSE_OK
2623 srcdir = from_utf8(src.text)
2624 destdir = from_utf8(dest.text)
2625 if !File.directory?(srcdir)
2626 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2628 elsif conf.text == ''
2629 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2631 elsif File.directory?(from_utf8(conf.text))
2632 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2634 elsif destdir != make_dest_filename(destdir)
2635 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2637 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2638 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2640 elsif File.exists?(destdir) && !File.directory?(destdir)
2641 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2643 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2644 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2646 system("mkdir '#{destdir}'")
2647 if !File.directory?(destdir)
2648 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2659 srcdir = from_utf8(src.text)
2660 destdir = from_utf8(dest.text)
2661 configskel = File.expand_path(from_utf8(conf.text))
2662 theme = theme_button.label
2663 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2664 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2665 opt432 = optimize432.active?
2666 madewith = madewithentry.text
2668 Thread.kill(src_nb_thread)
2669 gtk_thread_abandon #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2672 Gtk.timeout_remove(timeout_src_nb)
2675 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2676 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2677 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' #{additional_booh_options}",
2678 utf8(_("Please wait while scanning source directory...")),
2680 { :closure_after => proc { open_file_user(configskel) } })
2685 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2687 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2688 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2689 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2691 source = $xmldoc.root.attributes['source']
2692 dest = $xmldoc.root.attributes['destination']
2693 theme = $xmldoc.root.attributes['theme']
2694 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2695 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2696 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2698 limit_sizes = limit_sizes.split(/,/)
2700 madewith = $xmldoc.root.attributes['made-with']
2702 tooltips = Gtk::Tooltips.new
2703 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2704 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2705 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2706 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2707 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2708 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2709 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2710 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
2711 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2712 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2713 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2714 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
2715 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2717 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2718 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2719 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2720 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2721 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2722 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2723 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)
2724 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2725 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2726 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2727 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2729 madewithentry.text = madewith
2731 tooltips.set_tip(madewithentry, utf8(_("Optional HTML markup to use on pages bottom for a small 'made with' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!")), nil)
2735 recreate_theme_config = proc {
2736 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2738 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2740 $images_size.each { |s|
2741 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2743 if limit_sizes.include?(s['name'])
2751 tooltips.set_tip(cb, utf8(s['description']), nil)
2752 theme_sizes << { :widget => cb, :value => s['name'] }
2754 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2755 tooltips = Gtk::Tooltips.new
2756 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2757 if limit_sizes && limit_sizes.include?('original')
2760 theme_sizes << { :widget => cb, :value => 'original' }
2763 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2766 $allowed_N_values.each { |n|
2768 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2770 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2772 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2773 nperrowradios.add(Gtk::Label.new(' '))
2774 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
2777 nperrows << { :widget => rb, :value => n.to_s }
2779 nperrowradios.show_all
2781 recreate_theme_config.call
2783 theme_button.signal_connect('clicked') {
2784 if newtheme = theme_choose(theme_button.label)
2787 theme_button.label = newtheme
2788 recreate_theme_config.call
2792 dialog.vbox.add(frame1)
2793 dialog.vbox.add(frame2)
2794 dialog.window_position = Gtk::Window::POS_MOUSE
2800 dialog.run { |response|
2801 if response == Gtk::Dialog::RESPONSE_OK
2802 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2803 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2812 save_theme = theme_button.label
2813 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
2814 save_opt432 = optimize432.active?
2815 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2816 save_madewith = madewithentry.text
2819 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith)
2820 mark_document_as_dirty
2822 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
2823 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
2824 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' #{additional_booh_options}",
2825 utf8(_("Please wait while scanning source directory...")),
2827 { :closure_after => proc {
2828 open_file($filename)
2837 sel = $albums_tv.selection.selected_rows
2839 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2840 "--verbose-level #{$verbose_level} #{additional_booh_options}",
2841 utf8(_("Please wait while scanning source directory...")),
2843 { :closure_after => proc {
2844 open_file($filename)
2845 $albums_tv.selection.select_path(sel[0])
2853 sel = $albums_tv.selection.selected_rows
2855 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2856 "--verbose-level #{$verbose_level} #{additional_booh_options}",
2857 utf8(_("Please wait while scanning source directory...")),
2859 { :closure_after => proc {
2860 open_file($filename)
2861 $albums_tv.selection.select_path(sel[0])
2869 theme = $xmldoc.root.attributes['theme']
2870 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2872 limit_sizes = "--sizes #{limit_sizes}"
2874 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
2875 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
2876 utf8(_("Please wait while scanning source directory...")),
2878 { :closure_after => proc {
2879 open_file($filename)
2885 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
2887 Gtk::FileChooser::ACTION_SAVE,
2889 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2890 fc.transient_for = $main_window
2891 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2892 fc.set_current_folder(File.expand_path("~/.booh"))
2893 fc.filename = $orig_filename
2894 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2895 $orig_filename = fc.filename
2896 save_current_file_user
2902 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
2904 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2905 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2906 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2908 dialog.vbox.add(notebook = Gtk::Notebook.new)
2909 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
2910 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
2911 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2912 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
2913 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2914 tooltips = Gtk::Tooltips.new
2915 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
2916 for example: /usr/bin/mplayer %f")), nil)
2917 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
2918 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2919 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
2920 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2921 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
2922 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
2923 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
2924 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2925 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)),
2926 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2927 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)
2928 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
2929 0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2930 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)
2931 tbl.attach(emptycomments_check = Gtk::CheckButton.new(utf8(_("Use empty comments for new albums"))),
2932 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
2933 tooltips.set_tip(emptycomments_check, utf8(_("Normally, filenames are used as comments for new albums. Check this if you prefer empty comments.")), nil)
2934 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
2935 0, 2, 5, 6, Gtk::FILL, Gtk::SHRINK, 2, 2)
2936 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)
2937 smp_check.signal_connect('toggled') {
2938 if smp_check.active?
2939 smp_hbox.sensitive = true
2941 smp_hbox.sensitive = false
2945 smp_check.active = true
2946 smp_spin.value = $config['mproc'].to_i
2948 nogestures_check.active = $config['nogestures']
2949 emptycomments_check.active = $config['emptycomments']
2950 deleteondisk_check.active = $config['deleteondisk']
2952 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
2953 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
2954 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2955 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
2956 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2958 dialog.vbox.show_all
2959 dialog.run { |response|
2960 if response == Gtk::Dialog::RESPONSE_OK
2961 $config['video-viewer'] = video_viewer_entry.text
2962 $config['browser'] = browser_entry.text
2963 if smp_check.active?
2964 $config['mproc'] = smp_spin.value.to_i
2966 $config.delete('mproc')
2968 $config['nogestures'] = nogestures_check.active?
2969 $config['emptycomments'] = emptycomments_check.active?
2970 $config['deleteondisk'] = deleteondisk_check.active?
2972 $config['convert-enhance'] = enhance_entry.text
2979 if $undo_tb.sensitive?
2980 $redo_tb.sensitive = $redo_mb.sensitive = true
2981 if not more_undoes = UndoHandler.undo($statusbar)
2982 $undo_tb.sensitive = $undo_mb.sensitive = false
2988 if $redo_tb.sensitive?
2989 $undo_tb.sensitive = $undo_mb.sensitive = true
2990 if not more_redoes = UndoHandler.redo($statusbar)
2991 $redo_tb.sensitive = $redo_mb.sensitive = false
2996 def show_one_click_explanation(intro)
2997 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
2999 %s When such a tool is activated
3000 (<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
3001 on a thumbnail will immediately apply the desired action.
3003 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3009 GNU GENERAL PUBLIC LICENSE
3010 Version 2, June 1991
3012 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
3013 675 Mass Ave, Cambridge, MA 02139, USA
3014 Everyone is permitted to copy and distribute verbatim copies
3015 of this license document, but changing it is not allowed.
3019 The licenses for most software are designed to take away your
3020 freedom to share and change it. By contrast, the GNU General Public
3021 License is intended to guarantee your freedom to share and change free
3022 software--to make sure the software is free for all its users. This
3023 General Public License applies to most of the Free Software
3024 Foundation's software and to any other program whose authors commit to
3025 using it. (Some other Free Software Foundation software is covered by
3026 the GNU Library General Public License instead.) You can apply it to
3029 When we speak of free software, we are referring to freedom, not
3030 price. Our General Public Licenses are designed to make sure that you
3031 have the freedom to distribute copies of free software (and charge for
3032 this service if you wish), that you receive source code or can get it
3033 if you want it, that you can change the software or use pieces of it
3034 in new free programs; and that you know you can do these things.
3036 To protect your rights, we need to make restrictions that forbid
3037 anyone to deny you these rights or to ask you to surrender the rights.
3038 These restrictions translate to certain responsibilities for you if you
3039 distribute copies of the software, or if you modify it.
3041 For example, if you distribute copies of such a program, whether
3042 gratis or for a fee, you must give the recipients all the rights that
3043 you have. You must make sure that they, too, receive or can get the
3044 source code. And you must show them these terms so they know their
3047 We protect your rights with two steps: (1) copyright the software, and
3048 (2) offer you this license which gives you legal permission to copy,
3049 distribute and/or modify the software.
3051 Also, for each author's protection and ours, we want to make certain
3052 that everyone understands that there is no warranty for this free
3053 software. If the software is modified by someone else and passed on, we
3054 want its recipients to know that what they have is not the original, so
3055 that any problems introduced by others will not reflect on the original
3056 authors' reputations.
3058 Finally, any free program is threatened constantly by software
3059 patents. We wish to avoid the danger that redistributors of a free
3060 program will individually obtain patent licenses, in effect making the
3061 program proprietary. To prevent this, we have made it clear that any
3062 patent must be licensed for everyone's free use or not licensed at all.
3064 The precise terms and conditions for copying, distribution and
3065 modification follow.
3068 GNU GENERAL PUBLIC LICENSE
3069 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
3071 0. This License applies to any program or other work which contains
3072 a notice placed by the copyright holder saying it may be distributed
3073 under the terms of this General Public License. The "Program", below,
3074 refers to any such program or work, and a "work based on the Program"
3075 means either the Program or any derivative work under copyright law:
3076 that is to say, a work containing the Program or a portion of it,
3077 either verbatim or with modifications and/or translated into another
3078 language. (Hereinafter, translation is included without limitation in
3079 the term "modification".) Each licensee is addressed as "you".
3081 Activities other than copying, distribution and modification are not
3082 covered by this License; they are outside its scope. The act of
3083 running the Program is not restricted, and the output from the Program
3084 is covered only if its contents constitute a work based on the
3085 Program (independent of having been made by running the Program).
3086 Whether that is true depends on what the Program does.
3088 1. You may copy and distribute verbatim copies of the Program's
3089 source code as you receive it, in any medium, provided that you
3090 conspicuously and appropriately publish on each copy an appropriate
3091 copyright notice and disclaimer of warranty; keep intact all the
3092 notices that refer to this License and to the absence of any warranty;
3093 and give any other recipients of the Program a copy of this License
3094 along with the Program.
3096 You may charge a fee for the physical act of transferring a copy, and
3097 you may at your option offer warranty protection in exchange for a fee.
3099 2. You may modify your copy or copies of the Program or any portion
3100 of it, thus forming a work based on the Program, and copy and
3101 distribute such modifications or work under the terms of Section 1
3102 above, provided that you also meet all of these conditions:
3104 a) You must cause the modified files to carry prominent notices
3105 stating that you changed the files and the date of any change.
3107 b) You must cause any work that you distribute or publish, that in
3108 whole or in part contains or is derived from the Program or any
3109 part thereof, to be licensed as a whole at no charge to all third
3110 parties under the terms of this License.
3112 c) If the modified program normally reads commands interactively
3113 when run, you must cause it, when started running for such
3114 interactive use in the most ordinary way, to print or display an
3115 announcement including an appropriate copyright notice and a
3116 notice that there is no warranty (or else, saying that you provide
3117 a warranty) and that users may redistribute the program under
3118 these conditions, and telling the user how to view a copy of this
3119 License. (Exception: if the Program itself is interactive but
3120 does not normally print such an announcement, your work based on
3121 the Program is not required to print an announcement.)
3124 These requirements apply to the modified work as a whole. If
3125 identifiable sections of that work are not derived from the Program,
3126 and can be reasonably considered independent and separate works in
3127 themselves, then this License, and its terms, do not apply to those
3128 sections when you distribute them as separate works. But when you
3129 distribute the same sections as part of a whole which is a work based
3130 on the Program, the distribution of the whole must be on the terms of
3131 this License, whose permissions for other licensees extend to the
3132 entire whole, and thus to each and every part regardless of who wrote it.
3134 Thus, it is not the intent of this section to claim rights or contest
3135 your rights to work written entirely by you; rather, the intent is to
3136 exercise the right to control the distribution of derivative or
3137 collective works based on the Program.
3139 In addition, mere aggregation of another work not based on the Program
3140 with the Program (or with a work based on the Program) on a volume of
3141 a storage or distribution medium does not bring the other work under
3142 the scope of this License.
3144 3. You may copy and distribute the Program (or a work based on it,
3145 under Section 2) in object code or executable form under the terms of
3146 Sections 1 and 2 above provided that you also do one of the following:
3148 a) Accompany it with the complete corresponding machine-readable
3149 source code, which must be distributed under the terms of Sections
3150 1 and 2 above on a medium customarily used for software interchange; or,
3152 b) Accompany it with a written offer, valid for at least three
3153 years, to give any third party, for a charge no more than your
3154 cost of physically performing source distribution, a complete
3155 machine-readable copy of the corresponding source code, to be
3156 distributed under the terms of Sections 1 and 2 above on a medium
3157 customarily used for software interchange; or,
3159 c) Accompany it with the information you received as to the offer
3160 to distribute corresponding source code. (This alternative is
3161 allowed only for noncommercial distribution and only if you
3162 received the program in object code or executable form with such
3163 an offer, in accord with Subsection b above.)
3165 The source code for a work means the preferred form of the work for
3166 making modifications to it. For an executable work, complete source
3167 code means all the source code for all modules it contains, plus any
3168 associated interface definition files, plus the scripts used to
3169 control compilation and installation of the executable. However, as a
3170 special exception, the source code distributed need not include
3171 anything that is normally distributed (in either source or binary
3172 form) with the major components (compiler, kernel, and so on) of the
3173 operating system on which the executable runs, unless that component
3174 itself accompanies the executable.
3176 If distribution of executable or object code is made by offering
3177 access to copy from a designated place, then offering equivalent
3178 access to copy the source code from the same place counts as
3179 distribution of the source code, even though third parties are not
3180 compelled to copy the source along with the object code.
3183 4. You may not copy, modify, sublicense, or distribute the Program
3184 except as expressly provided under this License. Any attempt
3185 otherwise to copy, modify, sublicense or distribute the Program is
3186 void, and will automatically terminate your rights under this License.
3187 However, parties who have received copies, or rights, from you under
3188 this License will not have their licenses terminated so long as such
3189 parties remain in full compliance.
3191 5. You are not required to accept this License, since you have not
3192 signed it. However, nothing else grants you permission to modify or
3193 distribute the Program or its derivative works. These actions are
3194 prohibited by law if you do not accept this License. Therefore, by
3195 modifying or distributing the Program (or any work based on the
3196 Program), you indicate your acceptance of this License to do so, and
3197 all its terms and conditions for copying, distributing or modifying
3198 the Program or works based on it.
3200 6. Each time you redistribute the Program (or any work based on the
3201 Program), the recipient automatically receives a license from the
3202 original licensor to copy, distribute or modify the Program subject to
3203 these terms and conditions. You may not impose any further
3204 restrictions on the recipients' exercise of the rights granted herein.
3205 You are not responsible for enforcing compliance by third parties to
3208 7. If, as a consequence of a court judgment or allegation of patent
3209 infringement or for any other reason (not limited to patent issues),
3210 conditions are imposed on you (whether by court order, agreement or
3211 otherwise) that contradict the conditions of this License, they do not
3212 excuse you from the conditions of this License. If you cannot
3213 distribute so as to satisfy simultaneously your obligations under this
3214 License and any other pertinent obligations, then as a consequence you
3215 may not distribute the Program at all. For example, if a patent
3216 license would not permit royalty-free redistribution of the Program by
3217 all those who receive copies directly or indirectly through you, then
3218 the only way you could satisfy both it and this License would be to
3219 refrain entirely from distribution of the Program.
3221 If any portion of this section is held invalid or unenforceable under
3222 any particular circumstance, the balance of the section is intended to
3223 apply and the section as a whole is intended to apply in other
3226 It is not the purpose of this section to induce you to infringe any
3227 patents or other property right claims or to contest validity of any
3228 such claims; this section has the sole purpose of protecting the
3229 integrity of the free software distribution system, which is
3230 implemented by public license practices. Many people have made
3231 generous contributions to the wide range of software distributed
3232 through that system in reliance on consistent application of that
3233 system; it is up to the author/donor to decide if he or she is willing
3234 to distribute software through any other system and a licensee cannot
3237 This section is intended to make thoroughly clear what is believed to
3238 be a consequence of the rest of this License.
3241 8. If the distribution and/or use of the Program is restricted in
3242 certain countries either by patents or by copyrighted interfaces, the
3243 original copyright holder who places the Program under this License
3244 may add an explicit geographical distribution limitation excluding
3245 those countries, so that distribution is permitted only in or among
3246 countries not thus excluded. In such case, this License incorporates
3247 the limitation as if written in the body of this License.
3249 9. The Free Software Foundation may publish revised and/or new versions
3250 of the General Public License from time to time. Such new versions will
3251 be similar in spirit to the present version, but may differ in detail to
3252 address new problems or concerns.
3254 Each version is given a distinguishing version number. If the Program
3255 specifies a version number of this License which applies to it and "any
3256 later version", you have the option of following the terms and conditions
3257 either of that version or of any later version published by the Free
3258 Software Foundation. If the Program does not specify a version number of
3259 this License, you may choose any version ever published by the Free Software
3262 10. If you wish to incorporate parts of the Program into other free
3263 programs whose distribution conditions are different, write to the author
3264 to ask for permission. For software which is copyrighted by the Free
3265 Software Foundation, write to the Free Software Foundation; we sometimes
3266 make exceptions for this. Our decision will be guided by the two goals
3267 of preserving the free status of all derivatives of our free software and
3268 of promoting the sharing and reuse of software generally.
3272 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
3273 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
3274 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
3275 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
3276 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
3277 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
3278 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
3279 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
3280 REPAIR OR CORRECTION.
3282 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
3283 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
3284 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
3285 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
3286 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
3287 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
3288 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
3289 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
3290 POSSIBILITY OF SUCH DAMAGES.
3294 def create_menu_and_toolbar
3297 mb = Gtk::MenuBar.new
3299 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3300 filesubmenu = Gtk::Menu.new
3301 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3302 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3303 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3304 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3305 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3306 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3307 tooltips = Gtk::Tooltips.new
3308 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3309 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3310 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3311 filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3312 $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3313 tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3314 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3315 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3316 tooltips.set_tip($merge, utf8(_("Take into account new subalbums (subdirectories) and new/removed images/videos in existing subalbums (anywhere)")), nil)
3317 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3318 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3319 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3320 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
3321 filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
3322 $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
3323 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3324 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
3325 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
3326 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3327 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
3328 filemenu.set_submenu(filesubmenu)
3331 new.signal_connect('activate') { new_album }
3332 open.signal_connect('activate') { open_file_popup }
3333 $save.signal_connect('activate') { save_current_file_user }
3334 $save_as.signal_connect('activate') { save_as_do }
3335 $merge_current.signal_connect('activate') { merge_current }
3336 $merge_newsubs.signal_connect('activate') { merge_newsubs }
3337 $merge.signal_connect('activate') { merge }
3338 $generate.signal_connect('activate') {
3340 call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
3341 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
3343 { :successmsg => utf8(_("Your web-album is now ready in directory `%s'.
3344 Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
3345 :successmsg_linkurl => $xmldoc.root.attributes['destination'],
3346 :failuremsg => utf8(_("There was something wrong when generating the web-album, sorry.")),
3347 :closure_after => proc {
3348 $xmldoc.elements.each('//dir') { |elem|
3349 elem.add_attribute('already-generated', 'true')
3351 UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
3352 $undo_tb.sensitive = $undo_mb.sensitive = false
3353 $redo_tb.sensitive = $redo_mb.sensitive = false
3355 $generated_outofline = true
3358 $view_wa.signal_connect('activate') {
3359 indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
3360 if File.exists?(indexhtml)
3363 show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
3366 $properties.signal_connect('activate') { properties }
3368 quit.signal_connect('activate') { try_quit }
3370 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
3371 editsubmenu = Gtk::Menu.new
3372 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
3373 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
3374 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3375 editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
3376 $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
3377 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)
3378 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3379 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
3380 editmenu.set_submenu(editsubmenu)
3383 $remove_all_captions.signal_connect('activate') { remove_all_captions }
3385 prefs.signal_connect('activate') { preferences }
3387 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
3388 helpsubmenu = Gtk::Menu.new
3389 helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
3390 one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
3391 helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
3392 speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
3393 helpsubmenu.append( Gtk::SeparatorMenuItem.new)
3394 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
3395 helpmenu.set_submenu(helpsubmenu)
3398 one_click.signal_connect('activate') {
3399 show_one_click_explanation(_("One-Click tools are available in the toolbar."))
3402 speed.signal_connect('activate') {
3403 show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
3405 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
3406 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
3407 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
3408 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
3409 <span foreground='darkblue'>Control-Delete</span>: delete image
3410 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
3411 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
3412 <span foreground='darkblue'>Control-z</span>: undo
3413 <span foreground='darkblue'>Control-r</span>: redo
3415 <span size='large' weight='bold'>Mouse gestures:</span>
3417 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
3418 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
3420 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
3421 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
3422 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
3423 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
3424 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
3425 ")), { :pos_centered => true, :not_transient => true })
3429 about.signal_connect('activate') {
3430 Gtk::AboutDialog.set_url_hook { |dialog, url| open_url(url) }
3431 Gtk::AboutDialog.show($main_window, { :name => 'booh',
3432 :version => $VERSION,
3433 :copyright => 'Copyright (c) 2005 Guillaume Cottenceau',
3434 :license => get_license,
3435 :website => 'http://zarb.org/~gc/html/booh.html',
3436 :authors => [ 'Guillaume Cottenceau' ],
3437 :artists => [ 'Ayo73' ],
3438 :comments => utf8(_("''The Web-Album of choice for discriminating Linux users''")),
3439 :translator_credits => utf8(_('Japanese: Masao Mutoh
3440 German: Roland Eckert
3441 French: Guillaume Cottenceau')),
3442 :logo => Gdk::Pixbuf.new("#{$FPATH}/images/logo.png") })
3447 tb = Gtk::Toolbar.new
3449 tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
3450 open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
3451 open.menu = Gtk::Menu.new
3452 open.signal_connect('clicked') { open_file_popup }
3453 open.signal_connect('show-menu') {
3454 lastopens = Gtk::Menu.new
3456 if $config['last-opens']
3457 $config['last-opens'].reverse.each { |e|
3458 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
3459 item.signal_connect('activate') {
3460 if ask_save_modifications(utf8(_("Save this album?")),
3461 utf8(_("Do you want to save the changes to this album?")),
3462 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3463 push_mousecursor_wait
3464 msg = open_file_user(from_utf8(e))
3467 show_popup($main_window, msg)
3475 open.menu = lastopens
3478 tb.insert(-1, Gtk::SeparatorToolItem.new)
3480 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
3481 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
3482 $r90.label = utf8(_("Rotate"))
3483 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
3484 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
3485 $r270.label = utf8(_("Rotate"))
3486 tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
3487 $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
3488 $enhance.label = utf8(_("Enhance"))
3489 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
3490 $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
3491 tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
3492 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
3493 nothing.label = utf8(_("None"))
3495 tb.insert(-1, Gtk::SeparatorToolItem.new)
3497 tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
3498 tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
3501 $undo_tb.signal_connect('clicked') { perform_undo }
3502 $undo_mb.signal_connect('activate') { perform_undo }
3503 $redo_tb.signal_connect('clicked') { perform_redo }
3504 $redo_mb.signal_connect('activate') { perform_redo }
3506 one_click_explain_try = Proc.new {
3507 if !$config['one-click-explained']
3508 show_one_click_explanation(_("You have just clicked on a One-Click tool."))
3509 $config['one-click-explained'] = true
3513 $r90.signal_connect('toggled') {
3515 set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
3516 one_click_explain_try.call
3517 $r270.active = false
3518 $enhance.active = false
3519 $delete.active = false
3520 nothing.sensitive = true
3522 if !$r270.active? && !$enhance.active? && !$delete.active?
3523 set_mousecursor_normal
3524 nothing.sensitive = false
3526 nothing.sensitive = true
3530 $r270.signal_connect('toggled') {
3532 set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
3533 one_click_explain_try.call
3535 $enhance.active = false
3536 $delete.active = false
3537 nothing.sensitive = true
3539 if !$r90.active? && !$enhance.active? && !$delete.active?
3540 set_mousecursor_normal
3541 nothing.sensitive = false
3543 nothing.sensitive = true
3547 $enhance.signal_connect('toggled') {
3549 set_mousecursor(Gdk::Cursor::SPRAYCAN)
3550 one_click_explain_try.call
3552 $r270.active = false
3553 $delete.active = false
3554 nothing.sensitive = true
3556 if !$r90.active? && !$r270.active? && !$delete.active?
3557 set_mousecursor_normal
3558 nothing.sensitive = false
3560 nothing.sensitive = true
3564 $delete.signal_connect('toggled') {
3566 set_mousecursor(Gdk::Cursor::PIRATE)
3567 one_click_explain_try.call
3569 $r270.active = false
3570 $enhance.active = false
3571 nothing.sensitive = true
3573 if !$r90.active? && !$r270.active? && !$enhance.active?
3574 set_mousecursor_normal
3575 nothing.sensitive = false
3577 nothing.sensitive = true
3581 nothing.signal_connect('clicked') {
3582 $r90.active = $r270.active = $enhance.active = $delete.active = false
3583 set_mousecursor_normal
3589 def gtk_thread_protect(&proc)
3590 if Thread.current == Thread.main
3593 $protect_gtk_pending_calls.synchronize {
3594 $gtk_pending_calls << proc
3599 def gtk_thread_abandon
3600 $protect_gtk_pending_calls.try_lock
3601 $gtk_pending_calls = []
3602 $protect_gtk_pending_calls.unlock
3605 def ask_password_protect
3606 value = $xmldir.attributes['password-protect']
3608 dialog = Gtk::Dialog.new(utf8(_("Password protect this sub-album")),
3610 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3611 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3612 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3614 lbl = Gtk::Label.new
3616 _("You can choose to <b>password protect</b> the sub-album '%s' (only available
3617 if you plan to publish your web-album with an Apache web-server). This will use
3618 the .htaccess/.htpasswd feature of Apache (not so strongly crypted password, but
3619 generally ok for protecting web contents). Users will be prompted with a dialog
3620 asking for a username and a password, failure to give the correct pair will
3622 ") % File.basename($current_path))
3623 dialog.vbox.add(lbl)
3624 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("free access")))).
3625 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("password protect with password file:")))).
3626 add(file = Gtk::Entry.new)))
3627 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0.5, 0.2).add(Gtk::HBox.new.add(bt_help = Gtk::Button.new(utf8(_("help about password file")))).
3628 add(Gtk::Label.new).
3629 add(bt_gen = Gtk::Button.new(utf8(_("generate a password file"))))))
3630 dialog.window_position = Gtk::Window::POS_MOUSE
3635 rb_yes.active = true
3639 bt_help.signal_connect('clicked') {
3640 show_popup(dialog, utf8(
3641 _("Password protection proposed here uses the .htaccess/.htpasswd features
3642 proposed by Apache. So first, be sure you will publish your web-album on an
3643 Apache web-server. Second, you will need to have a .htpasswd file accessible
3644 by Apache somewhere on the web-server disks. The password file you must
3645 provide in the dialog when choosing to password protect is the full absolute
3646 path to access this file <b>on the web-server</b> (not on your machine). Note
3647 that if you use a relative path, it will be considered relative to the
3648 Document Root of the Apache configuration.")))
3651 bt_gen.signal_connect('clicked') {
3652 gendialog = Gtk::Dialog.new(utf8(_("Generate a password file")),
3654 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3655 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3656 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3658 lbl = Gtk::Label.new
3660 _("I can generate a password file (.htpasswd for Apache) for you. Just type
3661 the username and password you wish to put in it below and validate."))
3662 gendialog.vbox.add(lbl)
3663 gendialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(Gtk::Label.new(utf8(_('Username:')))).
3664 add(user = Gtk::Entry.new).
3665 add(Gtk::Label.new(utf8(_('Password:')))).
3666 add(pass = Gtk::Entry.new)))
3667 pass.visibility = false
3668 gendialog.window_position = Gtk::Window::POS_MOUSE
3670 gendialog.run { |response|
3674 if response == Gtk::Dialog::RESPONSE_OK
3676 ary = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + [ '.', '/' ]
3677 return ary[rand(ary.length)]
3679 fout = Tempfile.new("htpasswd")
3680 fout.write("#{u}:#{p.crypt(rand_letter + rand_letter)}\n")
3682 File.chmod(0644, fout.path)
3683 show_popup(dialog, utf8(
3684 _("The file <b>%s</b> now contains the username and the crypted password. Now
3685 copy it to a suitable location on the machine hosting the Apache web-server (better not
3686 below the Document Root), and specify this location in the password protect dialog.") % fout.path), { :selectable => true })
3691 dialog.run { |response|
3698 if response == Gtk::Dialog::RESPONSE_OK && value != newval
3700 msg 3, "changing password protection of #{$current_path} to #{newval}"
3702 $xmldir.delete_attribute('password-protect')
3704 $xmldir.add_attribute('password-protect', newval)
3706 save_undo(_("set password protection for %s") % File.basename($current_path),
3709 $xmldir.delete_attribute('password-protect')
3711 $xmldir.add_attribute('password-protect', value)
3715 $xmldir.delete_attribute('password-protect')
3717 $xmldir.add_attribute('password-protect', newval)
3721 show_password_protections
3726 def create_main_window
3728 mb, tb = create_menu_and_toolbar
3730 $albums_tv = Gtk::TreeView.new
3731 $albums_tv.set_size_request(120, -1)
3732 $albums_tv.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }))
3733 $albums_tv.append_column(tcol = Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, { :text => 0 }))
3734 $albums_tv.expander_column = tcol
3735 $albums_tv.set_headers_visible(false)
3736 $albums_tv.selection.signal_connect('changed') { |w|
3737 push_mousecursor_wait
3741 msg 3, "no selection"
3743 $current_path = $albums_ts.get_value(iter, 1)
3748 $albums_tv.signal_connect('button-release-event') { |w, event|
3749 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3 && !$current_path.nil?
3750 menu = Gtk::Menu.new
3751 menu.append(passprotect = Gtk::ImageMenuItem.new(utf8(_("Password protect"))))
3752 passprotect.image = Gtk::Image.new("#{$FPATH}/images/galeon-secure.png")
3753 passprotect.signal_connect('activate') { ask_password_protect }
3755 menu.popup(nil, nil, event.button, event.time)
3759 $albums_ts = Gtk::TreeStore.new(String, String, Gdk::Pixbuf)
3760 $albums_tv.set_model($albums_ts)
3761 $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
3763 albums_sw = Gtk::ScrolledWindow.new(nil, nil)
3764 albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
3765 albums_sw.add_with_viewport($albums_tv)
3767 $notebook = Gtk::Notebook.new
3768 create_subalbums_page
3769 $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
3771 $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
3773 $notebook.signal_connect('switch-page') { |w, page, num|
3775 $delete.active = false
3776 $delete.sensitive = false
3778 $delete.sensitive = true
3780 if $xmldir && $subalbums_edits[$xmldir.attributes['path']] && textview = $subalbums_edits[$xmldir.attributes['path']][:editzone]
3782 textview.buffer.text = $thumbnails_title.buffer.text
3784 if $notebook.get_tab_label($autotable_sw).sensitive?
3785 $thumbnails_title.buffer.text = textview.buffer.text
3791 paned = Gtk::HPaned.new
3792 paned.pack1(albums_sw, false, false)
3793 paned.pack2($notebook, true, true)
3795 main_vbox = Gtk::VBox.new(false, 0)
3796 main_vbox.pack_start(mb, false, false)
3797 main_vbox.pack_start(tb, false, false)
3798 main_vbox.pack_start(paned, true, true)
3799 main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
3801 $main_window = Gtk::Window.new
3802 $main_window.add(main_vbox)
3803 $main_window.signal_connect('delete-event') {
3804 try_quit({ :disallow_cancel => true })
3807 #- read/save size and position of window
3808 if $config['pos-x'] && $config['pos-y']
3809 $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
3811 $main_window.window_position = Gtk::Window::POS_CENTER
3813 msg 3, "size: #{$config['width']}x#{$config['height']}"
3814 $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
3815 $main_window.signal_connect('configure-event') {
3816 msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
3817 x, y = $main_window.window.root_origin
3818 width, height = $main_window.window.size
3819 $config['pos-x'] = x
3820 $config['pos-y'] = y
3821 $config['width'] = width
3822 $config['height'] = height
3826 $protect_gtk_pending_calls = Mutex.new
3827 $gtk_pending_calls = []
3828 Gtk.timeout_add(100) {
3829 $protect_gtk_pending_calls.synchronize {
3830 $gtk_pending_calls.each { |c| c.call }
3831 $gtk_pending_calls = []
3836 $statusbar.push(0, utf8(_("Ready.")))
3837 $main_window.show_all
3840 Thread.abort_on_exception = true
3850 open_file_user(ARGV[0])