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
)
27 if tooltip
is not None:
28 button
.setToolTip(tooltip
)
29 if layout
is not None:
30 layout
.addWidget(button
)
34 def create_action_button(tooltip
, icon
):
35 button
= QtGui
.QPushButton()
36 button
.setCursor(QtCore
.Qt
.PointingHandCursor
)
39 button
.setFixedSize(QtCore
.QSize(16, 16))
40 button
.setToolTip(tooltip
)
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()
50 font
.setCapitalization(QtGui
.QFont
.SmallCaps
)
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
)
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')
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
)
116 def create_menu(title
, parent
):
117 """Create a menu and set its title."""
118 qmenu
= QtGui
.QMenu(parent
)
119 qmenu
.setTitle(title
)
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
)
132 button
.setToolButtonStyle(Qt
.ToolButtonTextBesideIcon
)
134 button
.setToolTip(tooltip
)
135 if layout
is not None:
136 layout
.addWidget(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
):
160 if size
.width() * self
.aspect_ratio
< size
.height():
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
)
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
)
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
)
211 painter
.translate(self
.arrow_icon_size
+ defs
.spacing
, 0)
212 painter
.drawText(option
.rect
, Qt
.AlignLeft
, self
.title())
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
)
220 painter
.drawPrimitive(style
.PE_IndicatorArrowDown
, option
)
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&)'),
267 self
.setWindowModality(Qt
.WindowModal
)
268 self
.ok_button
.setEnabled(False)
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
)
280 def get(cls
, title
, button_text
, parent
, default
=None):
281 dlg
= cls(title
, button_text
, parent
)
283 dlg
.set_text(default
)
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
:
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
340 def rgba(r
, g
, b
, a
=255):
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
)
361 self
.generate_rules()
363 def generate_rules(self
):
366 def set_enabled(self
, enabled
):
367 self
.enabled
= enabled
369 def create_rules(self
, *rules
):
371 raise Exception('create_rules requires an even '
372 'number of arguments.')
373 for idx
, rule
in enumerate(rules
):
376 formats
= rules
[idx
+1]
377 terminal
= rule
.startswith(TERMINAL(''))
379 rule
= rule
[len(TERMINAL('')):]
381 regex
= _RGX_CACHE
[rule
]
383 regex
= _RGX_CACHE
[rule
] = re
.compile(rule
)
384 self
._rules
.append((regex
, formats
, terminal
,))
386 def formats(self
, line
):
388 for regex
, fmts
, terminal
in self
._rules
:
389 match
= regex
.match(line
)
392 matched
.append([match
, fmts
])
397 def mkformat(self
, fg
=None, bg
=None, bold
=False):
398 fmt
= QTextCharFormat()
400 fmt
.setForeground(fg
)
402 fmt
.setBackground(bg
)
404 fmt
.setFontWeight(QFont
.Bold
)
407 def highlightBlock(self
, qstr
):
410 ascii
= unicode(qstr
)
413 formats
= self
.formats(ascii
)
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
423 self
.setFormat(0, len(ascii
), fmts
)
426 # Groups exist, rule is a tuple corresponding to group
427 for grpidx
, group
in enumerate(groups
):
428 # allow empty matches
431 # allow None as a no-op format
434 self
.setFormat(start
, 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
)
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
,
495 diff_sum_rgx
, (diff_head
,
499 self
.create_rules('(..*?)(\s+)$', (None, bad_ws
))
502 if __name__
== '__main__':
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
)
512 if utils
.is_darwin():
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())