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 #include <string_view>
47 
48 using namespace com::sun::star;
49 
50 namespace
51 {
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 
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 
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 
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 
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 {
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  mpHilighted(nullptr),
204  maPicture(this),
205  maSaveAsText(CuiResId(RID_SVXSTR_SAVE_SCREENSHOT_AS))
206 {
207  VclPtr<VirtualDevice> xParentDialogSurface(rParentDialog.screenshot());
208  maParentDialogSize = xParentDialogSurface->GetOutputSizePixel();
209  maParentDialogBitmap = xParentDialogSurface->GetBitmapEx(Point(), maParentDialogSize);
211 
212  // image ain't empty
213  assert(!maParentDialogBitmap.IsEmpty());
214  assert(0 != maParentDialogBitmap.GetSizePixel().Width());
215  assert(0 != maParentDialogBitmap.GetSizePixel().Height());
216 
217  // get needed widgets
218  mxPicture.reset(new weld::CustomWeld(rParentBuilder, "picture", maPicture));
219  assert(mxPicture);
220  mxText = rParentBuilder.weld_text_view("text");
221  assert(mxText);
222  mxSave = rParentBuilder.weld_button("save");
223  assert(mxSave);
224 
225  // set screenshot image at DrawingArea, resize, set event listener
226  if (mxPicture)
227  {
229 
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);
233 
234  // init paint buffering VirtualDevice
236  mxVirtualBufferDevice->SetOutputSizePixel(maParentDialogSize);
238 
239  // initially set image for picture control
241 
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());
246 
247  mxPicture->queue_draw();
248  }
249 
250  // set some test text at VclMultiLineEdit and make read-only - only
251  // copying content to clipboard is allowed
252  if (mxText)
253  {
254  mxText->set_size_request(400, mxText->get_height_rows(10));
255  OUString aHelpId = OStringToOUString( mrParentDialog.get_help_id(), RTL_TEXTENCODING_UTF8 );
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);
260  }
261 
262  // set click handler for save button
263  if (mxSave)
264  {
265  mxSave->connect_clicked(LINK(this, ScreenshotAnnotationDlg_Impl, saveButtonHandler));
266  }
267 }
268 
270 {
272 }
273 
275 {
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  OString aDerivedFileName = mrParentDialog.get_buildable_name();
280 
281  auto xFileDlg = std::make_unique<sfx2::FileDialogHelper>(ui::dialogs::TemplateDescription::FILESAVE_AUTOEXTENSION,
282  FileDialogFlags::NONE, mpParentWindow);
283  xFileDlg->SetContext(sfx2::FileDialogHelper::ScreenshotAnnotation);
284 
285  const uno::Reference< ui::dialogs::XFilePicker3 > xFilePicker = xFileDlg->GetFilePicker();
286 
287  xFilePicker->setTitle(maSaveAsText);
288 
289  if (!maLastFolderURL.isEmpty())
290  {
291  xFilePicker->setDisplayDirectory(maLastFolderURL);
292  }
293 
294  xFilePicker->appendFilter("*.png", "*.png");
295  xFilePicker->setCurrentFilter("*.png");
296  xFilePicker->setDefaultName(OStringToOUString(aDerivedFileName, RTL_TEXTENCODING_UTF8));
297  xFilePicker->setMultiSelectionMode(false);
298 
299  if (xFilePicker->execute() != ui::dialogs::ExecutableDialogResults::OK)
300  return;
301 
302  maLastFolderURL = xFilePicker->getDisplayDirectory();
303  const uno::Sequence< OUString > files(xFilePicker->getSelectedFiles());
304 
305  if (!files.hasElements())
306  return;
307 
308  OUString aConfirmedName = files[0];
309 
310  if (aConfirmedName.isEmpty())
311  return;
312 
313  INetURLObject aConfirmedURL(aConfirmedName);
314  OUString aCurrentExtension(aConfirmedURL.getExtension());
315 
316  if (!aCurrentExtension.isEmpty() && aCurrentExtension != "png")
317  {
318  aConfirmedURL.removeExtension();
319  aCurrentExtension.clear();
320  }
321 
322  if (aCurrentExtension.isEmpty())
323  {
324  aConfirmedURL.setExtension(u"png");
325  }
326 
327  // open stream
328  SvFileStream aNew(aConfirmedURL.PathToFileName(), StreamMode::WRITE | StreamMode::TRUNC);
329 
330  if (!aNew.IsOpen())
331  return;
332 
333  // prepare bitmap to save - do use the original screenshot here,
334  // not the dimmed one
335  RepaintToBuffer();
336 
337  // extract Bitmap
338  const BitmapEx aTargetBitmap(
339  mxVirtualBufferDevice->GetBitmapEx(
340  Point(0, 0),
341  mxVirtualBufferDevice->GetOutputSizePixel()));
342 
343  // write as PNG
344  vcl::PNGWriter aPNGWriter(aTargetBitmap);
345  aPNGWriter.Write(aNew);
346 }
347 
349 {
350  weld::ScreenShotEntry* pRetval = nullptr;
351 
352  for (auto&& rCandidate : maAllChildren)
353  {
354  if (rCandidate.getB2IRange().isInside(rPosition))
355  {
356  if (pRetval)
357  {
358  if (pRetval->getB2IRange().isInside(rCandidate.getB2IRange().getMinimum())
359  && pRetval->getB2IRange().isInside(rCandidate.getB2IRange().getMaximum()))
360  {
361  pRetval = &rCandidate;
362  }
363  }
364  else
365  {
366  pRetval = &rCandidate;
367  }
368  }
369  }
370 
371  return pRetval;
372 }
373 
375  const weld::ScreenShotEntry& rEntry,
376  const Color& rColor,
377  double fLineWidth,
378  double fTransparency)
379 {
381  return;
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 
420 {
421  const Size aPixelSizeTarget(maPicture.GetOutputSizePixel());
422 
423  return Point(
424  aPixelSizeTarget.Width() > maParentDialogSize.Width() ? (aPixelSizeTarget.Width() - maParentDialogSize.Width()) >> 1 : 0,
425  aPixelSizeTarget.Height() > maParentDialogSize.Height() ? (aPixelSizeTarget.Height() - maParentDialogSize.Height()) >> 1 : 0);
426 }
427 
429  bool bUseDimmed,
430  bool bPaintHilight)
431 {
433  return;
434 
435  // reset with original screenshot bitmap
437  Point(0, 0),
439 
440  // get various options
441  const Color aHilightColor(SvtOptionsDrawinglayer::getHilightColor());
442  const double fTransparence(SvtOptionsDrawinglayer::GetTransparentSelectionPercent() * 0.01);
443  const bool bIsAntiAliasing(SvtOptionsDrawinglayer::IsAntiAliasing());
445 
446  if (bIsAntiAliasing)
447  {
448  mxVirtualBufferDevice->SetAntialiasing(AntialiasingFlags::Enable);
449  }
450 
451  // paint selected entries
452  for (auto&& rCandidate : maSelected)
453  {
454  static const double fLineWidthEntries(5.0);
455  PaintScreenShotEntry(*rCandidate, COL_LIGHTRED, fLineWidthEntries, fTransparence * 0.2);
456  }
457 
458  // paint highlighted entry
459  if (mpHilighted && bPaintHilight)
460  {
461  static const double fLineWidthHilight(7.0);
462  PaintScreenShotEntry(*mpHilighted, aHilightColor, fLineWidthHilight, fTransparence);
463  }
464 
465  if (bIsAntiAliasing)
466  {
468  }
469 }
470 
472 {
474  {
475  // reset image in buffer, use dimmed version and allow highlight
476  RepaintToBuffer(true, true);
477  mxPicture->queue_draw();
478  }
479 }
480 
482 {
483  Point aPos(GetOffsetInPicture());
485  rRenderContext.DrawOutDev(aPos, aSize, Point(), aSize, *mxVirtualBufferDevice);
486 }
487 
488 void Picture::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle&)
489 {
490  m_pDialog->Paint(rRenderContext);
491 }
492 
494 {
495  bool bRepaint(false);
496 
497  if (maPicture.IsMouseOver())
498  {
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);
505 
506  if (pHit && pOldHit != pHit)
507  {
508  mpHilighted = const_cast<weld::ScreenShotEntry*>(pHit);
509  bRepaint = true;
510  }
511  }
512  else if (mpHilighted)
513  {
514  mpHilighted = nullptr;
515  bRepaint = true;
516  }
517 
518  if (bRepaint)
519  {
521  }
522 
523  return true;
524 }
525 
526 bool Picture::MouseMove(const MouseEvent& rMouseEvent)
527 {
528  if (rMouseEvent.IsEnterWindow())
529  m_bMouseOver = true;
530  if (rMouseEvent.IsLeaveWindow())
531  m_bMouseOver = false;
532  return m_pDialog->MouseMove(rMouseEvent);
533 }
534 
536 {
537  // event in picture frame
538  bool bRepaint(false);
539 
540  if (maPicture.IsMouseOver() && mpHilighted)
541  {
542  if (maSelected.erase(mpHilighted) == 0)
543  {
544  maSelected.insert(mpHilighted);
545  }
546 
547  OUStringBuffer aBookmarks(maMainMarkupText);
548  for (auto&& rCandidate : maSelected)
549  {
550  OUString aHelpId = OStringToOUString( rCandidate->GetHelpId(), RTL_TEXTENCODING_UTF8 );
551  aBookmarks.append(lcl_Bookmark( aHelpId ));
552  }
553 
554  mxText->set_text( aBookmarks.makeStringAndClear() );
555  bRepaint = true;
556  }
557 
558  if (bRepaint)
559  {
561  }
562 
563  return true;
564 }
565 
567 {
568  return m_pDialog->MouseButtonUp();
569 }
570 
572  : GenericDialogController(&rParentDialog, "cui/ui/screenshotannotationdialog.ui", "ScreenshotAnnotationDialog")
573 {
574  m_pImpl.reset(new ScreenshotAnnotationDlg_Impl(m_xDialog.get(), *m_xBuilder, rParentDialog));
575 }
576 
578 {
579 }
580 
581 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
std::shared_ptr< weld::Dialog > m_xDialog
DECL_LINK(CheckNameHdl, SvxNameDialog &, bool)
IMPL_LINK_NOARG(ScreenshotAnnotationDlg_Impl, saveButtonHandler, weld::Button &, void)
void RepaintToBuffer(bool bUseDimmed=false, bool bPaintHilight=false)
virtual std::unique_ptr< TextView > weld_text_view(const OString &id)=0
void SetAntialiasing(AntialiasingFlags nMode)
std::unique_ptr< ScreenshotAnnotationDlg_Impl > m_pImpl
AntialiasingFlags
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)
void DrawBitmapEx(const Point &rDestPt, const BitmapEx &rBitmapEx)
std::vector< ScreenShotEntry > ScreenShotCollection
constexpr::Color COL_TRANSPARENT(ColorTransparency, 0xFF, 0xFF, 0xFF, 0xFF)
virtual bool MouseButtonUp(const MouseEvent &)
bool removeExtension(sal_Int32 nIndex=LAST_SEGMENT, bool bIgnoreFinalSlash=true)
virtual VclPtr< VirtualDevice > screenshot()=0
void DrawPolyLine(const tools::Polygon &rPoly)
bool setExtension(std::u16string_view rTheExtension, sal_Int32 nIndex=LAST_SEGMENT, bool bIgnoreFinalSlash=true, rtl_TextEncoding eCharset=RTL_TEXTENCODING_UTF8)
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 tools::Long Width() const
bool MouseMove(const MouseEvent &rMouseEvent)
bool IsEnterWindow() const
const ::std::vector< Color > ImpSvNumberformatScan::StandardColor COL_LIGHTRED
void PaintScreenShotEntry(const weld::ScreenShotEntry &rEntry, const Color &rColor, double fLineWidth, double fTransparency)
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))
OUString CuiResId(TranslateId aKey)
Definition: cuiresmgr.cxx:23
bool IsEmpty() const
void SetLineColor()
bool IsLeaveWindow() const
bool isInside(const B2ITuple &rTuple) const
bool SetOutputSizePixel(const Size &rNewSize, bool bErase=true)
B2DPolygon createPolygonFromRect(const B2DRectangle &rRect, double fRadiusX, double fRadiusY)
std::unique_ptr< weld::CustomWeld > mxPicture
void SetFillColor()
float u
bool Write(SvStream &rStream)
std::set< weld::ScreenShotEntry * > maSelected
const basegfx::B2IRange & getB2IRange() const
Size GetOutputSizePixel() const
ScreenshotAnnotationDlg(const ScreenshotAnnotationDlg &)=delete
SAL_WARN_UNUSED_RESULT Point PixelToLogic(const Point &rDevicePt) const
void Paint(vcl::RenderContext &rRenderContext)
std::unique_ptr< weld::TextView > mxText
constexpr tools::Long Height() const
std::unique_ptr< weld::Button > mxSave
virtual std::unique_ptr< Button > weld_button(const OString &id)=0
ScreenshotAnnotationDlg_Impl(weld::Window *pParent, weld::Builder &rParent, weld::Dialog &rParentDialog)
static VclPtr< reference_type > Create(Arg &&...arg)
const Point & GetPosPixel() const
virtual OString get_help_id() const =0
VclPtr< VirtualDevice > mxVirtualBufferDevice
const Size & GetSizePixel() const
sal_uInt16 GetTransparentSelectionPercent()
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