Wireless device driver: Make detecting and connecting to devices easier on networks where mdns is disabled (many home routers disable it)

This commit is contained in:
Kovid Goyal 2012-08-29 19:22:55 +05:30
commit 168ea69325
4 changed files with 216 additions and 83 deletions

View File

@ -8,6 +8,7 @@ Created on 29 Jun 2012
@author: charles @author: charles
''' '''
import socket, select, json, inspect, os, traceback, time, sys, random import socket, select, json, inspect, os, traceback, time, sys, random
import posixpath
import hashlib, threading import hashlib, threading
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from functools import wraps from functools import wraps
@ -26,6 +27,7 @@ from calibre.ebooks.metadata import title_sort
from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.metadata.book.json_codec import JsonCodec from calibre.ebooks.metadata.book.json_codec import JsonCodec
from calibre.library import current_library_name from calibre.library import current_library_name
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.ipc import eintr_retry_call from calibre.utils.ipc import eintr_retry_call
from calibre.utils.config import from_json, tweaks from calibre.utils.config import from_json, tweaks
from calibre.utils.date import isoformat, now from calibre.utils.date import isoformat, now
@ -49,6 +51,12 @@ def do_zeroconf(f, port):
'_calibresmartdeviceapp._tcp', port, {}) '_calibresmartdeviceapp._tcp', port, {})
class SDBook(Book):
def __init__(self, prefix, lpath, size=None, other=None):
Book.__init__(self, prefix, lpath, size=size, other=other)
path = getattr(self, 'path', lpath)
self.path = path.replace('\\', '/')
class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
name = 'SmartDevice App Interface' name = 'SmartDevice App Interface'
gui_name = _('SmartDevice') gui_name = _('SmartDevice')
@ -70,14 +78,15 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
DEVICE_PLUGBOARD_NAME = 'SMART_DEVICE_APP' DEVICE_PLUGBOARD_NAME = 'SMART_DEVICE_APP'
CAN_SET_METADATA = [] CAN_SET_METADATA = []
CAN_DO_DEVICE_DB_PLUGBOARD = False CAN_DO_DEVICE_DB_PLUGBOARD = False
SUPPORTS_SUB_DIRS = False SUPPORTS_SUB_DIRS = True
MUST_READ_METADATA = True MUST_READ_METADATA = True
NEWS_IN_FOLDER = False NEWS_IN_FOLDER = False
SUPPORTS_USE_AUTHOR_SORT = False SUPPORTS_USE_AUTHOR_SORT = False
WANTS_UPDATED_THUMBNAILS = True WANTS_UPDATED_THUMBNAILS = True
MAX_PATH_LEN = 100 MAX_PATH_LEN = 250
THUMBNAIL_HEIGHT = 160 THUMBNAIL_HEIGHT = 160
PREFIX = '' PREFIX = ''
BACKLOADING_ERROR_MESSAGE = None
# Some network protocol constants # Some network protocol constants
BASE_PACKET_LEN = 4096 BASE_PACKET_LEN = 4096
@ -88,6 +97,16 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
SEND_NOOP_EVERY_NTH_PROBE = 5 SEND_NOOP_EVERY_NTH_PROBE = 5
DISCONNECT_AFTER_N_SECONDS = 30*60 # 30 minutes DISCONNECT_AFTER_N_SECONDS = 30*60 # 30 minutes
ZEROCONF_CLIENT_STRING = b'calibre smart device client'
# A few "random" port numbers to use for detecting clients using broadcast
# The clients are expected to broadcast a UDP 'hi there' on all of these
# ports when they attempt to connect. Calibre will respond with the port
# number the client should use. This scheme backs up mdns. And yes, we
# must hope that no other application on the machine is using one of these
# ports in datagram mode.
# If you change the ports here, all clients will also need to change.
BROADCAST_PORTS = [54982, 48123, 39001, 44044, 59678]
opcodes = { opcodes = {
'NOOP' : 12, 'NOOP' : 12,
@ -196,25 +215,6 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
print() print()
self.debug_time = time.time() self.debug_time = time.time()
# Various methods required by the plugin architecture
@classmethod
def _default_save_template(cls):
from calibre.library.save_to_disk import config
st = cls.SAVE_TEMPLATE if cls.SAVE_TEMPLATE else \
config().parse().send_template
if st:
st = os.path.basename(st)
return st
@classmethod
def save_template(cls):
st = cls.settings().save_template
if st:
st = os.path.basename(st)
else:
st = cls._default_save_template()
return st
# local utilities # local utilities
# copied from USBMS. Perhaps this could be a classmethod in usbms? # copied from USBMS. Perhaps this could be a classmethod in usbms?
@ -276,6 +276,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
extra_components.append(sanitize(fname)) extra_components.append(sanitize(fname))
else: else:
extra_components[-1] = sanitize(extra_components[-1]+ext) extra_components[-1] = sanitize(extra_components[-1]+ext)
self._debug('1', extra_components)
if extra_components[-1] and extra_components[-1][0] in ('.', '_'): if extra_components[-1] and extra_components[-1][0] in ('.', '_'):
extra_components[-1] = 'x' + extra_components[-1][1:] extra_components[-1] = 'x' + extra_components[-1][1:]
@ -308,7 +309,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
extra_components = list(map(remove_trailing_periods, extra_components)) extra_components = list(map(remove_trailing_periods, extra_components))
components = shorten_components_to(maxlen, extra_components) components = shorten_components_to(maxlen, extra_components)
filepath = os.path.join(*components) filepath = posixpath.join(*components)
return filepath return filepath
def _strip_prefix(self, path): def _strip_prefix(self, path):
@ -525,19 +526,27 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self.device_socket = None self.device_socket = None
self.is_connected = False self.is_connected = False
def _attach_to_port(self, port): def _attach_to_port(self, sock, port):
try: try:
self._debug('try port', port) self._debug('try port', port)
self.listen_socket.bind(('', port)) sock.bind(('', port))
except socket.error: except socket.error:
self._debug('socket error on port', port) self._debug('socket error on port', port)
port = 0 port = 0
except: except:
self._debug('Unknown exception while allocating listen socket') self._debug('Unknown exception while attaching port to socket')
traceback.print_exc() traceback.print_exc()
raise raise
return port return port
def _close_listen_socket(self):
self.listen_socket.close()
self.listen_socket = None
self.is_connected = False
if getattr(self, 'broadcast_socket', None) is not None:
self.broadcast_socket.close()
self.broadcast_socket = None
# The public interface methods. # The public interface methods.
@synchronous('sync_lock') @synchronous('sync_lock')
@ -569,6 +578,23 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
except: except:
self._close_device_socket() self._close_device_socket()
return (self.is_connected, self) return (self.is_connected, self)
if getattr(self, 'broadcast_socket', None) is not None:
while True:
ans = select.select((self.broadcast_socket,), (), (), 0)
if len(ans[0]) > 0:
try:
packet = self.broadcast_socket.recvfrom(100)
remote = packet[1]
message = str(self.ZEROCONF_CLIENT_STRING + b' (on ' +
str(socket.gethostname().partition('.')[0]) +
b'),' + str(self.port))
self._debug('received broadcast', packet, message)
self.broadcast_socket.sendto(message, remote)
except:
pass
else:
break
if getattr(self, 'listen_socket', None) is not None: if getattr(self, 'listen_socket', None) is not None:
ans = select.select((self.listen_socket,), (), (), 0) ans = select.select((self.listen_socket,), (), (), 0)
if len(ans[0]) > 0: if len(ans[0]) > 0:
@ -625,11 +651,14 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
challenge = '' challenge = ''
hash_digest = '' hash_digest = ''
opcode, result = self._call_client('GET_INITIALIZATION_INFO', opcode, result = self._call_client('GET_INITIALIZATION_INFO',
{'serverProtocolVersion': self.PROTOCOL_VERSION, {'serverProtocolVersion': self.PROTOCOL_VERSION,
'validExtensions': self.ALL_FORMATS, 'validExtensions': self.ALL_FORMATS,
'passwordChallenge': challenge, 'passwordChallenge': challenge,
'currentLibraryName': self.current_library_name, 'currentLibraryName': self.current_library_name,
'currentLibraryUUID': library_uuid}) 'currentLibraryUUID': library_uuid,
'pubdateFormat': tweaks['gui_pubdate_display_format'],
'timestampFormat': tweaks['gui_timestamp_display_format'],
'lastModifiedFormat': tweaks['gui_last_modified_display_format']})
if opcode != 'OK': if opcode != 'OK':
# Something wrong with the return. Close the socket # Something wrong with the return. Close the socket
# and continue. # and continue.
@ -775,7 +804,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
if opcode == 'OK': if opcode == 'OK':
if '_series_sort_' in result: if '_series_sort_' in result:
del result['_series_sort_'] del result['_series_sort_']
book = self.json_codec.raw_to_book(result, Book, self.PREFIX) book = self.json_codec.raw_to_book(result, SDBook, self.PREFIX)
self._set_known_metadata(book) self._set_known_metadata(book)
bl.add_book(book, replace_metadata=True) bl.add_book(book, replace_metadata=True)
else: else:
@ -847,7 +876,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
lpath = self._create_upload_path(mdata, fname, create_dirs=False) lpath = self._create_upload_path(mdata, fname, create_dirs=False)
if not hasattr(infile, 'read'): if not hasattr(infile, 'read'):
infile = USBMS.normalize_path(infile) infile = USBMS.normalize_path(infile)
book = Book(self.PREFIX, lpath, other=mdata) book = SDBook(self.PREFIX, lpath, other=mdata)
length = self._put_file(infile, lpath, book, i, len(files)) length = self._put_file(infile, lpath, book, i, len(files))
if length < 0: if length < 0:
raise ControlError(desc='Sending book %s to device failed' % lpath) raise ControlError(desc='Sending book %s to device failed' % lpath)
@ -872,7 +901,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
lpath = location[0] lpath = location[0]
length = location[1] length = location[1]
lpath = self._strip_prefix(lpath) lpath = self._strip_prefix(lpath)
book = Book(self.PREFIX, lpath, other=info) book = SDBook(self.PREFIX, lpath, other=info)
if book.size is None: if book.size is None:
book.size = length book.size = length
b = booklists[0].add_book(book, replace_metadata=True) b = booklists[0].add_book(book, replace_metadata=True)
@ -929,6 +958,15 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
else: else:
raise ControlError(desc='request for book data failed') raise ControlError(desc='request for book data failed')
@synchronous('sync_lock')
def prepare_addable_books(self, paths):
for idx, path in enumerate(paths):
(ign, ext) = os.path.splitext(path)
tf = PersistentTemporaryFile(suffix=ext)
self.get_file(path, tf)
paths[idx] = tf.name
return paths
@synchronous('sync_lock') @synchronous('sync_lock')
def set_plugboards(self, plugboards, pb_func): def set_plugboards(self, plugboards, pb_func):
self._debug() self._debug()
@ -976,31 +1014,26 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
message = _('Invalid port in options: %s')% \ message = _('Invalid port in options: %s')% \
self.settings().extra_customization[self.OPT_PORT_NUMBER] self.settings().extra_customization[self.OPT_PORT_NUMBER]
self._debug(message) self._debug(message)
self.listen_socket.close() self._close_listen_socket()
self.listen_socket = None
self.is_connected = False
return message return message
port = self._attach_to_port(opt_port) port = self._attach_to_port(self.listen_socket, opt_port)
if port == 0: if port == 0:
message = _('Failed to connect to port %d. Try a different value.')%opt_port message = _('Failed to connect to port %d. Try a different value.')%opt_port
self._debug(message) self._debug(message)
self.listen_socket.close() self._close_listen_socket()
self.listen_socket = None
self.is_connected = False
return message return message
else: else:
while i < 100: # try up to 100 random port numbers while i < 100: # try up to 100 random port numbers
i += 1 i += 1
port = self._attach_to_port(random.randint(8192, 32000)) port = self._attach_to_port(self.listen_socket,
random.randint(8192, 32000))
if port != 0: if port != 0:
break break
if port == 0: if port == 0:
message = _('Failed to allocate a random port') message = _('Failed to allocate a random port')
self._debug(message) self._debug(message)
self.listen_socket.close() self._close_listen_socket()
self.listen_socket = None
self.is_connected = False
return message return message
try: try:
@ -1008,9 +1041,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
except: except:
message = 'listen on port %d failed' % port message = 'listen on port %d failed' % port
self._debug(message) self._debug(message)
self.listen_socket.close() self._close_listen_socket()
self.listen_socket = None
self.is_connected = False
return message return message
try: try:
@ -1018,21 +1049,40 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
except: except:
message = 'registration with bonjour failed' message = 'registration with bonjour failed'
self._debug(message) self._debug(message)
self.listen_socket.close() self._close_listen_socket()
self.listen_socket = None
self.is_connected = False
return message return message
self._debug('listening on port', port) self._debug('listening on port', port)
self.port = port self.port = port
# Now try to open a UDP socket to receive broadcasts on
try:
self.broadcast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
except:
message = 'creation of broadcast socket failed. This is not fatal.'
self._debug(message)
return message
for p in self.BROADCAST_PORTS:
port = self._attach_to_port(self.broadcast_socket, p)
if port != 0:
self._debug('broadcast socket listening on port', port)
break
if port == 0:
self.broadcast_socket.close()
self.broadcast_socket = None
message = 'attaching port to broadcast socket failed. This is not fatal.'
self._debug(message)
return message
@synchronous('sync_lock') @synchronous('sync_lock')
def shutdown(self): def shutdown(self):
if getattr(self, 'listen_socket', None) is not None: if getattr(self, 'listen_socket', None) is not None:
do_zeroconf(unpublish_zeroconf, self.port) do_zeroconf(unpublish_zeroconf, self.port)
self.listen_socket.close() self._close_listen_socket()
self.listen_socket = None
self.is_connected = False
# Methods for dynamic control # Methods for dynamic control

View File

@ -237,20 +237,28 @@ class ConnectShareAction(InterfaceAction):
self.share_conn_menu.hide_smartdevice_menus() self.share_conn_menu.hide_smartdevice_menus()
def set_smartdevice_action_state(self): def set_smartdevice_action_state(self):
from calibre.utils.mdns import get_external_ip from calibre.gui2.dialogs.smartdevice import get_all_ip_addresses
dm = self.gui.device_manager dm = self.gui.device_manager
all_ips = get_all_ip_addresses()
if len(all_ips) > 3:
formatted_addresses = _('Many IP addresses. See Start/Stop dialog.')
show_port = False
else:
formatted_addresses = ' or '.join(get_all_ip_addresses())
show_port = True
running = dm.is_running('smartdevice') running = dm.is_running('smartdevice')
if not running: if not running:
text = self.share_conn_menu.DEVICE_MSGS[0] text = self.share_conn_menu.DEVICE_MSGS[0]
else: else:
use_fixed_port = dm.get_option('smartdevice', 'use_fixed_port') use_fixed_port = dm.get_option('smartdevice', 'use_fixed_port')
port_number = dm.get_option('smartdevice', 'port_number') port_number = dm.get_option('smartdevice', 'port_number')
if use_fixed_port: if show_port and use_fixed_port:
text = self.share_conn_menu.DEVICE_MSGS[1] + ' [%s port %s]'%( text = self.share_conn_menu.DEVICE_MSGS[1] + ' [%s, port %s]'%(
get_external_ip(), port_number) formatted_addresses, port_number)
else: else:
text = self.share_conn_menu.DEVICE_MSGS[1] + ' [%s]'%get_external_ip() text = self.share_conn_menu.DEVICE_MSGS[1] + ' [' + formatted_addresses + ']'
icon = 'green' if running else 'red' icon = 'green' if running else 'red'
ac = self.share_conn_menu.control_smartdevice_action ac = self.share_conn_menu.control_smartdevice_action

View File

@ -5,10 +5,35 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.Qt import (QDialog, QLineEdit, Qt) from PyQt4.Qt import (QDialog, QLineEdit, Qt, QPushButton, QDialogButtonBox)
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.smartdevice_ui import Ui_Dialog from calibre.gui2.dialogs.smartdevice_ui import Ui_Dialog
from calibre.utils.config import prefs
from calibre.utils.mdns import get_all_ips
def _cmp_ipaddr(l, r):
lparts = ['%3s'%x for x in l.split('.')]
rparts = ['%3s'%x for x in r.split('.')]
if lparts[0] in ['192', '170', ' 10']:
if rparts[0] not in ['192', '170', '10']:
return -1
return cmp(rparts, lparts)
if rparts[0] in ['192', '170', ' 10']:
return 1
return cmp(lparts, rparts)
def get_all_ip_addresses():
ipaddrs = list()
for iface in get_all_ips().itervalues():
for addrs in iface:
if 'broadcast' in addrs and addrs['addr'] != '127.0.0.1':
ipaddrs.append(addrs['addr'])
ipaddrs.sort(cmp=_cmp_ipaddr)
return ipaddrs
class SmartdeviceDialog(QDialog, Ui_Dialog): class SmartdeviceDialog(QDialog, Ui_Dialog):
@ -40,6 +65,15 @@ class SmartdeviceDialog(QDialog, Ui_Dialog):
'to the port, try another number. You can use any number between ' 'to the port, try another number. You can use any number between '
'8,000 and 32,000.') + '</p>') '8,000 and 32,000.') + '</p>')
self.ip_addresses.setToolTip('<p>' +
_('These are the IP addresses for this computer. If you decide to have your device connect to '
'calibre using a fixed IP address, one of these addresses should '
'be the one you use. It is unlikely but possible that the correct '
'IP address is not listed here, in which case you will need to go '
"to your computer's control panel to get a complete list of "
"your computer's network interfaces and IP addresses.") + '</p>')
self.show_password.stateChanged[int].connect(self.toggle_password) self.show_password.stateChanged[int].connect(self.toggle_password)
self.use_fixed_port.stateChanged[int].connect(self.use_fixed_port_changed) self.use_fixed_port.stateChanged[int].connect(self.use_fixed_port_changed)
@ -57,15 +91,39 @@ class SmartdeviceDialog(QDialog, Ui_Dialog):
self.orig_port_number = self.device_manager.get_option('smartdevice', self.orig_port_number = self.device_manager.get_option('smartdevice',
'port_number') 'port_number')
self.fixed_port.setText(self.orig_port_number) self.fixed_port.setText(self.orig_port_number)
self.use_fixed_port.setChecked(self.orig_fixed_port); self.use_fixed_port.setChecked(self.orig_fixed_port)
if not self.orig_fixed_port: if not self.orig_fixed_port:
self.fixed_port.setEnabled(False); self.fixed_port.setEnabled(False)
if pw: if pw:
self.password_box.setText(pw) self.password_box.setText(pw)
self.auto_mgmt_button = QPushButton(_('Enable automatic metadata management'))
self.auto_mgmt_button.clicked.connect(self.auto_mgmt_button_clicked)
self.auto_mgmt_button.setToolTip('<p>' +
_('Enabling automatic metadata management tells calibre to send any '
'changes you made to books\' metadata when your device is '
'connected, which is the most useful setting when using the wireless '
'device interface. If automatic metadata management is not '
'enabled, changes are sent only when you re-send the book. You can '
'get more information or change this preference to some other '
'choice at Preferences -> Sending books to devices -> '
'Metadata management')
+ '</p>')
self.buttonBox.addButton(self.auto_mgmt_button, QDialogButtonBox.ActionRole)
if prefs['manage_device_metadata'] == 'on_connect':
self.auto_mgmt_button.setText(_('Automatic metadata management is enabled'))
self.auto_mgmt_button.setEnabled(False)
self.ip_addresses.setText(', '.join(get_all_ip_addresses()))
self.resize(self.sizeHint()) self.resize(self.sizeHint())
def auto_mgmt_button_clicked(self):
self.auto_mgmt_button.setText(_('Automatic metadata management is enabled'))
self.auto_mgmt_button.setEnabled(False)
prefs.set('manage_device_metadata', 'on_connect')
def use_fixed_port_changed(self, state): def use_fixed_port_changed(self, state):
self.fixed_port.setEnabled(state == Qt.Checked) self.fixed_port.setEnabled(state == Qt.Checked)

