fix for page change not working after veto, #3808
[wxWidgets.git] / src / gtk / notebook.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 // Name: src/gtk/notebook.cpp
3 // Purpose:
4 // Author: Robert Roebling
5 // Id: $Id$
6 // Copyright: (c) 1998 Robert Roebling, Vadim Zeitlin
7 // Licence: wxWindows licence
8 /////////////////////////////////////////////////////////////////////////////
9
10 // For compilers that support precompilation, includes "wx.h".
11 #include "wx/wxprec.h"
12
13 #if wxUSE_NOTEBOOK
14
15 #include "wx/notebook.h"
16
17 #ifndef WX_PRECOMP
18 #include "wx/intl.h"
19 #include "wx/log.h"
20 #include "wx/utils.h"
21 #include "wx/msgdlg.h"
22 #include "wx/bitmap.h"
23 #endif
24
25 #include "wx/imaglist.h"
26 #include "wx/fontutil.h"
27
28 #include "wx/gtk/private.h"
29
30 //-----------------------------------------------------------------------------
31 // wxGtkNotebookPage
32 //-----------------------------------------------------------------------------
33
34 // VZ: this is rather ugly as we keep the pages themselves in an array (it
35 // allows us to have quite a few functions implemented in the base class)
36 // but the page data is kept in a separate list, so we must maintain them
37 // in sync manually... of course, the list had been there before the base
38 // class which explains it but it still would be nice to do something
39 // about this one day
40
41 class wxGtkNotebookPage: public wxObject
42 {
43 public:
44 wxGtkNotebookPage()
45 {
46 m_image = -1;
47 m_box = NULL;
48 }
49
50 wxString m_text;
51 int m_image;
52 GtkLabel *m_label;
53 GtkWidget *m_box; // in which the label and image are packed
54 };
55
56
57 #include "wx/listimpl.cpp"
58 WX_DEFINE_LIST(wxGtkNotebookPagesList)
59
60 extern "C" {
61 static void event_after(GtkNotebook*, GdkEvent*, wxNotebook*);
62 }
63
64 //-----------------------------------------------------------------------------
65 // "switch_page"
66 //-----------------------------------------------------------------------------
67
68 extern "C" {
69 static void
70 switch_page_after(GtkWidget* widget, GtkNotebookPage*, guint, wxNotebook* win)
71 {
72 g_signal_handlers_block_by_func(widget, (void*)switch_page_after, win);
73 win->SendPageChangedEvent(win->m_oldSelection);
74 }
75 }
76
77 extern "C" {
78 static void
79 switch_page(GtkNotebook* widget, GtkNotebookPage*, int page, wxNotebook* win)
80 {
81 win->m_oldSelection = gtk_notebook_get_current_page(widget);
82
83 if (win->SendPageChangingEvent(page))
84 // allow change, unblock handler for changed event
85 g_signal_handlers_unblock_by_func(widget, (void*)switch_page_after, win);
86 else
87 // change vetoed, unblock handler to set selection back
88 g_signal_handlers_unblock_by_func(widget, (void*)event_after, win);
89 }
90 }
91
92 //-----------------------------------------------------------------------------
93 // "event_after" from m_widget
94 //-----------------------------------------------------------------------------
95
96 extern "C" {
97 static void event_after(GtkNotebook* widget, GdkEvent*, wxNotebook* win)
98 {
99 g_signal_handlers_block_by_func(widget, (void*)event_after, win);
100 g_signal_handlers_block_by_func(widget, (void*)switch_page, win);
101
102 // restore previous selection
103 gtk_notebook_set_current_page(widget, win->m_oldSelection);
104
105 g_signal_handlers_unblock_by_func(widget, (void*)switch_page, win);
106 }
107 }
108
109 //-----------------------------------------------------------------------------
110 // InsertChild callback for wxNotebook
111 //-----------------------------------------------------------------------------
112
113 static void wxInsertChildInNotebook(wxWindow* parent, wxWindow* child)
114 {
115 // Hack Alert! (Part I): This sets the notebook as the parent of the child
116 // widget, and takes care of some details such as updating the state and
117 // style of the child to reflect its new location. We do this early
118 // because without it GetBestSize (which is used to set the initial size
119 // of controls if an explicit size is not given) will often report
120 // incorrect sizes since the widget's style context is not fully known.
121 // See bug #901694 for details
122 // (http://sourceforge.net/tracker/?func=detail&aid=901694&group_id=9863&atid=109863)
123 gtk_widget_set_parent(child->m_widget, parent->m_widget);
124
125 // NOTE: This should be considered a temporary workaround until we can
126 // work out the details and implement delaying the setting of the initial
127 // size of widgets until the size is really needed.
128 }
129
130 //-----------------------------------------------------------------------------
131 // wxNotebook
132 //-----------------------------------------------------------------------------
133
134 IMPLEMENT_DYNAMIC_CLASS(wxNotebook,wxBookCtrlBase)
135
136 BEGIN_EVENT_TABLE(wxNotebook, wxBookCtrlBase)
137 EVT_NAVIGATION_KEY(wxNotebook::OnNavigationKey)
138 END_EVENT_TABLE()
139
140 void wxNotebook::Init()
141 {
142 m_padding = 0;
143
144 m_imageList = (wxImageList *) NULL;
145 m_oldSelection = -1;
146 m_themeEnabled = true;
147 }
148
149 wxNotebook::wxNotebook()
150 {
151 Init();
152 }
153
154 wxNotebook::wxNotebook( wxWindow *parent, wxWindowID id,
155 const wxPoint& pos, const wxSize& size,
156 long style, const wxString& name )
157 {
158 Init();
159 Create( parent, id, pos, size, style, name );
160 }
161
162 wxNotebook::~wxNotebook()
163 {
164 DeleteAllPages();
165 }
166
167 bool wxNotebook::Create(wxWindow *parent, wxWindowID id,
168 const wxPoint& pos, const wxSize& size,
169 long style, const wxString& name )
170 {
171 m_insertCallback = wxInsertChildInNotebook;
172
173 if ( (style & wxBK_ALIGN_MASK) == wxBK_DEFAULT )
174 style |= wxBK_TOP;
175
176 if (!PreCreation( parent, pos, size ) ||
177 !CreateBase( parent, id, pos, size, style, wxDefaultValidator, name ))
178 {
179 wxFAIL_MSG( wxT("wxNoteBook creation failed") );
180 return false;
181 }
182
183
184 m_widget = gtk_notebook_new();
185
186 gtk_notebook_set_scrollable( GTK_NOTEBOOK(m_widget), 1 );
187
188 g_signal_connect (m_widget, "switch_page",
189 G_CALLBACK(switch_page), this);
190
191 g_signal_connect_after (m_widget, "switch_page",
192 G_CALLBACK(switch_page_after), this);
193 g_signal_handlers_block_by_func(m_widget, (void*)switch_page_after, this);
194
195 g_signal_connect(m_widget, "event_after", G_CALLBACK(event_after), this);
196 g_signal_handlers_block_by_func(m_widget, (void*)event_after, this);
197
198 m_parent->DoAddChild( this );
199
200 if (m_windowStyle & wxBK_RIGHT)
201 gtk_notebook_set_tab_pos( GTK_NOTEBOOK(m_widget), GTK_POS_RIGHT );
202 if (m_windowStyle & wxBK_LEFT)
203 gtk_notebook_set_tab_pos( GTK_NOTEBOOK(m_widget), GTK_POS_LEFT );
204 if (m_windowStyle & wxBK_BOTTOM)
205 gtk_notebook_set_tab_pos( GTK_NOTEBOOK(m_widget), GTK_POS_BOTTOM );
206
207 PostCreation(size);
208
209 return true;
210 }
211
212 int wxNotebook::GetSelection() const
213 {
214 wxCHECK_MSG( m_widget != NULL, -1, wxT("invalid notebook") );
215
216 return gtk_notebook_get_current_page( GTK_NOTEBOOK(m_widget) );
217 }
218
219 wxString wxNotebook::GetPageText( size_t page ) const
220 {
221 wxCHECK_MSG( m_widget != NULL, wxEmptyString, wxT("invalid notebook") );
222
223 wxGtkNotebookPage* nb_page = GetNotebookPage(page);
224 if (nb_page)
225 return nb_page->m_text;
226 else
227 return wxEmptyString;
228 }
229
230 int wxNotebook::GetPageImage( size_t page ) const
231 {
232 wxCHECK_MSG( m_widget != NULL, -1, wxT("invalid notebook") );
233
234 wxGtkNotebookPage* nb_page = GetNotebookPage(page);
235 if (nb_page)
236 return nb_page->m_image;
237 else
238 return -1;
239 }
240
241 wxGtkNotebookPage* wxNotebook::GetNotebookPage( int page ) const
242 {
243 wxCHECK_MSG( m_widget != NULL, (wxGtkNotebookPage*) NULL, wxT("invalid notebook") );
244
245 wxCHECK_MSG( page < (int)m_pagesData.GetCount(), (wxGtkNotebookPage*) NULL, wxT("invalid notebook index") );
246
247 return m_pagesData.Item(page)->GetData();
248 }
249
250 int wxNotebook::DoSetSelection( size_t page, int flags )
251 {
252 wxCHECK_MSG( m_widget != NULL, -1, wxT("invalid notebook") );
253
254 wxCHECK_MSG( page < m_pagesData.GetCount(), -1, wxT("invalid notebook index") );
255
256 int selOld = GetSelection();
257
258 if ( !(flags & SetSelection_SendEvent) )
259 {
260 g_signal_handlers_block_by_func(m_widget, (void*)switch_page, this);
261 }
262
263 gtk_notebook_set_current_page( GTK_NOTEBOOK(m_widget), page );
264
265 if ( !(flags & SetSelection_SendEvent) )
266 {
267 g_signal_handlers_unblock_by_func(m_widget, (void*)switch_page, this);
268 }
269
270 wxNotebookPage *client = GetPage(page);
271 if ( client )
272 client->SetFocus();
273
274 return selOld;
275 }
276
277 bool wxNotebook::SetPageText( size_t page, const wxString &text )
278 {
279 wxCHECK_MSG( m_widget != NULL, false, wxT("invalid notebook") );
280
281 wxGtkNotebookPage* nb_page = GetNotebookPage(page);
282
283 wxCHECK_MSG( nb_page, false, wxT("SetPageText: invalid page index") );
284
285 nb_page->m_text = text;
286
287 gtk_label_set_text( nb_page->m_label, wxGTK_CONV( nb_page->m_text ) );
288
289 return true;
290 }
291
292 bool wxNotebook::SetPageImage( size_t page, int image )
293 {
294 /* HvdH 28-12-98: now it works, but it's a bit of a kludge */
295
296 wxGtkNotebookPage* nb_page = GetNotebookPage(page);
297
298 if (!nb_page) return false;
299
300 /* Optimization posibility: return immediately if image unchanged.
301 * Not enabled because it may break existing (stupid) code that
302 * manipulates the imagelist to cycle images */
303
304 /* if (image == nb_page->m_image) return true; */
305
306 /* For different cases:
307 1) no image -> no image
308 2) image -> no image
309 3) no image -> image
310 4) image -> image */
311
312 if (image == -1 && nb_page->m_image == -1)
313 return true; /* Case 1): Nothing to do. */
314
315 GtkWidget *pixmapwid = (GtkWidget*) NULL;
316
317 if (nb_page->m_image != -1)
318 {
319 /* Case 2) or 4). There is already an image in the gtkhbox. Let's find it */
320
321 GList *child = gtk_container_get_children(GTK_CONTAINER(nb_page->m_box));
322 while (child)
323 {
324 if (GTK_IS_IMAGE(child->data))
325 {
326 pixmapwid = GTK_WIDGET(child->data);
327 break;
328 }
329 child = child->next;
330 }
331
332 /* We should have the pixmap widget now */
333 wxASSERT(pixmapwid != NULL);
334
335 if (image == -1)
336 {
337 /* If there's no new widget, just remove the old from the box */
338 gtk_container_remove(GTK_CONTAINER(nb_page->m_box), pixmapwid);
339 nb_page->m_image = -1;
340
341 return true; /* Case 2) */
342 }
343 }
344
345 /* Only cases 3) and 4) left */
346 wxASSERT( m_imageList != NULL ); /* Just in case */
347
348 /* Construct the new pixmap */
349 const wxBitmap *bmp = m_imageList->GetBitmapPtr(image);
350
351 if (pixmapwid == NULL)
352 {
353 /* Case 3) No old pixmap. Create a new one and prepend it to the hbox */
354 pixmapwid = gtk_image_new_from_pixbuf(bmp->GetPixbuf());
355
356 /* CHECKME: Are these pack flags okay? */
357 gtk_box_pack_start(GTK_BOX(nb_page->m_box), pixmapwid, FALSE, FALSE, m_padding);
358 gtk_widget_show(pixmapwid);
359 }
360 else
361 {
362 /* Case 4) Simply replace the pixmap */
363 gtk_image_set_from_pixbuf((GtkImage*)pixmapwid, bmp->GetPixbuf());
364 }
365
366 nb_page->m_image = image;
367
368 return true;
369 }
370
371 void wxNotebook::SetPageSize( const wxSize &WXUNUSED(size) )
372 {
373 wxFAIL_MSG( wxT("wxNotebook::SetPageSize not implemented") );
374 }
375
376 void wxNotebook::SetPadding( const wxSize &padding )
377 {
378 wxCHECK_RET( m_widget != NULL, wxT("invalid notebook") );
379
380 m_padding = padding.GetWidth();
381
382 int i;
383 for (i=0; i<int(GetPageCount()); i++)
384 {
385 wxGtkNotebookPage* nb_page = GetNotebookPage(i);
386 wxASSERT(nb_page != NULL);
387
388 if (nb_page->m_image != -1)
389 {
390 // gtk_box_set_child_packing sets padding on BOTH sides
391 // icon provides left padding, label provides center and right
392 int image = nb_page->m_image;
393 SetPageImage(i,-1);
394 SetPageImage(i,image);
395 }
396 wxASSERT(nb_page->m_label);
397 gtk_box_set_child_packing(GTK_BOX(nb_page->m_box),
398 GTK_WIDGET(nb_page->m_label),
399 FALSE, FALSE, m_padding, GTK_PACK_END);
400 }
401 }
402
403 void wxNotebook::SetTabSize(const wxSize& WXUNUSED(sz))
404 {
405 wxFAIL_MSG( wxT("wxNotebook::SetTabSize not implemented") );
406 }
407
408 bool wxNotebook::DeleteAllPages()
409 {
410 wxCHECK_MSG( m_widget != NULL, false, wxT("invalid notebook") );
411
412 while (m_pagesData.GetCount() > 0)
413 DeletePage( m_pagesData.GetCount()-1 );
414
415 wxASSERT_MSG( GetPageCount() == 0, _T("all pages must have been deleted") );
416
417 InvalidateBestSize();
418 return wxNotebookBase::DeleteAllPages();
419 }
420
421 wxNotebookPage *wxNotebook::DoRemovePage( size_t page )
422 {
423 // We cannot remove the page yet, as GTK sends the "switch_page"
424 // signal before it has removed the notebook-page from its
425 // corresponding list. Thus, if we were to remove the page from
426 // m_pages at this point, the two lists of pages would be out
427 // of sync during the PAGE_CHANGING/PAGE_CHANGED events.
428 wxNotebookPage *client = GetPage(page);
429 if ( !client )
430 return NULL;
431
432 gtk_widget_ref( client->m_widget );
433 gtk_widget_unrealize( client->m_widget );
434
435 // we don't need to unparent the client->m_widget; GTK+ will do
436 // that for us (and will throw a warning if we do it!)
437 gtk_notebook_remove_page( GTK_NOTEBOOK(m_widget), page );
438
439 // It's safe to remove the page now.
440 wxASSERT_MSG(GetPage(page) == client, wxT("pages changed during delete"));
441 wxNotebookBase::DoRemovePage(page);
442
443 wxGtkNotebookPage* p = GetNotebookPage(page);
444 m_pagesData.DeleteObject(p);
445 delete p;
446
447 return client;
448 }
449
450 bool wxNotebook::InsertPage( size_t position,
451 wxNotebookPage* win,
452 const wxString& text,
453 bool select,
454 int imageId )
455 {
456 wxCHECK_MSG( m_widget != NULL, false, wxT("invalid notebook") );
457
458 wxCHECK_MSG( win->GetParent() == this, false,
459 wxT("Can't add a page whose parent is not the notebook!") );
460
461 wxCHECK_MSG( position <= GetPageCount(), false,
462 _T("invalid page index in wxNotebookPage::InsertPage()") );
463
464 // Hack Alert! (Part II): See above in wxInsertChildInNotebook callback
465 // why this has to be done. NOTE: using gtk_widget_unparent here does not
466 // work as it seems to undo too much and will cause errors in the
467 // gtk_notebook_insert_page below, so instead just clear the parent by
468 // hand here.
469 win->m_widget->parent = NULL;
470
471 if (m_themeEnabled)
472 win->SetThemeEnabled(true);
473
474 GtkNotebook *notebook = GTK_NOTEBOOK(m_widget);
475
476 wxGtkNotebookPage *nb_page = new wxGtkNotebookPage();
477
478 if ( position == GetPageCount() )
479 m_pagesData.Append( nb_page );
480 else
481 m_pagesData.Insert( position, nb_page );
482
483 m_pages.Insert(win, position);
484
485 // set the label image and text
486 // this must be done before adding the page, as GetPageText
487 // and GetPageImage will otherwise return wrong values in
488 // the page-changed event that results from inserting the
489 // first page.
490 nb_page->m_image = imageId;
491 nb_page->m_text = wxStripMenuCodes(text);
492
493 nb_page->m_box = gtk_hbox_new( FALSE, 1 );
494 gtk_container_set_border_width((GtkContainer*)nb_page->m_box, 2);
495
496 gtk_notebook_insert_page(notebook, win->m_widget, nb_page->m_box, position);
497
498 if (imageId != -1)
499 {
500 wxASSERT( m_imageList != NULL );
501
502 const wxBitmap *bmp = m_imageList->GetBitmapPtr(imageId);
503 GtkWidget* pixmapwid = gtk_image_new_from_pixbuf(bmp->GetPixbuf());
504 gtk_box_pack_start(GTK_BOX(nb_page->m_box), pixmapwid, FALSE, FALSE, m_padding);
505 gtk_widget_show(pixmapwid);
506 }
507
508 /* set the label text */
509 nb_page->m_label = GTK_LABEL( gtk_label_new(wxGTK_CONV(nb_page->m_text)) );
510 gtk_box_pack_end( GTK_BOX(nb_page->m_box), GTK_WIDGET(nb_page->m_label), FALSE, FALSE, m_padding );
511
512 /* apply current style */
513 GtkRcStyle *style = CreateWidgetStyle();
514 if ( style )
515 {
516 gtk_widget_modify_style(GTK_WIDGET(nb_page->m_label), style);
517 gtk_rc_style_unref(style);
518 }
519
520 /* show the label */
521 gtk_widget_show( GTK_WIDGET(nb_page->m_label) );
522
523 if (select && (m_pagesData.GetCount() > 1))
524 {
525 SetSelection( position );
526 }
527
528 InvalidateBestSize();
529 return true;
530 }
531
532 // helper for HitTest(): check if the point lies inside the given widget which
533 // is the child of the notebook whose position and border size are passed as
534 // parameters
535 static bool
536 IsPointInsideWidget(const wxPoint& pt, GtkWidget *w,
537 gint x, gint y, gint border = 0)
538 {
539 return
540 (pt.x >= w->allocation.x - x - border) &&
541 (pt.x <= w->allocation.x - x + border + w->allocation.width) &&
542 (pt.y >= w->allocation.y - y - border) &&
543 (pt.y <= w->allocation.y - y + border + w->allocation.height);
544 }
545
546 int wxNotebook::HitTest(const wxPoint& pt, long *flags) const
547 {
548 const gint x = m_widget->allocation.x;
549 const gint y = m_widget->allocation.y;
550
551 const size_t count = GetPageCount();
552 size_t i = 0;
553
554 GtkNotebook * notebook = GTK_NOTEBOOK(m_widget);
555 if (gtk_notebook_get_scrollable(notebook))
556 i = g_list_position( notebook->children, notebook->first_tab );
557
558 for ( ; i < count; i++ )
559 {
560 wxGtkNotebookPage* nb_page = GetNotebookPage(i);
561 GtkWidget *box = nb_page->m_box;
562
563 const gint border = gtk_container_get_border_width(GTK_CONTAINER(box));
564
565 if ( IsPointInsideWidget(pt, box, x, y, border) )
566 {
567 // ok, we're inside this tab -- now find out where, if needed
568 if ( flags )
569 {
570 GtkWidget *pixmap = NULL;
571
572 GList *children = gtk_container_get_children(GTK_CONTAINER(box));
573 for ( GList *child = children; child; child = child->next )
574 {
575 if (GTK_IS_IMAGE(child->data))
576 {
577 pixmap = GTK_WIDGET(child->data);
578 break;
579 }
580 }
581
582 if ( children )
583 g_list_free(children);
584
585 if ( pixmap && IsPointInsideWidget(pt, pixmap, x, y) )
586 {
587 *flags = wxBK_HITTEST_ONICON;
588 }
589 else if ( IsPointInsideWidget(pt, GTK_WIDGET(nb_page->m_label), x, y) )
590 {
591 *flags = wxBK_HITTEST_ONLABEL;
592 }
593 else
594 {
595 *flags = wxBK_HITTEST_ONITEM;
596 }
597 }
598
599 return i;
600 }
601 }
602
603 if ( flags )
604 {
605 *flags = wxBK_HITTEST_NOWHERE;
606 wxWindowBase * page = GetCurrentPage();
607 if ( page )
608 {
609 // rect origin is in notebook's parent coordinates
610 wxRect rect = page->GetRect();
611
612 // adjust it to the notebook's coordinates
613 wxPoint pos = GetPosition();
614 rect.x -= pos.x;
615 rect.y -= pos.y;
616 if ( rect.Contains( pt ) )
617 *flags |= wxBK_HITTEST_ONPAGE;
618 }
619 }
620
621 return wxNOT_FOUND;
622 }
623
624 void wxNotebook::OnNavigationKey(wxNavigationKeyEvent& event)
625 {
626 if (event.IsWindowChange())
627 AdvanceSelection( event.GetDirection() );
628 else
629 event.Skip();
630 }
631
632 #if wxUSE_CONSTRAINTS
633
634 // override these 2 functions to do nothing: everything is done in OnSize
635 void wxNotebook::SetConstraintSizes( bool WXUNUSED(recurse) )
636 {
637 // don't set the sizes of the pages - their correct size is not yet known
638 wxControl::SetConstraintSizes(false);
639 }
640
641 bool wxNotebook::DoPhase( int WXUNUSED(nPhase) )
642 {
643 return true;
644 }
645
646 #endif
647
648 void wxNotebook::DoApplyWidgetStyle(GtkRcStyle *style)
649 {
650 gtk_widget_modify_style(m_widget, style);
651 size_t cnt = m_pagesData.GetCount();
652 for (size_t i = 0; i < cnt; i++)
653 gtk_widget_modify_style(GTK_WIDGET(GetNotebookPage(i)->m_label), style);
654 }
655
656 GdkWindow *wxNotebook::GTKGetWindow(wxArrayGdkWindows& windows) const
657 {
658 windows.push_back(m_widget->window);
659 windows.push_back(GTK_NOTEBOOK(m_widget)->event_window);
660
661 return NULL;
662 }
663
664 // static
665 wxVisualAttributes
666 wxNotebook::GetClassDefaultAttributes(wxWindowVariant WXUNUSED(variant))
667 {
668 return GetDefaultAttributesFromGTKWidget(gtk_notebook_new);
669 }
670
671 #endif