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, 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 }
731 menu.append( Gtk::SeparatorMenuItem.new)
733 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
734 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
735 view.signal_connect('activate') { closures[:view].call }
736 menu.append( Gtk::SeparatorMenuItem.new)
739 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
740 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
741 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
742 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
743 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
744 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
745 if !possible_actions[:can_multiple] || $selected_elements.length == 0
746 menu.append( Gtk::SeparatorMenuItem.new)
747 if !possible_actions[:forbid_left]
748 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
749 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
750 moveleft.signal_connect('activate') { closures[:move].call('left') }
751 if !possible_actions[:can_left]
752 moveleft.sensitive = false
755 if !possible_actions[:forbid_right]
756 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
757 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
758 moveright.signal_connect('activate') { closures[:move].call('right') }
759 if !possible_actions[:can_right]
760 moveright.sensitive = false
763 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
764 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
765 moveup.signal_connect('activate') { closures[:move].call('up') }
766 if !possible_actions[:can_up]
767 moveup.sensitive = false
769 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
770 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
771 movedown.signal_connect('activate') { closures[:move].call('down') }
772 if !possible_actions[:can_down]
773 movedown.sensitive = false
777 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
778 menu.append( Gtk::SeparatorMenuItem.new)
779 menu.append( color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
780 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
781 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
782 menu.append( flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
783 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
784 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
785 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
786 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
787 frame_offset.signal_connect('activate') {
788 if possible_actions[:can_multiple] && $selected_elements.length > 0
789 if values = ask_new_frame_offset(nil, '')
790 distribute_multiple_call.call(:frame_offset, values)
793 closures[:frame_offset].call
798 menu.append( Gtk::SeparatorMenuItem.new)
799 if !possible_actions[:can_multiple] || $selected_elements.length == 0
800 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
801 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
802 whitebalance.signal_connect('activate') { closures[:whitebalance].call }
804 if !possible_actions[:can_multiple] || $selected_elements.length == 0
805 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
806 _("Enhance constrast"))))
808 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
810 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
811 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
812 if type == 'image' && possible_actions[:can_panorama]
813 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
814 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
815 panorama.signal_connect('activate') { closures[:pano].call }
817 if optionals.include?('delete')
818 menu.append( Gtk::SeparatorMenuItem.new)
819 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
820 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
821 if !possible_actions[:can_multiple] || $selected_elements.length == 0
822 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
823 paste_item.signal_connect('activate') { closures[:paste].call }
824 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
825 clear_item.signal_connect('activate') { $cuts = [] }
827 paste_item.sensitive = clear_item.sensitive = false
830 menu.append( Gtk::SeparatorMenuItem.new)
831 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
832 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
835 menu.popup(nil, nil, event.button, event.time)
838 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
841 frame1 = Gtk::Frame.new
842 fullpath = from_utf8("#{$current_path}/#{filename}")
844 my_gen_real_thumbnail = proc {
845 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
848 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
849 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
850 frame1.add(img = Gtk::Image.new)
851 my_gen_real_thumbnail.call
853 frame1.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
855 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
857 tooltips = Gtk::Tooltips.new
858 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
859 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
861 frame2, textview = create_editzone($autotable_sw, 1, img)
862 textview.buffer.text = utf8(caption)
863 textview.set_justification(Gtk::Justification::CENTER)
865 vbox = Gtk::VBox.new(false, 5)
866 vbox.pack_start(evtbox, false, false)
867 vbox.pack_start(frame2, false, false)
868 autotable.append(vbox, filename)
870 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
871 $vbox2widgets[vbox] = { :textview => textview, :image => img }
873 #- to be able to find widgets by name
874 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
876 cleanup_all_thumbnails = Proc.new {
877 #- remove out of sync images
878 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
879 for sizeobj in $images_size
880 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
885 rotate_and_cleanup = Proc.new { |angle|
886 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
887 cleanup_all_thumbnails.call
890 move = Proc.new { |direction|
891 do_method = "move_#{direction}"
892 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
894 done = autotable.method(do_method).call(vbox)
895 textview.grab_focus #- because if moving, focus is stolen
899 save_undo(_("move %s") % direction,
901 autotable.method(undo_method).call(vbox)
902 textview.grab_focus #- because if moving, focus is stolen
903 autoscroll_if_needed($autotable_sw, img, textview)
904 $notebook.set_page(1)
906 autotable.method(do_method).call(vbox)
907 textview.grab_focus #- because if moving, focus is stolen
908 autoscroll_if_needed($autotable_sw, img, textview)
909 $notebook.set_page(1)
915 color_swap_and_cleanup = Proc.new {
916 perform_color_swap_and_cleanup = Proc.new {
917 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
918 my_gen_real_thumbnail.call
921 cleanup_all_thumbnails.call
922 perform_color_swap_and_cleanup.call
924 save_undo(_("color swap"),
926 perform_color_swap_and_cleanup.call
928 autoscroll_if_needed($autotable_sw, img, textview)
929 $notebook.set_page(1)
931 perform_color_swap_and_cleanup.call
933 autoscroll_if_needed($autotable_sw, img, textview)
934 $notebook.set_page(1)
939 change_frame_offset_and_cleanup_real = Proc.new { |values|
940 perform_change_frame_offset_and_cleanup = Proc.new { |val|
941 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
942 my_gen_real_thumbnail.call
944 perform_change_frame_offset_and_cleanup.call(values[:new])
946 save_undo(_("specify frame offset"),
948 perform_change_frame_offset_and_cleanup.call(values[:old])
950 autoscroll_if_needed($autotable_sw, img, textview)
951 $notebook.set_page(1)
953 perform_change_frame_offset_and_cleanup.call(values[:new])
955 autoscroll_if_needed($autotable_sw, img, textview)
956 $notebook.set_page(1)
961 change_frame_offset_and_cleanup = Proc.new {
962 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
963 change_frame_offset_and_cleanup_real.call(values)
967 change_pano_amount_and_cleanup_real = Proc.new { |values|
968 perform_change_pano_amount_and_cleanup = Proc.new { |val|
969 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
971 perform_change_pano_amount_and_cleanup.call(values[:new])
973 save_undo(_("change panorama amount"),
975 perform_change_pano_amount_and_cleanup.call(values[:old])
977 autoscroll_if_needed($autotable_sw, img, textview)
978 $notebook.set_page(1)
980 perform_change_pano_amount_and_cleanup.call(values[:new])
982 autoscroll_if_needed($autotable_sw, img, textview)
983 $notebook.set_page(1)
988 change_pano_amount_and_cleanup = Proc.new {
989 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
990 change_pano_amount_and_cleanup_real.call(values)
994 whitebalance_and_cleanup = Proc.new {
995 if values = ask_whitebalance(fullpath, thumbnail_img, img,
996 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
997 perform_change_whitebalance_and_cleanup = Proc.new { |val|
998 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
999 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1000 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1001 cleanup_all_thumbnails.call
1003 perform_change_whitebalance_and_cleanup.call(values[:new])
1005 save_undo(_("fix white balance"),
1007 perform_change_whitebalance_and_cleanup.call(values[:old])
1009 autoscroll_if_needed($autotable_sw, img, textview)
1010 $notebook.set_page(1)
1012 perform_change_whitebalance_and_cleanup.call(values[:new])
1014 autoscroll_if_needed($autotable_sw, img, textview)
1015 $notebook.set_page(1)
1021 enhance_and_cleanup = Proc.new {
1022 perform_enhance_and_cleanup = Proc.new {
1023 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1024 my_gen_real_thumbnail.call
1027 cleanup_all_thumbnails.call
1028 perform_enhance_and_cleanup.call
1030 save_undo(_("enhance"),
1032 perform_enhance_and_cleanup.call
1034 autoscroll_if_needed($autotable_sw, img, textview)
1035 $notebook.set_page(1)
1037 perform_enhance_and_cleanup.call
1039 autoscroll_if_needed($autotable_sw, img, textview)
1040 $notebook.set_page(1)
1045 delete = Proc.new { |isacut|
1046 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed?")), { :okcancel => true })
1049 perform_delete = Proc.new {
1050 after = autotable.get_next_widget(vbox)
1052 after = autotable.get_previous_widget(vbox)
1054 if $config['deleteondisk'] && !isacut
1055 msg 3, "scheduling for delete: #{fullpath}"
1056 $todelete << fullpath
1058 autotable.remove(vbox)
1060 $vbox2widgets[after][:textview].grab_focus
1061 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1065 previous_pos = autotable.get_current_number(vbox)
1069 if $xmldir.child_byname_notattr('dir', 'deleted')
1070 $xmldir.delete_attribute('thumbnails-caption')
1071 $xmldir.delete_attribute('thumbnails-captionfile')
1073 $xmldir.add_attribute('deleted', 'true')
1075 while moveup.parent.name == 'dir'
1076 moveup = moveup.parent
1077 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1078 moveup.add_attribute('deleted', 'true')
1084 save_changes('forced')
1085 populate_subalbums_treeview
1087 save_undo(_("delete"),
1089 autotable.reinsert(pos, vbox, filename)
1090 $notebook.set_page(1)
1091 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1093 msg 3, "removing deletion schedule of: #{fullpath}"
1094 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1097 $notebook.set_page(1)
1106 $cuts << { :vbox => vbox, :filename => filename }
1107 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1112 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1115 autotable.queue_draws << proc {
1116 $vbox2widgets[last[:vbox]][:textview].grab_focus
1117 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1119 save_undo(_("paste"),
1121 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1122 $notebook.set_page(1)
1125 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1127 $notebook.set_page(1)
1130 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1135 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1136 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1137 :pano => change_pano_amount_and_cleanup }
1139 textview.signal_connect('key-press-event') { |w, event|
1142 x, y = autotable.get_current_pos(vbox)
1143 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1144 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1145 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1146 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1148 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1149 $vbox2widgets[widget_up][:textview].grab_focus
1156 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1158 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1159 $vbox2widgets[widget_down][:textview].grab_focus
1166 if event.keyval == Gdk::Keyval::GDK_Left
1169 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1176 rotate_and_cleanup.call(-90)
1179 if event.keyval == Gdk::Keyval::GDK_Right
1180 next_ = autotable.get_next_widget(vbox)
1181 if next_ && autotable.get_current_pos(next_)[0] > x
1183 $vbox2widgets[next_][:textview].grab_focus
1190 rotate_and_cleanup.call(90)
1193 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1196 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1197 view_element(filename, { :delete => delete })
1200 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1203 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1207 !propagate #- propagate if needed
1210 $ignore_next_release = false
1211 evtbox.signal_connect('button-press-event') { |w, event|
1212 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1213 if event.state & Gdk::Window::BUTTON3_MASK != 0
1214 #- gesture redo: hold right mouse button then click left mouse button
1215 $config['nogestures'] or perform_redo
1216 $ignore_next_release = true
1218 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1220 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1222 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1223 elsif $enhance.active?
1224 enhance_and_cleanup.call
1225 elsif $delete.active?
1229 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1232 $button1_pressed_autotable = true
1233 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1234 if event.state & Gdk::Window::BUTTON1_MASK != 0
1235 #- gesture undo: hold left mouse button then click right mouse button
1236 $config['nogestures'] or perform_undo
1237 $ignore_next_release = true
1239 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1240 view_element(filename, { :delete => delete })
1245 evtbox.signal_connect('button-release-event') { |w, event|
1246 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1247 if !$ignore_next_release
1248 x, y = autotable.get_current_pos(vbox)
1249 next_ = autotable.get_next_widget(vbox)
1250 popup_thumbnail_menu(event, ['delete'], type, $xmldir.elements["*[@filename='#{filename}']"], '',
1251 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1252 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1253 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1254 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1255 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1256 :pano => change_pano_amount_and_cleanup })
1258 $ignore_next_release = false
1259 $gesture_press = nil
1264 #- handle reordering with drag and drop
1265 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1266 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1267 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1268 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1271 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1273 #- mouse gesture first (dnd disables button-release-event)
1274 if $gesture_press && $gesture_press[:filename] == filename
1275 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1276 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1277 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1278 rotate_and_cleanup.call(angle)
1279 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1281 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1282 msg 3, "gesture delete: click-drag right button to the bottom"
1284 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1289 ctxt.targets.each { |target|
1290 if target.name == 'reorder-elements'
1291 move_dnd = Proc.new { |from,to|
1294 autotable.move(from, to)
1295 save_undo(_("reorder"),
1296 Proc.new { |from, to|
1298 autotable.move(to - 1, from)
1300 autotable.move(to, from + 1)
1302 $notebook.set_page(1)
1304 autotable.move(from, to)
1305 $notebook.set_page(1)
1310 if $multiple_dnd.size == 0
1311 move_dnd.call(selection_data.data.to_i,
1312 autotable.get_current_number(vbox))
1314 UndoHandler.begin_batch
1315 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1317 #- need to update current position between each call
1318 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1319 autotable.get_current_number(vbox))
1321 UndoHandler.end_batch
1332 def create_auto_table
1334 $autotable = Gtk::AutoTable.new(5)
1336 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1337 thumbnails_vb = Gtk::VBox.new(false, 5)
1339 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1340 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1341 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1342 thumbnails_vb.add($autotable)
1344 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1345 $autotable_sw.add_with_viewport(thumbnails_vb)
1347 #- follows stuff for handling multiple elements selection
1348 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1350 update_selected = Proc.new {
1351 $autotable.current_order.each { |path|
1352 w = $name2widgets[path][:evtbox].window
1353 xm = w.position[0] + w.size[0]/2
1354 ym = w.position[1] + w.size[1]/2
1355 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1356 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1357 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1358 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1361 if $selected_elements[path] && ! $selected_elements[path][:keep]
1362 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))
1363 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1364 $selected_elements.delete(path)
1369 $autotable.signal_connect('realize') { |w,e|
1370 gc = Gdk::GC.new($autotable.window)
1371 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1372 gc.function = Gdk::GC::INVERT
1373 #- autoscroll handling for DND and multiple selections
1374 Gtk.timeout_add(100) {
1375 w, x, y, mask = $autotable.window.pointer
1376 if mask & Gdk::Window::BUTTON1_MASK != 0
1377 if y < $autotable_sw.vadjustment.value
1379 $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]])
1381 if $button1_pressed_autotable || press_x
1382 scroll_upper($autotable_sw, y)
1385 w, pos_x, pos_y = $autotable.window.pointer
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]])
1387 update_selected.call
1390 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1392 $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 if $button1_pressed_autotable || press_x
1395 scroll_lower($autotable_sw, y)
1398 w, pos_x, pos_y = $autotable.window.pointer
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]])
1400 update_selected.call
1408 $autotable.signal_connect('button-press-event') { |w,e|
1410 if !$button1_pressed_autotable
1413 if e.state & Gdk::Window::SHIFT_MASK == 0
1414 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1415 $selected_elements = {}
1416 $statusbar.push(0, utf8(_("Nothing selected.")))
1418 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1420 set_mousecursor(Gdk::Cursor::TCROSS)
1424 $autotable.signal_connect('button-release-event') { |w,e|
1426 if $button1_pressed_autotable
1427 #- unselect all only now
1428 $multiple_dnd = $selected_elements.keys
1429 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1430 $selected_elements = {}
1431 $button1_pressed_autotable = false
1434 $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]])
1435 if $selected_elements.length > 0
1436 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1439 press_x = press_y = pos_x = pos_y = nil
1440 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1444 $autotable.signal_connect('motion-notify-event') { |w,e|
1447 $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]])
1451 $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]])
1452 update_selected.call
1458 def create_subalbums_page
1460 subalbums_hb = Gtk::HBox.new
1461 $subalbums_vb = Gtk::VBox.new(false, 5)
1462 subalbums_hb.pack_start($subalbums_vb, false, false)
1463 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1464 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1465 $subalbums_sw.add_with_viewport(subalbums_hb)
1468 def save_current_file
1472 ios = File.open($filename, "w")
1473 $xmldoc.write(ios, 0)
1478 def save_current_file_user
1479 save_tempfilename = $filename
1480 $filename = $orig_filename
1483 $generated_outofline = false
1484 $filename = save_tempfilename
1486 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1487 $todelete.each { |f|
1488 system("rm -f #{f}")
1492 def mark_document_as_dirty
1493 $xmldoc.elements.each('//dir') { |elem|
1494 elem.delete_attribute('already-generated')
1498 #- ret: true => ok false => cancel
1499 def ask_save_modifications(msg1, msg2, *options)
1501 options = options.size > 0 ? options[0] : {}
1503 if options[:disallow_cancel]
1504 dialog = Gtk::Dialog.new(msg1,
1506 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1507 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1508 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1510 dialog = Gtk::Dialog.new(msg1,
1512 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1513 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1514 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1515 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1517 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1518 dialog.vbox.add(Gtk::Label.new(msg2))
1519 dialog.window_position = Gtk::Window::POS_CENTER
1522 dialog.run { |response|
1524 if response == Gtk::Dialog::RESPONSE_YES
1525 save_current_file_user
1527 #- if we have generated an album but won't save modifications, we must remove
1528 #- already-generated markers in original file
1529 if $generated_outofline
1531 $xmldoc = REXML::Document.new File.new($orig_filename)
1532 mark_document_as_dirty
1533 ios = File.open($orig_filename, "w")
1534 $xmldoc.write(ios, 0)
1537 puts "exception: #{$!}"
1541 if response == Gtk::Dialog::RESPONSE_CANCEL
1544 $todelete = [] #- unconditionally clear the list of images/videos to delete
1550 def try_quit(*options)
1551 if ask_save_modifications(utf8(_("Save before quitting?")),
1552 utf8(_("Do you want to save your changes before quitting?")),
1558 def show_popup(parent, msg, *options)
1559 dialog = Gtk::Dialog.new
1560 dialog.title = utf8(_("Booh message"))
1561 lbl = Gtk::Label.new
1563 if options[0] && options[0][:centered]
1564 lbl.set_justify(Gtk::Justification::CENTER)
1566 if options[0] && options[0][:selectable]
1567 lbl.selectable = true
1569 if options[0] && options[0][:topwidget]
1570 dialog.vbox.add(options[0][:topwidget])
1572 dialog.vbox.add(lbl)
1573 if options[0] && options[0][:okcancel]
1574 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1576 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1578 dialog.set_default_size(200, 120)
1579 if options[0] && options[0][:pos_centered]
1580 dialog.window_position = Gtk::Window::POS_CENTER
1582 dialog.window_position = Gtk::Window::POS_MOUSE
1585 if options[0] && options[0][:linkurl]
1586 linkbut = Gtk::Button.new('')
1587 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1588 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1589 linkbut.relief = Gtk::RELIEF_NONE
1590 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1591 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1592 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1597 if !options[0] || !options[0][:not_transient]
1598 dialog.transient_for = parent
1599 dialog.run { |response|
1601 if options[0] && options[0][:okcancel]
1602 return response == Gtk::Dialog::RESPONSE_OK
1606 dialog.signal_connect('response') { dialog.destroy }
1610 def backend_wait_message(parent, msg, infopipe_path, mode)
1612 w.set_transient_for(parent)
1615 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1616 vb.pack_start(Gtk::Label.new(msg), false, false)
1618 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1619 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1620 if mode != 'one dir scan'
1621 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1623 if mode == 'web-album'
1624 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1625 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1627 vb.pack_start(Gtk::HSeparator.new, false, false)
1629 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1630 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1631 vb.pack_end(bottom, false, false)
1633 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1634 refresh_thread = Thread.new {
1635 directories_counter = 0
1636 while line = infopipe.gets
1637 if line =~ /^directories: (\d+), sizes: (\d+)/
1638 directories = $1.to_f + 1
1640 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1641 elements = $3.to_f + 1
1642 if mode == 'web-album'
1646 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = 0 }
1647 if mode != 'one dir scan'
1648 newtext = utf8(full_src_dir_to_rel($1, $2))
1649 newtext = '/' if newtext == ''
1650 gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.text = newtext }
1651 directories_counter += 1
1652 gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.fraction = directories_counter / directories }
1654 elsif line =~ /^processing element$/
1655 element_counter += 1
1656 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = element_counter / elements }
1657 elsif line =~ /^processing size$/
1658 element_counter += 1
1659 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = element_counter / elements }
1660 elsif line =~ /^finished processing sizes$/
1661 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = 1 }
1662 elsif line =~ /^creating index.html$/
1663 gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.text = utf8(_("finished")) }
1664 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = pb1_2.fraction = 1 }
1665 directories_counter = 0
1666 elsif line =~ /^index.html: (.+)\|(.+)/
1667 newtext = utf8(full_src_dir_to_rel($1, $2))
1668 newtext = '/' if newtext == ''
1669 gtk_thread_protect { puts "destroyed: " + pb2.destroyed?.to_s; pb2.text = newtext }
1670 directories_counter += 1
1671 gtk_thread_protect { puts "destroyed: " + pb2.destroyed?.to_s; pb2.fraction = directories_counter / directories }
1677 w.signal_connect('delete-event') { w.destroy }
1678 w.signal_connect('destroy') {
1679 Thread.kill(refresh_thread)
1680 gtk_thread_abandon #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1683 system("rm -f #{infopipe_path}")
1686 w.window_position = Gtk::Window::POS_CENTER
1692 def call_backend(cmd, waitmsg, mode, params)
1693 pipe = Tempfile.new("boohpipe")
1695 system("mkfifo #{pipe.path}")
1696 cmd += " --info-pipe #{pipe.path}"
1697 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1702 id, exitstatus = Process.waitpid2(pid)
1703 gtk_thread_protect { puts "destroyed: " + w8.destroyed?.to_s; w8.destroy }
1705 if params[:successmsg]
1706 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
1708 if params[:closure_after]
1709 gtk_thread_protect(¶ms[:closure_after])
1711 elsif exitstatus == 15
1712 #- say nothing, user aborted
1714 if params[:failuremsg]
1715 gtk_thread_protect { show_popup($main_window, params[:failuremsg]) }
1722 button.signal_connect('clicked') {
1723 Process.kill('SIGTERM', pid)
1727 def save_changes(*forced)
1728 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1732 $xmldir.delete_attribute('already-generated')
1734 propagate_children = Proc.new { |xmldir|
1735 if xmldir.attributes['subdirs-caption']
1736 xmldir.delete_attribute('already-generated')
1738 xmldir.elements.each('dir') { |element|
1739 propagate_children.call(element)
1743 if $xmldir.child_byname_notattr('dir', 'deleted')
1744 new_title = $subalbums_title.buffer.text
1745 if new_title != $xmldir.attributes['subdirs-caption']
1746 parent = $xmldir.parent
1747 if parent.name == 'dir'
1748 parent.delete_attribute('already-generated')
1750 propagate_children.call($xmldir)
1752 $xmldir.add_attribute('subdirs-caption', new_title)
1753 $xmldir.elements.each('dir') { |element|
1754 if !element.attributes['deleted']
1755 path = element.attributes['path']
1756 newtext = $subalbums_edits[path][:editzone].buffer.text
1757 if element.attributes['subdirs-caption']
1758 if element.attributes['subdirs-caption'] != newtext
1759 propagate_children.call(element)
1761 element.add_attribute('subdirs-caption', newtext)
1762 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1764 if element.attributes['thumbnails-caption'] != newtext
1765 element.delete_attribute('already-generated')
1767 element.add_attribute('thumbnails-caption', newtext)
1768 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1774 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
1775 if $xmldir.attributes['thumbnails-caption']
1776 path = $xmldir.attributes['path']
1777 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1779 elsif $xmldir.attributes['thumbnails-caption']
1780 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
1783 #- remove and reinsert elements to reflect new ordering
1786 $xmldir.elements.each { |element|
1787 if element.name == 'image' || element.name == 'video'
1788 saves[element.attributes['filename']] = element.remove
1792 $autotable.current_order.each { |path|
1793 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1794 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1797 saves.each_key { |path|
1798 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1799 chld.add_attribute('deleted', 'true')
1803 def remove_all_captions
1806 $autotable.current_order.each { |path|
1807 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
1808 $name2widgets[File.basename(path)][:textview].buffer.text = ''
1810 save_undo(_("remove all captions"),
1812 texts.each_key { |key|
1813 $name2widgets[key][:textview].buffer.text = texts[key]
1815 $notebook.set_page(1)
1817 texts.each_key { |key|
1818 $name2widgets[key][:textview].buffer.text = ''
1820 $notebook.set_page(1)
1826 $selected_elements.each_key { |path|
1827 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1833 $selected_elements = {}
1837 $undo_tb.sensitive = $undo_mb.sensitive = false
1838 $redo_tb.sensitive = $redo_mb.sensitive = false
1844 $subalbums_vb.children.each { |chld|
1845 $subalbums_vb.remove(chld)
1847 $subalbums = Gtk::Table.new(0, 0, true)
1848 current_y_sub_albums = 0
1850 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
1851 $subalbums_edits = {}
1852 subalbums_counter = 0
1853 subalbums_edits_bypos = {}
1855 add_subalbum = Proc.new { |xmldir, counter|
1856 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
1857 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
1858 if xmldir == $xmldir
1859 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
1860 caption = xmldir.attributes['thumbnails-caption']
1861 captionfile, dummy = find_subalbum_caption_info(xmldir)
1862 infotype = 'thumbnails'
1864 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
1865 captionfile, caption = find_subalbum_caption_info(xmldir)
1866 infotype = find_subalbum_info_type(xmldir)
1868 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
1869 hbox = Gtk::HBox.new
1870 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
1872 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
1875 my_gen_real_thumbnail = proc {
1876 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
1879 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
1880 f.add(img = Gtk::Image.new)
1881 my_gen_real_thumbnail.call
1883 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
1885 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
1886 $subalbums.attach(hbox,
1887 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1889 frame, textview = create_editzone($subalbums_sw, 0, img)
1890 textview.buffer.text = caption
1891 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1892 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1894 change_image = Proc.new {
1895 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
1897 Gtk::FileChooser::ACTION_OPEN,
1899 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1900 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
1901 fc.transient_for = $main_window
1902 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))
1903 f.add(preview_img = Gtk::Image.new)
1905 fc.signal_connect('update-preview') { |w|
1907 if fc.preview_filename
1908 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
1909 fc.preview_widget_active = true
1911 rescue Gdk::PixbufError
1912 fc.preview_widget_active = false
1915 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1917 old_file = captionfile
1918 old_rotate = xmldir.attributes["#{infotype}-rotate"]
1919 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
1920 old_enhance = xmldir.attributes["#{infotype}-enhance"]
1921 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
1923 new_file = fc.filename
1924 msg 3, "new captionfile is: #{fc.filename}"
1925 perform_changefile = Proc.new {
1926 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
1927 $modified_pixbufs.delete(thumbnail_file)
1928 xmldir.delete_attribute("#{infotype}-rotate")
1929 xmldir.delete_attribute("#{infotype}-color-swap")
1930 xmldir.delete_attribute("#{infotype}-enhance")
1931 xmldir.delete_attribute("#{infotype}-frame-offset")
1932 my_gen_real_thumbnail.call
1934 perform_changefile.call
1936 save_undo(_("change caption file for sub-album"),
1938 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
1939 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
1940 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
1941 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
1942 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
1943 my_gen_real_thumbnail.call
1944 $notebook.set_page(0)
1946 perform_changefile.call
1947 $notebook.set_page(0)
1954 rotate_and_cleanup = Proc.new { |angle|
1955 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
1956 system("rm -f '#{thumbnail_file}'")
1959 move = Proc.new { |direction|
1962 save_changes('forced')
1963 if direction == 'up'
1964 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
1965 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
1966 subalbums_edits_bypos[oldpos - 1][:position] += 1
1968 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
1969 $subalbums_edits[xmldir.attributes['path']][:position] += 1
1970 subalbums_edits_bypos[oldpos + 1][:position] -= 1
1974 $xmldir.elements.each('dir') { |element|
1975 if (!element.attributes['deleted'])
1976 elems << [ element.attributes['path'], element.remove ]
1979 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
1980 each { |e| $xmldir.add_element(e[1]) }
1981 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
1982 $xmldir.elements.each('descendant::dir') { |elem|
1983 elem.delete_attribute('already-generated')
1988 color_swap_and_cleanup = Proc.new {
1989 perform_color_swap_and_cleanup = Proc.new {
1990 color_swap(xmldir, "#{infotype}-")
1991 my_gen_real_thumbnail.call
1993 perform_color_swap_and_cleanup.call
1995 save_undo(_("color swap"),
1997 perform_color_swap_and_cleanup.call
1998 $notebook.set_page(0)
2000 perform_color_swap_and_cleanup.call
2001 $notebook.set_page(0)
2006 change_frame_offset_and_cleanup = Proc.new {
2007 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2008 perform_change_frame_offset_and_cleanup = Proc.new { |val|
2009 change_frame_offset(xmldir, "#{infotype}-", val)
2010 my_gen_real_thumbnail.call
2012 perform_change_frame_offset_and_cleanup.call(values[:new])
2014 save_undo(_("specify frame offset"),
2016 perform_change_frame_offset_and_cleanup.call(values[:old])
2017 $notebook.set_page(0)
2019 perform_change_frame_offset_and_cleanup.call(values[:new])
2020 $notebook.set_page(0)
2026 whitebalance_and_cleanup = Proc.new {
2027 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2028 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2029 perform_change_whitebalance_and_cleanup = Proc.new { |val|
2030 change_whitebalance(xmldir, "#{infotype}-", val)
2031 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2032 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2033 system("rm -f '#{thumbnail_file}'")
2035 perform_change_whitebalance_and_cleanup.call(values[:new])
2037 save_undo(_("fix white balance"),
2039 perform_change_whitebalance_and_cleanup.call(values[:old])
2040 $notebook.set_page(0)
2042 perform_change_whitebalance_and_cleanup.call(values[:new])
2043 $notebook.set_page(0)
2049 enhance_and_cleanup = Proc.new {
2050 perform_enhance_and_cleanup = Proc.new {
2051 enhance(xmldir, "#{infotype}-")
2052 my_gen_real_thumbnail.call
2055 perform_enhance_and_cleanup.call
2057 save_undo(_("enhance"),
2059 perform_enhance_and_cleanup.call
2060 $notebook.set_page(0)
2062 perform_enhance_and_cleanup.call
2063 $notebook.set_page(0)
2068 evtbox.signal_connect('button-press-event') { |w, event|
2069 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2071 rotate_and_cleanup.call(90)
2073 rotate_and_cleanup.call(-90)
2074 elsif $enhance.active?
2075 enhance_and_cleanup.call
2078 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2079 popup_thumbnail_menu(event, ['change_image'], entry2type(captionfile), xmldir, "#{infotype}-",
2080 { :forbid_left => true, :forbid_right => true,
2081 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter },
2082 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2083 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
2085 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2090 evtbox.signal_connect('button-press-event') { |w, event|
2091 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2095 evtbox.signal_connect('button-release-event') { |w, event|
2096 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2097 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2098 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2099 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2100 msg 3, "gesture rotate: #{angle}"
2101 rotate_and_cleanup.call(angle)
2104 $gesture_press = nil
2107 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2108 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2109 current_y_sub_albums += 1
2112 if $xmldir.child_byname_notattr('dir', 'deleted')
2114 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2115 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2116 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2117 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2118 #- this album image/caption
2119 if $xmldir.attributes['thumbnails-caption']
2120 add_subalbum.call($xmldir, 0)
2123 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2124 $xmldir.elements.each { |element|
2125 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2126 #- element (image or video) of this album
2127 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2128 msg 3, "dest_img: #{dest_img}"
2129 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
2130 total[element.name] += 1
2132 if element.name == 'dir' && !element.attributes['deleted']
2133 #- sub-album image/caption
2134 add_subalbum.call(element, subalbums_counter += 1)
2135 total[element.name] += 1
2138 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2139 total['image'], total['video'], total['dir'] ]))
2140 $subalbums_vb.add($subalbums)
2141 $subalbums_vb.show_all
2143 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2144 $notebook.get_tab_label($autotable_sw).sensitive = false
2145 $notebook.set_page(0)
2146 $thumbnails_title.buffer.text = ''
2148 $notebook.get_tab_label($autotable_sw).sensitive = true
2149 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2152 if !$xmldir.child_byname_notattr('dir', 'deleted')
2153 $notebook.get_tab_label($subalbums_sw).sensitive = false
2154 $notebook.set_page(1)
2156 $notebook.get_tab_label($subalbums_sw).sensitive = true
2160 def pixbuf_or_nil(filename)
2162 return Gdk::Pixbuf.new(filename)
2168 def theme_choose(current)
2169 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2171 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2172 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2173 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2175 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2176 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2177 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2178 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2179 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2180 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2181 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2182 treeview.signal_connect('button-press-event') { |w, event|
2183 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2184 dialog.response(Gtk::Dialog::RESPONSE_OK)
2188 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2190 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2193 iter[0] = File.basename(dir)
2194 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2195 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2196 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2197 if File.basename(dir) == current
2198 treeview.selection.select_iter(iter)
2202 dialog.set_default_size(700, 400)
2203 dialog.vbox.show_all
2204 dialog.run { |response|
2205 iter = treeview.selection.selected
2207 if response == Gtk::Dialog::RESPONSE_OK && iter
2208 return model.get_value(iter, 0)
2214 def show_password_protections
2215 examine_dir_elem = Proc.new { |parent_iter, xmldir, already_protected|
2216 child_iter = $albums_iters[xmldir.attributes['path']]
2217 if xmldir.attributes['password-protect']
2218 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2219 already_protected = true
2220 elsif already_protected
2221 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2223 pix = pix.saturate_and_pixelate(1, true)
2229 xmldir.elements.each('dir') { |elem|
2230 if !elem.attributes['deleted']
2231 examine_dir_elem.call(child_iter, elem, already_protected)
2235 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2238 def populate_subalbums_treeview
2242 $subalbums_vb.children.each { |chld|
2243 $subalbums_vb.remove(chld)
2246 source = $xmldoc.root.attributes['source']
2247 msg 3, "source: #{source}"
2249 xmldir = $xmldoc.elements['//dir']
2250 if !xmldir || xmldir.attributes['path'] != source
2251 msg 1, _("Corrupted booh file...")
2255 append_dir_elem = Proc.new { |parent_iter, xmldir|
2256 child_iter = $albums_ts.append(parent_iter)
2257 child_iter[0] = File.basename(xmldir.attributes['path'])
2258 child_iter[1] = xmldir.attributes['path']
2259 $albums_iters[xmldir.attributes['path']] = child_iter
2260 msg 3, "puttin location: #{xmldir.attributes['path']}"
2261 xmldir.elements.each('dir') { |elem|
2262 if !elem.attributes['deleted']
2263 append_dir_elem.call(child_iter, elem)
2267 append_dir_elem.call(nil, xmldir)
2268 show_password_protections
2270 $albums_tv.expand_all
2271 $albums_tv.selection.select_iter($albums_ts.iter_first)
2274 def open_file(filename)
2278 $current_path = nil #- invalidate
2279 $modified_pixbufs = {}
2282 $subalbums_vb.children.each { |chld|
2283 $subalbums_vb.remove(chld)
2286 if !File.exists?(filename)
2287 return utf8(_("File not found."))
2291 $xmldoc = REXML::Document.new File.new(filename)
2296 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2297 if entry2type(filename).nil?
2298 return utf8(_("Not a booh file!"))
2300 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."))
2304 if !source = $xmldoc.root.attributes['source']
2305 return utf8(_("Corrupted booh file..."))
2308 if !dest = $xmldoc.root.attributes['destination']
2309 return utf8(_("Corrupted booh file..."))
2312 if !theme = $xmldoc.root.attributes['theme']
2313 return utf8(_("Corrupted booh file..."))
2316 if $xmldoc.root.attributes['version'] != $VERSION
2317 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2318 mark_document_as_dirty
2319 $xmldoc.root.add_attribute('version', $VERSION)
2322 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2323 optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2324 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2326 $filename = filename
2327 select_theme(theme, limit_sizes, optimizefor32, nperrow)
2328 $default_size['thumbnails'] =~ /(.*)x(.*)/
2329 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2330 $albums_thumbnail_size =~ /(.*)x(.*)/
2331 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2333 populate_subalbums_treeview
2335 $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
2339 def open_file_user(filename)
2340 result = open_file(filename)
2342 $config['last-opens'] ||= []
2343 if $config['last-opens'][-1] != utf8(filename)
2344 $config['last-opens'] << utf8(filename)
2346 $orig_filename = $filename
2347 tmp = Tempfile.new("boohtemp")
2350 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2352 $tempfiles << $filename << "#{$filename}.backup"
2354 $orig_filename = nil
2360 if !ask_save_modifications(utf8(_("Save this album?")),
2361 utf8(_("Do you want to save the changes to this album?")),
2362 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2365 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2367 Gtk::FileChooser::ACTION_OPEN,
2369 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2370 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2371 fc.set_current_folder(File.expand_path("~/.booh"))
2372 fc.transient_for = $main_window
2375 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2376 push_mousecursor_wait(fc)
2377 msg = open_file_user(fc.filename)
2393 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2398 def additional_booh_options
2401 options += "--mproc #{$config['mproc'].to_i} "
2403 if $config['emptycomments']
2404 options += "--empty-comments "
2410 if !ask_save_modifications(utf8(_("Save this album?")),
2411 utf8(_("Do you want to save the changes to this album?")),
2412 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2415 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2417 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2418 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2419 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2421 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2422 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2423 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2424 tbl.attach(src = Gtk::Entry.new,
2425 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2426 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2427 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2428 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2429 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2430 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2431 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2432 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2433 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2434 tbl.attach(dest = Gtk::Entry.new,
2435 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2436 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2437 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2438 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2439 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2440 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2441 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2442 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2443 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2445 tooltips = Gtk::Tooltips.new
2446 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2447 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2448 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2449 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2450 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2451 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2452 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)
2453 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2454 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2456 src_nb_calculated_for = ''
2458 process_src_nb = Proc.new {
2459 if src.text != src_nb_calculated_for
2460 src_nb_calculated_for = src.text
2462 Thread.kill(src_nb_thread)
2465 if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2466 if File.readable?(from_utf8(src_nb_calculated_for))
2467 src_nb_thread = Thread.new {
2468 gtk_thread_protect { puts "destroyed: " + src_nb.destroyed?.to_s; src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2469 total = { 'image' => 0, 'video' => 0, nil => 0 }
2470 `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2471 if File.basename(dir) =~ /^\./
2475 Dir.entries(dir.chomp).each { |file|
2476 total[entry2type(file)] += 1
2478 rescue Errno::EACCES, Errno::ENOENT
2482 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'] ])) }
2486 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2489 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2494 timeout_src_nb = Gtk.timeout_add(100) {
2498 src_browse.signal_connect('clicked') {
2499 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2501 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2503 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2504 fc.transient_for = $main_window
2505 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2506 src.text = utf8(fc.filename)
2508 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2513 dest_browse.signal_connect('clicked') {
2514 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2516 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2518 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2519 fc.transient_for = $main_window
2520 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2521 dest.text = utf8(fc.filename)
2526 conf_browse.signal_connect('clicked') {
2527 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2529 Gtk::FileChooser::ACTION_SAVE,
2531 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2532 fc.transient_for = $main_window
2533 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2534 fc.set_current_folder(File.expand_path("~/.booh"))
2535 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2536 conf.text = utf8(fc.filename)
2543 recreate_theme_config = proc {
2544 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2546 select_theme(theme_button.label, 'all', optimize432.active?, nil)
2547 $images_size.each { |s|
2548 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2552 tooltips.set_tip(cb, utf8(s['description']), nil)
2553 theme_sizes << { :widget => cb, :value => s['name'] }
2555 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2556 tooltips = Gtk::Tooltips.new
2557 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2558 theme_sizes << { :widget => cb, :value => 'original' }
2561 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2564 $allowed_N_values.each { |n|
2566 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2568 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2573 nperrows << { :widget => rb, :value => n }
2575 nperrowradios.show_all
2577 recreate_theme_config.call
2579 theme_button.signal_connect('clicked') {
2580 if newtheme = theme_choose(theme_button.label)
2581 theme_button.label = newtheme
2582 recreate_theme_config.call
2586 dialog.vbox.add(frame1)
2587 dialog.vbox.add(frame2)
2588 dialog.window_position = Gtk::Window::POS_MOUSE
2594 dialog.run { |response|
2595 if response == Gtk::Dialog::RESPONSE_OK
2596 srcdir = from_utf8(src.text)
2597 destdir = from_utf8(dest.text)
2598 if !File.directory?(srcdir)
2599 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2601 elsif conf.text == ''
2602 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2604 elsif File.directory?(from_utf8(conf.text))
2605 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2607 elsif destdir != make_dest_filename(destdir)
2608 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2610 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2611 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2613 elsif File.exists?(destdir) && !File.directory?(destdir)
2614 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2616 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2617 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2619 system("mkdir '#{destdir}'")
2620 if !File.directory?(destdir)
2621 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2632 srcdir = from_utf8(src.text)
2633 destdir = from_utf8(dest.text)
2634 configskel = File.expand_path(from_utf8(conf.text))
2635 theme = theme_button.label
2636 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2637 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2638 opt432 = optimize432.active?
2640 Thread.kill(src_nb_thread)
2641 gtk_thread_abandon #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2644 Gtk.timeout_remove(timeout_src_nb)
2647 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2648 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2649 "#{opt432 ? '--optimize-for-32' : ''} #{additional_booh_options}",
2650 utf8(_("Please wait while scanning source directory...")),
2652 { :closure_after => proc { open_file_user(configskel) } })
2657 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2659 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2660 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2661 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2663 source = $xmldoc.root.attributes['source']
2664 dest = $xmldoc.root.attributes['destination']
2665 theme = $xmldoc.root.attributes['theme']
2666 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2667 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2668 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2670 limit_sizes = limit_sizes.split(/,/)
2673 tooltips = Gtk::Tooltips.new
2674 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2675 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2676 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2677 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2678 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2679 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2680 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2681 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>')),
2682 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2683 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2684 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2685 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>')),
2686 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2688 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2689 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2690 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2691 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2692 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2693 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2694 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)
2695 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2696 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2700 recreate_theme_config = proc {
2701 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2703 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2705 $images_size.each { |s|
2706 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2708 if limit_sizes.include?(s['name'])
2716 tooltips.set_tip(cb, utf8(s['description']), nil)
2717 theme_sizes << { :widget => cb, :value => s['name'] }
2719 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2720 tooltips = Gtk::Tooltips.new
2721 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2722 if limit_sizes && limit_sizes.include?('original')
2725 theme_sizes << { :widget => cb, :value => 'original' }
2728 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2731 $allowed_N_values.each { |n|
2733 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2735 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2737 nperrowradios.add(Gtk::Label.new(' '))
2738 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
2741 nperrows << { :widget => rb, :value => n.to_s }
2743 nperrowradios.show_all
2745 recreate_theme_config.call
2747 theme_button.signal_connect('clicked') {
2748 if newtheme = theme_choose(theme_button.label)
2751 theme_button.label = newtheme
2752 recreate_theme_config.call
2756 dialog.vbox.add(frame1)
2757 dialog.vbox.add(frame2)
2758 dialog.window_position = Gtk::Window::POS_MOUSE
2764 dialog.run { |response|
2765 if response == Gtk::Dialog::RESPONSE_OK
2766 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2767 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2776 save_theme = theme_button.label
2777 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
2778 save_opt432 = optimize432.active?
2779 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2782 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow)
2783 mark_document_as_dirty
2785 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
2786 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
2787 "#{save_opt432 ? '--optimize-for-32' : ''} #{additional_booh_options}",
2788 utf8(_("Please wait while scanning source directory...")),
2790 { :closure_after => proc {
2791 open_file($filename)
2800 sel = $albums_tv.selection.selected_rows
2802 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2803 "--verbose-level #{$verbose_level} #{additional_booh_options}",
2804 utf8(_("Please wait while scanning source directory...")),
2806 { :closure_after => proc {
2807 open_file($filename)
2808 $albums_tv.selection.select_path(sel[0])
2816 sel = $albums_tv.selection.selected_rows
2818 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2819 "--verbose-level #{$verbose_level} #{additional_booh_options}",
2820 utf8(_("Please wait while scanning source directory...")),
2822 { :closure_after => proc {
2823 open_file($filename)
2824 $albums_tv.selection.select_path(sel[0])
2832 theme = $xmldoc.root.attributes['theme']
2833 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2835 limit_sizes = "--sizes #{limit_sizes}"
2837 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
2838 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
2839 utf8(_("Please wait while scanning source directory...")),
2841 { :closure_after => proc {
2842 open_file($filename)
2848 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
2850 Gtk::FileChooser::ACTION_SAVE,
2852 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2853 fc.transient_for = $main_window
2854 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2855 fc.set_current_folder(File.expand_path("~/.booh"))
2856 fc.filename = $orig_filename
2857 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2858 $orig_filename = fc.filename
2859 save_current_file_user
2865 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
2867 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2868 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2869 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2871 dialog.vbox.add(notebook = Gtk::Notebook.new)
2872 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
2873 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
2874 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2875 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
2876 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2877 tooltips = Gtk::Tooltips.new
2878 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
2879 for example: /usr/bin/mplayer %f")), nil)
2880 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
2881 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2882 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
2883 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2884 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
2885 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
2886 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
2887 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2888 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)),
2889 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2890 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)
2891 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
2892 0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2893 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)
2894 tbl.attach(emptycomments_check = Gtk::CheckButton.new(utf8(_("Use empty comments for new albums"))),
2895 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
2896 tooltips.set_tip(emptycomments_check, utf8(_("Normally, filenames are used as comments for new albums. Check this if you prefer empty comments.")), nil)
2897 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
2898 0, 2, 5, 6, Gtk::FILL, Gtk::SHRINK, 2, 2)
2899 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)
2900 smp_check.signal_connect('toggled') {
2901 if smp_check.active?
2902 smp_hbox.sensitive = true
2904 smp_hbox.sensitive = false
2908 smp_check.active = true
2909 smp_spin.value = $config['mproc'].to_i
2911 nogestures_check.active = $config['nogestures']
2912 emptycomments_check.active = $config['emptycomments']
2913 deleteondisk_check.active = $config['deleteondisk']
2915 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
2916 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
2917 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2918 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
2919 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2921 dialog.vbox.show_all
2922 dialog.run { |response|
2923 if response == Gtk::Dialog::RESPONSE_OK
2924 $config['video-viewer'] = video_viewer_entry.text
2925 $config['browser'] = browser_entry.text
2926 if smp_check.active?
2927 $config['mproc'] = smp_spin.value.to_i
2929 $config.delete('mproc')
2931 $config['nogestures'] = nogestures_check.active?
2932 $config['emptycomments'] = emptycomments_check.active?
2933 $config['deleteondisk'] = deleteondisk_check.active?
2935 $config['convert-enhance'] = enhance_entry.text
2942 if $undo_tb.sensitive?
2943 $redo_tb.sensitive = $redo_mb.sensitive = true
2944 if not more_undoes = UndoHandler.undo($statusbar)
2945 $undo_tb.sensitive = $undo_mb.sensitive = false
2951 if $redo_tb.sensitive?
2952 $undo_tb.sensitive = $undo_mb.sensitive = true
2953 if not more_redoes = UndoHandler.redo($statusbar)
2954 $redo_tb.sensitive = $redo_mb.sensitive = false
2959 def show_one_click_explanation(intro)
2960 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
2962 %s When such a tool is activated
2963 (<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
2964 on a thumbnail will immediately apply the desired action.
2966 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
2972 GNU GENERAL PUBLIC LICENSE
2973 Version 2, June 1991
2975 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
2976 675 Mass Ave, Cambridge, MA 02139, USA
2977 Everyone is permitted to copy and distribute verbatim copies
2978 of this license document, but changing it is not allowed.
2982 The licenses for most software are designed to take away your
2983 freedom to share and change it. By contrast, the GNU General Public
2984 License is intended to guarantee your freedom to share and change free
2985 software--to make sure the software is free for all its users. This
2986 General Public License applies to most of the Free Software
2987 Foundation's software and to any other program whose authors commit to
2988 using it. (Some other Free Software Foundation software is covered by
2989 the GNU Library General Public License instead.) You can apply it to
2992 When we speak of free software, we are referring to freedom, not
2993 price. Our General Public Licenses are designed to make sure that you
2994 have the freedom to distribute copies of free software (and charge for
2995 this service if you wish), that you receive source code or can get it
2996 if you want it, that you can change the software or use pieces of it
2997 in new free programs; and that you know you can do these things.
2999 To protect your rights, we need to make restrictions that forbid
3000 anyone to deny you these rights or to ask you to surrender the rights.
3001 These restrictions translate to certain responsibilities for you if you
3002 distribute copies of the software, or if you modify it.
3004 For example, if you distribute copies of such a program, whether
3005 gratis or for a fee, you must give the recipients all the rights that
3006 you have. You must make sure that they, too, receive or can get the
3007 source code. And you must show them these terms so they know their
3010 We protect your rights with two steps: (1) copyright the software, and
3011 (2) offer you this license which gives you legal permission to copy,
3012 distribute and/or modify the software.
3014 Also, for each author's protection and ours, we want to make certain
3015 that everyone understands that there is no warranty for this free
3016 software. If the software is modified by someone else and passed on, we
3017 want its recipients to know that what they have is not the original, so
3018 that any problems introduced by others will not reflect on the original
3019 authors' reputations.
3021 Finally, any free program is threatened constantly by software
3022 patents. We wish to avoid the danger that redistributors of a free
3023 program will individually obtain patent licenses, in effect making the
3024 program proprietary. To prevent this, we have made it clear that any
3025 patent must be licensed for everyone's free use or not licensed at all.
3027 The precise terms and conditions for copying, distribution and
3028 modification follow.
3031 GNU GENERAL PUBLIC LICENSE
3032 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
3034 0. This License applies to any program or other work which contains
3035 a notice placed by the copyright holder saying it may be distributed
3036 under the terms of this General Public License. The "Program", below,
3037 refers to any such program or work, and a "work based on the Program"
3038 means either the Program or any derivative work under copyright law:
3039 that is to say, a work containing the Program or a portion of it,
3040 either verbatim or with modifications and/or translated into another
3041 language. (Hereinafter, translation is included without limitation in
3042 the term "modification".) Each licensee is addressed as "you".
3044 Activities other than copying, distribution and modification are not
3045 covered by this License; they are outside its scope. The act of
3046 running the Program is not restricted, and the output from the Program
3047 is covered only if its contents constitute a work based on the
3048 Program (independent of having been made by running the Program).
3049 Whether that is true depends on what the Program does.
3051 1. You may copy and distribute verbatim copies of the Program's
3052 source code as you receive it, in any medium, provided that you
3053 conspicuously and appropriately publish on each copy an appropriate
3054 copyright notice and disclaimer of warranty; keep intact all the
3055 notices that refer to this License and to the absence of any warranty;
3056 and give any other recipients of the Program a copy of this License
3057 along with the Program.
3059 You may charge a fee for the physical act of transferring a copy, and
3060 you may at your option offer warranty protection in exchange for a fee.
3062 2. You may modify your copy or copies of the Program or any portion
3063 of it, thus forming a work based on the Program, and copy and
3064 distribute such modifications or work under the terms of Section 1
3065 above, provided that you also meet all of these conditions:
3067 a) You must cause the modified files to carry prominent notices
3068 stating that you changed the files and the date of any change.
3070 b) You must cause any work that you distribute or publish, that in
3071 whole or in part contains or is derived from the Program or any
3072 part thereof, to be licensed as a whole at no charge to all third
3073 parties under the terms of this License.
3075 c) If the modified program normally reads commands interactively
3076 when run, you must cause it, when started running for such
3077 interactive use in the most ordinary way, to print or display an
3078 announcement including an appropriate copyright notice and a
3079 notice that there is no warranty (or else, saying that you provide
3080 a warranty) and that users may redistribute the program under
3081 these conditions, and telling the user how to view a copy of this
3082 License. (Exception: if the Program itself is interactive but
3083 does not normally print such an announcement, your work based on
3084 the Program is not required to print an announcement.)
3087 These requirements apply to the modified work as a whole. If
3088 identifiable sections of that work are not derived from the Program,
3089 and can be reasonably considered independent and separate works in
3090 themselves, then this License, and its terms, do not apply to those
3091 sections when you distribute them as separate works. But when you
3092 distribute the same sections as part of a whole which is a work based
3093 on the Program, the distribution of the whole must be on the terms of
3094 this License, whose permissions for other licensees extend to the
3095 entire whole, and thus to each and every part regardless of who wrote it.
3097 Thus, it is not the intent of this section to claim rights or contest
3098 your rights to work written entirely by you; rather, the intent is to
3099 exercise the right to control the distribution of derivative or
3100 collective works based on the Program.
3102 In addition, mere aggregation of another work not based on the Program
3103 with the Program (or with a work based on the Program) on a volume of
3104 a storage or distribution medium does not bring the other work under
3105 the scope of this License.
3107 3. You may copy and distribute the Program (or a work based on it,
3108 under Section 2) in object code or executable form under the terms of
3109 Sections 1 and 2 above provided that you also do one of the following:
3111 a) Accompany it with the complete corresponding machine-readable
3112 source code, which must be distributed under the terms of Sections
3113 1 and 2 above on a medium customarily used for software interchange; or,
3115 b) Accompany it with a written offer, valid for at least three
3116 years, to give any third party, for a charge no more than your
3117 cost of physically performing source distribution, a complete
3118 machine-readable copy of the corresponding source code, to be
3119 distributed under the terms of Sections 1 and 2 above on a medium
3120 customarily used for software interchange; or,
3122 c) Accompany it with the information you received as to the offer
3123 to distribute corresponding source code. (This alternative is
3124 allowed only for noncommercial distribution and only if you
3125 received the program in object code or executable form with such
3126 an offer, in accord with Subsection b above.)
3128 The source code for a work means the preferred form of the work for
3129 making modifications to it. For an executable work, complete source
3130 code means all the source code for all modules it contains, plus any
3131 associated interface definition files, plus the scripts used to
3132 control compilation and installation of the executable. However, as a
3133 special exception, the source code distributed need not include
3134 anything that is normally distributed (in either source or binary
3135 form) with the major components (compiler, kernel, and so on) of the
3136 operating system on which the executable runs, unless that component
3137 itself accompanies the executable.
3139 If distribution of executable or object code is made by offering
3140 access to copy from a designated place, then offering equivalent
3141 access to copy the source code from the same place counts as
3142 distribution of the source code, even though third parties are not
3143 compelled to copy the source along with the object code.
3146 4. You may not copy, modify, sublicense, or distribute the Program
3147 except as expressly provided under this License. Any attempt
3148 otherwise to copy, modify, sublicense or distribute the Program is
3149 void, and will automatically terminate your rights under this License.
3150 However, parties who have received copies, or rights, from you under
3151 this License will not have their licenses terminated so long as such
3152 parties remain in full compliance.
3154 5. You are not required to accept this License, since you have not
3155 signed it. However, nothing else grants you permission to modify or
3156 distribute the Program or its derivative works. These actions are
3157 prohibited by law if you do not accept this License. Therefore, by
3158 modifying or distributing the Program (or any work based on the
3159 Program), you indicate your acceptance of this License to do so, and
3160 all its terms and conditions for copying, distributing or modifying
3161 the Program or works based on it.
3163 6. Each time you redistribute the Program (or any work based on the
3164 Program), the recipient automatically receives a license from the
3165 original licensor to copy, distribute or modify the Program subject to
3166 these terms and conditions. You may not impose any further
3167 restrictions on the recipients' exercise of the rights granted herein.
3168 You are not responsible for enforcing compliance by third parties to
3171 7. If, as a consequence of a court judgment or allegation of patent
3172 infringement or for any other reason (not limited to patent issues),
3173 conditions are imposed on you (whether by court order, agreement or
3174 otherwise) that contradict the conditions of this License, they do not
3175 excuse you from the conditions of this License. If you cannot
3176 distribute so as to satisfy simultaneously your obligations under this
3177 License and any other pertinent obligations, then as a consequence you
3178 may not distribute the Program at all. For example, if a patent
3179 license would not permit royalty-free redistribution of the Program by
3180 all those who receive copies directly or indirectly through you, then
3181 the only way you could satisfy both it and this License would be to
3182 refrain entirely from distribution of the Program.
3184 If any portion of this section is held invalid or unenforceable under
3185 any particular circumstance, the balance of the section is intended to
3186 apply and the section as a whole is intended to apply in other
3189 It is not the purpose of this section to induce you to infringe any
3190 patents or other property right claims or to contest validity of any
3191 such claims; this section has the sole purpose of protecting the
3192 integrity of the free software distribution system, which is
3193 implemented by public license practices. Many people have made
3194 generous contributions to the wide range of software distributed
3195 through that system in reliance on consistent application of that
3196 system; it is up to the author/donor to decide if he or she is willing
3197 to distribute software through any other system and a licensee cannot
3200 This section is intended to make thoroughly clear what is believed to
3201 be a consequence of the rest of this License.
3204 8. If the distribution and/or use of the Program is restricted in
3205 certain countries either by patents or by copyrighted interfaces, the
3206 original copyright holder who places the Program under this License
3207 may add an explicit geographical distribution limitation excluding
3208 those countries, so that distribution is permitted only in or among
3209 countries not thus excluded. In such case, this License incorporates
3210 the limitation as if written in the body of this License.
3212 9. The Free Software Foundation may publish revised and/or new versions
3213 of the General Public License from time to time. Such new versions will
3214 be similar in spirit to the present version, but may differ in detail to
3215 address new problems or concerns.
3217 Each version is given a distinguishing version number. If the Program
3218 specifies a version number of this License which applies to it and "any
3219 later version", you have the option of following the terms and conditions
3220 either of that version or of any later version published by the Free
3221 Software Foundation. If the Program does not specify a version number of
3222 this License, you may choose any version ever published by the Free Software
3225 10. If you wish to incorporate parts of the Program into other free
3226 programs whose distribution conditions are different, write to the author
3227 to ask for permission. For software which is copyrighted by the Free
3228 Software Foundation, write to the Free Software Foundation; we sometimes
3229 make exceptions for this. Our decision will be guided by the two goals
3230 of preserving the free status of all derivatives of our free software and
3231 of promoting the sharing and reuse of software generally.
3235 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
3236 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
3237 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
3238 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
3239 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
3240 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
3241 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
3242 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
3243 REPAIR OR CORRECTION.
3245 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
3246 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
3247 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
3248 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
3249 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
3250 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
3251 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
3252 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
3253 POSSIBILITY OF SUCH DAMAGES.
3257 def create_menu_and_toolbar
3260 mb = Gtk::MenuBar.new
3262 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3263 filesubmenu = Gtk::Menu.new
3264 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3265 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3266 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3267 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3268 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3269 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3270 tooltips = Gtk::Tooltips.new
3271 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3272 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3273 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3274 filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3275 $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3276 tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3277 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3278 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3279 tooltips.set_tip($merge, utf8(_("Take into account new subalbums (subdirectories) and new/removed images/videos in existing subalbums (anywhere)")), nil)
3280 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3281 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3282 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3283 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
3284 filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
3285 $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
3286 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3287 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
3288 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
3289 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3290 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
3291 filemenu.set_submenu(filesubmenu)
3294 new.signal_connect('activate') { new_album }
3295 open.signal_connect('activate') { open_file_popup }
3296 $save.signal_connect('activate') { save_current_file_user }
3297 $save_as.signal_connect('activate') { save_as_do }
3298 $merge_current.signal_connect('activate') { merge_current }
3299 $merge_newsubs.signal_connect('activate') { merge_newsubs }
3300 $merge.signal_connect('activate') { merge }
3301 $generate.signal_connect('activate') {
3303 call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
3304 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
3306 { :successmsg => utf8(_("Your web-album is now ready in directory `%s'.
3307 Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
3308 :successmsg_linkurl => $xmldoc.root.attributes['destination'],
3309 :failuremsg => utf8(_("There was something wrong when generating the web-album, sorry.")),
3310 :closure_after => proc {
3311 $xmldoc.elements.each('//dir') { |elem|
3312 elem.add_attribute('already-generated', 'true')
3314 UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
3315 $undo_tb.sensitive = $undo_mb.sensitive = false
3316 $redo_tb.sensitive = $redo_mb.sensitive = false
3318 $generated_outofline = true
3321 $view_wa.signal_connect('activate') {
3322 indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
3323 if File.exists?(indexhtml)
3326 show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
3329 $properties.signal_connect('activate') { properties }
3331 quit.signal_connect('activate') { try_quit }
3333 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
3334 editsubmenu = Gtk::Menu.new
3335 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
3336 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
3337 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3338 editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
3339 $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
3340 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)
3341 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3342 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
3343 editmenu.set_submenu(editsubmenu)
3346 $remove_all_captions.signal_connect('activate') { remove_all_captions }
3348 prefs.signal_connect('activate') { preferences }
3350 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
3351 helpsubmenu = Gtk::Menu.new
3352 helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
3353 one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
3354 helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
3355 speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
3356 helpsubmenu.append( Gtk::SeparatorMenuItem.new)
3357 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
3358 helpmenu.set_submenu(helpsubmenu)
3361 one_click.signal_connect('activate') {
3362 show_one_click_explanation(_("One-Click tools are available in the toolbar."))
3365 speed.signal_connect('activate') {
3366 show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
3368 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
3369 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
3370 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
3371 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
3372 <span foreground='darkblue'>Control-Delete</span>: delete image
3373 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
3374 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
3375 <span foreground='darkblue'>Control-z</span>: undo
3376 <span foreground='darkblue'>Control-r</span>: redo
3378 <span size='large' weight='bold'>Mouse gestures:</span>
3380 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
3381 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
3383 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
3384 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
3385 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
3386 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
3387 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
3388 ")), { :pos_centered => true, :not_transient => true })
3392 about.signal_connect('activate') {
3393 Gtk::AboutDialog.set_url_hook { |dialog, url| open_url(url) }
3394 Gtk::AboutDialog.show($main_window, { :name => 'booh',
3395 :version => $VERSION,
3396 :copyright => 'Copyright (c) 2005 Guillaume Cottenceau',
3397 :license => get_license,
3398 :website => 'http://zarb.org/~gc/html/booh.html',
3399 :authors => [ 'Guillaume Cottenceau' ],
3400 :artists => [ 'Ayo73' ],
3401 :comments => utf8(_("''The Web-Album of choice for discriminating Linux users''")),
3402 :translator_credits => utf8(_('Japanese: Masao Mutoh
3403 German: Roland Eckert
3404 French: Guillaume Cottenceau')),
3405 :logo => Gdk::Pixbuf.new("#{$FPATH}/images/logo.png") })
3410 tb = Gtk::Toolbar.new
3412 tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
3413 open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
3414 open.menu = Gtk::Menu.new
3415 open.signal_connect('clicked') { open_file_popup }
3416 open.signal_connect('show-menu') {
3417 lastopens = Gtk::Menu.new
3419 if $config['last-opens']
3420 $config['last-opens'].reverse.each { |e|
3421 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
3422 item.signal_connect('activate') {
3423 if ask_save_modifications(utf8(_("Save this album?")),
3424 utf8(_("Do you want to save the changes to this album?")),
3425 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3426 push_mousecursor_wait
3427 msg = open_file_user(from_utf8(e))
3430 show_popup($main_window, msg)
3438 open.menu = lastopens
3441 tb.insert(-1, Gtk::SeparatorToolItem.new)
3443 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
3444 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
3445 $r90.label = utf8(_("Rotate"))
3446 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
3447 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
3448 $r270.label = utf8(_("Rotate"))
3449 tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
3450 $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
3451 $enhance.label = utf8(_("Enhance"))
3452 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
3453 $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
3454 tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
3455 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
3456 nothing.label = utf8(_("None"))
3458 tb.insert(-1, Gtk::SeparatorToolItem.new)
3460 tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
3461 tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
3464 $undo_tb.signal_connect('clicked') { perform_undo }
3465 $undo_mb.signal_connect('activate') { perform_undo }
3466 $redo_tb.signal_connect('clicked') { perform_redo }
3467 $redo_mb.signal_connect('activate') { perform_redo }
3469 one_click_explain_try = Proc.new {
3470 if !$config['one-click-explained']
3471 show_one_click_explanation(_("You have just clicked on a One-Click tool."))
3472 $config['one-click-explained'] = true
3476 $r90.signal_connect('toggled') {
3478 set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
3479 one_click_explain_try.call
3480 $r270.active = false
3481 $enhance.active = false
3482 $delete.active = false
3483 nothing.sensitive = true
3485 if !$r270.active? && !$enhance.active? && !$delete.active?
3486 set_mousecursor_normal
3487 nothing.sensitive = false
3489 nothing.sensitive = true
3493 $r270.signal_connect('toggled') {
3495 set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
3496 one_click_explain_try.call
3498 $enhance.active = false
3499 $delete.active = false
3500 nothing.sensitive = true
3502 if !$r90.active? && !$enhance.active? && !$delete.active?
3503 set_mousecursor_normal
3504 nothing.sensitive = false
3506 nothing.sensitive = true
3510 $enhance.signal_connect('toggled') {
3512 set_mousecursor(Gdk::Cursor::SPRAYCAN)
3513 one_click_explain_try.call
3515 $r270.active = false
3516 $delete.active = false
3517 nothing.sensitive = true
3519 if !$r90.active? && !$r270.active? && !$delete.active?
3520 set_mousecursor_normal
3521 nothing.sensitive = false
3523 nothing.sensitive = true
3527 $delete.signal_connect('toggled') {
3529 set_mousecursor(Gdk::Cursor::PIRATE)
3530 one_click_explain_try.call
3532 $r270.active = false
3533 $enhance.active = false
3534 nothing.sensitive = true
3536 if !$r90.active? && !$r270.active? && !$enhance.active?
3537 set_mousecursor_normal
3538 nothing.sensitive = false
3540 nothing.sensitive = true
3544 nothing.signal_connect('clicked') {
3545 $r90.active = $r270.active = $enhance.active = $delete.active = false
3546 set_mousecursor_normal
3552 def gtk_thread_protect(&proc)
3553 if Thread.current == Thread.main
3556 $protect_gtk_pending_calls.synchronize {
3557 $gtk_pending_calls << proc
3562 def gtk_thread_abandon
3563 $protect_gtk_pending_calls.try_lock
3564 $gtk_pending_calls = []
3565 $protect_gtk_pending_calls.unlock
3568 def ask_password_protect
3569 value = $xmldir.attributes['password-protect']
3571 dialog = Gtk::Dialog.new(utf8(_("Password protect this sub-album")),
3573 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3574 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3575 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3577 lbl = Gtk::Label.new
3579 _("You can choose to <b>password protect</b> the sub-album '%s' (only available
3580 if you plan to publish your web-album with an Apache web-server). This will use
3581 the .htaccess/.htpasswd feature of Apache (not so strongly crypted password, but
3582 generally ok for protecting web contents). Users will be prompted with a dialog
3583 asking for a username and a password, failure to give the correct pair will
3585 ") % File.basename($current_path))
3586 dialog.vbox.add(lbl)
3587 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")))).
3588 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("password protect with password file:")))).
3589 add(file = Gtk::Entry.new)))
3590 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")))).
3591 add(Gtk::Label.new).
3592 add(bt_gen = Gtk::Button.new(utf8(_("generate a password file"))))))
3593 dialog.window_position = Gtk::Window::POS_MOUSE
3598 rb_yes.active = true
3602 bt_help.signal_connect('clicked') {
3603 show_popup(dialog, utf8(
3604 _("Password protection proposed here uses the .htaccess/.htpasswd features
3605 proposed by Apache. So first, be sure you will publish your web-album on an
3606 Apache web-server. Second, you will need to have a .htpasswd file accessible
3607 by Apache somewhere on the web-server disks. The password file you must
3608 provide in the dialog when choosing to password protect is the full absolute
3609 path to access this file <b>on the web-server</b> (not on your machine). Note
3610 that if you use a relative path, it will be considered relative to the
3611 Document Root of the Apache configuration.")))
3614 bt_gen.signal_connect('clicked') {
3615 gendialog = Gtk::Dialog.new(utf8(_("Generate a password file")),
3617 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3618 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3619 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3621 lbl = Gtk::Label.new
3623 _("I can generate a password file (.htpasswd for Apache) for you. Just type
3624 the username and password you wish to put in it below and validate."))
3625 gendialog.vbox.add(lbl)
3626 gendialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(Gtk::Label.new(utf8(_('Username:')))).
3627 add(user = Gtk::Entry.new).
3628 add(Gtk::Label.new(utf8(_('Password:')))).
3629 add(pass = Gtk::Entry.new)))
3630 pass.visibility = false
3631 gendialog.window_position = Gtk::Window::POS_MOUSE
3633 gendialog.run { |response|
3637 if response == Gtk::Dialog::RESPONSE_OK
3639 ary = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + [ '.', '/' ]
3640 return ary[rand(ary.length)]
3642 fout = Tempfile.new("htpasswd")
3643 fout.write("#{u}:#{p.crypt(rand_letter + rand_letter)}\n")
3645 File.chmod(0644, fout.path)
3646 show_popup(dialog, utf8(
3647 _("The file <b>%s</b> now contains the username and the crypted password. Now
3648 copy it to a suitable location on the machine hosting the Apache web-server (better not
3649 below the Document Root), and specify this location in the password protect dialog.") % fout.path), { :selectable => true })
3654 dialog.run { |response|
3661 if response == Gtk::Dialog::RESPONSE_OK && value != newval
3663 msg 3, "changing password protection of #{$current_path} to #{newval}"
3665 $xmldir.delete_attribute('password-protect')
3667 $xmldir.add_attribute('password-protect', newval)
3669 save_undo(_("set password protection for %s") % File.basename($current_path),
3672 $xmldir.delete_attribute('password-protect')
3674 $xmldir.add_attribute('password-protect', value)
3678 $xmldir.delete_attribute('password-protect')
3680 $xmldir.add_attribute('password-protect', newval)
3684 show_password_protections
3689 def create_main_window
3691 mb, tb = create_menu_and_toolbar
3693 $albums_tv = Gtk::TreeView.new
3694 $albums_tv.set_size_request(120, -1)
3695 $albums_tv.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }))
3696 $albums_tv.append_column(tcol = Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, { :text => 0 }))
3697 $albums_tv.expander_column = tcol
3698 $albums_tv.set_headers_visible(false)
3699 $albums_tv.selection.signal_connect('changed') { |w|
3700 push_mousecursor_wait
3704 msg 3, "no selection"
3706 $current_path = $albums_ts.get_value(iter, 1)
3711 $albums_tv.signal_connect('button-release-event') { |w, event|
3712 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3 && !$current_path.nil?
3713 menu = Gtk::Menu.new
3714 menu.append(passprotect = Gtk::ImageMenuItem.new(utf8(_("Password protect"))))
3715 passprotect.image = Gtk::Image.new("#{$FPATH}/images/galeon-secure.png")
3716 passprotect.signal_connect('activate') { ask_password_protect }
3718 menu.popup(nil, nil, event.button, event.time)
3722 $albums_ts = Gtk::TreeStore.new(String, String, Gdk::Pixbuf)
3723 $albums_tv.set_model($albums_ts)
3724 $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
3726 albums_sw = Gtk::ScrolledWindow.new(nil, nil)
3727 albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
3728 albums_sw.add_with_viewport($albums_tv)
3730 $notebook = Gtk::Notebook.new
3731 create_subalbums_page
3732 $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
3734 $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
3736 $notebook.signal_connect('switch-page') { |w, page, num|
3738 $delete.active = false
3739 $delete.sensitive = false
3741 $delete.sensitive = true
3743 if $xmldir && $subalbums_edits[$xmldir.attributes['path']] && textview = $subalbums_edits[$xmldir.attributes['path']][:editzone]
3745 textview.buffer.text = $thumbnails_title.buffer.text
3747 if $notebook.get_tab_label($autotable_sw).sensitive?
3748 $thumbnails_title.buffer.text = textview.buffer.text
3754 paned = Gtk::HPaned.new
3755 paned.pack1(albums_sw, false, false)
3756 paned.pack2($notebook, true, true)
3758 main_vbox = Gtk::VBox.new(false, 0)
3759 main_vbox.pack_start(mb, false, false)
3760 main_vbox.pack_start(tb, false, false)
3761 main_vbox.pack_start(paned, true, true)
3762 main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
3764 $main_window = Gtk::Window.new
3765 $main_window.add(main_vbox)
3766 $main_window.signal_connect('delete-event') {
3767 try_quit({ :disallow_cancel => true })
3770 #- read/save size and position of window
3771 if $config['pos-x'] && $config['pos-y']
3772 $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
3774 $main_window.window_position = Gtk::Window::POS_CENTER
3776 msg 3, "size: #{$config['width']}x#{$config['height']}"
3777 $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
3778 $main_window.signal_connect('configure-event') {
3779 msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
3780 x, y = $main_window.window.root_origin
3781 width, height = $main_window.window.size
3782 $config['pos-x'] = x
3783 $config['pos-y'] = y
3784 $config['width'] = width
3785 $config['height'] = height
3789 $protect_gtk_pending_calls = Mutex.new
3790 $gtk_pending_calls = []
3791 Gtk.timeout_add(100) {
3792 $protect_gtk_pending_calls.synchronize {
3793 $gtk_pending_calls.each { |c| c.call }
3794 $gtk_pending_calls = []
3799 $statusbar.push(0, utf8(_("Ready.")))
3800 $main_window.show_all
3803 Thread.abort_on_exception = true
3813 open_file_user(ARGV[0])