PageRenderTime 134ms CodeModel.GetById 20ms app.highlight 103ms RepoModel.GetById 1ms app.codeStats 0ms

/tortoisehg/hgtk/status.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 1336 lines | 1290 code | 19 blank | 27 comment | 30 complexity | e1756b0e35d3121a518aef24b2ac322f MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1# status.py - status dialog for TortoiseHg
   2#
   3# Copyright 2007 Brad Schick, brad at gmail . com
   4# Copyright 2007 TK Soh <teekaysoh@gmail.com>
   5# Copyright 2008 Steve Borho <steve@borho.org>
   6# Copyright 2008 Emmanuel Rosa <goaway1000@gmail.com>
   7#
   8# This software may be used and distributed according to the terms of the
   9# GNU General Public License version 2, incorporated herein by reference.
  10
  11import os
  12import cStringIO
  13import gtk
  14import gobject
  15import threading
  16
  17from mercurial import cmdutil, util, patch, error, hg
  18from mercurial import merge as merge_, filemerge
  19
  20from tortoisehg.util.i18n import _
  21from tortoisehg.util import hglib, paths, hgshelve
  22
  23from tortoisehg.hgtk import dialog, gdialog, gtklib, guess, hgignore, statusbar, statusact
  24from tortoisehg.hgtk import chunks
  25
  26# file model row enumerations
  27FM_CHECKED = 0
  28FM_STATUS = 1
  29FM_PATH_UTF8 = 2
  30FM_PATH = 3
  31FM_MERGE_STATUS = 4
  32FM_PARTIAL_SELECTED = 5
  33
  34
  35class GStatus(gdialog.GWindow):
  36    """GTK+ based dialog for displaying repository status
  37
  38    Also provides related operations like add, delete, remove, revert, refresh,
  39    ignore, diff, and edit.
  40
  41    The following methods are meant to be overridden by subclasses. At this
  42    point GCommit is really the only intended subclass.
  43
  44        auto_check(self)
  45    """
  46
  47    ### Following methods are meant to be overridden by subclasses ###
  48
  49    def init(self):
  50        gdialog.GWindow.init(self)
  51        self.mode = 'status'
  52        self.ready = False
  53        self.status = ([],) * 7
  54        self.status_error = None
  55        self.preview_tab_name_label = None
  56        self.subrepos = []
  57        self.colorstyle = self.repo.ui.config('tortoisehg', 'diffcolorstyle')
  58        self.act = statusact.statusact(self)
  59
  60    def auto_check(self):
  61        # Only auto-check files once, and only if a pattern was given.
  62        if self.pats and self.opts.get('check'):
  63            for entry in self.filemodel:
  64                if entry[FM_PATH] not in self.excludes:
  65                    entry[FM_CHECKED] = True
  66            self.update_check_count()
  67            self.opts['check'] = False
  68
  69    def get_custom_menus(self):
  70        return []
  71
  72    ### End of overridable methods ###
  73
  74
  75    ### Overrides of base class methods ###
  76
  77    def parse_opts(self):
  78        # Disable refresh while we toggle checkboxes
  79        self.ready = False
  80
  81        # Determine which files to display
  82        if self.test_opt('all'):
  83            for check in self._show_checks.values():
  84                check.set_active(True)
  85        else:
  86            for opt in self.opts:
  87                if opt in self._show_checks and self.opts[opt]:
  88                    self._show_checks[opt].set_active(True)
  89        self.ready = True
  90
  91
  92    def get_title(self):
  93        root = self.get_reponame()
  94        revs = self.opts.get('rev')
  95        name = self.pats and _('filtered status') or _('status')
  96        r = revs and ':'.join(revs) or ''
  97        return root + ' - ' + ' '.join([name, r])
  98
  99    def get_icon(self):
 100        return 'menushowchanged.ico'
 101
 102    def get_defsize(self):
 103        return self._setting_defsize
 104
 105
 106    def get_tbbuttons(self):
 107        tbuttons = []
 108
 109        if self.count_revs() == 2:
 110            tbuttons += [
 111                    self.make_toolbutton(gtk.STOCK_SAVE_AS, _('Save As'),
 112                        self.save_clicked, tip=_('Save selected changes'))]
 113        else:
 114            tbuttons += [
 115                    self.make_toolbutton(gtk.STOCK_JUSTIFY_FILL, _('_Diff'),
 116                        self.diff_clicked, name='diff',
 117                        tip=_('Visual diff checked files')),
 118                    self.make_toolbutton(gtk.STOCK_MEDIA_REWIND, _('Re_vert'),
 119                        self.revert_clicked, name='revert',
 120                        tip=_('Revert checked files')),
 121                    self.make_toolbutton(gtk.STOCK_ADD, _('_Add'),
 122                        self.add_clicked, name='add',
 123                        tip=_('Add checked files')),
 124                    self.make_toolbutton(gtk.STOCK_JUMP_TO, _('Move'),
 125                        self.move_clicked, name='move',
 126                        tip=_('Move checked files to other directory')),
 127                    self.make_toolbutton(gtk.STOCK_DELETE, _('_Remove'),
 128                        self.remove_clicked, name='remove',
 129                        tip=_('Remove or delete checked files')),
 130                    self.make_toolbutton(gtk.STOCK_CLEAR, _('_Forget'),
 131                        self.forget_clicked, name='forget',
 132                        tip=_('Forget checked files on next commit')),
 133                    gtk.SeparatorToolItem(),
 134                    self.make_toolbutton(gtk.STOCK_REFRESH, _('Re_fresh'),
 135                        self.refresh_clicked,
 136                        tip=_('refresh')),
 137                    gtk.SeparatorToolItem()]
 138        return tbuttons
 139
 140
 141    def save_settings(self):
 142        settings = gdialog.GWindow.save_settings(self)
 143        settings['gstatus-hpane'] = self.diffpane.get_position()
 144        settings['gstatus-lastpos'] = self.setting_lastpos
 145        settings['gstatus-type-expander'] = self.types_expander.get_expanded()
 146        return settings
 147
 148
 149    def load_settings(self, settings):
 150        gdialog.GWindow.load_settings(self, settings)
 151        self.setting_pos = 270
 152        self.setting_lastpos = 64000
 153        self.setting_types_expanded = False
 154        try:
 155            self.setting_pos = settings['gstatus-hpane']
 156            self.setting_lastpos = settings['gstatus-lastpos']
 157            self.setting_types_expanded = settings['gstatus-type-expander']
 158        except KeyError:
 159            pass
 160        self.mqmode, repo = None, self.repo
 161        if hasattr(repo, 'mq') and repo.mq.applied and repo['.'] == repo['qtip']:
 162            self.mqmode = True
 163
 164    def is_merge(self):
 165        try:
 166            numparents = len(self.repo.parents())
 167        except error.Abort, e:
 168            self.stbar.set_text(str(e) + _(', please refresh'))
 169            numparents = 1
 170        return self.count_revs() < 2 and numparents == 2
 171
 172
 173    def get_accelgroup(self):
 174        accelgroup = gtk.AccelGroup()
 175        mod = gtklib.get_thg_modifier()
 176        
 177        gtklib.add_accelerator(self.filetree, 'thg-diff', accelgroup, mod+'d')
 178        self.filetree.connect('thg-diff', self.thgdiff)
 179        self.connect('thg-refresh', self.thgrefresh)
 180
 181        # set CTRL-c accelerator for copy-clipboard
 182        gtklib.add_accelerator(self.chunks.difftree(), 'copy-clipboard', accelgroup, mod+'c')
 183
 184        def scroll_diff_notebook(widget, direction=gtk.SCROLL_PAGE_DOWN):
 185            page_num = self.diff_notebook.get_current_page()
 186            page = self.diff_notebook.get_nth_page(page_num)
 187
 188            page.emit("scroll-child", direction, False)
 189
 190        def toggle_filetree_selection(*arguments):
 191            self.sel_clicked(not self.selcb.get_active())
 192
 193        def next_diff_notebook_page(*arguments):
 194            notebook = self.diff_notebook
 195            if notebook.get_current_page() >= len(notebook) - 1:
 196                notebook.set_current_page(0)
 197            else:
 198                notebook.next_page()
 199                
 200        def previous_diff_notebook_page(*arguments):
 201            notebook = self.diff_notebook
 202            if notebook.get_current_page() <= 0:
 203                notebook.set_current_page(len(notebook) - 1)
 204            else:
 205                notebook.prev_page()
 206                
 207        # signal, accelerator key, handler, (parameters)
 208        status_accelerators = [
 209            ('status-scroll-down', 'bracketright', scroll_diff_notebook,
 210             (gtk.SCROLL_PAGE_DOWN,)),
 211            ('status-scroll-up', 'bracketleft', scroll_diff_notebook,
 212             (gtk.SCROLL_PAGE_UP,)),
 213            ('status-next-file', 'period', gtklib.move_treeview_selection,
 214             (self.filetree, 1)),
 215            ('status-previous-file', 'comma', gtklib.move_treeview_selection,
 216             (self.filetree, -1)),
 217            ('status-select-all', 'u', toggle_filetree_selection, ()),
 218            ('status-next-page', 'p', next_diff_notebook_page, ()),
 219            ('status-previous-page', '<Shift>p',
 220             previous_diff_notebook_page, ()),
 221        ]
 222        
 223        for signal, accelerator, handler, parameters in status_accelerators:
 224            gtklib.add_accelerator(self, signal, accelgroup,
 225                                   mod + accelerator)
 226            self.connect(signal, handler, *parameters)
 227
 228        return accelgroup
 229                
 230    def get_body(self):
 231        is_merge = self.is_merge()
 232
 233        # model stores the file list.
 234        fm = gtk.ListStore(
 235              bool, # FM_CHECKED
 236              str,  # FM_STATUS
 237              str,  # FM_PATH_UTF8
 238              str,  # FM_PATH
 239              str,  # FM_MERGE_STATUS
 240              bool  # FM_PARTIAL_SELECTED
 241            )
 242        fm.set_sort_func(1001, self.sort_by_stat)
 243        fm.set_default_sort_func(self.sort_by_stat)
 244        self.filemodel = fm
 245
 246        self.filetree = gtk.TreeView(self.filemodel)
 247        self.filetree.connect('popup-menu', self.tree_popup_menu)
 248        self.filetree.connect('button-press-event', self.tree_button_press)
 249        self.filetree.connect('button-release-event', self.tree_button_release)
 250        self.filetree.connect('row-activated', self.tree_row_act)
 251        self.filetree.connect('key-press-event', self.tree_key_press)
 252        self.filetree.set_reorderable(False)
 253        self.filetree.set_enable_search(True)
 254        self.filetree.set_search_equal_func(self.search_filelist)
 255        if hasattr(self.filetree, 'set_rubber_banding'):
 256            self.filetree.set_rubber_banding(True)
 257        self.filetree.modify_font(self.fonts['list'])
 258        self.filetree.set_headers_clickable(True)
 259
 260        toggle_cell = gtk.CellRendererToggle()
 261        toggle_cell.connect('toggled', self.select_toggle)
 262        toggle_cell.set_property('activatable', True)
 263
 264        path_cell = gtk.CellRendererText()
 265        stat_cell = gtk.CellRendererText()
 266
 267        # file selection checkboxes
 268        col0 = gtk.TreeViewColumn('', toggle_cell)
 269        col0.set_visible(not is_merge) # hide when merging
 270        col0.add_attribute(toggle_cell, 'active', FM_CHECKED)
 271        col0.add_attribute(toggle_cell, 'radio', FM_PARTIAL_SELECTED)
 272        col0.set_resizable(False)
 273        self.filetree.append_column(col0)
 274        self.selcb = self.add_header_checkbox(col0, self.sel_clicked)
 275        self.file_sel_column = col0
 276
 277        col1 = gtk.TreeViewColumn(_('st'), stat_cell)
 278        col1.add_attribute(stat_cell, 'text', FM_STATUS)
 279        col1.set_cell_data_func(stat_cell, self.text_color)
 280        col1.set_sort_column_id(1001)
 281        col1.set_resizable(False)
 282        self.filetree.append_column(col1)
 283
 284        # merge status column
 285        col = gtk.TreeViewColumn(_('ms'), stat_cell)
 286        col.set_visible(self.count_revs() <= 1)
 287        col.add_attribute(stat_cell, 'text', FM_MERGE_STATUS)
 288        col.set_sort_column_id(4)
 289        col.set_resizable(False)
 290        self.filetree.append_column(col)
 291        self.merge_state_column = col
 292
 293        col2 = gtk.TreeViewColumn(_('path'), path_cell)
 294        col2.add_attribute(path_cell, 'text', FM_PATH_UTF8)
 295        col2.set_cell_data_func(path_cell, self.text_color)
 296        col2.set_sort_column_id(2)
 297        col2.set_resizable(True)
 298        self.filetree.append_column(col2)
 299
 300        scroller = gtk.ScrolledWindow()
 301        scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 302        scroller.add(self.filetree)
 303
 304        # Status Types expander
 305        # We don't assign an expander child. We instead monitor the
 306        # expanded property and do the hiding ourselves
 307        expander = gtk.Expander(_('View'))
 308        self.types_expander = expander
 309        expander.connect("notify::expanded", self.types_expander_expanded)
 310        exp_labelbox = gtk.HBox()
 311        exp_labelbox.pack_start(expander, False, False)
 312        exp_labelbox.pack_start(gtk.Label(), True, True)
 313        self.counter = gtk.Label('')
 314        exp_labelbox.pack_end(self.counter, False, False, 2)
 315        self.status_types = self.get_status_types()
 316        if self.setting_types_expanded:
 317            expander.set_expanded(True)
 318            self.status_types.show()
 319        else:
 320            self.status_types.hide()
 321        expander_box = gtk.VBox()
 322        expander_box.pack_start(exp_labelbox)
 323        expander_box.pack_start(self.status_types)
 324
 325        tvbox = gtk.VBox()
 326        tvbox.pack_start(scroller, True, True, 0)
 327        tvbox.pack_start(gtk.HSeparator(), False, False)
 328        tvbox.pack_start(expander_box, False, False)
 329        if self.pats:
 330            button = gtk.Button(_('Remove filter, show root'))
 331            button.connect('pressed', self.remove_filter)
 332            tvbox.pack_start( button, False, False, 2)
 333
 334        tree_frame = gtk.Frame()
 335        tree_frame.set_shadow_type(gtk.SHADOW_ETCHED_IN)
 336        tree_frame.add(tvbox)
 337
 338        diff_frame = gtk.Frame()
 339        diff_frame.set_shadow_type(gtk.SHADOW_ETCHED_IN)
 340
 341        self.diff_notebook = gtk.Notebook()
 342        self.diff_notebook.set_tab_pos(gtk.POS_BOTTOM)
 343        self.diff_notebook_pages = {}
 344
 345        self.difffont = self.fonts['diff']
 346
 347        self.clipboard = None
 348
 349        self.diff_text = gtk.TextView()
 350        self.diff_text.set_wrap_mode(gtk.WRAP_NONE)
 351        self.diff_text.set_editable(False)
 352        self.diff_text.modify_font(self.difffont)
 353        scroller = gtk.ScrolledWindow()
 354        scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 355        scroller.add(self.diff_text)
 356        self.append_page('text-diff', scroller, gtk.Label(_('Text Diff')))
 357
 358        # use treeview to show selectable diff hunks
 359        self.clipboard = gtk.Clipboard()
 360
 361        # create chunks object
 362        self.chunks = chunks.chunks(self)
 363
 364        scroller = gtk.ScrolledWindow()
 365        scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 366        scroller.add(self.chunks.difftree())
 367        self.append_page('hunk-selection', scroller, gtk.Label(_('Hunk Selection')))
 368
 369        # Add a page for commit preview
 370        self.preview_text = gtk.TextView()
 371        self.preview_text.set_wrap_mode(gtk.WRAP_NONE)
 372        self.preview_text.set_editable(False)
 373        self.preview_text.modify_font(self.difffont)
 374        scroller = gtk.ScrolledWindow()
 375        scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 376        scroller.add(self.preview_text)
 377        self.preview_tab_name_label = gtk.Label(self.get_preview_tab_name())
 378        self.append_page('commit-preview', scroller,
 379            self.preview_tab_name_label)
 380
 381        diff_frame.add(self.diff_notebook)
 382
 383        if self.diffbottom:
 384            self.diffpane = gtk.VPaned()
 385        else:
 386            self.diffpane = gtk.HPaned()
 387
 388        self.diffpane.pack1(tree_frame, shrink=False)
 389        self.diffpane.pack2(diff_frame, shrink=False)
 390        self.filetree.set_headers_clickable(True)
 391
 392        sel = self.filetree.get_selection()
 393        sel.set_mode(gtk.SELECTION_MULTIPLE)
 394        self.treeselid = sel.connect('changed', self.tree_sel_changed)
 395
 396        self.diff_notebook.connect('switch-page', self.page_switched, sel)
 397
 398        # add keyboard accelerators
 399        accelgroup = self.get_accelgroup()     
 400        self.add_accel_group(accelgroup)
 401        
 402        return self.diffpane
 403
 404    def append_page(self, name, child, label):
 405        num = self.diff_notebook.append_page(child,  label)
 406        self.diff_notebook_pages[num] = name
 407
 408    def page_switched(self, notebook, page, page_num, filesel):
 409        self.tree_sel_changed(filesel, page_num)
 410
 411    def get_extras(self):
 412        self.stbar = statusbar.StatusBar()
 413        return self.stbar
 414
 415    def add_header_checkbox(self, col, post=None, pre=None, toggle=False):
 416        def cbclick(hdr, cb):
 417            state = cb.get_active()
 418            if pre:
 419                pre(state)
 420            if toggle:
 421                cb.set_active(not state)
 422            if post:
 423                post(not state)
 424
 425        cb = gtk.CheckButton(col.get_title())
 426        cb.show()
 427        col.set_widget(cb)
 428        wgt = cb.get_parent()
 429        while wgt:
 430            if type(wgt) == gtk.Button:
 431                wgt.connect('clicked', cbclick, cb)
 432                return cb
 433            wgt = wgt.get_parent()
 434        return
 435
 436    def update_check_count(self):
 437        file_count = 0
 438        check_count = 0
 439        for row in self.filemodel:
 440            file_count = file_count + 1
 441            if row[FM_CHECKED]:
 442                check_count = check_count + 1
 443        self.counter.set_text(_('%d selected, %d total') % (check_count,
 444                              file_count))
 445        if self.selcb:
 446            self.selcb.set_active(file_count and file_count == check_count)
 447        if self.count_revs() == 2:
 448            return
 449        sensitive = check_count and not self.is_merge()
 450        for cmd in ('diff', 'revert', 'add', 'remove', 'move', 'forget'):
 451            self.cmd_set_sensitive(cmd, sensitive)
 452        if self.diff_notebook.get_current_page() == 2:
 453            self.update_commit_preview()
 454
 455    def prepare_display(self):
 456        val = self.repo.ui.config('tortoisehg', 'ciexclude', '')
 457        self.excludes = [i.strip() for i in val.split(',') if i.strip()]
 458        gtklib.idle_add_single_call(self.realize_status_settings)
 459
 460    def refresh_complete(self):
 461        pass
 462
 463    def get_preview_tab_name(self):
 464        if self.count_revs() == 2:
 465            res = _('Save Preview')
 466        elif self.mqmode:
 467            res = _('Patch Preview')
 468        elif self.mode == 'shelve':
 469            res = _('Shelf Preview')
 470        else:
 471            res = _('Commit Preview')
 472        return res
 473
 474    ### End of overrides ###
 475
 476    def set_preview_tab_name(self, name=None):
 477        if self.preview_tab_name_label == None:
 478            return
 479        if name == None:
 480            name = self.get_preview_tab_name()
 481        self.preview_tab_name_label.set_text(name)
 482
 483    def types_expander_expanded(self, expander, dummy):
 484        if expander.get_expanded():
 485            self.status_types.show()
 486        else:
 487            self.status_types.hide()
 488
 489    def get_status_types(self):
 490        # Tuple: (onmerge, ctype, translated label)
 491        allchecks = [(False, 'unknown',  _('?: unknown')),
 492                     (True,  'modified', _('M: modified')),
 493                     (False, 'ignored',  _('I: ignored')),
 494                     (True,  'added',    _('A: added')),
 495                     (False, 'clean',    _('C: clean')),
 496                     (True,  'removed',  _('R: removed')),
 497                     (False, 'deleted',  _('!: deleted')),
 498                     (True, 'subrepo',  _('S: subrepo'))]
 499
 500        checks = []
 501        nomerge = (self.count_revs() <= 1)
 502        for onmerge, button, text in allchecks:
 503            if onmerge or nomerge:
 504                checks.append((button, text))
 505
 506        table = gtk.Table(rows=2, columns=3)
 507        table.set_col_spacings(8)
 508
 509        self._show_checks = {}
 510        row, col = 0, 0
 511
 512        for name, labeltext in checks:
 513            button = gtk.CheckButton(labeltext)
 514            widget = button
 515            button.connect('toggled', self.show_toggle, name)
 516            self._show_checks[name] = button
 517            table.attach(widget, col, col+1, row, row+1)
 518            col += row
 519            row = not row
 520
 521        hbox = gtk.HBox()
 522        hbox.pack_start(table, False, False)
 523
 524        return hbox
 525
 526    def realize_status_settings(self):
 527        if not self.types_expander.get_expanded():
 528            self.status_types.hide()
 529        self.diffpane.set_position(self.setting_pos)
 530        try:
 531            tab = self.ui.config('tortoisehg', 'statustab', '0')
 532            tab = int(tab)
 533            self.diff_notebook.set_current_page(tab)
 534        except (error.ConfigError, ValueError):
 535            pass
 536        self.reload_status()
 537
 538    def remove_filter(self, button):
 539        button.hide()
 540        self.pats = []
 541        for name, check in self._show_checks.iteritems():
 542            check.set_sensitive(True)
 543        self.set_title(self.get_title())
 544        self.reload_status()
 545
 546    def search_filelist(self, model, column, key, iter):
 547        'case insensitive filename search'
 548        key = key.lower()
 549        if key in model.get_value(iter, FM_PATH).lower():
 550            return False
 551        return True
 552
 553    def thgdiff(self, treeview):
 554        selection = treeview.get_selection()
 555        model, tpaths = selection.get_selected_rows()
 556        files = [model[p][FM_PATH] for p in tpaths]
 557        self._do_diff(files, self.opts)
 558
 559    def thgrefresh(self, window):
 560        self.reload_status()
 561
 562    def refresh_file_tree(self):
 563        """Clear out the existing ListStore model and reload it from the
 564        repository status.  Also recheck and reselect files that remain
 565        in the list.
 566        """
 567
 568        is_merge = self.is_merge()
 569        self.file_sel_column.set_visible(not is_merge)
 570        self.merge_state_column.set_visible(self.count_revs() <= 1)
 571
 572        selection = self.filetree.get_selection()
 573        if selection is None:
 574            return
 575
 576        (M, A, R, D, U, I, C) = self.status
 577        changetypes = (('M', 'modified', M),
 578                       ('A', 'added', A),
 579                       ('R', 'removed', R),
 580                       ('!', 'deleted', D),
 581                       ('?', 'unknown', U),
 582                       ('I', 'ignored', I),
 583                       ('C', 'clean', C))
 584
 585        # List of the currently checked and selected files to pass on to
 586        # the new data
 587        model, tpaths = selection.get_selected_rows()
 588        model = self.filemodel
 589        reselect = [model[path][FM_PATH] for path in tpaths]
 590        waschecked = {}
 591        for row in model:
 592            waschecked[row[FM_PATH]] = row[FM_CHECKED], row[FM_PARTIAL_SELECTED]
 593
 594        # merge-state of files
 595        ms = merge_.mergestate(self.repo)
 596
 597        # Load the new data into the tree's model
 598        self.filetree.hide()
 599        selection.handler_block(self.treeselid)
 600        self.filemodel.clear()
 601
 602        types = [ct for ct in changetypes if self.opts.get(ct[1])]
 603        for stat, _, wfiles in types:
 604            for wfile in wfiles:
 605                mst = wfile in ms and ms[wfile].upper() or ""
 606                lfile = util.localpath(wfile)
 607                defcheck = stat in 'MAR' and lfile not in self.excludes
 608                ck, p = waschecked.get(lfile, (defcheck, False))
 609                model.append([ck, stat, hglib.toutf(lfile), lfile, mst, p])
 610
 611        if self.test_opt('subrepo') or self.is_merge():
 612            for sdir in self.subrepos:
 613                lfile = util.localpath(sdir)
 614                defcheck = lfile not in self.excludes
 615                ck, p = waschecked.get(lfile, (defcheck, False))
 616                model.append([ck, 'S', hglib.toutf(lfile), lfile, '', p])
 617
 618        self.auto_check() # may check more files
 619
 620        for row in model:
 621            if row[FM_PARTIAL_SELECTED]:
 622                # force refresh of partially selected files
 623                self.chunks.update_hunk_model(row[FM_PATH], row[FM_CHECKED])
 624                self.chunks.clear()
 625            else:
 626                # demand refresh of full or non selection
 627                self.chunks.del_file(row[FM_PATH])
 628
 629        # recover selections
 630        firstrow = None
 631        for i, row in enumerate(model):
 632            if row[FM_PATH] in reselect:
 633                if firstrow is None:
 634                    firstrow = i
 635                else:
 636                    selection.select_iter(row.iter)
 637        selection.handler_unblock(self.treeselid)
 638
 639        if len(model):
 640            selection.select_path((firstrow or 0,))
 641        else:
 642            # clear diff pane if no files
 643            self.diff_text.set_buffer(gtk.TextBuffer())
 644            self.preview_text.set_buffer(gtk.TextBuffer())
 645            if not is_merge:
 646                self.chunks.clear()
 647
 648        self.filetree.show()
 649        if self.mode == 'commit':
 650            self.text.grab_focus()
 651        else:
 652            self.filetree.grab_focus()
 653        return True
 654
 655
 656    def reload_status(self):
 657        if not self.ready: return False
 658
 659        def get_repo_status():
 660            # Create a new repo object
 661            repo = hg.repository(self.ui, path=self.repo.root)
 662            self.newrepo = repo
 663            self.subrepos = []
 664
 665            try:
 666                if self.mqmode and self.mode != 'status':
 667                    # when a patch is applied, show diffs to parent of top patch
 668                    qtip = repo['.']
 669                    n1 = qtip.parents()[0].node()
 670                    n2 = None
 671                else:
 672                    # node2 is None (working dir) when 0 or 1 rev is specified
 673                    n1, n2 = cmdutil.revpair(repo, self.opts.get('rev'))
 674            except (util.Abort, error.RepoError), e:
 675                self.status_error = str(e)
 676                return
 677
 678            self._node1, self._node2 = n1, n2
 679            self.status_error = None
 680            matcher = cmdutil.match(repo, self.pats, self.opts)
 681            unknown = self.test_opt('unknown') and not self.is_merge()
 682            clean = self.test_opt('clean') and not self.is_merge()
 683            ignored = self.test_opt('ignored') and not self.is_merge()
 684            try:
 685                status = repo.status(node1=n1, node2=n2, match=matcher,
 686                                     ignored=ignored,
 687                                     clean=clean,
 688                                     unknown=unknown)
 689                self.status = status
 690            except (OSError, IOError, util.Abort), e:
 691                self.status_error = str(e)
 692
 693            if n2 is not None or self.mqmode:
 694                return
 695
 696            wctx = repo[None]
 697            try:
 698                for s in wctx.substate:
 699                    if matcher(s) and wctx.sub(s).dirty():
 700                        self.subrepos.append(s)
 701            except (error.ConfigError, error.RepoError), e:
 702                self.status_error = str(e)
 703            except (OSError, IOError, util.Abort), e:
 704                self.status_error = str(e)
 705
 706        def status_wait(thread):
 707            if thread.isAlive():
 708                return True
 709            else:
 710                if self.status_error:
 711                    self.ready = True
 712                    self.update_check_count()
 713                    self.stbar.end()
 714                    self.stbar.set_text(self.status_error)
 715                    return False
 716                self.repo = self.newrepo
 717                self.ui = self.repo.ui
 718                self.refresh_file_tree()
 719                self.update_check_count()
 720                self.refresh_complete()
 721                self.ready = True
 722                self.stbar.end()
 723                return False
 724
 725        self.set_preview_tab_name()
 726        repo = self.repo
 727        hglib.invalidaterepo(repo)
 728        if hasattr(repo, 'mq'):
 729            self.mqmode = repo.mq.applied and repo['.'] == repo['qtip']
 730            self.set_title(self.get_title())
 731
 732        self.ready = False
 733        self.stbar.begin()
 734        thread = threading.Thread(target=get_repo_status)
 735        thread.setDaemon(True)
 736        thread.start()
 737        gobject.timeout_add(50, status_wait, thread)
 738        return True
 739
 740    def nodes(self):
 741        return (self._node1, self._node2)
 742
 743    def get_ctx(self):
 744        'Return current changectx or workingctx'
 745        if self._node2 == None and not self.mqmode:
 746            return self.repo[None]
 747        else:
 748            return self.repo[self._node1]
 749
 750    def set_file_states(self, paths, state=True):
 751        for p in paths:
 752            self.filemodel[p][FM_CHECKED] = state
 753            self.update_chunk_state(self.filemodel[p])
 754        self.update_check_count()
 755
 756    def select_toggle(self, cellrenderer, path):
 757        'User manually toggled file status via checkbox'
 758        self.filemodel[path][FM_CHECKED] = not self.filemodel[path][FM_CHECKED]
 759        self.update_chunk_state(self.filemodel[path])
 760        self.update_check_count()
 761        return True
 762
 763    def update_chunk_state(self, fileentry):
 764        'Update chunk toggle state to match file toggle state'
 765        fileentry[FM_PARTIAL_SELECTED] = False
 766        wfile = fileentry[FM_PATH]
 767        selected = fileentry[FM_CHECKED]
 768        self.chunks.update_chunk_state(wfile, selected)
 769
 770    def updated_codes(self):
 771        types = [('modified', 'M'),
 772                 ('added',    'A'),
 773                 ('removed',  'R'),
 774                 ('unknown',  '?'),
 775                 ('deleted',  '!'),
 776                 ('ignored',  'I'),
 777                 ('clean',    'C') ]
 778        codes = ''
 779        try:
 780            for name, code in types:
 781                if self.opts[name]:
 782                    codes += code
 783        except KeyError:
 784            pass
 785        self.types_expander.set_label(_("View '%s'") % codes)
 786
 787    def show_toggle(self, check, toggletype):
 788        self.opts[toggletype] = check.get_active()
 789        self.reload_status()
 790        self.updated_codes()
 791        return True
 792
 793    def sort_by_stat(self, model, iter1, iter2):
 794        order = 'MAR!?SIC'
 795        lhs, rhs = (model.get_value(iter1, FM_STATUS),
 796                    model.get_value(iter2, FM_STATUS))
 797        # GTK+ bug that calls sort before a full row is inserted causing
 798        # values to be None.  When this happens, just return any value
 799        # since the call is irrelevant and will be followed by another
 800        # with the correct (non-None) value
 801        if None in (lhs, rhs):
 802            return 0
 803
 804        result = order.find(lhs) - order.find(rhs)
 805        return min(max(result, -1), 1)
 806
 807
 808    def text_color(self, column, text_renderer, model, row_iter):
 809        stat = model[row_iter][FM_STATUS]
 810        if stat == 'M':
 811            text_renderer.set_property('foreground', gtklib.DBLUE)
 812        elif stat == 'A':
 813            text_renderer.set_property('foreground', gtklib.DGREEN)
 814        elif stat == 'R':
 815            text_renderer.set_property('foreground', gtklib.DRED)
 816        elif stat == 'C':
 817            text_renderer.set_property('foreground', gtklib.NORMAL)
 818        elif stat == '!':
 819            text_renderer.set_property('foreground', gtklib.RED)
 820        elif stat == '?':
 821            text_renderer.set_property('foreground', gtklib.DORANGE)
 822        elif stat == 'I':
 823            text_renderer.set_property('foreground', gtklib.DGRAY)
 824        else:
 825            text_renderer.set_property('foreground', gtklib.NORMAL)
 826
 827
 828    def tree_sel_changed(self, selection, page_num=None):
 829        'Selection changed in file tree'
 830        # page_num may be supplied, if called from switch-page event
 831        model, paths = selection.get_selected_rows()
 832        if not paths:
 833            return
 834        row = paths[0]
 835
 836        # desensitize the text diff and hunk selection tabs
 837        # if a non-MAR file is selected
 838        status = model[row][FM_STATUS]
 839        enable = (status in 'MAR')
 840        self.enable_page('text-diff', enable)
 841        self.enable_page('hunk-selection', enable and not self.is_merge())
 842
 843        if page_num is None:
 844            page_num = self.diff_notebook.get_current_page()
 845
 846        pname = self.get_page_name(page_num)
 847        if pname == 'text-diff':
 848            buf = self.generate_text_diffs(row)
 849            self.diff_text.set_buffer(buf)
 850        elif pname == 'hunk-selection':
 851            fmrow = self.filemodel[row]
 852            self.chunks.update_hunk_model(fmrow[FM_PATH], fmrow[FM_CHECKED])
 853            if not self.is_merge() and self.chunks.len():
 854                self.chunks.difftree().scroll_to_cell(0, use_align=True, row_align=0.0)
 855        elif pname == 'commit-preview':
 856            self.update_commit_preview()
 857
 858    def get_page_name(self, num):
 859        try:
 860            return self.diff_notebook_pages[num]
 861        except KeyError:
 862            return ''
 863
 864    def enable_page(self, name, enable):
 865        for pnum in self.diff_notebook_pages:
 866            pname = self.get_page_name(pnum)
 867            if pname == name:
 868                child = self.diff_notebook.get_nth_page(pnum)
 869                if child:
 870                    child.set_sensitive(enable)
 871                    lb = self.diff_notebook.get_tab_label(child)
 872                    lb.set_sensitive(enable)
 873                return
 874
 875    def update_commit_preview(self):
 876        if self.is_merge():
 877            opts = patch.diffopts(self.ui, self.opts)
 878            opts.git = True
 879            wctx = self.repo[None]
 880            pctx1, pctx2 = wctx.parents()
 881            difftext = [_('===== Diff to first parent %d:%s =====\n') % (
 882                        pctx1.rev(), str(pctx1))]
 883            try:
 884                for s in patch.diff(self.repo, pctx1.node(), None, opts=opts):
 885                    difftext.extend(s.splitlines(True))
 886                difftext.append(_('\n===== Diff to second parent %d:%s =====\n') % (
 887                                pctx2.rev(), str(pctx2)))
 888                for s in patch.diff(self.repo, pctx2.node(), None, opts=opts):
 889                    difftext.extend(s.splitlines(True))
 890            except (IOError, error.RepoError, error.LookupError, util.Abort), e:
 891                self.stbar.set_text(str(e))
 892        else:
 893            buf = cStringIO.StringIO()
 894            for row in self.filemodel:
 895                if not row[FM_CHECKED]:
 896                    continue
 897                wfile = row[FM_PATH]
 898                chunks = self.chunks.get_chunks(wfile)
 899                for i, chunk in enumerate(chunks):
 900                    if i == 0:
 901                        chunk.write(buf)
 902                    elif chunk.active:
 903                        chunk.write(buf)
 904
 905            difftext = buf.getvalue().splitlines(True)
 906        self.preview_text.set_buffer(self.diff_highlight_buffer(difftext))
 907
 908    def diff_highlight_buffer(self, difftext):
 909        buf = gtk.TextBuffer()
 910        if self.colorstyle == 'background':
 911            buf.create_tag('removed', paragraph_background=gtklib.PRED)
 912            buf.create_tag('added', paragraph_background=gtklib.PGREEN)
 913        elif self.colorstyle == 'none':
 914            buf.create_tag('removed')
 915            buf.create_tag('added')
 916        else:
 917            buf.create_tag('removed', foreground=gtklib.DRED)
 918            buf.create_tag('added', foreground=gtklib.DGREEN)
 919        buf.create_tag('position', foreground=gtklib.DORANGE)
 920        buf.create_tag('header', foreground=gtklib.DBLUE)
 921
 922        bufiter = buf.get_start_iter()
 923        for line in difftext:
 924            line = hglib.toutf(line)
 925            if line.startswith('---') or line.startswith('+++'):
 926                buf.insert_with_tags_by_name(bufiter, line, 'header')
 927            elif line.startswith('-'):
 928                line = hglib.diffexpand(line)
 929                buf.insert_with_tags_by_name(bufiter, line, 'removed')
 930            elif line.startswith('+'):
 931                line = hglib.diffexpand(line)
 932                buf.insert_with_tags_by_name(bufiter, line, 'added')
 933            elif line.startswith('@@'):
 934                buf.insert_with_tags_by_name(bufiter, line, 'position')
 935            else:
 936                line = hglib.diffexpand(line)
 937                buf.insert(bufiter, line)
 938        return buf
 939
 940    def generate_text_diffs(self, row):
 941        wfile = self.filemodel[row][FM_PATH]
 942        pfile = util.pconvert(wfile)
 943        lines = chunks.check_max_diff(self.get_ctx(), pfile)
 944        if lines:
 945            return self.diff_highlight_buffer(lines)
 946        matcher = cmdutil.matchfiles(self.repo, [pfile])
 947        opts = patch.diffopts(self.ui, self.opts)
 948        opts.git = True
 949        difftext = []
 950        if self.is_merge():
 951            wctx = self.repo[None]
 952            pctx1, pctx2 = wctx.parents()
 953            difftext = [_('===== Diff to first parent %d:%s =====\n') % (
 954                        pctx1.rev(), str(pctx1))]
 955            try:
 956                for s in patch.diff(self.repo, pctx1.node(), None,
 957                                    match=matcher, opts=opts):
 958                    difftext.extend(s.splitlines(True))
 959                difftext.append(_('\n===== Diff to second parent %d:%s =====\n') % (
 960                                pctx2.rev(), str(pctx2)))
 961                for s in patch.diff(self.repo, pctx2.node(), None,
 962                                    match=matcher, opts=opts):
 963                    difftext.extend(s.splitlines(True))
 964            except (IOError, error.RepoError, error.LookupError, util.Abort), e:
 965                self.stbar.set_text(str(e))
 966        else:
 967            try:
 968                for s in patch.diff(self.repo, self._node1, self._node2,
 969                        match=matcher, opts=opts):
 970                    difftext.extend(s.splitlines(True))
 971            except (IOError, error.RepoError, error.LookupError, util.Abort), e:
 972                self.stbar.set_text(str(e))
 973        return self.diff_highlight_buffer(difftext)
 974
 975    def update_check_state(self, wfile, partial, newvalue):
 976        for fr in self.filemodel:
 977            if fr[FM_PATH] == wfile:
 978                if fr[FM_PARTIAL_SELECTED] != partial:
 979                    fr[FM_PARTIAL_SELECTED] = partial
 980                if fr[FM_CHECKED] != newvalue:
 981                    fr[FM_CHECKED] = newvalue
 982                    self.update_check_count()
 983                return
 984
 985    def get_checked(self, wfile):
 986        for fr in self.filemodel:
 987            if fr[FM_PATH] == wfile:
 988                return fr[FM_CHECKED]
 989        return False
 990
 991    def refresh_clicked(self, toolbutton, data=None):
 992        self.reload_status()
 993        return True
 994
 995    def save_clicked(self, toolbutton, data=None):
 996        'Write selected diff hunks to a patch file'
 997        revrange = self.opts.get('rev')[0]
 998        filename = "%s.patch" % revrange.replace(':', '_to_')
 999        result = gtklib.NativeSaveFileDialogWrapper(title=_('Save patch to'),
1000                                                    initial=self.repo.root,
1001                                                    filename=filename).run()
1002        if not result:
1003            return
1004
1005        buf = cStringIO.StringIO()
1006        files = []
1007        for row in self.filemodel:
1008            if not row[FM_CHECKED]:
1009                continue
1010            files.append(row[FM_PATH])
1011
1012        self.chunks.save(files, result)
1013
1014    def diff_clicked(self, toolbutton, data=None):
1015        diff_list = self.relevant_checked_files('MAR!')
1016        if len(diff_list) > 0:
1017            self._do_diff(diff_list, self.opts)
1018        else:
1019            gdialog.Prompt(_('Nothing Diffed'),
1020                   _('No diffable files selected'), self).run()
1021        return True
1022
1023    def revert_clicked(self, toolbutton, data=None):
1024        revert_list = self.relevant_checked_files('MAR!')
1025        if len(revert_list) > 0:
1026            self.act.hg_revert(revert_list)
1027        else:
1028            gdialog.Prompt(_('Nothing Reverted'),
1029                   _('No revertable files selected'), self).run()
1030        return True
1031
1032    def add_clicked(self, toolbutton, data=None):
1033        add_list = self.relevant_checked_files('?IR')
1034        if len(add_list) > 0:
1035            self.act.hg_add(add_list)
1036        else:
1037            gdialog.Prompt(_('Nothing Added'),
1038                   _('No addable files selected'), self).run()
1039        return True
1040
1041    def remove_clicked(self, toolbutton, data=None):
1042        remove_list = self.relevant_checked_files('C!')
1043        delete_list = self.relevant_checked_files('?I')
1044        if len(remove_list) > 0:
1045            self.act.hg_remove(remove_list)
1046        if len(delete_list) > 0:
1047            self.act.delete_files(delete_list)
1048        if not remove_list and not delete_list:
1049            gdialog.Prompt(_('Nothing Removed'),
1050                   _('No removable files selected'), self).run()
1051        return True
1052
1053    def move_clicked(self, toolbutton, data=None):
1054        move_list = self.relevant_checked_files('C')
1055        if move_list:
1056            # get destination directory to files into
1057            dlg = gtklib.NativeFolderSelectDialog(
1058                    title=_('Move files to directory...'),
1059                    initial=self.repo.root)
1060            destdir = dlg.run()
1061            if not destdir:
1062                return True
1063
1064            # verify directory
1065            destroot = paths.find_root(destdir)
1066            if destroot != self.repo.root:
1067                gdialog.Prompt(_('Nothing Moved'),
1068                       _('Cannot move outside repo!'), self).run()
1069                return True
1070
1071            # move the files to dest directory
1072            move_list.append(hglib.fromutf(destdir))
1073            self.act.hg_move(move_list)
1074        else:
1075            gdialog.Prompt(_('Nothing Moved'), _('No movable files selected\n\n'
1076                    'Note: only clean files can be moved.'), self).run()
1077        return True
1078
1079    def forget_clicked(self, toolbutton, data=None):
1080        forget_list = self.relevant_checked_files('CM')
1081        if len(forget_list) > 0:
1082            self.act.hg_forget(forget_list)
1083        else:
1084            gdialog.Prompt(_('Nothing Forgotten'),
1085                   _('No clean files selected'), self).run()
1086
1087    def ignoremask_updated(self):
1088        '''User has changed the ignore mask in hgignore dialog'''
1089        self.opts['check'] = True
1090        self.reload_status()
1091
1092    def relevant_checked_files(self, stats):
1093        return [item[FM_PATH] for item in self.filemodel \
1094                if item[FM_CHECKED] and item[FM_STATUS] in stats]
1095
1096    def sel_clicked(self, state):
1097        'selection header checkbox clicked'
1098        for entry in self.filemodel:
1099            if entry[FM_CHECKED] != state:
1100                entry[FM_CHECKED] = state
1101                self.update_chunk_state(entry)
1102        self.update_check_count()
1103
1104    def tree_button_press(self, treeview, event):
1105        '''Selection management for filetree right-click
1106
1107        If the user right-clicks on a currently-selected item in the
1108        filetree, preserve their entire existing selection for the popup menu.
1109
1110        http://www.daa.com.au/pipermail/pygtk/2005-June/010465.html
1111        '''
1112        if event.button != 3:
1113            return False
1114
1115        clicked_row = treeview.get_path_at_pos(int(event.x),
1116                                               int(event.y))
1117        if not clicked_row:
1118            return False
1119
1120        selection = treeview.get_selection()
1121        selected_rows = selection.get_selected_rows()[1]
1122
1123        # If they didn't right-click on a currently selected row,
1124        # change the selection
1125        if clicked_row[0] not in selected_rows:
1126            selection.unselect_all()
1127            selection.select_path(clicked_row[0])
1128
1129        return True
1130
1131    def tree_button_release(self, treeview, event):
1132        if event.button != 3:
1133            return False
1134        self.tree_popup_menu(treeview)
1135        return True
1136
1137    def tree_popup_menu(self, treeview):
1138        model, tpaths = treeview.get_selection().get_selected_rows()
1139        types = {'M':[], 'A':[], 'R':[], '!':[], 'I':[], '?':[], 'C':[],
1140                 'r':[], 'u':[], 'S':[]}
1141        all = []
1142        pathmap = {}
1143        for p in tpaths:
1144            row = model[p]
1145            file = util.pconvert(row[FM_PATH])
1146            ms = row[FM_MERGE_STATUS]
1147            if ms == 'R':
1148                types['r'].append(file)
1149            elif ms == 'U':
1150                types['u'].append(file)
1151            else:
1152                types[row[FM_STATUS]].append(file)
1153            all.append(file)
1154            pathmap[file] = p
1155
1156        def make(label, func, stats, icon=None, sens=True, paths=False):
1157            files = []
1158            for t in stats:
1159                files.extend(types[t])
1160            if not files:
1161                return
1162            args = [files]
1163            if paths:
1164                p = [pathmap[f] for f in files]
1165                args.append(p)
1166            item = menu.append(label, func, icon, args=args, sensitive=sens)
1167            return files
1168
1169        def vdiff(menuitem, files):
1170            self._do_diff(files, self.opts)
1171        def viewmissing(menuitem, files):
1172            self._view_files(files, True)
1173        def edit(menuitem, files):
1174            self._view_files(files, False)
1175        def other(menuitem, files):
1176            self._view_files(files, True)
1177        def revert(menuitem, files):
1178            self.act.hg_revert(files)
1179        def remove(menuitem, files):
1180            self.act.hg_remove(files)
1181        def log(menuitem, files):
1182            from tortoisehg.hgtk import history
1183            dlg = history.run(self.ui, canonpats=files)
1184            dlg.display()
1185        def annotate(menuitem, files):
1186            from tortoisehg.hgtk import datamine
1187            dlg = datamine.run(self.ui, *files)
1188            dlg.display()
1189        def forget(menuitem, files, paths):
1190            self.act.hg_forget(files)
1191            self.set_file_states(paths, state=False)
1192        def add(menuitem, files, paths):
1193            self.act.hg_add(files)
1194            self.set_file_states(paths, state=True)
1195        def delete(menuitem, files):
1196            self.act.delete_files(files)
1197        def unmark(menuitem, files):
1198            ms = merge_.mergestate(self.repo)
1199            for wfile in files:
1200                ms.mark(wfile, "u")
1201            ms.commit()
1202            self.reload_status()
1203        def mark(menuitem, files):
1204            ms = merge_.mergestate(self.repo)
1205            for wfile in files:
1206                ms.mark(wfile, "r")
1207            ms.commit()
1208            self.reload_status()
1209        def resolve(stat, files):
1210            wctx = self.repo[None]
1211            mctx = wctx.parents()[-1]
1212            ms = merge_.mergestate(self.repo)
1213            for wfile in files:
1214                ms.resolve(wfile, wctx, mctx)
1215            ms.commit()
1216            self.reload_status()
1217        def resolve_with(stat, tool, files):
1218            if tool:
1219                exe = filemerge._findtool(self.repo.ui, tool)
1220                oldmergeenv = os.environ.get('HGMERGE')
1221                os.environ['HGMERGE'] = exe
1222            resolve(stat, files)
1223            if tool:
1224                if oldmergeenv:
1225                    os.environ['HGMERGE'] = oldmergeenv
1226                else:
1227                    del os.environ['HGMERGE']
1228        def rename(menuitem, files):
1229            self.act.rename_file(files[0])
1230        def copy(menuitem, files):
1231            self.act.copy_file(files[0])
1232        def guess_rename(menuitem, files):
1233            dlg = guess.DetectRenameDialog()
1234            dlg.show_all()
1235            dlg.set_notify_func(self.ignoremask_updated)
1236        def ignore(menuitem, files):
1237            dlg = hgignore.HgIgnoreDialog(files[0])
1238            dlg.show_all()
1239            dlg.set_notify_func(self.ignoremask_updated)
1240
1241        menu = gtklib.MenuBuilder()
1242        make(_('_Visual Diff'), vdiff, 'MAR!ru', gtk.STOCK_JUSTIFY_FILL)
1243        make(_('Edit'), edit, 'MACI?ru', gtk.STOCK_EDIT)
1244        make(_('View missing'), viewmissing, 'R!')
1245        make(_('View other'), other, 'MAru', None, self.is_merge())
1246        menu.append_sep()
1247        make(_('_Revert'), revert, 'MAR!ru', gtk.STOCK_MEDIA_REWIND)
1248        make(_('_Add'), add, 'R', gtk.STOCK_ADD, paths=True)
1249        menu.append_sep()
1250        make(_('File History'), log, 'MARC!ru', 'menulog.ico')
1251        make(_('Annotate'), annotate, 'MARC!ru', 'menublame.ico')
1252        menu.append_sep()
1253        make(_('_Forget'), forget, 'MAC!ru', gtk.STOCK_CLEAR, paths=True)
1254        make(_('_Add'), add, 'I?', gtk.STOCK_ADD, paths=True)
1255        make(_('_Guess Rename...'), guess_rename, 'A?!', 'detect_rename.ico')
1256        make(_('_Ignore'), ignore, '?', 'ignore.ico')
1257        make(_('Remove versioned'), remove, 'C', 'menudelete.ico')
1258        make(_('_Delete unversioned'), delete, '?I', gtk.STOCK_DELETE)
1259        if len(all) == 1:
1260            menu.append_sep()
1261            make(_('_Copy...'), copy, 'MC', gtk.STOCK_COPY)
1262            make(_('Rename...'), rename, 'MC', 'general.ico')
1263        menu.append_sep()
1264        f = make(_('Restart Merge...'), resolve, 'u', 'menumerge.ico')
1265        make(_('Mark unresolved'), unmark, 'r', gtk.STOCK_NO)
1266        make(_('Mark resolved'), mark, 'u', gtk.STOCK_YES)
1267        if f:
1268            rmenu = gtk.Menu()
1269            for tool in hglib.mergetools(self.repo.ui):
1270                item = gtk.MenuItem(tool, True)
1271                item.connect('activate', resolve_with, tool, f)
1272                item.set_border_width(1)
1273                rmenu.append(item)
1274            menu.append_submenu(_('Restart merge with'), rmenu,
1275                                'menumerge.ico')
1276
1277        for label, func, stats, icon in self.get_custom_menus():
1278            make(label, func, stats, icon)
1279
1280        menu = menu.build()
1281        if len(menu.get_children()) > 0:
1282            menu.show_all()
1283            menu.popup(None, None, None, 0, 0)
1284            return True
1285
1286
1287    def tree_key_press(self, tree, event):
1288        'Make spacebar toggle selected rows'
1289        if event.keyval == 32:
1290            def toggler(model, path, bufiter):
1291                model[path][FM_CHECKED] = not model[path][FM_CHECKED]
1292                self.update_chunk_state(model[path])
1293
1294            selection = self.filetree.get_selection()
1295            selection.selected_foreach(toggler)
1296            self.update_check_count()
1297            return True
1298        return False
1299
1300
1301    def tree_row_act(self, tree, path, column):
1302        'Activation (return) triggers visual diff of selected rows'
1303        # Ignore activations (like double click) on the first column
1304        if column.get_sort_column_id() == 0:
1305            return True
1306
1307        model, tpaths = self.filetree.get_selection().get_selected_rows()
1308        files = [model[p][FM_PATH] for p in tpaths]
1309        self._do_diff(files, self.opts)
1310        return True
1311
1312    def isuptodate(self):
1313        oldparents = self.repo.dirstate.parents()
1314        self.repo.dirstate.invalidate()
1315        if oldparents == self.repo.dirstate.parents():
1316            return True
1317        response = gdialog.CustomPrompt(_('not up to date'),
1318           

Large files files are truncated, but you can click here to view the full file