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 * This file incorporates work covered by the following license notice:
10 *
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 .
18 */
19
20 #include <screenshotannotationdlg.hxx>
21
22 #include <strings.hrc>
23 #include <dialmgr.hxx>
24
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>
29
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/pngwrite.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>
45 #include <set>
46 #include <string_view>
47
48 using namespace com::sun::star;
49
50 namespace
51 {
lcl_genRandom(std::u16string_view rId)52 OUString lcl_genRandom( std::u16string_view rId )
53 {
54 //FIXME: plus timestamp
55 unsigned int nRand = comphelper::rng::uniform_uint_distribution(0, 0xFFFF);
56 return OUString( rId + OUString::number( nRand ) );
57 }
58
59
lcl_AltDescr()60 OUString lcl_AltDescr()
61 {
62 OUString aTempl("<alt id=\"%1\">"
63 " " //FIXME real dialog title or something
64 "</alt>");
65 aTempl = aTempl.replaceFirst( "%1", lcl_genRandom(u"alt_id") );
66
67 return aTempl;
68 }
69
lcl_Image(std::u16string_view rScreenshotId,const Size & rSize)70 OUString lcl_Image( std::u16string_view rScreenshotId, const Size& rSize )
71 {
72 OUString aTempl("<image id=\"%1\" src=\"media/screenshots/%2.png\""
73 " width=\"%3cm\" height=\"%4cm\">"
74 "%5"
75 "</image>");
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() );
81
82 return aTempl;
83 }
84
lcl_ParagraphWithImage(std::u16string_view rScreenshotId,const Size & rSize)85 OUString lcl_ParagraphWithImage( std::u16string_view rScreenshotId, const Size& rSize )
86 {
87 OUString aTempl( "<paragraph id=\"%1\" role=\"paragraph\">%2"
88 "</paragraph>" SAL_NEWLINE_STRING );
89 aTempl = aTempl.replaceFirst( "%1", lcl_genRandom(u"par_id") );
90 aTempl = aTempl.replaceFirst( "%2", lcl_Image(rScreenshotId, rSize) );
91
92 return aTempl;
93 }
94
lcl_Bookmark(std::u16string_view rWidgetId)95 OUString lcl_Bookmark( std::u16string_view rWidgetId )
96 {
97 OUString aTempl = "<!-- Bookmark for widget %1 -->" SAL_NEWLINE_STRING
98 "<bookmark branch=\"hid/%2\" id=\"%3\" localize=\"false\"/>" SAL_NEWLINE_STRING;
99 aTempl = aTempl.replaceFirst( "%1", rWidgetId );
100 aTempl = aTempl.replaceFirst( "%2", rWidgetId );
101 aTempl = aTempl.replaceFirst( "%3", lcl_genRandom(u"bm_id") );
102
103 return aTempl;
104 }
105 }
106
107 namespace
108 {
109 class Picture : public weld::CustomWidgetController
110 {
111 private:
112 ScreenshotAnnotationDlg_Impl *m_pDialog;
113 bool m_bMouseOver;
114 private:
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;
118 public:
Picture(ScreenshotAnnotationDlg_Impl * pDialog)119 Picture(ScreenshotAnnotationDlg_Impl* pDialog)
120 : m_pDialog(pDialog)
121 , m_bMouseOver(false)
122 {
123 }
124
IsMouseOver() const125 bool IsMouseOver() const
126 {
127 return m_bMouseOver;
128 }
129 };
130 }
131
132 class ScreenshotAnnotationDlg_Impl
133 {
134 public:
135 ScreenshotAnnotationDlg_Impl(
136 weld::Window* pParent,
137 weld::Builder& rParent,
138 weld::Dialog& rParentDialog);
139 ~ScreenshotAnnotationDlg_Impl();
140
141 private:
142 // Handler for click on save
143 DECL_LINK(saveButtonHandler, weld::Button&, void);
144
145 // helper methods
146 weld::ScreenShotEntry* CheckHit(const basegfx::B2IPoint& rPosition);
147 void PaintScreenShotEntry(
148 const weld::ScreenShotEntry& rEntry,
149 const Color& rColor,
150 double fLineWidth,
151 double fTransparency);
152 void RepaintToBuffer(
153 bool bUseDimmed = false,
154 bool bPaintHilight = false);
155 void RepaintPictureElement();
156 Point GetOffsetInPicture() const;
157
158 // local variables
159 weld::Window* mpParentWindow;
160 weld::Dialog& mrParentDialog;
161 BitmapEx maParentDialogBitmap;
162 BitmapEx maDimmedDialogBitmap;
163 Size maParentDialogSize;
164
165 // VirtualDevice for buffered interaction paints
166 VclPtr<VirtualDevice> mxVirtualBufferDevice;
167
168 // all detected children
169 weld::ScreenShotCollection maAllChildren;
170
171 // highlighted/selected children
172 weld::ScreenShotEntry* mpHilighted;
173 std::set< weld::ScreenShotEntry* >
174 maSelected;
175
176 // list of detected controls
177 Picture maPicture;
178 std::unique_ptr<weld::CustomWeld> mxPicture;
179 std::unique_ptr<weld::TextView> mxText;
180 std::unique_ptr<weld::Button> mxSave;
181
182 // save as text
183 OUString maSaveAsText;
184 OUString maMainMarkupText;
185
186 // folder URL
187 static OUString maLastFolderURL;
188 public:
189 void Paint(vcl::RenderContext& rRenderContext);
190 bool MouseMove(const MouseEvent& rMouseEvent);
191 bool MouseButtonUp();
192 };
193
194 OUString ScreenshotAnnotationDlg_Impl::maLastFolderURL = OUString();
195
ScreenshotAnnotationDlg_Impl(weld::Window * pParent,weld::Builder & rParentBuilder,weld::Dialog & rParentDialog)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 maAllChildren(),
204 mpHilighted(nullptr),
205 maSelected(),
206 maPicture(this),
207 maSaveAsText(CuiResId(RID_SVXSTR_SAVE_SCREENSHOT_AS))
208 {
209 VclPtr<VirtualDevice> xParentDialogSurface(rParentDialog.screenshot());
210 maParentDialogSize = xParentDialogSurface->GetOutputSizePixel();
211 maParentDialogBitmap = xParentDialogSurface->GetBitmapEx(Point(), maParentDialogSize);
212 maDimmedDialogBitmap = maParentDialogBitmap;
213
214 // image ain't empty
215 assert(!maParentDialogBitmap.IsEmpty());
216 assert(0 != maParentDialogBitmap.GetSizePixel().Width());
217 assert(0 != maParentDialogBitmap.GetSizePixel().Height());
218
219 // get needed widgets
220 mxPicture.reset(new weld::CustomWeld(rParentBuilder, "picture", maPicture));
221 assert(mxPicture);
222 mxText = rParentBuilder.weld_text_view("text");
223 assert(mxText);
224 mxSave = rParentBuilder.weld_button("save");
225 assert(mxSave);
226
227 // set screenshot image at DrawingArea, resize, set event listener
228 if (mxPicture)
229 {
230 maAllChildren = mrParentDialog.collect_screenshot_data();
231
232 // to make clear that maParentDialogBitmap is a background image, adjust
233 // luminance a bit for maDimmedDialogBitmap - other methods may be applied
234 maDimmedDialogBitmap.Adjust(-15, 0, 0, 0, 0);
235
236 // init paint buffering VirtualDevice
237 mxVirtualBufferDevice = VclPtr<VirtualDevice>::Create(*Application::GetDefaultDevice(), DeviceFormat::DEFAULT);
238 mxVirtualBufferDevice->SetOutputSizePixel(maParentDialogSize);
239 mxVirtualBufferDevice->SetFillColor(COL_TRANSPARENT);
240
241 // initially set image for picture control
242 mxVirtualBufferDevice->DrawBitmapEx(Point(0, 0), maDimmedDialogBitmap);
243
244 // set size for picture control, this will re-layout so that
245 // the picture control shows the whole dialog
246 maPicture.SetOutputSizePixel(maParentDialogSize);
247 mxPicture->set_size_request(maParentDialogSize.Width(), maParentDialogSize.Height());
248
249 mxPicture->queue_draw();
250 }
251
252 // set some test text at VclMultiLineEdit and make read-only - only
253 // copying content to clipboard is allowed
254 if (mxText)
255 {
256 mxText->set_size_request(400, mxText->get_height_rows(10));
257 OUString aHelpId = OStringToOUString( mrParentDialog.get_help_id(), RTL_TEXTENCODING_UTF8 );
258 Size aSizeCm = Application::GetDefaultDevice()->PixelToLogic(maParentDialogSize, MapMode(MapUnit::MapCM));
259 maMainMarkupText = lcl_ParagraphWithImage( aHelpId, aSizeCm );
260 mxText->set_text( maMainMarkupText );
261 mxText->set_editable(false);
262 }
263
264 // set click handler for save button
265 if (mxSave)
266 {
267 mxSave->connect_clicked(LINK(this, ScreenshotAnnotationDlg_Impl, saveButtonHandler));
268 }
269 }
270
~ScreenshotAnnotationDlg_Impl()271 ScreenshotAnnotationDlg_Impl::~ScreenshotAnnotationDlg_Impl()
272 {
273 mxVirtualBufferDevice.disposeAndClear();
274 }
275
IMPL_LINK_NOARG(ScreenshotAnnotationDlg_Impl,saveButtonHandler,weld::Button &,void)276 IMPL_LINK_NOARG(ScreenshotAnnotationDlg_Impl, saveButtonHandler, weld::Button&, void)
277 {
278 // 'save screenshot...' pressed, offer to save maParentDialogBitmap
279 // as PNG image, use *.id file name as screenshot file name offering
280 // get a suggestion for the filename from buildable name
281 OString aDerivedFileName = mrParentDialog.get_buildable_name();
282
283 auto xFileDlg = std::make_unique<sfx2::FileDialogHelper>(ui::dialogs::TemplateDescription::FILESAVE_AUTOEXTENSION,
284 FileDialogFlags::NONE, mpParentWindow);
285
286 const uno::Reference< ui::dialogs::XFilePicker3 > xFilePicker = xFileDlg->GetFilePicker();
287
288 xFilePicker->setTitle(maSaveAsText);
289
290 if (!maLastFolderURL.isEmpty())
291 {
292 xFilePicker->setDisplayDirectory(maLastFolderURL);
293 }
294
295 xFilePicker->appendFilter("*.png", "*.png");
296 xFilePicker->setCurrentFilter("*.png");
297 xFilePicker->setDefaultName(OStringToOUString(aDerivedFileName, RTL_TEXTENCODING_UTF8));
298 xFilePicker->setMultiSelectionMode(false);
299
300 if (xFilePicker->execute() != ui::dialogs::ExecutableDialogResults::OK)
301 return;
302
303 maLastFolderURL = xFilePicker->getDisplayDirectory();
304 const uno::Sequence< OUString > files(xFilePicker->getSelectedFiles());
305
306 if (!files.hasElements())
307 return;
308
309 OUString aConfirmedName = files[0];
310
311 if (aConfirmedName.isEmpty())
312 return;
313
314 INetURLObject aConfirmedURL(aConfirmedName);
315 OUString aCurrentExtension(aConfirmedURL.getExtension());
316
317 if (!aCurrentExtension.isEmpty() && aCurrentExtension != "png")
318 {
319 aConfirmedURL.removeExtension();
320 aCurrentExtension.clear();
321 }
322
323 if (aCurrentExtension.isEmpty())
324 {
325 aConfirmedURL.setExtension(u"png");
326 }
327
328 // open stream
329 SvFileStream aNew(aConfirmedURL.PathToFileName(), StreamMode::WRITE | StreamMode::TRUNC);
330
331 if (!aNew.IsOpen())
332 return;
333
334 // prepare bitmap to save - do use the original screenshot here,
335 // not the dimmed one
336 RepaintToBuffer();
337
338 // extract Bitmap
339 const BitmapEx aTargetBitmap(
340 mxVirtualBufferDevice->GetBitmapEx(
341 Point(0, 0),
342 mxVirtualBufferDevice->GetOutputSizePixel()));
343
344 // write as PNG
345 vcl::PNGWriter aPNGWriter(aTargetBitmap);
346 aPNGWriter.Write(aNew);
347 }
348
CheckHit(const basegfx::B2IPoint & rPosition)349 weld::ScreenShotEntry* ScreenshotAnnotationDlg_Impl::CheckHit(const basegfx::B2IPoint& rPosition)
350 {
351 weld::ScreenShotEntry* pRetval = nullptr;
352
353 for (auto&& rCandidate : maAllChildren)
354 {
355 if (rCandidate.getB2IRange().isInside(rPosition))
356 {
357 if (pRetval)
358 {
359 if (pRetval->getB2IRange().isInside(rCandidate.getB2IRange().getMinimum())
360 && pRetval->getB2IRange().isInside(rCandidate.getB2IRange().getMaximum()))
361 {
362 pRetval = &rCandidate;
363 }
364 }
365 else
366 {
367 pRetval = &rCandidate;
368 }
369 }
370 }
371
372 return pRetval;
373 }
374
PaintScreenShotEntry(const weld::ScreenShotEntry & rEntry,const Color & rColor,double fLineWidth,double fTransparency)375 void ScreenshotAnnotationDlg_Impl::PaintScreenShotEntry(
376 const weld::ScreenShotEntry& rEntry,
377 const Color& rColor,
378 double fLineWidth,
379 double fTransparency)
380 {
381 if (!(mxPicture && mxVirtualBufferDevice))
382 return;
383
384 basegfx::B2DRange aB2DRange(rEntry.getB2IRange());
385
386 // grow in pixels to be a little bit 'outside'. This also
387 // ensures that getWidth()/getHeight() ain't 0.0 (see division below)
388 static const double fGrowTopLeft(1.5);
389 static const double fGrowBottomRight(0.5);
390 aB2DRange.expand(aB2DRange.getMinimum() - basegfx::B2DPoint(fGrowTopLeft, fGrowTopLeft));
391 aB2DRange.expand(aB2DRange.getMaximum() + basegfx::B2DPoint(fGrowBottomRight, fGrowBottomRight));
392
393 // edge rounding in pixel. Need to convert, value for
394 // createPolygonFromRect is relative [0.0 .. 1.0]
395 static const double fEdgeRoundPixel(8.0);
396 const basegfx::B2DPolygon aPolygon(
397 basegfx::utils::createPolygonFromRect(
398 aB2DRange,
399 fEdgeRoundPixel / aB2DRange.getWidth(),
400 fEdgeRoundPixel / aB2DRange.getHeight()));
401
402 mxVirtualBufferDevice->SetLineColor(rColor);
403
404 // try to use transparency
405 if (!mxVirtualBufferDevice->DrawPolyLineDirect(
406 basegfx::B2DHomMatrix(),
407 aPolygon,
408 fLineWidth,
409 fTransparency,
410 nullptr, // MM01
411 basegfx::B2DLineJoin::Round))
412 {
413 // no transparency, draw without
414 mxVirtualBufferDevice->DrawPolyLine(
415 aPolygon,
416 fLineWidth);
417 }
418 }
419
GetOffsetInPicture() const420 Point ScreenshotAnnotationDlg_Impl::GetOffsetInPicture() const
421 {
422 const Size aPixelSizeTarget(maPicture.GetOutputSizePixel());
423
424 return Point(
425 aPixelSizeTarget.Width() > maParentDialogSize.Width() ? (aPixelSizeTarget.Width() - maParentDialogSize.Width()) >> 1 : 0,
426 aPixelSizeTarget.Height() > maParentDialogSize.Height() ? (aPixelSizeTarget.Height() - maParentDialogSize.Height()) >> 1 : 0);
427 }
428
RepaintToBuffer(bool bUseDimmed,bool bPaintHilight)429 void ScreenshotAnnotationDlg_Impl::RepaintToBuffer(
430 bool bUseDimmed,
431 bool bPaintHilight)
432 {
433 if (!mxVirtualBufferDevice)
434 return;
435
436 // reset with original screenshot bitmap
437 mxVirtualBufferDevice->DrawBitmapEx(
438 Point(0, 0),
439 bUseDimmed ? maDimmedDialogBitmap : maParentDialogBitmap);
440
441 // get various options
442 const SvtOptionsDrawinglayer aSvtOptionsDrawinglayer;
443 const Color aHilightColor(aSvtOptionsDrawinglayer.getHilightColor());
444 const double fTransparence(aSvtOptionsDrawinglayer.GetTransparentSelectionPercent() * 0.01);
445 const bool bIsAntiAliasing(aSvtOptionsDrawinglayer.IsAntiAliasing());
446 const AntialiasingFlags nOldAA(mxVirtualBufferDevice->GetAntialiasing());
447
448 if (bIsAntiAliasing)
449 {
450 mxVirtualBufferDevice->SetAntialiasing(AntialiasingFlags::Enable);
451 }
452
453 // paint selected entries
454 for (auto&& rCandidate : maSelected)
455 {
456 static const double fLineWidthEntries(5.0);
457 PaintScreenShotEntry(*rCandidate, COL_LIGHTRED, fLineWidthEntries, fTransparence * 0.2);
458 }
459
460 // paint highlighted entry
461 if (mpHilighted && bPaintHilight)
462 {
463 static const double fLineWidthHilight(7.0);
464 PaintScreenShotEntry(*mpHilighted, aHilightColor, fLineWidthHilight, fTransparence);
465 }
466
467 if (bIsAntiAliasing)
468 {
469 mxVirtualBufferDevice->SetAntialiasing(nOldAA);
470 }
471 }
472
RepaintPictureElement()473 void ScreenshotAnnotationDlg_Impl::RepaintPictureElement()
474 {
475 if (mxPicture && mxVirtualBufferDevice)
476 {
477 // reset image in buffer, use dimmed version and allow highlight
478 RepaintToBuffer(true, true);
479 mxPicture->queue_draw();
480 }
481 }
482
Paint(vcl::RenderContext & rRenderContext)483 void ScreenshotAnnotationDlg_Impl::Paint(vcl::RenderContext& rRenderContext)
484 {
485 Point aPos(GetOffsetInPicture());
486 Size aSize(mxVirtualBufferDevice->GetOutputSizePixel());
487 rRenderContext.DrawOutDev(aPos, aSize, Point(), aSize, *mxVirtualBufferDevice);
488 }
489
Paint(vcl::RenderContext & rRenderContext,const tools::Rectangle &)490 void Picture::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle&)
491 {
492 m_pDialog->Paint(rRenderContext);
493 }
494
MouseMove(const MouseEvent & rMouseEvent)495 bool ScreenshotAnnotationDlg_Impl::MouseMove(const MouseEvent& rMouseEvent)
496 {
497 bool bRepaint(false);
498
499 if (maPicture.IsMouseOver())
500 {
501 const weld::ScreenShotEntry* pOldHit = mpHilighted;
502 const Point aOffset(GetOffsetInPicture());
503 const basegfx::B2IPoint aMousePos(
504 rMouseEvent.GetPosPixel().X() - aOffset.X(),
505 rMouseEvent.GetPosPixel().Y() - aOffset.Y());
506 const weld::ScreenShotEntry* pHit = CheckHit(aMousePos);
507
508 if (pHit && pOldHit != pHit)
509 {
510 mpHilighted = const_cast<weld::ScreenShotEntry*>(pHit);
511 bRepaint = true;
512 }
513 }
514 else if (mpHilighted)
515 {
516 mpHilighted = nullptr;
517 bRepaint = true;
518 }
519
520 if (bRepaint)
521 {
522 RepaintPictureElement();
523 }
524
525 return true;
526 }
527
MouseMove(const MouseEvent & rMouseEvent)528 bool Picture::MouseMove(const MouseEvent& rMouseEvent)
529 {
530 if (rMouseEvent.IsEnterWindow())
531 m_bMouseOver = true;
532 if (rMouseEvent.IsLeaveWindow())
533 m_bMouseOver = false;
534 return m_pDialog->MouseMove(rMouseEvent);
535 }
536
MouseButtonUp()537 bool ScreenshotAnnotationDlg_Impl::MouseButtonUp()
538 {
539 // event in picture frame
540 bool bRepaint(false);
541
542 if (maPicture.IsMouseOver() && mpHilighted)
543 {
544 if (maSelected.erase(mpHilighted) == 0)
545 {
546 maSelected.insert(mpHilighted);
547 }
548
549 OUStringBuffer aBookmarks(maMainMarkupText);
550 for (auto&& rCandidate : maSelected)
551 {
552 OUString aHelpId = OStringToOUString( rCandidate->GetHelpId(), RTL_TEXTENCODING_UTF8 );
553 aBookmarks.append(lcl_Bookmark( aHelpId ));
554 }
555
556 mxText->set_text( aBookmarks.makeStringAndClear() );
557 bRepaint = true;
558 }
559
560 if (bRepaint)
561 {
562 RepaintPictureElement();
563 }
564
565 return true;
566 }
567
MouseButtonUp(const MouseEvent &)568 bool Picture::MouseButtonUp(const MouseEvent&)
569 {
570 return m_pDialog->MouseButtonUp();
571 }
572
ScreenshotAnnotationDlg(weld::Dialog & rParentDialog)573 ScreenshotAnnotationDlg::ScreenshotAnnotationDlg(weld::Dialog& rParentDialog)
574 : GenericDialogController(&rParentDialog, "cui/ui/screenshotannotationdialog.ui", "ScreenshotAnnotationDialog")
575 {
576 m_pImpl.reset(new ScreenshotAnnotationDlg_Impl(m_xDialog.get(), *m_xBuilder, rParentDialog));
577 }
578
~ScreenshotAnnotationDlg()579 ScreenshotAnnotationDlg::~ScreenshotAnnotationDlg()
580 {
581 }
582
583 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
584