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.
Name Total lines Lines of code Total coverage Code coverage
lib/alexandria/ui/main_app.rb 1673 1368
12.1% 
9.3% 
   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
</