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 <config_features.h>
24 #include <comphelper/propertyvalue.hxx>
25 #include <osl/file.hxx>
26 #include <sfx2/bindings.hxx>
27 #include <sfx2/request.hxx>
28 #include <sfx2/docfilt.hxx>
29 #include <sfx2/fcontnr.hxx>
30 #include <sfx2/docfile.hxx>
31 #include <sfx2/sfxsids.hrc>
32 #include <vcl/outdev.hxx>
33 #include <vcl/pdfread.hxx>
34 #include <vcl/svapp.hxx>
35 #include <vcl/weld.hxx>
36 #include <svx/svdpagv.hxx>
37 #include <svx/xbtmpit.hxx>
38 #include <svx/svdundo.hxx>
39 #include <svx/xfillit0.hxx>
40 #include <svx/svdograf.hxx>
41 #include <svx/svdomedia.hxx>
42 #include <svx/svdoole2.hxx>
43 #include <svx/ImageMapInfo.hxx>
44 #include <sfx2/app.hxx>
45 #include <avmedia/mediawindow.hxx>
46 #include <svtools/ehdl.hxx>
47 #include <svtools/sfxecode.hxx>
48 #include <svtools/embedhlp.hxx>
49 #include <vcl/graphicfilter.hxx>
52 #include <DrawDocShell.hxx>
53 #include <DrawViewShell.hxx>
54 #include <fuinsfil.hxx>
55 #include <drawdoc.hxx>
56 #include <sdresid.hxx>
57 #include <strings.hrc>
59 #include <view/SlideSorterView.hxx>
60 #include <com/sun/star/embed/XEmbedPersist.hpp>
61 #include <com/sun/star/embed/Aspects.hpp>
62 #include <com/sun/star/embed/NoVisualAreaSizeException.hpp>
63 #include <com/sun/star/embed/XEmbeddedObject.hpp>
64 #include <com/sun/star/media/XPlayer.hpp>
65 #include <svtools/soerr.hxx>
66 #include <sfx2/ipclient.hxx>
67 #include <tools/debug.hxx>
69 using namespace com::sun::star
;
74 * If an empty graphic object is provided, we fill it. Otherwise we fill an
75 * existing object at the specified position. If there is no object at the
76 * position, we create a new object and return a pointer to it.
78 SdrGrafObj
* View::InsertGraphic( const Graphic
& rGraphic
, sal_Int8
& rAction
,
79 const Point
& rPos
, SdrObject
* pObj
, ImageMap
const * pImageMap
)
84 // Is there an object at the position rPos?
85 rtl::Reference
<SdrGrafObj
> pNewGrafObj
;
86 SdrPageView
* pPV
= GetSdrPageView();
87 SdrObject
* pPickObj
= pObj
;
88 const bool bOnMaster
= pPV
&& pPV
->GetPage() && pPV
->GetPage()->IsMasterPage();
90 if(pPV
&& dynamic_cast< const ::sd::slidesorter::view::SlideSorterView
* >(this) != nullptr)
92 if(!pPV
->GetPageRect().Contains(rPos
))
96 if( !pPickObj
&& pPV
)
98 SdrPageView
* pPageView
= pPV
;
99 pPickObj
= PickObj(rPos
, getHitTolLog(), pPageView
);
102 const bool bIsGraphic(dynamic_cast< const SdrGrafObj
* >(pPickObj
) != nullptr);
104 if (DND_ACTION_LINK
== mnAction
107 && (bIsGraphic
|| (pPickObj
->IsEmptyPresObj() && !bOnMaster
))) // #121603# Do not use pObj, it may be NULL
109 // hit on SdrGrafObj with wanted new linked graphic (or PresObj placeholder hit)
110 if( IsUndoEnabled() )
111 BegUndo(SdResId(STR_INSERTGRAPHIC
));
113 SdPage
* pPage
= static_cast<SdPage
*>( pPickObj
->getSdrPageFromSdrObject() );
117 // We fill the object with the Bitmap
118 pNewGrafObj
= SdrObject::Clone(static_cast<SdrGrafObj
&>(*pPickObj
), pPickObj
->getSdrModelFromSdrObject());
119 pNewGrafObj
->SetGraphic(rGraphic
);
123 pNewGrafObj
= new SdrGrafObj(
124 getSdrModelFromSdrView(),
126 pPickObj
->GetLogicRect());
127 pNewGrafObj
->SetEmptyPresObj(true);
130 if ( pNewGrafObj
->IsEmptyPresObj() )
132 ::tools::Rectangle
aRect( pNewGrafObj
->GetLogicRect() );
133 pNewGrafObj
->AdjustToMaxRect( aRect
);
134 pNewGrafObj
->SetOutlinerParaObject(std::nullopt
);
135 pNewGrafObj
->SetEmptyPresObj(false);
138 if (pPage
&& pPage
->IsPresObj(pPickObj
))
140 // Insert new PresObj into the list
141 pPage
->InsertPresObj( pNewGrafObj
.get(), PresObjKind::Graphic
);
142 pNewGrafObj
->SetUserCall(pPickObj
->GetUserCall());
146 pNewGrafObj
->AppendUserData(std::unique_ptr
<SdrObjUserData
>(new SvxIMapInfo(*pImageMap
)));
148 ReplaceObjectAtView(pPickObj
, *pPV
, pNewGrafObj
.get()); // maybe ReplaceObjectAtView
150 if( IsUndoEnabled() )
153 else if (DND_ACTION_LINK
== mnAction
156 && pPickObj
->IsClosedObj()
157 && !dynamic_cast< const SdrOle2Obj
* >(pPickObj
))
159 // fill style change (fill object with graphic), independent of mnAction
160 // and thus of DND_ACTION_LINK or DND_ACTION_MOVE
161 if( IsUndoEnabled() )
163 BegUndo(SdResId(STR_UNDO_DRAGDROP
));
164 AddUndo(GetModel().GetSdrUndoFactory().CreateUndoAttrObject(*pPickObj
));
168 SfxItemSetFixed
<XATTR_FILLSTYLE
, XATTR_FILLBITMAP
> aSet(mpDocSh
->GetPool());
170 aSet
.Put(XFillStyleItem(drawing::FillStyle_BITMAP
));
171 aSet
.Put(XFillBitmapItem(rGraphic
));
172 pPickObj
->SetMergedItemSetAndBroadcast(aSet
);
177 Size aSizePixel
= rGraphic
.GetSizePixel();
182 if ( rGraphic
.GetPrefMapMode().GetMapUnit() == MapUnit::MapPixel
)
184 ::OutputDevice
* pOutDev
= nullptr;
186 pOutDev
= mpViewSh
->GetActiveWindow()->GetOutDev();
189 pOutDev
= Application::GetDefaultDevice();
192 aSize
= pOutDev
->PixelToLogic(rGraphic
.GetPrefSize(), MapMode(MapUnit::Map100thMM
));
196 aSize
= OutputDevice::LogicToLogic( rGraphic
.GetPrefSize(),
197 rGraphic
.GetPrefMapMode(),
198 MapMode( MapUnit::Map100thMM
) );
201 sal_Int32 nPreferredDPI
= mrDoc
.getImagePreferredDPI();
203 if (rGraphic
.GetGfxLink().GetType() == GfxLinkType::NativePdf
&& nPreferredDPI
== 0 && vcl::PDF_INSERT_MAGIC_SCALE_FACTOR
> 1)
204 nPreferredDPI
= Application::GetDefaultDevice()->GetDPIX() * vcl::PDF_INSERT_MAGIC_SCALE_FACTOR
;
206 if (nPreferredDPI
> 0)
208 auto nWidth
= o3tl::convert(aSizePixel
.Width() / double(nPreferredDPI
), o3tl::Length::in
, o3tl::Length::mm100
);
209 auto nHeight
= o3tl::convert(aSizePixel
.Height() / double(nPreferredDPI
), o3tl::Length::in
, o3tl::Length::mm100
);
210 if (nWidth
> 0 && nHeight
> 0)
211 aSize
= Size(nWidth
, nHeight
);
214 pNewGrafObj
= new SdrGrafObj(getSdrModelFromSdrView(), rGraphic
, ::tools::Rectangle(rPos
, aSize
));
216 if (nPreferredDPI
> 0)
218 // move to the center of insertion point
219 pNewGrafObj
->NbcMove(Size(-aSize
.Width() / 2, -aSize
.Height() / 2));
223 SdrPage
* pPage
= pPV
->GetPage();
224 Size
aPageSize( pPage
->GetSize() );
225 aPageSize
.AdjustWidth( -(pPage
->GetLeftBorder() + pPage
->GetRightBorder()) );
226 aPageSize
.AdjustHeight( -(pPage
->GetUpperBorder() + pPage
->GetLowerBorder()) );
227 pNewGrafObj
->AdjustToMaxRect( ::tools::Rectangle( Point(), aPageSize
), true );
230 SdrInsertFlags nOptions
= SdrInsertFlags::SETDEFLAYER
;
231 bool bIsPresTarget
= false;
234 && mpViewSh
->GetViewShell()!=nullptr
235 && mpViewSh
->GetViewShell()->GetIPClient()
236 && mpViewSh
->GetViewShell()->GetIPClient()->IsObjectInPlaceActive())
237 || dynamic_cast<const ::sd::slidesorter::view::SlideSorterView
* >(this))
238 nOptions
|= SdrInsertFlags::DONTMARK
;
240 if( ( mnAction
& DND_ACTION_MOVE
) && pPickObj
&& (pPickObj
->IsEmptyPresObj() || pPickObj
->GetUserCall()) )
242 SdPage
* pP
= static_cast< SdPage
* >( pPickObj
->getSdrPageFromSdrObject() );
244 if ( pP
&& pP
->IsMasterPage() )
245 bIsPresTarget
= pP
->IsPresObj(pPickObj
);
248 if( ( mnAction
& DND_ACTION_MOVE
) && pPickObj
&& !bIsPresTarget
)
252 pNewGrafObj
->AppendUserData(std::unique_ptr
<SdrObjUserData
>(new SvxIMapInfo(*pImageMap
)));
254 ::tools::Rectangle
aPickObjRect(pPickObj
->GetCurrentBoundRect());
255 Size
aPickObjSize(aPickObjRect
.GetSize());
256 ::tools::Rectangle
aObjRect(pNewGrafObj
->GetCurrentBoundRect());
257 Size
aObjSize(aObjRect
.GetSize());
259 Fraction
aScaleWidth(aPickObjSize
.Width(), aObjSize
.Width());
260 Fraction
aScaleHeight(aPickObjSize
.Height(), aObjSize
.Height());
261 pNewGrafObj
->NbcResize(aObjRect
.TopLeft(), aScaleWidth
, aScaleHeight
);
263 Point aVec
= aPickObjRect
.TopLeft() - aObjRect
.TopLeft();
264 pNewGrafObj
->NbcMove(Size(aVec
.X(), aVec
.Y()));
266 const bool bUndo
= IsUndoEnabled();
269 BegUndo(SdResId(STR_UNDO_DRAGDROP
));
270 pNewGrafObj
->NbcSetLayer(pPickObj
->GetLayer());
271 SdrPage
* pP
= pPV
->GetPage();
272 pP
->InsertObject(pNewGrafObj
.get());
275 AddUndo(mrDoc
.GetSdrUndoFactory().CreateUndoNewObject(*pNewGrafObj
));
276 AddUndo(mrDoc
.GetSdrUndoFactory().CreateUndoDeleteObject(*pPickObj
));
278 pP
->RemoveObject(pPickObj
->GetOrdNum());
284 mnAction
= DND_ACTION_COPY
;
288 bool bSuccess
= InsertObjectAtView(pNewGrafObj
.get(), *pPV
, nOptions
);
290 pNewGrafObj
= nullptr;
292 pNewGrafObj
->AppendUserData(std::unique_ptr
<SdrObjUserData
>(new SvxIMapInfo(*pImageMap
)));
298 return pNewGrafObj
.get();
301 void View::InsertMediaURL( const OUString
& rMediaURL
, sal_Int8
& rAction
,
302 const Point
& rPos
, const Size
& rSize
,
312 uno::Reference
<frame::XModel
> const xModel(
313 GetDoc().GetObjectShell()->GetModel());
314 #if HAVE_FEATURE_AVMEDIA
315 bool const bRet
= ::avmedia::EmbedMedia(xModel
, rMediaURL
, realURL
);
316 if (!bRet
) { return; }
322 InsertMediaObj(realURL
, rAction
, rPos
, rSize
);
325 SdrMediaObj
* View::InsertMediaObj( const OUString
& rMediaURL
, sal_Int8
& rAction
,
326 const Point
& rPos
, const Size
& rSize
)
331 rtl::Reference
<SdrMediaObj
> pNewMediaObj
;
332 SdrPageView
* pPV
= GetSdrPageView();
333 SdrObject
* pPickObj
= GetEmptyPresentationObject( PresObjKind::Media
);
335 if(pPV
&& dynamic_cast<const ::sd::slidesorter::view::SlideSorterView
* >(this) )
337 if(!pPV
->GetPageRect().Contains(rPos
))
341 if( mnAction
== DND_ACTION_LINK
&& pPV
&& dynamic_cast< SdrMediaObj
*>( pPickObj
) )
343 pNewMediaObj
= SdrObject::Clone(static_cast<SdrMediaObj
&>(*pPickObj
), pPickObj
->getSdrModelFromSdrObject());
344 pNewMediaObj
->setURL(rMediaURL
, ""/*TODO?*/);
346 BegUndo(SdResId(STR_UNDO_DRAGDROP
));
347 ReplaceObjectAtView(pPickObj
, *pPV
, pNewMediaObj
.get());
352 ::tools::Rectangle
aRect( rPos
, rSize
);
353 SdrObjUserCall
* pUserCall
= nullptr;
356 aRect
= pPickObj
->GetLogicRect();
357 pUserCall
= pPickObj
->GetUserCall(); // ReplaceObjectAtView can free pPickObj
360 pNewMediaObj
= new SdrMediaObj(
361 getSdrModelFromSdrView(),
364 bool bIsPres
= false;
367 SdPage
* pPage
= static_cast< SdPage
* >(pPickObj
->getSdrPageFromSdrObject());
368 bIsPres
= pPage
&& pPage
->IsPresObj(pPickObj
);
371 pPage
->InsertPresObj( pNewMediaObj
.get(), PresObjKind::Media
);
376 ReplaceObjectAtView(pPickObj
, *pPV
, pNewMediaObj
.get());
379 if (!InsertObjectAtView(pNewMediaObj
.get(), *pPV
, SdrInsertFlags::SETDEFLAYER
))
380 pNewMediaObj
= nullptr;
384 DrawDocShell
* sh
= GetDocSh();
385 if (sh
!= nullptr && sh
->HasName()) {
386 referer
= sh
->GetMedium()->GetName();
391 pNewMediaObj
->setURL(rMediaURL
, referer
);
395 pNewMediaObj
->AdjustToMaxRect( aRect
);
397 pNewMediaObj
->SetUserCall( pUserCall
);
404 return pNewMediaObj
.get();
408 * Timer handler for InsertFile at Drop()
410 IMPL_LINK_NOARG(View
, DropInsertFileHdl
, Timer
*, void)
412 DBG_ASSERT( mpViewSh
, "sd::View::DropInsertFileHdl(), I need a view shell to work!" );
416 SfxErrorContext
aEc( ERRCTX_ERROR
, mpViewSh
->GetFrameWeld(), RID_SO_ERRCTX
);
417 ErrCode nError
= ERRCODE_NONE
;
419 ::std::vector
< OUString
>::const_iterator
aIter( maDropFileVector
.begin() );
421 while( (aIter
!= maDropFileVector
.end()) && !nError
)
423 OUString
aCurrentDropFile( *aIter
);
424 INetURLObject
aURL( aCurrentDropFile
);
425 bool bHandled
= false;
427 if( aURL
.GetProtocol() == INetProtocol::NotValid
)
430 osl::FileBase::getFileURLFromSystemPath( aCurrentDropFile
, aURLStr
);
431 aURL
= INetURLObject( aURLStr
);
434 GraphicFilter
& rGraphicFilter
= GraphicFilter::GetGraphicFilter();
437 aCurrentDropFile
= aURL
.GetMainURL( INetURLObject::DecodeMechanism::NONE
);
439 #if HAVE_FEATURE_AVMEDIA
440 if( !::avmedia::MediaWindow::isMediaURL( aCurrentDropFile
, ""/*TODO?*/ ) )
444 if( !rGraphicFilter
.ImportGraphic( aGraphic
, aURL
) )
446 sal_Int8 nTempAction
= ( aIter
== maDropFileVector
.begin() ) ? mnAction
: 0;
447 const bool bLink
= ( ( nTempAction
& DND_ACTION_LINK
) != 0 );
448 SdrGrafObj
* pGrafObj
= InsertGraphic( aGraphic
, nTempAction
, maDropPos
, nullptr, nullptr );
449 if(pGrafObj
&& bLink
)
451 pGrafObj
->SetGraphicLink( aCurrentDropFile
);
454 // return action from first inserted graphic
455 if( aIter
== maDropFileVector
.begin() )
456 mnAction
= nTempAction
;
462 std::shared_ptr
<const SfxFilter
> pFoundFilter
;
463 SfxMedium
aSfxMedium( aCurrentDropFile
, StreamMode::READ
| StreamMode::SHARE_DENYNONE
);
464 ErrCode nErr
= SfxGetpApp()->GetFilterMatcher().GuessFilter( aSfxMedium
, pFoundFilter
);
466 if( pFoundFilter
&& !nErr
)
468 ::std::vector
< OUString
> aFilterVector
;
469 OUString aFilterName
= pFoundFilter
->GetFilterName();
470 OUString aLowerAsciiFileName
= aCurrentDropFile
.toAsciiLowerCase();
472 FuInsertFile::GetSupportedFilterVector( aFilterVector
);
474 if( ( ::std::find( aFilterVector
.begin(), aFilterVector
.end(), pFoundFilter
->GetMimeType() ) != aFilterVector
.end() ) ||
475 aFilterName
.indexOf( "Text" ) != -1 ||
476 aFilterName
.indexOf( "Rich" ) != -1 ||
477 aFilterName
.indexOf( "RTF" ) != -1 ||
478 aFilterName
.indexOf( "HTML" ) != -1 ||
479 aLowerAsciiFileName
.indexOf(".sdd") != -1 ||
480 aLowerAsciiFileName
.indexOf(".sda") != -1 ||
481 aLowerAsciiFileName
.indexOf(".sxd") != -1 ||
482 aLowerAsciiFileName
.indexOf(".sxi") != -1 ||
483 aLowerAsciiFileName
.indexOf(".std") != -1 ||
484 aLowerAsciiFileName
.indexOf(".sti") != -1 )
486 ::sd::Window
* pWin
= mpViewSh
->GetActiveWindow();
487 SfxRequest
aReq(SID_INSERTFILE
, ::SfxCallMode::SLOT
, mrDoc
.GetItemPool());
488 SfxStringItem
aItem1( ID_VAL_DUMMY0
, aCurrentDropFile
), aItem2( ID_VAL_DUMMY1
, pFoundFilter
->GetFilterName() );
490 aReq
.AppendItem( aItem1
);
491 aReq
.AppendItem( aItem2
);
492 FuInsertFile::Create( mpViewSh
, pWin
, this, &mrDoc
, aReq
);
499 #if HAVE_FEATURE_AVMEDIA
502 bool bShallowDetect
= ::avmedia::MediaWindow::isMediaURL(aCurrentDropFile
, ""/*TODO?*/);
505 mxDropMediaSizeListener
.set(new avmedia::PlayerListener(
506 [this, aCurrentDropFile
](const css::uno::Reference
<css::media::XPlayer
>& rPlayer
){
509 css::awt::Size aSize
= rPlayer
->getPreferredPlayerWindowSize();
510 Size
aPrefSize(aSize
.Width
, aSize
.Height
);
512 if (aPrefSize
.Width() && aPrefSize
.Height())
514 ::sd::Window
* pWin
= mpViewSh
->GetActiveWindow();
517 aPrefSize
= pWin
->PixelToLogic(aPrefSize
, MapMode(MapUnit::Map100thMM
));
519 aPrefSize
= Application::GetDefaultDevice()->PixelToLogic(aPrefSize
, MapMode(MapUnit::Map100thMM
));
522 aPrefSize
= Size( 5000, 5000 );
524 InsertMediaURL(aCurrentDropFile
, mnAction
, maDropPos
, aPrefSize
, true);
526 mxDropMediaSizeListener
.clear();
529 bHandled
= bShallowDetect
&& ::avmedia::MediaWindow::isMediaURL(aCurrentDropFile
, ""/*TODO?*/, true, mxDropMediaSizeListener
);
535 if( mnAction
& DND_ACTION_LINK
)
536 static_cast< DrawViewShell
* >( mpViewSh
)->InsertURLButton( aCurrentDropFile
, aCurrentDropFile
, OUString(), &maDropPos
);
545 uno::Sequence
< beans::PropertyValue
> aMedium
{ comphelper::makePropertyValue(
546 "URL", aCurrentDropFile
) };
548 uno::Reference
< embed::XEmbeddedObject
> xObj
= mpDocSh
->GetEmbeddedObjectContainer().
549 InsertEmbeddedObject( aMedium
, aName
);
551 uno::Reference
< embed::XEmbedPersist
> xPersist( xObj
, uno::UNO_QUERY
);
554 // TODO/LEAN: VisualArea access can switch the object to running state
555 sal_Int64 nAspect
= embed::Aspects::MSOLE_CONTENT
;
557 xPersist
->storeOwn();
562 aSz
= xObj
->getVisualAreaSize( nAspect
);
564 catch( embed::NoVisualAreaSizeException
& )
566 // the default size will be set later
569 Size
aSize( aSz
.Width
, aSz
.Height
);
570 ::tools::Rectangle aRect
;
572 if (!aSize
.Width() || !aSize
.Height())
574 aSize
.setWidth( 1410 );
575 aSize
.setHeight( 1000 );
578 aRect
= ::tools::Rectangle( maDropPos
, aSize
);
580 rtl::Reference
<SdrOle2Obj
> pOleObj
= new SdrOle2Obj(
581 getSdrModelFromSdrView(),
582 svt::EmbeddedObjectRef(xObj
, nAspect
),
585 SdrInsertFlags nOptions
= SdrInsertFlags::SETDEFLAYER
;
587 if (mpViewSh
!= nullptr)
589 OSL_ASSERT (mpViewSh
->GetViewShell()!=nullptr);
590 SfxInPlaceClient
* pIpClient
=
591 mpViewSh
->GetViewShell()->GetIPClient();
592 if (pIpClient
!=nullptr && pIpClient
->IsObjectInPlaceActive())
593 nOptions
|= SdrInsertFlags::DONTMARK
;
596 if (InsertObjectAtView( pOleObj
.get(), *GetSdrPageView(), nOptions
))
597 pOleObj
->SetLogicRect( aRect
);
598 aSz
.Width
= aRect
.GetWidth();
599 aSz
.Height
= aRect
.GetHeight();
600 xObj
->setVisualAreaSize( nAspect
,aSz
);
603 catch( uno::Exception
& )
605 nError
= ERRCODE_IO_GENERAL
;
606 // TODO/LATER: better error handling
616 ErrorHandler::HandleError( nError
);
620 * Timer handler for Errorhandling at Drop()
622 IMPL_LINK_NOARG(View
, DropErrorHdl
, Timer
*, void)
624 vcl::Window
* pWin
= mpViewSh
? mpViewSh
->GetActiveWindow() : nullptr;
625 std::unique_ptr
<weld::MessageDialog
> xInfoBox(Application::CreateMessageDialog(pWin
? pWin
->GetFrameWeld() : nullptr,
626 VclMessageType::Info
, VclButtonsType::Ok
,
627 SdResId(STR_ACTION_NOTPOSSIBLE
)));
632 * @returns StyleSheet from selection
634 SfxStyleSheet
* View::GetStyleSheet() const
636 return SdrView::GetStyleSheet();
639 } // end of namespace sd
641 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */