Monday, February 9, 2015

Another pyqtdeploy hitch, edit feature, help text

Today I began work on setting up pyqtdeploy from scratch with static Python, PyQt and Sip, as it should be. I'm following in part the work of Lloyd Konneker in this blog entry, and in part these pyqtdeploy docs.

Things started well; following Lloyd's suggestion I created a new folder /Developer/Static and exported SYSROOT as that path. In it I build a Downloads folder containing three folders, one containing the Python 3.4.2 source, one with the PyQt5.4.1 source, and one with the latest Sip source.

Moved into the Python folder and ran pyqtdeploycli --package python configure. Looked good. Now to run QMake. And immediately it failed with an absolutely ridiculous error. Slightly edited the sequence goes:

$ echo $SYSROOT
/Developer/Static
$ qmake SYSROOT=$SYSROOT
ERROR: SYSROOT must be defined on the qmake command line

Can you believe it? Can an error be less credible?

Anyway I wrote to Lloyd who has been very helpful and encouraging. We'll see.

Pending a reply from him, I set to work to fix a functional hole. Writing the help text yesterday I was copying stuff from the V1 help file and noticed, oops, in V1 I documented that with the focus in the editor, you could key ^u to uppercase the selection, ^l to lowercase, and ^i (initial) to title-case it. (^t is preempted to mean, replace-and-find-again.)

I had certainly meant to implement that in V2, but somehow had forgotten. So now I set to work to do it.

The V1 code was rather bulky. It got the current selection as a QString; it looped over the string using a QRegExp to isolate the next word; and it used the QString methods to change the case and replace the word with the modified text. Per the comments I was particularly staying in the QString domain because I was afraid Python would not handle Unicode casing properly.

Now in PyQt5 there are no QStrings. If I get the selected text it is as a Python string. The Python 3 string methods .upper(), .lower() and .title() are Unicode-aware, so that's no longer an issue. So I looked at how to do this in a better way. The doc for the string .title() method tipped me off to the feature of the re.sub() method that lets you pass a function that will be applied to every matching substring. It's kind of magic. Based on that, here is the main part of the code. The editor has caught one of the three keystrokes, or the user has selected one of the three corresponding Edit menu actions.

    def case_mod(self, kkey) :
        tc = self.textCursor()
        if not tc.hasSelection() :
            return # no selection, nothing to do
        text = tc.selectedText() # full selection as string
        if kkey == C.CTL_SHFT_L :
            func = lambda m : m.group(0).lower()
        elif kkey == C.CTL_SHFT_U :
            func = lambda m : m.group(0).upper()
        else:
            func = lambda m : m.group(0)[0].upper() + m.group(0)[1:].lower()
        new_text = re_word.sub( func, text ) # do it!
        tc.insertText( new_text )

That's it! There are a few more lines devoted to re-establishing the selection after the text insertion. The whole thing is about 1/8th the LOC of V1.

The magic is in re_word.sub( func, text ). re_word is a global compiled regular expression that matches to whole words, including words that contain DP special character notes like [:u] for ΓΌ, or hyphens or apostrophes or curly apostrophes. The syntax highlighter used it in the process of marking spelling errors or scannos, but it repurposes here nicely.

The .sub() method, if given a function, applies that function to every match in the text. The function receives a regex match object, and is expected to return a string to replace the matched substring. So to change the case of the matched text, just return that text, which is match group 0, in lowercase or uppercase. Or for title case, return the initial uppercase and the rest lowercase. It's just deliciously slick.

After that enjoyable session I spent some time cleaning up the extras folder, making sure all the things that are supposed to be in it are, and putting everything under version control so they'll get onto github.

That leads to a complication with the Sphinx-based help page. This page relies upon just six helper files, three .css ones and three .js ones, in a folder named _static adjacent to the .html file. So I created this structure:

extras
    sphinx
        index.html <-- the actual help file as compiled by sphinx
        _static
            blah.css
            blah.js
            etc helper files
    ppqt2help.html as a symbolic link to sphinx/index.html

If I open extras/sphinx/index.html in a browser, it looks great.

Ditto, if I open extras/ppqt2help.html in a browser, it looks great. The browser, well Firefox at least, resolves the symlink and finds src=_static.blah.css.

Not so the PPQT code. It opens extras/ppqt2help.html, reads its contents, and stuffs it into a QWebView for display. And the QWebView can't find the helper files.

And as I write this, I see the problem. I think there is some way to tell QWebView the base path for its content. I shall investigate that tomorrow.

No comments: