/tortoisehg/hgtk/cslist.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 875 lines · 745 code · 42 blank · 88 comment · 29 complexity · 71340e9dbed71f7b778f12908cb05ade MD5 · raw file

  1. # cslist.py - embeddable changeset/patch list component
  2. #
  3. # Copyright 2009 Yuki KODAMA <endflow.net@gmail.com>
  4. #
  5. # This software may be used and distributed according to the terms of the
  6. # GNU General Public License version 2, incorporated herein by reference.
  7. import os
  8. import gtk
  9. import gobject
  10. from mercurial import hg, ui
  11. from tortoisehg.util.i18n import _
  12. from tortoisehg.util import hglib, paths
  13. from tortoisehg.hgtk import csinfo, gtklib
  14. CSL_DND_ITEM = 1024
  15. CSL_DND_URI_LIST = 1025
  16. ASYNC_LIMIT = 60
  17. class ChangesetList(gtk.Frame):
  18. __gsignals__ = {
  19. 'list-updated': (gobject.SIGNAL_RUN_FIRST,
  20. gobject.TYPE_NONE,
  21. (object, # number of all items or None
  22. object, # number of selections or None
  23. object, # number of showings or None
  24. bool)), # whether cslist is updating
  25. 'files-dropped': (gobject.SIGNAL_RUN_FIRST,
  26. gobject.TYPE_NONE,
  27. (object, # list of dropped files
  28. str)), # raw string data
  29. 'item-activated': (gobject.SIGNAL_RUN_FIRST,
  30. gobject.TYPE_NONE,
  31. (str, # revision number or patch file path
  32. object)) # reference of csinfo widget
  33. }
  34. def __init__(self):
  35. gtk.Frame.__init__(self)
  36. self.set_shadow_type(gtk.SHADOW_IN)
  37. # member variables
  38. self.curitems = None
  39. self.currepo = None
  40. self.showitems = None
  41. self.chkmap = {}
  42. self.limit = 20
  43. self.curfactory = None
  44. self.timeout_queue = []
  45. self.sel_enable = False
  46. self.dnd_enable = False
  47. self.act_enable = False
  48. # dnd variables
  49. self.itemmap = {}
  50. self.hlsep = None
  51. self.dnd_pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, 1, 1)
  52. self.scroll_timer = None
  53. # base box
  54. basebox = gtk.VBox()
  55. self.add(basebox)
  56. ## status box
  57. self.statusbox = statusbox = gtk.HBox()
  58. basebox.pack_start(statusbox, False, False)
  59. basebox.pack_start(gtk.HSeparator(), False, False, 2)
  60. # copy form thgstrip.py
  61. def createlabel():
  62. label = gtk.Label()
  63. label.set_alignment(0, 0.5)
  64. label.set_size_request(-1, 24)
  65. label.size_request()
  66. return label
  67. ### status label
  68. self.statuslabel = createlabel()
  69. statusbox.pack_start(self.statuslabel, False, False, 2)
  70. ### show all button
  71. self.allbtn = gtk.Button(_('Show all')) # add later
  72. ### list option
  73. self.compactopt = gtk.CheckButton(_('Use compact view'))
  74. statusbox.pack_end(self.compactopt, False, False, 2)
  75. ## item list
  76. scroll = gtk.ScrolledWindow()
  77. basebox.add(scroll)
  78. self.scroll = scroll
  79. scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
  80. scroll.set_size_request(400, 180)
  81. scroll.size_request()
  82. self.csbox = gtk.VBox()
  83. self.csevent = csevent = gtk.EventBox()
  84. csevent.add(self.csbox)
  85. csevent.add_events(gtk.gdk.BUTTON_PRESS_MASK |
  86. gtk.gdk.BUTTON_RELEASE_MASK)
  87. scroll.add_with_viewport(csevent)
  88. scroll.child.set_shadow_type(gtk.SHADOW_NONE)
  89. self.csbox.set_border_width(4)
  90. # signal handlers
  91. self.allbtn.connect('clicked', lambda b: self.expand_items())
  92. self.compactopt.connect('toggled', lambda b: self.update( \
  93. self.curitems, self.currepo, queue=False, keep=True))
  94. # dnd setup
  95. self.dnd_targets = [('thg-dnd', gtk.TARGET_SAME_WIDGET, CSL_DND_ITEM)]
  96. targets = self.dnd_targets + [('text/uri-list', 0, CSL_DND_URI_LIST)]
  97. csevent.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_DROP,
  98. targets, gtk.gdk.ACTION_MOVE)
  99. csevent.connect('drag-begin', self.dnd_begin)
  100. csevent.connect('drag-end', self.dnd_end)
  101. csevent.connect('drag-motion', self.dnd_motion)
  102. csevent.connect('drag-leave', self.dnd_leave)
  103. csevent.connect('drag-data-received', self.dnd_received)
  104. csevent.connect('drag-data-get', self.dnd_get)
  105. csevent.connect('button-press-event', self.button_press)
  106. # csetinfo
  107. def data_func(widget, item, ctx):
  108. if item in ('item', 'item_l'):
  109. if not isinstance(ctx, csinfo.patchctx):
  110. return True # dummy
  111. revid = widget.get_data('revid')
  112. if not revid:
  113. return widget.target
  114. filename = os.path.basename(widget.target)
  115. return filename, revid
  116. raise csinfo.UnknownItem(item)
  117. def label_func(widget, item):
  118. if item in ('item', 'item_l'):
  119. if not isinstance(widget.ctx, csinfo.patchctx):
  120. return _('Revision:')
  121. return _('Patch:')
  122. raise csinfo.UnknownItem(item)
  123. def markup_func(widget, item, value):
  124. if item in ('item', 'item_l'):
  125. if not isinstance(widget.ctx, csinfo.patchctx):
  126. if item == 'item':
  127. return widget.get_markup('rev')
  128. return widget.get_markup('revnum')
  129. mono = dict(face='monospace', size='9000')
  130. if isinstance(value, basestring):
  131. return gtklib.markup(value, **mono)
  132. filename = gtklib.markup(value[0])
  133. revid = gtklib.markup(value[1], **mono)
  134. if item == 'item':
  135. return '%s (%s)' % (filename, revid)
  136. return filename
  137. raise csinfo.UnknownItem(item)
  138. self.custom = csinfo.custom(data=data_func, label=label_func,
  139. markup=markup_func)
  140. self.lstyle = csinfo.labelstyle(
  141. contents=('%(item_l)s:', ' %(branch)s',
  142. ' %(tags)s', ' %(summary)s'))
  143. self.pstyle = csinfo.panelstyle(
  144. contents=('item', 'summary', 'user','dateage',
  145. 'rawbranch', 'tags', 'transplant',
  146. 'p4', 'svn'))
  147. # prepare to show
  148. gtklib.idle_add_single_call(self.after_init)
  149. ### public functions ###
  150. def update(self, items=None, repo=None, limit=True, queue=False, **kargs):
  151. """
  152. Update the item list.
  153. Public arguments:
  154. items: List of revision numbers and/or patch file path.
  155. You can pass mixed list. The order will be respected.
  156. If omitted, previous items will be used to show them.
  157. Default: None.
  158. repo: Repository used to get changeset information.
  159. If omitted, previous repo will be used to show them.
  160. Default: None.
  161. limit: If True, some of items will be shown. Default: True.
  162. queue: If True, the update request will be queued to prevent
  163. frequent updatings. In some cases, this option will help
  164. to improve UI response. Default: False.
  165. Internal argument:
  166. keep: If True, it keeps previous selection states and 'limit' value
  167. after refreshing. Note that if you use 'limit' and this options
  168. at the same time, 'limit' value is used against previous value.
  169. Default: False.
  170. return: True if the item list was updated successfully,
  171. False if it wasn't updated.
  172. """
  173. # check parameters
  174. if not items or not repo:
  175. self.clear()
  176. return False
  177. elif queue:
  178. def timeout(eid, items, repo):
  179. if self.timeout_queue and self.timeout_queue[-1] == eid[0]:
  180. self.timeout_queue = []
  181. self.update(items, repo, limit, False)
  182. return False # don't repeat
  183. eid = [None]
  184. eid[0] = gobject.timeout_add(650, timeout, eid, items, repo)
  185. self.timeout_queue.append(eid[0])
  186. return False
  187. # determine whether to keep previous 'limit' state
  188. if kargs.get('keep', False) and self.has_limit() is False:
  189. limit = False
  190. # initialize variables
  191. self.curitems = items
  192. self.currepo = repo
  193. self.itemmap = {}
  194. if self.sel_enable and not kargs.get('keep', False):
  195. self.chkmap = {}
  196. for item in items:
  197. self.chkmap[item] = True
  198. # determine items to show
  199. numtotal = len(items)
  200. if limit and self.limit < numtotal:
  201. toshow, lastitem = items[:self.limit-1], items[-1]
  202. else:
  203. toshow, lastitem = items, None
  204. numshow = len(toshow) + (lastitem and 1 or 0)
  205. self.showitems = toshow + (lastitem and [lastitem] or [])
  206. # prepare to update item list
  207. self.curfactory = csinfo.factory(repo, self.custom, withupdate=True)
  208. def add_sep():
  209. sep = self.create_sep()
  210. self.csbox.pack_start(sep, False, False)
  211. # clear item list
  212. self.csbox.foreach(lambda c: c.parent.remove(c))
  213. # update item list
  214. def proc():
  215. # add csinfo widgets
  216. for index, r in enumerate(toshow):
  217. self.add_csinfo(r)
  218. if lastitem:
  219. self.add_snip()
  220. self.add_csinfo(lastitem)
  221. add_sep()
  222. self.csbox.show_all()
  223. # show/hide separators
  224. self.update_seps()
  225. # update status
  226. self.update_status()
  227. # determine doing it now or later
  228. if numshow < ASYNC_LIMIT:
  229. proc()
  230. else:
  231. self.update_status(updating=True)
  232. gtklib.idle_add_single_call(proc)
  233. return True
  234. def clear(self):
  235. """ Clear the item list """
  236. self.csbox.foreach(lambda c: c.parent.remove(c))
  237. self.curitems = None
  238. self.update_status()
  239. def get_items(self, sel=False):
  240. """
  241. Return a list of items or tuples contained 2 values:
  242. 'item' (String) and 'selection state' (Boolean).
  243. If cslist lists no items, it returns an empty list.
  244. sel: If True, it returns a list of tuples. Default: False.
  245. """
  246. items = self.curitems
  247. if items:
  248. if not sel:
  249. return items
  250. return [(item, self.chkmap[item]) for item in items]
  251. return []
  252. def get_list_limit(self):
  253. """ Return number of items to limit to display """
  254. return self.limit
  255. def set_list_limit(self, limit):
  256. """
  257. Set number of items to limit to display.
  258. limit: Integer, must be more than 3. Default: 20.
  259. """
  260. if limit < 3:
  261. limit = 3
  262. self.limit = limit
  263. def get_dnd_enable(self):
  264. """ Return whether drag and drop feature is enabled """
  265. return self.dnd_enable
  266. def set_dnd_enable(self, enable):
  267. """
  268. Set whether drag and drop feature is enabled.
  269. enable: Boolean, if True, drag and drop feature will be enabled.
  270. Default: False.
  271. """
  272. self.dnd_enable = enable
  273. def get_checkbox_enable(self):
  274. """ Return whether the selection feature is enabled """
  275. return self.sel_enable
  276. def set_checkbox_enable(self, enable):
  277. """
  278. Set whether the selection feature is enabled.
  279. When it's enabled, checboxes will be placed at the left of
  280. csinfo widgets.
  281. enable: Boolean, if True, the selection feature will be enabled.
  282. Default: False.
  283. """
  284. self.sel_enable = enable
  285. def get_activatable_enable(self):
  286. """ Return whether items are activatable """
  287. return self.act_enable
  288. def set_activatable_enable(self, enable):
  289. """
  290. Set whether items are activatable.
  291. By enabling this, items in the list will be emitted 'item-activated'
  292. signal when the user double-clicked on the list.
  293. enable: Boolean, if True, items will be selectable. Default: False.
  294. """
  295. self.act_enable = enable
  296. def get_compact_view(self):
  297. """ Return whether the compact view is enabled """
  298. return self.compactopt.get_active()
  299. def set_compact_view(self, compact):
  300. """
  301. Set whether the compact view is enabled.
  302. enable: Boolean, if True, the compact view will be enabled.
  303. Default: False.
  304. """
  305. self.compactopt.set_active(compact)
  306. def has_limit(self):
  307. """
  308. Return whether the item list shows all items.
  309. If the item list has no items, it will return None.
  310. """
  311. if self.curitems:
  312. num = len(self.curitems)
  313. return self.limit < num and len(self.showitems) != num
  314. return None
  315. ### internal functions ###
  316. def after_init(self):
  317. self.statusbox.pack_start(self.allbtn, False, False, 4)
  318. def update_status(self, updating=False):
  319. numshow = numsel = numtotal = all = None
  320. if self.curitems is None:
  321. button = False
  322. text = _('No items to display')
  323. else:
  324. # prepare data
  325. numshow, numtotal = len(self.showitems), len(self.curitems)
  326. data = dict(count=numshow, total=numtotal)
  327. if self.sel_enable:
  328. items = self.get_items(sel=True)
  329. numsel = len([item for item, sel in items if sel])
  330. data['sel'] = numsel
  331. all = data['count'] == data['total']
  332. button = not all
  333. # generate status text
  334. if updating:
  335. text = _('Updating...')
  336. elif self.sel_enable:
  337. if all:
  338. text = _('Selecting %(sel)d of %(total)d, displaying '
  339. 'all items') % data
  340. else:
  341. text = _('Selecting %(sel)d, displaying %(count)d of '
  342. '%(total)d items') % data
  343. else:
  344. if all:
  345. text = _('Displaying all items')
  346. else:
  347. text = _('Displaying %(count)d of %(total)d items') % data
  348. self.statuslabel.set_text(text)
  349. self.allbtn.set_property('visible', button)
  350. self.emit('list-updated', numtotal, numsel, numshow, updating)
  351. def setup_dnd(self, restart=False):
  352. if not restart and self.scroll_timer is None:
  353. self.scroll_timer = gobject.timeout_add(25, self.scroll_timeout)
  354. def teardown_dnd(self, pause=False):
  355. first = self.get_sep(0)
  356. if first:
  357. first.set_visible(False)
  358. last = self.get_sep(-1)
  359. if last:
  360. last.set_visible(False)
  361. if self.hlsep:
  362. self.hlsep.drag_unhighlight()
  363. self.hlsep = None
  364. if not pause and self.scroll_timer:
  365. gobject.source_remove(self.scroll_timer)
  366. self.scroll_timer = None
  367. def get_item_pos(self, y, detail=False):
  368. pos = None
  369. items = self.curitems
  370. num = len(items)
  371. numshow = len(self.showitems)
  372. first = self.itemmap[items[0]]
  373. beforesnip = self.itemmap[items[numshow - 2]]
  374. snip = self.has_limit() and self.itemmap['snip'] or None
  375. last = self.itemmap[items[-1]]
  376. def calc_ratio(geom):
  377. return (y - geom['y']) / float(geom['height'])
  378. if y < first['y']:
  379. start, end = -1, 0
  380. elif last['bottom'] < y:
  381. start, end = num - 1, num
  382. elif snip and beforesnip['bottom'] < y and y < last['y']:
  383. ratio = calc_ratio(snip)
  384. if ratio < 0.5:
  385. start, end = numshow - 2, numshow - 1
  386. else:
  387. start, end = num - 2, num - 1
  388. else:
  389. # calc item showitems pos (binary search)
  390. def mid(start, end):
  391. return (start + end) / 2
  392. start, end = 0, numshow - 1
  393. pos = mid(start, end)
  394. while start < end:
  395. data = self.itemmap[self.showitems[pos]]
  396. if y < data['y']:
  397. end = pos - 1
  398. elif data['bottom'] < y:
  399. start = pos + 1
  400. else:
  401. break
  402. pos = mid(start, end)
  403. # translate to curitems pos
  404. pos = self.trans_to_cur(pos)
  405. # calc detailed pos if need
  406. if detail:
  407. data = self.itemmap[items[pos]]
  408. ratio = calc_ratio(data)
  409. if ratio < 0.5:
  410. start, end = pos - 1, pos
  411. else:
  412. start, end = pos, pos + 1
  413. if detail:
  414. return pos, start, end
  415. return pos
  416. def get_sep(self, pos):
  417. """
  418. pos: Number, the position of separator you need.
  419. If -1 or list length, indicates the last separator.
  420. """
  421. # invalid position/condition
  422. if pos < -1 or not self.showitems:
  423. return None
  424. def get_last():
  425. child = self.csbox.get_children()[-1]
  426. return isinstance(child, FixedHSeparator) and child or None
  427. # last separator
  428. if pos == -1:
  429. return get_last()
  430. # limiting case
  431. if self.has_limit():
  432. # snip box separator
  433. if pos == self.limit - 1:
  434. return self.itemmap['snip']['sep']
  435. # list length (+ snip box)
  436. if pos == self.limit + 1:
  437. return get_last()
  438. # separators after snip box
  439. if self.limit - 1 < pos:
  440. return self.itemmap[self.showitems[pos-1]]['sep']
  441. # list length
  442. elif pos == len(self.showitems):
  443. return get_last()
  444. # others
  445. return self.itemmap[self.showitems[pos]]['sep']
  446. def get_sep_by_y(self, y):
  447. pos, start, end = self.get_item_pos(y, detail=True)
  448. if self.has_limit() and self.limit - 1 < end:
  449. end -= len(self.curitems) - self.limit - 1
  450. return self.get_sep(end)
  451. def update_seps(self):
  452. """ Update visibility of all separators """
  453. compact = self.get_compact_view()
  454. for item in self.showitems[1:]:
  455. sep = self.itemmap[item]['sep']
  456. sep.set_visible(not compact)
  457. if self.has_limit():
  458. self.itemmap['snip']['sep'].set_visible(False)
  459. self.itemmap[self.showitems[-1]]['sep'].set_visible(False)
  460. self.get_sep(0).set_visible(False)
  461. self.get_sep(-1).set_visible(False)
  462. def expand_items(self):
  463. if not self.has_limit():
  464. return
  465. # fix up snipped items
  466. rest = self.curitems[self.limit - 1:-1]
  467. def proc():
  468. # insert snipped csinfo
  469. for pos, item in enumerate(rest):
  470. self.insert_csinfo(item, self.limit + pos)
  471. # remove snip
  472. self.remove_snip()
  473. self.showitems = self.curitems[:]
  474. self.update_seps()
  475. self.update_status()
  476. # determine doing it now or later
  477. if len(rest) < ASYNC_LIMIT:
  478. proc()
  479. else:
  480. self.update_status(updating=True)
  481. gtklib.idle_add_single_call(proc)
  482. def reorder_item(self, pos, insert):
  483. """
  484. pos: Number, the position of item to move. This must be curitems
  485. index, not showitems index.
  486. insert: Number, the new position to insert target item.
  487. If list length, indicates the end of the list.
  488. This must be curitems index, not showitems index.
  489. """
  490. # reject unneeded reordering
  491. if pos == insert or pos + 1 == insert:
  492. return
  493. # reorder target csinfo
  494. if self.has_limit() and self.limit - 1 <= pos:
  495. item = self.curitems[pos]
  496. if insert < self.limit - 1:
  497. # move target csinfo to insert pos
  498. target = self.itemmap[item]['widget']
  499. self.csbox.reorder_child(target, insert)
  500. # remove csinfo to be snipped
  501. item = self.showitems[-2]
  502. self.remove_csinfo(item)
  503. else:
  504. # remove target csinfo
  505. self.remove_csinfo(item)
  506. # insert csinfo the end of the item list
  507. item = self.curitems[-2]
  508. self.insert_csinfo(item, -1)
  509. elif self.has_limit() and self.limit - 1 < insert:
  510. if self.trans_to_show(insert) < self.limit:
  511. # remove target csinfo
  512. rm_item = self.curitems[pos]
  513. else:
  514. # move target csinfo to the end of VBox
  515. item = self.curitems[pos]
  516. target = self.itemmap[item]['widget']
  517. numc = len(self.csbox.get_children())
  518. self.csbox.reorder_child(target, numc - 2)
  519. # remove last csinfo
  520. rm_item = self.showitems[-1]
  521. # remove it
  522. self.remove_csinfo(rm_item)
  523. # insert csinfo before snip box
  524. item = self.curitems[self.limit - 1]
  525. self.insert_csinfo(item, self.limit - 1)
  526. else:
  527. info = self.itemmap[self.showitems[pos]]['widget']
  528. if insert < pos:
  529. self.csbox.reorder_child(info, insert)
  530. else:
  531. self.csbox.reorder_child(info, insert - 1)
  532. # reorder curitems
  533. item = self.curitems[pos]
  534. items = self.curitems[:pos] + self.curitems[pos+1:]
  535. if insert < pos:
  536. items.insert(insert, item)
  537. else:
  538. items.insert(insert - 1, item)
  539. self.curitems = items
  540. # reorder showitems
  541. if self.has_limit():
  542. self.showitems = items[:self.limit-1] + [items[-1]]
  543. else:
  544. self.showitems = items
  545. # show/hide separators
  546. self.update_seps()
  547. # just emit 'list-updated' signal
  548. self.update_status()
  549. def trans_to_show(self, index):
  550. """ Translate from curitems index to showitems index """
  551. numrest = len(self.curitems) - self.limit
  552. if self.has_limit() and numrest <= index:
  553. return index - numrest
  554. return index
  555. def trans_to_cur(self, index):
  556. """ Translate from showitems index to curitems index """
  557. if self.has_limit() and self.limit - 1 <= index:
  558. return index + len(self.curitems) - self.limit
  559. return index
  560. def create_sep(self):
  561. return FixedHSeparator()
  562. def add_csinfo(self, item):
  563. self.insert_csinfo(item, -1)
  564. def insert_csinfo(self, item, pos):
  565. """
  566. item: String, revision number or patch file path to display.
  567. pos: Number, an index of insertion point. If -1, indicates
  568. the end of the item list.
  569. """
  570. # create csinfo
  571. wrapbox = gtk.VBox()
  572. sep = self.create_sep()
  573. wrapbox.pack_start(sep, False, False)
  574. style = self.get_compact_view() and self.lstyle or self.pstyle
  575. if self.dnd_enable:
  576. style['selectable'] = False
  577. info = self.curfactory(item, style)
  578. if self.sel_enable:
  579. check = gtk.CheckButton()
  580. check.set_active(self.chkmap[item])
  581. check.connect('toggled', self.check_toggled, item)
  582. align = gtk.Alignment(0.5, 0)
  583. align.add(check)
  584. hbox = gtk.HBox()
  585. hbox.pack_start(align, False, False)
  586. hbox.pack_start(info, False, False)
  587. info = hbox
  588. wrapbox.pack_start(info, False, False)
  589. wrapbox.show_all()
  590. self.csbox.pack_start(wrapbox, False, False)
  591. self.itemmap[item] = {'widget': wrapbox,
  592. 'info': info,
  593. 'sep': sep}
  594. # reorder it
  595. children = self.csbox.get_children()
  596. if 1 < len(children) and isinstance(children[-2], FixedHSeparator):
  597. if pos == -1:
  598. numc = len(children)
  599. pos = numc - 2
  600. elif self.has_limit():
  601. pos = pos - 1
  602. self.csbox.reorder_child(wrapbox, pos)
  603. def remove_csinfo(self, item):
  604. info = self.itemmap[item]['widget']
  605. self.csbox.remove(info)
  606. del self.itemmap[item]
  607. def add_snip(self):
  608. wrapbox = gtk.VBox()
  609. sep = self.create_sep()
  610. wrapbox.pack_start(sep, False, False)
  611. snipbox = gtk.HBox()
  612. wrapbox.pack_start(snipbox, False, False)
  613. spacer = gtk.Label()
  614. snipbox.pack_start(spacer, False, False)
  615. spacer.set_width_chars(24)
  616. sniplbl = gtk.Label()
  617. snipbox.pack_start(sniplbl, False, False)
  618. sniplbl.set_markup('<span size="large" weight="heavy"'
  619. ' font_family="monospace">...</span>')
  620. sniplbl.set_angle(90)
  621. snipbox.pack_start(gtk.Label())
  622. self.csbox.pack_start(wrapbox, False, False, 2)
  623. self.itemmap['snip'] = {'widget': wrapbox,
  624. 'snip': snipbox,
  625. 'sep': sep}
  626. def remove_snip(self):
  627. if not self.has_limit():
  628. return
  629. snip = self.itemmap['snip']['widget']
  630. self.csbox.remove(snip)
  631. del self.itemmap['snip']
  632. ### signal handlers ###
  633. def check_toggled(self, button, item):
  634. self.chkmap[item] = button.get_active()
  635. self.update_status()
  636. def allbtn_clicked(self, button):
  637. self.update(self.curitems, self.currepo, limit=False,
  638. queue=False, keep=True)
  639. ### dnd signal handlers ###
  640. def dnd_begin(self, widget, context):
  641. self.setup_dnd()
  642. context.set_icon_pixbuf(self.dnd_pb, 0, 0)
  643. def dnd_end(self, widget, context):
  644. self.teardown_dnd()
  645. def dnd_motion(self, widget, context, x, y, event_time):
  646. if hasattr(self, 'item_drag') and self.item_drag is not None:
  647. num = len(self.curitems)
  648. if not self.hlsep:
  649. self.setup_dnd(restart=True)
  650. # highlight separator
  651. sep = self.get_sep_by_y(y)
  652. first = self.get_sep(0)
  653. first.set_visible(first == sep)
  654. last = self.get_sep(-1)
  655. last.set_visible(last == sep)
  656. if self.hlsep != sep:
  657. if self.hlsep:
  658. self.hlsep.drag_unhighlight()
  659. sep.drag_highlight()
  660. self.hlsep = sep
  661. def dnd_leave(self, widget, context, event_time):
  662. self.teardown_dnd(pause=True)
  663. def dnd_received(self, widget, context, x, y, sel, target_type, *args):
  664. if target_type == CSL_DND_ITEM:
  665. items = self.curitems
  666. pos, start, end = self.get_item_pos(y, detail=True)
  667. self.reorder_item(self.item_drag, end)
  668. elif target_type == CSL_DND_URI_LIST:
  669. paths = gtklib.normalize_dnd_paths(sel.data)
  670. if paths:
  671. self.emit('files-dropped', paths, sel.data)
  672. def dnd_get(self, widget, context, sel, target_type, event_time):
  673. pos = self.item_drag
  674. if target_type == CSL_DND_ITEM and pos is not None:
  675. sel.set(sel.target, 8, str(self.curitems[pos]))
  676. def button_press(self, widget, event):
  677. if not self.curitems:
  678. return
  679. # gather geometry data
  680. items = self.showitems
  681. if self.has_limit():
  682. items.append('snip')
  683. for item in items:
  684. data = self.itemmap[item]
  685. alloc = data['widget'].allocation
  686. data.update(y=alloc.y, height=alloc.height)
  687. data['bottom'] = alloc.y + alloc.height
  688. # get pressed csinfo widget based on pointer position
  689. pos = self.get_item_pos(event.y)
  690. if pos is None:
  691. return
  692. # emit activated signal
  693. if self.act_enable and event.type == gtk.gdk._2BUTTON_PRESS:
  694. item = self.curitems[pos]
  695. self.emit('item-activated', item, self.itemmap[item]['widget'])
  696. # dnd setup
  697. if self.dnd_enable and event.type == gtk.gdk.BUTTON_PRESS \
  698. and 1 < len(self.curitems):
  699. # prepare for dnd auto-scrolling
  700. W = 20
  701. alloc = self.scroll.child.allocation
  702. self.areas = {}
  703. def add(name, arg):
  704. region = gtk.gdk.region_rectangle(arg)
  705. self.areas[name] = (region, gtk.gdk.Rectangle(*arg))
  706. add('top', (0, 0, alloc.width, W))
  707. add('right', (alloc.width - W, 0, W, alloc.height))
  708. add('bottom', (0, alloc.height - W, alloc.width, W))
  709. add('left', (0, 0, W, alloc.height))
  710. add('center', (W, W, alloc.width - 2 * W, alloc.height - 2 * W))
  711. # start dnd
  712. self.item_drag = pos
  713. self.csevent.drag_begin(self.dnd_targets,
  714. gtk.gdk.ACTION_MOVE, 1, event)
  715. def scroll_timeout(self):
  716. x, y = self.scroll.get_pointer()
  717. if not self.areas['center'][0].point_in(x, y):
  718. def hscroll(left=False, fast=False):
  719. amount = 2
  720. if left: amount *= -1
  721. if fast: amount *= 3
  722. hadj = self.scroll.get_hadjustment()
  723. hadj.set_value(hadj.get_value() + amount)
  724. def vscroll(up=False, fast=False):
  725. amount = 2
  726. if up: amount *= -1
  727. if fast: amount *= 3
  728. vadj = self.scroll.get_vadjustment()
  729. vadj.set_value(vadj.get_value() + amount)
  730. top, topr = self.areas['top']
  731. bottom, bottomr = self.areas['bottom']
  732. if y < topr.y:
  733. vscroll(up=True, fast=True)
  734. elif top.point_in(x, y):
  735. vscroll(up=True)
  736. elif (bottomr.y + bottomr.height) < y:
  737. vscroll(fast=True)
  738. elif bottom.point_in(x, y):
  739. vscroll()
  740. left, leftr = self.areas['left']
  741. right, rightr = self.areas['right']
  742. if x < leftr.x:
  743. hscroll(left=True, fast=True)
  744. elif left.point_in(x, y):
  745. hscroll(left=True)
  746. elif (rightr.x + rightr.width) < x:
  747. hscroll(fast=True)
  748. elif right.point_in(x, y):
  749. hscroll()
  750. return True # repeat
  751. class FixedHSeparator(gtk.VBox):
  752. def __init__(self, visible=True):
  753. gtk.VBox.__init__(self)
  754. self.set_size_request(-1, 2)
  755. self.visible = visible
  756. self.sep = gtk.HSeparator()
  757. self.pack_start(self.sep, False, False)
  758. self.sep.set_no_show_all(not visible)
  759. def set_visible(self, visible):
  760. if self.visible != visible:
  761. self.visible = visible
  762. self.sep.set_no_show_all(False)
  763. self.sep.set_property('visible', visible)
  764. self.sep.set_no_show_all(not visible)