hotkeys: Change "Tree Navigation" to "Navigation"
[git-cola.git] / cola / qtutils.py
blob88ccba57e150c10a420ce91640a8f922ade3e3cb
1 # Copyright (c) 2008 David Aguilar
2 """This module provides miscellaneous Qt utility functions.
3 """
4 import os
6 from PyQt4 import QtGui
7 from PyQt4 import QtCore
8 from PyQt4.QtCore import Qt
9 from PyQt4.QtCore import SIGNAL
11 import cola
12 from cola import core
13 from cola import gitcfg
14 from cola import utils
15 from cola import settings
16 from cola import signals
17 from cola import resources
18 from cola.compat import set
19 from cola.decorators import memoize
22 @memoize
23 def logger():
24 from cola.widgets.log import LogView
25 logview = LogView()
26 cola.notifier().connect(signals.log_cmd, logview.log)
27 return logview
30 def log(status, output):
31 """Sends messages to the log window.
32 """
33 if not output:
34 return
35 cola.notifier().broadcast(signals.log_cmd, status, output)
38 def emit(widget, signal, *args, **opts):
39 """Return a function that emits a signal"""
40 def emitter(*local_args, **local_opts):
41 if args or opts:
42 widget.emit(SIGNAL(signal), *args, **opts)
43 else:
44 widget.emit(SIGNAL(signal), *local_args, **local_opts)
45 return emitter
48 def SLOT(signal, *args, **opts):
49 """
50 Returns a callback that broadcasts a message over the notifier.
52 If the caller of SLOT() provides args or opts then those are
53 used instead of the ones provided by the invoker of the callback.
55 """
56 def broadcast(*local_args, **local_opts):
57 if args or opts:
58 cola.notifier().broadcast(signal, *args, **opts)
59 else:
60 cola.notifier().broadcast(signal, *local_args, **local_opts)
61 return broadcast
64 def connect_action(action, callback):
65 action.connect(action, SIGNAL('triggered()'), callback)
68 def connect_action_bool(action, callback):
69 action.connect(action, SIGNAL('triggered(bool)'), callback)
72 def connect_button(button, callback):
73 button.connect(button, SIGNAL('clicked()'), callback)
76 def relay_button(button, signal):
77 connect_button(button, SLOT(signal))
80 def relay_signal(parent, child, signal):
81 """Relay a signal from the child widget through the parent"""
82 def relay_slot(*args, **opts):
83 parent.emit(signal, *args, **opts)
84 parent.connect(child, signal, relay_slot)
85 return relay_slot
88 def active_window():
89 return QtGui.QApplication.activeWindow()
92 def prompt(msg, title=None, text=''):
93 """Presents the user with an input widget and returns the input."""
94 if title is None:
95 title = msg
96 msg = tr(msg)
97 title = tr(title)
98 result = QtGui.QInputDialog.getText(active_window(), msg, title, text=text)
99 return (unicode(result[0]), result[1])
102 def create_listwidget_item(text, filename):
103 """Creates a QListWidgetItem with text and the icon at filename."""
104 item = QtGui.QListWidgetItem()
105 item.setIcon(QtGui.QIcon(filename))
106 item.setText(text)
107 return item
110 def create_treewidget_item(text, filename):
111 """Creates a QTreeWidgetItem with text and the icon at filename."""
112 icon = cached_icon_from_path(filename)
113 item = QtGui.QTreeWidgetItem()
114 item.setIcon(0, icon)
115 item.setText(0, text)
116 return item
119 @memoize
120 def cached_icon_from_path(filename):
121 return QtGui.QIcon(filename)
124 def confirm(title, text, informative_text, ok_text,
125 icon=None, default=True):
126 """Confirm that an action should take place"""
127 if icon is None:
128 icon = ok_icon()
129 elif icon and isinstance(icon, basestring):
130 icon = QtGui.QIcon(icon)
131 msgbox = QtGui.QMessageBox(active_window())
132 msgbox.setWindowTitle(tr(title))
133 msgbox.setText(tr(text))
134 msgbox.setInformativeText(tr(informative_text))
135 ok = msgbox.addButton(tr(ok_text), QtGui.QMessageBox.ActionRole)
136 ok.setIcon(icon)
137 cancel = msgbox.addButton(QtGui.QMessageBox.Cancel)
138 if default:
139 msgbox.setDefaultButton(ok)
140 else:
141 msgbox.setDefaultButton(cancel)
142 msgbox.exec_()
143 return msgbox.clickedButton() == ok
146 def critical(title, message=None, details=None):
147 """Show a warning with the provided title and message."""
148 if message is None:
149 message = title
150 title = tr(title)
151 message = tr(message)
152 mbox = QtGui.QMessageBox(active_window())
153 mbox.setWindowTitle(title)
154 mbox.setTextFormat(QtCore.Qt.PlainText)
155 mbox.setText(message)
156 mbox.setIcon(QtGui.QMessageBox.Critical)
157 mbox.setStandardButtons(QtGui.QMessageBox.Close)
158 mbox.setDefaultButton(QtGui.QMessageBox.Close)
159 if details:
160 mbox.setDetailedText(details)
161 mbox.exec_()
164 def information(title, message=None, details=None, informative_text=None):
165 """Show information with the provided title and message."""
166 if message is None:
167 message = title
168 title = tr(title)
169 message = tr(message)
170 mbox = QtGui.QMessageBox(active_window())
171 mbox.setStandardButtons(QtGui.QMessageBox.Close)
172 mbox.setDefaultButton(QtGui.QMessageBox.Close)
173 mbox.setWindowTitle(title)
174 mbox.setWindowModality(QtCore.Qt.WindowModal)
175 mbox.setTextFormat(QtCore.Qt.PlainText)
176 mbox.setText(message)
177 if informative_text:
178 mbox.setInformativeText(tr(informative_text))
179 if details:
180 mbox.setDetailedText(details)
181 # Render git.svg into a 1-inch wide pixmap
182 pixmap = QtGui.QPixmap(resources.icon('git.svg'))
183 xres = pixmap.physicalDpiX()
184 pixmap = pixmap.scaledToHeight(xres, QtCore.Qt.SmoothTransformation)
185 mbox.setIconPixmap(pixmap)
186 mbox.exec_()
189 def question(title, message, default=True):
190 """Launches a QMessageBox question with the provided title and message.
191 Passing "default=False" will make "No" the default choice."""
192 yes = QtGui.QMessageBox.Yes
193 no = QtGui.QMessageBox.No
194 buttons = yes | no
195 if default:
196 default = yes
197 else:
198 default = no
199 title = tr(title)
200 msg = tr(message)
201 result = (QtGui.QMessageBox
202 .question(active_window(), title, msg, buttons, default))
203 return result == QtGui.QMessageBox.Yes
206 def register_for_signals():
207 # Register globally with the notifier
208 notifier = cola.notifier()
209 notifier.connect(signals.confirm, confirm)
210 notifier.connect(signals.critical, critical)
211 notifier.connect(signals.information, information)
212 notifier.connect(signals.question, question)
213 register_for_signals()
216 def selected_treeitem(tree_widget):
217 """Returns a(id_number, is_selected) for a QTreeWidget."""
218 id_number = None
219 selected = False
220 item = tree_widget.currentItem()
221 if item:
222 id_number = item.data(0, QtCore.Qt.UserRole).toInt()[0]
223 selected = True
224 return(id_number, selected)
227 def selected_row(list_widget):
228 """Returns a(row_number, is_selected) tuple for a QListWidget."""
229 items = list_widget.selectedItems()
230 if not items:
231 return (-1, False)
232 item = items[0]
233 return (list_widget.row(item), True)
236 def selection_list(listwidget, items):
237 """Returns an array of model items that correspond to
238 the selected QListWidget indices."""
239 selected = []
240 itemcount = listwidget.count()
241 widgetitems = [ listwidget.item(idx) for idx in range(itemcount) ]
243 for item, widgetitem in zip(items, widgetitems):
244 if widgetitem.isSelected():
245 selected.append(item)
246 return selected
249 def tree_selection(treeitem, items):
250 """Returns model items that correspond to selected widget indices"""
251 itemcount = treeitem.childCount()
252 widgetitems = [ treeitem.child(idx) for idx in range(itemcount) ]
253 selected = []
254 for item, widgetitem in zip(items[:len(widgetitems)], widgetitems):
255 if widgetitem.isSelected():
256 selected.append(item)
258 return selected
261 def selected_item(list_widget, items):
262 """Returns the selected item in a QListWidget."""
263 widget_items = list_widget.selectedItems()
264 if not widget_items:
265 return None
266 widget_item = widget_items[0]
267 row = list_widget.row(widget_item)
268 if row < len(items):
269 return items[row]
270 else:
271 return None
274 def open_dialog(title, filename=None):
275 """Creates an Open File dialog and returns a filename."""
276 title_tr = tr(title)
277 return unicode(QtGui.QFileDialog
278 .getOpenFileName(active_window(), title_tr, filename))
281 def opendir_dialog(title, path):
282 """Prompts for a directory path"""
284 flags = (QtGui.QFileDialog.ShowDirsOnly |
285 QtGui.QFileDialog.DontResolveSymlinks)
286 title_tr = tr(title)
287 return unicode(QtGui.QFileDialog
288 .getExistingDirectory(active_window(),
289 title_tr, path, flags))
292 def save_as(filename, title='Save As...'):
293 """Creates a Save File dialog and returns a filename."""
294 title_tr = tr(title)
295 return unicode(QtGui.QFileDialog
296 .getSaveFileName(active_window(), title_tr, filename))
299 def icon(basename):
300 """Given a basename returns a QIcon from the corresponding cola icon."""
301 return QtGui.QIcon(resources.icon(basename))
304 def set_clipboard(text):
305 """Sets the copy/paste buffer to text."""
306 if not text:
307 return
308 clipboard = QtGui.QApplication.instance().clipboard()
309 clipboard.setText(text, QtGui.QClipboard.Clipboard)
310 clipboard.setText(text, QtGui.QClipboard.Selection)
313 def add_action(widget, text, fn, *shortcuts):
314 action = QtGui.QAction(tr(text), widget)
315 connect_action(action, fn)
316 if shortcuts:
317 shortcuts = list(set(shortcuts))
318 action.setShortcuts(shortcuts)
319 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
320 widget.addAction(action)
321 return action
324 def set_selected_item(widget, idx):
325 """Sets a the currently selected item to the item at index idx."""
326 if type(widget) is QtGui.QTreeWidget:
327 item = widget.topLevelItem(idx)
328 if item:
329 widget.setItemSelected(item, True)
330 widget.setCurrentItem(item)
333 def add_items(widget, items):
334 """Adds items to a widget."""
335 for item in items:
336 widget.addItem(item)
339 def set_items(widget, items):
340 """Clear the existing widget contents and set the new items."""
341 widget.clear()
342 add_items(widget, items)
345 def tr(txt):
346 """Translate a string into a local language."""
347 if type(txt) is QtCore.QString:
348 # This has already been translated; leave as-is
349 return unicode(txt)
350 return unicode(QtGui.QApplication.instance().translate('', txt))
353 def icon_file(filename, staged=False, untracked=False):
354 """Returns a file path representing a corresponding file path."""
355 if staged:
356 if os.path.exists(core.encode(filename)):
357 ifile = resources.icon('staged.png')
358 else:
359 ifile = resources.icon('removed.png')
360 elif untracked:
361 ifile = resources.icon('untracked.png')
362 else:
363 ifile = utils.file_icon(core.encode(filename))
364 return ifile
367 def icon_for_file(filename, staged=False, untracked=False):
368 """Returns a QIcon for a particular file path."""
369 ifile = icon_file(filename, staged=staged, untracked=untracked)
370 return icon(ifile)
373 def create_treeitem(filename, staged=False, untracked=False, check=True):
374 """Given a filename, return a QListWidgetItem suitable
375 for adding to a QListWidget. "staged" and "untracked"
376 controls whether to use the appropriate icons."""
377 if check:
378 ifile = icon_file(filename, staged=staged, untracked=untracked)
379 else:
380 ifile = resources.icon('staged.png')
381 return create_treewidget_item(filename, ifile)
384 def update_file_icons(widget, items, staged=True,
385 untracked=False, offset=0):
386 """Populate a QListWidget with custom icon items."""
387 for idx, model_item in enumerate(items):
388 item = widget.item(idx+offset)
389 if item:
390 item.setIcon(icon_for_file(model_item, staged, untracked))
392 @memoize
393 def cached_icon(key):
394 """Maintain a cache of standard icons and return cache entries."""
395 style = QtGui.QApplication.instance().style()
396 return style.standardIcon(key)
399 def dir_icon():
400 """Return a standard icon for a directory."""
401 return cached_icon(QtGui.QStyle.SP_DirIcon)
404 def file_icon():
405 """Return a standard icon for a file."""
406 return cached_icon(QtGui.QStyle.SP_FileIcon)
409 def apply_icon():
410 """Return a standard Apply icon"""
411 return cached_icon(QtGui.QStyle.SP_DialogApplyButton)
414 def new_icon():
415 return cached_icon(QtGui.QStyle.SP_FileDialogNewFolder)
417 def save_icon():
418 """Return a standard Save icon"""
419 return cached_icon(QtGui.QStyle.SP_DialogSaveButton)
423 def ok_icon():
424 """Return a standard Ok icon"""
425 return cached_icon(QtGui.QStyle.SP_DialogOkButton)
428 def open_icon():
429 """Return a standard open directory icon"""
430 return cached_icon(QtGui.QStyle.SP_DirOpenIcon)
433 def open_file_icon():
434 return icon('open.svg')
437 def options_icon():
438 """Return a standard open directory icon"""
439 return icon('options.svg')
442 def dir_close_icon():
443 """Return a standard closed directory icon"""
444 return cached_icon(QtGui.QStyle.SP_DirClosedIcon)
447 def titlebar_close_icon():
448 """Return a dock widget close icon"""
449 return cached_icon(QtGui.QStyle.SP_TitleBarCloseButton)
452 def titlebar_normal_icon():
453 """Return a dock widget close icon"""
454 return cached_icon(QtGui.QStyle.SP_TitleBarNormalButton)
457 def git_icon():
458 return icon('git.svg')
461 def reload_icon():
462 """Returna standard Refresh icon"""
463 return cached_icon(QtGui.QStyle.SP_BrowserReload)
466 def discard_icon():
467 """Return a standard Discard icon"""
468 return cached_icon(QtGui.QStyle.SP_DialogDiscardButton)
471 def close_icon():
472 """Return a standard Close icon"""
473 return cached_icon(QtGui.QStyle.SP_DialogCloseButton)
476 def add_close_action(widget):
477 """Adds close action and shortcuts to a widget."""
478 return add_action(widget, 'Close...',
479 widget.close, QtGui.QKeySequence.Close, 'Ctrl+Q')
482 def center_on_screen(widget):
483 """Move widget to the center of the default screen"""
484 desktop = QtGui.QApplication.instance().desktop()
485 rect = desktop.screenGeometry(QtGui.QCursor().pos())
486 cy = rect.height()/2
487 cx = rect.width()/2
488 widget.move(cx - widget.width()/2, cy - widget.height()/2)
491 def save_state(widget):
492 if gitcfg.instance().get('cola.savewindowsettings', True):
493 settings.Settings().save_gui_state(widget)
496 def export_window_state(widget, state, version):
497 # Save the window state
498 windowstate = widget.saveState(version)
499 state['windowstate'] = unicode(windowstate.toBase64().data())
500 return state
503 def apply_window_state(widget, state, version):
504 # Restore the dockwidget, etc. window state
505 try:
506 windowstate = state['windowstate']
507 widget.restoreState(QtCore.QByteArray.fromBase64(str(windowstate)),
508 version)
509 except KeyError:
510 pass
513 def apply_state(widget):
514 state = settings.Settings().get_gui_state(widget)
515 widget.apply_state(state)
516 return bool(state)
519 @memoize
520 def theme_icon(name):
521 """Grab an icon from the current theme with a fallback
523 Support older versions of Qt by catching AttributeError and
524 falling back to our default icons.
527 try:
528 base, ext = os.path.splitext(name)
529 qicon = QtGui.QIcon.fromTheme(base)
530 if not qicon.isNull():
531 return qicon
532 except AttributeError:
533 pass
534 return icon(name)
537 def default_monospace_font():
538 font = QtGui.QFont()
539 family = 'Monospace'
540 if utils.is_darwin():
541 family = 'Monaco'
542 font.setFamily(family)
543 return font