mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
More smart device plumbing
This commit is contained in:
commit
859d8c6f50
BIN
resources/images/dot_green.png
Normal file
BIN
resources/images/dot_green.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
resources/images/dot_red.png
Normal file
BIN
resources/images/dot_red.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -514,6 +514,67 @@ 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.
|
||||
|
||||
This method must be called from the device_manager thread.
|
||||
'''
|
||||
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.
|
||||
|
||||
This method must be called from the device_manager thread.
|
||||
'''
|
||||
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.
|
||||
|
||||
This method must be called from the device_manager thread.
|
||||
'''
|
||||
pass
|
||||
|
||||
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 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
|
||||
|
||||
class BookList(list):
|
||||
'''
|
||||
A list of books. Each Book object must have the fields
|
||||
|
@ -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): # {{{
|
||||
@ -24,6 +25,7 @@ class ShareConnMenu(QMenu): # {{{
|
||||
|
||||
config_email = pyqtSignal()
|
||||
toggle_server = pyqtSignal()
|
||||
control_smartdevice = pyqtSignal()
|
||||
dont_add_to = frozenset(['context-menu-device'])
|
||||
|
||||
def __init__(self, parent=None):
|
||||
@ -56,6 +58,11 @@ class ShareConnMenu(QMenu): # {{{
|
||||
_('Start Content Server'))
|
||||
self.toggle_server_action.triggered.connect(lambda x:
|
||||
self.toggle_server.emit())
|
||||
self.control_smartdevice_action = \
|
||||
self.addAction(QIcon(I('dot_green.png')),
|
||||
_('Control Smart Device Connections'))
|
||||
self.control_smartdevice_action.triggered.connect(lambda x:
|
||||
self.control_smartdevice.emit())
|
||||
self.addSeparator()
|
||||
|
||||
self.email_actions = []
|
||||
@ -80,6 +87,9 @@ class ShareConnMenu(QMenu): # {{{
|
||||
text = _('Stop Content Server') + ' [%s]'%get_external_ip()
|
||||
self.toggle_server_action.setText(text)
|
||||
|
||||
def hide_smartdevice_menus(self):
|
||||
self.control_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 +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.control_smartdevice.connect(self.control_smartdevice)
|
||||
self.share_conn_menu.config_email.connect(partial(
|
||||
self.gui.iactions['Preferences'].do_config,
|
||||
initial_plugin=('Sharing', 'Email')))
|
||||
@ -200,8 +211,21 @@ class ConnectShareAction(InterfaceAction):
|
||||
if not self.stopping_msg.isVisible():
|
||||
self.stopping_msg.exec_()
|
||||
return
|
||||
|
||||
|
||||
self.gui.content_server = None
|
||||
self.stopping_msg.accept()
|
||||
|
||||
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')))
|
||||
|
@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
# 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,6 +146,10 @@ class DeviceManager(Thread): # {{{
|
||||
self._device_information = None
|
||||
self.current_library_uuid = None
|
||||
self.call_shutdown_on_disconnect = False
|
||||
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
|
||||
@ -286,6 +291,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.set()
|
||||
|
||||
while self.keep_going:
|
||||
kls = None
|
||||
@ -309,6 +318,7 @@ class DeviceManager(Thread): # {{{
|
||||
traceback.print_exc()
|
||||
else:
|
||||
self.detect_device()
|
||||
|
||||
while True:
|
||||
job = self.next()
|
||||
if job is not None:
|
||||
@ -319,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(timeout=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:
|
||||
@ -508,6 +525,51 @@ class DeviceManager(Thread): # {{{
|
||||
if self.connected_device:
|
||||
self.connected_device.set_driveinfo_name(location_code, name)
|
||||
|
||||
# dynamic plugin interface
|
||||
|
||||
# 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:
|
||||
self.dynamic_plugin_requests.put((getattr(d, method), args, kwargs))
|
||||
return self.dynamic_plugin_responses.get()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
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.
|
||||
|
||||
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):
|
||||
self._queue_request(name, 'set_option', opt_string, opt_value)
|
||||
|
||||
def is_running(self, name):
|
||||
if self._queue_request(name, 'is_running'):
|
||||
return True
|
||||
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 +770,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.wait()
|
||||
if tweaks['auto_connect_to_folder']:
|
||||
self.connect_to_folder_named(tweaks['auto_connect_to_folder'])
|
||||
|
||||
|
81
src/calibre/gui2/dialogs/smartdevice.py
Normal file
81
src/calibre/gui2/dialogs/smartdevice.py
Normal file
@ -0,0 +1,81 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
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. <B>Please '
|
||||
'answer yes</b>. If you do not, the app will not work. It will '
|
||||
'be unable to connect to calibre.'))
|
||||
|
||||
self.password_box.setToolTip('<p>' +
|
||||
_('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.') + '</p>')
|
||||
|
||||
self.run_box.setToolTip('<p>' +
|
||||
_('Check this box to allow calibre to accept connections from the '
|
||||
'smart device. Uncheck the box to prevent connections.') + '</p>')
|
||||
|
||||
self.autostart_box.setToolTip('<p>' +
|
||||
_('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.') + '</p>')
|
||||
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)
|
||||
|
||||
def autostart_changed(self):
|
||||
if self.autostart_box.isChecked():
|
||||
self.run_box.setChecked(True)
|
||||
self.run_box.setEnabled(False)
|
||||
else:
|
||||
self.run_box.setEnabled(True)
|
||||
|
||||
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())
|
||||
if self.run_box.isChecked():
|
||||
self.device_manager.start_plugin('smartdevice')
|
||||
else:
|
||||
self.device_manager.stop_plugin('smartdevice')
|
||||
|
||||
QDialog.accept(self)
|
137
src/calibre/gui2/dialogs/smartdevice.ui
Normal file
137
src/calibre/gui2/dialogs/smartdevice.ui
Normal file
@ -0,0 +1,137 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>600</width>
|
||||
<height>209</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Smart device control</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/mimetypes/unknown.png</normaloff>:/images/mimetypes/unknown.png</iconset>
|
||||
</property>
|
||||
<layout class="QGridLayout">
|
||||
<item row="4" column="1">
|
||||
<widget class="QCheckBox" name="autostart_box">
|
||||
<property name="text">
|
||||
<string>&Automatically allow connections at startup</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QLabel" name="label_43">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>100</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="password_box">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>100</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="echoMode">
|
||||
<enum>QLineEdit::Password</enum>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>Optional password for security</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QCheckBox" name="run_box">
|
||||
<property name="text">
|
||||
<string>&Allow connections</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QLabel" name="msg">
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>&Password:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>password_box</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QCheckBox" name="show_password">
|
||||
<property name="text">
|
||||
<string>&Show password</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0" colspan="3">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -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')
|
||||
except:
|
||||
pass
|
||||
smartdevice_actions.set_smartdevice_icon()
|
||||
|
||||
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
|
||||
|
||||
self.read_settings()
|
||||
|
@ -955,11 +955,16 @@ class Reaper(threading.Thread):
|
||||
return
|
||||
if globals()['_GLOBAL_DONE']:
|
||||
return
|
||||
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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user