Robot Framework Integrated Development Environment (RIDE)
texteditor.py
Go to the documentation of this file.
1 # Copyright 2008-2015 Nokia Networks
2 # Copyright 2016- Robot Framework Foundation
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 
16 import string
17 from io import StringIO, BytesIO
18 from time import time
19 
20 import wx
21 from wx import stc, Colour
22 from wx.adv import HyperlinkCtrl, EVT_HYPERLINK
23 
24 from .. import robotapi
25 from ..context import IS_WINDOWS, IS_MAC
26 from ..controller.ctrlcommands import SetDataFile
27 from ..controller.filecontrollers import ResourceFileController
28 from ..controller.macrocontrollers import _WithStepsController
29 from ..namespace.suggesters import SuggestionSource
30 from ..pluginapi import Plugin, TreeAwarePluginMixin
31 from ..publish.messages import (RideSaving, RideTreeSelection, RideNotebookTabChanging, RideDataChanged, RideOpenSuite,
32  RideDataChangedToDirty)
33 from ..preferences.editors import ReadFonts
34 from ..publish import RideSettingsChanged, PUBLISHER
35 from ..publish.messages import RideMessage
36 from ..widgets import TextField, Label, HtmlDialog
37 from ..widgets import VerticalSizer, HorizontalSizer, ButtonWithHandler, RIDEDialog
38 
39 try: # import installed version first
40  from pygments.lexers import robotframework as robotframeworklexer
41 except ImportError:
42  robotframeworklexer = None
43 
44 
45 class TextEditorPlugin(Plugin, TreeAwarePluginMixin):
46  title = 'Text Edit'
47 
48  def __init__(self, application):
49  Plugin.__init__(self, application)
50  self._editor_component_editor_component = None
51  self.reformatreformat = application.settings.get('reformat', False)
52 
53  @property
54  _editor = property
55 
56  def _editor(self):
57  if self._editor_component_editor_component is None:
58  self._editor_component_editor_component = SourceEditor(self.notebook,
59  self.titletitle,
61  self._refresh_timer_refresh_timer = wx.Timer(self._editor_component_editor_component)
62  self._editor_component_editor_component.Bind(wx.EVT_TIMER, self._on_timer_on_timer)
63  return self._editor_component_editor_component
64 
65  def enable(self):
66  self.add_self_as_tree_aware_plugin()
67  self.subscribe(self.OnSavingOnSaving, RideSaving)
68  self.subscribe(self.OnTreeSelectionOnTreeSelection, RideTreeSelection)
69  self.subscribe(self.OnDataChangedOnDataChanged, RideMessage)
70  self.subscribe(self.OnTabChangeOnTabChange, RideNotebookTabChanging)
71  if self._editor_editor_editor.is_focused():
72  self._register_shortcuts_register_shortcuts()
73  self._open_open()
74 
76  def focused(func):
77  def f(event):
78  if self.is_focusedis_focused() and self._editor_editor_editor.is_focused():
79  func(event)
80  return f
81  self.register_shortcut('CtrlCmd-X', focused(lambda e: self._editor_editor_editor.cut()))
82  self.register_shortcut('CtrlCmd-C', focused(lambda e: self._editor_editor_editor.copy()))
83  if IS_MAC: # Mac needs this key binding
84  self.register_shortcut('CtrlCmd-A', focused(lambda e: self._editor_editor_editor.select_all()))
85  if IS_WINDOWS or IS_MAC: # Linux does not need this key binding
86  self.register_shortcut('CtrlCmd-V', focused(lambda e: self._editor_editor_editor.paste()))
87  self.register_shortcut('CtrlCmd-Z', focused(lambda e: self._editor_editor_editor.undo()))
88  self.register_shortcut('CtrlCmd-Y', focused(lambda e: self._editor_editor_editor.redo()))
89  # self.register_shortcut('Del', focused(lambda e: self._editor.delete()))
90  self.register_shortcut('Alt-Up', focused(lambda e: self._editor_editor_editor.move_row_up(e)))
91  self.register_shortcut('Alt-Down', focused(lambda e: self._editor_editor_editor.move_row_down(e)))
92  # self.register_shortcut('CtrlCmd-D', focused(lambda e: self._editor.delete_row(e)))
93  self.register_shortcut('CtrlCmd-I', focused(lambda e: self._editor_editor_editor.insert_row(e)))
94  self.register_shortcut('CtrlCmd-3', focused(lambda e: self._editor_editor_editor.execute_comment(e)))
95  self.register_shortcut('CtrlCmd-Shift-3', focused(lambda e: self._editor_editor_editor.execute_sharp_comment(e)))
96  self.register_shortcut('CtrlCmd-4', focused(lambda e: self._editor_editor_editor.execute_uncomment(e)))
97  self.register_shortcut('CtrlCmd-Shift-4', focused(lambda e: self._editor_editor_editor.execute_sharp_uncomment(e)))
98  self.register_shortcut('CtrlCmd-F', lambda e: self._editor_editor_editor._search_field.SetFocus())
99  self.register_shortcut('CtrlCmd-G', lambda e: self._editor_editor_editor.OnFind(e))
100  self.register_shortcut('CtrlCmd-Shift-G', lambda e: self._editor_editor_editor.OnFindBackwards(e))
101  self.register_shortcut('Ctrl-Space', lambda e: focused(self._editor_editor_editor.OnContentAssist(e)))
102  self.register_shortcut('CtrlCmd-Space', lambda e: focused(self._editor_editor_editor.OnContentAssist(e)))
103  self.register_shortcut('Alt-Space', lambda e: focused(self._editor_editor_editor.OnContentAssist(e)))
104 
105  def disable(self):
106  self.remove_self_from_tree_aware_plugins()
107  self.unsubscribe_all()
108  self.unregister_actions()
109  self.delete_tab(self._editor_editor_editor)
110  self._editor_component_editor_component = None
111 
112  def OnOpen(self, event):
113  self._open_open()
114 
115  def _open(self):
116  datafile_controller = self.tree.get_selected_datafile_controller()
117  if datafile_controller:
118  # print(f"DEBUG: _open called and datafile_controller exist")
119  self._open_data_for_controller_open_data_for_controller(datafile_controller)
120  self._editor_editor_editor.store_position()
121 
122  def OnSaving(self, message):
123  if self.is_focusedis_focused():
124  self._editor_editor_editor.save()
125  self._editor_editor_editor.GetFocus(None)
126  else:
127  # print(f"DEBUG: OnSaving open because was saved from other editor {message}")
128  self._open_open() # Was saved from other Editor
129 
130  def OnDataChanged(self, message):
131  # print(f"DEBUG: OnDataChanged entering function {message}")
132  if self._should_process_data_changed_message_should_process_data_changed_message(message):
133  if isinstance(message, RideOpenSuite):
134  # print(f"DEBUG: OnDataChanged message {message}")
135  self._editor_editor_editor.reset()
136  self._editor_editor_editor.set_editor_caret_position()
137  if isinstance(message, RideNotebookTabChanging):
138  return
139  if self._editor_editor_editor.dirty and not self._apply_txt_changes_to_model_apply_txt_changes_to_model():
140  return
141  self._refresh_timer_refresh_timer.Start(500, True)
142  # For performance reasons only run after all the data changes
143 
144  def _on_timer(self, event):
145  self._editor_editor_editor.store_position()
146  self._open_tree_selection_in_editor_open_tree_selection_in_editor()
147  event.Skip()
148 
150  return isinstance(message, RideDataChanged) and \
151  not isinstance(message, RideDataChangedToDirty)
152 
153  def OnTreeSelection(self, message):
154  # print(f"DEBUG: OnTreeSelection entering function {message}")
155  self._editor_editor_editor.store_position()
156  if self.is_focusedis_focused():
157  next_datafile_controller = message.item and message.item.datafile_controller
158  if self._editor_editor_editor.dirty and not self._apply_txt_changes_to_model_apply_txt_changes_to_model():
159  if self._editor_editor_editor.datafile_controller != next_datafile_controller:
160  self.tree.select_controller_node(self._editor_editor_editor.datafile_controller)
161  self._editor_editor_editor.set_editor_caret_position()
162  return
163  if next_datafile_controller:
164  self._open_data_for_controller_open_data_for_controller(next_datafile_controller)
165  self._set_read_only_set_read_only(message)
166  self._editor_editor_editor.set_editor_caret_position()
167  else:
168  self._editor_editor_editor.GetFocus(None)
169 
170  def _set_read_only(self, message):
171  if not isinstance(message, bool):
172  self._editor_editor_editor._editor.readonly = not message.item.datafile_controller.is_modifiable()
173  self._editor_editor_editor._editor.SetReadOnly(self._editor_editor_editor._editor.readonly)
174  self._editor_editor_editor._editor.stylizer._set_styles(self._editor_editor_editor._editor.readonly)
175  self._editor_editor_editor._editor.Update()
176 
178  try:
179  datafile_controller = self.tree.get_selected_datafile_controller()
180  except AttributeError:
181  return
182  if datafile_controller:
183  # print(f"DEBUG: _open_tree_selection_in_editor going to open data")
184  self._editor_editor_editor.open(DataFileWrapper(datafile_controller, self.global_settings))
185  self._editor_editor_editor._editor.readonly = not datafile_controller.is_modifiable()
186  self._editor_editor_editor.set_editor_caret_position()
187 
188  def _open_data_for_controller(self, datafile_controller):
189  # print(f"DEBUG: _open_data_for_controller going to open data")
190  self._editor_editor_editor.selected(DataFileWrapper(datafile_controller, self.global_settings))
191  self._editor_editor_editor._editor.readonly = not datafile_controller.is_modifiable()
192 
193  def OnTabChange(self, message):
194  # print(f"DEBUG: OnTabChange entering function {message}")
195  if message.newtab == self.titletitle:
196  self._register_shortcuts_register_shortcuts()
197  self._open_open()
198  self._editor_editor_editor.set_editor_caret_position()
199  try:
200  self._set_read_only_set_read_only(self._editor_editor_editor._editor.readonly)
201  except Exception: # When using only Text Editor exists error in message topic
202  pass
203  elif message.oldtab == self.titletitle:
204  self._editor_editor_editor.remove_and_store_state()
205  self.unregister_actions()
206  self._editor_component_editor_component.save()
207 
208  def OnTabChanged(self, event):
209  self._show_editor()
210 
211  def OnTabChanging(self, message):
212  if 'Edit' in message.oldtab:
213  self._editor_editor_editor.save()
214 
216  if not self._editor_editor_editor.save():
217  return False
218  # print(f"DEBUG: texteditor _apply_txt_changes_to_model going to RESET dirty={self._editor.dirty}")
219  self._editor_editor_editor.reset()
220  self._editor_editor_editor.set_editor_caret_position()
221  return True
222 
223  def is_focused(self):
224  return self.notebook.current_page_title == self.titletitle
225 
226 
228 
229 
233  filename = ""
234 
235  def _init(self, data=None):
236  self._data_data = data
237 
239  return {}
240 
241  def __eq__(self, other):
242  if self is other:
243  return True
244  if other.__class__ != self.__class__:
245  return False
246  return self._data_data == other._data
247 
248  def __hash__(self):
249  return hash(repr(self))
250 
251 
253 
254  def __init__(self, plugin):
255  self._plugin_plugin = plugin
256  self._last_answer_last_answer = None
257  self._last_answer_time_last_answer_time = 0
258 
259  def set_editor(self, editor):
260  self._editor_editor = editor
261 
262  def validate_and_update(self, data, text):
263  # print(f"DEBUG: validate ENTER type(text)={type(text)}")
264  m_text = text.decode("utf-8")
265  if not self._sanity_check_sanity_check(data, m_text):
266  handled = self._handle_sanity_check_failure_handle_sanity_check_failure()
267  if not handled:
268  return False
269  self._editor_editor.reset()
270  if self._editor_editor._reformat:
271  data.update_from(m_text)
272  else:
273  data.update_from(m_text) # TODO: This is the same code as _reformat == True
274  # There is no way to update the model without reformatting
275  # TODO this only updates the editor, but not the model, changes in Text Editor are not reflected in Grid or
276  # when saving
277  # self._editor._editor.set_text(m_text)
278  # print(f"DEBUG: validate Non reformatting:") # {m_text}")
279  self._editor_editor.set_editor_caret_position()
280  return True
281 
282  def _sanity_check(self, data, text):
283  # print(f"DEBUG: _sanity_check ENTER type(text)={type(text)}")
284  # First remove all lines starting with #
285  for line in text.split('\n'):
286  comment = line.strip().startswith('#')
287  # print(f"DEBUG: _sanity_check comment={comment} line={line}")
288  if comment:
289  text = text.replace(line, '')
290  # print(f"DEBUG: _sanity_check cleaned text={text}")
291  formatted_text = data.format_text(text)
292  c = self._normalize_normalize(formatted_text)
293  e = self._normalize_normalize(text)
294  # print(f"DEBUG: _sanity_check compare c={c}\n e={e}")
295  return len(c) == len(e)
296 
297  def _normalize(self, text):
298  for item in tuple(string.whitespace) + ('...', '*'):
299  if item in text:
300  text = text.replace(item, '')
301  return text
302 
304  if self._last_answer_last_answer == wx.ID_NO and \
305  time() - self._last_answer_time_last_answer_time <= 0.2:
306  # self._editor._mark_file_dirty(True)
307  return False
308  # TODO: use widgets.Dialog
309  dlg = wx.MessageDialog(self._editor_editor,
310  'ERROR: Data sanity check failed!\n'
311  'Reset changes?',
312  'Can not apply changes from Txt Editor',
313  style=wx.YES|wx.NO)
314  dlg.InheritAttributes()
315  """
316  dlg.SetBackgroundColour(Colour(200, 222, 40))
317  dlg.SetOwnBackgroundColour(Colour(200, 222, 40))
318  dlg.SetForegroundColour(Colour(7, 0, 70))
319  dlg.SetOwnForegroundColour(Colour(7, 0, 70))
320  """
321  # dlg.Refresh(True)
322  id = dlg.ShowModal()
323  self._last_answer_last_answer = id
324  self._last_answer_time_last_answer_time = time()
325  if id == wx.ID_YES:
326  self._editor_editor._revert()
327  return True
328  # else:
329  # self._editor._mark_file_dirty()
330  return False
331 
332 
333 class DataFileWrapper(): # TODO: bad class name
334 
335  def __init__(self, data, settings):
336  self._data_data = data
337  self._settings_settings = settings
338  self._tab_size_tab_size = self._settings_settings.get('txt number of spaces', 2) if self._settings_settings else 2
339 
340  def __eq__(self, other):
341  if other is None:
342  return False
343  return self._data_data == other._data
344 
345  def update_from(self, content):
346  # print(f"DEBUG: ENTER update_from type self._data={type(self._data)}")
347  self._data_data.execute(SetDataFile(self._create_target_from_create_target_from(content)))
348 
349  def _create_target_from(self, content):
350  src = BytesIO(content.encode("utf-8"))
351  target = self._create_target_create_target()
352  FromStringIOPopulator(target).populate(src, self._tab_size_tab_size)
353  # print(f"DEBUG: After populate: type target={type(target)}")
354  # print(f"DEBUG: After populate:\n{target.__reduce__()}")
355  return target
356 
357  def format_text(self, text):
358  return self._txt_data_txt_data(self._create_target_from_create_target_from(text))
359 
360  def mark_data_dirty(self):
361  self._data_data.mark_dirty()
362 
364  # print(f"DEBUG: texteditor mark_data_pristine calling unmark_dirty")
365  self._data_data.unmark_dirty()
366 
367  def _create_target(self):
368  data = self._data_data.data
369  target_class = type(data)
370  if isinstance(data, robotapi.TestDataDirectory):
371  target = robotapi.TestDataDirectory(source=self._data_data.directory)
372  target.initfile = data.initfile
373  return target
374  return target_class(source=self._data_data.source)
375 
376  @property
377  content = property
378 
379  def content(self):
380  return self._txt_data_txt_data(self._data_data.data)
381 
382  def _txt_data(self, data):
383  output = StringIO()
384  data.save(output=output, format='txt',
385  txt_separating_spaces=self._settings_settings.get(
386  'txt number of spaces', 4))
387  # print(f"DEBUG: In _txt_data returning content {output.getvalue()}")
388  return output.getvalue() # DEBUG .decode('utf-8')
389 
390 
391 class SourceEditor(wx.Panel):
392 
393  def __init__(self, parent, title, data_validator):
394  wx.Panel.__init__(self, parent)
395  self.dlgdlg = RIDEDialog()
396  self.SetBackgroundColour(Colour(self.dlgdlg.color_background))
397  self.SetForegroundColour(Colour(self.dlgdlg.color_foreground))
398  self._syntax_colorization_help_exists_syntax_colorization_help_exists = False
399  self._data_validator_data_validator = data_validator
400  self._data_validator_data_validator.set_editor(self)
401  self._parent_parent = parent
402  self._title_title = title
403  self._tab_size_tab_size = self._parent_parent._app.settings.get(
404  'txt number of spaces', 4)
405  self._reformat_reformat = self._parent_parent._app.settings.get('reformat', False)
406  self._create_ui_create_ui(title)
407  self._data_data = None
408  self._dirty_dirty = 0 # 0 is False and 1 is True, when changed on this editor
409  self._position_position = None
410  self._showing_list_showing_list = False
411  self._tab_open_tab_open = None
412  # self._autocomplete = None
413  self._controller_for_context_controller_for_context = None
414  PUBLISHER.subscribe(self.OnSettingsChangedOnSettingsChanged, RideSettingsChanged)
415  PUBLISHER.subscribe(self.OnTabChangeOnTabChange, RideNotebookTabChanging)
416 
417  def is_focused(self):
418  # foc = wx.Window.FindFocus()
419  # return any(elem == foc for elem in [self]+list(self.GetChildren()))
420  return self._tab_open_tab_open == self._title_title
421 
422  def OnTabChange(self, message):
423  self._tab_open_tab_open = message.newtab
424 
425  def _create_ui(self, title):
426  cnt = self._parent_parent.GetPageCount()
427  if cnt >= 0:
428  editor_created = False
429  while cnt > 0 and not editor_created:
430  cnt -= 1
431  editor_created = self._parent_parent.GetPageText(cnt) == self._title_title # TODO: Later we can adjust for several Text Editor tabs
432  if not editor_created:
433  self.SetSizer(VerticalSizer())
434  self._create_editor_toolbar_create_editor_toolbar()
435  self._create_editor_text_control_create_editor_text_control()
436  self._parent_parent.add_tab(self, title, allow_closing=False)
437 
439  # needs extra container, since we might add helper
440  # text about syntax colorization
441  self.editor_toolbareditor_toolbar = HorizontalSizer()
442  default_components = HorizontalSizer()
443  button = ButtonWithHandler(self, 'Apply Changes', handler=lambda e: self.savesave())
444  button.SetBackgroundColour(Colour(self.dlgdlg.color_secondary_background))
445  button.SetForegroundColour(Colour(self.dlgdlg.color_secondary_foreground))
446  default_components.add_with_padding(button)
447  self._create_search_create_search(default_components)
448  self.editor_toolbareditor_toolbar.add_expanding(default_components)
449  self.Sizer.add_expanding(self.editor_toolbareditor_toolbar, propotion=0)
450 
451  def _create_search(self, container_sizer):
452  container_sizer.AddSpacer(20)
453  self._search_field_search_field = TextField(self, '', process_enters=True)
454  self._search_field_search_field.SetBackgroundColour(Colour(self.dlgdlg.color_secondary_background))
455  self._search_field_search_field.SetForegroundColour(Colour(self.dlgdlg.color_secondary_foreground))
456  self._search_field_search_field.Bind(wx.EVT_TEXT_ENTER, self.OnFindOnFind)
457  container_sizer.add_with_padding(self._search_field_search_field)
458  button = ButtonWithHandler(self, 'Search', handler=self.OnFindOnFind)
459  button.SetBackgroundColour(Colour(self.dlgdlg.color_secondary_background))
460  button.SetForegroundColour(Colour(self.dlgdlg.color_secondary_foreground))
461  container_sizer.add_with_padding(button)
462  self._search_field_notification_search_field_notification = Label(self, label='')
463  container_sizer.add_with_padding(self._search_field_notification_search_field_notification)
464 
466  if self._syntax_colorization_help_exists_syntax_colorization_help_exists:
467  return
468  label = Label(self, label="Syntax colorization disabled due to missing requirements.")
469  link = HyperlinkCtrl(self, -1, label="Get help", url="")
470  link.Bind(EVT_HYPERLINK, self.show_help_dialogshow_help_dialog)
471  flags = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT
472  syntax_colorization_help_sizer = wx.BoxSizer(wx.VERTICAL)
473  syntax_colorization_help_sizer.AddMany([
474  (label, 0, flags),
475  (link, 0, flags)
476  ])
477  self.editor_toolbareditor_toolbar.add_expanding(syntax_colorization_help_sizer)
478  self.Layout()
479  self._syntax_colorization_help_exists_syntax_colorization_help_exists = True
480 
481  def show_help_dialog(self, event):
482  content = """<h1>Syntax colorization</h1>
483  <p>
484  Syntax colorization for Text Edit uses <a href='http://pygments.org/'>Pygments</a> syntax highlighter.
485  </p>
486  <p>
487  Install Pygments from command line with:
488  <pre>
489  pip install pygments
490  </pre>
491  Or:
492  <pre>
493  easy_install pygments
494  </pre>
495  Then, restart RIDE.
496  </p>
497  <p>
498  If you do not have pip or easy_install,
499  <a href='http://pythonhosted.org/an_example_pypi_project/setuptools.html#installing-setuptools-and-easy-install'>follow
500  these instructions</a>.
501  </p>
502  <p>
503  For more information about installing Pygments, <a href='http://pygments.org/download/'>see the site</a>.
504  </p>
505  """
506  HtmlDialog("Getting syntax colorization", content).Show()
507 
508  def store_position(self, force=False):
509  if self._editor_editor and self.datafile_controllerdatafile_controllerdatafile_controller:
510  cur_pos = self._editor_editor.GetCurrentPos()
511  if cur_pos > 0: # Cheating because it always go to zero
512  self._position_position = cur_pos
513  self._editor_editor.GotoPos(self._position_position)
514 
516  if not self.is_focusedis_focused(): # DEBUG was typing text when at Grid Editor
517  return
518  position = self._position_position
519  self._editor_editor.SetFocus()
520  if position:
521  self._editor_editor.SetCurrentPos(position)
522  self._editor_editor.SetSelection(position, position)
523  self._editor_editor.SetAnchor(position)
524  self._editor_editor.GotoPos(position)
525  self._editor_editor.Refresh()
526  self._editor_editor.Update()
527 
528  @property
529  dirty = property
530 
531  def dirty(self):
532  return self._dirty_dirty == 1 # self._editor.IsModified() and self._dirty == 1
533 
534  @property
535  datafile_controller = property
536 
538  return self._data_data._data if self._data_data else None
539 
540  def OnFind(self, event):
541  if self._editor_editor:
542  text = self._editor_editor.GetSelectedText()
543  if len(text) > 0 and text.lower() != self._search_field_search_field.GetValue().lower() and event.GetEventType() == wx.wxEVT_TOOL:
544  # if a search string selected in text and CTRL+G is pressed
545  # put the string into the _search_field
546  self._search_field_search_field.SelectAll()
547  self._search_field_search_field.Clear()
548  self._search_field_search_field.Update()
549  self._search_field_search_field.SetValue(text)
550  self._search_field_search_field.SelectAll()
551  self._search_field_search_field.Update()
552  # and set the start position to the beginning of the editor
553  self._editor_editor.SetAnchor(0)
554  self._editor_editor.SetCurrentPos(0)
555  self._editor_editor.Update()
556 
557  self._find_find()
558 
559  def OnFindBackwards(self, event):
560  if self._editor_editor:
561  self._find_find(forward=False)
562 
563  def _find(self, forward=True):
564  txt = self._search_field_search_field.GetValue().encode('utf-8')
565  position = self._find_text_position_find_text_position(forward, txt)
566  self._show_search_results_show_search_results(position, txt)
567 
568  # FIXME: This must be cleaned up
569  def _find_text_position(self, forward, txt):
570  file_end = len(self._editor_editor.utf8_text)
571  search_end = file_end if forward else 0
572  anchor = self._editor_editor.GetAnchor()
573  anchor += 1 if forward else 0
574  position = self._editor_editor.FindText(anchor, search_end, txt, 0)
575  if position == -1:
576  start, end = (0, file_end) if forward else (file_end - 1, 0)
577  position = self._editor_editor.FindText(start, end, txt, 0)
578  return position
579 
580  def _show_search_results(self, position, txt):
581  # if text is found start end end of the found text is returned but we do need just starting position which is the first value
582  if type(position) is tuple:
583  position = position[0]
584 
585  if position != -1:
586  self._editor_editor.SetCurrentPos(position)
587  self._editor_editor.SetSelection(position, position + len(txt))
588  self._editor_editor.ScrollToLine(self._editor_editor.GetCurrentLine())
589  self._search_field_notification_search_field_notification.SetLabel('')
590  else:
591  self._search_field_notification_search_field_notification.SetLabel('No matches found.')
592 
593  def OnContentAssist(self, event):
594  self._showing_list_showing_list = False
595  #if not self.is_focused():
596  # return
597  self.store_positionstore_position()
598  selected = self._editor_editor.get_selected_or_near_text()
599  sugs = [s.name for s in self._suggestions_suggestions.get_suggestions(
600  selected or '')]
601  if sugs:
602  self._editor_editor.AutoCompSetDropRestOfWord(True)
603  self._editor_editor.AutoCompSetSeparator(ord(';'))
604  self._editor_editor.AutoCompShow(0, ";".join(sugs))
605  self._showing_list_showing_list = True
606 
607  def open(self, data):
608  # print(f"DEBUG: Textedit enter open")
609  self.resetreset()
610  self._data_data = data
611  # print(f"DEBUG: Textedit in open before getting SuggestionSource {self._data._data}\n Type data is {type(self._data._data)}")
612  try:
613  if isinstance(self._data_data._data, ResourceFileController):
614  self._controller_for_context_controller_for_context = DummyController(self._data_data._data, self._data_data._data)
615  # print(f"DEBUG: Textedit in before getting to RESOURCE")
616  self._suggestions_suggestions = SuggestionSource(None,self._controller_for_context_controller_for_context)
617  else:
618  self._suggestions_suggestions = SuggestionSource(None, self._data_data._data.tests[0])
619  # print(f"DEBUG: Textedit in open After getting SuggestionSource")
620  except IndexError: # It is a new project, no content yet
621  # print(f"DEBUG: Textedit in open Exception SuggestionSource")
622  self._controller_for_context_controller_for_context = DummyController(self._data_data._data, self._data_data._data)
623  self._suggestions_suggestions = SuggestionSource(None, self._controller_for_context_controller_for_context)
624  # self._suggestions = SuggestionSource(None, BuiltInLibrariesSuggester())
625  if not self._editor_editor:
626  self._stored_text_stored_text = self._data_data.content
627  # print(f"DEBUG: open not editor yet self._stored_text= {self._stored_text}")
628  else:
629  self._editor_editor.set_text(self._data_data.content)
630  # print(f"DEBUG: open ->existing editor set_text: {self._data.content}")
631  self.set_editor_caret_positionset_editor_caret_position()
632 
633  def selected(self, data):
634  if not self._editor_editor:
635  self._create_editor_text_control_create_editor_text_control(self._stored_text_stored_text)
636  if self._data_data == data:
637  return
638  # print(f"DEBUG: selected going to open data")
639  self.openopen(data)
640 
641  def auto_ident(self):
642  # if not self.is_focused():
643  # return
644  line, _ = self._editor_editor.GetCurLine()
645  lenline = len(line)
646  linenum = self._editor_editor.GetCurrentLine()
647  if lenline > 0:
648  idx = 0
649  while idx<lenline and line[idx] == ' ':
650  idx += 1
651  tsize = idx // self._tab_size_tab_size
652  if idx < lenline and (line.strip().startswith("FOR") or line.strip().startswith("IF")
653  or line.strip().startswith("ELSE")):
654  tsize += 1
655  # print(f"DEBUG: SourceEditor auto_indent after block kw tsize={tsize} linenum={linenum}")
656  elif linenum > 0 and tsize == 0: # Advance if first task/test case or keyword
657  prevline = self._editor_editor.GetLine(linenum-1).lower()
658  if prevline.startswith("**") and not ("variables" in prevline or "settings" in prevline):
659  tsize = 1
660  elif prevline.startswith("\n"):
661  tsize = 1
662  elif line.strip().startswith("END"):
663  pos = self._editor_editor.GetCurrentPos()
664  self._editor_editor.SetCurrentPos(pos)
665  self._editor_editor.SetSelection(pos, pos)
666  self.deindent_blockdeindent_block()
667  tsize -= 1
668  # print(f"DEBUG: SourceEditor auto_indent after END block kw tsize={tsize} linenum={linenum}")
669  self._editor_editor.NewLine()
670  while tsize > 0:
671  self.write_identwrite_ident()
672  tsize -= 1
673  else:
674  self._editor_editor.NewLine()
675  pos = self._editor_editor.GetCurrentLine()
676  self._editor_editor.SetCurrentPos(self._editor_editor.GetLineEndPosition(pos))
677  self.store_positionstore_position()
678 
679  def deindent_block(self):
680  start, end = self._editor_editor.GetSelection()
681  caret = self._editor_editor.GetCurrentPos()
682  ini_line = self._editor_editor.LineFromPosition(start)
683  end_line = self._editor_editor.LineFromPosition(end)
684  count = 0
685  self._editor_editor.SelectNone()
686  line = ini_line
687  inconsistent = False
688  self._editor_editor.BeginUndoAction()
689  while line <= end_line:
690  inconsistent = False
691  pos = self._editor_editor.PositionFromLine(line)
692  self._editor_editor.SetCurrentPos(pos)
693  self._editor_editor.SetSelection(pos, pos)
694  self._editor_editor.SetInsertionPoint(pos)
695  content = self._editor_editor.GetRange(pos, pos + self._tab_size_tab_size)
696  if content == (' ' * self._tab_size_tab_size):
697  self._editor_editor.DeleteRange(pos, self._tab_size_tab_size)
698  count += 1
699  line += 1
700  else:
701  inconsistent = True
702  break
703  self._editor_editor.EndUndoAction()
704  if inconsistent:
705  self._editor_editor.Undo()
706  return
707  new_start = max(0, start - self._tab_size_tab_size)
708  new_end = max(0, end - (count * self._tab_size_tab_size))
709  if caret == start:
710  ini = new_start
711  fini = new_end
712  else:
713  ini = new_end
714  fini = new_start
715  self._editor_editor.SetSelection(new_start, new_end)
716  self._editor_editor.SetCurrentPos(ini)
717  self._editor_editor.SetAnchor(fini)
718 
719  def indent_line(self, line):
720  if line > 0:
721  pos = self._editor_editor.PositionFromLine(line)
722  text = self._editor_editor.GetLine(line-1)
723  lenline = len(text)
724  if lenline > 0:
725  idx = 0
726  while idx < lenline and text[idx] == ' ':
727  idx += 1
728  tsize = idx // self._tab_size_tab_size
729  if idx < lenline and (text.strip().startswith("FOR") or text.strip().startswith("IF")
730  or text.strip().startswith("ELSE") or text.strip().startswith("TRY")
731  or text.strip().startswith("EXCEPT") or text.strip().startswith("WHILE")):
732  tsize += 1
733  elif tsize == 0:
734  text = text.lower()
735  if text.startswith("**"):
736  if not ("variables" in text or "settings" in text):
737  tsize = 1
738  self._editor_editor.SetCurrentPos(pos)
739  self._editor_editor.SetSelection(pos, pos)
740  self._editor_editor.SetInsertionPoint(pos)
741  for _ in range(tsize):
742  self.write_identwrite_ident()
743 
744  def indent_block(self):
745  start, end = self._editor_editor.GetSelection()
746  caret = self._editor_editor.GetCurrentPos()
747  ini_line = self._editor_editor.LineFromPosition(start)
748  end_line = self._editor_editor.LineFromPosition(end)
749  count = 0
750  self._editor_editor.SelectNone()
751  line = ini_line
752  while line <= end_line:
753  pos = self._editor_editor.PositionFromLine(line)
754  self._editor_editor.SetCurrentPos(pos)
755  self._editor_editor.SetSelection(pos, pos)
756  self._editor_editor.SetInsertionPoint(pos)
757  self.write_identwrite_ident()
758  count += 1
759  line += 1
760  new_start = start + self._tab_size_tab_size
761  new_end = end + (count * self._tab_size_tab_size)
762  if caret == start:
763  ini = new_start
764  fini = new_end
765  else:
766  ini = new_end
767  fini = new_start
768  self._editor_editor.SetSelection(new_start, new_end)
769  self._editor_editor.SetCurrentPos(ini)
770  self._editor_editor.SetAnchor(fini)
771 
772  def write_ident(self):
773  spaces = ' ' * self._tab_size_tab_size
774  self._editor_editor.WriteText(spaces)
775 
776  def reset(self):
777  # print(f"DEBUG: textedit enter RESET calling _mark_file_dirty")
778  self._dirty_dirty = 0
779  self._mark_file_dirty_mark_file_dirty(False)
780 
781  def save(self, *args):
782  # print(f"DEBUG: enter save path={self.datafile_controller.source}")
783  self.store_positionstore_position()
784  if self.dirtydirtydirty:
785  if not self._data_validator_data_validator.validate_and_update(self._data_data, self._editor_editor.utf8_text):
786  return False
787  # DEBUG: Was resetting when leaving editor
788  # self.reset()
789  self.GetFocusGetFocus(None)
790  return True
791 
792  """
793  def direct_save(self, text):
794  print(f"DEBUG: direct_save path={self.datafile_controller.source}")
795  f = open(self.datafile_controller.source, "wb")
796  try:
797  f.write(text)
798  self._mark_file_dirty(False)
799  print(f"DEBUG: direct_save Content:\n{text}")
800  except Exception as e:
801  raise e
802  finally:
803  f.close()
804  """
805 
806  """
807  # DEBUG Code not in use
808  def delete(self):
809  if IS_WINDOWS:
810  # print(f"DEBUG: Delete called")
811  if self._editor.GetSelectionStart() == self._editor.GetSelectionEnd():
812  self._editor.CharRight()
813  self._editor.DeleteBack()
814  self._mark_file_dirty(self._editor.GetModify())
815  """
816 
817  def cut(self):
818  self._editor_editor.Cut()
819  self._mark_file_dirty_mark_file_dirty(self._editor_editor.GetModify())
820 
821  def copy(self):
822  self._editor_editor.Copy()
823 
824  def paste(self):
825  focus = wx.Window.FindFocus()
826  if focus == self._editor_editor:
827  self._editor_editor.Paste()
828  elif focus == self._search_field_search_field:
829  self._search_field_search_field.Paste()
830  self._mark_file_dirty_mark_file_dirty(self._editor_editor.GetModify())
831 
832  def select_all(self):
833  self._editor_editor.SelectAll()
834 
835  def undo(self):
836  self._editor_editor.Undo()
837  self.store_positionstore_position()
838  # print(f"DEBUG: TextEditor calling dirty from Undo self._dirty={self._dirty}")
839  self._mark_file_dirty_mark_file_dirty(self._dirty_dirty == 1 and self._editor_editor.GetModify())
840 
841  def redo(self):
842  self._editor_editor.Redo()
843  self.store_positionstore_position()
844  self._mark_file_dirty_mark_file_dirty(self._editor_editor.GetModify())
845 
847  if self._editor_editor:
848  self.store_positionstore_position()
849  self._stored_text_stored_text = self._editor_editor.GetText()
850 
851  def _create_editor_text_control(self, text=None):
852  self._editor_editor = RobotDataEditor(self)
853  self.Sizer.add_expanding(self._editor_editor)
854  self.Sizer.Layout()
855  if text is not None:
856  self._editor_editor.set_text(text)
857  self._editor_editor.Bind(wx.EVT_KEY_DOWN, self.OnKeyDownOnKeyDown)
858  self._editor_editor.Bind(wx.EVT_CHAR, self.OnCharOnChar)
859  self._editor_editor.Bind(wx.EVT_KEY_UP, self.OnEditorKeyOnEditorKey)
860  self._editor_editor.Bind(wx.EVT_KILL_FOCUS, self.LeaveFocusLeaveFocus)
861  self._editor_editor.Bind(wx.EVT_SET_FOCUS, self.GetFocusGetFocus)
862  # TODO Add here binding for keyword help
863 
864  def LeaveFocus(self, event):
865  self._editor_editor.AcceptsFocusFromKeyboard()
866  self.store_positionstore_position()
867  self._editor_editor.SetCaretPeriod(0)
868 
869  def GetFocus(self, event):
870  self._editor_editor.SetFocus()
871  self._editor_editor.AcceptsFocusFromKeyboard()
872  self._editor_editor.SetCaretPeriod(500)
873  if self._position_position:
874  self.set_editor_caret_positionset_editor_caret_position()
875  if event:
876  event.Skip()
877 
878  def _revert(self):
879  self.resetreset()
880  self._editor_editor.set_text(self._data_data.content)
881 
882  def OnEditorKey(self, event):
883  # if not self.is_focused(): # DEBUG was typing text when at Grid Editor
884  # self.GetFocus(event)
885  # print(f"DEBUG: EditorKey Got Focus")
886  keycode = event.GetKeyCode()
887  if keycode == wx.WXK_DELETE: # DEBUG on Windows we only get here, single Text Editor
888  selected = self._editor_editor.GetSelection()
889  if selected[0] == selected[1]:
890  pos = self._editor_editor.GetInsertionPoint()
891  if pos != self._editor_editor.GetLastPosition():
892  self._editor_editor.DeleteRange(selected[0], 1)
893  else:
894  self._editor_editor.DeleteRange(selected[0], selected[1] - selected[0])
895  if keycode in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]:
896  # print(f"DEBUG: Enter released {keycode}")
897  return
898  if self.is_focusedis_focused() and keycode != wx.WXK_CONTROL and self._dirty_dirty == 0:
899  # print(f"DEBUG: texteditor OnKeyDown calling _mark_file_dirty event={event}")
900  self._mark_file_dirty_mark_file_dirty(self._editor_editor.GetModify())
901  event.Skip()
902 
903  def OnKeyDown(self, event):
904  # if not self.is_focused():
905  # self.GetFocus(event)
906  # print(f"DEBUG: KeyDown Got Focus")
907  keycode = event.GetUnicodeKey()
908  if event.GetKeyCode() == wx.WXK_DELETE:
909  # print(f"DEBUG: Delete pressed {event.GetKeyCode()}") # Code never reached on Windows
910  return
911  if event.GetKeyCode() == wx.WXK_TAB and not event.ControlDown() and not event.ShiftDown():
912  if self._showing_list_showing_list: # Allows to use Tab for keyword selection
913  self._showing_list_showing_list = False
914  event.Skip()
915  return
916  selected = self._editor_editor.GetSelection()
917  if selected[0] == selected[1]:
918  self.write_identwrite_ident()
919  else:
920  self.indent_blockindent_block()
921  elif event.GetKeyCode() == wx.WXK_TAB and event.ShiftDown():
922  selected = self._editor_editor.GetSelection()
923  if selected[0] == selected[1]:
924  pos = self._editor_editor.GetCurrentPos()
925  self._editor_editor.SetCurrentPos(max(0, pos - self._tab_size_tab_size))
926  self.store_positionstore_position()
927  if not event.ControlDown(): # No text selection
928  pos = self._editor_editor.GetCurrentPos()
929  self._editor_editor.SetSelection(pos, pos)
930  else:
931  self.deindent_blockdeindent_block()
932  elif event.GetKeyCode() in [ wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ]:
933  if not self._showing_list_showing_list:
934  self.auto_identauto_ident()
935  else:
936  self._showing_list_showing_list = False
937  event.Skip()
938  elif keycode in (ord('1'), ord('2'), ord('5')) and event.ControlDown():
939  self.execute_variable_creatorexecute_variable_creator(list_variable=(keycode == ord('2')),
940  dict_variable=(keycode == ord('5')))
941  self.store_positionstore_position()
942  elif keycode == ord('D') and event.ControlDown():
943  self.delete_rowdelete_row(event)
944  else:
945  event.Skip()
946  """
947  elif keycode == ord('3') and event.ControlDown() and event.ShiftDown():
948  self.execute_sharp_comment()
949  self.store_position()
950  elif keycode == ord('4') and event.ControlDown() and event.ShiftDown():
951  self.execute_sharp_uncomment()
952  self.store_position()
953  """
954 
955  def OnChar(self, event):
956  if not self.is_focusedis_focused():
957  self.GetFocusGetFocus(None)
958  keycode = event.GetUnicodeKey()
959  if chr(keycode) in ['[', '{', '(', "'", '\"', '`']:
960  self.execute_enclose_textexecute_enclose_text(chr(keycode))
961  self.store_positionstore_position()
962  else:
963  event.Skip()
964 
965  def execute_variable_creator(self, list_variable=False, dict_variable=False):
966  from_, to_ = self._editor_editor.GetSelection()
967  text = self._editor_editor.SelectedText
968  size = len(bytes(text, encoding='utf-8'))
969  to_ = from_ + size
970  if list_variable:
971  symbol = '@'
972  elif dict_variable:
973  symbol = '&'
974  else:
975  symbol = '$'
976  if size == 0:
977  self._editor_editor.SetInsertionPoint(to_)
978  self._editor_editor.InsertText(from_, self._variable_creator_value_variable_creator_value(symbol))
979  self._editor_editor.SetInsertionPoint(from_ + 2)
980  else:
981  self._editor_editor.DeleteRange(from_, size)
982  self._editor_editor.SetInsertionPoint(from_)
983  self._editor_editor.ReplaceSelection(self._variable_creator_value_variable_creator_value(symbol, text))
984  self._editor_editor.SetSelection(from_ + 2, from_ + size + 2)
985 
986  @staticmethod
987  def _variable_creator_value(symbol, value=''):
988  return symbol + '{' + value + '}'
989 
990  def execute_enclose_text(self, keycode):
991  from_, to_ = self._editor_editor.GetSelection()
992  text = self._editor_editor.SelectedText
993  size = len(bytes(text, encoding='utf-8'))
994  to_ = from_ + size
995  if size == 0:
996  self._editor_editor.SetInsertionPoint(to_)
997  self._editor_editor.InsertText(from_, self._enclose_text_enclose_text(keycode))
998  pos = self._editor_editor.GetCurrentPos()
999  self._editor_editor.SetSelection(pos + 1, pos + 1)
1000  else:
1001  self._editor_editor.DeleteRange(from_, size)
1002  self._editor_editor.SetInsertionPoint(from_)
1003  self._editor_editor.ReplaceSelection(self._enclose_text_enclose_text(keycode, text))
1004  self._editor_editor.SetSelection(from_ + 1, from_ + size + 1)
1005 
1006  @staticmethod
1007  def _enclose_text(open_symbol, value=''):
1008  if open_symbol == '[':
1009  close_symbol = ']'
1010  elif open_symbol == '{':
1011  close_symbol = '}'
1012  elif open_symbol == '(':
1013  close_symbol = ')'
1014  else:
1015  close_symbol = open_symbol
1016  return open_symbol+value+close_symbol
1017 
1018  def move_row_up(self, event):
1019  start, end = self._editor_editor.GetSelection()
1020  cursor = self._editor_editor.GetCurrentPos()
1021  ini_line = self._editor_editor.LineFromPosition(start)
1022  # selection not on top?
1023  if ini_line > 0:
1024  end_line = self._editor_editor.LineFromPosition(end)
1025  # get the previous row content and length
1026  rowabove = self._editor_editor.GetLine(ini_line-1)
1027  lenabove = len(rowabove.encode('utf-8'))
1028  # get the content of the block rows
1029  rowselblock = ''
1030  rowcnt = ini_line
1031  while rowcnt <= end_line:
1032  rowselblock += self._editor_editor.GetLine(rowcnt)
1033  rowcnt += 1
1034  # add the content of previous row
1035  rowselblock += rowabove
1036  begpos = self._editor_editor.PositionFromLine(ini_line-1)
1037  endpos = self._editor_editor.PositionFromLine(end_line+1)
1038  self._editor_editor.Replace(begpos, endpos, rowselblock)
1039  self._editor_editor.SetSelection(begpos, endpos-lenabove-1)
1040  # TODO: recalculate line identation for new position and old
1041  #print(f"DEBUG: move_row_up Variables: select start={start}, end={end} cursor={cursor}"
1042  # f" ini_line={ini_line} end_line={end_line} begpos={begpos} endpos={endpos} lenabove={lenabove}")
1043 
1044  def move_row_down(self, event):
1045  start, end = self._editor_editor.GetSelection()
1046  cursor = self._editor_editor.GetCurrentPos()
1047  ini_line = self._editor_editor.LineFromPosition(start)
1048  end_line = self._editor_editor.LineFromPosition(end)
1049  # get the next row content and length
1050  rowbelow = self._editor_editor.GetLine(end_line+1)
1051  lenbelow = len(rowbelow.encode('utf-8'))
1052  # get the content of the block rows after adding the content below first
1053  # no new rows anymore?
1054  if lenbelow == 0:
1055  rowselblock = '\n'
1056  lenbelow = 1
1057  else:
1058  rowselblock = rowbelow
1059  rowcnt = ini_line
1060  while rowcnt <= end_line:
1061  rowselblock += self._editor_editor.GetLine(rowcnt)
1062  rowcnt += 1
1063  begpos = self._editor_editor.PositionFromLine(ini_line)
1064  endpos = self._editor_editor.PositionFromLine(end_line+2)
1065  self._editor_editor.Replace(begpos, endpos, rowselblock)
1066  self._editor_editor.SetSelection(begpos+lenbelow, endpos-1)
1067  # TODO: recalculate line identation for new position and old
1068  #print(f"DEBUG: move_row_down Variables: select start={start}, end={end} cursor={cursor}"
1069  # f" ini_line={ini_line} end_line={end_line} begpos={begpos} endpos={endpos} lenbelow={lenbelow}")
1070 
1071  def delete_row(self, event):
1072  start, end = self._editor_editor.GetSelection()
1073  cursor = self._editor_editor.GetCurrentPos()
1074  ini_line = self._editor_editor.LineFromPosition(start)
1075  end_line = self._editor_editor.LineFromPosition(end)
1076  begpos = self._editor_editor.PositionFromLine(ini_line)
1077  self._editor_editor.SelectNone()
1078  # print(f"DEBUG: delete_row Variables: select start={start}, end={end} cursor={cursor}"
1079  # f" ini_line={ini_line} end_line={end_line} begpos={begpos} endpos={endpos}")
1080  if start == end:
1081  end_line = ini_line
1082  for line in range(ini_line, end_line + 1):
1083  self._editor_editor.GotoLine(ini_line)
1084  self._editor_editor.LineDelete()
1085  # cursor position when doing block select is always the end of the selection
1086  if ini_line != end_line:
1087  self._editor_editor.SetCurrentPos(begpos)
1088  self._editor_editor.SetAnchor(begpos)
1089  else:
1090  self._editor_editor.SetCurrentPos(cursor)
1091  self._editor_editor.SetAnchor(cursor)
1092  self.store_positionstore_position()
1093 
1094  def insert_row(self, event):
1095  start, end = self._editor_editor.GetSelection()
1096  ini_line = self._editor_editor.LineFromPosition(start)
1097  end_line = self._editor_editor.LineFromPosition(end)
1098  delta = end_line - ini_line
1099  positionfromline = self._editor_editor.PositionFromLine(ini_line)
1100  self._editor_editor.SelectNone()
1101  self._editor_editor.InsertText(positionfromline, '\n')
1102  for nl in range(delta):
1103  self._editor_editor.InsertText(positionfromline + nl, '\n')
1104  self._editor_editor.SetCurrentPos(positionfromline)
1105  self._editor_editor.SetAnchor(positionfromline)
1106  self._editor_editor.GotoLine(ini_line)
1107  self.indent_lineindent_line(ini_line)
1108  self.store_positionstore_position()
1109 
1110  def execute_comment(self, event):
1111  start, end = self._editor_editor.GetSelection()
1112  cursor = self._editor_editor.GetCurrentPos()
1113  ini_line = self._editor_editor.LineFromPosition(start)
1114  end_line = self._editor_editor.LineFromPosition(end)
1115  spaces = ' ' * self._tab_size_tab_size
1116  comment = 'Comment' + spaces
1117  cpos = cursor + len(comment)
1118  count = 0
1119  self._editor_editor.SelectNone()
1120  row = ini_line
1121  # print(f"DEBUG: execute_comment Variables: select start={start}, end={end} cursor={cursor}"
1122  # f" ini_line={ini_line} end_line={end_line} positionfromline={self._editor.PositionFromLine(row)}")
1123  while row <= end_line:
1124  pos = self._editor_editor.PositionFromLine(row)
1125  self._editor_editor.SetCurrentPos(pos)
1126  self._editor_editor.SetSelection(pos, pos)
1127  self._editor_editor.SetInsertionPoint(pos)
1128  line = self._editor_editor.GetLine(row)
1129  lenline = len(line)
1130  # print(f"DEBUG: execute_comment Line={line}: pos={pos}, row={row} lenline={lenline}")
1131  if lenline > 0:
1132  idx = 0
1133  while idx < lenline and line[idx] == ' ':
1134  idx += 1
1135  self._editor_editor.InsertText(pos + idx, comment)
1136  count += 1
1137  row += 1
1138  new_start = start
1139  new_end = end + (count * len(comment))
1140  if cursor == start:
1141  ini = new_start
1142  fini = new_end
1143  else:
1144  ini = new_end
1145  fini = new_start
1146  self._editor_editor.SetSelection(new_start, new_end)
1147  self._editor_editor.SetCurrentPos(ini)
1148  self._editor_editor.SetAnchor(fini)
1149  self.store_positionstore_position()
1150 
1151  def execute_uncomment(self, event):
1152  start, end = self._editor_editor.GetSelection()
1153  cursor = self._editor_editor.GetCurrentPos()
1154  ini_line = self._editor_editor.LineFromPosition(start)
1155  end_line = self._editor_editor.LineFromPosition(end)
1156  spaces = ' ' * self._tab_size_tab_size
1157  comment = 'Comment' + spaces
1158  commentlong = 'BuiltIn.Comment' + spaces
1159  cpos = cursor - len(comment)
1160  self._editor_editor.SelectNone()
1161  count = 0
1162  row = ini_line
1163  while row <= end_line:
1164  pos = self._editor_editor.PositionFromLine(row)
1165  self._editor_editor.SetCurrentPos(pos)
1166  self._editor_editor.SetSelection(pos, pos)
1167  self._editor_editor.SetInsertionPoint(pos)
1168  line = self._editor_editor.GetLine(row)
1169  lenline = len(line)
1170  if lenline > 0:
1171  idx = 0
1172  while idx<lenline and line[idx] == ' ':
1173  idx += 1
1174  if (line[idx:len(comment) + idx]).lower() == comment.lower():
1175  self._editor_editor.DeleteRange(pos + idx, len(comment))
1176  if (line[idx:len(commentlong) + idx]).lower() == commentlong.lower():
1177  self._editor_editor.DeleteRange(pos + idx, len(commentlong))
1178  count += 1
1179  row += 1
1180  new_start = start
1181  new_end = end - (count * len(comment))
1182  if cursor == start:
1183  ini = new_start
1184  fini = new_end
1185  else:
1186  ini = new_end
1187  fini = new_start
1188  self._editor_editor.SetSelection(new_start, new_end)
1189  self._editor_editor.SetCurrentPos(ini)
1190  self._editor_editor.SetAnchor(fini)
1191  self.store_positionstore_position()
1192 
1193  def execute_sharp_comment(self, event):
1194  start, end = self._editor_editor.GetSelection()
1195  cursor = self._editor_editor.GetCurrentPos()
1196  ini_line = self._editor_editor.LineFromPosition(start)
1197  end_line = self._editor_editor.LineFromPosition(end)
1198  spaces = ' ' * self._tab_size_tab_size
1199  count = 0
1200  maxsize = self._editor_editor.GetLineCount()
1201  # If the selection spans on more than one line:
1202  if ini_line < end_line:
1203  for line in range(ini_line, end_line+1):
1204  count += 1
1205  if line < maxsize:
1206  self._editor_editor.GotoLine(line)
1207  else:
1208  self._editor_editor.GotoLine(maxsize)
1209  pos = self._editor_editor.PositionFromLine(line)
1210  self._editor_editor.SetCurrentPos(pos)
1211  self._editor_editor.SetSelection(pos, pos)
1212  self._editor_editor.SetInsertionPoint(pos)
1213  row = self._editor_editor.GetLine(line)
1214  lenline = len(row)
1215  if lenline > 0:
1216  idx = 0
1217  while idx < lenline and row[idx] == ' ':
1218  idx += 1
1219  self._editor_editor.InsertText(pos + idx, '# ')
1220  elif start == end: # On a single row, no selection
1221  count += 1
1222  pos = self._editor_editor.PositionFromLine(ini_line)
1223  row = self._editor_editor.GetLine(ini_line)
1224  lenline = len(row)
1225  if lenline > 0:
1226  idx = 0
1227  while idx < lenline and row[idx] == ' ':
1228  idx += 1
1229  self._editor_editor.InsertText(pos + idx, '# ')
1230  else: # On a single row, with selection
1231  count += 1
1232  pos = self._editor_editor.PositionFromLine(ini_line)
1233  row = self._editor_editor.GetLine(ini_line)
1234  if cursor > pos:
1235  idx = cursor - pos
1236  while idx >= len(spaces):
1237  if row[idx-len(spaces):idx] != spaces:
1238  idx -= 1
1239  else:
1240  break
1241  if idx < len(spaces):
1242  idx = 0
1243  self._editor_editor.InsertText(pos + idx, '# ')
1244  new_start = start
1245  new_end = end + (count * 2)
1246  if cursor == start:
1247  ini = new_start
1248  fini = new_end
1249  else:
1250  ini = new_end
1251  fini = new_start
1252  self._editor_editor.SetSelection(new_start, new_end) # TODO: For some reason the selection is not restored!
1253  self._editor_editor.SetCurrentPos(ini)
1254  self._editor_editor.SetAnchor(fini)
1255  self._editor_editor.SetCurrentPos(cursor + count * 2)
1256  self.store_positionstore_position()
1257 
1258  def execute_sharp_uncomment(self, event):
1259  start, end = self._editor_editor.GetSelection()
1260  cursor = self._editor_editor.GetCurrentPos()
1261  ini_line = self._editor_editor.LineFromPosition(start)
1262  end_line = self._editor_editor.LineFromPosition(end)
1263  spaces = ' ' * self._tab_size_tab_size
1264  # self._editor.SelectNone()
1265  count = 0
1266  maxsize = self._editor_editor.GetLineCount()
1267  # If the selection spans on more than one line:
1268  if ini_line < end_line:
1269  for line in range(ini_line, end_line+1):
1270  pos = self._editor_editor.PositionFromLine(line)
1271  row = self._editor_editor.GetLine(line)
1272  lenline = len(row)
1273  if lenline > 0:
1274  idx = 0
1275  while idx < lenline and row[idx] == ' ':
1276  idx += 1
1277  size = 1
1278  if idx + 1 < lenline and row[idx:idx+1] == '#':
1279  if idx + 2 < lenline and row[idx+1:idx+2] == ' ':
1280  size = 2
1281  # Here we clean up escaped spaces from Apply
1282  if idx + size < lenline:
1283  newrow = row[idx + size:]
1284  newrow = newrow.replace('\\ ', ' ')
1285  size += len(row[idx:]) - len(newrow) - size
1286  self._editor_editor.DeleteRange(pos + idx, len(newrow) + size)
1287  self._editor_editor.InsertText(pos + idx, newrow)
1288  count += size
1289  elif start == end: # On a single row, no selection
1290  pos = self._editor_editor.PositionFromLine(ini_line)
1291  row = self._editor_editor.GetLine(ini_line)
1292  lenline = len(row)
1293  if lenline > 0:
1294  idx = 0
1295  while idx < lenline and row[idx] == ' ':
1296  idx += 1
1297  while count == 0 and idx < lenline:
1298  size = 1
1299  if idx + 1 < lenline and row[idx:idx + 1] == '#':
1300  if idx + 2 < lenline and row[idx + 1:idx + 2] == ' ':
1301  size = 2
1302  # Here we clean up escaped spaces from Apply
1303  if idx + size < lenline:
1304  newrow = row[idx + size:]
1305  newrow = newrow.replace('\\ ', ' ')
1306  size += len(row[idx:]) - len(newrow) - size
1307  self._editor_editor.DeleteRange(pos + idx, len(newrow) + size)
1308  self._editor_editor.InsertText(pos + idx, newrow )
1309  count += size
1310  else:
1311  idx += 1
1312  else: # On a single row, with selection
1313  pos = self._editor_editor.PositionFromLine(ini_line)
1314  row = self._editor_editor.GetLine(ini_line)
1315  lenline = len(row)
1316  if cursor > pos:
1317  idx = cursor - pos
1318  while idx >= len(spaces):
1319  if row[idx-len(spaces):idx] != spaces:
1320  idx -= 1
1321  else:
1322  break
1323  if idx < len(spaces):
1324  idx = 0
1325  while count == 0 and idx > 0:
1326  size = 1
1327  if idx + 1 < lenline and row[idx:idx + 1] == '#':
1328  if idx + 2 < lenline and row[idx + 1:idx + 2] == ' ':
1329  size = 2
1330  # Here we clean up escaped spaces from Apply
1331  if idx + size < lenline:
1332  newrow = row[idx + size:]
1333  newrow = newrow.replace('\\ ', ' ')
1334  size += len(row[idx:]) - len(newrow) - size
1335  self._editor_editor.DeleteRange(pos + idx, len(newrow) + size)
1336  self._editor_editor.InsertText(pos + idx, newrow)
1337  count += size
1338  else:
1339  idx -= 1
1340  if count == 0:
1341  return
1342  new_start = start
1343  new_end = end - count
1344  if cursor == start:
1345  ini = new_start
1346  fini = new_end
1347  else:
1348  ini = new_end
1349  fini = new_start
1350  self._editor_editor.SetSelection(new_start, new_end) # TODO: For some reason the selection is not restored!
1351  self._editor_editor.SetCurrentPos(cursor - count)
1352  self.store_positionstore_position()
1353 
1354 
1355  def OnSettingsChanged(self, message):
1356  _, setting = message.keys
1357  if setting == 'txt number of spaces':
1358  self._tab_size_tab_size = self._parent_parent._app.settings.get('txt number of spaces', 4)
1359  if setting == 'reformat':
1360  self._reformat_reformat = self._parent_parent._app.settings.get('reformat', False)
1361 
1362  def _mark_file_dirty(self, dirty=True):
1363  if not self.is_focusedis_focused(): # DEBUG: Was marking file clean from Grid Editor
1364  return
1365  if self._data_data:
1366  if self._dirty_dirty == 0 and dirty:
1367  self._data_data.mark_data_dirty()
1368  self._dirty_dirty = 1
1369  elif self._dirty_dirty == 1:
1370  # print(f"DEBUG: texteditor _mark_file_dirty calling mark_data_pristine _dirty={self._dirty}")
1371  self._data_data.mark_data_pristine()
1372  self._dirty_dirty = 0
1373 
1374 
1375 class RobotDataEditor(stc.StyledTextCtrl):
1376  margin = 1
1377 
1378  def __init__(self, parent, readonly=False):
1379  stc.StyledTextCtrl.__init__(self, parent)
1380  self._settings_settings = parent._parent._app.settings
1381  self.readonlyreadonly=readonly
1382  self.SetMarginType(self.marginmargin, stc.STC_MARGIN_NUMBER)
1383  self.SetLexer(stc.STC_LEX_CONTAINER)
1384  self.SetReadOnly(True)
1385  self.SetUseTabs(False)
1386  self.SetTabWidth(parent._tab_size)
1387  self.Bind(stc.EVT_STC_STYLENEEDED, self.OnStyleOnStyle)
1388  self.Bind(stc.EVT_STC_ZOOM, self.OnZoomOnZoom)
1389  self.stylizerstylizer = RobotStylizer(self, self._settings_settings, self.readonlyreadonly)
1390 
1391  def set_text(self, text):
1392  self.SetReadOnly(False)
1393  self.SetText(text)
1394  self.stylizerstylizer.stylize()
1395  self.EmptyUndoBuffer()
1396  self.SetMarginWidth(self.marginmargin, self.calc_margin_widthcalc_margin_width())
1397 
1398  @property
1399  utf8_text = property
1400 
1401  def utf8_text(self):
1402  return self.GetText().encode('UTF-8')
1403 
1404  def OnStyle(self, event):
1405  self.stylizerstylizer.stylize()
1406 
1407  def OnZoom(self, event):
1408  self.SetMarginWidth(self.marginmargin, self.calc_margin_widthcalc_margin_width())
1409  self._set_zoom_set_zoom()
1410 
1411  def _set_zoom(self):
1412  new = self.GetZoom()
1413  old = self._settings_settings['Text Edit'].get('zoom factor', 0)
1414  if new != old:
1415  self._settings_settings['Text Edit'].set('zoom factor', new)
1416 
1418  style = stc.STC_STYLE_LINENUMBER
1419  width = self.TextWidth(style, str(self.GetLineCount()))
1420  return width + self.TextWidth(style, "1")
1421 
1423  # First get selected text
1424  selected = self.GetSelectedText()
1425  self.SetInsertionPoint(self.GetInsertionPoint() - len(selected))
1426  if selected:
1427  return selected
1428  # Next get text on the left
1429  self.SetSelectionEnd(self.GetInsertionPoint())
1430  self.WordLeftEndExtend()
1431  selected = self.GetSelectedText()
1432  select = selected.strip()
1433  self.SetInsertionPoint(self.GetInsertionPoint() + len(selected)
1434  - len(select))
1435  if select and len(select) > 0:
1436  return select
1437  # Finally get text on the right
1438  self.SetSelectionStart(self.GetInsertionPoint())
1439  self.WordRightEndExtend()
1440  selected = self.GetSelectedText()
1441  select = selected.strip()
1442  self.SetInsertionPoint(self.GetInsertionPoint() - len(select))
1443  if select and len(select) > 0:
1444  return select
1445 
1446 
1447 class FromStringIOPopulator(robotapi.populators.FromFilePopulator):
1448 
1449  def populate(self, content, tab_size):
1450  # print(f"DEBUG: FromStringIOPopulator spaces={tab_size} populate:\n{content}")
1451  robotapi.RobotReader(spaces=tab_size).read(content, self)
1452 
1453 
1455  def __init__(self, editor, settings, readonly=False):
1456  self.editoreditor = editor
1457  self.lexerlexer = None
1458  self.settingssettings = settings
1459  self._readonly_readonly = readonly
1460  self._ensure_default_font_is_valid_ensure_default_font_is_valid()
1461  if robotframeworklexer:
1462  self.lexerlexer = robotframeworklexer.RobotFrameworkLexer()
1463  else:
1464  self.editoreditor.GetParent().create_syntax_colorization_help()
1465  self._set_styles_set_styles(self._readonly_readonly)
1466  PUBLISHER.subscribe(self.OnSettingsChangedOnSettingsChanged, RideSettingsChanged)
1467 
1468 
1469  def OnSettingsChanged(self, message):
1470  section, setting = message.keys
1471  if section == 'Text Edit':
1472  self._set_styles_set_styles(self._readonly_readonly) # TODO: When on read-only file changing background color ignores flag
1473 
1474  def _font_size(self):
1475  return self.settingssettings['Text Edit'].get('font size', 10)
1476 
1477  def _font_face(self):
1478  return self.settingssettings['Text Edit'].get('font face', 'Courier New')
1479 
1480  def _zoom_factor(self):
1481  return self.settingssettings['Text Edit'].get('zoom factor', 0)
1482 
1483  def _set_styles(self, readonly=False):
1484  color_settings = self.settingssettings.get_without_default('Text Edit')
1485  background = color_settings.get('background', '#FFFFFF')
1486  if readonly:
1487  h = background.lstrip('#')
1488  if h.upper() == background.upper():
1489  from wx import ColourDatabase
1490  cdb = ColourDatabase()
1491  bkng = cdb.Find(h.upper())
1492  bkg = (bkng[0], bkng[1], bkng[2])
1493  else:
1494  bkg = tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
1495  if bkg >= (180, 180, 180):
1496  bkg = (max(160, bkg[0]-80), max(160, bkg[1]-80),
1497  max(160, bkg[2]-80))
1498  else:
1499  bkg = (min(255, bkg[0]+180), min(255, bkg[1]+180),
1500  min(255, bkg[2]+180))
1501  background = '#%02X%02X%02X' % bkg
1502  if robotframeworklexer:
1503  styles = {
1504  robotframeworklexer.ARGUMENT: {
1505  'fore': color_settings.get('argument', '#bb8844')
1506  },
1507  robotframeworklexer.COMMENT: {
1508  'fore': color_settings.get('comment', 'black')
1509  },
1510  robotframeworklexer.ERROR: {
1511  'fore': color_settings.get('error', 'black')
1512  },
1513  robotframeworklexer.GHERKIN: {
1514  'fore': color_settings.get('gherkin', 'black')
1515  },
1516  robotframeworklexer.HEADING: {
1517  'fore': color_settings.get('heading', '#999999'),
1518  'bold': 'true'
1519  },
1520  robotframeworklexer.IMPORT: {
1521  'fore': color_settings.get('import', '#555555')
1522  },
1523  robotframeworklexer.KEYWORD: {
1524  'fore': color_settings.get('keyword', '#990000'),
1525  'bold': 'true'
1526  },
1527  robotframeworklexer.SEPARATOR: {
1528  'fore': color_settings.get('separator', 'black')
1529  },
1530  robotframeworklexer.SETTING: {
1531  'fore': color_settings.get('setting', 'black'),
1532  'bold': 'true'
1533  },
1534  robotframeworklexer.SYNTAX: {
1535  'fore': color_settings.get('syntax', 'black')
1536  },
1537  robotframeworklexer.TC_KW_NAME: {
1538  'fore': color_settings.get('tc_kw_name', '#aaaaaa')
1539  },
1540  robotframeworklexer.VARIABLE: {
1541  'fore': color_settings.get('variable', '#008080')
1542  }
1543  }
1544  self.tokenstokens = {}
1545  for index, token in enumerate(styles):
1546  self.tokenstokens[token] = index
1547  self.editoreditor.StyleSetSpec(index,
1548  self._get_style_string_get_style_string(back=background,
1549  **styles[token]))
1550  else:
1551  foreground = color_settings.get('setting', 'black')
1552  self.editoreditor.StyleSetSpec(0, self._get_style_string_get_style_string(back=background,
1553  fore=foreground))
1554  self.editoreditor.StyleSetBackground(wx.stc.STC_STYLE_DEFAULT, background)
1555  self.editoreditor.SetZoom(self._zoom_factor_zoom_factor())
1556  self.editoreditor.Refresh()
1557 
1558  def _get_word_and_length(self, current_position):
1559  word = self.editoreditor.GetTextRange(current_position,
1560  self.editoreditor.WordEndPosition(
1561  current_position,
1562  False))
1563  return word, len(word)
1564 
1565  def _get_style_string(self, back='#FFFFFF', fore='#000000', bold='', underline=''):
1566  settings = locals()
1567  settings.update(size=self._font_size_font_size())
1568  settings.update(face=self._font_face_font_face())
1569  return ','.join('%s:%s' % (name, value)
1570  for name, value in settings.items() if value)
1571 
1572 
1576  default_font = self._font_face_font_face()
1577  if default_font not in ReadFonts():
1578  sys_font = wx.SystemSettings.GetFont(wx.SYS_ANSI_FIXED_FONT)
1579  self.settingssettings['Text Edit']['font face'] = sys_font.GetFaceName()
1580 
1581  def stylize(self):
1582  if not self.lexerlexer:
1583  return
1584  self.editoreditor.ConvertEOLs(2)
1585  shift = 0
1586  for position, token, value in self.lexerlexer.get_tokens_unprocessed(self.editoreditor.GetText()):
1587  if wx.VERSION < (4, 1, 0):
1588  self.editoreditor.StartStyling(position+shift, 31)
1589  else:
1590  self.editoreditor.StartStyling(position + shift)
1591  try:
1592  self.editoreditor.SetStyling(len(value.encode('utf-8')), self.tokenstokens[token])
1593  shift += len(value.encode('utf-8'))-len(value)
1594  except UnicodeEncodeError:
1595  self.editoreditor.SetStyling(len(value), self.tokenstokens[token])
1596  shift += len(value) - len(value)
def __init__(self, data, settings)
Definition: texteditor.py:335
def __init__(self, parent, readonly=False)
Definition: texteditor.py:1378
def _get_style_string(self, back='#FFFFFF', fore='#000000', bold='', underline='')
Definition: texteditor.py:1565
def _get_word_and_length(self, current_position)
Definition: texteditor.py:1558
def _ensure_default_font_is_valid(self)
Checks if default font is installed.
Definition: texteditor.py:1575
def __init__(self, editor, settings, readonly=False)
Definition: texteditor.py:1455
def OnSettingsChanged(self, message)
Redraw the colors if the color settings are modified.
Definition: texteditor.py:1469
def _set_styles(self, readonly=False)
Definition: texteditor.py:1483
def __init__(self, parent, title, data_validator)
Definition: texteditor.py:393
def store_position(self, force=False)
Definition: texteditor.py:508
def _create_editor_text_control(self, text=None)
Definition: texteditor.py:851
def execute_variable_creator(self, list_variable=False, dict_variable=False)
Definition: texteditor.py:965
def _variable_creator_value(symbol, value='')
Definition: texteditor.py:987
def _show_search_results(self, position, txt)
Definition: texteditor.py:580
def OnSettingsChanged(self, message)
Update tab size if txt spaces size setting is modified.
Definition: texteditor.py:1355
def _create_search(self, container_sizer)
Definition: texteditor.py:451
def _mark_file_dirty(self, dirty=True)
Definition: texteditor.py:1362
def _find_text_position(self, forward, txt)
Definition: texteditor.py:569
def _enclose_text(open_symbol, value='')
Definition: texteditor.py:1007
def _open_data_for_controller(self, datafile_controller)
Definition: texteditor.py:188
def _should_process_data_changed_message(self, message)
Definition: texteditor.py:149
The parsed test data directory object.
Definition: model.py:296
def ReadFonts(fixed=False)
Returns list with fixed width fonts.
Definition: editors.py:43