/tortoisehg/hgtk/logview/treeview.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 606 lines · 520 code · 59 blank · 27 comment · 78 complexity · 25e306ed1291bdd736e240e4e590ab7e MD5 · raw file

  1. # treeview.py - changelog viewer implementation
  2. #
  3. # Copyright 2008 Steve Borho <steve@borho.org>
  4. #
  5. # This software may be used and distributed according to the terms of the
  6. # GNU General Public License version 2, incorporated herein by reference.
  7. ''' Mercurial revision DAG visualization library
  8. Implements a gtk.TreeModel which visualizes a Mercurial repository
  9. revision history.
  10. Portions of this code stolen mercilessly from bzr-gtk visualization
  11. dialog. Other portions stolen from graphlog extension.
  12. '''
  13. import gtk
  14. import gobject
  15. import pango
  16. import os
  17. import time
  18. from tortoisehg.util.i18n import _
  19. from tortoisehg.util import hglib
  20. from tortoisehg.hgtk.logview import treemodel
  21. from tortoisehg.hgtk.logview.graphcell import CellRendererGraph
  22. from tortoisehg.hgtk.logview.revgraph import *
  23. COLS = 'graph rev id revhex branch changes msg user date utc age tag svn'
  24. class TreeView(gtk.ScrolledWindow):
  25. __gproperties__ = {
  26. 'repo': (gobject.TYPE_PYOBJECT,
  27. 'Repository',
  28. 'The Mercurial repository being visualized',
  29. gobject.PARAM_CONSTRUCT_ONLY | gobject.PARAM_WRITABLE),
  30. 'limit': (gobject.TYPE_PYOBJECT,
  31. 'Revision Display Limit',
  32. 'The maximum number of revisions to display',
  33. gobject.PARAM_READWRITE),
  34. 'original-tip-revision': (gobject.TYPE_PYOBJECT,
  35. 'Tip revision when application opened',
  36. 'Revisions above this number will be drawn green',
  37. gobject.PARAM_READWRITE),
  38. 'msg-column-visible': (gobject.TYPE_BOOLEAN,
  39. 'Summary',
  40. 'Show summary column',
  41. True,
  42. gobject.PARAM_READWRITE),
  43. 'user-column-visible': (gobject.TYPE_BOOLEAN,
  44. 'User',
  45. 'Show user column',
  46. True,
  47. gobject.PARAM_READWRITE),
  48. 'date-column-visible': (gobject.TYPE_BOOLEAN,
  49. 'Date',
  50. 'Show date column',
  51. False,
  52. gobject.PARAM_READWRITE),
  53. 'utc-column-visible': (gobject.TYPE_BOOLEAN,
  54. 'UTC',
  55. 'Show UTC/GMT date column',
  56. False,
  57. gobject.PARAM_READWRITE),
  58. 'age-column-visible': (gobject.TYPE_BOOLEAN,
  59. 'Age',
  60. 'Show age column',
  61. False,
  62. gobject.PARAM_READWRITE),
  63. 'rev-column-visible': (gobject.TYPE_BOOLEAN,
  64. 'Rev',
  65. 'Show revision number column',
  66. False,
  67. gobject.PARAM_READWRITE),
  68. 'id-column-visible': (gobject.TYPE_BOOLEAN,
  69. 'ID',
  70. 'Show revision ID column',
  71. False,
  72. gobject.PARAM_READWRITE),
  73. 'revhex-column-visible': (gobject.TYPE_BOOLEAN,
  74. 'Rev/ID',
  75. 'Show revision number/ID column',
  76. False,
  77. gobject.PARAM_READWRITE),
  78. 'branch-column-visible': (gobject.TYPE_BOOLEAN,
  79. 'Branch',
  80. 'Show branch',
  81. False,
  82. gobject.PARAM_READWRITE),
  83. 'changes-column-visible': (gobject.TYPE_BOOLEAN,
  84. 'Changes',
  85. 'Show changes column',
  86. False,
  87. gobject.PARAM_READWRITE),
  88. 'tag-column-visible': (gobject.TYPE_BOOLEAN,
  89. 'Tags',
  90. 'Show tag column',
  91. False,
  92. gobject.PARAM_READWRITE),
  93. 'svn-column-visible': (gobject.TYPE_BOOLEAN,
  94. 'Subversion',
  95. 'Show Subversion column',
  96. False,
  97. gobject.PARAM_READWRITE),
  98. 'branch-color': (gobject.TYPE_BOOLEAN,
  99. 'Branch color',
  100. 'Color by branch',
  101. False,
  102. gobject.PARAM_READWRITE)
  103. }
  104. __gsignals__ = {
  105. 'revisions-loaded': (gobject.SIGNAL_RUN_FIRST,
  106. gobject.TYPE_NONE,
  107. ()),
  108. 'batch-loaded': (gobject.SIGNAL_RUN_FIRST,
  109. gobject.TYPE_NONE,
  110. ()),
  111. 'revision-selected': (gobject.SIGNAL_RUN_FIRST,
  112. gobject.TYPE_NONE,
  113. ())
  114. }
  115. def __init__(self, repo, limit=500, stbar=None):
  116. """Create a new TreeView.
  117. :param repo: Repository object to show
  118. """
  119. gtk.ScrolledWindow.__init__(self)
  120. self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
  121. self.set_shadow_type(gtk.SHADOW_IN)
  122. self.batchsize = limit
  123. self.repo = repo
  124. self.currevid = None
  125. self.stbar = stbar
  126. self.grapher = None
  127. self.graphdata = []
  128. self.index = {}
  129. self.opts = { 'outgoing':[], 'orig-tip':None, 'npreviews':0,
  130. 'branch-color':False, 'show-graph':True }
  131. self.construct_treeview()
  132. def set_repo(self, repo, stbar=None):
  133. self.repo = repo
  134. self.stbar = stbar
  135. def search_in_tree(self, model, column, key, iter, data):
  136. """Searches all fields shown in the tree when the user hits crtr+f,
  137. not just the ones that are set via tree.set_search_column.
  138. Case insensitive
  139. """
  140. key = key.lower()
  141. row = model[iter]
  142. if row[treemodel.HEXID].startswith(key):
  143. return False
  144. for col in (treemodel.REVID, treemodel.COMMITER, treemodel.MESSAGE):
  145. if key in str(row[col]).lower():
  146. return False
  147. return True
  148. def create_log_generator(self, graphcol, pats, opts):
  149. if self.repo is None:
  150. self.grapher = None
  151. return
  152. only_branch = opts.get('branch', None)
  153. if opts.get('filehist') is not None:
  154. self.grapher = filelog_grapher(self.repo, opts['filehist'])
  155. elif graphcol:
  156. end = 0
  157. if only_branch is not None:
  158. b = self.repo.branchtags()
  159. if only_branch in b:
  160. node = b[only_branch]
  161. start = self.repo.changelog.rev(node)
  162. else:
  163. start = len(self.repo.changelog) - 1
  164. elif opts.get('revrange'):
  165. if len(opts['revrange']) >= 2:
  166. start, end = opts['revrange']
  167. else:
  168. start = opts['revrange'][0]
  169. end = start
  170. else:
  171. start = len(self.repo.changelog) - 1
  172. noheads = opts.get('noheads', False)
  173. if opts.get('branch-view', False):
  174. self.grapher = branch_grapher(self.repo, start, end,
  175. only_branch, self.opts.get('branch-color'))
  176. else:
  177. self.grapher = revision_grapher(self.repo, start, end,
  178. only_branch, noheads, self.opts.get('branch-color'))
  179. elif opts.get('revlist', None):
  180. graphcol = True
  181. self.grapher = revision_grapher(self.repo, only_revs=opts['revlist'])
  182. else:
  183. self.grapher = filtered_log_generator(self.repo, pats, opts)
  184. self.opts['show-graph'] = graphcol
  185. self.graphdata = []
  186. self.index = {}
  187. self.max_cols = 1
  188. self.model = None
  189. self.limit = self.batchsize
  190. def populate(self, revid=None):
  191. 'Fill the treeview with contents'
  192. stopped = False
  193. if self.repo is None:
  194. stopped = True
  195. return False
  196. if os.name == "nt":
  197. timer = time.clock
  198. else:
  199. timer = time.time
  200. startsec = timer()
  201. try:
  202. while (not self.limit) or len(self.graphdata) < self.limit:
  203. (rev, node, lines, wfile) = self.grapher.next()
  204. self.max_cols = max(self.max_cols, len(lines))
  205. self.index[rev] = len(self.graphdata)
  206. self.graphdata.append( (rev, node, lines, wfile) )
  207. if self.model:
  208. rowref = self.model.get_iter(len(self.graphdata)-1)
  209. path = self.model.get_path(rowref)
  210. self.model.row_inserted(path, rowref)
  211. cursec = timer()
  212. if cursec < startsec or cursec > startsec + 0.1:
  213. break
  214. except StopIteration:
  215. stopped = True
  216. if stopped:
  217. pass
  218. elif self.limit is None:
  219. return True
  220. elif len(self.graphdata) < self.limit:
  221. return True
  222. if not len(self.graphdata):
  223. self.treeview.set_model(None)
  224. if self.stbar is not None:
  225. self.stbar.end()
  226. self.emit('revisions-loaded')
  227. return False
  228. self.graph_cell.columns_len = self.max_cols
  229. width = self.graph_cell.get_size(self.treeview)[2]
  230. if width > 500:
  231. width = 500
  232. gcol = self.tvcolumns['graph']
  233. gcol.set_fixed_width(width)
  234. gcol.set_visible(self.opts.get('show-graph'))
  235. if not self.model:
  236. model = treemodel.TreeModel(self.repo, self.graphdata, self.opts)
  237. self.treeview.set_model(model)
  238. self.model = model
  239. self.emit('batch-loaded')
  240. if stopped:
  241. self.emit('revisions-loaded')
  242. if revid is not None:
  243. self.set_revision_id(revid)
  244. if self.stbar is not None:
  245. self.stbar.end()
  246. revision_text = _('%(count)d of %(total)d Revisions') % {
  247. 'count': len(self.model),
  248. 'total': len(self.repo) }
  249. self.stbar.set_text(revision_text, name='rev')
  250. return False
  251. def do_get_property(self, property):
  252. pn = property.name
  253. cv = '-column-visible'
  254. if pn.endswith(cv):
  255. colname = pn[:-len(cv)]
  256. return self.tvcolumns[colname].get_visible()
  257. elif pn == 'branch-color':
  258. return self.opts.get('branch-color')
  259. elif pn == 'repo':
  260. return self.repo
  261. elif pn == 'limit':
  262. return self.limit
  263. else:
  264. raise AttributeError, 'unknown property %s' % pn
  265. def do_set_property(self, property, value):
  266. pn = property.name
  267. cv = '-column-visible'
  268. if pn.endswith(cv):
  269. colname = pn[:-len(cv)]
  270. self.tvcolumns[colname].set_visible(value)
  271. elif pn == 'branch-color':
  272. self.opts['branch-color'] = value
  273. elif pn == 'repo':
  274. self.repo = value
  275. elif pn == 'limit':
  276. self.batchsize = value
  277. else:
  278. raise AttributeError, 'unknown property %s' % pn
  279. def get_revid_at_path(self, path):
  280. return self.model[path][treemodel.REVID]
  281. def get_path_at_revid(self, revid):
  282. if revid in self.index:
  283. row_index = self.index[revid]
  284. iter = self.model.get_iter(row_index)
  285. path = self.model.get_path(iter)
  286. return path
  287. else:
  288. return None
  289. def get_wfile_at_path(self, path):
  290. if self.model:
  291. return self.model[path][treemodel.WFILE]
  292. else:
  293. return None
  294. def next_revision_batch(self, size):
  295. if not self.grapher:
  296. self.emit('revisions-loaded')
  297. return
  298. self.batchsize = size
  299. self.limit += self.batchsize
  300. if self.stbar is not None:
  301. self.stbar.begin()
  302. gobject.idle_add(self.populate)
  303. def load_all_revisions(self):
  304. if not self.grapher:
  305. self.emit('revisions-loaded')
  306. return
  307. self.limit = None
  308. if self.stbar is not None:
  309. self.stbar.begin()
  310. gobject.idle_add(self.populate)
  311. def scroll_to_revision(self, revid):
  312. if revid in self.index:
  313. row = self.index[revid]
  314. self.treeview.scroll_to_cell(row, use_align=True, row_align=0.5)
  315. def set_revision_id(self, revid, load=False):
  316. """Change the currently selected revision.
  317. :param revid: Revision id of revision to display.
  318. """
  319. if revid in self.index:
  320. row = self.index[revid]
  321. self.treeview.set_cursor(row)
  322. self.treeview.grab_focus()
  323. elif load:
  324. handlers = [None, None]
  325. def loaded(dummy):
  326. if revid in self.index:
  327. hload = handlers[0]
  328. if hload is not None:
  329. self.disconnect(hload)
  330. handlers[0] = None
  331. self.set_revision_id(revid)
  332. self.scroll_to_revision(revid)
  333. else:
  334. self.next_revision_batch(self.batchsize)
  335. def stopped(dummy):
  336. hload, hstop = handlers
  337. if hload is not None:
  338. self.disconnect(hload)
  339. handlers[0] = None
  340. if hstop is not None:
  341. self.disconnect(hstop)
  342. handlers[1] = None
  343. self.stbar.set_text(_('Changeset not found in current view'))
  344. try:
  345. ctx = self.repo[revid]
  346. if ctx.rev() == -1:
  347. self.stbar.set_text(_('Null changeset is not viewable'))
  348. return
  349. except Exception, e:
  350. self.stbar.set_text(str(e))
  351. return
  352. handlers[0] = self.connect('batch-loaded', loaded)
  353. handlers[1] = self.connect('revisions-loaded', stopped)
  354. self.next_revision_batch(self.batchsize)
  355. def refresh(self, graphcol, pats, opts):
  356. self.opts.update(opts)
  357. if self.repo is not None:
  358. hglib.invalidaterepo(self.repo)
  359. if len(self.repo) > 0:
  360. self.create_log_generator(graphcol, pats, opts)
  361. if self.stbar is not None:
  362. self.stbar.begin()
  363. gobject.idle_add(self.populate, self.currevid)
  364. else:
  365. self.treeview.set_model(None)
  366. self.stbar.set_text(_('Repository is empty'))
  367. def construct_treeview(self):
  368. self.treeview = gtk.TreeView()
  369. self.treeview.set_rules_hint(True)
  370. self.treeview.set_reorderable(False)
  371. self.treeview.set_enable_search(True)
  372. self.treeview.set_search_equal_func(self.search_in_tree, None)
  373. self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
  374. self.treeview.connect("cursor-changed", self._on_selection_changed)
  375. self.treeview.set_property('fixed-height-mode', True)
  376. self.treeview.show()
  377. self.add(self.treeview)
  378. self.tvcolumns = {}
  379. self.graph_cell = CellRendererGraph()
  380. col = self.tvcolumns['graph'] = gtk.TreeViewColumn(_('Graph'))
  381. col.set_resizable(True)
  382. col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
  383. col.pack_start(self.graph_cell, expand=False)
  384. col.add_attribute(self.graph_cell,
  385. "node", treemodel.GRAPHNODE)
  386. col.add_attribute(self.graph_cell,
  387. "in-lines", treemodel.LAST_LINES)
  388. col.add_attribute(self.graph_cell,
  389. "out-lines", treemodel.LINES)
  390. cell = gtk.CellRendererText()
  391. cell.set_property("width-chars", 8)
  392. cell.set_property("ellipsize", pango.ELLIPSIZE_END)
  393. cell.set_property("xalign", 1.0)
  394. col = self.tvcolumns['rev'] = gtk.TreeViewColumn(_('Rev'))
  395. col.set_visible(False)
  396. col.set_resizable(True)
  397. col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
  398. col.set_fixed_width(cell.get_size(self.treeview)[2])
  399. col.pack_start(cell, expand=True)
  400. col.add_attribute(cell, "text", treemodel.REVID)
  401. col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
  402. cell = gtk.CellRendererText()
  403. cell.set_property("width-chars", 15)
  404. cell.set_property("ellipsize", pango.ELLIPSIZE_END)
  405. cell.set_property("family", "Monospace")
  406. col = self.tvcolumns['id'] = gtk.TreeViewColumn(_('ID'))
  407. col.set_visible(False)
  408. col.set_resizable(True)
  409. col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
  410. col.set_fixed_width(cell.get_size(self.treeview)[2])
  411. col.pack_start(cell, expand=True)
  412. col.add_attribute(cell, "text", treemodel.HEXID)
  413. col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
  414. cell = gtk.CellRendererText()
  415. cell.set_property("width-chars", 22)
  416. cell.set_property("ellipsize", pango.ELLIPSIZE_END)
  417. col = self.tvcolumns['revhex'] = gtk.TreeViewColumn(_('Rev/ID'))
  418. col.set_visible(False)
  419. col.set_resizable(True)
  420. col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
  421. col.set_fixed_width(cell.get_size(self.treeview)[2])
  422. col.pack_start(cell, expand=True)
  423. col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
  424. col.add_attribute(cell, "markup", treemodel.REVHEX)
  425. cell = gtk.CellRendererText()
  426. cell.set_property("width-chars", 15)
  427. cell.set_property("ellipsize", pango.ELLIPSIZE_END)
  428. col = self.tvcolumns['branch'] = gtk.TreeViewColumn(_('Branch'))
  429. col.set_visible(False)
  430. col.set_resizable(True)
  431. col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
  432. col.set_fixed_width(cell.get_size(self.treeview)[2])
  433. col.pack_start(cell, expand=True)
  434. col.add_attribute(cell, "text", treemodel.BRANCH)
  435. col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
  436. cell = gtk.CellRendererText()
  437. cell.set_property("width-chars", 10)
  438. cell.set_property("ellipsize", pango.ELLIPSIZE_END)
  439. col = self.tvcolumns['changes'] = gtk.TreeViewColumn(_('Changes'))
  440. col.set_visible(False)
  441. col.set_resizable(True)
  442. col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
  443. col.set_fixed_width(cell.get_size(self.treeview)[2])
  444. col.pack_start(cell, expand=True)
  445. col.add_attribute(cell, "markup", treemodel.CHANGES)
  446. cell = gtk.CellRendererText()
  447. cell.set_property("width-chars", 80)
  448. cell.set_property("ellipsize", pango.ELLIPSIZE_END)
  449. col = self.tvcolumns['msg'] = gtk.TreeViewColumn(_('Summary'))
  450. col.set_resizable(True)
  451. col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
  452. col.set_fixed_width(cell.get_size(self.treeview)[2])
  453. col.pack_end(cell, expand=True)
  454. col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
  455. col.add_attribute(cell, "markup", treemodel.MESSAGE)
  456. cell = gtk.CellRendererText()
  457. cell.set_property("width-chars", 20)
  458. cell.set_property("ellipsize", pango.ELLIPSIZE_END)
  459. col = self.tvcolumns['user'] = gtk.TreeViewColumn(_('User'))
  460. col.set_resizable(True)
  461. col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
  462. col.set_fixed_width(cell.get_size(self.treeview)[2])
  463. col.pack_start(cell, expand=True)
  464. col.add_attribute(cell, "text", treemodel.COMMITER)
  465. col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
  466. cell = gtk.CellRendererText()
  467. cell.set_property("width-chars", 20)
  468. cell.set_property("ellipsize", pango.ELLIPSIZE_END)
  469. col = self.tvcolumns['date'] = gtk.TreeViewColumn(_('Local Date'))
  470. col.set_visible(False)
  471. col.set_resizable(True)
  472. col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
  473. col.set_fixed_width(cell.get_size(self.treeview)[2])
  474. col.pack_start(cell, expand=True)
  475. col.add_attribute(cell, "text", treemodel.LOCALTIME)
  476. col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
  477. cell = gtk.CellRendererText()
  478. cell.set_property("width-chars", 20)
  479. cell.set_property("ellipsize", pango.ELLIPSIZE_END)
  480. col = self.tvcolumns['utc'] = gtk.TreeViewColumn(_('Universal Date'))
  481. col.set_visible(False)
  482. col.set_resizable(True)
  483. col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
  484. col.set_fixed_width(cell.get_size(self.treeview)[2])
  485. col.pack_start(cell, expand=True)
  486. col.add_attribute(cell, "text", treemodel.UTC)
  487. col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
  488. cell = gtk.CellRendererText()
  489. cell.set_property("width-chars", 16)
  490. cell.set_property("ellipsize", pango.ELLIPSIZE_END)
  491. col = self.tvcolumns['age'] = gtk.TreeViewColumn(_('Age'))
  492. col.set_visible(True)
  493. col.set_resizable(True)
  494. col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
  495. col.set_fixed_width(cell.get_size(self.treeview)[2])
  496. col.pack_start(cell, expand=True)
  497. col.add_attribute(cell, "text", treemodel.AGE)
  498. col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
  499. cell = gtk.CellRendererText()
  500. cell.set_property("width-chars", 10)
  501. cell.set_property("ellipsize", pango.ELLIPSIZE_END)
  502. col = self.tvcolumns['tag'] = gtk.TreeViewColumn(_('Tags'))
  503. col.set_visible(False)
  504. col.set_resizable(True)
  505. col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
  506. col.set_fixed_width(cell.get_size(self.treeview)[2])
  507. col.pack_start(cell, expand=True)
  508. col.add_attribute(cell, "text", treemodel.TAGS)
  509. col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
  510. cell = gtk.CellRendererText()
  511. cell.set_property("width-chars", 10)
  512. cell.set_property("ellipsize", pango.ELLIPSIZE_END)
  513. col = self.tvcolumns['svn'] = gtk.TreeViewColumn(_('Subversion'))
  514. col.set_visible(False)
  515. col.set_resizable(True)
  516. col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
  517. col.set_fixed_width(cell.get_size(self.treeview)[2])
  518. col.pack_start(cell, expand=True)
  519. col.add_attribute(cell, "text", treemodel.SVNREV)
  520. col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
  521. self.columns = COLS.split()
  522. def set_columns(self, columns):
  523. if ' '.join(columns) != ' '.join(self.columns):
  524. cols = self.treeview.get_columns()
  525. for cn in self.columns:
  526. c = self.tvcolumns[cn]
  527. if c in cols:
  528. self.treeview.remove_column(c)
  529. for cn in columns:
  530. try:
  531. c = self.tvcolumns[cn]
  532. self.treeview.append_column(c)
  533. except KeyError:
  534. continue
  535. self.columns = columns
  536. def get_columns(self):
  537. return self.columns
  538. def _on_selection_changed(self, treeview):
  539. """callback for when the treeview changes."""
  540. (path, focus) = treeview.get_cursor()
  541. if path and self.model:
  542. self.currevid = self.model[path][treemodel.REVID]
  543. self.emit('revision-selected')