1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
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 
10 // Design proposal: https://wiki.documentfoundation.org/Design/Whiteboards/Comments_Ruler_Control
11 
12 #include <swruler.hxx>
13 
14 #include <viewsh.hxx>
15 #include <edtwin.hxx>
16 #include <PostItMgr.hxx>
17 #include <view.hxx>
18 #include <cmdid.h>
19 #include <sfx2/request.hxx>
20 #include <tools/UnitConversion.hxx>
21 #include <vcl/commandevent.hxx>
22 #include <vcl/event.hxx>
23 #include <vcl/window.hxx>
24 #include <vcl/settings.hxx>
25 #include <tools/json_writer.hxx>
26 #include <strings.hrc>
27 #include <comphelper/lok.hxx>
28 #include <LibreOfficeKit/LibreOfficeKitEnums.h>
29 
30 #define CONTROL_BORDER_WIDTH 1
31 
32 namespace
33 {
34 /**
35  * Draw a little arrow / triangle with different directions
36  *
37  * \param nX left coordinate of arrow square
38  * \param nY top coordinate of arrow square
39  * \param nSize size of the long triangle side / arrow square
40  * \param Color arrow color
41  * \param bCollapsed if the arrow should display the collapsed state
42  */
ImplDrawArrow(vcl::RenderContext & rRenderContext,tools::Long nX,tools::Long nY,tools::Long nSize,const Color & rColor,bool bCollapsed)43 void ImplDrawArrow(vcl::RenderContext& rRenderContext, tools::Long nX, tools::Long nY,
44                    tools::Long nSize, const Color& rColor, bool bCollapsed)
45 {
46     tools::Polygon aTrianglePolygon(4);
47 
48     if (bCollapsed)
49     {
50         if (AllSettings::GetLayoutRTL()) // <
51         {
52             aTrianglePolygon.SetPoint({ nX + nSize / 2, nY }, 0);
53             aTrianglePolygon.SetPoint({ nX + nSize / 2, nY + nSize }, 1);
54             aTrianglePolygon.SetPoint({ nX, nY + nSize / 2 }, 2);
55             aTrianglePolygon.SetPoint({ nX + nSize / 2, nY }, 3);
56         }
57         else // >
58         {
59             aTrianglePolygon.SetPoint({ nX, nY }, 0);
60             aTrianglePolygon.SetPoint({ nX + nSize / 2, nY + nSize / 2 }, 1);
61             aTrianglePolygon.SetPoint({ nX, nY + nSize }, 2);
62             aTrianglePolygon.SetPoint({ nX, nY }, 3);
63         }
64     }
65     else // v
66     {
67         aTrianglePolygon.SetPoint({ nX, nY + nSize / 2 }, 0);
68         aTrianglePolygon.SetPoint({ nX + nSize, nY + nSize / 2 }, 1);
69         aTrianglePolygon.SetPoint({ nX + nSize / 2, nY + nSize }, 2);
70         aTrianglePolygon.SetPoint({ nX, nY + nSize / 2 }, 3);
71     }
72 
73     rRenderContext.SetLineColor();
74     rRenderContext.SetFillColor(rColor);
75     rRenderContext.DrawPolygon(aTrianglePolygon);
76 }
77 }
78 
79 // Constructor
SwCommentRuler(SwViewShell * pViewSh,vcl::Window * pParent,SwEditWin * pWin,SvxRulerSupportFlags nRulerFlags,SfxBindings & rBindings,WinBits nWinStyle)80 SwCommentRuler::SwCommentRuler(SwViewShell* pViewSh, vcl::Window* pParent, SwEditWin* pWin,
81                                SvxRulerSupportFlags nRulerFlags, SfxBindings& rBindings,
82                                WinBits nWinStyle)
83     : SvxRuler(pParent, pWin, nRulerFlags, rBindings, nWinStyle | WB_HSCROLL)
84     , mpViewShell(pViewSh)
85     , mpSwWin(pWin)
86     , mbIsHighlighted(false)
87     , mnFadeRate(0)
88     , maVirDev(VclPtr<VirtualDevice>::Create(*GetOutDev()))
89 {
90     // Set fading timeout: 5 x 40ms = 200ms
91     maFadeTimer.SetTimeout(40);
92     maFadeTimer.SetInvokeHandler(LINK(this, SwCommentRuler, FadeHandler));
93     maFadeTimer.SetDebugName("sw::SwCommentRuler maFadeTimer");
94 
95     // we have a little bit more space, as we don't draw ruler ticks
96     vcl::Font aFont(maVirDev->GetFont());
97     aFont.SetFontHeight(aFont.GetFontHeight() + 1);
98     maVirDev->SetFont(aFont);
99 }
100 
~SwCommentRuler()101 SwCommentRuler::~SwCommentRuler() { disposeOnce(); }
102 
dispose()103 void SwCommentRuler::dispose()
104 {
105     mpSwWin.clear();
106     SvxRuler::dispose();
107 }
108 
Paint(vcl::RenderContext & rRenderContext,const tools::Rectangle & rRect)109 void SwCommentRuler::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect)
110 {
111     if (comphelper::LibreOfficeKit::isActive())
112         return; // no need to waste time on startup
113 
114     SvxRuler::Paint(rRenderContext, rRect);
115 
116     // Don't draw if there is not any note
117     if (mpViewShell->GetPostItMgr() && mpViewShell->GetPostItMgr()->HasNotes())
118         DrawCommentControl(rRenderContext);
119 }
120 
DrawCommentControl(vcl::RenderContext & rRenderContext)121 void SwCommentRuler::DrawCommentControl(vcl::RenderContext& rRenderContext)
122 {
123     const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings();
124     const bool bIsCollapsed = !mpViewShell->GetPostItMgr()->ShowNotes();
125     const tools::Rectangle aControlRect = GetCommentControlRegion();
126 
127     maVirDev->SetOutputSizePixel(aControlRect.GetSize());
128 
129     // set colors
130     if (!bIsCollapsed)
131     {
132         if (mbIsHighlighted)
133             maVirDev->SetFillColor(
134                 GetFadedColor(rStyleSettings.GetHighlightColor(), rStyleSettings.GetDialogColor()));
135         else
136             maVirDev->SetFillColor(rStyleSettings.GetDialogColor());
137         maVirDev->SetLineColor(rStyleSettings.GetShadowColor());
138     }
139     else
140     {
141         if (mbIsHighlighted)
142             maVirDev->SetFillColor(GetFadedColor(rStyleSettings.GetHighlightColor(),
143                                                  rStyleSettings.GetWorkspaceColor()));
144         else
145             maVirDev->SetFillColor(rStyleSettings.GetWorkspaceColor());
146         maVirDev->SetLineColor();
147     }
148     Color aTextColor = GetFadedColor(rStyleSettings.GetHighlightTextColor(),
149                                      rStyleSettings.GetButtonTextColor());
150     maVirDev->SetTextColor(aTextColor);
151 
152     // calculate label and arrow positions
153     const OUString aLabel = SwResId(STR_COMMENTS_LABEL);
154     const tools::Long nTriangleSize = maVirDev->GetTextHeight() / 2 + 1;
155     const tools::Long nTrianglePad = maVirDev->GetTextHeight() / 4;
156 
157     Point aLabelPos(0, (aControlRect.GetHeight() - maVirDev->GetTextHeight()) / 2);
158     Point aArrowPos(0, (aControlRect.GetHeight() - nTriangleSize) / 2);
159 
160     if (!AllSettings::GetLayoutRTL()) // | > Comments |
161     {
162         aArrowPos.setX(nTrianglePad);
163         aLabelPos.setX(aArrowPos.X() + nTriangleSize + nTrianglePad);
164     }
165     else // RTL => | Comments < |
166     {
167         const tools::Long nLabelWidth = maVirDev->GetTextWidth(aLabel);
168         if (!bIsCollapsed)
169         {
170             aArrowPos.setX(aControlRect.GetWidth() - 1 - nTrianglePad - CONTROL_BORDER_WIDTH
171                            - nTriangleSize);
172             aLabelPos.setX(aArrowPos.X() - nTrianglePad - nLabelWidth);
173         }
174         else
175         {
176             // if comments are collapsed, left align the text, because otherwise it's very likely to be invisible
177             aArrowPos.setX(nLabelWidth + nTrianglePad + nTriangleSize);
178             aLabelPos.setX(aArrowPos.X() - nTrianglePad - nLabelWidth);
179         }
180     }
181 
182     // draw control
183     maVirDev->DrawRect(tools::Rectangle(Point(), aControlRect.GetSize()));
184     maVirDev->DrawText(aLabelPos, aLabel);
185     ImplDrawArrow(*maVirDev, aArrowPos.X(), aArrowPos.Y(), nTriangleSize, aTextColor, bIsCollapsed);
186     rRenderContext.DrawOutDev(aControlRect.TopLeft(), aControlRect.GetSize(), Point(),
187                               aControlRect.GetSize(), *maVirDev);
188 }
189 
190 // Just accept double-click outside comment control
Command(const CommandEvent & rCEvt)191 void SwCommentRuler::Command(const CommandEvent& rCEvt)
192 {
193     Point aMousePos = rCEvt.GetMousePosPixel();
194     // Ignore command request if it is inside Comment Control
195     if (!mpViewShell->GetPostItMgr() || !mpViewShell->GetPostItMgr()->HasNotes()
196         || !GetCommentControlRegion().IsInside(aMousePos))
197         SvxRuler::Command(rCEvt);
198 }
199 
MouseMove(const MouseEvent & rMEvt)200 void SwCommentRuler::MouseMove(const MouseEvent& rMEvt)
201 {
202     SvxRuler::MouseMove(rMEvt);
203     if (!mpViewShell->GetPostItMgr() || !mpViewShell->GetPostItMgr()->HasNotes())
204         return;
205 
206     UpdateCommentHelpText();
207 
208     Point aMousePos = rMEvt.GetPosPixel();
209     bool bWasHighlighted = mbIsHighlighted;
210     mbIsHighlighted = GetCommentControlRegion().IsInside(aMousePos);
211     if (mbIsHighlighted != bWasHighlighted)
212         // Do start fading
213         maFadeTimer.Start();
214 }
215 
MouseButtonDown(const MouseEvent & rMEvt)216 void SwCommentRuler::MouseButtonDown(const MouseEvent& rMEvt)
217 {
218     Point aMousePos = rMEvt.GetPosPixel();
219     if (!rMEvt.IsLeft() || IsTracking() || !GetCommentControlRegion().IsInside(aMousePos))
220     {
221         SvxRuler::MouseButtonDown(rMEvt);
222         return;
223     }
224 
225     // Toggle notes visibility
226     SwView& rView = mpSwWin->GetView();
227     SfxRequest aRequest(rView.GetViewFrame(), SID_TOGGLE_NOTES);
228     rView.ExecViewOptions(aRequest);
229 
230     // It is inside comment control, so update help text
231     UpdateCommentHelpText();
232 
233     Invalidate();
234 }
235 
CreateJsonNotification(tools::JsonWriter & rJsonWriter)236 void SwCommentRuler::CreateJsonNotification(tools::JsonWriter& rJsonWriter)
237 {
238     // Note that GetMargin1(), GetMargin2(), GetNullOffset(), and GetPageOffset() return values in
239     // pixels. Not twips. So "converting" the returned values with convertTwipToMm100() is quite
240     // wrong. (Also, even if the return values actually were in twips, it is questionable why we
241     // would want to pass them in mm100, as all other length values in the LOKit protocol apparently
242     // are in twips.)
243 
244     // Anyway, as the consuming code in Online mostly seems to work anyway, it is likely that it
245     // would work as well even if the values in pixels were passed without a bogus "conversion" to
246     // mm100. But let's keep this as is for now.
247 
248     // Also note that in desktop LibreOffice, these pixel values for the ruler of course change as
249     // one changes the zoom level. (Can be seen if one temporarily modifies the NotifyKit() function
250     // below to call this CreateJsonNotification() function and print its result in all cases even
251     // without LibreOfficeKit::isActive().) But in both web-based Online and in the iOS app, the
252     // zoom level from the point of view of this code here apparently does not change even if one
253     // zooms from the Online code's point of view.
254     rJsonWriter.put("margin1", convertTwipToMm100(GetMargin1()));
255     rJsonWriter.put("margin2", convertTwipToMm100(GetMargin2()));
256     rJsonWriter.put("leftOffset", convertTwipToMm100(GetNullOffset()));
257     rJsonWriter.put("pageOffset", convertTwipToMm100(GetPageOffset()));
258 
259     // GetPageWidth() on the other hand does return a value in twips.
260     // So here convertTwipToMm100() really does produce actual mm100. Fun.
261     rJsonWriter.put("pageWidth", convertTwipToMm100(GetPageWidth()));
262 
263     {
264         auto tabsNode = rJsonWriter.startNode("tabs");
265 
266         // The RulerTab array elements that GetTabs() returns have their nPos field in twips. So these
267         // too are actual mm100.
268         for (auto const& tab : GetTabs())
269         {
270             auto tabNode = rJsonWriter.startNode("");
271             rJsonWriter.put("position", convertTwipToMm100(tab.nPos));
272             rJsonWriter.put("style", tab.nStyle);
273         }
274     }
275 
276     RulerUnitData aUnitData = GetCurrentRulerUnit();
277     rJsonWriter.put("unit", aUnitData.aUnitStr);
278 }
279 
NotifyKit()280 void SwCommentRuler::NotifyKit()
281 {
282     if (!comphelper::LibreOfficeKit::isActive())
283         return;
284 
285     tools::JsonWriter aJsonWriter;
286     CreateJsonNotification(aJsonWriter);
287     char* pJsonData = aJsonWriter.extractData();
288     mpViewShell->GetSfxViewShell()->libreOfficeKitViewCallback(LOK_CALLBACK_RULER_UPDATE,
289                                                                pJsonData);
290     free(pJsonData);
291 }
292 
Update()293 void SwCommentRuler::Update()
294 {
295     tools::Rectangle aPreviousControlRect = GetCommentControlRegion();
296     SvxRuler::Update();
297     if (aPreviousControlRect != GetCommentControlRegion())
298         Invalidate();
299     NotifyKit();
300 }
301 
UpdateCommentHelpText()302 void SwCommentRuler::UpdateCommentHelpText()
303 {
304     const char* pTooltipResId;
305     if (mpViewShell->GetPostItMgr()->ShowNotes())
306         pTooltipResId = STR_HIDE_COMMENTS;
307     else
308         pTooltipResId = STR_SHOW_COMMENTS;
309     SetQuickHelpText(SwResId(pTooltipResId));
310 }
311 
312 // TODO Make Ruler return its central rectangle instead of margins.
GetCommentControlRegion()313 tools::Rectangle SwCommentRuler::GetCommentControlRegion()
314 {
315     SwPostItMgr* pPostItMgr = mpViewShell->GetPostItMgr();
316 
317     //rhbz#1006850 When the SwPostItMgr ctor is called from SwView::SwView it
318     //triggers an update of the uiview, but the result of the ctor hasn't been
319     //set into the mpViewShell yet, so GetPostItMgr is temporarily still NULL
320     if (!pPostItMgr)
321         return tools::Rectangle();
322 
323     const tools::ULong nSidebarWidth = pPostItMgr->GetSidebarWidth(true);
324 
325     //FIXME When the page width is larger then screen, the ruler is misplaced by one pixel
326     tools::Long nLeft = GetPageOffset();
327     if (GetTextRTL())
328         nLeft += GetBorderOffset() - nSidebarWidth;
329     else
330         nLeft += GetWinOffset() + mpSwWin->LogicToPixel(Size(GetPageWidth(), 0)).Width();
331 
332     // Ruler::ImplDraw uses RULER_OFF (value: 3px) as offset, and Ruler::ImplFormat adds one extra pixel
333     tools::Long nTop = 4;
334     // Somehow pPostItMgr->GetSidebarBorderWidth() returns border width already doubled
335     tools::Long nRight = nLeft + nSidebarWidth + pPostItMgr->GetSidebarBorderWidth(true);
336     tools::Long nBottom = nTop + GetRulerVirHeight() - 3;
337 
338     tools::Rectangle aRect(nLeft, nTop, nRight, nBottom);
339     return aRect;
340 }
341 
GetFadedColor(const Color & rHighColor,const Color & rLowColor)342 Color SwCommentRuler::GetFadedColor(const Color& rHighColor, const Color& rLowColor)
343 {
344     if (!maFadeTimer.IsActive())
345         return mbIsHighlighted ? rHighColor : rLowColor;
346 
347     Color aColor = rHighColor;
348     aColor.Merge(rLowColor, mnFadeRate * 255 / 100.0f);
349     return aColor;
350 }
351 
IMPL_LINK_NOARG(SwCommentRuler,FadeHandler,Timer *,void)352 IMPL_LINK_NOARG(SwCommentRuler, FadeHandler, Timer*, void)
353 {
354     const int nStep = 25;
355     if (mbIsHighlighted && mnFadeRate < 100)
356         mnFadeRate += nStep;
357     else if (!mbIsHighlighted && mnFadeRate > 0)
358         mnFadeRate -= nStep;
359     else
360         return;
361 
362     Invalidate();
363 
364     if (mnFadeRate != 0 && mnFadeRate != 100)
365         maFadeTimer.Start();
366 }
367 
368 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
369