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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
|
"""
This document contains the tree widget used to display the editor document
outline.
"""
import weakref
from pyqode.core import icons
from pyqode.core.panels import FoldingPanel
from pyqode.core.modes.outline import OutlineMode
from pyqode.qt import QtCore, QtGui, QtWidgets
from pyqode.core.api import TextBlockHelper, TextBlockUserData, TextHelper
class OutlineTreeWidget(QtWidgets.QTreeWidget):
"""
Displays the outline of a CodeEdit.
To use this widget:
1. add an OutlineMode to CodeEdit
2. call set_editor with a CodeEdit instance to show it's outline.
"""
sync_with_editor_changed = QtCore.Signal(bool)
@property
def sync_with_editor(self):
return self._sync_with_editor
@sync_with_editor.setter
def sync_with_editor(self, value):
if value != self.sync_with_editor:
self._sync_with_editor = value
self._action_sync.setChecked(value)
self.sync_with_editor_changed.emit(value)
def __init__(self, parent=None):
super(OutlineTreeWidget, self).__init__(parent)
self._context_actions = []
self._definitions = None
self._flattened_defs = None
self._editor = None
self._outline_mode = None
self._folding_panel = None
self._expanded_items = []
self.setHeaderHidden(True)
self.itemClicked.connect(self._on_item_clicked)
self.itemCollapsed.connect(self._on_item_state_changed)
self.itemExpanded.connect(self._on_item_state_changed)
self._updating = True
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._show_context_menu)
self._sync_with_editor = True
self._action_sync = QtWidgets.QAction(
QtGui.QIcon.fromTheme('view-refresh'), _('Sync with editor'), None)
self._action_sync.setCheckable(True)
self._action_sync.setChecked(self._sync_with_editor)
self._action_sync.toggled.connect(self._on_action_sync_toggled)
self.add_context_action(self._action_sync)
def set_editor(self, editor):
"""
Sets the current editor. The widget display the structure of that
editor.
:param editor: CodeEdit
"""
try:
self._editor.cursorPositionChanged.disconnect(self.sync)
except (AttributeError, TypeError, RuntimeError, ReferenceError):
pass
try:
self._outline_mode.document_changed.disconnect(
self._on_changed)
except (AttributeError, TypeError, RuntimeError, ReferenceError):
pass
try:
self._folding_panel.trigger_state_changed.disconnect(
self._on_block_state_changed)
except (AttributeError, TypeError, RuntimeError, ReferenceError):
pass
if editor:
self._editor = weakref.proxy(editor)
else:
self._editor = None
if editor is not None:
editor.cursorPositionChanged.connect(self.sync)
try:
self._folding_panel = weakref.proxy(
editor.panels.get(FoldingPanel))
except KeyError:
pass
else:
self._folding_panel.trigger_state_changed.connect(
self._on_block_state_changed)
try:
analyser = editor.modes.get(OutlineMode)
except KeyError:
self._outline_mode = None
else:
self._outline_mode = weakref.proxy(analyser)
analyser.document_changed.connect(self._on_changed)
self._on_changed()
def _on_item_state_changed(self, item):
if self._updating:
return
block = item.data(0, QtCore.Qt.UserRole).block
assert isinstance(item, QtWidgets.QTreeWidgetItem)
item_state = not item.isExpanded()
block_state = TextBlockHelper.is_collapsed(block)
if item_state != block_state:
self._updating = True
self._folding_panel.toggle_fold_trigger(block)
self._updating = False
def _on_block_state_changed(self, block, state):
if self._updating:
return
data = block.userData()
if data is not None:
try:
item_state = not data.tree_item.isExpanded()
if item_state != state:
if state:
self.collapseItem(data.tree_item)
else:
self.expandItem(data.tree_item)
except AttributeError:
# a block that is not represented in the tree view has
# folded/unfolded, just ignore it
pass
def _on_changed(self):
"""
Update the tree items
"""
self._updating = True
to_collapse = []
self.clear()
if self._editor and self._outline_mode and self._folding_panel:
items, to_collapse = self.to_tree_widget_items(
self._outline_mode.definitions, to_collapse=to_collapse)
if len(items):
self.addTopLevelItems(items)
self.expandAll()
for item in reversed(to_collapse):
self.collapseItem(item)
self._updating = False
return
# no data
root = QtWidgets.QTreeWidgetItem()
root.setText(0, _('No data'))
root.setIcon(0, icons.icon(
'dialog-information', ':/pyqode-icons/rc/dialog-info.png',
'fa.info-circle'))
self.addTopLevelItem(root)
self._updating = False
self.sync()
def _on_item_clicked(self, item):
"""
Go to the item position in the editor.
"""
if item:
name = item.data(0, QtCore.Qt.UserRole)
if name:
go = name.block.blockNumber()
helper = TextHelper(self._editor)
if helper.current_line_nbr() != go:
helper.goto_line(go, column=name.column)
self._editor.setFocus()
def to_tree_widget_items(self, definitions, to_collapse=None):
"""
Converts the list of top level definitions to a list of top level
tree items.
"""
def flatten(definitions):
"""
Flattens the document structure tree as a simple sequential list.
"""
ret_val = []
for de in definitions:
ret_val.append(de)
for sub_d in de.children:
ret_val.append(sub_d)
ret_val += flatten(sub_d.children)
return ret_val
def convert(name, editor, to_collapse):
ti = QtWidgets.QTreeWidgetItem()
ti.setText(0, name.name)
if isinstance(name.icon, list):
icon = QtGui.QIcon.fromTheme(
name.icon[0], QtGui.QIcon(name.icon[1]))
else:
icon = QtGui.QIcon(name.icon)
ti.setIcon(0, icon)
name.block = editor.document().findBlockByNumber(name.line)
ti.setData(0, QtCore.Qt.UserRole, name)
ti.setToolTip(0, name.description)
name.tree_item = ti
block_data = name.block.userData()
if block_data is None:
block_data = TextBlockUserData()
name.block.setUserData(block_data)
block_data.tree_item = ti
if to_collapse is not None and \
TextBlockHelper.is_collapsed(name.block):
to_collapse.append(ti)
for ch in name.children:
ti_ch, to_collapse = convert(ch, editor, to_collapse)
if ti_ch:
ti.addChild(ti_ch)
return ti, to_collapse
self._definitions = definitions
self._flattened_defs = flatten(self._definitions)
items = []
for d in definitions:
value, to_collapse = convert(d, self._editor, to_collapse)
items.append(value)
if to_collapse is not None:
return items, to_collapse
return items
def sync(self):
if not self.sync_with_editor or self._editor is None or \
not self._definitions:
return
to_select = None
previous = None
current_line = TextHelper(self._editor).current_line_nbr()
for d in self._flattened_defs:
if d.line == current_line:
to_select = d.tree_item
elif d.line > current_line:
to_select = d.tree_item
if previous is not None:
to_select = previous.tree_item
previous = d
if to_select is not None:
break
else:
if previous:
to_select = previous.tree_item
if to_select:
try:
self.setCurrentItem(to_select)
except RuntimeError:
# RuntimeError: wrapped C/C++ object of type QTreeWidgetItem
# has been deleted
pass
def add_context_action(self, action):
self._context_actions.append(action)
def _show_context_menu(self, pos):
mnu = QtWidgets.QMenu(self)
for action in self._context_actions:
mnu.addAction(action)
mnu.exec_(self.mapToGlobal(pos))
def _on_action_sync_toggled(self, value):
self.sync_with_editor = value
if value:
self.sync()
|