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