From 76c0892b510f639b6638ac4edce48eca0d458b0c Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 24 Jul 2012 14:18:52 +0200 Subject: [PATCH 1/9] Add an interface to permit starting and stopping of devices without disabling them. Will be used by the smartdevice driver --- src/calibre/devices/interface.py | 48 ++++++++++++++++++++++++ src/calibre/gui2/actions/device.py | 35 ++++++++++++++++- src/calibre/gui2/device.py | 60 ++++++++++++++++++++++++++++++ src/calibre/gui2/ui.py | 9 +++++ src/calibre/utils/Zeroconf.py | 15 +++++--- 5 files changed, 160 insertions(+), 7 deletions(-) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 26239b59e7..0f2027065e 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -514,6 +514,54 @@ class DevicePlugin(Plugin): ''' pass + # Dynamic control interface + + def is_dynamically_controllable(self): + ''' + Called by the device manager when starting plugins. If this method returns + a string, then a) it supports the device manager's dynamic control + interface, and b) that name is to be used when talking to the plugin + ''' + return None + + def start_plugin(self): + ''' + This method is called to start the plugin. The plugin should begin + to accept device connections however it does that. If the plugin is + already accepting connections, then do nothing. + ''' + pass + + def stop_plugin(self): + ''' + This method is called to stop the plugin. The plugin should no longer + accept connections, and should cleanup behind itself. It is likely that + this method should call shutdown. If the plugin is already not accepting + connections, then do nothing. + ''' + pass + + def get_option(self, opt_string): + ''' + Return the value of the option indicated by opt_string. This method can + be called when the plugin is not started. Return None if the option does + not exist. + ''' + return None + + def set_option(self, opt_string, opt_value): + ''' + Set the value of the option indicated by opt_string. This method can + be called when the plugin is not started. + ''' + pass + + def is_running(self): + ''' + Return True if the plugin is started, otherwise false + ''' + return False + class BookList(list): ''' A list of books. Each Book object must have the fields diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index 0ef06d59d5..afbf2584ed 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -24,6 +24,7 @@ class ShareConnMenu(QMenu): # {{{ config_email = pyqtSignal() toggle_server = pyqtSignal() + toggle_smartdevice = pyqtSignal() dont_add_to = frozenset(['context-menu-device']) def __init__(self, parent=None): @@ -56,6 +57,11 @@ class ShareConnMenu(QMenu): # {{{ _('Start Content Server')) self.toggle_server_action.triggered.connect(lambda x: self.toggle_server.emit()) + self.toggle_smartdevice_action = \ + self.addAction(QIcon(I('devices/galaxy_s3.png')), + _('Start Smart Device Connections')) + self.toggle_smartdevice_action.triggered.connect(lambda x: + self.toggle_smartdevice.emit()) self.addSeparator() self.email_actions = [] @@ -80,6 +86,15 @@ class ShareConnMenu(QMenu): # {{{ text = _('Stop Content Server') + ' [%s]'%get_external_ip() self.toggle_server_action.setText(text) + def smartdevice_state_changed(self, accepting): + if accepting: + self.toggle_smartdevice_action.setText(_('Stop Smart Device Connections')) + else: + self.toggle_smartdevice_action.setText(_('Start Smart Device Connections')) + + def hide_smartdevice_menus(self): + self.toggle_smartdevice_action.setVisible(False) + def build_email_entries(self, sync_menu): from calibre.gui2.device import DeviceAction for ac in self.email_actions: @@ -158,6 +173,7 @@ class ConnectShareAction(InterfaceAction): def genesis(self): self.share_conn_menu = ShareConnMenu(self.gui) self.share_conn_menu.toggle_server.connect(self.toggle_content_server) + self.share_conn_menu.toggle_smartdevice.connect(self.toggle_smartdevice) self.share_conn_menu.config_email.connect(partial( self.gui.iactions['Preferences'].do_config, initial_plugin=('Sharing', 'Email'))) @@ -200,8 +216,23 @@ class ConnectShareAction(InterfaceAction): if not self.stopping_msg.isVisible(): self.stopping_msg.exec_() return - - self.gui.content_server = None self.stopping_msg.accept() + def toggle_smartdevice(self): + info_dialog(self.gui, _('Foobar'), + _('Start server bla bla blah...'), + show_copy_button=False, show=True) + if self.gui.device_manager.is_running('smartdevice'): + self.gui.device_manager.stop_plugin('smartdevice') + else: + self.gui.device_manager.start_plugin('smartdevice') + self.share_conn_menu.smartdevice_state_changed( + self.gui.device_manager.is_running('smartdevice')) + + def smartdevice_state_changed(self, running): + self.share_conn_menu.smartdevice_state_changed(running) + + def check_smartdevice_menus(self): + if not self.gui.device_manager.is_enabled('smartdevice'): + self.share_conn_menu.hide_smartdevice_menus() diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 17f1e47853..14a9093bd4 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -145,6 +145,8 @@ class DeviceManager(Thread): # {{{ self._device_information = None self.current_library_uuid = None self.call_shutdown_on_disconnect = False + self.devices_initialized = Queue.Queue(0) + self.dynamic_plugins = {} def report_progress(self, *args): pass @@ -286,6 +288,10 @@ class DeviceManager(Thread): # {{{ # Do any device-specific startup processing. for d in self.devices: self.run_startup(d) + n = d.is_dynamically_controllable() + if n: + self.dynamic_plugins[n] = d + self.devices_initialized.put(None) while self.keep_going: kls = None @@ -508,6 +514,59 @@ class DeviceManager(Thread): # {{{ if self.connected_device: self.connected_device.set_driveinfo_name(location_code, name) + # dynamic plugin interface + + def start_plugin(self, name): + try: + d = self.dynamic_plugins.get(name, None) + if d: + d.start_plugin() + except: + pass + + def stop_plugin(self, name): + try: + d = self.dynamic_plugins.get(name, None) + if d: + d.stop_plugin() + except: + pass + + def get_option(self, name, opt_string): + try: + d = self.dynamic_plugins.get(name, None) + if d: + return d.get_option(opt_string) + except: + pass + return None + + def set_option(self, name, opt_string, opt_value): + try: + d = self.dynamic_plugins.get(name, None) + if d: + d.set_option(opt_string, opt_value) + except: + pass + + def is_running(self, name): + try: + d = self.dynamic_plugins.get(name, None) + if d: + return d.is_running() + except: + pass + return False + + def is_enabled(self, name): + try: + d = self.dynamic_plugins.get(name, None) + if d: + return True + except: + pass + return False + # }}} class DeviceAction(QAction): # {{{ @@ -708,6 +767,7 @@ class DeviceMixin(object): # {{{ self.job_manager, Dispatcher(self.status_bar.show_message), Dispatcher(self.show_open_feedback)) self.device_manager.start() + self.device_manager.devices_initialized.get() if tweaks['auto_connect_to_folder']: self.connect_to_folder_named(tweaks['auto_connect_to_folder']) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index a597445f43..be86e91c29 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -337,6 +337,15 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ if config['autolaunch_server']: self.start_content_server() + smartdevice_actions = self.iactions['Connect Share'] + smartdevice_actions.check_smartdevice_menus() + if self.device_manager.get_option('smartdevice', 'autostart'): + try: + self.device_manager.start_plugin('smartdevice') + smartdevice_actions.smartdevice_state_changed(True) + except: + pass + self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) self.read_settings() diff --git a/src/calibre/utils/Zeroconf.py b/src/calibre/utils/Zeroconf.py index b722865101..1287148476 100755 --- a/src/calibre/utils/Zeroconf.py +++ b/src/calibre/utils/Zeroconf.py @@ -955,11 +955,16 @@ class Reaper(threading.Thread): return if globals()['_GLOBAL_DONE']: return - now = currentTimeMillis() - for record in self.zeroconf.cache.entries(): - if record.isExpired(now): - self.zeroconf.updateRecord(now, record) - self.zeroconf.cache.remove(record) + try: + # can get here in a race condition with shutdown. Swallow the + # exception and run around the loop again. + now = currentTimeMillis() + for record in self.zeroconf.cache.entries(): + if record.isExpired(now): + self.zeroconf.updateRecord(now, record) + self.zeroconf.cache.remove(record) + except: + pass class ServiceBrowser(threading.Thread): From b2f1c6294b2b68bd7ae21cf54d9707f8b5a4a74c Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 24 Jul 2012 16:42:59 +0200 Subject: [PATCH 2/9] Implement the smartdevice control dialog. --- src/calibre/gui2/actions/device.py | 10 +- src/calibre/gui2/dialogs/smartdevice.py | 78 ++++++++++++++ src/calibre/gui2/dialogs/smartdevice.ui | 129 ++++++++++++++++++++++++ 3 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 src/calibre/gui2/dialogs/smartdevice.py create mode 100644 src/calibre/gui2/dialogs/smartdevice.ui diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index afbf2584ed..8faf9f1717 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -14,6 +14,7 @@ from calibre.utils.smtp import config as email_config from calibre.constants import iswindows, isosx from calibre.customize.ui import is_disabled from calibre.devices.bambook.driver import BAMBOOK +from calibre.gui2.dialogs.smartdevice import SmartdeviceDialog from calibre.gui2 import info_dialog class ShareConnMenu(QMenu): # {{{ @@ -220,13 +221,8 @@ class ConnectShareAction(InterfaceAction): self.stopping_msg.accept() def toggle_smartdevice(self): - info_dialog(self.gui, _('Foobar'), - _('Start server bla bla blah...'), - show_copy_button=False, show=True) - if self.gui.device_manager.is_running('smartdevice'): - self.gui.device_manager.stop_plugin('smartdevice') - else: - self.gui.device_manager.start_plugin('smartdevice') + sd_dialog = SmartdeviceDialog(self.gui) + sd_dialog.exec_() self.share_conn_menu.smartdevice_state_changed( self.gui.device_manager.is_running('smartdevice')) diff --git a/src/calibre/gui2/dialogs/smartdevice.py b/src/calibre/gui2/dialogs/smartdevice.py new file mode 100644 index 0000000000..63c49a5fc7 --- /dev/null +++ b/src/calibre/gui2/dialogs/smartdevice.py @@ -0,0 +1,78 @@ +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal ' +import re +from PyQt4.QtGui import QDialog, QLineEdit +from PyQt4.QtCore import SIGNAL, Qt + +from calibre.gui2.dialogs.smartdevice_ui import Ui_Dialog +from calibre.gui2 import dynamic + +class SmartdeviceDialog(QDialog, Ui_Dialog): + + def __init__(self, parent): + QDialog.__init__(self, parent) + Ui_Dialog.__init__(self) + self.setupUi(self) + + self.msg.setText( + _('This dialog starts and stops the smart device app interface. ' + 'When you start the interface, you might see some messages from ' + 'your computer\'s firewall or anti-virus manager asking you ' + 'if it is OK for calibre to connect to the network. Please ' + 'answer yes. If you do not, the app will not work. It will ' + 'be unable to connect to calibre.')) + + self.passwd_msg.setText( + _('Use a password if calibre is running on a network that ' + 'is not secure. For example, if you run calibre on a laptop, ' + 'use that laptop in an airport, and want to connect your ' + 'smart device to calibre, you should use a password.')) + + self.auto_start_msg.setText( + _('Check this box if you want calibre to automatically start the ' + 'smart device interface when calibre starts. You should not do ' + 'this if you are using a network that is not secure and you ' + 'are not setting a password.')) + self.connect(self.show_password, SIGNAL('stateChanged(int)'), self.toggle_password) + + self.device_manager = parent.device_manager + if self.device_manager.get_option('smartdevice', 'autostart'): + self.autostart_box.setChecked(True) + pw = self.device_manager.get_option('smartdevice', 'password') + if pw: + self.password_box.setText(pw) + + if self.device_manager.is_running('smartdevice'): + self.start_button.setEnabled(False) + self.stop_button.setEnabled(True) + else: + self.start_button.setEnabled(True) + self.stop_button.setEnabled(False) + self.start_button.clicked.connect(self.start_button_clicked) + self.stop_button.clicked.connect(self.stop_button_clicked) + self.cancel_button.clicked.connect(self.cancel_button_clicked) + self.OK_button.clicked.connect(self.accept) + + def start_button_clicked(self): + self.device_manager.start_plugin('smartdevice') + self.accept() + + def stop_button_clicked(self): + self.device_manager.stop_plugin('smartdevice') + self.accept() + + def cancel_button_clicked(self): + QDialog.reject(self) + + def toggle_password(self, state): + if state == Qt.Unchecked: + self.password_box.setEchoMode(QLineEdit.Password) + else: + self.password_box.setEchoMode(QLineEdit.Normal) + + def accept(self): + self.device_manager.set_option('smartdevice', 'password', + unicode(self.password_box.text())) + self.device_manager.set_option('smartdevice', 'autostart', + self.autostart_box.isChecked()) + QDialog.accept(self) diff --git a/src/calibre/gui2/dialogs/smartdevice.ui b/src/calibre/gui2/dialogs/smartdevice.ui new file mode 100644 index 0000000000..26795249be --- /dev/null +++ b/src/calibre/gui2/dialogs/smartdevice.ui @@ -0,0 +1,129 @@ + + + Dialog + + + + 0 + 0 + 600 + 209 + + + + Smart device control + + + + :/images/mimetypes/unknown.png:/images/mimetypes/unknown.png + + + + + + TextLabel + + + true + + + + + + + &Password: + + + password_box + + + + + + + QLineEdit::Password + + + + + + + TextLabel + + + true + + + + 100 + 0 + + + + + + + + &Show password + + + + + + + &Auto-start + + + + + + + true + + + + + + + All the buttons except Cancel will save the above settings + + + + + + + + + Start interface + + + + + + + Stop interface + + + + + + + OK + + + + + + + Cancel + + + + + + + + + + + From 0f16e4d9c7006c2be08993f5e325f75432c7fea5 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 24 Jul 2012 18:14:22 +0200 Subject: [PATCH 3/9] Improved smartdevice control dialog --- src/calibre/gui2/actions/device.py | 27 ++---- src/calibre/gui2/dialogs/smartdevice.py | 51 +++++------ src/calibre/gui2/dialogs/smartdevice.ui | 110 +++++++++++++----------- src/calibre/gui2/ui.py | 1 - 4 files changed, 93 insertions(+), 96 deletions(-) diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index 8faf9f1717..8d08f53f0a 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -25,7 +25,7 @@ class ShareConnMenu(QMenu): # {{{ config_email = pyqtSignal() toggle_server = pyqtSignal() - toggle_smartdevice = pyqtSignal() + control_smartdevice = pyqtSignal() dont_add_to = frozenset(['context-menu-device']) def __init__(self, parent=None): @@ -58,11 +58,11 @@ class ShareConnMenu(QMenu): # {{{ _('Start Content Server')) self.toggle_server_action.triggered.connect(lambda x: self.toggle_server.emit()) - self.toggle_smartdevice_action = \ + self.control_smartdevice_action = \ self.addAction(QIcon(I('devices/galaxy_s3.png')), - _('Start Smart Device Connections')) - self.toggle_smartdevice_action.triggered.connect(lambda x: - self.toggle_smartdevice.emit()) + _('Control Smart Device Connections')) + self.control_smartdevice_action.triggered.connect(lambda x: + self.control_smartdevice.emit()) self.addSeparator() self.email_actions = [] @@ -87,14 +87,8 @@ class ShareConnMenu(QMenu): # {{{ text = _('Stop Content Server') + ' [%s]'%get_external_ip() self.toggle_server_action.setText(text) - def smartdevice_state_changed(self, accepting): - if accepting: - self.toggle_smartdevice_action.setText(_('Stop Smart Device Connections')) - else: - self.toggle_smartdevice_action.setText(_('Start Smart Device Connections')) - def hide_smartdevice_menus(self): - self.toggle_smartdevice_action.setVisible(False) + self.control_smartdevice_action.setVisible(False) def build_email_entries(self, sync_menu): from calibre.gui2.device import DeviceAction @@ -174,7 +168,7 @@ class ConnectShareAction(InterfaceAction): def genesis(self): self.share_conn_menu = ShareConnMenu(self.gui) self.share_conn_menu.toggle_server.connect(self.toggle_content_server) - self.share_conn_menu.toggle_smartdevice.connect(self.toggle_smartdevice) + self.share_conn_menu.control_smartdevice.connect(self.control_smartdevice) self.share_conn_menu.config_email.connect(partial( self.gui.iactions['Preferences'].do_config, initial_plugin=('Sharing', 'Email'))) @@ -220,14 +214,9 @@ class ConnectShareAction(InterfaceAction): self.gui.content_server = None self.stopping_msg.accept() - def toggle_smartdevice(self): + def control_smartdevice(self): sd_dialog = SmartdeviceDialog(self.gui) sd_dialog.exec_() - self.share_conn_menu.smartdevice_state_changed( - self.gui.device_manager.is_running('smartdevice')) - - def smartdevice_state_changed(self, running): - self.share_conn_menu.smartdevice_state_changed(running) def check_smartdevice_menus(self): if not self.gui.device_manager.is_enabled('smartdevice'): diff --git a/src/calibre/gui2/dialogs/smartdevice.py b/src/calibre/gui2/dialogs/smartdevice.py index 63c49a5fc7..15b40d1077 100644 --- a/src/calibre/gui2/dialogs/smartdevice.py +++ b/src/calibre/gui2/dialogs/smartdevice.py @@ -22,47 +22,45 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): 'answer yes. If you do not, the app will not work. It will ' 'be unable to connect to calibre.')) - self.passwd_msg.setText( + self.password_box.setToolTip('

' + _('Use a password if calibre is running on a network that ' 'is not secure. For example, if you run calibre on a laptop, ' 'use that laptop in an airport, and want to connect your ' - 'smart device to calibre, you should use a password.')) + 'smart device to calibre, you should use a password.') + '

') - self.auto_start_msg.setText( + self.run_box.setToolTip('

' + + _('Check this box to allow calibre to accept connections from the ' + 'smart device. Uncheck the box to prevent connections.') + '

') + + self.autostart_box.setToolTip('

' + _('Check this box if you want calibre to automatically start the ' 'smart device interface when calibre starts. You should not do ' 'this if you are using a network that is not secure and you ' - 'are not setting a password.')) + 'are not setting a password.') + '

') self.connect(self.show_password, SIGNAL('stateChanged(int)'), self.toggle_password) + self.autostart_box.stateChanged.connect(self.autostart_changed) self.device_manager = parent.device_manager + if self.device_manager.is_running('smartdevice'): + self.run_box.setChecked(True) + else: + self.run_box.setChecked(False) + if self.device_manager.get_option('smartdevice', 'autostart'): self.autostart_box.setChecked(True) + self.run_box.setChecked(True) + self.run_box.setEnabled(False) + pw = self.device_manager.get_option('smartdevice', 'password') if pw: self.password_box.setText(pw) - if self.device_manager.is_running('smartdevice'): - self.start_button.setEnabled(False) - self.stop_button.setEnabled(True) + def autostart_changed(self): + if self.autostart_box.isChecked(): + self.run_box.setChecked(True) + self.run_box.setEnabled(False) else: - self.start_button.setEnabled(True) - self.stop_button.setEnabled(False) - self.start_button.clicked.connect(self.start_button_clicked) - self.stop_button.clicked.connect(self.stop_button_clicked) - self.cancel_button.clicked.connect(self.cancel_button_clicked) - self.OK_button.clicked.connect(self.accept) - - def start_button_clicked(self): - self.device_manager.start_plugin('smartdevice') - self.accept() - - def stop_button_clicked(self): - self.device_manager.stop_plugin('smartdevice') - self.accept() - - def cancel_button_clicked(self): - QDialog.reject(self) + self.run_box.setEnabled(True) def toggle_password(self, state): if state == Qt.Unchecked: @@ -75,4 +73,9 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): unicode(self.password_box.text())) self.device_manager.set_option('smartdevice', 'autostart', self.autostart_box.isChecked()) + if self.run_box.isChecked(): + self.device_manager.start_plugin('smartdevice') + else: + self.device_manager.stop_plugin('smartdevice') + QDialog.accept(self) diff --git a/src/calibre/gui2/dialogs/smartdevice.ui b/src/calibre/gui2/dialogs/smartdevice.ui index 26795249be..97b4b71c00 100644 --- a/src/calibre/gui2/dialogs/smartdevice.ui +++ b/src/calibre/gui2/dialogs/smartdevice.ui @@ -18,7 +18,7 @@ :/images/mimetypes/unknown.png:/images/mimetypes/unknown.png - + TextLabel @@ -43,16 +43,6 @@ QLineEdit::Password - - - - - - TextLabel - - - true - 100 @@ -69,61 +59,77 @@ + + + &Allow connections + + + + - &Auto-start + &Automatically allow connections at startup - - - - true + + + + + 0 + 100 + - - - - All the buttons except Cancel will save the above settings + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - Start interface - - - - - - - Stop interface - - - - - - - OK - - - - - - - Cancel - - - - - + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index be86e91c29..8a7dfa1153 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -342,7 +342,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ if self.device_manager.get_option('smartdevice', 'autostart'): try: self.device_manager.start_plugin('smartdevice') - smartdevice_actions.smartdevice_state_changed(True) except: pass From 26d010d31580e7397b45005fa171dfa249332dd9 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 24 Jul 2012 19:23:45 +0200 Subject: [PATCH 4/9] Improved thread handling in device_manager dynamic plugin methods. Improved smartdevice dialog box. --- src/calibre/devices/interface.py | 19 ++++++-- src/calibre/gui2/device.py | 78 ++++++++++++++++---------------- 2 files changed, 55 insertions(+), 42 deletions(-) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 0f2027065e..1466732169 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -515,12 +515,15 @@ class DevicePlugin(Plugin): pass # Dynamic control interface + # All of these methods are called on the device_manager thread def is_dynamically_controllable(self): ''' Called by the device manager when starting plugins. If this method returns a string, then a) it supports the device manager's dynamic control - interface, and b) that name is to be used when talking to the plugin + interface, and b) that name is to be used when talking to the plugin. + + This method must be called from the device_manager thread. ''' return None @@ -529,6 +532,8 @@ class DevicePlugin(Plugin): This method is called to start the plugin. The plugin should begin to accept device connections however it does that. If the plugin is already accepting connections, then do nothing. + + This method must be called from the device_manager thread. ''' pass @@ -538,27 +543,35 @@ class DevicePlugin(Plugin): accept connections, and should cleanup behind itself. It is likely that this method should call shutdown. If the plugin is already not accepting connections, then do nothing. + + This method must be called from the device_manager thread. ''' pass - def get_option(self, opt_string): + def get_option(self, opt_string, default=None): ''' Return the value of the option indicated by opt_string. This method can be called when the plugin is not started. Return None if the option does not exist. + + This method must be called from the device_manager thread. ''' - return None + return default def set_option(self, opt_string, opt_value): ''' Set the value of the option indicated by opt_string. This method can be called when the plugin is not started. + + This method must be called from the device_manager thread. ''' pass def is_running(self): ''' Return True if the plugin is started, otherwise false + + This method must be called from the device_manager thread. ''' return False diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 14a9093bd4..63d7a03220 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal ' # Imports {{{ import os, traceback, Queue, time, cStringIO, re, sys -from threading import Thread +from threading import Thread, Event from PyQt4.Qt import (QMenu, QAction, QActionGroup, QIcon, SIGNAL, Qt, pyqtSignal, QDialog, QObject, QVBoxLayout, @@ -30,6 +30,7 @@ from calibre.constants import DEBUG from calibre.utils.config import prefs, tweaks from calibre.utils.magick.draw import thumbnail from calibre.library.save_to_disk import find_plugboard +from calibre.gui2 import is_gui_thread # }}} class DeviceJob(BaseJob): # {{{ @@ -145,8 +146,10 @@ class DeviceManager(Thread): # {{{ self._device_information = None self.current_library_uuid = None self.call_shutdown_on_disconnect = False - self.devices_initialized = Queue.Queue(0) + self.devices_initialized = Event() self.dynamic_plugins = {} + self.dynamic_plugin_requests = Queue.Queue(0) + self.dynamic_plugin_responses = Queue.Queue(0) def report_progress(self, *args): pass @@ -291,7 +294,7 @@ class DeviceManager(Thread): # {{{ n = d.is_dynamically_controllable() if n: self.dynamic_plugins[n] = d - self.devices_initialized.put(None) + self.devices_initialized.set() while self.keep_going: kls = None @@ -315,6 +318,7 @@ class DeviceManager(Thread): # {{{ traceback.print_exc() else: self.detect_device() + while True: job = self.next() if job is not None: @@ -325,8 +329,15 @@ class DeviceManager(Thread): # {{{ self.current_job = None else: break - time.sleep(self.sleep_time) - + while True: + dynamic_method = None + try: + (dynamic_method, args, kwargs) = \ + self.dynamic_plugin_requests.get(self.sleep_time) + res = dynamic_method(*args, **kwargs) + self.dynamic_plugin_responses.put(res) + except Queue.Empty: + break # We are exiting. Call the shutdown method for each plugin for p in self.devices: try: @@ -516,47 +527,36 @@ class DeviceManager(Thread): # {{{ # dynamic plugin interface - def start_plugin(self, name): + # This is a helper function that handles queueing with the device manager + def _queue_request(self, name, method, *args, **kwargs): + if not is_gui_thread(): + raise ValueError( + 'The device_manager dynamic plugin methods must be called from the GUI thread') try: d = self.dynamic_plugins.get(name, None) - if d: - d.start_plugin() + self.dynamic_plugin_requests.put((getattr(d, method), args, kwargs)) + return self.dynamic_plugin_responses.get() except: - pass - - def stop_plugin(self, name): - try: - d = self.dynamic_plugins.get(name, None) - if d: - d.stop_plugin() - except: - pass - - def get_option(self, name, opt_string): - try: - d = self.dynamic_plugins.get(name, None) - if d: - return d.get_option(opt_string) - except: - pass + traceback.print_exc() return None + # The dynamic plugin methods below must be called on the GUI thread. They + # will switch to the device thread before calling the plugin. + + def start_plugin(self, name): + self._queue_request(name, 'start_plugin') + + def stop_plugin(self, name): + self._queue_request(name, 'stop_plugin') + + def get_option(self, name, opt_string, default=None): + return self._queue_request(name, 'get_option', opt_string, default=default) + def set_option(self, name, opt_string, opt_value): - try: - d = self.dynamic_plugins.get(name, None) - if d: - d.set_option(opt_string, opt_value) - except: - pass + self._queue_request(name, 'set_option', opt_string, opt_value) def is_running(self, name): - try: - d = self.dynamic_plugins.get(name, None) - if d: - return d.is_running() - except: - pass - return False + return self._queue_request(name, 'is_running') def is_enabled(self, name): try: @@ -767,7 +767,7 @@ class DeviceMixin(object): # {{{ self.job_manager, Dispatcher(self.status_bar.show_message), Dispatcher(self.show_open_feedback)) self.device_manager.start() - self.device_manager.devices_initialized.get() + self.device_manager.devices_initialized.wait() if tweaks['auto_connect_to_folder']: self.connect_to_folder_named(tweaks['auto_connect_to_folder']) From 9d28e7a366e4538b57420dbb7224a518691f0032 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 24 Jul 2012 20:50:58 +0200 Subject: [PATCH 5/9] Make the menu icon for the smartdevice control change according to whether or not it is running. --- resources/images/dot_green.png | Bin 0 -> 1526 bytes resources/images/dot_red.png | Bin 0 -> 1450 bytes src/calibre/gui2/actions/device.py | 10 +++++++++- src/calibre/gui2/ui.py | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 resources/images/dot_green.png create mode 100644 resources/images/dot_red.png diff --git a/resources/images/dot_green.png b/resources/images/dot_green.png new file mode 100644 index 0000000000000000000000000000000000000000..c05376d7a78c5b5b0eb7e8155fac75d829445444 GIT binary patch literal 1526 zcmV*tc)l1%y>hAOt^11;26xB{c;^8L2dN#!<%<8xza2$uHEBN~N$SM@!O4 zB}dH}G_7Pb6eKGV91|7P`~qd=dqv#u_jcdwyhn>L?lV7jrvB(MXV2r@d(ZjZbMCwM z5uzxVPFf!>2>9uEW9UY62d`DVRVM9j#6XHPEClsHo0Bqg<;mhSODh z5TtoM`dZq~m#kM!PyqPwCq~xOVpCyn$#&_V4dMF1Y4QE$flFur&Q%YBU`Mmkz9E1l zSzxHHK#@UEcyTLK8P7_YScE<~^eK;8{SxOHVD-UPfa!6mu;=9V&C#Kw(_(w3fWv73 zUTAqp;0Hleq(7+XP$;>62+9pb+30ft`fOe3LP?Rr zxMQQREDYTZj+O6%TgFT4(Il5+&8rFkD{|G6it$nVmQ0SF40TLN$A~^-XGeDHk2P<6jI=A4gQ2>>mD&sN+vTGvv=Fu$j1-I843mDTBL9p~20|!2VOaz-+(Q8O%N^MFYu@eb*X^Mm0p@MdIA$eG zWLSp5)CAh-1mo7y8l>Q@@a?4(y1ki&Z)K>E$e|AE{7rUJlg8+{DZ z6UK+Ij0!iQ%c}q+L16}X*i&%i_P1|aJ(pm{7e4tZ!{f#V>-Biq@J|#-snMhgYK%rW zRDAO2?su(89s#C(=Cl0y$jHo?zJ2frwtE$Tp;;)sbQ%7*awUE5VyoxZaO$Vp5S@lK z%uY_iBbdW1IK8xb8joZKat`N%*=7&TU1n+fVCeubX`NP5o8yNJOpA^P0~==rZyd!M z#s|tPZ$aUibJEqk;J^yY{oeKfFMlip^w+ZN`P7s|>~(>If4WkrRV=Q7f*pAUxO&?F zG+p@deFcD&44wNivqx~(<&(z@!%rguEo;olN=Vilv~Q$eqev3(d`4b1jr#1LHzB{|Ja85!C!qSpOhFLtu`FwzUwwS~W1b{3vS->wrZ4< zxfdrALZCqK-K2S>sH!DIs+s2einZ48Ul?nb1`O&%;ao14>5(%a0Tit_x9x%2+FF&L zpC655D9i{!(=_RHI%#xDGYmtZCfsu=ijo|m$;IOsL8H+KI7h0H%WO9D?maMiiB3G8 zb^jyx0RVTCKqM`}xNQ{`6+~lWBM})HDchQxo7?&V0s_R~;9wEQh*42dZBB8%{451_ c+u8&C1?524-}?^}>;M1&07*qoM6N<$f}CvUw*UYD literal 0 HcmV?d00001 diff --git a/resources/images/dot_red.png b/resources/images/dot_red.png new file mode 100644 index 0000000000000000000000000000000000000000..88df5ccf1538f96e29aba049ed95279a9a3dcb67 GIT binary patch literal 1450 zcmV;b1y%ZqP)~6Q+UbaDKZ8eme6seSJDuRSSB>D$z6Uv3*%`|ZFjyF% z5v7%~iLY_>>wv3W1r%=sro1|{sZ5T6mW)B_?f|5IzX4iII*IBND82cdH539E*ggxe z>QXFMca*Z)4~fltfUjEvT0#O{3V|jC!BCZ1Co}_;=mt`B0VRl#9O?(D>nv#T=rQ#5 z&V&3f0Jt@0uo?|;ZNcUH?6SI6SIYr(CV>{3%*;@zZwLY?s|b{v2G$?eBXeF!pxu=~*{|WSY zFxP)xfE(Ma+2v8z<2~fs{5zUxSMxo>Zu0C7ZrRSqO>a0H^HtSK$lZZsJgcPashzrvjCrn%j;gYJ^2*geCpBD z{-AQWJP^C{D@Y?lr%?0ChC%?>wpdqC7IwIF)q0>DrTB9SFD>}oplnXiA^`{t^nsR^ zYc^hq|Ctw{XN&bP?{v01pI8fqrsD035+9@BA4Nm>x0{d*hufO2#NW#c(7oAu!Qpnd z*h~O9Rix;1fmjY=2oMPdFK)UTe>N|`)n;L=w8G=%mahauk&6q!u#g(L4S|U%-{!7F zbzXqW&4OHB>9JUBT(p$R{Y+iuQO@i$8dMt)!6n^a%odN>; zfB>IwD!rq-0Kmmf!dP{AnU}!sGqmDnfS^e9O2F9Uw6CSRKnZ?$MmSed>UfT2XwVh4 zr~oATl+ouzAaZ`^&xu`m0nRn?hn=Fey~Hknno1WHfaWYP6HP!^ins0RN#_3UG0%~$;rJ#8WkQN61@XeUFUxTV! zdkX=4^E97bw%Y~E;m0Z+phT0Iax7+;X=m&_{N;~8G?6yz-e&6e{Zatn>kVA)`48O= z;SFz@9h6uSn^J!Cau8)@B<`)2bg#ZV7W#Y(@BgFXl zIJK+q(+^6l%m7}( z@j@Yh4g~1m*G2||L1toN!u&XJ>>IuCu2WCGT8-r>75Pj#%)ZdDKeCUN`N&XMXS3dB z+5=J&#xU(6N;viHvYqX^u1g%pMMp zSs~qPs0|PA_Nb8-mqUOOCkG5o0mssrb17K`Mb%&?oPtm!0h6@j$Ggs&^CRN2I~Y8O zJ|=JuplJ-VpcDl70s#j0FU$Z*k|NGL&U8i0@2Lq7Z*$60Gi7Kcw60c~=TwsRYXlVv zv5xK$SM8;7!83(HRg|=*Y0*c{fC=zY-~Ar&`~8e4iZt3#m=S`eX;M{H8lBP>i-o|M znD2OsG995xQ_x1|d0xjeW{tF1ET&|8;PdsY@nUa*9xws2!vryB6DF%092_L3r>BYf z`udD66bjAnxm+%zs;bIB8>6A2Vcsd8&#cYBSzRu`-&1Jc-`{=mrT_o{07*qoM6N<$ Ef-$PXS^xk5 literal 0 HcmV?d00001 diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index 8d08f53f0a..92ed77e324 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -59,7 +59,7 @@ class ShareConnMenu(QMenu): # {{{ self.toggle_server_action.triggered.connect(lambda x: self.toggle_server.emit()) self.control_smartdevice_action = \ - self.addAction(QIcon(I('devices/galaxy_s3.png')), + self.addAction(QIcon(I('dot_green.png')), _('Control Smart Device Connections')) self.control_smartdevice_action.triggered.connect(lambda x: self.control_smartdevice.emit()) @@ -217,7 +217,15 @@ class ConnectShareAction(InterfaceAction): def control_smartdevice(self): sd_dialog = SmartdeviceDialog(self.gui) sd_dialog.exec_() + self.set_smartdevice_icon() def check_smartdevice_menus(self): if not self.gui.device_manager.is_enabled('smartdevice'): self.share_conn_menu.hide_smartdevice_menus() + + def set_smartdevice_icon(self): + running = self.gui.device_manager.is_running('smartdevice') + if running: + self.share_conn_menu.control_smartdevice_action.setIcon(QIcon(I('dot_green.png'))) + else: + self.share_conn_menu.control_smartdevice_action.setIcon(QIcon(I('dot_red.png'))) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 8a7dfa1153..b414ef04dd 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -344,6 +344,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ self.device_manager.start_plugin('smartdevice') except: pass + smartdevice_actions.set_smartdevice_icon() self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) From 40a273b316db7a719699af594b158975c6cc6834 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 24 Jul 2012 21:12:04 +0200 Subject: [PATCH 6/9] Fix sleep time parameter of dynamic plugin queue.get --- src/calibre/gui2/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 63d7a03220..75ec2f9a12 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -333,7 +333,7 @@ class DeviceManager(Thread): # {{{ dynamic_method = None try: (dynamic_method, args, kwargs) = \ - self.dynamic_plugin_requests.get(self.sleep_time) + self.dynamic_plugin_requests.get(timeout=self.sleep_time) res = dynamic_method(*args, **kwargs) self.dynamic_plugin_responses.put(res) except Queue.Empty: From bd6acc80c6e9769155e84f9cc809006a5809c688 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 25 Jul 2012 07:28:11 +0200 Subject: [PATCH 7/9] Eliminate spurious exception message in dynamic control interface --- src/calibre/gui2/device.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 75ec2f9a12..8364e06f0f 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -534,8 +534,9 @@ class DeviceManager(Thread): # {{{ 'The device_manager dynamic plugin methods must be called from the GUI thread') try: d = self.dynamic_plugins.get(name, None) - self.dynamic_plugin_requests.put((getattr(d, method), args, kwargs)) - return self.dynamic_plugin_responses.get() + if d: + self.dynamic_plugin_requests.put((getattr(d, method), args, kwargs)) + return self.dynamic_plugin_responses.get() except: traceback.print_exc() return None @@ -556,7 +557,9 @@ class DeviceManager(Thread): # {{{ self._queue_request(name, 'set_option', opt_string, opt_value) def is_running(self, name): - return self._queue_request(name, 'is_running') + if self._queue_request(name, 'is_running'): + return True + return False def is_enabled(self, name): try: From f1a0e3ccb1709f792ee219ba6d46d9ec3dd289db Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 25 Jul 2012 08:23:19 +0200 Subject: [PATCH 8/9] Fix searching with localized strings that contain capital letters. --- src/calibre/library/caches.py | 50 ++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index e9bb6286f3..a516681fab 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -352,6 +352,14 @@ class ResultCache(SearchQueryParser): # {{{ '<=':[2, relop_le] } + local_today = ('_today', icu_lower(_('today'))) + local_yesterday = ('_yesterday', icu_lower(_('yesterday'))) + local_thismonth = ('_thismonth', icu_lower(_('thismonth'))) + local_daysago = icu_lower(_('daysago')) + local_daysago_len = len(local_daysago) + untrans_daysago = '_daysago' + untrans_daysago_len = len('_daysago') + def get_dates_matches(self, location, query, candidates): matches = set([]) if len(query) < 2: @@ -390,17 +398,24 @@ class ResultCache(SearchQueryParser): # {{{ if relop is None: (p, relop) = self.date_search_relops['='] - if query == _('today'): + if query in self.local_today: qd = now() field_count = 3 - elif query == _('yesterday'): + elif query in self.local_yesterday: qd = now() - timedelta(1) field_count = 3 - elif query == _('thismonth'): + elif query in self.local_thismonth: qd = now() field_count = 2 - elif query.endswith(_('daysago')): - num = query[0:-len(_('daysago'))] + elif query.endswith(self.local_daysago): + num = query[0:-self.local_daysago_len] + try: + qd = now() - timedelta(int(num)) + except: + raise ParseException(query, len(query), 'Number conversion error', self) + field_count = 3 + elif query.endswith(self.untrans_daysago): + num = query[0:-self.untrans_daysago_len] try: qd = now() - timedelta(int(num)) except: @@ -591,14 +606,23 @@ class ResultCache(SearchQueryParser): # {{{ query = icu_lower(query) return matchkind, query + local_no = icu_lower(_('no')) + local_yes = icu_lower(_('yes')) + local_unchecked = icu_lower(_('unchecked')) + local_checked = icu_lower(_('checked')) + local_empty = icu_lower(_('empty')) + local_blank = icu_lower(_('blank')) + local_bool_values = ( + local_no, local_unchecked, '_no', 'false', + local_yes, local_checked, '_yes', 'true', + local_empty, local_blank, '_empty') + def get_bool_matches(self, location, query, candidates): bools_are_tristate = self.db_prefs.get('bools_are_tristate') loc = self.field_metadata[location]['rec_index'] matches = set() query = icu_lower(query) - if query not in (_('no'), _('unchecked'), '_no', 'false', - _('yes'), _('checked'), '_yes', 'true', - _('empty'), _('blank'), '_empty'): + if query not in self.local_bool_values: raise ParseException(_('Invalid boolean query "{0}"').format(query)) for id_ in candidates: item = self._data[id_] @@ -608,20 +632,20 @@ class ResultCache(SearchQueryParser): # {{{ val = force_to_bool(item[loc]) if not bools_are_tristate: if val is None or not val: # item is None or set to false - if query in [_('no'), _('unchecked'), '_no', 'false']: + if query in (self.local_no, self.local_unchecked, '_no', 'false'): matches.add(item[0]) else: # item is explicitly set to true - if query in [_('yes'), _('checked'), '_yes', 'true']: + if query in (self.local_yes, self.local_checked, '_yes', 'true'): matches.add(item[0]) else: if val is None: - if query in [_('empty'), _('blank'), '_empty', 'false']: + if query in (self.local_empty, self.local_blank, '_empty', 'false'): matches.add(item[0]) elif not val: # is not None and false - if query in [_('no'), _('unchecked'), '_no', 'true']: + if query in (self.local_no, self.local_unchecked, '_no', 'true'): matches.add(item[0]) else: # item is not None and true - if query in [_('yes'), _('checked'), '_yes', 'true']: + if query in (self.local_yes, self.local_checked, '_yes', 'true'): matches.add(item[0]) return matches From abbc24e9c1e987229f591a722bc8e356e00ff755 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 25 Jul 2012 08:58:08 +0200 Subject: [PATCH 9/9] Properly handle the default argument in get_option when the requested plugin does not exist. --- src/calibre/gui2/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 8364e06f0f..f08f87bf80 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -539,7 +539,7 @@ class DeviceManager(Thread): # {{{ return self.dynamic_plugin_responses.get() except: traceback.print_exc() - return None + return kwargs.get('default', None) # The dynamic plugin methods below must be called on the GUI thread. They # will switch to the device thread before calling the plugin.