# -*- coding:utf-8 -*-
"""
/***************************************************************************
Python Console for QGIS
                             -------------------
begin                : 2012-09-10
copyright            : (C) 2012 by Salvatore Larosa
email                : lrssvtml (at) gmail (dot) com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
Some portions of code were taken from https://code.google.com/p/pydee/
"""

import codecs
import importlib
import os
import pyclbr
import re
import sys
import tempfile
from functools import partial
from operator import itemgetter
from pathlib import Path

from qgis.core import Qgis, QgsApplication, QgsBlockingNetworkRequest, QgsFileUtils, QgsSettings
from qgis.gui import QgsCodeEditorPython, QgsMessageBar
from qgis.PyQt.Qsci import QsciScintilla
from qgis.PyQt.QtCore import QByteArray, QCoreApplication, QDir, QEvent, QFileInfo, QJsonDocument, QSize, Qt, QUrl
from qgis.PyQt.QtGui import QKeySequence
from qgis.PyQt.QtNetwork import QNetworkRequest
from qgis.PyQt.QtWidgets import (
    QApplication,
    QFileDialog,
    QFrame,
    QGridLayout,
    QLabel,
    QMenu,
    QMessageBox,
    QShortcut,
    QSizePolicy,
    QSpacerItem,
    QTabWidget,
    QToolButton,
    QTreeWidgetItem,
    QWidget,
)
from qgis.utils import OverrideCursor


