Disable on-device editing of metadata in the GUI for devices that don't support it (all the non SONY devices)

This commit is contained in:
Kovid Goyal 2009-03-25 09:33:27 -07:00
parent 2a7c0bab5e
commit 03870b6a68
5 changed files with 193 additions and 182 deletions

View File

@ -24,6 +24,8 @@ class Device(object):
# it can be a list of the BCD numbers of all devices supported by this driver. # it can be a list of the BCD numbers of all devices supported by this driver.
BCD = None BCD = None
THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device
# Whether the metadata on books can be set via the GUI.
CAN_SET_METADATA = True
def __init__(self, key='-1', log_packets=False, report_progress=None) : def __init__(self, key='-1', log_packets=False, report_progress=None) :
""" """

View File

@ -22,17 +22,17 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
### Usage Type Data ### Usage Type Data
### wMaxPacketSize 0x0040 1x 64 bytes ### wMaxPacketSize 0x0040 1x 64 bytes
### bInterval 0 ### bInterval 0
###
### ###
### Endpoint 0x81 is device->host and endpoint 0x02 is host->device. ###
### Endpoint 0x81 is device->host and endpoint 0x02 is host->device.
### You can establish Stream pipes to/from these endpoints for Bulk transfers. ### You can establish Stream pipes to/from these endpoints for Bulk transfers.
### Has two configurations 1 is the USB charging config 2 is the self-powered ### Has two configurations 1 is the USB charging config 2 is the self-powered
### config. I think config management is automatic. Endpoints are the same ### config. I think config management is automatic. Endpoints are the same
""" """
Contains the logic for communication with the device (a SONY PRS-500). Contains the logic for communication with the device (a SONY PRS-500).
The public interface of class L{PRS500} defines the The public interface of class L{PRS500} defines the
methods for performing various tasks. methods for performing various tasks.
""" """
import sys, os import sys, os
from tempfile import TemporaryFile from tempfile import TemporaryFile
@ -49,12 +49,12 @@ from calibre.devices.prs500.books import BookList, fix_ids
from calibre import __author__, __appname__ from calibre import __author__, __appname__
# Protocol versions this driver has been tested with # Protocol versions this driver has been tested with
KNOWN_USB_PROTOCOL_VERSIONS = [0x3030303030303130L] KNOWN_USB_PROTOCOL_VERSIONS = [0x3030303030303130L]
class File(object): class File(object):
""" """
Wrapper that allows easy access to all information about files/directories Wrapper that allows easy access to all information about files/directories
""" """
def __init__(self, _file): def __init__(self, _file):
self.is_dir = _file[1].is_dir #: True if self is a directory self.is_dir = _file[1].is_dir #: True if self is a directory
@ -63,9 +63,9 @@ class File(object):
self.ctime = _file[1].ctime #: Creation time of self as a epoch self.ctime = _file[1].ctime #: Creation time of self as a epoch
self.wtime = _file[1].wtime #: Creation time of self as an epoch self.wtime = _file[1].wtime #: Creation time of self as an epoch
path = _file[0] path = _file[0]
if path.endswith("/"): if path.endswith("/"):
path = path[:-1] path = path[:-1]
self.path = path #: Path to self self.path = path #: Path to self
self.name = path[path.rfind("/")+1:].rstrip() #: Name of self self.name = path[path.rfind("/")+1:].rstrip() #: Name of self
def __repr__(self): def __repr__(self):
@ -80,7 +80,7 @@ class PRS500(Device):
""" """
Implements the backend for communication with the SONY Reader. Implements the backend for communication with the SONY Reader.
Each method decorated by C{safe} performs a task. Each method decorated by C{safe} performs a task.
""" """
VENDOR_ID = 0x054c #: SONY Vendor Id VENDOR_ID = 0x054c #: SONY Vendor Id
@ -92,33 +92,33 @@ class PRS500(Device):
BULK_IN_EP = 0x81 #: Endpoint for Bulk reads BULK_IN_EP = 0x81 #: Endpoint for Bulk reads
BULK_OUT_EP = 0x02 #: Endpoint for Bulk writes BULK_OUT_EP = 0x02 #: Endpoint for Bulk writes
# Location of media.xml file on device # Location of media.xml file on device
MEDIA_XML = "/Data/database/cache/media.xml" MEDIA_XML = "/Data/database/cache/media.xml"
# Location of cache.xml on storage card in device # Location of cache.xml on storage card in device
CACHE_XML = "/Sony Reader/database/cache.xml" CACHE_XML = "/Sony Reader/database/cache.xml"
# Ordered list of supported formats # Ordered list of supported formats
FORMATS = ["lrf", "lrx", "rtf", "pdf", "txt"] FORMATS = ["lrf", "lrx", "rtf", "pdf", "txt"]
# Height for thumbnails of books/images on the device # Height for thumbnails of books/images on the device
THUMBNAIL_HEIGHT = 68 THUMBNAIL_HEIGHT = 68
# Directory on card to which books are copied # Directory on card to which books are copied
CARD_PATH_PREFIX = __appname__ CARD_PATH_PREFIX = __appname__
_packet_number = 0 #: Keep track of the packet number for packet tracing _packet_number = 0 #: Keep track of the packet number for packet tracing
def log_packet(self, packet, header, stream=sys.stderr): def log_packet(self, packet, header, stream=sys.stderr):
""" """
Log C{packet} to stream C{stream}. Log C{packet} to stream C{stream}.
Header should be a small word describing the type of packet. Header should be a small word describing the type of packet.
""" """
self._packet_number += 1 self._packet_number += 1
print >> stream, str(self._packet_number), header, "Type:", \ print >> stream, str(self._packet_number), header, "Type:", \
packet.__class__.__name__ packet.__class__.__name__
print >> stream, packet print >> stream, packet
print >> stream, "--" print >> stream, "--"
@classmethod @classmethod
def validate_response(cls, res, _type=0x00, number=0x00): def validate_response(cls, res, _type=0x00, number=0x00):
""" """
Raise a ProtocolError if the type and number of C{res} Raise a ProtocolError if the type and number of C{res}
is not the same as C{type} and C{number}. is not the same as C{type} and C{number}.
""" """
if _type != res.type or number != res.rnumber: if _type != res.type or number != res.rnumber:
raise ProtocolError("Inavlid response.\ntype: expected=" + \ raise ProtocolError("Inavlid response.\ntype: expected=" + \
@ -127,31 +127,31 @@ class PRS500(Device):
" actual="+hex(res.rnumber)) " actual="+hex(res.rnumber))
@classmethod @classmethod
def signature(cls): def signature(cls):
""" Return a two element tuple (vendor id, product id) """ """ Return a two element tuple (vendor id, product id) """
return (cls.VENDOR_ID, cls.PRODUCT_ID ) return (cls.VENDOR_ID, cls.PRODUCT_ID )
def safe(func): def safe(func):
""" """
Decorator that wraps a call to C{func} to ensure that Decorator that wraps a call to C{func} to ensure that
exceptions are handled correctly. It also calls L{open} to claim exceptions are handled correctly. It also calls L{open} to claim
the interface and initialize the Reader if needed. the interface and initialize the Reader if needed.
As a convenience, C{safe} automatically sends the a As a convenience, C{safe} automatically sends the a
L{EndSession} after calling func, unless func has L{EndSession} after calling func, unless func has
a keyword argument named C{end_session} set to C{False}. a keyword argument named C{end_session} set to C{False}.
An L{ArgumentError} will cause the L{EndSession} command to An L{ArgumentError} will cause the L{EndSession} command to
be sent to the device, unless end_session is set to C{False}. be sent to the device, unless end_session is set to C{False}.
An L{usb.USBError} will cause the library to release control of the An L{usb.USBError} will cause the library to release control of the
USB interface via a call to L{close}. USB interface via a call to L{close}.
""" """
@wraps(func) @wraps(func)
def run_session(*args, **kwargs): def run_session(*args, **kwargs):
dev = args[0] dev = args[0]
res = None res = None
try: try:
if not dev.handle: if not dev.handle:
dev.open() dev.open()
if not dev.in_session: if not dev.in_session:
dev.send_validated_command(BeginEndSession(end=False)) dev.send_validated_command(BeginEndSession(end=False))
@ -161,19 +161,19 @@ class PRS500(Device):
if not kwargs.has_key("end_session") or kwargs["end_session"]: if not kwargs.has_key("end_session") or kwargs["end_session"]:
dev.send_validated_command(BeginEndSession(end=True)) dev.send_validated_command(BeginEndSession(end=True))
dev.in_session = False dev.in_session = False
raise raise
except USBError, err: except USBError, err:
if "No such device" in str(err): if "No such device" in str(err):
raise DeviceError() raise DeviceError()
elif "Connection timed out" in str(err): elif "Connection timed out" in str(err):
dev.close() dev.close()
raise TimeoutError(func.__name__) raise TimeoutError(func.__name__)
elif "Protocol error" in str(err): elif "Protocol error" in str(err):
dev.close() dev.close()
raise ProtocolError("There was an unknown error in the"+\ raise ProtocolError("There was an unknown error in the"+\
" protocol. Contact " + __author__) " protocol. Contact " + __author__)
dev.close() dev.close()
raise raise
if not kwargs.has_key("end_session") or kwargs["end_session"]: if not kwargs.has_key("end_session") or kwargs["end_session"]:
dev.send_validated_command(BeginEndSession(end=True)) dev.send_validated_command(BeginEndSession(end=True))
dev.in_session = False dev.in_session = False
@ -182,15 +182,15 @@ class PRS500(Device):
return run_session return run_session
def __init__(self, key='-1', log_packets=False, report_progress=None) : def __init__(self, key='-1', log_packets=False, report_progress=None) :
""" """
@param key: The key to unlock the device @param key: The key to unlock the device
@param log_packets: If true the packet stream to/from the device is logged @param log_packets: If true the packet stream to/from the device is logged
@param report_progress: Function that is called with a % progress @param report_progress: Function that is called with a % progress
(number between 0 and 100) for various tasks (number between 0 and 100) for various tasks
If it is called with -1 that means that the If it is called with -1 that means that the
task does not have any progress information task does not have any progress information
""" """
self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID) self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID)
# Handle that is used to communicate with device. Setup in L{open} # Handle that is used to communicate with device. Setup in L{open}
self.handle = None self.handle = None
self.in_session = False self.in_session = False
@ -204,14 +204,14 @@ class PRS500(Device):
def reconnect(self): def reconnect(self):
""" Only recreates the device node and deleted the connection handle """ """ Only recreates the device node and deleted the connection handle """
self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID) self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID)
self.handle = None self.handle = None
@classmethod @classmethod
def is_connected(cls, helper=None): def is_connected(cls, helper=None):
""" """
This method checks to see whether the device is physically connected. This method checks to see whether the device is physically connected.
It does not return any information about the validity of the It does not return any information about the validity of the
software connection. You may need to call L{reconnect} if you keep software connection. You may need to call L{reconnect} if you keep
getting L{DeviceError}. getting L{DeviceError}.
""" """
@ -222,15 +222,15 @@ class PRS500(Device):
def set_progress_reporter(self, report_progress): def set_progress_reporter(self, report_progress):
self.report_progress = report_progress self.report_progress = report_progress
def open(self) : def open(self) :
""" """
Claim an interface on the device for communication. Claim an interface on the device for communication.
Requires write privileges to the device file. Requires write privileges to the device file.
Also initialize the device. Also initialize the device.
See the source code for the sequence of initialization commands. See the source code for the sequence of initialization commands.
""" """
self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID) self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID)
if not self.device: if not self.device:
raise DeviceError() raise DeviceError()
configs = self.device.configurations configs = self.device.configurations
@ -238,7 +238,7 @@ class PRS500(Device):
self.handle = self.device.open() self.handle = self.device.open()
config = configs[0] config = configs[0]
try: try:
self.handle.set_configuration(configs[0]) self.handle.set_configuration(configs[0])
except USBError: except USBError:
self.handle.set_configuration(configs[1]) self.handle.set_configuration(configs[1])
config = configs[1] config = configs[1]
@ -250,13 +250,13 @@ class PRS500(Device):
else: else:
red, wed = ed2, ed1 red, wed = ed2, ed1
self.bulk_read_max_packet_size = red.MaxPacketSize self.bulk_read_max_packet_size = red.MaxPacketSize
self.bulk_write_max_packet_size = wed.MaxPacketSize self.bulk_write_max_packet_size = wed.MaxPacketSize
self.handle.claim_interface(self.INTERFACE_ID) self.handle.claim_interface(self.INTERFACE_ID)
except USBError, err: except USBError, err:
raise DeviceBusy(str(err)) raise DeviceBusy(str(err))
# Large timeout as device may still be initializing # Large timeout as device may still be initializing
res = self.send_validated_command(GetUSBProtocolVersion(), timeout=20000) res = self.send_validated_command(GetUSBProtocolVersion(), timeout=20000)
if res.code != 0: if res.code != 0:
raise ProtocolError("Unable to get USB Protocol version.") raise ProtocolError("Unable to get USB Protocol version.")
version = self._bulk_read(24, data_type=USBProtocolVersion)[0].version version = self._bulk_read(24, data_type=USBProtocolVersion)[0].version
if version not in KNOWN_USB_PROTOCOL_VERSIONS: if version not in KNOWN_USB_PROTOCOL_VERSIONS:
@ -265,16 +265,16 @@ class PRS500(Device):
res = self.send_validated_command(SetBulkSize(\ res = self.send_validated_command(SetBulkSize(\
chunk_size = 512*self.bulk_read_max_packet_size, \ chunk_size = 512*self.bulk_read_max_packet_size, \
unknown = 2)) unknown = 2))
if res.code != 0: if res.code != 0:
raise ProtocolError("Unable to set bulk size.") raise ProtocolError("Unable to set bulk size.")
res = self.send_validated_command(UnlockDevice(key=self.key))#0x312d)) res = self.send_validated_command(UnlockDevice(key=self.key))#0x312d))
if res.code != 0: if res.code != 0:
raise DeviceLocked() raise DeviceLocked()
res = self.send_validated_command(SetTime()) res = self.send_validated_command(SetTime())
if res.code != 0: if res.code != 0:
raise ProtocolError("Could not set time on device") raise ProtocolError("Could not set time on device")
def close(self): def close(self):
""" Release device interface """ """ Release device interface """
try: try:
self.handle.reset() self.handle.reset()
@ -285,16 +285,16 @@ class PRS500(Device):
self.in_session = False self.in_session = False
def _send_command(self, command, response_type=Response, timeout=1000): def _send_command(self, command, response_type=Response, timeout=1000):
""" """
Send L{command<Command>} to device and return its L{response<Response>}. Send L{command<Command>} to device and return its L{response<Response>}.
@param command: an object of type Command or one of its derived classes @param command: an object of type Command or one of its derived classes
@param response_type: an object of type 'type'. The return packet @param response_type: an object of type 'type'. The return packet
from the device is returned as an object of type response_type. from the device is returned as an object of type response_type.
@param timeout: The time to wait for a response from the @param timeout: The time to wait for a response from the
device, in milliseconds. If there is no response, a L{usb.USBError} is raised. device, in milliseconds. If there is no response, a L{usb.USBError} is raised.
""" """
if self.log_packets: if self.log_packets:
self.log_packet(command, "Command") self.log_packet(command, "Command")
bytes_sent = self.handle.control_msg(0x40, 0x80, command) bytes_sent = self.handle.control_msg(0x40, 0x80, command)
if bytes_sent != len(command): if bytes_sent != len(command):
@ -302,19 +302,19 @@ class PRS500(Device):
+ str(command)) + str(command))
response = response_type(self.handle.control_msg(0xc0, 0x81, \ response = response_type(self.handle.control_msg(0xc0, 0x81, \
Response.SIZE, timeout=timeout)) Response.SIZE, timeout=timeout))
if self.log_packets: if self.log_packets:
self.log_packet(response, "Response") self.log_packet(response, "Response")
return response return response
def send_validated_command(self, command, cnumber=None, \ def send_validated_command(self, command, cnumber=None, \
response_type=Response, timeout=1000): response_type=Response, timeout=1000):
""" """
Wrapper around L{_send_command} that checks if the Wrapper around L{_send_command} that checks if the
C{Response.rnumber == cnumber or C{Response.rnumber == cnumber or
command.number if cnumber==None}. Also check that command.number if cnumber==None}. Also check that
C{Response.type == Command.type}. C{Response.type == Command.type}.
""" """
if cnumber == None: if cnumber == None:
cnumber = command.number cnumber = command.number
res = self._send_command(command, response_type=response_type, \ res = self._send_command(command, response_type=response_type, \
timeout=timeout) timeout=timeout)
@ -322,18 +322,18 @@ class PRS500(Device):
return res return res
def _bulk_write(self, data, packet_size=0x1000): def _bulk_write(self, data, packet_size=0x1000):
""" """
Send data to device via a bulk transfer. Send data to device via a bulk transfer.
@type data: Any listable type supporting __getslice__ @type data: Any listable type supporting __getslice__
@param packet_size: Size of packets to be sent to device. @param packet_size: Size of packets to be sent to device.
C{data} is broken up into packets to be sent to device. C{data} is broken up into packets to be sent to device.
""" """
def bulk_write_packet(packet): def bulk_write_packet(packet):
self.handle.bulk_write(self.BULK_OUT_EP, packet) self.handle.bulk_write(self.BULK_OUT_EP, packet)
if self.log_packets: if self.log_packets:
self.log_packet(Answer(packet), "Answer h->d") self.log_packet(Answer(packet), "Answer h->d")
bytes_left = len(data) bytes_left = len(data)
if bytes_left + 16 <= packet_size: if bytes_left + 16 <= packet_size:
packet_size = bytes_left +16 packet_size = bytes_left +16
first_packet = Answer(bytes_left+16) first_packet = Answer(bytes_left+16)
@ -355,11 +355,11 @@ class PRS500(Device):
pos = endpos pos = endpos
res = Response(self.handle.control_msg(0xc0, 0x81, Response.SIZE, \ res = Response(self.handle.control_msg(0xc0, 0x81, Response.SIZE, \
timeout=5000)) timeout=5000))
if self.log_packets: if self.log_packets:
self.log_packet(res, "Response") self.log_packet(res, "Response")
if res.rnumber != 0x10005 or res.code != 0: if res.rnumber != 0x10005 or res.code != 0:
raise ProtocolError("Sending via Bulk Transfer failed with response:\n"\ raise ProtocolError("Sending via Bulk Transfer failed with response:\n"\
+str(res)) +str(res))
if res.data_size != len(data): if res.data_size != len(data):
raise ProtocolError("Unable to transfer all data to device. "+\ raise ProtocolError("Unable to transfer all data to device. "+\
"Response packet:\n"\ "Response packet:\n"\
@ -368,12 +368,12 @@ class PRS500(Device):
def _bulk_read(self, bytes, command_number=0x00, packet_size=0x1000, \ def _bulk_read(self, bytes, command_number=0x00, packet_size=0x1000, \
data_type=Answer): data_type=Answer):
""" """
Read in C{bytes} bytes via a bulk transfer in Read in C{bytes} bytes via a bulk transfer in
packets of size S{<=} C{packet_size} packets of size S{<=} C{packet_size}
@param data_type: an object of type type. @param data_type: an object of type type.
The data packet is returned as an object of type C{data_type}. The data packet is returned as an object of type C{data_type}.
@return: A list of packets read from the device. @return: A list of packets read from the device.
Each packet is of type data_type Each packet is of type data_type
""" """
msize = self.bulk_read_max_packet_size msize = self.bulk_read_max_packet_size
@ -392,7 +392,7 @@ class PRS500(Device):
bytes_left = bytes bytes_left = bytes
packets = [] packets = []
while bytes_left > 0: while bytes_left > 0:
if packet_size > bytes_left: if packet_size > bytes_left:
packet_size = bytes_left packet_size = bytes_left
packet = bulk_read_packet(data_type=data_type, size=packet_size) packet = bulk_read_packet(data_type=data_type, size=packet_size)
bytes_left -= len(packet) bytes_left -= len(packet)
@ -404,8 +404,8 @@ class PRS500(Device):
@safe @safe
def get_device_information(self, end_session=True): def get_device_information(self, end_session=True):
""" """
Ask device for device information. See L{DeviceInfoQuery}. Ask device for device information. See L{DeviceInfoQuery}.
@return: (device name, device version, software version on device, mime type) @return: (device name, device version, software version on device, mime type)
""" """
size = self.send_validated_command(DeviceInfoQuery()).data[2] + 16 size = self.send_validated_command(DeviceInfoQuery()).data[2] + 16
@ -416,21 +416,21 @@ class PRS500(Device):
@safe @safe
def path_properties(self, path, end_session=True): def path_properties(self, path, end_session=True):
""" """
Send command asking device for properties of C{path}. Send command asking device for properties of C{path}.
Return L{FileProperties}. Return L{FileProperties}.
""" """
res = self.send_validated_command(PathQuery(path), \ res = self.send_validated_command(PathQuery(path), \
response_type=ListResponse) response_type=ListResponse)
data = self._bulk_read(0x28, data_type=FileProperties, \ data = self._bulk_read(0x28, data_type=FileProperties, \
command_number=PathQuery.NUMBER)[0] command_number=PathQuery.NUMBER)[0]
if path.endswith('/') and path != '/': if path.endswith('/') and path != '/':
path = path[:-1] path = path[:-1]
if res.path_not_found : if res.path_not_found :
raise PathError(path + " does not exist on device") raise PathError(path + " does not exist on device")
if res.is_invalid: if res.is_invalid:
raise PathError(path + " is not a valid path") raise PathError(path + " is not a valid path")
if res.is_unmounted: if res.is_unmounted:
raise PathError(path + " is not mounted") raise PathError(path + " is not mounted")
if res.permission_denied: if res.permission_denied:
raise PathError('Permission denied for: ' + path + '\nYou can only '+\ raise PathError('Permission denied for: ' + path + '\nYou can only '+\
@ -443,20 +443,20 @@ class PRS500(Device):
@safe @safe
def get_file(self, path, outfile, end_session=True): def get_file(self, path, outfile, end_session=True):
""" """
Read the file at path on the device and write it to outfile. Read the file at path on the device and write it to outfile.
The data is fetched in chunks of size S{<=} 32K. Each chunk is The data is fetched in chunks of size S{<=} 32K. Each chunk is
made of packets of size S{<=} 4K. See L{FileOpen}, made of packets of size S{<=} 4K. See L{FileOpen},
L{FileRead} and L{FileClose} for details on the command packets used. L{FileRead} and L{FileClose} for details on the command packets used.
@param outfile: file object like C{sys.stdout} or the result of an C{open} call @param outfile: file object like C{sys.stdout} or the result of an C{open} call
""" """
if path.endswith("/"): if path.endswith("/"):
path = path[:-1] # We only copy files path = path[:-1] # We only copy files
cp = self.card_prefix(False) cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '') path = path.replace('card:/', cp if cp else '')
_file = self.path_properties(path, end_session=False) _file = self.path_properties(path, end_session=False)
if _file.is_dir: if _file.is_dir:
raise PathError("Cannot read as " + path + " is a directory") raise PathError("Cannot read as " + path + " is a directory")
bytes = _file.file_size bytes = _file.file_size
res = self.send_validated_command(FileOpen(path)) res = self.send_validated_command(FileOpen(path))
@ -464,12 +464,12 @@ class PRS500(Device):
raise PathError("Unable to open " + path + \ raise PathError("Unable to open " + path + \
" for reading. Response code: " + hex(res.code)) " for reading. Response code: " + hex(res.code))
_id = self._bulk_read(20, data_type=IdAnswer, \ _id = self._bulk_read(20, data_type=IdAnswer, \
command_number=FileOpen.NUMBER)[0].id command_number=FileOpen.NUMBER)[0].id
# The first 16 bytes from the device are meta information on the packet stream # The first 16 bytes from the device are meta information on the packet stream
bytes_left, chunk_size = bytes, 512 * self.bulk_read_max_packet_size -16 bytes_left, chunk_size = bytes, 512 * self.bulk_read_max_packet_size -16
packet_size, pos = 64 * self.bulk_read_max_packet_size, 0 packet_size, pos = 64 * self.bulk_read_max_packet_size, 0
while bytes_left > 0: while bytes_left > 0:
if chunk_size > bytes_left: if chunk_size > bytes_left:
chunk_size = bytes_left chunk_size = bytes_left
res = self.send_validated_command(FileIO(_id, pos, chunk_size)) res = self.send_validated_command(FileIO(_id, pos, chunk_size))
if res.code != 0: if res.code != 0:
@ -477,21 +477,21 @@ class PRS500(Device):
raise ProtocolError("Error while reading from " + path + \ raise ProtocolError("Error while reading from " + path + \
". Response code: " + hex(res.code)) ". Response code: " + hex(res.code))
packets = self._bulk_read(chunk_size+16, \ packets = self._bulk_read(chunk_size+16, \
command_number=FileIO.RNUMBER, packet_size=packet_size) command_number=FileIO.RNUMBER, packet_size=packet_size)
try: try:
outfile.write("".join(map(chr, packets[0][16:]))) outfile.write("".join(map(chr, packets[0][16:])))
for i in range(1, len(packets)): for i in range(1, len(packets)):
outfile.write("".join(map(chr, packets[i]))) outfile.write("".join(map(chr, packets[i])))
except IOError, err: except IOError, err:
self.send_validated_command(FileClose(_id)) self.send_validated_command(FileClose(_id))
raise ArgumentError("File get operation failed. " + \ raise ArgumentError("File get operation failed. " + \
"Could not write to local location: " + str(err)) "Could not write to local location: " + str(err))
bytes_left -= chunk_size bytes_left -= chunk_size
pos += chunk_size pos += chunk_size
if self.report_progress: if self.report_progress:
self.report_progress(int(100*((1.*pos)/bytes))) self.report_progress(int(100*((1.*pos)/bytes)))
self.send_validated_command(FileClose(_id)) self.send_validated_command(FileClose(_id))
# Not going to check response code to see if close was successful # Not going to check response code to see if close was successful
# as there's not much we can do if it wasnt # as there's not much we can do if it wasnt
@safe @safe
@ -503,26 +503,26 @@ class PRS500(Device):
@type path: string @type path: string
@param path: The path to list @param path: The path to list
@type recurse: boolean @type recurse: boolean
@param recurse: If true do a recursive listing @param recurse: If true do a recursive listing
@return: A list of tuples. The first element of each tuple is a path. @return: A list of tuples. The first element of each tuple is a path.
The second element is a list of L{Files<File>}. The second element is a list of L{Files<File>}.
The path is the path we are listing, the C{Files} are the The path is the path we are listing, the C{Files} are the
files/directories in that path. If it is a recursive list, then the first files/directories in that path. If it is a recursive list, then the first
element will be (C{path}, children), the next will be element will be (C{path}, children), the next will be
(child, its children) and so on. If it is not recursive the length of the (child, its children) and so on. If it is not recursive the length of the
outermost list will be 1. outermost list will be 1.
""" """
def _list(path): def _list(path):
""" Do a non recursive listsing of path """ """ Do a non recursive listsing of path """
if not path.endswith("/"): if not path.endswith("/"):
path += "/" # Initially assume path is a directory path += "/" # Initially assume path is a directory
cp = self.card_prefix(False) cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '') path = path.replace('card:/', cp if cp else '')
files = [] files = []
candidate = self.path_properties(path, end_session=False) candidate = self.path_properties(path, end_session=False)
if not candidate.is_dir: if not candidate.is_dir:
path = path[:-1] path = path[:-1]
data = self.path_properties(path, end_session=False) data = self.path_properties(path, end_session=False)
files = [ File((path, data)) ] files = [ File((path, data)) ]
else: else:
# Get query ID used to ask for next element in list # Get query ID used to ask for next element in list
@ -536,20 +536,20 @@ class PRS500(Device):
next = DirRead(_id) next = DirRead(_id)
items = [] items = []
while True: while True:
res = self.send_validated_command(next, response_type=ListResponse) res = self.send_validated_command(next, response_type=ListResponse)
size = res.data_size + 16 size = res.data_size + 16
data = self._bulk_read(size, data_type=ListAnswer, \ data = self._bulk_read(size, data_type=ListAnswer, \
command_number=DirRead.NUMBER)[0] command_number=DirRead.NUMBER)[0]
# path_not_found seems to happen if the usb server # path_not_found seems to happen if the usb server
# doesn't have the permissions to access the directory # doesn't have the permissions to access the directory
if res.is_eol or res.path_not_found: if res.is_eol or res.path_not_found:
break break
elif res.code != 0: elif res.code != 0:
raise ProtocolError("Unknown error occured while "+\ raise ProtocolError("Unknown error occured while "+\
"reading contents of directory " + path + \ "reading contents of directory " + path + \
". Response code: " + hex(res.code)) ". Response code: " + hex(res.code))
items.append(data.name) items.append(data.name)
self.send_validated_command(DirClose(_id)) self.send_validated_command(DirClose(_id))
# Ignore res.code as we cant do anything if close fails # Ignore res.code as we cant do anything if close fails
for item in items: for item in items:
ipath = path + item ipath = path + item
@ -568,23 +568,23 @@ class PRS500(Device):
@safe @safe
def total_space(self, end_session=True): def total_space(self, end_session=True):
""" """
Get total space available on the mountpoints: Get total space available on the mountpoints:
1. Main memory 1. Main memory
2. Memory Stick 2. Memory Stick
3. SD Card 3. SD Card
@return: A 3 element list with total space in bytes of (1, 2, 3) @return: A 3 element list with total space in bytes of (1, 2, 3)
""" """
data = [] data = []
for path in ("/Data/", "a:/", "b:/"): for path in ("/Data/", "a:/", "b:/"):
# Timeout needs to be increased as it takes time to read card # Timeout needs to be increased as it takes time to read card
res = self.send_validated_command(TotalSpaceQuery(path), \ res = self.send_validated_command(TotalSpaceQuery(path), \
timeout=5000) timeout=5000)
buffer_size = 16 + res.data[2] buffer_size = 16 + res.data[2]
pkt = self._bulk_read(buffer_size, data_type=TotalSpaceAnswer, \ pkt = self._bulk_read(buffer_size, data_type=TotalSpaceAnswer, \
command_number=TotalSpaceQuery.NUMBER)[0] command_number=TotalSpaceQuery.NUMBER)[0]
data.append( pkt.total ) data.append( pkt.total )
return data return data
@safe @safe
@ -600,26 +600,26 @@ class PRS500(Device):
return path return path
except PathError: except PathError:
return None return None
@safe @safe
def free_space(self, end_session=True): def free_space(self, end_session=True):
""" """
Get free space available on the mountpoints: Get free space available on the mountpoints:
1. Main memory 1. Main memory
2. Memory Stick 2. Memory Stick
3. SD Card 3. SD Card
@return: A 3 element list with free space in bytes of (1, 2, 3) @return: A 3 element list with free space in bytes of (1, 2, 3)
""" """
data = [] data = []
for path in ("/", "a:/", "b:/"): for path in ("/", "a:/", "b:/"):
# Timeout needs to be increased as it takes time to read card # Timeout needs to be increased as it takes time to read card
self.send_validated_command(FreeSpaceQuery(path), \ self.send_validated_command(FreeSpaceQuery(path), \
timeout=5000) timeout=5000)
pkt = self._bulk_read(FreeSpaceAnswer.SIZE, \ pkt = self._bulk_read(FreeSpaceAnswer.SIZE, \
data_type=FreeSpaceAnswer, \ data_type=FreeSpaceAnswer, \
command_number=FreeSpaceQuery.NUMBER)[0] command_number=FreeSpaceQuery.NUMBER)[0]
data.append( pkt.free ) data.append( pkt.free )
return data return data
def _exists(self, path): def _exists(self, path):
@ -628,21 +628,21 @@ class PRS500(Device):
try: try:
dest = self.path_properties(path, end_session=False) dest = self.path_properties(path, end_session=False)
except PathError, err: except PathError, err:
if "does not exist" in str(err) or "not mounted" in str(err): if "does not exist" in str(err) or "not mounted" in str(err):
return (False, None) return (False, None)
else: raise else: raise
return (True, dest) return (True, dest)
@safe @safe
def touch(self, path, end_session=True): def touch(self, path, end_session=True):
""" """
Create a file at path Create a file at path
@todo: Update file modification time if it exists. @todo: Update file modification time if it exists.
Opening the file in write mode and then closing it doesn't work. Opening the file in write mode and then closing it doesn't work.
""" """
cp = self.card_prefix(False) cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '') path = path.replace('card:/', cp if cp else '')
if path.endswith("/") and len(path) > 1: if path.endswith("/") and len(path) > 1:
path = path[:-1] path = path[:-1]
exists, _file = self._exists(path) exists, _file = self._exists(path)
if exists and _file.is_dir: if exists and _file.is_dir:
@ -651,18 +651,18 @@ class PRS500(Device):
res = self.send_validated_command(FileCreate(path)) res = self.send_validated_command(FileCreate(path))
if res.code != 0: if res.code != 0:
raise PathError("Could not create file " + path + \ raise PathError("Could not create file " + path + \
". Response code: " + str(hex(res.code))) ". Response code: " + str(hex(res.code)))
@safe @safe
def put_file(self, infile, path, replace_file=False, end_session=True): def put_file(self, infile, path, replace_file=False, end_session=True):
""" """
Put infile onto the devoce at path Put infile onto the devoce at path
@param infile: An open file object. infile must have a name attribute. @param infile: An open file object. infile must have a name attribute.
If you are using a StringIO object set its name attribute manually. If you are using a StringIO object set its name attribute manually.
@param path: The path on the device at which to put infile. @param path: The path on the device at which to put infile.
It should point to an existing directory. It should point to an existing directory.
@param replace_file: If True and path points to a file that already exists, it is replaced @param replace_file: If True and path points to a file that already exists, it is replaced
""" """
pos = infile.tell() pos = infile.tell()
infile.seek(0, 2) infile.seek(0, 2)
bytes = infile.tell() - pos bytes = infile.tell() - pos
@ -673,12 +673,12 @@ class PRS500(Device):
exists, dest = self._exists(path) exists, dest = self._exists(path)
if exists: if exists:
if dest.is_dir: if dest.is_dir:
if not path.endswith("/"): if not path.endswith("/"):
path += "/" path += "/"
path += os.path.basename(infile.name) path += os.path.basename(infile.name)
return self.put_file(infile, path, replace_file=replace_file, end_session=False) return self.put_file(infile, path, replace_file=replace_file, end_session=False)
else: else:
if not replace_file: if not replace_file:
raise PathError("Cannot write to " + \ raise PathError("Cannot write to " + \
path + " as it already exists", path=path) path + " as it already exists", path=path)
_file = self.path_properties(path, end_session=False) _file = self.path_properties(path, end_session=False)
@ -693,7 +693,7 @@ class PRS500(Device):
raise ProtocolError("Unable to open " + path + \ raise ProtocolError("Unable to open " + path + \
" for writing. Response code: " + hex(res.code)) " for writing. Response code: " + hex(res.code))
_id = self._bulk_read(20, data_type=IdAnswer, \ _id = self._bulk_read(20, data_type=IdAnswer, \
command_number=FileOpen.NUMBER)[0].id command_number=FileOpen.NUMBER)[0].id
while data_left: while data_left:
data = array('B') data = array('B')
@ -704,7 +704,7 @@ class PRS500(Device):
data.fromstring(ind) data.fromstring(ind)
if len(ind) < chunk_size: if len(ind) < chunk_size:
raise EOFError raise EOFError
except EOFError: except EOFError:
data_left = False data_left = False
res = self.send_validated_command(FileIO(_id, pos, len(data), \ res = self.send_validated_command(FileIO(_id, pos, len(data), \
mode=FileIO.WNUMBER)) mode=FileIO.WNUMBER))
@ -715,7 +715,7 @@ class PRS500(Device):
pos += len(data) pos += len(data)
if self.report_progress: if self.report_progress:
self.report_progress( int(100*(pos-start_pos)/(1.*bytes)) ) self.report_progress( int(100*(pos-start_pos)/(1.*bytes)) )
self.send_validated_command(FileClose(_id)) self.send_validated_command(FileClose(_id))
# Ignore res.code as cant do anything if close fails # Ignore res.code as cant do anything if close fails
_file = self.path_properties(path, end_session=False) _file = self.path_properties(path, end_session=False)
if _file.file_size != pos: if _file.file_size != pos:
@ -727,7 +727,7 @@ class PRS500(Device):
def del_file(self, path, end_session=True): def del_file(self, path, end_session=True):
""" Delete C{path} from device iff path is a file """ """ Delete C{path} from device iff path is a file """
data = self.path_properties(path, end_session=False) data = self.path_properties(path, end_session=False)
if data.is_dir: if data.is_dir:
raise PathError("Cannot delete directories") raise PathError("Cannot delete directories")
res = self.send_validated_command(FileDelete(path), \ res = self.send_validated_command(FileDelete(path), \
response_type=ListResponse) response_type=ListResponse)
@ -741,7 +741,7 @@ class PRS500(Device):
if path.startswith('card:/'): if path.startswith('card:/'):
cp = self.card_prefix(False) cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '') path = path.replace('card:/', cp if cp else '')
if not path.endswith("/"): if not path.endswith("/"):
path += "/" path += "/"
error_prefix = "Cannot create directory " + path error_prefix = "Cannot create directory " + path
res = self.send_validated_command(DirCreate(path)).data[0] res = self.send_validated_command(DirCreate(path)).data[0]
@ -764,8 +764,8 @@ class PRS500(Device):
if not dir.is_dir: if not dir.is_dir:
self.del_file(path, end_session=False) self.del_file(path, end_session=False)
else: else:
if not path.endswith("/"): if not path.endswith("/"):
path += "/" path += "/"
res = self.send_validated_command(DirDelete(path)) res = self.send_validated_command(DirDelete(path))
if res.code == PathResponseCodes.HAS_CHILDREN: if res.code == PathResponseCodes.HAS_CHILDREN:
raise PathError("Cannot delete directory " + path + \ raise PathError("Cannot delete directory " + path + \
@ -778,24 +778,24 @@ class PRS500(Device):
def card(self, end_session=True): def card(self, end_session=True):
""" Return path prefix to installed card or None """ """ Return path prefix to installed card or None """
card = None card = None
if self._exists("a:/")[0]: if self._exists("a:/")[0]:
card = "a:" card = "a:"
if self._exists("b:/")[0]: if self._exists("b:/")[0]:
card = "b:" card = "b:"
return card return card
@safe @safe
def books(self, oncard=False, end_session=True): def books(self, oncard=False, end_session=True):
""" """
Return a list of ebooks on the device. Return a list of ebooks on the device.
@param oncard: If True return a list of ebooks on the storage card, @param oncard: If True return a list of ebooks on the storage card,
otherwise return list of ebooks in main memory of device otherwise return list of ebooks in main memory of device
@return: L{BookList} @return: L{BookList}
""" """
root = "/Data/media/" root = "/Data/media/"
tfile = TemporaryFile() tfile = TemporaryFile()
if oncard: if oncard:
try: try:
self.get_file("a:"+self.CACHE_XML, tfile, end_session=False) self.get_file("a:"+self.CACHE_XML, tfile, end_session=False)
root = "a:/" root = "a:/"
@ -804,9 +804,9 @@ class PRS500(Device):
self.get_file("b:"+self.CACHE_XML, tfile, end_session=False) self.get_file("b:"+self.CACHE_XML, tfile, end_session=False)
root = "b:/" root = "b:/"
except PathError: pass except PathError: pass
if tfile.tell() == 0: if tfile.tell() == 0:
tfile = None tfile = None
else: else:
self.get_file(self.MEDIA_XML, tfile, end_session=False) self.get_file(self.MEDIA_XML, tfile, end_session=False)
bl = BookList(root=root, sfile=tfile) bl = BookList(root=root, sfile=tfile)
paths = bl.purge_corrupted_files() paths = bl.purge_corrupted_files()
@ -822,26 +822,26 @@ class PRS500(Device):
""" """
Remove the books specified by paths from the device. The metadata Remove the books specified by paths from the device. The metadata
cache on the device should also be updated. cache on the device should also be updated.
""" """
for path in paths: for path in paths:
self.del_file(path, end_session=False) self.del_file(path, end_session=False)
fix_ids(booklists[0], booklists[1]) fix_ids(booklists[0], booklists[1])
self.sync_booklists(booklists, end_session=False) self.sync_booklists(booklists, end_session=False)
@safe @safe
def sync_booklists(self, booklists, end_session=True): def sync_booklists(self, booklists, end_session=True):
''' '''
Upload bookslists to device. Upload bookslists to device.
@param booklists: A tuple containing the result of calls to @param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)). (L{books}(oncard=False), L{books}(oncard=True)).
''' '''
fix_ids(*booklists) fix_ids(*booklists)
self.upload_book_list(booklists[0], end_session=False) self.upload_book_list(booklists[0], end_session=False)
if booklists[1].root: if booklists[1].root:
self.upload_book_list(booklists[1], end_session=False) self.upload_book_list(booklists[1], end_session=False)
@safe @safe
def upload_books(self, files, names, on_card=False, end_session=True, def upload_books(self, files, names, on_card=False, end_session=True,
metadata=None): metadata=None):
card = self.card(end_session=False) card = self.card(end_session=False)
prefix = card + '/' + self.CARD_PATH_PREFIX +'/' if on_card else '/Data/media/books/' prefix = card + '/' + self.CARD_PATH_PREFIX +'/' if on_card else '/Data/media/books/'
@ -856,21 +856,21 @@ class PRS500(Device):
space = self.free_space(end_session=False) space = self.free_space(end_session=False)
mspace = space[0] mspace = space[0]
cspace = space[1] if space[1] >= space[2] else space[2] cspace = space[1] if space[1] >= space[2] else space[2]
if on_card and size > cspace - 1024*1024: if on_card and size > cspace - 1024*1024:
raise FreeSpaceError("There is insufficient free space "+\ raise FreeSpaceError("There is insufficient free space "+\
"on the storage card") "on the storage card")
if not on_card and size > mspace - 2*1024*1024: if not on_card and size > mspace - 2*1024*1024:
raise FreeSpaceError("There is insufficient free space " +\ raise FreeSpaceError("There is insufficient free space " +\
"in main memory") "in main memory")
for infile in infiles: for infile in infiles:
infile.seek(0) infile.seek(0)
name = names.next() name = names.next()
paths.append(prefix+name) paths.append(prefix+name)
self.put_file(infile, paths[-1], replace_file=True, end_session=False) self.put_file(infile, paths[-1], replace_file=True, end_session=False)
ctimes.append(self.path_properties(paths[-1], end_session=False).ctime) ctimes.append(self.path_properties(paths[-1], end_session=False).ctime)
return zip(paths, sizes, ctimes) return zip(paths, sizes, ctimes)
@classmethod @classmethod
def add_books_to_metadata(cls, locations, metadata, booklists): def add_books_to_metadata(cls, locations, metadata, booklists):
metadata = iter(metadata) metadata = iter(metadata)
@ -882,35 +882,35 @@ class PRS500(Device):
name = (cls.CARD_PATH_PREFIX+'/' if on_card else 'books/') + name name = (cls.CARD_PATH_PREFIX+'/' if on_card else 'books/') + name
booklists[on_card].add_book(info, name, *location[1:]) booklists[on_card].add_book(info, name, *location[1:])
fix_ids(*booklists) fix_ids(*booklists)
@safe @safe
def delete_books(self, paths, end_session=True): def delete_books(self, paths, end_session=True):
for path in paths: for path in paths:
self.del_file(path, end_session=False) self.del_file(path, end_session=False)
@classmethod @classmethod
def remove_books_from_metadata(cls, paths, booklists): def remove_books_from_metadata(cls, paths, booklists):
for path in paths: for path in paths:
on_card = 1 if path[1] == ':' else 0 on_card = 1 if path[1] == ':' else 0
booklists[on_card].remove_book(path) booklists[on_card].remove_book(path)
fix_ids(*booklists) fix_ids(*booklists)
@safe @safe
def add_book(self, infile, name, info, booklists, oncard=False, \ def add_book(self, infile, name, info, booklists, oncard=False, \
sync_booklists=False, end_session=True): sync_booklists=False, end_session=True):
""" """
Add a book to the device. If oncard is True then the book is copied Add a book to the device. If oncard is True then the book is copied
to the card rather than main memory. to the card rather than main memory.
@param infile: The source file, should be opened in "rb" mode @param infile: The source file, should be opened in "rb" mode
@param name: The name of the book file when uploaded to the @param name: The name of the book file when uploaded to the
device. The extension of name must be one of device. The extension of name must be one of
the supported formats for this device. the supported formats for this device.
@param info: A dictionary that must have the keys "title", "authors", "cover". @param info: A dictionary that must have the keys "title", "authors", "cover".
C{info["cover"]} should be a three element tuple (width, height, data) C{info["cover"]} should be a three element tuple (width, height, data)
where data is the image data in JPEG format as a string where data is the image data in JPEG format as a string
@param booklists: A tuple containing the result of calls to @param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)). (L{books}(oncard=False), L{books}(oncard=True)).
""" """
infile.seek(0, 2) infile.seek(0, 2)
size = infile.tell() size = infile.tell()
@ -922,11 +922,11 @@ class PRS500(Device):
if oncard and size > cspace - 1024*1024: if oncard and size > cspace - 1024*1024:
raise FreeSpaceError("There is insufficient free space "+\ raise FreeSpaceError("There is insufficient free space "+\
"on the storage card") "on the storage card")
if not oncard and size > mspace - 1024*1024: if not oncard and size > mspace - 1024*1024:
raise FreeSpaceError("There is insufficient free space " +\ raise FreeSpaceError("There is insufficient free space " +\
"in main memory") "in main memory")
prefix = "/Data/media/" prefix = "/Data/media/"
if oncard: if oncard:
prefix = card + "/" prefix = card + "/"
else: name = "books/"+name else: name = "books/"+name
path = prefix + name path = prefix + name
@ -943,12 +943,12 @@ class PRS500(Device):
path = self.MEDIA_XML path = self.MEDIA_XML
if not booklist.prefix: if not booklist.prefix:
card = self.card(end_session=True) card = self.card(end_session=True)
if not card: if not card:
raise ArgumentError("Cannot upload list to card as "+\ raise ArgumentError("Cannot upload list to card as "+\
"card is not present") "card is not present")
path = card + self.CACHE_XML path = card + self.CACHE_XML
f = StringIO() f = StringIO()
booklist.write(f) booklist.write(f)
f.seek(0) f.seek(0)
self.put_file(f, path, replace_file=True, end_session=False) self.put_file(f, path, replace_file=True, end_session=False)
f.close() f.close()

