PageRenderTime 54ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/tortoisehg/hgtk/status.py

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