Add wxMessageDialog::GetEffectiveIcon() and use it in all ports.
[wxWidgets.git] / src / msw / msgdlg.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 // Name: src/msw/msgdlg.cpp
3 // Purpose: wxMessageDialog
4 // Author: Julian Smart
5 // Modified by:
6 // Created: 04/01/98
7 // RCS-ID: $Id$
8 // Copyright: (c) Julian Smart
9 // Licence: wxWindows licence
10 /////////////////////////////////////////////////////////////////////////////
11
12 // For compilers that support precompilation, includes "wx.h".
13 #include "wx/wxprec.h"
14
15 #ifdef __BORLANDC__
16 #pragma hdrstop
17 #endif
18
19 #if wxUSE_MSGDLG
20
21 #include "wx/msgdlg.h"
22
23 // there is no hook support under CE so we can't use the code for message box
24 // positioning there
25 #ifndef __WXWINCE__
26 #define wxUSE_MSGBOX_HOOK 1
27 #else
28 #define wxUSE_MSGBOX_HOOK 0
29 #endif
30
31 #ifndef WX_PRECOMP
32 #include "wx/app.h"
33 #include "wx/intl.h"
34 #include "wx/utils.h"
35 #include "wx/dialog.h"
36 #if wxUSE_MSGBOX_HOOK
37 #include "wx/hashmap.h"
38 #endif
39 #endif
40
41 #include "wx/msw/private.h"
42 #include "wx/msw/private/button.h"
43 #include "wx/msw/private/metrics.h"
44
45 #if wxUSE_MSGBOX_HOOK
46 #include "wx/fontutil.h"
47 #include "wx/textbuf.h"
48 #include "wx/display.h"
49 #endif
50
51 // For MB_TASKMODAL
52 #ifdef __WXWINCE__
53 #include "wx/msw/wince/missing.h"
54 #endif
55
56 IMPLEMENT_CLASS(wxMessageDialog, wxDialog)
57
58 #if wxUSE_MSGBOX_HOOK
59
60 // there can potentially be one message box per thread so we use a hash map
61 // with thread ids as keys and (currently shown) message boxes as values
62 //
63 // TODO: replace this with wxTLS once it's available
64 WX_DECLARE_HASH_MAP(unsigned long, wxMessageDialog *,
65 wxIntegerHash, wxIntegerEqual,
66 wxMessageDialogMap);
67
68 // the order in this array is the one in which buttons appear in the
69 // message box
70 const wxMessageDialog::ButtonAccessors wxMessageDialog::ms_buttons[] =
71 {
72 { IDYES, &wxMessageDialog::GetYesLabel },
73 { IDNO, &wxMessageDialog::GetNoLabel },
74 { IDOK, &wxMessageDialog::GetOKLabel },
75 { IDCANCEL, &wxMessageDialog::GetCancelLabel },
76 };
77
78 namespace
79 {
80
81 wxMessageDialogMap& HookMap()
82 {
83 static wxMessageDialogMap s_Map;
84
85 return s_Map;
86 }
87
88 /*
89 All this code is used for adjusting the message box layout when we mess
90 with its contents. It's rather complicated because we try hard to avoid
91 assuming much about the standard layout details and so, instead of just
92 laying out everything ourselves (which would have been so much simpler!)
93 we try to only modify the existing controls positions by offsetting them
94 from their default ones in the hope that this will continue to work with
95 the future Windows versions.
96 */
97
98 // convert the given RECT from screen to client coordinates in place
99 void ScreenRectToClient(HWND hwnd, RECT& rc)
100 {
101 // map from desktop (i.e. screen) coordinates to ones of this window
102 //
103 // notice that a RECT is laid out as 2 consecutive POINTs so the cast is
104 // valid
105 ::MapWindowPoints(HWND_DESKTOP, hwnd, reinterpret_cast<POINT *>(&rc), 2);
106 }
107
108 // set window position to the given rect
109 inline void SetWindowRect(HWND hwnd, const RECT& rc)
110 {
111 ::MoveWindow(hwnd,
112 rc.left, rc.top,
113 rc.right - rc.left, rc.bottom - rc.top,
114 FALSE);
115 }
116
117 // set window position expressed in screen coordinates, whether the window is
118 // child or top level
119 void MoveWindowToScreenRect(HWND hwnd, RECT rc)
120 {
121 ScreenRectToClient(::GetParent(hwnd), rc);
122
123 SetWindowRect(hwnd, rc);
124 }
125
126 // helper of AdjustButtonLabels(): move the given window by dx
127 //
128 // works for both child and top level windows
129 void OffsetWindow(HWND hwnd, int dx)
130 {
131 RECT rc = wxGetWindowRect(hwnd);
132
133 rc.left += dx;
134 rc.right += dx;
135
136 MoveWindowToScreenRect(hwnd, rc);
137 }
138
139 } // anonymous namespace
140
141 /* static */
142 WXLRESULT wxCALLBACK
143 wxMessageDialog::HookFunction(int code, WXWPARAM wParam, WXLPARAM lParam)
144 {
145 // Find the thread-local instance of wxMessageDialog
146 const DWORD tid = ::GetCurrentThreadId();
147 wxMessageDialogMap::iterator node = HookMap().find(tid);
148 wxCHECK_MSG( node != HookMap().end(), false,
149 wxT("bogus thread id in wxMessageDialog::Hook") );
150
151 wxMessageDialog * const wnd = node->second;
152
153 const HHOOK hhook = (HHOOK)wnd->m_hook;
154 const LRESULT rc = ::CallNextHookEx(hhook, code, wParam, lParam);
155
156 if ( code == HCBT_ACTIVATE )
157 {
158 // we won't need this hook any longer
159 ::UnhookWindowsHookEx(hhook);
160 wnd->m_hook = NULL;
161 HookMap().erase(tid);
162
163 wnd->SetHWND((HWND)wParam);
164
165 // replace the static text with an edit control if the message box is
166 // too big to fit the display
167 wnd->ReplaceStaticWithEdit();
168
169 // update the labels if necessary: we need to do it before centering
170 // the dialog as this can change its size
171 if ( wnd->HasCustomLabels() )
172 wnd->AdjustButtonLabels();
173
174 // centre the message box on its parent if requested
175 if ( wnd->GetMessageDialogStyle() & wxCENTER )
176 wnd->Center(); // center on parent
177 //else: default behaviour, center on screen
178
179 // there seems to be no reason to leave it set
180 wnd->SetHWND(NULL);
181 }
182
183 return rc;
184 }
185
186 void wxMessageDialog::ReplaceStaticWithEdit()
187 {
188 // check if the message box fits the display
189 int nDisplay = wxDisplay::GetFromWindow(this);
190 if ( nDisplay == wxNOT_FOUND )
191 nDisplay = 0;
192 const wxRect rectDisplay = wxDisplay(nDisplay).GetClientArea();
193
194 if ( rectDisplay.Contains(GetRect()) )
195 {
196 // nothing to do
197 return;
198 }
199
200
201 // find the static control to replace: normally there are two of them, the
202 // icon and the text itself so search for all of them and ignore the icon
203 // ones
204 HWND hwndStatic = ::FindWindowEx(GetHwnd(), NULL, wxT("STATIC"), NULL);
205 if ( ::GetWindowLong(hwndStatic, GWL_STYLE) & SS_ICON )
206 hwndStatic = ::FindWindowEx(GetHwnd(), hwndStatic, wxT("STATIC"), NULL);
207
208 if ( !hwndStatic )
209 {
210 wxLogDebug("Failed to find the static text control in message box.");
211 return;
212 }
213
214 // set the right font for GetCharHeight() call below
215 wxWindowBase::SetFont(GetMessageFont());
216
217 // put the new edit control at the same place
218 RECT rc = wxGetWindowRect(hwndStatic);
219 ScreenRectToClient(GetHwnd(), rc);
220
221 // but make it less tall so that the message box fits on the screen: we try
222 // to make the message box take no more than 7/8 of the screen to leave
223 // some space above and below it
224 const int hText = (7*rectDisplay.height)/8 -
225 (
226 2*::GetSystemMetrics(SM_CYFIXEDFRAME) +
227 ::GetSystemMetrics(SM_CYCAPTION) +
228 5*GetCharHeight() // buttons + margins
229 );
230 const int dh = (rc.bottom - rc.top) - hText; // vertical space we save
231 rc.bottom -= dh;
232
233 // and it also must be wider as it needs a vertical scrollbar (in order
234 // to preserve the word wrap, otherwise the number of lines would change
235 // and we want the control to look as similar as possible to the original)
236 //
237 // NB: you would have thought that 2*SM_CXEDGE would be enough but it
238 // isn't, somehow, and the text control breaks lines differently from
239 // the static one so fudge by adding some extra space
240 const int dw = ::GetSystemMetrics(SM_CXVSCROLL) +
241 4*::GetSystemMetrics(SM_CXEDGE);
242 rc.right += dw;
243
244
245 // chop of the trailing new line(s) from the message box text, they are
246 // ignored by the static control but result in extra lines and hence extra
247 // scrollbar position in the edit one
248 wxString text(wxGetWindowText(hwndStatic));
249 for ( wxString::reverse_iterator i = text.rbegin(); i != text.rend(); ++i )
250 {
251 if ( *i != '\n' )
252 {
253 // found last non-newline char, remove everything after it and stop
254 text.erase(i.base() + 1, text.end());
255 break;
256 }
257 }
258
259 // do create the new control
260 HWND hwndEdit = ::CreateWindow
261 (
262 wxT("EDIT"),
263 wxTextBuffer::Translate(text).wx_str(),
264 WS_CHILD | WS_VSCROLL | WS_VISIBLE |
265 ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL,
266 rc.left, rc.top,
267 rc.right - rc.left, rc.bottom - rc.top,
268 GetHwnd(),
269 NULL,
270 wxGetInstance(),
271 NULL
272 );
273
274 if ( !hwndEdit )
275 {
276 wxLogDebug("Creation of replacement edit control failed in message box");
277 return;
278 }
279
280 // copy the font from the original control
281 LRESULT hfont = ::SendMessage(hwndStatic, WM_GETFONT, 0, 0);
282 ::SendMessage(hwndEdit, WM_SETFONT, hfont, 0);
283
284 // and get rid of it
285 ::DestroyWindow(hwndStatic);
286
287
288 // shrink and centre the message box vertically and widen it box to account
289 // for the extra scrollbar
290 RECT rcBox = wxGetWindowRect(GetHwnd());
291 const int hMsgBox = rcBox.bottom - rcBox.top - dh;
292 rcBox.top = (rectDisplay.height - hMsgBox)/2;
293 rcBox.bottom = rcBox.top + hMsgBox + (rectDisplay.height - hMsgBox)%2;
294 rcBox.left -= dw/2;
295 rcBox.right += dw - dw/2;
296 SetWindowRect(GetHwnd(), rcBox);
297
298 // and adjust all the buttons positions
299 for ( unsigned n = 0; n < WXSIZEOF(ms_buttons); n++ )
300 {
301 const HWND hwndBtn = ::GetDlgItem(GetHwnd(), ms_buttons[n].id);
302 if ( !hwndBtn )
303 continue; // it's ok, not all buttons are always present
304
305 RECT rc = wxGetWindowRect(hwndBtn);
306 rc.top -= dh;
307 rc.bottom -= dh;
308 rc.left += dw/2;
309 rc.right += dw/2;
310 MoveWindowToScreenRect(hwndBtn, rc);
311 }
312 }
313
314 void wxMessageDialog::AdjustButtonLabels()
315 {
316 // changing the button labels is the easy part but we also need to ensure
317 // that the buttons are big enough for the label strings and increase their
318 // size (and maybe the size of the message box itself) if they are not
319
320 // TODO-RTL: check whether this works correctly in RTL
321
322 // we want to use this font in GetTextExtent() calls below but we don't
323 // want to send WM_SETFONT to the message box, who knows how is it going to
324 // react to it (right now it doesn't seem to do anything but what if this
325 // changes)
326 wxWindowBase::SetFont(GetMessageFont());
327
328 // first iteration: find the widest button and update the buttons labels
329 int wBtnOld = 0, // current buttons width
330 wBtnNew = 0; // required new buttons width
331 RECT rcBtn; // stores the button height and y positions
332 unsigned numButtons = 0; // total number of buttons in the message box
333 unsigned n;
334 for ( n = 0; n < WXSIZEOF(ms_buttons); n++ )
335 {
336 const HWND hwndBtn = ::GetDlgItem(GetHwnd(), ms_buttons[n].id);
337 if ( !hwndBtn )
338 continue; // it's ok, not all buttons are always present
339
340 numButtons++;
341
342 const wxString label = (this->*ms_buttons[n].getter)();
343 const wxSize sizeLabel = wxWindowBase::GetTextExtent(label);
344
345 // check if the button is big enough for this label
346 const RECT rc = wxGetWindowRect(hwndBtn);
347 if ( !wBtnOld )
348 {
349 // initialize wBtnOld using the first button width, all the other
350 // ones should have the same one
351 wBtnOld = rc.right - rc.left;
352
353 rcBtn = rc; // remember for use below when we reposition the buttons
354 }
355 else
356 {
357 wxASSERT_MSG( wBtnOld == rc.right - rc.left,
358 "all buttons are supposed to be of same width" );
359 }
360
361 const int widthNeeded = wxMSWButton::GetFittingSize(this, sizeLabel).x;
362 if ( widthNeeded > wBtnNew )
363 wBtnNew = widthNeeded;
364
365 ::SetWindowText(hwndBtn, label.wx_str());
366 }
367
368 if ( wBtnNew <= wBtnOld )
369 {
370 // all buttons fit, nothing else to do
371 return;
372 }
373
374 // resize the message box to be wider if needed
375 const int wBoxOld = wxGetClientRect(GetHwnd()).right;
376
377 const int CHAR_WIDTH = GetCharWidth();
378 const int MARGIN_OUTER = 2*CHAR_WIDTH; // margin between box and buttons
379 const int MARGIN_INNER = CHAR_WIDTH; // margin between buttons
380
381 RECT rcBox = wxGetWindowRect(GetHwnd());
382
383 const int wAllButtons = numButtons*(wBtnNew + MARGIN_INNER) - MARGIN_INNER;
384 int wBoxNew = 2*MARGIN_OUTER + wAllButtons;
385 if ( wBoxNew > wBoxOld )
386 {
387 const int dw = wBoxNew - wBoxOld;
388 rcBox.left -= dw/2;
389 rcBox.right += dw - dw/2;
390
391 SetWindowRect(GetHwnd(), rcBox);
392
393 // surprisingly, we don't need to resize the static text control, it
394 // seems to adjust itself to the new size, at least under Windows 2003
395 // (TODO: test if this happens on older Windows versions)
396 }
397 else // the current width is big enough
398 {
399 wBoxNew = wBoxOld;
400 }
401
402
403 // finally position all buttons
404
405 // notice that we have to take into account the difference between window
406 // and client width
407 rcBtn.left = (rcBox.left + rcBox.right - wxGetClientRect(GetHwnd()).right +
408 wBoxNew - wAllButtons) / 2;
409 rcBtn.right = rcBtn.left + wBtnNew;
410
411 for ( n = 0; n < WXSIZEOF(ms_buttons); n++ )
412 {
413 const HWND hwndBtn = ::GetDlgItem(GetHwnd(), ms_buttons[n].id);
414 if ( !hwndBtn )
415 continue;
416
417 MoveWindowToScreenRect(hwndBtn, rcBtn);
418
419 rcBtn.left += wBtnNew + MARGIN_INNER;
420 rcBtn.right += wBtnNew + MARGIN_INNER;
421 }
422 }
423
424 #endif // wxUSE_MSGBOX_HOOK
425
426 /* static */
427 wxFont wxMessageDialog::GetMessageFont()
428 {
429 const NONCLIENTMETRICS& ncm = wxMSWImpl::GetNonClientMetrics();
430 return wxNativeFontInfo(ncm.lfMessageFont);
431 }
432
433 int wxMessageDialog::ShowModal()
434 {
435 if ( !wxTheApp->GetTopWindow() )
436 {
437 // when the message box is shown from wxApp::OnInit() (i.e. before the
438 // message loop is entered), this must be done or the next message box
439 // will never be shown - just try putting 2 calls to wxMessageBox() in
440 // OnInit() to see it
441 while ( wxTheApp->Pending() )
442 wxTheApp->Dispatch();
443 }
444
445 // use the top level window as parent if none specified
446 if ( !m_parent )
447 m_parent = GetParentForModalDialog();
448 HWND hWnd = m_parent ? GetHwndOf(m_parent) : NULL;
449
450 #if wxUSE_INTL
451 // native message box always uses the current user locale but the program
452 // may be using a different one and in this case we need to manually
453 // translate the button labels to avoid mismatch between the language of
454 // the message box text and its buttons
455 wxLocale * const loc = wxGetLocale();
456 if ( loc && loc->GetLanguage() != wxLocale::GetSystemLanguage() )
457 {
458 if ( m_dialogStyle & wxYES_NO )
459 {
460 // use the strings with mnemonics here as the native message box
461 // does
462 SetYesNoLabels(_("&Yes"), _("&No"));
463 }
464
465 // we may or not have the Ok/Cancel buttons but either we do have them
466 // or we already made the labels custom because we called
467 // SetYesNoLabels() above so doing this does no harm -- and is
468 // necessary in wxYES_NO | wxCANCEL case
469 //
470 // note that we don't use mnemonics here for consistency with the
471 // native message box (which probably doesn't use them because
472 // Enter/Esc keys can be already used to dismiss the message box
473 // using keyboard)
474 SetOKCancelLabels(_("OK"), _("Cancel"));
475 }
476 #endif // wxUSE_INTL
477
478 // translate wx style in MSW
479 unsigned int msStyle;
480 const long wxStyle = GetMessageDialogStyle();
481 if ( wxStyle & wxYES_NO )
482 {
483 #if !(defined(__SMARTPHONE__) && defined(__WXWINCE__))
484 if (wxStyle & wxCANCEL)
485 msStyle = MB_YESNOCANCEL;
486 else
487 #endif // !(__SMARTPHONE__ && __WXWINCE__)
488 msStyle = MB_YESNO;
489
490 if ( wxStyle & wxNO_DEFAULT )
491 msStyle |= MB_DEFBUTTON2;
492 else if ( wxStyle & wxCANCEL_DEFAULT )
493 msStyle |= MB_DEFBUTTON3;
494 }
495 else // without Yes/No we're going to have an OK button
496 {
497 if ( wxStyle & wxCANCEL )
498 {
499 msStyle = MB_OKCANCEL;
500
501 if ( wxStyle & wxCANCEL_DEFAULT )
502 msStyle |= MB_DEFBUTTON2;
503 }
504 else // just "OK"
505 {
506 msStyle = MB_OK;
507 }
508 }
509
510 // set the icon style
511 switch ( GetEffectiveIcon() )
512 {
513 case wxICON_ERROR:
514 msStyle |= MB_ICONHAND;
515 break;
516
517 case wxICON_WARNING:
518 msStyle |= MB_ICONEXCLAMATION;
519 break;
520
521 case wxICON_QUESTION:
522 msStyle |= MB_ICONQUESTION;
523 break;
524
525 case wxICON_INFORMATION:
526 msStyle |= MB_ICONINFORMATION;
527 break;
528 }
529
530 if ( wxStyle & wxSTAY_ON_TOP )
531 msStyle |= MB_TOPMOST;
532
533 #ifndef __WXWINCE__
534 if ( wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft )
535 msStyle |= MB_RTLREADING | MB_RIGHT;
536 #endif
537
538 if (hWnd)
539 msStyle |= MB_APPLMODAL;
540 else
541 msStyle |= MB_TASKMODAL;
542
543 // per MSDN documentation for MessageBox() we can prefix the message with 2
544 // right-to-left mark characters to tell the function to use RTL layout
545 // (unfortunately this only works in Unicode builds)
546 wxString message = GetFullMessage();
547 #if wxUSE_UNICODE
548 if ( wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft )
549 {
550 // NB: not all compilers support \u escapes
551 static const wchar_t wchRLM = 0x200f;
552 message.Prepend(wxString(wchRLM, 2));
553 }
554 #endif // wxUSE_UNICODE
555
556 #if wxUSE_MSGBOX_HOOK
557 // install the hook in any case as we don't know in advance if the message
558 // box is not going to be too big (requiring the replacement of the static
559 // control with an edit one)
560 const DWORD tid = ::GetCurrentThreadId();
561 m_hook = ::SetWindowsHookEx(WH_CBT,
562 &wxMessageDialog::HookFunction, NULL, tid);
563 HookMap()[tid] = this;
564 #endif // wxUSE_MSGBOX_HOOK
565
566 // do show the dialog
567 int msAns = MessageBox(hWnd, message.wx_str(), m_caption.wx_str(), msStyle);
568 int ans;
569 switch (msAns)
570 {
571 default:
572 wxFAIL_MSG(wxT("unexpected ::MessageBox() return code"));
573 // fall through
574
575 case IDCANCEL:
576 ans = wxID_CANCEL;
577 break;
578 case IDOK:
579 ans = wxID_OK;
580 break;
581 case IDYES:
582 ans = wxID_YES;
583 break;
584 case IDNO:
585 ans = wxID_NO;
586 break;
587 }
588 return ans;
589 }
590
591 #endif // wxUSE_MSGDLG