Coverage for src/robotide/application/application.py: 79%

327 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 builtins 1ab

17import os 1ab

18import wx 1ab

19 

20from contextlib import contextmanager 1ab

21from pathlib import Path 1ab

22 

23from ..namespace import Namespace 1ab

24from ..controller import Project 1ab

25from ..spec import librarydatabase 1ab

26from ..ui import LoadProgressObserver 1ab

27from ..ui.mainframe import RideFrame 1ab

28from .. import publish 1ab

29from .. import context, contrib 1ab

30from ..preferences import Preferences, RideSettings 1ab

31from ..application.pluginloader import PluginLoader 1ab

32from ..application.editorprovider import EditorProvider 1ab

33from ..application.releasenotes import ReleaseNotes 1ab

34from ..application.updatenotifier import UpdateNotifierController, UpdateDialog 1ab

35from ..ui.mainframe import ToolBar 1ab

36from ..ui.fileexplorerplugin import FileExplorerPlugin 1ab

37from ..utils import RideFSWatcherHandler, run_python_command 1ab

38from ..lib.robot.utils.encodingsniffer import get_system_encoding 1ab

39from ..publish import PUBLISHER 1ab

40from ..publish.messages import RideSettingsChanged 1ab

41from ..widgets.button import ButtonWithHandler 1ab

42from wx.lib.agw.aui import AuiDefaultToolBarArt 1ab

43from wx.lib.agw.aui.auibar import AuiToolBar 1ab

44from wx.lib.agw.aui.auibook import AuiTabCtrl, TabFrame 1ab

45try: 1ab

46 from robot.conf import languages 1ab

47except ImportError: 

48 languages = None 

49 

50# add translation macro to builtin similar to what gettext does 

51# generated pot with: /usr/bin/python /usr/bin/pygettext.py -a -d RIDE -o RIDE.pot -p ./localization ../robotide 

52_ = wx.GetTranslation # To keep linter/code analyser happy 1ab

53builtins.__dict__['_'] = wx.GetTranslation 1ab

54 

55BACKGROUND_HELP = 'background help' 1ab

56FOREGROUND_TEXT = 'foreground text' 1ab

57FONT_SIZE = 'font size' 1ab

58FONT_FACE = 'font face' 1ab

59 

60 

61class UnthemableWidgetError(Exception): 1ab

62 def __init__(self): 1ab

63 Exception.__init__(self, 'HELP! I have no clue how to theme this.') 

64 

65 

66class RIDE(wx.App): 1ab

67 _controller = None 1ab

68 _editor_provider = None 1ab

69 _locale = None 1ab

70 _plugin_loader = None 1ab

71 editor = None 1ab

72 fileexplorerplugin = None 1ab

73 fontinfo = None 1ab

74 frame = None 1ab

75 namespace = None 1ab

76 preferences = None 1ab

77 robot_version = None 1ab

78 settings = None 1ab

79 treeplugin = None 1ab

80 

81 def __init__(self, path=None, updatecheck=True, settingspath=None): 1ab

82 self._updatecheck = updatecheck 

83 self.workspace_path = path 

84 self.changed_workspace = False 

85 self.settings_path = settingspath 

86 context.APP = self 

87 wx.App.__init__(self, redirect=False) 

88 

89 def OnInit(self): # Overrides wx method 1ab

90 # DEBUG To test RTL 

91 # self._initial_locale = wx.Locale(wx.LANGUAGE_ARABIC) 

92 self._locale = wx.Locale(wx.LANGUAGE_ENGLISH_US) # LANGUAGE_PORTUGUESE 

93 # Needed for SetToolTipString to work 

94 wx.HelpProvider.Set(wx.SimpleHelpProvider()) # DEBUG: adjust to wx versions 

95 self.settings = RideSettings(self.settings_path) 

96 

97 class Message: 

98 keys = ['General'] 

99 

100 self.change_locale(Message) # This was done here to have menus translated, but not working 

101 # print(f"DEBUG: application.py RIDE OnInit after changing localization {self._locale.GetCanonicalName()=}") 

102 # Importing libraries after setting language 

103 from ..context import coreplugins, SETTINGS_DIRECTORY 

104 from ..ui.treeplugin import TreePlugin 

105 librarydatabase.initialize_database() 

106 self.preferences = Preferences(self.settings) 

107 self.namespace = Namespace(self.settings) 

108 self._controller = Project(self.namespace, self.settings) 

