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