class Editor(QtWidgets.QPlainTextEdit):
    """
    Source code editor used in Myokit.

    Provides the signal ``find_action(str)`` which is fired everything a find
    action occurred with a description that can be used in an application's
    status bar.
    """
    # Signal: Find action happened, update with text
    # Attributes: (description)
    find_action = QtCore.Signal(str)

    def __init__(self, parent=None):
        super(Editor, self).__init__(parent)
        # Apply default settings
        self._default_settings()
        # Add line number area
        self._line_number_area = LineNumberArea(self)
        self._line_number_area.update_width(0)
        # Add current line highlighting and bracket matching
        self.cursorPositionChanged.connect(self.cursor_changed)
        self.cursor_changed()
        # Find/replace dialog
        self._find = FindDialog(self)
        self._find.find_action.connect(self._find_action)
        # Line position
        self._line_offset = self.fontMetrics().width(' ' * 79)
        # Number of blocks in page up/down
        self._blocks_per_page = 1
        # Last position in line, used for smart up/down buttons
        self._last_column = None
        self.textChanged.connect(self._text_has_changed)

    def activate_find_dialog(self):
        """
        Activates the find/replace dialog for this editor: Shows the dialog if
        hidden, sets the focus to the query field and copies any current
        selection into the query field.
        """
        self._find.activate()

    def cursor_changed(self):
        """
        Slot: Called when the cursor position is changed
        """
        # Highlight current line
        extra_selections = []
        selection = QtWidgets.QTextEdit.ExtraSelection()
        selection.format.setBackground(COLOR_CURRENT_LINE)
        selection.format.setProperty(QtGui.QTextFormat.FullWidthSelection,
                                     True)
        selection.cursor = self.textCursor()
        selection.cursor.clearSelection()
        extra_selections.append(selection)
        # Bracket matching
        cursor = self.textCursor()
        if not cursor.hasSelection():
            # Test if in front of or behind an opening or closing bracket
            pos = cursor.position()
            bracket = None
            if not cursor.atEnd():
                cursor.setPosition(pos + 1, QtGui.QTextCursor.KeepAnchor)
                text = cursor.selectedText()
                if text in BRACKETS:
                    bracket = cursor
            elif bracket is None and not cursor.atStart():
                cursor.setPosition(pos - 1)
                cursor.setPosition(pos, QtGui.QTextCursor.KeepAnchor)
                text = cursor.selectedText()
                if text in BRACKETS:
                    bracket = cursor
            if bracket:
                # Find matching partner
                doc = self.document()
                depth = 1
                start = bracket.position()
                while depth > 0:
                    if text in BRACKETS_CLOSE:
                        other = doc.find(text, start - 1,
                                         QtGui.QTextDocument.FindBackward)
                        match = doc.find(BRACKETS[text], start - 1,
                                         QtGui.QTextDocument.FindBackward)
                    else:
                        other = doc.find(text, start)
                        match = doc.find(BRACKETS[text], start)
                    if match.isNull():
                        break
                    if other.isNull():
                        depth -= 1
                        start = match.position()
                    elif text in BRACKETS_CLOSE:
                        if other.position() < match.position():
                            depth -= 1
                            start = match.position()
                        else:
                            depth += 1
                            start = other.position()
                    else:
                        if match.position() < other.position():
                            depth -= 1
                            start = match.position()
                        else:
                            depth += 1
                            start = other.position()
                if depth == 0:
                    # Apply formatting
                    selection = QtWidgets.QTextEdit.ExtraSelection()
                    selection.cursor = bracket
                    selection.format.setBackground(COLOR_BG_BRACKET)
                    extra_selections.append(selection)
                    selection = QtWidgets.QTextEdit.ExtraSelection()
                    selection.cursor = match
                    selection.format.setBackground(COLOR_BG_BRACKET)
                    extra_selections.append(selection)
        if extra_selections:
            self.setExtraSelections(extra_selections)

    def cursor_position(self):
        """
        Returns a tuple ``(line, char)`` with the current cursor position. If
        a selection is made only the left position is used.

        Line and char counts both start at zero.
        """
        cursor = self.textCursor()
        line = cursor.blockNumber()
        char = cursor.selectionStart() - cursor.block().position()
        return (line, char)

    def _default_settings(self):
        """
        Applies this editor's default settings.
        """
        # Set font
        self.setFont(FONT)
        # Set frame
        self.setFrameStyle(QtWidgets.QFrame.WinPanel | QtWidgets.QFrame.Sunken)
        # Disable wrapping
        self.setLineWrapMode(self.NoWrap)
        # Set tab width (if ever seen) to 4 spaces
        self.setTabStopWidth(self.fontMetrics().width(' ' * 4))

    def _find_action(self, text):
        """
        Passes on the find action signal.
        """
        self.find_action.emit(text)

    def get_text(self):
        """
        Returns the text in this editor.
        """
        return self.toPlainText()

    def hide_find_dialog(self):
        """
        Hides the find/replace dialog for this editor.
        """
        self._find.hide()

    def jump_to(self, line, char):
        """
        Jumps to the given line and row.
        """
        block = self.document().findBlockByNumber(line)
        cursor = self.textCursor()
        cursor.setPosition(block.position() + char)
        self.setTextCursor(cursor)
        self.centerCursor()

    def keyPressEvent(self, event):
        """
        Qt event: A key was pressed.
        """
        # Get key and modifiers
        key = event.key()
        mod = event.modifiers()
        # Possible modifiers:
        #  NoModifier
        #  ShiftModifier, ControlModifier, AltModifiier
        #  MetaModifier (i.e. super key)
        #  KeyPadModifier (button is part of keypad)
        #  GroupSwitchModifier (x11 thing)
        # Ignore the keypad modifier, we don't care!
        if mod & Qt.KeypadModifier:
            mod = mod ^ Qt.KeypadModifier  # xor!
        # Actions per key/modifier combination
        if key == Qt.Key_Tab and mod == Qt.NoModifier:
            # Indent
            cursor = self.textCursor()
            start, end = cursor.selectionStart(), cursor.selectionEnd()
            if cursor.hasSelection():
                # Add single tab to all lines in selection
                cursor.beginEditBlock()  # Undo grouping
                doc = self.document()
                b = doc.findBlock(start)
                e = doc.findBlock(end).next()
                while b != e:
                    cursor.setPosition(b.position())
                    cursor.insertText(TABS * SPACE)
                    b = b.next()
                cursor.endEditBlock()
            else:
                # Insert spaces until next tab stop
                pos = cursor.positionInBlock()
                cursor.insertText((TABS - pos % TABS) * SPACE)
        elif key == Qt.Key_Backtab and mod == Qt.ShiftModifier:
            # Dedent all lines in selection (or single line if no selection)
            '''
            cursor = self.textCursor()
            start, end = cursor.selectionStart(), cursor.selectionEnd()
            cursor.beginEditBlock() # Undo grouping
            doc = self.document()
            # Get blocks in selection
            blocks = []
            b = doc.findBlock(start)
            while b.isValid() and b.position() <= end:
                blocks.append(b)
                b = b.next()
            # Dedent
            for b in blocks:
                t = b.text()
                p1 = b.position()
                p2 = p1 + min(4, len(t) - len(t.lstrip()))
                c = self.textCursor()
                c.setPosition(p1)
                c.setPosition(p2, QtGui.QTextCursor.KeepAnchor)
                c.removeSelectedText()
            cursor.endEditBlock()
            '''
            # This silly method is required because of a bug in qt4/qt5
            cursor = self.textCursor()
            start, end = cursor.selectionStart(), cursor.selectionEnd()
            first = self.document().findBlock(start)
            q = 0
            new_text = []
            new_start, new_end = start, end
            b = QtGui.QTextBlock(first)
            while b.isValid() and b.position() <= end:
                t = b.text()
                p = min(4, len(t) - len(t.lstrip()))
                new_text.append(t[p:])
                if b == first:
                    new_start -= p
                new_end -= p
                q += p
                b = b.next()
            last = b.previous()
            new_start = max(new_start, first.position())
            new_end = max(new_end, new_start)
            if q > 0:
                # Cut text, replace with new
                cursor.beginEditBlock()
                cursor.setPosition(first.position())
                cursor.setPosition(last.position() + last.length() - 1,
                                   QtGui.QTextCursor.KeepAnchor)
                cursor.removeSelectedText()
                cursor.insertText('\n'.join(new_text))
                cursor.endEditBlock()
                # Set new cursor
                cursor.setPosition(new_start)
                cursor.setPosition(new_end, QtGui.QTextCursor.KeepAnchor)
                self.setTextCursor(cursor)
        elif key == Qt.Key_Enter or key == Qt.Key_Return:
            # Enter/Return with modifier is overruled here to mean nothing
            # This is very important as the default for shift-enter is to
            # start a new line within the same block (this can't happen with
            # copy-pasting, so it's safe to just catch it here).
            if mod == Qt.NoModifier:
                # "Smart" enter:
                #   - If selection, selection is deleted
                #   - Else, autoindenting is performed
                cursor = self.textCursor()
                cursor.beginEditBlock()
                if cursor.hasSelection():
                    # Replace selection with newline,
                    cursor.removeSelectedText()
                    cursor.insertBlock()
                else:
                    # Insert new line with correct indenting
                    b = self.document().findBlock(cursor.position())
                    t = b.text()
                    i = t[:len(t) - len(t.lstrip())]
                    i = i[:cursor.positionInBlock()]
                    cursor.insertBlock()
                    cursor.insertText(i)
                cursor.endEditBlock()
                # Scroll if necessary
                self.ensureCursorVisible()
        elif key == Qt.Key_Home and (mod == Qt.NoModifier
                                     or mod == Qt.ShiftModifier):
            # Plain home button: move to start of line
            # If Control is used: Jump to start of document
            # Ordinary home button: Jump to first column or first
            # non-whitespace character
            cursor = self.textCursor()
            block = cursor.block()
            cp = cursor.position()
            bp = block.position()
            if cp != bp:
                # Jump to first column
                newpos = bp
                # Smart up/down:
                self._last_column = 0
            else:
                # Already at first column: Jump to first non-whitespace or
                # end of line if all whitespace
                t = block.text()
                indent = len(t) - len(t.lstrip())
                newpos = bp + indent
                # Smart up/down:
                self._last_column = indent
            # If Shift is used: only move position (keep anchor, i.e. select)
            anchor = (QtGui.QTextCursor.KeepAnchor if mod == Qt.ShiftModifier
                      else QtGui.QTextCursor.MoveAnchor)
            cursor.setPosition(newpos, anchor)
            self.setTextCursor(cursor)
        elif key == Qt.Key_Home and (mod == Qt.ControlModifier or mod
                                     == Qt.ControlModifier & Qt.ShiftModifier):
            # Move to start of document
            # If Shift is used: only move position (keep anchor, i.e. select)
            anchor = (QtGui.QTextCursor.KeepAnchor if mod == Qt.ShiftModifier
                      else QtGui.QTextCursor.MoveAnchor)
            cursor = self.textCursor()
            cursor.setPosition(0, anchor)
            self.setTextCursor(cursor)
        elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown) \
                and (mod == Qt.NoModifier or mod == Qt.ShiftModifier):
            # Move cursor up/down
            # Maintain the column position, even when the current row doesn't
            # have as many characters. Reset this behavior as soon as a
            # left/right home/end action is made or whenever the text is
            # changed.
            # Set up operation
            anchor = (QtGui.QTextCursor.KeepAnchor if mod == Qt.ShiftModifier
                      else QtGui.QTextCursor.MoveAnchor)
            operation = \
                QtGui.QTextCursor.PreviousBlock if key in (
                    Qt.Key_Up, Qt.Key_PageUp) else QtGui.QTextCursor.NextBlock
            n = 1 if key in (Qt.Key_Up,
                             Qt.Key_Down) else (self._blocks_per_page - 3)
            # Move
            cursor = self.textCursor()
            if self._last_column is None:
                # Update "smart" column
                self._last_column = cursor.positionInBlock()
            if cursor.movePosition(operation, anchor, n):
                column = min(cursor.block().length() - 1, self._last_column)
                cursor.setPosition(cursor.position() + column, anchor)
            else:
                # Up/Down beyond document start/end? Move cursor to document
                # start/end and update last column
                if operation == QtGui.QTextCursor.NextBlock:
                    cursor.movePosition(QtGui.QTextCursor.EndOfBlock, anchor)
                else:
                    cursor.movePosition(QtGui.QTextCursor.StartOfBlock, anchor)
                self._last_column = cursor.positionInBlock()
            self.setTextCursor(cursor)
        elif key in (Qt.Key_Left, Qt.Key_Right,
                     Qt.Key_End) and not (mod & Qt.AltModifier):
            # Allow all modifiers except alt
            # Reset smart up/down behavior
            self._last_column = None
            # Pass to parent class
            super(Editor, self).keyPressEvent(event)
        elif key == Qt.Key_Insert and mod == Qt.NoModifier:
            # Insert/replace
            self.setOverwriteMode(not self.overwriteMode())
        else:
            # Default keyboard shortcuts / functions:
            # Backspace             OK
            # Delete                OK
            # Control+C             OK
            # Control+V             OK
            # Control+X             OK
            # Control+Insert        OK
            # Shift+Insert          OK
            # Shift+Delete          OK
            # Control+Z             OK
            # Control+Y             OK
            # LeftArrow             Overwritten (maintained)
            # RightArrow            Overwritten (maintained)
            # UpArrow               Overwritten (maintained)
            # DownArrow             Overwritten (maintained)
            # Control+RightArrow    OK (Jump to next word)
            # Control+LeftArrow     OK (Jump to previous word)
            # Control+UpArrow       Removed
            # Control+Down Arrow    Removed
            # PageUp                Overwritten (maintained)
            # PageDown              Overwritten (maintained)
            # Home                  Overwritten (maintained)
            # End                   Overwritten (maintained)
            # Control+Home          Overwritten (maintained)
            # Control+End           Overwritten (maintained)
            # Alt+Wheel             OK (Horizontal scrolling)
            # Control+Wheel         OK (Fast scrolling)
            # Control+K             Removed
            # Not listed, but very important:
            # Shift-Enter           Starts new line within the same block!
            #                       Definitely removed
            # Ctrl-i                Undocumented, but inserts tab...
            ctrl_ignore = (Qt.Key_K, Qt.Key_I)
            if mod == Qt.ControlModifier and key in ctrl_ignore:
                # Control-K: ignore
                pass
            elif key == Qt.Key_Up or key == Qt.Key_Down:
                # Up/down with modifiers: ignore
                pass
            else:
                # Let parent class handle it
                super(Editor, self).keyPressEvent(event)

    def _line_number_area_width(self):
        """
        Returns the required width for the number area
        """
        return 4 + self.fontMetrics().width(str(max(1, self.blockCount())))

    def _line_number_area_paint(self, area, event):
        """
        Repaints the line number area.
        """
        # Repaint area
        rect = event.rect()
        etop = rect.top()
        ebot = rect.bottom()
        # Repaint metrics
        metrics = self.fontMetrics()
        height = metrics.height()
        width = area.width()
        # Create painter, get font metrics
        painter = QtGui.QPainter(area)
        painter.fillRect(rect, COLOR_BG_LINE_NUMBER)
        # Get top and bottom of first block
        block = self.firstVisibleBlock()
        geom = self.blockBoundingGeometry(block)
        btop = geom.translated(self.contentOffset()).top()
        bbot = btop + geom.height()
        # Iterate over blocks
        count = block.blockNumber()
        while block.isValid() and btop <= ebot:
            count += 1
            if block.isVisible() and bbot >= etop:
                painter.drawText(0, btop, width, height, Qt.AlignRight,
                                 str(count))
            block = block.next()
            btop = bbot
            bbot += self.blockBoundingRect(block).height()

    def paintEvent(self, e):
        """
        Paints this editor.
        """
        # Paint the editor
        super(Editor, self).paintEvent(e)
        # Paint a line between the editor and the line number area
        x = self.contentOffset().x() + self.document().documentMargin() \
            + self._line_offset
        p = QtGui.QPainter(self.viewport())
        p.setPen(QtGui.QPen(QtGui.QColor('#ddd')))
        rect = e.rect()
        p.drawLine(x, rect.top(), x, rect.bottom())

    def replace(self, text):
        """
        Replaces the current text with the given text, in a single operation
        that does not reset undo/redo.
        """
        self.selectAll()
        cursor = self.textCursor()
        cursor.beginEditBlock()
        cursor.removeSelectedText()
        self.appendPlainText(str(text))
        cursor.endEditBlock()

    def resizeEvent(self, event):
        """
        Qt event: Editor is resized.
        """
        super(Editor, self).resizeEvent(event)
        # Update line number area
        rect = self.contentsRect()
        self._line_number_area.setGeometry(rect.left(), rect.top(),
                                           self._line_number_area_width(),
                                           rect.height())
        # Set number of "blocks" per page
        font = self.fontMetrics()
        self._blocks_per_page = int(rect.height() / font.height())

    def save_config(self, config, section):
        """
        Saves this editor's configuration using the given :class:`ConfigParser`
        ``config``. Stores all settings in the section ``section``.
        """
        config.add_section(section)
        # Find options: case sensitive / whole word
        config.set(section, 'case_sensitive', self._find.case_sensitive())
        config.set(section, 'whole_word', self._find.whole_word())

    def load_config(self, config, section):
        """
        Loads this editor's configuration using the given :class:`ConfigParser`
        ``config``. Loads all settings from the section ``section``.
        """
        if config.has_section(section):
            # Find options: case sensitive / whole word
            if config.has_option(section, 'case_sensitive'):
                self._find.set_case_sensitive(
                    config.getboolean(section, 'case_sensitive'))
            if config.has_option(section, 'whole_word'):
                self._find.set_whole_word(
                    config.getboolean(section, 'whole_word'))

    def set_cursor(self, pos):
        """
        Changes the current cursor to the given position and scrolls so that
        its visible.
        """
        cursor = self.textCursor()
        cursor.setPosition(pos)
        self.setTextCursor(cursor)
        self.centerCursor()

    def set_text(self, text):
        """
        Replaces the text in this editor.
        """
        if text:
            self.setPlainText(str(text))
        else:
            # Bizarre workaround for bug:
            #   https://bugreports.qt.io/browse/QTBUG-42318
            self.selectAll()
            cursor = self.textCursor()
            cursor.removeSelectedText()
            doc = self.document()
            doc.clearUndoRedoStacks()
            doc.setModified(False)

    def show_find_dialog(self):
        """
        Displays a find/replace dialog for this editor.
        """
        self._find.show()

    def _text_has_changed(self):
        """
        Called whenever the text has changed, resets the smart up/down
        behavior.
        """
        self._last_column = None

    def toggle_comment(self):
        """
        Comments or uncomments the selected lines
        """
        # Comment or uncomment selected lines
        cursor = self.textCursor()
        start, end = cursor.selectionStart(), cursor.selectionEnd()
        doc = self.document()
        first, last = doc.findBlock(start), doc.findBlock(end)
        # Determine minimum indent and adding or removing
        block = first
        blocks = [first]
        while block != last:
            block = block.next()
            blocks.append(block)
        lines = [block.text() for block in blocks]
        indent = [len(t) - len(t.lstrip()) for t in lines if len(t) > 0]
        indent = min(indent) if indent else 0
        remove = True
        for line in lines:
            if line[indent:indent + 1] != '#':
                remove = False
                break
        cursor.beginEditBlock()
        if remove:
            for block in blocks:
                p = block.position() + indent
                cursor.setPosition(p)
                cursor.setPosition(p + 1, QtGui.QTextCursor.KeepAnchor)
                cursor.removeSelectedText()
        else:

            for block in blocks:
                p = block.position()
                n = len(block.text())
                if len(block.text()) < indent:
                    cursor.setPosition(p)
                    cursor.setPosition(p + n, QtGui.QTextCursor.KeepAnchor)
                    cursor.removeSelectedText()
                    cursor.insertText(' ' * indent + '#')
                else:
                    cursor.setPosition(p + indent)
                    cursor.insertText('#')
        cursor.endEditBlock()

    def trim_trailing_whitespace(self):
        """
        Trims all trailing whitespace from this document.
        """
        block = self.document().begin()
        cursor = self.textCursor()
        cursor.beginEditBlock()  # Undo grouping
        while block.isValid():
            t = block.text()
            a = len(t)
            b = len(t.rstrip())
            if a > b:
                cursor.setPosition(block.position() + b)
                cursor.setPosition(block.position() + a,
                                   QtGui.QTextCursor.KeepAnchor)
                cursor.removeSelectedText()
            block = block.next()
        cursor.endEditBlock()
class FindDialog(QtWidgets.QDialog):
    """
    Find/replace dialog for :class:`Editor`.
    """
    # Signal: Find action happened, update with text
    # Attributes: (description)
    find_action = QtCore.Signal(str)

    def __init__(self, editor):
        # New style doesn't work:
        QtWidgets.QDialog.__init__(self, editor, Qt.Window)
        self.setWindowTitle('Find and replace')
        self._editor = editor
        # Fix background color of line edits
        self.setStyleSheet('QLineEdit{background: white;}')
        # Create widgets
        self._close_button = QtWidgets.QPushButton('Close')
        self._close_button.clicked.connect(self.action_close)
        self._replace_all_button = QtWidgets.QPushButton('Replace all')
        self._replace_all_button.clicked.connect(self.action_replace_all)
        self._replace_button = QtWidgets.QPushButton('Replace')
        self._replace_button.clicked.connect(self.action_replace)
        self._find_button = QtWidgets.QPushButton('Find')
        self._find_button.clicked.connect(self.action_find)
        self._search_label = QtWidgets.QLabel('Search for')
        self._search_field = QtWidgets.QLineEdit()
        self._replace_label = QtWidgets.QLabel('Replace with')
        self._replace_field = QtWidgets.QLineEdit()
        self._case_check = QtWidgets.QCheckBox('Case sensitive')
        self._whole_check = QtWidgets.QCheckBox('Match whole word only')
        # Create layout
        text_layout = QtWidgets.QGridLayout()
        text_layout.addWidget(self._search_label, 0, 0)
        text_layout.addWidget(self._search_field, 0, 1)
        text_layout.addWidget(self._replace_label, 1, 0)
        text_layout.addWidget(self._replace_field, 1, 1)
        check_layout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.TopToBottom)
        check_layout.addWidget(self._case_check)
        check_layout.addWidget(self._whole_check)
        button_layout = QtWidgets.QGridLayout()
        button_layout.addWidget(self._close_button, 0, 0)
        button_layout.addWidget(self._replace_all_button, 0, 1)
        button_layout.addWidget(self._replace_button, 0, 2)
        button_layout.addWidget(self._find_button, 0, 3)
        layout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.TopToBottom)
        layout.addLayout(text_layout)
        layout.addLayout(check_layout)
        layout.addLayout(button_layout)
        self.setLayout(layout)
        self._search_field.setEnabled(True)
        self._replace_field.setEnabled(True)

    def action_close(self):
        """
        Qt slot: Close this window.
        """
        self.close()

    def action_find(self):
        """
        Qt slot: Find (next) item.
        """
        query = self._search_field.text()
        if query == '':
            self.find_action.emit('No query set')
            return
        flags = 0x0
        if self._case_check.isChecked():
            flags |= QtGui.QTextDocument.FindCaseSensitively
        if self._whole_check.isChecked():
            flags |= QtGui.QTextDocument.FindWholeWords
        if flags:
            found = self._editor.find(query, flags)
        else:
            found = self._editor.find(query)
        if found is False:
            # Not found? Try from top of document
            previous_cursor = self._editor.textCursor()
            cursor = self._editor.textCursor()
            cursor.setPosition(0)
            self._editor.setTextCursor(cursor)
            if flags:
                found = self._editor.find(query, flags)
            else:
                found = self._editor.find(query)
            if found is False:
                self._editor.setTextCursor(previous_cursor)
                self.find_action.emit('Query not found.')
                return
        cursor = self._editor.textCursor()
        line = 1 + cursor.blockNumber()
        char = cursor.selectionStart() - cursor.block().position()
        self.find_action.emit('Match found on line ' + str(line) + ' char ' +
                              str(char) + '.')

    def action_replace(self):
        """
        Qt slot: Replace found item with replacement.
        """
        query = self._search_field.text()
        replacement = self._replace_field.text()
        if query == '':
            self.find_action.emit('No query set')
            return
        cursor = self._editor.textCursor()
        a, b = cursor.selectedText(), query
        if not self._case_check.isChecked():
            a, b = a.lower(), b.lower()
        if a == b:
            cursor.insertText(replacement)
        self.action_find()

    def action_replace_all(self):
        """
        Qt slot: Replace all found items with replacement
        """
        query = self._search_field.text()
        replacement = self._replace_field.text()
        if query == '':
            self.find_action.emit('No query set')
            return
        flags = 0x0
        if self._case_check.isChecked():
            flags |= QtGui.QTextDocument.FindCaseSensitively
        if self._whole_check.isChecked():
            flags |= QtGui.QTextDocument.FindWholeWords
        n = 0
        found = True
        scrollpos = self._editor.verticalScrollBar().value()
        grouping = self._editor.textCursor()
        grouping.beginEditBlock()
        continue_from_top = True
        while found:
            if flags:
                found = self._editor.find(query, flags)
            else:
                found = self._editor.find(query)
            if not found and continue_from_top:
                # Not found? Try from top of document
                cursor = self._editor.textCursor()
                cursor.setPosition(0)
                self._editor.setTextCursor(cursor)
                if flags:
                    found = self._editor.find(query, flags)
                else:
                    found = self._editor.find(query)
                # Don't keep going round and round
                # (This can happen if you replace something with itself, or
                # with a different case version of itself in a case-insensitive
                # search).
                continue_from_top = False
            if found:
                cursor = self._editor.textCursor()
                cursor.insertText(replacement)
                n += 1
        grouping.endEditBlock()
        self._editor.setTextCursor(grouping)
        self._editor.verticalScrollBar().setValue(scrollpos)
        self.find_action.emit('Replaced ' + str(n) + ' occurrences.')

    def activate(self):
        """
        Activates this dialog.
        """
        # Check for selection
        cursor = self._editor.textCursor()
        if cursor.hasSelection():
            self._search_field.setText(cursor.selectedText())
        # Show dialog
        self.show()
        self.raise_()
        self.activateWindow()
        # Set focus
        self._search_field.selectAll()
        self._search_field.setFocus()

    def case_sensitive(self):
        """
        Returns ``True`` if this dialog is set for case-sensitive searching.
        """
        return self._case_check.isChecked()

    def keyPressEvent(self, event):
        """
        Qt event: A key-press reaches the dialog.
        """
        key = event.key()
        if key == Qt.Key_Enter or key == Qt.Key_Return:
            self.action_find()
        else:
            super(FindDialog, self).keyPressEvent(event)

    def set_case_sensitive(self, case_sensitive):
        """
        Sets/unsets the case-sensitive option.
        """
        return self._case_check.setChecked(bool(case_sensitive))

    def set_whole_word(self, whole_word):
        """
        Sets/unsets the whole-word option.
        """
        return self._whole_check.setChecked(bool(whole_word))

    def whole_word(self):
        """
        Returns ``True`` if this dialog is set for whole-word searching.
        """
        return self._whole_check.isChecked()
