bracket_matcher.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. """ Provides bracket matching for Q[Plain]TextEdit widgets.
  2. """
  3. # System library imports
  4. from qtpy import QtCore, QtGui, QtWidgets
  5. class BracketMatcher(QtCore.QObject):
  6. """ Matches square brackets, braces, and parentheses based on cursor
  7. position.
  8. """
  9. # Protected class variables.
  10. _opening_map = { '(':')', '{':'}', '[':']' }
  11. _closing_map = { ')':'(', '}':'{', ']':'[' }
  12. #--------------------------------------------------------------------------
  13. # 'QObject' interface
  14. #--------------------------------------------------------------------------
  15. def __init__(self, text_edit):
  16. """ Create a call tip manager that is attached to the specified Qt
  17. text edit widget.
  18. """
  19. assert isinstance(text_edit, (QtWidgets.QTextEdit, QtWidgets.QPlainTextEdit))
  20. super(BracketMatcher, self).__init__()
  21. # The format to apply to matching brackets.
  22. self.format = QtGui.QTextCharFormat()
  23. self.format.setBackground(QtGui.QColor('silver'))
  24. self._text_edit = text_edit
  25. text_edit.cursorPositionChanged.connect(self._cursor_position_changed)
  26. #--------------------------------------------------------------------------
  27. # Protected interface
  28. #--------------------------------------------------------------------------
  29. def _find_match(self, position):
  30. """ Given a valid position in the text document, try to find the
  31. position of the matching bracket. Returns -1 if unsuccessful.
  32. """
  33. # Decide what character to search for and what direction to search in.
  34. document = self._text_edit.document()
  35. start_char = document.characterAt(position)
  36. search_char = self._opening_map.get(start_char)
  37. if search_char:
  38. increment = 1
  39. else:
  40. search_char = self._closing_map.get(start_char)
  41. if search_char:
  42. increment = -1
  43. else:
  44. return -1
  45. # Search for the character.
  46. char = start_char
  47. depth = 0
  48. while position >= 0 and position < document.characterCount():
  49. if char == start_char:
  50. depth += 1
  51. elif char == search_char:
  52. depth -= 1
  53. if depth == 0:
  54. break
  55. position += increment
  56. char = document.characterAt(position)
  57. else:
  58. position = -1
  59. return position
  60. def _selection_for_character(self, position):
  61. """ Convenience method for selecting a character.
  62. """
  63. selection = QtWidgets.QTextEdit.ExtraSelection()
  64. cursor = self._text_edit.textCursor()
  65. cursor.setPosition(position)
  66. cursor.movePosition(QtGui.QTextCursor.NextCharacter,
  67. QtGui.QTextCursor.KeepAnchor)
  68. selection.cursor = cursor
  69. selection.format = self.format
  70. return selection
  71. #------ Signal handlers ----------------------------------------------------
  72. def _cursor_position_changed(self):
  73. """ Updates the document formatting based on the new cursor position.
  74. """
  75. # Clear out the old formatting.
  76. self._text_edit.setExtraSelections([])
  77. # Attempt to match a bracket for the new cursor position.
  78. cursor = self._text_edit.textCursor()
  79. if not cursor.hasSelection():
  80. position = cursor.position() - 1
  81. match_position = self._find_match(position)
  82. if match_position != -1:
  83. extra_selections = [ self._selection_for_character(pos)
  84. for pos in (position, match_position) ]
  85. self._text_edit.setExtraSelections(extra_selections)