1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 * This file incorporates work covered by the following license notice:
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
19 #ifndef INCLUDED_SW_INC_SWTABLE_HXX
20 #define INCLUDED_SW_INC_SWTABLE_HXX
22 #include <tools/solar.h>
23 #include <tools/ref.hxx>
24 #include "tblenum.hxx"
25 #include "swtypes.hxx"
28 #include "swtblfmt.hxx"
33 #include <o3tl/sorted_vector.hxx>
38 class SwHTMLTableLayout
;
45 class SwTableCalcPara
;
50 class SwUndoTableMerge
;
53 class SwUndoTableCpyTable
;
59 void sw_GetTableBoxColStr( sal_uInt16 nCol
, OUString
& rNm
);
63 std::vector
<SwTableLine
*> m_vLines
;
66 typedef std::vector
<SwTableLine
*>::size_type size_type
;
67 typedef std::vector
<SwTableLine
*>::iterator iterator
;
68 typedef std::vector
<SwTableLine
*>::const_iterator const_iterator
;
70 // free's any remaining child objects
73 bool empty() const { return m_vLines
.empty(); }
74 size_type
size() const { return m_vLines
.size(); }
75 iterator
begin() { return m_vLines
.begin(); }
76 const_iterator
begin() const { return m_vLines
.begin(); }
77 iterator
end() { return m_vLines
.end(); }
78 const_iterator
end() const { return m_vLines
.end(); }
79 SwTableLine
* front() const { return m_vLines
.front(); }
80 SwTableLine
* back() const { return m_vLines
.back(); }
81 void clear() { m_vLines
.clear(); }
82 iterator
erase( iterator aIt
) { return m_vLines
.erase( aIt
); }
83 iterator
erase( iterator aFirst
, iterator aLast
) { return m_vLines
.erase( aFirst
, aLast
); }
84 iterator
insert( iterator aIt
, SwTableLine
* pLine
) { return m_vLines
.insert( aIt
, pLine
); }
85 template<typename TInputIterator
>
86 void insert( iterator aIt
, TInputIterator aFirst
, TInputIterator aLast
)
88 m_vLines
.insert( aIt
, aFirst
, aLast
);
90 void push_back( SwTableLine
* pLine
) { m_vLines
.push_back( pLine
); }
91 void reserve( size_type nSize
) { m_vLines
.reserve( nSize
); }
92 SwTableLine
*& operator[]( size_type nPos
) { return m_vLines
[ nPos
]; }
93 SwTableLine
* operator[]( size_type nPos
) const { return m_vLines
[ nPos
]; }
95 // return USHRT_MAX if not found, else index of position
96 sal_uInt16
GetPos(const SwTableLine
* pBox
) const
98 const_iterator it
= std::find(begin(), end(), pBox
);
99 return it
== end() ? USHRT_MAX
: it
- begin();
103 using SwTableBoxes
= std::vector
<SwTableBox
*>;
105 // Save content-bearing box-pointers additionally in a sorted array
106 // (for calculation in table).
107 class SwTableSortBoxes
: public o3tl::sorted_vector
<SwTableBox
*> {};
109 /// SwTable is one table in the document model, containing rows (which contain cells).
110 class SW_DLLPUBLIC SwTable
: public SwClient
//Client of FrameFormat.
114 SwTableLines m_aLines
;
115 SwTableSortBoxes m_TabSortContentBoxes
;
116 tools::SvRef
<SwServerObject
> m_xRefObj
; // In case DataServer -> pointer is set.
118 std::shared_ptr
<SwHTMLTableLayout
> m_xHTMLLayout
;
120 // Usually, the table node of a SwTable can be accessed by getting a box
121 // out of m_TabSortContentBoxes, which know their SwStartNode. But in some rare
122 // cases, we need to know the table node of a SwTable, before the table
123 // boxes have been build (SwTableNode::MakeCopy with tables in tables).
124 SwTableNode
* m_pTableNode
;
126 // Should that be adjustable for every table?
127 TableChgMode m_eTableChgMode
;
129 sal_uInt16 m_nGraphicsThatResize
; // Count of Grfs that initiate a resize of table
131 sal_uInt16 m_nRowsToRepeat
; // Number of rows to repeat on every page.
133 /// Name of the table style to be applied on this table.
134 OUString maTableStyleName
;
136 bool m_bModifyLocked
:1;
137 bool m_bNewModel
:1; // false: old SubTableModel; true: new RowSpanModel
139 virtual void Modify( const SfxPoolItem
* pOld
, const SfxPoolItem
* pNew
) override
;
144 SEARCH_NONE
, // Default: expand to rectangle
145 SEARCH_ROW
, // row selection
146 SEARCH_COL
// column selection
151 virtual ~SwTable() override
;
153 // @@@ public copy ctor, but no copy assignment?
154 SwTable( const SwTable
& rTable
); // no copy of the lines !!
156 // @@@ public copy ctor, but no copy assignment?
157 SwTable
& operator= (const SwTable
&) = delete;
158 bool OldMerge( SwDoc
*, const SwSelBoxes
&, SwTableBox
*, SwUndoTableMerge
* );
159 bool OldSplitRow( SwDoc
*, const SwSelBoxes
&, sal_uInt16
, bool );
160 bool NewMerge( SwDoc
*, const SwSelBoxes
&, const SwSelBoxes
& rMerged
,
162 bool NewSplitRow( SwDoc
*, const SwSelBoxes
&, sal_uInt16
, bool );
163 std::unique_ptr
<SwBoxSelection
> CollectBoxSelection( const SwPaM
& rPam
) const;
164 void InsertSpannedRow( SwDoc
* pDoc
, sal_uInt16 nIdx
, sal_uInt16 nCnt
);
165 bool InsertRow_( SwDoc
*, const SwSelBoxes
&, sal_uInt16 nCnt
, bool bBehind
);
166 bool NewInsertCol( SwDoc
*, const SwSelBoxes
& rBoxes
, sal_uInt16 nCnt
, bool );
167 void FindSuperfluousRows_( SwSelBoxes
& rBoxes
, SwTableLine
*, SwTableLine
* );
168 void AdjustWidths( const long nOld
, const long nNew
);
169 void NewSetTabCols( Parm
&rP
, const SwTabCols
&rNew
, const SwTabCols
&rOld
,
170 const SwTableBox
*pStart
, bool bCurRowOnly
);
174 SwHTMLTableLayout
*GetHTMLTableLayout() { return m_xHTMLLayout
.get(); }
175 const SwHTMLTableLayout
*GetHTMLTableLayout() const { return m_xHTMLLayout
.get(); }
176 void SetHTMLTableLayout(std::shared_ptr
<SwHTMLTableLayout
> const& r
); //Change of property!
178 sal_uInt16
IncGrfsThatResize() { return ++m_nGraphicsThatResize
; }
179 sal_uInt16
DecGrfsThatResize() { return m_nGraphicsThatResize
? --m_nGraphicsThatResize
: 0; }
181 void LockModify() { m_bModifyLocked
= true; } // Must be used always
182 void UnlockModify() { m_bModifyLocked
= false;} // in pairs!
184 void SetTableModel( bool bNew
){ m_bNewModel
= bNew
; }
185 bool IsNewModel() const { return m_bNewModel
; }
187 /// Return the table style name of this table.
188 const OUString
& GetTableStyleName() const { return maTableStyleName
; }
190 /// Set the new table style name for this table.
191 void SetTableStyleName(const OUString
& rName
) { maTableStyleName
= rName
; }
193 sal_uInt16
GetRowsToRepeat() const { return std::min( static_cast<sal_uInt16
>(GetTabLines().size()), m_nRowsToRepeat
); }
194 void SetRowsToRepeat( sal_uInt16 nNumOfRows
) { m_nRowsToRepeat
= nNumOfRows
; }
196 bool IsHeadline( const SwTableLine
& rLine
) const;
198 SwTableLines
&GetTabLines() { return m_aLines
; }
199 const SwTableLines
&GetTabLines() const { return m_aLines
; }
201 SwTableFormat
* GetFrameFormat() { return static_cast<SwTableFormat
*>(GetRegisteredIn()); }
202 SwTableFormat
* GetFrameFormat() const { return const_cast<SwTableFormat
*>(static_cast<const SwTableFormat
*>(GetRegisteredIn())); }
204 void GetTabCols( SwTabCols
&rToFill
, const SwTableBox
*pStart
,
205 bool bHidden
= false, bool bCurRowOnly
= false ) const;
206 void SetTabCols( const SwTabCols
&rNew
, const SwTabCols
&rOld
,
207 const SwTableBox
*pStart
, bool bCurRowOnly
);
209 // The following functions are for new table model only...
210 void CreateSelection( const SwPaM
& rPam
, SwSelBoxes
& rBoxes
,
211 const SearchType eSearchType
, bool bProtect
) const;
212 void CreateSelection( const SwNode
* pStart
, const SwNode
* pEnd
,
213 SwSelBoxes
& rBoxes
, const SearchType eSearchType
, bool bProtect
) const;
214 void ExpandSelection( SwSelBoxes
& rBoxes
) const;
215 // When a table is split into two tables, the row spans which overlaps
216 // the split have to be corrected and stored for undo
217 // SwSavRowSpan is the structure needed by Undo to undo the split operation
218 // CleanUpRowSpan corrects the (top of the) second table and delivers the structure
220 std::unique_ptr
<SwSaveRowSpan
> CleanUpTopRowSpan( sal_uInt16 nSplitLine
);
221 // RestoreRowSpan is called by Undo to restore the old row span values
222 void RestoreRowSpan( const SwSaveRowSpan
& );
223 // CleanUpBottomRowSpan corrects the overhanging row spans at the end of the first table
224 void CleanUpBottomRowSpan( sal_uInt16 nDelLines
);
226 // The following functions are "pseudo-virtual", i.e. they are different for old and new table model
227 // It's not allowed to change the table model after the first call of one of these functions.
229 bool Merge( SwDoc
* pDoc
, const SwSelBoxes
& rBoxes
, const SwSelBoxes
& rMerged
,
230 SwTableBox
* pMergeBox
, SwUndoTableMerge
* pUndo
)
232 return m_bNewModel
? NewMerge( pDoc
, rBoxes
, rMerged
, pUndo
) :
233 OldMerge( pDoc
, rBoxes
, pMergeBox
, pUndo
);
235 bool SplitRow( SwDoc
* pDoc
, const SwSelBoxes
& rBoxes
, sal_uInt16 nCnt
,
238 return m_bNewModel
? NewSplitRow( pDoc
, rBoxes
, nCnt
, bSameHeight
) :
239 OldSplitRow( pDoc
, rBoxes
, nCnt
, bSameHeight
);
241 bool PrepareMerge( const SwPaM
& rPam
, SwSelBoxes
& rBoxes
,
242 SwSelBoxes
& rMerged
, SwTableBox
** ppMergeBox
, SwUndoTableMerge
* pUndo
);
243 void ExpandColumnSelection( SwSelBoxes
& rBoxes
, long &rMin
, long &rMax
) const;
244 void PrepareDeleteCol( long nMin
, long nMax
);
246 bool InsertCol( SwDoc
*, const SwSelBoxes
& rBoxes
,
247 sal_uInt16 nCnt
, bool bBehind
);
248 bool InsertRow( SwDoc
*, const SwSelBoxes
& rBoxes
,
249 sal_uInt16 nCnt
, bool bBehind
);
250 void PrepareDelBoxes( const SwSelBoxes
& rBoxes
);
251 bool DeleteSel( SwDoc
*, const SwSelBoxes
& rBoxes
, const SwSelBoxes
* pMerged
,
252 SwUndo
* pUndo
, const bool bDelMakeFrames
, const bool bCorrBorder
);
253 bool SplitCol( SwDoc
* pDoc
, const SwSelBoxes
& rBoxes
, sal_uInt16 nCnt
);
255 void FindSuperfluousRows( SwSelBoxes
& rBoxes
)
256 { FindSuperfluousRows_( rBoxes
, nullptr, nullptr ); }
257 void CheckRowSpan( SwTableLine
* &rpLine
, bool bUp
) const;
259 SwTableSortBoxes
& GetTabSortBoxes() { return m_TabSortContentBoxes
; }
260 const SwTableSortBoxes
& GetTabSortBoxes() const { return m_TabSortContentBoxes
; }
262 // Read 1st number and delete it from string (used by GetTableBox and SwTableField).
265 // add 3rd parameter in order to control validation check on <rStr>
266 static sal_uInt16
GetBoxNum( OUString
& rStr
,
268 const bool bPerformValidCheck
= false );
270 // Search content-bearing box with that name.
273 // add 2nd parameter in order to control validation check in called method
275 const SwTableBox
* GetTableBox( const OUString
& rName
,
276 const bool bPerformValidCheck
= false ) const;
277 // Copy selected boxes to another document.
278 bool MakeCopy( SwDoc
*, const SwPosition
&, const SwSelBoxes
&,
279 bool bCpyName
= false ) const;
280 // Copy table in this
281 bool InsTable( const SwTable
& rCpyTable
, const SwNodeIndex
&,
282 SwUndoTableCpyTable
* pUndo
);
283 bool InsTable( const SwTable
& rCpyTable
, const SwSelBoxes
&,
284 SwUndoTableCpyTable
* pUndo
);
285 bool InsNewTable( const SwTable
& rCpyTable
, const SwSelBoxes
&,
286 SwUndoTableCpyTable
* pUndo
);
287 // Copy headline of table (with content!) into another one.
288 void CopyHeadlineIntoTable( SwTableNode
& rTableNd
);
290 // Get box, whose start index is set on nBoxStt.
291 SwTableBox
* GetTableBox( sal_uLong nSttIdx
);
292 const SwTableBox
* GetTableBox( sal_uLong nSttIdx
) const
293 { return const_cast<SwTable
*>(this)->GetTableBox( nSttIdx
); }
295 // Returns true if table contains nestings.
296 bool IsTableComplex() const;
298 // Returns true if table or selection is balanced.
299 bool IsTableComplexForChart( const OUString
& rSel
) const;
301 // Search all content-bearing boxes of the base line on which this box stands.
302 // rBoxes as a return value for immediate use.
303 // bToTop = true -> up to base line, false-> else only line of box.
304 static SwSelBoxes
& SelLineFromBox( const SwTableBox
* pBox
,
305 SwSelBoxes
& rBoxes
, bool bToTop
= true );
307 // Get information from client.
308 virtual bool GetInfo( SfxPoolItem
& ) const override
;
310 // Search in format for registered table.
311 static SwTable
* FindTable( SwFrameFormat
const*const pFormat
);
313 // Clean up structure a bit.
316 // Returns the table node via m_TabSortContentBoxes or pTableNode.
317 SwTableNode
* GetTableNode() const;
318 void SetTableNode( SwTableNode
* pNode
) { m_pTableNode
= pNode
; }
320 // Data server methods.
321 void SetRefObject( SwServerObject
* );
322 const SwServerObject
* GetObject() const { return m_xRefObj
.get(); }
323 SwServerObject
* GetObject() { return m_xRefObj
.get(); }
325 // Fill data for chart.
326 void UpdateCharts() const;
328 TableChgMode
GetTableChgMode() const { return m_eTableChgMode
; }
329 void SetTableChgMode( TableChgMode eMode
) { m_eTableChgMode
= eMode
; }
331 bool SetColWidth( SwTableBox
& rCurrentBox
, TableChgWidthHeightType eType
,
332 SwTwips nAbsDiff
, SwTwips nRelDiff
, std::unique_ptr
<SwUndo
>* ppUndo
);
333 bool SetRowHeight( SwTableBox
& rCurrentBox
, TableChgWidthHeightType eType
,
334 SwTwips nAbsDiff
, SwTwips nRelDiff
, std::unique_ptr
<SwUndo
>* ppUndo
);
335 void RegisterToFormat( SwFormat
& rFormat
);
337 void CheckConsistency() const;
340 bool HasLayout() const;
343 /// SwTableLine is one table row in the document model.
344 class SW_DLLPUBLIC SwTableLine
: public SwClient
// Client of FrameFormat.
346 SwTableBoxes m_aBoxes
;
347 SwTableBox
*m_pUpper
;
351 SwTableLine( SwTableLineFormat
*, sal_uInt16 nBoxes
, SwTableBox
*pUp
);
352 virtual ~SwTableLine() override
;
354 SwTableBoxes
&GetTabBoxes() { return m_aBoxes
; }
355 const SwTableBoxes
&GetTabBoxes() const { return m_aBoxes
; }
356 sal_uInt16
GetBoxPos(const SwTableBox
* pBox
) const
358 SwTableBoxes::const_iterator it
= std::find(m_aBoxes
.begin(), m_aBoxes
.end(), pBox
);
359 return it
== m_aBoxes
.end() ? USHRT_MAX
: it
- m_aBoxes
.begin();
362 SwTableBox
*GetUpper() { return m_pUpper
; }
363 const SwTableBox
*GetUpper() const { return m_pUpper
; }
364 void SetUpper( SwTableBox
*pNew
) { m_pUpper
= pNew
; }
366 SwFrameFormat
* GetFrameFormat() { return static_cast<SwFrameFormat
*>(GetRegisteredIn()); }
367 SwFrameFormat
* GetFrameFormat() const { return const_cast<SwFrameFormat
*>(static_cast<const SwFrameFormat
*>(GetRegisteredIn())); }
369 // Creates an own FrameFormat if more lines depend on it.
370 SwFrameFormat
* ClaimFrameFormat();
371 void ChgFrameFormat( SwTableLineFormat
* pNewFormat
);
373 // Search next/previous box with content.
374 SwTableBox
* FindNextBox( const SwTable
&, const SwTableBox
* =nullptr,
375 bool bOvrTableLns
=true ) const;
376 SwTableBox
* FindPreviousBox( const SwTable
&, const SwTableBox
* =nullptr,
377 bool bOvrTableLns
=true ) const;
379 SwTwips
GetTableLineHeight( bool& bLayoutAvailable
) const;
381 bool hasSoftPageBreak() const;
382 void RegisterToFormat( SwFormat
& rFormat
);
385 /// SwTableBox is one table cell in the document model.
386 class SW_DLLPUBLIC SwTableBox
: public SwClient
//Client of FrameFormat.
388 friend class SwNodes
; // Transpose index.
389 friend void DelBoxNode(SwTableSortBoxes
const &); // Delete StartNode* !
390 friend class SwXMLTableContext
;
392 SwTableBox( const SwTableBox
& ) = delete;
393 SwTableBox
&operator=( const SwTableBox
&) = delete;
395 SwTableLines m_aLines
;
396 const SwStartNode
* m_pStartNode
;
397 SwTableLine
*m_pUpper
;
399 std::unique_ptr
<Color
> mpUserColor
;
400 std::unique_ptr
<Color
> mpNumFormatColor
;
404 /// Do we contain any direct formatting?
405 bool mbDirectFormatting
;
407 // In case Format contains formulas/values already,
408 // a new one must be created for the new box.
409 static SwTableBoxFormat
* CheckBoxFormat( SwTableBoxFormat
* );
413 SwTableBox( SwTableBoxFormat
*, sal_uInt16 nLines
, SwTableLine
*pUp
);
414 SwTableBox( SwTableBoxFormat
*, const SwStartNode
&, SwTableLine
*pUp
);
415 SwTableBox( SwTableBoxFormat
*, const SwNodeIndex
&, SwTableLine
*pUp
);
416 virtual ~SwTableBox() override
;
418 SwTableLines
&GetTabLines() { return m_aLines
; }
419 const SwTableLines
&GetTabLines() const { return m_aLines
; }
421 SwTableLine
*GetUpper() { return m_pUpper
; }
422 const SwTableLine
*GetUpper() const { return m_pUpper
; }
423 void SetUpper( SwTableLine
*pNew
) { m_pUpper
= pNew
; }
425 SwFrameFormat
* GetFrameFormat() { return static_cast<SwFrameFormat
*>(GetRegisteredIn()); }
426 SwFrameFormat
* GetFrameFormat() const { return const_cast<SwFrameFormat
*>(static_cast<const SwFrameFormat
*>(GetRegisteredIn())); }
428 /// Set that this table box contains formatting that is not set by the table style.
429 void SetDirectFormatting(bool bDirect
) { mbDirectFormatting
= bDirect
; }
431 /// Do we contain any direct formatting (ie. something not affected by the table style)?
432 bool HasDirectFormatting() const { return mbDirectFormatting
; }
434 // Creates its own FrameFormat if more boxes depend on it.
435 SwFrameFormat
* ClaimFrameFormat();
436 void ChgFrameFormat( SwTableBoxFormat
*pNewFormat
, bool bNeedToReregister
= true );
438 void RemoveFromTable();
439 const SwStartNode
*GetSttNd() const { return m_pStartNode
; }
440 sal_uLong
GetSttIdx() const;
442 // Search next/previous box with content.
443 SwTableBox
* FindNextBox( const SwTable
&, const SwTableBox
*,
444 bool bOvrTableLns
=true ) const;
445 SwTableBox
* FindPreviousBox( const SwTable
&, const SwTableBox
* ) const;
446 // Return name of this box. It is determined dynamically and
447 // is calculated from the position in the lines/boxes/table.
448 OUString
GetName() const;
449 // Return "value" of box (for calculating in table).
450 double GetValue( SwTableCalcPara
& rPara
) const;
452 // Computes "coordinates" of a box, used to computed selection
453 // width or height when inserting cols or rows
454 Point
GetCoordinates() const;
456 bool IsInHeadline( const SwTable
* pTable
) const;
458 // Contains box contents, that can be formatted as a number?
459 bool HasNumContent( double& rNum
, sal_uInt32
& rFormatIndex
,
460 bool& rIsEmptyTextNd
) const;
461 sal_uLong
IsValidNumTextNd( bool bCheckAttr
= true ) const;
462 // If a table formula is set, test if box contents is congruent with number.
463 // (For Redo of change of NumFormat!).
464 bool IsNumberChanged() const;
466 // Is that a formula box or a box with numeric contents (AutoSum)?
467 // What it is indicated by the return value - the WhichId of the attribute.
468 // Empty boxes have the return value USHRT_MAX !!
469 sal_uInt16
IsFormulaOrValueBox() const;
471 // Loading of a document requires an actualization of cells with values
472 void ActualiseValueBox();
474 // Access on internal data - currently used for the NumFormatter.
475 inline const Color
* GetSaveUserColor() const;
476 inline const Color
* GetSaveNumFormatColor() const;
477 inline void SetSaveUserColor(const Color
* p
);
478 inline void SetSaveNumFormatColor( const Color
* p
);
480 long getRowSpan() const;
481 void setRowSpan( long nNewRowSpan
);
482 bool getDummyFlag() const;
483 void setDummyFlag( bool bDummy
);
485 SwTableBox
& FindStartOfRowSpan( const SwTable
&, sal_uInt16 nMaxStep
= USHRT_MAX
);
486 const SwTableBox
& FindStartOfRowSpan( const SwTable
& rTable
,
487 sal_uInt16 nMaxStep
= USHRT_MAX
) const
488 { return const_cast<SwTableBox
*>(this)->FindStartOfRowSpan( rTable
, nMaxStep
); }
490 SwTableBox
& FindEndOfRowSpan( const SwTable
&, sal_uInt16 nMaxStep
);
491 const SwTableBox
& FindEndOfRowSpan( const SwTable
& rTable
,
492 sal_uInt16 nMaxStep
) const
493 { return const_cast<SwTableBox
*>(this)->FindEndOfRowSpan( rTable
, nMaxStep
); }
494 void RegisterToFormat( SwFormat
& rFormat
) ;
498 class SW_DLLPUBLIC SwTableCellInfo
501 std::unique_ptr
<Impl
> m_pImpl
;
503 const SwCellFrame
* getCellFrame() const;
505 SwTableCellInfo(SwTableCellInfo
const&) = delete;
506 SwTableCellInfo
& operator=(SwTableCellInfo
const&) = delete;
509 SwTableCellInfo(const SwTable
* pTable
);
513 SwRect
getRect() const;
514 const SwTableBox
* getTableBox() const;
517 #endif // INCLUDED_SW_INC_SWTABLE_HXX
519 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */