1 ///////////////////////////////////////////////////////////////////////////////
2 // Name:        src/msw/tooltip.cpp
3 // Purpose:     wxToolTip class implementation for MSW
4 // Author:      Vadim Zeitlin
5 // Modified by:
6 // Created:     31.01.99
7 // RCS-ID:      $Id: tooltip.cpp 62542 2009-11-03 14:11:01Z VZ $
8 // Copyright:   (c) 1999 Vadim Zeitlin
9 // Licence:     wxWindows licence
10 ///////////////////////////////////////////////////////////////////////////////
11 
12 // ============================================================================
13 // declarations
14 // ============================================================================
15 
16 // ----------------------------------------------------------------------------
17 // headers
18 // ----------------------------------------------------------------------------
19 
20 #include "wx/wxprec.h"
21 
22 #ifdef __BORLANDC__
23     #pragma hdrstop
24 #endif
25 
26 #if wxUSE_TOOLTIPS
27 
28 #include "wx/tooltip.h"
29 
30 #ifndef WX_PRECOMP
31     #include "wx/msw/wrapcctl.h" // include <commctrl.h> "properly"
32     #include "wx/app.h"
33     #include "wx/control.h"
34     #include "wx/combobox.h"
35 #endif
36 
37 #include "wx/tokenzr.h"
38 #include "wx/msw/private.h"
39 
40 #ifndef TTTOOLINFO_V1_SIZE
41     #define TTTOOLINFO_V1_SIZE 0x28
42 #endif
43 
44 #ifndef TTF_TRANSPARENT
45     #define TTF_TRANSPARENT 0x0100
46 #endif
47 
48 // VZ: normally, the trick with subclassing the tooltip control and processing
49 //     TTM_WINDOWFROMPOINT should work but, somehow, it doesn't. I leave the
50 //     code here for now (but it's not compiled) in case we need it later.
51 //
52 //     For now I use an ugly workaround and process TTN_NEEDTEXT directly in
53 //     radio button wnd proc - fixing TTM_WINDOWFROMPOINT code would be nice
54 //     because it would then work for all controls, not only radioboxes but for
55 //     now I don't understand what's wrong with it...
56 #define wxUSE_TTM_WINDOWFROMPOINT   0
57 
58 // ----------------------------------------------------------------------------
59 // global variables
60 // ----------------------------------------------------------------------------
61 
62 // the tooltip parent window
63 WXHWND wxToolTip::ms_hwndTT = (WXHWND)NULL;
64 
65 #if wxUSE_TTM_WINDOWFROMPOINT
66 
67 // the tooltip window proc
68 static WNDPROC gs_wndprocToolTip = (WNDPROC)NULL;
69 
70 #endif // wxUSE_TTM_WINDOWFROMPOINT
71 
72 // ----------------------------------------------------------------------------
73 // private classes
74 // ----------------------------------------------------------------------------
75 
76 // a wrapper around TOOLINFO Win32 structure
77 #ifdef __VISUALC__
78     #pragma warning( disable : 4097 ) // we inherit from a typedef - so what?
79 #endif
80 
81 class wxToolInfo : public TOOLINFO
82 {
83 public:
wxToolInfo(HWND hwndOwner)84     wxToolInfo(HWND hwndOwner)
85     {
86         // initialize all members
87         ::ZeroMemory(this, sizeof(TOOLINFO));
88 
89         // the structure TOOLINFO has been extended with a 4 byte field in
90         // version 4.70 of comctl32.dll and another one in 5.01 but we don't
91         // use these extended fields so use the old struct size to ensure that
92         // the tooltips work on old (Windows 95) systems too
93         cbSize = TTTOOLINFO_V1_SIZE;
94 
95         hwnd = hwndOwner;
96         uFlags = TTF_IDISHWND;
97 
98         // we use TTF_TRANSPARENT to fix a problem which arises at least with
99         // the text controls but may presumably happen with other controls
100         // which display the tooltip at mouse position: it can start flashing
101         // then as the control gets "focus lost" events and dismisses the
102         // tooltip which then reappears because mouse remains hovering over the
103         // control, see SF patch 1821229
104         if ( wxApp::GetComCtl32Version() >= 470 )
105         {
106             uFlags |= TTF_TRANSPARENT;
107         }
108 
109         uId = (UINT)hwndOwner;
110     }
111 };
112 
113 #ifdef __VISUALC__
114     #pragma warning( default : 4097 )
115 #endif
116 
117 // ----------------------------------------------------------------------------
118 // private functions
119 // ----------------------------------------------------------------------------
120 
121 // send a message to the tooltip control if it exists
122 //
123 // NB: wParam is always 0 for the TTM_XXX messages we use
SendTooltipMessage(WXHWND hwnd,UINT msg,void * lParam)124 static inline LRESULT SendTooltipMessage(WXHWND hwnd, UINT msg, void *lParam)
125 {
126     return hwnd ? ::SendMessage((HWND)hwnd, msg, 0, (LPARAM)lParam) : 0;
127 }
128 
129 // send a message to all existing tooltip controls
130 static inline void
SendTooltipMessageToAll(WXHWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)131 SendTooltipMessageToAll(WXHWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
132 {
133     if ( hwnd )
134         ::SendMessage((HWND)hwnd, msg, wParam, lParam);
135 }
136 
137 // ============================================================================
138 // implementation
139 // ============================================================================
140 
141 #if wxUSE_TTM_WINDOWFROMPOINT
142 
143 // ----------------------------------------------------------------------------
144 // window proc for our tooltip control
145 // ----------------------------------------------------------------------------
146 
wxToolTipWndProc(HWND hwndTT,UINT msg,WPARAM wParam,LPARAM lParam)147 LRESULT APIENTRY wxToolTipWndProc(HWND hwndTT,
148                                   UINT msg,
149                                   WPARAM wParam,
150                                   LPARAM lParam)
151 {
152     if ( msg == TTM_WINDOWFROMPOINT )
153     {
154         LPPOINT ppt = (LPPOINT)lParam;
155 
156         // the window on which event occurred
157         HWND hwnd = ::WindowFromPoint(*ppt);
158 
159         OutputDebugString("TTM_WINDOWFROMPOINT: ");
160         OutputDebugString(wxString::Format("0x%08x => ", hwnd));
161 
162         // return a HWND corresponding to a wxWindow because only wxWidgets are
163         // associated with tooltips using TTM_ADDTOOL
164         wxWindow *win = wxGetWindowFromHWND((WXHWND)hwnd);
165 
166         if ( win )
167         {
168             hwnd = GetHwndOf(win);
169             OutputDebugString(wxString::Format("0x%08x\r\n", hwnd));
170 
171 #if 0
172             // modify the point too!
173             RECT rect;
174             GetWindowRect(hwnd, &rect);
175 
176             ppt->x = (rect.right - rect.left) / 2;
177             ppt->y = (rect.bottom - rect.top) / 2;
178 #endif // 0
179             return (LRESULT)hwnd;
180         }
181         else
182         {
183             OutputDebugString("no window\r\n");
184         }
185     }
186 
187     return ::CallWindowProc(CASTWNDPROC gs_wndprocToolTip, hwndTT, msg, wParam, lParam);
188 }
189 
190 #endif // wxUSE_TTM_WINDOWFROMPOINT
191 
192 // ----------------------------------------------------------------------------
193 // static functions
194 // ----------------------------------------------------------------------------
195 
Enable(bool flag)196 void wxToolTip::Enable(bool flag)
197 {
198     // Make sure the tooltip has been created
199     (void) GetToolTipCtrl();
200 
201     SendTooltipMessageToAll(ms_hwndTT, TTM_ACTIVATE, flag, 0);
202 }
203 
SetDelay(long milliseconds)204 void wxToolTip::SetDelay(long milliseconds)
205 {
206     // Make sure the tooltip has been created
207     (void) GetToolTipCtrl();
208 
209     SendTooltipMessageToAll(ms_hwndTT, TTM_SETDELAYTIME,
210                             TTDT_INITIAL, milliseconds);
211 }
212 
213 // ---------------------------------------------------------------------------
214 // implementation helpers
215 // ---------------------------------------------------------------------------
216 
217 // create the tooltip ctrl for our parent frame if it doesn't exist yet
218 /* static */
GetToolTipCtrl()219 WXHWND wxToolTip::GetToolTipCtrl()
220 {
221     if ( !ms_hwndTT )
222     {
223         WXDWORD exflags = 0;
224         if ( wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft )
225         {
226             exflags |= WS_EX_LAYOUTRTL;
227         }
228 
229         // we want to show the tooltips always (even when the window is not
230         // active) and we don't want to strip "&"s from them
231         ms_hwndTT = (WXHWND)::CreateWindowEx(exflags,
232                                              TOOLTIPS_CLASS,
233                                              (LPCTSTR)NULL,
234                                              TTS_ALWAYSTIP | TTS_NOPREFIX,
235                                              CW_USEDEFAULT, CW_USEDEFAULT,
236                                              CW_USEDEFAULT, CW_USEDEFAULT,
237                                              NULL, (HMENU)NULL,
238                                              wxGetInstance(),
239                                              NULL);
240        if ( ms_hwndTT )
241        {
242            HWND hwnd = (HWND)ms_hwndTT;
243            SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0,
244                         SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
245 
246 #if wxUSE_TTM_WINDOWFROMPOINT
247            // subclass the newly created control
248            gs_wndprocToolTip = wxSetWindowProc(hwnd, wxToolTipWndProc);
249 #endif // wxUSE_TTM_WINDOWFROMPOINT
250        }
251     }
252 
253     return ms_hwndTT;
254 }
255 
256 /* static */
RelayEvent(WXMSG * msg)257 void wxToolTip::RelayEvent(WXMSG *msg)
258 {
259     (void)SendTooltipMessage(GetToolTipCtrl(), TTM_RELAYEVENT, msg);
260 }
261 
262 // ----------------------------------------------------------------------------
263 // ctor & dtor
264 // ----------------------------------------------------------------------------
265 
IMPLEMENT_ABSTRACT_CLASS(wxToolTip,wxObject)266 IMPLEMENT_ABSTRACT_CLASS(wxToolTip, wxObject)
267 
268 wxToolTip::wxToolTip(const wxString &tip)
269          : m_text(tip)
270 {
271     m_window = NULL;
272 }
273 
~wxToolTip()274 wxToolTip::~wxToolTip()
275 {
276     // the tooltip has to be removed before deleting. Otherwise, if it is visible
277     // while being deleted, there will be a delay before it goes away.
278     Remove();
279 }
280 
281 // ----------------------------------------------------------------------------
282 // others
283 // ----------------------------------------------------------------------------
284 
Remove(WXHWND hWnd)285 void wxToolTip::Remove(WXHWND hWnd)
286 {
287     wxToolInfo ti((HWND)hWnd);
288     (void)SendTooltipMessage(GetToolTipCtrl(), TTM_DELTOOL, &ti);
289 }
290 
Remove()291 void wxToolTip::Remove()
292 {
293     // remove this tool from the tooltip control
294     if ( m_window )
295     {
296         Remove(m_window->GetHWND());
297     }
298 }
299 
Add(WXHWND hWnd)300 void wxToolTip::Add(WXHWND hWnd)
301 {
302     HWND hwnd = (HWND)hWnd;
303 
304     wxToolInfo ti(hwnd);
305 
306     // another possibility would be to specify LPSTR_TEXTCALLBACK here as we
307     // store the tooltip text ourselves anyhow, and provide it in response to
308     // TTN_NEEDTEXT (sent via WM_NOTIFY), but then we would be limited to 79
309     // character tooltips as this is the size of the szText buffer in
310     // NMTTDISPINFO struct -- and setting the tooltip here we can have tooltips
311     // of any length
312     ti.hwnd = hwnd;
313     ti.lpszText = (wxChar *)m_text.c_str(); // const_cast
314 
315     if ( !SendTooltipMessage(GetToolTipCtrl(), TTM_ADDTOOL, &ti) )
316     {
317         wxLogDebug(_T("Failed to create the tooltip '%s'"), m_text.c_str());
318     }
319     else
320     {
321         // check for multiline toopltip
322         int index = m_text.Find(_T('\n'));
323 
324         if ( index != wxNOT_FOUND )
325         {
326 #ifdef TTM_SETMAXTIPWIDTH
327             if ( wxApp::GetComCtl32Version() >= 470 )
328             {
329                 // use TTM_SETMAXTIPWIDTH to make tooltip multiline using the
330                 // extent of its first line as max value
331                 HFONT hfont = (HFONT)
332                     SendTooltipMessage(GetToolTipCtrl(), WM_GETFONT, 0);
333 
334                 if ( !hfont )
335                 {
336                     hfont = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
337                     if ( !hfont )
338                     {
339                         wxLogLastError(wxT("GetStockObject(DEFAULT_GUI_FONT)"));
340                     }
341                 }
342 
343                 MemoryHDC hdc;
344                 if ( !hdc )
345                 {
346                     wxLogLastError(wxT("CreateCompatibleDC(NULL)"));
347                 }
348 
349                 if ( !SelectObject(hdc, hfont) )
350                 {
351                     wxLogLastError(wxT("SelectObject(hfont)"));
352                 }
353 
354                 // find the width of the widest line
355                 int max = 0;
356                 wxStringTokenizer tokenizer(m_text, _T("\n"));
357                 wxString token = tokenizer.GetNextToken();
358                 while (token.length())
359                 {
360                     SIZE sz;
361                     if ( !::GetTextExtentPoint32(hdc, token, token.length(), &sz) )
362                     {
363                         wxLogLastError(wxT("GetTextExtentPoint32"));
364                     }
365                     if ( sz.cx > max )
366                         max = sz.cx;
367 
368                     token = tokenizer.GetNextToken();
369                 }
370 
371                 // only set a new width if it is bigger than the current setting
372                 if (max > SendTooltipMessage(GetToolTipCtrl(), TTM_GETMAXTIPWIDTH, 0))
373                     SendTooltipMessage(GetToolTipCtrl(), TTM_SETMAXTIPWIDTH,
374                                        (void *)max);
375             }
376             else
377 #endif // comctl32.dll >= 4.70
378             {
379                 // replace the '\n's with spaces because otherwise they appear as
380                 // unprintable characters in the tooltip string
381                 m_text.Replace(_T("\n"), _T(" "));
382                 ti.lpszText = (wxChar *)m_text.c_str(); // const_cast
383 
384                 if ( !SendTooltipMessage(GetToolTipCtrl(), TTM_ADDTOOL, &ti) )
385                 {
386                     wxLogDebug(_T("Failed to create the tooltip '%s'"), m_text.c_str());
387                 }
388             }
389         }
390     }
391 }
392 
SetWindow(wxWindow * win)393 void wxToolTip::SetWindow(wxWindow *win)
394 {
395     Remove();
396 
397     m_window = win;
398 
399     // add the window itself
400     if ( m_window )
401     {
402         Add(m_window->GetHWND());
403     }
404 #if !defined(__WXUNIVERSAL__)
405     // and all of its subcontrols (e.g. radiobuttons in a radiobox) as well
406     wxControl *control = wxDynamicCast(m_window, wxControl);
407     if ( control )
408     {
409         const wxArrayLong& subcontrols = control->GetSubcontrols();
410         size_t count = subcontrols.GetCount();
411         for ( size_t n = 0; n < count; n++ )
412         {
413             int id = subcontrols[n];
414             HWND hwnd = GetDlgItem(GetHwndOf(m_window), id);
415             if ( !hwnd )
416             {
417                 // may be it's a child of parent of the control, in fact?
418                 // (radiobuttons are subcontrols, i.e. children of the radiobox
419                 // for wxWidgets but are its siblings at Windows level)
420                 hwnd = GetDlgItem(GetHwndOf(m_window->GetParent()), id);
421             }
422 
423             // must have it by now!
424             wxASSERT_MSG( hwnd, _T("no hwnd for subcontrol?") );
425 
426             Add((WXHWND)hwnd);
427         }
428     }
429 
430     // VZ: it's ugly to do it here, but I don't want any major changes right
431     //     now, later we will probably want to have wxWindow::OnGotToolTip() or
432     //     something like this where the derived class can do such things
433     //     itself instead of wxToolTip "knowing" about them all
434     wxComboBox *combo = wxDynamicCast(control, wxComboBox);
435     if ( combo )
436     {
437         WXHWND hwndComboEdit = combo->GetWindowStyle() & wxCB_READONLY
438                                 ? combo->GetHWND()
439                                 : combo->GetEditHWND();
440         if ( hwndComboEdit )
441         {
442             Add(hwndComboEdit);
443         }
444     }
445 #endif // !defined(__WXUNIVERSAL__)
446 }
447 
SetTip(const wxString & tip)448 void wxToolTip::SetTip(const wxString& tip)
449 {
450     m_text = tip;
451 
452     if ( m_window )
453     {
454         // update the tip text shown by the control
455         wxToolInfo ti(GetHwndOf(m_window));
456         ti.lpszText = (wxChar *)wxT("");
457         (void)SendTooltipMessage(GetToolTipCtrl(), TTM_UPDATETIPTEXT, &ti);
458 
459         ti.lpszText = (wxChar *)m_text.c_str();
460         (void)SendTooltipMessage(GetToolTipCtrl(), TTM_UPDATETIPTEXT, &ti);
461     }
462 }
463 
464 #endif // wxUSE_TOOLTIPS
465