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

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. 

15 

16import builtins 1ab

17import re 1ab

18 

19import wx 1ab

20import wx.lib.mixins.listctrl as listmix 1ab

21from wx import Colour 1ab

22 

23from .. import utils 1ab

24from ..controller.ctrlcommands import ChangeTag 1ab

25from ..publish import RideOpenTagSearch 1ab

26from ..widgets import ButtonWithHandler, PopupMenuItems, RIDEDialog 1ab

27 

28_ = wx.GetTranslation # To keep linter/code analyser happy 1ab

29builtins.__dict__['_'] = wx.GetTranslation 1ab

30 

31 

32class ViewAllTagsDialog(RIDEDialog, listmix.ColumnSorterMixin): 1ab

33 

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() 

55 

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) 

59 

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() 

67 

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")) 

83 

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) 

103 

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) 

109 

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) 

115 

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) 

121 

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()))] 

126 

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 

132 

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]) 

145 

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) 

151 

152 def show_dialog(self): 1ab

153 self._execute() 

154 if not self.IsShown(): 

155 self.Show() 

156 self.Raise() 

157 

158 def _clear_search_results(self): 1ab

159 self.selected_tests = list() 

160 self._tags_list.ClearAll() 

161 

162 def _add_view_components(self): 1ab

163 """ Just ignore it """ 

164 pass 

165 

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] 

183 

184 isreversed = (self.sort_state[1] != 1) 

185 self.total_test_cases = len(self._test_cases) 

186 

187 self._results = sorted(unique_tags.items(), 

188 key=lambda item: item[0].lower(), 

189 reverse=isreversed) 

190 

191 def GetListCtrl(self): 1ab

192 return self._tags_list 

193 

194 def on_col_click(self, event): 1ab

195 self.sort_state = self.GetSortState() 

196 event.Skip() 

197 

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 

203 

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() 

209 

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() 

215 

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() 

222 

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() 

230 

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) 

236 

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)) 

240 

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() 

247 

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() 

254 

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) 

269 

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) 

285 

286 def _close_dialog(self, event): 1ab

287 if event.CanVeto(): 

288 self.Hide() 

289 else: 

290 self.Destroy() 

291 

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() 

303 

304 

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 

321 

322 def OnCheckItem(self, index, flag): # Overrides wx method 1ab

323 if self._dlg: 

324 self._dlg.item_in_kw_list_checked(index, flag) 

325 

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 

332 

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 

339 

340 def set_dialog(self, dialog): 1ab

341 self._dlg = dialog 

342 

343 def add_tag(self, tag_to_tests): 1ab

344 """ Append a tag with associated tests to the list. 

345 

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. 

351 

352 Args: 

353 tag_to_tests: A tuple mapping tests(list, index 0) to a 

354 tag(str, index 1). 

355 

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 """ 

360 

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 

369 

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. 

373 

374 Args: 

375 index: An int marking the position of the element in the list. 

376 

377 Returns: 

378 A tuple mapping tests(list, index 0) to a tag(str, index 1). 

379 """ 

380 

381 model_index = self.GetItemData(index) 

382 return self._clientData.get(model_index, None) 

383 

384 def remove_tag(self, index): 1ab

385 """ Remove a tag based on its position in the list. 

386 

387 Args: 

388 index: An int marking the position of the element in the list. 

389 """ 

390 

391 model_index = self.GetItemData(index) 

392 self.DeleteItem(model_index) 

393 del self._clientData[model_index] 

394 

395 def ClearAll(self): 1ab

396 self.DeleteAllItems() 

397 self._clientData.clear() 

398 

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)