3
This module contains the code to manage the branch information window,
4
which contains both the revision graph and details panes.
7
__copyright__ = "Copyright (c) 2005 Canonical Ltd."
8
__author__ = "Scott James Remnant <scott@ubuntu.com>"
11
from gi.repository import Gdk
12
from gi.repository import Gtk
14
from bzrlib.plugins.gtk import icon_path
15
from bzrlib.plugins.gtk.branchview import TreeView
16
from bzrlib.plugins.gtk.preferences import PreferencesWindow
17
from bzrlib.plugins.gtk.revisionmenu import RevisionMenu
18
from bzrlib.plugins.gtk.window import Window
20
from bzrlib.config import GlobalConfig
21
from bzrlib.revision import NULL_REVISION
22
from bzrlib.trace import mutter
25
class BranchWindow(Window):
28
This object represents and manages a single window containing information
29
for a particular branch.
32
def __init__(self, branch, start_revs, maxnum, parent=None):
33
"""Create a new BranchWindow.
35
:param branch: Branch object for branch to show.
36
:param start_revs: Revision ids of top revisions.
37
:param maxnum: Maximum number of revisions to display,
41
super(BranchWindow, self).__init__(parent=parent)
42
self.set_border_width(0)
45
self.start_revs = start_revs
47
self.config = GlobalConfig()
49
if self.config.get_user_option('viz-compact-view') == 'yes':
50
self.compact_view = True
52
self.compact_view = False
54
self.set_title(branch._get_nick(local=True) + " - revision history")
56
# user-configured window size
57
size = self._load_size('viz-window-size')
61
# Use three-quarters of the screen by default
62
screen = self.get_screen()
63
monitor = screen.get_monitor_geometry(0)
64
width = int(monitor.width * 0.75)
65
height = int(monitor.height * 0.75)
66
self.set_default_size(width, height)
67
self.set_size_request(width/3, height/3)
68
self._save_size_on_destroy(self, 'viz-window-size')
71
icon = self.render_icon_pixbuf(Gtk.STOCK_INDEX, Gtk.IconSize.BUTTON)
74
Gtk.AccelMap.add_entry("<viz>/Go/Next Revision", Gdk.KEY_Up, Gdk.ModifierType.MOD1_MASK)
75
Gtk.AccelMap.add_entry("<viz>/Go/Previous Revision", Gdk.KEY_Down, Gdk.ModifierType.MOD1_MASK)
76
Gtk.AccelMap.add_entry("<viz>/View/Refresh", Gdk.KEY_F5, 0)
78
self.accel_group = Gtk.AccelGroup()
79
self.add_accel_group(self.accel_group)
81
self.prev_rev_action = Gtk.Action("prev-rev", "_Previous Revision", "Go to the previous revision", Gtk.STOCK_GO_DOWN)
82
self.prev_rev_action.set_accel_path("<viz>/Go/Previous Revision")
83
self.prev_rev_action.set_accel_group(self.accel_group)
84
self.prev_rev_action.connect("activate", self._back_clicked_cb)
85
self.prev_rev_action.connect_accelerator()
87
self.next_rev_action = Gtk.Action("next-rev", "_Next Revision", "Go to the next revision", Gtk.STOCK_GO_UP)
88
self.next_rev_action.set_accel_path("<viz>/Go/Next Revision")
89
self.next_rev_action.set_accel_group(self.accel_group)
90
self.next_rev_action.connect("activate", self._fwd_clicked_cb)
91
self.next_rev_action.connect_accelerator()
93
self.refresh_action = Gtk.Action("refresh", "_Refresh", "Refresh view", Gtk.STOCK_REFRESH)
94
self.refresh_action.set_accel_path("<viz>/View/Refresh")
95
self.refresh_action.set_accel_group(self.accel_group)
96
self.refresh_action.connect("activate", self._refresh_clicked)
97
self.refresh_action.connect_accelerator()
99
self.vbox = self.construct()
101
def _save_size_on_destroy(self, widget, config_name):
102
"""Creates a hook that saves the size of widget to config option
103
config_name when the window is destroyed/closed."""
105
allocation = widget.get_allocation()
106
width, height = allocation.width, allocation.height
107
value = '%sx%s' % (width, height)
108
self.config.set_user_option(config_name, value)
109
self.connect("destroy", save_size)
111
def set_revision(self, revid):
112
self.treeview.set_revision_id(revid)
115
"""Construct the window contents."""
116
vbox = Gtk.VBox(spacing=0)
119
# order is important here
120
paned = self.construct_paned()
121
nav = self.construct_navigation()
122
menubar = self.construct_menubar()
124
vbox.pack_start(menubar, False, True, 0)
125
vbox.pack_start(nav, False, True, 0)
126
vbox.pack_start(paned, True, True, 0)
127
vbox.set_focus_child(paned)
134
def construct_paned(self):
135
"""Construct the main HPaned/VPaned contents."""
136
if self.config.get_user_option('viz-vertical') == 'True':
137
self.paned = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL)
139
self.paned = Gtk.Paned.new(Gtk.Orientation.VERTICAL)
141
self.paned.pack1(self.construct_top(), resize=False, shrink=True)
142
self.paned.pack2(self.construct_bottom(), resize=True, shrink=False)
147
def construct_menubar(self):
148
menubar = Gtk.MenuBar()
150
file_menu = Gtk.Menu()
151
file_menuitem = Gtk.MenuItem.new_with_mnemonic("_File")
152
file_menuitem.set_submenu(file_menu)
154
file_menu_close = Gtk.ImageMenuItem.new_from_stock(
155
Gtk.STOCK_CLOSE, self.accel_group)
156
file_menu_close.connect('activate', lambda x: self.destroy())
158
file_menu_quit = Gtk.ImageMenuItem.new_from_stock(
159
Gtk.STOCK_QUIT, self.accel_group)
160
file_menu_quit.connect('activate', lambda x: Gtk.main_quit())
162
if self._parent is not None:
163
file_menu.add(file_menu_close)
164
file_menu.add(file_menu_quit)
166
edit_menu = Gtk.Menu()
167
edit_menuitem = Gtk.MenuItem.new_with_mnemonic("_Edit")
168
edit_menuitem.set_submenu(edit_menu)
170
edit_menu_branchopts = Gtk.MenuItem(label="Branch Settings")
171
edit_menu_branchopts.connect('activate', lambda x: PreferencesWindow(self.branch.get_config()).show())
173
edit_menu_globopts = Gtk.MenuItem(label="Global Settings")
174
edit_menu_globopts.connect('activate', lambda x: PreferencesWindow().show())
176
edit_menu.add(edit_menu_branchopts)
177
edit_menu.add(edit_menu_globopts)
179
view_menu = Gtk.Menu()
180
view_menuitem = Gtk.MenuItem.new_with_mnemonic("_View")
181
view_menuitem.set_submenu(view_menu)
183
view_menu_refresh = self.refresh_action.create_menu_item()
184
view_menu_refresh.connect('activate', self._refresh_clicked)
186
view_menu.add(view_menu_refresh)
187
view_menu.add(Gtk.SeparatorMenuItem())
189
view_menu_toolbar = Gtk.CheckMenuItem(label="Show Toolbar")
190
view_menu_toolbar.set_active(True)
191
if self.config.get_user_option('viz-toolbar-visible') == 'False':
192
view_menu_toolbar.set_active(False)
194
view_menu_toolbar.connect('toggled', self._toolbar_visibility_changed)
196
view_menu_compact = Gtk.CheckMenuItem(label="Show Compact Graph")
197
view_menu_compact.set_active(self.compact_view)
198
view_menu_compact.connect('activate', self._brokenlines_toggled_cb)
200
view_menu_vertical = Gtk.CheckMenuItem(label="Side-by-side Layout")
201
view_menu_vertical.set_active(False)
202
if self.config.get_user_option('viz-vertical') == 'True':
203
view_menu_vertical.set_active(True)
204
view_menu_vertical.connect('toggled', self._vertical_layout)
206
view_menu_diffs = Gtk.CheckMenuItem(label="Show Diffs")
207
view_menu_diffs.set_active(False)
208
if self.config.get_user_option('viz-show-diffs') == 'True':
209
view_menu_diffs.set_active(True)
210
view_menu_diffs.connect('toggled', self._diff_visibility_changed)
212
view_menu_wide_diffs = Gtk.CheckMenuItem(label="Wide Diffs")
213
view_menu_wide_diffs.set_active(False)
214
if self.config.get_user_option('viz-wide-diffs') == 'True':
215
view_menu_wide_diffs.set_active(True)
216
view_menu_wide_diffs.connect('toggled', self._diff_placement_changed)
218
view_menu_wrap_diffs = Gtk.CheckMenuItem.new_with_mnemonic(
219
"Wrap _Long Lines in Diffs")
220
view_menu_wrap_diffs.set_active(False)
221
if self.config.get_user_option('viz-wrap-diffs') == 'True':
222
view_menu_wrap_diffs.set_active(True)
223
view_menu_wrap_diffs.connect('toggled', self._diff_wrap_changed)
225
view_menu.add(view_menu_toolbar)
226
view_menu.add(view_menu_compact)
227
view_menu.add(view_menu_vertical)
228
view_menu.add(Gtk.SeparatorMenuItem())
229
view_menu.add(view_menu_diffs)
230
view_menu.add(view_menu_wide_diffs)
231
view_menu.add(view_menu_wrap_diffs)
232
view_menu.add(Gtk.SeparatorMenuItem())
234
self.mnu_show_revno_column = Gtk.CheckMenuItem.new_with_mnemonic(
235
"Show Revision _Number Column")
236
self.mnu_show_date_column = Gtk.CheckMenuItem.new_with_mnemonic(
239
# Revision numbers are pointless if there are multiple branches
240
if len(self.start_revs) > 1:
241
self.mnu_show_revno_column.set_sensitive(False)
242
self.treeview.set_property('revno-column-visible', False)
244
for (col, name) in [(self.mnu_show_revno_column, "revno"),
245
(self.mnu_show_date_column, "date")]:
246
col.set_active(self.treeview.get_property(name + "-column-visible"))
247
col.connect('toggled', self._col_visibility_changed, name)
251
go_menu.set_accel_group(self.accel_group)
252
go_menuitem = Gtk.MenuItem.new_with_mnemonic("_Go")
253
go_menuitem.set_submenu(go_menu)
255
go_menu_next = self.next_rev_action.create_menu_item()
256
go_menu_prev = self.prev_rev_action.create_menu_item()
258
tag_image = Gtk.Image()
259
tag_image.set_from_file(icon_path("tag-16.png"))
260
self.go_menu_tags = Gtk.ImageMenuItem.new_with_mnemonic("_Tags")
261
self.go_menu_tags.set_image(tag_image)
262
self.treeview.connect('refreshed', lambda w: self._update_tags())
264
go_menu.add(go_menu_next)
265
go_menu.add(go_menu_prev)
266
go_menu.add(Gtk.SeparatorMenuItem())
267
go_menu.add(self.go_menu_tags)
269
self.revision_menu = RevisionMenu(self.branch.repository, [],
270
self.branch, parent=self)
271
revision_menuitem = Gtk.MenuItem.new_with_mnemonic("_Revision")
272
revision_menuitem.set_submenu(self.revision_menu)
274
branch_menu = Gtk.Menu()
275
branch_menuitem = Gtk.MenuItem.new_with_mnemonic("_Branch")
276
branch_menuitem.set_submenu(branch_menu)
278
branch_menu.add(Gtk.MenuItem.new_with_mnemonic("Pu_ll Revisions"))
279
branch_menu.add(Gtk.MenuItem.new_with_mnemonic("Pu_sh Revisions"))
282
from bzrlib.plugins import search
284
mutter("Didn't find search plugin")
286
branch_menu.add(Gtk.SeparatorMenuItem())
288
branch_index_menuitem = Gtk.MenuItem.new_with_mnemonic("_Index")
289
branch_index_menuitem.connect('activate', self._branch_index_cb)
290
branch_menu.add(branch_index_menuitem)
292
branch_search_menuitem = Gtk.MenuItem.new_with_mnemonic("_Search")
293
branch_search_menuitem.connect('activate', self._branch_search_cb)
294
branch_menu.add(branch_search_menuitem)
296
help_menu = Gtk.Menu()
297
help_menuitem = Gtk.MenuItem.new_with_mnemonic("_Help")
298
help_menuitem.set_submenu(help_menu)
300
help_about_menuitem = Gtk.ImageMenuItem.new_from_stock(
301
Gtk.STOCK_ABOUT, self.accel_group)
302
help_about_menuitem.connect('activate', self._about_dialog_cb)
304
help_menu.add(help_about_menuitem)
306
menubar.add(file_menuitem)
307
menubar.add(edit_menuitem)
308
menubar.add(view_menuitem)
309
menubar.add(go_menuitem)
310
menubar.add(revision_menuitem)
311
menubar.add(branch_menuitem)
312
menubar.add(help_menuitem)
317
def construct_top(self):
318
"""Construct the top-half of the window."""
319
# FIXME: Make broken_line_length configurable
321
self.treeview = TreeView(self.branch, self.start_revs, self.maxnum,
324
for col in ["revno", "date"]:
325
option = self.config.get_user_option(col + '-column-visible')
326
if option is not None:
327
self.treeview.set_property(col + '-column-visible',
330
self.treeview.set_property(col + '-column-visible', False)
334
align = Gtk.Alignment.new(0.0, 0.0, 1.0, 1.0)
335
align.set_padding(5, 0, 0, 0)
336
align.add(self.treeview)
337
# user-configured size
338
size = self._load_size('viz-graph-size')
339
self._save_size_on_destroy(align, 'viz-graph-size')
344
def construct_navigation(self):
345
"""Construct the navigation buttons."""
346
self.toolbar = Gtk.Toolbar()
347
self.toolbar.set_style(Gtk.ToolbarStyle.BOTH_HORIZ)
349
self.prev_button = self.prev_rev_action.create_tool_item()
350
self.toolbar.insert(self.prev_button, -1)
352
self.next_button = self.next_rev_action.create_tool_item()
353
self.toolbar.insert(self.next_button, -1)
355
self.toolbar.insert(Gtk.SeparatorToolItem(), -1)
357
refresh_button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_REFRESH)
358
refresh_button.connect('clicked', self._refresh_clicked)
359
self.toolbar.insert(refresh_button, -1)
361
self.toolbar.show_all()
365
def construct_bottom(self):
366
"""Construct the bottom half of the window."""
367
if self.config.get_user_option('viz-wide-diffs') == 'True':
368
self.diff_paned = Gtk.Paned.new(Gtk.Orientation.VERTICAL)
370
self.diff_paned = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL)
371
(width, height) = self.get_size()
372
self.diff_paned.set_size_request(20, 20) # shrinkable
374
from bzrlib.plugins.gtk.revisionview import RevisionView
375
self.revisionview = RevisionView(branch=self.branch)
376
self.revisionview.set_size_request(width/3, int(height / 2.5))
377
# user-configured size
378
size = self._load_size('viz-revisionview-size')
381
self.revisionview.set_size_request(width, height)
382
self._save_size_on_destroy(self.revisionview, 'viz-revisionview-size')
383
self.revisionview.show()
384
self.revisionview.set_show_callback(self._show_clicked_cb)
385
self.revisionview.connect('notify::revision', self._go_clicked_cb)
386
self.treeview.connect('tag-added',
387
lambda w, t, r: self.revisionview.update_tags())
388
self.treeview.connect('revision-selected',
389
self._treeselection_changed_cb)
390
self.treeview.connect('revision-activated',
391
self._tree_revision_activated)
392
self.diff_paned.pack1(self.revisionview)
394
from bzrlib.plugins.gtk.diff import DiffWidget
395
self.diff = DiffWidget()
396
self.diff_paned.pack2(self.diff)
398
self.diff_paned.show_all()
399
if self.config.get_user_option('viz-show-diffs') != 'True':
402
return self.diff_paned
404
def _tag_selected_cb(self, menuitem, revid):
405
self.treeview.set_revision_id(revid)
407
def _treeselection_changed_cb(self, selection, *args):
408
"""callback for when the treeview changes."""
409
revision = self.treeview.get_revision()
410
parents = self.treeview.get_parents()
411
children = self.treeview.get_children()
413
if revision and revision.revision_id != NULL_REVISION:
414
self.revision_menu.set_revision_ids([revision.revision_id])
415
prev_menu = Gtk.Menu()
417
self.prev_rev_action.set_sensitive(True)
418
for parent_id in parents:
419
if parent_id and parent_id != NULL_REVISION:
420
parent = self.branch.repository.get_revision(parent_id)
422
str = ' (%s)' % parent.properties['branch-nick']
427
label=parent.message.split("\n")[0] + str)
428
item.connect('activate', self._set_revision_cb, parent_id)
432
self.prev_rev_action.set_sensitive(False)
435
if getattr(self.prev_button, 'set_menu', None) is not None:
436
self.prev_button.set_menu(prev_menu)
438
next_menu = Gtk.Menu()
439
if len(children) > 0:
440
self.next_rev_action.set_sensitive(True)
441
for child_id in children:
442
child = self.branch.repository.get_revision(child_id)
444
str = ' (%s)' % child.properties['branch-nick']
449
label=child.message.split("\n")[0] + str)
450
item.connect('activate', self._set_revision_cb, child_id)
454
self.next_rev_action.set_sensitive(False)
457
if getattr(self.next_button, 'set_menu', None) is not None:
458
self.next_button.set_menu(next_menu)
460
self.revisionview.set_revision(revision)
461
self.revisionview.set_children(children)
462
self.update_diff_panel(revision, parents)
464
def _tree_revision_activated(self, widget, path, col):
465
# TODO: more than one parent
466
"""Callback for when a treeview row gets activated."""
467
revision = self.treeview.get_revision()
468
parents = self.treeview.get_parents()
470
if len(parents) == 0:
471
parent_id = NULL_REVISION
473
parent_id = parents[0]
475
if revision is not None:
476
self.show_diff(revision.revision_id, parent_id)
478
self.show_diff(NULL_REVISION)
479
self.treeview.grab_focus()
481
def _back_clicked_cb(self, *args):
482
"""Callback for when the back button is clicked."""
485
def _fwd_clicked_cb(self, *args):
486
"""Callback for when the forward button is clicked."""
487
self.treeview.forward()
489
def _go_clicked_cb(self, w, p):
490
"""Callback for when the go button for a parent is clicked."""
491
if self.revisionview.get_revision() is not None:
492
self.treeview.set_revision(self.revisionview.get_revision())
494
def _show_clicked_cb(self, revid, parentid):
495
"""Callback for when the show button for a parent is clicked."""
496
self.show_diff(revid, parentid)
497
self.treeview.grab_focus()
499
def _set_revision_cb(self, w, revision_id):
500
self.treeview.set_revision_id(revision_id)
502
def _brokenlines_toggled_cb(self, button):
503
self.compact_view = button.get_active()
505
if self.compact_view:
510
self.config.set_user_option('viz-compact-view', option)
511
self.treeview.set_property('compact', self.compact_view)
512
self.treeview.refresh()
514
def _branch_index_cb(self, w):
515
from bzrlib.plugins.search import index as _mod_index
516
_mod_index.index_url(self.branch.base)
518
def _branch_search_cb(self, w):
519
from bzrlib.plugins.search import (
521
errors as search_errors,
523
from bzrlib.plugins.gtk.search import SearchDialog
526
index = _mod_index.open_index_url(self.branch.base)
527
except search_errors.NoSearchIndex:
528
dialog = Gtk.MessageDialog(self, type=Gtk.MessageType.QUESTION,
529
buttons=Gtk.ButtonsType.OK_CANCEL,
530
message_format="This branch has not been indexed yet. "
532
if dialog.run() == Gtk.ResponseType.OK:
534
index = _mod_index.index_url(self.branch.base)
539
dialog = SearchDialog(index)
541
if dialog.run() == Gtk.ResponseType.OK:
542
revid = dialog.get_revision()
543
if revid is not None:
544
self.set_revision(revid)
548
def _about_dialog_cb(self, w):
549
from bzrlib.plugins.gtk.about import AboutDialog
552
def _col_visibility_changed(self, col, property):
553
self.config.set_user_option(property + '-column-visible', col.get_active())
554
self.treeview.set_property(property + '-column-visible', col.get_active())
556
def _toolbar_visibility_changed(self, col):
561
self.config.set_user_option('viz-toolbar-visible', col.get_active())
563
def _vertical_layout(self, col):
564
"""Toggle the layout vertical/horizontal"""
565
self.config.set_user_option('viz-vertical', str(col.get_active()))
568
self.vbox.remove(old)
569
self.vbox.pack_start(
570
self.construct_paned(), True, True, 0)
571
self._make_diff_paned_nonzero_size()
572
self._make_diff_nonzero_size()
574
self.treeview.emit('revision-selected')
576
def _make_diff_paned_nonzero_size(self):
577
"""make sure the diff/revision pane isn't zero-width or zero-height"""
578
alloc = self.diff_paned.get_allocation()
579
if (alloc.width < 10) or (alloc.height < 10):
580
width, height = self.get_size()
581
self.diff_paned.set_size_request(width/3, int(height / 2.5))
583
def _make_diff_nonzero_size(self):
584
"""make sure the diff isn't zero-width or zero-height"""
585
alloc = self.diff.get_allocation()
586
if (alloc.width < 10) or (alloc.height < 10):
587
width, height = self.get_size()
588
self.revisionview.set_size_request(width/3, int(height / 2.5))
590
def _diff_visibility_changed(self, col):
591
"""Hide or show the diff panel."""
594
self._make_diff_nonzero_size()
597
self.config.set_user_option('viz-show-diffs', str(col.get_active()))
598
self.update_diff_panel()
600
def _diff_placement_changed(self, col):
601
"""Toggle the diff panel's position."""
602
self.config.set_user_option('viz-wide-diffs', str(col.get_active()))
604
old = self.paned.get_child2()
605
self.paned.remove(old)
606
self.paned.pack2(self.construct_bottom(), resize=True, shrink=False)
607
self._make_diff_nonzero_size()
609
self.treeview.emit('revision-selected')
611
def _diff_wrap_changed(self, widget):
612
"""Toggle word wrap in the diff widget."""
613
self.config.set_user_option('viz-wrap-diffs', widget.get_active())
614
self.diff._on_wraplines_toggled(widget)
616
def _refresh_clicked(self, w):
617
self.treeview.refresh()
619
def _update_tags(self):
622
if self.branch.supports_tags():
623
tags = self.branch.tags.get_tag_dict().items()
624
tags.sort(reverse=True)
625
for tag, revid in tags:
626
tag_image = Gtk.Image()
627
tag_image.set_from_file(icon_path('tag-16.png'))
628
tag_item = Gtk.ImageMenuItem.new_with_mnemonic(
629
tag.replace('_', '__'))
630
tag_item.set_image(tag_image)
631
tag_item.connect('activate', self._tag_selected_cb, revid)
632
tag_item.set_sensitive(self.treeview.has_revision_id(revid))
634
self.go_menu_tags.set_submenu(menu)
636
self.go_menu_tags.set_sensitive(len(tags) != 0)
638
self.go_menu_tags.set_sensitive(False)
640
self.go_menu_tags.show_all()
642
def _load_size(self, name):
643
"""Read and parse 'name' from self.config.
644
The value is a string, formatted as WIDTHxHEIGHT
645
Returns None, or (width, height)
647
size = self.config.get_user_option(name)
649
width, height = [int(num) for num in size.split('x')]
650
# avoid writing config every time we start
654
def show_diff(self, revid, parentid=NULL_REVISION):
655
"""Open a new window to show a diff between the given revisions."""
656
from bzrlib.plugins.gtk.diff import DiffWindow
657
window = DiffWindow(parent=self)
659
rev_tree = self.branch.repository.revision_tree(revid)
660
parent_tree = self.branch.repository.revision_tree(parentid)
662
description = revid + " - " + self.branch._get_nick(local=True)
663
window.set_diff(description, rev_tree, parent_tree)
666
def update_diff_panel(self, revision=None, parents=None):
667
"""Show the current revision in the diff panel."""
668
if self.config.get_user_option('viz-show-diffs') != 'True':
671
if not revision: # default to selected row
672
revision = self.treeview.get_revision()
673
if revision == NULL_REVISION:
676
if not parents: # default to selected row's parents
677
parents = self.treeview.get_parents()
678
if len(parents) == 0:
679
parent_id = NULL_REVISION
681
parent_id = parents[0]
683
rev_tree = self.branch.repository.revision_tree(revision.revision_id)
684
parent_tree = self.branch.repository.revision_tree(parent_id)
686
self.diff.set_diff(rev_tree, parent_tree)
687
if self.config.get_user_option('viz-wrap-diffs') == 'True':
688
self.diff._on_wraplines_toggled(wrap=True)