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()
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 = 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: 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() # 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_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 _mark_file_dirty(self): if self._data: self._dirty = True self._data.mark_data_dirty()