Enabling and Disabling Actions

Sometimes particular actions are applicable only in certain circumstances. For example, it doesn't make much sense to allow "file save" on a document with no unsaved changes, or, arguably, to allow "file save as" on an empty document. Similarly, neither "edit copy" nor "edit cut" makes sense if there is no selected text. One way to deal with this is to leave all the actions enabled all the time, but to make sure that they do nothing in cases where they don't make sense; for example, if we call QTextEdit.cut(), it will safely do nothing if there is no selected text.

Another solution is to enable and disable actions depending on the application's state. This can be achieved by doing three things: First, making actions that will be enabled and disabled into instance variables so that they can be accessed outside of the initializer; second, creating a method (e.g., updateUi()) that enables and disables actions depending on the application's state; and third, making application-specific signal-slot connections to updateUi() so that it is called whenever the application's state changes.

Using the Python Editor as an example, we need these connections:

self.connect(self.editor,

SIGNAL("selectionChanged()"), self.updateUi) self.connect(self.editor.document(),

SIGNAL("modificationChanged(bool)"), self.updateUi) self.connect(QApplication.clipboard(),

SIGNAL("dataChanged()"), self.updateUi)

These connections mean that if the editor's selection changes, or if the document is modified, or if the clipboard's data changes, we can enable or disable the relevant actions.

def updateUi(self, arg=None):

self.fileSaveAction.setEnabled(

self.editor.document().isModified()) self.fileSaveAsAction.setEnabled(

not self.editor.document().isEmpty()) enable = self.editor.textCursor().hasSelection() self.editCopyAction.setEnabled(enable) self.editCutAction.setEnabled(enable) self.editPasteAction.setEnabled(self.editor.canPaste())

This method is called in response to the signal-slot connections earlier. It is also called explicitly at the end of the initializer to set the user interface's initial state, and at the end of the fileNew() method. The QText-Edit.canPaste() method was introduced with Qt 4.2; for earlier versions, use not QApplication.clipboard().text().isEmpty().The text cursor and text document classes are covered later in this chapter.

The event() handler is called before any of the specific key and mouse event QWidget. handlers, and it is the only place where we can intercept and handle Tab key evento presses. If the user pressed Tab we get a QTextCursor from the QTextEdit; this Insert-310« allows us to programmatically interact with the underlying QTextDocument ingtext that holds the text. By default, the text cursor returned by a text edit is at the with current insertion point (also called the cursor position), so we can simply insert four spaces. Then we return True to tell the event-handling system that we have handled the Tab key press and that no further action (such as changing focus) should take place.

To provide syntax highlighting we must create a QSyntaxHighlighter subclass, reimplement the highlightBlock() method, and create an instance of our highlighter with the QTextDocument we want it to apply to as an argument. We did the last part in the MainWindow's initializer, so now we will turn to our QSyntaxHighlighter subclass.

The QSyntaxHighlighter works on a line-by-line basis and provides a simple means of keeping track of state across multiple lines. For the Python Editor we will use regular expressions to identify the Python keywords, comments, and strings, including triple-quoted strings that span multiple lines, so that we can apply highlighting to them. We begin by setting up the regular expressions in the subclass's initializer, which we will look at in two parts.

class PythonHighlighter(QSyntaxHighlighter): Rules = []

def_init_(self, parent=None):

super(PythonHighlighter, self).__init__(parent)

keywordFormat = QTextCharFormat()

keywordFormat.setForeground(Qt.darkBlue)

keywordFormat.setFontWeight(QFont.Bold)

for pattern in ((r"\band\b", r"\bas\b", r"\bassert\b",

QTextCursor

r"\byield\b")): PythonHighlighter.Rules.append((QRegExp(pattern), keywordFormat))

The Rules static list holds a list of pairs. The first element of each pair is a regular expression (a QRegExp) that is used to match a syntactic construct that can occupy only a single line (such as a Python keyword). The second element is a QTextCharFormat, an object that can hold information regarding how a piece of text should be formatted, such as its font and the pen that should be used to paint it.

We have created a rule for each Python keyword, giving each one the same keywordFormat format. (Most of the keywords are not shown in the snippet as indicated by the ellipsis.) Each keyword has a \b before and after it; this is a regular expression symbol that does not match any text, but rather matches at a word boundary. This means, for example, that in the expression a and b, and will be recognized as a keyword, whereas in the expression a = sand, the and in sand will (correctly) not be recognized.

