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 .
20 #include <basic/basicmanagerrepository.hxx>
21 #include <basic/basmgr.hxx>
22 #include "scriptcont.hxx"
23 #include "dlgcont.hxx"
24 #include <basic/sbuno.hxx>
25 #include "sbintern.hxx"
27 #include <com/sun/star/beans/XPropertySet.hpp>
28 #include <com/sun/star/document/XStorageBasedDocument.hpp>
29 #include <com/sun/star/document/XEmbeddedScripts.hpp>
30 #include <svtools/ehdl.hxx>
31 #include <svtools/sfxecode.hxx>
32 #include <unotools/pathoptions.hxx>
33 #include <svl/smplhint.hxx>
34 #include <vcl/svapp.hxx>
35 #include <tools/debug.hxx>
36 #include <tools/diagnose_ex.h>
37 #include <tools/urlobj.hxx>
38 #include <comphelper/stl_types.hxx>
39 #include <comphelper/processfactory.hxx>
40 #include <comphelper/documentinfo.hxx>
41 #include <unotools/eventlisteneradapter.hxx>
43 #include <rtl/instance.hxx>
44 #include <rtl/strbuf.hxx>
48 //........................................................................
51 //........................................................................
53 /** === begin UNO using === **/
54 using ::com::sun::star::uno::Reference
;
55 using ::com::sun::star::frame::XModel
;
56 using ::com::sun::star::uno::XInterface
;
57 using ::com::sun::star::uno::UNO_QUERY
;
58 using ::com::sun::star::embed::XStorage
;
59 using ::com::sun::star::script::XPersistentLibraryContainer
;
60 using ::com::sun::star::uno::Any
;
61 using ::com::sun::star::lang::XMultiServiceFactory
;
62 using ::com::sun::star::uno::UNO_QUERY_THROW
;
63 using ::com::sun::star::beans::XPropertySet
;
64 using ::com::sun::star::uno::Exception
;
65 using ::com::sun::star::document::XStorageBasedDocument
;
66 using ::com::sun::star::lang::XComponent
;
67 using ::com::sun::star::document::XEmbeddedScripts
;
68 /** === end UNO using === **/
70 typedef BasicManager
* BasicManagerPointer
;
71 typedef ::std::map
< Reference
< XInterface
>, BasicManagerPointer
, ::comphelper::OInterfaceCompare
< XInterface
> > BasicManagerStore
;
73 typedef ::std::vector
< BasicManagerCreationListener
* > CreationListeners
;
75 //====================================================================
76 //= BasicManagerCleaner
77 //====================================================================
78 /// is the only instance which is allowed to delete a BasicManager instance
79 class BasicManagerCleaner
82 static void deleteBasicManager( BasicManager
*& _rpManager
)
89 //====================================================================
91 //====================================================================
92 class ImplRepository
: public ::utl::OEventListenerAdapter
, public SfxListener
95 friend struct CreateImplRepository
;
99 ::osl::Mutex m_aMutex
;
100 BasicManagerStore m_aStore
;
101 CreationListeners m_aCreationListeners
;
104 static ImplRepository
& Instance();
106 BasicManager
* getDocumentBasicManager( const Reference
< XModel
>& _rxDocumentModel
);
107 BasicManager
* getApplicationBasicManager( bool _bCreate
);
108 void setApplicationBasicManager( BasicManager
* _pBasicManager
);
109 void registerCreationListener( BasicManagerCreationListener
& _rListener
);
110 void revokeCreationListener( BasicManagerCreationListener
& _rListener
);
113 /** retrieves the location at which the BasicManager for the given model
116 If previously, the BasicManager for this model has never been requested,
117 then the model is added to the map, with an initial NULL BasicManager.
119 @param _rxDocumentModel
120 the model whose BasicManager's location is to be retrieved. Must not be <NULL/>.
126 impl_getLocationForModel( const Reference
< XModel
>& _rxDocumentModel
);
128 /** creates a new BasicManager instance for the given model
130 @param _out_rpBasicManager
131 reference to the pointer variable that will hold the new
134 @param _rxDocumentModel
135 the model whose BasicManager will be created. Must not be <NULL/>.
137 void impl_createManagerForModel(
138 BasicManagerPointer
& _out_rpBasicManager
,
139 const Reference
< XModel
>& _rxDocumentModel
);
141 /** creates the application-wide BasicManager
143 BasicManagerPointer
impl_createApplicationBasicManager();
145 /** notifies all listeners which expressed interest in the creation of BasicManager instances.
147 void impl_notifyCreationListeners(
148 const Reference
< XModel
>& _rxDocumentModel
,
149 BasicManager
& _rManager
152 /** retrieves the current storage of a given document
155 the document whose storage is to be retrieved.
158 takes the storage upon successful return. Note that this might be <NULL/> even
159 if <TRUE/> is returned. In this case, the document has not yet been saved.
162 <TRUE/> if the storage could be successfully retrieved (in which case
163 <arg>_out_rStorage</arg> might or might not be <NULL/>), <FALSE/> otherwise.
164 In the latter case, processing this document should stop.
166 bool impl_getDocumentStorage_nothrow( const Reference
< XModel
>& _rxDocument
, Reference
< XStorage
>& _out_rStorage
);
168 /** retrieves the containers for Basic and Dialog libraries for a given document
171 the document whose containers are to be retrieved.
173 @param _out_rxBasicLibraries
174 takes the basic library container upon successful return
176 @param _out_rxDialogLibraries
177 takes the dialog library container upon successful return
180 <TRUE/> if and only if both containers exist, and could successfully be retrieved
182 bool impl_getDocumentLibraryContainers_nothrow(
183 const Reference
< XModel
>& _rxDocument
,
184 Reference
< XPersistentLibraryContainer
>& _out_rxBasicLibraries
,
185 Reference
< XPersistentLibraryContainer
>& _out_rxDialogLibraries
188 /** initializes the given library containers, which belong to a document
190 void impl_initDocLibraryContainers_nothrow(
191 const Reference
< XPersistentLibraryContainer
>& _rxBasicLibraries
,
192 const Reference
< XPersistentLibraryContainer
>& _rxDialogLibraries
195 // OEventListenerAdapter overridables
196 virtual void _disposing( const ::com::sun::star::lang::EventObject
& _rSource
);
198 // SfxListener overridables
199 virtual void Notify( SfxBroadcaster
& _rBC
, const SfxHint
& _rHint
);
201 /** removes the Model/BasicManager pair given by iterator from our store
203 void impl_removeFromRepository( BasicManagerStore::iterator _pos
);
206 StarBASIC
* impl_getDefaultAppBasicLibrary();
209 //====================================================================
210 //= CreateImplRepository
211 //====================================================================
212 struct CreateImplRepository
214 ImplRepository
* operator()()
216 static ImplRepository
* pRepository
= new ImplRepository
;
222 //====================================================================
224 //====================================================================
225 //--------------------------------------------------------------------
226 ImplRepository::ImplRepository()
230 //--------------------------------------------------------------------
231 ImplRepository
& ImplRepository::Instance()
233 return *rtl_Instance
< ImplRepository
, CreateImplRepository
, ::osl::MutexGuard
, ::osl::GetGlobalMutex
>::
234 create( CreateImplRepository(), ::osl::GetGlobalMutex() );
237 //--------------------------------------------------------------------
238 BasicManager
* ImplRepository::getDocumentBasicManager( const Reference
< XModel
>& _rxDocumentModel
)
240 ::osl::MutexGuard
aGuard( m_aMutex
);
242 /* #163556# (DR) - This function may be called recursively while
243 constructing the Basic manager and loading the Basic storage. By
244 passing the map entry received from impl_getLocationForModel() to
245 the function impl_createManagerForModel(), the new Basic manager
246 will be put immediately into the map of existing Basic managers,
247 thus a recursive call of this function will find and return it
248 without creating another instance.
250 BasicManagerPointer
& pBasicManager
= impl_getLocationForModel( _rxDocumentModel
);
251 if ( pBasicManager
== NULL
)
252 impl_createManagerForModel( pBasicManager
, _rxDocumentModel
);
254 return pBasicManager
;
257 //--------------------------------------------------------------------
258 BasicManager
* ImplRepository::getApplicationBasicManager( bool _bCreate
)
260 ::osl::MutexGuard
aGuard( m_aMutex
);
262 BasicManager
* pAppManager
= GetSbData()->pAppBasMgr
;
263 if ( ( pAppManager
== NULL
) && _bCreate
)
264 pAppManager
= impl_createApplicationBasicManager();
269 //--------------------------------------------------------------------
270 void ImplRepository::setApplicationBasicManager( BasicManager
* _pBasicManager
)
272 ::osl::MutexGuard
aGuard( m_aMutex
);
274 BasicManager
* pPreviousManager
= getApplicationBasicManager( false );
275 BasicManagerCleaner::deleteBasicManager( pPreviousManager
);
277 GetSbData()->pAppBasMgr
= _pBasicManager
;
280 //--------------------------------------------------------------------
281 BasicManager
* ImplRepository::impl_createApplicationBasicManager()
283 ::osl::MutexGuard
aGuard( m_aMutex
);
284 OSL_PRECOND( getApplicationBasicManager( false ) == NULL
, "ImplRepository::impl_createApplicationBasicManager: there already is one!" );
286 // Determine Directory
287 SvtPathOptions aPathCFG
;
288 OUString
aAppBasicDir( aPathCFG
.GetBasicPath() );
289 if ( aAppBasicDir
.isEmpty() )
291 aPathCFG
.SetBasicPath(rtl::OUString("$(prog)"));
293 // soffice.new search only in user dir => first dir
294 OUString aAppFirstBasicDir
= aAppBasicDir
.getToken(1, ';');
296 // Create basic and load it
297 // AppBasicDir is now a PATH
298 INetURLObject
aAppBasic( SvtPathOptions().SubstituteVariable(rtl::OUString("$(progurl)")) );
299 aAppBasic
.insertName( Application::GetAppName() );
301 BasicManager
* pBasicManager
= new BasicManager( new StarBASIC
, &aAppBasicDir
);
302 setApplicationBasicManager( pBasicManager
);
304 // The first dir in the path as destination:
305 OUString
aFileName( aAppBasic
.getName() );
306 aAppBasic
= INetURLObject( aAppBasicDir
.getToken(1, ';') );
307 DBG_ASSERT(aAppBasic
.GetProtocol() != INET_PROT_NOT_VALID
,
308 rtl::OStringBuffer(RTL_CONSTASCII_STRINGPARAM("Invalid URL: \"")).
309 append(rtl::OUStringToOString(aAppBasicDir
,
310 osl_getThreadTextEncoding())).
313 aAppBasic
.insertName( aFileName
);
314 pBasicManager
->SetStorageName( aAppBasic
.PathToFileName() );
317 SfxScriptLibraryContainer
* pBasicCont
= new SfxScriptLibraryContainer( Reference
< XStorage
>() );
318 Reference
< XPersistentLibraryContainer
> xBasicCont( pBasicCont
);
319 pBasicCont
->setBasicManager( pBasicManager
);
322 SfxDialogLibraryContainer
* pDialogCont
= new SfxDialogLibraryContainer( Reference
< XStorage
>() );
323 Reference
< XPersistentLibraryContainer
> xDialogCont( pDialogCont
);
325 LibraryContainerInfo
aInfo( xBasicCont
, xDialogCont
, static_cast< OldBasicPassword
* >( pBasicCont
) );
326 pBasicManager
->SetLibraryContainerInfo( aInfo
);
331 Reference
< XMultiServiceFactory
> xSMgr
= ::comphelper::getProcessServiceFactory();
332 pBasicManager
->SetGlobalUNOConstant( "StarDesktop",
333 makeAny( xSMgr
->createInstance("com.sun.star.frame.Desktop")));
335 // (BasicLibraries and DialogLibraries have automatically been added in SetLibraryContainerInfo)
338 impl_notifyCreationListeners( NULL
, *pBasicManager
);
341 return pBasicManager
;
344 //--------------------------------------------------------------------
345 void ImplRepository::registerCreationListener( BasicManagerCreationListener
& _rListener
)
347 ::osl::MutexGuard
aGuard( m_aMutex
);
348 m_aCreationListeners
.push_back( &_rListener
);
351 //--------------------------------------------------------------------
352 void ImplRepository::revokeCreationListener( BasicManagerCreationListener
& _rListener
)
354 ::osl::MutexGuard
aGuard( m_aMutex
);
355 CreationListeners::iterator pos
= ::std::find( m_aCreationListeners
.begin(), m_aCreationListeners
.end(), &_rListener
);
356 if ( pos
!= m_aCreationListeners
.end() )
357 m_aCreationListeners
.erase( pos
);
359 OSL_FAIL( "ImplRepository::revokeCreationListener: listener is not registered!" );
363 //--------------------------------------------------------------------
364 void ImplRepository::impl_notifyCreationListeners( const Reference
< XModel
>& _rxDocumentModel
, BasicManager
& _rManager
)
366 for ( CreationListeners::const_iterator loop
= m_aCreationListeners
.begin();
367 loop
!= m_aCreationListeners
.end();
371 (*loop
)->onBasicManagerCreated( _rxDocumentModel
, _rManager
);
375 //--------------------------------------------------------------------
376 StarBASIC
* ImplRepository::impl_getDefaultAppBasicLibrary()
378 BasicManager
* pAppManager
= getApplicationBasicManager( true );
380 StarBASIC
* pAppBasic
= pAppManager
? pAppManager
->GetLib(0) : NULL
;
381 DBG_ASSERT( pAppBasic
!= NULL
, "impl_getApplicationBasic: unable to determine the default application's Basic library!" );
385 //--------------------------------------------------------------------
386 BasicManagerPointer
& ImplRepository::impl_getLocationForModel( const Reference
< XModel
>& _rxDocumentModel
)
388 Reference
< XInterface
> xNormalized( _rxDocumentModel
, UNO_QUERY
);
389 DBG_ASSERT( xNormalized
.is(), "ImplRepository::impl_getLocationForModel: invalid model!" );
391 BasicManagerPointer
& location
= m_aStore
[ xNormalized
];
395 //--------------------------------------------------------------------
396 void ImplRepository::impl_initDocLibraryContainers_nothrow( const Reference
< XPersistentLibraryContainer
>& _rxBasicLibraries
, const Reference
< XPersistentLibraryContainer
>& _rxDialogLibraries
)
398 OSL_PRECOND( _rxBasicLibraries
.is() && _rxDialogLibraries
.is(),
399 "ImplRepository::impl_initDocLibraryContainers_nothrow: illegal library containers, this will crash!" );
403 // ensure there's a standard library in the basic container
404 OUString
aStdLibName( "Standard" );
405 if ( !_rxBasicLibraries
->hasByName( aStdLibName
) )
407 _rxBasicLibraries
->createLibrary( aStdLibName
);
409 // as well as in the dialog container
410 if ( !_rxDialogLibraries
->hasByName( aStdLibName
) )
412 _rxDialogLibraries
->createLibrary( aStdLibName
);
415 catch( const Exception
& )
417 DBG_UNHANDLED_EXCEPTION();
421 //--------------------------------------------------------------------
422 void ImplRepository::impl_createManagerForModel( BasicManagerPointer
& _out_rpBasicManager
, const Reference
< XModel
>& _rxDocumentModel
)
424 StarBASIC
* pAppBasic
= impl_getDefaultAppBasicLibrary();
426 _out_rpBasicManager
= 0;
427 Reference
< XStorage
> xStorage
;
428 if ( !impl_getDocumentStorage_nothrow( _rxDocumentModel
, xStorage
) )
430 // the document is not able to provide the storage it is based on.
433 Reference
< XPersistentLibraryContainer
> xBasicLibs
;
434 Reference
< XPersistentLibraryContainer
> xDialogLibs
;
435 if ( !impl_getDocumentLibraryContainers_nothrow( _rxDocumentModel
, xBasicLibs
, xDialogLibs
) )
436 // the document does not have BasicLibraries and DialogLibraries
441 // load BASIC-manager
442 SfxErrorContext
aErrContext( ERRCTX_SFX_LOADBASIC
,
443 ::comphelper::DocumentInfo::getDocumentTitle( _rxDocumentModel
) );
444 OUString aAppBasicDir
= SvtPathOptions().GetBasicPath();
446 // Storage and BaseURL are only needed by binary documents!
447 SotStorageRef xDummyStor
= new SotStorage( OUString() );
448 _out_rpBasicManager
= new BasicManager( *xDummyStor
, OUString() /* TODO/LATER: xStorage */,
450 &aAppBasicDir
, sal_True
);
451 if ( !_out_rpBasicManager
->GetErrors().empty() )
454 std::vector
<BasicError
>& aErrors
= _out_rpBasicManager
->GetErrors();
455 for(std::vector
<BasicError
>::const_iterator i
= aErrors
.begin(); i
!= aErrors
.end(); ++i
)
457 // show message to user
458 if ( ERRCODE_BUTTON_CANCEL
== ErrorHandler::HandleError( i
->GetErrorId() ) )
460 // user wants to break loading of BASIC-manager
461 BasicManagerCleaner::deleteBasicManager( _out_rpBasicManager
);
470 if ( !xStorage
.is() )
472 // create new BASIC-manager
473 StarBASIC
* pBasic
= new StarBASIC( pAppBasic
);
474 pBasic
->SetFlag( SBX_EXTSEARCH
);
475 _out_rpBasicManager
= new BasicManager( pBasic
, NULL
, sal_True
);
478 // knit the containers with the BasicManager
479 LibraryContainerInfo
aInfo( xBasicLibs
, xDialogLibs
, dynamic_cast< OldBasicPassword
* >( xBasicLibs
.get() ) );
480 OSL_ENSURE( aInfo
.mpOldBasicPassword
, "ImplRepository::impl_createManagerForModel: wrong BasicLibraries implementation!" );
481 _out_rpBasicManager
->SetLibraryContainerInfo( aInfo
);
483 // initialize the containers
484 impl_initDocLibraryContainers_nothrow( xBasicLibs
, xDialogLibs
);
486 // so that also dialogs etc. could be 'qualified' addressed
487 _out_rpBasicManager
->GetLib(0)->SetParent( pAppBasic
);
489 // global properties in the document's Basic
490 _out_rpBasicManager
->SetGlobalUNOConstant( "ThisComponent", makeAny( _rxDocumentModel
) );
493 impl_notifyCreationListeners( _rxDocumentModel
, *_out_rpBasicManager
);
495 // register as listener for this model being disposed/closed
496 Reference
< XComponent
> xDocumentComponent( _rxDocumentModel
, UNO_QUERY
);
497 OSL_ENSURE( xDocumentComponent
.is(), "ImplRepository::impl_createManagerForModel: the document must be an XComponent!" );
498 startComponentListening( xDocumentComponent
);
500 // register as listener for the BasicManager being destroyed
501 StartListening( *_out_rpBasicManager
);
503 // #i104876: Library container must not be modified just after
504 // creation. This happens as side effect when creating default
505 // "Standard" libraries and needs to be corrected here
506 xBasicLibs
->setModified( sal_False
);
507 xDialogLibs
->setModified( sal_False
);
511 //--------------------------------------------------------------------
512 bool ImplRepository::impl_getDocumentStorage_nothrow( const Reference
< XModel
>& _rxDocument
, Reference
< XStorage
>& _out_rStorage
)
514 _out_rStorage
.clear();
517 Reference
< XStorageBasedDocument
> xStorDoc( _rxDocument
, UNO_QUERY_THROW
);
518 _out_rStorage
.set( xStorDoc
->getDocumentStorage() );
520 catch( const Exception
& )
522 DBG_UNHANDLED_EXCEPTION();
528 //--------------------------------------------------------------------
529 bool ImplRepository::impl_getDocumentLibraryContainers_nothrow( const Reference
< XModel
>& _rxDocument
,
530 Reference
< XPersistentLibraryContainer
>& _out_rxBasicLibraries
, Reference
< XPersistentLibraryContainer
>& _out_rxDialogLibraries
)
532 _out_rxBasicLibraries
.clear();
533 _out_rxDialogLibraries
.clear();
536 Reference
< XEmbeddedScripts
> xScripts( _rxDocument
, UNO_QUERY_THROW
);
537 _out_rxBasicLibraries
.set( xScripts
->getBasicLibraries(), UNO_QUERY_THROW
);
538 _out_rxDialogLibraries
.set( xScripts
->getDialogLibraries(), UNO_QUERY_THROW
);
540 catch( const Exception
& )
542 DBG_UNHANDLED_EXCEPTION();
544 return _out_rxBasicLibraries
.is() && _out_rxDialogLibraries
.is();
547 //--------------------------------------------------------------------
548 void ImplRepository::impl_removeFromRepository( BasicManagerStore::iterator _pos
)
550 OSL_PRECOND( _pos
!= m_aStore
.end(), "ImplRepository::impl_removeFromRepository: invalid position!" );
552 BasicManager
* pManager
= _pos
->second
;
554 // *first* remove from map (else Notify won't work properly)
555 m_aStore
.erase( _pos
);
557 // *then* delete the BasicManager
558 EndListening( *pManager
);
559 BasicManagerCleaner::deleteBasicManager( pManager
);
562 //--------------------------------------------------------------------
563 void ImplRepository::_disposing( const ::com::sun::star::lang::EventObject
& _rSource
)
565 ::osl::MutexGuard
aGuard( m_aMutex
);
567 Reference
< XInterface
> xNormalizedSource( _rSource
.Source
, UNO_QUERY
);
568 #if OSL_DEBUG_LEVEL > 0
572 for ( BasicManagerStore::iterator loop
= m_aStore
.begin();
573 loop
!= m_aStore
.end();
577 if ( loop
->first
.get() == xNormalizedSource
.get() )
579 impl_removeFromRepository( loop
);
580 #if OSL_DEBUG_LEVEL > 0
587 OSL_ENSURE( bFound
, "ImplRepository::_disposing: where does this come from?" );
590 //--------------------------------------------------------------------
591 void ImplRepository::Notify( SfxBroadcaster
& _rBC
, const SfxHint
& _rHint
)
593 const SfxSimpleHint
* pSimpleHint
= dynamic_cast< const SfxSimpleHint
* >( &_rHint
);
594 if ( !pSimpleHint
|| ( pSimpleHint
->GetId() != SFX_HINT_DYING
) )
598 BasicManager
* pManager
= dynamic_cast< BasicManager
* >( &_rBC
);
599 OSL_ENSURE( pManager
, "ImplRepository::Notify: where does this come from?" );
601 for ( BasicManagerStore::iterator loop
= m_aStore
.begin();
602 loop
!= m_aStore
.end();
606 if ( loop
->second
== pManager
)
608 // a BasicManager which is still in our repository is being deleted.
609 // That's bad, since by definition, we *own* all instances in our
611 OSL_FAIL( "ImplRepository::Notify: nobody should tamper with the managers, except ourself!" );
612 m_aStore
.erase( loop
);
618 //====================================================================
619 //= BasicManagerRepository
620 //====================================================================
621 //--------------------------------------------------------------------
622 BasicManager
* BasicManagerRepository::getDocumentBasicManager( const Reference
< XModel
>& _rxDocumentModel
)
624 return ImplRepository::Instance().getDocumentBasicManager( _rxDocumentModel
);
627 //--------------------------------------------------------------------
628 BasicManager
* BasicManagerRepository::getApplicationBasicManager( bool _bCreate
)
630 return ImplRepository::Instance().getApplicationBasicManager( _bCreate
);
633 //--------------------------------------------------------------------
634 void BasicManagerRepository::resetApplicationBasicManager()
636 return ImplRepository::Instance().setApplicationBasicManager( NULL
);
639 //--------------------------------------------------------------------
640 void BasicManagerRepository::registerCreationListener( BasicManagerCreationListener
& _rListener
)
642 ImplRepository::Instance().registerCreationListener( _rListener
);
645 //--------------------------------------------------------------------
646 void BasicManagerRepository::revokeCreationListener( BasicManagerCreationListener
& _rListener
)
648 ImplRepository::Instance().revokeCreationListener( _rListener
);
651 //........................................................................
653 //........................................................................
655 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */