diff --git a/src/libprs500/devices/interface.py b/src/libprs500/devices/interface.py index da825c9ed0..63294830fc 100644 --- a/src/libprs500/devices/interface.py +++ b/src/libprs500/devices/interface.py @@ -102,7 +102,7 @@ class Device(object): otherwise return list of ebooks in main memory of device. If True and no books on card return empty list. @return: A list of Books. Each Book object must have the fields: - title, author, size, datetime (a time tuple), path, thumbnail (can be None). + title, authors, size, datetime (a UTC time tuple), path, thumbnail (can be None). """ raise NotImplementedError() @@ -131,5 +131,13 @@ class Device(object): @param booklists: A tuple containing the result of calls to (L{books}(oncard=False), L{books}(oncard=True)). """ - raise NotImplementedError() + raise NotImplementedError() + + def sync_booklists(self, booklists, end_session=True): + ''' + Update metadata on device. + @param booklists: A tuple containing the result of calls to + (L{books}(oncard=False), L{books}(oncard=True)). + ''' + raise NotImplementedError() diff --git a/src/libprs500/devices/prs500/books.py b/src/libprs500/devices/prs500/books.py index 524298cf49..7cdee8a678 100644 --- a/src/libprs500/devices/prs500/books.py +++ b/src/libprs500/devices/prs500/books.py @@ -19,7 +19,7 @@ in the reader cache. import xml.dom.minidom as dom from base64 import b64decode as decode from base64 import b64encode as encode -import time +import time, re MIME_MAP = { \ "lrf":"application/x-sony-bbeb", \ @@ -28,6 +28,9 @@ MIME_MAP = { \ "txt":"text/plain" \ } +def sortable_title(title): + return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', ' An Crum').rstrip() + class book_metadata_field(object): """ Represents metadata stored as an attribute """ def __init__(self, attr, formatter=None, setter=None): @@ -48,7 +51,7 @@ class book_metadata_field(object): class Book(object): """ Provides a view onto the XML element that represents a book """ title = book_metadata_field("title") - author = book_metadata_field("author", \ + authors = book_metadata_field("author", \ formatter=lambda x: x if x and x.strip() else "Unknown") mime = book_metadata_field("mime") rpath = book_metadata_field("path") @@ -60,6 +63,18 @@ class Book(object): formatter=lambda x: time.strptime(x.strip(), "%a, %d %b %Y %H:%M:%S %Z"), setter=lambda x: time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(x))) + @apply + def title_sorter(): + doc = '''String to sort the title. If absent, title is returned''' + def fget(self): + src = self.elem.getAttribute('titleSorter').strip() + if not src: + src = self.title + return src + def fset(self, val): + self.elem.setAttribute('titleSorter', sortable_title(str(val))) + return property(doc=doc, fget=fget, fset=fset) + @apply def thumbnail(): doc = \ @@ -186,6 +201,7 @@ class BookList(list): sourceid = str(self[0].sourceid) if len(self) else "1" attrs = { "title" : info["title"], + 'titleSorter' : info['title'], "author" : info["authors"] if info['authors'] else 'Unknown', \ "page":"0", "part":"0", "scale":"0", \ "sourceid":sourceid, "id":str(cid), "date":"", \ diff --git a/src/libprs500/devices/prs500/driver.py b/src/libprs500/devices/prs500/driver.py index 3d9ae5b900..abe42a3684 100755 --- a/src/libprs500/devices/prs500/driver.py +++ b/src/libprs500/devices/prs500/driver.py @@ -813,10 +813,19 @@ class PRS500(Device): for path in paths: self.del_file(path, end_session=False) fix_ids(booklists[0], booklists[1]) + self.sync_booklists(booklists, end_session=False) + + @safe + def sync_booklists(self, booklists, end_session=True): + ''' + Upload bookslists to device. + @param booklists: A tuple containing the result of calls to + (L{books}(oncard=False), L{books}(oncard=True)). + ''' self.upload_book_list(booklists[0], end_session=False) if len(booklists[1]): self.upload_book_list(booklists[1], end_session=False) - + @safe def add_book(self, infile, name, info, booklists, oncard=False, \ sync_booklists=False, end_session=True): @@ -858,9 +867,7 @@ class PRS500(Device): bkl.add_book(info, name, size, ctime) fix_ids(booklists[0], booklists[1]) if sync_booklists: - self.upload_book_list(booklists[0], end_session=False) - if len(booklists[1]): - self.upload_book_list(booklists[1], end_session=False) + self.sync_booklists(booklists, end_session=False) @safe def upload_book_list(self, booklist, end_session=True): diff --git a/src/libprs500/ebooks/lrf/__init__.py b/src/libprs500/ebooks/lrf/__init__.py index 96729b81aa..a1a1f51834 100644 --- a/src/libprs500/ebooks/lrf/__init__.py +++ b/src/libprs500/ebooks/lrf/__init__.py @@ -16,13 +16,17 @@ This package contains logic to read and write LRF files. The LRF file format is documented at U{http://www.sven.de/librie/Librie/LrfFormat}. """ - +import sys, os from optparse import OptionParser, OptionValueError +from ttfquery import describe, findsystem +from fontTools.ttLib import TTLibError from libprs500.ebooks.lrf.pylrs.pylrs import Book as _Book from libprs500.ebooks.lrf.pylrs.pylrs import TextBlock, Header, PutObj, \ Paragraph, TextStyle, BlockStyle +from libprs500.ebooks.lrf.fonts import FONT_FILE_MAP from libprs500 import __version__ as VERSION +from libprs500 import iswindows __docformat__ = "epytext" __author__ = "Kovid Goyal " @@ -33,11 +37,13 @@ class PRS500_PROFILE(object): dpi = 166 # Number of pixels to subtract from screen_height when calculating height of text area fudge = 18 - font_size = 10 #: Default (in pt) - parindent = 80 #: Default (in px) - line_space = 1.2 #: Default (in pt) + font_size = 10 #: Default (in pt) + parindent = 80 #: Default (in px) + line_space = 1.2 #: Default (in pt) header_font_size = 6 #: In pt header_height = 30 #: In px + default_fonts = { 'sans': "Swis721 BT Roman", 'mono': "Courier10 BT Roman", + 'serif': "Dutch801 Rm BT Roman"} @@ -47,6 +53,20 @@ def profile_from_string(option, opt_str, value, parser): else: raise OptionValueError('Profile: '+value+' is not implemented') +def font_family(option, opt_str, value, parser): + if value: + value = value.split(',') + if len(value) != 2: + raise OptionValueError('Font family specification must be of the form'+\ + ' "path to font directory, font family"') + path, family = tuple(value) + if not os.path.isdir(path) or not os.access(path, os.R_OK|os.X_OK): + raise OptionValueError('Cannot read from ' + path) + setattr(parser.values, option.dest, (path, family)) + else: + setattr(parser.values, option.dest, tuple()) + + class ConversionError(Exception): pass @@ -98,6 +118,24 @@ def option_parser(usage): page.add_option('--bottom-margin', default=0, dest='bottom_margin', type='int', help='''Bottom margin of page. Default is %default px.''') + fonts = parser.add_option_group('FONT FAMILIES', + '''Specify trutype font families for serif, sans-serif and monospace fonts. ''' + '''These fonts will be embedded in the LRF file. Note that custom fonts lead to ''' + '''slower page turns. Each family specification is of the form: ''' + '''"path to fonts directory, family" ''' + '''For example: ''' + '''--serif-family "%s, Times New Roman" + ''' % ('C:\Windows\Fonts' if iswindows else '/usr/share/fonts/corefonts')) + fonts.add_option('--serif-family', action='callback', callback=font_family, + default=None, dest='serif_family', type='string', + help='The serif family of fonts to embed') + fonts.add_option('--sans-family', action='callback', callback=font_family, + default=None, dest='sans_family', type='string', + help='The sans-serif family of fonts to embed') + fonts.add_option('--mono-family', action='callback', callback=font_family, + default=None, dest='mono_family', type='string', + help='The monospace family of fonts to embed') + debug = parser.add_option_group('DEBUG OPTIONS') debug.add_option('--verbose', dest='verbose', action='store_true', default=False, help='''Be verbose while processing''') @@ -105,6 +143,42 @@ def option_parser(usage): help='Convert to LRS', default=False) return parser +def find_custom_fonts(options): + fonts = {'serif' : None, 'sans' : None, 'mono' : None} + def find_family(option): + path, family = option + paths = findsystem.findFonts([path]) + results = {} + for path in paths: + if len(results.keys()) == 4: + break + f = describe.openFont(path) + name, cfamily = describe.shortName(f) + if cfamily.lower().strip() != family.lower().strip(): + continue + try: + wt, italic = describe.modifiers(f) + except TTLibError: + print >>sys.stderr, 'Could not process', path + result = (path, name) + if wt == 400 and italic == 0: + results['normal'] = result + elif wt == 400 and italic > 0: + results['italic'] = result + elif wt >= 700 and italic == 0: + results['bold'] = result + elif wt >= 700 and italic > 0: + results['bi'] = result + return results + if options.serif_family: + fonts['serif'] = find_family(options.serif_family) + if options.sans_family: + fonts['sans'] = find_family(options.sans_family) + if options.mono_family: + fonts['mono'] = find_family(options.mono_family) + return fonts + + def Book(options, font_delta=0, header=None, profile=PRS500_PROFILE, **settings): ps = {} @@ -126,9 +200,27 @@ def Book(options, font_delta=0, header=None, ps['textheight'] = profile.screen_height - (options.bottom_margin + ps['topmargin'] + ps['headheight'] + profile.fudge) fontsize = int(10*profile.font_size+font_delta*20) baselineskip = fontsize + 20 - return _Book(textstyledefault=dict(fontsize=fontsize, - parindent=int(profile.parindent), - linespace=int(10*profile.line_space), - baselineskip=baselineskip), \ - pagestyledefault=ps, blockstyledefault=dict(blockwidth=ps['textwidth']), - **settings) \ No newline at end of file + fonts = find_custom_fonts(options) + tsd = dict(fontsize=fontsize, + parindent=int(profile.parindent), + linespace=int(10*profile.line_space), + baselineskip=baselineskip) + if fonts['serif'] and fonts['serif'].has_key('normal'): + tsd['fontfacename'] = fonts['serif']['normal'][1] + + book = _Book(textstyledefault=tsd, + pagestyledefault=ps, + blockstyledefault=dict(blockwidth=ps['textwidth']), + **settings) + for family in fonts.keys(): + if fonts[family]: + for font in fonts[family].values(): + book.embed_font(*font) + FONT_FILE_MAP[font[1]] = font[0] + + for family in ['serif', 'sans', 'mono']: + if not fonts[family]: + fonts[family] = { 'normal' : (None, profile.default_fonts[family]) } + elif not fonts[family].has_key('normal'): + raise ConversionError, 'Could not find the normal version of the ' + family + ' font' + return book, fonts \ No newline at end of file diff --git a/src/libprs500/ebooks/lrf/fonts/__init__.py b/src/libprs500/ebooks/lrf/fonts/__init__.py index 6487758aee..2587e2808d 100644 --- a/src/libprs500/ebooks/lrf/fonts/__init__.py +++ b/src/libprs500/ebooks/lrf/fonts/__init__.py @@ -56,4 +56,5 @@ def get_font(name, size, encoding='unic'): if name in FONT_MAP.keys(): path = get_font_path(name) return ImageFont.truetype(path, size, encoding=encoding) - \ No newline at end of file + elif name in FONT_FILE_MAP.keys(): + return ImageFont.truetype(FONT_FILE_MAP[name], size, encoding=encoding) \ No newline at end of file diff --git a/src/libprs500/ebooks/lrf/html/convert_from.py b/src/libprs500/ebooks/lrf/html/convert_from.py index adad211332..143bf760b0 100644 --- a/src/libprs500/ebooks/lrf/html/convert_from.py +++ b/src/libprs500/ebooks/lrf/html/convert_from.py @@ -81,29 +81,46 @@ class Span(_Span): return result @staticmethod - def translate_attrs(d, dpi, font_delta=0, memory=None): + def translate_attrs(d, dpi, fonts, font_delta=0, memory=None): """ Receives a dictionary of html attributes and styles and returns approximate Xylog equivalents in a new dictionary """ def font_weight(val): - ans = None + ans = 0 m = re.search("([0-9]+)", val) if m: - ans = str(int(m.group(1))) + ans = int(m.group(1)) elif val.find("bold") >= 0 or val.find("strong") >= 0: - ans = "1000" + ans = 700 + return 'bold' if ans >= 700 else 'normal' + + def font_style(val): + ans = 'normal' + if 'italic' in val or 'oblique' in val: + ans = 'italic' return ans def font_family(val): - ans = None + ans = 'serif' if max(val.find("courier"), val.find("mono"), val.find("fixed"), val.find("typewriter"))>=0: - ans = "Courier10 BT Roman" + ans = 'mono' elif max(val.find("arial"), val.find("helvetica"), val.find("verdana"), val.find("trebuchet"), val.find("sans")) >= 0: - ans = "Swis721 BT Roman" + ans = 'sans' return ans + def font_key(family, style, weight): + key = 'normal' + if style == 'italic' and weight == 'normal': + key = 'italic' + elif style == 'normal' and weight == 'bold': + key = 'bold' + elif style == 'italic' and weight == 'bold': + key = 'bi' + return key + + def font_size(val): ans = None unit = Span.unit_convert(val, dpi, 14) @@ -129,37 +146,38 @@ class Span(_Span): return ans t = dict() + family, weight, style = 'serif', 'normal', 'normal' for key in d.keys(): val = d[key].lower() if key == 'font': - val = val.split() - val.reverse() - for sval in val: - ans = font_family(sval) - if ans: - t['fontfacename'] = ans - else: - ans = font_size(sval) - if ans: - t['fontsize'] = ans - else: - ans = font_weight(sval) - if ans: - t['fontweight'] = ans + vals = val.split() + for val in vals: + family = font_family(val) + if family != 'serif': + break + for val in vals: + weight = font_weight(val) + if weight != 'normal': + break + for val in vals: + style = font_style(val) + if style != 'normal': + break + for val in vals: + sz = font_size(val) + if sz: + t['fontsize'] = sz + break elif key in ['font-family', 'font-name']: - ans = font_family(val) - if ans: - t['fontfacename'] = ans + family = font_family(val) elif key == "font-size": ans = font_size(val) if ans: t['fontsize'] = ans elif key == 'font-weight': - ans = font_weight(val) - if ans: - t['fontweight'] = ans - if int(ans) > 140: - t['wordspace'] = '50' + weight = font_weight(val) + elif key == 'font-style': + style = font_style(val) else: report = True if memory != None: @@ -169,22 +187,32 @@ class Span(_Span): memory.append(key) if report: print >>sys.stderr, 'Unhandled/malformed CSS key:', key, d[key] - return t + t['fontfacename'] = (family, font_key(family, style, weight)) + if t.has_key('fontsize') and int(t['fontsize']) > 120: + t['wordspace'] = 50 + return t - def __init__(self, ns, css, memory, dpi, font_delta=0): + def __init__(self, ns, css, memory, dpi, fonts, font_delta=0): src = ns.string if hasattr(ns, 'string') else ns src = re.sub(r'\s{2,}', ' ', src) # Remove multiple spaces for pat, repl in Span.rules: src = pat.sub(repl, src) if not src: raise ConversionError('No point in adding an empty string to a Span') - if 'font-style' in css.keys(): - fs = css.pop('font-style') - if fs.lower() == 'italic': + attrs = Span.translate_attrs(css, dpi, fonts, font_delta=font_delta, memory=memory) + family, key = attrs['fontfacename'] + if fonts[family].has_key(key): + attrs['fontfacename'] = fonts[family][key][1] + else: + attrs['fontfacename'] = fonts[family]['normal'][1] + if key in ['bold', 'bi']: + attrs['fontweight'] = 700 + if key in ['italic', 'bi']: src = Italic(src) - attrs = Span.translate_attrs(css, dpi, font_delta=font_delta, memory=memory) if 'fontsize' in attrs.keys(): attrs['baselineskip'] = int(attrs['fontsize']) + 20 + if attrs['fontfacename'] == fonts['serif']['normal'][1]: + attrs.pop('fontfacename') _Span.__init__(self, text=src, **attrs) @@ -214,7 +242,7 @@ class HTMLConverter(object): processed_files = {} #: Files that have been processed - def __init__(self, book, path, + def __init__(self, book, fonts, path, font_delta=0, verbose=False, cover=None, max_link_levels=sys.maxint, link_level=0, is_root=True, baen=False, chapter_detection=True, @@ -231,6 +259,7 @@ class HTMLConverter(object): @param book: The LRF book @type book: L{libprs500.lrf.pylrs.Book} + @param fonts: dict specifying the font families to use @param path: path to the HTML file to process @type path: C{str} @param width: Width of the device on which the LRF file is to be read @@ -278,6 +307,7 @@ class HTMLConverter(object): th = {'font-size' : 'large', 'font-weight':'bold'}, big = {'font-size' : 'large', 'font-weight':'bold'}, ) + self.fonts = fonts #: dict specifting font families to use self.profile = profile #: Defines the geometry of the display device self.chapter_detection = chapter_detection #: Flag to toggle chapter detection self.chapter_regex = chapter_regex #: Regex used to search for chapter titles @@ -553,7 +583,8 @@ class HTMLConverter(object): path = os.path.abspath(path) if not path in HTMLConverter.processed_files.keys(): try: - self.files[path] = HTMLConverter(self.book, path, + self.files[path] = HTMLConverter( + self.book, self.fonts, path, profile=self.profile, font_delta=self.font_delta, verbose=self.verbose, link_level=self.link_level+1, @@ -690,7 +721,7 @@ class HTMLConverter(object): self.process_alignment(css) try: self.current_para.append(Span(src, self.sanctify_css(css), self.memory,\ - self.profile.dpi, font_delta=self.font_delta)) + self.profile.dpi, self.fonts, font_delta=self.font_delta)) except ConversionError, err: if self.verbose: print >>sys.stderr, err @@ -949,7 +980,8 @@ class HTMLConverter(object): elif tagname == 'pre': self.end_current_para() self.current_block.append_to(self.current_page) - attrs = Span.translate_attrs(tag_css, self.profile.dpi, self.font_delta, self.memory) + attrs = Span.translate_attrs(tag_css, self.profile.dpi, self.fonts, self.font_delta, self.memory) + attrs['fontfacename'] = self.fonts['mono']['normal'][1] ts = self.book.create_text_style(**self.unindented_style.attrs) ts.attrs.update(attrs) self.current_block = self.book.create_text_block( @@ -959,7 +991,7 @@ class HTMLConverter(object): lines = src.split('\n') for line in lines: try: - self.current_para.append(Span(line, tag_css, self.memory, self.profile.dpi)) + self.current_para.append(Span(line, tag_css, self.memory, self.profile.dpi, self.fonts)) self.current_para.CR() except ConversionError: pass @@ -1145,14 +1177,14 @@ def process_file(path, options): header.append(Bold(options.title)) header.append(' by ') header.append(Italic(options.author+" ")) - book = Book(options, header=header, **args) + book, fonts = Book(options, header=header, **args) le = re.compile(options.link_exclude) if options.link_exclude else \ re.compile('$') pb = re.compile(options.page_break, re.IGNORECASE) if options.page_break else \ re.compile('$') fpb = re.compile(options.force_page_break, re.IGNORECASE) if options.force_page_break else \ re.compile('$') - conv = HTMLConverter(book, path, profile=options.profile, + conv = HTMLConverter(book, fonts, path, profile=options.profile, font_delta=options.font_delta, cover=cpath, max_link_levels=options.link_levels, verbose=options.verbose, baen=options.baen, diff --git a/src/libprs500/ebooks/lrf/html/demo/demo.html b/src/libprs500/ebooks/lrf/html/demo/demo.html index b48c2f3acc..a79aabb94c 100644 --- a/src/libprs500/ebooks/lrf/html/demo/demo.html +++ b/src/libprs500/ebooks/lrf/html/demo/demo.html @@ -8,7 +8,7 @@

Demo of html2lrf

- This file contains a demonstration of the capabilities of html2lrf, the HTML to LRF converter from libprs500. To obtain libprs500 visit
https://libprs500.kovidgoyal.net + This file contains a demonstration of the capabilities of html2lrf, the HTML to LRF converter from libprs500. To obtain libprs500 visit
https://libprs500.kovidgoyal.net


Table of Contents

@@ -17,18 +17,21 @@
  • Tables
  • Text formatting and ruled lines
  • Inline images
  • +
  • Embedded Fonts
  • Recursive link following
  • The HTML used to create this file

    Lists

    -

    Unordered lists

    +

    +

    Unordered lists

    -

    Ordered lists

    +

    +

    Ordered lists

    1. Item 1
    2. Item 2
    3. @@ -36,7 +39,7 @@


      - Note that nested lists are not supported. + Note that nested lists are not supported.


      @@ -94,7 +97,7 @@

      Text formatting

      A simple paragraph of formatted - text with a ruled line following it. + text, with a ruled line following it.


      A @@ -113,7 +116,7 @@


      A very indented paragraph

      An unindented paragraph

      -

      A default indented paragrpah

      +

      A default indented paragrpah


      Table of Contents @@ -128,9 +131,29 @@ Table of Contents

      +

      Embedded fonts

      +

      This LRF file has been prepared by embedding Times New Roman and Andale Mono + as the default serif and monospace fonts. This allows it to correctly display + non English characters such as:

      +
        +
      • mouse in German: mūs
      • +
      • mouse in Russian: мышь
      • +
      +

      + Note that embedding fonts in LRF files slows down page turns slightly. +
      +

      + +

      +


      + Table of Contents +

      + +

      Recursive link following

      html2lrf follows links in HTML files that point to other files, recursively. Thus it can be used to convert a whole tree of HTML files into a single LRF file. +


      diff --git a/src/libprs500/ebooks/lrf/pylrs/pylrs.py b/src/libprs500/ebooks/lrf/pylrs/pylrs.py index 06ecda8238..54ed51776c 100644 --- a/src/libprs500/ebooks/lrf/pylrs/pylrs.py +++ b/src/libprs500/ebooks/lrf/pylrs/pylrs.py @@ -503,6 +503,10 @@ class Book(Delegator): if isinstance(candidate, Page): return candidate + def embed_font(self, file, facename): + f = Font(file, facename) + self.append(f) + def getSettings(self): return ["sourceencoding"] diff --git a/src/libprs500/gui/widgets.py b/src/libprs500/gui/widgets.py index a23d47eb9c..7c2f304da3 100644 --- a/src/libprs500/gui/widgets.py +++ b/src/libprs500/gui/widgets.py @@ -700,7 +700,7 @@ class DeviceBooksModel(QAbstractTableModel): if col == 0: text = TableView.wrap(book.title, width=40) elif col == 1: - au = book.author + au = book.authors au = au.split("&") jau = [ TableView.wrap(a, width=25).strip() for a in au ] text = "\n".join(jau) @@ -723,7 +723,7 @@ class DeviceBooksModel(QAbstractTableModel): cover = None if pix.isNull() else pix except: traceback.print_exc() - au = row.author if row.author else "Unknown" + au = row.authors if row.authors else "Unknown" return row.title, au, TableView.human_readable(row.size), row.mime, cover def sort(self, col, order): diff --git a/src/libprs500/gui2/device.py b/src/libprs500/gui2/device.py index 0b29580984..6df91b58f3 100644 --- a/src/libprs500/gui2/device.py +++ b/src/libprs500/gui2/device.py @@ -75,8 +75,7 @@ class DeviceJob(QThread): self.id, self.result, exception, last_traceback) def progress_update(self, val): - print val - self.emit(SIGNAL('status_update(int)'), int(val), Qt.QueuedConnection) + self.emit(SIGNAL('status_update(int)'), int(val)) class DeviceManager(QObject): @@ -85,7 +84,7 @@ class DeviceManager(QObject): self.device_class = device_class self.device = device_class() - def get_info_func(self): + def info_func(self): ''' Return callable that returns device information and free space on device''' def get_device_information(updater): self.device.set_progress_reporter(updater) @@ -102,5 +101,12 @@ class DeviceManager(QObject): self.device.set_progress_reporter(updater) mainlist = self.device.books(oncard=False, end_session=False) cardlist = self.device.books(oncard=True) - return mainlist, cardlist - return books \ No newline at end of file + return (mainlist, cardlist) + return books + + def sync_booklists_func(self): + '''Upload booklists to device''' + def sync_booklists(updater, booklists): + self.device.set_progress_reporter(updater) + self.device.sync_booklists(booklists) + return sync_booklists \ No newline at end of file diff --git a/src/libprs500/gui2/jobs.py b/src/libprs500/gui2/jobs.py index 96b05e3433..fb2684907a 100644 --- a/src/libprs500/gui2/jobs.py +++ b/src/libprs500/gui2/jobs.py @@ -39,6 +39,7 @@ class JobManager(QAbstractTableModel): job = job_class(self.next_id, lock, *args, **kwargs) QObject.connect(job, SIGNAL('finished()'), self.cleanup_jobs) self.jobs[self.next_id] = job + self.emit(SIGNAL('job_added(int)'), self.next_id) return job finally: self.job_create_lock.unlock() @@ -55,8 +56,9 @@ class JobManager(QAbstractTableModel): job = self.create_job(DeviceJob, self.device_lock, callable, *args, **kwargs) QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), self.job_done) - QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), - slot) + if slot: + QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), + slot) job.start() def job_done(self, id, *args, **kwargs): @@ -69,6 +71,8 @@ class JobManager(QAbstractTableModel): self.cleanup_lock.lock() self.cleanup[id] = job self.cleanup_lock.unlock() + if len(self.jobs.keys()) == 0: + self.emit(SIGNAL('no_more_jobs()')) finally: self.job_remove_lock.unlock() diff --git a/src/libprs500/gui2/library.py b/src/libprs500/gui2/library.py index f8ad7c4a68..e8ac781046 100644 --- a/src/libprs500/gui2/library.py +++ b/src/libprs500/gui2/library.py @@ -13,7 +13,8 @@ ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os, textwrap, traceback, time, re -from datetime import timedelta +from datetime import timedelta, datetime +from operator import attrgetter from math import cos, sin, pi from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \ QItemDelegate, QPainterPath, QLinearGradient, QBrush, \ @@ -76,66 +77,6 @@ class LibraryDelegate(QItemDelegate): except Exception, e: traceback.print_exc(e) painter.restore() - -class BooksView(QTableView): - wrapper = textwrap.TextWrapper(width=20) - - @classmethod - def wrap(cls, s, width=20): - cls.wrapper.width = width - return cls.wrapper.fill(s) - - @classmethod - def human_readable(cls, size): - """ Convert a size in bytes into a human readable form """ - if size < 1024: - divisor, suffix = 1, "B" - elif size < 1024*1024: - divisor, suffix = 1024., "KB" - elif size < 1024*1024*1024: - divisor, suffix = 1024*1024, "MB" - elif size < 1024*1024*1024*1024: - divisor, suffix = 1024*1024, "GB" - size = str(size/divisor) - if size.find(".") > -1: - size = size[:size.find(".")+2] - return size + " " + suffix - - def __init__(self, parent): - QTableView.__init__(self, parent) - self.display_parent = parent - self.model = BooksModel(self) - self.setModel(self.model) - self.setSelectionBehavior(QAbstractItemView.SelectRows) - self.setSortingEnabled(True) - self.setItemDelegateForColumn(4, LibraryDelegate(self)) - QObject.connect(self.model, SIGNAL('sorted()'), self.resizeRowsToContents) - QObject.connect(self.model, SIGNAL('searched()'), self.resizeRowsToContents) - self.verticalHeader().setVisible(False) - - def set_database(self, db): - self.model.set_database(db) - - def migrate_database(self): - if self.model.database_needs_migration(): - print 'Migrating database from pre 0.4.0 version' - path = os.path.abspath(os.path.expanduser('~/library.db')) - progress = QProgressDialog('Upgrading database from pre 0.4.0 version.
      '+\ - 'The new database is stored in the file '+self.model.db.dbpath, - QString(), 0, LibraryDatabase.sizeof_old_database(path), - self) - progress.setModal(True) - app = QCoreApplication.instance() - def meter(count): - progress.setValue(count) - app.processEvents() - progress.setWindowTitle('Upgrading database') - progress.show() - LibraryDatabase.import_old_database(path, self.model.db.conn, meter) - - def connect_to_search_box(self, sb): - QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), self.model.search) - class BooksModel(QAbstractTableModel): @@ -150,7 +91,7 @@ class BooksModel(QAbstractTableModel): db = LibraryDatabase(os.path.expanduser(str(db))) self.db = db - def search(self, text, refinement): + def search_tokens(self, text): tokens = [] quot = re.search('"(.*?)"', text) while quot: @@ -158,6 +99,10 @@ class BooksModel(QAbstractTableModel): text = text.replace('"'+quot.group(1)+'"', '') quot = re.search('"(.*?)"', text) tokens += text.split(' ') + return [re.compile(i, re.IGNORECASE) for i in tokens] + + def search(self, text, refinement): + tokens = self.search_tokens(text) self.db.filter(tokens, refinement) self.reset() self.emit(SIGNAL('searched()')) @@ -204,7 +149,7 @@ class BooksModel(QAbstractTableModel): dt = self.db.timestamp(row) if dt: dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight) - return QVariant(dt.strftime('%d %b %Y')) + return QVariant(dt.strftime(BooksView.TIME_FMT)) elif col == 4: r = self.db.rating(row) r = r/2 if r else 0 @@ -216,13 +161,9 @@ class BooksModel(QAbstractTableModel): return NONE elif role == Qt.TextAlignmentRole and index.column() in [2, 3, 4]: return QVariant(Qt.AlignRight | Qt.AlignVCenter) - elif role == Qt.ToolTipRole and index.isValid(): + elif role == Qt.ToolTipRole and index.isValid(): if index.column() in [0, 1, 4, 5]: - edit = "Double click to edit me

      " - else: - edit = "" - return QVariant(edit + "You can drag and drop me to the \ - desktop to save all my formats to your hard disk.") + return QVariant("Double click to edit me

      ") return NONE def headerData(self, section, orientation, role): @@ -232,13 +173,13 @@ class BooksModel(QAbstractTableModel): if orientation == Qt.Horizontal: if section == 0: text = "Title" elif section == 1: text = "Author(s)" - elif section == 2: text = "Size" + elif section == 2: text = "Size (MB)" elif section == 3: text = "Date" elif section == 4: text = "Rating" elif section == 5: text = "Publisher" return QVariant(self.trUtf8(text)) else: - return NONE + return QVariant(section+1) def flags(self, index): flags = QAbstractTableModel.flags(self, index) @@ -266,11 +207,202 @@ class BooksModel(QAbstractTableModel): self.sort(col, self.sorted_on[1]) done = True return done + + +class BooksView(QTableView): + TIME_FMT = '%d %b %Y' + wrapper = textwrap.TextWrapper(width=20) + @classmethod + def wrap(cls, s, width=20): + cls.wrapper.width = width + return cls.wrapper.fill(s) + + @classmethod + def human_readable(cls, size, precision=1): + """ Convert a size in bytes into megabytes """ + return ('%.'+str(precision)+'f') % ((size/(1024.*1024.)),) + + def __init__(self, parent, modelcls=BooksModel): + QTableView.__init__(self, parent) + self.display_parent = parent + self._model = modelcls(self) + self.setModel(self._model) + self.setSelectionBehavior(QAbstractItemView.SelectRows) + self.setSortingEnabled(True) + self.setItemDelegateForColumn(4, LibraryDelegate(self)) + QObject.connect(self._model, SIGNAL('sorted()'), self.resizeRowsToContents) + QObject.connect(self._model, SIGNAL('searched()'), self.resizeRowsToContents) + #self.verticalHeader().setVisible(False) + + def set_database(self, db): + self._model.set_database(db) + + def migrate_database(self): + if self._model.database_needs_migration(): + print 'Migrating database from pre 0.4.0 version' + path = os.path.abspath(os.path.expanduser('~/library.db')) + progress = QProgressDialog('Upgrading database from pre 0.4.0 version.
      '+\ + 'The new database is stored in the file '+self._model.db.dbpath, + QString(), 0, LibraryDatabase.sizeof_old_database(path), + self) + progress.setModal(True) + app = QCoreApplication.instance() + def meter(count): + progress.setValue(count) + app.processEvents() + progress.setWindowTitle('Upgrading database') + progress.show() + LibraryDatabase.import_old_database(path, self._model.db.conn, meter) + + def connect_to_search_box(self, sb): + QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), self._model.search) + +class DeviceBooksView(BooksView): + + def __init__(self, parent): + BooksView.__init__(self, parent, DeviceBooksModel) + self.columns_resized = False + + def resizeColumnsToContents(self): + QTableView.resizeColumnsToContents(self) + self.columns_resized = True + + def connect_dirtied_signal(self, slot): + QObject.connect(self._model, SIGNAL('booklist_dirtied()'), slot) + +class DeviceBooksModel(BooksModel): + + def __init__(self, parent): + QAbstractTableModel.__init__(self, parent) + self.db = [] + self.map = [] + self.sorted_map = [] + self.unknown = str(self.trUtf8('Unknown')) + + def search(self, text, refinement): + tokens = self.search_tokens(text) + base = self.map if refinement else self.sorted_map + result = [] + for i in base: + add = True + q = self.db[i].title + ' ' + self.db[i].authors + for token in tokens: + if not token.search(q): + add = False + break + if add: + result.append(i) + self.map = result + self.reset() + self.emit(SIGNAL('searched()')) + + def sort(self, col, order): + if not self.db: + return + descending = order != Qt.AscendingOrder + def strcmp(attr): + ag = attrgetter(attr) + def _strcmp(x, y): + x = ag(self.db[x]) + y = ag(self.db[y]) + if x == None: + x = '' + if y == None: + y = '' + x, y = x.strip().lower(), y.strip().lower() + return cmp(x, y) + return _strcmp + def datecmp(x, y): + x = self.db[x].datetime + y = self.db[y].datetime + return cmp(datetime(*x[0:6]), datetime(*y[0:6])) + def sizecmp(x, y): + x, y = int(self.db[x].size), int(self.db[y].size) + return cmp(x, y) + fcmp = strcmp('title_sorter') if col == 0 else strcmp('authors') if col == 1 else \ + sizecmp if col == 2 else datecmp + self.map.sort(cmp=fcmp, reverse=descending) + if len(self.map) == len(self.db): + self.sorted_map = list(self.map) + else: + self.sorted_map = list(range(len(self.db))) + self.sorted_map.sort(cmp=fcmp, reverse=descending) + self.sorted_on = (col, order) + self.reset() + self.emit(SIGNAL('sorted()')) + + def columnCount(self, parent): + return 4 + + def rowCount(self, parent): + return len(self.map) + + def set_database(self, db): + self.db = db + self.map = list(range(0, len(db))) + + def data(self, index, role): + if role == Qt.DisplayRole or role == Qt.EditRole: + row, col = index.row(), index.column() + if col == 0: + text = self.db[self.map[row]].title + if not text: + text = self.unknown + return QVariant(BooksView.wrap(text, width=35)) + elif col == 1: + au = self.db[self.map[row]].authors + if not au: + au = self.unknown + if role == Qt.EditRole: + return QVariant(au) + au = au.split(',') + authors = [] + for i in au: + authors += i.strip().split('&') + jau = [ BooksView.wrap(a.strip(), width=30).strip() for a in authors ] + return QVariant("\n".join(jau)) + elif col == 2: + size = self.db[self.map[row]].size + return QVariant(BooksView.human_readable(size)) + elif col == 3: + dt = self.db[self.map[row]].datetime + dt = datetime(*dt[0:6]) + dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight) + return QVariant(dt.strftime(BooksView.TIME_FMT)) + elif role == Qt.TextAlignmentRole and index.column() in [2, 3]: + return QVariant(Qt.AlignRight | Qt.AlignVCenter) + elif role == Qt.ToolTipRole and index.isValid(): + if index.column() in [0, 1]: + return QVariant("Double click to edit me

      ") + return NONE + + def setData(self, index, value, role): + done = False + if role == Qt.EditRole: + row, col = index.row(), index.column() + if col in [2, 3]: + return False + val = unicode(value.toString().toUtf8(), 'utf-8').strip() + idx = self.map[row] + if col == 0: + self.db.title = val + self.db.title_sorter = val + elif col == 1: + self.db.authors = val + self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \ + index, index) + self.emit(SIGNAL('booklist_dirtied()')) + if col == self.sorted_on[0]: + self.sort(col, self.sorted_on[1]) + done = True + return done + class SearchBox(QLineEdit): def __init__(self, parent): QLineEdit.__init__(self, parent) - self.setText('Search by title, author, publisher, tags and comments') + self.help_text = 'Search by title, author, publisher, tags and comments' + self.setText(self.help_text) self.home(False) QObject.connect(self, SIGNAL('textEdited(QString)'), self.text_edited_slot) self.default_palette = QApplication.palette(self) @@ -282,12 +414,21 @@ class SearchBox(QLineEdit): self.timer = None self.interval = 1000 #: Time to wait before emitting search signal + def normalize_state(self): + self.setText('') + self.setPalette(self.default_palette) + def keyPressEvent(self, event): if self.initial_state: - self.setText('') + self.normalize_state() self.initial_state = False - self.setPalette(self.default_palette) QLineEdit.keyPressEvent(self, event) + + def mouseReleaseEvent(self, event): + if self.initial_state: + self.normalize_state() + self.initial_state = False + QLineEdit.mouseReleaseEvent(self, event) def text_edited_slot(self, text): text = str(text) diff --git a/src/libprs500/gui2/main.py b/src/libprs500/gui2/main.py index c3d6ade3aa..6326e81e90 100644 --- a/src/libprs500/gui2/main.py +++ b/src/libprs500/gui2/main.py @@ -15,11 +15,8 @@ import os, tempfile, sys from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \ - QSettings, QVariant, QSize, QEventLoop, QString, \ - QBuffer, QIODevice, QModelIndex, QThread -from PyQt4.QtGui import QPixmap, QErrorMessage, QLineEdit, \ - QMessageBox, QFileDialog, QIcon, QDialog, QInputDialog -from PyQt4.Qt import qDebug, qFatal, qWarning, qCritical + QSettings, QVariant, QSize, QThread +from PyQt4.QtGui import QErrorMessage from libprs500 import __version__ as VERSION from libprs500.gui2 import APP_TITLE, installErrorHandler @@ -40,6 +37,10 @@ class Main(QObject, Ui_MainWindow): self.device_manager = None self.temporary_slots = {} + ####################### Location View ######################## + QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'), + self.location_selected) + ####################### Vanity ######################## self.vanity_template = self.vanity.text().arg(VERSION) self.vanity.setText(self.vanity_template.arg(' ')) @@ -47,10 +48,17 @@ class Main(QObject, Ui_MainWindow): ####################### Status Bar ##################### self.status_bar = StatusBar() self.window.setStatusBar(self.status_bar) + QObject.connect(self.job_manager, SIGNAL('job_added(int)'), self.status_bar.job_added) + QObject.connect(self.job_manager, SIGNAL('no_more_jobs()'), self.status_bar.no_more_jobs) - ####################### Setup books view ######################## + + ####################### Library view ######################## self.library_view.set_database(self.database_path) self.library_view.connect_to_search_box(self.search) + self.memory_view.connect_to_search_box(self.search) + self.card_view.connect_to_search_box(self.search) + self.memory_view.connect_dirtied_signal(self.upload_booklists) + self.card_view.connect_dirtied_signal(self.upload_booklists) window.closeEvent = self.close_event window.show() @@ -67,16 +75,28 @@ class Main(QObject, Ui_MainWindow): self.detector.start(QThread.InheritPriority) + def upload_booklists(self): + booklists = self.memory_view.model().db, self.card_view.model().db + self.job_manager.run_device_job(None, self.device_manager.sync_booklists_func(), + booklists) + + def location_selected(self, location): + page = 0 if location == 'library' else 1 if location == 'main' else 2 + self.stack.setCurrentIndex(page) + if page == 1: + self.memory_view.resizeRowsToContents() + self.memory_view.resizeColumnsToContents() + if page == 2: + self.card_view.resizeRowsToContents() + self.card_view.resizeColumnsToContents() + def job_exception(self, id, exception, formatted_traceback): raise JobException, str(exception) + '\n\r' + formatted_traceback def device_detected(self, cls, connected): - if connected: - - + if connected: self.device_manager = DeviceManager(cls) - func = self.device_manager.get_info_func() - + func = self.device_manager.info_func() self.job_manager.run_device_job(self.info_read, func) def info_read(self, id, result, exception, formatted_traceback): @@ -86,6 +106,20 @@ class Main(QObject, Ui_MainWindow): info, cp, fs = result self.location_view.model().update_devices(cp, fs) self.vanity.setText(self.vanity_template.arg('Connected '+' '.join(info[:-1]))) + func = self.device_manager.books_func() + self.job_manager.run_device_job(self.books_read, func) + + def books_read(self, id, result, exception, formatted_traceback): + if exception: + self.job_exception(id, exception, formatted_traceback) + return + mainlist, cardlist = result + self.memory_view.set_database(mainlist) + self.card_view.set_database(cardlist) + self.memory_view.sortByColumn(3, Qt.DescendingOrder) + self.card_view.sortByColumn(3, Qt.DescendingOrder) + self.location_selected('main') + def read_settings(self): diff --git a/src/libprs500/gui2/main.ui b/src/libprs500/gui2/main.ui index 8382fc0374..7f2f9d4d0e 100644 --- a/src/libprs500/gui2/main.ui +++ b/src/libprs500/gui2/main.ui @@ -170,7 +170,7 @@ - + 100 @@ -178,7 +178,7 @@ - 0 + 2 @@ -218,7 +218,42 @@ - + + + + 100 + 10 + + + + true + + + true + + + false + + + QAbstractItemView::DragDrop + + + true + + + QAbstractItemView::SelectRows + + + false + + + + + + + + + 100 @@ -347,6 +382,11 @@ QListView
      widgets.h
      + + DeviceBooksView + QTableView +
      library.h
      +
      diff --git a/src/libprs500/gui2/main_ui.py b/src/libprs500/gui2/main_ui.py index 784cb071da..eb1bc7cb04 100644 --- a/src/libprs500/gui2/main_ui.py +++ b/src/libprs500/gui2/main_ui.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'main.ui' # -# Created: Fri Jun 22 16:40:15 2007 +# Created: Wed Jun 27 16:19:53 2007 # by: PyQt4 UI code generator 4-snapshot-20070606 # # WARNING! All changes made in this file will be lost! @@ -86,14 +86,14 @@ class Ui_MainWindow(object): self.hboxlayout1.addWidget(self.clear_button) self.gridlayout.addLayout(self.hboxlayout1,1,0,1,1) - self.stacks = QtGui.QStackedWidget(self.centralwidget) + self.stack = QtGui.QStackedWidget(self.centralwidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(100) sizePolicy.setVerticalStretch(100) - sizePolicy.setHeightForWidth(self.stacks.sizePolicy().hasHeightForWidth()) - self.stacks.setSizePolicy(sizePolicy) - self.stacks.setObjectName("stacks") + sizePolicy.setHeightForWidth(self.stack.sizePolicy().hasHeightForWidth()) + self.stack.setSizePolicy(sizePolicy) + self.stack.setObjectName("stack") self.library = QtGui.QWidget() self.library.setObjectName("library") @@ -117,7 +117,7 @@ class Ui_MainWindow(object): self.library_view.setShowGrid(False) self.library_view.setObjectName("library_view") self.vboxlayout.addWidget(self.library_view) - self.stacks.addWidget(self.library) + self.stack.addWidget(self.library) self.main_memory = QtGui.QWidget() self.main_memory.setObjectName("main_memory") @@ -125,24 +125,48 @@ class Ui_MainWindow(object): self.gridlayout1 = QtGui.QGridLayout(self.main_memory) self.gridlayout1.setObjectName("gridlayout1") - self.main_memory_view = BooksView(self.main_memory) + self.memory_view = DeviceBooksView(self.main_memory) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(100) sizePolicy.setVerticalStretch(10) - sizePolicy.setHeightForWidth(self.main_memory_view.sizePolicy().hasHeightForWidth()) - self.main_memory_view.setSizePolicy(sizePolicy) - self.main_memory_view.setAcceptDrops(True) - self.main_memory_view.setDragEnabled(True) - self.main_memory_view.setDragDropOverwriteMode(False) - self.main_memory_view.setDragDropMode(QtGui.QAbstractItemView.DragDrop) - self.main_memory_view.setAlternatingRowColors(True) - self.main_memory_view.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) - self.main_memory_view.setShowGrid(False) - self.main_memory_view.setObjectName("main_memory_view") - self.gridlayout1.addWidget(self.main_memory_view,0,0,1,1) - self.stacks.addWidget(self.main_memory) - self.gridlayout.addWidget(self.stacks,2,0,1,1) + sizePolicy.setHeightForWidth(self.memory_view.sizePolicy().hasHeightForWidth()) + self.memory_view.setSizePolicy(sizePolicy) + self.memory_view.setAcceptDrops(True) + self.memory_view.setDragEnabled(True) + self.memory_view.setDragDropOverwriteMode(False) + self.memory_view.setDragDropMode(QtGui.QAbstractItemView.DragDrop) + self.memory_view.setAlternatingRowColors(True) + self.memory_view.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) + self.memory_view.setShowGrid(False) + self.memory_view.setObjectName("memory_view") + self.gridlayout1.addWidget(self.memory_view,0,0,1,1) + self.stack.addWidget(self.main_memory) + + self.page = QtGui.QWidget() + self.page.setObjectName("page") + + self.gridlayout2 = QtGui.QGridLayout(self.page) + self.gridlayout2.setObjectName("gridlayout2") + + self.card_view = DeviceBooksView(self.page) + + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(100) + sizePolicy.setVerticalStretch(10) + sizePolicy.setHeightForWidth(self.card_view.sizePolicy().hasHeightForWidth()) + self.card_view.setSizePolicy(sizePolicy) + self.card_view.setAcceptDrops(True) + self.card_view.setDragEnabled(True) + self.card_view.setDragDropOverwriteMode(False) + self.card_view.setDragDropMode(QtGui.QAbstractItemView.DragDrop) + self.card_view.setAlternatingRowColors(True) + self.card_view.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) + self.card_view.setShowGrid(False) + self.card_view.setObjectName("card_view") + self.gridlayout2.addWidget(self.card_view,0,0,1,1) + self.stack.addWidget(self.page) + self.gridlayout.addWidget(self.stack,2,0,1,1) MainWindow.setCentralWidget(self.centralwidget) self.tool_bar = QtGui.QToolBar(MainWindow) @@ -178,7 +202,7 @@ class Ui_MainWindow(object): self.label.setBuddy(self.search) self.retranslateUi(MainWindow) - self.stacks.setCurrentIndex(0) + self.stack.setCurrentIndex(2) QtCore.QObject.connect(self.clear_button,QtCore.SIGNAL("clicked()"),self.search.clear) QtCore.QMetaObject.connectSlotsByName(MainWindow) @@ -198,5 +222,5 @@ class Ui_MainWindow(object): self.action_edit.setShortcut(QtGui.QApplication.translate("MainWindow", "E", None, QtGui.QApplication.UnicodeUTF8)) from widgets import LocationView -from library import BooksView, SearchBox +from library import BooksView, DeviceBooksView, SearchBox import images_rc diff --git a/src/libprs500/gui2/status.py b/src/libprs500/gui2/status.py index 432413d05a..552c58573c 100644 --- a/src/libprs500/gui2/status.py +++ b/src/libprs500/gui2/status.py @@ -56,7 +56,7 @@ class MovieButton(QLabel): self.movie = movie self.setMovie(movie) self.movie.start() - self.movie.stop() + self.movie.setPaused(True) class StatusBar(QStatusBar): def __init__(self): @@ -66,6 +66,16 @@ class StatusBar(QStatusBar): self.book_info = BookInfoDisplay() self.addWidget(self.book_info) + def job_added(self, id): + if self.movie_button.movie.state() == QMovie.Paused: + self.movie_button.movie.setPaused(False) + + def no_more_jobs(self): + if self.movie_button.movie.state() == QMovie.Running: + self.movie_button.movie.setPaused(True) + self.movie_button.movie.jumpToFrame(0) # This causes MNG error 11, but seems to work regardless + + if __name__ == '__main__': # Used to create the animated status icon from PyQt4.Qt import QApplication, QPainter, QSvgRenderer, QPixmap, QColor @@ -99,7 +109,6 @@ if __name__ == '__main__': pixmaps[i].save(name, 'PNG') filesc = ' '.join(filesl) cmd = 'convert -dispose Background -delay '+str(delay)+ ' ' + filesc + ' -loop 0 animated.mng' - print cmd try: check_call(cmd, shell=True) finally: diff --git a/src/libprs500/gui2/widgets.py b/src/libprs500/gui2/widgets.py index 5a9b92722c..45e6388e9e 100644 --- a/src/libprs500/gui2/widgets.py +++ b/src/libprs500/gui2/widgets.py @@ -16,7 +16,7 @@ Miscellanous widgets used in the GUI ''' from PyQt4.QtGui import QListView, QIcon, QFont -from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, QSize, SIGNAL +from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, QSize, SIGNAL, QObject from libprs500.gui2 import human_readable, NONE @@ -60,8 +60,7 @@ class LocationModel(QAbstractListModel): self.free[1] = max(fs[1:]) if cp == None: self.free[1] = -1 - print self.free, self.rowCount(None) - self.reset() + self.reset() class LocationView(QListView): @@ -69,4 +68,12 @@ class LocationView(QListView): QListView.__init__(self, parent) self.setModel(LocationModel(self)) self.reset() + QObject.connect(self.selectionModel(), SIGNAL('currentChanged(QModelIndex, QModelIndex)'), self.current_changed) + + + def current_changed(self, current, previous): + i = current.row() + location = 'library' if i == 0 else 'main' if i == 1 else 'card' + self.emit(SIGNAL('location_selected(PyQt_PyObject)'), location) + diff --git a/src/libprs500/library/database.py b/src/libprs500/library/database.py index 01cabfc34e..c366829abd 100644 --- a/src/libprs500/library/database.py +++ b/src/libprs500/library/database.py @@ -595,14 +595,13 @@ class LibraryDatabase(object): ''' Filter data based on filters. All the filters must match for an item to be accepted. Matching is case independent regexp matching. - @param filters: A list of strings suitable for compilation into regexps + @param filters: A list of compiled regexps @param refilter: If True filters are applied to the results of the previous filtering. ''' if not filters: self.data = self.data if refilter else self.cache else: - filters = [re.compile(i, re.IGNORECASE) for i in filters if i] matches = [] for item in self.data if refilter else self.cache: keep = True