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 <com/sun/star/ui/dialogs/TemplateDescription.hpp>
27 #include <com/sun/star/ui/dialogs/ExecutableDialogResults.hpp>
28 #include <com/sun/star/ui/dialogs/XFilePicker3.hpp>
30 #include <comphelper/random.hxx>
31 #include <basegfx/polygon/b2dpolygontools.hxx>
32 #include <sfx2/filedlghelper.hxx>
33 #include <tools/stream.hxx>
34 #include <tools/urlobj.hxx>
35 #include <vcl/bitmapex.hxx>
36 #include <vcl/customweld.hxx>
37 #include <vcl/event.hxx>
38 #include <vcl/filter/PngImageWriter.hxx>
39 #include <vcl/svapp.hxx>
40 #include <vcl/salgtype.hxx>
41 #include <vcl/virdev.hxx>
42 #include <vcl/weld.hxx>
43 #include <svtools/optionsdrawinglayer.hxx>
44 #include <basegfx/matrix/b2dhommatrix.hxx>
46 #include <string_view>
48 using namespace com::sun::star
;
52 OUString
lcl_genRandom( std::u16string_view rId
)
54 //FIXME: plus timestamp
55 unsigned int nRand
= comphelper::rng::uniform_uint_distribution(0, 0xFFFF);
56 return OUString( rId
+ OUString::number( nRand
) );
60 OUString
lcl_AltDescr()
62 OUString
aTempl(u
"<alt id=\"%1\">"
63 " " //FIXME real dialog title or something
65 aTempl
= aTempl
.replaceFirst( "%1", lcl_genRandom(u
"alt_id") );
70 OUString
lcl_Image( std::u16string_view rScreenshotId
, const Size
& rSize
)
72 OUString
aTempl(u
"<image id=\"%1\" src=\"media/screenshots/%2.png\""
73 " width=\"%3cm\" height=\"%4cm\">"
76 aTempl
= aTempl
.replaceFirst( "%1", lcl_genRandom(u
"img_id") );
77 aTempl
= aTempl
.replaceFirst( "%2", rScreenshotId
);
78 aTempl
= aTempl
.replaceFirst( "%3", OUString::number( rSize
.Width() ) );
79 aTempl
= aTempl
.replaceFirst( "%4", OUString::number( rSize
.Height() ) );
80 aTempl
= aTempl
.replaceFirst( "%5", lcl_AltDescr() );
85 OUString
lcl_ParagraphWithImage( std::u16string_view rScreenshotId
, const Size
& rSize
)
87 OUString
aTempl( u
"<paragraph id=\"%1\" role=\"paragraph\">%2"
88 "</paragraph>" SAL_NEWLINE_STRING
""_ustr
);
89 aTempl
= aTempl
.replaceFirst( "%1", lcl_genRandom(u
"par_id") );
90 aTempl
= aTempl
.replaceFirst( "%2", lcl_Image(rScreenshotId
, rSize
) );
95 OUString
lcl_Bookmark( std::u16string_view rWidgetId
)
97 OUString aTempl
= u
"<!-- Bookmark for widget %1 -->" SAL_NEWLINE_STRING
98 "<bookmark branch=\"hid/%2\" id=\"%3\" localize=\"false\"/>" SAL_NEWLINE_STRING
""_ustr
;
99 aTempl
= aTempl
.replaceFirst( "%1", rWidgetId
);
100 aTempl
= aTempl
.replaceFirst( "%2", rWidgetId
);
101 aTempl
= aTempl
.replaceFirst( "%3", lcl_genRandom(u
"bm_id") );
109 class Picture
: public weld::CustomWidgetController
112 ScreenshotAnnotationDlg_Impl
*m_pDialog
;
115 virtual void Paint(vcl::RenderContext
& rRenderContext
, const tools::Rectangle
&) override
;
116 virtual bool MouseMove(const MouseEvent
& rMouseEvent
) override
;
117 virtual bool MouseButtonUp(const MouseEvent
& rMouseEvent
) override
;
119 Picture(ScreenshotAnnotationDlg_Impl
* pDialog
)
121 , m_bMouseOver(false)
125 bool IsMouseOver() const
132 class ScreenshotAnnotationDlg_Impl
135 ScreenshotAnnotationDlg_Impl(
136 weld::Window
* pParent
,
137 weld::Builder
& rParent
,
138 weld::Dialog
& rParentDialog
);
139 ~ScreenshotAnnotationDlg_Impl();
142 // Handler for click on save
143 DECL_LINK(saveButtonHandler
, weld::Button
&, void);
146 weld::ScreenShotEntry
* CheckHit(const basegfx::B2IPoint
& rPosition
);
147 void PaintScreenShotEntry(
148 const weld::ScreenShotEntry
& rEntry
,
151 double fTransparency
);
152 void RepaintToBuffer(
153 bool bUseDimmed
= false,
154 bool bPaintHilight
= false);
155 void RepaintPictureElement();
156 Point
GetOffsetInPicture() const;
159 weld::Window
* mpParentWindow
;
160 weld::Dialog
& mrParentDialog
;
161 BitmapEx maParentDialogBitmap
;
162 BitmapEx maDimmedDialogBitmap
;
163 Size maParentDialogSize
;
165 // VirtualDevice for buffered interaction paints
166 VclPtr
<VirtualDevice
> mxVirtualBufferDevice
;
168 // all detected children
169 weld::ScreenShotCollection maAllChildren
;
171 // highlighted/selected children
172 weld::ScreenShotEntry
* mpHilighted
;
173 std::set
< weld::ScreenShotEntry
* >
176 // list of detected controls
178 std::unique_ptr
<weld::CustomWeld
> mxPicture
;
179 std::unique_ptr
<weld::TextView
> mxText
;
180 std::unique_ptr
<weld::Button
> mxSave
;
183 OUString maSaveAsText
;
184 OUString maMainMarkupText
;
187 static OUString maLastFolderURL
;
189 void Paint(vcl::RenderContext
& rRenderContext
);
190 bool MouseMove(const MouseEvent
& rMouseEvent
);
191 bool MouseButtonUp();
194 OUString
ScreenshotAnnotationDlg_Impl::maLastFolderURL
= OUString();
196 ScreenshotAnnotationDlg_Impl::ScreenshotAnnotationDlg_Impl(
197 weld::Window
* pParent
,
198 weld::Builder
& rParentBuilder
,
199 weld::Dialog
& rParentDialog
)
200 : mpParentWindow(pParent
),
201 mrParentDialog(rParentDialog
),
202 mxVirtualBufferDevice(nullptr),
203 mpHilighted(nullptr),
205 maSaveAsText(CuiResId(RID_CUISTR_SAVE_SCREENSHOT_AS
))
207 VclPtr
<VirtualDevice
> xParentDialogSurface(rParentDialog
.screenshot());
208 maParentDialogSize
= xParentDialogSurface
->GetOutputSizePixel();
209 maParentDialogBitmap
= xParentDialogSurface
->GetBitmapEx(Point(), maParentDialogSize
);
210 maDimmedDialogBitmap
= maParentDialogBitmap
;
213 assert(!maParentDialogBitmap
.IsEmpty());
214 assert(0 != maParentDialogBitmap
.GetSizePixel().Width());
215 assert(0 != maParentDialogBitmap
.GetSizePixel().Height());
217 // get needed widgets
218 mxPicture
.reset(new weld::CustomWeld(rParentBuilder
, u
"picture"_ustr
, maPicture
));
220 mxText
= rParentBuilder
.weld_text_view(u
"text"_ustr
);
222 mxSave
= rParentBuilder
.weld_button(u
"save"_ustr
);
225 // set screenshot image at DrawingArea, resize, set event listener
228 maAllChildren
= mrParentDialog
.collect_screenshot_data();
230 // to make clear that maParentDialogBitmap is a background image, adjust
231 // luminance a bit for maDimmedDialogBitmap - other methods may be applied
232 maDimmedDialogBitmap
.Adjust(-15, 0, 0, 0, 0);
234 // init paint buffering VirtualDevice
235 mxVirtualBufferDevice
= VclPtr
<VirtualDevice
>::Create(*Application::GetDefaultDevice(), DeviceFormat::WITHOUT_ALPHA
);
236 mxVirtualBufferDevice
->SetOutputSizePixel(maParentDialogSize
);
237 mxVirtualBufferDevice
->SetFillColor(COL_TRANSPARENT
);
239 // initially set image for picture control
240 mxVirtualBufferDevice
->DrawBitmapEx(Point(0, 0), maDimmedDialogBitmap
);
242 // set size for picture control, this will re-layout so that
243 // the picture control shows the whole dialog
244 maPicture
.SetOutputSizePixel(maParentDialogSize
);
245 mxPicture
->set_size_request(maParentDialogSize
.Width(), maParentDialogSize
.Height());
247 mxPicture
->queue_draw();
250 // set some test text at VclMultiLineEdit and make read-only - only
251 // copying content to clipboard is allowed
254 mxText
->set_size_request(400, mxText
->get_height_rows(10));
255 OUString aHelpId
= mrParentDialog
.get_help_id();
256 Size aSizeCm
= Application::GetDefaultDevice()->PixelToLogic(maParentDialogSize
, MapMode(MapUnit::MapCM
));
257 maMainMarkupText
= lcl_ParagraphWithImage( aHelpId
, aSizeCm
);
258 mxText
->set_text( maMainMarkupText
);
259 mxText
->set_editable(false);
262 // set click handler for save button
265 mxSave
->connect_clicked(LINK(this, ScreenshotAnnotationDlg_Impl
, saveButtonHandler
));
269 ScreenshotAnnotationDlg_Impl::~ScreenshotAnnotationDlg_Impl()
271 mxVirtualBufferDevice
.disposeAndClear();
274 IMPL_LINK_NOARG(ScreenshotAnnotationDlg_Impl
, saveButtonHandler
, weld::Button
&, void)
276 // 'save screenshot...' pressed, offer to save maParentDialogBitmap
277 // as PNG image, use *.id file name as screenshot file name offering
278 // get a suggestion for the filename from buildable name
279 OUString aDerivedFileName
= mrParentDialog
.get_buildable_name();
281 sfx2::FileDialogHelper
aFileDlg(ui::dialogs::TemplateDescription::FILESAVE_AUTOEXTENSION
,
282 FileDialogFlags::NONE
, mpParentWindow
);
283 aFileDlg
.SetContext(sfx2::FileDialogHelper::ScreenshotAnnotation
);
285 const uno::Reference
< ui::dialogs::XFilePicker3
> xFilePicker
= aFileDlg
.GetFilePicker();
287 xFilePicker
->setTitle(maSaveAsText
);
289 if (!maLastFolderURL
.isEmpty())
291 xFilePicker
->setDisplayDirectory(maLastFolderURL
);
294 xFilePicker
->appendFilter(u
"*.png"_ustr
, u
"*.png"_ustr
);
295 xFilePicker
->setCurrentFilter(u
"*.png"_ustr
);
296 xFilePicker
->setDefaultName(aDerivedFileName
);
297 xFilePicker
->setMultiSelectionMode(false);
299 if (xFilePicker
->execute() != ui::dialogs::ExecutableDialogResults::OK
)
302 maLastFolderURL
= xFilePicker
->getDisplayDirectory();
303 const uno::Sequence
< OUString
> files(xFilePicker
->getSelectedFiles());
305 if (!files
.hasElements())
308 const OUString
& aConfirmedName
= files
[0];
310 if (aConfirmedName
.isEmpty())
313 INetURLObject
aConfirmedURL(aConfirmedName
);
314 OUString
aCurrentExtension(aConfirmedURL
.getExtension());
316 if (!aCurrentExtension
.isEmpty() && aCurrentExtension
!= "png")
318 aConfirmedURL
.removeExtension();
319 aCurrentExtension
.clear();
322 if (aCurrentExtension
.isEmpty())
324 aConfirmedURL
.setExtension(u
"png");
328 SvFileStream
aNew(aConfirmedURL
.PathToFileName(), StreamMode::WRITE
| StreamMode::TRUNC
);
333 // prepare bitmap to save - do use the original screenshot here,
334 // not the dimmed one
338 const BitmapEx
aTargetBitmap(
339 mxVirtualBufferDevice
->GetBitmapEx(
341 mxVirtualBufferDevice
->GetOutputSizePixel()));
344 vcl::PngImageWriter
aPNGWriter(aNew
);
345 aPNGWriter
.write(aTargetBitmap
);
348 weld::ScreenShotEntry
* ScreenshotAnnotationDlg_Impl::CheckHit(const basegfx::B2IPoint
& rPosition
)
350 weld::ScreenShotEntry
* pRetval
= nullptr;
352 for (auto&& rCandidate
: maAllChildren
)
354 if (rCandidate
.getB2IRange().isInside(rPosition
))
358 if (pRetval
->getB2IRange().isInside(rCandidate
.getB2IRange().getMinimum())
359 && pRetval
->getB2IRange().isInside(rCandidate
.getB2IRange().getMaximum()))
361 pRetval
= &rCandidate
;
366 pRetval
= &rCandidate
;
374 void ScreenshotAnnotationDlg_Impl::PaintScreenShotEntry(
375 const weld::ScreenShotEntry
& rEntry
,
378 double fTransparency
)
380 if (!(mxPicture
&& mxVirtualBufferDevice
))
383 basegfx::B2DRange
aB2DRange(rEntry
.getB2IRange());
385 // grow in pixels to be a little bit 'outside'. This also
386 // ensures that getWidth()/getHeight() ain't 0.0 (see division below)
387 static const double fGrowTopLeft(1.5);
388 static const double fGrowBottomRight(0.5);
389 aB2DRange
.expand(aB2DRange
.getMinimum() - basegfx::B2DPoint(fGrowTopLeft
, fGrowTopLeft
));
390 aB2DRange
.expand(aB2DRange
.getMaximum() + basegfx::B2DPoint(fGrowBottomRight
, fGrowBottomRight
));
392 // edge rounding in pixel. Need to convert, value for
393 // createPolygonFromRect is relative [0.0 .. 1.0]
394 static const double fEdgeRoundPixel(8.0);
395 const basegfx::B2DPolygon
aPolygon(
396 basegfx::utils::createPolygonFromRect(
398 fEdgeRoundPixel
/ aB2DRange
.getWidth(),
399 fEdgeRoundPixel
/ aB2DRange
.getHeight()));
401 mxVirtualBufferDevice
->SetLineColor(rColor
);
403 // try to use transparency
404 if (!mxVirtualBufferDevice
->DrawPolyLineDirect(
405 basegfx::B2DHomMatrix(),
410 basegfx::B2DLineJoin::Round
))
412 // no transparency, draw without
413 mxVirtualBufferDevice
->DrawPolyLine(
419 Point
ScreenshotAnnotationDlg_Impl::GetOffsetInPicture() const
421 const Size
aPixelSizeTarget(maPicture
.GetOutputSizePixel());
424 aPixelSizeTarget
.Width() > maParentDialogSize
.Width() ? (aPixelSizeTarget
.Width() - maParentDialogSize
.Width()) >> 1 : 0,
425 aPixelSizeTarget
.Height() > maParentDialogSize
.Height() ? (aPixelSizeTarget
.Height() - maParentDialogSize
.Height()) >> 1 : 0);
428 void ScreenshotAnnotationDlg_Impl::RepaintToBuffer(
432 if (!mxVirtualBufferDevice
)
435 // reset with original screenshot bitmap
436 mxVirtualBufferDevice
->DrawBitmapEx(
438 bUseDimmed
? maDimmedDialogBitmap
: maParentDialogBitmap
);
440 // get various options
441 const Color
aHilightColor(SvtOptionsDrawinglayer::getHilightColor());
442 const double fTransparence(SvtOptionsDrawinglayer::GetTransparentSelectionPercent() * 0.01);
443 const bool bIsAntiAliasing(SvtOptionsDrawinglayer::IsAntiAliasing());
444 const AntialiasingFlags
nOldAA(mxVirtualBufferDevice
->GetAntialiasing());
448 mxVirtualBufferDevice
->SetAntialiasing(AntialiasingFlags::Enable
);
451 // paint selected entries
452 for (auto&& rCandidate
: maSelected
)
454 static const double fLineWidthEntries(5.0);
455 PaintScreenShotEntry(*rCandidate
, COL_LIGHTRED
, fLineWidthEntries
, fTransparence
* 0.2);
458 // paint highlighted entry
459 if (mpHilighted
&& bPaintHilight
)
461 static const double fLineWidthHilight(7.0);
462 PaintScreenShotEntry(*mpHilighted
, aHilightColor
, fLineWidthHilight
, fTransparence
);
467 mxVirtualBufferDevice
->SetAntialiasing(nOldAA
);
471 void ScreenshotAnnotationDlg_Impl::RepaintPictureElement()
473 if (mxPicture
&& mxVirtualBufferDevice
)
475 // reset image in buffer, use dimmed version and allow highlight
476 RepaintToBuffer(true, true);
477 mxPicture
->queue_draw();
481 void ScreenshotAnnotationDlg_Impl::Paint(vcl::RenderContext
& rRenderContext
)
483 Point
aPos(GetOffsetInPicture());
484 Size
aSize(mxVirtualBufferDevice
->GetOutputSizePixel());
485 rRenderContext
.DrawOutDev(aPos
, aSize
, Point(), aSize
, *mxVirtualBufferDevice
);
488 void Picture::Paint(vcl::RenderContext
& rRenderContext
, const tools::Rectangle
&)
490 m_pDialog
->Paint(rRenderContext
);
493 bool ScreenshotAnnotationDlg_Impl::MouseMove(const MouseEvent
& rMouseEvent
)
495 bool bRepaint(false);
497 if (maPicture
.IsMouseOver())
499 const weld::ScreenShotEntry
* pOldHit
= mpHilighted
;
500 const Point
aOffset(GetOffsetInPicture());
501 const basegfx::B2IPoint
aMousePos(
502 rMouseEvent
.GetPosPixel().X() - aOffset
.X(),
503 rMouseEvent
.GetPosPixel().Y() - aOffset
.Y());
504 const weld::ScreenShotEntry
* pHit
= CheckHit(aMousePos
);
506 if (pHit
&& pOldHit
!= pHit
)
508 mpHilighted
= const_cast<weld::ScreenShotEntry
*>(pHit
);
512 else if (mpHilighted
)
514 mpHilighted
= nullptr;
520 RepaintPictureElement();
526 bool Picture::MouseMove(const MouseEvent
& rMouseEvent
)
528 if (rMouseEvent
.IsEnterWindow())
530 if (rMouseEvent
.IsLeaveWindow())
531 m_bMouseOver
= false;
532 return m_pDialog
->MouseMove(rMouseEvent
);
535 bool ScreenshotAnnotationDlg_Impl::MouseButtonUp()
537 // event in picture frame
538 bool bRepaint(false);
540 if (maPicture
.IsMouseOver() && mpHilighted
)
542 if (maSelected
.erase(mpHilighted
) == 0)
544 maSelected
.insert(mpHilighted
);
547 OUStringBuffer
aBookmarks(maMainMarkupText
);
548 for (auto&& rCandidate
: maSelected
)
549 aBookmarks
.append(lcl_Bookmark(rCandidate
->GetHelpId()));
551 mxText
->set_text( aBookmarks
.makeStringAndClear() );
557 RepaintPictureElement();
563 bool Picture::MouseButtonUp(const MouseEvent
&)
565 return m_pDialog
->MouseButtonUp();
568 ScreenshotAnnotationDlg::ScreenshotAnnotationDlg(weld::Dialog
& rParentDialog
)
569 : GenericDialogController(&rParentDialog
, u
"cui/ui/screenshotannotationdialog.ui"_ustr
, u
"ScreenshotAnnotationDialog"_ustr
)
571 m_pImpl
.reset(new ScreenshotAnnotationDlg_Impl(m_xDialog
.get(), *m_xBuilder
, rParentDialog
));
574 ScreenshotAnnotationDlg::~ScreenshotAnnotationDlg()
578 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */