Publicaciones sobre programming (publicaciones antiguas, página 60)
El momento ajá!
Hacer un "fade in" de un widget
Setear una variable
Hacer un "fade in" de otro widget
Es importante hacerlo en ese orden y es importante que la aplicación siga respondiendo.
Acá está el código que usé (simplificado):
def fadein(thing, target=1., thendo=None): """ * thing is a QWidget * thing.proxy is a QGraphicsWidget * thendo is callable * target is the desired opacity """ thing.anim=QtCore.QPropertyAnimation(thing.proxy, "opacity") thing.anim.setDuration(200) thing.anim.setStartValue(thing.proxy.opacity()) thing.anim.setEndValue(target) thing.anim.start() thing.anim.finished.connect(thing.anim.deleteLater) if thendo: thing.anim.finished.connect(thendo)
Y se usa así:
¿No es lindo? Tener funciones como objetos de primera clase significa que puedo tomar later
como un closure, junto con widget2
y avar
que sólo necesitan estar definidas en el scope local, y la cadena de llamadas funciona ¡exactamente como quiero!
Sí, en muchos otros lenguajes se hace lo mismo, y en Javascript es un truco común... ¡pero PyQt es un wrapper de C++!
Me parece que este tipo de uso muestra el valor agregado que PyQt te da, no es solamente que con python evitás la compilación aburrida, o que tenés la increíble biblioteca estándar, sino que el lenguaje mismo te deja hacer cosas que no son prácticas en C++.
La única manera que se me ocurre de hacer esto en C++ es crear un slot que sea el equivalente de later
, y encadenarlo a la señal... lo que quiere decir que ese later
descartable se convierte en parte de la interface de la clase. (!?)
Habría que definir later
en algún otro lado del archivo, separado de su único uso (tal vez inine en el header).
Aún así, eso no es equivalente: avalue
podría ser algo no fácil de acceder cuando se ejecuta later
(por ejemplo, el timestamp del primer fadein), habría que buscar donde guardarlo para que later
lo encuentre, no se puede volver a hacer esto hasta después que se ejecute later
... se pone complicado.
A veces programar es como una cachetada... te das cuenta que cosas que usás sin pensar no son nada triviales.
Así que recuerda joven aprendiz: podés elegir las herramientas. Elegí con cuidado.
Extendiendo Marave
Entonces la solución, en la antigua tradición de Emacs y Vim es... hacerlo extensible.
Soy un gran fan de los programas extensibles por el usuario.
Así que... acá está la anatomía de un plugin de Marave tal como funciona ahora mismo en SVN trunk, lo que por supuesto puede cambiar en cualquier momento.
Creando un plugin
Sólo hay que crear un archivo .py en la carpeta plugins. Éste es el plugin más básico, que no hace nada:
# -*- coding: utf-8 -*- from plugins import Plugin class Smarty(Plugin): name='smarty' shortcut='Ctrl+.' description='Smart quote and dash replacement' mode="qBde"
Valores por default de algo configurable (en este caso "mode") se ponen en la clase.
Los campos obligatorios:
shortcut: un atajo de teclado que dispara este plugin
name: un nombre corto
description: una descripción de una línea
¿Qué hace esto? Agrega el plugin a la lista en el diálogo de preferencias, y se puede abrir el diálogo de configuración del plugin, donde se puede cambiar el shortcut:
Si se habilita este plugin, cuando el usuario use ese shortcut, se llama al método "run" del plugin.
Haciéndolo Configurable
Éste plugin soporta distintos modos de operación. Para hacer que esto sea accesible al usuario, hay que implementar unos pocos métodos mas.
El método addConfigWidgets toma como argumento un diálogo, y agrega lo que uno quiera ahí:
@classmethod def addConfigWidgets(self, dialog): print 'Adding widgets to smarty config' l=dialog.ui.layout self.q=QtGui.QCheckBox(dialog.tr('Replace normal quotes')) if 'q' in self.mode: self.q.setChecked(True) self.b=QtGui.QCheckBox(dialog.tr('Replace backtick-style quotes (` and ``)')) if 'B' in self.mode: self.b.setChecked(True) self.d=QtGui.QCheckBox(dialog.tr('Replace -- by en-dash, --- by em-dash')) if 'd' in self.mode: self.d.setChecked(True) self.e=QtGui.QCheckBox(dialog.tr('Replace ellipses')) if 'e' in self.mode: self.e.setChecked(True) l.addWidget(self.q) l.addWidget(self.b) l.addWidget(self.d) l.addWidget(self.e)
Y entonces el diálogo de configuración se ve así:
También queremos que esas opciones se puedan guardar en algún lado, entonces reimplementamos saveConfig:
@classmethod def saveConfig(self, dialog): self.shortcut=unicode(dialog.ui.shortcut.text()) self.settings.setValue('plugin-'+self.name+'-shortcut', self.shortcut) newmode="" if self.q.isChecked(): newmode+='q' if self.b.isChecked(): newmode+='B' if self.d.isChecked(): newmode+='d' if self.e.isChecked(): newmode+='e' self.mode=newmode self.settings.setValue('plugin-smarty-mode',self.mode) self.settings.sync()
Y queremos que esas opciones se lean antes de crear el plugin, entonces:
@classmethod def loadConfig(self): print 'SMARTY loadconfig', self.settings if self.settings: sc=self.settings.value('plugin-'+self.name+'-shortcut') if sc.isValid(): self.shortcut=unicode(sc.toString()) mode=self.settings.value('plugin-smarty-mode') if mode.isValid(): self.mode=unicode(mode.toString())
Que haga algo!
Y sí, hay que hacer que sirva para algo. El plugin tiene acceso a un "client" que es la ventana principal de Marave. Todo está ahí, en alguna parte ;-)
def run(self): print 'running smarty plugin' text=unicode(self.client.editor.toPlainText()).splitlines() prog=QtGui.QProgressDialog(self.client.tr("Applying smarty"), self.client.tr("Cancel"), 0,len(text), self.client) prog.show() output=[] for i,l in enumerate(text): output.append(unescape(smartyPants(l,self.mode))) prog.setValue(i) QtGui.QApplication.instance().processEvents() prog.hide() self.client.editor.setPlainText('\n'.join(output))
Y ya está, si se habilita el plugin smarty, se pueden "arreglar" las comillas, guiones y elipsis con una combinación de teclas :-)
Código fuente completo aquí: http://code.google.com/p/marave/source/browse/trunk/marave/plugins/smarty.py
Falta hacer: otras maneras de integrar plugins en la interface, botones, paneles, etc.
Afeitando yaks: 16/2/2010
Hace un tiempo escribí acerca de como implementé un resaltador de sintaxis generalizado para PyQt usando pygments.
Recibí un pedido de un feature similar en Marave, así que desenterré ese código y... no sirve para nada. Es demasiado lento para un uso razonable.
Entonces a este yak ya le creció de nuevo todo el pelo, y ¡justo tengo este par de tijeras nuevas!
La meta es lograr resaltar sintaxis en un QPlainTextEdit de forma que:
No requiera programar para añadir un nuevo resaltador.
No requiera programar para añadir un esquema de colores.
No requiera que me pase el 2010 escribiendo resaltadores para lenguajes existentes.
Sea suficientemente rápido
Una búsqueda rápida en google muestra que para C++ se puede usar Source highlight qt que está basado en GNU source highlight.
Obviamente, no hay binding python que yo vea, así que... ¡me escribí uno!
Acá está: http://marave.googlecode.com/svn/trunk/marave/highlight/
Y ésta es una captura del programa de demo corriendo, mostrandose a sí mismo entero:
Se puede crear un esquema de colores usando CSS, un lenguaje se define con un archivo de texto, hay una pila ya hechos, y parece lo bastante rápido.
Entonces declaro a este yak afeitado, y otro feature (no terminado!) para Marave
Como implementar "reemplazar todos" en un QPlainTextEdit
Así es como se implementa 'reemplazar todos' en un QPlainTextEdit (o un QTextEdit, probablemente) usando PyQt (es similar para C++).
def doReplaceAll(self): # Replace all occurences without interaction # Here I am just getting the replacement data # from my UI so it will be different for you old=self.searchReplaceWidget.ui.text.text() new=self.searchReplaceWidget.ui.replaceWith.text() # Beginning of undo block cursor=self.editor.textCursor() cursor.beginEditBlock() # Use flags for case match flags=QtGui.QTextDocument.FindFlags() if self.searchReplaceWidget.ui.matchCase.isChecked(): flags=flags|QtGui.QTextDocument.FindCaseSensitively # Replace all we can while True: # self.editor is the QPlainTextEdit r=self.editor.find(old,flags) if r: qc=self.editor.textCursor() if qc.hasSelection(): qc.insertText(new) else: break # Mark end of undo block cursor.endEditBlock()
Hay otras maneras más fáciles, pero esta hace que todo aparezca en una sola operación en la pila undo/redo y esas cosas.