C0 code coverage information
Generated on Tue Oct 16 11:40:53 -0400 2007 with rcov 0.8.0
Code reported as executed by Ruby looks like this...
and this: this line is also marked as covered.
Lines considered as run by rcov, but not reported by Ruby, look like this,
and this: these lines were inferred by rcov (using simple heuristics).
Finally, here's a line marked as not executed.
1 # Copyright (C) 2004-2006 Laurent Sansonetti
2 #
3 # Alexandria is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License as
5 # published by the Free Software Foundation; either version 2 of the
6 # License, or (at your option) any later version.
7 #
8 # Alexandria is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 # General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public
14 # License along with Alexandria; see the file COPYING. If not,
15 # write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
16 # Boston, MA 02111-1307, USA.
17
18 require 'alexandria/ui/dialogs/alert_dialog'
19 # misc_dialogs depends on alert_dialog
20 require 'alexandria/ui/dialogs/misc_dialogs'
21 require 'alexandria/ui/dialogs/about_dialog'
22 require 'alexandria/ui/dialogs/book_properties_dialog_base'
23 require 'alexandria/ui/dialogs/book_properties_dialog'
24 require 'alexandria/ui/dialogs/new_book_dialog_manual'
25 require 'alexandria/ui/dialogs/new_book_dialog'
26 require 'alexandria/ui/dialogs/preferences_dialog'
27 require 'alexandria/ui/dialogs/export_dialog'
28 require 'alexandria/ui/dialogs/import_dialog'
29 require 'alexandria/ui/dialogs/acquire_dialog'
30 require 'alexandria/ui/dialogs/smart_library_properties_dialog_base'
31 require 'alexandria/ui/dialogs/smart_library_properties_dialog'
32 require 'alexandria/ui/dialogs/new_smart_library_dialog'
33 require 'alexandria/ui/dialogs/bad_isbns_dialog'
34
35 class CellRendererToggle < Gtk::CellRendererToggle
36 attr_accessor :text
37 type_register
38 install_property(GLib::Param::String.new(
39 "text",
40 "text",
41 "Some damn value",
42 "",
43 GLib::Param::READABLE|GLib::Param::WRITABLE))
44 end
45
46 class Gtk::ActionGroup
47 def [](x)
48 get_action(x)
49 end
50 end
51
52 class Gtk::IconView
53 def freeze
54 @old_model = self.model
55 self.model = nil
56 end
57
58 def unfreeze
59 self.model = @old_model
60 end
61 end
62
63 class Alexandria::Library
64 def action_name
65 "MoveIn" + name.gsub(/\s/, '')
66 end
67 end
68
69 class Alexandria::BookProviders::AbstractProvider
70 def action_name
71 "At" + name
72 end
73 end
74
75
76 module Alexandria
77 module UI
78 class MainApp < GladeBase
79 attr_accessor :main_app, :actiongroup, :appbar
80 include GetText
81 GetText.bindtextdomain(Alexandria::TEXTDOMAIN, nil, nil, "UTF-8")
82
83 module Columns
84 COVER_LIST, COVER_ICON, TITLE, TITLE_REDUCED, AUTHORS,
85 ISBN, PUBLISHER, PUBLISH_DATE, EDITION, RATING, IDENT,
86 NOTES, REDD, OWN, WANT = (0..15).to_a
87 end
88
89 # The maximum number of rating stars displayed.
90 MAX_RATING_STARS = 5
91
92 def initialize
93 super("main_app.glade")
94 @prefs = Preferences.instance
95 puts "Loading Libraries..." if $DEBUG
96 load_libraries
97 puts "Initializing UI..." if $DEBUG
98 initialize_ui
99 puts "Initializing books_selection choice..." if $DEBUG
100 on_books_selection_changed
101 puts "Restoring preferences..." if $DEBUG
102 restore_preferences
103 end
104
105 def on_library_button_press_event(widget, event)
106 # right click
107 if event.event_type == Gdk::Event::BUTTON_PRESS and
108 event.button == 3
109
110 # This works, but the library loading is too slow!
111 # if path = widget.get_path_at_pos(event.x, event.y)
112 # obj, path = widget.is_a?(Gtk::TreeView) \
113 # ? [widget.selection, path.first] : [widget, path]
114
115 # unless obj.path_is_selected?(path)
116 # widget.unselect_all
117 # obj.select_path(path)
118 # end
119 # else
120 # widget.unselect_all
121 # end
122 #This kind of thing seems too clever.
123 menu = widget.get_path_at_pos(event.x, event.y) == nil \
124 ? @nolibrary_popup \
125 : selected_library.is_a?(SmartLibrary) \
126 ? @smart_library_popup : @library_popup
127
128 menu.popup(nil, nil, event.button, event.time)
129 end
130 end
131
132 def on_books_button_press_event(widget, event)
133 # right click
134 if event.event_type == Gdk::Event::BUTTON_PRESS and
135 event.button == 3
136
137 widget.grab_focus
138
139 if path = widget.get_path_at_pos(event.x, event.y)
140 obj, path = widget.is_a?(Gtk::TreeView) \
141 ? [widget.selection, path.first] : [widget, path]
142
143 unless obj.path_is_selected?(path)
144 widget.unselect_all
145 obj.select_path(path)
146 end
147 else
148 widget.unselect_all
149 end
150
151 menu = (selected_books.empty?) ? @nobook_popup : @book_popup
152 menu.popup(nil, nil, event.button, event.time)
153 end
154 end
155
156 def on_books_selection_changed
157 library = selected_library
158 books = selected_books
159 @appbar.status = case books.length
160 when 0
161 case library.length
162 when 0
163 _("Library '%s' selected") % library.name
164
165 else
166 n_unrated = library.n_unrated
167 if n_unrated == library.length
168 n_("Library '%s' selected, %d unrated book",
169 "Library '%s' selected, %d unrated books",
170 library.length) % [ library.name,
171 library.length ]
172 elsif n_unrated == 0
173 n_("Library '%s' selected, %d book",
174 "Library '%s' selected, %d books",
175 library.length) % [ library.name,
176 library.length ]
177 else
178 n_("Library '%s' selected, %d book, " +
179 "%d unrated",
180 "Library '%s' selected, %d books, " +
181 "%d unrated",
182 library.length) % [ library.name,
183 library.length,
184 n_unrated ]
185 end
186 end
187 when 1
188 _("'%s' selected") % books.first.title
189 else
190 n_("%d book selected", "%d books selected",
191 books.length) % books.length
192 end
193 unless @library_listview.has_focus?
194 @actiongroup["Properties"].sensitive = \
195 @actiongroup["OnlineInformation"].sensitive = \
196 books.length == 1
197 @actiongroup["SelectAll"].sensitive = \
198 books.length < library.length
199 @actiongroup["Delete"].sensitive = \
200 @actiongroup["DeselectAll"].sensitive = \
201 @actiongroup["Move"].sensitive =
202 @actiongroup["SetRating"].sensitive = !books.empty?
203
204 if library.is_a?(SmartLibrary)
205 @actiongroup["Delete"].sensitive =
206 @actiongroup["Move"].sensitive = false
207 end
208
209 # Sensitize providers URL
210 if books.length == 1
211 all_url = false
212 BookProviders.each do |provider|
213 has_url = books.first.isbn and provider.url(books.first) != nil
214 @actiongroup[provider.action_name].sensitive = has_url
215 all_url = true if has_url and !all_url
216 end
217 unless all_url
218 @actiongroup["OnlineInformation"].sensitive = false
219 end
220 end
221 end
222 end
223
224 def on_switch_page
225 @actiongroup["ArrangeIcons"].sensitive = @notebook.page == 0
226 on_books_selection_changed
227 end
228
229 def on_focus(widget, event_focus)
230 if widget == @library_listview
231 %w{OnlineInformation SelectAll DeselectAll}.each do |action|
232 @actiongroup[action].sensitive = false
233 end
234 @actiongroup["Properties"].sensitive =
235 selected_library.is_a?(SmartLibrary)
236 @actiongroup["Delete"].sensitive =
237 (@libraries.all_regular_libraries.length > 1 or
238 selected_library.is_a?(SmartLibrary))
239 else
240 on_books_selection_changed
241 end
242 end
243
244 def on_refresh
245 load_libraries
246 refresh_libraries
247 refresh_books
248 end
249
250 def on_close_sidepane
251 @actiongroup["Sidepane"].active = false
252 end
253
254 def update(*ary)
255 caller = ary.first
256 if caller.is_a?(UndoManager)
257 @actiongroup["Undo"].sensitive = caller.can_undo?
258 @actiongroup["Redo"].sensitive = caller.can_redo?
259 elsif caller.is_a?(Library)
260 library, kind, book = ary
261 if library == selected_library
262 @iconview.freeze
263 case kind
264 when Library::BOOK_ADDED
265 append_book(book)
266
267 when Library::BOOK_UPDATED
268 iter = iter_from_ident(book.saved_ident)
269 if iter
270 fill_iter_with_book(iter, book)
271 end
272
273
274 when Library::BOOK_REMOVED
275 @model.remove(iter_from_book(book))
276 end
277 @iconview.unfreeze
278 elsif selected_library.is_a?(SmartLibrary)
279 refresh_books
280 end
281 else
282 raise "unrecognized update event"
283 end
284 end
285
286 #######
287 private
288 #######
289
290 def display_help
291 Gnome::Help.display('alexandria',
292 nil) rescue ErrorDialog.new(@main_app,
293 e.message)
294 end
295
296 def open_web_browser(url)
297 unless (cmd = Preferences.instance.www_browser).nil?
298 Thread.new { system(cmd % "\"" + url + "\"") }
299 else
300 ErrorDialog.new(@main_app,
301 _("Unable to launch the web browser"),
302 _("Check out that a web browser is " +
303 "configured as default (Desktop " +
304 "Preferences -> Advanced -> Preferred " +
305 "Applications) and try again."))
306 end
307 end
308
309 def open_email_client(url)
310 unless (cmd = Preferences.instance.email_client).nil?
311 Thread.new { system(cmd % "\"" + url + "\"") }
312 else
313 ErrorDialog.new(@main_app,
314 _("Unable to launch the mail reader"),
315 _("Check out that a mail reader is " +
316 "configured as default (Desktop " +
317 "Preferences -> Advanced -> Preferred " +
318 "Applications) and try again."))
319 end
320 end
321
322 def load_libraries
323 completion_models = CompletionModels.instance
324 if @libraries
325 @libraries.all_regular_libraries.each do |library|
326 if library.is_a?(Library)
327 library.delete_observer(self)
328 completion_models.remove_source(library)
329 end
330 end
331 @libraries.reload
332 else
333 #On start
334
335 @libraries = Libraries.instance
336 @libraries.reload
337 unless @libraries.ruined_books.empty?
338 message = _("These books do not conform to the ISBN-13 standard. We will attempt to replace them from the book providers. Otherwise, we will turn them into manual entries.\n" )
339 @libraries.ruined_books.each {|bi| message += "\n#{bi[1] or bi[1].inspect}"}
340 bad_isbn_warn = Gtk::MessageDialog.new(@main_app, Gtk::Dialog::MODAL, Gtk::MessageDialog::WARNING, Gtk::MessageDialog::BUTTONS_CLOSE, message ).show
341 bad_isbn_warn.signal_connect('response') { bad_isbn_warn.destroy }
342 books_to_add = []
343
344 #This is the restoration thread. We can come up with strategies for restoring 'bad' books here.
345
346 Thread.new do
347 #Needs a progress indicator.
348 @libraries.ruined_books.each {|book, isbn, library|
349 begin
350 books_to_add << [Alexandria::BookProviders.isbn_search(isbn.to_s), library].flatten
351 puts book.title
352 rescue
353
354 books_to_add << [book, nil, library]
355 puts "#{book.title} didn't make it."
356 end
357 }
358 # Will crash here when it gets to it.
359 books_to_add.each do |book, cover_uri, library|
360
361 unless cover_uri.nil?
362 library.save_cover(book, cover_uri)
363 end
364
365 library << book
366 library.save(book)
367
368 end
369
370 end
371 puts books_to_add if $DEBUG
372 end
373 end
374 @libraries.all_regular_libraries.each do |library|
375 library.add_observer(self)
376 completion_models.add_source(library)
377 end
378 end
379
380 def cache_scaled_icon(icon, width, height)
381 @cache ||= {}
382 @cache[[icon, width, height]] ||= icon.scale(width, height)
383 end
384
385 ICON_TITLE_MAXLEN = 20 # characters
386 ICON_WIDTH = 60
387 ICON_HEIGHT = 90 # pixels
388 REDUCE_TITLE_REGEX = /^(.{#{ICON_TITLE_MAXLEN}}).*$/
389
390 def fill_iter_with_book(iter, book)
391
392 iter[Columns::IDENT] = book.ident.to_s
393 iter[Columns::TITLE] = book.title
394 title = book.title.sub(REDUCE_TITLE_REGEX, '\1...')
395 iter[Columns::TITLE_REDUCED] = title
396 iter[Columns::AUTHORS] = book.authors.join(', ')
397 iter[Columns::ISBN] = book.isbn.to_s
398 iter[Columns::PUBLISHER] = book.publisher
399 iter[Columns::PUBLISH_DATE] = (book.publishing_year.to_s rescue "")
400 iter[Columns::EDITION] = book.edition
401 iter[Columns::NOTES] = (book.notes or "")
402 rating = (book.rating or Book::DEFAULT_RATING)
403 iter[Columns::RATING] = MAX_RATING_STARS - rating # ascending order is the default
404 iter[Columns::OWN] = book.own?
405 iter[Columns::REDD] = book.redd?
406 iter[Columns::WANT] = book.want?
407
408 icon = Icons.cover(selected_library, book)
409 iter[Columns::COVER_LIST] = cache_scaled_icon(icon, 20, 25)
410
411 if icon.height > ICON_HEIGHT
412 new_width = icon.width / (icon.height / ICON_HEIGHT.to_f)
413 new_height = [ICON_HEIGHT, icon.height].min
414 icon = cache_scaled_icon(icon, new_width, new_height)
415 end
416 if rating == MAX_RATING_STARS
417 icon = icon.tag(Icons::FAVORITE_TAG)
418 end
419 iter[Columns::COVER_ICON] = icon
420 end
421
422 def append_book(book, tail=nil)
423 #puts "Blah: #{@model.inspect}" if $DEBUG
424 iter = tail ? @model.insert_after(tail) : @model.append
425 #puts iter
426 if iter
427 fill_iter_with_book(iter, book)
428 else
429 puts "no iter for book #{book}" if $DEBUG
430 end
431 return iter
432 end
433
434 def append_library(library, autoselect=false)
435 model = @library_listview.model
436 is_smart = library.is_a?(SmartLibrary)
437 if is_smart
438 if @library_separator_iter.nil?
439 @library_separator_iter = append_library_separator
440 end
441 iter = model.append
442 else
443 iter = if @library_separator_iter.nil?
444 model.append
445 else
446 model.insert_before(@library_separator_iter)
447 end
448 end
449
450 iter[0] = is_smart \
451 ? Icons::SMART_LIBRARY_SMALL : Icons::LIBRARY_SMALL
452 iter[1] = library.name
453 iter[2] = true # editable?
454 iter[3] = false # separator?
455 if autoselect
456 @library_listview.set_cursor(iter.path,
457 @library_listview.get_column(0),
458 true)
459 @actiongroup["Sidepane"].active = true
460 end
461 return iter
462 end
463
464 def append_library_separator
465 iter = @library_listview.model.append
466 iter[0] = nil
467 iter[1] = nil
468 iter[2] = false # editable?
469 iter[3] = true # separator?
470 return iter
471 end
472
473 BADGE_MARKUP = "<span weight=\"heavy\" foreground=\"white\">%d</span>"
474 BOOKS_TARGET_TABLE = [["ALEXANDRIA_BOOKS",
475 Gtk::Drag::TARGET_SAME_APP,
476 0]]
477 def setup_view_source_dnd(view)
478 puts "setup_view_source_dnd for %s" % view if $DEBUG
479 view.signal_connect_after('drag-begin') do |widget, drag_context|
480 n_books = selected_books.length
481 if n_books > 1
482 # Render generic book icon.
483 pixmap, mask = Icons::BOOK_ICON.render_pixmap_and_mask(255)
484
485 # Create number badge.
486 context = Gdk::Pango.context
487 layout = Pango::Layout.new(context)
488 layout.markup = BADGE_MARKUP % n_books
489 width, height = layout.pixel_size
490 x = Icons::BOOK_ICON.width - width - 11
491 y = Icons::BOOK_ICON.height - height - 11
492
493 # Draw a red ellipse where the badge number should be.
494 red_gc = Gdk::GC.new(pixmap)
495 red_gc.rgb_fg_color = Gdk::Color.parse('red')
496 red_gc.rgb_bg_color = Gdk::Color.parse('red')
497 pixmap.draw_arc(red_gc,
498 true,
499 x - 5, y - 2,
500 width + 9, height + 4,
501 0, 360 * 64)
502
503 # Draw the number badge.
504 pixmap.draw_layout(Gdk::GC.new(pixmap), x, y, layout)
505
506 # And set the drag icon.
507 Gtk::Drag.set_icon(drag_context,
508 pixmap.colormap,
509 pixmap,
510 mask,
511 10,
512 10)
513 end
514 end
515
516 view.signal_connect('drag-data-get') do |widget, drag_context,
517 selection_data, info,
518 time|
519
520 idents = selected_books.map { |book| book.ident }
521 unless idents.empty?
522 selection_data.set(Gdk::Selection::TYPE_STRING,
523 idents.join(','))
524 end
525 end
526
527 view.enable_model_drag_source(Gdk::Window::BUTTON1_MASK,
528 BOOKS_TARGET_TABLE,
529 Gdk::DragContext::ACTION_MOVE)
530 end
531
532 def setup_books_iconview
533 @iconview.model = @iconview_model
534 @iconview.selection_mode = Gtk::SELECTION_MULTIPLE
535 @iconview.text_column = Columns::TITLE_REDUCED
536 @iconview.pixbuf_column = Columns::COVER_ICON
537 @iconview.orientation = Gtk::ORIENTATION_VERTICAL
538 @iconview.row_spacing = 4
539 @iconview.column_spacing = 16
540 @iconview.item_width = ICON_WIDTH + 16
541
542 @iconview.signal_connect('selection-changed') do
543 on_books_selection_changed
544 end
545
546 @iconview.signal_connect('item-activated') do
547 # Dirty hack to avoid the beginning of a drag within this
548 # handler.
549 Gtk.timeout_add(100) do
550 @actiongroup["Properties"].activate
551 false
552 end
553 end
554
555 # DND support for Gtk::IconView is shipped since GTK+ 2.8.0.
556 if @iconview.respond_to?(:enable_model_drag_source)
557 setup_view_source_dnd(@iconview)
558 end
559 end
560
561 ICONS_SORTS = [
562 Columns::TITLE, Columns::AUTHORS, Columns::ISBN,
563 Columns::PUBLISHER, Columns::EDITION, Columns::RATING, Columns::REDD, Columns::OWN, Columns::WANT
564 ]
565 def setup_books_iconview_sorting
566 mode = ICONS_SORTS[@prefs.arrange_icons_mode]
567 @iconview_model.set_sort_column_id(mode,
568 @prefs.reverse_icons \
569 ? Gtk::SORT_DESCENDING \
570 : Gtk::SORT_ASCENDING)
571 @filtered_model.refilter # force redraw
572 end
573
574 def setup_books_listview
575 # first column
576 puts "setup_books_listview" if $DEBUG
577 @listview.model = @listview_model
578 renderer = Gtk::CellRendererPixbuf.new
579 title = _("Title")
580 puts "Create listview column for %s" % title if $DEBUG
581 column = Gtk::TreeViewColumn.new(title)
582 column.widget = Gtk::Label.new(title).show
583 column.pack_start(renderer, false)
584 column.set_cell_data_func(renderer) do |column, cell, model, iter|
585 iter = @listview_model.convert_iter_to_child_iter(iter)
586 iter = @filtered_model.convert_iter_to_child_iter(iter)
587 cell.pixbuf = iter[Columns::COVER_LIST]
588 end
589 renderer = Gtk::CellRendererText.new
590 renderer.ellipsize = Pango::ELLIPSIZE_END if Pango.ellipsizable?
591 =begin
592 # Editable tree views are behaving strangely
593 renderer.signal_connect('editing_started') do |cell, entry,
594 path_string|
595 entry.complete_titles
596 end
597 renderer.signal_connect('edited') do |cell, path_string, new_string|
598 path = Gtk::TreePath.new(path_string)
599 path = @listview_model.convert_path_to_child_path(path)
600 path = @filtered_model.convert_path_to_child_path(path)
601 iter = @model.get_iter(path)
602 book = book_from_iter(selected_library, iter)
603 book.title = new_string
604 @iconview.freeze
605 fill_iter_with_book(iter, book)
606 @iconview.unfreeze
607 end
608 =end
609 column.pack_start(renderer, true)
610 column.set_cell_data_func(renderer) do |column, cell, model, iter|
611 iter = @listview_model.convert_iter_to_child_iter(iter)
612 iter = @filtered_model.convert_iter_to_child_iter(iter)
613 cell.text, cell.editable = iter[Columns::TITLE], false #true
614 end
615 column.sort_column_id = Columns::TITLE
616 column.resizable = true
617 @listview.append_column(column)
618
619 # other columns
620 names = [
621 [ _("Authors"), Columns::AUTHORS ],
622 [ _("ISBN"), Columns::ISBN ],
623 [ _("Publisher"), Columns::PUBLISHER ],
624 [ _("Publish Year"), Columns::PUBLISH_DATE ],
625 [ _("Binding"), Columns::EDITION ]]
626
627 check_names= [
628 [ _("Read"), Columns::REDD],
629 [ _("Own"), Columns::OWN],
630 [ _("Want"), Columns::WANT]]
631
632
633 names.each do |title, iterid|
634 puts "Create listview column for %s..." % title if $DEBUG
635 renderer = Gtk::CellRendererText.new
636 renderer.ellipsize = Pango::ELLIPSIZE_END if Pango.ellipsizable?
637 column = Gtk::TreeViewColumn.new(title, renderer,
638 :text => iterid)
639 column.widget = Gtk::Label.new(title).show
640 column.sort_column_id = iterid
641 column.resizable = true
642 @listview.append_column(column)
643 end
644
645 check_names.each do |title, iterid|
646 renderer= CellRendererToggle.new
647 column = Gtk::TreeViewColumn.new(title, renderer)
648 column.widget = Gtk::Label.new(title).show
649 column.sort_column_id = iterid
650 column.resizable = true
651 #column.pack_start(renderer, false)
652 column.add_attribute(renderer, 'text', iterid)
653 puts "Create listview column for %s..." % title if $DEBUG
654 column.set_cell_data_func(renderer) do |column, cell,
655 model, iter|
656 case iterid
657 when 12
658 state = iter[Columns::REDD]
659 cell.set_active(state)
660
661 when 13
662 state = iter[Columns::OWN]
663 cell.set_active(state)
664
665 when 14
666 state = iter[Columns::WANT]
667 own_state = iter[Columns::OWN]
668 cell.inconsistent = own_state
669 cell.set_active(state)
670
671 end
672 end
673 @listview.append_column(column)
674 end
675
676 # final column
677 title = _("Rating")
678 puts "Create listview column for %s..." % title if $DEBUG
679
680 column = Gtk::TreeViewColumn.new(title)
681 column.widget = Gtk::Label.new(title).show
682 column.sizing = Gtk::TreeViewColumn::FIXED
683 column.fixed_width = column.min_width = column.max_width =
684 (Icons::STAR_SET.width + 1) * MAX_RATING_STARS
685 MAX_RATING_STARS.times do |i|
686 renderer = Gtk::CellRendererPixbuf.new
687 column.pack_start(renderer, false)
688 column.set_cell_data_func(renderer) do |column, cell,
689 model, iter|
690 iter = @listview_model.convert_iter_to_child_iter(iter)
691 iter = @filtered_model.convert_iter_to_child_iter(iter)
692 rating = (iter[Columns::RATING] - MAX_RATING_STARS).abs
693 cell.pixbuf = rating >= i.succ ?
694 Icons::STAR_SET : Icons::STAR_UNSET
695 end
696 end
697 column.sort_column_id = Columns::RATING
698 column.resizable = false
699 @listview.append_column(column)
700
701 @listview.selection.mode = Gtk::SELECTION_MULTIPLE
702 @listview.selection.signal_connect('changed') do
703 on_books_selection_changed
704 end
705
706 @listview.signal_connect('row-activated') do
707 # Dirty hack to avoid the beginning of a drag within this
708 # handler.
709 Gtk.timeout_add(100) do
710 @actiongroup["Properties"].activate
711 false
712 end
713 end
714
715 setup_view_source_dnd(@listview)
716 end
717
718 def setup_listview_columns_visibility
719 # Show or hide list view columns according to the preferences.
720 cols_visibility = [
721 @prefs.col_authors_visible,
722 @prefs.col_isbn_visible,
723 @prefs.col_publisher_visible,
724 @prefs.col_publish_date_visible,
725 @prefs.col_edition_visible,
726 @prefs.col_rating_visible,
727 @prefs.col_redd_visible,
728 @prefs.col_want_visible,
729 @prefs.col_own_visible
730 ]
731 cols = @listview.columns[1..-1] # skip "Title"
732 cols.each_index do |i|
733 cols[i].visible = cols_visibility[i]
734 end
735 end
736
737 # Sets the width of each column based on any respective
738 # preference value stored.
739 def setup_listview_columns_width
740 if @prefs.cols_width
741 cols_width = YAML.load(@prefs.cols_width)
742 @listview.columns.each do |c|
743 if cols_width.has_key?(c.title)
744 c.sizing = Gtk::TreeViewColumn::FIXED
745 c.fixed_width = cols_width[c.title]
746 end
747 end
748 end
749 end
750
751 def refresh_books
752 # Clear the views.
753 library = selected_library
754 @model.clear
755 @iconview.freeze
756 @model.freeze_notify do
757 tail = nil
758 library.each { |book| tail = append_book(book, tail) }
759 end
760 @filtered_model.refilter
761 @iconview.unfreeze
762 @listview.columns_autosize
763
764 =begin
765 # Append books - we do that in a separate thread.
766 library = selected_library
767 @appbar.progress_percentage = 0
768 @appbar.children.first.visible = true # show the progress bar
769 @appbar.status = _("Loading '%s'...") % library.name
770 exec_queue = ExecutionQueue.new
771
772 on_progress = proc do |percent|
773 @appbar.progress_percentage = percent
774 end
775
776 thread = Thread.start do
777 total = library.length
778 library.each_with_index do |book, n|
779 append_book(book)
780 # convert to percents
781 coeff = total / 100.0
782 percent = n / coeff
783 fraction = percent / 100
784 #puts "#index #{n} percent #{percent} fraction #{fraction}"
785 exec_queue.call(on_progress, fraction)
786 end
787 end
788
789 while thread.alive?
790 exec_queue.iterate
791 Gtk.main_iteration_do(false)
792 end
793
794 @appbar.progress_percentage = 1
795 =end
796
797 # Hide the progress bar.
798 @appbar.children.first.visible = false
799
800 # Refresh the status bar.
801 on_books_selection_changed
802 end
803
804 def selected_library
805 if iter = @library_listview.selection.selected
806 @libraries.all_libraries.find { |x| x.name == iter[1] }
807 else
808 @libraries.all_libraries.first
809 end
810 end
811
812 def select_library(library)
813 iter = @library_listview.model.iter_first
814 ok = true
815 while ok do
816 if iter[1] == library.name
817 @library_listview.selection.select_iter(iter)
818 break
819 end
820 ok = iter.next!
821 end
822 end
823
824 def book_from_iter(library, iter)
825 library.find { |x| x.ident == iter[Columns::IDENT] }
826 end
827
828 def iter_from_ident(ident)
829 iter = @model.iter_first
830 ok = true
831 while ok do
832 if iter[Columns::IDENT] == ident
833 return iter
834 end
835 ok = iter.next!
836 end
837 return nil
838 end
839
840 def iter_from_book(book)
841 iter_from_ident(book.ident)
842 end
843
844 def selected_books
845 a = []
846 library = selected_library
847 view = case @notebook.page
848 when 0
849 @iconview.selected_each do |iconview, path|
850 path = @iconview_model.convert_path_to_child_path(path)
851 path = @filtered_model.convert_path_to_child_path(path)
852 iter = @model.get_iter(path)
853 a << book_from_iter(library, iter)
854 end
855
856 when 1
857 @listview.selection.selected_each do |model, path,
858 iter|
859 path = @listview_model.convert_path_to_child_path(path)
860 path = @filtered_model.convert_path_to_child_path(path)
861 iter = @model.get_iter(path)
862 a << book_from_iter(library, iter)
863 end
864 end
865 a.select { |x| x != nil }
866 end
867
868 def setup_sidepane
869 @library_listview.model = Gtk::ListStore.new(Gdk::Pixbuf,
870 String,
871 TrueClass,
872 TrueClass)
873 @library_separator_iter = nil
874 @libraries.all_regular_libraries.each { |x| append_library(x) }
875 @libraries.all_smart_libraries.each { |x| append_library(x) }
876
877 renderer = Gtk::CellRendererPixbuf.new
878 column = Gtk::TreeViewColumn.new(_("Library"))
879 column.pack_start(renderer, false)
880 column.set_cell_data_func(renderer) do |column, cell, model, iter|
881 cell.pixbuf = iter[0]
882 end
883 renderer = Gtk::CellRendererText.new
884 renderer.ellipsize = Pango::ELLIPSIZE_END if Pango.ellipsizable?
885 column.pack_start(renderer, true)
886 column.set_cell_data_func(renderer) do |column, cell, model, iter|
887 cell.text, cell.editable = iter[1], iter[2]
888 end
889 renderer.signal_connect('edited') do |cell, path_string, new_text|
890 if cell.text != new_text
891 if match = /([^\w\s'"()?!:;.\-])/.match(new_text)
892 ErrorDialog.new(@main_app,
893 _("Invalid library name '%s'") %
894 new_text,
895 _("The name provided contains the " +
896 "illegal character '<i>%s</i>'.") %
897 match[1])
898 elsif new_text.strip.empty?
899 ErrorDialog.new(@main_app, _("The library name " +
900 "can not be empty"))
901 elsif x = (@libraries.all_libraries +
902 Library.deleted_libraries).find {
903 |library| library.name == new_text.strip } \
904 and x.name != selected_library.name
905 ErrorDialog.new(@main_app,
906 _("The library can not be renamed"),
907 _("There is already a library named " +
908 "'%s'. Please choose a different " +
909 "name.") % new_text.strip)
910 else
911 path = Gtk::TreePath.new(path_string)
912 iter = @library_listview.model.get_iter(path)
913 iter[1] = selected_library.name = new_text.strip
914 setup_move_actions
915 refresh_libraries
916 end
917 end
918 end
919 @library_listview.append_column(column)
920
921 @library_listview.set_row_separator_func { |model, iter| iter[3] }
922
923 @library_listview.selection.signal_connect('changed') do
924 refresh_libraries
925 refresh_books
926 end
927
928 @library_listview.enable_model_drag_dest(
929 BOOKS_TARGET_TABLE,
930 Gdk::DragContext::ACTION_MOVE)
931
932 @library_listview.signal_connect('drag-motion') do
933 |widget, drag_context, x, y, time, data|
934
935 path, column, cell_x, cell_y =
936 @library_listview.get_path_at_pos(x, y)
937
938 if path
939 # Refuse drags from/to smart libraries.
940 if selected_library.is_a?(SmartLibrary)
941 path = nil
942 else
943 iter = @library_listview.model.get_iter(path)
944 if iter[3] # separator?
945 path = nil
946 else
947 library = @libraries.all_libraries.find do |x|
948 x.name == iter[1]
949 end
950 path = nil if library.is_a?(SmartLibrary)
951 end
952 end
953 end
954
955 @library_listview.set_drag_dest_row(
956 path,
957 Gtk::TreeView::DROP_INTO_OR_AFTER)
958
959 drag_context.drag_status(
960 path != nil ? drag_context.suggested_action : 0,
961 time)
962 end
963
964 @library_listview.signal_connect('drag-drop') do
965 |widget, drag_context, x, y, time, data|
966
967 Gtk::Drag.get_data(widget,
968 drag_context,
969 drag_context.targets.first,
970 time)
971 true
972 end
973
974 @library_listview.signal_connect('drag-data-received') do
975 |widget, drag_context, x, y, selection_data, info, time|
976
977 success = false
978 if selection_data.type == Gdk::Selection::TYPE_STRING
979 path, position =
980 @library_listview.get_dest_row_at_pos(x, y)
981
982 if path
983 iter = @library_listview.model.get_iter(path)
984 library = @libraries.all_libraries.find do |x|
985 x.name == iter[1]
986 end
987 move_selected_books_to_library(library)
988 end
989 end
990 Gtk::Drag.finish(drag_context, success, false, time)
991 end
992 end
993
994 def refresh_libraries
995 library = selected_library
996
997 # Change the application's title.
998 @main_app.title = library.name + " - " + TITLE
999
1000 # Disable the selected library in the move libraries actions.
1001 @libraries.all_regular_libraries.each do |i_library|
1002 action = @actiongroup[i_library.action_name]
1003 action.sensitive = i_library != library if action
1004 end
1005
1006 # Disable some actions if we selected a smart library.
1007 smart = library.is_a?(SmartLibrary)
1008 @actiongroup["AddBook"].sensitive = !smart
1009 @actiongroup["AddBookManual"].sensitive = !smart
1010 @actiongroup["Properties"].sensitive = smart
1011 @actiongroup["Delete"].sensitive =
1012 (@libraries.all_regular_libraries.length > 1 or smart)
1013 end
1014
1015 def restore_preferences
1016 if @prefs.maximized
1017 @main_app.maximize
1018 else
1019 @main_app.move(*@prefs.position) \
1020 unless @prefs.position == [0, 0]
1021 @main_app.resize(*@prefs.size)
1022 @maximized = false
1023 end
1024 @paned.position = @prefs.sidepane_position
1025 @actiongroup["Sidepane"].active = @prefs.sidepane_visible
1026 @actiongroup["Toolbar"].active = @prefs.toolbar_visible
1027 @actiongroup["Statusbar"].active = @prefs.statusbar_visible
1028 @appbar.visible = @prefs.statusbar_visible
1029 action = case @prefs.view_as
1030 when 0
1031 @actiongroup["AsIcons"]
1032 when 1
1033 @actiongroup["AsList"]
1034 end
1035 action.activate
1036 library = nil
1037 unless @prefs.selected_library.nil?
1038 library = @libraries.all_libraries.find do |x|
1039 x.name == @prefs.selected_library
1040 end
1041 end
1042 if library
1043 select_library(library)
1044 else
1045 # Select the first item by default.
1046 iter = @library_listview.model.iter_first
1047 @library_listview.selection.select_iter(iter)
1048 end
1049 end
1050
1051 def save_preferences
1052 @prefs.position = @main_app.position
1053 @prefs.size = @main_app.allocation.to_a[2..3]
1054 @prefs.maximized = @maximized
1055 @prefs.sidepane_position = @paned.position
1056 @prefs.sidepane_visible = @actiongroup["Sidepane"].active?
1057 @prefs.toolbar_visible = @actiongroup["Toolbar"].active?
1058 @prefs.statusbar_visible = @actiongroup["Statusbar"].active?
1059 @prefs.view_as = @notebook.page
1060 @prefs.selected_library = selected_library.name
1061 cols_width = Hash.new
1062 @listview.columns.each do |c|
1063 cols_width[c.title] = [c.widget.size_request.first, c.width].max
1064 end
1065 @prefs.cols_width = '{' + cols_width.to_a.collect do |t, v|
1066 '"' + t + '": ' + v.to_s
1067 end.join(', ') + '}'
1068 end
1069
1070 def undoable_move(source, dest, books)
1071 Library.move(source, dest, *books)
1072 UndoManager.instance.push { undoable_move(dest, source, books) }
1073 end
1074
1075 def move_selected_books_to_library(library)
1076 books = selected_books.select do |book|
1077 !library.include?(book) or
1078 ConflictWhileCopyingDialog.new(@main_app,
1079 library,
1080 book).replace?
1081 end
1082 undoable_move(selected_library, library, books)
1083 end
1084
1085 def setup_move_actions
1086 @actiongroup.actions.each do |action|
1087 next unless /^MoveIn/.match(action.name)
1088 @actiongroup.remove_action(action)
1089 end
1090 actions = []
1091 @libraries.all_regular_libraries.each do |library|
1092 actions << [
1093 library.action_name, nil,
1094 _("In '_%s'") % library.name,
1095 nil, nil, proc { move_selected_books_to_library(library) }
1096 ]
1097 end
1098 @actiongroup.add_actions(actions)
1099 @uimanager.remove_ui(@move_mid) if @move_mid
1100 @move_mid = @uimanager.new_merge_id
1101 @libraries.all_regular_libraries.each do |library|
1102 name = library.action_name
1103 [ "ui/MainMenubar/EditMenu/Move/",
1104 "ui/BookPopup/Move/" ].each do |path|
1105 @uimanager.add_ui(@move_mid, path, name, name,
1106 Gtk::UIManager::MENUITEM, false)
1107 end
1108 end
1109 end
1110
1111 def undoable_delete(library, books=nil)
1112 # Deleting a library.
1113 if books.nil?
1114 library.delete_observer(self) if library.is_a?(Library)
1115 library.delete
1116 @libraries.remove_library(library)
1117 if @library_separator_iter != nil and
1118 @libraries.all_smart_libraries.empty?
1119
1120 @library_listview.model.remove(@library_separator_iter)
1121 @library_separator_iter = nil
1122 end
1123 previous_selected_library = selected_library
1124 if previous_selected_library != library
1125 select_library(library)
1126 else
1127 previous_selected_library = nil
1128 end
1129 iter = @library_listview.selection.selected
1130 next_iter = @library_listview.selection.selected
1131 next_iter.next!
1132 @library_listview.model.remove(iter)
1133 @library_listview.selection.select_iter(next_iter)
1134 setup_move_actions
1135 select_library(previous_selected_library) \
1136 unless previous_selected_library.nil?
1137 # Deleting books.
1138 else
1139 books.each { |book| library.delete(book) }
1140 end
1141 UndoManager.instance.push { undoable_undelete(library, books) }
1142 end
1143
1144 def undoable_undelete(library, books=nil)
1145 # Undeleting a library.
1146 if books.nil?
1147 library.undelete
1148 @libraries.add_library(library)
1149 append_library(library)
1150 setup_move_actions
1151 library.add_observer(self) if library.is_a?(Library)
1152 # Undeleting books.
1153 else
1154 books.each { |book| library.undelete(book) }
1155 end
1156 select_library(library)
1157 UndoManager.instance.push { undoable_delete(library, books) }
1158 end
1159
1160 def initialize_ui
1161 # @main_app.icon = Icons::ALEXANDRIA_SMALL
1162 Gtk::Window.set_default_icon_name("alexandria")
1163 @main_app.icon_name = "alexandria"
1164 puts "Initializing UI elements..." if $DEBUG
1165
1166 on_new = proc do
1167 name = Library.generate_new_name(@libraries.all_libraries)
1168 library = Library.load(name)
1169 @libraries.add_library(library)
1170 append_library(library, true)
1171 setup_move_actions
1172 library.add_observer(self)
1173 end
1174
1175 on_new_smart = proc do
1176 NewSmartLibraryDialog.new(@main_app) do |smart_library|
1177 smart_library.refilter
1178 @libraries.add_library(smart_library)
1179 append_library(smart_library, true)
1180 smart_library.save
1181 end
1182 end
1183
1184 on_add_book = proc do