Пример #1
0
class SourceEditor(wx.Panel):
    def __init__(self, parent, title, data_validator):
        wx.Panel.__init__(self, parent)
        self._syntax_colorization_help_exists = False
        self._data_validator = data_validator
        self._data_validator.set_editor(self)
        self._parent = parent
        self._create_ui(title)
        self._data = None
        self._dirty = False
        self._positions = {}

    def is_focused(self):
        foc = wx.Window.FindFocus()
        return any(elem == foc for elem in [self] + list(self.GetChildren()))

    def _create_ui(self, title):
        self.SetSizer(VerticalSizer())
        self._create_editor_toolbar()
        self._create_editor_text_control()
        self._parent.add_tab(self, title, allow_closing=False)

    def _create_editor_toolbar(self):
        # needs extra container, since we might add helper text about syntax colorization
        self.editor_toolbar = HorizontalSizer()
        default_components = HorizontalSizer()
        default_components.add_with_padding(
            ButtonWithHandler(self,
                              'Apply Changes',
                              handler=lambda e: self.save()))
        self._create_search(default_components)
        self.editor_toolbar.add_expanding(default_components)
        self.Sizer.add_expanding(self.editor_toolbar, propotion=0)

    def _create_search(self, container_sizer):
        container_sizer.AddSpacer(20)
        self._search_field = TextField(self, '', process_enters=True)
        self._search_field.Bind(wx.EVT_TEXT_ENTER, self.OnFind)
        container_sizer.add_with_padding(self._search_field)
        container_sizer.add_with_padding(
            ButtonWithHandler(self, 'Search', handler=self.OnFind))
        self._search_field_notification = Label(self, label='')
        container_sizer.add_with_padding(self._search_field_notification)

    def create_syntax_colorization_help(self):
        if self._syntax_colorization_help_exists:
            return
        label = Label(
            self,
            label="Syntax colorization disabled due to missing requirements.")
        link = wx.HyperlinkCtrl(self, -1, label="Get help", url="")
        link.Bind(wx.EVT_HYPERLINK, self.show_help_dialog)
        flags = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT
        syntax_colorization_help_sizer = wx.BoxSizer(wx.VERTICAL)
        syntax_colorization_help_sizer.AddMany([(label, 0, flags),
                                                (link, 0, flags)])
        self.editor_toolbar.add_expanding(syntax_colorization_help_sizer)
        self.Layout()
        self._syntax_colorization_help_exists = True

    def show_help_dialog(self, event):
        content = """<h1>Syntax colorization</h1>
        <p>
        Syntax colorization for Text Edit uses <a href='http://pygments.org/'>Pygments</a> syntax highlighter.
        </p>
        <p>
        Install Pygments from command line with:
        <pre>
            pip install pygments
        </pre>
        Or:
        <pre>
            easy_install pygments
        </pre>
        Then, restart RIDE.
        </p>
        <p>
        If you do not have pip or easy_install,
        <a href='http://pythonhosted.org/an_example_pypi_project/setuptools.html#installing-setuptools-and-easy-install'>follow
        these instructions</a>.
        </p>
        <p>
        For more information about installing Pygments, <a href='http://pygments.org/download/'>see the site</a>.
        </p>
        """
        HtmlDialog("Getting syntax colorization", content).Show()

    def store_position(self):
        if self._editor:
            self._positions[
                self.datafile_controller] = self._editor.GetCurrentPos()

    def set_editor_caret_position(self):
        position = self._positions.get(self.datafile_controller, None)
        if position:
            self._editor.SetFocus()
            self._editor.SetCurrentPos(position)
            self._editor.SetSelection(position, position)

    @property
    def dirty(self):
        return self._dirty

    @property
    def datafile_controller(self):
        return self._data._data if self._data else None

    def OnFind(self, event):
        if self._editor:
            self._find()

    def OnFindBackwards(self, event):
        if self._editor:
            self._find(forward=False)

    def _find(self, forward=True):
        txt = self._search_field.GetValue()
        position = self._find_text_position(forward, txt)
        self._show_search_results(position, txt)

    # FIXME: This must be cleaned up
    def _find_text_position(self, forward, txt):
        file_end = len(self._editor.utf8_text)
        search_end = file_end if forward else 0
        anchor = self._editor.GetAnchor()
        anchor += 1 if forward else 0
        position = self._editor.FindText(anchor, search_end, txt, 0)
        if position == -1:
            start, end = (0, file_end) if forward else (file_end - 1, 0)
            position = self._editor.FindText(start, end, txt, 0)
        return position

    def _show_search_results(self, position, txt):
        if position != -1:
            self._editor.SetSelection(position, position + len(txt))
            self._search_field_notification.SetLabel('')
        else:
            self._search_field_notification.SetLabel('No matches found.')

    def open(self, data):
        self.reset()
        self._data = data
        if not self._editor:
            self._stored_text = self._data.content
        else:
            self._editor.set_text(self._data.content)
            self.set_editor_caret_position()

    def selected(self, data):
        if not self._editor:
            self._create_editor_text_control(self._stored_text)
        if self._data == data:
            return
        self.open(data)

    def reset(self):
        self._dirty = False

    def save(self, *args):
        if self.dirty:
            if not self._data_validator.validate_and_update(
                    self._data, self._editor.utf8_text):
                return False
        return True

    def delete(self):
        if self._editor.GetSelectionStart() == self._editor.GetSelectionEnd():
            self._editor.CharRight()
        self._editor.DeleteBack()

    def cut(self):
        self._editor.Cut()

    def copy(self):
        self._editor.Copy()

    def paste(self):
        focus = wx.Window.FindFocus()
        if focus == self._editor:
            self._editor.Paste()
        elif focus == self._search_field:
            self._search_field.Paste()

    def select_all(self):
        self._editor.SelectAll()

    def undo(self):
        self._editor.Undo()

    def redo(self):
        self._editor.Redo()

    def remove_and_store_state(self):
        if self._editor:
            self.store_position()
            self._stored_text = self._editor.GetText()
            self._editor.Destroy()
            self._editor = None

    def _create_editor_text_control(self, text=None):
        self._editor = RobotDataEditor(self)
        self.Sizer.add_expanding(self._editor)
        self.Sizer.Layout()
        if text is not None:
            self._editor.set_text(text)
        self._editor.Bind(wx.EVT_KEY_UP, self.OnEditorKey)
        self._editor.Bind(wx.EVT_KILL_FOCUS, self.LeaveFocus)
        self._editor.Bind(wx.EVT_SET_FOCUS, self.GetFocus)

    def LeaveFocus(self, event):
        self._editor.SetCaretPeriod(0)
        self.save()

    def GetFocus(self, event):
        self._editor.SetCaretPeriod(500)
        event.Skip()

    def _revert(self):
        self.reset()
        self._editor.set_text(self._data.content)

    def OnEditorKey(self, event):
        if not self.dirty and self._editor.GetModify():
            self._mark_file_dirty()
        event.Skip()

    def _mark_file_dirty(self):
        if self._data:
            self._dirty = True
            self._data.mark_data_dirty()
