Coverage for src/robotide/editor/customsourceeditor.py: 14%

280 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-06 10:40 +0100

1#!/usr/bin/env python 

2# -*- encoding: utf-8 -*- 

3# 

4# Created by Hélio Guilherme <helioxentric@gmail.com> 

5# Copyright 2016- Robot Framework Foundation 

6# 

7# Licensed under the Apache License, Version 2.0 (the "License"); 

8# you may not use this file except in compliance with the License. 

9# You may obtain a copy of the License at 

10# 

11# http://www.apache.org/licenses/LICENSE-2.0 

12# 

13# Unless required by applicable law or agreed to in writing, software 

14# distributed under the License is distributed on an "AS IS" BASIS, 

15# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

16# See the License for the specific language governing permissions and 

17# limitations under the License. 

18 

19 

20import os 1ab

21import sys 1ab

22 

23import wx 1ab

24import wx.stc as stc 1ab

25from robotide.editor.pythoneditor import PythonSTC 1ab

26from wx import Colour 1ab

27from ..widgets import ImageProvider 1ab

28 

29# --------------------------------------------------------------------------- 

30# This is how you pre-establish a file filter so that the dialog 

31# only shows the extension(s) you want it to. 

32wildcard = "All files (*.*)|*.*|" \ 1ab

33 "JASON file (*.json)|*.json|" \ 

34 "Python source (*.py)|*.py|" \ 

35 "Robot Framework (*.robot)|*.robot|" \ 

36 "Robot Framework (*.txt)|*.txt|" \ 

37 "YAML file (*.yaml)|*.yaml" 

38# ---------------------------------------------------------------------- 

39 

40class SourceCodeEditor(PythonSTC): 1ab

41 def __init__(self, parent, options, style=wx.BORDER_NONE): 1ab

42 PythonSTC.__init__(self, parent, -1, options, style=style) 

43 self.SetUpEditor() 

44 

45 # Some methods to make it compatible with how the wxTextCtrl is used 

46 def SetValue(self, value): 1ab

47 val = self.GetReadOnly() 

48 self.SetReadOnly(False) 

49 self.SetText(value) 

50 self.EmptyUndoBuffer() 

51 self.SetSavePoint() 

52 self.SetReadOnly(val) 

53 

54 def SetEditable(self, val): 1ab

55 self.SetReadOnly(not val) 

56 

57 def IsModified(self): 1ab

58 return self.GetModify() 

59 

60 def Clear(self): 1ab

61 self.ClearAll() 

62 

63 def SetInsertionPoint(self, pos): 1ab

64 self.SetCurrentPos(pos) 

65 self.SetAnchor(pos) 

66 

67 def ShowPosition(self, pos): 1ab

68 line = self.LineFromPosition(pos) 

69 # self.EnsureVisible(line) 

70 self.GotoLine(line) 

71 

72 def GetLastPosition(self): 1ab

73 return self.GetLength() 

74 

75 def GetPositionFromLine(self, line): 1ab

76 return self.PositionFromLine(line) 

77 

78 def GetRange(self, start, end): 1ab

79 return self.GetTextRange(start, end) 

80 

81 def GetSelection(self): 1ab

82 return self.GetAnchor(), self.GetCurrentPos() 

83 

84 def SetSelection(self, start, end): 1ab

85 self.SetSelectionStart(start) 

86 self.SetSelectionEnd(end) 

87 

88 def SelectLine(self, line): 1ab

89 start = self.PositionFromLine(line) 

90 end = self.GetLineEndPosition(line) 

91 self.SetSelection(start, end) 

92 

93 def SetUpEditor(self): 1ab

94 """ 

95 This method carries out the work of setting up the Code editor. 

96 It's seperate so as not to clutter up the init code. 

97 """ 

98 import keyword 

99 

100 self.SetLexer(stc.STC_LEX_PYTHON) 

101 self.SetKeyWords(0, " ".join(keyword.kwlist)) 

102 

103 # Enable folding 

104 self.SetProperty("fold", "1") 

105 

106 # Highlight tab/space mixing (shouldn't be any) 

107 self.SetProperty("tab.timmy.whinge.level", "1") 

108 

109 # Set left and right margins 

110 self.SetMargins(2, 2) 

111 

112 # Set up the numbers in the margin for margin #1 

113 self.SetMarginType(1, wx.stc.STC_MARGIN_NUMBER) 

114 # Reasonable value for, say, 4-5 digits using a mono font (40 pix) 

115 self.SetMarginWidth(1, 40) 

116 

117 # Indentation and tab stuff 

118 self.SetIndent(4) # Proscribed indent size for wx 

119 self.SetIndentationGuides(True) # Show indent guides 

120 self.SetBackSpaceUnIndents(True) # Backspace unindents rather than delete 1 space 

121 self.SetTabIndents(True) # Tab key indents 

122 self.SetTabWidth(4) # Proscribed tab size for wx 

123 self.SetUseTabs(False) # Use spaces rather than tabs, or TabTimmy will complain! 

124 # White space 

125 self.SetViewWhiteSpace(False) # Don't view white space 

126 

127 # EOL: Since we are loading/saving ourselves, and the 

128 # strings will always have \n's in them, set the STC to 

129 # edit them that way. 

130 self.SetEOLMode(wx.stc.STC_EOL_LF) 

131 self.SetViewEOL(False) 

132 

133 # No right-edge mode indicator 

134 self.SetEdgeMode(stc.STC_EDGE_NONE) 

135 

136 # Set up a margin to hold fold markers 

137 self.SetMarginType(2, stc.STC_MARGIN_SYMBOL) 

138 self.SetMarginMask(2, stc.STC_MASK_FOLDERS) 

139 self.SetMarginSensitive(2, True) 

140 self.SetMarginWidth(2, 12) 

141 

142 # Global default style 

143 if wx.Platform == '__WXMSW__': 

144 self.StyleSetSpec(stc.STC_STYLE_DEFAULT, 'fore:#000000,back:#FFFFFF,face:Space Mono') # Courier New 

145 elif wx.Platform == '__WXMAC__': 

146 # DEBUG: if this looks fine on Linux too, remove the Mac-specific case 

147 # and use this whenever OS != MSW. 

148 self.StyleSetSpec(stc.STC_STYLE_DEFAULT, 

149 'fore:#000000,back:#FFFFFF,face:Monaco') 

150 else: 

151 # print("DEBUG: Setup on Linux") 

152 defsize = wx.SystemSettings.GetFont(wx.SYS_ANSI_FIXED_FONT).GetPointSize() 

153 # Courier, Space Mono, Source Pro Mono, 

154 self.StyleSetSpec(stc.STC_STYLE_DEFAULT, 'fore:#000000,back:#FFFFFF,face:Hack,size:%d' % defsize) 

155 """ 

156 self.StyleSetBackground(stc.STC_STYLE_DEFAULT, Colour(200, 222, 40)) 

157 self.StyleSetForeground(stc.STC_STYLE_DEFAULT, Colour(7, 0, 70)) 

158 """ 

159 # Clear styles and revert to default. 

160 self.StyleClearAll() 

161 

162 # Following style specs only indicate differences from default. 

163 # The rest remains unchanged. 

164 

165 # Line numbers in margin 

166 self.StyleSetSpec(wx.stc.STC_STYLE_LINENUMBER, 'fore:#000000,back:#99A9C2') 

167 # Highlighted brace 

168 self.StyleSetSpec(wx.stc.STC_STYLE_BRACELIGHT, 'fore:#00009D,back:#FFFF00') 

169 # Unmatched brace 

170 self.StyleSetSpec(wx.stc.STC_STYLE_BRACEBAD, 'fore:#00009D,back:#FF0000') 

171 # Indentation guide 

172 self.StyleSetSpec(wx.stc.STC_STYLE_INDENTGUIDE, "fore:#CDCDCD") 

173 

