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
« 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.
16import builtins 1ab
17import os 1ab
18import wx 1ab
20from contextlib import contextmanager 1ab
21from pathlib import Path 1ab
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
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
55BACKGROUND_HELP = 'background help' 1ab
56FOREGROUND_TEXT = 'foreground text' 1ab
57FONT_SIZE = 'font size' 1ab
58FONT_FACE = 'font face' 1ab
61class UnthemableWidgetError(Exception): 1ab
62 def __init__(self): 1ab
63 Exception.__init__(self, 'HELP! I have no clue how to theme this.')
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
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)
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)
97 class Message:
98 keys = ['General']
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
159 def OnExit(self): 1ab
160 PUBLISHER.unsubscribe_all()
161 self.Destroy()
162 wx.Exit()
163 return True
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)
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()
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)
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
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
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)
306 @staticmethod 1ab
307 def _publish_system_info(): 1ab
308 from ..context import SYSTEM_INFO
309 publish.RideLogMessage(SYSTEM_INFO).publish()
311 @property 1ab
312 def model(self): 1ab
313 return self._controller
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]
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
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)
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()
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]
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
366 def get_plugins(self): 1ab
367 return self._plugin_loader.plugins
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)
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)
377 def register_editor(self, object_class, editor_class, activate): 1ab
378 self._editor_provider.register_editor(object_class, editor_class,
379 activate)
381 def unregister_editor(self, object_class, editor_class): 1ab
382 self._editor_provider.unregister_editor(object_class, editor_class)
384 def activate_editor(self, object_class, editor_class): 1ab
385 self._editor_provider.set_active_editor(object_class, editor_class)
387 def get_editors(self, object_class): 1ab
388 return self._editor_provider.get_editors(object_class)
390 def get_editor(self, object_class): 1ab
391 return self._editor_provider.get_editor(object_class)
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
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)
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()