1 /*!
2  * \file src/hid/gtk/gui-command-window.c
3  *
4  * \brief Provides a command entry window for the GTK UI.
5  *
6  * This file was written by Bill Wilson for the PCB Gtk port.
7  *
8  * gui-command-window.c provides two interfaces for getting user input
9  * for executing a command.
10  *
11  * As the Xt PCB was ported to Gtk, the traditional user entry in the
12  * status line window presented some focus problems which require that
13  * there can be no menu key shortcuts that might be a key the user would
14  * type in.  It also requires a coordinating flag so the drawing area
15  * won't grab focus while the command entry is up.
16  *
17  * I thought the interface should be cleaner, so I made an alternate
18  * command window interface which works better I think as a gui interface.
19  * The user must focus onto the command window, but since it's a separate
20  * window, there's no confusion.  It has the restriction that objects to
21  * be operated on must be selected, but that actually seems a better user
22  * interface than one where typing into one location requires the user to
23  * be careful about which object might be under the cursor somewhere else.
24  *
25  * In any event, both interfaces are here to work with.
26  *
27  * <hr>
28  *
29  * <h1><b>Copyright.</b></h1>\n
30  *
31  * PCB, interactive printed circuit board design
32  *
33  * Copyright (C) 1994,1995,1996 Thomas Nau
34  *
35  * This program is free software; you can redistribute it and/or modify
36  * it under the terms of the GNU General Public License as published by
37  * the Free Software Foundation; either version 2 of the License, or
38  * (at your option) any later version.
39  *
40  * This program is distributed in the hope that it will be useful,
41  * but WITHOUT ANY WARRANTY; without even the implied warranty of
42  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
43  * GNU General Public License for more details.
44  *
45  * You should have received a copy of the GNU General Public License along
46  * with this program; if not, write to the Free Software Foundation, Inc.,
47  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
48  *
49  *  Contact addresses for paper mail and Email:
50  *  Thomas Nau, Schlehenweg 15, 88471 Baustetten, Germany
51  *  Thomas.Nau@rz.uni-ulm.de
52  */
53 
54 
55 #ifdef HAVE_CONFIG_H
56 #include "config.h"
57 #endif
58 
59 #include "gui.h"
60 #include <gdk/gdkkeysyms.h>
61 
62 #include "crosshair.h"
63 
64 #ifdef HAVE_LIBDMALLOC
65 #include <dmalloc.h>
66 #endif
67 
68 static GtkWidget *command_window;
69 static GtkWidget *combo_vbox;
70 static GList *history_list;
71 static gchar *command_entered;
72 static GMainLoop *loop;
73 
74 
75 /*!
76  * \brief When using a command window for command entry, provide a quick
77  * and abbreviated reference to available commands.
78  *
79  * This is currently just a start and can be expanded if it proves
80  * useful.
81    */
82 static gchar *command_ref_text[] = {
83   N_("Common commands easily accessible via the gui may not be included here.\n"),
84   "\n",
85   N_("In user commands below, 'size' values may be absolute or relative\n"
86      "if preceded by a '+' or '-'.  Where 'units' are indicated, use \n"
87      "'mil' or 'mm' otherwise PCB internal units will be used.\n"),
88   "\n",
89   "<b>changesize(target, size, units)\n",
90   "\ttarget = {selectedlines | selectedpins | selectedvias | selectedpads \n"
91     "\t\t\t| selectedtexts | selectednames | selectedelements | selected}\n",
92   "\n",
93   "<b>changedrillsize(target, size, units)\n",
94   "\ttarget = {selectedpins | selectedvias | selectedobjects | selected}\n",
95   "\n",
96   "<b>changeclearsize(target, size, units)\n",
97   "\ttarget = {selectedpins | selectedpads | selectedvias | selectedlines\n"
98     "\t\t\t| selectedarcs | selectedobjects | selected}\n",
99   N_("\tChanges the clearance of objects.\n"),
100   "\n",
101   "<b>setvalue(target, size, units)\n",
102   "\ttarget = {grid | zoom | line | textscale | viadrillinghole\n"
103     "\t\t\t| viadrillinghole | via}\n",
104   N_("\tChanges values.  Omit 'units' for 'grid' and 'zoom'.\n"),
105   "\n",
106   "<b>changejoin(target)\n",
107   "\ttarget = {object | selectedlines | selectedarcs | selected}\n",
108   N_("\tChanges the join (clearance through polygons) of objects.\n"),
109   "\n",
110   "<b>changesquare(target)\n",
111   "<b>setsquare(target)\n",
112   "<b>clearsquare(target)\n",
113   "\ttarget = {object | selectedelements | selectedpins | selected}\n",
114   N_("\tToggles, sets, or clears the square flag of objects.\n"),
115   "\n",
116   "<b>changeoctagon(target)\n",
117   "<b>setoctagon(target)\n",
118   "<b>clearoctagon(target)\n",
119   "\ttarget = {object | selectedelements | selectedpins selectedvias | selected}\n",
120   N_("\tToggles, sets, or clears the octagon flag of objects.\n"),
121   "\n",
122   "<b>changehole(target)\n",
123   "\ttarget = {object | selectedvias | selected}\n",
124   N_("\tChanges the hole flag of objects.\n"),
125   "\n",
126   "<b>flip(target)\n",
127   "\ttarget = {object | selectedelements | selected}\n",
128   N_("\tFlip elements to the opposite side of the board.\n"),
129   "\n",
130   "<b>setthermal(target, style)\n",
131   "\ttarget = {object | selectedpins | selectedvias | selected}\n",
132   "\tstyle  = {0 | 1 | 2 | 3 | 4 | 5}\n\n",
133   N_("\tSet or clear a thermal (on the current layer) to pins or vias.\n"
134      "\tIf 'style' is omitted, the layout's default style is taken. Setting\n"
135      "\tthermals to style 0 (zero) turns the thermals off.\n"),
136   "\n",
137   "<b>loadvendorfrom(filename)\n",
138   "<b>unloadvendor()\n",
139   "\ttarget = [filename]\n",
140   N_("\tLoad a vendor file.  If 'filename' is omitted, pop up a file select dialog.\n"),
141 };
142 
143 
144 /*!
145  * \brief Put an allocated string on the history list and combo text
146  * list if it is not a duplicate.
147  *
148  * The history_list is just a shadow of the combo list, but I think is
149  * needed because I don't see an api for reading the combo strings.
150  * The combo box strings take "const gchar *", so the same allocated
151  * string can go in both the history list and the combo list.
152  *
153  * If removed from both lists, a string can be freed.
154  */
155 static void
command_history_add(gchar * cmd)156 command_history_add (gchar * cmd)
157 {
158   GList *list;
159   gchar *s;
160   gint i;
161 
162   if (!cmd || !*cmd)
163     return;
164 
165   /* Check for a duplicate command.  If found, move it to the
166      |  top of the list and similarly modify the combo box strings.
167    */
168   for (i = 0, list = history_list; list; list = list->next, ++i)
169     {
170       s = (gchar *) list->data;
171       if (!strcmp (cmd, s))
172 	{
173 	  history_list = g_list_remove (history_list, s);
174 	  history_list = g_list_prepend (history_list, s);
175 	  gtk_combo_box_remove_text (GTK_COMBO_BOX
176 				     (ghidgui->command_combo_box), i);
177 	  gtk_combo_box_prepend_text (GTK_COMBO_BOX
178 				      (ghidgui->command_combo_box), s);
179 	  return;
180 	}
181     }
182 
183   /* Not a duplicate, so put first in history list and combo box text list.
184    */
185   s = g_strdup (cmd);
186   history_list = g_list_prepend (history_list, s);
187   gtk_combo_box_prepend_text (GTK_COMBO_BOX (ghidgui->command_combo_box), s);
188 
189   /* And keep the lists trimmed!
190    */
191   if (g_list_length (history_list) > ghidgui->history_size)
192     {
193       s = (gchar *) g_list_nth_data (history_list, ghidgui->history_size);
194       history_list = g_list_remove (history_list, s);
195       gtk_combo_box_remove_text (GTK_COMBO_BOX (ghidgui->command_combo_box),
196 				 ghidgui->history_size);
197       g_free (s);
198     }
199 }
200 
201 
202 /*!
203  * \brief Called when user hits "Enter" key in command entry.
204  *
205  * The action to take depends on where the combo box is.
206  * If it's in the command window, we can immediately execute the command
207  * and carry on.
208  * If it's in the status line hbox, then we need stop the command
209  * entry g_main_loop from running and save the allocated string so it
210  * can be returned from ghid_command_entry_get()
211  */
212 static void
command_entry_activate_cb(GtkWidget * widget,gpointer data)213 command_entry_activate_cb (GtkWidget * widget, gpointer data)
214 {
215   gchar *command;
216 
217   command =
218     g_strdup (ghid_entry_get_text (GTK_WIDGET (ghidgui->command_entry)));
219   gtk_entry_set_text (ghidgui->command_entry, "");
220 
221   if (*command)
222     command_history_add (command);
223 
224   if (ghidgui->use_command_window)
225     {
226       hid_parse_command (command);
227       g_free (command);
228     }
229   else
230     {
231       if (loop && g_main_loop_is_running (loop))	/* should always be */
232 	g_main_loop_quit (loop);
233       command_entered = command;	/* Caller will free it */
234     }
235 }
236 
237 
238 /*!
239  * \brief Create the command_combo_box.
240  *
241  * Called once, either by ghid_command_window_show() or
242  * ghid_command_entry_get().
243  * Then as long as ghidgui->use_command_window is TRUE, the
244  * command_combo_box will live in a command window vbox or float if the
245  * command window is not up.
246  * But if ghidgui->use_command_window is FALSE, the command_combo_box
247  * will live in the status_line_hbox either shown or hidden.
248  * Since it's never destroyed, the combo history strings never need
249  * rebuilding and history is maintained if the combo box location is
250  * moved.
251  */
252 static void
command_combo_box_entry_create(void)253 command_combo_box_entry_create (void)
254 {
255   ghidgui->command_combo_box = gtk_combo_box_entry_new_text ();
256   ghidgui->command_entry =
257     GTK_ENTRY (gtk_bin_get_child (GTK_BIN (ghidgui->command_combo_box)));
258 
259   gtk_entry_set_width_chars (ghidgui->command_entry, 40);
260   gtk_entry_set_activates_default (ghidgui->command_entry, TRUE);
261 
262   g_signal_connect (G_OBJECT (ghidgui->command_entry), "activate",
263 		    G_CALLBACK (command_entry_activate_cb), NULL);
264 
265   g_object_ref (G_OBJECT (ghidgui->command_combo_box));	/* so can move it */
266 }
267 
268 
269 static void
command_window_disconnect_combobox()270 command_window_disconnect_combobox ()
271 {
272   if (command_window)
273   {
274     gtk_container_remove (GTK_CONTAINER (combo_vbox),	/* Float it */
275                           ghidgui->command_combo_box);
276   }
277   combo_vbox = NULL;
278 }
279 
280 
281 static void
command_window_close_cb(GtkWidget * widget,gpointer data)282 command_window_close_cb (GtkWidget * widget, gpointer data)
283 {
284   command_window_disconnect_combobox();
285   gtk_widget_destroy (command_window);
286 }
287 
288 
289 static gboolean
command_window_delete_event_cb(GtkWidget * widget,GdkEvent * event,gpointer data)290 command_window_delete_event_cb (GtkWidget * widget, GdkEvent * event,
291                                 gpointer data)
292 {
293   command_window_disconnect_combobox();
294   return FALSE;
295 }
296 
297 
298 static void
command_destroy_cb(GtkWidget * widget,gpointer data)299 command_destroy_cb (GtkWidget * widget, gpointer data)
300 {
301   command_window = NULL;
302 }
303 
304 
305 static gboolean
command_escape_cb(GtkWidget * widget,GdkEventKey * kev,gpointer data)306 command_escape_cb (GtkWidget * widget, GdkEventKey * kev, gpointer data)
307 {
308   gint ksym = kev->keyval;
309 
310   if (ksym != GDK_Escape)
311     return FALSE;
312 
313   if (command_window) {
314     command_window_disconnect_combobox();
315     gtk_widget_destroy (command_window);
316   }
317 
318   if (loop && g_main_loop_is_running (loop))	/* should always be */
319     g_main_loop_quit (loop);
320   command_entered = NULL;	/* We are aborting */
321 
322   return TRUE;
323 }
324 
325 
326 /*!
327  * \brief If ghidgui->use_command_window toggles, the config code calls
328  * this to ensure the command_combo_box is set up for living in the
329  * right place.
330  */
331 void
ghid_command_use_command_window_sync(void)332 ghid_command_use_command_window_sync (void)
333 {
334   /* The combo box will be NULL and not living anywhere until the
335      |  first command entry.
336    */
337   if (!ghidgui->command_combo_box)
338     return;
339 
340   if (ghidgui->use_command_window)
341     gtk_container_remove (GTK_CONTAINER (ghidgui->status_line_hbox),
342 			  ghidgui->command_combo_box);
343   else
344     {
345       /* Destroy the window (if it's up) which floats the command_combo_box
346          |  so we can pack it back into the status line hbox.  If the window
347          |  wasn't up, the command_combo_box was already floating.
348        */
349       if(command_window)  command_window_close_cb (NULL, NULL);
350       gtk_widget_hide (ghidgui->command_combo_box);
351       gtk_box_pack_start (GTK_BOX (ghidgui->status_line_hbox),
352 			  ghidgui->command_combo_box, FALSE, FALSE, 0);
353     }
354 }
355 
356 
357 /*!
358  * \brief If ghidgui->use_command_window is TRUE this will get called
359  * from ActionCommand() to show the command window.
360  */
361 void
ghid_command_window_show(gboolean raise)362 ghid_command_window_show (gboolean raise)
363 {
364   GtkWidget *vbox, *vbox1, *hbox, *button, *expander, *text;
365   gint i;
366 
367   if (command_window)
368     {
369       if (raise)
370         gtk_window_present (GTK_WINDOW(command_window));
371       return;
372     }
373   command_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
374   g_signal_connect (G_OBJECT (command_window), "destroy",
375                     G_CALLBACK (command_destroy_cb), NULL);
376   g_signal_connect (G_OBJECT (command_window), "delete-event",
377                     G_CALLBACK (command_window_delete_event_cb), NULL);
378   g_signal_connect (G_OBJECT (command_window), "key_press_event",
379 		    G_CALLBACK (command_escape_cb), NULL);
380   gtk_window_set_title (GTK_WINDOW (command_window), _("PCB Command Entry"));
381   gtk_window_set_wmclass (GTK_WINDOW (command_window), "PCB_Command", "PCB");
382   gtk_window_set_resizable (GTK_WINDOW (command_window), FALSE);
383 
384   vbox = gtk_vbox_new (FALSE, 0);
385   gtk_container_set_border_width (GTK_CONTAINER (vbox), 6);
386   gtk_container_add (GTK_CONTAINER (command_window), vbox);
387 
388   if (!ghidgui->command_combo_box)
389     command_combo_box_entry_create ();
390 
391   gtk_box_pack_start (GTK_BOX (vbox), ghidgui->command_combo_box,
392 		      FALSE, FALSE, 0);
393   combo_vbox = vbox;
394 
395   /* Make the command reference scrolled text view.  Use high level
396      |  utility functions in gui-utils.c
397    */
398   expander = gtk_expander_new (_("Command Reference"));
399   gtk_box_pack_start (GTK_BOX (vbox), expander, TRUE, TRUE, 2);
400   vbox1 = gtk_vbox_new (FALSE, 0);
401   gtk_container_add (GTK_CONTAINER (expander), vbox1);
402   gtk_widget_set_size_request (vbox1, -1, 350);
403 
404   text = ghid_scrolled_text_view (vbox1, NULL,
405 				  GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
406   for (i = 0; i < sizeof (command_ref_text) / sizeof (gchar *); ++i)
407     ghid_text_view_append (text, _(command_ref_text[i]));
408 
409   /* The command window close button.
410    */
411   hbox = gtk_hbutton_box_new ();
412   gtk_button_box_set_layout (GTK_BUTTON_BOX (hbox), GTK_BUTTONBOX_END);
413   gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 3);
414   button = gtk_button_new_from_stock (GTK_STOCK_CLOSE);
415   g_signal_connect (G_OBJECT (button), "clicked",
416 		    G_CALLBACK (command_window_close_cb), NULL);
417   gtk_box_pack_start (GTK_BOX (hbox), button, TRUE, TRUE, 0);
418 
419   gtk_widget_show_all (command_window);
420 }
421 
422 
423 /*!
424  * \brief This is the command entry function called from ActionCommand()
425  * when ghidgui->use_command_window is FALSE.
426  *
427  * The command_combo_box is already packed into the status line label
428  * hbox in this case.
429  */
430 gchar *
ghid_command_entry_get(gchar * prompt,gchar * command)431 ghid_command_entry_get (gchar * prompt, gchar * command)
432 {
433   gchar *s;
434   gint escape_sig_id;
435   GHidPort *out = &ghid_port;
436 
437   /* If this is the first user command entry, we have to create the
438      |  command_combo_box and pack it into the status_line_hbox.
439    */
440   if (!ghidgui->command_combo_box)
441     {
442       command_combo_box_entry_create ();
443       gtk_box_pack_start (GTK_BOX (ghidgui->status_line_hbox),
444 			  ghidgui->command_combo_box, FALSE, FALSE, 0);
445     }
446 
447   /* Make the prompt bold and set the label before showing the combo to
448      |  avoid window resizing wider.
449    */
450   s = g_strdup_printf ("<b>%s</b>", prompt ? prompt : "");
451   ghid_status_line_set_text (s);
452   g_free (s);
453 
454   /* Flag so output drawing area won't try to get focus away from us and
455      |  so resetting the status line label can be blocked when resize
456      |  callbacks are invokded from the resize caused by showing the combo box.
457    */
458   ghidgui->command_entry_status_line_active = TRUE;
459 
460   gtk_entry_set_text (ghidgui->command_entry, command ? command : "");
461   gtk_widget_show_all (ghidgui->command_combo_box);
462 
463   /* Remove the top window accel group so keys intended for the entry
464      |  don't get intercepted by the menu system.  Set the interface
465      |  insensitive so all the user can do is enter a command, grab focus
466      |  and connect a handler to look for the escape key.
467    */
468   ghid_remove_accel_groups (GTK_WINDOW (gport->top_window), ghidgui);
469   ghid_interface_input_signals_disconnect ();
470   ghid_interface_set_sensitive (FALSE);
471   gtk_widget_grab_focus (GTK_WIDGET (ghidgui->command_entry));
472   escape_sig_id = g_signal_connect (G_OBJECT (ghidgui->command_entry),
473 				    "key_press_event",
474 				    G_CALLBACK (command_escape_cb), NULL);
475 
476   loop = g_main_loop_new (NULL, FALSE);
477   g_main_loop_run (loop);
478 
479   g_main_loop_unref (loop);
480   loop = NULL;
481 
482   ghidgui->command_entry_status_line_active = FALSE;
483 
484   /* Restore the damage we did before entering the loop.
485    */
486   g_signal_handler_disconnect (ghidgui->command_entry, escape_sig_id);
487   ghid_interface_input_signals_connect ();
488   ghid_interface_set_sensitive (TRUE);
489   ghid_install_accel_groups (GTK_WINDOW (gport->top_window), ghidgui);
490 
491   /* Restore the status line label and give focus back to the drawing area
492    */
493   gtk_widget_hide (ghidgui->command_combo_box);
494   gtk_widget_grab_focus (out->drawing_area);
495 
496   return command_entered;
497 }
498 
499 
500 void
ghid_handle_user_command(gboolean raise)501 ghid_handle_user_command (gboolean raise)
502 {
503   char *command;
504   static char *previous = NULL;
505 
506   if (ghidgui->use_command_window)
507     ghid_command_window_show (raise);
508   else
509     {
510       command = ghid_command_entry_get (_("Enter command:"),
511 					(Settings.SaveLastCommand && previous) ? previous : (gchar *)"");
512       if (command != NULL)
513 	{
514 	  /* copy new comand line to save buffer */
515 	  g_free (previous);
516 	  previous = g_strdup (command);
517 	  hid_parse_command (command);
518 	  g_free (command);
519 	}
520     }
521   ghid_window_set_name_label (PCB->Name);
522   ghid_set_status_line_label ();
523 }
524