The rounded corners look really dumb at this size.
[wxWidgets.git] / src / generic / richtooltipg.cpp
1 ///////////////////////////////////////////////////////////////////////////////
2 // Name: src/generic/richtooltipg.cpp
3 // Purpose: Implementation of wxRichToolTip.
4 // Author: Vadim Zeitlin
5 // Created: 2011-10-07
6 // Copyright: (c) 2011 Vadim Zeitlin <vadim@wxwidgets.org>
7 // Licence: wxWindows licence
8 ///////////////////////////////////////////////////////////////////////////////
9
10 // ============================================================================
11 // declarations
12 // ============================================================================
13
14 // ----------------------------------------------------------------------------
15 // headers
16 // ----------------------------------------------------------------------------
17
18 // for compilers that support precompilation, includes "wx.h".
19 #include "wx/wxprec.h"
20
21 #ifdef __BORLANDC__
22 #pragma hdrstop
23 #endif
24
25 #if wxUSE_RICHTOOLTIP
26
27 #ifndef WX_PRECOMP
28 #include "wx/dcmemory.h"
29 #include "wx/icon.h"
30 #include "wx/region.h"
31 #include "wx/settings.h"
32 #include "wx/sizer.h"
33 #include "wx/statbmp.h"
34 #include "wx/stattext.h"
35 #include "wx/timer.h"
36 #include "wx/utils.h"
37 #endif // WX_PRECOMP
38
39 #include "wx/private/richtooltip.h"
40 #include "wx/generic/private/richtooltip.h"
41
42 #include "wx/artprov.h"
43 #include "wx/custombgwin.h"
44 #include "wx/display.h"
45 #include "wx/graphics.h"
46 #include "wx/popupwin.h"
47 #include "wx/textwrapper.h"
48
49 #ifdef __WXMSW__
50 #include "wx/msw/uxtheme.h"
51
52 static const int TTP_BALLOONTITLE = 4;
53
54 static const int TMT_TEXTCOLOR = 3803;
55 static const int TMT_GRADIENTCOLOR1 = 3810;
56 static const int TMT_GRADIENTCOLOR2 = 3811;
57 #endif
58
59 // ----------------------------------------------------------------------------
60 // wxRichToolTipPopup: the popup window used by wxRichToolTip.
61 // ----------------------------------------------------------------------------
62
63 class wxRichToolTipPopup :
64 public wxCustomBackgroundWindow<wxPopupTransientWindow>
65 {
66 public:
67 wxRichToolTipPopup(wxWindow* parent,
68 const wxString& title,
69 const wxString& message,
70 const wxIcon& icon,
71 wxTipKind tipKind,
72 const wxFont& titleFont_) :
73 m_timer(this)
74 {
75 Create(parent, wxFRAME_SHAPED);
76
77
78 wxBoxSizer* const sizerTitle = new wxBoxSizer(wxHORIZONTAL);
79 if ( icon.IsOk() )
80 {
81 sizerTitle->Add(new wxStaticBitmap(this, wxID_ANY, icon),
82 wxSizerFlags().Centre().Border(wxRIGHT));
83 }
84 //else: Simply don't show any icon.
85
86 wxStaticText* const labelTitle = new wxStaticText(this, wxID_ANY, "");
87 labelTitle->SetLabelText(title);
88
89 wxFont titleFont(titleFont_);
90 if ( !titleFont.IsOk() )
91 {
92 // Determine the appropriate title font for the current platform.
93 titleFont = labelTitle->GetFont();
94
95 #ifdef __WXMSW__
96 // When using themes MSW tooltips use larger bluish version of the
97 // normal font.
98 wxUxThemeEngine* const theme = GetTooltipTheme();
99 if ( theme )
100 {
101 titleFont.MakeLarger();
102
103 COLORREF c;
104 if ( FAILED(theme->GetThemeColor
105 (
106 wxUxThemeHandle(parent, L"TOOLTIP"),
107 TTP_BALLOONTITLE,
108 0,
109 TMT_TEXTCOLOR,
110 &c
111 )) )
112 {
113 // Use the standard value of this colour as fallback.
114 c = 0x993300;
115 }
116
117 labelTitle->SetForegroundColour(wxRGBToColour(c));
118 }
119 else
120 #endif // __WXMSW__
121 {
122 // Everything else, including "classic" MSW look uses just the
123 // bold version of the base font.
124 titleFont.MakeBold();
125 }
126 }
127
128 labelTitle->SetFont(titleFont);
129 sizerTitle->Add(labelTitle, wxSizerFlags().Centre());
130
131 wxBoxSizer* const sizerTop = new wxBoxSizer(wxVERTICAL);
132 sizerTop->Add(sizerTitle,
133 wxSizerFlags().DoubleBorder(wxLEFT|wxRIGHT|wxTOP));
134
135 // Use a spacer as we don't want to have a double border between the
136 // elements, just a simple one will do.
137 sizerTop->AddSpacer(wxSizerFlags::GetDefaultBorder());
138
139 wxTextSizerWrapper wrapper(this);
140 wxSizer* sizerText = wrapper.CreateSizer(message, -1 /* No wrapping */);
141
142 #ifdef __WXMSW__
143 if ( icon.IsOk() && GetTooltipTheme() )
144 {
145 // Themed tooltips under MSW align the text with the title, not
146 // with the icon, so use a helper horizontal sizer in this case.
147 wxBoxSizer* const sizerTextIndent = new wxBoxSizer(wxHORIZONTAL);
148 sizerTextIndent->AddSpacer(icon.GetWidth());
149 sizerTextIndent->Add(sizerText,
150 wxSizerFlags().Border(wxLEFT).Centre());
151
152 sizerText = sizerTextIndent;
153 }
154 #endif // !__WXMSW__
155 sizerTop->Add(sizerText,
156 wxSizerFlags().DoubleBorder(wxLEFT|wxRIGHT|wxBOTTOM)
157 .Centre());
158
159 SetSizer(sizerTop);
160
161 const int offsetY = SetTipShapeAndSize(tipKind, GetBestSize());
162 if ( offsetY > 0 )
163 {
164 // Offset our contents by the tip height to make it appear in the
165 // main rectangle.
166 sizerTop->PrependSpacer(offsetY);
167 }
168
169 Layout();
170 }
171
172 void SetBackgroundColours(wxColour colStart, wxColour colEnd)
173 {
174 if ( !colStart.IsOk() )
175 {
176 // Determine the best colour(s) to use on our own.
177 #ifdef __WXMSW__
178 wxUxThemeEngine* const theme = GetTooltipTheme();
179 if ( theme )
180 {
181 wxUxThemeHandle hTheme(GetParent(), L"TOOLTIP");
182
183 COLORREF c1, c2;
184 if ( FAILED(theme->GetThemeColor
185 (
186 hTheme,
187 TTP_BALLOONTITLE,
188 0,
189 TMT_GRADIENTCOLOR1,
190 &c1
191 )) ||
192 FAILED(theme->GetThemeColor
193 (
194 hTheme,
195 TTP_BALLOONTITLE,
196 0,
197 TMT_GRADIENTCOLOR2,
198 &c2
199 )) )
200 {
201 c1 = 0xffffff;
202 c2 = 0xf0e5e4;
203 }
204
205 colStart = wxRGBToColour(c1);
206 colEnd = wxRGBToColour(c2);
207 }
208 else
209 #endif // __WXMSW__
210 {
211 colStart = wxSystemSettings::GetColour(wxSYS_COLOUR_INFOBK);
212 }
213 }
214
215 if ( colEnd.IsOk() )
216 {
217 // Use gradient-filled background bitmap.
218 const wxSize size = GetClientSize();
219 wxBitmap bmp(size);
220 {
221 wxMemoryDC dc(bmp);
222 dc.Clear();
223 dc.GradientFillLinear(size, colStart, colEnd, wxDOWN);
224 }
225
226 SetBackgroundBitmap(bmp);
227 }
228 else // Use solid colour.
229 {
230 SetBackgroundColour(colStart);
231 }
232 }
233
234 void SetPosition(const wxRect* rect)
235 {
236 wxPoint pos;
237
238 if ( !rect || rect->IsEmpty() )
239 pos = GetTipPoint();
240 else
241 pos = GetParent()->ClientToScreen( wxPoint( rect->x + rect->width / 2, rect->y + rect->height / 2 ) );
242
243 // We want our anchor point to coincide with this position so offset
244 // the position of the top left corner passed to Move() accordingly.
245 pos -= m_anchorPos;
246
247 Move(pos, wxSIZE_NO_ADJUSTMENTS);
248 }
249
250 void DoShow()
251 {
252 Popup();
253 }
254
255 void SetTimeoutAndShow(unsigned timeout, unsigned delay)
256 {
257 if ( !timeout && !delay )
258 {
259 DoShow();
260 return;
261 }
262
263 Connect(wxEVT_TIMER, wxTimerEventHandler(wxRichToolTipPopup::OnTimer));
264
265 m_timeout = timeout; // set for use in OnTimer if we have a delay
266 m_delayShow = delay != 0;
267
268 if ( !m_delayShow )
269 DoShow();
270
271 m_timer.Start((delay ? delay : timeout), true /* one shot */);
272 }
273
274 protected:
275 virtual void OnDismiss()
276 {
277 Destroy();
278 }
279
280 private:
281 #ifdef __WXMSW__
282 // Returns non-NULL theme only if we're using Win7-style tooltips.
283 static wxUxThemeEngine* GetTooltipTheme()
284 {
285 // Even themed applications under XP still use "classic" tooltips.
286 if ( wxGetWinVersion() <= wxWinVersion_XP )
287 return NULL;
288
289 return wxUxThemeEngine::GetIfActive();
290 }
291 #endif // __WXMSW__
292
293 // For now we just hard code the tip height, would be nice to do something
294 // smarter in the future.
295 static int GetTipHeight()
296 {
297 #ifdef __WXMSW__
298 if ( GetTooltipTheme() )
299 return 20;
300 #endif // __WXMSW__
301
302 return 15;
303 }
304
305 // Get the point to which our tip should point.
306 wxPoint GetTipPoint() const
307 {
308 // Currently we always use the middle of the window. It seems that MSW
309 // native tooltips use a different point but it's not really clear how
310 // do they determine it nor whether it's worth the trouble to emulate
311 // their behaviour.
312 const wxRect r = GetParent()->GetScreenRect();
313 return wxPoint(r.x + r.width/2, r.y + r.height/2);
314 }
315
316 // Choose the correct orientation depending on the window position.
317 //
318 // Also use the tip kind appropriate for the current environment. For MSW
319 // the right triangles are used and for Mac the equilateral ones as this is
320 // the prevailing kind under these systems. For everything else we go with
321 // right triangles as well but without any real rationale so this could be
322 // tweaked in the future.
323 wxTipKind GetBestTipKind() const
324 {
325 const wxPoint pos = GetTipPoint();
326
327 // Use GetFromWindow() and not GetFromPoint() here to try to get the
328 // correct display even if the tip point itself is not visible.
329 int dpy = wxDisplay::GetFromWindow(GetParent());
330 if ( dpy == wxNOT_FOUND )
331 dpy = 0; // What else can we do?
332
333 const wxRect rectDpy = wxDisplay(dpy).GetClientArea();
334
335 #ifdef __WXMAC__
336 return pos.y > rectDpy.height/2 ? wxTipKind_Bottom : wxTipKind_Top;
337 #else // !__WXMAC__
338 return pos.y > rectDpy.height/2
339 ? pos.x > rectDpy.width/2
340 ? wxTipKind_BottomRight
341 : wxTipKind_BottomLeft
342 : pos.x > rectDpy.width/2
343 ? wxTipKind_TopRight
344 : wxTipKind_TopLeft;
345 #endif // __WXMAC__/!__WXMAC__
346 }
347
348 // Set the size and shape of the tip window and returns the offset of its
349 // content area from the top (horizontal offset is always 0 currently).
350 int SetTipShapeAndSize(wxTipKind tipKind, const wxSize& contentSize)
351 {
352 #if wxUSE_GRAPHICS_CONTEXT
353 wxSize size = contentSize;
354
355 // The size is the vertical size and the offset is the distance from
356 // edge for asymmetric tips, currently hard-coded to be the same as the
357 // size.
358 const int tipSize = GetTipHeight();
359 const int tipOffset = tipSize;
360
361 // The horizontal position of the tip.
362 int x = -1;
363
364 // The vertical coordinates of the tip base and apex.
365 int yBase = -1,
366 yApex = -1;
367
368 // The offset of the content part of the window.
369 int dy = -1;
370
371 // Define symbolic names for the rectangle corners and mid-way points
372 // that we use below in an attempt to make the code more clear. Notice
373 // that these values must be consecutive as we iterate over them.
374 enum RectPoint
375 {
376 RectPoint_TopLeft,
377 RectPoint_Top,
378 RectPoint_TopRight,
379 RectPoint_Right,
380 RectPoint_BotRight,
381 RectPoint_Bot,
382 RectPoint_BotLeft,
383 RectPoint_Left,
384 RectPoint_Max
385 };
386
387 // The starting point for AddArcToPoint() calls below, we iterate over
388 // all RectPoints from it.
389 RectPoint pointStart = RectPoint_Max;
390
391
392 // Hard-coded radius of the round main rectangle corners.
393 const double RADIUS = 5;
394
395 // Create a path defining the shape of the tooltip window.
396 wxGraphicsPath
397 path = wxGraphicsRenderer::GetDefaultRenderer()->CreatePath();
398
399 if ( tipKind == wxTipKind_Auto )
400 tipKind = GetBestTipKind();
401
402 // Points defining the tip shape (in clockwise order as we must end at
403 // tipPoints[0] after drawing the rectangle outline in this order).
404 wxPoint2DDouble tipPoints[3];
405
406 switch ( tipKind )
407 {
408 case wxTipKind_Auto:
409 wxFAIL_MSG( "Impossible kind value" );
410 break;
411
412 case wxTipKind_TopLeft:
413 x = tipOffset;
414 yApex = 0;
415 yBase = tipSize;
416 dy = tipSize;
417
418 tipPoints[0] = wxPoint2DDouble(x, yBase);
419 tipPoints[1] = wxPoint2DDouble(x, yApex);
420 tipPoints[2] = wxPoint2DDouble(x + tipSize, yBase);
421
422 pointStart = RectPoint_TopRight;
423 break;
424
425 case wxTipKind_TopRight:
426 x = size.x - tipOffset;
427 yApex = 0;
428 yBase = tipSize;
429 dy = tipSize;
430
431 tipPoints[0] = wxPoint2DDouble(x - tipSize, yBase);
432 tipPoints[1] = wxPoint2DDouble(x, yApex);
433 tipPoints[2] = wxPoint2DDouble(x, yBase);
434
435 pointStart = RectPoint_TopRight;
436 break;
437
438 case wxTipKind_BottomLeft:
439 x = tipOffset;
440 yApex = size.y + tipSize;
441 yBase = size.y;
442 dy = 0;
443
444 tipPoints[0] = wxPoint2DDouble(x + tipSize, yBase);
445 tipPoints[1] = wxPoint2DDouble(x, yApex);
446 tipPoints[2] = wxPoint2DDouble(x, yBase);
447
448 pointStart = RectPoint_BotLeft;
449 break;
450
451 case wxTipKind_BottomRight:
452 x = size.x - tipOffset;
453 yApex = size.y + tipSize;
454 yBase = size.y;
455 dy = 0;
456
457 tipPoints[0] = wxPoint2DDouble(x, yBase);
458 tipPoints[1] = wxPoint2DDouble(x, yApex);
459 tipPoints[2] = wxPoint2DDouble(x - tipSize, yBase);
460
461 pointStart = RectPoint_BotLeft;
462 break;
463
464 case wxTipKind_Top:
465 x = size.x/2;
466 yApex = 0;
467 yBase = tipSize;
468 dy = tipSize;
469
470 {
471 // A half-side of an equilateral triangle is its altitude
472 // divided by sqrt(3) ~= 1.73.
473 const double halfside = tipSize/1.73;
474
475 tipPoints[0] = wxPoint2DDouble(x - halfside, yBase);
476 tipPoints[1] = wxPoint2DDouble(x, yApex);
477 tipPoints[2] = wxPoint2DDouble(x + halfside, yBase);
478 }
479
480 pointStart = RectPoint_TopRight;
481 break;
482
483 case wxTipKind_Bottom:
484 x = size.x/2;
485 yApex = size.y + tipSize;
486 yBase = size.y;
487 dy = 0;
488
489 {
490 const double halfside = tipSize/1.73;
491
492 tipPoints[0] = wxPoint2DDouble(x + halfside, yBase);
493 tipPoints[1] = wxPoint2DDouble(x, yApex);
494 tipPoints[2] = wxPoint2DDouble(x - halfside, yBase);
495 }
496
497 pointStart = RectPoint_BotLeft;
498 break;
499
500 case wxTipKind_None:
501 x = size.x/2;
502 dy = 0;
503
504 path.AddRoundedRectangle(0, 0, size.x, size.y, RADIUS);
505 break;
506 }
507
508 wxASSERT_MSG( dy != -1, wxS("Unknown tip kind?") );
509
510 size.y += tipSize;
511 SetSize(size);
512
513 if ( tipKind != wxTipKind_None )
514 {
515 path.MoveToPoint(tipPoints[0]);
516 path.AddLineToPoint(tipPoints[1]);
517 path.AddLineToPoint(tipPoints[2]);
518
519 const double xLeft = 0.;
520 const double xMid = size.x/2.;
521 const double xRight = size.x;
522
523 const double yTop = dy;
524 const double yMid = (dy + size.y)/2.;
525 const double yBot = dy + contentSize.y;
526
527 wxPoint2DDouble rectPoints[RectPoint_Max];
528 rectPoints[RectPoint_TopLeft] = wxPoint2DDouble(xLeft, yTop);
529 rectPoints[RectPoint_Top] = wxPoint2DDouble(xMid, yTop);
530 rectPoints[RectPoint_TopRight] = wxPoint2DDouble(xRight, yTop);
531 rectPoints[RectPoint_Right] = wxPoint2DDouble(xRight, yMid);
532 rectPoints[RectPoint_BotRight] = wxPoint2DDouble(xRight, yBot);
533 rectPoints[RectPoint_Bot] = wxPoint2DDouble(xMid, yBot);
534 rectPoints[RectPoint_BotLeft] = wxPoint2DDouble(xLeft, yBot);
535 rectPoints[RectPoint_Left] = wxPoint2DDouble(xLeft, yMid);
536
537 // Iterate over all rectangle rectPoints for the first 3 corners.
538 unsigned n = pointStart;
539 for ( unsigned corner = 0; corner < 3; corner++ )
540 {
541 const wxPoint2DDouble& pt1 = rectPoints[n];
542
543 n = (n + 1) % RectPoint_Max;
544
545 const wxPoint2DDouble& pt2 = rectPoints[n];
546
547 path.AddArcToPoint(pt1.m_x, pt1.m_y, pt2.m_x, pt2.m_y, RADIUS);
548
549 n = (n + 1) % RectPoint_Max;
550 }
551
552 // Last one wraps to the first point of the tip.
553 const wxPoint2DDouble& pt1 = rectPoints[n];
554 const wxPoint2DDouble& pt2 = tipPoints[0];
555
556 path.AddArcToPoint(pt1.m_x, pt1.m_y, pt2.m_x, pt2.m_y, RADIUS);
557
558 path.CloseSubpath();
559 }
560
561 SetShape(path);
562 #else // !wxUSE_GRAPHICS_CONTEXT
563 wxUnusedVar(tipKind);
564
565 int x = contentSize.x/2,
566 yApex = 0,
567 dy = 0;
568
569 SetSize(contentSize);
570 #endif // wxUSE_GRAPHICS_CONTEXT/!wxUSE_GRAPHICS_CONTEXT
571
572 m_anchorPos.x = x;
573 m_anchorPos.y = yApex;
574
575 return dy;
576 }
577
578 // Timer event handler hides the tooltip when the timeout expires.
579 void OnTimer(wxTimerEvent& WXUNUSED(event))
580 {
581 if ( !m_delayShow )
582 {
583 // Doing "Notify" here ensures that our OnDismiss() is called and so we
584 // also Destroy() ourselves. We could use Dismiss() and call Destroy()
585 // explicitly from here as well.
586 DismissAndNotify();
587
588 return;
589 }
590
591 m_delayShow = false;
592
593 if ( m_timeout )
594 m_timer.Start(m_timeout, true);
595
596 DoShow();
597 }
598
599
600 // The anchor point offset if we show a tip or the middle of the top side
601 // otherwise.
602 wxPoint m_anchorPos;
603
604 // The timer counting down the time until we're hidden.
605 wxTimer m_timer;
606
607 // We will need to accesss the timeout period when delaying showing tooltip.
608 int m_timeout;
609
610 // If true, delay showing the tooltip.
611 bool m_delayShow;
612
613 wxDECLARE_NO_COPY_CLASS(wxRichToolTipPopup);
614 };
615
616 // ----------------------------------------------------------------------------
617 // wxRichToolTipGenericImpl: generic implementation of wxRichToolTip.
618 // ----------------------------------------------------------------------------
619
620 void
621 wxRichToolTipGenericImpl::SetBackgroundColour(const wxColour& col,
622 const wxColour& colEnd)
623 {
624 m_colStart = col;
625 m_colEnd = colEnd;
626 }
627
628 void wxRichToolTipGenericImpl::SetCustomIcon(const wxIcon& icon)
629 {
630 m_icon = icon;
631 }
632
633 void wxRichToolTipGenericImpl::SetStandardIcon(int icon)
634 {
635 switch ( icon & wxICON_MASK )
636 {
637 case wxICON_WARNING:
638 case wxICON_ERROR:
639 case wxICON_INFORMATION:
640 // Although we don't use this icon in a list, we need a smallish
641 // icon here and not an icon of a typical message box size so use
642 // wxART_LIST to get it.
643 m_icon = wxArtProvider::GetIcon
644 (
645 wxArtProvider::GetMessageBoxIconId(icon),
646 wxART_LIST
647 );
648 break;
649
650 case wxICON_QUESTION:
651 wxFAIL_MSG("Question icon doesn't make sense for a tooltip");
652 break;
653
654 case wxICON_NONE:
655 m_icon = wxNullIcon;
656 break;
657 }
658 }
659
660 void wxRichToolTipGenericImpl::SetTimeout(unsigned millisecondsTimeout,
661 unsigned millisecondsDelay)
662 {
663 m_delay = millisecondsDelay;
664 m_timeout = millisecondsTimeout;
665 }
666
667 void wxRichToolTipGenericImpl::SetTipKind(wxTipKind tipKind)
668 {
669 m_tipKind = tipKind;
670 }
671
672 void wxRichToolTipGenericImpl::SetTitleFont(const wxFont& font)
673 {
674 m_titleFont = font;
675 }
676
677 void wxRichToolTipGenericImpl::ShowFor(wxWindow* win, const wxRect* rect)
678 {
679 // Set the focus to the window the tooltip refers to to make it look active.
680 win->SetFocus();
681
682 wxRichToolTipPopup* const popup = new wxRichToolTipPopup
683 (
684 win,
685 m_title,
686 m_message,
687 m_icon,
688 m_tipKind,
689 m_titleFont
690 );
691
692 popup->SetBackgroundColours(m_colStart, m_colEnd);
693
694 popup->SetPosition(rect);
695 // show or start the timer to delay showing the popup
696 popup->SetTimeoutAndShow( m_timeout, m_delay );
697 }
698
699 // Currently only wxMSW provides a native implementation.
700 #ifndef __WXMSW__
701
702 /* static */
703 wxRichToolTipImpl*
704 wxRichToolTipImpl::Create(const wxString& title, const wxString& message)
705 {
706 return new wxRichToolTipGenericImpl(title, message);
707 }
708
709 #endif // !__WXMSW__
710
711 #endif // wxUSE_RICHTOOLTIP