4 * Copyright 2016 Sylvain Deverre <deverre dot sylv at gmail dot com>
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22 #include <commoncontrols.h>
23 #include <undocshell.h>
28 #define UNIMPLEMENTED DbgPrint("%s is UNIMPLEMENTED!\n", __FUNCTION__)
32 HRESULT WINAPI
CExplorerBand_Constructor(REFIID riid
, LPVOID
*ppv
)
34 return ShellObjectCreator
<CExplorerBand
>(riid
, ppv
);
37 CExplorerBand::CExplorerBand() :
38 pSite(NULL
), fVisible(FALSE
), bNavigating(FALSE
), dwBandID(0)
42 CExplorerBand::~CExplorerBand()
46 void CExplorerBand::InitializeExplorerBand()
48 // Init the treeview here
51 CComPtr
<IWebBrowser2
> browserService
;
52 SHChangeNotifyEntry shcne
;
54 hr
= SHGetDesktopFolder(&pDesktop
);
55 if (FAILED_UNEXPECTEDLY(hr
))
58 hr
= SHGetFolderLocation(m_hWnd
, CSIDL_DESKTOP
, NULL
, 0, &pidl
);
59 if (FAILED_UNEXPECTEDLY(hr
))
63 hr
= SHGetImageList(SHIL_SMALL
, IID_PPV_ARG(IImageList
, &piml
));
64 if (FAILED_UNEXPECTEDLY(hr
))
67 TreeView_SetImageList(m_hWnd
, (HIMAGELIST
)piml
, TVSIL_NORMAL
);
69 // Insert the root node
70 hRoot
= InsertItem(0, pDesktop
, pidl
, pidl
, FALSE
);
73 ERR("Failed to create root item\n");
77 NodeInfo
* pNodeInfo
= GetNodeInfo(hRoot
);
80 InsertSubitems(hRoot
, pNodeInfo
);
81 TreeView_Expand(m_hWnd
, hRoot
, TVE_EXPAND
);
83 // Navigate to current folder position
84 NavigateToCurrentFolder();
86 // Register shell notification
88 shcne
.fRecursive
= TRUE
;
89 shellRegID
= SHChangeNotifyRegister(
91 SHCNRF_ShellLevel
| SHCNRF_InterruptLevel
| SHCNRF_RecursiveInterrupt
,
92 SHCNE_DISKEVENTS
| SHCNE_RENAMEFOLDER
| SHCNE_RMDIR
| SHCNE_MKDIR
,
98 ERR("Something went wrong, error %08x\n", GetLastError());
100 // Register browser connection endpoint
101 hr
= IUnknown_QueryService(pSite
, SID_SWebBrowserApp
, IID_PPV_ARG(IWebBrowser2
, &browserService
));
102 if (FAILED_UNEXPECTEDLY(hr
))
105 hr
= AtlAdvise(browserService
, dynamic_cast<IDispatch
*>(this), DIID_DWebBrowserEvents
, &adviseCookie
);
106 if (FAILED_UNEXPECTEDLY(hr
))
112 void CExplorerBand::DestroyExplorerBand()
115 CComPtr
<IWebBrowser2
> browserService
;
117 TRACE("Cleaning up explorer band ...\n");
119 hr
= IUnknown_QueryService(pSite
, SID_SWebBrowserApp
, IID_PPV_ARG(IWebBrowser2
, &browserService
));
120 if (FAILED_UNEXPECTEDLY(hr
))
123 hr
= AtlUnadvise(browserService
, DIID_DWebBrowserEvents
, adviseCookie
);
124 /* Remove all items of the treeview */
125 RevokeDragDrop(m_hWnd
);
126 TreeView_DeleteAllItems(m_hWnd
);
129 TRACE("Cleanup done !\n");
132 CExplorerBand::NodeInfo
* CExplorerBand::GetNodeInfo(HTREEITEM hItem
)
136 tvItem
.mask
= TVIF_PARAM
;
137 tvItem
.hItem
= hItem
;
139 if (!TreeView_GetItem(m_hWnd
, &tvItem
))
142 return reinterpret_cast<NodeInfo
*>(tvItem
.lParam
);
145 HRESULT
CExplorerBand::UpdateBrowser(LPITEMIDLIST pidlGoto
)
147 CComPtr
<IShellBrowser
> pBrowserService
;
150 hr
= IUnknown_QueryService(pSite
, SID_STopLevelBrowser
, IID_PPV_ARG(IShellBrowser
, &pBrowserService
));
151 if (FAILED_UNEXPECTEDLY(hr
))
154 hr
= pBrowserService
->BrowseObject(pidlGoto
, SBSP_SAMEBROWSER
| SBSP_ABSOLUTE
);
155 if (FAILED_UNEXPECTEDLY(hr
))
161 // *** notifications handling ***
162 BOOL
CExplorerBand::OnTreeItemExpanding(LPNMTREEVIEW pnmtv
)
166 if (pnmtv
->action
== TVE_COLLAPSE
) {
167 if (pnmtv
->itemNew
.hItem
== hRoot
)
169 // Prenvent root from collapsing
170 pnmtv
->itemNew
.mask
|= TVIF_STATE
;
171 pnmtv
->itemNew
.stateMask
|= TVIS_EXPANDED
;
172 pnmtv
->itemNew
.state
&= ~TVIS_EXPANDED
;
173 pnmtv
->action
= TVE_EXPAND
;
177 if (pnmtv
->action
== TVE_EXPAND
) {
178 // Grab our directory PIDL
179 pNodeInfo
= GetNodeInfo(pnmtv
->itemNew
.hItem
);
180 // We have it, let's try
181 if (pNodeInfo
&& !pNodeInfo
->expanded
)
182 if (!InsertSubitems(pnmtv
->itemNew
.hItem
, pNodeInfo
)) {
183 // remove subitem "+" since we failed to add subitems
186 tvItem
.mask
= TVIF_CHILDREN
;
187 tvItem
.hItem
= pnmtv
->itemNew
.hItem
;
188 tvItem
.cChildren
= 0;
190 TreeView_SetItem(m_hWnd
, &tvItem
);
196 void CExplorerBand::OnSelectionChanged(LPNMTREEVIEW pnmtv
)
198 NodeInfo
* pNodeInfo
= GetNodeInfo(pnmtv
->itemNew
.hItem
);
200 UpdateBrowser(pNodeInfo
->absolutePidl
);
202 /* Prevents navigation if selection is initiated inside the band */
208 //TreeView_Expand(m_hWnd, pnmtv->itemNew.hItem, TVE_EXPAND);
211 // *** Helper functions ***
212 HTREEITEM
CExplorerBand::InsertItem(HTREEITEM hParent
, IShellFolder
*psfParent
, LPITEMIDLIST pElt
, LPITEMIDLIST pEltRelative
, BOOL bSort
)
214 TV_INSERTSTRUCT tvInsert
;
215 HTREEITEM htiCreated
;
217 /* Get the attributes of the node */
218 SFGAOF attrs
= SFGAO_STREAM
| SFGAO_HASSUBFOLDER
;
219 HRESULT hr
= psfParent
->GetAttributesOf(1, &pEltRelative
, &attrs
);
220 if (FAILED_UNEXPECTEDLY(hr
))
224 if ((attrs
& SFGAO_STREAM
))
226 TRACE("Ignoring stream\n");
230 /* Get the name of the node */
231 WCHAR wszDisplayName
[MAX_PATH
];
232 if (!ILGetDisplayNameEx(psfParent
, pEltRelative
, wszDisplayName
, ILGDN_INFOLDER
))
234 ERR("Failed to get node name\n");
238 /* Get the icon of the node */
239 INT iIcon
= SHMapPIDLToSystemImageListIndex(psfParent
, pEltRelative
, NULL
);
241 NodeInfo
* pChildInfo
= new NodeInfo
;
244 ERR("Failed to allocate NodeInfo\n");
248 // Store our node info
249 pChildInfo
->absolutePidl
= ILClone(pElt
);
250 pChildInfo
->relativePidl
= ILClone(pEltRelative
);
251 pChildInfo
->expanded
= FALSE
;
253 // Set up our treeview template
254 tvInsert
.hParent
= hParent
;
255 tvInsert
.hInsertAfter
= TVI_LAST
;
256 tvInsert
.item
.mask
= TVIF_PARAM
| TVIF_TEXT
| TVIF_IMAGE
| TVIF_SELECTEDIMAGE
| TVIF_CHILDREN
;
257 tvInsert
.item
.cchTextMax
= MAX_PATH
;
258 tvInsert
.item
.pszText
= wszDisplayName
;
259 tvInsert
.item
.iImage
= tvInsert
.item
.iSelectedImage
= iIcon
;
260 tvInsert
.item
.cChildren
= (attrs
& SFGAO_HASSUBFOLDER
) ? 1 : 0;
261 tvInsert
.item
.lParam
= (LPARAM
)pChildInfo
;
263 htiCreated
= TreeView_InsertItem(m_hWnd
, &tvInsert
);
268 /* This is the slow version of the above method */
269 HTREEITEM
CExplorerBand::InsertItem(HTREEITEM hParent
, LPITEMIDLIST pElt
, LPITEMIDLIST pEltRelative
, BOOL bSort
)
271 CComPtr
<IShellFolder
> psfFolder
;
272 HRESULT hr
= SHBindToParent(pElt
, IID_PPV_ARG(IShellFolder
, &psfFolder
), NULL
);
273 if (FAILED_UNEXPECTEDLY(hr
))
276 return InsertItem(hParent
, psfFolder
, pElt
, pEltRelative
, bSort
);
279 BOOL
CExplorerBand::InsertSubitems(HTREEITEM hItem
, NodeInfo
*pNodeInfo
)
281 CComPtr
<IEnumIDList
> pEnumIDList
;
282 LPITEMIDLIST pidlSub
;
288 CComPtr
<IShellFolder
> pFolder
;
290 entry
= pNodeInfo
->absolutePidl
;
293 EnumFlags
= SHCONTF_FOLDERS
;
295 hr
= SHGetFolderLocation(m_hWnd
, CSIDL_DESKTOP
, NULL
, 0, &pidlSub
);
298 ERR("Can't get desktop PIDL !\n");
302 if (!pDesktop
->CompareIDs(NULL
, pidlSub
, entry
))
304 // We are the desktop, so use pDesktop as pFolder
309 // Get an IShellFolder of our pidl
310 hr
= pDesktop
->BindToObject(entry
, NULL
, IID_PPV_ARG(IShellFolder
, &pFolder
));
314 ERR("Can't bind folder to desktop !\n");
320 // TODO: handle hidden folders according to settings !
321 EnumFlags
|= SHCONTF_INCLUDEHIDDEN
;
323 // Enum through objects
324 hr
= pFolder
->EnumObjects(NULL
,EnumFlags
,&pEnumIDList
);
326 // avoid broken IShellFolder implementations that return null pointer with success
327 if (!SUCCEEDED(hr
) || !pEnumIDList
)
329 ERR("Can't enum the folder !\n");
333 /* Don't redraw while we add stuff into the tree */
334 SendMessage(WM_SETREDRAW
, FALSE
, 0);
335 while(SUCCEEDED(pEnumIDList
->Next(1, &pidlSub
, &fetched
)) && pidlSub
&& fetched
)
337 LPITEMIDLIST pidlSubComplete
;
338 pidlSubComplete
= ILCombine(entry
, pidlSub
);
340 if (InsertItem(hItem
, pFolder
, pidlSubComplete
, pidlSub
, FALSE
))
342 ILFree(pidlSubComplete
);
345 pNodeInfo
->expanded
= TRUE
;
347 /* Now we can redraw */
348 SendMessage(WM_SETREDRAW
, TRUE
, 0);
350 return (uItemCount
> 0) ? TRUE
: FALSE
;
354 * Navigate to a given PIDL in the treeview, and return matching tree item handle
355 * - dest: The absolute PIDL we should navigate in the treeview
356 * - item: Handle of the tree item matching the PIDL
357 * - bExpand: expand collapsed nodes in order to find the right element
358 * - bInsert: insert the element at the right place if we don't find it
359 * - bSelect: select the item after we found it
361 BOOL
CExplorerBand::NavigateToPIDL(LPITEMIDLIST dest
, HTREEITEM
*item
, BOOL bExpand
, BOOL bInsert
,
369 LPITEMIDLIST relativeChild
;
380 nodeData
= GetNodeInfo(current
);
383 ERR("Something has gone wrong, no data associated to node !\n");
387 // If we found our node, give it back
388 if (!pDesktop
->CompareIDs(0, nodeData
->absolutePidl
, dest
))
391 TreeView_SelectItem(m_hWnd
, current
);
396 // Check if we are a parent of the requested item
397 relativeChild
= ILFindChild(nodeData
->absolutePidl
, dest
);
398 if (relativeChild
!= 0)
400 // Notify treeview we have children
401 tvItem
.mask
= TVIF_CHILDREN
;
402 tvItem
.hItem
= current
;
403 tvItem
.cChildren
= 1;
404 TreeView_SetItem(m_hWnd
, &tvItem
);
406 // If we can expand and the node isn't expanded yet, do it
409 if (!nodeData
->expanded
)
410 InsertSubitems(current
, nodeData
);
411 TreeView_Expand(m_hWnd
, current
, TVE_EXPAND
);
414 // Try to get a child
415 tmp
= TreeView_GetChild(m_hWnd
, current
);
418 // We have a child, let's continue with it
424 if (bInsert
&& nodeData
->expanded
)
426 // Happens when we have to create a subchild inside a child
427 current
= InsertItem(current
, dest
, relativeChild
, TRUE
);
429 // We end up here, without any children, so we found nothing
430 // Tell the parent node it has children
431 ZeroMemory(&tvItem
, sizeof(tvItem
));
437 tmp
= TreeView_GetNextSibling(m_hWnd
, current
);
445 current
= InsertItem(parent
, dest
, ILFindLastID(dest
), TRUE
);
455 BOOL
CExplorerBand::NavigateToCurrentFolder()
457 LPITEMIDLIST explorerPidl
;
458 CComPtr
<IBrowserService
> pBrowserService
;
464 hr
= IUnknown_QueryService(pSite
, SID_STopLevelBrowser
, IID_PPV_ARG(IBrowserService
, &pBrowserService
));
467 ERR("Can't get IBrowserService !\n");
471 hr
= pBrowserService
->GetPidl(&explorerPidl
);
472 if (!SUCCEEDED(hr
) || !explorerPidl
)
474 ERR("Unable to get browser PIDL !\n");
478 /* find PIDL into our explorer */
479 result
= NavigateToPIDL(explorerPidl
, &dummy
, TRUE
, FALSE
, TRUE
);
484 // *** IOleWindow methods ***
485 HRESULT STDMETHODCALLTYPE
CExplorerBand::GetWindow(HWND
*lphwnd
)
493 HRESULT STDMETHODCALLTYPE
CExplorerBand::ContextSensitiveHelp(BOOL fEnterMode
)
500 // *** IDockingWindow methods ***
501 HRESULT STDMETHODCALLTYPE
CExplorerBand::CloseDW(DWORD dwReserved
)
503 // We do nothing, we don't have anything to save yet
504 TRACE("CloseDW called\n");
508 HRESULT STDMETHODCALLTYPE
CExplorerBand::ResizeBorderDW(const RECT
*prcBorder
, IUnknown
*punkToolbarSite
, BOOL fReserved
)
510 /* Must return E_NOTIMPL according to MSDN */
514 HRESULT STDMETHODCALLTYPE
CExplorerBand::ShowDW(BOOL fShow
)
522 // *** IDeskBand methods ***
523 HRESULT STDMETHODCALLTYPE
CExplorerBand::GetBandInfo(DWORD dwBandID
, DWORD dwViewMode
, DESKBANDINFO
*pdbi
)
529 this->dwBandID
= dwBandID
;
531 if (pdbi
->dwMask
& DBIM_MINSIZE
)
533 pdbi
->ptMinSize
.x
= 200;
534 pdbi
->ptMinSize
.y
= 30;
537 if (pdbi
->dwMask
& DBIM_MAXSIZE
)
539 pdbi
->ptMaxSize
.y
= -1;
542 if (pdbi
->dwMask
& DBIM_INTEGRAL
)
544 pdbi
->ptIntegral
.y
= 1;
547 if (pdbi
->dwMask
& DBIM_ACTUAL
)
549 pdbi
->ptActual
.x
= 200;
550 pdbi
->ptActual
.y
= 30;
553 if (pdbi
->dwMask
& DBIM_TITLE
)
555 lstrcpyW(pdbi
->wszTitle
, L
"Explorer");
558 if (pdbi
->dwMask
& DBIM_MODEFLAGS
)
560 pdbi
->dwModeFlags
= DBIMF_NORMAL
| DBIMF_VARIABLEHEIGHT
;
563 if (pdbi
->dwMask
& DBIM_BKCOLOR
)
565 pdbi
->dwMask
&= ~DBIM_BKCOLOR
;
571 // *** IObjectWithSite methods ***
572 HRESULT STDMETHODCALLTYPE
CExplorerBand::SetSite(IUnknown
*pUnkSite
)
577 if (pUnkSite
== pSite
)
580 TRACE("SetSite called \n");
583 DestroyExplorerBand();
588 if (pUnkSite
!= pSite
)
596 hr
= IUnknown_GetWindow(pUnkSite
, &parentWnd
);
599 ERR("Could not get parent's window ! Status: %08lx\n", hr
);
608 SetParent(parentWnd
);
612 HWND wnd
= CreateWindow(WC_TREEVIEW
, NULL
,
613 WS_CHILD
| WS_CLIPCHILDREN
| WS_CLIPSIBLINGS
| TVS_HASLINES
| TVS_HASBUTTONS
| TVS_SHOWSELALWAYS
| TVS_EDITLABELS
/* | TVS_SINGLEEXPAND*/ , // remove TVS_SINGLEEXPAND for now since it has strange behaviour
614 0, 0, 0, 0, parentWnd
, NULL
, _AtlBaseModule
.GetModuleInstance(), NULL
);
616 // Subclass the window
619 // Initialize our treeview now
620 InitializeExplorerBand();
621 RegisterDragDrop(m_hWnd
, dynamic_cast<IDropTarget
*>(this));
626 HRESULT STDMETHODCALLTYPE
CExplorerBand::GetSite(REFIID riid
, void **ppvSite
)
635 // *** IOleCommandTarget methods ***
636 HRESULT STDMETHODCALLTYPE
CExplorerBand::QueryStatus(const GUID
*pguidCmdGroup
, ULONG cCmds
, OLECMD prgCmds
[], OLECMDTEXT
*pCmdText
)
642 HRESULT STDMETHODCALLTYPE
CExplorerBand::Exec(const GUID
*pguidCmdGroup
, DWORD nCmdID
, DWORD nCmdexecopt
, VARIANT
*pvaIn
, VARIANT
*pvaOut
)
649 // *** IServiceProvider methods ***
650 HRESULT STDMETHODCALLTYPE
CExplorerBand::QueryService(REFGUID guidService
, REFIID riid
, void **ppvObject
)
657 // *** IInputObject methods ***
658 HRESULT STDMETHODCALLTYPE
CExplorerBand::UIActivateIO(BOOL fActivate
, LPMSG lpMsg
)
665 // TODO: handle message
668 TranslateMessage(lpMsg
);
669 DispatchMessage(lpMsg
);
674 HRESULT STDMETHODCALLTYPE
CExplorerBand::HasFocusIO()
676 return bFocused
? S_OK
: S_FALSE
;
679 HRESULT STDMETHODCALLTYPE
CExplorerBand::TranslateAcceleratorIO(LPMSG lpMsg
)
681 TranslateMessage(lpMsg
);
682 DispatchMessage(lpMsg
);
687 // *** IPersist methods ***
688 HRESULT STDMETHODCALLTYPE
CExplorerBand::GetClassID(CLSID
*pClassID
)
692 memcpy(pClassID
, &CLSID_ExplorerBand
, sizeof(CLSID
));
697 // *** IPersistStream methods ***
698 HRESULT STDMETHODCALLTYPE
CExplorerBand::IsDirty()
704 HRESULT STDMETHODCALLTYPE
CExplorerBand::Load(IStream
*pStm
)
710 HRESULT STDMETHODCALLTYPE
CExplorerBand::Save(IStream
*pStm
, BOOL fClearDirty
)
716 HRESULT STDMETHODCALLTYPE
CExplorerBand::GetSizeMax(ULARGE_INTEGER
*pcbSize
)
723 // *** IWinEventHandler methods ***
724 HRESULT STDMETHODCALLTYPE
CExplorerBand::OnWinEvent(HWND hWnd
, UINT uMsg
, WPARAM wParam
, LPARAM lParam
, LRESULT
*theResult
)
726 if (uMsg
== WM_NOTIFY
)
728 NMHDR
*pNotifyHeader
= (NMHDR
*)lParam
;
729 switch (pNotifyHeader
->code
)
731 case TVN_ITEMEXPANDING
:
732 *theResult
= OnTreeItemExpanding((LPNMTREEVIEW
)lParam
);
735 OnSelectionChanged((LPNMTREEVIEW
)lParam
);
744 HRESULT STDMETHODCALLTYPE
CExplorerBand::IsWindowOwner(HWND hWnd
)
746 return (hWnd
== m_hWnd
) ? S_OK
: S_FALSE
;
749 // *** IBandNavigate methods ***
750 HRESULT STDMETHODCALLTYPE
CExplorerBand::Select(long paramC
)
756 // *** INamespaceProxy ***
757 HRESULT STDMETHODCALLTYPE
CExplorerBand::GetNavigateTarget(long paramC
, long param10
, long param14
)
763 HRESULT STDMETHODCALLTYPE
CExplorerBand::Invoke(long paramC
)
769 HRESULT STDMETHODCALLTYPE
CExplorerBand::OnSelectionChanged(long paramC
)
775 HRESULT STDMETHODCALLTYPE
CExplorerBand::RefreshFlags(long paramC
, long param10
, long param14
)
781 HRESULT STDMETHODCALLTYPE
CExplorerBand::CacheItem(long paramC
)
787 // *** IDispatch methods ***
788 HRESULT STDMETHODCALLTYPE
CExplorerBand::GetTypeInfoCount(UINT
*pctinfo
)
794 HRESULT STDMETHODCALLTYPE
CExplorerBand::GetTypeInfo(UINT iTInfo
, LCID lcid
, ITypeInfo
**ppTInfo
)
800 HRESULT STDMETHODCALLTYPE
CExplorerBand::GetIDsOfNames(REFIID riid
, LPOLESTR
*rgszNames
, UINT cNames
, LCID lcid
, DISPID
*rgDispId
)
806 HRESULT STDMETHODCALLTYPE
CExplorerBand::Invoke(DISPID dispIdMember
, REFIID riid
, LCID lcid
, WORD wFlags
, DISPPARAMS
*pDispParams
, VARIANT
*pVarResult
, EXCEPINFO
*pExcepInfo
, UINT
*puArgErr
)
808 switch (dispIdMember
)
810 case DISPID_DOWNLOADCOMPLETE
:
811 case DISPID_NAVIGATECOMPLETE2
:
812 TRACE("DISPID_NAVIGATECOMPLETE2 received\n");
813 NavigateToCurrentFolder();
816 TRACE("Unknown dispid requested: %08x\n", dispIdMember
);
820 // *** IDropTarget methods ***
821 HRESULT STDMETHODCALLTYPE
CExplorerBand::DragEnter(IDataObject
*pObj
, DWORD glfKeyState
, POINTL pt
, DWORD
*pdwEffect
)
827 HRESULT STDMETHODCALLTYPE
CExplorerBand::DragOver(DWORD glfKeyState
, POINTL pt
, DWORD
*pdwEffect
)
833 HRESULT STDMETHODCALLTYPE
CExplorerBand::DragLeave()
839 HRESULT STDMETHODCALLTYPE
CExplorerBand::Drop(IDataObject
*pObj
, DWORD glfKeyState
, POINTL pt
, DWORD
*pdwEffect
)
845 // *** IDropSource methods ***
846 HRESULT STDMETHODCALLTYPE
CExplorerBand::QueryContinueDrag(BOOL fEscapePressed
, DWORD grfKeyState
)
852 HRESULT STDMETHODCALLTYPE
CExplorerBand::GiveFeedback(DWORD dwEffect
)