Coverage for src/robotide/editor/gridbase.py: 67%

262 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 wx 1ab

18from wx import grid, Colour 1ab

19 

20from .clipboard import ClipboardHandler 1ab

21from ..context import IS_WINDOWS 1ab

22from ..utils import unescape_newlines_and_whitespaces 1ab

23from ..widgets import PopupCreator, PopupMenuItems 1ab

24 

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

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

27 

28 

29class GridEditor(grid.Grid): 1ab

30 _col_add_threshold = 6 1ab

31 _popup_items = [] 1ab

32 

33 _popup_items_nt = [] 1ab

34 

35 def __init__(self, parent, num_rows, num_cols, popup_creator=None): 1ab

36 grid.Grid.__init__(self, parent) 

37 try: 

38 self.settings = parent.plugin.global_settings['Grid'] 

39 self.general_settings = parent.plugin.global_settings['General'] 

40 except AttributeError: 

41 from ..preferences import RideSettings 

42 _settings = RideSettings() 

43 self.general_settings = _settings['General'] 

44 self.settings = _settings['Grid'] 

45 self.filter_newlines = self.settings.get("filter newlines", True) 

46 self.color_background = self.settings['background unknown'] 

47 self.color_foreground = self.settings['text empty'] 

48 self.color_background_help = self.general_settings['background help'] 

49 self.color_foreground_text = self.general_settings['foreground text'] 

50 self.color_secondary_background = self.general_settings['secondary background'] 

51 self.color_secondary_foreground = self.general_settings['secondary foreground'] 

52 

53 self._bind_to_events() 

54 self.selection = _GridSelection(self) 

55 self._clipboard_handler = ClipboardHandler(self) 

56 self._history = _GridState() 

57 self.CreateGrid(int(num_rows), int(num_cols)) 

58 self.SetDefaultCellBackgroundColour(Colour(self.color_background)) 

59 self.SetDefaultCellTextColour(Colour(self.color_foreground)) 

60 self.GetGridColLabelWindow().SetBackgroundColour(Colour(self.color_secondary_background)) 

61 self.GetGridColLabelWindow().SetForegroundColour(Colour(self.color_secondary_foreground)) 

62 self.GetGridRowLabelWindow().SetBackgroundColour(Colour(self.color_secondary_background)) 

63 self.GetGridRowLabelWindow().SetForegroundColour(Colour(self.color_secondary_foreground)) 

64 self.SetLabelBackgroundColour(Colour(self.color_secondary_background)) 

65 self.SetLabelTextColour(Colour(self.color_secondary_foreground)) 

66 self._popup_creator = popup_creator or PopupCreator() 

67 """ 

68 DEBUG: This block adds aditional scrollbars in mains Grid Editor, making hard to focus on cells keeping the  

69 row numbers visible. 

70 if hasattr(parent, 'SetupScrolling'): 

71 parent.SetupScrolling(scrollToTop=True, scrollIntoView=True) 

72 print("DEBUG: GridBase init at PARENT SetupScrolling\n") 

73 elif 

74 """ 

75 if hasattr(self, 'SetupScrolling'): 75 ↛ 76line 75 didn't jump to line 76 because the condition on line 75 was never true

76 self.SetupScrolling(scrollToTop=True, scrollIntoView=True) 

77 # print("DEBUG: GridBase init at SELF SetupScrolling\n") 

78 # else: 

79 # print("DEBUG: GridBase init NO SetupScrolling\n") 

80 

81 def _bind_to_events(self): 1ab

82 self.Bind(grid.EVT_GRID_SELECT_CELL, self.on_select_cell) 

83 self.Bind(grid.EVT_GRID_RANGE_SELECT, self.on_range_select) 

84 self.Bind(grid.EVT_GRID_CELL_RIGHT_CLICK, self.on_cell_right_click) 

85 

86 def register_context_menu_hook(self, cb): 1ab

87 self._popup_creator.add_hook(cb) 

88 

89 def unregister_context_menu_hook(self, cb): 1ab

90 self._popup_creator.remove_hook(cb) 

91 

92 def write_cell(self, row, col, value, update_history=True): 1ab

93 if update_history: 1afghcijklde

94 self._update_history() 1fghcde

95 self._expand_if_necessary(row, col) 1afghcijklde

96 if self.filter_newlines: 96 ↛ 99line 96 didn't jump to line 99 because the condition on line 96 was always true1afghcijklde

97 # unescape \n to support multi lines display in grid cells 

98 value = unescape_newlines_and_whitespaces(value) 1afghcijklde

99 self.SetCellValue(row, col, value) 1afghcijklde

100 

101 def _expand_if_necessary(self, row, col): 1ab

102 # Changed col and row fill because of blank spacing not changing color 

103 # print(f"DEBUG: GridEditor ENTER_expand_if_necessary row={row}, col={col}") 

104 while self.NumberRows <= max(1, row+1, 10-row): # DEBUG 25 makes slower rendering 1afghcijklde

105 self.AppendRows(1) 1acde

106 while self.NumberCols <= max(1, col+1, 10-col): # DEBUG 40 makes slower rendering 1afghcijklde

107 self.AppendCols(max(1, self._col_add_threshold)) # DEBUG: was infinite when value was 0 1acde

108 

109 def has_focus(self): 1ab

110 return self.FindFocus() == self 

111 

112 def _update_history(self): 1ab

113 self._history.change(self._get_all_content()) 1fghcijklde

114 

115 def _get_all_content(self): 1ab

116 return self._get_block_content(range(self.NumberRows), 1fghcijklde

117 range(self.NumberCols)) 

118 

119 @property 1ab

120 def cell_under_cursor(self): 1ab

121 x, y = self.ScreenToClient(wx.GetMousePosition()) 

122 x -= self.RowLabelSize 

123 y -= self.GetColLabelSize() 

124 return self.XYToCell(*self.CalcUnscrolledPosition(x, y)) 

125 

126 def select(self, row, column): 1ab

127 self.SelectBlock(row, column, row, column) 

128 self.SetGridCursor(row, column) 

129 self.MakeCellVisible(row, column) 

130 

131 def copy(self): 1ab

132 print("DEBUG: GridBase copy() called\n") 1mnoijkl

133 self._clipboard_handler.clear() 1mnoijkl

134 data = self._clipboard_handler.clipboard_content() 1mnoijkl

135 print(f"DEBUG: GridBase copy() clipboard_content =={data}\n") 1mnoijkl

136 self._clipboard_handler.copy() 1mnoijkl

137 

138 def cut(self): 1ab

139 self._update_history() 1fghcde

140 self._clipboard_handler.clear() 1fghcde

141 self._clipboard_handler.cut() 1fghcde

142 self._clear_selected_cells() 1fghcde

143 

144 def _clear_selected_cells(self): 1ab

145 self.BeginBatch() 1fghcde

146 for row, col in self.selection.cells(): 1fghcde

147 self.write_cell(row, col, '') # , update_history=False) 1fghcde

148 self.EndBatch() 1fghcde

149 

150 def paste(self): 1ab

151 self._update_history() 1ijkl

152 self._clipboard_handler.paste() 1ijkl

153 

154 def delete(self): 1ab

155 self._update_history() 

156 _iscelleditcontrolshown = self.IsCellEditControlShown() 

157 if _iscelleditcontrolshown: 

158 if IS_WINDOWS: 

159 self._delete_from_cell_editor() 

160 else: 

161 self._clear_selected_cells() 

162 

163 def _delete_from_cell_editor(self): 1ab

164 editor = self.get_cell_edit_control() 

165 start, end = editor.Selection 

166 if start == end: 

167 end += 1 

168 editor.Remove(start, end) 

169 

170 def _is_whole_row_selection(self): 1ab

171 return self.SelectedRows 1aqrmnofghcpijklde

172 

173 def _is_whole_column_selection(self): 1ab

174 return self.SelectedCols 

175 

176 def get_cell_edit_control(self): 1ab

177 return self.GetCellEditor(*self.selection.cell).GetControl() 

178 

179 def get_selected_content(self): 1ab

180 return self._get_block_content(self.selection.rows(), self.selection.cols()) 1mnofghcpijklde

181 

182 def get_single_selection_content(self): 1ab

183 cells = self.get_selected_content() 

184 if len(cells) != 1 or len(cells[0]) != 1: 

185 return None 

186 return cells[0][0] 

187 

188 def _current_cell_value(self): 1ab

189 return self.GetCellValue(*self.selection.cell) 

190 

191 def _get_block_content(self, row_range, col_range): 1ab

192 try: 1mnofghcpijklde

193 content = [[self.GetCellValue(row, col) for col in col_range] for row in row_range] 1mnofghcpijklde

194 except RuntimeError: 

195 return [] 

