Coverage for src/robotide/ui/treeplugin.py: 51%
899 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
20from wx import Colour, Point 1ab
21from wx.lib.agw import customtreectrl 1ab
22from wx.lib.agw.aui import GetManager 1ab
23from wx.lib.agw.customtreectrl import GenericTreeItem 1ab
24from wx.lib.mixins import treemixin 1ab
26from ..context import IS_WINDOWS 1ab
27from ..publish.messages import (RideTestRunning, RideTestPaused, RideTestPassed, RideTestFailed, RideTestSkipped, 1ab
28 RideTestExecutionStarted, RideTestStopped, RideImportSetting, RideExcludesChanged,
29 RideIncludesChanged, RideOpenSuite, RideNewProject)
30from ..ui.images import (RUNNING_IMAGE_INDEX, PASSED_IMAGE_INDEX, FAILED_IMAGE_INDEX, PAUSED_IMAGE_INDEX, 1ab
31 SKIPPED_IMAGE_INDEX, ROBOT_IMAGE_INDEX)
32from ..ui.treenodehandlers import TestCaseHandler, TestDataDirectoryHandler, TestCaseFileHandler 1ab
33from ..publish import (PUBLISHER, RideTreeSelection, RideFileNameChanged, RideItem, RideUserKeywordAdded, 1ab
34 RideTestCaseAdded, RideUserKeywordRemoved, RideUserKeywordRenamed, RideTestCaseRemoved,
35 RideDataFileRemoved, RideDataChangedToDirty, RideDataDirtyCleared, RideVariableRemoved,
36 RideVariableAdded, RideVariableMovedUp, RideVariableMovedDown, RideVariableUpdated,
37 RideOpenResource, RideSuiteAdded, RideSelectResource, RideDataFileSet, RideItemNameChanged)
38from ..controller.ctrlcommands import MoveTo 1ab
39from ..pluginapi import Plugin 1ab
40from ..action import ActionInfo 1ab
41from ..widgets import PopupCreator 1ab
42from .. import utils 1ab
43from .treenodehandlers import ResourceRootHandler, action_handler_class, ResourceFileHandler 1ab
44from .images import TreeImageList 1ab
46_ = wx.GetTranslation # To keep linter/code analyser happy 1ab
47builtins.__dict__['_'] = wx.GetTranslation 1ab
50TREETEXTCOLOUR = Colour(0xA9, 0xA9, 0xA9) 1ab
52_TREE_ARGS = {'style': wx.HSCROLL | wx.VSCROLL, 1ab
53 'agwStyle': customtreectrl.TR_DEFAULT_STYLE | customtreectrl.TR_HIDE_ROOT | customtreectrl.TR_EDIT_LABELS}
54_TREE_ARGS['agwStyle'] |= customtreectrl.TR_TOOLTIP_ON_LONG_ITEMS | customtreectrl.TR_HAS_VARIABLE_ROW_HEIGHT 1ab
56if IS_WINDOWS: 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true1ab
57 _TREE_ARGS['style'] |= wx.TR_EDIT_LABELS
60class TreePlugin(Plugin): 1ab
61 __doc__ = _("""Provides a tree view for Test Suites """) 1ab
63 datafile = property(lambda self: self.get_selected_datafile()) 1ab
64 defaults = {"opened": True, 1ab
65 "docked": True,
66 "own colors": False
67 }
69 def __init__(self, application): 1ab
70 Plugin.__init__(self, application, default_settings=self.defaults)
71 # print("DEBUG: treeplugin.py TreePlugin __init__")
72 self._app = application
73 self.settings = self._app.settings.config_obj['Plugins']['Tree']
74 self._parent = None
75 self._tree = self.tree
76 """
77 self._tree.SetBackgroundColour(Colour(200, 222, 40))
78 self._tree.SetOwnBackgroundColour(Colour(200, 222, 40))
79 self._tree.SetForegroundColour(Colour(7, 0, 70))
80 self._tree.SetOwnForegroundColour(Colour(7, 0, 70))
81 """
82 self._mgr = GetManager(self._tree)
83 """
84 self._action_registerer = action_registerer
85 self.tree = parent.tree
86 """
87 self.pane_id = self._tree.GetId()
88 self._model = self.model
89 # DEBUG: self.frame.Bind(wx.EVT_MOVE, self.OnShowTree) self.frame.Bind(wx.EVT_SHOW, self.OnShowTree)
90 self._tree.Bind(wx.EVT_SHOW, self.on_show_tree)
91 self._tree.Bind(wx.EVT_MOVE, self.on_tab_changed)
92 # parent, action_registerer, , default_settings={'collapsed':True}
93 self._pane = self._mgr.GetPane(self._tree)
94 self.font = wx.Font(self._app.fontinfo) # self._tree.GetFont()
95 self._tree.SetFont(self.font)
96 # print(f"DEBUG: TreePlugin init self.pane_id={self.pane_id} \n"
97 # f"self._pane = {self._pane}")
99 def register_frame(self, parent=None): 1ab
100 if parent: 100 ↛ exitline 100 didn't return from function 'register_frame' because the condition on line 100 was always true
101 self._parent = parent
102 if self._mgr.GetPane("tree_content") in self._mgr._panes: 102 ↛ 105line 102 didn't jump to line 105 because the condition on line 102 was always true
103 register = self._mgr.InsertPane
104 else:
105 register = self._mgr.AddPane
106 register(self._tree, wx.lib.agw.aui.AuiPaneInfo().Name("tree_content").
107 Caption(_("Test Suites")).LeftDockable(True))
108 self._mgr.Update()
110 def enable(self): 1ab
111 # DEBUG this does not work, the panel has no tree, when we dock from floating
112 # DEBUG: Attempt to create Panel and then calling self.OnShowTree
113 """
114 self.register_action(ActionInfo('View','View Test Suites Explorer', self.OnShowTree,
115 shortcut='F12',
116 doc='Show Test Suites tree panel',
117 position=1))
118 """
119 self.subscribe(self.on_tree_selection, RideTreeSelection)
120 # self.save_setting('opened', True)
121 # DEBUG: Add toggle checkbox to menu View/Hide Tree
122 if self.opened: 122 ↛ exitline 122 didn't return from function 'enable' because the condition on line 122 was always true
123 self.on_show_tree(None)
125 def close_tree(self): 1ab
126 self._mgr.DetachPane(self._tree)
127 self._tree.Hide()
128 self._mgr.Update()
129 self.save_setting('opened', False)
131 def disable(self): 1ab
132 self.close_tree()
133 # self.save_setting('opened', False)
134 self.unsubscribe_all()
135 self.unregister_actions()
137 def is_focused(self): 1ab
138 return self._tree.HasFocus()
140 def populate(self, model): 1ab
141 if model: # DEBUG: Always populate ... and model != self._model: 141 ↛ 144line 141 didn't jump to line 144 because the condition on line 141 was always true
142 self._model = model
143 # print(f"DEBUG: Populating model... {self._model}\n\n")
144 self._tree.populate(self._model)
146 def set_editor(self, editor): 1ab
147 self._tree.set_editor(editor)
149 def on_show_tree(self, event): 1ab
150 __ = event
151 if not self._parent:
152 self._parent = self.frame
153 if not self._tree: # This is not needed because tree is always created 153 ↛ 154line 153 didn't jump to line 154 because the condition on line 153 was never true
154 return # On Windows this code is executed when closing the app, we return now
156 global_settings = self._app.settings.config_obj['General']
157 apply_global = global_settings['apply to panels']
158 use_own = self.settings['own colors']
159 if apply_global or not use_own: 159 ↛ 163line 159 didn't jump to line 163 because the condition on line 159 was always true
160 html_background = self.settings.get('background help', (240, 242, 80))
161 html_foreground = self.settings.get('foreground text', (7, 0, 70))
162 else:
163 html_background = self.settings.get('background', (240, 242, 80))
164 html_foreground = self.settings.get('foreground', (7, 0, 70))
165 html_font_face = self.settings.get('font face', '')
166 html_font_size = self.settings.get('font size', 11)
167 self._tree.Show(True)
168 # print(f"DEBUG: treeplugin on_show_tree {html_font_face=} {html_font_size=}")
169 self._tree.SetMinSize(wx.Size(200, 225))
170 # self.aui_mgr.DetachPane(self._tree)
171 # self.aui_mgr.Update()
172 # DEBUG: Let's use own method
173 # self.aui_mgr.AddPane(self._tree,
174 # wx.lib.agw.aui.AuiPaneInfo().Name("tree_content").
175 # Caption("Test Suites").LeftDockable(True).
176 # CloseButton(True))
177 # self.register_frame(self._parent)
178 self._tree.SetBackgroundStyle(wx.BG_STYLE_SYSTEM)
179 self._tree.SetBackgroundColour(html_background)
180 self._tree.SetForegroundColour(html_foreground)
181 # self.font = self._tree.GetFont()
182 self._tree.SetFont(self.font)
183 if html_font_face is not None: 183 ↛ 187line 183 didn't jump to line 187 because the condition on line 183 was always true
184 self.font.SetFaceName(html_font_face)
185 self.font.SetPointSize(html_font_size)
186 self._tree.SetFont(self.font)
187 self._tree.Refresh()
188 self._tree.Raise()
189 self.save_setting('opened', True)
190 self._tree.populate(self._model)
191 self._update_tree()
192 self._mgr.Update()
194 def on_tree_selection(self, message): 1ab
195 if self.is_focused():
196 self._tree.tree_node_selected(message.item)
198 def on_tab_changed(self, event): 1ab
199 __ = event
200 self._update_tree()
202 def _update_tree(self, event=None): 1ab
203 __ = event
204 self._tree.populate(self._model)
205 self._tree.refresh_view()
206 self._tree.Update()
209class Tree(treemixin.DragAndDrop, customtreectrl.CustomTreeCtrl, wx.Panel): 1ab
211 def __init__(self, parent, action_registerer, settings=None): 1ab
212 from ..controller.ui.treecontroller import TreeController
213 self._RESOURCES_NODE_LABEL = _('External Resources')
214 # print(f"DEBUG: treeplugin.py Tree after importing TreeController __init__ "
215 # f"translated label={self._RESOURCES_NODE_LABEL}")
216 self.theme = settings.get_without_default('General')
217 self.background = self.theme['background']
218 self.foreground = self.theme['foreground']
219 self._checkboxes_for_tests = False
220 self._test_selection_controller = self._create_test_selection_controller()
221 self.controller = TreeController(self, action_registerer, settings=settings,
222 test_selection=self._test_selection_controller)
223 treemixin.DragAndDrop.__init__(self, parent, **_TREE_ARGS)
224 self.controller.register_tree_actions()
225 self._bind_tree_events()
226 self._images = TreeImageList()
227 self._animctrl = None
228 self._silent_mode = False
229 self.SetImageList(self._images)
230 self.label_editor = TreeLabelEditListener(self, action_registerer)
231 self.controller.bind_keys()
232 self._subscribe_to_messages()
233 self._popup_creator = PopupCreator()
234 self._dragging = False
235 self._clear_tree_data()
236 self._editor = None
237 self._execution_results = None
238 self._resources = []
239 self._right_click = False
240 self.dirty = False
241 # DEBUG: This menu is not working because is being attached to main frame
242 # self._menu = wx.Menu()
243 # self._menu.Append(wx.ID_CLOSE, item="&Close", helpString="Closes panel")
244 # self._mb = wx.MenuBar()
245 # self._mb.Append(self._menu, "Menu")
246 # self.GetParent().SetMenuBar(self._mb)
247 # print(f"DEBUG: Tree tried to add menu to {self.GetParent().__repr__()} parent={parent}")
248 self.pane_id = self.GetId()
249 if not hasattr(self, 'OnCancelEdit'): 249 ↛ 250line 249 didn't jump to line 250 because the condition on line 249 was never true
250 self.OnCancelEdit = self._on_cancel_edit
252 @staticmethod 1ab
253 def _create_test_selection_controller(): 1ab
254 from ..controller.ui.treecontroller import TestSelectionController
256 tsc = TestSelectionController()
257 PUBLISHER.subscribe(tsc.clear_all, RideOpenSuite)
258 PUBLISHER.subscribe(tsc.clear_all, RideNewProject)
259 return tsc
261 def _on_cancel_edit(self, item): 1ab
262 le = customtreectrl.TreeEvent(
263 customtreectrl.wxEVT_TREE_END_LABEL_EDIT, self.GetId())
264 le._item = item
265 le.SetEventObject(self)
266 le._label = ""
267 le._editCancelled = True
268 self.GetEventHandler().ProcessEvent(le)
270 def _bind_tree_events(self): 1ab
271 self.Bind(wx.EVT_LEFT_DCLICK, self.on_double_click)
272 self.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_sel_changed)
273 self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.on_tree_item_expanding)
274 self.Bind(wx.EVT_RIGHT_DOWN, self.on_right_click)
275 self.Bind(wx.EVT_TREE_SEL_CHANGING, self.on_selection)
276 # self.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.OnRightClick)
277 self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.on_item_activated)
278 self.Bind(customtreectrl.EVT_TREE_ITEM_CHECKED, self.on_tree_item_checked)
279 self.Bind(wx.EVT_TREE_ITEM_COLLAPSING, self.on_tree_item_collapsing)
280 self.Bind(wx.EVT_CLOSE, self.on_close)
282 def on_selection(self, event): 1ab
283 if self._right_click: 283 ↛ 284line 283 didn't jump to line 284 because the condition on line 283 was never true1ahijklmgnopqrcdus
284 event.Skip()
286 def on_double_click(self, event): 1ab
287 item, _ = self.HitTest(self.ScreenToClient(wx.GetMousePosition()))
288 if item:
289 handler = self.controller.get_handler(item)
290 handler.double_clicked()
291 event.Skip()
293 def set_editor(self, editor): 1ab
294 self._editor = editor
296 def StartDragging(self): # Overrides wx method 1ab
297 self._dragging = True
298 treemixin.DragAndDrop.StartDragging(self)
300 def OnEndDrag(self, event): # Overrides wx method 1ab
301 self._dragging = False
302 treemixin.DragAndDrop.OnEndDrag(self, event)
304 def register_context_menu_hook(self, callable_m): 1ab
305 self._popup_creator.add_hook(callable_m)
307 def unregister_context_menu_hook(self, callable_m): 1ab
308 self._popup_creator.remove_hook(callable_m)
310 def _subscribe_to_messages(self): 1ab
311 subscriptions = [
312 (self._item_renamed, RideItemNameChanged),
313 (self._item_changed, RideItem),
314 (self._resource_added, RideOpenResource),
315 (self._select_resource, RideSelectResource),
316 (self._suite_added, RideSuiteAdded),
317 (self._keyword_added, RideUserKeywordAdded),
318 (self._test_added, RideTestCaseAdded),
319 (self._variable_added, RideVariableAdded),
320 (self._leaf_item_removed, RideUserKeywordRemoved),
321 (self._leaf_item_removed, RideTestCaseRemoved),
322 (self._leaf_item_removed, RideVariableRemoved),
323 (self._datafile_removed, RideDataFileRemoved),
324 (self._datafile_set, RideDataFileSet),
325 (self._data_dirty, RideDataChangedToDirty),
326 (self._data_undirty, RideDataDirtyCleared),
327 (self._variable_moved_up, RideVariableMovedUp),
328 (self._variable_moved_down, RideVariableMovedDown),
329 (self._variable_updated, RideVariableUpdated),
330 (self._filename_changed, RideFileNameChanged),
331 (self._testing_started, RideTestExecutionStarted),
332 (self._test_result, RideTestRunning),
333 (self._test_result, RideTestPaused),
334 (self._test_result, RideTestPassed),
335 (self._test_result, RideTestFailed),
336 (self._test_result, RideTestSkipped),
337 (self._test_result, RideTestStopped),
338 (self._handle_import_setting_message, RideImportSetting),
339 (self._mark_excludes, RideExcludesChanged),
340 (self._mark_excludes, RideIncludesChanged),
341 ]
342 for listener, topic in subscriptions:
343 PUBLISHER.subscribe(listener, topic)
345 def _mark_excludes(self, message): 1ab
346 tree = self.controller.find_node_by_controller(message.old_controller)
347 self._render_datafile(self.GetItemParent(tree), message.new_controller)
348 self._remove_datafile_node(tree)
350 def _set_item_excluded(self, node): 1ab
351 self.SetItemTextColour(node, wx.TheColourDatabase.Find(colourName="GREY"))
352 self.SetItemItalic(node, True)
353 self.SetItemText(node, _("%s (excluded)") % self.GetItemText(node))
355 def _handle_import_setting_message(self, message): 1ab
356 if message.is_resource():
357 self._set_resource_color(
358 message.import_controller.get_imported_controller())
359 self._set_resource_color(
360 message.import_controller.get_previous_imported_controller())
362 def _set_resource_color(self, resource_controller): 1ab
363 if not resource_controller:
364 return
365 node = self.controller.find_node_by_controller(resource_controller)
366 if node:
367 self.SetItemTextColour(
368 node, self._get_resource_text_color(resource_controller))
370 def _get_resource_text_color(self, resource_controller): 1ab
371 if resource_controller.is_used():
372 return self.GetDefaultAttributes().colFg
373 else:
374 return wx.LIGHT_GREY
376 def _testing_started(self, message): 1ab
377 self._for_all_drawn_tests(
378 self.root, lambda t: self.SetItemImage(t, ROBOT_IMAGE_INDEX))
379 self._execution_results = message.results
380 self._images.set_execution_results(message.results)
382 def _test_result(self, message): 1ab
383 from ..controller.macrocontrollers import TestCaseController
385 test: TestCaseController = message.item
386 if not test:
387 # test object will be None when running with DataDriver
388 # when runner is interrupted, is also None, so let's stop animation
389 if self._animctrl:
390 self._animctrl.Stop()
391 self._animctrl.Animation.Destroy()
392 self._animctrl.Destroy()
393 self._animctrl = None
394 return
395 if isinstance(message, RideTestPassed):
396 test.run_passed = True
397 elif isinstance(message, RideTestFailed):
398 test.run_passed = False
399 elif isinstance(message, RideTestSkipped):
400 test.run_passed = False
401 else:
402 test.run_passed = None
403 wx.CallAfter(self._set_icon_from_execution_results, message.item)
405 def _set_icon_from_execution_results(self, controller): 1ab
406 node = self.controller.find_node_by_controller(controller)
407 if not node:
408 return
409 img_index = self._get_icon_index_for(controller)
410 # Always set the static icon
411 self.SetItemImage(node, img_index)
412 if self._animctrl:
413 self._animctrl.Stop()
414 self._animctrl.Animation.Destroy()
415 self._animctrl.Destroy()
416 self._animctrl = None
417 self.DeleteItemWindow(node)
418 if img_index in (RUNNING_IMAGE_INDEX, PAUSED_IMAGE_INDEX):
419 from wx.adv import Animation, AnimationCtrl
420 import os
421 _BASE = os.path.join(os.path.dirname(__file__), '..', 'widgets')
422 if img_index == RUNNING_IMAGE_INDEX:
423 img = os.path.join(_BASE, 'robot-running.gif')
424 else:
425 img = os.path.join(_BASE, 'robot-pause.gif')
426 ani = Animation(img)
427 obj = self
428 rect = Point(node.GetX()+20, node.GetY()) # Overlaps robot icon
429 try:
430 self._animctrl = AnimationCtrl(obj, -1, ani, rect)
431 self._animctrl.SetBackgroundColour('white')
432 self.SetItemWindow(node, self._animctrl, False)
433 self._animctrl.Play()
434 except AttributeError: # In fast executions the element self._animctrl.Play() does not exists
435 pass
436 # Make visible the running or paused test
437 parent = node.GetParent()
438 self.EnsureVisible(parent) # DEBUG add animation to parent if suite setup/teardown started
439 self.ExpandAllChildren(parent)
440 self.EnsureVisible(node)
441 self.ExpandAllChildren(node)
442 self.Update()
444 def _get_icon_index_for(self, controller): 1ab
445 if not self._execution_results:
446 return ROBOT_IMAGE_INDEX
447 if self._execution_results.is_paused(controller):
448 return PAUSED_IMAGE_INDEX
449 if self._execution_results.is_running(controller):
450 return RUNNING_IMAGE_INDEX
451 if self._execution_results.has_passed(controller):
452 return PASSED_IMAGE_INDEX
453 if self._execution_results.has_failed(controller):
454 return FAILED_IMAGE_INDEX
455 if self._execution_results.has_skipped(controller):
456 return SKIPPED_IMAGE_INDEX
457 return ROBOT_IMAGE_INDEX
459 def populate(self, model): 1ab
460 self._clear_tree_data()
461 self._populate_model(model)
462 self.refresh_view()
463 self.SetFocus() # Needed for keyboard shortcuts
465 def _clear_tree_data(self): 1ab
466 self.DeleteAllItems()
467 self.root = self.AddRoot('')
468 self._resource_root = self._create_resource_root()
469 self.datafile_nodes = []
470 self._resources = []
471 self.controller.clear_history()
473 def _create_resource_root(self): 1ab
474 return self._create_node(self.root, self._RESOURCES_NODE_LABEL,
475 self._images.directory)
477 def _populate_model(self, model): 1ab
478 handler = ResourceRootHandler(model, self, self._resource_root,
479 self.controller.settings)
480 # print(f"DEBUG: _populate_model model={model} self._resource_root={self._resource_root}"
481 # f" self._controller.settings={self._controller.settings}")
482 self.SetPyData(self._resource_root, handler)
483 if model.data:
484 self._render_datafile(self.root, model.data, 0)
485 for res in model.external_resources:
486 if not res.parent: 486 ↛ 485line 486 didn't jump to line 485 because the condition on line 486 was always true
487 self._render_datafile(self._resource_root, res)
489 def _resource_added(self, message): 1ab
490 ctrl = message.datafile
491 if self.controller.find_node_by_controller(ctrl):
492 return
493 if ctrl.parent:
494 parent = self._get_dir_node(ctrl.parent)
495 else:
496 parent = self._resource_root
497 self._render_datafile(parent, ctrl)
499 def _get_dir_node(self, ctrl): 1ab
500 if ctrl is None:
501 return self.root
502 dir_node = self._get_datafile_node(ctrl.data)
503 if dir_node is None: 503 ↛ 507line 503 didn't jump to line 507 because the condition on line 503 was always true
504 parent = self._get_dir_node(ctrl.parent)
505 self._render_datafile(parent, ctrl)
506 dir_node = self._get_datafile_node(ctrl.data)
507 return dir_node
509 def _select_resource(self, message): 1ab
510 self.select_controller_node(message.item)
512 def select_controller_node(self, controller): 1ab
513 self.SelectItem(self.controller.find_node_by_controller(controller))
515 def _suite_added(self, message): 1ab
516 self.add_datafile(message.parent, message.suite)
518 def refresh_view(self): 1ab
519 self.Show()
520 self.Refresh()
521 # print(f"DEBUG: Called Tree._refresh_view {self.GetParent().GetClassName()}")
522 if self._resource_root: 522 ↛ 524line 522 didn't jump to line 524 because the condition on line 522 was always true
523 self.Expand(self._resource_root)
524 if self.datafile_nodes:
525 self._expand_and_render_children(self.datafile_nodes[0])
526 wx.CallAfter(self.SelectItem, self.datafile_nodes[0])
527 self.Update()
528 # print(f"DEBUG: Called Tree._refresh_view parent={self.GetParent().GetClassName()} self={self}")
530 def _render_datafile(self, parent_node, controller, index=None): 1ab
531 node = self._create_node_with_handler(parent_node, controller, index) 1aef
532 if not node: 532 ↛ 533line 532 didn't jump to line 533 because the condition on line 532 was never true1aef
533 return None
534 if controller.dirty: 1aef
535 self.controller.mark_node_dirty(node) 1ef
536 self.datafile_nodes.append(node) 1aef
537 self.SetItemHasChildren(node, True) 1aef
539 for child in controller.children: 1aef
540 self._render_datafile(node, child)
541 return node 1aef
543 @staticmethod 1ab
544 def _normalize(path): 1ab
545 return os.path.normcase(os.path.normpath(os.path.abspath(path)))
547 def _create_node_with_handler(self, parent_node, controller, index=None): 1ab
548 from ..controller.filecontrollers import ResourceFileController 1acdef
549 if (controller.display_name.startswith("#") or 549 ↛ 551line 549 didn't jump to line 551 because the condition on line 549 was never true1acdef
550 controller.display_name.startswith("...")): # If it is a comment or continuation marker don't create
551 return None
553 handler_class = action_handler_class(controller) 1acdef
554 with_checkbox = (handler_class == TestCaseHandler and self._checkboxes_for_tests) 1acdef
555 node = self._create_node(parent_node, controller.display_name, self._images[controller], 1acdef
556 index, with_checkbox=with_checkbox)
557 if isinstance(controller, ResourceFileController) and not controller.is_used(): 1acdef
558 self.SetItemTextColour(node, TREETEXTCOLOUR) # wxPython3 hack 1ae
559 action_handler = handler_class(controller, self, node, self.controller.settings) 1acdef
560 self.SetPyData(node, action_handler) 1acdef
562 # if we have a TestCase node we have to make sure that
563 # we retain the checked state
564 if (handler_class == TestCaseHandler and self._checkboxes_for_tests) \ 564 ↛ 566line 564 didn't jump to line 566 because the condition on line 564 was never true1acdef
565 and self._test_selection_controller.is_test_selected(controller):
566 self.CheckItem(node, True)
567 if controller.is_excluded(): 567 ↛ 568line 567 didn't jump to line 568 because the condition on line 567 was never true1acdef
568 self._set_item_excluded(node)
569 return node 1acdef
571 def set_checkboxes_for_tests(self): 1ab
572 self._checkboxes_for_tests = True
574 def _expand_and_render_children(self, node): 1ab
575 assert node is not None 1acdef
576 self._render_children(node) 1acdef
577 self.Expand(node) 1acdef
579 def _render_children(self, node): 1ab
580 handler = self.controller.get_handler(node) 1acdef
581 if not handler or not handler.can_be_rendered: 1acdef
582 return 1acdef
583 self._create_child_nodes( 1aef
584 node, handler, lambda item: item.is_test_suite)
585 handler.set_rendered() 1aef
587 def _create_child_nodes(self, node, handler, predicate): 1ab
588 for childitem in self._children_of(handler): 1aef
589 index = self._get_insertion_index(node, predicate) 1aef
590 self._create_node_with_handler(node, childitem, index) 1aef
592 @staticmethod 1ab
593 def _children_of(handler): 1ab
594 return [v for v in handler.variables if v.has_data()] + list(handler.tests) + list(handler.keywords) 1aef
596 def _create_node(self, parent_node, label, img, index=None, with_checkbox=False): 1ab
597 node = self._wx_node(parent_node, index, label, with_checkbox) 1acdef
598 self.SetItemImage(node, img.normal, wx.TreeItemIcon_Normal) 1acdef
599 self.SetItemImage(node, img.expanded, wx.TreeItemIcon_Expanded) 1acdef
600 return node 1acdef
602 def _wx_node(self, parent_node, index, label, with_checkbox): 1ab
603 ct_type = 1 if with_checkbox else 0 1acdef
604 if index is not None: 1acdef
605 # blame wxPython for this ugliness
606 if isinstance(index, int): 1acdef
607 return self.InsertItemByIndex( 1ae
608 parent_node, index, label, ct_type=ct_type)
609 else:
610 return self.InsertItem( 1acdf
611 parent_node, index, label, ct_type=ct_type)
612 return self.AppendItem(parent_node, label, ct_type=ct_type) 1aef
614 def add_datafile(self, parent, suite): 1ab
615 snode = self._render_datafile(
616 self._get_datafile_node(parent.data), suite)
617 self.SelectItem(snode)
619 def add_test(self, parent_node, test): 1ab
620 self._add_dataitem( 1DvEFGHc
621 parent_node, test, lambda item: item.is_user_keyword)
623 def add_keyword(self, parent_node, kw): 1ab
624 self._add_dataitem(parent_node, kw, lambda item: item.is_test_suite) 1xtyzIJKABCd
626 def _add_dataitem(self, parent_node, dataitem, predicate): 1ab
627 node = self._get_or_create_node(parent_node, dataitem, predicate) 1DvEFGHxtyzIJKABCcd
628 self._select(node) 1cd
629 self.controller.mark_node_dirty(parent_node) 1cd
631 def _get_or_create_node(self, parent_node, dataitem, predicate): 1ab
632 if not self.IsExpanded(parent_node): 632 ↛ 633line 632 didn't jump to line 633 because the condition on line 632 was never true1DvEFGHxtyzIJKABCcd
633 self._expand_and_render_children(parent_node)
634 return self.controller.find_node_with_label(
635 parent_node, dataitem.display_name)
637 index = self._get_insertion_index(parent_node, predicate) 1cd
638 return self._create_node_with_handler(parent_node, dataitem, index) 1cd
640 def _select(self, node): 1ab
641 if node:
642 wx.CallAfter(self.SelectItem, node)
644 def _get_insertion_index(self, parent_node, predicate): 1ab
645 if not predicate: 645 ↛ 646line 645 didn't jump to line 646 because the condition on line 645 was never true1acdef
646 return None
647 item, cookie = self.GetFirstChild(parent_node) 1acdef
648 while item: 1acdef
649 if predicate(self.controller.get_handler(item)): 1acdf
650 index = self.GetPrevSibling(item) 1acd
651 if not index: 1acd
652 index = 0
653 return index 1acd
654 item, cookie = self.GetNextChild(parent_node, cookie) 1acdf
655 return None 1aef
657 def _keyword_added(self, message): 1ab
658 self.add_keyword(self._get_datafile_node(self.get_selected_datafile()), 1xtyzIJKABCd
659 message.item)
661 def _variable_added(self, message): 1ab
662 self._get_or_create_node(
663 self._get_datafile_node(self.get_selected_datafile()),
664 message.item,
665 lambda item: not item.is_variable or item.index > message.index)
667 def _leaf_item_removed(self, message): 1ab
668 node = self.controller.find_node_by_controller(message.item)
669 parent_node = self._get_datafile_node(message.datafile)
670 # DEBUG The below call causes not calling delete_node
671 # self._test_selection_controller.select(message.item, False)
672 self.controller.mark_node_dirty(parent_node)
673 self.delete_node(node)
675 def _test_added(self, message): 1ab
676 self.add_test(self._get_datafile_node(self.get_selected_datafile()), 1DvEFGHc
677 message.item)
679 def _datafile_removed(self, message): 1ab
680 dfnode = self._get_datafile_node(message.datafile.data)
681 self.datafile_nodes.remove(dfnode)
682 self.DeleteChildren(dfnode)
683 self.Delete(dfnode)
685 def _datafile_set(self, message): 1ab
686 wx.CallAfter(self._refresh_datafile_when_file_set, message.item) 1aDvEFGHxtyzIJKABC
688 def _filename_changed(self, message): 1ab
689 from ..controller import ResourceFileController
690 df = message.datafile
691 node = self.controller.find_node_by_controller(df)
692 if not node:
693 raise AssertionError('No node found with controller "%s"' % df)
694 old_name = node.GetText()
695 # print(f"DEBUG: treeplugin.py Tree _filename_changed message={message} df={df}"
696 # f" node={node} {old_name=} df.display_name={df.display_name}")
697 wx.CallAfter(self.SetItemText, node, df.display_name)
698 if isinstance(df, ResourceFileController):
699 RideItemNameChanged(item=node, old_name=old_name, new_name=df.display_name).publish()
701 def add_keyword_controller(self, controller): 1ab
702 parent = self._get_datafile_node(self.get_selected_datafile())
703 self.add_keyword(parent, controller)
705 def delete_node(self, node): 1ab
706 if node is None:
707 return
708 parent = self.GetItemParent(node)
709 self.controller.mark_node_dirty(parent)
710 self.Delete(node)
711 wx.CallAfter(self.SelectItem, parent)
713 def _data_dirty(self, message): 1ab
714 # print("DEBUG: TreePlugin tree _data_dirty ENTER")
715 if not self.dirty: 1DvEFGHxtyzIJKABCcdef
716 self.dirty = True 1Dcdef
717 self.controller.mark_controller_dirty(message.datafile) 1Dcdef
719 def _data_undirty(self, message): 1ab
720 _ = message
721 # print("DEBUG: TreePlugin tree _data_undirty")
722 self.unset_dirty()
724 def unset_dirty(self): 1ab
725 for node in self.datafile_nodes:
726 text = self.GetItemText(node)
727 handler = self.controller.get_handler(node)
728 if text.startswith('*') and not handler.controller.dirty:
729 self.SetItemText(node, text[1:])
730 self.dirty = False
732 def select_node_by_data(self, controller): 1ab
733 """Find and select the tree item associated with the given controller.
735 Controller can be any of the controllers that are represented in the
736 tree."""
737 parent_node = self._get_datafile_node(controller.datafile) 1awcd
738 # print(f"DEBUG: treeplugin.py Tree select_node_by_data parent_node={parent_node}")
739 if not parent_node: 1awcd
740 return None 1w
741 # if not self.IsExpanded(parent_node):
742 self.EnsureVisible(parent_node) 1acd
743 self._expand_and_render_children(parent_node) 1acd
744 node = self.controller.find_node_by_controller(controller) 1acd
745 if node and node != self.GetSelection(): 745 ↛ 747line 745 didn't jump to line 747 because the condition on line 745 was always true1acd
746 self.SelectItem(node) 1acd
747 return node 1acd
749 def select_user_keyword_node(self, uk): 1ab
750 parent_node = self._get_datafile_node(uk.parent.parent) 1s
751 if not parent_node: 751 ↛ 752line 751 didn't jump to line 752 because the condition on line 751 was never true1s
752 return
753 self.EnsureVisible(parent_node) 1s
754 if not self.IsExpanded(parent_node): 754 ↛ 755line 754 didn't jump to line 755 because the condition on line 754 was never true1s
755 self._expand_and_render_children(parent_node)
756 node = self.controller.find_node_with_label(parent_node, utils.normalize(uk.name)) 1s
757 # print(f"DEBUG: treeplugin.py Tree select_user_keyword_node node= {node}")
758 if node != self.GetSelection(): 758 ↛ exitline 758 didn't return from function 'select_user_keyword_node' because the condition on line 758 was always true1s
759 self.SelectItem(node) 1s
761 def _get_datafile_node(self, datafile): 1ab
762 # print(f"DEBUG: treeplugin.py Tree _get_datafile_node ENTER dataffile= {datafile}")
763 for node in self.datafile_nodes: 1awDvEFGHxtyzIJKABCcds
764 item = self.controller.get_handler(node).item 1awcds
765 if item == datafile: # This only works before editing a resource item because the obj id changes 1awcds
766 return node 1acds
767 if type(item) == type(datafile) and hasattr(item, 'name') and item.name == datafile.name: 1awcs
768 return node
769 return None 1awDvEFGHxtyzIJKABC
771 def get_selected_datafile(self): 1ab
772 """Returns currently selected data file.
774 If a test or user keyword node is selected, returns parent of that
775 item."""
776 datafile = self._get_selected_datafile_node() 1awDvEFGHxtyzIJKABCcdu
777 if not datafile: 1awDvEFGHxtyzIJKABCcdu
778 return None 1awDvEFGHxtyzIJKABC
779 return self.controller.get_handler(datafile).item 1cdu
781 def get_selected_datafile_controller(self): 1ab
782 """Returns controller associated with currently active data file.
784 If a test or user keyword node is selected, returns parent of that
785 item."""
786 dfnode = self._get_selected_datafile_node() 1ahijklmMNwOPQRSTUVgnopqrLvt
788 if dfnode: 788 ↛ 789line 788 didn't jump to line 789 because the condition on line 788 was never true1ahijklmMNwOPQRSTUVgnopqrLvt
789 return self.controller.get_handler(dfnode).controller
790 else:
791 return None 1ahijklmMNwOPQRSTUVgnopqrLvt
793 def _get_selected_datafile_node(self): 1ab
794 node = self.GetSelection() 1ahijklmMNwOPQRSTUVgnopqrLDvEFGHxtyzIJKABCcdu
795 if not node or node in (self._resource_root, self.root): 1ahijklmMNwOPQRSTUVgnopqrLDvEFGHxtyzIJKABCcdu
796 return None 1ahijklmMNwOPQRSTUVgnopqrLDvEFGHxtyzIJKABC
797 while node not in self.datafile_nodes: 1cdu
798 node = self.GetItemParent(node) 1u
799 return node 1cdu
801 def get_selected_item(self): 1ab
802 """Returns model object associated with currently selected tree node.
803 """
804 selection = self.GetSelection() 1awgWX
805 if not selection: 805 ↛ 806line 805 didn't jump to line 806 because the condition on line 805 was never true1awgWX
806 return None
807 handler = self.controller.get_handler(selection) 1awgWX
808 return handler and handler.controller or None 1awgWX
810 def tree_node_selected(self, node): 1ab
811 # print(f"DEBUG: TreePlugin tree node selected {str(node)}")
812 pass
814 def move_up(self, node): 1ab
815 prev = self.GetPrevSibling(node)
816 if prev.IsOk():
817 self._switch_items(prev, node, node)
819 def move_down(self, node): 1ab
820 _next = self.GetNextSibling(node)
821 if _next.IsOk():
822 self._switch_items(node, _next, node)
824 def _switch_items(self, first, second, currently_selected): 1ab
825 """Changes the order of given items, first is expected to be directly
826 above the second"""
827 selection = self.GetItemData(currently_selected).controller
828 controller = self.controller.get_handler(first).controller
829 self.Delete(first)
830 self._create_node_with_handler(self.GetItemParent(second), controller, second)
831 self.select_node_by_data(selection)
833 def _refresh_datafile_when_file_set(self, controller): 1ab
834 # remove invalid cases selection when data file is changed in text editor
835 self._test_selection_controller.remove_invalid_cases_selection(controller) 1hijklmgnopqrLvt
836 # Prevent tab selections based on tree item selected events
837 self._start_silent_mode() 1hijklmgnopqrLvt
838 current = self.get_selected_datafile_controller() 1hijklmgnopqrLvt
839 if not current: # If tree is not yet in use - do not expand anything. 839 ↛ 843line 839 didn't jump to line 843 because the condition on line 839 was always true1hijklmgnopqrLvt
840 self._end_silent_mode() 1hijklmgnopqrLvt
841 return 1hijklmgnopqrLvt
843 item = self.GetSelection()
844 current_txt = self.GetItemText(item) if item.IsOk() else ''
845 # after refresh current and current_txt might have been changed
846 node = self._refresh_datafile(controller)
847 if node is None:
848 # DEBUG: Find out why this sometimes happens
849 return
850 self._expand_and_render_children(node)
851 if current == controller:
852 select_item = self.controller.find_node_with_label(
853 node, current_txt)
854 if select_item is None:
855 select_item = node
856 wx.CallAfter(self.SelectItem, select_item)
857 wx.CallAfter(self._end_silent_mode)
858 else:
859 self._end_silent_mode()
861 def _uncheck_tests(self, controller): 1ab
862 self._test_selection_controller.unselect_all(controller.tests)
864 def _start_silent_mode(self): 1ab
865 self._silent_mode = True 1hijklmgnopqrLvt
867 def _end_silent_mode(self): 1ab
868 self._silent_mode = False 1hijklmgnopqrLvt
870 def refresh_datafile(self, controller, event): 1ab
871 to_be_selected = self._get_pending_selection(event) 1ef
872 new_node = self._refresh_datafile(controller) 1ef
873 self._handle_pending_selection(to_be_selected, new_node) 1ef
875 def _refresh_datafile(self, controller): 1ab
876 orig_node = self._get_data_controller_node(controller) 1ef
877 if orig_node is not None: 877 ↛ exitline 877 didn't return from function '_refresh_datafile' because the condition on line 877 was always true1ef
878 insertion_index = self._get_datafile_index(orig_node) 1ef
879 parent = self.GetItemParent(orig_node) 1ef
880 self._remove_datafile_node(orig_node) 1ef
881 return self._render_datafile(parent, controller, insertion_index) 1ef
883 def _get_pending_selection(self, event): 1ab
884 if hasattr(event, 'Item'): 884 ↛ 885line 884 didn't jump to line 885 because the condition on line 884 was never true1ef
885 item = event.GetItem()
886 event.Veto()
887 elif hasattr(event, 'Position'): 887 ↛ 888line 887 didn't jump to line 888 because the condition on line 887 was never true1ef
888 item, flags = self.HitTest(event.Position)
889 if not self._click_on_item(item, flags):
890 return
891 else:
892 return 1ef
893 return self.GetItemText(item)
895 def _get_data_controller_node(self, controller): 1ab
896 for node in self.datafile_nodes: 896 ↛ 899line 896 didn't jump to line 899 because the loop on line 896 didn't complete1ef
897 if self.GetItemData(node).controller == controller: 1ef
898 return node 1ef
899 return None
901 @staticmethod 1ab
902 def _click_on_item(item, flags): 1ab
903 return item is not None and item.IsOk() and flags & wx.TREE_HITTEST_ONITEM
905 def _get_datafile_index(self, node): 1ab
906 insertion_index = self.GetPrevSibling(node) 1ef
907 if not insertion_index: 1ef
908 insertion_index = 0 1e
909 return insertion_index 1ef
911 def _remove_datafile_node(self, node): 1ab
912 for child in self.GetItemChildren(node): 1ef
913 if child in self.datafile_nodes: 913 ↛ 914line 913 didn't jump to line 914 because the condition on line 913 was never true1ef
914 self._remove_datafile_node(child)
915 self.datafile_nodes.remove(node) 1ef
916 self.Delete(node) 1ef
918 def _handle_pending_selection(self, to_be_selected, parent_node): 1ab
919 if to_be_selected: 919 ↛ 920line 919 didn't jump to line 920 because the condition on line 919 was never true1ef
920 self._expand_and_render_children(parent_node)
921 select_item = self.controller.find_node_with_label(
922 parent_node, to_be_selected)
923 wx.CallAfter(self.SelectItem, select_item)
925 def on_sel_changed(self, event): 1ab
926 node = event.GetItem() 1ahijklmgnopqrcdus
927 if not node.IsOk() or self._dragging: 927 ↛ 928line 927 didn't jump to line 928 because the condition on line 927 was never true1ahijklmgnopqrcdus
928 event.Skip()
929 return
930 self.controller.add_to_history(node) 1ahijklmgnopqrcdus
931 handler = self.controller.get_handler(node) 1ahijklmgnopqrcdus
932 if handler and handler.item: 932 ↛ 938line 932 didn't jump to line 938 because the condition on line 932 was always true1ahijklmgnopqrcdus
933 self._update_data_file_namespace(node) 1ahijklmgnopqrcdus
934 RideTreeSelection( 1ahijklmgnopqrcdus
935 node=node,
936 item=handler.controller,
937 silent=self._silent_mode).publish()
938 self.SetFocus() 1ahijklmgnopqrcdus
940 def _update_data_file_namespace(self, node): 1ab
941 while True: 1ahijklmgnopqrcdus
942 if not node: 942 ↛ 943line 942 didn't jump to line 943 because the condition on line 942 was never true1ahijklmgnopqrcdus
943 return
944 handler = self.controller.get_handler(node) 1ahijklmgnopqrcdus
945 if hasattr(handler.controller, 'get_namespace'): 1ahijklmgnopqrcdus
946 data_file_ns = handler.controller.get_namespace() 1ahijklmgnopqrcdus
947 if not data_file_ns: 1ahijklmgnopqrcdus
948 return 1cdus
949 cur_dir = handler.controller.directory 1ahijklmgnopqru
950 if data_file_ns: 950 ↛ 952line 950 didn't jump to line 952 because the condition on line 950 was always true1ahijklmgnopqru
951 data_file_ns.update_cur_dir_global_var(cur_dir) 1ahijklmgnopqru
952 return 1ahijklmgnopqr
953 else:
954 node = node.GetParent() 1cdus
956 def on_tree_item_expanding(self, event): 1ab
957 node = event.GetItem() 1aef
958 if node.IsOk(): 958 ↛ exitline 958 didn't return from function 'on_tree_item_expanding' because the condition on line 958 was always true1aef
959 self._render_children(node) 1aef
961 # This exists because CustomTreeItem does not remove animations
962 def on_tree_item_collapsing(self, event): 1ab
963 item = event.GetItem()
964 self._hide_item(item)
965 event.Skip()
967 def _hide_item(self, item): 1ab
968 for item in item.GetChildren():
969 itemwindow = item.GetWindow()
970 if itemwindow:
971 itemwindow.Hide()
972 if self.ItemHasChildren(item):
973 self._hide_item(item)
975 def SelectAllTests(self, item: GenericTreeItem, selected=True): 1ab
976 """
977 Select tests for execution
978 :param item: The node of the graphical tree where the user triggered the action from
979 :param selected: Whether we want to select or un-select for execution
980 :return: Nothing
981 """
982 test_controllers = self.retrieveTestCaseControllers(item)
983 self._test_selection_controller.select_all(test_controllers, selected)
985 @staticmethod 1ab
986 def retrieveTestCaseControllers(item: GenericTreeItem): 1ab
987 from ..controller.filecontrollers import TestDataDirectoryController, TestCaseFileController
989 data = item.GetData()
990 if isinstance(data, TestDataDirectoryHandler):
991 item_controller: TestDataDirectoryController = data.tests.parent
992 elif isinstance(data, TestCaseFileHandler):
993 item_controller: TestCaseFileController = data.controller
994 else:
995 raise AttributeError("Unexpected type of handler: " + str(data))
996 test_controllers = item_controller.retrieve_test_controllers()
997 return test_controllers
999 def SelectTests(self, tests): 1ab
1000 self._test_selection_controller.select_all(tests)
1002 def ExpandAllSubNodes(self, item): 1ab
1003 self._expand_or_collapse_nodes(item, self.Expand)
1005 def CollapseAllSubNodes(self, item): 1ab
1006 self._expand_or_collapse_nodes(item, self.Collapse)
1008 def _expand_or_collapse_nodes(self, item, callback): 1ab
1009 if not self.HasAGWFlag(customtreectrl.TR_HIDE_ROOT) or item != self.GetRootItem():
1010 callback(item)
1011 for child in item.GetChildren():
1012 self._expand_or_collapse_nodes(child, callback)
1014 def _for_all_drawn_tests(self, item, func): 1ab
1015 if self._is_test_node(item):
1016 func(item)
1017 for child in item.GetChildren():
1018 self._for_all_drawn_tests(child, func)
1020 @staticmethod 1ab
1021 def _is_test_node(node): 1ab
1022 return node.GetType() == 1
1024 def DeselectTests(self, tests): 1ab
1025 self._test_selection_controller.unselect_all(tests)
1027 def SelectFailedTests(self, item): 1ab
1028 all_controllers = self.retrieveTestCaseControllers(item)
1029 test_controllers = filter(
1030 lambda ctrl: ctrl.run_passed is False,
1031 all_controllers)
1032 self._test_selection_controller.unselect_all(all_controllers)
1033 self._test_selection_controller.select_all(test_controllers)
1035 def SelectPassedTests(self, item): 1ab
1036 all_controllers = self.retrieveTestCaseControllers(item)
1037 test_controllers = filter(
1038 lambda ctrl: ctrl.run_passed is True,
1039 all_controllers)
1040 self._test_selection_controller.unselect_all(all_controllers)
1041 self._test_selection_controller.select_all(test_controllers)
1043 def on_close(self, event): 1ab
1044 __ = event
1045 print("DEBUG: Tree OnClose hidding")
1046 self.Hide()
1048 def on_tree_item_checked(self, event): 1ab
1049 node: GenericTreeItem = event.GetItem()
1050 handler: TestCaseHandler = self.controller.get_handler(node=node)
1051 self._test_selection_controller.select(
1052 handler.controller, node.IsChecked())
1054 def on_item_activated(self, event): 1ab
1055 node = event.GetItem()
1056 if self.IsExpanded(node):
1057 self.Collapse(node)
1058 elif self.ItemHasChildren(node):
1059 self._expand_and_render_children(node)
1061 def on_left_arrow(self, event): 1ab
1062 node = self.GetSelection()
1063 if self.IsExpanded(node):
1064 self.Collapse(node)
1065 else:
1066 event.Skip()
1068 def on_right_click(self, event): 1ab
1069 __ = event
1070 if not self._right_click:
1071 self._right_click = True
1072 handler = None
1073 item, _ = self.HitTest(self.ScreenToClient(wx.GetMousePosition()), wx.TREE_HITTEST_ONITEMLABEL)
1074 if item:
1075 # print(f"DEBUG: tree mouse RightClick pos={pos}")
1076 handler = self.GetItemData(item)
1077 if handler:
1078 handler.show_popup()
1079 self._right_click = False
1081 def on_new_test_case(self, event): 1ab
1082 handler = self.controller.get_handler()
1083 if handler:
1084 handler.on_new_test_case(event)
1086 def OnDrop(self, target, dragged): # Overrides wx method 1ab
1087 dragged = self.controller.get_handler(dragged)
1088 target = self.controller.get_handler(target)
1089 if target and target.accepts_drag(dragged):
1090 dragged.controller.execute(MoveTo(target.controller))
1091 self.Refresh() # DEBUG Always refresh
1093 def IsValidDragItem(self, item): # Overrides wx method 1ab
1094 return self.controller.get_handler(item).is_draggable
1096 def on_move_up(self, event): 1ab
1097 handler = self.controller.get_handler()
1098 if handler.is_draggable:
1099 handler.on_move_up(event)
1101 def on_move_down(self, event): 1ab
1102 handler = self.controller.get_handler()
1103 if handler.is_draggable:
1104 handler.on_move_down(event)
1106 def _item_changed(self, message): 1ab
1107 # print(f"DEBUG: treeplugin.py Tree _item_changed ENTER message={message}")
1108 if isinstance(message, RideItemNameChanged): 1108 ↛ 1109line 1108 didn't jump to line 1109 because the condition on line 1108 was never true1xtyzABC
1109 return
1110 controller = message.item 1xtyzABC
1111 node = self.controller.find_node_by_controller(controller) 1xtyzABC
1112 if node: 1112 ↛ 1113line 1112 didn't jump to line 1113 because the condition on line 1112 was never true1xtyzABC
1113 self.SetItemText(node, message.item.name)
1114 self.controller.mark_node_dirty(self._get_datafile_node(controller.datafile))
1115 # if controller.dirty:
1117 def _item_renamed(self, message): 1ab
1118 from ..lib.robot.parsing.settings import Resource
1119 # print(f"DEBUG: treeplugin.py Tree _item_renamed ENTER message={message}")
1120 if not isinstance(message, RideItemNameChanged):
1121 return
1122 controller = message.item
1123 # print(f"DEBUG: treeplugin.py Tree _item_renamed DEFINED controller={controller}"
1124 # f" old_name={message.old_name} new_name={message.new_name}\n")
1125 node = (self.controller.find_node_by_controller(controller) or
1126 self.controller.find_node_with_label(controller, message.old_name))
1127 # print(f"DEBUG: treeplugin.py Tree _item_renamed ENTER controller={controller}"
1128 # f" NODE={node}")
1129 if hasattr(controller, 'datafile') and hasattr(controller.datafile, 'setting_table'):
1130 imports = controller.datafile.setting_table.imports
1131 # print(f"DEBUG: treeplugin.py Tree _item_renamed HAS IMPORTS imports={imports}")
1132 for imp in imports:
1133 if isinstance(imp, Resource) and hasattr(message.item, 'keyword'):
1134 # print(f"DEBUG: treeplugin.py Tree _item_renamed IMPORT message.item.name={message.item.keyword},"
1135 # f" old_name={message.old_name} resource name={imp.name}")
1136 # print(f"DEBUG: treeplugin.py Tree _item_renamed IMPORT message.item.name={message},"
1137 # f" old_name={message.old_name} import={imp}")
1138 self._rename_resource_kw(new_name=message.item.keyword, old_name=message.old_name, resource=imp)
1139 # if node and hasattr(message, 'old_name'):
1140 # print(f"DEBUG: treeplugin.py Tree _item_renamed RENAMED node={node}, old_name={message.old_name}\n"
1141 # f"new_name={message.new_name}")
1142 # Here we need to rename all resourc prefixed occurrencees
1144 self.Refresh()
1145 if node and hasattr(message.item, 'name'):
1146 self.EnsureVisible(node)
1147 self.SelectItem(node)
1148 self.SetItemText(node, message.item.name)
1149 new_name=self.GetItemText(node)
1150 # print(f"DEBUG: treeplugin.py Tree _item_renamed AFTER RENAMED new_name={new_name}"
1151 # f" controller={controller.datafile}")
1152 else:
1153 return
1154 if controller.dirty:
1155 self.controller.mark_node_dirty(
1156 self._get_datafile_node(controller.datafile))
1158 def _rename_resource_kw(self, new_name, old_name, resource): 1ab
1159 prefix_new = new_name.split('.')[0]
1160 prefix_old = old_name.split('.')[0]
1161 prefix_res = os.path.basename(resource.name).split('.')[0]
1162 if not prefix_new == prefix_old == prefix_res:
1163 return
1164 keyword_name = old_name.split('.')[-1]
1165 new_keyword_name = new_name.split('.')[-1]
1166 # print(f"DEBUG: treeplugin.py Tree _rename_resource_kw {prefix_res=} {keyword_name=}")
1167 res_name=os.path.basename(resource.name)
1168 # print(f"DEBUG: treeplugin.py Tree _rename_resource_kw {res_name=}")
1169 res_node=self.FindItem(self._resource_root, res_name)
1170 if res_node:
1171 self.EnsureVisible(res_node)
1172 # print(f"DEBUG: treeplugin.py Tree _rename_resource_kw root node {res_node=}")
1173 # node = self.controller.find_node_with_label(res_node, keyword_name)
1174 self._expand_and_render_children(res_node)
1175 node = self.FindItem(res_node, keyword_name)
1176 if node:
1177 from ..controller.ctrlcommands import RenameKeywordOccurrences
1178 from ..ui.progress import RenameProgressObserver
1179 nlabel = node.GetText()
1180 # print(f"DEBUG: treeplugin.py Tree _rename_resource_kw node label {nlabel} {keyword_name}")
1181 if nlabel == keyword_name:
1182 # print(f"DEBUG: treeplugin.py Tree _rename_resource_kw {node=} {nlabel} change to {new_keyword_name=}")
1183 self.EnsureVisible(node)
1184 # self.SetItemText(node, new_keyword_name)
1185 self.SelectItem(node)
1186 controller = self.get_selected_datafile_controller()
1187 keywords = controller.keywords
1188 keyword_names = controller.get_keyword_names()
1189 # print(f"DEBUG: treeplugin.py Tree _rename_resource_kw already changed to {new_keyword_name=}"
1190 # f" controller={controller} keywords={keywords} \n"
1191 # f"{keyword_names=}")
1192 for k in keywords.items:
1193 # print(f"DEBUG: treeplugin.py Tree _rename_resource_kw keywords: {k=}")
1194 if k.name == keyword_name:
1195 # print(f"DEBUG: treeplugin.py Tree _rename_resource_kw CHANGING: {k.name=},"
1196 # f" {k.source=} ") # {k.details=}")
1197 if controller.validate_keyword_name(new_keyword_name):
1198 k.name = new_keyword_name
1199 node.SetText(new_keyword_name)
1200 RideUserKeywordRenamed(datafile=controller.datafile, item=k,
1201 old_name=keyword_name).publish()
1202 controller.mark_dirty()
1203 # print(f"DEBUG: treeplugin.py Tree _rename_resource_kw DONE CHANGING: {k.name=}"
1204 # f"background={self.background}, foreground={self.foreground}")
1205 # self.controller.mark_node_dirty(self._get_datafile_node(controller.datafile))
1206 self.observer = RenameProgressObserver(self.GetParent(), background=self.background,
1207 foreground=self.foreground)
1208 RenameKeywordOccurrences(keyword_name, new_keyword_name, self.observer)
1209 self.observer.finish()
1210 self.Collapse(res_node)
1211 self.Expand(res_node)
1212 self.Refresh()
1213 self.SelectItem(node)
1214 else:
1215 from ..widgets import RIDEDialog
1216 message_box = RIDEDialog(title=_('Validation Error'),
1217 message=_("Invalid keyword name: ") % f"{new_keyword_name}",
1218 style=wx.ICON_ERROR | wx.OK)
1219 message_box.ShowModal()
1220 return
1222 def _variable_moved_up(self, message): 1ab
1223 if self._should_update_variable_positions(message):
1224 self._do_action_if_datafile_node_is_expanded(self.move_up, message)
1226 def _variable_moved_down(self, message): 1ab
1227 if self._should_update_variable_positions(message):
1228 self._do_action_if_datafile_node_is_expanded(self.move_down, message)
1230 @staticmethod 1ab
1231 def _should_update_variable_positions(message): 1ab
1232 return message.item != message.other and message.item.has_data() and message.other.has_data()
1234 def _do_action_if_datafile_node_is_expanded(self, action, data): 1ab
1235 if self.IsExpanded(self._get_datafile_node(data.item.datafile)):
1236 node = self.controller.find_node_by_controller(data.item)
1237 action(node)
1239 def _variable_updated(self, message): 1ab
1240 self._item_changed(message)
1242 def highlight(self, data, text): 1ab
1243 self.select_node_by_data(data)
1244 self._editor.highlight(text)
1246 def node_is_resource_file(self, node): 1ab
1247 return self.controller.get_handler(node).__class__ == \ 1Y
1248 ResourceFileHandler
1251class TreeLabelEditListener(object): 1ab
1253 def __init__(self, tree, action_registerer): 1ab
1254 self._tree = tree
1255 tree.Bind(wx.EVT_TREE_BEGIN_LABEL_EDIT, self.on_begin_label_edit)
1256 tree.Bind(wx.EVT_TREE_END_LABEL_EDIT, self.on_label_edited)
1257 tree.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
1258 if IS_WINDOWS: 1258 ↛ 1260line 1258 didn't jump to line 1260 because the condition on line 1258 was never true
1259 # Delete key does not work in windows without registration
1260 delete_key_action = ActionInfo(
1261 None, None, action=self.on_delete, shortcut='Del')
1262 action_registerer.register_shortcut(delete_key_action)
1263 self._editing_label = False
1264 self._on_label_edit_called = False
1266 def on_begin_label_edit(self, event): 1ab
1267 # See http://code.google.com/p/robotframework-ride/issues/detail?id=756
1268 self._editing_label = True
1269 if not self._on_label_edit_called:
1270 self.on_label_edit()
1271 event.Veto()
1272 # On windows CustomTreeCtrl will create Editor component
1273 # And we want this to be done by the handler -- as it knows if
1274 # there should be one or not. And because this will make it work
1275 # the same way as when pressing F2 so in other words there is
1276 # a bug if we don't Veto this event
1278 def on_label_edit(self, event=None): 1ab
1279 __ = event
1280 if not self._on_label_edit_called:
1281 self._on_label_edit_called = True
1282 handler = self._tree.controller.get_handler()
1283 if handler and not handler.begin_label_edit():
1284 self._on_label_edit_called = False
1285 self._editing_label = False
1287 def on_label_edited(self, event): 1ab
1288 self._editing_label = False
1289 self._on_label_edit_called = False
1290 self._tree.controller.get_handler(event.GetItem()).end_label_edit(event)
1292 # Reset edit control as it doesn't seem to reset it in case the focus
1293 # goes directly away from the tree control
1294 # Use CallAfter to prevent messing up the current end label edit
1295 # and the another CallAfter because of
1296 # customtreectrl.TreeTextCtrl#OnChar will call CallAfter(self.Finish)
1297 # when Enter is pressed --> Results in PyDeadObject if called after
1298 # ResetEditControl
1299 wx.CallAfter(wx.CallAfter, self._stop_editing)
1301 def _stop_editing(self): 1ab
1302 control = self._tree.GetEditControl()
1303 if control and wx.Window.FindFocus():
1304 control.StopEditing()
1306 def on_delete(self, event): 1ab
1307 __ = event
1308 editor = self._tree.GetEditControl()
1309 if editor and wx.Window.FindFocus() == editor:
1310 start, end = editor.GetSelection()
1311 editor.Remove(start, max(end, start + 1))
1313 def on_left_down(self, event): 1ab
1314 # See http://code.google.com/p/robotframework-ride/issues/detail?id=756
1315 if IS_WINDOWS and self._editing_label:
1316 # This method works only on Windows, luckily the issue 756 exists
1317 # only on Windows
1318 self._tree.OnCancelEdit(self._tree.GetSelection())
1319 event.Skip()
1321 def get_handler(self, item=None): 1ab
1322 return self._tree.get_handler(item)