Coverage for src/robotide/ui/tagdialogs.py: 39%
277 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 re 1ab
19import wx 1ab
20import wx.lib.mixins.listctrl as listmix 1ab
21from wx import Colour 1ab
23from .. import utils 1ab
24from ..controller.ctrlcommands import ChangeTag 1ab
25from ..publish import RideOpenTagSearch 1ab
26from ..widgets import ButtonWithHandler, PopupMenuItems, RIDEDialog 1ab
28_ = wx.GetTranslation # To keep linter/code analyser happy 1ab
29builtins.__dict__['_'] = wx.GetTranslation 1ab
32class ViewAllTagsDialog(RIDEDialog, listmix.ColumnSorterMixin): 1ab
34 def __init__(self, controller, frame): 1ab
35 style = wx.SYSTEM_MENU | wx.CAPTION | wx.CLOSE_BOX | wx.CLIP_CHILDREN | \
36 wx.FRAME_FLOAT_ON_PARENT
37 RIDEDialog.__init__(self, parent=frame, title=_("View all tags"), size=(500, 400), style=style)
38 self.SetBackgroundColour(Colour(self.color_background))
39 self.SetForegroundColour(Colour(self.color_foreground))
40 # set Left to Right direction (while we don't have localization)
41 self.SetLayoutDirection(wx.Layout_LeftToRight)
42 self.frame = frame
43 self.tree = self.frame.tree
44 self._controller = controller
45 self._results = utils.NormalizedDict()
46 self.selected_tests = list()
47 self.tagged_test_cases = list()
48 self.unique_tags = 0
49 self.total_test_cases = 0
50 self.itemDataMap = dict()
51 self.sort_state = (0, 1)
52 self._index = -1
53 self._build_ui()
54 self._make_bindings()
56 # init ColumnSorterMixin at the end because it calls self.GetListCtrl
57 # and therefore self._tags_list has to be declared
58 listmix.ColumnSorterMixin.__init__(self, 2)
60 def _build_ui(self): 1ab
61 self.SetSizer(wx.BoxSizer(wx.VERTICAL))
62 self._build_notebook()
63 self._build_tag_lister()
64 self._build_controls()
65 self._build_footer()
66 self.CenterOnParent()
68 def _build_tag_lister(self): 1ab
69 panel_tag_vw = wx.Panel(self._notebook)
70 panel_tag_vw.SetBackgroundColour(Colour(self.color_background))
71 panel_tag_vw.SetForegroundColour(Colour(self.color_foreground))
72 sizer_tag_vw = wx.BoxSizer(wx.VERTICAL)
73 panel_tag_vw.SetSizer(sizer_tag_vw)
74 self._tags_list = TagsListCtrl(panel_tag_vw, style=wx.LC_REPORT, color_bg=self.color_secondary_background,
75 color_fg=self.color_secondary_foreground)
76 self._tags_list.InsertColumn(0, _("Tag"), width=200)
77 self._tags_list.InsertColumn(1, _("Occurrences"), width=25,
78 format=wx.LIST_FORMAT_CENTER)
79 self._tags_list.SetMinSize((450, 250))
80 self._tags_list.set_dialog(self)
81 sizer_tag_vw.Add(self._tags_list, 1, wx.ALL | wx.EXPAND, 3)
82 self._notebook.AddPage(panel_tag_vw, _("The List"))
84 def _build_controls(self): 1ab
85 self._clear_button = ButtonWithHandler(self, _('Refresh'), handler=self.on_clear)
86 self._show_tagged_tests_button = ButtonWithHandler(self, _('Included Tag Search'),
87 mk_handler='Included Tag Search',
88 handler=self.on_included_tag_search)
89 self._show_excluded_tests_button = ButtonWithHandler(self, _('Excluded Tag Search'),
90 mk_handler='Excluded Tag Search',
91 handler=self.on_excluded_tag_search)
92 self._clear_button.SetBackgroundColour(Colour(self.color_secondary_background))
93 self._clear_button.SetForegroundColour(Colour(self.color_secondary_foreground))
94 self._show_tagged_tests_button.SetBackgroundColour(Colour(self.color_secondary_background))
95 self._show_tagged_tests_button.SetForegroundColour(Colour(self.color_secondary_foreground))
96 self._show_excluded_tests_button.SetBackgroundColour(Colour(self.color_secondary_background))
97 self._show_excluded_tests_button.SetForegroundColour(Colour(self.color_secondary_foreground))
98 controls = wx.BoxSizer(wx.HORIZONTAL)
99 controls.Add(self._show_tagged_tests_button, 0, wx.ALL, 3)
100 controls.Add(self._show_excluded_tests_button, 0, wx.ALL, 3)
101 controls.Add(self._clear_button, 0, wx.ALL, 3)
102 self.Sizer.Add(controls, 0, wx.ALL | wx.EXPAND, 3)
104 def _build_footer(self): 1ab
105 footer = wx.BoxSizer(wx.HORIZONTAL)
106 self._footer_text = wx.StaticText(self, -1, '')
107 footer.Add(self._footer_text)
108 self.Sizer.Add(footer, 0, wx.ALL, 3)
110 def _build_notebook(self): 1ab
111 self._notebook = wx.Notebook(self, wx.ID_ANY, style=wx.NB_TOP)
112 self._notebook.SetBackgroundColour(Colour(self.color_background))
113 self._notebook.SetForegroundColour(Colour(self.color_foreground))
114 self.Sizer.Add(self._notebook, 1, wx.ALL | wx.EXPAND, 3)
116 def _make_bindings(self): 1ab
117 self.Bind(wx.EVT_CLOSE, self._close_dialog)
118 self._tags_list.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.on_right_click)
119 self._tags_list.Bind(wx.EVT_LIST_COL_CLICK, self.on_col_click)
120 self._tags_list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_select_item)
122 @staticmethod 1ab
123 def _tag_name_for_sort(tag_name): 1ab
124 return [part if index % 2 == 0 else int(part) for index, part in 1cdef
125 enumerate(re.split(r'(\d+)', tag_name.lower()))]
127 def _execute(self): 1ab
128 self._clear_search_results()
129 self._search_for_tags()
130 self.tagged_test_cases = list()
131 self.unique_tags = 0
133 for tag_name, tests in self._results:
134 model_entry = self._tags_list.add_tag((tests, tag_name))
135 self.tagged_test_cases += tests
136 # Mapping the lists model entry with the model for sorting.
137 self.itemDataMap[model_entry] = \
138 (self._tag_name_for_sort(tag_name), len(tests))
139 self.unique_tags += 1
140 self._tags_list.SetColumnWidth(1, wx.LIST_AUTOSIZE_USEHEADER)
141 self._tags_list.setResizeColumn(1)
142 self.tagged_test_cases = list(set(self.tagged_test_cases))
143 self.update_footer()
144 self.SortListItems(self.sort_state[0], self.sort_state[1])
146 def update_footer(self): 1ab
147 footer_string = (_("Total tests %d, Tests with tags %d, Unique tags %d\n"
148 "Currently selected tests %d")) % (self.total_test_cases, len(self.tagged_test_cases),
149 self.unique_tags, len(self.selected_tests))
150 self._footer_text.SetLabel(footer_string)
152 def show_dialog(self): 1ab
153 self._execute()
154 if not self.IsShown():
155 self.Show()
156 self.Raise()
158 def _clear_search_results(self): 1ab
159 self.selected_tests = list()
160 self._tags_list.ClearAll()
162 def _add_view_components(self): 1ab
163 """ Just ignore it """
164 pass
166 def _search_for_tags(self): 1ab
167 unique_tags = utils.NormalizedDict()
168 self._tags = utils.NormalizedDict()
169 self._test_cases = []
170 for test in self.frame.controller.all_testcases():
171 self._test_cases.append(test)
172 for tag in test.tags:
173 if tag.is_empty() or len(str(tag).strip()) == 0:
174 continue
175 else:
176 tag_name = str(tag)
177 if tag_name in unique_tags:
178 unique_tags[tag_name].append(test)
179 self._tags[tag_name].append(tag)
180 else:
181 unique_tags[tag_name] = [test]
182 self._tags[tag_name] = [tag]
184 isreversed = (self.sort_state[1] != 1)
185 self.total_test_cases = len(self._test_cases)
187 self._results = sorted(unique_tags.items(),
188 key=lambda item: item[0].lower(),
189 reverse=isreversed)
191 def GetListCtrl(self): 1ab
192 return self._tags_list
194 def on_col_click(self, event): 1ab
195 self.sort_state = self.GetSortState()
196 event.Skip()
198 def _add_checked_tags_into_list(self): 1ab
199 tags = []
200 for _, tag_name in self._tags_list.get_checked_items():
201 tags.append(tag_name)
202 return tags
204 def on_included_tag_search(self, event): 1ab
205 __ = event
206 included_tags = self._add_checked_tags_into_list()
207 RideOpenTagSearch(includes=' '.join(included_tags),
208 excludes='').publish()
210 def on_excluded_tag_search(self, event): 1ab
211 __ = event
212 excluded_tags = self._add_checked_tags_into_list()
213 RideOpenTagSearch(includes='',
214 excludes=' '.join(excluded_tags)).publish()
216 def on_clear(self, event): 1ab
217 __ = event
218 self._execute()
219 for _, tests in self._results:
220 self.tree.DeselectTests(tests)
221 self.update_footer()
223 def on_select_all(self, event): 1ab
224 __ = event
225 all_tests = []
226 for _, tests in self._results:
227 all_tests += tests
228 self.tree.SelectTests(all_tests)
229 self._tags_list.CheckAll()
231 def on_right_click(self, event): 1ab
232 self._index = event.GetIndex()
233 menu_items = [_("Select all"), _("Clear"), "---", _("Rename"), _("Delete"), "---",
234 _("Show tests with this tag"), _("Show tests without this tag")]
235 self.tree._popup_creator.show(self, PopupMenuItems(self, menu_items), self._controller)
237 def on_select_item(self, event): 1ab
238 self._index = event.GetIndex()
239 self._tags_list.CheckItem(self._index, not self._tags_list.IsChecked(self._index))
241 def on_show_tests_with_this_tag(self, event): 1ab
242 __ = event
243 if self._index == -1:
244 return
245 _, tag_name = self._tags_list.get_tag(self._index)
246 RideOpenTagSearch(includes=tag_name, excludes="").publish()
248 def on_show_tests_without_this_tag(self, event): 1ab
249 __ = event
250 if self._index == -1:
251 return
252 _, tag_name = self._tags_list.get_tag(self._index)
253 RideOpenTagSearch(includes="", excludes=tag_name).publish()
255 def on_rename(self, event): 1ab
256 __ = event
257 if self._index == -1:
258 return
259 tests, tag_name = self._tags_list.get_tag(self._index)
260 tags_to_rename = self._tags[tag_name.lower()]
261 name = wx.GetTextFromUser(message=_("Renaming tag '%s'.") % tag_name, default_value=tag_name,
262 caption=_('Rename')).strip()
263 if name:
264 for tag in tags_to_rename:
265 tag.controller.execute(ChangeTag(tag, name))
266 self._execute()
267 for tag_name, tests in self._results:
268 self.tree.DeselectTests(tests)
270 def on_delete(self, event): 1ab
271 __ = event
272 if self._index == -1:
273 return
274 tests, tag_name = self._tags_list.get_tag(self._index)
275 tags_to_delete = self._tags[tag_name.lower()]
276 message_box = RIDEDialog(title=_('Confirm'), message=_("Delete a tag '%s' ?") % tag_name,
277 style=wx.YES_NO | wx.ICON_QUESTION)
278 ret = message_box.ShowModal()
279 if ret == wx.ID_YES:
280 for tag in tags_to_delete:
281 tag.controller.execute(ChangeTag(tag, ''))
282 self._execute()
283 for tag_name, tests in self._results:
284 self.tree.DeselectTests(tests)
286 def _close_dialog(self, event): 1ab
287 if event.CanVeto():
288 self.Hide()
289 else:
290 self.Destroy()
292 def item_in_kw_list_checked(self, index, flag): 1ab
293 self.selected_tests = list()
294 if flag is False:
295 tests, _ = self._tags_list.get_tag(index)
296 self.tree.DeselectTests(tests)
297 if self._tags_list.get_number_of_checked_items() > 0:
298 for tests, _ in self._tags_list.get_checked_items():
299 self.selected_tests += tests
300 self.tree.SelectTests(tests)
301 self.selected_tests = list(set(self.selected_tests))
302 self.update_footer()
305class TagsListCtrl(wx.ListCtrl, listmix.CheckListCtrlMixin, 1ab
306 listmix.ListCtrlAutoWidthMixin):
307 def __init__(self, parent, style, color_bg, color_fg): 1ab
308 self.parent = parent
309 wx.ListCtrl.__init__(self, parent=parent, style=style)
310 if wx.VERSION < (4, 1, 0): 310 ↛ 311line 310 didn't jump to line 311 because the condition on line 310 was never true
311 listmix.CheckListCtrlMixin.__init__(self)
312 listmix.ListCtrlAutoWidthMixin.__init__(self)
313 if wx.VERSION >= (4, 1, 0): 313 ↛ 316line 313 didn't jump to line 316 because the condition on line 313 was always true
314 # print(f"DEBUG: CheckAll tags")
315 self.EnableCheckBoxes(True)
316 self.SetBackgroundColour(Colour(color_bg))
317 self.SetForegroundColour(Colour(color_fg))
318 self.setResizeColumn(2)
319 self._clientData = {}
320 self._dlg = None
322 def OnCheckItem(self, index, flag): # Overrides wx method 1ab
323 if self._dlg:
324 self._dlg.item_in_kw_list_checked(index, flag)
326 def get_checked_items(self): 1ab
327 items = []
328 for i in range(self.GetItemCount()):
329 if self.IsChecked(i):
330 items.append(self.get_tag(i))
331 return items
333 def get_number_of_checked_items(self): 1ab
334 total = 0
335 for i in range(self.GetItemCount()):
336 if self.IsChecked(i):
337 total += 1
338 return total
340 def set_dialog(self, dialog): 1ab
341 self._dlg = dialog
343 def add_tag(self, tag_to_tests): 1ab
344 """ Append a tag with associated tests to the list.
346 This method associates the internal model data with the GUI. While the
347 sort order of the GUI can change at any time, the order of the data in
348 the model does not. The entry in the model is referenced by a new id
349 generated by wx.NewIdRef(). This prevents overwriting entries when
350 combining delete and insert statements.
352 Args:
353 tag_to_tests: A tuple mapping tests(list, index 0) to a
354 tag(str, index 1).
356 Returns:
357 An int indicating the entry in the model so that it can be
358 associated with the sorting algorithm for example.
359 """
361 model_index = wx.NewIdRef()
362 position = self.GetItemCount()
363 self._clientData[model_index] = tag_to_tests
364 self.InsertItem(position, str(tag_to_tests[1]))
365 self.SetItem(position, 1, str(len(tag_to_tests[0])))
366 # associate the model with the GUI
367 self.SetItemData(position, model_index)
368 return model_index
370 def get_tag(self, index): 1ab
371 """ Get a tag with associated test from the list based on the position
372 in the list.
374 Args:
375 index: An int marking the position of the element in the list.
377 Returns:
378 A tuple mapping tests(list, index 0) to a tag(str, index 1).
379 """
381 model_index = self.GetItemData(index)
382 return self._clientData.get(model_index, None)
384 def remove_tag(self, index): 1ab
385 """ Remove a tag based on its position in the list.
387 Args:
388 index: An int marking the position of the element in the list.
389 """
391 model_index = self.GetItemData(index)
392 self.DeleteItem(model_index)
393 del self._clientData[model_index]
395 def ClearAll(self): 1ab
396 self.DeleteAllItems()
397 self._clientData.clear()
399 def CheckAll(self): 1ab
400 for i in range(self.GetItemCount()):
401 if wx.VERSION >= (4, 1, 0):
402 # print(f"DEBUG: CheckAll tags")
403 self.CheckItem(i, True)
404 else:
405 self.CheckItem(i)