KG updates
BIN
resources/images/plugins/mobileread.png
Normal file
After Width: | Height: | Size: 641 B |
BIN
resources/images/plugins/plugin_deprecated.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
resources/images/plugins/plugin_disabled_invalid.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
resources/images/plugins/plugin_disabled_ok.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
resources/images/plugins/plugin_disabled_valid.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
resources/images/plugins/plugin_new.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
resources/images/plugins/plugin_new_invalid.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
resources/images/plugins/plugin_new_valid.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
resources/images/plugins/plugin_updater.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
resources/images/plugins/plugin_updater_updates.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
resources/images/plugins/plugin_upgrade_invalid.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
resources/images/plugins/plugin_upgrade_ok.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
resources/images/plugins/plugin_upgrade_valid.png
Normal file
After Width: | Height: | Size: 14 KiB |
@ -95,6 +95,11 @@ void launch_calibre(LPCTSTR exe, LPCTSTR config_dir, LPCTSTR library_dir) {
|
|||||||
ExitProcess(1);
|
ExitProcess(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! SetEnvironmentVariable(TEXT("CALIBRE_PORTABLE_BUILD"), exe)) {
|
||||||
|
show_last_error(TEXT("Failed to set environment variables"));
|
||||||
|
ExitProcess(1);
|
||||||
|
}
|
||||||
|
|
||||||
dwFlags = CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_PROCESS_GROUP;
|
dwFlags = CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_PROCESS_GROUP;
|
||||||
_sntprintf_s(cmdline, BUFSIZE, _TRUNCATE, TEXT(" \"--with-library=%s\""), library_dir);
|
_sntprintf_s(cmdline, BUFSIZE, _TRUNCATE, TEXT(" \"--with-library=%s\""), library_dir);
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ isbsd = isfreebsd or isnetbsd
|
|||||||
islinux = not(iswindows or isosx or isbsd)
|
islinux = not(iswindows or isosx or isbsd)
|
||||||
isfrozen = hasattr(sys, 'frozen')
|
isfrozen = hasattr(sys, 'frozen')
|
||||||
isunix = isosx or islinux
|
isunix = isosx or islinux
|
||||||
|
isportable = os.environ.get('CALIBRE_PORTABLE_BUILD', None) is not None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
preferred_encoding = locale.getpreferredencoding()
|
preferred_encoding = locale.getpreferredencoding()
|
||||||
|
@ -867,13 +867,20 @@ class ActionStore(InterfaceActionBase):
|
|||||||
from calibre.gui2.store.config.store import save_settings as save
|
from calibre.gui2.store.config.store import save_settings as save
|
||||||
save(config_widget)
|
save(config_widget)
|
||||||
|
|
||||||
|
class ActionPluginUpdates(InterfaceActionBase):
|
||||||
|
name = 'Plugin Updates'
|
||||||
|
author = 'Grant Drake'
|
||||||
|
description = 'Queries the MobileRead forums for updates to plugins to install'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.plugin_updates:PluginUpdatesAction'
|
||||||
|
|
||||||
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
||||||
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
|
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
|
||||||
ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails,
|
ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails,
|
||||||
ActionRestart, ActionOpenFolder, ActionConnectShare,
|
ActionRestart, ActionOpenFolder, ActionConnectShare,
|
||||||
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
|
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
|
||||||
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
|
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
|
||||||
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore]
|
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore,
|
||||||
|
ActionPluginUpdates]
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -493,6 +493,8 @@ def initialize_plugin(plugin, path_to_zip_file):
|
|||||||
raise InvalidPlugin((_('Initialization of plugin %s failed with traceback:')
|
raise InvalidPlugin((_('Initialization of plugin %s failed with traceback:')
|
||||||
%tb) + '\n'+tb)
|
%tb) + '\n'+tb)
|
||||||
|
|
||||||
|
def has_external_plugins():
|
||||||
|
return bool(config['plugins'])
|
||||||
|
|
||||||
def initialize_plugins():
|
def initialize_plugins():
|
||||||
global _initialized_plugins
|
global _initialized_plugins
|
||||||
|
@ -88,6 +88,7 @@ class NOOK_COLOR(NOOK):
|
|||||||
|
|
||||||
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOK_DISK'
|
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOK_DISK'
|
||||||
EBOOK_DIR_MAIN = 'My Files'
|
EBOOK_DIR_MAIN = 'My Files'
|
||||||
|
NEWS_IN_FOLDER = False
|
||||||
|
|
||||||
def upload_cover(self, path, filename, metadata, filepath):
|
def upload_cover(self, path, filename, metadata, filepath):
|
||||||
pass
|
pass
|
||||||
|
@ -101,6 +101,9 @@ class Device(DeviceConfig, DevicePlugin):
|
|||||||
#: The maximum length of paths created on the device
|
#: The maximum length of paths created on the device
|
||||||
MAX_PATH_LEN = 250
|
MAX_PATH_LEN = 250
|
||||||
|
|
||||||
|
#: Put news in its own folder
|
||||||
|
NEWS_IN_FOLDER = True
|
||||||
|
|
||||||
def reset(self, key='-1', log_packets=False, report_progress=None,
|
def reset(self, key='-1', log_packets=False, report_progress=None,
|
||||||
detected_device=None):
|
detected_device=None):
|
||||||
self._main_prefix = self._card_a_prefix = self._card_b_prefix = None
|
self._main_prefix = self._card_a_prefix = self._card_b_prefix = None
|
||||||
@ -946,7 +949,8 @@ class Device(DeviceConfig, DevicePlugin):
|
|||||||
extra_components = []
|
extra_components = []
|
||||||
tag = special_tag
|
tag = special_tag
|
||||||
if tag.startswith(_('News')):
|
if tag.startswith(_('News')):
|
||||||
extra_components.append('News')
|
if self.NEWS_IN_FOLDER:
|
||||||
|
extra_components.append('News')
|
||||||
else:
|
else:
|
||||||
for c in tag.split('/'):
|
for c in tag.split('/'):
|
||||||
c = sanitize(c)
|
c = sanitize(c)
|
||||||
|
33
src/calibre/gui2/actions/plugin_updates.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#!/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__ = '2011, Grant Drake <grant.drake@gmail.com>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from PyQt4.Qt import QApplication, Qt, QIcon
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog,
|
||||||
|
FILTER_ALL, FILTER_UPDATE_AVAILABLE)
|
||||||
|
|
||||||
|
class PluginUpdatesAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Plugin Updates'
|
||||||
|
action_spec = (_('Plugin Updates'), None, None, None)
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.qaction.setIcon(QIcon(I('plugins/plugin_updater.png')))
|
||||||
|
self.qaction.triggered.connect(self.check_for_plugin_updates)
|
||||||
|
|
||||||
|
def check_for_plugin_updates(self):
|
||||||
|
# Get the user to choose a plugin to install
|
||||||
|
initial_filter = FILTER_UPDATE_AVAILABLE
|
||||||
|
mods = QApplication.keyboardModifiers()
|
||||||
|
if mods & Qt.ControlModifier or mods & Qt.ShiftModifier:
|
||||||
|
initial_filter = FILTER_ALL
|
||||||
|
|
||||||
|
d = PluginUpdaterDialog(self.gui, initial_filter=initial_filter)
|
||||||
|
d.exec_()
|
@ -24,6 +24,8 @@ class PreferencesAction(InterfaceAction):
|
|||||||
pm.addAction(QIcon(I('config.png')), _('Change calibre behavior'), self.do_config)
|
pm.addAction(QIcon(I('config.png')), _('Change calibre behavior'), self.do_config)
|
||||||
pm.addAction(QIcon(I('wizard.png')), _('Run welcome wizard'),
|
pm.addAction(QIcon(I('wizard.png')), _('Run welcome wizard'),
|
||||||
self.gui.run_wizard)
|
self.gui.run_wizard)
|
||||||
|
pm.addAction(QIcon(I('plugins/plugin_updater.png')),
|
||||||
|
_('Get plugins to enhance calibre'), self.get_plugins)
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
pm.addSeparator()
|
pm.addSeparator()
|
||||||
ac = pm.addAction(QIcon(I('debug.png')), _('Restart in debug mode'),
|
ac = pm.addAction(QIcon(I('debug.png')), _('Restart in debug mode'),
|
||||||
@ -36,6 +38,12 @@ class PreferencesAction(InterfaceAction):
|
|||||||
for x in (self.gui.preferences_action, self.qaction):
|
for x in (self.gui.preferences_action, self.qaction):
|
||||||
x.triggered.connect(self.do_config)
|
x.triggered.connect(self.do_config)
|
||||||
|
|
||||||
|
def get_plugins(self):
|
||||||
|
from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog,
|
||||||
|
FILTER_NOT_INSTALLED)
|
||||||
|
d = PluginUpdaterDialog(self.gui,
|
||||||
|
initial_filter=FILTER_NOT_INSTALLED)
|
||||||
|
d.exec_()
|
||||||
|
|
||||||
def do_config(self, checked=False, initial_plugin=None,
|
def do_config(self, checked=False, initial_plugin=None,
|
||||||
close_after_initial=False):
|
close_after_initial=False):
|
||||||
|
869
src/calibre/gui2/dialogs/plugin_updater.py
Normal file
@ -0,0 +1,869 @@
|
|||||||
|
#!/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__ = '2011, Grant Drake <grant.drake@gmail.com>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import re, datetime, traceback
|
||||||
|
from lxml import html
|
||||||
|
from PyQt4.Qt import (Qt, QUrl, QFrame, QVBoxLayout, QLabel, QBrush, QTextEdit,
|
||||||
|
QComboBox, QAbstractItemView, QHBoxLayout, QDialogButtonBox,
|
||||||
|
QAbstractTableModel, QVariant, QTableView, QModelIndex,
|
||||||
|
QSortFilterProxyModel, QAction, QIcon, QDialog,
|
||||||
|
QFont, QPixmap, QSize)
|
||||||
|
from calibre import browser, prints
|
||||||
|
from calibre.constants import numeric_version, iswindows, isosx, DEBUG
|
||||||
|
from calibre.customize.ui import (initialized_plugins, is_disabled, remove_plugin,
|
||||||
|
add_plugin, enable_plugin, disable_plugin,
|
||||||
|
NameConflict, has_external_plugins)
|
||||||
|
from calibre.gui2 import error_dialog, question_dialog, info_dialog, NONE, open_url, gprefs
|
||||||
|
from calibre.gui2.preferences.plugins import ConfigWidget
|
||||||
|
from calibre.utils.date import UNDEFINED_DATE, format_date
|
||||||
|
|
||||||
|
|
||||||
|
MR_URL = 'http://www.mobileread.com/forums/'
|
||||||
|
MR_INDEX_URL = MR_URL + 'showpost.php?p=1362767&postcount=1'
|
||||||
|
|
||||||
|
FILTER_ALL = 0
|
||||||
|
FILTER_INSTALLED = 1
|
||||||
|
FILTER_UPDATE_AVAILABLE = 2
|
||||||
|
FILTER_NOT_INSTALLED = 3
|
||||||
|
|
||||||
|
def get_plugin_updates_available():
|
||||||
|
'''
|
||||||
|
API exposed to read whether there are updates available for any
|
||||||
|
of the installed user plugins.
|
||||||
|
Returns None if no updates found
|
||||||
|
Returns list(DisplayPlugin) of plugins installed that have a new version
|
||||||
|
'''
|
||||||
|
if not has_external_plugins():
|
||||||
|
return None
|
||||||
|
display_plugins = read_available_plugins()
|
||||||
|
if display_plugins:
|
||||||
|
update_plugins = filter(filter_upgradeable_plugins, display_plugins)
|
||||||
|
if len(update_plugins) > 0:
|
||||||
|
return update_plugins
|
||||||
|
return None
|
||||||
|
|
||||||
|
def filter_upgradeable_plugins(display_plugin):
|
||||||
|
return display_plugin.is_upgrade_available()
|
||||||
|
|
||||||
|
def filter_not_installed_plugins(display_plugin):
|
||||||
|
return not display_plugin.is_installed()
|
||||||
|
|
||||||
|
def read_available_plugins():
|
||||||
|
display_plugins = []
|
||||||
|
br = browser()
|
||||||
|
br.set_handle_gzip(True)
|
||||||
|
try:
|
||||||
|
raw = br.open_novisit(MR_INDEX_URL).read()
|
||||||
|
if not raw:
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
return
|
||||||
|
raw = raw.decode('utf-8', errors='replace')
|
||||||
|
root = html.fromstring(raw)
|
||||||
|
list_nodes = root.xpath('//div[@id="post_message_1362767"]/ul/li')
|
||||||
|
# Add our deprecated plugins which are nested in a grey span
|
||||||
|
list_nodes.extend(root.xpath('//div[@id="post_message_1362767"]/span/ul/li'))
|
||||||
|
for list_node in list_nodes:
|
||||||
|
try:
|
||||||
|
display_plugin = DisplayPlugin(list_node)
|
||||||
|
get_installed_plugin_status(display_plugin)
|
||||||
|
display_plugins.append(display_plugin)
|
||||||
|
except:
|
||||||
|
if DEBUG:
|
||||||
|
prints('======= MobileRead Parse Error =======')
|
||||||
|
traceback.print_exc()
|
||||||
|
prints(html.tostring(list_node))
|
||||||
|
display_plugins = sorted(display_plugins, key=lambda k: k.name)
|
||||||
|
return display_plugins
|
||||||
|
|
||||||
|
def get_installed_plugin_status(display_plugin):
|
||||||
|
display_plugin.installed_version = None
|
||||||
|
display_plugin.plugin = None
|
||||||
|
for plugin in initialized_plugins():
|
||||||
|
if plugin.name == display_plugin.name:
|
||||||
|
display_plugin.plugin = plugin
|
||||||
|
display_plugin.installed_version = plugin.version
|
||||||
|
break
|
||||||
|
if display_plugin.uninstall_plugins:
|
||||||
|
# Plugin requires a specific plugin name to be uninstalled first
|
||||||
|
# This could occur when a plugin is renamed (Kindle Collections)
|
||||||
|
# or multiple plugins deprecated into a newly named one.
|
||||||
|
# Check whether user has the previous version(s) installed
|
||||||
|
plugins_to_remove = list(display_plugin.uninstall_plugins)
|
||||||
|
for plugin_to_uninstall in plugins_to_remove:
|
||||||
|
found = False
|
||||||
|
for plugin in initialized_plugins():
|
||||||
|
if plugin.name == plugin_to_uninstall:
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
display_plugin.uninstall_plugins.remove(plugin_to_uninstall)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageTitleLayout(QHBoxLayout):
|
||||||
|
'''
|
||||||
|
A reusable layout widget displaying an image followed by a title
|
||||||
|
'''
|
||||||
|
def __init__(self, parent, icon_name, title):
|
||||||
|
QHBoxLayout.__init__(self)
|
||||||
|
title_font = QFont()
|
||||||
|
title_font.setPointSize(16)
|
||||||
|
title_image_label = QLabel(parent)
|
||||||
|
pixmap = QPixmap()
|
||||||
|
pixmap.load(I(icon_name))
|
||||||
|
if pixmap is None:
|
||||||
|
error_dialog(parent, _('Restart required'),
|
||||||
|
_('You must restart Calibre before using this plugin!'), show=True)
|
||||||
|
else:
|
||||||
|
title_image_label.setPixmap(pixmap)
|
||||||
|
title_image_label.setMaximumSize(32, 32)
|
||||||
|
title_image_label.setScaledContents(True)
|
||||||
|
self.addWidget(title_image_label)
|
||||||
|
shelf_label = QLabel(title, parent)
|
||||||
|
shelf_label.setFont(title_font)
|
||||||
|
self.addWidget(shelf_label)
|
||||||
|
self.insertStretch(-1)
|
||||||
|
|
||||||
|
|
||||||
|
class SizePersistedDialog(QDialog):
|
||||||
|
'''
|
||||||
|
This dialog is a base class for any dialogs that want their size/position
|
||||||
|
restored when they are next opened.
|
||||||
|
'''
|
||||||
|
|
||||||
|
initial_extra_size = QSize(0, 0)
|
||||||
|
|
||||||
|
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()+self.initial_extra_size)
|
||||||
|
else:
|
||||||
|
self.restoreGeometry(self.geom)
|
||||||
|
|
||||||
|
def dialog_closing(self, result):
|
||||||
|
geom = bytearray(self.saveGeometry())
|
||||||
|
gprefs[self.unique_pref_name] = geom
|
||||||
|
|
||||||
|
|
||||||
|
class VersionHistoryDialog(SizePersistedDialog):
|
||||||
|
|
||||||
|
def __init__(self, parent, plugin_name, html):
|
||||||
|
SizePersistedDialog.__init__(self, parent, 'Plugin Updater plugin:version history dialog')
|
||||||
|
self.setWindowTitle(_('Version History for %s')%plugin_name)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
self.notes = QTextEdit(html, self)
|
||||||
|
self.notes.setReadOnly(True)
|
||||||
|
layout.addWidget(self.notes)
|
||||||
|
|
||||||
|
self.button_box = QDialogButtonBox(QDialogButtonBox.Close)
|
||||||
|
self.button_box.rejected.connect(self.reject)
|
||||||
|
layout.addWidget(self.button_box)
|
||||||
|
|
||||||
|
# Cause our dialog size to be restored from prefs or created on first usage
|
||||||
|
self.resize_dialog()
|
||||||
|
|
||||||
|
|
||||||
|
class PluginFilterComboBox(QComboBox):
|
||||||
|
def __init__(self, parent):
|
||||||
|
QComboBox.__init__(self, parent)
|
||||||
|
items = [_('All'), _('Installed'), _('Update available'), _('Not installed')]
|
||||||
|
self.addItems(items)
|
||||||
|
|
||||||
|
|
||||||
|
class DisplayPlugin(object):
|
||||||
|
|
||||||
|
def __init__(self, list_node):
|
||||||
|
# The html from the index web page looks like this:
|
||||||
|
'''
|
||||||
|
<li><a href="http://www.mobileread.com/forums/showthread.php?t=121787">Book Sync</a><br />
|
||||||
|
<i>Add books to a list to be automatically sent to your device the next time it is connected.<br />
|
||||||
|
<span class="resize_1">Version: 1.1; Released: 02-22-2011; Calibre: 0.7.42; Author: kiwidude; <br />
|
||||||
|
Platforms: Windows, OSX, Linux; History: Yes;</span></i></li>
|
||||||
|
'''
|
||||||
|
self.name = list_node.xpath('a')[0].text_content().strip()
|
||||||
|
self.forum_link = list_node.xpath('a/@href')[0].strip()
|
||||||
|
self.installed_version = None
|
||||||
|
|
||||||
|
description_text = list_node.xpath('i')[0].text_content()
|
||||||
|
description_parts = description_text.partition('Version:')
|
||||||
|
self.description = description_parts[0].strip()
|
||||||
|
|
||||||
|
details_text = description_parts[1] + description_parts[2].replace('\r\n','')
|
||||||
|
details_pairs = details_text.split(';')
|
||||||
|
details = {}
|
||||||
|
for details_pair in details_pairs:
|
||||||
|
pair = details_pair.split(':')
|
||||||
|
if len(pair) == 2:
|
||||||
|
key = pair[0].strip().lower()
|
||||||
|
value = pair[1].strip()
|
||||||
|
details[key] = value
|
||||||
|
|
||||||
|
donation_node = list_node.xpath('i/span/a/@href')
|
||||||
|
self.donation_link = donation_node[0] if donation_node else None
|
||||||
|
|
||||||
|
self.available_version = self._version_text_to_tuple(details.get('version', None))
|
||||||
|
|
||||||
|
release_date = details.get('released', '01-01-0101').split('-')
|
||||||
|
date_parts = [int(re.search(r'(\d+)', x).group(1)) for x in release_date]
|
||||||
|
self.release_date = datetime.date(date_parts[2], date_parts[0], date_parts[1])
|
||||||
|
|
||||||
|
self.calibre_required_version = self._version_text_to_tuple(details.get('calibre', None))
|
||||||
|
self.author = details.get('author', '')
|
||||||
|
self.platforms = [p.strip().lower() for p in details.get('platforms', '').split(',')]
|
||||||
|
# Optional pairing just for plugins which require checking for uninstall first
|
||||||
|
self.uninstall_plugins = []
|
||||||
|
uninstall = details.get('uninstall', None)
|
||||||
|
if uninstall:
|
||||||
|
self.uninstall_plugins = [i.strip() for i in uninstall.split(',')]
|
||||||
|
self.has_changelog = details.get('history', 'No').lower() in ['yes', 'true']
|
||||||
|
self.is_deprecated = details.get('deprecated', 'No').lower() in ['yes', 'true']
|
||||||
|
|
||||||
|
def _version_text_to_tuple(self, version_text):
|
||||||
|
if version_text:
|
||||||
|
ver = version_text.split('.')
|
||||||
|
while len(ver) < 3:
|
||||||
|
ver.append('0')
|
||||||
|
ver = [int(re.search(r'(\d+)', x).group(1)) for x in ver]
|
||||||
|
return tuple(ver)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_disabled(self):
|
||||||
|
if self.plugin is None:
|
||||||
|
return False
|
||||||
|
return is_disabled(self.plugin)
|
||||||
|
|
||||||
|
def is_installed(self):
|
||||||
|
return self.installed_version is not None
|
||||||
|
|
||||||
|
def is_upgrade_available(self):
|
||||||
|
return self.is_installed() and (self.installed_version < self.available_version \
|
||||||
|
or self.is_deprecated)
|
||||||
|
|
||||||
|
def is_valid_platform(self):
|
||||||
|
if iswindows:
|
||||||
|
return 'windows' in self.platforms
|
||||||
|
if isosx:
|
||||||
|
return 'osx' in self.platforms
|
||||||
|
return 'linux' in self.platforms
|
||||||
|
|
||||||
|
def is_valid_calibre(self):
|
||||||
|
return numeric_version >= self.calibre_required_version
|
||||||
|
|
||||||
|
def is_valid_to_install(self):
|
||||||
|
return self.is_valid_platform() and self.is_valid_calibre() and not self.is_deprecated
|
||||||
|
|
||||||
|
|
||||||
|
class DisplayPluginSortFilterModel(QSortFilterProxyModel):
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
QSortFilterProxyModel.__init__(self, parent)
|
||||||
|
self.setSortRole(Qt.UserRole)
|
||||||
|
self.filter_criteria = FILTER_ALL
|
||||||
|
|
||||||
|
def filterAcceptsRow(self, sourceRow, sourceParent):
|
||||||
|
index = self.sourceModel().index(sourceRow, 0, sourceParent)
|
||||||
|
display_plugin = self.sourceModel().display_plugins[index.row()]
|
||||||
|
if self.filter_criteria == FILTER_ALL:
|
||||||
|
return not (display_plugin.is_deprecated and not display_plugin.is_installed())
|
||||||
|
if self.filter_criteria == FILTER_INSTALLED:
|
||||||
|
return display_plugin.is_installed()
|
||||||
|
if self.filter_criteria == FILTER_UPDATE_AVAILABLE:
|
||||||
|
return display_plugin.is_upgrade_available()
|
||||||
|
if self.filter_criteria == FILTER_NOT_INSTALLED:
|
||||||
|
return not display_plugin.is_installed() and not display_plugin.is_deprecated
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_filter_criteria(self, filter_value):
|
||||||
|
self.filter_criteria = filter_value
|
||||||
|
self.invalidateFilter()
|
||||||
|
|
||||||
|
|
||||||
|
class DisplayPluginModel(QAbstractTableModel):
|
||||||
|
|
||||||
|
def __init__(self, display_plugins):
|
||||||
|
QAbstractTableModel.__init__(self)
|
||||||
|
self.display_plugins = display_plugins
|
||||||
|
self.headers = map(QVariant, [_('Plugin Name'), _('Donate'), _('Status'), _('Installed'),
|
||||||
|
_('Available'), _('Released'), _('Calibre'), _('Author')])
|
||||||
|
|
||||||
|
def rowCount(self, *args):
|
||||||
|
return len(self.display_plugins)
|
||||||
|
|
||||||
|
def columnCount(self, *args):
|
||||||
|
return len(self.headers)
|
||||||
|
|
||||||
|
def headerData(self, section, orientation, role):
|
||||||
|
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
|
||||||
|
return self.headers[section]
|
||||||
|
return NONE
|
||||||
|
|
||||||
|
def data(self, index, role):
|
||||||
|
if not index.isValid():
|
||||||
|
return NONE;
|
||||||
|
row, col = index.row(), index.column()
|
||||||
|
if row < 0 or row >= self.rowCount():
|
||||||
|
return NONE
|
||||||
|
display_plugin = self.display_plugins[row]
|
||||||
|
if role in [Qt.DisplayRole, Qt.UserRole]:
|
||||||
|
if col == 0:
|
||||||
|
return QVariant(display_plugin.name)
|
||||||
|
if col == 1:
|
||||||
|
if display_plugin.donation_link:
|
||||||
|
return QVariant(_('PayPal'))
|
||||||
|
if col == 2:
|
||||||
|
return self._get_status(display_plugin)
|
||||||
|
if col == 3:
|
||||||
|
return QVariant(self._get_display_version(display_plugin.installed_version))
|
||||||
|
if col == 4:
|
||||||
|
return QVariant(self._get_display_version(display_plugin.available_version))
|
||||||
|
if col == 5:
|
||||||
|
if role == Qt.UserRole:
|
||||||
|
return self._get_display_release_date(display_plugin.release_date, 'yyyyMMdd')
|
||||||
|
else:
|
||||||
|
return self._get_display_release_date(display_plugin.release_date)
|
||||||
|
if col == 6:
|
||||||
|
return QVariant(self._get_display_version(display_plugin.calibre_required_version))
|
||||||
|
if col == 7:
|
||||||
|
return QVariant(display_plugin.author)
|
||||||
|
elif role == Qt.DecorationRole:
|
||||||
|
if col == 0:
|
||||||
|
return self._get_status_icon(display_plugin)
|
||||||
|
if col == 1:
|
||||||
|
if display_plugin.donation_link:
|
||||||
|
return QIcon(I('donate.png'))
|
||||||
|
elif role == Qt.ToolTipRole:
|
||||||
|
if col == 1 and display_plugin.donation_link:
|
||||||
|
return QVariant(_('This plugin is FREE but you can reward the developer for their effort\n'
|
||||||
|
'by donating to them via PayPal.\n\n'
|
||||||
|
'Right-click and choose Donate to reward: ')+display_plugin.author)
|
||||||
|
else:
|
||||||
|
return self._get_status_tooltip(display_plugin)
|
||||||
|
elif role == Qt.ForegroundRole:
|
||||||
|
if col != 1: # Never change colour of the donation column
|
||||||
|
if display_plugin.is_deprecated:
|
||||||
|
return QVariant(QBrush(Qt.blue))
|
||||||
|
if display_plugin.is_disabled():
|
||||||
|
return QVariant(QBrush(Qt.gray))
|
||||||
|
return NONE
|
||||||
|
|
||||||
|
def plugin_to_index(self, display_plugin):
|
||||||
|
for i, p in enumerate(self.display_plugins):
|
||||||
|
if display_plugin == p:
|
||||||
|
return self.index(i, 0, QModelIndex())
|
||||||
|
return QModelIndex()
|
||||||
|
|
||||||
|
def refresh_plugin(self, display_plugin):
|
||||||
|
idx = self.plugin_to_index(display_plugin)
|
||||||
|
self.dataChanged.emit(idx, idx)
|
||||||
|
|
||||||
|
def _get_display_release_date(self, date_value, format='dd MMM yyyy'):
|
||||||
|
if date_value and date_value != UNDEFINED_DATE:
|
||||||
|
return QVariant(format_date(date_value, format))
|
||||||
|
return NONE
|
||||||
|
|
||||||
|
def _get_display_version(self, version):
|
||||||
|
if version is None:
|
||||||
|
return ''
|
||||||
|
return '.'.join([str(v) for v in list(version)])
|
||||||
|
|
||||||
|
def _get_status(self, display_plugin):
|
||||||
|
if not display_plugin.is_valid_platform():
|
||||||
|
return _('Platform unavailable')
|
||||||
|
if not display_plugin.is_valid_calibre():
|
||||||
|
return _('Calibre upgrade required')
|
||||||
|
if display_plugin.is_installed():
|
||||||
|
if display_plugin.is_deprecated:
|
||||||
|
return _('Plugin deprecated')
|
||||||
|
elif display_plugin.is_upgrade_available():
|
||||||
|
return _('New version available')
|
||||||
|
else:
|
||||||
|
return _('Latest version installed')
|
||||||
|
return _('Not installed')
|
||||||
|
|
||||||
|
def _get_status_icon(self, display_plugin):
|
||||||
|
if display_plugin.is_deprecated:
|
||||||
|
icon_name = 'plugin_deprecated.png'
|
||||||
|
elif display_plugin.is_disabled():
|
||||||
|
if display_plugin.is_upgrade_available():
|
||||||
|
if display_plugin.is_valid_to_install():
|
||||||
|
icon_name = 'plugin_disabled_valid.png'
|
||||||
|
else:
|
||||||
|
icon_name = 'plugin_disabled_invalid.png'
|
||||||
|
else:
|
||||||
|
icon_name = 'plugin_disabled_ok.png'
|
||||||
|
elif display_plugin.is_installed():
|
||||||
|
if display_plugin.is_upgrade_available():
|
||||||
|
if display_plugin.is_valid_to_install():
|
||||||
|
icon_name = 'plugin_upgrade_valid.png'
|
||||||
|
else:
|
||||||
|
icon_name = 'plugin_upgrade_invalid.png'
|
||||||
|
else:
|
||||||
|
icon_name = 'plugin_upgrade_ok.png'
|
||||||
|
else: # A plugin available not currently installed
|
||||||
|
if display_plugin.is_valid_to_install():
|
||||||
|
icon_name = 'plugin_new_valid.png'
|
||||||
|
else:
|
||||||
|
icon_name = 'plugin_new_invalid.png'
|
||||||
|
return QIcon(I('plugins/' + icon_name))
|
||||||
|
|
||||||
|
def _get_status_tooltip(self, display_plugin):
|
||||||
|
if display_plugin.is_deprecated:
|
||||||
|
return QVariant(_('This plugin has been deprecated and should be uninstalled')+'\n\n'+
|
||||||
|
_('Right-click to see more options'))
|
||||||
|
if not display_plugin.is_valid_platform():
|
||||||
|
return QVariant(_('This plugin can only be installed on: %s') % \
|
||||||
|
', '.join(display_plugin.platforms)+'\n\n'+
|
||||||
|
_('Right-click to see more options'))
|
||||||
|
if numeric_version < display_plugin.calibre_required_version:
|
||||||
|
return QVariant(_('You must upgrade to at least Calibre %s before installing this plugin') % \
|
||||||
|
self._get_display_version(display_plugin.calibre_required_version)+'\n\n'+
|
||||||
|
_('Right-click to see more options'))
|
||||||
|
if display_plugin.installed_version < display_plugin.available_version:
|
||||||
|
if display_plugin.installed_version is None:
|
||||||
|
return QVariant(_('You can install this plugin')+'\n\n'+
|
||||||
|
_('Right-click to see more options'))
|
||||||
|
else:
|
||||||
|
return QVariant(_('A new version of this plugin is available')+'\n\n'+
|
||||||
|
_('Right-click to see more options'))
|
||||||
|
return QVariant(_('This plugin is installed and up-to-date')+'\n\n'+
|
||||||
|
_('Right-click to see more options'))
|
||||||
|
|
||||||
|
|
||||||
|
class PluginUpdaterDialog(SizePersistedDialog):
|
||||||
|
|
||||||
|
initial_extra_size = QSize(350, 100)
|
||||||
|
|
||||||
|
def __init__(self, gui, initial_filter=FILTER_UPDATE_AVAILABLE):
|
||||||
|
SizePersistedDialog.__init__(self, gui, 'Plugin Updater plugin:plugin updater dialog')
|
||||||
|
self.gui = gui
|
||||||
|
self.forum_link = None
|
||||||
|
self.model = None
|
||||||
|
self._initialize_controls()
|
||||||
|
self._create_context_menu()
|
||||||
|
|
||||||
|
display_plugins = read_available_plugins()
|
||||||
|
|
||||||
|
if display_plugins:
|
||||||
|
self.model = DisplayPluginModel(display_plugins)
|
||||||
|
self.proxy_model = DisplayPluginSortFilterModel(self)
|
||||||
|
self.proxy_model.setSourceModel(self.model)
|
||||||
|
self.plugin_view.setModel(self.proxy_model)
|
||||||
|
self.plugin_view.resizeColumnsToContents()
|
||||||
|
self.plugin_view.selectionModel().currentRowChanged.connect(self._plugin_current_changed)
|
||||||
|
self.plugin_view.doubleClicked.connect(self.install_button.click)
|
||||||
|
self.filter_combo.setCurrentIndex(initial_filter)
|
||||||
|
self._select_and_focus_view()
|
||||||
|
else:
|
||||||
|
error_dialog(self.gui, _('Update Check Failed'),
|
||||||
|
_('Unable to reach the MobileRead plugins forum index page.'),
|
||||||
|
det_msg=MR_INDEX_URL, show=True)
|
||||||
|
self.filter_combo.setEnabled(False)
|
||||||
|
# Cause our dialog size to be restored from prefs or created on first usage
|
||||||
|
self.resize_dialog()
|
||||||
|
|
||||||
|
def _initialize_controls(self):
|
||||||
|
self.setWindowTitle(_('User plugins'))
|
||||||
|
self.setWindowIcon(QIcon(I('plugins/plugin_updater.png')))
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
self.setLayout(layout)
|
||||||
|
title_layout = ImageTitleLayout(self, 'plugins/plugin_updater.png',
|
||||||
|
_('User Plugins'))
|
||||||
|
layout.addLayout(title_layout)
|
||||||
|
|
||||||
|
header_layout = QHBoxLayout()
|
||||||
|
layout.addLayout(header_layout)
|
||||||
|
self.filter_combo = PluginFilterComboBox(self)
|
||||||
|
self.filter_combo.setMinimumContentsLength(20)
|
||||||
|
self.filter_combo.currentIndexChanged[int].connect(self._filter_combo_changed)
|
||||||
|
header_layout.addWidget(QLabel(_('Filter list of plugins')+':', self))
|
||||||
|
header_layout.addWidget(self.filter_combo)
|
||||||
|
header_layout.addStretch(10)
|
||||||
|
|
||||||
|
self.plugin_view = QTableView(self)
|
||||||
|
self.plugin_view.horizontalHeader().setStretchLastSection(True)
|
||||||
|
self.plugin_view.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
|
self.plugin_view.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||||
|
self.plugin_view.setAlternatingRowColors(True)
|
||||||
|
self.plugin_view.setSortingEnabled(True)
|
||||||
|
self.plugin_view.setIconSize(QSize(28, 28))
|
||||||
|
layout.addWidget(self.plugin_view)
|
||||||
|
|
||||||
|
details_layout = QHBoxLayout()
|
||||||
|
layout.addLayout(details_layout)
|
||||||
|
forum_label = QLabel('<a href="http://www.foo.com/">Plugin Forum Thread</a>', self)
|
||||||
|
forum_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
|
||||||
|
forum_label.linkActivated.connect(self._forum_label_activated)
|
||||||
|
details_layout.addWidget(QLabel(_('Description')+':', self), 0, Qt.AlignLeft)
|
||||||
|
details_layout.addWidget(forum_label, 1, Qt.AlignRight)
|
||||||
|
|
||||||
|
self.description = QLabel(self)
|
||||||
|
self.description.setFrameStyle(QFrame.Panel | QFrame.Sunken)
|
||||||
|
self.description.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
||||||
|
self.description.setMinimumHeight(40)
|
||||||
|
layout.addWidget(self.description)
|
||||||
|
|
||||||
|
self.button_box = QDialogButtonBox(QDialogButtonBox.Close)
|
||||||
|
self.button_box.rejected.connect(self._close_clicked)
|
||||||
|
self.install_button = self.button_box.addButton(_('&Install'), QDialogButtonBox.AcceptRole)
|
||||||
|
self.install_button.setToolTip(_('Install the selected plugin'))
|
||||||
|
self.install_button.clicked.connect(self._install_clicked)
|
||||||
|
self.install_button.setEnabled(False)
|
||||||
|
self.configure_button = self.button_box.addButton(' '+_('&Customize plugin ')+' ', QDialogButtonBox.ResetRole)
|
||||||
|
self.configure_button.setToolTip(_('Customize the options for this plugin'))
|
||||||
|
self.configure_button.clicked.connect(self._configure_clicked)
|
||||||
|
self.configure_button.setEnabled(False)
|
||||||
|
layout.addWidget(self.button_box)
|
||||||
|
|
||||||
|
def _create_context_menu(self):
|
||||||
|
self.plugin_view.setContextMenuPolicy(Qt.ActionsContextMenu)
|
||||||
|
self.install_action = QAction(QIcon(I('plugins/plugin_upgrade_ok.png')), _('&Install'), self)
|
||||||
|
self.install_action.setToolTip(_('Install the selected plugin'))
|
||||||
|
self.install_action.triggered.connect(self._install_clicked)
|
||||||
|
self.install_action.setEnabled(False)
|
||||||
|
self.plugin_view.addAction(self.install_action)
|
||||||
|
self.history_action = QAction(QIcon(I('chapters.png')), _('Version &History'), self)
|
||||||
|
self.history_action.setToolTip(_('Show history of changes to this plugin'))
|
||||||
|
self.history_action.triggered.connect(self._history_clicked)
|
||||||
|
self.history_action.setEnabled(False)
|
||||||
|
self.plugin_view.addAction(self.history_action)
|
||||||
|
self.forum_action = QAction(QIcon(I('plugins/mobileread.png')), _('Plugin &Forum Thread'), self)
|
||||||
|
self.forum_action.triggered.connect(self._forum_label_activated)
|
||||||
|
self.forum_action.setEnabled(False)
|
||||||
|
self.plugin_view.addAction(self.forum_action)
|
||||||
|
|
||||||
|
sep1 = QAction(self)
|
||||||
|
sep1.setSeparator(True)
|
||||||
|
self.plugin_view.addAction(sep1)
|
||||||
|
|
||||||
|
self.toggle_enabled_action = QAction(_('Enable/&Disable plugin'), self)
|
||||||
|
self.toggle_enabled_action.setToolTip(_('Enable or disable this plugin'))
|
||||||
|
self.toggle_enabled_action.triggered.connect(self._toggle_enabled_clicked)
|
||||||
|
self.toggle_enabled_action.setEnabled(False)
|
||||||
|
self.plugin_view.addAction(self.toggle_enabled_action)
|
||||||
|
self.uninstall_action = QAction(_('&Remove plugin'), self)
|
||||||
|
self.uninstall_action.setToolTip(_('Uninstall the selected plugin'))
|
||||||
|
self.uninstall_action.triggered.connect(self._uninstall_clicked)
|
||||||
|
self.uninstall_action.setEnabled(False)
|
||||||
|
self.plugin_view.addAction(self.uninstall_action)
|
||||||
|
|
||||||
|
sep2 = QAction(self)
|
||||||
|
sep2.setSeparator(True)
|
||||||
|
self.plugin_view.addAction(sep2)
|
||||||
|
|
||||||
|
self.donate_enabled_action = QAction(QIcon(I('donate.png')), _('Donate to developer'), self)
|
||||||
|
self.donate_enabled_action.setToolTip(_('Donate to the developer of this plugin'))
|
||||||
|
self.donate_enabled_action.triggered.connect(self._donate_clicked)
|
||||||
|
self.donate_enabled_action.setEnabled(False)
|
||||||
|
self.plugin_view.addAction(self.donate_enabled_action)
|
||||||
|
|
||||||
|
sep3 = QAction(self)
|
||||||
|
sep3.setSeparator(True)
|
||||||
|
self.plugin_view.addAction(sep3)
|
||||||
|
|
||||||
|
self.configure_action = QAction(QIcon(I('config.png')), _('&Customize plugin'), self)
|
||||||
|
self.configure_action.setToolTip(_('Customize the options for this plugin'))
|
||||||
|
self.configure_action.triggered.connect(self._configure_clicked)
|
||||||
|
self.configure_action.setEnabled(False)
|
||||||
|
self.plugin_view.addAction(self.configure_action)
|
||||||
|
|
||||||
|
def _close_clicked(self):
|
||||||
|
# Force our toolbar/action to be updated based on uninstalled updates
|
||||||
|
if self.model:
|
||||||
|
update_plugins = filter(filter_upgradeable_plugins, self.model.display_plugins)
|
||||||
|
self.gui.recalc_update_label(len(update_plugins))
|
||||||
|
self.reject()
|
||||||
|
|
||||||
|
def _plugin_current_changed(self, current, previous):
|
||||||
|
if current.isValid():
|
||||||
|
actual_idx = self.proxy_model.mapToSource(current)
|
||||||
|
display_plugin = self.model.display_plugins[actual_idx.row()]
|
||||||
|
self.description.setText(display_plugin.description)
|
||||||
|
self.forum_link = display_plugin.forum_link
|
||||||
|
self.forum_action.setEnabled(bool(self.forum_link))
|
||||||
|
self.install_button.setEnabled(display_plugin.is_valid_to_install())
|
||||||
|
self.install_action.setEnabled(self.install_button.isEnabled())
|
||||||
|
self.uninstall_action.setEnabled(display_plugin.is_installed())
|
||||||
|
self.history_action.setEnabled(display_plugin.has_changelog)
|
||||||
|
self.configure_button.setEnabled(display_plugin.is_installed())
|
||||||
|
self.configure_action.setEnabled(self.configure_button.isEnabled())
|
||||||
|
self.toggle_enabled_action.setEnabled(display_plugin.is_installed())
|
||||||
|
self.donate_enabled_action.setEnabled(bool(display_plugin.donation_link))
|
||||||
|
else:
|
||||||
|
self.description.setText('')
|
||||||
|
self.forum_link = None
|
||||||
|
self.forum_action.setEnabled(False)
|
||||||
|
self.install_button.setEnabled(False)
|
||||||
|
self.install_action.setEnabled(False)
|
||||||
|
self.uninstall_action.setEnabled(False)
|
||||||
|
self.history_action.setEnabled(False)
|
||||||
|
self.configure_button.setEnabled(False)
|
||||||
|
self.configure_action.setEnabled(False)
|
||||||
|
self.toggle_enabled_action.setEnabled(False)
|
||||||
|
self.donate_enabled_action.setEnabled(False)
|
||||||
|
|
||||||
|
def _donate_clicked(self):
|
||||||
|
plugin = self._selected_display_plugin()
|
||||||
|
if plugin and plugin.donation_link:
|
||||||
|
open_url(QUrl(plugin.donation_link))
|
||||||
|
|
||||||
|
def _select_and_focus_view(self, change_selection=True):
|
||||||
|
if change_selection and self.plugin_view.model().rowCount() > 0:
|
||||||
|
self.plugin_view.selectRow(0)
|
||||||
|
else:
|
||||||
|
idx = self.plugin_view.selectionModel().currentIndex()
|
||||||
|
self._plugin_current_changed(idx, 0)
|
||||||
|
self.plugin_view.setFocus()
|
||||||
|
|
||||||
|
def _filter_combo_changed(self, idx):
|
||||||
|
self.proxy_model.set_filter_criteria(idx)
|
||||||
|
if idx == FILTER_NOT_INSTALLED:
|
||||||
|
self.plugin_view.sortByColumn(5, Qt.DescendingOrder)
|
||||||
|
else:
|
||||||
|
self.plugin_view.sortByColumn(0, Qt.AscendingOrder)
|
||||||
|
self._select_and_focus_view()
|
||||||
|
|
||||||
|
def _forum_label_activated(self):
|
||||||
|
if self.forum_link:
|
||||||
|
open_url(QUrl(self.forum_link))
|
||||||
|
|
||||||
|
def _selected_display_plugin(self):
|
||||||
|
idx = self.plugin_view.selectionModel().currentIndex()
|
||||||
|
actual_idx = self.proxy_model.mapToSource(idx)
|
||||||
|
return self.model.display_plugins[actual_idx.row()]
|
||||||
|
|
||||||
|
def _uninstall_plugin(self, name_to_remove):
|
||||||
|
if DEBUG:
|
||||||
|
prints('Removing plugin: ', name_to_remove)
|
||||||
|
remove_plugin(name_to_remove)
|
||||||
|
# Make sure that any other plugins that required this plugin
|
||||||
|
# to be uninstalled first have the requirement removed
|
||||||
|
for display_plugin in self.model.display_plugins:
|
||||||
|
# Make sure we update the status and display of the
|
||||||
|
# plugin we just uninstalled
|
||||||
|
if name_to_remove in display_plugin.uninstall_plugins:
|
||||||
|
if DEBUG:
|
||||||
|
prints('Removing uninstall dependency for: ', display_plugin.name)
|
||||||
|
display_plugin.uninstall_plugins.remove(name_to_remove)
|
||||||
|
if display_plugin.name == name_to_remove:
|
||||||
|
if DEBUG:
|
||||||
|
prints('Resetting plugin to uninstalled status: ', display_plugin.name)
|
||||||
|
display_plugin.installed_version = None
|
||||||
|
display_plugin.plugin = None
|
||||||
|
display_plugin.uninstall_plugins = []
|
||||||
|
if self.proxy_model.filter_criteria not in [FILTER_INSTALLED, FILTER_UPDATE_AVAILABLE]:
|
||||||
|
self.model.refresh_plugin(display_plugin)
|
||||||
|
|
||||||
|
def _uninstall_clicked(self):
|
||||||
|
display_plugin = self._selected_display_plugin()
|
||||||
|
if not question_dialog(self, _('Are you sure?'), '<p>'+
|
||||||
|
_('Are you sure you want to uninstall the <b>%s</b> plugin?')%display_plugin.name,
|
||||||
|
show_copy_button=False):
|
||||||
|
return
|
||||||
|
self._uninstall_plugin(display_plugin.name)
|
||||||
|
if self.proxy_model.filter_criteria in [FILTER_INSTALLED, FILTER_UPDATE_AVAILABLE]:
|
||||||
|
self.model.reset()
|
||||||
|
self._select_and_focus_view()
|
||||||
|
else:
|
||||||
|
self._select_and_focus_view(change_selection=False)
|
||||||
|
|
||||||
|
def _install_clicked(self):
|
||||||
|
display_plugin = self._selected_display_plugin()
|
||||||
|
if not question_dialog(self, _('Install %s')%display_plugin.name, '<p>' + \
|
||||||
|
_('Installing plugins is a <b>security risk</b>. '
|
||||||
|
'Plugins can contain a virus/malware. '
|
||||||
|
'Only install it if you got it from a trusted source.'
|
||||||
|
' Are you sure you want to proceed?'),
|
||||||
|
show_copy_button=False):
|
||||||
|
return
|
||||||
|
|
||||||
|
if display_plugin.uninstall_plugins:
|
||||||
|
uninstall_names = list(display_plugin.uninstall_plugins)
|
||||||
|
if DEBUG:
|
||||||
|
prints('Uninstalling plugin: ', ', '.join(uninstall_names))
|
||||||
|
for name_to_remove in uninstall_names:
|
||||||
|
self._uninstall_plugin(name_to_remove)
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
prints('Locating zip file for %s: %s'% (display_plugin.name, display_plugin.forum_link))
|
||||||
|
self.gui.status_bar.showMessage(_('Locating zip file for %s: %s') % (display_plugin.name, display_plugin.forum_link))
|
||||||
|
plugin_zip_url = self._read_zip_attachment_url(display_plugin.forum_link)
|
||||||
|
if not plugin_zip_url:
|
||||||
|
return error_dialog(self.gui, _('Install Plugin Failed'),
|
||||||
|
_('Unable to locate a plugin zip file for <b>%s</b>') % display_plugin.name,
|
||||||
|
det_msg=display_plugin.forum_link, show=True)
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
prints('Downloading plugin zip attachment: ', plugin_zip_url)
|
||||||
|
self.gui.status_bar.showMessage(_('Downloading plugin zip attachment: %s') % plugin_zip_url)
|
||||||
|
zip_path = self._download_zip(plugin_zip_url)
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
prints('Installing plugin: ', zip_path)
|
||||||
|
self.gui.status_bar.showMessage(_('Installing plugin: %s') % zip_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
plugin = add_plugin(zip_path)
|
||||||
|
except NameConflict as e:
|
||||||
|
return error_dialog(self.gui, _('Already exists'),
|
||||||
|
unicode(e), show=True)
|
||||||
|
# Check for any toolbars to add to.
|
||||||
|
widget = ConfigWidget(self.gui)
|
||||||
|
widget.gui = self.gui
|
||||||
|
widget.check_for_add_to_toolbars(plugin)
|
||||||
|
self.gui.status_bar.showMessage(_('Plugin installed: %s') % display_plugin.name)
|
||||||
|
info_dialog(self.gui, _('Success'),
|
||||||
|
_('Plugin <b>{0}</b> successfully installed under <b>'
|
||||||
|
' {1} plugins</b>. You may have to restart calibre '
|
||||||
|
'for the plugin to take effect.').format(plugin.name, plugin.type),
|
||||||
|
show=True, show_copy_button=False)
|
||||||
|
|
||||||
|
display_plugin.plugin = plugin
|
||||||
|
# We cannot read the 'actual' version information as the plugin will not be loaded yet
|
||||||
|
display_plugin.installed_version = display_plugin.available_version
|
||||||
|
except:
|
||||||
|
if DEBUG:
|
||||||
|
prints('ERROR occurred while installing plugin: %s'%display_plugin.name)
|
||||||
|
traceback.print_exc()
|
||||||
|
error_dialog(self.gui, _('Install Plugin Failed'),
|
||||||
|
_('A problem occurred while installing this plugin.'
|
||||||
|
' This plugin will now be uninstalled.'
|
||||||
|
' Please post the error message in details below into'
|
||||||
|
' the forum thread for this plugin and restart Calibre.'),
|
||||||
|
det_msg=traceback.format_exc(), show=True)
|
||||||
|
if DEBUG:
|
||||||
|
prints('Due to error now uninstalling plugin: %s'%display_plugin.name)
|
||||||
|
remove_plugin(display_plugin.name)
|
||||||
|
display_plugin.plugin = None
|
||||||
|
|
||||||
|
display_plugin.uninstall_plugins = []
|
||||||
|
if self.proxy_model.filter_criteria in [FILTER_NOT_INSTALLED, FILTER_UPDATE_AVAILABLE]:
|
||||||
|
self.model.reset()
|
||||||
|
self._select_and_focus_view()
|
||||||
|
else:
|
||||||
|
self.model.refresh_plugin(display_plugin)
|
||||||
|
self._select_and_focus_view(change_selection=False)
|
||||||
|
|
||||||
|
def _history_clicked(self):
|
||||||
|
display_plugin = self._selected_display_plugin()
|
||||||
|
text = self._read_version_history_html(display_plugin.forum_link)
|
||||||
|
if text:
|
||||||
|
dlg = VersionHistoryDialog(self, display_plugin.name, text)
|
||||||
|
dlg.exec_()
|
||||||
|
else:
|
||||||
|
return error_dialog(self, _('Version history missing'),
|
||||||
|
_('Unable to find the version history for %s')%display_plugin.name,
|
||||||
|
show=True)
|
||||||
|
|
||||||
|
def _configure_clicked(self):
|
||||||
|
display_plugin = self._selected_display_plugin()
|
||||||
|
plugin = display_plugin.plugin
|
||||||
|
if not plugin.is_customizable():
|
||||||
|
return info_dialog(self, _('Plugin not customizable'),
|
||||||
|
_('Plugin: %s does not need customization')%plugin.name, show=True)
|
||||||
|
from calibre.customize import InterfaceActionBase
|
||||||
|
if isinstance(plugin, InterfaceActionBase) and not getattr(plugin,
|
||||||
|
'actual_iaction_plugin_loaded', False):
|
||||||
|
return error_dialog(self, _('Must restart'),
|
||||||
|
_('You must restart calibre before you can'
|
||||||
|
' configure the <b>%s</b> plugin')%plugin.name, show=True)
|
||||||
|
plugin.do_user_config(self.parent())
|
||||||
|
|
||||||
|
def _toggle_enabled_clicked(self):
|
||||||
|
display_plugin = self._selected_display_plugin()
|
||||||
|
plugin = display_plugin.plugin
|
||||||
|
if not plugin.can_be_disabled:
|
||||||
|
return error_dialog(self,_('Plugin cannot be disabled'),
|
||||||
|
_('The plugin: %s cannot be disabled')%plugin.name, show=True)
|
||||||
|
if is_disabled(plugin):
|
||||||
|
enable_plugin(plugin)
|
||||||
|
else:
|
||||||
|
disable_plugin(plugin)
|
||||||
|
self.model.refresh_plugin(display_plugin)
|
||||||
|
|
||||||
|
def _read_version_history_html(self, forum_link):
|
||||||
|
br = browser()
|
||||||
|
br.set_handle_gzip(True)
|
||||||
|
try:
|
||||||
|
raw = br.open_novisit(forum_link).read()
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
raw = raw.decode('utf-8', errors='replace')
|
||||||
|
root = html.fromstring(raw)
|
||||||
|
spoiler_nodes = root.xpath('//div[@class="smallfont" and strong="Spoiler"]')
|
||||||
|
for spoiler_node in spoiler_nodes:
|
||||||
|
try:
|
||||||
|
if spoiler_node.getprevious() is None:
|
||||||
|
# This is a spoiler node that has been indented using [INDENT]
|
||||||
|
# Need to go up to parent div, then previous node to get header
|
||||||
|
heading_node = spoiler_node.getparent().getprevious()
|
||||||
|
else:
|
||||||
|
# This is a spoiler node after a BR tag from the heading
|
||||||
|
heading_node = spoiler_node.getprevious().getprevious()
|
||||||
|
if heading_node is None:
|
||||||
|
continue
|
||||||
|
if heading_node.text_content().lower().find('version history') != -1:
|
||||||
|
div_node = spoiler_node.xpath('div')[0]
|
||||||
|
text = html.tostring(div_node, method='html', encoding=unicode)
|
||||||
|
return re.sub('<div\s.*?>', '<div>', text)
|
||||||
|
except:
|
||||||
|
if DEBUG:
|
||||||
|
prints('======= MobileRead Parse Error =======')
|
||||||
|
traceback.print_exc()
|
||||||
|
prints(html.tostring(spoiler_node))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _read_zip_attachment_url(self, forum_link):
|
||||||
|
br = browser()
|
||||||
|
br.set_handle_gzip(True)
|
||||||
|
try:
|
||||||
|
raw = br.open_novisit(forum_link).read()
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
raw = raw.decode('utf-8', errors='replace')
|
||||||
|
root = html.fromstring(raw)
|
||||||
|
attachment_nodes = root.xpath('//fieldset/table/tr/td/a')
|
||||||
|
for attachment_node in attachment_nodes:
|
||||||
|
try:
|
||||||
|
filename = attachment_node.text_content().lower()
|
||||||
|
if filename.find('.zip') != -1:
|
||||||
|
full_url = MR_URL + attachment_node.attrib['href']
|
||||||
|
return full_url
|
||||||
|
except:
|
||||||
|
if DEBUG:
|
||||||
|
prints('======= MobileRead Parse Error =======')
|
||||||
|
traceback.print_exc()
|
||||||
|
prints(html.tostring(attachment_node))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _download_zip(self, plugin_zip_url):
|
||||||
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
|
br = browser()
|
||||||
|
br.set_handle_gzip(True)
|
||||||
|
raw = br.open_novisit(plugin_zip_url).read()
|
||||||
|
pt = PersistentTemporaryFile('.zip')
|
||||||
|
pt.write(raw)
|
||||||
|
pt.close()
|
||||||
|
return pt.name
|
@ -27,7 +27,6 @@ def partial(*args, **kwargs):
|
|||||||
_keep_refs.append(ans)
|
_keep_refs.append(ans)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
class LibraryViewMixin(object): # {{{
|
class LibraryViewMixin(object): # {{{
|
||||||
|
|
||||||
def __init__(self, db):
|
def __init__(self, db):
|
||||||
@ -145,6 +144,7 @@ class UpdateLabel(QLabel): # {{{
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
QLabel.__init__(self, *args, **kwargs)
|
QLabel.__init__(self, *args, **kwargs)
|
||||||
|
self.setCursor(Qt.PointingHandCursor)
|
||||||
|
|
||||||
def contextMenuEvent(self, e):
|
def contextMenuEvent(self, e):
|
||||||
pass
|
pass
|
||||||
@ -182,14 +182,6 @@ class StatusBar(QStatusBar): # {{{
|
|||||||
self.defmsg.setText(self.default_message)
|
self.defmsg.setText(self.default_message)
|
||||||
self.clearMessage()
|
self.clearMessage()
|
||||||
|
|
||||||
def new_version_available(self, ver, url):
|
|
||||||
msg = (u'<span style="color:red; font-weight: bold">%s: <a'
|
|
||||||
' href="update:%s">%s<a></span>') % (
|
|
||||||
_('Update found'), ver, ver)
|
|
||||||
self.update_label.setText(msg)
|
|
||||||
self.update_label.setCursor(Qt.PointingHandCursor)
|
|
||||||
self.update_label.setVisible(True)
|
|
||||||
|
|
||||||
def get_version(self):
|
def get_version(self):
|
||||||
dv = os.environ.get('CALIBRE_DEVELOP_FROM', None)
|
dv = os.environ.get('CALIBRE_DEVELOP_FROM', None)
|
||||||
v = __version__
|
v = __version__
|
||||||
@ -257,12 +249,6 @@ class LayoutMixin(object): # {{{
|
|||||||
self.setStatusBar(self.status_bar)
|
self.setStatusBar(self.status_bar)
|
||||||
self.status_bar.update_label.linkActivated.connect(self.update_link_clicked)
|
self.status_bar.update_label.linkActivated.connect(self.update_link_clicked)
|
||||||
|
|
||||||
def update_link_clicked(self, url):
|
|
||||||
url = unicode(url)
|
|
||||||
if url.startswith('update:'):
|
|
||||||
version = url.partition(':')[-1]
|
|
||||||
self.update_found(version, force=True)
|
|
||||||
|
|
||||||
def finalize_layout(self):
|
def finalize_layout(self):
|
||||||
self.status_bar.initialize(self.system_tray_icon)
|
self.status_bar.initialize(self.system_tray_icon)
|
||||||
self.book_details.show_book_info.connect(self.iactions['Show Book Details'].show_book_info)
|
self.book_details.show_book_info.connect(self.iactions['Show Book Details'].show_book_info)
|
||||||
|
@ -8,16 +8,16 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import textwrap, os
|
import textwrap, os
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from PyQt4.Qt import Qt, QModelIndex, QAbstractItemModel, QVariant, QIcon, \
|
from PyQt4.Qt import (Qt, QModelIndex, QAbstractItemModel, QVariant, QIcon,
|
||||||
QBrush
|
QBrush)
|
||||||
|
|
||||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
||||||
from calibre.gui2.preferences.plugins_ui import Ui_Form
|
from calibre.gui2.preferences.plugins_ui import Ui_Form
|
||||||
from calibre.customize.ui import (initialized_plugins, is_disabled, enable_plugin,
|
from calibre.customize.ui import (initialized_plugins, is_disabled, enable_plugin,
|
||||||
disable_plugin, plugin_customization, add_plugin,
|
disable_plugin, plugin_customization, add_plugin,
|
||||||
remove_plugin, NameConflict)
|
remove_plugin, NameConflict)
|
||||||
from calibre.gui2 import NONE, error_dialog, info_dialog, choose_files, \
|
from calibre.gui2 import (NONE, error_dialog, info_dialog, choose_files,
|
||||||
question_dialog, gprefs
|
question_dialog, gprefs)
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
from calibre.utils.icu import lower
|
from calibre.utils.icu import lower
|
||||||
|
|
||||||
@ -217,6 +217,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
self.customize_plugin_button.clicked.connect(self.customize_plugin)
|
self.customize_plugin_button.clicked.connect(self.customize_plugin)
|
||||||
self.remove_plugin_button.clicked.connect(self.remove_plugin)
|
self.remove_plugin_button.clicked.connect(self.remove_plugin)
|
||||||
self.button_plugin_add.clicked.connect(self.add_plugin)
|
self.button_plugin_add.clicked.connect(self.add_plugin)
|
||||||
|
self.button_plugin_updates.clicked.connect(self.update_plugins)
|
||||||
|
self.button_plugin_new.clicked.connect(self.get_plugins)
|
||||||
self.search.initialize('plugin_search_history',
|
self.search.initialize('plugin_search_history',
|
||||||
help_text=_('Search for plugin'))
|
help_text=_('Search for plugin'))
|
||||||
self.search.search.connect(self.find)
|
self.search.search.connect(self.find)
|
||||||
@ -353,6 +355,19 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
plugin.name + _(' cannot be removed. It is a '
|
plugin.name + _(' cannot be removed. It is a '
|
||||||
'builtin plugin. Try disabling it instead.')).exec_()
|
'builtin plugin. Try disabling it instead.')).exec_()
|
||||||
|
|
||||||
|
def get_plugins(self):
|
||||||
|
self.update_plugins(not_installed=True)
|
||||||
|
|
||||||
|
def update_plugins(self, not_installed=False):
|
||||||
|
from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog,
|
||||||
|
FILTER_UPDATE_AVAILABLE, FILTER_NOT_INSTALLED)
|
||||||
|
mode = FILTER_NOT_INSTALLED if not_installed else FILTER_UPDATE_AVAILABLE
|
||||||
|
d = PluginUpdaterDialog(self.gui, initial_filter=mode)
|
||||||
|
d.exec_()
|
||||||
|
self._plugin_model.populate()
|
||||||
|
self._plugin_model.reset()
|
||||||
|
self.changed_signal.emit()
|
||||||
|
|
||||||
def reload_store_plugins(self):
|
def reload_store_plugins(self):
|
||||||
self.gui.load_store_plugins()
|
self.gui.load_store_plugins()
|
||||||
if self.gui.iactions.has_key('Store'):
|
if self.gui.iactions.has_key('Store'):
|
||||||
|
@ -113,16 +113,49 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="button_plugin_add">
|
<widget class="QFrame" name="frame">
|
||||||
<property name="text">
|
<property name="frameShape">
|
||||||
<string>&Add a new plugin</string>
|
<enum>QFrame::HLine</enum>
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset resource="../../../../resources/images.qrc">
|
|
||||||
<normaloff>:/images/plugins.png</normaloff>:/images/plugins.png</iconset>
|
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="button_plugin_add">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Add a new plugin</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/plugins.png</normaloff>:/images/plugins.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="button_plugin_updates">
|
||||||
|
<property name="text">
|
||||||
|
<string>Check for &updated plugins</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/plugins/plugin_updater.png</normaloff>:/images/plugins/plugin_updater.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="button_plugin_new">
|
||||||
|
<property name="text">
|
||||||
|
<string>Get &new plugins</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/plugins/plugin_new.png</normaloff>:/images/plugins/plugin_new.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
|
@ -3,16 +3,30 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from PyQt4.Qt import QThread, pyqtSignal, Qt, QUrl, QDialog, QGridLayout, \
|
from PyQt4.Qt import (QThread, pyqtSignal, Qt, QUrl, QDialog, QGridLayout,
|
||||||
QLabel, QCheckBox, QDialogButtonBox, QIcon, QPixmap
|
QLabel, QCheckBox, QDialogButtonBox, QIcon, QPixmap)
|
||||||
import mechanize
|
import mechanize
|
||||||
|
|
||||||
from calibre.constants import __appname__, __version__, iswindows, isosx
|
from calibre.constants import (__appname__, __version__, iswindows, isosx,
|
||||||
|
isportable)
|
||||||
from calibre import browser
|
from calibre import browser
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.gui2 import config, dynamic, open_url
|
from calibre.gui2 import config, dynamic, open_url
|
||||||
|
from calibre.gui2.dialogs.plugin_updater import get_plugin_updates_available
|
||||||
|
|
||||||
URL = 'http://status.calibre-ebook.com/latest'
|
URL = 'http://status.calibre-ebook.com/latest'
|
||||||
|
NO_CALIBRE_UPDATE = '-0.0.0'
|
||||||
|
VSEP = '|'
|
||||||
|
|
||||||
|
def get_newest_version():
|
||||||
|
br = browser()
|
||||||
|
req = mechanize.Request(URL)
|
||||||
|
req.add_header('CALIBRE_VERSION', __version__)
|
||||||
|
req.add_header('CALIBRE_OS',
|
||||||
|
'win' if iswindows else 'osx' if isosx else 'oth')
|
||||||
|
req.add_header('CALIBRE_INSTALL_UUID', prefs['installation_uuid'])
|
||||||
|
version = br.open(req).read().strip()
|
||||||
|
return version
|
||||||
|
|
||||||
class CheckForUpdates(QThread):
|
class CheckForUpdates(QThread):
|
||||||
|
|
||||||
@ -24,23 +38,29 @@ class CheckForUpdates(QThread):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while True:
|
while True:
|
||||||
|
calibre_update_version = NO_CALIBRE_UPDATE
|
||||||
|
plugins_update_found = 0
|
||||||
try:
|
try:
|
||||||
br = browser()
|
version = get_newest_version()
|
||||||
req = mechanize.Request(URL)
|
|
||||||
req.add_header('CALIBRE_VERSION', __version__)
|
|
||||||
req.add_header('CALIBRE_OS',
|
|
||||||
'win' if iswindows else 'osx' if isosx else 'oth')
|
|
||||||
req.add_header('CALIBRE_INSTALL_UUID', prefs['installation_uuid'])
|
|
||||||
version = br.open(req).read().strip()
|
|
||||||
if version and version != __version__ and len(version) < 10:
|
if version and version != __version__ and len(version) < 10:
|
||||||
self.update_found.emit(version)
|
calibre_update_version = version
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
try:
|
||||||
|
update_plugins = get_plugin_updates_available()
|
||||||
|
if update_plugins is not None:
|
||||||
|
plugins_update_found = len(update_plugins)
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
if (calibre_update_version != NO_CALIBRE_UPDATE or
|
||||||
|
plugins_update_found > 0):
|
||||||
|
self.update_found.emit('%s%s%d'%(calibre_update_version,
|
||||||
|
VSEP, plugins_update_found))
|
||||||
self.sleep(self.INTERVAL)
|
self.sleep(self.INTERVAL)
|
||||||
|
|
||||||
class UpdateNotification(QDialog):
|
class UpdateNotification(QDialog):
|
||||||
|
|
||||||
def __init__(self, version, parent=None):
|
def __init__(self, calibre_version, plugin_updates, parent=None):
|
||||||
QDialog.__init__(self, parent)
|
QDialog.__init__(self, parent)
|
||||||
self.resize(400, 250)
|
self.resize(400, 250)
|
||||||
self.l = QGridLayout()
|
self.l = QGridLayout()
|
||||||
@ -54,7 +74,8 @@ class UpdateNotification(QDialog):
|
|||||||
'See the <a href="http://calibre-ebook.com/whats-new'
|
'See the <a href="http://calibre-ebook.com/whats-new'
|
||||||
'">new features</a>.') + '<p>'+_('Update <b>only</b> if one of the '
|
'">new features</a>.') + '<p>'+_('Update <b>only</b> if one of the '
|
||||||
'new features or bug fixes is important to you. '
|
'new features or bug fixes is important to you. '
|
||||||
'If the current version works well for you, do not update.'))%(__appname__, version))
|
'If the current version works well for you, do not update.'))%(
|
||||||
|
__appname__, calibre_version))
|
||||||
self.label.setOpenExternalLinks(True)
|
self.label.setOpenExternalLinks(True)
|
||||||
self.label.setWordWrap(True)
|
self.label.setWordWrap(True)
|
||||||
self.setWindowTitle(_('Update available!'))
|
self.setWindowTitle(_('Update available!'))
|
||||||
@ -70,18 +91,30 @@ class UpdateNotification(QDialog):
|
|||||||
b = self.bb.addButton(_('&Get update'), self.bb.AcceptRole)
|
b = self.bb.addButton(_('&Get update'), self.bb.AcceptRole)
|
||||||
b.setDefault(True)
|
b.setDefault(True)
|
||||||
b.setIcon(QIcon(I('arrow-down.png')))
|
b.setIcon(QIcon(I('arrow-down.png')))
|
||||||
|
if plugin_updates > 0:
|
||||||
|
b = self.bb.addButton(_('Update &plugins'), self.bb.ActionRole)
|
||||||
|
b.setIcon(QIcon(I('plugins/plugin_updater.png')))
|
||||||
|
b.clicked.connect(self.get_plugins, type=Qt.QueuedConnection)
|
||||||
self.bb.addButton(self.bb.Cancel)
|
self.bb.addButton(self.bb.Cancel)
|
||||||
self.l.addWidget(self.bb, 2, 0, 1, -1)
|
self.l.addWidget(self.bb, 2, 0, 1, -1)
|
||||||
self.bb.accepted.connect(self.accept)
|
self.bb.accepted.connect(self.accept)
|
||||||
self.bb.rejected.connect(self.reject)
|
self.bb.rejected.connect(self.reject)
|
||||||
dynamic.set('update to version %s'%version, False)
|
dynamic.set('update to version %s'%calibre_version, False)
|
||||||
|
|
||||||
|
def get_plugins(self):
|
||||||
|
from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog,
|
||||||
|
FILTER_UPDATE_AVAILABLE)
|
||||||
|
d = PluginUpdaterDialog(self.parent(),
|
||||||
|
initial_filter=FILTER_UPDATE_AVAILABLE)
|
||||||
|
d.exec_()
|
||||||
|
|
||||||
def show_future(self, *args):
|
def show_future(self, *args):
|
||||||
config.set('new_version_notification', bool(self.cb.isChecked()))
|
config.set('new_version_notification', bool(self.cb.isChecked()))
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
url = 'http://calibre-ebook.com/download_'+\
|
url = ('http://calibre-ebook.com/download_' +
|
||||||
('windows' if iswindows else 'osx' if isosx else 'linux')
|
('portable' if isportable else 'windows' if iswindows
|
||||||
|
else 'osx' if isosx else 'linux'))
|
||||||
open_url(QUrl(url))
|
open_url(QUrl(url))
|
||||||
|
|
||||||
QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
@ -89,21 +122,79 @@ class UpdateNotification(QDialog):
|
|||||||
class UpdateMixin(object):
|
class UpdateMixin(object):
|
||||||
|
|
||||||
def __init__(self, opts):
|
def __init__(self, opts):
|
||||||
|
self.last_newest_calibre_version = NO_CALIBRE_UPDATE
|
||||||
if not opts.no_update_check:
|
if not opts.no_update_check:
|
||||||
self.update_checker = CheckForUpdates(self)
|
self.update_checker = CheckForUpdates(self)
|
||||||
self.update_checker.update_found.connect(self.update_found,
|
self.update_checker.update_found.connect(self.update_found,
|
||||||
type=Qt.QueuedConnection)
|
type=Qt.QueuedConnection)
|
||||||
self.update_checker.start()
|
self.update_checker.start()
|
||||||
|
|
||||||
def update_found(self, version, force=False):
|
def recalc_update_label(self, number_of_plugin_updates):
|
||||||
os = 'windows' if iswindows else 'osx' if isosx else 'linux'
|
self.update_found('%s%s%d'%(self.last_newest_calibre_version, VSEP,
|
||||||
url = 'http://calibre-ebook.com/download_%s'%os
|
number_of_plugin_updates), no_show_popup=True)
|
||||||
self.status_bar.new_version_available(version, url)
|
|
||||||
|
|
||||||
if force or (config.get('new_version_notification') and \
|
def update_found(self, version, force=False, no_show_popup=False):
|
||||||
dynamic.get('update to version %s'%version, True)):
|
try:
|
||||||
self._update_notification__ = UpdateNotification(version,
|
calibre_version, plugin_updates = version.split(VSEP)
|
||||||
parent=self)
|
plugin_updates = int(plugin_updates)
|
||||||
self._update_notification__.show()
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
return
|
||||||
|
self.last_newest_calibre_version = calibre_version
|
||||||
|
has_calibre_update = calibre_version and calibre_version != NO_CALIBRE_UPDATE
|
||||||
|
has_plugin_updates = plugin_updates > 0
|
||||||
|
self.plugin_update_found(plugin_updates)
|
||||||
|
|
||||||
|
if not has_calibre_update and not has_plugin_updates:
|
||||||
|
self.status_bar.update_label.setVisible(False)
|
||||||
|
return
|
||||||
|
if has_calibre_update:
|
||||||
|
plt = u''
|
||||||
|
if has_plugin_updates:
|
||||||
|
plt = _(' (%d plugin updates)')%plugin_updates
|
||||||
|
msg = (u'<span style="color:red; font-weight: bold">%s: '
|
||||||
|
u'<a href="update:%s">%s%s</a></span>') % (
|
||||||
|
_('Update found'), version, calibre_version, plt)
|
||||||
|
else:
|
||||||
|
msg = (u'<a href="update:%s">%d %s</a>')%(version, plugin_updates,
|
||||||
|
_('updated plugins'))
|
||||||
|
self.status_bar.update_label.setText(msg)
|
||||||
|
self.status_bar.update_label.setVisible(True)
|
||||||
|
|
||||||
|
|
||||||
|
if has_calibre_update:
|
||||||
|
if (force or (config.get('new_version_notification') and
|
||||||
|
dynamic.get('update to version %s'%calibre_version, True))):
|
||||||
|
if not no_show_popup:
|
||||||
|
self._update_notification__ = UpdateNotification(calibre_version,
|
||||||
|
plugin_updates, parent=self)
|
||||||
|
self._update_notification__.show()
|
||||||
|
elif has_plugin_updates:
|
||||||
|
if force:
|
||||||
|
from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog,
|
||||||
|
FILTER_UPDATE_AVAILABLE)
|
||||||
|
d = PluginUpdaterDialog(self,
|
||||||
|
initial_filter=FILTER_UPDATE_AVAILABLE)
|
||||||
|
d.exec_()
|
||||||
|
|
||||||
|
def plugin_update_found(self, number_of_updates):
|
||||||
|
# Change the plugin icon to indicate there are updates available
|
||||||
|
plugin = self.iactions.get('Plugin Updates', None)
|
||||||
|
if not plugin:
|
||||||
|
return
|
||||||
|
if number_of_updates:
|
||||||
|
plugin.qaction.setText(_('Plugin Updates')+'*')
|
||||||
|
plugin.qaction.setIcon(QIcon(I('plugins/plugin_updater_updates.png')))
|
||||||
|
plugin.qaction.setToolTip(
|
||||||
|
_('There are %d plugin updates available')%number_of_updates)
|
||||||
|
else:
|
||||||
|
plugin.qaction.setText(_('Plugin Updates'))
|
||||||
|
plugin.qaction.setIcon(QIcon(I('plugins/plugin_updater.png')))
|
||||||
|
plugin.qaction.setToolTip(_('Install and configure user plugins'))
|
||||||
|
|
||||||
|
def update_link_clicked(self, url):
|
||||||
|
url = unicode(url)
|
||||||
|
if url.startswith('update:'):
|
||||||
|
version = url[len('update:'):]
|
||||||
|
self.update_found(version, force=True)
|
||||||
|
|
||||||
|
@ -5,15 +5,15 @@ Miscellaneous widgets used in the GUI
|
|||||||
'''
|
'''
|
||||||
import re, traceback
|
import re, traceback
|
||||||
|
|
||||||
from PyQt4.Qt import QIcon, QFont, QLabel, QListWidget, QAction, \
|
from PyQt4.Qt import (QIcon, QFont, QLabel, QListWidget, QAction,
|
||||||
QListWidgetItem, QTextCharFormat, QApplication, \
|
QListWidgetItem, QTextCharFormat, QApplication,
|
||||||
QSyntaxHighlighter, QCursor, QColor, QWidget, \
|
QSyntaxHighlighter, QCursor, QColor, QWidget,
|
||||||
QPixmap, QSplitterHandle, QToolButton, \
|
QPixmap, QSplitterHandle, QToolButton,
|
||||||
QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, \
|
QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal,
|
||||||
QRegExp, QSettings, QSize, QSplitter, \
|
QRegExp, QSettings, QSize, QSplitter,
|
||||||
QPainter, QLineEdit, QComboBox, QPen, QGraphicsScene, \
|
QPainter, QLineEdit, QComboBox, QPen, QGraphicsScene,
|
||||||
QMenu, QStringListModel, QCompleter, QStringList, \
|
QMenu, QStringListModel, QCompleter, QStringList,
|
||||||
QTimer, QRect, QFontDatabase, QGraphicsView
|
QTimer, QRect, QFontDatabase, QGraphicsView)
|
||||||
|
|
||||||
from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs
|
from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs
|
||||||
from calibre.gui2.filename_pattern_ui import Ui_Form
|
from calibre.gui2.filename_pattern_ui import Ui_Form
|
||||||
@ -21,12 +21,12 @@ from calibre import fit_image
|
|||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.utils.config import prefs, XMLConfig, tweaks
|
from calibre.utils.config import prefs, XMLConfig, tweaks
|
||||||
from calibre.gui2.progress_indicator import ProgressIndicator as _ProgressIndicator
|
from calibre.gui2.progress_indicator import ProgressIndicator as _ProgressIndicator
|
||||||
from calibre.gui2.dnd import dnd_has_image, dnd_get_image, dnd_get_files, \
|
from calibre.gui2.dnd import (dnd_has_image, dnd_get_image, dnd_get_files,
|
||||||
IMAGE_EXTENSIONS, dnd_has_extension, DownloadDialog
|
IMAGE_EXTENSIONS, dnd_has_extension, DownloadDialog)
|
||||||
|
|
||||||
history = XMLConfig('history')
|
history = XMLConfig('history')
|
||||||
|
|
||||||
class ProgressIndicator(QWidget):
|
class ProgressIndicator(QWidget): # {{{
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
QWidget.__init__(self, *args)
|
QWidget.__init__(self, *args)
|
||||||
@ -57,8 +57,9 @@ class ProgressIndicator(QWidget):
|
|||||||
def stop(self):
|
def stop(self):
|
||||||
self.pi.stopAnimation()
|
self.pi.stopAnimation()
|
||||||
self.setVisible(False)
|
self.setVisible(False)
|
||||||
|
# }}}
|
||||||
|
|
||||||
class FilenamePattern(QWidget, Ui_Form):
|
class FilenamePattern(QWidget, Ui_Form): # {{{
|
||||||
|
|
||||||
changed_signal = pyqtSignal()
|
changed_signal = pyqtSignal()
|
||||||
|
|
||||||
@ -148,8 +149,9 @@ class FilenamePattern(QWidget, Ui_Form):
|
|||||||
|
|
||||||
return pat
|
return pat
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class FormatList(QListWidget):
|
class FormatList(QListWidget): # {{{
|
||||||
DROPABBLE_EXTENSIONS = BOOK_EXTENSIONS
|
DROPABBLE_EXTENSIONS = BOOK_EXTENSIONS
|
||||||
formats_dropped = pyqtSignal(object, object)
|
formats_dropped = pyqtSignal(object, object)
|
||||||
delete_format = pyqtSignal()
|
delete_format = pyqtSignal()
|
||||||
@ -188,6 +190,8 @@ class FormatList(QListWidget):
|
|||||||
else:
|
else:
|
||||||
return QListWidget.keyPressEvent(self, event)
|
return QListWidget.keyPressEvent(self, event)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class ImageDropMixin(object): # {{{
|
class ImageDropMixin(object): # {{{
|
||||||
'''
|
'''
|
||||||
Adds support for dropping images onto widgets and a context menu for
|
Adds support for dropping images onto widgets and a context menu for
|
||||||
@ -262,7 +266,7 @@ class ImageDropMixin(object): # {{{
|
|||||||
pixmap_to_data(pmap))
|
pixmap_to_data(pmap))
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class ImageView(QWidget, ImageDropMixin):
|
class ImageView(QWidget, ImageDropMixin): # {{{
|
||||||
|
|
||||||
BORDER_WIDTH = 1
|
BORDER_WIDTH = 1
|
||||||
cover_changed = pyqtSignal(object)
|
cover_changed = pyqtSignal(object)
|
||||||
@ -314,8 +318,9 @@ class ImageView(QWidget, ImageDropMixin):
|
|||||||
p.drawRect(target)
|
p.drawRect(target)
|
||||||
#p.drawRect(self.rect())
|
#p.drawRect(self.rect())
|
||||||
p.end()
|
p.end()
|
||||||
|
# }}}
|
||||||
|
|
||||||
class CoverView(QGraphicsView, ImageDropMixin):
|
class CoverView(QGraphicsView, ImageDropMixin): # {{{
|
||||||
|
|
||||||
cover_changed = pyqtSignal(object)
|
cover_changed = pyqtSignal(object)
|
||||||
|
|
||||||
@ -333,7 +338,9 @@ class CoverView(QGraphicsView, ImageDropMixin):
|
|||||||
self.scene.addPixmap(pmap)
|
self.scene.addPixmap(pmap)
|
||||||
self.setScene(self.scene)
|
self.setScene(self.scene)
|
||||||
|
|
||||||
class FontFamilyModel(QAbstractListModel):
|
# }}}
|
||||||
|
|
||||||
|
class FontFamilyModel(QAbstractListModel): # {{{
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
QAbstractListModel.__init__(self, *args)
|
QAbstractListModel.__init__(self, *args)
|
||||||
@ -371,7 +378,9 @@ class FontFamilyModel(QAbstractListModel):
|
|||||||
|
|
||||||
def index_of(self, family):
|
def index_of(self, family):
|
||||||
return self.families.index(family.strip())
|
return self.families.index(family.strip())
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# BasicList {{{
|
||||||
class BasicListItem(QListWidgetItem):
|
class BasicListItem(QListWidgetItem):
|
||||||
|
|
||||||
def __init__(self, text, user_data=None):
|
def __init__(self, text, user_data=None):
|
||||||
@ -404,9 +413,9 @@ class BasicList(QListWidget):
|
|||||||
def items(self):
|
def items(self):
|
||||||
for i in range(self.count()):
|
for i in range(self.count()):
|
||||||
yield self.item(i)
|
yield self.item(i)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class LineEditECM(object): # {{{
|
||||||
class LineEditECM(object):
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Extend the context menu of a QLineEdit to include more actions.
|
Extend the context menu of a QLineEdit to include more actions.
|
||||||
@ -449,8 +458,9 @@ class LineEditECM(object):
|
|||||||
from calibre.utils.icu import capitalize
|
from calibre.utils.icu import capitalize
|
||||||
self.setText(capitalize(unicode(self.text())))
|
self.setText(capitalize(unicode(self.text())))
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class EnLineEdit(LineEditECM, QLineEdit):
|
class EnLineEdit(LineEditECM, QLineEdit): # {{{
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Enhanced QLineEdit.
|
Enhanced QLineEdit.
|
||||||
@ -459,9 +469,9 @@ class EnLineEdit(LineEditECM, QLineEdit):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class ItemsCompleter(QCompleter): # {{{
|
||||||
class ItemsCompleter(QCompleter):
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
A completer object that completes a list of tags. It is used in conjunction
|
A completer object that completes a list of tags. It is used in conjunction
|
||||||
@ -486,8 +496,9 @@ class ItemsCompleter(QCompleter):
|
|||||||
model = QStringListModel(items, self)
|
model = QStringListModel(items, self)
|
||||||
self.setModel(model)
|
self.setModel(model)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class CompleteLineEdit(EnLineEdit):
|
class CompleteLineEdit(EnLineEdit): # {{{
|
||||||
|
|
||||||
'''
|
'''
|
||||||
A QLineEdit that can complete parts of text separated by separator.
|
A QLineEdit that can complete parts of text separated by separator.
|
||||||
@ -550,8 +561,9 @@ class CompleteLineEdit(EnLineEdit):
|
|||||||
self.setText(complete_text_pat % (before_text[:cursor_pos - prefix_len], text, self.separator, after_text))
|
self.setText(complete_text_pat % (before_text[:cursor_pos - prefix_len], text, self.separator, after_text))
|
||||||
self.setCursorPosition(cursor_pos - prefix_len + len(text) + len_extra)
|
self.setCursorPosition(cursor_pos - prefix_len + len(text) + len_extra)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class EnComboBox(QComboBox):
|
class EnComboBox(QComboBox): # {{{
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Enhanced QComboBox.
|
Enhanced QComboBox.
|
||||||
@ -575,7 +587,9 @@ class EnComboBox(QComboBox):
|
|||||||
idx = 0
|
idx = 0
|
||||||
self.setCurrentIndex(idx)
|
self.setCurrentIndex(idx)
|
||||||
|
|
||||||
class CompleteComboBox(EnComboBox):
|
# }}}
|
||||||
|
|
||||||
|
class CompleteComboBox(EnComboBox): # {{{
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
EnComboBox.__init__(self, *args)
|
EnComboBox.__init__(self, *args)
|
||||||
@ -590,8 +604,9 @@ class CompleteComboBox(EnComboBox):
|
|||||||
def set_space_before_sep(self, space_before):
|
def set_space_before_sep(self, space_before):
|
||||||
self.lineEdit().set_space_before_sep(space_before)
|
self.lineEdit().set_space_before_sep(space_before)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class HistoryLineEdit(QComboBox):
|
class HistoryLineEdit(QComboBox): # {{{
|
||||||
|
|
||||||
lost_focus = pyqtSignal()
|
lost_focus = pyqtSignal()
|
||||||
|
|
||||||
@ -638,7 +653,9 @@ class HistoryLineEdit(QComboBox):
|
|||||||
QComboBox.focusOutEvent(self, e)
|
QComboBox.focusOutEvent(self, e)
|
||||||
self.lost_focus.emit()
|
self.lost_focus.emit()
|
||||||
|
|
||||||
class ComboBoxWithHelp(QComboBox):
|
# }}}
|
||||||
|
|
||||||
|
class ComboBoxWithHelp(QComboBox): # {{{
|
||||||
'''
|
'''
|
||||||
A combobox where item 0 is help text. CurrentText will return '' for item 0.
|
A combobox where item 0 is help text. CurrentText will return '' for item 0.
|
||||||
Be sure to always fetch the text with currentText. Don't use the signals
|
Be sure to always fetch the text with currentText. Don't use the signals
|
||||||
@ -685,8 +702,9 @@ class ComboBoxWithHelp(QComboBox):
|
|||||||
QComboBox.hidePopup(self)
|
QComboBox.hidePopup(self)
|
||||||
self.set_state()
|
self.set_state()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class EncodingComboBox(QComboBox):
|
class EncodingComboBox(QComboBox): # {{{
|
||||||
'''
|
'''
|
||||||
A combobox that holds text encodings support
|
A combobox that holds text encodings support
|
||||||
by Python. This is only populated with the most
|
by Python. This is only populated with the most
|
||||||
@ -709,8 +727,9 @@ class EncodingComboBox(QComboBox):
|
|||||||
for item in self.ENCODINGS:
|
for item in self.ENCODINGS:
|
||||||
self.addItem(item)
|
self.addItem(item)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class PythonHighlighter(QSyntaxHighlighter):
|
class PythonHighlighter(QSyntaxHighlighter): # {{{
|
||||||
|
|
||||||
Rules = []
|
Rules = []
|
||||||
Formats = {}
|
Formats = {}
|
||||||
@ -948,6 +967,9 @@ class PythonHighlighter(QSyntaxHighlighter):
|
|||||||
QSyntaxHighlighter.rehighlight(self)
|
QSyntaxHighlighter.rehighlight(self)
|
||||||
QApplication.restoreOverrideCursor()
|
QApplication.restoreOverrideCursor()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Splitter {{{
|
||||||
class SplitterHandle(QSplitterHandle):
|
class SplitterHandle(QSplitterHandle):
|
||||||
|
|
||||||
double_clicked = pyqtSignal(object)
|
double_clicked = pyqtSignal(object)
|
||||||
@ -1179,3 +1201,5 @@ class Splitter(QSplitter):
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|