Exemple #3
0
class VideoView(QtWidgets.QGraphicsView):
    """
    Views a color data scene.
    """
    # Signals
    # The view was resized
    resize_event = QtCore.Signal()

    def __init__(self, scene):
        super(VideoView, self).__init__(scene)
        # Disable scrollbars (they can cause a cyclical resizing event!)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        # Always track mouse position
        self.setMouseTracking(True)
        # Set crosshair cursor
        self.setCursor(Qt.CrossCursor)
        # Set rendering hints
        self.setRenderHint(QtGui.QPainter.Antialiasing)
        self.setViewportUpdateMode(
            QtWidgets.QGraphicsView.BoundingRectViewportUpdate)
        # Fit scene rect in view
        self.fitInView(self.sceneRect(), keepAspect=True)
        self.setAlignment(Qt.AlignCenter)
        # Delayed resizing
        self._resize_timer = QtCore.QTimer()
        self._resize_timer.timeout.connect(self._resize_timeout)

    def fitInView(self, rect, keepAspect=False):
        """
        For some reason, Qt has a stupid bug in it that gives the scene a
        (hardcoded) margin of 2px. To remove, this is a re-implementation of
        the fitInView method, loosely based on the original C.
        
        https://bugreports.qt-project.org/browse/QTBUG-11945
        """
        # Reset the view scale
        try:
            unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1))
        except AttributeError:
            # PyQt4
            unity = self.matrix().mapRect(QtCore.QRectF(0, 0, 1, 1))
        w, h = max(1, unity.width()), max(1, unity.height())
        self.scale(1. / w, 1. / h)
        # Find the ideal scaling ratio
        viewRect = self.viewport().rect()
        try:
            sceneRect = self.transform().mapRect(rect)
        except AttributeError:
            # PyQt4
            sceneRect = self.matrix().mapRect(rect)
        xr = viewRect.width() / sceneRect.width()
        yr = viewRect.height() / sceneRect.height()
        if keepAspect:
            xr = xy = min(xr, yr)
        # Scale and center
        self.scale(xr, yr)
        self.centerOn(rect.center())

    def resizeEvent(self, event=None):
        """
        Called when the view is resized.
        """
        # Tell others a resize is happening
        # (This is used to pause the video playback)
        self.resize_event.emit()
        # Schedule delayed (and grouped) resize action
        self._resize_timer.start(100)

    def _resize_timeout(self, event=None):
        """
        Called a few ms after time out.
        """
        #import time
        #print(str(time.time()) + 'Resize!')
        self._resize_timer.stop()
        self.fitInView(self.sceneRect(), keepAspect=True)
