1111import textInfos
1212import controlTypes
1313
14-
1514_TableID = Union [int , Tuple , Any ]
1615"""
1716A variety of types can be used for a tableID.
@@ -27,6 +26,8 @@ class _Axis(str, Enum):
2726class _Movement (str , Enum ):
2827 NEXT = "next"
2928 PREVIOUS = "previous"
29+ FIRST = "first"
30+ LAST = "last"
3031
3132
3233@dataclass
@@ -67,6 +68,30 @@ class DocumentWithTableNavigation(TextContainerObject,ScriptableObject):
6768
6869 _lastTableSelection : Optional [_TableSelection ] = None
6970
71+ def _maybeGetLayoutTableIds (self , info : textInfos .TextInfo ):
72+ """
73+ If "Include layout tables" option is on, this will
74+ compute the set of layout tables that this textInfo is enclosed in,
75+ otherwise it will return empty set.
76+ @param info: the position where the layout tables should be looked for.
77+ @returns: A set of table IDs or empty set.
78+ """
79+ fields = list (info .getTextWithFields ())
80+ # If layout tables should not be reported, we should First record the ID of all layout tables,
81+ # so that we can skip them when searching for the deepest table
82+ layoutIDs = set ()
83+ if not config .conf ["documentFormatting" ]["includeLayoutTables" ]:
84+ for field in fields :
85+ if (
86+ isinstance (field , textInfos .FieldCommand )
87+ and field .command == "controlStart"
88+ and field .field .get ('table-layout' )
89+ ):
90+ tableID = field .field .get ('table-id' )
91+ if tableID is not None :
92+ layoutIDs .add (tableID )
93+ return layoutIDs
94+
7095 def _getTableCellCoords (self , info ):
7196 """
7297 Fetches information about the deepest table cell at the given position.
@@ -80,14 +105,7 @@ def _getTableCellCoords(self, info):
80105 info = info .copy ()
81106 info .expand (textInfos .UNIT_CHARACTER )
82107 fields = list (info .getTextWithFields ())
83- # If layout tables should not be reported, we should First record the ID of all layout tables so that we can skip them when searching for the deepest table
84- layoutIDs = set ()
85- if not config .conf ["documentFormatting" ]["includeLayoutTables" ]:
86- for field in fields :
87- if isinstance (field , textInfos .FieldCommand ) and field .command == "controlStart" and field .field .get ('table-layout' ):
88- tableID = field .field .get ('table-id' )
89- if tableID is not None :
90- layoutIDs .add (tableID )
108+ layoutIDs = self ._maybeGetLayoutTableIds (info )
91109 for field in reversed (fields ):
92110 if not (isinstance (field , textInfos .FieldCommand ) and field .command == "controlStart" ):
93111 # Not a control field.
@@ -104,6 +122,40 @@ def _getTableCellCoords(self, info):
104122 attrs ["table-rownumber" ], attrs ["table-columnnumber" ],
105123 attrs .get ("table-rowsspanned" , 1 ), attrs .get ("table-columnsspanned" , 1 ))
106124
125+ def _getTableDimensions (self , info : textInfos .TextInfo ) -> Tuple [int , int ]:
126+ """
127+ Fetches information about the deepest table dimension.
128+ @param info: the position where the table cell should be looked for.
129+ @returns: a tuple of table height and width.
130+ @raises: LookupError if there is no table cell at this position.
131+ """
132+ if info .isCollapsed :
133+ info = info .copy ()
134+ info .expand (textInfos .UNIT_CHARACTER )
135+ fields = list (info .getTextWithFields ())
136+ layoutIDs = self ._maybeGetLayoutTableIds (info )
137+ for field in reversed (fields ):
138+ if not (
139+ isinstance (field , textInfos .FieldCommand )
140+ and field .command == "controlStart"
141+ and field .field .get ("role" ) == controlTypes .Role .TABLE
142+ ):
143+ # Not a table control field.
144+ continue
145+ attrs = field .field
146+ tableID = attrs .get ('table-id' )
147+ if tableID is None or tableID in layoutIDs :
148+ continue
149+ break
150+ else :
151+ raise LookupError ("Not in a table cell" )
152+ try :
153+ nRows = int (attrs .get ("table-rowcount" ))
154+ nCols = int (attrs .get ("table-columncount" ))
155+ except (TypeError , ValueError ):
156+ raise LookupError ("Not in a table cell" )
157+ return (nRows , nCols )
158+
107159 def _getTableCellAt (self ,tableID ,startPos ,row ,column ):
108160 """
109161 Starting from the given start position, Locates the table cell with the given row and column coordinates and table ID.
@@ -153,10 +205,10 @@ def _getNearestTableCell(
153205 # Determine destination row and column.
154206 destRow = origRow
155207 destCol = origCol
156- if axis == "row" :
157- destRow += origRowSpan if movement == "next" else - 1
158- elif axis == "column" :
159- destCol += origColSpan if movement == "next" else - 1
208+ if axis == _Axis . ROW :
209+ destRow += origRowSpan if movement == _Movement . NEXT else - 1
210+ elif axis == _Axis . COLUMN :
211+ destCol += origColSpan if movement == _Movement . NEXT else - 1
160212
161213 # Try and fetch the cell at these coordinates, though if a cell is missing, try several more times moving the coordinates on by one cell each time
162214 limit = self ._missingTableCellSearchLimit
@@ -169,12 +221,68 @@ def _getNearestTableCell(
169221 return self ._getTableCellAt (tableID ,startPos ,destRow ,destCol )
170222 except LookupError :
171223 pass
172- if axis == "row" :
173- destRow += 1 if movement == "next" else - 1
224+ if axis == _Axis . ROW :
225+ destRow += 1 if movement == _Movement . NEXT else - 1
174226 else :
175- destCol += 1 if movement == "next" else - 1
227+ destCol += 1 if movement == _Movement . NEXT else - 1
176228 raise LookupError
177229
230+ def _getFirstOrLastTableCell (
231+ self ,
232+ tableID : _TableID ,
233+ startPos : textInfos .TextInfo ,
234+ origRow : int ,
235+ origCol : int ,
236+ origRowSpan : int ,
237+ origColSpan : int ,
238+ movement : str ,
239+ axis : str
240+ ) -> textInfos .TextInfo :
241+ """
242+ Locates the first or last cell in current row or column given coordinates of current cell.
243+ When jumping to the first row/column, It will try to set current row/column index to 1.
244+ When jumping to the last row/column, it will query table dimensions and set row/column index
245+ to corresponding dimension.
246+ After figuring out exact coordinates of the cell it will try to jump directly to that cell,
247+ or if that fails (due to missing table cell), it will walk in the opposite direction skipping missing cells
248+ up to the number of times set by _missingTableCellSearchLimit set on this instance.
249+ @param tableID: the ID of the table
250+ @param startPos: the position in the document to start searching from.
251+ @param origRow: the row number of the starting cell
252+ @param origCol: the column number of the starting cell
253+ @param origRowSpan: the row span of the row of the starting cell
254+ @param origColSpan: the column span of the column of the starting cell
255+ @param movement: the direction ("next" or "previous")
256+ @param axis: the axis of movement ("row" or "column")
257+ @returns: the position of the destination table cell
258+ """
259+ destRow , destCol = origRow , origCol
260+ if movement == _Movement .FIRST :
261+ if axis == _Axis .COLUMN :
262+ destCol = 1
263+ else :
264+ destRow = 1
265+ else :
266+ nRows , nCols = self ._getTableDimensions (startPos )
267+ if axis == _Axis .COLUMN :
268+ destCol = nCols
269+ else :
270+ destRow = nRows
271+ try :
272+ return self ._getTableCellAt (tableID , startPos , destRow , destCol )
273+ except LookupError :
274+ oppositeMovement = _Movement .PREVIOUS if movement == _Movement .LAST else _Movement .NEXT
275+ return self ._getNearestTableCell (
276+ tableID ,
277+ startPos ,
278+ destRow ,
279+ destCol ,
280+ origRowSpan = 1 ,
281+ origColSpan = 1 ,
282+ movement = oppositeMovement ,
283+ axis = axis
284+ )
285+
178286 def _tableMovementScriptHelper (
179287 self ,
180288 movement : _Movement = _Movement .NEXT ,
@@ -215,7 +323,30 @@ def _tableMovementScriptHelper(
215323 origRowSpan = self ._lastTableSelection .rowSpan
216324
217325 try :
218- info = self ._getNearestTableCell (tableID , self .selection , origRow , origCol , origRowSpan , origColSpan , movement , axis )
326+ if movement in {_Movement .PREVIOUS , _Movement .NEXT }:
327+ info = self ._getNearestTableCell (
328+ tableID ,
329+ self .selection ,
330+ origRow ,
331+ origCol ,
332+ origRowSpan ,
333+ origColSpan ,
334+ movement ,
335+ axis
336+ )
337+ elif movement in {_Movement .FIRST , _Movement .LAST }:
338+ info = self ._getFirstOrLastTableCell (
339+ tableID ,
340+ self .selection ,
341+ origRow ,
342+ origCol ,
343+ origRowSpan ,
344+ origColSpan ,
345+ movement ,
346+ axis
347+ )
348+ else :
349+ raise ValueError (f"Unknown movement { movement } " )
219350 except LookupError :
220351 # Translators: The message reported when a user attempts to use a table movement command
221352 # but the cursor can't be moved in that direction because it is at the edge of the table.
@@ -236,25 +367,45 @@ def _tableMovementScriptHelper(
236367 )
237368
238369 def script_nextRow (self , gesture ):
239- self ._tableMovementScriptHelper (axis = "row" , movement = "next" )
370+ self ._tableMovementScriptHelper (axis = _Axis . ROW , movement = _Movement . NEXT )
240371 # Translators: the description for the next table row script on browseMode documents.
241372 script_nextRow .__doc__ = _ ("moves to the next table row" )
242373
243374 def script_previousRow (self , gesture ):
244- self ._tableMovementScriptHelper (axis = "row" , movement = "previous" )
375+ self ._tableMovementScriptHelper (axis = _Axis . ROW , movement = _Movement . PREVIOUS )
245376 # Translators: the description for the previous table row script on browseMode documents.
246377 script_previousRow .__doc__ = _ ("moves to the previous table row" )
247378
248379 def script_nextColumn (self , gesture ):
249- self ._tableMovementScriptHelper (axis = "column" , movement = "next" )
380+ self ._tableMovementScriptHelper (axis = _Axis . COLUMN , movement = _Movement . NEXT )
250381 # Translators: the description for the next table column script on browseMode documents.
251382 script_nextColumn .__doc__ = _ ("moves to the next table column" )
252383
253384 def script_previousColumn (self , gesture ):
254- self ._tableMovementScriptHelper (axis = "column" , movement = "previous" )
385+ self ._tableMovementScriptHelper (axis = _Axis . COLUMN , movement = _Movement . PREVIOUS )
255386 # Translators: the description for the previous table column script on browseMode documents.
256387 script_previousColumn .__doc__ = _ ("moves to the previous table column" )
257388
389+ def script_firstRow (self , gesture ):
390+ self ._tableMovementScriptHelper (axis = _Axis .ROW , movement = _Movement .FIRST )
391+ # Translators: the description for the first table row script on browseMode documents.
392+ script_firstRow .__doc__ = _ ("moves to the first table row" )
393+
394+ def script_lastRow (self , gesture ):
395+ self ._tableMovementScriptHelper (axis = _Axis .ROW , movement = _Movement .LAST )
396+ # Translators: the description for the last table row script on browseMode documents.
397+ script_lastRow .__doc__ = _ ("moves to the last table row" )
398+
399+ def script_firstColumn (self , gesture ):
400+ self ._tableMovementScriptHelper (axis = _Axis .COLUMN , movement = _Movement .FIRST )
401+ # Translators: the description for the first table column script on browseMode documents.
402+ script_firstColumn .__doc__ = _ ("moves to the first table column" )
403+
404+ def script_lastColumn (self , gesture ):
405+ self ._tableMovementScriptHelper (axis = _Axis .COLUMN , movement = _Movement .LAST )
406+ # Translators: the description for the last table column script on browseMode documents.
407+ script_lastColumn .__doc__ = _ ("moves to the last table column" )
408+
258409 def script_toggleIncludeLayoutTables (self ,gesture ):
259410 # documentBase is a core module and should not depend on UI, so it is imported at run-time. (#12404)
260411 import ui
@@ -275,4 +426,8 @@ def script_toggleIncludeLayoutTables(self,gesture):
275426 "kb:control+alt+upArrow" : "previousRow" ,
276427 "kb:control+alt+rightArrow" : "nextColumn" ,
277428 "kb:control+alt+leftArrow" : "previousColumn" ,
429+ "kb:control+alt+pageUp" : "firstRow" ,
430+ "kb:control+alt+pageDown" : "lastRow" ,
431+ "kb:control+alt+Home" : "firstColumn" ,
432+ "kb:control+alt+End" : "lastColumn" ,
278433 }
0 commit comments