More work on the new recipe editor

This commit is contained in:
Kovid Goyal 2014-11-22 22:20:36 +05:30
parent d77b91cb39
commit fb60427a3d

View File

@ -6,15 +6,20 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import os import os, re, textwrap, time
from PyQt5.Qt import ( from PyQt5.Qt import (
QVBoxLayout, QStackedWidget, QSize, QPushButton, QIcon, QWidget, QListView, QVBoxLayout, QStackedWidget, QSize, QPushButton, QIcon, QWidget, QListView,
QHBoxLayout, QAbstractListModel, Qt, QLabel, QSizePolicy) QHBoxLayout, QAbstractListModel, Qt, QLabel, QSizePolicy, pyqtSignal,
QFormLayout, QSpinBox, QLineEdit, QGroupBox, QListWidget, QListWidgetItem,
QToolButton)
from calibre.gui2 import error_dialog, open_local_file from calibre.gui2 import error_dialog, open_local_file
from calibre.gui2.widgets2 import Dialog from calibre.gui2.widgets2 import Dialog
from calibre.web.feeds.recipes import custom_recipes from calibre.web.feeds.recipes import custom_recipes, compile_recipe
def is_basic_recipe(src):
return re.search(r'^class BasicUserRecipe', src, flags=re.MULTILINE) is not None
class CustomRecipeModel(QAbstractListModel): # {{{ class CustomRecipeModel(QAbstractListModel): # {{{
@ -100,8 +105,31 @@ class CustomRecipeModel(QAbstractListModel): # {{{
self.endResetModel() self.endResetModel()
# }}} # }}}
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)
src = textwrap.dedent('''\
from calibre.web.feeds.news import {base}
class {classname}({base}):
title = {title!r}
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,
max_articles_per_feed=max_articles_per_feed, base='AutomaticNewsRecipe')
return src
class RecipeList(QWidget): class RecipeList(QWidget):
edit_recipe = pyqtSignal(object)
def __init__(self, parent, model): def __init__(self, parent, model):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
@ -129,10 +157,11 @@ class RecipeList(QWidget):
l.addWidget(la) l.addWidget(la)
l.setSpacing(20) l.setSpacing(20)
# TODO: Implement these buttons
self.edit_button = b = QPushButton(QIcon(I('modified.png')), _('&Edit this recipe'), w) self.edit_button = b = QPushButton(QIcon(I('modified.png')), _('&Edit this recipe'), w)
b.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) b.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
b.clicked.connect(self.edit_requested)
l.addWidget(b) l.addWidget(b)
# TODO: Implement this button
self.remove_button = b = QPushButton(QIcon(I('list_remove.png')), _('&Remove this recipe'), w) self.remove_button = b = QPushButton(QIcon(I('list_remove.png')), _('&Remove this recipe'), w)
b.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) b.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
l.addWidget(b) l.addWidget(b)
@ -149,8 +178,110 @@ class RecipeList(QWidget):
self.stacks.setCurrentIndex(1) self.stacks.setCurrentIndex(1)
self.title.setText('<h2 style="text-align:center">%s</h2>' % self.model.title(cur)) self.title.setText('<h2 style="text-align:center">%s</h2>' % self.model.title(cur))
def edit_requested(self):
idx = self.view.currentIndex()
if idx.isValid():
src = self.model.script(idx)
if src is not None:
self.edit_recipe.emit(src)
class BasicRecipe(QWidget): class BasicRecipe(QWidget):
def __init__(self, parent):
QWidget.__init__(self, parent)
self.original_title_of_recipe = None
self.l = l = QFormLayout(self)
self.hm = hm = QLabel(_(
'Create a basic news recipe, by adding RSS feeds to it.\n'
'For some news sources, you will have to use the "Switch to advanced mode"'
'button below to further customize the fetch process.'))
hm.setWordWrap(True)
l.addRow(hm)
self.title = t = QLineEdit(self)
l.addRow(_('Recipe &title:'), t)
self.oldest_article = o = QSpinBox(self)
o.setSuffix(' ' + _('days'))
o.setToolTip(_("The oldest article to download"))
o.setMinimum(1), o.setMaximum(36500)
l.addRow(_('&Oldest article:'), o)
self.max_articles = m = QSpinBox(self)
m.setMinimum(5), m.setMaximum(100)
m.setToolTip(_("Maximum number of articles to download per feed."))
l.addRow(_("&Max. number of articles per feed:"), m)
self.fg = fg = QGroupBox(self)
fg.setTitle(_("Feeds in recipe"))
self.feeds = f = QListWidget(self)
fg.h = QHBoxLayout(fg)
fg.h.addWidget(f)
fg.l = QVBoxLayout()
self.up_button = b = QToolButton(self)
b.setIcon(QIcon(I('arrow-up.png')))
b.setToolTip(_('Move selected feed up'))
fg.l.addWidget(b)
b.clicked.connect(self.move_up)
self.remove_button = b = QToolButton(self)
b.setIcon(QIcon(I('list_remove.png')))
b.setToolTip(_('Remove selected feed'))
fg.l.addWidget(b)
b.clicked.connect(self.remove_feed)
self.down_button = b = QToolButton(self)
b.setIcon(QIcon(I('arrow-down.png')))
b.setToolTip(_('Move selected feed down'))
fg.l.addWidget(b)
b.clicked.connect(self.move_down)
fg.h.addLayout(fg.l)
l.addRow(fg)
# TODO: Implement these
def move_up(self):
pass
def move_down(self):
pass
def remove_feed(self):
pass
@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)
def fset(self, src):
self.feeds.clear()
self.feed_title.setText('')
self.feed_url.setText('')
if src is None:
self.original_title_of_recipe = None
self.title.setText(_('My News Source'))
self.oldest_article.setValue(7)
self.max_articles.setValue(100)
else:
recipe = compile_recipe(src)
self.original_title_of_recipe = recipe.title
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 ()):
i = QListWidgetItem('%s - %s' % (title, url), self.feeds)
i.setData(Qt.UserRole, (title, url))
return property(fget=fget, fset=fset)
class AdvancedRecipe(QWidget):
def __init__(self, parent): def __init__(self, parent):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
@ -166,27 +297,26 @@ class CustomRecipes(Dialog):
l.addWidget(s) l.addWidget(s)
self.recipe_list = rl = RecipeList(self, self.recipe_model) self.recipe_list = rl = RecipeList(self, self.recipe_model)
rl.edit_recipe.connect(self.edit_recipe)
s.addWidget(rl) s.addWidget(rl)
self.basic_recipe = br = BasicRecipe(self)
s.addWidget(br)
self.advanced_recipe = ar = AdvancedRecipe(self)
s.addWidget(ar)
l.addWidget(self.bb) l.addWidget(self.bb)
# TODO: Implement these buttons self.list_actions = []
self.new_button = b = QPushButton(QIcon(I('plus.png')), _('&New recipe'), self) la = lambda *args:self.list_actions.append(args)
b.setToolTip(_('Create a new recipe from scratch')) la('plus.png', _('&New recipe'), _('Create a new recipe from scratch'), self.add_recipe)
la('news.png', _('Customize &builtin recipe'), _('Customize a builtin news download source'), self.customize_recipe)
self.customize_button = b = QPushButton(QIcon(I('news.png')), _('Customize &builtin recipe'), self) la('document_open.png', _('Load recipe from &file'), _('Load a recipe from a &file'), self.load_recipe)
b.setToolTip(_('Customize a builtin news download source')) la('mimetypes/dir.png', _('&Show recipe files'), _('Show the folder containing all recipe files'), self.show_recipe_files)
la('mimetypes/opml.png', _('Import &OPML'), _(
self.load_button = b = QPushButton(QIcon(I('document_open.png')), _('Load recipe from &file'), self) "Import a collection of RSS feeds in OPML format\n"
b.setToolTip(_('Load a recipe from a &file'))
self.files_button = b = QPushButton(QIcon(I('mimetypes/dir.png')), _('&Show recipe files'), self)
b.setToolTip(_('Show the folder containing all recipe files'))
b.clicked.connect(self.show_recipe_files)
self.opml_button = b = QPushButton(QIcon(I('mimetypes/opml.png')), _('Import &OPML'), self)
b.setToolTip(_("Import a collection of RSS feeds in OPML format\n"
"Many RSS readers can export their subscribed RSS feeds\n" "Many RSS readers can export their subscribed RSS feeds\n"
"in OPML format")) "in OPML format"), self.import_opml)
s.currentChanged.connect(self.update_button_box) s.currentChanged.connect(self.update_button_box)
self.update_button_box() self.update_button_box()
@ -196,13 +326,38 @@ class CustomRecipes(Dialog):
bb.clear() bb.clear()
if index == 0: if index == 0:
bb.setStandardButtons(bb.Close) bb.setStandardButtons(bb.Close)
bb.addButton(self.new_button, bb.ActionRole) for icon, text, tooltip, receiver in self.list_actions:
bb.addButton(self.customize_button, bb.ActionRole) b = bb.addButton(text, bb.ActionRole)
bb.addButton(self.load_button, bb.ActionRole) b.setIcon(QIcon(I(icon))), b.setToolTip(tooltip)
bb.addButton(self.files_button, bb.ActionRole) b.clicked.connect(receiver)
bb.addButton(self.opml_button, bb.ActionRole)
else: else:
bb.setStandardButtons(bb.Discard | bb.Save) bb.setStandardButtons(bb.Cancel | bb.Save)
if self.stack.currentIndex() == 1:
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)
def accept(self):
idx = self.stack.currentIndex()
if idx > 0:
self.editing_finished()
self.stack.setCurrentIndex(0)
return
Dialog.accept(self)
def reject(self):
idx = self.stack.currentIndex()
if idx > 0:
self.stack.setCurrentIndex(0)
return
Dialog.reject(self)
def sizeHint(self): def sizeHint(self):
sh = Dialog.sizeHint(self) sh = Dialog.sizeHint(self)
@ -215,10 +370,39 @@ class CustomRecipes(Dialog):
_('No custom recipes created.'), show=True) _('No custom recipes created.'), show=True)
open_local_file(bdir) open_local_file(bdir)
def edit_recipe(self, src):
if is_basic_recipe(src):
self.stack.setCurrentIndex(1)
else:
self.stack.setCurrentIndex(2)
# TODO: Implement these functions
def editing_finished(self):
w = self.stack.currentWidget()
w
def add_recipe(self):
pass
def customize_recipe(self):
pass
def load_recipe(self):
pass
def import_opml(self):
pass
def switch_to_advanced(self):
self.stack.setCurrentIndex(2)
def switch_to_basic(self):
self.stack.setCurrentIndex(1)
if __name__ == '__main__': if __name__ == '__main__':
from calibre.gui2 import Application from calibre.gui2 import Application
from calibre.web.feeds.recipes.model import RecipeModel from calibre.web.feeds.recipes.model import RecipeModel
app = Application([]) app = Application([])
CustomRecipes(RecipeModel()).exec_() CustomRecipes(RecipeModel()).exec_()
del app del app