class Editor(QgsCodeEditorPython):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.parent = parent
        self.path = None
        #  recent modification time
        self.lastModified = 0
        self.settings = QgsSettings()

        self.setMinimumHeight(120)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)

        # Disable command key
        ctrl, shift = self.SCMOD_CTRL << 16, self.SCMOD_SHIFT << 16
        self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord('L') + ctrl)
        self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord('T') + ctrl)
        self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord('D') + ctrl)
        self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord('L') + ctrl + shift)

        # New QShortcut = ctrl+space/ctrl+alt+space for Autocomplete
        self.newShortcutCS = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Space), self)
        self.newShortcutCS.setContext(Qt.WidgetShortcut)
        self.redoScut = QShortcut(QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_Z), self)
        self.redoScut.setContext(Qt.WidgetShortcut)
        self.redoScut.activated.connect(self.redo)
        self.newShortcutCS.activated.connect(self.autoComplete)
        self.runScut = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_E), self)
        self.runScut.setContext(Qt.WidgetShortcut)
        self.runScut.activated.connect(self.runSelectedCode)  # spellok
        self.runScriptScut = QShortcut(QKeySequence(Qt.SHIFT + Qt.CTRL + Qt.Key_E), self)
        self.runScriptScut.setContext(Qt.WidgetShortcut)
        self.runScriptScut.activated.connect(self.runScriptCode)

        self.syntaxCheckScut = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_4), self)
        self.syntaxCheckScut.setContext(Qt.WidgetShortcut)
        self.syntaxCheckScut.activated.connect(self.syntaxCheck)
        self.modificationChanged.connect(self.parent.modified)
        self.modificationAttempted.connect(self.fileReadOnly)

    def settingsEditor(self):
        # Set Python lexer
        self.initializeLexer()

    def contextMenuEvent(self, e):
        menu = QMenu(self)
        menu.addAction(
            QCoreApplication.translate("PythonConsole", "Hide Editor"),
            self.hideEditor)
        menu.addSeparator()  # ------------------------------
        syntaxCheckAction = menu.addAction(QgsApplication.getThemeIcon("console/iconSyntaxErrorConsole.svg"),
                                           QCoreApplication.translate("PythonConsole", "Check Syntax"),
                                           self.syntaxCheck, 'Ctrl+4')
        runSelected = menu.addAction(QgsApplication.getThemeIcon("console/mIconRunConsole.svg"),  # spellok
                                     QCoreApplication.translate("PythonConsole", "Run Selected"),
                                     self.runSelectedCode, 'Ctrl+E')  # spellok
        pyQGISHelpAction = menu.addAction(QgsApplication.getThemeIcon("console/iconHelpConsole.svg"),
                                          QCoreApplication.translate("PythonConsole", "Search Selected in PyQGIS docs"),
                                          self.searchSelectedTextInPyQGISDocs)
        menu.addAction(QgsApplication.getThemeIcon("mActionStart.svg"),
                       QCoreApplication.translate("PythonConsole", "Run Script"),
                       self.runScriptCode, 'Ctrl+Shift+E')
        menu.addSeparator()
        undoAction = menu.addAction(QgsApplication.getThemeIcon("mActionUndo.svg"),
                                    QCoreApplication.translate("PythonConsole", "Undo"),
                                    self.undo, QKeySequence.Undo)
        redoAction = menu.addAction(QgsApplication.getThemeIcon("mActionRedo.svg"),
                                    QCoreApplication.translate("PythonConsole", "Redo"),
                                    self.redo, 'Ctrl+Shift+Z')
        menu.addSeparator()
        menu.addAction(QgsApplication.getThemeIcon("console/iconSearchEditorConsole.svg"),
                       QCoreApplication.translate("PythonConsole", "Find Text"),
                       self.openFindWidget)
        cutAction = menu.addAction(QgsApplication.getThemeIcon("mActionEditCut.svg"),
                                   QCoreApplication.translate("PythonConsole", "Cut"),
                                   self.cut, QKeySequence.Cut)
        copyAction = menu.addAction(QgsApplication.getThemeIcon("mActionEditCopy.svg"),
                                    QCoreApplication.translate("PythonConsole", "Copy"),
                                    self.copy, QKeySequence.Copy)
        pasteAction = menu.addAction(QgsApplication.getThemeIcon("mActionEditPaste.svg"),
                                     QCoreApplication.translate("PythonConsole", "Paste"),
                                     self.paste, QKeySequence.Paste)
        selectAllAction = menu.addAction(
            QCoreApplication.translate("PythonConsole", "Select All"),
            self.selectAll, QKeySequence.SelectAll)
        menu.addSeparator()
        menu.addAction(QgsApplication.getThemeIcon("console/iconCommentEditorConsole.svg"),
                       QCoreApplication.translate("PythonConsole", "Toggle Comment"),
                       self.toggleComment, 'Ctrl+:')
        menu.addSeparator()
        gist_menu = QMenu(self)
        gist_menu.setTitle(QCoreApplication.translate("PythonConsole", "Share on GitHub"))
        gist_menu.setIcon(QgsApplication.getThemeIcon("console/iconCodepadConsole.svg"))
        gist_menu.addAction(QCoreApplication.translate("PythonConsole", "Secret Gist"),
                            partial(self.shareOnGist, False))
        gist_menu.addAction(QCoreApplication.translate("PythonConsole", "Public Gist"),
                            partial(self.shareOnGist, True))
        menu.addMenu(gist_menu)
        showCodeInspection = menu.addAction(QgsApplication.getThemeIcon("console/iconClassBrowserConsole.svg"),
                                            QCoreApplication.translate("PythonConsole", "Hide/Show Object Inspector"),
                                            self.objectListEditor)
        menu.addSeparator()
        menu.addAction(QgsApplication.getThemeIcon("console/iconSettingsConsole.svg"),
                       QCoreApplication.translate("PythonConsole", "Options…"),
                       self.pythonconsole.openSettings)
        syntaxCheckAction.setEnabled(False)
        pasteAction.setEnabled(False)
        pyQGISHelpAction.setEnabled(False)
        gist_menu.setEnabled(False)
        cutAction.setEnabled(False)
        runSelected.setEnabled(False)  # spellok
        copyAction.setEnabled(False)
        selectAllAction.setEnabled(False)
        undoAction.setEnabled(False)
        redoAction.setEnabled(False)
        showCodeInspection.setEnabled(False)
        if self.hasSelectedText():
            runSelected.setEnabled(True)  # spellok
            copyAction.setEnabled(True)
            cutAction.setEnabled(True)
            gist_menu.setEnabled(True)
            pyQGISHelpAction.setEnabled(True)
        if not self.text() == '':
            selectAllAction.setEnabled(True)
            syntaxCheckAction.setEnabled(True)
        if self.isUndoAvailable():
            undoAction.setEnabled(True)
        if self.isRedoAvailable():
            redoAction.setEnabled(True)
        if QApplication.clipboard().text():
            pasteAction.setEnabled(True)
        if self.settings.value("pythonConsole/enableObjectInsp",
                               False, type=bool):
            showCodeInspection.setEnabled(True)
        menu.exec_(self.mapToGlobal(e.pos()))

    def findText(self, forward, showMessage=True, findFirst=False):
        lineFrom, indexFrom, lineTo, indexTo = self.getSelection()
        if findFirst:
            line = 0
            index = 0
        else:
            line, index = self.getCursorPosition()
        text = self.pythonconsole.lineEditFind.text()
        re = False
        wrap = self.pythonconsole.wrapAround.isChecked()
        cs = self.pythonconsole.caseSensitive.isChecked()
        wo = self.pythonconsole.wholeWord.isChecked()
        notFound = False
        if text:
            if not forward:
                line = lineFrom
                index = indexFrom
            # findFirst(QString(), re bool, cs bool, wo bool, wrap, bool, forward=True)
            # re = Regular Expression, cs = Case Sensitive, wo = Whole Word, wrap = Wrap Around
            if not self.findFirst(text, re, cs, wo, wrap, forward, line, index):
                notFound = True
            if notFound:
                styleError = 'QLineEdit {background-color: #d65253; \
                                        color: #ffffff;}'
                if showMessage:
                    msgText = QCoreApplication.translate('PythonConsole',
                                                         '<b>"{0}"</b> was not found.').format(text)
                    self.pythonconsole.callWidgetMessageBarEditor(msgText, 0, True)
            else:
                styleError = ''
            self.pythonconsole.lineEditFind.setStyleSheet(styleError)

    def findNext(self):
        self.findText(True)

    def findPrevious(self):
        self.findText(False)

    def objectListEditor(self):
        listObj = self.pythonconsole.listClassMethod
        if listObj.isVisible():
            listObj.hide()
            self.pythonconsole.objectListButton.setChecked(False)
        else:
            listObj.show()
            self.pythonconsole.objectListButton.setChecked(True)

    def shareOnGist(self, is_public):
        ACCESS_TOKEN = self.settings.value("pythonConsole/accessTokenGithub", '', type=QByteArray)
        if not ACCESS_TOKEN:
            msg_text = QCoreApplication.translate(
                'PythonConsole', 'GitHub personal access token must be generated (see Console Options)')
            self.pythonconsole.callWidgetMessageBarEditor(msg_text, 1, True)
            return

        URL = "https://api.github.com/gists"

        path = self.tabwidget.currentWidget().path
        filename = os.path.basename(path) if path else None
        filename = filename if filename else "pyqgis_snippet.py"

        selected_text = self.selectedText()
        data = {"description": "Gist created by PyQGIS Console",
                "public": is_public,
                "files": {filename: {"content": selected_text}}}

        request = QgsBlockingNetworkRequest()
        net_req = QNetworkRequest()
        url = QUrl(URL)
        net_req.setUrl(url)
        net_req.setRawHeader(b"Authorization", b"token %s" % ACCESS_TOKEN)
        err = request.post(net_req, QJsonDocument(data).toJson())
        if not err:
            response = request.reply().content()
            json_doc = QJsonDocument()
            _json = json_doc.fromJson(response)
            link = _json.object()['html_url'].toString()
            QApplication.clipboard().setText(link)
            msg = QCoreApplication.translate('PythonConsole', 'URL copied to clipboard.')
            self.pythonconsole.callWidgetMessageBarEditor(msg, 0, True)
        else:
            msg = QCoreApplication.translate('PythonConsole', 'Connection error: ')
            self.pythonconsole.callWidgetMessageBarEditor(msg + request.erroMessage(), 0, True)

    def hideEditor(self):
        self.pythonconsole.splitterObj.hide()
        self.pythonconsole.showEditorButton.setChecked(False)

    def openFindWidget(self):
        wF = self.pythonconsole.widgetFind
        wF.show()
        if self.hasSelectedText():
            self.pythonconsole.lineEditFind.setText(self.selectedText().strip())
        self.pythonconsole.lineEditFind.setFocus()
        self.pythonconsole.findTextButton.setChecked(True)

    def closeFindWidget(self):
        wF = self.pythonconsole.widgetFind
        wF.hide()
        self.pythonconsole.findTextButton.setChecked(False)

    def toggleFindWidget(self):
        wF = self.pythonconsole.widgetFind
        if wF.isVisible():
            self.closeFindWidget()
        else:
            self.openFindWidget()

    def createTempFile(self):
        name = tempfile.NamedTemporaryFile(delete=False).name
        # Need to use newline='' to avoid adding extra \r characters on Windows
        with open(name, 'w', encoding='utf-8', newline='') as f:
            f.write(self.text())
        return name

    def runScriptCode(self):
        autoSave = self.settings.value("pythonConsole/autoSaveScript", False, type=bool)
        tabWidget = self.tabwidget.currentWidget()
        filename = tabWidget.path
        msgEditorBlank = QCoreApplication.translate('PythonConsole',
                                                    'Hey, type something to run!')
        if filename is None:
            if not self.isModified():
                self.pythonconsole.callWidgetMessageBarEditor(msgEditorBlank, 0, True)
                return

        deleteTempFile = False
        if self.syntaxCheck():
            if filename and self.isModified() and autoSave:
                self.save(filename)
            elif not filename or self.isModified():
                # Create a new temp file if the file isn't already saved.
                filename = self.createTempFile()
                deleteTempFile = True

            self.pythonconsole.shell.runCommand("exec(Path('{0}').read_text())"
                                                .format(filename.replace("\\", "/")))
            if deleteTempFile:
                Path(filename).unlink()

    def runSelectedCode(self):  # spellok
        cmd = self.selectedText()
        self.pythonconsole.shell.insertFromDropPaste(cmd)
        self.pythonconsole.shell.entered()
        self.setFocus()

    def getTextFromEditor(self):
        text = self.text()
        textList = text.split("\n")
        return textList

    def goToLine(self, objName, linenr):
        self.SendScintilla(QsciScintilla.SCI_GOTOLINE, linenr - 1)
        self.SendScintilla(QsciScintilla.SCI_SETTARGETSTART,
                           self.SendScintilla(QsciScintilla.SCI_GETCURRENTPOS))
        self.SendScintilla(QsciScintilla.SCI_SETTARGETEND, len(self.text()))
        pos = self.SendScintilla(QsciScintilla.SCI_SEARCHINTARGET, len(objName), objName)
        index = pos - self.SendScintilla(QsciScintilla.SCI_GETCURRENTPOS)
        # line, _ = self.getCursorPosition()
        self.setSelection(linenr - 1, index, linenr - 1, index + len(objName))
        self.ensureLineVisible(linenr)
        self.setFocus()

    def syntaxCheck(self):
        self.clearWarnings()
        source = self.text().encode("utf-8")
        try:
            compile(source, "", "exec")
        except SyntaxError as detail:
            eline = detail.lineno or 1
            eline -= 1
            ecolumn = detail.offset or 1
            edescr = detail.msg
            self.addWarning(eline, edescr)
            self.setCursorPosition(eline, ecolumn - 1)
            self.ensureLineVisible(eline)
            return False

        return True

    def focusInEvent(self, e):
        if self.path:
            if not QFileInfo(self.path).exists():
                msgText = QCoreApplication.translate('PythonConsole',
                                                     'The file <b>"{0}"</b> has been deleted or is not accessible').format(self.path)
                self.pythonconsole.callWidgetMessageBarEditor(msgText, 2, False)
                return
        if self.path and self.lastModified != QFileInfo(self.path).lastModified():
            self.beginUndoAction()
            self.selectAll()
            self.removeSelectedText()
            self.insert(Path(self.path).read_text(encoding='utf-8'))
            self.setModified(False)
            self.endUndoAction()

            self.tabwidget.listObject(self.tabwidget.currentWidget())
            self.lastModified = QFileInfo(self.path).lastModified()
        super().focusInEvent(e)

    def fileReadOnly(self):
        tabWidget = self.tabwidget.currentWidget()
        msgText = QCoreApplication.translate('PythonConsole',
                                             'The file <b>"{0}"</b> is read only, please save to different file first.').format(tabWidget.path)
        self.pythonconsole.callWidgetMessageBarEditor(msgText, 1, False)

    def loadFile(self, filename, readOnly=False):
        self.lastModified = QFileInfo(filename).lastModified()
        self.path = filename
        self.setText(Path(filename).read_text(encoding='utf-8'))
        self.setReadOnly(readOnly)
        self.setModified(False)
        self.recolor()

    def save(self, filename=None):
        if self.isReadOnly():
            return
        tabwidget = self.tabwidget
        index = tabwidget.indexOf(self.parent)
        if filename:
            self.path = filename
        if self.path is None:
            saveTr = QCoreApplication.translate('PythonConsole',
                                                'Python Console: Save file')
            folder = self.pythonconsole.settings.value("pythonConsole/lastDirPath", QDir.homePath())
            self.path, filter = QFileDialog().getSaveFileName(self,
                                                              saveTr,
                                                              os.path.join(folder, tabwidget.tabText(index).replace('*', '') + '.py'),
                                                              "Script file (*.py)")
            # If the user didn't select a file, abort the save operation
            if not self.path:
                self.path = None
                return

            msgText = QCoreApplication.translate('PythonConsole',
                                                 'Script was correctly saved.')
            self.pythonconsole.callWidgetMessageBarEditor(msgText, 0, True)

        # Save the new contents
        # Need to use newline='' to avoid adding extra \r characters on Windows
        with open(self.path, 'w', encoding='utf-8', newline='') as f:
            f.write(self.text())
        tabwidget.setTabTitle(index, Path(self.path).name)
        tabwidget.setTabToolTip(index, self.path)
        self.setModified(False)
        self.pythonconsole.saveFileButton.setEnabled(False)
        self.lastModified = QFileInfo(self.path).lastModified()
        self.pythonconsole.updateTabListScript(self.path, action='append')
        tabwidget.listObject(self.parent)
        lastDirPath = str(Path(self.path).parent)
        self.pythonconsole.settings.setValue("pythonConsole/lastDirPath", lastDirPath)

    def event(self, e):
        """ Used to override the Application shortcuts when the editor has focus """

        if e.type() == QEvent.ShortcutOverride:
            ctrl = e.modifiers() == Qt.ControlModifier
            ctrl_shift = e.modifiers() == (Qt.ControlModifier | Qt.ShiftModifier)
            if (
                (ctrl and e.key() == Qt.Key_W)
                or (ctrl_shift and e.key() == Qt.Key_W)
                or (ctrl and e.key() == Qt.Key_S)
                or (ctrl_shift and e.key() == Qt.Key_S)
                or (ctrl and e.key() == Qt.Key_T)
                or (ctrl and e.key() == Qt.Key_Tab)
            ):
                e.accept()
                return True

        return super().event(e)

    def keyPressEvent(self, e):
        ctrl = e.modifiers() == Qt.ControlModifier
        ctrl_shift = e.modifiers() == (Qt.ControlModifier | Qt.ShiftModifier)

        # Ctrl+W: close current tab
        if ctrl and e.key() == Qt.Key_W:
            self.parent.close()

        # Ctrl+Shift+W: close all tabs
        if ctrl_shift and e.key() == Qt.Key_W:
            self.tabwidget.closeAll()

        # Ctrl+S: save current tab
        if ctrl and e.key() == Qt.Key_S:
            self.save()

        # Ctrl+Shift+S: save current tab as
        if ctrl_shift and e.key() == Qt.Key_S:
            self.tabwidget.saveAs()

        # Ctrl+T: open new tab
        if ctrl and e.key() == Qt.Key_T:
            self.tabwidget.newTabEditor()

        super().keyPressEvent(e)


