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
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)
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 © %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)
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)