174 # Python styles 

175 self.StyleSetSpec(wx.stc.STC_P_DEFAULT, 'fore:#000000') 

176 # Comments 

177 self.StyleSetSpec(wx.stc.STC_P_COMMENTLINE, 'fore:#008000,back:#F0FFF0') 

178 self.StyleSetSpec(wx.stc.STC_P_COMMENTBLOCK, 'fore:#008000,back:#F0FFF0') 

179 # Numbers 

180 self.StyleSetSpec(wx.stc.STC_P_NUMBER, 'fore:#008080') 

181 # Strings and characters 

182 self.StyleSetSpec(wx.stc.STC_P_STRING, 'fore:#800080') 

183 self.StyleSetSpec(wx.stc.STC_P_CHARACTER, 'fore:#800080') 

184 # Keywords 

185 self.StyleSetSpec(wx.stc.STC_P_WORD, 'fore:#000080,bold') 

186 # Triple quotes 

187 self.StyleSetSpec(wx.stc.STC_P_TRIPLE, 'fore:#800080,back:#FFFFEA') 

188 self.StyleSetSpec(wx.stc.STC_P_TRIPLEDOUBLE, 'fore:#800080,back:#FFFFEA') 

189 # Class names 

190 self.StyleSetSpec(wx.stc.STC_P_CLASSNAME, 'fore:#0000FF,bold') 

191 # Function names 

192 self.StyleSetSpec(wx.stc.STC_P_DEFNAME, 'fore:#008080,bold') 

193 # Operators 

194 self.StyleSetSpec(wx.stc.STC_P_OPERATOR, 'fore:#800000,bold') 

195 # Identifiers. I leave this as not bold because everything seems 

196 # to be an identifier if it doesn't match the above criterae 

197 self.StyleSetSpec(wx.stc.STC_P_IDENTIFIER, 'fore:#000000') 

198 

199 # Caret color 

200 self.SetCaretForeground("BLUE") 

201 # Selection background 

202 # self.SetSelBackground(1, '#66CCFF') 

203 """ 

204 self.SetBackgroundColour(Colour(200, 222, 40)) 

205 self.SetForegroundColour(Colour(7, 0, 70)) 

206 """ 

207 

208 self.SetSelBackground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT)) 

209 self.SetSelForeground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT)) 

210 

211 def RegisterModifiedEvent(self, event_handler): 1ab

212 self.Bind(wx.stc.EVT_STC_CHANGE, event_handler) 

213 

214 

215# --------------------------------------------------------------------------- 

216# Constants for module versions 

217 

218modOriginal = 0 1ab

219modModified = 1 1ab

220modDefault = modOriginal 1ab

221 

222# --------------------------------------------------------------------------- 

223 

224 

225def is_utf8_strict(data): 1ab

226 try: 

227 decoded = data.decode('UTF-8') 

228 except UnicodeDecodeError: 

229 return False 

230 else: 

231 for ch in decoded: 

232 if 0xD800 <= ord(ch) <= 0xDFFF: 

233 return False 

234 return True 

235 

236 

237class CodeEditorPanel(wx.Panel): 1ab

238 """Panel for the 'Code Editor' tab""" 

239 def __init__(self, parent, main_frame, filepath=None): 1ab

240 self.log = sys.stdout # From FileDialog 

241 self.path = filepath 

242 self.parent = parent 

243 wx.Panel.__init__(self, parent, size=(1, 1)) 

244 self.mainFrame = main_frame 

245 self.editor = SourceCodeEditor(self, options={'tab markers':True, 'fold symbols':2}) 

246 self.editor.RegisterModifiedEvent(self.on_code_modified) 

247 parent.SetName(f'Code Editor: {filepath}') 

248 """ 

249 self.SetBackgroundColour(Colour(200, 222, 40)) 

250 self.SetOwnBackgroundColour(Colour(200, 222, 40)) 

251 self.SetForegroundColour(Colour(7, 0, 70)) 

252 self.SetOwnForegroundColour(Colour(7, 0, 70)) 

253 """ 