Exemple #4
0
class VideoScene(QtWidgets.QGraphicsScene):
    """
    Color data display scene.
    """
    # Signals
    # Somebody moved the mouse
    # Attributes: cell x, cell y
    mouse_moved = QtCore.Signal(int, int)
    # Single click
    # Attributes: cell x, cell y
    single_click = QtCore.Signal(float, float)
    # Double click!
    # Attributes: cell x, cell y
    double_click = QtCore.Signal(float, float)

    def __init__(self, *args):
        super(VideoScene, self).__init__(*args)
        self.setBackgroundBrush(QtGui.QColor(192, 192, 192))
        self._w = None
        self._h = None
        self._p = None
        self.resize(1, 1, 0)

    def mousePressEvent(self, event):
        """
        Single-click
        """
        if event.button() == QtCore.Qt.LeftButton:
            if event.modifiers() == Qt.NoModifier:
                p = event.scenePos()
                x, y = int(p.x()), int(p.y())
                if x >= 0 and x < self._w and y >= 0 and y < self._h:
                    self.single_click.emit(x, y)
                    return

    def mouseDoubleClickEvent(self, event):
        """
        Double-click
        """
        if event.button() == QtCore.Qt.LeftButton:
            if event.modifiers() == Qt.NoModifier:
                p = event.scenePos()
                x, y = int(p.x()), int(p.y())
                if x >= 0 and x < self._w and y >= 0 and y < self._h:
                    self.double_click.emit(x, y)
                    return

    def mouseMoveEvent(self, event):
        """
        Show mouse position in status bar
        """
        p = event.scenePos()
        x, y = int(p.x()), int(p.y())
        self.mouse_moved.emit(x, y)

    def resize(self, w, h, p):
        """
        Resizes the scene to match the given dimensions.
        """
        self._w = float(w)
        self._h = float(h)
        self._p = float(p)  # Add room for colormap
        self.setSceneRect(0, 0, self._w + self._p, self._h)
