Showing posts with label ExtraSelection. Show all posts
Showing posts with label ExtraSelection. Show all posts

Monday, September 1, 2014

Sometimes things just work...

In PPQT V1 I emulated a great feature of Guiguts: the ability to limit find/replace to a range of text. For the savvy user this is very useful. Say you are formatting a big table, or the particular range of text like the index of a book. You'd like to do a multiple replace operation within that block of text. A typical example for a table of contents or an index would be to change all \b(\d+)\b page numbers into links to #Page_\1 anchors. You don't want to change every number in the book that way, only the ones in the block of text you are working on.

So you select a block of text and through some feature of the UI, tell the program that this is the range for find/replace operations. Then you can safely do a global replace and know that it will only affect that span.

I forget what the UI for this is in Guiguts. It was something arbitrary and tricky. PPQT V1 was no less arbitrary and tricky. You clicked the "In Selection" checkbox of the Find panel and then clicked either the First or Last search button. At that point the current selection got set as the range.

You had to trust that this was the case, however. Neither V1 nor its model Guiguts showed any visible indication that a search range had been set. You just had to hope you'd set it to cover all, but only, the text you wanted to work on.

For V2, I want to make the setting of the search range more intuitive and also give a visible sign of it. After the great success using the Extra Selections feature of the editor for the current line, I realized that this was the way to put a visible background color under the chosen find range. So what I did this morning was to make this happen:

What you see is a pale blue background showing the extent of a limited find/replace range. Superimposed on it is the pale yellow current-line highlight, and therein lies a tale.

First, the UI. For V2, the way you say "I want a restricted search range" is to click the "In Selection" checkbox making it checked. When that checkbox goes from unchecked to checked, I look at the current selection and if it is "large enough" (a rather arbitrary 100 characters or 4 lines) that selection is made the find-range. And it turns light blue and stays that way until you click "In Selection" off again. So much more obvious than the V1 rule, which I don't even want to think about, it's so dumb.

Displaying the blueness required changes in two other modules. First, all the colors are localized in colors.py, so I had to add get/set_find_range_brush methods to that, and to save and restore the find-range color choice from settings.

Second, the visible display of the document is up to editview.py, which was already handling the current-line mechanism. So for both practicality and MVC purity, that's where display of the range should be. So I generalized that code to always have not one, but two "extra" selections.

Recall that an extra selection is basically a tuple of a QTextCharFormat and a QTextCursor. Anytime any extra selection is altered, you have to call setExtraSelections with a list of them. Previously the list had only one element, the current-line selection. But it was easy to change that so the list always included two selections, one for current line and one for find-range.

For the current line, the related cursor gets updated whenever the edit cursor moves. But for the find-range selection, the related cursor has normally no selection, and thus has no visible effect. The findview code calls into the editview code to set_find_range(cursor) and that puts a text selection in the find-range cursor. And a clear_find_range() call takes it out again.

With that in place I could add the code to the half-done Find panel to use it, and test it and it worked! Well, except for one thing. When the cursor moved through the blue area, the yellow highlight of the current line disappeared. The blue background had priority over the yellow background. Awwww. But wait! The editview was initializing its list of extra selections so:

self.extra_sel_list = [self.current_line_sel, self.range_sel]

Just maybe... I reversed the order of the list,

self.extra_sel_list = [self.range_sel, self.current_line_sel]

and the current line brush now took priority over the blue range brush!

This doesn't seem to be documented but it's good to know: the "extra selection" list is prioritized left to right. Or maybe it goes back-most to front-most in visual depth?

Whatever. Find still has no code for finding anything, but all its widgets work and the In Selection highlight is purty.

Saturday, August 16, 2014

OK, I am embarrassed (again)

I try not to make a blithering idiot of myself in public too often, but in this matter of highlighting the current line, I certainly have. Proudly presenting code snippets showing an entirely wrong way of doing it... I blush.

OK the right way to highlight some line is to present it to the editor (QTextEdit or QPlainTextEdit) as an "extra selection." An extra selection is a ... what? It isn't a class on its own. It's an object that you acquire as follows:

        self.current_line_sel = QTextEdit.ExtraSelection()

You may have many of these; you tell the editor about them by passing a list of them to its setExtraSelections() method. In my case, a list of one item, a selection marking the current line.

An extra selection object is basically a tuple comprising a QTextCursor and a QTextCharFormat. These are assignable properties, there are no get-set methods for them. So here's the whole initialization.

        self.current_line_fmt = QTextCharFormat()
        self.current_line_fmt.setProperty(QTextFormat.FullWidthSelection, True)
        self.current_line_fmt.setBackground(colors.get_current_line_brush())
        self.current_line_sel = QTextEdit.ExtraSelection()
        self.current_line_sel.format = QTextCharFormat(self.current_line_fmt)

The bit about setting the FullWidthSelection property of the QTextCharFormat is key; it ensures that the new background color will be painted the width of the editor's viewport regardless of the length of the current line. I wouldn't have know that without seeing it in the Code Editor example.

When the cursor-move signal arrives, it is only necessary to update the position of the cursor in the selection, and to re-assign the list of selections. Here is the abbreviated cursor-move code now.

    def _cursor_moved(self):
        tc = QTextCursor(self.Editor.textCursor()) # copy of cursor
...several lines snipped...
        # Change the extra selection to the current line.
        tc.clearSelection()
        self.current_line_sel.cursor = tc
        self.Editor.setExtraSelections([self.current_line_sel])

It is necessary to clear the selection from the cursor before using it. Without that step, my current-line highlight disappears as soon as a non-empty selection is made. Double-click a word and it is highlighted, and the current-line highlight goes out.

It is also necessary to re-assign the list of extra selections every time. It is not enough to update the existing selection's cursor. You have to make the editor aware of the change. Which kind of makes sense.

Anyway that's all there was to it. The code is 50% less than the method I displayed in three previous blog posts. And it has no effect on the undo/redo stack or the document's modified status. So it's all good now, except for my ego.