254 

255 self.btnSave = wx.Button(self, -1, "Save Changes") 

256 self.btnSave.Enable(False) 

257 self.btnSave.Bind(wx.EVT_BUTTON, self.on_save) 

258 

259 # From FileDialog 

260 self.btnOpen = wx.Button(self, -1, "Open...") 

261 self.btnOpen.Bind(wx.EVT_BUTTON, self.on_button) 

262 

263 self.btnSaveAs = wx.Button(self, -1, "Save as...") 

264 self.btnSaveAs.Bind(wx.EVT_BUTTON, self.on_button2) 

265 self.controlBox = wx.BoxSizer(wx.HORIZONTAL) 

266 self.controlBox.Add(self.btnSave, 0, wx.RIGHT, 5) 

267 self.controlBox.Add(self.btnOpen, 0, wx.RIGHT, 5) 

268 self.controlBox.Add(self.btnSaveAs, 0) 

269 

270 self.box = wx.BoxSizer(wx.VERTICAL) 

271 self.box.Add(self.controlBox, 0, wx.EXPAND) 

272 self.box.Add(wx.StaticLine(self), 0, wx.EXPAND) 

273 self.box.Add(self.editor, 1, wx.EXPAND) 

274 

275 self.box.Fit(self) 

276 self.SetSizer(self.box) 

277 if self.path: 

278 # print("DEBUG: path is init = %s" % self.path) 

279 self.LoadFile(self.path) 

280 

281 def LoadFile(self, filepath): 1ab

282 # Open 

283 f = open(filepath, "rb") 

284 try: 

285 source = f.read() 

286 finally: 

287 f.close() 

288 self.LoadSource(source) 

289 

290 def LoadSource(self, source): 1ab

291 self.editor.Clear() 

292 self.editor.SetTextRaw(source) # DEBUG SetValue 

293 self.JumpToLine(0) 

294 self.btnSave.Enable(False) 

295 

296 def JumpToLine(self, line, highlight=False): 1ab

297 self.editor.GotoLine(line) 

298 self.editor.SetFocus() 

299 if highlight: 

300 self.editor.SelectLine(line) 

301 

302 def on_code_modified(self, event): 1ab

303 __ = event 

304 self.btnSave.Enable(self.editor.IsModified()) 

305 

306 def on_save(self, event, filepath=None): 1ab

307 if self.path is None: 

308 self.path = "noname" 

309 self.on_button2(event) 

310 return 

311 if filepath: 

312 if filepath != self.path and os.path.isfile(filepath): 

313 overwrite_msg = "You are about to overwrite an existing file\n" + \ 

314 "Do you want to continue?" 

315 dlg = wx.MessageDialog(self, overwrite_msg, "Editor Writer", 

316 wx.YES_NO | wx.NO_DEFAULT | wx.ICON_EXCLAMATION) 

317 dlg.SetBackgroundColour(Colour(200, 222, 40)) 

318 dlg.SetForegroundColour(Colour(7, 0, 70)) 

319 result = dlg.ShowModal() 

320 if result == wx.ID_NO: 

321 return 

322 dlg.Destroy() 

323 self.path = filepath 

324 

325 # Save 

326 f = open(self.path, "wb") 

327 source = self.editor.GetTextRaw() 

328 # print("DEBUG: Test is Unicode %s",isUTF8Strict(source)) 

329 if is_utf8_strict(source): 

330 try: 

331 f.write(source) 

332 # print("DEBUG: Saved as Unicode") 

333 finally: 

334 f.close() 

335 else: 

336 # print("DEBUG: there were problems with source not being Unicode.") 

337 # Attempt to isolate the problematic bytes 

338 # DEBUG bytearray(source) 

339 try: 

340 chunksize = 1024 

341 for c in range(0, len(source), chunksize): 

342 data = [chr(int(x, base=2)) for x in source[c:c + chunksize]] 

343 f.write(b''.join(data)) 

344 finally: 

345 f.close() 

346 

347 def on_button(self, evt): 1ab

348 _ = evt 

349 # Create the dialog. In this case the current directory is forced as the starting 

350 # directory for the dialog, and no default file name is forced. This can easilly 

351 # be changed in your program. This is an 'open' dialog, and allows multitple 

352 # file selections as well. 

353 # 

354 # Finally, if the directory is changed in the process of getting files, this 

355 # dialog is set up to change the current working directory to the path chosen. 

356 if self.path: 

357 cwd = os.path.dirname(self.path) 

358 else: 

359 cwd = os.getcwd() 

360 dlg = wx.FileDialog( 

361 self, message="Choose a file", 

362 defaultDir=cwd, 

363 defaultFile="", 

364 wildcard=wildcard, 

365 style=wx.FD_OPEN | wx.FD_CHANGE_DIR | wx.FD_FILE_MUST_EXIST | wx.FD_PREVIEW) # wx.FD_MULTIPLE | 

366 

367 # Show the dialog and retrieve the user response. If it is the OK response, 

368 # process the data. 

369 if dlg.ShowModal() == wx.ID_OK: 

370 # This returns a Python list of files that were selected. 

371 paths = dlg.GetPaths() 

372 

373 # self.log.WriteText('You selected %d files:' % len(paths)) 

374 # DEBUG self.log.write('You selected %d files:' % len(paths)) 

375 

376 filepath = paths[-1] # just get the last one 

377 # Open 

378 f = open(filepath, "rb") 

379 try: 

380 source = f.read() 

381 finally: 

382 f.close() 

383 

384 # store the new path 

385 self.path = filepath 

386 # self.log.write('%s\n' % source) 

387 self.LoadSource(source) # Just the last file 

388 self.parent.SetTitle(filepath) 

389 # Compare this with the debug above; did we change working dirs? 

390 # self.log.WriteText("CWD: %s\n" % os.getcwd()) 

391 # self.log.write("CWD: %s\n" % os.getcwd()) 

392 

393 # Destroy the dialog. Don't do this until you are done with it! 

394 # BAD things can happen otherwise! 

395 dlg.Destroy() 

396 

397 def on_button2(self, evt): 1ab

398 # Create the dialog. In this case the current directory is forced as the starting 

399 # directory for the dialog, and no default file name is forced. This can easilly 

400 # be changed in your program. This is an 'save' dialog. 

401 # 

402 # Unlike the 'open dialog' example found elsewhere, this example does NOT 

403 # force the current working directory to change if the user chooses a different 

404 # directory than the one initially set. 

405 fname = "" 

406 if self.path: 

407 cwd = os.path.dirname(self.path) 

408 fname = os.path.basename(self.path) 

409 else: 

410 cwd = os.getcwd() 

411 self.path = "noname" 

412 dlg = wx.FileDialog( 

413 self, message="Save file as ...", defaultDir=cwd, 

414 defaultFile=fname, wildcard=wildcard, style=wx.FD_SAVE 

415 ) # | wx.FD_OVERWRITE_PROMPT 

416 

417 # This sets the default filter that the user will initially see. Otherwise, 

418 # the first filter in the list will be used by default. 

419 # dlg.SetFilterIndex(2) 

420 

421 # Show the dialog and retrieve the user response. If it is the OK response, 

422 # process the data. 

423 if dlg.ShowModal() == wx.ID_OK: 

424 filepath = dlg.GetPath() 

425 # Normally, at this point you would save your data using the file and path 

426 # data that the user provided to you, but since we didn't actually start 

427 # with any data to work with, that would be difficult. 

428 # 

429 # The code to do so would be similar to this, assuming 'data' contains 

430 # the data you want to save: 

431 # 

432 # fp = file(path, 'w') # Create file anew 

433 # fp.write(data) 

434 # fp.close() 

435 # 

436 # You might want to add some error checking :-) 

437 # 

438 # store the new path 

439 # self.path = path 

440 self.on_save(evt, filepath) 