Exemple #5
0
class GraphArea(QtWidgets.QWidget):
    """
    Area that can draw several graphs.
    """
    # Signals
    # Somebody moved the mouse
    # Attributes: cell x, cell y
    mouse_moved = QtCore.Signal(float, float)

    def __init__(self):
        super(GraphArea, self).__init__()
        self._data = None
        self._time = None
        self._temp_index = None
        self._temp_path = None
        self._frozen = collections.OrderedDict()
        self._scaling = {}
        self._position = 0
        self._color_temp = Qt.black
        self._color_cycle = [
            Qt.red,
            #Qt.green,
            Qt.blue,
            #Qt.cyan,
            Qt.magenta,
            #Qt.yellow,
            Qt.darkRed,
            Qt.darkGreen,
            Qt.darkBlue,
            Qt.darkCyan,
            Qt.darkMagenta,
            Qt.darkYellow,
        ]
        self._sw = 1.0
        self._sh = 1.0

    def clear(self):
        """
        Removes all graphs from the widget.
        """
        self._frozen = collections.OrderedDict()
        self._scaling = {}
        self._temp_path = self._temp_index = None
        self.update()

    def freeze(self):
        """
        Adds the temporary graph to the set of frozen graphs.
        """
        if self._temp_index and self._temp_path:
            self._frozen[self._temp_index] = self._temp_path
        self._temp_index = self._temp_path = None
        self.update()

    def graph(self, variable, x, y):
        """
        Adds temporary graph to this widget.
        """
        if self._data is None:
            return
        # Create index, check for duplicates
        variable = str(variable)
        x, y = int(x), int(y)
        index = (x, y, variable)
        if index == self._temp_index:
            return
        if index in self._frozen:
            self._temp_index = self._temp_path = None
            self.update()
            return
        # Get scaling info
        try:
            ymin, ymax = self._scaling[variable]
        except KeyError:
            data = self._data.get2d(variable)
            ymin = np.min(data)
            ymax = np.max(data)
            d = ymax - ymin
            ymin -= 0.05 * d
            ymax += 0.05 * d
            if ymin == ymax:
                ymin -= 1
                ymax += 1
            self._scaling[variable] = (ymin, ymax)
        # Create path, using real time and scaled y data
        xx = iter(self._tpad)
        yy = (self._data.trace(variable, x, y) - ymin) / (ymax - ymin)
        yy = iter(1 - yy)
        path = QtGui.QPainterPath()
        x, y = xx.next(), yy.next()
        path.moveTo(x, y)
        for i in xrange(1, len(self._time)):
            x, y = xx.next(), yy.next()
            path.lineTo(x, y)
        self._temp_index = index
        self._temp_path = path
        # Update!
        self.update()

    def log(self):
        """
        Returns a myokit DataLog containing the data currently displayed in the
        graph area.
        """
        d = myokit.DataLog()
        if self._data:
            d['engine.time'] = self._data.time()
            for index in self._frozen.iterkeys():
                x, y, variable = index
                d[variable, x, y] = self._data.trace(variable, x, y)
            if self._temp_index:
                x, y, variable = self._temp_index
                d[variable, x, y] = self._data.trace(variable, x, y)
        return d

    def minimumSizeHint(self):
        """
        Returns a minimum size.
        """
        return QtCore.QSize(250, 5)

    def mouseMoveEvent(self, event):
        """
        Show mouse position in status bar
        """
        p = event.pos()
        x, y = float(p.x()), float(p.y())
        self.mouse_moved.emit(x * self._sw, y * self._sh)

    def paintEvent(self, event):
        """
        Draws all the graphs.
        """
        if self._data is None:
            return
        # Create painter
        painter = QtGui.QPainter()
        painter.begin(self)
        # Fill background
        painter.fillRect(self.rect(), QtGui.QBrush(Qt.white))
        # Create coordinate system for graphs
        painter.scale(self.width() / self._tmax, self.height())
        painter.setRenderHint(QtGui.QPainter.Antialiasing)
        # Create pen
        pen = QtGui.QPen()
        pen.setWidth(0)
        # Draw frozen graphs
        colors = iter(self._color_cycle)
        for path in self._frozen.itervalues():
            try:
                pen.setColor(colors.next())
            except StopIteration:
                colors = iter(self._color_cycle)
                pen.setColor(colors.next())
            painter.setPen(pen)
            painter.drawPath(path)
        # Draw temp graph
        if self._temp_path:
            pen.setColor(self._color_temp)
            painter.setPen(pen)
            painter.drawPath(self._temp_path)
        # Show time indicator
        pen.setColor(Qt.red)
        painter.setPen(pen)
        painter.drawLine(self._position, 0, self._position, 1)
        # Finish
        painter.end()

    def resizeEvent(self, e=None):
        """
        Resized.
        """
        s = self.size()
        w, h = s.width(), s.height()
        self._sw = 1.0 / w if w > 0 else 1
        self._sh = 1.0 / h if h > 0 else 1

    def set_data(self, data):
        """
        Passes in the DataBlock2d this graph area extracts its data from.
        """
        self.clear()
        self._data = data
        self._time = data.time()
        tmin, tmax = self._time[0], self._time[-1]
        tpad = 0.01 * (tmax - tmin)
        self._tpad = self._time + tpad
        self._tmax = self._time[-1] + 2 * tpad

    def set_position(self, pos):
        """
        Sets the position of the time indicator.
        """
        if self._data:
            self._position = self._tpad[int(pos)]
            self.update()

    def sizeHint(self):
        """
        Returns a size suggestion.
        """
        return QtCore.QSize(250, 250)

    def sizePolicy(self):
        """
        Tells Qt that this widget shout expand.
        """
        return QtCore.QSizePolicy.Expanding
