main.view: Fix interactive diff font setting
[git-cola.git] / cola / qtutils.py
blob59476b2dd42b7e5b5f67d3fdce021362e0adf421
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
20 from cola.widgets.log import LogView
23 @memoize
24 def logger():
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_button(button, callback):
65 button.connect(button, SIGNAL('clicked()'), callback)
68 def relay_button(button, signal):
69 connect_button(button, SLOT(signal))
72 def relay_signal(parent, child, signal):
73 """Relay a signal from the child widget through the parent"""
74 def relay_slot(*args, **opts):
75 parent.emit(signal, *args, **opts)
76 parent.connect(child, signal, relay_slot)
77 return relay_slot
80 def active_window():
81 return QtGui.QApplication.activeWindow()
84 def prompt(msg, title=None, text=None):
85 """Presents the user with an input widget and returns the input."""
86 if title is None:
87 title = msg
88 msg = tr(msg)
89 title = tr(title)
90 result = QtGui.QInputDialog.getText(active_window(), msg, title, text=text)
91 return (unicode(result[0]), result[1])
94 def create_listwidget_item(text, filename):
95 """Creates a QListWidgetItem with text and the icon at filename."""
96 item = QtGui.QListWidgetItem()
97 item.setIcon(QtGui.QIcon(filename))
98 item.setText(text)
99 return item
102 def create_treewidget_item(text, filename):
103 """Creates a QTreeWidgetItem with text and the icon at filename."""
104 icon = cached_icon_from_path(filename)
105 item = QtGui.QTreeWidgetItem()
106 item.setIcon(0, icon)
107 item.setText(0, text)
108 return item
111 @memoize
112 def cached_icon_from_path(filename):
113 return QtGui.QIcon(filename)
116 def confirm(title, text, informative_text, ok_text,
117 icon=None, default=True):
118 """Confirm that an action should take place"""
119 if icon is None:
120 icon = ok_icon()
121 elif icon and isinstance(icon, basestring):
122 icon = QtGui.QIcon(icon)
123 msgbox = QtGui.QMessageBox(active_window())
124 msgbox.setWindowTitle(tr(title))
125 msgbox.setText(tr(text))
126 msgbox.setInformativeText(tr(informative_text))
127 ok = msgbox.addButton(tr(ok_text), QtGui.QMessageBox.ActionRole)
128 ok.setIcon(icon)
129 cancel = msgbox.addButton(QtGui.QMessageBox.Cancel)
130 if default:
131 msgbox.setDefaultButton(ok)
132 else:
133 msgbox.setDefaultButton(cancel)
134 msgbox.exec_()
135 return msgbox.clickedButton() == ok
138 def critical(title, message=None, details=None):
139 """Show a warning with the provided title and message."""
140 if message is None:
141 message = title
142 title = tr(title)
143 message = tr(message)
144 mbox = QtGui.QMessageBox(active_window())
145 mbox.setWindowTitle(title)
146 mbox.setTextFormat(QtCore.Qt.PlainText)
147 mbox.setText(message)
148 mbox.setIcon(QtGui.QMessageBox.Critical)
149 mbox.setStandardButtons(QtGui.QMessageBox.Close)
150 mbox.setDefaultButton(QtGui.QMessageBox.Close)
151 if details:
152 mbox.setDetailedText(details)
153 mbox.exec_()
156 def information(title, message=None, details=None, informative_text=None):
157 """Show information with the provided title and message."""
158 if message is None:
159 message = title
160 title = tr(title)
161 message = tr(message)
162 mbox = QtGui.QMessageBox(active_window())
163 mbox.setStandardButtons(QtGui.QMessageBox.Close)
164 mbox.setDefaultButton(QtGui.QMessageBox.Close)
165 mbox.setWindowTitle(title)
166 mbox.setWindowModality(QtCore.Qt.WindowModal)
167 mbox.setTextFormat(QtCore.Qt.PlainText)
168 mbox.setText(message)
169 if informative_text:
170 mbox.setInformativeText(tr(informative_text))
171 if details:
172 mbox.setDetailedText(details)
173 # Render git.svg into a 1-inch wide pixmap
174 pixmap = QtGui.QPixmap(resources.icon('git.svg'))
175 xres = pixmap.physicalDpiX()
176 pixmap = pixmap.scaledToHeight(xres, QtCore.Qt.SmoothTransformation)
177 mbox.setIconPixmap(pixmap)
178 mbox.exec_()
181 def question(title, message, default=True):
182 """Launches a QMessageBox question with the provided title and message.
183 Passing "default=False" will make "No" the default choice."""
184 yes = QtGui.QMessageBox.Yes
185 no = QtGui.QMessageBox.No
186 buttons = yes | no
187 if default:
188 default = yes
189 else:
190 default = no
191 title = tr(title)
192 msg = tr(message)
193 result = (QtGui.QMessageBox
194 .question(active_window(), title, msg, buttons, default))
195 return result == QtGui.QMessageBox.Yes
198 def register_for_signals():
199 # Register globally with the notifier
200 notifier = cola.notifier()
201 notifier.connect(signals.confirm, confirm)
202 notifier.connect(signals.critical, critical)
203 notifier.connect(signals.information, information)
204 notifier.connect(signals.question, question)
205 register_for_signals()
208 def selected_treeitem(tree_widget):
209 """Returns a(id_number, is_selected) for a QTreeWidget."""
210 id_number = None
211 selected = False
212 item = tree_widget.currentItem()
213 if item:
214 id_number = item.data(0, QtCore.Qt.UserRole).toInt()[0]
215 selected = True
216 return(id_number, selected)
219 def selected_row(list_widget):
220 """Returns a(row_number, is_selected) tuple for a QListWidget."""
221 items = list_widget.selectedItems()
222 if not items:
223 return (-1, False)
224 item = items[0]
225 return (list_widget.row(item), True)
228 def selection_list(listwidget, items):
229 """Returns an array of model items that correspond to
230 the selected QListWidget indices."""
231 selected = []
232 itemcount = listwidget.count()
233 widgetitems = [ listwidget.item(idx) for idx in range(itemcount) ]
235 for item, widgetitem in zip(items, widgetitems):
236 if widgetitem.isSelected():
237 selected.append(item)
238 return selected
241 def tree_selection(treeitem, items):
242 """Returns model items that correspond to selected widget indices"""
243 itemcount = treeitem.childCount()
244 widgetitems = [ treeitem.child(idx) for idx in range(itemcount) ]
245 selected = []
246 for item, widgetitem in zip(items[:len(widgetitems)], widgetitems):
247 if widgetitem.isSelected():
248 selected.append(item)
250 return selected
253 def selected_item(list_widget, items):
254 """Returns the selected item in a QListWidget."""
255 widget_items = list_widget.selectedItems()
256 if not widget_items:
257 return None
258 widget_item = widget_items[0]
259 row = list_widget.row(widget_item)
260 if row < len(items):
261 return items[row]
262 else:
263 return None
266 def open_dialog(title, filename=None):
267 """Creates an Open File dialog and returns a filename."""
268 title_tr = tr(title)
269 return unicode(QtGui.QFileDialog
270 .getOpenFileName(active_window(), title_tr, filename))
273 def opendir_dialog(title, path):
274 """Prompts for a directory path"""
276 flags = (QtGui.QFileDialog.ShowDirsOnly |
277 QtGui.QFileDialog.DontResolveSymlinks)
278 title_tr = tr(title)
279 return unicode(QtGui.QFileDialog
280 .getExistingDirectory(active_window(),
281 title_tr, path, flags))
284 def save_as(filename, title='Save As...'):
285 """Creates a Save File dialog and returns a filename."""
286 title_tr = tr(title)
287 return unicode(QtGui.QFileDialog
288 .getSaveFileName(active_window(), title_tr, filename))
291 def icon(basename):
292 """Given a basename returns a QIcon from the corresponding cola icon."""
293 return QtGui.QIcon(resources.icon(basename))
296 def set_clipboard(text):
297 """Sets the copy/paste buffer to text."""
298 if not text:
299 return
300 clipboard = QtGui.QApplication.instance().clipboard()
301 clipboard.setText(text, QtGui.QClipboard.Clipboard)
302 clipboard.setText(text, QtGui.QClipboard.Selection)
305 def add_action(widget, text, fn, *shortcuts):
306 action = QtGui.QAction(text, widget)
307 action.connect(action, SIGNAL('triggered()'), fn)
308 if shortcuts:
309 shortcuts = list(set(shortcuts))
310 action.setShortcuts(shortcuts)
311 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
312 widget.addAction(action)
313 return action
316 def set_selected_item(widget, idx):
317 """Sets a the currently selected item to the item at index idx."""
318 if type(widget) is QtGui.QTreeWidget:
319 item = widget.topLevelItem(idx)
320 if item:
321 widget.setItemSelected(item, True)
322 widget.setCurrentItem(item)
325 def add_items(widget, items):
326 """Adds items to a widget."""
327 for item in items:
328 widget.addItem(item)
331 def set_items(widget, items):
332 """Clear the existing widget contents and set the new items."""
333 widget.clear()
334 add_items(widget, items)
337 def tr(txt):
338 """Translate a string into a local language."""
339 if type(txt) is QtCore.QString:
340 # This has already been translated; leave as-is
341 return unicode(txt)
342 return unicode(QtGui.QApplication.instance().translate('', txt))
345 def icon_file(filename, staged=False, untracked=False):
346 """Returns a file path representing a corresponding file path."""
347 if staged:
348 if os.path.exists(core.encode(filename)):
349 ifile = resources.icon('staged.png')
350 else:
351 ifile = resources.icon('removed.png')
352 elif untracked:
353 ifile = resources.icon('untracked.png')
354 else:
355 ifile = utils.file_icon(core.encode(filename))
356 return ifile
359 def icon_for_file(filename, staged=False, untracked=False):
360 """Returns a QIcon for a particular file path."""
361 ifile = icon_file(filename, staged=staged, untracked=untracked)
362 return icon(ifile)
365 def create_treeitem(filename, staged=False, untracked=False, check=True):
366 """Given a filename, return a QListWidgetItem suitable
367 for adding to a QListWidget. "staged" and "untracked"
368 controls whether to use the appropriate icons."""
369 if check:
370 ifile = icon_file(filename, staged=staged, untracked=untracked)
371 else:
372 ifile = resources.icon('staged.png')
373 return create_treewidget_item(filename, ifile)
376 def update_file_icons(widget, items, staged=True,
377 untracked=False, offset=0):
378 """Populate a QListWidget with custom icon items."""
379 for idx, model_item in enumerate(items):
380 item = widget.item(idx+offset)
381 if item:
382 item.setIcon(icon_for_file(model_item, staged, untracked))
384 def set_listwidget_strings(widget, items):
385 """Sets a list widget to the strings passed in items."""
386 widget.clear()
387 add_items(widget, [ QtGui.QListWidgetItem(i) for i in items ])
389 @memoize
390 def cached_icon(key):
391 """Maintain a cache of standard icons and return cache entries."""
392 style = QtGui.QApplication.instance().style()
393 return style.standardIcon(key)
396 def dir_icon():
397 """Return a standard icon for a directory."""
398 return cached_icon(QtGui.QStyle.SP_DirIcon)
401 def file_icon():
402 """Return a standard icon for a file."""
403 return cached_icon(QtGui.QStyle.SP_FileIcon)
406 def apply_icon():
407 """Return a standard Apply icon"""
408 return cached_icon(QtGui.QStyle.SP_DialogApplyButton)
411 def save_icon():
412 """Return a standard Save icon"""
413 return cached_icon(QtGui.QStyle.SP_DialogSaveButton)
416 def ok_icon():
417 """Return a standard Ok icon"""
418 return cached_icon(QtGui.QStyle.SP_DialogOkButton)
421 def open_icon():
422 """Return a standard Save icon"""
423 return cached_icon(QtGui.QStyle.SP_DirOpenIcon)
426 def git_icon():
427 return icon('git.svg')
430 def reload_icon():
431 """Returna standard Refresh icon"""
432 return cached_icon(QtGui.QStyle.SP_BrowserReload)
435 def discard_icon():
436 """Return a standard Discard icon"""
437 return cached_icon(QtGui.QStyle.SP_DialogDiscardButton)
440 def close_icon():
441 """Return a standard Close icon"""
442 return cached_icon(QtGui.QStyle.SP_DialogCloseButton)
445 def add_close_action(widget):
446 """Adds close action and shortcuts to a widget."""
447 return add_action(widget, 'Close...',
448 widget.close, QtGui.QKeySequence.Close, 'Ctrl+Q')
451 def center_on_screen(widget):
452 """Move widget to the center of the default screen"""
453 desktop = QtGui.QApplication.instance().desktop()
454 rect = desktop.screenGeometry(QtGui.QCursor().pos())
455 cy = rect.height()/2
456 cx = rect.width()/2
457 widget.move(cx - widget.width()/2, cy - widget.height()/2)
460 def save_state(widget):
461 if gitcfg.instance().get('cola.savewindowsettings', True):
462 settings.Settings().save_gui_state(widget)
465 def export_window_state(widget, state, version):
466 # Save the window state
467 windowstate = widget.saveState(version)
468 state['windowstate'] = unicode(windowstate.toBase64().data())
469 return state
472 def apply_window_state(widget, state, version):
473 # Restore the dockwidget, etc. window state
474 try:
475 windowstate = state['windowstate']
476 widget.restoreState(QtCore.QByteArray.fromBase64(str(windowstate)),
477 version)
478 except KeyError:
479 pass
482 def apply_state(widget):
483 state = settings.Settings().get_gui_state(widget)
484 widget.apply_state(state)
485 return bool(state)
488 @memoize
489 def theme_icon(name):
490 """Grab an icon from the current theme with a fallback
492 Support older versions of Qt by catching AttributeError and
493 falling back to our default icons.
496 try:
497 base, ext = os.path.splitext(name)
498 qicon = QtGui.QIcon.fromTheme(base)
499 if not qicon.isNull():
500 return qicon
501 except AttributeError:
502 pass
503 return icon(name)