109 # Try to get FontInfo as soon as possible 

110 font_size = self.settings['General'].get('font size', 12) 

111 font_face = self.settings['General'].get('font face', 'Helvetica') 

112 self.fontinfo = wx.FontInfo(font_size).FaceName(font_face).Bold(False) 

113 self.frame = RideFrame(self, self._controller) 

114 # DEBUG self.frame.Show() 

115 self._editor_provider = EditorProvider() 

116 self._plugin_loader = PluginLoader(self, self._get_plugin_dirs(), 

117 coreplugins.get_core_plugins()) 

118 self._plugin_loader.enable_plugins() 

119 perspective = self.settings.get('AUI Perspective', None) 

120 if perspective: 120 ↛ 122line 120 didn't jump to line 122 because the condition on line 120 was always true

121 self.frame.aui_mgr.LoadPerspective(perspective, True) 

122 try: 

123 nb_perspective = self.settings.get('AUI NB Perspective', None) 

124 if nb_perspective: 124 ↛ 132line 124 didn't jump to line 132 because the condition on line 124 was always true

125 self.frame.notebook.LoadPerspective(nb_perspective) 

126 except Exception as e: 

127 print(f"RIDE: There was a problem loading panels position." 

128 f" Please delete the definition 'AUI NB Perspective' in " 

129 f"{os.path.join(SETTINGS_DIRECTORY, 'settings.cfg')}") 

130 if not isinstance(e, IndexError): # If is with all notebooks disabled, continue 

131 raise e 

132 self.fileexplorerplugin = FileExplorerPlugin(self, self._controller) 

133 self.treeplugin = TreePlugin(self) 

134 if self.treeplugin.settings['_enabled']: 134 ↛ 136line 134 didn't jump to line 136 because the condition on line 134 was always true

135 self.treeplugin.register_frame(self.frame) 

136 if not self.treeplugin.opened: 136 ↛ 137line 136 didn't jump to line 137 because the condition on line 136 was never true

137 self.treeplugin.close_tree() 

138 self.editor = self._get_editor() 

139 self.robot_version = self._find_robot_installation() 

140 self._load_data() 

141 self.treeplugin.populate(self.model) 

142 self.treeplugin.set_editor(self.editor) 

143 self._publish_system_info() 

144 self.frame.Show() # ###### DEBUG DANGER ZONE 

145 self.SetTopWindow(self.frame) 

146 self.frame.aui_mgr.Update() 

147 if self._updatecheck: 147 ↛ 150line 147 didn't jump to line 150 because the condition on line 147 was always true

148 wx.CallAfter(UpdateNotifierController(self.settings, self.frame.notebook).notify_update_if_needed, 

149 UpdateDialog) 

150 self.Bind(wx.EVT_ACTIVATE_APP, self.on_app_activate) 

151 PUBLISHER.subscribe(self.SetGlobalColour, RideSettingsChanged) 

152 PUBLISHER.subscribe(self.update_excludes, RideSettingsChanged) 

153 RideSettingsChanged(keys=('Excludes', 'init'), old=None, new=None).publish() 

154 PUBLISHER.subscribe(self.change_locale, RideSettingsChanged) 

155 RideSettingsChanged(keys=('General', 'ui language'), old=None, new=None).publish() 

156 wx.CallLater(600, ReleaseNotes(self).bring_to_front) 

157 return True 

158 

159 def OnExit(self): 1ab

160 PUBLISHER.unsubscribe_all() 

161 self.Destroy() 

162 wx.Exit() 

163 return True 

164 

165 @staticmethod 1ab

166 def _ApplyThemeToWidget(widget, fore_color=wx.BLUE, back_color=wx.LIGHT_GREY, theme: (None, dict) = None): 1ab

167 if theme is None: 167 ↛ 168line 167 didn't jump to line 168 because the condition on line 167 was never true

168 theme = {'background': back_color, 'foreground': fore_color, 'secondary background': back_color, 

169 'secondary foreground': fore_color} 

170 background = theme['background'] 

171 foreground = theme['foreground'] 

172 secondary_background = theme['secondary background'] 

173 secondary_foreground = theme['secondary foreground'] 

174 background_help = theme[BACKGROUND_HELP] 

175 foreground_text = theme[FOREGROUND_TEXT] 

176 if isinstance(widget, AuiToolBar) or isinstance(widget, ToolBar): 

177 aui_default_tool_bar_art = AuiDefaultToolBarArt() 

178 aui_default_tool_bar_art.SetDefaultColours(wx.GREEN) 

179 widget.SetBackgroundColour(background) 

180 widget.SetForegroundColour(foreground) 

181 elif isinstance(widget, wx.Control): 

182 if not isinstance(widget, (wx.Button, wx.BitmapButton, ButtonWithHandler)): 

183 widget.SetForegroundColour(foreground) 

184 widget.SetBackgroundColour(background) 

185 else: 

186 widget.SetForegroundColour(secondary_foreground) 

187 widget.SetBackgroundColour(secondary_background) 

188 elif isinstance(widget, (wx.TextCtrl, TabFrame, AuiTabCtrl)): 

189 widget.SetForegroundColour(foreground_text) # or fore_color 

190 widget.SetBackgroundColour(background_help) # or back_color 

191 elif isinstance(widget, (RideFrame, wx.Panel)): 

192 widget.SetForegroundColour(foreground) # or fore_color 

193 widget.SetBackgroundColour(background) # or fore_color 

194 elif isinstance(widget, wx.MenuItem): 194 ↛ 195line 194 didn't jump to line 195 because the condition on line 194 was never true

195 widget.SetTextColour(foreground) 

196 widget.SetBackgroundColour(background) 

197 else: 

198 widget.SetBackgroundColour(background) 

199 widget.SetForegroundColour(foreground) 

200 

201 def _WalkWidgets(self, widget, indent=0, indent_level=4, theme=None): 1ab

202 if theme is None: 202 ↛ 203line 202 didn't jump to line 203 because the condition on line 202 was never true

203 theme = {} 

204 widget.Freeze() 

205 self._ApplyThemeToWidget(widget=widget, theme=theme) 

206 for child in widget.GetChildren(): 

207 if not child.IsTopLevel(): # or isinstance(child, wx.PopupWindow)): 

208 indent += indent_level 

209 self._WalkWidgets(child, indent, indent_level, theme) 

210 indent -= indent_level 

211 widget.Thaw() 

212 

213 def SetGlobalColour(self, message): 1ab

214 if message.keys[0] != "General": 

215 return 

216 app = wx.App.Get() 

217 _root = app.GetTopWindow() 

218 theme = self.settings.get_without_default('General') 

219 font_size = theme[FONT_SIZE] 

220 font_face = theme[FONT_FACE] 

221 font = _root.GetFont() 

222 font.SetFaceName(font_face) 

223 font.SetPointSize(font_size) 

224 _root.SetFont(font) 

225 self._WalkWidgets(_root, theme=theme) 

226 if theme['apply to panels'] and self.fileexplorerplugin.settings['_enabled']: 226 ↛ 235line 226 didn't jump to line 235 because the condition on line 226 was always true

227 self.fileexplorerplugin.settings['background'] = theme['background'] 

228 self.fileexplorerplugin.settings['foreground'] = theme['foreground'] 

229 self.fileexplorerplugin.settings[FOREGROUND_TEXT] = theme[FOREGROUND_TEXT] 

230 self.fileexplorerplugin.settings[BACKGROUND_HELP] = theme[BACKGROUND_HELP] 

231 self.fileexplorerplugin.settings[FONT_SIZE] = theme[FONT_SIZE] 

232 self.fileexplorerplugin.settings[FONT_FACE] = theme[FONT_FACE] 

233 if self.fileexplorerplugin.settings['opened']: 233 ↛ 234line 233 didn't jump to line 234 because the condition on line 233 was never true

234 self.fileexplorerplugin.show_file_explorer() 

235 if theme['apply to panels'] and self.treeplugin.settings['_enabled']: 235 ↛ exitline 235 didn't return from function 'SetGlobalColour' because the condition on line 235 was always true

236 self.treeplugin.settings['background'] = theme['background'] 

237 self.treeplugin.settings['foreground'] = theme['foreground'] 

238 self.treeplugin.settings[FOREGROUND_TEXT] = theme[FOREGROUND_TEXT] 

239 self.treeplugin.settings[BACKGROUND_HELP] = theme[BACKGROUND_HELP] 

240 self.treeplugin.settings[FONT_SIZE] = theme[FONT_SIZE] 

241 self.treeplugin.settings[FONT_FACE] = theme[FONT_FACE] 

242 if self.treeplugin.settings['opened']: 242 ↛ exitline 242 didn't return from function 'SetGlobalColour' because the condition on line 242 was always true

243 self.treeplugin.on_show_tree(None) 

244 

245 def change_locale(self, message): 1ab

246 if message.keys[0] != "General": 

247 return 

248 initial_locale = self._locale.GetName() 

249 code = self._get_language_code() 

250 del self._locale 

251 self._locale = wx.Locale(code) 

252 if not self._locale.IsOk(): 252 ↛ 253line 252 didn't jump to line 253 because the condition on line 252 was never true

253 self._locale = wx.Locale(wx.LANGUAGE_ENGLISH_WORLD) 

254 lpath = Path(__file__).parent.absolute() 

255 lpath = str(Path(Path.joinpath(lpath.parent, 'localization')).absolute()) 

256 wx.Locale.AddCatalogLookupPathPrefix(lpath) 

257 self._locale.AddCatalog('RIDE') 

258 if len(message.keys) > 1: # Avoid initial setting 

259 from multiprocessing import shared_memory 

260 from .restartutil import do_restart 

261 new_locale = self._locale.GetName() 

262 # print(f"DEBUG: application.py RIDE change_locale from {initial_locale} to {new_locale}") 

263 if initial_locale != new_locale: 263 ↛ 267line 263 didn't jump to line 267 because the condition on line 263 was never true

264 #if restart_dialog(): # DEBUG: See the in implementation why we don't restart 

265 # print("DEBUG: application.py RIDE change_locale Restart accepted.") 

266 # Shared memory to store language definition 

267 try: 

268 sharemem = shared_memory.ShareableList(['en'], name="language") 

269 except FileExistsError: # Other instance created file 

270 sharemem = shared_memory.ShareableList(name="language") 

271 result = do_restart() 

272 if result: 

273 try: 

274 sharemem.shm.close() 

275 sharemem.shm.unlink() 

276 except FileNotFoundError: 

277 pass 

278 

279 def _get_language_code(self) -> str: 1ab

280 if languages: 280 ↛ 284line 280 didn't jump to line 284 because the condition on line 280 was always true1acd

281 from ..preferences import Languages 1acd

282 names = [n for n in Languages.names] 1acd

283 else: 

284 names = [('English', 'en', wx.LANGUAGE_ENGLISH)] 

285 general = self.settings.get_without_default('General') 1acd

286 language = general.get('ui language', 'English') 1acd

287 try: 1acd

288 idx = [lang[0] for lang in names].index(language) 1acd

289 code = names[idx][2] 1acd

290 except (IndexError, ValueError): 

291 print(f"DEBUG: application.py RIDE change_locale ERROR: Could not find {language=}") 

292 code = wx.LANGUAGE_ENGLISH_WORLD 

293 return code 1acd

294 

295 @staticmethod 1ab

296 def update_excludes(message): 1ab

297 if message.keys[0] != "Excludes": 

298 return 

299 from ..preferences.excludes_class import Excludes 

300 from ..context import SETTINGS_DIRECTORY 

301 excludes = Excludes(SETTINGS_DIRECTORY) 

302 paths = excludes.get_excludes().split() 

303 if paths: 303 ↛ 304line 303 didn't jump to line 304 because the condition on line 303 was never true

304 RideFSWatcherHandler.exclude_listening(paths) 

305 

306 @staticmethod 1ab

307 def _publish_system_info(): 1ab

308 from ..context import SYSTEM_INFO 

309 publish.RideLogMessage(SYSTEM_INFO).publish() 

310 

311 @property 1ab

312 def model(self): 1ab

313 return self._controller 

314 

315 def _get_plugin_dirs(self): 1ab

316 return [self.settings.get_path('plugins'), 

317 os.path.join(self.settings['install root'], 'site-plugins'), 

318 contrib.CONTRIB_PATH] 

319 

320 def _get_editor(self): 1ab

321 from ..editor import EditorPlugin 

322 from ..editor.texteditor import TextEditorPlugin 

323 for pl in self._plugin_loader.plugins: 323 ↛ exitline 323 didn't return from function '_get_editor' because the loop on line 323 didn't complete

324 maybe_editor = pl.conn_plugin 

325 if (isinstance(maybe_editor, EditorPlugin) or isinstance(maybe_editor, TextEditorPlugin)) and\ 

326 maybe_editor.__getattr__("_enabled"): 

327 return maybe_editor 

328 

329 def _load_data(self): 1ab

330 self.workspace_path = self.workspace_path or self._get_latest_path() 

331 if self.workspace_path: 331 ↛ exitline 331 didn't return from function '_load_data' because the condition on line 331 was always true

332 self._controller.update_default_dir(self.workspace_path) 

333 theme = self.settings.get_without_default('General') 

334 background = theme['background'] 

335 foreground = theme['foreground'] 

336 # print(f"DEBUG: application.py RIDE _load_data CALL PROGRESS {background=} {foreground=}") 

337 observer = LoadProgressObserver(self.frame, background=background, foreground=foreground) 

338 self._controller.load_data(self.workspace_path, observer) 

339 

340 @staticmethod 1ab

341 def _find_robot_installation(): 1ab

342 output = run_python_command( 

343 ['import robot; print(robot.__file__ + \", \" + robot.__version__)']) 

344 robot_found = b"ModuleNotFoundError" not in output and output 

345 if robot_found: 345 ↛ 352line 345 didn't jump to line 352 because the condition on line 345 was always true

346 system_encoding = get_system_encoding() 

347 rf_file, rf_version = output.strip().split(b", ") 

348 publish.RideLogMessage(_("Found Robot Framework version %s from %s.") % ( 

349 str(rf_version, system_encoding), str(os.path.dirname(rf_file), system_encoding))).publish() 

350 return rf_version 

351 else: 

352 publish.RideLogMessage(publish.get_html_message('no_robot'), notify_user=True).publish() 

353 

354 def _get_latest_path(self): 1ab

355 recent = self._get_recentfiles_plugin() 

356 if not recent or not recent.recent_files: 356 ↛ 357line 356 didn't jump to line 357 because the condition on line 356 was never true

357 return None 

358 return recent.recent_files[0] 

359 

360 def _get_recentfiles_plugin(self): 1ab

361 from ..recentfiles import RecentFilesPlugin 

362 for pl in self.get_plugins(): 362 ↛ exitline 362 didn't return from function '_get_recentfiles_plugin' because the loop on line 362 didn't complete

363 if isinstance(pl.conn_plugin, RecentFilesPlugin): 

364 return pl.conn_plugin 

365 

366 def get_plugins(self): 1ab

367 return self._plugin_loader.plugins 

368 

369 def register_preference_panel(self, panel_class): 1ab

370 """Add the given panel class to the list of known preference panels""" 

371 self.preferences.add(panel_class) 

372 

373 def unregister_preference_panel(self, panel_class): 1ab

374 """Remove the given panel class from the known preference panels""" 

375 self.preferences.remove(panel_class) 

376 

377 def register_editor(self, object_class, editor_class, activate): 1ab

378 self._editor_provider.register_editor(object_class, editor_class, 

379 activate) 

380 

381 def unregister_editor(self, object_class, editor_class): 1ab

382 self._editor_provider.unregister_editor(object_class, editor_class) 

383 

384 def activate_editor(self, object_class, editor_class): 1ab

385 self._editor_provider.set_active_editor(object_class, editor_class) 

386 

387 def get_editors(self, object_class): 1ab

388 return self._editor_provider.get_editors(object_class) 

389 

390 def get_editor(self, object_class): 1ab

391 return self._editor_provider.get_editor(object_class) 

392 

393 @contextmanager 1ab

394 def active_event_loop(self): 1ab

395 # With wxPython 2.9.1, ProgressBar.Pulse breaks if there's no active 

396 # event loop. 

397 # See http://code.google.com/p/robotframework-ride/issues/detail?id=798 

398 loop = wx.EventLoop() 

399 wx.EventLoop.SetActive(loop) 

400 yield 

401 del loop 

402 

403 def OnEventLoopEnter(self, loop): # Overrides wx method 1ab

404 if loop and wx.EventLoopBase.IsMain(loop): 404 ↛ 405line 404 didn't jump to line 405 because the condition on line 404 was never true

405 RideFSWatcherHandler.create_fs_watcher(self.workspace_path) 

406 

407 def on_app_activate(self, event): 1ab

408 if self.workspace_path is not None and RideFSWatcherHandler.is_watcher_created(): 

409 if event.GetActive(): 

410 if not self.changed_workspace and RideFSWatcherHandler.is_workspace_dirty(): 

411 self.frame.show_confirm_reload_dlg(event) 

412 RideFSWatcherHandler.stop_listening() 

413 else: 

414 RideFSWatcherHandler.start_listening(self.workspace_path) 

415 event.Skip()