mirror of
https://github.com/netsurf-browser/netsurf
synced 2024-11-28 17:23:09 +03:00
e7850d9762
svn://svn.netsurf-browser.org/branches/mikeL/netsurf ........ r4432 | mikeL | 2008-06-24 04:00:36 +0100 (Tue, 24 Jun 2008) | 1 line Drag events are now emited from where the press originated, instead of from where they became a drag ........ r4433 | mikeL | 2008-06-24 04:25:33 +0100 (Tue, 24 Jun 2008) | 1 line Added accelerator to 'Select All' ........ r4495 | mikeL | 2008-07-02 21:36:32 +0100 (Wed, 02 Jul 2008) | 1 line Selections are now deleted and replaced when a key is typed in a text area or text box. All input box behavior while a selection is active is now implemented (ex: pressing the right arrow key moves the caret to the end of the selection). Cut now works properly in both versions. Fixed discrepancy between where the caret was placed and selection began when starting a drag-select. Fixed bug with calculation of a selections end box. ........ r4496 | mikeL | 2008-07-02 22:11:24 +0100 (Wed, 02 Jul 2008) | 1 line Added support for cut in input boxes ........ r4497 | mikeL | 2008-07-02 22:21:35 +0100 (Wed, 02 Jul 2008) | 1 line Removed unused variables (Thanks tlsa) ........ r4498 | mikeL | 2008-07-02 23:30:30 +0100 (Wed, 02 Jul 2008) | 1 line Modified selection clearing behavior to allow for drag-saves ........ r4499 | mikeL | 2008-07-03 00:51:50 +0100 (Thu, 03 Jul 2008) | 1 line Fixed regression where it would take two clicks to place caret in an input (Thanks tlsa) ........ r4509 | mikeL | 2008-07-06 07:55:09 +0100 (Sun, 06 Jul 2008) | 1 line Basic download support implemented. Only downloading of text files works ........ r4510 | mikeL | 2008-07-06 18:55:31 +0100 (Sun, 06 Jul 2008) | 1 line Downloading of non-text files is now possible. Progress bar and size downloaded are now updated ........ r4511 | mikeL | 2008-07-06 20:46:00 +0100 (Sun, 06 Jul 2008) | 1 line Added downloads glade file ........ r4512 | mikeL | 2008-07-06 20:47:39 +0100 (Sun, 06 Jul 2008) | 1 line Downloads window now spawns in the center of the parent browser window ........ r4513 | mikeL | 2008-07-06 20:56:12 +0100 (Sun, 06 Jul 2008) | 1 line Fixed bug where backspace would be ignored if the selection began at the beginning on an input ........ r4514 | mikeL | 2008-07-06 21:26:45 +0100 (Sun, 06 Jul 2008) | 1 line Fixed compiler warnings by adding casts ........ r4516 | mikeL | 2008-07-06 21:32:41 +0100 (Sun, 06 Jul 2008) | 1 line Fixed initialization of size string, added initialization of progress ........ r4518 | mikeL | 2008-07-06 21:51:08 +0100 (Sun, 06 Jul 2008) | 1 line Added an option for short units (with space) to human_friendly_bytesize ........ r4519 | mikeL | 2008-07-06 21:52:05 +0100 (Sun, 06 Jul 2008) | 1 line Removed function size_to_string ........ r4520 | mikeL | 2008-07-06 22:03:11 +0100 (Sun, 06 Jul 2008) | 1 line Fixed pedantic error (kB instead of KB). Added missing necessary parameters to human_friendly_bytesize. Fixed incorrect bool types ........ r4521 | mikeL | 2008-07-06 22:08:15 +0100 (Sun, 06 Jul 2008) | 1 line Removed unnecessary parameter and units list from human_friendly_bytesize ........ r4522 | mikeL | 2008-07-06 22:57:03 +0100 (Sun, 06 Jul 2008) | 1 line Removed unnused variable ........ r4523 | mikeL | 2008-07-06 23:03:46 +0100 (Sun, 06 Jul 2008) | 1 line Fixed url parsing by replacing url_parse_filename with url_nice. Total size and size downloaded are now in human readable form. Speed is now calculated (roughly) ........ r4524 | mikeL | 2008-07-07 01:19:01 +0100 (Mon, 07 Jul 2008) | 1 line Added file overwrite confirmation. Changed speed to a double ........ r4546 | mikeL | 2008-07-09 17:21:43 +0100 (Wed, 09 Jul 2008) | 1 line Changed parameter of selection_get_end/start to a size_t instead of int and changed all offset variables to size_t as well ........ r4547 | mikeL | 2008-07-09 17:30:47 +0100 (Wed, 09 Jul 2008) | 1 line Added action buttons to the bottom toolbar. Added ability to clear selected (and completed) downloads with a framework for other actions. ........ r4556 | jmb | 2008-07-10 00:17:24 +0100 (Thu, 10 Jul 2008) | 3 lines A large bunch of tidying and general fixes to text input handling. Make selection code treat password fields as inputs, too. ........ r4557 | mikeL | 2008-07-10 00:24:46 +0100 (Thu, 10 Jul 2008) | 1 line Added functionality to gui_empty_clipboard and gui_start_selection (Thanks jmb) ........ r4573 | mikeL | 2008-07-10 16:33:27 +0100 (Thu, 10 Jul 2008) | 1 line Removed example download. Made the list store row aware of its download and vise versa. Added new way of handling actions from the dialog (e.g. buttons) which handles all rows in the selection. Added a few memory management function calls to clean up better ........ r4577 | mikeL | 2008-07-10 21:11:51 +0100 (Thu, 10 Jul 2008) | 1 line Download write channels now close properly. Added status column to the tree store which will change the progress bar text if a download is canceled or completed. Implemented cancel button functionality. ........ r4578 | mikeL | 2008-07-10 21:17:51 +0100 (Thu, 10 Jul 2008) | 1 line Speed is now displayed as '-' when 0 or download has stopped ........ r4580 | mikeL | 2008-07-11 02:10:57 +0100 (Fri, 11 Jul 2008) | 1 line Added two download related options (Download directory & Clear completed downloads) and a Downloads tab to the preferences dialog. Also moved the option to ask when overwriting files to Downloads tab. Added another option to the pre-download dialog, Save, which downloads the file immediately to your 'Download directory' ........ r4581 | mikeL | 2008-07-11 02:26:00 +0100 (Fri, 11 Jul 2008) | 1 line Rearranged pre-download dialog buttons to conform to the HIG ........ r4616 | mikeL | 2008-07-11 19:54:12 +0100 (Fri, 11 Jul 2008) | 1 line Limited download window updates to a user-defined variable that can be set in options (default is .5). Updates are now only done if the download window is visible. This greatly reduces the cpu usage. ........ r4617 | mikeL | 2008-07-11 20:07:48 +0100 (Fri, 11 Jul 2008) | 1 line Removed unnecessary update limit option (it is now fixed at .5) ........ r4629 | mikeL | 2008-07-13 04:21:07 +0100 (Sun, 13 Jul 2008) | 1 line Reorganized button sensitivity functions. Sensitivities are now updated when the selection changes as well as when a selected download's state changes. ........ r4635 | mikeL | 2008-07-13 17:00:05 +0100 (Sun, 13 Jul 2008) | 1 line Added error handling. Added word-wrap to the "info" cell renderer so that to keep everything under control. Fixed bug where downloads would always go to you default folder (missing breaks). ........ r4642 | mikeL | 2008-07-13 21:46:09 +0100 (Sun, 13 Jul 2008) | 1 line Added time remaining column. Fixed regression where the download info would be erased upon completion/cancelation. ........ r4655 | mikeL | 2008-07-14 23:20:33 +0100 (Mon, 14 Jul 2008) | 1 line Downloads dialog is now initialized in gtk_gui.c with no parent window. The parent windows are now set when a download is created (through an extra parameter in gui_download_window_create) and when nsgtk_download_show is called. When it is closed (when NS shuts down) all incomplete downloads are canceled (and the files deleted). Added a warning dialog when netsurf tries to close with incomplete downloads. Fixed bug where default save directory would initialize to NULL. ........ r4676 | mikeL | 2008-07-15 21:01:17 +0100 (Tue, 15 Jul 2008) | 1 line Downloads dialog is now initialized in gtk_gui.c with no parent window. The parent windows are now set when a download is created (through an extra parameter in gui_download_window_create) and when nsgtk_download_show is called. (This is the second half of the patch, last commit was only partial for some reason) ........ r4678 | mikeL | 2008-07-16 01:18:52 +0100 (Wed, 16 Jul 2008) | 1 line Addresses almost all of rjek and jmb's concerns, fixes most of the sloppiness that was present earlier. Downloads without a total_size are now handled correctly (thanks tlsa). Changes to the default download directly are now saved correctly. Billions of other small bugs fixed ........ svn path=/trunk/netsurf/; revision=4681
2179 lines
56 KiB
C
2179 lines
56 KiB
C
/*
|
|
* 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>
|
|
* Copyright 2005 Adrian Lees <adrianl@users.sourceforge.net>
|
|
*
|
|
* This file is part of NetSurf, http://www.netsurf-browser.org/
|
|
*
|
|
* NetSurf is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; version 2 of the License.
|
|
*
|
|
* NetSurf is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/** \file
|
|
* Textual input handling (implementation)
|
|
*/
|
|
|
|
#include <assert.h>
|
|
#include <ctype.h>
|
|
#include <string.h>
|
|
|
|
#include "desktop/browser.h"
|
|
#include "desktop/gui.h"
|
|
#include "desktop/selection.h"
|
|
#include "desktop/textinput.h"
|
|
#include "render/box.h"
|
|
#include "render/font.h"
|
|
#include "render/form.h"
|
|
#include "render/layout.h"
|
|
#define NDEBUG
|
|
#include "utils/log.h"
|
|
#include "utils/talloc.h"
|
|
#include "utils/utf8.h"
|
|
#include "utils/utils.h"
|
|
|
|
|
|
/** ghost caret used to indicate the insertion point when dragging text
|
|
into a textarea/input field */
|
|
struct caret ghost_caret;
|
|
|
|
|
|
static bool browser_window_textarea_callback(struct browser_window *bw,
|
|
uint32_t key, void *p);
|
|
static bool browser_window_input_callback(struct browser_window *bw,
|
|
uint32_t key, void *p);
|
|
static void browser_window_place_caret(struct browser_window *bw,
|
|
int x, int y, int height,
|
|
browser_caret_callback caret_cb,
|
|
browser_paste_callback paste_cb,
|
|
browser_move_callback move_cb,
|
|
void *p);
|
|
static bool browser_window_textarea_paste_text(struct browser_window *bw,
|
|
const char *utf8, unsigned utf8_len, bool last, void *handle);
|
|
static bool browser_window_input_paste_text(struct browser_window *bw,
|
|
const char *utf8, unsigned utf8_len, bool last, void *handle);
|
|
static void browser_window_textarea_move_caret(struct browser_window *bw,
|
|
void *p);
|
|
static void browser_window_input_move_caret(struct browser_window *bw, void *p);
|
|
static void input_update_display(struct browser_window *bw, struct box *input,
|
|
unsigned box_offset, bool to_textarea, bool redraw);
|
|
static size_t get_form_offset(struct box* input, struct box* text_box,
|
|
size_t char_offset);
|
|
static bool textbox_insert(struct browser_window *bw, struct box *text_box,
|
|
unsigned char_offset, const char *utf8, unsigned utf8_len);
|
|
static bool textbox_delete(struct browser_window *bw, struct box *text_box,
|
|
unsigned char_offset, unsigned utf8_len);
|
|
static struct box *textarea_insert_break(struct browser_window *bw,
|
|
struct box *text_box, size_t char_offset);
|
|
static bool delete_handler(struct browser_window *bw, struct box *b,
|
|
int offset, size_t length);
|
|
static void delete_selection(struct selection *s);
|
|
static struct box *line_start(struct box *text_box);
|
|
static struct box *line_end(struct box *text_box);
|
|
static struct box *line_above(struct box *text_box);
|
|
static struct box *line_below(struct box *text_box);
|
|
static bool textarea_cut(struct browser_window *bw,
|
|
struct box *start_box, unsigned start_idx,
|
|
struct box *end_box, unsigned end_idx);
|
|
static void textarea_reflow(struct browser_window *bw, struct box *textarea,
|
|
struct box *inline_container);
|
|
static bool word_left(const char *text, size_t *poffset, size_t *pchars);
|
|
static bool word_right(const char *text, size_t len, size_t *poffset,
|
|
size_t *pchars);
|
|
static bool ensure_caret_visible(struct box *textarea);
|
|
|
|
/**
|
|
* Remove the given text caret from the window by invalidating it
|
|
* and causing its former position to be redrawn.
|
|
*
|
|
* \param c structure describing text caret
|
|
*/
|
|
|
|
void caret_remove(struct caret *c)
|
|
{
|
|
if (c->defined) {
|
|
int w = (c->height + 7) / 8;
|
|
int xc = c->x;
|
|
c->defined = false;
|
|
browser_window_redraw_rect(c->bw,
|
|
xc - w, c->y, 2 * w, c->height);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Set the given text caret's position within the window (text box
|
|
* and byte/pixel offsets within the UTF-8 content of that text box)
|
|
* and draw it.
|
|
*
|
|
* \param c structure describing text caret
|
|
* \param bw browser window containing caret
|
|
* \param box TEXT box containing caret
|
|
* \param char_offset byte offset within UTF-8 representation
|
|
* \param pixel_offset from left side of box
|
|
*/
|
|
|
|
void caret_set_position(struct caret *c, struct browser_window *bw,
|
|
struct box *text_box, int char_offset, int pixel_offset)
|
|
{
|
|
struct rect r;
|
|
int xc;
|
|
int w;
|
|
|
|
box_bounds(text_box, &r);
|
|
|
|
c->bw = bw;
|
|
c->text_box = text_box;
|
|
c->char_offset = char_offset;
|
|
|
|
c->x = xc = r.x0 + pixel_offset;
|
|
c->y = r.y0;
|
|
c->height = r.y1 - r.y0;
|
|
w = (c->height + 7) / 8;
|
|
|
|
c->defined = true;
|
|
|
|
browser_window_redraw_rect(c->bw, xc - w, c->y, w * 2, c->height);
|
|
}
|
|
|
|
|
|
/**
|
|
* Given the x,y co-ordinates of a point within a textarea, return the
|
|
* TEXT box pointer, and the character and pixel offsets within that
|
|
* box at which the caret should be positioned. (eg. for mouse clicks,
|
|
* drag-and-drop insertions etc)
|
|
*
|
|
* \param textarea the textarea being considered
|
|
* \param x x ordinate of point
|
|
* \param y y ordinate of point
|
|
* \param pchar_offset receives the char offset within the TEXT box
|
|
* \param ppixel_offset receives the pixel offset within the TEXT box
|
|
* \return pointer to TEXT box
|
|
*/
|
|
|
|
struct box *textarea_get_position(struct box *textarea, int x, int y,
|
|
int *pchar_offset, int *ppixel_offset)
|
|
{
|
|
/* A textarea is an INLINE_BLOCK containing a single
|
|
* INLINE_CONTAINER, which contains the text as runs of TEXT
|
|
* separated by BR. There is at least one TEXT. The first and
|
|
* last boxes are TEXT. Consecutive BR may not be present. These
|
|
* constraints are satisfied by using a 0-length TEXT for blank
|
|
* lines. */
|
|
|
|
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_TEXT);
|
|
assert(text_box->text);
|
|
/** \todo handle errors */
|
|
nsfont_position_in_string(text_box->style, text_box->text,
|
|
text_box->length,
|
|
(unsigned int)(x - text_box->x),
|
|
(size_t *) pchar_offset, ppixel_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_TEXT);
|
|
assert(text_box->text);
|
|
nsfont_position_in_string(text_box->style,
|
|
text_box->text,
|
|
text_box->length,
|
|
textarea->width,
|
|
(size_t *) pchar_offset,
|
|
ppixel_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_TEXT);
|
|
assert(text_box->text);
|
|
nsfont_position_in_string(text_box->style,
|
|
text_box->text,
|
|
text_box->length,
|
|
(unsigned int)(x - text_box->x),
|
|
(size_t *) pchar_offset,
|
|
ppixel_offset);
|
|
}
|
|
}
|
|
|
|
assert(text_box);
|
|
return text_box;
|
|
}
|
|
|
|
|
|
/**
|
|
* 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 TEXT
|
|
* separated by BR. There is at least one TEXT. The first and
|
|
* last boxes are TEXT. Consecutive BR may not be present. These
|
|
* constraints are satisfied by using a 0-length TEXT for blank
|
|
* lines. */
|
|
|
|
int char_offset = 0, pixel_offset = 0;
|
|
struct box *inline_container = textarea->children;
|
|
struct box *text_box;
|
|
bool scrolled;
|
|
|
|
text_box = textarea_get_position(textarea, x, y,
|
|
&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;
|
|
|
|
box_x += textarea->scroll_x;
|
|
box_y += textarea->scroll_y;
|
|
scrolled = ensure_caret_visible(textarea);
|
|
box_x -= textarea->scroll_x;
|
|
box_y -= textarea->scroll_y;
|
|
|
|
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,
|
|
browser_window_textarea_paste_text,
|
|
browser_window_textarea_move_caret,
|
|
textarea);
|
|
|
|
if (scrolled)
|
|
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
|
|
* \return true if the keypress is dealt with, false otherwise. It can
|
|
* return true even if it ran out of memory; this just means that
|
|
* it would have claimed it if it could.
|
|
*/
|
|
bool browser_window_textarea_callback(struct browser_window *bw,
|
|
uint32_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_text;
|
|
size_t char_offset = textarea->gadget->caret_box_offset;
|
|
int pixel_offset = textarea->gadget->caret_pixel_offset;
|
|
int box_x, box_y;
|
|
char utf8[6];
|
|
unsigned int utf8_len;
|
|
bool scrolled,reflow = false;
|
|
bool selection_exists = bw->sel->defined;
|
|
|
|
/* 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);
|
|
|
|
if (!textbox_insert(bw, text_box, char_offset, utf8, utf8_len))
|
|
return true;
|
|
|
|
char_offset += utf8_len;
|
|
reflow = true;
|
|
|
|
} else switch (key) {
|
|
case KEY_DELETE_LEFT:
|
|
if (selection_exists) {
|
|
/* Have a selection; delete it */
|
|
textbox_delete(bw, text_box, 0, 0);
|
|
} else if (char_offset == 0) {
|
|
/* at the start of a text box */
|
|
struct box *prev;
|
|
|
|
if (text_box->prev && text_box->prev->type == BOX_BR) {
|
|
/* previous box is BR: remove it */
|
|
box_unlink_and_free(text_box->prev);
|
|
}
|
|
|
|
/* This needs to be after the BR removal, as that may
|
|
* result in no previous box existing */
|
|
if (!text_box->prev)
|
|
/* at very beginning of text area: ignore */
|
|
return true;
|
|
|
|
/* delete space by merging with previous text box */
|
|
prev = text_box->prev;
|
|
assert(prev->type == BOX_TEXT);
|
|
assert(prev->text);
|
|
|
|
char_offset = prev->length; /* caret at join */
|
|
|
|
if (!textbox_insert(bw, prev, prev->length,
|
|
text_box->text, text_box->length))
|
|
return true;
|
|
|
|
box_unlink_and_free(text_box);
|
|
|
|
/* place caret at join (see above) */
|
|
text_box = prev;
|
|
|
|
} else {
|
|
/* delete a character */
|
|
size_t prev_offset = char_offset;
|
|
size_t new_offset =
|
|
utf8_prev(text_box->text, char_offset);
|
|
|
|
if (textbox_delete(bw, text_box, new_offset,
|
|
prev_offset - new_offset))
|
|
char_offset = new_offset;
|
|
}
|
|
reflow = true;
|
|
break;
|
|
|
|
case KEY_DELETE_LINE_START:
|
|
{
|
|
struct box *start_box = line_start(text_box);
|
|
|
|
/* Clear the selection, if one exists */
|
|
if (selection_exists)
|
|
selection_clear(bw->sel, false);
|
|
|
|
textarea_cut(bw, start_box, 0, text_box, char_offset);
|
|
text_box = start_box;
|
|
char_offset = 0;
|
|
reflow = true;
|
|
}
|
|
break;
|
|
|
|
case KEY_DELETE_LINE_END:
|
|
{
|
|
struct box *end_box = line_end(text_box);
|
|
|
|
/* Clear the selection, if one exists */
|
|
if (selection_exists)
|
|
selection_clear(bw->sel, false);
|
|
|
|
if (end_box != text_box ||
|
|
char_offset < text_box->length + text_box->space) {
|
|
/* there's something at the end of the line to delete */
|
|
textarea_cut(bw, text_box, char_offset,
|
|
end_box, end_box->length + end_box->space);
|
|
reflow = true;
|
|
break;
|
|
}
|
|
}
|
|
/* no break */
|
|
case KEY_DELETE_RIGHT: /* delete to right */
|
|
if (selection_exists) {
|
|
/* Delete selection */
|
|
textbox_delete(bw, text_box, 0, 0);
|
|
} else if (char_offset >= text_box->length) {
|
|
/* at the end of a text box */
|
|
struct box *next;
|
|
|
|
if (text_box->next && text_box->next->type == BOX_BR) {
|
|
/* next box is a BR: remove it */
|
|
box_unlink_and_free(text_box->next);
|
|
}
|
|
|
|
/* This test is after the BR removal, as that may
|
|
* result in no subsequent box being present */
|
|
if (!text_box->next)
|
|
/* at very end of text area: ignore */
|
|
return true;
|
|
|
|
/* delete space by merging with next text box */
|
|
|
|
next = text_box->next;
|
|
assert(next->type == BOX_TEXT);
|
|
assert(next->text);
|
|
|
|
if (!textbox_insert(bw, text_box, text_box->length,
|
|
next->text, next->length))
|
|
return true;
|
|
|
|
box_unlink_and_free(next);
|
|
|
|
/* leave caret at join */
|
|
reflow = true;
|
|
}
|
|
else {
|
|
/* delete a character */
|
|
size_t next_offset = utf8_next(text_box->text,
|
|
text_box->length, char_offset);
|
|
|
|
textbox_delete(bw, text_box, char_offset,
|
|
next_offset - char_offset);
|
|
}
|
|
reflow = true;
|
|
break;
|
|
|
|
case 10:
|
|
case 13: /* paragraph break */
|
|
if (selection_exists) {
|
|
/* If we have a selection, then delete it,
|
|
* so it's replaced by the break */
|
|
textbox_delete(bw, text_box, 0, 0);
|
|
}
|
|
|
|
new_text = textarea_insert_break(bw, text_box, char_offset);
|
|
if (!new_text)
|
|
return true;
|
|
|
|
/* place caret at start of new text box */
|
|
text_box = new_text;
|
|
char_offset = 0;
|
|
|
|
reflow = true;
|
|
break;
|
|
|
|
case 21: /* Ctrl + U */
|
|
{
|
|
struct box *start_box = line_start(text_box);
|
|
struct box *end_box = line_end(text_box);
|
|
|
|
/* Clear the selection, if one exists */
|
|
if (selection_exists)
|
|
selection_clear(bw->sel, false);
|
|
|
|
textarea_cut(bw, start_box, 0, end_box, end_box->length);
|
|
|
|
text_box = start_box;
|
|
char_offset = 0;
|
|
reflow = true;
|
|
}
|
|
break;
|
|
|
|
case 22: /* Ctrl + V */
|
|
gui_paste_from_clipboard(bw->window,
|
|
box_x + inline_container->x +
|
|
text_box->x + pixel_offset,
|
|
box_y + inline_container->y + text_box->y);
|
|
|
|
/* screen updated and caret repositioned already */
|
|
return true;
|
|
|
|
case 24: /* Ctrl + X */
|
|
{
|
|
size_t start_idx, end_idx;
|
|
struct box *start_box =
|
|
selection_get_start(bw->sel, &start_idx);
|
|
struct box *end_box = selection_get_end(bw->sel, &end_idx);
|
|
|
|
if (start_box && end_box) {
|
|
selection_clear(bw->sel, false);
|
|
textarea_cut(bw, start_box, start_idx,
|
|
end_box, end_idx);
|
|
text_box = start_box;
|
|
char_offset = start_idx;
|
|
reflow = true;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEY_RIGHT:
|
|
if (selection_exists) {
|
|
/* In selection, move caret to end */
|
|
text_box = selection_get_end(bw->sel, &char_offset);
|
|
} else if (char_offset < text_box->length) {
|
|
/* Within-box movement */
|
|
char_offset = utf8_next(text_box->text,
|
|
text_box->length, char_offset);
|
|
} else {
|
|
/* Between-box movement */
|
|
if (!text_box->next)
|
|
/* at end of text area: ignore */
|
|
return true;
|
|
|
|
text_box = text_box->next;
|
|
if (text_box->type == BOX_BR)
|
|
text_box = text_box->next;
|
|
char_offset = 0;
|
|
}
|
|
break;
|
|
|
|
case KEY_LEFT:
|
|
if (selection_exists) {
|
|
/* In selection, move caret to start */
|
|
text_box = selection_get_start(bw->sel, &char_offset);
|
|
} else if (char_offset > 0) {
|
|
/* Within-box movement */
|
|
char_offset = utf8_prev(text_box->text, char_offset);
|
|
} else {
|
|
/* Between-box movement */
|
|
if (!text_box->prev)
|
|
/* at start of text area: ignore */
|
|
return true;
|
|
|
|
text_box = text_box->prev;
|
|
if (text_box->type == BOX_BR)
|
|
text_box = text_box->prev;
|
|
char_offset = text_box->length;
|
|
}
|
|
break;
|
|
|
|
case KEY_UP:
|
|
selection_clear(bw->sel, true);
|
|
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 true;
|
|
|
|
case KEY_DOWN:
|
|
selection_clear(bw->sel, true);
|
|
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 true;
|
|
|
|
case KEY_LINE_START:
|
|
text_box = line_start(text_box);
|
|
char_offset = 0;
|
|
break;
|
|
|
|
case KEY_LINE_END:
|
|
text_box = line_end(text_box);
|
|
char_offset = text_box->length;
|
|
break;
|
|
|
|
case KEY_TEXT_START:
|
|
assert(text_box->parent);
|
|
|
|
/* place caret at start of first box */
|
|
text_box = text_box->parent->children;
|
|
char_offset = 0;
|
|
break;
|
|
|
|
case KEY_TEXT_END:
|
|
assert(text_box->parent);
|
|
|
|
/* place caret at end of last box */
|
|
text_box = text_box->parent->last;
|
|
char_offset = text_box->length;
|
|
break;
|
|
|
|
case KEY_WORD_LEFT:
|
|
{
|
|
/* if there is a selection, caret should stay at beginning */
|
|
if (selection_exists)
|
|
break;
|
|
|
|
bool start_of_word = (char_offset <= 0 ||
|
|
isspace(text_box->text[char_offset - 1]));
|
|
|
|
while (!word_left(text_box->text, &char_offset, NULL)) {
|
|
struct box *prev = NULL;
|
|
|
|
assert(char_offset == 0);
|
|
|
|
if (start_of_word) {
|
|
/* find the preceding non-BR box */
|
|
prev = text_box->prev;
|
|
if (prev && prev->type == BOX_BR)
|
|
prev = prev->prev;
|
|
}
|
|
|
|
if (!prev) {
|
|
/* just stay at the start of this box */
|
|
break;
|
|
}
|
|
|
|
assert(prev->type == BOX_TEXT);
|
|
|
|
text_box = prev;
|
|
char_offset = prev->length;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEY_WORD_RIGHT:
|
|
{
|
|
/* if there is a selection, caret should move to the end */
|
|
if (selection_exists) {
|
|
text_box = selection_get_end(bw->sel, &char_offset);
|
|
break;
|
|
}
|
|
|
|
bool in_word = (char_offset < text_box->length &&
|
|
!isspace(text_box->text[char_offset]));
|
|
|
|
while (!word_right(text_box->text, text_box->length,
|
|
&char_offset, NULL)) {
|
|
struct box *next = text_box->next;
|
|
|
|
/* find the next non-BR box */
|
|
if (next && next->type == BOX_BR)
|
|
next = next->next;
|
|
|
|
if (!next) {
|
|
/* just stay at the end of this box */
|
|
char_offset = text_box->length;
|
|
break;
|
|
}
|
|
|
|
assert(next->type == BOX_TEXT);
|
|
|
|
text_box = next;
|
|
char_offset = 0;
|
|
|
|
if (in_word && text_box->length > 0 &&
|
|
!isspace(text_box->text[0])) {
|
|
/* just stay at the start of this box */
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEY_PAGE_UP:
|
|
{
|
|
int nlines = (textarea->height / text_box->height) - 1;
|
|
|
|
while (nlines-- > 0)
|
|
text_box = line_above(text_box);
|
|
|
|
if (char_offset > text_box->length)
|
|
char_offset = text_box->length;
|
|
}
|
|
break;
|
|
|
|
case KEY_PAGE_DOWN:
|
|
{
|
|
int nlines = (textarea->height / text_box->height) - 1;
|
|
|
|
while (nlines-- > 0)
|
|
text_box = line_below(text_box);
|
|
|
|
/* vague attempt to keep the caret at the same horizontal
|
|
* position, given that the code currently cannot support it
|
|
* being beyond the end of a line */
|
|
if (char_offset > text_box->length)
|
|
char_offset = text_box->length;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
box_dump(textarea, 0);
|
|
for (struct box *t = inline_container->children; t; t = t->next) {
|
|
assert(t->type == BOX_TEXT);
|
|
assert(t->text);
|
|
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)
|
|
textarea_reflow(bw, textarea, inline_container);
|
|
|
|
if (text_box->length + text_box->space <= char_offset) {
|
|
if (text_box->next && text_box->next->type == BOX_TEXT) {
|
|
/* the text box has been split when reflowing and
|
|
the caret is in the second part */
|
|
char_offset -= (text_box->length + text_box->space);
|
|
text_box = text_box->next;
|
|
assert(text_box);
|
|
assert(char_offset <= text_box->length);
|
|
/* Scroll back to the left */
|
|
box_x += textarea->scroll_x;
|
|
textarea->scroll_x = 0;
|
|
} else {
|
|
assert(!text_box->next ||
|
|
(text_box->next &&
|
|
text_box->next->type == BOX_BR));
|
|
|
|
char_offset = text_box->length + text_box->space;
|
|
}
|
|
}
|
|
|
|
nsfont_width(text_box->style, text_box->text,
|
|
char_offset, &pixel_offset);
|
|
|
|
selection_clear(bw->sel, true);
|
|
|
|
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,
|
|
browser_window_textarea_paste_text,
|
|
browser_window_textarea_move_caret,
|
|
textarea);
|
|
|
|
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;
|
|
|
|
box_x += textarea->scroll_x;
|
|
box_y += textarea->scroll_y;
|
|
scrolled = ensure_caret_visible(textarea);
|
|
box_x -= textarea->scroll_x;
|
|
box_y -= textarea->scroll_y;
|
|
|
|
if (scrolled || reflow)
|
|
browser_redraw_box(bw->current_content, textarea);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* 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;
|
|
|
|
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);
|
|
|
|
/* Shift the text box horizontally to ensure that the
|
|
* caret position is visible, and ideally centred */
|
|
text_box->x = 0;
|
|
if ((input->width < text_box->width) &&
|
|
(input->width / 2 < pixel_offset)) {
|
|
dx = text_box->x;
|
|
/* Move left so caret is centred */
|
|
text_box->x = input->width / 2 - pixel_offset;
|
|
/* Clamp, so text box's right hand edge coincides
|
|
* with the input's right hand edge */
|
|
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;
|
|
input->gadget->caret_form_offset =
|
|
get_form_offset(input, text_box, char_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,
|
|
browser_window_input_paste_text,
|
|
browser_window_input_move_caret,
|
|
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
|
|
* \return true if the keypress is dealt with, false otherwise. It can
|
|
* return true even if it ran out of memory; this just means that
|
|
* it would have claimed it if it could.
|
|
*/
|
|
bool browser_window_input_callback(struct browser_window *bw,
|
|
uint32_t key, void *p)
|
|
{
|
|
struct box *input = (struct box *)p;
|
|
struct box *text_box = input->children->children;
|
|
size_t box_offset = input->gadget->caret_box_offset;
|
|
size_t form_offset = input->gadget->caret_form_offset =
|
|
get_form_offset(input, text_box, box_offset);
|
|
size_t end_offset;
|
|
int pixel_offset = input->gadget->caret_pixel_offset;
|
|
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;
|
|
bool selection_exists = bw->sel->defined;
|
|
|
|
selection_get_end(bw->sel, &end_offset);
|
|
|
|
box_coords(input, &box_x, &box_y);
|
|
|
|
/* normal character insertion */
|
|
if (!(key <= 0x001F || (0x007F <= key && key <= 0x009F))) {
|
|
/* have we exceeded max length of input? */
|
|
utf8_len = utf8_length(input->gadget->value);
|
|
if (utf8_len >= input->gadget->maxlength)
|
|
return true;
|
|
|
|
utf8_len = utf8_from_ucs4(key, utf8);
|
|
|
|
if (!textbox_insert(bw, text_box, box_offset, utf8, utf8_len))
|
|
return true;
|
|
|
|
box_offset += utf8_len;
|
|
|
|
changed = true;
|
|
|
|
} else switch (key) {
|
|
case KEY_DELETE_LEFT:
|
|
{
|
|
int prev_offset, new_offset;
|
|
|
|
if (selection_exists) {
|
|
textbox_delete(bw, text_box, 0, 0);
|
|
} else {
|
|
/* Can't delete left from text box start */
|
|
if (box_offset == 0)
|
|
return true;
|
|
|
|
prev_offset = box_offset;
|
|
new_offset = utf8_prev(text_box->text, box_offset);
|
|
|
|
if (textbox_delete(bw, text_box, new_offset,
|
|
prev_offset - new_offset))
|
|
box_offset = new_offset;
|
|
}
|
|
|
|
changed = true;
|
|
}
|
|
break;
|
|
|
|
case KEY_DELETE_RIGHT:
|
|
{
|
|
unsigned next_offset;
|
|
|
|
if (selection_exists) {
|
|
textbox_delete(bw, text_box, 0, 0);
|
|
} else {
|
|
/* Can't delete right from text box end */
|
|
if (box_offset >= text_box->length)
|
|
return true;
|
|
|
|
/* Go to the next valid UTF-8 character */
|
|
next_offset = utf8_next(text_box->text,
|
|
text_box->length, box_offset);
|
|
|
|
textbox_delete(bw, text_box, box_offset,
|
|
next_offset - box_offset);
|
|
}
|
|
|
|
changed = true;
|
|
}
|
|
break;
|
|
|
|
case 9: /* Tab */
|
|
{
|
|
struct form_control *next_input;
|
|
/* Find next text entry field that is actually
|
|
* displayed (i.e. has an associated box) */
|
|
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->box);
|
|
next_input = next_input->next)
|
|
;
|
|
if (!next_input)
|
|
return true;
|
|
|
|
input = next_input->box;
|
|
text_box = input->children->children;
|
|
box_offset = 0;
|
|
to_textarea = next_input->type == GADGET_TEXTAREA;
|
|
}
|
|
break;
|
|
|
|
case 10:
|
|
case 13: /* Return/Enter hit */
|
|
selection_clear(bw->sel, true);
|
|
|
|
if (form)
|
|
browser_form_submit(bw, bw, form, 0);
|
|
return true;
|
|
|
|
case 11: /* Shift + Tab */
|
|
{
|
|
struct form_control *prev_input;
|
|
/* Find previous text entry field that is actually
|
|
* displayed (i.e. has an associated box) */
|
|
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->box);
|
|
prev_input = prev_input->prev)
|
|
;
|
|
if (!prev_input)
|
|
return true;
|
|
|
|
input = prev_input->box;
|
|
text_box = input->children->children;
|
|
box_offset = 0;
|
|
to_textarea = prev_input->type == GADGET_TEXTAREA;
|
|
}
|
|
break;
|
|
|
|
case 21: /* Ctrl + U */
|
|
/* Clear the selection, if one exists */
|
|
if (selection_exists)
|
|
selection_clear(bw->sel, false);
|
|
|
|
textarea_cut(bw, text_box, 0, text_box, text_box->length);
|
|
box_offset = 0;
|
|
|
|
changed = true;
|
|
break;
|
|
|
|
case 22: /* Ctrl + V */
|
|
gui_paste_from_clipboard(bw->window,
|
|
box_x + input->children->x + text_box->x + pixel_offset,
|
|
box_y + input->children->y + text_box->y);
|
|
|
|
/* screen updated and caret repositioned already */
|
|
return true;
|
|
|
|
case 24: /* Ctrl + X */
|
|
{
|
|
size_t start_idx, end_idx;
|
|
struct box *start_box =
|
|
selection_get_start(bw->sel, &start_idx);
|
|
struct box *end_box = selection_get_end(bw->sel, &end_idx);
|
|
|
|
if (start_box && end_box) {
|
|
selection_clear(bw->sel, false);
|
|
textarea_cut(bw, start_box, start_idx,
|
|
end_box, end_idx);
|
|
|
|
text_box = start_box;
|
|
box_offset = start_idx;
|
|
changed = true;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEY_RIGHT:
|
|
if (selection_exists) {
|
|
box_offset = end_offset;
|
|
break;
|
|
}
|
|
|
|
/* Go to the next valid UTF-8 character */
|
|
box_offset = utf8_next(text_box->text, text_box->length,
|
|
box_offset);
|
|
break;
|
|
|
|
case KEY_LEFT:
|
|
/* If there is a selection, caret should remain at start */
|
|
if (selection_exists)
|
|
break;
|
|
|
|
/* Go to the previous valid UTF-8 character */
|
|
box_offset = utf8_prev(text_box->text, box_offset);
|
|
break;
|
|
|
|
case KEY_LINE_START:
|
|
box_offset = 0;
|
|
break;
|
|
|
|
case KEY_LINE_END:
|
|
box_offset = text_box->length;
|
|
break;
|
|
|
|
case KEY_WORD_LEFT:
|
|
/* If there is a selection, caret should remain at start */
|
|
if (selection_exists)
|
|
break;
|
|
|
|
if (!word_left(text_box->text, &box_offset, NULL))
|
|
box_offset = 0;
|
|
|
|
break;
|
|
|
|
case KEY_WORD_RIGHT:
|
|
if (selection_exists) {
|
|
box_offset = end_offset;
|
|
break;
|
|
}
|
|
|
|
if (!word_right(text_box->text, text_box->length,
|
|
&box_offset, NULL))
|
|
box_offset = text_box->length;
|
|
|
|
break;
|
|
|
|
case KEY_DELETE_LINE_START:
|
|
if (selection_exists)
|
|
selection_clear(bw->sel, true);
|
|
|
|
if (box_offset == 0)
|
|
return true;
|
|
|
|
textarea_cut(bw, text_box, 0, text_box, box_offset);
|
|
box_offset = 0;
|
|
|
|
changed = true;
|
|
break;
|
|
|
|
case KEY_DELETE_LINE_END:
|
|
if (selection_exists)
|
|
selection_clear(bw->sel, true);
|
|
|
|
if (box_offset >= text_box->length)
|
|
return true;
|
|
|
|
textarea_cut(bw, text_box, box_offset,
|
|
text_box, text_box->length);
|
|
|
|
changed = true;
|
|
break;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
selection_clear(bw->sel, true);
|
|
input_update_display(bw, input, box_offset, to_textarea, changed);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* 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 caret_cb Callback function for keypresses
|
|
* \param paste_cb Callback function for pasting text
|
|
* \param move_cb Callback function for caret movement
|
|
* \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,
|
|
browser_caret_callback caret_cb,
|
|
browser_paste_callback paste_cb,
|
|
browser_move_callback move_cb,
|
|
void *p)
|
|
{
|
|
gui_window_place_caret(bw->window, x, y, height);
|
|
bw->caret_callback = caret_cb;
|
|
bw->paste_callback = paste_cb;
|
|
bw->move_callback = move_cb;
|
|
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->paste_callback = NULL;
|
|
bw->move_callback = NULL;
|
|
bw->caret_p = NULL;
|
|
|
|
selection_clear(bw->sel, true);
|
|
}
|
|
|
|
/**
|
|
* Calculates the form_offset from the box_offset
|
|
*
|
|
* \param input The root box containing both the textbox and gadget
|
|
* \param text_box The textbox containing the caret
|
|
* \param char_offset The caret offset within text_box
|
|
* \return the translated form_offset
|
|
*/
|
|
|
|
size_t get_form_offset(struct box* input, struct box* text_box,
|
|
size_t char_offset)
|
|
{
|
|
int uchars;
|
|
unsigned int 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);
|
|
return offset;
|
|
}
|
|
|
|
/**
|
|
* 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, uint32_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;
|
|
}
|
|
/* if there's no selection,
|
|
* leave Escape for the caller */
|
|
return false;
|
|
}
|
|
|
|
/* pass on to the appropriate field */
|
|
if (!bw->caret_callback)
|
|
return false;
|
|
|
|
return bw->caret_callback(bw, key, bw->caret_p);
|
|
}
|
|
|
|
|
|
/**
|
|
* Paste a block of text into a browser window at the caret position.
|
|
*
|
|
* \param bw browser window
|
|
* \param utf8 pointer to block of text
|
|
* \param utf8_len length (bytes) of text block
|
|
* \param last true iff this is the last chunk (update screen too)
|
|
* \return true iff successful
|
|
*/
|
|
|
|
bool browser_window_paste_text(struct browser_window *bw, const char *utf8,
|
|
unsigned utf8_len, bool last)
|
|
{
|
|
if (!bw->paste_callback)
|
|
return false;
|
|
|
|
return bw->paste_callback(bw, utf8, utf8_len, last, bw->caret_p);
|
|
}
|
|
|
|
|
|
/**
|
|
* Paste a block of text into a textarea at the
|
|
* current caret position.
|
|
*
|
|
* \param bw browser window
|
|
* \param utf8 pointer to block of text
|
|
* \param utf8_len length (bytes) of text block
|
|
* \param last true iff this is the last chunk (update screen too)
|
|
* \param handle pointer to textarea
|
|
* \return true iff successful
|
|
*/
|
|
|
|
bool browser_window_textarea_paste_text(struct browser_window *bw,
|
|
const char *utf8, unsigned utf8_len, bool last, void *handle)
|
|
{
|
|
struct box *textarea = handle;
|
|
struct box *inline_container =
|
|
textarea->gadget->caret_inline_container;
|
|
struct box *text_box = textarea->gadget->caret_text_box;
|
|
size_t char_offset = textarea->gadget->caret_box_offset;
|
|
int pixel_offset = textarea->gadget->caret_pixel_offset;
|
|
const char *ep = utf8 + utf8_len;
|
|
const char *p = utf8;
|
|
bool success = true;
|
|
bool update = last;
|
|
|
|
while (p < ep) {
|
|
struct box *new_text;
|
|
unsigned utf8_len;
|
|
|
|
while (p < ep) {
|
|
if (*p == '\n' || *p == '\r') break;
|
|
p++;
|
|
}
|
|
|
|
utf8_len = p - utf8;
|
|
if (!textbox_insert(bw, text_box, char_offset, utf8, utf8_len))
|
|
return false;
|
|
|
|
char_offset += utf8_len;
|
|
if (p == ep)
|
|
break;
|
|
|
|
new_text = textarea_insert_break(bw, text_box, char_offset);
|
|
if (!new_text) {
|
|
/* we still need to update the screen */
|
|
update = true;
|
|
success = false;
|
|
break;
|
|
}
|
|
|
|
/* place caret at start of new text box */
|
|
text_box = new_text;
|
|
char_offset = 0;
|
|
|
|
/* handle CR/LF and LF/CR terminations */
|
|
if ((*p == '\n' && p[1] == '\r') ||
|
|
(*p == '\r' && p[1] == '\n'))
|
|
p++;
|
|
utf8 = ++p;
|
|
}
|
|
|
|
// textarea->gadget->caret_inline_container = inline_container;
|
|
textarea->gadget->caret_text_box = text_box;
|
|
textarea->gadget->caret_box_offset = char_offset;
|
|
|
|
if (update) {
|
|
int box_x, box_y;
|
|
|
|
/* reflow textarea preserving width and height */
|
|
textarea_reflow(bw, textarea, inline_container);
|
|
/* reflowing may have broken our caret offset
|
|
* this bit should hopefully continue to work if
|
|
* textarea_reflow is fixed to update the caret itself */
|
|
char_offset = textarea->gadget->caret_box_offset;
|
|
text_box = textarea->gadget->caret_text_box;
|
|
|
|
while ((char_offset > text_box->length + text_box->space) &&
|
|
(text_box->next) &&
|
|
(text_box->next->type == BOX_TEXT)) {
|
|
LOG(("Caret out of range: Was %d in boxlen %d "
|
|
"space %d", char_offset,
|
|
text_box->length, text_box->space));
|
|
char_offset -= text_box->length + text_box->space;
|
|
text_box = text_box->next;
|
|
}
|
|
|
|
/* not sure if this will happen or not...
|
|
* but won't stick an assert here as we can recover from it */
|
|
if (char_offset > text_box->length) {
|
|
LOG(("Caret moved beyond end of line: "
|
|
"Was %d in boxlen %d", char_offset,
|
|
text_box->length));
|
|
char_offset = text_box->length;
|
|
}
|
|
|
|
textarea->gadget->caret_text_box = text_box;
|
|
textarea->gadget->caret_box_offset = char_offset;
|
|
|
|
nsfont_width(text_box->style, text_box->text,
|
|
char_offset, &pixel_offset);
|
|
|
|
textarea->gadget->caret_pixel_offset = pixel_offset;
|
|
|
|
box_coords(textarea, &box_x, &box_y);
|
|
box_x += textarea->scroll_x;
|
|
box_y += textarea->scroll_y;
|
|
ensure_caret_visible(textarea);
|
|
box_x -= textarea->scroll_x;
|
|
box_y -= textarea->scroll_y;
|
|
|
|
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,
|
|
browser_window_textarea_paste_text,
|
|
browser_window_textarea_move_caret,
|
|
textarea);
|
|
|
|
browser_redraw_box(bw->current_content, textarea);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
|
|
/**
|
|
* Paste a block of text into an input field at the caret position.
|
|
*
|
|
* \param bw browser window
|
|
* \param utf8 pointer to block of text
|
|
* \param utf8_len length (bytes) of text block
|
|
* \param last true iff this is the last chunk (update screen too)
|
|
* \param p pointer to input box
|
|
* \return true iff successful
|
|
*/
|
|
|
|
bool browser_window_input_paste_text(struct browser_window *bw,
|
|
const char *utf8, unsigned utf8_len, bool last, void *handle)
|
|
{
|
|
struct box *input = handle;
|
|
struct box *text_box = input->children->children;
|
|
size_t box_offset = input->gadget->caret_box_offset;
|
|
unsigned int nchars = utf8_length(input->gadget->value);
|
|
const char *ep = utf8 + utf8_len;
|
|
const char *p = utf8;
|
|
bool success = true;
|
|
bool update = last;
|
|
|
|
/* keep adding chars until we've run out or would exceed
|
|
the maximum length of the field (in which we silently
|
|
ignore all others)
|
|
*/
|
|
while (p < ep && nchars < input->gadget->maxlength) {
|
|
char buf[80 + 6];
|
|
int nbytes = 0;
|
|
unsigned utf8_len;
|
|
|
|
/* how many more chars can we insert in one go? */
|
|
while (p < ep && nbytes < 80 &&
|
|
nchars < input->gadget->maxlength &&
|
|
*p != '\n' && *p != '\r') {
|
|
unsigned len = utf8_next(p, ep - p, 0);
|
|
|
|
if (input->gadget->type == GADGET_PASSWORD)
|
|
buf[nbytes++] = '*';
|
|
else if (*p == ' ')
|
|
nbytes += utf8_from_ucs4(160, &buf[nbytes]);
|
|
else {
|
|
memcpy(&buf[nbytes], p, len);
|
|
nbytes += len;
|
|
}
|
|
|
|
p += len;
|
|
nchars++;
|
|
}
|
|
|
|
utf8_len = p - utf8;
|
|
|
|
if (!textbox_insert(bw, text_box, box_offset, buf, nbytes)) {
|
|
/* we still need to update the screen */
|
|
update = true;
|
|
success = false;
|
|
break;
|
|
}
|
|
box_offset += nbytes;
|
|
/* Keep caret_form_offset in sync -- textbox_insert uses this
|
|
* to determine where to insert into the gadget's value */
|
|
input->gadget->caret_form_offset += nbytes;
|
|
|
|
/* handle CR/LF and LF/CR terminations */
|
|
if (*p == '\n') {
|
|
p++;
|
|
if (*p == '\r') p++;
|
|
}
|
|
else if (*p == '\r') {
|
|
p++;
|
|
if (*p == '\n') p++;
|
|
}
|
|
utf8 = p;
|
|
}
|
|
|
|
if (update)
|
|
input_update_display(bw, input, box_offset, false, true);
|
|
|
|
return success;
|
|
}
|
|
|
|
|
|
/**
|
|
* Move caret to new position after reformatting
|
|
*
|
|
* \param bw browser window
|
|
* \param p pointer textarea box
|
|
* \return none
|
|
*/
|
|
|
|
void browser_window_textarea_move_caret(struct browser_window *bw, void *p)
|
|
{
|
|
struct box *textarea = p;
|
|
struct box *inline_container = textarea->gadget->caret_inline_container;
|
|
struct box *text_box = textarea->gadget->caret_text_box;
|
|
size_t char_offset = textarea->gadget->caret_box_offset;
|
|
int pixel_offset;
|
|
int box_x, box_y;
|
|
|
|
box_coords(textarea, &box_x, &box_y);
|
|
box_x -= textarea->scroll_x;
|
|
box_y -= textarea->scroll_y;
|
|
|
|
nsfont_width(text_box->style, text_box->text,
|
|
char_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,
|
|
browser_window_textarea_paste_text,
|
|
browser_window_textarea_move_caret,
|
|
textarea);
|
|
}
|
|
|
|
|
|
/**
|
|
* Move caret to new position after reformatting
|
|
*
|
|
* \param bw browser window
|
|
* \param p pointer to text input box
|
|
* \return none
|
|
*/
|
|
|
|
void browser_window_input_move_caret(struct browser_window *bw, void *p)
|
|
{
|
|
struct box *input = (struct box *)p;
|
|
struct box *text_box = input->children->children;
|
|
unsigned int box_offset = input->gadget->caret_box_offset;
|
|
int pixel_offset;
|
|
int box_x, box_y;
|
|
|
|
box_coords(input, &box_x, &box_y);
|
|
|
|
nsfont_width(text_box->style, text_box->text, box_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,
|
|
browser_window_input_paste_text,
|
|
browser_window_input_move_caret,
|
|
input);
|
|
}
|
|
|
|
|
|
/**
|
|
* Update display to reflect modified input field
|
|
*
|
|
* \param bw browser window
|
|
* \param input input field
|
|
* \param form_offset
|
|
* \param box_offset offset of caret within text box
|
|
* \param to_textarea caret is to be moved to a textarea
|
|
* \param redraw force redraw even if field hasn't scrolled
|
|
*/
|
|
|
|
void input_update_display(struct browser_window *bw, struct box *input,
|
|
unsigned box_offset, bool to_textarea, bool redraw)
|
|
{
|
|
struct box *text_box = input->children->children;
|
|
unsigned pixel_offset;
|
|
int box_x, box_y;
|
|
int dx;
|
|
|
|
if (redraw)
|
|
nsfont_width(text_box->style, text_box->text, text_box->length,
|
|
&text_box->width);
|
|
|
|
box_coords(input, &box_x, &box_y);
|
|
|
|
nsfont_width(text_box->style, text_box->text, box_offset,
|
|
(int *) &pixel_offset);
|
|
|
|
/* Shift text box horizontally, so caret is visible */
|
|
dx = text_box->x;
|
|
text_box->x = 0;
|
|
if (input->width < text_box->width &&
|
|
input->width / 2 < (int) pixel_offset) {
|
|
/* Make caret appear in centre of text input */
|
|
text_box->x = input->width / 2 - pixel_offset;
|
|
/* Clamp if we've shifted too far left */
|
|
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;
|
|
|
|
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,
|
|
to_textarea ? browser_window_textarea_paste_text
|
|
: browser_window_input_paste_text,
|
|
to_textarea ? browser_window_textarea_move_caret
|
|
: browser_window_input_move_caret,
|
|
input);
|
|
|
|
if (dx || redraw)
|
|
browser_redraw_box(bw->current_content, input);
|
|
}
|
|
|
|
|
|
/**
|
|
* Insert a number of chars into a text box
|
|
*
|
|
* \param bw browser window
|
|
* \param text_box text box
|
|
* \param char_offset offset (bytes) at which to insert text
|
|
* \param utf8 UTF-8 text to insert
|
|
* \param utf8_len length (bytes) of UTF-8 text to insert
|
|
* \return true iff successful
|
|
*/
|
|
|
|
bool textbox_insert(struct browser_window *bw, struct box *text_box,
|
|
unsigned char_offset, const char *utf8, unsigned utf8_len)
|
|
{
|
|
char *text;
|
|
struct box *input = text_box->parent->parent;
|
|
|
|
if (bw->sel->defined)
|
|
delete_selection(bw->sel);
|
|
|
|
/* insert into form gadget (text and password inputs only) */
|
|
if (input->gadget && (input->gadget->type == GADGET_TEXTBOX ||
|
|
input->gadget->type == GADGET_PASSWORD) &&
|
|
input->gadget->value) {
|
|
size_t form_offset = input->gadget->caret_form_offset;
|
|
char *value = realloc(input->gadget->value,
|
|
input->gadget->length + utf8_len + 1);
|
|
if (!value) {
|
|
warn_user("NoMemory", 0);
|
|
return true;
|
|
}
|
|
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;
|
|
}
|
|
|
|
/* insert in text box */
|
|
text = talloc_realloc(bw->current_content, text_box->text,
|
|
char,
|
|
text_box->length + text_box->space + utf8_len + 1);
|
|
if (!text) {
|
|
warn_user("NoMemory", 0);
|
|
return false;
|
|
}
|
|
text_box->text = text;
|
|
|
|
if (text_box->space &&
|
|
char_offset == text_box->length + text_box->space) {
|
|
unsigned int last_off = utf8_prev(utf8, utf8_len);
|
|
|
|
if (utf8[last_off] != ' ')
|
|
text_box->space = 0;
|
|
else
|
|
utf8_len = last_off;
|
|
|
|
text_box->text[text_box->length++] = ' ';
|
|
} else {
|
|
memmove(text_box->text + char_offset + utf8_len,
|
|
text_box->text + char_offset,
|
|
text_box->length - char_offset);
|
|
}
|
|
|
|
memcpy(text_box->text + char_offset,
|
|
input->gadget->type == GADGET_PASSWORD ? "*": utf8,
|
|
utf8_len);
|
|
text_box->length += utf8_len;
|
|
|
|
/* nothing should assume that the text is terminated,
|
|
* but just in case */
|
|
text_box->text[text_box->length] = 0;
|
|
|
|
text_box->width = UNKNOWN_WIDTH;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Delete a number of chars from a text box
|
|
*
|
|
* \param bw browser window
|
|
* \param text_box text box
|
|
* \param char_offset offset within text box (bytes) of first char to delete
|
|
* \param utf8_len length (bytes) of chars to be deleted
|
|
* \return true on success, false otherwise
|
|
*
|
|
* ::char_offset and ::utf8_len are only considered when there is no selection.
|
|
* If there is a selection, the entire selected area is deleted.
|
|
*/
|
|
|
|
bool textbox_delete(struct browser_window *bw, struct box *text_box,
|
|
unsigned char_offset, unsigned utf8_len)
|
|
{
|
|
unsigned next_offset = char_offset + utf8_len;
|
|
struct box *form = text_box->parent->parent;
|
|
|
|
if (bw->sel->defined) {
|
|
delete_selection(bw->sel);
|
|
return true;
|
|
}
|
|
|
|
/* delete from form gadget (text and password inputs only) */
|
|
if (form->gadget && (form->gadget->type == GADGET_TEXTBOX ||
|
|
form->gadget->type == GADGET_PASSWORD) &&
|
|
form->gadget->value) {
|
|
size_t form_offset = get_form_offset(form, text_box,
|
|
char_offset);
|
|
size_t next_offset = get_form_offset(form, text_box,
|
|
char_offset + utf8_len);
|
|
|
|
memmove(form->gadget->value + form_offset,
|
|
form->gadget->value + next_offset,
|
|
form->gadget->length - next_offset);
|
|
form->gadget->length -= utf8_len;
|
|
form->gadget->value[form->gadget->length] = 0;
|
|
}
|
|
|
|
/* delete from visible textbox */
|
|
if (next_offset <= text_box->length + text_box->space) {
|
|
/* handle removal of trailing space */
|
|
if (text_box->space && next_offset > text_box->length) {
|
|
if (char_offset > 0) {
|
|
/* is the trailing character still a space? */
|
|
int tmp = utf8_prev(text_box->text, char_offset);
|
|
if (isspace(text_box->text[tmp]))
|
|
char_offset = tmp;
|
|
else
|
|
text_box->space = false;
|
|
} else {
|
|
text_box->space = false;
|
|
}
|
|
|
|
text_box->length = char_offset;
|
|
} else {
|
|
memmove(text_box->text + char_offset,
|
|
text_box->text + next_offset,
|
|
text_box->length - next_offset);
|
|
text_box->length -= utf8_len;
|
|
}
|
|
|
|
/* nothing should assume that the text is terminated,
|
|
* but just in case */
|
|
text_box->text[text_box->length] = 0;
|
|
|
|
text_box->width = UNKNOWN_WIDTH;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Delete some text from a box, or delete the box in its entirety
|
|
*
|
|
* \param bw browser window
|
|
* \param b box
|
|
* \param offset start offset of text to be deleted (in bytes)
|
|
* \param length length of text to be deleted
|
|
* \return true iff successful
|
|
*/
|
|
|
|
bool delete_handler(struct browser_window *bw, struct box *b,
|
|
int offset, size_t length)
|
|
{
|
|
size_t text_length = b->length + b->space;
|
|
|
|
/* only remove if its not the first box */
|
|
if (offset <= 0 && length >= text_length && b->prev != NULL) {
|
|
/* remove the entire box */
|
|
box_unlink_and_free(b);
|
|
|
|
return true;
|
|
} else
|
|
return textbox_delete(bw, b, offset,
|
|
min(length, text_length - offset));
|
|
}
|
|
|
|
|
|
/**
|
|
* Remove the selected text from a text box and gadget (if applicable)
|
|
*
|
|
* \param s The selection to be removed
|
|
*/
|
|
|
|
void delete_selection(struct selection *s)
|
|
{
|
|
assert(s->defined);
|
|
size_t start_offset, end_offset;
|
|
struct box *text_box = selection_get_start(s, &start_offset);
|
|
struct box *end_box = selection_get_end(s, &end_offset);
|
|
struct box *next;
|
|
size_t sel_len = s->end_idx - s->start_idx;
|
|
int beginning = 0;
|
|
|
|
/* Clear selection so that deletion from textboxes proceeds */
|
|
selection_clear(s, true);
|
|
|
|
/* handle first box */
|
|
delete_handler(s->bw, text_box, start_offset, sel_len);
|
|
if (text_box == end_box)
|
|
return;
|
|
|
|
for (text_box = text_box->next; text_box != end_box; text_box = next) {
|
|
next = text_box->next;
|
|
box_unlink_and_free(text_box);
|
|
}
|
|
|
|
delete_handler(s->bw, end_box, beginning, end_offset);
|
|
}
|
|
|
|
/**
|
|
* Locate the first inline box at the start of this line
|
|
*
|
|
* \param text_box text box from which to start searching
|
|
*/
|
|
|
|
struct box *line_start(struct box *text_box)
|
|
{
|
|
while (text_box->prev && text_box->prev->type == BOX_TEXT)
|
|
text_box = text_box->prev;
|
|
return text_box;
|
|
}
|
|
|
|
|
|
/**
|
|
* Locate the last inline box in this line
|
|
*
|
|
* \param text_box text box from which to start searching
|
|
*/
|
|
|
|
struct box *line_end(struct box *text_box)
|
|
{
|
|
while (text_box->next && text_box->next->type == BOX_TEXT)
|
|
text_box = text_box->next;
|
|
return text_box;
|
|
}
|
|
|
|
|
|
/**
|
|
* Backtrack to the start of the previous line, if there is one.
|
|
*/
|
|
|
|
struct box *line_above(struct box *text_box)
|
|
{
|
|
struct box *prev;
|
|
|
|
text_box = line_start(text_box);
|
|
|
|
prev = text_box->prev;
|
|
while (prev && prev->type == BOX_BR)
|
|
prev = prev->prev;
|
|
|
|
return prev ? line_start(prev) : text_box;
|
|
}
|
|
|
|
|
|
/**
|
|
* Advance to the start of the next line, if there is one.
|
|
*/
|
|
|
|
struct box *line_below(struct box *text_box)
|
|
{
|
|
struct box *next;
|
|
|
|
text_box = line_end(text_box);
|
|
|
|
next = text_box->next;
|
|
while (next && next->type == BOX_BR)
|
|
next = next->next;
|
|
|
|
return next ? next : text_box;
|
|
}
|
|
|
|
|
|
/**
|
|
* Break a text box into two
|
|
*
|
|
* \param bw browser window
|
|
* \param text_box text box to be split
|
|
* \param char_offset offset (in bytes) at which text box is to be split
|
|
*/
|
|
|
|
struct box *textarea_insert_break(struct browser_window *bw,
|
|
struct box *text_box, size_t char_offset)
|
|
{
|
|
struct box *new_br, *new_text;
|
|
char *text = talloc_array(bw->current_content, char,
|
|
text_box->length + 1);
|
|
if (!text) {
|
|
warn_user("NoMemory", 0);
|
|
return NULL;
|
|
}
|
|
|
|
new_br = box_create(text_box->style, 0, 0, text_box->title, 0,
|
|
bw->current_content);
|
|
new_text = talloc(bw->current_content, struct box);
|
|
if (!new_text) {
|
|
warn_user("NoMemory", 0);
|
|
return NULL;
|
|
}
|
|
|
|
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);
|
|
|
|
return new_text;
|
|
}
|
|
|
|
|
|
/**
|
|
* Cut a range of text to the global clipboard.
|
|
*
|
|
* \param bw browser window
|
|
* \param start_box text box at start of range
|
|
* \param start_idx index (bytes) within start box
|
|
* \param end_box text box at end of range
|
|
* \param end_idx index (bytes) within end box
|
|
* \return true iff successful
|
|
*/
|
|
|
|
bool textarea_cut(struct browser_window *bw,
|
|
struct box *start_box, unsigned start_idx,
|
|
struct box *end_box, unsigned end_idx)
|
|
{
|
|
struct box *box = start_box;
|
|
bool success = true;
|
|
bool del = false; /* caller expects start_box to persist */
|
|
|
|
if (!gui_empty_clipboard())
|
|
return false;
|
|
|
|
while (box && box != end_box) {
|
|
/* read before deletion, in case the whole box goes */
|
|
struct box *next = box->next;
|
|
|
|
if (box->type == BOX_BR) {
|
|
if (!gui_add_to_clipboard("\n", 1, false)) {
|
|
gui_commit_clipboard();
|
|
return false;
|
|
}
|
|
box_unlink_and_free(box);
|
|
} else {
|
|
/* append box text to clipboard and then delete it */
|
|
if (!gui_add_to_clipboard(box->text + start_idx,
|
|
box->length - start_idx, box->space)) {
|
|
gui_commit_clipboard();
|
|
return false;
|
|
}
|
|
|
|
if (del) {
|
|
if (!delete_handler(bw, box, start_idx,
|
|
(box->length + box->space) - start_idx)) {
|
|
gui_commit_clipboard();
|
|
return false;
|
|
}
|
|
} else {
|
|
textbox_delete(bw, box, start_idx,
|
|
(box->length + box->space) - start_idx);
|
|
}
|
|
}
|
|
|
|
del = true;
|
|
start_idx = 0;
|
|
box = next;
|
|
}
|
|
|
|
/* and the last box */
|
|
if (box) {
|
|
if (gui_add_to_clipboard(box->text + start_idx,
|
|
end_idx - start_idx, end_idx > box->length)) {
|
|
if (del) {
|
|
if (!delete_handler(bw, box, start_idx,
|
|
end_idx - start_idx))
|
|
success = false;
|
|
} else {
|
|
textbox_delete(bw, box, start_idx,
|
|
end_idx - start_idx);
|
|
}
|
|
} else {
|
|
success = false;
|
|
}
|
|
}
|
|
|
|
return gui_commit_clipboard() ? success : false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Reflow textarea preserving width and height
|
|
*
|
|
* \param bw browser window
|
|
* \param textarea text area box
|
|
* \param inline_container container holding text box
|
|
*/
|
|
|
|
void textarea_reflow(struct browser_window *bw, struct box *textarea,
|
|
struct box *inline_container)
|
|
{
|
|
int width = textarea->width;
|
|
int 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);
|
|
}
|
|
|
|
|
|
/**
|
|
* Move to the start of the word containing the given character position,
|
|
* or the start of the preceding word if already at the start of this one.
|
|
*
|
|
* \param text UTF-8 text string
|
|
* \param poffset offset of caret within string (updated on exit)
|
|
* \param pchars receives the number of characters skipped
|
|
* \return true iff the start of a word was found before/at the string start
|
|
*/
|
|
|
|
bool word_left(const char *text, size_t *poffset, size_t *pchars)
|
|
{
|
|
size_t offset = *poffset;
|
|
bool success = false;
|
|
size_t nchars = 0;
|
|
|
|
/* Skip any spaces immediately prior to the offset */
|
|
while (offset > 0) {
|
|
offset = utf8_prev(text, offset);
|
|
nchars++;
|
|
if (!isspace(text[offset])) break;
|
|
}
|
|
|
|
/* Now skip all non-space characters */
|
|
while (offset > 0) {
|
|
size_t prev = utf8_prev(text, offset);
|
|
success = true;
|
|
if (isspace(text[prev]))
|
|
break;
|
|
offset = prev;
|
|
nchars++;
|
|
}
|
|
|
|
*poffset = offset;
|
|
if (pchars) *pchars = nchars;
|
|
|
|
return success;
|
|
}
|
|
|
|
|
|
/**
|
|
* Move to the start of the first word following the given character position.
|
|
*
|
|
* \param text UTF-8 text string
|
|
* \param len length of string in bytes
|
|
* \param poffset offset of caret within string (updated on exit)
|
|
* \param pchars receives the number of characters skipped
|
|
* \return true iff the start of a word was found before the string end
|
|
*/
|
|
|
|
bool word_right(const char *text, size_t len, size_t *poffset, size_t *pchars)
|
|
{
|
|
size_t offset = *poffset;
|
|
bool success = false;
|
|
size_t nchars = 0;
|
|
|
|
/* Skip all non-space characters after the offset */
|
|
while (offset < len) {
|
|
if (isspace(text[offset])) break;
|
|
offset = utf8_next(text, len, offset);
|
|
nchars++;
|
|
}
|
|
|
|
/* Now skip all space characters */
|
|
while (offset < len) {
|
|
offset = utf8_next(text, len, offset);
|
|
nchars++;
|
|
if (offset < len && !isspace(text[offset])) {
|
|
success = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
*poffset = offset;
|
|
if (pchars) *pchars = nchars;
|
|
|
|
return success;
|
|
}
|
|
|
|
/**
|
|
* Adjust scroll offsets so that the caret is visible
|
|
* \param textarea textarea box
|
|
* \return true if a change in scroll offsets has occured
|
|
*/
|
|
|
|
bool ensure_caret_visible(struct box *textarea)
|
|
{
|
|
int cx, cy;
|
|
int scrollx, scrolly;
|
|
|
|
assert(textarea->gadget);
|
|
|
|
scrollx = textarea->scroll_x;
|
|
scrolly = textarea->scroll_y;
|
|
|
|
/* Calculate the caret coordinates */
|
|
cx = textarea->gadget->caret_pixel_offset +
|
|
textarea->gadget->caret_text_box->x;
|
|
cy = textarea->gadget->caret_text_box->y;
|
|
|
|
/* Ensure they are visible */
|
|
if (!box_hscrollbar_present(textarea)) {
|
|
scrollx = 0;
|
|
} else if (cx-textarea->scroll_x < 0) {
|
|
scrollx = cx;
|
|
} else if (cx > textarea->scroll_x + textarea->width) {
|
|
scrollx = cx - textarea->width;
|
|
}
|
|
|
|
if (!box_vscrollbar_present(textarea)) {
|
|
scrolly = 0;
|
|
} else if (cy - textarea->scroll_y < 0) {
|
|
scrolly = cy;
|
|
} else if (cy + textarea->gadget->caret_text_box->height >
|
|
textarea->scroll_y + textarea->height) {
|
|
scrolly = (cy + textarea->gadget->caret_text_box->height) -
|
|
textarea->height;
|
|
}
|
|
|
|
if ((scrollx == textarea->scroll_x) && (scrolly == textarea->scroll_y))
|
|
return false;
|
|
|
|
textarea->scroll_x = scrollx;
|
|
textarea->scroll_y = scrolly;
|
|
|
|
return true;
|
|
}
|
|
|