Coverage for src/robotide/editor/contentassist.py: 39%

491 statements  

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

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 

16import wx 1bc

17from wx import Colour 1bc

18from wx.lib.expando import ExpandoTextCtrl 1bc

19from wx.lib.filebrowsebutton import FileBrowseButton 1bc

20from os.path import relpath, dirname, isdir 1bc

21 

22from .gridbase import GridEditor 1bc

23from .. import context, utils 1bc

24from ..context import IS_MAC, IS_WINDOWS, IS_WX_410_OR_HIGHER 1bc

25from ..namespace.suggesters import SuggestionSource 1bc

26from ..spec.iteminfo import VariableInfo 1bc

27from .popupwindow import RidePopupWindow, HtmlPopupWindow 1bc

28from ..publish import PUBLISHER 1bc

29from ..publish.messages import RideSettingsChanged 1bc

30 

31 

32def obtain_bdd_prefixes(language): 1bc

33 from robotide.lib.compat.parsing.language import Language 1klm

34 lang = Language.from_name(language[0] if isinstance(language, list) else language) 1klm

35 bdd_prefixes = lang.bdd_prefixes 1klm

36 return list(bdd_prefixes) 1klm

37 

38 

39_PREFERRED_POPUP_SIZE = (200, 400) 1bc

40_AUTO_SUGGESTION_CFG_KEY = "enable auto suggestions" 1bc

41 

42 

43class _ContentAssistTextCtrlBase(wx.TextCtrl): 1bc

44 

45 def __init__(self, suggestion_source, language='En', **kw): 1bc

46 super().__init__(**kw) 1daef

47 from ..preferences import RideSettings 1daef

48 _settings = RideSettings() 1daef

49 self.general_settings = _settings['General'] 1daef

50 self.color_background = self.general_settings['background'] 1daef

51 self.color_foreground = self.general_settings['foreground'] 1daef

52 self.color_secondary_background = self.general_settings['secondary background'] 1daef

53 self.color_secondary_foreground = self.general_settings['secondary foreground'] 1daef

54 self.color_background_help = self.general_settings['background help'] 1daef

55 self.color_foreground_text = self.general_settings['foreground text'] 1daef

56 self.language = language 1daef

57 self._popup = ContentAssistPopup(self, suggestion_source) 1daef

58 self.Bind(wx.EVT_KEY_DOWN, self.on_key_down) 1daef

59 self.Bind(wx.EVT_CHAR, self.on_char) 1daef

60 self.Bind(wx.EVT_KILL_FOCUS, self.on_focus_lost) 1daef

61 self.Bind(wx.EVT_MOVE, self.on_focus_lost) 1daef

62 # self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy) 

63 self._showing_content_assist = False 1daef

64 self.Bind(wx.EVT_WINDOW_DESTROY, self.pop_event_handlers) 1daef

65 self._row = None 1daef

66 self._selection = None 1daef

67 self.gherkin_prefix = '' 1daef

68 # Store gherkin prefix from input to add \ 

69 # later after search is performed 

70 if IS_MAC and IS_WX_410_OR_HIGHER: 70 ↛ 72line 70 didn't jump to line 72 because the condition on line 70 was always true1daef

71 self.OSXDisableAllSmartSubstitutions() 1daef

72 self._is_auto_suggestion_enabled = self._get_auto_suggestion_config() 1daef

73 PUBLISHER.subscribe(self.on_settings_changed, RideSettingsChanged) 1daef

74 

75 @staticmethod 1bc

76 def _get_auto_suggestion_config(): 1bc

77 from robotide.context import APP 1daef

78 if not APP: 78 ↛ 80line 78 didn't jump to line 80 because the condition on line 78 was always true1daef

79 return True 1daef

80 settings = APP.settings['Grid'] 

81 return settings.get(_AUTO_SUGGESTION_CFG_KEY, False) 

82 

83 def on_settings_changed(self, message): 1bc

84 """Update auto suggestion settings from PUBLISHER message""" 

85 section, setting = message.keys 

86 if section == 'Grid' and _AUTO_SUGGESTION_CFG_KEY in setting: 

87 self._is_auto_suggestion_enabled = message.new 

88 

89 def set_row(self, row): 1bc

90 self._row = row 

91 

92 def is_shown(self): 1bc

93 return self._popup.is_shown() 1daef

94 

95 def on_key_down(self, event): 1bc

96 key_code, alt_down = event.GetKeyCode(), event.AltDown() 

97 control_down = event.CmdDown() or event.ControlDown() 

98 key_char = event.GetUnicodeKey() 

99 if key_char not in [ord('['), ord('{'), ord('('), ord("'"), ord('\"'), ord('`')]: 

100 self._selection = self.GetStringSelection() 

101 # Ctrl-Space handling needed for dialogs # DEBUG add Ctrl-m 

102 if (control_down or alt_down) and key_code in [wx.WXK_SPACE, ord('m')]: 

103 self.show_content_assist() 

104 elif key_code in (wx.WXK_RIGHT, wx.WXK_LEFT): # To skip list and continue editing 

105 if self._popup.is_shown(): 

106 value = self.GetValue() 

107 if value: 

108 self.SetValue(value) 

109 self.SetInsertionPoint(len(value)) 

110 self._popup.hide() 

111 self.reset() 

112 else: 

113 event.Skip() 

114 elif key_code == wx.WXK_RETURN and self._popup.is_shown(): 

115 self.on_focus_lost(event) 

116 elif key_code == wx.WXK_TAB: 

117 self.on_focus_lost(event, False) 

118 elif key_code == wx.WXK_ESCAPE and self._popup.is_shown(): 

119 self._popup.hide() 

120 elif key_code in [wx.WXK_UP, wx.WXK_DOWN, wx.WXK_PAGEUP, wx.WXK_PAGEDOWN] and self._popup.is_shown(): 

121 self._popup.select_and_scroll(key_code) 

122 elif key_code in (ord('1'), ord('2'), ord('5')) and control_down and not alt_down: 

123 self.execute_variable_creator(list_variable=(key_code == ord('2')), 

124 dict_variable=(key_code == ord('5'))) 

125 elif key_code == ord('3') and control_down and event.ShiftDown() and not alt_down: 

126 self.execute_sharp_comment() 

127 elif key_code == ord('4') and control_down and event.ShiftDown() and not alt_down: 

128 self.execute_sharp_uncomment() 

129 elif self._popup.is_shown() and key_code < 256: 

130 wx.CallAfter(self._populate_content_assist) 

131 event.Skip() 

132 wx.CallAfter(self._show_auto_suggestions_when_enabled) 

133 # Can not catch the following keyEvent from grid cell 

134 elif key_code == wx.WXK_RETURN: 

135 # fill suggestion in dialogs when pressing enter 

136 self.fill_suggestion() 

137 event.Skip() 

138 # Can not catch the following keyEvent at all 

139 # elif key_code == wx.WXK_TAB: 

140 # self.fill_suggestion() 

141 # elif key_code == wx.WXK_ESCAPE and self.is_shown(): 

142 # self._popup.hide() 

143 else: 

144 event.Skip() 

145 

146 def _show_auto_suggestions_when_enabled(self): 1bc

147 if self._is_auto_suggestion_enabled or self.is_shown(): 

148 self.show_content_assist() 

149 

150 def on_char(self, event): 1bc

151 key_char = event.GetUnicodeKey() 

152 if key_char != wx.WXK_RETURN: 

153 self._show_auto_suggestions_when_enabled() 

154 if key_char == wx.WXK_NONE: 

155 event.Skip() 

156 return 

157 if key_char in [ord('['), ord('{'), ord('('), ord("'"), ord('\"'), ord('`')]: 

158 wx.CallAfter(self.execute_enclose_text, chr(key_char)) 

159 else: 

160 event.Skip() 

161 

162 def execute_variable_creator(self, list_variable=False, dict_variable=False): 1bc

163 from_, to_ = self.GetSelection() 

