Exemplo n.º 1
0
class HgClient(QObject):
    """
    Class implementing the Mercurial command server interface.
    """
    InputFormat = ">I"
    OutputFormat = ">cI"
    OutputFormatSize = struct.calcsize(OutputFormat)
    ReturnFormat = ">i"
    
    Channels = (b"I", b"L", b"o", b"e", b"r", b"d")
    
    def __init__(self, repoPath, encoding, vcs, parent=None):
        """
        Constructor
        
        @param repoPath root directory of the repository (string)
        @param encoding encoding to be used by the command server (string)
        @param vcs reference to the VCS object (Hg)
        @param parent reference to the parent object (QObject)
        """
        super(HgClient, self).__init__(parent)
        
        self.__server = None
        self.__started = False
        self.__version = None
        self.__encoding = vcs.getEncoding()
        self.__cancel = False
        self.__commandRunning = False
        self.__repoPath = repoPath
        
        # generate command line and environment
        self.__serverArgs = vcs.initCommand("serve")
        self.__serverArgs.append("--cmdserver")
        self.__serverArgs.append("pipe")
        self.__serverArgs.append("--config")
        self.__serverArgs.append("ui.interactive=True")
        if repoPath:
            self.__serverArgs.append("--repository")
            self.__serverArgs.append(repoPath)
        
        if encoding:
            self.__encoding = encoding
            if "--encoding" in self.__serverArgs:
                # use the defined encoding via the environment
                index = self.__serverArgs.index("--encoding")
                del self.__serverArgs[index:index + 2]
    
    def startServer(self):
        """
        Public method to start the command server.
        
        @return tuple of flag indicating a successful start (boolean) and
            an error message (string) in case of failure
        """
        self.__server = QProcess()
        self.__server.setWorkingDirectory(self.__repoPath)
        
        # connect signals
        self.__server.finished.connect(self.__serverFinished)
        
        prepareProcess(self.__server, self.__encoding)
        
        self.__server.start('hg', self.__serverArgs)
        serverStarted = self.__server.waitForStarted(5000)
        if not serverStarted:
            return False, self.tr(
                'The process {0} could not be started. '
                'Ensure, that it is in the search path.'
            ).format('hg')
        
        self.__server.setReadChannel(QProcess.StandardOutput)
        ok, error = self.__readHello()
        self.__started = ok
        return ok, error
    
    def stopServer(self):
        """
        Public method to stop the command server.
        """
        if self.__server is not None:
            self.__server.closeWriteChannel()
            res = self.__server.waitForFinished(5000)
            if not res:
                self.__server.terminate()
                res = self.__server.waitForFinished(3000)
                if not res:
                    self.__server.kill()
                    self.__server.waitForFinished(3000)
            
            self.__started = False
            self.__server.deleteLater()
            self.__server = None
    
    def restartServer(self):
        """
        Public method to restart the command server.
        
        @return tuple of flag indicating a successful start (boolean) and
            an error message (string) in case of failure
        """
        self.stopServer()
        return self.startServer()
    
    def __readHello(self):
        """
        Private method to read the hello message sent by the command server.
        
        @return tuple of flag indicating success (boolean) and an error message
            in case of failure (string)
        """
        ch, msg = self.__readChannel()
        if not ch:
            return False, self.tr("Did not receive the 'hello' message.")
        elif ch != "o":
            return False, self.tr("Received data on unexpected channel.")
        
        msg = msg.split("\n")
        
        if not msg[0].startswith("capabilities: "):
            return False, self.tr(
                "Bad 'hello' message, expected 'capabilities: '"
                " but got '{0}'.").format(msg[0])
        self.__capabilities = msg[0][len('capabilities: '):]
        if not self.__capabilities:
            return False, self.tr("'capabilities' message did not contain"
                                  " any capability.")
        
        self.__capabilities = set(self.__capabilities.split())
        if "runcommand" not in self.__capabilities:
            return False, "'capabilities' did not contain 'runcommand'."
        
        if not msg[1].startswith("encoding: "):
            return False, self.tr(
                "Bad 'hello' message, expected 'encoding: '"
                " but got '{0}'.").format(msg[1])
        encoding = msg[1][len('encoding: '):]
        if not encoding:
            return False, self.tr("'encoding' message did not contain"
                                  " any encoding.")
        self.__encoding = encoding
        
        return True, ""
    
    def __serverFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__started = False
    
    def __readChannel(self):
        """
        Private method to read data from the command server.
        
        @return tuple of channel designator and channel data
            (string, integer or string or bytes)
        """
        if self.__server.bytesAvailable() > 0 or \
           self.__server.waitForReadyRead(10000):
            data = bytes(self.__server.peek(HgClient.OutputFormatSize))
            if not data or len(data) < HgClient.OutputFormatSize:
                return "", ""
            
            channel, length = struct.unpack(HgClient.OutputFormat, data)
            channel = channel.decode(self.__encoding)
            if channel in "IL":
                self.__server.read(HgClient.OutputFormatSize)
                return channel, length
            else:
                if self.__server.bytesAvailable() < \
                        HgClient.OutputFormatSize + length:
                    return "", ""
                self.__server.read(HgClient.OutputFormatSize)
                data = self.__server.read(length)
                if channel == "r":
                    return (channel, data)
                else:
                    return (channel, str(data, self.__encoding, "replace"))
        else:
            return "", ""
    
    def __writeDataBlock(self, data):
        """
        Private slot to write some data to the command server.
        
        @param data data to be sent (string)
        """
        if not isinstance(data, bytes):
            data = data.encode(self.__encoding)
        self.__server.write(
            QByteArray(struct.pack(HgClient.InputFormat, len(data))))
        self.__server.write(QByteArray(data))
        self.__server.waitForBytesWritten()
    
    def __runcommand(self, args, inputChannels, outputChannels):
        """
        Private method to run a command in the server (low level).
        
        @param args list of arguments for the command (list of string)
        @param inputChannels dictionary of input channels. The dictionary must
            have the keys 'I' and 'L' and each entry must be a function
            receiving the number of bytes to write.
        @param outputChannels dictionary of output channels. The dictionary
            must have the keys 'o' and 'e' and each entry must be a function
            receiving the data.
        @return result code of the command, -1 if the command server wasn't
            started or -10, if the command was canceled (integer)
        @exception RuntimeError raised to indicate an unexpected command
            channel
        """
        if not self.__started:
            return -1
        
        self.__server.write(QByteArray(b'runcommand\n'))
        self.__writeDataBlock('\0'.join(args))
        
        while True:
            QCoreApplication.processEvents()
            
            if self.__cancel:
                return -10
            
            if self.__server is None:
                return -1
            
            if self.__server is None or self.__server.bytesAvailable() == 0:
                QThread.msleep(50)
                continue
            channel, data = self.__readChannel()
            
            # input channels
            if channel in inputChannels:
                input = inputChannels[channel](data)
                if channel == "L":
                    # echo the input to the output if it was a prompt
                    outputChannels["o"](input)
                self.__writeDataBlock(input)
            
            # output channels
            elif channel in outputChannels:
                outputChannels[channel](data)
            
            # result channel, command is finished
            elif channel == "r":
                return struct.unpack(HgClient.ReturnFormat, data)[0]
            
            # unexpected but required channel
            elif channel.isupper():
                raise RuntimeError(
                    "Unexpected but required channel '{0}'.".format(channel))
            
            # optional channels or no channel at all
            else:
                pass
    
    def __prompt(self, size, message):
        """
        Private method to prompt the user for some input.
        
        @param size maximum length of the requested input (integer)
        @param message message sent by the server (string)
        @return data entered by the user (string)
        """
        from .HgClientPromptDialog import HgClientPromptDialog
        input = ""
        dlg = HgClientPromptDialog(size, message)
        if dlg.exec_() == QDialog.Accepted:
            input = dlg.getInput() + '\n'
        return input
    
    def runcommand(self, args, prompt=None, input=None, output=None,
                   error=None):
        """
        Public method to execute a command via the command server.
        
        @param args list of arguments for the command (list of string)
        @keyparam prompt function to reply to prompts by the server. It
            receives the max number of bytes to return and the contents
            of the output channel received so far.
        @keyparam input function to reply to bulk data requests by the server.
            It receives the max number of bytes to return.
        @keyparam output function receiving the data from the server (string).
            If a prompt function is given, this parameter will be ignored.
        @keyparam error function receiving error messages from the server
            (string)
        @return output and errors of the command server (string). In case
            output and/or error functions were given, the respective return
            value will be an empty string.
        """
        self.__commandRunning = True
        outputChannels = {}
        outputBuffer = None
        errorBuffer = None
        
        if prompt is not None or output is None:
            outputBuffer = io.StringIO()
            outputChannels["o"] = outputBuffer.write
        else:
            outputChannels["o"] = output
        if error:
            outputChannels["e"] = error
        else:
            errorBuffer = io.StringIO()
            outputChannels["e"] = errorBuffer.write
        
        inputChannels = {}
        if prompt is not None:
            def func(size):
                reply = prompt(size, outputBuffer.getvalue())
                return reply
            inputChannels["L"] = func
        else:
            def myprompt(size):
                if outputBuffer is None:
                    msg = self.tr("For message see output dialog.")
                else:
                    msg = outputBuffer.getvalue()
                reply = self.__prompt(size, msg)
                return reply
            inputChannels["L"] = myprompt
        if input is not None:
            inputChannels["I"] = input
        
        self.__cancel = False
        self.__runcommand(args, inputChannels, outputChannels)
        if outputBuffer:
            out = outputBuffer.getvalue()
        else:
            out = ""
        if errorBuffer:
            err = errorBuffer.getvalue()
        else:
            err = ""
        
        self.__commandRunning = False
        
        return out, err
    
    def cancel(self):
        """
        Public method to cancel the running command.
        """
        self.__cancel = True
        self.restartServer()
    
    def wasCanceled(self):
        """
        Public method to check, if the last command was canceled.
        
        @return flag indicating the cancel state (boolean)
        """
        return self.__cancel
    
    def isExecuting(self):
        """
        Public method to check, if the server is executing a command.
        
        @return flag indicating the execution of a command (boolean)
        """
        return self.__commandRunning
