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


__license__   = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

from functools import partial

from qt.core import (
    QAbstractListModel, Qt, QKeySequence, QListView, QVBoxLayout, QLabel, QAbstractItemView,
    QHBoxLayout, QWidget, QApplication, QStyledItemDelegate, QStyle, QIcon, QAbstractItemDelegate,
    QTextDocument, QRectF, QFrame, QSize, QFont, QKeyEvent, QRadioButton, QPushButton, QToolButton, QEvent
)

from calibre.gui2 import error_dialog
from calibre.utils.config import XMLConfig
from calibre.utils.icu import sort_key

DEFAULTS = Qt.ItemDataRole.UserRole
DESCRIPTION = Qt.ItemDataRole.UserRole + 1
CUSTOM = Qt.ItemDataRole.UserRole + 2
KEY = Qt.ItemDataRole.UserRole + 3


class Customize(QFrame):

    def __init__(self, index, dup_check, parent=None):
        QFrame.__init__(self, parent)
        self.setFrameShape(QFrame.Shape.StyledPanel)
        self.setFrameShadow(QFrame.Shadow.Raised)
        self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
        self.setAutoFillBackground(True)
        self.l = l = QVBoxLayout(self)
        self.header = la = QLabel(self)
        la.setWordWrap(True)
        l.addWidget(la)
        self.default_shortcuts = QRadioButton(_("&Default"), self)
        self.custom = QRadioButton(_("&Custom"), self)
        self.custom.toggled.connect(self.custom_toggled)
        l.addWidget(self.default_shortcuts)
        l.addWidget(self.custom)
        for which in 1, 2:
            la = QLabel(_("&Shortcut:") if which == 1 else _("&Alternate shortcut:"))
            setattr(self, 'label%d' % which, la)
            h = QHBoxLayout()
            l.addLayout(h)
            h.setContentsMargins(25, -1, -1, -1)
            h.addWidget(la)
            b = QPushButton(_("Click to change"), self)
            la.setBuddy(b)
            b.clicked.connect(partial(self.capture_clicked, which=which))
            b.installEventFilter(self)
            setattr(self, 'button%d' % which, b)
            h.addWidget(b)
            c = QToolButton(self)
            c.setIcon(QIcon(I('clear_left.png')))
            c.setToolTip(_('Clear'))
            h.addWidget(c)
            c.clicked.connect(partial(self.clear_clicked, which=which))
            setattr(self, 'clear%d' % which, c)
        self.data_model = index.model()
        self.capture = 0
        self.key = None
        self.shorcut1 = self.shortcut2 = None
        self.dup_check = dup_check
        self.custom_toggled(False)

    def eventFilter(self, obj, event):
        if self.capture == 0 or obj not in (self.button1, self.button2):
            return QFrame.eventFilter(self, obj, event)
        t = event.type()
        if t == QEvent.Type.ShortcutOverride:
            event.accept()
            return True
        if t == QEvent.Type.KeyPress:
            self.key_press_event(event, 1 if obj is self.button1 else 2)
            return True
        return QFrame.eventFilter(self, obj, event)

    def clear_button(self, which):
        b = getattr(self, 'button%d' % which)
        s = getattr(self, 'shortcut%d' % which, None)
        b.setText(_('None') if s is None else s.toString(QKeySequence.SequenceFormat.NativeText))
        b.setFont(QFont())

    def clear_clicked(self, which=0):
        setattr(self, 'shortcut%d'%which, None)
        self.clear_button(which)

    def custom_toggled(self, checked):
        for w in ('1', '2'):
            for o in ('label', 'button', 'clear'):
                getattr(self, o+w).setEnabled(checked)

    def capture_clicked(self, which=1):
        self.capture = which
        for w in 1, 2:
            self.clear_button(w)
        button = getattr(self, 'button%d'%which)
        button.setText(_('Press a key...'))
        button.setFocus(Qt.FocusReason.OtherFocusReason)
        font = QFont()
        font.setBold(True)
        button.setFont(font)

    def key_press_event(self, ev, which=0):
        code = ev.key()
        if self.capture == 0 or code in (0, Qt.Key.Key_unknown,
                Qt.Key.Key_Shift, Qt.Key.Key_Control, Qt.Key.Key_Alt, Qt.Key.Key_Meta,
                Qt.Key.Key_AltGr, Qt.Key.Key_CapsLock, Qt.Key.Key_NumLock, Qt.Key.Key_ScrollLock):
            return QWidget.keyPressEvent(self, ev)
        sequence = QKeySequence(code|(int(ev.modifiers()) & (~Qt.KeyboardModifier.KeypadModifier)))
        setattr(self, 'shortcut%d'%which, sequence)
        self.clear_button(which)
        self.capture = 0
        dup_desc = self.dup_check(sequence, self.key)
        if dup_desc is not None:
            error_dialog(self, _('Already assigned'),
                    str(sequence.toString(QKeySequence.SequenceFormat.NativeText)) + ' ' +
                    _('already assigned to') + ' ' + dup_desc, show=True)
            self.clear_clicked(which=which)