164 if list_variable: 

165 symbol = '@' 

166 elif dict_variable: 

167 symbol = '&' 

168 else: 

169 symbol = '$' 

170 self.SetValue(self._variable_creator_value(self.Value, symbol, from_, to_)) 

171 if from_ == to_: 

172 self.SetInsertionPoint(from_ + 2) 

173 else: 

174 self.SetInsertionPoint(to_ + 3) 

175 self.SetSelection(from_ + 2, to_ + 2) 

176 

177 @staticmethod 1bc

178 def _variable_creator_value(value, symbol, from_, to_): 1bc

179 return value[:from_] + symbol + '{' + value[from_:to_] + '}' + value[to_:] 

180 

181 def execute_enclose_text(self, key_code): 1bc

182 # DEBUG: move this code to kweditor 

183 from_, to_ = self.GetSelection() 

184 if not self._selection or IS_WINDOWS: # On windows selection is not deleted 

185 content = self._enclose_text(self.Value, key_code, from_, to_) 

186 else: 

187 enclosed = self._enclose_text(self._selection, key_code, 0, len(self._selection)) 

188 value = self.Value 

189 if len(value) <= from_: 

190 content = value + enclosed 

191 else: 

192 content = value[:from_] + enclosed + value[from_:] 

193 self.SetValue(content) 

194 self._selection = None 

195 elem = self 

196 if from_ == to_: 

197 elem.SetInsertionPoint(from_ + 1) 

198 else: 

199 elem.SetInsertionPoint(to_ + 2) 

200 elem.SetSelection(from_ + 1, to_ + 1) 

201 

202 @staticmethod 1bc

203 def _enclose_text(value, open_symbol, from_, to_): 1bc

204 if open_symbol == '[': 

205 close_symbol = ']' 

206 elif open_symbol == '{': 

207 close_symbol = '}' 

208 elif open_symbol == '(': 

209 close_symbol = ')' 

210 else: 

211 close_symbol = open_symbol 

212 return value[:from_] + open_symbol + value[from_:to_] + close_symbol + value[to_:] 

213 

214 def execute_sharp_comment(self): 1bc

215 # DEBUG: Will only comment the left top cell for a multi cell select block! 

216 from_, to_ = self.GetSelection() 

217 add_text = '# ' 

218 self.SetValue(self._add_text(self.Value, add_text, True, False, from_, to_)) 

219 lenadd = len(add_text) 

220 elem = self 

221 elem.SetInsertionPoint(from_ + lenadd) 

222 if from_ != to_: 

223 elem.SetInsertionPoint(to_ + lenadd) 

224 elem.SetSelection(from_ + lenadd, to_ + lenadd) 

225 

226 @staticmethod 1bc

227 def _add_text(value, add_text, on_the_left, on_the_right, from_, to_): 1bc

228 if on_the_left and on_the_right: 

229 return value[:from_]+add_text+value[from_:to_]+add_text+value[to_:] 

230 if on_the_left: 

231 return value[:from_]+add_text+value[from_:to_]+value[to_:] 

232 if on_the_right: 

233 return value[:from_]+value[from_:to_]+add_text+value[to_:] 

234 return value 

235 

236 def execute_sharp_uncomment(self): 1bc

237 # DEBUG: Will only uncomment the left top cell for a multi cell select block! 

238 from_, to_ = self.GetSelection() 

239 lenold = len(self.Value) 

240 self.SetValue(self._remove_text(self.Value, '# ', True, False, from_, to_)) 

241 lenone = len(self.Value) 

242 diffone = lenold - lenone 

243 elem = self 

244 if from_ == to_: 

245 elem.SetInsertionPoint(from_ - diffone) 

246 else: 

247 elem.SetInsertionPoint(to_ - diffone) 

248 elem.SetSelection(from_ - diffone, to_ - diffone) 

249 

250 @staticmethod 1bc

251 def _remove_text(value, remove_text, on_the_left, on_the_right, from_, to_): 1bc

252 if on_the_left and on_the_right: 

253 value = value[:from_]+value[from_:to_].strip(remove_text) + remove_text+value[to_:] 

254 elif on_the_left: 

255 value = value[:from_]+value[from_:to_].lstrip(remove_text) + value[to_:] 

256 elif on_the_right: 

257 value = value[:from_]+value[from_:to_].rstrip(remove_text) + value[to_:] 

258 value = value.replace('\\ ', ' ') 

259 return value 

260 

261 def on_focus_lost(self, event, set_value=True): 1bc

262 event.Skip() 1a

263 if not self._popup.is_shown(): 263 ↛ 265line 263 didn't jump to line 265 because the condition on line 263 was always true1a

264 return 1a

265 if self.gherkin_prefix: 

266 value = self.gherkin_prefix + self._popup.get_value() or self.GetValue() 

267 else: 

268 value = self._popup.get_value() or self.GetValue() 

269 if set_value and value: 

270 self.SetValue(value) 

271 self.SetInsertionPoint(len(value)) # DEBUG was self.Value 

272 else: 

273 self.Clear() 

274 self.hide() 

275 

276 def fill_suggestion(self): 1bc

277 if self.gherkin_prefix: 

278 value = self.gherkin_prefix + self._popup.get_value() or self.GetValue() 

279 else: 

280 value = self._popup.get_value() or self.GetValue() 

281 if value: 

282 wrapper_view = self.GetParent().GetParent() 

283 if hasattr(wrapper_view, 'open_cell_editor'): 

284 # in grid cell, need to make sure cell editor is open 

285 wrapper_view.open_cell_editor() 

286 self.SetValue(value) 

287 self.SetInsertionPoint(len(value)) 

288 self.hide() 

289 

290 def pop_event_handlers(self, event): 1bc

291 __ = event 

292 # all pushed eventHandlers need to be popped before close 

293 # the last event handler is window object itself - do not pop itself 

294 if self: 

295 while self.GetEventHandler() is not self: 

296 self.PopEventHandler() 

297 

298 def on_destroy(self, event): 1bc

299 __ = event 

300 # all pushed eventHandlers need to be popped before close 

301 # the last event handler is window object itself - do not pop itself 

302 while self.GetEventHandler() is not self: 

303 self.PopEventHandler() 

304 

305 def reset(self): 1bc

306 self._popup.reset() 

307 self._showing_content_assist = False 

308 

309 def show_content_assist(self): 1bc

310 if self._showing_content_assist: 

311 return 

312 if self._populate_content_assist(): 

313 self._showing_content_assist = True 

314 self._show_content_assist() 

315 

316 def _populate_content_assist(self): 1bc

317 # DEBUG: Get partial content if not found in full 

318 value = self.GetValue() 

319 (self.gherkin_prefix, value) = self._remove_bdd_prefix(value) 

320 return self._popup.content_assist_for(value, row=self._row) 

321 

322 def _remove_bdd_prefix(self, name): 1bc

323 bdd_prefix = [] 

324 if self.language.lower() not in ['en', 'english']: 

325 bdd_prefix = [f"{x.lower()} " for x in obtain_bdd_prefixes(self.language)] 

326 bdd_prefix += ['given ', 'when ', 'then ', 'and ', 'but '] 

327 # print(f"DEBUG: contentassist.py ContentAssistTextCtrlBase _remove_bdd_prefix bdd_prefix={bdd_prefix}") 

328 for match in bdd_prefix: 

329 if name.lower().startswith(match): 

330 return name[:len(match)], name[len(match):] 

331 return '', name 

332 

333 def _show_content_assist(self): 1bc

334 _, height = self.GetSize() 

335 x, y = self.ClientToScreen((0, 0)) 

336 self._popup.show(x, y, height) 

337 

338 def content_assist_value(self): 1bc

339 suggestion = self._popup.content_assist_value(self.Value) 

340 if suggestion is None: 

341 return suggestion 

342 else: 

343 return self.gherkin_prefix + suggestion 

344 