196 return content 1mnofghcpijklde

197 

198 @staticmethod 1ab

199 def _strip_trailing_empty_cells(rowdata): 1ab

200 while rowdata and not rowdata[-1]: 

201 rowdata.pop() 

202 return rowdata 

203 

204 def undo(self): 1ab

205 prev_data = self._history.back() 1cde

206 if prev_data: 206 ↛ exitline 206 didn't return from function 'undo' because the condition on line 206 was always true1cde

207 self.ClearGrid() 1cde

208 self._write_data(prev_data, update_history=False) 1cde

209 

210 def _write_data(self, data, update_history=True): 1ab

211 self.BeginBatch() 1cde

212 for row_index, row_data in enumerate(data): 1cde

213 for col_index, cell_value in enumerate(row_data): 1cde

214 self.write_cell(row_index, col_index, cell_value, update_history) 1cde

215 self.EndBatch() 1cde

216 

217 def on_select_cell(self, event): 1ab

218 if self._is_whole_row_selection() or self._is_whole_column_selection(): 218 ↛ 219line 218 didn't jump to line 219 because the condition on line 218 was never true

219 self.SelectBlock(self.selection.topleft.row, self.selection.topleft.col, 

220 self.selection.bottomright.row, self.selection.bottomright.col, 

221 addToSelected=True) 

222 else: 

223 self.selection.set_from_single_selection(event) 

224 event.Skip() 

225 

226 def on_range_select(self, event): 1ab

227 if not event.Selecting(): 1qrmnofghcpijklde

228 self.selection.clear() 1qrmnofghcpijklde

229 return 1qrmnofghcpijklde

230 if event.ControlDown(): 230 ↛ 231line 230 didn't jump to line 231 because the condition on line 230 was never true1qrmnofghcpijklde

231 self.SetGridCursor(event.TopRow, event.LeftCol) 

232 self.SelectBlock(event.TopRow, event.LeftCol, 

233 event.BottomRow, event.RightCol, addToSelected=False) 

234 else: 

235 self.selection.set_from_range_selection(self, event) 1qrmnofghcpijklde

236 self._ensure_selected_row_is_visible(event.BottomRow) 1qrmnofghcpijklde

237 

238 def _ensure_selected_row_is_visible(self, bottom_row): 1ab

239 if not self.IsVisible(bottom_row, 0) and bottom_row < self.NumberRows and \ 239 ↛ 241line 239 didn't jump to line 241 because the condition on line 239 was never true1qrmnofghcpijklde

240 self._is_whole_row_selection(): 

241 self.MakeCellVisible(bottom_row, 0) 

242 

243 def on_cell_right_click(self, event): 1ab

244 if hasattr(event, 'Row') and hasattr(event, 'Col'): 

245 if (event.Row, event.Col) not in self.selection.cells(): 

246 self.select(event.Row, event.Col) 

247 self.selection.set_from_single_selection(event) 

248 self._popup_creator.show(self, PopupMenuItems(self, self._popup_items, self._popup_items_nt), 

249 self.get_selected_content()) 

250 

251 # DEBUG: This code is overriden at fieldeditors 

252 def on_insert_cells(self, event): 1ab

253 self._insert_or_delete_cells(self._insert_cells, event) 

254 

255 # DEBUG:This code is overriden at fieldeditors 

256 def on_delete_cells(self, event): 1ab

257 # print("DEBUG delete cells %s" % event) 

258 self._insert_or_delete_cells(self._delete_cells, event) 

259 

260 def _insert_or_delete_cells(self, action, event): 1ab

261 self._update_history() 

262 # print("DEBUG insert or delete selected %s" % self.selection.rows()) 

263 for index in self.selection.rows(): 

264 data = action(self._row_data(index)) 

265 self._write_row(index, data) 

266 self._refresh_layout() 

267 event.Skip() 

268 

269 def _insert_cells(self, data): 1ab

270 cols = self.selection.cols() 

271 left = right = cols[0] 

272 data[left:right] = [''] * len(cols) 

273 return self._strip_trailing_empty_cells(data) 

274 

275 def _delete_cells(self, data): 1ab

276 cols = self.selection.cols() 

277 # print("DEBUG delete cols selected %s" % cols) 

278 left, right = cols[0], cols[-1] # + 1 # DEBUG removed extra cell 

279 # print("DEBUG delete left, right (%d,%d) values %s" % (left, right, data[left:right])) 

