]> de.git.xonotic.org Git - xonotic/netradiant.git/blob - radiant/commands.cpp
Remove <gtk/gtk.h> from most of radiant/*
[xonotic/netradiant.git] / radiant / commands.cpp
1 /*
2    Copyright (C) 2001-2006, William Joseph.
3    All Rights Reserved.
4
5    This file is part of GtkRadiant.
6
7    GtkRadiant is free software; you can redistribute it and/or modify
8    it under the terms of the GNU General Public License as published by
9    the Free Software Foundation; either version 2 of the License, or
10    (at your option) any later version.
11
12    GtkRadiant is distributed in the hope that it will be useful,
13    but WITHOUT ANY WARRANTY; without even the implied warranty of
14    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15    GNU General Public License for more details.
16
17    You should have received a copy of the GNU General Public License
18    along with GtkRadiant; if not, write to the Free Software
19    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
20  */
21
22 #include "commands.h"
23
24 #include "debugging/debugging.h"
25 #include "warnings.h"
26
27 #include <map>
28 #include "string/string.h"
29 #include "versionlib.h"
30 #include "gtkutil/messagebox.h"
31 #include "gtkmisc.h"
32
33 typedef std::pair<Accelerator, int> ShortcutValue; // accelerator, isRegistered
34 typedef std::map<CopiedString, ShortcutValue> Shortcuts;
35
36 void Shortcuts_foreach( Shortcuts& shortcuts, CommandVisitor& visitor ){
37         for ( Shortcuts::iterator i = shortcuts.begin(); i != shortcuts.end(); ++i )
38         {
39                 visitor.visit( ( *i ).first.c_str(), ( *i ).second.first );
40         }
41 }
42
43 Shortcuts g_shortcuts;
44
45 const Accelerator& GlobalShortcuts_insert( const char* name, const Accelerator& accelerator ){
46         return ( *g_shortcuts.insert( Shortcuts::value_type( name, ShortcutValue( accelerator, false ) ) ).first ).second.first;
47 }
48
49 void GlobalShortcuts_foreach( CommandVisitor& visitor ){
50         Shortcuts_foreach( g_shortcuts, visitor );
51 }
52
53 void GlobalShortcuts_register( const char* name, int type ){
54         Shortcuts::iterator i = g_shortcuts.find( name );
55         if ( i != g_shortcuts.end() ) {
56                 ( *i ).second.second = type;
57         }
58 }
59
60 void GlobalShortcuts_reportUnregistered(){
61         for ( Shortcuts::iterator i = g_shortcuts.begin(); i != g_shortcuts.end(); ++i )
62         {
63                 if ( ( *i ).second.first.key != 0 && !( *i ).second.second ) {
64                         globalOutputStream() << "shortcut not registered: " << ( *i ).first.c_str() << "\n";
65                 }
66         }
67 }
68
69 typedef std::map<CopiedString, Command> Commands;
70
71 Commands g_commands;
72
73 void GlobalCommands_insert( const char* name, const Callback& callback, const Accelerator& accelerator ){
74         bool added = g_commands.insert( Commands::value_type( name, Command( callback, GlobalShortcuts_insert( name, accelerator ) ) ) ).second;
75         ASSERT_MESSAGE( added, "command already registered: " << makeQuoted( name ) );
76 }
77
78 const Command& GlobalCommands_find( const char* command ){
79         Commands::iterator i = g_commands.find( command );
80         ASSERT_MESSAGE( i != g_commands.end(), "failed to lookup command " << makeQuoted( command ) );
81         return ( *i ).second;
82 }
83
84 typedef std::map<CopiedString, Toggle> Toggles;
85
86
87 Toggles g_toggles;
88
89 void GlobalToggles_insert( const char* name, const Callback& callback, const BoolExportCallback& exportCallback, const Accelerator& accelerator ){
90         bool added = g_toggles.insert( Toggles::value_type( name, Toggle( callback, GlobalShortcuts_insert( name, accelerator ), exportCallback ) ) ).second;
91         ASSERT_MESSAGE( added, "toggle already registered: " << makeQuoted( name ) );
92 }
93 const Toggle& GlobalToggles_find( const char* name ){
94         Toggles::iterator i = g_toggles.find( name );
95         ASSERT_MESSAGE( i != g_toggles.end(), "failed to lookup toggle " << makeQuoted( name ) );
96         return ( *i ).second;
97 }
98
99 typedef std::map<CopiedString, KeyEvent> KeyEvents;
100
101
102 KeyEvents g_keyEvents;
103
104 void GlobalKeyEvents_insert( const char* name, const Accelerator& accelerator, const Callback& keyDown, const Callback& keyUp ){
105         bool added = g_keyEvents.insert( KeyEvents::value_type( name, KeyEvent( GlobalShortcuts_insert( name, accelerator ), keyDown, keyUp ) ) ).second;
106         ASSERT_MESSAGE( added, "command already registered: " << makeQuoted( name ) );
107 }
108 const KeyEvent& GlobalKeyEvents_find( const char* name ){
109         KeyEvents::iterator i = g_keyEvents.find( name );
110         ASSERT_MESSAGE( i != g_keyEvents.end(), "failed to lookup keyEvent " << makeQuoted( name ) );
111         return ( *i ).second;
112 }
113
114
115 void disconnect_accelerator( const char *name ){
116         Shortcuts::iterator i = g_shortcuts.find( name );
117         if ( i != g_shortcuts.end() ) {
118                 switch ( ( *i ).second.second )
119                 {
120                 case 1:
121                         // command
122                         command_disconnect_accelerator( name );
123                         break;
124                 case 2:
125                         // toggle
126                         toggle_remove_accelerator( name );
127                         break;
128                 }
129         }
130 }
131
132 void connect_accelerator( const char *name ){
133         Shortcuts::iterator i = g_shortcuts.find( name );
134         if ( i != g_shortcuts.end() ) {
135                 switch ( ( *i ).second.second )
136                 {
137                 case 1:
138                         // command
139                         command_connect_accelerator( name );
140                         break;
141                 case 2:
142                         // toggle
143                         toggle_add_accelerator( name );
144                         break;
145                 }
146         }
147 }
148
149
150 #include <uilib/uilib.h>
151 #include <gdk/gdkkeysyms.h>
152
153 #include "gtkutil/dialog.h"
154 #include "mainframe.h"
155
156 #include "stream/textfilestream.h"
157 #include "stream/stringstream.h"
158
159
160 struct command_list_dialog_t : public ModalDialog
161 {
162         command_list_dialog_t()
163                 : m_close_button( *this, eIDCANCEL ), m_list( NULL ), m_command_iter(), m_model( NULL ), m_waiting_for_key( false ){
164         }
165         ModalDialogButton m_close_button;
166
167         GtkTreeView *m_list;
168         GtkTreeIter m_command_iter;
169         GtkTreeModel *m_model;
170         bool m_waiting_for_key;
171 };
172
173 void accelerator_clear_button_clicked( GtkButton *btn, gpointer dialogptr ){
174         command_list_dialog_t &dialog = *(command_list_dialog_t *) dialogptr;
175
176         if ( dialog.m_waiting_for_key ) {
177                 // just unhighlight, user wanted to cancel
178                 dialog.m_waiting_for_key = false;
179                 gtk_list_store_set( GTK_LIST_STORE( dialog.m_model ), &dialog.m_command_iter, 2, false, -1 );
180                 gtk_widget_set_sensitive( GTK_WIDGET( dialog.m_list ), true );
181                 dialog.m_model = NULL;
182                 return;
183         }
184
185         GtkTreeSelection *sel = gtk_tree_view_get_selection( dialog.m_list );
186         GtkTreeModel *model;
187         GtkTreeIter iter;
188         if ( !gtk_tree_selection_get_selected( sel, &model, &iter ) ) {
189                 return;
190         }
191
192         GValue val;
193         memset( &val, 0, sizeof( val ) );
194         gtk_tree_model_get_value( GTK_TREE_MODEL( model ), &iter, 0, &val );
195         const char *commandName = g_value_get_string( &val );;
196
197         // clear the ACTUAL accelerator too!
198         disconnect_accelerator( commandName );
199
200         Shortcuts::iterator thisShortcutIterator = g_shortcuts.find( commandName );
201         if ( thisShortcutIterator == g_shortcuts.end() ) {
202                 return;
203         }
204         thisShortcutIterator->second.first = accelerator_null();
205
206         gtk_list_store_set( GTK_LIST_STORE( model ), &iter, 1, "", -1 );
207
208         g_value_unset( &val );
209 }
210
211 void accelerator_edit_button_clicked( GtkButton *btn, gpointer dialogptr ){
212         command_list_dialog_t &dialog = *(command_list_dialog_t *) dialogptr;
213
214         // 1. find selected row
215         GtkTreeSelection *sel = gtk_tree_view_get_selection( dialog.m_list );
216         GtkTreeModel *model;
217         GtkTreeIter iter;
218         if ( !gtk_tree_selection_get_selected( sel, &model, &iter ) ) {
219                 return;
220         }
221         dialog.m_command_iter = iter;
222         dialog.m_model = model;
223
224         // 2. disallow changing the row
225         //gtk_widget_set_sensitive(GTK_WIDGET(dialog.m_list), false);
226
227         // 3. highlight the row
228         gtk_list_store_set( GTK_LIST_STORE( model ), &iter, 2, true, -1 );
229
230         // 4. grab keyboard focus
231         dialog.m_waiting_for_key = true;
232 }
233
234 bool accelerator_window_key_press( ui::Widget widget, GdkEventKey *event, gpointer dialogptr ){
235         command_list_dialog_t &dialog = *(command_list_dialog_t *) dialogptr;
236
237         if ( !dialog.m_waiting_for_key ) {
238                 return false;
239         }
240
241 #if 0
242         if ( event->is_modifier ) {
243                 return false;
244         }
245 #else
246         switch ( event->keyval )
247         {
248         case GDK_KEY_Shift_L:
249         case GDK_KEY_Shift_R:
250         case GDK_KEY_Control_L:
251         case GDK_KEY_Control_R:
252         case GDK_KEY_Caps_Lock:
253         case GDK_KEY_Shift_Lock:
254         case GDK_KEY_Meta_L:
255         case GDK_KEY_Meta_R:
256         case GDK_KEY_Alt_L:
257         case GDK_KEY_Alt_R:
258         case GDK_KEY_Super_L:
259         case GDK_KEY_Super_R:
260         case GDK_KEY_Hyper_L:
261         case GDK_KEY_Hyper_R:
262                 return false;
263         }
264 #endif
265
266         dialog.m_waiting_for_key = false;
267
268         // 7. find the name of the accelerator
269         GValue val;
270         memset( &val, 0, sizeof( val ) );
271         gtk_tree_model_get_value( GTK_TREE_MODEL( dialog.m_model ), &dialog.m_command_iter, 0, &val );
272         const char *commandName = g_value_get_string( &val );;
273         Shortcuts::iterator thisShortcutIterator = g_shortcuts.find( commandName );
274         if ( thisShortcutIterator == g_shortcuts.end() ) {
275                 gtk_list_store_set( GTK_LIST_STORE( dialog.m_model ), &dialog.m_command_iter, 2, false, -1 );
276                 gtk_widget_set_sensitive( GTK_WIDGET( dialog.m_list ), true );
277                 return true;
278         }
279
280         // 8. build an Accelerator
281         Accelerator newAccel( event->keyval, (GdkModifierType) event->state );
282
283         // 8. verify the key is still free, show a dialog to ask what to do if not
284         class VerifyAcceleratorNotTaken : public CommandVisitor
285         {
286         const char *commandName;
287         const Accelerator &newAccel;
288         ui::Widget widget;
289         GtkTreeModel *model;
290 public:
291         bool allow;
292         VerifyAcceleratorNotTaken( const char *name, const Accelerator &accelerator, ui::Widget w, GtkTreeModel *m ) : commandName( name ), newAccel( accelerator ), widget( w ), model( m ), allow( true ){
293         }
294         void visit( const char* name, Accelerator& accelerator ){
295                 if ( !strcmp( name, commandName ) ) {
296                         return;
297                 }
298                 if ( !allow ) {
299                         return;
300                 }
301                 if ( accelerator.key == 0 ) {
302                         return;
303                 }
304                 if ( accelerator == newAccel ) {
305                         StringOutputStream msg;
306                         msg << "The command " << name << " is already assigned to the key " << accelerator << ".\n\n"
307                                 << "Do you want to unassign " << name << " first?";
308                         auto r = widget.alert( msg.c_str(), "Key already used", ui::alert_type::YESNOCANCEL );
309                         if ( r == ui::alert_response::YES ) {
310                                 // clear the ACTUAL accelerator too!
311                                 disconnect_accelerator( name );
312                                 // delete the modifier
313                                 accelerator = accelerator_null();
314                                 // empty the cell of the key binds dialog
315                                 GtkTreeIter i;
316                                 if ( gtk_tree_model_get_iter_first( GTK_TREE_MODEL( model ), &i ) ) {
317                                         for (;; )
318                                         {
319                                                 GValue val;
320                                                 memset( &val, 0, sizeof( val ) );
321                                                 gtk_tree_model_get_value( GTK_TREE_MODEL( model ), &i, 0, &val );
322                                                 const char *thisName = g_value_get_string( &val );;
323                                                 if ( !strcmp( thisName, name ) ) {
324                                                         gtk_list_store_set( GTK_LIST_STORE( model ), &i, 1, "", -1 );
325                                                 }
326                                                 g_value_unset( &val );
327                                                 if ( !gtk_tree_model_iter_next( GTK_TREE_MODEL( model ), &i ) ) {
328                                                         break;
329                                                 }
330                                         }
331                                 }
332                         }
333                         else if ( r == ui::alert_response::CANCEL ) {
334                                 // aborted
335                                 allow = false;
336                         }
337                 }
338         }
339         } verify_visitor( commandName, newAccel, widget, dialog.m_model );
340         GlobalShortcuts_foreach( verify_visitor );
341
342         gtk_list_store_set( GTK_LIST_STORE( dialog.m_model ), &dialog.m_command_iter, 2, false, -1 );
343         gtk_widget_set_sensitive( GTK_WIDGET( dialog.m_list ), true );
344
345         if ( verify_visitor.allow ) {
346                 // clear the ACTUAL accelerator first
347                 disconnect_accelerator( commandName );
348
349                 thisShortcutIterator->second.first = newAccel;
350
351                 // write into the cell
352                 StringOutputStream modifiers;
353                 modifiers << newAccel;
354                 gtk_list_store_set( GTK_LIST_STORE( dialog.m_model ), &dialog.m_command_iter, 1, modifiers.c_str(), -1 );
355
356                 // set the ACTUAL accelerator too!
357                 connect_accelerator( commandName );
358         }
359
360         g_value_unset( &val );
361
362         dialog.m_model = NULL;
363
364         return true;
365 }
366
367 /*
368     GtkTreeIter row;
369     GValue val;
370     if(!model) {g_error("Unable to get model from cell renderer");}
371     gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(model), &row, path_string);
372
373     gtk_tree_model_get_value(GTK_TREE_MODEL(model), &row, 0, &val);
374     const char *name = g_value_get_string(&val);
375     Shortcuts::iterator i = g_shortcuts.find(name);
376     if(i != g_shortcuts.end())
377     {
378         accelerator_parse(i->second.first, new_text);
379         StringOutputStream modifiers;
380         modifiers << i->second.first;
381         gtk_list_store_set(GTK_LIST_STORE(model), &row, 1, modifiers.c_str(), -1);
382     }
383    };
384  */
385
386 void DoCommandListDlg(){
387         command_list_dialog_t dialog;
388
389         ui::Window window = MainFrame_getWindow().create_modal_dialog_window("Mapped Commands", dialog, -1, 400);
390         window.on_key_press([](ui::Widget widget, GdkEventKey *event, gpointer dialogptr) {
391                 return accelerator_window_key_press(widget, event, dialogptr);
392         }, &dialog);
393
394         auto accel = ui::AccelGroup();
395         window.add_accel_group( accel );
396
397         GtkHBox* hbox = create_dialog_hbox( 4, 4 );
398         gtk_container_add( GTK_CONTAINER( window ), GTK_WIDGET( hbox ) );
399
400         {
401                 GtkScrolledWindow* scr = create_scrolled_window( GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC );
402                 gtk_box_pack_start( GTK_BOX( hbox ), GTK_WIDGET( scr ), TRUE, TRUE, 0 );
403
404                 {
405                         ui::ListStore store = ui::ListStore(gtk_list_store_new( 4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_INT ));
406
407                         ui::Widget view = ui::TreeView(ui::TreeModel(GTK_TREE_MODEL(store)));
408                         dialog.m_list = GTK_TREE_VIEW( view );
409
410                         gtk_tree_view_set_enable_search( GTK_TREE_VIEW( view ), false ); // annoying
411
412                         {
413                                 auto renderer = ui::CellRendererText();
414                                 GtkTreeViewColumn* column = ui::TreeViewColumn( "Command", renderer, {{"text", 0}, {"weight-set", 2}, {"weight", 3}} );
415                                 gtk_tree_view_append_column( GTK_TREE_VIEW( view ), column );
416                         }
417
418                         {
419                                 auto renderer = ui::CellRendererText();
420                                 GtkTreeViewColumn* column = ui::TreeViewColumn( "Key", renderer, {{"text", 1}, {"weight-set", 2}, {"weight", 3}} );
421                                 gtk_tree_view_append_column( GTK_TREE_VIEW( view ), column );
422                         }
423
424                         gtk_widget_show( view );
425                         gtk_container_add( GTK_CONTAINER( scr ), view );
426
427                         {
428                                 // Initialize dialog
429                                 StringOutputStream path( 256 );
430                                 path << SettingsPath_get() << "commandlist.txt";
431                                 globalOutputStream() << "Writing the command list to " << path.c_str() << "\n";
432                                 class BuildCommandList : public CommandVisitor
433                                 {
434                                 TextFileOutputStream m_commandList;
435                                 ui::ListStore m_store;
436 public:
437                                 BuildCommandList( const char* filename, ui::ListStore store ) : m_commandList( filename ), m_store( store ){
438                                 }
439                                 void visit( const char* name, Accelerator& accelerator ){
440                                         StringOutputStream modifiers;
441                                         modifiers << accelerator;
442
443                                         {
444                                                 GtkTreeIter iter;
445                                                 gtk_list_store_append( m_store, &iter );
446                                                 gtk_list_store_set( m_store, &iter, 0, name, 1, modifiers.c_str(), 2, false, 3, 800, -1 );
447                                         }
448
449                                         if ( !m_commandList.failed() ) {
450                                                 int l = strlen( name );
451                                                 m_commandList << name;
452                                                 while ( l++ < 25 )
453                                                         m_commandList << ' ';
454                                                 m_commandList << modifiers.c_str() << '\n';
455                                         }
456                                 }
457                                 } visitor( path.c_str(), store );
458
459                                 GlobalShortcuts_foreach( visitor );
460                         }
461
462                         g_object_unref( G_OBJECT( store ) );
463                 }
464         }
465
466         GtkVBox* vbox = create_dialog_vbox( 4 );
467         gtk_box_pack_start( GTK_BOX( hbox ), GTK_WIDGET( vbox ), TRUE, TRUE, 0 );
468         {
469                 GtkButton* editbutton = create_dialog_button( "Edit", (GCallback) accelerator_edit_button_clicked, &dialog );
470                 gtk_box_pack_start( GTK_BOX( vbox ), GTK_WIDGET( editbutton ), FALSE, FALSE, 0 );
471
472                 GtkButton* clearbutton = create_dialog_button( "Clear", (GCallback) accelerator_clear_button_clicked, &dialog );
473                 gtk_box_pack_start( GTK_BOX( vbox ), GTK_WIDGET( clearbutton ), FALSE, FALSE, 0 );
474
475                 ui::Widget spacer = ui::Image();
476                 gtk_widget_show( spacer );
477                 gtk_box_pack_start( GTK_BOX( vbox ), GTK_WIDGET( spacer ), TRUE, TRUE, 0 );
478
479                 auto button = create_modal_dialog_button( "Close", dialog.m_close_button );
480                 gtk_box_pack_start( GTK_BOX( vbox ), GTK_WIDGET( button ), FALSE, FALSE, 0 );
481                 widget_make_default( button );
482                 gtk_widget_grab_default( GTK_WIDGET( button ) );
483                 gtk_widget_add_accelerator( GTK_WIDGET( button ), "clicked", accel, GDK_Return, (GdkModifierType)0, (GtkAccelFlags)0 );
484                 gtk_widget_add_accelerator( GTK_WIDGET( button ), "clicked", accel, GDK_Escape, (GdkModifierType)0, (GtkAccelFlags)0 );
485         }
486
487         modal_dialog_show( window, dialog );
488         gtk_widget_destroy( GTK_WIDGET( window ) );
489 }
490
491 #include "profile/profile.h"
492
493 const char* const COMMANDS_VERSION = "1.0-gtk-accelnames";
494
495 void SaveCommandMap( const char* path ){
496         StringOutputStream strINI( 256 );
497         strINI << path << "shortcuts.ini";
498
499         TextFileOutputStream file( strINI.c_str() );
500         if ( !file.failed() ) {
501                 file << "[Version]\n";
502                 file << "number=" << COMMANDS_VERSION << "\n";
503                 file << "\n";
504                 file << "[Commands]\n";
505                 class WriteCommandMap : public CommandVisitor
506                 {
507                 TextFileOutputStream& m_file;
508 public:
509                 WriteCommandMap( TextFileOutputStream& file ) : m_file( file ){
510                 }
511                 void visit( const char* name, Accelerator& accelerator ){
512                         m_file << name << "=";
513
514                         const char* key = gtk_accelerator_name( accelerator.key, accelerator.modifiers );
515                         m_file << key;
516                         m_file << "\n";
517                 }
518                 } visitor( file );
519                 GlobalShortcuts_foreach( visitor );
520         }
521 }
522
523 const char* stringrange_find( const char* first, const char* last, char c ){
524         const char* p = strchr( first, '+' );
525         if ( p == 0 ) {
526                 return last;
527         }
528         return p;
529 }
530
531 class ReadCommandMap : public CommandVisitor
532 {
533 const char* m_filename;
534 std::size_t m_count;
535 public:
536 ReadCommandMap( const char* filename ) : m_filename( filename ), m_count( 0 ){
537 }
538 void visit( const char* name, Accelerator& accelerator ){
539         char value[1024];
540         if ( read_var( m_filename, "Commands", name, value ) ) {
541                 if ( string_empty( value ) ) {
542                         accelerator.key = 0;
543                         accelerator.modifiers = (GdkModifierType)0;
544                         return;
545                 }
546
547                 gtk_accelerator_parse( value, &accelerator.key, &accelerator.modifiers );
548                 accelerator = accelerator; // fix modifiers
549
550                 if ( accelerator.key != 0 ) {
551                         ++m_count;
552                 }
553                 else
554                 {
555                         globalOutputStream() << "WARNING: failed to parse user command " << makeQuoted( name ) << ": unknown key " << makeQuoted( value ) << "\n";
556                 }
557         }
558 }
559 std::size_t count() const {
560         return m_count;
561 }
562 };
563
564 void LoadCommandMap( const char* path ){
565         StringOutputStream strINI( 256 );
566         strINI << path << "shortcuts.ini";
567
568         FILE* f = fopen( strINI.c_str(), "r" );
569         if ( f != 0 ) {
570                 fclose( f );
571                 globalOutputStream() << "loading custom shortcuts list from " << makeQuoted( strINI.c_str() ) << "\n";
572
573                 Version version = version_parse( COMMANDS_VERSION );
574                 Version dataVersion = { 0, 0 };
575
576                 {
577                         char value[1024];
578                         if ( read_var( strINI.c_str(), "Version", "number", value ) ) {
579                                 dataVersion = version_parse( value );
580                         }
581                 }
582
583                 if ( version_compatible( version, dataVersion ) ) {
584                         globalOutputStream() << "commands import: data version " << dataVersion << " is compatible with code version " << version << "\n";
585                         ReadCommandMap visitor( strINI.c_str() );
586                         GlobalShortcuts_foreach( visitor );
587                         globalOutputStream() << "parsed " << Unsigned( visitor.count() ) << " custom shortcuts\n";
588                 }
589                 else
590                 {
591                         globalOutputStream() << "commands import: data version " << dataVersion << " is not compatible with code version " << version << "\n";
592                 }
593         }
594         else
595         {
596                 globalOutputStream() << "failed to load custom shortcuts from " << makeQuoted( strINI.c_str() ) << "\n";
597         }
598 }