345 def hide(self): 1bc

346 if not self.is_shown(): 346 ↛ 347line 346 didn't jump to line 347 because the condition on line 346 was never true1daef

347 return 

348 self._popup.hide() 1daef

349 self._showing_content_assist = False 1daef

350 

351 def dismiss(self): 1bc

352 if not self.is_shown(): 

353 return 

354 self._popup.dismiss() 

355 

356 

357class ExpandingContentAssistTextCtrl(_ContentAssistTextCtrlBase, ExpandoTextCtrl): 1bc

358 

359 def __init__(self, parent, plugin, controller, language='En'): 1bc

360 """ According to class MRO, super().__init__ in _ContentAssistTextCtrlBase will init ExpandoTextCtrl 

361 instance """ 

362 

363 self.suggestion_source = SuggestionSource(plugin, controller) 1d

364 _ContentAssistTextCtrlBase.__init__(self, self.suggestion_source, language=language, 1d

365 parent=parent, size=wx.DefaultSize, 

366 style=wx.WANTS_CHARS | wx.TE_NOHIDESEL) 

367 self.SetBackgroundColour(context.POPUP_BACKGROUND) 1d

368 # self.SetOwnBackgroundColour(Colour(200, 222, 40)) 

369 self.SetForegroundColour(context.POPUP_FOREGROUND) 1d

370 # self.SetOwnForegroundColour(Colour(7, 0, 70)) 

371 

372 

373class ContentAssistTextCtrl(_ContentAssistTextCtrlBase): 1bc

374 

375 def __init__(self, parent, suggestion_source, language='En', size=wx.DefaultSize): 1bc

376 super().__init__(suggestion_source, language=language, parent=parent, 1e

377 size=size, style=wx.WANTS_CHARS | wx.TE_NOHIDESEL) 

378 self.SetBackgroundColour(Colour(self.color_background_help)) 1e

379 # self.SetOwnBackgroundColour(Colour(self.color_background_help)) 

380 self.SetForegroundColour(Colour(self.color_foreground_text)) 1e

381 # self.SetOwnForegroundColour(Colour(self.color_foreground_text)) 

382 

383 

384class ContentAssistTextEditor(_ContentAssistTextCtrlBase): 1bc

385 

386 def __init__(self, parent, suggestion_source, pos, language='En', size=wx.DefaultSize): 1bc

387 super().__init__(suggestion_source, language=language, 1f

388 parent=parent, id=-1, value="", pos=pos, size=size, 

389 style=wx.WANTS_CHARS | wx.BORDER_NONE | wx.WS_EX_TRANSIENT | wx.TE_PROCESS_ENTER | 

390 wx.TE_NOHIDESEL) 

391 self.SetBackgroundColour(Colour(self.color_background_help)) 1f

392 # self.SetOwnBackgroundColour(Colour(self.color_background_help)) 

393 self.SetForegroundColour(Colour(self.color_foreground_text)) 1f

394 # self.SetOwnForegroundColour(Colour(self.color_foreground_text)) 

395 

396 

397class ContentAssistFileButton(FileBrowseButton): 1bc

398 def __init__(self, parent, suggestion_source, label, controller, size=wx.DefaultSize): 1bc

399 self.suggestion_source = suggestion_source 1a

400 FileBrowseButton.__init__(self, parent, labelText=label, 1a

401 size=size, fileMask="*", 

402 changeCallback=self.on_file_changed) 

403 self._parent = parent 1a

404 self._controller = controller 1a

405 self._browsed = False 1a

406 

407 self.SetBackgroundColour(Colour(context.POPUP_BACKGROUND)) 1a

408 # self.SetOwnBackgroundColour(Colour(context.POPUP_BACKGROUND)) 

409 self.SetForegroundColour(Colour(context.POPUP_FOREGROUND)) 1a

410 # self.SetOwnForegroundColour(Colour(context.POPUP_FOREGROUND)) 

411 

412 def Bind(self, *args): 1bc

413 self.textControl.Bind(*args) 

414 

415 def createTextControl(self): 1bc

416 """Create the text control""" 

417 text_control = _ContentAssistTextCtrlBase(parent=self, id=-1, suggestion_source=self.suggestion_source) 1a

418 text_control.SetToolTip(self.toolTip) 1a

419 if self.changeCallback: 419 ↛ 422line 419 didn't jump to line 422 because the condition on line 419 was always true1a

420 text_control.Bind(wx.EVT_TEXT, self.OnChanged) 1a

421 text_control.Bind(wx.EVT_COMBOBOX, self.OnChanged) 1a

422 return text_control 1a

423 

424 def __getattr__(self, item): 1bc

425 return getattr(self.textControl, item) 1a

426 

427 def OnBrowse(self, evt=None): # Overrides wx method 1bc

428 self._browsed = True 

429 FileBrowseButton.OnBrowse(self, evt) 

430 self._browsed = False 

431 

432 def on_destroy(self, event): 1bc

433 __ = event 

434 # all pushed eventHandlers need to be popped before close 

435 # the last event handler is window object itself - do not pop itself 

436 try: 

437 while self.GetEventHandler() is not self: 

438 self.PopEventHandler() 

439 except RuntimeError: 

440 pass 

441 

442 def on_file_changed(self, evt): 1bc

443 _ = evt 

444 if self._browsed: 

445 self._browsed = False 

446 self.SetValue(self._relative_path(self.GetValue())) 

447 self._parent.setFocusToOK() 

448 

449 def _relative_path(self, value): 1bc

450 src = self._controller.datafile.source 

451 if utils.is_same_drive(src, value): 

452 path = relpath(value, src if isdir(src) else dirname(src)) 

453 else: 

454 path = value 

455 return path.replace('\\', '/') if context.IS_WINDOWS else \ 

456 path.replace('\\', '\\\\') 

457 

458 

459class Suggestions(object): 1bc

460 

461 def __init__(self, suggestion_source): 1bc

462 self._suggestion_source = suggestion_source 1ihjgdaef

463 self._previous_value = None 1ihjgdaef

464 self._previous_choices = [] 1ihjgdaef

465 

466 def get_for(self, value, row=None): 1bc

467 self._previous_choices = self._get_choices(value, row) 1ihjgdaef

468 self._previous_value = value 1ihjgdaef

469 return [k for k, _ in self._previous_choices] 1ihjgdaef

470 

471 def get_item(self, name): 1bc

472 for k, v in self._previous_choices: 

473 if k == name: 

474 return v 

475 raise AttributeError('Item not in choices "%s"' % name) 

476 

477 def _get_choices(self, value, row): 1bc

478 if self._previous_value and value.startswith(self._previous_value): 1ihjgdaef

479 return [(key, val) for key, val in self._previous_choices 1h

480 if utils.normalize(key).startswith(utils.normalize(value))] 

481 choices = self._suggestion_source.get_suggestions(value, row) 1ihjgdaef

482 duplicate_names = self._get_duplicate_names(choices) 1ihjgdaef

483 return self._format_choices(choices, value, duplicate_names) 1ihjgdaef

484 

485 @staticmethod 1bc

486 def _get_duplicate_names(choices): 1bc

487 results = set() 1ihjgdaef

488 normalized_names = [utils.normalize(ch.name if hasattr(ch, 'name') else ch) for ch in choices] 1ihjgdaef

489 for choice in choices: 1ihjgdaef

490 normalized = utils.normalize(choice.name if hasattr(choice, 'name') else choice) 1ihj

491 if normalized_names.count(normalized) > 1: 1ihj

492 results.add(normalized) 1ihj

493 return results 1ihjgdaef

494 

495 def _format_choices(self, choices, prefix, duplicate_names): 1bc

496 return [(self._format(val, prefix, duplicate_names), val) for val in 1ihjgdaef

497 choices] 

498 

499 def _format(self, choice, prefix, duplicate_names): 1bc

500 if hasattr(choice, 'name'): 500 ↛ 503line 500 didn't jump to line 503 because the condition on line 500 was always true1ihj