280 data[left:right] = [] 

281 return data + [''] * len(cols) 

282 

283 def _row_data(self, row): 1ab

284 return [self.GetCellValue(row, col) for col in range(self.NumberCols)] 

285 

286 def _write_row(self, row, data): 1ab

287 for col, value in enumerate(data): 

288 self.write_cell(row, col, value, update_history=False) 

289 

290 def _refresh_layout(self): 1ab

291 self.SetFocus() 

292 self.SetGridCursor(*self.selection.cell) 

293 self.GetParent().Sizer.Layout() 

294 

295 

296# DEBUG: refactor this internal state away if possible 

297class _GridSelection(object): 1ab

298 cell = property(lambda self: (self.topleft.row, self.topleft.col)) 1ab

299 

300 def __init__(self, gridd): 1ab

301 self._set((0, 0)) 

302 self._grid = gridd 

303 

304 def _set(self, topleft, bottomright=None): 1ab

305 self.topleft = _Cell(topleft[0], topleft[1]) 1aqrmnofghcpijklde

306 self.bottomright = self._count_bottomright(topleft, bottomright) 1aqrmnofghcpijklde

307 

308 def _count_bottomright(self, topleft, bottomright): 1ab

309 if not bottomright: 1aqrmnofghcpijklde

310 return _Cell(topleft[0], topleft[1]) 1aqrmnofghcpijklde

311 return _Cell(min(self._grid.NumberRows - 1, bottomright[0]), 1qrmnofghcpijklde

312 min(self._grid.NumberCols - 1, bottomright[1])) 

313 

314 def set_from_single_selection(self, event): 1ab

315 self._set((event.Row, event.Col)) 

316 

317 def set_from_range_selection(self, gridd, event): 1ab

318 self._set(*self._get_bounding_coordinates(gridd, event)) 1qrmnofghcpijklde

319 

320 def clear(self): 1ab

321 selection = (self._grid.GetGridCursorRow(), self._grid.GetGridCursorCol()) 1qrmnofghcpijklde

322 self._set(selection) 1qrmnofghcpijklde

323 

324 @staticmethod 1ab

325 def _get_bounding_coordinates(gridd, event): 1ab

326 whole_row_selection = sorted(gridd.SelectedRows) 1qrmnofghcpijklde

327 if whole_row_selection: 327 ↛ 328line 327 didn't jump to line 328 because the condition on line 327 was never true1qrmnofghcpijklde

328 return (whole_row_selection[0], 0), \ 

329 (whole_row_selection[-1], gridd.NumberCols - 1) 

330 return (event.TopLeftCoords.Row, event.TopLeftCoords.Col), \ 1qrmnofghcpijklde

331 (event.BottomRightCoords.Row, event.BottomRightCoords.Col) 

332 

333 def rows(self): 1ab

334 """Returns a list containing indices of rows currently selected.""" 

335 return range(self.topleft.row, self.bottomright.row + 1) 1mnofghcpijklde

336 

337 def cols(self): 1ab

338 """Returns a list containing indices of columns currently selected.""" 

339 return range(self.topleft.col, self.bottomright.col + 1) 1mnofghcpijklde

340 

341 def cells(self): 1ab

342 """Return selected cells as a list of tuples (row, column).""" 

343 return [(row, col) for col in self.cols() 1fghcde

344 for row in self.rows()] 

345 

346 

347class _Cell(object): 1ab

348 

349 def __init__(self, row, col): 1ab

350 self.row = row 1aqrmnofghcpijklde

351 self.col = col 1aqrmnofghcpijklde

352 

353 def __iter__(self): 1ab

354 for item in self.row, self.col: 

355 yield item 

356 

357 

358class _GridState(object): 1ab

359 

360 def __init__(self): 1ab

361 self._back = [] 

362 self._forward = [] 

363 

364 def change(self, state): 1ab

365 if not self._back or state != self._back[-1]: 1fghcijklde

366 self._back.append(state) 1fghcijklde

367 self._forward = [] 1fghcijklde

368 

369 def back(self): 1ab

370 if not self._back: 370 ↛ 371line 370 didn't jump to line 371 because the condition on line 370 was never true1cde

371 return None 

372 self._forward.append(self._back.pop()) 1cde

373 return self._forward[-1] 1cde

374 

375 def forward(self): 1ab

376 if not self._forward: 

377 return None 

378 state = self._forward.pop() 

379 self._back.append(state) 

380 return state