View File

@ -38,7 +38,34 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0">
<widget class="QLabel" name="label_23">
<property name="text">
<string>Calibre IP addresses:</string>
</property>
</widget>
</item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QLabel" name="ip_addresses">
<property name="text">
<string>Possibe IP addresses:</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Optional &amp;password:</string>
</property>
<property name="buddy">
<cstring>password_box</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="password_box"> <widget class="QLineEdit" name="password_box">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
@ -54,24 +81,14 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="2" column="2">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Optional &amp;password:</string>
</property>
<property name="buddy">
<cstring>password_box</cstring>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="show_password"> <widget class="QCheckBox" name="show_password">
<property name="text"> <property name="text">
<string>&amp;Show password</string> <string>&amp;Show password</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="4" column="0">
<widget class="QLabel" name="label_21"> <widget class="QLabel" name="label_21">
<property name="text"> <property name="text">
<string>Optional &amp;fixed port:</string> <string>Optional &amp;fixed port:</string>
@ -81,7 +98,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="4" column="1">
<widget class="QLineEdit" name="fixed_port"> <widget class="QLineEdit" name="fixed_port">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
@ -94,14 +111,21 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="2"> <item row="4" column="2">
<widget class="QCheckBox" name="use_fixed_port"> <widget class="QCheckBox" name="use_fixed_port">
<property name="text"> <property name="text">
<string>&amp;Use a fixed port</string> <string>&amp;Use a fixed port</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0" colspan="3"> <item row="6" column="0" colspan="3">
<widget class="QCheckBox" name="autostart_box">
<property name="text">
<string>&amp;Automatically allow connections at calibre startup</string>
</property>
</widget>
</item>
<item row="10" column="0" colspan="3">
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
@ -111,13 +135,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0" colspan="3">
<widget class="QCheckBox" name="autostart_box">
<property name="text">
<string>&amp;Automatically allow connections at calibre startup</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources> <resources>