fetch: add ability to fetch into a remote tracking branch
[git-cola.git] / cola / widgets / toolbar.py
blob56c2e9ec8267c0b2f1d1626d761ac449772ec1ce
1 from functools import partial
3 from qtpy import QtGui
4 from qtpy.QtCore import Qt
5 from qtpy import QtWidgets
7 from ..i18n import N_
8 from .. import icons
9 from .. import qtutils
10 from . import defs
11 from . import standard
12 from .toolbarcmds import COMMANDS
15 TREE_LAYOUT = {
16 'Others': ['Others::LaunchEditor', 'Others::RevertUnstagedEdits'],
17 'File': [
18 'File::QuickOpen',
19 'File::NewRepo',
20 'File::OpenRepo',
21 'File::OpenRepoNewWindow',
22 'File::Refresh',
23 'File::EditRemotes',
24 'File::RecentModified',
25 'File::SaveAsTarZip',
26 'File::ApplyPatches',
27 'File::ExportPatches',
29 'Actions': [
30 'Actions::Fetch',
31 'Actions::Pull',
32 'Actions::Push',
33 'Actions::Stash',
34 'Actions::CreateTag',
35 'Actions::CherryPick',
36 'Actions::Merge',
37 'Actions::AbortMerge',
38 'Actions::UpdateSubmodules',
39 'Actions::Grep',
40 'Actions::Search',
42 'Commit@@verb': [
43 'Commit::Stage',
44 'Commit::AmendLast',
45 'Commit::UndoLastCommit',
46 'Commit::StageModified',
47 'Commit::StageUntracked',
48 'Commit::UnstageAll',
49 'Commit::Unstage',
50 'Commit::LoadCommitMessage',
51 'Commit::GetCommitMessageTemplate',
53 'Diff': ['Diff::Difftool', 'Diff::Expression', 'Diff::Branches', 'Diff::Diffstat'],
54 'Branch': [
55 'Branch::Review',
56 'Branch::Create',
57 'Branch::Checkout',
58 'Branch::Delete',
59 'Branch::DeleteRemote',
60 'Branch::Rename',
61 'Branch::BrowseCurrent',
62 'Branch::BrowseOther',
63 'Branch::VisualizeCurrent',
64 'Branch::VisualizeAll',
66 'Reset': [
67 'Commit::UndoLastCommit',
68 'Commit::UnstageAll',
69 'Actions::ResetSoft',
70 'Actions::ResetMixed',
71 'Actions::RestoreWorktree',
72 'Actions::ResetKeep',
73 'Actions::ResetHard',
75 'View': ['View::DAG', 'View::FileBrowser'],
78 # Backwards-compatibility: Commit::StageUntracked was previously
79 # exposed as Commit::StageUntracked.
80 RENAMED = {
81 'Commit::StageAll': 'Commit::StageUntracked',
85 def configure(toolbar, parent=None):
86 """Launches the Toolbar configure dialog"""
87 if not parent:
88 parent = qtutils.active_window()
89 view = ToolbarView(toolbar, parent)
90 view.show()
91 return view
94 def get_toolbars(widget):
95 return widget.findChildren(ToolBar)
98 def add_toolbar(context, widget):
99 toolbars = get_toolbars(widget)
100 name = 'ToolBar%d' % (len(toolbars) + 1)
101 toolbar = ToolBar.create(context, name)
102 widget.addToolBar(toolbar)
103 configure(toolbar)
106 class ToolBarState:
107 """export_state() and apply_state() providers for toolbars"""
109 def __init__(self, context, widget):
110 """widget must be a QMainWindow for toolBarArea(), etc."""
111 self.context = context
112 self.widget = widget
114 def apply_state(self, toolbars):
115 context = self.context
116 widget = self.widget
118 for data in toolbars:
119 toolbar = ToolBar.create(context, data['name'])
120 toolbar.load_items(data['items'])
121 try:
122 toolbar.set_toolbar_style(data['toolbar_style'])
123 except KeyError:
124 # Maintain compatibility for toolbars created in git-cola <= 3.11.0
125 if data['show_icons']:
126 data['toolbar_style'] = ToolBar.STYLE_FOLLOW_SYSTEM
127 toolbar.set_toolbar_style(ToolBar.STYLE_FOLLOW_SYSTEM)
128 else:
129 data['toolbar_style'] = ToolBar.STYLE_TEXT_ONLY
130 toolbar.set_toolbar_style(ToolBar.STYLE_TEXT_ONLY)
131 toolbar.setVisible(data['visible'])
133 toolbar_area = decode_toolbar_area(data['area'])
134 if data['break']:
135 widget.addToolBarBreak(toolbar_area)
136 widget.addToolBar(toolbar_area, toolbar)
138 # floating toolbars must be set after added
139 if data['float']:
140 toolbar.setWindowFlags(Qt.Tool | Qt.FramelessWindowHint)
141 toolbar.move(data['x'], data['y'])
143 def export_state(self):
144 result = []
145 widget = self.widget
146 toolbars = widget.findChildren(ToolBar)
148 for toolbar in toolbars:
149 toolbar_area = widget.toolBarArea(toolbar)
150 if toolbar_area == Qt.NoToolBarArea:
151 continue # filter out removed toolbars
152 items = [x.data() for x in toolbar.actions()]
153 # show_icons is for backwards compatibility with git-cola <= 3.11.0
154 show_icons = toolbar.toolbar_style() != ToolBar.STYLE_TEXT_ONLY
155 result.append({
156 'name': toolbar.windowTitle(),
157 'area': encode_toolbar_area(toolbar_area),
158 'break': widget.toolBarBreak(toolbar),
159 'float': toolbar.isFloating(),
160 'x': toolbar.pos().x(),
161 'y': toolbar.pos().y(),
162 'width': toolbar.width(),
163 'height': toolbar.height(),
164 'show_icons': show_icons,
165 'toolbar_style': toolbar.toolbar_style(),
166 'visible': toolbar.isVisible(),
167 'items': items,
170 return result
173 class ToolBar(QtWidgets.QToolBar):
174 SEPARATOR = 'Separator'
175 STYLE_FOLLOW_SYSTEM = 'follow-system'
176 STYLE_ICON_ONLY = 'icon'
177 STYLE_TEXT_ONLY = 'text'
178 STYLE_TEXT_BESIDE_ICON = 'text-beside-icon'
179 STYLE_TEXT_UNDER_ICON = 'text-under-icon'
180 STYLE_NAMES = [
181 N_('Follow System Style'),
182 N_('Icon Only'),
183 N_('Text Only'),
184 N_('Text Beside Icon'),
185 N_('Text Under Icon'),
187 STYLE_SYMBOLS = [
188 STYLE_FOLLOW_SYSTEM,
189 STYLE_ICON_ONLY,
190 STYLE_TEXT_ONLY,
191 STYLE_TEXT_BESIDE_ICON,
192 STYLE_TEXT_UNDER_ICON,
195 @staticmethod
196 def create(context, name):
197 return ToolBar(context, name, TREE_LAYOUT, COMMANDS)
199 def __init__(self, context, title, tree_layout, toolbar_commands):
200 QtWidgets.QToolBar.__init__(self)
201 self.setWindowTitle(title)
202 self.setObjectName(title)
203 self.setToolButtonStyle(Qt.ToolButtonFollowStyle)
205 self.context = context
206 self.tree_layout = tree_layout
207 self.commands = toolbar_commands
209 def set_toolbar_style(self, style_id):
210 style_to_qt = {
211 self.STYLE_FOLLOW_SYSTEM: Qt.ToolButtonFollowStyle,
212 self.STYLE_ICON_ONLY: Qt.ToolButtonIconOnly,
213 self.STYLE_TEXT_ONLY: Qt.ToolButtonTextOnly,
214 self.STYLE_TEXT_BESIDE_ICON: Qt.ToolButtonTextBesideIcon,
215 self.STYLE_TEXT_UNDER_ICON: Qt.ToolButtonTextUnderIcon,
217 default = Qt.ToolButtonFollowStyle
218 return self.setToolButtonStyle(style_to_qt.get(style_id, default))
220 def toolbar_style(self):
221 qt_to_style = {
222 Qt.ToolButtonFollowStyle: self.STYLE_FOLLOW_SYSTEM,
223 Qt.ToolButtonIconOnly: self.STYLE_ICON_ONLY,
224 Qt.ToolButtonTextOnly: self.STYLE_TEXT_ONLY,
225 Qt.ToolButtonTextBesideIcon: self.STYLE_TEXT_BESIDE_ICON,
226 Qt.ToolButtonTextUnderIcon: self.STYLE_TEXT_UNDER_ICON,
228 default = self.STYLE_FOLLOW_SYSTEM
229 return qt_to_style.get(self.toolButtonStyle(), default)
231 def load_items(self, items):
232 for data in items:
233 self.add_action_from_data(data)
235 def add_action_from_data(self, data):
236 parent = data['parent']
237 child = data['child']
238 child = RENAMED.get(child, child)
240 if child == self.SEPARATOR:
241 toolbar_action = self.addSeparator()
242 toolbar_action.setData(data)
243 return
245 tree_items = self.tree_layout.get(parent, [])
246 if child in tree_items and child in self.commands:
247 command = self.commands[child]
248 title = N_(command['title'])
249 callback = partial(command['action'], self.context)
251 tooltip = command.get('tooltip', None)
252 icon = None
253 command_icon = command.get('icon', None)
254 if command_icon:
255 icon = getattr(icons, command_icon, None)
256 if callable(icon):
257 icon = icon()
258 if icon:
259 toolbar_action = self.addAction(icon, title, callback)
260 else:
261 toolbar_action = self.addAction(title, callback)
263 toolbar_action.setData(data)
265 tooltip = command.get('tooltip', None)
266 if tooltip:
267 toolbar_action.setToolTip(f'{title}\n{tooltip}')
269 def delete_toolbar(self):
270 self.parent().removeToolBar(self)
272 def contextMenuEvent(self, event):
273 menu = QtWidgets.QMenu()
274 tool_config = menu.addAction(N_('Configure Toolbar'), partial(configure, self))
275 tool_config.setIcon(icons.configure())
276 tool_delete = menu.addAction(N_('Delete Toolbar'), self.delete_toolbar)
277 tool_delete.setIcon(icons.remove())
279 menu.exec_(event.globalPos())
282 def encode_toolbar_area(toolbar_area):
283 """Encode a Qt::ToolBarArea as a string"""
284 default = 'bottom'
285 return {
286 Qt.LeftToolBarArea: 'left',
287 Qt.RightToolBarArea: 'right',
288 Qt.TopToolBarArea: 'top',
289 Qt.BottomToolBarArea: 'bottom',
290 }.get(toolbar_area, default)
293 def decode_toolbar_area(string):
294 """Decode an encoded toolbar area string into a Qt::ToolBarArea"""
295 default = Qt.BottomToolBarArea
296 return {
297 'left': Qt.LeftToolBarArea,
298 'right': Qt.RightToolBarArea,
299 'top': Qt.TopToolBarArea,
300 'bottom': Qt.BottomToolBarArea,
301 }.get(string, default)
304 class ToolbarView(standard.Dialog):
305 """Provides the git-cola 'ToolBar' configure dialog"""
307 SEPARATOR_TEXT = '----------------------------'
309 def __init__(self, toolbar, parent=None):
310 standard.Dialog.__init__(self, parent)
311 self.setWindowTitle(N_('Configure Toolbar'))
313 self.toolbar = toolbar
314 self.left_list = ToolbarTreeWidget(self)
315 self.right_list = DraggableListWidget(self)
316 self.text_toolbar_name = QtWidgets.QLabel()
317 self.text_toolbar_name.setText(N_('Name'))
318 self.toolbar_name = QtWidgets.QLineEdit()
319 self.toolbar_name.setText(toolbar.windowTitle())
320 self.add_separator = qtutils.create_button(N_('Add Separator'))
321 self.remove_item = qtutils.create_button(N_('Remove Element'))
322 self.toolbar_style_label = QtWidgets.QLabel(N_('Toolbar Style:'))
323 self.toolbar_style = QtWidgets.QComboBox()
324 for style_name in ToolBar.STYLE_NAMES:
325 self.toolbar_style.addItem(style_name)
326 style_idx = get_index_from_style(toolbar.toolbar_style())
327 self.toolbar_style.setCurrentIndex(style_idx)
328 self.apply_button = qtutils.ok_button(N_('Apply'))
329 self.close_button = qtutils.close_button()
330 self.close_button.setDefault(True)
332 self.right_actions = qtutils.hbox(
333 defs.no_margin, defs.spacing, self.add_separator, self.remove_item
335 self.name_layout = qtutils.hbox(
336 defs.no_margin, defs.spacing, self.text_toolbar_name, self.toolbar_name
338 self.left_layout = qtutils.vbox(defs.no_margin, defs.spacing, self.left_list)
339 self.right_layout = qtutils.vbox(
340 defs.no_margin, defs.spacing, self.right_list, self.right_actions
342 self.top_layout = qtutils.hbox(
343 defs.no_margin, defs.spacing, self.left_layout, self.right_layout
345 self.actions_layout = qtutils.hbox(
346 defs.no_margin,
347 defs.spacing,
348 self.toolbar_style_label,
349 self.toolbar_style,
350 qtutils.STRETCH,
351 self.close_button,
352 self.apply_button,
354 self.main_layout = qtutils.vbox(
355 defs.margin,
356 defs.spacing,
357 self.name_layout,
358 self.top_layout,
359 self.actions_layout,
361 self.setLayout(self.main_layout)
363 qtutils.connect_button(self.add_separator, self.add_separator_action)
364 qtutils.connect_button(self.remove_item, self.remove_item_action)
365 qtutils.connect_button(self.apply_button, self.apply_action)
366 qtutils.connect_button(self.close_button, self.accept)
368 self.load_right_items()
369 self.load_left_items()
371 self.init_size(parent=parent)
373 def load_right_items(self):
374 commands = self.toolbar.commands
375 for action in self.toolbar.actions():
376 data = action.data()
377 try:
378 command_name = data['child']
379 except KeyError:
380 continue
381 command_name = RENAMED.get(command_name, command_name)
382 if command_name == self.toolbar.SEPARATOR:
383 self.add_separator_action()
384 continue
385 try:
386 command = commands[command_name]
387 except KeyError:
388 continue
389 title = command['title']
390 icon = command.get('icon', None)
391 tooltip = command.get('tooltip', None)
392 self.right_list.add_item(title, tooltip, data, icon)
394 def load_left_items(self):
395 commands = self.toolbar.commands
396 for parent in self.toolbar.tree_layout:
397 top = self.left_list.insert_top(parent)
398 for item in self.toolbar.tree_layout[parent]:
399 item = RENAMED.get(item, item)
400 try:
401 command = commands[item]
402 except KeyError:
403 continue
404 icon = command.get('icon', None)
405 tooltip = command.get('tooltip', None)
406 child = create_child(parent, item, command['title'], tooltip, icon)
407 top.appendRow(child)
409 top.sortChildren(0, Qt.AscendingOrder)
411 def add_separator_action(self):
412 data = {'parent': None, 'child': self.toolbar.SEPARATOR}
413 self.right_list.add_separator(self.SEPARATOR_TEXT, data)
415 def remove_item_action(self):
416 items = self.right_list.selectedItems()
418 for item in items:
419 self.right_list.takeItem(self.right_list.row(item))
421 def apply_action(self):
422 self.toolbar.clear()
423 style = get_style_from_index(self.toolbar_style.currentIndex())
424 self.toolbar.set_toolbar_style(style)
425 self.toolbar.setWindowTitle(self.toolbar_name.text())
427 for item in self.right_list.get_items():
428 data = item.data(Qt.UserRole)
429 self.toolbar.add_action_from_data(data)
432 def get_style_from_index(index):
433 """Return the symbolic toolbar style name for the given (combobox) index"""
434 return ToolBar.STYLE_SYMBOLS[index]
437 def get_index_from_style(style):
438 """Return the toolbar style (combobox) index for the symbolic name"""
439 return ToolBar.STYLE_SYMBOLS.index(style)
442 class DraggableListMixin:
443 items = []
445 def __init__(self, widget, Base):
446 self.widget = widget
447 self.Base = Base
449 widget.setAcceptDrops(True)
450 widget.setSelectionMode(widget.SingleSelection)
451 widget.setDragEnabled(True)
452 widget.setDropIndicatorShown(True)
454 def dragEnterEvent(self, event):
455 widget = self.widget
456 self.Base.dragEnterEvent(widget, event)
458 def dragMoveEvent(self, event):
459 widget = self.widget
460 self.Base.dragMoveEvent(widget, event)
462 def dragLeaveEvent(self, event):
463 widget = self.widget
464 self.Base.dragLeaveEvent(widget, event)
466 def dropEvent(self, event):
467 widget = self.widget
468 event.setDropAction(Qt.MoveAction)
469 self.Base.dropEvent(widget, event)
471 def get_items(self):
472 widget = self.widget
473 base = self.Base
474 items = [base.item(widget, i) for i in range(base.count(widget))]
476 return items
479 class DraggableListWidget(QtWidgets.QListWidget):
480 Mixin = DraggableListMixin
482 def __init__(self, parent=None):
483 QtWidgets.QListWidget.__init__(self, parent)
485 self.setAcceptDrops(True)
486 self.setSelectionMode(self.SingleSelection)
487 self.setDragEnabled(True)
488 self.setDropIndicatorShown(True)
490 self._mixin = self.Mixin(self, QtWidgets.QListWidget)
492 def dragEnterEvent(self, event):
493 return self._mixin.dragEnterEvent(event)
495 def dragMoveEvent(self, event):
496 return self._mixin.dragMoveEvent(event)
498 def dropEvent(self, event):
499 return self._mixin.dropEvent(event)
501 def add_separator(self, title, data):
502 item = QtWidgets.QListWidgetItem()
503 item.setText(title)
504 item.setData(Qt.UserRole, data)
506 self.addItem(item)
508 def add_item(self, title, tooltip, data, icon):
509 item = QtWidgets.QListWidgetItem()
510 item.setText(N_(title))
511 item.setData(Qt.UserRole, data)
512 if tooltip:
513 item.setToolTip(tooltip)
515 if icon:
516 icon_func = getattr(icons, icon)
517 item.setIcon(icon_func())
519 self.addItem(item)
521 def get_items(self):
522 return self._mixin.get_items()
525 class ToolbarTreeWidget(standard.TreeView):
526 def __init__(self, parent):
527 standard.TreeView.__init__(self, parent)
529 self.setDragEnabled(True)
530 self.setDragDropMode(QtWidgets.QAbstractItemView.DragOnly)
531 self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
532 self.setDropIndicatorShown(True)
533 self.setRootIsDecorated(True)
534 self.setHeaderHidden(True)
535 self.setAlternatingRowColors(False)
536 self.setSortingEnabled(False)
538 self.setModel(QtGui.QStandardItemModel())
540 def insert_top(self, title):
541 item = create_item(title, title)
542 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
544 self.model().insertRow(0, item)
545 self.model().sort(0)
547 return item
550 def create_child(parent, child, title, tooltip, icon):
551 data = {'parent': parent, 'child': child}
552 item = create_item(title, data)
553 if tooltip:
554 item.setToolTip(tooltip)
555 if icon:
556 icon_func = getattr(icons, icon, None)
557 item.setIcon(icon_func())
559 return item
562 def create_item(name, data):
563 item = QtGui.QStandardItem()
565 item.setEditable(False)
566 item.setDragEnabled(True)
567 item.setText(N_(name))
568 item.setData(data, Qt.UserRole)
570 return item