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