441 # Note that the current working dir didn't change. This is good since 

442 # that's the way we set it up. 

443 # self.log.WriteText("CWD: %s\n" % os.getcwd()) 

444 # self.log.write("CWD: %s\n" % os.getcwd()) 

445 

446 # Destroy the dialog. Don't do this until you are done with it! 

447 # BAD things can happen otherwise! 

448 dlg.Destroy() 

449 

450 

451# --------------------------------------------------------------------------- 

452 

453def opj(filepath): 1ab

454 """Convert paths to the platform-specific separator""" 

455 st = os.path.join(*tuple(filepath.split('/'))) 

456 # HACK: on Linux, a leading / gets lost... 

457 if filepath.startswith('/'): 

458 st = '/' + st 

459 return st 

460 

461 

462def get_data_dir(): 1ab

463 """ 

464 Return the standard location on this platform for application data 

465 """ 

466 sp = wx.StandardPaths.Get() 

467 return sp.GetUserDataDir() 

468 

469 

470def get_modified_directory(): 1ab

471 """ 

472 Returns the directory where modified versions of the Code files 

473 are stored 

474 """ 

475 return os.path.join(get_data_dir(), "modified") 

476 

477 

478def get_modified_filename(name): 1ab

479 """ 

480 Returns the filename of the modified version of the specified Code 

481 """ 

482 if not name.endswith(".py"): 

483 name = name + ".py" 

484 return os.path.join(get_modified_directory(), name) 

485 

486 

487def get_original_filename(name): 1ab

488 """ 

489 Returns the filename of the original version of the specified Code 

490 """ 

491 if not name.endswith(".py"): 

492 name = name + ".py" 

493 

494 if os.path.isfile(name): 

495 return name 

496 

497 original_dir = os.getcwd() 

498 list_dir = os.listdir(original_dir) 

499 # Loop over the content of the Code directory 

500 for item in list_dir: 

501 if not os.path.isdir(item): 

502 # Not a directory, continue 

503 continue 

504 dir_file = os.listdir(item) 

505 # See if a file called "name" is there 

506 if name in dir_file: 

507 return os.path.join(item, name) 

508 

509 # We must return a string... 

510 return "" 

511 

512 

513def does_modified_exist(name): 1ab

514 """Returns whether the specified Code has a modified copy""" 

515 if os.path.exists(get_modified_filename(name)): 

516 return True 

517 else: 

518 return False 

519 

520 

521def get_config(): 1ab

522 if not os.path.exists(get_data_dir()): 

523 os.makedirs(get_data_dir()) 

524 

525 config = wx.FileConfig( 

526 localFilename=os.path.join(get_data_dir(), "options")) 

527 return config 

528 

529 

530_platformNames = ["wxMSW", "wxGTK", "wxMac"] 1ab

531 

532 

533def main(filepath, frame=None): 1ab

534 __name__ = f'Code Editor: {filepath}' 

535 app = wx.App() 

536 app.SetAppDisplayName(__name__) 

537 if frame is None: 

538 frame = wx.Frame(None) 

539 CodeEditorPanel(frame, None, filepath) 

540 image_provider = ImageProvider() 

541 frame.SetTitle(filepath) 

542 frame.SetSize((800, 600)) 

543 frame.SetIcon(wx.Icon(image_provider.RIDE_ICON)) 

544 frame.CenterOnScreen() 

545 frame.Show(True) 

546 app.MainLoop() 

547# ---------------------------------------------------------------------------- 

548# ---------------------------------------------------------------------------- 

549# ---------------------------------------------------------------------------- 

550 

551 

552if __name__ == '__main__' and __package__ is None: 552 ↛ 553line 552 didn't jump to line 553 because the condition on line 552 was never true1ab

553 from os import path 

554 sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 

555 path = None 

556 try: 

557 if sys.argv[1]: 

558 path = sys.argv[1] 

559 except IndexError: 

560 pass 

561 finally: 

562 main(path) 

563# ----------------------------------------------------------------------------