/* forms.c - forms handling for html+ ParseHTML() creates a linked list of forms. Each form has a linked list of fields. This file includes routines for drawing fields, handling mouse clicks/drags and key presses on fields, plus sending the contents of forms to HTTP servers. When resizing windows, it is important to keep the existing form data structures as otherwise the current values of fields will be lost and overridden by the original starting values. This requires the global new_form to be set to false. It should be set to true when reading new documents. In addition, The browser history list needs to preserve the form data structures so that they can be reused upon backtracking to restore previous field values. The desire to evolve to a wysiwyg editor means that its a bad idea to have pointers into the paint stream as this would be expensive to update when users edit the html document source . Consequently, one has to search the form structure to find fields that need to be redrawn under programatic control, e.g. unsetting of radio buttons. Note that anyway, the y-position of a field is unresolved when it is added to the paint stream, and only becomes fixed when the end of the line is reached. A compromise is to cache the baseline position when painting each field and trash the cache each time the user alters the document. Another complication is the need to save current field values when following a link to another document, as otherwise there is now way of restoring them when the user backtracks to the document containing the form. A simple approach is to save the linked list of forms in memory as part of a memory resident history list. Note for local links, the same form data should be used, rather than recreating the list from new. The corollary is that the list should only be freed when backtracking the document containing the form or reloading the same. Note that in many cases a sequence of form interactions with a server causes a state change in the server (e.g. updating a database) and hence is not simply reversed by backtracking in the normal way. In this case it makes sense to avoid pushing intermediate steps onto the history stack, e.g. when submitting the form. This probably requires the document or server to disable the history nechanism via suitable attributes. */ #include #include #include #include #include #include #include "www.h" extern Display *display; extern int screen; extern Window win; extern GC disp_gc, gc_fill; extern Cursor hourglass; extern int UsePaper; extern int debug; /* controls display of errors */ extern int document; /* HTMLDOCUMENT or TEXTDOCUMENT */ extern int busy; extern int OpenURL; extern int IsIndex; extern int FindStr; extern char *FindNextStr; extern int SaveFile; extern int sbar_width; extern int statusHeight; extern int ToolBarHeight; extern unsigned long windowColor; extern unsigned int win_width, win_height, tileWidth, tileHeight; extern XFontStruct *h1_font, *h2_font, *h3_font, *normal_font, *italic_font, *bold_font, *fixed_i_font, *fixed_b_font, *fixed_font, *legend_font; extern unsigned long textColor, labelColor, windowTopShadow, strikeColor, windowBottomShadow, statusColor, windowColor; /* The current top line is displayed at the top of the window,the pixel offset is the number of pixels from the start of the document. */ extern char *buffer; /* the start of the document buffer */ extern long PixelOffset; /* the pixel offset to top of window */ extern int PixelIndent; extern Doc NewDoc, CurrentDoc; extern XFontStruct *pFontInfo; extern XFontStruct *Fonts[FONTS]; extern int LineSpacing[FONTS], BaseLine[FONTS], StrikeLine[FONTS]; extern int preformatted; extern int font; /* index into Fonts[] array */ extern int above, below; extern XRectangle displayRect; /* clipping limits for painting html */ Form *forms = NULL; Field *focus; /* which field has focus (NULL if none do) */ int cursorpos = 5; /* cursor position in characters */ int new_form; /* if false then reuse existing data structures */ /* version of strdup for use with non-terminated strings */ char *strdup2(char *s, int len) { char *str; str = (char *)malloc(len + 1); memcpy(str, s, len); str[len] = '\0'; return str; } void FreeForms(void) { Option *option; Form *form; Field *fields, *field; new_form = 1; while (forms != NULL) { form = forms; forms = forms->next; fields = form->fields; while (fields) { field = fields; free(field->name); free(field->value); fields = fields->next; while (field->options) { option = field->options; if (option->label) free(option->label); field->options = option->next; free(option); } free(field); } if (form->action) free(form->action); free(form); } } /* Preserves form contents across window resizing / backtracking. Multiple FORM elements with the same ACTION URL will be treated as if they belong to the same form! It is assumed that FreeForms() is called before loading new documents or reloading existing ones. */ Form *GetForm(char *url, int len) { Form *form; /* is form already defined */ for (form = forms; form != NULL; form = form->next) { if (strlen(form->action) == len && strncasecmp(url, form->action, len) == 0) { form->i = 0; return form; } } form = (Form *)malloc(sizeof(Form)); form->next = forms; form->fields = NULL; form->action = strdup2(url, len); form->i = 0; /* reset field index */ forms = form; return form; } Form *DefaultForm(void) { return GetForm(CurrentDoc.url, strlen(CurrentDoc.url)); } /* font should be passed in lower 4 bits of flags */ Field *GetField(Form *form, int type, int x, char *name, int nlen, char *value, int vlen, int rows, int cols , int flags) { int em, font, y_above, y_below; Field *field; if (form == NULL) return NULL; /* if we are resizing the form or backtracking to it then we need to resuse the existing data structures to preserve any chances that have been made to the default values */ if (!new_form) { for (field = form->fields; field != NULL; field = field->next) { if (field->i == form->i) { form->i += 1; field->x = x; field->baseline = -1; /* invalidate position */ font = flags & 0x0F; goto set_size; } } Warn("Can't find field %d in form for %s", form->i, form->action); return NULL; } field = (Field *)malloc(sizeof(Field)); if (type == SUBMITBUTTON || type == RESETBUTTON) { flags &= 0xF0; flags |= IDX_H3FONT; } font = flags & 0x0F; field->next = form->fields; field->form = form; form->fields = field; field->i = form->i; form->i += 1; field->type = type; field->name = strdup2(name, nlen); field->value = strdup2(value, vlen); field->bufsize = vlen+1; field->buflen = vlen; field->flags = flags; field->x = x; field->x_indent = 0; field->y_indent = 0; field->baseline = -1; field->options = 0; em = WIDTH(font, "m", 1); set_size: field->j = 0; /* reset option index */ if (type == RADIOBUTTON || type == CHECKBOX) { field->width = ASCENT(font) + DESCENT(font) - 2; field->height = field->width; y_above = ASCENT(font) - 2; if (above < y_above) above = 1 + y_above; y_below = field->height - y_above; if (below < y_below) below = 1 + y_below; } else /* TEXTFIELD and OPTIONLIST */ { if (type == SUBMITBUTTON) field->width = 8 + WIDTH(font, " Submit Query ", 14); else if (type == RESETBUTTON) field->width = 8 + WIDTH(font, " Reset ", 7); else field->width = 8 + cols * em; field->height = 4 + rows * SPACING(Fonts[font]); y_above = ASCENT(font) + 2; if (above < y_above) above = 1 + y_above; y_below = field->height - y_above; if (below < y_below) below = 1 + y_below; } return field; } Option *AddOption(Field *field, int flags, char *label, int len) { int width, font; Option *option, *options; /* if we are resizing the form or backtracking to it then we need to resuse the existing data structures to preserve any chances that have been made to the default values */ if (!new_form) { for (option = field->options; option != NULL; option = option->next) { if (option->j == option->j) { field->j += 1; font = flags & 0x0F; width = 6 + WIDTH(font, label, len) + field->height; if (width > field->width) field->width = width; return option; } } Warn("Can't find option %d in field for %s", field->i, field->name); return NULL; } option = (Option *)malloc(sizeof(Option)); option->next = NULL; option->flags = flags; option->j = field->j; field->j += 1; option->label = strdup(label); font = flags & 0xF; width = 6 + WIDTH(font, label, len) + field->height; field->y_indent++; if (width > field->width) field->width = width; if (field->options) { options = field->options; while (options->next) options = options->next; options->next = option; } else field->options = option; } Field *CurrentRadioButton(Form *form, char *name) { Field *field; if (form == NULL) return NULL; field = form->fields; while (field) { if (strcmp(field->name, name) == 0 && (field->flags & CHECKED)) return field; field = field->next; } return field; } void HideDropDown(GC gc, Field *field) { int width, height, h, x1, y1; height = field->height; width = field->width - height + 2; x1 = field->x - PixelIndent; y1 = height + Abs2Win(field->baseline) - ASCENT(font) - 2; DisplayHTML(x1, y1, width, 6 + field->y_indent * (height - 4)); ClipToWindow(); } /* called when testing mouse down/up (event = BUTTONDOWN or BUTTONUP) */ int ClickedInField(GC gc, int baseline, Field *field, int x, int y, int event) { int y1; Field *field1; if (field->type == CHECKBOX || field->type == RADIOBUTTON) y1 = baseline - ASCENT(font) + 2; else if (field->type == OPTIONLIST) y1 = baseline - ASCENT(font) - 2; else /* TEXTFIELD */ y1 = baseline - ASCENT(font) - 2; if (field->x <= x && x < field->x + field->width && y1 <= y && y < y1 + field->height) { if (event == BUTTONUP) { /* remove focus from current field */ if (focus && focus != field) { focus->flags &= ~CHECKED; if (focus->type == OPTIONLIST) HideDropDown(gc, focus); else if (focus->type == TEXTFIELD) PaintField(gc, Abs2Win(focus->baseline), focus); focus = NULL; } if (field->type == RADIOBUTTON) { /* deselect matching radio button */ field1 = CurrentRadioButton(field->form, field->name); if (field1 && field1 != field) { field1->flags &= ~CHECKED; if (field1->baseline >= 0) PaintField(gc, Abs2Win(field1->baseline), field1); } if (!(field->flags & CHECKED)) { field->flags |= CHECKED; PaintField(gc, baseline, field); } } else if (field->type == CHECKBOX) { if (field->flags & CHECKED) field->flags &= ~CHECKED; else field->flags |= CHECKED; PaintField(gc, baseline, field); } if (field->type == OPTIONLIST) { if (field->flags & CHECKED) { field->flags &= ~CHECKED; HideDropDown(gc, field); } else { field->flags |= CHECKED; PaintDropDown(gc, field); focus = field; } } else if (field->type == TEXTFIELD) { field->flags |= CHECKED; PaintField(gc, baseline, field); focus = field; } } return 1; } return 0; } /* set clip rectangle to intersection with displayRect to clip text strings in text fields */ int ClipIntersection(GC gc, int x, int y, unsigned int width, unsigned int height) { int xl, yl, xm, ym; XRectangle rect; xl = x; xm = x + width; yl = y; ym = y + height; if (xl < displayRect.x) xl = displayRect.x; if (yl < displayRect.y) yl = displayRect.y; if (xm > displayRect.x + displayRect.width) xm = displayRect.x + displayRect.width; if (ym > displayRect.y + displayRect.height) ym = displayRect.y + displayRect.height; if (xm > xl && ym > yl) { rect.x = xl; rect.y = yl; rect.width = xm - xl; rect.height = ym - yl; XSetClipRectangles(display, gc, 0, 0, &rect, 1, Unsorted); } return 0; } int ClickedInDropDown(GC gc, Field *field, int event, int x, int y) { int font, baseline, bw, bh, dh, x1, y1, y2, n, i; Option *option; font = field->flags & 0x0F; baseline = Abs2Win(field->baseline); bw = field->width - field->height + 2; bh = 6 + field->y_indent * (field->height - 4); dh = field->height; x1 = field->x - PixelIndent; y1 = field->height + baseline - ASCENT(font) - 2; n = field->x_indent; if (x1 + 2 < x && x < x1 + bw - 2 && y1 + 2 < y && y < y1 + bh) { if (event == BUTTONDOWN) return 1; n = ((y - y1) * field->y_indent) / bh; focus->x_indent = n; option = field->options; while (n-- > 0) option = option->next; focus->flags &= ~CHECKED; free(focus->value); focus->value = strdup(option->label); ClipToWindow(); PaintField(gc, Abs2Win(focus->baseline), focus); HideDropDown(gc, focus); return 1; } return 0; } void PaintDropDown(GC gc, Field *field) { int font, baseline, width, height, h, x1, y1, y2, n, i; Option *option; baseline = Abs2Win(field->baseline); font = field->flags & 0x0F; SetFont(gc, font); height = field->height; width = field->width - height + 2; x1 = field->x - PixelIndent; y1 = height + baseline - ASCENT(font) - 2; n = field->x_indent; h = 6 + field->y_indent * (height - 4); y2 = baseline + height; option = field->options; XSetForeground(display, gc, windowColor); XFillRectangle(display, win, gc, x1, y1, width, h); DrawOutSet(gc, x1, y1, width, h); y1 += 2; for (option = field->options, i = 0; option; option = option->next) { if (i == n) { XSetForeground(display, gc, statusColor); XFillRectangle(display, win, gc, x1+2, y1, width-4, height-2); } XSetForeground(display, gc, textColor); XDrawString(display, win, gc, x1+4, y2, option->label, strlen(option->label)); y2 += height - 4; y1 += height - 4; ++i; } } void PaintTickMark(GC gc, int x, int y, unsigned int w, unsigned int h) { int x1, y1, x2, y2, x3, y3; x1 = x; x2 = x + w/3; x3 = x + w-1; y1 = y + h - h/3 - 1; y2 = y + h - 1; y3 = y; XSetForeground(display, gc, textColor); XDrawLine(display, win, gc, x1, y1, x2, y2); XDrawLine(display, win, gc, x2, y2, x3, y3); } void PaintCross(GC gc, int x, int y, unsigned int w, unsigned int h) { XSetForeground(display, gc, strikeColor); XDrawLine(display, win, gc, x, y, x+w, y+w); XDrawLine(display, win, gc, x, y+w, x+w, y); XSetForeground(display, gc, textColor); } void PaintField(GC gc, int baseline, Field *field) { char *s; int font, active, width, height, x1, y1, y2, r, n; Option *option; active = field->flags & CHECKED; font = field->flags & 0x0F; width = field->width; height = field->height; /* cache absolute position of baseline */ field->baseline = Win2Abs(baseline); if (field->type == TEXTFIELD) { x1 = field->x - PixelIndent - field->x_indent; y2 = baseline - ASCENT(font) - 2; ClipIntersection(gc, x1, y2, width, height); XSetForeground(display, gc, (active ? statusColor : windowColor)); XFillRectangle(display, win, gc, x1, y2, width, height); DrawInSet(gc, x1, y2, width, height); ClipIntersection(gc, x1+2, y2, width-4, height); if (field->buflen > 0) { y1 = y2 + ASCENT(font) + 2; SetFont(gc, font); XSetForeground(display, gc, textColor); XDrawString(display, win, gc, x1+4, y1, field->value, field->buflen); } if (active) { if (field->buflen > 0) r = WIDTH(font, field->value, cursorpos); else r = 0; XSetForeground(display, gc, strikeColor); XFillRectangle(display, win, gc, x1 + 3 + r, y2+2, 1, SPACING(Fonts[font])); } XSetForeground(display, gc, textColor); XSetClipRectangles(display, gc, 0, 0, &displayRect, 1, Unsorted); } else if (field->type == SUBMITBUTTON || field->type == RESETBUTTON) { x1 = field->x - PixelIndent - field->x_indent; y2 = baseline - ASCENT(font) - 2; ClipIntersection(gc, x1, y2, width, height); XSetForeground(display, gc, windowColor); XFillRectangle(display, win, gc, x1, y2, width, height); DrawOutSet(gc, x1, y2, width, height); ClipIntersection(gc, x1+2, y2, width-4, height); s = (field->type == SUBMITBUTTON ? " Submit Query " : " Reset "); if (field->buflen > 0) { y1 = y2 + ASCENT(font) + 2; SetFont(gc, font); XSetForeground(display, gc, textColor); XDrawString(display, win, gc, x1+4, y1, s, strlen(s)); } XSetForeground(display, gc, textColor); XSetClipRectangles(display, gc, 0, 0, &displayRect, 1, Unsorted); } else if (field->type == OPTIONLIST) { x1 = field->x - PixelIndent; y2 = baseline - ASCENT(font) - 2; XSetForeground(display, gc, windowColor); XFillRectangle(display, win, gc, x1, y2, width, height); DrawOutSet(gc, x1, y2, width, height); if (field->flags & MULTIPLE) { DrawOutSet(gc, x1+3+width-height, y2 - 2 + height/3, height-7, 6); DrawOutSet(gc, x1+3+width-height, y2 - 3 + 2*height/3, height-7, 6); } else /* single choice menu drawn with one bar */ DrawOutSet(gc, x1+3+width-height, y2 - 3 + height/2, height-7, 6); if (field->y_indent > 0) { option = field->options; n = field->x_indent; while (n-- > 0) option = option->next; y1 = y2 + ASCENT(font) + 2; SetFont(gc, font); XSetForeground(display, gc, textColor); XDrawString(display, win, gc, x1+4, y1, option->label, strlen(option->label)); } XSetForeground(display, gc, textColor); } else if (field->type == CHECKBOX) { x1 = field->x - PixelIndent; y2 = baseline-ASCENT(font) + 2; XSetForeground(display, gc, (active ? statusColor : windowColor)); XFillRectangle(display, win, gc, x1, y2, width, width); if (active) { PaintTickMark(gc, x1+3, y2+3, width-6, width-7); #if 0 XSetForeground(display, gc, windowBottomShadow); XFillRectangle(display, win, gc, x1+3, y2+3, width-6, width-6); #endif DrawInSet(gc, x1, y2, width, width); } else DrawOutSet(gc, x1, y2, width, width); XSetForeground(display, gc, textColor); } else if (field->type == RADIOBUTTON) { x1 = field->x - PixelIndent; y2 = baseline-ASCENT(font)+2; XSetForeground(display, gc, (active ? statusColor : windowColor)); XFillArc(display, win, gc, x1, y2, width, width, 0, 360<<6); if (active) { r = width/4; DrawInSetCircle(gc, x1, y2, width, width); XSetForeground(display, gc, windowBottomShadow); width -= r+r; XFillArc(display, win, gc, x1+r, y2+r, width, width, 0, 360<<6); } else DrawOutSetCircle(gc, x1, y2, width, width); XSetForeground(display, gc, textColor); } } /* use this routine to hide/show cursor by using color = windowShadow/textColor respectively */ void PaintFieldCursor(GC gc, unsigned long color) { int font, width, height, x1, y2, r; font = focus->flags & 0x0F; width = focus->width; height = focus->height; x1 = focus->x - PixelIndent - focus->x_indent; y2 = focus->baseline - ASCENT(font) - 2; if (focus->buflen > 0) r = WIDTH(font, focus->value, cursorpos); else r = 0; XSetForeground(display, gc, color); XFillRectangle(display, win, gc, x1 + 3 + r, y2+2, 1, height-4); } /* Repair a given area of a field - called by ScrollField() also useful when typing a char into field */ void RepairField(GC gc, int start, int width) { int font, height, x1, y1, y2, r; XRectangle rect; font = focus->flags & 0x0F; width = focus->width; height = focus->height; x1 = focus->x - PixelIndent - focus->x_indent; rect.x = start; rect.y = focus->baseline - ASCENT(font); rect.width = width; rect.height = focus->height - 4; XSetClipRectangles(display, gc, 0, 0, &rect, 1, Unsorted); y2 = focus->baseline - ASCENT(font) - 2; XSetForeground(display, gc, statusColor); XFillRectangle(display, win, gc, x1, y2, width, height); if (focus->buflen > 0) { y1 = y2 + ASCENT(font) + 2; SetFont(gc, font); XSetForeground(display, gc, textColor); XDrawString(display, win, gc, x1+4, y1, focus->value, focus->buflen); } if (focus->buflen > 0) r = WIDTH(font, focus->value, cursorpos); XSetForeground(display, gc, strikeColor); XFillRectangle(display, win, gc, x1 + 3 + r, y2+2, 1, height-4); rect.x = focus->x + 2; rect.y = focus->baseline - ASCENT(font); rect.width = focus->width - 4; rect.height = focus->height - 4; XSetClipRectangles(display, gc, 0, 0, &rect, 1, Unsorted); } /* Text field lies in rectangle box, scroll it horizontally by delta pixels and repair exposed area. When scrolling to left restrict scroll area to right of "start" delta is greater than zero for right scrolls and less than zero for left scrolls Useful for backspace, del char, and cursor motion at extreme limits of field */ void ScrollField(GC gc, int toleft, int start, int delta) { XRectangle rect; int width; rect.x = focus->x + 2; rect.y = focus->baseline - ASCENT(font); rect.width = focus->width - 4; rect.height = focus->height - 4; XSetClipRectangles(display, gc, 0, 0, &rect, 1, Unsorted); if (delta < 0) /* scroll left: delta < 0 */ { if (start <= rect.x) { XCopyArea(display, win, win, gc, rect.x - delta, rect.y, rect.width + delta, rect.y, rect.x, rect.y); RepairField(gc, rect.x + rect.width + delta, -delta); } else if (start < rect.x + width) { width = rect.width + rect.x - start + delta; XCopyArea(display, win, win, gc, start - delta, rect.y, width, rect.y, rect.x, rect.y); RepairField(gc, start + width, -delta); } } else /* scroll right: delta is > 0 */ { if (delta < rect.width) { XCopyArea(display, win, win, gc, rect.x, rect.y, rect.width - delta, rect.y, rect.x + delta, rect.y); RepairField(gc, rect.x, delta); } } }