#! /usr/bin/ruby # # * BOOH * # # A.k.a `Best web-album Of the world, Or your money back, Humerus'. # # The acronyn sucks, however this is a tribute to Dragon Ball by # Akira Toriyama, where the last enemy beaten by heroes of Dragon # Ball is named "Boo". But there was already a free software project # called Boo, so this one will be it "Booh". Or whatever. # # # Copyright (c) 2004 Guillaume Cottenceau # # This software may be freely redistributed under the terms of the GNU # public license version 2. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. require 'getoptlong' require 'tempfile' require 'thread' require 'gtk2' require 'booh/gtkadds' require 'booh/GtkAutoTable' require 'gettext' include GetText bindtextdomain("booh") require 'rexml/document' include REXML require 'booh/booh-lib' include Booh require 'booh/UndoHandler' #- options $options = [ [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ], [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ], ] def usage puts _("Usage: %s [OPTION]...") % File.basename($0) $options.each { |ary| printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3] } end def handle_options parser = GetoptLong.new parser.set_options(*$options.collect { |ary| ary[0..2] }) begin parser.each_option do |name, arg| case name when '--help' usage exit(0) when '--verbose-level' $verbose_level = arg.to_i end end rescue puts $! usage exit(1) end end def read_config $config = {} $config_file = File.expand_path('~/.booh-gui-rc') if File.readable?($config_file) $xmldoc = REXML::Document.new(File.new($config_file)) $xmldoc.root.elements.each { |element| txt = element.get_text if txt if txt.value =~ /~~~/ || element.name == 'last-opens' $config[element.name] = txt.value.split(/~~~/) else $config[element.name] = txt.value end else $config[element.name] = {} element.each { |chld| txt = chld.get_text $config[element.name][chld.name] = txt ? txt.value : nil } end } end $config['video-viewer'] ||= 'mplayer %f' if !FileTest.directory?(File.expand_path('~/.booh')) system("mkdir ~/.booh") end $tempfiles = [] end def write_config if $config['last-opens'] && $config['last-opens'].size > 5 $config['last-opens'] = $config['last-opens'][-5, 5] end ios = File.open($config_file, "w") $xmldoc = Document.new "" $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET) $config.each_pair { |key, value| elem = $xmldoc.root.add_element key if value.is_a? Hash $config[key].each_pair { |subkey, subvalue| subelem = elem.add_element subkey subelem.add_text subvalue.to_s } elsif value.is_a? Array elem.add_text value.join('~~~') else if !value elem.remove else elem.add_text value.to_s end end } $xmldoc.write(ios, 0) ios.close $tempfiles.each { |f| system("rm -f #{f}") } end def set_mousecursor(what, *widget) if widget[0] && widget[0].window widget[0].window.set_cursor(Gdk::Cursor.new(what)) end if $main_window.window $main_window.window.set_cursor(Gdk::Cursor.new(what)) end $current_cursor = what end def set_mousecursor_wait(*widget) gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) } if Thread.current == Thread.main Gtk.main_iteration while Gtk.events_pending? end end def set_mousecursor_normal(*widget) gtk_thread_protect { set_mousecursor($save_cursor = Gdk::Cursor::LEFT_PTR, *widget) } end def push_mousecursor_wait(*widget) if $current_cursor != Gdk::Cursor::WATCH $save_cursor = $current_cursor gtk_thread_protect { set_mousecursor_wait(*widget) } end end def pop_mousecursor(*widget) gtk_thread_protect { set_mousecursor($save_cursor || Gdk::Cursor::LEFT_PTR, *widget) } end def current_dest_dir source = $xmldoc.root.attributes['source'] dest = $xmldoc.root.attributes['destination'] return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest))) end def full_src_dir_to_rel(path) source = from_utf8($xmldoc.root.attributes['source']) return path.sub(/^#{Regexp.quote(source)}/, '') end def build_full_dest_filename(filename) return current_dest_dir + '/' + make_dest_filename(from_utf8(filename)) end def save_undo(name, closure, *params) UndoHandler.save_undo(name, closure, [ *params ]) $undo_tb.sensitive = $undo_mb.sensitive = true $redo_tb.sensitive = $redo_mb.sensitive = false end def view_element(filename, closures) if entry2type(filename) == 'video' cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &' msg 2, cmd system(cmd) return end w = Gtk::Window.new.set_title(filename) msg 3, "filename: #{filename}" dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg" #- typically this file won't exist in case of videos; try with the largest thumbnail around if !File.exists?(dest_img) if entry2type(filename) == 'video' alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort if not alternatives.empty? dest_img = alternatives[-1] end else push_mousecursor_wait gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ]) pop_mousecursor if !File.exists?(dest_img) msg 2, _("Could not generate fullscreen thumbnail!") return end end end 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))) evt.signal_connect('button-press-event') { |this, event| if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y } end if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3 menu = Gtk::Menu.new menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE)) delete_item.signal_connect('activate') { w.destroy closures[:delete].call } menu.show_all menu.popup(nil, nil, event.button, event.time) end } evt.signal_connect('button-release-event') { |this, event| if $gesture_press if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5 msg 3, "gesture delete: click-drag right button to the bottom" w.destroy closures[:delete].call $statusbar.push(0, utf8(_("Mouse gesture: delete."))) end end } tooltips = Gtk::Tooltips.new tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil) w.signal_connect('key-press-event') { |w,event| if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete w.destroy closures[:delete].call end } bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE)) b.signal_connect('clicked') { w.destroy } vb = Gtk::VBox.new vb.pack_start(evt, false, false) vb.pack_end(bottom, false, false) w.add(vb) w.signal_connect('delete-event') { w.destroy } w.window_position = Gtk::Window::POS_CENTER w.show_all end def scroll_upper(scrolledwindow, ypos_top) newval = scrolledwindow.vadjustment.value - ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment if newval < scrolledwindow.vadjustment.lower newval = scrolledwindow.vadjustment.lower end scrolledwindow.vadjustment.value = newval end def scroll_lower(scrolledwindow, ypos_bottom) newval = scrolledwindow.vadjustment.value + ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size end scrolledwindow.vadjustment.value = newval end def autoscroll_if_needed(scrolledwindow, image, textview) #- autoscroll if cursor or image is not visible, if possible if image && image.window || textview.window ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1] ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1) current_miny_visible = scrolledwindow.vadjustment.value current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size if ypos_top < current_miny_visible scroll_upper(scrolledwindow, ypos_top) elsif ypos_bottom > current_maxy_visible scroll_lower(scrolledwindow, ypos_bottom) end end end def create_editzone(scrolledwindow, pagenum, image) frame = Gtk::Frame.new frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD)) frame.set_shadow_type(Gtk::SHADOW_IN) textview.signal_connect('key-press-event') { |w, event| textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab) if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down scrolledwindow.signal_emit('key-press-event', event) end if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) && event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0 if event.keyval == Gdk::Keyval::GDK_Up if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment else scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower end else if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment else scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size end end end false #- propagate } textview.signal_connect('focus-in-event') { |w, event| textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1)) false #- propagate } candidate_undo_text = nil textview.signal_connect('focus-in-event') { |w, event| candidate_undo_text = textview.buffer.text false #- propagate } textview.signal_connect('key-release-event') { |w, event| if candidate_undo_text && candidate_undo_text != textview.buffer.text $modified = true save_undo(_("text edit"), Proc.new { |text| save_text = textview.buffer.text textview.buffer.text = text textview.grab_focus $notebook.set_page(pagenum) Proc.new { textview.buffer.text = save_text textview.grab_focus $notebook.set_page(pagenum) } }, candidate_undo_text) candidate_undo_text = nil end 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) autoscroll_if_needed(scrolledwindow, image, textview) end false #- propagate } return [ frame, textview ] end def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y) if !$modified_pixbufs[thumbnail_img] $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf } elsif !$modified_pixbufs[thumbnail_img][:orig] $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf end pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup #- rotate if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig]) msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}" if pixbuf.height > desired_y pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR) elsif pixbuf.width < desired_x && pixbuf.height < desired_y pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR) end end #- fix white balance if $modified_pixbufs[thumbnail_img][:whitebalance] pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance]) end img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf end def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y) $modified = true #- update rotate attribute xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360) $modified_pixbufs[thumbnail_img] ||= {} $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}" update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y) end def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y) $modified = true rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y) save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"), Proc.new { |angle| rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y) $notebook.set_page(attributes_prefix != '' ? 0 : 1) Proc.new { rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y) $notebook.set_page(0) $notebook.set_page(attributes_prefix != '' ? 0 : 1) } }, -angle) end def color_swap(xmldir, attributes_prefix) $modified = true if xmldir.attributes["#{attributes_prefix}color-swap"] xmldir.delete_attribute("#{attributes_prefix}color-swap") else xmldir.add_attribute("#{attributes_prefix}color-swap", '1') end end def enhance(xmldir, attributes_prefix) $modified = true if xmldir.attributes["#{attributes_prefix}enhance"] xmldir.delete_attribute("#{attributes_prefix}enhance") else xmldir.add_attribute("#{attributes_prefix}enhance", '1') end end def change_frame_offset(xmldir, attributes_prefix, value) $modified = true xmldir.add_attribute("#{attributes_prefix}frame-offset", value) end def ask_new_frame_offset(xmldir, attributes_prefix) if xmldir value = xmldir.attributes["#{attributes_prefix}frame-offset"] else value = '' end dialog = Gtk::Dialog.new(utf8(_("Change frame offset")), $main_window, Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT, [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL]) lbl = Gtk::Label.new lbl.markup = utf8( _("Please specify the frame offset of the video, to take the thumbnail from. There are approximately 25 frames per second in a video. ")) dialog.vbox.add(lbl) dialog.vbox.add(entry = Gtk::Entry.new.set_text(value)) entry.signal_connect('key-press-event') { |w, event| if event.keyval == Gdk::Keyval::GDK_Return dialog.response(Gtk::Dialog::RESPONSE_OK) true elsif event.keyval == Gdk::Keyval::GDK_Escape dialog.response(Gtk::Dialog::RESPONSE_CANCEL) true else false #- propagate if needed end } dialog.window_position = Gtk::Window::POS_MOUSE dialog.show_all dialog.run { |response| newval = entry.text dialog.destroy if response == Gtk::Dialog::RESPONSE_OK $modified = true msg 3, "changing frame offset to #{newval}" return { :old => value, :new => newval } else return nil end } end def change_whitebalance(xmlelem, attributes_prefix, value) $modified = true xmlelem.add_attribute("#{attributes_prefix}white-balance", value) end def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype) #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"] save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"] xmlelem.delete_attribute("#{attributes_prefix}white-balance") destfile = "#{thumbnail_img}-orig-whitebalance.jpg" gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile, xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype) $modified_pixbufs[thumbnail_img] ||= {} $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile) xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance) $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0 end $modified_pixbufs[thumbnail_img] ||= {} $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y) end def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype) #- init $modified_pixbufs correctly # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y) value = xmlelem.attributes["#{attributes_prefix}white-balance"] || "0" dialog = Gtk::Dialog.new(utf8(_("Fix white balance")), $main_window, Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT, [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL]) lbl = Gtk::Label.new lbl.markup = utf8( _("You can fix the white balance of the image, if your image is too blue or too yellow because your camera didn't detect the light correctly. Drag the slider below the image to the left for more blue, to the right for more yellow. ")) dialog.vbox.add(lbl) dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf))) dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i)) dialog.window_position = Gtk::Window::POS_MOUSE dialog.show_all lastval = nil timeout = Gtk.timeout_add(100) { if hs.value != lastval lastval = hs.value recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype) end true } dialog.run { |response| Gtk.timeout_remove(timeout) if response == Gtk::Dialog::RESPONSE_OK $modified = true newval = hs.value.to_s msg 3, "changing white balance to #{newval}" dialog.destroy return { :old => value, :new => newval } else $modified_pixbufs[thumbnail_img] ||= {} $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf dialog.destroy return nil end } end def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype) system("rm -f '#{destfile}'") #- type can be 'element' or 'subdir' if type == 'element' gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ]) else gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype) end end def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype) Thread.new { push_mousecursor_wait gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype) gtk_thread_protect { img.set(destfile) $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 } } pop_mousecursor } end def popup_thumbnail_menu(event, optionals, type, xmldir, attributes_prefix, possible_actions, closures) distribute_multiple_call = Proc.new { |action, arg| $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] } if possible_actions[:can_multiple] && $selected_elements.length > 0 UndoHandler.begin_batch $selected_elements.each_key { |k| $name2closures[k][action].call(arg) } UndoHandler.end_batch else closures[action].call(arg) end $selected_elements = {} } menu = Gtk::Menu.new if optionals.include?('change_image') menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image")))) changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png") changeimg.signal_connect('activate') { closures[:change].call } menu.append( Gtk::SeparatorMenuItem.new) end menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise")))) r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png") r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) } menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise")))) r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png") r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) } if !possible_actions[:can_multiple] || $selected_elements.length == 0 menu.append( Gtk::SeparatorMenuItem.new) if !possible_actions[:forbid_left] menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left")))) moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png") moveleft.signal_connect('activate') { closures[:move].call('left') } if !possible_actions[:can_left] moveleft.sensitive = false end end if !possible_actions[:forbid_right] menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right")))) moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png") moveright.signal_connect('activate') { closures[:move].call('right') } if !possible_actions[:can_right] moveright.sensitive = false end end menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up")))) moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png") moveup.signal_connect('activate') { closures[:move].call('up') } if !possible_actions[:can_up] moveup.sensitive = false end menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down")))) movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png") movedown.signal_connect('activate') { closures[:move].call('down') } if !possible_actions[:can_down] movedown.sensitive = false end end if type == 'video' if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty? menu.append( Gtk::SeparatorMenuItem.new) menu.append( color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap")))) color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png") color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) } menu.append( flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down")))) flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png") flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) } menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset")))) frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png") frame_offset.signal_connect('activate') { if possible_actions[:can_multiple] && $selected_elements.length > 0 if values = ask_new_frame_offset(nil, '') distribute_multiple_call.call(:frame_offset, values) end else closures[:frame_offset].call end } end end menu.append( Gtk::SeparatorMenuItem.new) if !possible_actions[:can_multiple] || $selected_elements.length == 0 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance")))) whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png") whitebalance.signal_connect('activate') { closures[:whitebalance].call } end if !possible_actions[:can_multiple] || $selected_elements.length == 0 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") : _("Enhance constrast")))) else menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement")))) end enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png") enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) } if optionals.include?('delete') menu.append( Gtk::SeparatorMenuItem.new) menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT)) cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) } if !possible_actions[:can_multiple] || $selected_elements.length == 0 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE)) paste_item.signal_connect('activate') { closures[:paste].call } menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR)) clear_item.signal_connect('activate') { $cuts = [] } if $cuts.size == 0 paste_item.sensitive = clear_item.sensitive = false end end menu.append( Gtk::SeparatorMenuItem.new) menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE)) delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) } end menu.show_all menu.popup(nil, nil, event.button, event.time) end def add_thumbnail(autotable, filename, type, thumbnail_img, caption) img = nil frame1 = Gtk::Frame.new my_gen_real_thumbnail = proc { gen_real_thumbnail('element', from_utf8("#{$current_path}/#{filename}"), thumbnail_img, $xmldir, $default_size['thumbnails'], img, '') } #- generate the thumbnail if missing (if image was rotated but booh was not relaunched) if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img) frame1.add(img = Gtk::Image.new) my_gen_real_thumbnail.call else frame1.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)) end evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT))) tooltips = Gtk::Tooltips.new tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, '')) tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(from_utf8("#{$current_path}/#{filename}"))/1024)]) : tipname), nil) frame2, textview = create_editzone($autotable_sw, 1, img) textview.buffer.text = utf8(caption) textview.set_justification(Gtk::Justification::CENTER) vbox = Gtk::VBox.new(false, 5) vbox.pack_start(evtbox, false, false) vbox.pack_start(frame2, false, false) autotable.append(vbox, filename) #- to be able to grab focus of textview when retrieving vbox's position from AutoTable $vbox2widgets[vbox] = { :textview => textview, :image => img } #- to be able to find widgets by name $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type } cleanup_all_thumbnails = Proc.new { #- remove out of sync images dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') for sizeobj in $images_size system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg") end } rotate_and_cleanup = Proc.new { |angle| rotate(angle, thumbnail_img, img, $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y]) cleanup_all_thumbnails.call } move = Proc.new { |direction| do_method = "move_#{direction}" undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end perform = Proc.new { done = autotable.method(do_method).call(vbox) textview.grab_focus #- because if moving, focus is stolen done } if perform.call save_undo(_("move %s") % direction, Proc.new { autotable.method(undo_method).call(vbox) textview.grab_focus #- because if moving, focus is stolen autoscroll_if_needed($autotable_sw, img, textview) $notebook.set_page(1) Proc.new { autotable.method(do_method).call(vbox) textview.grab_focus #- because if moving, focus is stolen autoscroll_if_needed($autotable_sw, img, textview) $notebook.set_page(1) } }) end } color_swap_and_cleanup = Proc.new { perform_color_swap_and_cleanup = Proc.new { color_swap($xmldir.elements["[@filename='#{filename}']"], '') my_gen_real_thumbnail.call } cleanup_all_thumbnails.call perform_color_swap_and_cleanup.call save_undo(_("color swap"), Proc.new { perform_color_swap_and_cleanup.call textview.grab_focus autoscroll_if_needed($autotable_sw, img, textview) $notebook.set_page(1) Proc.new { perform_color_swap_and_cleanup.call textview.grab_focus autoscroll_if_needed($autotable_sw, img, textview) $notebook.set_page(1) } }) } change_frame_offset_and_cleanup_real = Proc.new { |values| perform_change_frame_offset_and_cleanup = Proc.new { |val| change_frame_offset($xmldir.elements["[@filename='#{filename}']"], '', val) my_gen_real_thumbnail.call } perform_change_frame_offset_and_cleanup.call(values[:new]) save_undo(_("specify frame offset"), Proc.new { perform_change_frame_offset_and_cleanup.call(values[:old]) textview.grab_focus autoscroll_if_needed($autotable_sw, img, textview) $notebook.set_page(1) Proc.new { perform_change_frame_offset_and_cleanup.call(values[:new]) textview.grab_focus autoscroll_if_needed($autotable_sw, img, textview) $notebook.set_page(1) } }) } change_frame_offset_and_cleanup = Proc.new { if values = ask_new_frame_offset($xmldir.elements["[@filename='#{filename}']"], '') change_frame_offset_and_cleanup_real.call(values) end } whitebalance_and_cleanup = Proc.new { if values = ask_whitebalance(from_utf8("#{$current_path}/#{filename}"), thumbnail_img, img, $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '') perform_change_whitebalance_and_cleanup = Proc.new { |val| change_whitebalance($xmldir.elements["[@filename='#{filename}']"], '', val) recalc_whitebalance(val, from_utf8("#{$current_path}/#{filename}"), thumbnail_img, img, $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '') cleanup_all_thumbnails.call } perform_change_whitebalance_and_cleanup.call(values[:new]) save_undo(_("fix white balance"), Proc.new { perform_change_whitebalance_and_cleanup.call(values[:old]) textview.grab_focus autoscroll_if_needed($autotable_sw, img, textview) $notebook.set_page(1) Proc.new { perform_change_whitebalance_and_cleanup.call(values[:new]) textview.grab_focus autoscroll_if_needed($autotable_sw, img, textview) $notebook.set_page(1) } }) end } enhance_and_cleanup = Proc.new { perform_enhance_and_cleanup = Proc.new { enhance($xmldir.elements["[@filename='#{filename}']"], '') my_gen_real_thumbnail.call } cleanup_all_thumbnails.call perform_enhance_and_cleanup.call save_undo(_("enhance"), Proc.new { perform_enhance_and_cleanup.call textview.grab_focus autoscroll_if_needed($autotable_sw, img, textview) $notebook.set_page(1) Proc.new { perform_enhance_and_cleanup.call textview.grab_focus autoscroll_if_needed($autotable_sw, img, textview) $notebook.set_page(1) } }) } delete = Proc.new { if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed?")), { :okcancel => true }) $modified = true after = nil perform_delete = Proc.new { after = autotable.get_next_widget(vbox) if !after after = autotable.get_previous_widget(vbox) end autotable.remove(vbox) if after $vbox2widgets[after][:textview].grab_focus autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview]) end } previous_pos = autotable.get_current_number(vbox) perform_delete.call if !after if $xmldir.child_byname_notattr('dir', 'deleted') $xmldir.delete_attribute('thumbnails-caption') $xmldir.delete_attribute('thumbnails-captionfile') else $xmldir.add_attribute('deleted', 'true') moveup = $xmldir while moveup.parent.name == 'dir' moveup = moveup.parent if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted') moveup.add_attribute('deleted', 'true') else break end end end save_changes('forced') populate_subalbums_treeview else save_undo(_("delete"), Proc.new { |pos| autotable.reinsert(pos, vbox, filename) $notebook.set_page(1) autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) } $cuts = [] Proc.new { perform_delete.call $notebook.set_page(1) } }, previous_pos) end end } cut = Proc.new { delete.call $cuts << { :vbox => vbox, :filename => filename } $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size )) } paste = Proc.new { if $cuts.size > 0 $cuts.each { |elem| autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename]) } last = $cuts[-1] autotable.queue_draws << proc { $vbox2widgets[last[:vbox]][:textview].grab_focus autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview]) } save_undo(_("paste"), Proc.new { |cuts| cuts.each { |elem| autotable.remove(elem[:vbox]) } $notebook.set_page(1) Proc.new { cuts.each { |elem| autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename]) } $notebook.set_page(1) } }, $cuts) $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size )) $cuts = [] end } $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut, :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real } textview.signal_connect('key-press-event') { |w, event| propagate = true if event.state != 0 x, y = autotable.get_current_pos(vbox) control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0 if event.keyval == Gdk::Keyval::GDK_Up && y > 0 if control_pressed if widget_up = autotable.get_widget_at_pos(x, y - 1) $vbox2widgets[widget_up][:textview].grab_focus end end if shift_pressed move.call('up') end end if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y if control_pressed if widget_down = autotable.get_widget_at_pos(x, y + 1) $vbox2widgets[widget_down][:textview].grab_focus end end if shift_pressed move.call('down') end end if event.keyval == Gdk::Keyval::GDK_Left if x > 0 if control_pressed $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus end if shift_pressed move.call('left') end end if alt_pressed rotate_and_cleanup.call(-90) end end if event.keyval == Gdk::Keyval::GDK_Right next_ = autotable.get_next_widget(vbox) if next_ && autotable.get_current_pos(next_)[0] > x if control_pressed $vbox2widgets[next_][:textview].grab_focus end if shift_pressed move.call('right') end end if alt_pressed rotate_and_cleanup.call(90) end end if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed delete.call end if event.keyval == Gdk::Keyval::GDK_Return && control_pressed view_element(filename, { :delete => delete }) propagate = false end if event.keyval == Gdk::Keyval::GDK_z && control_pressed perform_undo end if event.keyval == Gdk::Keyval::GDK_r && control_pressed perform_redo end end !propagate #- propagate if needed } $ignore_next_release = false evtbox.signal_connect('button-press-event') { |w, event| if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1 if event.state & Gdk::Window::BUTTON3_MASK != 0 #- gesture redo: hold right mouse button then click left mouse button $config['nogestures'] or perform_redo $ignore_next_release = true else shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0 if $r90.active? rotate_and_cleanup.call(shift_or_control ? -90 : 90) elsif $r270.active? rotate_and_cleanup.call(shift_or_control ? 90 : -90) elsif $enhance.active? enhance_and_cleanup.call elsif $delete.active? delete.call else textview.grab_focus $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y } end end $ignore_for_multiple_selections = true elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3 if event.state & Gdk::Window::BUTTON1_MASK != 0 #- gesture undo: hold left mouse button then click right mouse button $config['nogestures'] or perform_undo $ignore_next_release = true end elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1 view_element(filename, { :delete => delete }) end false #- propagate } evtbox.signal_connect('button-release-event') { |w, event| if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3 if !$ignore_next_release x, y = autotable.get_current_pos(vbox) next_ = autotable.get_next_widget(vbox) popup_thumbnail_menu(event, ['delete'], type, $xmldir.elements["[@filename='#{filename}']"], '', { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x, :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true }, { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup, :cut => cut, :paste => paste }) end $ignore_next_release = false $gesture_press = nil end false #- propagate } #- handle reordering with drag and drop Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE) Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE) vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time| selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s) } vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time| done = false #- mouse gesture first (dnd disables button-release-event) if $gesture_press && $gesture_press[:filename] == filename if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5 angle = x-$gesture_press[:x] > 0 ? 90 : -90 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right" rotate_and_cleanup.call(angle) $statusbar.push(0, utf8(_("Mouse gesture: rotate."))) done = true elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5 msg 3, "gesture delete: click-drag right button to the bottom" delete.call $statusbar.push(0, utf8(_("Mouse gesture: delete."))) done = true end end if !done ctxt.targets.each { |target| if target.name == 'reorder-elements' move = Proc.new { |from,to| if from != to $modified = true autotable.move(from, to) save_undo(_("reorder"), Proc.new { |from, to| if to > from autotable.move(to - 1, from) else autotable.move(to, from + 1) end $notebook.set_page(1) Proc.new { autotable.move(from, to) $notebook.set_page(1) } }, from, to) end } if $multiple_dnd.size == 0 move.call(selection_data.data.to_i, autotable.get_current_number(vbox)) else UndoHandler.begin_batch $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }. each { |path| #- need to update current position between each call move.call(autotable.get_current_number($name2widgets[path][:vbox]), autotable.get_current_number(vbox)) } UndoHandler.end_batch end $multiple_dnd = [] end } end } vbox.show_all end def create_auto_table $autotable = Gtk::AutoTable.new(5) $autotable_sw = Gtk::ScrolledWindow.new(nil, nil) thumbnails_vb = Gtk::VBox.new(false, 5) frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil) $thumbnails_title.set_justification(Gtk::Justification::CENTER) thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false) thumbnails_vb.add($autotable) $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS) $autotable_sw.add_with_viewport(thumbnails_vb) #- follows stuff for handling multiple elements selection press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {} gc = nil update_selected = Proc.new { $autotable.current_order.each { |path| w = $name2widgets[path][:evtbox].window xm = w.position[0] + w.size[0]/2 ym = w.position[1] + w.size[1]/2 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path] $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf } $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true) end end if $selected_elements[path] && ! $selected_elements[path][:keep] 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)) $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] $selected_elements.delete(path) end end } } $autotable.signal_connect('realize') { |w,e| gc = Gdk::GC.new($autotable.window) gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND) gc.function = Gdk::GC::INVERT #- autoscroll handling for DND and multiple selections Gtk.timeout_add(100) { w, x, y, mask = $autotable.window.pointer if mask & Gdk::Window::BUTTON1_MASK != 0 if y < $autotable_sw.vadjustment.value if pos_x $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]]) end scroll_upper($autotable_sw, y) if not press_x.nil? w, pos_x, pos_y = $autotable.window.pointer $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]]) update_selected.call end end if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size if pos_x $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]]) end scroll_lower($autotable_sw, y) if not press_x.nil? w, pos_x, pos_y = $autotable.window.pointer $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]]) update_selected.call end end end true } } $autotable.signal_connect('button-press-event') { |w,e| if e.button == 1 if !$ignore_for_multiple_selections press_x = e.x press_y = e.y if e.state & Gdk::Window::SHIFT_MASK == 0 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] } $selected_elements = {} $statusbar.push(0, utf8(_("Nothing selected."))) else $selected_elements.each_key { |path| $selected_elements[path][:keep] = true } end set_mousecursor(Gdk::Cursor::TCROSS) end end } $autotable.signal_connect('button-release-event') { |w,e| if e.button == 1 if $ignore_for_multiple_selections #- unselect all only now $multiple_dnd = $selected_elements.keys $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] } $selected_elements = {} $ignore_for_multiple_selections = false else if pos_x $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]]) if $selected_elements.length > 0 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length)) end end press_x = press_y = pos_x = pos_y = nil set_mousecursor(Gdk::Cursor::LEFT_PTR) end end } $autotable.signal_connect('motion-notify-event') { |w,e| if ! press_x.nil? if pos_x $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]]) end pos_x = e.x pos_y = e.y $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]]) update_selected.call end } end def create_subalbums_page subalbums_hb = Gtk::HBox.new $subalbums_vb = Gtk::VBox.new(false, 5) subalbums_hb.pack_start($subalbums_vb, false, false) $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil) $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC) $subalbums_sw.add_with_viewport(subalbums_hb) end def save_current_file save_changes if $filename ios = File.open($filename, "w") $xmldoc.write(ios, 0) ios.close end end def save_current_file_user save_tempfilename = $filename $filename = $orig_filename save_current_file $modified = false $generated_outofline = false $filename = save_tempfilename end def mark_document_as_dirty $xmldoc.elements.each('//dir') { |elem| elem.delete_attribute('already-generated') } end #- ret: true => ok false => cancel def ask_save_modifications(msg1, msg2, *options) ret = true options = options.size > 0 ? options[0] : {} if $modified if options[:disallow_cancel] dialog = Gtk::Dialog.new(msg1, $main_window, Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT, [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO], [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES]) else dialog = Gtk::Dialog.new(msg1, $main_window, Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT, [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL], [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO], [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES]) end dialog.default_response = Gtk::Dialog::RESPONSE_YES dialog.vbox.add(Gtk::Label.new(msg2)) dialog.window_position = Gtk::Window::POS_CENTER dialog.show_all dialog.run { |response| dialog.destroy if response == Gtk::Dialog::RESPONSE_YES save_current_file_user else #- if we have generated an album but won't save modifications, we must remove #- already-generated markers in original file if $generated_outofline begin $xmldoc = REXML::Document.new File.new($orig_filename) mark_document_as_dirty ios = File.open($orig_filename, "w") $xmldoc.write(ios, 0) ios.close rescue Exception puts "exception: #{$!}" end end end if response == Gtk::Dialog::RESPONSE_CANCEL ret = false end } end return ret end def try_quit(*options) if ask_save_modifications(utf8(_("Save before quitting?")), utf8(_("Do you want to save your changes before quitting?")), *options) Gtk.main_quit end end def show_popup(parent, msg, *options) dialog = Gtk::Dialog.new dialog.title = utf8(_("Booh message")) lbl = Gtk::Label.new lbl.markup = msg if options[0] && options[0][:centered] lbl.set_justify(Gtk::Justification::CENTER) end if options[0] && options[0][:topwidget] dialog.vbox.add(options[0][:topwidget]) end dialog.vbox.add(lbl) if options[0] && options[0][:okcancel] dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL) end dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK) dialog.set_default_size(200, 120) if options[0] && options[0][:pos_centered] dialog.window_position = Gtk::Window::POS_CENTER else dialog.window_position = Gtk::Window::POS_MOUSE end dialog.show_all if !options[0] || !options[0][:not_transient] dialog.transient_for = parent dialog.run { |response| dialog.destroy if options[0] && options[0][:okcancel] return response == Gtk::Dialog::RESPONSE_OK end } else dialog.signal_connect('response') { dialog.destroy } end end def backend_wait_message(parent, msg, infopipe_path, mode) w = Gtk::Window.new w.set_transient_for(parent) w.modal = true vb = Gtk::VBox.new(false, 5).set_border_width(5) vb.pack_start(Gtk::Label.new(msg), false, false) vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5))) vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false) if mode != 'one dir scan' vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false) end if mode == 'web-album' vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5))) vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false) end vb.pack_start(Gtk::HSeparator.new, false, false) bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort")))) b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png") vb.pack_end(bottom, false, false) infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK) refresh_thread = Thread.new { directories_counter = 0 while line = infopipe.gets if line =~ /^directories: (\d+), sizes: (\d+)/ directories = $1.to_f + 1 sizes = $2.to_f elsif line =~ /^walking: (.+), (\d+) elements$/ elements = $2.to_f + 1 if mode == 'web-album' elements += sizes end element_counter = 0 gtk_thread_protect { pb1_1.fraction = 0 } if mode != 'one dir scan' newtext = utf8(full_src_dir_to_rel($1)) newtext = '/' if newtext == '' gtk_thread_protect { pb1_2.text = newtext } directories_counter += 1 gtk_thread_protect { pb1_2.fraction = directories_counter / directories } end elsif line =~ /^processing element$/ element_counter += 1 gtk_thread_protect { pb1_1.fraction = element_counter / elements } elsif line =~ /^processing size$/ element_counter += 1 gtk_thread_protect { pb1_1.fraction = element_counter / elements } elsif line =~ /^finished processing sizes$/ gtk_thread_protect { pb1_1.fraction = 1 } elsif line =~ /^creating index.html$/ gtk_thread_protect { pb1_2.text = utf8(_("finished")) } gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 } directories_counter = 0 elsif line =~ /^index.html: (.+)/ newtext = utf8(full_src_dir_to_rel($1)) newtext = '/' if newtext == '' gtk_thread_protect { pb2.text = newtext } directories_counter += 1 gtk_thread_protect { pb2.fraction = directories_counter / directories } end end } w.add(vb) w.signal_connect('delete-event') { w.destroy } w.signal_connect('destroy') { Thread.kill(refresh_thread) gtk_thread_abandon #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls if infopipe_path infopipe.close system("rm -f #{infopipe_path}") end } w.window_position = Gtk::Window::POS_CENTER w.show_all return [ b, w ] end def call_backend(cmd, waitmsg, mode, params) pipe = Tempfile.new("boohpipe") pipe.close! system("mkfifo #{pipe.path}") cmd += " --info-pipe #{pipe.path}" button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode) pid = nil Thread.new { msg 2, cmd if pid = fork id, exitstatus = Process.waitpid2(pid) gtk_thread_protect { w8.destroy } if exitstatus == 0 if params[:successmsg] gtk_thread_protect { show_popup($main_window, params[:successmsg]) } end if params[:closure_after] gtk_thread_protect(¶ms[:closure_after]) end elsif exitstatus == 15 #- say nothing, user aborted else if params[:failuremsg] gtk_thread_protect { show_popup($main_window, params[:failuremsg]) } end end else exec(cmd) end } button.signal_connect('clicked') { Process.kill('SIGTERM', pid) } end def save_changes(*forced) if forced.empty? && (!$current_path || !$undo_tb.sensitive?) return end $xmldir.delete_attribute('already-generated') propagate_children = Proc.new { |xmldir| if xmldir.attributes['subdirs-caption'] xmldir.delete_attribute('already-generated') end xmldir.elements.each('dir') { |element| propagate_children.call(element) } } if $xmldir.child_byname_notattr('dir', 'deleted') new_title = $subalbums_title.buffer.text if new_title != $xmldir.attributes['subdirs-caption'] parent = $xmldir.parent if parent.name == 'dir' parent.delete_attribute('already-generated') end propagate_children.call($xmldir) end $xmldir.add_attribute('subdirs-caption', new_title) $xmldir.elements.each('dir') { |element| if !element.attributes['deleted'] path = element.attributes['path'] newtext = $subalbums_edits[path][:editzone].buffer.text if element.attributes['subdirs-caption'] if element.attributes['subdirs-caption'] != newtext propagate_children.call(element) end element.add_attribute('subdirs-caption', newtext) element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile])) else if element.attributes['thumbnails-caption'] != newtext element.delete_attribute('already-generated') end element.add_attribute('thumbnails-caption', newtext) element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile])) end end } end if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted') if $xmldir.attributes['thumbnails-caption'] path = $xmldir.attributes['path'] $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text) end elsif $xmldir.attributes['thumbnails-caption'] $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text) end #- remove and reinsert elements to reflect new ordering saves = {} cpt = 0 $xmldir.elements.each { |element| if element.name == 'image' || element.name == 'video' saves[element.attributes['filename']] = element.remove cpt += 1 end } $autotable.current_order.each { |path| chld = $xmldir.add_element(saves[path].name, saves[path].attributes) chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text) saves.delete(path) } saves.each_key { |path| chld = $xmldir.add_element(saves[path].name, saves[path].attributes) chld.add_attribute('deleted', 'true') } end def remove_all_captions $modified = true texts = {} $autotable.current_order.each { |path| texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text $name2widgets[File.basename(path)][:textview].buffer.text = '' } save_undo(_("remove all captions"), Proc.new { |texts| texts.each_key { |key| $name2widgets[key][:textview].buffer.text = texts[key] } $notebook.set_page(1) Proc.new { texts.each_key { |key| $name2widgets[key][:textview].buffer.text = '' } $notebook.set_page(1) } }, texts) end def change_dir $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] } $autotable.clear $vbox2widgets = {} $name2widgets = {} $name2closures = {} $selected_elements = {} $cuts = [] $multiple_dnd = [] UndoHandler.cleanup $undo_tb.sensitive = $undo_mb.sensitive = false $redo_tb.sensitive = $redo_mb.sensitive = false if !$current_path return end $subalbums_vb.children.each { |chld| $subalbums_vb.remove(chld) } $subalbums = Gtk::Table.new(0, 0, true) current_y_sub_albums = 0 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"] $subalbums_edits = {} subalbums_counter = 0 subalbums_edits_bypos = {} add_subalbum = Proc.new { |xmldir, counter| $subalbums_edits[xmldir.attributes['path']] = { :position => counter } subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']] if xmldir == $xmldir thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg" caption = xmldir.attributes['thumbnails-caption'] captionfile, dummy = find_subalbum_caption_info(xmldir) infotype = 'thumbnails' else thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg" captionfile, caption = find_subalbum_caption_info(xmldir) infotype = find_subalbum_info_type(xmldir) end msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}" hbox = Gtk::HBox.new hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('' + File.basename(xmldir.attributes['path']) + ':'))) f = Gtk::Frame.new f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT) img = nil my_gen_real_thumbnail = proc { gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype) } if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file) f.add(img = Gtk::Image.new) my_gen_real_thumbnail.call else f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file)) end hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false) $subalbums.attach(hbox, 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2) frame, textview = create_editzone($subalbums_sw, 0, img) textview.buffer.text = caption $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame), 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2) change_image = Proc.new { fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")), nil, Gtk::FileChooser::ACTION_OPEN, nil, [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL]) fc.set_current_folder(from_utf8(xmldir.attributes['path'])) fc.transient_for = $main_window 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)) f.add(preview_img = Gtk::Image.new) preview.show_all fc.signal_connect('update-preview') { |w| begin if fc.preview_filename preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename)) fc.preview_widget_active = true end rescue Gdk::PixbufError fc.preview_widget_active = false end } if fc.run == Gtk::Dialog::RESPONSE_ACCEPT $modified = true old_file = captionfile old_rotate = xmldir.attributes["#{infotype}-rotate"] old_color_swap = xmldir.attributes["#{infotype}-color-swap"] old_enhance = xmldir.attributes["#{infotype}-enhance"] old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"] new_file = fc.filename msg 3, "new captionfile is: #{fc.filename}" perform_changefile = Proc.new { $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file $modified_pixbufs.delete(thumbnail_file) xmldir.delete_attribute("#{infotype}-rotate") xmldir.delete_attribute("#{infotype}-color-swap") xmldir.delete_attribute("#{infotype}-enhance") xmldir.delete_attribute("#{infotype}-frame-offset") my_gen_real_thumbnail.call } perform_changefile.call save_undo(_("change caption file for sub-album"), Proc.new { $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file xmldir.add_attribute("#{infotype}-rotate", old_rotate) xmldir.add_attribute("#{infotype}-color-swap", old_color_swap) xmldir.add_attribute("#{infotype}-enhance", old_enhance) xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset) my_gen_real_thumbnail.call $notebook.set_page(0) Proc.new { perform_changefile.call $notebook.set_page(0) } }) end fc.destroy } rotate_and_cleanup = Proc.new { |angle| rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y]) system("rm -f '#{thumbnail_file}'") } move = Proc.new { |direction| save_changes('forced') if direction == 'up' oldpos = $subalbums_edits[xmldir.attributes['path']][:position] $subalbums_edits[xmldir.attributes['path']][:position] -= 1 subalbums_edits_bypos[oldpos - 1][:position] += 1 else oldpos = $subalbums_edits[xmldir.attributes['path']][:position] $subalbums_edits[xmldir.attributes['path']][:position] += 1 subalbums_edits_bypos[oldpos + 1][:position] -= 1 end elems = [] $xmldir.elements.each('dir') { |element| if (!element.attributes['deleted']) elems << [ element.attributes['path'], element.remove ] end } elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }. each { |e| $xmldir.add_element(e[1]) } #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed $xmldir.elements.each('descendant::dir') { |elem| elem.delete_attribute('already-generated') } change_dir } color_swap_and_cleanup = Proc.new { perform_color_swap_and_cleanup = Proc.new { color_swap(xmldir, "#{infotype}-") my_gen_real_thumbnail.call } perform_color_swap_and_cleanup.call save_undo(_("color swap"), Proc.new { perform_color_swap_and_cleanup.call $notebook.set_page(0) Proc.new { perform_color_swap_and_cleanup.call $notebook.set_page(0) } }) } change_frame_offset_and_cleanup = Proc.new { if values = ask_new_frame_offset(xmldir, "#{infotype}-") perform_change_frame_offset_and_cleanup = Proc.new { |val| change_frame_offset(xmldir, "#{infotype}-", val) my_gen_real_thumbnail.call } perform_change_frame_offset_and_cleanup.call(values[:new]) save_undo(_("specify frame offset"), Proc.new { perform_change_frame_offset_and_cleanup.call(values[:old]) $notebook.set_page(0) Proc.new { perform_change_frame_offset_and_cleanup.call(values[:new]) $notebook.set_page(0) } }) end } whitebalance_and_cleanup = Proc.new { if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype) perform_change_whitebalance_and_cleanup = Proc.new { |val| change_whitebalance(xmldir, "#{infotype}-", val) recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype) system("rm -f '#{thumbnail_file}'") } perform_change_whitebalance_and_cleanup.call(values[:new]) save_undo(_("fix white balance"), Proc.new { perform_change_whitebalance_and_cleanup.call(values[:old]) $notebook.set_page(0) Proc.new { perform_change_whitebalance_and_cleanup.call(values[:new]) $notebook.set_page(0) } }) end } enhance_and_cleanup = Proc.new { perform_enhance_and_cleanup = Proc.new { enhance(xmldir, "#{infotype}-") my_gen_real_thumbnail.call } perform_enhance_and_cleanup.call save_undo(_("enhance"), Proc.new { perform_enhance_and_cleanup.call $notebook.set_page(0) Proc.new { perform_enhance_and_cleanup.call $notebook.set_page(0) } }) } evtbox.signal_connect('button-press-event') { |w, event| if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1 if $r90.active? rotate_and_cleanup.call(90) elsif $r270.active? rotate_and_cleanup.call(-90) elsif $enhance.active? enhance_and_cleanup.call end end if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3 popup_thumbnail_menu(event, ['change_image'], entry2type(captionfile), xmldir, "#{infotype}-", { :forbid_left => true, :forbid_right => true, :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter }, { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup }) end if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1 change_image.call true #- handled end } evtbox.signal_connect('button-press-event') { |w, event| $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y } false } evtbox.signal_connect('button-release-event') { |w, event| if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file msg 3, "press: #{$gesture_press[:x]} release: #{event.x}" if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90 msg 3, "gesture rotate: #{angle}" rotate_and_cleanup.call(angle) end end $gesture_press = nil } $subalbums_edits[xmldir.attributes['path']][:editzone] = textview $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile current_y_sub_albums += 1 } if $xmldir.child_byname_notattr('dir', 'deleted') #- title edition frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil) $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] $subalbums_title.set_justification(Gtk::Justification::CENTER) $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false) #- this album image/caption if $xmldir.attributes['thumbnails-caption'] add_subalbum.call($xmldir, 0) end end total = { 'image' => 0, 'video' => 0, 'dir' => 0 } $xmldir.elements.each { |element| if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted'] #- element (image or video) of this album dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg" msg 3, "dest_img: #{dest_img}" add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption'])) total[element.name] += 1 end if element.name == 'dir' && !element.attributes['deleted'] #- sub-album image/caption add_subalbum.call(element, subalbums_counter += 1) total[element.name] += 1 end } $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])), total['image'], total['video'], total['dir'] ])) $subalbums_vb.add($subalbums) $subalbums_vb.show_all if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted') $notebook.get_tab_label($autotable_sw).sensitive = false $notebook.set_page(0) $thumbnails_title.buffer.text = '' else $notebook.get_tab_label($autotable_sw).sensitive = true $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption'] end if !$xmldir.child_byname_notattr('dir', 'deleted') $notebook.get_tab_label($subalbums_sw).sensitive = false $notebook.set_page(1) else $notebook.get_tab_label($subalbums_sw).sensitive = true end end def pixbuf_or_nil(filename) begin return Gdk::Pixbuf.new(filename) rescue return nil end end def theme_choose(current) dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")), $main_window, Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT, [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL]) model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf) treeview = Gtk::TreeView.new(model).set_rules_hint(true) treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5)) treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5)) treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5)) treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5)) treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {})) treeview.signal_connect('button-press-event') { |w, event| if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1 dialog.response(Gtk::Dialog::RESPONSE_OK) end } dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)) `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir| dir.chomp! iter = model.append iter[0] = File.basename(dir) iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png") iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png") iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png") if File.basename(dir) == current treeview.selection.select_iter(iter) end } dialog.set_default_size(700, 400) dialog.vbox.show_all dialog.run { |response| iter = treeview.selection.selected dialog.destroy if response == Gtk::Dialog::RESPONSE_OK && iter return model.get_value(iter, 0) end } return nil end def populate_subalbums_treeview $albums_ts.clear $autotable.clear $subalbums_vb.children.each { |chld| $subalbums_vb.remove(chld) } source = $xmldoc.root.attributes['source'] msg 3, "source: #{source}" xmldir = $xmldoc.elements['//dir'] if !xmldir || xmldir.attributes['path'] != source msg 1, _("Corrupted booh file...") return end append_dir_elem = Proc.new { |parent_iter, xmldir| child_iter = $albums_ts.append(parent_iter) child_iter[0] = File.basename(xmldir.attributes['path']) child_iter[1] = xmldir.attributes['path'] msg 3, "puttin location: #{xmldir.attributes['path']}" xmldir.elements.each('dir') { |elem| if !elem.attributes['deleted'] append_dir_elem.call(child_iter, elem) end } } append_dir_elem.call(nil, xmldir) $albums_tv.expand_all $albums_tv.selection.select_iter($albums_ts.iter_first) end def open_file(filename) $filename = nil $modified = false $current_path = nil #- invalidate $modified_pixbufs = {} $albums_ts.clear $autotable.clear $subalbums_vb.children.each { |chld| $subalbums_vb.remove(chld) } if !File.exists?(filename) return utf8(_("File not found.")) end begin $xmldoc = REXML::Document.new File.new(filename) rescue Exception $xmldoc = nil end if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh' if entry2type(filename).nil? return utf8(_("Not a booh file!")) else 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.")) end end if !source = $xmldoc.root.attributes['source'] return utf8(_("Corrupted booh file...")) end if !dest = $xmldoc.root.attributes['destination'] return utf8(_("Corrupted booh file...")) end if !theme = $xmldoc.root.attributes['theme'] return utf8(_("Corrupted booh file...")) end if $xmldoc.root.attributes['version'] != $VERSION msg 2, _("File's version %s, booh version now #{$VERSION}, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ] mark_document_as_dirty $xmldoc.root.add_attribute('version', $VERSION) end limit_sizes = $xmldoc.root.attributes['limit-sizes'] optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil? nperrow = $xmldoc.root.attributes['thumbnails-per-row'] $filename = filename select_theme(theme, limit_sizes, optimizefor32, nperrow) $default_size['thumbnails'] =~ /(.*)x(.*)/ $default_thumbnails = { :x => $1.to_i, :y => $2.to_i } $albums_thumbnail_size =~ /(.*)x(.*)/ $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i } populate_subalbums_treeview $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge.sensitive = $generate.sensitive = $properties.sensitive = $remove_all_captions.sensitive = true return nil end def open_file_user(filename) result = open_file(filename) if !result $config['last-opens'] ||= [] if $config['last-opens'][-1] != utf8(filename) $config['last-opens'] << utf8(filename) end $orig_filename = $filename tmp = Tempfile.new("boohtemp") tmp.close! #- for security ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL) ios.close $tempfiles << $filename << "#{$filename}.backup" else $orig_filename = nil end return result end def open_file_popup if !ask_save_modifications(utf8(_("Save this album?")), utf8(_("Do you want to save the changes to this album?")), { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO }) return end fc = Gtk::FileChooserDialog.new(utf8(_("Open file")), nil, Gtk::FileChooser::ACTION_OPEN, nil, [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL]) fc.add_shortcut_folder(File.expand_path("~/.booh")) fc.set_current_folder(File.expand_path("~/.booh")) fc.transient_for = $main_window ok = false while !ok if fc.run == Gtk::Dialog::RESPONSE_ACCEPT push_mousecursor_wait(fc) msg = open_file_user(fc.filename) pop_mousecursor(fc) if msg show_popup(fc, msg) ok = false else ok = true end else ok = true end end fc.destroy end def additional_booh_options options = '' if $config['mproc'] options += "--mproc #{$config['mproc'].to_i} " end if $config['emptycomments'] options += "--empty-comments " end return options end def new_album if !ask_save_modifications(utf8(_("Save this album?")), utf8(_("Do you want to save the changes to this album?")), { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO }) return end dialog = Gtk::Dialog.new(utf8(_("Create a new album")), $main_window, Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT, [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL]) frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false)) tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))), 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2) tbl.attach(src = Gtk::Entry.new, 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2) tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))), 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2) tbl.attach(Gtk::Label.new.set_markup(utf8(_("number of images/videos down this directory: "))), 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2) tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("N/A"))), 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2) tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))), 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2) tbl.attach(dest = Gtk::Entry.new, 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2) tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))), 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2) tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))), 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2) tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1), 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2) tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))), 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2) tooltips = Gtk::Tooltips.new frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new) vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0). pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0)) vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0). pack_start(sizes = Gtk::HBox.new, false, false, 0)) vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio")))) 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) vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0). pack_start(nperrowradios = Gtk::HBox.new, false, false, 0)) src_nb_calculated_for = '' src_nb_thread = nil process_src_nb = Proc.new { if src.text != src_nb_calculated_for src_nb_calculated_for = src.text if src_nb_thread Thread.kill(src_nb_thread) src_nb_thread = nil end if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/' if File.readable?(from_utf8(src_nb_calculated_for)) src_nb_thread = Thread.new { gtk_thread_protect { src_nb.set_markup(utf8(_("processing..."))) } total = { 'image' => 0, 'video' => 0, nil => 0 } `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir| if File.basename(dir) =~ /^\./ next else begin Dir.entries(dir.chomp).each { |file| total[entry2type(file)] += 1 } rescue Errno::EACCES, Errno::ENOENT end end } gtk_thread_protect { src_nb.set_markup(utf8(_("%s images and %s videos") % [ total['image'], total['video'] ])) } src_nb_thread = nil } else src_nb.set_markup(utf8(_("permission denied"))) end else src_nb.set_markup(utf8(_("N/A"))) end end true } timeout_src_nb = Gtk.timeout_add(100) { process_src_nb.call } src_browse.signal_connect('clicked') { fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")), nil, Gtk::FileChooser::ACTION_SELECT_FOLDER, nil, [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL]) fc.transient_for = $main_window if fc.run == Gtk::Dialog::RESPONSE_ACCEPT src.text = utf8(fc.filename) process_src_nb.call conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}") end fc.destroy } dest_browse.signal_connect('clicked') { fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")), nil, Gtk::FileChooser::ACTION_CREATE_FOLDER, nil, [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL]) fc.transient_for = $main_window if fc.run == Gtk::Dialog::RESPONSE_ACCEPT dest.text = utf8(fc.filename) end fc.destroy } conf_browse.signal_connect('clicked') { fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")), nil, Gtk::FileChooser::ACTION_SAVE, nil, [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL]) fc.transient_for = $main_window fc.add_shortcut_folder(File.expand_path("~/.booh")) fc.set_current_folder(File.expand_path("~/.booh")) if fc.run == Gtk::Dialog::RESPONSE_ACCEPT conf.text = utf8(fc.filename) end fc.destroy } theme_sizes = [] nperrows = [] recreate_theme_config = proc { theme_sizes.each { |e| sizes.remove(e[:widget]) } theme_sizes = [] select_theme(theme_button.label, 'all', optimize432.active?, nil) $images_size.each { |s| sizes.add(cb = Gtk::CheckButton.new(sizename(s['name']))) if !s['optional'] cb.active = true end tooltips.set_tip(cb, utf8(s['description']), nil) theme_sizes << { :widget => cb, :value => s['name'] } } sizes.add(cb = Gtk::CheckButton.new(utf8(_('original')))) tooltips = Gtk::Tooltips.new tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil) theme_sizes << { :widget => cb, :value => 'original' } sizes.show_all nperrows.each { |e| nperrowradios.remove(e[:widget]) } nperrow_group = nil nperrows = [] $allowed_N_values.each { |n| if nperrow_group nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false)) else nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s)) end if $default_N == n rb.active = true end nperrows << { :widget => rb, :value => n } } nperrowradios.show_all } recreate_theme_config.call theme_button.signal_connect('clicked') { if newtheme = theme_choose(theme_button.label) theme_button.label = newtheme recreate_theme_config.call end } dialog.vbox.add(frame1) dialog.vbox.add(frame2) dialog.window_position = Gtk::Window::POS_MOUSE dialog.show_all keepon = true ok = true while keepon dialog.run { |response| if response == Gtk::Dialog::RESPONSE_OK srcdir = from_utf8(src.text) destdir = from_utf8(dest.text) if !File.directory?(srcdir) show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input."))) src.grab_focus elsif conf.text == '' show_popup(dialog, utf8(_("Please specify a filename to store the album's properties."))) conf.grab_focus elsif destdir != make_dest_filename(destdir) show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters."))) dest.grab_focus elsif File.directory?(destdir) keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true }) dest.grab_focus elsif File.exists?(destdir) show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one."))) dest.grab_focus elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? } show_popup(dialog, utf8(_("You need to select at least one size (not counting original)."))) else system("mkdir '#{destdir}'") if !File.directory?(destdir) show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?"))) dest.grab_focus else keepon = false end end else keepon = ok = false end } end srcdir = from_utf8(src.text) destdir = from_utf8(dest.text) configskel = File.expand_path(from_utf8(conf.text)) theme = theme_button.label sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',') nperrow = nperrows.find { |e| e[:widget].active? }[:value] opt432 = optimize432.active? if src_nb_thread Thread.kill(src_nb_thread) gtk_thread_abandon #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls end dialog.destroy Gtk.timeout_remove(timeout_src_nb) if ok call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " + "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " + "#{opt432 ? '--optimize-for-32' : ''} #{additional_booh_options}", utf8(_("Please wait while scanning source directory...")), 'full scan', { :closure_after => proc { open_file_user(configskel) } }) end end def properties dialog = Gtk::Dialog.new(utf8(_("Properties of your album")), $main_window, Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT, [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL]) source = $xmldoc.root.attributes['source'] dest = $xmldoc.root.attributes['destination'] theme = $xmldoc.root.attributes['theme'] opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil? nperrow = $xmldoc.root.attributes['thumbnails-per-row'] limit_sizes = $xmldoc.root.attributes['limit-sizes'] if limit_sizes limit_sizes = limit_sizes.split(/,/) end tooltips = Gtk::Tooltips.new frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false)) tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))), 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2) tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('' + source + '')), 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2) tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))), 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2) tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('' + dest + '')), 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2) tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))), 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2) tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('' + $orig_filename + '')), 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2) frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new) vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0). pack_start(theme_button = Gtk::Button.new(theme), false, false, 0)) vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0). pack_start(sizes = Gtk::HBox.new, false, false, 0)) vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432)) 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) vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0). pack_start(nperrowradios = Gtk::HBox.new, false, false, 0)) theme_sizes = [] nperrows = [] recreate_theme_config = proc { theme_sizes.each { |e| sizes.remove(e[:widget]) } theme_sizes = [] select_theme(theme_button.label, 'all', optimize432.active?, nperrow) $images_size.each { |s| sizes.add(cb = Gtk::CheckButton.new(sizename(s['name']))) if limit_sizes if limit_sizes.include?(s['name']) cb.active = true end else if !s['optional'] cb.active = true end end tooltips.set_tip(cb, utf8(s['description']), nil) theme_sizes << { :widget => cb, :value => s['name'] } } sizes.add(cb = Gtk::CheckButton.new(utf8(_('original')))) tooltips = Gtk::Tooltips.new tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil) if limit_sizes && limit_sizes.include?('original') cb.active = true end theme_sizes << { :widget => cb, :value => 'original' } sizes.show_all nperrows.each { |e| nperrowradios.remove(e[:widget]) } nperrow_group = nil nperrows = [] $allowed_N_values.each { |n| if nperrow_group nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false)) else nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s)) end nperrowradios.add(Gtk::Label.new(' ')) if nperrow && n.to_s == nperrow || !nperrow && $default_N == n rb.active = true end nperrows << { :widget => rb, :value => n.to_s } } nperrowradios.show_all } recreate_theme_config.call theme_button.signal_connect('clicked') { if newtheme = theme_choose(theme_button.label) limit_sizes = nil nperrow = nil theme_button.label = newtheme recreate_theme_config.call end } dialog.vbox.add(frame1) dialog.vbox.add(frame2) dialog.window_position = Gtk::Window::POS_MOUSE dialog.show_all keepon = true ok = true while keepon dialog.run { |response| if response == Gtk::Dialog::RESPONSE_OK if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? } show_popup(dialog, utf8(_("You need to select at least one size (not counting original)."))) else keepon = false end else keepon = ok = false end } end save_theme = theme_button.label save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] } save_opt432 = optimize432.active? save_nperrow = nperrows.find { |e| e[:widget].active? }[:value] dialog.destroy if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow) mark_document_as_dirty save_current_file call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " + "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " + "#{save_opt432 ? '--optimize-for-32' : ''} #{additional_booh_options}", utf8(_("Please wait while scanning source directory...")), 'full scan', { :closure_after => proc { open_file($filename) $modified = true } }) end end def merge_current save_current_file sel = $albums_tv.selection.selected_rows call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " + "--verbose-level #{$verbose_level} #{additional_booh_options}", utf8(_("Please wait while scanning source directory...")), 'one dir scan', { :closure_after => proc { open_file($filename) $albums_tv.selection.select_path(sel[0]) $modified = true } }) end def merge save_current_file theme = $xmldoc.root.attributes['theme'] limit_sizes = $xmldoc.root.attributes['limit-sizes'] if limit_sizes limit_sizes = "--sizes #{limit_sizes}" end call_backend("booh-backend --merge-config '#{$filename}' --for-gui " + "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}", utf8(_("Please wait while scanning source directory...")), 'full scan', { :closure_after => proc { open_file($filename) $modified = true } }) end def save_as_do fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")), nil, Gtk::FileChooser::ACTION_SAVE, nil, [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL]) fc.transient_for = $main_window fc.add_shortcut_folder(File.expand_path("~/.booh")) fc.set_current_folder(File.expand_path("~/.booh")) fc.filename = $orig_filename if fc.run == Gtk::Dialog::RESPONSE_ACCEPT $orig_filename = fc.filename save_current_file_user end fc.destroy end def preferences dialog = Gtk::Dialog.new(utf8(_("Edit preferences")), $main_window, Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT, [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL]) dialog.vbox.add(notebook = Gtk::Notebook.new) notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options")))) tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))), 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2) tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])), 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2) tooltips = Gtk::Tooltips.new tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename; for example: mplayer %f")), nil) tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))), 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2) 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)), 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2) 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) tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))), 0, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2) 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) tbl.attach(emptycomments_check = Gtk::CheckButton.new(utf8(_("Use empty comments for new albums"))), 0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2) tooltips.set_tip(emptycomments_check, utf8(_("Normally, filenames are used as comments for new albums. Check this if you prefer empty comments.")), nil) smp_check.signal_connect('toggled') { if smp_check.active? smp_hbox.sensitive = true else smp_hbox.sensitive = false end } if $config['mproc'] smp_check.active = true smp_spin.value = $config['mproc'].to_i end nogestures_check.active = $config['nogestures'] emptycomments_check.active = $config['emptycomments'] notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced")))) tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to convert when\nperforming 'enhance contrast': "))), 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2) tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1), 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2) dialog.vbox.show_all dialog.run { |response| if response == Gtk::Dialog::RESPONSE_OK $config['video-viewer'] = video_viewer_entry.text if smp_check.active? $config['mproc'] = smp_spin.value.to_i else $config.delete('mproc') end $config['nogestures'] = nogestures_check.active? $config['emptycomments'] = emptycomments_check.active? $config['convert-enhance'] = enhance_entry.text end } dialog.destroy end def perform_undo if $undo_tb.sensitive? $redo_tb.sensitive = $redo_mb.sensitive = true if not more_undoes = UndoHandler.undo($statusbar) $undo_tb.sensitive = $undo_mb.sensitive = false end end end def perform_redo if $redo_tb.sensitive? $undo_tb.sensitive = $undo_mb.sensitive = true if not more_redoes = UndoHandler.redo($statusbar) $redo_tb.sensitive = $redo_mb.sensitive = false end end end def show_one_click_explanation(intro) show_popup($main_window, utf8(_("One-Click tools. %s When such a tool is activated (Rotate clockwise, Rotate counter-clockwise, Enhance or Delete), clicking on a thumbnail will immediately apply the desired action. Click the None icon when you're finished with One-Click tools. ") % intro)) end def create_menu_and_toolbar #- menu mb = Gtk::MenuBar.new filemenu = Gtk::MenuItem.new(utf8(_("_File"))) filesubmenu = Gtk::Menu.new filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW)) filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN)) filesubmenu.append( Gtk::SeparatorMenuItem.new) filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false)) filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false)) filesubmenu.append( Gtk::SeparatorMenuItem.new) tooltips = Gtk::Tooltips.new filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false)) $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png") tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil) filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false)) $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png") tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums")), nil) filesubmenu.append( Gtk::SeparatorMenuItem.new) filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false)) $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png") tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil) filesubmenu.append( Gtk::SeparatorMenuItem.new) filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false)) tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil) filesubmenu.append( Gtk::SeparatorMenuItem.new) filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT)) filemenu.set_submenu(filesubmenu) mb.append(filemenu) new.signal_connect('activate') { new_album } open.signal_connect('activate') { open_file_popup } $save.signal_connect('activate') { save_current_file_user } $save_as.signal_connect('activate') { save_as_do } $merge_current.signal_connect('activate') { merge_current } $merge.signal_connect('activate') { merge } $generate.signal_connect('activate') { save_current_file call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}", utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")), 'web-album', { :successmsg => utf8(_("Your web-album is now ready in directory `%s'.") % $xmldoc.root.attributes['destination']), :failuremsg => utf8(_("There was something wrong when generating the web-album, sorry.")), :closure_after => proc { $xmldoc.elements.each('//dir') { |elem| elem.add_attribute('already-generated', 'true') } UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated $undo_tb.sensitive = $undo_mb.sensitive = false $redo_tb.sensitive = $redo_mb.sensitive = false save_current_file $generated_outofline = true }}) } $properties.signal_connect('activate') { properties } quit.signal_connect('activate') { try_quit } editmenu = Gtk::MenuItem.new(utf8(_("_Edit"))) editsubmenu = Gtk::Menu.new editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false)) editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false)) editsubmenu.append( Gtk::SeparatorMenuItem.new) editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false)) $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png") 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) editsubmenu.append( Gtk::SeparatorMenuItem.new) editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES)) editmenu.set_submenu(editsubmenu) mb.append(editmenu) $remove_all_captions.signal_connect('activate') { remove_all_captions } prefs.signal_connect('activate') { preferences } helpmenu = Gtk::MenuItem.new(utf8(_("_Help"))) helpsubmenu = Gtk::Menu.new helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools")))) one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png") helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures")))) speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png") helpsubmenu.append( Gtk::SeparatorMenuItem.new) helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT)) helpmenu.set_submenu(helpsubmenu) mb.append(helpmenu) one_click.signal_connect('activate') { show_one_click_explanation(_("One-Click tools are available in the toolbar.")) } speed.signal_connect('activate') { show_popup($main_window, utf8(_("Key shortcuts: Tab: go to next image caption and select text (begin typing to erase current text!) Shift-Tab: go to previous image caption Control-Left/Right/Up/Down: go to specified direction's image caption Control-Enter: for an image, open larger view; for a video, launch player Control-Delete: delete image Shift-Left/Right/Up/Down: move image left/right/up/down Alt-Left/Right: rotate image clockwise/counter-clockwise Control-z: undo Control-r: redo Mouse gestures: Mouse gestures are 'unusual' mouse movements triggering special actions, and are great for speeding up your editions. If bothered, you can disable them from Edit/Preferences. Left click, drag to the right, release: rotate image clockwise Left click, drag to the left, release: rotate image counter-clockwise Left click, drag to the bottom, release: remove image Left click, hold left button, right click: undo Right click, hold right button, left click: redo ")), { :pos_centered => true, :not_transient => true }) } about.signal_connect('activate') { show_popup($main_window, utf8(_("Booh %s ``The Web-Album of choice for discriminating Linux users'' Copyright (c) 2005 Guillaume Cottenceau Artwork: Ayo73 Translations: Japanese: Masao Mutoh French: Guillaume Cottenceau") % $VERSION), { :centered => true, :pos_centered => true, :topwidget => Gtk::Image.new("#{$FPATH}/images/logo.png") }) } #- toolbar tb = Gtk::Toolbar.new tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN)) open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs open.menu = Gtk::Menu.new open.signal_connect('clicked') { open_file_popup } open.signal_connect('show-menu') { lastopens = Gtk::Menu.new j = 0 if $config['last-opens'] $config['last-opens'].reverse.each { |e| lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1) item.signal_connect('activate') { if ask_save_modifications(utf8(_("Save this album?")), utf8(_("Do you want to save the changes to this album?")), { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO }) push_mousecursor_wait msg = open_file_user(from_utf8(e)) pop_mousecursor if msg show_popup($main_window, msg) end end } j += 1 } lastopens.show_all end open.menu = lastopens } tb.insert(-1, Gtk::SeparatorToolItem.new) tb.insert(-1, $r90 = Gtk::ToggleToolButton.new) $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png") $r90.label = utf8(_("Rotate")) tb.insert(-1, $r270 = Gtk::ToggleToolButton.new) $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png") $r270.label = utf8(_("Rotate")) tb.insert(-1, $enhance = Gtk::ToggleToolButton.new) $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png") $enhance.label = utf8(_("Enhance")) tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE)) $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false)) nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png") nothing.label = utf8(_("None")) tb.insert(-1, Gtk::SeparatorToolItem.new) tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false)) tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false)) $undo_tb.signal_connect('clicked') { perform_undo } $undo_mb.signal_connect('activate') { perform_undo } $redo_tb.signal_connect('clicked') { perform_redo } $redo_mb.signal_connect('activate') { perform_redo } one_click_explain_try = Proc.new { if !$config['one-click-explained'] show_one_click_explanation(_("You have just clicked on a One-Click tool.")) $config['one-click-explained'] = true end } $r90.signal_connect('toggled') { if $r90.active? set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW) one_click_explain_try.call $r270.active = false $enhance.active = false $delete.active = false nothing.sensitive = true else if !$r270.active? && !$enhance.active? && !$delete.active? set_mousecursor_normal nothing.sensitive = false else nothing.sensitive = true end end } $r270.signal_connect('toggled') { if $r270.active? set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW) one_click_explain_try.call $r90.active = false $enhance.active = false $delete.active = false nothing.sensitive = true else if !$r90.active? && !$enhance.active? && !$delete.active? set_mousecursor_normal nothing.sensitive = false else nothing.sensitive = true end end } $enhance.signal_connect('toggled') { if $enhance.active? set_mousecursor(Gdk::Cursor::SPRAYCAN) one_click_explain_try.call $r90.active = false $r270.active = false $delete.active = false nothing.sensitive = true else if !$r90.active? && !$r270.active? && !$delete.active? set_mousecursor_normal nothing.sensitive = false else nothing.sensitive = true end end } $delete.signal_connect('toggled') { if $delete.active? set_mousecursor(Gdk::Cursor::PIRATE) one_click_explain_try.call $r90.active = false $r270.active = false $enhance.active = false nothing.sensitive = true else if !$r90.active? && !$r270.active? && !$enhance.active? set_mousecursor_normal nothing.sensitive = false else nothing.sensitive = true end end } nothing.signal_connect('clicked') { $r90.active = $r270.active = $enhance.active = $delete.active = false set_mousecursor_normal } return [ mb, tb ] end def gtk_thread_protect(&proc) if Thread.current == Thread.main proc.call else $protect_gtk_pending_calls.synchronize { $gtk_pending_calls << proc } end end def gtk_thread_abandon $protect_gtk_pending_calls.try_lock $gtk_pending_calls = [] $protect_gtk_pending_calls.unlock end def create_main_window mb, tb = create_menu_and_toolbar $albums_tv = Gtk::TreeView.new $albums_tv.set_size_request(120, -1) renderer = Gtk::CellRendererText.new column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 }) $albums_tv.append_column(column) $albums_tv.set_headers_visible(false) $albums_tv.selection.signal_connect('changed') { |w| push_mousecursor_wait save_changes iter = w.selected if !iter msg 3, "no selection" else $current_path = $albums_ts.get_value(iter, 1) change_dir end pop_mousecursor } $albums_ts = Gtk::TreeStore.new(String, String) $albums_tv.set_model($albums_ts) $albums_tv.signal_connect('realize') { $albums_tv.grab_focus } albums_sw = Gtk::ScrolledWindow.new(nil, nil) albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC) albums_sw.add_with_viewport($albums_tv) $notebook = Gtk::Notebook.new create_subalbums_page $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page")))) create_auto_table $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page")))) $notebook.show_all $notebook.signal_connect('switch-page') { |w, page, num| if num == 0 $delete.active = false $delete.sensitive = false else $delete.sensitive = true end if $xmldir && $subalbums_edits[$xmldir.attributes['path']] && textview = $subalbums_edits[$xmldir.attributes['path']][:editzone] if num == 0 textview.buffer.text = $thumbnails_title.buffer.text else if $notebook.get_tab_label($autotable_sw).sensitive? $thumbnails_title.buffer.text = textview.buffer.text end end end } paned = Gtk::HPaned.new paned.pack1(albums_sw, false, false) paned.pack2($notebook, true, true) main_vbox = Gtk::VBox.new(false, 0) main_vbox.pack_start(mb, false, false) main_vbox.pack_start(tb, false, false) main_vbox.pack_start(paned, true, true) main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false) $main_window = Gtk::Window.new $main_window.add(main_vbox) $main_window.signal_connect('delete-event') { try_quit({ :disallow_cancel => true }) } #- read/save size and position of window if $config['pos-x'] && $config['pos-y'] $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i) else $main_window.window_position = Gtk::Window::POS_CENTER end msg 3, "size: #{$config['width']}x#{$config['height']}" $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i) $main_window.signal_connect('configure-event') { msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}" x, y = $main_window.window.root_origin width, height = $main_window.window.size $config['pos-x'] = x $config['pos-y'] = y $config['width'] = width $config['height'] = height false } $protect_gtk_pending_calls = Mutex.new $gtk_pending_calls = [] Gtk.timeout_add(100) { $protect_gtk_pending_calls.synchronize { $gtk_pending_calls.each { |c| c.call } $gtk_pending_calls = [] } true } $statusbar.push(0, utf8(_("Ready."))) $main_window.show_all end Thread.abort_on_exception = true handle_options read_config Gtk.init create_main_window if ARGV[0] open_file_user(ARGV[0]) end Gtk.main write_config