class EditorTab(QWidget):

    def __init__(self, parent, pythonconsole, filename, readOnly):
        super().__init__(parent)
        self.tabwidget = parent

        self._editor = Editor(self)
        self._editor.pythonconsole = pythonconsole
        self._editor.tabwidget = parent
        if filename:
            if QFileInfo(filename).exists():
                self._editor.loadFile(filename, readOnly)

        # Creates layout for message bar
        self.layout = QGridLayout(self._editor)
        self.layout.setContentsMargins(0, 0, 0, 0)
        spacerItem = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.layout.addItem(spacerItem, 1, 0, 1, 1)
        # messageBar instance
        self.infoBar = QgsMessageBar()
        sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
        self.infoBar.setSizePolicy(sizePolicy)
        self.layout.addWidget(self.infoBar, 0, 0, 1, 1)

        self.tabLayout = QGridLayout(self)
        self.tabLayout.setContentsMargins(0, 0, 0, 0)
        self.tabLayout.addWidget(self._editor)

    def modified(self, modified):
        self.tabwidget.tabModified(self, modified)

    def close(self):
        self.tabwidget._removeTab(self, tab2index=True)

    def __getattr__(self, name):
        """ Forward all missing attribute requests to the editor."""
        try:
            return super().__getattr__(name)
        except AttributeError:
            return getattr(self._editor, name)

    def __setattr__(self, name, value):
        """ Forward all missing attribute requests to the editor."""
        try:
            return super().__setattr__(name, value)
        except AttributeError:
            return setattr(self._editor, name, value)


