dag: disambiguate between refspec and pathspec
[git-cola.git] / cola / qt.py
blob4a8383256e6d0c5a6dfc3beaba2fa8fcd1d8f0f0
1 import re
3 from PyQt4 import QtGui
4 from PyQt4 import QtCore
5 from PyQt4.QtCore import Qt
6 from PyQt4.QtCore import SIGNAL
7 from PyQt4.QtGui import QFont
8 from PyQt4.QtGui import QSyntaxHighlighter
9 from PyQt4.QtGui import QTextCharFormat
10 from PyQt4.QtGui import QColor
12 from cola import utils
13 from cola import qtutils
14 from cola.i18n import N_
15 from cola.widgets import completion
16 from cola.widgets import defs
19 def create_button(text='', layout=None, tooltip=None, icon=None):
20 """Create a button, set its title, and add it to the parent."""
21 button = QtGui.QPushButton()
22 button.setCursor(Qt.PointingHandCursor)
23 if text:
24 button.setText(text)
25 if icon:
26 button.setIcon(icon)
27 if tooltip is not None:
28 button.setToolTip(tooltip)
29 if layout is not None:
30 layout.addWidget(button)
31 return button
34 def create_action_button(tooltip, icon):
35 button = QtGui.QPushButton()
36 button.setCursor(QtCore.Qt.PointingHandCursor)
37 button.setFlat(True)
38 button.setIcon(icon)
39 button.setFixedSize(QtCore.QSize(16, 16))
40 button.setToolTip(tooltip)
41 return button
44 class DockTitleBarWidget(QtGui.QWidget):
46 def __init__(self, parent, title, stretch=True):
47 QtGui.QWidget.__init__(self, parent)
48 self.label = label = QtGui.QLabel()
49 font = label.font()
50 font.setCapitalization(QtGui.QFont.SmallCaps)
51 label.setFont(font)
52 label.setText(title)
54 self.setCursor(QtCore.Qt.OpenHandCursor)
56 self.close_button = create_action_button(
57 N_('Close'), qtutils.titlebar_close_icon())
59 self.toggle_button = create_action_button(
60 N_('Detach'),qtutils.titlebar_normal_icon())
62 self.corner_layout = QtGui.QHBoxLayout()
63 self.corner_layout.setMargin(defs.no_margin)
64 self.corner_layout.setSpacing(defs.spacing)
66 self.main_layout = QtGui.QHBoxLayout()
67 self.main_layout.setMargin(defs.small_margin)
68 self.main_layout.setSpacing(defs.spacing)
69 self.main_layout.addWidget(label)
70 self.main_layout.addSpacing(defs.spacing)
71 if stretch:
72 self.main_layout.addStretch()
73 self.main_layout.addLayout(self.corner_layout)
74 self.main_layout.addSpacing(defs.spacing)
75 self.main_layout.addWidget(self.toggle_button)
76 self.main_layout.addWidget(self.close_button)
78 self.setLayout(self.main_layout)
80 qtutils.connect_button(self.toggle_button, self.toggle_floating)
81 qtutils.connect_button(self.close_button, self.toggle_visibility)
83 def toggle_floating(self):
84 self.parent().setFloating(not self.parent().isFloating())
85 self.update_tooltips()
87 def toggle_visibility(self):
88 self.parent().toggleViewAction().trigger()
90 def set_title(self, title):
91 self.label.setText(title)
93 def add_corner_widget(self, widget):
94 self.corner_layout.addWidget(widget)
96 def update_tooltips(self):
97 if self.parent().isFloating():
98 tooltip = N_('Attach')
99 else:
100 tooltip = N_('Detach')
101 self.toggle_button.setToolTip(tooltip)
104 def create_dock(title, parent, stretch=True):
105 """Create a dock widget and set it up accordingly."""
106 dock = QtGui.QDockWidget(parent)
107 dock.setWindowTitle(title)
108 dock.setObjectName(title)
109 titlebar = DockTitleBarWidget(dock, title, stretch=stretch)
110 dock.setTitleBarWidget(titlebar)
111 if hasattr(parent, 'dockwidgets'):
112 parent.dockwidgets.append(dock)
113 return dock
116 def create_menu(title, parent):
117 """Create a menu and set its title."""
118 qmenu = QtGui.QMenu(parent)
119 qmenu.setTitle(title)
120 return qmenu
123 def create_toolbutton(text=None, layout=None, tooltip=None, icon=None):
124 button = QtGui.QToolButton()
125 button.setAutoRaise(True)
126 button.setAutoFillBackground(True)
127 button.setCursor(Qt.PointingHandCursor)
128 if icon:
129 button.setIcon(icon)
130 if text:
131 button.setText(text)
132 button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
133 if tooltip:
134 button.setToolTip(tooltip)
135 if layout is not None:
136 layout.addWidget(button)
137 return button
140 class QFlowLayoutWidget(QtGui.QWidget):
142 _horizontal = QtGui.QBoxLayout.LeftToRight
143 _vertical = QtGui.QBoxLayout.TopToBottom
145 def __init__(self, parent):
146 QtGui.QWidget.__init__(self, parent)
147 self._direction = self._vertical
148 self._layout = layout = QtGui.QBoxLayout(self._direction)
149 layout.setSpacing(defs.spacing)
150 layout.setMargin(defs.margin)
151 self.setLayout(layout)
152 policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum,
153 QtGui.QSizePolicy.Minimum)
154 self.setSizePolicy(policy)
155 self.setMinimumSize(QtCore.QSize(1, 1))
156 self.aspect_ratio = 0.8
158 def resizeEvent(self, event):
159 size = event.size()
160 if size.width() * self.aspect_ratio < size.height():
161 dxn = self._vertical
162 else:
163 dxn = self._horizontal
165 if dxn != self._direction:
166 self._direction = dxn
167 self.layout().setDirection(dxn)
170 class ExpandableGroupBox(QtGui.QGroupBox):
171 def __init__(self, parent=None):
172 QtGui.QGroupBox.__init__(self, parent)
173 self.setFlat(True)
174 self.expanded = True
175 self.click_pos = None
176 self.arrow_icon_size = 16
178 def set_expanded(self, expanded):
179 if expanded == self.expanded:
180 self.emit(SIGNAL('expanded(bool)'), expanded)
181 return
182 self.expanded = expanded
183 for widget in self.findChildren(QtGui.QWidget):
184 widget.setHidden(not expanded)
185 self.emit(SIGNAL('expanded(bool)'), expanded)
187 def mousePressEvent(self, event):
188 if event.button() == Qt.LeftButton:
189 option = QtGui.QStyleOptionGroupBox()
190 self.initStyleOption(option)
191 icon_size = self.arrow_icon_size
192 button_area = QtCore.QRect(0, 0, icon_size, icon_size)
193 offset = self.arrow_icon_size + defs.spacing
194 adjusted = option.rect.adjusted(0, 0, -offset, 0)
195 top_left = adjusted.topLeft()
196 button_area.moveTopLeft(QtCore.QPoint(top_left))
197 self.click_pos = event.pos()
198 QtGui.QGroupBox.mousePressEvent(self, event)
200 def mouseReleaseEvent(self, event):
201 if (event.button() == Qt.LeftButton and
202 self.click_pos == event.pos()):
203 self.set_expanded(not self.expanded)
204 QtGui.QGroupBox.mouseReleaseEvent(self, event)
206 def paintEvent(self, event):
207 painter = QtGui.QStylePainter(self)
208 option = QtGui.QStyleOptionGroupBox()
209 self.initStyleOption(option)
210 painter.save()
211 painter.translate(self.arrow_icon_size + defs.spacing, 0)
212 painter.drawText(option.rect, Qt.AlignLeft, self.title())
213 painter.restore()
215 style = QtGui.QStyle
216 point = option.rect.adjusted(0, -4, 0, 0).topLeft()
217 icon_size = self.arrow_icon_size
218 option.rect = QtCore.QRect(point.x(), point.y(), icon_size, icon_size)
219 if self.expanded:
220 painter.drawPrimitive(style.PE_IndicatorArrowDown, option)
221 else:
222 painter.drawPrimitive(style.PE_IndicatorArrowRight, option)
225 class GitDialog(QtGui.QDialog):
227 def __init__(self, lineedit, title, button_text, parent):
228 QtGui.QDialog.__init__(self, parent)
229 self.setWindowTitle(title)
230 self.setMinimumWidth(333)
232 self.label = QtGui.QLabel()
233 self.label.setText(title)
235 self.lineedit = lineedit(self)
236 self.setFocusProxy(self.lineedit)
238 self.ok_button = QtGui.QPushButton()
239 self.ok_button.setText(button_text)
240 self.ok_button.setIcon(qtutils.apply_icon())
242 self.close_button = QtGui.QPushButton()
243 self.close_button.setText(N_('Close'))
245 self.button_layout = QtGui.QHBoxLayout()
246 self.button_layout.setMargin(defs.no_margin)
247 self.button_layout.setSpacing(defs.button_spacing)
248 self.button_layout.addStretch()
249 self.button_layout.addWidget(self.ok_button)
250 self.button_layout.addWidget(self.close_button)
252 self.main_layout = QtGui.QVBoxLayout()
253 self.main_layout.setMargin(defs.margin)
254 self.main_layout.setSpacing(defs.spacing)
256 self.main_layout.addWidget(self.label)
257 self.main_layout.addWidget(self.lineedit)
258 self.main_layout.addLayout(self.button_layout)
259 self.setLayout(self.main_layout)
261 qtutils.connect_button(self.ok_button, self.accept)
262 qtutils.connect_button(self.close_button, self.reject)
264 self.connect(self.lineedit, SIGNAL('textChanged(const QString&)'),
265 self.text_changed)
267 self.setWindowModality(Qt.WindowModal)
268 self.ok_button.setEnabled(False)
270 def text(self):
271 return unicode(self.lineedit.text())
273 def text_changed(self, txt):
274 self.ok_button.setEnabled(bool(self.text()))
276 def set_text(self, ref):
277 self.lineedit.setText(ref)
279 @classmethod
280 def get(cls, title, button_text, parent, default=None):
281 dlg = cls(title, button_text, parent)
282 if default:
283 dlg.set_text(default)
285 dlg.show()
286 dlg.raise_()
288 def show_popup():
289 x = dlg.lineedit.x()
290 y = dlg.lineedit.y() + dlg.lineedit.height()
291 point = QtCore.QPoint(x, y)
292 mapped = dlg.mapToGlobal(point)
293 dlg.lineedit.popup().move(mapped.x(), mapped.y())
294 dlg.lineedit.popup().show()
295 dlg.lineedit.refresh()
297 QtCore.QTimer().singleShot(0, show_popup)
299 if dlg.exec_() == cls.Accepted:
300 return dlg.text()
301 else:
302 return None
305 class GitRefDialog(GitDialog):
307 def __init__(self, title, button_text, parent):
308 GitDialog.__init__(self, completion.GitRefLineEdit,
309 title, button_text, parent)
312 class GitBranchDialog(GitDialog):
314 def __init__(self, title, button_text, parent):
315 GitDialog.__init__(self, completion.GitBranchLineEdit,
316 title, button_text, parent)
319 class GitRemoteBranchDialog(GitDialog):
321 def __init__(self, title, button_text, parent):
322 GitDialog.__init__(self, completion.GitRemoteBranchLineEdit,
323 title, button_text, parent)
326 # Syntax highlighting
328 def TERMINAL(pattern):
330 Denotes that a pattern is the final pattern that should
331 be matched. If this pattern matches no other formats
332 will be applied, even if they would have matched.
334 return '__TERMINAL__:%s' % pattern
336 # Cache the results of re.compile so that we don't keep
337 # rebuilding the same regexes whenever stylesheets change
338 _RGX_CACHE = {}
340 def rgba(r, g, b, a=255):
341 c = QColor()
342 c.setRgb(r, g, b)
343 c.setAlpha(a)
344 return c
346 default_colors = {
347 'color_text': rgba(0x00, 0x00, 0x00),
348 'color_add': rgba(0xcd, 0xff, 0xe0),
349 'color_remove': rgba(0xff, 0xd0, 0xd0),
350 'color_header': rgba(0xbb, 0xbb, 0xbb),
354 class GenericSyntaxHighligher(QSyntaxHighlighter):
355 def __init__(self, doc, *args, **kwargs):
356 QSyntaxHighlighter.__init__(self, doc)
357 for attr, val in default_colors.items():
358 setattr(self, attr, val)
359 self._rules = []
360 self.enabled = True
361 self.generate_rules()
363 def generate_rules(self):
364 pass
366 def set_enabled(self, enabled):
367 self.enabled = enabled
369 def create_rules(self, *rules):
370 if len(rules) % 2:
371 raise Exception('create_rules requires an even '
372 'number of arguments.')
373 for idx, rule in enumerate(rules):
374 if idx % 2:
375 continue
376 formats = rules[idx+1]
377 terminal = rule.startswith(TERMINAL(''))
378 if terminal:
379 rule = rule[len(TERMINAL('')):]
380 try:
381 regex = _RGX_CACHE[rule]
382 except KeyError:
383 regex = _RGX_CACHE[rule] = re.compile(rule)
384 self._rules.append((regex, formats, terminal,))
386 def formats(self, line):
387 matched = []
388 for regex, fmts, terminal in self._rules:
389 match = regex.match(line)
390 if not match:
391 continue
392 matched.append([match, fmts])
393 if terminal:
394 return matched
395 return matched
397 def mkformat(self, fg=None, bg=None, bold=False):
398 fmt = QTextCharFormat()
399 if fg:
400 fmt.setForeground(fg)
401 if bg:
402 fmt.setBackground(bg)
403 if bold:
404 fmt.setFontWeight(QFont.Bold)
405 return fmt
407 def highlightBlock(self, qstr):
408 if not self.enabled:
409 return
410 ascii = unicode(qstr)
411 if not ascii:
412 return
413 formats = self.formats(ascii)
414 if not formats:
415 return
416 for match, fmts in formats:
417 start = match.start()
418 groups = match.groups()
420 # No groups in the regex, assume this is a single rule
421 # that spans the entire line
422 if not groups:
423 self.setFormat(0, len(ascii), fmts)
424 continue
426 # Groups exist, rule is a tuple corresponding to group
427 for grpidx, group in enumerate(groups):
428 # allow empty matches
429 if not group:
430 continue
431 # allow None as a no-op format
432 length = len(group)
433 if fmts[grpidx]:
434 self.setFormat(start, start+length,
435 fmts[grpidx])
436 start += length
438 def set_colors(self, colordict):
439 for attr, val in colordict.items():
440 setattr(self, attr, val)
443 class DiffSyntaxHighlighter(GenericSyntaxHighligher):
444 """Implements the diff syntax highlighting
446 This class is used by widgets that display diffs.
449 def __init__(self, doc, whitespace=True):
450 self.whitespace = whitespace
451 GenericSyntaxHighligher.__init__(self, doc)
453 def generate_rules(self):
454 diff_head = self.mkformat(fg=self.color_header)
455 diff_head_bold = self.mkformat(fg=self.color_header, bold=True)
457 diff_add = self.mkformat(fg=self.color_text, bg=self.color_add)
458 diff_remove = self.mkformat(fg=self.color_text, bg=self.color_remove)
460 if self.whitespace:
461 bad_ws = self.mkformat(fg=Qt.black, bg=Qt.red)
463 # We specify the whitespace rule last so that it is
464 # applied after the diff addition/removal rules.
465 # The rules for the header
466 diff_old_rgx = TERMINAL(r'^--- ')
467 diff_new_rgx = TERMINAL(r'^\+\+\+ ')
468 diff_ctx_rgx = TERMINAL(r'^@@ ')
470 diff_hd1_rgx = TERMINAL(r'^diff --git a/.*b/.*')
471 diff_hd2_rgx = TERMINAL(r'^index \S+\.\.\S+')
472 diff_hd3_rgx = TERMINAL(r'^new file mode')
473 diff_hd4_rgx = TERMINAL(r'^deleted file mode')
474 diff_add_rgx = TERMINAL(r'^\+')
475 diff_rmv_rgx = TERMINAL(r'^-')
476 diff_bar_rgx = TERMINAL(r'^([ ]+.*)(\|[ ]+\d+[ ]+[+-]+)$')
477 diff_sts_rgx = (r'(.+\|.+?)(\d+)(.+?)([\+]*?)([-]*?)$')
478 diff_sum_rgx = (r'(\s+\d+ files changed[^\d]*)'
479 r'(:?\d+ insertions[^\d]*)'
480 r'(:?\d+ deletions.*)$')
482 self.create_rules(diff_old_rgx, diff_head,
483 diff_new_rgx, diff_head,
484 diff_ctx_rgx, diff_head_bold,
485 diff_bar_rgx, (diff_head_bold, diff_head),
486 diff_hd1_rgx, diff_head,
487 diff_hd2_rgx, diff_head,
488 diff_hd3_rgx, diff_head,
489 diff_hd4_rgx, diff_head,
490 diff_add_rgx, diff_add,
491 diff_rmv_rgx, diff_remove,
492 diff_sts_rgx, (None, diff_head,
493 None, diff_head,
494 diff_head),
495 diff_sum_rgx, (diff_head,
496 diff_head,
497 diff_head))
498 if self.whitespace:
499 self.create_rules('(..*?)(\s+)$', (None, bad_ws))
502 if __name__ == '__main__':
503 import sys
504 class SyntaxTestDialog(QtGui.QDialog):
505 def __init__(self, parent):
506 QtGui.QDialog.__init__(self, parent)
507 self.resize(720, 512)
508 self.vboxlayout = QtGui.QVBoxLayout(self)
509 self.vboxlayout.setObjectName('vboxlayout')
510 self.output_text = QtGui.QTextEdit(self)
511 font = QtGui.QFont()
512 if utils.is_darwin():
513 family = 'Monaco'
514 else:
515 family = 'Monospace'
516 font.setFamily(family)
517 font.setPointSize(12)
518 self.output_text.setFont(font)
519 self.output_text.setAcceptDrops(False)
520 self.vboxlayout.addWidget(self.output_text)
521 self.syntax = DiffSyntaxHighlighter(self.output_text.document())
523 app = QtGui.QApplication(sys.argv)
524 dialog = SyntaxTestDialog(qtutils.active_window())
525 dialog.show()
526 dialog.raise_()
527 app.exec_()