mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 18:54:09 -04:00
Allow users to create their own snippets
This commit is contained in:
parent
4e140ea27d
commit
3a30197900
@ -9,13 +9,17 @@ __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
import re, copy, weakref
|
||||
from collections import OrderedDict, namedtuple
|
||||
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.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.widgets import Dialog
|
||||
from calibre.utils.config import JSONConfig
|
||||
from calibre.utils.icu import string_length
|
||||
|
||||
@ -143,6 +147,8 @@ def snippets(refresh=False):
|
||||
_snippets[key] = {'template':snip['template'], 'description':snip['description']}
|
||||
return _snippets
|
||||
|
||||
# Editor integration {{{
|
||||
|
||||
class EditorTabStop(object):
|
||||
|
||||
def __init__(self, left, tab_stops, editor):
|
||||
@ -385,3 +391,231 @@ class SnippetManager(QObject):
|
||||
ev.accept()
|
||||
return True
|
||||
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
|
||||
|
@ -152,7 +152,7 @@ class EditorSettings(BasicSettings):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
BasicSettings.__init__(self, parent)
|
||||
self.dictionaries_changed = False
|
||||
self.dictionaries_changed = self.snippets_changed = False
|
||||
self.l = l = QFormLayout(self)
|
||||
self.setLayout(l)
|
||||
|
||||
@ -217,11 +217,22 @@ class EditorSettings(BasicSettings):
|
||||
d.clicked.connect(self.manage_dictionaries)
|
||||
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):
|
||||
d = ManageDictionaries(self)
|
||||
d.exec_()
|
||||
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):
|
||||
choices = {k:k for k in all_theme_names()}
|
||||
choices['auto'] = _('Automatic (%s)') % default_theme()
|
||||
@ -664,6 +675,10 @@ class Preferences(QDialog):
|
||||
def dictionaries_changed(self):
|
||||
return self.editor_panel.dictionaries_changed
|
||||
|
||||
@property
|
||||
def snippets_changed(self):
|
||||
return self.editor_panel.snippets_changed
|
||||
|
||||
@property
|
||||
def toolbars_changed(self):
|
||||
return self.toolbars_panel.changed
|
||||
|
Loading…
x
Reference in New Issue
Block a user