Пример #2
0
class SourceEditor(wx.Panel):
    def __init__(self, parent, title, data_validator):
        wx.Panel.__init__(self, parent)
        self._syntax_colorization_help_exists = False
        self._data_validator = data_validator
        self._data_validator.set_editor(self)
        self._parent = parent
        self._title = title
        self._tab_size = self._parent._app.settings.get(
            'txt number of spaces', 4)
        self._create_ui(title)
        self._data = None
        self._dirty = False
        self._position = None
        self._showing_list = False
        self._tab_open = None
        # self._autocomplete = None
        PUBLISHER.subscribe(self.OnSettingsChanged, RideSettingsChanged)
        PUBLISHER.subscribe(self.OnTabChange, RideNotebookTabChanging)

    def is_focused(self):
        # foc = wx.Window.FindFocus()
        # return any(elem == foc for elem in [self]+list(self.GetChildren()))
        return self._tab_open == self._title

    def OnTabChange(self, message):
        self._tab_open = message.newtab

    def _create_ui(self, title):
        self.SetSizer(VerticalSizer())
        self._create_editor_toolbar()
        self._create_editor_text_control()
        self._parent.add_tab(self, title, allow_closing=False)

    def _create_editor_toolbar(self):
        # needs extra container, since we might add helper
        # text about syntax colorization
        self.editor_toolbar = HorizontalSizer()
        default_components = HorizontalSizer()
        default_components.add_with_padding(
            ButtonWithHandler(self,
                              'Apply Changes',
                              handler=lambda e: self.save()))
        self._create_search(default_components)
        self.editor_toolbar.add_expanding(default_components)
        self.Sizer.add_expanding(self.editor_toolbar, propotion=0)

    def _create_search(self, container_sizer):
        container_sizer.AddSpacer(20)
        self._search_field = TextField(self, '', process_enters=True)
        self._search_field.Bind(wx.EVT_TEXT_ENTER, self.OnFind)
        container_sizer.add_with_padding(self._search_field)
        container_sizer.add_with_padding(
            ButtonWithHandler(self, 'Search', handler=self.OnFind))
        self._search_field_notification = Label(self, label='')
        container_sizer.add_with_padding(self._search_field_notification)

    def create_syntax_colorization_help(self):
        if self._syntax_colorization_help_exists:
            return
        label = Label(
            self,
            label="Syntax colorization disabled due to missing requirements.")
        link = HyperlinkCtrl(self, -1, label="Get help", url="")
        link.Bind(EVT_HYPERLINK, self.show_help_dialog)
        flags = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT
        syntax_colorization_help_sizer = wx.BoxSizer(wx.VERTICAL)
        syntax_colorization_help_sizer.AddMany([(label, 0, flags),
                                                (link, 0, flags)])
        self.editor_toolbar.add_expanding(syntax_colorization_help_sizer)
        self.Layout()
        self._syntax_colorization_help_exists = True

    def show_help_dialog(self, event):
        content = """<h1>Syntax colorization</h1>
        <p>
        Syntax colorization for Text Edit uses <a href='http://pygments.org/'>Pygments</a> syntax highlighter.
        </p>
        <p>
        Install Pygments from command line with:
        <pre>
            pip install pygments
        </pre>
        Or:
        <pre>
            easy_install pygments
        </pre>
        Then, restart RIDE.
        </p>
        <p>
        If you do not have pip or easy_install,
        <a href='http://pythonhosted.org/an_example_pypi_project/setuptools.html#installing-setuptools-and-easy-install'>follow
        these instructions</a>.
        </p>
        <p>
        For more information about installing Pygments, <a href='http://pygments.org/download/'>see the site</a>.
        </p>
        """
        HtmlDialog("Getting syntax colorization", content).Show()

    def store_position(self, force=False):
        if self._editor and self.datafile_controller:
            cur_pos = self._editor.GetCurrentPos()
            if cur_pos > 0:  # Cheating because it always go to zero
                self._position = cur_pos
                self._editor.GotoPos(self._position)

    def set_editor_caret_position(self):
        if not self.is_focused():  # DEBUG was typing text when at Grid Editor
            return
        position = self._position
        self._editor.SetFocus()
        if position:
            self._editor.SetCurrentPos(position)
            self._editor.SetSelection(position, position)
            self._editor.SetAnchor(position)
            self._editor.GotoPos(position)
            self._editor.Refresh()
            self._editor.Update()

    @property
    def dirty(self):
        return self._dirty

    @property
    def datafile_controller(self):
        return self._data._data if self._data else None

    def OnFind(self, event):
        if self._editor:
            text = self._editor.GetSelectedText()
            if len(text) > 0 and text.lower() != self._search_field.GetValue(
            ).lower():
                self._search_field.SelectAll()
                self._search_field.Clear()
                self._search_field.Update()
                self._search_field.SetValue(text)
                self._search_field.SelectAll()
                self._search_field.Update()
            self._find()

    def OnFindBackwards(self, event):
        if self._editor:
            self._find(forward=False)

    def _find(self, forward=True):
        txt = self._search_field.GetValue().encode('utf-8')
        position = self._find_text_position(forward, txt)
        self._show_search_results(position, txt)

    # FIXME: This must be cleaned up
    def _find_text_position(self, forward, txt):
        file_end = len(self._editor.utf8_text)
        search_end = file_end if forward else 0
        anchor = self._editor.GetAnchor()
        anchor += 1 if forward else 0
        position = self._editor.FindText(anchor, search_end, txt, 0)
        if position == -1:
            start, end = (0, file_end) if forward else (file_end - 1, 0)
            position = self._editor.FindText(start, end, txt, 0)
        return position

    def _show_search_results(self, position, txt):
        if position != -1:
            self._editor.SetCurrentPos(position)
            self._editor.SetSelection(position, position + len(txt))
            self._editor.ScrollToLine(self._editor.GetCurrentLine())
            self._search_field_notification.SetLabel('')
        else:
            self._search_field_notification.SetLabel('No matches found.')

    def OnContentAssist(self, event):
        self._showing_list = False
        if not self.is_focused():
            return
        self.store_position()
        selected = self._editor.get_selected_or_near_text()
        sugs = [
            s.name for s in self._suggestions.get_suggestions(selected or '')
        ]
        if sugs:
            self._editor.AutoCompSetDropRestOfWord(True)
            self._editor.AutoCompSetSeparator(ord(';'))
            self._editor.AutoCompShow(0, ";".join(sugs))
            self._showing_list = True

    def open(self, data):
        self.reset()
        self._data = data
        try:
            self._suggestions = SuggestionSource(None, data._data.tests[0])
        except IndexError:  # It is a new project, no content yet
            self._suggestions = SuggestionSource(None,
                                                 BuiltInLibrariesSuggester())
        if not self._editor:
            self._stored_text = self._data.content
        else:
            self._editor.set_text(self._data.content)
            self.set_editor_caret_position()

    def selected(self, data):
        if not self._editor:
            self._create_editor_text_control(self._stored_text)
        if self._data == data:
            return
        self.open(data)

    def auto_ident(self):
        if not self.is_focused():
            return
        line, _ = self._editor.GetCurLine()
        lenline = len(line)
        linenum = self._editor.GetCurrentLine()
        if lenline > 0:
            idx = 0
            while idx < lenline and line[idx] == ' ':
                idx += 1
            tsize = idx // self._tab_size
            if 3 < idx < lenline and line.strip().startswith("FOR"):
                tsize += 1
            elif linenum > 0 and tsize == 0:  # Advance if first task/test case or keyword
                prevline = self._editor.GetLine(linenum - 1).lower()
                if prevline.startswith("**") and not (
                        "variables" in prevline or "settings" in prevline):
                    tsize = 1
            self._editor.NewLine()
            while tsize > 0:
                self.write_ident()
                tsize -= 1
        else:
            self._editor.NewLine()
        pos = self._editor.GetCurrentLine()
        self._editor.SetCurrentPos(self._editor.GetLineEndPosition(pos))
        self.store_position()

    def deindent_block(self):
        start, end = self._editor.GetSelection()
        caret = self._editor.GetCurrentPos()
        ini_line = self._editor.LineFromPosition(start)
        end_line = self._editor.LineFromPosition(end)
        count = 0
        self._editor.SelectNone()
        line = ini_line
        inconsistent = False
        self._editor.BeginUndoAction()
        while line <= end_line:
            inconsistent = False
            pos = self._editor.PositionFromLine(line)
            self._editor.SetCurrentPos(pos)
            self._editor.SetSelection(pos, pos)
            self._editor.SetInsertionPoint(pos)
            content = self._editor.GetRange(pos, pos + self._tab_size)
            if content == (' ' * self._tab_size):
                self._editor.DeleteRange(pos, self._tab_size)
                count += 1
                line += 1
            else:
                inconsistent = True
                break
        self._editor.EndUndoAction()
        if inconsistent:
            self._editor.Undo()
            return
        new_start = max(0, start - self._tab_size)
        new_end = max(0, end - (count * self._tab_size))
        if caret == start:
            ini = new_start
            fini = new_end
        else:
            ini = new_end
            fini = new_start
        self._editor.SetSelection(new_start, new_end)
        self._editor.SetCurrentPos(ini)
        self._editor.SetAnchor(fini)

    def indent_block(self):
        start, end = self._editor.GetSelection()
        caret = self._editor.GetCurrentPos()
        ini_line = self._editor.LineFromPosition(start)
        end_line = self._editor.LineFromPosition(end)
        count = 0
        self._editor.SelectNone()
        line = ini_line
        while line <= end_line:
            pos = self._editor.PositionFromLine(line)
            self._editor.SetCurrentPos(pos)
            self._editor.SetSelection(pos, pos)
            self._editor.SetInsertionPoint(pos)
            self.write_ident()
            count += 1
            line += 1
        new_start = start + self._tab_size
        new_end = end + (count * self._tab_size)
        if caret == start:
            ini = new_start
            fini = new_end
        else:
            ini = new_end
            fini = new_start
        self._editor.SetSelection(new_start, new_end)
        self._editor.SetCurrentPos(ini)
        self._editor.SetAnchor(fini)

    def write_ident(self):
        spaces = ' ' * self._tab_size
        self._editor.WriteText(spaces)

    def reset(self):
        self._dirty = False

    def save(self, *args):
        self.store_position()
        if self.dirty and not self._data_validator.validate_and_update(
                self._data, self._editor.utf8_text):
            return False
        self.GetFocus(None)
        return True

    def delete(self):
        if IS_WINDOWS:
            if self._editor.GetSelectionStart(
            ) == self._editor.GetSelectionEnd():
                self._editor.CharRight()
            self._editor.DeleteBack()

    def cut(self):
        self._editor.Cut()

    def copy(self):
        self._editor.Copy()

    def paste(self):
        focus = wx.Window.FindFocus()
        if focus == self._editor:
            self._editor.Paste()
        elif focus == self._search_field:
            self._search_field.Paste()

    def select_all(self):
        self._editor.SelectAll()

    def undo(self):
        self._editor.Undo()
        self.store_position()

    def redo(self):
        self._editor.Redo()
        self.store_position()

    def remove_and_store_state(self):
        if self._editor:
            self.store_position()
            self._stored_text = self._editor.GetText()

    def _create_editor_text_control(self, text=None):
        self._editor = RobotDataEditor(self)
        self.Sizer.add_expanding(self._editor)
        self.Sizer.Layout()
        if text is not None:
            self._editor.set_text(text)
        self._editor.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
        self._editor.Bind(wx.EVT_CHAR, self.OnChar)
        self._editor.Bind(wx.EVT_KEY_UP, self.OnEditorKey)
        self._editor.Bind(wx.EVT_KILL_FOCUS, self.LeaveFocus)
        self._editor.Bind(wx.EVT_SET_FOCUS, self.GetFocus)
        # TODO Add here binding for keyword help

    def LeaveFocus(self, event):
        self._editor.AcceptsFocusFromKeyboard()
        self.store_position()
        self._editor.SetCaretPeriod(0)

    def GetFocus(self, event):
        self._editor.SetFocus()
        self._editor.AcceptsFocusFromKeyboard()
        self._editor.SetCaretPeriod(500)
        if self._position:
            self.set_editor_caret_position()
        if event:
            event.Skip()

    def _revert(self):
        self.reset()
        self._editor.set_text(self._data.content)

    def OnEditorKey(self, event):
        if not self.is_focused():  # DEBUG was typing text when at Grid Editor
            return
        keycode = event.GetKeyCode()
        if (keycode >=
                ord(' ')) and not self.dirty and self._editor.GetModify():
            self._mark_file_dirty()
        event.Skip()

    def OnKeyDown(self, event):
        if not self.is_focused():
            self.GetFocus(None)
        keycode = event.GetUnicodeKey()
        if event.GetKeyCode(
        ) == wx.WXK_TAB and not event.ControlDown() and not event.ShiftDown():
            selected = self._editor.GetSelection()
            if selected[0] == selected[1]:
                self.write_ident()
            else:
                self.indent_block()
        elif event.GetKeyCode() == wx.WXK_TAB and event.ShiftDown():
            selected = self._editor.GetSelection()
            if selected[0] == selected[1]:
                pos = self._editor.GetCurrentPos()
                self._editor.SetCurrentPos(max(0, pos - self._tab_size))
                self.store_position()
                if not event.ControlDown():  # No text selection
                    pos = self._editor.GetCurrentPos()
                    self._editor.SetSelection(pos, pos)
            else:
                self.deindent_block()
        elif event.GetKeyCode() in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]:
            if not self._showing_list:
                self.auto_ident()
            else:
                self._showing_list = False
                event.Skip()
        elif keycode in (ord('1'), ord('2'), ord('5')) and event.ControlDown():
            self.execute_variable_creator(list_variable=(keycode == ord('2')),
                                          dict_variable=(keycode == ord('5')))
            self.store_position()
        else:
            event.Skip()

    def OnChar(self, event):
        if not self.is_focused():
            return
        keycode = event.GetUnicodeKey()
        if chr(keycode) in ['[', '{', '(', "'", '\"', '`']:
            self.execute_enclose_text(chr(keycode))
            self.store_position()
        else:
            event.Skip()

    def execute_variable_creator(self,
                                 list_variable=False,
                                 dict_variable=False):
        from_, to_ = self._editor.GetSelection()
        text = self._editor.SelectedText
        size = len(bytes(text, encoding='utf-8'))
        to_ = from_ + size
        if list_variable:
            symbol = '@'
        elif dict_variable:
            symbol = '&'
        else:
            symbol = '$'
        if size == 0:
            self._editor.SetInsertionPoint(to_)
            self._editor.InsertText(from_,
                                    self._variable_creator_value(symbol))
            self._editor.SetInsertionPoint(from_ + 2)
        else:
            self._editor.DeleteRange(from_, size)
            self._editor.SetInsertionPoint(from_)
            self._editor.ReplaceSelection(
                self._variable_creator_value(symbol, text))
            self._editor.SetSelection(from_ + 2, from_ + size + 2)

    @staticmethod
    def _variable_creator_value(symbol, value=''):
        return symbol + '{' + value + '}'

    def execute_enclose_text(self, keycode):
        from_, to_ = self._editor.GetSelection()
        text = self._editor.SelectedText
        size = len(bytes(text, encoding='utf-8'))
        to_ = from_ + size
        if size == 0:
            self._editor.SetInsertionPoint(to_)
            self._editor.InsertText(from_, self._enclose_text(keycode))
            pos = self._editor.GetCurrentPos()
            self._editor.SetSelection(pos + 1, pos + 1)
        else:
            self._editor.DeleteRange(from_, size)
            self._editor.SetInsertionPoint(from_)
            self._editor.ReplaceSelection(self._enclose_text(keycode, text))
            self._editor.SetSelection(from_ + 1, from_ + size + 1)

    @staticmethod
    def _enclose_text(open_symbol, value=''):
        if open_symbol == '[':
            close_symbol = ']'
        elif open_symbol == '{':
            close_symbol = '}'
        elif open_symbol == '(':
            close_symbol = ')'
        else:
            close_symbol = open_symbol
        return open_symbol + value + close_symbol

    def execute_comment(self, event):
        cursor = self._editor.GetCurrentPos()
        line, pos = self._editor.GetCurLine()
        spaces = ' ' * self._tab_size
        comment = 'Comment' + spaces
        cpos = cursor + len(comment)
        lenline = len(line)
        if lenline > 0:
            idx = 0
            while idx < lenline and line[idx] == ' ':
                idx += 1
            self._editor.InsertText(cursor - pos + idx, comment)
        else:
            self._editor.InsertText(cursor, comment)
        self._editor.SetCurrentPos(cpos)
        self._editor.SetSelection(cpos, cpos)
        self.store_position()

    def execute_uncomment(self, event):
        cursor = self._editor.GetCurrentPos()
        line, pos = self._editor.GetCurLine()
        spaces = ' ' * self._tab_size
        comment = 'Comment' + spaces
        cpos = cursor - len(comment)
        lenline = len(line)
        if lenline > 0:
            idx = 0
            while idx < lenline and line[idx] == ' ':
                idx += 1
            if (line[idx:len(comment) + idx]).lower() == comment.lower():
                self._editor.DeleteRange(cursor - pos + idx, len(comment))
                self._editor.SetCurrentPos(cpos)
                self._editor.SetSelection(cpos, cpos)
                self.store_position()

    def OnSettingsChanged(self, data):
        """Update tab size if txt spaces size setting is modified"""
        _, setting = data.keys
        if setting == 'txt number of spaces':
            self._tab_size = self._parent._app.settings.get(
                'txt number of spaces', 4)

    def _mark_file_dirty(self):
        if self._data:
            self._dirty = True
            self._data.mark_data_dirty()
Пример #3
0
class SourceEditor(wx.Panel):
    def __init__(self, parent, title, data_validator):
        wx.Panel.__init__(self, parent)
        self._syntax_colorization_help_exists = False
        self._data_validator = data_validator
        self._data_validator.set_editor(self)
        self._parent = parent
        self._tab_size = self._parent._app.settings.get(
            'txt number of spaces', 4)
        self._create_ui(title)
        self._data = None
        self._dirty = False
        self._positions = {}
        PUBLISHER.subscribe(self.OnSettingsChanged, RideSettingsChanged)

    def is_focused(self):
        foc = wx.Window.FindFocus()
        return any(elem == foc for elem in [self] + list(self.GetChildren()))

    def _create_ui(self, title):
        self.SetSizer(VerticalSizer())
        self._create_editor_toolbar()
        self._create_editor_text_control()
        self._parent.add_tab(self, title, allow_closing=False)

    def _create_editor_toolbar(self):
        # needs extra container, since we might add helper
        # text about syntax colorization
        self.editor_toolbar = HorizontalSizer()
        default_components = HorizontalSizer()
        default_components.add_with_padding(
            ButtonWithHandler(self,
                              'Apply Changes',
                              handler=lambda e: self.save()))
        self._create_search(default_components)
        self.editor_toolbar.add_expanding(default_components)
        self.Sizer.add_expanding(self.editor_toolbar, propotion=0)

    def _create_search(self, container_sizer):
        container_sizer.AddSpacer(20)
        self._search_field = TextField(self, '', process_enters=True)
        self._search_field.Bind(wx.EVT_TEXT_ENTER, self.OnFind)
        container_sizer.add_with_padding(self._search_field)
        container_sizer.add_with_padding(
            ButtonWithHandler(self, 'Search', handler=self.OnFind))
        self._search_field_notification = Label(self, label='')
        container_sizer.add_with_padding(self._search_field_notification)

    def create_syntax_colorization_help(self):
        if self._syntax_colorization_help_exists:
            return
        label = Label(
            self,
            label="Syntax colorization disabled due to missing requirements.")
        link = HyperlinkCtrl(self, -1, label="Get help", url="")
        link.Bind(EVT_HYPERLINK, self.show_help_dialog)
        flags = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT
        syntax_colorization_help_sizer = wx.BoxSizer(wx.VERTICAL)
        syntax_colorization_help_sizer.AddMany([(label, 0, flags),
                                                (link, 0, flags)])
        self.editor_toolbar.add_expanding(syntax_colorization_help_sizer)
        self.Layout()
        self._syntax_colorization_help_exists = True

    def show_help_dialog(self, event):
        content = """<h1>Syntax colorization</h1>
        <p>
        Syntax colorization for Text Edit uses <a href='http://pygments.org/'>Pygments</a> syntax highlighter.
        </p>
        <p>
        Install Pygments from command line with:
        <pre>
            pip install pygments
        </pre>
        Or:
        <pre>
            easy_install pygments
        </pre>
        Then, restart RIDE.
        </p>
        <p>
        If you do not have pip or easy_install,
        <a href='http://pythonhosted.org/an_example_pypi_project/setuptools.html#installing-setuptools-and-easy-install'>follow
        these instructions</a>.
        </p>
        <p>
        For more information about installing Pygments, <a href='http://pygments.org/download/'>see the site</a>.
        </p>
        """
        HtmlDialog("Getting syntax colorization", content).Show()

    def store_position(self):
        if self._editor:
            self._positions[
                self.datafile_controller] = self._editor.GetCurrentPos()

    def set_editor_caret_position(self):
        if not self.is_focused():  # DEBUG was typing text when at Grid Editor
            # print("DEBUG: Text Edit avoid set caret pos")
            return
        position = self._positions.get(self.datafile_controller, None)
        # print("DEBUG: Called set caret position=%s" % position)
        if position:
            self._editor.SetFocus()
            self._editor.SetCurrentPos(position)
            self._editor.SetSelection(position, position)

    @property
    def dirty(self):
        return self._dirty

    @property
    def datafile_controller(self):
        return self._data._data if self._data else None

    def OnFind(self, event):
        if self._editor:
            text = self._editor.GetSelectedText()
            # print("DEBUG: Find text:%s" % text)
            if len(text) > 0 and text.lower() != self._search_field.GetValue(
            ).lower():
                self._search_field.SelectAll()
                self._search_field.Clear()
                self._search_field.Update()
                self._search_field.SetValue(text)
                self._search_field.SelectAll()
                self._search_field.Update()
            self._find()

    def OnFindBackwards(self, event):
        if self._editor:
            self._find(forward=False)

    def _find(self, forward=True):
        txt = self._search_field.GetValue()
        position = self._find_text_position(forward, txt)
        self._show_search_results(position, txt)

    # FIXME: This must be cleaned up
    def _find_text_position(self, forward, txt):
        file_end = len(self._editor.utf8_text)
        search_end = file_end if forward else 0
        anchor = self._editor.GetAnchor()
        anchor += 1 if forward else 0
        position = self._editor.FindText(anchor, search_end, txt, 0)
        if position == -1:
            start, end = (0, file_end) if forward else (file_end - 1, 0)
            position = self._editor.FindText(start, end, txt, 0)
        return position

    def _show_search_results(self, position, txt):
        if position != -1:
            self._editor.SetCurrentPos(position)
            self._editor.SetSelection(position, position + len(txt))
            self._editor.ScrollToLine(self._editor.GetCurrentLine())
            self._search_field_notification.SetLabel('')
        else:
            self._search_field_notification.SetLabel('No matches found.')

    def OnContentAssist(self, event):
        # print("DEBUG: Content assist called Focused: %s" % self.is_focused())
        if not self.is_focused():
            return
        #  if wx.Window.FindFocus() is self._editor:
        selected = self._editor.get_selected_or_near_text()
        sugs = [
            s.name for s in self._suggestions.get_suggestions(selected or '')
        ]
        if sugs:
            # caretpos = self._editor.AutoCompPosStart() # Never used :(
            self._editor.AutoCompSetDropRestOfWord(True)
            self._editor.AutoCompSetSeparator(ord(';'))
            self._editor.AutoCompShow(0, ";".join(sugs))
        # event.Skip()

    def open(self, data):
        self.reset()
        self._data = data
        try:
            self._suggestions = SuggestionSource(None, data._data.tests[0])
        except IndexError:  # It is a new project, no content yet
            self._suggestions = SuggestionSource(None,
                                                 BuiltInLibrariesSuggester())
        if not self._editor:
            self._stored_text = self._data.content
        else:
            self._editor.set_text(self._data.content)
            self.set_editor_caret_position()

    def selected(self, data):
        if not self._editor:
            self._create_editor_text_control(self._stored_text)
        if self._data == data:
            return
        self.open(data)

    def reset(self):
        self._dirty = False

    def save(self, *args):
        if self.dirty and not self._data_validator.validate_and_update(
                self._data, self._editor.utf8_text):
            return False
        return True

    def delete(self):
        if IS_WINDOWS:
            if self._editor.GetSelectionStart(
            ) == self._editor.GetSelectionEnd():
                self._editor.CharRight()
            self._editor.DeleteBack()

    def cut(self):
        self._editor.Cut()

    def copy(self):
        self._editor.Copy()

    def paste(self):
        focus = wx.Window.FindFocus()
        if focus == self._editor:
            self._editor.Paste()
        elif focus == self._search_field:
            self._search_field.Paste()

    def select_all(self):
        self._editor.SelectAll()

    def undo(self):
        self._editor.Undo()

    def redo(self):
        self._editor.Redo()

    def remove_and_store_state(self):
        if self._editor:
            self.store_position()
            self._stored_text = self._editor.GetText()
            # self._editor.Destroy()  # DEBUG Pressing F8 would crash
            # self._editor = None

    def _create_editor_text_control(self, text=None):
        self._editor = RobotDataEditor(self)
        self.Sizer.add_expanding(self._editor)
        self.Sizer.Layout()
        if text is not None:
            self._editor.set_text(text)
        self._editor.Bind(wx.EVT_KEY_UP, self.OnEditorKey)
        self._editor.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
        self._editor.Bind(wx.EVT_KILL_FOCUS, self.LeaveFocus)
        self._editor.Bind(wx.EVT_SET_FOCUS, self.GetFocus)
        # TODO Add here binding for keyword help

    def LeaveFocus(self, event):
        self._editor.SetCaretPeriod(0)
        # self.save()

    def GetFocus(self, event):
        self._editor.SetCaretPeriod(500)
        event.Skip()

    def _revert(self):
        self.reset()
        self._editor.set_text(self._data.content)

    def OnEditorKey(self, event):
        #print("DEBUG: Text Edit enter keypress")
        if not self.is_focused():  # DEBUG was typing text when at Grid Editor
            # print("DEBUG: Text Edit skip keypress")
            return
        if not self.dirty and self._editor.GetModify():
            self._mark_file_dirty()
        event.Skip()

    def OnKeyDown(self, event):
        if not self.is_focused():
            return
        # Process Tab key
        if event.GetKeyCode(
        ) == wx.WXK_TAB and not event.CmdDown() and not event.ShiftDown():
            spaces = ' ' * self._tab_size
            self._editor.WriteText(spaces)
        elif event.GetKeyCode() == wx.WXK_TAB and event.ShiftDown():
            pos = self._editor.GetCurrentPos()
            self._editor.SetCurrentPos(max(0, pos - self._tab_size))
            if not event.CmdDown():  # No text selection
                pos = self._editor.GetCurrentPos()
                self._editor.SetSelection(pos, pos)
        else:
            event.Skip()

    def OnSettingsChanged(self, data):
        """Update tab size if txt spaces size setting is modified"""
        _, setting = data.keys
        if setting == 'txt number of spaces':
            self._tab_size = self._parent._app.settings.get(
                'txt number of spaces', 4)

    def _mark_file_dirty(self):
        if self._data:
            self._dirty = True
            self._data.mark_data_dirty()