77import config
88import textInfos
99import controlTypes
10-
10+ from typing import Tuple
1111
1212class TextContainerObject (AutoPropertyObject ):
1313 """
@@ -33,6 +33,30 @@ class DocumentWithTableNavigation(TextContainerObject,ScriptableObject):
3333 The document could be an NVDAObject, or a BrowseModeDocument treeIntercepter for example.
3434 """
3535
36+ def _maybeGetLayoutTableIds (self , info : textInfos .TextInfo ):
37+ """
38+ If "Include layout tables" option is on, this will
39+ compute the set of layout tables that this textInfo is enclosed in,
40+ otherwise it will return empty set.
41+ @param info: the position where the layout tables should be looked for.
42+ @returns: A set of table IDs or empty set.
43+ """
44+ fields = list (info .getTextWithFields ())
45+ # If layout tables should not be reported, we should First record the ID of all layout tables,
46+ # so that we can skip them when searching for the deepest table
47+ layoutIDs = set ()
48+ if not config .conf ["documentFormatting" ]["includeLayoutTables" ]:
49+ for field in fields :
50+ if (
51+ isinstance (field , textInfos .FieldCommand )
52+ and field .command == "controlStart"
53+ and field .field .get ('table-layout' )
54+ ):
55+ tableID = field .field .get ('table-id' )
56+ if tableID is not None :
57+ layoutIDs .add (tableID )
58+ return layoutIDs
59+
3660 def _getTableCellCoords (self , info ):
3761 """
3862 Fetches information about the deepest table cell at the given position.
@@ -46,14 +70,7 @@ def _getTableCellCoords(self, info):
4670 info = info .copy ()
4771 info .expand (textInfos .UNIT_CHARACTER )
4872 fields = list (info .getTextWithFields ())
49- # 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
50- layoutIDs = set ()
51- if not config .conf ["documentFormatting" ]["includeLayoutTables" ]:
52- for field in fields :
53- if isinstance (field , textInfos .FieldCommand ) and field .command == "controlStart" and field .field .get ('table-layout' ):
54- tableID = field .field .get ('table-id' )
55- if tableID is not None :
56- layoutIDs .add (tableID )
73+ layoutIDs = self ._maybeGetLayoutTableIds (info )
5774 for field in reversed (fields ):
5875 if not (isinstance (field , textInfos .FieldCommand ) and field .command == "controlStart" ):
5976 # Not a control field.
@@ -70,6 +87,40 @@ def _getTableCellCoords(self, info):
7087 attrs ["table-rownumber" ], attrs ["table-columnnumber" ],
7188 attrs .get ("table-rowsspanned" , 1 ), attrs .get ("table-columnsspanned" , 1 ))
7289
90+ def _getTableDimensions (self , info : textInfos .TextInfo ) -> Tuple [int , int ]:
91+ """
92+ Fetches information about the deepest table dimension.
93+ @param info: the position where the table cell should be looked for.
94+ @returns: a tuple of table height and width.
95+ @raises: LookupError if there is no table cell at this position.
96+ """
97+ if info .isCollapsed :
98+ info = info .copy ()
99+ info .expand (textInfos .UNIT_CHARACTER )
100+ fields = list (info .getTextWithFields ())
101+ layoutIDs = self ._maybeGetLayoutTableIds (info )
102+ for field in reversed (fields ):
103+ if not (
104+ isinstance (field , textInfos .FieldCommand )
105+ and field .command == "controlStart"
106+ and field .field .get ("role" ) == controlTypes .Role .TABLE
107+ ):
108+ # Not a table control field.
109+ continue
110+ attrs = field .field
111+ tableID = attrs .get ('table-id' )
112+ if tableID is None or tableID in layoutIDs :
113+ continue
114+ break
115+ else :
116+ raise LookupError ("Not in a table cell" )
117+ try :
118+ nRows = int (attrs .get ("table-rowcount" ))
119+ nCols = int (attrs .get ("table-columncount" ))
120+ except (TypeError , ValueError ):
121+ raise LookupError ("Not in a table cell" )
122+ return (nRows , nCols )
123+
73124 def _getTableCellAt (self ,tableID ,startPos ,row ,column ):
74125 """
75126 Starting from the given start position, Locates the table cell with the given row and column coordinates and table ID.
@@ -138,6 +189,62 @@ def _getNearestTableCell(self, tableID, startPos, origRow, origCol, origRowSpan,
138189 destCol += 1 if movement == "next" else - 1
139190 raise LookupError
140191
192+ def _getFirstOrLastTableCell (
193+ self ,
194+ tableID ,
195+ startPos : textInfos .TextInfo ,
196+ origRow : int ,
197+ origCol : int ,
198+ origRowSpan : int ,
199+ origColSpan : int ,
200+ movement : str ,
201+ axis : str
202+ ) -> textInfos .TextInfo :
203+ """
204+ Locates the first or last cell in current row or column given coordinates of current cell.
205+ When jumping to the first row/column, It will try to set current row/column index to 1.
206+ When jumping to the last row/column, it will query table dimensions and set row/column index
207+ to corresponding dimension.
208+ After figuring out exact coordinates of the cell it will try to jump directly to that cell,
209+ or if that fails (due to missing table cell), it will walk in the opposite direction skipping missing cells
210+ up to the number of times set by _missingTableCellSearchLimit set on this instance.
211+ @param tableID: the ID of the table
212+ @param startPos: the position in the document to start searching from.
213+ @param origRow: the row number of the starting cell
214+ @param origCol: the column number of the starting cell
215+ @param origRowSpan: the row span of the row of the starting cell
216+ @param origColSpan: the column span of the column of the starting cell
217+ @param movement: the direction ("next" or "previous")
218+ @param axis: the axis of movement ("row" or "column")
219+ @returns: the position of the destination table cell
220+ """
221+ destRow , destCol = origRow , origCol
222+ if movement == "first" :
223+ if axis == "column" :
224+ destCol = 1
225+ else :
226+ destRow = 1
227+ else :
228+ nRows , nCols = self ._getTableDimensions (startPos )
229+ if axis == "column" :
230+ destCol = nCols
231+ else :
232+ destRow = nRows
233+ try :
234+ return self ._getTableCellAt (tableID , startPos , destRow , destCol )
235+ except LookupError :
236+ oppositeMovement = "previous" if movement == "last" else "next"
237+ return self ._getNearestTableCell (
238+ tableID ,
239+ startPos ,
240+ destRow ,
241+ destCol ,
242+ origRowSpan = 1 ,
243+ origColSpan = 1 ,
244+ movement = oppositeMovement ,
245+ axis = axis
246+ )
247+
141248 def _tableMovementScriptHelper (self , movement = "next" , axis = None ):
142249 # documentBase is a core module and should not depend on these UI modules and so they are imported
143250 # at run-time. (#12404)
@@ -158,7 +265,28 @@ def _tableMovementScriptHelper(self, movement="next", axis=None):
158265 return
159266
160267 try :
161- info = self ._getNearestTableCell (tableID , self .selection , origRow , origCol , origRowSpan , origColSpan , movement , axis )
268+ if movement in {"previous" , "next" }:
269+ info = self ._getNearestTableCell (
270+ tableID ,
271+ self .selection ,
272+ origRow ,
273+ origCol ,
274+ origRowSpan ,
275+ origColSpan ,
276+ movement ,
277+ axis
278+ )
279+ else :
280+ info = self ._getFirstOrLastTableCell (
281+ tableID ,
282+ self .selection ,
283+ origRow ,
284+ origCol ,
285+ origRowSpan ,
286+ origColSpan ,
287+ movement ,
288+ axis
289+ )
162290 except LookupError :
163291 # Translators: The message reported when a user attempts to use a table movement command
164292 # but the cursor can't be moved in that direction because it is at the edge of the table.
@@ -190,6 +318,26 @@ def script_previousColumn(self, gesture):
190318 # Translators: the description for the previous table column script on browseMode documents.
191319 script_previousColumn .__doc__ = _ ("moves to the previous table column" )
192320
321+ def script_firstRow (self , gesture ):
322+ self ._tableMovementScriptHelper (axis = "row" , movement = "first" )
323+ # Translators: the description for the first table row script on browseMode documents.
324+ script_firstRow .__doc__ = _ ("moves to the first table row" )
325+
326+ def script_lastRow (self , gesture ):
327+ self ._tableMovementScriptHelper (axis = "row" , movement = "last" )
328+ # Translators: the description for the last table row script on browseMode documents.
329+ script_lastRow .__doc__ = _ ("moves to the last table row" )
330+
331+ def script_firstColumn (self , gesture ):
332+ self ._tableMovementScriptHelper (axis = "column" , movement = "first" )
333+ # Translators: the description for the first table column script on browseMode documents.
334+ script_firstColumn .__doc__ = _ ("moves to the first table column" )
335+
336+ def script_lastColumn (self , gesture ):
337+ self ._tableMovementScriptHelper (axis = "column" , movement = "last" )
338+ # Translators: the description for the last table column script on browseMode documents.
339+ script_lastColumn .__doc__ = _ ("moves to the last table column" )
340+
193341 def script_toggleIncludeLayoutTables (self ,gesture ):
194342 # documentBase is a core module and should not depend on UI, so it is imported at run-time. (#12404)
195343 import ui
@@ -210,4 +358,8 @@ def script_toggleIncludeLayoutTables(self,gesture):
210358 "kb:control+alt+upArrow" : "previousRow" ,
211359 "kb:control+alt+rightArrow" : "nextColumn" ,
212360 "kb:control+alt+leftArrow" : "previousColumn" ,
361+ "kb:control+alt+pageUp" : "firstRow" ,
362+ "kb:control+alt+pageDown" : "lastRow" ,
363+ "kb:control+alt+Home" : "firstColumn" ,
364+ "kb:control+alt+End" : "lastColumn" ,
213365 }
0 commit comments