501 return choice.name if self._matches_unique_shortname( 1ihj

502 choice, prefix, duplicate_names) else choice.longname 

503 elif self._matches_unique_shortname(choice, prefix, duplicate_names): 

504 return choice 

505 

506 @staticmethod 1bc

507 def _matches_unique_shortname(choice, prefix, duplicate_names): 1bc

508 if isinstance(choice, VariableInfo): 508 ↛ 509line 508 didn't jump to line 509 because the condition on line 508 was never true1ihj

509 return True 

510 if hasattr(choice, 'name'): 510 ↛ 513line 510 didn't jump to line 513 because the condition on line 510 was always true1ihj

511 name = choice.name 1ihj

512 else: 

513 name = choice 

514 if not utils.normalize(name).startswith( 514 ↛ 516line 514 didn't jump to line 516 because the condition on line 514 was never true1ihj

515 utils.normalize(prefix)): 

516 return False 

517 if utils.normalize(name) in duplicate_names: 1ihj

518 return False 1ihj

519 return True 1ihj

520 

521 

522class ContentAssistPopup(object): 1bc

523 

524 def __init__(self, parent, suggestion_source): 1bc

525 self._parent = parent 1gdaef

526 self._main_popup = RidePopupWindow(parent, _PREFERRED_POPUP_SIZE) 1gdaef

527 self._details_popup = HtmlPopupWindow(parent, _PREFERRED_POPUP_SIZE) 1gdaef

528 self._selection = -1 1gdaef

529 self._list: ContentAssistList = ContentAssistList(self._main_popup, 1gdaef

530 self.on_list_item_selected, 

531 self.on_list_item_activated) 

532 self._suggestions = Suggestions(suggestion_source) 1gdaef

533 self._choices = None 1gdaef

534 

535 def reset(self): 1bc

536 self._selection = -1 1g

537 

538 def get_value(self): 1bc

539 return self._selection != -1 and self._list.get_text( 

540 self._selection) or None 

541 

542 def content_assist_for(self, value, row=None): 1bc

543 self._choices = self._suggestions.get_for(value, row=row) 1gdaef

544 if not self._choices: 544 ↛ 549line 544 didn't jump to line 549 because the condition on line 544 was always true1gdaef

545 self._list.ClearAll() 1gdaef

546 if not isinstance(self._parent, GridEditor): 1gdaef

547 self._parent.hide() 1daef

548 return False 1gdaef

549 self._choices = list(set([c for c in self._choices if c is not None])) 

550 # print(f"DEBUG: contentassist.py ContentAssistPopup content_assist_for CALL POPULATE Choices={self._choices}") 

551 self._list.populate(self._choices) 

552 return True 

553 

554 @staticmethod 1bc

555 def _starts(val1, val2): 1bc

556 return val1.lower().startswith(val2.lower()) 

557 

558 def content_assist_value(self, value): 1bc

559 _ = value # DEBUG: why we have this argument 

560 if self._selection > -1: 

561 return self._list.GetItem(self._selection).GetText() 

562 return None 

563 

564 def show(self, xcoord, ycoord, cell_height): 1bc

565 self._main_popup.SetPosition((xcoord, 1gdaef

566 self._move_y_where_room(ycoord, 

567 cell_height))) 

568 self._details_popup.SetPosition((self._move_x_where_room(xcoord), 1gdaef

569 self._move_y_where_room(ycoord, 

570 cell_height))) 

571 self._main_popup.Show() 1gdaef

572 self._list.SetFocus() 1gdaef

573 

574 @staticmethod 1bc

575 def _move_x_where_room(start_x): 1bc

576 width = _PREFERRED_POPUP_SIZE[0] 1gdaef

577 max_horizontal = wx.GetDisplaySize()[0] 1gdaef

578 free_right = max_horizontal - start_x - width 1gdaef

579 free_left = start_x - width 1gdaef

