def tribler_api(api_port, tmpdir_factory): # Run real Core and record responses core_env = QProcessEnvironment.systemEnvironment() core_env.insert("CORE_BASE_PATH", str(RUN_TRIBLER_PY.parent / "tribler-core")) core_env.insert("CORE_PROCESS", "1") core_env.insert("CORE_API_PORT", f"{api_port}") core_env.insert("CORE_API_KEY", "") core_env.insert("TRIBLER_CORE_TEST_MODE", "1") temp_state_dir = tmpdir_factory.mktemp('tribler_state_dir') core_env.insert("TSTATEDIR", str(temp_state_dir)) core_process = QProcess() def on_core_read_ready(): raw_output = bytes(core_process.readAll()) decoded_output = raw_output.decode(errors="replace") print(decoded_output.strip()) # noqa: T001 core_process.setProcessEnvironment(core_env) core_process.setReadChannel(QProcess.StandardOutput) core_process.setProcessChannelMode(QProcess.MergedChannels) connect(core_process.readyRead, on_core_read_ready) core_process.start("python3", [str(RUN_TRIBLER_PY.absolute())]) yield core_process core_process.kill() core_process.waitForFinished()
class AbstractTsharkDataSource(DataSource): def startFetch(self): self.plist = ByteBufferList() self.process = QProcess() self.process.finished.connect(self.onProcessFinished) self.process.readyReadStandardError.connect(self.onReadyReadStderr) self.process.readyReadStandardOutput.connect(self.onReadyReadStdout) self.process.start(findTshark(), self.getArgs()) self.target = PdmlToPacketListParser(self.plist) return self.plist def onReadyReadStderr(self): s = "STD-ERR FROM Tshark:" + self.process.readAllStandardError().data( ).decode("utf-8", "replace") print(s) self.on_log.emit(s) def onReadyReadStdout(self): self.plist.beginUpdate() self.target.feed(self.process.readAllStandardOutput()) self.plist.endUpdate() def onProcessFinished(self, exitCode, exitStatus): self.on_finished.emit() def cancelFetch(self): self.process.terminate() self.process.waitForFinished(500) self.process.kill() pass
class Converter: """Converter class to provide conversion functionality.""" def __init__(self, media_list, conversion_lib=CONV_LIB.ffmpeg): """Class initializer.""" self.conversion_lib = conversion_lib self.media_list = media_list self.process = QProcess() def start_encoding(self, cmd): """Start the encoding process.""" self.process.start(which(self.conversion_lib), cmd) def stop_encoding(self): """Terminate encoding process.""" self.process.terminate() if self.is_running: self.process.kill() @property def is_running(self): """Return the individual file encoding process state.""" return self.process.state() == QProcess.Running @property def encoding_done(self): """Return True if media list is done.""" return self.media_list.running_index + 1 >= self.media_list.length
class MainView(QMainWindow): def __init__(self, flags=None, *args, **kwargs): super().__init__(flags, *args, **kwargs) loadUi(os.path.join(os.path.dirname(__file__), "server.ui"), self) self.output_path = os.path.join(os.path.dirname(__file__), "log") self._buffer = "" self._server_process: QProcess = None self.startStopButton: QPushButton = self.startStopButton self.logTextBrowser: QTextBrowser = self.logTextBrowser self.formWidget: QWidget = self.formWidget self.portLabel: QLabel = self.portLabel self.portLineEdit: QLineEdit = self.portLineEdit self.addressLabel: QLabel = self.addressLabel self.addressLineEdit: QLineEdit = self.addressLineEdit self.outputDirectoryButton: QPushButton = self.outputDirectoryButton self.outputDirectoryButton.setText(self.output_path) self.logTextBrowser.verticalScrollBar().rangeChanged.connect( lambda x: self.logTextBrowser.verticalScrollBar().setValue( self.logTextBrowser.verticalScrollBar().maximum())) self.outputDirectoryButton.clicked.connect(self.set_output_path) self.startStopButton.clicked.connect(self.start_server) self.show() def start_server(self): if not self._server_process: self._server_process = QProcess(self) self._server_process.readyRead.connect(self.data_ready) addr = self.addressLineEdit.text() port = self.portLineEdit.text() self._server_process.start(sys.executable, [ "-m", "chat.server.chat_server", "--addr", addr, "--port", port, "--output", self.output_path ]) self.startStopButton.setText("Stop server") else: self._server_process.kill() self._server_process = None self.startStopButton.setText("Start server") def set_output_path(self): diag = QFileDialog() diag.setFileMode(QFileDialog.Directory) self.output_path = diag.getExistingDirectory( self, "Folder do zapisu wyników", self.output_path) self.outputDirectoryButton.setText(self.output_path) def data_ready(self): prev = self.logTextBrowser.toPlainText() try: new = bytes(self._server_process.readAll()).decode("UTF-8") self.logTextBrowser.setText(prev + new) except UnicodeDecodeError: pass def closeEvent(self, QCloseEvent): if self._server_process: self._server_process.kill() self._server_process = None
class PROCESS(QObject): queue_finished = pyqtSignal(int) prog_finished = pyqtSignal(tuple) stoped = pyqtSignal(int, int) # 1: start, 0: finished, 2: error | queue state = pyqtSignal(int, str) def __init__(self, parent=None): QObject.__init__(self, parent) self.process = QProcess() self.process.setProcessChannelMode(QProcess.MergedChannels) self.process.finished.connect(self.emit_finished) self.process.stateChanged.connect(self.emit_state) self.process_type = 0 # 0 process, 1 runnable self.threadpool = QThreadPool() self.worker = Worker() self.worker.signals.state.connect(self.emit_state) self.queue = None def emit_state(self, state): self.state.emit(state, self.prog_name) def emit_finished(self, exitcode, exitstatus): self.prog_finished.emit((self.prog_name, exitcode, exitstatus)) def set_queue(self, queue): self.queue = queue def start_process(self): try: obj = self.queue.get(False) self.prog_name = list(obj.keys())[0] if callable(list(obj.values())[0][0]): self.process_type = 1 funct = list(obj.values())[0][0] args = list(obj.values())[0][1] self.worker.insert_function(funct, args, self.prog_name) self.threadpool.start(self.worker) else: self.process_type = 0 self.process.start( list(obj.values())[0][0], list(obj.values())[0][1]) except Empty: self.queue_finished.emit(self.queue.name) def force_finished(self): # for process (programs) self.stoped.emit(1, self.queue.name) if self.process_type == 0: self.process.terminate() if not self.process.waitForFinished(1000): self.process.kill() else: if self.threadpool.activeThreadCount(): self.threadpool.clear() self.threadpool.waitForDone() with self.queue.mutex: self.queue.queue.clear() self.stoped.emit(0, self.queue.name)
class Tunnel(QWidget): def __init__(self, name, data): super(Tunnel, self).__init__() uic.loadUi("tunnel.ui", self) self.tunnelconfig = TunnelConfig(self, data) self.tunnelconfig.setWindowTitle(name) self.tunnelconfig.setModal(True) self.name.setText(name) self.tunnelconfig.icon = F"./icons/{name}.png" if not os.path.exists(self.tunnelconfig.icon): self.tunnelconfig.icon = "./icons/robi.png" self.icon.setPixmap(QPixmap(self.tunnelconfig.icon)) self.action_tunnel.clicked.connect(self.do_tunnel) self.action_settings.clicked.connect(self.tunnelconfig.show) self.action_open.clicked.connect(self.do_open_browser) self.action_tunnel.setStyleSheet(LANG.QSS_START) self.process = None def do_open_browser(self): browser_open = self.tunnelconfig.browser_open.text() if browser_open: urlobj = urlparse(browser_open) local_port = self.tunnelconfig.local_port.value() new_url = urlobj._replace( netloc=F"{urlobj.hostname}:{local_port}").geturl() QDesktopServices.openUrl(QUrl(new_url)) def do_tunnel(self): if self.process: self.stop_tunnel() else: self.start_tunnel() def start_tunnel(self): params = self.tunnelconfig.ssh_command.text().split(" ") self.process = QProcess() self.process.start(params[0], params[1:]) self.action_tunnel.setStyleSheet(LANG.QSS_STOP) self.action_tunnel.setIcon(QIcon.fromTheme(LANG.ICON_STOP)) self.do_open_browser() def stop_tunnel(self): try: self.process.kill() self.process = None except: pass self.action_tunnel.setIcon(QIcon.fromTheme(LANG.ICON_START)) self.action_tunnel.setStyleSheet(LANG.QSS_START)
class ProcessThread(QThread): _ProcssOutput = pyqtSignal(object) def __init__(self, cmd, directory_exec=None): QThread.__init__(self) self.directory_exec = directory_exec self.cmd = cmd @pyqtSlot() def getNameThread(self): return "[New Thread {} ({})]".format(self.procThread.pid(), self.objectName()) def readProcessOutput(self): try: self.data = str(self.procThread.readAllStandardOutput(), encoding="ascii") self._ProcssOutput.emit(self.data) except Exception: pass def getpid(self): """ return the pid of current process in background""" return self.procThread.pid() def getID(self): """ return the name of process in background""" return self.objectName() def start(self): self.procThread = QProcess(self) self.procThread.setProcessChannelMode(QProcess.MergedChannels) if self.directory_exec: self.procThread.setWorkingDirectory(self.directory_exec) self.procThread.start( list(self.cmd.keys())[0], self.cmd[list(self.cmd.keys())[0]] ) self.procThread.readyReadStandardOutput.connect(self.readProcessOutput) print( display_messages( "starting {} pid: [{}]".format( self.objectName(), self.procThread.pid() ), sucess=True, ) ) def stop(self): print( display_messages( "thread {} successfully stopped".format(self.objectName()), info=True ) ) if hasattr(self, "procThread"): self.procThread.terminate() self.procThread.waitForFinished() self.procThread.kill()
class OutputPane(QTextEdit): def __init__(self, parent=None): super().__init__(parent) self.process = None self.setAcceptRichText(False) self.setReadOnly(True) self.setLineWrapMode(QTextEdit.NoWrap) self.setObjectName('outputpane') def append(self, txt): tc = self.textCursor() tc.movePosition(QTextCursor.End) self.setTextCursor(tc) self.insertPlainText(txt) self.ensureCursorVisible() def clear(self): self.setText('') def get_subprocess_env(self): """Get the environment variables for running the subprocess.""" return {'PYTHONIOENCODING': ENCODING} def run(self, *args, cwd=None): env = QProcessEnvironment().systemEnvironment() for k, v in self.get_subprocess_env().items(): env.insert(k, v) self.process = QProcess(self) self.process.setProcessEnvironment(env) if cwd: self.process.setWorkingDirectory(cwd) # self.process.stateChanged.connect(self.stateChanged) self.process.readyReadStandardOutput.connect(self.on_stdout_read) self.process.readyReadStandardError.connect(self.on_stderr_read) self.process.finished.connect(self.on_process_end) self.clear() self.process.start(args[0], args[1:], QIODevice.ReadWrite) def on_stdout_read(self): while self.process and self.process.canReadLine(): text = self.process.readLine().data().decode(ENCODING) self.append(text) def on_stderr_read(self): text = self.process.readAllStandardError().data().decode(ENCODING) self.append(text) def kill(self): if self.process: self.process.kill() def on_process_end(self): self.process = None
class ThreadDNSspoofNF(QObject): DnsReq = pyqtSignal(object) def __init__(self, domains, interface, redirect, APmode=True, parent=None): super(ThreadDNSspoofNF, self).__init__(parent) self.domains = domains self.interface = interface self.redirect = redirect self.APmode = APmode self.desc = "DNS spoof Module::NetFilter" @pyqtSlot() def readProcessOutput(self): self.data = str(self.procThreadDNS.readAllStandardOutput()) self.DnsReq.emit(self.data) def start(self): self.setIptables(self.APmode, option="A") self.procThreadDNS = QProcess(self) self.procThreadDNS.setProcessChannelMode(QProcess.MergedChannels) QObject.connect( self.procThreadDNS, pyqtSignal("readyReadStandardOutput()"), self, pyqtSlot("readProcessOutput()"), ) self.procThreadDNS.start( "python", [ "core/packets/dnsspoofNF.py", "-r", self.redirect, "-d", ",".join(self.domains), ], ) def setIptables(self, APMode=True, option=str()): if APMode: system( "iptables -{} INPUT -i {} -p udp --dport 53 -s {} -j ACCEPT". format(option, self.interface, self.redirect)) system("iptables -{} INPUT -i {} -p udp --dport 53 -j DROP".format( option, self.interface)) system( "iptables -t nat -{} PREROUTING -p udp --dport 53 -j NFQUEUE". format(option)) def stop(self): self.setIptables(self.APmode, option="D") if hasattr(self, "procThreadDNS"): self.procThreadDNS.terminate() self.procThreadDNS.waitForFinished() self.procThreadDNS.kill()
class _Converter: """_Converter class to provide conversion functionality.""" def __init__(self, library_path): """Class initializer.""" self._library_path = library_path self._process = QProcess() def setup_converter(self, reader, finisher, process_channel): """Set up the QProcess object.""" self._process.setProcessChannelMode(process_channel) self._process.readyRead.connect(reader) self._process.finished.connect(finisher) def start_converter(self, cmd): """Start the encoding process.""" self._process.start(self._library_path, cmd) def stop_converter(self): """Terminate the encoding process.""" self._process.terminate() if self.converter_is_running: self._process.kill() def converter_finished_disconnect(self, connected): """Disconnect the QProcess.finished method.""" self._process.finished.disconnect(connected) def close_converter(self): """Call QProcess.close method.""" self._process.close() def kill_converter(self): """Call QProcess.kill method.""" self._process.kill() def converter_state(self): """Call QProcess.state method.""" return self._process.state() def converter_exit_status(self): """Call QProcess.exit_status method.""" return self._process.exitStatus() def read_converter_output(self): """Call QProcess.readAll method.""" return str(self._process.readAll()) @property def converter_is_running(self): """Return QProcess state.""" return self._process.state() == QProcess.Running
def test_qprocess(self, py_proc): """Test PyQIODevice with a QProcess which is non-sequential. This also verifies seek() and tell() behave as expected. """ proc = QProcess() proc.start(*py_proc('print("Hello World")')) dev = qtutils.PyQIODevice(proc) assert not dev.closed with pytest.raises(OSError, match='Random access not allowed!'): dev.seek(0) with pytest.raises(OSError, match='Random access not allowed!'): dev.tell() proc.waitForFinished(1000) proc.kill() assert bytes(dev.read()).rstrip() == b'Hello World'
def test_qprocess(self): """Test PyQIODevice with a QProcess which is non-sequential. This also verifies seek() and tell() behave as expected. """ proc = QProcess() proc.start(sys.executable, ['-c', 'print("Hello World")']) dev = qtutils.PyQIODevice(proc) assert not dev.closed with pytest.raises(OSError) as excinfo: dev.seek(0) assert str(excinfo.value) == 'Random access not allowed!' with pytest.raises(OSError) as excinfo: dev.tell() assert str(excinfo.value) == 'Random access not allowed!' proc.waitForFinished(1000) proc.kill() assert bytes(dev.read()).rstrip() == b'Hello World'
class Worker(QObject): sendOutput = pyqtSignal(str) def __init__(self): super(Worker, self).__init__() self.process = QProcess() self.setupProcess() def __del__(self): self.process.terminate() if not self.process.waitForFinished(10000): self.process.kill() def setupProcess(self): self.process.setProcessChannelMode(QProcess.MergedChannels) self.process.readyReadStandardOutput.connect(self.readStdOutput) self._envgcc = QProcessEnvironment.systemEnvironment() self._envgcc.insert("QT5DIR", "/home/epson/Qt/5.8/gcc_64") self._envgcc.insert("QT_QPA_PLATFORM_PLUGIN_PATH", "/home/epson/Qt/5.8/gcc_64/plugins/platforms") self._envgcc.insert("QT_PLUGIN_PATH", "/home/epson/Qt/5.8/gcc_64/plugins") self._envgcc.insert("QML_IMPORT_PATH", "/home/epson/Qt/5.8/gcc_64/qml") self._envgcc.insert("QML2_IMPORT_PATH", "/home/epson/Qt/5.8/gcc_64/qml") self._envgcc.insert( "QT_VIRTUALKEYBOARD_LAYOUT_PATH", "/home/epson/Qt/5.8/Src/qtvirtualkeyboard/src/virtualkeyboard/content" ) self._envgcc.insert("QT_VIRTUALKEYBOARD_STYLE", "custom") self._envgcc.insert("QT_IM_MODULE", "qtvirtualkeyboard") self.process.setProcessEnvironment(self._envgcc) self.process.start( "/home/epson/INTERACT/interact-ii/examples/ProcessExample.py") @pyqtSlot() def readStdOutput(self): output = str(self.process.readAllStandardOutput()) # Do some extra processing of the output here if required # ... self.sendOutput.emit(output)
def runProcess(self, args): """ Public method to execute the conda with the given arguments. The conda executable is called with the given arguments and waited for its end. @param args list of command line arguments @type list of str @return tuple containing a flag indicating success and the output of the process @rtype tuple of (bool, str) """ exe = Preferences.getConda("CondaExecutable") if not exe: exe = "conda" process = QProcess() process.start(exe, args) procStarted = process.waitForStarted(15000) if procStarted: finished = process.waitForFinished(30000) if finished: if process.exitCode() == 0: output = str(process.readAllStandardOutput(), Preferences.getSystem("IOEncoding"), 'replace').strip() return True, output else: return ( False, self.tr("conda exited with an error ({0}).").format( process.exitCode())) else: process.terminate() process.waitForFinished(2000) process.kill() process.waitForFinished(3000) return False, self.tr("conda did not finish within" " 30 seconds.") return False, self.tr("conda could not be started.")
def runProcess(self, args, interpreter): """ Public method to execute the current pip with the given arguments. The selected pip executable is called with the given arguments and waited for its end. @param args list of command line arguments @type list of str @param interpreter path of the Python interpreter to be used @type str @return tuple containing a flag indicating success and the output of the process @rtype tuple of (bool, str) """ ioEncoding = Preferences.getSystem("IOEncoding") process = QProcess() process.start(interpreter, args) procStarted = process.waitForStarted() if procStarted: finished = process.waitForFinished(30000) if finished: if process.exitCode() == 0: output = str(process.readAllStandardOutput(), ioEncoding, 'replace') return True, output else: return ( False, self.tr("python exited with an error ({0}).").format( process.exitCode())) else: process.terminate() process.waitForFinished(2000) process.kill() process.waitForFinished(3000) return False, self.tr("python did not finish within" " 30 seconds.") return False, self.tr("python could not be started.")
class WorkerProcess(QThread): sendOutput = pyqtSignal(str) def __init__(self, command): super(WorkerProcess, self).__init__() self.process = QProcess(self) self.command = command def __del__(self): self.process.terminate() if not self.process.waitForFinished(10000): self.process.kill() def start(self): self.setupProcess() def setupProcess(self): self.process.setProcessChannelMode(QProcess.MergedChannels) self.process.start( list(self.command.keys())[0], self.command[list(self.command.keys())[0]])
class ConsoleProcess(): #subclass that attaches a process and pipes the output to textedit widget console widget def __init__(self, console=None, errorHandler=None, finishHandler=None): self.process=QProcess() self.console=console self.cidFile="" self.state='stopped' if console: self.process.readyReadStandardOutput.connect(lambda: self.writeConsole(self.process, console,self.process.readAllStandardOutput,Qt.white)) self.process.readyReadStandardError.connect(lambda: self.writeConsole(self.process, console,self.process.readAllStandardError,Qt.red)) if finishHandler: self.process.finished.connect(finishHandler) def writeConsole(self,process,console,read,color): console.setTextColor(color) console.append(read().data().decode('utf-8',errors="ignore")) #console.append('</span>') def writeMessage(self,message,color=Qt.green): #for bwb messages self.console.setTextColor(color) self.console.append(message) def stop(self,message=None): self.state='stopped' #we need to write our own code to with open (self.cidFile,'r') as f: cid=f.read() stopCmd='docker stop {} '.format(cid) sys.stderr.write('Stop command: {}'.format(stopCmd)) os.system(stopCmd) self.process.kill() self.cleanup() if message: self.writeMessage(message) def cleanup(self): os.unlink(self.cidFile)
def run_ffprobe(self, is_image_BOOL): program = str(Path('./ffprobe')) if is_image_BOOL == False: args = [ '-v', 'error', '-select_streams', 'v:0', '-show_entries', 'format=duration,size:stream=width,height,r_frame_rate', self.filePath ] else: args = [ '-v', 'error', '-select_streams', 'v:0', '-show_entries', 'format=size:stream=width,height', self.filePath ] process = QProcess() process.setProcessChannelMode(QProcess.MergedChannels) process.start(program, args) if process.waitForFinished(-1): process.kill() ffprobe_output = bytearray( process.readAllStandardOutput()).decode('UTF-8') self.parse_ffprobe_out(ffprobe_output, is_image_BOOL)
class WorkerProcess(QThread): sendOutput = pyqtSignal(str) def __init__(self, command): super(WorkerProcess, self).__init__() self.process = QProcess(self) self.command = command def __del__(self): try: self.process.terminate() self.process.kill() except Exception: pass def start(self): self.setupProcess() def setupProcess(self): self.process.setProcessChannelMode(QProcess.MergedChannels) self.process.start( list(self.command.keys())[0], self.command[list(self.command.keys())[0]] )
def runProcess(self, args, cmd=""): """ Public method to execute the current pip with the given arguments. The selected pip executable is called with the given arguments and waited for its end. @param args list of command line arguments (list of string) @param cmd pip command to be used (string) @return tuple containing a flag indicating success and the output of the process (string) """ if not cmd: cmd = self.__plugin.getPreferences("CurrentPipExecutable") ioEncoding = Preferences.getSystem("IOEncoding") process = QProcess() process.start(cmd, args) procStarted = process.waitForStarted() if procStarted: finished = process.waitForFinished(30000) if finished: if process.exitCode() == 0: output = str(process.readAllStandardOutput(), ioEncoding, 'replace') return True, output else: return False, self.tr("pip exited with an error ({0}).")\ .format(process.exitCode()) else: process.terminate() process.waitForFinished(2000) process.kill() process.waitForFinished(3000) return False, self.tr("pip did not finish within 30 seconds.") return False, self.tr("pip could not be started.")
class Program(QObject): def __init__(self, **kwargs): QObject.__init__(self) self.filename = kwargs.get("filename") self.text_code = kwargs.get("code") self.__python_exec = kwargs.get("python_exec") self.pre_script = kwargs.get("pre_script") self.post_script = kwargs.get("post_script") self.__params = kwargs.get("params") self.__elapsed = QElapsedTimer() self.outputw = None self.__current_process = None self.main_process = QProcess(self) self.main_process.started.connect(self._process_started) self.main_process.finished.connect(self._process_finished) self.main_process.finished.connect(self.__post_execution) self.main_process.readyReadStandardOutput.connect(self._refresh_output) self.main_process.readyReadStandardError.connect(self._refresh_error) self.pre_process = QProcess(self) self.pre_process.started.connect(self._process_started) self.pre_process.finished.connect(self._process_finished) self.pre_process.finished.connect(self.__main_execution) self.pre_process.readyReadStandardOutput.connect(self._refresh_output) self.pre_process.readyReadStandardError.connect(self._refresh_error) self.post_process = QProcess(self) self.post_process.started.connect(self._process_started) self.post_process.finished.connect(self._process_finished) self.post_process.readyReadStandardOutput.connect(self._refresh_output) self.post_process.readyReadStandardError.connect(self._refresh_error) def start(self): self.__pre_execution() self.outputw.setFocus() def __pre_execution(self): """Execute a script before executing the project""" self.__current_process = self.pre_process file_pre_exec = QFile(self.pre_script) if file_pre_exec.exists(): ext = file_manager.get_file_extension(self.pre_script) args = [] if ext == "py": program = self.python_exec # -u: Force python to unbuffer stding ad stdout args.append("-u") args.append(self.pre_script) elif ext == "sh": program = "bash" args.append(self.pre_script) else: program = self.pre_script self.pre_process.setProgram(program) self.pre_process.setArguments(args) self.pre_process.start() else: self.__main_execution() def __main_execution(self): self.__elapsed.start() self.__current_process = self.main_process if not self.only_text: # In case a text is executed and not a file or project file_directory = file_manager.get_folder(self.filename) self.main_process.setWorkingDirectory(file_directory) self.main_process.setProgram(self.python_exec) self.main_process.setArguments(self.arguments) environment = QProcessEnvironment() system_environment = self.main_process.systemEnvironment() for env in system_environment: key, value = env.split("=", 1) environment.insert(key, value) self.main_process.setProcessEnvironment(environment) self.main_process.start() def __post_execution(self): """Execute a script after executing the project.""" self.__current_process = self.post_process file_pre_exec = QFile(self.post_script) if file_pre_exec.exists(): ext = file_manager.get_file_extension(self.post_script) args = [] if ext == "py": program = self.python_exec # -u: Force python to unbuffer stding ad stdout args.append("-u") args.append(self.post_script) elif ext == "sh": program = "bash" args.append(self.post_script) else: program = self.post_script self.post_process.setProgram(program) self.post_process.setArguments(args) self.post_process.start() @property def process_name(self): proc = self.__current_process return proc.program() + " " + " ".join(proc.arguments()) def update(self, **kwargs): self.text_code = kwargs.get("code") self.__python_exec = kwargs.get("python_exec") self.pre_script = kwargs.get("pre_script") self.post_script = kwargs.get("post_script") self.__params = kwargs.get("params") def set_output_widget(self, ow): self.outputw = ow self.outputw.inputRequested.connect(self._write_input) def _write_input(self, data): self.main_process.write(data.encode()) self.main_process.write(b"\n") def is_running(self): running = False if self.main_process.state() == QProcess.Running: running = True return running def _process_started(self): time_str = QTime.currentTime().toString("hh:mm:ss") text = time_str + " Running: " + self.process_name self.outputw.append_text(text) self.outputw.setReadOnly(False) def _process_finished(self, code, status): frmt = OutputWidget.Format.NORMAL if status == QProcess.NormalExit == code: text = translations.TR_PROCESS_EXITED_NORMALLY % code else: text = translations.TR_PROCESS_INTERRUPTED frmt = OutputWidget.Format.ERROR self.outputw.append_text(text, frmt) if self.__current_process is self.main_process: tformat = QTime(0, 0, 0, 0).addMSecs( self.__elapsed.elapsed() + 500) time = tformat.toString("h:mm:ss") if time.startswith("0:"): # Don't display zero hours time = time[2:] self.outputw.append_text(translations.TR_ELAPSED_TIME.format(time)) self.outputw.setReadOnly(True) def _refresh_output(self): data = self.__current_process.readAllStandardOutput().data().decode() for line in data.splitlines(): self.outputw.append_text( line, text_format=OutputWidget.Format.NORMAL) def _refresh_error(self): data = self.__current_process.readAllStandardError().data().decode() for line_text in data.splitlines(): frmt = OutputWidget.Format.ERROR if self.outputw.patLink.match(line_text): frmt = OutputWidget.Format.ERROR_UNDERLINE self.outputw.append_text(line_text, frmt) def display_name(self): name = "New document" if not self.only_text: name = file_manager.get_basename(self.filename) return name @property def only_text(self): return self.filename is None @property def python_exec(self): py_exec = self.__python_exec if not py_exec: py_exec = settings.PYTHON_EXEC return py_exec @property def arguments(self): args = [] if self.text_code: args.append("-c") args.append(self.text_code) else: # Force python to unbuffer stding and stdout args.append("-u") args += settings.EXECUTION_OPTIONS.split() args.append(self.filename) return args def kill(self): self.main_process.kill()
class HgGpgSignaturesDialog(QDialog, Ui_HgGpgSignaturesDialog): """ Class implementing a dialog showing signed changesets. """ def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent reference to the parent widget (QWidget) """ super(HgGpgSignaturesDialog, self).__init__(parent) self.setupUi(self) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.process = QProcess() self.vcs = vcs self.__hgClient = vcs.getClient() self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.show() QCoreApplication.processEvents() def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.__hgClient: if self.__hgClient.isExecuting(): self.__hgClient.cancel() else: if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def start(self, path): """ Public slot to start the list command. @param path name of directory (string) """ self.errorGroup.hide() self.intercept = False self.activateWindow() self.__path = path dname, fname = self.vcs.splitPath(path) # find the root of the repo repodir = dname while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)): repodir = os.path.dirname(repodir) if os.path.splitdrive(repodir)[1] == os.sep: return args = self.vcs.initCommand("sigs") if self.__hgClient: self.inputGroup.setEnabled(False) self.inputGroup.hide() out, err = self.__hgClient.runcommand(args) if err: self.__showError(err) if out: for line in out.splitlines(): self.__processOutputLine(line) if self.__hgClient.wasCanceled(): break self.__finish() else: self.process.kill() self.process.setWorkingDirectory(repodir) self.process.start('hg', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('hg')) else: self.inputGroup.setEnabled(True) self.inputGroup.show() def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.inputGroup.setEnabled(False) self.inputGroup.hide() self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.buttonBox.button(QDialogButtonBox.Close).setFocus( Qt.OtherFocusReason) self.process = None if self.signaturesList.topLevelItemCount() == 0: # no patches present self.__generateItem("", "", self.tr("no signatures found")) self.__resizeColumns() self.__resort() def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): if self.__hgClient: self.__hgClient.cancel() else: self.__finish() def __procFinished(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.__finish() def __resort(self): """ Private method to resort the tree. """ self.signaturesList.sortItems( self.signaturesList.sortColumn(), self.signaturesList.header().sortIndicatorOrder()) def __resizeColumns(self): """ Private method to resize the list columns. """ self.signaturesList.header().resizeSections( QHeaderView.ResizeToContents) self.signaturesList.header().setStretchLastSection(True) def __generateItem(self, revision, changeset, signature): """ Private method to generate a patch item in the list of patches. @param revision revision number (string) @param changeset changeset of the bookmark (string) @param signature signature of the changeset (string) """ if revision == "" and changeset == "": QTreeWidgetItem(self.signaturesList, [signature]) else: revString = "{0:>7}:{1}".format(revision, changeset) topItems = self.signaturesList.findItems( revString, Qt.MatchExactly) if len(topItems) == 0: # first signature for this changeset topItm = QTreeWidgetItem(self.signaturesList, [ "{0:>7}:{1}".format(revision, changeset)]) topItm.setExpanded(True) font = topItm.font(0) font.setBold(True) topItm.setFont(0, font) else: topItm = topItems[0] QTreeWidgetItem(topItm, [signature]) def __readStdout(self): """ Private slot to handle the readyReadStdout signal. It reads the output of the process, formats it and inserts it into the contents pane. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): s = str(self.process.readLine(), self.vcs.getEncoding(), 'replace').strip() self.__processOutputLine(s) def __processOutputLine(self, line): """ Private method to process the lines of output. @param line output line to be processed (string) """ li = line.split() if li[-1][0] in "1234567890": # last element is a rev:changeset rev, changeset = li[-1].split(":", 1) del li[-1] signature = " ".join(li) self.__generateItem(rev, changeset, signature) def __readStderr(self): """ Private slot to handle the readyReadStderr signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: s = str(self.process.readAllStandardError(), self.vcs.getEncoding(), 'replace') self.__showError(s) def __showError(self, out): """ Private slot to show some error. @param out error to be shown (string) """ self.errorGroup.show() self.errors.insertPlainText(out) self.errors.ensureCursorVisible() @pyqtSlot() def on_signaturesList_itemSelectionChanged(self): """ Private slot handling changes of the selection. """ selectedItems = self.signaturesList.selectedItems() if len(selectedItems) == 1 and \ self.signaturesList.indexOfTopLevelItem(selectedItems[0]) != -1: self.verifyButton.setEnabled(True) else: self.verifyButton.setEnabled(False) @pyqtSlot() def on_verifyButton_clicked(self): """ Private slot to verify the signatures of the selected revision. """ rev = self.signaturesList.selectedItems()[0].text(0)\ .split(":")[0].strip() self.vcs.getExtensionObject("gpg")\ .hgGpgVerifySignatures(self.__path, rev) @pyqtSlot(str) def on_categoryCombo_activated(self, txt): """ Private slot called, when a new filter category is selected. @param txt text of the selected category (string) """ self.__filterSignatures() @pyqtSlot(str) def on_rxEdit_textChanged(self, txt): """ Private slot called, when a filter expression is entered. @param txt filter expression (string) """ self.__filterSignatures() def __filterSignatures(self): """ Private method to filter the log entries. """ searchRxText = self.rxEdit.text() filterTop = self.categoryCombo.currentText() == self.tr("Revision") if filterTop and searchRxText.startswith("^"): searchRx = QRegExp( "^\s*{0}".format(searchRxText[1:]), Qt.CaseInsensitive) else: searchRx = QRegExp(searchRxText, Qt.CaseInsensitive) for topIndex in range(self.signaturesList.topLevelItemCount()): topLevelItem = self.signaturesList.topLevelItem(topIndex) if filterTop: topLevelItem.setHidden( searchRx.indexIn(topLevelItem.text(0)) == -1) else: visibleChildren = topLevelItem.childCount() for childIndex in range(topLevelItem.childCount()): childItem = topLevelItem.child(childIndex) if searchRx.indexIn(childItem.text(0)) == -1: childItem.setHidden(True) visibleChildren -= 1 else: childItem.setHidden(False) topLevelItem.setHidden(visibleChildren == 0) def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the subversion process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(HgGpgSignaturesDialog, self).keyPressEvent(evt)
class HgStatusDialog(QWidget, Ui_HgStatusDialog): """ Class implementing a dialog to show the output of the hg status command process. """ def __init__(self, vcs, mq=False, parent=None): """ Constructor @param vcs reference to the vcs object @param mq flag indicating to show a queue repo status (boolean) @param parent parent widget (QWidget) """ super(HgStatusDialog, self).__init__(parent) self.setupUi(self) self.__toBeCommittedColumn = 0 self.__statusColumn = 1 self.__pathColumn = 2 self.__lastColumn = self.statusList.columnCount() self.refreshButton = self.buttonBox.addButton(self.tr("Refresh"), QDialogButtonBox.ActionRole) self.refreshButton.setToolTip(self.tr("Press to refresh the status display")) self.refreshButton.setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.diff = None self.vcs = vcs self.vcs.committed.connect(self.__committed) self.__hgClient = self.vcs.getClient() self.__mq = mq if self.__hgClient: self.process = None else: self.process = QProcess() self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.diffSplitter.setSizes([350, 250]) self.__diffSplitterState = None self.statusList.headerItem().setText(self.__lastColumn, "") self.statusList.header().setSortIndicator(self.__pathColumn, Qt.AscendingOrder) font = Preferences.getEditorOtherFonts("MonospacedFont") self.diffEdit.setFontFamily(font.family()) self.diffEdit.setFontPointSize(font.pointSize()) self.diffHighlighter = HgDiffHighlighter(self.diffEdit.document()) self.__diffGenerator = HgDiffGenerator(vcs, self) self.__diffGenerator.finished.connect(self.__generatorFinished) self.__selectedName = "" if mq: self.buttonsLine.setVisible(False) self.addButton.setVisible(False) self.diffButton.setVisible(False) self.sbsDiffButton.setVisible(False) self.revertButton.setVisible(False) self.forgetButton.setVisible(False) self.restoreButton.setVisible(False) self.diffEdit.setVisible(False) self.menuactions = [] self.lfActions = [] self.menu = QMenu() if not mq: self.__commitAct = self.menu.addAction(self.tr("Commit changes to repository..."), self.__commit) self.menuactions.append(self.__commitAct) self.menuactions.append(self.menu.addAction(self.tr("Select all for commit"), self.__commitSelectAll)) self.menuactions.append(self.menu.addAction(self.tr("Deselect all from commit"), self.__commitDeselectAll)) self.menu.addSeparator() self.__addAct = self.menu.addAction(self.tr("Add to repository"), self.__add) self.menuactions.append(self.__addAct) if self.vcs.version >= (2, 0): self.lfActions.append(self.menu.addAction(self.tr("Add as Large File"), lambda: self.__lfAdd("large"))) self.lfActions.append( self.menu.addAction(self.tr("Add as Normal File"), lambda: self.__lfAdd("normal")) ) self.__diffAct = self.menu.addAction(self.tr("Show differences"), self.__diff) self.menuactions.append(self.__diffAct) self.__sbsDiffAct = self.menu.addAction(self.tr("Show differences side-by-side"), self.__sbsDiff) self.menuactions.append(self.__sbsDiffAct) self.__revertAct = self.menu.addAction(self.tr("Revert changes"), self.__revert) self.menuactions.append(self.__revertAct) self.__forgetAct = self.menu.addAction(self.tr("Forget missing"), self.__forget) self.menuactions.append(self.__forgetAct) self.__restoreAct = self.menu.addAction(self.tr("Restore missing"), self.__restoreMissing) self.menuactions.append(self.__restoreAct) self.menu.addSeparator() self.menuactions.append(self.menu.addAction(self.tr("Adjust column sizes"), self.__resizeColumns)) for act in self.menuactions: act.setEnabled(False) for act in self.lfActions: act.setEnabled(False) self.statusList.setContextMenuPolicy(Qt.CustomContextMenu) self.statusList.customContextMenuRequested.connect(self.__showContextMenu) if not mq and self.vcs.version >= (2, 0): self.__lfAddActions = [] self.__addButtonMenu = QMenu() self.__addButtonMenu.addAction(self.tr("Add"), self.__add) self.__lfAddActions.append( self.__addButtonMenu.addAction(self.tr("Add as Large File"), lambda: self.__lfAdd("large")) ) self.__lfAddActions.append( self.__addButtonMenu.addAction(self.tr("Add as Normal File"), lambda: self.__lfAdd("normal")) ) self.addButton.setMenu(self.__addButtonMenu) self.__addButtonMenu.aboutToShow.connect(self.__showAddMenu) self.modifiedIndicators = [self.tr("added"), self.tr("modified"), self.tr("removed")] self.unversionedIndicators = [self.tr("not tracked")] self.missingIndicators = [self.tr("missing")] self.status = { "A": self.tr("added"), "C": self.tr("normal"), "I": self.tr("ignored"), "M": self.tr("modified"), "R": self.tr("removed"), "?": self.tr("not tracked"), "!": self.tr("missing"), } def show(self): """ Public slot to show the dialog. """ super(HgStatusDialog, self).show() if not self.__mq and self.__diffSplitterState: self.diffSplitter.restoreState(self.__diffSplitterState) def __resort(self): """ Private method to resort the tree. """ self.statusList.sortItems(self.statusList.sortColumn(), self.statusList.header().sortIndicatorOrder()) def __resizeColumns(self): """ Private method to resize the list columns. """ self.statusList.header().resizeSections(QHeaderView.ResizeToContents) self.statusList.header().setStretchLastSection(True) def __generateItem(self, status, path): """ Private method to generate a status item in the status list. @param status status indicator (string) @param path path of the file or directory (string) """ statusText = self.status[status] itm = QTreeWidgetItem(self.statusList, ["", statusText, path]) itm.setTextAlignment(1, Qt.AlignHCenter) itm.setTextAlignment(2, Qt.AlignLeft) if status in "AMR": itm.setFlags(itm.flags() | Qt.ItemIsUserCheckable) itm.setCheckState(self.__toBeCommittedColumn, Qt.Checked) else: itm.setFlags(itm.flags() & ~Qt.ItemIsUserCheckable) if statusText not in self.__statusFilters: self.__statusFilters.append(statusText) def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.__hgClient: if self.__hgClient.isExecuting(): self.__hgClient.cancel() else: if self.process is not None and self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) if not self.__mq: self.__diffSplitterState = self.diffSplitter.saveState() e.accept() def start(self, fn): """ Public slot to start the hg status command. @param fn filename(s)/directoryname(s) to show the status of (string or list of strings) """ self.errorGroup.hide() self.intercept = False self.args = fn for act in self.menuactions: act.setEnabled(False) for act in self.lfActions: act.setEnabled(False) self.addButton.setEnabled(False) self.commitButton.setEnabled(False) self.diffButton.setEnabled(False) self.sbsDiffButton.setEnabled(False) self.revertButton.setEnabled(False) self.forgetButton.setEnabled(False) self.restoreButton.setEnabled(False) self.statusFilterCombo.clear() self.__statusFilters = [] self.statusList.clear() if self.__mq: self.setWindowTitle(self.tr("Mercurial Queue Repository Status")) else: self.setWindowTitle(self.tr("Mercurial Status")) args = self.vcs.initCommand("status") if self.__mq: args.append("--mq") if isinstance(fn, list): self.dname, fnames = self.vcs.splitPathList(fn) else: self.dname, fname = self.vcs.splitPath(fn) else: if self.vcs.hasSubrepositories(): args.append("--subrepos") if isinstance(fn, list): self.dname, fnames = self.vcs.splitPathList(fn) self.vcs.addArguments(args, fn) else: self.dname, fname = self.vcs.splitPath(fn) args.append(fn) # find the root of the repo repodir = self.dname while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)): repodir = os.path.dirname(repodir) if os.path.splitdrive(repodir)[1] == os.sep: return if self.__hgClient: self.inputGroup.setEnabled(False) self.inputGroup.hide() self.refreshButton.setEnabled(False) out, err = self.__hgClient.runcommand(args) if err: self.__showError(err) if out: for line in out.splitlines(): self.__processOutputLine(line) if self.__hgClient.wasCanceled(): break self.__finish() else: if self.process: self.process.kill() self.process.setWorkingDirectory(repodir) self.process.start("hg", args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr("Process Generation Error"), self.tr("The process {0} could not be started. " "Ensure, that it is in the search path.").format( "hg" ), ) else: self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.inputGroup.setEnabled(True) self.inputGroup.show() self.refreshButton.setEnabled(False) def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.inputGroup.setEnabled(False) self.inputGroup.hide() self.refreshButton.setEnabled(True) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.buttonBox.button(QDialogButtonBox.Close).setFocus(Qt.OtherFocusReason) self.__statusFilters.sort() self.__statusFilters.insert(0, "<{0}>".format(self.tr("all"))) self.statusFilterCombo.addItems(self.__statusFilters) for act in self.menuactions: act.setEnabled(True) self.__resort() self.__resizeColumns() self.__updateButtons() self.__updateCommitButton() self.__refreshDiff() def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): if self.__hgClient: self.__hgClient.cancel() else: self.__finish() elif button == self.refreshButton: self.on_refreshButton_clicked() def __procFinished(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.__finish() def __readStdout(self): """ Private slot to handle the readyReadStandardOutput signal. It reads the output of the process, formats it and inserts it into the contents pane. """ if self.process is not None: self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): line = str(self.process.readLine(), self.vcs.getEncoding(), "replace") self.__processOutputLine(line) def __processOutputLine(self, line): """ Private method to process the lines of output. @param line output line to be processed (string) """ if line[0] in "ACIMR?!" and line[1] == " ": status, path = line.strip().split(" ", 1) self.__generateItem(status, path) def __readStderr(self): """ Private slot to handle the readyReadStandardError signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: s = str(self.process.readAllStandardError(), self.vcs.getEncoding(), "replace") self.__showError(s) def __showError(self, out): """ Private slot to show some error. @param out error to be shown (string) """ self.errorGroup.show() self.errors.insertPlainText(out) self.errors.ensureCursorVisible() def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the subversion process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(HgStatusDialog, self).keyPressEvent(evt) @pyqtSlot() def on_refreshButton_clicked(self): """ Private slot to refresh the status display. """ selectedItems = self.statusList.selectedItems() if len(selectedItems) == 1: self.__selectedName = selectedItems[0].text(self.__pathColumn) else: self.__selectedName = "" self.start(self.args) def __updateButtons(self): """ Private method to update the VCS buttons status. """ modified = len(self.__getModifiedItems()) unversioned = len(self.__getUnversionedItems()) missing = len(self.__getMissingItems()) self.addButton.setEnabled(unversioned) self.diffButton.setEnabled(modified) self.sbsDiffButton.setEnabled(modified == 1) self.revertButton.setEnabled(modified) self.forgetButton.setEnabled(missing) self.restoreButton.setEnabled(missing) def __updateCommitButton(self): """ Private method to update the Commit button status. """ commitable = len(self.__getCommitableItems()) self.commitButton.setEnabled(commitable) @pyqtSlot(str) def on_statusFilterCombo_activated(self, txt): """ Private slot to react to the selection of a status filter. @param txt selected status filter (string) """ if txt == "<{0}>".format(self.tr("all")): for topIndex in range(self.statusList.topLevelItemCount()): topItem = self.statusList.topLevelItem(topIndex) topItem.setHidden(False) else: for topIndex in range(self.statusList.topLevelItemCount()): topItem = self.statusList.topLevelItem(topIndex) topItem.setHidden(topItem.text(self.__statusColumn) != txt) @pyqtSlot(QTreeWidgetItem, int) def on_statusList_itemChanged(self, item, column): """ Private slot to act upon item changes. @param item reference to the changed item (QTreeWidgetItem) @param column index of column that changed (integer) """ if column == self.__toBeCommittedColumn: self.__updateCommitButton() @pyqtSlot() def on_statusList_itemSelectionChanged(self): """ Private slot to act upon changes of selected items. """ self.__updateButtons() self.__generateDiffs() @pyqtSlot() def on_commitButton_clicked(self): """ Private slot to handle the press of the Commit button. """ self.__commit() @pyqtSlot() def on_addButton_clicked(self): """ Private slot to handle the press of the Add button. """ self.__add() @pyqtSlot() def on_diffButton_clicked(self): """ Private slot to handle the press of the Differences button. """ self.__diff() @pyqtSlot() def on_sbsDiffButton_clicked(self): """ Private slot to handle the press of the Side-by-Side Diff button. """ self.__sbsDiff() @pyqtSlot() def on_revertButton_clicked(self): """ Private slot to handle the press of the Revert button. """ self.__revert() @pyqtSlot() def on_forgetButton_clicked(self): """ Private slot to handle the press of the Forget button. """ self.__forget() @pyqtSlot() def on_restoreButton_clicked(self): """ Private slot to handle the press of the Restore button. """ self.__restoreMissing() ########################################################################### ## Context menu handling methods ########################################################################### def __showContextMenu(self, coord): """ Private slot to show the context menu of the status list. @param coord the position of the mouse pointer (QPoint) """ modified = len(self.__getModifiedItems()) unversioned = len(self.__getUnversionedItems()) missing = len(self.__getMissingItems()) commitable = len(self.__getCommitableItems()) self.__addAct.setEnabled(unversioned) self.__diffAct.setEnabled(modified) self.__sbsDiffAct.setEnabled(modified == 1) self.__revertAct.setEnabled(modified) self.__forgetAct.setEnabled(missing) self.__restoreAct.setEnabled(missing) self.__commitAct.setEnabled(commitable) if self.vcs.isExtensionActive("largefiles"): enable = len(self.__getUnversionedItems()) > 0 else: enable = False for act in self.lfActions: act.setEnabled(enable) self.menu.popup(self.statusList.mapToGlobal(coord)) def __showAddMenu(self): """ Private slot to prepare the Add button menu before it is shown. """ enable = self.vcs.isExtensionActive("largefiles") for act in self.__lfAddActions: act.setEnabled(enable) def __commit(self): """ Private slot to handle the Commit context menu entry. """ if self.__mq: self.vcs.vcsCommit(self.dname, "", mq=True) else: names = [os.path.join(self.dname, itm.text(self.__pathColumn)) for itm in self.__getCommitableItems()] if not names: E5MessageBox.information( self, self.tr("Commit"), self.tr("""There are no entries selected to be""" """ committed.""") ) return if Preferences.getVCS("AutoSaveFiles"): vm = e5App().getObject("ViewManager") for name in names: vm.saveEditor(name) self.vcs.vcsCommit(names, "") def __committed(self): """ Private slot called after the commit has finished. """ if self.isVisible(): self.on_refreshButton_clicked() self.vcs.checkVCSStatus() def __commitSelectAll(self): """ Private slot to select all entries for commit. """ self.__commitSelect(True) def __commitDeselectAll(self): """ Private slot to deselect all entries from commit. """ self.__commitSelect(False) def __add(self): """ Private slot to handle the Add context menu entry. """ names = [os.path.join(self.dname, itm.text(self.__pathColumn)) for itm in self.__getUnversionedItems()] if not names: E5MessageBox.information( self, self.tr("Add"), self.tr("""There are no unversioned entries""" """ available/selected.""") ) return self.vcs.vcsAdd(names) self.on_refreshButton_clicked() project = e5App().getObject("Project") for name in names: project.getModel().updateVCSStatus(name) self.vcs.checkVCSStatus() def __lfAdd(self, mode): """ Private slot to add a file to the repository. @param mode add mode (string one of 'normal' or 'large') """ names = [os.path.join(self.dname, itm.text(self.__pathColumn)) for itm in self.__getUnversionedItems()] if not names: E5MessageBox.information( self, self.tr("Add"), self.tr("""There are no unversioned entries""" """ available/selected.""") ) return self.vcs.getExtensionObject("largefiles").hgAdd(names, mode) self.on_refreshButton_clicked() project = e5App().getObject("Project") for name in names: project.getModel().updateVCSStatus(name) self.vcs.checkVCSStatus() def __forget(self): """ Private slot to handle the Remove context menu entry. """ names = [os.path.join(self.dname, itm.text(self.__pathColumn)) for itm in self.__getMissingItems()] if not names: E5MessageBox.information( self, self.tr("Remove"), self.tr("""There are no missing entries""" """ available/selected.""") ) return self.vcs.hgForget(names) self.on_refreshButton_clicked() def __revert(self): """ Private slot to handle the Revert context menu entry. """ names = [os.path.join(self.dname, itm.text(self.__pathColumn)) for itm in self.__getModifiedItems()] if not names: E5MessageBox.information( self, self.tr("Revert"), self.tr("""There are no uncommitted changes""" """ available/selected.""") ) return self.vcs.hgRevert(names) self.raise_() self.activateWindow() self.on_refreshButton_clicked() project = e5App().getObject("Project") for name in names: project.getModel().updateVCSStatus(name) self.vcs.checkVCSStatus() def __restoreMissing(self): """ Private slot to handle the Restore Missing context menu entry. """ names = [os.path.join(self.dname, itm.text(self.__pathColumn)) for itm in self.__getMissingItems()] if not names: E5MessageBox.information( self, self.tr("Revert"), self.tr("""There are no missing entries""" """ available/selected.""") ) return self.vcs.hgRevert(names) self.on_refreshButton_clicked() self.vcs.checkVCSStatus() def __diff(self): """ Private slot to handle the Diff context menu entry. """ names = [os.path.join(self.dname, itm.text(self.__pathColumn)) for itm in self.__getModifiedItems()] if not names: E5MessageBox.information( self, self.tr("Differences"), self.tr("""There are no uncommitted changes""" """ available/selected.""") ) return if self.diff is None: from .HgDiffDialog import HgDiffDialog self.diff = HgDiffDialog(self.vcs) self.diff.show() self.diff.start(names, refreshable=True) def __sbsDiff(self): """ Private slot to handle the Diff context menu entry. """ names = [os.path.join(self.dname, itm.text(self.__pathColumn)) for itm in self.__getModifiedItems()] if not names: E5MessageBox.information( self, self.tr("Side-by-Side Diff"), self.tr("""There are no uncommitted changes""" """ available/selected."""), ) return elif len(names) > 1: E5MessageBox.information( self, self.tr("Side-by-Side Diff"), self.tr("""Only one file with uncommitted changes""" """ must be selected."""), ) return self.vcs.hgSbsDiff(names[0]) def __getCommitableItems(self): """ Private method to retrieve all entries the user wants to commit. @return list of all items, the user has checked """ commitableItems = [] for index in range(self.statusList.topLevelItemCount()): itm = self.statusList.topLevelItem(index) if itm.checkState(self.__toBeCommittedColumn) == Qt.Checked: commitableItems.append(itm) return commitableItems def __getModifiedItems(self): """ Private method to retrieve all entries, that have a modified status. @return list of all items with a modified status """ modifiedItems = [] for itm in self.statusList.selectedItems(): if itm.text(self.__statusColumn) in self.modifiedIndicators: modifiedItems.append(itm) return modifiedItems def __getUnversionedItems(self): """ Private method to retrieve all entries, that have an unversioned status. @return list of all items with an unversioned status """ unversionedItems = [] for itm in self.statusList.selectedItems(): if itm.text(self.__statusColumn) in self.unversionedIndicators: unversionedItems.append(itm) return unversionedItems def __getMissingItems(self): """ Private method to retrieve all entries, that have a missing status. @return list of all items with a missing status """ missingItems = [] for itm in self.statusList.selectedItems(): if itm.text(self.__statusColumn) in self.missingIndicators: missingItems.append(itm) return missingItems def __commitSelect(self, selected): """ Private slot to select or deselect all entries. @param selected commit selection state to be set (boolean) """ for index in range(self.statusList.topLevelItemCount()): itm = self.statusList.topLevelItem(index) if itm.flags() & Qt.ItemIsUserCheckable: if selected: itm.setCheckState(self.__toBeCommittedColumn, Qt.Checked) else: itm.setCheckState(self.__toBeCommittedColumn, Qt.Unchecked) ########################################################################### ## Diff handling methods below ########################################################################### def __generateDiffs(self): """ Private slot to generate diff outputs for the selected item. """ self.diffEdit.clear() if not self.__mq: selectedItems = self.statusList.selectedItems() if len(selectedItems) == 1: fn = os.path.join(self.dname, selectedItems[0].text(self.__pathColumn)) self.__diffGenerator.start(fn) def __generatorFinished(self): """ Private slot connected to the finished signal of the diff generator. """ diff = self.__diffGenerator.getResult()[0] if diff: for line in diff[:]: if line.startswith("@@ "): break else: diff.pop(0) self.diffEdit.setPlainText("".join(diff)) tc = self.diffEdit.textCursor() tc.movePosition(QTextCursor.Start) self.diffEdit.setTextCursor(tc) self.diffEdit.ensureCursorVisible() def __refreshDiff(self): """ Private method to refresh the diff output after a refresh. """ if self.__selectedName and not self.__mq: for index in range(self.statusList.topLevelItemCount()): itm = self.statusList.topLevelItem(index) if itm.text(self.__pathColumn) == self.__selectedName: itm.setSelected(True) break self.__selectedName = ""
class HgLogDialog(QWidget, Ui_HgLogDialog): """ Class implementing a dialog to show the output of the hg log command process. The dialog is nonmodal. Clicking a link in the upper text pane shows a diff of the revisions. """ def __init__(self, vcs, mode="log", bundle=None, isFile=False, parent=None): """ Constructor @param vcs reference to the vcs object @param mode mode of the dialog (string; one of log, incoming, outgoing) @param bundle name of a bundle file (string) @param isFile flag indicating log for a file is to be shown (boolean) @param parent parent widget (QWidget) """ super(HgLogDialog, self).__init__(parent) self.setupUi(self) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.process = QProcess() self.vcs = vcs if mode in ("log", "incoming", "outgoing"): self.mode = mode else: self.mode = "log" self.bundle = bundle self.__hgClient = self.vcs.getClient() self.contents.setHtml( self.tr('<b>Processing your request, please wait...</b>')) self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.contents.anchorClicked.connect(self.__sourceChanged) self.revisions = [] # stack of remembered revisions self.revString = self.tr('Revision') self.projectMode = False self.logEntries = [] # list of log entries self.lastLogEntry = {} self.fileCopies = {} self.endInitialText = False self.initialText = [] self.diff = None self.sbsCheckBox.setEnabled(isFile) self.sbsCheckBox.setVisible(isFile) def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.__hgClient: if self.__hgClient.isExecuting(): self.__hgClient.cancel() else: if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def start(self, fn, noEntries=0, revisions=None): """ Public slot to start the hg log command. @param fn filename to show the log for (string) @param noEntries number of entries to show (integer) @param revisions revisions to show log for (list of strings) """ self.errorGroup.hide() QApplication.processEvents() self.intercept = False self.filename = fn self.dname, self.fname = self.vcs.splitPath(fn) # find the root of the repo self.repodir = self.dname while not os.path.isdir(os.path.join(self.repodir, self.vcs.adminDir)): self.repodir = os.path.dirname(self.repodir) if os.path.splitdrive(self.repodir)[1] == os.sep: return self.projectMode = (self.fname == "." and self.dname == self.repodir) self.activateWindow() self.raise_() preargs = [] args = self.vcs.initCommand(self.mode) if noEntries and self.mode == "log": args.append('--limit') args.append(str(noEntries)) if self.mode in ("incoming", "outgoing"): args.append("--newest-first") if self.vcs.hasSubrepositories(): args.append("--subrepos") if self.mode == "log": args.append('--copies') if self.vcs.version >= (3, 0): args.append('--template') args.append(os.path.join(os.path.dirname(__file__), "templates", "logDialogBookmarkPhase.tmpl")) else: args.append('--style') if self.vcs.version >= (2, 1): args.append(os.path.join(os.path.dirname(__file__), "styles", "logDialogBookmarkPhase.style")) else: args.append(os.path.join(os.path.dirname(__file__), "styles", "logDialogBookmark.style")) if self.mode == "incoming": if self.bundle: args.append(self.bundle) elif not self.vcs.hasSubrepositories(): project = e5App().getObject("Project") self.vcs.bundleFile = os.path.join( project.getProjectManagementDir(), "hg-bundle.hg") if os.path.exists(self.vcs.bundleFile): os.remove(self.vcs.bundleFile) preargs = args[:] preargs.append("--quiet") preargs.append('--bundle') preargs.append(self.vcs.bundleFile) args.append(self.vcs.bundleFile) if revisions: for rev in revisions: args.append("--rev") args.append(rev) if not self.projectMode: args.append(self.filename) if self.__hgClient: self.inputGroup.setEnabled(False) self.inputGroup.hide() if preargs: out, err = self.__hgClient.runcommand(preargs) else: err = "" if err: self.__showError(err) elif self.mode != "incoming" or \ (self.vcs.bundleFile and os.path.exists(self.vcs.bundleFile)) or \ self.bundle: out, err = self.__hgClient.runcommand(args) if err: self.__showError(err) if out and self.isVisible(): for line in out.splitlines(True): self.__processOutputLine(line) if self.__hgClient.wasCanceled(): break self.__finish() else: self.process.kill() self.process.setWorkingDirectory(self.repodir) if preargs: process = QProcess() process.setWorkingDirectory(self.repodir) process.start('hg', args) procStarted = process.waitForStarted(5000) if procStarted: process.waitForFinished(30000) if self.mode != "incoming" or \ (self.vcs.bundleFile and os.path.exists(self.vcs.bundleFile)) or \ self.bundle: self.process.start('hg', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('hg')) else: self.__finish() def __getParents(self, rev): """ Private method to get the parents of the currently viewed file/directory. @param rev revision number to get parents for (string) @return list of parent revisions (list of strings) """ errMsg = "" parents = [] if int(rev) > 0: args = self.vcs.initCommand("parents") if self.mode == "incoming": if self.bundle: args.append("--repository") args.append(self.bundle) elif self.vcs.bundleFile and \ os.path.exists(self.vcs.bundleFile): args.append("--repository") args.append(self.vcs.bundleFile) args.append("--template") args.append("{rev}:{node|short}\n") args.append("-r") args.append(rev) if not self.projectMode: args.append(self.filename) output = "" if self.__hgClient: output, errMsg = self.__hgClient.runcommand(args) else: process = QProcess() process.setWorkingDirectory(self.repodir) process.start('hg', args) procStarted = process.waitForStarted(5000) if procStarted: finished = process.waitForFinished(30000) if finished and process.exitCode() == 0: output = str(process.readAllStandardOutput(), self.vcs.getEncoding(), 'replace') else: if not finished: errMsg = self.tr( "The hg process did not finish within 30s.") else: errMsg = self.tr("Could not start the hg executable.") if errMsg: E5MessageBox.critical( self, self.tr("Mercurial Error"), errMsg) if output: parents = [p for p in output.strip().splitlines()] return parents def __procFinished(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.__finish() def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ self.inputGroup.setEnabled(False) self.inputGroup.hide() self.contents.clear() if not self.logEntries: self.errors.append(self.tr("No log available for '{0}'") .format(self.filename)) self.errorGroup.show() return html = "" if self.initialText: for line in self.initialText: html += Utilities.html_encode(line.strip()) html += '<br />\n' html += '{0}<br/>\n'.format(80 * "=") for entry in self.logEntries: fileCopies = {} if entry["file_copies"]: for fentry in entry["file_copies"].split(", "): newName, oldName = fentry[:-1].split(" (") fileCopies[newName] = oldName rev, hexRev = entry["change"].split(":") dstr = '<p><b>{0} {1}</b>'.format(self.revString, entry["change"]) if entry["parents"]: parents = entry["parents"].split() else: parents = self.__getParents(rev) for parent in parents: url = QUrl() url.setScheme("file") url.setPath(self.filename) if qVersion() >= "5.0.0": query = parent.split(":")[0] + '_' + rev url.setQuery(query) else: query = QByteArray() query.append(parent.split(":")[0]).append('_').append(rev) url.setEncodedQuery(query) dstr += ' [<a href="{0}" name="{1}" id="{1}">{2}</a>]'.format( url.toString(), query, self.tr('diff to {0}').format(parent), ) dstr += '<br />\n' html += dstr if "phase" in entry: html += self.tr("Phase: {0}<br />\n")\ .format(entry["phase"]) html += self.tr("Branch: {0}<br />\n")\ .format(entry["branches"]) html += self.tr("Tags: {0}<br />\n").format(entry["tags"]) if "bookmarks" in entry: html += self.tr("Bookmarks: {0}<br />\n")\ .format(entry["bookmarks"]) html += self.tr("Parents: {0}<br />\n")\ .format(entry["parents"]) html += self.tr('<i>Author: {0}</i><br />\n')\ .format(Utilities.html_encode(entry["user"])) date, time = entry["date"].split()[:2] html += self.tr('<i>Date: {0}, {1}</i><br />\n')\ .format(date, time) for line in entry["description"]: html += Utilities.html_encode(line.strip()) html += '<br />\n' if entry["file_adds"]: html += '<br />\n' for f in entry["file_adds"].strip().split(", "): if f in fileCopies: html += self.tr( 'Added {0} (copied from {1})<br />\n')\ .format(Utilities.html_encode(f), Utilities.html_encode(fileCopies[f])) else: html += self.tr('Added {0}<br />\n')\ .format(Utilities.html_encode(f)) if entry["files_mods"]: html += '<br />\n' for f in entry["files_mods"].strip().split(", "): html += self.tr('Modified {0}<br />\n')\ .format(Utilities.html_encode(f)) if entry["file_dels"]: html += '<br />\n' for f in entry["file_dels"].strip().split(", "): html += self.tr('Deleted {0}<br />\n')\ .format(Utilities.html_encode(f)) html += '</p>{0}<br/>\n'.format(60 * "=") self.contents.setHtml(html) tc = self.contents.textCursor() tc.movePosition(QTextCursor.Start) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() def __readStdout(self): """ Private slot to handle the readyReadStandardOutput signal. It reads the output of the process and inserts it into a buffer. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): s = str(self.process.readLine(), self.vcs.getEncoding(), 'replace') self.__processOutputLine(s) def __processOutputLine(self, line): """ Private method to process the lines of output. @param line output line to be processed (string) """ if line == "@@@\n": self.logEntries.append(self.lastLogEntry) self.lastLogEntry = {} self.fileCopies = {} else: try: key, value = line.split("|", 1) except ValueError: key = "" value = line if key == "change": self.endInitialText = True if key in ("change", "tags", "parents", "user", "date", "file_copies", "file_adds", "files_mods", "file_dels", "bookmarks", "phase"): self.lastLogEntry[key] = value.strip() elif key == "branches": if value.strip(): self.lastLogEntry[key] = value.strip() else: self.lastLogEntry[key] = "default" elif key == "description": self.lastLogEntry[key] = [value.strip()] else: if self.endInitialText: self.lastLogEntry["description"].append(value.strip()) else: self.initialText.append(value) def __readStderr(self): """ Private slot to handle the readyReadStandardError signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: s = str(self.process.readAllStandardError(), self.vcs.getEncoding(), 'replace') self.__showError(s) def __showError(self, out): """ Private slot to show some error. @param out error to be shown (string) """ self.errorGroup.show() self.errors.insertPlainText(out) self.errors.ensureCursorVisible() def __sourceChanged(self, url): """ Private slot to handle the sourceChanged signal of the contents pane. @param url the url that was clicked (QUrl) """ filename = url.path() if Utilities.isWindowsPlatform(): if filename.startswith("/"): filename = filename[1:] if qVersion() >= "5.0.0": ver = url.query() else: ver = bytes(url.encodedQuery()).decode() v1, v2 = ver.split('_') if v1 == "" or v2 == "": return self.contents.scrollToAnchor(ver) if self.sbsCheckBox.isEnabled() and self.sbsCheckBox.isChecked(): self.vcs.hgSbsDiff(filename, revisions=(v1, v2)) else: if self.diff is None: from .HgDiffDialog import HgDiffDialog self.diff = HgDiffDialog(self.vcs) self.diff.show() self.diff.start(filename, [v1, v2], self.bundle) def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the hg process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(HgLogDialog, self).keyPressEvent(evt)
class PythonProcessPane(QTextEdit): """ Handles / displays a Python process's stdin/out with working command history and simple buffer editing. """ on_append_text = pyqtSignal(bytes) def __init__(self, parent=None): super().__init__(parent) self.setFont(Font().load()) self.setAcceptRichText(False) self.setReadOnly(False) self.setUndoRedoEnabled(False) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.context_menu) self.running = False # Flag to show the child process is running. self.setObjectName('PythonRunner') self.process = None # Will eventually reference the running process. self.input_history = [] # history of inputs entered in this session. self.start_of_current_line = 0 # start position of the input line. self.history_position = 0 # current position when navigation history. def start_process(self, script_name, working_directory, interactive=True, debugger=False, command_args=None, envars=None, runner=None): """ Start the child Python process. Will run the referenced Python script_name within the context of the working directory. If interactive is True (the default) the Python process will run in interactive mode (dropping the user into the REPL when the script completes). If debugger is True (the default is False) then the script will run within a debug runner session. If there is a list of command_args (the default is None), then these will be passed as further arguments into the script to be run. If there is a list of environment variables, these will be part of the context of the new child process. If runner is give, this is used as the command to start the Python process. """ self.script = os.path.abspath(os.path.normcase(script_name)) logger.info('Running script: {}'.format(self.script)) if interactive: logger.info('Running with interactive mode.') if command_args is None: command_args = [] logger.info('Command args: {}'.format(command_args)) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.MergedChannels) # Force buffers to flush immediately. env = QProcessEnvironment.systemEnvironment() env.insert('PYTHONUNBUFFERED', '1') if envars: logger.info('Running with environment variables: ' '{}'.format(envars)) for name, value in envars: env.insert(name, value) logger.info('Working directory: {}'.format(working_directory)) self.process.setWorkingDirectory(working_directory) self.process.setProcessEnvironment(env) self.process.readyRead.connect(self.read_from_stdout) self.process.finished.connect(self.finished) logger.info('Python path: {}'.format(sys.path)) if debugger: # Start the mu-debug runner for the script. args = [ self.script, ] + command_args self.process.start('mu-debug', args) else: if runner: # Use the passed in Python "runner" to run the script. python_exec = runner else: # Use the current system Python to run the script. python_exec = sys.executable if interactive: # Start the script in interactive Python mode. args = [ '-i', self.script, ] + command_args else: # Just run the command with no additional flags. args = [ self.script, ] + command_args self.process.start(python_exec, args) self.running = True def finished(self, code, status): """ Handle when the child process finishes. """ self.running = False cursor = self.textCursor() cursor.movePosition(cursor.End) cursor.insertText('\n\n---------- FINISHED ----------\n') msg = 'exit code: {} status: {}'.format(code, status) cursor.insertText(msg) cursor.movePosition(QTextCursor.End) self.setTextCursor(cursor) self.setReadOnly(True) def context_menu(self): """ Creates custom context menu with just copy and paste. """ menu = QMenu(self) if platform.system() == 'Darwin': copy_keys = QKeySequence(Qt.CTRL + Qt.Key_C) paste_keys = QKeySequence(Qt.CTRL + Qt.Key_V) else: copy_keys = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_C) paste_keys = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_V) menu.addAction("Copy", self.copy, copy_keys) menu.addAction("Paste", self.paste, paste_keys) menu.exec_(QCursor.pos()) def paste(self): """ Grabs clipboard contents then writes to the REPL. """ clipboard = QApplication.clipboard() if clipboard and clipboard.text(): # normalize for Windows line-ends. text = '\n'.join(clipboard.text().splitlines()) if text: self.parse_paste(text) def parse_paste(self, text): """ Recursively takes characters from text to be parsed as input. We do this so the event loop has time to respond to output from the process to which the characters are sent (for example, when a newline is sent). Yes, this is a quick and dirty hack, but ensures the pasted input is also evaluated in an interactive manner rather than as a single-shot splurge of data. Essentially, it's simulating someone typing in the characters of the pasted text *really fast* but in such a way that the event loop cycles. """ character = text[0] # the current character to process. remainder = text[1:] # remaining characters to process in the future. if character in string.printable: if character == '\n' or character == '\r': self.parse_input(Qt.Key_Enter, character, None) else: self.parse_input(None, character, None) if remainder: # Schedule a recursive call of parse_paste with the remaining text # to process. This allows the event loop to cycle and handle any # output from the child process as a result of the text pasted so # far (especially useful for handling responses from newlines). QTimer.singleShot(2, lambda text=remainder: self.parse_paste(text)) def keyPressEvent(self, data): """ Called when the user types something in the REPL. """ key = data.key() text = data.text() modifiers = data.modifiers() self.parse_input(key, text, modifiers) def parse_input(self, key, text, modifiers): """ Correctly encodes user input and sends it to the connected process. The key is a Qt.Key_Something value, text is the textual representation of the input, and modifiers are the control keys (shift, CTRL, META, etc) also used. """ msg = b'' # Eventually to be inserted into the pane at the cursor. if key == Qt.Key_Enter or key == Qt.Key_Return: msg = b'\n' elif (platform.system() == 'Darwin' and modifiers == Qt.MetaModifier) or \ (platform.system() != 'Darwin' and modifiers == Qt.ControlModifier): # Handle CTRL-C and CTRL-D if self.process and self.running: pid = self.process.processId() # NOTE: Windows related constraints don't allow us to send a # CTRL-C, rather, the process will just terminate. if key == Qt.Key_C: os.kill(pid, signal.SIGINT) if key == Qt.Key_D: self.process.kill() return elif key == Qt.Key_Up: self.history_back() elif key == Qt.Key_Down: self.history_forward() elif key == Qt.Key_Right: cursor = self.textCursor() cursor.movePosition(QTextCursor.Right) self.setTextCursor(cursor) elif key == Qt.Key_Left: cursor = self.textCursor() if cursor.position() > self.start_of_current_line: cursor.movePosition(QTextCursor.Left) self.setTextCursor(cursor) elif key == Qt.Key_Home: cursor = self.textCursor() cursor.movePosition(QTextCursor.End) buffer_len = len(self.toPlainText()) - self.start_of_current_line for i in range(buffer_len): cursor.movePosition(QTextCursor.Left) self.setTextCursor(cursor) elif key == Qt.Key_End: cursor = self.textCursor() cursor.movePosition(QTextCursor.End) self.setTextCursor(cursor) elif (modifiers == Qt.ControlModifier | Qt.ShiftModifier) or \ (platform.system() == 'Darwin' and modifiers == Qt.ControlModifier): # Command-key on Mac, Ctrl-Shift on Win/Lin if key == Qt.Key_C: self.copy() elif key == Qt.Key_V: self.paste() elif text in string.printable: # If the key is for a printable character then add it to the # active buffer and display it. msg = bytes(text, 'utf8') if key == Qt.Key_Backspace: self.backspace() if key == Qt.Key_Delete: self.delete() if not self.isReadOnly() and msg: self.insert(msg) if key == Qt.Key_Enter or key == Qt.Key_Return: content = self.toPlainText() line = content[self.start_of_current_line:].encode('utf-8') self.write_to_stdin(line) if line.strip(): self.input_history.append(line.replace(b'\n', b'')) self.history_position = 0 self.start_of_current_line = self.textCursor().position() def history_back(self): """ Replace the current input line with the next item BACK from the current history position. """ if self.input_history: self.history_position -= 1 history_pos = len(self.input_history) + self.history_position if history_pos < 0: self.history_position += 1 history_pos = 0 history_item = self.input_history[history_pos] self.replace_input_line(history_item) def history_forward(self): """ Replace the current input line with the next item FORWARD from the current history position. """ if self.input_history: self.history_position += 1 history_pos = len(self.input_history) + self.history_position if history_pos >= len(self.input_history): # At the most recent command. self.history_position = 0 self.clear_input_line() return history_item = self.input_history[history_pos] self.replace_input_line(history_item) def read_from_stdout(self): """ Process incoming data from the process's stdout. """ data = self.process.readAll().data() if data: self.append(data) self.on_append_text.emit(data) cursor = self.textCursor() self.start_of_current_line = cursor.position() def write_to_stdin(self, data): """ Writes data from the Qt application to the child process's stdin. """ if self.process: self.process.write(data) def append(self, msg): """ Append text to the text area. """ cursor = self.textCursor() cursor.movePosition(QTextCursor.End) cursor.insertText(msg.decode('utf-8')) cursor.movePosition(QTextCursor.End) self.setTextCursor(cursor) def insert(self, msg): """ Insert text to the text area at the current cursor position. """ cursor = self.textCursor() if cursor.position() < self.start_of_current_line: cursor.movePosition(QTextCursor.End) cursor.insertText(msg.decode('utf-8')) self.setTextCursor(cursor) def backspace(self): """ Removes a character from the current buffer -- to the left of cursor. """ cursor = self.textCursor() if cursor.position() > self.start_of_current_line: cursor = self.textCursor() cursor.deletePreviousChar() self.setTextCursor(cursor) def delete(self): """ Removes a character from the current buffer -- to the right of cursor. """ cursor = self.textCursor() if cursor.position() >= self.start_of_current_line: cursor.deleteChar() self.setTextCursor(cursor) def clear_input_line(self): """ Remove all the characters currently in the input buffer line. """ cursor = self.textCursor() cursor.movePosition(QTextCursor.End) buffer_len = len(self.toPlainText()) - self.start_of_current_line for i in range(buffer_len): cursor.deletePreviousChar() self.setTextCursor(cursor) def replace_input_line(self, text): """ Replace the current input line with the passed in text. """ self.clear_input_line() self.append(text) def zoomIn(self, delta=2): """ Zoom in (increase) the size of the font by delta amount difference in point size upto 34 points. """ old_size = self.font().pointSize() new_size = old_size + delta if new_size <= 34: super().zoomIn(delta) def zoomOut(self, delta=2): """ Zoom out (decrease) the size of the font by delta amount difference in point size down to 4 points. """ old_size = self.font().pointSize() new_size = old_size - delta if new_size >= 4: super().zoomOut(delta) def set_theme(self, theme): """ Sets the theme / look for the REPL pane. """ if theme == 'day': self.setStyleSheet(DAY_STYLE) elif theme == 'night': self.setStyleSheet(NIGHT_STYLE) else: self.setStyleSheet(CONTRAST_STYLE)
class JobRunner(QObject, MooseWidget): """ Actually runs the process. It will read the output and translate any terminal color codes into html. It will also attempt to parse the output to check to see if we are at a new time step and emit the timestep_updated signal. Signals: started: Emitted when we start running. finished: Emitted when we are finished. Arguments are exit code and status message. outputAdded: Emitted when there is new output. timeStepUpdated: A new time step has started error: Emitted when an error is encountered. Arguments are QProcess code and error description """ started = pyqtSignal() finished = pyqtSignal(int, str) outputAdded = pyqtSignal(str) timeStepUpdated = pyqtSignal(int) error = pyqtSignal(int, str) def __init__(self, **kwds): super(JobRunner, self).__init__(**kwds) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.MergedChannels) self.process.readyReadStandardOutput.connect(self._readOutput) self.process.finished.connect(self._jobFinished) self.process.started.connect(self.started) self.process.error.connect(self._error) self._error_map = { QProcess.FailedToStart: "Failed to start", QProcess.Crashed: "Crashed", QProcess.Timedout: "Timedout", QProcess.WriteError: "Write error", QProcess.ReadError: "Read error", QProcess.UnknownError: "Unknown error", } self.killed = False self.setup() def run(self, cmd, args): """ Start the command. Arguments: cmd: The command to run args: A list of string arguments """ self.killed = False self._sendMessage("Running command: %s %s" % (cmd, ' '.join(args))) self._sendMessage("Working directory: %s" % os.getcwd()) self.process.start(cmd, args) self.process.waitForStarted() def _sendMessage(self, msg): mooseutils.mooseMessage(msg, color="MAGENTA") self.outputAdded.emit('<span style="color:magenta;">%s</span>' % msg) @pyqtSlot(QProcess.ProcessError) def _error(self, err): """ Slot called when the QProcess encounters an error. Inputs: err: One of the QProcess.ProcessError enums """ if not self.killed: msg = self._error_map.get(err, "Unknown error") self.error.emit(int(err), msg) mooseutils.mooseMessage(msg, color="RED") self.outputAdded.emit(msg) @pyqtSlot(int, QProcess.ExitStatus) def _jobFinished(self, code, status): """ Slot called when the QProcess is finished. Inputs: code: Exit code of the process. status: QProcess.ExitStatus """ exit_status = "Finished" if status != QProcess.NormalExit: if self.killed: exit_status = "Killed by user" else: exit_status = "Crashed" self.finished.emit(code, exit_status) self._sendMessage("%s: Exit code: %s" % (exit_status, code)) def kill(self): """ Kills the QProcess """ self.killed = True mooseutils.mooseMessage("Killing") self.process.kill() self.process.waitForFinished() @pyqtSlot() def _readOutput(self): """ Slot called when the QProcess produces output. """ lines = [] while self.process.canReadLine(): tmp = self.process.readLine().data().decode("utf-8").rstrip() lines.append(TerminalUtils.terminalOutputToHtml(tmp)) match = re.search(r'Time\sStep\s*([0-9]{1,})', tmp) if match: ts = int(match.group(1)) self.timeStepUpdated.emit(ts) output = '<pre style="display: inline; margin: 0;">%s</pre>' % '\n'.join(lines) self.outputAdded.emit(output) def isRunning(self): return self.process.state() == QProcess.Running
class HgDiffDialog(QWidget, Ui_HgDiffDialog): """ Class implementing a dialog to show the output of the hg diff command process. """ def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent parent widget (QWidget) """ super(HgDiffDialog, self).__init__(parent) self.setupUi(self) self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.process = QProcess() self.vcs = vcs self.__hgClient = self.vcs.getClient() font = Preferences.getEditorOtherFonts("MonospacedFont") self.contents.setFontFamily(font.family()) self.contents.setFontPointSize(font.pointSize()) self.cNormalFormat = self.contents.currentCharFormat() self.cAddedFormat = self.contents.currentCharFormat() self.cAddedFormat.setBackground(QBrush(QColor(190, 237, 190))) self.cRemovedFormat = self.contents.currentCharFormat() self.cRemovedFormat.setBackground(QBrush(QColor(237, 190, 190))) self.cLineNoFormat = self.contents.currentCharFormat() self.cLineNoFormat.setBackground(QBrush(QColor(255, 220, 168))) self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.__hgClient: if self.__hgClient.isExecuting(): self.__hgClient.cancel() else: if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def __getVersionArg(self, version): """ Private method to get a hg revision argument for the given revision. @param version revision (integer or string) @return version argument (string) """ if version == "WORKING": return None else: return str(version) def start(self, fn, versions=None, bundle=None, qdiff=False): """ Public slot to start the hg diff command. @param fn filename to be diffed (string) @param versions list of versions to be diffed (list of up to 2 strings or None) @param bundle name of a bundle file (string) @param qdiff flag indicating qdiff command shall be used (boolean) """ self.errorGroup.hide() self.inputGroup.show() self.intercept = False self.filename = fn self.contents.clear() self.paras = 0 self.filesCombo.clear() if qdiff: args = self.vcs.initCommand("qdiff") self.setWindowTitle(self.tr("Patch Contents")) else: args = self.vcs.initCommand("diff") if self.vcs.hasSubrepositories(): args.append("--subrepos") if bundle: args.append('--repository') args.append(bundle) elif self.vcs.bundleFile and os.path.exists(self.vcs.bundleFile): args.append('--repository') args.append(self.vcs.bundleFile) if versions is not None: self.raise_() self.activateWindow() rev1 = self.__getVersionArg(versions[0]) rev2 = None if len(versions) == 2: rev2 = self.__getVersionArg(versions[1]) if rev1 is not None or rev2 is not None: args.append('-r') if rev1 is not None and rev2 is not None: args.append('{0}:{1}'.format(rev1, rev2)) elif rev2 is None: args.append(rev1) elif rev1 is None: args.append(':{0}'.format(rev2)) if isinstance(fn, list): dname, fnames = self.vcs.splitPathList(fn) self.vcs.addArguments(args, fn) else: dname, fname = self.vcs.splitPath(fn) args.append(fn) self.__oldFile = "" self.__oldFileLine = -1 self.__fileSeparators = [] QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) if self.__hgClient: self.inputGroup.setEnabled(False) self.inputGroup.hide() out, err = self.__hgClient.runcommand(args) if err: self.__showError(err) if out: for line in out.splitlines(True): self.__processOutputLine(line) if self.__hgClient.wasCanceled(): break self.__finish() else: # find the root of the repo repodir = dname while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)): repodir = os.path.dirname(repodir) if os.path.splitdrive(repodir)[1] == os.sep: return self.process.kill() self.process.setWorkingDirectory(repodir) self.process.start('hg', args) procStarted = self.process.waitForStarted(5000) if not procStarted: QApplication.restoreOverrideCursor() self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.').format('hg')) def __procFinished(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.__finish() def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ QApplication.restoreOverrideCursor() self.inputGroup.setEnabled(False) self.inputGroup.hide() if self.paras == 0: self.contents.setCurrentCharFormat(self.cNormalFormat) self.contents.setPlainText(self.tr('There is no difference.')) self.buttonBox.button(QDialogButtonBox.Save).setEnabled(self.paras > 0) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.buttonBox.button(QDialogButtonBox.Close).setFocus( Qt.OtherFocusReason) tc = self.contents.textCursor() tc.movePosition(QTextCursor.Start) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() self.filesCombo.addItem(self.tr("<Start>"), 0) self.filesCombo.addItem(self.tr("<End>"), -1) for oldFile, newFile, pos in sorted(self.__fileSeparators): if oldFile != newFile: self.filesCombo.addItem("{0}\n{1}".format(oldFile, newFile), pos) else: self.filesCombo.addItem(oldFile, pos) def __appendText(self, txt, format): """ Private method to append text to the end of the contents pane. @param txt text to insert (string) @param format text format to be used (QTextCharFormat) """ tc = self.contents.textCursor() tc.movePosition(QTextCursor.End) self.contents.setTextCursor(tc) self.contents.setCurrentCharFormat(format) self.contents.insertPlainText(txt) def __extractFileName(self, line): """ Private method to extract the file name out of a file separator line. @param line line to be processed (string) @return extracted file name (string) """ f = line.split(None, 1)[1] f = f.rsplit(None, 6)[0] f = f.split("/", 1)[1] return f def __processFileLine(self, line): """ Private slot to process a line giving the old/new file. @param line line to be processed (string) """ if line.startswith('---'): self.__oldFileLine = self.paras self.__oldFile = self.__extractFileName(line) else: self.__fileSeparators.append( (self.__oldFile, self.__extractFileName(line), self.__oldFileLine)) def __processOutputLine(self, line): """ Private method to process the lines of output. @param line output line to be processed (string) """ if line.startswith("--- ") or \ line.startswith("+++ "): self.__processFileLine(line) if line.startswith('+'): format = self.cAddedFormat elif line.startswith('-'): format = self.cRemovedFormat elif line.startswith('@@'): format = self.cLineNoFormat else: format = self.cNormalFormat self.__appendText(line, format) self.paras += 1 def __readStdout(self): """ Private slot to handle the readyReadStandardOutput signal. It reads the output of the process, formats it and inserts it into the contents pane. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): line = str(self.process.readLine(), self.vcs.getEncoding(), 'replace') self.__processOutputLine(line) def __readStderr(self): """ Private slot to handle the readyReadStandardError signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: s = str(self.process.readAllStandardError(), self.vcs.getEncoding(), 'replace') self.__showError(s) def __showError(self, out): """ Private slot to show some error. @param out error to be shown (string) """ self.errorGroup.show() self.errors.insertPlainText(out) self.errors.ensureCursorVisible() def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Save): self.on_saveButton_clicked() @pyqtSlot(int) def on_filesCombo_activated(self, index): """ Private slot to handle the selection of a file. @param index activated row (integer) """ para = self.filesCombo.itemData(index) if para == 0: tc = self.contents.textCursor() tc.movePosition(QTextCursor.Start) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() elif para == -1: tc = self.contents.textCursor() tc.movePosition(QTextCursor.End) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() else: # step 1: move cursor to end tc = self.contents.textCursor() tc.movePosition(QTextCursor.End) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() # step 2: move cursor to desired line tc = self.contents.textCursor() delta = tc.blockNumber() - para tc.movePosition(QTextCursor.PreviousBlock, QTextCursor.MoveAnchor, delta) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() @pyqtSlot() def on_saveButton_clicked(self): """ Private slot to handle the Save button press. It saves the diff shown in the dialog to a file in the local filesystem. """ if isinstance(self.filename, list): if len(self.filename) > 1: fname = self.vcs.splitPathList(self.filename)[0] else: dname, fname = self.vcs.splitPath(self.filename[0]) if fname != '.': fname = "{0}.diff".format(self.filename[0]) else: fname = dname else: fname = self.vcs.splitPath(self.filename)[0] fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( self, self.tr("Save Diff"), fname, self.tr("Patch Files (*.diff)"), None, E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) if not fname: return # user aborted ext = QFileInfo(fname).suffix() if not ext: ex = selectedFilter.split("(*")[1].split(")")[0] if ex: fname += ex if QFileInfo(fname).exists(): res = E5MessageBox.yesNo( self, self.tr("Save Diff"), self.tr("<p>The patch file <b>{0}</b> already exists." " Overwrite it?</p>").format(fname), icon=E5MessageBox.Warning) if not res: return fname = Utilities.toNativeSeparators(fname) eol = e5App().getObject("Project").getEolString() try: f = open(fname, "w", encoding="utf-8", newline="") f.write(eol.join(self.contents.toPlainText().splitlines())) f.close() except IOError as why: E5MessageBox.critical( self, self.tr('Save Diff'), self.tr('<p>The patch file <b>{0}</b> could not be saved.' '<br>Reason: {1}</p>').format(fname, str(why))) def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the subversion process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(HgDiffDialog, self).keyPressEvent(evt)
class MainWindow(QMainWindow): """Voice Changer main window.""" def __init__(self, parent=None): super(MainWindow, self).__init__() self.statusBar().showMessage("Move Dial to Deform Microphone Voice !.") self.setWindowTitle(__doc__) self.setMinimumSize(240, 240) self.setMaximumSize(480, 480) self.resize(self.minimumSize()) self.setWindowIcon(QIcon.fromTheme("audio-input-microphone")) self.tray = QSystemTrayIcon(self) self.center() QShortcut("Ctrl+q", self, activated=lambda: self.close()) self.menuBar().addMenu("&File").addAction("Quit", lambda: exit()) self.menuBar().addMenu("Sound").addAction( "STOP !", lambda: call('killall rec', shell=True)) windowMenu = self.menuBar().addMenu("&Window") windowMenu.addAction("Hide", lambda: self.hide()) windowMenu.addAction("Minimize", lambda: self.showMinimized()) windowMenu.addAction("Maximize", lambda: self.showMaximized()) windowMenu.addAction("Restore", lambda: self.showNormal()) windowMenu.addAction("FullScreen", lambda: self.showFullScreen()) windowMenu.addAction("Center", lambda: self.center()) windowMenu.addAction("Top-Left", lambda: self.move(0, 0)) windowMenu.addAction("To Mouse", lambda: self.move_to_mouse_position()) # widgets group0 = QGroupBox("Voice Deformation") self.setCentralWidget(group0) self.process = QProcess(self) self.process.error.connect( lambda: self.statusBar().showMessage("Info: Process Killed", 5000)) self.control = QDial() self.control.setRange(-10, 20) self.control.setSingleStep(5) self.control.setValue(0) self.control.setCursor(QCursor(Qt.OpenHandCursor)) self.control.sliderPressed.connect( lambda: self.control.setCursor(QCursor(Qt.ClosedHandCursor))) self.control.sliderReleased.connect( lambda: self.control.setCursor(QCursor(Qt.OpenHandCursor))) self.control.valueChanged.connect( lambda: self.control.setToolTip(f"<b>{self.control.value()}")) self.control.valueChanged.connect( lambda: self.statusBar().showMessage( f"Voice deformation: {self.control.value()}", 5000)) self.control.valueChanged.connect(self.run) self.control.valueChanged.connect(lambda: self.process.kill()) # Graphic effect self.glow = QGraphicsDropShadowEffect(self) self.glow.setOffset(0) self.glow.setBlurRadius(99) self.glow.setColor(QColor(99, 255, 255)) self.control.setGraphicsEffect(self.glow) self.glow.setEnabled(False) # Timer to start self.slider_timer = QTimer(self) self.slider_timer.setSingleShot(True) self.slider_timer.timeout.connect(self.on_slider_timer_timeout) # an icon and set focus QLabel(self.control).setPixmap( QIcon.fromTheme("audio-input-microphone").pixmap(32)) self.control.setFocus() QVBoxLayout(group0).addWidget(self.control) self.menu = QMenu(__doc__) self.menu.addAction(__doc__).setDisabled(True) self.menu.setIcon(self.windowIcon()) self.menu.addSeparator() self.menu.addAction( "Show / Hide", lambda: self.hide() if self.isVisible() else self.showNormal()) self.menu.addAction("STOP !", lambda: call('killall rec', shell=True)) self.menu.addSeparator() self.menu.addAction("Quit", lambda: exit()) self.tray.setContextMenu(self.menu) self.make_trayicon() def run(self): """Run/Stop the QTimer.""" if self.slider_timer.isActive(): self.slider_timer.stop() self.glow.setEnabled(True) call('killall rec ; killall play', shell=True) self.slider_timer.start(3000) def on_slider_timer_timeout(self): """Run subprocess to deform voice.""" self.glow.setEnabled(False) value = int(self.control.value()) * 100 command = f'play -q -V0 "|rec -q -V0 -n -d -R riaa bend pitch {value} "' print(f"Voice Deformation Value: {value}") print(f"Voice Deformation Command: {command}") self.process.start(command) if self.isVisible(): self.statusBar().showMessage("Minimizing to System TrayIcon", 3000) print("Minimizing Main Window to System TrayIcon now...") sleep(3) self.hide() def center(self): """Center Window on the Current Screen,with Multi-Monitor support.""" window_geometry = self.frameGeometry() mousepointer_position = QApplication.desktop().cursor().pos() screen = QApplication.desktop().screenNumber(mousepointer_position) centerPoint = QApplication.desktop().screenGeometry(screen).center() window_geometry.moveCenter(centerPoint) self.move(window_geometry.topLeft()) def move_to_mouse_position(self): """Center the Window on the Current Mouse position.""" window_geometry = self.frameGeometry() window_geometry.moveCenter(QApplication.desktop().cursor().pos()) self.move(window_geometry.topLeft()) def make_trayicon(self): """Make a Tray Icon.""" if self.windowIcon() and __doc__: self.tray.setIcon(self.windowIcon()) self.tray.setToolTip(__doc__) self.tray.activated.connect( lambda: self.hide() if self.isVisible() else self.showNormal()) return self.tray.show()
class VideoPlayer(QObject): positionChanged = pyqtSignal(int) playbackChanged = pyqtSignal(bool) playerStopped = pyqtSignal() videoDataChanged = pyqtSignal(bool) # True if all data is available def __init__(self, window, parent = None): super().__init__(parent) self._executable = "mplayer" self._window = window self._filePath = None self._proc = QProcess(self) self._isPlaying = False self._data = VideoData() self._params = VideoParameters() # connect some signals self._proc.readyReadStandardOutput.connect(self._readStdout) self._proc.readyReadStandardError.connect(self._readStdErr) self._compileRegex() def _compileRegex(self): # bunch of used regex patterns. Note that sometimes it's not necessary to use regex to # parse MPlayer output self._commandLinePattern = re.compile(r""" V:\ *?(?P<time>\d+\.\d) # fallback time (always available). Note spaces after V: .* # anything between time and frame number \d+/\s*?(?P<frame>\d+)\ # frame number (not always available). Note that space is # the last character """, re.X) self._idVideoBitratePattern = re.compile(r"^ID_VIDEO_BITRATE=(?P<bitrate>\d+)") self._idWidthPattern = re.compile(r"^ID_VIDEO_WIDTH=(?P<width>\d+)") self._idHeightPattern= re.compile(r"^ID_VIDEO_HEIGHT=(?P<height>\d+)") self._idFpsPattern= re.compile(r"^ID_VIDEO_FPS=(?P<fps>[0-9.]+)") self._idLengthPattern= re.compile(r"ID_LENGTH=(?P<length>[0-9.]+)") def __del__(self): log.debug(_("Closing VideoPlayer")) self._kill() @property def isPlaying(self): return self._isPlaying @property def videoData(self): """Fetches meta data for loaded file""" return self._data def loadFile(self, filePath): """Loads a file""" self._filePath = filePath if self._proc.state() != QProcess.Running: self._kill() self._run(self._filePath) else: self._execute("pausing_keep_force pt_step 1") self._execute("get_property pause") self._execute("loadfile \"%s\"" % self._filePath) self._data.reset() self.videoDataChanged.emit(False) self._changePlayingState(True) def play(self): """Starts a playback""" if self._proc.state() == QProcess.Running: if self.isPlaying is False: self._execute("pause") self._changePlayingState(True) elif self._filePath is not None: self._kill() self._run(self._filePath) self._changePlayingState(True) def jump(self, timestamp): """Jumps to a given position in a movie""" self._execute("pausing_keep seek %.1f 2" % timestamp) def pause(self): """Pauses playback""" if self.isPlaying is True: self._execute("pause") self._changePlayingState(False) def stop(self): """Stops playback""" if self.isPlaying is True: self._execute("stop") self._changePlayingState(False) def seek(self, val): """Seeks +/- given seconds.miliseconds""" self._execute("pausing_keep seek %.1f 0" % val) def frameStep(self): self._execute("frame_step") def mute(self): """Mutes autio""" self._execute("mute") def setVolume(self, volume): """Changes volume""" val = float(val) cmd = "volume %s" % val self._execute(cmd) @property def path(self): return self._filePath def _run(self, filepath): arguments = [ "-slave", "-noautosub", "-identify", "-nomouseinput", "-osdlevel", "0", "-input", "nodefault-bindings", "-noconfig", "all", "-wid", str(int(self._window.winId())), filepath ] self._proc.start(self._executable, arguments) if self._proc.waitForStarted() is False: raise VideoPlayerException("Cannot start MPlayer!") def _kill(self): log.debug(_("Killing MPlayer process")) self._proc.kill() if self._proc.waitForFinished() is False: log.debug(_("Cannot kill MPlayer process (if you killed Subconvert this is not " "necessary an error)!")) def _execute(self, cmd): if self._proc.state() == QProcess.Running: if cmd is not None and cmd != "": procCmd = "%s%s" % (str(cmd), "\n") self._proc.write(procCmd.encode('utf-8')) def _changePlayingState(self, state): if state != self._isPlaying: self._isPlaying = state self.playbackChanged.emit(state) def _readStdout(self): message = bytes(self._proc.readAllStandardOutput()).decode() log.debug("stderr: %s", message) self._parseMessage(message) def _readStdErr(self): message = bytes(self._proc.readAllStandardError()).decode() log.debug("stdout: %s", message) self._parseMessage(message) def _parseMessage(self, message): lines = message.splitlines() for line in lines: if line.startswith("A:") or line.startswith("V:"): self._parsePositionChange(line) elif line.startswith("ID_"): self._parseMovieInfo(line) def _parsePositionChange(self, message): if self.videoData.isAllDataSet(): pos = self._commandLinePattern.search(message) if pos is not None: frame = int(pos.group("frame")) time = float(pos.group("time")) if frame == 0: frame = int(time * self.videoData.fps) self.positionChanged.emit(frame) def _parseMovieInfo(self, message): if message.startswith("ID_VIDEO_BITRATE"): bitrate = self._idVideoBitratePattern.search(message).group("bitrate") self._data.bitrate = int(bitrate) self._dataChanged() elif message.startswith("ID_VIDEO_WIDTH"): width = self._idWidthPattern.search(message).group("width") self._data.width = int(width) self._dataChanged() elif message.startswith("ID_VIDEO_HEIGHT"): height = self._idHeightPattern.search(message).group("height") self._data.height = int(height) self._dataChanged() elif message.startswith("ID_VIDEO_FPS"): fps = self._idFpsPattern.search(message).group("fps") self._data.fps = float(fps) self._dataChanged() elif message.startswith("ID_LENGTH"): length = self._idLengthPattern.search(message).group("length") self._data.length = float(length) self._dataChanged() elif message.startswith("ID_PAUSED"): self._changePlayingState(False) elif message.startswith("ID_EXIT"): self._changePlayingState(False) self.playerStopped.emit() def _dataChanged(self): if self._data.isAllDataSet(): self.videoDataChanged.emit(True)
class HgTagBranchListDialog(QDialog, Ui_HgTagBranchListDialog): """ Class implementing a dialog to show a list of tags or branches. """ def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent parent widget (QWidget) """ super(HgTagBranchListDialog, self).__init__(parent) self.setupUi(self) self.setWindowFlags(Qt.Window) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.vcs = vcs self.tagsList = None self.allTagsList = None self.__hgClient = vcs.getClient() self.tagList.headerItem().setText(self.tagList.columnCount(), "") self.tagList.header().setSortIndicator(3, Qt.AscendingOrder) if self.__hgClient: self.process = None else: self.process = QProcess() self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.show() QCoreApplication.processEvents() def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.__hgClient: if self.__hgClient.isExecuting(): self.__hgClient.cancel() else: if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def start(self, path, tags, tagsList, allTagsList): """ Public slot to start the tags command. @param path name of directory to be listed (string) @param tags flag indicating a list of tags is requested (False = branches, True = tags) @param tagsList reference to string list receiving the tags (list of strings) @param allTagsList reference to string list all tags (list of strings) """ self.errorGroup.hide() self.intercept = False self.tagsMode = tags if not tags: self.setWindowTitle(self.tr("Mercurial Branches List")) self.tagList.headerItem().setText(2, self.tr("Status")) self.activateWindow() self.tagsList = tagsList self.allTagsList = allTagsList dname, fname = self.vcs.splitPath(path) # find the root of the repo repodir = dname while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)): repodir = os.path.dirname(repodir) if os.path.splitdrive(repodir)[1] == os.sep: return if self.tagsMode: args = self.vcs.initCommand("tags") args.append('--verbose') else: args = self.vcs.initCommand("branches") args.append('--closed') if self.__hgClient: self.inputGroup.setEnabled(False) self.inputGroup.hide() out, err = self.__hgClient.runcommand(args) if err: self.__showError(err) if out: for line in out.splitlines(): self.__processOutputLine(line) if self.__hgClient.wasCanceled(): break self.__finish() else: self.process.kill() self.process.setWorkingDirectory(repodir) self.process.start('hg', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('hg')) else: self.inputGroup.setEnabled(True) self.inputGroup.show() def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.inputGroup.setEnabled(False) self.inputGroup.hide() self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.buttonBox.button(QDialogButtonBox.Close).setFocus( Qt.OtherFocusReason) self.__resizeColumns() self.__resort() def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): if self.__hgClient: self.__hgClient.cancel() else: self.__finish() def __procFinished(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.__finish() def __resort(self): """ Private method to resort the tree. """ self.tagList.sortItems( self.tagList.sortColumn(), self.tagList.header().sortIndicatorOrder()) def __resizeColumns(self): """ Private method to resize the list columns. """ self.tagList.header().resizeSections(QHeaderView.ResizeToContents) self.tagList.header().setStretchLastSection(True) def __generateItem(self, revision, changeset, status, name): """ Private method to generate a tag item in the tag list. @param revision revision of the tag/branch (string) @param changeset changeset of the tag/branch (string) @param status of the tag/branch (string) @param name name of the tag/branch (string) """ itm = QTreeWidgetItem(self.tagList) itm.setData(0, Qt.DisplayRole, int(revision)) itm.setData(1, Qt.DisplayRole, changeset) itm.setData(2, Qt.DisplayRole, status) itm.setData(3, Qt.DisplayRole, name) itm.setTextAlignment(0, Qt.AlignRight) itm.setTextAlignment(1, Qt.AlignRight) itm.setTextAlignment(2, Qt.AlignHCenter) def __readStdout(self): """ Private slot to handle the readyReadStdout signal. It reads the output of the process, formats it and inserts it into the contents pane. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): s = str(self.process.readLine(), self.vcs.getEncoding(), 'replace').strip() self.__processOutputLine(s) def __processOutputLine(self, line): """ Private method to process the lines of output. @param line output line to be processed (string) """ li = line.split() if li[-1][0] in "1234567890": # last element is a rev:changeset if self.tagsMode: status = "" else: status = self.tr("active") rev, changeset = li[-1].split(":", 1) del li[-1] else: if self.tagsMode: status = self.tr("yes") else: status = li[-1][1:-1] rev, changeset = li[-2].split(":", 1) del li[-2:] name = " ".join(li) self.__generateItem(rev, changeset, status, name) if name not in ["tip", "default"]: if self.tagsList is not None: self.tagsList.append(name) if self.allTagsList is not None: self.allTagsList.append(name) def __readStderr(self): """ Private slot to handle the readyReadStderr signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: s = str(self.process.readAllStandardError(), self.vcs.getEncoding(), 'replace') self.__showError(s) def __showError(self, out): """ Private slot to show some error. @param out error to be shown (string) """ self.errorGroup.show() self.errors.insertPlainText(out) self.errors.ensureCursorVisible() def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the subversion process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(HgTagBranchListDialog, self).keyPressEvent(evt)
class HgDiffDialog(QWidget, Ui_HgDiffDialog): """ Class implementing a dialog to show the output of the hg diff command process. """ def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent parent widget (QWidget) """ super(HgDiffDialog, self).__init__(parent) self.setupUi(self) self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.process = QProcess() self.vcs = vcs self.__hgClient = self.vcs.getClient() font = Preferences.getEditorOtherFonts("MonospacedFont") self.contents.setFontFamily(font.family()) self.contents.setFontPointSize(font.pointSize()) self.cNormalFormat = self.contents.currentCharFormat() self.cAddedFormat = self.contents.currentCharFormat() self.cAddedFormat.setBackground(QBrush(QColor(190, 237, 190))) self.cRemovedFormat = self.contents.currentCharFormat() self.cRemovedFormat.setBackground(QBrush(QColor(237, 190, 190))) self.cLineNoFormat = self.contents.currentCharFormat() self.cLineNoFormat.setBackground(QBrush(QColor(255, 220, 168))) self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.__hgClient: if self.__hgClient.isExecuting(): self.__hgClient.cancel() else: if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def __getVersionArg(self, version): """ Private method to get a hg revision argument for the given revision. @param version revision (integer or string) @return version argument (string) """ if version == "WORKING": return None else: return str(version) def start(self, fn, versions=None, bundle=None, qdiff=False): """ Public slot to start the hg diff command. @param fn filename to be diffed (string) @param versions list of versions to be diffed (list of up to 2 strings or None) @param bundle name of a bundle file (string) @param qdiff flag indicating qdiff command shall be used (boolean) """ self.errorGroup.hide() self.inputGroup.show() self.intercept = False self.filename = fn self.contents.clear() self.paras = 0 self.filesCombo.clear() if qdiff: args = self.vcs.initCommand("qdiff") self.setWindowTitle(self.tr("Patch Contents")) else: args = self.vcs.initCommand("diff") if self.vcs.hasSubrepositories(): args.append("--subrepos") if bundle: args.append('--repository') args.append(bundle) elif self.vcs.bundleFile and os.path.exists(self.vcs.bundleFile): args.append('--repository') args.append(self.vcs.bundleFile) if versions is not None: self.raise_() self.activateWindow() rev1 = self.__getVersionArg(versions[0]) rev2 = None if len(versions) == 2: rev2 = self.__getVersionArg(versions[1]) if rev1 is not None or rev2 is not None: args.append('-r') if rev1 is not None and rev2 is not None: args.append('{0}:{1}'.format(rev1, rev2)) elif rev2 is None: args.append(rev1) elif rev1 is None: args.append(':{0}'.format(rev2)) if isinstance(fn, list): dname, fnames = self.vcs.splitPathList(fn) self.vcs.addArguments(args, fn) else: dname, fname = self.vcs.splitPath(fn) args.append(fn) self.__oldFile = "" self.__oldFileLine = -1 self.__fileSeparators = [] QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) if self.__hgClient: self.inputGroup.setEnabled(False) self.inputGroup.hide() out, err = self.__hgClient.runcommand(args) if err: self.__showError(err) if out: for line in out.splitlines(True): self.__processOutputLine(line) if self.__hgClient.wasCanceled(): break self.__finish() else: # find the root of the repo repodir = dname while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)): repodir = os.path.dirname(repodir) if os.path.splitdrive(repodir)[1] == os.sep: return self.process.kill() self.process.setWorkingDirectory(repodir) self.process.start('hg', args) procStarted = self.process.waitForStarted(5000) if not procStarted: QApplication.restoreOverrideCursor() self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('hg')) def __procFinished(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.__finish() def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ QApplication.restoreOverrideCursor() self.inputGroup.setEnabled(False) self.inputGroup.hide() if self.paras == 0: self.contents.insertPlainText( self.tr('There is no difference.')) return self.buttonBox.button(QDialogButtonBox.Save).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.buttonBox.button(QDialogButtonBox.Close).setFocus( Qt.OtherFocusReason) tc = self.contents.textCursor() tc.movePosition(QTextCursor.Start) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() self.filesCombo.addItem(self.tr("<Start>"), 0) self.filesCombo.addItem(self.tr("<End>"), -1) for oldFile, newFile, pos in sorted(self.__fileSeparators): if oldFile != newFile: self.filesCombo.addItem( "{0}\n{1}".format(oldFile, newFile), pos) else: self.filesCombo.addItem(oldFile, pos) def __appendText(self, txt, format): """ Private method to append text to the end of the contents pane. @param txt text to insert (string) @param format text format to be used (QTextCharFormat) """ tc = self.contents.textCursor() tc.movePosition(QTextCursor.End) self.contents.setTextCursor(tc) self.contents.setCurrentCharFormat(format) self.contents.insertPlainText(txt) def __extractFileName(self, line): """ Private method to extract the file name out of a file separator line. @param line line to be processed (string) @return extracted file name (string) """ f = line.split(None, 1)[1] f = f.rsplit(None, 6)[0] f = f.split("/", 1)[1] return f def __processFileLine(self, line): """ Private slot to process a line giving the old/new file. @param line line to be processed (string) """ if line.startswith('---'): self.__oldFileLine = self.paras self.__oldFile = self.__extractFileName(line) else: self.__fileSeparators.append( (self.__oldFile, self.__extractFileName(line), self.__oldFileLine)) def __processOutputLine(self, line): """ Private method to process the lines of output. @param line output line to be processed (string) """ if line.startswith("--- ") or \ line.startswith("+++ "): self.__processFileLine(line) if line.startswith('+'): format = self.cAddedFormat elif line.startswith('-'): format = self.cRemovedFormat elif line.startswith('@@'): format = self.cLineNoFormat else: format = self.cNormalFormat self.__appendText(line, format) self.paras += 1 def __readStdout(self): """ Private slot to handle the readyReadStandardOutput signal. It reads the output of the process, formats it and inserts it into the contents pane. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): line = str(self.process.readLine(), self.vcs.getEncoding(), 'replace') self.__processOutputLine(line) def __readStderr(self): """ Private slot to handle the readyReadStandardError signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: s = str(self.process.readAllStandardError(), self.vcs.getEncoding(), 'replace') self.__showError(s) def __showError(self, out): """ Private slot to show some error. @param out error to be shown (string) """ self.errorGroup.show() self.errors.insertPlainText(out) self.errors.ensureCursorVisible() def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Save): self.on_saveButton_clicked() @pyqtSlot(int) def on_filesCombo_activated(self, index): """ Private slot to handle the selection of a file. @param index activated row (integer) """ para = self.filesCombo.itemData(index) if para == 0: tc = self.contents.textCursor() tc.movePosition(QTextCursor.Start) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() elif para == -1: tc = self.contents.textCursor() tc.movePosition(QTextCursor.End) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() else: # step 1: move cursor to end tc = self.contents.textCursor() tc.movePosition(QTextCursor.End) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() # step 2: move cursor to desired line tc = self.contents.textCursor() delta = tc.blockNumber() - para tc.movePosition(QTextCursor.PreviousBlock, QTextCursor.MoveAnchor, delta) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() @pyqtSlot() def on_saveButton_clicked(self): """ Private slot to handle the Save button press. It saves the diff shown in the dialog to a file in the local filesystem. """ if isinstance(self.filename, list): if len(self.filename) > 1: fname = self.vcs.splitPathList(self.filename)[0] else: dname, fname = self.vcs.splitPath(self.filename[0]) if fname != '.': fname = "{0}.diff".format(self.filename[0]) else: fname = dname else: fname = self.vcs.splitPath(self.filename)[0] fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( self, self.tr("Save Diff"), fname, self.tr("Patch Files (*.diff)"), None, E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) if not fname: return # user aborted ext = QFileInfo(fname).suffix() if not ext: ex = selectedFilter.split("(*")[1].split(")")[0] if ex: fname += ex if QFileInfo(fname).exists(): res = E5MessageBox.yesNo( self, self.tr("Save Diff"), self.tr("<p>The patch file <b>{0}</b> already exists." " Overwrite it?</p>").format(fname), icon=E5MessageBox.Warning) if not res: return fname = Utilities.toNativeSeparators(fname) eol = e5App().getObject("Project").getEolString() try: f = open(fname, "w", encoding="utf-8", newline="") f.write(eol.join(self.contents.toPlainText().splitlines())) f.close() except IOError as why: E5MessageBox.critical( self, self.tr('Save Diff'), self.tr( '<p>The patch file <b>{0}</b> could not be saved.' '<br>Reason: {1}</p>') .format(fname, str(why))) def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the subversion process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(HgDiffDialog, self).keyPressEvent(evt)
def _performMonitor(self): """ Protected method implementing the monitoring action. This method populates the statusList member variable with a list of strings giving the status in the first column and the path relative to the project directory starting with the third column. The allowed status flags are: <ul> <li>"A" path was added but not yet comitted</li> <li>"M" path has local changes</li> <li>"O" path was removed</li> <li>"R" path was deleted and then re-added</li> <li>"U" path needs an update</li> <li>"Z" path contains a conflict</li> <li>" " path is back at normal</li> </ul> @return tuple of flag indicating successful operation (boolean) and a status message in case of non successful operation (string) """ self.shouldUpdate = False if self.__client is None and not self.__useCommandLine: if self.vcs.version >= (2, 9, 9): # versions below that have a bug causing a second # instance to not recognize changes to the status from .HgClient import HgClient client = HgClient(self.projectDir, "utf-8", self.vcs) ok, err = client.startServer() if ok: self.__client = client else: self.__useCommandLine = True else: self.__useCommandLine = True # step 1: get overall status args = self.vcs.initCommand("status") args.append('--noninteractive') args.append('--all') output = "" error = "" if self.__client: output, error = self.__client.runcommand(args) else: process = QProcess() process.setWorkingDirectory(self.projectDir) process.start('hg', args) procStarted = process.waitForStarted(5000) if procStarted: finished = process.waitForFinished(300000) if finished and process.exitCode() == 0: output = str(process.readAllStandardOutput(), self.vcs.getEncoding(), 'replace') else: process.kill() process.waitForFinished() error = str(process.readAllStandardError(), self.vcs.getEncoding(), 'replace') else: process.kill() process.waitForFinished() error = self.tr("Could not start the Mercurial process.") if error: return False, error states = {} for line in output.splitlines(): if not line.startswith(" "): flag, name = line.split(" ", 1) if flag in "AMR": if flag == "R": status = "O" else: status = flag states[name] = status # step 2: get conflicting changes args = self.vcs.initCommand("resolve") args.append('--list') output = "" error = "" if self.__client: output, error = self.__client.runcommand(args) else: process.setWorkingDirectory(self.projectDir) process.start('hg', args) procStarted = process.waitForStarted(5000) if procStarted: finished = process.waitForFinished(300000) if finished and process.exitCode() == 0: output = str(process.readAllStandardOutput(), self.vcs.getEncoding(), 'replace') for line in output.splitlines(): flag, name = line.split(" ", 1) if flag == "U": states[name] = "Z" # conflict # step 3: collect the status to be reported back for name in states: try: if self.reportedStates[name] != states[name]: self.statusList.append("{0} {1}".format( states[name], name)) except KeyError: self.statusList.append("{0} {1}".format(states[name], name)) for name in self.reportedStates.keys(): if name not in states: self.statusList.append(" {0}".format(name)) self.reportedStates = states return True, \ self.tr("Mercurial status checked successfully")
def _performMonitor(self): """ Protected method implementing the monitoring action. This method populates the statusList member variable with a list of strings giving the status in the first column and the path relative to the project directory starting with the third column. The allowed status flags are: <ul> <li>"A" path was added but not yet comitted</li> <li>"M" path has local changes</li> <li>"O" path was removed</li> <li>"R" path was deleted and then re-added</li> <li>"U" path needs an update</li> <li>"Z" path contains a conflict</li> <li>" " path is back at normal</li> </ul> @return tuple of flag indicating successful operation (boolean) and a status message in case of non successful operation (string) """ self.shouldUpdate = False process = QProcess() args = [] args.append('status') if not Preferences.getVCS("MonitorLocalStatus"): args.append('--show-updates') args.append('--non-interactive') args.append('.') process.setWorkingDirectory(self.projectDir) process.start('svn', args) procStarted = process.waitForStarted(5000) if procStarted: finished = process.waitForFinished(300000) if finished and process.exitCode() == 0: output = str(process.readAllStandardOutput(), self.__ioEncoding, 'replace') states = {} for line in output.splitlines(): if self.rx_status1.exactMatch(line): flags = self.rx_status1.cap(1) path = self.rx_status1.cap(3).strip() elif self.rx_status2.exactMatch(line): flags = self.rx_status2.cap(1) path = self.rx_status2.cap(5).strip() else: continue if flags[0] in "ACDMR" or \ (flags[0] == " " and flags[-1] == "*"): if flags[-1] == "*": status = "U" else: status = flags[0] if status == "C": status = "Z" # give it highest priority elif status == "D": status = "O" if status == "U": self.shouldUpdate = True name = path states[name] = status try: if self.reportedStates[name] != status: self.statusList.append( "{0} {1}".format(status, name)) except KeyError: self.statusList.append( "{0} {1}".format(status, name)) for name in list(self.reportedStates.keys()): if name not in states: self.statusList.append(" {0}".format(name)) self.reportedStates = states return True, self.tr( "Subversion status checked successfully (using svn)") else: process.kill() process.waitForFinished() return False, \ str(process.readAllStandardError(), Preferences.getSystem("IOEncoding"), 'replace') else: process.kill() process.waitForFinished() return False, self.tr( "Could not start the Subversion process.")
class JobRunner(QObject, MooseWidget): """ Actually runs the process. It will read the output and translate any terminal color codes into html. It will also attempt to parse the output to check to see if we are at a new time step and emit the timestep_updated signal. Signals: started: Emitted when we start running. finished: Emitted when we are finished. Arguments are exit code and status message. outputAdded: Emitted when there is new output. timeStepUpdated: A new time step has started error: Emitted when an error is encountered. Arguments are QProcess code and error description """ started = pyqtSignal() finished = pyqtSignal(int, str) outputAdded = pyqtSignal(str) timeStepUpdated = pyqtSignal(int) error = pyqtSignal(int, str) def __init__(self, **kwds): super(JobRunner, self).__init__(**kwds) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.MergedChannels) self.process.readyReadStandardOutput.connect(self._readOutput) self.process.finished.connect(self._jobFinished) self.process.started.connect(self.started) self.process.error.connect(self._error) self._error_map = { QProcess.FailedToStart: "Failed to start", QProcess.Crashed: "Crashed", QProcess.Timedout: "Timedout", QProcess.WriteError: "Write error", QProcess.ReadError: "Read error", QProcess.UnknownError: "Unknown error", } self.killed = False self.setup() def run(self, cmd, args): """ Start the command. Arguments: cmd: The command to run args: A list of string arguments """ self.killed = False self._sendMessage("Running command: %s %s" % (cmd, ' '.join(args))) self._sendMessage("Working directory: %s" % os.getcwd()) self.process.start(cmd, args) self.process.waitForStarted() def _sendMessage(self, msg): mooseutils.mooseMessage(msg, color="MAGENTA") self.outputAdded.emit('<span style="color:magenta;">%s</span>' % msg) @pyqtSlot(QProcess.ProcessError) def _error(self, err): """ Slot called when the QProcess encounters an error. Inputs: err: One of the QProcess.ProcessError enums """ if not self.killed: msg = self._error_map.get(err, "Unknown error") self.error.emit(int(err), msg) mooseutils.mooseMessage(msg, color="RED") self.outputAdded.emit(msg) @pyqtSlot(int, QProcess.ExitStatus) def _jobFinished(self, code, status): """ Slot called when the QProcess is finished. Inputs: code: Exit code of the process. status: QProcess.ExitStatus """ exit_status = "Finished" if status != QProcess.NormalExit: if self.killed: exit_status = "Killed by user" else: exit_status = "Crashed" self.finished.emit(code, exit_status) self._sendMessage("%s: Exit code: %s" % (exit_status, code)) def kill(self): """ Kills the QProcess """ self.killed = True mooseutils.mooseMessage("Killing") self.process.terminate() self.process.waitForFinished(1000) if self.isRunning(): mooseutils.mooseMessage("Failed to terminate job cleanly. Doing a hard kill.") self.process.kill() self.process.waitForFinished() @pyqtSlot() def _readOutput(self): """ Slot called when the QProcess produces output. """ lines = [] while self.process.canReadLine(): tmp = self.process.readLine().data().decode("utf-8").rstrip() lines.append(TerminalUtils.terminalOutputToHtml(tmp)) match = re.search(r'Time\sStep\s*([0-9]{1,})', tmp) if match: ts = int(match.group(1)) self.timeStepUpdated.emit(ts) output = '<pre style="display: inline; margin: 0;">%s</pre>' % '\n'.join(lines) self.outputAdded.emit(output) def isRunning(self): return self.process.state() == QProcess.Running
class HgConflictsListDialog(QWidget, Ui_HgConflictsListDialog): """ Class implementing a dialog to show a list of files which had or still have conflicts. """ StatusRole = Qt.UserRole + 1 FilenameRole = Qt.UserRole + 2 def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent parent widget (QWidget) """ super(HgConflictsListDialog, self).__init__(parent) self.setupUi(self) self.__position = QPoint() self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.conflictsList.headerItem().setText( self.conflictsList.columnCount(), "") self.conflictsList.header().setSortIndicator(0, Qt.AscendingOrder) self.refreshButton = self.buttonBox.addButton( self.tr("&Refresh"), QDialogButtonBox.ActionRole) self.refreshButton.setToolTip( self.tr("Press to refresh the list of conflicts")) self.refreshButton.setEnabled(False) self.vcs = vcs self.project = e5App().getObject("Project") self.__hgClient = vcs.getClient() if self.__hgClient: self.process = None else: self.process = QProcess() self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.__hgClient: if self.__hgClient.isExecuting(): self.__hgClient.cancel() else: if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.__position = self.pos() e.accept() def show(self): """ Public slot to show the dialog. """ if not self.__position.isNull(): self.move(self.__position) super(HgConflictsListDialog, self).show() def start(self, path): """ Public slot to start the tags command. @param path name of directory to list conflicts for (string) """ self.errorGroup.hide() QApplication.processEvents() self.intercept = False dname, fname = self.vcs.splitPath(path) # find the root of the repo self.__repodir = dname while not os.path.isdir( os.path.join(self.__repodir, self.vcs.adminDir)): self.__repodir = os.path.dirname(self.__repodir) if os.path.splitdrive(self.__repodir)[1] == os.sep: return self.activateWindow() self.raise_() self.conflictsList.clear() self.__started = True self.__getEntries() def __getEntries(self): """ Private method to get the conflict entries. """ args = self.vcs.initCommand("resolve") args.append('--list') if self.__hgClient: self.inputGroup.setEnabled(False) self.inputGroup.hide() out, err = self.__hgClient.runcommand(args) if err: self.__showError(err) if out: for line in out.splitlines(): self.__processOutputLine(line) if self.__hgClient.wasCanceled(): break self.__finish() else: self.process.kill() self.process.setWorkingDirectory(self.__repodir) self.process.start('hg', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('hg')) else: self.inputGroup.setEnabled(True) self.inputGroup.show() def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) QApplication.restoreOverrideCursor() self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.inputGroup.setEnabled(False) self.inputGroup.hide() self.refreshButton.setEnabled(True) self.__resizeColumns() self.__resort() self.on_conflictsList_itemSelectionChanged() @pyqtSlot(QAbstractButton) def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): if self.__hgClient: self.__hgClient.cancel() else: self.__finish() elif button == self.refreshButton: self.on_refreshButton_clicked() def __procFinished(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.__finish() def __resort(self): """ Private method to resort the tree. """ self.conflictsList.sortItems( self.conflictsList.sortColumn(), self.conflictsList.header().sortIndicatorOrder()) def __resizeColumns(self): """ Private method to resize the list columns. """ self.conflictsList.header().resizeSections( QHeaderView.ResizeToContents) self.conflictsList.header().setStretchLastSection(True) def __generateItem(self, status, name): """ Private method to generate a tag item in the tag list. @param status status of the file (string) @param name name of the file (string) """ itm = QTreeWidgetItem(self.conflictsList) if status == "U": itm.setText(0, self.tr("Unresolved")) elif status == "R": itm.setText(0, self.tr("Resolved")) else: itm.setText(0, self.tr("Unknown Status")) itm.setText(1, name) itm.setData(0, self.StatusRole, status) itm.setData(0, self.FilenameRole, self.project.getAbsolutePath(name)) def __readStdout(self): """ Private slot to handle the readyReadStdout signal. It reads the output of the process, formats it and inserts it into the contents pane. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): s = str(self.process.readLine(), self.vcs.getEncoding(), 'replace').strip() self.__processOutputLine(s) def __processOutputLine(self, line): """ Private method to process the lines of output. @param line output line to be processed (string) """ status, filename = line.strip().split(None, 1) self.__generateItem(status, filename) @pyqtSlot() def on_refreshButton_clicked(self): """ Private slot to refresh the log. """ self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.inputGroup.setEnabled(True) self.inputGroup.show() self.refreshButton.setEnabled(False) self.start(self.__repodir) def __readStderr(self): """ Private slot to handle the readyReadStderr signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: s = str(self.process.readAllStandardError(), self.vcs.getEncoding(), 'replace') self.__showError(s) def __showError(self, out): """ Private slot to show some error. @param out error to be shown (string) """ self.errorGroup.show() self.errors.insertPlainText(out) self.errors.ensureCursorVisible() def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the subversion process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(HgConflictsListDialog, self).keyPressEvent(evt) @pyqtSlot(QTreeWidgetItem, int) def on_conflictsList_itemDoubleClicked(self, item, column): """ Private slot to open the double clicked entry. @param item reference to the double clicked item (QTreeWidgetItem) @param column column that was double clicked (integer) """ self.on_editButton_clicked() @pyqtSlot() def on_conflictsList_itemSelectionChanged(self): """ Private slot to handle a change of selected conflict entries. """ selectedCount = len(self.conflictsList.selectedItems()) unresolved = resolved = 0 for itm in self.conflictsList.selectedItems(): status = itm.data(0, self.StatusRole) if status == "U": unresolved += 1 elif status == "R": resolved += 1 self.resolvedButton.setEnabled(unresolved > 0) self.unresolvedButton.setEnabled(resolved > 0) self.reMergeButton.setEnabled(unresolved > 0) self.editButton.setEnabled( selectedCount == 1 and Utilities.MimeTypes.isTextFile( self.conflictsList.selectedItems()[0].data( 0, self.FilenameRole))) @pyqtSlot() def on_resolvedButton_clicked(self): """ Private slot to mark the selected entries as resolved. """ names = [ itm.data(0, self.FilenameRole) for itm in self.conflictsList.selectedItems() if itm.data(0, self.StatusRole) == "U" ] if names: self.vcs.hgResolved(names) self.on_refreshButton_clicked() @pyqtSlot() def on_unresolvedButton_clicked(self): """ Private slot to mark the selected entries as unresolved. """ names = [ itm.data(0, self.FilenameRole) for itm in self.conflictsList.selectedItems() if itm.data(0, self.StatusRole) == "R" ] if names: self.vcs.hgResolved(names, unresolve=True) self.on_refreshButton_clicked() @pyqtSlot() def on_reMergeButton_clicked(self): """ Private slot to re-merge the selected entries. """ names = [ itm.data(0, self.FilenameRole) for itm in self.conflictsList.selectedItems() if itm.data(0, self.StatusRole) == "U" ] if names: self.vcs.hgReMerge(names) @pyqtSlot() def on_editButton_clicked(self): """ Private slot to open the selected file in an editor. """ itm = self.conflictsList.selectedItems()[0] filename = itm.data(0, self.FilenameRole) if Utilities.MimeTypes.isTextFile(filename): e5App().getObject("ViewManager").getEditor(filename)
class LivePcapCaptureDataSource(DataSource): @staticmethod def getConfigFields(): return [("shell_cmd", "Shell command line", "text", { "default": "sudo tcpdump -w -" })] def startFetch(self): self.plist = ByteBufferList() self.packetFI = None self.ctx = structinfo.ParseContext() self.process = QProcess() self.process.finished.connect(self.onProcessFinished) self.process.readyReadStandardError.connect(self.onReadyReadStderr) self.process.readyReadStandardOutput.connect(self.onReadyReadStdout) self.process.start("/bin/sh", ["-c", self.params["shell_cmd"]]) return self.plist def tryParseHeader(self): for headerFI, packetFI in PcapVariants: try: header = headerFI.read_from_buffer(self.ctx) self.packetFI = packetFI self.plist.metadata.update(header) return except structinfo.invalid as ex: self.on_log.emit(str(ex)) pass raise structinfo.invalid(self.ctx, "no PcapVariant matched") def onReadyReadStderr(self): self.on_log.emit("STD-ERR:" + self.process.readAllStandardError().data().decode( "utf-8", "replace")) def onReadyReadStdout(self): self.ctx.feed_bytes(self.process.readAllStandardOutput()) try: if self.packetFI == None: self.tryParseHeader() while True: packet = self.packetFI.read_from_buffer(self.ctx) self.plist.add( ByteBuffer(packet['payload'], metadata=packet['header'])) except structinfo.incomplete: return except structinfo.invalid as ex: self.on_log.emit("Invalid packet format - killing pcap") self.on_log.emit(str(ex)) self.cancelFetch() def onProcessFinished(self, exitCode, exitStatus): self.on_finished.emit() def cancelFetch(self): self.process.terminate() self.process.waitForFinished(500) self.process.kill() pass
class HgQueuesListDialog(QDialog, Ui_HgQueuesListDialog): """ Class implementing a dialog to show a list of applied and unapplied patches. """ def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent parent widget (QWidget) """ super(HgQueuesListDialog, self).__init__(parent) self.setupUi(self) self.setWindowFlags(Qt.Window) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.vcs = vcs self.__hgClient = vcs.getClient() self.patchesList.header().setSortIndicator(0, Qt.AscendingOrder) if self.__hgClient: self.process = None else: self.process = QProcess() self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.__statusDict = { "A": self.tr("applied"), "U": self.tr("not applied"), "G": self.tr("guarded"), "D": self.tr("missing"), } self.show() QCoreApplication.processEvents() def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.__hgClient: if self.__hgClient.isExecuting(): self.__hgClient.cancel() else: if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def start(self, path): """ Public slot to start the list command. @param path name of directory to be listed (string) """ self.errorGroup.hide() self.intercept = False self.activateWindow() dname, fname = self.vcs.splitPath(path) # find the root of the repo repodir = dname while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)): repodir = os.path.dirname(repodir) if os.path.splitdrive(repodir)[1] == os.sep: return self.__repodir = repodir self.__getSeries() def __getSeries(self, missing=False): """ Private slot to get the list of applied, unapplied and guarded patches and patches missing in the series file. @param missing flag indicating to get the patches missing in the series file (boolean) """ if missing: self.__mode = "missing" else: self.__mode = "qseries" args = self.vcs.initCommand("qseries") args.append('--summary') args.append('--verbose') if missing: args.append('--missing') if self.__hgClient: self.inputGroup.setEnabled(False) self.inputGroup.hide() out, err = self.__hgClient.runcommand(args) if err: self.__showError(err) if out: for line in out.splitlines(): self.__processOutputLine(line) if self.__hgClient.wasCanceled(): self.__mode = "" break if self.__mode == "qseries": self.__getSeries(True) elif self.__mode == "missing": self.__getTop() else: self.__finish() else: self.process.kill() self.process.setWorkingDirectory(self.__repodir) self.process.start('hg', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('hg')) else: self.inputGroup.setEnabled(True) self.inputGroup.show() def __getTop(self): """ Private slot to get patch at the top of the stack. """ self.__mode = "qtop" args = self.vcs.initCommand("qtop") if self.__hgClient: self.inputGroup.setEnabled(False) self.inputGroup.hide() out, err = self.__hgClient.runcommand(args) if err: self.__showError(err) if out: for line in out.splitlines(): self.__processOutputLine(line) if self.__hgClient.wasCanceled(): break self.__finish() else: self.process.kill() self.process.setWorkingDirectory(self.__repodir) self.process.start('hg', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('hg')) else: self.inputGroup.setEnabled(True) self.inputGroup.show() def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.inputGroup.setEnabled(False) self.inputGroup.hide() self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.buttonBox.button(QDialogButtonBox.Close).setFocus( Qt.OtherFocusReason) if self.patchesList.topLevelItemCount() == 0: # no patches present self.__generateItem( 0, "", self.tr("no patches found"), "", True) self.__resizeColumns() self.__resort() def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): self.__mode = "" if self.__hgClient: self.__hgClient.cancel() else: self.__finish() def __procFinished(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) """ if self.__mode == "qseries": self.__getSeries(True) elif self.__mode == "missing": self.__getTop() else: self.__finish() def __resort(self): """ Private method to resort the tree. """ self.patchesList.sortItems( self.patchesList.sortColumn(), self.patchesList.header().sortIndicatorOrder()) def __resizeColumns(self): """ Private method to resize the list columns. """ self.patchesList.header().resizeSections(QHeaderView.ResizeToContents) self.patchesList.header().setStretchLastSection(True) def __generateItem(self, index, status, name, summary, error=False): """ Private method to generate a patch item in the list of patches. @param index index of the patch (integer, -1 for missing) @param status status of the patch (string) @param name name of the patch (string) @param summary first line of the patch header (string) @param error flag indicating an error entry (boolean) """ if error: itm = QTreeWidgetItem(self.patchesList, [ "", name, "", summary ]) else: if index == -1: index = "" try: statusStr = self.__statusDict[status] except KeyError: statusStr = self.tr("unknown") itm = QTreeWidgetItem(self.patchesList) itm.setData(0, Qt.DisplayRole, index) itm.setData(1, Qt.DisplayRole, name) itm.setData(2, Qt.DisplayRole, statusStr) itm.setData(3, Qt.DisplayRole, summary) if status == "A": # applied for column in range(itm.columnCount()): itm.setForeground(column, Qt.blue) elif status == "D": # missing for column in range(itm.columnCount()): itm.setForeground(column, Qt.red) itm.setTextAlignment(0, Qt.AlignRight) itm.setTextAlignment(2, Qt.AlignHCenter) def __markTopItem(self, name): """ Private slot to mark the top patch entry. @param name name of the patch (string) """ items = self.patchesList.findItems(name, Qt.MatchCaseSensitive, 1) if items: itm = items[0] for column in range(itm.columnCount()): font = itm.font(column) font.setBold(True) itm.setFont(column, font) def __readStdout(self): """ Private slot to handle the readyReadStdout signal. It reads the output of the process, formats it and inserts it into the contents pane. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): s = str(self.process.readLine(), self.vcs.getEncoding(), 'replace').strip() self.__processOutputLine(s) def __processOutputLine(self, line): """ Private method to process the lines of output. @param line output line to be processed (string) """ if self.__mode == "qtop": self.__markTopItem(line) else: li = line.split(": ", 1) if len(li) == 1: data, summary = li[0][:-1], "" else: data, summary = li[0], li[1] li = data.split(None, 2) if len(li) == 2: # missing entry index, status, name = -1, li[0], li[1] elif len(li) == 3: index, status, name = li[:3] else: return self.__generateItem(index, status, name, summary) def __readStderr(self): """ Private slot to handle the readyReadStderr signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: s = str(self.process.readAllStandardError(), self.vcs.getEncoding(), 'replace') self.__showError(s) def __showError(self, out): """ Private slot to show some error. @param out error to be shown (string) """ self.errorGroup.show() self.errors.insertPlainText(out) self.errors.ensureCursorVisible() def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the subversion process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(HgQueuesListDialog, self).keyPressEvent(evt)
class Process(QObject): """Abstraction over a running test subprocess process. Reads the log from its stdout and parses it. Attributes: _invalid: A list of lines which could not be parsed. _data: A list of parsed lines. _started: Whether the process was ever started. proc: The QProcess for the underlying process. exit_expected: Whether the process is expected to quit. request: The request object for the current test. Signals: ready: Emitted when the server finished starting up. new_data: Emitted when a new line was parsed. """ ready = pyqtSignal() new_data = pyqtSignal(object) KEYS = ['data'] def __init__(self, request, parent=None): super().__init__(parent) self.request = request self.captured_log = [] self._started = False self._invalid = [] self._data = [] self.proc = QProcess() self.proc.setReadChannel(QProcess.StandardError) self.exit_expected = None # Not started at all yet def _log(self, line): """Add the given line to the captured log output.""" if self.request.config.getoption('--capture') == 'no': print(line) self.captured_log.append(line) def log_summary(self, text): """Log the given line as summary/title.""" text = '\n{line} {text} {line}\n'.format(line='=' * 30, text=text) self._log(text) def _parse_line(self, line): """Parse the given line from the log. Return: A self.ParseResult member. """ raise NotImplementedError def _executable_args(self): """Get the executable and necessary arguments as a tuple.""" raise NotImplementedError def _default_args(self): """Get the default arguments to use if none were passed to start().""" raise NotImplementedError def _get_data(self): """Get the parsed data for this test. Also waits for 0.5s to make sure any new data is received. Subprocesses are expected to alias this to a public method with a better name. """ self.proc.waitForReadyRead(500) self.read_log() return self._data def _wait_signal(self, signal, timeout=5000, raising=True): """Wait for a signal to be emitted. Should be used in a contextmanager. """ blocker = pytestqt.plugin.SignalBlocker(timeout=timeout, raising=raising) blocker.connect(signal) return blocker @pyqtSlot() def read_log(self): """Read the log from the process' stdout.""" if not hasattr(self, 'proc'): # I have no idea how this happens, but it does... return while self.proc.canReadLine(): line = self.proc.readLine() line = bytes(line).decode('utf-8', errors='ignore').rstrip('\r\n') try: parsed = self._parse_line(line) except InvalidLine: self._invalid.append(line) self._log("INVALID: {}".format(line)) continue if parsed is None: if self._invalid: self._log("IGNORED: {}".format(line)) else: self._data.append(parsed) self.new_data.emit(parsed) def start(self, args=None, *, env=None): """Start the process and wait until it started.""" self._start(args, env=env) self._started = True verbose = self.request.config.getoption('--verbose') timeout = 60 if 'CI' in os.environ else 20 for _ in range(timeout): with self._wait_signal(self.ready, timeout=1000, raising=False) as blocker: pass if not self.is_running(): if self.exit_expected: return # _start ensures it actually started, but it might quit shortly # afterwards raise ProcessExited( '\n' + _render_log(self.captured_log, verbose=verbose)) if blocker.signal_triggered: self._after_start() return raise WaitForTimeout("Timed out while waiting for process start.\n" + _render_log(self.captured_log, verbose=verbose)) def _start(self, args, env): """Actually start the process.""" executable, exec_args = self._executable_args() if args is None: args = self._default_args() procenv = QProcessEnvironment.systemEnvironment() if env is not None: for k, v in env.items(): procenv.insert(k, v) self.proc.readyRead.connect(self.read_log) self.proc.setProcessEnvironment(procenv) self.proc.start(executable, exec_args + args) ok = self.proc.waitForStarted() assert ok assert self.is_running() def _after_start(self): """Do things which should be done immediately after starting.""" def before_test(self): """Restart process before a test if it exited before.""" self._invalid = [] if not self.is_running(): self.start() def after_test(self): """Clean up data after each test. Also checks self._invalid so the test counts as failed if there were unexpected output lines earlier. """ __tracebackhide__ = lambda e: e.errisinstance(ProcessExited) self.captured_log = [] if self._invalid: # Wait for a bit so the full error has a chance to arrive time.sleep(1) # Exit the process to make sure we're in a defined state again self.terminate() self.clear_data() raise InvalidLine('\n' + '\n'.join(self._invalid)) self.clear_data() if not self.is_running() and not self.exit_expected and self._started: raise ProcessExited self.exit_expected = False def clear_data(self): """Clear the collected data.""" self._data.clear() def terminate(self): """Clean up and shut down the process.""" if not self.is_running(): return if quteutils.is_windows: self.proc.kill() else: self.proc.terminate() ok = self.proc.waitForFinished() if not ok: self.proc.kill() self.proc.waitForFinished() def is_running(self): """Check if the process is currently running.""" return self.proc.state() == QProcess.Running def _match_data(self, value, expected): """Helper for wait_for to match a given value. The behavior of this method is slightly different depending on the types of the filtered values: - If expected is None, the filter always matches. - If the value is a string or bytes object and the expected value is too, the pattern is treated as a glob pattern (with only * active). - If the value is a string or bytes object and the expected value is a compiled regex, it is used for matching. - If the value is any other type, == is used. Return: A bool """ regex_type = type(re.compile('')) if expected is None: return True elif isinstance(expected, regex_type): return expected.search(value) elif isinstance(value, (bytes, str)): return utils.pattern_match(pattern=expected, value=value) else: return value == expected def _wait_for_existing(self, override_waited_for, after, **kwargs): """Check if there are any line in the history for wait_for. Return: either the found line or None. """ for line in self._data: matches = [] for key, expected in kwargs.items(): value = getattr(line, key) matches.append(self._match_data(value, expected)) if after is None: too_early = False else: too_early = ((line.timestamp, line.msecs) < (after.timestamp, after.msecs)) if (all(matches) and (not line.waited_for or override_waited_for) and not too_early): # If we waited for this line, chances are we don't mean the # same thing the next time we use wait_for and it matches # this line again. line.waited_for = True self._log("\n----> Already found {!r} in the log: {}".format( kwargs.get('message', 'line'), line)) return line return None def _wait_for_new(self, timeout, do_skip, **kwargs): """Wait for a log message which doesn't exist yet. Called via wait_for. """ __tracebackhide__ = lambda e: e.errisinstance(WaitForTimeout) message = kwargs.get('message', None) if message is not None: elided = quteutils.elide(repr(message), 100) self._log("\n----> Waiting for {} in the log".format(elided)) spy = QSignalSpy(self.new_data) elapsed_timer = QElapsedTimer() elapsed_timer.start() while True: # Skip if there are pending messages causing a skip self._maybe_skip() got_signal = spy.wait(timeout) if not got_signal or elapsed_timer.hasExpired(timeout): msg = "Timed out after {}ms waiting for {!r}.".format( timeout, kwargs) if do_skip: pytest.skip(msg) else: raise WaitForTimeout(msg) match = self._wait_for_match(spy, kwargs) if match is not None: if message is not None: self._log("----> found it") return match raise quteutils.Unreachable def _wait_for_match(self, spy, kwargs): """Try matching the kwargs with the given QSignalSpy.""" for args in spy: assert len(args) == 1 line = args[0] matches = [] for key, expected in kwargs.items(): value = getattr(line, key) matches.append(self._match_data(value, expected)) if all(matches): # If we waited for this line, chances are we don't mean the # same thing the next time we use wait_for and it matches # this line again. line.waited_for = True return line return None def _maybe_skip(self): """Can be overridden by subclasses to skip on certain log lines. We can't run pytest.skip directly while parsing the log, as that would lead to a pytest.skip.Exception error in a virtual Qt method, which means pytest-qt fails the test. Instead, we check for skip messages periodically in QuteProc._maybe_skip, and call _maybe_skip after every parsed message in wait_for (where it's most likely that new messages arrive). """ def wait_for(self, timeout=None, *, override_waited_for=False, do_skip=False, divisor=1, after=None, **kwargs): """Wait until a given value is found in the data. Keyword arguments to this function get interpreted as attributes of the searched data. Every given argument is treated as a pattern which the attribute has to match against. Args: timeout: How long to wait for the message. override_waited_for: If set, gets triggered by previous messages again. do_skip: If set, call pytest.skip on a timeout. divisor: A factor to decrease the timeout by. after: If it's an existing line, ensure it's after the given one. Return: The matched line. """ __tracebackhide__ = lambda e: e.errisinstance(WaitForTimeout) if timeout is None: if do_skip: timeout = 2000 elif 'CI' in os.environ: timeout = 15000 else: timeout = 5000 timeout //= divisor if not kwargs: raise TypeError("No keyword arguments given!") for key in kwargs: assert key in self.KEYS existing = self._wait_for_existing(override_waited_for, after, **kwargs) if existing is not None: return existing else: return self._wait_for_new(timeout=timeout, do_skip=do_skip, **kwargs) def ensure_not_logged(self, delay=500, **kwargs): """Make sure the data matching the given arguments is not logged. If nothing is found in the log, we wait for delay ms to make sure nothing arrives. """ __tracebackhide__ = lambda e: e.errisinstance(BlacklistedMessageError) try: line = self.wait_for(timeout=delay, override_waited_for=True, **kwargs) except WaitForTimeout: return else: raise BlacklistedMessageError(line) def wait_for_quit(self): """Wait until the process has quit.""" self.exit_expected = True with self._wait_signal(self.proc.finished, timeout=15000): pass
class HgAnnotateDialog(QDialog, Ui_HgAnnotateDialog): """ Class implementing a dialog to show the output of the hg annotate command. """ def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent parent widget (QWidget) """ super(HgAnnotateDialog, self).__init__(parent) self.setupUi(self) self.setWindowFlags(Qt.Window) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.vcs = vcs self.__hgClient = vcs.getClient() self.__annotateRe = re.compile( r"""(.+)\s+(\d+)\s+([0-9a-fA-F]+)\s+([0-9-]+)\s+(.+)""") self.annotateList.headerItem().setText( self.annotateList.columnCount(), "") font = Preferences.getEditorOtherFonts("MonospacedFont") self.annotateList.setFont(font) if self.__hgClient: self.process = None else: self.process = QProcess() self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.show() QCoreApplication.processEvents() def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.__hgClient: if self.__hgClient.isExecuting(): self.__hgClient.cancel() else: if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def start(self, fn): """ Public slot to start the annotate command. @param fn filename to show the annotation for (string) """ self.annotateList.clear() self.errorGroup.hide() self.intercept = False self.activateWindow() self.lineno = 1 dname, fname = self.vcs.splitPath(fn) # find the root of the repo repodir = dname while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)): repodir = os.path.dirname(repodir) if os.path.splitdrive(repodir)[1] == os.sep: return args = self.vcs.initCommand("annotate") args.append('--follow') args.append('--user') args.append('--date') args.append('--number') args.append('--changeset') args.append('--quiet') args.append(fn) if self.__hgClient: self.inputGroup.setEnabled(False) self.inputGroup.hide() out, err = self.__hgClient.runcommand(args) if err: self.__showError(err) if out: for line in out.splitlines(): self.__processOutputLine(line) if self.__hgClient.wasCanceled(): break self.__finish() else: self.process.kill() self.process.setWorkingDirectory(repodir) self.process.start('hg', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('hg')) else: self.inputGroup.setEnabled(True) self.inputGroup.show() def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.inputGroup.setEnabled(False) self.inputGroup.hide() self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.buttonBox.button(QDialogButtonBox.Close).setFocus( Qt.OtherFocusReason) self.__resizeColumns() def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): if self.__hgClient: self.__hgClient.cancel() else: self.__finish() def __procFinished(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.__finish() def __resizeColumns(self): """ Private method to resize the list columns. """ self.annotateList.header().resizeSections(QHeaderView.ResizeToContents) def __generateItem(self, revision, changeset, author, date, text): """ Private method to generate an annotate item in the annotation list. @param revision revision string (string) @param changeset changeset string (string) @param author author of the change (string) @param date date of the change (string) @param text text of the change (string) """ itm = QTreeWidgetItem( self.annotateList, [revision, changeset, author, date, "{0:d}".format(self.lineno), text]) self.lineno += 1 itm.setTextAlignment(0, Qt.AlignRight) itm.setTextAlignment(4, Qt.AlignRight) def __readStdout(self): """ Private slot to handle the readyReadStdout signal. It reads the output of the process, formats it and inserts it into the annotation list. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): s = str(self.process.readLine(), self.vcs.getEncoding(), 'replace').strip() self.__processOutputLine(s) def __processOutputLine(self, line): """ Private method to process the lines of output. @param line output line to be processed (string) """ try: info, text = line.split(": ", 1) except ValueError: info = line[:-2] text = "" match = self.__annotateRe.match(info) author, rev, changeset, date, file = match.groups() self.__generateItem(rev.strip(), changeset.strip(), author.strip(), date.strip(), text) def __readStderr(self): """ Private slot to handle the readyReadStderr signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: s = str(self.process.readAllStandardError(), self.vcs.getEncoding(), 'replace') self.__showError(s) def __showError(self, out): """ Private slot to show some error. @param out error to be shown (string) """ self.errorGroup.show() self.errors.insertPlainText(out) self.errors.ensureCursorVisible() def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the hg process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(HgAnnotateDialog, self).keyPressEvent(evt)
class GUIProcess(QObject): """An external process which shows notifications in the GUI. Args: cmd: The command which was started. args: A list of arguments which gets passed. verbose: Whether to show more messages. running: Whether the underlying process is started. what: What kind of thing is spawned (process/editor/userscript/...). Used in messages. _output_messages: Show output as messages. _proc: The underlying QProcess. Signals: error/finished/started signals proxied from QProcess. """ error = pyqtSignal(QProcess.ProcessError) finished = pyqtSignal(int, QProcess.ExitStatus) started = pyqtSignal() def __init__( self, what: str, *, verbose: bool = False, additional_env: Mapping[str, str] = None, output_messages: bool = False, parent: QObject = None, ): super().__init__(parent) self.what = what self.verbose = verbose self._output_messages = output_messages self.outcome = ProcessOutcome(what=what) self.cmd: Optional[str] = None self.args: Optional[Sequence[str]] = None self.pid: Optional[int] = None self.stdout: str = "" self.stderr: str = "" self._cleanup_timer = usertypes.Timer(self, 'process-cleanup') self._cleanup_timer.setTimerType(Qt.VeryCoarseTimer) self._cleanup_timer.setInterval(3600 * 1000) # 1h self._cleanup_timer.timeout.connect(self._on_cleanup_timer) self._cleanup_timer.setSingleShot(True) self._proc = QProcess(self) self._proc.errorOccurred.connect(self._on_error) self._proc.errorOccurred.connect(self.error) self._proc.finished.connect(self._on_finished) self._proc.finished.connect(self.finished) self._proc.started.connect(self._on_started) self._proc.started.connect(self.started) self._proc.readyReadStandardOutput.connect(self._on_ready_read_stdout) self._proc.readyReadStandardError.connect(self._on_ready_read_stderr) if additional_env is not None: procenv = QProcessEnvironment.systemEnvironment() for k, v in additional_env.items(): procenv.insert(k, v) self._proc.setProcessEnvironment(procenv) def __str__(self) -> str: if self.cmd is None or self.args is None: return f'<unknown {self.what} command>' return ' '.join(shlex.quote(e) for e in [self.cmd] + list(self.args)) def _decode_data(self, qba: QByteArray) -> str: """Decode data coming from a process.""" encoding = locale.getpreferredencoding(do_setlocale=False) return qba.data().decode(encoding, 'replace') def _process_text(self, data: QByteArray, attr: str) -> None: """Process new stdout/stderr text. Arguments: data: The new process data. attr: Either 'stdout' or 'stderr'. """ text = self._decode_data(data) if '\r' in text and not utils.is_windows: # Crude handling of CR for e.g. progress output. # Discard everything before the last \r in the new input, then discard # everything after the last \n in self.stdout/self.stderr. text = text.rsplit('\r', maxsplit=1)[-1] existing = getattr(self, attr) if '\n' in existing: new = existing.rsplit('\n', maxsplit=1)[0] + '\n' else: new = '' setattr(self, attr, new) if attr == 'stdout': self.stdout += text elif attr == 'stderr': self.stderr += text else: raise utils.Unreachable(attr) @pyqtSlot() def _on_ready_read_stdout(self) -> None: if not self._output_messages: return self._process_text(self._proc.readAllStandardOutput(), 'stdout') message.info(self._elide_output(self.stdout), replace=f"stdout-{self.pid}") @pyqtSlot() def _on_ready_read_stderr(self) -> None: if not self._output_messages: return self._process_text(self._proc.readAllStandardError(), 'stderr') message.error(self._elide_output(self.stderr), replace=f"stderr-{self.pid}") @pyqtSlot(QProcess.ProcessError) def _on_error(self, error: QProcess.ProcessError) -> None: """Show a message if there was an error while spawning.""" if error == QProcess.Crashed and not utils.is_windows: # Already handled via ExitStatus in _on_finished return what = f"{self.what} {self.cmd!r}" error_descriptions = { QProcess.FailedToStart: f"{what.capitalize()} failed to start", QProcess.Crashed: f"{what.capitalize()} crashed", QProcess.Timedout: f"{what.capitalize()} timed out", QProcess.WriteError: f"Write error for {what}", QProcess.ReadError: f"Read error for {what}", } error_string = self._proc.errorString() msg = ': '.join([error_descriptions[error], error_string]) # We can't get some kind of error code from Qt... # https://bugreports.qt.io/browse/QTBUG-44769 # However, it looks like those strings aren't actually translated? known_errors = ['No such file or directory', 'Permission denied'] if (': ' in error_string and # pragma: no branch error_string.split(': ', maxsplit=1)[1] in known_errors): msg += f'\nHint: Make sure {self.cmd!r} exists and is executable' if version.is_flatpak(): msg += ' inside the Flatpak container' message.error(msg) def _elide_output(self, output: str) -> str: """Shorten long output before showing it.""" output = output.strip() lines = output.splitlines() count = len(lines) threshold = 20 if count > threshold: lines = [ f'[{count - threshold} lines hidden, see :process for the full output]' ] + lines[-threshold:] output = '\n'.join(lines) return output @pyqtSlot(int, QProcess.ExitStatus) def _on_finished(self, code: int, status: QProcess.ExitStatus) -> None: """Show a message when the process finished.""" log.procs.debug("Process finished with code {}, status {}.".format( code, status)) self.outcome.running = False self.outcome.code = code self.outcome.status = status self.stderr += self._decode_data(self._proc.readAllStandardError()) self.stdout += self._decode_data(self._proc.readAllStandardOutput()) if self._output_messages: if self.stdout: message.info( self._elide_output(self.stdout), replace=f"stdout-{self.pid}") if self.stderr: message.error( self._elide_output(self.stderr), replace=f"stderr-{self.pid}") if self.outcome.was_successful(): if self.verbose: message.info(str(self.outcome)) self._cleanup_timer.start() else: if self.stdout: log.procs.error("Process stdout:\n" + self.stdout.strip()) if self.stderr: log.procs.error("Process stderr:\n" + self.stderr.strip()) message.error(str(self.outcome) + " See :process for details.") @pyqtSlot() def _on_started(self) -> None: """Called when the process started successfully.""" log.procs.debug("Process started.") assert not self.outcome.running self.outcome.running = True def _pre_start(self, cmd: str, args: Sequence[str]) -> None: """Prepare starting of a QProcess.""" if self.outcome.running: raise ValueError("Trying to start a running QProcess!") self.cmd = cmd self.args = args log.procs.debug(f"Executing: {self}") if self.verbose: message.info(f'Executing: {self}') def start(self, cmd: str, args: Sequence[str]) -> None: """Convenience wrapper around QProcess::start.""" log.procs.debug("Starting process.") self._pre_start(cmd, args) self._proc.start(cmd, args) self._post_start() self._proc.closeWriteChannel() def start_detached(self, cmd: str, args: Sequence[str]) -> bool: """Convenience wrapper around QProcess::startDetached.""" log.procs.debug("Starting detached.") self._pre_start(cmd, args) ok, self.pid = self._proc.startDetached( cmd, args, None) # type: ignore[call-arg] if not ok: message.error("Error while spawning {}".format(self.what)) return False log.procs.debug("Process started.") self.outcome.running = True self._post_start() return True def _post_start(self) -> None: """Register this process and remember the process ID after starting.""" self.pid = self._proc.processId() all_processes[self.pid] = self global last_pid last_pid = self.pid @pyqtSlot() def _on_cleanup_timer(self) -> None: """Remove the process from all registered processes.""" log.procs.debug(f"Cleaning up data for {self.pid}") assert self.pid in all_processes all_processes[self.pid] = None self.deleteLater() def terminate(self, kill: bool = False) -> None: """Terminate or kill the process.""" if kill: self._proc.kill() else: self._proc.terminate()
class HgQueuesHeaderDialog(QDialog, Ui_HgQueuesHeaderDialog): """ Class implementing a dialog to show the commit message of the current patch. """ def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent reference to the parent widget (QWidget) """ super(HgQueuesHeaderDialog, self).__init__(parent) self.setupUi(self) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.vcs = vcs self.__hgClient = vcs.getClient() if self.__hgClient: self.process = None else: self.process = QProcess() self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.show() QCoreApplication.processEvents() def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.__hgClient: if self.__hgClient.isExecuting(): self.__hgClient.cancel() else: if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def start(self, path): """ Public slot to start the list command. @param path name of directory to be listed (string) """ self.activateWindow() dname, fname = self.vcs.splitPath(path) # find the root of the repo repodir = dname while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)): repodir = os.path.dirname(repodir) if os.path.splitdrive(repodir)[1] == os.sep: return args = self.vcs.initCommand("qheader") if self.__hgClient: out, err = self.__hgClient.runcommand( args, output=self.__showOutput, error=self.__showError) if err: self.__showError(err) if out: self.__showOutPut(out) self.__finish() else: self.process.kill() self.process.setWorkingDirectory(repodir) self.process.start('hg', args) procStarted = self.process.waitForStarted(5000) if not procStarted: E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('hg')) def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.buttonBox.button(QDialogButtonBox.Close).setFocus( Qt.OtherFocusReason) def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): if self.__hgClient: self.__hgClient.cancel() else: self.__finish() def __procFinished(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.__finish() def __readStdout(self): """ Private slot to handle the readyReadStdout signal. It reads the output of the process, formats it and inserts it into the contents pane. """ if self.process is not None: s = str(self.process.readAllStandardOutput(), self.vcs.getEncoding(), 'replace') self.__showOutput(s) def __showOutput(self, out): """ Private slot to show some output. @param out output to be shown (string) """ self.messageEdit.appendPlainText(out) def __readStderr(self): """ Private slot to handle the readyReadStderr signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: s = str(self.process.readAllStandardError(), self.vcs.getEncoding(), 'replace') self.__showError(s) def __showError(self, out): """ Private slot to show some error. @param out error to be shown (string) """ self.messageEdit.appendPlainText(self.tr("Error: ")) self.messageEdit.appendPlainText(out)
class HgBookmarksInOutDialog(QDialog, Ui_HgBookmarksInOutDialog): """ Class implementing a dialog to show a list of incoming or outgoing bookmarks. """ INCOMING = 0 OUTGOING = 1 def __init__(self, vcs, mode, parent=None): """ Constructor @param vcs reference to the vcs object @param mode mode of the dialog (HgBookmarksInOutDialog.INCOMING, HgBookmarksInOutDialog.OUTGOING) @param parent reference to the parent widget (QWidget) @exception ValueError raised to indicate an invalid dialog mode """ super(HgBookmarksInOutDialog, self).__init__(parent) self.setupUi(self) self.setWindowFlags(Qt.Window) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) if mode not in [self.INCOMING, self.OUTGOING]: raise ValueError("Bad value for mode") if mode == self.INCOMING: self.setWindowTitle(self.tr("Mercurial Incoming Bookmarks")) elif mode == self.OUTGOING: self.setWindowTitle(self.tr("Mercurial Outgoing Bookmarks")) self.process = QProcess() self.vcs = vcs self.mode = mode self.__hgClient = vcs.getClient() self.bookmarksList.headerItem().setText( self.bookmarksList.columnCount(), "") self.bookmarksList.header().setSortIndicator(3, Qt.AscendingOrder) self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.show() QCoreApplication.processEvents() def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.__hgClient: if self.__hgClient.isExecuting(): self.__hgClient.cancel() else: if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def start(self, path): """ Public slot to start the bookmarks command. @param path name of directory to be listed (string) @exception ValueError raised to indicate an invalid dialog mode """ self.errorGroup.hide() self.intercept = False self.activateWindow() dname, fname = self.vcs.splitPath(path) # find the root of the repo repodir = dname while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)): repodir = os.path.dirname(repodir) if os.path.splitdrive(repodir)[1] == os.sep: return if self.mode == self.INCOMING: args = self.vcs.initCommand("incoming") elif self.mode == self.OUTGOING: args = self.vcs.initCommand("outgoing") else: raise ValueError("Bad value for mode") args.append('--bookmarks') if self.__hgClient: self.inputGroup.setEnabled(False) self.inputGroup.hide() out, err = self.__hgClient.runcommand(args) if err: self.__showError(err) if out: for line in out.splitlines(): self.__processOutputLine(line) if self.__hgClient.wasCanceled(): break self.__finish() else: self.process.kill() self.process.setWorkingDirectory(repodir) self.process.start('hg', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('hg')) else: self.inputGroup.setEnabled(True) self.inputGroup.show() def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.inputGroup.setEnabled(False) self.inputGroup.hide() self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.buttonBox.button(QDialogButtonBox.Close).setFocus( Qt.OtherFocusReason) if self.bookmarksList.topLevelItemCount() == 0: # no bookmarks defined self.__generateItem(self.tr("no bookmarks found"), "") self.__resizeColumns() self.__resort() def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): if self.__hgClient: self.__hgClient.cancel() else: self.__finish() def __procFinished(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.__finish() def __resort(self): """ Private method to resort the tree. """ self.bookmarksList.sortItems( self.bookmarksList.sortColumn(), self.bookmarksList.header().sortIndicatorOrder()) def __resizeColumns(self): """ Private method to resize the list columns. """ self.bookmarksList.header().resizeSections( QHeaderView.ResizeToContents) self.bookmarksList.header().setStretchLastSection(True) def __generateItem(self, changeset, name): """ Private method to generate a bookmark item in the bookmarks list. @param changeset changeset of the bookmark (string) @param name name of the bookmark (string) """ QTreeWidgetItem(self.bookmarksList, [ name, changeset]) def __readStdout(self): """ Private slot to handle the readyReadStdout signal. It reads the output of the process, formats it and inserts it into the contents pane. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): s = str(self.process.readLine(), self.vcs.getEncoding(), 'replace') self.__processOutputLine(s) def __processOutputLine(self, line): """ Private method to process the lines of output. @param line output line to be processed (string) """ if line.startswith(" "): li = line.strip().split() changeset = li[-1] del li[-1] name = " ".join(li) self.__generateItem(changeset, name) def __readStderr(self): """ Private slot to handle the readyReadStderr signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: s = str(self.process.readAllStandardError(), self.vcs.getEncoding(), 'replace') self.__showError(s) def __showError(self, out): """ Private slot to show some error. @param out error to be shown (string) """ self.errorGroup.show() self.errors.insertPlainText(out) self.errors.ensureCursorVisible() def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the subversion process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(HgBookmarksInOutDialog, self).keyPressEvent(evt)
class SvnLogDialog(QWidget, Ui_SvnLogDialog): """ Class implementing a dialog to show the output of the svn log command process. The dialog is nonmodal. Clicking a link in the upper text pane shows a diff of the versions. """ def __init__(self, vcs, isFile=False, parent=None): """ Constructor @param vcs reference to the vcs object @param isFile flag indicating log for a file is to be shown (boolean) @param parent parent widget (QWidget) """ super(SvnLogDialog, self).__init__(parent) self.setupUi(self) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.process = QProcess() self.vcs = vcs self.contents.setHtml( self.tr('<b>Processing your request, please wait...</b>')) self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.contents.anchorClicked.connect(self.__sourceChanged) self.rx_sep = QRegExp('\\-+\\s*') self.rx_sep2 = QRegExp('=+\\s*') self.rx_rev = QRegExp( 'rev ([0-9]+): ([^|]*) \| ([^|]*) \| ([0-9]+) .*') # "rev" followed by one or more decimals followed by a colon followed # anything up to " | " (twice) followed by one or more decimals # followed by anything self.rx_rev2 = QRegExp( 'r([0-9]+) \| ([^|]*) \| ([^|]*) \| ([0-9]+) .*') # "r" followed by one or more decimals followed by " | " followed # anything up to " | " (twice) followed by one or more decimals # followed by anything self.rx_flags = QRegExp(' ([ADM])( .*)\\s*') # three blanks followed by A or D or M self.rx_changed = QRegExp('Changed .*\\s*') self.flags = { 'A': self.tr('Added'), 'D': self.tr('Deleted'), 'M': self.tr('Modified') } self.revisions = [] # stack of remembered revisions self.revString = self.tr('revision') self.buf = [] # buffer for stdout self.diff = None self.sbsCheckBox.setEnabled(isFile) self.sbsCheckBox.setVisible(isFile) def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def start(self, fn, noEntries=0): """ Public slot to start the cvs log command. @param fn filename to show the log for (string) @param noEntries number of entries to show (integer) """ self.errorGroup.hide() QApplication.processEvents() self.intercept = False self.filename = fn self.dname, self.fname = self.vcs.splitPath(fn) self.process.kill() args = [] args.append('log') self.vcs.addArguments(args, self.vcs.options['global']) self.vcs.addArguments(args, self.vcs.options['log']) if noEntries: args.append('--limit') args.append(str(noEntries)) self.activateWindow() self.raise_() args.append(self.fname) self.process.setWorkingDirectory(self.dname) self.process.start('svn', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('svn')) def __procFinished(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.inputGroup.setEnabled(False) self.inputGroup.hide() self.contents.clear() lvers = 1 for s in self.buf: rev_match = False if self.rx_rev.exactMatch(s): ver = self.rx_rev.cap(1) author = self.rx_rev.cap(2) date = self.rx_rev.cap(3) # number of lines is ignored rev_match = True elif self.rx_rev2.exactMatch(s): ver = self.rx_rev2.cap(1) author = self.rx_rev2.cap(2) date = self.rx_rev2.cap(3) # number of lines is ignored rev_match = True if rev_match: dstr = '<b>{0} {1}</b>'.format(self.revString, ver) try: lv = self.revisions[lvers] lvers += 1 url = QUrl() url.setScheme("file") url.setPath(self.filename) if qVersion() >= "5.0.0": query = lv + '_' + ver url.setQuery(query) else: query = QByteArray() query.append(lv).append('_').append(ver) url.setEncodedQuery(query) dstr += ' [<a href="{0}" name="{1}">{2}</a>]'.format( url.toString(), query, self.tr('diff to {0}').format(lv), ) except IndexError: pass dstr += '<br />\n' self.contents.insertHtml(dstr) dstr = self.tr('<i>author: {0}</i><br />\n').format(author) self.contents.insertHtml(dstr) dstr = self.tr('<i>date: {0}</i><br />\n').format(date) self.contents.insertHtml(dstr) elif self.rx_sep.exactMatch(s) or self.rx_sep2.exactMatch(s): self.contents.insertHtml('<hr />\n') elif self.rx_flags.exactMatch(s): dstr = self.flags[self.rx_flags.cap(1)] dstr += self.rx_flags.cap(2) dstr += '<br />\n' self.contents.insertHtml(dstr) elif self.rx_changed.exactMatch(s): dstr = '<br />{0}<br />\n'.format(s) self.contents.insertHtml(dstr) else: if s == "": s = self.contents.insertHtml('<br />\n') else: self.contents.insertHtml(Utilities.html_encode(s)) self.contents.insertHtml('<br />\n') tc = self.contents.textCursor() tc.movePosition(QTextCursor.Start) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() def __readStdout(self): """ Private slot to handle the readyReadStandardOutput signal. It reads the output of the process and inserts it into a buffer. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): line = str(self.process.readLine(), Preferences.getSystem("IOEncoding"), 'replace') self.buf.append(line) if self.rx_rev.exactMatch(line): ver = self.rx_rev.cap(1) # save revision number for later use self.revisions.append(ver) elif self.rx_rev2.exactMatch(line): ver = self.rx_rev2.cap(1) # save revision number for later use self.revisions.append(ver) def __readStderr(self): """ Private slot to handle the readyReadStandardError signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: self.errorGroup.show() s = str(self.process.readAllStandardError(), Preferences.getSystem("IOEncoding"), 'replace') self.errors.insertPlainText(s) self.errors.ensureCursorVisible() def __sourceChanged(self, url): """ Private slot to handle the sourceChanged signal of the contents pane. @param url the url that was clicked (QUrl) """ self.contents.setSource(QUrl('')) filename = url.path() if Utilities.isWindowsPlatform(): if filename.startswith("/"): filename = filename[1:] if qVersion() >= "5.0.0": ver = url.query() else: ver = bytes(url.encodedQuery()).decode() v1 = ver.split('_')[0] v2 = ver.split('_')[1] if v1 == "" or v2 == "": return self.contents.scrollToAnchor(ver) if self.sbsCheckBox.isEnabled() and self.sbsCheckBox.isChecked(): self.vcs.svnSbsDiff(filename, revisions=(v1, v2)) else: if self.diff is None: from .SvnDiffDialog import SvnDiffDialog self.diff = SvnDiffDialog(self.vcs) self.diff.show() self.diff.start(filename, [v1, v2]) def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the subversion process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(SvnLogDialog, self).keyPressEvent(evt)
class SvnLogBrowserDialog(QWidget, Ui_SvnLogBrowserDialog): """ Class implementing a dialog to browse the log history. """ def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent parent widget (QWidget) """ super(SvnLogBrowserDialog, self).__init__(parent) self.setupUi(self) self.__position = QPoint() self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.filesTree.headerItem().setText(self.filesTree.columnCount(), "") self.filesTree.header().setSortIndicator(0, Qt.AscendingOrder) self.vcs = vcs self.__initData() self.fromDate.setDisplayFormat("yyyy-MM-dd") self.toDate.setDisplayFormat("yyyy-MM-dd") self.__resetUI() self.__messageRole = Qt.UserRole self.__changesRole = Qt.UserRole + 1 self.process = QProcess() self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.rx_sep1 = QRegExp('\\-+\\s*') self.rx_sep2 = QRegExp('=+\\s*') self.rx_rev1 = QRegExp( 'rev ([0-9]+): ([^|]*) \| ([^|]*) \| ([0-9]+) .*') # "rev" followed by one or more decimals followed by a colon followed # anything up to " | " (twice) followed by one or more decimals # followed by anything self.rx_rev2 = QRegExp( 'r([0-9]+) \| ([^|]*) \| ([^|]*) \| ([0-9]+) .*') # "r" followed by one or more decimals followed by " | " followed # anything up to " | " (twice) followed by one or more decimals # followed by anything self.rx_flags1 = QRegExp( r""" ([ADM])\s(.*)\s+\(\w+\s+(.*):([0-9]+)\)\s*""") # three blanks followed by A or D or M followed by path followed by # path copied from followed by copied from revision self.rx_flags2 = QRegExp(' ([ADM]) (.*)\\s*') # three blanks followed by A or D or M followed by path self.flags = { 'A': self.tr('Added'), 'D': self.tr('Deleted'), 'M': self.tr('Modified'), 'R': self.tr('Replaced'), } self.intercept = False def __initData(self): """ Private method to (re-)initialize some data. """ self.__maxDate = QDate() self.__minDate = QDate() self.__filterLogsEnabled = True self.buf = [] # buffer for stdout self.diff = None self.__started = False self.__lastRev = 0 def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.__position = self.pos() e.accept() def show(self): """ Public slot to show the dialog. """ if not self.__position.isNull(): self.move(self.__position) self.__resetUI() super(SvnLogBrowserDialog, self).show() def __resetUI(self): """ Private method to reset the user interface. """ self.fromDate.setDate(QDate.currentDate()) self.toDate.setDate(QDate.currentDate()) self.fieldCombo.setCurrentIndex(self.fieldCombo.findText( self.tr("Message"))) self.limitSpinBox.setValue(self.vcs.getPlugin().getPreferences( "LogLimit")) self.stopCheckBox.setChecked(self.vcs.getPlugin().getPreferences( "StopLogOnCopy")) self.logTree.clear() self.nextButton.setEnabled(True) self.limitSpinBox.setEnabled(True) def __resizeColumnsLog(self): """ Private method to resize the log tree columns. """ self.logTree.header().resizeSections(QHeaderView.ResizeToContents) self.logTree.header().setStretchLastSection(True) def __resortLog(self): """ Private method to resort the log tree. """ self.logTree.sortItems( self.logTree.sortColumn(), self.logTree.header().sortIndicatorOrder()) def __resizeColumnsFiles(self): """ Private method to resize the changed files tree columns. """ self.filesTree.header().resizeSections(QHeaderView.ResizeToContents) self.filesTree.header().setStretchLastSection(True) def __resortFiles(self): """ Private method to resort the changed files tree. """ sortColumn = self.filesTree.sortColumn() self.filesTree.sortItems( 1, self.filesTree.header().sortIndicatorOrder()) self.filesTree.sortItems( sortColumn, self.filesTree.header().sortIndicatorOrder()) def __generateLogItem(self, author, date, message, revision, changedPaths): """ Private method to generate a log tree entry. @param author author info (string) @param date date info (string) @param message text of the log message (list of strings) @param revision revision info (string) @param changedPaths list of dictionary objects containing info about the changed files/directories @return reference to the generated item (QTreeWidgetItem) """ msg = [] for line in message: msg.append(line.strip()) itm = QTreeWidgetItem(self.logTree) itm.setData(0, Qt.DisplayRole, int(revision)) itm.setData(1, Qt.DisplayRole, author) itm.setData(2, Qt.DisplayRole, date) itm.setData(3, Qt.DisplayRole, " ".join(msg)) itm.setData(0, self.__messageRole, message) itm.setData(0, self.__changesRole, changedPaths) itm.setTextAlignment(0, Qt.AlignRight) itm.setTextAlignment(1, Qt.AlignLeft) itm.setTextAlignment(2, Qt.AlignLeft) itm.setTextAlignment(3, Qt.AlignLeft) itm.setTextAlignment(4, Qt.AlignLeft) try: self.__lastRev = int(revision) except ValueError: self.__lastRev = 0 return itm def __generateFileItem(self, action, path, copyFrom, copyRev): """ Private method to generate a changed files tree entry. @param action indicator for the change action ("A", "D" or "M") @param path path of the file in the repository (string) @param copyFrom path the file was copied from (None, string) @param copyRev revision the file was copied from (None, string) @return reference to the generated item (QTreeWidgetItem) """ itm = QTreeWidgetItem(self.filesTree, [ self.flags[action], path, copyFrom, copyRev, ]) itm.setTextAlignment(3, Qt.AlignRight) return itm def __getLogEntries(self, startRev=None): """ Private method to retrieve log entries from the repository. @param startRev revision number to start from (integer, string) """ self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) QApplication.processEvents() QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) QApplication.processEvents() self.intercept = False self.process.kill() self.buf = [] self.cancelled = False self.errors.clear() args = [] args.append('log') self.vcs.addArguments(args, self.vcs.options['global']) self.vcs.addArguments(args, self.vcs.options['log']) args.append('--verbose') args.append('--limit') args.append('{0:d}'.format(self.limitSpinBox.value())) if startRev is not None: args.append('--revision') args.append('{0}:0'.format(startRev)) if self.stopCheckBox.isChecked(): args.append('--stop-on-copy') args.append(self.fname) self.process.setWorkingDirectory(self.dname) self.inputGroup.setEnabled(True) self.inputGroup.show() self.process.start('svn', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('svn')) def start(self, fn, isFile=False): """ Public slot to start the svn log command. @param fn filename to show the log for (string) @keyparam isFile flag indicating log for a file is to be shown (boolean) """ self.sbsCheckBox.setEnabled(isFile) self.sbsCheckBox.setVisible(isFile) self.errorGroup.hide() QApplication.processEvents() self.__initData() self.filename = fn self.dname, self.fname = self.vcs.splitPath(fn) self.activateWindow() self.raise_() self.logTree.clear() self.__started = True self.__getLogEntries() def __procFinished(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.__processBuffer() self.__finish() def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) QApplication.restoreOverrideCursor() self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.inputGroup.setEnabled(False) self.inputGroup.hide() def __processBuffer(self): """ Private method to process the buffered output of the svn log command. """ noEntries = 0 log = {"message": []} changedPaths = [] for s in self.buf: if self.rx_rev1.exactMatch(s): log["revision"] = self.rx_rev.cap(1) log["author"] = self.rx_rev.cap(2) log["date"] = self.rx_rev.cap(3) # number of lines is ignored elif self.rx_rev2.exactMatch(s): log["revision"] = self.rx_rev2.cap(1) log["author"] = self.rx_rev2.cap(2) log["date"] = self.rx_rev2.cap(3) # number of lines is ignored elif self.rx_flags1.exactMatch(s): changedPaths.append({ "action": self.rx_flags1.cap(1).strip(), "path": self.rx_flags1.cap(2).strip(), "copyfrom_path": self.rx_flags1.cap(3).strip(), "copyfrom_revision": self.rx_flags1.cap(4).strip(), }) elif self.rx_flags2.exactMatch(s): changedPaths.append({ "action": self.rx_flags2.cap(1).strip(), "path": self.rx_flags2.cap(2).strip(), "copyfrom_path": "", "copyfrom_revision": "", }) elif self.rx_sep1.exactMatch(s) or self.rx_sep2.exactMatch(s): if len(log) > 1: self.__generateLogItem( log["author"], log["date"], log["message"], log["revision"], changedPaths) dt = QDate.fromString(log["date"], Qt.ISODate) if not self.__maxDate.isValid() and \ not self.__minDate.isValid(): self.__maxDate = dt self.__minDate = dt else: if self.__maxDate < dt: self.__maxDate = dt if self.__minDate > dt: self.__minDate = dt noEntries += 1 log = {"message": []} changedPaths = [] else: if s.strip().endswith(":") or not s.strip(): continue else: log["message"].append(s) self.__resizeColumnsLog() self.__resortLog() if self.__started: self.logTree.setCurrentItem(self.logTree.topLevelItem(0)) self.__started = False if noEntries < self.limitSpinBox.value() and not self.cancelled: self.nextButton.setEnabled(False) self.limitSpinBox.setEnabled(False) self.__filterLogsEnabled = False self.fromDate.setMinimumDate(self.__minDate) self.fromDate.setMaximumDate(self.__maxDate) self.fromDate.setDate(self.__minDate) self.toDate.setMinimumDate(self.__minDate) self.toDate.setMaximumDate(self.__maxDate) self.toDate.setDate(self.__maxDate) self.__filterLogsEnabled = True self.__filterLogs() def __readStdout(self): """ Private slot to handle the readyReadStandardOutput signal. It reads the output of the process and inserts it into a buffer. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): line = str(self.process.readLine(), Preferences.getSystem("IOEncoding"), 'replace') self.buf.append(line) def __readStderr(self): """ Private slot to handle the readyReadStandardError signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: self.errorGroup.show() s = str(self.process.readAllStandardError(), Preferences.getSystem("IOEncoding"), 'replace') self.errors.insertPlainText(s) self.errors.ensureCursorVisible() def __diffRevisions(self, rev1, rev2): """ Private method to do a diff of two revisions. @param rev1 first revision number (integer) @param rev2 second revision number (integer) """ if self.sbsCheckBox.isEnabled() and self.sbsCheckBox.isChecked(): self.vcs.svnSbsDiff(self.filename, revisions=(str(rev1), str(rev2))) else: if self.diff is None: from .SvnDiffDialog import SvnDiffDialog self.diff = SvnDiffDialog(self.vcs) self.diff.show() self.diff.raise_() self.diff.start(self.filename, [rev1, rev2]) def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): self.cancelled = True self.__finish() @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) def on_logTree_currentItemChanged(self, current, previous): """ Private slot called, when the current item of the log tree changes. @param current reference to the new current item (QTreeWidgetItem) @param previous reference to the old current item (QTreeWidgetItem) """ if current is not None: self.messageEdit.clear() for line in current.data(0, self.__messageRole): self.messageEdit.append(line.strip()) self.filesTree.clear() changes = current.data(0, self.__changesRole) if len(changes) > 0: for change in changes: self.__generateFileItem( change["action"], change["path"], change["copyfrom_path"], change["copyfrom_revision"]) self.__resizeColumnsFiles() self.__resortFiles() self.diffPreviousButton.setEnabled( current != self.logTree.topLevelItem( self.logTree.topLevelItemCount() - 1)) @pyqtSlot() def on_logTree_itemSelectionChanged(self): """ Private slot called, when the selection has changed. """ self.diffRevisionsButton.setEnabled( len(self.logTree.selectedItems()) == 2) @pyqtSlot() def on_nextButton_clicked(self): """ Private slot to handle the Next button. """ if self.__lastRev > 1: self.__getLogEntries(self.__lastRev - 1) @pyqtSlot() def on_diffPreviousButton_clicked(self): """ Private slot to handle the Diff to Previous button. """ itm = self.logTree.currentItem() if itm is None: self.diffPreviousButton.setEnabled(False) return rev2 = int(itm.text(0)) itm = self.logTree.topLevelItem( self.logTree.indexOfTopLevelItem(itm) + 1) if itm is None: self.diffPreviousButton.setEnabled(False) return rev1 = int(itm.text(0)) self.__diffRevisions(rev1, rev2) @pyqtSlot() def on_diffRevisionsButton_clicked(self): """ Private slot to handle the Compare Revisions button. """ items = self.logTree.selectedItems() if len(items) != 2: self.diffRevisionsButton.setEnabled(False) return rev2 = int(items[0].text(0)) rev1 = int(items[1].text(0)) self.__diffRevisions(min(rev1, rev2), max(rev1, rev2)) @pyqtSlot(QDate) def on_fromDate_dateChanged(self, date): """ Private slot called, when the from date changes. @param date new date (QDate) """ self.__filterLogs() @pyqtSlot(QDate) def on_toDate_dateChanged(self, date): """ Private slot called, when the from date changes. @param date new date (QDate) """ self.__filterLogs() @pyqtSlot(str) def on_fieldCombo_activated(self, txt): """ Private slot called, when a new filter field is selected. @param txt text of the selected field (string) """ self.__filterLogs() @pyqtSlot(str) def on_rxEdit_textChanged(self, txt): """ Private slot called, when a filter expression is entered. @param txt filter expression (string) """ self.__filterLogs() def __filterLogs(self): """ Private method to filter the log entries. """ if self.__filterLogsEnabled: from_ = self.fromDate.date().toString("yyyy-MM-dd") to_ = self.toDate.date().addDays(1).toString("yyyy-MM-dd") txt = self.fieldCombo.currentText() if txt == self.tr("Author"): fieldIndex = 1 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive) elif txt == self.tr("Revision"): fieldIndex = 0 txt = self.rxEdit.text() if txt.startswith("^"): searchRx = QRegExp( "^\s*{0}".format(txt[1:]), Qt.CaseInsensitive) else: searchRx = QRegExp(txt, Qt.CaseInsensitive) else: fieldIndex = 3 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive) currentItem = self.logTree.currentItem() for topIndex in range(self.logTree.topLevelItemCount()): topItem = self.logTree.topLevelItem(topIndex) if topItem.text(2) <= to_ and topItem.text(2) >= from_ and \ searchRx.indexIn(topItem.text(fieldIndex)) > -1: topItem.setHidden(False) if topItem is currentItem: self.on_logTree_currentItemChanged(topItem, None) else: topItem.setHidden(True) if topItem is currentItem: self.messageEdit.clear() self.filesTree.clear() @pyqtSlot(bool) def on_stopCheckBox_clicked(self, checked): """ Private slot called, when the stop on copy/move checkbox is clicked. @param checked flag indicating the checked state (boolean) """ self.vcs.getPlugin().setPreferences("StopLogOnCopy", self.stopCheckBox.isChecked()) self.nextButton.setEnabled(True) self.limitSpinBox.setEnabled(True) def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the subversion process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.errorGroup.show() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(SvnLogBrowserDialog, self).keyPressEvent(evt)
class SvnBlameDialog(QDialog, Ui_SvnBlameDialog): """ Class implementing a dialog to show the output of the svn blame command. """ def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent parent widget (QWidget) """ super(SvnBlameDialog, self).__init__(parent) self.setupUi(self) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.process = QProcess() self.vcs = vcs self.blameList.headerItem().setText(self.blameList.columnCount(), "") font = Preferences.getEditorOtherFonts("MonospacedFont") self.blameList.setFont(font) self.__ioEncoding = Preferences.getSystem("IOEncoding") self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def start(self, fn): """ Public slot to start the svn status command. @param fn filename to show the log for (string) """ self.errorGroup.hide() self.intercept = False self.activateWindow() self.lineno = 1 dname, fname = self.vcs.splitPath(fn) self.process.kill() args = [] args.append('blame') self.vcs.addArguments(args, self.vcs.options['global']) args.append(fname) self.process.setWorkingDirectory(dname) self.process.start('svn', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('svn')) else: self.inputGroup.setEnabled(True) self.inputGroup.show() def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.buttonBox.button(QDialogButtonBox.Close).setFocus( Qt.OtherFocusReason) self.inputGroup.setEnabled(False) self.inputGroup.hide() self.process = None self.__resizeColumns() def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): self.__finish() def __procFinished(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.__finish() def __resizeColumns(self): """ Private method to resize the list columns. """ self.blameList.header().resizeSections(QHeaderView.ResizeToContents) def __generateItem(self, revision, author, text): """ Private method to generate a blame item in the blame list. @param revision revision string (string) @param author author of the change (string) @param text line of text from the annotated file (string) """ itm = QTreeWidgetItem( self.blameList, [revision, author, "{0:d}".format(self.lineno), text]) self.lineno += 1 itm.setTextAlignment(0, Qt.AlignRight) itm.setTextAlignment(2, Qt.AlignRight) def __readStdout(self): """ Private slot to handle the readyReadStdout signal. It reads the output of the process, formats it and inserts it into the contents pane. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): s = str(self.process.readLine(), self.__ioEncoding, 'replace')\ .strip() rev, s = s.split(None, 1) try: author, text = s.split(' ', 1) except ValueError: author = s.strip() text = "" self.__generateItem(rev, author, text) def __readStderr(self): """ Private slot to handle the readyReadStderr signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: self.errorGroup.show() s = str(self.process.readAllStandardError(), Preferences.getSystem("IOEncoding"), 'replace') self.errors.insertPlainText(s) self.errors.ensureCursorVisible() def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the subversion process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(SvnBlameDialog, self).keyPressEvent(evt)
class SvnDiffDialog(QWidget, Ui_SvnDiffDialog): """ Class implementing a dialog to show the output of the svn diff command process. """ def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent parent widget (QWidget) """ super(SvnDiffDialog, self).__init__(parent) self.setupUi(self) self.refreshButton = self.buttonBox.addButton( self.tr("Refresh"), QDialogButtonBox.ActionRole) self.refreshButton.setToolTip( self.tr("Press to refresh the display")) self.refreshButton.setEnabled(False) self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.searchWidget.attachTextEdit(self.contents) self.process = QProcess() self.vcs = vcs font = Preferences.getEditorOtherFonts("MonospacedFont") self.contents.setFontFamily(font.family()) self.contents.setFontPointSize(font.pointSize()) self.highlighter = SvnDiffHighlighter(self.contents.document()) self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def __getVersionArg(self, version): """ Private method to get a svn revision argument for the given revision. @param version revision (integer or string) @return version argument (string) """ if version == "WORKING": return None else: return str(version) def start(self, fn, versions=None, urls=None, summary=False, refreshable=False): """ Public slot to start the svn diff command. @param fn filename to be diffed (string) @param versions list of versions to be diffed (list of up to 2 strings or None) @keyparam urls list of repository URLs (list of 2 strings) @keyparam summary flag indicating a summarizing diff (only valid for URL diffs) (boolean) @keyparam refreshable flag indicating a refreshable diff (boolean) """ self.refreshButton.setVisible(refreshable) self.errorGroup.hide() self.inputGroup.show() self.inputGroup.setEnabled(True) self.intercept = False self.filename = fn self.process.kill() self.contents.clear() self.paras = 0 self.filesCombo.clear() self.__oldFile = "" self.__oldFileLine = -1 self.__fileSeparators = [] args = [] args.append('diff') self.vcs.addArguments(args, self.vcs.options['global']) self.vcs.addArguments(args, self.vcs.options['diff']) if '--diff-cmd' in self.vcs.options['diff']: self.buttonBox.button(QDialogButtonBox.Save).hide() if versions is not None: self.raise_() self.activateWindow() rev1 = self.__getVersionArg(versions[0]) rev2 = None if len(versions) == 2: rev2 = self.__getVersionArg(versions[1]) if rev1 is not None or rev2 is not None: args.append('-r') if rev1 is not None and rev2 is not None: args.append('{0}:{1}'.format(rev1, rev2)) elif rev2 is None: args.append(rev1) elif rev1 is None: args.append(rev2) self.summaryPath = None if urls is not None: if summary: args.append("--summarize") self.summaryPath = urls[0] args.append("--old={0}".format(urls[0])) args.append("--new={0}".format(urls[1])) if isinstance(fn, list): dname, fnames = self.vcs.splitPathList(fn) else: dname, fname = self.vcs.splitPath(fn) fnames = [fname] project = e5App().getObject('Project') if dname == project.getProjectPath(): path = "" else: path = project.getRelativePath(dname) if path: path += "/" for fname in fnames: args.append(path + fname) else: if isinstance(fn, list): dname, fnames = self.vcs.splitPathList(fn) self.vcs.addArguments(args, fnames) else: dname, fname = self.vcs.splitPath(fn) args.append(fname) self.process.setWorkingDirectory(dname) self.process.start('svn', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('svn')) def __procFinished(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.inputGroup.setEnabled(False) self.inputGroup.hide() self.refreshButton.setEnabled(True) if self.paras == 0: self.contents.setPlainText(self.tr('There is no difference.')) self.buttonBox.button(QDialogButtonBox.Save).setEnabled(self.paras > 0) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.buttonBox.button(QDialogButtonBox.Close).setFocus( Qt.OtherFocusReason) tc = self.contents.textCursor() tc.movePosition(QTextCursor.Start) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() self.filesCombo.addItem(self.tr("<Start>"), 0) self.filesCombo.addItem(self.tr("<End>"), -1) for oldFile, newFile, pos in sorted(self.__fileSeparators): if oldFile != newFile: self.filesCombo.addItem( "{0}\n{1}".format(oldFile, newFile), pos) else: self.filesCombo.addItem(oldFile, pos) def __appendText(self, txt): """ Private method to append text to the end of the contents pane. @param txt text to insert (string) """ tc = self.contents.textCursor() tc.movePosition(QTextCursor.End) self.contents.setTextCursor(tc) self.contents.insertPlainText(txt) def __extractFileName(self, line): """ Private method to extract the file name out of a file separator line. @param line line to be processed (string) @return extracted file name (string) """ f = line.split(None, 1)[1] f = f.rsplit(None, 2)[0] return f def __processFileLine(self, line): """ Private slot to process a line giving the old/new file. @param line line to be processed (string) """ if line.startswith('---'): self.__oldFileLine = self.paras self.__oldFile = self.__extractFileName(line) else: self.__fileSeparators.append( (self.__oldFile, self.__extractFileName(line), self.__oldFileLine)) def __readStdout(self): """ Private slot to handle the readyReadStandardOutput signal. It reads the output of the process, formats it and inserts it into the contents pane. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): line = str(self.process.readLine(), Preferences.getSystem("IOEncoding"), 'replace') if self.summaryPath: line = line.replace(self.summaryPath + '/', '') line = " ".join(line.split()) if line.startswith("--- ") or line.startswith("+++ "): self.__processFileLine(line) self.__appendText(line) self.paras += 1 def __readStderr(self): """ Private slot to handle the readyReadStandardError signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: self.errorGroup.show() s = str(self.process.readAllStandardError(), Preferences.getSystem("IOEncoding"), 'replace') self.errors.insertPlainText(s) self.errors.ensureCursorVisible() def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Save): self.on_saveButton_clicked() elif button == self.refreshButton: self.on_refreshButton_clicked() @pyqtSlot(int) def on_filesCombo_activated(self, index): """ Private slot to handle the selection of a file. @param index activated row (integer) """ para = self.filesCombo.itemData(index) if para == 0: tc = self.contents.textCursor() tc.movePosition(QTextCursor.Start) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() elif para == -1: tc = self.contents.textCursor() tc.movePosition(QTextCursor.End) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() else: # step 1: move cursor to end tc = self.contents.textCursor() tc.movePosition(QTextCursor.End) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() # step 2: move cursor to desired line tc = self.contents.textCursor() delta = tc.blockNumber() - para tc.movePosition(QTextCursor.PreviousBlock, QTextCursor.MoveAnchor, delta) self.contents.setTextCursor(tc) self.contents.ensureCursorVisible() @pyqtSlot() def on_saveButton_clicked(self): """ Private slot to handle the Save button press. It saves the diff shown in the dialog to a file in the local filesystem. """ if isinstance(self.filename, list): if len(self.filename) > 1: fname = self.vcs.splitPathList(self.filename)[0] else: dname, fname = self.vcs.splitPath(self.filename[0]) if fname != '.': fname = "{0}.diff".format(self.filename[0]) else: fname = dname else: fname = self.vcs.splitPath(self.filename)[0] fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( self, self.tr("Save Diff"), fname, self.tr("Patch Files (*.diff)"), None, E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) if not fname: return # user aborted ext = QFileInfo(fname).suffix() if not ext: ex = selectedFilter.split("(*")[1].split(")")[0] if ex: fname += ex if QFileInfo(fname).exists(): res = E5MessageBox.yesNo( self, self.tr("Save Diff"), self.tr("<p>The patch file <b>{0}</b> already exists." " Overwrite it?</p>").format(fname), icon=E5MessageBox.Warning) if not res: return fname = Utilities.toNativeSeparators(fname) eol = e5App().getObject("Project").getEolString() try: f = open(fname, "w", encoding="utf-8", newline="") f.write(eol.join(self.contents.toPlainText().splitlines())) f.close() except IOError as why: E5MessageBox.critical( self, self.tr('Save Diff'), self.tr( '<p>The patch file <b>{0}</b> could not be saved.' '<br>Reason: {1}</p>') .format(fname, str(why))) @pyqtSlot() def on_refreshButton_clicked(self): """ Private slot to refresh the display. """ self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False) self.refreshButton.setEnabled(False) self.start(self.filename, refreshable=True) def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the subversion process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(SvnDiffDialog, self).keyPressEvent(evt)
class GitRemoteRepositoriesDialog(QWidget, Ui_GitRemoteRepositoriesDialog): """ Class implementing a dialog to show available remote repositories. """ def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent parent widget (QWidget) """ super(GitRemoteRepositoriesDialog, self).__init__(parent) self.setupUi(self) self.vcs = vcs self.process = QProcess() self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.refreshButton = self.buttonBox.addButton( self.tr("Refresh"), QDialogButtonBox.ActionRole) self.refreshButton.setToolTip( self.tr("Press to refresh the repositories display")) self.refreshButton.setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.__lastColumn = self.repolist.columnCount() self.repolist.headerItem().setText(self.__lastColumn, "") self.repolist.header().setSortIndicator(0, Qt.AscendingOrder) self.__ioEncoding = Preferences.getSystem("IOEncoding") def __resort(self): """ Private method to resort the list. """ self.repolist.sortItems( self.repolist.sortColumn(), self.repolist.header().sortIndicatorOrder()) def __resizeColumns(self): """ Private method to resize the list columns. """ self.repolist.header().resizeSections(QHeaderView.ResizeToContents) self.repolist.header().setStretchLastSection(True) def __generateItem(self, name, url, oper): """ Private method to generate a status item in the status list. @param name name of the remote repository (string) @param url URL of the remote repository (string) @param oper operation the remote repository may be used for (string) """ foundItems = self.repolist.findItems(name, Qt.MatchExactly, 0) if foundItems: # modify the operations column only foundItems[0].setText( 2, "{0} + {1}".format(foundItems[0].text(2), oper)) else: QTreeWidgetItem(self.repolist, [name, url, oper]) def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def start(self, projectDir): """ Public slot to start the git remote command. @param projectDir name of the project directory (string) """ self.repolist.clear() self.errorGroup.hide() self.intercept = False self.projectDir = projectDir self.__ioEncoding = Preferences.getSystem("IOEncoding") self.removeButton.setEnabled(False) self.renameButton.setEnabled(False) self.pruneButton.setEnabled(False) self.showInfoButton.setEnabled(False) args = self.vcs.initCommand("remote") args.append('--verbose') # find the root of the repo repodir = self.projectDir while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)): repodir = os.path.dirname(repodir) if os.path.splitdrive(repodir)[1] == os.sep: return self.process.kill() self.process.setWorkingDirectory(repodir) self.process.start('git', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('git')) else: self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.inputGroup.setEnabled(True) self.inputGroup.show() self.refreshButton.setEnabled(False) def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.inputGroup.setEnabled(False) self.inputGroup.hide() self.refreshButton.setEnabled(True) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.buttonBox.button(QDialogButtonBox.Close).setFocus( Qt.OtherFocusReason) self.__resort() self.__resizeColumns() self.__updateButtons() def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): self.__finish() elif button == self.refreshButton: self.on_refreshButton_clicked() def __procFinished(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.__finish() def __readStdout(self): """ Private slot to handle the readyReadStandardOutput signal. It reads the output of the process, formats it and inserts it into the contents pane. """ if self.process is not None: self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): line = str(self.process.readLine(), self.__ioEncoding, 'replace').strip() name, line = line.split(None, 1) url, oper = line.rsplit(None, 1) oper = oper[1:-1] # it is enclosed in () self.__generateItem(name, url, oper) def __readStderr(self): """ Private slot to handle the readyReadStandardError signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: s = str(self.process.readAllStandardError(), self.__ioEncoding, 'replace') self.errorGroup.show() self.errors.insertPlainText(s) self.errors.ensureCursorVisible() def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the git process. """ inputTxt = self.input.text() inputTxt += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(inputTxt) self.errors.ensureCursorVisible() self.process.write(inputTxt) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(GitRemoteRepositoriesDialog, self).keyPressEvent(evt) @pyqtSlot() def on_refreshButton_clicked(self): """ Private slot to refresh the status display. """ self.start(self.projectDir) def __updateButtons(self): """ Private method to update the buttons status. """ enable = len(self.repolist.selectedItems()) == 1 self.removeButton.setEnabled(enable) self.renameButton.setEnabled(enable) self.pruneButton.setEnabled(enable) self.showInfoButton.setEnabled(enable) @pyqtSlot() def on_repolist_itemSelectionChanged(self): """ Private slot to act upon changes of selected items. """ self.__updateButtons() @pyqtSlot() def on_addButton_clicked(self): """ Private slot to add a remote repository. """ self.vcs.gitAddRemote(self.projectDir) self.on_refreshButton_clicked() @pyqtSlot() def on_renameButton_clicked(self): """ Private slot to rename a remote repository. """ remoteName = self.repolist.selectedItems()[0].text(0) self.vcs.gitRenameRemote(self.projectDir, remoteName) self.on_refreshButton_clicked() @pyqtSlot() def on_removeButton_clicked(self): """ Private slot to remove a remote repository. """ remoteName = self.repolist.selectedItems()[0].text(0) self.vcs.gitRemoveRemote(self.projectDir, remoteName) self.on_refreshButton_clicked() @pyqtSlot() def on_showInfoButton_clicked(self): """ Private slot to show information about a remote repository. """ remoteName = self.repolist.selectedItems()[0].text(0) self.vcs.gitShowRemote(self.projectDir, remoteName) @pyqtSlot() def on_pruneButton_clicked(self): """ Private slot to prune all stale remote-tracking branches. """ remoteName = self.repolist.selectedItems()[0].text(0) self.vcs.gitPruneRemote(self.projectDir, remoteName)
class HgShelveBrowserDialog(QWidget, Ui_HgShelveBrowserDialog): """ Class implementing Mercurial shelve browser dialog. """ NameColumn = 0 AgeColumn = 1 MessageColumn = 2 def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent parent widget (QWidget) """ super(HgShelveBrowserDialog, self).__init__(parent) self.setupUi(self) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.__position = QPoint() self.__fileStatisticsRole = Qt.UserRole self.__totalStatisticsRole = Qt.UserRole + 1 self.shelveList.header().setSortIndicator(0, Qt.AscendingOrder) self.refreshButton = self.buttonBox.addButton( self.tr("&Refresh"), QDialogButtonBox.ActionRole) self.refreshButton.setToolTip( self.tr("Press to refresh the list of shelves")) self.refreshButton.setEnabled(False) self.vcs = vcs self.__hgClient = vcs.getClient() self.__resetUI() if self.__hgClient: self.process = None else: self.process = QProcess() self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.__contextMenu = QMenu() self.__unshelveAct = self.__contextMenu.addAction( self.tr("Restore selected shelve"), self.__unshelve) self.__deleteAct = self.__contextMenu.addAction( self.tr("Delete selected shelves"), self.__deleteShelves) self.__contextMenu.addAction( self.tr("Delete all shelves"), self.__cleanupShelves) def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.__hgClient: if self.__hgClient.isExecuting(): self.__hgClient.cancel() else: if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.__position = self.pos() e.accept() def show(self): """ Public slot to show the dialog. """ if not self.__position.isNull(): self.move(self.__position) self.__resetUI() super(HgShelveBrowserDialog, self).show() def __resetUI(self): """ Private method to reset the user interface. """ self.shelveList.clear() def __resizeColumnsShelves(self): """ Private method to resize the shelve list columns. """ self.shelveList.header().resizeSections(QHeaderView.ResizeToContents) self.shelveList.header().setStretchLastSection(True) def __generateShelveEntry(self, name, age, message, fileStatistics, totals): """ Private method to generate the shelve items. @param name name of the shelve (string) @param age age of the shelve (string) @param message shelve message (string) @param fileStatistics per file change statistics (tuple of four strings with file name, number of changes, number of added lines and number of deleted lines) @param totals overall statistics (tuple of three strings with number of changed files, number of added lines and number of deleted lines) """ itm = QTreeWidgetItem(self.shelveList, [name, age, message]) itm.setData(0, self.__fileStatisticsRole, fileStatistics) itm.setData(0, self.__totalStatisticsRole, totals) def __getShelveEntries(self): """ Private method to retrieve the list of shelves. """ self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) QApplication.processEvents() QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) QApplication.processEvents() self.buf = [] self.errors.clear() self.intercept = False args = self.vcs.initCommand("shelve") args.append("--list") args.append("--stat") if self.__hgClient: self.inputGroup.setEnabled(False) self.inputGroup.hide() out, err = self.__hgClient.runcommand(args) self.buf = out.splitlines(True) if err: self.__showError(err) self.__processBuffer() self.__finish() else: self.process.kill() self.process.setWorkingDirectory(self.repodir) self.inputGroup.setEnabled(True) self.inputGroup.show() self.process.start('hg', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('hg')) def start(self, projectDir): """ Public slot to start the hg shelve command. @param projectDir name of the project directory (string) """ self.errorGroup.hide() QApplication.processEvents() self.__projectDir = projectDir # find the root of the repo self.repodir = self.__projectDir while not os.path.isdir(os.path.join(self.repodir, self.vcs.adminDir)): self.repodir = os.path.dirname(self.repodir) if os.path.splitdrive(self.repodir)[1] == os.sep: return self.activateWindow() self.raise_() self.shelveList.clear() self.__started = True self.__getShelveEntries() def __procFinished(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.__processBuffer() self.__finish() def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) QApplication.restoreOverrideCursor() self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.inputGroup.setEnabled(False) self.inputGroup.hide() self.refreshButton.setEnabled(True) def __processBuffer(self): """ Private method to process the buffered output of the hg shelve command. """ lastWasFileStats = False firstLine = True itemData = {} for line in self.buf: if firstLine: name, line = line.split("(", 1) age, message = line.split(")", 1) itemData["name"] = name.strip() itemData["age"] = age.strip() itemData["message"] = message.strip() itemData["files"] = [] firstLine = False elif '|' in line: # file stats: foo.py | 3 ++- file, changes = line.strip().split("|", 1) if changes.strip().endswith(("+", "-")): total, addDelete = changes.strip().split(None, 1) additions = str(addDelete.count("+")) deletions = str(addDelete.count("-")) else: total = changes.strip() additions = '0' deletions = '0' itemData["files"].append((file, total, additions, deletions)) lastWasFileStats = True elif lastWasFileStats: # summary line # 2 files changed, 15 insertions(+), 1 deletions(-) total, added, deleted = line.strip().split(",", 2) total = total.split()[0] added = added.split()[0] deleted = deleted.split()[0] itemData["summary"] = (total, added, deleted) self.__generateShelveEntry( itemData["name"], itemData["age"], itemData["message"], itemData["files"], itemData["summary"]) lastWasFileStats = False firstLine = True itemData = {} self.__resizeColumnsShelves() if self.__started: self.shelveList.setCurrentItem(self.shelveList.topLevelItem(0)) self.__started = False def __readStdout(self): """ Private slot to handle the readyReadStandardOutput signal. It reads the output of the process and inserts it into a buffer. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): line = str(self.process.readLine(), self.vcs.getEncoding(), 'replace') self.buf.append(line) def __readStderr(self): """ Private slot to handle the readyReadStandardError signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: s = str(self.process.readAllStandardError(), self.vcs.getEncoding(), 'replace') self.__showError(s) def __showError(self, out): """ Private slot to show some error. @param out error to be shown (string) """ self.errorGroup.show() self.errors.insertPlainText(out) self.errors.ensureCursorVisible() @pyqtSlot(QAbstractButton) def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): self.cancelled = True if self.__hgClient: self.__hgClient.cancel() else: self.__finish() elif button == self.refreshButton: self.on_refreshButton_clicked() @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) def on_shelveList_currentItemChanged(self, current, previous): """ Private slot called, when the current item of the shelve list changes. @param current reference to the new current item (QTreeWidgetItem) @param previous reference to the old current item (QTreeWidgetItem) """ self.statisticsList.clear() if current: for dataSet in current.data(0, self.__fileStatisticsRole): QTreeWidgetItem(self.statisticsList, list(dataSet)) self.statisticsList.header().resizeSections( QHeaderView.ResizeToContents) self.statisticsList.header().setStretchLastSection(True) totals = current.data(0, self.__totalStatisticsRole) self.filesLabel.setText( self.tr("%n file(s) changed", None, int(totals[0]))) self.insertionsLabel.setText( self.tr("%n line(s) inserted", None, int(totals[1]))) self.deletionsLabel.setText( self.tr("%n line(s) deleted", None, int(totals[2]))) else: self.filesLabel.setText("") self.insertionsLabel.setText("") self.deletionsLabel.setText("") @pyqtSlot(QPoint) def on_shelveList_customContextMenuRequested(self, pos): """ Private slot to show the context menu of the shelve list. @param pos position of the mouse pointer (QPoint) """ selectedItemsCount = len(self.shelveList.selectedItems()) self.__unshelveAct.setEnabled(selectedItemsCount == 1) self.__deleteAct.setEnabled(selectedItemsCount > 0) self.__contextMenu.popup(self.mapToGlobal(pos)) @pyqtSlot() def on_refreshButton_clicked(self): """ Private slot to refresh the list of shelves. """ self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.inputGroup.setEnabled(True) self.inputGroup.show() self.refreshButton.setEnabled(False) self.start(self.__projectDir) def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the mercurial process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.errorGroup.show() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(HgShelveBrowserDialog, self).keyPressEvent(evt) def __unshelve(self): """ Private slot to restore the selected shelve of changes. """ itm = self.shelveList.selectedItems()[0] if itm is not None: name = itm.text(self.NameColumn) self.vcs.getExtensionObject("shelve")\ .hgUnshelve(self.__projectDir, shelveName=name) self.on_refreshButton_clicked() def __deleteShelves(self): """ Private slot to delete the selected shelves. """ shelveNames = [] for itm in self.shelveList.selectedItems(): shelveNames.append(itm.text(self.NameColumn)) if shelveNames: self.vcs.getExtensionObject("shelve")\ .hgDeleteShelves(self.__projectDir, shelveNames=shelveNames) self.on_refreshButton_clicked() def __cleanupShelves(self): """ Private slot to delete all shelves. """ self.vcs.getExtensionObject("shelve")\ .hgCleanupShelves(self.__projectDir) self.on_refreshButton_clicked()
class SvnPropListDialog(QWidget, Ui_SvnPropListDialog): """ Class implementing a dialog to show the output of the svn proplist command process. """ def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent parent widget (QWidget) """ super(SvnPropListDialog, self).__init__(parent) self.setupUi(self) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.process = QProcess() env = QProcessEnvironment.systemEnvironment() env.insert("LANG", "C") self.process.setProcessEnvironment(env) self.vcs = vcs self.propsList.headerItem().setText(self.propsList.columnCount(), "") self.propsList.header().setSortIndicator(0, Qt.AscendingOrder) self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.rx_path = QRegExp(r"Properties on '([^']+)':\s*") self.rx_prop = QRegExp(r" (.*) *: *(.*)[\r\n]") self.lastPath = None self.lastProp = None self.propBuffer = "" def __resort(self): """ Private method to resort the tree. """ self.propsList.sortItems( self.propsList.sortColumn(), self.propsList.header().sortIndicatorOrder()) def __resizeColumns(self): """ Private method to resize the list columns. """ self.propsList.header().resizeSections(QHeaderView.ResizeToContents) self.propsList.header().setStretchLastSection(True) def __generateItem(self, path, propName, propValue): """ Private method to generate a properties item in the properties list. @param path file/directory name the property applies to (string) @param propName name of the property (string) @param propValue value of the property (string) """ QTreeWidgetItem(self.propsList, [path, propName, propValue.strip()]) def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def start(self, fn, recursive=False): """ Public slot to start the svn status command. @param fn filename(s) (string or list of string) @param recursive flag indicating a recursive list is requested """ self.errorGroup.hide() self.process.kill() args = [] args.append('proplist') self.vcs.addArguments(args, self.vcs.options['global']) args.append('--verbose') if recursive: args.append('--recursive') if isinstance(fn, list): dname, fnames = self.vcs.splitPathList(fn) self.vcs.addArguments(args, fnames) else: dname, fname = self.vcs.splitPath(fn) args.append(fname) self.process.setWorkingDirectory(dname) self.process.start('svn', args) procStarted = self.process.waitForStarted(5000) if not procStarted: E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('svn')) def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.process = None if self.lastProp: self.__generateItem(self.lastPath, self.lastProp, self.propBuffer) self.__resort() self.__resizeColumns() def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): self.__finish() def __procFinished(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) """ if self.lastPath is None: self.__generateItem('', 'None', '') self.__finish() def __readStdout(self): """ Private slot to handle the readyReadStandardOutput signal. It reads the output of the process, formats it and inserts it into the contents pane. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): s = str(self.process.readLine(), Preferences.getSystem("IOEncoding"), 'replace') if self.rx_path.exactMatch(s): if self.lastProp: self.__generateItem( self.lastPath, self.lastProp, self.propBuffer) self.lastPath = self.rx_path.cap(1) self.lastProp = None self.propBuffer = "" elif self.rx_prop.exactMatch(s): if self.lastProp: self.__generateItem( self.lastPath, self.lastProp, self.propBuffer) self.lastProp = self.rx_prop.cap(1) self.propBuffer = self.rx_prop.cap(2) else: self.propBuffer += ' ' self.propBuffer += s def __readStderr(self): """ Private slot to handle the readyReadStandardError signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: self.errorGroup.show() s = str(self.process.readAllStandardError(), Preferences.getSystem("IOEncoding"), 'replace') self.errors.insertPlainText(s) self.errors.ensureCursorVisible()
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 SvnTagBranchListDialog(QDialog, Ui_SvnTagBranchListDialog): """ Class implementing a dialog to show a list of tags or branches. """ def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent parent widget (QWidget) """ super(SvnTagBranchListDialog, self).__init__(parent) self.setupUi(self) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.process = QProcess() self.vcs = vcs self.tagsList = None self.allTagsList = None self.tagList.headerItem().setText(self.tagList.columnCount(), "") self.tagList.header().setSortIndicator(3, Qt.AscendingOrder) self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.rx_list = QRegExp( r"""\w*\s*(\d+)\s+(\w+)\s+\d*\s*""" r"""((?:\w+\s+\d+|[0-9.]+\s+\w+)\s+[0-9:]+)\s+(.+)/\s*""") def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def start(self, path, tags, tagsList, allTagsList): """ Public slot to start the svn status command. @param path name of directory to be listed (string) @param tags flag indicating a list of tags is requested (False = branches, True = tags) @param tagsList reference to string list receiving the tags (list of strings) @param allTagsList reference to string list all tags (list of strings) """ self.errorGroup.hide() self.intercept = False if not tags: self.setWindowTitle(self.tr("Subversion Branches List")) self.activateWindow() self.tagsList = tagsList self.allTagsList = allTagsList dname, fname = self.vcs.splitPath(path) self.process.kill() reposURL = self.vcs.svnGetReposName(dname) if reposURL is None: E5MessageBox.critical( self, self.tr("Subversion Error"), self.tr( """The URL of the project repository could not be""" """ retrieved from the working copy. The list operation""" """ will be aborted""")) self.close() return args = [] args.append('list') self.vcs.addArguments(args, self.vcs.options['global']) args.append('--verbose') if self.vcs.otherData["standardLayout"]: # determine the base path of the project in the repository rx_base = QRegExp('(.+)/(trunk|tags|branches).*') if not rx_base.exactMatch(reposURL): E5MessageBox.critical( self, self.tr("Subversion Error"), self.tr("""The URL of the project repository has an""" """ invalid format. The list operation will""" """ be aborted""")) return reposRoot = rx_base.cap(1) if tags: args.append("{0}/tags".format(reposRoot)) else: args.append("{0}/branches".format(reposRoot)) self.path = None else: reposPath, ok = QInputDialog.getText( self, self.tr("Subversion List"), self.tr("Enter the repository URL containing the tags" " or branches"), QLineEdit.Normal, self.vcs.svnNormalizeURL(reposURL)) if not ok: self.close() return if not reposPath: E5MessageBox.critical( self, self.tr("Subversion List"), self.tr("""The repository URL is empty.""" """ Aborting...""")) self.close() return args.append(reposPath) self.path = reposPath self.process.setWorkingDirectory(dname) self.process.start('svn', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.').format('svn')) else: self.inputGroup.setEnabled(True) self.inputGroup.show() def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.buttonBox.button(QDialogButtonBox.Close).setFocus( Qt.OtherFocusReason) self.inputGroup.setEnabled(False) self.inputGroup.hide() self.process = None self.__resizeColumns() self.__resort() def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): self.__finish() def __procFinished(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.__finish() def __resort(self): """ Private method to resort the tree. """ self.tagList.sortItems(self.tagList.sortColumn(), self.tagList.header().sortIndicatorOrder()) def __resizeColumns(self): """ Private method to resize the list columns. """ self.tagList.header().resizeSections(QHeaderView.ResizeToContents) self.tagList.header().setStretchLastSection(True) def __generateItem(self, revision, author, date, name): """ Private method to generate a tag item in the taglist. @param revision revision string (string) @param author author of the tag (string) @param date date of the tag (string) @param name name (path) of the tag (string) """ itm = QTreeWidgetItem(self.tagList) itm.setData(0, Qt.DisplayRole, int(revision)) itm.setData(1, Qt.DisplayRole, author) itm.setData(2, Qt.DisplayRole, date) itm.setData(3, Qt.DisplayRole, name) itm.setTextAlignment(0, Qt.AlignRight) def __readStdout(self): """ Private slot to handle the readyReadStdout signal. It reads the output of the process, formats it and inserts it into the contents pane. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): s = str(self.process.readLine(), Preferences.getSystem("IOEncoding"), 'replace') if self.rx_list.exactMatch(s): rev = "{0:6}".format(self.rx_list.cap(1)) author = self.rx_list.cap(2) date = self.rx_list.cap(3) path = self.rx_list.cap(4) if path == ".": continue self.__generateItem(rev, author, date, path) if not self.vcs.otherData["standardLayout"]: path = self.path + '/' + path if self.tagsList is not None: self.tagsList.append(path) if self.allTagsList is not None: self.allTagsList.append(path) def __readStderr(self): """ Private slot to handle the readyReadStderr signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: self.errorGroup.show() s = str(self.process.readAllStandardError(), Preferences.getSystem("IOEncoding"), 'replace') self.errors.insertPlainText(s) self.errors.ensureCursorVisible() def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the subversion process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(SvnTagBranchListDialog, self).keyPressEvent(evt)
class SvnRepoBrowserDialog(QDialog, Ui_SvnRepoBrowserDialog): """ Class implementing the subversion repository browser dialog. """ def __init__(self, vcs, mode="browse", parent=None): """ Constructor @param vcs reference to the vcs object @param mode mode of the dialog (string, "browse" or "select") @param parent parent widget (QWidget) """ super(SvnRepoBrowserDialog, self).__init__(parent) self.setupUi(self) self.setWindowFlags(Qt.Window) self.repoTree.headerItem().setText(self.repoTree.columnCount(), "") self.repoTree.header().setSortIndicator(0, Qt.AscendingOrder) self.vcs = vcs self.mode = mode self.process = QProcess() self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) if self.mode == "select": self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).hide() else: self.buttonBox.button(QDialogButtonBox.Ok).hide() self.buttonBox.button(QDialogButtonBox.Cancel).hide() self.__dirIcon = UI.PixmapCache.getIcon("dirClosed.png") self.__fileIcon = UI.PixmapCache.getIcon("fileMisc.png") self.__urlRole = Qt.UserRole self.__ignoreExpand = False self.intercept = False self.__rx_dir = QRegExp( r"""\s*([0-9]+)\s+(\w+)\s+""" r"""((?:\w+\s+\d+|[0-9.]+\s+\w+)\s+[0-9:]+)\s+(.+)\s*""") self.__rx_file = QRegExp( r"""\s*([0-9]+)\s+(\w+)\s+([0-9]+)\s""" r"""((?:\w+\s+\d+|[0-9.]+\s+\w+)\s+[0-9:]+)\s+(.+)\s*""") def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def __resort(self): """ Private method to resort the tree. """ self.repoTree.sortItems( self.repoTree.sortColumn(), self.repoTree.header().sortIndicatorOrder()) def __resizeColumns(self): """ Private method to resize the tree columns. """ self.repoTree.header().resizeSections(QHeaderView.ResizeToContents) self.repoTree.header().setStretchLastSection(True) def __generateItem(self, repopath, revision, author, size, date, nodekind, url): """ Private method to generate a tree item in the repository tree. @param repopath path of the item (string) @param revision revision info (string) @param author author info (string) @param size size info (string) @param date date info (string) @param nodekind node kind info (string, "dir" or "file") @param url url of the entry (string) @return reference to the generated item (QTreeWidgetItem) """ path = repopath if revision == "": rev = "" else: rev = int(revision) if size == "": sz = "" else: sz = int(size) itm = QTreeWidgetItem(self.parentItem) itm.setData(0, Qt.DisplayRole, path) itm.setData(1, Qt.DisplayRole, rev) itm.setData(2, Qt.DisplayRole, author) itm.setData(3, Qt.DisplayRole, sz) itm.setData(4, Qt.DisplayRole, date) if nodekind == "dir": itm.setIcon(0, self.__dirIcon) itm.setChildIndicatorPolicy(QTreeWidgetItem.ShowIndicator) elif nodekind == "file": itm.setIcon(0, self.__fileIcon) itm.setData(0, self.__urlRole, url) itm.setTextAlignment(0, Qt.AlignLeft) itm.setTextAlignment(1, Qt.AlignRight) itm.setTextAlignment(2, Qt.AlignLeft) itm.setTextAlignment(3, Qt.AlignRight) itm.setTextAlignment(4, Qt.AlignLeft) return itm def __repoRoot(self, url): """ Private method to get the repository root using the svn info command. @param url the repository URL to browser (string) @return repository root (string) """ ioEncoding = Preferences.getSystem("IOEncoding") repoRoot = None process = QProcess() args = [] args.append('info') self.vcs.addArguments(args, self.vcs.options['global']) args.append('--xml') args.append(url) process.start('svn', args) procStarted = process.waitForStarted(5000) if procStarted: finished = process.waitForFinished(30000) if finished: if process.exitCode() == 0: output = str(process.readAllStandardOutput(), ioEncoding, 'replace') for line in output.splitlines(): line = line.strip() if line.startswith('<root>'): repoRoot = line.replace('<root>', '')\ .replace('</root>', '') break else: error = str(process.readAllStandardError(), Preferences.getSystem("IOEncoding"), 'replace') self.errors.insertPlainText(error) self.errors.ensureCursorVisible() else: QApplication.restoreOverrideCursor() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('svn')) return repoRoot def __listRepo(self, url, parent=None): """ Private method to perform the svn list command. @param url the repository URL to browse (string) @param parent reference to the item, the data should be appended to (QTreeWidget or QTreeWidgetItem) """ self.errorGroup.hide() QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) QApplication.processEvents() self.repoUrl = url if parent is None: self.parentItem = self.repoTree else: self.parentItem = parent if self.parentItem == self.repoTree: repoRoot = self.__repoRoot(url) if repoRoot is None: self.__finish() return self.__ignoreExpand = True itm = self.__generateItem( repoRoot, "", "", "", "", "dir", repoRoot) itm.setExpanded(True) self.parentItem = itm urlPart = repoRoot for element in url.replace(repoRoot, "").split("/"): if element: urlPart = "{0}/{1}".format(urlPart, element) itm = self.__generateItem( element, "", "", "", "", "dir", urlPart) itm.setExpanded(True) self.parentItem = itm itm.setExpanded(False) self.__ignoreExpand = False self.__finish() return self.intercept = False self.process.kill() args = [] args.append('list') self.vcs.addArguments(args, self.vcs.options['global']) if '--verbose' not in self.vcs.options['global']: args.append('--verbose') args.append(url) self.process.start('svn', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.__finish() self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('svn')) else: self.inputGroup.setEnabled(True) self.inputGroup.show() def __normalizeUrl(self, url): """ Private method to normalite the url. @param url the url to normalize (string) @return normalized URL (string) """ if url.endswith("/"): return url[:-1] return url def start(self, url): """ Public slot to start the svn info command. @param url the repository URL to browser (string) """ self.repoTree.clear() self.url = "" url = self.__normalizeUrl(url) if self.urlCombo.findText(url) == -1: self.urlCombo.addItem(url) @pyqtSlot(str) def on_urlCombo_currentIndexChanged(self, text): """ Private slot called, when a new repository URL is entered or selected. @param text the text of the current item (string) """ url = self.__normalizeUrl(text) if url != self.url: self.url = url self.repoTree.clear() self.__listRepo(url) @pyqtSlot(QTreeWidgetItem) def on_repoTree_itemExpanded(self, item): """ Private slot called when an item is expanded. @param item reference to the item to be expanded (QTreeWidgetItem) """ if not self.__ignoreExpand: url = item.data(0, self.__urlRole) self.__listRepo(url, item) @pyqtSlot(QTreeWidgetItem) def on_repoTree_itemCollapsed(self, item): """ Private slot called when an item is collapsed. @param item reference to the item to be collapsed (QTreeWidgetItem) """ for child in item.takeChildren(): del child @pyqtSlot() def on_repoTree_itemSelectionChanged(self): """ Private slot called when the selection changes. """ if self.mode == "select": self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(True) def accept(self): """ Public slot called when the dialog is accepted. """ if self.focusWidget() == self.urlCombo: return super(SvnRepoBrowserDialog, self).accept() def getSelectedUrl(self): """ Public method to retrieve the selected repository URL. @return the selected repository URL (string) """ items = self.repoTree.selectedItems() if len(items) == 1: return items[0].data(0, self.__urlRole) else: return "" def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.inputGroup.setEnabled(False) self.inputGroup.hide() self.__resizeColumns() self.__resort() QApplication.restoreOverrideCursor() def __procFinished(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.__finish() def __readStdout(self): """ Private slot to handle the readyReadStandardOutput signal. It reads the output of the process, formats it and inserts it into the contents pane. """ if self.process is not None: self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): s = str(self.process.readLine(), Preferences.getSystem("IOEncoding"), 'replace') if self.__rx_dir.exactMatch(s): revision = self.__rx_dir.cap(1) author = self.__rx_dir.cap(2) date = self.__rx_dir.cap(3) name = self.__rx_dir.cap(4).strip() if name.endswith("/"): name = name[:-1] size = "" nodekind = "dir" if name == ".": continue elif self.__rx_file.exactMatch(s): revision = self.__rx_file.cap(1) author = self.__rx_file.cap(2) size = self.__rx_file.cap(3) date = self.__rx_file.cap(4) name = self.__rx_file.cap(5).strip() nodekind = "file" else: continue url = "{0}/{1}".format(self.repoUrl, name) self.__generateItem( name, revision, author, size, date, nodekind, url) def __readStderr(self): """ Private slot to handle the readyReadStandardError signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: s = str(self.process.readAllStandardError(), Preferences.getSystem("IOEncoding"), 'replace') self.errors.insertPlainText(s) self.errors.ensureCursorVisible() self.errorGroup.show() def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the subversion process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(SvnRepoBrowserDialog, self).keyPressEvent(evt)
class MyQProcess(QWidget): """creates a q process for running the classification""" def __init__(self,text,title): super().__init__() # Add the UI components (here we use a QTextEdit to display the stdout from the process) self.layout = QVBoxLayout() self.stoppush = QPushButton('Stop') self.edit = QTextEdit() self.label =QLabel() self.edit.setStyleSheet('font-size:12pt') self.setLayout(self.layout) self.cmd = None self.setGeometry(100,100, 800, 300) self.setMaximumHeight(300) self.setMaximumWidth(800) self.setWindowTitle(title) self.edit.setText(text) self.label.setText(text) self.args =None #self.show() # Add the process and start it self.process = QProcess() self.setupProcess() self.stoppush.clicked.connect(self.kill) self.process.finished.connect(lambda: self.edit.append('Done')) def setupProcess(self): # Set the channels self.process.setProcessChannelMode(QProcess.MergedChannels) # Connect the signal readyReadStandardOutput to the slot of the widget self.process.readyReadStandardOutput.connect(self.readStdOutput) def start_process2(self): """start the process with a poping Qlabel massage (used for: polygonize,rasterize,confusion matrix""" self.setGeometry(100, 100, 400, 300) self.layout.addWidget(self.label) self.layout.addWidget(self.stoppush) # Show the widget # print('starting process') self.process.start(self.cmd) # print('started') self.show() def start_process(self): """start the process with a poping Qedit that shows all the input (used for classification running""" self.layout.addWidget(self.edit) self.layout.addWidget(self.stoppush) # Show the widget self.show() # print('starting process') self.process.start(self.cmd) # print('started') def __del__(self): # If QApplication is closed attempt to kill the process self.process.terminate() self.edit.clear() self.label.clear() # Wait for Xms and then elevate the situation to terminate if not self.process.waitForFinished(10000): print('killing process') self.process.kill() self.edit.clear() self.label.clear() def kill(self): print('killing process') self.process.terminate() self.edit.clear() self.label.clear() self.close() def readStdOutput(self): # Every time the process has something to output we attach it to the QTextEdit #self.edit.setText(str(self.process.readAllStandardOutput())) self.edit.append(str(self.process.readAllStandardOutput())) def main(): app = QApplication(sys.argv) w = MyQProcess() return app.exec_()
class Process(QObject): """Abstraction over a running test subprocess process. Reads the log from its stdout and parses it. Attributes: _invalid: A list of lines which could not be parsed. _data: A list of parsed lines. _started: Whether the process was ever started. proc: The QProcess for the underlying process. exit_expected: Whether the process is expected to quit. request: The request object for the current test. Signals: ready: Emitted when the server finished starting up. new_data: Emitted when a new line was parsed. """ ready = pyqtSignal() new_data = pyqtSignal(object) KEYS = ['data'] def __init__(self, request, parent=None): super().__init__(parent) self.request = request self.captured_log = [] self._started = False self._invalid = [] self._data = [] self.proc = QProcess() self.proc.setReadChannel(QProcess.StandardError) self.exit_expected = None # Not started at all yet def _log(self, line): """Add the given line to the captured log output.""" if self.request.config.getoption('--capture') == 'no': print(line) self.captured_log.append(line) def log_summary(self, text): """Log the given line as summary/title.""" text = '\n{line} {text} {line}\n'.format(line='='*30, text=text) self._log(text) def _parse_line(self, line): """Parse the given line from the log. Return: A self.ParseResult member. """ raise NotImplementedError def _executable_args(self): """Get the executable and necessary arguments as a tuple.""" raise NotImplementedError def _default_args(self): """Get the default arguments to use if none were passed to start().""" raise NotImplementedError def _get_data(self): """Get the parsed data for this test. Also waits for 0.5s to make sure any new data is received. Subprocesses are expected to alias this to a public method with a better name. """ self.proc.waitForReadyRead(500) self.read_log() return self._data def _wait_signal(self, signal, timeout=5000, raising=True): """Wait for a signal to be emitted. Should be used in a contextmanager. """ blocker = pytestqt.plugin.SignalBlocker(timeout=timeout, raising=raising) blocker.connect(signal) return blocker @pyqtSlot() def read_log(self): """Read the log from the process' stdout.""" if not hasattr(self, 'proc'): # I have no idea how this happens, but it does... return while self.proc.canReadLine(): line = self.proc.readLine() line = bytes(line).decode('utf-8', errors='ignore').rstrip('\r\n') try: parsed = self._parse_line(line) except InvalidLine: self._invalid.append(line) self._log("INVALID: {}".format(line)) continue if parsed is None: if self._invalid: self._log("IGNORED: {}".format(line)) else: self._data.append(parsed) self.new_data.emit(parsed) def start(self, args=None, *, env=None): """Start the process and wait until it started.""" self._start(args, env=env) self._started = True verbose = self.request.config.getoption('--verbose') timeout = 60 if 'CI' in os.environ else 20 for _ in range(timeout): with self._wait_signal(self.ready, timeout=1000, raising=False) as blocker: pass if not self.is_running(): if self.exit_expected: return # _start ensures it actually started, but it might quit shortly # afterwards raise ProcessExited('\n' + _render_log(self.captured_log, verbose=verbose)) if blocker.signal_triggered: self._after_start() return raise WaitForTimeout("Timed out while waiting for process start.\n" + _render_log(self.captured_log, verbose=verbose)) def _start(self, args, env): """Actually start the process.""" executable, exec_args = self._executable_args() if args is None: args = self._default_args() procenv = QProcessEnvironment.systemEnvironment() if env is not None: for k, v in env.items(): procenv.insert(k, v) self.proc.readyRead.connect(self.read_log) self.proc.setProcessEnvironment(procenv) self.proc.start(executable, exec_args + args) ok = self.proc.waitForStarted() assert ok assert self.is_running() def _after_start(self): """Do things which should be done immediately after starting.""" def before_test(self): """Restart process before a test if it exited before.""" self._invalid = [] if not self.is_running(): self.start() def after_test(self): """Clean up data after each test. Also checks self._invalid so the test counts as failed if there were unexpected output lines earlier. """ __tracebackhide__ = lambda e: e.errisinstance(ProcessExited) self.captured_log = [] if self._invalid: # Wait for a bit so the full error has a chance to arrive time.sleep(1) # Exit the process to make sure we're in a defined state again self.terminate() self.clear_data() raise InvalidLine('\n' + '\n'.join(self._invalid)) self.clear_data() if not self.is_running() and not self.exit_expected and self._started: raise ProcessExited self.exit_expected = False def clear_data(self): """Clear the collected data.""" self._data.clear() def terminate(self): """Clean up and shut down the process.""" if not self.is_running(): return if quteutils.is_windows: self.proc.kill() else: self.proc.terminate() ok = self.proc.waitForFinished() assert ok def is_running(self): """Check if the process is currently running.""" return self.proc.state() == QProcess.Running def _match_data(self, value, expected): """Helper for wait_for to match a given value. The behavior of this method is slightly different depending on the types of the filtered values: - If expected is None, the filter always matches. - If the value is a string or bytes object and the expected value is too, the pattern is treated as a glob pattern (with only * active). - If the value is a string or bytes object and the expected value is a compiled regex, it is used for matching. - If the value is any other type, == is used. Return: A bool """ regex_type = type(re.compile('')) if expected is None: return True elif isinstance(expected, regex_type): return expected.search(value) elif isinstance(value, (bytes, str)): return utils.pattern_match(pattern=expected, value=value) else: return value == expected def _wait_for_existing(self, override_waited_for, after, **kwargs): """Check if there are any line in the history for wait_for. Return: either the found line or None. """ for line in self._data: matches = [] for key, expected in kwargs.items(): value = getattr(line, key) matches.append(self._match_data(value, expected)) if after is None: too_early = False else: too_early = ((line.timestamp, line.msecs) < (after.timestamp, after.msecs)) if (all(matches) and (not line.waited_for or override_waited_for) and not too_early): # If we waited for this line, chances are we don't mean the # same thing the next time we use wait_for and it matches # this line again. line.waited_for = True self._log("\n----> Already found {!r} in the log: {}".format( kwargs.get('message', 'line'), line)) return line return None def _wait_for_new(self, timeout, do_skip, **kwargs): """Wait for a log message which doesn't exist yet. Called via wait_for. """ __tracebackhide__ = lambda e: e.errisinstance(WaitForTimeout) message = kwargs.get('message', None) if message is not None: elided = quteutils.elide(repr(message), 100) self._log("\n----> Waiting for {} in the log".format(elided)) spy = QSignalSpy(self.new_data) elapsed_timer = QElapsedTimer() elapsed_timer.start() while True: # Skip if there are pending messages causing a skip self._maybe_skip() got_signal = spy.wait(timeout) if not got_signal or elapsed_timer.hasExpired(timeout): msg = "Timed out after {}ms waiting for {!r}.".format( timeout, kwargs) if do_skip: pytest.skip(msg) else: raise WaitForTimeout(msg) match = self._wait_for_match(spy, kwargs) if match is not None: if message is not None: self._log("----> found it") return match raise quteutils.Unreachable def _wait_for_match(self, spy, kwargs): """Try matching the kwargs with the given QSignalSpy.""" for args in spy: assert len(args) == 1 line = args[0] matches = [] for key, expected in kwargs.items(): value = getattr(line, key) matches.append(self._match_data(value, expected)) if all(matches): # If we waited for this line, chances are we don't mean the # same thing the next time we use wait_for and it matches # this line again. line.waited_for = True return line return None def _maybe_skip(self): """Can be overridden by subclasses to skip on certain log lines. We can't run pytest.skip directly while parsing the log, as that would lead to a pytest.skip.Exception error in a virtual Qt method, which means pytest-qt fails the test. Instead, we check for skip messages periodically in QuteProc._maybe_skip, and call _maybe_skip after every parsed message in wait_for (where it's most likely that new messages arrive). """ def wait_for(self, timeout=None, *, override_waited_for=False, do_skip=False, divisor=1, after=None, **kwargs): """Wait until a given value is found in the data. Keyword arguments to this function get interpreted as attributes of the searched data. Every given argument is treated as a pattern which the attribute has to match against. Args: timeout: How long to wait for the message. override_waited_for: If set, gets triggered by previous messages again. do_skip: If set, call pytest.skip on a timeout. divisor: A factor to decrease the timeout by. after: If it's an existing line, ensure it's after the given one. Return: The matched line. """ __tracebackhide__ = lambda e: e.errisinstance(WaitForTimeout) if timeout is None: if do_skip: timeout = 2000 elif 'CI' in os.environ: timeout = 15000 else: timeout = 5000 timeout /= divisor if not kwargs: raise TypeError("No keyword arguments given!") for key in kwargs: assert key in self.KEYS existing = self._wait_for_existing(override_waited_for, after, **kwargs) if existing is not None: return existing else: return self._wait_for_new(timeout=timeout, do_skip=do_skip, **kwargs) def ensure_not_logged(self, delay=500, **kwargs): """Make sure the data matching the given arguments is not logged. If nothing is found in the log, we wait for delay ms to make sure nothing arrives. """ __tracebackhide__ = lambda e: e.errisinstance(BlacklistedMessageError) try: line = self.wait_for(timeout=delay, override_waited_for=True, **kwargs) except WaitForTimeout: return else: raise BlacklistedMessageError(line) def wait_for_quit(self): """Wait until the process has quit.""" self.exit_expected = True with self._wait_signal(self.proc.finished, timeout=15000): pass
class SvnTagBranchListDialog(QDialog, Ui_SvnTagBranchListDialog): """ Class implementing a dialog to show a list of tags or branches. """ def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent parent widget (QWidget) """ super(SvnTagBranchListDialog, self).__init__(parent) self.setupUi(self) self.setWindowFlags(Qt.Window) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.vcs = vcs self.tagsList = None self.allTagsList = None self.tagList.headerItem().setText(self.tagList.columnCount(), "") self.tagList.header().setSortIndicator(3, Qt.AscendingOrder) self.process = QProcess() self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.rx_list = QRegExp( r"""\w*\s*(\d+)\s+(\w+)\s+\d*\s*""" r"""((?:\w+\s+\d+|[0-9.]+\s+\w+)\s+[0-9:]+)\s+(.+)/\s*""") def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) e.accept() def start(self, path, tags, tagsList, allTagsList): """ Public slot to start the svn status command. @param path name of directory to be listed (string) @param tags flag indicating a list of tags is requested (False = branches, True = tags) @param tagsList reference to string list receiving the tags (list of strings) @param allTagsList reference to string list all tags (list of strings) """ self.errorGroup.hide() self.tagList.clear() self.intercept = False if not tags: self.setWindowTitle(self.tr("Subversion Branches List")) self.activateWindow() self.tagsList = tagsList self.allTagsList = allTagsList dname, fname = self.vcs.splitPath(path) self.process.kill() reposURL = self.vcs.svnGetReposName(dname) if reposURL is None: E5MessageBox.critical( self, self.tr("Subversion Error"), self.tr( """The URL of the project repository could not be""" """ retrieved from the working copy. The list operation""" """ will be aborted""")) self.close() return args = [] args.append('list') self.vcs.addArguments(args, self.vcs.options['global']) args.append('--verbose') if self.vcs.otherData["standardLayout"]: # determine the base path of the project in the repository rx_base = QRegExp('(.+)/(trunk|tags|branches).*') if not rx_base.exactMatch(reposURL): E5MessageBox.critical( self, self.tr("Subversion Error"), self.tr( """The URL of the project repository has an""" """ invalid format. The list operation will""" """ be aborted""")) return reposRoot = rx_base.cap(1) if tags: args.append("{0}/tags".format(reposRoot)) else: args.append("{0}/branches".format(reposRoot)) self.path = None else: reposPath, ok = QInputDialog.getText( self, self.tr("Subversion List"), self.tr("Enter the repository URL containing the tags" " or branches"), QLineEdit.Normal, self.vcs.svnNormalizeURL(reposURL)) if not ok: self.close() return if not reposPath: E5MessageBox.critical( self, self.tr("Subversion List"), self.tr("""The repository URL is empty.""" """ Aborting...""")) self.close() return args.append(reposPath) self.path = reposPath self.process.setWorkingDirectory(dname) self.process.start('svn', args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format('svn')) else: self.inputGroup.setEnabled(True) self.inputGroup.show() def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.buttonBox.button(QDialogButtonBox.Close).setFocus( Qt.OtherFocusReason) self.inputGroup.setEnabled(False) self.inputGroup.hide() self.__resizeColumns() self.__resort() def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): self.__finish() def __procFinished(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.__finish() def __resort(self): """ Private method to resort the tree. """ self.tagList.sortItems( self.tagList.sortColumn(), self.tagList.header().sortIndicatorOrder()) def __resizeColumns(self): """ Private method to resize the list columns. """ self.tagList.header().resizeSections(QHeaderView.ResizeToContents) self.tagList.header().setStretchLastSection(True) def __generateItem(self, revision, author, date, name): """ Private method to generate a tag item in the taglist. @param revision revision string (string) @param author author of the tag (string) @param date date of the tag (string) @param name name (path) of the tag (string) """ itm = QTreeWidgetItem(self.tagList) itm.setData(0, Qt.DisplayRole, int(revision)) itm.setData(1, Qt.DisplayRole, author) itm.setData(2, Qt.DisplayRole, date) itm.setData(3, Qt.DisplayRole, name) itm.setTextAlignment(0, Qt.AlignRight) def __readStdout(self): """ Private slot to handle the readyReadStdout signal. It reads the output of the process, formats it and inserts it into the contents pane. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): s = str(self.process.readLine(), Preferences.getSystem("IOEncoding"), 'replace') if self.rx_list.exactMatch(s): rev = "{0:6}".format(self.rx_list.cap(1)) author = self.rx_list.cap(2) date = self.rx_list.cap(3) path = self.rx_list.cap(4) if path == ".": continue self.__generateItem(rev, author, date, path) if not self.vcs.otherData["standardLayout"]: path = self.path + '/' + path if self.tagsList is not None: self.tagsList.append(path) if self.allTagsList is not None: self.allTagsList.append(path) def __readStderr(self): """ Private slot to handle the readyReadStderr signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: self.errorGroup.show() s = str(self.process.readAllStandardError(), Preferences.getSystem("IOEncoding"), 'replace') self.errors.insertPlainText(s) self.errors.ensureCursorVisible() def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSlot() def on_sendButton_clicked(self): """ Private slot to send the input to the subversion process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return super(SvnTagBranchListDialog, self).keyPressEvent(evt)
def _performMonitor(self): """ Protected method implementing the monitoring action. This method populates the statusList member variable with a list of strings giving the status in the first column and the path relative to the project directory starting with the third column. The allowed status flags are: <ul> <li>"A" path was added but not yet comitted</li> <li>"M" path has local changes</li> <li>"O" path was removed</li> <li>"R" path was deleted and then re-added</li> <li>"U" path needs an update</li> <li>"Z" path contains a conflict</li> <li>" " path is back at normal</li> </ul> @return tuple of flag indicating successful operation (boolean) and a status message in case of non successful operation (string) """ self.shouldUpdate = False if self.__client is None and not self.__useCommandLine: if self.vcs.version >= (2, 9, 9): # versions below that have a bug causing a second # instance to not recognize changes to the status from .HgClient import HgClient client = HgClient(self.projectDir, "utf-8", self.vcs) ok, err = client.startServer() if ok: self.__client = client else: self.__useCommandLine = True else: self.__useCommandLine = True # step 1: get overall status args = self.vcs.initCommand("status") args.append('--noninteractive') args.append('--all') output = "" error = "" if self.__client: output, error = self.__client.runcommand(args) else: process = QProcess() process.setWorkingDirectory(self.projectDir) process.start('hg', args) procStarted = process.waitForStarted(5000) if procStarted: finished = process.waitForFinished(300000) if finished and process.exitCode() == 0: output = str(process.readAllStandardOutput(), self.vcs.getEncoding(), 'replace') else: process.kill() process.waitForFinished() error = str(process.readAllStandardError(), self.vcs.getEncoding(), 'replace') else: process.kill() process.waitForFinished() error = self.tr("Could not start the Mercurial process.") if error: return False, error states = {} for line in output.splitlines(): if not line.startswith(" "): flag, name = line.split(" ", 1) if flag in "AMR": if flag == "R": status = "O" else: status = flag states[name] = status # step 2: get conflicting changes args = self.vcs.initCommand("resolve") args.append('--list') output = "" error = "" if self.__client: output, error = self.__client.runcommand(args) else: process.setWorkingDirectory(self.projectDir) process.start('hg', args) procStarted = process.waitForStarted(5000) if procStarted: finished = process.waitForFinished(300000) if finished and process.exitCode() == 0: output = str(process.readAllStandardOutput(), self.vcs.getEncoding(), 'replace') for line in output.splitlines(): flag, name = line.split(" ", 1) if flag == "U": states[name] = "Z" # conflict # step 3: collect the status to be reported back for name in states: try: if self.reportedStates[name] != states[name]: self.statusList.append( "{0} {1}".format(states[name], name)) except KeyError: self.statusList.append("{0} {1}".format(states[name], name)) for name in self.reportedStates.keys(): if name not in states: self.statusList.append(" {0}".format(name)) self.reportedStates = states return True, \ self.tr("Mercurial status checked successfully")