173f97e87adb1464fb80119e6bcb07eec9815e5e
[rugen] / src / page_clips.c
1 /*
2 * page_clips.c -- Clips Page Handling
3 * Copyright (C) 2002-2003 Charles Yates <charles.yates@pandora.be>
4 * Copyright (C) 2010 Dan Dennedy <dan@dennedy.org>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 */
20
21 #ifdef HAVE_CONFIG_H
22 # include <config.h>
23 #endif
24
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <gtk/gtk.h>
29 #include <gdk/gdkkeysyms.h>
30
31 #include "interface.h"
32 #include "support.h"
33 #include "dv1394app.h"
34 #include "page.h"
35
36 typedef struct
37 {
38 struct page_t parent;
39 dv1394app app;
40 GtkWidget *widget;
41 mvcp dv;
42 char *path;
43 int unit;
44 int generation;
45 int clip;
46
47 // TODO: This comes out later
48 int mode;
49 GtkWidget *modes[ 4 ];
50 }
51 *page_clips, page_clips_t;
52
53 static GtkWidget *this_page_get_widget( page_clips this );
54
55 static void list_clips( page_clips this, char *path )
56 {
57 GtkWidget *treeview = lookup_widget( this_page_get_widget( this ), "list_clips" );
58 GtkWidget *dirview = lookup_widget( this_page_get_widget( this ), "list_dir" );
59 GtkWidget *label = lookup_widget( this_page_get_widget( this ), "label_directory" );
60 if ( path != NULL )
61 {
62 mvcp_dir dir = mvcp_dir_init( this->dv, path );
63 GtkListStore *dir_store = NULL;
64 GtkListStore *clip_store = NULL;
65 GtkTreeIter iter;
66 int index;
67 mvcp_dir_entry_t entry;
68
69 free( this->path );
70 this->path = strdup( path );
71
72 gtk_label_set_text( GTK_LABEL( label ), this->path );
73
74 if ( gtk_tree_view_get_model( GTK_TREE_VIEW( treeview ) ) == NULL )
75 {
76 GtkCellRenderer *renderer;
77 GtkTreeViewColumn *column;
78
79 clip_store = gtk_list_store_new( 2, G_TYPE_STRING, G_TYPE_STRING );
80 gtk_tree_view_set_model( GTK_TREE_VIEW( treeview ), GTK_TREE_MODEL( clip_store ) );
81
82 renderer = gtk_cell_renderer_text_new( );
83 column = gtk_tree_view_column_new_with_attributes ( "Description", renderer, "text", 0, NULL);
84 gtk_tree_view_column_set_sort_column_id( column, 0 );
85 gtk_tree_view_append_column( GTK_TREE_VIEW( treeview ), column );
86
87 dir_store = gtk_list_store_new( 2, G_TYPE_STRING, G_TYPE_STRING );
88 gtk_tree_view_set_model( GTK_TREE_VIEW( dirview ), GTK_TREE_MODEL( dir_store ) );
89
90 renderer = gtk_cell_renderer_text_new( );
91 column = gtk_tree_view_column_new_with_attributes ( "Description", renderer, "text", 0, NULL);
92 gtk_tree_view_column_set_sort_column_id( column, 0 );
93 gtk_tree_view_append_column( GTK_TREE_VIEW( dirview ), column );
94 }
95 else
96 {
97 clip_store = GTK_LIST_STORE( gtk_tree_view_get_model( GTK_TREE_VIEW( treeview ) ) );
98 dir_store = GTK_LIST_STORE( gtk_tree_view_get_model( GTK_TREE_VIEW( dirview ) ) );
99 gtk_list_store_clear( clip_store );
100 gtk_list_store_clear( dir_store );
101 }
102
103 if ( strcmp( path, "/" ) )
104 {
105 gtk_list_store_append( clip_store, &iter );
106 gtk_list_store_set( clip_store, &iter, 0, "..", -1 );
107 gtk_list_store_append( dir_store, &iter );
108 gtk_list_store_set( dir_store, &iter, 0, "..", -1 );
109 }
110
111 for ( index = 0; index < mvcp_dir_count( dir ); index ++ )
112 {
113 mvcp_dir_get( dir, index, &entry );
114 if ( strchr( entry.name, '/' ) )
115 {
116 gtk_list_store_append( dir_store, &iter );
117 gtk_list_store_set( dir_store, &iter, 0, entry.name, -1 );
118 }
119 else
120 {
121 gtk_list_store_append( clip_store, &iter );
122 gtk_list_store_set( clip_store, &iter, 0, entry.name, -1 );
123 }
124 }
125
126 mvcp_dir_close( dir );
127 }
128 else
129 {
130 gtk_label_set_text( GTK_LABEL( label ), "Disconnected" );
131 if ( gtk_tree_view_get_model( GTK_TREE_VIEW( treeview ) ) )
132 {
133 GtkListStore *list_store = GTK_LIST_STORE( gtk_tree_view_get_model( GTK_TREE_VIEW( treeview ) ) );
134 gtk_list_store_clear( list_store );
135 list_store = GTK_LIST_STORE( gtk_tree_view_get_model( GTK_TREE_VIEW( dirview ) ) );
136 gtk_list_store_clear( list_store );
137 treeview = lookup_widget( this_page_get_widget( this ), "treeview1" );
138 gtk_list_store_clear( list_store );
139 }
140 }
141 }
142
143 static void list_queue( page_clips this, int clip )
144 {
145 GtkWidget *treeview = lookup_widget( this_page_get_widget( this ), "treeview1" );
146 mvcp_list list = mvcp_list_init( this->dv, dv1394app_get_selected_unit( this->app ) );
147 GtkListStore *list_store = NULL;
148 GtkTreeIter iter;
149 GtkTreePath *path;
150 int index;
151 mvcp_list_entry_t entry;
152
153 if ( gtk_tree_view_get_model( GTK_TREE_VIEW( treeview ) ) == NULL )
154 {
155 GtkCellRenderer *renderer;
156 GtkTreeViewColumn *column;
157
158 list_store = gtk_list_store_new( 6,
159 G_TYPE_BOOLEAN,
160 G_TYPE_STRING,
161 G_TYPE_STRING,
162 G_TYPE_STRING,
163 G_TYPE_STRING,
164 G_TYPE_INT );
165 gtk_tree_view_set_model( GTK_TREE_VIEW( treeview ), GTK_TREE_MODEL( list_store ) );
166
167 renderer = gtk_cell_renderer_toggle_new( );
168 column = gtk_tree_view_column_new_with_attributes ( "", renderer, "active", 0, NULL);
169 gtk_tree_view_append_column( GTK_TREE_VIEW( treeview ), column );
170
171 renderer = gtk_cell_renderer_text_new( );
172 column = gtk_tree_view_column_new_with_attributes ( "In", renderer, "text", 1, NULL);
173 gtk_tree_view_append_column( GTK_TREE_VIEW( treeview ), column );
174
175 renderer = gtk_cell_renderer_text_new( );
176 column = gtk_tree_view_column_new_with_attributes ( "Out", renderer, "text", 2, NULL);
177 gtk_tree_view_append_column( GTK_TREE_VIEW( treeview ), column );
178
179 renderer = gtk_cell_renderer_text_new( );
180 column = gtk_tree_view_column_new_with_attributes ( "Length", renderer, "text", 3, NULL);
181 gtk_tree_view_append_column( GTK_TREE_VIEW( treeview ), column );
182
183 renderer = gtk_cell_renderer_text_new( );
184 column = gtk_tree_view_column_new_with_attributes ( "Clip", renderer, "text", 4, NULL);
185 gtk_tree_view_append_column( GTK_TREE_VIEW( treeview ), column );
186 }
187 else
188 {
189 list_store = GTK_LIST_STORE( gtk_tree_view_get_model( GTK_TREE_VIEW( treeview ) ) );
190 gtk_list_store_clear( list_store );
191 }
192
193 this->generation = list->generation;
194
195 for ( index = 0; index < mvcp_list_count( list ); index ++ )
196 {
197 char tc1[12], tc2[12], tc3[12];
198 mvcp_list_get( list, index, &entry );
199 gtk_list_store_append( list_store, &iter );
200 gtk_list_store_set( list_store, &iter,
201 0, index == clip,
202 1, frames2tc( entry.in, entry.fps, tc1),
203 2, frames2tc( entry.out, entry.fps, tc2),
204 3, frames2tc( entry.size, entry.fps, tc3),
205 4, entry.full,
206 5, entry.clip,
207 -1 );
208 }
209
210 this->clip = clip;
211 if ( clip < mvcp_list_count( list ) )
212 {
213 path = gtk_tree_path_new_from_indices( this->clip, -1 );
214 gtk_tree_view_scroll_to_cell( GTK_TREE_VIEW( treeview ), path, NULL, TRUE, 0.5, 0 );
215 gtk_tree_path_free( path );
216 }
217
218 mvcp_list_close( list );
219 }
220
221 static void list_active( page_clips this, int clip )
222 {
223 GtkWidget *treeview = lookup_widget( this_page_get_widget( this ), "treeview1" );
224 GtkListStore *list_store = GTK_LIST_STORE( gtk_tree_view_get_model( GTK_TREE_VIEW( treeview ) ) );
225 GtkTreePath *path = gtk_tree_path_new_from_indices( this->clip, -1 );
226 GtkTreeIter iter;
227
228 gtk_tree_model_get_iter( GTK_TREE_MODEL (list_store), &iter, path );
229 gtk_tree_path_free( path );
230 gtk_list_store_set( list_store, &iter, 0, FALSE, -1 );
231
232 this->clip = clip;
233 path = gtk_tree_path_new_from_indices( this->clip, -1 );
234 gtk_tree_view_scroll_to_cell( GTK_TREE_VIEW( treeview ), path, NULL, TRUE, 0.5, 0 );
235 gtk_tree_model_get_iter( GTK_TREE_MODEL (list_store), &iter, path );
236 gtk_tree_path_free( path );
237 gtk_list_store_set( list_store, &iter, 0, TRUE, -1 );
238 }
239
240 static gboolean on_ok( GtkWidget *dummy, gpointer data )
241 {
242 page_clips this = data;
243 GtkWidget *widget = lookup_widget( this_page_get_widget( this ), "list_clips" );
244 GtkTreeSelection *select = gtk_tree_view_get_selection( GTK_TREE_VIEW( widget ) );
245 GtkTreeModel *model;
246 GtkTreeIter iter;
247 gchar *text;
248
249 if ( gtk_tree_selection_get_selected( select, &model, &iter ) )
250 {
251 gtk_tree_model_get( model, &iter, 0, &text, -1 );
252
253 if ( !strcmp( text, ".." ) )
254 {
255 char *temp = strdup( this->path );
256 temp[ strlen( temp ) - 1 ] = '\0';
257 *( strrchr( temp, '/' ) + 1 ) = '\0';
258 list_clips( this, temp );
259 free( temp );
260 }
261 else
262 {
263 char *temp = malloc( strlen( this->path ) + strlen( text ) + 1 );
264 strcpy( temp, this->path );
265 strcat( temp, text );
266 switch( this->mode )
267 {
268 case 0:
269 mvcp_unit_load_back( this->dv, dv1394app_get_selected_unit( this->app ), temp );
270 mvcp_unit_play( this->dv, dv1394app_get_selected_unit( this->app ) );
271 break;
272 case 1:
273 mvcp_unit_load( this->dv, dv1394app_get_selected_unit( this->app ), temp );
274 break;
275 case 2:
276 mvcp_unit_append( this->dv, dv1394app_get_selected_unit( this->app ), temp, -1, -1 );
277 break;
278 case 3:
279 {
280 GtkWidget *widget = lookup_widget( this_page_get_widget( this ), "treeview1" );
281 GtkTreeSelection *select = gtk_tree_view_get_selection( GTK_TREE_VIEW( widget ) );
282 GtkTreeModel *model;
283 GtkTreeIter iter;
284 int clip;
285
286 if ( gtk_tree_selection_get_selected( select, &model, &iter ) )
287 {
288 gtk_tree_model_get( model, &iter, 5, &clip, -1 );
289 mvcp_unit_clip_insert( this->dv, dv1394app_get_selected_unit( this->app ), mvcp_absolute, clip, temp, -1, -1 );
290 }
291 break;
292 }
293 }
294
295 free( temp );
296 }
297
298 g_free( text );
299 }
300
301 return TRUE;
302 }
303
304 static gboolean on_dir( GtkWidget *dummy, gpointer data )
305 {
306 page_clips this = data;
307 GtkWidget *widget = lookup_widget( this_page_get_widget( this ), "list_dir" );
308 GtkTreeSelection *select = gtk_tree_view_get_selection( GTK_TREE_VIEW( widget ) );
309 GtkTreeModel *model;
310 GtkTreeIter iter;
311 gchar *text;
312
313 if ( gtk_tree_selection_get_selected( select, &model, &iter ) )
314 {
315 gtk_tree_model_get( model, &iter, 0, &text, -1 );
316
317 if ( !strcmp( text, ".." ) )
318 {
319 char *temp = strdup( this->path );
320 temp[ strlen( temp ) - 1 ] = '\0';
321 *( strrchr( temp, '/' ) + 1 ) = '\0';
322 list_clips( this, temp );
323 free( temp );
324 }
325 else if ( text[ strlen( text ) - 1 ] == '/' )
326 {
327 char *temp = malloc( strlen( this->path ) + strlen( text ) + 1 );
328 strcpy( temp, this->path );
329 strcat( temp, text );
330 list_clips( this, temp );
331 free( temp );
332 }
333
334 g_free( text );
335 }
336
337 return TRUE;
338 }
339
340 static gboolean on_queue_item( GtkWidget *dummy, gpointer data )
341 {
342 page_clips this = data;
343 GtkWidget *widget = lookup_widget( this_page_get_widget( this ), "treeview1" );
344 GtkTreeSelection *select = gtk_tree_view_get_selection( GTK_TREE_VIEW( widget ) );
345 GtkTreeModel *model;
346 GtkTreeIter iter;
347 int clip;
348
349 if ( gtk_tree_selection_get_selected( select, &model, &iter ) )
350 {
351 gtk_tree_model_get( model, &iter, 5, &clip, -1 );
352 mvcp_unit_clip_goto( this->dv, dv1394app_get_selected_unit( this->app ), mvcp_absolute, clip, 0 );
353 mvcp_unit_play( this->dv, dv1394app_get_selected_unit( this->app ) );
354 }
355
356 return TRUE;
357 }
358
359 static gboolean on_clip_selected( GtkWidget *widget, GdkEventButton *event, gpointer data )
360 {
361 if ( event->button==1 && event->type==GDK_2BUTTON_PRESS )
362 return on_ok( widget, data );
363 return FALSE;
364 }
365
366 static gboolean on_clip_key_press( GtkWidget *widget, GdkEventKey *event, gpointer data )
367 {
368 if ( event->keyval == GDK_Return )
369 return on_ok( widget, data );
370 return FALSE;
371 }
372
373 static gboolean on_dir_selected( GtkWidget *widget, GdkEventButton *event, gpointer data )
374 {
375 if ( event->button==1 && event->type==GDK_2BUTTON_PRESS )
376 return on_dir( widget, data );
377 return FALSE;
378 }
379
380 static gboolean on_dir_key_press( GtkWidget *widget, GdkEventKey *event, gpointer data )
381 {
382 if ( event->keyval == GDK_Return )
383 return on_dir( widget, data );
384 return FALSE;
385 }
386
387 static gboolean on_queue_selected( GtkWidget *widget, GdkEventButton *event, gpointer data )
388 {
389 if ( event->button==1 && event->type==GDK_2BUTTON_PRESS )
390 return on_queue_item( widget, data );
391 return FALSE;
392 }
393
394 static gboolean on_queue_key_press( GtkWidget *widget, GdkEventKey *event, gpointer data )
395 {
396 if ( event->keyval == GDK_Return )
397 return on_queue_item( widget, data );
398 return FALSE;
399 }
400
401 static gboolean on_home( GtkWidget *button, gpointer data )
402 {
403 page_clips this = data;
404 list_clips( this, "/" );
405 return TRUE;
406 }
407
408 static gboolean on_refresh( GtkWidget *button, gpointer data )
409 {
410 page_clips this = data;
411 char *temp = strdup( this->path );
412 list_clips( this, temp );
413 free( temp );
414
415 return TRUE;
416 }
417
418 static gboolean on_up( GtkWidget *dummy, gpointer data )
419 {
420 page_clips this = data;
421 GtkWidget *widget = lookup_widget( this_page_get_widget( this ), "treeview1" );
422 GtkTreeSelection *select = gtk_tree_view_get_selection( GTK_TREE_VIEW( widget ) );
423 GtkTreeModel *model;
424 GtkTreeIter iter;
425 int clip;
426
427 if ( gtk_tree_selection_get_selected( select, &model, &iter ) )
428 {
429 gtk_tree_model_get( model, &iter, 5, &clip, -1 );
430 mvcp_unit_clip_move( this->dv, dv1394app_get_selected_unit( this->app ), mvcp_absolute, clip, mvcp_absolute, clip - 1 < 0 ? 0 : clip - 1 );
431 }
432 return TRUE;
433 }
434
435 static gboolean on_down( GtkWidget *dummy, gpointer data )
436 {
437 page_clips this = data;
438 GtkWidget *widget = lookup_widget( this_page_get_widget( this ), "treeview1" );
439 GtkTreeSelection *select = gtk_tree_view_get_selection( GTK_TREE_VIEW( widget ) );
440 GtkTreeModel *model;
441 GtkTreeIter iter;
442 int clip;
443
444 if ( gtk_tree_selection_get_selected( select, &model, &iter ) )
445 {
446 gtk_tree_model_get( model, &iter, 5, &clip, -1 );
447 mvcp_unit_clip_move( this->dv, dv1394app_get_selected_unit( this->app ), mvcp_absolute, clip, mvcp_absolute, clip + 1 );
448 }
449 return TRUE;
450 }
451
452 static gboolean on_remove( GtkWidget *dummy, gpointer data )
453 {
454 page_clips this = data;
455 GtkWidget *widget = lookup_widget( this_page_get_widget( this ), "treeview1" );
456 GtkTreeSelection *select = gtk_tree_view_get_selection( GTK_TREE_VIEW( widget ) );
457 GtkTreeModel *model;
458 GtkTreeIter iter;
459 int clip;
460
461 if ( gtk_tree_selection_get_selected( select, &model, &iter ) )
462 {
463 gtk_tree_model_get( model, &iter, 5, &clip, -1 );
464 mvcp_unit_clip_remove( this->dv, dv1394app_get_selected_unit( this->app ), mvcp_absolute, clip );
465 }
466 return TRUE;
467 }
468
469 static gboolean on_clean( GtkWidget *dummy, gpointer data )
470 {
471 page_clips this = data;
472 mvcp_unit_clean( this->dv, dv1394app_get_selected_unit( this->app ) );
473 return TRUE;
474 }
475
476 void on_mode_change( GtkMenuItem *menuitem, gpointer data )
477 {
478 page_clips this = data;
479 int index = 0;
480
481 for ( index = 0; index < 4; index ++ )
482 if ( GTK_WIDGET( menuitem ) == this->modes[ index ] )
483 break;
484
485 this->mode = index;
486 }
487
488 static GtkWidget *this_page_get_widget( page_clips this )
489 {
490 if ( this->widget == NULL )
491 this->widget = create_page_clips( );
492 return this->widget;
493 }
494
495 static void this_page_get_toolbar_info( page this, GtkIconSize size, GtkWidget **icon, char **label )
496 {
497 *icon = gtk_image_new_from_stock( "gtk-justify-fill", size );
498 *label = _("_Playlist");
499 }
500
501 static void this_page_on_connect( page_clips this )
502 {
503 this->dv = mvcp_init( dv1394app_get_parser( this->app ) );
504 list_clips( this, "/" );
505 }
506
507 static void this_page_on_unit_change( page_clips this, int unit )
508 {
509 if ( unit != this->unit )
510 this->unit = unit;
511 }
512
513 static void this_page_on_disconnect( page_clips this )
514 {
515 list_clips( this, NULL );
516 mvcp_close( this->dv );
517 }
518
519 static void this_page_show_status( page_clips this, mvcp_status status )
520 {
521 if ( status->status != unit_disconnected )
522 {
523 if ( this->generation != status->generation )
524 list_queue( this, status->clip_index );
525 else if ( this->clip != status->clip_index )
526 list_active( this, status->clip_index );
527 }
528 }
529
530 static void this_page_close( page_clips this )
531 {
532 if ( this != NULL )
533 free( this );
534 }
535
536 page page_clips_init( dv1394app app )
537 {
538 page_clips this = calloc( 1, sizeof( page_clips_t ) );
539 GtkWidget *widget;
540 int index = 0;
541
542 this->parent.get_widget = ( GtkWidget *(*)( page ) )this_page_get_widget;
543 this->parent.get_toolbar_info = this_page_get_toolbar_info;
544 this->parent.on_connect = ( void (*)( page ) )this_page_on_connect;
545 this->parent.on_unit_change = ( void (*)( page, int ) )this_page_on_unit_change;
546 this->parent.on_disconnect = ( void (*)( page ) )this_page_on_disconnect;
547 this->parent.show_status = ( void (*)( page, mvcp_status ) )this_page_show_status;
548 this->parent.close = ( void (*)( page ) )this_page_close;
549 this->app = app;
550 this->generation = -1;
551
552 widget = lookup_widget( this_page_get_widget( this ), "list_clips" );
553 g_signal_connect( G_OBJECT( widget ), "button-press-event", G_CALLBACK( on_clip_selected ), this );
554 g_signal_connect( G_OBJECT( widget ), "key-press-event", G_CALLBACK( on_clip_key_press ), this );
555 widget = lookup_widget( this_page_get_widget( this ), "list_dir" );
556 g_signal_connect( G_OBJECT( widget ), "button-press-event", G_CALLBACK( on_dir_selected ), this );
557 g_signal_connect( G_OBJECT( widget ), "key-press-event", G_CALLBACK( on_dir_key_press ), this );
558 widget = lookup_widget( this_page_get_widget( this ), "button_clips_refresh" );
559 gtk_signal_connect( GTK_OBJECT( widget ), "clicked", GTK_SIGNAL_FUNC( on_refresh ), this );
560 widget = lookup_widget( this_page_get_widget( this ), "button_clips_home" );
561 gtk_signal_connect( GTK_OBJECT( widget ), "clicked", GTK_SIGNAL_FUNC( on_home ), this );
562 widget = lookup_widget( this_page_get_widget( this ), "button_up" );
563 gtk_signal_connect( GTK_OBJECT( widget ), "clicked", GTK_SIGNAL_FUNC( on_up ), this );
564 widget = lookup_widget( this_page_get_widget( this ), "button_down" );
565 gtk_signal_connect( GTK_OBJECT( widget ), "clicked", GTK_SIGNAL_FUNC( on_down ), this );
566 widget = lookup_widget( this_page_get_widget( this ), "button_remove" );
567 gtk_signal_connect( GTK_OBJECT( widget ), "clicked", GTK_SIGNAL_FUNC( on_remove ), this );
568 widget = lookup_widget( this_page_get_widget( this ), "button_clean" );
569 gtk_signal_connect( GTK_OBJECT( widget ), "clicked", GTK_SIGNAL_FUNC( on_clean ), this );
570
571 widget = lookup_widget( this_page_get_widget( this ), "table4" );
572 gtk_widget_show( widget );
573
574 widget = lookup_widget( this_page_get_widget( this ), "treeview1" );
575 g_signal_connect( G_OBJECT( widget ), "button-press-event", G_CALLBACK( on_queue_selected ), this );
576 g_signal_connect( G_OBJECT( widget ), "key-press-event", G_CALLBACK( on_queue_key_press ), this );
577
578 for ( index = 0; index < 4; index ++ )
579 {
580 char item[ 256 ];
581 sprintf( item, "mode_%d", index );
582 widget = lookup_widget( this_page_get_widget( this ), item );
583 gtk_signal_connect( GTK_OBJECT( widget ), "activate", GTK_SIGNAL_FUNC( on_mode_change ), this );
584 this->modes[ index ] = widget;
585 }
586
587 return ( page )this;
588 }