Merge pull request #1391 from davvid/macos/hotkeys
[git-cola.git] / cola / sequenceeditor.py
blob4ca57f393ec4472764005e346e51501aa72f5b89
1 import sys
2 import re
3 from argparse import ArgumentParser
4 from functools import partial
6 from cola import app # prints a message if Qt cannot be found
7 from qtpy import QtGui
8 from qtpy import QtWidgets
9 from qtpy.QtCore import Qt
10 from qtpy.QtCore import Signal
12 from cola import core
13 from cola import difftool
14 from cola import gitcmds
15 from cola import hotkeys
16 from cola import icons
17 from cola import qtutils
18 from cola import utils
19 from cola.i18n import N_
20 from cola.models import dag
21 from cola.models import prefs
22 from cola.widgets import defs
23 from cola.widgets import filelist
24 from cola.widgets import diff
25 from cola.widgets import standard
26 from cola.widgets import text
29 PICK = 'pick'
30 REWORD = 'reword'
31 EDIT = 'edit'
32 FIXUP = 'fixup'
33 SQUASH = 'squash'
34 UPDATE_REF = 'update-ref'
35 EXEC = 'exec'
36 COMMANDS = (
37 PICK,
38 REWORD,
39 EDIT,
40 FIXUP,
41 SQUASH,
43 COMMAND_IDX = {cmd_: idx_ for idx_, cmd_ in enumerate(COMMANDS)}
44 ABBREV = {
45 'p': PICK,
46 'r': REWORD,
47 'e': EDIT,
48 'f': FIXUP,
49 's': SQUASH,
50 'x': EXEC,
51 'u': UPDATE_REF,
55 def main():
56 """Start a git-cola-sequence-editor session"""
57 args = parse_args()
58 context = app.application_init(args)
59 view = new_window(context, args.filename)
60 app.application_run(context, view, start=view.start, stop=stop)
61 return view.status
64 def stop(context, _view):
65 """All done, cleanup"""
66 context.view.stop()
67 context.runtask.wait()
70 def parse_args():
71 parser = ArgumentParser()
72 parser.add_argument(
73 'filename', metavar='<filename>', help='git-rebase-todo file to edit'
75 app.add_common_arguments(parser)
76 return parser.parse_args()
79 def new_window(context, filename):
80 window = MainWindow(context)
81 editor = Editor(context, filename, parent=window)
82 window.set_editor(editor)
83 return window
86 def unabbrev(cmd):
87 """Expand shorthand commands into their full name"""
88 return ABBREV.get(cmd, cmd)
91 class MainWindow(standard.MainWindow):
92 """The main git-cola application window"""
94 def __init__(self, context, parent=None):
95 super().__init__(parent)
96 self.context = context
97 self.status = 1
98 # If the user closes the window without confirmation it's considered cancelled.
99 self.cancelled = True
100 self.editor = None
101 default_title = '%s - git cola sequence editor' % core.getcwd()
102 title = core.getenv('GIT_COLA_SEQ_EDITOR_TITLE', default_title)
103 self.setWindowTitle(title)
104 self.show_help_action = qtutils.add_action(
105 self, N_('Show Help'), partial(show_help, context), hotkeys.QUESTION
107 self.menubar = QtWidgets.QMenuBar(self)
108 self.help_menu = self.menubar.addMenu(N_('Help'))
109 self.help_menu.addAction(self.show_help_action)
110 self.setMenuBar(self.menubar)
112 qtutils.add_close_action(self)
113 self.init_state(context.settings, self.init_window_size)
115 def init_window_size(self):
116 """Set the window size on the first initial view"""
117 if utils.is_darwin():
118 width, height = qtutils.desktop_size()
119 self.resize(width, height)
120 else:
121 self.showMaximized()
123 def set_editor(self, editor):
124 self.editor = editor
125 self.setCentralWidget(editor)
126 editor.cancel.connect(self.close)
127 editor.rebase.connect(self.rebase)
128 editor.setFocus()
130 def start(self, _context, _view):
131 """Start background tasks"""
132 self.editor.start()
134 def stop(self):
135 """Stop background tasks"""
136 self.editor.stop()
138 def rebase(self):
139 """Exit the editor and initiate a rebase"""
140 self.status = self.editor.save()
141 self.close()
144 class Editor(QtWidgets.QWidget):
145 cancel = Signal()
146 rebase = Signal()
148 def __init__(self, context, filename, parent=None):
149 super().__init__(parent)
151 self.widget_version = 1
152 self.context = context
153 self.filename = filename
154 self.comment_char = comment_char = prefs.comment_char(context)
156 self.diff = diff.DiffWidget(context, self)
157 self.tree = RebaseTreeWidget(context, comment_char, self)
158 self.filewidget = filelist.FileWidget(context, self, remarks=True)
159 self.setFocusProxy(self.tree)
161 self.rebase_button = qtutils.create_button(
162 text=core.getenv('GIT_COLA_SEQ_EDITOR_ACTION', N_('Rebase')),
163 tooltip=N_('Accept changes and rebase\nShortcut: Ctrl+Enter'),
164 icon=icons.ok(),
165 default=True,
168 self.extdiff_button = qtutils.create_button(
169 text=N_('Launch Diff Tool'),
170 tooltip=N_('Launch external diff tool\nShortcut: Ctrl+D'),
172 self.extdiff_button.setEnabled(False)
174 self.help_button = qtutils.create_button(
175 text=N_('Help'), tooltip=N_('Show help\nShortcut: ?'), icon=icons.question()
178 self.cancel_button = qtutils.create_button(
179 text=N_('Cancel'),
180 tooltip=N_('Cancel rebase\nShortcut: Ctrl+Q'),
181 icon=icons.close(),
184 top = qtutils.splitter(Qt.Horizontal, self.tree, self.filewidget)
185 top.setSizes([75, 25])
187 main_split = qtutils.splitter(Qt.Vertical, top, self.diff)
188 main_split.setSizes([25, 75])
190 controls_layout = qtutils.hbox(
191 defs.no_margin,
192 defs.button_spacing,
193 self.cancel_button,
194 qtutils.STRETCH,
195 self.help_button,
196 self.extdiff_button,
197 self.rebase_button,
199 layout = qtutils.vbox(defs.no_margin, defs.spacing, main_split, controls_layout)
200 self.setLayout(layout)
202 self.action_rebase = qtutils.add_action(
203 self,
204 N_('Rebase'),
205 self.rebase.emit,
206 hotkeys.CTRL_RETURN,
207 hotkeys.CTRL_ENTER,
210 self.tree.commits_selected.connect(self.commits_selected)
211 self.tree.commits_selected.connect(self.filewidget.commits_selected)
212 self.tree.commits_selected.connect(self.diff.commits_selected)
213 self.tree.external_diff.connect(self.external_diff)
215 self.filewidget.files_selected.connect(self.diff.files_selected)
216 self.filewidget.remark_toggled.connect(self.remark_toggled_for_files)
218 # `git` calls are expensive. When user toggles a remark of all commits touching
219 # selected paths the GUI freezes for a while on a big enough sequence. This
220 # cache is used (commit ID to paths tuple) to minimize calls to git.
221 self.oid_to_paths = {}
222 self.task = None # A task fills the cache in the background.
223 self.running = False # This flag stops it.
225 qtutils.connect_button(self.rebase_button, self.rebase.emit)
226 qtutils.connect_button(self.extdiff_button, self.external_diff)
227 qtutils.connect_button(self.help_button, partial(show_help, context))
228 qtutils.connect_button(self.cancel_button, self.cancel.emit)
230 def start(self):
231 insns = core.read(self.filename)
232 self.parse_sequencer_instructions(insns)
234 # Assume that the tree is filled at this point.
235 self.running = True
236 self.task = qtutils.SimpleTask(self.calculate_oid_to_paths)
237 self.context.runtask.start(self.task)
239 def stop(self):
240 self.running = False
242 # signal callbacks
243 def commits_selected(self, commits):
244 self.extdiff_button.setEnabled(bool(commits))
246 def remark_toggled_for_files(self, remark, filenames):
247 filenames = set(filenames)
249 items = self.tree.items()
250 touching_items = []
252 for item in items:
253 if not item.is_commit():
254 continue
255 oid = item.oid
256 paths = self.paths_touched_by_oid(oid)
257 if filenames.intersection(paths):
258 touching_items.append(item)
260 self.tree.toggle_remark_of_items(remark, touching_items)
262 def external_diff(self):
263 items = self.tree.selected_items()
264 if not items:
265 return
266 item = items[0]
267 difftool.diff_expression(self.context, self, item.oid + '^!', hide_expr=True)
269 # helpers
271 def paths_touched_by_oid(self, oid):
272 try:
273 return self.oid_to_paths[oid]
274 except KeyError:
275 pass
277 paths = gitcmds.changed_files(self.context, oid)
278 self.oid_to_paths[oid] = paths
280 return paths
282 def calculate_oid_to_paths(self):
283 """Fills the oid_to_paths cache in the background"""
284 for item in self.tree.items():
285 if not self.running:
286 return
287 self.paths_touched_by_oid(item.oid)
289 def parse_sequencer_instructions(self, insns):
290 idx = 1
291 re_comment_char = re.escape(self.comment_char)
292 exec_rgx = re.compile(r'^\s*(%s)?\s*(x|exec)\s+(.+)$' % re_comment_char)
293 update_ref_rgx = re.compile(
294 r'^\s*(%s)?\s*(u|update-ref)\s+(.+)$' % re_comment_char
296 # The upper bound of 40 below must match git.OID_LENGTH.
297 # We'll have to update this to the new hash length when that happens.
298 pick_rgx = re.compile(
300 r'^\s*(%s)?\s*'
301 + r'(p|pick|r|reword|e|edit|f|fixup|s|squash)'
302 + r'\s+([0-9a-f]{7,40})'
303 + r'\s+(.+)$'
305 % re_comment_char
307 for line in insns.splitlines():
308 match = pick_rgx.match(line)
309 if match:
310 enabled = match.group(1) is None
311 command = unabbrev(match.group(2))
312 oid = match.group(3)
313 summary = match.group(4)
314 self.tree.add_item(idx, enabled, command, oid=oid, summary=summary)
315 idx += 1
316 continue
317 match = exec_rgx.match(line)
318 if match:
319 enabled = match.group(1) is None
320 command = unabbrev(match.group(2))
321 cmdexec = match.group(3)
322 self.tree.add_item(idx, enabled, command, cmdexec=cmdexec)
323 idx += 1
324 continue
325 match = update_ref_rgx.match(line)
326 if match:
327 enabled = match.group(1) is None
328 command = unabbrev(match.group(2))
329 branch = match.group(3)
330 self.tree.add_item(idx, enabled, command, branch=branch)
331 idx += 1
332 continue
334 self.tree.decorate(self.tree.items())
335 self.tree.refit()
336 self.tree.select_first()
338 def save(self, string=None):
339 """Save the instruction sheet"""
341 if string is None:
342 lines = [item.value() for item in self.tree.items()]
343 # sequencer instructions
344 string = '\n'.join(lines) + '\n'
346 try:
347 core.write(self.filename, string)
348 status = 0
349 except (OSError, ValueError) as exc:
350 msg, details = utils.format_exception(exc)
351 sys.stderr.write(msg + '\n\n' + details)
352 status = 128
353 return status
356 class RebaseTreeWidget(standard.DraggableTreeWidget):
357 commits_selected = Signal(object)
358 external_diff = Signal()
359 move_rows = Signal(object, object)
361 def __init__(self, context, comment_char, parent):
362 super().__init__(parent=parent)
363 self.context = context
364 self.comment_char = comment_char
365 # header
366 self.setHeaderLabels([
367 N_('#'),
368 N_('Enabled'),
369 N_('Command'),
370 N_('SHA-1'),
371 N_('Remarks'),
372 N_('Summary'),
374 self.header().setStretchLastSection(True)
375 self.setColumnCount(6)
376 self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
378 # actions
379 self.copy_oid_action = qtutils.add_action(
380 self, N_('Copy SHA-1'), self.copy_oid, QtGui.QKeySequence.Copy
383 self.external_diff_action = qtutils.add_action(
384 self, N_('Launch Diff Tool'), self.external_diff.emit, hotkeys.DIFF
387 self.toggle_enabled_action = qtutils.add_action(
388 self, N_('Toggle Enabled'), self.toggle_enabled, hotkeys.PRIMARY_ACTION
391 self.action_pick = qtutils.add_action(
392 self, N_('Pick'), lambda: self.set_selected_to(PICK), *hotkeys.REBASE_PICK
395 self.action_reword = qtutils.add_action(
396 self,
397 N_('Reword'),
398 lambda: self.set_selected_to(REWORD),
399 *hotkeys.REBASE_REWORD,
402 self.action_edit = qtutils.add_action(
403 self, N_('Edit'), lambda: self.set_selected_to(EDIT), *hotkeys.REBASE_EDIT
406 self.action_fixup = qtutils.add_action(
407 self,
408 N_('Fixup'),
409 lambda: self.set_selected_to(FIXUP),
410 *hotkeys.REBASE_FIXUP,
413 self.action_squash = qtutils.add_action(
414 self,
415 N_('Squash'),
416 lambda: self.set_selected_to(SQUASH),
417 *hotkeys.REBASE_SQUASH,
420 self.action_shift_down = qtutils.add_action(
421 self, N_('Shift Down'), self.shift_down, hotkeys.MOVE_DOWN_TERTIARY
424 self.action_shift_up = qtutils.add_action(
425 self, N_('Shift Up'), self.shift_up, hotkeys.MOVE_UP_TERTIARY
428 self.toggle_remark_actions = tuple(
429 qtutils.add_action(
430 self,
432 lambda remark=r: self.toggle_remark(remark),
433 hotkeys.hotkey(Qt.CTRL | getattr(Qt, 'Key_' + r)),
435 for r in map(str, range(10))
438 self.itemChanged.connect(self.item_changed)
439 self.itemSelectionChanged.connect(self.selection_changed)
440 self.move_rows.connect(self.move)
441 self.items_moved.connect(self.decorate)
443 def add_item(
444 self, idx, enabled, command, oid='', summary='', cmdexec='', branch=''
446 comment_char = self.comment_char
447 item = RebaseTreeWidgetItem(
448 idx,
449 enabled,
450 command,
451 oid=oid,
452 summary=summary,
453 cmdexec=cmdexec,
454 branch=branch,
455 comment_char=comment_char,
456 parent=self,
458 self.invisibleRootItem().addChild(item)
460 def decorate(self, items):
461 for item in items:
462 item.decorate(self)
464 def refit(self):
465 """Resize columns to fit content"""
466 for i in range(RebaseTreeWidgetItem.COLUMN_COUNT - 1):
467 self.resizeColumnToContents(i)
469 def item_changed(self, item, column):
470 """Validate item ordering when toggling their enabled state"""
471 if column == item.ENABLED_COLUMN:
472 self.validate()
474 def validate(self):
475 invalid_first_choice = {FIXUP, SQUASH}
476 for item in self.items():
477 if item.is_enabled() and item.is_commit():
478 if item.command in invalid_first_choice:
479 item.reset_command(PICK)
480 break
482 def set_selected_to(self, command):
483 for i in self.selected_items():
484 i.reset_command(command)
485 self.validate()
487 def set_command(self, item, command):
488 item.reset_command(command)
489 self.validate()
491 def copy_oid(self):
492 item = self.selected_item()
493 if item is None:
494 return
495 clipboard = item.oid or item.cmdexec
496 qtutils.set_clipboard(clipboard)
498 def selection_changed(self):
499 item = self.selected_item()
500 if item is None or not item.is_commit():
501 return
502 context = self.context
503 oid = item.oid
504 params = dag.DAG(oid, 2)
505 repo = dag.RepoReader(context, params)
506 commits = []
507 for commit in repo.get():
508 commits.append(commit)
509 if commits:
510 commits = commits[-1:]
511 self.commits_selected.emit(commits)
513 def toggle_enabled(self):
514 """Toggle the enabled state of each selected item"""
515 items = self.selected_items()
516 enable = should_enable(items, lambda item: item.is_enabled())
517 for item in items:
518 if enable:
519 needs_update = not item.is_enabled()
520 else:
521 needs_update = item.is_enabled()
522 if needs_update:
523 item.set_enabled(enable)
525 def select_first(self):
526 items = self.items()
527 if not items:
528 return
529 idx = self.model().index(0, 0)
530 if idx.isValid():
531 self.setCurrentIndex(idx)
533 def shift_down(self):
534 sel_items = self.selected_items()
535 all_items = self.items()
536 sel_idx = sorted([all_items.index(item) for item in sel_items])
537 if not sel_idx:
538 return
539 idx = sel_idx[0] + 1
540 if not (
541 idx > len(all_items) - len(sel_items)
542 or all_items[sel_idx[-1]] is all_items[-1]
544 self.move_rows.emit(sel_idx, idx)
546 def shift_up(self):
547 sel_items = self.selected_items()
548 all_items = self.items()
549 sel_idx = sorted([all_items.index(item) for item in sel_items])
550 if not sel_idx:
551 return
552 idx = sel_idx[0] - 1
553 if idx >= 0:
554 self.move_rows.emit(sel_idx, idx)
556 def toggle_remark(self, remark):
557 """Toggle remarks for all selected items"""
558 items = self.selected_items()
559 self.toggle_remark_of_items(remark, items)
561 def toggle_remark_of_items(self, remark, items):
562 """Toggle remarks for the selected items"""
563 enable = should_enable(items, lambda item: remark in item.remarks)
564 for item in items:
565 needs_update = enable ^ (remark in item.remarks)
566 if needs_update:
567 if enable:
568 item.add_remark(remark)
569 else:
570 item.remove_remark(remark)
572 def move(self, src_idxs, dst_idx):
573 moved_items = []
574 src_base = sorted(src_idxs)[0]
575 for idx in reversed(sorted(src_idxs)):
576 item = self.invisibleRootItem().takeChild(idx)
577 moved_items.insert(0, [dst_idx + (idx - src_base), item])
579 for item in moved_items:
580 self.invisibleRootItem().insertChild(item[0], item[1])
581 self.setCurrentItem(item[1])
583 if moved_items:
584 moved_items = [item[1] for item in moved_items]
585 # If we've moved to the top then we need to re-decorate all items.
586 # Otherwise, we can decorate just the new items.
587 if dst_idx == 0:
588 self.decorate(self.items())
589 else:
590 self.decorate(moved_items)
592 for item in moved_items:
593 item.setSelected(True)
594 self.validate()
596 # Qt events
598 def dropEvent(self, event):
599 super().dropEvent(event)
600 self.validate()
602 def contextMenuEvent(self, event):
603 items = self.selected_items()
604 menu = qtutils.create_menu(N_('Actions'), self)
605 menu.addAction(self.action_pick)
606 menu.addAction(self.action_reword)
607 menu.addAction(self.action_edit)
608 menu.addAction(self.action_fixup)
609 menu.addAction(self.action_squash)
610 menu.addSeparator()
611 menu.addAction(self.toggle_enabled_action)
612 menu.addSeparator()
613 menu.addAction(self.copy_oid_action)
614 self.copy_oid_action.setDisabled(len(items) > 1)
615 menu.addAction(self.external_diff_action)
616 self.external_diff_action.setDisabled(len(items) > 1)
617 menu.addSeparator()
618 menu_toggle_remark = menu.addMenu(N_('Toggle Remark'))
619 for action in self.toggle_remark_actions:
620 menu_toggle_remark.addAction(action)
621 menu.exec_(self.mapToGlobal(event.pos()))
624 def should_enable(items, predicate):
625 """Calculate whether items should be toggled on or off.
627 If all items are enabled then return False.
628 If all items are disabled then return True.
629 If more items are enabled then return True, otherwise return False.
631 count = len(items)
632 enabled = sum(predicate(item) for item in items)
633 disabled = len(items) - enabled
634 enable = count > enabled >= disabled or disabled == count
635 return enable
638 class ComboBox(QtWidgets.QComboBox):
639 validate = Signal()
642 class RebaseTreeWidgetItem(QtWidgets.QTreeWidgetItem):
643 """A single data row in the rebase tree widget"""
645 NUMBER_COLUMN = 0
646 ENABLED_COLUMN = 1
647 COMMAND_COLUMN = 2
648 COMMIT_COLUMN = 3
649 REMARKS_COLUMN = 4
650 SUMMARY_COLUMN = 5
651 COLUMN_COUNT = 6
652 OID_LENGTH = 7
653 COLORS = {
654 '0': ('white', 'darkred'),
655 '1': ('black', 'salmon'),
656 '2': ('black', 'sandybrown'),
657 '3': ('black', 'yellow'),
658 '4': ('black', 'yellowgreen'),
659 '5': ('white', 'forestgreen'),
660 '6': ('white', 'dodgerblue'),
661 '7': ('white', 'royalblue'),
662 '8': ('white', 'slateblue'),
663 '9': ('black', 'rosybrown'),
666 def __init__(
667 self,
668 idx,
669 enabled,
670 command,
671 oid='',
672 summary='',
673 cmdexec='',
674 branch='',
675 comment_char='#',
676 remarks=(),
677 parent=None,
679 QtWidgets.QTreeWidgetItem.__init__(self, parent)
680 self.combo = None
681 self.command = command
682 self.idx = idx
683 self.oid = oid
684 self.summary = summary
685 self.cmdexec = cmdexec
686 self.branch = branch
687 self.comment_char = comment_char
688 self.remarks = remarks
689 self.remarks_label = None
690 self._parent = parent
692 # if core.abbrev is set to a higher value then we will notice by
693 # simply tracking the longest oid we've seen
694 oid_len = self.OID_LENGTH
695 self.__class__.OID_LENGTH = max(len(oid), oid_len)
697 self.setText(self.NUMBER_COLUMN, '%02d' % idx)
698 self.set_enabled(enabled)
699 # checkbox on 1
700 # combo box on 2
701 if self.is_exec():
702 self.setText(self.COMMIT_COLUMN, '')
703 self.setText(self.SUMMARY_COLUMN, cmdexec)
704 elif self.is_update_ref():
705 self.setText(self.COMMIT_COLUMN, '')
706 self.setText(self.SUMMARY_COLUMN, branch)
707 else:
708 self.setText(self.COMMIT_COLUMN, oid)
709 self.setText(self.SUMMARY_COLUMN, summary)
711 self.set_remarks(remarks)
713 flags = self.flags() | Qt.ItemIsUserCheckable
714 flags = flags | Qt.ItemIsDragEnabled
715 flags = flags & ~Qt.ItemIsDropEnabled
716 self.setFlags(flags)
718 def __eq__(self, other):
719 return self is other
721 def __hash__(self):
722 return self.oid
724 def copy(self):
725 return self.__class__(
726 self.idx,
727 self.is_enabled(),
728 self.command,
729 oid=self.oid,
730 summary=self.summary,
731 cmdexec=self.cmdexec,
732 branch=self.branch,
733 comment_char=self.comment_char,
734 remarks=self.remarks,
737 def decorate(self, parent):
738 if self.is_exec():
739 items = [EXEC]
740 idx = 0
741 elif self.is_update_ref():
742 items = [UPDATE_REF]
743 idx = 0
744 else:
745 items = COMMANDS
746 idx = COMMAND_IDX[self.command]
747 combo = self.combo = ComboBox()
748 combo.setEditable(False)
749 combo.addItems(items)
750 combo.setCurrentIndex(idx)
751 combo.setEnabled(self.is_commit())
753 signal = combo.currentIndexChanged
754 signal.connect(lambda x: self.set_command_and_validate(combo))
755 combo.validate.connect(parent.validate)
757 parent.setItemWidget(self, self.COMMAND_COLUMN, combo)
759 self.remarks_label = remarks_label = QtWidgets.QLabel()
760 parent.setItemWidget(self, self.REMARKS_COLUMN, remarks_label)
761 self.update_remarks()
763 def is_exec(self):
764 return self.command == EXEC
766 def is_update_ref(self):
767 return self.command == UPDATE_REF
769 def is_commit(self):
770 return bool(
771 not (self.is_exec() or self.is_update_ref()) and self.oid and self.summary
774 def value(self):
775 """Return the serialized representation of an item"""
776 if self.is_enabled():
777 comment = ''
778 else:
779 comment = self.comment_char + ' '
780 if self.is_exec():
781 return f'{comment}{self.command} {self.cmdexec}'
782 if self.is_update_ref():
783 return f'{comment}{self.command} {self.branch}'
784 return f'{comment}{self.command} {self.oid} {self.summary}'
786 def is_enabled(self):
787 """Is the item enabled?"""
788 return self.checkState(self.ENABLED_COLUMN) == Qt.Checked
790 def set_enabled(self, enabled):
791 """Enable the item by checking its enabled checkbox"""
792 self.setCheckState(self.ENABLED_COLUMN, enabled and Qt.Checked or Qt.Unchecked)
794 def toggle_enabled(self):
795 """Toggle the enabled state of the item"""
796 self.set_enabled(not self.is_enabled())
798 def add_remark(self, remark):
799 """Add a remark to the item"""
800 self.set_remarks(tuple(sorted(set(self.remarks + (remark,)))))
802 def remove_remark(self, remark):
803 """Remove a remark from the item"""
804 self.set_remarks(tuple(r for r in self.remarks if r != remark))
806 def set_remarks(self, remarks):
807 """Set the remarks and update the remark display"""
808 if remarks == self.remarks:
809 return
810 self.remarks = remarks
811 self.update_remarks()
812 self._parent.resizeColumnToContents(self.REMARKS_COLUMN)
814 def update_remarks(self):
815 """Update the remarks label display to match the current remarks"""
816 label = self.remarks_label
817 if label is None:
818 return
819 label_text = ''
820 for remark in self.remarks:
821 fg_color, bg_color = self.COLORS[remark]
822 label_text += f"""
823 <span style="
824 color: {fg_color};
825 background-color: {bg_color};
826 ">&nbsp;{remark} </span>
828 label.setText(label_text)
830 def set_command(self, command):
831 """Set the item to a different command, no-op for exec items"""
832 if self.is_exec():
833 return
834 self.command = command
836 def refresh(self):
837 """Update the view to match the updated state"""
838 if self.is_commit():
839 command = self.command
840 self.combo.setCurrentIndex(COMMAND_IDX[command])
842 def reset_command(self, command):
843 """Set and refresh the item in one shot"""
844 self.set_command(command)
845 self.refresh()
847 def set_command_and_validate(self, combo):
848 """Set the command and validate the command order"""
849 command = COMMANDS[combo.currentIndex()]
850 self.set_command(command)
851 self.combo.validate.emit()
854 def show_help(context):
855 help_text = N_(
857 Commands
858 --------
859 pick = use commit
860 reword = use commit, but edit the commit message
861 edit = use commit, but stop for amending
862 squash = use commit, but meld into previous commit
863 fixup = like "squash", but discard this commit's log message
864 exec = run command (the rest of the line) using shell
865 update-ref = update branches that point to commits
867 These lines can be re-ordered; they are executed from top to bottom.
869 If you disable a line here THAT COMMIT WILL BE LOST.
871 However, if you disable everything, the rebase will be aborted.
873 Keyboard Shortcuts
874 ------------------
875 ? = show help
876 j = move down
877 k = move up
878 J = shift row down
879 K = shift row up
881 1, p = pick
882 2, r = reword
883 3, e = edit
884 4, f = fixup
885 5, s = squash
886 spacebar = toggle enabled
888 ctrl+enter = accept changes and rebase
889 ctrl+q = cancel and abort the rebase
890 ctrl+d = launch difftool
893 title = N_('Help - git-cola-sequence-editor')
894 return text.text_dialog(context, help_text, title)