Coverage for src/robotide/ui/mainframe.py: 52%
646 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
19import wx 1ab
20import wx.lib.agw.aui as aui 1ab
21from wx import Colour 1ab
22from wx.adv import TaskBarIcon, TBI_DOCK, EVT_TASKBAR_LEFT_DOWN 1ab
23from multiprocessing import shared_memory 1ab
25from .actiontriggers import (MenuBar, ToolBarButton, ShortcutRegistry, _RideSearchMenuItem) 1ab
26from .filedialogs import (NewProjectDialog, InitFileFormatDialog) 1ab
27from .fileexplorerplugin import FileExplorer 1ab
28from .notebook import NoteBook 1ab
29from .pluginmanager import PluginManager 1ab
30from .progress import LoadProgressObserver 1ab
31from .review import ReviewDialog 1ab
32from .treeplugin import Tree 1ab
33from ..action import action_info_collection, action_factory, SeparatorInfo 1ab
34from ..action.shortcut import localize_shortcuts 1ab
35from ..context import get_about_ride, SHORTCUT_KEYS, IS_WINDOWS 1ab
36from ..controller.ctrlcommands import SaveFile, SaveAll 1ab
37from ..editor import customsourceeditor 1ab
38from ..preferences import PreferenceEditor 1ab
39from ..publish import (RideSaveAll, RideClosing, RideSaved, PUBLISHER, RideInputValidationError, RideTreeSelection, 1ab
40 RideModificationPrevented, RideBeforeSaving, RideSettingsChanged)
41from ..ui.filedialogs import RobotFilePathDialog 1ab
42from ..ui.tagdialogs import ViewAllTagsDialog 1ab
43from ..utils import RideFSWatcherHandler 1ab
44from ..widgets import RIDEDialog, ImageProvider, HtmlWindow 1ab
46_ = wx.GetTranslation # To keep linter/code analyser happy 1ab
47builtins.__dict__['_'] = wx.GetTranslation 1ab
49ID_CustomizeToolbar = wx.ID_HIGHEST + 1 1ab
50ID_SampleItem = ID_CustomizeToolbar + 1 1ab
51MAINFRAME_POSITION = 'mainframe position' 1ab
52MAINFRAME_MAXIMIZED = 'mainframe maximized' 1ab
55def get_menudata(): 1ab
56 # Menus to translate
57 file_0 = _("[File]\n")
58 file_1 = _("!&New Project | Create a new top level suite | Ctrlcmd-N | ART_NEW\n")
59 separator = "---\n"
60 file_2 = _("!&Open Test Suite | Open file containing tests | Ctrlcmd-O | ART_FILE_OPEN\n")
61 file_3 = _("!Open &Directory | Open directory containing datafiles | Shift-Ctrlcmd-O | ART_FOLDER_OPEN\n")
62 file_4 = _("!Open External File | Open file in Code Editor | | ART_NORMAL_FILE\n")
63 file_5 = _("!&Save | Save selected datafile | Ctrlcmd-S | ART_FILE_SAVE\n")
64 file_6 = _("!Save &All | Save all changes | Ctrlcmd-Shift-S | ART_FILE_SAVE_AS\n")
65 file_7 = _("!E&xit | Exit RIDE | Ctrlcmd-Q\n")
66 tool_0 = _("[Tools]\n")
67 tool_1 = _("!Search Unused Keywords | | | | POSITION-54\n")
68 tool_2 = _("!Manage Plugins | | | | POSITION-81\n")
69 tool_3 = _("!View All Tags | | F7 | | POSITION-82\n")
70 tool_4 = _("!Preferences | | | | POSITION-99\n")
71 help_0 = _("[Help]\n")
72 help_1 = _("!Shortcut keys | RIDE shortcut keys\n")
73 help_2 = _("!User Guide | Robot Framework User Guide\n")
74 help_3 = _("!Wiki | RIDE User Guide (Wiki)\n")
75 help_4 = _("!Report a Problem | Open browser to SEARCH on the RIDE issue tracker\n")
76 help_6 = _("!About | Information about RIDE\n")
77 help_7 = _("!Check for Upgrade | Looks at PyPi for new released version\n")
79 return (file_0 + file_1 + separator + file_2 + file_3 + file_4 + separator + file_5 + file_6 + separator +
80 file_7 + '\n' + tool_0 + tool_1 + tool_2 + tool_3 + tool_4 + '\n' + help_0 + help_1 + help_2 +
81 help_3 + help_4 + help_6 + help_7)
84class RideFrame(wx.Frame): 1ab
86 _menudata_nt = """[File] 1ab
87 !&New Project | Create a new top level suite | Ctrlcmd-N | ART_NEW
88 ---
89 !&Open Test Suite | Open file containing tests | Ctrlcmd-O | ART_FILE_OPEN
90 !Open &Directory | Open directory containing datafiles | Shift-Ctrlcmd-O | ART_FOLDER_OPEN
91 !Open External File | Open file in Code Editor | | ART_NORMAL_FILE
92 ---
93 !&Save | Save selected datafile | Ctrlcmd-S | ART_FILE_SAVE
94 !Save &All | Save all changes | Ctrlcmd-Shift-S | ART_FILE_SAVE_AS
95 ---
96 !E&xit | Exit RIDE | Ctrlcmd-Q
98 [Tools]
99 !Search Unused Keywords | | | | POSITION-54
100 !Manage Plugins | | | | POSITION-81
101 !View All Tags | | F7 | | POSITION-82
102 !Preferences | | | | POSITION-99
104 [Help]
105 !Shortcut keys | RIDE shortcut keys
106 !User Guide | Robot Framework User Guide
107 !Wiki | RIDE User Guide (Wiki)
108 !Report a Problem | Open browser to SEARCH on the RIDE issue tracker
109 !About | Information about RIDE
110 !Check for Upgrade | Looks at PyPi for new released version
111 """
113 def __init__(self, application, controller): 1ab
114 size = application.settings.get('mainframe size', (1100, 700))
115 # DEBUG self.general_settings = application.settings['General']
116 wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, title='RIDE',
117 pos=application.settings.get(MAINFRAME_POSITION, (50, 30)),
118 size=size, style=wx.DEFAULT_FRAME_STYLE | wx.SUNKEN_BORDER | wx.BORDER_THEME)
120 # Shared memory to store language definition
121 try:
122 self.sharemem = shared_memory.ShareableList(['en'], name="language")
123 except FileExistsError: # Other instance created file
124 self.sharemem = shared_memory.ShareableList(name="language")
125 # set Left to Right direction (while we don't have localization)
126 self.SetLayoutDirection(wx.Layout_LeftToRight)
127 # self.SetLayoutDirection(wx.Layout_RightToLeft)
129 self.fontinfo = application.fontinfo
130 self.SetFont(wx.Font(self.fontinfo))
132 self.aui_mgr = aui.AuiManager(self)
134 # tell AuiManager to manage this frame
135 self.aui_mgr.SetManagedWindow(self)
137 self.SetMinSize(wx.Size(400, 300))
139 self.ensure_on_screen()
140 if application.settings.get(MAINFRAME_MAXIMIZED, False): 140 ↛ 141line 140 didn't jump to line 141 because the condition on line 140 was never true
141 self.Maximize()
142 self._application = application
143 self.controller = controller
144 self._image_provider = ImageProvider()
145 self.reformat = application.settings.get('reformat', False)
146 self.tasks = application.settings.get('tasks', False)
147 self.doc_language = application.settings.get('doc language', '')
148 self._notebook_theme = application.settings.get('notebook theme', 0)
149 self.general_settings = application.settings['General'] # .get_without_default('General')
150 self.color_background_help = self.general_settings.get('background help', (240, 242, 80))
151 self.color_foreground_text = self.general_settings.get('foreground text', (7, 0, 70))
152 self.color_background = self.general_settings.get_without_default('background')
153 self.color_foreground = self.general_settings.get_without_default('foreground')
154 self.font_face = self.general_settings.get('font face', '')
155 self.font_size = self.general_settings.get('font size', 11)
156 self.ui_language = self.general_settings.get('ui language', 'English')
157 self.main_menu = None
158 self._init_ui()
159 self._task_bar_icon = RIDETaskBarIcon(self, self._image_provider)
160 self._plugin_manager = PluginManager(self.notebook)
161 self._review_dialog = None
162 self._view_all_tags_dialog = None
163 self._current_external_dir = None
164 self.Bind(wx.EVT_CLOSE, self.on_close)
165 self.Bind(wx.EVT_SIZE, self.on_size)
166 self.Bind(wx.EVT_MOVE, self.on_move)
167 self.Bind(wx.EVT_MAXIMIZE, self.on_maximize)
168 self.Bind(wx.EVT_DIRCTRL_FILEACTIVATED, self.on_open_file)
169 self.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.on_menu_open_file)
170 self._subscribe_messages()
171 wx.CallAfter(self.actions.register_tools) # DEBUG
172 # DEBUG wx.CallAfter(self.OnSettingsChanged, self.general_settings)
174 def _subscribe_messages(self): 1ab
175 for listener, topic in [
176 (lambda message: self.SetStatusText(_('Saved %s') % message.path), RideSaved),
177 (lambda message: self.SetStatusText(_('Saved all files')), RideSaveAll),
178 (self._set_label, RideTreeSelection),
179 (self._show_validation_error, RideInputValidationError),
180 (self._show_modification_prevented_error, RideModificationPrevented),
181 (self.on_ui_language_changed, RideSettingsChanged)
182 ]:
183 PUBLISHER.subscribe(listener, topic)
185 def _set_label(self, message): 1ab
186 self.SetTitle(self._create_title(message))
188 @staticmethod 1ab
189 def _create_title(message): 1ab
190 title = 'RIDE'
191 if message:
192 item = message.item
193 title += ' - ' + item.datafile.name
194 if not item.is_modifiable():
195 title += _(' (READ ONLY)')
196 return title
198 @staticmethod 1ab
199 def _show_validation_error(message): 1ab
200 message_box = RIDEDialog(title=_('Validation Error'), message=message.message, style=wx.ICON_ERROR|wx.OK)
201 message_box.ShowModal()
203 @staticmethod 1ab
204 def _show_modification_prevented_error(message): 1ab
205 message_box = RIDEDialog(title=_("Modification prevented"),
206 message=_("\"%s\" is read only") % message.controller.datafile_controller.filename,
207 style=wx.ICON_ERROR|wx.OK)
208 # message_box.CenterOnParent()
209 message_box.ShowModal()
211 def _init_ui(self): 1ab
212 """ DEBUG:
213 self.aui_mgr.AddPane(wx.Panel(self), aui.AuiPaneInfo().CenterPane())
214 self.splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE)
215 """
216 if not self.main_menu: 216 ↛ 219line 216 didn't jump to line 219 because the condition on line 216 was always true
217 new_ui = True
218 else:
219 new_ui = False
220 if new_ui: # Only when creating UI we add panes 220 ↛ 235line 220 didn't jump to line 235 because the condition on line 220 was always true
221 self.aui_mgr.AddPane(wx.Panel(self), aui.AuiPaneInfo().Name("right_pane").Right())
222 # set up default notebook style
223 self._notebook_style = (aui.AUI_NB_DEFAULT_STYLE | aui.AUI_NB_WINDOWLIST_BUTTON | aui.AUI_NB_TAB_FLOAT |
224 aui.AUI_NB_TAB_EXTERNAL_MOVE | aui.AUI_NB_SUB_NOTEBOOK | aui.AUI_NB_SMART_TABS)
225 self._notebook_style ^= aui.AUI_NB_TAB_FIXED_WIDTH
226 # DEBUG: self._notebook_theme = 0 (allow to select themes for notebooks)
227 # DEBUG:self.notebook = NoteBook(self.splitter, self._application, self._notebook_style)
228 self.notebook = NoteBook(self, self._application, self._notebook_style)
229 self.notebook.SetFont(wx.Font(self.fontinfo))
230 self.notebook.SetBackgroundColour(Colour(self.color_background))
231 self.notebook.SetForegroundColour(Colour(self.color_foreground))
232 self.aui_mgr.AddPane(self.notebook, aui.AuiPaneInfo().Name("notebook_editors").
233 CenterPane().PaneBorder(True))
234 # we need to remake Menu if language changes
235 if self.main_menu: 235 ↛ 236line 235 didn't jump to line 236 because the condition on line 235 was never true
236 del self.main_menu
237 pane = self.aui_mgr.GetPaneByName("maintoolbar")
238 self.aui_mgr.DetachPane(pane)
239 pane.DestroyOnClose(True)
240 self.aui_mgr.ClosePane(pane)
241 del pane
242 # DEBUG: del self.toolbar
244 _menudata = get_menudata()
246 self.main_menu = MenuBar(self)
247 self.main_menu.take_menu_bar_into_use()
248 self.toolbar = ToolBar(self)
249 self.toolbar.SetMinSize(wx.Size(100, 60))
250 self.toolbar.SetBackgroundColour(Colour(self.color_background))
251 self.toolbar.SetForegroundColour(Colour(self.color_foreground))
252 # self.SetToolBar(self.toolbar.GetToolBar())
253 self.main_menu._mb.SetFont(wx.Font(self.fontinfo))
254 self.main_menu.m_frame.SetFont(wx.Font(self.fontinfo))
255 self.main_menu.m_frame.SetBackgroundColour(Colour(self.color_background))
256 self.main_menu.m_frame.SetForegroundColour(Colour(self.color_foreground))
258 self.aui_mgr.AddPane(self.toolbar, aui.AuiPaneInfo().Name("maintoolbar").ToolbarPane().Top())
259 self.actions = ActionRegisterer(self.aui_mgr, self.main_menu, self.toolbar, ShortcutRegistry(self))
260 """
261 ##### Test
262 tb3 = self.testToolbar()
264 self.aui_mgr.AddPane(tb3,
265 aui.AuiPaneInfo().Name("tb3").Caption("Toolbar 3").
266 ToolbarPane().Top().Row(1).Position(1))
268 ##### End Test
269 """
270 # DEBUG: self.leftpanel = wx.Panel(self, name="left_panel", size = (275, 250))
271 if new_ui: # Only when creating UI we add panes 271 ↛ 284line 271 didn't jump to line 284 because the condition on line 271 was always true
272 # Tree is always created here
273 self.tree = Tree(self, self.actions, self._application.settings)
274 self.tree.SetMinSize(wx.Size(275, 250))
275 self.tree.SetFont(wx.Font(self.fontinfo))
276 # self.leftpanel.Bind(wx.EVT_SIZE, self.tree.OnSize)
277 # self.aui_mgr.AddPane(self.leftpanel, aui.AuiPaneInfo().Name("left_panel").Caption("left_panel").Left())
278 # DEBUG: Next was already called from application.py
279 self.aui_mgr.AddPane(self.tree,
280 aui.AuiPaneInfo().Name("tree_content").Caption(_("Test Suites")).CloseButton(False).
281 LeftDockable()) # DEBUG: remove .CloseButton(False) when restore is fixed
282 # DEBUG: self.aui_mgr.GetPane(self.tree).DestroyOnClose()
283 # TreePlugin will manage showing the Tree
284 self.actions.register_actions(action_info_collection(_menudata, self, data_nt=self._menudata_nt,
285 container=self.tree))
286 if new_ui: # Only when creating UI we add panes 286 ↛ 295line 286 didn't jump to line 295 because the condition on line 286 was always true
287 # ##### File explorer panel is always created here
288 self.filemgr = FileExplorer(self, self.controller)
289 self.filemgr.SetFont(wx.Font(self.fontinfo))
290 self.filemgr.SetMinSize(wx.Size(275, 250))
291 # DEBUG: Next was already called from application.py
292 self.aui_mgr.AddPane(self.filemgr, aui.AuiPaneInfo().Name("file_manager").LeftDockable())
294 # self.main_menu.take_menu_bar_into_use()
295 if new_ui: # Only when creating UI we add panes 295 ↛ 301line 295 didn't jump to line 301 because the condition on line 295 was always true
296 self.CreateStatusBar(number=2, name="StatusBar")
297 self._status_bar = self.FindWindowByName("StatusBar", self)
298 self._status_bar.SetStatusWidths([-2, -3])
299 # set main frame icon
300 self.SetIcons(self._image_provider.PROGICONS)
301 if self._status_bar: 301 ↛ 308line 301 didn't jump to line 308 because the condition on line 301 was always true
302 self.SetStatusBarPane(0)
303 self._status_bar.SetFont(wx.Font(self.fontinfo))
304 self._status_bar.SetBackgroundColour(Colour(self.color_background))
305 self._status_bar.SetForegroundColour(Colour(self.color_foreground))
306 self._status_bar.SetOwnForegroundColour(Colour(self.color_foreground))
307 # change notebook theme
308 self.set_notebook_theme()
309 # tell the manager to "commit" all the changes just made
310 self.aui_mgr.Update()
311 # wx.CallLater(2000, RideSettingsChanged(keys=("General", ''), old='', new='').publish)
313 def set_notebook_theme(self): 1ab
314 if not self.notebook: 314 ↛ 315line 314 didn't jump to line 315 because the condition on line 314 was never true
315 return
316 try:
317 self._notebook_theme = int(self._notebook_theme)
318 except ValueError:
319 self._notebook_theme = 0
320 if self._notebook_theme == 1: 320 ↛ 321line 320 didn't jump to line 321 because the condition on line 320 was never true
321 self.notebook.SetArtProvider(aui.AuiSimpleTabArt())
322 elif self._notebook_theme == 2: 322 ↛ 323line 322 didn't jump to line 323 because the condition on line 322 was never true
323 self.notebook.SetArtProvider(aui.VC71TabArt())
324 elif self._notebook_theme == 3: 324 ↛ 325line 324 didn't jump to line 325 because the condition on line 324 was never true
325 self.notebook.SetArtProvider(aui.FF2TabArt())
326 elif self._notebook_theme == 4: 326 ↛ 327line 326 didn't jump to line 327 because the condition on line 326 was never true
327 self.notebook.SetArtProvider(aui.VC8TabArt())
328 elif self._notebook_theme == 5:
329 self.notebook.SetArtProvider(aui.ChromeTabArt())
330 else:
331 self.notebook.SetArtProvider(aui.AuiDefaultTabArt())
333 def get_selected_datafile(self): 1ab
334 return self.tree.get_selected_datafile()
336 def get_selected_datafile_controller(self): 1ab
337 return self.tree.get_selected_datafile_controller()
339 def on_close(self, event): 1ab
340 from ..preferences import RideSettings
341 if self._allowed_to_exit():
342 try:
343 perspective = self.aui_mgr.SavePerspective()
344 # Next restore of settings is because we may have edited from Preferences
345 self._application.settings = RideSettings(self._application.settings_path)
346 self._application.settings.set('AUI Perspective', perspective)
347 # deinitialize the frame manager
348 self.aui_mgr.UnInit()
349 del self.aui_mgr
350 except AttributeError:
351 pass
352 try:
353 nb_perspective = self.notebook.SavePerspective()
354 self._application.settings.set('AUI NB Perspective', nb_perspective)
355 except AttributeError:
356 pass
357 PUBLISHER.unsubscribe(self._set_label, RideTreeSelection)
358 RideClosing().publish()
359 # DEBUG: Wrap in try/except for RunTime error
360 try:
361 self._task_bar_icon.RemoveIcon()
362 self._task_bar_icon.Destroy()
363 except RuntimeError:
364 pass
365 try:
366 PUBLISHER.unsubscribe_all()
367 self.Destroy()
368 wx.Exit()
369 except RuntimeError:
370 pass
371 app = wx.GetApp()
372 if app is not self._application:
373 # other wx app instance created unexpectedly
374 # this will cause RIDE app instance cannot invoke ExitMainLoop properly
375 self._application.ExitMainLoop()
376 wx.Exit()
377 else:
378 wx.CloseEvent.Veto(event)
380 def on_size(self, event): 1ab
381 if wx.VERSION >= (4, 1, 0): 381 ↛ 384line 381 didn't jump to line 384 because the condition on line 381 was always true1ac
382 size = self.DoGetSize() 1ac
383 else:
384 size = tuple(self.GetSize())
385 is_full_screen_mode = size == wx.DisplaySize() 1ac
386 self._application.settings[MAINFRAME_MAXIMIZED] = self.IsMaximized() or is_full_screen_mode 1ac
387 if not is_full_screen_mode: 387 ↛ 389line 387 didn't jump to line 389 because the condition on line 387 was always true1ac
388 self._application.settings['mainframe size'] = size 1ac
389 if wx.VERSION >= (4, 1, 0): 389 ↛ 392line 389 didn't jump to line 392 because the condition on line 389 was always true1ac
390 self._application.settings[MAINFRAME_POSITION] = self.DoGetPosition() 1ac
391 else:
392 self._application.settings[MAINFRAME_POSITION] = tuple(self.GetPosition())
393 event.Skip() 1ac
395 def on_move(self, event): 1ab
396 # When the window is Iconized, a move event is also raised, but we
397 # don't want to update the position in the settings file
398 if not self.IsIconized() and not self.IsMaximized():
399 if wx.VERSION >= (4, 1, 0):
400 self._application.settings[MAINFRAME_POSITION] = self.DoGetPosition()
401 else:
402 self._application.settings[MAINFRAME_POSITION] = tuple(self.GetPosition())
403 event.Skip()
405 def on_maximize(self, event): 1ab
406 self._application.settings[MAINFRAME_MAXIMIZED] = True
407 event.Skip()
409 def _allowed_to_exit(self): 1ab
410 if self.has_unsaved_changes():
411 message_box = RIDEDialog(title=_('Warning'), message=_("There are unsaved modifications.\n"
412 "Do you want to save your changes before "
413 "exiting?"), style=wx.ICON_WARNING | wx.CANCEL | wx.YES_NO)
414 ret = message_box.execute()
415 if ret in (wx.CANCEL, wx.ID_CANCEL):
416 return False
417 if ret in (wx.YES, wx.ID_YES, wx.OK, wx.ID_OK):
418 self.save()
419 return True
421 def has_unsaved_changes(self): 1ab
422 return self.controller.is_dirty()
424 def on_new_project(self, event): 1ab
425 __ = event
426 if not self.check_unsaved_modifications():
427 return
428 NewProjectDialog(self.controller).execute()
429 self._populate_tree()
431 def _populate_tree(self): 1ab
432 self.tree.populate(self.controller)
433 self.filemgr.update_tree()
435 def on_open_file(self, event): 1ab
436 __ = event
437 if not self.filemgr:
438 return
439 # EVT_DIRCTRL_FILEACTIVATED
440 from os.path import splitext
441 robottypes = self._application.settings.get('robot types', ['robot',
442 'resource',
443 'txt',
444 'tsv']) # Removed 'html'
445 path = self.filemgr.GetFilePath()
446 ext = ''
447 if len(path) > 0:
448 ext = splitext(path)
449 ext = ext[1].replace('.', '')
450 # print("DEBUG: path %s ext %s" % (path, ext))
451 if len(ext) > 0 and ext in robottypes:
452 if not self.check_unsaved_modifications():
453 return
454 if self.open_suite(path):
455 return
456 customsourceeditor.main(path)
458 def on_menu_open_file(self, event): 1ab
459 if not self.filemgr:
460 return
461 # DEBUG: Use widgets/popupmenu tools
462 path = self.filemgr.GetFilePath()
463 if len(path) > 0:
464 self.on_open_file(event)
465 else:
466 path = self.filemgr.GetPath()
467 if not self.check_unsaved_modifications():
468 return
469 self.open_suite(path) # It is a directory, do not edit
470 event.Skip()
472 def on_open_external_file(self, event): 1ab
473 __ = event
474 if not self._current_external_dir:
475 curdir = self.controller.default_dir
476 else:
477 curdir = self._current_external_dir
478 fdlg = wx.FileDialog(self, defaultDir=curdir, style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
479 if fdlg.ShowModal() == wx.ID_CANCEL:
480 return
481 path = fdlg.GetPath()
482 try:
483 self._current_external_dir = os.path.dirname(path)
484 customsourceeditor.main(path)
485 except IOError:
486 wx.LogError(f"Cannot open file {path}")
488 def on_open_test_suite(self, event): 1ab
489 __ = event
490 if not self.check_unsaved_modifications():
491 return
492 path = RobotFilePathDialog(
493 self, self.controller, self._application.settings).execute()
494 if path:
495 if self.open_suite(path):
496 return
497 customsourceeditor.main(path)
499 def check_unsaved_modifications(self): 1ab
500 if self.has_unsaved_changes():
501 message_box = RIDEDialog(title=_("Warning"), message=_("There are unsaved modifications.\n"
502 "Do you want to proceed without saving?"), style=wx.ICON_WARNING | wx.YES_NO)
503 ret = message_box.ShowModal()
504 return ret == wx.ID_YES
505 return True
507 def open_suite(self, path): 1ab
508 self.controller.update_default_dir(path)
509 # self._controller.default_dir will only save dir path
510 # need to save path to self._application.workspace_path too
511 self._application.workspace_path = path
512 if IS_WINDOWS:
513 self._application.changed_workspace = True
514 from ..lib.compat.parsing.language import check_file_language
515 self.controller.file_language = check_file_language(path)
516 set_lang = []
517 set_lang.append('en')
518 try:
519 set_lang = shared_memory.ShareableList(name="language")
520 except FileNotFoundError:
521 set_lang[0] = 'en'
522 if self.controller.file_language:
523 set_lang[0] = self.controller.file_language[0]
524 # print(f"DEBUG: project.py Project load_data file_language = {self.controller.file_language}\n"
525 # f"sharedmem={set_lang}")
526 else:
527 set_lang[0] = 'en'
528 try:
529 err = self.controller.load_datafile(path, LoadProgressObserver(self, background=self.color_background,
530 foreground=self.color_foreground))
531 if isinstance(err, UserWarning):
532 # DEBUG: raise err # Just leave message in Parser Log
533 return False
534 except UserWarning:
535 return False
536 self._populate_tree()
537 if IS_WINDOWS:
538 wx.CallLater(60000, self.clear_workspace_state)
539 return True
541 def clear_workspace_state(self): 1ab
542 self._application.changed_workspace = False
544 def refresh_datafile(self, item, event): 1ab
545 self.tree.refresh_datafile(item, event)
546 if self.filemgr:
547 self.filemgr.ReCreateTree()
549 def on_open_directory(self, event): 1ab
550 __ = event
551 if self.check_unsaved_modifications():
552 path = wx.DirSelector(message=_("Choose a directory containing Robot files"),
553 default_path=self.controller.default_dir)
554 if path:
555 self.open_suite(path)
557 def on_save(self, event): 1ab
558 __ = event
559 RideBeforeSaving().publish()
560 self.save()
562 def on_save_all(self, event): 1ab
563 __ = event
564 RideBeforeSaving().publish()
565 self.save_all()
567 def save_all(self): 1ab
568 self._show_dialog_for_files_without_format()
569 self.controller.execute(SaveAll(self.reformat))
571 def save(self, controller=None): 1ab
572 if controller is None:
573 controller = self.get_selected_datafile_controller()
574 if controller is not None:
575 if not controller.has_format():
576 self._show_dialog_for_files_without_format(controller)
577 else:
578 controller.execute(SaveFile(self.reformat))
580 def _show_dialog_for_files_without_format(self, controller=None): 1ab
581 files_without_format = self.controller.get_files_without_format(
582 controller)
583 for f in files_without_format:
584 self._show_format_dialog_for(f)
586 @staticmethod 1ab
587 def _show_format_dialog_for(file_controller_without_format): 1ab
588 InitFileFormatDialog(file_controller_without_format).execute()
590 def on_exit(self, event): 1ab
591 __ = event
592 try:
593 self.sharemem.shm.close()
594 self.sharemem.shm.unlink()
595 except FileNotFoundError:
596 pass
597 self.Close()
599 def on_manage_plugins(self, event): 1ab
600 __ = event
601 self._plugin_manager.show(self._application.get_plugins())
603 def on_view_all_tags(self, event): 1ab
604 __ = event
605 if self._view_all_tags_dialog is None:
606 self._view_all_tags_dialog = ViewAllTagsDialog(self.controller, self)
607 self._view_all_tags_dialog.show_dialog()
609 def on_search_unused_keywords(self, event): 1ab
610 __ = event
611 if self._review_dialog is None:
612 self._review_dialog = ReviewDialog(self.controller, self)
613 self._review_dialog.show_dialog()
615 def on_preferences(self, event): 1ab
616 __ = event
617 dlg = PreferenceEditor(self, _("RIDE - Preferences"),
618 self._application.preferences, style='tree')
619 # Changed to non-modal, original comment follows:
620 # I would prefer that this not be modal, but making it non-modal
621 # opens up a can of worms. We don't want to have to deal
622 # with settings getting changed out from under us while the
623 # dialog is open.
624 dlg.Show()
626 @staticmethod 1ab
627 def on_about(event): 1ab
628 __ = event
629 dlg = AboutDialog()
630 dlg.ShowModal()
631 dlg.Destroy()
633 def on_check_for_upgrade(self, event): 1ab
634 __ = event
635 from ..application.updatenotifier import UpdateNotifierController, UpdateDialog
636 wx.CallAfter(UpdateNotifierController(self.general_settings, self.notebook).notify_update_if_needed,
637 UpdateDialog, ignore_check_condition=True, show_no_update=True)
639 @staticmethod 1ab
640 def on_shortcut_keys(event): 1ab
641 __ = event
642 dialog = ShortcutKeysDialog()
643 """ DEBUG:
644 self.aui_mgr.AddPane(dialog.GetContentWindow(),aui.AuiPaneInfo().Name("shortcuts").Caption("Shortcuts Keys")
645 .CloseButton(True).RightDockable().Floatable().Float(), self.notebook)
646 self.aui_mgr.Update()
647 """
648 dialog.Show()
650 @staticmethod 1ab
651 def on_report_a_problem(event): 1ab
652 __ = event
653 wx.LaunchDefaultBrowser("https://github.com/robotframework/RIDE/issues"
654 "?utf8=%E2%9C%93&q=is%3Aissue+%22search"
655 "%20your%20problem%22"
656 )
658 @staticmethod 1ab
659 def on_user_guide(event): 1ab
660 __ = event
661 wx.LaunchDefaultBrowser("https://robotframework.org/robotframework/#user-guide")
663 @staticmethod 1ab
664 def on_wiki(event): 1ab
665 __ = event
666 wx.LaunchDefaultBrowser("https://github.com/robotframework/RIDE/wiki")
668 def _has_data(self): 1ab
669 return self.controller.data is not None
671 def _refresh(self): 1ab
672 self.controller.update_namespace()
674 # This code is copied from http://wiki.wxpython.org/EnsureFrameIsOnScreen,
675 # and adapted to fit our code style.
676 def ensure_on_screen(self): 1ab
677 try:
678 display_id = wx.Display.GetFromWindow(self)
679 except NotImplementedError:
680 display_id = 0
681 if display_id == -1: 681 ↛ 682line 681 didn't jump to line 682 because the condition on line 681 was never true
682 display_id = 0
683 geometry = wx.Display(display_id).GetGeometry()
684 position = self.GetPosition()
685 if position.x < geometry.x: 685 ↛ 686line 685 didn't jump to line 686 because the condition on line 685 was never true
686 position.x = geometry.x
687 if position.y < geometry.y: 687 ↛ 688line 687 didn't jump to line 688 because the condition on line 687 was never true
688 position.y = geometry.y
689 size = self.GetSize()
690 if size.width > geometry.width: 690 ↛ 691line 690 didn't jump to line 691 because the condition on line 690 was never true
691 size.width = geometry.width
692 position.x = geometry.x
693 elif position.x + size.width > geometry.x + geometry.width: 693 ↛ 694line 693 didn't jump to line 694 because the condition on line 693 was never true
694 position.x = geometry.x + geometry.width - size.width
695 if size.height > geometry.height: 695 ↛ 696line 695 didn't jump to line 696 because the condition on line 695 was never true
696 size.height = geometry.height
697 position.y = geometry.y
698 elif position.y + size.height > geometry.y + geometry.height: 698 ↛ 699line 698 didn't jump to line 699 because the condition on line 698 was never true
699 position.y = geometry.y + geometry.height - size.height
700 self.SetPosition(position)
701 self.SetSize(size)
703 def show_confirm_reload_dlg(self, event): 1ab
704 msg = [_('Workspace modifications detected on the file system.'),
705 _('Do you want to reload the workspace?')]
706 if self.controller.is_dirty():
707 msg += [_('Answering <Yes> will discard unsaved changes.'),
708 _('Answering <No> will ignore the changes on disk.')]
709 message_box = RIDEDialog(title=_('Files Changed On Disk'), message='\n'.join(msg),
710 style=wx.ICON_WARNING | wx.YES_NO)
711 ret = message_box.ShowModal()
712 confirmed = ret == wx.ID_YES
713 if confirmed:
714 # workspace_path should update after open directory/suite
715 # There are two scenarios:
716 # 1. path is a directory
717 # 2. path is a suite file
718 new_path = RideFSWatcherHandler.get_workspace_new_path()
719 if new_path and os.path.exists(new_path):
720 wx.CallAfter(self.open_suite, new_path)
721 else:
722 # in case workspace is totally removed
723 # ask user to open new directory
724 # DEBUG: add some notification msg to users
725 wx.CallAfter(self.on_open_directory, event)
727 def on_ui_language_changed(self, message): 1ab
728 if message.keys[0] != "General": 1ac
729 return 1ac
730 self.ui_language = self.general_settings.get('ui language', 'English')
731 # print(f"DEBUG: mainframe.py on_ui_language_changed message.items={message.keys}, menudata={get_menudata}\n"
732 # f"language={self.ui_language} Translated Warning={_('Warning')} ")
733 # DANGER!!! # The below refresh works, but we lose TestRunner buttons in taskbar and Edit menu is broken
734 # wx.CallLater(1000, self._init_ui) # Let the change happen at application
737# Code moved from actiontriggers
738class ToolBar(aui.AuiToolBar): 1ab
740 def __init__(self, frame): 1ab
741 aui.AuiToolBar.__init__(self, frame)
742 # prepare a few custom overflow elements for the toolbars' overflow buttons
743 prepend_items, append_items = [], []
744 item = aui.AuiToolBarItem()
746 item.SetKind(wx.ITEM_SEPARATOR)
747 append_items.append(item)
749 item = aui.AuiToolBarItem()
750 item.SetKind(wx.ITEM_NORMAL)
751 item.SetId(ID_CustomizeToolbar)
752 item.SetLabel(_("Customize..."))
753 append_items.append(item)
755 self._frame = frame
756 # DEBUG If we attach to frame it won't be detachable, and overlaps
757 # If self, buttons are not shown
758 self.tb = aui.AuiToolBar(self, -1, wx.DefaultPosition, wx.DefaultSize,
759 agwStyle=aui.AUI_TB_DEFAULT_STYLE | aui.AUI_TB_OVERFLOW)
760 self.tb.SetToolBitmapSize(wx.Size(16, 16))
761 self._buttons = []
762 self._search_handlers = {}
763 self._current_description = None
764 self.SetMinSize(wx.Size(100, 60))
765 self.tb.SetCustomOverflowItems(prepend_items, append_items)
766 self.tb.Realize()
768 def register(self, action): 1ab
769 if action.has_icon(): 1ad
770 button = self._get_existing_button(action)
771 if not button: 771 ↛ 773line 771 didn't jump to line 773 because the condition on line 771 was always true
772 button = self._create_button(action)
773 button.register(action)
775 def _get_existing_button(self, action): 1ab
776 for button in self._buttons:
777 if button.icon == action.icon: 777 ↛ 778line 777 didn't jump to line 778 because the condition on line 777 was never true
778 return button
779 return None
781 def enabled_status_changed(self, idd, action): 1ab
782 self.EnableTool(idd, action.is_active())
784 def _create_button(self, action): 1ab
785 button = ToolBarButton(self._frame, self, action)
786 name = self._format_button_tooltip(action)
787 self.AddTool(button.id, name, action.icon, wx.NullBitmap,
788 wx.ITEM_NORMAL, name, action.doc)
789 self.Realize()
790 self._buttons.append(button)
791 return button
793 @staticmethod 1ab
794 def _format_button_tooltip(action): 1ab
795 tooltip = action.name.replace('&', '')
796 if action.shortcut and action.shortcut.value:
797 tooltip = '%s (%s)' % (tooltip, action.shortcut.value)
798 return tooltip
800 def remove_toolbar_button(self, button): 1ab
801 self._buttons.remove(button)
802 self.DeleteTool(button.id)
803 self.Realize()
805 def register_search_handler(self, description, handler, icon, 1ab
806 default=False):
807 if default:
808 self._current_description = description
809 self._search_handlers[description] = _RideSearchMenuItem(handler, icon)
812class ActionRegisterer(object): 1ab
814 def __init__(self, aui_mgr, menubar, toolbar, shortcut_registry): 1ab
815 self._aui_mgr = aui_mgr
816 self._menubar = menubar
817 self._toolbar = toolbar
818 self._shortcut_registry = shortcut_registry
819 self._tools_items = dict()
821 def register_action(self, action_info, update_aui=True): 1ab
822 menubar_can_be_registered = True 1ad
823 action = action_factory(action_info) 1ad
824 self._shortcut_registry.register(action) 1ad
825 if hasattr(action_info, "menu_name"): 825 ↛ 830line 825 didn't jump to line 830 because the condition on line 825 was always true1ad
826 # print(f"DEBUG: mainframe.py ActionRegister register_action menu_name={action_info.menu_name}")
827 if action_info.menu_name == _("Tools"): 1ad
828 self._tools_items[action_info.position] = action 1ad
829 menubar_can_be_registered = False 1ad
830 if menubar_can_be_registered: 1ad
831 self._menubar.register(action) 1ad
832 self._toolbar.register(action) 1ad
833 if update_aui: 1ad
834 # tell the manager to "commit" all the changes just made
835 self._aui_mgr.Update() 1ad
836 return action 1ad
838 def register_tools(self): 1ab
839 separator_action = action_factory(SeparatorInfo(_("Tools"))) 1ac
840 add_separator_after = [_("stop test run"), _("search unused keywords"), 1ac
841 _("preview"), _("view ride log")]
842 # for key in sorted(self._tools_items.iterkeys()):
843 # print("DEBUG: at register_tools, tools: %s" % self._tools_items)
844 for key in sorted(self._tools_items.keys()): # DEBUG Python3 1ac
845 self._menubar.register(self._tools_items[key]) 1ac
846 # print("DEBUG: key=%s name=%s" % (key, self._tools_items[key].name.lower()))
847 if self._tools_items[key].name.lower() in add_separator_after: 1ac
848 self._menubar.register(separator_action) 1ac
850 def register_actions(self, actions): 1ab
851 for action in actions:
852 if not isinstance(action, SeparatorInfo): # DEBUG
853 # print("DEBUG: action=%s" % action.name)
854 self.register_action(action, update_aui=False)
855 # tell the manager to "commit" all the changes just made
856 self._aui_mgr.Update()
858 def register_shortcut(self, action_info): 1ab
859 action = action_factory(action_info) 1ad
860 self._shortcut_registry.register(action) 1ad
861 return action 1ad
864class AboutDialog(RIDEDialog): 1ab
866 def __init__(self): 1ab
867 RIDEDialog.__init__(self, title='RIDE')
868 # set Left to Right direction (while we don't have localization)
869 self.SetLayoutDirection(wx.Layout_LeftToRight)
870 sizer = wx.BoxSizer(wx.VERTICAL)
871 content = get_about_ride()
872 sizer.Add(HtmlWindow(self, (650, 350), content), 1, flag=wx.EXPAND)
873 self.SetSizerAndFit(sizer)
875 def on_key(self, *args): 1ab
876 """ Just ignore keystrokes """
877 pass
880class ShortcutKeysDialog(RIDEDialog): 1ab
882 def __init__(self): 1ab
883 RIDEDialog.__init__(self, title=_("Shortcut keys for RIDE"))
884 # set Left to Right direction (while we don't have localization)
885 self.SetLayoutDirection(wx.Layout_LeftToRight)
886 sizer = wx.BoxSizer(wx.HORIZONTAL)
887 sizer.Add(HtmlWindow(self, (350, 400),
888 self._get_platform_specific_shortcut_keys()), 1,
889 flag=wx.EXPAND)
890 self.SetSizerAndFit(sizer)
892 def on_key(self, *args): 1ab
893 """ Just ignore keystrokes """
894 pass
896 @staticmethod 1ab
897 def _get_platform_specific_shortcut_keys(): 1ab
898 return localize_shortcuts(SHORTCUT_KEYS)
901class RIDETaskBarIcon(TaskBarIcon): 1ab
903 def __init__(self, frame, img_provider): 1ab
904 TaskBarIcon.__init__(self, TBI_DOCK)
905 self.frame = frame
906 self._img_provider = img_provider
907 self.SetIcon(wx.Icon(self._img_provider.RIDE_ICON), "RIDE")
908 self.Bind(EVT_TASKBAR_LEFT_DOWN, self.on_click)
909 self.Bind(wx.EVT_MENU, self.on_task_bar_activate, id=1)
910 self.Bind(wx.EVT_MENU, self.on_task_bar_deactivate, id=2)
911 self.Bind(wx.EVT_MENU, self.on_task_bar_close, id=3)
913 def on_click(self, event): 1ab
914 __ = event
915 self.frame.Raise()
916 self.frame.Restore()
917 self.frame.Show(True)
919 def CreatePopupMenu(self): 1ab
920 menu = wx.Menu()
921 menu.Append(1, _('Show'))
922 menu.Append(2, _('Hide'))
923 menu.Append(3, _('Close'))
924 return menu
926 def on_task_bar_close(self, event): 1ab
927 __ = event
928 self.frame.Close()
930 def on_task_bar_activate(self, event): 1ab
931 __ = event
932 if not self.frame.IsShown():
933 self.frame.Show()
934 self.frame.Restore()
936 def on_task_bar_deactivate(self, event): 1ab
937 __ = event
938 if self.frame.IsShown():
939 self.frame.Hide()