580 if max_horizontal - start_x < 2 * width and free_left > free_right: 580 ↛ 581line 580 didn't jump to line 581 because the condition on line 580 was never true1gdaef

581 return start_x - width 

582 return start_x + width 1gdaef

583 

584 @staticmethod 1bc

585 def _move_y_where_room(start_y, cell_height): 1bc

586 height = _PREFERRED_POPUP_SIZE[1] 1gdaef

587 max_vertical = wx.GetDisplaySize()[1] 1gdaef

588 if max_vertical - start_y - cell_height < height: 588 ↛ 589line 588 didn't jump to line 589 because the condition on line 588 was never true1gdaef

589 return start_y - height 

590 return start_y + cell_height 1gdaef

591 

592 def is_shown(self): 1bc

593 return self._main_popup.IsShown() 1gdaef

594 

595 def select_and_scroll(self, key_code): 1bc

596 sel = self._list.GetFirstSelected() 

597 count = self._list.GetItemCount() 

598 pos = 0 

599 if key_code == wx.WXK_DOWN: 

600 pos = sel + 1 if sel < count - 1 else 0 

601 elif key_code == wx.WXK_UP: 

602 pos = sel - 1 if sel > 0 else count - 1 

603 elif key_code == wx.WXK_PAGEDOWN: 

604 pos = self._selection + 14 if count - self._selection > 14 else count - 1 

605 elif key_code == wx.WXK_PAGEUP: 

606 pos = self._selection - 14 if self._selection > 14 else 0 

607 self._select_and_scroll(pos) 

608 

609 def _select_and_scroll(self, selection): 1bc

610 self._selection = selection 

611 self._list.Select(self._selection) 

612 self._list.EnsureVisible(self._selection) 

613 value = self.get_value() 

614 if value: 

615 self._parent.SetValue(value) 

616 

617 def dismiss(self): 1bc

618 if not self._list.HasFocus(): 

619 self.hide() 

620 

621 def hide(self): 1bc

622 self._selection = -1 1daef

623 self._main_popup.Show(False) 1daef

624 self._details_popup.Show(False) 1daef

625 

626 def on_list_item_activated(self, event): 1bc

627 __ = event 

628 self._parent.fill_suggestion() 

629 

630 def on_list_item_selected(self, event): 1bc

631 self._selection = event.GetIndex() 

632 item = self._suggestions.get_item(event.GetText()) 

633 if hasattr(item, 'details') and item.details: 

634 self._details_popup.Show() 

635 self._details_popup.set_content(item.details, item.name) 

636 elif self._details_popup.IsShown(): 

637 self._details_popup.Show(False) 

638 

639 

640class ContentAssistList(wx.ListCtrl): 1bc

641 

642 def __init__(self, parent, selection_callback, activation_callback=None): 1bc

643 self.parent = parent 1gdaef

644 from ..preferences import RideSettings 1gdaef

645 _settings = RideSettings() 1gdaef

646 self.general_settings = _settings['General'] 1gdaef

647 self.color_background_help = self.general_settings['background help'] 1gdaef

648 self.color_foreground_text = self.general_settings['foreground text'] 1gdaef

649 style = wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_NO_HEADER 1gdaef

650 wx.ListCtrl.__init__(self, parent, style=style) 1gdaef

651 self._selection_callback = selection_callback 1gdaef

652 self._activation_callback = activation_callback 1gdaef

653 self.SetSize(parent.GetSize()) 1gdaef

654 self.SetBackgroundColour(self.color_background_help) 1gdaef

655 self.SetForegroundColour(self.color_foreground_text) 1gdaef

656 self.Bind(wx.EVT_LIST_ITEM_SELECTED, selection_callback) 1gdaef

657 self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, activation_callback) 1gdaef

658 

659 def populate(self, data): 1bc

660 self.ClearAll() 

661 self.InsertColumn(0, '', width=self.Size[0]) 

662 for row, item in enumerate(data): 

663 self.InsertItem(row, item) 

664 self.Select(0) 

665 

666 def get_text(self, index): 1bc

667 return self.GetItem(index).GetText()