Exemple #6
0
class GraphArea(QtWidgets.QWidget):
    """
    Area that can draw several graphs.
    """
    # Signals
    # Somebody moved the mouse
    # Attributes: cell x, cell y
    mouse_moved = QtCore.Signal(float, float)

    def __init__(self):
        super(GraphArea, self).__init__()
        # DataBlock 2d, and its time vector
        self._data = None
        self._time = None

        # Index (x, y, variable) of temporary graph (if any)
        self._temp_index = None
        self._temp_path = None

        # Map from indices to paths for all frozen graphs
        self._frozen = collections.OrderedDict()

        # Last variable used in temp or frozen graph: used to scale mouse y
        # coordinate
        self._last_variable = None

        # Scaling per variable
        self._scaling = {}

        # Time scaled to fit
        self._time_scaled = None
        self._tmin = 0
        self._tmax = 1
        self._trange = self._tmax - self._tmin

        # Current position in time
        self._position = self._tmin

        # Colours for drawing
        self._color_temp = Qt.black
        self._color_cycle = [
            Qt.red,
            #Qt.green,
            Qt.blue,
            #Qt.cyan,
            Qt.magenta,
            #Qt.yellow,
            Qt.darkRed,
            Qt.darkGreen,
            Qt.darkBlue,
            Qt.darkCyan,
            Qt.darkMagenta,
            Qt.darkYellow,
        ]

        # Scaling factors from pixels to normalised (0, 1) coordinates. Updated
        # after every resize.
        self._sw = 1.0
        self._sh = 1.0

    def clear(self):
        """
        Removes all graphs from the widget.
        """
        self._frozen = collections.OrderedDict()
        self._scaling = {}
        self._temp_path = self._temp_index = None
        self._last_variable = None
        self.update()

    def freeze(self):
        """
        Adds the temporary graph to the set of frozen graphs.
        """
        if self._temp_index and self._temp_path:
            self._frozen[self._temp_index] = self._temp_path
        self._temp_index = self._temp_path = None
        self.update()

    def graph(self, variable, x, y):
        """
        Adds temporary graph to this widget.
        """
        if self._data is None:
            return

        # Create index, check for duplicates
        variable = self._last_variable = str(variable)
        x, y = int(x), int(y)
        index = (x, y, variable)
        if index == self._temp_index:
            return
        if index in self._frozen:
            self._temp_index = self._temp_path = None
            self.update()
            return

        # Get scaling info
        try:
            ymin, ymax = self._scaling[variable]
        except KeyError:
            data = self._data.get2d(variable)
            ymin = np.min(data)
            ymax = np.max(data)
            d = ymax - ymin
            ymin -= 0.05 * d
            ymax += 0.05 * d
            if ymin == ymax:
                ymin -= 1
                ymax += 1
            self._scaling[variable] = (ymin, ymax)

        # Create path, using real time and scaled y data
        xx = iter(self._time_scaled)
        yy = (self._data.trace(variable, x, y) - ymin) / (ymax - ymin)
        yy = iter(1 - yy)
        path = QtGui.QPainterPath()
        x, y = next(xx), next(yy)
        path.moveTo(x, y)
        for i in range(1, len(self._time)):
            x, y = next(xx), next(yy)
            path.lineTo(x, y)
        self._temp_index = index
        self._temp_path = path

        # Update!
        self.update()

    def log(self):
        """
        Returns a myokit DataLog containing the data currently displayed in the
        graph area.
        """
        d = myokit.DataLog()
        if self._data is not None:
            d['engine.time'] = self._data.time()
            for index in self._frozen.keys():
                x, y, variable = index
                d[variable, x, y] = self._data.trace(variable, x, y)
            if self._temp_index:
                x, y, variable = self._temp_index
                d[variable, x, y] = self._data.trace(variable, x, y)
        return d

    def minimumSizeHint(self):
        """
        Returns a minimum size.
        """
        return QtCore.QSize(250, 5)

    def mouseMoveEvent(self, event):
        """
        Trigger mouse moved event with graph coordinates.
        """
        if self._last_variable is None:
            return

        # Get normalised x, y coordinates ([0, 1])
        p = event.pos()
        x = float(p.x()) * self._sw
        y = 1 - float(p.y()) * self._sh

        # Scale x-axis according to time
        x = self._tmin + x * self._trange

        # Scale y-axis according to last shown variable
        ymin, ymax = self._scaling[self._last_variable]
        y = ymin + y * (ymax - ymin)

        # Emit event
        self.mouse_moved.emit(x, y)

    def paintEvent(self, event):
        """
        Draws all the graphs.
        """
        if self._data is None:
            return

        # Create painter
        painter = QtGui.QPainter()
        painter.begin(self)

        # Fill background
        painter.fillRect(self.rect(), QtGui.QBrush(Qt.white))

        # Create coordinate system for graphs
        painter.scale(self.width(), self.height())
        painter.setRenderHint(QtGui.QPainter.Antialiasing)

        # Create pen
        pen = QtGui.QPen()
        pen.setWidth(0)

        # Draw frozen graphs
        colors = iter(self._color_cycle)
        for path in self._frozen.values():
            try:
                pen.setColor(next(colors))
            except StopIteration:
                colors = iter(self._color_cycle)
                pen.setColor(next(colors))
            painter.setPen(pen)
            painter.drawPath(path)

        # Draw temp graph
        if self._temp_path:
            pen.setColor(self._color_temp)
            painter.setPen(pen)
            painter.drawPath(self._temp_path)

        # Show time indicator
        pen.setColor(Qt.red)
        painter.setPen(pen)
        t = (self._position - self._tmin) / self._trange
        painter.drawLine(QtCore.QLineF(t, 0, t, 1))

        # Finish
        painter.end()

    def resizeEvent(self, e=None):
        """
        Resized.
        """
        s = self.size()
        w, h = s.width(), s.height()
        self._sw = 1.0 / w if w > 0 else 1
        self._sh = 1.0 / h if h > 0 else 1

    def set_data(self, data):
        """
        Passes in the DataBlock2d this graph area extracts its data from.
        """
        self.clear()
        self._data = data
        self._time = data.time()

        self._tmin = self._time[0]
        self._tmax = self._time[-1]
        self._trange = self._tmax - self._tmin
        tpad = 0.01 * self._trange
        self._tmin -= tpad
        self._tmax += tpad
        self._trange += 2 * tpad
        self._time_scaled = (self._time - self._tmin) / self._trange

        self._position = self._tmin

    def set_position(self, pos):
        """
        Sets the position of the time indicator.
        """
        if self._data is not None:
            self._position = self._time[int(pos)]
            self.update()

    def sizeHint(self):
        """
        Returns a size suggestion.
        """
        return QtCore.QSize(250, 250)

    def sizePolicy(self):
        """
        Tells Qt that this widget shout expand.
        """
        return QtCore.QSizePolicy.Expanding
