1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2011 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing the editor assembly widget containing the navigation
8combos and the editor widget.
9"""
10
11import contextlib
12
13from PyQt5.QtCore import QTimer
14from PyQt5.QtWidgets import QWidget, QGridLayout, QComboBox
15
16from E5Gui.E5Application import e5App
17
18import UI.PixmapCache
19import Preferences
20
21
22class EditorAssembly(QWidget):
23    """
24    Class implementing the editor assembly widget containing the navigation
25    combos and the editor widget.
26    """
27    def __init__(self, dbs, fn="", vm=None, filetype="", editor=None,
28                 tv=None):
29        """
30        Constructor
31
32        @param dbs reference to the debug server object
33        @type DebugServer
34        @param fn name of the file to be opened. If it is None,
35            a new (empty) editor is opened.
36        @type str
37        @param vm reference to the view manager object
38        @type ViewManager.ViewManager
39        @param filetype type of the source file
40        @type str
41        @param editor reference to an Editor object, if this is a cloned view
42        @type Editor
43        @param tv reference to the task viewer object
44        @type TaskViewer
45        """
46        super().__init__()
47
48        self.__layout = QGridLayout(self)
49        self.__layout.setContentsMargins(0, 0, 0, 0)
50        self.__layout.setSpacing(1)
51
52        from .EditorButtonsWidget import EditorButtonsWidget
53        from .Editor import Editor
54        from .EditorOutline import EditorOutlineView
55
56        self.__showOutline = Preferences.getEditor("ShowSourceOutline")
57
58        self.__editor = Editor(dbs, fn, vm, filetype, editor, tv)
59        self.__buttonsWidget = EditorButtonsWidget(self.__editor, self)
60        self.__globalsCombo = QComboBox()
61        self.__globalsCombo.setDuplicatesEnabled(True)
62        self.__membersCombo = QComboBox()
63        self.__membersCombo.setDuplicatesEnabled(True)
64        self.__sourceOutline = EditorOutlineView(
65            self.__editor, populate=self.__showOutline)
66        self.__sourceOutline.setMaximumWidth(
67            Preferences.getEditor("SourceOutlineWidth"))
68
69        self.__layout.addWidget(self.__buttonsWidget, 1, 0, -1, 1)
70        self.__layout.addWidget(self.__globalsCombo, 0, 1)
71        self.__layout.addWidget(self.__membersCombo, 0, 2)
72        self.__layout.addWidget(self.__editor, 1, 1, 1, 2)
73        self.__layout.addWidget(self.__sourceOutline, 0, 3, -1, -1)
74
75        self.setFocusProxy(self.__editor)
76
77        self.__module = None
78
79        self.__shutdownTimerCalled = False
80        self.__parseTimer = QTimer(self)
81        self.__parseTimer.setSingleShot(True)
82        self.__parseTimer.setInterval(5 * 1000)
83        self.__editor.textChanged.connect(self.__resetParseTimer)
84        self.__editor.refreshed.connect(self.__resetParseTimer)
85
86        self.__selectedGlobal = ""
87        self.__selectedMember = ""
88        self.__globalsBoundaries = {}
89        self.__membersBoundaries = {}
90
91        self.__activateOutline(self.__showOutline)
92        self.__activateCombos(not self.__showOutline)
93
94        e5App().getObject("UserInterface").preferencesChanged.connect(
95            self.__preferencesChanged)
96
97    def shutdownTimer(self):
98        """
99        Public method to stop and disconnect the timer.
100        """
101        self.__parseTimer.stop()
102        if not self.__shutdownTimerCalled:
103            self.__editor.textChanged.disconnect(self.__resetParseTimer)
104            self.__editor.refreshed.disconnect(self.__resetParseTimer)
105            self.__shutdownTimerCalled = True
106
107    def getEditor(self):
108        """
109        Public method to get the reference to the editor widget.
110
111        @return reference to the editor widget
112        @rtype Editor
113        """
114        return self.__editor
115
116    def __preferencesChanged(self):
117        """
118        Private slot handling a change of preferences.
119        """
120        showOutline = Preferences.getEditor("ShowSourceOutline")
121        if showOutline != self.__showOutline:
122            self.__showOutline = showOutline
123            self.__activateOutline(self.__showOutline)
124            self.__activateCombos(not self.__showOutline)
125
126    #######################################################################
127    ## Methods dealing with the navigation combos below
128    #######################################################################
129
130    def __activateCombos(self, activate):
131        """
132        Private slot to activate the navigation combo boxes.
133
134        @param activate flag indicating to activate the combo boxes
135        @type bool
136        """
137        self.__globalsCombo.setVisible(activate)
138        self.__membersCombo.setVisible(activate)
139        if activate:
140            self.__globalsCombo.activated[int].connect(
141                self.__globalsActivated)
142            self.__membersCombo.activated[int].connect(
143                self.__membersActivated)
144            self.__editor.cursorLineChanged.connect(
145                self.__editorCursorLineChanged)
146            self.__parseTimer.timeout.connect(self.__parseEditor)
147
148            self.__parseEditor()
149
150            line, _ = self.__editor.getCursorPosition()
151            self.__editorCursorLineChanged(line)
152        else:
153            with contextlib.suppress(TypeError):
154                self.__globalsCombo.activated[int].disconnect(
155                    self.__globalsActivated)
156                self.__membersCombo.activated[int].disconnect(
157                    self.__membersActivated)
158                self.__editor.cursorLineChanged.disconnect(
159                    self.__editorCursorLineChanged)
160                self.__parseTimer.timeout.disconnect(self.__parseEditor)
161
162            self.__globalsCombo.clear()
163            self.__membersCombo.clear()
164            self.__globalsBoundaries = {}
165            self.__membersBoundaries = {}
166
167    def __globalsActivated(self, index, moveCursor=True):
168        """
169        Private method to jump to the line of the selected global entry and to
170        populate the members combo box.
171
172        @param index index of the selected entry
173        @type int
174        @param moveCursor flag indicating to move the editor cursor
175        @type bool
176        """
177        # step 1: go to the line of the selected entry
178        lineno = self.__globalsCombo.itemData(index)
179        if lineno is not None:
180            if moveCursor:
181                txt = self.__editor.text(lineno - 1).rstrip()
182                pos = len(txt.replace(txt.strip(), ""))
183                self.__editor.gotoLine(
184                    lineno, pos if pos == 0 else pos + 1, True)
185                self.__editor.setFocus()
186
187            # step 2: populate the members combo, if the entry is a class
188            self.__membersCombo.clear()
189            self.__membersBoundaries = {}
190            self.__membersCombo.addItem("")
191            memberIndex = 0
192            entryName = self.__globalsCombo.itemText(index)
193            if self.__module:
194                if entryName in self.__module.classes:
195                    entry = self.__module.classes[entryName]
196                elif entryName in self.__module.modules:
197                    entry = self.__module.modules[entryName]
198                    # step 2.0: add module classes
199                    items = []
200                    for cl in entry.classes.values():
201                        if cl.isPrivate():
202                            icon = UI.PixmapCache.getIcon("class_private")
203                        elif cl.isProtected():
204                            icon = UI.PixmapCache.getIcon(
205                                "class_protected")
206                        else:
207                            icon = UI.PixmapCache.getIcon("class")
208                        items.append((icon, cl.name, cl.lineno, cl.endlineno))
209                    for itm in sorted(items, key=lambda x: (x[1], x[2])):
210                        self.__membersCombo.addItem(itm[0], itm[1], itm[2])
211                        memberIndex += 1
212                        self.__membersBoundaries[(itm[2], itm[3])] = (
213                            memberIndex
214                        )
215                else:
216                    return
217
218                # step 2.1: add class methods
219                from Utilities.ModuleParser import Function
220                items = []
221                for meth in entry.methods.values():
222                    if meth.modifier == Function.Static:
223                        icon = UI.PixmapCache.getIcon("method_static")
224                    elif meth.modifier == Function.Class:
225                        icon = UI.PixmapCache.getIcon("method_class")
226                    elif meth.isPrivate():
227                        icon = UI.PixmapCache.getIcon("method_private")
228                    elif meth.isProtected():
229                        icon = UI.PixmapCache.getIcon("method_protected")
230                    else:
231                        icon = UI.PixmapCache.getIcon("method")
232                    items.append(
233                        (icon, meth.name, meth.lineno, meth.endlineno)
234                    )
235                for itm in sorted(items, key=lambda x: (x[1], x[2])):
236                    self.__membersCombo.addItem(itm[0], itm[1], itm[2])
237                    memberIndex += 1
238                    self.__membersBoundaries[(itm[2], itm[3])] = memberIndex
239
240                # step 2.2: add class instance attributes
241                items = []
242                for attr in entry.attributes.values():
243                    if attr.isPrivate():
244                        icon = UI.PixmapCache.getIcon("attribute_private")
245                    elif attr.isProtected():
246                        icon = UI.PixmapCache.getIcon(
247                            "attribute_protected")
248                    else:
249                        icon = UI.PixmapCache.getIcon("attribute")
250                    items.append((icon, attr.name, attr.lineno))
251                for itm in sorted(items, key=lambda x: (x[1], x[2])):
252                    self.__membersCombo.addItem(itm[0], itm[1], itm[2])
253
254                # step 2.3: add class attributes
255                items = []
256                icon = UI.PixmapCache.getIcon("attribute_class")
257                for globalVar in entry.globals.values():
258                    items.append((icon, globalVar.name, globalVar.lineno))
259                for itm in sorted(items, key=lambda x: (x[1], x[2])):
260                    self.__membersCombo.addItem(itm[0], itm[1], itm[2])
261
262    def __membersActivated(self, index, moveCursor=True):
263        """
264        Private method to jump to the line of the selected members entry.
265
266        @param index index of the selected entry
267        @type int
268        @param moveCursor flag indicating to move the editor cursor
269        @type bool
270        """
271        lineno = self.__membersCombo.itemData(index)
272        if lineno is not None and moveCursor:
273            txt = self.__editor.text(lineno - 1).rstrip()
274            pos = len(txt.replace(txt.strip(), ""))
275            self.__editor.gotoLine(lineno, pos if pos == 0 else pos + 1,
276                                   firstVisible=True, expand=True)
277            self.__editor.setFocus()
278
279    def __resetParseTimer(self):
280        """
281        Private slot to reset the parse timer.
282        """
283        self.__parseTimer.stop()
284        self.__parseTimer.start()
285
286    def __parseEditor(self):
287        """
288        Private method to parse the editor source and repopulate the globals
289        combo.
290        """
291        from Utilities.ModuleParser import Module, getTypeFromTypeName
292
293        self.__module = None
294        sourceType = getTypeFromTypeName(self.__editor.determineFileType())
295        if sourceType != -1:
296            src = self.__editor.text()
297            if src:
298                fn = self.__editor.getFileName()
299                if fn is None:
300                    fn = ""
301                self.__module = Module("", fn, sourceType)
302                self.__module.scan(src)
303
304                # remember the current selections
305                self.__selectedGlobal = self.__globalsCombo.currentText()
306                self.__selectedMember = self.__membersCombo.currentText()
307
308                self.__globalsCombo.clear()
309                self.__membersCombo.clear()
310                self.__globalsBoundaries = {}
311                self.__membersBoundaries = {}
312
313                self.__globalsCombo.addItem("")
314                index = 0
315
316                # step 1: add modules
317                items = []
318                for module in self.__module.modules.values():
319                    items.append(
320                        (UI.PixmapCache.getIcon("module"), module.name,
321                         module.lineno, module.endlineno)
322                    )
323                for itm in sorted(items, key=lambda x: (x[1], x[2])):
324                    self.__globalsCombo.addItem(itm[0], itm[1], itm[2])
325                    index += 1
326                    self.__globalsBoundaries[(itm[2], itm[3])] = index
327
328                # step 2: add classes
329                items = []
330                for cl in self.__module.classes.values():
331                    if cl.isPrivate():
332                        icon = UI.PixmapCache.getIcon("class_private")
333                    elif cl.isProtected():
334                        icon = UI.PixmapCache.getIcon("class_protected")
335                    else:
336                        icon = UI.PixmapCache.getIcon("class")
337                    items.append(
338                        (icon, cl.name, cl.lineno, cl.endlineno)
339                    )
340                for itm in sorted(items, key=lambda x: (x[1], x[2])):
341                    self.__globalsCombo.addItem(itm[0], itm[1], itm[2])
342                    index += 1
343                    self.__globalsBoundaries[(itm[2], itm[3])] = index
344
345                # step 3: add functions
346                items = []
347                for func in self.__module.functions.values():
348                    if func.isPrivate():
349                        icon = UI.PixmapCache.getIcon("method_private")
350                    elif func.isProtected():
351                        icon = UI.PixmapCache.getIcon("method_protected")
352                    else:
353                        icon = UI.PixmapCache.getIcon("method")
354                    items.append(
355                        (icon, func.name, func.lineno, func.endlineno)
356                    )
357                for itm in sorted(items, key=lambda x: (x[1], x[2])):
358                    self.__globalsCombo.addItem(itm[0], itm[1], itm[2])
359                    index += 1
360                    self.__globalsBoundaries[(itm[2], itm[3])] = index
361
362                # step 4: add attributes
363                items = []
364                for globalValue in self.__module.globals.values():
365                    if globalValue.isPrivate():
366                        icon = UI.PixmapCache.getIcon("attribute_private")
367                    elif globalValue.isProtected():
368                        icon = UI.PixmapCache.getIcon(
369                            "attribute_protected")
370                    else:
371                        icon = UI.PixmapCache.getIcon("attribute")
372                    items.append(
373                        (icon, globalValue.name, globalValue.lineno)
374                    )
375                for itm in sorted(items, key=lambda x: (x[1], x[2])):
376                    self.__globalsCombo.addItem(itm[0], itm[1], itm[2])
377
378                # reset the currently selected entries without moving the
379                # text cursor
380                index = self.__globalsCombo.findText(self.__selectedGlobal)
381                if index != -1:
382                    self.__globalsCombo.setCurrentIndex(index)
383                    self.__globalsActivated(index, moveCursor=False)
384                index = self.__membersCombo.findText(self.__selectedMember)
385                if index != -1:
386                    self.__membersCombo.setCurrentIndex(index)
387                    self.__membersActivated(index, moveCursor=False)
388        else:
389            self.__globalsCombo.clear()
390            self.__membersCombo.clear()
391            self.__globalsBoundaries = {}
392            self.__membersBoundaries = {}
393
394    def __editorCursorLineChanged(self, lineno):
395        """
396        Private slot handling a line change of the cursor of the editor.
397
398        @param lineno line number of the cursor
399        @type int
400        """
401        lineno += 1     # cursor position is zero based, code info one based
402
403        # step 1: search in the globals
404        indexFound = 0
405        for (lower, upper), index in self.__globalsBoundaries.items():
406            if upper == -1:
407                upper = 1000000     # it is the last line
408            if lower <= lineno <= upper:
409                indexFound = index
410                break
411        self.__globalsCombo.setCurrentIndex(indexFound)
412        self.__globalsActivated(indexFound, moveCursor=False)
413
414        # step 2: search in members
415        indexFound = 0
416        for (lower, upper), index in self.__membersBoundaries.items():
417            if upper == -1:
418                upper = 1000000     # it is the last line
419            if lower <= lineno <= upper:
420                indexFound = index
421                break
422        self.__membersCombo.setCurrentIndex(indexFound)
423        self.__membersActivated(indexFound, moveCursor=False)
424
425    #######################################################################
426    ## Methods dealing with the source outline below
427    #######################################################################
428
429    def __activateOutline(self, activate):
430        """
431        Private slot to activate the source outline view.
432
433        @param activate flag indicating to activate the source outline view
434        @type bool
435        """
436        self.__sourceOutline.setActive(activate)
437
438        if activate:
439            self.__sourceOutline.setVisible(
440                self.__sourceOutline.isSupportedLanguage(
441                    self.__editor.getLanguage()
442                )
443            )
444
445            self.__parseTimer.timeout.connect(self.__sourceOutline.repopulate)
446            self.__editor.languageChanged.connect(self.__editorChanged)
447            self.__editor.editorRenamed.connect(self.__editorChanged)
448        else:
449            self.__sourceOutline.hide()
450
451            with contextlib.suppress(TypeError):
452                self.__parseTimer.timeout.disconnect(
453                    self.__sourceOutline.repopulate)
454                self.__editor.languageChanged.disconnect(self.__editorChanged)
455                self.__editor.editorRenamed.disconnect(self.__editorChanged)
456
457    def __editorChanged(self):
458        """
459        Private slot handling changes of the editor language or file name.
460        """
461        supported = self.__sourceOutline.isSupportedLanguage(
462            self.__editor.getLanguage())
463
464        self.__sourceOutline.setVisible(supported)
465
466#
467# eflag: noqa = Y113
468