Add wxTEST_DIALOG for testing of modal dialogs.
[wxWidgets.git] / src / motif / toolbar.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 // Name: src/motif/toolbar.cpp
3 // Purpose: wxToolBar
4 // Author: Julian Smart
5 // Modified by: 13.12.99 by VZ during toolbar classes reorganization
6 // Created: 04/01/98
7 // RCS-ID: $Id$
8 // Copyright: (c) Julian Smart
9 // Licence: wxWindows licence
10 /////////////////////////////////////////////////////////////////////////////
11
12 // ============================================================================
13 // declarations
14 // ============================================================================
15
16 // ----------------------------------------------------------------------------
17 // headers
18 // ----------------------------------------------------------------------------
19
20 // For compilers that support precompilation, includes "wx.h".
21 #include "wx/wxprec.h"
22
23 #include "wx/toolbar.h"
24
25 #ifndef WX_PRECOMP
26 #include "wx/app.h"
27 #include "wx/frame.h"
28 #include "wx/timer.h"
29 #include "wx/settings.h"
30 #endif
31
32 #ifdef __VMS__
33 #pragma message disable nosimpint
34 #endif
35 #include <Xm/Xm.h>
36 #include <Xm/PushBG.h>
37 #include <Xm/PushB.h>
38 #include <Xm/Label.h>
39 #include <Xm/ToggleB.h>
40 #include <Xm/ToggleBG.h>
41 #include <Xm/Form.h>
42 #ifdef __VMS__
43 #pragma message enable nosimpint
44 #endif
45
46 #include "wx/motif/private.h"
47 #include "wx/motif/bmpmotif.h"
48
49 // ----------------------------------------------------------------------------
50 // wxWin macros
51 // ----------------------------------------------------------------------------
52
53 IMPLEMENT_DYNAMIC_CLASS(wxToolBar, wxControl)
54
55 // ----------------------------------------------------------------------------
56 // private functions
57 // ----------------------------------------------------------------------------
58
59 static void wxToolButtonCallback (Widget w, XtPointer clientData,
60 XtPointer ptr);
61 static void wxToolButtonPopupCallback (Widget w, XtPointer client_data,
62 XEvent *event, Boolean *continue_to_dispatch);
63
64 // ----------------------------------------------------------------------------
65 // private classes
66 // ----------------------------------------------------------------------------
67
68 class wxToolBarTimer : public wxTimer
69 {
70 public:
71 virtual void Notify();
72
73 static Widget help_popup;
74 static Widget buttonWidget;
75 static wxString helpString;
76 };
77
78 class wxToolBarTool : public wxToolBarToolBase
79 {
80 public:
81 wxToolBarTool(wxToolBar *tbar,
82 int id,
83 const wxString& label,
84 const wxBitmap& bmpNormal,
85 const wxBitmap& bmpToggled,
86 wxItemKind kind,
87 wxObject *clientData,
88 const wxString& shortHelp,
89 const wxString& longHelp)
90 : wxToolBarToolBase(tbar, id, label, bmpNormal, bmpToggled, kind,
91 clientData, shortHelp, longHelp)
92 {
93 Init();
94 }
95
96 wxToolBarTool(wxToolBar *tbar, wxControl *control, const wxString& label)
97 : wxToolBarToolBase(tbar, control, label)
98 {
99 Init();
100 }
101
102 virtual ~wxToolBarTool();
103
104 // accessors
105 void SetWidget(Widget widget) { m_widget = widget; }
106 Widget GetButtonWidget() const { return m_widget; }
107
108 Pixmap GetArmPixmap()
109 {
110 m_bitmapCache.SetBitmap( GetNormalBitmap() );
111 return (Pixmap)m_bitmapCache.GetArmPixmap( (WXWidget)m_widget );
112 }
113
114 Pixmap GetInsensPixmap()
115 {
116 m_bitmapCache.SetBitmap( GetNormalBitmap() );
117 return (Pixmap)m_bitmapCache.GetInsensPixmap( (WXWidget)m_widget );
118 }
119 protected:
120 void Init();
121
122 Widget m_widget;
123 wxBitmapCache m_bitmapCache;
124 };
125
126 // ----------------------------------------------------------------------------
127 // globals
128 // ----------------------------------------------------------------------------
129
130 static wxToolBarTimer* wxTheToolBarTimer = NULL;
131
132 Widget wxToolBarTimer::help_popup = (Widget) 0;
133 Widget wxToolBarTimer::buttonWidget = (Widget) 0;
134 wxString wxToolBarTimer::helpString;
135
136 // ============================================================================
137 // implementation
138 // ============================================================================
139
140 // ----------------------------------------------------------------------------
141 // wxToolBarTool
142 // ----------------------------------------------------------------------------
143
144 wxToolBarToolBase *wxToolBar::CreateTool(int id,
145 const wxString& label,
146 const wxBitmap& bmpNormal,
147 const wxBitmap& bmpToggled,
148 wxItemKind kind,
149 wxObject *clientData,
150 const wxString& shortHelp,
151 const wxString& longHelp)
152 {
153 return new wxToolBarTool(this, id, label, bmpNormal, bmpToggled, kind,
154 clientData, shortHelp, longHelp);
155 }
156
157
158 wxToolBarToolBase *
159 wxToolBar::CreateTool(wxControl *control, const wxString& label)
160 {
161 return new wxToolBarTool(this, control, label);
162 }
163
164 void wxToolBarTool::Init()
165 {
166 m_widget = (Widget)0;
167 }
168
169 wxToolBarTool::~wxToolBarTool()
170 {
171 if ( m_widget )
172 XtDestroyWidget(m_widget);
173 }
174
175 // ----------------------------------------------------------------------------
176 // wxToolBar construction
177 // ----------------------------------------------------------------------------
178
179 void wxToolBar::Init()
180 {
181 m_maxWidth = -1;
182 m_maxHeight = -1;
183 m_defaultWidth = 24;
184 m_defaultHeight = 22;
185 m_toolPacking = 2;
186 m_toolSeparation = 8;
187 m_xMargin = 2;
188 m_yMargin = 2;
189 m_maxRows = 100;
190 m_maxCols = 100;
191 }
192
193 bool wxToolBar::Create(wxWindow *parent,
194 wxWindowID id,
195 const wxPoint& pos,
196 const wxSize& size,
197 long style,
198 const wxString& name)
199 {
200 if( !wxControl::CreateControl( parent, id, pos, size, style,
201 wxDefaultValidator, name ) )
202 return false;
203 PreCreation();
204
205 FixupStyle();
206
207 Widget parentWidget = (Widget) parent->GetClientWidget();
208
209 Widget toolbar = XtVaCreateManagedWidget("toolbar",
210 xmBulletinBoardWidgetClass, (Widget) parentWidget,
211 XmNmarginWidth, 0,
212 XmNmarginHeight, 0,
213 XmNresizePolicy, XmRESIZE_NONE,
214 NULL);
215 /*
216 Widget toolbar = XtVaCreateManagedWidget("toolbar",
217 xmFormWidgetClass, (Widget) m_clientWidget,
218 XmNtraversalOn, False,
219 XmNhorizontalSpacing, 0,
220 XmNverticalSpacing, 0,
221 XmNleftOffset, 0,
222 XmNrightOffset, 0,
223 XmNmarginWidth, 0,
224 XmNmarginHeight, 0,
225 NULL);
226 */
227
228 m_mainWidget = (WXWidget) toolbar;
229
230 wxPoint rPos = pos;
231 wxSize rSize = size;
232
233 if( rPos.x == -1 ) rPos.x = 0;
234 if( rPos.y == -1 ) rPos.y = 0;
235 if( rSize.x == -1 && GetParent() )
236 rSize.x = GetParent()->GetSize().x;
237
238 PostCreation();
239 AttachWidget (parent, m_mainWidget, (WXWidget) NULL,
240 rPos.x, rPos.y, rSize.x, rSize.y);
241
242 return true;
243 }
244
245 wxToolBar::~wxToolBar()
246 {
247 wxDELETE(wxTheToolBarTimer);
248 }
249
250 bool wxToolBar::Realize()
251 {
252 if ( m_tools.GetCount() == 0 )
253 {
254 // nothing to do
255 return true;
256 }
257
258 bool isVertical = GetWindowStyle() & wxTB_VERTICAL;
259
260 // Separator spacing
261 const int separatorSize = GetToolSeparation(); // 8;
262 wxSize margins = GetToolMargins();
263 int packing = GetToolPacking();
264 int marginX = margins.x;
265 int marginY = margins.y;
266
267 int currentX = marginX;
268 int currentY = marginY;
269
270 int buttonHeight = 0, buttonWidth = 0;
271
272 Widget button;
273 Pixmap pixmap, insensPixmap;
274 wxBitmap bmp, insensBmp;
275
276 wxToolBarToolsList::compatibility_iterator node = m_tools.GetFirst();
277 while ( node )
278 {
279 wxToolBarTool *tool = (wxToolBarTool *)node->GetData();
280
281 switch ( tool->GetStyle() )
282 {
283 case wxTOOL_STYLE_CONTROL:
284 {
285 wxControl* control = tool->GetControl();
286 wxSize sz = control->GetSize();
287 wxPoint pos = control->GetPosition();
288 // Allow a control to specify a y[x]-offset by setting
289 // its initial position, but still don't allow it to
290 // position itself above the top[left] margin.
291 int controlY = (pos.y > 0) ? pos.y : currentY;
292 int controlX = (pos.x > 0) ? pos.x : currentX;
293 control->Move( isVertical ? controlX : currentX,
294 isVertical ? currentY : controlY );
295 if ( isVertical )
296 currentY += sz.y + packing;
297 else
298 currentX += sz.x + packing;
299
300 break;
301 }
302 case wxTOOL_STYLE_SEPARATOR:
303 // skip separators for vertical toolbars
304 if( !isVertical )
305 {
306 currentX += separatorSize;
307 }
308 break;
309
310 case wxTOOL_STYLE_BUTTON:
311 button = (Widget) 0;
312
313 if ( tool->CanBeToggled() && !tool->GetButtonWidget() )
314 {
315 button = XtVaCreateWidget("toggleButton",
316 xmToggleButtonWidgetClass, (Widget) m_mainWidget,
317 XmNx, currentX, XmNy, currentY,
318 XmNindicatorOn, False,
319 XmNshadowThickness, 2,
320 XmNborderWidth, 0,
321 XmNspacing, 0,
322 XmNmarginWidth, 0,
323 XmNmarginHeight, 0,
324 XmNmultiClick, XmMULTICLICK_KEEP,
325 XmNlabelType, XmPIXMAP,
326 NULL);
327 XtAddCallback ((Widget) button,
328 XmNvalueChangedCallback,
329 (XtCallbackProc) wxToolButtonCallback,
330 (XtPointer) this);
331
332 XtVaSetValues ((Widget) button,
333 XmNselectColor,
334 m_backgroundColour.AllocColour
335 (XtDisplay((Widget) button)),
336 NULL);
337 }
338 else if( !tool->GetButtonWidget() )
339 {
340 button = XtVaCreateWidget("button",
341 xmPushButtonWidgetClass, (Widget) m_mainWidget,
342 XmNx, currentX, XmNy, currentY,
343 XmNpushButtonEnabled, True,
344 XmNmultiClick, XmMULTICLICK_KEEP,
345 XmNlabelType, XmPIXMAP,
346 NULL);
347 XtAddCallback (button,
348 XmNactivateCallback,
349 (XtCallbackProc) wxToolButtonCallback,
350 (XtPointer) this);
351 }
352
353 if( !tool->GetButtonWidget() )
354 {
355 wxDoChangeBackgroundColour((WXWidget) button,
356 m_backgroundColour, true);
357
358 tool->SetWidget(button);
359 }
360 else
361 {
362 button = (Widget)tool->GetButtonWidget();
363 XtVaSetValues( button,
364 XmNx, currentX, XmNy, currentY,
365 NULL );
366 }
367
368 // For each button, if there is a mask, we must create
369 // a new wxBitmap that has the correct background colour
370 // for the button. Otherwise the background will just be
371 // e.g. black if a transparent XPM has been loaded.
372 bmp = tool->GetNormalBitmap();
373 insensBmp = tool->GetDisabledBitmap();
374 if ( bmp.GetMask() || insensBmp.GetMask() )
375 {
376 WXPixel backgroundPixel;
377 XtVaGetValues(button, XmNbackground, &backgroundPixel,
378 NULL);
379
380 wxColour col;
381 col.SetPixel(backgroundPixel);
382
383 if( bmp.IsOk() && bmp.GetMask() )
384 {
385 bmp = wxCreateMaskedBitmap(bmp, col);
386 tool->SetNormalBitmap(bmp);
387 }
388
389 if( insensBmp.IsOk() && insensBmp.GetMask() )
390 {
391 insensBmp = wxCreateMaskedBitmap(insensBmp, col);
392 tool->SetDisabledBitmap(insensBmp);
393 }
394 }
395
396 // Create a selected/toggled bitmap. If there isn't a 2nd
397 // bitmap, we need to create it (with a darker, selected
398 // background)
399 WXPixel backgroundPixel;
400 if ( tool->CanBeToggled() )
401 XtVaGetValues(button, XmNselectColor, &backgroundPixel,
402 NULL);
403 else
404 XtVaGetValues(button, XmNarmColor, &backgroundPixel,
405 NULL);
406 wxColour col;
407 col.SetPixel(backgroundPixel);
408
409 pixmap = (Pixmap) bmp.GetDrawable();
410 {
411 wxBitmap tmp = tool->GetDisabledBitmap();
412
413 insensPixmap = tmp.IsOk() ?
414 (Pixmap)tmp.GetDrawable() :
415 tool->GetInsensPixmap();
416 }
417
418 if (tool->CanBeToggled())
419 {
420 // Toggle button
421 Pixmap pixmap2 = tool->GetArmPixmap();
422 Pixmap insensPixmap2 = tool->GetInsensPixmap();
423
424 XtVaSetValues (button,
425 XmNfillOnSelect, True,
426 XmNlabelPixmap, pixmap,
427 XmNselectPixmap, pixmap2,
428 XmNlabelInsensitivePixmap, insensPixmap,
429 XmNselectInsensitivePixmap, insensPixmap2,
430 XmNlabelType, XmPIXMAP,
431 NULL);
432 }
433 else
434 {
435 Pixmap pixmap2 = tool->GetArmPixmap();
436
437 // Normal button
438 XtVaSetValues(button,
439 XmNlabelPixmap, pixmap,
440 XmNlabelInsensitivePixmap, insensPixmap,
441 XmNarmPixmap, pixmap2,
442 NULL);
443 }
444
445 XtManageChild(button);
446
447 {
448 Dimension width, height;
449 XtVaGetValues(button,
450 XmNwidth, &width,
451 XmNheight, & height,
452 NULL);
453 if ( isVertical )
454 currentY += height + packing;
455 else
456 currentX += width + packing;
457 buttonHeight = wxMax(buttonHeight, height);
458 buttonWidth = wxMax(buttonWidth, width);
459 }
460
461 XtAddEventHandler (button, EnterWindowMask | LeaveWindowMask,
462 False, wxToolButtonPopupCallback, (XtPointer) this);
463
464 break;
465 }
466
467 node = node->GetNext();
468 }
469
470 SetSize( -1, -1,
471 isVertical ? buttonWidth + 2 * marginX : -1,
472 isVertical ? -1 : buttonHeight + 2*marginY );
473
474 return true;
475 }
476
477 wxToolBarToolBase *wxToolBar::FindToolForPosition(wxCoord WXUNUSED(x),
478 wxCoord WXUNUSED(y)) const
479 {
480 wxFAIL_MSG( wxT("TODO") );
481
482 return NULL;
483 }
484
485 bool wxToolBar::DoInsertTool(size_t WXUNUSED(pos), wxToolBarToolBase *tool)
486 {
487 tool->Attach(this);
488
489 return true;
490 }
491
492 bool wxToolBar::DoDeleteTool(size_t WXUNUSED(pos), wxToolBarToolBase *tool)
493 {
494 tool->Detach();
495
496 bool isVertical = GetWindowStyle() & wxTB_VERTICAL;
497 const int separatorSize = GetToolSeparation(); // 8;
498 int packing = GetToolPacking();
499 int offset = 0;
500
501 for( wxToolBarToolsList::compatibility_iterator node = m_tools.GetFirst();
502 node; node = node->GetNext() )
503 {
504 wxToolBarTool *t = (wxToolBarTool*)node->GetData();
505
506 if( t == tool )
507 {
508 switch ( t->GetStyle() )
509 {
510 case wxTOOL_STYLE_CONTROL:
511 {
512 wxSize size = t->GetControl()->GetSize();
513 offset = isVertical ? size.y : size.x;
514 offset += packing;
515 break;
516 }
517 case wxTOOL_STYLE_SEPARATOR:
518 offset = isVertical ? 0 : separatorSize;
519 break;
520 case wxTOOL_STYLE_BUTTON:
521 {
522 Widget w = t->GetButtonWidget();
523 Dimension width, height;
524
525 XtVaGetValues( w,
526 XmNwidth, &width,
527 XmNheight, &height,
528 NULL );
529
530 offset = isVertical ? height : width;
531 offset += packing;
532 break;
533 }
534 }
535 }
536 else if( offset )
537 {
538 switch ( t->GetStyle() )
539 {
540 case wxTOOL_STYLE_CONTROL:
541 {
542 wxPoint location = t->GetControl()->GetPosition();
543
544 if( isVertical )
545 location.y -= offset;
546 else
547 location.x -= offset;
548
549 t->GetControl()->Move( location );
550 break;
551 }
552 case wxTOOL_STYLE_SEPARATOR:
553 break;
554 case wxTOOL_STYLE_BUTTON:
555 {
556 Dimension x, y;
557 XtVaGetValues( t->GetButtonWidget(),
558 XmNx, &x,
559 XmNy, &y,
560 NULL );
561
562 if( isVertical )
563 y = (Dimension)(y - offset);
564 else
565 x = (Dimension)(x - offset);
566
567 XtVaSetValues( t->GetButtonWidget(),
568 XmNx, x,
569 XmNy, y,
570 NULL );
571 break;
572 }
573 }
574 }
575 }
576
577 return true;
578 }
579
580 void wxToolBar::DoEnableTool(wxToolBarToolBase *toolBase, bool enable)
581 {
582 wxToolBarTool *tool = (wxToolBarTool *)toolBase;
583 if (tool->GetButtonWidget()){
584 XtSetSensitive(tool->GetButtonWidget(), (Boolean) enable);
585 } else if (wxTOOL_STYLE_CONTROL == tool->GetStyle()){
586 // Controls (such as wxChoice) do not have button widgets
587 tool->GetControl()->Enable(enable);
588 }
589 }
590
591 void wxToolBar::DoToggleTool(wxToolBarToolBase *toolBase, bool toggle)
592 {
593 wxToolBarTool *tool = (wxToolBarTool *)toolBase;
594
595 XmToggleButtonSetState(tool->GetButtonWidget(), (Boolean) toggle, False);
596 }
597
598 void wxToolBar::DoSetToggle(wxToolBarToolBase * WXUNUSED(tool),
599 bool WXUNUSED(toggle))
600 {
601 // nothing to do
602 }
603
604 void wxToolBar::DoSetSize(int x, int y, int width, int height, int sizeFlags)
605 {
606 int old_width, old_height;
607 GetSize(&old_width, &old_height);
608
609 // Correct width and height if needed.
610 if ( width == -1 || height == -1 )
611 {
612 wxSize defaultSize = GetSize();
613
614 if ( width == -1 )
615 width = defaultSize.x;
616 if ( height == -1 )
617 height = defaultSize.y;
618 }
619
620 wxToolBarBase::DoSetSize(x, y, width, height, sizeFlags);
621
622 // We must refresh the frame size when the toolbar changes size
623 // otherwise the toolbar can be shown incorrectly
624 if ( old_width != width || old_height != height )
625 {
626 // But before we send the size event check it
627 // we have a frame that is not being deleted.
628 wxFrame *frame = wxDynamicCast(GetParent(), wxFrame);
629 if ( frame && !frame->IsBeingDeleted() )
630 {
631 frame->SendSizeEvent();
632 }
633 }
634 }
635
636 // ----------------------------------------------------------------------------
637 // Motif callbacks
638 // ----------------------------------------------------------------------------
639
640 wxToolBarToolBase *wxToolBar::FindToolByWidget(WXWidget w) const
641 {
642 wxToolBarToolsList::compatibility_iterator node = m_tools.GetFirst();
643 while ( node )
644 {
645 wxToolBarTool *tool = (wxToolBarTool *)node->GetData();
646 if ( tool->GetButtonWidget() == w)
647 {
648 return tool;
649 }
650
651 node = node->GetNext();
652 }
653
654 return NULL;
655 }
656
657 static void wxToolButtonCallback(Widget w,
658 XtPointer clientData,
659 XtPointer WXUNUSED(ptr))
660 {
661 wxToolBar *toolBar = (wxToolBar *) clientData;
662 wxToolBarToolBase *tool = toolBar->FindToolByWidget((WXWidget) w);
663 if ( !tool )
664 return;
665
666 if ( tool->CanBeToggled() )
667 tool->Toggle();
668
669 if ( !toolBar->OnLeftClick(tool->GetId(), tool->IsToggled()) )
670 {
671 // revert
672 tool->Toggle();
673 }
674 }
675
676
677 static void wxToolButtonPopupCallback(Widget w,
678 XtPointer client_data,
679 XEvent *event,
680 Boolean *WXUNUSED(continue_to_dispatch))
681 {
682 // TODO: retrieve delay before popping up tooltip from wxSystemSettings.
683 static const int delayMilli = 800;
684
685 wxToolBar* toolBar = (wxToolBar*) client_data;
686 wxToolBarToolBase *tool = toolBar->FindToolByWidget((WXWidget) w);
687
688 if ( !tool )
689 return;
690
691 wxString tooltip = tool->GetShortHelp();
692 if ( !tooltip )
693 return;
694
695 if (!wxTheToolBarTimer)
696 wxTheToolBarTimer = new wxToolBarTimer;
697
698 wxToolBarTimer::buttonWidget = w;
699 wxToolBarTimer::helpString = tooltip;
700
701 /************************************************************/
702 /* Popup help label */
703 /************************************************************/
704 if (event->type == EnterNotify)
705 {
706 if (wxToolBarTimer::help_popup != (Widget) 0)
707 {
708 XtDestroyWidget (wxToolBarTimer::help_popup);
709 XtPopdown (wxToolBarTimer::help_popup);
710 }
711 wxToolBarTimer::help_popup = (Widget) 0;
712
713 // One shot
714 wxTheToolBarTimer->Start(delayMilli, true);
715
716 }
717 /************************************************************/
718 /* Popdown help label */
719 /************************************************************/
720 else if (event->type == LeaveNotify)
721 {
722 if (wxTheToolBarTimer)
723 wxTheToolBarTimer->Stop();
724 if (wxToolBarTimer::help_popup != (Widget) 0)
725 {
726 XtDestroyWidget (wxToolBarTimer::help_popup);
727 XtPopdown (wxToolBarTimer::help_popup);
728 }
729 wxToolBarTimer::help_popup = (Widget) 0;
730 }
731 }
732
733 void wxToolBarTimer::Notify()
734 {
735 Position x, y;
736
737 /************************************************************/
738 /* Create shell without window decorations */
739 /************************************************************/
740 help_popup = XtVaCreatePopupShell ("shell",
741 overrideShellWidgetClass, (Widget) wxTheApp->GetTopLevelWidget(),
742 NULL);
743
744 /************************************************************/
745 /* Get absolute position on display of toolbar button */
746 /************************************************************/
747 XtTranslateCoords (buttonWidget,
748 (Position) 0,
749 (Position) 0,
750 &x, &y);
751
752 // Move the tooltip more or less above the button
753 int yOffset = 20; // TODO: What should be really?
754 y = (Position)(y - yOffset);
755 if (y < yOffset) y = 0;
756
757 /************************************************************/
758 /* Set the position of the help popup */
759 /************************************************************/
760 XtVaSetValues (help_popup,
761 XmNx, (Position) x,
762 XmNy, (Position) y,
763 NULL);
764
765 /************************************************************/
766 /* Create help label */
767 /************************************************************/
768 XmString text = XmStringCreateSimple ((char*) (const char*) helpString);
769 XtVaCreateManagedWidget ("help_label",
770 xmLabelWidgetClass, help_popup,
771 XmNlabelString, text,
772 XtVaTypedArg,
773 XmNforeground, XtRString, "black",
774 strlen("black")+1,
775 XtVaTypedArg,
776 XmNbackground, XtRString, "LightGoldenrod",
777 strlen("LightGoldenrod")+1,
778 NULL);
779 XmStringFree (text);
780
781 /************************************************************/
782 /* Popup help label */
783 /************************************************************/
784 XtPopup (help_popup, XtGrabNone);
785 }