1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
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>
47 using namespace com::sun::star
;
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
64 aTempl
= aTempl
.replaceFirst( "%1", lcl_genRandom("alt_id") );
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\">"
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() );
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
) );
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") );
106 class ControlDataEntry
110 const vcl::Window
& rControl
,
111 const basegfx::B2IRange
& rB2IRange
)
112 : mrControl(rControl
),
113 maB2IRange(rB2IRange
)
117 const basegfx::B2IRange
& getB2IRange() const
122 OString
const & GetHelpId() const { return mrControl
.GetHelpId(); }
125 const vcl::Window
& mrControl
;
126 basegfx::B2IRange maB2IRange
;
129 typedef std::vector
< ControlDataEntry
> ControlDataCollection
;
131 class ScreenshotAnnotationDlg_Impl
// : public ModalDialog
134 ScreenshotAnnotationDlg_Impl(
135 ScreenshotAnnotationDlg
& rParent
,
136 Dialog
& rParentDialog
);
137 ~ScreenshotAnnotationDlg_Impl();
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);
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
,
156 double fTransparency
);
157 void RepaintToBuffer(
158 bool bUseDimmed
= false,
159 bool bPaintHilight
= false);
160 void RepaintPictureElement();
161 Point
GetOffsetInPicture() const;
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
* >
180 // list of detected controls
181 VclPtr
<FixedImage
> mpPicture
;
182 VclPtr
<VclMultiLineEdit
> mpText
;
183 VclPtr
<PushButton
> mpSave
;
186 OUString maSaveAsText
;
187 OUString maMainMarkupText
;
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),
204 mpHilighted(nullptr),
209 maSaveAsText(CuiResId(RID_SVXSTR_SAVE_SCREENSHOT_AS
))
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
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());
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
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
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();
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");
381 SvFileStream
aNew(aConfirmedURL
.PathToFileName(), StreamMode::WRITE
| StreamMode::TRUNC
);
385 // prepare bitmap to save - do use the original screenshot here,
386 // not the dimmed one
390 const BitmapEx
aTargetBitmap(
391 mpVirtualBufferDevice
->GetBitmapEx(
393 mpVirtualBufferDevice
->GetOutputSizePixel()));
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
))
414 if (pRetval
->getB2IRange().isInside(rCandidate
.getB2IRange().getMinimum())
415 && pRetval
->getB2IRange().isInside(rCandidate
.getB2IRange().getMaximum()))
417 pRetval
= &rCandidate
;
422 pRetval
= &rCandidate
;
430 void ScreenshotAnnotationDlg_Impl::PaintControlDataEntry(
431 const ControlDataEntry
& rEntry
,
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(
453 fEdgeRoundPixel
/ aB2DRange
.getWidth(),
454 fEdgeRoundPixel
/ aB2DRange
.getHeight()));
456 mpVirtualBufferDevice
->SetLineColor(rColor
);
458 // try to use transparency
459 if (!mpVirtualBufferDevice
->DrawPolyLineDirect(
460 basegfx::B2DHomMatrix(),
464 basegfx::B2DLineJoin::Round
))
466 // no transparency, draw without
467 mpVirtualBufferDevice
->DrawPolyLine(
474 Point
ScreenshotAnnotationDlg_Impl::GetOffsetInPicture() const
481 const Size
aPixelSizeTarget(mpPicture
->GetOutputSizePixel());
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(
492 if (mpVirtualBufferDevice
)
494 // reset with original screenshot bitmap
495 mpVirtualBufferDevice
->DrawBitmapEx(
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());
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
);
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(),
545 *mpVirtualBufferDevice
);
547 // also set image to get repaints right, but trigger no repaint
550 mpVirtualBufferDevice
->GetBitmapEx(
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());
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
);
590 else if (mpHilighted
)
592 mpHilighted
= nullptr;
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() );
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()
652 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */