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
Window.__init__(self, 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
if getattr(Gtk.Action, 'set_tool_item_type', None) is not None:
82
Gtk.Action.set_tool_item_type(Gtk.MenuToolButton)
84
self.prev_rev_action = Gtk.Action("prev-rev", "_Previous Revision", "Go to the previous revision", Gtk.STOCK_GO_DOWN)
85
self.prev_rev_action.set_accel_path("<viz>/Go/Previous Revision")
86
self.prev_rev_action.set_accel_group(self.accel_group)
87
self.prev_rev_action.connect("activate", self._back_clicked_cb)
88
self.prev_rev_action.connect_accelerator()
90
self.next_rev_action = Gtk.Action("next-rev", "_Next Revision", "Go to the next revision", Gtk.STOCK_GO_UP)
91
self.next_rev_action.set_accel_path("<viz>/Go/Next Revision")
92
self.next_rev_action.set_accel_group(self.accel_group)
93
self.next_rev_action.connect("activate", self._fwd_clicked_cb)
94
self.next_rev_action.connect_accelerator()
96
self.refresh_action = Gtk.Action("refresh", "_Refresh", "Refresh view", Gtk.STOCK_REFRESH)
97
self.refresh_action.set_accel_path("<viz>/View/Refresh")
98
self.refresh_action.set_accel_group(self.accel_group)
99
self.refresh_action.connect("activate", self._refresh_clicked)
100
self.refresh_action.connect_accelerator()
102
self.vbox = self.construct()
104
def _save_size_on_destroy(self, widget, config_name):
105
"""Creates a hook that saves the size of widget to config option
106
config_name when the window is destroyed/closed."""
108
allocation = widget.get_allocation()
109
width, height = allocation.width, allocation.height
110
value = '%sx%s' % (width, height)
111
self.config.set_user_option(config_name, value)
112
self.connect("destroy", save_size)
114
def set_revision(self, revid):
115
self.treeview.set_revision_id(revid)
118
"""Construct the window contents."""
119
vbox = Gtk.VBox(spacing=0)
122
# order is important here
123
paned = self.construct_paned()
124
nav = self.construct_navigation()
125
menubar = self.construct_menubar()
127
vbox.pack_start(menubar, False, True, 0)
128
vbox.pack_start(nav, False, True, 0)
129
vbox.pack_start(paned, True, True, 0)
130
vbox.set_focus_child(paned)
137
def construct_paned(self):
138
"""Construct the main HPaned/VPaned contents."""
139
if self.config.get_user_option('viz-vertical') == 'True':
140
self.paned = Gtk.HPaned()
142
self.paned = Gtk.VPaned()
144
self.paned.pack1(self.construct_top(), resize=False, shrink=True)
145
self.paned.pack2(self.construct_bottom(), resize=True, shrink=False)
150
def construct_menubar(self):
151
menubar = Gtk.MenuBar()
153
file_menu = Gtk.Menu()
154
file_menuitem = Gtk.MenuItem.new_with_mnemonic("_File")
155
file_menuitem.set_submenu(file_menu)
157
file_menu_close = Gtk.ImageMenuItem.new_from_stock(
158
Gtk.STOCK_CLOSE, self.accel_group)
159
file_menu_close.connect('activate', lambda x: self.destroy())
161
file_menu_quit = Gtk.ImageMenuItem.new_from_stock(
162
Gtk.STOCK_QUIT, self.accel_group)
163
file_menu_quit.connect('activate', lambda x: Gtk.main_quit())
165
if self._parent is not None:
166
file_menu.add(file_menu_close)
167
file_menu.add(file_menu_quit)
169
edit_menu = Gtk.Menu()
170
edit_menuitem = Gtk.MenuItem.new_with_mnemonic("_Edit")
171
edit_menuitem.set_submenu(edit_menu)
173
edit_menu_branchopts = Gtk.MenuItem(label="Branch Settings")
174
edit_menu_branchopts.connect('activate', lambda x: PreferencesWindow(self.branch.get_config()).show())
176
edit_menu_globopts = Gtk.MenuItem(label="Global Settings")
177
edit_menu_globopts.connect('activate', lambda x: PreferencesWindow().show())
179
edit_menu.add(edit_menu_branchopts)
180
edit_menu.add(edit_menu_globopts)
182
view_menu = Gtk.Menu()
183
view_menuitem = Gtk.MenuItem.new_with_mnemonic("_View")
184
view_menuitem.set_submenu(view_menu)
186
view_menu_refresh = self.refresh_action.create_menu_item()
187
view_menu_refresh.connect('activate', self._refresh_clicked)
189
view_menu.add(view_menu_refresh)
190
view_menu.add(Gtk.SeparatorMenuItem())
192
view_menu_toolbar = Gtk.CheckMenuItem(label="Show Toolbar")
193
view_menu_toolbar.set_active(True)
194
if self.config.get_user_option('viz-toolbar-visible') == 'False':
195
view_menu_toolbar.set_active(False)
197
view_menu_toolbar.connect('toggled', self._toolbar_visibility_changed)
199
view_menu_compact = Gtk.CheckMenuItem(label="Show Compact Graph")
200
view_menu_compact.set_active(self.compact_view)
201
view_menu_compact.connect('activate', self._brokenlines_toggled_cb)
203
view_menu_vertical = Gtk.CheckMenuItem(label="Side-by-side Layout")
204
view_menu_vertical.set_active(False)
205
if self.config.get_user_option('viz-vertical') == 'True':
206
view_menu_vertical.set_active(True)
207
view_menu_vertical.connect('toggled', self._vertical_layout)
209
view_menu_diffs = Gtk.CheckMenuItem(label="Show Diffs")
210
view_menu_diffs.set_active(False)
211
if self.config.get_user_option('viz-show-diffs') == 'True':
212
view_menu_diffs.set_active(True)
213
view_menu_diffs.connect('toggled', self._diff_visibility_changed)
215
view_menu_wide_diffs = Gtk.CheckMenuItem(label="Wide Diffs")
216
view_menu_wide_diffs.set_active(False)
217
if self.config.get_user_option('viz-wide-diffs') == 'True':
218
view_menu_wide_diffs.set_active(True)
219
view_menu_wide_diffs.connect('toggled', self._diff_placement_changed)
221
view_menu_wrap_diffs = Gtk.CheckMenuItem.new_with_mnemonic(
222
"Wrap _Long Lines in Diffs")
223
view_menu_wrap_diffs.set_active(False)
224
if self.config.get_user_option('viz-wrap-diffs') == 'True':
225
view_menu_wrap_diffs.set_active(True)
226
view_menu_wrap_diffs.connect('toggled', self._diff_wrap_changed)
228
view_menu.add(view_menu_toolbar)
229
view_menu.add(view_menu_compact)
230
view_menu.add(view_menu_vertical)
231
view_menu.add(Gtk.SeparatorMenuItem())
232
view_menu.add(view_menu_diffs)
233
view_menu.add(view_menu_wide_diffs)
234
view_menu.add(view_menu_wrap_diffs)
235
view_menu.add(Gtk.SeparatorMenuItem())
237
self.mnu_show_revno_column = Gtk.CheckMenuItem.new_with_mnemonic(
238
"Show Revision _Number Column")
239
self.mnu_show_date_column = Gtk.CheckMenuItem.new_with_mnemonic(
242
# Revision numbers are pointless if there are multiple branches
243
if len(self.start_revs) > 1:
244
self.mnu_show_revno_column.set_sensitive(False)
245
self.treeview.set_property('revno-column-visible', False)
247
for (col, name) in [(self.mnu_show_revno_column, "revno"),
248
(self.mnu_show_date_column, "date")]:
249
col.set_active(self.treeview.get_property(name + "-column-visible"))
250
col.connect('toggled', self._col_visibility_changed, name)
254
go_menu.set_accel_group(self.accel_group)
255
go_menuitem = Gtk.MenuItem.new_with_mnemonic("_Go")
256
go_menuitem.set_submenu(go_menu)
258
go_menu_next = self.next_rev_action.create_menu_item()
259
go_menu_prev = self.prev_rev_action.create_menu_item()
261
tag_image = Gtk.Image()
262
tag_image.set_from_file(icon_path("tag-16.png"))
263
self.go_menu_tags = Gtk.ImageMenuItem.new_with_mnemonic("_Tags")
264
self.go_menu_tags.set_image(tag_image)
265
self.treeview.connect('refreshed', lambda w: self._update_tags())
267
go_menu.add(go_menu_next)
268
go_menu.add(go_menu_prev)
269
go_menu.add(Gtk.SeparatorMenuItem())
270
go_menu.add(self.go_menu_tags)
272
self.revision_menu = RevisionMenu(self.branch.repository, [],
273
self.branch, parent=self)
274
revision_menuitem = Gtk.MenuItem.new_with_mnemonic("_Revision")
275
revision_menuitem.set_submenu(self.revision_menu)
277
branch_menu = Gtk.Menu()
278
branch_menuitem = Gtk.MenuItem.new_with_mnemonic("_Branch")
279
branch_menuitem.set_submenu(branch_menu)
281
branch_menu.add(Gtk.MenuItem.new_with_mnemonic("Pu_ll Revisions"))
282
branch_menu.add(Gtk.MenuItem.new_with_mnemonic("Pu_sh Revisions"))
285
from bzrlib.plugins import search
287
mutter("Didn't find search plugin")
289
branch_menu.add(Gtk.SeparatorMenuItem())
291
branch_index_menuitem = Gtk.MenuItem.new_with_mnemonic("_Index")
292
branch_index_menuitem.connect('activate', self._branch_index_cb)
293
branch_menu.add(branch_index_menuitem)
295
branch_search_menuitem = Gtk.MenuItem.new_with_mnemonic("_Search")
296
branch_search_menuitem.connect('activate', self._branch_search_cb)
297
branch_menu.add(branch_search_menuitem)
299
help_menu = Gtk.Menu()
300
help_menuitem = Gtk.MenuItem.new_with_mnemonic("_Help")
301
help_menuitem.set_submenu(help_menu)
303
help_about_menuitem = Gtk.ImageMenuItem.new_from_stock(
304
Gtk.STOCK_ABOUT, self.accel_group)
305
help_about_menuitem.connect('activate', self._about_dialog_cb)
307
help_menu.add(help_about_menuitem)
309
menubar.add(file_menuitem)
310
menubar.add(edit_menuitem)
311
menubar.add(view_menuitem)
312
menubar.add(go_menuitem)
313
menubar.add(revision_menuitem)
314
menubar.add(branch_menuitem)
315
menubar.add(help_menuitem)
320
def construct_top(self):
321
"""Construct the top-half of the window."""
322
# FIXME: Make broken_line_length configurable
324
self.treeview = TreeView(self.branch, self.start_revs, self.maxnum,
327
for col in ["revno", "date"]:
328
option = self.config.get_user_option(col + '-column-visible')
329
if option is not None:
330
self.treeview.set_property(col + '-column-visible',
333
self.treeview.set_property(col + '-column-visible', False)
337
align = Gtk.Alignment.new(0.0, 0.0, 1.0, 1.0)
338
align.set_padding(5, 0, 0, 0)
339
align.add(self.treeview)
340
# user-configured size
341
size = self._load_size('viz-graph-size')
344
align.set_size_request(width, height)
346
(width, height) = self.get_size()
347
align.set_size_request(width, int(height / 2.5))
348
self._save_size_on_destroy(align, 'viz-graph-size')
353
def construct_navigation(self):
354
"""Construct the navigation buttons."""
355
self.toolbar = Gtk.Toolbar()
356
self.toolbar.set_style(Gtk.ToolbarStyle.BOTH_HORIZ)
358
self.prev_button = self.prev_rev_action.create_tool_item()
359
self.toolbar.insert(self.prev_button, -1)
361
self.next_button = self.next_rev_action.create_tool_item()
362
self.toolbar.insert(self.next_button, -1)
364
self.toolbar.insert(Gtk.SeparatorToolItem(), -1)
366
refresh_button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_REFRESH)
367
refresh_button.connect('clicked', self._refresh_clicked)
368
self.toolbar.insert(refresh_button, -1)
370
self.toolbar.show_all()
374
def construct_bottom(self):
375
"""Construct the bottom half of the window."""
376
if self.config.get_user_option('viz-wide-diffs') == 'True':
377
self.diff_paned = Gtk.VPaned()
379
self.diff_paned = Gtk.HPaned()
380
(width, height) = self.get_size()
381
self.diff_paned.set_size_request(20, 20) # shrinkable
383
from bzrlib.plugins.gtk.revisionview import RevisionView
384
self.revisionview = RevisionView(branch=self.branch)
385
self.revisionview.set_size_request(width/3, int(height / 2.5))
386
# user-configured size
387
size = self._load_size('viz-revisionview-size')
390
self.revisionview.set_size_request(width, height)
391
self._save_size_on_destroy(self.revisionview, 'viz-revisionview-size')
392
self.revisionview.show()
393
self.revisionview.set_show_callback(self._show_clicked_cb)
394
self.revisionview.connect('notify::revision', self._go_clicked_cb)
395
self.treeview.connect('tag-added',
396
lambda w, t, r: self.revisionview.update_tags())
397
self.treeview.connect('revision-selected',
398
self._treeselection_changed_cb)
399
self.treeview.connect('revision-activated',
400
self._tree_revision_activated)
401
self.diff_paned.pack1(self.revisionview)
403
from bzrlib.plugins.gtk.diff import DiffWidget
404
self.diff = DiffWidget()
405
self.diff_paned.pack2(self.diff)
407
self.diff_paned.show_all()
408
if self.config.get_user_option('viz-show-diffs') != 'True':
411
return self.diff_paned
413
def _tag_selected_cb(self, menuitem, revid):
414
self.treeview.set_revision_id(revid)
416
def _treeselection_changed_cb(self, selection, *args):
417
"""callback for when the treeview changes."""
418
revision = self.treeview.get_revision()
419
parents = self.treeview.get_parents()
420
children = self.treeview.get_children()
422
if revision and revision.revision_id != NULL_REVISION:
423
self.revision_menu.set_revision_ids([revision.revision_id])
424
prev_menu = Gtk.Menu()
426
self.prev_rev_action.set_sensitive(True)
427
for parent_id in parents:
428
if parent_id and parent_id != NULL_REVISION:
429
parent = self.branch.repository.get_revision(parent_id)
431
str = ' (%s)' % parent.properties['branch-nick']
436
label=parent.message.split("\n")[0] + str)
437
item.connect('activate', self._set_revision_cb, parent_id)
441
self.prev_rev_action.set_sensitive(False)
444
if getattr(self.prev_button, 'set_menu', None) is not None:
445
self.prev_button.set_menu(prev_menu)
447
next_menu = Gtk.Menu()
448
if len(children) > 0:
449
self.next_rev_action.set_sensitive(True)
450
for child_id in children:
451
child = self.branch.repository.get_revision(child_id)
453
str = ' (%s)' % child.properties['branch-nick']
457
item = Gtk.MenuItem(child.message.split("\n")[0] + str)
458
item.connect('activate', self._set_revision_cb, child_id)
462
self.next_rev_action.set_sensitive(False)
465
if getattr(self.next_button, 'set_menu', None) is not None:
466
self.next_button.set_menu(next_menu)
468
self.revisionview.set_revision(revision)
469
self.revisionview.set_children(children)
470
self.update_diff_panel(revision, parents)
472
def _tree_revision_activated(self, widget, path, col):
473
# TODO: more than one parent
474
"""Callback for when a treeview row gets activated."""
475
revision = self.treeview.get_revision()
476
parents = self.treeview.get_parents()
478
if len(parents) == 0:
479
parent_id = NULL_REVISION
481
parent_id = parents[0]
483
self.show_diff(revision.revision_id, parent_id)
484
self.treeview.grab_focus()
486
def _back_clicked_cb(self, *args):
487
"""Callback for when the back button is clicked."""
490
def _fwd_clicked_cb(self, *args):
491
"""Callback for when the forward button is clicked."""
492
self.treeview.forward()
494
def _go_clicked_cb(self, w, p):
495
"""Callback for when the go button for a parent is clicked."""
496
if self.revisionview.get_revision() is not None:
497
self.treeview.set_revision(self.revisionview.get_revision())
499
def _show_clicked_cb(self, revid, parentid):
500
"""Callback for when the show button for a parent is clicked."""
501
self.show_diff(revid, parentid)
502
self.treeview.grab_focus()
504
def _set_revision_cb(self, w, revision_id):
505
self.treeview.set_revision_id(revision_id)
507
def _brokenlines_toggled_cb(self, button):
508
self.compact_view = button.get_active()
510
if self.compact_view:
515
self.config.set_user_option('viz-compact-view', option)
516
self.treeview.set_property('compact', self.compact_view)
517
self.treeview.refresh()
519
def _branch_index_cb(self, w):
520
from bzrlib.plugins.search import index as _mod_index
521
_mod_index.index_url(self.branch.base)
523
def _branch_search_cb(self, w):
524
from bzrlib.plugins.search import (
526
errors as search_errors,
528
from bzrlib.plugins.gtk.search import SearchDialog
531
index = _mod_index.open_index_url(self.branch.base)
532
except search_errors.NoSearchIndex:
533
dialog = Gtk.MessageDialog(self, type=Gtk.MessageType.QUESTION,
534
buttons=Gtk.ButtonsType.OK_CANCEL,
535
message_format="This branch has not been indexed yet. "
537
if dialog.run() == Gtk.ResponseType.OK:
539
index = _mod_index.index_url(self.branch.base)
544
dialog = SearchDialog(index)
546
if dialog.run() == Gtk.ResponseType.OK:
547
self.set_revision(dialog.get_revision())
551
def _about_dialog_cb(self, w):
552
from bzrlib.plugins.gtk.about import AboutDialog
555
def _col_visibility_changed(self, col, property):
556
self.config.set_user_option(property + '-column-visible', col.get_active())
557
self.treeview.set_property(property + '-column-visible', col.get_active())
559
def _toolbar_visibility_changed(self, col):
564
self.config.set_user_option('viz-toolbar-visible', col.get_active())
566
def _vertical_layout(self, col):
567
"""Toggle the layout vertical/horizontal"""
568
self.config.set_user_option('viz-vertical', str(col.get_active()))
571
self.vbox.remove(old)
572
self.vbox.pack_start(
573
self.construct_paned(), True, True, True, 0)
574
self._make_diff_paned_nonzero_size()
575
self._make_diff_nonzero_size()
577
self.treeview.emit('revision-selected')
579
def _make_diff_paned_nonzero_size(self):
580
"""make sure the diff/revision pane isn't zero-width or zero-height"""
581
alloc = self.diff_paned.get_allocation()
582
if (alloc.width < 10) or (alloc.height < 10):
583
width, height = self.get_size()
584
self.diff_paned.set_size_request(width/3, int(height / 2.5))
586
def _make_diff_nonzero_size(self):
587
"""make sure the diff isn't zero-width or zero-height"""
588
alloc = self.diff.get_allocation()
589
if (alloc.width < 10) or (alloc.height < 10):
590
width, height = self.get_size()
591
self.revisionview.set_size_request(width/3, int(height / 2.5))
593
def _diff_visibility_changed(self, col):
594
"""Hide or show the diff panel."""
597
self._make_diff_nonzero_size()
600
self.config.set_user_option('viz-show-diffs', str(col.get_active()))
601
self.update_diff_panel()
603
def _diff_placement_changed(self, col):
604
"""Toggle the diff panel's position."""
605
self.config.set_user_option('viz-wide-diffs', str(col.get_active()))
607
old = self.paned.get_child2()
608
self.paned.remove(old)
609
self.paned.pack2(self.construct_bottom(), resize=True, shrink=False)
610
self._make_diff_nonzero_size()
612
self.treeview.emit('revision-selected')
614
def _diff_wrap_changed(self, widget):
615
"""Toggle word wrap in the diff widget."""
616
self.config.set_user_option('viz-wrap-diffs', widget.get_active())
617
self.diff._on_wraplines_toggled(widget)
619
def _refresh_clicked(self, w):
620
self.treeview.refresh()
622
def _update_tags(self):
625
if self.branch.supports_tags():
626
tags = self.branch.tags.get_tag_dict().items()
627
tags.sort(reverse=True)
628
for tag, revid in tags:
629
tag_image = Gtk.Image()
630
tag_image.set_from_file(icon_path('tag-16.png'))
631
tag_item = Gtk.ImageMenuItem.new_with_mnemonic(
632
tag.replace('_', '__'))
633
tag_item.set_image(tag_image)
634
tag_item.connect('activate', self._tag_selected_cb, revid)
635
tag_item.set_sensitive(self.treeview.has_revision_id(revid))
637
self.go_menu_tags.set_submenu(menu)
639
self.go_menu_tags.set_sensitive(len(tags) != 0)
641
self.go_menu_tags.set_sensitive(False)
643
self.go_menu_tags.show_all()
645
def _load_size(self, name):
646
"""Read and parse 'name' from self.config.
647
The value is a string, formatted as WIDTHxHEIGHT
648
Returns None, or (width, height)
650
size = self.config.get_user_option(name)
652
width, height = [int(num) for num in size.split('x')]
653
# avoid writing config every time we start
657
def show_diff(self, revid=None, parentid=NULL_REVISION):
658
"""Open a new window to show a diff between the given revisions."""
659
from bzrlib.plugins.gtk.diff import DiffWindow
660
window = DiffWindow(parent=self)
662
rev_tree = self.branch.repository.revision_tree(revid)
663
parent_tree = self.branch.repository.revision_tree(parentid)
665
description = revid + " - " + self.branch._get_nick(local=True)
666
window.set_diff(description, rev_tree, parent_tree)
669
def update_diff_panel(self, revision=None, parents=None):
670
"""Show the current revision in the diff panel."""
671
if self.config.get_user_option('viz-show-diffs') != 'True':
674
if not revision: # default to selected row
675
revision = self.treeview.get_revision()
676
if revision == NULL_REVISION:
679
if not parents: # default to selected row's parents
680
parents = self.treeview.get_parents()
681
if len(parents) == 0:
682
parent_id = NULL_REVISION
684
parent_id = parents[0]
686
rev_tree = self.branch.repository.revision_tree(revision.revision_id)
687
parent_tree = self.branch.repository.revision_tree(parent_id)
689
self.diff.set_diff(rev_tree, parent_tree)
690
if self.config.get_user_option('viz-wrap-diffs') == 'True':
691
self.diff._on_wraplines_toggled(wrap=True)