@@ -86,6 +86,15 @@ def _getTableCellAt(self,tableID,startPos,row,column):
8686 """
8787 raise NotImplementedError
8888
89+ def _getTableDimensions (self ,tableID ):
90+ """
91+ Returns height and width of the table.
92+ @param tableID: the ID of the table.
93+ @returns: Height and width of the table.
94+ @rtype: L{Tuple[int, int]}
95+ """
96+ raise NotImplementedError
97+
8998 _missingTableCellSearchLimit = 3 #: The number of missing cells L{_getNearestTableCell} is allowed to skip over to locate the next available cell
9099 def _getNearestTableCell (self , tableID , startPos , origRow , origCol , origRowSpan , origColSpan , movement , axis ):
91100 """
@@ -138,6 +147,94 @@ def _getNearestTableCell(self, tableID, startPos, origRow, origCol, origRowSpan,
138147 destCol += 1 if movement == "next" else - 1
139148 raise LookupError
140149
150+ def _probeTableDimension (self , tableID , startPos , origRow , origCol , axis ):
151+ """
152+ Measures the dimension (width or height) of current table along L{axis} by repeatedly trying
153+ to call L{_getTableCellAt} with ever increasing index along L{axis} to determine the largest possible value
154+ for which L{_getTableCellAt} doesn't raise L{LookupError}.
155+ @param tableID: the ID of the table
156+ @param startPos: the position in the document to start searching from.
157+ @type startPos: L{textInfos.TextInfo}
158+ @param origRow: the row number of the starting cell
159+ @type origRow: int
160+ @param origCol: the column number of the starting cell
161+ @type origCol: int
162+ @param axis: the axis of movement ("row" or "column")
163+ @type axis: string
164+ @returns: the measured dimension of the table along requested axis.
165+ @rtype: L{int}
166+ """
167+ result = origRow if axis == "row" else origCol
168+ row , col = origRow , origCol
169+ maxTableSize = 1000
170+ numLookupErrors = 0
171+ for i in range (maxTableSize ):
172+ if axis == "row" :
173+ row += 1
174+ newResult = row
175+ else :
176+ col += 1
177+ newResult = col
178+ try :
179+ self ._getTableCellAt (tableID , startPos , row , col )
180+ numLookupErrors = 0
181+ result = newResult
182+ except LookupError :
183+ numLookupErrors += 1
184+ if numLookupErrors >= self ._missingTableCellSearchLimit :
185+ break
186+ return result
187+
188+
189+ def _getFirstOrLastTableCell (self , tableID , startPos , origRow , origCol , origRowSpan , origColSpan , movement , axis ):
190+ """
191+ Locates the first or last cell in current row or column given coordinates of current cell.
192+ When jumping to the first row/column, It will try to set current row/column index to 1.
193+ When jumping to the last row/column, it will try to get table size if implementation of _getTableDimensions is provided,
194+ or otherwise, it will call _probeTableDimension to compute corresponding table dimension via brute force.
195+ After figuring out exact coordinates of the cell it will try to jump directly to that cell,
196+ or if that fails (due to missing table cell), it will walk in the opposite direction skipping missing cells
197+ up to the number of times set by _missingTableCellSearchLimit set on this instance.
198+ @param tableID: the ID of the table
199+ @param startPos: the position in the document to start searching from.
200+ @type startPos: L{textInfos.TextInfo}
201+ @param origRow: the row number of the starting cell
202+ @type origRow: int
203+ @param origCol: the column number of the starting cell
204+ @type origCol: int
205+ @param origRowSpan: the row span of the row of the starting cell
206+ @type origRowSpan: int
207+ @param origColSpan: the column span of the column of the starting cell
208+ @type origColSpan: int
209+ @param movement: the direction ("next" or "previous")
210+ @type movement: string
211+ @param axis: the axis of movement ("row" or "column")
212+ @type axis: string
213+ @returns: the position of the destination table cell
214+ @rtype: L{textInfos.TextInfo}
215+ """
216+ destRow , destCol = origRow , origCol
217+ if movement == "first" :
218+ if axis == "column" :
219+ destCol = 1
220+ else :
221+ destRow = 1
222+ else :
223+ try :
224+ nRows , nCols = self ._getTableDimensions (tableID )
225+ except (NotImplementedError , LookupError ):
226+ nRows , nCols = None , None
227+ probedDimension = self ._probeTableDimension (tableID , startPos , origRow , origCol , axis )
228+ if axis == "column" :
229+ destCol = nCols or probedDimension
230+ else :
231+ destRow = nRows or probedDimension
232+ try :
233+ return self ._getTableCellAt (tableID ,startPos ,destRow ,destCol )
234+ except LookupError :
235+ oppositeMovement = "previous" if movement == "last" else "next"
236+ return self ._getNearestTableCell (tableID , startPos , destRow , destCol , origRowSpan = 1 , origColSpan = 1 , movement = oppositeMovement , axis = axis )
237+
141238 def _tableMovementScriptHelper (self , movement = "next" , axis = None ):
142239 # documentBase is a core module and should not depend on these UI modules and so they are imported
143240 # at run-time. (#12404)
@@ -158,7 +255,10 @@ def _tableMovementScriptHelper(self, movement="next", axis=None):
158255 return
159256
160257 try :
161- info = self ._getNearestTableCell (tableID , self .selection , origRow , origCol , origRowSpan , origColSpan , movement , axis )
258+ if movement in {"previous" , "next" }:
259+ info = self ._getNearestTableCell (tableID , self .selection , origRow , origCol , origRowSpan , origColSpan , movement , axis )
260+ else :
261+ info = self ._getFirstOrLastTableCell (tableID , self .selection , origRow , origCol , origRowSpan , origColSpan , movement , axis )
162262 except LookupError :
163263 # Translators: The message reported when a user attempts to use a table movement command
164264 # but the cursor can't be moved in that direction because it is at the edge of the table.
@@ -189,7 +289,22 @@ def script_previousColumn(self, gesture):
189289 self ._tableMovementScriptHelper (axis = "column" , movement = "previous" )
190290 # Translators: the description for the previous table column script on browseMode documents.
191291 script_previousColumn .__doc__ = _ ("moves to the previous table column" )
192-
292+ def script_firstRow (self , gesture ):
293+ self ._tableMovementScriptHelper (axis = "row" , movement = "first" )
294+ # Translators: the description for the first table row script on browseMode documents.
295+ script_firstRow .__doc__ = _ ("moves to the first table row" )
296+ def script_lastRow (self , gesture ):
297+ self ._tableMovementScriptHelper (axis = "row" , movement = "last" )
298+ # Translators: the description for the last table row script on browseMode documents.
299+ script_lastRow .__doc__ = _ ("moves to the last table row" )
300+ def script_firstColumn (self , gesture ):
301+ self ._tableMovementScriptHelper (axis = "column" , movement = "first" )
302+ # Translators: the description for the first table column script on browseMode documents.
303+ script_firstColumn .__doc__ = _ ("moves to the first table column" )
304+ def script_lastColumn (self , gesture ):
305+ self ._tableMovementScriptHelper (axis = "column" , movement = "last" )
306+ # Translators: the description for the last table column script on browseMode documents.
307+ script_lastColumn .__doc__ = _ ("moves to the last table column" )
193308 def script_toggleIncludeLayoutTables (self ,gesture ):
194309 # documentBase is a core module and should not depend on UI, so it is imported at run-time. (#12404)
195310 import ui
@@ -210,4 +325,8 @@ def script_toggleIncludeLayoutTables(self,gesture):
210325 "kb:control+alt+upArrow" : "previousRow" ,
211326 "kb:control+alt+rightArrow" : "nextColumn" ,
212327 "kb:control+alt+leftArrow" : "previousColumn" ,
328+ "kb:control+alt+pageUp" : "firstRow" ,
329+ "kb:control+alt+pageDown" : "lastRow" ,
330+ "kb:control+alt+Home" : "firstColumn" ,
331+ "kb:control+alt+End" : "lastColumn" ,
213332 }
0 commit comments