Exemplo n.º 2
0
class HgClient(QObject):
    """
    Class implementing the Mercurial command server interface.
    """
    InputFormat = ">I"
    OutputFormat = ">cI"
    OutputFormatSize = struct.calcsize(OutputFormat)
    ReturnFormat = ">i"
    
    Channels = (b"I", b"L", b"o", b"e", b"r", b"d")
    
    def __init__(self, repoPath, encoding, vcs, parent=None):
        """
        Constructor
        
        @param repoPath root directory of the repository (string)
        @param encoding encoding to be used by the command server (string)
        @param vcs reference to the VCS object (Hg)
        @param parent reference to the parent object (QObject)
        """
        super(HgClient, self).__init__(parent)
        
        self.__server = None
        self.__started = False
        self.__version = None
        self.__encoding = vcs.getEncoding()
        self.__cancel = False
        self.__commandRunning = False
        self.__repoPath = repoPath
        
        # generate command line and environment
        self.__serverArgs = vcs.initCommand("serve")
        self.__serverArgs.append("--cmdserver")
        self.__serverArgs.append("pipe")
        self.__serverArgs.append("--config")
        self.__serverArgs.append("ui.interactive=True")
        if repoPath:
            self.__serverArgs.append("--repository")
            self.__serverArgs.append(repoPath)
        
        if encoding:
            self.__encoding = encoding
            if "--encoding" in self.__serverArgs:
                # use the defined encoding via the environment
                index = self.__serverArgs.index("--encoding")
                del self.__serverArgs[index:index + 2]
    
    def startServer(self):
        """
        Public method to start the command server.
        
        @return tuple of flag indicating a successful start (boolean) and
            an error message (string) in case of failure
        """
        self.__server = QProcess()
        self.__server.setWorkingDirectory(self.__repoPath)
        
        # connect signals
        self.__server.finished.connect(self.__serverFinished)
        
        prepareProcess(self.__server, self.__encoding)
        
        self.__server.start('hg', self.__serverArgs)
        serverStarted = self.__server.waitForStarted(5000)
        if not serverStarted:
            return False, self.tr(
                'The process {0} could not be started. '
                'Ensure, that it is in the search path.'
            ).format('hg')
        
        self.__server.setReadChannel(QProcess.StandardOutput)
        ok, error = self.__readHello()
        self.__started = ok
        return ok, error
    
    def stopServer(self):
        """
        Public method to stop the command server.
        """
        if self.__server is not None:
            self.__server.closeWriteChannel()
            res = self.__server.waitForFinished(5000)
            if not res:
                self.__server.terminate()
                res = self.__server.waitForFinished(3000)
                if not res:
                    self.__server.kill()
                    self.__server.waitForFinished(3000)
            
            self.__started = False
            self.__server.deleteLater()
            self.__server = None
    
    def restartServer(self):
        """
        Public method to restart the command server.
        
        @return tuple of flag indicating a successful start (boolean) and
            an error message (string) in case of failure
        """
        self.stopServer()
        return self.startServer()
    
    def __readHello(self):
        """
        Private method to read the hello message sent by the command server.
        
        @return tuple of flag indicating success (boolean) and an error message
            in case of failure (string)
        """
        ch, msg = self.__readChannel()
        if not ch:
            return False, self.tr("Did not receive the 'hello' message.")
        elif ch != "o":
            return False, self.tr("Received data on unexpected channel.")
        
        msg = msg.split("\n")
        
        if not msg[0].startswith("capabilities: "):
            return False, self.tr(
                "Bad 'hello' message, expected 'capabilities: '"
                " but got '{0}'.").format(msg[0])
        self.__capabilities = msg[0][len('capabilities: '):]
        if not self.__capabilities:
            return False, self.tr("'capabilities' message did not contain"
                                  " any capability.")
        
        self.__capabilities = set(self.__capabilities.split())
        if "runcommand" not in self.__capabilities:
            return False, "'capabilities' did not contain 'runcommand'."
        
        if not msg[1].startswith("encoding: "):
            return False, self.tr(
                "Bad 'hello' message, expected 'encoding: '"
                " but got '{0}'.").format(msg[1])
        encoding = msg[1][len('encoding: '):]
        if not encoding:
            return False, self.tr("'encoding' message did not contain"
                                  " any encoding.")
        self.__encoding = encoding
        
        return True, ""
    
    def __serverFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__started = False
    
    def __readChannel(self):
        """
        Private method to read data from the command server.
        
        @return tuple of channel designator and channel data
            (string, integer or string or bytes)
        """
        if self.__server.bytesAvailable() > 0 or \
           self.__server.waitForReadyRead(10000):
            data = bytes(self.__server.peek(HgClient.OutputFormatSize))
            if not data or len(data) < HgClient.OutputFormatSize:
                return "", ""
            
            channel, length = struct.unpack(HgClient.OutputFormat, data)
            channel = channel.decode(self.__encoding)
            if channel in "IL":
                self.__server.read(HgClient.OutputFormatSize)
                return channel, length
            else:
                if self.__server.bytesAvailable() < \
                        HgClient.OutputFormatSize + length:
                    return "", ""
                self.__server.read(HgClient.OutputFormatSize)
                data = self.__server.read(length)
                if channel == "r":
                    return (channel, data)
                else:
                    return (channel, str(data, self.__encoding, "replace"))
        else:
            return "", ""
    
    def __writeDataBlock(self, data):
        """
        Private slot to write some data to the command server.
        
        @param data data to be sent (string)
        """
        if not isinstance(data, bytes):
            data = data.encode(self.__encoding)
        self.__server.write(
            QByteArray(struct.pack(HgClient.InputFormat, len(data))))
        self.__server.write(QByteArray(data))
        self.__server.waitForBytesWritten()
    
    def __runcommand(self, args, inputChannels, outputChannels):
        """
        Private method to run a command in the server (low level).
        
        @param args list of arguments for the command (list of string)
        @param inputChannels dictionary of input channels. The dictionary must
            have the keys 'I' and 'L' and each entry must be a function
            receiving the number of bytes to write.
        @param outputChannels dictionary of output channels. The dictionary
            must have the keys 'o' and 'e' and each entry must be a function
            receiving the data.
        @return result code of the command, -1 if the command server wasn't
            started or -10, if the command was canceled (integer)
        @exception RuntimeError raised to indicate an unexpected command
            channel
        """
        if not self.__started:
            return -1
        
        self.__server.write(QByteArray(b'runcommand\n'))
        self.__writeDataBlock('\0'.join(args))
        
        while True:
            QCoreApplication.processEvents()
            
            if self.__cancel:
                return -10
            
            if self.__server is None:
                return -1
            
            if self.__server is None or self.__server.bytesAvailable() == 0:
                QThread.msleep(50)
                continue
            channel, data = self.__readChannel()
            
            # input channels
            if channel in inputChannels:
                input = inputChannels[channel](data)
                if channel == "L":
                    # echo the input to the output if it was a prompt
                    outputChannels["o"](input)
                self.__writeDataBlock(input)
            
            # output channels
            elif channel in outputChannels:
                outputChannels[channel](data)
            
            # result channel, command is finished
            elif channel == "r":
                return struct.unpack(HgClient.ReturnFormat, data)[0]
            
            # unexpected but required channel
            elif channel.isupper():
                raise RuntimeError(
                    "Unexpected but required channel '{0}'.".format(channel))
            
            # optional channels or no channel at all
            else:
                pass
    
    def __prompt(self, size, message):
        """
        Private method to prompt the user for some input.
        
        @param size maximum length of the requested input (integer)
        @param message message sent by the server (string)
        @return data entered by the user (string)
        """
        from .HgClientPromptDialog import HgClientPromptDialog
        input = ""
        dlg = HgClientPromptDialog(size, message)
        if dlg.exec_() == QDialog.Accepted:
            input = dlg.getInput() + '\n'
        return input
    
    def runcommand(self, args, prompt=None, input=None, output=None,
                   error=None):
        """
        Public method to execute a command via the command server.
        
        @param args list of arguments for the command (list of string)
        @keyparam prompt function to reply to prompts by the server. It
            receives the max number of bytes to return and the contents
            of the output channel received so far.
        @keyparam input function to reply to bulk data requests by the server.
            It receives the max number of bytes to return.
        @keyparam output function receiving the data from the server (string).
            If a prompt function is given, this parameter will be ignored.
        @keyparam error function receiving error messages from the server
            (string)
        @return output and errors of the command server (string). In case
            output and/or error functions were given, the respective return
            value will be an empty string.
        """
        self.__commandRunning = True
        outputChannels = {}
        outputBuffer = None
        errorBuffer = None
        
        if prompt is not None or output is None:
            outputBuffer = io.StringIO()
            outputChannels["o"] = outputBuffer.write
        else:
            outputChannels["o"] = output
        if error:
            outputChannels["e"] = error
        else:
            errorBuffer = io.StringIO()
            outputChannels["e"] = errorBuffer.write
        
        inputChannels = {}
        if prompt is not None:
            def func(size):
                reply = prompt(size, outputBuffer.getvalue())
                return reply
            inputChannels["L"] = func
        else:
            def myprompt(size):
                if outputBuffer is None:
                    msg = self.tr("For message see output dialog.")
                else:
                    msg = outputBuffer.getvalue()
                reply = self.__prompt(size, msg)
                return reply
            inputChannels["L"] = myprompt
        if input is not None:
            inputChannels["I"] = input
        
        self.__cancel = False
        self.__runcommand(args, inputChannels, outputChannels)
        if outputBuffer:
            out = outputBuffer.getvalue()
        else:
            out = ""
        if errorBuffer:
            err = errorBuffer.getvalue()
        else:
            err = ""
        
        self.__commandRunning = False
        
        return out, err
    
    def cancel(self):
        """
        Public method to cancel the running command.
        """
        self.__cancel = True
        self.restartServer()
    
    def wasCanceled(self):
        """
        Public method to check, if the last command was canceled.
        
        @return flag indicating the cancel state (boolean)
        """
        return self.__cancel
    
    def isExecuting(self):
        """
        Public method to check, if the server is executing a command.
        
        @return flag indicating the execution of a command (boolean)
        """
        return self.__commandRunning
Exemplo n.º 3
0
class ConsoleWidget(QPlainTextEdit):
    def __init__(self):
        super(ConsoleWidget, self).__init__('>>> ')
        self.setUndoRedoEnabled(False)
        self.setFrameShape(0)
        self.apply_editor_style()
        self.setToolTip(self.tr("Show/Hide (F4)"))
        self.moveCursor(QTextCursor.EndOfLine)

        self._patIsWord = re.compile('\w+')
        self.prompt = '>>> '
        self._console = console.Console()
        self._history = []
        self.history_index = 0
        self._current_command = ''
        self._braces = None
        self.imports = ['import __builtin__']
        self.patFrom = re.compile('^(\\s)*from ((\\w)+(\\.)*(\\w)*)+ import')
        self.patImport = re.compile('^(\\s)*import (\\w)+')
        self.patObject = re.compile('[^a-zA-Z0-9_\\.]')
        # self.completer = completer_widget.CompleterWidget(self)
        self.okPrefix = QRegExp('[.)}:,\]]')

        self._pre_key_press = {
            Qt.Key_Enter: self._enter_pressed,
            Qt.Key_Return: self._enter_pressed,
            Qt.Key_Tab: self._tab_pressed,
            Qt.Key_Home: self._home_pressed,
            Qt.Key_PageUp: lambda x: True,
            Qt.Key_PageDown: lambda x: True,
            Qt.Key_Left: self._left_pressed,
            Qt.Key_Up: self._up_pressed,
            Qt.Key_Down: self._down_pressed,
            Qt.Key_Backspace: self._backspace,
        }

        # Create Context Menu
        self._create_context_menu()

        #Set Font
        self.set_font(settings.FONT)
        #Create Highlighter
        # parts_scanner, code_scanner, formats = \
        #    syntax_highlighter.load_syntax(python_syntax.syntax)
        # self.highlighter = syntax_highlighter.SyntaxHighlighter(
        #    self.document(),
        #    parts_scanner, code_scanner, formats)

        self.cursorPositionChanged.connect(self.highlight_current_line)
        self.highlight_current_line()

        self._proc = QProcess(self)
        self._proc.readyReadStandardOutput.connect(self._python_path_detected)
        self._proc.error[QProcess.ProcessError].connect(self.process_error)
        self._add_system_path_for_frozen()

        # ninjaide = IDE.get_service('ide')
        # self.connect(ninjaide,
        #    SIGNAL("ns_preferences_editor_font(PyQt_PyObject)"),
        #    self.set_font)

    def _add_system_path_for_frozen(self):
        try:
            self._proc.start(settings.PYTHON_EXEC, [resources.GET_SYSTEM_PATH])
        except Exception as reason:
            logger.warning('Could not get system path, error: %r' % reason)

    def _python_path_detected(self):
        paths = self._proc.readAllStandardOutput().data().decode('utf8')
        add_system_path = ('import sys; '
                           'sys.path = list(set(sys.path + %s))' % paths)
        self._write(add_system_path)
        self._proc.deleteLater()

    def process_error(self, error):
        message = ''
        if error == 0:
            message = 'Failed to start'
        else:
            message = 'Error during execution, QProcess error: %d' % error
        logger.warning('Could not get system path, error: %r' % message)

    def set_font(self, font):
        self.document().setDefaultFont(font)
        # Fix for older version of Qt which doens't has ForceIntegerMetrics
        if "ForceIntegerMetrics" in dir(QFont):
            self.document().defaultFont().setStyleStrategy(
                QFont.ForceIntegerMetrics)

    def _create_context_menu(self):
        self.popup_menu = self.createStandardContextMenu()

        self.popup_menu.clear()

        actionCut = self.popup_menu.addAction(self.tr("Cut"))
        actionCopy = self.popup_menu.addAction(self.tr("Copy"))
        actionPaste = self.popup_menu.addAction(self.tr("Paste"))
        actionClean = self.popup_menu.addAction(self.tr("Clean Console"))
        actionCopyHistory = self.popup_menu.addAction(self.tr("Copy History"))
        actionCopyConsoleContent = self.popup_menu.addAction(
            self.tr("Copy Console Content"))

        self.popup_menu.addAction(actionCut)
        self.popup_menu.addAction(actionCopy)
        self.popup_menu.addAction(actionPaste)
        self.popup_menu.addSeparator()
        self.popup_menu.addAction(actionClean)
        self.popup_menu.addSeparator()
        self.popup_menu.addAction(actionCopyHistory)
        self.popup_menu.addAction(actionCopyConsoleContent)

        # Connections
        actionCut.triggered.connect(self._cut)
        actionCopy.triggered.connect(self.copy)
        actionPaste.triggered.connect(self._paste)
        actionClean.triggered.connect(self._clean_console)
        actionCopyHistory.triggered.connect(self._copy_history)
        actionCopyConsoleContent.triggered.connect(self._copy_console_content)

    def _cut(self):
        event = QKeyEvent(QEvent.KeyPress, Qt.Key_X, Qt.ControlModifier, "x")
        self.keyPressEvent(event)

    def _paste(self):
        if self.textCursor().hasSelection():
            self.moveCursor(QTextCursor.End)
        self.paste()

    def _clean_console(self):
        self.clear()
        self._add_prompt()

    def _copy_history(self):
        historyContent = '\n'.join(self._history)
        clipboard = QApplication.instance().clipboard()
        clipboard.setText(historyContent)

    def _copy_console_content(self):
        content = self.toPlainText()
        clipboard = QApplication.instance().clipboard()
        clipboard.setText(content)

    def setCursorPosition(self, position, mode=QTextCursor.MoveAnchor):
        self.moveCursor(QTextCursor.StartOfLine, mode)
        for i in range(len(self.prompt) + position):
            self.moveCursor(QTextCursor.Right, mode)

    def _check_event_on_selection(self, event):
        if event.text():
            cursor = self.textCursor()
            begin_last_block = (self.document().lastBlock().position() +
                                len(self.prompt))
            if cursor.hasSelection() and \
               ((cursor.selectionEnd() < begin_last_block) or
               (cursor.selectionStart() < begin_last_block)):
                self.moveCursor(QTextCursor.End)

    def _enter_pressed(self, event):
        self._write_command()
        return True

    def _tab_pressed(self, event):
        self.textCursor().insertText(' ' * settings.INDENT)
        return True

    def _home_pressed(self, event):
        if event.modifiers() == Qt.ShiftModifier:
            self.setCursorPosition(0, QTextCursor.KeepAnchor)
        else:
            self.setCursorPosition(0)
        return True

    def _left_pressed(self, event):
        return self._get_cursor_position() == 0

    def _up_pressed(self, event):
        if self.history_index == len(self._history):
            command = self.document().lastBlock().text()[len(self.prompt):]
            self._current_command = command
        self._set_command(self._get_prev_history_entry())
        return True

    def _down_pressed(self, event):
        if len(self._history) == self.history_index:
            command = self._current_command
        else:
            command = self._get_next_history_entry()
        self._set_command(command)
        return True

    def _backspace(self, event):
        cursor = self.textCursor()
        selected_text = cursor.selectedText()
        cursor.movePosition(QTextCursor.StartOfLine, QTextCursor.KeepAnchor)
        text = cursor.selectedText()[len(self.prompt):]
        if (len(text) % settings.INDENT == 0) and text.isspace():
            cursor.movePosition(QTextCursor.StartOfLine)
            cursor.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor,
                                settings.INDENT)
            cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor,
                                settings.INDENT)
            cursor.removeSelectedText()
            return True
        elif (selected_text == self.document().lastBlock().text()
              [len(self.prompt):]):
            self.textCursor().removeSelectedText()
            return True

        position = self.textCursor().positionInBlock() - len(self.prompt)
        text = self.document().lastBlock().text()[len(self.prompt):]
        if position < len(text):
            if (text[position - 1] in BRACES
                    and text[position] in BRACES.values()):
                self.textCursor().deleteChar()

        return self._get_cursor_position() == 0

    def keyPressEvent(self, event):
        #if self.completer.popup().isVisible():
        #if event.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab):
        #event.ignore()
        #self.completer.popup().hide()
        #return
        #elif event.key in (Qt.Key_Space, Qt.Key_Escape, Qt.Key_Backtab):
        #self.completer.popup().hide()

        self._check_event_on_selection(event)
        if self._pre_key_press.get(event.key(), lambda x: False)(event):
            return

        if event.text() in (set(BRACES.values()) - set(["'", '"'])):
            cursor = self.textCursor()
            cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor)
            brace = cursor.selection().toPlainText()
            cursor = self.textCursor()
            cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor)
            braceClose = cursor.selection().toPlainText()
            if BRACES.get(brace, False) == event.text() and \
              braceClose == event.text():
                self.moveCursor(QTextCursor.Right)
                return

        QPlainTextEdit.keyPressEvent(self, event)

        if event.text() in BRACES:
            cursor = self.textCursor()
            cursor.movePosition(QTextCursor.StartOfLine,
                                QTextCursor.KeepAnchor)
            self.textCursor().insertText(BRACES[event.text()])
            self.moveCursor(QTextCursor.Left)

        #completionPrefix = self._text_under_cursor()
        #if event.key() == Qt.Key_Period or (event.key() == Qt.Key_Space and
        #event.modifiers() == Qt.ControlModifier):
        #self.completer.setCompletionPrefix(completionPrefix)
        #self._resolve_completion_argument()
        #if self.completer.popup().isVisible() and \
        #completionPrefix != self.completer.completionPrefix():
        #self.completer.setCompletionPrefix(completionPrefix)
        #self.completer.popup().setCurrentIndex(
        #self.completer.completionModel().index(0, 0))
        #self.completer.setCurrentRow(0)
        #self._resolve_completion_argument()

    #def _resolve_completion_argument(self):
    #try:
    #cursor = self.textCursor()
    #cursor.movePosition(QTextCursor.StartOfLine,
    #QTextCursor.KeepAnchor)
    #var = cursor.selectedText()
    #chars = self.patObject.findall(var)
    #var = var[var.rfind(chars[-1]) + 1:]
    #cr = self.cursorRect()
    #proposals = completer.get_all_completions(var,
    #imports=self.imports)
    #if not proposals:
    #if self.completer.popup().isVisible():
    #prefix = var[var.rfind('.') + 1:]
    #var = var[:var.rfind('.') + 1]
    #var = self._console.get_type(var)
    #var += prefix
    #else:
    #var = self._console.get_type(var)
    #proposals = completer.get_all_completions(var,
    #imports=self.imports)
    #self.completer.complete(cr, proposals)
    #except:
    #self.completer.popup().hide()

    def highlight_current_line(self):
        self.extraSelections = []
        selection = QTextEdit.ExtraSelection()
        lineColor = QColor(
            resources.CUSTOM_SCHEME.get('CurrentLine',
                                        resources.COLOR_SCHEME['CurrentLine']))
        lineColor.setAlpha(20)
        selection.format.setBackground(lineColor)
        selection.format.setProperty(QTextFormat.FullWidthSelection, True)
        selection.cursor = self.textCursor()
        selection.cursor.clearSelection()
        self.extraSelections.append(selection)
        self.setExtraSelections(self.extraSelections)

        if self._braces is not None:
            self._braces = None
        cursor = self.textCursor()
        if cursor.position() == 0:
            self.setExtraSelections(self.extraSelections)
            return
        cursor.movePosition(QTextCursor.PreviousCharacter,
                            QTextCursor.KeepAnchor)
        text = cursor.selectedText()
        pos1 = cursor.position()
        if text in (')', ']', '}'):
            pos2 = self._match_braces(pos1, text, forward=False)
        elif text in ('(', '[', '{'):
            pos2 = self._match_braces(pos1, text, forward=True)
        else:
            self.setExtraSelections(self.extraSelections)
            return
        #if pos2 is not None:
        #self._braces = (pos1, pos2)
        #selection = QTextEdit.ExtraSelection()
        #selection.format.setForeground(QColor(
        #resources.CUSTOM_SCHEME.get('brace-foreground',
        #resources.COLOR_SCHEME.get('brace-foreground'))))
        #selection.format.setBackground(QColor(
        #resources.CUSTOM_SCHEME.get('brace-background',
        #resources.COLOR_SCHEME.get('brace-background'))))
        #selection.cursor = cursor
        #self.extraSelections.append(selection)
        #selection = QTextEdit.ExtraSelection()
        #selection.format.setForeground(QColor(
        #resources.CUSTOM_SCHEME.get('brace-foreground',
        #resources.COLOR_SCHEME.get('brace-foreground'))))
        #selection.format.setBackground(QColor(
        #resources.CUSTOM_SCHEME.get('brace-background',
        #resources.COLOR_SCHEME.get('brace-background'))))
        #selection.cursor = self.textCursor()
        #selection.cursor.setPosition(pos2)
        #selection.cursor.movePosition(QTextCursor.NextCharacter,
        #QTextCursor.KeepAnchor)
        #self.extraSelections.append(selection)
        #else:
        #self._braces = (pos1,)
        #selection = QTextEdit.ExtraSelection()
        #selection.format.setBackground(QColor(
        #resources.CUSTOM_SCHEME.get('brace-background',
        #resources.COLOR_SCHEME.get('brace-background'))))
        #selection.format.setForeground(QColor(
        #resources.CUSTOM_SCHEME.get('brace-foreground',
        #resources.COLOR_SCHEME.get('brace-foreground'))))
        #selection.cursor = cursor
        #self.extraSelections.append(selection)
        self.setExtraSelections(self.extraSelections)

    def _text_under_cursor(self):
        tc = self.textCursor()
        tc.select(QTextCursor.WordUnderCursor)
        return tc.selectedText()

    def get_selection(self, posStart, posEnd):
        cursor = self.textCursor()
        cursor.setPosition(posStart)
        if posEnd == QTextCursor.End:
            cursor2 = self.textCursor()
            cursor2.movePosition(posEnd)
            cursor.setPosition(cursor2.position(), QTextCursor.KeepAnchor)
        else:
            cursor.setPosition(posEnd, QTextCursor.KeepAnchor)
        return cursor.selectedText()

    def _match_braces(self, position, brace, forward):
        """based on: http://gitorious.org/khteditor"""
        if forward:
            braceMatch = {'(': ')', '[': ']', '{': '}'}
            text = self.get_selection(position, QTextCursor.End)
            braceOpen, braceClose = 1, 1
        else:
            braceMatch = {')': '(', ']': '[', '}': '{'}
            text = self.get_selection(QTextCursor.Start, position)
            braceOpen, braceClose = len(text) - 1, len(text) - 1
        while True:
            if forward:
                posClose = text.find(braceMatch[brace], braceClose)
            else:
                posClose = text.rfind(braceMatch[brace], 0, braceClose + 1)
            if posClose > -1:
                if forward:
                    braceClose = posClose + 1
                    posOpen = text.find(brace, braceOpen, posClose)
                else:
                    braceClose = posClose - 1
                    posOpen = text.rfind(brace, posClose, braceOpen + 1)
                if posOpen > -1:
                    if forward:
                        braceOpen = posOpen + 1
                    else:
                        braceOpen = posOpen - 1
                else:
                    if forward:
                        return position + posClose
                    else:
                        return position - (len(text) - posClose)
            else:
                return

    def _add_prompt(self, incomplete=False):
        if incomplete:
            prompt = '.' * 3 + ' '
        else:
            prompt = self.prompt
        self.appendPlainText(prompt)
        self.moveCursor(QTextCursor.End)

    def _get_cursor_position(self):
        return self.textCursor().columnNumber() - len(self.prompt)

    def _write_command(self):
        text = self.textCursor().block().text()
        command = text[len(self.prompt):]
        incomplete = False
        print(command)
        if command:
            self._add_history(command)
            incomplete = self._write(command)
            output = self._read()
            if output:
                self.appendPlainText(output)
        self._add_prompt(incomplete)
        """command = self.document().lastBlock().text()
        #remove the prompt from the QString
        command = command[len(self.prompt):]
        self._add_history(command)
        conditional = command.strip() != 'quit()'
        incomplete = self._write(command) if conditional else None
        if self.patFrom.match(command) or self.patImport.match(command):
            self.imports += [command]
        if not incomplete:
            output = self._read()
            if output is not None:
                if isinstance(output, str):
                    output = output.encode('utf8')
                self.appendPlainText(output.decode('utf8'))
        # self._add_prompt(incomplete)"""

    def _set_command(self, command):
        self.moveCursor(QTextCursor.End)
        cursor = self.textCursor()
        cursor.movePosition(QTextCursor.StartOfLine, QTextCursor.KeepAnchor)
        cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor,
                            len(self.prompt))
        cursor.insertText(command)

    def contextMenuEvent(self, event):
        self.popup_menu.exec_(event.globalPos())

    def _write(self, line):
        return self._console.push(line)

    def _read(self):
        return self._console.output

    def _add_history(self, command):
        if command and (not self._history or self._history[-1] != command):
            self._history.append(command)
        self.history_index = len(self._history)

    def _get_prev_history_entry(self):
        if self._history:
            self.history_index = max(0, self.history_index - 1)
            return self._history[self.history_index]
        return ''

    def _get_next_history_entry(self):
        if self._history:
            hist_len = len(self._history) - 1
            self.history_index = min(hist_len, self.history_index + 1)
            index = self.history_index
            if self.history_index == hist_len:
                self.history_index += 1
            return self._history[index]
        return ''

    def restyle(self):
        self.apply_editor_style()
        parts_scanner, code_scanner, formats = \
            syntax_highlighter.load_syntax(python_syntax.syntax)
        self.highlighter = syntax_highlighter.SyntaxHighlighter(
            self.document(), parts_scanner, code_scanner, formats)

    def apply_editor_style(self):
        css = 'QPlainTextEdit {color: %s; background-color: %s;' \
            'selection-color: %s; selection-background-color: %s;}' \
            % (resources.CUSTOM_SCHEME.get('editor-text',
            resources.COLOR_SCHEME['Default']),
            resources.CUSTOM_SCHEME.get('EditorBackground',
                resources.COLOR_SCHEME['EditorBackground']),
            resources.CUSTOM_SCHEME.get('EditorSelectionColor',
                resources.COLOR_SCHEME['EditorSelectionColor']),
            resources.CUSTOM_SCHEME.get('EditorSelectionBackground',
                resources.COLOR_SCHEME['EditorSelectionBackground']))
        self.setStyleSheet(css)

    def load_project_into_console(self, projectFolder):
        """Load the projectFolder received into the sys.path."""
        self._console.push("import sys; sys.path += ['%s']" % projectFolder)

    def unload_project_from_console(self, projectFolder):
        """Unload the project from the system path."""
        self._console.push("import sys; "
                           "sys.path = [path for path in sys.path "
                           "if path != '%s']" % projectFolder)

    def zoom_in(self):
        font = self.document().defaultFont()
        size = font.pointSize()
        if size < settings.FONT_MAX_SIZE:
            size += 2
            font.setPointSize(size)
        self.setFont(font)

    def zoom_out(self):
        font = self.document().defaultFont()
        size = font.pointSize()
        if size > settings.FONT_MIN_SIZE:
            size -= 2
            font.setPointSize(size)
        self.setFont(font)

    def wheelEvent(self, event):
        if event.modifiers() == Qt.ControlModifier:
            if event.delta() > 0:
                self.zoom_in()
            elif event.delta() < 0:
                self.zoom_out()
            event.ignore()
        super(ConsoleWidget, self).wheelEvent(event)
Exemplo n.º 4
0
class TVLinker(QWidget):
    def __init__(self, settings: QSettings, parent=None):
        super(TVLinker, self).__init__(parent)
        self.firstrun = True
        self.rows, self.cols = 0, 0
        self.parent = parent
        self.settings = settings
        self.taskbar = TaskbarProgress(self)
        self.init_styles()
        self.init_settings()
        self.init_icons()
        if sys.platform.startswith('linux'):
            notify.init(qApp.applicationName())
        layout = QVBoxLayout()
        layout.setSpacing(0)
        layout.setContentsMargins(15, 15, 15, 0)
        form_groupbox = QGroupBox(self, objectName='mainForm')
        form_groupbox.setLayout(self.init_form())
        self.table = TVLinkerTable(0, 4, self)
        self.table.doubleClicked.connect(self.show_hosters)
        layout.addWidget(form_groupbox)
        layout.addWidget(self.table)
        layout.addLayout(self.init_metabar())
        self.setLayout(layout)
        qApp.setWindowIcon(self.icon_app)
        self.resize(FixedSettings.windowSize)
        self.show()
        self.start_scraping()
        self.firstrun = False

    class ProcError(Enum):
        FAILED_TO_START = 0
        CRASHED = 1
        TIMED_OUT = 2
        READ_ERROR = 3
        WRITE_ERROR = 4
        UNKNOWN_ERROR = 5

    class NotifyIcon(Enum):
        SUCCESS = ':assets/images/tvlinker.png'
        DEFAULT = ':assets/images/tvlinker.png'

    def init_threads(self, threadtype: str = 'scrape') -> None:
        if threadtype == 'scrape':
            if hasattr(self, 'scrapeThread'):
                if not sip.isdeleted(
                        self.scrapeThread) and self.scrapeThread.isRunning():
                    self.scrapeThread.terminate()
                    del self.scrapeWorker
                    del self.scrapeThread
            self.scrapeThread = QThread(self)
            self.scrapeWorker = ScrapeWorker(self.source_url, self.user_agent,
                                             self.dl_pagecount)
            self.scrapeThread.started.connect(self.show_progress)
            self.scrapeThread.started.connect(self.scrapeWorker.begin)
            self.scrapeWorker.moveToThread(self.scrapeThread)
            self.scrapeWorker.addRow.connect(self.add_row)
            self.scrapeWorker.workFinished.connect(self.scrape_finished)
            self.scrapeWorker.workFinished.connect(
                self.scrapeWorker.deleteLater, Qt.DirectConnection)
            self.scrapeWorker.workFinished.connect(self.scrapeThread.quit,
                                                   Qt.DirectConnection)
            self.scrapeThread.finished.connect(self.scrapeThread.deleteLater,
                                               Qt.DirectConnection)
        elif threadtype == 'unrestrict':
            pass

    @staticmethod
    def load_stylesheet(qssfile: str) -> None:
        if QFileInfo(qssfile).exists():
            qss = QFile(qssfile)
            qss.open(QFile.ReadOnly | QFile.Text)
            qApp.setStyleSheet(QTextStream(qss).readAll())

    def init_styles(self) -> None:
        if sys.platform == 'darwin':
            qss_stylesheet = self.get_path('%s_osx.qss' %
                                           qApp.applicationName().lower())
        else:
            qss_stylesheet = self.get_path('%s.qss' %
                                           qApp.applicationName().lower())
        TVLinker.load_stylesheet(qss_stylesheet)
        QFontDatabase.addApplicationFont(':assets/fonts/opensans.ttf')
        QFontDatabase.addApplicationFont(':assets/fonts/opensans-bold.ttf')
        QFontDatabase.addApplicationFont(':assets/fonts/opensans-semibold.ttf')
        qApp.setFont(QFont('Open Sans',
                           12 if sys.platform == 'darwin' else 10))

    def init_icons(self) -> None:
        self.icon_app = QIcon(
            self.get_path('images/%s.png' % qApp.applicationName().lower()))
        self.icon_faves_off = QIcon(':assets/images/star_off.png')
        self.icon_faves_on = QIcon(':assets/images/star_on.png')
        self.icon_refresh = QIcon(':assets/images/refresh.png')
        self.icon_menu = QIcon(':assets/images/menu.png')
        self.icon_settings = QIcon(':assets/images/cog.png')
        self.icon_updates = QIcon(':assets/images/cloud.png')

    def init_settings(self) -> None:
        self.provider = 'Scene-RLS'
        self.select_provider(0)
        self.user_agent = self.settings.value('user_agent')
        self.dl_pagecount = self.settings.value('dl_pagecount', 20, int)
        self.dl_pagelinks = FixedSettings.linksPerPage
        self.realdebrid_api_token = self.settings.value('realdebrid_apitoken')
        self.realdebrid_api_proxy = self.settings.value('realdebrid_apiproxy')
        self.download_manager = self.settings.value('download_manager')
        self.persepolis_cmd = self.settings.value('persepolis_cmd')
        self.pyload_host = self.settings.value('pyload_host')
        self.pyload_username = self.settings.value('pyload_username')
        self.pyload_password = self.settings.value('pyload_password')
        self.idm_exe_path = self.settings.value('idm_exe_path')
        self.kget_cmd = self.settings.value('kget_cmd')
        self.favorites = self.settings.value('favorites')

    def init_form(self) -> QHBoxLayout:
        self.search_field = QLineEdit(self,
                                      clearButtonEnabled=True,
                                      placeholderText='Enter search criteria')
        self.search_field.setObjectName('searchInput')
        self.search_field.setSizePolicy(QSizePolicy.Expanding,
                                        QSizePolicy.Fixed)
        self.search_field.setFocus()
        self.search_field.textChanged.connect(self.clear_filters)
        self.search_field.returnPressed.connect(
            lambda: self.filter_table(self.search_field.text()))
        self.favorites_button = QPushButton(parent=self,
                                            flat=True,
                                            cursor=Qt.PointingHandCursor,
                                            objectName='favesButton',
                                            toolTip='Favorites',
                                            checkable=True,
                                            toggled=self.filter_faves,
                                            checked=self.settings.value(
                                                'faves_filter', False, bool))
        self.refresh_button = QPushButton(parent=self,
                                          flat=True,
                                          cursor=Qt.PointingHandCursor,
                                          objectName='refreshButton',
                                          toolTip='Refresh',
                                          clicked=self.start_scraping)
        self.dlpages_field = QComboBox(self,
                                       toolTip='Pages',
                                       editable=False,
                                       cursor=Qt.PointingHandCursor)
        self.dlpages_field.addItems(
            ('10', '20', '30', '40', '50', '60', '70', '80'))
        self.dlpages_field.setCurrentIndex(
            self.dlpages_field.findText(str(self.dl_pagecount),
                                        Qt.MatchFixedString))
        self.dlpages_field.currentIndexChanged.connect(self.update_pagecount)
        self.settings_button = QPushButton(parent=self,
                                           flat=True,
                                           toolTip='Menu',
                                           objectName='menuButton',
                                           cursor=Qt.PointingHandCursor)
        self.settings_button.setMenu(self.settings_menu())
        layout = QHBoxLayout(spacing=10)
        # providerCombo = QComboBox(self, toolTip='Provider', editable=False, cursor=Qt.PointingHandCursor)
        # providerCombo.setObjectName('providercombo')
        # providerCombo.addItem(QIcon(':assets/images/provider-scenerls.png'), '')
        # providerCombo.addItem(QIcon(':assets/images/provider-tvrelease.png'), '')
        # providerCombo.setIconSize(QSize(146, 36))
        # providerCombo.setMinimumSize(QSize(160, 40))
        # providerCombo.setStyleSheet('''
        #     QComboBox, QComboBox::drop-down { background-color: transparent; border: none; margin: 5px; }
        #     QComboBox::down-arrow { image: url(:assets/images/down_arrow.png); }
        #     QComboBox QAbstractItemView { selection-background-color: #DDDDE4; }
        # ''')
        # providerCombo.currentIndexChanged.connect(self.select_provider)
        layout.addWidget(
            QLabel(pixmap=QPixmap(':assets/images/provider-scenerls.png')))
        layout.addWidget(self.search_field)
        layout.addWidget(self.favorites_button)
        layout.addWidget(self.refresh_button)
        layout.addWidget(QLabel('Pages:'))
        layout.addWidget(self.dlpages_field)
        layout.addWidget(self.settings_button)
        return layout

    @pyqtSlot(int)
    def select_provider(self, index: int):
        if index == 0:
            self.provider = 'Scene-RLS'
            self.source_url = 'http://scene-rls.net/releases/index.php?p={0}&cat=TV%20Shows'
        elif index == 1:
            self.provider = 'TV-Release'
            self.source_url = 'http://tv-release.pw/?cat=TV'
        self.setWindowTitle('%s :: %s' %
                            (qApp.applicationName(), self.provider))

    def settings_menu(self) -> QMenu:
        settings_action = QAction(self.icon_settings,
                                  'Settings',
                                  self,
                                  triggered=self.show_settings)
        updates_action = QAction(self.icon_updates,
                                 'Check for updates',
                                 self,
                                 triggered=self.check_update)
        aboutqt_action = QAction('About Qt', self, triggered=qApp.aboutQt)
        about_action = QAction('About %s' % qApp.applicationName(),
                               self,
                               triggered=self.about_app)
        menu = QMenu()
        menu.addAction(settings_action)
        menu.addAction(updates_action)
        menu.addSeparator()
        menu.addAction(aboutqt_action)
        menu.addAction(about_action)
        return menu

    def init_metabar(self) -> QHBoxLayout:
        self.meta_template = 'Total number of links retrieved: <b>%i</b> / <b>%i</b>'
        self.progress = QProgressBar(parent=self,
                                     minimum=0,
                                     maximum=(self.dl_pagecount *
                                              self.dl_pagelinks),
                                     visible=False)
        self.taskbar.setProgress(0.0, True)
        if sys.platform == 'win32':
            self.win_taskbar_button = QWinTaskbarButton(self)

        self.meta_label = QLabel(textFormat=Qt.RichText,
                                 alignment=Qt.AlignRight,
                                 objectName='totals')
        self.update_metabar()
        layout = QHBoxLayout()
        layout.setContentsMargins(10, 5, 10, 10)
        layout.addWidget(self.progress, Qt.AlignLeft)
        layout.addWidget(self.meta_label, Qt.AlignRight)
        return layout

    @pyqtSlot()
    def check_update(self) -> None:
        QDesktopServices.openUrl(QUrl(FixedSettings.latest_release_url))

    @pyqtSlot()
    def show_settings(self) -> None:
        settings_win = Settings(self, self.settings)
        settings_win.exec_()

    def update_metabar(self) -> bool:
        rowcount = self.table.rowCount()
        self.meta_label.setText(
            self.meta_template %
            (rowcount, self.dl_pagecount * self.dl_pagelinks))
        self.progress.setValue(rowcount)
        self.taskbar.setProgress(rowcount / self.progress.maximum())
        if sys.platform == 'win32':
            self.win_taskbar_button.progress().setValue(self.progress.value())
        return True

    def start_scraping(self) -> None:
        self.init_threads('scrape')
        self.rows = 0
        self.table.clearContents()
        self.table.setRowCount(0)
        self.table.setSortingEnabled(False)
        self.update_metabar()
        self.scrapeThread.start()

    @pyqtSlot()
    def about_app(self) -> None:
        about_html = '''<style>
        a { color:#441d4e; text-decoration:none; font-weight:bold; }
        a:hover { text-decoration:underline; }
    </style>
    <p style="font-size:24pt; font-weight:bold; color:#6A687D;">%s</p>
    <p>
        <span style="font-size:13pt;"><b>Version: %s</b></span>
        <span style="font-size:10pt;position:relative;left:5px;">( %s )</span>
    </p>
    <p style="font-size:13px;">
        Copyright &copy; %s <a href="mailto:[email protected]">Pete Alexandrou</a>
        <br/>
        Web: <a href="%s">%s</a>
    </p>
    <p style="font-size:11px;">
        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.
    </p>''' % (qApp.applicationName(), qApp.applicationVersion(),
               platform.architecture()[0], datetime.now().year,
               qApp.organizationDomain(), qApp.organizationDomain())
        QMessageBox.about(self, 'About %s' % qApp.applicationName(),
                          about_html)

    @pyqtSlot(int)
    def update_pagecount(self, index: int) -> None:
        self.dl_pagecount = int(self.dlpages_field.itemText(index))
        self.scrapeWorker.maxpages = self.dl_pagecount
        self.progress.setMaximum(self.dl_pagecount * self.dl_pagelinks)
        self.settings.setValue('dl_pagecount', self.dl_pagecount)
        if sys.platform == 'win32':
            self.win_taskbar_button.progress().setMaximum(self.dl_pagecount *
                                                          self.dl_pagelinks)
        if self.scrapeThread.isRunning():
            self.scrapeThread.requestInterruption()
        self.start_scraping()

    @pyqtSlot()
    def show_progress(self):
        self.progress.show()
        self.taskbar.setProgress(0.0, True)
        if sys.platform == 'win32':
            self.win_taskbar_button.setWindow(self.windowHandle())
            self.win_taskbar_button.progress().setRange(
                0, self.dl_pagecount * self.dl_pagelinks)
            self.win_taskbar_button.progress().setVisible(True)
            self.win_taskbar_button.progress().setValue(self.progress.value())

    @pyqtSlot()
    def scrape_finished(self) -> None:
        self.progress.hide()
        self.taskbar.setProgress(0.0, False)
        if sys.platform == 'win32':
            self.win_taskbar_button.progress().setVisible(False)
        self.table.setSortingEnabled(True)
        self.filter_table(text='')

    @pyqtSlot(list)
    def add_row(self, row: list) -> None:
        if self.scrapeThread.isInterruptionRequested():
            self.scrapeThread.terminate()
        else:
            self.cols = 0
            self.table.setRowCount(self.rows + 1)
            if self.table.cursor() != Qt.PointingHandCursor:
                self.table.setCursor(Qt.PointingHandCursor)
            for item in row:
                table_item = QTableWidgetItem(item)
                table_item.setToolTip(
                    '%s\n\nDouble-click to view hoster links.' % row[1])
                table_item.setFont(QFont('Open Sans', weight=QFont.Normal))
                if self.cols == 2:
                    if sys.platform == 'win32':
                        table_item.setFont(
                            QFont('Open Sans Semibold', pointSize=10))
                    elif sys.platform == 'darwin':
                        table_item.setFont(
                            QFont('Open Sans Bold', weight=QFont.Bold))
                    else:
                        table_item.setFont(
                            QFont('Open Sans',
                                  weight=QFont.DemiBold,
                                  pointSize=10))
                    table_item.setText('  ' + table_item.text())
                elif self.cols in (0, 3):
                    table_item.setTextAlignment(Qt.AlignCenter)
                self.table.setItem(self.rows, self.cols, table_item)
                self.update_metabar()
                self.cols += 1
            self.rows += 1

    @pyqtSlot(list)
    def add_hosters(self, links: list) -> None:
        self.hosters_win.show_hosters(links)

    @pyqtSlot(QModelIndex)
    def show_hosters(self, index: QModelIndex) -> None:
        qApp.setOverrideCursor(Qt.BusyCursor)
        self.hosters_win = HosterLinks(self)
        self.hosters_win.downloadLink.connect(self.download_link)
        self.hosters_win.copyLink.connect(self.copy_download_link)
        self.links = HostersThread(
            self.table.item(self.table.currentRow(), 1).text(),
            self.user_agent)
        self.links.setHosters.connect(self.add_hosters)
        self.links.noLinks.connect(self.no_links)
        self.links.start()

    @pyqtSlot()
    def no_links(self) -> None:
        self.hosters_win.loading_progress.cancel()
        self.hosters_win.close()
        QMessageBox.warning(
            self, 'No Links Available',
            'No links are available yet for the chosen TV show. ' +
            'This is most likely due to the files still being uploaded. This is normal if the '
            +
            'link was published 30-45 mins ago.\n\nPlease check back again in 10-15 minutes.'
        )

    @pyqtSlot(bool)
    def filter_faves(self, checked: bool) -> None:
        self.settings.setValue('faves_filter', checked)
        # if hasattr(self, 'scrapeWorker') and (sip.isdeleted(self.scrapeWorker) or self.scrapeWorker.complete):
        if not self.firstrun:
            self.filter_table()

    @pyqtSlot(str)
    @pyqtSlot()
    def filter_table(self, text: str = '') -> None:
        filters = []
        if self.favorites_button.isChecked():
            filters = self.favorites
            self.table.sortItems(2, Qt.AscendingOrder)
        else:
            self.table.sortItems(0, Qt.DescendingOrder)
        if len(text):
            filters.append(text)
        if not len(filters) or not hasattr(self, 'valid_rows'):
            self.valid_rows = []
        for search_term in filters:
            for item in self.table.findItems(search_term, Qt.MatchContains):
                self.valid_rows.append(item.row())
        for row in range(0, self.table.rowCount()):
            if not len(filters):
                self.table.showRow(row)
            else:
                if row not in self.valid_rows:
                    self.table.hideRow(row)
                else:
                    self.table.showRow(row)

    @pyqtSlot()
    def clear_filters(self):
        if not len(self.search_field.text()):
            self.filter_table('')

    @pyqtSlot(bool)
    def aria2_confirmation(self, success: bool) -> None:
        qApp.restoreOverrideCursor()
        if success:
            if sys.platform.startswith('linux'):
                self.notify(
                    title=qApp.applicationName(),
                    msg='Your download link has been unrestricted and now ' +
                    'queued in Aria2 RPC Daemon',
                    icon=self.NotifyIcon.SUCCESS)
            else:
                QMessageBox.information(
                    self, qApp.applicationName(),
                    'Download link has been queued in Aria2.', QMessageBox.Ok)
        else:
            QMessageBox.critical(
                self, 'Aria2 RPC Daemon',
                'Could not connect to Aria2 RPC Daemon. ' +
                'Check your %s settings and try again.' %
                qApp.applicationName(), QMessageBox.Ok)

    @pyqtSlot(str)
    def download_link(self, link: str) -> None:
        if len(self.realdebrid_api_token) > 0 and 'real-debrid.com' not in link \
            and 'rdeb.io' not in link:
            qApp.setOverrideCursor(Qt.BusyCursor)
            self.unrestrict_link(link, True)
        else:
            if self.download_manager == 'aria2':
                self.aria2 = Aria2Thread(settings=self.settings, link_url=link)
                self.aria2.aria2Confirmation.connect(self.aria2_confirmation)
                self.aria2.start()
                self.hosters_win.close()
            elif self.download_manager == 'pyload':
                self.pyload_conn = PyloadConnection(self.pyload_host,
                                                    self.pyload_username,
                                                    self.pyload_password)
                pid = self.pyload_conn.addPackage(name='TVLinker',
                                                  links=[link])
                qApp.restoreOverrideCursor()
                self.hosters_win.close()
                if sys.platform.startswith('linux'):
                    self.notify(title='Download added to %s' %
                                self.download_manager,
                                icon=self.NotifyIcon.SUCCESS)
                else:
                    QMessageBox.information(
                        self, self.download_manager,
                        'Your link has been queued in %s.' %
                        self.download_manager, QMessageBox.Ok)
                # open_pyload = msgbox.addButton('Open pyLoad', QMessageBox.AcceptRole)
                # open_pyload.clicked.connect(self.open_pyload)
            elif self.download_manager in ('kget', 'persepolis'):
                provider = self.kget_cmd if self.download_manager == 'kget' else self.persepolis_cmd
                cmd = '{0} "{1}"'.format(provider, link)
                if self.cmdexec(cmd):
                    qApp.restoreOverrideCursor()
                    self.hosters_win.close()
                    if sys.platform.startswith('linux'):
                        self.notify(title='Download added to %s' %
                                    self.download_manager,
                                    icon=self.NotifyIcon.SUCCESS)
                    else:
                        QMessageBox.information(
                            self, self.download_manager,
                            'Your link has been queued in %s.' %
                            self.download_manager, QMessageBox.Ok)
            elif self.download_manager == 'idm':
                cmd = '"%s" /n /d "%s"' % (self.idm_exe_path, link)
                if self.cmdexec(cmd):
                    qApp.restoreOverrideCursor()
                    self.hosters_win.close()
                    QMessageBox.information(
                        self, 'Internet Download Manager',
                        'Your link has been queued in IDM.')
                else:
                    print('IDM QProcess error = %s' %
                          self.ProcError(self.idm.error()).name)
                    qApp.restoreOverrideCursor()
                    self.hosters_win.close()
                    QMessageBox.critical(
                        self, 'Internet Download Manager',
                        '<p>Could not connect to your local IDM application instance. '
                        +
                        'Please check your settings and ensure the IDM executable path is correct '
                        +
                        'according to your installation.</p><p>Error Code: %s</p>'
                        % self.ProcError(self.idm.error()).name,
                        QMessageBox.Ok)
            else:
                dlpath, _ = QFileDialog.getSaveFileName(
                    self, 'Save File',
                    link.split('/')[-1])
                if dlpath != '':
                    self.directdl_win = DirectDownload(parent=self)
                    self.directdl = DownloadThread(link_url=link,
                                                   dl_path=dlpath)
                    self.directdl.dlComplete.connect(
                        self.directdl_win.download_complete)
                    if sys.platform.startswith('linux'):
                        self.directdl.dlComplete.connect(
                            lambda: self.notify(qApp.applicationName(
                            ), 'Download complete', self.NotifyIcon.SUCCESS))
                    else:
                        self.directdl.dlComplete.connect(
                            lambda: QMessageBox.information(
                                self, qApp.applicationName(),
                                'Download complete', QMessageBox.Ok))
                    self.directdl.dlProgressTxt.connect(
                        self.directdl_win.update_progress_label)
                    self.directdl.dlProgress.connect(
                        self.directdl_win.update_progress)
                    self.directdl_win.cancelDownload.connect(
                        self.cancel_download)
                    self.directdl.start()
                    self.hosters_win.close()

    def _init_notification_icons(self):
        for icon in self.NotifyIcon:
            icon_file = QPixmap(icon.value, 'PNG')
            icon_file.save(
                os.path.join(FixedSettings.config_path,
                             os.path.basename(icon.value)), 'PNG', 100)

    def notify(self,
               title: str,
               msg: str = '',
               icon: Enum = None,
               urgency: int = 1) -> bool:
        icon_path = icon.value if icon is not None else self.NotifyIcon.DEFAULT.value
        icon_path = os.path.join(FixedSettings.config_path,
                                 os.path.basename(icon_path))
        if not os.path.exists(icon_path):
            self._init_notification_icons()
        notification = notify.Notification(title, msg, icon_path)
        notification.set_urgency(urgency)
        return notification.show()

    def cmdexec(self, cmd: str) -> bool:
        self.proc = QProcess()
        self.proc.setProcessChannelMode(QProcess.MergedChannels)
        if hasattr(self.proc, 'errorOccurred'):
            self.proc.errorOccurred.connect(lambda error: print(
                'Process error = %s' % self.ProcError(error).name))
        if self.proc.state() == QProcess.NotRunning:
            self.proc.start(cmd)
            self.proc.waitForFinished(-1)
            rc = self.proc.exitStatus(
            ) == QProcess.NormalExit and self.proc.exitCode() == 0
            self.proc.deleteLater()
            return rc
        return False

    @pyqtSlot()
    def cancel_download(self) -> None:
        self.directdl.cancel_download = True
        self.directdl.quit()
        self.directdl.deleteLater()

    def open_pyload(self) -> None:
        QDesktopServices.openUrl(QUrl(self.pyload_config.host))

    @pyqtSlot(str)
    def copy_download_link(self, link: str) -> None:
        if len(self.realdebrid_api_token) > 0 and 'real-debrid.com' not in link \
            and 'rdeb.io' not in link:
            qApp.setOverrideCursor(Qt.BusyCursor)
            self.unrestrict_link(link, False)
        else:
            clip = qApp.clipboard()
            clip.setText(link)
            self.hosters_win.close()
            qApp.restoreOverrideCursor()

    def unrestrict_link(self, link: str, download: bool = True) -> None:
        caller = inspect.stack()[1].function
        self.realdebrid = RealDebridThread(
            settings=self.settings,
            api_url=FixedSettings.realdebrid_api_url,
            link_url=link,
            action=RealDebridThread.RealDebridAction.UNRESTRICT_LINK)
        self.realdebrid.errorMsg.connect(self.error_handler)
        if download:
            self.realdebrid.unrestrictedLink.connect(self.download_link)
        else:
            self.realdebrid.unrestrictedLink.connect(self.copy_download_link)
        self.realdebrid.start()

    def closeEvent(self, event: QCloseEvent) -> None:
        if hasattr(self, 'scrapeThread'):
            if not sip.isdeleted(
                    self.scrapeThread) and self.scrapeThread.isRunning():
                self.scrapeThread.requestInterruption()
                self.scrapeThread.quit()
        qApp.quit()

    def error_handler(self, props: list) -> None:
        qApp.restoreOverrideCursor()
        QMessageBox.critical(self, props[0], props[1], QMessageBox.Ok)

    @staticmethod
    def get_path(path: str = None, override: bool = False) -> str:
        if override:
            if getattr(sys, 'frozen', False):
                return os.path.join(sys._MEIPASS, path)
            return os.path.join(QFileInfo(__file__).absolutePath(), path)
        return ':assets/%s' % path

    @staticmethod
    def get_version(filename: str = '__init__.py') -> str:
        with open(TVLinker.get_path(filename, override=True), 'r') as initfile:
            for line in initfile.readlines():
                m = re.match('__version__ *= *[\'](.*)[\']', line)
                if m:
                    return m.group(1)
Exemplo n.º 5
0
class ConsoleWidget(QPlainTextEdit):

    def __init__(self):
        super(ConsoleWidget, self).__init__('>>> ')
        self.setUndoRedoEnabled(False)
        self.apply_editor_style()
        self.setToolTip(self.tr("Show/Hide (F4)"))
        self.moveCursor(QTextCursor.EndOfLine)

        self._patIsWord = re.compile('\w+')
        self.prompt = '>>> '
        self._console = console.Console()
        self._history = []
        self.history_index = 0
        self._current_command = ''
        self._braces = None
        self.imports = ['import __builtin__']
        self.patFrom = re.compile('^(\\s)*from ((\\w)+(\\.)*(\\w)*)+ import')
        self.patImport = re.compile('^(\\s)*import (\\w)+')
        self.patObject = re.compile('[^a-zA-Z0-9_\\.]')
        #self.completer = completer_widget.CompleterWidget(self)
        self.okPrefix = QRegExp('[.)}:,\]]')

        self._pre_key_press = {
            Qt.Key_Enter: self._enter_pressed,
            Qt.Key_Return: self._enter_pressed,
            Qt.Key_Tab: self._tab_pressed,
            Qt.Key_Home: self._home_pressed,
            Qt.Key_PageUp: lambda x: True,
            Qt.Key_PageDown: lambda x: True,
            Qt.Key_Left: self._left_pressed,
            Qt.Key_Up: self._up_pressed,
            Qt.Key_Down: self._down_pressed,
            Qt.Key_Backspace: self._backspace,
        }

        #Create Context Menu
        self._create_context_menu()

        #Set Font
        self.set_font(settings.FONT)
        #Create Highlighter
        parts_scanner, code_scanner, formats = \
            syntax_highlighter.load_syntax(python_syntax.syntax)
        self.highlighter = syntax_highlighter.SyntaxHighlighter(
            self.document(),
            parts_scanner, code_scanner, formats)

        self.cursorPositionChanged.connect(self.highlight_current_line)
        self.highlight_current_line()

        self._proc = QProcess(self)
        self._proc.readyReadStandardOutput.connect(self._python_path_detected)
        self._proc.error['QProcess::ProcessError'].connect(self.process_error)
        self._add_system_path_for_frozen()

        ninjaide = IDE.getInstance()
        ninjaide.ns_preferences_editor_font.connect(self.set_font)

    def _add_system_path_for_frozen(self):
        try:
            self._proc.start(settings.PYTHON_PATH, [resources.GET_SYSTEM_PATH])
        except Exception as reason:
            logger.warning('Could not get system path, error: %r' % reason)

    def _python_path_detected(self):
        paths = self._proc.readAllStandardOutput().data()#.decode('utf8')
        add_system_path = ('import sys; '
                           'sys.path = list(set(sys.path + %s))' % paths)
        self._write(add_system_path)
        self._proc.deleteLater()

    def process_error(self, error):
        message = ''
        if error == 0:
            message = 'Failed to start'
        else:
            message = 'Error during execution, QProcess error: %d' % error
        logger.warning('Could not get system path, error: %r' % message)

    def set_font(self, font):
        self.document().setDefaultFont(font)
        # Fix for older version of Qt which doens't has ForceIntegerMetrics
        if "ForceIntegerMetrics" in dir(QFont):
            self.document().defaultFont().setStyleStrategy(
                QFont.ForceIntegerMetrics)

    def _create_context_menu(self):
        self.popup_menu = self.createStandardContextMenu()

        self.popup_menu.clear()

        actionCut = self.popup_menu.addAction(self.tr("Cut"))
        actionCopy = self.popup_menu.addAction(self.tr("Copy"))
        actionPaste = self.popup_menu.addAction(self.tr("Paste"))
        actionClean = self.popup_menu.addAction(self.tr("Clean Console"))
        actionCopyHistory = self.popup_menu.addAction(self.tr("Copy History"))
        actionCopyConsoleContent = self.popup_menu.addAction(
            self.tr("Copy Console Content"))

        self.popup_menu.addAction(actionCut)
        self.popup_menu.addAction(actionCopy)
        self.popup_menu.addAction(actionPaste)
        self.popup_menu.addSeparator()
        self.popup_menu.addAction(actionClean)
        self.popup_menu.addSeparator()
        self.popup_menu.addAction(actionCopyHistory)
        self.popup_menu.addAction(actionCopyConsoleContent)

        actionCut.triggered['bool'].connect(lambda s: self._cut())
        actionCopy.triggered['bool'].connect(lambda s: self.copy())
        actionPaste.triggered['bool'].connect(lambda s: self._paste())
        actionClean.triggered['bool'].connect(lambda s: self._clean_console())
        actionCopyHistory.triggered['bool'].connect(lambda s: self._copy_history())
        actionCopyConsoleContent.triggered['bool'].connect(lambda s: self._copy_console_content())

    def _cut(self):
        event = QKeyEvent(QEvent.KeyPress, Qt.Key_X, Qt.ControlModifier, "x")
        self.keyPressEvent(event)

    def _paste(self):
        if self.textCursor().hasSelection():
            self.moveCursor(QTextCursor.End)
        self.paste()

    def _clean_console(self):
        self.clear()
        self._add_prompt()

    def _copy_history(self):
        historyContent = '\n'.join(self._history)
        clipboard = QApplication.instance().clipboard()
        clipboard.setText(historyContent)

    def _copy_console_content(self):
        content = self.toPlainText()
        clipboard = QApplication.instance().clipboard()
        clipboard.setText(content)

    def setCursorPosition(self, position, mode=QTextCursor.MoveAnchor):
        self.moveCursor(QTextCursor.StartOfLine, mode)
        for i in range(len(self.prompt) + position):
            self.moveCursor(QTextCursor.Right, mode)

    def _check_event_on_selection(self, event):
        if event.text():
            cursor = self.textCursor()
            begin_last_block = (self.document().lastBlock().position() +
                len(self.prompt))
            if cursor.hasSelection() and \
               ((cursor.selectionEnd() < begin_last_block) or
               (cursor.selectionStart() < begin_last_block)):
                self.moveCursor(QTextCursor.End)

    def _enter_pressed(self, event):
        self._write_command()
        return True

    def _tab_pressed(self, event):
        self.textCursor().insertText(' ' * settings.INDENT)
        return True

    def _home_pressed(self, event):
        if event.modifiers() == Qt.ShiftModifier:
            self.setCursorPosition(0, QTextCursor.KeepAnchor)
        else:
            self.setCursorPosition(0)
        return True

    def _left_pressed(self, event):
        return self._get_cursor_position() == 0

    def _up_pressed(self, event):
        if self.history_index == len(self._history):
            command = self.document().lastBlock().text()[len(self.prompt):]
            self._current_command = command
        self._set_command(self._get_prev_history_entry())
        return True

    def _down_pressed(self, event):
        if len(self._history) == self.history_index:
            command = self._current_command
        else:
            command = self._get_next_history_entry()
        self._set_command(command)
        return True

    def _backspace(self, event):
        cursor = self.textCursor()
        selected_text = cursor.selectedText()
        cursor.movePosition(QTextCursor.StartOfLine, QTextCursor.KeepAnchor)
        text = cursor.selectedText()[len(self.prompt):]
        if (len(text) % settings.INDENT == 0) and text.isspace():
            cursor.movePosition(QTextCursor.StartOfLine)
            cursor.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor,
                settings.INDENT)
            cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor,
                settings.INDENT)
            cursor.removeSelectedText()
            return True
        elif (selected_text ==
             self.document().lastBlock().text()[len(self.prompt):]):
            self.textCursor().removeSelectedText()
            return True

        position = self.textCursor().positionInBlock() - len(self.prompt)
        text = self.document().lastBlock().text()[len(self.prompt):]
        if position < len(text):
            if (text[position - 1] in BRACES and
            text[position] in BRACES.values()):
                self.textCursor().deleteChar()

        return self._get_cursor_position() == 0

    def keyPressEvent(self, event):
        #if self.completer.popup().isVisible():
            #if event.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab):
                #event.ignore()
                #self.completer.popup().hide()
                #return
            #elif event.key in (Qt.Key_Space, Qt.Key_Escape, Qt.Key_Backtab):
                #self.completer.popup().hide()

        self._check_event_on_selection(event)
        if self._pre_key_press.get(event.key(), lambda x: False)(event):
            return

        if event.text() in (set(BRACES.values()) - set(["'", '"'])):
            cursor = self.textCursor()
            cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor)
            brace = cursor.selection().toPlainText()
            cursor = self.textCursor()
            cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor)
            braceClose = cursor.selection().toPlainText()
            if BRACES.get(brace, False) == event.text() and \
              braceClose == event.text():
                self.moveCursor(QTextCursor.Right)
                return

        QPlainTextEdit.keyPressEvent(self, event)

        if event.text() in BRACES:
            cursor = self.textCursor()
            cursor.movePosition(QTextCursor.StartOfLine,
                QTextCursor.KeepAnchor)
            self.textCursor().insertText(
                BRACES[event.text()])
            self.moveCursor(QTextCursor.Left)

        #completionPrefix = self._text_under_cursor()
        #if event.key() == Qt.Key_Period or (event.key() == Qt.Key_Space and
           #event.modifiers() == Qt.ControlModifier):
            #self.completer.setCompletionPrefix(completionPrefix)
            #self._resolve_completion_argument()
        #if self.completer.popup().isVisible() and \
           #completionPrefix != self.completer.completionPrefix():
            #self.completer.setCompletionPrefix(completionPrefix)
            #self.completer.popup().setCurrentIndex(
                #self.completer.completionModel().index(0, 0))
            #self.completer.setCurrentRow(0)
            #self._resolve_completion_argument()

    #def _resolve_completion_argument(self):
        #try:
            #cursor = self.textCursor()
            #cursor.movePosition(QTextCursor.StartOfLine,
                #QTextCursor.KeepAnchor)
            #var = cursor.selectedText()
            #chars = self.patObject.findall(var)
            #var = var[var.rfind(chars[-1]) + 1:]
            #cr = self.cursorRect()
            #proposals = completer.get_all_completions(var,
                #imports=self.imports)
            #if not proposals:
                #if self.completer.popup().isVisible():
                    #prefix = var[var.rfind('.') + 1:]
                    #var = var[:var.rfind('.') + 1]
                    #var = self._console.get_type(var)
                    #var += prefix
                #else:
                    #var = self._console.get_type(var)
                #proposals = completer.get_all_completions(var,
                    #imports=self.imports)
            #self.completer.complete(cr, proposals)
        #except:
            #self.completer.popup().hide()

    def highlight_current_line(self):
        self.extraSelections = []
        selection = QTextEdit.ExtraSelection()
        lineColor = QColor(resources.CUSTOM_SCHEME.get('CurrentLine',
                    resources.COLOR_SCHEME['CurrentLine']))
        lineColor.setAlpha(20)
        selection.format.setBackground(lineColor)
        selection.format.setProperty(QTextFormat.FullWidthSelection, True)
        selection.cursor = self.textCursor()
        selection.cursor.clearSelection()
        self.extraSelections.append(selection)
        self.setExtraSelections(self.extraSelections)

        if self._braces is not None:
            self._braces = None
        cursor = self.textCursor()
        if cursor.position() == 0:
            self.setExtraSelections(self.extraSelections)
            return
        cursor.movePosition(QTextCursor.PreviousCharacter,
                             QTextCursor.KeepAnchor)
        text = cursor.selectedText()
        pos1 = cursor.position()
        if text in (')', ']', '}'):
            pos2 = self._match_braces(pos1, text, forward=False)
        elif text in ('(', '[', '{'):
            pos2 = self._match_braces(pos1, text, forward=True)
        else:
            self.setExtraSelections(self.extraSelections)
            return
        #if pos2 is not None:
            #self._braces = (pos1, pos2)
            #selection = QTextEdit.ExtraSelection()
            #selection.format.setForeground(QColor(
                #resources.CUSTOM_SCHEME.get('brace-foreground',
                #resources.COLOR_SCHEME.get('brace-foreground'))))
            #selection.format.setBackground(QColor(
                #resources.CUSTOM_SCHEME.get('brace-background',
                #resources.COLOR_SCHEME.get('brace-background'))))
            #selection.cursor = cursor
            #self.extraSelections.append(selection)
            #selection = QTextEdit.ExtraSelection()
            #selection.format.setForeground(QColor(
                #resources.CUSTOM_SCHEME.get('brace-foreground',
                #resources.COLOR_SCHEME.get('brace-foreground'))))
            #selection.format.setBackground(QColor(
                #resources.CUSTOM_SCHEME.get('brace-background',
                #resources.COLOR_SCHEME.get('brace-background'))))
            #selection.cursor = self.textCursor()
            #selection.cursor.setPosition(pos2)
            #selection.cursor.movePosition(QTextCursor.NextCharacter,
                             #QTextCursor.KeepAnchor)
            #self.extraSelections.append(selection)
        #else:
            #self._braces = (pos1,)
            #selection = QTextEdit.ExtraSelection()
            #selection.format.setBackground(QColor(
                #resources.CUSTOM_SCHEME.get('brace-background',
                #resources.COLOR_SCHEME.get('brace-background'))))
            #selection.format.setForeground(QColor(
                #resources.CUSTOM_SCHEME.get('brace-foreground',
                #resources.COLOR_SCHEME.get('brace-foreground'))))
            #selection.cursor = cursor
            #self.extraSelections.append(selection)
        self.setExtraSelections(self.extraSelections)

    def _text_under_cursor(self):
        tc = self.textCursor()
        tc.select(QTextCursor.WordUnderCursor)
        return tc.selectedText()

    def get_selection(self, posStart, posEnd):
        cursor = self.textCursor()
        cursor.setPosition(posStart)
        if posEnd == QTextCursor.End:
            cursor2 = self.textCursor()
            cursor2.movePosition(posEnd)
            cursor.setPosition(cursor2.position(), QTextCursor.KeepAnchor)
        else:
            cursor.setPosition(posEnd, QTextCursor.KeepAnchor)
        return cursor.selectedText()

    def _match_braces(self, position, brace, forward):
        """based on: http://gitorious.org/khteditor"""
        if forward:
            braceMatch = {'(': ')', '[': ']', '{': '}'}
            text = self.get_selection(position, QTextCursor.End)
            braceOpen, braceClose = 1, 1
        else:
            braceMatch = {')': '(', ']': '[', '}': '{'}
            text = self.get_selection(QTextCursor.Start, position)
            braceOpen, braceClose = len(text) - 1, len(text) - 1
        while True:
            if forward:
                posClose = text.find(braceMatch[brace], braceClose)
            else:
                posClose = text.rfind(braceMatch[brace], 0, braceClose + 1)
            if posClose > -1:
                if forward:
                    braceClose = posClose + 1
                    posOpen = text.find(brace, braceOpen, posClose)
                else:
                    braceClose = posClose - 1
                    posOpen = text.rfind(brace, posClose, braceOpen + 1)
                if posOpen > -1:
                    if forward:
                        braceOpen = posOpen + 1
                    else:
                        braceOpen = posOpen - 1
                else:
                    if forward:
                        return position + posClose
                    else:
                        return position - (len(text) - posClose)
            else:
                return

    def _add_prompt(self, incomplete=False):
        if incomplete:
            prompt = '.' * 3 + ' '
        else:
            prompt = self.prompt
        self.appendPlainText(prompt)
        self.moveCursor(QTextCursor.End)

    def _get_cursor_position(self):
        return self.textCursor().columnNumber() - len(self.prompt)

    def _write_command(self):
        command = self.document().lastBlock().text()
        #remove the prompt from the QString
        command = command[len(self.prompt):]
        self._add_history(command)
        conditional = command.strip() != 'quit()'
        incomplete = self._write(command) if conditional else None
        if self.patFrom.match(command) or self.patImport.match(command):
            self.imports += [command]
        if not incomplete:
            output = self._read()
            if output is not None:
                if isinstance(output, str):
                    pass
                    ##output = output.encode('utf8')
                self.appendPlainText(output)#.decode('utf8'))
        self._add_prompt(incomplete)

    def _set_command(self, command):
        self.moveCursor(QTextCursor.End)
        cursor = self.textCursor()
        cursor.movePosition(QTextCursor.StartOfLine, QTextCursor.KeepAnchor)
        cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor,
            len(self.prompt))
        cursor.insertText(command)

    def contextMenuEvent(self, event):
        self.popup_menu.exec_(event.globalPos())

    def _write(self, line):
        return self._console.push(line)

    def _read(self):
        return self._console.output

    def _add_history(self, command):
        if command and (not self._history or self._history[-1] != command):
            self._history.append(command)
        self.history_index = len(self._history)

    def _get_prev_history_entry(self):
        if self._history:
            self.history_index = max(0, self.history_index - 1)
            return self._history[self.history_index]
        return ''

    def _get_next_history_entry(self):
        if self._history:
            hist_len = len(self._history) - 1
            self.history_index = min(hist_len, self.history_index + 1)
            index = self.history_index
            if self.history_index == hist_len:
                self.history_index += 1
            return self._history[index]
        return ''

    def restyle(self):
        self.apply_editor_style()
        parts_scanner, code_scanner, formats = \
            syntax_highlighter.load_syntax(python_syntax.syntax)
        self.highlighter = syntax_highlighter.SyntaxHighlighter(
            self.document(),
            parts_scanner, code_scanner, formats)

    def apply_editor_style(self):
        css = 'QPlainTextEdit {color: %s; background-color: %s;' \
            'selection-color: %s; selection-background-color: %s;}' \
            % (resources.CUSTOM_SCHEME.get('editor-text',
            resources.COLOR_SCHEME['Default']),
            resources.CUSTOM_SCHEME.get('EditorBackground',
                resources.COLOR_SCHEME['EditorBackground']),
            resources.CUSTOM_SCHEME.get('EditorSelectionColor',
                resources.COLOR_SCHEME['EditorSelectionColor']),
            resources.CUSTOM_SCHEME.get('EditorSelectionBackground',
                resources.COLOR_SCHEME['EditorSelectionBackground']))
        self.setStyleSheet(css)

    def load_project_into_console(self, projectFolder):
        """Load the projectFolder received into the sys.path."""
        self._console.push("import sys; sys.path += ['%s']" % projectFolder)

    def unload_project_from_console(self, projectFolder):
        """Unload the project from the system path."""
        self._console.push("import sys; "
            "sys.path = [path for path in sys.path "
            "if path != '%s']" % projectFolder)

    def zoom_in(self):
        font = self.document().defaultFont()
        size = font.pointSize()
        if size < settings.FONT_MAX_SIZE:
            size += 2
            font.setPointSize(size)
        self.setFont(font)

    def zoom_out(self):
        font = self.document().defaultFont()
        size = font.pointSize()
        if size > settings.FONT_MIN_SIZE:
            size -= 2
            font.setPointSize(size)
        self.setFont(font)

    def wheelEvent(self, event):
        if event.modifiers() == Qt.ControlModifier:
            if event.delta() > 0:
                self.zoom_in()
            elif event.delta() < 0:
                self.zoom_out()
            event.ignore()
        super(ConsoleWidget, self).wheelEvent(event)