PageRenderTime 98ms CodeModel.GetById 41ms app.highlight 51ms RepoModel.GetById 1ms app.codeStats 0ms

/tortoisehg/hgqt/csinfo.py

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