Coverage for src/robotide/controller/ui/treecontroller.py: 79%
183 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 wx 1ab
18from wx.lib.agw.customtreectrl import GenericTreeItem 1ab
20from robotide import utils 1ab
21from robotide.action.actioninfo import action_info_collection, ActionInfo 1ab
22from robotide.context import IS_WINDOWS, ctrl_or_cmd, bind_keys_to_evt_menu 1ab
23from ..macrocontrollers import TestCaseController 1ab
24from robotide.controller import ctrlcommands 1ab
25from robotide.controller.tags import Tag, DefaultTag 1ab
26from robotide.publish import RideTestSelectedForRunningChanged 1ab
28_ = wx.GetTranslation # To keep linter/code analyser happy 1ab
29builtins.__dict__['_'] = wx.GetTranslation 1ab
32class TreeController(object): 1ab
34 def __init__(self, tree, action_registerer, settings, test_selection, history=None): 1ab
35 self._tree = tree 1aC
36 self._action_registerer = action_registerer 1aC
37 self.settings = settings 1aC
38 self._history = history or _History() 1aC
39 self._test_selection = test_selection 1aC
41 def register_tree_actions(self): 1ab
42 tree_actions = _("""[Navigate] 1aC
43 !Go &Back | Go back to previous location in tree | Alt-%s | ART_GO_BACK
44 !Go &Forward | Go forward to next location in tree | Alt-%s | ART_GO_FORWARD
45 """) % (('Left', 'Right') if IS_WINDOWS else ('Z', 'X'))
46 # Left and right cannot be overridden in tree on non Windows OSses, issue 354
48 tree_actions_nt = """[Navigate] 1aC
49 !Go &Back | Go back to previous location in tree | Alt-%s | ART_GO_BACK
50 !Go &Forward | Go forward to next location in tree | Alt-%s | ART_GO_FORWARD
51 """ % (('Left', 'Right') if IS_WINDOWS else ('Z', 'X'))
53 # print(f"DEBUG: treecontroller.py register_tree_actions ENTER tree_actions={tree_actions}")
54 actions = action_info_collection(tree_actions, self, data_nt=tree_actions_nt, container=self._tree) 1aC
55 self._action_registerer.register_actions(actions) 1aC
56 self._action_registerer.register_action(ActionInfo(menu_name=_('Edit'), name=_('Add Tag to selected'), 1aC
57 action=self.on_add_tag_to_selected))
58 self._action_registerer.register_action(ActionInfo(menu_name=_('Edit'), name=_('Clear Selected'), 1aC
59 action=self.on_clear_selected))
61 def on_go_back(self, event): 1ab
62 __ = event 1hKLMiN
63 node = self._history.back() 1hKLMiN
64 if node: 64 ↛ exitline 64 didn't return from function 'on_go_back' because the condition on line 64 was always true1hKLMiN
65 self._tree.SelectItem(node) 1hKLMiN
67 def on_add_tag_to_selected(self, event): 1ab
68 __ = event
69 if self._test_selection.is_empty():
70 return
71 name = wx.GetTextFromUser(message=_('Enter Tag Name'), caption=_('Add Tag To Selected'))
72 if name:
73 self._test_selection.add_tag(name)
75 def on_clear_selected(self, event): 1ab
76 __ = event
77 self._test_selection.clear_all(message=None)
79 def on_go_forward(self, event): 1ab
80 __ = event 1hi
81 node = self._history.forward() 1hi
82 if node: 82 ↛ exitline 82 didn't return from function 'on_go_forward' because the condition on line 82 was always true1hi
83 self._tree.SelectItem(node) 1hi
85 def add_to_history(self, node): 1ab
86 self._history.change(node) 1apqrsthKLMiNuvwxyzAcdjo
88 def clear_history(self): 1ab
89 self._history.clear()
91 def mark_controller_dirty(self, controller): 1ab
92 if not controller.dirty: 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true1mcdef
93 return
94 node = self.find_node_by_controller(controller) 1mcdef
95 if node: 1mcdef
96 self.mark_node_dirty(node) 1cdef
98 def mark_node_dirty(self, node): 1ab
99 text = self._tree.GetItemText(node) 1cdef
100 if not text.startswith('*'): 1cdef
101 self._tree.SetItemText(node, '*' + text) 1cdef
103 def find_node_by_controller(self, controller): 1ab
104 def match_handler(n): 1amDgEFGHIcdef
105 handler = self.get_handler(n) 1amDgEFGHIcdef
106 return handler and controller is handler.controller 1amDgEFGHIcdef
107 return self._find_node_with_predicate(self._tree.root, match_handler) 1amDgEFGHIcdef
109 def find_node_with_label(self, node, label): 1ab
110 # print(f"DEBUG: treecontroller.py TreeController find_node_with_label node={node} LABEL={label}")
111 def matcher(n): return utils.eq(self._tree.GetItemText(n), label) 1cdBjoef
112 return self._find_node_with_predicate(node, matcher) 1cdBjoef
114 def _find_node_with_predicate(self, node, predicate): 1ab
115 # print(f"DEBUG: treecontroller.py TreeController find_node_with_label ENTER node={node}"
116 # f" node is type={type(node)}")
117 if node != self._tree.root and isinstance(node, GenericTreeItem) and predicate(node): 117 ↛ 118line 117 didn't jump to line 118 because the condition on line 117 was never true1amDgEFGHIcdBjoef
118 return node
119 if not isinstance(node, GenericTreeItem): 119 ↛ 120line 119 didn't jump to line 120 because the condition on line 119 was never true1amDgEFGHIcdBjoef
120 node = self._tree.root
121 item, cookie = self._tree.GetFirstChild(node) 1amDgEFGHIcdBjoef
122 while item: 1amDgEFGHIcdBjoef
123 if predicate(item): 1amDgEFGHIcdBjoef
124 return item 1acdBjoef
125 if self._tree.ItemHasChildren(item): 1amDgEFGHIcdBjoef
126 result = self._find_node_with_predicate(item, predicate) 1acdBjef
127 if result: 1acdBjef
128 return result 1acBjef
129 item, cookie = self._tree.GetNextChild(node, cookie) 1amDgEFGHIcdBjoef
130 return None 1amDgEFGHIcdBjef
132 def get_handler(self, node=None): 1ab
133 return self._tree.GetItemData(node or self._tree.GetSelection()) 1apqrstu4vwxy5zA67mDgEFGHIcdjoef
135 def bind_keys(self): 1ab
136 bind_keys_to_evt_menu(self._tree, self._get_bind_keys())
138 def _get_bind_keys(self): 1ab
139 bindings = [
140 (ctrl_or_cmd(), wx.WXK_UP, self._tree.on_move_up),
141 (ctrl_or_cmd(), wx.WXK_DOWN, self._tree.on_move_down),
142 (wx.ACCEL_NORMAL, wx.WXK_F2, self._tree.label_editor.on_label_edit),
143 (wx.ACCEL_NORMAL, wx.WXK_WINDOWS_MENU, self._tree.on_right_click),
144 (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('d'), lambda event: self._expanded_handler().on_safe_delete(event)),
145 (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('f'),
146 lambda event: self._expanded_handler().on_new_suite(event)),
147 (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('k'),
148 lambda event: self._expanded_handler().on_new_user_keyword(event)),
149 (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('t'),
150 lambda event: self._expanded_handler().on_new_test_case(event)),
151 (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('v'),
152 lambda event: self._expanded_handler().on_new_scalar(event)),
153 (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('l'),
154 lambda event: self._expanded_handler().on_new_list_variable(event)),
155 (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('c'),
156 lambda event: self._expanded_handler().on_copy(event))
157 ]
158 if not IS_WINDOWS: 158 ↛ 160line 158 didn't jump to line 160 because the condition on line 158 was always true
159 bindings.append((wx.ACCEL_NORMAL, wx.WXK_LEFT, self._tree.on_left_arrow))
160 return bindings
162 def _expanded_handler(self): 1ab
163 handler = self.get_handler()
164 if not self._tree.IsExpanded(handler.node):
165 self._tree.Expand(handler.node)
166 return handler
169class _History(object): 1ab
171 def __init__(self): 1ab
172 self._back = [] 1aCYZ
173 self._forward = [] 1aCYZ
175 def change(self, state): 1ab
176 if not self._back or state != self._back[-1]: 1apqrsthKLMiNuvwxyzAcdjo8OXQ
177 self._back.append(state) 1apqrsthKLMiNuvwxyzAcdjoOQ
178 self._forward = [] 1apqrsthKLMiNuvwxyzAcdjoOQ
180 def back(self): 1ab
181 if not self._back: 1hKLMiNY012OXUQVW
182 return None 1Y
183 if len(self._back) > 1: 1hKLMiN012OXUQVW
184 self._forward.append(self._back.pop()) 1hKLMiN012OXUQVW
185 return self._back[-1] 1hKLMiN012OXUQVW
187 def forward(self): 1ab
188 if not self._forward: 1hiOUQ3ZVW
189 return None 1Q3Z
190 state = self._forward.pop() 1hiOUVW
191 self._back.append(state) 1hiOUVW
192 return state 1hiOUVW
194 def top(self): 1ab
195 return self._back and self._back[-1] or None
197 def clear(self): 1ab
198 self._back.clear()
199 self._forward.clear()
202class TestSelectionController(object): 1ab
203 __test__ = False 1ab
205 def __init__(self): 1ab
206 self._tests: {TestCaseController} = set()
208 def is_empty(self): 1ab
209 return not bool(self._tests) 1kJ9P
211 def is_test_selected(self, test): 1ab
212 return test in self._tests 1lnRkJP
214 def clear_all(self, message): 1ab
215 _ = message
216 self._tests = set()
217 self._send_selection_changed_message()
219 def unselect_all(self, tests): 1ab
220 self.select_all(tests, selected=False)
222 def select_all(self, tests, selected=True): 1ab
223 for test in tests:
224 self.select(test, selected, notify_selection=False)
225 self._send_selection_changed_message()
227 def select(self, test: TestCaseController, do_select=True, notify_selection=True): 1ab
228 changed = False 1lnRkJP
229 if do_select and not self.is_test_selected(test): 1lnRkJP
230 self._tests.add(test) 1lnRkJP
231 changed = True 1lnRkJP
232 elif not do_select and self.is_test_selected(test): 232 ↛ 235line 232 didn't jump to line 235 because the condition on line 232 was always true1J
233 self._tests.remove(test) 1J
234 changed = True 1J
235 if notify_selection and changed: 235 ↛ exitline 235 didn't return from function 'select' because the condition on line 235 was always true1lnRkJP
236 self._send_selection_changed_message() 1lnRkJP
238 def remove_invalid_cases_selection(self, cases_file_controller): 1ab
239 from .. import ResourceFileController 1pqrstkuvwxyzASTg
240 invalid_cases = list() 1pqrstkuvwxyzASTg
241 to_select_cases = list() 1pqrstkuvwxyzASTg
242 for test in self._tests: 1pqrstkuvwxyzASTg
243 if test.datafile_controller == cases_file_controller: 243 ↛ 244line 243 didn't jump to line 244 because the condition on line 243 was never true1k
244 if not isinstance(cases_file_controller, ResourceFileController):
245 for newobj in cases_file_controller.tests:
246 if test.longname == newobj.longname:
247 to_select_cases.append(newobj)
248 invalid_cases.append(test)
249 for _ in invalid_cases: 249 ↛ 250line 249 didn't jump to line 250 because the loop on line 249 never started1pqrstkuvwxyzASTg
250 self._tests.remove(_)
251 for test in to_select_cases: 251 ↛ 252line 251 didn't jump to line 252 because the loop on line 251 never started1pqrstkuvwxyzASTg
252 self.select(test, True, False)
253 self._send_selection_changed_message() 1pqrstkuvwxyzASTg
255 def _send_selection_changed_message(self): 1ab
256 message = RideTestSelectedForRunningChanged(tests=self._tests) 1apqrstlnRkJPuvwxyzASTg
257 wx.CallAfter(message.publish) 1apqrstlnRkJPuvwxyzASTg
259 def add_tag(self, name): 1ab
260 for test in self._tests: 1ln
261 self._add_tag_to_test(name, test) 1ln
263 def _add_tag_to_test(self, name, test): 1ab
264 if name not in [t.name for t in test.tags]: 264 ↛ exitline 264 didn't return from function '_add_tag_to_test' because the condition on line 264 was always true1ln
265 self._move_default_tags_to_test(test) 1ln
266 self._add_tag(test, name) 1ln
268 def _move_default_tags_to_test(self, test): 1ab
269 for tag in test.tags: 1ln
270 if isinstance(tag, DefaultTag): 270 ↛ 269line 270 didn't jump to line 269 because the condition on line 270 was always true1l
271 self._add_tag(test, tag.name) 1l
273 @staticmethod 1ab
274 def _add_tag(test, name): 1ab
275 test.tags.execute(ctrlcommands.ChangeTag(Tag(None), name)) 1ln