1 /** @file
2 * @brief Widget that listens and modifies repr attributes
3 */
4 /* Authors:
5 * Lauris Kaplinski <lauris@ximian.com>
6 * Abhishek Sharma
7 *
8 * Copyright (C) 2001 Ximian, Inc.
9 * Released under GNU GPL, read the file 'COPYING' for more information
10 */
12 #include <gtk/gtktable.h>
13 #include <gtk/gtklabel.h>
14 #include "xml/repr.h"
15 #include "macros.h"
16 #include "document.h"
17 #include "sp-object.h"
18 #include <glibmm/i18n.h>
20 #include <sigc++/functors/ptr_fun.h>
21 #include <sigc++/adaptors/bind.h>
23 #include "sp-attribute-widget.h"
25 using Inkscape::DocumentUndo;
27 static void sp_attribute_widget_class_init (SPAttributeWidgetClass *klass);
28 static void sp_attribute_widget_init (SPAttributeWidget *widget);
29 static void sp_attribute_widget_destroy (GtkObject *object);
31 static void sp_attribute_widget_changed (GtkEditable *editable);
33 static void sp_attribute_widget_object_modified ( SPObject *object,
34 guint flags,
35 SPAttributeWidget *spaw );
36 static void sp_attribute_widget_object_release ( SPObject *object,
37 SPAttributeWidget *spaw );
39 static GtkEntryClass *parent_class;
44 GType sp_attribute_widget_get_type(void)
45 {
46 static GtkType type = 0;
47 if (!type) {
48 GTypeInfo info = {
49 sizeof(SPAttributeWidgetClass),
50 0, // base_init
51 0, // base_finalize
52 (GClassInitFunc)sp_attribute_widget_class_init,
53 0, // class_finalize
54 0, // class_data
55 sizeof(SPAttributeWidget),
56 0, // n_preallocs
57 (GInstanceInitFunc)sp_attribute_widget_init,
58 0 // value_table
59 };
60 type = g_type_register_static(GTK_TYPE_ENTRY, "SPAttributeWidget", &info, static_cast<GTypeFlags>(0));
61 }
62 return type;
63 } // end of sp_attribute_widget_get_type()
67 static void
68 sp_attribute_widget_class_init (SPAttributeWidgetClass *klass)
69 {
70 GtkObjectClass *object_class;
71 GtkWidgetClass *widget_class;
72 GtkEditableClass *editable_class;
74 object_class = GTK_OBJECT_CLASS (klass);
75 widget_class = GTK_WIDGET_CLASS (klass);
76 editable_class = GTK_EDITABLE_CLASS (klass);
78 parent_class = (GtkEntryClass*)gtk_type_class (GTK_TYPE_ENTRY);
80 object_class->destroy = sp_attribute_widget_destroy;
82 editable_class->changed = sp_attribute_widget_changed;
84 } // end of sp_attribute_widget_class_init()
88 static void
89 sp_attribute_widget_init (SPAttributeWidget *spaw)
90 {
91 spaw->blocked = FALSE;
92 spaw->hasobj = FALSE;
94 spaw->src.object = NULL;
96 spaw->attribute = NULL;
98 new (&spaw->modified_connection) sigc::connection();
99 new (&spaw->release_connection) sigc::connection();
100 }
104 static void
105 sp_attribute_widget_destroy (GtkObject *object)
106 {
108 SPAttributeWidget *spaw;
110 spaw = SP_ATTRIBUTE_WIDGET (object);
112 if (spaw->attribute) {
113 g_free (spaw->attribute);
114 spaw->attribute = NULL;
115 }
118 if (spaw->hasobj) {
120 if (spaw->src.object) {
121 spaw->modified_connection.disconnect();
122 spaw->release_connection.disconnect();
123 spaw->src.object = NULL;
124 }
125 } else {
127 if (spaw->src.repr) {
128 spaw->src.repr = Inkscape::GC::release(spaw->src.repr);
129 }
130 } // end of if()
132 spaw->modified_connection.~connection();
133 spaw->release_connection.~connection();
135 ((GtkObjectClass *) parent_class)->destroy (object);
137 }
141 static void
142 sp_attribute_widget_changed (GtkEditable *editable)
143 {
145 SPAttributeWidget *spaw;
147 spaw = SP_ATTRIBUTE_WIDGET (editable);
149 if (!spaw->blocked) {
151 const gchar *text;
152 spaw->blocked = TRUE;
153 text = gtk_entry_get_text (GTK_ENTRY (spaw));
154 if (!*text)
155 text = NULL;
157 if (spaw->hasobj && spaw->src.object) {
159 SP_OBJECT_REPR (spaw->src.object)->setAttribute(spaw->attribute, text, false);
160 DocumentUndo::done(SP_OBJECT_DOCUMENT (spaw->src.object), SP_VERB_NONE,
161 _("Set attribute"));
163 } else if (spaw->src.repr) {
165 spaw->src.repr->setAttribute(spaw->attribute, text, false);
166 /* TODO: Warning! Undo will not be flushed in given case */
167 }
168 spaw->blocked = FALSE;
169 }
171 } // end of sp_attribute_widget_changed()
175 GtkWidget *
176 sp_attribute_widget_new ( SPObject *object, const gchar *attribute )
177 {
178 SPAttributeWidget *spaw;
180 g_return_val_if_fail (!object || SP_IS_OBJECT (object), NULL);
181 g_return_val_if_fail (!object || attribute, NULL);
183 spaw = (SPAttributeWidget*)gtk_type_new (SP_TYPE_ATTRIBUTE_WIDGET);
185 sp_attribute_widget_set_object (spaw, object, attribute);
187 return GTK_WIDGET (spaw);
189 } // end of sp_attribute_widget_new()
193 GtkWidget *
194 sp_attribute_widget_new_repr ( Inkscape::XML::Node *repr, const gchar *attribute )
195 {
196 SPAttributeWidget *spaw;
198 spaw = (SPAttributeWidget*)gtk_type_new (SP_TYPE_ATTRIBUTE_WIDGET);
200 sp_attribute_widget_set_repr (spaw, repr, attribute);
202 return GTK_WIDGET (spaw);
203 }
207 void
208 sp_attribute_widget_set_object ( SPAttributeWidget *spaw,
209 SPObject *object,
210 const gchar *attribute )
211 {
213 g_return_if_fail (spaw != NULL);
214 g_return_if_fail (SP_IS_ATTRIBUTE_WIDGET (spaw));
215 g_return_if_fail (!object || SP_IS_OBJECT (object));
216 g_return_if_fail (!object || attribute);
217 g_return_if_fail (attribute != NULL);
219 if (spaw->attribute) {
220 g_free (spaw->attribute);
221 spaw->attribute = NULL;
222 }
224 if (spaw->hasobj) {
226 if (spaw->src.object) {
227 spaw->modified_connection.disconnect();
228 spaw->release_connection.disconnect();
229 spaw->src.object = NULL;
230 }
231 } else {
233 if (spaw->src.repr) {
234 spaw->src.repr = Inkscape::GC::release(spaw->src.repr);
235 }
236 }
238 spaw->hasobj = TRUE;
240 if (object) {
241 const gchar *val;
243 spaw->blocked = TRUE;
244 spaw->src.object = object;
246 spaw->modified_connection = object->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_attribute_widget_object_modified), spaw));
247 spaw->release_connection = object->connectRelease(sigc::bind<1>(sigc::ptr_fun(&sp_attribute_widget_object_release), spaw));
249 spaw->attribute = g_strdup (attribute);
251 val = SP_OBJECT_REPR (object)->attribute(attribute);
252 gtk_entry_set_text (GTK_ENTRY (spaw), val ? val : (const gchar *) "");
253 spaw->blocked = FALSE;
254 }
256 gtk_widget_set_sensitive (GTK_WIDGET (spaw), (spaw->src.object != NULL));
258 } // end of sp_attribute_widget_set_object()
262 void
263 sp_attribute_widget_set_repr ( SPAttributeWidget *spaw,
264 Inkscape::XML::Node *repr,
265 const gchar *attribute )
266 {
268 g_return_if_fail (spaw != NULL);
269 g_return_if_fail (SP_IS_ATTRIBUTE_WIDGET (spaw));
270 g_return_if_fail (attribute != NULL);
272 if (spaw->attribute) {
273 g_free (spaw->attribute);
274 spaw->attribute = NULL;
275 }
277 if (spaw->hasobj) {
279 if (spaw->src.object) {
280 spaw->modified_connection.disconnect();
281 spaw->release_connection.disconnect();
282 spaw->src.object = NULL;
283 }
284 } else {
286 if (spaw->src.repr) {
287 spaw->src.repr = Inkscape::GC::release(spaw->src.repr);
288 }
289 }
291 spaw->hasobj = FALSE;
293 if (repr) {
294 const gchar *val;
296 spaw->blocked = TRUE;
297 spaw->src.repr = Inkscape::GC::anchor(repr);
298 spaw->attribute = g_strdup (attribute);
300 val = repr->attribute(attribute);
301 gtk_entry_set_text (GTK_ENTRY (spaw), val ? val : (const gchar *) "");
302 spaw->blocked = FALSE;
303 }
305 gtk_widget_set_sensitive (GTK_WIDGET (spaw), (spaw->src.repr != NULL));
307 } // end of sp_attribute_widget_set_repr()
311 static void
312 sp_attribute_widget_object_modified ( SPObject */*object*/,
313 guint flags,
314 SPAttributeWidget *spaw )
315 {
317 if (flags && SP_OBJECT_MODIFIED_FLAG) {
319 const gchar *val, *text;
320 val = SP_OBJECT_REPR (spaw->src.object)->attribute(spaw->attribute);
321 text = gtk_entry_get_text (GTK_ENTRY (spaw));
323 if (val || text) {
325 if (!val || !text || strcmp (val, text)) {
326 /* We are different */
327 spaw->blocked = TRUE;
328 gtk_entry_set_text ( GTK_ENTRY (spaw),
329 val ? val : (const gchar *) "");
330 spaw->blocked = FALSE;
331 } // end of if()
333 } // end of if()
335 } //end of if()
337 } // end of sp_attribute_widget_object_modified()
341 static void
342 sp_attribute_widget_object_release ( SPObject */*object*/,
343 SPAttributeWidget *spaw )
344 {
345 sp_attribute_widget_set_object (spaw, NULL, NULL);
346 }
350 /* SPAttributeTable */
352 static void sp_attribute_table_class_init (SPAttributeTableClass *klass);
353 static void sp_attribute_table_init (SPAttributeTable *widget);
354 static void sp_attribute_table_destroy (GtkObject *object);
356 static void sp_attribute_table_object_modified (SPObject *object, guint flags, SPAttributeTable *spaw);
357 static void sp_attribute_table_object_release (SPObject *object, SPAttributeTable *spaw);
358 static void sp_attribute_table_entry_changed (GtkEditable *editable, SPAttributeTable *spat);
360 static GtkVBoxClass *table_parent_class;
365 GType sp_attribute_table_get_type(void)
366 {
367 static GtkType type = 0;
368 if (!type) {
369 GTypeInfo info = {
370 sizeof(SPAttributeTableClass),
371 0, // base_init
372 0, // base_finalize
373 (GClassInitFunc)sp_attribute_table_class_init,
374 0, // class_finalize
375 0, // class_data
376 sizeof(SPAttributeTable),
377 0, // n_preallocs
378 (GInstanceInitFunc)sp_attribute_table_init,
379 0 // value_table
380 };
381 type = g_type_register_static(GTK_TYPE_VBOX, "SPAttributeTable", &info, static_cast<GTypeFlags>(0));
382 }
383 return type;
384 } // end of sp_attribute_table_get_type()
388 static void
389 sp_attribute_table_class_init (SPAttributeTableClass *klass)
390 {
391 GtkObjectClass *object_class;
392 GtkWidgetClass *widget_class;
394 object_class = GTK_OBJECT_CLASS (klass);
395 widget_class = GTK_WIDGET_CLASS (klass);
397 table_parent_class = (GtkVBoxClass*)gtk_type_class (GTK_TYPE_VBOX);
399 object_class->destroy = sp_attribute_table_destroy;
401 } // end of sp_attribute_table_class_init()
405 static void
406 sp_attribute_table_init ( SPAttributeTable *spat )
407 {
408 spat->blocked = FALSE;
409 spat->hasobj = FALSE;
410 spat->table = NULL;
411 spat->src.object = NULL;
412 spat->num_attr = 0;
413 spat->attributes = NULL;
414 spat->entries = NULL;
416 new (&spat->modified_connection) sigc::connection();
417 new (&spat->release_connection) sigc::connection();
418 }
420 static void
421 sp_attribute_table_destroy ( GtkObject *object )
422 {
423 SPAttributeTable *spat;
425 spat = SP_ATTRIBUTE_TABLE (object);
427 if (spat->attributes) {
428 gint i;
429 for (i = 0; i < spat->num_attr; i++) {
430 g_free (spat->attributes[i]);
431 }
432 g_free (spat->attributes);
433 spat->attributes = NULL;
434 }
436 if (spat->hasobj) {
438 if (spat->src.object) {
439 spat->modified_connection.disconnect();
440 spat->release_connection.disconnect();
441 spat->src.object = NULL;
442 }
443 } else {
444 if (spat->src.repr) {
445 spat->src.repr = Inkscape::GC::release(spat->src.repr);
446 }
447 } // end of if()
449 spat->modified_connection.~connection();
450 spat->release_connection.~connection();
452 if (spat->entries) {
453 g_free (spat->entries);
454 spat->entries = NULL;
455 }
457 spat->table = NULL;
459 if (((GtkObjectClass *) table_parent_class)->destroy) {
460 (* ((GtkObjectClass *) table_parent_class)->destroy) (object);
461 }
463 } // end of sp_attribute_table_destroy()
466 GtkWidget *
467 sp_attribute_table_new ( SPObject *object,
468 gint num_attr,
469 const gchar **labels,
470 const gchar **attributes )
471 {
472 SPAttributeTable *spat;
474 g_return_val_if_fail (!object || SP_IS_OBJECT (object), NULL);
475 g_return_val_if_fail (!object || (num_attr > 0), NULL);
476 g_return_val_if_fail (!num_attr || (labels && attributes), NULL);
478 spat = (SPAttributeTable*)gtk_type_new (SP_TYPE_ATTRIBUTE_TABLE);
480 sp_attribute_table_set_object (spat, object, num_attr, labels, attributes);
482 return GTK_WIDGET (spat);
484 } // end of sp_attribute_table_new()
488 GtkWidget *
489 sp_attribute_table_new_repr ( Inkscape::XML::Node *repr,
490 gint num_attr,
491 const gchar **labels,
492 const gchar **attributes )
493 {
494 SPAttributeTable *spat;
496 g_return_val_if_fail (!num_attr || (labels && attributes), NULL);
498 spat = (SPAttributeTable*)gtk_type_new (SP_TYPE_ATTRIBUTE_TABLE);
500 sp_attribute_table_set_repr (spat, repr, num_attr, labels, attributes);
502 return GTK_WIDGET (spat);
504 } // end of sp_attribute_table_new_repr()
508 #define XPAD 4
509 #define YPAD 0
511 void
512 sp_attribute_table_set_object ( SPAttributeTable *spat,
513 SPObject *object,
514 gint num_attr,
515 const gchar **labels,
516 const gchar **attributes )
517 {
519 g_return_if_fail (spat != NULL);
520 g_return_if_fail (SP_IS_ATTRIBUTE_TABLE (spat));
521 g_return_if_fail (!object || SP_IS_OBJECT (object));
522 g_return_if_fail (!object || (num_attr > 0));
523 g_return_if_fail (!num_attr || (labels && attributes));
525 if (spat->table) {
526 gtk_widget_destroy (spat->table);
527 spat->table = NULL;
528 }
530 if (spat->attributes) {
531 gint i;
532 for (i = 0; i < spat->num_attr; i++) {
533 g_free (spat->attributes[i]);
534 }
535 g_free (spat->attributes);
536 spat->attributes = NULL;
537 }
539 if (spat->entries) {
540 g_free (spat->entries);
541 spat->entries = NULL;
542 }
544 if (spat->hasobj) {
545 if (spat->src.object) {
546 spat->modified_connection.disconnect();
547 spat->release_connection.disconnect();
548 spat->src.object = NULL;
549 }
550 } else {
551 if (spat->src.repr) {
552 spat->src.repr = Inkscape::GC::release(spat->src.repr);
553 }
554 }
556 spat->hasobj = TRUE;
558 if (object) {
559 gint i;
561 spat->blocked = TRUE;
563 /* Set up object */
564 spat->src.object = object;
565 spat->num_attr = num_attr;
567 spat->modified_connection = object->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_attribute_table_object_modified), spat));
568 spat->release_connection = object->connectRelease(sigc::bind<1>(sigc::ptr_fun(&sp_attribute_table_object_release), spat));
570 /* Create table */
571 spat->table = gtk_table_new (num_attr, 2, FALSE);
572 gtk_container_add (GTK_CONTAINER (spat), spat->table);
573 /* Arrays */
574 spat->attributes = g_new0 (gchar *, num_attr);
575 spat->entries = g_new0 (GtkWidget *, num_attr);
576 /* Fill rows */
577 for (i = 0; i < num_attr; i++) {
578 GtkWidget *w;
579 const gchar *val;
581 spat->attributes[i] = g_strdup (attributes[i]);
582 w = gtk_label_new (_(labels[i]));
583 gtk_widget_show (w);
584 gtk_misc_set_alignment (GTK_MISC (w), 1.0, 0.5);
585 gtk_table_attach ( GTK_TABLE (spat->table), w, 0, 1, i, i + 1,
586 GTK_FILL,
587 (GtkAttachOptions)(GTK_EXPAND | GTK_FILL),
588 XPAD, YPAD );
589 w = gtk_entry_new ();
590 gtk_widget_show (w);
591 val = SP_OBJECT_REPR (object)->attribute(attributes[i]);
592 gtk_entry_set_text (GTK_ENTRY (w), val ? val : (const gchar *) "");
593 gtk_table_attach ( GTK_TABLE (spat->table), w, 1, 2, i, i + 1,
594 (GtkAttachOptions)(GTK_EXPAND | GTK_FILL),
595 (GtkAttachOptions)(GTK_EXPAND | GTK_FILL),
596 XPAD, YPAD );
597 spat->entries[i] = w;
598 g_signal_connect ( G_OBJECT (w), "changed",
599 G_CALLBACK (sp_attribute_table_entry_changed),
600 spat );
601 }
602 /* Show table */
603 gtk_widget_show (spat->table);
605 spat->blocked = FALSE;
606 }
608 gtk_widget_set_sensitive ( GTK_WIDGET (spat),
609 (spat->src.object != NULL) );
611 } // end of sp_attribute_table_set_object()
615 void
616 sp_attribute_table_set_repr ( SPAttributeTable *spat,
617 Inkscape::XML::Node *repr,
618 gint num_attr,
619 const gchar **labels,
620 const gchar **attributes )
621 {
622 g_return_if_fail (spat != NULL);
623 g_return_if_fail (SP_IS_ATTRIBUTE_TABLE (spat));
624 g_return_if_fail (!num_attr || (labels && attributes));
626 if (spat->table) {
627 gtk_widget_destroy (spat->table);
628 spat->table = NULL;
629 }
631 if (spat->attributes) {
632 gint i;
633 for (i = 0; i < spat->num_attr; i++) {
634 g_free (spat->attributes[i]);
635 }
636 g_free (spat->attributes);
637 spat->attributes = NULL;
638 }
640 if (spat->entries) {
641 g_free (spat->entries);
642 spat->entries = NULL;
643 }
645 if (spat->hasobj) {
646 if (spat->src.object) {
647 spat->modified_connection.disconnect();
648 spat->release_connection.disconnect();
649 spat->src.object = NULL;
650 }
651 } else {
652 if (spat->src.repr) {
653 spat->src.repr = Inkscape::GC::release(spat->src.repr);
654 }
655 }
657 spat->hasobj = FALSE;
659 if (repr) {
660 gint i;
662 spat->blocked = TRUE;
664 /* Set up repr */
665 spat->src.repr = Inkscape::GC::anchor(repr);
666 spat->num_attr = num_attr;
667 /* Create table */
668 spat->table = gtk_table_new (num_attr, 2, FALSE);
669 gtk_container_add (GTK_CONTAINER (spat), spat->table);
670 /* Arrays */
671 spat->attributes = g_new0 (gchar *, num_attr);
672 spat->entries = g_new0 (GtkWidget *, num_attr);
674 /* Fill rows */
675 for (i = 0; i < num_attr; i++) {
676 GtkWidget *w;
677 const gchar *val;
679 spat->attributes[i] = g_strdup (attributes[i]);
680 w = gtk_label_new (labels[i]);
681 gtk_widget_show (w);
682 gtk_misc_set_alignment (GTK_MISC (w), 1.0, 0.5);
683 gtk_table_attach ( GTK_TABLE (spat->table), w, 0, 1, i, i + 1,
684 GTK_FILL,
685 (GtkAttachOptions)(GTK_EXPAND | GTK_FILL),
686 XPAD, YPAD );
687 w = gtk_entry_new ();
688 gtk_widget_show (w);
689 val = repr->attribute(attributes[i]);
690 gtk_entry_set_text (GTK_ENTRY (w), val ? val : (const gchar *) "");
691 gtk_table_attach ( GTK_TABLE (spat->table), w, 1, 2, i, i + 1,
692 (GtkAttachOptions)(GTK_EXPAND | GTK_FILL),
693 (GtkAttachOptions)(GTK_EXPAND | GTK_FILL),
694 XPAD, YPAD );
695 spat->entries[i] = w;
696 g_signal_connect ( G_OBJECT (w), "changed",
697 G_CALLBACK (sp_attribute_table_entry_changed),
698 spat );
699 }
700 /* Show table */
701 gtk_widget_show (spat->table);
703 spat->blocked = FALSE;
704 }
706 gtk_widget_set_sensitive (GTK_WIDGET (spat), (spat->src.repr != NULL));
708 } // end of sp_attribute_table_set_repr()
712 static void
713 sp_attribute_table_object_modified ( SPObject */*object*/,
714 guint flags,
715 SPAttributeTable *spat )
716 {
717 if (flags && SP_OBJECT_MODIFIED_FLAG)
718 {
719 gint i;
720 for (i = 0; i < spat->num_attr; i++) {
721 const gchar *val, *text;
722 val = SP_OBJECT_REPR (spat->src.object)->attribute(spat->attributes[i]);
723 text = gtk_entry_get_text (GTK_ENTRY (spat->entries[i]));
724 if (val || text) {
725 if (!val || !text || strcmp (val, text)) {
726 /* We are different */
727 spat->blocked = TRUE;
728 gtk_entry_set_text ( GTK_ENTRY (spat->entries[i]),
729 val ? val : (const gchar *) "");
730 spat->blocked = FALSE;
731 }
732 }
733 }
734 } // end of if()
736 } // end of sp_attribute_table_object_modified()
740 static void
741 sp_attribute_table_object_release (SPObject */*object*/, SPAttributeTable *spat)
742 {
743 sp_attribute_table_set_object (spat, NULL, 0, NULL, NULL);
744 }
748 static void
749 sp_attribute_table_entry_changed ( GtkEditable *editable,
750 SPAttributeTable *spat )
751 {
752 if (!spat->blocked)
753 {
754 gint i;
755 for (i = 0; i < spat->num_attr; i++) {
757 if (GTK_WIDGET (editable) == spat->entries[i]) {
758 const gchar *text;
759 spat->blocked = TRUE;
760 text = gtk_entry_get_text (GTK_ENTRY (spat->entries[i]));
762 if (!*text)
763 text = NULL;
765 if (spat->hasobj && spat->src.object) {
766 SP_OBJECT_REPR (spat->src.object)->setAttribute(spat->attributes[i], text, false);
767 DocumentUndo::done(SP_OBJECT_DOCUMENT (spat->src.object), SP_VERB_NONE,
768 _("Set attribute"));
770 } else if (spat->src.repr) {
772 spat->src.repr->setAttribute(spat->attributes[i], text, false);
773 /* TODO: Warning! Undo will not be flushed in given case */
774 }
775 spat->blocked = FALSE;
776 return;
777 }
778 }
779 g_warning ("file %s: line %d: Entry signalled change, but there is no such entry", __FILE__, __LINE__);
780 } // end of if()
782 } // end of sp_attribute_table_entry_changed()
784 /*
785 Local Variables:
786 mode:c++
787 c-file-style:"stroustrup"
788 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
789 indent-tabs-mode:nil
790 fill-column:99
791 End:
792 */
793 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :