netsurf/desktop/textinput.c
Adrian Lees f10db1f6f1 [project @ 2005-04-16 08:19:56 by adrianl]
missing #include

svn path=/import/netsurf/; revision=1651
2005-04-16 08:20:20 +00:00

840 lines
23 KiB
C

/*
* This file is part of NetSurf, http://netsurf.sourceforge.net/
* Licensed under the GNU General Public License,
* http://www.opensource.org/licenses/gpl-license
* Copyright 2003 Phil Mellor <monkeyson@users.sourceforge.net>
* Copyright 2004 James Bursa <bursa@users.sourceforge.net>
* Copyright 2004 Andrew Timmins <atimmins@blueyonder.co.uk>
* Copyright 2004 John Tytgat <John.Tytgat@aaug.net>
*/
/** \file
* Textual input handling (implementation)
*/
#include <assert.h>
#include <string.h>
#include "netsurf/desktop/browser.h"
#include "netsurf/desktop/gui.h"
#include "netsurf/desktop/selection.h"
#include "netsurf/desktop/textinput.h"
#include "netsurf/render/box.h"
#include "netsurf/render/font.h"
#include "netsurf/render/form.h"
#include "netsurf/render/layout.h"
#define NDEBUG
#include "netsurf/utils/log.h"
#include "netsurf/utils/talloc.h"
#include "netsurf/utils/utf8.h"
#include "netsurf/utils/utils.h"
static void browser_window_textarea_callback(struct browser_window *bw,
wchar_t key, void *p);
static void browser_window_input_callback(struct browser_window *bw,
wchar_t key, void *p);
static void browser_window_place_caret(struct browser_window *bw,
int x, int y, int height,
void (*callback)(struct browser_window *bw,
wchar_t key, void *p),
void *p);
/**
* Handle clicks in a text area by placing the caret.
*
* \param bw browser window where click occurred
* \param mouse state of mouse buttons and modifier keys
* \param textarea textarea box
* \param box_x position of textarea in global document coordinates
* \param box_y position of textarea in global document coordinates
* \param x coordinate of click relative to textarea
* \param y coordinate of click relative to textarea
*/
void browser_window_textarea_click(struct browser_window *bw,
browser_mouse_state mouse,
struct box *textarea,
int box_x, int box_y,
int x, int y)
{
/* A textarea is an INLINE_BLOCK containing a single
* INLINE_CONTAINER, which contains the text as runs of INLINE
* separated by BR. There is at least one INLINE. The first and
* last boxes are INLINE. Consecutive BR may not be present. These
* constraints are satisfied by using a 0-length INLINE for blank
* lines. */
int char_offset = 0, pixel_offset = 0, new_scroll_y;
struct box *inline_container, *text_box;
inline_container = textarea->children;
if (inline_container->y + inline_container->height < y) {
/* below the bottom of the textarea: place caret at end */
text_box = inline_container->last;
assert(text_box->type == BOX_INLINE);
assert(text_box->text);
/** \todo handle errors */
nsfont_position_in_string(text_box->style, text_box->text,
text_box->length,
textarea->width,
&char_offset, &pixel_offset);
} else {
/* find the relevant text box */
y -= inline_container->y;
for (text_box = inline_container->children;
text_box &&
text_box->y + text_box->height < y;
text_box = text_box->next)
;
for (; text_box && text_box->type != BOX_BR &&
text_box->y <= y &&
text_box->x + text_box->width < x;
text_box = text_box->next)
;
if (!text_box) {
/* past last text box */
text_box = inline_container->last;
assert(text_box->type == BOX_INLINE);
assert(text_box->text);
nsfont_position_in_string(text_box->style,
text_box->text,
text_box->length,
textarea->width,
&char_offset, &pixel_offset);
} else {
/* in a text box */
if (text_box->type == BOX_BR)
text_box = text_box->prev;
else if (y < text_box->y && text_box->prev) {
if (text_box->prev->type == BOX_BR) {
assert(text_box->prev->prev);
text_box = text_box->prev->prev;
}
else
text_box = text_box->prev;
}
assert(text_box->type == BOX_INLINE);
assert(text_box->text);
nsfont_position_in_string(text_box->style,
text_box->text,
text_box->length,
(unsigned int)(x - text_box->x),
&char_offset, &pixel_offset);
}
}
/* scroll to place the caret in the centre of the visible region */
new_scroll_y = inline_container->y + text_box->y +
text_box->height / 2 -
textarea->height / 2;
if (textarea->descendant_y1 - textarea->height < new_scroll_y)
new_scroll_y = textarea->descendant_y1 - textarea->height;
if (new_scroll_y < 0)
new_scroll_y = 0;
box_y += textarea->scroll_y - new_scroll_y;
textarea->gadget->caret_inline_container = inline_container;
textarea->gadget->caret_text_box = text_box;
textarea->gadget->caret_box_offset = char_offset;
textarea->gadget->caret_pixel_offset = pixel_offset;
browser_window_place_caret(bw,
box_x + inline_container->x + text_box->x +
pixel_offset,
box_y + inline_container->y + text_box->y,
text_box->height,
browser_window_textarea_callback, textarea);
if (new_scroll_y != textarea->scroll_y) {
textarea->scroll_y = new_scroll_y;
browser_redraw_box(bw->current_content, textarea);
}
}
/**
* Key press callback for text areas.
*
* \param bw The browser window containing the text area
* \param key The ucs4 character codepoint
* \param p The text area box
*/
void browser_window_textarea_callback(struct browser_window *bw,
wchar_t key, void *p)
{
struct box *textarea = p;
struct box *inline_container =
textarea->gadget->caret_inline_container;
struct box *text_box = textarea->gadget->caret_text_box;
struct box *new_br, *new_text, *t;
struct box *prev;
size_t char_offset = textarea->gadget->caret_box_offset;
int pixel_offset = textarea->gadget->caret_pixel_offset;
int new_scroll_y;
int box_x, box_y;
char utf8[6];
unsigned int utf8_len, i;
char *text;
int width = 0, height = 0;
bool reflow = false;
/* box_dump(textarea, 0); */
LOG(("key %i at %i in '%.*s'", key, char_offset,
(int) text_box->length, text_box->text));
box_coords(textarea, &box_x, &box_y);
box_x -= textarea->scroll_x;
box_y -= textarea->scroll_y;
if (!(key <= 0x001F || (0x007F <= key && key <= 0x009F))) {
/* normal character insertion */
utf8_len = utf8_from_ucs4(key, utf8);
text = talloc_realloc(bw->current_content, text_box->text,
char, text_box->length + 8);
if (!text) {
warn_user("NoMemory", 0);
return;
}
text_box->text = text;
memmove(text_box->text + char_offset + utf8_len,
text_box->text + char_offset,
text_box->length - char_offset);
for (i = 0; i != utf8_len; i++)
text_box->text[char_offset + i] = utf8[i];
text_box->length += utf8_len;
text_box->text[text_box->length] = 0;
text_box->width = UNKNOWN_WIDTH;
char_offset += utf8_len;
reflow = true;
} else switch (key) {
case 8:
case 127: /* delete to left */
if (char_offset == 0) {
/* at the start of a text box */
if (!text_box->prev)
/* at very beginning of text area: ignore */
return;
if (text_box->prev->type == BOX_BR) {
/* previous box is BR: remove it */
t = text_box->prev;
t->prev->next = t->next;
t->next->prev = t->prev;
box_free(t);
}
/* delete space by merging with previous text box */
prev = text_box->prev;
assert(prev->text);
text = talloc_realloc(bw->current_content,
prev->text, char,
prev->length + text_box->length + 1);
if (!text) {
warn_user("NoMemory", 0);
return;
}
prev->text = text;
memcpy(prev->text + prev->length, text_box->text,
text_box->length);
char_offset = prev->length; /* caret at join */
prev->length += text_box->length;
prev->text[prev->length] = 0;
prev->width = UNKNOWN_WIDTH;
prev->next = text_box->next;
if (prev->next)
prev->next->prev = prev;
else
prev->parent->last = prev;
box_free(text_box);
/* place caret at join (see above) */
text_box = prev;
} else {
/* delete a character */
int prev_offset = char_offset;
char_offset = utf8_prev(text_box->text, char_offset);
memmove(text_box->text + char_offset,
text_box->text + prev_offset,
text_box->length - prev_offset);
text_box->length -= (prev_offset - char_offset);
text_box->width = UNKNOWN_WIDTH;
}
reflow = true;
break;
case 10:
case 13: /* paragraph break */
text = talloc_array(bw->current_content, char,
text_box->length + 1);
if (!text) {
warn_user("NoMemory", 0);
return;
}
new_br = box_create(text_box->style, 0, text_box->title, 0,
bw->current_content);
new_text = talloc(bw->current_content, struct box);
if (!new_text) {
warn_user("NoMemory", 0);
return;
}
new_br->type = BOX_BR;
box_insert_sibling(text_box, new_br);
memcpy(new_text, text_box, sizeof (struct box));
new_text->clone = 1;
new_text->text = text;
memcpy(new_text->text, text_box->text + char_offset,
text_box->length - char_offset);
new_text->length = text_box->length - char_offset;
text_box->length = char_offset;
text_box->width = new_text->width = UNKNOWN_WIDTH;
box_insert_sibling(new_br, new_text);
/* place caret at start of new text box */
text_box = new_text;
char_offset = 0;
reflow = true;
break;
case 22: /* Ctrl + V */
// gui_paste_from_clipboard();
break;
case 24: /* Ctrl + X */
if (gui_copy_to_clipboard(bw->sel)) {
/** \todo block delete */
}
break;
case 28: /* Right cursor -> */
if ((unsigned int) char_offset != text_box->length) {
char_offset = utf8_next(text_box->text,
text_box->length, char_offset);
} else {
if (!text_box->next)
/* at end of text area: ignore */
return;
text_box = text_box->next;
if (text_box->type == BOX_BR)
text_box = text_box->next;
char_offset = 0;
}
break;
case 29: /* Left cursor <- */
if (char_offset != 0) {
char_offset = utf8_prev(text_box->text, char_offset);
} else {
if (!text_box->prev)
/* at start of text area: ignore */
return;
text_box = text_box->prev;
if (text_box->type == BOX_BR)
text_box = text_box->prev;
char_offset = text_box->length;
}
break;
case 30: /* Up cursor */
browser_window_textarea_click(bw, BROWSER_MOUSE_CLICK_1,
textarea,
box_x, box_y,
text_box->x + pixel_offset,
inline_container->y + text_box->y - 1);
return;
case 31: /* Down cursor */
browser_window_textarea_click(bw, BROWSER_MOUSE_CLICK_1,
textarea,
box_x, box_y,
text_box->x + pixel_offset,
inline_container->y + text_box->y +
text_box->height + 1);
return;
default:
return;
}
/* box_dump(textarea, 0); */
/* for (struct box *t = inline_container->children; t; t = t->next) {
assert(t->type == BOX_INLINE);
assert(t->text);
assert(t->font);
assert(t->parent == inline_container);
if (t->next) assert(t->next->prev == t);
if (t->prev) assert(t->prev->next == t);
if (!t->next) {
assert(inline_container->last == t);
break;
}
if (t->next->type == BOX_BR) {
assert(t->next->next);
t = t->next;
}
} */
if (reflow) {
/* reflow textarea preserving width and height */
width = textarea->width;
height = textarea->height;
if (!layout_inline_container(inline_container, width,
textarea, 0, 0,
bw->current_content))
warn_user("NoMemory", 0);
textarea->width = width;
textarea->height = height;
layout_calculate_descendant_bboxes(textarea);
}
if (text_box->length < char_offset) {
/* the text box has been split and the caret is in the
* second part */
char_offset -= (text_box->length + 1); /* +1 for the space */
text_box = text_box->next;
assert(text_box);
assert(char_offset <= text_box->length);
}
/* scroll to place the caret in the centre of the visible region */
new_scroll_y = inline_container->y + text_box->y +
text_box->height / 2 -
textarea->height / 2;
if (textarea->descendant_y1 - textarea->height < new_scroll_y)
new_scroll_y = textarea->descendant_y1 - textarea->height;
if (new_scroll_y < 0)
new_scroll_y = 0;
box_y += textarea->scroll_y - new_scroll_y;
nsfont_width(text_box->style, text_box->text,
char_offset, &pixel_offset);
textarea->gadget->caret_inline_container = inline_container;
textarea->gadget->caret_text_box = text_box;
textarea->gadget->caret_box_offset = char_offset;
textarea->gadget->caret_pixel_offset = pixel_offset;
browser_window_place_caret(bw,
box_x + inline_container->x + text_box->x +
pixel_offset,
box_y + inline_container->y + text_box->y,
text_box->height,
browser_window_textarea_callback, textarea);
if (new_scroll_y != textarea->scroll_y || reflow) {
textarea->scroll_y = new_scroll_y;
browser_redraw_box(bw->current_content, textarea);
}
}
/**
* Handle clicks in a text or password input box by placing the caret.
*
* \param bw browser window where click occurred
* \param input input box
* \param box_x position of input in global document coordinates
* \param box_y position of input in global document coordinates
* \param x coordinate of click relative to input
* \param y coordinate of click relative to input
*/
void browser_window_input_click(struct browser_window* bw,
struct box *input,
int box_x, int box_y,
int x, int y)
{
size_t char_offset = 0;
int pixel_offset = 0, dx = 0;
struct box *text_box = input->children->children;
int uchars;
unsigned int offset;
nsfont_position_in_string(text_box->style, text_box->text,
text_box->length, x - text_box->x,
&char_offset, &pixel_offset);
assert(char_offset <= text_box->length);
text_box->x = 0;
if ((input->width < text_box->width) &&
(input->width / 2 < pixel_offset)) {
dx = text_box->x;
text_box->x = input->width / 2 - pixel_offset;
if (text_box->x < input->width - text_box->width)
text_box->x = input->width - text_box->width;
dx -= text_box->x;
}
input->gadget->caret_box_offset = char_offset;
/* Update caret_form_offset */
for (uchars = 0, offset = 0; offset < char_offset; uchars++) {
if ((text_box->text[offset] & 0x80) == 0x00) {
offset++;
continue;
}
assert((text_box->text[offset] & 0xC0) == 0xC0);
for (++offset; offset < char_offset &&
(text_box->text[offset] & 0xC0) == 0x80;
offset++)
/* do nothing */;
}
/* uchars is the number of real Unicode characters at the left
* side of the caret.
*/
for (offset = 0; uchars > 0 && offset < input->gadget->length;
uchars--) {
if ((input->gadget->value[offset] & 0x80) == 0x00) {
offset++;
continue;
}
assert((input->gadget->value[offset] & 0xC0) == 0xC0);
for (++offset; offset < input->gadget->length &&
(input->gadget->value[offset] & 0xC0) == 0x80;
offset++)
/* do nothing */;
}
assert(uchars == 0);
input->gadget->caret_form_offset = offset;
input->gadget->caret_pixel_offset = pixel_offset;
browser_window_place_caret(bw,
box_x + input->children->x +
text_box->x + pixel_offset,
box_y + input->children->y + text_box->y,
text_box->height,
browser_window_input_callback, input);
if (dx)
browser_redraw_box(bw->current_content, input);
}
/**
* Key press callback for text or password input boxes.
*
* \param bw The browser window containing the input box
* \param key The UCS4 character codepoint
* \param p The input box
*/
void browser_window_input_callback(struct browser_window *bw,
wchar_t key, void *p)
{
struct box *input = (struct box *)p;
struct box *text_box = input->children->children;
unsigned int box_offset = input->gadget->caret_box_offset;
unsigned int form_offset = input->gadget->caret_form_offset;
int pixel_offset, dx;
int box_x, box_y;
struct form* form = input->gadget->form;
bool changed = false;
char utf8[6];
unsigned int utf8_len;
bool to_textarea = false;
box_coords(input, &box_x, &box_y);
if (!(key <= 0x001F || (0x007F <= key && key <= 0x009F))) {
char *value;
/* have we exceeded max length of input? */
utf8_len = utf8_length(input->gadget->value);
if (utf8_len >= input->gadget->maxlength)
return;
/* normal character insertion */
/* Insert key in gadget */
utf8_len = utf8_from_ucs4(key, utf8);
value = realloc(input->gadget->value,
input->gadget->length + utf8_len + 1);
if (!value) {
warn_user("NoMemory", 0);
return;
}
input->gadget->value = value;
memmove(input->gadget->value + form_offset + utf8_len,
input->gadget->value + form_offset,
input->gadget->length - form_offset);
memcpy(input->gadget->value + form_offset, utf8, utf8_len);
input->gadget->length += utf8_len;
input->gadget->value[input->gadget->length] = 0;
form_offset += utf8_len;
/* Insert key in text box */
/* Convert space into NBSP */
utf8_len = utf8_from_ucs4(
(input->gadget->type == GADGET_PASSWORD) ?
'*' : (key == ' ') ? 160 : key,
utf8);
value = talloc_realloc(bw->current_content, text_box->text,
char, text_box->length + utf8_len + 1);
if (!value) {
warn_user("NoMemory", 0);
return;
}
text_box->text = value;
memmove(text_box->text + box_offset + utf8_len,
text_box->text + box_offset,
text_box->length - box_offset);
memcpy(text_box->text + box_offset, utf8, utf8_len);
text_box->length += utf8_len;
text_box->text[text_box->length] = 0;
box_offset += utf8_len;
nsfont_width(text_box->style, text_box->text,
text_box->length, &text_box->width);
changed = true;
} else switch (key) {
case 8:
case 127: { /* Delete to left */
int prev_offset;
if (box_offset == 0)
return;
/* Gadget */
prev_offset = form_offset;
/* Go to the previous valid UTF-8 character */
form_offset = utf8_prev(input->gadget->value,
form_offset);
memmove(input->gadget->value + form_offset,
input->gadget->value + prev_offset,
input->gadget->length - prev_offset);
input->gadget->length -= prev_offset - form_offset;
input->gadget->value[input->gadget->length] = 0;
/* Text box */
prev_offset = box_offset;
/* Go to the previous valid UTF-8 character */
box_offset = utf8_prev(text_box->text, box_offset);
memmove(text_box->text + box_offset,
text_box->text + prev_offset,
text_box->length - prev_offset);
text_box->length -= prev_offset - box_offset;
text_box->text[text_box->length] = 0;
nsfont_width(text_box->style, text_box->text,
text_box->length, &text_box->width);
changed = true;
}
break;
case 9: { /* Tab */
struct form_control *next_input;
for (next_input = input->gadget->next;
next_input &&
next_input->type != GADGET_TEXTBOX &&
next_input->type != GADGET_TEXTAREA &&
next_input->type != GADGET_PASSWORD;
next_input = next_input->next)
;
if (!next_input)
return;
input = next_input->box;
text_box = input->children->children;
box_coords(input, &box_x, &box_y);
form_offset = box_offset = 0;
to_textarea = next_input->type == GADGET_TEXTAREA;
}
break;
case 10:
case 13: /* Return/Enter hit */
if (form)
browser_form_submit(bw, form, 0);
return;
case 11: { /* Shift + Tab */
struct form_control *prev_input;
for (prev_input = input->gadget->prev;
prev_input &&
prev_input->type != GADGET_TEXTBOX &&
prev_input->type != GADGET_TEXTAREA &&
prev_input->type != GADGET_PASSWORD;
prev_input = prev_input->prev)
;
if (!prev_input)
return;
input = prev_input->box;
text_box = input->children->children;
box_coords(input, &box_x, &box_y);
form_offset = box_offset = 0;
to_textarea = prev_input->type == GADGET_TEXTAREA;
}
break;
case 21: /* Ctrl + U */
text_box->text[0] = 0;
text_box->length = 0;
box_offset = 0;
input->gadget->value[0] = 0;
input->gadget->length = 0;
form_offset = 0;
text_box->width = 0;
changed = true;
break;
case 22: /* Ctrl + V */
// gui_paste_from_clipboard();
break;
case 28: /* Right cursor -> */
/* Text box */
/* Go to the next valid UTF-8 character */
box_offset = utf8_next(text_box->text, text_box->length,
box_offset);
/* Gadget */
/* Go to the next valid UTF-8 character */
form_offset = utf8_next(input->gadget->value,
input->gadget->length, form_offset);
break;
case 29: /* Left cursor -> */
/* Text box */
/* Go to the previous valid UTF-8 character */
box_offset = utf8_prev(text_box->text, box_offset);
/* Gadget */
/* Go to the previous valid UTF-8 character */
form_offset = utf8_prev(input->gadget->value, form_offset);
break;
case 128: /* Ctrl + Left */
box_offset = form_offset = 0;
break;
case 129: /* Ctrl + Right */
box_offset = text_box->length;
form_offset = input->gadget->length;
break;
default:
return;
}
nsfont_width(text_box->style, text_box->text, box_offset,
&pixel_offset);
dx = text_box->x;
text_box->x = 0;
if (input->width < text_box->width &&
input->width / 2 < pixel_offset) {
text_box->x = input->width / 2 - pixel_offset;
if (text_box->x < input->width - text_box->width)
text_box->x = input->width - text_box->width;
}
dx -= text_box->x;
input->gadget->caret_pixel_offset = pixel_offset;
if (to_textarea) {
/* moving to textarea so need to set these up */
input->gadget->caret_inline_container = input->children;
input->gadget->caret_text_box = text_box;
}
input->gadget->caret_box_offset = box_offset;
input->gadget->caret_form_offset = form_offset;
browser_window_place_caret(bw,
box_x + input->children->x +
text_box->x + pixel_offset,
box_y + input->children->y + text_box->y,
text_box->height,
/* use the appropriate callback */
to_textarea ? browser_window_textarea_callback
: browser_window_input_callback, input);
if (dx || changed)
browser_redraw_box(bw->current_content, input);
}
/**
* Position the caret and assign a callback for key presses.
*
* \param bw The browser window in which to place the caret
* \param x X coordinate of the caret
* \param y Y coordinate
* \param height Height of caret
* \param callback Callback function for keypresses
* \param p Callback private data pointer, passed to callback function
*/
void browser_window_place_caret(struct browser_window *bw,
int x, int y, int height,
void (*callback)(struct browser_window *bw,
wchar_t key, void *p),
void *p)
{
gui_window_place_caret(bw->window, x, y, height);
bw->caret_callback = callback;
bw->caret_p = p;
}
/**
* Removes the caret and callback for key process.
*
* \param bw The browser window from which to remove caret
*/
void browser_window_remove_caret(struct browser_window *bw)
{
gui_window_remove_caret(bw->window);
bw->caret_callback = NULL;
bw->caret_p = NULL;
}
/**
* Handle key presses in a browser window.
*
* \param bw The browser window with input focus
* \param key The UCS4 character codepoint
* \return true if key handled, false otherwise
*/
bool browser_window_key_press(struct browser_window *bw, wchar_t key)
{
/* keys that take effect wherever the caret is positioned */
switch (key) {
case 1: /* Ctrl + A */
selection_select_all(bw->sel);
return true;
case 3: /* Ctrl + C */
gui_copy_to_clipboard(bw->sel);
return true;
case 26: /* Ctrl + Z */
selection_clear(bw->sel, true);
return true;
case 27: /** Escape */
if (selection_defined(bw->sel)) {
selection_clear(bw->sel, true);
return true;
}
break;
}
/* pass on to the appropriate field */
if (!bw->caret_callback)
return false;
bw->caret_callback(bw, key, bw->caret_p);
return true;
}