1
0
Fork 0
libreoffice/cui/source/dialogs/screenshotannotationdlg.cxx
Daniel Baumann 8e63e14cf6
Adding upstream version 4:25.2.3.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-22 16:20:04 +02:00

578 lines
18 KiB
C++

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* This file is part of the LibreOffice project.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* This file incorporates work covered by the following license notice:
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed
* with this work for additional information regarding copyright
* ownership. The ASF licenses this file to you under the Apache
* License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of
* the License at http://www.apache.org/licenses/LICENSE-2.0 .
*/
#include <screenshotannotationdlg.hxx>
#include <strings.hrc>
#include <dialmgr.hxx>
#include <basegfx/range/b2irange.hxx>
#include <com/sun/star/ui/dialogs/TemplateDescription.hpp>
#include <com/sun/star/ui/dialogs/ExecutableDialogResults.hpp>
#include <com/sun/star/ui/dialogs/XFilePicker3.hpp>
#include <comphelper/random.hxx>
#include <basegfx/polygon/b2dpolygontools.hxx>
#include <sfx2/filedlghelper.hxx>
#include <tools/stream.hxx>
#include <tools/urlobj.hxx>
#include <vcl/bitmapex.hxx>
#include <vcl/customweld.hxx>
#include <vcl/event.hxx>
#include <vcl/filter/PngImageWriter.hxx>
#include <vcl/svapp.hxx>
#include <vcl/salgtype.hxx>
#include <vcl/virdev.hxx>
#include <vcl/weld.hxx>
#include <svtools/optionsdrawinglayer.hxx>
#include <basegfx/matrix/b2dhommatrix.hxx>
#include <set>
#include <string_view>
using namespace com::sun::star;
namespace
{
OUString lcl_genRandom( std::u16string_view rId )
{
//FIXME: plus timestamp
unsigned int nRand = comphelper::rng::uniform_uint_distribution(0, 0xFFFF);
return OUString( rId + OUString::number( nRand ) );
}
OUString lcl_AltDescr()
{
OUString aTempl(u"<alt id=\"%1\">"
" " //FIXME real dialog title or something
"</alt>"_ustr);
aTempl = aTempl.replaceFirst( "%1", lcl_genRandom(u"alt_id") );
return aTempl;
}
OUString lcl_Image( std::u16string_view rScreenshotId, const Size& rSize )
{
OUString aTempl(u"<image id=\"%1\" src=\"media/screenshots/%2.png\""
" width=\"%3cm\" height=\"%4cm\">"
"%5"
"</image>"_ustr);
aTempl = aTempl.replaceFirst( "%1", lcl_genRandom(u"img_id") );
aTempl = aTempl.replaceFirst( "%2", rScreenshotId );
aTempl = aTempl.replaceFirst( "%3", OUString::number( rSize.Width() ) );
aTempl = aTempl.replaceFirst( "%4", OUString::number( rSize.Height() ) );
aTempl = aTempl.replaceFirst( "%5", lcl_AltDescr() );
return aTempl;
}
OUString lcl_ParagraphWithImage( std::u16string_view rScreenshotId, const Size& rSize )
{
OUString aTempl( u"<paragraph id=\"%1\" role=\"paragraph\">%2"
"</paragraph>" SAL_NEWLINE_STRING ""_ustr );
aTempl = aTempl.replaceFirst( "%1", lcl_genRandom(u"par_id") );
aTempl = aTempl.replaceFirst( "%2", lcl_Image(rScreenshotId, rSize) );
return aTempl;
}
OUString lcl_Bookmark( std::u16string_view rWidgetId )
{
OUString aTempl = u"<!-- Bookmark for widget %1 -->" SAL_NEWLINE_STRING
"<bookmark branch=\"hid/%2\" id=\"%3\" localize=\"false\"/>" SAL_NEWLINE_STRING ""_ustr;
aTempl = aTempl.replaceFirst( "%1", rWidgetId );
aTempl = aTempl.replaceFirst( "%2", rWidgetId );
aTempl = aTempl.replaceFirst( "%3", lcl_genRandom(u"bm_id") );
return aTempl;
}
}
namespace
{
class Picture : public weld::CustomWidgetController
{
private:
ScreenshotAnnotationDlg_Impl *m_pDialog;
bool m_bMouseOver;
private:
virtual void Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle&) override;
virtual bool MouseMove(const MouseEvent& rMouseEvent) override;
virtual bool MouseButtonUp(const MouseEvent& rMouseEvent) override;
public:
Picture(ScreenshotAnnotationDlg_Impl* pDialog)
: m_pDialog(pDialog)
, m_bMouseOver(false)
{
}
bool IsMouseOver() const
{
return m_bMouseOver;
}
};
}
class ScreenshotAnnotationDlg_Impl
{
public:
ScreenshotAnnotationDlg_Impl(
weld::Window* pParent,
weld::Builder& rParent,
weld::Dialog& rParentDialog);
~ScreenshotAnnotationDlg_Impl();
private:
// Handler for click on save
DECL_LINK(saveButtonHandler, weld::Button&, void);
// helper methods
weld::ScreenShotEntry* CheckHit(const basegfx::B2IPoint& rPosition);
void PaintScreenShotEntry(
const weld::ScreenShotEntry& rEntry,
const Color& rColor,
double fLineWidth,
double fTransparency);
void RepaintToBuffer(
bool bUseDimmed = false,
bool bPaintHilight = false);
void RepaintPictureElement();
Point GetOffsetInPicture() const;
// local variables
weld::Window* mpParentWindow;
weld::Dialog& mrParentDialog;
BitmapEx maParentDialogBitmap;
BitmapEx maDimmedDialogBitmap;
Size maParentDialogSize;
// VirtualDevice for buffered interaction paints
VclPtr<VirtualDevice> mxVirtualBufferDevice;
// all detected children
weld::ScreenShotCollection maAllChildren;
// highlighted/selected children
weld::ScreenShotEntry* mpHilighted;
std::set< weld::ScreenShotEntry* >
maSelected;
// list of detected controls
Picture maPicture;
std::unique_ptr<weld::CustomWeld> mxPicture;
std::unique_ptr<weld::TextView> mxText;
std::unique_ptr<weld::Button> mxSave;
// save as text
OUString maSaveAsText;
OUString maMainMarkupText;
// folder URL
static OUString maLastFolderURL;
public:
void Paint(vcl::RenderContext& rRenderContext);
bool MouseMove(const MouseEvent& rMouseEvent);
bool MouseButtonUp();
};
OUString ScreenshotAnnotationDlg_Impl::maLastFolderURL = OUString();
ScreenshotAnnotationDlg_Impl::ScreenshotAnnotationDlg_Impl(
weld::Window* pParent,
weld::Builder& rParentBuilder,
weld::Dialog& rParentDialog)
: mpParentWindow(pParent),
mrParentDialog(rParentDialog),
mxVirtualBufferDevice(nullptr),
mpHilighted(nullptr),
maPicture(this),
maSaveAsText(CuiResId(RID_CUISTR_SAVE_SCREENSHOT_AS))
{
VclPtr<VirtualDevice> xParentDialogSurface(rParentDialog.screenshot());
maParentDialogSize = xParentDialogSurface->GetOutputSizePixel();
maParentDialogBitmap = xParentDialogSurface->GetBitmapEx(Point(), maParentDialogSize);
maDimmedDialogBitmap = maParentDialogBitmap;
// image ain't empty
assert(!maParentDialogBitmap.IsEmpty());
assert(0 != maParentDialogBitmap.GetSizePixel().Width());
assert(0 != maParentDialogBitmap.GetSizePixel().Height());
// get needed widgets
mxPicture.reset(new weld::CustomWeld(rParentBuilder, u"picture"_ustr, maPicture));
assert(mxPicture);
mxText = rParentBuilder.weld_text_view(u"text"_ustr);
assert(mxText);
mxSave = rParentBuilder.weld_button(u"save"_ustr);
assert(mxSave);
// set screenshot image at DrawingArea, resize, set event listener
if (mxPicture)
{
maAllChildren = mrParentDialog.collect_screenshot_data();
// to make clear that maParentDialogBitmap is a background image, adjust
// luminance a bit for maDimmedDialogBitmap - other methods may be applied
maDimmedDialogBitmap.Adjust(-15, 0, 0, 0, 0);
// init paint buffering VirtualDevice
mxVirtualBufferDevice = VclPtr<VirtualDevice>::Create(*Application::GetDefaultDevice(), DeviceFormat::WITHOUT_ALPHA);
mxVirtualBufferDevice->SetOutputSizePixel(maParentDialogSize);
mxVirtualBufferDevice->SetFillColor(COL_TRANSPARENT);
// initially set image for picture control
mxVirtualBufferDevice->DrawBitmapEx(Point(0, 0), maDimmedDialogBitmap);
// set size for picture control, this will re-layout so that
// the picture control shows the whole dialog
maPicture.SetOutputSizePixel(maParentDialogSize);
mxPicture->set_size_request(maParentDialogSize.Width(), maParentDialogSize.Height());
mxPicture->queue_draw();
}
// set some test text at VclMultiLineEdit and make read-only - only
// copying content to clipboard is allowed
if (mxText)
{
mxText->set_size_request(400, mxText->get_height_rows(10));
OUString aHelpId = mrParentDialog.get_help_id();
Size aSizeCm = Application::GetDefaultDevice()->PixelToLogic(maParentDialogSize, MapMode(MapUnit::MapCM));
maMainMarkupText = lcl_ParagraphWithImage( aHelpId, aSizeCm );
mxText->set_text( maMainMarkupText );
mxText->set_editable(false);
}
// set click handler for save button
if (mxSave)
{
mxSave->connect_clicked(LINK(this, ScreenshotAnnotationDlg_Impl, saveButtonHandler));
}
}
ScreenshotAnnotationDlg_Impl::~ScreenshotAnnotationDlg_Impl()
{
mxVirtualBufferDevice.disposeAndClear();
}
IMPL_LINK_NOARG(ScreenshotAnnotationDlg_Impl, saveButtonHandler, weld::Button&, void)
{
// 'save screenshot...' pressed, offer to save maParentDialogBitmap
// as PNG image, use *.id file name as screenshot file name offering
// get a suggestion for the filename from buildable name
OUString aDerivedFileName = mrParentDialog.get_buildable_name();
sfx2::FileDialogHelper aFileDlg(ui::dialogs::TemplateDescription::FILESAVE_AUTOEXTENSION,
FileDialogFlags::NONE, mpParentWindow);
aFileDlg.SetContext(sfx2::FileDialogHelper::ScreenshotAnnotation);
const uno::Reference< ui::dialogs::XFilePicker3 > xFilePicker = aFileDlg.GetFilePicker();
xFilePicker->setTitle(maSaveAsText);
if (!maLastFolderURL.isEmpty())
{
xFilePicker->setDisplayDirectory(maLastFolderURL);
}
xFilePicker->appendFilter(u"*.png"_ustr, u"*.png"_ustr);
xFilePicker->setCurrentFilter(u"*.png"_ustr);
xFilePicker->setDefaultName(aDerivedFileName);
xFilePicker->setMultiSelectionMode(false);
if (xFilePicker->execute() != ui::dialogs::ExecutableDialogResults::OK)
return;
maLastFolderURL = xFilePicker->getDisplayDirectory();
const uno::Sequence< OUString > files(xFilePicker->getSelectedFiles());
if (!files.hasElements())
return;
const OUString& aConfirmedName = files[0];
if (aConfirmedName.isEmpty())
return;
INetURLObject aConfirmedURL(aConfirmedName);
OUString aCurrentExtension(aConfirmedURL.getExtension());
if (!aCurrentExtension.isEmpty() && aCurrentExtension != "png")
{
aConfirmedURL.removeExtension();
aCurrentExtension.clear();
}
if (aCurrentExtension.isEmpty())
{
aConfirmedURL.setExtension(u"png");
}
// open stream
SvFileStream aNew(aConfirmedURL.PathToFileName(), StreamMode::WRITE | StreamMode::TRUNC);
if (!aNew.IsOpen())
return;
// prepare bitmap to save - do use the original screenshot here,
// not the dimmed one
RepaintToBuffer();
// extract Bitmap
const BitmapEx aTargetBitmap(
mxVirtualBufferDevice->GetBitmapEx(
Point(0, 0),
mxVirtualBufferDevice->GetOutputSizePixel()));
// write as PNG
vcl::PngImageWriter aPNGWriter(aNew);
aPNGWriter.write(aTargetBitmap);
}
weld::ScreenShotEntry* ScreenshotAnnotationDlg_Impl::CheckHit(const basegfx::B2IPoint& rPosition)
{
weld::ScreenShotEntry* pRetval = nullptr;
for (auto&& rCandidate : maAllChildren)
{
if (rCandidate.getB2IRange().isInside(rPosition))
{
if (pRetval)
{
if (pRetval->getB2IRange().isInside(rCandidate.getB2IRange().getMinimum())
&& pRetval->getB2IRange().isInside(rCandidate.getB2IRange().getMaximum()))
{
pRetval = &rCandidate;
}
}
else
{
pRetval = &rCandidate;
}
}
}
return pRetval;
}
void ScreenshotAnnotationDlg_Impl::PaintScreenShotEntry(
const weld::ScreenShotEntry& rEntry,
const Color& rColor,
double fLineWidth,
double fTransparency)
{
if (!(mxPicture && mxVirtualBufferDevice))
return;
basegfx::B2DRange aB2DRange(rEntry.getB2IRange());
// grow in pixels to be a little bit 'outside'. This also
// ensures that getWidth()/getHeight() ain't 0.0 (see division below)
static const double fGrowTopLeft(1.5);
static const double fGrowBottomRight(0.5);
aB2DRange.expand(aB2DRange.getMinimum() - basegfx::B2DPoint(fGrowTopLeft, fGrowTopLeft));
aB2DRange.expand(aB2DRange.getMaximum() + basegfx::B2DPoint(fGrowBottomRight, fGrowBottomRight));
// edge rounding in pixel. Need to convert, value for
// createPolygonFromRect is relative [0.0 .. 1.0]
static const double fEdgeRoundPixel(8.0);
const basegfx::B2DPolygon aPolygon(
basegfx::utils::createPolygonFromRect(
aB2DRange,
fEdgeRoundPixel / aB2DRange.getWidth(),
fEdgeRoundPixel / aB2DRange.getHeight()));
mxVirtualBufferDevice->SetLineColor(rColor);
// try to use transparency
if (!mxVirtualBufferDevice->DrawPolyLineDirect(
basegfx::B2DHomMatrix(),
aPolygon,
fLineWidth,
fTransparency,
nullptr, // MM01
basegfx::B2DLineJoin::Round))
{
// no transparency, draw without
mxVirtualBufferDevice->DrawPolyLine(
aPolygon,
fLineWidth);
}
}
Point ScreenshotAnnotationDlg_Impl::GetOffsetInPicture() const
{
const Size aPixelSizeTarget(maPicture.GetOutputSizePixel());
return Point(
aPixelSizeTarget.Width() > maParentDialogSize.Width() ? (aPixelSizeTarget.Width() - maParentDialogSize.Width()) >> 1 : 0,
aPixelSizeTarget.Height() > maParentDialogSize.Height() ? (aPixelSizeTarget.Height() - maParentDialogSize.Height()) >> 1 : 0);
}
void ScreenshotAnnotationDlg_Impl::RepaintToBuffer(
bool bUseDimmed,
bool bPaintHilight)
{
if (!mxVirtualBufferDevice)
return;
// reset with original screenshot bitmap
mxVirtualBufferDevice->DrawBitmapEx(
Point(0, 0),
bUseDimmed ? maDimmedDialogBitmap : maParentDialogBitmap);
// get various options
const Color aHilightColor(SvtOptionsDrawinglayer::getHilightColor());
const double fTransparence(SvtOptionsDrawinglayer::GetTransparentSelectionPercent() * 0.01);
const bool bIsAntiAliasing(SvtOptionsDrawinglayer::IsAntiAliasing());
const AntialiasingFlags nOldAA(mxVirtualBufferDevice->GetAntialiasing());
if (bIsAntiAliasing)
{
mxVirtualBufferDevice->SetAntialiasing(AntialiasingFlags::Enable);
}
// paint selected entries
for (auto&& rCandidate : maSelected)
{
static const double fLineWidthEntries(5.0);
PaintScreenShotEntry(*rCandidate, COL_LIGHTRED, fLineWidthEntries, fTransparence * 0.2);
}
// paint highlighted entry
if (mpHilighted && bPaintHilight)
{
static const double fLineWidthHilight(7.0);
PaintScreenShotEntry(*mpHilighted, aHilightColor, fLineWidthHilight, fTransparence);
}
if (bIsAntiAliasing)
{
mxVirtualBufferDevice->SetAntialiasing(nOldAA);
}
}
void ScreenshotAnnotationDlg_Impl::RepaintPictureElement()
{
if (mxPicture && mxVirtualBufferDevice)
{
// reset image in buffer, use dimmed version and allow highlight
RepaintToBuffer(true, true);
mxPicture->queue_draw();
}
}
void ScreenshotAnnotationDlg_Impl::Paint(vcl::RenderContext& rRenderContext)
{
Point aPos(GetOffsetInPicture());
Size aSize(mxVirtualBufferDevice->GetOutputSizePixel());
rRenderContext.DrawOutDev(aPos, aSize, Point(), aSize, *mxVirtualBufferDevice);
}
void Picture::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle&)
{
m_pDialog->Paint(rRenderContext);
}
bool ScreenshotAnnotationDlg_Impl::MouseMove(const MouseEvent& rMouseEvent)
{
bool bRepaint(false);
if (maPicture.IsMouseOver())
{
const weld::ScreenShotEntry* pOldHit = mpHilighted;
const Point aOffset(GetOffsetInPicture());
const basegfx::B2IPoint aMousePos(
rMouseEvent.GetPosPixel().X() - aOffset.X(),
rMouseEvent.GetPosPixel().Y() - aOffset.Y());
const weld::ScreenShotEntry* pHit = CheckHit(aMousePos);
if (pHit && pOldHit != pHit)
{
mpHilighted = const_cast<weld::ScreenShotEntry*>(pHit);
bRepaint = true;
}
}
else if (mpHilighted)
{
mpHilighted = nullptr;
bRepaint = true;
}
if (bRepaint)
{
RepaintPictureElement();
}
return true;
}
bool Picture::MouseMove(const MouseEvent& rMouseEvent)
{
if (rMouseEvent.IsEnterWindow())
m_bMouseOver = true;
if (rMouseEvent.IsLeaveWindow())
m_bMouseOver = false;
return m_pDialog->MouseMove(rMouseEvent);
}
bool ScreenshotAnnotationDlg_Impl::MouseButtonUp()
{
// event in picture frame
bool bRepaint(false);
if (maPicture.IsMouseOver() && mpHilighted)
{
if (maSelected.erase(mpHilighted) == 0)
{
maSelected.insert(mpHilighted);
}
OUStringBuffer aBookmarks(maMainMarkupText);
for (auto&& rCandidate : maSelected)
aBookmarks.append(lcl_Bookmark(rCandidate->GetHelpId()));
mxText->set_text( aBookmarks.makeStringAndClear() );
bRepaint = true;
}
if (bRepaint)
{
RepaintPictureElement();
}
return true;
}
bool Picture::MouseButtonUp(const MouseEvent&)
{
return m_pDialog->MouseButtonUp();
}
ScreenshotAnnotationDlg::ScreenshotAnnotationDlg(weld::Dialog& rParentDialog)
: GenericDialogController(&rParentDialog, u"cui/ui/screenshotannotationdialog.ui"_ustr, u"ScreenshotAnnotationDialog"_ustr)
{
m_pImpl.reset(new ScreenshotAnnotationDlg_Impl(m_xDialog.get(), *m_xBuilder, rParentDialog));
}
ScreenshotAnnotationDlg::~ScreenshotAnnotationDlg()
{
}
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */