# cmdui.py - A widget to execute Mercurial command for TortoiseHg## Copyright 2010 Yuki KODAMA <endflow.net@gmail.com>## This software may be used and distributed according to the terms of the# GNU General Public License version 2, incorporated herein by reference.importos,sys,timefromPyQt4.QtCoreimport*fromPyQt4.QtGuiimport*fromPyQt4.QsciimportQsciScintillafromtortoisehg.utilimporthglib,pathsfromtortoisehg.hgqt.i18nimport_,localgettextfromtortoisehg.hgqtimportqtlib,qscilib,threadlocal=localgettext()defstartProgress(topic,status):topic,item,pos,total,unit=topic,'...',status,None,''return(topic,pos,item,unit,total)defstopProgress(topic):topic,item,pos,total,unit=topic,'',None,None,''return(topic,pos,item,unit,total)classProgressMonitor(QWidget):'Progress bar for use in workbench status bar'def__init__(self,topic,parent):super(ProgressMonitor,self).__init__(parent=parent)hbox=QHBoxLayout()hbox.setContentsMargins(*(0,)*4)self.setLayout(hbox)self.idle=Falseself.pbar=QProgressBar()self.pbar.setTextVisible(False)self.pbar.setMinimum(0)hbox.addWidget(self.pbar)self.topic=QLabel(topic)hbox.addWidget(self.topic,0)self.status=QLabel()hbox.addWidget(self.status,1)self.pbar.setMaximum(100)self.pbar.reset()self.status.setText('')defclear(self):self.pbar.setMinimum(0)self.pbar.setMaximum(100)self.pbar.setValue(100)self.status.setText('')self.idle=Truedefsetcounts(self,cur,max):self.pbar.setMaximum(max)self.pbar.setValue(cur)defunknown(self):self.pbar.setMinimum(0)self.pbar.setMaximum(0)classThgStatusBar(QStatusBar):linkActivated=pyqtSignal(QString)def__init__(self,parent=None):QStatusBar.__init__(self,parent=parent)self.topics={}self.lbl=QLabel()self.lbl.linkActivated.connect(self.linkActivated)self.addWidget(self.lbl)self.setStyleSheet('QStatusBar::item { border: none }')@pyqtSlot(unicode)defshowMessage(self,ustr,error=False):self.lbl.setText(ustr)iferror:self.lbl.setStyleSheet('QLabel { color: red }')else:self.lbl.setStyleSheet('')defclear(self):keys=self.topics.keys()forkeyinkeys:pm=self.topics[key]self.removeWidget(pm)delself.topics[key]@pyqtSlot(QString,object,QString,QString,object)defprogress(self,topic,pos,item,unit,total,root=None):'Progress signal received from repowidget'# topic is current operation# pos is the current numeric position (revision, bytes)# item is a non-numeric marker of current position (current file)# unit is a string label# total is the highest expected pos## All topics should be marked closed by setting pos to Noneifroot:key=(root,topic)else:key=topicifposisNoneor(notposandnottotal):ifkeyinself.topics:pm=self.topics[key]self.removeWidget(pm)delself.topics[key]returnifkeynotinself.topics:pm=ProgressMonitor(topic,self)pm.setMaximumHeight(self.lbl.sizeHint().height())self.addWidget(pm)self.topics[key]=pmelse:pm=self.topics[key]iftotal:fmt='%s / %s '%(unicode(pos),unicode(total))ifunit:fmt+=unitpm.status.setText(fmt)pm.setcounts(pos,total)else:ifitem:item=item[-30:]pm.status.setText('%s%s'%(unicode(pos),item))pm.unknown()classCore(QObject):"""Core functionality for running Mercurial command. Do not attempt to instantiate and use this directly. """commandStarted=pyqtSignal()commandFinished=pyqtSignal(int)commandCanceling=pyqtSignal()output=pyqtSignal(QString,QString)progress=pyqtSignal(QString,object,QString,QString,object)def__init__(self,logWindow,parent):super(Core,self).__init__(parent)self.thread=Noneself.extproc=Noneself.stbar=Noneself.queue=[]self.rawoutlines=[]self.display=Noneself.useproc=FalseiflogWindow:self.outputLog=LogWidget()self.outputLog.installEventFilter(qscilib.KeyPressInterceptor(self))self.output.connect(self.outputLog.appendLog)### Public Methods ###defrun(self,cmdline,*cmdlines,**opts):'''Execute or queue Mercurial command'''self.display=opts.get('display')self.useproc=opts.get('useproc',False)self.queue.append(cmdline)iflen(cmdlines):self.queue.extend(cmdlines)ifself.useproc:self.runproc()elifnotself.running():self.runNext()defcancel(self):'''Cancel running Mercurial command'''ifself.running():try:ifself.extproc:self.extproc.close()elifself.thread:self.thread.abort()exceptAttributeError:passself.commandCanceling.emit()defsetStbar(self,stbar):self.stbar=stbardefrunning(self):try:ifself.extproc:returnself.extproc.state()!=QProcess.NotRunningelifself.thread:returnself.thread.isRunning()exceptAttributeError:passreturnFalsedefrawoutput(self):return''.join(self.rawoutlines)### Private Method ###defrunproc(self):'Run mercurial command in separate process'exepath=Noneifhasattr(sys,'frozen'):progdir=paths.get_prog_root()exe=os.path.join(progdir,'hg.exe')ifos.path.exists(exe):exepath=exeifnotexepath:exepath=paths.find_in_path('hg')defstart(cmdline,display):self.rawoutlines=[]ifdisplay:cmd='%% hg %s\n'%displayelse:cmd='%% hg %s\n'%' '.join(cmdline)self.output.emit(cmd,'control')proc.start(exepath,cmdline,QIODevice.ReadOnly)@pyqtSlot(int)deffinished(ret):ifret:msg=_('[command returned code %d%%s]')%int(ret)else:msg=_('[command completed successfully %s]')msg=msg%time.asctime()+'\n'self.output.emit(msg,'control')ifret==0andself.queue:start(self.queue.pop(0),'')else:self.queue=[]self.extproc=Noneself.commandFinished.emit(ret)defhandleerror(error):iferror==QProcess.FailedToStart:self.output.emit(_('failed to start command\n'),'ui.error')finished(-1)eliferror!=QProcess.Crashed:self.output.emit(_('error while running command\n'),'ui.error')defstdout():data=proc.readAllStandardOutput().data()self.rawoutlines.append(data)self.output.emit(hglib.tounicode(data),'')defstderr():data=proc.readAllStandardError().data()self.output.emit(hglib.tounicode(data),'ui.error')self.extproc=proc=QProcess(self)proc.started.connect(self.onCommandStarted)proc.finished.connect(finished)proc.readyReadStandardOutput.connect(stdout)proc.readyReadStandardError.connect(stderr)proc.error.connect(handleerror)start(self.queue.pop(0),self.display)defrunNext(self):ifnotself.queue:returnFalsecmdline=self.queue.pop(0)self.thread=thread.CmdThread(cmdline,self.display,self.parent())self.thread.started.connect(self.onCommandStarted)self.thread.commandFinished.connect(self.onThreadFinished)self.thread.outputReceived.connect(self.output)self.thread.progressReceived.connect(self.progress)ifself.stbar:self.thread.progressReceived.connect(self.stbar.progress)self.thread.start()returnTruedefclearOutput(self):ifhasattr(self,'outputLog'):self.outputLog.clear()### Signal Handlers ###@pyqtSlot()defonCommandStarted(self):ifself.stbar:self.stbar.showMessage(_('Running...'))self.commandStarted.emit()@pyqtSlot(int)defonThreadFinished(self,ret):ifself.stbar:error=FalseifretisNone:self.stbar.clear()ifself.thread.abortbyuser:status=_('Terminated by user')else:status=_('Terminated')elifret==0:status=_('Finished')else:status=_('Failed!')error=Trueself.stbar.showMessage(status,error)self.display=Noneifret==0andself.runNext():return# run next commandelse:self.queue=[]text=self.thread.rawoutput.join('')self.rawoutlines=[hglib.fromunicode(text,'replace')]self.commandFinished.emit(ret)classLogWidget(QsciScintilla):"""Output log viewer"""def__init__(self,parent=None):super(LogWidget,self).__init__(parent)self.setReadOnly(True)self.setUtf8(True)self.setMarginWidth(1,0)self.setWrapMode(QsciScintilla.WrapCharacter)self._initfont()self._initmarkers()qscilib.unbindConflictedKeys(self)def_initfont(self):tf=qtlib.getfont('fontoutputlog')tf.changed.connect(self.forwardFont)self.setFont(tf.font())@pyqtSlot(QFont)defforwardFont(self,font):self.setFont(font)def_initmarkers(self):self._markers={}forlin('ui.error','control'):self._markers[l]=m=self.markerDefine(QsciScintilla.Background)c=QColor(qtlib.getbgcoloreffect(l))ifc.isValid():self.setMarkerBackgroundColor(c,m)# NOTE: self.setMarkerForegroundColor() doesn't take effect,# because it's a *Background* marker.@pyqtSlot(unicode,str)defappendLog(self,msg,label):"""Append log text to the last line; scrolls down to there"""self.append(msg)self._setmarker(xrange(self.lines()-unicode(msg).count('\n')-1,self.lines()-1),label)self.setCursorPosition(self.lines()-1,0)def_setmarker(self,lines,label):forminself._markersforlabel(label):foriinlines:self.markerAdd(i,m)def_markersforlabel(self,label):returniter(self._markers[l]forlinstr(label).split()iflinself._markers)classWidget(QWidget):"""An embeddable widget for running Mercurial command"""commandStarted=pyqtSignal()commandFinished=pyqtSignal(int)commandCanceling=pyqtSignal()output=pyqtSignal(QString,QString)progress=pyqtSignal(QString,object,QString,QString,object)makeLogVisible=pyqtSignal(bool)def__init__(self,logWindow,statusBar,parent):super(Widget,self).__init__(parent)self.core=Core(logWindow,self)self.core.commandStarted.connect(self.commandStarted)self.core.commandFinished.connect(self.onCommandFinished)self.core.commandCanceling.connect(self.commandCanceling)self.core.output.connect(self.output)self.core.progress.connect(self.progress)ifnotlogWindow:returnvbox=QVBoxLayout()vbox.setSpacing(4)vbox.setContentsMargins(*(1,)*4)self.setLayout(vbox)# command output areaself.core.outputLog.setHidden(True)self.layout().addWidget(self.core.outputLog,1)ifstatusBar:## status and progress labelsself.stbar=ThgStatusBar()self.stbar.setSizeGripEnabled(False)self.core.setStbar(self.stbar)self.layout().addWidget(self.stbar)### Public Methods ###defrun(self,cmdline,*args,**opts):self.core.run(cmdline,*args,**opts)defcancel(self):self.core.cancel()defsetShowOutput(self,visible):ifhasattr(self.core,'outputLog'):self.core.outputLog.setShown(visible)defoutputShown(self):ifhasattr(self.core,'outputLog'):returnself.core.outputLog.isVisible()else:returnFalse### Signal Handler ###@pyqtSlot(int)defonCommandFinished(self,ret):ifret==-1:self.makeLogVisible.emit(True)self.setShowOutput(True)self.commandFinished.emit(ret)classDialog(QDialog):"""A dialog for running random Mercurial command"""def__init__(self,cmdline,parent=None):super(Dialog,self).__init__(parent)self.setWindowFlags(self.windowFlags()&~Qt.WindowContextHelpButtonHint)self.core=Core(True,self)self.core.commandFinished.connect(self.onCommandFinished)vbox=QVBoxLayout()vbox.setSpacing(4)vbox.setContentsMargins(5,5,5,5)# command output areavbox.addWidget(self.core.outputLog,1)## status and progress labelsself.stbar=ThgStatusBar()self.stbar.setSizeGripEnabled(False)self.core.setStbar(self.stbar)vbox.addWidget(self.stbar)# bottom buttonsbuttons=QDialogButtonBox()self.cancelBtn=buttons.addButton(QDialogButtonBox.Cancel)self.cancelBtn.clicked.connect(self.core.cancel)self.core.commandCanceling.connect(self.commandCanceling)self.closeBtn=buttons.addButton(QDialogButtonBox.Close)self.closeBtn.setHidden(True)self.closeBtn.clicked.connect(self.reject)self.detailBtn=buttons.addButton(_('Detail'),QDialogButtonBox.ResetRole)self.detailBtn.setAutoDefault(False)self.detailBtn.setCheckable(True)self.detailBtn.setChecked(True)self.detailBtn.toggled.connect(self.setShowOutput)vbox.addWidget(buttons)self.setLayout(vbox)self.setWindowTitle(_('TortoiseHg Command Dialog'))self.resize(540,420)# start commandself.core.run(cmdline)defsetShowOutput(self,visible):"""show/hide command output"""self.core.outputLog.setVisible(visible)self.detailBtn.setChecked(visible)# workaround to adjust only window heightself.setMinimumWidth(self.width())self.adjustSize()self.setMinimumWidth(0)### Private Method ###defreject(self):ifself.core.running():ret=QMessageBox.question(self,_('Confirm Exit'),_('Mercurial command is still running.\n''Are you sure you want to terminate?'),QMessageBox.Yes|QMessageBox.No,QMessageBox.No)ifret==QMessageBox.Yes:self.core.cancel()# don't close dialogreturn# close dialogifself.returnCode==0:self.accept()# means command successfully finishedelse:super(Dialog,self).reject()@pyqtSlot()defcommandCanceling(self):self.cancelBtn.setDisabled(True)@pyqtSlot(int)defonCommandFinished(self,ret):self.returnCode=retself.cancelBtn.setHidden(True)self.closeBtn.setShown(True)self.closeBtn.setFocus()classRunner(QObject):"""A component for running Mercurial command without UI This command runner doesn't show any UI element unless it gets a warning or an error while the command is running. Once an error or a warning is received, it pops-up a small dialog which contains the command log. """commandStarted=pyqtSignal()commandFinished=pyqtSignal(int)commandCanceling=pyqtSignal()output=pyqtSignal(QString,QString)progress=pyqtSignal(QString,object,QString,QString,object)makeLogVisible=pyqtSignal(bool)def__init__(self,logWindow,parent):super(Runner,self).__init__(parent)self.title=_('TortoiseHg')self.core=Core(logWindow,parent)self.core.commandStarted.connect(self.commandStarted)self.core.commandFinished.connect(self.onCommandFinished)self.core.commandCanceling.connect(self.commandCanceling)self.core.output.connect(self.output)self.core.progress.connect(self.progress)### Public Methods ###defsetTitle(self,title):self.title=titledefrun(self,cmdline,*args,**opts):self.core.run(cmdline,*args,**opts)defrunning(self):returnself.core.running()defcancel(self):self.core.cancel()defoutputShown(self):ifhasattr(self,'dlg'):returnself.dlg.isVisible()else:returnFalsedefsetShowOutput(self,visible=True):ifnothasattr(self.core,'outputLog'):returnifnothasattr(self,'dlg'):self.dlg=dlg=QDialog(self.parent())dlg.setWindowTitle(self.title)dlg.setWindowFlags(Qt.Dialog)dlg.setLayout(QVBoxLayout())dlg.layout().addWidget(self.core.outputLog)self.core.outputLog.setMinimumSize(460,320)self.dlg.setVisible(visible)### Signal Handler ###@pyqtSlot(int)defonCommandFinished(self,ret):ifret!=0:self.makeLogVisible.emit(True)self.setShowOutput(True)self.commandFinished.emit(ret)