1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 from python_qt_binding.QtCore import QFile, QFileInfo, QIODevice, QRegExp, Qt, Signal
34 from python_qt_binding.QtGui import QFont, QTextCursor
35 import os
36
37 from node_manager_fkie.common import package_name
38 from node_manager_fkie.detailed_msg_box import WarningMessageBox
39 import node_manager_fkie as nm
40
41 from .parser_functions import interpret_path
42 from .xml_highlighter import XmlHighlighter
43 from .yaml_highlighter import YamlHighlighter
44
45
46 try:
47 from python_qt_binding.QtGui import QApplication, QMenu, QMessageBox, QTextEdit
48 except:
49 from python_qt_binding.QtWidgets import QApplication, QMenu, QMessageBox, QTextEdit
50
51
52 -class TextEdit(QTextEdit):
53 '''
54 The XML editor to handle the included files. If an included file in the opened
55 launch file is detected, this can be open by STRG+(mouse click) in a new
56 editor.
57 '''
58
59 load_request_signal = Signal(str)
60 ''' @ivar: A signal for request to open a configuration file'''
61
62 search_result_signal = Signal(str, bool, str, int)
63 ''' @ivar: A signal emitted after search_threaded was started.
64 (search text, found or not, file, position in text)
65 for each result a signal will be emitted.
66 '''
67
68 SUBSTITUTION_ARGS = ['env', 'optenv', 'find', 'anon', 'arg']
69 CONTEXT_FILE_EXT = ['.launch', '.test', '.xml']
70 YAML_VALIDATION_FILES = ['.yaml', '.iface', '.sync']
71
72 - def __init__(self, filename, parent=None):
73 self.parent = parent
74 QTextEdit.__init__(self, parent)
75 self.setObjectName(' - '.join(['Editor', filename]))
76 self.setContextMenuPolicy(Qt.CustomContextMenu)
77 self.customContextMenuRequested.connect(self.show_custom_context_menu)
78
79 self.setAcceptRichText(False)
80 font = QFont()
81 font.setFamily("Fixed".decode("utf-8"))
82 font.setPointSize(12)
83 self.setFont(font)
84 self.setLineWrapMode(QTextEdit.NoWrap)
85 self.setTabStopWidth(25)
86 self.setAcceptRichText(False)
87 self.setCursorWidth(2)
88 self.setFontFamily("courier new")
89 self.setProperty("backgroundVisible", True)
90 self.regexp_list = [QRegExp("\\binclude\\b"), QRegExp("\\btextfile\\b"),
91 QRegExp("\\bfile\\b"), QRegExp("\\bvalue=.*pkg:\/\/\\b"),
92 QRegExp("\\bvalue=.*package:\/\/\\b"),
93 QRegExp("\\bvalue=.*\$\(find\\b"),
94 QRegExp("\\bdefault=.*\$\(find\\b")]
95 self.filename = filename
96 self.file_info = None
97 if self.filename:
98 f = QFile(filename)
99 if f.open(QIODevice.ReadOnly | QIODevice.Text):
100 self.file_info = QFileInfo(filename)
101 self.setText(unicode(f.readAll(), "utf-8"))
102
103 self.path = '.'
104
105 self.setAcceptDrops(True)
106 if filename.endswith('.launch'):
107 self.hl = XmlHighlighter(self.document())
108 self.cursorPositionChanged.connect(self._document_position_changed)
109 else:
110 self.hl = YamlHighlighter(self.document())
111
112 self._search_thread = None
113 self._stop = False
114
116 if isinstance(self.hl, XmlHighlighter) and nm.settings().highlight_xml_blocks:
117
118
119 self.hl.mark_block(self.textCursor().block(), self.textCursor().positionInBlock())
120
121
122 - def save(self, force=False):
123 '''
124 Saves changes to the file.
125 :return: saved, errors, msg
126 :rtype: bool, bool, str
127 '''
128 if force or self.document().isModified() or not QFileInfo(self.filename).exists():
129 f = QFile(self.filename)
130 if f.open(QIODevice.WriteOnly | QIODevice.Text):
131 f.write(self.toPlainText().encode('utf-8'))
132 self.document().setModified(False)
133 self.file_info = QFileInfo(self.filename)
134
135 ext = os.path.splitext(self.filename)
136
137 if ext[1] in self.CONTEXT_FILE_EXT:
138 imported = False
139 try:
140 from lxml import etree
141 imported = True
142 parser = etree.XMLParser()
143 etree.fromstring(self.toPlainText().encode('utf-8'), parser)
144 except Exception as e:
145 if imported:
146 self.markLine(e.position[0])
147 return True, True, "%s" % e
148
149 elif ext[1] in self.YAML_VALIDATION_FILES:
150 try:
151 import yaml
152 yaml.load(self.toPlainText().encode('utf-8'))
153 except yaml.MarkedYAMLError as e:
154 return True, True, "%s" % e
155 return True, False, ''
156 else:
157 return False, True, "Cannot write XML file"
158 return False, False, ''
159
160 - def markLine(self, no):
161 try:
162 cursor = self.textCursor()
163 cursor.setPosition(0, QTextCursor.MoveAnchor)
164 while (cursor.block().blockNumber() + 1 < no):
165 cursor.movePosition(QTextCursor.NextBlock, QTextCursor.MoveAnchor)
166 cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
167 self.setTextCursor(cursor)
168 except:
169 pass
170
171 - def setCurrentPath(self, path):
172 '''
173 Sets the current working path. This path is to open the included files,
174 which contains the relative path.
175 @param path: the path of the current opened file (without the file)
176 @type path: C{str}
177 '''
178 self.path = path
179
180 - def index(self, text):
181 '''
182 Searches in the given text for key indicates the including of a file and
183 return their index.
184 @param text: text to find
185 @type text: C{str}
186 @return: the index of the including key or -1
187 @rtype: C{int}
188 '''
189 for pattern in self.regexp_list:
190 index = pattern.indexIn(text)
191 if index > -1:
192 return index
193 return -1
194
195 - def includedFiles(self):
196 '''
197 Returns all included files in the document.
198 '''
199 result = []
200 b = self.document().begin()
201 while b != self.document().end():
202 text = b.text()
203 index = self.index(text)
204 if index > -1:
205 startIndex = text.find('"', index)
206 if startIndex > -1:
207 endIndex = text.find('"', startIndex + 1)
208 fileName = text[startIndex + 1:endIndex]
209 if len(fileName) > 0:
210 try:
211 path = interpret_path(fileName)
212 f = QFile(path)
213 ext = os.path.splitext(path)
214 if f.exists() and ext[1] in nm.settings().SEARCH_IN_EXT:
215 result.append(path)
216 except:
217 import traceback
218 print traceback.format_exc(1)
219 b = b.next()
220 return result
221
222 - def focusInEvent(self, event):
223
224 try:
225 if self.filename and self.file_info:
226 if self.file_info.lastModified() != QFileInfo(self.filename).lastModified():
227 self.file_info = QFileInfo(self.filename)
228 result = QMessageBox.question(self, "File changed", "File was changed, reload?", QMessageBox.Yes | QMessageBox.No)
229 if result == QMessageBox.Yes:
230 f = QFile(self.filename)
231 if f.open(QIODevice.ReadOnly | QIODevice.Text):
232 self.setText(unicode(f.readAll(), "utf-8"))
233 self.document().setModified(False)
234 self.textChanged.emit()
235 else:
236 QMessageBox.critical(self, "Error", "Cannot open launch file%s" % self.filename)
237 except:
238 pass
239 QTextEdit.focusInEvent(self, event)
240
241 - def mouseReleaseEvent(self, event):
242 '''
243 Opens the new editor, if the user clicked on the included file and sets the
244 default cursor.
245 '''
246 if event.modifiers() == Qt.ControlModifier or event.modifiers() == Qt.ShiftModifier:
247 cursor = self.cursorForPosition(event.pos())
248 index = self.index(cursor.block().text())
249 if index > -1:
250 startIndex = cursor.block().text().find('"', index)
251 if startIndex > -1:
252 endIndex = cursor.block().text().find('"', startIndex + 1)
253 fileName = cursor.block().text()[startIndex + 1:endIndex]
254 if len(fileName) > 0:
255 try:
256 qf = QFile(interpret_path(fileName))
257 if not qf.exists():
258
259 result = QMessageBox.question(self, "File not found", '\n\n'.join(["Create a new file?", qf.fileName()]), QMessageBox.Yes | QMessageBox.No)
260 if result == QMessageBox.Yes:
261 d = os.path.dirname(qf.fileName())
262 if not os.path.exists(d):
263 os.makedirs(d)
264 with open(qf.fileName(), 'w') as f:
265 if qf.fileName().endswith('.launch'):
266 f.write('<launch>\n\n</launch>')
267 event.setAccepted(True)
268 self.load_request_signal.emit(qf.fileName())
269 else:
270 event.setAccepted(True)
271 self.load_request_signal.emit(qf.fileName())
272 except Exception, e:
273 WarningMessageBox(QMessageBox.Warning, "File not found %s" % fileName, str(e)).exec_()
274 QTextEdit.mouseReleaseEvent(self, event)
275
276 - def mouseMoveEvent(self, event):
277 '''
278 Sets the X{Qt.PointingHandCursor} if the control key is pressed and
279 the mouse is over the included file.
280 '''
281 if event.modifiers() == Qt.ControlModifier or event.modifiers() == Qt.ShiftModifier:
282 cursor = self.cursorForPosition(event.pos())
283 index = self.index(cursor.block().text())
284 if index > -1:
285 self.viewport().setCursor(Qt.PointingHandCursor)
286 else:
287 self.viewport().setCursor(Qt.IBeamCursor)
288 else:
289 self.viewport().setCursor(Qt.IBeamCursor)
290 QTextEdit.mouseMoveEvent(self, event)
291
292 - def keyPressEvent(self, event):
293 '''
294 Enable the mouse tracking by X{setMouseTracking()} if the control key is pressed.
295 '''
296 if event.key() == Qt.Key_Control or event.key() == Qt.Key_Shift:
297 self.setMouseTracking(True)
298 if event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_7:
299 self.commentText()
300 elif event.modifiers() == Qt.ControlModifier | Qt.ShiftModifier and event.key() == Qt.Key_Slash:
301 self.commentText()
302 elif event.modifiers() == Qt.AltModifier and event.key() == Qt.Key_Space:
303 ext = os.path.splitext(self.filename)
304 if ext[1] in self.CONTEXT_FILE_EXT:
305 menu = self._create_context_substitution_menu(False)
306 if menu is None:
307 menu = self._create_context_menu_for_tag()
308 if menu:
309 menu.exec_(self.mapToGlobal(self.cursorRect().bottomRight()))
310 elif event.key() != Qt.Key_Escape:
311
312 if event.modifiers() == Qt.NoModifier and event.key() == Qt.Key_Tab:
313 self.shiftText()
314 elif event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_Tab:
315 self.shiftText(back=True)
316 else:
317 QTextEdit.keyPressEvent(self, event)
318 else:
319 event.accept()
320 QTextEdit.keyPressEvent(self, event)
321
322 - def keyReleaseEvent(self, event):
323 '''
324 Disable the mouse tracking by X{setMouseTracking()} if the control key is
325 released and set the cursor back to X{Qt.IBeamCursor}.
326 '''
327 if event.key() == Qt.Key_Control or event.key() == Qt.Key_Shift:
328 self.setMouseTracking(False)
329 self.viewport().setCursor(Qt.IBeamCursor)
330 else:
331 event.accept()
332 QTextEdit.keyReleaseEvent(self, event)
333
335 cursor = self.textCursor()
336 if not cursor.isNull():
337 cursor.beginEditBlock()
338 start = cursor.selectionStart()
339 end = cursor.selectionEnd()
340 cursor.setPosition(start)
341 block_start = cursor.blockNumber()
342 cursor.setPosition(end)
343 block_end = cursor.blockNumber()
344 if block_end - block_start > 0 and end - cursor.block().position() <= 0:
345
346 block_end -= 1
347 cursor.setPosition(start, QTextCursor.MoveAnchor)
348 cursor.movePosition(QTextCursor.StartOfLine)
349 start = cursor.position()
350 while (cursor.block().blockNumber() < block_end + 1):
351 cursor.movePosition(QTextCursor.StartOfLine)
352 ext = os.path.splitext(self.filename)
353
354 if ext[1] in self.CONTEXT_FILE_EXT:
355
356 cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, 1)
357 while cursor.selectedText() in [' ', '\t']:
358 cursor.setPosition(cursor.position(), QTextCursor.MoveAnchor)
359 cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, 1)
360 cursor.movePosition(QTextCursor.PreviousCharacter, 1)
361 cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, 4)
362
363 if cursor.selectedText() == '<!--':
364 cursor.insertText('')
365
366 cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, 1)
367 while cursor.selectedText() in [' ', '\t']:
368 cursor.insertText('')
369 cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, 1)
370 cursor.movePosition(QTextCursor.EndOfLine)
371 cursor.movePosition(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor, 3)
372 if cursor.selectedText() == '-->':
373 cursor.insertText('')
374
375 cursor.movePosition(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor, 1)
376 while cursor.selectedText() in [' ', '\t']:
377 cursor.insertText('')
378 cursor.movePosition(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor, 1)
379 else:
380 cursor.setPosition(cursor.anchor())
381 cursor.movePosition(QTextCursor.EndOfLine, QTextCursor.KeepAnchor)
382
383 if cursor.selectedText().find('<!--') < 0 and cursor.selectedText().find('-->') < 0:
384 cursor.movePosition(QTextCursor.StartOfLine)
385
386 cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, 1)
387 while cursor.selectedText() in [' ', '\t']:
388 cursor.setPosition(cursor.position(), QTextCursor.MoveAnchor)
389 cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, 1)
390 cursor.movePosition(QTextCursor.PreviousCharacter, 1)
391 cursor.insertText('<!-- ')
392 cursor.movePosition(QTextCursor.EndOfLine)
393 cursor.insertText(' -->')
394 else:
395 if cursor.block().length() < 2:
396 cursor.movePosition(QTextCursor.NextBlock)
397 continue
398 cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, 2)
399
400 if cursor.selectedText() == '# ':
401 cursor.insertText('')
402 else:
403 cursor.movePosition(QTextCursor.StartOfLine)
404 cursor.insertText('# ')
405 cursor.movePosition(QTextCursor.NextBlock)
406
407 cursor.endEditBlock()
408 cursor.setPosition(start, QTextCursor.MoveAnchor)
409 cursor.movePosition(QTextCursor.StartOfBlock, QTextCursor.MoveAnchor)
410 while (cursor.block().blockNumber() < block_end):
411 cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor)
412 cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
413
414 self.setTextCursor(cursor)
415
416 - def shiftText(self, back=False):
417 '''
418 Increase (Decrease) indentation using Tab (Ctrl+Tab).
419 '''
420 cursor = self.textCursor()
421 if not cursor.isNull():
422
423 cursor.beginEditBlock()
424 start = cursor.selectionStart()
425 end = cursor.selectionEnd()
426 cursor.setPosition(start)
427 block_start = cursor.blockNumber()
428 cursor.setPosition(end)
429 block_end = cursor.blockNumber()
430 if block_end - block_start == 0:
431
432 if back:
433 for _ in range(2):
434 cursor.movePosition(QTextCursor.StartOfLine)
435 cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, 1)
436 if cursor.selectedText() == ' ':
437 cursor.insertText('')
438 elif cursor.selectedText() == "\t":
439 cursor.insertText('')
440 break
441 cursor.movePosition(QTextCursor.StartOfLine)
442 else:
443
444 cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, end - start)
445 cursor.insertText(' ')
446 else:
447
448 if back:
449 removed = 0
450 for i in reversed(range(start, end)):
451 cursor.setPosition(i)
452 if cursor.atBlockStart():
453 cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, 2)
454 if cursor.selectedText() == ' ':
455 cursor.insertText('')
456 removed += 2
457 else:
458 cursor.movePosition(QTextCursor.StartOfLine)
459 cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, 1)
460 if cursor.selectedText() == ' ':
461 cursor.insertText('')
462 removed += 1
463 elif cursor.selectedText() == "\t":
464 cursor.insertText('')
465 removed += 1
466 cursor.setPosition(start)
467 cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, end - start - removed)
468 else:
469
470 inserted = 0
471 for i in reversed(range(start, end)):
472 cursor.setPosition(i)
473 if cursor.atBlockStart():
474 cursor.insertText(' ')
475 inserted += 2
476 cursor.setPosition(start)
477 cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, end - start + inserted)
478 self.setTextCursor(cursor)
479 cursor.endEditBlock()
480
481
482
483
484
485 - def dragEnterEvent(self, e):
486 if e.mimeData().hasFormat('text/plain'):
487 e.accept()
488 else:
489 e.ignore()
490
491 - def dragMoveEvent(self, e):
493
494 - def dropEvent(self, e):
495 cursor = self.cursorForPosition(e.pos())
496 if not cursor.isNull():
497 text = e.mimeData().text()
498
499 if text.startswith('file://'):
500 text = text[7:]
501 if os.path.exists(text) and os.path.isfile(text):
502
503 (package, path) = package_name(os.path.dirname(text))
504 if text.endswith('.launch'):
505 if package:
506 cursor.insertText('<include file="$(find %s)%s" />' % (package, text.replace(path, '')))
507 else:
508 cursor.insertText('<include file="%s" />' % text)
509 else:
510 if package:
511 cursor.insertText('<rosparam file="$(find %s)%s" command="load" />' % (package, text.replace(path, '')))
512 else:
513 cursor.insertText('<rosparam file="%s" command="load" />' % text)
514 else:
515 cursor.insertText(e.mimeData().text())
516 e.accept()
517
518
519
520
521
523 menu = QTextEdit.createStandardContextMenu(self)
524
525
526 submenu = self._create_context_menu_for_tag()
527 if submenu is not None:
528 menu.addMenu(submenu)
529 argmenu = self._create_context_substitution_menu()
530 if argmenu is not None:
531 menu.addMenu(argmenu)
532 menu.exec_(self.mapToGlobal(pos))
533
535 QTextEdit.contextMenuEvent(self, event)
536
538 if isinstance(self.hl, XmlHighlighter):
539 tag = self.hl.get_tag_of_current_block(self.textCursor().block(), self.textCursor().positionInBlock())
540 if tag:
541 try:
542 menu = QMenu("ROS <%s>" % tag, self)
543 menu.triggered.connect(self._context_activated)
544
545 menu_attr = QMenu("attributes", menu)
546 attributes = sorted(list(set(XmlHighlighter.LAUNCH_ATTR[tag])))
547 for attr in attributes:
548 action = menu_attr.addAction(attr.rstrip('='))
549 action.setData('%s""' % attr)
550 menu.addMenu(menu_attr)
551
552 tags = sorted(XmlHighlighter.LAUNCH_CHILDS[tag])
553 if tags:
554 menu_tags = QMenu("tags", menu)
555 for tag in tags:
556 data = '<%s></%s>' % (tag, tag) if XmlHighlighter.LAUNCH_CHILDS[tag] else '<%s/>' % tag
557 action = menu_tags.addAction(tag)
558 action.setData(data)
559 menu.addMenu(menu_tags)
560 return menu
561 except:
562 import traceback
563 print traceback.format_exc(1)
564 return None
565 return None
566
568 if isinstance(self.hl, XmlHighlighter):
569 text = self.toPlainText()
570 pos = self.textCursor().position() - 1
571 try:
572 if force_all or (text[pos] == '$' or (text[pos] == '(' and text[pos - 1] == '$')):
573 menu = QMenu("ROS substitution args", self)
574 menu.triggered.connect(self._context_activated)
575 for arg in self.SUBSTITUTION_ARGS:
576 action = menu.addAction("%s" % arg)
577 if force_all:
578 action.setData("$(%s )" % arg)
579 else:
580 action.setData("(%s" % arg if text[pos] == '$' else "%s" % arg)
581 return menu
582 except:
583 pass
584 return None
585
586 - def _context_activated(self, arg):
587 cursor = self.textCursor()
588 if not cursor.isNull():
589 cursor.insertText(arg.data())
590