View File

@ -35,6 +35,7 @@ class USBMS(Device):
EBOOK_DIR_MAIN = '' EBOOK_DIR_MAIN = ''
EBOOK_DIR_CARD = '' EBOOK_DIR_CARD = ''
SUPPORTS_SUB_DIRS = False SUPPORTS_SUB_DIRS = False
CAN_SET_METADATA = False
def __init__(self, key='-1', log_packets=False, report_progress=None): def __init__(self, key='-1', log_packets=False, report_progress=None):
Device.__init__(self, key=key, log_packets=log_packets, Device.__init__(self, key=key, log_packets=log_packets,

View File

@ -706,6 +706,9 @@ class BooksView(TableView):
def close(self): def close(self):
self._model.close() self._model.close()
def set_editable(self, editable):
self._model.set_editable(editable)
def connect_to_search_box(self, sb): def connect_to_search_box(self, sb):
QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'),
@ -783,7 +786,7 @@ class DeviceBooksModel(BooksModel):
self.unknown = str(self.trUtf8('Unknown')) self.unknown = str(self.trUtf8('Unknown'))
self.marked_for_deletion = {} self.marked_for_deletion = {}
self.search_engine = OnDeviceSearch(self) self.search_engine = OnDeviceSearch(self)
self.editable = True
def mark_for_deletion(self, job, rows): def mark_for_deletion(self, job, rows):
self.marked_for_deletion[job] = self.indices(rows) self.marked_for_deletion[job] = self.indices(rows)
@ -791,7 +794,6 @@ class DeviceBooksModel(BooksModel):
indices = self.row_indices(row) indices = self.row_indices(row)
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1]) self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
def deletion_done(self, job, succeeded=True): def deletion_done(self, job, succeeded=True):
if not self.marked_for_deletion.has_key(job): if not self.marked_for_deletion.has_key(job):
return return
@ -816,7 +818,7 @@ class DeviceBooksModel(BooksModel):
if self.map[index.row()] in self.indices_to_be_deleted(): if self.map[index.row()] in self.indices_to_be_deleted():
return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python
flags = QAbstractTableModel.flags(self, index) flags = QAbstractTableModel.flags(self, index)
if index.isValid(): if index.isValid() and self.editable:
if index.column() in [0, 1] or (index.column() == 4 and self.db.supports_tags()): if index.column() in [0, 1] or (index.column() == 4 and self.db.supports_tags()):
flags |= Qt.ItemIsEditable flags |= Qt.ItemIsEditable
return flags return flags
@ -997,6 +999,10 @@ class DeviceBooksModel(BooksModel):
self.sort(col, self.sorted_on[1]) self.sort(col, self.sorted_on[1])
done = True done = True
return done return done
def set_editable(self, editable):
self.editable = editable
class SearchBox(QLineEdit): class SearchBox(QLineEdit):

View File

@ -644,7 +644,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
return return
mainlist, cardlist = job.result mainlist, cardlist = job.result
self.memory_view.set_database(mainlist) self.memory_view.set_database(mainlist)
self.memory_view.set_editable(self.device_manager.device_class.CAN_SET_METADATA)
self.card_view.set_database(cardlist) self.card_view.set_database(cardlist)
self.card_view.set_editable(self.device_manager.device_class.CAN_SET_METADATA)
for view in (self.memory_view, self.card_view): for view in (self.memory_view, self.card_view):
view.sortByColumn(3, Qt.DescendingOrder) view.sortByColumn(3, Qt.DescendingOrder)
if not view.restore_column_widths(): if not view.restore_column_widths():