alexandria-book-collection-manager 0.6.9.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (293) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rubocop.yml +68 -0
  4. data/.rubocop_todo.yml +200 -0
  5. data/CHANGELOG.md +23 -0
  6. data/COPYING +339 -0
  7. data/ChangeLog.0 +3598 -0
  8. data/Gemfile +9 -0
  9. data/INSTALL.rdoc +156 -0
  10. data/PACKAGING +36 -0
  11. data/README.md +88 -0
  12. data/RELEASE_CHECKLIST +18 -0
  13. data/Rakefile +264 -0
  14. data/TODO +24 -0
  15. data/alexandria-book-collection-manager.gemspec +35 -0
  16. data/alexandria.desktop.in +10 -0
  17. data/bin/alexandria +82 -0
  18. data/doc/AUTHORS +60 -0
  19. data/doc/BUGS +31 -0
  20. data/doc/FAQ +369 -0
  21. data/doc/HACKING +19 -0
  22. data/doc/NEWS +341 -0
  23. data/doc/alexandria.1 +120 -0
  24. data/doc/cuecat_support.rdoc +67 -0
  25. data/lib/alexandria.rb +85 -0
  26. data/lib/alexandria/about.rb +82 -0
  27. data/lib/alexandria/book_providers.rb +411 -0
  28. data/lib/alexandria/book_providers/adlibris.rb +235 -0
  29. data/lib/alexandria/book_providers/amazon_aws.rb +261 -0
  30. data/lib/alexandria/book_providers/amazon_ecs_util.rb +405 -0
  31. data/lib/alexandria/book_providers/barnes_and_noble.rb +229 -0
  32. data/lib/alexandria/book_providers/bol_it.rb +162 -0
  33. data/lib/alexandria/book_providers/deastore.rb +277 -0
  34. data/lib/alexandria/book_providers/douban.rb +135 -0
  35. data/lib/alexandria/book_providers/ibs_it.rb +149 -0
  36. data/lib/alexandria/book_providers/mcu.rb +177 -0
  37. data/lib/alexandria/book_providers/proxis.rb +205 -0
  38. data/lib/alexandria/book_providers/pseudomarc.rb +185 -0
  39. data/lib/alexandria/book_providers/renaud.rb +142 -0
  40. data/lib/alexandria/book_providers/siciliano.rb +271 -0
  41. data/lib/alexandria/book_providers/thalia.rb +197 -0
  42. data/lib/alexandria/book_providers/web.rb +59 -0
  43. data/lib/alexandria/book_providers/webster_it.rb +173 -0
  44. data/lib/alexandria/book_providers/worldcat.rb +251 -0
  45. data/lib/alexandria/book_providers/z3950.rb +422 -0
  46. data/lib/alexandria/config.rb +8 -0
  47. data/lib/alexandria/console.rb +31 -0
  48. data/lib/alexandria/execution_queue.rb +96 -0
  49. data/lib/alexandria/export_library.rb +536 -0
  50. data/lib/alexandria/import_library.rb +316 -0
  51. data/lib/alexandria/import_library_csv.rb +270 -0
  52. data/lib/alexandria/logging.rb +159 -0
  53. data/lib/alexandria/models/book.rb +72 -0
  54. data/lib/alexandria/models/library.rb +714 -0
  55. data/lib/alexandria/net.rb +53 -0
  56. data/lib/alexandria/preferences.rb +324 -0
  57. data/lib/alexandria/scanners.rb +42 -0
  58. data/lib/alexandria/scanners/cuecat.rb +118 -0
  59. data/lib/alexandria/scanners/keyboard.rb +57 -0
  60. data/lib/alexandria/smart_library.rb +525 -0
  61. data/lib/alexandria/ui.rb +53 -0
  62. data/lib/alexandria/ui/builder_base.rb +43 -0
  63. data/lib/alexandria/ui/callbacks.rb +389 -0
  64. data/lib/alexandria/ui/completion_models.rb +228 -0
  65. data/lib/alexandria/ui/dialogs/about_dialog.rb +59 -0
  66. data/lib/alexandria/ui/dialogs/acquire_dialog.rb +640 -0
  67. data/lib/alexandria/ui/dialogs/alert_dialog.rb +68 -0
  68. data/lib/alexandria/ui/dialogs/bad_isbns_dialog.rb +43 -0
  69. data/lib/alexandria/ui/dialogs/barcode_animation.rb +159 -0
  70. data/lib/alexandria/ui/dialogs/book_properties_dialog.rb +210 -0
  71. data/lib/alexandria/ui/dialogs/book_properties_dialog_base.rb +432 -0
  72. data/lib/alexandria/ui/dialogs/export_dialog.rb +172 -0
  73. data/lib/alexandria/ui/dialogs/import_dialog.rb +205 -0
  74. data/lib/alexandria/ui/dialogs/misc_dialogs.rb +85 -0
  75. data/lib/alexandria/ui/dialogs/new_book_dialog.rb +639 -0
  76. data/lib/alexandria/ui/dialogs/new_book_dialog_manual.rb +153 -0
  77. data/lib/alexandria/ui/dialogs/new_smart_library_dialog.rb +67 -0
  78. data/lib/alexandria/ui/dialogs/preferences_dialog.rb +587 -0
  79. data/lib/alexandria/ui/dialogs/smart_library_properties_dialog.rb +56 -0
  80. data/lib/alexandria/ui/dialogs/smart_library_properties_dialog_base.rb +432 -0
  81. data/lib/alexandria/ui/dndable.rb +81 -0
  82. data/lib/alexandria/ui/gtk_thread_help.rb +88 -0
  83. data/lib/alexandria/ui/icons.rb +100 -0
  84. data/lib/alexandria/ui/iconview.rb +91 -0
  85. data/lib/alexandria/ui/iconview_tooltips.rb +162 -0
  86. data/lib/alexandria/ui/init.rb +93 -0
  87. data/lib/alexandria/ui/libraries_combo.rb +75 -0
  88. data/lib/alexandria/ui/listview.rb +310 -0
  89. data/lib/alexandria/ui/main_app.rb +67 -0
  90. data/lib/alexandria/ui/multi_drag_treeview.rb +150 -0
  91. data/lib/alexandria/ui/sidepane.rb +194 -0
  92. data/lib/alexandria/ui/sound.rb +107 -0
  93. data/lib/alexandria/ui/ui_manager.rb +1308 -0
  94. data/lib/alexandria/undo_manager.rb +78 -0
  95. data/lib/alexandria/utils.rb +30 -0
  96. data/lib/alexandria/version.rb +24 -0
  97. data/lib/alexandria/web_themes.rb +75 -0
  98. data/misc/sounds/README +15 -0
  99. data/misc/sounds/bad_scan.csd +62 -0
  100. data/misc/sounds/good_scan.csd +61 -0
  101. data/misc/sounds/scanning.csd +46 -0
  102. data/po/ChangeLog +488 -0
  103. data/po/Makefile +44 -0
  104. data/po/README +29 -0
  105. data/po/commit-po +72 -0
  106. data/po/cs.po +1437 -0
  107. data/po/cy.po +1521 -0
  108. data/po/de.po +1400 -0
  109. data/po/el.po +1379 -0
  110. data/po/es.po +1376 -0
  111. data/po/fr.po +1420 -0
  112. data/po/ga.po +1359 -0
  113. data/po/gl.po +1397 -0
  114. data/po/it.po +1406 -0
  115. data/po/ja.po +1355 -0
  116. data/po/mk.po +1373 -0
  117. data/po/nb.po +1386 -0
  118. data/po/nl.po +1405 -0
  119. data/po/pl.po +1373 -0
  120. data/po/pt.po +1398 -0
  121. data/po/pt_BR.po +1409 -0
  122. data/po/ru.po +1372 -0
  123. data/po/sk.po +1380 -0
  124. data/po/sv.po +1402 -0
  125. data/po/uk.po +1423 -0
  126. data/po/zh_TW.po +1394 -0
  127. data/schemas/alexandria.schemas +300 -0
  128. data/share/alexandria/glade/acquire_dialog__builder.glade +201 -0
  129. data/share/alexandria/glade/book_properties_dialog__builder.glade +910 -0
  130. data/share/alexandria/glade/main_app__builder.glade +229 -0
  131. data/share/alexandria/glade/new_book_dialog__builder.glade +379 -0
  132. data/share/alexandria/glade/preferences_dialog__builder.glade +733 -0
  133. data/share/alexandria/icons/alexandria.png +0 -0
  134. data/share/alexandria/icons/alexandria_small.png +0 -0
  135. data/share/alexandria/icons/book.png +0 -0
  136. data/share/alexandria/icons/book_icon.png +0 -0
  137. data/share/alexandria/icons/book_small.png +0 -0
  138. data/share/alexandria/icons/cuecat.png +0 -0
  139. data/share/alexandria/icons/cuecat_inactive.png +0 -0
  140. data/share/alexandria/icons/favorite_tag.png +0 -0
  141. data/share/alexandria/icons/less.png +0 -0
  142. data/share/alexandria/icons/library.png +0 -0
  143. data/share/alexandria/icons/library_small.png +0 -0
  144. data/share/alexandria/icons/lookup.png +0 -0
  145. data/share/alexandria/icons/more.png +0 -0
  146. data/share/alexandria/icons/no_cover.png +0 -0
  147. data/share/alexandria/icons/smart_library.png +0 -0
  148. data/share/alexandria/icons/smart_library_small.png +0 -0
  149. data/share/alexandria/icons/star_set.png +0 -0
  150. data/share/alexandria/icons/star_unset.png +0 -0
  151. data/share/alexandria/icons/view_as_icons.png +0 -0
  152. data/share/alexandria/icons/view_as_list.png +0 -0
  153. data/share/alexandria/ui/menus.xml +91 -0
  154. data/share/alexandria/ui/popups.xml +91 -0
  155. data/share/alexandria/web-themes/clean/clean.css +85 -0
  156. data/share/alexandria/web-themes/clean/preview.jpg +0 -0
  157. data/share/alexandria/web-themes/list/list.css +105 -0
  158. data/share/alexandria/web-themes/list/preview.jpg +0 -0
  159. data/share/app-icon/16x16/alexandria.png +0 -0
  160. data/share/app-icon/16x16/alexandria.svg +263 -0
  161. data/share/app-icon/22x22/alexandria.png +0 -0
  162. data/share/app-icon/22x22/alexandria.svg +465 -0
  163. data/share/app-icon/24x24/alexandria.png +0 -0
  164. data/share/app-icon/32x32/alexandria.png +0 -0
  165. data/share/app-icon/32x32/alexandria.svg +813 -0
  166. data/share/app-icon/32x32/alexandria.xpm +241 -0
  167. data/share/app-icon/48x48/alexandria.png +0 -0
  168. data/share/app-icon/scalable/alexandria.svg +700 -0
  169. data/share/gnome/help/alexandria/C/about.xml +44 -0
  170. data/share/gnome/help/alexandria/C/adding-books.xml +339 -0
  171. data/share/gnome/help/alexandria/C/alexandria.xml +185 -0
  172. data/share/gnome/help/alexandria/C/bugs.xml +18 -0
  173. data/share/gnome/help/alexandria/C/editing-book-properties.xml +124 -0
  174. data/share/gnome/help/alexandria/C/exporting.xml +81 -0
  175. data/share/gnome/help/alexandria/C/figures/adding_books_acquire_from_scanner_process.png +0 -0
  176. data/share/gnome/help/alexandria/C/figures/adding_books_add_by_isbn.png +0 -0
  177. data/share/gnome/help/alexandria/C/figures/adding_books_isbn_import.png +0 -0
  178. data/share/gnome/help/alexandria/C/figures/adding_books_manual_details.png +0 -0
  179. data/share/gnome/help/alexandria/C/figures/adding_books_rename_library_after_import.png +0 -0
  180. data/share/gnome/help/alexandria/C/figures/adding_books_search_results.png +0 -0
  181. data/share/gnome/help/alexandria/C/figures/editing_book_properties_info.png +0 -0
  182. data/share/gnome/help/alexandria/C/figures/editing_book_properties_loaning.png +0 -0
  183. data/share/gnome/help/alexandria/C/figures/exporting_information_html.png +0 -0
  184. data/share/gnome/help/alexandria/C/figures/getting_started_first_launched.png +0 -0
  185. data/share/gnome/help/alexandria/C/figures/searching_filtering_views_list_view.png +0 -0
  186. data/share/gnome/help/alexandria/C/figures/searching_filtering_views_list_view_search.png +0 -0
  187. data/share/gnome/help/alexandria/C/figures/settings_providers_new_z3950.png +0 -0
  188. data/share/gnome/help/alexandria/C/figures/smart_libraries_new_smart_library.png +0 -0
  189. data/share/gnome/help/alexandria/C/figures/working_with_libraries_library_pane.png +0 -0
  190. data/share/gnome/help/alexandria/C/getting-started.xml +154 -0
  191. data/share/gnome/help/alexandria/C/gnu-fdl-1.2.xml +543 -0
  192. data/share/gnome/help/alexandria/C/introduction.xml +142 -0
  193. data/share/gnome/help/alexandria/C/searching.xml +90 -0
  194. data/share/gnome/help/alexandria/C/settings.xml +140 -0
  195. data/share/gnome/help/alexandria/C/smart-libraries.xml +160 -0
  196. data/share/gnome/help/alexandria/C/working-with-libraries.xml +76 -0
  197. data/share/gnome/help/alexandria/ChangeLog +99 -0
  198. data/share/gnome/help/alexandria/fr/alexandria.xml +2292 -0
  199. data/share/gnome/help/alexandria/fr/figures/alexandria_add_button.png +0 -0
  200. data/share/gnome/help/alexandria/fr/figures/alexandria_add_by_isbn_1.png +0 -0
  201. data/share/gnome/help/alexandria/fr/figures/alexandria_add_by_search_1.png +0 -0
  202. data/share/gnome/help/alexandria/fr/figures/alexandria_add_manually.png +0 -0
  203. data/share/gnome/help/alexandria/fr/figures/alexandria_add_z3950.png +0 -0
  204. data/share/gnome/help/alexandria/fr/figures/alexandria_close_button.png +0 -0
  205. data/share/gnome/help/alexandria/fr/figures/alexandria_edit_info.png +0 -0
  206. data/share/gnome/help/alexandria/fr/figures/alexandria_export_web_page.png +0 -0
  207. data/share/gnome/help/alexandria/fr/figures/alexandria_importing.png +0 -0
  208. data/share/gnome/help/alexandria/fr/figures/alexandria_library_pane.png +0 -0
  209. data/share/gnome/help/alexandria/fr/figures/alexandria_list_view.png +0 -0
  210. data/share/gnome/help/alexandria/fr/figures/alexandria_list_view_search.png +0 -0
  211. data/share/gnome/help/alexandria/fr/figures/alexandria_loaning.png +0 -0
  212. data/share/gnome/help/alexandria/fr/figures/alexandria_main_window.png +0 -0
  213. data/share/gnome/help/alexandria/fr/figures/alexandria_remove_button.png +0 -0
  214. data/share/gnome/help/alexandria/ja/about.xml +33 -0
  215. data/share/gnome/help/alexandria/ja/adding-books.xml +314 -0
  216. data/share/gnome/help/alexandria/ja/alexandria.xml +172 -0
  217. data/share/gnome/help/alexandria/ja/bugs.xml +11 -0
  218. data/share/gnome/help/alexandria/ja/editing-book-properties.xml +100 -0
  219. data/share/gnome/help/alexandria/ja/exporting.xml +98 -0
  220. data/share/gnome/help/alexandria/ja/figures/adding_books_acquire_from_scanner_process.png +0 -0
  221. data/share/gnome/help/alexandria/ja/figures/adding_books_add_by_isbn.png +0 -0
  222. data/share/gnome/help/alexandria/ja/figures/adding_books_isbn_import.png +0 -0
  223. data/share/gnome/help/alexandria/ja/figures/adding_books_manual_details.png +0 -0
  224. data/share/gnome/help/alexandria/ja/figures/adding_books_rename_library_after_import.png +0 -0
  225. data/share/gnome/help/alexandria/ja/figures/adding_books_search_results.png +0 -0
  226. data/share/gnome/help/alexandria/ja/figures/editing_book_properties_info.png +0 -0
  227. data/share/gnome/help/alexandria/ja/figures/editing_book_properties_loaning.png +0 -0
  228. data/share/gnome/help/alexandria/ja/figures/exporting_information_html.png +0 -0
  229. data/share/gnome/help/alexandria/ja/figures/getting_started_first_launched.png +0 -0
  230. data/share/gnome/help/alexandria/ja/figures/searching_filtering_views_list_view.png +0 -0
  231. data/share/gnome/help/alexandria/ja/figures/searching_filtering_views_list_view_search.png +0 -0
  232. data/share/gnome/help/alexandria/ja/figures/settings_providers_new_z3950.png +0 -0
  233. data/share/gnome/help/alexandria/ja/figures/smart_libraries_new_smart_library.png +0 -0
  234. data/share/gnome/help/alexandria/ja/figures/working_with_libraries_library_pane.png +0 -0
  235. data/share/gnome/help/alexandria/ja/getting-started.xml +144 -0
  236. data/share/gnome/help/alexandria/ja/gnu-fdl-1.2.xml +541 -0
  237. data/share/gnome/help/alexandria/ja/introduction.xml +134 -0
  238. data/share/gnome/help/alexandria/ja/searching.xml +104 -0
  239. data/share/gnome/help/alexandria/ja/settings.xml +129 -0
  240. data/share/gnome/help/alexandria/ja/smart-libraries.xml +140 -0
  241. data/share/gnome/help/alexandria/ja/working-with-libraries.xml +88 -0
  242. data/share/menu/alexandria +7 -0
  243. data/share/omf/alexandria/alexandria-C.omf.in +27 -0
  244. data/share/omf/alexandria/alexandria-fr.omf.in +30 -0
  245. data/share/sounds/alexandria/bad_scan.ogg +0 -0
  246. data/share/sounds/alexandria/bad_scan.wav +0 -0
  247. data/share/sounds/alexandria/good_scan.ogg +0 -0
  248. data/share/sounds/alexandria/good_scan.wav +0 -0
  249. data/share/sounds/alexandria/scanning.ogg +0 -0
  250. data/share/sounds/alexandria/scanning.wav +0 -0
  251. data/spec/alexandria/library_spec.rb +205 -0
  252. data/spec/alexandria/preferences_spec.rb +22 -0
  253. data/spec/alexandria/scanners/cuecat_spec.rb +68 -0
  254. data/spec/alexandria/smart_library_spec.rb +22 -0
  255. data/spec/alexandria/ui/dialogs_spec.rb +90 -0
  256. data/spec/alexandria/ui/iconview_spec.rb +27 -0
  257. data/spec/alexandria/ui/listview_spec.rb +28 -0
  258. data/spec/alexandria/ui/main_app_spec.rb +48 -0
  259. data/spec/alexandria/ui/sidepane_spec.rb +27 -0
  260. data/spec/alexandria/ui/ui_manager_spec.rb +26 -0
  261. data/spec/alexandria/ui/ui_utilities_spec.rb +60 -0
  262. data/spec/alexandria/utilities_spec.rb +50 -0
  263. data/spec/data/libraries/0.6.1-noisbn/My Library/0201398257.yaml +10 -0
  264. data/spec/data/libraries/0.6.1-noisbn/My Library/1565920007.yaml +10 -0
  265. data/spec/data/libraries/0.6.1/My Library/0192812173.yaml +9 -0
  266. data/spec/data/libraries/0.6.1/My Library/0201398257.cover +0 -0
  267. data/spec/data/libraries/0.6.1/My Library/0201398257.yaml +10 -0
  268. data/spec/data/libraries/0.6.1/My Library/1565920007.yaml +10 -0
  269. data/spec/data/libraries/0.6.2/My Library/9780140266146.cover +0 -0
  270. data/spec/data/libraries/0.6.2/My Library/9780140266146.yaml +16 -0
  271. data/spec/data/libraries/0.6.2/My Library/9780140278781.cover +0 -0
  272. data/spec/data/libraries/0.6.2/My Library/9780140278781.yaml +21 -0
  273. data/spec/data/libraries/0.6.2/My Library/9780571147168.cover +0 -0
  274. data/spec/data/libraries/0.6.2/My Library/9780571147168.yaml +20 -0
  275. data/spec/data/libraries/0.6.2/My Library/9780575079038.cover +0 -0
  276. data/spec/data/libraries/0.6.2/My Library/9780575079038.yaml +20 -0
  277. data/spec/data/libraries/0.6.2/My Library/9780755322800.cover +0 -0
  278. data/spec/data/libraries/0.6.2/My Library/9780755322800.yaml +20 -0
  279. data/spec/spec_helper.rb +46 -0
  280. data/tasks/rdoc.rake +6 -0
  281. data/tasks/setup.rb +30 -0
  282. data/tasks/spec.rake +29 -0
  283. data/tasks/test.rake +38 -0
  284. data/test/application_test.rb +39 -0
  285. data/test/book_test.rb +34 -0
  286. data/test/data/isbns.txt +3 -0
  287. data/test/isbn_test.rb +68 -0
  288. data/test/providers_test.rb +254 -0
  289. data/test/test_helper.rb +42 -0
  290. data/util/rake/fileinstall.rb +313 -0
  291. data/util/rake/gettextgenerate.rb +158 -0
  292. data/util/rake/omfgenerate.rb +79 -0
  293. metadata +452 -0
@@ -0,0 +1,1308 @@
1
+ # Copyright (C) 2004-2006 Laurent Sansonetti
2
+ # Copyright (C) 2008 Joseph Method
3
+ # Modifications Copyright (C) 2011 Matijs van Zuijlen
4
+ #
5
+ # Alexandria is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU General Public License as
7
+ # published by the Free Software Foundation; either version 2 of the
8
+ # License, or (at your option) any later version.
9
+ #
10
+ # Alexandria is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
+ # General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public
16
+ # License along with Alexandria; see the file COPYING. If not,
17
+ # write to the Free Software Foundation, Inc., 51 Franklin Street,
18
+ # Fifth Floor, Boston, MA 02110-1301 USA.
19
+
20
+ module Alexandria
21
+ module UI
22
+ MAX_RATING_STARS = 5
23
+ class UIManager < BuilderBase
24
+ attr_accessor :main_app, :actiongroup, :appbar, :prefs, :listview, :iconview, :listview_model,
25
+ :iconview_model, :filtered_model
26
+ attr_reader :model
27
+ include Logging
28
+ include GetText
29
+ GetText.bindtextdomain(Alexandria::TEXTDOMAIN, charset: 'UTF-8')
30
+
31
+ module Columns
32
+ COVER_LIST, COVER_ICON, TITLE, TITLE_REDUCED, AUTHORS,
33
+ ISBN, PUBLISHER, PUBLISH_DATE, EDITION, RATING, IDENT,
34
+ NOTES, REDD, OWN, WANT, TAGS, LOANED_TO = (0..17).to_a # duplicated from listview.rb
35
+ end
36
+
37
+ # The maximum number of rating stars displayed.
38
+ MAX_RATING_STARS = 5
39
+
40
+ def initialize(parent)
41
+ super('main_app__builder.glade', widget_names)
42
+ @parent = parent
43
+
44
+ @library_separator_iter = nil
45
+ @libraries = nil
46
+ @move_mid = nil
47
+ @clicking_on_sidepane = true
48
+
49
+ get_preferences
50
+ load_libraries
51
+ setup_window_icons
52
+ setup_callbacks
53
+ create_uimanager
54
+ add_menus_and_popups_from_xml
55
+ setup_menus
56
+ setup_toolbar
57
+ setup_move_actions
58
+ setup_active_model
59
+ setup_dependents
60
+ setup_accel_group
61
+ setup_popups
62
+ setup_window_events
63
+ setup_dialog_hooks
64
+ setup_books_iconview_sorting
65
+ on_books_selection_changed
66
+ restore_preferences
67
+ log.debug { "UI Manager initialized: #{@iconview.model.inspect}" }
68
+ @clicking_on_sidepane = true
69
+
70
+ @library_listview.signal_connect('cursor-changed') do
71
+ @clicking_on_sidepane = true
72
+ end
73
+ end
74
+
75
+ def show
76
+ @main_app.show
77
+ end
78
+
79
+ def widget_names
80
+ [:main_app, :paned, :vbox1, :library_listview,
81
+ :notebook, :iconview, :listview, :status_label, :appbar,
82
+ :progressbar]
83
+ end
84
+
85
+ def create_uimanager
86
+ log.debug { 'Adding actiongroup to uimanager' }
87
+ @uimanager = Gtk::UIManager.new
88
+ @uimanager.insert_action_group(@actiongroup, 0)
89
+ end
90
+
91
+ def setup_dependents
92
+ @listview_model = Gtk::TreeModelSort.new(@filtered_model)
93
+ @iconview_model = Gtk::TreeModelSort.new(@filtered_model)
94
+ @listview_manager = ListViewManager.new @listview, self
95
+ @iconview_manager = IconViewManager.new @iconview, self
96
+ @sidepane_manager = SidePaneManager.new @library_listview, self
97
+ @library_listview = @sidepane_manager.library_listview
98
+ @listview_manager.setup_listview_columns_visibility
99
+ @listview_manager.setup_listview_columns_width
100
+ end
101
+
102
+ def setup_callbacks
103
+ require 'alexandria/ui/callbacks'
104
+ self.class.send(:include, Callbacks)
105
+ connect_signals
106
+ end
107
+
108
+ def get_preferences
109
+ @prefs = Preferences.instance
110
+ end
111
+
112
+ def setup_toolbar
113
+ log.debug { 'setup_toolbar' }
114
+ setup_book_providers
115
+ add_main_toolbar_items
116
+ @toolbar = @uimanager.get_widget('/MainToolbar')
117
+ @toolbar.show_arrow = true
118
+ @toolbar.insert(-1, Gtk::SeparatorToolItem.new)
119
+ setup_toolbar_combobox
120
+ setup_toolbar_filter_entry
121
+ @toolbar.insert(-1, Gtk::SeparatorToolItem.new)
122
+ setup_toolbar_viewas
123
+ @toolbar.show_all
124
+ @actiongroup['Undo'].sensitive = @actiongroup['Redo'].sensitive = false
125
+ UndoManager.instance.add_observer(self)
126
+ @vbox1.add(@toolbar, position: 1, expand: false, fill: false)
127
+ end
128
+
129
+ def add_main_toolbar_items
130
+ mid = @uimanager.new_merge_id
131
+ @uimanager.add_ui(mid, 'ui/', 'MainToolbar', 'MainToolbar',
132
+ Gtk::UIManager::TOOLBAR, false)
133
+ @uimanager.add_ui(mid, 'ui/MainToolbar/', 'New', 'New',
134
+ Gtk::UIManager::TOOLITEM, false)
135
+ @uimanager.add_ui(mid, 'ui/MainToolbar/', 'AddBook', 'AddBook',
136
+ Gtk::UIManager::TOOLITEM, false)
137
+ # @uimanager.add_ui(mid, "ui/MainToolbar/", "sep", "sep",
138
+ # Gtk::UIManager::SEPARATOR, false)
139
+ # @uimanager.add_ui(mid, "ui/MainToolbar/", "Refresh", "Refresh",
140
+ # Gtk::UIManager::TOOLITEM, false)
141
+ end
142
+
143
+ def setup_toolbar_filter_entry
144
+ @filter_entry = Gtk::Entry.new
145
+ @filter_entry.signal_connect('changed', &method(:on_toolbar_filter_entry_changed))
146
+ @toolitem = Gtk::ToolItem.new
147
+ @toolitem.expand = true
148
+ @toolitem.border_width = 5
149
+ @tooltips.set_tip(@filter_entry,
150
+ _('Type here the search criterion'), nil)
151
+ @toolitem << @filter_entry
152
+ @toolbar.insert(-1, @toolitem)
153
+ end
154
+
155
+ def setup_toolbar_combobox
156
+ @tooltips = Gtk::Tooltips.new
157
+
158
+ cb = Gtk::ComboBox.new
159
+ cb.set_row_separator_func do |_model, iter|
160
+ # log.debug { "row_separator" }
161
+ iter[0] == '-'
162
+ end
163
+ [_('Match everything'),
164
+ '-',
165
+ _('Title contains'),
166
+ _('Authors contain'),
167
+ _('ISBN contains'),
168
+ _('Publisher contains'),
169
+ _('Notes contain'),
170
+ _('Tags contain')
171
+ ].each do |item|
172
+ cb.append_text(item)
173
+ end
174
+ cb.active = 0
175
+ cb.signal_connect('changed', &method(:on_criterion_combobox_changed))
176
+
177
+ # Put the combo box in a event box because it is not currently
178
+ # possible assign a tooltip to a combo box.
179
+ eb = Gtk::EventBox.new
180
+ eb << cb
181
+ @toolitem = Gtk::ToolItem.new
182
+ @toolitem.border_width = 5
183
+ @toolitem << eb
184
+ @toolbar.insert(-1, @toolitem)
185
+ @tooltips.set_tip(eb, _('Change the search type'), nil)
186
+ end
187
+
188
+ def setup_toolbar_viewas
189
+ @toolbar_view_as = Gtk::ComboBox.new
190
+ @toolbar_view_as.append_text(_('View as Icons'))
191
+ @toolbar_view_as.append_text(_('View as List'))
192
+ @toolbar_view_as.active = 0
193
+ @toolbar_view_as_signal_hid = \
194
+ @toolbar_view_as.signal_connect('changed', &method(:on_toolbar_view_as_changed))
195
+
196
+ # Put the combo box in a event box because it is not currently
197
+ # possible assign a tooltip to a combo box.
198
+ eb = Gtk::EventBox.new
199
+ eb << @toolbar_view_as
200
+ @toolitem = Gtk::ToolItem.new
201
+ @toolitem.border_width = 5
202
+ @toolitem << eb
203
+ @toolbar.insert(-1, @toolitem)
204
+ @tooltips.set_tip(eb, _('Choose how to show books'), nil)
205
+ end
206
+
207
+ def setup_book_providers
208
+ log.debug { 'setup_book_providers' }
209
+ mid = @uimanager.new_merge_id
210
+ BookProviders.each do |provider|
211
+
212
+ name = provider.action_name
213
+ ['ui/MainMenubar/ViewMenu/OnlineInformation/',
214
+ 'ui/BookPopup/OnlineInformation/',
215
+ 'ui/NoBookPopup/OnlineInformation/'].each do |path|
216
+ log.debug { "Adding #{name} to #{path}" }
217
+ @uimanager.add_ui(mid, path, name, name,
218
+ Gtk::UIManager::MENUITEM, false)
219
+ end
220
+ end
221
+ end
222
+
223
+ def add_menus_and_popups_from_xml
224
+ log.debug { 'add_menus_and_popups_from_xml' }
225
+ ['menus.xml', 'popups.xml'].each do |ui_file|
226
+ @uimanager.add_ui(File.join(Alexandria::Config::DATA_DIR,
227
+ 'ui', ui_file))
228
+ end
229
+ end
230
+
231
+ def setup_accel_group
232
+ log.debug { 'setup_accel_group' }
233
+ @main_app.add_accel_group(@uimanager.accel_group)
234
+ end
235
+
236
+ def setup_menus
237
+ @menubar = @uimanager.get_widget('/MainMenubar')
238
+ @vbox1.add(@menubar, position: 0, expand: false, fill: false)
239
+ end
240
+
241
+ def setup_dialog_hooks
242
+ log.debug { 'setup_dialog_hooks' }
243
+ Gtk::AboutDialog.set_url_hook do |_about, link|
244
+ log.debug { 'set_url_hook' }
245
+ open_web_browser(link)
246
+ end
247
+ Gtk::AboutDialog.set_email_hook do |_about, link|
248
+ log.debug { 'set_email_hook' }
249
+ open_email_client('mailto:' + link)
250
+ end
251
+ end
252
+
253
+ def setup_popups
254
+ log.debug { 'setup_popups' }
255
+ @library_popup = @uimanager.get_widget('/LibraryPopup')
256
+ @smart_library_popup = @uimanager.get_widget('/SmartLibraryPopup')
257
+ @nolibrary_popup = @uimanager.get_widget('/NoLibraryPopup')
258
+ @book_popup = @uimanager.get_widget('/BookPopup')
259
+ @nobook_popup = @uimanager.get_widget('/NoBookPopup')
260
+ end
261
+
262
+ def setup_window_events
263
+ log.debug { 'setup_window_events' }
264
+ @main_app.signal_connect('window-state-event', &method(:on_window_state_event))
265
+ @main_app.signal_connect('destroy', &method(:on_window_destroy))
266
+ end
267
+
268
+ def setup_active_model
269
+ log.debug { 'setting up active model' }
270
+ # The active model.
271
+
272
+ list = [
273
+ Gdk::Pixbuf, # COVER_LIST
274
+ Gdk::Pixbuf, # COVER_ICON
275
+ String, # TITLE
276
+ String, # TITLE_REDUCED
277
+ String, # AUTHORS
278
+ String, # ISBN
279
+ String, # PUBLISHER
280
+ String, # PUBLISH_DATE
281
+ String, # EDITION
282
+ Integer, # RATING
283
+ String, # IDENT
284
+ String, # NOTES
285
+ TrueClass, # REDD
286
+ TrueClass, # OWN
287
+ TrueClass, # WANT
288
+ String, # TAGS
289
+ String # LOANED TO
290
+ ]
291
+
292
+ @model = Gtk::ListStore.new(*list)
293
+
294
+ # Filter books according to the search toolbar widgets.
295
+ @filtered_model = Gtk::TreeModelFilter.new(@model)
296
+ @filtered_model.set_visible_func do |_model, iter|
297
+ # log.debug { "visible_func" }
298
+ @filter_books_mode ||= 0
299
+ filter = @filter_entry.text
300
+ if filter.empty?
301
+ true
302
+ else
303
+ data = case @filter_books_mode
304
+ when 0 then
305
+ (iter[Columns::TITLE] or '') +
306
+ (iter[Columns::AUTHORS] or '') +
307
+ (iter[Columns::ISBN] or '') +
308
+ (iter[Columns::PUBLISHER] or '') +
309
+ (iter[Columns::NOTES] or '') +
310
+ (iter[Columns::TAGS] or '')
311
+ when 2 then iter[Columns::TITLE]
312
+ when 3 then iter[Columns::AUTHORS]
313
+ when 4 then iter[Columns::ISBN]
314
+ when 5 then iter[Columns::PUBLISHER]
315
+ when 6 then iter[Columns::NOTES]
316
+ when 7 then iter[Columns::TAGS]
317
+ end
318
+ !data.nil? and data.downcase.include?(filter.downcase)
319
+ end
320
+ end
321
+
322
+ # Give filter entry the initial keyboard focus.
323
+ @filter_entry.grab_focus
324
+ log.debug { 'done setting up active model' }
325
+ end
326
+
327
+ def on_library_button_press_event(widget, event)
328
+ log.debug { 'library_button_press_event' }
329
+
330
+ # right click
331
+ if event_is_right_click event
332
+ log.debug { 'library right click!' }
333
+ library_already_selected = true
334
+ if (path = widget.get_path_at_pos(event.x, event.y))
335
+ @clicking_on_sidepane = true
336
+ obj, path = widget.is_a?(Gtk::TreeView) ? [widget.selection, path.first] : [widget, path]
337
+ widget.has_focus = true
338
+
339
+ unless obj.path_is_selected?(path)
340
+
341
+ log.debug { "Select #{path}" }
342
+ library_already_selected = false
343
+ widget.unselect_all
344
+ obj.select_path(path)
345
+ sensitize_library selected_library
346
+
347
+ if widget.is_a?(Gtk::TreeView)
348
+ Gtk.idle_add do
349
+ # cur_path, focus_col = widget.cursor
350
+
351
+ widget.focus = true
352
+
353
+ widget.set_cursor(path, nil, false)
354
+ widget.grab_focus
355
+ widget.has_focus = true
356
+ false
357
+ end
358
+ # widget.has_focus = true
359
+ end
360
+
361
+ # library_already_selected = true
362
+ end
363
+ else
364
+ widget.unselect_all
365
+ end
366
+
367
+ menu = determine_library_popup widget, event
368
+
369
+ # Fixes part of bug #25021.
370
+ #
371
+ # If the library was not selected when it was right-clicked
372
+ # we should select the library first (we call on_focus
373
+ # manually, since the above call to obj.select_path(path) doesn't
374
+ # seem to suffice).
375
+ #
376
+ # Then we wait a while and only *then* pop up the menu.
377
+
378
+ if library_already_selected
379
+ sensitize_library selected_library
380
+
381
+ Gtk.idle_add do
382
+ menu.popup(nil, nil, event.button, event.time)
383
+ # @clicking_on_sidepane = false
384
+ false
385
+ end
386
+ else
387
+ Gtk.idle_add do
388
+ menu.popup(nil, nil, event.button, event.time)
389
+ # @clicking_on_sidepane = false
390
+
391
+ false
392
+ end
393
+ end
394
+
395
+ else
396
+ # not a right click
397
+ if (path = widget.get_path_at_pos(event.x, event.y))
398
+ @clicking_on_sidepane = true
399
+ obj, path = widget.is_a?(Gtk::TreeView) ? [widget.selection, path.first] : [widget, path]
400
+ obj.select_path(path)
401
+ sensitize_library selected_library
402
+
403
+ end
404
+
405
+ end
406
+ end
407
+
408
+ def determine_library_popup(widget, event)
409
+ # widget.grab_focus
410
+ widget.get_path_at_pos(event.x, event.y).nil? ? @nolibrary_popup : selected_library.is_a?(SmartLibrary) ? @smart_library_popup : @library_popup
411
+ end
412
+
413
+ def event_is_right_click(event)
414
+ event.event_type == Gdk::Event::BUTTON_PRESS and event.button == 3
415
+ end
416
+
417
+ def on_books_button_press_event(widget, event)
418
+ log.debug { 'books_button_press_event' }
419
+ if event_is_right_click event
420
+ widget.grab_focus
421
+
422
+ if (path = widget.get_path_at_pos(event.x.to_i, event.y.to_i))
423
+ obj, path = widget.is_a?(Gtk::TreeView) ? [widget.selection, path.first] : [widget, path]
424
+
425
+ unless obj.path_is_selected?(path)
426
+ log.debug { "Select #{path}" }
427
+ widget.unselect_all
428
+ obj.select_path(path)
429
+ end
430
+ else
431
+ widget.unselect_all
432
+ end
433
+
434
+ menu = (selected_books.empty?) ? @nobook_popup : @book_popup
435
+ menu.popup(nil, nil, event.button, event.time)
436
+ end
437
+ end
438
+
439
+ def get_library_selection_text(library)
440
+ case library.length
441
+ when 0
442
+ _("Library '%s' selected") % library.name
443
+
444
+ else
445
+ n_unrated = library.n_unrated
446
+ if n_unrated == library.length
447
+ n_("Library '%s' selected, %d unrated book",
448
+ "Library '%s' selected, %d unrated books",
449
+ library.length) % [library.name,
450
+ library.length]
451
+ elsif n_unrated == 0
452
+ n_("Library '%s' selected, %d book",
453
+ "Library '%s' selected, %d books",
454
+ library.length) % [library.name,
455
+ library.length]
456
+ else
457
+ n_("Library '%s' selected, %d book, " \
458
+ '%d unrated',
459
+ "Library '%s' selected, %d books, " \
460
+ '%d unrated',
461
+ library.length) % [library.name,
462
+ library.length,
463
+ n_unrated]
464
+ end
465
+ end
466
+ end
467
+
468
+ def get_appbar_status(library, books)
469
+ case books.length
470
+ when 0
471
+ get_library_selection_text library
472
+ when 1
473
+ _("'%s' selected") % books.first.title
474
+ else
475
+ n_('%d book selected', '%d books selected',
476
+ books.length) % books.length
477
+ end
478
+ end
479
+
480
+ def set_status_label(txt)
481
+ @status_label.text = txt
482
+ end
483
+
484
+ def on_books_selection_changed
485
+ library = selected_library
486
+ books = selected_books
487
+ set_status_label(get_appbar_status(library, books))
488
+ # selection = @library_listview.selection.selected ? @library_listview.selection.selected.has_focus? : false
489
+
490
+ # Focus is the wrong idiom here.
491
+ unless @clicking_on_sidepane or (@main_app.focus == @library_listview)
492
+ # unless @main_app.focus == @library_listview
493
+
494
+ log.debug { "Currently focused widget: #{@main_app.focus.inspect}" }
495
+ log.debug { "#{@library_listview} : #{@library_popup} : #{@listview}" }
496
+ log.debug { "@library_listview: #{@library_listview.has_focus?} or @library_popup:#{@library_popup.has_focus?}" } # or selection: #{selection}"}
497
+ log.debug { '@library_listview does *NOT* have focus' }
498
+ log.debug { "Books are empty: #{books.empty?}" }
499
+ @actiongroup['Properties'].sensitive = \
500
+ @actiongroup['OnlineInformation'].sensitive = \
501
+ books.length == 1
502
+ @actiongroup['SelectAll'].sensitive = \
503
+ books.length < library.length
504
+
505
+ @actiongroup['Delete'].sensitive = \
506
+ @actiongroup['DeselectAll'].sensitive = \
507
+ @actiongroup['Move'].sensitive =
508
+ @actiongroup['SetRating'].sensitive = !books.empty?
509
+
510
+ log.debug { "on_books_selection_changed Delete: #{@actiongroup['Delete'].sensitive?}" }
511
+
512
+ if library.is_a?(SmartLibrary)
513
+ @actiongroup['Delete'].sensitive =
514
+ @actiongroup['Move'].sensitive = false
515
+ end
516
+
517
+ # Sensitize providers URL
518
+ if books.length == 1
519
+ b = books.first
520
+ # FIXME: Clean up endless negation in this logic
521
+ no_urls = true
522
+ BookProviders.each do |provider|
523
+ has_no_url = true
524
+ begin
525
+ has_no_url = (b.isbn.nil? or b.isbn.strip.empty? or provider.url(b).nil?)
526
+ rescue => ex
527
+ log.warn { "Error determining URL from #{provider.name}; #{ex.message}" }
528
+ end
529
+ @actiongroup[provider.action_name].sensitive = !has_no_url
530
+ no_urls = false unless has_no_url
531
+ end
532
+ if no_urls
533
+ @actiongroup['OnlineInformation'].sensitive = false
534
+ end
535
+ end
536
+ end
537
+ @clicking_on_sidepane = false
538
+ end
539
+
540
+ def on_switch_page
541
+ log.debug { 'on_switch_page' }
542
+ @actiongroup['ArrangeIcons'].sensitive = @notebook.page == 0
543
+ on_books_selection_changed
544
+ end
545
+
546
+ def on_focus(widget, _event_focus)
547
+ if @clicking_on_sidepane or widget == @library_listview
548
+ log.debug { 'on_focus: @library_listview' }
549
+ Gtk.idle_add do
550
+ %w(OnlineInformation SelectAll DeselectAll).each do |action|
551
+ @actiongroup[action].sensitive = false
552
+ end
553
+ @actiongroup['Properties'].sensitive = selected_library.is_a?(SmartLibrary)
554
+ @actiongroup['Delete'].sensitive = determine_delete_option
555
+ false
556
+ end
557
+ else
558
+ on_books_selection_changed
559
+ end
560
+ end
561
+
562
+ def determine_delete_option
563
+ sensitive = (@libraries.all_regular_libraries.length > 1 or selected_library.is_a?(SmartLibrary))
564
+ sensitive
565
+ end
566
+
567
+ def on_close_sidepane
568
+ log.debug { 'on_close_sidepane' }
569
+ @actiongroup['Sidepane'].active = false
570
+ end
571
+
572
+ def select_a_book(book)
573
+ select_this_book = proc do |bk, view|
574
+ @filtered_model.refilter
575
+ iter = iter_from_book bk
576
+ unless iter
577
+ next
578
+ end
579
+ path = iter.path
580
+ unless view.model
581
+ next
582
+ end
583
+ path = view.model.convert_path_to_child_path(path)
584
+ path = @filtered_model.convert_path_to_child_path(path)
585
+ log.debug { "Path for #{bk.ident} is #{path}" }
586
+ selection = view.respond_to?(:selection) ? @listview.selection : @iconview
587
+ selection.unselect_all
588
+ selection.select_path(path)
589
+ end
590
+ begin
591
+ log.debug { 'select_a_book: listview' }
592
+ select_this_book.call(book, @listview)
593
+ log.debug { 'select_a_book: listview' }
594
+ select_this_book.call(book, @iconview)
595
+ rescue => ex
596
+ trace = ex.backtrace.join("\n> ")
597
+ log.warn { "Failed to automatically select book: #{ex.message} #{trace}" }
598
+ end
599
+ # TODO: Figure out why this frequently selects the wrong book!
600
+ end
601
+
602
+ def update(*ary)
603
+ log.debug { "on_update #{ary}" }
604
+ caller = ary.first
605
+ if caller.is_a?(UndoManager)
606
+ @actiongroup['Undo'].sensitive = caller.can_undo?
607
+ @actiongroup['Redo'].sensitive = caller.can_redo?
608
+ elsif caller.is_a?(Library)
609
+ unless caller.updating?
610
+ Gtk.queue do
611
+ handle_update_caller_library ary
612
+ end
613
+ end
614
+ else
615
+ raise 'unrecognized update event'
616
+ end
617
+ end
618
+
619
+ def handle_update_caller_library(ary)
620
+ library, kind, book = ary
621
+ if library == selected_library
622
+ @iconview.freeze # This makes @iconview.model == nil
623
+ @listview.freeze # NEW
624
+ case kind
625
+ when Library::BOOK_ADDED
626
+ append_book(book)
627
+ when Library::BOOK_UPDATED
628
+ iter = iter_from_ident(book.saved_ident)
629
+ if iter
630
+ fill_iter_with_book(iter, book)
631
+ end
632
+ when Library::BOOK_REMOVED
633
+ @model.remove(iter_from_book(book))
634
+ end
635
+ @iconview.unfreeze
636
+ @listview.unfreeze # NEW
637
+ select_a_book(book) if [Library::BOOK_ADDED, Library::BOOK_UPDATED].include? kind
638
+ elsif selected_library.is_a?(SmartLibrary)
639
+ refresh_books
640
+ end
641
+ end
642
+
643
+ #######
644
+ # private
645
+ #######
646
+
647
+ def open_web_browser(url)
648
+ if url.nil?
649
+ log.warn('Attempt to open browser with nil url')
650
+ return
651
+ end
652
+ if (cmd = Preferences.instance.www_browser)
653
+ launch_command = cmd
654
+ if cmd.downcase.index('%u')
655
+ launch_command = cmd.gsub(/%U/i, "\"" + url + "\"")
656
+ else
657
+ launch_command = cmd + " \"" + url + "\""
658
+ end
659
+ Thread.new { system(launch_command) }
660
+ else
661
+ ErrorDialog.new(@main_app,
662
+ _('Unable to launch the web browser'),
663
+ _('Check out that a web browser is ' \
664
+ 'configured as default (Desktop ' \
665
+ 'Preferences -> Advanced -> Preferred ' \
666
+ 'Applications) and try again.'))
667
+ end
668
+ end
669
+
670
+ def open_email_client(url)
671
+ if (cmd = Preferences.instance.email_client)
672
+ launch_command = cmd
673
+ if cmd.downcase.index('%u')
674
+ launch_command = cmd.gsub(/%u/i, "\"" + url + "\"")
675
+ else
676
+ launch_command = cmd + " \"" + url + "\""
677
+ end
678
+ Thread.new { system(launch_command) }
679
+ else
680
+ ErrorDialog.new(@main_app,
681
+ _('Unable to launch the mail reader'),
682
+ _('Check out that a mail reader is ' \
683
+ 'configured as default (Desktop ' \
684
+ 'Preferences -> Advanced -> Preferred ' \
685
+ 'Applications) and try again.'))
686
+ end
687
+ end
688
+
689
+ def detach_old_libraries
690
+ log.debug { 'Un-observing old libraries' }
691
+ @libraries.all_regular_libraries.each do |library|
692
+ if library.is_a?(Library)
693
+ library.delete_observer(self)
694
+ @completion_models.remove_source(library)
695
+ end
696
+ end
697
+ end
698
+
699
+ def load_libraries
700
+ log.info { 'Loading Libraries...' }
701
+ @completion_models = CompletionModels.instance
702
+ if @libraries
703
+ detach_old_libraries
704
+ @libraries.reload
705
+ else
706
+ @libraries = Libraries.instance
707
+ @libraries.reload
708
+ handle_ruined_books unless @libraries.ruined_books.empty?
709
+ end
710
+ @libraries.all_regular_libraries.each do |library|
711
+ library.add_observer(self)
712
+ @completion_models.add_source(library)
713
+ end
714
+ end
715
+
716
+ def handle_ruined_books
717
+ new_message = _("The data files for the following books are malformed or empty. Do you wish to attempt to download new information for them from the online book providers?\n")
718
+
719
+ # message = _("These books do not conform to the ISBN-13
720
+ # standard. We will attempt to replace them from the book
721
+ # providers. Otherwise, we will turn them into manual
722
+ # entries.\n" )
723
+
724
+ @libraries.ruined_books.each { |bi|
725
+ new_message += "\n#{bi[1] or bi[1].inspect}"
726
+ }
727
+ recovery_dialog = Gtk::MessageDialog.new(@main_app, Gtk::Dialog::MODAL,
728
+ Gtk::MessageDialog::WARNING,
729
+ Gtk::MessageDialog::BUTTONS_OK_CANCEL,
730
+ new_message).show
731
+ recovery_dialog.signal_connect('response') do |_dialog, response_type|
732
+ recovery_dialog.destroy
733
+ if response_type == Gtk::Dialog::RESPONSE_OK
734
+ # progress indicator...
735
+ @progressbar.fraction = 0
736
+ @appbar.children.first.visible = true # show the progress bar
737
+
738
+ total_book_count = @libraries.ruined_books.size
739
+ fraction_per_book = 1.0 / total_book_count
740
+ prog_percentage = 0
741
+
742
+ @libraries.ruined_books.reverse!
743
+ Gtk.idle_add do
744
+ ruined_book = @libraries.ruined_books.pop
745
+ if ruined_book
746
+ book, isbn, library = ruined_book
747
+ begin
748
+ book_rslt = Alexandria::BookProviders.isbn_search(isbn.to_s)
749
+ book = book_rslt[0]
750
+ cover_uri = book_rslt[1]
751
+
752
+ # TODO if the book was saved okay, make sure the old
753
+ # empty yaml file doesn't stick around esp if doing
754
+ # isbn-10 --> isbn-13 conversion...
755
+ if isbn.size == 10
756
+ filename = library.yaml(isbn)
757
+ log.debug { "removing old file #{filename}" }
758
+ begin
759
+ File.delete(filename)
760
+ rescue => ex
761
+ log.error { "Could not delete empty file #{filename}" }
762
+ end
763
+ end
764
+
765
+ log.debug { "Trying to add #{book.title}, #{cover_uri} in library ''#{library.name}'" }
766
+ unless cover_uri.nil?
767
+ library.save_cover(book, cover_uri)
768
+ end
769
+ library << book
770
+ library.save(book)
771
+ set_status_label(_("Added '%s' to library '%s'") % [book.title, library.name])
772
+
773
+ rescue => ex
774
+ log.error { "Couldn't add book #{isbn}: #{ex}" }
775
+ log.error { ex.backtrace.join("\n") }
776
+ end
777
+
778
+ prog_percentage += fraction_per_book
779
+ @progressbar.fraction = prog_percentage
780
+
781
+ true
782
+ else
783
+ ## Totally copied and pasted from refresh_books...
784
+ ## call this the second strike... (CathalMagus)
785
+
786
+ # @iconview.unfreeze
787
+ # @filtered_model.refilter
788
+ # @listview.columns_autosize
789
+
790
+ @progressbar.fraction = 1
791
+ ## Hide the progress bar.
792
+ @appbar.children.first.visible = false
793
+ ## Refresh the status bar.
794
+ set_status_label('')
795
+ # on_books_selection_changed
796
+ false
797
+ end
798
+ end
799
+ end
800
+ end
801
+ end
802
+
803
+ def cache_scaled_icon(icon, width, height)
804
+ log.debug { "cache_scaled_icon #{icon}, #{width}, #{height}" }
805
+ @cache ||= {}
806
+ @cache[[icon, width, height]] ||= icon.scale(width, height)
807
+ end
808
+
809
+ ICON_TITLE_MAXLEN = 20 # characters
810
+ ICON_HEIGHT = 90 # pixels
811
+ REDUCE_TITLE_REGEX = Regexp.new("^(.{#{ICON_TITLE_MAXLEN}}).*$")
812
+
813
+ def fill_iter_with_book(iter, book)
814
+ log.debug { "fill iter #{iter} with book #{book}" }
815
+ iter[Columns::IDENT] = book.ident.to_s
816
+ iter[Columns::TITLE] = book.title
817
+ title = book.title.sub(REDUCE_TITLE_REGEX, '\1...')
818
+ iter[Columns::TITLE_REDUCED] = title
819
+ iter[Columns::AUTHORS] = book.authors.join(', ')
820
+ iter[Columns::ISBN] = book.isbn.to_s
821
+ iter[Columns::PUBLISHER] = book.publisher
822
+ iter[Columns::PUBLISH_DATE] = book.publishing_year.to_s
823
+ iter[Columns::EDITION] = book.edition
824
+ iter[Columns::NOTES] = (book.notes or '')
825
+ iter[Columns::LOANED_TO] = (book.loaned_to or '')
826
+ rating = (book.rating or Book::DEFAULT_RATING)
827
+ iter[Columns::RATING] = MAX_RATING_STARS - rating # ascending order is the default
828
+ iter[Columns::OWN] = book.own?
829
+ iter[Columns::REDD] = book.redd?
830
+ iter[Columns::WANT] = book.want?
831
+ if book.tags
832
+ iter[Columns::TAGS] = book.tags.join(',')
833
+ else
834
+ iter[Columns::TAGS] = ''
835
+ end
836
+
837
+ icon = Icons.cover(selected_library, book)
838
+ log.debug { "Setting icon #{icon} for book #{book.title}" }
839
+ iter[Columns::COVER_LIST] = cache_scaled_icon(icon, 20, 25)
840
+
841
+ if icon.height > ICON_HEIGHT
842
+ new_width = icon.width / (icon.height / ICON_HEIGHT.to_f)
843
+ new_height = [ICON_HEIGHT, icon.height].min
844
+ icon = cache_scaled_icon(icon, new_width, new_height)
845
+ end
846
+ if rating == MAX_RATING_STARS
847
+ icon = icon.tag(Icons::FAVORITE_TAG)
848
+ end
849
+ iter[Columns::COVER_ICON] = icon
850
+ log.debug { 'Full iter: ' + (0..15).map { |num| iter[num].inspect }.join(', ') }
851
+ end
852
+
853
+ def append_book(book, _tail = nil)
854
+ log.debug { @model.inspect }
855
+ iter = @model.append
856
+ log.debug { "iter == #{iter}" }
857
+ if iter
858
+ fill_iter_with_book(iter, book)
859
+ else
860
+ log.debug { '@model.append' }
861
+ iter = @model.append
862
+ fill_iter_with_book(iter, book)
863
+ log.debug { "no iter for book #{book}" }
864
+ end
865
+ library = selected_library
866
+ if library.deleted_books.include?(book)
867
+ log.debug { "Stop! Don't delete this book! We re-added it!" }
868
+ library.undelete(book)
869
+ UndoManager.instance.push { undoable_delete(library, [book]) }
870
+ end
871
+ iter
872
+ end
873
+
874
+ def append_library(library, autoselect = false)
875
+ log.debug { "append_library #{library.name}" }
876
+ model = @library_listview.model
877
+ is_smart = library.is_a?(SmartLibrary)
878
+ if is_smart
879
+ if @library_separator_iter.nil?
880
+ @library_separator_iter = append_library_separator
881
+ end
882
+ iter = model.append
883
+ else
884
+ iter = if @library_separator_iter.nil?
885
+ model.append
886
+ else
887
+ model.insert_before(@library_separator_iter)
888
+ end
889
+ end
890
+
891
+ iter[0] = is_smart ? Icons::SMART_LIBRARY_SMALL : Icons::LIBRARY_SMALL
892
+ iter[1] = library.name
893
+ iter[2] = true # editable?
894
+ iter[3] = false # separator?
895
+ if autoselect
896
+ @library_listview.set_cursor(iter.path,
897
+ @library_listview.get_column(0),
898
+ true)
899
+ @actiongroup['Sidepane'].active = true
900
+ end
901
+ iter
902
+ end
903
+
904
+ def append_library_separator
905
+ log.debug { 'append_library_separator' }
906
+ iter = @library_listview.model.append
907
+ iter[0] = nil
908
+ iter[1] = nil
909
+ iter[2] = false # editable?
910
+ iter[3] = true # separator?
911
+ iter
912
+ end
913
+
914
+ def refresh_books
915
+ log.debug { 'refresh_books' }
916
+ @library_listview.set_sensitive(false)
917
+ library = selected_library
918
+ @model.clear
919
+ @iconview.freeze
920
+ @listview.freeze # NEW / bdewey
921
+ @progressbar.fraction = 0
922
+ @appbar.children.first.visible = true # show the progress bar
923
+ set_status_label(_("Loading '%s'...") % library.name)
924
+ total = library.length
925
+ log.debug { "library #{library.name} length #{library.length}" }
926
+ n = 0
927
+
928
+ Gtk.idle_add do
929
+
930
+ block_return = true
931
+ book = library[n]
932
+ if book
933
+ Gtk.queue do
934
+ begin
935
+ append_book(book)
936
+ rescue => ex
937
+ trace = ex.backtrace.join("\n > ")
938
+ log.error { "append_books failed #{ex.message} #{trace}" }
939
+ end
940
+ # convert to percents
941
+ coeff = total / 100.0
942
+ percent = n / coeff
943
+ fraction = percent / 100
944
+ log.debug { "#index #{n} percent #{percent} fraction #{fraction}" }
945
+ @progressbar.fraction = fraction
946
+ n += 1
947
+ end
948
+ else
949
+ Gtk.queue do
950
+ @iconview.unfreeze
951
+ @listview.unfreeze # NEW / bdewey
952
+ @filtered_model.refilter
953
+ @listview.columns_autosize
954
+ @progressbar.fraction = 1
955
+ # Hide the progress bar.
956
+ @appbar.children.first.visible = false
957
+ # Refresh the status bar.
958
+ on_books_selection_changed
959
+ @library_listview.set_sensitive(true)
960
+ block_return = false
961
+ end
962
+ end
963
+
964
+ block_return
965
+ end
966
+ end
967
+
968
+ def selected_library
969
+ log.debug { 'selected_library' }
970
+ if (iter = @library_listview.selection.selected)
971
+ @libraries.all_libraries.find { |x| x.name == iter[1] }
972
+ else
973
+ @libraries.all_libraries.first
974
+ end
975
+ end
976
+
977
+ def select_library(library)
978
+ log.debug { "select library #{library}" }
979
+ iter = @library_listview.model.iter_first
980
+ ok = true
981
+ while ok
982
+ if iter[1] == library.name
983
+ @library_listview.selection.select_iter(iter)
984
+ break
985
+ end
986
+ ok = iter.next!
987
+ end
988
+ end
989
+
990
+ def book_from_iter(library, iter)
991
+ log.debug { "Book from iter: #{library} #{iter}" }
992
+ library.find { |x| x.ident == iter[Columns::IDENT] }
993
+ end
994
+
995
+ def iter_from_ident(ident)
996
+ log.debug { "#{ident}" }
997
+ iter = @model.iter_first
998
+ ok = true
999
+ while ok
1000
+ if iter[Columns::IDENT] == ident
1001
+ return iter
1002
+ end
1003
+ ok = iter.next!
1004
+ end
1005
+ nil
1006
+ end
1007
+
1008
+ def iter_from_book(book)
1009
+ log.debug { "#{book}" }
1010
+ iter_from_ident(book.ident)
1011
+ end
1012
+
1013
+ def collate_selected_books(page)
1014
+ a = []
1015
+ library = selected_library
1016
+ view = page == 0 ? @iconview : @listview
1017
+ selection = page == 0 ? @iconview : @listview.selection
1018
+
1019
+ selection.selected_each do |_the_view, path|
1020
+ # don't use the_view which is passed in by this block
1021
+ # as it doesn't consider the filtering for some reason
1022
+ # see bug #24568
1023
+ unless view.model
1024
+ next
1025
+ end
1026
+ path = view.model.convert_path_to_child_path(path)
1027
+ if path
1028
+ path = @filtered_model.convert_path_to_child_path(path)
1029
+ # FIX this sometimes returns a nil path for iconview...
1030
+ if path
1031
+ iter = @model.get_iter(path)
1032
+ if iter
1033
+ a << book_from_iter(library, iter)
1034
+ end
1035
+ end
1036
+ # This used to cause a crash when manually adding the first
1037
+ # book to a Library displayed in an Iconview
1038
+ # TODO find root cause of this
1039
+ end
1040
+ end
1041
+ a
1042
+ end
1043
+
1044
+ def selected_books
1045
+ a = collate_selected_books(@notebook.page)
1046
+ selected = a.select { |x| !x.nil? }
1047
+ log.debug { "Selected books = #{selected.inspect}" }
1048
+ selected
1049
+ end
1050
+
1051
+ def refresh_libraries
1052
+ log.debug { 'refresh_libraries' }
1053
+ library = selected_library
1054
+
1055
+ # Change the application's title.
1056
+ @main_app.title = library.name + ' - ' + TITLE
1057
+
1058
+ # Disable the selected library in the move libraries actions.
1059
+ @libraries.all_regular_libraries.each do |i_library|
1060
+ action = @actiongroup[i_library.action_name]
1061
+ if action
1062
+ action.sensitive = i_library != library
1063
+ end
1064
+ end
1065
+ sensitize_library library
1066
+ end
1067
+
1068
+ def sensitize_library(library)
1069
+ smart = library.is_a?(SmartLibrary)
1070
+ log.debug { "sensitize_library: smartlibrary = #{smart}" }
1071
+ Gtk.idle_add do
1072
+ @actiongroup['AddBook'].sensitive = !smart
1073
+ @actiongroup['AddBookManual'].sensitive = !smart
1074
+ @actiongroup['Properties'].sensitive = smart
1075
+ can_delete = smart || (@libraries.all_regular_libraries.length > 1)
1076
+ @actiongroup['Delete'].sensitive = can_delete ## true #(@libraries.all_regular_libraries.length > 1)
1077
+ log.debug { "sensitize_library delete: #{@actiongroup['Delete'].sensitive?}" }
1078
+ false
1079
+ end
1080
+ end
1081
+
1082
+ def get_view_actiongroup
1083
+ case @prefs.view_as
1084
+ when 0
1085
+ @actiongroup['AsIcons']
1086
+ when 1
1087
+ @actiongroup['AsList']
1088
+ end
1089
+ end
1090
+
1091
+ def restore_preferences
1092
+ log.debug { 'Restoring preferences...' }
1093
+ if @prefs.maximized
1094
+ @main_app.maximize
1095
+ else
1096
+ @main_app.move(*@prefs.position) unless @prefs.position == [0, 0]
1097
+ @main_app.resize(*@prefs.size)
1098
+ @maximized = false
1099
+ end
1100
+ @paned.position = @prefs.sidepane_position
1101
+ @actiongroup['Sidepane'].active = @prefs.sidepane_visible
1102
+ @actiongroup['Toolbar'].active = @prefs.toolbar_visible
1103
+ @actiongroup['Statusbar'].active = @prefs.statusbar_visible
1104
+ @appbar.visible = @prefs.statusbar_visible
1105
+ action = get_view_actiongroup
1106
+ action.activate
1107
+ library = nil
1108
+ unless @prefs.selected_library.nil?
1109
+ library = @libraries.all_libraries.find do |x|
1110
+ x.name == @prefs.selected_library
1111
+ end
1112
+ end
1113
+ select_a_library library
1114
+ end
1115
+
1116
+ def select_a_library(library)
1117
+ if library
1118
+ select_library(library)
1119
+ else
1120
+ # Select the first item by default.
1121
+ iter = @library_listview.model.iter_first
1122
+ @library_listview.selection.select_iter(iter)
1123
+ end
1124
+ end
1125
+
1126
+ def save_preferences
1127
+ log.debug { 'save_preferences' }
1128
+ @prefs.position = @main_app.position
1129
+ @prefs.size = @main_app.allocation.to_a[2..3]
1130
+ @prefs.maximized = @maximized
1131
+ @prefs.sidepane_position = @paned.position
1132
+ @prefs.sidepane_visible = @actiongroup['Sidepane'].active?
1133
+ @prefs.toolbar_visible = @actiongroup['Toolbar'].active?
1134
+ @prefs.statusbar_visible = @actiongroup['Statusbar'].active?
1135
+ @prefs.view_as = @notebook.page
1136
+ @prefs.selected_library = selected_library.name
1137
+ cols_width = {}
1138
+ @listview.columns.each do |c|
1139
+ cols_width[c.title] = [c.widget.size_request.first, c.width].max
1140
+ end
1141
+ @prefs.cols_width = '{' + cols_width.to_a.map do |t, v|
1142
+ '"' + t + '": ' + v.to_s
1143
+ end.join(', ') + '}'
1144
+ log.debug { "cols_width: #{@prefs.cols_width} " }
1145
+ @prefs.save!
1146
+ end
1147
+
1148
+ def undoable_move(source, dest, books)
1149
+ log.debug { 'undoable_move' }
1150
+ Library.move(source, dest, *books)
1151
+ UndoManager.instance.push { undoable_move(dest, source, books) }
1152
+ end
1153
+
1154
+ def move_selected_books_to_library(library)
1155
+ books = selected_books.select do |book|
1156
+ !library.include?(book) or
1157
+ ConflictWhileCopyingDialog.new(@main_app,
1158
+ library,
1159
+ book).replace?
1160
+ end
1161
+ undoable_move(selected_library, library, books)
1162
+ end
1163
+
1164
+ def setup_move_actions
1165
+ @actiongroup.actions.each do |action|
1166
+ next unless /^MoveIn/.match(action.name)
1167
+ @actiongroup.remove_action(action)
1168
+ end
1169
+ actions = []
1170
+ @libraries.all_regular_libraries.each do |library|
1171
+ actions << [
1172
+ library.action_name, nil,
1173
+ _("In '_%s'") % library.name,
1174
+ nil, nil, proc { move_selected_books_to_library(library) }
1175
+ ]
1176
+ end
1177
+ @actiongroup.add_actions(actions)
1178
+ @uimanager.remove_ui(@move_mid) if @move_mid
1179
+ @move_mid = @uimanager.new_merge_id
1180
+ @libraries.all_regular_libraries.each do |library|
1181
+ name = library.action_name
1182
+ ['ui/MainMenubar/EditMenu/Move/',
1183
+ 'ui/BookPopup/Move/'].each do |path|
1184
+ @uimanager.add_ui(@move_mid, path, name, name,
1185
+ Gtk::UIManager::MENUITEM, false)
1186
+ end
1187
+ end
1188
+ end
1189
+
1190
+ def current_view
1191
+ case @notebook.page
1192
+ when 0
1193
+ @iconview
1194
+ when 1
1195
+ @listview
1196
+ end
1197
+ end
1198
+
1199
+ # Gets the sort order of the current library, for use by export
1200
+ def library_sort_order
1201
+ # added by Cathal Mc Ginley, 23 Oct 2007
1202
+ log.debug { "library_sort_order #{@notebook.page}: #{@iconview.model.inspect} #{@listview.model.inspect}" }
1203
+ sorted_on = current_view.model.sort_column_id
1204
+ if sorted_on
1205
+ sort_column = sorted_on[0]
1206
+ sort_order = sorted_on[1]
1207
+
1208
+ column_ids_to_attributes = { 2 => :title,
1209
+ 4 => :authors,
1210
+ 5 => :isbn,
1211
+ 6 => :publisher,
1212
+ 7 => :publishing_year,
1213
+ 8 => :edition, # binding
1214
+ 12 => :redd,
1215
+ 13 => :own,
1216
+ 14 => :want,
1217
+ 9 => :rating }
1218
+
1219
+ sort_attribute = column_ids_to_attributes[sort_column]
1220
+ ascending = (sort_order == Gtk::SORT_ASCENDING)
1221
+ LibrarySortOrder.new(sort_attribute, ascending)
1222
+ else
1223
+ LibrarySortOrder::Unsorted.new
1224
+ end
1225
+ end
1226
+
1227
+ def get_previous_selected_library(library)
1228
+ log.debug { "get_previous_selected_library: #{library}" }
1229
+ @previous_selected_library = selected_library
1230
+ if @previous_selected_library != library
1231
+ select_library(library)
1232
+ else
1233
+ @previous_selected_library = nil
1234
+ end
1235
+ end
1236
+
1237
+ def remove_library_iter
1238
+ old_iter = @library_listview.selection.selected
1239
+ # commenting out this code seems to fix #20681
1240
+ # "crashes when switching to smart library mid-load"
1241
+ # next_iter = @library_listview.selection.selected
1242
+ # next_iter.next!
1243
+ @library_listview.model.remove(old_iter)
1244
+ # @library_listview.selection.select_iter(next_iter)
1245
+ end
1246
+
1247
+ def undoable_delete(library, books = nil)
1248
+ # Deleting a library.
1249
+ if books.nil?
1250
+ library.delete_observer(self) if library.is_a?(Library)
1251
+ library.delete
1252
+ @libraries.remove_library(library)
1253
+ remove_library_separator
1254
+ remove_library_iter
1255
+ get_previous_selected_library library
1256
+ setup_move_actions
1257
+ select_library(@previous_selected_library) unless @previous_selected_library.nil?
1258
+ @previous_selected_library = nil
1259
+ else
1260
+ # Deleting books.
1261
+ books.each { |book| library.delete(book) }
1262
+ end
1263
+ UndoManager.instance.push { undoable_undelete(library, books) }
1264
+ end
1265
+
1266
+ def remove_library_separator
1267
+ if !@library_separator_iter.nil? and @libraries.all_smart_libraries.empty?
1268
+ @library_listview.model.remove(@library_separator_iter)
1269
+ @library_separator_iter = nil
1270
+ end
1271
+ end
1272
+
1273
+ def undoable_undelete(library, books = nil)
1274
+ # Undeleting a library.
1275
+ if books.nil?
1276
+ library.undelete
1277
+ @libraries.add_library(library)
1278
+ append_library(library)
1279
+ setup_move_actions
1280
+ library.add_observer(self) if library.is_a?(Library)
1281
+ # Undeleting books.
1282
+ else
1283
+ books.each { |book| library.undelete(book) }
1284
+ end
1285
+ select_library(library)
1286
+ UndoManager.instance.push { undoable_delete(library, books) }
1287
+ end
1288
+
1289
+ def setup_window_icons
1290
+ @main_app.icon = Icons::ALEXANDRIA_SMALL
1291
+ Gtk::Window.set_default_icon_name('alexandria')
1292
+ @main_app.icon_name = 'alexandria'
1293
+ end
1294
+
1295
+ ICONS_SORTS = [
1296
+ Columns::TITLE, Columns::AUTHORS, Columns::ISBN,
1297
+ Columns::PUBLISHER, Columns::EDITION, Columns::RATING, Columns::REDD, Columns::OWN, Columns::WANT
1298
+ ]
1299
+
1300
+ def setup_books_iconview_sorting
1301
+ sort_order = @prefs.reverse_icons ? Gtk::SORT_DESCENDING : Gtk::SORT_ASCENDING
1302
+ mode = ICONS_SORTS[@prefs.arrange_icons_mode]
1303
+ @iconview_model.set_sort_column_id(mode, sort_order)
1304
+ @filtered_model.refilter # force redraw
1305
+ end
1306
+ end
1307
+ end
1308
+ end