commentFormat = QTextCharFormat() commentFormat.setForeground(QColor(0, 127, 0)) commentFormat.setFontItalic(True) PythonHighlighter.Rules.append((QRegExp(r"#.*"), commentFormat)) self.stringFormat = QTextCharFormat() self.stringFormat.setForeground(Qt.darkYellow)

stringRe = QRegExp(r (?:,[^,]*,|"[^"]*"> )

stringRe.setMinimal(True)

PythonHighlighter.Rules.append((stringRe, self.stringFormat))

self.stringRe = QRegExp(r (:?"["]".*"["]"|"'i*'") )

self.stringRe.setMinimal(True) PythonHighlighter.Rules.append((self.stringRe, self.stringFormat))

After the keywords, we create a format and regular expression for handling Python comments. The regular expression is not perfect; it does not account for quoted # characters for example.

For strings, we keep the string format as an instance variable since we will need it in the highlightBlock() method when we handle multiline strings. Single-line strings are handled by naive (but fast) regular expressions set up in the initializer and added to the Rules list. At the end we create two more regular expressions. These both use negative lookahead; for example, (?!") means "not followed by "". They are for use in the highlightBlock() method, which we will review next in two parts.

def highlightBlock(self, text):

NORMAL, TRIPLESINGLE, TRIPLEDOUBLE = range(3)

for regex, format in PythonHighlighter.Rules: i = text.indexOf(regex) while i >= 0:

length = regex.matchedLength() self.setFormat(i, length, format) i = text.indexOf(regex, i + length)

The highlightBlock() method is called for every line that is displayed with the line in the QString text argument.

We begin by setting three possible states: normal, inside a triple-quoted string, and inside a triple double-quoted string. Then we iterate over every rule and wherever we find a match to the rule's regular expression, we set the text's format to the corresponding format for the length of the regular expression's match. The combination of a list of regular expression and format pairs and the for loop shown in the preceding code is sufficient for all syntax highlighting where each syntactic component only ever occupies a single line and where each is capable of being represented by a regular expression.

self.setCurrentBlockState(NORMAL) if text.indexOf(self.stringRe) != -1: return for i, state in ((text.indexOf(self.tripleSingleRe), TRIPLESINGLE),

(text.indexOf(self.tripleDoubleRe), TRIPLEDOUBLE)): if self.previousBlockState() == state: if i == -1:

i = text.length() self.setCurrentBlockState(state) self.setFormat(0, i + 3, self.stringFormat) elif i > -1:

self.setCurrentBlockState(state) self.setFormat(i, text.length(), self.stringFormat)

Next we set the current block's state to normal. The state is an integer of our choice that the QSyntaxHighlighter will associate with the current line. We then test to see whether we have a complete triple quoted string, that is, one that begins and ends in the line; if we do, we have already formatted it, so we are finished and can return.

In PyQt's text-handling classes such as QTextDocument, QTextEdit, and QSyntax-Highlighter, a block of text is essentially a sequence of characters delimited by a newline. For word processor-type documents this equates to a paragraph (since the text-handling class does the line wrapping), but for source code we insert newlines manually, so in this case each block is effectively a single line of code.

We now have three cases to deal with. We are either in a triple-quoted string that began on a previous line and that has not finished in this line; or we have the beginning or end of a triple-quoted string in this line.

If the previous line was in a triple-quoted string and there is no triple quote in this line, this entire line is still in a triple-quoted string, so we set the current block state to the same value as the previous line and format the entire line as triple-quoted. If the previous line's state is triple-quoted and we have a triple quote, it must be the closing triple quote, so we format triple-quoted up to and including the triple quote. In this case we leave the state as normal since that will apply from the end of the triple-quoted string onward. On the other hand, if the previous line's state was not triple-quoted and we find triple quotes, we set the state to triple-quoted and format from these quotes to the end of the line.

This completes our syntax highlighting example. Clearly we could use more sophisticated regular expressions, or even avoid them altogether and use a finite state automaton or a parser to identify which portions of each line require particular formatting. For large texts with complex syntax, syntax highlighting can be computationally expensive, but QSyntaxHighlighter helps to keep the overhead down by formatting only enough lines to correctly highlight the lines that are visible.

Was this article helpful?

0 0
Tube Jacker

Tube Jacker

Download Tube Jacker And Discover Everything You Need To Know About Jacking Unlimited Traffic From The Video Giant. The drop dead easy way to create winning video campaigns that will FLOOD your website with unstoppable FREE traffic, all on complete and total autopilot. How to exploit a sneaky method of boosting exposure and getting your videos to the top of Google within 72 hours, guaranteed.

Get My Free Ebook


Post a comment