From 0ad6ab164f2086f17e2600dcd2d7474a5c4981f9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 16 Jul 2010 20:45:33 -0600 Subject: [PATCH] Nicer unified toolbar --- src/calibre/gui2/__init__.py | 6 +- src/calibre/gui2/device.py | 26 +- src/calibre/gui2/init.py | 158 +---------- src/calibre/gui2/layout.py | 518 +++++++++++++++++++---------------- src/calibre/gui2/ui.py | 25 +- 5 files changed, 302 insertions(+), 431 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index d922af0914..41d72d17f1 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal ' import os, sys from threading import RLock -from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \ +from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \ QByteArray, QTranslator, QCoreApplication, QThread, \ QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \ QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \ @@ -33,10 +33,6 @@ def _config(): help=_('Send file to storage card instead of main memory by default')) c.add_opt('confirm_delete', default=False, help=_('Confirm before deleting')) - c.add_opt('toolbar_icon_size', default=QSize(48, 48), - help=_('Toolbar icon size')) # value QVariant.toSize - c.add_opt('show_text_in_toolbar', default=True, - help=_('Show button labels in the toolbar')) c.add_opt('main_window_geometry', default=None, help=_('Main window geometry')) # value QVariant.toByteArray c.add_opt('new_version_notification', default=True, diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index bc8ba7c381..c919547956 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -638,7 +638,6 @@ class DeviceMixin(object): # {{{ self.device_error_dialog = error_dialog(self, _('Error'), _('Error communicating with device'), ' ') self.device_error_dialog.setModal(Qt.NonModal) - self.device_connected = None self.emailer = Emailer() self.emailer.start() self.device_manager = DeviceManager(Dispatcher(self.device_detected), @@ -755,17 +754,14 @@ class DeviceMixin(object): # {{{ self.device_manager.device.__class__.get_gui_name()+\ _(' detected.'), 3000) self.device_connected = device_kind - self.location_view.model().device_connected(self.device_manager.device) self.refresh_ondevice_info (device_connected = True, reset_only = True) else: self.device_connected = None self.status_bar.device_disconnected() - self.location_view.model().update_devices() if self.current_view() != self.library_view: self.book_details.reset_info() - self.location_view.setCurrentIndex(self.location_view.model().index(0)) - self.refresh_ondevice_info (device_connected = False) - self.tool_bar.device_status_changed(bool(connected)) + self.location_manager.update_devices() + self.refresh_ondevice_info(device_connected=False) def info_read(self, job): ''' @@ -774,7 +770,8 @@ class DeviceMixin(object): # {{{ if job.failed: return self.device_job_exception(job) info, cp, fs = job.result - self.location_view.model().update_devices(cp, fs) + self.location_manager.update_devices(cp, fs, + self.device_manager.device.icon) self.status_bar.device_connected(info[0]) self.device_manager.books(Dispatcher(self.metadata_downloaded)) @@ -1076,9 +1073,9 @@ class DeviceMixin(object): # {{{ dynamic.set('catalogs_to_be_synced', set([])) if files: remove = [] - space = { self.location_view.model().free[0] : None, - self.location_view.model().free[1] : 'carda', - self.location_view.model().free[2] : 'cardb' } + space = { self.location_manager.free[0] : None, + self.location_manager.free[1] : 'carda', + self.location_manager.free[2] : 'cardb' } on_card = space.get(sorted(space.keys(), reverse=True)[0], None) self.upload_books(files, names, metadata, on_card=on_card, @@ -1140,9 +1137,9 @@ class DeviceMixin(object): # {{{ dynamic.set('news_to_be_synced', set([])) if config['upload_news_to_device'] and files: remove = ids if del_on_upload else [] - space = { self.location_view.model().free[0] : None, - self.location_view.model().free[1] : 'carda', - self.location_view.model().free[2] : 'cardb' } + space = { self.location_manager.free[0] : None, + self.location_manager.free[1] : 'carda', + self.location_manager.free[2] : 'cardb' } on_card = space.get(sorted(space.keys(), reverse=True)[0], None) self.upload_books(files, names, metadata, on_card=on_card, @@ -1263,7 +1260,8 @@ class DeviceMixin(object): # {{{ self.device_job_exception(job) return cp, fs = job.result - self.location_view.model().update_devices(cp, fs) + self.location_manager.update_devices(cp, fs, + self.device_manager.device.icon) # reset the views so that up-to-date info is shown. These need to be # here because the sony driver updates collections in sync_booklists self.memory_view.reset() diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 2474685522..254d2c3d00 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -7,14 +7,13 @@ __docformat__ = 'restructuredtext en' import functools, sys, os -from PyQt4.Qt import QMenu, Qt, pyqtSignal, QIcon, QStackedWidget, \ - QSize, QSizePolicy, QStatusBar, QUrl, QLabel, QFont +from PyQt4.Qt import QMenu, Qt, QStackedWidget, \ + QSize, QSizePolicy, QStatusBar, QLabel, QFont from calibre.utils.config import prefs -from calibre.ebooks import BOOK_EXTENSIONS from calibre.constants import isosx, __appname__, preferred_encoding, \ __version__ -from calibre.gui2 import config, is_widescreen, open_url +from calibre.gui2 import config, is_widescreen from calibre.gui2.library.views import BooksView, DeviceBooksView from calibre.gui2.widgets import Splitter from calibre.gui2.tag_view import TagBrowserWidget @@ -28,157 +27,6 @@ def partial(*args, **kwargs): _keep_refs.append(ans) return ans -class SaveMenu(QMenu): # {{{ - - save_fmt = pyqtSignal(object) - - def __init__(self, parent): - QMenu.__init__(self, _('Save single format to disk...'), parent) - for ext in sorted(BOOK_EXTENSIONS): - action = self.addAction(ext.upper()) - setattr(self, 'do_'+ext, partial(self.do, ext)) - action.triggered.connect( - getattr(self, 'do_'+ext)) - - def do(self, ext, *args): - self.save_fmt.emit(ext) - -# }}} - -class ToolbarMixin(object): # {{{ - - def __init__(self): - self.action_help.triggered.connect(self.show_help) - md = QMenu() - md.addAction(_('Edit metadata individually'), - partial(self.edit_metadata, False, bulk=False)) - md.addSeparator() - md.addAction(_('Edit metadata in bulk'), - partial(self.edit_metadata, False, bulk=True)) - md.addSeparator() - md.addAction(_('Download metadata and covers'), - partial(self.download_metadata, False, covers=True), - Qt.ControlModifier+Qt.Key_D) - md.addAction(_('Download only metadata'), - partial(self.download_metadata, False, covers=False)) - md.addAction(_('Download only covers'), - partial(self.download_metadata, False, covers=True, - set_metadata=False, set_social_metadata=False)) - md.addAction(_('Download only social metadata'), - partial(self.download_metadata, False, covers=False, - set_metadata=False, set_social_metadata=True)) - self.metadata_menu = md - - mb = QMenu() - mb.addAction(_('Merge into first selected book - delete others'), - self.merge_books) - mb.addSeparator() - mb.addAction(_('Merge into first selected book - keep others'), - partial(self.merge_books, safe_merge=True)) - self.merge_menu = mb - self.action_merge.setMenu(mb) - md.addSeparator() - md.addAction(self.action_merge) - - self.add_menu = QMenu() - self.add_menu.addAction(_('Add books from a single directory'), - self.add_books) - self.add_menu.addAction(_('Add books from directories, including ' - 'sub-directories (One book per directory, assumes every ebook ' - 'file is the same book in a different format)'), - self.add_recursive_single) - self.add_menu.addAction(_('Add books from directories, including ' - 'sub directories (Multiple books per directory, assumes every ' - 'ebook file is a different book)'), self.add_recursive_multiple) - self.add_menu.addAction(_('Add Empty book. (Book entry with no ' - 'formats)'), self.add_empty) - self.action_add.setMenu(self.add_menu) - self.action_add.triggered.connect(self.add_books) - self.action_del.triggered.connect(self.delete_books) - self.action_edit.triggered.connect(self.edit_metadata) - self.action_merge.triggered.connect(self.merge_books) - - self.action_save.triggered.connect(self.save_to_disk) - self.save_menu = QMenu() - self.save_menu.addAction(_('Save to disk'), partial(self.save_to_disk, - False)) - self.save_menu.addAction(_('Save to disk in a single directory'), - partial(self.save_to_single_dir, False)) - self.save_menu.addAction(_('Save only %s format to disk')% - prefs['output_format'].upper(), - partial(self.save_single_format_to_disk, False)) - self.save_menu.addAction( - _('Save only %s format to disk in a single directory')% - prefs['output_format'].upper(), - partial(self.save_single_fmt_to_single_dir, False)) - self.save_sub_menu = SaveMenu(self) - self.save_menu.addMenu(self.save_sub_menu) - self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk) - - self.action_view.triggered.connect(self.view_book) - self.view_menu = QMenu() - self.view_menu.addAction(_('View'), partial(self.view_book, False)) - ac = self.view_menu.addAction(_('View specific format')) - ac.setShortcut((Qt.ControlModifier if isosx else Qt.AltModifier)+Qt.Key_V) - self.action_view.setMenu(self.view_menu) - ac.triggered.connect(self.view_specific_format, type=Qt.QueuedConnection) - - self.delete_menu = QMenu() - self.delete_menu.addAction(_('Remove selected books'), self.delete_books) - self.delete_menu.addAction( - _('Remove files of a specific format from selected books..'), - self.delete_selected_formats) - self.delete_menu.addAction( - _('Remove all formats from selected books, except...'), - self.delete_all_but_selected_formats) - self.delete_menu.addAction( - _('Remove covers from selected books'), self.delete_covers) - self.delete_menu.addSeparator() - self.delete_menu.addAction( - _('Remove matching books from device'), - self.remove_matching_books_from_device) - self.action_del.setMenu(self.delete_menu) - - self.action_open_containing_folder.setShortcut(Qt.Key_O) - self.addAction(self.action_open_containing_folder) - self.action_open_containing_folder.triggered.connect(self.view_folder) - self.action_sync.setShortcut(Qt.Key_D) - self.action_sync.setEnabled(True) - self.create_device_menu() - self.action_sync.triggered.connect( - self._sync_action_triggered) - - self.action_edit.setMenu(md) - self.action_save.setMenu(self.save_menu) - - cm = QMenu() - cm.addAction(_('Convert individually'), partial(self.convert_ebook, - False, bulk=False)) - cm.addAction(_('Bulk convert'), - partial(self.convert_ebook, False, bulk=True)) - cm.addSeparator() - ac = cm.addAction( - _('Create catalog of books in your calibre library')) - ac.triggered.connect(self.generate_catalog) - self.action_convert.setMenu(cm) - self.action_convert.triggered.connect(self.convert_ebook) - self.convert_menu = cm - - pm = QMenu() - pm.addAction(QIcon(I('config.svg')), _('Preferences'), self.do_config) - pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'), - self.run_wizard) - self.action_preferences.setMenu(pm) - self.preferences_menu = pm - for x in (self.preferences_action, self.action_preferences): - x.triggered.connect(self.do_config) - - def show_help(self, *args): - open_url(QUrl('http://calibre-ebook.com/user_manual')) - - -# }}} - class LibraryViewMixin(object): # {{{ def __init__(self, db): diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index c1ff3ab505..d4d2f86ef7 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -6,108 +6,105 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' from operator import attrgetter +from functools import partial -from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, QVariant, \ - QAbstractListModel, QFont, QApplication, QPalette, pyqtSignal, QToolButton, \ - QModelIndex, QListView, QAbstractButton, QPainter, QPixmap, QColor, \ - QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout +from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, \ + pyqtSignal, QToolButton, \ + QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup, \ + QMenu, QUrl -from calibre.constants import __appname__, filesystem_encoding +from calibre.constants import __appname__, isosx from calibre.gui2.search_box import SearchBox2, SavedSearchBox from calibre.gui2.throbber import ThrobbingButton -from calibre.gui2 import NONE, config +from calibre.gui2 import config, open_url from calibre.gui2.widgets import ComboBoxWithHelp from calibre import human_readable +from calibre.utils.config import prefs +from calibre.ebooks import BOOK_EXTENSIONS ICON_SIZE = 48 -# Location View {{{ +class SaveMenu(QMenu): # {{{ -class LocationModel(QAbstractListModel): # {{{ - - devicesChanged = pyqtSignal() + save_fmt = pyqtSignal(object) def __init__(self, parent): - QAbstractListModel.__init__(self, parent) - self.icons = [QVariant(QIcon(I('library.png'))), - QVariant(QIcon(I('reader.svg'))), - QVariant(QIcon(I('sd.svg'))), - QVariant(QIcon(I('sd.svg')))] - self.text = [_('Library\n%d books'), - _('Reader\n%s'), - _('Card A\n%s'), - _('Card B\n%s')] + QMenu.__init__(self, _('Save single format to disk...'), parent) + for ext in sorted(BOOK_EXTENSIONS): + action = self.addAction(ext.upper()) + setattr(self, 'do_'+ext, partial(self.do, ext)) + action.triggered.connect( + getattr(self, 'do_'+ext)) + + def do(self, ext, *args): + self.save_fmt.emit(ext) + +# }}} + +class LocationManager(QObject): # {{{ + + locations_changed = pyqtSignal() + unmount_device = pyqtSignal() + location_selected = pyqtSignal(object) + + def __init__(self, parent=None): + QObject.__init__(self, parent) self.free = [-1, -1, -1] self.count = 0 - self.highlight_row = 0 - self.library_tooltip = _('Click to see the books available on your computer') - self.tooltips = [ - self.library_tooltip, - _('Click to see the books in the main memory of your reader'), - _('Click to see the books on storage card A in your reader'), - _('Click to see the books on storage card B in your reader') - ] + self.location_actions = QActionGroup(self) + self.location_actions.setExclusive(True) + self.current_location = 'library' + self._mem = [] + self.tooltips = {} - def database_changed(self, db): - lp = db.library_path - if not isinstance(lp, unicode): - lp = lp.decode(filesystem_encoding, 'replace') - self.tooltips[0] = self.library_tooltip + '\n\n' + \ - _('Books located at') + ' ' + lp - self.dataChanged.emit(self.index(0), self.index(0)) + def ac(name, text, icon, tooltip): + icon = QIcon(I(icon)) + ac = self.location_actions.addAction(icon, text) + setattr(self, 'location_'+name, ac) + ac.setAutoRepeat(False) + ac.setCheckable(True) + receiver = partial(self._location_selected, name) + ac.triggered.connect(receiver) + self.tooltips[name] = tooltip + if name != 'library': + m = QMenu(parent) + self._mem.append(m) + a = m.addAction(icon, tooltip) + a.triggered.connect(receiver) + self._mem.append(a) + a = m.addAction(QIcon(I('eject.svg')), _('Eject this device')) + a.triggered.connect(self._eject_requested) + ac.setMenu(m) + self._mem.append(a) + else: + ac.setToolTip(tooltip) - def rowCount(self, *args): - return 1 + len([i for i in self.free if i >= 0]) + return ac - def get_device_row(self, row): - if row == 2 and self.free[1] == -1 and self.free[2] > -1: - row = 3 - return row + ac('library', _('Library'), 'lt.png', + _('Show books in calibre library')) + ac('main', _('Main'), 'reader.svg', + _('Show books in the main memory of the device')) + ac('carda', _('Card A'), 'sd.svg', + _('Show books in storage card A')) + ac('cardb', _('Card B'), 'sd.svg', + _('Show books in storage card B')) - def get_tooltip(self, row, drow): - ans = self.tooltips[row] - if row > 0: - fs = self.free[drow-1] - if fs > -1: - ans += '\n\n%s '%(human_readable(fs)) + _('free') - return ans + def _location_selected(self, location, *args): + if location != self.current_location and hasattr(self, + 'location_'+location): + self.current_location = location + self.location_selected.emit(location) + getattr(self, 'location_'+location).setChecked(True) - def data(self, index, role): - row = index.row() - drow = self.get_device_row(row) - data = NONE - if role == Qt.DisplayRole: - text = self.text[drow]%(human_readable(self.free[drow-1])) if row > 0 \ - else self.text[drow]%self.count - data = QVariant(text) - elif role == Qt.DecorationRole: - data = self.icons[drow] - elif role in (Qt.ToolTipRole, Qt.StatusTipRole): - ans = self.get_tooltip(row, drow) - data = QVariant(ans) - elif role == Qt.SizeHintRole: - data = QVariant(QSize(155, 90)) - elif role == Qt.FontRole: - font = QFont('monospace') - font.setBold(row == self.highlight_row) - data = QVariant(font) - elif role == Qt.ForegroundRole and row == self.highlight_row: - return QVariant(QApplication.palette().brush( - QPalette.HighlightedText)) - elif role == Qt.BackgroundRole and row == self.highlight_row: - return QVariant(QApplication.palette().brush( - QPalette.Highlight)) + def _eject_requested(self, *args): + self.unmount_device.emit() - return data - - def device_connected(self, dev): - self.icons[1] = QIcon(dev.icon) - self.dataChanged.emit(self.index(1), self.index(1)) - - def headerData(self, section, orientation, role): - return NONE - - def update_devices(self, cp=(None, None), fs=[-1, -1, -1]): + def update_devices(self, cp=(None, None), fs=[-1, -1, -1], icon=None): + if icon is None: + icon = I('reader.svg') + self.location_main.setIcon(QIcon(icon)) + had_device = self.has_device if cp is None: cp = (None, None) if isinstance(cp, (str, unicode)): @@ -120,137 +117,34 @@ class LocationModel(QAbstractListModel): # {{{ cpa, cpb = cp self.free[1] = fs[1] if fs[1] is not None and cpa is not None else -1 self.free[2] = fs[2] if fs[2] is not None and cpb is not None else -1 - self.reset() - self.devicesChanged.emit() + self.update_tooltips() + if self.has_device != had_device: + self.locations_changed.emit() + if not self.has_device: + self.location_library.trigger() - def location_changed(self, row): - self.highlight_row = row - self.dataChanged.emit( - self.index(0), self.index(self.rowCount(QModelIndex())-1)) + def update_tooltips(self): + for i, loc in enumerate(('main', 'carda', 'cardb')): + t = self.tooltips[loc] + if self.free[i] > -1: + t += u'\n\n%s '%human_readable(self.free[i]) + _('available') + ac = getattr(self, 'location_'+loc) + ac.setToolTip(t) + ac.setWhatsThis(t) + ac.setStatusTip(t) - def location_for_row(self, row): - if row == 0: return 'library' - if row == 1: return 'main' - if row == 3: return 'cardb' - return 'carda' if self.free[1] > -1 else 'cardb' - -# }}} - -class LocationView(QListView): - - umount_device = pyqtSignal() - location_selected = pyqtSignal(object) - - def __init__(self, parent): - QListView.__init__(self, parent) - self.setModel(LocationModel(self)) - self.reset() - self.currentChanged = self.current_changed - - self.eject_button = EjectButton(self) - self.eject_button.hide() - - self.entered.connect(self.item_entered) - self.viewportEntered.connect(self.viewport_entered) - self.eject_button.clicked.connect(self.eject_clicked) - self.model().devicesChanged.connect(self.eject_button.hide) - self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, - QSizePolicy.Expanding)) - self.setMouseTracking(True) - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - self.setEditTriggers(self.NoEditTriggers) - self.setTabKeyNavigation(True) - self.setProperty("showDropIndicator", True) - self.setSelectionMode(self.SingleSelection) - self.setIconSize(QSize(ICON_SIZE, ICON_SIZE)) - self.setMovement(self.Static) - self.setFlow(self.LeftToRight) - self.setGridSize(QSize(175, ICON_SIZE)) - self.setViewMode(self.ListMode) - self.setWordWrap(True) - self.setObjectName("location_view") - self.setMaximumSize(QSize(600, ICON_SIZE+16)) - self.setMinimumWidth(400) - - def eject_clicked(self, *args): - self.umount_device.emit() - - def count_changed(self, new_count): - self.model().count = new_count - self.model().reset() @property - def book_count(self): - return self.model().count - - def current_changed(self, current, previous): - if current.isValid(): - i = current.row() - location = self.model().location_for_row(i) - self.location_selected.emit(location) - self.model().location_changed(i) - - def location_changed(self, row): - if 0 <= row and row <= 3: - self.model().location_changed(row) - - def leaveEvent(self, event): - self.unsetCursor() - self.eject_button.hide() - - def item_entered(self, location): - self.setCursor(Qt.PointingHandCursor) - self.eject_button.hide() - - if location.row() == 1: - rect = self.visualRect(location) - - self.eject_button.resize(rect.height()/2, rect.height()/2) - - x, y = rect.left(), rect.top() - x = x + (rect.width() - self.eject_button.width() - 2) - y += 6 - - self.eject_button.move(x, y) - self.eject_button.show() - - def viewport_entered(self): - self.unsetCursor() - self.eject_button.hide() - - -class EjectButton(QAbstractButton): - - def __init__(self, parent): - QAbstractButton.__init__(self, parent) - self.mouse_over = False - self.setMouseTracking(True) - - def enterEvent(self, event): - self.mouse_over = True - QAbstractButton.enterEvent(self, event) - - def leaveEvent(self, event): - self.mouse_over = False - QAbstractButton.leaveEvent(self, event) - - def paintEvent(self, event): - painter = QPainter(self) - painter.setClipRect(event.rect()) - image = QPixmap(I('eject')).scaledToHeight(event.rect().height(), - Qt.SmoothTransformation) - - if not self.mouse_over: - alpha_mask = QPixmap(image.width(), image.height()) - color = QColor(128, 128, 128) - alpha_mask.fill(color) - image.setAlphaChannel(alpha_mask) - - painter.drawPixmap(0, 0, image) - - + def has_device(self): + return max(self.free) > -1 + @property + def available_actions(self): + ans = [self.location_library] + for i, loc in enumerate(('main', 'carda', 'cardb')): + if self.free[i] > -1: + ans.append(getattr(self, 'location_'+loc)) + return ans # }}} @@ -326,7 +220,7 @@ class SearchBar(QWidget): # {{{ class ToolBar(QToolBar): # {{{ - def __init__(self, actions, donate, location_view, parent=None): + def __init__(self, actions, donate, location_manager, parent=None): QToolBar.__init__(self, parent) self.setContextMenuPolicy(Qt.PreventContextMenu) self.setMovable(False) @@ -335,11 +229,12 @@ class ToolBar(QToolBar): # {{{ self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea) self.setIconSize(QSize(ICON_SIZE, ICON_SIZE)) self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) + self.setStyleSheet('QToolButton:checked { font-weight: bold }') - self.showing_device = False self.all_actions = actions self.donate = donate - self.location_view = location_view + self.location_manager = location_manager + self.location_manager.locations_changed.connect(self.build_bar) self.d_widget = QWidget() self.d_widget.setLayout(QVBoxLayout()) self.d_widget.layout().addWidget(donate) @@ -350,40 +245,45 @@ class ToolBar(QToolBar): # {{{ def contextMenuEvent(self, *args): pass - def device_status_changed(self, connected): - self.showing_device = connected - self.build_bar() - def build_bar(self): - order_field = 'device' if self.showing_device else 'normal' + showing_device = self.location_manager.has_device + order_field = 'device' if showing_device else 'normal' o = attrgetter(order_field+'_order') - sepvals = [2] if self.showing_device else [1] + sepvals = [2] if showing_device else [1] sepvals += [3] actions = [x for x in self.all_actions if o(x) > -1] actions.sort(cmp=lambda x,y : cmp(o(x), o(y))) self.clear() - for x in actions: - self.addAction(x) - ch = self.widgetForAction(x) + + + def setup_tool_button(ac): + ch = self.widgetForAction(ac) ch.setCursor(Qt.PointingHandCursor) ch.setAutoRaise(True) - - if x.action_name == 'choose_library': - self.location_action = self.addWidget(self.location_view) - self.choose_action = x - if config['show_donate_button']: - self.addWidget(self.d_widget) - if x.action_name not in ('choose_library', 'help'): + if ac.menu() is not None: ch.setPopupMode(ch.MenuButtonPopup) + for x in actions: + self.addAction(x) + setup_tool_button(x) + + if x.action_name == 'choose_library': + self.choose_action = x + if showing_device: + self.addSeparator() + for ac in self.location_manager.available_actions: + self.addAction(ac) + setup_tool_button(ac) + self.addSeparator() + self.location_manager.location_library.trigger() + elif config['show_donate_button']: + self.addWidget(self.d_widget) for x in actions: if x.separator_before in sepvals: self.insertSeparator(x) - - self.location_action.setVisible(self.showing_device) - self.choose_action.setVisible(not self.showing_device) + self.choose_action.setVisible(not showing_device) def count_changed(self, new_count): text = _('%d books')%new_count @@ -397,6 +297,9 @@ class ToolBar(QToolBar): # {{{ self.setToolButtonStyle(style) QToolBar.resizeEvent(self, ev) + def database_changed(self, db): + pass + # }}} class Action(QAction): @@ -405,6 +308,7 @@ class Action(QAction): class MainWindowMixin(object): def __init__(self): + self.device_connected = None self.setObjectName('MainWindow') self.setWindowIcon(QIcon(I('library.png'))) self.setWindowTitle(__appname__) @@ -417,9 +321,23 @@ class MainWindowMixin(object): self.resize(1012, 740) self.donate_button = ThrobbingButton(self.centralwidget) self.donate_button.set_normal_icon_size(ICON_SIZE, ICON_SIZE) + self.location_manager = LocationManager(self) - # Actions {{{ + all_actions = self.setup_actions() + self.search_bar = SearchBar(self) + self.tool_bar = ToolBar(all_actions, self.donate_button, + self.location_manager, self) + self.addToolBar(Qt.TopToolBarArea, self.tool_bar) + + l = self.centralwidget.layout() + l.addWidget(self.search_bar) + + + def read_toolbar_settings(self): + pass + + def setup_actions(self): # {{{ all_actions = [] def ac(normal_order, device_order, separator_before, @@ -467,17 +385,135 @@ class MainWindowMixin(object): ac(-1, -1, 0, 'books_with_the_same_tags', _('Books with the same tags'), 'tags.svg') - # }}} + self.action_help.triggered.connect(self.show_help) + md = QMenu() + md.addAction(_('Edit metadata individually'), + partial(self.edit_metadata, False, bulk=False)) + md.addSeparator() + md.addAction(_('Edit metadata in bulk'), + partial(self.edit_metadata, False, bulk=True)) + md.addSeparator() + md.addAction(_('Download metadata and covers'), + partial(self.download_metadata, False, covers=True), + Qt.ControlModifier+Qt.Key_D) + md.addAction(_('Download only metadata'), + partial(self.download_metadata, False, covers=False)) + md.addAction(_('Download only covers'), + partial(self.download_metadata, False, covers=True, + set_metadata=False, set_social_metadata=False)) + md.addAction(_('Download only social metadata'), + partial(self.download_metadata, False, covers=False, + set_metadata=False, set_social_metadata=True)) + self.metadata_menu = md - self.location_view = LocationView(self.centralwidget) - self.search_bar = SearchBar(self) - self.tool_bar = ToolBar(all_actions, self.donate_button, self.location_view, self) - self.addToolBar(Qt.TopToolBarArea, self.tool_bar) + mb = QMenu() + mb.addAction(_('Merge into first selected book - delete others'), + self.merge_books) + mb.addSeparator() + mb.addAction(_('Merge into first selected book - keep others'), + partial(self.merge_books, safe_merge=True)) + self.merge_menu = mb + self.action_merge.setMenu(mb) + md.addSeparator() + md.addAction(self.action_merge) - l = self.centralwidget.layout() - l.addWidget(self.search_bar) + self.add_menu = QMenu() + self.add_menu.addAction(_('Add books from a single directory'), + self.add_books) + self.add_menu.addAction(_('Add books from directories, including ' + 'sub-directories (One book per directory, assumes every ebook ' + 'file is the same book in a different format)'), + self.add_recursive_single) + self.add_menu.addAction(_('Add books from directories, including ' + 'sub directories (Multiple books per directory, assumes every ' + 'ebook file is a different book)'), self.add_recursive_multiple) + self.add_menu.addAction(_('Add Empty book. (Book entry with no ' + 'formats)'), self.add_empty) + self.action_add.setMenu(self.add_menu) + self.action_add.triggered.connect(self.add_books) + self.action_del.triggered.connect(self.delete_books) + self.action_edit.triggered.connect(self.edit_metadata) + self.action_merge.triggered.connect(self.merge_books) + + self.action_save.triggered.connect(self.save_to_disk) + self.save_menu = QMenu() + self.save_menu.addAction(_('Save to disk'), partial(self.save_to_disk, + False)) + self.save_menu.addAction(_('Save to disk in a single directory'), + partial(self.save_to_single_dir, False)) + self.save_menu.addAction(_('Save only %s format to disk')% + prefs['output_format'].upper(), + partial(self.save_single_format_to_disk, False)) + self.save_menu.addAction( + _('Save only %s format to disk in a single directory')% + prefs['output_format'].upper(), + partial(self.save_single_fmt_to_single_dir, False)) + self.save_sub_menu = SaveMenu(self) + self.save_menu.addMenu(self.save_sub_menu) + self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk) + + self.action_view.triggered.connect(self.view_book) + self.view_menu = QMenu() + self.view_menu.addAction(_('View'), partial(self.view_book, False)) + ac = self.view_menu.addAction(_('View specific format')) + ac.setShortcut((Qt.ControlModifier if isosx else Qt.AltModifier)+Qt.Key_V) + self.action_view.setMenu(self.view_menu) + ac.triggered.connect(self.view_specific_format, type=Qt.QueuedConnection) + + self.delete_menu = QMenu() + self.delete_menu.addAction(_('Remove selected books'), self.delete_books) + self.delete_menu.addAction( + _('Remove files of a specific format from selected books..'), + self.delete_selected_formats) + self.delete_menu.addAction( + _('Remove all formats from selected books, except...'), + self.delete_all_but_selected_formats) + self.delete_menu.addAction( + _('Remove covers from selected books'), self.delete_covers) + self.delete_menu.addSeparator() + self.delete_menu.addAction( + _('Remove matching books from device'), + self.remove_matching_books_from_device) + self.action_del.setMenu(self.delete_menu) + + self.action_open_containing_folder.setShortcut(Qt.Key_O) + self.addAction(self.action_open_containing_folder) + self.action_open_containing_folder.triggered.connect(self.view_folder) + self.action_sync.setShortcut(Qt.Key_D) + self.action_sync.setEnabled(True) + self.create_device_menu() + self.action_sync.triggered.connect( + self._sync_action_triggered) + + self.action_edit.setMenu(md) + self.action_save.setMenu(self.save_menu) + + cm = QMenu() + cm.addAction(_('Convert individually'), partial(self.convert_ebook, + False, bulk=False)) + cm.addAction(_('Bulk convert'), + partial(self.convert_ebook, False, bulk=True)) + cm.addSeparator() + ac = cm.addAction( + _('Create catalog of books in your calibre library')) + ac.triggered.connect(self.generate_catalog) + self.action_convert.setMenu(cm) + self.action_convert.triggered.connect(self.convert_ebook) + self.convert_menu = cm + + pm = QMenu() + pm.addAction(QIcon(I('config.svg')), _('Preferences'), self.do_config) + pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'), + self.run_wizard) + self.action_preferences.setMenu(pm) + self.preferences_menu = pm + for x in (self.preferences_action, self.action_preferences): + x.triggered.connect(self.do_config) + + return all_actions + # }}} + + def show_help(self, *args): + open_url(QUrl('http://calibre-ebook.com/user_manual')) - def read_toolbar_settings(self): - pass - diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index ba4c637932..be669d4aca 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -12,7 +12,7 @@ __docformat__ = 'restructuredtext en' import collections, os, sys, textwrap, time from Queue import Queue, Empty from threading import Thread -from PyQt4.Qt import Qt, SIGNAL, QObject, QTimer, \ +from PyQt4.Qt import Qt, SIGNAL, QTimer, \ QPixmap, QMenu, QIcon, pyqtSignal, \ QDialog, \ QSystemTrayIcon, QApplication, QKeySequence, QAction, \ @@ -38,7 +38,7 @@ from calibre.gui2.dialogs.config import ConfigDialog from calibre.gui2.dialogs.book_info import BookInfo from calibre.library.database2 import LibraryDatabase2 -from calibre.gui2.init import ToolbarMixin, LibraryViewMixin, LayoutMixin +from calibre.gui2.init import LibraryViewMixin, LayoutMixin from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin from calibre.gui2.tag_view import TagBrowserMixin @@ -91,7 +91,7 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{ # }}} -class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{ +class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin, SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin, AnnotationsAction, AddAction, DeleteAction, @@ -192,21 +192,14 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{ ####################### Start spare job server ######################## QTimer.singleShot(1000, self.add_spare_server) - ####################### Location View ######################## - QObject.connect(self.location_view, - SIGNAL('location_selected(PyQt_PyObject)'), - self.location_selected) - QObject.connect(self.location_view, - SIGNAL('umount_device()'), - self.device_manager.umount_device) + ####################### Location Manager ######################## + self.location_manager.location_selected.connect(self.location_selected) + self.location_manager.unmount_device.connect(self.device_manager.umount_device) self.eject_action.triggered.connect(self.device_manager.umount_device) #################### Update notification ################### UpdateMixin.__init__(self, opts) - ####################### Setup Toolbar ##################### - ToolbarMixin.__init__(self) - ####################### Search boxes ######################## SavedSearchBoxMixin.__init__(self) SearchBoxMixin.__init__(self) @@ -218,7 +211,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{ if self.system_tray_icon.isVisible() and opts.start_in_tray: self.hide_windows() - for t in (self.location_view, self.tool_bar): + for t in (self.tool_bar, ): self.library_view.model().count_changed_signal.connect \ (t.count_changed) if not gprefs.get('quick_start_guide_added', False): @@ -235,8 +228,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{ self.db_images.reset() self.library_view.model().count_changed() - self.location_view.model().database_changed(self.library_view.model().db) - self.library_view.model().database_changed.connect(self.location_view.model().database_changed, + self.tool_bar.database_changed(self.library_view.model().db) + self.library_view.model().database_changed.connect(self.tool_bar.database_changed, type=Qt.QueuedConnection) ########################### Tags Browser ##############################