1#!/usr/local/bin/python3.8 2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai 3 4 5__license__ = 'GPL v3' 6__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' 7__docformat__ = 'restructuredtext en' 8 9import functools 10from qt.core import ( 11 QAction, QApplication, QDialog, QEvent, QIcon, QLabel, QMenu, QPainter, 12 QSizePolicy, QSplitter, QStackedWidget, QStatusBar, QStyle, QStyleOption, Qt, 13 QTabBar, QTimer, QToolButton, QVBoxLayout, QWidget 14) 15 16from calibre.constants import get_appname_for_display, get_version, ismacos 17from calibre.customize.ui import find_plugin 18from calibre.gui2 import ( 19 config, error_dialog, gprefs, is_widescreen, open_local_file, open_url 20) 21from calibre.gui2.book_details import BookDetails 22from calibre.gui2.layout_menu import LayoutMenu 23from calibre.gui2.library.alternate_views import GridView 24from calibre.gui2.library.views import BooksView, DeviceBooksView 25from calibre.gui2.notify import get_notifier 26from calibre.gui2.tag_browser.ui import TagBrowserWidget 27from calibre.gui2.widgets import LayoutButton, Splitter 28from calibre.utils.config import prefs 29from calibre.utils.icu import sort_key 30from calibre.utils.localization import localize_website_link 31 32_keep_refs = [] 33 34 35def partial(*args, **kwargs): 36 ans = functools.partial(*args, **kwargs) 37 _keep_refs.append(ans) 38 return ans 39 40 41class LibraryViewMixin: # {{{ 42 43 def __init__(self, *args, **kwargs): 44 pass 45 46 def init_library_view_mixin(self, db): 47 self.library_view.files_dropped.connect(self.iactions['Add Books'].files_dropped, type=Qt.ConnectionType.QueuedConnection) 48 self.library_view.books_dropped.connect(self.iactions['Edit Metadata'].books_dropped, type=Qt.ConnectionType.QueuedConnection) 49 self.library_view.add_column_signal.connect(partial(self.iactions['Preferences'].do_config, 50 initial_plugin=('Interface', 'Custom Columns'), close_after_initial=True), 51 type=Qt.ConnectionType.QueuedConnection) 52 for func, args in [ 53 ('connect_to_search_box', (self.search, 54 self.search_done)), 55 ('connect_to_book_display', 56 (self.book_details.show_data,)), 57 ]: 58 for view in (self.library_view, self.memory_view, self.card_a_view, self.card_b_view): 59 getattr(view, func)(*args) 60 61 self.memory_view.connect_dirtied_signal(self.upload_dirtied_booklists) 62 self.memory_view.connect_upload_collections_signal( 63 func=self.upload_collections, oncard=None) 64 self.card_a_view.connect_dirtied_signal(self.upload_dirtied_booklists) 65 self.card_a_view.connect_upload_collections_signal( 66 func=self.upload_collections, oncard='carda') 67 self.card_b_view.connect_dirtied_signal(self.upload_dirtied_booklists) 68 self.card_b_view.connect_upload_collections_signal( 69 func=self.upload_collections, oncard='cardb') 70 self.book_on_device(None, reset=True) 71 db.set_book_on_device_func(self.book_on_device) 72 self.library_view.set_database(db) 73 self.library_view.model().set_book_on_device_func(self.book_on_device) 74 prefs['library_path'] = self.library_path 75 76 for view in ('library', 'memory', 'card_a', 'card_b'): 77 view = getattr(self, view+'_view') 78 view.verticalHeader().sectionDoubleClicked.connect(self.iactions['View'].view_specific_book) 79 80 self.library_view.model().set_highlight_only(config['highlight_search_matches']) 81 82 def build_context_menus(self): 83 from calibre.gui2.bars import populate_menu 84 lm = QMenu(self) 85 populate_menu(lm, gprefs['action-layout-context-menu'], self.iactions) 86 dm = QMenu(self) 87 populate_menu(dm, gprefs['action-layout-context-menu-device'], self.iactions) 88 ec = self.iactions['Edit Collections'].qaction 89 self.library_view.set_context_menu(lm, ec) 90 sm = QMenu(self) 91 populate_menu(sm, gprefs['action-layout-context-menu-split'], self.iactions) 92 self.library_view.pin_view.set_context_menu(sm) 93 for v in (self.memory_view, self.card_a_view, self.card_b_view): 94 v.set_context_menu(dm, ec) 95 96 if hasattr(self.cover_flow, 'set_context_menu'): 97 cm = QMenu(self.cover_flow) 98 populate_menu(cm, 99 gprefs['action-layout-context-menu-cover-browser'], self.iactions) 100 self.cover_flow.set_context_menu(cm) 101 102 def search_done(self, view, ok): 103 if view is self.current_view(): 104 self.search.search_done(ok) 105 self.set_number_of_books_shown() 106 if ok: 107 v = self.current_view() 108 if hasattr(v, 'set_current_row'): 109 v.set_current_row(0) 110 if v is self.library_view and v.row_count() == 0: 111 self.book_details.reset_info() 112 113 # }}} 114 115 116class QuickviewSplitter(QSplitter): # {{{ 117 118 def __init__(self, parent=None, orientation=Qt.Orientation.Vertical, qv_widget=None): 119 QSplitter.__init__(self, parent=parent, orientation=orientation) 120 self.splitterMoved.connect(self.splitter_moved) 121 self.setChildrenCollapsible(False) 122 self.qv_widget = qv_widget 123 124 def splitter_moved(self): 125 gprefs['quickview_dialog_heights'] = self.sizes() 126 127 def resizeEvent(self, *args): 128 QSplitter.resizeEvent(self, *args) 129 if self.sizes()[1] != 0: 130 gprefs['quickview_dialog_heights'] = self.sizes() 131 132 def set_sizes(self): 133 sizes = gprefs.get('quickview_dialog_heights', []) 134 if len(sizes) == 2: 135 self.setSizes(sizes) 136 137 def add_quickview_dialog(self, qv_dialog): 138 self.qv_widget.layout().addWidget(qv_dialog) 139 140 def show_quickview_widget(self): 141 self.qv_widget.show() 142 143 def hide_quickview_widget(self): 144 self.qv_widget.hide() 145# }}} 146 147 148class LibraryWidget(Splitter): # {{{ 149 150 def __init__(self, parent): 151 orientation = Qt.Orientation.Vertical 152 if config['gui_layout'] == 'narrow': 153 orientation = Qt.Orientation.Horizontal if is_widescreen() else Qt.Orientation.Vertical 154 idx = 0 if orientation == Qt.Orientation.Vertical else 1 155 size = 300 if orientation == Qt.Orientation.Vertical else 550 156 Splitter.__init__(self, 'cover_browser_splitter', _('Cover browser'), 157 I('cover_flow.png'), 158 orientation=orientation, parent=parent, 159 connect_button=not config['separate_cover_flow'], 160 side_index=idx, initial_side_size=size, initial_show=False, 161 shortcut='Shift+Alt+B') 162 163 quickview_widget = QWidget() 164 parent.quickview_splitter = QuickviewSplitter( 165 parent=self, orientation=Qt.Orientation.Vertical, qv_widget=quickview_widget) 166 parent.library_view = BooksView(parent) 167 parent.library_view.setObjectName('library_view') 168 stack = QStackedWidget(self) 169 av = parent.library_view.alternate_views 170 parent.pin_container = av.set_stack(stack) 171 parent.grid_view = GridView(parent) 172 parent.grid_view.setObjectName('grid_view') 173 av.add_view('grid', parent.grid_view) 174 parent.quickview_splitter.addWidget(stack) 175 176 l = QVBoxLayout() 177 l.setContentsMargins(4, 0, 0, 0) 178 quickview_widget.setLayout(l) 179 parent.quickview_splitter.addWidget(quickview_widget) 180 parent.quickview_splitter.hide_quickview_widget() 181 182 self.addWidget(parent.quickview_splitter) 183# }}} 184 185 186class Stack(QStackedWidget): # {{{ 187 188 def __init__(self, parent): 189 QStackedWidget.__init__(self, parent) 190 191 parent.cb_splitter = LibraryWidget(parent) 192 self.tb_widget = TagBrowserWidget(parent) 193 parent.tb_splitter = Splitter('tag_browser_splitter', 194 _('Tag browser'), I('tags.png'), 195 parent=parent, side_index=0, initial_side_size=200, 196 shortcut='Shift+Alt+T') 197 parent.tb_splitter.state_changed.connect( 198 self.tb_widget.set_pane_is_visible, Qt.ConnectionType.QueuedConnection) 199 parent.tb_splitter.addWidget(self.tb_widget) 200 parent.tb_splitter.addWidget(parent.cb_splitter) 201 parent.tb_splitter.setCollapsible(parent.tb_splitter.other_index, False) 202 203 self.addWidget(parent.tb_splitter) 204 for x in ('memory', 'card_a', 'card_b'): 205 name = x+'_view' 206 w = DeviceBooksView(parent) 207 setattr(parent, name, w) 208 self.addWidget(w) 209 w.setObjectName(name) 210 211 212# }}} 213 214class UpdateLabel(QLabel): # {{{ 215 216 def __init__(self, *args, **kwargs): 217 QLabel.__init__(self, *args, **kwargs) 218 self.setCursor(Qt.CursorShape.PointingHandCursor) 219 220 def contextMenuEvent(self, e): 221 pass 222# }}} 223 224 225class VersionLabel(QLabel): # {{{ 226 227 def __init__(self, parent): 228 QLabel.__init__(self, parent) 229 self.mouse_over = False 230 self.setCursor(Qt.CursorShape.PointingHandCursor) 231 self.setToolTip(_('See what\'s new in this calibre release')) 232 233 def mouseReleaseEvent(self, ev): 234 open_url(localize_website_link('https://calibre-ebook.com/whats-new')) 235 ev.accept() 236 return QLabel.mouseReleaseEvent(self, ev) 237 238 def event(self, ev): 239 m = None 240 et = ev.type() 241 if et == QEvent.Type.Enter: 242 m = True 243 elif et == QEvent.Type.Leave: 244 m = False 245 if m is not None and m != self.mouse_over: 246 self.mouse_over = m 247 self.update() 248 return QLabel.event(self, ev) 249 250 def paintEvent(self, ev): 251 if self.mouse_over: 252 p = QPainter(self) 253 tool = QStyleOption() 254 tool.rect = self.rect() 255 tool.state = QStyle.StateFlag.State_Raised | QStyle.StateFlag.State_Active | QStyle.StateFlag.State_MouseOver 256 s = self.style() 257 s.drawPrimitive(QStyle.PrimitiveElement.PE_PanelButtonTool, tool, p, self) 258 p.end() 259 return QLabel.paintEvent(self, ev) 260# }}} 261 262 263class StatusBar(QStatusBar): # {{{ 264 265 def __init__(self, parent=None): 266 QStatusBar.__init__(self, parent) 267 self.version = get_version() 268 self.base_msg = f'{get_appname_for_display()} {self.version}' 269 self.device_string = '' 270 self.update_label = UpdateLabel('') 271 self.total = self.current = self.selected = self.library_total = 0 272 self.addPermanentWidget(self.update_label) 273 self.update_label.setVisible(False) 274 self.defmsg = VersionLabel(self) 275 self.addWidget(self.defmsg) 276 self.set_label() 277 278 def initialize(self, systray=None): 279 self.systray = systray 280 self.notifier = get_notifier(systray) 281 282 def device_connected(self, devname): 283 self.device_string = _('Connected ') + devname 284 self.set_label() 285 286 def update_state(self, library_total, total, current, selected): 287 self.library_total = library_total 288 self.total, self.current, self.selected = total, current, selected 289 self.set_label() 290 291 def set_label(self): 292 try: 293 self._set_label() 294 except: 295 import traceback 296 traceback.print_exc() 297 298 def _set_label(self): 299 msg = self.base_msg 300 if self.device_string: 301 msg += ' ..::.. ' + self.device_string 302 else: 303 msg += _(' %(created)s %(name)s') % dict(created=_('created by'), name='Kovid Goyal') 304 305 if self.total != self.current: 306 base = _('%(num)d of %(total)d books') % dict(num=self.current, total=self.total) 307 else: 308 base = ngettext('one book', '{} books', self.total).format(self.total) 309 if self.selected > 0: 310 base = ngettext('%(num)s, %(sel)d selected', '%(num)s, %(sel)d selected', self.selected) % dict(num=base, sel=self.selected) 311 if self.library_total != self.total: 312 base = _('{0}, {1} total').format(base, self.library_total) 313 314 self.defmsg.setText('\xa0%s\xa0\xa0\xa0\xa0[%s]' % (msg, base)) 315 self.clearMessage() 316 317 def device_disconnected(self): 318 self.device_string = '' 319 self.set_label() 320 321 def show_message(self, msg, timeout=0, show_notification=True): 322 self.showMessage(msg, timeout) 323 if self.notifier is not None and not config['disable_tray_notification'] and show_notification: 324 self.notifier(msg) 325 326 def clear_message(self): 327 self.clearMessage() 328 329# }}} 330 331 332class GridViewButton(LayoutButton): # {{{ 333 334 def __init__(self, gui): 335 sc = 'Alt+Shift+G' 336 LayoutButton.__init__(self, I('grid.png'), _('Cover grid'), parent=gui, shortcut=sc) 337 self.set_state_to_show() 338 self.action_toggle = QAction(self.icon(), _('Toggle') + ' ' + self.label, self) 339 gui.addAction(self.action_toggle) 340 gui.keyboard.register_shortcut('grid view toggle' + self.label, str(self.action_toggle.text()), 341 default_keys=(sc,), action=self.action_toggle) 342 self.action_toggle.triggered.connect(self.toggle) 343 self.action_toggle.changed.connect(self.update_shortcut) 344 self.toggled.connect(self.update_state) 345 346 def update_state(self, checked): 347 if checked: 348 self.set_state_to_hide() 349 else: 350 self.set_state_to_show() 351 352 def save_state(self): 353 gprefs['grid view visible'] = bool(self.isChecked()) 354 355 def restore_state(self): 356 if gprefs.get('grid view visible', False): 357 self.toggle() 358 359 360# }}} 361 362class SearchBarButton(LayoutButton): # {{{ 363 364 def __init__(self, gui): 365 sc = 'Alt+Shift+F' 366 LayoutButton.__init__(self, I('search.png'), _('Search bar'), parent=gui, shortcut=sc) 367 self.set_state_to_hide() 368 self.action_toggle = QAction(self.icon(), _('Toggle') + ' ' + self.label, self) 369 gui.addAction(self.action_toggle) 370 gui.keyboard.register_shortcut('search bar toggle' + self.label, str(self.action_toggle.text()), 371 default_keys=(sc,), action=self.action_toggle) 372 self.action_toggle.triggered.connect(self.toggle) 373 self.action_toggle.changed.connect(self.update_shortcut) 374 self.toggled.connect(self.update_state) 375 376 def update_state(self, checked): 377 if checked: 378 self.set_state_to_hide() 379 else: 380 self.set_state_to_show() 381 382 def save_state(self): 383 gprefs['search bar visible'] = bool(self.isChecked()) 384 385 def restore_state(self): 386 self.setChecked(bool(gprefs.get('search bar visible', True))) 387 388 389# }}} 390 391class VLTabs(QTabBar): # {{{ 392 393 def __init__(self, parent): 394 QTabBar.__init__(self, parent) 395 self.setDocumentMode(True) 396 self.setDrawBase(False) 397 self.setMovable(True) 398 self.setTabsClosable(gprefs['vl_tabs_closable']) 399 self.gui = parent 400 self.ignore_tab_changed = False 401 self.currentChanged.connect(self.tab_changed) 402 self.tabMoved.connect(self.tab_moved, type=Qt.ConnectionType.QueuedConnection) 403 self.tabCloseRequested.connect(self.tab_close) 404 self.setVisible(gprefs['show_vl_tabs']) 405 self.next_action = a = QAction(self) 406 a.triggered.connect(partial(self.next_tab, delta=1)), self.gui.addAction(a) 407 self.previous_action = a = QAction(self) 408 a.triggered.connect(partial(self.next_tab, delta=-1)), self.gui.addAction(a) 409 self.gui.keyboard.register_shortcut( 410 'virtual-library-tab-bar-next', _('Next Virtual library'), action=self.next_action, 411 default_keys=('Ctrl+Right',), 412 description=_('Switch to the next Virtual library in the Virtual library tab bar') 413 ) 414 self.gui.keyboard.register_shortcut( 415 'virtual-library-tab-bar-previous', _('Previous Virtual library'), action=self.previous_action, 416 default_keys=('Ctrl+Left',), 417 description=_('Switch to the previous Virtual library in the Virtual library tab bar') 418 ) 419 420 def next_tab(self, delta=1): 421 if self.count() > 1 and self.isVisible(): 422 idx = (self.currentIndex() + delta) % self.count() 423 self.setCurrentIndex(idx) 424 425 def enable_bar(self): 426 gprefs['show_vl_tabs'] = True 427 self.setVisible(True) 428 self.gui.set_number_of_books_shown() 429 430 def disable_bar(self): 431 gprefs['show_vl_tabs'] = False 432 self.setVisible(False) 433 self.gui.set_number_of_books_shown() 434 435 def lock_tab(self): 436 gprefs['vl_tabs_closable'] = False 437 self.setTabsClosable(False) 438 439 def unlock_tab(self): 440 gprefs['vl_tabs_closable'] = True 441 self.setTabsClosable(True) 442 try: 443 self.tabButton(0, QTabBar.ButtonPosition.RightSide).setVisible(False) 444 except AttributeError: 445 try: 446 self.tabButton(0, QTabBar.ButtonPosition.LeftSide).setVisible(False) 447 except AttributeError: 448 # On some OS X machines (using native style) the tab button is 449 # on the left 450 pass 451 452 def tab_changed(self, idx): 453 if self.ignore_tab_changed: 454 return 455 vl = str(self.tabData(idx) or '').strip() or None 456 self.gui.apply_virtual_library(vl, update_tabs=False) 457 458 def tab_moved(self, from_, to): 459 self.current_db.new_api.set_pref('virt_libs_order', [str(self.tabData(i) or '') for i in range(self.count())]) 460 461 def tab_close(self, index): 462 vl = str(self.tabData(index) or '') 463 if vl: # Dont allow closing the All Books tab 464 self.current_db.new_api.set_pref('virt_libs_hidden', list( 465 self.current_db.new_api.pref('virt_libs_hidden', ())) + [vl]) 466 self.removeTab(index) 467 468 @property 469 def current_db(self): 470 return self.gui.current_db 471 472 def rebuild(self): 473 self.ignore_tab_changed = True 474 try: 475 self._rebuild() 476 finally: 477 self.ignore_tab_changed = False 478 479 def _rebuild(self): 480 db = self.current_db 481 vl_map = db.new_api.pref('virtual_libraries', {}) 482 virt_libs = frozenset(vl_map) 483 hidden = set(db.new_api.pref('virt_libs_hidden', ())) 484 if hidden - virt_libs: 485 hidden = hidden.intersection(virt_libs) 486 db.new_api.set_pref('virt_libs_hidden', list(hidden)) 487 order = db.new_api.pref('virt_libs_order', ()) 488 while self.count(): 489 self.removeTab(0) 490 current_lib = db.data.get_base_restriction_name() 491 if current_lib in hidden: 492 hidden.discard(current_lib) 493 db.new_api.set_pref('virt_libs_hidden', list(hidden)) 494 current_idx = all_idx = None 495 virt_libs = (set(virt_libs) - hidden) | {''} 496 order = {x:i for i, x in enumerate(order)} 497 for i, vl in enumerate(sorted(virt_libs, key=lambda x:(order.get(x, 0), sort_key(x)))): 498 self.addTab(vl.replace('&', '&&') or _('All books')) 499 sexp = vl_map.get(vl, None) 500 if sexp is not None: 501 self.setTabToolTip(i, _('Search expression for this Virtual library:') + '\n\n' + sexp) 502 self.setTabData(i, vl) 503 if vl == current_lib: 504 current_idx = i 505 if not vl: 506 all_idx = i 507 self.setCurrentIndex(all_idx if current_idx is None else current_idx) 508 if current_idx is None and current_lib: 509 self.setTabText(all_idx, current_lib) 510 try: 511 self.tabButton(all_idx, QTabBar.ButtonPosition.RightSide).setVisible(False) 512 except AttributeError: 513 try: 514 self.tabButton(all_idx, QTabBar.ButtonPosition.LeftSide).setVisible(False) 515 except AttributeError: 516 # On some OS X machines (using native style) the tab button is 517 # on the left 518 pass 519 520 def update_current(self): 521 self.rebuild() 522 523 def contextMenuEvent(self, ev): 524 m = QMenu(self) 525 m.addAction(_('Sort tabs alphabetically'), self.sort_alphabetically) 526 hidden = self.current_db.new_api.pref('virt_libs_hidden') 527 if hidden: 528 s = m._s = m.addMenu(_('Restore hidden tabs')) 529 for x in hidden: 530 s.addAction(x, partial(self.restore, x)) 531 m.addAction(_('Hide Virtual library tabs'), self.disable_bar) 532 if gprefs['vl_tabs_closable']: 533 m.addAction(_('Lock Virtual library tabs'), self.lock_tab) 534 else: 535 m.addAction(_('Unlock Virtual library tabs'), self.unlock_tab) 536 i = self.tabAt(ev.pos()) 537 if i > -1: 538 vl = str(self.tabData(i) or '') 539 if vl: 540 vln = vl.replace('&', '&&') 541 m.addSeparator() 542 m.addAction(_('Edit "%s"') % vln, partial(self.gui.do_create_edit, name=vl)) 543 m.addAction(_('Delete "%s"') % vln, partial(self.gui.remove_vl_triggered, name=vl)) 544 m.exec(ev.globalPos()) 545 546 def sort_alphabetically(self): 547 self.current_db.new_api.set_pref('virt_libs_order', ()) 548 self.rebuild() 549 550 def restore(self, x): 551 h = self.current_db.new_api.pref('virt_libs_hidden', ()) 552 self.current_db.new_api.set_pref('virt_libs_hidden', list(set(h) - {x})) 553 self.rebuild() 554 555# }}} 556 557 558class LayoutMixin: # {{{ 559 560 def __init__(self, *args, **kwargs): 561 pass 562 563 def init_layout_mixin(self): 564 self.vl_tabs = VLTabs(self) 565 self.centralwidget.layout().addWidget(self.vl_tabs) 566 567 if config['gui_layout'] == 'narrow': # narrow {{{ 568 self.book_details = BookDetails(False, self) 569 self.stack = Stack(self) 570 self.bd_splitter = Splitter('book_details_splitter', 571 _('Book details'), I('book.png'), 572 orientation=Qt.Orientation.Vertical, parent=self, side_index=1, 573 shortcut='Shift+Alt+D') 574 self.bd_splitter.addWidget(self.stack) 575 self.bd_splitter.addWidget(self.book_details) 576 self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False) 577 self.centralwidget.layout().addWidget(self.bd_splitter) 578 button_order = ('sb', 'tb', 'bd', 'gv', 'cb', 'qv') 579 # }}} 580 else: # wide {{{ 581 self.bd_splitter = Splitter('book_details_splitter', 582 _('Book details'), I('book.png'), initial_side_size=200, 583 orientation=Qt.Orientation.Horizontal, parent=self, side_index=1, 584 shortcut='Shift+Alt+D') 585 self.stack = Stack(self) 586 self.bd_splitter.addWidget(self.stack) 587 self.book_details = BookDetails(True, self) 588 self.bd_splitter.addWidget(self.book_details) 589 self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False) 590 self.bd_splitter.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Expanding, 591 QSizePolicy.Policy.Expanding)) 592 self.centralwidget.layout().addWidget(self.bd_splitter) 593 button_order = ('sb', 'tb', 'cb', 'gv', 'qv', 'bd') 594 # }}} 595 596 # This must use the base method to find the plugin because it hasn't 597 # been fully initialized yet 598 self.qv = find_plugin('Quickview') 599 if self.qv and self.qv.actual_plugin_: 600 self.qv = self.qv.actual_plugin_ 601 602 self.status_bar = StatusBar(self) 603 stylename = str(self.style().objectName()) 604 self.grid_view_button = GridViewButton(self) 605 self.search_bar_button = SearchBarButton(self) 606 self.grid_view_button.toggled.connect(self.toggle_grid_view) 607 self.search_bar_button.toggled.connect(self.toggle_search_bar) 608 609 self.layout_buttons = [] 610 for x in button_order: 611 if hasattr(self, x + '_splitter'): 612 button = getattr(self, x + '_splitter').button 613 else: 614 if x == 'gv': 615 button = self.grid_view_button 616 elif x == 'qv': 617 if self.qv is None: 618 continue 619 button = self.qv.qv_button 620 else: 621 button = self.search_bar_button 622 self.layout_buttons.append(button) 623 button.setVisible(False) 624 if ismacos and stylename != 'Calibre': 625 button.setStyleSheet(''' 626 QToolButton { background: none; border:none; padding: 0px; } 627 QToolButton:checked { background: rgba(0, 0, 0, 25%); } 628 ''') 629 self.status_bar.addPermanentWidget(button) 630 if gprefs['show_layout_buttons']: 631 for b in self.layout_buttons: 632 b.setVisible(True) 633 self.status_bar.addPermanentWidget(b) 634 else: 635 self.layout_button = b = QToolButton(self) 636 b.setAutoRaise(True), b.setCursor(Qt.CursorShape.PointingHandCursor) 637 b.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) 638 b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) 639 b.setText(_('Layout')), b.setIcon(QIcon(I('config.png'))) 640 b.setMenu(LayoutMenu(self)) 641 b.setToolTip(_( 642 'Show and hide various parts of the calibre main window')) 643 self.status_bar.addPermanentWidget(b) 644 self.status_bar.addPermanentWidget(self.jobs_button) 645 self.setStatusBar(self.status_bar) 646 self.status_bar.update_label.linkActivated.connect(self.update_link_clicked) 647 648 def finalize_layout(self): 649 self.status_bar.initialize(self.system_tray_icon) 650 self.book_details.show_book_info.connect(self.iactions['Show Book Details'].show_book_info) 651 self.book_details.files_dropped.connect(self.iactions['Add Books'].files_dropped_on_book) 652 self.book_details.cover_changed.connect(self.bd_cover_changed, 653 type=Qt.ConnectionType.QueuedConnection) 654 self.book_details.open_cover_with.connect(self.bd_open_cover_with, 655 type=Qt.ConnectionType.QueuedConnection) 656 self.book_details.open_fmt_with.connect(self.bd_open_fmt_with, 657 type=Qt.ConnectionType.QueuedConnection) 658 self.book_details.edit_book.connect(self.bd_edit_book, 659 type=Qt.ConnectionType.QueuedConnection) 660 self.book_details.cover_removed.connect(self.bd_cover_removed, 661 type=Qt.ConnectionType.QueuedConnection) 662 self.book_details.remote_file_dropped.connect( 663 self.iactions['Add Books'].remote_file_dropped_on_book, 664 type=Qt.ConnectionType.QueuedConnection) 665 self.book_details.open_containing_folder.connect(self.iactions['View'].view_folder_for_id) 666 self.book_details.view_specific_format.connect(self.iactions['View'].view_format_by_id) 667 self.book_details.search_requested.connect(self.set_search_string_with_append) 668 self.book_details.remove_specific_format.connect( 669 self.iactions['Remove Books'].remove_format_by_id) 670 self.book_details.remove_metadata_item.connect( 671 self.iactions['Edit Metadata'].remove_metadata_item) 672 self.book_details.save_specific_format.connect( 673 self.iactions['Save To Disk'].save_library_format_by_ids) 674 self.book_details.restore_specific_format.connect( 675 self.iactions['Remove Books'].restore_format) 676 self.book_details.set_cover_from_format.connect( 677 self.iactions['Edit Metadata'].set_cover_from_format) 678 self.book_details.copy_link.connect(self.bd_copy_link, 679 type=Qt.ConnectionType.QueuedConnection) 680 self.book_details.view_device_book.connect( 681 self.iactions['View'].view_device_book) 682 self.book_details.manage_category.connect(self.manage_category_triggerred) 683 self.book_details.find_in_tag_browser.connect(self.find_in_tag_browser_triggered) 684 self.book_details.edit_identifiers.connect(self.edit_identifiers_triggerred) 685 self.book_details.compare_specific_format.connect(self.compare_format) 686 687 m = self.library_view.model() 688 if m.rowCount(None) > 0: 689 QTimer.singleShot(0, self.library_view.set_current_row) 690 m.current_changed(self.library_view.currentIndex(), 691 self.library_view.currentIndex()) 692 self.library_view.setFocus(Qt.FocusReason.OtherFocusReason) 693 694 def set_search_string_with_append(self, expression, append=''): 695 current = self.search.text().strip() 696 if append: 697 expr = f'{current} {append} {expression}' if current else expression 698 else: 699 expr = expression 700 self.search.set_search_string(expr) 701 702 def edit_identifiers_triggerred(self): 703 book_id = self.library_view.current_book 704 db = self.current_db.new_api 705 identifiers = db.field_for('identifiers', book_id, default_value={}) 706 from calibre.gui2.metadata.basic_widgets import Identifiers 707 d = Identifiers(identifiers, self) 708 if d.exec() == QDialog.DialogCode.Accepted: 709 identifiers = d.get_identifiers() 710 db.set_field('identifiers', {book_id: identifiers}) 711 self.iactions['Edit Metadata'].refresh_books_after_metadata_edit({book_id}) 712 713 def manage_category_triggerred(self, field, value): 714 if field and value: 715 if field == 'authors': 716 self.do_author_sort_edit(self, value, select_sort=False, 717 select_link=False, lookup_author=True) 718 elif field: 719 self.do_tags_list_edit(value, field) 720 721 def find_in_tag_browser_triggered(self, field, value): 722 if field and value: 723 tb = self.stack.tb_widget 724 tb.set_focus_to_find_box() 725 tb.item_search.lineEdit().setText(field + ':=' + value) 726 tb.do_find() 727 728 def toggle_grid_view(self, show): 729 self.library_view.alternate_views.show_view('grid' if show else None) 730 self.sort_sep.setVisible(show) 731 self.sort_button.setVisible(show) 732 733 def toggle_search_bar(self, show): 734 self.search_bar.setVisible(show) 735 if show: 736 self.search.setFocus(Qt.FocusReason.OtherFocusReason) 737 738 def bd_cover_changed(self, id_, cdata): 739 self.library_view.model().db.set_cover(id_, cdata) 740 self.refresh_cover_browser() 741 742 def bd_open_cover_with(self, book_id, entry): 743 cpath = self.current_db.new_api.format_abspath(book_id, '__COVER_INTERNAL__') 744 if cpath: 745 if entry is None: 746 open_local_file(cpath) 747 return 748 from calibre.gui2.open_with import run_program 749 run_program(entry, cpath, self) 750 751 def bd_open_fmt_with(self, book_id, fmt, entry): 752 path = self.current_db.new_api.format_abspath(book_id, fmt) 753 if path: 754 from calibre.gui2.open_with import run_program 755 run_program(entry, path, self) 756 else: 757 fmt = fmt.upper() 758 error_dialog(self, _('No %s format') % fmt, _( 759 'The book {0} does not have the {1} format').format( 760 self.current_db.new_api.field_for('title', book_id, default_value=_('Unknown')), 761 fmt), show=True) 762 763 def bd_edit_book(self, book_id, fmt): 764 from calibre.gui2.device import BusyCursor 765 with BusyCursor(): 766 self.iactions['Tweak ePub'].ebook_edit_format(book_id, fmt) 767 768 def open_with_action_triggerred(self, fmt, entry, *args): 769 book_id = self.library_view.current_book 770 if book_id is not None: 771 if fmt == 'cover_image': 772 self.bd_open_cover_with(book_id, entry) 773 else: 774 self.bd_open_fmt_with(book_id, fmt, entry) 775 776 def bd_cover_removed(self, id_): 777 self.library_view.model().db.remove_cover(id_, commit=True, 778 notify=False) 779 self.refresh_cover_browser() 780 781 def bd_copy_link(self, url): 782 if url: 783 QApplication.clipboard().setText(url) 784 785 def compare_format(self, book_id, fmt): 786 db = self.current_db.new_api 787 ofmt = fmt 788 if fmt.startswith('ORIGINAL_'): 789 fmt = fmt.partition('_')[-1] 790 else: 791 ofmt = 'ORIGINAL_' + fmt 792 path1, path2 = db.format_abspath(book_id, ofmt), db.format_abspath(book_id, fmt) 793 from calibre.gui2.tweak_book.diff.main import compare_books 794 compare_books(path1, path2, parent=self, revert_msg=_('Restore %s') % ofmt, revert_callback=partial( 795 self.iactions['Remove Books'].restore_format, book_id, ofmt), names=(ofmt, fmt)) 796 797 def save_layout_state(self): 798 for x in ('library', 'memory', 'card_a', 'card_b'): 799 getattr(self, x+'_view').save_state() 800 801 for x in ('cb', 'tb', 'bd'): 802 s = getattr(self, x+'_splitter') 803 s.update_desired_state() 804 s.save_state() 805 self.grid_view_button.save_state() 806 self.search_bar_button.save_state() 807 if self.qv: 808 self.qv.qv_button.save_state() 809 810 def read_layout_settings(self): 811 # View states are restored automatically when set_database is called 812 for x in ('cb', 'tb', 'bd'): 813 getattr(self, x+'_splitter').restore_state() 814 self.grid_view_button.restore_state() 815 self.search_bar_button.restore_state() 816 # Can't do quickview here because the gui isn't totally set up. Do it in ui 817 818 def update_status_bar(self, *args): 819 v = self.current_view() 820 selected = len(v.selectionModel().selectedRows()) 821 library_total, total, current = v.model().counts() 822 self.status_bar.update_state(library_total, total, current, selected) 823 824# }}} 825