class EditorTabWidget(QTabWidget):

    def __init__(self, parent):
        super().__init__(parent=None)
        self.parent = parent

        self.settings = QgsSettings()

        self.idx = -1
        # Layout for top frame (restore tabs)
        self.layoutTopFrame = QGridLayout(self)
        self.layoutTopFrame.setContentsMargins(0, 0, 0, 0)
        spacerItem = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.layoutTopFrame.addItem(spacerItem, 1, 0, 1, 1)
        self.topFrame = QFrame(self)
        self.topFrame.setStyleSheet('background-color: rgb(255, 255, 230);')
        self.topFrame.setFrameShape(QFrame.StyledPanel)
        self.topFrame.setMinimumHeight(24)
        self.layoutTopFrame2 = QGridLayout(self.topFrame)
        self.layoutTopFrame2.setContentsMargins(0, 0, 0, 0)
        label = QCoreApplication.translate("PythonConsole",
                                           "Click on button to restore all tabs from last session.")
        self.label = QLabel(label)

        self.restoreTabsButton = QToolButton()
        toolTipRestore = QCoreApplication.translate("PythonConsole",
                                                    "Restore tabs")
        self.restoreTabsButton.setToolTip(toolTipRestore)
        self.restoreTabsButton.setIcon(QgsApplication.getThemeIcon("console/iconRestoreTabsConsole.svg"))
        self.restoreTabsButton.setIconSize(QSize(24, 24))
        self.restoreTabsButton.setAutoRaise(True)
        self.restoreTabsButton.setCursor(Qt.PointingHandCursor)
        self.restoreTabsButton.setStyleSheet('QToolButton:hover{border: none } \
                                              QToolButton:pressed{border: none}')

        self.clButton = QToolButton()
        toolTipClose = QCoreApplication.translate("PythonConsole",
                                                  "Close")
        self.clButton.setToolTip(toolTipClose)
        self.clButton.setIcon(QgsApplication.getThemeIcon("/mIconClose.svg"))
        self.clButton.setIconSize(QSize(18, 18))
        self.clButton.setCursor(Qt.PointingHandCursor)
        self.clButton.setStyleSheet('QToolButton:hover{border: none } \
                                     QToolButton:pressed{border: none}')
        self.clButton.setAutoRaise(True)

        sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
        self.topFrame.setSizePolicy(sizePolicy)
        self.layoutTopFrame.addWidget(self.topFrame, 0, 0, 1, 1)
        self.layoutTopFrame2.addWidget(self.label, 0, 1, 1, 1)
        self.layoutTopFrame2.addWidget(self.restoreTabsButton, 0, 0, 1, 1)
        self.layoutTopFrame2.addWidget(self.clButton, 0, 2, 1, 1)

        self.topFrame.hide()
        self.restoreTabsButton.clicked.connect(self.restoreTabs)
        self.clButton.clicked.connect(self.closeRestore)

        # Fixes #7653
        if sys.platform != 'darwin':
            self.setDocumentMode(True)

        self.setMovable(True)
        self.setTabsClosable(True)
        self.setTabPosition(QTabWidget.North)

        # Menu button list tabs
        self.fileTabMenu = QMenu()
        self.fileTabMenu.aboutToShow.connect(self.showFileTabMenu)
        self.fileTabMenu.triggered.connect(self.showFileTabMenuTriggered)
        self.fileTabButton = QToolButton()
        txtToolTipMenuFile = QCoreApplication.translate("PythonConsole",
                                                        "List all tabs")
        self.fileTabButton.setToolTip(txtToolTipMenuFile)
        self.fileTabButton.setIcon(QgsApplication.getThemeIcon("console/iconFileTabsMenuConsole.svg"))
        self.fileTabButton.setIconSize(QSize(24, 24))
        self.fileTabButton.setAutoRaise(True)
        self.fileTabButton.setPopupMode(QToolButton.InstantPopup)
        self.fileTabButton.setMenu(self.fileTabMenu)
        self.setCornerWidget(self.fileTabButton, Qt.TopRightCorner)
        self.tabCloseRequested.connect(self._removeTab)
        self.currentChanged.connect(self._currentWidgetChanged)

        # New Editor button
        self.newTabButton = QToolButton()
        txtToolTipNewTab = QCoreApplication.translate("PythonConsole",
                                                      "New Editor")
        self.newTabButton.setToolTip(txtToolTipNewTab)
        self.newTabButton.setAutoRaise(True)
        self.newTabButton.setIcon(QgsApplication.getThemeIcon("console/iconNewTabEditorConsole.svg"))
        self.newTabButton.setIconSize(QSize(24, 24))
        self.setCornerWidget(self.newTabButton, Qt.TopLeftCorner)
        self.newTabButton.clicked.connect(self.newTabEditor)

    def _currentWidgetChanged(self, tab):
        if self.settings.value("pythonConsole/enableObjectInsp",
                               False, type=bool):
            self.listObject(tab)
        self.changeLastDirPath(tab)
        self.enableSaveIfModified(tab)

    def contextMenuEvent(self, e):
        tabBar = self.tabBar()
        self.idx = tabBar.tabAt(e.pos())
        if self.widget(self.idx):
            cW = self.widget(self.idx)
            menu = QMenu(self)
            menu.addSeparator()
            menu.addAction(
                QCoreApplication.translate("PythonConsole", "New Editor"),
                self.newTabEditor)
            menu.addSeparator()
            closeTabAction = menu.addAction(
                QCoreApplication.translate("PythonConsole", "Close Tab"),
                cW.close)
            closeAllTabAction = menu.addAction(
                QCoreApplication.translate("PythonConsole", "Close All"),
                self.closeAll)
            closeOthersTabAction = menu.addAction(
                QCoreApplication.translate("PythonConsole", "Close Others"),
                self.closeOthers)
            menu.addSeparator()
            saveAction = menu.addAction(
                QCoreApplication.translate("PythonConsole", "Save"),
                cW.save)
            menu.addAction(
                QCoreApplication.translate("PythonConsole", "Save As"),
                self.saveAs)
            closeTabAction.setEnabled(False)
            closeAllTabAction.setEnabled(False)
            closeOthersTabAction.setEnabled(False)
            saveAction.setEnabled(False)
            if self.count() > 1:
                closeTabAction.setEnabled(True)
                closeAllTabAction.setEnabled(True)
                closeOthersTabAction.setEnabled(True)
            if self.widget(self.idx).isModified():
                saveAction.setEnabled(True)
            menu.exec_(self.mapToGlobal(e.pos()))

    def closeOthers(self):
        idx = self.idx
        countTab = self.count()
        for i in list(range(countTab - 1, idx, -1)) + list(range(idx - 1, -1, -1)):
            self._removeTab(i)

    def closeAll(self):
        countTab = self.count()
        for i in range(countTab - 1, 0, -1):
            self._removeTab(i)

        self.newTabEditor(tabName=QCoreApplication.translate("PythonConsole", "Untitled-0"))
        self._removeTab(0)

    def saveAs(self):
        self.parent.saveAsScriptFile(self.idx)

    def enableSaveIfModified(self, tab):
        tabWidget = self.widget(tab)
        if tabWidget:
            self.parent.saveFileButton.setEnabled(tabWidget.isModified())

    def enableToolBarEditor(self, enable):
        if self.topFrame.isVisible():
            enable = False
        self.parent.toolBarEditor.setEnabled(enable)

    def newTabEditor(self, tabName=None, filename=None):
        readOnly = False
        if filename:
            readOnly = not QFileInfo(filename).isWritable()
            try:
                fn = codecs.open(filename, "rb", encoding='utf-8')
                fn.read()
                fn.close()
            except IOError as error:
                IOErrorTr = QCoreApplication.translate('PythonConsole',
                                                       'The file {0} could not be opened. Error: {1}\n').format(filename,
                                                                                                                error.strerror)
                print('## Error: ')
                sys.stderr.write(IOErrorTr)
                return

        nr = self.count()
        if not tabName:
            tabName = QCoreApplication.translate('PythonConsole', 'Untitled-{0}').format(nr)
        tab = EditorTab(self, self.parent, filename, readOnly)
        self.iconTab = QgsApplication.getThemeIcon('console/iconTabEditorConsole.svg')
        self.addTab(tab, self.iconTab, tabName + ' (ro)' if readOnly else tabName)
        self.setCurrentWidget(tab)
        if filename:
            self.setTabToolTip(self.currentIndex(), filename)
        else:
            self.setTabToolTip(self.currentIndex(), tabName)

    def tabModified(self, tab, modified):
        index = self.indexOf(tab)
        s = self.tabText(index)
        self.setTabTitle(index, '*{}'.format(s) if modified else re.sub(r'^(\*)', '', s))
        self.parent.saveFileButton.setEnabled(modified)

    def setTabTitle(self, tab, title):
        self.setTabText(tab, title)

    def _removeTab(self, tab, tab2index=False):
        if tab2index:
            tab = self.indexOf(tab)
        editorTab = self.widget(tab)
        if editorTab.isModified():
            txtSaveOnRemove = QCoreApplication.translate("PythonConsole",
                                                         "Python Console: Save File")
            txtMsgSaveOnRemove = QCoreApplication.translate("PythonConsole",
                                                            "The file <b>'{0}'</b> has been modified, save changes?").format(self.tabText(tab))
            res = QMessageBox.question(self, txtSaveOnRemove,
                                       txtMsgSaveOnRemove,
                                       QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel)
            if res == QMessageBox.Cancel:
                return
            if res == QMessageBox.Save:
                editorTab.save()
            if editorTab.path:
                self.parent.updateTabListScript(editorTab.path, action='remove')
            self.removeTab(tab)
            if self.count() < 1:
                self.newTabEditor()
        else:
            if editorTab.path:
                self.parent.updateTabListScript(editorTab.path, action='remove')
            if self.count() <= 1:
                self.removeTab(tab)
                self.newTabEditor()
            else:
                self.removeTab(tab)

        editorTab.deleteLater()
        self.currentWidget()._editor.setFocus(Qt.TabFocusReason)

    def restoreTabsOrAddNew(self):
        """
        Restore tabs if they are found in the settings. If none are found it will add a new empty tab.
        """
        # Restore scripts from the previous session
        tabScripts = self.settings.value("pythonConsole/tabScripts", [])
        self.restoreTabList = tabScripts

        if self.restoreTabList:
            self.restoreTabs()
        else:
            self.newTabEditor(filename=None)

    def restoreTabs(self):
        for script in self.restoreTabList:
            pathFile = script
            if QFileInfo(pathFile).exists():
                tabName = pathFile.split('/')[-1]
                self.newTabEditor(tabName, pathFile)
            else:
                errOnRestore = QCoreApplication.translate("PythonConsole",
                                                          "Unable to restore the file: \n{0}\n").format(pathFile)
                print('## Error: ')
                s = errOnRestore
                sys.stderr.write(s)
                self.parent.updateTabListScript(pathFile, action='remove')
        if self.count() < 1:
            self.newTabEditor(filename=None)
        self.topFrame.close()
        self.enableToolBarEditor(True)
        self.currentWidget()._editor.setFocus(Qt.TabFocusReason)

    def closeRestore(self):
        self.parent.updateTabListScript(None)
        self.topFrame.close()
        self.newTabEditor(filename=None)
        self.enableToolBarEditor(True)

    def showFileTabMenu(self):
        self.fileTabMenu.clear()
        for index in range(self.count()):
            action = self.fileTabMenu.addAction(self.tabIcon(index), self.tabText(index))
            action.setData(index)

    def showFileTabMenuTriggered(self, action):
        index = action.data()
        if index is not None:
            self.setCurrentIndex(index)

    def listObject(self, tab):
        self.parent.listClassMethod.clear()
        if isinstance(tab, EditorTab):
            tabWidget = self.widget(self.indexOf(tab))
        else:
            tabWidget = self.widget(tab)
        if tabWidget:
            if tabWidget.path:
                pathFile, file = os.path.split(tabWidget.path)
                module, ext = os.path.splitext(file)
                found = False
                if pathFile not in sys.path:
                    sys.path.append(pathFile)
                    found = True
                try:
                    importlib.reload(pyclbr)  # NOQA
                    dictObject = {}
                    readModule = pyclbr.readmodule(module)
                    readModuleFunction = pyclbr.readmodule_ex(module)
                    for name, class_data in sorted(list(readModule.items()), key=lambda x: x[1].lineno):
                        if os.path.normpath(class_data.file) == os.path.normpath(tabWidget.path):
                            superClassName = []
                            for superClass in class_data.super:
                                if superClass == 'object':
                                    continue
                                if isinstance(superClass, str):
                                    superClassName.append(superClass)
                                else:
                                    superClassName.append(superClass.name)
                            classItem = QTreeWidgetItem()
                            if superClassName:
                                super = ', '.join([i for i in superClassName])
                                classItem.setText(0, name + ' [' + super + ']')
                                classItem.setToolTip(0, name + ' [' + super + ']')
                            else:
                                classItem.setText(0, name)
                                classItem.setToolTip(0, name)
                            if sys.platform.startswith('win'):
                                classItem.setSizeHint(0, QSize(18, 18))
                            classItem.setText(1, str(class_data.lineno))
                            iconClass = QgsApplication.getThemeIcon("console/iconClassTreeWidgetConsole.svg")
                            classItem.setIcon(0, iconClass)
                            dictObject[name] = class_data.lineno
                            for meth, lineno in sorted(list(class_data.methods.items()), key=itemgetter(1)):
                                methodItem = QTreeWidgetItem()
                                methodItem.setText(0, meth + ' ')
                                methodItem.setText(1, str(lineno))
                                methodItem.setToolTip(0, meth)
                                iconMeth = QgsApplication.getThemeIcon("console/iconMethodTreeWidgetConsole.svg")
                                methodItem.setIcon(0, iconMeth)
                                if sys.platform.startswith('win'):
                                    methodItem.setSizeHint(0, QSize(18, 18))
                                classItem.addChild(methodItem)
                                dictObject[meth] = lineno
                            self.parent.listClassMethod.addTopLevelItem(classItem)
                    for func_name, data in sorted(list(readModuleFunction.items()), key=lambda x: x[1].lineno):
                        if isinstance(data, pyclbr.Function) and \
                           os.path.normpath(data.file) == os.path.normpath(tabWidget.path):
                            funcItem = QTreeWidgetItem()
                            funcItem.setText(0, func_name + ' ')
                            funcItem.setText(1, str(data.lineno))
                            funcItem.setToolTip(0, func_name)
                            iconFunc = QgsApplication.getThemeIcon("console/iconFunctionTreeWidgetConsole.svg")
                            funcItem.setIcon(0, iconFunc)
                            if sys.platform.startswith('win'):
                                funcItem.setSizeHint(0, QSize(18, 18))
                            dictObject[func_name] = data.lineno
                            self.parent.listClassMethod.addTopLevelItem(funcItem)
                    if found:
                        sys.path.remove(pathFile)
                except:
                    msgItem = QTreeWidgetItem()
                    msgItem.setText(0, QCoreApplication.translate("PythonConsole", "Check Syntax"))
                    msgItem.setText(1, 'syntaxError')
                    iconWarning = QgsApplication.getThemeIcon("console/iconSyntaxErrorConsole.svg")
                    msgItem.setIcon(0, iconWarning)
                    self.parent.listClassMethod.addTopLevelItem(msgItem)

    def refreshSettingsEditor(self):
        objInspectorEnabled = self.settings.value("pythonConsole/enableObjectInsp",
                                                  False, type=bool)
        listObj = self.parent.objectListButton
        if self.parent.listClassMethod.isVisible():
            listObj.setChecked(objInspectorEnabled)
        listObj.setEnabled(objInspectorEnabled)
        if objInspectorEnabled:
            cW = self.currentWidget()
            if cW and not self.parent.listClassMethod.isVisible():
                with OverrideCursor(Qt.WaitCursor):
                    self.listObject(cW)

    def changeLastDirPath(self, tab):
        tabWidget = self.widget(tab)
        if tabWidget and tabWidget.path:
            self.settings.setValue("pythonConsole/lastDirPath", tabWidget.path)

    def widgetMessageBar(self, iface, text, level, timed=True):
        messageLevel = [Qgis.Info, Qgis.Warning, Qgis.Critical]
        if timed:
            timeout = iface.messageTimeout()
        else:
            timeout = 0
        currWidget = self.currentWidget()
        currWidget.infoBar.pushMessage(text, messageLevel[level], timeout)
