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
@@ -75,6 +76,30 @@ class DocumentWithTableNavigation(TextContainerObject,ScriptableObject):
7576
7677 _lastTableSelection : Optional [_TableSelection ] = None
7778
79+ def _maybeGetLayoutTableIds (self , info : textInfos .TextInfo ):
80+ """
81+ If "Include layout tables" option is on, this will
82+ compute the set of layout tables that this textInfo is enclosed in,
83+ otherwise it will return empty set.
84+ @param info: the position where the layout tables should be looked for.
85+ @returns: A set of table IDs or empty set.
86+ """
87+ fields = list (info .getTextWithFields ())
88+ # If layout tables should not be reported, we should First record the ID of all layout tables,
89+ # so that we can skip them when searching for the deepest table
90+ layoutIDs = set ()
91+ if not config .conf ["documentFormatting" ]["includeLayoutTables" ]:
92+ for field in fields :
93+ if (
94+ isinstance (field , textInfos .FieldCommand )
95+ and field .command == "controlStart"
96+ and field .field .get ('table-layout' )
97+ ):
98+ tableID = field .field .get ('table-id' )
99+ if tableID is not None :
100+ layoutIDs .add (tableID )
101+ return layoutIDs
102+
78103 def _getTableCellCoords (self , info ):
79104 """
80105 Fetches information about the deepest table cell at the given position.
@@ -88,14 +113,7 @@ def _getTableCellCoords(self, info):
88113 info = info .copy ()
89114 info .expand (textInfos .UNIT_CHARACTER )
90115 fields = list (info .getTextWithFields ())
91- # 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
92- layoutIDs = set ()
93- if not config .conf ["documentFormatting" ]["includeLayoutTables" ]:
94- for field in fields :
95- if isinstance (field , textInfos .FieldCommand ) and field .command == "controlStart" and field .field .get ('table-layout' ):
96- tableID = field .field .get ('table-id' )
97- if tableID is not None :
98- layoutIDs .add (tableID )
116+ layoutIDs = self ._maybeGetLayoutTableIds (info )
99117 for field in reversed (fields ):
100118 if not (isinstance (field , textInfos .FieldCommand ) and field .command == "controlStart" ):
101119 # Not a control field.
@@ -112,6 +130,40 @@ def _getTableCellCoords(self, info):
112130 attrs ["table-rownumber" ], attrs ["table-columnnumber" ],
113131 attrs .get ("table-rowsspanned" , 1 ), attrs .get ("table-columnsspanned" , 1 ))
114132
133+ def _getTableDimensions (self , info : textInfos .TextInfo ) -> Tuple [int , int ]:
134+ """
135+ Fetches information about the deepest table dimension.
136+ @param info: the position where the table cell should be looked for.
137+ @returns: a tuple of table height and width.
138+ @raises: LookupError if there is no table cell at this position.
139+ """
140+ if info .isCollapsed :
141+ info = info .copy ()
142+ info .expand (textInfos .UNIT_CHARACTER )
143+ fields = list (info .getTextWithFields ())
144+ layoutIDs = self ._maybeGetLayoutTableIds (info )
145+ for field in reversed (fields ):
146+ if not (
147+ isinstance (field , textInfos .FieldCommand )
148+ and field .command == "controlStart"
149+ and field .field .get ("role" ) == controlTypes .Role .TABLE
150+ ):
151+ # Not a table control field.
152+ continue
153+ attrs = field .field
154+ tableID = attrs .get ('table-id' )
155+ if tableID is None or tableID in layoutIDs :
156+ continue
157+ break
158+ else :
159+ raise LookupError ("Not in a table cell" )
160+ try :
161+ nRows = int (attrs .get ("table-rowcount" ))
162+ nCols = int (attrs .get ("table-columncount" ))
163+ except (TypeError , ValueError ):
164+ raise LookupError ("Not in a table cell" )
165+ return (nRows , nCols )
166+
115167 def _getTableCellAt (self ,tableID ,startPos ,row ,column ):
116168 """
117169 Starting from the given start position, Locates the table cell with the given row and column coordinates and table ID.
@@ -161,10 +213,10 @@ def _getNearestTableCell(
161213 # Determine destination row and column.
162214 destRow = origRow
163215 destCol = origCol
164- if axis == "row" :
165- destRow += origRowSpan if movement == "next" else - 1
166- elif axis == "column" :
167- destCol += origColSpan if movement == "next" else - 1
216+ if axis == _Axis . ROW :
217+ destRow += origRowSpan if movement == _Movement . NEXT else - 1
218+ elif axis == _Axis . COLUMN :
219+ destCol += origColSpan if movement == _Movement . NEXT else - 1
168220
169221 # 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
170222 limit = self ._missingTableCellSearchLimit
@@ -177,12 +229,68 @@ def _getNearestTableCell(
177229 return self ._getTableCellAt (tableID ,startPos ,destRow ,destCol )
178230 except LookupError :
179231 pass
180- if axis == "row" :
181- destRow += 1 if movement == "next" else - 1
232+ if axis == _Axis . ROW :
233+ destRow += 1 if movement == _Movement . NEXT else - 1
182234 else :
183- destCol += 1 if movement == "next" else - 1
235+ destCol += 1 if movement == _Movement . NEXT else - 1
184236 raise LookupError
185237
238+ def _getFirstOrLastTableCell (
239+ self ,
240+ tableID : _TableID ,
241+ startPos : textInfos .TextInfo ,
242+ origRow : int ,
243+ origCol : int ,
244+ origRowSpan : int ,
245+ origColSpan : int ,
246+ movement : _Movement ,
247+ axis : _Axis
248+ ) -> textInfos .TextInfo :
249+ """
250+ Locates the first or last cell in current row or column given coordinates of current cell.
251+ When jumping to the first row/column, It will try to set current row/column index to 1.
252+ When jumping to the last row/column, it will query table dimensions and set row/column index
253+ to corresponding dimension.
254+ After figuring out exact coordinates of the cell it will try to jump directly to that cell,
255+ or if that fails (due to missing table cell), it will walk in the opposite direction skipping missing cells
256+ up to the number of times set by _missingTableCellSearchLimit set on this instance.
257+ @param tableID: the ID of the table
258+ @param startPos: the position in the document to start searching from.
259+ @param origRow: the row number of the starting cell
260+ @param origCol: the column number of the starting cell
261+ @param origRowSpan: the row span of the row of the starting cell
262+ @param origColSpan: the column span of the column of the starting cell
263+ @param movement: the direction ("first" or "last")
264+ @param axis: the axis of movement ("row" or "column")
265+ @returns: the position of the destination table cell
266+ """
267+ destRow , destCol = origRow , origCol
268+ if movement == _Movement .FIRST :
269+ if axis == _Axis .COLUMN :
270+ destCol = 1
271+ else :
272+ destRow = 1
273+ else :
274+ nRows , nCols = self ._getTableDimensions (startPos )
275+ if axis == _Axis .COLUMN :
276+ destCol = nCols
277+ else :
278+ destRow = nRows
279+ try :
280+ return self ._getTableCellAt (tableID , startPos , destRow , destCol )
281+ except LookupError :
282+ oppositeMovement = _Movement .PREVIOUS if movement == _Movement .LAST else _Movement .NEXT
283+ return self ._getNearestTableCell (
284+ tableID ,
285+ startPos ,
286+ destRow ,
287+ destCol ,
288+ origRowSpan = 1 ,
289+ origColSpan = 1 ,
290+ movement = oppositeMovement ,
291+ axis = axis
292+ )
293+
186294 def _tableMovementScriptHelper (
187295 self ,
188296 movement : _Movement = _Movement .NEXT ,
@@ -224,7 +332,30 @@ def _tableMovementScriptHelper(
224332 origRowSpan = self ._lastTableSelection .rowSpan
225333
226334 try :
227- info = self ._getNearestTableCell (tableID , self .selection , origRow , origCol , origRowSpan , origColSpan , movement , axis )
335+ if movement in {_Movement .PREVIOUS , _Movement .NEXT }:
336+ info = self ._getNearestTableCell (
337+ tableID ,
338+ self .selection ,
339+ origRow ,
340+ origCol ,
341+ origRowSpan ,
342+ origColSpan ,
343+ movement ,
344+ axis
345+ )
346+ elif movement in {_Movement .FIRST , _Movement .LAST }:
347+ info = self ._getFirstOrLastTableCell (
348+ tableID ,
349+ self .selection ,
350+ origRow ,
351+ origCol ,
352+ origRowSpan ,
353+ origColSpan ,
354+ movement ,
355+ axis
356+ )
357+ else :
358+ raise ValueError (f"Unknown movement { movement } " )
228359 newTableID , newRow , newCol , newRowSpan , newColSpan = self ._getTableCellCoords (info )
229360 except LookupError :
230361 # Translators: The message reported when a user attempts to use a table movement command
@@ -248,25 +379,45 @@ def _tableMovementScriptHelper(
248379 )
249380
250381 def script_nextRow (self , gesture ):
251- self ._tableMovementScriptHelper (axis = "row" , movement = "next" )
382+ self ._tableMovementScriptHelper (axis = _Axis . ROW , movement = _Movement . NEXT )
252383 # Translators: the description for the next table row script on browseMode documents.
253384 script_nextRow .__doc__ = _ ("moves to the next table row" )
254385
255386 def script_previousRow (self , gesture ):
256- self ._tableMovementScriptHelper (axis = "row" , movement = "previous" )
387+ self ._tableMovementScriptHelper (axis = _Axis . ROW , movement = _Movement . PREVIOUS )
257388 # Translators: the description for the previous table row script on browseMode documents.
258389 script_previousRow .__doc__ = _ ("moves to the previous table row" )
259390
260391 def script_nextColumn (self , gesture ):
261- self ._tableMovementScriptHelper (axis = "column" , movement = "next" )
392+ self ._tableMovementScriptHelper (axis = _Axis . COLUMN , movement = _Movement . NEXT )
262393 # Translators: the description for the next table column script on browseMode documents.
263394 script_nextColumn .__doc__ = _ ("moves to the next table column" )
264395
265396 def script_previousColumn (self , gesture ):
266- self ._tableMovementScriptHelper (axis = "column" , movement = "previous" )
397+ self ._tableMovementScriptHelper (axis = _Axis . COLUMN , movement = _Movement . PREVIOUS )
267398 # Translators: the description for the previous table column script on browseMode documents.
268399 script_previousColumn .__doc__ = _ ("moves to the previous table column" )
269400
401+ def script_firstRow (self , gesture ):
402+ self ._tableMovementScriptHelper (axis = _Axis .ROW , movement = _Movement .FIRST )
403+ # Translators: the description for the first table row script on browseMode documents.
404+ script_firstRow .__doc__ = _ ("moves to the first table row" )
405+
406+ def script_lastRow (self , gesture ):
407+ self ._tableMovementScriptHelper (axis = _Axis .ROW , movement = _Movement .LAST )
408+ # Translators: the description for the last table row script on browseMode documents.
409+ script_lastRow .__doc__ = _ ("moves to the last table row" )
410+
411+ def script_firstColumn (self , gesture ):
412+ self ._tableMovementScriptHelper (axis = _Axis .COLUMN , movement = _Movement .FIRST )
413+ # Translators: the description for the first table column script on browseMode documents.
414+ script_firstColumn .__doc__ = _ ("moves to the first table column" )
415+
416+ def script_lastColumn (self , gesture ):
417+ self ._tableMovementScriptHelper (axis = _Axis .COLUMN , movement = _Movement .LAST )
418+ # Translators: the description for the last table column script on browseMode documents.
419+ script_lastColumn .__doc__ = _ ("moves to the last table column" )
420+
270421 def script_toggleIncludeLayoutTables (self ,gesture ):
271422 # documentBase is a core module and should not depend on UI, so it is imported at run-time. (#12404)
272423 import ui
@@ -287,4 +438,8 @@ def script_toggleIncludeLayoutTables(self,gesture):
287438 "kb:control+alt+upArrow" : "previousRow" ,
288439 "kb:control+alt+rightArrow" : "nextColumn" ,
289440 "kb:control+alt+leftArrow" : "previousColumn" ,
441+ "kb:control+alt+pageUp" : "firstRow" ,
442+ "kb:control+alt+pageDown" : "lastRow" ,
443+ "kb:control+alt+Home" : "firstColumn" ,
444+ "kb:control+alt+End" : "lastColumn" ,
290445 }
0 commit comments