PK 4U3 common_compatibility.py#!/usr/bin/env python # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai from __future__ import (unicode_literals, division, absolute_import, print_function) __license__ = 'GPL v3' __copyright__ = '2022, Grant Drake' # Maintain backwards compatibility with older versions of Qt and calibre. try: from qt.core import QSizePolicy, QTextEdit, Qt except ImportError: from PyQt5.Qt import QSizePolicy, QTextEdit, Qt try: qSizePolicy_Minimum = QSizePolicy.Policy.Minimum qSizePolicy_Maximum = QSizePolicy.Policy.Maximum qSizePolicy_Expanding = QSizePolicy.Policy.Expanding qSizePolicy_Preferred = QSizePolicy.Policy.Preferred qSizePolicy_Ignored = QSizePolicy.Policy.Ignored except: qSizePolicy_Minimum = QSizePolicy.Minimum qSizePolicy_Maximum = QSizePolicy.Maximum qSizePolicy_Expanding = QSizePolicy.Expanding qSizePolicy_Preferred = QSizePolicy.Preferred qSizePolicy_Ignored = QSizePolicy.Ignored try: qTextEdit_NoWrap = QTextEdit.LineWrapMode.NoWrap except: qTextEdit_NoWrap = QTextEdit.NoWrap try: qtDropActionCopyAction = Qt.DropAction.CopyAction qtDropActionMoveAction = Qt.DropAction.MoveAction except: qtDropActionCopyAction = Qt.CopyAction qtDropActionMoveAction = Qt.MoveAction PK {~PUbpG. G. common_dialogs.py#!/usr/bin/env python # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai from __future__ import (unicode_literals, division, absolute_import, print_function) __license__ = 'GPL v3' __copyright__ = '2022, Grant Drake' # calibre Python 3 compatibility. import six from six import text_type as unicode try: from qt.core import (QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget, QProgressBar, QAbstractItemView, QTextEdit, QIcon, QApplication, Qt, QTextBrowser, QSize, QLabel) except ImportError: from PyQt5.Qt import (QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget, QProgressBar, QAbstractItemView, QTextEdit, QIcon, QApplication, Qt, QTextBrowser, QSize, QLabel) try: load_translations() except NameError: pass # load_translations() from calibre.gui2 import gprefs, info_dialog, Application from calibre.gui2.keyboard import ShortcutConfig from calibre_plugins.baen.common_icons import get_icon # ---------------------------------------------- # Dialog functions # ---------------------------------------------- class SizePersistedDialog(QDialog): ''' This dialog is a base class for any dialogs that want their size/position restored when they are next opened. ''' def __init__(self, parent, unique_pref_name): QDialog.__init__(self, parent) self.unique_pref_name = unique_pref_name self.geom = gprefs.get(unique_pref_name, None) self.finished.connect(self.dialog_closing) def resize_dialog(self): if self.geom is None: self.resize(self.sizeHint()) else: self.restoreGeometry(self.geom) def dialog_closing(self, result): geom = bytearray(self.saveGeometry()) gprefs[self.unique_pref_name] = geom self.persist_custom_prefs() def persist_custom_prefs(self): ''' Invoked when the dialog is closing. Override this function to call save_custom_pref() if you have a setting you want persisted that you can retrieve in your __init__() using load_custom_pref() when next opened ''' pass def load_custom_pref(self, name, default=None): return gprefs.get(self.unique_pref_name+':'+name, default) def save_custom_pref(self, name, value): gprefs[self.unique_pref_name+':'+name] = value def help_link_activated(self, url): if self.plugin_action is not None: self.plugin_action.show_help(anchor=self.help_anchor) class KeyboardConfigDialog(SizePersistedDialog): ''' This dialog is used to allow editing of keyboard shortcuts. ''' def __init__(self, gui, group_name): SizePersistedDialog.__init__(self, gui, 'Keyboard shortcut dialog') self.gui = gui self.setWindowTitle(_('Keyboard shortcuts')) layout = QVBoxLayout(self) self.setLayout(layout) self.keyboard_widget = ShortcutConfig(self) layout.addWidget(self.keyboard_widget) self.group_name = group_name button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(self.commit) button_box.rejected.connect(self.reject) layout.addWidget(button_box) # Cause our dialog size to be restored from prefs or created on first usage self.resize_dialog() self.initialize() def initialize(self): self.keyboard_widget.initialize(self.gui.keyboard) self.keyboard_widget.highlight_group(self.group_name) def commit(self): self.keyboard_widget.commit() self.accept() def prompt_for_restart(parent, title, message): d = info_dialog(parent, title, message, show_copy_button=False) b = d.bb.addButton(_('Restart calibre now'), d.bb.AcceptRole) b.setIcon(QIcon(I('lt.png'))) d.do_restart = False def rf(): d.do_restart = True b.clicked.connect(rf) d.set_details('') d.exec_() b.clicked.disconnect() return d.do_restart class PrefsViewerDialog(SizePersistedDialog): def __init__(self, gui, namespace): SizePersistedDialog.__init__(self, gui, 'Prefs Viewer dialog') self.setWindowTitle(_('Preferences for:')+' '+namespace) self.gui = gui self.db = gui.current_db self.namespace = namespace self._init_controls() self.resize_dialog() self._populate_settings() if self.keys_list.count(): self.keys_list.setCurrentRow(0) def _init_controls(self): layout = QVBoxLayout(self) self.setLayout(layout) ml = QHBoxLayout() layout.addLayout(ml, 1) self.keys_list = QListWidget(self) self.keys_list.setSelectionMode(QAbstractItemView.SingleSelection) self.keys_list.setFixedWidth(150) self.keys_list.setAlternatingRowColors(True) ml.addWidget(self.keys_list) self.value_text = QTextEdit(self) self.value_text.setReadOnly(False) ml.addWidget(self.value_text, 1) button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(self._apply_changes) button_box.rejected.connect(self.reject) self.clear_button = button_box.addButton(_('Clear'), QDialogButtonBox.ResetRole) self.clear_button.setIcon(get_icon('trash.png')) self.clear_button.setToolTip(_('Clear all settings for this plugin')) self.clear_button.clicked.connect(self._clear_settings) layout.addWidget(button_box) def _populate_settings(self): self.keys_list.clear() ns_prefix = self._get_ns_prefix() keys = sorted([k[len(ns_prefix):] for k in six.iterkeys(self.db.prefs) if k.startswith(ns_prefix)]) for key in keys: self.keys_list.addItem(key) self.keys_list.setMinimumWidth(self.keys_list.sizeHintForColumn(0)) self.keys_list.currentRowChanged[int].connect(self._current_row_changed) def _current_row_changed(self, new_row): if new_row < 0: self.value_text.clear() return key = unicode(self.keys_list.currentItem().text()) val = self.db.prefs.get_namespaced(self.namespace, key, '') self.value_text.setPlainText(self.db.prefs.to_raw(val)) def _get_ns_prefix(self): return 'namespaced:%s:'% self.namespace def _apply_changes(self): from calibre.gui2.dialogs.confirm_delete import confirm message = '
'+_('Are you sure you want to change your settings in this library for this plugin?')+'
' \ ''+_('Any settings in other libraries or stored in a JSON file in your calibre plugins ' \ 'folder will not be touched.')+'
' \ '<>'+_('You must restart calibre afterwards.')+'' if not confirm(message, self.namespace+'_clear_settings', self): return val = self.db.prefs.raw_to_object(unicode(self.value_text.toPlainText())) key = unicode(self.keys_list.currentItem().text()) self.db.prefs.set_namespaced(self.namespace, key, val) restart = prompt_for_restart(self, _('Settings changed'), ''+_('Settings for this plugin in this library have been changed.')+'
' \ ''+_('Please restart calibre now.')+'
') self.close() if restart: self.gui.quit(restart=True) def _clear_settings(self): from calibre.gui2.dialogs.confirm_delete import confirm message = ''+_('Are you sure you want to clear your settings in this library for this plugin?')+'
' \ ''+_('Any settings in other libraries or stored in a JSON file in your calibre plugins ' \ 'folder will not be touched.')+'
' \ ''+_('You must restart calibre afterwards.')+'
' if not confirm(message, self.namespace+'_clear_settings', self): return ns_prefix = self._get_ns_prefix() keys = [k for k in six.iterkeys(self.db.prefs) if k.startswith(ns_prefix)] for k in keys: del self.db.prefs[k] self._populate_settings() restart = prompt_for_restart(self, _('Settings deleted'), ''+_('All settings for this plugin in this library have been cleared.')+'
' ''+_('Please restart calibre now.')+'
') self.close() if restart: self.gui.quit(restart=True) class ProgressBarDialog(QDialog): def __init__(self, parent=None, max_items=100, window_title='Progress Bar', label='Label goes here', on_top=False): if on_top: super(ProgressBarDialog, self).__init__(parent=parent, flags=Qt.WindowStaysOnTopHint) else: super(ProgressBarDialog, self).__init__(parent=parent) self.application = Application self.setWindowTitle(window_title) self.l = QVBoxLayout(self) self.setLayout(self.l) self.label = QLabel(label) # self.label.setAlignment(Qt.AlignHCenter) self.l.addWidget(self.label) self.progressBar = QProgressBar(self) self.progressBar.setRange(0, max_items) self.progressBar.setValue(0) self.l.addWidget(self.progressBar) def increment(self): self.progressBar.setValue(self.progressBar.value() + 1) self.refresh() def refresh(self): self.application.processEvents() def set_label(self, value): self.label.setText(value) self.refresh() def left_align_label(self): self.label.setAlignment(Qt.AlignLeft ) def set_maximum(self, value): self.progressBar.setMaximum(value) self.refresh() def set_value(self, value): self.progressBar.setValue(value) self.refresh() def set_progress_format(self, progress_format=None): pass class ViewLogDialog(QDialog): def __init__(self, title, html, parent=None): QDialog.__init__(self, parent) self.l = l = QVBoxLayout() self.setLayout(l) self.tb = QTextBrowser(self) QApplication.setOverrideCursor(Qt.WaitCursor) # Rather than formatting the text inblocks like the calibre # ViewLog does, instead just format it inside divs to keep style formatting html = html.replace('\t',' ').replace('\n', '
') html = html.replace('> ','> ') self.tb.setHtml('%s' % html) QApplication.restoreOverrideCursor() l.addWidget(self.tb) self.bb = QDialogButtonBox(QDialogButtonBox.Ok) self.bb.accepted.connect(self.accept) self.bb.rejected.connect(self.reject) self.copy_button = self.bb.addButton(_('Copy to clipboard'), self.bb.ActionRole) self.copy_button.setIcon(QIcon(I('edit-copy.png'))) self.copy_button.clicked.connect(self.copy_to_clipboard) l.addWidget(self.bb) self.setModal(False) self.resize(QSize(700, 500)) self.setWindowTitle(title) self.setWindowIcon(QIcon(I('debug.png'))) self.show() def copy_to_clipboard(self): txt = self.tb.toPlainText() QApplication.clipboard().setText(txt) PK [3U9` ` common_icons.py#!/usr/bin/env python # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai from __future__ import (unicode_literals, division, absolute_import, print_function) __license__ = 'GPL v3' __copyright__ = '2022, Grant Drake' import os # calibre Python 3 compatibility. import six try: from qt.core import (QIcon, QPixmap) except ImportError: from PyQt5.Qt import (QIcon, QPixmap) from calibre.constants import iswindows from calibre.constants import numeric_version as calibre_version from calibre.utils.config import config_dir # ---------------------------------------------- # Global resources / state # ---------------------------------------------- # Global definition of our plugin name. Used for common functions that require this. plugin_name = None # Global definition of our plugin resources. Used to share between the xxxAction and xxxBase # classes if you need any zip images to be displayed on the configuration dialog. plugin_icon_resources = {} def set_plugin_icon_resources(name, resources): ''' Set our global store of plugin name and icon resources for sharing between the InterfaceAction class which reads them and the ConfigWidget if needed for use on the customization dialog for this plugin. ''' global plugin_icon_resources, plugin_name plugin_name = name plugin_icon_resources = resources # ---------------------------------------------- # Icon Management functions # ---------------------------------------------- def get_icon_6_2_plus(icon_name): ''' Retrieve a QIcon for the named image from 1. Calibre's image cache 2. resources/images 3. the icon theme 4. the plugin zip Only plugin zip has images/ in the image name for backward compatibility. ''' icon = None if icon_name: icon = QIcon.ic(icon_name) ## both .ic and get_icons return an empty QIcon if not found. if not icon or icon.isNull(): icon = get_icons(icon_name.replace('images/',''), plugin_name, print_tracebacks_for_missing_resources=False) if not icon or icon.isNull(): icon = get_icons(icon_name, plugin_name, print_tracebacks_for_missing_resources=False) if not icon: icon = QIcon() return icon def get_icon_old(icon_name): ''' Retrieve a QIcon for the named image from the zip file if it exists, or if not then from Calibre's image cache. ''' if icon_name: pixmap = get_pixmap(icon_name) if pixmap is None: # Look in Calibre's cache for the icon return QIcon(I(icon_name)) else: return QIcon(pixmap) return QIcon() def get_pixmap(icon_name): ''' Retrieve a QPixmap for the named image Any icons belonging to the plugin must be prefixed with 'images/' ''' global plugin_icon_resources, plugin_name if not icon_name.startswith('images/'): # We know this is definitely not an icon belonging to this plugin pixmap = QPixmap() pixmap.load(I(icon_name)) return pixmap # Check to see whether the icon exists as a Calibre resource # This will enable skinning if the user stores icons within a folder like: # ...\AppData\Roaming\calibre\resources\images\Plugin Name\ if plugin_name: local_images_dir = get_local_images_dir(plugin_name) local_image_path = os.path.join(local_images_dir, icon_name.replace('images/', '')) if os.path.exists(local_image_path): pixmap = QPixmap() pixmap.load(local_image_path) return pixmap # As we did not find an icon elsewhere, look within our zip resources if icon_name in plugin_icon_resources: pixmap = QPixmap() pixmap.loadFromData(plugin_icon_resources[icon_name]) return pixmap return None def get_local_images_dir(subfolder=None): ''' Returns a path to the user's local resources/images folder If a subfolder name parameter is specified, appends this to the path ''' images_dir = os.path.join(config_dir, 'resources/images') if subfolder: images_dir = os.path.join(images_dir, subfolder) if iswindows: images_dir = os.path.normpath(images_dir) return images_dir if calibre_version >= (6,2,0): get_icon = get_icon_6_2_plus else: get_icon = get_icon_old PK {~PU, common_menus.py#!/usr/bin/env python # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai from __future__ import (unicode_literals, division, absolute_import, print_function) __license__ = 'GPL v3' __copyright__ = '2022, Grant Drake' from calibre.gui2.actions import menu_action_unique_name from calibre.constants import numeric_version as calibre_version from calibre_plugins.baen.common_icons import get_icon # ---------------------------------------------- # Global resources / state # ---------------------------------------------- # Global definition of our menu actions. Used to ensure we can cleanly unregister # keyboard shortcuts when rebuilding our menus. plugin_menu_actions = [] # ---------------------------------------------- # Menu functions # ---------------------------------------------- def unregister_menu_actions(ia): ''' For plugins that dynamically rebuild their menus, we need to ensure that any keyboard shortcuts are unregistered for them each time. Make sure to call this before .clear() of the menu items. ''' global plugin_menu_actions for action in plugin_menu_actions: ia.gui.keyboard.unregister_shortcut(action.calibre_shortcut_unique_name) # starting in calibre 2.10.0, actions are registers at # the top gui level for OSX' benefit. if calibre_version >= (2,10,0): ia.gui.removeAction(action) plugin_menu_actions = [] def create_menu_action_unique(ia, parent_menu, menu_text, image=None, tooltip=None, shortcut=None, triggered=None, is_checked=None, shortcut_name=None, unique_name=None, favourites_menu_unique_name=None): ''' Create a menu action with the specified criteria and action, using the new InterfaceAction.create_menu_action() function which ensures that regardless of whether a shortcut is specified it will appear in Preferences->Keyboard For a full description of the parameters, see: calibre\gui2\actions\__init__.py ''' orig_shortcut = shortcut kb = ia.gui.keyboard if unique_name is None: unique_name = menu_text if not shortcut == False: full_unique_name = menu_action_unique_name(ia, unique_name) if full_unique_name in kb.shortcuts: shortcut = False else: if shortcut is not None and not shortcut == False: if len(shortcut) == 0: shortcut = None if shortcut_name is None: shortcut_name = menu_text.replace('&','') if calibre_version >= (5,4,0): # The persist_shortcut parameter only added from 5.4.0 onwards. # Used so that shortcuts specific to other libraries aren't discarded. ac = ia.create_menu_action(parent_menu, unique_name, menu_text, icon=None, shortcut=shortcut, description=tooltip, triggered=triggered, shortcut_name=shortcut_name, persist_shortcut=True) else: ac = ia.create_menu_action(parent_menu, unique_name, menu_text, icon=None, shortcut=shortcut, description=tooltip, triggered=triggered, shortcut_name=shortcut_name) if shortcut == False and not orig_shortcut == False: if ac.calibre_shortcut_unique_name in ia.gui.keyboard.shortcuts: kb.replace_action(ac.calibre_shortcut_unique_name, ac) if image: ac.setIcon(get_icon(image)) if is_checked is not None: ac.setCheckable(True) if is_checked: ac.setChecked(True) # For use by the Favourites Menu plugin. If this menu action has text # that is not constant through the life of this plugin, then we need # to attribute it with something that will be constant that the # Favourites Menu plugin can use to identify it. if favourites_menu_unique_name: ac.favourites_menu_unique_name = favourites_menu_unique_name # Append to our list of actions for this plugin to unregister when menu rebuilt global plugin_menu_actions plugin_menu_actions.append(ac) return ac def create_menu_item(ia, parent_menu, menu_text, image=None, tooltip=None, shortcut=(), triggered=None, is_checked=None): ''' Create a menu action with the specified criteria and action Note that if no shortcut is specified, will not appear in Preferences->Keyboard This method should only be used for actions which either have no shortcuts, or register their menus only once. Use create_menu_action_unique for all else. Currently this function is only used by open_with and search_the_internet plugins and would like to investigate one day if it can be removed from them. ''' if shortcut is not None: if len(shortcut) == 0: shortcut = () ac = ia.create_action(spec=(menu_text, None, tooltip, shortcut), attr=menu_text) if image: ac.setIcon(get_icon(image)) if triggered is not None: ac.triggered.connect(triggered) if is_checked is not None: ac.setCheckable(True) if is_checked: ac.setChecked(True) parent_menu.addAction(ac) # Append to our list of actions for this plugin to unregister when menu rebuilt global plugin_menu_actions plugin_menu_actions.append(ac) return ac PK {~PUKhc. . common_widgets.py#!/usr/bin/env python # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai from __future__ import (unicode_literals, division, absolute_import, print_function) __license__ = 'GPL v3' __copyright__ = '2022, Grant Drake' from six import text_type as unicode try: from qt.core import (Qt, QTableWidgetItem, QComboBox, QHBoxLayout, QLabel, QFont, QDateTime, QStyledItemDelegate, QLineEdit) except ImportError: from PyQt5.Qt import (Qt, QTableWidgetItem, QComboBox, QHBoxLayout, QLabel, QFont, QDateTime, QStyledItemDelegate, QLineEdit) try: load_translations() except NameError: pass # load_translations() added in calibre 1.9 from calibre.gui2 import error_dialog, UNDEFINED_QDATETIME from calibre.utils.date import now, format_date, UNDEFINED_DATE from calibre_plugins.baen.common_icons import get_pixmap # CheckableTableWidgetItem # DateDelegate # DateTableWidgetItem # ImageTitleLayout # ReadOnlyTableWidgetItem # ReadOnlyTextIconWidgetItem # ReadOnlyCheckableTableWidgetItem # TextIconWidgetItem # # CustomColumnComboBox # KeyValueComboBox # NoWheelComboBox # ReadOnlyLineEdit # ---------------------------------------------- # Widgets # ---------------------------------------------- class CheckableTableWidgetItem(QTableWidgetItem): ''' For use in a table cell, displays a checkbox that can potentially be tristate ''' def __init__(self, checked=False, is_tristate=False): super(CheckableTableWidgetItem, self).__init__('') try: self.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsEnabled ) except: self.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled )) if is_tristate: self.setFlags(self.flags() | Qt.ItemFlag.ItemIsUserTristate) if checked: self.setCheckState(Qt.Checked) else: if is_tristate and checked is None: self.setCheckState(Qt.PartiallyChecked) else: self.setCheckState(Qt.Unchecked) def get_boolean_value(self): ''' Return a boolean value indicating whether checkbox is checked If this is a tristate checkbox, a partially checked value is returned as None ''' if self.checkState() == Qt.PartiallyChecked: return None else: return self.checkState() == Qt.Checked from calibre.gui2.library.delegates import DateDelegate as _DateDelegate class DateDelegate(_DateDelegate): ''' Delegate for dates. Because this delegate stores the format as an instance variable, a new instance must be created for each column. This differs from all the other delegates. ''' def __init__(self, parent, fmt='dd MMM yyyy', default_to_today=True): super(DateDelegate, self).__init__(parent) self.format = fmt self.default_to_today = default_to_today def createEditor(self, parent, option, index): qde = QStyledItemDelegate.createEditor(self, parent, option, index) qde.setDisplayFormat(self.format) qde.setMinimumDateTime(UNDEFINED_QDATETIME) qde.setSpecialValueText(_('Undefined')) qde.setCalendarPopup(True) return qde def setEditorData(self, editor, index): val = index.model().data(index, Qt.DisplayRole) if val is None or val == UNDEFINED_QDATETIME: if self.default_to_today: val = self.default_date else: val = UNDEFINED_QDATETIME editor.setDateTime(val) def setModelData(self, editor, model, index): val = editor.dateTime() if val <= UNDEFINED_QDATETIME: model.setData(index, UNDEFINED_QDATETIME, Qt.EditRole) else: model.setData(index, QDateTime(val), Qt.EditRole) class DateTableWidgetItem(QTableWidgetItem): def __init__(self, date_read, is_read_only=False, default_to_today=False, fmt=None): if date_read is None or date_read == UNDEFINED_DATE and default_to_today: date_read = now() if is_read_only: super(DateTableWidgetItem, self).__init__(format_date(date_read, fmt)) self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) self.setData(Qt.DisplayRole, QDateTime(date_read)) else: super(DateTableWidgetItem, self).__init__('') self.setData(Qt.DisplayRole, QDateTime(date_read)) class ImageTitleLayout(QHBoxLayout): ''' A reusable layout widget displaying an image followed by a title ''' def __init__(self, parent, icon_name, title): super(ImageTitleLayout, self).__init__() self.title_image_label = QLabel(parent) self.update_title_icon(icon_name) self.addWidget(self.title_image_label) title_font = QFont() title_font.setPointSize(16) shelf_label = QLabel(title, parent) shelf_label.setFont(title_font) self.addWidget(shelf_label) self.insertStretch(-1) def update_title_icon(self, icon_name): pixmap = get_pixmap(icon_name) if pixmap is None: error_dialog(self.parent(), _('Restart required'), _('Title image not found - you must restart Calibre before using this plugin!'), show=True) else: self.title_image_label.setPixmap(pixmap) self.title_image_label.setMaximumSize(32, 32) self.title_image_label.setScaledContents(True) class ReadOnlyTableWidgetItem(QTableWidgetItem): ''' For use in a table cell, displays text the user cannot select or modify. ''' def __init__(self, text): if text is None: text = '' super(ReadOnlyTableWidgetItem, self).__init__(text) self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) class ReadOnlyTextIconWidgetItem(ReadOnlyTableWidgetItem): ''' For use in a table cell, displays an icon the user cannot select or modify. ''' def __init__(self, text, icon): super(ReadOnlyTextIconWidgetItem, self).__init__(text) if icon: self.setIcon(icon) class ReadOnlyCheckableTableWidgetItem(ReadOnlyTableWidgetItem): ''' For use in a table cell, displays a checkbox next to some text the user cannot select or modify. ''' def __init__(self, text, checked=False, is_tristate=False): super(ReadOnlyCheckableTableWidgetItem, self).__init__(text) try: # For Qt Backwards compatibility. self.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsEnabled ) except: self.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled )) if is_tristate: self.setFlags(self.flags() | Qt.ItemIsTristate) if checked: self.setCheckState(Qt.Checked) else: if is_tristate and checked is None: self.setCheckState(Qt.PartiallyChecked) else: self.setCheckState(Qt.Unchecked) def get_boolean_value(self): ''' Return a boolean value indicating whether checkbox is checked If this is a tristate checkbox, a partially checked value is returned as None ''' if self.checkState() == Qt.PartiallyChecked: return None else: return self.checkState() == Qt.Checked class TextIconWidgetItem(QTableWidgetItem): ''' For use in a table cell, displays text with an icon next to it. ''' def __init__(self, text, icon): super(TextIconWidgetItem, self).__init__(text) self.setIcon(icon) # ---------------------------------------------- # Controls # ---------------------------------------------- class CustomColumnComboBox(QComboBox): CREATE_NEW_COLUMN_ITEM = _("Create new column") def __init__(self, parent, custom_columns={}, selected_column='', initial_items=[''], create_column_callback=None): super(CustomColumnComboBox, self).__init__(parent) self.create_column_callback = create_column_callback self.current_index = 0 if create_column_callback is not None: self.currentTextChanged.connect(self.current_text_changed) self.populate_combo(custom_columns, selected_column, initial_items) def populate_combo(self, custom_columns, selected_column, initial_items=[''], show_lookup_name=True): self.clear() self.column_names = [] selected_idx = 0 if isinstance(initial_items, dict): for key in sorted(initial_items.keys()): self.column_names.append(key) display_name = initial_items[key] self.addItem(display_name) if key == selected_column: selected_idx = len(self.column_names) - 1 else: for display_name in initial_items: self.column_names.append(display_name) self.addItem(display_name) if display_name == selected_column: selected_idx = len(self.column_names) - 1 for key in sorted(custom_columns.keys()): self.column_names.append(key) display_name = '%s (%s)'%(key, custom_columns[key]['name']) if show_lookup_name else custom_columns[key]['name'] self.addItem(display_name) if key == selected_column: selected_idx = len(self.column_names) - 1 if self.create_column_callback is not None: self.addItem(self.CREATE_NEW_COLUMN_ITEM) self.column_names.append(self.CREATE_NEW_COLUMN_ITEM) self.setCurrentIndex(selected_idx) def get_selected_column(self): selected_column = self.column_names[self.currentIndex()] if selected_column == self.CREATE_NEW_COLUMN_ITEM: selected_column = None return selected_column def current_text_changed(self, new_text): if new_text == self.CREATE_NEW_COLUMN_ITEM: result = self.create_column_callback() if not result: self.setCurrentIndex(self.current_index) else: self.current_index = self.currentIndex() class KeyValueComboBox(QComboBox): def __init__(self, parent, values, selected_key): QComboBox.__init__(self, parent) self.values = values self.populate_combo(selected_key) def populate_combo(self, selected_key): self.clear() selected_idx = idx = -1 for key, value in self.values.items(): idx = idx + 1 self.addItem(value) if key == selected_key: selected_idx = idx self.setCurrentIndex(selected_idx) def selected_key(self): for key, value in self.values.items(): if value == unicode(self.currentText()).strip(): return key class NoWheelComboBox(QComboBox): ''' For combobox displayed in a table cell using the mouse wheel has nasty interactions due to the conflict between scrolling the table vs scrolling the combobox item. Inherit from this class to disable the combobox changing value with mouse wheel. ''' def wheelEvent(self, event): event.ignore() class ReadOnlyLineEdit(QLineEdit): def __init__(self, text, parent): if text is None: text = '' super(ReadOnlyLineEdit, self).__init__(text, parent) self.setEnabled(False) PK .U~ worker.pyfrom __future__ import unicode_literals, division, absolute_import, print_function __license__ = 'GPL v3' __copyright__ = '2011, Grant Drake' import socket, re, datetime from threading import Thread from lxml.html import fromstring, tostring from calibre.ebooks.metadata.book.base import Metadata from calibre.library.comments import sanitize_comments_html from calibre.utils.cleantext import clean_ascii_chars from calibre.utils.icu import lower class Worker(Thread): # Get details ''' Get book details from Baen book page in a separate thread ''' def __init__(self, url, match_authors, result_queue, browser, log, relevance, plugin, timeout=20): Thread.__init__(self) self.daemon = True self.url, self.result_queue = url, result_queue self.match_authors = match_authors self.log, self.timeout = log, timeout self.relevance, self.plugin = relevance, plugin self.browser = browser.clone_browser() self.cover_url = self.baen_id = self.isbn = None def run(self): try: self.get_details() except: self.log.exception('get_details failed for url: %r'%self.url) def get_details(self): try: self.log.info('Baen url: %r'%self.url) self.log.info('Baen relevance: %r'%self.relevance) raw = self.browser.open_novisit(self.url, timeout=self.timeout).read().strip() except Exception as e: if callable(getattr(e, 'getcode', None)) and \ e.getcode() == 404: self.log.error('URL malformed: %r'%self.url) return attr = getattr(e, 'args', [None]) attr = attr if attr else [None] if isinstance(attr[0], socket.timeout): msg = 'Baen timed out. Try again later.' self.log.error(msg) else: msg = 'Failed to make details query: %r'%self.url self.log.exception(msg) return raw = raw.decode('utf-8', errors='replace') #open('E:\\t3.html', 'wb').write(raw) if '404 - ' in raw: self.log.error('URL malformed: %r'%self.url) return try: root = fromstring(clean_ascii_chars(raw)) except: msg = 'Failed to parse Baen details page: %r'%self.url self.log.exception(msg) return self.parse_details(root) def parse_details(self, root): try: baen_id = self.parse_baen_id(self.url) except: self.log.exception('Error parsing Baen id for url: %r'%self.url) baen_id = None try: title = self.parse_title(root) except: self.log.exception('Error parsing title for url: %r'%self.url) title = None try: authors = self.parse_authors(root) except: self.log.exception('Error parsing authors for url: %r'%self.url) authors = [] if not title or not authors or not baen_id: self.log.error('Could not find title/authors/Baen id for %r'%self.url) self.log.error('Baen: %r Title: %r Authors: %r'%(baen_id, title, authors)) return mi = Metadata(title, authors) mi.set_identifier('baen', baen_id) self.baen_id = baen_id try: mi.pubdate = self.parse_published_date(root) except: self.log.exception('Error parsing published date for url: %r'%self.url) try: self.cover_url = self.parse_cover(root) except: self.log.exception('Error parsing cover for url: %r'%self.url) mi.has_cover = bool(self.cover_url) try: mi.comments = self.parse_comments(root) except: self.log.exception('Error parsing comments for url: %r'%self.url) try: mi.rating = self.parse_rating(root) except: self.log.exception('Error parsing rating for url: %r'%self.url) # There will be no other on Baen's website! mi.publisher = 'Baen' mi.source_relevance = self.relevance if self.baen_id: if self.cover_url: self.plugin.cache_identifier_to_cover_url(self.baen_id, self.cover_url) self.plugin.clean_downloaded_metadata(mi) self.result_queue.put(mi) def parse_baen_id(self, url): from calibre_plugins.baen import Baen # import BASE_URL return re.search(Baen.BASE_URL + '/(.*)\.html', url).groups(0)[0] def parse_title(self, root): title_node = root.xpath('//span[@class="product-title"]') if title_node: self.log.info("parse_title: title=", title_node[0].text) return title_node[0].text def parse_authors(self, root): author_node = root.xpath('//div[@class="product-shop"]/div/span[@class="author-name"]/a/text()') self.log.info('parse_authors: author_node=', author_node) authors = [a.strip() for a in author_node] def ismatch(authors): authors = lower(' '.join(authors)) amatch = not self.match_authors for a in self.match_authors: if lower(a) in authors: amatch = True break if not self.match_authors: amatch = True return amatch if not self.match_authors or ismatch(authors): return authors self.log.info('Rejecting authors as not a close match: ', ','.join(authors)) def parse_published_date(self, root): published_node = root.xpath('//div[@class="product-shop"]//p[@class="publish-date"]') if published_node: date_match = re.search('Published:\s+(\d+)/(\d+)/(\d+)', published_node[0].text.strip()) if date_match: year = int(date_match.groups(0)[2]) month = int(date_match.groups(0)[0]) day = int(date_match.groups(0)[1]) self.log.info('parse_published_date: year=%s, month=%s, day=%s' %(year, month, day)) from calibre.utils.date import utc_tz return datetime.datetime(year, month, day, tzinfo=utc_tz) def parse_comments(self, root): description_node = root.xpath('//div[contains(@class,"product-description")]') if description_node: comments = tostring(description_node[0], method='html') comments = sanitize_comments_html(comments) return comments def parse_rating(self, root): rating_node = root.xpath('//p[@class="review-overall-score-container"]/span[@class="review-overall-score"]') if rating_node: rating_text = rating_node[0].text.strip() self.log.info('parse_rating: rating_text=', rating_text ) try: rating = float(rating_text) except: rating = None return rating def parse_cover(self, root): cover_node = root.xpath('//div[@class="product-img-box"]') if cover_node: cover_node = cover_node[0].xpath('//a[@onclick]') #self.log.info('parse_cover: cover_node=', cover_node[0].get('onclick') ) match = re.search('window.open\(\'(.*?)\'', cover_node[0].get('onclick')) if match: self.log.info('parse_cover: cover="%s"' % match.groups(0)[0]) cover_url = match.groups(0)[0] cover_url = None if cover_url.endswith('nopicture.gif') else cover_url return cover_url PK ϡ4UtR&