UI for tag mapper

This commit is contained in:
Kovid Goyal 2015-09-24 14:24:51 +05:30
parent ac60235775
commit f8f9b2a0cd

View File

@ -0,0 +1,347 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
from __future__ import (unicode_literals, division, absolute_import,
print_function)
from collections import OrderedDict
from functools import partial
from PyQt5.Qt import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QListWidget, QIcon,
QSize, QComboBox, QLineEdit, QListWidgetItem, QStyledItemDelegate,
QStaticText, Qt, QStyle, QToolButton, QInputDialog, QMenu
)
from calibre.gui2 import error_dialog, elided_text, Application, question_dialog
from calibre.gui2.widgets2 import Dialog
from calibre.utils.config import JSONConfig
tag_maps = JSONConfig('tag-map-rules')
class RuleEdit(QWidget):
ACTION_MAP = OrderedDict((
('remove', _('Remove')),
('replace', _('Replace')),
('keep', _('Keep'))
))
MATCH_TYPE_MAP = OrderedDict((
('one_of', _('is one of')),
('not_one_of', _('is not one of')),
('matches', _('matches pattern')),
('not_matches', _('does not match pattern'))
))
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.l = l = QVBoxLayout(self)
self.h = h = QHBoxLayout()
self.la = la = QLabel(_(
'Create the rule below, the rule can be used to remove or replace tags'))
la.setWordWrap(True)
l.addWidget(la)
l.addLayout(h)
self.action = a = QComboBox(self)
h.addWidget(a)
for action, text in self.ACTION_MAP.iteritems():
a.addItem(text, action)
a.currentIndexChanged.connect(self.update_state)
self.la1 = la = QLabel('\xa0' + _('the tag, if it') + '\xa0')
h.addWidget(la)
self.match_type = q = QComboBox(self)
h.addWidget(q)
for action, text in self.MATCH_TYPE_MAP.iteritems():
q.addItem(text, action)
self.la2 = la = QLabel(':\xa0')
h.addWidget(la)
self.query = q = QLineEdit(self)
h.addWidget(q)
self.h2 = h = QHBoxLayout()
l.addLayout(h)
self.la3 = la = QLabel(_('with the tag:') + '\xa0')
h.addWidget(la)
self.replace = r = QLineEdit(self)
h.addWidget(r)
l.addStretch(10)
self.update_state()
def sizeHint(self):
a = QWidget.sizeHint(self)
a.setHeight(a.height() + 75)
a.setWidth(a.width() + 100)
return a
def update_state(self):
replace = self.action.currentData() == 'replace'
self.la3.setVisible(replace), self.replace.setVisible(replace)
tt = _('A comma separated list of tags')
if 'pattern' in self.match_type.currentData():
tt = _('A regular expression')
self.query.setToolTip(tt)
@property
def rule(self):
return {
'action': self.action.currentData(),
'match_type': self.match_type.currentData(),
'query': self.query.text().strip(),
'replace': self.replace.text().strip(),
}
@rule.setter
def rule(self, rule):
def sc(name):
c = getattr(self, name)
idx = c.findData(unicode(rule.get(name, '')))
if idx < 0:
idx = 0
c.setCurrentIndex(idx)
sc('action'), sc('match_type')
self.query.setText(unicode(rule.get('query', '')).strip())
self.replace.setText(unicode(rule.get('replace', '')).strip())
def validate(self):
rule = self.rule
if not rule['query']:
error_dialog(self, _('Query required'), _(
'You must provide a value for the tag to match'), show=True)
return False
return True
class RuleEditDialog(Dialog):
def __init__(self, parent=None):
Dialog.__init__(self, _('Edit rule'), 'edit-tag-mapper-rule', parent=None)
def setup_ui(self):
self.l = l = QVBoxLayout(self)
self.edit_widget = w = RuleEdit(self)
l.addWidget(w)
l.addWidget(self.bb)
def accept(self):
if self.edit_widget.validate():
Dialog.accept(self)
DATA_ROLE = Qt.UserRole
RENDER_ROLE = DATA_ROLE + 1
class RuleItem(QListWidgetItem):
@staticmethod
def text_from_rule(rule, parent):
query = elided_text(rule['query'], font=parent.font(), width=200, pos='right')
text = _(
'<b>{action}</b> the tag, if it <i>{match_type}</i>: <b>{query}</b>').format(
action=RuleEdit.ACTION_MAP[rule['action']], match_type=RuleEdit.MATCH_TYPE_MAP[rule['match_type']], query=query)
if rule['action'] == 'replace':
text += '<br>' + _('with the tag:') + ' <b>%s</b>' % rule['replace']
return text
def __init__(self, rule, parent):
QListWidgetItem.__init__(self, '', parent)
st = self.text_from_rule(rule, parent)
self.setData(RENDER_ROLE, st)
self.setData(DATA_ROLE, rule)
class Delegate(QStyledItemDelegate):
MARGIN = 16
def sizeHint(self, option, index):
st = QStaticText(index.data(RENDER_ROLE))
st.prepare(font=self.parent().font())
width = max(option.rect.width(), self.parent().width() - 50)
if width and width != st.textWidth():
st.setTextWidth(width)
br = st.size()
return QSize(br.width(), br.height() + self.MARGIN)
def paint(self, painter, option, index):
QStyledItemDelegate.paint(self, painter, option, index)
pal = option.palette
color = pal.color(pal.HighlightedText if option.state & QStyle.State_Selected else pal.Text).name()
text = '<div style="color:%s">%s</div>' % (color, index.data(RENDER_ROLE))
st = QStaticText(text)
st.setTextWidth(option.rect.width())
painter.drawStaticText(option.rect.left() + self.MARGIN // 2, option.rect.top() + self.MARGIN // 2, st)
class Rules(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.l = l = QVBoxLayout(self)
self.la = la = QLabel(
'<p>' + _('You can specify rules to filter/transform tags here. Click the "Add Rule" button'
' below to get started. The rules will be processed in order for every tag until either a'
' "remove" or a "keep" rule matches.') + '<p>' + _(
'You can <b>change an existing rule</b> by double clicking it')
)
la.setWordWrap(True)
l.addWidget(la)
self.h = h = QHBoxLayout()
l.addLayout(h)
self.add_button = b = QPushButton(QIcon(I('plus.png')), _('&Add rule'), self)
b.clicked.connect(self.add_rule)
h.addWidget(b)
self.remove_button = b = QPushButton(QIcon(I('minus.png')), _('&Remove rule(s)'), self)
b.clicked.connect(self.remove_rules)
h.addWidget(b)
self.h3 = h = QHBoxLayout()
l.addLayout(h)
self.rule_list = r = QListWidget(self)
self.delegate = Delegate(self)
r.setSelectionMode(r.ExtendedSelection)
r.setItemDelegate(self.delegate)
r.doubleClicked.connect(self.edit_rule)
h.addWidget(r)
r.setDragEnabled(True)
r.viewport().setAcceptDrops(True)
r.setDropIndicatorShown(True)
r.setDragDropMode(r.InternalMove)
r.setDefaultDropAction(Qt.MoveAction)
self.l2 = l = QVBoxLayout()
h.addLayout(l)
self.up_button = b = QToolButton(self)
b.setIcon(QIcon(I('arrow-up.png'))), b.setToolTip(_('Move current rule up'))
b.clicked.connect(self.move_up)
l.addWidget(b)
self.down_button = b = QToolButton(self)
b.setIcon(QIcon(I('arrow-down.png'))), b.setToolTip(_('Move current rule down'))
b.clicked.connect(self.move_down)
l.addStretch(10), l.addWidget(b)
def sizeHint(self):
return QSize(800, 600)
def add_rule(self):
d = RuleEditDialog(self)
if d.exec_() == d.Accepted:
i = RuleItem(d.edit_widget.rule, self.rule_list)
self.rule_list.scrollTo(i)
def edit_rule(self):
i = self.rule_list.currentItem()
if i is not None:
d = RuleEditDialog(self)
d.edit_widget.rule = i.data(Qt.UserRole)
if d.exec_() == d.Accepted:
rule = d.edit_widget.rule
i.setData(DATA_ROLE, rule)
i.setData(RENDER_ROLE, RuleItem.text_from_rule(rule, self.rule_list))
def remove_rules(self):
for item in self.rule_list.selectedItems():
self.rule_list.takeItem(self.rule_list.row(item))
def move_up(self):
i = self.rule_list.currentItem()
if i is not None:
row = self.rule_list.row(i)
if row > 0:
self.rule_list.takeItem(row)
self.rule_list.insertItem(row - 1, i)
self.rule_list.setCurrentItem(i)
def move_down(self):
i = self.rule_list.currentItem()
if i is not None:
row = self.rule_list.row(i)
if row < self.rule_list.count() - 1:
self.rule_list.takeItem(row)
self.rule_list.insertItem(row + 1, i)
self.rule_list.setCurrentItem(i)
@property
def rules(self):
ans = []
for r in xrange(self.rule_list.count()):
ans.append(self.rule_list.item(r).data(DATA_ROLE))
return ans
@rules.setter
def rules(self, rules):
self.rule_list.clear()
for rule in rules:
if 'action' in rule and 'match_type' in rule and 'query' in rule:
RuleItem(rule, self.rule_list)
class RulesDialog(Dialog):
def __init__(self, parent=None):
self.loaded_ruleset = None
Dialog.__init__(self, _('Edit tag mapper rules'), 'edit-tag-mapper-rules', parent=None)
def setup_ui(self):
self.l = l = QVBoxLayout(self)
self.edit_widget = w = Rules(self)
l.addWidget(w)
l.addWidget(self.bb)
self.save_button = b = self.bb.addButton(_('&Save'), self.bb.ActionRole)
b.setToolTip(_('Save this ruleset for later re-use'))
b.clicked.connect(self.save_ruleset)
self.load_button = b = self.bb.addButton(_('&Load'), self.bb.ActionRole)
b.setToolTip(_('Load a previously saved ruleset'))
self.load_menu = QMenu(self)
b.setMenu(self.load_menu)
self.build_load_menu()
@property
def rules(self):
return self.edit_widget.rules
@rules.setter
def rules(self, rules):
self.edit_widget.rules = rules
def save_ruleset(self):
text, ok = QInputDialog.getText(self, _('Save ruleset as'), _(
'Enter a name for this ruleset:'), text=self.loaded_ruleset or '')
if ok and text:
if self.loaded_ruleset and text == self.loaded_ruleset:
if not question_dialog(self, _('Are you sure?'), _(
'A ruleset with the name "%s" already exists, do you want to replace it?') % text):
return
self.loaded_ruleset = text
rules = self.rules
if rules:
tag_maps[text] = self.rules
elif text in tag_maps:
del tag_maps[text]
self.build_load_menu()
def build_load_menu(self):
self.load_menu.clear()
if len(tag_maps):
for name, rules in tag_maps.iteritems():
self.load_menu.addAction(name).triggered.connect(partial(self.load_ruleset, name))
self.load_menu.addSeparator()
m = self.load_menu.addMenu(_('Delete saved rulesets'))
for name, rules in tag_maps.iteritems():
m.addAction(name).triggered.connect(partial(self.delete_ruleset, name))
else:
self.load_menu.addAction(_('No saved rulesets available'))
def load_ruleset(self, name):
self.rules = tag_maps[name]
self.loaded_ruleset = name
def delete_ruleset(self, name):
del tag_maps[name]
self.build_load_menu()
if __name__ == '__main__':
app = Application([])
d = RulesDialog()
d.rules = [
{'action':'remove', 'query':'moose', 'match_type':'one_of', 'replace':''},
{'action':'replace', 'query':'moose', 'match_type':'one_of', 'replace':'xxxx'},
]
d.exec_()
del d, app