LibreOffice Module cui (master)  1
screenshotannotationdlg.cxx
Go to the documentation of this file.
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 
21 
22 #include <strings.hrc>
23 #include <dialmgr.hxx>
24 
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>
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>
45 #include <set>
46 
47 using namespace com::sun::star;
48 
49 namespace
50 {
51  OUString lcl_genRandom( const OUString &rId )
52  {
53  //FIXME: plus timestamp
54  unsigned int nRand = comphelper::rng::uniform_uint_distribution(0, 0xFFFF);
55  return OUString( rId + OUString::number( nRand ) );
56  }
57 
58 
59  OUString lcl_AltDescr()
60  {
61  OUString aTempl("<alt id=\"%1\">"
62  " " //FIXME real dialog title or something
63  "</alt>");
64  aTempl = aTempl.replaceFirst( "%1", lcl_genRandom("alt_id") );
65 
66  return aTempl;
67  }
68 
69  OUString lcl_Image( const OUString& rScreenshotId, const Size& rSize )
70  {
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() );
80 
81  return aTempl;
82  }
83 
84  OUString lcl_ParagraphWithImage( const OUString& rScreenshotId, const Size& rSize )
85  {
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) );
90 
91  return aTempl;
92  }
93 
94  OUString lcl_Bookmark( const OUString& rWidgetId )
95  {
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") );
101 
102  return aTempl;
103  }
104 }
105 
106 namespace
107 {
109  {
110  private:
111  ScreenshotAnnotationDlg_Impl *m_pDialog;
112  bool m_bMouseOver;
113  private:
114  virtual void Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle&) override;
115  virtual bool MouseMove(const MouseEvent& rMouseEvent) override;
116  virtual bool MouseButtonUp(const MouseEvent& rMouseEvent) override;
117  public:
118  Picture(ScreenshotAnnotationDlg_Impl* pDialog)
119  : m_pDialog(pDialog)
120  , m_bMouseOver(false)
121  {
122  }
123 
124  bool IsMouseOver() const
125  {
126  return m_bMouseOver;
127  }
128  };
129 }
130 
132 {
133 public:
135  weld::Window* pParent,
136  weld::Builder& rParent,
137  weld::Dialog& rParentDialog);
139 
140 private:
141  // Handler for click on save
142  DECL_LINK(saveButtonHandler, weld::Button&, void);
143 
144  // helper methods
145  weld::ScreenShotEntry* CheckHit(const basegfx::B2IPoint& rPosition);
146  void PaintScreenShotEntry(
147  const weld::ScreenShotEntry& rEntry,
148  const Color& rColor,
149  double fLineWidth,
150  double fTransparency);
151  void RepaintToBuffer(
152  bool bUseDimmed = false,
153  bool bPaintHilight = false);
154  void RepaintPictureElement();
155  Point GetOffsetInPicture() const;
156 
157  // local variables
163 
164  // VirtualDevice for buffered interaction paints
166 
167  // all detected children
169 
170  // highlighted/selected children
172  std::set< weld::ScreenShotEntry* >
174 
175  // list of detected controls
176  Picture maPicture;
177  std::unique_ptr<weld::CustomWeld> mxPicture;
178  std::unique_ptr<weld::TextView> mxText;
179  std::unique_ptr<weld::Button> mxSave;
180 
181  // save as text
182  OUString maSaveAsText;
184 
185  // folder URL
186  static OUString maLastFolderURL;
187 public:
188  void Paint(vcl::RenderContext& rRenderContext);
189  bool MouseMove(const MouseEvent& rMouseEvent);
190  bool MouseButtonUp();
191 };
192 
194 
196  weld::Window* pParent,
197  weld::Builder& rParentBuilder,
198  weld::Dialog& rParentDialog)
199 : mpParentWindow(pParent),
200  mrParentDialog(rParentDialog),
201  mxVirtualBufferDevice(nullptr),
202  maAllChildren(),
203  mpHilighted(nullptr),
204  maSelected(),
205  maPicture(this),
206  maSaveAsText(CuiResId(RID_SVXSTR_SAVE_SCREENSHOT_AS))
207 {
208  VclPtr<VirtualDevice> xParentDialogSurface(VclPtr<VirtualDevice>::Create(DeviceFormat::DEFAULT));
209  rParentDialog.draw(*xParentDialogSurface);
210  maParentDialogSize = xParentDialogSurface->GetOutputSizePixel();
211  maParentDialogBitmap = xParentDialogSurface->GetBitmapEx(Point(), maParentDialogSize);
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.get());
222  mxText = rParentBuilder.weld_text_view("text");
223  assert(mxText.get());
224  mxSave = rParentBuilder.weld_button("save");
225  assert(mxSave.get());
226 
227  // set screenshot image at DrawingArea, resize, set event listener
228  if (mxPicture)
229  {
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, DeviceFormat::BITMASK);
240 
241  // initially set image for picture control
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);
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 );
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 
272 {
274 }
275 
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  {
302  maLastFolderURL = xFilePicker->getDisplayDirectory();
303  const uno::Sequence< OUString > files(xFilePicker->getSelectedFiles());
304 
305  if (files.hasElements())
306  {
307  OUString aConfirmedName = files[0];
308 
309  if (!aConfirmedName.isEmpty())
310  {
311  INetURLObject aConfirmedURL(aConfirmedName);
312  OUString aCurrentExtension(aConfirmedURL.getExtension());
313 
314  if (!aCurrentExtension.isEmpty() && aCurrentExtension != "png")
315  {
316  aConfirmedURL.removeExtension();
317  aCurrentExtension.clear();
318  }
319 
320  if (aCurrentExtension.isEmpty())
321  {
322  aConfirmedURL.setExtension("png");
323  }
324 
325  // open stream
326  SvFileStream aNew(aConfirmedURL.PathToFileName(), StreamMode::WRITE | StreamMode::TRUNC);
327 
328  if (aNew.IsOpen())
329  {
330  // prepare bitmap to save - do use the original screenshot here,
331  // not the dimmed one
332  RepaintToBuffer();
333 
334  // extract Bitmap
335  const BitmapEx aTargetBitmap(
336  mxVirtualBufferDevice->GetBitmapEx(
337  Point(0, 0),
338  mxVirtualBufferDevice->GetOutputSizePixel()));
339 
340  // write as PNG
341  vcl::PNGWriter aPNGWriter(aTargetBitmap);
342  aPNGWriter.Write(aNew);
343  }
344  }
345  }
346  }
347 }
348 
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 
376  const weld::ScreenShotEntry& rEntry,
377  const Color& rColor,
378  double fLineWidth,
379  double fTransparency)
380 {
382  {
383  basegfx::B2DRange aB2DRange(rEntry.getB2IRange());
384 
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));
391 
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(
397  aB2DRange,
398  fEdgeRoundPixel / aB2DRange.getWidth(),
399  fEdgeRoundPixel / aB2DRange.getHeight()));
400 
402 
403  // try to use transparency
406  aPolygon,
407  fLineWidth,
408  fTransparency,
409  nullptr, // MM01
411  {
412  // no transparency, draw without
414  aPolygon,
415  fLineWidth);
416  }
417  }
418 }
419 
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 
430  bool bUseDimmed,
431  bool bPaintHilight)
432 {
434  {
435  // reset with original screenshot bitmap
437  Point(0, 0),
439 
440  // get various options
441  const SvtOptionsDrawinglayer aSvtOptionsDrawinglayer;
442  const Color aHilightColor(aSvtOptionsDrawinglayer.getHilightColor());
443  const double fTransparence(aSvtOptionsDrawinglayer.GetTransparentSelectionPercent() * 0.01);
444  const bool bIsAntiAliasing(aSvtOptionsDrawinglayer.IsAntiAliasing());
446 
447  if (bIsAntiAliasing)
448  {
449  mxVirtualBufferDevice->SetAntialiasing(AntialiasingFlags::EnableB2dDraw);
450  }
451 
452  // paint selected entries
453  for (auto&& rCandidate : maSelected)
454  {
455  static const double fLineWidthEntries(5.0);
456  PaintScreenShotEntry(*rCandidate, COL_LIGHTRED, fLineWidthEntries, fTransparence * 0.2);
457  }
458 
459  // paint highlighted entry
460  if (mpHilighted && bPaintHilight)
461  {
462  static const double fLineWidthHilight(7.0);
463  PaintScreenShotEntry(*mpHilighted, aHilightColor, fLineWidthHilight, fTransparence);
464  }
465 
466  if (bIsAntiAliasing)
467  {
469  }
470  }
471 }
472 
474 {
476  {
477  // reset image in buffer, use dimmed version and allow highlight
478  RepaintToBuffer(true, true);
479  mxPicture->queue_draw();
480  }
481 }
482 
484 {
485  Point aPos(GetOffsetInPicture());
487  rRenderContext.DrawOutDev(aPos, aSize, Point(), aSize, *mxVirtualBufferDevice);
488 }
489 
490 void Picture::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle&)
491 {
492  m_pDialog->Paint(rRenderContext);
493 }
494 
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  {
523  }
524 
525  return true;
526 }
527 
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 
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  {
563  }
564 
565  return true;
566 }
567 
569 {
570  return m_pDialog->MouseButtonUp();
571 }
572 
574  : GenericDialogController(&rParentDialog, "cui/ui/screenshotannotationdialog.ui", "ScreenshotAnnotationDialog")
575 {
576  m_pImpl.reset(new ScreenshotAnnotationDlg_Impl(m_xDialog.get(), *m_xBuilder, rParentDialog));
577 }
578 
580 {
581 }
582 
583 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
long Width() const
std::shared_ptr< weld::Dialog > m_xDialog
IMPL_LINK_NOARG(ScreenshotAnnotationDlg_Impl, saveButtonHandler, weld::Button &, void)
void RepaintToBuffer(bool bUseDimmed=false, bool bPaintHilight=false)
void SetAntialiasing(AntialiasingFlags nMode)
std::unique_ptr< weld::Builder > m_xBuilder
bool Adjust(short nLuminancePercent, short nContrastPercent, short nChannelRPercent, short nChannelGPercent, short nChannelBPercent, double fGamma=1.0, bool bInvert=false, bool msoBrightness=false)
sal_uInt16 GetTransparentSelectionPercent() const
long Height() const
void DrawBitmapEx(const Point &rDestPt, const BitmapEx &rBitmapEx)
std::vector< ScreenShotEntry > ScreenShotCollection
bool DrawPolyLineDirect(const basegfx::B2DHomMatrix &rObjectTransform, const basegfx::B2DPolygon &rB2DPolygon, double fLineWidth=0.0, double fTransparency=0.0, const std::vector< double > *=nullptr, basegfx::B2DLineJoin eLineJoin=basegfx::B2DLineJoin::NONE, css::drawing::LineCap eLineCap=css::drawing::LineCap_BUTT, double fMiterMinimumAngle=basegfx::deg2rad(15.0), bool bBypassAACheck=false)
virtual bool MouseButtonUp(const MouseEvent &)
bool removeExtension(sal_Int32 nIndex=LAST_SEGMENT, bool bIgnoreFinalSlash=true)
std::unique_ptr< ScreenshotAnnotationDlg_Impl > m_pImpl
void DrawPolyLine(const tools::Polygon &rPoly)
virtual std::unique_ptr< Button > weld_button(const OString &id, bool bTakeOwnership=false)=0
constexpr::Color COL_LIGHTRED(0xFF, 0x00, 0x00)
virtual bool MouseMove(const MouseEvent &)
virtual ScreenShotCollection collect_screenshot_data()=0
AntialiasingFlags GetAntialiasing() const
unsigned int uniform_uint_distribution(unsigned int a, unsigned int b)
weld::ScreenShotCollection maAllChildren
OUString PathToFileName() const
static OutputDevice * GetDefaultDevice()
constexpr::Color COL_TRANSPARENT(0xFF, 0xFF, 0xFF, 0xFF)
bool setExtension(OUString const &rTheExtension, sal_Int32 nIndex=LAST_SEGMENT, bool bIgnoreFinalSlash=true, rtl_TextEncoding eCharset=RTL_TEXTENCODING_UTF8)
bool MouseMove(const MouseEvent &rMouseEvent)
bool IsEnterWindow() const
void PaintScreenShotEntry(const weld::ScreenShotEntry &rEntry, const Color &rColor, double fLineWidth, double fTransparency)
bool IsEmpty() const
void SetLineColor()
bool IsLeaveWindow() const
bool isInside(const B2ITuple &rTuple) const
bool SetOutputSizePixel(const Size &rNewSize, bool bErase=true)
virtual std::unique_ptr< TextView > weld_text_view(const OString &id, bool bTakeOwnership=false)=0
B2DPolygon createPolygonFromRect(const B2DRectangle &rRect, double fRadiusX, double fRadiusY)
std::unique_ptr< weld::CustomWeld > mxPicture
void SetFillColor()
AntialiasingFlags
bool Write(SvStream &rStream)
std::set< weld::ScreenShotEntry * > maSelected
const basegfx::B2IRange & getB2IRange() const
Size GetOutputSizePixel() const
virtual void draw(VirtualDevice &rOutput)=0
OUString CuiResId(const char *pKey)
Definition: cuiresmgr.cxx:23
ScreenshotAnnotationDlg(const ScreenshotAnnotationDlg &)=delete
Point PixelToLogic(const Point &rDevicePt) const
void Paint(vcl::RenderContext &rRenderContext)
std::unique_ptr< weld::TextView > mxText
std::unique_ptr< weld::Button > mxSave
ScreenshotAnnotationDlg_Impl(weld::Window *pParent, weld::Builder &rParent, weld::Dialog &rParentDialog)
static VclPtr< reference_type > Create(Arg &&...arg)
const Point & GetPosPixel() const
bool IsAntiAliasing() const
BitmapEx GetBitmapEx(const Point &rSrcPt, const Size &rSize) const
virtual OString get_help_id() const =0
VclPtr< VirtualDevice > mxVirtualBufferDevice
const Size & GetSizePixel() const
virtual void Paint(vcl::RenderContext &rRenderContext, const tools::Rectangle &rRect)=0
virtual ~ScreenshotAnnotationDlg() override
#define SAL_NEWLINE_STRING
weld::ScreenShotEntry * CheckHit(const basegfx::B2IPoint &rPosition)
SAL_DLLPRIVATE void DrawOutDev(const Point &, const Size &, const Point &, const Size &, const Printer &)=delete
OUString getExtension(sal_Int32 nIndex=LAST_SEGMENT, bool bIgnoreFinalSlash=true, DecodeMechanism eMechanism=DecodeMechanism::ToIUri, rtl_TextEncoding eCharset=RTL_TEXTENCODING_UTF8) const
Color getHilightColor() const