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 <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>
30 
31 #include <comphelper/random.hxx>
33 #include <sfx2/filedlghelper.hxx>
34 #include <tools/stream.hxx>
35 #include <tools/urlobj.hxx>
36 #include <vcl/bitmapex.hxx>
37 #include <vcl/customweld.hxx>
38 #include <vcl/event.hxx>
39 #include <vcl/pngwrite.hxx>
40 #include <vcl/svapp.hxx>
41 #include <vcl/salgtype.hxx>
42 #include <vcl/virdev.hxx>
43 #include <vcl/weld.hxx>
46 #include <set>
47 
48 using namespace com::sun::star;
49 
50 namespace
51 {
52  OUString lcl_genRandom( const OUString &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 
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("alt_id") );
66 
67  return aTempl;
68  }
69 
70  OUString lcl_Image( const OUString& 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("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 
85  OUString lcl_ParagraphWithImage( const OUString& 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("par_id") );
90  aTempl = aTempl.replaceFirst( "%2", lcl_Image(rScreenshotId, rSize) );
91 
92  return aTempl;
93  }
94 
95  OUString lcl_Bookmark( const OUString& 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("bm_id") );
102 
103  return aTempl;
104  }
105 }
106 
107 namespace
108 {
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:
119  Picture(ScreenshotAnnotationDlg_Impl* pDialog)
120  : m_pDialog(pDialog)
121  , m_bMouseOver(false)
122  {
123  }
124 
125  bool IsMouseOver() const
126  {
127  return m_bMouseOver;
128  }
129  };
130 }
131 
133 {
134 public:
136  weld::Window* pParent,
137  weld::Builder& rParent,
138  weld::Dialog& rParentDialog);
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
164 
165  // VirtualDevice for buffered interaction paints
167 
168  // all detected children
170 
171  // highlighted/selected children
173  std::set< weld::ScreenShotEntry* >
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;
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 
195 
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(VclPtr<VirtualDevice>::Create(DeviceFormat::DEFAULT));
210  rParentDialog.draw(*xParentDialogSurface);
211  maParentDialogSize = xParentDialogSurface->GetOutputSizePixel();
212  maParentDialogBitmap = xParentDialogSurface->GetBitmapEx(Point(), maParentDialogSize);
214 
215  // image ain't empty
216  assert(!maParentDialogBitmap.IsEmpty());
217  assert(0 != maParentDialogBitmap.GetSizePixel().Width());
218  assert(0 != maParentDialogBitmap.GetSizePixel().Height());
219 
220  // get needed widgets
221  mxPicture.reset(new weld::CustomWeld(rParentBuilder, "picture", maPicture));
222  assert(mxPicture.get());
223  mxText = rParentBuilder.weld_text_view("text");
224  assert(mxText.get());
225  mxSave = rParentBuilder.weld_button("save");
226  assert(mxSave.get());
227 
228  // set screenshot image at DrawingArea, resize, set event listener
229  if (mxPicture)
230  {
232 
233  // to make clear that maParentDialogBitmap is a background image, adjust
234  // luminance a bit for maDimmedDialogBitmap - other methods may be applied
235  maDimmedDialogBitmap.Adjust(-15, 0, 0, 0, 0);
236 
237  // init paint buffering VirtualDevice
238  mxVirtualBufferDevice = VclPtr<VirtualDevice>::Create(*Application::GetDefaultDevice(), DeviceFormat::DEFAULT, DeviceFormat::BITMASK);
241 
242  // initially set image for picture control
244 
245  // set size for picture control, this will re-layout so that
246  // the picture control shows the whole dialog
247  maPicture.SetOutputSizePixel(maParentDialogSize);
249 
250  mxPicture->queue_draw();
251  }
252 
253  // set some test text at VclMultiLineEdit and make read-only - only
254  // copying content to clipboard is allowed
255  if (mxText)
256  {
257  mxText->set_size_request(400, mxText->get_height_rows(10));
258  OUString aHelpId = OStringToOUString( mrParentDialog.get_help_id(), RTL_TEXTENCODING_UTF8 );
260  maMainMarkupText = lcl_ParagraphWithImage( aHelpId, aSizeCm );
261  mxText->set_text( maMainMarkupText );
262  mxText->set_editable(false);
263  }
264 
265  // set click handler for save button
266  if (mxSave)
267  {
268  mxSave->connect_clicked(LINK(this, ScreenshotAnnotationDlg_Impl, saveButtonHandler));
269  }
270 }
271 
273 {
275 }
276 
278 {
279  // 'save screenshot...' pressed, offer to save maParentDialogBitmap
280  // as PNG image, use *.id file name as screenshot file name offering
281  // get a suggestion for the filename from buildable name
282  OString aDerivedFileName = mrParentDialog.get_buildable_name();
283 
284  auto xFileDlg = std::make_unique<sfx2::FileDialogHelper>(ui::dialogs::TemplateDescription::FILESAVE_AUTOEXTENSION,
285  FileDialogFlags::NONE, mpParentWindow);
286 
287  const uno::Reference< ui::dialogs::XFilePicker3 > xFilePicker = xFileDlg->GetFilePicker();
288 
289  xFilePicker->setTitle(maSaveAsText);
290 
291  if (!maLastFolderURL.isEmpty())
292  {
293  xFilePicker->setDisplayDirectory(maLastFolderURL);
294  }
295 
296  xFilePicker->appendFilter("*.png", "*.png");
297  xFilePicker->setCurrentFilter("*.png");
298  xFilePicker->setDefaultName(OStringToOUString(aDerivedFileName, RTL_TEXTENCODING_UTF8));
299  xFilePicker->setMultiSelectionMode(false);
300 
301  if (xFilePicker->execute() == ui::dialogs::ExecutableDialogResults::OK)
302  {
303  maLastFolderURL = xFilePicker->getDisplayDirectory();
304  const uno::Sequence< OUString > files(xFilePicker->getSelectedFiles());
305 
306  if (files.hasElements())
307  {
308  OUString aConfirmedName = files[0];
309 
310  if (!aConfirmedName.isEmpty())
311  {
312  INetURLObject aConfirmedURL(aConfirmedName);
313  OUString aCurrentExtension(aConfirmedURL.getExtension());
314 
315  if (!aCurrentExtension.isEmpty() && aCurrentExtension != "png")
316  {
317  aConfirmedURL.removeExtension();
318  aCurrentExtension.clear();
319  }
320 
321  if (aCurrentExtension.isEmpty())
322  {
323  aConfirmedURL.setExtension("png");
324  }
325 
326  // open stream
327  SvFileStream aNew(aConfirmedURL.PathToFileName(), StreamMode::WRITE | StreamMode::TRUNC);
328 
329  if (aNew.IsOpen())
330  {
331  // prepare bitmap to save - do use the original screenshot here,
332  // not the dimmed one
333  RepaintToBuffer();
334 
335  // extract Bitmap
336  const BitmapEx aTargetBitmap(
337  mxVirtualBufferDevice->GetBitmapEx(
338  Point(0, 0),
339  mxVirtualBufferDevice->GetOutputSizePixel()));
340 
341  // write as PNG
342  vcl::PNGWriter aPNGWriter(aTargetBitmap);
343  aPNGWriter.Write(aNew);
344  }
345  }
346  }
347  }
348 }
349 
351 {
352  weld::ScreenShotEntry* pRetval = nullptr;
353 
354  for (auto&& rCandidate : maAllChildren)
355  {
356  if (rCandidate.getB2IRange().isInside(rPosition))
357  {
358  if (pRetval)
359  {
360  if (pRetval->getB2IRange().isInside(rCandidate.getB2IRange().getMinimum())
361  && pRetval->getB2IRange().isInside(rCandidate.getB2IRange().getMaximum()))
362  {
363  pRetval = &rCandidate;
364  }
365  }
366  else
367  {
368  pRetval = &rCandidate;
369  }
370  }
371  }
372 
373  return pRetval;
374 }
375 
377  const weld::ScreenShotEntry& rEntry,
378  const Color& rColor,
379  double fLineWidth,
380  double fTransparency)
381 {
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(
398  aB2DRange,
399  fEdgeRoundPixel / aB2DRange.getWidth(),
400  fEdgeRoundPixel / aB2DRange.getHeight()));
401 
403 
404  // try to use transparency
407  aPolygon,
408  fLineWidth,
409  fTransparency,
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
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 DrawPolyLineDirect(const basegfx::B2DHomMatrix &rObjectTransform, const basegfx::B2DPolygon &rB2DPolygon, double fLineWidth=0.0, double fTransparency=0.0, basegfx::B2DLineJoin eLineJoin=basegfx::B2DLineJoin::NONE, css::drawing::LineCap eLineCap=css::drawing::LineCap_BUTT, double fMiterMinimumAngle=basegfx::deg2rad(15.0), bool bBypassAACheck=false)
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
long X() const
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
long Y() const
OUString getExtension(sal_Int32 nIndex=LAST_SEGMENT, bool bIgnoreFinalSlash=true, DecodeMechanism eMechanism=DecodeMechanism::ToIUri, rtl_TextEncoding eCharset=RTL_TEXTENCODING_UTF8) const
Color getHilightColor() const