bump product version to 6.3.0.0.beta1
[LibreOffice.git] / cui / source / dialogs / screenshotannotationdlg.cxx
blobe567944d226c66a6a45108c997982753365c02e6
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 <screenshotannotationdlg.hxx>
22 #include <strings.hrc>
23 #include <dialmgr.hxx>
25 #include <basegfx/range/b2irange.hxx>
26 #include <cppuhelper/bootstrap.hxx>
27 #include <com/sun/star/ui/dialogs/FilePicker.hpp>
28 #include <com/sun/star/ui/dialogs/TemplateDescription.hpp>
29 #include <com/sun/star/ui/dialogs/ExecutableDialogResults.hpp>
31 #include <comphelper/random.hxx>
32 #include <vcl/pngwrite.hxx>
33 #include <basegfx/polygon/b2dpolygontools.hxx>
34 #include <tools/stream.hxx>
35 #include <tools/urlobj.hxx>
36 #include <vcl/event.hxx>
37 #include <vcl/fixed.hxx>
38 #include <vcl/svapp.hxx>
39 #include <vcl/salgtype.hxx>
40 #include <vcl/virdev.hxx>
41 #include <vcl/vclmedit.hxx>
42 #include <vcl/button.hxx>
43 #include <svtools/optionsdrawinglayer.hxx>
44 #include <basegfx/matrix/b2dhommatrix.hxx>
45 #include <set>
47 using namespace com::sun::star;
49 namespace
51 OUString lcl_genRandom( const OUString &rId )
53 //FIXME: plus timestamp
54 unsigned int nRand = comphelper::rng::uniform_uint_distribution(0, 0xFFFF);
55 return OUString( rId + OUString::number( nRand ) );
59 OUString lcl_AltDescr()
61 OUString aTempl("<alt id=\"%1\">"
62 " " //FIXME real dialog title or something
63 "</alt>");
64 aTempl = aTempl.replaceFirst( "%1", lcl_genRandom("alt_id") );
66 return aTempl;
69 OUString lcl_Image( const OUString& rScreenshotId, const Size& rSize )
71 OUString aTempl("<image id=\"%1\" src=\"media/screenshots/%2.png\""
72 " width=\"%3cm\" height=\"%4cm\">"
73 "%5"
74 "</image>");
75 aTempl = aTempl.replaceFirst( "%1", lcl_genRandom("img_id") );
76 aTempl = aTempl.replaceFirst( "%2", rScreenshotId );
77 aTempl = aTempl.replaceFirst( "%3", OUString::number( rSize.Width() ) );
78 aTempl = aTempl.replaceFirst( "%4", OUString::number( rSize.Height() ) );
79 aTempl = aTempl.replaceFirst( "%5", lcl_AltDescr() );
81 return aTempl;
84 OUString lcl_ParagraphWithImage( const OUString& rScreenshotId, const Size& rSize )
86 OUString aTempl( "<paragraph id=\"%1\" role=\"paragraph\">%2"
87 "</paragraph>" SAL_NEWLINE_STRING );
88 aTempl = aTempl.replaceFirst( "%1", lcl_genRandom("par_id") );
89 aTempl = aTempl.replaceFirst( "%2", lcl_Image(rScreenshotId, rSize) );
91 return aTempl;
94 OUString lcl_Bookmark( const OUString& rWidgetId )
96 OUString aTempl = "<!-- Bookmark for widget %1 -->" SAL_NEWLINE_STRING
97 "<bookmark branch=\"hid/%2\" id=\"%3\" localize=\"false\"/>" SAL_NEWLINE_STRING;
98 aTempl = aTempl.replaceFirst( "%1", rWidgetId );
99 aTempl = aTempl.replaceFirst( "%2", rWidgetId );
100 aTempl = aTempl.replaceFirst( "%3", lcl_genRandom("bm_id") );
102 return aTempl;
106 class ControlDataEntry
108 public:
109 ControlDataEntry(
110 const vcl::Window& rControl,
111 const basegfx::B2IRange& rB2IRange)
112 : mrControl(rControl),
113 maB2IRange(rB2IRange)
117 const basegfx::B2IRange& getB2IRange() const
119 return maB2IRange;
122 OString const & GetHelpId() const { return mrControl.GetHelpId(); }
124 private:
125 const vcl::Window& mrControl;
126 basegfx::B2IRange maB2IRange;
129 typedef std::vector< ControlDataEntry > ControlDataCollection;
131 class ScreenshotAnnotationDlg_Impl // : public ModalDialog
133 public:
134 ScreenshotAnnotationDlg_Impl(
135 ScreenshotAnnotationDlg& rParent,
136 Dialog& rParentDialog);
137 ~ScreenshotAnnotationDlg_Impl();
139 private:
140 // Handler for click on save
141 DECL_LINK(saveButtonHandler, Button*, void);
143 // Handler for clicks on picture frame
144 DECL_LINK(pictureFrameListener, VclWindowEvent&, void);
146 // helper methods
147 void CollectChildren(
148 const vcl::Window& rCurrent,
149 const basegfx::B2IPoint& rTopLeft,
150 ControlDataCollection& rControlDataCollection);
151 ControlDataEntry* CheckHit(const basegfx::B2IPoint& rPosition);
152 void PaintControlDataEntry(
153 const ControlDataEntry& rEntry,
154 const Color& rColor,
155 double fLineWidth,
156 double fTransparency);
157 void RepaintToBuffer(
158 bool bUseDimmed = false,
159 bool bPaintHilight = false);
160 void RepaintPictureElement();
161 Point GetOffsetInPicture() const;
163 // local variables
164 Dialog& mrParentDialog;
165 BitmapEx maParentDialogBitmap;
166 BitmapEx maDimmedDialogBitmap;
167 Size maParentDialogSize;
169 // VirtualDevice for buffered interaction paints
170 VclPtr<VirtualDevice> mpVirtualBufferDevice;
172 // all detected children
173 ControlDataCollection maAllChildren;
175 // hilighted/selected children
176 ControlDataEntry* mpHilighted;
177 std::set< ControlDataEntry* >
178 maSelected;
180 // list of detected controls
181 VclPtr<FixedImage> mpPicture;
182 VclPtr<VclMultiLineEdit> mpText;
183 VclPtr<PushButton> mpSave;
185 // save as text
186 OUString maSaveAsText;
187 OUString maMainMarkupText;
189 // folder URL
190 static OUString maLastFolderURL;
193 OUString ScreenshotAnnotationDlg_Impl::maLastFolderURL = OUString();
195 ScreenshotAnnotationDlg_Impl::ScreenshotAnnotationDlg_Impl(
196 ScreenshotAnnotationDlg& rParent,
197 Dialog& rParentDialog)
198 : mrParentDialog(rParentDialog),
199 maParentDialogBitmap(rParentDialog.createScreenshot()),
200 maDimmedDialogBitmap(maParentDialogBitmap),
201 maParentDialogSize(maParentDialogBitmap.GetSizePixel()),
202 mpVirtualBufferDevice(nullptr),
203 maAllChildren(),
204 mpHilighted(nullptr),
205 maSelected(),
206 mpPicture(nullptr),
207 mpText(nullptr),
208 mpSave(nullptr),
209 maSaveAsText(CuiResId(RID_SVXSTR_SAVE_SCREENSHOT_AS))
211 // image ain't empty
212 assert(!maParentDialogBitmap.IsEmpty());
213 assert(0 != maParentDialogBitmap.GetSizePixel().Width());
214 assert(0 != maParentDialogBitmap.GetSizePixel().Height());
216 // get needed widgets
217 rParent.get(mpPicture, "picture");
218 assert(mpPicture.get());
219 rParent.get(mpText, "text");
220 assert(mpText.get());
221 rParent.get(mpSave, "save");
222 assert(mpSave.get());
224 // set screenshot image at FixedImage, resize, set event listener
225 if (mpPicture)
227 // collect all children. Choose start pos to be negative
228 // of target dialog's position to get all positions relative to (0,0)
229 const Point aParentPos(mrParentDialog.GetPosPixel());
230 const basegfx::B2IPoint aTopLeft(-aParentPos.X(), -aParentPos.Y());
232 CollectChildren(
233 mrParentDialog,
234 aTopLeft,
235 maAllChildren);
237 // to make clear that maParentDialogBitmap is a background image, adjust
238 // luminance a bit for maDimmedDialogBitmap - other methods may be applied
239 maDimmedDialogBitmap.Adjust(-15, 0, 0, 0, 0);
241 // init paint buffering VirtualDevice
242 mpVirtualBufferDevice = VclPtr<VirtualDevice>::Create(*Application::GetDefaultDevice(), DeviceFormat::DEFAULT, DeviceFormat::BITMASK);
243 mpVirtualBufferDevice->SetOutputSizePixel(maParentDialogSize);
244 mpVirtualBufferDevice->SetFillColor(COL_TRANSPARENT);
246 // initially set image for picture control
247 mpPicture->SetImage(Image(maDimmedDialogBitmap));
249 // set size for picture control, this will re-layout so that
250 // the picture control shows the whole dialog
251 mpPicture->set_width_request(maParentDialogSize.Width());
252 mpPicture->set_height_request(maParentDialogSize.Height());
254 // add local event listener to allow interactions with mouse
255 mpPicture->AddEventListener(LINK(this, ScreenshotAnnotationDlg_Impl, pictureFrameListener));
257 // avoid image scaling, this is needed for images smaller than the
258 // minimal dialog size
259 const WinBits aWinBits(mpPicture->GetStyle());
260 mpPicture->SetStyle(aWinBits & ~WB_SCALE);
263 // set some test text at VclMultiLineEdit and make read-only - only
264 // copying content to clipboard is allowed
265 if (mpText)
267 mpText->set_width_request(400);
268 mpText->set_height_request( mpText->GetTextHeight() * 10 );
269 OUString aHelpId = OStringToOUString( mrParentDialog.GetHelpId(), RTL_TEXTENCODING_UTF8 );
270 Size aSizeCm = mrParentDialog.PixelToLogic(maParentDialogSize, MapMode(MapUnit::MapCM));
271 maMainMarkupText = lcl_ParagraphWithImage( aHelpId, aSizeCm );
272 mpText->SetText( maMainMarkupText );
273 mpText->SetReadOnly();
276 // set click handler for save button
277 if (mpSave)
279 mpSave->SetClickHdl(LINK(this, ScreenshotAnnotationDlg_Impl, saveButtonHandler));
283 void ScreenshotAnnotationDlg_Impl::CollectChildren(
284 const vcl::Window& rCurrent,
285 const basegfx::B2IPoint& rTopLeft,
286 ControlDataCollection& rControlDataCollection)
288 if (rCurrent.IsVisible())
290 const Point aCurrentPos(rCurrent.GetPosPixel());
291 const Size aCurrentSize(rCurrent.GetSizePixel());
292 const basegfx::B2IPoint aCurrentTopLeft(rTopLeft.getX() + aCurrentPos.X(), rTopLeft.getY() + aCurrentPos.Y());
293 const basegfx::B2IRange aCurrentRange(aCurrentTopLeft, aCurrentTopLeft + basegfx::B2IPoint(aCurrentSize.Width(), aCurrentSize.Height()));
295 if (!aCurrentRange.isEmpty())
297 rControlDataCollection.emplace_back(rCurrent, aCurrentRange);
300 for (sal_uInt16 a(0); a < rCurrent.GetChildCount(); a++)
302 vcl::Window* pChild = rCurrent.GetChild(a);
304 if (nullptr != pChild)
306 CollectChildren(*pChild, aCurrentTopLeft, rControlDataCollection);
312 ScreenshotAnnotationDlg_Impl::~ScreenshotAnnotationDlg_Impl()
314 mpVirtualBufferDevice.disposeAndClear();
317 IMPL_LINK_NOARG(ScreenshotAnnotationDlg_Impl, saveButtonHandler, Button*, void)
319 // 'save screenshot...' pressed, offer to save maParentDialogBitmap
320 // as PNG image, use *.id file name as screenshot file name offering
321 OString aDerivedFileName;
323 // get a suggestion for the filename from ui file name
325 const OString& rUIFileName = mrParentDialog.getUIFile();
326 sal_Int32 nIndex(0);
330 const OString aToken(rUIFileName.getToken(0, '/', nIndex));
332 if (!aToken.isEmpty())
334 aDerivedFileName = aToken;
336 } while (nIndex >= 0);
339 uno::Reference< uno::XComponentContext > xContext = cppu::defaultBootstrap_InitialComponentContext();
340 const uno::Reference< ui::dialogs::XFilePicker3 > xFilePicker =
341 ui::dialogs::FilePicker::createWithMode(xContext, ui::dialogs::TemplateDescription::FILESAVE_AUTOEXTENSION);
343 xFilePicker->setTitle(maSaveAsText);
345 if (!maLastFolderURL.isEmpty())
347 xFilePicker->setDisplayDirectory(maLastFolderURL);
350 xFilePicker->appendFilter("*.png", "*.png");
351 xFilePicker->setCurrentFilter("*.png");
352 xFilePicker->setDefaultName(OStringToOUString(aDerivedFileName, RTL_TEXTENCODING_UTF8));
353 xFilePicker->setMultiSelectionMode(false);
355 if (xFilePicker->execute() == ui::dialogs::ExecutableDialogResults::OK)
357 maLastFolderURL = xFilePicker->getDisplayDirectory();
358 const uno::Sequence< OUString > files(xFilePicker->getSelectedFiles());
360 if (files.getLength())
362 OUString aConfirmedName = files[0];
364 if (!aConfirmedName.isEmpty())
366 INetURLObject aConfirmedURL(aConfirmedName);
367 OUString aCurrentExtension(aConfirmedURL.getExtension());
369 if (!aCurrentExtension.isEmpty() && aCurrentExtension != "png")
371 aConfirmedURL.removeExtension();
372 aCurrentExtension.clear();
375 if (aCurrentExtension.isEmpty())
377 aConfirmedURL.setExtension("png");
380 // open stream
381 SvFileStream aNew(aConfirmedURL.PathToFileName(), StreamMode::WRITE | StreamMode::TRUNC);
383 if (aNew.IsOpen())
385 // prepare bitmap to save - do use the original screenshot here,
386 // not the dimmed one
387 RepaintToBuffer();
389 // extract Bitmap
390 const BitmapEx aTargetBitmap(
391 mpVirtualBufferDevice->GetBitmapEx(
392 Point(0, 0),
393 mpVirtualBufferDevice->GetOutputSizePixel()));
395 // write as PNG
396 vcl::PNGWriter aPNGWriter(aTargetBitmap);
397 aPNGWriter.Write(aNew);
404 ControlDataEntry* ScreenshotAnnotationDlg_Impl::CheckHit(const basegfx::B2IPoint& rPosition)
406 ControlDataEntry* pRetval = nullptr;
408 for (auto&& rCandidate : maAllChildren)
410 if (rCandidate.getB2IRange().isInside(rPosition))
412 if (pRetval)
414 if (pRetval->getB2IRange().isInside(rCandidate.getB2IRange().getMinimum())
415 && pRetval->getB2IRange().isInside(rCandidate.getB2IRange().getMaximum()))
417 pRetval = &rCandidate;
420 else
422 pRetval = &rCandidate;
427 return pRetval;
430 void ScreenshotAnnotationDlg_Impl::PaintControlDataEntry(
431 const ControlDataEntry& rEntry,
432 const Color& rColor,
433 double fLineWidth,
434 double fTransparency)
436 if (mpPicture && mpVirtualBufferDevice)
438 basegfx::B2DRange aB2DRange(rEntry.getB2IRange());
440 // grow in pixels to be a little bit 'outside'. This also
441 // ensures that getWidth()/getHeight() ain't 0.0 (see division below)
442 static const double fGrowTopLeft(1.5);
443 static const double fGrowBottomRight(0.5);
444 aB2DRange.expand(aB2DRange.getMinimum() - basegfx::B2DPoint(fGrowTopLeft, fGrowTopLeft));
445 aB2DRange.expand(aB2DRange.getMaximum() + basegfx::B2DPoint(fGrowBottomRight, fGrowBottomRight));
447 // edge rounding in pixel. Need to convert, value for
448 // createPolygonFromRect is relative [0.0 .. 1.0]
449 static const double fEdgeRoundPixel(8.0);
450 const basegfx::B2DPolygon aPolygon(
451 basegfx::utils::createPolygonFromRect(
452 aB2DRange,
453 fEdgeRoundPixel / aB2DRange.getWidth(),
454 fEdgeRoundPixel / aB2DRange.getHeight()));
456 mpVirtualBufferDevice->SetLineColor(rColor);
458 // try to use transparency
459 if (!mpVirtualBufferDevice->DrawPolyLineDirect(
460 basegfx::B2DHomMatrix(),
461 aPolygon,
462 fLineWidth,
463 fTransparency,
464 basegfx::B2DLineJoin::Round))
466 // no transparency, draw without
467 mpVirtualBufferDevice->DrawPolyLine(
468 aPolygon,
469 fLineWidth);
474 Point ScreenshotAnnotationDlg_Impl::GetOffsetInPicture() const
476 if (!mpPicture)
478 return Point(0, 0);
481 const Size aPixelSizeTarget(mpPicture->GetOutputSizePixel());
483 return Point(
484 aPixelSizeTarget.Width() > maParentDialogSize.Width() ? (aPixelSizeTarget.Width() - maParentDialogSize.Width()) >> 1 : 0,
485 aPixelSizeTarget.Height() > maParentDialogSize.Height() ? (aPixelSizeTarget.Height() - maParentDialogSize.Height()) >> 1 : 0);
488 void ScreenshotAnnotationDlg_Impl::RepaintToBuffer(
489 bool bUseDimmed,
490 bool bPaintHilight)
492 if (mpVirtualBufferDevice)
494 // reset with original screenshot bitmap
495 mpVirtualBufferDevice->DrawBitmapEx(
496 Point(0, 0),
497 bUseDimmed ? maDimmedDialogBitmap : maParentDialogBitmap);
499 // get various options
500 const SvtOptionsDrawinglayer aSvtOptionsDrawinglayer;
501 const Color aHilightColor(aSvtOptionsDrawinglayer.getHilightColor());
502 const double fTransparence(aSvtOptionsDrawinglayer.GetTransparentSelectionPercent() * 0.01);
503 const bool bIsAntiAliasing(aSvtOptionsDrawinglayer.IsAntiAliasing());
504 const AntialiasingFlags nOldAA(mpVirtualBufferDevice->GetAntialiasing());
506 if (bIsAntiAliasing)
508 mpVirtualBufferDevice->SetAntialiasing(AntialiasingFlags::EnableB2dDraw);
511 // paint selected entries
512 for (auto&& rCandidate : maSelected)
514 static const double fLineWidthEntries(5.0);
515 PaintControlDataEntry(*rCandidate, COL_LIGHTRED, fLineWidthEntries, fTransparence * 0.2);
518 // paint hilighted entry
519 if (mpHilighted && bPaintHilight)
521 static const double fLineWidthHilight(7.0);
522 PaintControlDataEntry(*mpHilighted, aHilightColor, fLineWidthHilight, fTransparence);
525 if (bIsAntiAliasing)
527 mpVirtualBufferDevice->SetAntialiasing(nOldAA);
532 void ScreenshotAnnotationDlg_Impl::RepaintPictureElement()
534 if (mpPicture && mpVirtualBufferDevice)
536 // reset image in buffer, use dimmed version and allow hilight
537 RepaintToBuffer(true, true);
539 // copy new content to picture control (hard paint)
540 mpPicture->DrawOutDev(
541 GetOffsetInPicture(),
542 maParentDialogSize,
543 Point(0, 0),
544 maParentDialogSize,
545 *mpVirtualBufferDevice);
547 // also set image to get repaints right, but trigger no repaint
548 mpPicture->SetImage(
549 Image(
550 mpVirtualBufferDevice->GetBitmapEx(
551 Point(0, 0),
552 mpVirtualBufferDevice->GetOutputSizePixel())));
553 mpPicture->Validate();
557 IMPL_LINK(ScreenshotAnnotationDlg_Impl, pictureFrameListener, VclWindowEvent&, rEvent, void)
559 // event in picture frame
560 bool bRepaint(false);
562 switch (rEvent.GetId())
564 case VclEventId::WindowMouseMove:
565 case VclEventId::WindowMouseButtonUp:
567 MouseEvent* pMouseEvent = static_cast< MouseEvent* >(rEvent.GetData());
569 if (pMouseEvent)
571 switch (rEvent.GetId())
573 case VclEventId::WindowMouseMove:
575 if (mpPicture->IsMouseOver())
577 const ControlDataEntry* pOldHit = mpHilighted;
578 const Point aOffset(GetOffsetInPicture());
579 const basegfx::B2IPoint aMousePos(
580 pMouseEvent->GetPosPixel().X() - aOffset.X(),
581 pMouseEvent->GetPosPixel().Y() - aOffset.Y());
582 const ControlDataEntry* pHit = CheckHit(aMousePos);
584 if (pHit && pOldHit != pHit)
586 mpHilighted = const_cast< ControlDataEntry* >(pHit);
587 bRepaint = true;
590 else if (mpHilighted)
592 mpHilighted = nullptr;
593 bRepaint = true;
595 break;
597 case VclEventId::WindowMouseButtonUp:
599 if (mpPicture->IsMouseOver() && mpHilighted)
601 if (maSelected.erase(mpHilighted) == 0)
603 maSelected.insert(mpHilighted);
606 OUStringBuffer aBookmarks(maMainMarkupText);
607 for (auto&& rCandidate : maSelected)
609 OUString aHelpId = OStringToOUString( rCandidate->GetHelpId(), RTL_TEXTENCODING_UTF8 );
610 aBookmarks.append(lcl_Bookmark( aHelpId ));
613 mpText->SetText( aBookmarks.makeStringAndClear() );
614 bRepaint = true;
616 break;
618 default:
620 break;
624 break;
626 default:
628 break;
632 if (bRepaint)
634 RepaintPictureElement();
638 ScreenshotAnnotationDlg::ScreenshotAnnotationDlg(
639 vcl::Window* pParent,
640 Dialog& rParentDialog)
641 : SfxModalDialog(pParent, "ScreenshotAnnotationDialog", "cui/ui/screenshotannotationdialog.ui")
643 m_pImpl.reset(new ScreenshotAnnotationDlg_Impl(*this, rParentDialog));
647 ScreenshotAnnotationDlg::~ScreenshotAnnotationDlg()
649 disposeOnce();
652 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */