#!/usr/pkg/bin/python3.13


__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'

import copy
import re
import weakref
from collections import OrderedDict, namedtuple
from itertools import groupby
from operator import attrgetter, itemgetter
from qt.core import (
    QDialog, QDialogButtonBox, QFrame, QGridLayout, QHBoxLayout, QIcon, QLabel,
    QLineEdit, QListView, QListWidget, QListWidgetItem, QObject, QPushButton, QSize,
    QStackedLayout, Qt, QTextCursor, QToolButton, QVBoxLayout, QWidget
)

from calibre.constants import ismacos
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, PlainTextEdit
from calibre.utils.config import JSONConfig
from calibre.utils.icu import string_length as strlen
from calibre.utils.localization import localize_user_manual_link
from polyglot.builtins import codepoint_to_chr, iteritems, itervalues

string_length = lambda x: strlen(str(x))  # Needed on narrow python builds, as subclasses of unicode dont work
KEY = Qt.Key.Key_J
MODIFIER = Qt.KeyboardModifier.MetaModifier if ismacos else Qt.KeyboardModifier.ControlModifier

SnipKey = namedtuple('SnipKey', 'trigger syntaxes')


def snip_key(trigger, *syntaxes):
    if '*' in syntaxes:
        syntaxes = all_text_syntaxes
    return SnipKey(trigger, frozenset(syntaxes))


def contains(l1, r1, l2, r2):
    # True iff (l2, r2) if contained in (l1, r1)
    return l2 > l1 and r2 < r1


builtin_snippets = {  # {{{
    snip_key('Lorem', 'html', 'xml'):  {
        'description': _('Insert filler text'),
        'template': '''\
<p>The actual teachings of the great explorer of the truth, the master-builder
of human happiness. No one rejects, dislikes, or avoids pleasure itself,
because it is pleasure, but because those who do not know how to pursue
pleasure rationally encounter consequences that are extremely painful.</p>

<p>Nor again is there anyone who loves or pursues or desires to obtain pain of
itself, because it is pain, but because occasionally circumstances occur in
which toil and pain can procure him some great pleasure. To take a trivial
example, which of us ever undertakes laborious physical exercise, except to
obtain some advantage from it? But.</p>
''',
    },

    snip_key('<<', 'html', 'xml'):  {
        'description': _('Insert a tag'),
        'template': '<$1>${2*}</$1>$3',
    },

    snip_key('<>', 'html', 'xml'): {
        'description': _('Insert a self closing tag'),
        'template': '<$1/>$2',
    },

    snip_key('<a', 'html'): {
        'description': _('Insert a HTML link'),
        'template': '<a href="${1:filename}">${2*}</a>$3',
    },

    snip_key('<i', 'html'): {
        'description': _('Insert a HTML image'),
        'template': '<img src="${1:filename}" alt="${2*:description}" />$3',
    },

    snip_key('<c', 'html'): {
        'description': _('Insert a HTML tag with a class'),
        'template': '<$1 class="${2:classname}">${3*}</$1>$4',
    },

}  # }}}

# Parsing of snippets {{{
escape = unescape = None


def escape_funcs():
    global escape, unescape
    if escape is None:
        escapem = {('\\' + x):codepoint_to_chr(i+1) for i, x in enumerate('\\${}')}
        escape_pat = re.compile('|'.join(map(re.escape, escapem)))
        escape = lambda x: escape_pat.sub(lambda m: escapem[m.group()], x.replace(r'\\', '\x01'))
        unescapem = {v:k[1] for k, v in iteritems(escapem)}
        unescape_pat = re.compile('|'.join(unescapem))
        unescape = lambda x:unescape_pat.sub(lambda m:unescapem[m.group()], x)
    return escape, unescape


class TabStop(str):

    def __new__(self, raw, start_offset, tab_stops, is_toplevel=True):
        if raw.endswith('}'):
            unescape = escape_funcs()[1]
            num, default = raw[2:-1].partition(':')[0::2]
            # Look for tab stops defined in the default text
            uraw, child_stops = parse_template(unescape(default), start_offset=start_offset, is_toplevel=False, grouped=False)
            for c in child_stops:
                c.parent = self
            tab_stops.extend(child_stops)
            self = str.__new__(self, uraw)
            if num.endswith('*'):
                self.takes_selection = True
                num = num[:-1]
            else:
                self.takes_selection = False
            self.num = int(num)
        else:
            self = str.__new__(self, '')
            self.num = int(raw[1:])
            self.takes_selection = False
        self.start = start_offset
        self.is_toplevel = is_toplevel
        self.is_mirror = False
        self.parent = None
        tab_stops.append(self)
        return self

    def __repr__(self):
        return 'TabStop(text=%s num=%d start=%d is_mirror=%s takes_selection=%s is_toplevel=%s)' % (
            str.__repr__(self), self.num, self.start, self.is_mirror, self.takes_selection, self.is_toplevel)


def parse_template(template, start_offset=0, is_toplevel=True, grouped=True):
    escape, unescape = escape_funcs()
    template = escape(template)
    pos, parts, tab_stops = start_offset, [], []
    for part in re.split(r'(\$(?:\d+|\{[^}]+\}))', template):
        is_tab_stop = part.startswith('$')
        if is_tab_stop:
            ts = TabStop(part, pos, tab_stops, is_toplevel=is_toplevel)
            parts.append(ts)
        else:
            parts.append(unescape(part))
        pos += string_length(parts[-1])
    if grouped:
        key = attrgetter('num')
        tab_stops.sort(key=key)
        ans = OrderedDict()
        for num, stops in groupby(tab_stops, key):
            stops = tuple(stops)
            for ts in stops[1:]:
                ts.is_mirror = True
            ans[num] = stops
        tab_stops = ans
    return ''.join(parts), tab_stops

# }}}


_snippets = None
user_snippets = JSONConfig('editor_snippets')


def snippets(refresh=False):
    global _snippets
    if _snippets is None or refresh:
        _snippets = copy.deepcopy(builtin_snippets)
        for snip in user_snippets.get('snippets', []):
            if snip['trigger'] and isinstance(snip['trigger'], str):
                key = snip_key(snip['trigger'], *snip['syntaxes'])
                _snippets[key] = {'template':snip['template'], 'description':snip['description']}
        _snippets = sorted(iteritems(_snippets), key=(lambda key_snip:string_length(key_snip[0].trigger)), reverse=True)
    return _snippets

# Editor integration {{{


class EditorTabStop:

    def __init__(self, left, tab_stops, editor):
        self.editor = weakref.ref(editor)
        tab_stop = tab_stops[0]
        self.num = tab_stop.num
        self.is_mirror = tab_stop.is_mirror
        self.is_deleted = False
        self.is_toplevel = tab_stop.is_toplevel
        self.takes_selection = tab_stop.takes_selection
        self.left = left + tab_stop.start
        l = string_length(tab_stop)
        self.right = self.left + l
        self.mirrors = tuple(EditorTabStop(left, [ts], editor) for ts in tab_stops[1:])
        self.ignore_position_update = False
        self.join_previous_edit = False
        self.transform = None
        self.has_transform = self.transform is not None

    def __enter__(self):
        self.join_previous_edit = True

    def __exit__(self, *args):
        self.join_previous_edit = False

    def __repr__(self):
        return 'EditorTabStop(num={!r} text={!r} left={!r} right={!r} is_deleted={!r} mirrors={!r})'.format(
            self.num, self.text, self.left, self.right, self.is_deleted, self.mirrors)
    __str__ = __unicode__ = __repr__

    def apply_selected_text(self, text):
        if self.takes_selection and not self.is_deleted:
            with self:
                self.text = text
            for m in self.mirrors:
                with m:
                    m.text = text

    @property
    def text(self):
        editor = self.editor()
        if editor is None or self.is_deleted:
            return ''
        c = editor.textCursor()
        c.setPosition(self.left), c.setPosition(self.right, QTextCursor.MoveMode.KeepAnchor)
        return editor.selected_text_from_cursor(c)

    @text.setter
    def text(self, text):
        editor = self.editor()
        if editor is None or self.is_deleted:
            return
        c = editor.textCursor()
        c.joinPreviousEditBlock() if self.join_previous_edit else c.beginEditBlock()
        c.setPosition(self.left), c.setPosition(self.right, QTextCursor.MoveMode.KeepAnchor)
        c.insertText(text)
        c.endEditBlock()

    def set_editor_cursor(self, editor):
        if not self.is_deleted:
            c = editor.textCursor()
            c.setPosition(self.left), c.setPosition(self.right, QTextCursor.MoveMode.KeepAnchor)
            editor.setTextCursor(c)

    def contained_in(self, left, right):
        return contains(left, right, self.left, self.right)

    def contains(self, left, right):
        return contains(self.left, self.right, left, right)

    def update_positions(self, position, chars_removed, chars_added):
        for m in self.mirrors:
            m.update_positions(position, chars_removed, chars_added)
        if position > self.right or self.is_deleted or self.ignore_position_update:
            return
        # First handle deletions
        if chars_removed > 0:
            if self.contained_in(position, position + chars_removed):
                self.is_deleted = True
                return
            if position <= self.left:
                self.left = max(self.left - chars_removed, position)
            if position <= self.right:
                self.right = max(self.right - chars_removed, position)

        if chars_added > 0:
            if position < self.left:
                self.left += chars_added
            if position <= self.right:
                self.right += chars_added


class Template(list):

    def __new__(self, tab_stops):
        self = list.__new__(self)
        self.left_most_ts = self.right_most_ts = None
        self.extend(tab_stops)
        for c in self:
            if self.left_most_ts is None or self.left_most_ts.left > c.left:
                self.left_most_ts = c
            if self.right_most_ts is None or self.right_most_ts.right <= c.right:
                self.right_most_ts = c
        self.has_tab_stops = bool(self)
        self.active_tab_stop = None
        return self

    @property
    def left_most_position(self):
        return getattr(self.left_most_ts, 'left', None)

    @property
    def right_most_position(self):
        return getattr(self.right_most_ts, 'right', None)

    def contains_cursor(self, cursor):
        if not self.has_tab_stops:
            return False
        pos = cursor.position()
        if self.left_most_position <= pos <= self.right_most_position:
            return True
        return False

    def jump_to_next(self, editor):
        if self.active_tab_stop is None:
            self.active_tab_stop = ts = self.find_closest_tab_stop(editor.textCursor().position())
            if ts is not None:
                ts.set_editor_cursor(editor)
            return ts
        ts = self.active_tab_stop
        if not ts.is_deleted:
            if ts.has_transform:
                ts.text = ts.transform(ts.text)
            for m in ts.mirrors:
                if not m.is_deleted:
                    m.text = ts.text
        for x in self:
            if x.num > ts.num and not x.is_deleted:
                self.active_tab_stop = x
                x.set_editor_cursor(editor)
                return x

    def remains_active(self):
        if self.active_tab_stop is None:
            return False
        ts = self.active_tab_stop
        for x in self:
            if x.num > ts.num and not x.is_deleted:
                return True
        return bool(ts.mirrors) or ts.has_transform

    def find_closest_tab_stop(self, position):
        ans = dist = None
        for c in self:
            x = min(abs(c.left - position), abs(c.right - position))
            if ans is None or x < dist:
                dist, ans = x, c
        return ans


def expand_template(editor, trigger, template):
    c = editor.textCursor()
    c.beginEditBlock()
    c.setPosition(c.position())
    right = c.position()
    left = right - string_length(trigger)
    text, tab_stops = parse_template(template)
    c.setPosition(left), c.setPosition(right, QTextCursor.MoveMode.KeepAnchor), c.insertText(text)
    editor_tab_stops = [EditorTabStop(left, ts, editor) for ts in itervalues(tab_stops)]

    tl = Template(editor_tab_stops)
    if tl.has_tab_stops:
        tl.active_tab_stop = ts = editor_tab_stops[0]
        ts.set_editor_cursor(editor)
    else:
        editor.setTextCursor(c)
    c.endEditBlock()
    return tl


def find_matching_snip(text, syntax=None, snip_func=None):
    ans_snip = ans_trigger = None
    for key, snip in (snip_func or snippets)():
        if text.endswith(key.trigger) and (syntax in key.syntaxes or syntax is None):
            ans_snip, ans_trigger = snip, key.trigger
            break
    return ans_snip, ans_trigger


class SnippetManager(QObject):

    def __init__(self, editor):
        QObject.__init__(self, editor)
        self.active_templates = []
        self.last_selected_text = ''
        editor.document().contentsChange.connect(self.contents_changed)
        self.snip_func = None

    def contents_changed(self, position, chars_removed, chars_added):
        for template in self.active_templates:
            for ets in template:
                ets.update_positions(position, chars_removed, chars_added)

    def get_active_template(self, cursor):
        remove = []
        at = None
        pos = cursor.position()
        for template in self.active_templates:
            if at is None and template.contains_cursor(cursor):
                at = template
            elif pos > template.right_most_position or pos < template.left_most_position:
                remove.append(template)
        for template in remove:
            self.active_templates.remove(template)
        return at

    def handle_key_press(self, ev):
        editor = self.parent()
        if ev.key() == KEY and ev.modifiers() & MODIFIER:
            at = self.get_active_template(editor.textCursor())
            if at is not None:
                if at.jump_to_next(editor) is None:
                    self.active_templates.remove(at)
                else:
                    if not at.remains_active():
                        self.active_templates.remove(at)
                ev.accept()
                return True
            lst, self.last_selected_text = self.last_selected_text, editor.selected_text
            if self.last_selected_text:
                editor.textCursor().insertText('')
                ev.accept()
                return True
            c, text = get_text_before_cursor(editor)
            snip, trigger = find_matching_snip(text, editor.syntax, self.snip_func)
            if snip is None:
                error_dialog(self.parent(), _('No snippet found'), _(
                    'No matching snippet was found'), show=True)
                self.last_selected_text = self.last_selected_text or lst
                return True
            template = expand_template(editor, trigger, snip['template'])
            if template.has_tab_stops:
                self.active_templates.append(template)
                if lst:
                    for ts in template:
                        ts.apply_selected_text(lst)

            ev.accept()
            return True
        return False
# }}}

# Config {{{


class SnippetTextEdit(PlainTextEdit):

    def __init__(self, text, parent=None):
        PlainTextEdit.__init__(self, parent)
        if text:
            self.setPlainText(text)
        self.snippet_manager = SnippetManager(self)

    def keyPressEvent(self, ev):
        if self.snippet_manager.handle_key_press(ev):
            return
        PlainTextEdit.keyPressEvent(self, ev)


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.AlignmentFlag.AlignRight), l.addWidget(args[1], r, 1)
                la.setBuddy(args[1])

        self.heading = la = QLabel('<h2>\xa0')
        add_row(la)
        self.helpl = la = QLabel(_('For help with snippets, see the <a href="%s">User Manual</a>') %
                                 localize_user_manual_link('https://manual.calibre-ebook.com/snippets.html'))
        la.setOpenExternalLinks(True)
        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 = PlainTextEdit(self)
        la.setBuddy(t)
        add_row(_('&Template:'), t)

        self.types = t = QListWidget(self)
        t.setFlow(QListView.Flow.LeftToRight)
        t.setWrapping(True), t.setResizeMode(QListView.ResizeMode.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'))

        self.frame = f = QFrame(self)
        f.setFrameShape(QFrame.Shape.HLine)
        add_row(f)
        self.test = d = SnippetTextEdit('', self)
        d.snippet_manager.snip_func = self.snip_func
        d.setToolTip(_('You can test your snippet here'))
        d.setMaximumHeight(t.maximumHeight() + 15)
        add_row(_('T&est:'), d)

        i = QListWidgetItem(_('All'), t)
        i.setData(Qt.ItemDataRole.UserRole, '*')
        i.setCheckState(Qt.CheckState.Checked)
        i.setFlags(i.flags() | Qt.ItemFlag.ItemIsUserCheckable)
        for ftype in sorted(all_text_syntaxes):
            i = QListWidgetItem(ftype, t)
            i.setData(Qt.ItemDataRole.UserRole, ftype)
            i.setCheckState(Qt.CheckState.Checked)
            i.setFlags(i.flags() | Qt.ItemFlag.ItemIsUserCheckable)

        self.creating_snippet = False

    def snip_func(self):
        key = snip_key(self.trig.text(), '*')
        return ((key, self.snip),)

    def apply_snip(self, snip, creating_snippet=None):
        self.creating_snippet = not snip if creating_snippet is None else creating_snippet
        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 range(self.types.count()):
            i = self.types.item(i)
            ftype = i.data(Qt.ItemDataRole.UserRole)
            i.setCheckState(Qt.CheckState.Checked if ftype in ftypes else Qt.CheckState.Unchecked)
        if self.creating_snippet and not ftypes:
            self.types.item(0).setCheckState(Qt.CheckState.Checked)
        (self.name if self.creating_snippet else self.template).setFocus(Qt.FocusReason.OtherFocusReason)

    @property
    def snip(self):
        ftypes = []
        for i in range(self.types.count()):
            i = self.types.item(i)
            if i.checkState() == Qt.CheckState.Checked:
                ftypes.append(i.data(Qt.ItemDataRole.UserRole))
        return {'description':self.name.text().strip(), 'trigger':self.trig.text(), 'template':self.template.toPlainText(), 'syntaxes':ftypes}

    @snip.setter
    def snip(self, snip):
        self.apply_snip(snip)

    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)
        self.setWindowIcon(QIcon(I('snippets.png')))

    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.ToolButtonStyle.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.ToolButtonStyle.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.ToolButtonStyle.ToolButtonTextUnderIcon)
        b.clicked.connect(self.remove_snippet)
        l.addWidget(b)

        self.add_button = b = QToolButton(self)
        b.setIcon(QIcon(I('config.png'))), b.setText(_('Change &built-in')), b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextUnderIcon)
        b.clicked.connect(self.change_builtin)
        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 '{} - {}'.format(snip['trigger'], snip['description'])

    def snip_to_item(self, snip):
        i = QListWidgetItem(self.snip_to_text(snip), self.snip_list)
        i.setData(Qt.ItemDataRole.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.ItemDataRole.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.ItemDataRole.UserRole) for i in range(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.ItemDataRole.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.MatchFlag.MatchContains | Qt.MatchFlag.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)

    def change_builtin(self):
        d = QDialog(self)
        lw = QListWidget(d)
        for (trigger, syntaxes), snip in iteritems(builtin_snippets):
            snip = copy.deepcopy(snip)
            snip['trigger'], snip['syntaxes'] = trigger, syntaxes
            i = QListWidgetItem(self.snip_to_text(snip), lw)
            i.setData(Qt.ItemDataRole.UserRole, snip)
        d.l = l = QVBoxLayout(d)
        l.addWidget(QLabel(_('Choose the built-in snippet to modify:')))
        l.addWidget(lw)
        lw.itemDoubleClicked.connect(d.accept)
        d.bb = bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
        l.addWidget(bb)
        bb.accepted.connect(d.accept), bb.rejected.connect(d.reject)
        if d.exec() == QDialog.DialogCode.Accepted and lw.currentItem() is not None:
            self.stack.setCurrentIndex(1)
            self.edit_snip.apply_snip(lw.currentItem().data(Qt.ItemDataRole.UserRole), creating_snippet=True)
# }}}


if __name__ == '__main__':
    from calibre.gui2 import Application
    app = Application([])
    d = UserSnippets()
    d.exec()
    del app
