cid#1606940 Check of thread-shared field evades lock acquisition
[LibreOffice.git] / unotools / source / config / historyoptions.cxx
blob13a06c82633e73f0ddb74054dcb96c309a00442b
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
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 <unotools/historyoptions.hxx>
21 #include <com/sun/star/uno/Any.hxx>
22 #include <com/sun/star/uno/Sequence.hxx>
24 #include <com/sun/star/beans/XPropertySet.hpp>
25 #include <com/sun/star/container/XNameAccess.hpp>
26 #include <com/sun/star/container/XNameContainer.hpp>
27 #include <com/sun/star/lang/XSingleServiceFactory.hpp>
28 #include <comphelper/configurationhelper.hxx>
29 #include <comphelper/processfactory.hxx>
30 #include <comphelper/diagnose_ex.hxx>
31 #include <optional>
33 using namespace ::com::sun::star;
34 using namespace ::com::sun::star::uno;
35 using namespace ::com::sun::star::beans;
37 namespace {
38 constexpr OUString s_sItemList = u"ItemList"_ustr;
39 constexpr OUString s_sOrderList = u"OrderList"_ustr;
40 constexpr OUString s_sHistoryItemRef = u"HistoryItemRef"_ustr;
41 constexpr OUString s_sFilter = u"Filter"_ustr;
42 constexpr OUString s_sTitle = u"Title"_ustr;
43 constexpr OUString s_sThumbnail = u"Thumbnail"_ustr;
44 constexpr OUString s_sReadOnly = u"ReadOnly"_ustr;
45 constexpr OUString s_sPinned = u"Pinned"_ustr;
48 static uno::Reference<container::XNameAccess> GetConfig();
49 static uno::Reference<container::XNameAccess> GetCommonXCU();
50 static uno::Reference<container::XNameAccess> GetListAccess(
51 uno::Reference<container::XNameAccess> const & xCfg,
52 EHistoryType eHistory);
53 static void TruncateList(
54 const uno::Reference<container::XNameAccess>& xCfg,
55 const uno::Reference<container::XNameAccess>& xList,
56 sal_uInt32 nSize);
58 static void PrependItem(const uno::Reference<container::XNameAccess>& xCfg,
59 uno::Reference<container::XNameContainer>& xList, std::u16string_view sURL);
60 static void MoveItemToUnpinned(const uno::Reference<container::XNameAccess>& xCfg,
61 uno::Reference<container::XNameContainer>& xOrderList,
62 uno::Reference<container::XNameContainer>& xItemList,
63 std::u16string_view sURL);
65 static sal_uInt32 GetCapacity(const uno::Reference<container::XNameAccess>& xCommonXCU, EHistoryType eHistory);
67 namespace SvtHistoryOptions
70 void Clear(EHistoryType eHistory, const bool bClearPinnedItems)
72 try
74 uno::Reference<container::XNameAccess> xCfg = GetConfig();
75 uno::Reference<container::XNameAccess> xListAccess(GetListAccess(xCfg, eHistory));
77 // Retrieve order and item lists using name access to check properties of individual items
78 uno::Reference<container::XNameAccess> xItemList;
79 uno::Reference<container::XNameAccess> xOrderList;
80 xListAccess->getByName(s_sItemList) >>= xItemList;
81 xListAccess->getByName(s_sOrderList) >>= xOrderList;
83 // Retrieve order and item lists using name container to delete individual items
84 uno::Reference<container::XNameContainer> xOrderNode;
85 uno::Reference<container::XNameContainer> xItemNode;
86 xListAccess->getByName(s_sItemList) >>= xItemNode;
87 xListAccess->getByName(s_sOrderList) >>= xOrderNode;
89 const sal_Int32 nLength = xOrderList->getElementNames().getLength();
90 for (sal_Int32 nItem = 0; nItem < nLength; ++nItem)
92 // Retrieve single item from the order list
93 OUString sUrl;
94 uno::Reference<beans::XPropertySet> xSet;
95 xOrderList->getByName(OUString::number(nItem)) >>= xSet;
96 xSet->getPropertyValue(s_sHistoryItemRef) >>= sUrl;
98 // tdf#155698 - check if pinned items should be cleared
99 bool bIsItemPinned = false;
100 if (!bClearPinnedItems)
102 xItemList->getByName(sUrl) >>= xSet;
103 if (xSet->getPropertySetInfo()->hasPropertyByName(s_sPinned))
104 xSet->getPropertyValue(s_sPinned) >>= bIsItemPinned;
107 if (!bIsItemPinned)
109 xItemNode->removeByName(sUrl);
110 xOrderNode->removeByName(OUString::number(nItem));
114 ::comphelper::ConfigurationHelper::flush(xCfg);
116 catch(const uno::Exception&)
118 DBG_UNHANDLED_EXCEPTION("unotools.config");
122 std::vector< HistoryItem > GetList( EHistoryType eHistory )
124 std::vector< HistoryItem > aRet;
127 uno::Reference<container::XNameAccess> xCfg = GetConfig();
128 uno::Reference<container::XNameAccess> xCommonXCU = GetCommonXCU();
129 uno::Reference<container::XNameAccess> xListAccess(GetListAccess(xCfg, eHistory));
131 TruncateList(xCfg, xListAccess, GetCapacity(xCommonXCU, eHistory));
133 uno::Reference<container::XNameAccess> xItemList;
134 uno::Reference<container::XNameAccess> xOrderList;
135 xListAccess->getByName(s_sItemList) >>= xItemList;
136 xListAccess->getByName(s_sOrderList) >>= xOrderList;
138 const sal_Int32 nLength = xOrderList->getElementNames().getLength();
139 aRet.reserve(nLength);
141 for (sal_Int32 nItem = 0; nItem < nLength; ++nItem)
145 OUString sUrl;
146 uno::Reference<beans::XPropertySet> xSet;
147 xOrderList->getByName(OUString::number(nItem)) >>= xSet;
148 xSet->getPropertyValue(s_sHistoryItemRef) >>= sUrl;
150 xItemList->getByName(sUrl) >>= xSet;
151 HistoryItem aItem;
152 aItem.sURL = sUrl;
153 xSet->getPropertyValue(s_sFilter) >>= aItem.sFilter;
154 xSet->getPropertyValue(s_sTitle) >>= aItem.sTitle;
155 xSet->getPropertyValue(s_sThumbnail) >>= aItem.sThumbnail;
156 xSet->getPropertyValue(s_sReadOnly) >>= aItem.isReadOnly;
157 if (xSet->getPropertySetInfo()->hasPropertyByName(s_sPinned))
158 xSet->getPropertyValue(s_sPinned) >>= aItem.isPinned;
160 aRet.push_back(aItem);
162 catch(const uno::Exception&)
164 // <https://bugs.libreoffice.org/show_bug.cgi?id=46074>
165 // "FILEOPEN: No Recent Documents..." discusses a problem
166 // with corrupted /org.openoffice.Office/Histories/Histories
167 // configuration items; to work around that problem, simply
168 // ignore such corrupted individual items here, so that at
169 // least newly added items are successfully reported back
170 // from this function:
171 DBG_UNHANDLED_EXCEPTION("unotools.config");
175 catch(const uno::Exception&)
177 DBG_UNHANDLED_EXCEPTION("unotools.config");
179 return aRet;
182 void AppendItem(EHistoryType eHistory, const OUString& sURL, const OUString& sFilter,
183 const OUString& sTitle, const std::optional<OUString>& sThumbnail,
184 ::std::optional<bool> const oIsReadOnly)
188 uno::Reference<container::XNameAccess> xCfg = GetConfig();
189 uno::Reference<container::XNameAccess> xCommonXCU = GetCommonXCU();
190 uno::Reference<container::XNameAccess> xListAccess(GetListAccess(xCfg, eHistory));
192 TruncateList(xCfg, xListAccess, GetCapacity(xCommonXCU, eHistory));
194 sal_Int32 nMaxSize = GetCapacity(xCommonXCU, eHistory);
195 if (nMaxSize == 0)
196 return;
198 uno::Reference<container::XNameContainer> xItemList;
199 uno::Reference<container::XNameContainer> xOrderList;
200 xListAccess->getByName(s_sItemList) >>= xItemList;
201 xListAccess->getByName(s_sOrderList) >>= xOrderList;
202 sal_Int32 nLength = xOrderList->getElementNames().getLength();
204 // The item to be appended already exists
205 if (xItemList->hasByName(sURL))
207 uno::Reference<beans::XPropertySet> xSet;
208 xItemList->getByName(sURL) >>= xSet;
209 if (sThumbnail)
211 // update the thumbnail
212 xSet->setPropertyValue(s_sThumbnail, uno::Any(*sThumbnail));
214 if (oIsReadOnly)
216 xSet->setPropertyValue(s_sReadOnly, uno::Any(*oIsReadOnly));
219 // tdf#38742 - check the current pinned state of the item and move it accordingly
220 bool bIsItemPinned = false;
221 if (xSet->getPropertySetInfo()->hasPropertyByName(s_sPinned))
222 xSet->getPropertyValue(s_sPinned) >>= bIsItemPinned;
223 if (bIsItemPinned)
224 PrependItem(xCfg, xOrderList, sURL);
225 else
226 MoveItemToUnpinned(xCfg, xOrderList, xItemList, sURL);
228 else // The item to be appended does not exist yet
230 uno::Reference<beans::XPropertySet> xSet;
231 uno::Reference<lang::XSingleServiceFactory> xFac;
232 uno::Reference<uno::XInterface> xInst;
233 uno::Reference<beans::XPropertySet> xPrevSet;
234 uno::Reference<beans::XPropertySet> xNextSet;
236 // Append new item to OrderList.
237 if ( nLength == nMaxSize )
239 OUString sRemove;
240 xOrderList->getByName(OUString::number(nLength-1)) >>= xSet;
241 xSet->getPropertyValue(s_sHistoryItemRef) >>= sRemove;
244 xItemList->removeByName(sRemove);
246 catch (container::NoSuchElementException &)
248 // <https://bugs.libreoffice.org/show_bug.cgi?id=46074>
249 // "FILEOPEN: No Recent Documents..." discusses a problem
250 // with corrupted /org.openoffice.Office/Histories/Histories
251 // configuration items; to work around that problem, simply
252 // ignore such corrupted individual items here, so that at
253 // least newly added items are successfully added:
254 if (!sRemove.isEmpty())
256 throw;
260 if (nLength != nMaxSize)
262 xFac.set(xOrderList, uno::UNO_QUERY);
263 xInst = xFac->createInstance();
264 OUString sPush = OUString::number(nLength++);
265 xOrderList->insertByName(sPush, uno::Any(xInst));
267 for (sal_Int32 j=nLength-1; j>0; --j)
269 xOrderList->getByName( OUString::number(j) ) >>= xPrevSet;
270 xOrderList->getByName( OUString::number(j-1) ) >>= xNextSet;
271 OUString sTemp;
272 xNextSet->getPropertyValue(s_sHistoryItemRef) >>= sTemp;
273 xPrevSet->setPropertyValue(s_sHistoryItemRef, uno::Any(sTemp));
275 xOrderList->getByName( OUString::number(0) ) >>= xSet;
276 xSet->setPropertyValue(s_sHistoryItemRef, uno::Any(sURL));
278 // Append the item to ItemList.
279 xFac.set(xItemList, uno::UNO_QUERY);
280 xInst = xFac->createInstance();
281 xItemList->insertByName(sURL, uno::Any(xInst));
283 xSet.set(xInst, uno::UNO_QUERY);
284 xSet->setPropertyValue(s_sFilter, uno::Any(sFilter));
285 xSet->setPropertyValue(s_sTitle, uno::Any(sTitle));
286 xSet->setPropertyValue(s_sThumbnail, uno::Any(sThumbnail.value_or(OUString())));
287 if (oIsReadOnly)
289 xSet->setPropertyValue(s_sReadOnly, uno::Any(*oIsReadOnly));
292 // tdf#38742 - check the current pinned state of the item and move it accordingly
293 bool bIsItemPinned = false;
294 if (xSet->getPropertySetInfo()->hasPropertyByName(s_sPinned))
295 xSet->getPropertyValue(s_sPinned) >>= bIsItemPinned;
296 if (bIsItemPinned)
297 PrependItem(xCfg, xOrderList, sURL);
298 else
299 MoveItemToUnpinned(xCfg, xOrderList, xItemList, sURL);
301 ::comphelper::ConfigurationHelper::flush(xCfg);
304 catch(const uno::Exception&)
306 DBG_UNHANDLED_EXCEPTION("unotools.config");
310 void DeleteItem(EHistoryType eHistory, const OUString& sURL, const bool bClearPinned)
314 uno::Reference<container::XNameAccess> xCfg = GetConfig();
315 uno::Reference<container::XNameAccess> xListAccess(GetListAccess(xCfg, eHistory));
317 uno::Reference<container::XNameContainer> xItemList;
318 uno::Reference<container::XNameContainer> xOrderList;
319 xListAccess->getByName(s_sItemList) >>= xItemList;
320 xListAccess->getByName(s_sOrderList) >>= xOrderList;
321 sal_Int32 nLength = xOrderList->getElementNames().getLength();
323 // if it does not exist, nothing to do
324 if (!xItemList->hasByName(sURL))
325 return;
327 // it's the last one and pinned items can be cleared, just clear the lists
328 if (nLength == 1 && bClearPinned)
330 Clear(eHistory);
331 return;
334 // find it in the OrderList
335 sal_Int32 nFromWhere = 0;
336 bool bIsItemPinned = false;
337 for (; nFromWhere < nLength - 1; ++nFromWhere)
339 uno::Reference<beans::XPropertySet> xSet;
340 OUString aItem;
341 xOrderList->getByName(OUString::number(nFromWhere)) >>= xSet;
342 xSet->getPropertyValue(s_sHistoryItemRef) >>= aItem;
343 // tdf#155698 - check if pinned item should be deleted
344 if (!bClearPinned && xSet->getPropertySetInfo()->hasPropertyByName(s_sPinned))
345 xSet->getPropertyValue(s_sPinned) >>= bIsItemPinned;
347 if (aItem == sURL)
348 break;
351 // tdf#155698 - check if pinned item should be deleted
352 if (!bIsItemPinned)
354 // and shift the rest of the items in OrderList accordingly
355 for (sal_Int32 i = nFromWhere; i < nLength - 1; ++i)
357 uno::Reference<beans::XPropertySet> xPrevSet;
358 uno::Reference<beans::XPropertySet> xNextSet;
359 xOrderList->getByName(OUString::number(i)) >>= xPrevSet;
360 xOrderList->getByName(OUString::number(i + 1)) >>= xNextSet;
362 OUString sTemp;
363 xNextSet->getPropertyValue(s_sHistoryItemRef) >>= sTemp;
364 xPrevSet->setPropertyValue(s_sHistoryItemRef, uno::Any(sTemp));
366 xOrderList->removeByName(OUString::number(nLength - 1));
368 // and finally remove it from the ItemList
369 xItemList->removeByName(sURL);
371 ::comphelper::ConfigurationHelper::flush(xCfg);
374 catch (const uno::Exception&)
376 DBG_UNHANDLED_EXCEPTION("unotools.config");
380 void TogglePinItem(EHistoryType eHistory, const OUString& sURL)
384 uno::Reference<container::XNameAccess> xCfg = GetConfig();
385 uno::Reference<container::XNameAccess> xListAccess(GetListAccess(xCfg, eHistory));
387 uno::Reference<container::XNameContainer> xItemList;
388 xListAccess->getByName(s_sItemList) >>= xItemList;
390 // Check if item exists
391 if (xItemList->hasByName(sURL))
393 // Toggle pinned option
394 uno::Reference<beans::XPropertySet> xSet;
395 xItemList->getByName(sURL) >>= xSet;
396 bool bIsItemPinned = false;
397 if (xSet->getPropertySetInfo()->hasPropertyByName(s_sPinned))
398 xSet->getPropertyValue(s_sPinned) >>= bIsItemPinned;
399 xSet->setPropertyValue(s_sPinned, uno::Any(!bIsItemPinned));
401 uno::Reference<container::XNameContainer> xOrderList;
402 xListAccess->getByName(s_sOrderList) >>= xOrderList;
404 // Shift item to the beginning of the document list if is not pinned now
405 if (bIsItemPinned)
406 MoveItemToUnpinned(xCfg, xOrderList, xItemList, sURL);
407 else
408 PrependItem(xCfg, xOrderList, sURL);
411 catch (const uno::Exception&)
413 DBG_UNHANDLED_EXCEPTION("unotools.config");
417 } // namespace
420 static uno::Reference<container::XNameAccess> GetConfig()
422 return uno::Reference<container::XNameAccess>(
423 ::comphelper::ConfigurationHelper::openConfig(
424 ::comphelper::getProcessComponentContext(),
425 u"org.openoffice.Office.Histories/Histories"_ustr,
426 ::comphelper::EConfigurationModes::Standard),
427 uno::UNO_QUERY_THROW);
430 static uno::Reference<container::XNameAccess> GetCommonXCU()
432 return uno::Reference<container::XNameAccess>(
433 ::comphelper::ConfigurationHelper::openConfig(
434 ::comphelper::getProcessComponentContext(),
435 u"org.openoffice.Office.Common/History"_ustr,
436 ::comphelper::EConfigurationModes::Standard),
437 uno::UNO_QUERY_THROW);
440 static uno::Reference<container::XNameAccess> GetListAccess(
441 const uno::Reference<container::XNameAccess>& xCfg,
442 EHistoryType eHistory)
444 uno::Reference<container::XNameAccess> xListAccess;
445 switch (eHistory)
447 case EHistoryType::PickList:
448 xCfg->getByName(u"PickList"_ustr) >>= xListAccess;
449 break;
451 case EHistoryType::HelpBookmarks:
452 xCfg->getByName(u"HelpBookmarks"_ustr) >>= xListAccess;
453 break;
455 return xListAccess;
458 static void TruncateList(
459 const uno::Reference<container::XNameAccess>& xCfg,
460 const uno::Reference<container::XNameAccess>& xList,
461 sal_uInt32 nSize)
463 uno::Reference<container::XNameContainer> xItemList;
464 uno::Reference<container::XNameContainer> xOrderList;
465 xList->getByName(s_sOrderList) >>= xOrderList;
466 xList->getByName(s_sItemList) >>= xItemList;
468 const sal_uInt32 nLength = xOrderList->getElementNames().getLength();
469 if (nSize >= nLength)
470 return;
472 for (sal_uInt32 i=nLength-1; i>=nSize; --i)
474 uno::Reference<beans::XPropertySet> xSet;
475 OUString sTmp;
476 const OUString sRemove = OUString::number(i);
477 xOrderList->getByName(sRemove) >>= xSet;
478 xSet->getPropertyValue(s_sHistoryItemRef) >>= sTmp;
479 xItemList->removeByName(sTmp);
480 xOrderList->removeByName(sRemove);
483 ::comphelper::ConfigurationHelper::flush(xCfg);
486 static void PrependItem(const uno::Reference<container::XNameAccess>& xCfg,
487 uno::Reference<container::XNameContainer>& xList, std::u16string_view sURL)
489 uno::Reference<beans::XPropertySet> xSet;
490 const sal_Int32 nLength = xList->getElementNames().getLength();
491 for (sal_Int32 i = 0; i < nLength; i++)
493 OUString aItem;
494 xList->getByName(OUString::number(i)) >>= xSet;
495 xSet->getPropertyValue(s_sHistoryItemRef) >>= aItem;
497 if (aItem == sURL)
499 for (sal_Int32 j = i - 1; j >= 0; --j)
501 uno::Reference<beans::XPropertySet> xPrevSet;
502 uno::Reference<beans::XPropertySet> xNextSet;
503 xList->getByName(OUString::number(j + 1)) >>= xPrevSet;
504 xList->getByName(OUString::number(j)) >>= xNextSet;
506 OUString sTemp;
507 xNextSet->getPropertyValue(s_sHistoryItemRef) >>= sTemp;
508 xPrevSet->setPropertyValue(s_sHistoryItemRef, uno::Any(sTemp));
510 xList->getByName(OUString::number(0)) >>= xSet;
511 xSet->setPropertyValue(s_sHistoryItemRef, uno::Any(aItem));
512 ::comphelper::ConfigurationHelper::flush(xCfg);
513 return;
518 static void MoveItemToUnpinned(const uno::Reference<container::XNameAccess>& xCfg,
519 uno::Reference<container::XNameContainer>& xOrderList,
520 uno::Reference<container::XNameContainer>& xItemList,
521 std::u16string_view sURL)
523 uno::Reference<beans::XPropertySet> xSet;
524 const sal_Int32 nLength = xOrderList->getElementNames().getLength();
525 // Search for item in the ordered list list
526 for (sal_Int32 i = 0; i < nLength; i++)
528 OUString aItem;
529 xOrderList->getByName(OUString::number(i)) >>= xSet;
530 xSet->getPropertyValue(s_sHistoryItemRef) >>= aItem;
532 if (aItem == sURL)
534 // Move item to the unpinned document section to the right if it was previously pinned
535 for (sal_Int32 j = i + 1; j < nLength; j++)
537 uno::Reference<beans::XPropertySet> xNextSet;
538 xOrderList->getByName(OUString::number(j)) >>= xNextSet;
540 OUString aNextItem;
541 xNextSet->getPropertyValue(s_sHistoryItemRef) >>= aNextItem;
543 uno::Reference<beans::XPropertySet> xNextItemSet;
544 xItemList->getByName(aNextItem) >>= xNextItemSet;
545 bool bIsItemPinned = false;
546 if (xNextItemSet->getPropertySetInfo()->hasPropertyByName(s_sPinned))
547 xNextItemSet->getPropertyValue(s_sPinned) >>= bIsItemPinned;
548 if (bIsItemPinned)
550 xOrderList->getByName(OUString::number(j - 1)) >>= xSet;
551 xSet->getPropertyValue(s_sHistoryItemRef) >>= aItem;
552 xSet->setPropertyValue(s_sHistoryItemRef, uno::Any(aNextItem));
553 xNextSet->setPropertyValue(s_sHistoryItemRef, uno::Any(aItem));
555 else
556 break;
559 // Move item to the unpinned document section to the left if it was previously unpinned
560 for (sal_Int32 j = i - 1; j >= 0; --j)
562 uno::Reference<beans::XPropertySet> xPrevSet;
563 xOrderList->getByName(OUString::number(j)) >>= xPrevSet;
565 OUString aPrevItem;
566 xPrevSet->getPropertyValue(s_sHistoryItemRef) >>= aPrevItem;
568 uno::Reference<beans::XPropertySet> xPrevItemSet;
569 xItemList->getByName(aPrevItem) >>= xPrevItemSet;
570 bool bIsItemPinned = false;
571 if (xPrevItemSet->getPropertySetInfo()->hasPropertyByName(s_sPinned))
572 xPrevItemSet->getPropertyValue(s_sPinned) >>= bIsItemPinned;
573 if (!bIsItemPinned)
575 xOrderList->getByName(OUString::number(j + 1)) >>= xSet;
576 xSet->getPropertyValue(s_sHistoryItemRef) >>= aItem;
577 xSet->setPropertyValue(s_sHistoryItemRef, uno::Any(aPrevItem));
578 xPrevSet->setPropertyValue(s_sHistoryItemRef, uno::Any(aItem));
580 else
581 break;
584 ::comphelper::ConfigurationHelper::flush(xCfg);
585 return;
590 static sal_uInt32 GetCapacity(const uno::Reference<container::XNameAccess>& xCommonXCU, EHistoryType eHistory)
592 uno::Reference<beans::XPropertySet> xListAccess(xCommonXCU, uno::UNO_QUERY_THROW);
594 sal_uInt32 nSize = 0;
596 switch (eHistory)
598 case EHistoryType::PickList:
599 xListAccess->getPropertyValue(u"PickListSize"_ustr) >>= nSize;
600 break;
602 case EHistoryType::HelpBookmarks:
603 xListAccess->getPropertyValue(u"HelpBookmarkSize"_ustr) >>= nSize;
604 break;
607 return nSize;
610 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */