/tortoisehg/hgqt/csinfo.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 478 lines · 396 code · 67 blank · 15 comment · 124 complexity · de98afc574d0b961c96d02cab3f0a6c8 MD5 · raw file

  1. # csinfo.py - An embeddable widget for changeset summary
  2. #
  3. # Copyright 2010 Yuki KODAMA <endflow.net@gmail.com>
  4. #
  5. # This software may be used and distributed according to the terms of the
  6. # GNU General Public License version 2, incorporated herein by reference.
  7. import re
  8. import binascii
  9. from PyQt4.QtCore import *
  10. from PyQt4.QtGui import *
  11. from mercurial import error
  12. from mercurial.node import hex
  13. from tortoisehg.util import hglib, paths
  14. from tortoisehg.hgqt.i18n import _
  15. from tortoisehg.hgqt import qtlib, thgrepo
  16. PANEL_DEFAULT = ('rev', 'summary', 'user', 'dateage', 'branch', 'tags',
  17. 'transplant', 'p4', 'svn')
  18. def create(repo, target=None, style=None, custom=None, **kargs):
  19. return Factory(repo, custom, style, target, **kargs)()
  20. def factory(*args, **kargs):
  21. return Factory(*args, **kargs)
  22. def panelstyle(**kargs):
  23. kargs['type'] = 'panel'
  24. if 'contents' not in kargs:
  25. kargs['contents'] = PANEL_DEFAULT
  26. return kargs
  27. def labelstyle(**kargs):
  28. kargs['type'] = 'label'
  29. return kargs
  30. def custom(**kargs):
  31. return kargs
  32. class Factory(object):
  33. def __init__(self, repo, custom=None, style=None, target=None,
  34. withupdate=False):
  35. if repo is None:
  36. raise _('must be specified repository')
  37. self.repo = repo
  38. self.target = target
  39. if custom is None:
  40. custom = {}
  41. self.custom = custom
  42. if style is None:
  43. style = panelstyle()
  44. self.csstyle = style
  45. self.info = SummaryInfo()
  46. self.withupdate = withupdate
  47. def __call__(self, target=None, style=None, custom=None, repo=None):
  48. # try to create a context object
  49. if target is None:
  50. target = self.target
  51. if repo is None:
  52. repo = self.repo
  53. if style is None:
  54. style = self.csstyle
  55. else:
  56. # need to override styles
  57. newstyle = self.csstyle.copy()
  58. newstyle.update(style)
  59. style = newstyle
  60. if custom is None:
  61. custom = self.custom
  62. else:
  63. # need to override customs
  64. newcustom = self.custom.copy()
  65. newcustom.update(custom)
  66. custom = newcustom
  67. if 'type' not in style:
  68. raise _("must be specified 'type' in style")
  69. type = style['type']
  70. assert type in ('panel', 'label')
  71. # create widget
  72. args = (target, style, custom, repo, self.info)
  73. if type == 'panel':
  74. widget = SummaryPanel(*args)
  75. else:
  76. widget = SummaryLabel(*args)
  77. if self.withupdate:
  78. widget.update()
  79. return widget
  80. class UnknownItem(Exception):
  81. pass
  82. class SummaryInfo(object):
  83. LABELS = {'rev': _('Revision:'), 'revnum': _('Revision:'),
  84. 'revid': _('Revision:'), 'summary': _('Summary:'),
  85. 'user': _('User:'), 'date': _('Date:'),'age': _('Age:'),
  86. 'dateage': _('Date:'), 'branch': _('Branch:'),
  87. 'tags': _('Tags:'), 'rawbranch': _('Branch:'),
  88. 'rawtags': _('Tags:'), 'transplant': _('Transplant:'),
  89. 'p4': _('Perforce:'), 'svn': _('Subversion:'),
  90. 'shortuser': _('User:')}
  91. def __init__(self):
  92. pass
  93. def get_data(self, item, widget, ctx, custom, **kargs):
  94. args = (widget, ctx, custom)
  95. def default_func(widget, item, ctx):
  96. return None
  97. def preset_func(widget, item, ctx):
  98. if item == 'rev':
  99. revnum = self.get_data('revnum', *args)
  100. revid = self.get_data('revid', *args)
  101. if revid:
  102. return (revnum, revid)
  103. return None
  104. elif item == 'revnum':
  105. return ctx.rev()
  106. elif item == 'revid':
  107. return str(ctx)
  108. elif item == 'desc':
  109. return hglib.tounicode(ctx.description().replace('\0', ''))
  110. elif item == 'summary':
  111. value = ctx.description().replace('\0', '').split('\n')[0]
  112. if len(value) == 0:
  113. return None
  114. return hglib.tounicode(value)[:80]
  115. elif item == 'user':
  116. return hglib.tounicode(ctx.user())
  117. elif item == 'shortuser':
  118. return hglib.tounicode(hglib.username(ctx.user()))
  119. elif item == 'dateage':
  120. date = self.get_data('date', *args)
  121. age = self.get_data('age', *args)
  122. if date and age:
  123. return (date, age)
  124. return None
  125. elif item == 'date':
  126. date = ctx.date()
  127. if date:
  128. return hglib.displaytime(date)
  129. return None
  130. elif item == 'age':
  131. date = ctx.date()
  132. if date:
  133. return hglib.age(date)
  134. return None
  135. elif item == 'rawbranch':
  136. return ctx.branch() or None
  137. elif item == 'branch':
  138. value = self.get_data('rawbranch', *args)
  139. if value:
  140. repo = ctx._repo
  141. if ctx.node() not in repo.branchtags().values():
  142. return None
  143. if value in repo.deadbranches:
  144. return None
  145. return value
  146. return None
  147. elif item == 'rawtags':
  148. return hglib.getrawctxtags(ctx)
  149. elif item == 'tags':
  150. return hglib.getctxtags(ctx)
  151. elif item == 'transplant':
  152. extra = ctx.extra()
  153. try:
  154. ts = extra['transplant_source']
  155. if ts:
  156. return binascii.hexlify(ts)
  157. except KeyError:
  158. pass
  159. return None
  160. elif item == 'p4':
  161. extra = ctx.extra()
  162. p4cl = extra.get('p4', None)
  163. return p4cl and ('changelist %s' % p4cl)
  164. elif item == 'svn':
  165. extra = ctx.extra()
  166. cvt = extra.get('convert_revision', '')
  167. if cvt.startswith('svn:'):
  168. result = cvt.split('/', 1)[-1]
  169. if cvt != result:
  170. return result
  171. return cvt.split('@')[-1]
  172. else:
  173. return None
  174. elif item == 'ishead':
  175. childbranches = [cctx.branch() for cctx in ctx.children()]
  176. return ctx.branch() not in childbranches
  177. raise UnknownItem(item)
  178. if 'data' in custom and not kargs.get('usepreset', False):
  179. try:
  180. return custom['data'](widget, item, ctx)
  181. except UnknownItem:
  182. pass
  183. try:
  184. return preset_func(widget, item, ctx)
  185. except UnknownItem:
  186. pass
  187. return default_func(widget, item, ctx)
  188. def get_label(self, item, widget, ctx, custom, **kargs):
  189. def default_func(widget, item):
  190. return ''
  191. def preset_func(widget, item):
  192. try:
  193. return self.LABELS[item]
  194. except KeyError:
  195. raise UnknownItem(item)
  196. if 'label' in custom and not kargs.get('usepreset', False):
  197. try:
  198. return custom['label'](widget, item)
  199. except UnknownItem:
  200. pass
  201. try:
  202. return preset_func(widget, item)
  203. except UnknownItem:
  204. pass
  205. return default_func(widget, item)
  206. def get_markup(self, item, widget, ctx, custom, **kargs):
  207. args = (widget, ctx, custom)
  208. mono = dict(family='monospace', size='9pt', space='pre')
  209. def default_func(widget, item, value):
  210. return ''
  211. def preset_func(widget, item, value):
  212. if item == 'rev':
  213. revnum, revid = value
  214. revid = qtlib.markup(revid, **mono)
  215. if revnum is not None and revid is not None:
  216. return '%s (%s)' % (revnum, revid)
  217. return '%s' % revid
  218. elif item in ('revid', 'transplant'):
  219. return qtlib.markup(value, **mono)
  220. elif item in ('revnum', 'p4', 'svn'):
  221. return str(value)
  222. elif item in ('rawbranch', 'branch'):
  223. opts = dict(fg='black', bg='#aaffaa')
  224. return qtlib.markup(' %s ' % value, **opts)
  225. elif item in ('rawtags', 'tags'):
  226. opts = dict(fg='black', bg='#ffffaa')
  227. tags = [qtlib.markup(' %s ' % tag, **opts) for tag in value]
  228. return ' '.join(tags)
  229. elif item in ('desc', 'summary', 'user', 'shortuser',
  230. 'date', 'age'):
  231. return qtlib.markup(value)
  232. elif item == 'dateage':
  233. return qtlib.markup('%s (%s)' % value)
  234. raise UnknownItem(item)
  235. value = self.get_data(item, *args)
  236. if value is None:
  237. return None
  238. if 'markup' in custom and not kargs.get('usepreset', False):
  239. try:
  240. return custom['markup'](widget, item, value)
  241. except UnknownItem:
  242. pass
  243. try:
  244. return preset_func(widget, item, value)
  245. except UnknownItem:
  246. pass
  247. return default_func(widget, item, value)
  248. def get_widget(self, item, widget, ctx, custom, **kargs):
  249. args = (widget, ctx, custom)
  250. def default_func(widget, item, markups):
  251. if isinstance(markups, basestring):
  252. markups = (markups,)
  253. labels = []
  254. for text in markups:
  255. label = QLabel()
  256. label.setText(text)
  257. labels.append(label)
  258. return labels
  259. markups = self.get_markup(item, *args)
  260. if not markups:
  261. return None
  262. if 'widget' in custom and not kargs.get('usepreset', False):
  263. try:
  264. return custom['widget'](widget, item, markups)
  265. except UnknownItem:
  266. pass
  267. return default_func(widget, item, markups)
  268. class SummaryBase(object):
  269. def __init__(self, target, custom, repo, info):
  270. if target is None:
  271. self.target = None
  272. else:
  273. self.target = str(target)
  274. self.custom = custom
  275. self.repo = repo
  276. self.info = info
  277. self.ctx = repo.changectx(self.target)
  278. def get_data(self, item, **kargs):
  279. return self.info.get_data(item, self, self.ctx, self.custom, **kargs)
  280. def get_label(self, item, **kargs):
  281. return self.info.get_label(item, self, self.ctx, self.custom, **kargs)
  282. def get_markup(self, item, **kargs):
  283. return self.info.get_markup(item, self, self.ctx, self.custom, **kargs)
  284. def get_widget(self, item, **kargs):
  285. return self.info.get_widget(item, self, self.ctx, self.custom, **kargs)
  286. def set_revision(self, rev):
  287. self.target = rev
  288. def update(self, target=None, custom=None, repo=None):
  289. self.ctx = None
  290. if target is None:
  291. target = self.target
  292. if target is not None:
  293. target = str(target)
  294. self.target = target
  295. if custom is not None:
  296. self.custom = custom
  297. if repo is None:
  298. repo = self.repo
  299. if repo is not None:
  300. self.repo = repo
  301. if self.ctx is None:
  302. self.ctx = repo.changectx(target)
  303. PANEL_TMPL = '<tr><td style="padding-right:6px">%s</td><td>%s</td></tr>'
  304. class SummaryPanel(SummaryBase, QWidget):
  305. linkActivated = pyqtSignal(QString)
  306. def __init__(self, target, style, custom, repo, info):
  307. SummaryBase.__init__(self, target, custom, repo, info)
  308. QWidget.__init__(self)
  309. self.csstyle = style
  310. hbox = QHBoxLayout()
  311. hbox.setMargin(0)
  312. hbox.setSpacing(0)
  313. self.setLayout(hbox)
  314. self.revlabel = None
  315. self.expand_btn = qtlib.PMButton()
  316. def update(self, target=None, style=None, custom=None, repo=None):
  317. SummaryBase.update(self, target, custom, repo)
  318. if style is not None:
  319. self.csstyle = style
  320. if self.revlabel is None:
  321. self.revlabel = QLabel()
  322. self.revlabel.linkActivated.connect(
  323. lambda s: self.linkActivated.emit(s))
  324. self.layout().addWidget(self.revlabel, alignment=Qt.AlignTop)
  325. if 'expandable' in self.csstyle and self.csstyle['expandable']:
  326. if self.expand_btn.parentWidget() is None:
  327. self.expand_btn.clicked.connect(lambda: self.update())
  328. margin = QHBoxLayout()
  329. margin.setMargin(3)
  330. margin.addWidget(self.expand_btn, alignment=Qt.AlignTop)
  331. self.layout().insertLayout(0, margin)
  332. self.expand_btn.setShown(True)
  333. elif self.expand_btn.parentWidget() is not None:
  334. self.expand_btn.setHidden(True)
  335. interact = Qt.LinksAccessibleByMouse
  336. if 'selectable' in self.csstyle and self.csstyle['selectable']:
  337. interact |= Qt.TextBrowserInteraction
  338. self.revlabel.setTextInteractionFlags(interact)
  339. # build info
  340. contents = self.csstyle.get('contents', ())
  341. if 'expandable' in self.csstyle and self.csstyle['expandable'] \
  342. and self.expand_btn.is_collapsed():
  343. contents = contents[0:1]
  344. if 'margin' in self.csstyle:
  345. margin = self.csstyle['margin']
  346. assert isinstance(margin, (int, long))
  347. buf = '<table style="margin: %spx">' % margin
  348. else:
  349. buf = '<table>'
  350. for item in contents:
  351. markups = self.get_markup(item)
  352. if not markups:
  353. continue
  354. label = qtlib.markup(self.get_label(item), weight='bold')
  355. if isinstance(markups, basestring):
  356. markups = [markups,]
  357. buf += PANEL_TMPL % (label, markups.pop(0))
  358. for markup in markups:
  359. buf += PANEL_TMPL % ('&nbsp;', markup)
  360. buf += '</table>'
  361. self.revlabel.setText(buf)
  362. return True
  363. def set_expanded(self, state):
  364. self.expand_btn.set_expanded(state)
  365. self.update()
  366. def is_expanded(self):
  367. return self.expand_btn.is_expanded()
  368. def minimumSizeHint(self):
  369. s = QWidget.minimumSizeHint(self)
  370. return QSize(0, s.height())
  371. LABEL_PAT = re.compile(r'(?:(?<=%%)|(?<!%)%\()(\w+)(?:\)s)')
  372. class SummaryLabel(SummaryBase, QLabel):
  373. def __init__(self, target, style, custom, repo, info):
  374. SummaryBase.__init__(self, target, custom, repo, info)
  375. QLabel.__init__(self)
  376. self.csstyle = style
  377. def update(self, target=None, style=None, custom=None, repo=None):
  378. SummaryBase.update(self, target, custom, repo)
  379. if style is not None:
  380. self.csstyle = style
  381. if 'selectable' in self.csstyle:
  382. sel = self.csstyle['selectable']
  383. val = sel and Qt.TextSelectableByMouse or Qt.TextBrowserInteraction
  384. self.setTextInteractionFlags(val)
  385. if 'width' in self.csstyle:
  386. width = self.csstyle.get('width', 0)
  387. self.setMinimumWidth(width)
  388. if 'height' in self.csstyle:
  389. height = self.csstyle.get('height', 0)
  390. self.setMinimumHeight(height)
  391. contents = self.csstyle.get('contents', None)
  392. # build info
  393. info = ''
  394. for snip in contents:
  395. # extract all placeholders
  396. items = LABEL_PAT.findall(snip)
  397. # fetch required data
  398. data = {}
  399. for item in items:
  400. markups = self.get_markup(item)
  401. if not markups:
  402. continue
  403. if isinstance(markups, basestring):
  404. markups = (markups,)
  405. data[item] = ', '.join(markups)
  406. if len(data) == 0:
  407. continue
  408. # insert data & append to label
  409. info += snip % data
  410. self.setText(info)
  411. return True