class QPyShell(QWidget): """QPyShell - a Qt based python command shell based on code.InteractiveInterpreter. Because it catches stdout and stderr there can be only one active instance of this class. Make sure initInterpreter() and exitInterpreter() is called! """ activeInstance = None def __init__(self, parent=None): QWidget.__init__(self, parent) if parent: self.standalone = False else: self.standalone = True self.setWindowTitle(QCoreApplication.translate("QPyShell", "QPyShell - a simple python shell widget for Qt")) self.mainLayout = QVBoxLayout(self) self.outputBrowser = QTextEdit(self) self.outputBrowser.setMinimumSize(QSize(100,100)) self.outputBrowser.setReadOnly(True) self.mainLayout.addWidget(self.outputBrowser) self.clLayout = QHBoxLayout() self.mainLayout.addLayout(self.clLayout) self.promptLabel = QLabel() self.promptLabel.setText(">>>") self.clLayout.addWidget(self.promptLabel) self.lineInput = QLineEdit() self.lineInput.setToolTip(QCoreApplication.translate('QPyShell', 'The python commandline: enter code her')) self.clLayout.addWidget(self.lineInput) self.enterButton = QToolButton(self) self.enterButton.setText(QCoreApplication.translate("QPyShell", "Enter")) self.enterButton.setToolTip(QCoreApplication.translate('QPyShell', 'This button or [Enter] executes the command')) self.enterButton.setIcon(QIcon(QPixmap(enterXPM))) self.clLayout.addWidget(self.enterButton) self.clLayout.addSpacing(8) self.clearButton = QToolButton(self) self.clearButton.setText(QCoreApplication.translate("QPyShell", "Clear")) self.clearButton.setToolTip(QCoreApplication.translate('QPyShell', 'Clear the output window')) self.clearButton.setIcon(QIcon(QPixmap(clearXPM))) self.clLayout.addWidget(self.clearButton) self.saveButton = QToolButton(self) self.saveButton.setText(QCoreApplication.translate("QPyShell", "Save ...")) self.saveButton.setToolTip(QCoreApplication.translate('QPyShell', 'Save the contents of the output window')) self.saveButton.setIcon(QIcon(QPixmap(saveXPM))) self.clLayout.addWidget(self.saveButton) self.printButton = QToolButton(self) self.printButton.setText(QCoreApplication.translate("QPyShell", "Print")) self.printButton.setToolTip(QCoreApplication.translate('QPyShell', 'Print the contents of the output window')) self.printButton.setIcon(QIcon(QPixmap(printXPM))) self.clLayout.addWidget(self.printButton) self.history = [] self.historyFile = None self.history_i = -1 self.cmdBuffer = [] self.showCompletionLimit = 100 self.interpreter = None self.old_displayhook = None self.cursor = QTextCursor(self.outputBrowser.document()) self.outputBrowser.setTextCursor(self.cursor) self.outputBrowser.append(greeting) self.setTabOrder(self.lineInput, self.outputBrowser) self.setTabOrder(self.outputBrowser, self.enterButton) self.setTabOrder(self.enterButton, self.saveButton) self.setTabOrder(self.saveButton, self.clearButton) self.connect(self.enterButton, SIGNAL("pressed()"), self.run) self.connect(self.saveButton, SIGNAL("pressed()"), self.saveContents) self.connect(self.printButton, SIGNAL("pressed()"), self.printContents) self.connect(self.clearButton, SIGNAL("pressed()"), self.outputBrowser.clear) def initInterpreter(self, loc=None, greet=greeting, historyFile=None): if QPyShell.activeInstance: raise Exception(QCoreApplication.translate("QPyShell", "QPyShell: There can be only one highlander... sorry, I mean one active QPyShell widget!")) QPyShell.activeInstance = self self.loadHistoryFile(historyFile) self.interpreter = InteractiveInterpreter(loc) self.completer = Completer(loc) self.old_stdout = sys.stdout self.old_stderr = sys.stderr sys.stdout = DummyFileW(self.write) sys.stderr = sys.stdout # there's a strange problem with gettext and interactive interpreters # gettext's "_"" will be overwritten by the standard sys.displayhook... self.old_displayhook = sys.displayhook sys.displayhook = mydisplayhook def loadHistoryFile(self, historyFile): self.historyFile = historyFile if historyFile and os.path.exists(historyFile): lines = open(historyFile, 'r').read().split(os.linesep) self.history = [l for l in lines if not l.startswith('#')] def saveHistoryFile(self): if self.historyFile: h = self.history if len(h) > 100: h = h[:100] h.insert(0, unicode(QCoreApplication.translate('QPyShell', '# this is the command history of the QPyShell'))) open(self.historyFile, 'w').write(os.linesep.join(h)) def exitInterpreter(self, loc=None): sys.stdout = self.old_stdout sys.stderr = self.old_stderr self.saveHistoryFile() del self.interpreter self.interpreter = None sys.displayhook = self.old_displayhook QPyShell.ativeInstance = None def run(self): if not self.interpreter: raise Exception(QCoreApplication.translate("QPyShell", "No interpreter found! You need to call QPyShell.initInterpreter() first!")) line = unicode(self.lineInput.text()) self.lineInput.clear() if line: if (not self.history) or line != self.history[0]: # no consecutive identical entries self.history.insert(0, line) self.history_i = -1 self.cursor.movePosition(QTextCursor.End) if not self.cmdBuffer: self.cursor.insertHtml('<br><b>|>>> %s</b><br>' % formatPyLine(line)) else: self.cursor.insertHtml('<br><b>|. . . %s</b><br>' % formatPyLine(line)) self.cursor.movePosition(QTextCursor.End) self.outputBrowser.ensureCursorVisible() self.cmdBuffer.append(line) more = self.interpreter.runsource('\n'.join(self.cmdBuffer)) if more: self.promptLabel.setText('...') else: self.cmdBuffer = [] self.promptLabel.setText('>>>') def appendHtml(self, txt): self.cursor.movePosition(QTextCursor.End) self.cursor.insertHtml(txt) self.cursor.movePosition(QTextCursor.End) self.outputBrowser.ensureCursorVisible() def appendText(self, txt): self.cursor.movePosition(QTextCursor.End) self.outputBrowser.insertPlainText(txt) self.cursor.movePosition(QTextCursor.End) self.outputBrowser.ensureCursorVisible() write = appendText def keyPressEvent(self, event): key = event.key() mod = event.modifiers() if key == Qt.Key_Up and self.history: self.history_i = min(self.history_i+1, len(self.history)-1) self.lineInput.setText(self.history[self.history_i]) elif key == Qt.Key_Down and self.history: self.history_i = max(self.history_i-1, -1) if self.history_i >= 0: self.lineInput.setText(self.history[self.history_i]) else: self.lineInput.setText('') elif key == Qt.Key_Enter or key == Qt.Key_Return: if mod & Qt.ShiftModifier: self.complete() else: self.run() elif key == Qt.Key_Escape: txt, r = QInputDialog.getItem(self, QCoreApplication.translate("QPyShell", "Command line history"), QCoreApplication.translate("QPyShell", "Please select a history item:"), self.history, 0, False) if r and txt: self.lineInput.setText(txt) elif self.standalone and key == Qt.Key_Print: self.printContents() elif self.standalone and key == Qt.Key_Q and (mod & Qt.ControlModifier): self.close() else: QWidget.keyPressEvent(self, event) def closeEvent(self, event): if self.standalone: self.exitInterpreter() QWidget.closeEvent(self, event) def printContents(self, printer=None): if not printer: printer = QPrinter() dialog = QPrintDialog(printer, self) dialog.setWindowTitle(QCoreApplication.translate("QPyShell", "Print Document")) if dialog.exec_() != QDialog.Accepted: return self.outputBrowser.document().print_(printer) def saveContents(self, fileName=None): if not fileName: fileTypes = {'Text':('txt',), 'HTML':('htm', 'html')} filters = ';;'.join(['%s (%s)' % (k, ' '.join(['*.'+e for e in v])) for k, v in fileTypes.items()]) dlg = QFileDialog(self, QCoreApplication.translate('QPyShell', 'Select name of file to save'), os.getcwd(), filters) dlg.setFileMode(QFileDialog.AnyFile) dlg.setAcceptMode(QFileDialog.AcceptSave) if dlg.exec_() != QDialog.Accepted: return tmp = unicode(dlg.selectedFilter()) fileType = tmp[:tmp.find('(')-1] dlg.setDefaultSuffix(fileTypes[fileType][0]) files = dlg.selectedFiles() if not files: return fileName = unicode(files[0]) if fileType == 'Text': txt = self.outputBrowser.toPlainText() elif fileType == 'HTML': txt = self.outputBrowser.toHtml() else: raise IOError('Unknown FileType: %s' % fileType) try: open(fileName, 'w').write(txt) except IOError: QMessageBox.critical(self, QCoreApplication.translate('QPyShell', 'Could not save file!'), QCoreApplication.translate('QPyShell', 'Writing failed! Make sure you have write permissions!')) def complete(self): """ a very simple, quick and dirty completion of identifiers in the namespace """ # FIXME: fertig machen! txt = unicode(self.lineInput.text()) cur = self.lineInput.cursorPosition() s = cur while s>0 and txt[s-1] in identChars: s -= 1 try: completions = self.completer.matches(txt[s:cur]) except: return if not completions: return n_comp = len(completions) if n_comp == 1: comp = completions.pop() self.lineInput.insert(comp[cur-s:]) elif n_comp < self.showCompletionLimit: tmp = list(completions) tmp.sort() self.appendHtml('<font color="#0000ff">[%s]</font>' % ', '.join(tmp)) # get common prefix ... stolen from the python cookbook pref = tmp[0][:([min([x[0]==elem for elem in x]) for x in zip(*tmp)]+[0]).index(0)] if len(pref) > (cur-s): self.lineInput.insert(pref[cur-s:]) else: self.appendHtml(unicode(QCoreApplication.translate("QPyShell", '<font color="#0000ff">[Too many completions: %d]</font>')) % n_comp)
class LigneCommande(QWidget): u"""Un TextCtrl muni d'un historique et associé à un bouton pour valider. On peut personnaliser le texte du bouton (via `texte="Mon texte"`), ou même directement le bouton, en utilisant `bouton=...`. """ def __init__(self, parent, longueur = 500, texte = None, bouton = None, action = (lambda *args, **kw: True), afficher_bouton = True, legende = None): self.parent = parent self.action = action QWidget.__init__(self, parent) sizer = QHBoxLayout() self.texte = QLineEdit() self.texte.setMinimumWidth(longueur) if bouton is None: self.bouton = QPushButton('OK' if texte is None else texte) else: self.bouton = bouton self.bouton.setVisible(afficher_bouton) self.bouton.clicked.connect(self.valider) if legende is not None: sizer.addWidget(QLabel(legende, self)) sizer.addWidget(self.texte) sizer.addWidget(self.bouton) self.setLayout(sizer) self.initialiser() def initialiser(self): self.historique = [] self.position = None self.setFocus() self.clear() def text(self): return self.texte.text() def setText(self, value): self.texte.setText(value) def setFocus(self): self.texte.setFocus() def clear(self): self.texte.clear() def getSelection(self): if self.texte.hasSelectedText(): start = self.texte.selectionStart() length = len(self.texte.selectedText()) end = start + length else: start = end = self.texte.cursorPosition() return start, end def cursorPosition(self): return self.texte.cursorPosition() def setCursorPosition(self, num): return self.texte.setCursorPosition(num) def insert(self, texte): self.texte.insert(texte) def setSelection(self, deb, fin): self.texte.setSelection(deb, fin) def setToolTip(self, tip): self.texte.setToolTip(tip) def valider(self, **kw): # kw: shift|alt|meta|control=True|False' commande = unicode(self.text()) self.position = None if commande: self.historique.append(commande) elif self.historique: # Appuyer une deuxième fois sur [Entrée] permet de répéter l'action précédente. commande = self.historique[-1] self.action(commande, **kw) def keyPressEvent(self, event): key = event.key() commande = self.text() if key == Qt.Key_Up and self.historique: # On remonte dans l'historique (-> entrées plus anciennes) if self.position is None: # cas d'une commande en cours d'édition : if commande: if commande != self.historique[-1]: # on enregistre la commande en cours self.historique.append(commande) self.position = len(self.historique) - 1 else: self.position = len(self.historique) if self.position > 0: self.position -= 1 self.texte.setText(self.historique[self.position]) elif key == Qt.Key_Down and self.historique: # On redescend dans l'historique (-> entrées plus récentes) if self.position is None or self.position == len(self.historique) - 1: if commande and commande != self.historique[-1]: self.historique.append(commande) self.texte.clear() self.position = len(self.historique) elif self.position < len(self.historique) - 1: self.position += 1 self.texte.setText(self.historique[self.position]) elif key in (Qt.Key_Enter, Qt.Key_Return): modifiers = event.modifiers() kw = {'shift': modifiers & Qt.ShiftModifier, 'alt': modifiers & Qt.AltModifier, 'meta': modifiers & Qt.MetaModifier, 'control': modifiers & Qt.ControlModifier, } self.valider(**kw) else: self.position = None
class TagCompleterWidget(QObject): """ widget in lineEdit-style with integrated qcompleter """ def __init__(self, max_tags, expiry_prefix=None, tag_list=None, parent=None, separator=",", show_datestamp=False): QWidget.__init__(self, parent) self.__completer_active = False self.__max_tags = max_tags self.__tag_separator = separator self.__tag_list = tag_list self.__parent = parent self.__tag_line = QLineEdit(self.__parent) #self.__tag_line = TagLineEdit(self.__parent) self.__show_datestamp = show_datestamp self.__datestamp_format = TsConstants.DATESTAMP_FORMAT_DAY self.__expiry_prefix = expiry_prefix ## flag, if the line should be checked of emptiness self.__check_not_empty = False self.__check_tag_limit = False self.__restricted_vocabulary = False ## the latest activated suggestion self.__activated_text = None # value of the actual datestamp self.__datestamp = None self.__datestamp_hidden = False self.__completer = QCompleter(self.__tag_list, self); self.__completer.setCaseSensitivity(Qt.CaseInsensitive) self.__completer.setWidget(self.__tag_line) #self.__handle_datestamp() self.connect(self.__tag_line, SIGNAL("textChanged(QString)"), self.__text_changed_by_user) self.connect(self.__completer, SIGNAL("activated(QString)"), self.__text_activated) self.connect(self.__completer, SIGNAL("highlighted(QString)"), self.__text_highlighted) def __text_highlighted(self, item_name): """ a suggestion has been selected in the dropdownbox """ # set this variable to True just to know, that # this value comes from the completer and not from the user self.__completer_active = True self.__text_selected(item_name) self.__completer_active = False def __handle_datestamp(self, is_hidden): """ if the show_datestamp flag is set to True, provide an automatic datestamp on the tagline """ if self.__show_datestamp: self.__datestamp = time.strftime(self.__datestamp_format) if not is_hidden: self.__tag_line.clear() self.__tag_line.setText(self.__datestamp) def set_datestamp_format(self, format, is_hidden): self.__datestamp_format = format self.__datestamp_hidden = is_hidden self.__handle_datestamp(is_hidden) def show_datestamp(self, show): self.__show_datestamp = show self.__handle_datestamp(show) def clear_line(self): """ clear the tagline ... if auto datestamp is set to "on" a fresh stamp will be placed into the tagline """ self.__tag_line.clear() if self.__show_datestamp: self.__handle_datestamp(self.__datestamp_hidden) def set_check_not_empty(self, check_necessary): """ set this to True, if there should be sent a signal that indicates that the tagline is not empty anymore """ self.__check_not_empty = True def set_restricted_vocabulary(self, is_restricted): """ use True/False to turn the restricted function on/off """ self.__restricted_vocabulary = is_restricted def select_line(self): """ select the tagline ... """ self.__tag_line.selectAll() self.__tag_line.setFocus(QtCore.Qt.OtherFocusReason) def __text_changed_by_user(self, text): # create a QByteArray in utf8 all_text = text.toUtf8() # make a python string out of it all_text = str(all_text) # convert the python string tu unicode utf-8 all_text = unicode(all_text, "utf-8") if self.__check_not_empty: if all_text is not None and all_text != "": self.emit(QtCore.SIGNAL("line_empty"), False) else: self.emit(QtCore.SIGNAL("line_empty"), True) text = all_text[:self.__tag_line.cursorPosition()] ## remove whitespace and filter out duplicates by using a set tag_set = set([]) for tag in all_text.split(self.__tag_separator): strip_tag = tag.strip() if strip_tag != "": tag_set.add(strip_tag) max_tags = self.__max_tags if self.__datestamp_hidden: max_tags = max_tags - 1; ## do not proceed if the max tag count is reached if len(tag_set) > max_tags: self.emit(QtCore.SIGNAL("tag_limit_reached"), True) self.__check_tag_limit = True return else: if self.__check_tag_limit: self.emit(QtCore.SIGNAL("tag_limit_reached"), False) self.__check_tag_limit = False prefix = text.split(self.__tag_separator)[-1].strip() if not self.__completer_active: self.__update_completer(tag_set, prefix) def __update_completer(self, tag_set, completion_prefix): if self.__tag_list is None: return tags = list(set(self.__tag_list).difference(tag_set)) #tags = list(self.__tag_list) model = QStringListModel(tags, self) self.__completer.setModel(model) self.__completer.setCompletionPrefix(completion_prefix) if self.__restricted_vocabulary: self.__check_vocabulary(tag_set, completion_prefix) if completion_prefix.strip() != '': ## use the default completion algorithm self.__completer.complete() def __check_finished_tags(self, typed_tags_list): """ use this method to control all typed tags. this means all tags terminated with a comma """ pass def __check_in_completion_list(self, tag): """ if a written tag equals a tag of the completion list - the tag will be removed from the completion list so the completer will return a completion count of 0 for this tag. in this case there would be displayed an error message at the dialog (when controlled vocab is activated) so check manually, if the provided tag is contained in the suggestion_list """ #for sug_tag in self.__tag_list: # if sug_tag == tag: # return True #return False return tag in self.__tag_list def __check_vocabulary(self, tag_set, completion_prefix): """ have a look at the entered tag to be completed. if restricted vocabulary is turned on: datestamps do not have to be checked. """ not_allowed_tags_count = 0 no_completion_found = False stripped_text = unicode(self.__tag_line.text()).strip() ##when a tag separator is on the last position, there should have been entered a new tag ##check this tag for its correctness if len(stripped_text) > 0: ##check if all written tags are allowed (incl. datestamps an expiry tags) for tag in tag_set: ## tag can be a datestamp -> OK if not SpecialCharHelper.is_datestamp(tag) and tag != "": ## tag can be an expiry tag -> OK # if self.__expiry_prefix is not None and not SpecialCharHelper.is_expiry_tag(self.__expiry_prefix, tag): if self.__expiry_prefix is None or not SpecialCharHelper.is_partial_expiry_tag(self.__expiry_prefix, tag): if unicode(tag) not in self.__tag_list: not_allowed_tags_count += 1 if(completion_prefix.strip() == ""): ## if the prefix is an empty string - manually set the completion_count to 0 ## because the completer would return the whole number of tags in its suggestion list completion_count = 0 else: completion_count = self.__completer.completionCount() if self.__restricted_vocabulary and completion_count == 0: ## additionally check if the prefix equals a tag from the suggestion list ## this has to be done, because we do not get a completionCount > 0 if the prefix equals a given tag #if completion_prefix not in self.__tag_list: if completion_prefix is not None and len(completion_prefix) > 0 and completion_prefix.strip() != "": ## just send the signal if the tag is no datestamp AND if it is no full tag if not SpecialCharHelper.is_datestamp(completion_prefix) and not self.__check_in_completion_list(completion_prefix): if not SpecialCharHelper.is_partial_expiry_tag(self.__expiry_prefix, completion_prefix): no_completion_found = True ## there are tags (terminated with comma) which are not in the allowed tag_list if not_allowed_tags_count > 1: self.emit(QtCore.SIGNAL("no_completion_found"), True) return if not_allowed_tags_count > 0: ## in this case the user has entered a not allowed tag and terminated it with a comma to mark it as a tag ## the completion count is 0 because there is nothing after the last comma in the taglist if completion_count == 0: self.emit(QtCore.SIGNAL("no_completion_found"), True) return ## it could be the case, that the user is still typing an allowed tag ## so check, if the completer has a possible completion ## if not -> send the signal if no_completion_found: self.emit(QtCore.SIGNAL("no_completion_found"), True) return ## everytime there is no completion found, emit the signal elif no_completion_found: self.emit(QtCore.SIGNAL("no_completion_found"), True) return ## in this case everything is fine self.emit(QtCore.SIGNAL("no_completion_found"), False) def __text_selected(self, text): self.__activated_text = text cursor_pos = self.__tag_line.cursorPosition() before_text = unicode(self.__tag_line.text())[:cursor_pos] #after_text = unicode(self.__tag_line.text())[cursor_pos:] prefix_len = len(before_text.split(self.__tag_separator)[-1].strip()) self.__tag_line.setText("%s%s" % (before_text[:cursor_pos - prefix_len], text)) self.__tag_line.setCursorPosition(cursor_pos - prefix_len + len(text) + 2) def __text_activated(self, text): """ a suggestion has been choosen by the user """ self.__text_selected(text) self.emit(QtCore.SIGNAL("activated")) def get_tag_list(self): tag_string = unicode(self.__tag_line.text()) result = set([]) tag_list = tag_string.split(self.__tag_separator) for tag in tag_list: strip_tag = tag.strip() if strip_tag != "": result.add(strip_tag) # if the datestamp is hidden, add it manually to the taglist if self.__datestamp_hidden: result.add(self.__datestamp) return result def get_tag_line(self): return self.__tag_line def get_completer(self): return self.__completer def set_enabled(self, enable): self.__tag_line.setEnabled(enable) def set_text(self, text): self.__tag_line.setText(text) def set_tag_completion_list(self, tag_list): self.__tag_list = tag_list self.__completer.setModel(QtGui.QStringListModel(QtCore.QStringList(tag_list))) def get_tag_completion_list(self): return self.__tag_list def is_empty(self): if self.__tag_line.text() == "": return True else: return False def set_place_holder_text(self, text): self.__tag_line.setPlaceholderText(text)
class LigneCommande(QWidget): u"Un TextCtrl muni d'un historique et associé à un bouton pour valider." def __init__(self, parent, longueur = 500, texte = None, action = (lambda *args, **kw: True), afficher_bouton = True, legende = None): self.parent = parent self.action = action QWidget.__init__(self, parent) # self.SetBackgroundColour(self.parent.GetBackgroundColour()) sizer = QHBoxLayout() self.texte = QLineEdit() self.texte.setMinimumWidth(longueur) self.texte.returnPressed.connect(self.EvtButton) self.bouton = QPushButton('OK' if texte is None else texte) self.bouton.setVisible(afficher_bouton) self.bouton.clicked.connect(self.EvtButton) if legende is not None: sizer.addWidget(QLabel(legende, self)) sizer.addWidget(self.texte) sizer.addWidget(self.bouton) self.setLayout(sizer) self.initialiser() def initialiser(self): self.historique = [] self.position = None self.setFocus() self.clear() def text(self): return self.texte.text() def setText(self, value): self.texte.setText(value) def setFocus(self): self.texte.setFocus() def clear(self): self.texte.clear() def getSelection(self): if self.texte.hasSelectedText(): start = self.texte.selectionStart() length = len(self.texte.selectedText()) end = start + length else: start = end = self.texte.cursorPosition() return start, end def cursorPosition(self): return self.texte.cursorPosition() def setCursorPosition(self, num): return self.texte.setCursorPosition(num) def insert(self, texte): self.texte.insert(texte) def setSelection(self, deb, fin): self.texte.setSelection(deb, fin) def setToolTip(self, tip): self.texte.setToolTip(tip) def EvtButton(self, event=None): commande = unicode(self.text()) self.position = None if commande: self.historique.append(commande) elif self.historique: # Appuyer une deuxième fois sur [Entrée] permet de répéter l'action précédente. commande = self.historique[-1] kw = {} for modifier in ('shift', 'alt', 'meta', 'control'): kw[modifier] = getattr(event, modifier.capitalize() + 'Down', lambda: None)() self.action(commande, **kw) def keyPressEvent(self, event): key = event.key() commande = self.text() if key == Qt.Key_Up: # On remonte dans l'historique (-> entrées plus anciennes) if self.position is None: # cas d'une commande en cours d'édition : if commande: if commande != self.historique[-1]: # on enregistre la commande en cours self.historique.append(commande) self.position = len(self.historique) - 1 else: self.position = len(self.historique) if self.position > 0: self.position -= 1 self.texte.setText(self.historique[self.position]) elif key == Qt.Key_Down: # On redescend dans l'historique (-> entrées plus récentes) if self.position is None or self.position == len(self.historique) - 1: if commande and commande != self.historique[-1]: self.historique.append(commande) self.texte.clear() self.position = len(self.historique) elif self.position < len(self.historique) - 1: self.position += 1 self.texte.setText(self.historique[self.position]) else: self.position = None