Allow users to create their own snippets

This commit is contained in:
Kovid Goyal 2015-01-08 19:00:06 +05:30
parent 4e140ea27d
commit 3a30197900
2 changed files with 252 additions and 3 deletions

View File

@ -9,13 +9,17 @@ __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import re, copy, weakref import re, copy, weakref
from collections import OrderedDict, namedtuple from collections import OrderedDict, namedtuple
from itertools import groupby from itertools import groupby
from operator import attrgetter from operator import attrgetter, itemgetter
from PyQt5.Qt import Qt, QObject from PyQt5.Qt import (
Qt, QObject, QSize, QVBoxLayout, QStackedLayout, QWidget, QLineEdit,
QToolButton, QIcon, QHBoxLayout, QPushButton, QListWidget, QListWidgetItem,
QGridLayout, QPlainTextEdit, QLabel)
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.gui2.tweak_book.editor import all_text_syntaxes from calibre.gui2.tweak_book.editor import all_text_syntaxes
from calibre.gui2.tweak_book.editor.smarts.utils import get_text_before_cursor from calibre.gui2.tweak_book.editor.smarts.utils import get_text_before_cursor
from calibre.gui2.tweak_book.widgets import Dialog
from calibre.utils.config import JSONConfig from calibre.utils.config import JSONConfig
from calibre.utils.icu import string_length from calibre.utils.icu import string_length
@ -143,6 +147,8 @@ def snippets(refresh=False):
_snippets[key] = {'template':snip['template'], 'description':snip['description']} _snippets[key] = {'template':snip['template'], 'description':snip['description']}
return _snippets return _snippets
# Editor integration {{{
class EditorTabStop(object): class EditorTabStop(object):
def __init__(self, left, tab_stops, editor): def __init__(self, left, tab_stops, editor):
@ -385,3 +391,231 @@ class SnippetManager(QObject):
ev.accept() ev.accept()
return True return True
return False return False
# }}}
# Config {{{
class EditSnippet(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.l = l = QGridLayout(self)
def add_row(*args):
r = l.rowCount()
if len(args) == 1:
l.addWidget(args[0], r, 0, 1, 2)
else:
la = QLabel(args[0])
l.addWidget(la, r, 0, Qt.AlignRight), l.addWidget(args[1], r, 1)
la.setBuddy(args[1])
self.heading = la = QLabel('<h2>\xa0')
add_row(la)
self.name = n = QLineEdit(self)
n.setPlaceholderText(_('The name of this snippet'))
add_row(_('&Name:'), n)
self.trig = t = QLineEdit(self)
t.setPlaceholderText(_('The text used to trigger this snippet'))
add_row(_('Tri&gger:'), t)
self.template = t = QPlainTextEdit(self)
la.setBuddy(t)
add_row(_('&Template:'), t)
self.types = t = QListWidget(self)
t.setFlow(t.LeftToRight)
t.setWrapping(True), t.setResizeMode(t.Adjust), t.setSpacing(5)
fm = t.fontMetrics()
t.setMaximumHeight(2*(fm.ascent() + fm.descent()) + 25)
add_row(_('&File types:'), t)
t.setToolTip(_('Which file types this snippet should be active in'))
i = QListWidgetItem(_('All'), t)
i.setData(Qt.UserRole, '*')
i.setCheckState(Qt.Checked)
i.setFlags(i.flags() | Qt.ItemIsUserCheckable)
for ftype in sorted(all_text_syntaxes):
i = QListWidgetItem(ftype, t)
i.setData(Qt.UserRole, ftype)
i.setCheckState(Qt.Checked)
i.setFlags(i.flags() | Qt.ItemIsUserCheckable)
self.creating_snippet = False
@dynamic_property
def snip(self):
def fset(self, snip):
self.creating_snippet = not snip
self.heading.setText('<h2>' + (_('Create a snippet') if self.creating_snippet else _('Edit snippet')))
snip = snip or {}
self.name.setText(snip.get('description') or '')
self.trig.setText(snip.get('trigger') or '')
self.template.setPlainText(snip.get('template') or '')
ftypes = snip.get('syntaxes', ())
for i in xrange(self.types.count()):
i = self.types.item(i)
ftype = i.data(Qt.UserRole)
i.setCheckState(Qt.Checked if ftype in ftypes else Qt.Unchecked)
if self.creating_snippet:
self.types.item(0).setCheckState(Qt.Checked)
(self.name if self.creating_snippet else self.template).setFocus(Qt.OtherFocusReason)
def fget(self):
ftypes = []
for i in xrange(self.types.count()):
i = self.types.item(i)
if i.checkState() == Qt.Checked:
ftypes.append(i.data(Qt.UserRole))
return {'description':self.name.text().strip(), 'trigger':self.trig.text(), 'template':self.template.toPlainText(), 'syntaxes':ftypes}
return property(fget=fget, fset=fset)
def validate(self):
snip = self.snip
err = None
if not snip['description']:
err = _('You must provide a name for this snippet')
elif not snip['trigger']:
err = _('You must provide a trigger for this snippet')
elif not snip['template']:
err = _('You must provide a template for this snippet')
elif not snip['syntaxes']:
err = _('You must specify at least one file type')
return err
class UserSnippets(Dialog):
def __init__(self, parent=None):
Dialog.__init__(self, _('Create/Edit snippets'), 'snippet-editor', parent=parent)
def setup_ui(self):
self.setWindowIcon(QIcon(I('modified.png')))
self.l = l = QVBoxLayout(self)
self.stack = s = QStackedLayout()
l.addLayout(s), l.addWidget(self.bb)
self.listc = c = QWidget(self)
s.addWidget(c)
c.l = l = QVBoxLayout(c)
c.h = h = QHBoxLayout()
l.addLayout(h)
self.search_bar = sb = QLineEdit(self)
sb.setPlaceholderText(_('Search for a snippet'))
h.addWidget(sb)
self.next_button = b = QPushButton(_('&Next'))
b.clicked.connect(self.find_next)
h.addWidget(b)
c.h2 = h = QHBoxLayout()
l.addLayout(h)
self.snip_list = sl = QListWidget(self)
sl.doubleClicked.connect(self.edit_snippet)
h.addWidget(sl)
c.l2 = l = QVBoxLayout()
h.addLayout(l)
self.add_button = b = QToolButton(self)
b.setIcon(QIcon(I('plus.png'))), b.setText(_('&Add snippet')), b.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
b.clicked.connect(self.add_snippet)
l.addWidget(b)
self.edit_button = b = QToolButton(self)
b.setIcon(QIcon(I('modified.png'))), b.setText(_('&Edit snippet')), b.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
b.clicked.connect(self.edit_snippet)
l.addWidget(b)
self.add_button = b = QToolButton(self)
b.setIcon(QIcon(I('minus.png'))), b.setText(_('&Remove snippet')), b.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
b.clicked.connect(self.remove_snippet)
l.addWidget(b)
for i, snip in enumerate(sorted(user_snippets.get('snippets', []), key=itemgetter('trigger'))):
item = self.snip_to_item(snip)
if i == 0:
self.snip_list.setCurrentItem(item)
self.edit_snip = es = EditSnippet(self)
self.stack.addWidget(es)
def snip_to_text(self, snip):
return '%s - %s' % (snip['trigger'], snip['description'])
def snip_to_item(self, snip):
i = QListWidgetItem(self.snip_to_text(snip), self.snip_list)
i.setData(Qt.UserRole, copy.deepcopy(snip))
return i
def reject(self):
if self.stack.currentIndex() > 0:
self.stack.setCurrentIndex(0)
return
return Dialog.reject(self)
def accept(self):
if self.stack.currentIndex() > 0:
err = self.edit_snip.validate()
if err is None:
self.stack.setCurrentIndex(0)
if self.edit_snip.creating_snippet:
item = self.snip_to_item(self.edit_snip.snip)
else:
item = self.snip_list.currentItem()
snip = self.edit_snip.snip
item.setText(self.snip_to_text(snip))
item.setData(Qt.UserRole, snip)
self.snip_list.setCurrentItem(item)
self.snip_list.scrollToItem(item)
else:
error_dialog(self, _('Invalid snippet'), err, show=True)
return
user_snippets['snippets'] = [self.snip_list.item(i).data(Qt.UserRole) for i in xrange(self.snip_list.count())]
snippets(refresh=True)
return Dialog.accept(self)
def sizeHint(self):
return QSize(900, 600)
def edit_snippet(self, *args):
item = self.snip_list.currentItem()
if item is None:
return error_dialog(self, _('Cannot edit snippet'), _('No snippet selected'), show=True)
self.stack.setCurrentIndex(1)
self.edit_snip.snip = item.data(Qt.UserRole)
def add_snippet(self, *args):
self.stack.setCurrentIndex(1)
self.edit_snip.snip = None
def remove_snippet(self, *args):
item = self.snip_list.currentItem()
if item is not None:
self.snip_list.takeItem(self.snip_list.row(item))
def find_next(self, *args):
q = self.search_bar.text().strip()
if not q:
return
matches = self.snip_list.findItems(q, Qt.MatchContains | Qt.MatchWrap)
if len(matches) < 1:
return error_dialog(self, _('No snippets found'), _(
'No snippets found for query: %s') % q, show=True)
ci = self.snip_list.currentItem()
try:
item = matches[(matches.index(ci) + 1) % len(matches)]
except Exception:
item = matches[0]
self.snip_list.setCurrentItem(item)
self.snip_list.scrollToItem(item)
# }}}
if __name__ == '__main__':
from calibre.gui2 import Application
app = Application([])
d = UserSnippets()
d.exec_()
del app

View File

@ -152,7 +152,7 @@ class EditorSettings(BasicSettings):
def __init__(self, parent=None): def __init__(self, parent=None):
BasicSettings.__init__(self, parent) BasicSettings.__init__(self, parent)
self.dictionaries_changed = False self.dictionaries_changed = self.snippets_changed = False
self.l = l = QFormLayout(self) self.l = l = QFormLayout(self)
self.setLayout(l) self.setLayout(l)
@ -217,11 +217,22 @@ class EditorSettings(BasicSettings):
d.clicked.connect(self.manage_dictionaries) d.clicked.connect(self.manage_dictionaries)
l.addRow(d) l.addRow(d)
self.snippets = s = QPushButton(_('Manage sni&ppets'), self)
s.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
s.clicked.connect(self.manage_snippets)
l.addRow(s)
def manage_dictionaries(self): def manage_dictionaries(self):
d = ManageDictionaries(self) d = ManageDictionaries(self)
d.exec_() d.exec_()
self.dictionaries_changed = True self.dictionaries_changed = True
def manage_snippets(self):
from calibre.gui2.tweak_book.editor.snippets import UserSnippets
d = UserSnippets(self)
if d.exec_() == d.Accepted:
self.snippets_changed = True
def theme_choices(self): def theme_choices(self):
choices = {k:k for k in all_theme_names()} choices = {k:k for k in all_theme_names()}
choices['auto'] = _('Automatic (%s)') % default_theme() choices['auto'] = _('Automatic (%s)') % default_theme()
@ -664,6 +675,10 @@ class Preferences(QDialog):
def dictionaries_changed(self): def dictionaries_changed(self):
return self.editor_panel.dictionaries_changed return self.editor_panel.dictionaries_changed
@property
def snippets_changed(self):
return self.editor_panel.snippets_changed
@property @property
def toolbars_changed(self): def toolbars_changed(self):
return self.toolbars_panel.changed return self.toolbars_panel.changed