diff --git a/src/calibre/gui2/dialogs/custom_recipes.py b/src/calibre/gui2/dialogs/custom_recipes.py index 7958e2a8a8..e0560d21cd 100644 --- a/src/calibre/gui2/dialogs/custom_recipes.py +++ b/src/calibre/gui2/dialogs/custom_recipes.py @@ -17,6 +17,7 @@ from PyQt5.Qt import ( from calibre.gui2 import error_dialog, open_local_file from calibre.gui2.widgets2 import Dialog from calibre.web.feeds.recipes import custom_recipes, compile_recipe +from calibre.gui2.tweak_book.editor.text import TextEdit def is_basic_recipe(src): return re.search(r'^class BasicUserRecipe', src, flags=re.MULTILINE) is not None @@ -105,24 +106,41 @@ class CustomRecipeModel(QAbstractListModel): # {{{ self.endResetModel() # }}} +def py3_repr(x): + ans = repr(x) + if isinstance(x, bytes) and not ans.startswith('b'): + ans = 'b' + ans + if isinstance(x, unicode) and ans.startswith('u'): + ans = ans[1:] + return ans + def options_to_recipe_source(title, oldest_article, max_articles_per_feed, feeds): classname = 'BasicUserRecipe%d' % int(time.time()) title = unicode(title).strip() or classname indent = ' ' * 8 - feeds = '\n'.join(indent + repr(x) + ',' for x in feeds) if feeds: - feeds = 'feeds = [\n%s%s\n ]' % (indent, feeds) + if len(feeds[0]) == 1: + feeds = '\n'.join('%s%s,' % (indent, py3_repr(url)) for url in feeds) + else: + feeds = '\n'.join('%s(%s, %s),' % (indent, py3_repr(title), py3_repr(url)) for title, url in feeds) + else: + feeds = '' + if feeds: + feeds = 'feeds = [\n%s\n ]' % feeds src = textwrap.dedent('''\ + #!/usr/bin/env python + # vim:fileencoding=utf-8 + from __future__ import unicode_literals, division, absolute_import, print_function from calibre.web.feeds.news import {base} class {classname}({base}): - title = {title!r} + title = {title} oldest_article = {oldest_article} max_articles_per_feed = {max_articles_per_feed} auto_cleanup = True {feeds}''').format( - classname=classname, title=title, oldest_article=oldest_article, feeds=feeds, + classname=classname, title=py3_repr(title), oldest_article=oldest_article, feeds=feeds, max_articles_per_feed=max_articles_per_feed, base='AutomaticNewsRecipe') return src @@ -166,8 +184,12 @@ class RecipeList(QWidget): b.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) l.addWidget(b) + if v.model().rowCount() > 0: + idx = v.model().index(0) + if idx.isValid(): + v.selectionModel().select(idx, v.selectionModel().ClearAndSelect) + self.recipe_selected(v.currentIndex()) v.selectionModel().currentRowChanged.connect(self.recipe_selected) - self.recipe_selected(v.currentIndex()) @property def model(self): @@ -191,6 +213,7 @@ class BasicRecipe(QWidget): QWidget.__init__(self, parent) self.original_title_of_recipe = None self.l = l = QFormLayout(self) + l.setFieldGrowthPolicy(l.ExpandingFieldsGrow) self.hm = hm = QLabel(_( 'Create a basic news recipe, by adding RSS feeds to it.\n' @@ -203,7 +226,7 @@ class BasicRecipe(QWidget): l.addRow(_('Recipe &title:'), t) self.oldest_article = o = QSpinBox(self) - o.setSuffix(' ' + _('days')) + o.setSuffix(' ' + _('day(s)')) o.setToolTip(_("The oldest article to download")) o.setMinimum(1), o.setMaximum(36500) l.addRow(_('&Oldest article:'), o) @@ -237,6 +260,20 @@ class BasicRecipe(QWidget): fg.h.addLayout(fg.l) l.addRow(fg) + self.afg = afg = QGroupBox(self) + afg.setTitle(_('Add feed to recipe')) + afg.l = QFormLayout(afg) + afg.l.setFieldGrowthPolicy(l.ExpandingFieldsGrow) + self.feed_title = ft = QLineEdit(self) + afg.l.addRow(_('Feed title:'), ft) + self.feed_url = fu = QLineEdit(self) + afg.l.addRow(_('Feed &URL:'), fu) + self.afb = b = QPushButton(QIcon(I('plus.png')), _('&Add feed'), self) + b.setToolTip(_('Add this feed to the recipe')) + b.clicked.connect(self.add_feed) + afg.l.addRow(b) + l.addRow(afg) + # TODO: Implement these def move_up(self): pass @@ -247,22 +284,39 @@ class BasicRecipe(QWidget): def remove_feed(self): pass + def add_feed(self): + pass + + def validate(self): + title = self.title.text().strip() + if not title: + error_dialog(self, _('Title required'), _( + 'You must give your news source a title'), show=True) + return False + if self.feeds.count() < 1: + error_dialog(self, _('Feed required'), _( + 'You must add at least one feed to your news source'), show=True) + return False + try: + compile_recipe(self.recipe_source) + except Exception as err: + error_dialog(self, _('Invalid recipe'), _( + 'Failed to compile the recipe, with syntax error: %s' % err), show=True) + return False + return True + @dynamic_property def recipe_source(self): def fget(self): title = self.title.text().strip() - if not title: - error_dialog(self, _('Title required'), _( - 'You must give your news source a title'), show=True) - return - feeds = [self.feeds.itemAt(i).data(Qt.UserRole) for i in xrange(self.feeds.count())] - return options_to_recipe_source(title, self.oldest_article.value(), self.max_articles_per_feed.value(), feeds) + feeds = [self.feeds.item(i).data(Qt.UserRole) for i in xrange(self.feeds.count())] + return options_to_recipe_source(title, self.oldest_article.value(), self.max_articles.value(), feeds) def fset(self, src): self.feeds.clear() - self.feed_title.setText('') - self.feed_url.setText('') + self.feed_title.clear() + self.feed_url.clear() if src is None: self.original_title_of_recipe = None self.title.setText(_('My News Source')) @@ -274,7 +328,8 @@ class BasicRecipe(QWidget): self.title.setText(recipe.title) self.oldest_article.setValue(recipe.oldest_article) self.max_articles.setValue(recipe.max_articles_per_feed) - for title, url in (recipe.feeds or ()): + for x in (recipe.feeds or ()): + title, url = ('', x) if len(x) == 1 else x i = QListWidgetItem('%s - %s' % (title, url), self.feeds) i.setData(Qt.UserRole, (title, url)) @@ -284,6 +339,43 @@ class AdvancedRecipe(QWidget): def __init__(self, parent): QWidget.__init__(self, parent) + self.original_title_of_recipe = None + self.l = l = QVBoxLayout(self) + + self.la = la = QLabel(_( + 'For help with writing advanced news recipes, see the User Manual' + ) % 'http://manual.calibre-ebook.com/news.html') + l.addWidget(la) + + self.editor = TextEdit(self) + l.addWidget(self.editor) + + def validate(self): + src = self.recipe_source + try: + compile_recipe(src) + except Exception as err: + error_dialog(self, _('Invalid recipe'), _( + 'Failed to compile the recipe, with syntax error: %s' % err), show=True) + return False + return True + + @dynamic_property + def recipe_source(self): + + def fget(self): + return self.editor.toPlainText() + + def fset(self, src): + recipe = compile_recipe(src) + self.original_title_of_recipe = recipe.title + self.editor.load_text(src, syntax='python', doc_name='') + + return property(fget=fget, fset=fset) + + def sizeHint(self): + return QSize(800, 500) + class CustomRecipes(Dialog): @@ -336,13 +428,9 @@ class CustomRecipes(Dialog): text = _('S&witch to Advanced mode') tooltip = _('Edit this recipe in advanced mode') receiver = self.switch_to_advanced - else: - text = _('S&witch to Basic mode') - tooltip = _('Edit this recipe in basic mode') - receiver = self.switch_to_basic - b = bb.addButton(text, bb.ActionRole) - b.setToolTip(tooltip) - b.clicked.connect(receiver) + b = bb.addButton(text, bb.ActionRole) + b.setToolTip(tooltip) + b.clicked.connect(receiver) def accept(self): idx = self.stack.currentIndex() @@ -372,15 +460,18 @@ class CustomRecipes(Dialog): def edit_recipe(self, src): if is_basic_recipe(src): + self.basic_recipe.recipe_source = src self.stack.setCurrentIndex(1) else: + self.advanced_recipe.recipe_source = src self.stack.setCurrentIndex(2) # TODO: Implement these functions def editing_finished(self): w = self.stack.currentWidget() - w + if not w.validate(): + return def add_recipe(self): pass @@ -395,11 +486,9 @@ class CustomRecipes(Dialog): pass def switch_to_advanced(self): + self.advanced_recipe.recipe_source = self.basic_recipe.recipe_source self.stack.setCurrentIndex(2) - def switch_to_basic(self): - self.stack.setCurrentIndex(1) - if __name__ == '__main__': from calibre.gui2 import Application from calibre.web.feeds.recipes.model import RecipeModel