Nicer unified toolbar

This commit is contained in:
Kovid Goyal 2010-07-16 20:45:33 -06:00
parent 93ece30686
commit 0ad6ab164f
5 changed files with 302 additions and 431 deletions

View File

@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys import os, sys
from threading import RLock 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, \ QByteArray, QTranslator, QCoreApplication, QThread, \
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \ QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \
QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \ QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
@ -33,10 +33,6 @@ def _config():
help=_('Send file to storage card instead of main memory by default')) help=_('Send file to storage card instead of main memory by default'))
c.add_opt('confirm_delete', default=False, c.add_opt('confirm_delete', default=False,
help=_('Confirm before deleting')) 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, c.add_opt('main_window_geometry', default=None,
help=_('Main window geometry')) # value QVariant.toByteArray help=_('Main window geometry')) # value QVariant.toByteArray
c.add_opt('new_version_notification', default=True, c.add_opt('new_version_notification', default=True,

View File

@ -638,7 +638,6 @@ class DeviceMixin(object): # {{{
self.device_error_dialog = error_dialog(self, _('Error'), self.device_error_dialog = error_dialog(self, _('Error'),
_('Error communicating with device'), ' ') _('Error communicating with device'), ' ')
self.device_error_dialog.setModal(Qt.NonModal) self.device_error_dialog.setModal(Qt.NonModal)
self.device_connected = None
self.emailer = Emailer() self.emailer = Emailer()
self.emailer.start() self.emailer.start()
self.device_manager = DeviceManager(Dispatcher(self.device_detected), self.device_manager = DeviceManager(Dispatcher(self.device_detected),
@ -755,17 +754,14 @@ class DeviceMixin(object): # {{{
self.device_manager.device.__class__.get_gui_name()+\ self.device_manager.device.__class__.get_gui_name()+\
_(' detected.'), 3000) _(' detected.'), 3000)
self.device_connected = device_kind 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) self.refresh_ondevice_info (device_connected = True, reset_only = True)
else: else:
self.device_connected = None self.device_connected = None
self.status_bar.device_disconnected() self.status_bar.device_disconnected()
self.location_view.model().update_devices()
if self.current_view() != self.library_view: if self.current_view() != self.library_view:
self.book_details.reset_info() self.book_details.reset_info()
self.location_view.setCurrentIndex(self.location_view.model().index(0)) self.location_manager.update_devices()
self.refresh_ondevice_info (device_connected = False) self.refresh_ondevice_info(device_connected=False)
self.tool_bar.device_status_changed(bool(connected))
def info_read(self, job): def info_read(self, job):
''' '''
@ -774,7 +770,8 @@ class DeviceMixin(object): # {{{
if job.failed: if job.failed:
return self.device_job_exception(job) return self.device_job_exception(job)
info, cp, fs = job.result 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.status_bar.device_connected(info[0])
self.device_manager.books(Dispatcher(self.metadata_downloaded)) self.device_manager.books(Dispatcher(self.metadata_downloaded))
@ -1076,9 +1073,9 @@ class DeviceMixin(object): # {{{
dynamic.set('catalogs_to_be_synced', set([])) dynamic.set('catalogs_to_be_synced', set([]))
if files: if files:
remove = [] remove = []
space = { self.location_view.model().free[0] : None, space = { self.location_manager.free[0] : None,
self.location_view.model().free[1] : 'carda', self.location_manager.free[1] : 'carda',
self.location_view.model().free[2] : 'cardb' } self.location_manager.free[2] : 'cardb' }
on_card = space.get(sorted(space.keys(), reverse=True)[0], None) on_card = space.get(sorted(space.keys(), reverse=True)[0], None)
self.upload_books(files, names, metadata, self.upload_books(files, names, metadata,
on_card=on_card, on_card=on_card,
@ -1140,9 +1137,9 @@ class DeviceMixin(object): # {{{
dynamic.set('news_to_be_synced', set([])) dynamic.set('news_to_be_synced', set([]))
if config['upload_news_to_device'] and files: if config['upload_news_to_device'] and files:
remove = ids if del_on_upload else [] remove = ids if del_on_upload else []
space = { self.location_view.model().free[0] : None, space = { self.location_manager.free[0] : None,
self.location_view.model().free[1] : 'carda', self.location_manager.free[1] : 'carda',
self.location_view.model().free[2] : 'cardb' } self.location_manager.free[2] : 'cardb' }
on_card = space.get(sorted(space.keys(), reverse=True)[0], None) on_card = space.get(sorted(space.keys(), reverse=True)[0], None)
self.upload_books(files, names, metadata, self.upload_books(files, names, metadata,
on_card=on_card, on_card=on_card,
@ -1263,7 +1260,8 @@ class DeviceMixin(object): # {{{
self.device_job_exception(job) self.device_job_exception(job)
return return
cp, fs = job.result 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 # 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 # here because the sony driver updates collections in sync_booklists
self.memory_view.reset() self.memory_view.reset()

View File

@ -7,14 +7,13 @@ __docformat__ = 'restructuredtext en'
import functools, sys, os import functools, sys, os
from PyQt4.Qt import QMenu, Qt, pyqtSignal, QIcon, QStackedWidget, \ from PyQt4.Qt import QMenu, Qt, QStackedWidget, \
QSize, QSizePolicy, QStatusBar, QUrl, QLabel, QFont QSize, QSizePolicy, QStatusBar, QLabel, QFont
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.constants import isosx, __appname__, preferred_encoding, \ from calibre.constants import isosx, __appname__, preferred_encoding, \
__version__ __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.library.views import BooksView, DeviceBooksView
from calibre.gui2.widgets import Splitter from calibre.gui2.widgets import Splitter
from calibre.gui2.tag_view import TagBrowserWidget from calibre.gui2.tag_view import TagBrowserWidget
@ -28,157 +27,6 @@ def partial(*args, **kwargs):
_keep_refs.append(ans) _keep_refs.append(ans)
return 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): # {{{ class LibraryViewMixin(object): # {{{
def __init__(self, db): def __init__(self, db):

View File

@ -6,108 +6,105 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from operator import attrgetter from operator import attrgetter
from functools import partial
from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, QVariant, \ from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, \
QAbstractListModel, QFont, QApplication, QPalette, pyqtSignal, QToolButton, \ pyqtSignal, QToolButton, \
QModelIndex, QListView, QAbstractButton, QPainter, QPixmap, QColor, \ QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup, \
QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout 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.search_box import SearchBox2, SavedSearchBox
from calibre.gui2.throbber import ThrobbingButton 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.gui2.widgets import ComboBoxWithHelp
from calibre import human_readable from calibre import human_readable
from calibre.utils.config import prefs
from calibre.ebooks import BOOK_EXTENSIONS
ICON_SIZE = 48 ICON_SIZE = 48
# Location View {{{ class SaveMenu(QMenu): # {{{
class LocationModel(QAbstractListModel): # {{{ save_fmt = pyqtSignal(object)
devicesChanged = pyqtSignal()
def __init__(self, parent): def __init__(self, parent):
QAbstractListModel.__init__(self, parent) QMenu.__init__(self, _('Save single format to disk...'), parent)
self.icons = [QVariant(QIcon(I('library.png'))), for ext in sorted(BOOK_EXTENSIONS):
QVariant(QIcon(I('reader.svg'))), action = self.addAction(ext.upper())
QVariant(QIcon(I('sd.svg'))), setattr(self, 'do_'+ext, partial(self.do, ext))
QVariant(QIcon(I('sd.svg')))] action.triggered.connect(
self.text = [_('Library\n%d books'), getattr(self, 'do_'+ext))
_('Reader\n%s'),
_('Card A\n%s'), def do(self, ext, *args):
_('Card B\n%s')] 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.free = [-1, -1, -1]
self.count = 0 self.count = 0
self.highlight_row = 0 self.location_actions = QActionGroup(self)
self.library_tooltip = _('Click to see the books available on your computer') self.location_actions.setExclusive(True)
self.tooltips = [ self.current_location = 'library'
self.library_tooltip, self._mem = []
_('Click to see the books in the main memory of your reader'), self.tooltips = {}
_('Click to see the books on storage card A in your reader'),
_('Click to see the books on storage card B in your reader')
]
def database_changed(self, db): def ac(name, text, icon, tooltip):
lp = db.library_path icon = QIcon(I(icon))
if not isinstance(lp, unicode): ac = self.location_actions.addAction(icon, text)
lp = lp.decode(filesystem_encoding, 'replace') setattr(self, 'location_'+name, ac)
self.tooltips[0] = self.library_tooltip + '\n\n' + \ ac.setAutoRepeat(False)
_('Books located at') + ' ' + lp ac.setCheckable(True)
self.dataChanged.emit(self.index(0), self.index(0)) 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 ac
return 1 + len([i for i in self.free if i >= 0])
def get_device_row(self, row): ac('library', _('Library'), 'lt.png',
if row == 2 and self.free[1] == -1 and self.free[2] > -1: _('Show books in calibre library'))
row = 3 ac('main', _('Main'), 'reader.svg',
return row _('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): def _location_selected(self, location, *args):
ans = self.tooltips[row] if location != self.current_location and hasattr(self,
if row > 0: 'location_'+location):
fs = self.free[drow-1] self.current_location = location
if fs > -1: self.location_selected.emit(location)
ans += '\n\n%s '%(human_readable(fs)) + _('free') getattr(self, 'location_'+location).setChecked(True)
return ans
def data(self, index, role): def _eject_requested(self, *args):
row = index.row() self.unmount_device.emit()
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))
return data def update_devices(self, cp=(None, None), fs=[-1, -1, -1], icon=None):
if icon is None:
def device_connected(self, dev): icon = I('reader.svg')
self.icons[1] = QIcon(dev.icon) self.location_main.setIcon(QIcon(icon))
self.dataChanged.emit(self.index(1), self.index(1)) had_device = self.has_device
def headerData(self, section, orientation, role):
return NONE
def update_devices(self, cp=(None, None), fs=[-1, -1, -1]):
if cp is None: if cp is None:
cp = (None, None) cp = (None, None)
if isinstance(cp, (str, unicode)): if isinstance(cp, (str, unicode)):
@ -120,137 +117,34 @@ class LocationModel(QAbstractListModel): # {{{
cpa, cpb = cp cpa, cpb = cp
self.free[1] = fs[1] if fs[1] is not None and cpa is not None else -1 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.free[2] = fs[2] if fs[2] is not None and cpb is not None else -1
self.reset() self.update_tooltips()
self.devicesChanged.emit() if self.has_device != had_device:
self.locations_changed.emit()
if not self.has_device:
self.location_library.trigger()
def location_changed(self, row): def update_tooltips(self):
self.highlight_row = row for i, loc in enumerate(('main', 'carda', 'cardb')):
self.dataChanged.emit( t = self.tooltips[loc]
self.index(0), self.index(self.rowCount(QModelIndex())-1)) 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 @property
def book_count(self): def has_device(self):
return self.model().count return max(self.free) > -1
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)
@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): # {{{ 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) QToolBar.__init__(self, parent)
self.setContextMenuPolicy(Qt.PreventContextMenu) self.setContextMenuPolicy(Qt.PreventContextMenu)
self.setMovable(False) self.setMovable(False)
@ -335,11 +229,12 @@ class ToolBar(QToolBar): # {{{
self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea) self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
self.setIconSize(QSize(ICON_SIZE, ICON_SIZE)) self.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
self.setStyleSheet('QToolButton:checked { font-weight: bold }')
self.showing_device = False
self.all_actions = actions self.all_actions = actions
self.donate = donate 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 = QWidget()
self.d_widget.setLayout(QVBoxLayout()) self.d_widget.setLayout(QVBoxLayout())
self.d_widget.layout().addWidget(donate) self.d_widget.layout().addWidget(donate)
@ -350,40 +245,45 @@ class ToolBar(QToolBar): # {{{
def contextMenuEvent(self, *args): def contextMenuEvent(self, *args):
pass pass
def device_status_changed(self, connected):
self.showing_device = connected
self.build_bar()
def build_bar(self): 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') o = attrgetter(order_field+'_order')
sepvals = [2] if self.showing_device else [1] sepvals = [2] if showing_device else [1]
sepvals += [3] sepvals += [3]
actions = [x for x in self.all_actions if o(x) > -1] actions = [x for x in self.all_actions if o(x) > -1]
actions.sort(cmp=lambda x,y : cmp(o(x), o(y))) actions.sort(cmp=lambda x,y : cmp(o(x), o(y)))
self.clear() 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.setCursor(Qt.PointingHandCursor)
ch.setAutoRaise(True) ch.setAutoRaise(True)
if ac.menu() is not None:
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'):
ch.setPopupMode(ch.MenuButtonPopup) 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: for x in actions:
if x.separator_before in sepvals: if x.separator_before in sepvals:
self.insertSeparator(x) self.insertSeparator(x)
self.choose_action.setVisible(not showing_device)
self.location_action.setVisible(self.showing_device)
self.choose_action.setVisible(not self.showing_device)
def count_changed(self, new_count): def count_changed(self, new_count):
text = _('%d books')%new_count text = _('%d books')%new_count
@ -397,6 +297,9 @@ class ToolBar(QToolBar): # {{{
self.setToolButtonStyle(style) self.setToolButtonStyle(style)
QToolBar.resizeEvent(self, ev) QToolBar.resizeEvent(self, ev)
def database_changed(self, db):
pass
# }}} # }}}
class Action(QAction): class Action(QAction):
@ -405,6 +308,7 @@ class Action(QAction):
class MainWindowMixin(object): class MainWindowMixin(object):
def __init__(self): def __init__(self):
self.device_connected = None
self.setObjectName('MainWindow') self.setObjectName('MainWindow')
self.setWindowIcon(QIcon(I('library.png'))) self.setWindowIcon(QIcon(I('library.png')))
self.setWindowTitle(__appname__) self.setWindowTitle(__appname__)
@ -417,9 +321,23 @@ class MainWindowMixin(object):
self.resize(1012, 740) self.resize(1012, 740)
self.donate_button = ThrobbingButton(self.centralwidget) self.donate_button = ThrobbingButton(self.centralwidget)
self.donate_button.set_normal_icon_size(ICON_SIZE, ICON_SIZE) 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 = [] all_actions = []
def ac(normal_order, device_order, separator_before, 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'), ac(-1, -1, 0, 'books_with_the_same_tags', _('Books with the same tags'),
'tags.svg') '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) mb = QMenu()
self.search_bar = SearchBar(self) mb.addAction(_('Merge into first selected book - delete others'),
self.tool_bar = ToolBar(all_actions, self.donate_button, self.location_view, self) self.merge_books)
self.addToolBar(Qt.TopToolBarArea, self.tool_bar) 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() self.add_menu = QMenu()
l.addWidget(self.search_bar) 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

View File

@ -12,7 +12,7 @@ __docformat__ = 'restructuredtext en'
import collections, os, sys, textwrap, time import collections, os, sys, textwrap, time
from Queue import Queue, Empty from Queue import Queue, Empty
from threading import Thread from threading import Thread
from PyQt4.Qt import Qt, SIGNAL, QObject, QTimer, \ from PyQt4.Qt import Qt, SIGNAL, QTimer, \
QPixmap, QMenu, QIcon, pyqtSignal, \ QPixmap, QMenu, QIcon, pyqtSignal, \
QDialog, \ QDialog, \
QSystemTrayIcon, QApplication, QKeySequence, QAction, \ 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.gui2.dialogs.book_info import BookInfo
from calibre.library.database2 import LibraryDatabase2 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_box import SearchBoxMixin, SavedSearchBoxMixin
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
from calibre.gui2.tag_view import TagBrowserMixin 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, TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin, SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin,
AnnotationsAction, AddAction, DeleteAction, AnnotationsAction, AddAction, DeleteAction,
@ -192,21 +192,14 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
####################### Start spare job server ######################## ####################### Start spare job server ########################
QTimer.singleShot(1000, self.add_spare_server) QTimer.singleShot(1000, self.add_spare_server)
####################### Location View ######################## ####################### Location Manager ########################
QObject.connect(self.location_view, self.location_manager.location_selected.connect(self.location_selected)
SIGNAL('location_selected(PyQt_PyObject)'), self.location_manager.unmount_device.connect(self.device_manager.umount_device)
self.location_selected)
QObject.connect(self.location_view,
SIGNAL('umount_device()'),
self.device_manager.umount_device)
self.eject_action.triggered.connect(self.device_manager.umount_device) self.eject_action.triggered.connect(self.device_manager.umount_device)
#################### Update notification ################### #################### Update notification ###################
UpdateMixin.__init__(self, opts) UpdateMixin.__init__(self, opts)
####################### Setup Toolbar #####################
ToolbarMixin.__init__(self)
####################### Search boxes ######################## ####################### Search boxes ########################
SavedSearchBoxMixin.__init__(self) SavedSearchBoxMixin.__init__(self)
SearchBoxMixin.__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: if self.system_tray_icon.isVisible() and opts.start_in_tray:
self.hide_windows() 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 \ self.library_view.model().count_changed_signal.connect \
(t.count_changed) (t.count_changed)
if not gprefs.get('quick_start_guide_added', False): if not gprefs.get('quick_start_guide_added', False):
@ -235,8 +228,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
self.db_images.reset() self.db_images.reset()
self.library_view.model().count_changed() self.library_view.model().count_changed()
self.location_view.model().database_changed(self.library_view.model().db) self.tool_bar.database_changed(self.library_view.model().db)
self.library_view.model().database_changed.connect(self.location_view.model().database_changed, self.library_view.model().database_changed.connect(self.tool_bar.database_changed,
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
########################### Tags Browser ############################## ########################### Tags Browser ##############################