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
« 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 import grid, Colour 1ab
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
25_ = wx.GetTranslation # To keep linter/code analyser happy 1ab
26builtins.__dict__['_'] = wx.GetTranslation 1ab
29class GridEditor(grid.Grid): 1ab
30 _col_add_threshold = 6 1ab
31 _popup_items = [] 1ab
33 _popup_items_nt = [] 1ab
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']
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")
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)
86 def register_context_menu_hook(self, cb): 1ab
87 self._popup_creator.add_hook(cb)
89 def unregister_context_menu_hook(self, cb): 1ab
90 self._popup_creator.remove_hook(cb)
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
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
109 def has_focus(self): 1ab
110 return self.FindFocus() == self
112 def _update_history(self): 1ab
113 self._history.change(self._get_all_content()) 1fghcijklde
115 def _get_all_content(self): 1ab
116 return self._get_block_content(range(self.NumberRows), 1fghcijklde
117 range(self.NumberCols))
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))
126 def select(self, row, column): 1ab
127 self.SelectBlock(row, column, row, column)
128 self.SetGridCursor(row, column)
129 self.MakeCellVisible(row, column)
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
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
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
150 def paste(self): 1ab
151 self._update_history() 1ijkl
152 self._clipboard_handler.paste() 1ijkl
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()
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)
170 def _is_whole_row_selection(self): 1ab
171 return self.SelectedRows 1aqrmnofghcpijklde
173 def _is_whole_column_selection(self): 1ab
174 return self.SelectedCols
176 def get_cell_edit_control(self): 1ab
177 return self.GetCellEditor(*self.selection.cell).GetControl()
179 def get_selected_content(self): 1ab
180 return self._get_block_content(self.selection.rows(), self.selection.cols()) 1mnofghcpijklde
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]
188 def _current_cell_value(self): 1ab
189 return self.GetCellValue(*self.selection.cell)
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
198 @staticmethod 1ab
199 def _strip_trailing_empty_cells(rowdata): 1ab
200 while rowdata and not rowdata[-1]:
201 rowdata.pop()
202 return rowdata
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
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
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()
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
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)
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())
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)
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)
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()
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)
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)
283 def _row_data(self, row): 1ab
284 return [self.GetCellValue(row, col) for col in range(self.NumberCols)]
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)
290 def _refresh_layout(self): 1ab
291 self.SetFocus()
292 self.SetGridCursor(*self.selection.cell)
293 self.GetParent().Sizer.Layout()
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
300 def __init__(self, gridd): 1ab
301 self._set((0, 0))
302 self._grid = gridd
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
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]))
314 def set_from_single_selection(self, event): 1ab
315 self._set((event.Row, event.Col))
317 def set_from_range_selection(self, gridd, event): 1ab
318 self._set(*self._get_bounding_coordinates(gridd, event)) 1qrmnofghcpijklde
320 def clear(self): 1ab
321 selection = (self._grid.GetGridCursorRow(), self._grid.GetGridCursorCol()) 1qrmnofghcpijklde
322 self._set(selection) 1qrmnofghcpijklde
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)
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
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
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()]
347class _Cell(object): 1ab
349 def __init__(self, row, col): 1ab
350 self.row = row 1aqrmnofghcpijklde
351 self.col = col 1aqrmnofghcpijklde
353 def __iter__(self): 1ab
354 for item in self.row, self.col:
355 yield item
358class _GridState(object): 1ab
360 def __init__(self): 1ab
361 self._back = []
362 self._forward = []
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
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
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