Merge pull request #1209 from living180/tmp_filename
[git-cola.git] / cola / widgets / toolbar.py
blob1253f2bfc1a4cbf5b76a8ab271d1be2d1fcf3487
1 from __future__ import absolute_import, division, print_function, unicode_literals
2 from functools import partial
4 from qtpy import QtGui
5 from qtpy.QtCore import Qt
6 from qtpy import QtWidgets
8 from ..i18n import N_
9 from ..widgets import standard
10 from .. import icons
11 from .. import qtutils
12 from .toolbarcmds import COMMANDS
13 from . import defs
15 TREE_LAYOUT = {
16 'Others': ['Others::LaunchEditor', 'Others::RevertUnstagedEdits'],
17 'File': [
18 'File::NewRepo',
19 'File::OpenRepo',
20 'File::OpenRepoNewWindow',
21 'File::Refresh',
22 'File::EditRemotes',
23 'File::RecentModified',
24 'File::SaveAsTarZip',
25 'File::ApplyPatches',
26 'File::ExportPatches',
28 'Actions': [
29 'Actions::Fetch',
30 'Actions::Pull',
31 'Actions::Push',
32 'Actions::Stash',
33 'Actions::CreateTag',
34 'Actions::CherryPick',
35 'Actions::Merge',
36 'Actions::AbortMerge',
37 'Actions::UpdateSubmodules',
38 'Actions::Grep',
39 'Actions::Search',
41 'Commit@@verb': [
42 'Commit::Stage',
43 'Commit::AmendLast',
44 'Commit::UndoLastCommit',
45 'Commit::StageAll',
46 'Commit::UnstageAll',
47 'Commit::Unstage',
48 'Commit::LoadCommitMessage',
49 'Commit::GetCommitMessageTemplate',
51 'Diff': ['Diff::Difftool', 'Diff::Expression', 'Diff::Branches', 'Diff::Diffstat'],
52 'Branch': [
53 'Branch::Review',
54 'Branch::Create',
55 'Branch::Checkout',
56 'Branch::Delete',
57 'Branch::DeleteRemote',
58 'Branch::Rename',
59 'Branch::BrowseCurrent',
60 'Branch::BrowseOther',
61 'Branch::VisualizeCurrent',
62 'Branch::VisualizeAll',
64 'Reset': [
65 'Commit::UndoLastCommit',
66 'Commit::UnstageAll',
67 'Actions::ResetSoft',
68 'Actions::ResetMixed',
69 'Actions::RestoreWorktree',
70 'Actions::ResetKeep',
71 'Actions::ResetHard',
73 'View': ['View::DAG', 'View::FileBrowser'],
77 def configure(toolbar, parent=None):
78 """Launches the Toolbar configure dialog"""
79 if not parent:
80 parent = qtutils.active_window()
81 view = ToolbarView(toolbar, parent)
82 view.show()
83 return view
86 def get_toolbars(widget):
87 return widget.findChildren(ToolBar)
90 def add_toolbar(context, widget):
91 toolbars = get_toolbars(widget)
92 name = 'ToolBar%d' % (len(toolbars) + 1)
93 toolbar = ToolBar.create(context, name)
94 widget.addToolBar(toolbar)
95 configure(toolbar)
98 class ToolBarState(object):
99 """export_state() and apply_state() providers for toolbars"""
101 def __init__(self, context, widget):
102 """widget must be a QMainWindow for toolBarArea(), etc."""
103 self.context = context
104 self.widget = widget
106 def apply_state(self, toolbars):
107 context = self.context
108 widget = self.widget
110 for data in toolbars:
111 toolbar = ToolBar.create(context, data['name'])
112 toolbar.load_items(data['items'])
113 try:
114 toolbar.set_toolbar_style(data['toolbar_style'])
115 except KeyError:
116 # Maintain compatibility for toolbars created in git-cola <= 3.11.0
117 if data['show_icons']:
118 data['toolbar_style'] = ToolBar.STYLE_FOLLOW_SYSTEM
119 toolbar.set_toolbar_style(ToolBar.STYLE_FOLLOW_SYSTEM)
120 else:
121 data['toolbar_style'] = ToolBar.STYLE_TEXT_ONLY
122 toolbar.set_toolbar_style(ToolBar.STYLE_TEXT_ONLY)
123 toolbar.setVisible(data['visible'])
125 toolbar_area = decode_toolbar_area(data['area'])
126 if data['break']:
127 widget.addToolBarBreak(toolbar_area)
128 widget.addToolBar(toolbar_area, toolbar)
130 # floating toolbars must be set after added
131 if data['float']:
132 toolbar.setWindowFlags(Qt.Tool | Qt.FramelessWindowHint)
133 toolbar.move(data['x'], data['y'])
134 # TODO: handle changed width when exists more than one toolbar in
135 # an area
137 def export_state(self):
138 result = []
139 widget = self.widget
140 toolbars = widget.findChildren(ToolBar)
142 for toolbar in toolbars:
143 toolbar_area = widget.toolBarArea(toolbar)
144 if toolbar_area == Qt.NoToolBarArea:
145 continue # filter out removed toolbars
146 items = [x.data() for x in toolbar.actions()]
148 result.append(
150 'name': toolbar.windowTitle(),
151 'area': encode_toolbar_area(toolbar_area),
152 'break': widget.toolBarBreak(toolbar),
153 'float': toolbar.isFloating(),
154 'x': toolbar.pos().x(),
155 'y': toolbar.pos().y(),
156 'width': toolbar.width(),
157 'height': toolbar.height(),
158 # show_icons kept for backwards compatibility in git-cola <= 3.11.0
159 'show_icons': toolbar.toolbar_style() != ToolBar.STYLE_TEXT_ONLY,
160 'toolbar_style': toolbar.toolbar_style(),
161 'visible': toolbar.isVisible(),
162 'items': items,
166 return result
169 class ToolBar(QtWidgets.QToolBar):
170 SEPARATOR = 'Separator'
171 STYLE_FOLLOW_SYSTEM = 'follow-system'
172 STYLE_ICON_ONLY = 'icon'
173 STYLE_TEXT_ONLY = 'text'
174 STYLE_TEXT_BESIDE_ICON = 'text-beside-icon'
175 STYLE_TEXT_UNDER_ICON = 'text-under-icon'
176 STYLE_NAMES = [
177 N_('Follow System Style'),
178 N_('Icon Only'),
179 N_('Text Only'),
180 N_('Text Beside Icon'),
181 N_('Text Under Icon'),
183 STYLE_SYMBOLS = [
184 STYLE_FOLLOW_SYSTEM,
185 STYLE_ICON_ONLY,
186 STYLE_TEXT_ONLY,
187 STYLE_TEXT_BESIDE_ICON,
188 STYLE_TEXT_UNDER_ICON,
191 @staticmethod
192 def create(context, name):
193 return ToolBar(context, name, TREE_LAYOUT, COMMANDS)
195 def __init__(self, context, title, tree_layout, toolbar_commands):
196 QtWidgets.QToolBar.__init__(self)
197 self.setWindowTitle(title)
198 self.setObjectName(title)
199 self.setToolButtonStyle(Qt.ToolButtonFollowStyle)
201 self.context = context
202 self.tree_layout = tree_layout
203 self.commands = toolbar_commands
205 def set_toolbar_style(self, style_id):
206 style_to_qt = {
207 self.STYLE_FOLLOW_SYSTEM: Qt.ToolButtonFollowStyle,
208 self.STYLE_ICON_ONLY: Qt.ToolButtonIconOnly,
209 self.STYLE_TEXT_ONLY: Qt.ToolButtonTextOnly,
210 self.STYLE_TEXT_BESIDE_ICON: Qt.ToolButtonTextBesideIcon,
211 self.STYLE_TEXT_UNDER_ICON: Qt.ToolButtonTextUnderIcon,
213 default = Qt.ToolButtonFollowStyle
214 return self.setToolButtonStyle(style_to_qt.get(style_id, default))
216 def toolbar_style(self):
217 qt_to_style = {
218 Qt.ToolButtonFollowStyle: self.STYLE_FOLLOW_SYSTEM,
219 Qt.ToolButtonIconOnly: self.STYLE_ICON_ONLY,
220 Qt.ToolButtonTextOnly: self.STYLE_TEXT_ONLY,
221 Qt.ToolButtonTextBesideIcon: self.STYLE_TEXT_BESIDE_ICON,
222 Qt.ToolButtonTextUnderIcon: self.STYLE_TEXT_UNDER_ICON,
224 default = self.STYLE_FOLLOW_SYSTEM
225 return qt_to_style.get(self.toolButtonStyle(), default)
227 def load_items(self, items):
228 for data in items:
229 self.add_action_from_data(data)
231 def add_action_from_data(self, data):
232 parent = data['parent']
233 child = data['child']
235 if child == self.SEPARATOR:
236 toolbar_action = self.addSeparator()
237 toolbar_action.setData(data)
238 return
240 tree_items = self.tree_layout.get(parent, [])
241 if child in tree_items and child in self.commands:
242 command = self.commands[child]
243 title = N_(command['title'])
244 callback = partial(command['action'], self.context)
246 icon = None
247 command_icon = command.get('icon', None)
248 if command_icon:
249 icon = getattr(icons, command_icon, None)
250 if callable(icon):
251 icon = icon()
252 if icon:
253 toolbar_action = self.addAction(icon, title, callback)
254 else:
255 toolbar_action = self.addAction(title, callback)
257 toolbar_action.setData(data)
259 tooltip = command.get('tooltip', None)
260 if tooltip:
261 toolbar_action.setToolTip('%s\n%s' % (title, tooltip))
263 def delete_toolbar(self):
264 self.parent().removeToolBar(self)
266 def contextMenuEvent(self, event):
267 menu = QtWidgets.QMenu()
268 tool_config = menu.addAction(N_('Configure Toolbar'), partial(configure, self))
269 tool_config.setIcon(icons.configure())
270 tool_delete = menu.addAction(N_('Delete Toolbar'), self.delete_toolbar)
271 tool_delete.setIcon(icons.remove())
273 menu.exec_(event.globalPos())
276 def encode_toolbar_area(toolbar_area):
277 """Encode a Qt::ToolBarArea as a string"""
278 default = 'bottom'
279 return {
280 Qt.LeftToolBarArea: 'left',
281 Qt.RightToolBarArea: 'right',
282 Qt.TopToolBarArea: 'top',
283 Qt.BottomToolBarArea: 'bottom',
284 }.get(toolbar_area, default)
287 def decode_toolbar_area(string):
288 """Decode an encoded toolbar area string into a Qt::ToolBarArea"""
289 default = Qt.BottomToolBarArea
290 return {
291 'left': Qt.LeftToolBarArea,
292 'right': Qt.RightToolBarArea,
293 'top': Qt.TopToolBarArea,
294 'bottom': Qt.BottomToolBarArea,
295 }.get(string, default)
298 class ToolbarView(standard.Dialog):
299 """Provides the git-cola 'ToolBar' configure dialog"""
301 SEPARATOR_TEXT = '----------------------------'
303 def __init__(self, toolbar, parent=None):
304 standard.Dialog.__init__(self, parent)
305 self.setWindowTitle(N_('Configure Toolbar'))
307 self.toolbar = toolbar
308 self.left_list = ToolbarTreeWidget(self)
309 self.right_list = DraggableListWidget(self)
310 self.text_toolbar_name = QtWidgets.QLabel()
311 self.text_toolbar_name.setText(N_('Name'))
312 self.toolbar_name = QtWidgets.QLineEdit()
313 self.toolbar_name.setText(toolbar.windowTitle())
314 self.add_separator = qtutils.create_button(N_('Add Separator'))
315 self.remove_item = qtutils.create_button(N_('Remove Element'))
316 self.toolbar_style_label = QtWidgets.QLabel(N_('Toolbar Style:'))
317 self.toolbar_style = QtWidgets.QComboBox()
318 for style_name in ToolBar.STYLE_NAMES:
319 self.toolbar_style.addItem(style_name)
320 style_idx = get_index_from_style(toolbar.toolbar_style())
321 self.toolbar_style.setCurrentIndex(style_idx)
322 self.apply_button = qtutils.ok_button(N_('Apply'))
323 self.close_button = qtutils.close_button()
324 self.close_button.setDefault(True)
326 self.right_actions = qtutils.hbox(
327 defs.no_margin, defs.spacing, self.add_separator, self.remove_item
329 self.name_layout = qtutils.hbox(
330 defs.no_margin, defs.spacing, self.text_toolbar_name, self.toolbar_name
332 self.left_layout = qtutils.vbox(defs.no_margin, defs.spacing, self.left_list)
333 self.right_layout = qtutils.vbox(
334 defs.no_margin, defs.spacing, self.right_list, self.right_actions
336 self.top_layout = qtutils.hbox(
337 defs.no_margin, defs.spacing, self.left_layout, self.right_layout
339 self.actions_layout = qtutils.hbox(
340 defs.no_margin,
341 defs.spacing,
342 self.toolbar_style_label,
343 self.toolbar_style,
344 qtutils.STRETCH,
345 self.close_button,
346 self.apply_button,
348 self.main_layout = qtutils.vbox(
349 defs.margin,
350 defs.spacing,
351 self.name_layout,
352 self.top_layout,
353 self.actions_layout,
355 self.setLayout(self.main_layout)
357 qtutils.connect_button(self.add_separator, self.add_separator_action)
358 qtutils.connect_button(self.remove_item, self.remove_item_action)
359 qtutils.connect_button(self.apply_button, self.apply_action)
360 qtutils.connect_button(self.close_button, self.accept)
362 self.load_right_items()
363 self.load_left_items()
365 self.init_size(parent=parent)
367 def load_right_items(self):
368 commands = self.toolbar.commands
369 for action in self.toolbar.actions():
370 data = action.data()
371 if data['child'] == self.toolbar.SEPARATOR:
372 self.add_separator_action()
373 else:
374 try:
375 child_data = data['child']
376 command = commands[child_data]
377 except KeyError:
378 pass
379 title = command['title']
380 icon = command.get('icon', None)
381 tooltip = command.get('tooltip', None)
382 self.right_list.add_item(title, tooltip, data, icon)
384 def load_left_items(self):
385 commands = self.toolbar.commands
386 for parent in self.toolbar.tree_layout:
387 top = self.left_list.insert_top(parent)
388 for item in self.toolbar.tree_layout[parent]:
389 try:
390 command = commands[item]
391 except KeyError:
392 pass
393 icon = command.get('icon', None)
394 tooltip = command.get('tooltip', None)
395 child = create_child(parent, item, command['title'], tooltip, icon)
396 top.appendRow(child)
398 top.sortChildren(0, Qt.AscendingOrder)
400 def add_separator_action(self):
401 data = {'parent': None, 'child': self.toolbar.SEPARATOR}
402 self.right_list.add_separator(self.SEPARATOR_TEXT, data)
404 def remove_item_action(self):
405 items = self.right_list.selectedItems()
407 for item in items:
408 self.right_list.takeItem(self.right_list.row(item))
410 def apply_action(self):
411 self.toolbar.clear()
412 style = get_style_from_index(self.toolbar_style.currentIndex())
413 self.toolbar.set_toolbar_style(style)
414 self.toolbar.setWindowTitle(self.toolbar_name.text())
416 for item in self.right_list.get_items():
417 data = item.data(Qt.UserRole)
418 self.toolbar.add_action_from_data(data)
421 def get_style_from_index(index):
422 """Return the symbolic toolbar style name for the given (combobox) index"""
423 return ToolBar.STYLE_SYMBOLS[index]
426 def get_index_from_style(style):
427 """Return the toolbar style (combobox) index for the symbolic name"""
428 return ToolBar.STYLE_SYMBOLS.index(style)
431 class DraggableListMixin(object):
432 items = []
434 def __init__(self, widget, Base):
435 self.widget = widget
436 self.Base = Base
438 widget.setAcceptDrops(True)
439 widget.setSelectionMode(widget.SingleSelection)
440 widget.setDragEnabled(True)
441 widget.setDropIndicatorShown(True)
443 def dragEnterEvent(self, event):
444 widget = self.widget
445 self.Base.dragEnterEvent(widget, event)
447 def dragMoveEvent(self, event):
448 widget = self.widget
449 self.Base.dragMoveEvent(widget, event)
451 def dragLeaveEvent(self, event):
452 widget = self.widget
453 self.Base.dragLeaveEvent(widget, event)
455 def dropEvent(self, event):
456 widget = self.widget
457 event.setDropAction(Qt.MoveAction)
458 self.Base.dropEvent(widget, event)
460 def get_items(self):
461 widget = self.widget
462 base = self.Base
463 items = [base.item(widget, i) for i in range(base.count(widget))]
465 return items
468 # pylint: disable=too-many-ancestors
469 class DraggableListWidget(QtWidgets.QListWidget):
470 Mixin = DraggableListMixin
472 def __init__(self, parent=None):
473 QtWidgets.QListWidget.__init__(self, parent)
475 self.setAcceptDrops(True)
476 self.setSelectionMode(self.SingleSelection)
477 self.setDragEnabled(True)
478 self.setDropIndicatorShown(True)
480 self._mixin = self.Mixin(self, QtWidgets.QListWidget)
482 def dragEnterEvent(self, event):
483 return self._mixin.dragEnterEvent(event)
485 def dragMoveEvent(self, event):
486 return self._mixin.dragMoveEvent(event)
488 def dropEvent(self, event):
489 return self._mixin.dropEvent(event)
491 def add_separator(self, title, data):
492 item = QtWidgets.QListWidgetItem()
493 item.setText(title)
494 item.setData(Qt.UserRole, data)
496 self.addItem(item)
498 def add_item(self, title, tooltip, data, icon):
499 item = QtWidgets.QListWidgetItem()
500 item.setText(N_(title))
501 item.setData(Qt.UserRole, data)
502 if tooltip:
503 item.setToolTip(tooltip)
505 if icon:
506 icon_func = getattr(icons, icon)
507 item.setIcon(icon_func())
509 self.addItem(item)
511 def get_items(self):
512 return self._mixin.get_items()
515 # pylint: disable=too-many-ancestors
516 class ToolbarTreeWidget(standard.TreeView):
517 def __init__(self, parent):
518 standard.TreeView.__init__(self, parent)
520 self.setDragEnabled(True)
521 self.setDragDropMode(QtWidgets.QAbstractItemView.DragOnly)
522 self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
523 self.setDropIndicatorShown(True)
524 self.setRootIsDecorated(True)
525 self.setHeaderHidden(True)
526 self.setAlternatingRowColors(False)
527 self.setSortingEnabled(False)
529 self.setModel(QtGui.QStandardItemModel())
531 def insert_top(self, title):
532 item = create_item(title, title)
533 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
535 self.model().insertRow(0, item)
536 self.model().sort(0)
538 return item
541 def create_child(parent, child, title, tooltip, icon):
542 data = {'parent': parent, 'child': child}
543 item = create_item(title, data)
544 if tooltip:
545 item.setToolTip(tooltip)
546 if icon:
547 icon_func = getattr(icons, icon, None)
548 item.setIcon(icon_func())
550 return item
553 def create_item(name, data):
554 item = QtGui.QStandardItem()
556 item.setEditable(False)
557 item.setDragEnabled(True)
558 item.setText(N_(name))
559 item.setData(data, Qt.UserRole)
561 return item