Original message is over a month old: http://mail.gnome.org/archives/gtk-list/2001-August/msg00224.html (The issue was constraining the contents of a GtkEntry to be e.g. a number) The GTK program below is a little study piece for nonmodal constrained text entry. Imagine that the window has other stuff in it, maybe a workspace or other controls which can be frobbed. It isn't meant to be a modal box where you just verify once and the box goes away. I think UI-wise it is representative of a common kind of dialog box frequently developed by researchers, engineers, and etc for internal or public tools. I consider this "market segment" to be particularly important for a toolkit like GTK because, frankly, they're one of the only demographics that gives a damn about GTK, Gnome, Free Software and etc. Besides pure software people, that's where most of the users seem to be. The applet has three numerical entries representing the lengths of three sides of a triangle. It has a drawing area to the left in which it constructs a triangle of those dimensions. This is a good example because there are two levels of validation: the numerical conversion, and the triangle inequality constraint. (also nonnegativity, so 3) The boxes accept numbers of the form 1e5. While the numerical grammar is simple, I don't want to make any assumptions as they did with the GIMP file|new box, which verifies upon character entry. That completely breaks if you have a grammar with members which are not connected by a sequence of simple character edits. (Eg, what do you do if you need a set of matched parenthesees?) After buckets of code, (okay, maybe not buckets, but too many) I am getting close to satisfied with the behavior. One problem is that gtk_widget_grab_focus is broken. (For reference, I'm using GTK 1.3 for windows.) Like I mentioned before, I want to revert the focus when I revert the value of the entry. This I see as an important mental handshaking thing. The alternative would be maybe to beep. Or both. But I think that silently reverting the value is definitely unacceptable. Also, if there are no speakers, you still need handshaking. And what good would it be to turn the field red but not change focus, if the user will definitely have to change it back to submit after mentally changing gears? My comment below about OpenInventor coexistence is another reason for changing the focus. I saw some web Q&A where some other confused programmers could not get gtk_widget_grab_focus to work in this context and other people said it should work: http://mail.gnome.org/archives/gnome-list/1999-November/msg00783.html This guy thought it might be because focus-out was already executing: http://mail.gnome.org/archives/gtk-list/2001-March/msg00050.html If he's right, then it does in fact seem impossible to get this correctly implemented, unless there is some guru way to work around this event behavior. There definitely needs to be some reference manual comment on the GtkEditable page that describes "changed" and "activate" saying to check out GtkWidget::"focus_out_event". Presuming of course that that ends up catching all the salient cases. Otherwise, GtkEditable needs another signal, maybe called "changed_compound" or "changed_fully" that catches all the focus outs and whatever else is appropriate. This would be a first step toward giving this problem an obvious solution. [this is Damon, responses to two of Eric's comments are below.] > I do think this is an important usability issue, and I hope it > does get addressed if we ever have a usability style guide. There is a bunch of discussion constructing (what I understand to be) a Gnome style guide on [EMAIL PROTECTED] I've been trying to tune in but most of the discussion is foriegn to me as I don't have a full (let alone recent 2.0 development) gnome installation of any kind running on my own machines. Without reading their work more carefully, I risk putting my foot in my mouth, but it seems like a GTK style guide is prerequisite to what they are trying to do. This seems bad. > I couldn't see it covered in the FAQ, by the way - that only seems to > cover conversion/validation of characters while typing, which is a > slightly different issue (unless I missed this question). You're right. Not only is it slightly different, but really obvious to solve. Tony Gale's terse response really pissed me off, actually. People who will only do discourse given a code example are one sort of people that create a lot of usability problems in the free software world. I am somewhat bitter that I had to construct such a basic thing from scratch just to explain it. Real industrial strength UI design is almost always done without code examples. I could easily see that people feeling too lazy to write an example for posting could have been a contributing factor to people not getting this constrained text thing right a long time ago. It's too easy to just look in the GIMP and say "Oh, look the GIMP File|New box floating point entries don't have an easily generalizable UI, oh well that's okay, we just have floating point numbers so we'll just do that." At least I'll be able to post an example cleanly separated from my app so as to save others time if I get it right. > Either show an application-modal dialog, or just revert the text, select > it all, and set the application's focus back to it. So when the user > switches back to the application they can sort it out. In the app I was developing previously, my OpenInventor/Motif window seemed to know nothing of my Gtk window and vice versa. There, since the inventor window was a view parameterized by the values in the GTK panel, it would be definitely unacceptable to quietly revert the value and let the user wonder why the graphic does not seem to respond in the way he thought. Note that in practice, the reason focus left the GTK window was so that the mouse could trackball-drag the 3D model around. Granted, this particular circumstance does seem exotic and perhaps not representative of a large class of apps. I tried to simulate this using GTK only, but it didn't work. In my example, you can set SEPARATE_WINDOWS to 1 and get two disjoint independent GTK windows. But the focus_out event comes just the same, as one would hope. I believe when I had a window from a different toolkit, this behavior was not consistent. I don't remember exactly what the behavior was, and I don't have access to the code. Even on my machine now, with SEPARATE_WINDOWS==0, the first GtkWindow::focus_out_event does not propagate to its child widgets on the very first time. Try typing a letter into a box and then focusing out. (on windows alt+tab or clicking out either way) The letter stays the first time, though on all subsequent times it is reverted. (on my machine anyway) This is a bug or feature. [now this is all Eric Monsler] > One UI feature you could use might be to change the font or color of the > text being entered, until the user hits 'Enter' and the string is > parsed, checked, and used. It seems most common for "enter" in a modal dialog to be equivalent to clicking ok. Tabing and clicking fields is reserved for moving, and fields are automatically verified on moving as here. It's certainly the Win/Mac convention anyway. I think that would ultimately be my preference in this situation, assuming we reoriented to a modal state of mind and the "do nothing" button was a cancel/ok pair. I think that the GTK API has encouraged a different meaning of the "activate" event and thus "enter", though. In the GIMP File|New box for example "enter" just verifies the field, despite the fact that in basically every Mac and Windows modal dialog enter means OK. More evidence that programmers often prefer what's easy to what makes sense to the user. I can see "enter" in a nonmodal entry causing submission of just the item to make sense. This is like what happens in a spreadsheet when you edit a formula cell, though the analogy lacks a second entry which you might tab to. > I don't like choices 1) or 3), because I commonly start to enter text > into an entry, switch to another app or window to check something, and > then switch back, losing my start at changes. Those behaviors may be > acceptable for trivial entry items, but would be very annoying for > longer entries. Even a filename entry could be very anooying to have > lost the patially typed path. Since filename boxes don't necessarily start with a valid item, a reasonable (and reasonably conventional) response to a bad filename would be to not close the dialog, and to put the focus back in the entry box but not revert it. You're right about the size of the data being an important heuristic. I can imagine auto reverting a math expression would be really annoying. But, especially if you grab the focus on an invalid nonmodal entry, what you do need to keep is the option to revert (escape key? Not sure for a mouse exit. Double click out means revert?) so that you don't get wedged into a particular entry. Thanks guys for your interest, Jeff Henrikson Here is the code example, nonmodal.cpp: #include <gtk/gtk.h> #include <gdk/gdk.h> #include <assert.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> #define M_PI 3.1415926535897932384626433832795 #define SEPARATE_WINDOWS 0 // setting this nonzero makes two separate GtkWindows typedef struct _ConstrainedEntry ConstrainedEntry; GtkWidget* theTriangle=0; // these 7 are the global "singletons" ConstrainedEntry* sidea; ConstrainedEntry* sideb; ConstrainedEntry* sidec; GtkWidget* theTriangleWindow=0; GtkWidget* theEntryWindow=0; GtkWidget* theEntryBox=0; #define MAX_DOUBLE_STRING_LEN 40 typedef struct _ConstrainedEntry { char text[MAX_DOUBLE_STRING_LEN]; GtkWidget* widget; float value; GtkWidget* triangle; bool (*isValid)(void*, double); // (void* this, double newvalue) } ConstrainedEntry; /********************** verification functions ************************/ bool verify_double(char* s, double* d) { if(!s) return false; if(s[0]==0) return false; char* end=NULL; double newvalue = strtod(s,&end); if(end==NULL) return false; if(end<s+strlen(s)) for(char* ch=end; ch[0]!=0; ch++) if(!((ch[0]==' ') || (ch[0]=='\t') || (ch[0]=='\n')|| (ch[0]=='\r'))) return false; *d=newvalue; return true; } bool verify_nonnegative(double d) { return d>=0; } bool verify_triangle_inequality(double a, double b, double c) { assert((a>=0)&&(b>=0)&&(c>=0)); return((a<b+c)&&(b<c+a)&&(c<a+b)); } bool verify_triangle_entry(void* e, double newvalue) { if(sidea!=NULL && sideb!=NULL && sidec!=NULL) return verify_triangle_inequality(e==sidea? newvalue: sidea->value, e==sideb? newvalue: sideb->value, e==sidec? newvalue: sidec->value); else return true; // all entries aren't constructed yet } /************************** math functions **************************/ /** returns cosine of angle opposite c */ float law_of_cosines(float a, float b, float c) { float ret= (a*a + b*b - c*c)/(2*a*b); //printf("(%f,%f,%f->%f) ",a,b,c,ret); return ret; } // just for a printf, 2d euclidean distance: float d(float x, float y) {return sqrt(x*x+y*y);} float clamp(float x) {return (x<-1)? -1: (x<1)? x: 1;} void compute_triangle(float a, float b, float c, float* xyz) { float cphia = law_of_cosines(c,b,a); float cphib = law_of_cosines(a,c,b); float cphic = law_of_cosines(a,b,c); assert(!((cphia>1)|| (cphib>1)|| (cphic>1)|| //should have been caught by (cphia<-1)||(cphib<-1)||(cphic<-1))); //verify_triangle_entry float phia=acos(clamp(cphia)); float phib=acos(clamp(cphib)); float phic=acos(clamp(cphic)); float thetaa=M_PI-(phib+phic)/2; float thetab=M_PI-(phic+phia)/2; float thetac=M_PI-(phia+phib)/2; float db=sin(phia/2)*c/sin(thetac); float da=sin(phib/2)*c/sin(thetac); float dc=sin(phib/2)*a/sin(thetaa); xyz[0]=-db; xyz[1]=0; xyz[2]=-dc*cos(thetaa); xyz[3]=dc*sin(thetaa); xyz[4]=-da*cos(thetac); xyz[5]=-da*sin(thetac); /* printf("[%f,%f,%f] ",a,b,c); printf("[%f,%f,%f]\n", d(xyz[0]-xyz[2],xyz[1]-xyz[3]), d(xyz[2]-xyz[4],xyz[3]-xyz[5]), d(xyz[4]-xyz[0],xyz[5]-xyz[1])); fflush(stdout); */ } /********************* BEGIN ConstrainedEntry operations ***************/ void constrained_entry_revert(ConstrainedEntry* e) { gtk_entry_set_text(GTK_ENTRY(e->widget),e->text); } void constrained_entry_verify(ConstrainedEntry* e) { gchar* text = gtk_entry_get_text(GTK_ENTRY(e->widget)); // do not mutate or free result if(strcmp(text,e->text)==0) return; // do nothing if no input has changed. double newvalue; bool verified = verify_double(text,&newvalue) && verify_nonnegative(newvalue) && (e->isValid((void*)e,newvalue)); if(!verified) { constrained_entry_revert(e); /**************** These below all seem to be broken. Do I misunderstand? *****************/ //gtk_widget_grab_focus(e->widget); //gtk_widget_draw_focus(e->widget); //gtk_widget_show(e->widget); //gtk_container_set_focus_child(GTK_CONTAINER(theWindow),e->widget); gtk_container_set_focus_child(GTK_CONTAINER(theEntryBox),e->widget); printf("<grab> "); fflush(stdout); // it really is running here } else { e->value = newvalue; printf("[%f taken as value] ",e->value); fflush(stdout); assert(strlen(text)<=MAX_DOUBLE_STRING_LEN-1); e->text[MAX_DOUBLE_STRING_LEN-1]=0; strncpy(e->text, text, MAX_DOUBLE_STRING_LEN-1); // no particularly interesting error condition. constrained_entry_revert(e); // to truncate unused results //triangle_expose_event(NULL,NULL,NULL); //gtk_signal_emit_by_name(GTK_OBJECT(theTriangle),"expose_event"); gtk_widget_queue_clear(theTriangle); //gtk_widget_queue_draw_area(theTriangle,0,0,100,100); } } gint constrained_entry_focus_out(void* junk1, void* junk2, void* user) { ConstrainedEntry* e = (ConstrainedEntry*)user; //printf("(focus_out: %08x,%08x,%08x) ",junk1,junk2,user); constrained_entry_verify(e); return(FALSE); } gint constrained_entry_activate(void* junk1, void* user) { ConstrainedEntry* e = (ConstrainedEntry*)user; //printf("(activate: %08x,%08x,%08x) ",junk1,junk2,user); constrained_entry_verify(e); return(FALSE); } gint leave_notify_handler(void* junk1, void* junk2, void* user) { printf("leave_notify_event "); fflush(stdout); return TRUE; } ConstrainedEntry* new_constrained_entry(char* initial_input, bool (*_isValid) (void*, double)) { ConstrainedEntry* ret = (ConstrainedEntry*)g_malloc(sizeof(ConstrainedEntry)); printf("(new_centry at %08x) ",ret); ret->isValid = _isValid; // predicate for verification // (eg, for testing triangle inequality) ret->widget = gtk_entry_new (); gtk_signal_connect (GTK_OBJECT (ret->widget), "focus_out_event", GTK_SIGNAL_FUNC (constrained_entry_focus_out), ret); gtk_signal_connect (GTK_OBJECT (ret->widget), "activate", GTK_SIGNAL_FUNC (constrained_entry_activate), ret); gtk_signal_connect (GTK_OBJECT (ret->widget), "leave_notify_event", GTK_SIGNAL_FUNC (leave_notify_handler), ret); // check "can_focus" arg gpointer p=gtk_object_get_data(GTK_OBJECT(ret->widget), "can_focus"); printf("(p=%08x",p); if(p!=0 && p != (gpointer)1 && p != (gpointer)-1) printf(",*p=%08x",p); printf(")"); fflush(stdout); /* gtk_object_set_data(GTK_OBJECT(ret->widget), "can_focus",(gpointer)-1); // check "can_focus" arg p=gtk_object_get_data(GTK_OBJECT(ret->widget), "can_focus"); printf("(p=%08x",p); if(p!=0 && p != (gpointer)1 && p != (gpointer)-1) printf(",*p=%08x",p); printf(")"); fflush(stdout); */ // put initial input into widget. Make different than stored text // so that constrained_entry_verify parses it. ret->text[0]=0; gtk_entry_set_text(GTK_ENTRY(ret->widget),initial_input); ret->value=1; constrained_entry_verify(ret); // if initial_input bad, then 0 appears in box return ret; } /*********************** end ConstrainedEntry operations *************/ /************************** paint routine ****************************/ gboolean triangle_expose_event(GtkWidget *widget, GdkEventExpose *event, gpointer data) { gdk_window_clear_area (widget->window, event->area.x, event->area.y, event->area.width, event->area.height); gdk_gc_set_clip_rectangle (widget->style->fg_gc[widget->state], &event->area); float xyz[6] = {0,0,0,0,0,0}; compute_triangle(sidea->value,sideb->value,sidec->value,xyz); gdk_draw_line(widget->window, widget->style->fg_gc[widget->state], (int)(50.5+10*xyz[0]), (int)(50.5-10*xyz[1]), (int)(50.5+10*xyz[2]), (int)(50.5-10*xyz[3])); gdk_draw_line(widget->window, widget->style->fg_gc[widget->state], (int)(50.5+10*xyz[2]), (int)(50.5-10*xyz[3]), (int)(50.5+10*xyz[4]), (int)(50.5-10*xyz[5])); gdk_draw_line(widget->window, widget->style->fg_gc[widget->state], (int)(50.5+10*xyz[4]), (int)(50.5-10*xyz[5]), (int)(50.5+10*xyz[0]), (int)(50.5-10*xyz[1])); gdk_gc_set_clip_rectangle (widget->style->fg_gc[widget->state], NULL); return TRUE; } /************************* GTK box assembly ****************************/ GtkWidget* make_side_entry_box() { gint homogeneous=0; gint spacing=0; gint expand=0; gint fill=0; gint padding=0; GtkWidget* box = gtk_vbox_new (homogeneous, spacing); GtkWidget* widget = 0; theEntryBox=box; ConstrainedEntry* centry; centry = new_constrained_entry("4",verify_triangle_entry); gtk_box_pack_start (GTK_BOX (box), centry->widget, expand, fill, padding); gtk_widget_show (centry->widget); sidea=centry; centry = new_constrained_entry("5",verify_triangle_entry); gtk_box_pack_start (GTK_BOX (box), centry->widget, expand, fill, padding); gtk_widget_show (centry->widget); sideb=centry; centry = new_constrained_entry("3",verify_triangle_entry); gtk_box_pack_start (GTK_BOX (box), centry->widget, expand, fill, padding); gtk_widget_show (centry->widget); sidec=centry; widget = gtk_button_new_with_label ("do nothing"); gtk_box_pack_start (GTK_BOX (box), widget, expand, fill, padding); gtk_widget_show (widget); gtk_widget_show(box); return box; } GtkWidget* make_triangle_hbox() { gint homogeneous=0; gint spacing=10; gint expand=0; gint fill=0; gint padding=0; /* and spacing settings */ GtkWidget* box = gtk_hbox_new (homogeneous, spacing); GtkWidget* widget = 0; /* Create a series of buttons with the appropriate settings */ widget = gtk_drawing_area_new (); gtk_drawing_area_size (GTK_DRAWING_AREA (widget), 100, 100); gtk_signal_connect (GTK_OBJECT (widget), "expose_event", GTK_SIGNAL_FUNC (triangle_expose_event), NULL); gtk_box_pack_start (GTK_BOX (box), widget, expand, fill, padding); gtk_widget_show (widget); theTriangle = widget; #if (!(SEPARATE_WINDOWS)) widget = make_side_entry_box(); gtk_box_pack_start (GTK_BOX (box), widget, expand, fill, padding); gtk_widget_show (widget); #endif SEPARATE_WINDOWS gtk_widget_show(box); return box; } void focus_out() { printf("GtkWindow::focus_out_event "); fflush(stdout); } gint delete_event( GtkWidget *widget, GdkEvent *event, gpointer data ) { return(FALSE); } void destroy(GtkWidget *widget, gpointer data) { gtk_main_quit(); } void main(int argc, char** argv) { GtkWidget *window; GtkWidget *box; gtk_init(&argc, &argv); //gdk_rgb_init(); window = gtk_window_new (GTK_WINDOW_TOPLEVEL); theTriangleWindow=window; gtk_signal_connect (GTK_OBJECT (window), "delete_event", GTK_SIGNAL_FUNC (delete_event), NULL); gtk_signal_connect (GTK_OBJECT (window), "destroy", GTK_SIGNAL_FUNC (destroy), NULL); gtk_signal_connect (GTK_OBJECT (window), "focus_out_event", GTK_SIGNAL_FUNC (focus_out), NULL); gtk_container_set_border_width (GTK_CONTAINER (window), 10); box=make_triangle_hbox(); gtk_container_add (GTK_CONTAINER (window), box); #if SEPARATE_WINDOWS window = gtk_window_new (GTK_WINDOW_TOPLEVEL); theEntryWindow=window; gtk_signal_connect (GTK_OBJECT (window), "delete_event", GTK_SIGNAL_FUNC (delete_event), NULL); gtk_signal_connect (GTK_OBJECT (window), "destroy", GTK_SIGNAL_FUNC (destroy), NULL); gtk_container_set_border_width (GTK_CONTAINER (window), 10); box = make_side_entry_box(); gtk_container_add (GTK_CONTAINER (window), box); #endif SEPARATE_WINDOWS gtk_widget_show(theTriangleWindow); #if SEPARATE_WINDOWS gtk_widget_show(theEntryWindow); #endif SEPARATE_WINDOWS gtk_main (); } _______________________________________________ gtk-list mailing list [EMAIL PROTECTED] http://mail.gnome.org/mailman/listinfo/gtk-list
