/src/libreoffice/sfx2/source/appl/appdde.cxx
Line | Count | Source |
1 | | /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
2 | | /* |
3 | | * This file is part of the LibreOffice project. |
4 | | * |
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/. |
8 | | * |
9 | | * This file incorporates work covered by the following license notice: |
10 | | * |
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 . |
18 | | */ |
19 | | |
20 | | #include <sal/config.h> |
21 | | |
22 | | #include <string_view> |
23 | | |
24 | | #include <config_features.h> |
25 | | #include <rtl/character.hxx> |
26 | | #include <rtl/malformeduriexception.hxx> |
27 | | #include <rtl/uri.hxx> |
28 | | #include <sot/exchange.hxx> |
29 | | #include <svl/eitem.hxx> |
30 | | #include <basic/sbstar.hxx> |
31 | | #include <svl/stritem.hxx> |
32 | | #include <svl/svdde.hxx> |
33 | | #include <sfx2/lnkbase.hxx> |
34 | | #include <sfx2/linkmgr.hxx> |
35 | | |
36 | | #include <o3tl/test_info.hxx> |
37 | | #include <tools/debug.hxx> |
38 | | #include <tools/urlobj.hxx> |
39 | | #include <comphelper/diagnose_ex.hxx> |
40 | | #include <unotools/pathoptions.hxx> |
41 | | #include <vcl/svapp.hxx> |
42 | | |
43 | | #include <sfx2/app.hxx> |
44 | | #include <appdata.hxx> |
45 | | #include <sfx2/objsh.hxx> |
46 | | #include <sfx2/viewfrm.hxx> |
47 | | #include <sfx2/dispatch.hxx> |
48 | | #include <sfx2/sfxsids.hrc> |
49 | | #include <sfx2/docfile.hxx> |
50 | | #include <ucbhelper/content.hxx> |
51 | | #include <comphelper/processfactory.hxx> |
52 | | |
53 | | #if defined(_WIN32) |
54 | | |
55 | | static OUString SfxDdeServiceName_Impl( const OUString& sIn ) |
56 | | { |
57 | | OUStringBuffer sReturn(sIn.getLength()); |
58 | | |
59 | | for ( sal_uInt16 n = sIn.getLength(); n; --n ) |
60 | | { |
61 | | sal_Unicode cChar = sIn[n-1]; |
62 | | if (rtl::isAsciiAlphanumeric(cChar)) |
63 | | sReturn.append(cChar); |
64 | | } |
65 | | |
66 | | return sReturn.makeStringAndClear(); |
67 | | } |
68 | | |
69 | | namespace { |
70 | | |
71 | | class ImplDdeService : public DdeService |
72 | | { |
73 | | public: |
74 | | explicit ImplDdeService( const OUString& rNm ) |
75 | | : DdeService( rNm ) |
76 | | {} |
77 | | virtual bool MakeTopic( const OUString& ) override; |
78 | | |
79 | | virtual OUString Topics(); |
80 | | |
81 | | virtual bool SysTopicExecute( const OUString* pStr ); |
82 | | }; |
83 | | |
84 | | bool lcl_IsDocument( std::u16string_view rContent ) |
85 | | { |
86 | | using namespace com::sun::star; |
87 | | |
88 | | bool bRet = false; |
89 | | INetURLObject aObj( rContent ); |
90 | | DBG_ASSERT( aObj.GetProtocol() != INetProtocol::NotValid, "Invalid URL!" ); |
91 | | |
92 | | try |
93 | | { |
94 | | ::ucbhelper::Content aCnt( aObj.GetMainURL( INetURLObject::DecodeMechanism::NONE ), uno::Reference< ucb::XCommandEnvironment >(), comphelper::getProcessComponentContext() ); |
95 | | bRet = aCnt.isDocument(); |
96 | | } |
97 | | catch( const uno::Exception& ) |
98 | | { |
99 | | TOOLS_WARN_EXCEPTION( "sfx.appl", "" ); |
100 | | } |
101 | | |
102 | | return bRet; |
103 | | } |
104 | | } |
105 | | |
106 | | bool ImplDdeService::MakeTopic( const OUString& rNm ) |
107 | | { |
108 | | // Workaround for Event after Main() under OS/2 |
109 | | // happens when exiting starts the App again |
110 | | if ( !Application::IsInExecute() && !o3tl::IsRunningUnitTest() ) |
111 | | return false; |
112 | | |
113 | | // The Topic rNm is sought, do we have it? |
114 | | // First only loop over the ObjectShells to find those |
115 | | // with the specific name: |
116 | | bool bRet = false; |
117 | | OUString sNm( rNm.toAsciiLowerCase() ); |
118 | | SfxObjectShell* pShell = SfxObjectShell::GetFirst(); |
119 | | while( pShell ) |
120 | | { |
121 | | OUString sTmp( pShell->GetTitle(SFX_TITLE_FULLNAME) ); |
122 | | if( sNm == sTmp.toAsciiLowerCase() ) |
123 | | { |
124 | | SfxGetpApp()->AddDdeTopic( pShell ); |
125 | | bRet = true; |
126 | | break; |
127 | | } |
128 | | pShell = SfxObjectShell::GetNext( *pShell ); |
129 | | } |
130 | | |
131 | | if( !bRet ) |
132 | | { |
133 | | bool abs; |
134 | | OUString url; |
135 | | try { |
136 | | url = rtl::Uri::convertRelToAbs(SvtPathOptions().GetWorkPath(), rNm); |
137 | | abs = true; |
138 | | } catch (rtl::MalformedUriException &) { |
139 | | abs = false; |
140 | | } |
141 | | if ( abs && lcl_IsDocument( url ) ) |
142 | | { |
143 | | // File exists? then try to load it: |
144 | | SfxStringItem aName( SID_FILE_NAME, url ); |
145 | | SfxBoolItem aNewView(SID_OPEN_NEW_VIEW, true); |
146 | | |
147 | | SfxBoolItem aSilent(SID_SILENT, true); |
148 | | const SfxPoolItemHolder aResult(SfxGetpApp()->GetDispatcher_Impl()->ExecuteList(SID_OPENDOC, |
149 | | SfxCallMode::SYNCHRON, |
150 | | { &aName, &aNewView, &aSilent })); |
151 | | |
152 | | if( auto const item = dynamic_cast< const SfxViewFrameItem *>(aResult.getItem()); |
153 | | item && |
154 | | item->GetFrame() && |
155 | | nullptr != ( pShell = item->GetFrame()->GetObjectShell() ) ) |
156 | | { |
157 | | SfxGetpApp()->AddDdeTopic( pShell ); |
158 | | bRet = true; |
159 | | } |
160 | | } |
161 | | } |
162 | | return bRet; |
163 | | } |
164 | | |
165 | | OUString ImplDdeService::Topics() |
166 | | { |
167 | | OUString sRet; |
168 | | if( GetSysTopic() ) |
169 | | sRet += GetSysTopic()->GetName(); |
170 | | |
171 | | SfxObjectShell* pShell = SfxObjectShell::GetFirst(); |
172 | | while( pShell ) |
173 | | { |
174 | | if( SfxViewFrame::GetFirst( pShell ) ) |
175 | | { |
176 | | if( !sRet.isEmpty() ) |
177 | | sRet += "\t"; |
178 | | sRet += pShell->GetTitle(SFX_TITLE_FULLNAME); |
179 | | } |
180 | | pShell = SfxObjectShell::GetNext( *pShell ); |
181 | | } |
182 | | if( !sRet.isEmpty() ) |
183 | | sRet += "\r\n"; |
184 | | return sRet; |
185 | | } |
186 | | |
187 | | bool ImplDdeService::SysTopicExecute( const OUString* pStr ) |
188 | | { |
189 | | return SfxApplication::DdeExecute( *pStr ); |
190 | | } |
191 | | #endif |
192 | | |
193 | | class SfxDdeDocTopic_Impl : public DdeTopic |
194 | | { |
195 | | #if defined(_WIN32) |
196 | | public: |
197 | | SfxObjectShell* pSh; |
198 | | DdeData aData; |
199 | | css::uno::Sequence< sal_Int8 > aSeq; |
200 | | |
201 | | explicit SfxDdeDocTopic_Impl( SfxObjectShell* pShell ) |
202 | | : DdeTopic( pShell->GetTitle(SFX_TITLE_FULLNAME) ), pSh( pShell ) |
203 | | {} |
204 | | |
205 | | virtual DdeData* Get( SotClipboardFormatId ) override; |
206 | | virtual bool Put( const DdeData* ) override; |
207 | | virtual bool Execute( const OUString* ) override; |
208 | | virtual bool StartAdviseLoop() override; |
209 | | virtual bool MakeItem( const OUString& rItem ) override; |
210 | | #endif |
211 | | }; |
212 | | |
213 | | |
214 | | #if defined(_WIN32) |
215 | | |
216 | | namespace { |
217 | | |
218 | | /* [Description] |
219 | | |
220 | | Checks if 'rCmd' of the event 'rEvent' is (without '(') and then assemble |
221 | | this data into a <ApplicationEvent>, which is then executed through |
222 | | <Application::AppEvent()>. If 'rCmd' is the given event 'rEvent', then |
223 | | TRUE is returned, otherwise FALSE. |
224 | | |
225 | | [Example] |
226 | | |
227 | | rCmd = "Open(\"d:\doc\doc.sdw\")" |
228 | | rEvent = "Open" |
229 | | */ |
230 | | bool SfxAppEvent_Impl( const OUString& rCmd, std::u16string_view rEvent, |
231 | | ApplicationEvent::Type eType ) |
232 | | { |
233 | | OUString sEvent(OUString::Concat(rEvent) + "("); |
234 | | if (rCmd.startsWithIgnoreAsciiCase(sEvent)) |
235 | | { |
236 | | sal_Int32 start = sEvent.getLength(); |
237 | | if ( rCmd.getLength() - start >= 2 ) |
238 | | { |
239 | | // Transform into the ApplicationEvent Format |
240 | | //TODO: I /assume/ that rCmd should match the syntax of |
241 | | // <http://msdn.microsoft.com/en-us/library/ms648995.aspx> |
242 | | // "WM_DDE_EXECUTE message" but does not (handle commands enclosed |
243 | | // in [...]; handle commas separating multiple arguments; handle |
244 | | // double "", ((, )), [[, ]] in quoted arguments); see also the mail |
245 | | // thread starting at <http://lists.freedesktop.org/archives/ |
246 | | // libreoffice/2013-July/054779.html> "DDE on Windows." |
247 | | std::vector<OUString> aData; |
248 | | for ( sal_Int32 n = start; n < rCmd.getLength() - 1; ) |
249 | | { |
250 | | // Resiliently read arguments either starting with " and |
251 | | // spanning to the next " (if any; TODO: do we need to undo any |
252 | | // escaping within the string?) or with neither " nor SPC and |
253 | | // spanning to the next SPC (if any; TODO: is this from not |
254 | | // wrapped in "..." relevant? it would have been parsed by the |
255 | | // original code even if that was only by accident, so I left it |
256 | | // in), with runs of SPCs treated like single ones: |
257 | | switch ( rCmd[n] ) |
258 | | { |
259 | | case '"': |
260 | | { |
261 | | sal_Int32 i = rCmd.indexOf('"', ++n); |
262 | | if (i < 0 || i > rCmd.getLength() - 1) { |
263 | | i = rCmd.getLength() - 1; |
264 | | } |
265 | | aData.push_back(rCmd.copy(n, i - n)); |
266 | | n = i + 1; |
267 | | break; |
268 | | } |
269 | | case ' ': |
270 | | ++n; |
271 | | break; |
272 | | default: |
273 | | { |
274 | | sal_Int32 i = rCmd.indexOf(' ', n); |
275 | | if (i < 0 || i > rCmd.getLength() - 1) { |
276 | | i = rCmd.getLength() - 1; |
277 | | } |
278 | | aData.push_back(rCmd.copy(n, i - n)); |
279 | | n = i + 1; |
280 | | break; |
281 | | } |
282 | | } |
283 | | } |
284 | | |
285 | | GetpApp()->AppEvent( ApplicationEvent(eType, std::move(aData)) ); |
286 | | return true; |
287 | | } |
288 | | } |
289 | | |
290 | | return false; |
291 | | } |
292 | | |
293 | | } |
294 | | |
295 | | /* Description] |
296 | | |
297 | | This method can be overridden by application developers, to receive |
298 | | DDE-commands directed to their SfxApplication subclass. |
299 | | |
300 | | The base implementation understands the API functionality of the |
301 | | relevant SfxApplication subclass in BASIC syntax. Return values can |
302 | | not be transferred, unfortunately. |
303 | | */ |
304 | | bool SfxApplication::DdeExecute( const OUString& rCmd ) // Expressed in our BASIC-Syntax |
305 | | { |
306 | | // Print or Open-Event? |
307 | | if ( !( SfxAppEvent_Impl( rCmd, u"Print", ApplicationEvent::Type::Print ) || |
308 | | SfxAppEvent_Impl( rCmd, u"Open", ApplicationEvent::Type::Open ) ) ) |
309 | | { |
310 | | // all others are BASIC |
311 | | StarBASIC* pBasic = GetBasic(); |
312 | | DBG_ASSERT( pBasic, "Where is the Basic???" ); |
313 | | SbxVariable* pRet = pBasic->Execute( rCmd ); |
314 | | if( !pRet ) |
315 | | { |
316 | | SbxBase::ResetError(); |
317 | | return false; |
318 | | } |
319 | | } |
320 | | return true; |
321 | | } |
322 | | |
323 | | /* [Description] |
324 | | |
325 | | This method can be overridden by application developers, to receive |
326 | | DDE-commands directed to the their SfxApplication subclass. |
327 | | |
328 | | The base implementation does nothing and returns 0. |
329 | | */ |
330 | | bool SfxObjectShell::DdeExecute( const OUString& rCmd ) // Expressed in our BASIC-Syntax |
331 | | { |
332 | | #if !HAVE_FEATURE_SCRIPTING |
333 | | (void) rCmd; |
334 | | #else |
335 | | StarBASIC* pBasic = GetBasic(); |
336 | | DBG_ASSERT( pBasic, "Where is the Basic???" ) ; |
337 | | SbxVariable* pRet = pBasic->Execute( rCmd ); |
338 | | if( !pRet ) |
339 | | { |
340 | | SbxBase::ResetError(); |
341 | | return false; |
342 | | } |
343 | | #endif |
344 | | return true; |
345 | | } |
346 | | |
347 | | /* [Description] |
348 | | |
349 | | This method can be overridden by application developers, to receive |
350 | | DDE-data-requests directed to their SfxApplication subclass. |
351 | | |
352 | | The base implementation provides no data and returns false. |
353 | | */ |
354 | | bool SfxObjectShell::DdeGetData( const OUString&, // the Item to be addressed |
355 | | const OUString&, // in: Format |
356 | | css::uno::Any& )// out: requested data |
357 | | { |
358 | | return false; |
359 | | } |
360 | | |
361 | | |
362 | | /* [Description] |
363 | | |
364 | | This method can be overridden by application developers, to receive |
365 | | DDE-data directed to their SfxApplication subclass. |
366 | | |
367 | | The base implementation is not receiving any data and returns false. |
368 | | */ |
369 | | bool SfxObjectShell::DdeSetData( const OUString&, // the Item to be addressed |
370 | | const OUString&, // in: Format |
371 | | const css::uno::Any& )// out: requested data |
372 | | { |
373 | | return false; |
374 | | } |
375 | | |
376 | | #endif |
377 | | |
378 | | /* [Description] |
379 | | |
380 | | This method can be overridden by application developers, to establish |
381 | | a DDE-hotlink to their SfxApplication subclass. |
382 | | |
383 | | The base implementation is not generate a link and returns 0. |
384 | | */ |
385 | | ::sfx2::SvLinkSource* SfxObjectShell::DdeCreateLinkSource( const OUString& ) // the Item to be addressed |
386 | 0 | { |
387 | 0 | return nullptr; |
388 | 0 | } |
389 | | |
390 | | void SfxObjectShell::ReconnectDdeLink(SfxObjectShell& /*rServer*/) |
391 | 0 | { |
392 | 0 | } |
393 | | |
394 | | void SfxObjectShell::ReconnectDdeLinks(SfxObjectShell& rServer) |
395 | 0 | { |
396 | 0 | SfxObjectShell* p = GetFirst(nullptr, false); |
397 | 0 | while (p) |
398 | 0 | { |
399 | 0 | if (&rServer != p) |
400 | 0 | p->ReconnectDdeLink(rServer); |
401 | |
|
402 | 0 | p = GetNext(*p, nullptr, false); |
403 | 0 | } |
404 | 0 | } |
405 | | |
406 | | bool SfxApplication::InitializeDde() |
407 | 26 | { |
408 | 26 | int nError = 0; |
409 | | #if defined(_WIN32) |
410 | | DBG_ASSERT( !pImpl->pDdeService, |
411 | | "Dde can not be initialized multiple times" ); |
412 | | |
413 | | pImpl->pDdeService.reset(new ImplDdeService( Application::GetAppName() )); |
414 | | nError = pImpl->pDdeService->GetError(); |
415 | | if( !nError ) |
416 | | { |
417 | | // we certainly want to support RTF! |
418 | | pImpl->pDdeService->AddFormat( SotClipboardFormatId::RTF ); |
419 | | pImpl->pDdeService->AddFormat( SotClipboardFormatId::RICHTEXT ); |
420 | | |
421 | | // Config path as a topic because of multiple starts |
422 | | INetURLObject aOfficeLockFile( SvtPathOptions().GetUserConfigPath() ); |
423 | | aOfficeLockFile.insertName( u"soffice.lck" ); |
424 | | OUString aService( SfxDdeServiceName_Impl( |
425 | | aOfficeLockFile.GetMainURL(INetURLObject::DecodeMechanism::ToIUri) ) ); |
426 | | aService = aService.toAsciiUpperCase(); |
427 | | pImpl->pDdeService2.reset( new ImplDdeService( aService )); |
428 | | pImpl->pTriggerTopic.reset(new SfxDdeTriggerTopic_Impl); |
429 | | pImpl->pDdeService2->AddTopic( *pImpl->pTriggerTopic ); |
430 | | } |
431 | | #endif |
432 | 26 | return !nError; |
433 | 26 | } |
434 | | |
435 | | void SfxAppData_Impl::DeInitDDE() |
436 | 0 | { |
437 | 0 | pTriggerTopic.reset(); |
438 | 0 | pDdeService2.reset(); |
439 | 0 | maDocTopics.clear(); |
440 | 0 | pDdeService.reset(); |
441 | 0 | } |
442 | | |
443 | | #if defined(_WIN32) |
444 | | void SfxApplication::AddDdeTopic( SfxObjectShell* pSh ) |
445 | | { |
446 | | //OV: DDE is disconnected in server mode! |
447 | | if (!pImpl->pDdeService) |
448 | | return; |
449 | | |
450 | | // prevent double submit |
451 | | OUString sShellNm; |
452 | | bool bFnd = false; |
453 | | for (size_t n = pImpl->maDocTopics.size(); n;) |
454 | | { |
455 | | if( pImpl->maDocTopics[ --n ]->pSh == pSh ) |
456 | | { |
457 | | // If the document is untitled, is still a new Topic is created! |
458 | | if( !bFnd ) |
459 | | { |
460 | | bFnd = true; |
461 | | sShellNm = pSh->GetTitle(SFX_TITLE_FULLNAME).toAsciiLowerCase(); |
462 | | } |
463 | | OUString sNm( pImpl->maDocTopics[ n ]->GetName() ); |
464 | | if( sShellNm == sNm.toAsciiLowerCase() ) |
465 | | return ; |
466 | | } |
467 | | } |
468 | | |
469 | | SfxDdeDocTopic_Impl *const pTopic = new SfxDdeDocTopic_Impl(pSh); |
470 | | pImpl->maDocTopics.push_back(pTopic); |
471 | | pImpl->pDdeService->AddTopic( *pTopic ); |
472 | | } |
473 | | #endif |
474 | | |
475 | | void SfxApplication::RemoveDdeTopic( SfxObjectShell const * pSh ) |
476 | 0 | { |
477 | | #if defined(_WIN32) |
478 | | //OV: DDE is disconnected in server mode! |
479 | | if( pImpl->maDocTopics.empty() ) |
480 | | return; |
481 | | |
482 | | for (size_t n = pImpl->maDocTopics.size(); n; ) |
483 | | { |
484 | | SfxDdeDocTopic_Impl *const pTopic = pImpl->maDocTopics[ --n ]; |
485 | | if (pTopic->pSh == pSh) |
486 | | { |
487 | | pImpl->pDdeService->RemoveTopic( *pTopic ); |
488 | | delete pTopic; |
489 | | pImpl->maDocTopics.erase( pImpl->maDocTopics.begin() + n ); |
490 | | } |
491 | | } |
492 | | #else |
493 | 0 | (void) pSh; |
494 | 0 | #endif |
495 | 0 | } |
496 | | |
497 | | const DdeService* SfxApplication::GetDdeService() const |
498 | 0 | { |
499 | 0 | return pImpl->pDdeService.get(); |
500 | 0 | } |
501 | | |
502 | | DdeService* SfxApplication::GetDdeService() |
503 | 261k | { |
504 | 261k | return pImpl->pDdeService.get(); |
505 | 261k | } |
506 | | |
507 | | #if defined(_WIN32) |
508 | | |
509 | | DdeData* SfxDdeDocTopic_Impl::Get(SotClipboardFormatId nFormat) |
510 | | { |
511 | | OUString sMimeType( SotExchange::GetFormatMimeType( nFormat )); |
512 | | css::uno::Any aValue; |
513 | | bool bRet = pSh->DdeGetData( GetCurItem(), sMimeType, aValue ); |
514 | | if( bRet && aValue.hasValue() && ( aValue >>= aSeq ) ) |
515 | | { |
516 | | aData = DdeData( aSeq.getConstArray(), aSeq.getLength(), nFormat ); |
517 | | return &aData; |
518 | | } |
519 | | aSeq.realloc( 0 ); |
520 | | return nullptr; |
521 | | } |
522 | | |
523 | | bool SfxDdeDocTopic_Impl::Put( const DdeData* pData ) |
524 | | { |
525 | | if (pData->getSize()) |
526 | | { |
527 | | css::uno::Any aValue; |
528 | | if (pData->GetFormat() == SotClipboardFormatId::STRING) |
529 | | { |
530 | | aValue <<= OUString(reinterpret_cast<const sal_Unicode*>(pData->getData())); |
531 | | } |
532 | | else |
533 | | { |
534 | | aValue <<= css::uno::Sequence(static_cast<sal_Int8 const*>(pData->getData()), |
535 | | pData->getSize()); |
536 | | } |
537 | | return pSh->DdeSetData(GetCurItem(), SotExchange::GetFormatMimeType(pData->GetFormat()), |
538 | | aValue); |
539 | | } |
540 | | return false; |
541 | | } |
542 | | |
543 | | bool SfxDdeDocTopic_Impl::Execute( const OUString* pStr ) |
544 | | { |
545 | | return pStr && pSh->DdeExecute( *pStr ); |
546 | | } |
547 | | |
548 | | bool SfxDdeDocTopic_Impl::MakeItem( const OUString& rItem ) |
549 | | { |
550 | | AddItem( DdeItem( rItem ) ); |
551 | | return true; |
552 | | } |
553 | | |
554 | | bool SfxDdeDocTopic_Impl::StartAdviseLoop() |
555 | | { |
556 | | bool bRet = false; |
557 | | ::sfx2::SvLinkSource* pNewObj = pSh->DdeCreateLinkSource( GetCurItem() ); |
558 | | if( pNewObj ) |
559 | | { |
560 | | // then we also establish a corresponding SvBaseLink |
561 | | OUString sNm, sTmp( Application::GetAppName() ); |
562 | | ::sfx2::MakeLnkName( sNm, &sTmp, pSh->GetTitle(SFX_TITLE_FULLNAME), GetCurItem() ); |
563 | | new ::sfx2::SvBaseLink( sNm, sfx2::SvBaseLinkObjectType::DdeExternal, pNewObj ); |
564 | | bRet = true; |
565 | | } |
566 | | return bRet; |
567 | | } |
568 | | |
569 | | #endif |
570 | | |
571 | | /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |