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):