Skip to main content

Ralsina.Me — Roberto Alsina's website

Posts about programming (old posts, page 60)

Marave 0.6 is out

Ver­sion 0.6 of Mar­ave, my peace­ful, fullscreen text ed­i­tor is now avail­able at the usu­al place: http://­mar­ave.­google­code.­com

New stuff:

  • Syn­­tax high­­­lighter

  • Plug­ins

  • Bugs fixed

  • Nicer an­i­­ma­­tions

  • Code cleanup

Gra­tu­itous screen­shot:

The aha! moment

I had a small task to­day in Mar­ave. The goal was:

  1. Fade in a wid­get

  2. Set a var­i­able

  3. Fade in an­oth­er one

It's im­por­tant that things are done in that or­der and it's al­so im­por­tant that the app re­mains in­ter­ac­tive.

And here's the code to do that (sim­pli­fied):

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)

And this is how you use it:

def later():
    avar=avalue
    fadein(widget2)

fadein(widget1, thendo=later)

Isn't that lovely? Having functions as first class objects means I can just take later as a closure, along with widget2 and avar, which need only be defined in the local scope, and the call chain will work just as I wanted!

Yes, many oth­er lan­guages can do this, and in Javascript it's one of the most com­mon trick­s, but con­sid­er that PyQt is a wrap­per for a C++ li­brary!

I think this kind of us­age shows the re­al added val­ue PyQt brings to the table, it's not just that python avoids the bor­ing com­piles, or that you have the awe­some stan­dard li­brary to use, but that the lan­guage it­self en­ables you to do things that are not prac­ti­cal in C++.

In C++ the only way I can think of is creating a slot that's the equivalent of later, then chaining the signals... which means that this throwaway later becomes part of the interface of a class!

I would have to define later somewhere else on the file, separate from its only usage (maybe even inlined in the header file).

Even then, that's not equivalent: avalue may be something that was only avalable before the first call to fadein, (for example, the time of the first fadein): I would have to create a place to store it, make it reachable by later... and wht happens if you try to do this again while the first fadein is in progress?... it gets hairy.

Pro­gram­ming is like a slap in the face some­times... you re­al­ize that things you use with­out even notic­ing are far from triv­ial.

So, re­mem­ber young padawan: you can choose you tool­s. Choose wise­ly.

Extending Marave

Mar­ave is a text ed­i­tor. If there's one thing that's true of most text ed­i­tors, it's this: they lack the ex­act fea­tures you need.

So, the so­lu­tion, in the an­cient tra­di­tion of Emacs and Vim is... make it ex­ten­si­ble.

I am a big fan of pro­grams that can be ex­tend­ed by users.

So... here's the anato­my of a Mar­ave plug­in as it stands right now on SVN trunk, which of course can change any minute.

Creating a plugin

You just need to cre­ate a .py file in the plug­ins fold­er.

Here's the most ba­sic plug­in, which does noth­ing:

# -*- coding: utf-8 -*-

from plugins import Plugin
class Smarty(Plugin):
    name='smarty'
    shortcut='Ctrl+.'
    description='Smart quote and dash replacement'
    mode="qBde"

De­fault val­ues for any­thing con­fig­urable (in this case, "mod­e") is just added to the class.

The manda­to­ry field­s:

  • short­­­cut: a key­board short­­­cut that trig­gers this plug­in

  • name: a short name

  • de­scrip­­tion: a one-­­line de­scrip­­tion of what it does

What does it do? It adds the plug­in to the plug­in list in the prefs di­alog, and you can open its con­fig­u­ra­tion di­alog, where you can change the short­cut:

maraveplugin1

If you en­able this plug­in, when­ev­er the short­cut is used the "run" method of the plug­in is called.

Making the Plugin Configurable

This plug­in sup­ports dif­fer­ent modes of op­er­a­tion. To make this reach­able to the user, you need to im­ple­ment a few ex­tra meth­od­s.

The ad­d­Con­fig­Wid­gets method takes a di­a­log ar­gu­ment and adds what­ev­er you want there:

@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)

And then the con­fig di­a­log will look like this:

maraveplugin2

But then you need to save those op­tions some­where, which you do reim­ple­ment­ing save­Con­fig:

@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()

And you need to load those set­tings and put them in your class, too:

@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())

Making it Work

And yes, you need to make it do some­thing use­ful. The plug­in has ac­cess to a "clien­t" which is Mar­ave's main win­dow. Ev­ery­thing is avail­able there, some­where ;-)

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

And there it is, if you en­able the smar­ty plug­in, you can "fix" your quotes, dash­es and el­lip­sis with a key com­bi­na­tion :-)

Full source code here: http://­code.­google.­com/p/­mar­ave/­source/browse/trunk­/­mar­ave/­plu­g­in­s/s­mar­ty.py

Still to be done: oth­er ways to in­te­grate plug­ins in­to the UI, but­ton­s, pan­el­s, etc.

Yak Shavings for February 16, 2010

yak shaving

(id­iomat­ic) Any ap­par­ent­ly use­less ac­tiv­i­ty which, by al­low­ing you to over­come in­ter­me­di­ate dif­fi­cul­ties, al­lows you to solve a larg­er prob­lem.

A while ago, I wrote how I im­ple­ment­ed a gener­ic syn­tax high­lighter for PyQt us­ing Pyg­ments.

I got a re­quest for such a fea­ture in Mar­ave, so I digged that code and... it's freak­ing use­less. It's just too slow for rea­son­able use.

So, that yak's hair is all grown up again, and I just got this new pair of scis­sors!

The goal is a way to high­light syn­tax in a QPlain­TextE­d­it that:

  • Does­n't re­quire pro­­gram­ming to add a new high­­­lighter

  • Does­n't re­quire pro­­gram­ming to add a new col­or scheme

  • Does­n't re­quire me to spend a year writ­ing high­­­lighters for ex­ist­ing lan­guages

  • Is fast enough

A quick google shows that for C++ you can use Source high­light qt which is based on GNU source high­light.

Alas, no python bind­ing that I could find. So, let's write one!

Here it is: http://­mar­ave.­google­code.­com/svn/trunk­/­mar­ave/high­light/

And here's a screen­shot of the de­mo pro­gram run­ning, show­ing it­self in its en­tire­ty:

You can cre­ate a col­or scheme us­ing CSS, a lan­guage def­i­ni­tion is a text file, there are a bazil­lion al­ready writ­ten, it seems to be fast enough.

So, an­oth­er yak shaved, an­oth­er fea­ture (not fin­ished!) for Mar­ave

How to implement "replace all" in a QPlainTextEdit

This is not in­ter­est­ing for al­most noone, but since my google-­fu did­n't let me find it and it was a bit of a pain to do:

This is how you im­ple­ment 're­place al­l' in a QPlain­TextE­d­it (or a QTextE­d­it, for that mat­ter) us­ing PyQt (sim­i­lar for C++ of course).

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()

There are oth­er, eas­i­er ways to do it, but this one makes it all ap­pear as a sin­gle op­er­a­tion in the un­do stack and all that.


Contents © 2000-2023 Roberto Alsina