PageRenderTime 90ms CodeModel.GetById 39ms app.highlight 43ms RepoModel.GetById 1ms app.codeStats 0ms

/tortoisehg/hgtk/thgpbranch.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 871 lines | 825 code | 26 blank | 20 comment | 20 complexity | f6223616c8d7fab4d883b30b4d21d742 MD5 | raw file
  1# thgpbranch.py - embeddable widget for the PatchBranch extension
  2#
  3# Copyright 2009 Peer Sommerlund <peer.sommerlund@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 os
  9import tempfile
 10import gtk
 11import gobject
 12
 13from mercurial import cmdutil, extensions, util
 14from mercurial import commands as hg
 15import mercurial.ui
 16
 17from tortoisehg.util.i18n import _
 18
 19from tortoisehg.hgtk import hgcmd
 20from tortoisehg.hgtk import update
 21from tortoisehg.hgtk import gtklib, dialog
 22from tortoisehg.hgtk.logview import graphcell
 23
 24# Patch Branch model enumeration
 25M_NODE      = 0
 26M_IN_LINES  = 1
 27M_OUT_LINES = 2
 28M_NAME      = 3
 29M_STATUS    = 4
 30M_TITLE     = 5
 31M_MSG       = 6
 32M_MSGESC    = 7
 33
 34# Patch Branch column enumeration
 35C_GRAPH   = 0
 36C_STATUS  = 1
 37C_NAME    = 2
 38C_TITLE   = 3
 39C_MSG     = 4
 40
 41class PBranchWidget(gtk.VBox):
 42
 43    __gproperties__ = {
 44        'graph-column-visible': (gobject.TYPE_BOOLEAN,
 45                                    'Graph',
 46                                    'Show graph column',
 47                                    False,
 48                                    gobject.PARAM_READWRITE),
 49        'status-column-visible': (gobject.TYPE_BOOLEAN,
 50                                    'Status',
 51                                    'Show status column',
 52                                    False,
 53                                    gobject.PARAM_READWRITE),
 54        'name-column-visible': (gobject.TYPE_BOOLEAN,
 55                                    'Name',
 56                                    'Show name column',
 57                                    False,
 58                                    gobject.PARAM_READWRITE),
 59        'title-column-visible': (gobject.TYPE_BOOLEAN,
 60                                    'Title',
 61                                    'Show title column',
 62                                    False,
 63                                    gobject.PARAM_READWRITE),
 64        'message-column-visible': (gobject.TYPE_BOOLEAN,
 65                                    'Title',
 66                                    'Show title column',
 67                                    False,
 68                                    gobject.PARAM_READWRITE),
 69        'show-internal-branches': (gobject.TYPE_BOOLEAN,
 70                            'ShowInternalBranches',
 71                            "Show internal branches",
 72                            False,
 73                            gobject.PARAM_READWRITE)
 74    }
 75
 76    __gsignals__ = {
 77        'repo-invalidated': (gobject.SIGNAL_RUN_FIRST,
 78                             gobject.TYPE_NONE,
 79                             ()),
 80        'patch-selected': (gobject.SIGNAL_RUN_FIRST,
 81                           gobject.TYPE_NONE,
 82                           (int,  # revision number for patch head
 83                            str)) # patch name
 84    }
 85
 86    def __init__(self, parentwin, repo, statusbar, accelgroup=None, tooltips=None):
 87        gtk.VBox.__init__(self)
 88
 89        self.parent_window = parentwin
 90        self.repo = repo
 91        self.pbranch = extensions.find('pbranch')
 92        self.statusbar = statusbar
 93
 94        # top toolbar
 95        tbar = gtklib.SlimToolbar(tooltips)
 96
 97        ## buttons
 98        self.btn = {}
 99        pmergebtn = tbar.append_button(gtk.STOCK_CONVERT,
100                                      _('Merge all pending dependencies'))
101        pmergebtn.connect('clicked', self.pmerge_clicked)
102        self.btn['pmerge'] = pmergebtn
103
104        pbackoutbtn = tbar.append_button(gtk.STOCK_GO_BACK,
105                                   _('Backout current patch branch'))
106        pbackoutbtn.connect('clicked', self.pbackout_clicked)
107        self.btn['pbackout'] = pbackoutbtn
108
109        reapplybtn = gtk.ToolButton(gtk.STOCK_GO_FORWARD)
110        reapplybtn = tbar.append_button(gtk.STOCK_GO_FORWARD,
111                                    _('Backport part of a changeset to a dependency'))
112        reapplybtn.connect('clicked', self.reapply_clicked)
113        self.btn['reapply'] = reapplybtn
114
115        pnewbtn = tbar.append_button(gtk.STOCK_NEW,
116                                       _('Start a new patch branch'))
117        pnewbtn.connect('clicked', self.pnew_clicked)
118        self.btn['pnew'] = pnewbtn
119
120        pgraphbtn = tbar.append_button(gtk.STOCK_EDIT,
121                                       _('Edit patch dependency graph'))
122        pgraphbtn.connect('clicked', self.edit_pgraph_clicked)
123        self.btn['pnew'] = pnewbtn
124
125        ## separator
126        tbar.append_space()
127
128        ## drop-down menu
129        menubtn = gtk.MenuToolButton('')
130        menubtn.set_menu(self.create_view_menu())
131        tbar.append_widget(menubtn, padding=0)
132        self.btn['menu'] = menubtn
133        def after_init():
134            menubtn.child.get_children()[0].hide()
135        gtklib.idle_add_single_call(after_init)
136        self.pack_start(tbar, False, False)
137
138        # center pane
139        mainbox = gtk.VBox()
140        self.pack_start(mainbox, True, True)
141
142        ## scrolled pane
143        pane = gtk.ScrolledWindow()
144        pane.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
145        pane.set_shadow_type(gtk.SHADOW_IN)
146        mainbox.pack_start(pane)
147
148        ### patch list
149        #### patch list model
150        self.model = gtk.ListStore(
151                gobject.TYPE_PYOBJECT, # node info
152                gobject.TYPE_PYOBJECT, # in-lines
153                gobject.TYPE_PYOBJECT, # out-lines
154                str, # patch name
155                str, # patch status
156                str, # patch title
157                str, # patch message
158                str) # patch message escaped
159        #### patch list view
160        self.list = gtk.TreeView(self.model)
161        # To support old PyGTK (<2.12)
162        if hasattr(self.list, 'set_tooltip_column'):
163            self.list.set_tooltip_column(M_MSGESC)
164        self.list.connect('cursor-changed', self.list_sel_changed)
165        self.list.connect('button-press-event', self.list_pressed)
166        self.list.connect('row-activated', self.list_row_activated)
167        self.list.connect('size-allocate', self.list_size_allocated)
168
169        #### patch list columns
170        self.cols = {}
171        self.cells = {}
172
173        def addcol(header, col_idx, model_idx=None, right=False, resizable=False,
174                   editable=False, editfunc=None, cell_renderer=None,
175                   properties=[]):
176            header = (right and '%s ' or ' %s') % header
177            cell = cell_renderer or gtk.CellRendererText()
178            if editfunc:
179                cell.set_property('editable', editable)
180                cell.connect('edited', editfunc)
181            col = gtk.TreeViewColumn(header, cell)
182            if cell_renderer is None:
183                col.add_attribute(cell, 'text', model_idx)
184            col.set_resizable(resizable)
185            col.set_visible(self.get_property(self.col_to_prop(col_idx)))
186            if right:
187                col.set_alignment(1)
188                cell.set_property('xalign', 1)
189            for (property_name, model_index) in properties:
190                col.add_attribute(cell, property_name, model_index)
191            self.list.append_column(col)
192            self.cols[col_idx] = col
193            self.cells[col_idx] = cell
194
195        def cell_edited(cell, path, newname):
196            row = self.model[path]
197            patchname = row[M_NAME]
198            if newname != patchname:
199                self.qrename(newname, patch=patchname)
200
201        #### patch list columns and cell renderers
202
203        addcol(_('Graph'), C_GRAPH, resizable=True, 
204            cell_renderer=graphcell.CellRendererGraph(),
205            properties=[("node", M_NODE), 
206                        ("in-lines",M_IN_LINES), 
207                        ("out-lines", M_OUT_LINES)]
208            )
209        addcol(_('St'), C_STATUS, M_STATUS)
210        addcol(_('Name'), C_NAME, M_NAME, editfunc=cell_edited)
211        addcol(_('Title'), C_TITLE, M_TITLE)
212        addcol(_('Message'), C_MSG, M_MSG)
213
214        pane.add(self.list)
215
216        ## command widget
217        self.cmd = hgcmd.CmdWidget(style=hgcmd.STYLE_COMPACT,
218                                   tooltips=tooltips)
219        mainbox.pack_start(self.cmd, False, False)
220
221        # accelerator
222        if accelgroup:
223            # TODO
224            pass
225
226    ### public functions ###
227
228    def refresh(self):
229        """
230        Refresh the list of patches.
231        This operation will try to keep selection state.
232        """
233        if not self.pbranch:
234            return
235
236        # store selected patch name
237        selname = None
238        model, paths = self.list.get_selection().get_selected_rows()
239        if len(paths) > 0:
240            selname = model[paths[0]][M_NAME]
241
242        # compute model data
243        self.model.clear()
244        opts = {'tips': True}
245        mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
246        graph = mgr.graphforopts(opts)
247        if not self.get_property('show-internal-branches'):
248            graph = mgr.patchonlygraph(graph)
249        names = None
250        patch_list = graph.topolist(names)
251        in_lines = []
252        if patch_list:
253            dep_list = [patch_list[0]]
254        cur_branch = self.repo['.'].branch()
255        patch_status = {}
256        for name in patch_list:
257            patch_status[name] = self.pstatus(name)
258        for name in patch_list:
259            parents = graph.deps(name)
260
261            # Node properties
262            if name in dep_list: 
263                node_column = dep_list.index(name)
264            else:
265                node_column = len(dep_list)
266            node_colour = patch_status[name] and '#ff0000' or 0
267            node_status = (name == cur_branch) and 4 or 0
268            node = (node_column, node_colour, node_status)
269            
270            # Find next dependency list
271            my_deps = []
272            for p in parents:
273                if p not in dep_list:
274                    my_deps.append(p)
275            next_dep_list = dep_list[:]
276            next_dep_list[node_column:node_column+1] = my_deps
277            
278            # Dependency lines
279            shift = len(parents) - 1
280            out_lines = []
281            for p in parents:
282                dep_column = next_dep_list.index(p)
283                colour = 0 # black
284                if patch_status[p]:
285                    colour = '#ff0000' # red
286                style = 0 # solid lines
287                out_lines.append((node_column, dep_column, colour, style))
288            for lines in in_lines:
289                (start_column, end_column, colour, style) = lines
290                if end_column == node_column:
291                    # Deps to current patch end here
292                    pass
293                else:
294                    # Find line continuations
295                    dep = dep_list[end_column]
296                    dep_column = next_dep_list.index(dep)
297                    out_lines.append((end_column, dep_column, colour, style))
298                    
299            stat = patch_status[name] and 'M' or 'C' # patch status
300            patchname = name
301            msg = self.pmessage(name) # summary
302            if msg:
303                msg_esc = gtklib.markup_escape_text(msg) # escaped summary (utf-8)
304                title = msg.split('\n')[0]
305            else:
306                msg_esc = None
307                title = None
308            self.model.append((node, in_lines, out_lines, patchname, stat,
309                               title, msg, msg_esc))
310            # Loop
311            in_lines = out_lines
312            dep_list = next_dep_list
313
314
315        # restore patch selection
316        if selname:
317            iter = self.get_iter_by_patchname(selname)
318            if iter:
319                self.list.get_selection().select_iter(iter)
320
321        # update UI sensitives
322        self.update_sensitives()
323
324        # report status
325        status_text = ''
326        idle_text = None
327        if self.has_patch():
328            status_text = self.pending_merges() \
329                and _('pending pmerges') \
330                or _('no pending pmerges')
331        self.statusbar.set_text(status_text, 'pbranch')
332        self.statusbar.set_idle_text(idle_text)
333
334    def pgraph(self):
335        """
336        [pbranch] Execute 'pgraph' command.
337        
338        :returns: A list of patches and dependencies
339        """
340        if self.pbranch is None:
341            return None
342        opts = {}
343        mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
344        return mgr.graphforopts(opts)
345
346    def patch_list(self, opts={}):
347        """List all patches in pbranch dependency DAG"""
348        mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
349        graph = mgr.graphforopts(opts)
350        names = None
351        return graph.topolist(names)
352        
353    def pending_merges(self):
354        """Return True if there are pending pmerge operations"""
355        for patch in self.patch_list():
356            if self.pstatus(patch):
357                return True
358        return False
359
360    def pstatus(self, patch_name):
361        """
362        [pbranch] Execute 'pstatus' command.
363        
364        :param patch_name: Name of patch-branch
365        :retv: list of status messages. If empty there is no pending merges
366        """
367        if self.pbranch is None:
368            return None
369        status = []
370        opts = {}
371        mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
372        graph = mgr.graphforopts(opts)
373        heads = self.repo.branchheads(patch_name)
374        if len(heads) > 1:
375            status.append(_('needs merge of %i heads\n') % len(heads))
376        for dep, through in graph.pendingmerges(patch_name):
377            if through:
378                status.append(_('needs merge with %s (through %s)\n') %
379                          (dep, ", ".join(through)))
380            else:
381                status.append(_('needs merge with %s\n') % dep)
382        for dep in graph.pendingrebases(patch_name):
383            status.append(_('needs update of diff base to tip of %s\n') % dep)
384        return status
385
386    def pmessage(self, patch_name):
387        """
388        Get patch message
389
390        :param patch_name: Name of patch-branch
391        :retv: Full patch message. If you extract the first line
392        you will get the patch title. If the repo does not contain
393        message or patch, the function returns None
394        """
395        opts = {}
396        mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
397        try:
398            return mgr.patchdesc(patch_name)
399        except:
400            return None
401
402    def peditmessage(self, patch_name):
403        """
404        Edit patch message
405
406        :param patch_name: Name of patch-branch
407        """
408        if not patch_name in self.patch_list():
409            return
410        cmdline = ['hg', 'peditmessage', patch_name]
411        self.cmd.execute(cmdline, self.cmd_done)
412        
413    def pdiff(self, patch_name):
414        """
415        [pbranch] Execute 'pdiff --tips' command.
416        
417        :param patch_name: Name of patch-branch
418        :retv: list of lines of generated patch
419        """
420        opts = {}
421        mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
422        graph = mgr.graphattips()
423        return graph.diff(patch_name, None, opts)
424
425    def pnew_ui(self):
426        """
427        Create new patch.
428        Propmt user for new patch name. Patch is created
429        on current branch.
430        """
431        parent =  None
432        title = _('New Patch Name')
433        new_name = dialog.entry_dialog(parent, title)
434        if not new_name:
435            return False
436        self.pnew(new_name)
437        return True
438
439    def pnew(self, patch_name):
440        """
441        [pbranch] Execute 'pnew' command.
442        
443        :param patch_name: Name of new patch-branch
444        """
445        if self.pbranch is None:
446            return False
447        self.pbranch.cmdnew(self.repo.ui, self.repo, patch_name)
448        self.emit('repo-invalidated')
449        return True
450        
451    def pmerge(self, patch_name=None):
452        """
453        [pbranch] Execute 'pmerge' command.
454
455        :param patch_name: Merge to this patch-branch
456        """
457        if not self.has_patch():
458            return
459        cmdline = ['hg', 'pmerge']
460        if patch_name:
461            cmdline += [patch_name]
462        else:
463            cmdline += ['--all']
464        self.cmd.execute(cmdline, self.cmd_done)
465        
466    def pbackout(self):
467        """
468        [pbranch] Execute 'pbackout' command.
469        """
470        assert False
471
472    def pfinish(self, patch_name):
473        """
474        [pbranch] Execute 'pfinish' command.
475        
476        The workdir must be clean.
477        The patch branch dependencies must be merged.
478        
479        :param patch_name: A patch branch (not an internal branch)
480        """
481        # Check preconditions for pfinish
482
483        assert self.is_patch(patch_name)
484
485        pmerge_status = self.pstatus(patch_name)
486        if pmerge_status != []:
487            dialog.error_dialog(self.parent_window,
488            _('Pending Pmerge'),
489            _('You cannot finish this patch branch unless you pmerge it first.\n'
490              'pmerge will solve the following issues with %(patch)s:\n'
491              '* %(issuelist)s') %
492            {'patch': patch_name,
493             'issuelist': '* '.join(pmerge_status)}
494            )
495            return
496
497        if not self.workdir_is_clean():
498            dialog.error_dialog(self.parent_window,
499            _('Uncommitted Local Changes'),
500            _('pfinish uses your working directory for temporary work.\n'
501              'Please commit your local changes before issuing pfinish.')
502            )
503            return
504
505        if hasattr(self.repo, 'mq') and len(self.repo.mq.applied) > 0:
506            dialog.error_dialog(self.parent_window,
507            _('Applied MQ patch'),
508            _('pfinish must be able to commit, but this is not allowed\n'
509              'as long as you have MQ patches applied.')
510            )
511            return
512
513        # Set up environment for mercurial commands
514        class CmdWidgetUi(mercurial.ui.ui):
515            def __init__(self, cmdLogWidget):
516                src = None
517                super(CmdWidgetUi, self).__init__(src)
518                self.cmdLogWidget = cmdLogWidget
519            def write(self, *args, **opts):
520                for a in args:
521                    self.cmdLogWidget.append(str(a))
522            def write_err(self, *args, **opts):
523                for a in args:
524                    self.cmdLogWidget.append(str(a), error=True)
525            def flush(self):
526                pass
527            def prompt(self, msg, choices=None, default="y"):
528                raise util.Abort("Internal Error: prompt not available")
529            def promptchoice(self, msg, choices, default=0):
530                raise util.Abort("Internal Error: promptchoice not available")
531            def getpass(self, prompt=None, default=None):
532                raise util.Abort("Internal Error: getpass not available")
533        repo = self.repo
534        ui = CmdWidgetUi(self.cmd.log)
535        old_ui = repo.ui
536        repo.ui = ui
537
538        # Commit patch to dependency
539        fd, patch_file_name = tempfile.mkstemp(prefix='thg-patch-')
540        patch_file = os.fdopen(fd, 'w')
541        patch_file.writelines(self.pdiff(patch_name))
542        patch_file.close()    
543        upstream_branch = self.pgraph().deps(patch_name)[0]
544        hg.update(ui, repo, rev=upstream_branch)
545        hg.import_(ui, repo, patch_file_name, base='', strip=1)
546        os.unlink(patch_file_name)
547        
548        # Close patch branch
549        hg.update(ui, repo, rev=patch_name)
550        hg.merge(ui, repo, upstream_branch)
551        msg = _('Patch branch finished')
552        hg.commit(ui, repo, close_branch=True, message=msg)
553        
554        # Update GUI
555        repo.ui = old_ui
556        self.emit('repo-invalidated')
557
558    def has_pbranch(self):
559        """ return True if pbranch extension can be used """
560        return self.pbranch is not None
561
562    def has_patch(self):
563        """ return True if pbranch extension is in use on repo """
564        if not self.has_pbranch():
565            return False
566        g = self.pgraph()
567        return len(self.pbranch.patchonlygraph(g.mgr, g)._nodes) > 0
568
569    def is_patch(self, branch_name):
570        """ return True if branch is a patch. This excludes root branches
571        and internal diff base branches (for patches with multiple 
572        dependencies. """
573        return self.has_pbranch() and self.pgraph().ispatch(branch_name)
574
575    def cur_branch(self):
576        """ Return branch that workdir belongs to. """
577        return self.repo.dirstate.branch()
578
579    def workdir_is_clean(self):
580        """ return True if the working directory is clean """
581        c = self.repo[None]
582        return not (c.modified() or c.added() or c.removed())
583
584    ### internal functions ###
585
586    def get_iter_by_patchname(self, name):
587        """ return iter has specified patch name """
588        if name:
589            for row in self.model:
590                if row[M_NAME] == name:
591                    return row.iter
592        return None
593
594    def get_path_by_patchname(self, name):
595        """ return path has specified patch name """
596        iter = self.get_iter_by_patchname(name)
597        if iter:
598            return self.model.get_path(iter)
599        return None
600
601    def update_sensitives(self):
602        """ Update the sensitives of entire UI """
603        def disable_pbranchcmd():
604            for name in ('pbackout', 'pmerge', 'pnew', 'reapply'):
605                self.btn[name].set_sensitive(False)
606        if self.pbranch:
607            self.list.set_sensitive(True)
608            self.btn['menu'].set_sensitive(True)
609            in_pbranch = True #TODO
610            is_merge = len(self.repo.parents()) > 1
611            self.btn['pmerge'].set_sensitive(in_pbranch)
612            self.btn['pbackout'].set_sensitive(in_pbranch)
613            self.btn['pnew'].set_sensitive(not is_merge)
614            self.btn['reapply'].set_sensitive(True)
615        else:
616            self.list.set_sensitive(False)
617            self.btn['menu'].set_sensitive(False)
618            disable_pbranchcmd()
619
620    def scroll_to_current(self):
621        """
622        Scroll to current patch in the patch list.
623        If the patch is selected, it will do nothing.
624        """
625        if self.list.get_selection().count_selected_rows() > 0:
626            return
627        curpatch = self.cur_branch()
628        if not curpatch:
629            return
630        path = self.get_path_by_patchname(curpatch)
631        if path:
632            self.list.scroll_to_cell(path)
633
634    def show_patch_cmenu(self, list, path):
635        """Context menu for selected patch"""
636        row = self.model[path]
637
638        menu = gtk.Menu()
639        def append(label, handler=None):
640            item = gtk.MenuItem(label, True)
641            item.set_border_width(1)
642            if handler:
643                item.connect('activate', handler, row)
644            menu.append(item)
645
646        has_pbranch = self.has_pbranch()
647        is_current = self.has_patch() and self.cur_branch() == row[M_NAME]
648        is_patch = self.is_patch(row[M_NAME])
649        is_internal = self.pbranch.isinternal(row[M_NAME])
650        is_merge = len(self.repo.branchheads(row[M_NAME])) > 1
651
652        if has_pbranch and not is_merge and not is_internal:
653            append(_('_new'), self.pnew_activated)
654        if not is_current:
655            append(_('_goto (update workdir)'), self.goto_activated)
656        if is_patch:
657            append(_('_edit message'), self.edit_message_activated)
658            append(_('_rename'), self.rename_activated)
659            append(_('_delete'), self.delete_activated)
660            append(_('_finish'), self.finish_activated)
661
662        if len(menu.get_children()) > 0:
663            menu.show_all()
664            menu.popup(None, None, None, 0, 0)
665
666    def create_view_menu(self):
667        """Top right menu for selection of columns and 
668        view configuration."""
669        menu = gtk.Menu()
670        def append(item=None, handler=None, check=False,
671                   active=False, sep=False):
672            if sep:
673                item = gtk.SeparatorMenuItem()
674            else:
675                if isinstance(item, str):
676                    if check:
677                        item = gtk.CheckMenuItem(item)
678                        item.set_active(active)
679                    else:
680                        item = gtk.MenuItem(item)
681                item.set_border_width(1)
682            if handler:
683                item.connect('activate', handler)
684            menu.append(item)
685            return item
686        def colappend(label, col_idx, active=True):
687            def handler(menuitem):
688                col = self.cols[col_idx]
689                col.set_visible(menuitem.get_active())
690            propname = self.col_to_prop(col_idx)
691            item = append(label, handler, check=True, active=active)
692            self.vmenu[propname] = item
693
694        self.vmenu = {}
695
696        colappend(_('Show graph'), C_GRAPH)
697        colappend(_('Show status'), C_STATUS, active=False)
698        colappend(_('Show name'), C_NAME)
699        colappend(_('Show title'), C_TITLE, active=False)
700        colappend(_('Show message'), C_MSG, active=False)
701
702        append(sep=True)
703
704        def enable_editable(item):
705            self.cells[C_NAME].set_property('editable', item.get_active())
706        item = append(_('Enable editable cells'), enable_editable,
707                check=True, active=False)
708        self.vmenu['editable-cell'] = item
709        item = append(_("Show internal branches"), lambda item: self.refresh(),
710                check=True, active=False)
711        self.vmenu['show-internal-branches'] = item
712
713        menu.show_all()
714        return menu
715
716    def show_dialog(self, dlg):
717        """Show modal dialog and block application
718        See also show_dialog in history.py
719        """
720        dlg.set_transient_for(self.parent_window)
721        dlg.show_all()
722        dlg.run()
723        if gtk.pygtk_version < (2, 12, 0):
724            # Workaround for old PyGTK (< 2.12.0) issue.
725            # See background of this: f668034aeda3
726            dlg.set_transient_for(None)
727        
728
729    def update_by_row(self, row):
730        branch = row[M_NAME]
731        rev = cmdutil.revrange(self.repo, [branch])
732        parents = [x.node() for x in self.repo.parents()]
733        dialog = update.UpdateDialog(rev[0])
734        self.show_dialog(dialog)
735        self.update_completed(parents)
736
737    def update_completed(self, oldparents):
738        self.repo.invalidate()
739        self.repo.dirstate.invalidate()
740        newparents = [x.node() for x in self.repo.parents()]
741        if not oldparents == newparents:
742            self.emit('repo-invalidated')
743
744    def cmd_done(self, returncode, useraborted, noemit=False):
745        if returncode == 0:
746            if self.cmd.get_pbar():
747                self.cmd.set_result(_('Succeed'), style='ok')
748        elif useraborted:
749            self.cmd.set_result(_('Canceled'), style='error')
750        else:
751            self.cmd.set_result(_('Failed'), style='error')
752        self.refresh()
753        if not noemit:
754            self.emit('repo-invalidated')
755
756    def do_get_property(self, property):
757        try:
758            return self.vmenu[property.name].get_active()
759        except:
760            raise AttributeError, 'unknown property %s' % property.name
761
762    def do_set_property(self, property, value):
763        try:
764            self.vmenu[property.name].set_active(value)
765        except:
766            raise AttributeError, 'unknown property %s' % property.name
767
768    def col_to_prop(self, col_idx):
769        if col_idx == C_GRAPH:
770            return 'graph-column-visible'
771        if col_idx == C_STATUS:
772            return 'status-column-visible'
773        if col_idx == C_NAME:
774            return 'name-column-visible'
775        if col_idx == C_TITLE:
776            return 'title-column-visible'
777        if col_idx == C_MSG:
778            return 'message-column-visible'
779        return ''
780
781    ### signal handlers ###
782
783    def list_pressed(self, list, event):
784        x, y = int(event.x), int(event.y)
785        pathinfo = list.get_path_at_pos(x, y)
786        if event.button == 1:
787            if not pathinfo:
788                # HACK: clear selection after this function calling,
789                # against selection by getting focus
790                def unselect():
791                    selection = list.get_selection()
792                    selection.unselect_all()
793                gtklib.idle_add_single_call(unselect)
794        elif event.button == 3:
795            if pathinfo:
796                self.show_patch_cmenu(self.list, pathinfo[0])
797
798    def list_sel_changed(self, list):
799        path, focus = list.get_cursor()
800        row = self.model[path]
801        patchname = row[M_NAME]
802        try:
803            ctx = self.repo[patchname]
804            revid = ctx.rev()
805        except hglib.RepoError:
806            revid = -1
807        self.emit('patch-selected', revid, patchname)
808
809    def list_row_activated(self, list, path, column):
810        self.update_by_row(self.model[path])
811
812    def list_size_allocated(self, list, req):
813        if self.has_patch():
814            self.scroll_to_current()
815
816    def pbackout_clicked(self, toolbutton):
817        pass
818
819    def pmerge_clicked(self, toolbutton):
820        self.pmerge()
821
822    def pnew_clicked(self, toolbutton):
823        self.pnew_ui()
824
825    def reapply_clicked(self, toolbutton):
826        pass
827
828    def edit_pgraph_clicked(self, toolbutton):
829        opts = {} # TODO: How to find user ID
830        mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
831        oldtext = mgr.graphdesc()
832        # run editor in the repository root
833        olddir = os.getcwd()
834        os.chdir(self.repo.root)
835        newtext = self.repo.ui.edit(oldtext, opts.get('user'))
836        os.chdir(olddir)
837        mgr.updategraphdesc(newtext)
838
839    ### context menu signal handlers ###
840
841    def pnew_activated(self, menuitem, row):
842        """Insert new patch after this row"""
843        if self.cur_branch() == row[M_NAME]:
844            self.pnew_ui()
845            return
846        # pnew from patch different than current
847        assert False
848        if self.wdir_modified():
849            # Ask user if current changes should be discarded
850            # Abort if user does not agree
851            pass
852        # remember prev branch
853        # Update to row[M_NAME]
854        # pnew_ui
855        # if aborted, update back to prev branch
856        pass
857
858    def edit_message_activated(self, menuitem, row):
859        self.peditmessage(row[M_NAME])
860
861    def goto_activated(self, menuitem, row):
862        self.update_by_row(row)
863
864    def delete_activated(self, menuitem, row):
865        assert False
866
867    def rename_activated(self, menuitem, row):
868        assert False
869
870    def finish_activated(self, menuitem, row):
871        self.pfinish(row[M_NAME])