class Delegate(QStyledItemDelegate):

    def __init__(self, parent=None):
        QStyledItemDelegate.__init__(self, parent)
        self.editing_indices = {}
        self.closeEditor.connect(self.editing_done)

    def to_doc(self, index):
        doc =  QTextDocument()
        doc.setHtml(index.data())
        return doc

    def editing_done(self, editor, hint):
        remove = None
        for row, w in self.editing_indices.items():
            remove = (row, w.data_model.index(row))
        if remove is not None:
            self.editing_indices.pop(remove[0])
            self.sizeHintChanged.emit(remove[1])

    def sizeHint(self, option, index):
        if index.row() in self.editing_indices:
            return QSize(200, 200)
        ans = self.to_doc(index).size().toSize()
        ans.setHeight(ans.height()+10)
        return ans

    def paint(self, painter, option, index):
        painter.save()
        painter.setClipRect(QRectF(option.rect))
        if hasattr(QStyle, 'CE_ItemViewItem'):
            QApplication.style().drawControl(QStyle.ControlElement.CE_ItemViewItem, option, painter)
        elif option.state & QStyle.StateFlag.State_Selected:
            painter.fillRect(option.rect, option.palette.highlight())
        painter.translate(option.rect.topLeft())
        self.to_doc(index).drawContents(painter)
        painter.restore()

    def createEditor(self, parent, option, index):
        w = Customize(index, index.model().duplicate_check, parent=parent)
        self.editing_indices[index.row()] = w
        self.sizeHintChanged.emit(index)
        return w

    def setEditorData(self, editor, index):
        defs = index.data(DEFAULTS)
        defs = _(' or ').join([str(x.toString(QKeySequence.SequenceFormat.NativeText)) for x in defs])
        editor.key = str(index.data(KEY))
        editor.default_shortcuts.setText(_('&Default') + ': %s' % defs)
        editor.default_shortcuts.setChecked(True)
        editor.header.setText('<b>%s: %s</b>'%(_('Customize shortcuts for'),
            str(index.data(DESCRIPTION))))
        custom = index.data(CUSTOM)
        if custom:
            editor.custom.setChecked(True)
            for x in (0, 1):
                button = getattr(editor, 'button%d'%(x+1))
                if len(custom) > x:
                    seq = QKeySequence(custom[x])
                    button.setText(seq.toString(QKeySequence.SequenceFormat.NativeText))
                    setattr(editor, 'shortcut%d'%(x+1), seq)

    def setModelData(self, editor, model, index):
        self.closeEditor.emit(editor, QAbstractItemDelegate.EndEditHint.NoHint)
        custom = []
        if editor.custom.isChecked():
            for x in ('1', '2'):
                sc = getattr(editor, 'shortcut'+x, None)
                if sc is not None:
                    custom.append(sc)

        model.set_data(index, custom)

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)


class Shortcuts(QAbstractListModel):

    TEMPLATE = '''
    <p><b>{0}</b><br>
    {2}: <code>{1}</code></p>
    '''

    def __init__(self, shortcuts, config_file_base_name, parent=None):
        QAbstractListModel.__init__(self, parent)

        self.descriptions = {}
        for k, v in shortcuts.items():
            self.descriptions[k] = v[-1]
        self.keys = {}
        for k, v in shortcuts.items():
            self.keys[k] = v[0]
        self.order = list(shortcuts)
        self.order.sort(key=lambda x : sort_key(self.descriptions[x]))
        self.sequences = {}
        for k, v in self.keys.items():
            self.sequences[k] = [QKeySequence(x) for x in v]

        self.custom = XMLConfig(config_file_base_name)

    def rowCount(self, parent):
        return len(self.order)

    def get_sequences(self, key):
        custom = self.custom.get(key, [])
        if custom:
            return [QKeySequence(x) for x in custom]
        return self.sequences[key]

    def get_match(self, event_or_sequence, ignore=tuple()):
        q = event_or_sequence
        if isinstance(q, QKeyEvent):
            q = QKeySequence(q.key()|(int(q.modifiers()) & (~Qt.KeyboardModifier.KeypadModifier)))
        for key in self.order:
            if key not in ignore:
                for seq in self.get_sequences(key):
                    if seq.matches(q) == QKeySequence.SequenceMatch.ExactMatch:
                        return key
        return None

    def duplicate_check(self, seq, ignore):
        key = self.get_match(seq, ignore=[ignore])
        if key is not None:
            return self.descriptions[key]

    def get_shortcuts(self, key):
        return [str(x.toString(QKeySequence.SequenceFormat.NativeText)) for x in
                self.get_sequences(key)]

    def data(self, index, role):
        row = index.row()
        if row < 0 or row >= len(self.order):
            return None
        key = self.order[row]
        if role == Qt.ItemDataRole.DisplayRole:
            return self.TEMPLATE.format(self.descriptions[key],
                    _(' or ').join(self.get_shortcuts(key)), _('Keys'))
        if role == Qt.ItemDataRole.ToolTipRole:
            return _('Double click to change')
        if role == DEFAULTS:
            return self.sequences[key]
        if role == DESCRIPTION:
            return self.descriptions[key]
        if role == CUSTOM:
            if key in self.custom:
                return self.custom[key]
            else:
                return []
        if role == KEY:
            return key
        return None

    def set_data(self, index, custom):
        key = self.order[index.row()]
        if custom:
            self.custom[key] = [str(x.toString(QKeySequence.SequenceFormat.PortableText)) for x in custom]
        elif key in self.custom:
            del self.custom[key]

    def flags(self, index):
        if not index.isValid():
            return Qt.ItemFlag.ItemIsEnabled
        return QAbstractListModel.flags(self, index) | Qt.ItemFlag.ItemIsEditable


class ShortcutConfig(QWidget):

    def __init__(self, model, parent=None):
        QWidget.__init__(self, parent)
        self._layout = QHBoxLayout()
        self.setLayout(self._layout)
        self.view = QListView(self)
        self._layout.addWidget(self.view)
        self.view.setModel(model)
        self.delegate = Delegate()
        self.view.setItemDelegate(self.delegate)
        self.delegate.sizeHintChanged.connect(self.scrollTo,
                type=Qt.ConnectionType.QueuedConnection)

    def scrollTo(self, index):
        self.view.scrollTo(index, QAbstractItemView.ScrollHint.EnsureVisible)

    @property
    def is_editing(self):
        return self.view.state() == QAbstractItemView.State.EditingState


if __name__ == '__main__':
    app = QApplication([])
    from calibre.gui2.viewer.keys import SHORTCUTS
    model = Shortcuts(SHORTCUTS, 'shortcuts/viewer')
    conf = ShortcutConfig(model)
    conf.resize(400, 500)
    conf.show()
    app.exec()