Exemple #7
0
class FindReplaceWidget(QtWidgets.QWidget):
    """
    Find/replace widget for :class:`Editor`.
    """
    # Signal: Find action happened, update with text
    # Attributes: (description)
    find_action = QtCore.Signal(str)

    def __init__(self, parent, editor):
        super(FindReplaceWidget, self).__init__(parent)
        self._editor = editor

        # Create widgets
        self._replace_all_button = QtWidgets.QPushButton('Replace all')
        self._replace_all_button.clicked.connect(self.action_replace_all)
        self._replace_button = QtWidgets.QPushButton('Replace')
        self._replace_button.clicked.connect(self.action_replace)
        self._find_button = QtWidgets.QPushButton('Find')
        self._find_button.clicked.connect(self.action_find)
        self._search_label = QtWidgets.QLabel('Search for')
        self._search_field = QtWidgets.QLineEdit()
        self._replace_label = QtWidgets.QLabel('Replace with')
        self._replace_field = QtWidgets.QLineEdit()
        self._case_check = QtWidgets.QCheckBox('Case sensitive')
        self._whole_check = QtWidgets.QCheckBox('Match whole word only')

        # Create layout
        text_layout = QtWidgets.QGridLayout()
        text_layout.addWidget(self._search_label, 0, 0)
        text_layout.addWidget(self._search_field, 0, 1)
        text_layout.addWidget(self._replace_label, 1, 0)
        text_layout.addWidget(self._replace_field, 1, 1)
        check_layout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.TopToBottom)
        check_layout.addWidget(self._case_check)
        check_layout.addWidget(self._whole_check)
        button_layout = QtWidgets.QGridLayout()
        button_layout.addWidget(self._replace_all_button, 0, 1)
        button_layout.addWidget(self._replace_button, 0, 2)
        button_layout.addWidget(self._find_button, 0, 3)

        layout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.TopToBottom)
        layout.addLayout(text_layout)
        layout.addLayout(check_layout)
        layout.addLayout(button_layout)
        layout.addStretch(1)
        self.setLayout(layout)

        # Accept keyboard focus on search and replace fields
        self._search_field.setEnabled(True)
        self._replace_field.setEnabled(True)

    def action_find(self):
        """ Qt slot: Find (next) item. """
        query = self._search_field.text()
        if query == '':
            self.find_action.emit('No query set')
            return
        flags = 0x0
        if self._case_check.isChecked():
            flags |= QtGui.QTextDocument.FindCaseSensitively
        if self._whole_check.isChecked():
            flags |= QtGui.QTextDocument.FindWholeWords
        if flags:
            found = self._editor.find(query, flags)
        else:
            found = self._editor.find(query)
        if found is False:
            # Not found? Try from top of document
            previous_cursor = self._editor.textCursor()
            previous_scroll = self._editor.verticalScrollBar().value()
            cursor = self._editor.textCursor()
            cursor.setPosition(0)
            self._editor.setTextCursor(cursor)
            if flags:
                found = self._editor.find(query, flags)
            else:
                found = self._editor.find(query)
            if found is False:
                self._editor.setTextCursor(previous_cursor)
                self._editor.verticalScrollBar().setValue(previous_scroll)
                self.find_action.emit('Query not found.')
                return
        cursor = self._editor.textCursor()
        line = 1 + cursor.blockNumber()
        char = cursor.selectionStart() - cursor.block().position()
        self.find_action.emit(
            'Match found on line ' + str(line) + ' char ' + str(char) + '.')

    def action_replace(self):
        """ Qt slot: Replace found item with replacement. """
        query = self._search_field.text()
        replacement = self._replace_field.text()
        if query == '':
            self.find_action.emit('No query set')
            return
        cursor = self._editor.textCursor()
        a, b = cursor.selectedText(), query
        if not self._case_check.isChecked():
            a, b = a.lower(), b.lower()
        if a == b:
            cursor.insertText(replacement)
        self.action_find()

    def action_replace_all(self):
        """ Qt slot: Replace all found items with replacement """
        query = self._search_field.text()
        replacement = self._replace_field.text()
        if query == '':
            self.find_action.emit('No query set')
            return
        flags = 0x0
        if self._case_check.isChecked():
            flags |= QtGui.QTextDocument.FindCaseSensitively
        if self._whole_check.isChecked():
            flags |= QtGui.QTextDocument.FindWholeWords
        n = 0
        found = True
        scrollpos = self._editor.verticalScrollBar().value()
        grouping = self._editor.textCursor()
        grouping.beginEditBlock()
        continue_from_top = True
        while found:
            if flags:
                found = self._editor.find(query, flags)
            else:
                found = self._editor.find(query)
            if not found and continue_from_top:
                # Not found? Try from top of document
                cursor = self._editor.textCursor()
                cursor.setPosition(0)
                self._editor.setTextCursor(cursor)
                if flags:
                    found = self._editor.find(query, flags)
                else:
                    found = self._editor.find(query)
                # Don't keep going round and round
                # (This can happen if you replace something with itself, or
                # with a different case version of itself in a case-insensitive
                # search).
                continue_from_top = False
            if found:
                cursor = self._editor.textCursor()
                cursor.insertText(replacement)
                n += 1
        grouping.endEditBlock()
        self._editor.setTextCursor(grouping)
        self._editor.verticalScrollBar().setValue(scrollpos)
        self.find_action.emit('Replaced ' + str(n) + ' occurrences.')

    def activate(self):
        """ Updates the contents of the search field and gives it focus. """
        cursor = self._editor.textCursor()
        if cursor.hasSelection():
            self._search_field.setText(cursor.selectedText())
        self._search_field.selectAll()
        self._search_field.setFocus()

    def keyPressEvent(self, event):
        """ Qt event: A key-press reaches the widget. """
        key = event.key()
        if key == Qt.Key_Enter or key == Qt.Key_Return:
            self.action_find()
        else:
            super(FindReplaceWidget, self).keyPressEvent(event)

    def load_config(self, config, section):
        """
        Loads this search's configuration using the given :class:`ConfigParser`
        ``config``. Loads all settings from the section ``section``.
        """
        if config.has_section(section):
            # Find options: case sensitive / whole word
            if config.has_option(section, 'case_sensitive'):
                self._case_check.setChecked(
                    config.getboolean(section, 'case_sensitive'))
            if config.has_option(section, 'whole_word'):
                self._whole_check.setChecked(
                    config.getboolean(section, 'whole_word'))

    def save_config(self, config, section):
        """
        Saves this search's configuration using the given :class:`ConfigParser`
        ``config``. Stores all settings in the section ``section``.
        """
        config.add_section(section)
        # Find options: case sensitive / whole word
        config.set(section, 'case_sensitive', self._case_check.isChecked())
        config.set(section, 'whole_word', self._whole_check.isChecked())