disallow directories with '
[booh] / bin / booh-gui
1 #!/usr/bin/ruby
2 #
3 #                         *  BOOH  *
4 #
5 # A.k.a `Best web-album Of the world, Or your money back, Humerus'.
6 #
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
11 #
12 #
13 # Copyright (c) 2004 Guillaume Cottenceau <gc3 at bluewin.ch>
14 #
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21
22
23 require 'gtk2'
24 class Gtk::Allocation
25     def to_s; "x:#{x} y:#{y} width:#{width} height:#{height}"; end
26 end
27 require 'booh/GtkAutoTable'
28
29 require 'gettext'
30 include GetText
31 bindtextdomain("booh")
32
33 require 'rexml/document'
34 include REXML
35
36 require 'booh/booh-lib'
37 include Booh
38
39
40 def read_config
41     $config = {}
42     $config_file = File.expand_path('~/.booh-gui-rc')
43     if File.readable?($config_file)
44         $xmldoc = REXML::Document.new(File.new($config_file))
45         $xmldoc.root.elements.each { |element|
46             txt = element.get_text
47             $config[element.name] = txt ? txt.value : nil
48         }
49     end
50     if !FileTest.directory?(File.expand_path('~/.booh-gui-files'))
51         system("mkdir ~/.booh-gui-files")
52     end
53 end
54
55 def write_config
56     ios = File.open($config_file, "w")
57     $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
58     $xmldoc << XMLDecl.new( XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET )
59     $config.each_pair { |key, value|
60     elem = $xmldoc.root.add_element key
61         elem.add_text value.to_s
62     }
63     $xmldoc.write(ios, 0)
64     ios.close
65 end
66
67 def set_mousecursor_wait(window)
68     window.set_cursor(Gdk::Cursor.new(Gdk::Cursor::WATCH))
69     Gtk.main_iteration while Gtk.events_pending?
70 end
71 def set_mousecursor_normal(window)
72     window.set_cursor(Gdk::Cursor.new(Gdk::Cursor::LEFT_PTR))
73 end
74
75 def build_full_dest_filename(filename)
76     source = $xmldoc.root.attributes['source']
77     dest = $xmldoc.root.attributes['destination']
78     dest_dir = make_dest_filename(from_utf8($current_path).sub(/^#{Regexp.quote(source)}/, dest))
79     return dest_dir + '/' + make_dest_filename(from_utf8(filename))
80 end
81
82 def view_image(filename)
83     w = Gtk::Window.new
84
85     default_size = $images_size.detect { |sizeobj| sizeobj['default'] }
86     dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{default_size['fullscreen']}.jpg"
87     #- typically this file won't exist in case of videos; try with the largest thumbnail around
88     if !File.exists?(dest_img)
89         alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
90         if alternatives
91             dest_img = alternatives[-1]
92         else
93             return
94         end
95     end
96     i = Gtk::Image.new(dest_img)
97     f = Gtk::Frame.new
98     f.add(i)
99     f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
100     hb = Gtk::HBox.new
101     hb.pack_start(Gtk::Label.new, true, true)
102     hb.pack_start(f, false, false)
103     hb.pack_end(Gtk::Label.new, true, true)
104     evt = Gtk::EventBox.new
105     evt.add(hb)
106
107     tooltips = Gtk::Tooltips.new
108     tooltips.set_tip(evt, utf8(File.basename(filename).gsub(/\.jpg/, '')), nil)
109     
110     hb2 = Gtk::HBox.new
111     hb2.pack_start(Gtk::Label.new, true, true)
112     hb2.pack_start(b = Gtk::Button.new(Gtk::Stock::CLOSE), false, false)
113     b.signal_connect('clicked') { w.destroy }
114     hb2.pack_end(Gtk::Label.new, true, true)
115
116     vb = Gtk::VBox.new
117     vb.pack_start(evt, false, false)
118     vb.pack_end(hb2, false, false)
119
120     w.add(vb)
121     w.signal_connect('delete-event') { w.destroy }
122     w.window_position = Gtk::Window::POS_CENTER
123     w.show_all
124 end
125
126 def add_thumbnail(autotable, name, type, filename, caption)
127
128     frame1 = Gtk::Frame.new
129     frame1.add(img = Gtk::Image.new(filename))
130     frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
131     hbox1 = Gtk::HBox.new
132     hbox1.pack_start(Gtk::Label.new, true, true)
133     hbox1.pack_start(frame1, false, false)
134     hbox1.pack_end(Gtk::Label.new, true, true)
135     evtbox = Gtk::EventBox.new
136     evtbox.add(hbox1)
137
138     tooltips = Gtk::Tooltips.new
139     tipname = utf8(File.basename(filename).gsub(/-\d+x\d+\.jpg/, ''))
140     tooltips.set_tip(evtbox, type == 'video' ? __("%s (video)", tipname) : tipname, nil)
141
142     frame2 = Gtk::Frame.new
143     frame2.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
144     frame2.set_shadow_type(Gtk::SHADOW_IN)
145
146     textview.buffer.text = utf8(caption)
147     textview.set_justification(Gtk::Justification::CENTER)
148
149     vbox = Gtk::VBox.new(false, 5)
150     vbox.pack_start(evtbox, false, false)
151     vbox.pack_start(frame2, false, false)
152     autotable.append(vbox, name)
153
154     #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
155     $vbox2textview[vbox] = textview
156
157     #- to be able to find widgets by name
158     $name2widgets[name] = { :textview => textview }
159
160     textview.signal_connect('key-press-event') { |w, event|
161         propagate = true
162         textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
163         if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
164             $autotable_sw.signal_emit('key-press-event', event)
165         end
166         if event.state != 0
167             x, y = autotable.get_current_pos(vbox)
168             control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
169             shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
170             if event.keyval == Gdk::Keyval::GDK_Up && y > 0
171                 if control_pressed
172                     $vbox2textview[autotable.get_widget_at_pos(x, y - 1)].grab_focus
173                 end
174                 if shift_pressed
175                     autotable.move_up(vbox)
176                     textview.grab_focus  #- because if moving, focus is stolen
177                 end
178             end
179             if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
180                 if control_pressed
181                     $vbox2textview[autotable.get_widget_at_pos(x, y + 1)].grab_focus
182                 end
183                 if shift_pressed
184                     autotable.move_down(vbox)
185                     textview.grab_focus  #- because if moving, focus is stolen
186                 end
187             end
188             if event.keyval == Gdk::Keyval::GDK_Left
189                 previous = autotable.get_previous_widget(vbox)
190                 if autotable.get_current_pos(previous)[0] < x
191                     if control_pressed
192                         $vbox2textview[previous].grab_focus
193                     end
194                     if shift_pressed
195                         autotable.move_left(vbox)
196                         textview.grab_focus  #- because if moving, focus is stolen
197                     end
198                 end
199             end
200             if event.keyval == Gdk::Keyval::GDK_Right
201                 next_ = autotable.get_next_widget(vbox)
202                 if autotable.get_current_pos(next_)[0] > x
203                     if control_pressed
204                         $vbox2textview[next_].grab_focus
205                     end
206                     if shift_pressed
207                         autotable.move_right(vbox)
208                         textview.grab_focus  #- because if moving, focus is stolen
209                     end
210                 end
211             end
212             if event.keyval == Gdk::Keyval::GDK_Delete && shift_pressed
213                 after = autotable.get_next_widget(vbox)
214                 if !after
215                     after = autotable.get_previous_widget(vbox)
216                 end
217                 autotable.remove(vbox)
218                 if after
219                     $vbox2textview[after].grab_focus
220                 end
221             end
222             if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
223                 view_image(File.basename(name))
224                 propagate = false
225             end
226         end
227         !propagate  #- propagate if needed
228     }
229
230     textview.signal_connect('focus-in-event') { |w, event|
231         textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
232         false  #- propagate
233     }
234
235     evtbox.signal_connect('button-press-event') { |w, event|
236         textview.grab_focus
237         if event.event_type == Gdk::Event::BUTTON2_PRESS
238             view_image(File.basename(name))
239             true
240         else
241             false  #- propagate
242         end
243     }
244
245     vbox.show_all
246 end
247
248 def create_auto_table
249
250     $autotable = Gtk::AutoTable.new(5)
251
252     $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
253     $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
254     $autotable_sw.add_with_viewport($autotable)
255
256     return $autotable_sw
257 end
258
259 def try_quit
260     save_changes
261     if $filename
262         ios = File.open($filename, "w")
263         $xmldoc.write(ios, 0)
264         ios.close
265     end
266     Gtk.main_quit
267 end
268
269 def show_popup(parent, msg)
270     dialog = Gtk::Dialog.new
271     dialog.title = utf8(_("Booh message"))
272     dialog.transient_for = parent
273     dialog.set_default_size(200, 120)
274     dialog.vbox.add(Gtk::Label.new(msg))
275
276     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
277     dialog.show_all
278     dialog.run
279     dialog.destroy
280 end
281
282 def save_changes
283     if !$current_path
284         return
285     end
286     #- remove and reinsert elements to reflect new ordering
287     save_attributes = {}
288     save_types = {}
289     xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
290     xmldir.elements.each { |element|
291         if element.name == 'image' || element.name == 'video'
292             save_types[element.attributes['filename']] = element.name
293             save_attributes[element.attributes['filename']] = element.attributes
294             element.remove
295         end
296     }
297     $autotable.current_order.each { |path|
298         chld = xmldir.add_element save_types[path], save_attributes[path]
299         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
300     }
301 end
302
303 def show_thumbnails
304     default_size = $images_size.detect { |sizeobj| sizeobj['default'] }
305     $autotable.clear
306     $vbox2textview = {}
307     $name2widgets = {}
308
309     xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
310     xmldir.elements.each { |element|
311         if element.name == 'image' || element.name == 'video'
312             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{default_size['thumbnails']}.jpg"
313             puts "dest_img: #{dest_img}"
314             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
315         end
316     }
317 end
318
319 def open_file(filename)
320     $filename = nil
321     $current_path = nil   #- invalidate
322
323     begin
324         $xmldoc = REXML::Document.new File.new(filename)
325     rescue
326         $xmldoc = nil
327     end
328
329     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
330         return utf8(_("Not a booh file!"))
331     end
332
333     if !source = $xmldoc.root.attributes['source']
334         return utf8(_("Corrupted booh file..."))
335     end
336
337     if !dest = $xmldoc.root.attributes['destination']
338         return utf8(_("Corrupted booh file..."))
339     end
340
341     $filename = filename
342     select_theme('simple')
343     puts "source: #{source}"
344
345     xmldir = $xmldoc.elements["//dir[@path='#{source}']"]
346     if !xmldir
347         return utf8(_("Corrupted booh file..."))
348     end
349
350     $albums_ts.clear
351
352     append_dir_elem = Proc.new { |parent_iter, location|
353         child_iter = $albums_ts.append(parent_iter)
354         child_iter[0] = File.basename(location)
355         child_iter[1] = location
356         puts "puttin location: #{location}"
357         $xmldoc.elements.each("//dir[@path='#{location}']/dir") { |elem|
358             append_dir_elem.call(child_iter, elem.attributes['path'])
359         }
360     }
361     append_dir_elem.call(nil, source)
362
363     $albums_tv.expand_all
364     $albums_tv.selection.select_iter($albums_ts.iter_first)
365
366     return nil
367 end
368
369 def create_menu
370     mb = Gtk::MenuBar.new
371
372     filemenu = Gtk::MenuItem.new(utf8(_("_File")))
373     filesubmenu = Gtk::Menu.new
374     filesubmenu.append(new     = Gtk::MenuItem.new(utf8(_("_New (do nothing)"))))
375     filesubmenu.append(open    = Gtk::MenuItem.new(utf8(_("_Open"))))
376     filesubmenu.append(          Gtk::SeparatorMenuItem.new)
377     filesubmenu.append(save    = Gtk::MenuItem.new(utf8(_("_Save (do nothing - autosave on quit)"))))
378     filesubmenu.append(save_as = Gtk::MenuItem.new(utf8(_("Save _as... (do nothing)"))))
379     filesubmenu.append(          Gtk::SeparatorMenuItem.new)
380     filesubmenu.append(quit    = Gtk::MenuItem.new(utf8(_("_Quit"))))
381     filemenu.set_submenu(filesubmenu)
382     mb.append(filemenu)
383
384     open.signal_connect('activate') { |w|
385         fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
386                                         nil,
387                                         Gtk::FileChooser::ACTION_OPEN,
388                                         nil,
389                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
390         fc.add_shortcut_folder(File.expand_path("~/.booh-gui-files"))
391         fc.set_current_folder(File.expand_path("~/.booh-gui-files"))
392         fc.transient_for = $main_window
393         ok = false
394         while !ok
395             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
396                 set_mousecursor_wait(fc.window)
397                 msg = open_file(fc.filename)
398                 set_mousecursor_normal(fc.window)
399                 if msg
400                     show_popup(fc, msg)
401                     ok = false
402                 else
403                     ok = true
404                 end
405             else
406                 ok = true
407             end
408         end
409         fc.destroy
410     }
411
412     quit.signal_connect('activate') { |w|
413         try_quit
414     }
415
416     return mb
417 end
418
419 def create_main_window
420
421     mb = create_menu
422
423     tbl = create_auto_table
424 #    open_file('/home/gc/booh/foo')
425
426     $albums_tv = Gtk::TreeView.new
427     $albums_tv.set_size_request(120, -1)
428     renderer = Gtk::CellRendererText.new
429     column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 })
430     $albums_tv.append_column(column)
431     $albums_tv.set_headers_visible(false)
432     $albums_tv.selection.signal_connect('changed') { |w|
433         save_changes
434         iter = w.selected
435         if !iter
436             puts "no selection"
437         else
438             $current_path = $albums_ts.get_value(iter, 1)
439             show_thumbnails
440         end
441     }
442     $albums_ts = Gtk::TreeStore.new(String, String)
443     $albums_tv.set_model($albums_ts)
444
445     paned = Gtk::HPaned.new
446     paned.pack1($albums_tv, false, false)
447     paned.pack2(tbl, true, true)
448
449     main_vbox = Gtk::VBox.new(false, 5)
450     main_vbox.pack_start(mb, false, false)
451     main_vbox.pack_start(paned, true, true)
452     main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
453
454     $main_window = Gtk::Window.new
455     $main_window.add(main_vbox)
456     $main_window.signal_connect('delete-event') {
457         try_quit
458     }
459
460     #- read/save size and position of window
461     if $config['pos-x'] && $config['pos-y']
462         $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
463     else
464         $main_window.window_position = Gtk::Window::POS_CENTER
465     end
466     puts "width: #{$config['width']}\n\n"
467     $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
468     $main_window.signal_connect('configure-event') {
469         puts "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
470         x, y = $main_window.window.root_origin
471         width, height = $main_window.window.size
472         $config['pos-x'] = x
473         $config['pos-y'] = y
474         $config['width'] = width
475         $config['height'] = height
476         false
477     }
478
479     $statusbar.push(0, utf8(_("Ready.")))
480     $main_window.show_all
481 end
482
483
484 read_config
485
486 Gtk.init
487 create_main_window
488 if ARGV[0]
489     open_file(ARGV[0])
490 end
491 Gtk.main
492
493 write_config