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
10 #include <config_wasm_strip.h>
11
12 #include <sfx2/thumbnailview.hxx>
13 #include <sfx2/thumbnailviewitem.hxx>
14
15 #include <utility>
16
17 #include "thumbnailviewacc.hxx"
18 #include "thumbnailviewitemacc.hxx"
19
20 #include <basegfx/color/bcolortools.hxx>
21 #include <comphelper/processfactory.hxx>
22 #include <comphelper/propertyvalue.hxx>
23 #include <drawinglayer/attribute/fontattribute.hxx>
24 #include <drawinglayer/primitive2d/PolyPolygonColorPrimitive2D.hxx>
25 #include <drawinglayer/primitive2d/Primitive2DContainer.hxx>
26 #include <drawinglayer/primitive2d/textlayoutdevice.hxx>
27 #include <drawinglayer/processor2d/baseprocessor2d.hxx>
28 #include <drawinglayer/processor2d/processor2dtools.hxx>
29 #include <o3tl/safeint.hxx>
30 #include <rtl/ustring.hxx>
31 #include <sal/log.hxx>
32 #include <svtools/optionsdrawinglayer.hxx>
33 #include <comphelper/diagnose_ex.hxx>
34 #include <unotools/ucbstreamhelper.hxx>
35 #include <vcl/svapp.hxx>
36 #include <vcl/settings.hxx>
37 #include <vcl/event.hxx>
38 #include <vcl/filter/PngImageReader.hxx>
39 #include <vcl/graphicfilter.hxx>
40 #include <vcl/weldutils.hxx>
41
42 #include <com/sun/star/accessibility/AccessibleEventId.hpp>
43 #include <com/sun/star/embed/ElementModes.hpp>
44 #include <com/sun/star/embed/StorageFactory.hpp>
45 #include <com/sun/star/embed/StorageFormats.hpp>
46 #include <com/sun/star/embed/XHierarchicalStorageAccess.hpp>
47 #include <com/sun/star/embed/XRelationshipAccess.hpp>
48 #include <com/sun/star/embed/XStorage.hpp>
49 #include <com/sun/star/packages/zip/ZipFileAccess.hpp>
50
51 #include <memory>
52 #if !ENABLE_WASM_STRIP_RECENT
53 #include "recentdocsviewitem.hxx"
54 #endif
55
56 using namespace basegfx;
57 using namespace drawinglayer::attribute;
58 using namespace drawinglayer::primitive2d;
59
60 constexpr int gnFineness = 5;
61
renameItem(ThumbnailViewItem &,const OUString &)62 bool ThumbnailView::renameItem(ThumbnailViewItem&, const OUString&)
63 {
64 // Do nothing by default
65 return false;
66 }
67
68 static css::uno::Reference<css::embed::XHierarchicalStorageAccess>
getStorageAccess(const OUString & URL,sal_Int32 format)69 getStorageAccess(const OUString& URL, sal_Int32 format)
70 {
71 auto xFactory = css::embed::StorageFactory::create(comphelper::getProcessComponentContext());
72 css::uno::Sequence descriptor{ comphelper::makePropertyValue(u"StorageFormat"_ustr, format) };
73 css::uno::Sequence args{ css::uno::Any(URL), css::uno::Any(css::embed::ElementModes::READ),
74 css::uno::Any(descriptor) };
75 return xFactory->createInstanceWithArguments(args)
76 .queryThrow<css::embed::XHierarchicalStorageAccess>();
77 }
78
79 static css::uno::Reference<css::io::XInputStream>
getHierarchicalStream(const css::uno::Reference<css::embed::XHierarchicalStorageAccess> & xStorage,const OUString & name)80 getHierarchicalStream(const css::uno::Reference<css::embed::XHierarchicalStorageAccess>& xStorage,
81 const OUString& name)
82 {
83 auto xStream
84 = xStorage->openStreamElementByHierarchicalName(name, css::embed::ElementModes::READ);
85 return xStream->getInputStream();
86 }
87
88 static css::uno::Reference<css::io::XInputStream>
getFirstHierarchicalStream(const OUString & URL,sal_Int32 format,std::initializer_list<OUString> names)89 getFirstHierarchicalStream(const OUString& URL, sal_Int32 format,
90 std::initializer_list<OUString> names)
91 {
92 auto xStorage(getStorageAccess(URL, format));
93 for (const auto& name : names)
94 {
95 try
96 {
97 return getHierarchicalStream(xStorage, name);
98 }
99 catch (const css::uno::Exception&)
100 {
101 TOOLS_WARN_EXCEPTION("sfx", "caught exception while trying to access " << name << " of "
102 << URL);
103 }
104 }
105 return {};
106 }
107
108 static css::uno::Reference<css::io::XInputStream>
getFirstStreamByRelType(const OUString & URL,std::initializer_list<OUString> types)109 getFirstStreamByRelType(const OUString& URL, std::initializer_list<OUString> types)
110 {
111 auto xStorage(getStorageAccess(URL, css::embed::StorageFormats::OFOPXML));
112 if (auto xRelationshipAccess = xStorage.query<css::embed::XRelationshipAccess>())
113 {
114 for (const auto& type : types)
115 {
116 auto rels = xRelationshipAccess->getRelationshipsByType(type);
117 if (rels.hasElements())
118 {
119 // ISO/IEC 29500-1:2016(E) 15.2.16 Thumbnail Part: "Packages shall not contain
120 // more than one thumbnail relationship associated with the package as a whole"
121 for (const auto& [tag, value] : rels[0])
122 {
123 if (tag == "Id")
124 {
125 return getHierarchicalStream(xStorage,
126 xRelationshipAccess->getTargetByID(value));
127 }
128 }
129 }
130 }
131 }
132 return {};
133 }
134
readThumbnail(const OUString & msURL)135 Bitmap ThumbnailView::readThumbnail(const OUString &msURL)
136 {
137 using namespace ::com::sun::star;
138 using namespace ::com::sun::star::uno;
139
140 // Load the thumbnail from a template document.
141 uno::Reference<io::XInputStream> xIStream;
142
143 try
144 {
145 // An (older) implementation had a bug - The storage
146 // name was "Thumbnail" instead of "Thumbnails". The
147 // old name is still used as fallback but this code can
148 // be removed soon.
149 xIStream = getFirstHierarchicalStream(
150 msURL, embed::StorageFormats::PACKAGE,
151 { u"Thumbnails/thumbnail.png"_ustr, u"Thumbnail/thumbnail.png"_ustr });
152 }
153 catch (const uno::Exception&)
154 {
155 TOOLS_WARN_EXCEPTION("sfx",
156 "caught exception while trying to access thumbnail of "
157 << msURL);
158 }
159
160 if (!xIStream.is())
161 {
162 // OOXML?
163 try
164 {
165 // Check both Transitional and Strict relationships
166 xIStream = getFirstStreamByRelType(
167 msURL,
168 { u"http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail"_ustr,
169 u"http://purl.oclc.org/ooxml/officeDocument/relationships/metadata/thumbnail"_ustr });
170 }
171 catch (const uno::Exception&)
172 {
173 // Not an OOXML; fine
174 }
175 }
176
177 // Extract the image from the stream.
178 Bitmap aThumbnail;
179 if (auto pStream = utl::UcbStreamHelper::CreateStream(xIStream, /*CloseStream=*/true))
180 {
181 Graphic aGraphic = GraphicFilter::GetGraphicFilter().ImportUnloadedGraphic(*pStream);
182 aThumbnail = aGraphic.GetBitmap();
183 }
184
185 // Note that the preview is returned without scaling it to the desired
186 // width. This gives the caller the chance to take advantage of a
187 // possibly larger resolution then was asked for.
188 return aThumbnail;
189 }
190
ThumbnailView(std::unique_ptr<weld::ScrolledWindow> xWindow,std::unique_ptr<weld::Menu> xMenu)191 ThumbnailView::ThumbnailView(std::unique_ptr<weld::ScrolledWindow> xWindow, std::unique_ptr<weld::Menu> xMenu)
192 : mnThumbnailHeight(0)
193 , mnDisplayHeight(0)
194 , mnVItemSpace(-1)
195 , mbAllowVScrollBar(xWindow->get_vpolicy() != VclPolicyType::NEVER)
196 , mbSelectOnFocus(true)
197 , mpItemAttrs(new ThumbnailItemAttributes)
198 , mxScrolledWindow(std::move(xWindow))
199 , mxContextMenu(std::move(xMenu))
200 {
201 ImplInit();
202 mxScrolledWindow->connect_vadjustment_value_changed(LINK(this, ThumbnailView, ImplScrollHdl));
203 }
204
~ThumbnailView()205 ThumbnailView::~ThumbnailView()
206 {
207 ImplDeleteItems();
208
209 if (mxAccessible.is())
210 mxAccessible->dispose();
211
212 mpItemAttrs.reset();
213 }
214
MouseMove(const MouseEvent & rMEvt)215 bool ThumbnailView::MouseMove(const MouseEvent& rMEvt)
216 {
217 size_t nItemCount = mFilteredItemList.size();
218 Point aPoint = rMEvt.GetPosPixel();
219
220 for (size_t i = 0; i < nItemCount; i++)
221 {
222 ThumbnailViewItem *pItem = mFilteredItemList[i];
223 ::tools::Rectangle aToInvalidate(pItem->updateHighlight(pItem->mbVisible && !rMEvt.IsLeaveWindow(), aPoint));
224 if (!aToInvalidate.IsEmpty() && IsReallyVisible() && IsUpdateMode())
225 Invalidate(aToInvalidate);
226 }
227
228 return true;
229 }
230
RequestHelp(tools::Rectangle & rHelpRect)231 OUString ThumbnailView::RequestHelp(tools::Rectangle& rHelpRect)
232 {
233 if (!mbShowTooltips)
234 return OUString();
235
236 Point aPos = rHelpRect.TopLeft();
237 size_t nItemCount = mFilteredItemList.size();
238 for (size_t i = 0; i < nItemCount; i++)
239 {
240 ThumbnailViewItem *pItem = mFilteredItemList[i];
241 if (!pItem->mbVisible)
242 continue;
243 const tools::Rectangle& rDrawArea = pItem->getDrawArea();
244 if (rDrawArea.Contains(aPos))
245 {
246 rHelpRect = rDrawArea;
247 return pItem->getHelpText();
248 }
249 }
250
251 return OUString();
252 }
253
AppendItem(std::unique_ptr<ThumbnailViewItem> pItem)254 void ThumbnailView::AppendItem(std::unique_ptr<ThumbnailViewItem> pItem)
255 {
256 if (maFilterFunc(pItem.get()))
257 {
258 // Save current start,end range, iterator might get invalidated
259 size_t nSelStartPos = 0;
260 ThumbnailViewItem *pSelStartItem = nullptr;
261
262 if (mpStartSelRange != mFilteredItemList.end())
263 {
264 pSelStartItem = *mpStartSelRange;
265 nSelStartPos = mpStartSelRange - mFilteredItemList.begin();
266 }
267
268 mFilteredItemList.push_back(pItem.get());
269 mpStartSelRange = pSelStartItem != nullptr ? mFilteredItemList.begin() + nSelStartPos : mFilteredItemList.end();
270 }
271
272 mItemList.push_back(std::move(pItem));
273 }
274
ImplInit()275 void ThumbnailView::ImplInit()
276 {
277 mnItemWidth = 0;
278 mnItemHeight = 0;
279 mnItemPadding = 0;
280 mnVisLines = 0;
281 mnLines = 0;
282 mnFirstLine = 0;
283 mnCols = 0;
284 mbScroll = false;
285 mbHasVisibleItems = false;
286 mbShowTooltips = false;
287 mbDrawMnemonics = false;
288 mbAllowMultiSelection = true;
289 maFilterFunc = ViewFilterAll();
290
291 const StyleSettings& rSettings = Application::GetSettings().GetStyleSettings();
292 maFillColor = rSettings.GetFieldColor();
293 maTextColor = rSettings.GetWindowTextColor();
294 maHighlightColor = rSettings.GetHighlightColor();
295 maHighlightTextColor = rSettings.GetHighlightTextColor();
296
297 mfHighlightTransparence = SvtOptionsDrawinglayer::GetTransparentSelectionPercent() * 0.01;
298
299 mpStartSelRange = mFilteredItemList.end();
300
301 UpdateColors();
302
303 mpItemAttrs->nMaxTextLength = 0;
304 }
305
UpdateColors()306 void ThumbnailView::UpdateColors()
307 {
308 mpItemAttrs->aFillColor = maFillColor.getBColor();
309 mpItemAttrs->aTextColor = maTextColor.getBColor();
310 mpItemAttrs->aHighlightColor = maHighlightColor.getBColor();
311 mpItemAttrs->aHighlightTextColor = maHighlightTextColor.getBColor();
312 mpItemAttrs->fHighlightTransparence = mfHighlightTransparence;
313 }
314
ImplDeleteItems()315 void ThumbnailView::ImplDeleteItems()
316 {
317 const size_t n = mItemList.size();
318
319 for ( size_t i = 0; i < n; ++i )
320 {
321 ThumbnailViewItem *const pItem = mItemList[i].get();
322
323 // deselect all current selected items and fire events
324 if (pItem->isSelected())
325 {
326 pItem->setSelection(false);
327 maItemStateHdl.Call(pItem);
328
329 // fire accessible event???
330 }
331
332 rtl::Reference<ThumbnailViewItemAcc> xItemAcc = pItem->GetAccessible(false);
333 if (xItemAcc.is())
334 {
335 css::uno::Any aOldAny, aNewAny;
336 aOldAny <<= css::uno::Reference<css::accessibility::XAccessible>(pItem->GetAccessible());
337 ImplFireAccessibleEvent( css::accessibility::AccessibleEventId::CHILD, aOldAny, aNewAny );
338
339 xItemAcc->dispose();
340 }
341
342 mItemList[i].reset();
343 }
344
345 mItemList.clear();
346 mFilteredItemList.clear();
347
348 mpStartSelRange = mFilteredItemList.end();
349 }
350
DrawItem(ThumbnailViewItem const * pItem)351 void ThumbnailView::DrawItem(ThumbnailViewItem const *pItem)
352 {
353 if (pItem->isVisible())
354 {
355 ::tools::Rectangle aRect = pItem->getDrawArea();
356
357 if (!aRect.IsEmpty())
358 Invalidate(aRect);
359 }
360 }
361
OnItemDblClicked(ThumbnailViewItem *)362 void ThumbnailView::OnItemDblClicked (ThumbnailViewItem*)
363 {
364 }
365
CreateAccessible()366 rtl::Reference<comphelper::OAccessible> ThumbnailView::CreateAccessible()
367 {
368 mxAccessible.set(new ThumbnailViewAcc(this));
369 return mxAccessible;
370 }
371
getAccessible() const372 const rtl::Reference< ThumbnailViewAcc > & ThumbnailView::getAccessible() const
373 {
374 return mxAccessible;
375 }
376
CalculateItemPositions(bool bScrollBarUsed)377 void ThumbnailView::CalculateItemPositions(bool bScrollBarUsed)
378 {
379 if (!mnItemHeight || !mnItemWidth)
380 return;
381
382 Size aWinSize = GetOutputSizePixel();
383 size_t nItemCount = mFilteredItemList.size();
384
385 // calculate window scroll ratio
386 float nScrollRatio;
387 if (bScrollBarUsed)
388 {
389 nScrollRatio = static_cast<float>(mxScrolledWindow->vadjustment_get_value()) /
390 static_cast<float>(mxScrolledWindow->vadjustment_get_upper() -
391 mxScrolledWindow->vadjustment_get_page_size());
392 }
393 else
394 nScrollRatio = 0;
395
396 // calculate ScrollBar width
397 tools::Long nScrBarWidth = mbAllowVScrollBar ? mxScrolledWindow->get_scroll_thickness() : 0;
398
399 // calculate maximum number of visible columns
400 mnCols = static_cast<sal_uInt16>((aWinSize.Width()-nScrBarWidth) / mnItemWidth);
401
402 if (!mnCols)
403 mnCols = 1;
404
405 // calculate maximum number of visible rows
406 mnVisLines = static_cast<sal_uInt16>(aWinSize.Height() / mnItemHeight);
407
408 // calculate empty space
409 tools::Long nHSpace = aWinSize.Width()-nScrBarWidth - mnCols*mnItemWidth;
410 tools::Long nVSpace = aWinSize.Height() - mnVisLines*mnItemHeight;
411 tools::Long nHItemSpace = nHSpace / (mnCols+1);
412 tools::Long nVItemSpace = mnVItemSpace;
413 if (nVItemSpace == -1) // auto, split up extra space to use as vertical spacing
414 nVItemSpace = nVSpace / (mnVisLines+1);
415
416 // tdf#162510 - calculate maximum number of rows
417 size_t nItemCountPinned = 0;
418 #if !ENABLE_WASM_STRIP_RECENT
419 bool bPinnedItems = true;
420 for (size_t i = 0; bPinnedItems && i < nItemCount; ++i)
421 {
422 ThumbnailViewItem& rItem = *mFilteredItemList[i];
423 if (auto const pRecentDocsItem = dynamic_cast<RecentDocsViewItem*>(&rItem))
424 {
425 if (pRecentDocsItem->isPinned())
426 ++nItemCountPinned;
427 else
428 bPinnedItems = false;
429 }
430 }
431 #endif
432
433 // calculate maximum number of rows
434 // Floor( (M+N-1)/N )==Ceiling( M/N )
435 mnLines = (static_cast<tools::Long>(nItemCount - nItemCountPinned) + mnCols - 1) / mnCols;
436 // tdf#162510 - add pinned items to number of lines
437 mnLines += (static_cast<tools::Long>(nItemCountPinned) + mnCols - 1) / mnCols;
438
439 if ( !mnLines )
440 mnLines = 1;
441
442 if ( mnLines <= mnVisLines )
443 mnFirstLine = 0;
444 else if ( mnFirstLine > o3tl::make_unsigned(mnLines-mnVisLines) )
445 mnFirstLine = static_cast<sal_uInt16>(mnLines-mnVisLines);
446
447 mbHasVisibleItems = true;
448
449 tools::Long nFullSteps = (mnLines > mnVisLines) ? mnLines - mnVisLines + 1 : 1;
450
451 tools::Long nItemHeightOffset = mnItemHeight + nVItemSpace;
452 tools::Long nHiddenLines = static_cast<tools::Long>((nFullSteps - 1) * nScrollRatio);
453
454 // calculate offsets
455 tools::Long nStartX = nHItemSpace;
456 tools::Long nStartY = nVItemSpace;
457
458 // calculate and draw items
459 tools::Long x = nStartX;
460 tools::Long y = nStartY - ((nFullSteps - 1) * nScrollRatio - nHiddenLines) * nItemHeightOffset;
461
462 // draw items
463 // Unless we are scrolling (via scrollbar) we just use the precalculated
464 // mnFirstLine -- our nHiddenLines calculation takes into account only
465 // what the user has done with the scrollbar but not any changes of selection
466 // using the keyboard, meaning we could accidentally hide the selected item
467 // if we believe the scrollbar (fdo#72287).
468 size_t nFirstItem = (bScrollBarUsed ? nHiddenLines : mnFirstLine) * mnCols;
469 size_t nLastItem = nFirstItem + (mnVisLines + 1) * mnCols;
470
471 // tdf#162510 - helper for in order to handle accessibility events
472 auto handleAccessibleEvent = [&](ThumbnailViewItem& rItem, bool bIsVisible)
473 {
474 if (ImplHasAccessibleListeners())
475 {
476 css::uno::Any aOldAny, aNewAny;
477 if (bIsVisible)
478 aNewAny <<= css::uno::Reference<css::accessibility::XAccessible>(rItem.GetAccessible());
479 else
480 aOldAny <<= css::uno::Reference<css::accessibility::XAccessible>(rItem.GetAccessible());
481 ImplFireAccessibleEvent(css::accessibility::AccessibleEventId::CHILD, aOldAny, aNewAny);
482 }
483 };
484
485 // tdf#162510 - helper to set visibility and update layout
486 auto updateItemLayout = [&](ThumbnailViewItem& rItem, bool bIsVisible, size_t& nVisibleCount)
487 {
488 if (bIsVisible != rItem.isVisible())
489 {
490 handleAccessibleEvent(rItem, bIsVisible);
491 rItem.show(bIsVisible);
492 maItemStateHdl.Call(&rItem);
493 }
494
495 if (bIsVisible)
496 {
497 rItem.setDrawArea(::tools::Rectangle(Point(x, y), Size(mnItemWidth, mnItemHeight)));
498 rItem.calculateItemsPosition(mnThumbnailHeight, mnItemPadding,
499 mpItemAttrs->nMaxTextLength, mpItemAttrs.get());
500
501 if ((nVisibleCount + 1) % mnCols)
502 x += mnItemWidth + nHItemSpace;
503 else
504 {
505 x = nStartX;
506 y += mnItemHeight + nVItemSpace;
507 }
508 ++nVisibleCount;
509 }
510 };
511
512 size_t nCurCountVisible = 0;
513 #if !ENABLE_WASM_STRIP_RECENT
514 // tdf#162510 - process pinned items
515 for (size_t i = 0; i < nItemCountPinned; i++)
516 updateItemLayout(*mFilteredItemList[i], nFirstItem <= i && i < nLastItem, nCurCountVisible);
517
518 // tdf#162510 - start a new line only if the entire line is not filled with pinned items
519 if (nCurCountVisible && nCurCountVisible % mnCols)
520 {
521 x = nStartX;
522 y += mnItemHeight + nVItemSpace;
523 }
524
525 // tdf#164102 - adjust first item only if there are any pinned items
526 if (const auto nRemainingPinnedSlots = nItemCountPinned % mnCols)
527 {
528 // tdf#162510 - adjust first item to take into account the new line after pinned items
529 const auto nFirstItemAdjustment = mnCols - nRemainingPinnedSlots;
530 if (nFirstItemAdjustment <= nFirstItem)
531 nFirstItem -= nFirstItemAdjustment;
532 }
533
534 #endif
535
536 // If want also draw parts of items in the last line,
537 // then we add one more line if parts of this line are visible
538 nCurCountVisible = 0;
539 for (size_t i = nItemCountPinned; i < nItemCount; i++)
540 updateItemLayout(*mFilteredItemList[i], nFirstItem <= i && i < nLastItem, nCurCountVisible);
541
542 // check if scroll is needed
543 mbScroll = mnLines > mnVisLines;
544
545 mxScrolledWindow->vadjustment_set_upper(mnLines * gnFineness);
546 mxScrolledWindow->vadjustment_set_page_size(mnVisLines * gnFineness);
547 if (!bScrollBarUsed)
548 mxScrolledWindow->vadjustment_set_value(static_cast<tools::Long>(mnFirstLine)*gnFineness);
549 tools::Long nPageSize = mnVisLines;
550 if ( nPageSize < 1 )
551 nPageSize = 1;
552 mxScrolledWindow->vadjustment_set_page_increment(nPageSize*gnFineness);
553 if (mbAllowVScrollBar)
554 mxScrolledWindow->set_vpolicy(mbScroll ? VclPolicyType::ALWAYS : VclPolicyType::NEVER);
555 }
556
ImplGetItem(const Point & rPos) const557 size_t ThumbnailView::ImplGetItem( const Point& rPos ) const
558 {
559 if ( !mbHasVisibleItems )
560 {
561 return THUMBNAILVIEW_ITEM_NOTFOUND;
562 }
563
564 for (size_t i = 0; i < mFilteredItemList.size(); ++i)
565 {
566 if (mFilteredItemList[i]->isVisible() && mFilteredItemList[i]->getDrawArea().Contains(rPos))
567 return i;
568 }
569
570 return THUMBNAILVIEW_ITEM_NOTFOUND;
571 }
572
ImplGetItem(size_t nPos)573 ThumbnailViewItem* ThumbnailView::ImplGetItem( size_t nPos )
574 {
575 return ( nPos < mFilteredItemList.size() ) ? mFilteredItemList[nPos] : nullptr;
576 }
577
ImplGetVisibleItemCount() const578 sal_uInt16 ThumbnailView::ImplGetVisibleItemCount() const
579 {
580 sal_uInt16 nRet = 0;
581 const size_t nItemCount = mItemList.size();
582
583 for ( size_t n = 0; n < nItemCount; ++n )
584 {
585 if ( mItemList[n]->isVisible() )
586 ++nRet;
587 }
588
589 return nRet;
590 }
591
ImplGetVisibleItem(sal_uInt16 nVisiblePos)592 ThumbnailViewItem* ThumbnailView::ImplGetVisibleItem( sal_uInt16 nVisiblePos )
593 {
594 const size_t nItemCount = mItemList.size();
595
596 for ( size_t n = 0; n < nItemCount; ++n )
597 {
598 ThumbnailViewItem *const pItem = mItemList[n].get();
599
600 if ( pItem->isVisible() && !nVisiblePos-- )
601 return pItem;
602 }
603
604 return nullptr;
605 }
606
ImplFireAccessibleEvent(short nEventId,const css::uno::Any & rOldValue,const css::uno::Any & rNewValue)607 void ThumbnailView::ImplFireAccessibleEvent( short nEventId, const css::uno::Any& rOldValue, const css::uno::Any& rNewValue )
608 {
609 if( mxAccessible )
610 mxAccessible->FireAccessibleEvent( nEventId, rOldValue, rNewValue );
611 }
612
ImplHasAccessibleListeners() const613 bool ThumbnailView::ImplHasAccessibleListeners() const
614 {
615 return mxAccessible && mxAccessible->HasAccessibleListeners();
616 }
617
IMPL_LINK_NOARG(ThumbnailView,ImplScrollHdl,weld::ScrolledWindow &,void)618 IMPL_LINK_NOARG(ThumbnailView, ImplScrollHdl, weld::ScrolledWindow&, void)
619 {
620 CalculateItemPositions(true);
621 if (IsReallyVisible() && IsUpdateMode())
622 Invalidate();
623 }
624
KeyInput(const KeyEvent & rKEvt)625 bool ThumbnailView::KeyInput( const KeyEvent& rKEvt )
626 {
627 bool bHandled = true;
628
629 // Get the last selected item in the list
630 size_t nLastPos = 0;
631 bool bFoundLast = false;
632 for ( tools::Long i = mFilteredItemList.size() - 1; !bFoundLast && i >= 0; --i )
633 {
634 ThumbnailViewItem* pItem = mFilteredItemList[i];
635 if ( pItem->isSelected() )
636 {
637 nLastPos = i;
638 bFoundLast = true;
639 }
640 }
641
642 bool bValidRange = false;
643 bool bHasSelRange = mpStartSelRange != mFilteredItemList.end();
644 size_t nNextPos = nLastPos;
645 vcl::KeyCode aKeyCode = rKEvt.GetKeyCode();
646 ThumbnailViewItem* pNext = nullptr;
647
648 if (aKeyCode.IsShift() && bHasSelRange)
649 {
650 //If the last element selected is the start range position
651 //search for the first selected item
652 size_t nSelPos = mpStartSelRange - mFilteredItemList.begin();
653
654 if (nLastPos == nSelPos)
655 {
656 while (nLastPos && mFilteredItemList[nLastPos-1]->isSelected())
657 --nLastPos;
658 }
659 }
660
661 switch ( aKeyCode.GetCode() )
662 {
663 case KEY_RIGHT:
664 if (!mFilteredItemList.empty())
665 {
666 if ( bFoundLast && nLastPos + 1 < mFilteredItemList.size() )
667 {
668 bValidRange = true;
669 nNextPos = nLastPos + 1;
670 }
671
672 pNext = mFilteredItemList[nNextPos];
673 }
674 break;
675 case KEY_LEFT:
676 if (!mFilteredItemList.empty())
677 {
678 if ( nLastPos > 0 )
679 {
680 bValidRange = true;
681 nNextPos = nLastPos - 1;
682 }
683
684 pNext = mFilteredItemList[nNextPos];
685 }
686 break;
687 case KEY_DOWN:
688 if (!mFilteredItemList.empty())
689 {
690 if ( bFoundLast )
691 {
692 //If we are in the second last row just go the one in
693 //the row below, if there's not row below just go to the
694 //last item but for the last row don't do anything.
695 if ( nLastPos + mnCols < mFilteredItemList.size( ) )
696 {
697 bValidRange = true;
698 nNextPos = nLastPos + mnCols;
699 }
700 else
701 {
702 int curRow = nLastPos/mnCols;
703
704 if (curRow < mnLines-1)
705 nNextPos = mFilteredItemList.size()-1;
706 }
707 }
708
709 pNext = mFilteredItemList[nNextPos];
710 }
711 break;
712 case KEY_UP:
713 if (!mFilteredItemList.empty())
714 {
715 if ( nLastPos >= mnCols )
716 {
717 bValidRange = true;
718 nNextPos = nLastPos - mnCols;
719 }
720
721 pNext = mFilteredItemList[nNextPos];
722 }
723 break;
724 case KEY_RETURN:
725 {
726 if ( bFoundLast )
727 OnItemDblClicked( mFilteredItemList[nLastPos] );
728 }
729 [[fallthrough]];
730 default:
731 bHandled = CustomWidgetController::KeyInput(rKEvt);
732 }
733
734 if ( pNext )
735 {
736 if (aKeyCode.IsShift() && bValidRange && mbAllowMultiSelection)
737 {
738 std::pair<size_t,size_t> aRange;
739 size_t nSelPos = mpStartSelRange - mFilteredItemList.begin();
740
741 if (nLastPos < nSelPos)
742 {
743 if (nNextPos > nLastPos)
744 {
745 if ( nNextPos > nSelPos)
746 aRange = std::make_pair(nLastPos,nNextPos);
747 else
748 aRange = std::make_pair(nLastPos,nNextPos-1);
749 }
750 else
751 {
752 assert(nLastPos > 0);
753 aRange = std::make_pair(nNextPos,nLastPos-1);
754 }
755 }
756 else if (nLastPos == nSelPos)
757 {
758 if (nNextPos > nLastPos)
759 aRange = std::make_pair(nLastPos+1,nNextPos);
760 else
761 {
762 assert(nLastPos > 0);
763 aRange = std::make_pair(nNextPos,nLastPos-1);
764 }
765 }
766 else
767 {
768 if (nNextPos > nLastPos)
769 aRange = std::make_pair(nLastPos+1,nNextPos);
770 else
771 {
772 if ( nNextPos < nSelPos)
773 aRange = std::make_pair(nNextPos,nLastPos);
774 else
775 aRange = std::make_pair(nNextPos+1,nLastPos);
776 }
777 }
778
779 for (size_t i = aRange.first; i <= aRange.second; ++i)
780 {
781 if (i != nSelPos)
782 {
783 ThumbnailViewItem *pCurItem = mFilteredItemList[i];
784
785 pCurItem->setSelection(!pCurItem->isSelected());
786
787 DrawItem(pCurItem);
788
789 maItemStateHdl.Call(pCurItem);
790 }
791 }
792 }
793 else if (!aKeyCode.IsShift())
794 {
795 deselectItems();
796 SelectItem(pNext->mnId);
797
798 //Mark it as the selection range start position
799 mpStartSelRange = mFilteredItemList.begin() + nNextPos;
800 }
801
802 MakeItemVisible(pNext->mnId);
803 }
804 return bHandled;
805 }
806
MakeItemVisible(sal_uInt16 nItemId)807 void ThumbnailView::MakeItemVisible( sal_uInt16 nItemId )
808 {
809 // Get the item row
810 size_t nPos = 0;
811 bool bFound = false;
812 for ( size_t i = 0; !bFound && i < mFilteredItemList.size(); ++i )
813 {
814 ThumbnailViewItem* pItem = mFilteredItemList[i];
815 if ( pItem->mnId == nItemId )
816 {
817 nPos = i;
818 bFound = true;
819 }
820 }
821 sal_uInt16 nRow = mnCols ? nPos / mnCols : 0;
822
823 // Move the visible rows as little as possible to include that one
824 if ( nRow < mnFirstLine )
825 mnFirstLine = nRow;
826 else if ( nRow > mnFirstLine + mnVisLines )
827 mnFirstLine = nRow - mnVisLines;
828
829 CalculateItemPositions();
830 Invalidate();
831 }
832
MouseButtonDown(const MouseEvent & rMEvt)833 bool ThumbnailView::MouseButtonDown( const MouseEvent& rMEvt )
834 {
835 GrabFocus();
836
837 if (!rMEvt.IsLeft())
838 {
839 return CustomWidgetController::MouseButtonDown( rMEvt );
840 }
841
842 size_t nPos = ImplGetItem(rMEvt.GetPosPixel());
843 ThumbnailViewItem* pItem = ImplGetItem(nPos);
844
845 if ( !pItem )
846 {
847 deselectItems();
848 return CustomWidgetController::MouseButtonDown( rMEvt );
849 }
850
851 if ( rMEvt.GetClicks() == 2 )
852 {
853 OnItemDblClicked(pItem);
854 return true;
855 }
856
857 if(rMEvt.GetClicks() == 1)
858 {
859 if (rMEvt.IsMod1())
860 {
861 //Keep selected item group state and just invert current desired one state
862 pItem->setSelection(!pItem->isSelected());
863
864 //This one becomes the selection range start position if it changes its state to selected otherwise resets it
865 mpStartSelRange = pItem->isSelected() ? mFilteredItemList.begin() + nPos : mFilteredItemList.end();
866 }
867 else if (rMEvt.IsShift() && mpStartSelRange != mFilteredItemList.end())
868 {
869 std::pair<size_t,size_t> aNewRange;
870 aNewRange.first = mpStartSelRange - mFilteredItemList.begin();
871 aNewRange.second = nPos;
872
873 if (aNewRange.first > aNewRange.second)
874 std::swap(aNewRange.first,aNewRange.second);
875
876 //Deselect the ones outside of it
877 for (size_t i = 0, n = mFilteredItemList.size(); i < n; ++i)
878 {
879 ThumbnailViewItem *pCurItem = mFilteredItemList[i];
880
881 if (pCurItem->isSelected() && (i < aNewRange.first || i > aNewRange.second))
882 {
883 pCurItem->setSelection(false);
884
885 DrawItem(pCurItem);
886
887 maItemStateHdl.Call(pCurItem);
888 }
889 }
890
891 size_t nSelPos = mpStartSelRange - mFilteredItemList.begin();
892
893 //Select the items between start range and the selected item
894 if (nSelPos != nPos)
895 {
896 int dir = nSelPos < nPos ? 1 : -1;
897 size_t nCurPos = nSelPos + dir;
898
899 while (nCurPos != nPos)
900 {
901 ThumbnailViewItem *pCurItem = mFilteredItemList[nCurPos];
902
903 if (!pCurItem->isSelected())
904 {
905 pCurItem->setSelection(true);
906
907 DrawItem(pCurItem);
908
909 maItemStateHdl.Call(pCurItem);
910 }
911
912 nCurPos += dir;
913 }
914 }
915
916 pItem->setSelection(true);
917 }
918 else
919 {
920 //If we got a group of selected items deselect the rest and only keep the desired one
921 //mark items as not selected to not fire unnecessary change state events.
922 pItem->setSelection(false);
923 deselectItems();
924 pItem->setSelection(true);
925
926 //Mark as initial selection range position and reset end one
927 mpStartSelRange = mFilteredItemList.begin() + nPos;
928 }
929
930 if (!pItem->isHighlighted())
931 DrawItem(pItem);
932
933 maItemStateHdl.Call(pItem);
934
935 //fire accessible event??
936 }
937 return true;
938 }
939
SetDrawingArea(weld::DrawingArea * pDrawingArea)940 void ThumbnailView::SetDrawingArea(weld::DrawingArea* pDrawingArea)
941 {
942 CustomWidgetController::SetDrawingArea(pDrawingArea);
943
944 OutputDevice& rDevice = pDrawingArea->get_ref_device();
945 weld::SetPointFont(rDevice, pDrawingArea->get_font());
946 mpItemAttrs->aFontAttr = getFontAttributeFromVclFont(mpItemAttrs->aFontSize, rDevice.GetFont(), false, true);
947
948 SetOutputSizePixel(pDrawingArea->get_preferred_size());
949 }
950
Paint(vcl::RenderContext & rRenderContext,const::tools::Rectangle &)951 void ThumbnailView::Paint(vcl::RenderContext& rRenderContext, const ::tools::Rectangle& /*rRect*/)
952 {
953 auto popIt = rRenderContext.ScopedPush(vcl::PushFlags::ALL);
954
955 rRenderContext.SetTextFillColor();
956 rRenderContext.SetBackground(maFillColor);
957
958 size_t nItemCount = mItemList.size();
959
960 // Draw background
961 drawinglayer::primitive2d::Primitive2DContainer aSeq(1);
962 aSeq[0] = drawinglayer::primitive2d::Primitive2DReference(
963 new PolyPolygonColorPrimitive2D(
964 B2DPolyPolygon( ::tools::Polygon(::tools::Rectangle(Point(), GetOutputSizePixel()), 0, 0).getB2DPolygon()),
965 maFillColor.getBColor()));
966
967 // Create the processor and process the primitives
968 const drawinglayer::geometry::ViewInformation2D aNewViewInfos;
969
970 std::unique_ptr<drawinglayer::processor2d::BaseProcessor2D> pProcessor(
971 drawinglayer::processor2d::createProcessor2DFromOutputDevice(rRenderContext, aNewViewInfos));
972 pProcessor->process(aSeq);
973
974 // draw items
975 for (size_t i = 0; i < nItemCount; i++)
976 {
977 ThumbnailViewItem *const pItem = mItemList[i].get();
978 if (!pItem->isVisible())
979 continue;
980 pItem->Paint(pProcessor.get(), mpItemAttrs.get());
981 }
982 }
983
GetFocus()984 void ThumbnailView::GetFocus()
985 {
986 if (mbSelectOnFocus)
987 {
988 // Select the first item if nothing selected
989 int nSelected = -1;
990 for (size_t i = 0, n = mItemList.size(); i < n && nSelected == -1; ++i)
991 {
992 if (mItemList[i]->isSelected())
993 nSelected = i;
994 }
995
996 if (nSelected == -1 && !mItemList.empty())
997 {
998 ThumbnailViewItem* pFirst = nullptr;
999 if (!mFilteredItemList.empty()) {
1000 pFirst = mFilteredItemList[0];
1001 } else {
1002 pFirst = mItemList[0].get();
1003 }
1004
1005 SelectItem(pFirst->mnId);
1006 }
1007 }
1008
1009 // Tell the accessible object that we got the focus.
1010 if( mxAccessible )
1011 mxAccessible->GetFocus();
1012
1013 CustomWidgetController::GetFocus();
1014 }
1015
LoseFocus()1016 void ThumbnailView::LoseFocus()
1017 {
1018 CustomWidgetController::LoseFocus();
1019
1020 // Tell the accessible object that we lost the focus.
1021 if( mxAccessible )
1022 mxAccessible->LoseFocus();
1023 }
1024
Resize()1025 void ThumbnailView::Resize()
1026 {
1027 CustomWidgetController::Resize();
1028 CalculateItemPositions();
1029
1030 if ( IsReallyVisible() && IsUpdateMode() )
1031 Invalidate();
1032 }
1033
RemoveItem(sal_uInt16 nItemId)1034 void ThumbnailView::RemoveItem( sal_uInt16 nItemId )
1035 {
1036 size_t nPos = GetItemPos( nItemId );
1037
1038 if ( nPos == THUMBNAILVIEW_ITEM_NOTFOUND )
1039 return;
1040
1041 if ( nPos < mFilteredItemList.size() ) {
1042
1043 // keep it alive until after we have deleted it from the filter item list
1044 std::unique_ptr<ThumbnailViewItem> xKeepAliveViewItem;
1045
1046 // delete item from the thumbnail list
1047 for (auto it = mItemList.begin(); it != mItemList.end(); ++it)
1048 {
1049 if ((*it)->mnId == nItemId)
1050 {
1051 xKeepAliveViewItem = std::move(*it);
1052 mItemList.erase(it);
1053 break;
1054 }
1055 }
1056
1057 // delete item from the filter item list
1058 ThumbnailValueItemList::iterator it = mFilteredItemList.begin();
1059 ::std::advance( it, nPos );
1060
1061 if ((*it)->isSelected())
1062 {
1063 (*it)->setSelection(false);
1064 maItemStateHdl.Call(*it);
1065 }
1066
1067 mFilteredItemList.erase( it );
1068 mpStartSelRange = mFilteredItemList.end();
1069 }
1070
1071 CalculateItemPositions();
1072
1073 if ( IsReallyVisible() && IsUpdateMode() )
1074 Invalidate();
1075 }
1076
Clear()1077 void ThumbnailView::Clear()
1078 {
1079 ImplDeleteItems();
1080
1081 // reset variables
1082 mnFirstLine = 0;
1083
1084 CalculateItemPositions();
1085
1086 if ( IsReallyVisible() && IsUpdateMode() )
1087 Invalidate();
1088 }
1089
updateItems(std::vector<std::unique_ptr<ThumbnailViewItem>> items)1090 void ThumbnailView::updateItems (std::vector<std::unique_ptr<ThumbnailViewItem>> items)
1091 {
1092 ImplDeleteItems();
1093
1094 // reset variables
1095 mnFirstLine = 0;
1096
1097 mItemList = std::move(items);
1098
1099 filterItems(maFilterFunc);
1100 }
1101
GetItemPos(sal_uInt16 nItemId) const1102 size_t ThumbnailView::GetItemPos( sal_uInt16 nItemId ) const
1103 {
1104 for ( size_t i = 0, n = mFilteredItemList.size(); i < n; ++i ) {
1105 if ( mFilteredItemList[i]->mnId == nItemId ) {
1106 return i;
1107 }
1108 }
1109 return THUMBNAILVIEW_ITEM_NOTFOUND;
1110 }
1111
GetItemId(size_t nPos) const1112 sal_uInt16 ThumbnailView::GetItemId( size_t nPos ) const
1113 {
1114 return ( nPos < mFilteredItemList.size() ) ? mFilteredItemList[nPos]->mnId : 0 ;
1115 }
1116
GetItemId(const Point & rPos) const1117 sal_uInt16 ThumbnailView::GetItemId( const Point& rPos ) const
1118 {
1119 size_t nItemPos = ImplGetItem( rPos );
1120 if ( nItemPos != THUMBNAILVIEW_ITEM_NOTFOUND )
1121 return GetItemId( nItemPos );
1122
1123 return 0;
1124 }
1125
setItemMaxTextLength(sal_uInt32 nLength)1126 void ThumbnailView::setItemMaxTextLength(sal_uInt32 nLength)
1127 {
1128 mpItemAttrs->nMaxTextLength = nLength;
1129 }
1130
setItemDimensions(tools::Long itemWidth,tools::Long thumbnailHeight,tools::Long displayHeight,int itemPadding)1131 void ThumbnailView::setItemDimensions(tools::Long itemWidth, tools::Long thumbnailHeight, tools::Long displayHeight, int itemPadding)
1132 {
1133 mnItemWidth = itemWidth + 2*itemPadding;
1134 mnThumbnailHeight = thumbnailHeight;
1135 mnDisplayHeight = displayHeight;
1136 mnItemPadding = itemPadding;
1137 mnItemHeight = mnDisplayHeight + mnThumbnailHeight + 2*itemPadding;
1138 }
1139
SelectItem(sal_uInt16 nItemId)1140 void ThumbnailView::SelectItem( sal_uInt16 nItemId )
1141 {
1142 size_t nItemPos = GetItemPos( nItemId );
1143 if ( nItemPos == THUMBNAILVIEW_ITEM_NOTFOUND )
1144 return;
1145
1146 ThumbnailViewItem* pItem = mFilteredItemList[nItemPos];
1147 if (pItem->isSelected())
1148 return;
1149
1150 pItem->setSelection(true);
1151 maItemStateHdl.Call(pItem);
1152
1153 if (IsReallyVisible() && IsUpdateMode())
1154 Invalidate();
1155
1156 bool bNewOut = IsReallyVisible() && IsUpdateMode();
1157
1158 // if necessary scroll to the visible area
1159 if (mbScroll && nItemId && mnCols)
1160 {
1161 sal_uInt16 nNewLine = static_cast<sal_uInt16>(nItemPos / mnCols);
1162 if ( nNewLine < mnFirstLine )
1163 {
1164 mnFirstLine = nNewLine;
1165 }
1166 else if ( mnVisLines != 0 && nNewLine > o3tl::make_unsigned(mnFirstLine+mnVisLines-1) )
1167 {
1168 mnFirstLine = static_cast<sal_uInt16>(nNewLine-mnVisLines+1);
1169 }
1170 }
1171
1172 if ( bNewOut )
1173 {
1174 if ( IsReallyVisible() && IsUpdateMode() )
1175 Invalidate();
1176 }
1177
1178 if( !ImplHasAccessibleListeners() )
1179 return;
1180
1181 // focus event (select)
1182 const rtl::Reference<ThumbnailViewItemAcc>& pItemAcc = pItem->GetAccessible();
1183
1184 if( pItemAcc )
1185 {
1186 css::uno::Any aOldAny, aNewAny;
1187 aNewAny <<= css::uno::Reference<css::accessibility::XAccessible>( pItemAcc );
1188 ImplFireAccessibleEvent( css::accessibility::AccessibleEventId::ACTIVE_DESCENDANT_CHANGED, aOldAny, aNewAny );
1189 }
1190
1191 // selection event
1192 css::uno::Any aOldAny, aNewAny;
1193 ImplFireAccessibleEvent( css::accessibility::AccessibleEventId::SELECTION_CHANGED, aOldAny, aNewAny );
1194 }
1195
IsItemSelected(sal_uInt16 nItemId) const1196 bool ThumbnailView::IsItemSelected( sal_uInt16 nItemId ) const
1197 {
1198 size_t nItemPos = GetItemPos( nItemId );
1199 if ( nItemPos == THUMBNAILVIEW_ITEM_NOTFOUND )
1200 return false;
1201
1202 ThumbnailViewItem* pItem = mFilteredItemList[nItemPos];
1203 return pItem->isSelected();
1204 }
1205
deselectItems()1206 void ThumbnailView::deselectItems()
1207 {
1208 for (std::unique_ptr<ThumbnailViewItem>& p : mItemList)
1209 {
1210 if (p->isSelected())
1211 {
1212 p->setSelection(false);
1213
1214 maItemStateHdl.Call(p.get());
1215 }
1216 }
1217
1218 if (IsReallyVisible() && IsUpdateMode())
1219 Invalidate();
1220 }
1221
ShowTooltips(bool bShowTooltips)1222 void ThumbnailView::ShowTooltips( bool bShowTooltips )
1223 {
1224 mbShowTooltips = bShowTooltips;
1225 }
1226
DrawMnemonics(bool bDrawMnemonics)1227 void ThumbnailView::DrawMnemonics( bool bDrawMnemonics )
1228 {
1229 mbDrawMnemonics = bDrawMnemonics;
1230 }
1231
filterItems(const std::function<bool (const ThumbnailViewItem *)> & func)1232 void ThumbnailView::filterItems(const std::function<bool (const ThumbnailViewItem*)> &func)
1233 {
1234 mnFirstLine = 0; // start at the top of the list instead of the current position
1235 maFilterFunc = func;
1236
1237 size_t nSelPos = 0;
1238 bool bHasSelRange = false;
1239 ThumbnailViewItem *curSel = mpStartSelRange != mFilteredItemList.end() ? *mpStartSelRange : nullptr;
1240
1241 mFilteredItemList.clear();
1242
1243 for (size_t i = 0, n = mItemList.size(); i < n; ++i)
1244 {
1245 ThumbnailViewItem *const pItem = mItemList[i].get();
1246
1247 if (maFilterFunc(pItem))
1248 {
1249 if (curSel == pItem)
1250 {
1251 nSelPos = i;
1252 bHasSelRange = true;
1253 }
1254
1255 mFilteredItemList.push_back(pItem);
1256 }
1257 else
1258 {
1259 if( pItem->isVisible())
1260 {
1261 if ( ImplHasAccessibleListeners() )
1262 {
1263 css::uno::Any aOldAny, aNewAny;
1264
1265 aOldAny <<= css::uno::Reference<css::accessibility::XAccessible>(pItem->GetAccessible());
1266 ImplFireAccessibleEvent( css::accessibility::AccessibleEventId::CHILD, aOldAny, aNewAny );
1267 }
1268
1269 pItem->show(false);
1270 pItem->setSelection(false);
1271
1272 maItemStateHdl.Call(pItem);
1273 }
1274 }
1275 }
1276
1277 mpStartSelRange = bHasSelRange ? mFilteredItemList.begin() + nSelPos : mFilteredItemList.end();
1278 CalculateItemPositions();
1279
1280 Invalidate();
1281 }
1282
1283 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
1284