diff options
author | Armin Le Grand <Armin.Le.Grand@cib.de> | 2016-08-25 16:50:41 +0200 |
---|---|---|
committer | Thorsten Behrens <Thorsten.Behrens@CIB.de> | 2016-08-28 00:32:50 +0200 |
commit | f9238c34f090adcc6a9da71349b776e7416ade91 (patch) | |
tree | 9fe512a6909c57608236d7263f5f613c21a51ed3 /vcl | |
parent | 25aa9f30489801b2ed51395d7dad20ec0a2b0473 (diff) |
screenshot: develop interactive screenshot dialog
Implemented a first version of an interactive
screenshot dialog that supports annotations, text excerpts
for these and allows to create a screenshot file
How to use:
* enable experimental features in Tools->Options
* open any random dialog
* open context menu - there's now a 'Screenshot' entry
* click that, screenshot dialog pops open, mouse over the
grayed-out screenshot to highlight controls
* click once to highlight, click a 2nd time to unselect
Change-Id: I3bcd76b96ad023e11421e4fcfac866ebf4f5ff78
Diffstat (limited to 'vcl')
-rw-r--r-- | vcl/UIConfig_vcl.mk | 1 | ||||
-rw-r--r-- | vcl/source/window/layout.cxx | 609 | ||||
-rw-r--r-- | vcl/uiconfig/ui/screenshotannotationdialog.ui | 105 |
3 files changed, 715 insertions, 0 deletions
diff --git a/vcl/UIConfig_vcl.mk b/vcl/UIConfig_vcl.mk index a81b30ed497a..888737ff82e1 100644 --- a/vcl/UIConfig_vcl.mk +++ b/vcl/UIConfig_vcl.mk @@ -19,6 +19,7 @@ $(eval $(call gb_UIConfig_add_uifiles,vcl,\ vcl/uiconfig/ui/printerpropertiesdialog \ vcl/uiconfig/ui/printprogressdialog \ vcl/uiconfig/ui/querydialog \ + vcl/uiconfig/ui/screenshotannotationdialog \ )) # vim: set noet sw=4 ts=4: diff --git a/vcl/source/window/layout.cxx b/vcl/source/window/layout.cxx index dd776a5b68dc..79d4efd89d09 100644 --- a/vcl/source/window/layout.cxx +++ b/vcl/source/window/layout.cxx @@ -18,6 +18,7 @@ #include <vcl/settings.hxx> #include "window.h" #include <boost/multi_array.hpp> +#include <officecfg/Office/Common.hxx> VclContainer::VclContainer(vcl::Window *pParent, WinBits nStyle) : Window(WINDOW_CONTAINER) @@ -168,6 +169,614 @@ void VclContainer::queue_resize(StateChangedType eReason) Window::queue_resize(eReason); } +////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// +#include <comphelper/random.hxx> +#include <basegfx/range/b2irange.hxx> + +class ControlDataEntry +{ +public: + ControlDataEntry( + const vcl::Window& rControl, + const basegfx::B2IRange& rB2IRange) + : mrControl(rControl), + maB2IRange(rB2IRange) + { + } + + const vcl::Window& getControl() const + { + return mrControl; + } + + const basegfx::B2IRange& getB2IRange() const + { + return maB2IRange; + } + +private: + const vcl::Window& mrControl; + basegfx::B2IRange maB2IRange; +}; + +typedef ::std::vector< ControlDataEntry > ControlDataCollection; +typedef ::std::set< ControlDataEntry* > ControlDataSet; + +class ScreenshotAnnotationDlg : public ModalDialog +{ +public: + ScreenshotAnnotationDlg( + vcl::Window* pParent, + Dialog& rParentDialog); + virtual ~ScreenshotAnnotationDlg(); + virtual void dispose() override; + +private: + // Handler for click on save + DECL_LINK_TYPED(saveButtonHandler, Button*, void); + + // Handler for clicks on picture frame + DECL_LINK_TYPED(pictureFrameListener, VclWindowEvent&, void); + + // helper methods + void CollectChildren( + const vcl::Window& rCurrent, + const basegfx::B2IPoint& rTopLeft, + ControlDataCollection& rControlDataCollection); + ControlDataEntry* CheckHit(const basegfx::B2IPoint& rPosition); + void PaintControlDataEntry( + const ControlDataEntry& rEntry, + const Color& rColor, + double fLineWidth, + double fTransparency = 0.0); + void RepaintPictureElement(); + Point GetOffsetInPicture() const; + + // local variables + Dialog& mrParentDialog; + Bitmap maParentDialogBitmap; + Size maParentDialogSize; + + // VirtualDevice for buffered interation paints + VclPtr<VirtualDevice> mpVirtualBufferDevice; + + // all detected children + ControlDataCollection maAllChildren; + + // hilighted/selected children + ControlDataEntry* mpHilighted; + ControlDataSet maSelected; + + // list of detected controls + VclPtr<FixedImage> mpPicture; + VclPtr<VclMultiLineEdit> mpText; + VclPtr<PushButton> mpSave; +}; + +void ScreenshotAnnotationDlg::CollectChildren( + const vcl::Window& rCurrent, + const basegfx::B2IPoint& rTopLeft, + ControlDataCollection& rControlDataCollection) +{ + if (rCurrent.IsVisible()) + { + const Point aCurrentPos(rCurrent.GetPosPixel()); + const Size aCurrentSize(rCurrent.GetSizePixel()); + const basegfx::B2IPoint aCurrentTopLeft(rTopLeft.getX() + aCurrentPos.X(), rTopLeft.getY() + aCurrentPos.Y()); + const basegfx::B2IRange aCurrentRange(aCurrentTopLeft, aCurrentTopLeft + basegfx::B2IPoint(aCurrentSize.Width(), aCurrentSize.Height())); + + if (!aCurrentRange.isEmpty()) + { + rControlDataCollection.push_back( + ControlDataEntry( + rCurrent, + aCurrentRange)); + } + + for (sal_uInt16 a(0); a < rCurrent.GetChildCount(); a++) + { + vcl::Window* pChild = rCurrent.GetChild(a); + + if (nullptr != pChild) + { + CollectChildren(*pChild, aCurrentTopLeft, rControlDataCollection); + } + } + } +} + +ScreenshotAnnotationDlg::ScreenshotAnnotationDlg( + vcl::Window* pParent, + Dialog& rParentDialog) + : ModalDialog(pParent, "ScreenshotAnnotationDialog", "vcl/ui/screenshotannotationdialog.ui"), + mrParentDialog(rParentDialog), + maParentDialogBitmap(rParentDialog.createScreenshot()), + maParentDialogSize(maParentDialogBitmap.GetSizePixel()), + mpVirtualBufferDevice(nullptr), + maAllChildren(), + mpHilighted(nullptr), + maSelected(), + mpPicture(nullptr), + mpText(nullptr), + mpSave(nullptr) +{ + // image ain't empty + assert(!maParentDialogBitmap.IsEmpty()); + assert(0 != maParentDialogBitmap.GetSizePixel().Width()); + assert(0 != maParentDialogBitmap.GetSizePixel().Height()); + + // get needed widgets + get(mpPicture, "picture"); + assert(mpPicture.get()); + get(mpText, "text"); + assert(mpText.get()); + get(mpSave, "save"); + assert(mpSave.get()); + + // set screenshot image at FixedImage, resize, set event listener + if (mpPicture) + { + // colelct all children. Choose start pos to be negative + // of target dialog's position to get all positions relative to (0,0) + const Point aParentPos(rParentDialog.GetPosPixel()); + const basegfx::B2IPoint aTopLeft(-aParentPos.X(), -aParentPos.Y()); + + CollectChildren( + rParentDialog, + aTopLeft, + maAllChildren); + + // to make clear that maParentDialogBitmap is a background image, adjust + // luminance a bit - other methods may be applied + maParentDialogBitmap.Adjust(-15); + + // init paint buffering VuirtualDevice + mpVirtualBufferDevice = new VirtualDevice(*Application::GetDefaultDevice(), DeviceFormat::DEFAULT, DeviceFormat::BITMASK); + mpVirtualBufferDevice->SetOutputSizePixel(maParentDialogSize); + mpVirtualBufferDevice->SetFillColor(COL_TRANSPARENT); + + // do paint all collected children for test purposes + static bool bTestPaint(true); + + if (bTestPaint) + { + mpVirtualBufferDevice->DrawBitmap(Point(0, 0), maParentDialogBitmap); + + for (auto aCandidate = maAllChildren.begin(); aCandidate != maAllChildren.end(); aCandidate++) + { + ControlDataEntry& rCandidate = *aCandidate; + + const basegfx::B2IRange& rB2IRange(rCandidate.getB2IRange()); + const Rectangle aRect(rB2IRange.getMinX(), rB2IRange.getMinY(), rB2IRange.getMaxX(), rB2IRange.getMaxY()); + const Color aRandomColor(comphelper::rng::uniform_uint_distribution(0, 255), comphelper::rng::uniform_uint_distribution(0, 255), comphelper::rng::uniform_uint_distribution(0, 255)); + + mpVirtualBufferDevice->SetLineColor(aRandomColor); + mpVirtualBufferDevice->DrawRect(aRect); + } + + maParentDialogBitmap = mpVirtualBufferDevice->GetBitmap(Point(0, 0), maParentDialogSize); + } + + // set image for picture control + mpPicture->SetImage(Image(maParentDialogBitmap)); + + // set size for picture control, this will re-layout so that + // the picture control shows the whole dialog + mpPicture->set_width_request(maParentDialogSize.Width()); + mpPicture->set_height_request(maParentDialogSize.Height()); + + // add local event listener to allow interactions with mouse + mpPicture->AddEventListener(LINK(this, ScreenshotAnnotationDlg, pictureFrameListener)); + + // avoid image scaling, this is needed for images smaller than the + // minimal dialog size + const WinBits aWinBits(mpPicture->GetStyle()); + mpPicture->SetStyle(aWinBits & (!WinBits(WB_SCALE))); + } + + // set some test text at VclMultiLineEdit and make read-only - only + // copying content to clipboard is allowed + if (mpText) + { + mpText->SetText("The quick brown fox jumps over the lazy dog :)"); + mpText->SetReadOnly(true); + } + + // set click handler for save button + if (mpSave) + { + mpSave->SetClickHdl(LINK(this, ScreenshotAnnotationDlg, saveButtonHandler)); + } +} + +ScreenshotAnnotationDlg::~ScreenshotAnnotationDlg() +{ + mpVirtualBufferDevice.disposeAndClear(); + disposeOnce(); +} + +void ScreenshotAnnotationDlg::dispose() +{ + ModalDialog::dispose(); +} + +IMPL_LINK_TYPED(ScreenshotAnnotationDlg, saveButtonHandler, Button*, pButton, void) +{ + // 'save screenshot...' pressed, offer to save maParentDialogBitmap + // as PNG image, use *.id file name as screenshot file name offering + const OString& rUIFileName = mrParentDialog.getUIFile(); + + + + + bool bBla = true; +} + +ControlDataEntry* ScreenshotAnnotationDlg::CheckHit(const basegfx::B2IPoint& rPosition) +{ + ControlDataEntry* pRetval = nullptr; + + for (auto aCandidate = maAllChildren.begin(); aCandidate != maAllChildren.end(); aCandidate++) + { + ControlDataEntry& rCandidate = *aCandidate; + + 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::PaintControlDataEntry( + const ControlDataEntry& rEntry, + const Color& rColor, + double fLineWidth, + double fTransparency) +{ + if (mpPicture && mpVirtualBufferDevice) + { + const basegfx::B2IRange& rRange = rEntry.getB2IRange(); + const basegfx::B2DPolygon aPolygon(basegfx::tools::createPolygonFromRect(basegfx::B2DRange(rRange))); + mpVirtualBufferDevice->SetLineColor(rColor); + + if (!mpVirtualBufferDevice->DrawPolyLineDirect( + aPolygon, + fLineWidth, + fTransparency, + basegfx::B2DLineJoin::Round)) + { + mpVirtualBufferDevice->DrawPolyLine( + aPolygon, + fLineWidth, + basegfx::B2DLineJoin::Round); + } + } +} + +Point ScreenshotAnnotationDlg::GetOffsetInPicture() const +{ + if (!mpPicture) + { + return Point(0, 0); + } + + const Size aPixelSizeTarget(mpPicture->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::RepaintPictureElement() +{ + if (mpPicture && mpVirtualBufferDevice) + { + // restore with start bitmap + mpVirtualBufferDevice->DrawBitmap(Point(0, 0), maParentDialogBitmap); + + // get various options - sorry, no SvtOptionsDrawinglayer in vcl + const Color aHilightColor(Application::GetSettings().GetStyleSettings().GetHighlightColor()); + const bool bIsAntiAliasing(true); + const double fTransparence(0.4); + const AntialiasingFlags nOldAA(mpVirtualBufferDevice->GetAntialiasing()); + + if (bIsAntiAliasing) + { + mpVirtualBufferDevice->SetAntialiasing(AntialiasingFlags::EnableB2dDraw); + } + + // paint selected + for (auto candidate = maSelected.begin(); candidate != maSelected.end(); candidate++) + { + PaintControlDataEntry(**candidate, Color(COL_LIGHTRED), 3.0); + } + + // paint hilight + if (mpHilighted) + { + PaintControlDataEntry(*mpHilighted, aHilightColor, 5.0, fTransparence); + } + + if (bIsAntiAliasing) + { + mpVirtualBufferDevice->SetAntialiasing(nOldAA); + } + + // copy new content to picture control + mpPicture->DrawOutDev( + GetOffsetInPicture(), + maParentDialogSize, + Point(0, 0), + maParentDialogSize, + *mpVirtualBufferDevice); + + // also set image to get repaints right, but trigger no repaint + mpPicture->SetImage(Image(mpVirtualBufferDevice->GetBitmap(Point(0, 0), mpVirtualBufferDevice->GetOutputSizePixel()))); + mpPicture->Validate(); + + // const Color aRandomColor(comphelper::rng::uniform_uint_distribution(0, 255), comphelper::rng::uniform_uint_distribution(0, 255), comphelper::rng::uniform_uint_distribution(0, 255)); + // mpPicture->SetImage(Image(mpVirtualBufferDevice->GetBitmap(Point(0, 0), mpVirtualBufferDevice->GetOutputSizePixel()))); + } +} + +IMPL_LINK_TYPED(ScreenshotAnnotationDlg, pictureFrameListener, VclWindowEvent&, rEvent, void) +{ + // event in picture frame + bool bRepaint(false); + + switch (rEvent.GetId()) + { + case VCLEVENT_WINDOW_MOUSEMOVE: + // case VCLEVENT_WINDOW_MOUSEBUTTONDOWN: + case VCLEVENT_WINDOW_MOUSEBUTTONUP: + { + MouseEvent* pMouseEvent = static_cast< MouseEvent* >(rEvent.GetData()); + + if (pMouseEvent) + { + switch (rEvent.GetId()) + { + case VCLEVENT_WINDOW_MOUSEMOVE: + { + if (mpPicture->IsMouseOver()) + { + const ControlDataEntry* pOldHit = mpHilighted; + const Point aOffset(GetOffsetInPicture()); + const basegfx::B2IPoint aMousePos( + pMouseEvent->GetPosPixel().X() - aOffset.X(), + pMouseEvent->GetPosPixel().Y() - aOffset.Y()); + const ControlDataEntry* pHit = CheckHit(aMousePos); + + if (pHit && pOldHit != pHit) + { + mpHilighted = const_cast< ControlDataEntry* >(pHit); + bRepaint = true; + } + } + else if (mpHilighted) + { + mpHilighted = nullptr; + bRepaint = true; + } + break; + } + case VCLEVENT_WINDOW_MOUSEBUTTONUP: + { + if (mpPicture->IsMouseOver() && mpHilighted) + { + if (maSelected.find(mpHilighted) != maSelected.end()) + { + maSelected.erase(mpHilighted); + } + else + { + maSelected.insert(mpHilighted); + } + + bRepaint = true; + } + break; + } + default: + { + break; + } + } + } + break; + } + default: + { + break; + } + } + + if (bRepaint) + { + RepaintPictureElement(); + } +} + +Button* isVisibleButtonWithText(vcl::Window* pCandidate) +{ + if (!pCandidate) + return nullptr; + + if (!pCandidate->IsVisible()) + return nullptr; + + if (pCandidate->GetText().isEmpty()) + return nullptr; + + return dynamic_cast<Button*>(pCandidate); +} + +// evtl. support for screenshot context menu +void VclContainer::Command(const CommandEvent& rCEvt) +{ + if (rCEvt.IsMouseEvent() && CommandEventId::ContextMenu == rCEvt.GetCommand()) + { + const bool bIsExperimentalMode(officecfg::Office::Common::Misc::ExperimentalMode::get()); + + if (bIsExperimentalMode) + { + bool bVisibleChildren(false); + vcl::Window* pChild(nullptr); + + for (pChild = GetWindow(GetWindowType::FirstChild); !bVisibleChildren && pChild; pChild = pChild->GetWindow(GetWindowType::Next)) + { + Button* pCandidate = isVisibleButtonWithText(pChild); + + if (nullptr == pCandidate) + continue; + + bVisibleChildren = true; + } + + if (bVisibleChildren) + { + static bool bAddButtonsToMenu(true); + static bool bAddScreenshotButtonToMenu(true); + + if (bAddButtonsToMenu || bAddScreenshotButtonToMenu) + { + const Point aMenuPos(rCEvt.GetMousePosPixel()); + ScopedVclPtrInstance<PopupMenu> aMenu; + sal_uInt16 nLocalID(1); + sal_uInt16 nScreenshotButtonID(0); + + if (bAddButtonsToMenu) + { + for (pChild = GetWindow(GetWindowType::FirstChild); pChild; pChild = pChild->GetWindow(GetWindowType::Next)) + { + Button* pCandidate = isVisibleButtonWithText(pChild); + + if (nullptr == pCandidate) + continue; + + aMenu->InsertItem( + nLocalID, + pChild->GetText(), + MenuItemBits::NONE); // MenuItemBits::CHECKABLE | MenuItemBits::RADIOCHECK); + aMenu->SetHelpText( + nLocalID, + pChild->GetHelpText()); + aMenu->SetHelpId( + nLocalID, + pChild->GetHelpId()); + aMenu->EnableItem( + nLocalID, + pChild->IsEnabled()); + nLocalID++; + } + } + + if (bAddScreenshotButtonToMenu) + { + if (nLocalID > 1) + { + aMenu->InsertSeparator(); + } + + aMenu->InsertItem( + nLocalID, + "Screenshot", + MenuItemBits::NONE); // MenuItemBits::CHECKABLE | MenuItemBits::RADIOCHECK); + aMenu->SetHelpText( + nLocalID, + "Go into interactive screenshot annotation mode"); + aMenu->SetHelpId( + nLocalID, + "InteractiveScreenshotMode"); + aMenu->EnableItem( + nLocalID, + true); + nScreenshotButtonID = nLocalID; + } + + const sal_uInt16 nId(aMenu->Execute(this, aMenuPos)); + + // 0 == no selection (so not usable as ID) + if (0 != nId) + { + if (bAddButtonsToMenu && nId < nLocalID) + { + nLocalID = 1; + + for (pChild = GetWindow(GetWindowType::FirstChild); pChild; pChild = pChild->GetWindow(GetWindowType::Next)) + { + Button* pCandidate = isVisibleButtonWithText(pChild); + + if (nullptr == pCandidate) + continue; + + if (nLocalID++ == nId) + { + // pCandidate is the selected button, trigger it + pCandidate->Click(); + break; + } + } + } + + if (bAddScreenshotButtonToMenu && nId == nScreenshotButtonID) + { + // screenshot was selected, access parent dialog (needed for + // screenshot and other data access) + Dialog* pParentDialog = GetParentDialog(); + + if (pParentDialog) + { + // open annotation work dialog + VclPtr<ScreenshotAnnotationDlg> pDlg = VclPtr<ScreenshotAnnotationDlg>::Create( + Application::GetDefDialogParent(), + *pParentDialog); + + if (pDlg && pDlg->Execute() == RET_OK) + { + + + + + + bool bBla2 = true; + } + } + } + } + + // consume event when: + // - CommandEventId::ContextMenu + // - bIsExperimentalMode + // - bVisibleChildren + return; + } + } + } + } + + Window::Command(rCEvt); +} + +////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// + void VclBox::accumulateMaxes(const Size &rChildSize, Size &rSize) const { long nSecondaryChildDimension = getSecondaryDimension(rChildSize); diff --git a/vcl/uiconfig/ui/screenshotannotationdialog.ui b/vcl/uiconfig/ui/screenshotannotationdialog.ui new file mode 100644 index 000000000000..6ab343679118 --- /dev/null +++ b/vcl/uiconfig/ui/screenshotannotationdialog.ui @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.16.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkDialog" id="ScreenshotAnnotationDialog"> + <property name="can_focus">False</property> + <property name="border_width">6</property> + <property name="title" translatable="yes">Interactive Screenshot Annotation</property> + <property name="modal">True</property> + <property name="default_width">600</property> + <property name="default_height">460</property> + <property name="type_hint">normal</property> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkImage" id="picture"> + <property name="name">picture</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="stock">gtk-missing-image</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area1"> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="ok"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="cancel"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="has_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="save"> + <property name="label" translatable="yes">Save Screenshot...</property> + <property name="name">save</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + <property name="secondary">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkTextView" id="text"> + <property name="name">text</property> + <property name="height_request">80</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> +</interface> |