From ed2b94ac9d98be1ed3564c36071b62e6335ea60d Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 5 Feb 2011 10:46:32 -0500 Subject: [PATCH 01/20] Heuristics: Tweak italicize patterns to make them more robust. --- src/calibre/ebooks/conversion/utils.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/calibre/ebooks/conversion/utils.py b/src/calibre/ebooks/conversion/utils.py index 63eca10714..e8e2a82949 100644 --- a/src/calibre/ebooks/conversion/utils.py +++ b/src/calibre/ebooks/conversion/utils.py @@ -149,17 +149,17 @@ class HeuristicProcessor(object): ] ITALICIZE_STYLE_PATS = [ - r'(?msu)(?<=\s)_(?P\S[^_]{0,40}?\S)?_(?=[\s\.,\!\?])', - r'(?msu)(?<=\s)/(?P\S[^/]{0,40}?\S)?/(?=[\s\.,\!\?])', - r'(?msu)(?<=\s)~~(?P\S[^~]{0,40}?\S)?~~(?=[\s\.,\!\?])', - r'(?msu)(?<=\s)\*(?P\S[^\*]{0,40}?\S)?\*(?=[\s\.,\!\?])', - r'(?msu)(?<=\s)~(?P\S[^~]{0,40}?\S)?~(?=[\s\.,\!\?])', - r'(?msu)(?<=\s)_/(?P\S[^/_]{0,40}?\S)?/_(?=[\s\.,\!\?])', - r'(?msu)(?<=\s)_\*(?P\S[^\*_]{0,40}?\S)?\*_(?=[\s\.,\!\?])', - r'(?msu)(?<=\s)\*/(?P\S[^/\*]{0,40}?\S)?/\*(?=[\s\.,\!\?])', - r'(?msu)(?<=\s)_\*/(?P\S[^\*_]{0,40}?\S)?/\*_(?=[\s\.,\!\?])', - r'(?msu)(?<=\s)/:(?P\S[^:/]{0,40}?\S)?:/(?=[\s\.,\!\?])', - r'(?msu)(?<=\s)\|:(?P\S[^:\|]{0,40}?\S)?:\|(?=[\s\.,\!\?])', + r'(?msu)(?<=[\s>"])_(?P[^_]+)?_', + r'(?msu)(?<=[\s>"])/(?P[^/]+)?/', + r'(?msu)(?<=[\s>"])~~(?P[^~]+)?~~', + r'(?msu)(?<=[\s>"])\*(?P[^\*]+)?\*', + r'(?msu)(?<=[\s>"])~(?P[^~]+)?~', + r'(?msu)(?<=[\s>"])_/(?P[^/_]+)?/_', + r'(?msu)(?<=[\s>"])_\*(?P[^\*_]+)?\*_', + r'(?msu)(?<=[\s>"])\*/(?P[^/\*]+)?/\*', + r'(?msu)(?<=[\s>"])_\*/(?P[^\*_]+)?/\*_', + r'(?msu)(?<=[\s>"])/:(?P[^:/]+)?:/', + r'(?msu)(?<=[\s>"])\|:(?P[^:\|]+)?:\|', ] for word in ITALICIZE_WORDS: From deee20d8f85010059019acfdfd4d6c719711ec73 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 5 Feb 2011 11:02:00 -0500 Subject: [PATCH 02/20] TXT Output: Fix inline toc not showing all items. --- src/calibre/ebooks/txt/txtml.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/txt/txtml.py b/src/calibre/ebooks/txt/txtml.py index c2ee3f37c5..fa7bfbb380 100644 --- a/src/calibre/ebooks/txt/txtml.py +++ b/src/calibre/ebooks/txt/txtml.py @@ -55,6 +55,7 @@ class TXTMLizer(object): self.log.info('Converting XHTML to TXT...') self.oeb_book = oeb_book self.opts = opts + self.toc_titles = [] self.toc_ids = [] self.last_was_heading = False @@ -94,8 +95,8 @@ class TXTMLizer(object): if getattr(self.opts, 'inline_toc', None): self.log.debug('Generating table of contents...') toc.append(u'%s\n\n' % _(u'Table of Contents:')) - for item in self.oeb_book.toc: - toc.append(u'* %s\n\n' % item.title) + for item in self.toc_titles: + toc.append(u'* %s\n\n' % item) return ''.join(toc) def create_flat_toc(self, nodes): @@ -103,6 +104,7 @@ class TXTMLizer(object): Turns a hierarchical list of TOC href's into a flat list. ''' for item in nodes: + self.toc_titles.append(item.title) self.toc_ids.append(item.href) self.create_flat_toc(item.nodes) From ee420551cb7206b683f359b879ed40f69c505cc2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 5 Feb 2011 09:46:52 -0700 Subject: [PATCH 03/20] FIx regression that broke the convenience Email to xxx entry in the connect share menu. Fixes #8775 (0.7.44 - Problem with sending via email function - seems not to work as 0.7.43) --- src/calibre/gui2/actions/device.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index b32568f8fd..429bc641d0 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -94,6 +94,7 @@ class ShareConnMenu(QMenu): # {{{ I('mail.png'), _('Email to') + ' ' +account) self.addAction(ac) self.email_actions.append(ac) + ac.a_s.connect(sync_menu.action_triggered) action1.a_s.connect(sync_menu.action_triggered) action2.a_s.connect(sync_menu.action_triggered) ac = self.addMenu(self.email_to_and_delete_menu) From b5fd4a07d893e87c1e8c140aafb372c8e5624303 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 5 Feb 2011 10:00:33 -0700 Subject: [PATCH 04/20] RTF Input: More encoding token splitting fixes. --- src/calibre/ebooks/rtf2xml/ParseRtf.py | 12 +++---- src/calibre/ebooks/rtf2xml/colors.py | 2 +- src/calibre/ebooks/rtf2xml/process_tokens.py | 14 +++++---- src/calibre/ebooks/rtf2xml/tokenize.py | 33 ++++++-------------- 4 files changed, 25 insertions(+), 36 deletions(-) diff --git a/src/calibre/ebooks/rtf2xml/ParseRtf.py b/src/calibre/ebooks/rtf2xml/ParseRtf.py index 831183f0dd..0fc1c431db 100755 --- a/src/calibre/ebooks/rtf2xml/ParseRtf.py +++ b/src/calibre/ebooks/rtf2xml/ParseRtf.py @@ -226,7 +226,7 @@ class ParseRtf: try: return_value = process_tokens_obj.process_tokens() except InvalidRtfException, msg: - #Check to see if the file is correctly encoded + # Check to see if the file is correctly encoded encode_obj = default_encoding.DefaultEncoding( in_file = self.__temp_file, run_level = self.__run_level, @@ -237,14 +237,14 @@ class ParseRtf: check_encoding_obj = check_encoding.CheckEncoding( bug_handler = RtfInvalidCodeException, ) - enc = 'cp' + encode_obj.get_codepage() - if enc == 'cp10000': - enc = 'mac_roman' - msg = 'Exception in token processing' + enc = encode_obj.get_codepage() + if enc != 'mac_roman': + enc = 'cp' + enc + msg = '%s\nException in token processing' % str(msg) if check_encoding_obj.check_encoding(self.__file, enc): file_name = self.__file if isinstance(self.__file, str) \ else self.__file.encode('utf-8') - msg = 'File %s does not appear to be correctly encoded.\n' % file_name + msg +='\nFile %s does not appear to be correctly encoded.\n' % file_name try: os.remove(self.__temp_file) except OSError: diff --git a/src/calibre/ebooks/rtf2xml/colors.py b/src/calibre/ebooks/rtf2xml/colors.py index eba03547c8..e85b59571c 100755 --- a/src/calibre/ebooks/rtf2xml/colors.py +++ b/src/calibre/ebooks/rtf2xml/colors.py @@ -210,7 +210,7 @@ class Colors: hex_num = self.__color_dict.get(num) if hex_num is None: hex_num = '0' - if self.__run_level > 5: + if self.__run_level > 3: msg = 'no value in self.__color_dict' \ 'for key %s at line %d\n' % (num, self.__line) raise self.__bug_handler, msg diff --git a/src/calibre/ebooks/rtf2xml/process_tokens.py b/src/calibre/ebooks/rtf2xml/process_tokens.py index c6cf124425..65162d0d37 100755 --- a/src/calibre/ebooks/rtf2xml/process_tokens.py +++ b/src/calibre/ebooks/rtf2xml/process_tokens.py @@ -786,21 +786,23 @@ class ProcessTokens: token = line.replace("\n","") line_count += 1 if line_count == 1 and token != '\\{': - msg = 'Invalid RTF: document doesn\'t start with {\n' + msg = '\nInvalid RTF: document doesn\'t start with {\n' raise self.__exception_handler, msg elif line_count == 2 and token[0:4] != '\\rtf': - msg = 'Invalid RTF: document doesn\'t start with \\rtf \n' + msg = '\nInvalid RTF: document doesn\'t start with \\rtf \n' raise self.__exception_handler, msg the_index = token.find('\\ ') if token is not None and the_index > -1: - msg = 'Invalid RTF: token "\\ " not valid.\n' + msg = '\nInvalid RTF: token "\\ " not valid.\nError at line %d'\ + % line_count raise self.__exception_handler, msg elif token[:1] == "\\": try: token.decode('us-ascii') except UnicodeError, msg: - msg = 'Invalid RTF: Tokens not ascii encoded.\n%s' % str(msg) + msg = '\nInvalid RTF: Tokens not ascii encoded.\n%s\nError at line %d'\ + % (str(msg), line_count) raise self.__exception_handler, msg line = self.process_cw(token) if line is not None: @@ -816,7 +818,7 @@ class ProcessTokens: write_obj.write('tx\n\g<2>", input_file) input_file = self.__ms_hex_exp.sub("\\mshex0\g<1> ", input_file) input_file = self.__utf_ud.sub("\\{\\uc0 \g<1>\\}", input_file) #remove \n in bin data @@ -139,17 +140,17 @@ class Tokenize: "\\_": "\\_ ", "\\:": "\\: ", "\\-": "\\- ", - # turn into a generic token to eliminate special - # cases and make processing easier + #turn into a generic token to eliminate special + #cases and make processing easier "\\{": "\\ob ", - # turn into a generic token to eliminate special - # cases and make processing easier + #turn into a generic token to eliminate special + #cases and make processing easier "\\}": "\\cb ", - # put a backslash in front of to eliminate special cases and - # make processing easier + #put a backslash in front of to eliminate special cases and + #make processing easier "{": "\\{", - # put a backslash in front of to eliminate special cases and - # make processing easier + #put a backslash in front of to eliminate special cases and + #make processing easier "}": "\\}", } self.__replace_spchar = MReplace(SIMPLE_RPL) @@ -165,21 +166,9 @@ class Tokenize: #remove \n from endline char self.__splitexp = re.compile(r"(\\[{}]|\n|\\[^\s\\{}&]+(?:[ \t\r\f\v])?)") #this is for old RTF - self.__par_exp = re.compile(r'\\\n+') + self.__par_exp = re.compile(r'(\\\n+|\\ )') #handle cw using a digit as argument and without space as delimiter self.__cwdigit_exp = re.compile(r"(\\[a-zA-Z]+[\-0-9]+)([^0-9 \\]+)") - #self.__bin_exp = re.compile(r"\\bin(-?\d{1,8}) {0,1}") - #self.__utf_exp = re.compile(r"^\\u(-?\d{3,6})") - #self.__splitexp = re.compile(r"(\\[\\{}]|{|}|\n|\\[^\s\\{}&]+(?:\s)?)") - #self.__remove_line = re.compile(r'\n+') - ##self.num_exp = re.compile(r"(\*|:|[a-zA-Z]+)(.*)") - - def __correct_spliting(self, token): - match_obj = re.search(self.__cwdigit_exp, token) - if match_obj is None: - return token - else: - return '%s\n%s' % (match_obj.group(1), match_obj.group(2)) def tokenize(self): """Main class for handling other methods. Reads the file \ @@ -196,8 +185,6 @@ class Tokenize: tokens = map(self.__unicode_process, tokens) #remove empty items created by removing \uc tokens = filter(lambda x: len(x) > 0, tokens) - #handles bothersome cases - tokens = map(self.__correct_spliting, tokens) #write with open(self.__write_to, 'wb') as write_obj: From 9cdad92468b25f289f5531be56be0ec0ee32e01d Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 5 Feb 2011 12:51:47 -0500 Subject: [PATCH 05/20] TXT Input: Restructure to run dehyphenator when auto and heuristic formatting options are used. This causes textile and markdown to be dehyphenated. --- src/calibre/ebooks/txt/input.py | 47 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/calibre/ebooks/txt/input.py b/src/calibre/ebooks/txt/input.py index e1392ef732..85bd781ff8 100644 --- a/src/calibre/ebooks/txt/input.py +++ b/src/calibre/ebooks/txt/input.py @@ -77,20 +77,6 @@ class TXTInput(InputFormatPlugin): # Normalize line endings txt = normalize_line_endings(txt) - # Detect formatting - if options.formatting_type == 'auto': - options.formatting_type = detect_formatting_type(txt) - log.debug('Auto detected formatting as %s' % options.formatting_type) - - if options.formatting_type == 'heuristic': - setattr(options, 'enable_heuristics', True) - setattr(options, 'markup_chapter_headings', True) - setattr(options, 'italicize_common_cases', True) - setattr(options, 'fix_indents', True) - setattr(options, 'delete_blank_paragraphs', True) - setattr(options, 'format_scene_breaks', True) - setattr(options, 'dehyphenate', True) - # Determine the paragraph type of the document. if options.paragraph_type == 'auto': options.paragraph_type = detect_paragraph_type(txt) @@ -99,16 +85,27 @@ class TXTInput(InputFormatPlugin): options.paragraph_type = 'block' else: log.debug('Auto detected paragraph type as %s' % options.paragraph_type) + + dehyphenate = False + if options.formatting_type in ('auto', 'heuristic'): + # Set this here because we want it to run over all + # formatting types if auto is used. + dehyphenate = True + + # Detect formatting + if options.formatting_type == 'auto': + options.formatting_type = detect_formatting_type(txt) + log.debug('Auto detected formatting as %s' % options.formatting_type) + + if options.formatting_type == 'heuristic': + setattr(options, 'enable_heuristics', True) + setattr(options, 'unwrap_lines', False) # Preserve spaces will replace multiple spaces to a space # followed by the   entity. if options.preserve_spaces: txt = preserve_spaces(txt) - # Get length for hyphen removal and punctuation unwrap - docanalysis = DocAnalysis('txt', txt) - length = docanalysis.line_length(.5) - # Reformat paragraphs to block formatting based on the detected type. # We don't check for block because the processor assumes block. # single and print at transformed to block for processing. @@ -119,9 +116,17 @@ class TXTInput(InputFormatPlugin): elif options.paragraph_type == 'unformatted': from calibre.ebooks.conversion.utils import HeuristicProcessor # unwrap lines based on punctuation + docanalysis = DocAnalysis('txt', txt) + length = docanalysis.line_length(.5) preprocessor = HeuristicProcessor(options, log=getattr(self, 'log', None)) txt = preprocessor.punctuation_unwrap(length, txt, 'txt') + if dehyphenate: + docanalysis = DocAnalysis('txt', txt) + length = docanalysis.line_length(.5) + dehyphenator = Dehyphenator(options.verbose, log=self.log) + txt = dehyphenator(txt,'txt', length) + # Process the text using the appropriate text processor. html = '' if options.formatting_type == 'markdown': @@ -134,14 +139,8 @@ class TXTInput(InputFormatPlugin): elif options.formatting_type == 'textile': log.debug('Running text through textile conversion...') html = convert_textile(txt) - else: log.debug('Running text through basic conversion...') - if options.formatting_type == 'heuristic': - # Dehyphenate - dehyphenator = Dehyphenator(options.verbose, log=self.log) - txt = dehyphenator(txt,'txt', length) - flow_size = getattr(options, 'flow_size', 0) html = convert_basic(txt, epub_split_size_kb=flow_size) From 2796960f420cf26ad621f137845e6db84bc3019d Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 5 Feb 2011 13:04:32 -0500 Subject: [PATCH 06/20] Heuristics: Fix issue with invalid markup from italicize patterns. --- src/calibre/ebooks/conversion/utils.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/calibre/ebooks/conversion/utils.py b/src/calibre/ebooks/conversion/utils.py index e8e2a82949..c0c2ee8978 100644 --- a/src/calibre/ebooks/conversion/utils.py +++ b/src/calibre/ebooks/conversion/utils.py @@ -149,17 +149,17 @@ class HeuristicProcessor(object): ] ITALICIZE_STYLE_PATS = [ - r'(?msu)(?<=[\s>"])_(?P[^_]+)?_', - r'(?msu)(?<=[\s>"])/(?P[^/]+)?/', - r'(?msu)(?<=[\s>"])~~(?P[^~]+)?~~', - r'(?msu)(?<=[\s>"])\*(?P[^\*]+)?\*', - r'(?msu)(?<=[\s>"])~(?P[^~]+)?~', - r'(?msu)(?<=[\s>"])_/(?P[^/_]+)?/_', - r'(?msu)(?<=[\s>"])_\*(?P[^\*_]+)?\*_', - r'(?msu)(?<=[\s>"])\*/(?P[^/\*]+)?/\*', - r'(?msu)(?<=[\s>"])_\*/(?P[^\*_]+)?/\*_', - r'(?msu)(?<=[\s>"])/:(?P[^:/]+)?:/', - r'(?msu)(?<=[\s>"])\|:(?P[^:\|]+)?:\|', + r'(?msu)(?<=[\s>])_(?P[^_]+)?_', + r'(?msu)(?<=[\s>])/(?P[^/]+)?/', + r'(?msu)(?<=[\s>])~~(?P[^~]+)?~~', + r'(?msu)(?<=[\s>])\*(?P[^\*]+)?\*', + r'(?msu)(?<=[\s>])~(?P[^~]+)?~', + r'(?msu)(?<=[\s>])_/(?P[^/_]+)?/_', + r'(?msu)(?<=[\s>])_\*(?P[^\*_]+)?\*_', + r'(?msu)(?<=[\s>])\*/(?P[^/\*]+)?/\*', + r'(?msu)(?<=[\s>])_\*/(?P[^\*_]+)?/\*_', + r'(?msu)(?<=[\s>])/:(?P[^:/]+)?:/', + r'(?msu)(?<=[\s>])\|:(?P[^:\|]+)?:\|', ] for word in ITALICIZE_WORDS: From 0a267d2932d94b614475f66e095463c7c0e72ef7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 5 Feb 2011 11:14:28 -0700 Subject: [PATCH 07/20] Switch bulk download of metadata to use a singleSHot architecture instead of a QTimer --- src/calibre/gui2/metadata/bulk_download.py | 55 ++++++++++++---------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/src/calibre/gui2/metadata/bulk_download.py b/src/calibre/gui2/metadata/bulk_download.py index 19859ed3ec..461f56b60c 100644 --- a/src/calibre/gui2/metadata/bulk_download.py +++ b/src/calibre/gui2/metadata/bulk_download.py @@ -11,7 +11,7 @@ from threading import Thread from Queue import Queue, Empty from functools import partial -from PyQt4.Qt import QObject, Qt, pyqtSignal, QTimer, QDialog, \ +from PyQt4.Qt import QObject, QTimer, QDialog, \ QVBoxLayout, QTextBrowser, QLabel, QGroupBox, QDialogButtonBox from calibre.ebooks.metadata.fetch import search, get_social_metadata @@ -163,27 +163,23 @@ class DownloadMetadata(Thread): class DoDownload(QObject): - idle_process = pyqtSignal() - def __init__(self, parent, title, db, ids, get_covers, set_metadata=True, get_social_metadata=True): QObject.__init__(self, parent) self.pd = ProgressDialog(title, min=0, max=0, parent=parent) self.pd.canceled_signal.connect(self.cancel) - self.idle_process.connect(self.do_one, type=Qt.QueuedConnection) self.downloader = None self.create = partial(DownloadMetadata, db, ids, get_covers, set_metadata=set_metadata, get_social_metadata=get_social_metadata) - self.timer = QTimer(self) self.get_covers = get_covers - self.timer.timeout.connect(self.do_one, type=Qt.QueuedConnection) self.db = db self.updated = set([]) self.total = len(ids) + self.keep_going = True def exec_(self): - self.timer.start(50) + QTimer.singleShot(50, self.do_one) ret = self.pd.exec_() if getattr(self.downloader, 'exception', None) is not None and \ ret == self.pd.Accepted: @@ -194,30 +190,37 @@ class DoDownload(QObject): return ret def cancel(self, *args): - self.timer.stop() + self.keep_going = False self.downloader.keep_going = False self.pd.reject() def do_one(self): - if self.downloader is None: - self.downloader = self.create() - self.downloader.start() - self.pd.set_min(0) - self.pd.set_max(self.downloader.total) try: - r = self.downloader.results.get_nowait() - self.handle_result(r) - except Empty: - pass - if not self.downloader.is_alive(): - self.timer.stop() - while True: - try: - r = self.downloader.results.get_nowait() - self.handle_result(r) - except Empty: - break - self.pd.accept() + if not self.keep_going: + return + if self.downloader is None: + self.downloader = self.create() + self.downloader.start() + self.pd.set_min(0) + self.pd.set_max(self.downloader.total) + try: + r = self.downloader.results.get_nowait() + self.handle_result(r) + except Empty: + pass + if not self.downloader.is_alive(): + while True: + try: + r = self.downloader.results.get_nowait() + self.handle_result(r) + except Empty: + break + self.pd.accept() + return + except: + self.cancel() + raise + QTimer.singleShot(50, self.do_one) def handle_result(self, r): id_, typ, ok, title = r From 1f708746d0e6e62234f9a2a9a96cd5bcf73bfc94 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 5 Feb 2011 13:35:41 -0500 Subject: [PATCH 08/20] TXT Input: Fix bug where spaces were not retained properly. Fix bug where spaces were replaced with entities (this should only have happened at the beginning of lines). Add option to remove indents. --- src/calibre/ebooks/txt/input.py | 8 +++++++- src/calibre/ebooks/txt/processor.py | 12 ++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/calibre/ebooks/txt/input.py b/src/calibre/ebooks/txt/input.py index 85bd781ff8..b1374bbeec 100644 --- a/src/calibre/ebooks/txt/input.py +++ b/src/calibre/ebooks/txt/input.py @@ -12,7 +12,7 @@ from calibre.ebooks.chardet import detect from calibre.ebooks.txt.processor import convert_basic, convert_markdown, \ separate_paragraphs_single_line, separate_paragraphs_print_formatted, \ preserve_spaces, detect_paragraph_type, detect_formatting_type, \ - normalize_line_endings, convert_textile + normalize_line_endings, convert_textile, remove_indents from calibre import _ent_pat, xml_entity_to_unicode class TXTInput(InputFormatPlugin): @@ -47,6 +47,9 @@ class TXTInput(InputFormatPlugin): OptionRecommendation(name='preserve_spaces', recommended_value=False, help=_('Normally extra spaces are condensed into a single space. ' 'With this option all spaces will be displayed.')), + OptionRecommendation(name='txt_in_remove_indents', recommended_value=False, + help=_('Normally extra space at the beginning of lines is retained. ' + 'With this option they will be removed.')), OptionRecommendation(name="markdown_disable_toc", recommended_value=False, help=_('Do not insert a Table of Contents into the output text.')), ]) @@ -101,6 +104,9 @@ class TXTInput(InputFormatPlugin): setattr(options, 'enable_heuristics', True) setattr(options, 'unwrap_lines', False) + if options.txt_in_remove_indents: + txt = remove_indents(txt) + # Preserve spaces will replace multiple spaces to a space # followed by the   entity. if options.preserve_spaces: diff --git a/src/calibre/ebooks/txt/processor.py b/src/calibre/ebooks/txt/processor.py index 546d3f1842..987d7cdc73 100644 --- a/src/calibre/ebooks/txt/processor.py +++ b/src/calibre/ebooks/txt/processor.py @@ -24,14 +24,14 @@ def clean_txt(txt): # all line breaks with \n. txt = '\n'.join([line.rstrip() for line in txt.splitlines()]) - # Replace whitespace at the beginning of the list with   - txt = re.sub('(?m)(?P[ ]+)', lambda mo: ' ' * mo.groups('space').count(' '), txt) - txt = re.sub('(?m)(?P[\t]+)', lambda mo: ' ' * 4 * mo.groups('space').count('\t'), txt) + # Replace whitespace at the beginning of the line with   + txt = re.sub('(?m)(?P^[ ]+)(?=.)', lambda mo: ' ' * mo.groups('space').count(' '), txt) + txt = re.sub('(?m)(?P^[\t]+)(?=.)', lambda mo: ' ' * 4 * mo.groups('space').count('\t'), txt) # Condense redundant spaces txt = re.sub('[ ]{2,}', ' ', txt) - # Remove blank lines from the beginning and end of the document. + # Remove blank space from the beginning and end of the document. txt = re.sub('^\s+(?=.)', '', txt) txt = re.sub('(?<=.)\s+$', '', txt) # Remove excessive line breaks. @@ -107,6 +107,10 @@ def preserve_spaces(txt): txt = txt.replace('\t', '    ') return txt +def remove_indents(txt): + txt = re.sub('(?miu)^\s+', '', txt) + return txt + def opf_writer(path, opf_name, manifest, spine, mi): opf = OPFCreator(path, mi) opf.create_manifest(manifest) From 392bb9f49cc7366f3218802df760d3f5a081af46 Mon Sep 17 00:00:00 2001 From: GRiker Date: Sat, 5 Feb 2011 11:37:30 -0700 Subject: [PATCH 09/20] GwR fixes for #8780, #8764 --- resources/jacket/stylesheet.css | 4 +- src/calibre/ebooks/oeb/transforms/jacket.py | 4 +- src/calibre/library/catalog.py | 41 +++++++++------------ 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/resources/jacket/stylesheet.css b/resources/jacket/stylesheet.css index c45f8fe977..56bef24ac2 100644 --- a/resources/jacket/stylesheet.css +++ b/resources/jacket/stylesheet.css @@ -113,8 +113,8 @@ table.cbj_header tr.cbj_series { /* display:none; */ } -table.cbj_header tr.cbj_pubdate { - /* Uncomment the next line to remove 'Published' from banner section */ +table.cbj_header tr.cbj_pubdata { + /* Uncomment the next line to remove 'Published (year of publication)' from banner section */ /* display:none; */ } diff --git a/src/calibre/ebooks/oeb/transforms/jacket.py b/src/calibre/ebooks/oeb/transforms/jacket.py index 84f2dd5d6a..fe0d60de7a 100644 --- a/src/calibre/ebooks/oeb/transforms/jacket.py +++ b/src/calibre/ebooks/oeb/transforms/jacket.py @@ -109,7 +109,7 @@ def get_rating(rating, rchar, e_rchar): def render_jacket(mi, output_profile, alt_title=_('Unknown'), alt_tags=[], alt_comments='', - alt_publisher=('Unknown publisher')): + alt_publisher=('')): css = P('jacket/stylesheet.css', data=True).decode('utf-8') try: @@ -127,7 +127,7 @@ def render_jacket(mi, output_profile, try: publisher = mi.publisher if mi.publisher else alt_publisher except: - publisher = _('Unknown publisher') + publisher = '' try: pubdate = strftime(u'%Y', mi.pubdate.timetuple()) diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index 23127035d2..cb55b2318d 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -4442,46 +4442,39 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) # Insert the link to the series or remove aTag = body.find('a', attrs={'class':'series_id'}) - if book['series']: - if self.opts.generate_series: - aTag['href'] = "%s.html#%s_series" % ('BySeries', - re.sub('\W','',book['series']).lower()) - else: - aTag.extract() + if aTag: + if book['series']: + if self.opts.generate_series: + aTag['href'] = "%s.html#%s_series" % ('BySeries', + re.sub('\W','',book['series']).lower()) + else: + aTag.extract() - # Insert the author link (always) + # Insert the author link aTag = body.find('a', attrs={'class':'author'}) - if self.opts.generate_authors: + if self.opts.generate_authors and aTag: aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author'])) if publisher == ' ': - try: - publisherTag = body.find('td', attrs={'class':'publisher'}) + publisherTag = body.find('td', attrs={'class':'publisher'}) + if publisherTag: publisherTag.contents[0].replaceWith(' ') - except: - pass if not genres: - try: - genresTag = body.find('p',attrs={'class':'genres'}) + genresTag = body.find('p',attrs={'class':'genres'}) + if genresTag: genresTag.extract() - except: - pass if not formats: - try: - formatsTag = body.find('p',attrs={'class':'formats'}) + formatsTag = body.find('p',attrs={'class':'formats'}) + if formatsTag: formatsTag.extract() - except: - pass if note_content == '': - try: - tdTag = body.find('td', attrs={'class':'notes'}) + tdTag = body.find('td', attrs={'class':'notes'}) + if tdTag: tdTag.contents[0].replaceWith(' ') - except: - pass emptyTags = body.findAll('td', attrs={'class':'empty'}) for mt in emptyTags: From 3fa7f2a0314af17668ac1836b5dea3697bc3bc75 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 5 Feb 2011 11:37:38 -0700 Subject: [PATCH 10/20] ... --- src/calibre/gui2/complete.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/calibre/gui2/complete.py b/src/calibre/gui2/complete.py index a013065690..2cbbb31076 100644 --- a/src/calibre/gui2/complete.py +++ b/src/calibre/gui2/complete.py @@ -6,7 +6,7 @@ __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import QLineEdit, QListView, QAbstractListModel, Qt, QTimer, \ +from PyQt4.Qt import QLineEdit, QListView, QAbstractListModel, Qt, \ QApplication, QPoint, QItemDelegate, QStyleOptionViewItem, \ QStyle, QEvent, pyqtSignal @@ -242,8 +242,8 @@ class MultiCompleteLineEdit(QLineEdit): else: prefix_len = len(before_text.split(self.sep)[-1].lstrip()) completed_text = before_text[:cursor_pos - prefix_len] + text + after_text - return prefix_len, completed_text - + return prefix_len, completed_text + def completion_selected(self, text): prefix_len, ctext = self.get_completed_text(text) @@ -298,17 +298,6 @@ class MultiCompleteLineEdit(QLineEdit): self.position_complete_window() self.complete_window.show() - def moveEvent(self, ev): - ret = QLineEdit.moveEvent(self, ev) - QTimer.singleShot(0, self.position_complete_window) - return ret - - def resizeEvent(self, ev): - ret = QLineEdit.resizeEvent(self, ev) - QTimer.singleShot(0, self.position_complete_window) - return ret - - @dynamic_property def all_items(self): def fget(self): From a0fd28d9660f56ed2a37abe7b185c53f65e1dff7 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 5 Feb 2011 13:46:15 -0500 Subject: [PATCH 11/20] TXT Input GUI: Add remove indents option. Restructure options to make them grouped cleaner. --- src/calibre/gui2/convert/txt_input.py | 3 +- src/calibre/gui2/convert/txt_input.ui | 127 ++++++++++++++++++-------- 2 files changed, 91 insertions(+), 39 deletions(-) diff --git a/src/calibre/gui2/convert/txt_input.py b/src/calibre/gui2/convert/txt_input.py index 62672cc0f9..acdf5f43c0 100644 --- a/src/calibre/gui2/convert/txt_input.py +++ b/src/calibre/gui2/convert/txt_input.py @@ -16,7 +16,8 @@ class PluginWidget(Widget, Ui_Form): def __init__(self, parent, get_option, get_help, db=None, book_id=None): Widget.__init__(self, parent, - ['paragraph_type', 'formatting_type', 'markdown_disable_toc', 'preserve_spaces']) + ['paragraph_type', 'formatting_type', 'markdown_disable_toc', + 'preserve_spaces', 'txt_in_remove_indents']) self.db, self.book_id = db, book_id for x in get_option('paragraph_type').option.choices: self.opt_paragraph_type.addItem(x) diff --git a/src/calibre/gui2/convert/txt_input.ui b/src/calibre/gui2/convert/txt_input.ui index 6cbd68135f..211b03294a 100644 --- a/src/calibre/gui2/convert/txt_input.ui +++ b/src/calibre/gui2/convert/txt_input.ui @@ -7,57 +7,95 @@ 0 0 518 - 300 + 353 Form - - - - - Paragraph style: + + + + + Structure + + + + + + 0 + 0 + + + + Paragraph style: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + Formatting style: + + + + + + + + 0 + 0 + + + + + - - - - - - - Preserve &spaces + + + + Common + + + + + Preserve &spaces + + + + + + + Remove indents at the beginning of lines + + + + - - - - Qt::Vertical - - - - 20 - 213 - - - - - - - - - - - Formatting style: - - - - + - Markdown Options + Markdown @@ -83,6 +121,19 @@ + + + + Qt::Vertical + + + + 20 + 213 + + + + From f9fbb6e6fc3ad33fe506573b91dc997258d8bbcd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 5 Feb 2011 12:46:35 -0700 Subject: [PATCH 12/20] ... --- src/calibre/gui2/tools.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/tools.py b/src/calibre/gui2/tools.py index 655c7ea7c6..447e0dc240 100644 --- a/src/calibre/gui2/tools.py +++ b/src/calibre/gui2/tools.py @@ -9,7 +9,7 @@ Logic for setting up conversion jobs import cPickle, os -from PyQt4.Qt import QDialog, QProgressDialog, QString, QTimer, SIGNAL +from PyQt4.Qt import QDialog, QProgressDialog, QString, QTimer from calibre.ptempfile import PersistentTemporaryFile from calibre.gui2 import warning_dialog, question_dialog @@ -125,14 +125,11 @@ class QueueBulk(QProgressDialog): self.parent = parent self.use_saved_single_settings = use_saved_single_settings self.i, self.bad, self.jobs, self.changed = 0, [], [], False - self.timer = QTimer(self) - self.connect(self.timer, SIGNAL('timeout()'), self.do_book) - self.timer.start() + QTimer.singleShot(0, self.do_book) self.exec_() def do_book(self): if self.i >= len(self.book_ids): - self.timer.stop() return self.do_queue() book_id = self.book_ids[self.i] self.i += 1 @@ -191,6 +188,7 @@ class QueueBulk(QProgressDialog): self.setValue(self.i) except NoSupportedInputFormats: self.bad.append(book_id) + QTimer.singleShot(0, self.do_book) def do_queue(self): self.hide() From 0b4f6669ba6bcb23fdb0024b408d74d4e7eabace Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 5 Feb 2011 12:53:43 -0700 Subject: [PATCH 13/20] ... --- Changelog.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Changelog.yaml b/Changelog.yaml index 71f7f1c52b..702378719f 100644 --- a/Changelog.yaml +++ b/Changelog.yaml @@ -55,7 +55,7 @@ - title: "Add search to the plugin preferences dialog" bug fixes: - - title: "Fix a bug that could cause fiels to be lost when changing metadata on east asian windows installs if the title and/or author is very long." + - title: "Fix a bug that could cause files to be lost when changing metadata on east asian windows installs if the title and/or author is very long." tickets: [8620] - title: "Tag browser: Fix searching with items in a user category not owrking if the main category is hidden" @@ -88,7 +88,7 @@ - title: "Do not discard the result of a conversion if the user opens the edit metadata dialog while the conversion is running" tickets: [8672] - - title: "CHM Input: When the chm file lacks a hhc, lookf for index.html instead" + - title: "CHM Input: When the chm file lacks a hhc, look for index.html instead" tickets: [8688] - title: "EPUB Input: Filter some invalid media types from the spine" From a0312461520a9b6583193bbd2ccf0dca3c7f28e5 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 5 Feb 2011 15:02:47 -0500 Subject: [PATCH 14/20] TXT Input: Only run dehyphenate when option set. --- src/calibre/ebooks/txt/input.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/calibre/ebooks/txt/input.py b/src/calibre/ebooks/txt/input.py index b1374bbeec..75bafc7cef 100644 --- a/src/calibre/ebooks/txt/input.py +++ b/src/calibre/ebooks/txt/input.py @@ -88,12 +88,6 @@ class TXTInput(InputFormatPlugin): options.paragraph_type = 'block' else: log.debug('Auto detected paragraph type as %s' % options.paragraph_type) - - dehyphenate = False - if options.formatting_type in ('auto', 'heuristic'): - # Set this here because we want it to run over all - # formatting types if auto is used. - dehyphenate = True # Detect formatting if options.formatting_type == 'auto': @@ -127,7 +121,7 @@ class TXTInput(InputFormatPlugin): preprocessor = HeuristicProcessor(options, log=getattr(self, 'log', None)) txt = preprocessor.punctuation_unwrap(length, txt, 'txt') - if dehyphenate: + if getattr(options, 'enable_heuristics', False) and getattr(options, 'dehyphenate', False): docanalysis = DocAnalysis('txt', txt) length = docanalysis.line_length(.5) dehyphenator = Dehyphenator(options.verbose, log=self.log) From 4719df178ae265616aae191e71776b2446fed272 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 5 Feb 2011 13:13:20 -0700 Subject: [PATCH 15/20] ... --- src/calibre/gui2/complete.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/complete.py b/src/calibre/gui2/complete.py index 2cbbb31076..b0d00c3718 100644 --- a/src/calibre/gui2/complete.py +++ b/src/calibre/gui2/complete.py @@ -178,7 +178,8 @@ class MultiCompleteLineEdit(QLineEdit): self._model = CompleteModel(parent=self) self.complete_window = CompleteWindow(self, self._model) self.textEdited.connect(self.text_edited) - self.complete_window.completion_selected.connect(self.completion_selected) + self.complete_window.completion_selected.connect(self.completion_selected, + type=Qt.QueuedConnection) self.installEventFilter(self) # Interface {{{ From d6d4f9d444cb07df8ae6512560e4af041df56d05 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 5 Feb 2011 13:37:42 -0700 Subject: [PATCH 16/20] Korespondent and Kopalnia Wiedzy by Attis --- resources/images/news/kopalniawiedzy.png | Bin 0 -> 466 bytes resources/images/news/korespondent.png | Bin 0 -> 860 bytes resources/recipes/kopalniawiedzy.recipe | 80 +++++++++++++++++++++++ resources/recipes/korespondent.recipe | 40 ++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 resources/images/news/kopalniawiedzy.png create mode 100644 resources/images/news/korespondent.png create mode 100644 resources/recipes/kopalniawiedzy.recipe create mode 100644 resources/recipes/korespondent.recipe diff --git a/resources/images/news/kopalniawiedzy.png b/resources/images/news/kopalniawiedzy.png new file mode 100644 index 0000000000000000000000000000000000000000..73a4a338aa25418b42a66c4aafced68e48e6f721 GIT binary patch literal 466 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~q0VEhsNC=VVPs zNr_hLqX$JYcbH2G1-d48D?CzQk&#g;P?d4#aB|qd!tyd8hDF@`;WWLK&e!XI)jf!i zIQV>p!nVIH8=f-cyyFR9rz00yX7EiSL0Q0Ny2@_5Wj{of5-cxoSy8ql{qz-GI~D`M zuND)!Jii9=y=2@Ia8NQ+>1Oe+f+D8$k2BhnrN8!+-FzFwV9)=q>Dyyd-TlYU>oM`Y zK6E?(*2ek4nw7PT8fqq@@r$MzraqKpPN`}VFP_|;z+cXGYsm%oe3g&6?_cbCre9h2^#1PayJo5F1qOv`iEBhjN@7W> zRdP`(kYX@0Ff`OPFw!+J4KXyfGB&d^0kX}l3=I13?N3C}kei>9nN|tZU|^wZV5w_h g5&}|ZVr5{gYhVV{FjeJOGf)GAr>mdKI;Vst091CT&Hw-a literal 0 HcmV?d00001 diff --git a/resources/images/news/korespondent.png b/resources/images/news/korespondent.png new file mode 100644 index 0000000000000000000000000000000000000000..e2724b11b44a42e49a50f5e0dd327489ae2aa0c3 GIT binary patch literal 860 zcmV-i1Ec(jP)8rEZC~nV0Nhl$Vi#mYAR;$fVGOh#=5zGAJ}DBKqNv ziYQ7c5H0gZ(7#d$%L`00vGpTG(wdrsg=?E*ZJzCHo}FvICI_Qlc;MxIcplz|2a{zP z{x`uWo36qUA7&B;lE68HVN#qEsLV9&s!mPBu--A}rJm7+78QZ`ioKarKfLtrWq)MX z^^?U9n``#;TiOR_ec^ReFv6y*Rd@m+kYcQ$8@OR@s4vSML@Xz2Y7b<;UkS#tyGNY1 z#=|>apSKpgu+MpG1W_W)M#>wY*E1wpM(doQO{GYBi&hoYkT?qfKwLe(|A{Ec_@vWc zIkm7_QJSAVU6^B-2LP}=`_w%=<2j~`l%^xsQnac_O_A9S06+i$;0P0M)|cF`y*v8e zu^2cc17NNFj;`@JU$s(f(5u6Br8z@KEm>nEfw&C=0D$C6uO?k*`^T4>KQ9H!mEsH5 zg1*kCvNpX&#VGF^21&uhH(%A}CXVde_OZ96;xfW8X)}kCi_0I~xtUC%$Or(y7gzA$ zl|IK)hUNADG%y>r-sFy9Am2Gtdi!V3>D*hVO76uI0@Xd@v<*$K)X6enN&~#3qXPf{ zUVd0UH|q{r006ATX8Utn05UK! zF)c7MEig4yF*7ppnFgPtR mIV~_aR4_C;F*!OhH!UzWIxsMUSv7b70000'), + lambda match: '' ), + (re.compile(u'

'), + lambda match: '') + ] + + feeds = [ + (u'Biologia', u'http://kopalniawiedzy.pl/wiadomosci_biologia.rss'), + (u'Medycyna', u'http://kopalniawiedzy.pl/wiadomosci_medycyna.rss'), + (u'Psychologia', u'http://kopalniawiedzy.pl/wiadomosci_psychologia.rss'), + (u'Technologie', u'http://kopalniawiedzy.pl/wiadomosci_technologie.rss'), + (u'Ciekawostki', u'http://kopalniawiedzy.pl/wiadomosci_ciekawostki.rss'), + (u'Artykuły', u'http://kopalniawiedzy.pl/artykuly.rss') + ] + + def is_link_wanted(self, url, tag): + return tag['class'] == 'next' + + def remove_beyond(self, tag, next): + while tag is not None and getattr(tag, 'name', None) != 'body': + after = getattr(tag, next) + while after is not None: + ns = getattr(tag, next) + after.extract() + after = ns + tag = tag.parent + + def append_page(self, soup, appendtag, position): + pager = soup.find('a',attrs={'class':'next'}) + if pager: + nexturl = self.INDEX + pager['href'] + soup2 = self.index_to_soup(nexturl) + texttag = soup2.find('div', attrs={'id':'articleContent'}) + + tag = texttag.find(attrs={'class':'pages'}) + self.remove_beyond(tag, 'nextSibling') + + newpos = len(texttag.contents) + self.append_page(soup2,texttag,newpos) + + appendtag.insert(position,texttag) + + + def preprocess_html(self, soup): + self.append_page(soup, soup.body, 3) + + for item in soup.findAll('div',attrs={'class':'pages'}): + item.extract() + + for item in soup.findAll('p', attrs={'class':'wykop'}): + item.extract() + + return soup diff --git a/resources/recipes/korespondent.recipe b/resources/recipes/korespondent.recipe new file mode 100644 index 0000000000..aa9cf6e828 --- /dev/null +++ b/resources/recipes/korespondent.recipe @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL v3' +__copyright__ = '2011, Attis ' +__version__ = 'v. 0.1' + +import re +from calibre.web.feeds.recipes import BasicNewsRecipe + +class KorespondentPL(BasicNewsRecipe): + title = u'Korespondent.pl' + publisher = u'Korespondent.pl' + description = u'Centrum wolnorynkowe - serwis ludzi wolnych' + encoding = 'utf-8' + __author__ = 'Attis' + language = 'pl' + oldest_article = 15 + max_articles_per_feed = 100 + remove_javascript = True + no_stylesheets = True + + keep_only_tags = [dict(name='div', attrs={'class':'publicystyka'})] + remove_tags = [{'name': 'meta'}, {'name':'div', 'attrs': {'class': 'zdjecie'} }] + extra_css = '.naglowek {font-size: small}\n .tytul {font-size: x-large; padding-bottom: 10px; padding-top: 30px} \n .external {font-size: small}' + + preprocess_regexps = [ + (re.compile(u'' ), + (re.compile(u'

Więcej'), + lambda match:'Więcej' ), + (re.compile(u'target="_blank"'), + lambda match:'target="_blank" class="external"' ), + (re.compile(u'

\nPoczytaj inne teksty w Serwisie wolnorynkowym Korespondent.pl.*', re.DOTALL|re.IGNORECASE), + lambda match: ''), + ] + + feeds = [(u'Serwis informacyjny', u'http://korespondent.pl/rss.xml')] + From 5e089ea3760b7a057f29e4ee116d400a9cc68974 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 5 Feb 2011 17:40:26 -0700 Subject: [PATCH 17/20] Use QCompleter instead of custom replacement for it, simplifies the code greatly. --- src/calibre/gui2/complete.py | 205 +++-------------------------------- src/calibre/gui2/tools.py | 18 ++- 2 files changed, 31 insertions(+), 192 deletions(-) diff --git a/src/calibre/gui2/complete.py b/src/calibre/gui2/complete.py index b0d00c3718..8b37a2da4e 100644 --- a/src/calibre/gui2/complete.py +++ b/src/calibre/gui2/complete.py @@ -6,113 +6,14 @@ __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import QLineEdit, QListView, QAbstractListModel, Qt, \ - QApplication, QPoint, QItemDelegate, QStyleOptionViewItem, \ - QStyle, QEvent, pyqtSignal +from PyQt4.Qt import QLineEdit, QAbstractListModel, Qt, \ + QApplication, QCompleter from calibre.utils.config import tweaks from calibre.utils.icu import sort_key, lower from calibre.gui2 import NONE from calibre.gui2.widgets import EnComboBox -class CompleterItemDelegate(QItemDelegate): # {{{ - - ''' Renders the current item as thought it were selected ''' - - def __init__(self, view): - self.view = view - QItemDelegate.__init__(self, view) - - def paint(self, p, opt, idx): - opt = QStyleOptionViewItem(opt) - opt.showDecorationSelected = True - if self.view.currentIndex() == idx: - opt.state |= QStyle.State_HasFocus - QItemDelegate.paint(self, p, opt, idx) - -# }}} - -class CompleteWindow(QListView): # {{{ - - ''' - The completion popup. For keyboard and mouse handling see - :meth:`eventFilter`. - ''' - - #: This signal is emitted when the user selects one of the listed - #: completions, by mouse or keyboard - completion_selected = pyqtSignal(object) - - def __init__(self, widget, model): - self.widget = widget - QListView.__init__(self) - self.setVisible(False) - self.setParent(None, Qt.Popup) - self.setAlternatingRowColors(True) - self.setFocusPolicy(Qt.NoFocus) - self._d = CompleterItemDelegate(self) - self.setItemDelegate(self._d) - self.setModel(model) - self.setFocusProxy(widget) - self.installEventFilter(self) - self.clicked.connect(self.do_selected) - self.entered.connect(self.do_entered) - self.setMouseTracking(True) - - def do_entered(self, idx): - if idx.isValid(): - self.setCurrentIndex(idx) - - def do_selected(self, idx=None): - idx = self.currentIndex() if idx is None else idx - if idx.isValid(): - data = unicode(self.model().data(idx, Qt.DisplayRole)) - self.completion_selected.emit(data) - self.hide() - - def eventFilter(self, o, e): - if o is not self: - return False - if e.type() == e.KeyPress: - key = e.key() - if key in (Qt.Key_Escape, Qt.Key_Backtab) or \ - (key == Qt.Key_F4 and (e.modifiers() & Qt.AltModifier)): - self.hide() - return True - elif key in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab): - if key == Qt.Key_Tab and not self.currentIndex().isValid(): - if self.model().rowCount() > 0: - self.setCurrentIndex(self.model().index(0)) - self.do_selected() - return True - elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, - Qt.Key_PageDown): - return False - # Send key event to associated line edit - self.widget.eat_focus_out = False - try: - self.widget.event(e) - finally: - self.widget.eat_focus_out = True - if not self.widget.hasFocus(): - # Line edit lost focus - self.hide() - if e.isAccepted(): - # Line edit consumed event - return True - elif e.type() == e.MouseButtonPress: - # Hide popup if user clicks outside it, otherwise - # pass event to popup - if not self.underMouse(): - self.hide() - return True - elif e.type() in (e.InputMethod, e.ShortcutOverride): - QApplication.sendEvent(self.widget, e) - - return False # Do not filter this event - -# }}} - class CompleteModel(QAbstractListModel): def __init__(self, parent=None): @@ -120,43 +21,25 @@ class CompleteModel(QAbstractListModel): self.sep = ',' self.space_before_sep = False self.items = [] - self.lowered_items = [] - self.matches = [] def set_items(self, items): items = [unicode(x.strip()) for x in items] self.items = list(sorted(items, key=lambda x: sort_key(x))) self.lowered_items = [lower(x) for x in self.items] - self.matches = [] self.reset() def rowCount(self, *args): - return len(self.matches) + return len(self.items) def data(self, index, role): if role == Qt.DisplayRole: r = index.row() try: - return self.matches[r] + return self.items[r] except IndexError: pass return NONE - def get_matches(self, prefix): - ''' - Return all matches that (case insensitively) start with prefix - ''' - prefix = lower(prefix) - ans = [] - if prefix: - for i, test in enumerate(self.lowered_items): - if test.startswith(prefix): - ans.append(self.items[i]) - return ans - - def update_matches(self, matches): - self.matches = matches - self.reset() class MultiCompleteLineEdit(QLineEdit): ''' @@ -170,17 +53,19 @@ class MultiCompleteLineEdit(QLineEdit): ''' def __init__(self, parent=None): - self.eat_focus_out = True - self.max_visible_items = 7 - self.current_prefix = None QLineEdit.__init__(self, parent) - self._model = CompleteModel(parent=self) - self.complete_window = CompleteWindow(self, self._model) - self.textEdited.connect(self.text_edited) - self.complete_window.completion_selected.connect(self.completion_selected, + self._completer = c = QCompleter(self._model, self) + c.setWidget(self) + c.setCompletionMode(QCompleter.PopupCompletion) + c.setCaseSensitivity(Qt.CaseInsensitive) + c.setModelSorting(QCompleter.CaseInsensitivelySortedModel) + c.setCompletionRole(Qt.DisplayRole) + c.popup().setAlternatingRowColors(True) + + c.activated.connect(self.completion_selected, type=Qt.QueuedConnection) - self.installEventFilter(self) + self.textEdited.connect(self.text_edited) # Interface {{{ def update_items_cache(self, complete_items): @@ -194,23 +79,12 @@ class MultiCompleteLineEdit(QLineEdit): # }}} - def eventFilter(self, o, e): - if self.eat_focus_out and o is self and e.type() == QEvent.FocusOut: - if self.complete_window.isVisible(): - return True # Filter this event since the cw is visible - return QLineEdit.eventFilter(self, o, e) - - def hide_completion_window(self): - self.complete_window.hide() - - def text_edited(self, *args): self.update_completions() + self._completer.complete() def update_completions(self): ' Update the list of completions ' - if not self.complete_window.isVisible() and not self.hasFocus(): - return cpos = self.cursorPosition() text = unicode(self.text()) prefix = text[:cpos] @@ -218,9 +92,7 @@ class MultiCompleteLineEdit(QLineEdit): complete_prefix = prefix.lstrip() if self.sep: complete_prefix = prefix = prefix.split(self.sep)[-1].lstrip() - - matches = self._model.get_matches(complete_prefix) - self.update_complete_window(matches) + self._completer.setCompletionPrefix(complete_prefix) def get_completed_text(self, text): ''' @@ -247,7 +119,7 @@ class MultiCompleteLineEdit(QLineEdit): def completion_selected(self, text): - prefix_len, ctext = self.get_completed_text(text) + prefix_len, ctext = self.get_completed_text(unicode(text)) if self.sep is None: self.setText(ctext) self.setCursorPosition(len(ctext)) @@ -256,49 +128,6 @@ class MultiCompleteLineEdit(QLineEdit): self.setText(ctext) self.setCursorPosition(cursor_pos - prefix_len + len(text)) - def update_complete_window(self, matches): - self._model.update_matches(matches) - if matches: - self.show_complete_window() - else: - self.complete_window.hide() - - - def position_complete_window(self): - popup = self.complete_window - screen = QApplication.desktop().availableGeometry(self) - h = (popup.sizeHintForRow(0) * min(self.max_visible_items, - popup.model().rowCount()) + 3) + 3 - hsb = popup.horizontalScrollBar() - if hsb and hsb.isVisible(): - h += hsb.sizeHint().height() - - rh = self.height() - pos = self.mapToGlobal(QPoint(0, self.height() - 2)) - w = self.width() - - if w > screen.width(): - w = screen.width() - if (pos.x() + w) > (screen.x() + screen.width()): - pos.setX(screen.x() + screen.width() - w) - if (pos.x() < screen.x()): - pos.setX(screen.x()) - - top = pos.y() - rh - screen.top() + 2 - bottom = screen.bottom() - pos.y() - h = max(h, popup.minimumHeight()) - if h > bottom: - h = min(max(top, bottom), h) - if top > bottom: - pos.setY(pos.y() - h - rh + 2) - - popup.setGeometry(pos.x(), pos.y(), w, h) - - - def show_complete_window(self): - self.position_complete_window() - self.complete_window.show() - @dynamic_property def all_items(self): def fget(self): diff --git a/src/calibre/gui2/tools.py b/src/calibre/gui2/tools.py index 447e0dc240..39224c8b35 100644 --- a/src/calibre/gui2/tools.py +++ b/src/calibre/gui2/tools.py @@ -24,7 +24,8 @@ from calibre.ebooks.conversion.config import GuiRecommendations, \ load_defaults, load_specifics, save_specifics from calibre.gui2.convert import bulk_defaults_for_input_format -def convert_single_ebook(parent, db, book_ids, auto_conversion=False, out_format=None): +def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{ + out_format=None): changed = False jobs = [] bad = [] @@ -95,7 +96,9 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, out_format msg).exec_() return jobs, changed, bad +# }}} +# Bulk convert {{{ def convert_bulk_ebook(parent, queue, db, book_ids, out_format=None, args=[]): total = len(book_ids) if total == 0: @@ -207,7 +210,9 @@ class QueueBulk(QProgressDialog): self.jobs.reverse() self.queue(self.jobs, self.changed, self.bad, *self.args) -def fetch_scheduled_recipe(arg): +# }}} + +def fetch_scheduled_recipe(arg): # {{{ fmt = prefs['output_format'].lower() pt = PersistentTemporaryFile(suffix='_recipe_out.%s'%fmt.lower()) pt.close() @@ -248,7 +253,9 @@ def fetch_scheduled_recipe(arg): return 'gui_convert', args, _('Fetch news from ')+arg['title'], fmt.upper(), [pt] -def generate_catalog(parent, dbspec, ids, device_manager, db): +# }}} + +def generate_catalog(parent, dbspec, ids, device_manager, db): # {{{ from calibre.gui2.dialogs.catalog import Catalog # Build the Catalog dialog in gui2.dialogs.catalog @@ -306,8 +313,9 @@ def generate_catalog(parent, dbspec, ids, device_manager, db): # Which then calls gui2.convert.gui_conversion:gui_catalog() with the args inline return 'gui_catalog', args, _('Generate catalog'), out.name, d.catalog_sync, \ d.catalog_title +# }}} -def convert_existing(parent, db, book_ids, output_format): +def convert_existing(parent, db, book_ids, output_format): # {{{ already_converted_ids = [] already_converted_titles = [] for book_id in book_ids: @@ -323,3 +331,5 @@ def convert_existing(parent, db, book_ids, output_format): book_ids = [x for x in book_ids if x not in already_converted_ids] return book_ids +# }}} + From 986641d938a3d1636fee4677e077d91b1435aae8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 5 Feb 2011 17:41:15 -0700 Subject: [PATCH 18/20] ... --- src/calibre/gui2/complete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/complete.py b/src/calibre/gui2/complete.py index 8b37a2da4e..0f35b00068 100644 --- a/src/calibre/gui2/complete.py +++ b/src/calibre/gui2/complete.py @@ -91,7 +91,7 @@ class MultiCompleteLineEdit(QLineEdit): self.current_prefix = prefix complete_prefix = prefix.lstrip() if self.sep: - complete_prefix = prefix = prefix.split(self.sep)[-1].lstrip() + complete_prefix = prefix.split(self.sep)[-1].lstrip() self._completer.setCompletionPrefix(complete_prefix) def get_completed_text(self, text): From d483fda433cae5de69fec1736d0e526545b4a790 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 5 Feb 2011 17:44:50 -0700 Subject: [PATCH 19/20] ... --- src/calibre/gui2/complete.py | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/calibre/gui2/complete.py b/src/calibre/gui2/complete.py index 0f35b00068..6cb375e320 100644 --- a/src/calibre/gui2/complete.py +++ b/src/calibre/gui2/complete.py @@ -18,8 +18,6 @@ class CompleteModel(QAbstractListModel): def __init__(self, parent=None): QAbstractListModel.__init__(self, parent) - self.sep = ',' - self.space_before_sep = False self.items = [] def set_items(self, items): @@ -54,6 +52,10 @@ class MultiCompleteLineEdit(QLineEdit): def __init__(self, parent=None): QLineEdit.__init__(self, parent) + + self.sep = ',' + self.space_before_sep = False + self._model = CompleteModel(parent=self) self._completer = c = QCompleter(self._model, self) c.setWidget(self) @@ -136,22 +138,6 @@ class MultiCompleteLineEdit(QLineEdit): self._model.set_items(items) return property(fget=fget, fset=fset) - @dynamic_property - def sep(self): - def fget(self): - return self._model.sep - def fset(self, val): - self._model.sep = val - return property(fget=fget, fset=fset) - - @dynamic_property - def space_before_sep(self): - def fget(self): - return self._model.space_before_sep - def fset(self, val): - self._model.space_before_sep = val - return property(fget=fget, fset=fset) - class MultiCompleteComboBox(EnComboBox): def __init__(self, *args): From 47c25481f25dd5ce8679c46701480f5fc65efa62 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 5 Feb 2011 17:48:26 -0700 Subject: [PATCH 20/20] ... --- src/calibre/gui2/complete.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/calibre/gui2/complete.py b/src/calibre/gui2/complete.py index 6cb375e320..58020f924a 100644 --- a/src/calibre/gui2/complete.py +++ b/src/calibre/gui2/complete.py @@ -63,6 +63,9 @@ class MultiCompleteLineEdit(QLineEdit): c.setCaseSensitivity(Qt.CaseInsensitive) c.setModelSorting(QCompleter.CaseInsensitivelySortedModel) c.setCompletionRole(Qt.DisplayRole) + p = c.popup() + p.setMouseTracking(True) + p.entered.connect(self.item_entered) c.popup().setAlternatingRowColors(True) c.activated.connect(self.completion_selected, @@ -81,6 +84,9 @@ class MultiCompleteLineEdit(QLineEdit): # }}} + def item_entered(self, idx): + self._completer.popup().setCurrentIndex(idx) + def text_edited(self, *args): self.update_completions() self._completer.complete()