widgets: do not set window modality without a parent
[git-cola.git] / cola / widgets / grep.py
blob19c588c8b4ba6243d91e8892048058198e308c8a
1 from PyQt4 import QtCore
2 from PyQt4 import QtGui
3 from PyQt4.QtCore import Qt
4 from PyQt4.QtCore import SIGNAL
6 from cola import cmds
7 from cola import utils
8 from cola import qtutils
9 from cola.cmds import do
10 from cola.git import git
11 from cola.i18n import N_
12 from cola.qtutils import diff_font
13 from cola.widgets import defs
14 from cola.widgets.standard import Dialog
15 from cola.widgets.text import HintedTextView, HintedLineEdit
18 def grep():
19 """Prompt and use 'git grep' to find the content."""
20 widget = new_grep(parent=qtutils.active_window())
21 widget.show()
22 widget.raise_()
23 return widget
26 def new_grep(text=None, parent=None):
27 widget = Grep(parent=parent)
28 if text:
29 widget.search_for(text)
30 return widget
33 def goto_grep(line):
34 """Called when Search -> Grep's right-click 'goto' action."""
35 filename, line_number, contents = line.split(':', 2)
36 do(cmds.Edit, [filename], line_number=line_number)
39 class GrepThread(QtCore.QThread):
41 def __init__(self, parent):
42 QtCore.QThread.__init__(self, parent)
43 self.query = None
44 self.shell = False
45 self.regexp_mode = '--basic-regexp'
47 def run(self):
48 if self.query is None:
49 return
50 query = self.query
51 if self.shell:
52 args = utils.shell_split(query)
53 else:
54 args = [query]
55 status, out, err = git.grep(self.regexp_mode, n=True, *args)
56 if query == self.query:
57 self.emit(SIGNAL('result'), status, out, err)
58 else:
59 self.run()
62 class Grep(Dialog):
64 def __init__(self, parent=None):
65 Dialog.__init__(self, parent)
66 self.setAttribute(Qt.WA_MacMetalStyle)
67 self.setWindowTitle(N_('Search'))
68 if parent is not None:
69 self.setWindowModality(Qt.WindowModal)
71 self.input_label = QtGui.QLabel('git grep')
72 self.input_label.setFont(diff_font())
74 self.input_txt = HintedLineEdit(N_('command-line arguments'), self)
75 self.input_txt.enable_hint(True)
77 self.regexp_combo = combo = QtGui.QComboBox()
78 combo.setToolTip(N_('Choose the "git grep" regular expression mode'))
79 items = [N_('Basic Regexp'), N_('Extended Regexp'), N_('Fixed String')]
80 combo.addItems(items)
81 combo.setCurrentIndex(0)
82 combo.setEditable(False)
83 combo.setItemData(0,
84 N_('Search using a POSIX basic regular expression'),
85 Qt.ToolTipRole)
86 combo.setItemData(1,
87 N_('Search using a POSIX extended regular expression'),
88 Qt.ToolTipRole)
89 combo.setItemData(2,
90 N_('Search for a fixed string'),
91 Qt.ToolTipRole)
92 combo.setItemData(0, '--basic-regexp', Qt.UserRole)
93 combo.setItemData(1, '--extended-regexp', Qt.UserRole)
94 combo.setItemData(2, '--fixed-strings', Qt.UserRole)
96 self.result_txt = GrepTextView(N_('grep result...'), self)
97 self.result_txt.enable_hint(True)
99 self.edit_button = QtGui.QPushButton(N_('Edit'))
100 self.edit_button.setIcon(qtutils.open_file_icon())
101 self.edit_button.setEnabled(False)
102 self.edit_button.setShortcut(cmds.Edit.SHORTCUT)
104 self.refresh_button = QtGui.QPushButton(N_('Refresh'))
105 self.refresh_button.setIcon(qtutils.reload_icon())
106 self.refresh_button.setShortcut(QtGui.QKeySequence.Refresh)
108 self.shell_checkbox = QtGui.QCheckBox(N_('Shell arguments'))
109 self.shell_checkbox.setToolTip(
110 N_('Parse arguments using a shell.\n'
111 'Queries with spaces will require "double quotes".'))
112 self.shell_checkbox.setChecked(False)
114 self.close_button = QtGui.QPushButton(N_('Close'))
116 self.input_layout = QtGui.QHBoxLayout()
117 self.input_layout.setMargin(0)
118 self.input_layout.setSpacing(defs.button_spacing)
120 self.bottom_layout = QtGui.QHBoxLayout()
121 self.bottom_layout.setMargin(0)
122 self.bottom_layout.setSpacing(defs.button_spacing)
124 self.mainlayout = QtGui.QVBoxLayout()
125 self.mainlayout.setMargin(defs.margin)
126 self.mainlayout.setSpacing(defs.spacing)
128 self.input_layout.addWidget(self.input_label)
129 self.input_layout.addWidget(self.input_txt)
130 self.input_layout.addWidget(self.regexp_combo)
132 self.bottom_layout.addWidget(self.edit_button)
133 self.bottom_layout.addWidget(self.refresh_button)
134 self.bottom_layout.addWidget(self.shell_checkbox)
135 self.bottom_layout.addStretch()
136 self.bottom_layout.addWidget(self.close_button)
138 self.mainlayout.addLayout(self.input_layout)
139 self.mainlayout.addWidget(self.result_txt)
140 self.mainlayout.addLayout(self.bottom_layout)
141 self.setLayout(self.mainlayout)
143 self.grep_thread = GrepThread(self)
145 self.connect(self.grep_thread, SIGNAL('result'),
146 self.process_result)
148 self.connect(self.input_txt, SIGNAL('textChanged(QString)'),
149 lambda s: self.search())
151 self.connect(self.regexp_combo, SIGNAL('currentIndexChanged(int)'),
152 lambda x: self.search())
154 self.connect(self.result_txt, SIGNAL('leave()'),
155 lambda: self.input_txt.setFocus())
157 qtutils.add_action(self.input_txt, 'FocusResults',
158 lambda: self.result_txt.setFocus(),
159 Qt.Key_Down, Qt.Key_Enter, Qt.Key_Return)
160 qtutils.add_action(self, 'FocusSearch',
161 lambda: self.input_txt.setFocus(),
162 'Ctrl+l')
163 qtutils.connect_button(self.edit_button, self.edit)
164 qtutils.connect_button(self.refresh_button, self.search)
165 qtutils.connect_toggle(self.shell_checkbox, lambda x: self.search())
166 qtutils.connect_button(self.close_button, self.close)
167 qtutils.add_close_action(self)
169 if not qtutils.apply_state(self):
170 self.resize(666, 420)
172 def done(self, exit_code):
173 qtutils.save_state(self)
174 return Dialog.done(self, exit_code)
176 def regexp_mode(self):
177 idx = self.regexp_combo.currentIndex()
178 data = self.regexp_combo.itemData(idx, Qt.UserRole).toPyObject()
179 return unicode(data)
181 def search(self):
182 self.edit_button.setEnabled(False)
183 self.refresh_button.setEnabled(False)
184 query = self.input_txt.value()
185 if len(query) < 2:
186 self.result_txt.set_value('')
187 return
188 self.grep_thread.query = query
189 self.grep_thread.shell = self.shell_checkbox.isChecked()
190 self.grep_thread.regexp_mode = self.regexp_mode()
191 self.grep_thread.start()
193 def search_for(self, txt):
194 self.input_txt.set_value(txt)
195 self.search()
197 def text_scroll(self):
198 scrollbar = self.result_txt.verticalScrollBar()
199 if scrollbar:
200 return scrollbar.value()
201 return None
203 def set_text_scroll(self, scroll):
204 scrollbar = self.result_txt.verticalScrollBar()
205 if scrollbar and scroll is not None:
206 scrollbar.setValue(scroll)
208 def text_offset(self):
209 return self.result_txt.textCursor().position()
211 def set_text_offset(self, offset):
212 cursor = self.result_txt.textCursor()
213 cursor.setPosition(offset)
214 self.result_txt.setTextCursor(cursor)
216 def process_result(self, status, out, err):
218 if status == 0:
219 value = out + err
220 elif out + err:
221 value = 'git grep: ' + out + err
222 else:
223 value = ''
225 # save scrollbar and text cursor
226 scroll = self.text_scroll()
227 offset = min(len(value), self.text_offset())
229 self.result_txt.set_value(value)
230 # restore
231 self.set_text_scroll(scroll)
232 self.set_text_offset(offset)
234 self.edit_button.setEnabled(status == 0)
235 self.refresh_button.setEnabled(status == 0)
237 def edit(self):
238 goto_grep(self.result_txt.selected_line()),
241 class GrepTextView(HintedTextView):
243 def __init__(self, hint, parent):
244 HintedTextView.__init__(self, hint, parent)
245 self.goto_action = qtutils.add_action(self, 'Launch Editor', self.edit)
246 self.goto_action.setShortcut(cmds.Edit.SHORTCUT)
248 qtutils.add_action(self, 'Up',
249 lambda: self.move(QtGui.QTextCursor.Up),
250 Qt.Key_K)
252 qtutils.add_action(self, 'Down',
253 lambda: self.move(QtGui.QTextCursor.Down),
254 Qt.Key_J)
256 qtutils.add_action(self, 'Left',
257 lambda: self.move(QtGui.QTextCursor.Left),
258 Qt.Key_H)
260 qtutils.add_action(self, 'Right',
261 lambda: self.move(QtGui.QTextCursor.Right),
262 Qt.Key_L)
264 qtutils.add_action(self, 'StartOfLine',
265 lambda: self.move(QtGui.QTextCursor.StartOfLine),
266 Qt.Key_0)
268 qtutils.add_action(self, 'EndOfLine',
269 lambda: self.move(QtGui.QTextCursor.EndOfLine),
270 Qt.Key_Dollar)
272 qtutils.add_action(self, 'WordLeft',
273 lambda: self.move(QtGui.QTextCursor.WordLeft),
274 Qt.Key_B)
276 qtutils.add_action(self, 'WordRight',
277 lambda: self.move(QtGui.QTextCursor.WordRight),
278 Qt.Key_W)
280 qtutils.add_action(self, 'PageUp',
281 lambda: self.page(-self.height()/2),
282 'Shift+Space')
284 qtutils.add_action(self, 'PageDown',
285 lambda: self.page(self.height()/2),
286 Qt.Key_Space)
288 def contextMenuEvent(self, event):
289 menu = self.createStandardContextMenu(event.pos())
290 menu.addSeparator()
291 menu.addAction(self.goto_action)
292 menu.exec_(self.mapToGlobal(event.pos()))
294 def edit(self):
295 goto_grep(self.selected_line())
297 def page(self, offset):
298 rect = self.cursorRect()
299 x = rect.x()
300 y = rect.y() + offset
301 new_cursor = self.cursorForPosition(QtCore.QPoint(x, y))
302 if new_cursor is not None:
303 self.set_text_cursor(new_cursor)
305 def set_text_cursor(self, cursor):
306 self.setTextCursor(cursor)
307 self.ensureCursorVisible()
308 self.viewport().update()
310 def move(self, direction):
311 cursor = self.textCursor()
312 if cursor.movePosition(direction):
313 self.set_text_cursor(cursor)
315 def paintEvent(self, event):
316 HintedTextView.paintEvent(self, event)
317 if self.hasFocus():
318 # Qt doesn't redraw the cursor when using movePosition().
319 # So.. draw our own cursor.
320 rect = self.cursorRect()
321 painter = QtGui.QPainter(self.viewport())
322 painter.fillRect(rect, Qt.SolidPattern)
324 def keyPressEvent(self, event):
325 if event.key() == Qt.Key_Up:
326 cursor = self.textCursor()
327 position = cursor.position()
328 if position == 0 and not cursor.hasSelection():
329 # The cursor is at the beginning of the line.
330 # If we have selection then simply reset the cursor.
331 # Otherwise, emit a signal so that the parent can
332 # change focus.
333 self.emit(SIGNAL('leave()'))
334 return HintedTextView.keyPressEvent(self, event)