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