/* * Copyright 2006 James Bursa * Copyright 2006 Richard Wilson * Copyright 2008 Michael Drake * Copyright 2009 Paul Blokus * * 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 . */ /** * \file * implementation of user interaction with a CONTENT_HTML. */ #include #include #include #include "utils/corestrings.h" #include "utils/messages.h" #include "utils/utils.h" #include "utils/log.h" #include "utils/nsoption.h" #include "netsurf/content.h" #include "netsurf/browser_window.h" #include "netsurf/mouse.h" #include "netsurf/misc.h" #include "netsurf/layout.h" #include "netsurf/keypress.h" #include "content/hlcache.h" #include "desktop/frames.h" #include "desktop/scrollbar.h" #include "desktop/selection.h" #include "desktop/textarea.h" #include "javascript/js.h" #include "desktop/gui_internal.h" #include "html/box.h" #include "html/box_textarea.h" #include "html/box_inspect.h" #include "html/font.h" #include "html/form_internal.h" #include "html/private.h" #include "html/imagemap.h" #include "html/search.h" #include "html/interaction.h" /** * Get pointer shape for given box * * \param box box in question * \param imagemap whether an imagemap applies to the box */ static browser_pointer_shape get_pointer_shape(struct box *box, bool imagemap) { browser_pointer_shape pointer; css_computed_style *style; enum css_cursor_e cursor; lwc_string **cursor_uris; if (box->type == BOX_FLOAT_LEFT || box->type == BOX_FLOAT_RIGHT) style = box->children->style; else style = box->style; if (style == NULL) return BROWSER_POINTER_DEFAULT; cursor = css_computed_cursor(style, &cursor_uris); switch (cursor) { case CSS_CURSOR_AUTO: if (box->href || (box->gadget && (box->gadget->type == GADGET_IMAGE || box->gadget->type == GADGET_SUBMIT)) || imagemap) { /* link */ pointer = BROWSER_POINTER_POINT; } else if (box->gadget && (box->gadget->type == GADGET_TEXTBOX || box->gadget->type == GADGET_PASSWORD || box->gadget->type == GADGET_TEXTAREA)) { /* text input */ pointer = BROWSER_POINTER_CARET; } else { /* html content doesn't mind */ pointer = BROWSER_POINTER_AUTO; } break; case CSS_CURSOR_CROSSHAIR: pointer = BROWSER_POINTER_CROSS; break; case CSS_CURSOR_POINTER: pointer = BROWSER_POINTER_POINT; break; case CSS_CURSOR_MOVE: pointer = BROWSER_POINTER_MOVE; break; case CSS_CURSOR_E_RESIZE: pointer = BROWSER_POINTER_RIGHT; break; case CSS_CURSOR_W_RESIZE: pointer = BROWSER_POINTER_LEFT; break; case CSS_CURSOR_N_RESIZE: pointer = BROWSER_POINTER_UP; break; case CSS_CURSOR_S_RESIZE: pointer = BROWSER_POINTER_DOWN; break; case CSS_CURSOR_NE_RESIZE: pointer = BROWSER_POINTER_RU; break; case CSS_CURSOR_SW_RESIZE: pointer = BROWSER_POINTER_LD; break; case CSS_CURSOR_SE_RESIZE: pointer = BROWSER_POINTER_RD; break; case CSS_CURSOR_NW_RESIZE: pointer = BROWSER_POINTER_LU; break; case CSS_CURSOR_TEXT: pointer = BROWSER_POINTER_CARET; break; case CSS_CURSOR_WAIT: pointer = BROWSER_POINTER_WAIT; break; case CSS_CURSOR_PROGRESS: pointer = BROWSER_POINTER_PROGRESS; break; case CSS_CURSOR_HELP: pointer = BROWSER_POINTER_HELP; break; default: pointer = BROWSER_POINTER_DEFAULT; break; } return pointer; } /** * Start drag scrolling the contents of a box * * \param box the box to be scrolled * \param x x ordinate of initial mouse position * \param y y ordinate */ static void html_box_drag_start(struct box *box, int x, int y) { int box_x, box_y; int scroll_mouse_x, scroll_mouse_y; box_coords(box, &box_x, &box_y); if (box->scroll_x != NULL) { scroll_mouse_x = x - box_x ; scroll_mouse_y = y - (box_y + box->padding[TOP] + box->height + box->padding[BOTTOM] - SCROLLBAR_WIDTH); scrollbar_start_content_drag(box->scroll_x, scroll_mouse_x, scroll_mouse_y); } else if (box->scroll_y != NULL) { scroll_mouse_x = x - (box_x + box->padding[LEFT] + box->width + box->padding[RIGHT] - SCROLLBAR_WIDTH); scroll_mouse_y = y - box_y; scrollbar_start_content_drag(box->scroll_y, scroll_mouse_x, scroll_mouse_y); } } /** * End overflow scroll scrollbar drags * * \param html html content * \param mouse state of mouse buttons and modifier keys * \param x coordinate of mouse * \param y coordinate of mouse * \param dir Direction of drag */ static size_t html_selection_drag_end(struct html_content *html, browser_mouse_state mouse, int x, int y, int dir) { int pixel_offset; struct box *box; int dx, dy; size_t idx = 0; box = box_pick_text_box(html, x, y, dir, &dx, &dy); if (box) { plot_font_style_t fstyle; font_plot_style_from_css(&html->len_ctx, box->style, &fstyle); guit->layout->position(&fstyle, box->text, box->length, dx, &idx, &pixel_offset); idx += box->byte_offset; } return idx; } /** * Helper for file gadgets to store their filename. * * Stores the filename unencoded on the dom node associated with the * gadget. * * \todo Get rid of this crap eventually * * \param operation DOM operation * \param key DOM node key being considerd * \param _data The data assocated with the key * \param src The source DOM node. * \param dst The destination DOM node. */ static void html__image_coords_dom_user_data_handler(dom_node_operation operation, dom_string *key, void *_data, struct dom_node *src, struct dom_node *dst) { struct image_input_coords *oldcoords, *coords = _data, *newcoords; if (!dom_string_isequal(corestring_dom___ns_key_image_coords_node_data, key) || coords == NULL) { return; } switch (operation) { case DOM_NODE_CLONED: newcoords = calloc(1, sizeof(*newcoords)); if (newcoords != NULL) { *newcoords = *coords; if (dom_node_set_user_data(dst, corestring_dom___ns_key_image_coords_node_data, newcoords, html__image_coords_dom_user_data_handler, &oldcoords) == DOM_NO_ERR) { free(oldcoords); } } break; case DOM_NODE_DELETED: free(coords); break; case DOM_NODE_RENAMED: case DOM_NODE_IMPORTED: case DOM_NODE_ADOPTED: break; default: NSLOG(netsurf, INFO, "User data operation not handled."); assert(0); } } /** * End overflow scroll scrollbar drags * * \param scrollbar scrollbar widget * \param mouse state of mouse buttons and modifier keys * \param x coordinate of mouse * \param y coordinate of mouse */ static void html_overflow_scroll_drag_end(struct scrollbar *scrollbar, browser_mouse_state mouse, int x, int y) { int scroll_mouse_x, scroll_mouse_y, box_x, box_y; struct html_scrollbar_data *data = scrollbar_get_data(scrollbar); struct box *box; box = data->box; box_coords(box, &box_x, &box_y); if (scrollbar_is_horizontal(scrollbar)) { scroll_mouse_x = x - box_x; scroll_mouse_y = y - (box_y + box->padding[TOP] + box->height + box->padding[BOTTOM] - SCROLLBAR_WIDTH); scrollbar_mouse_drag_end(scrollbar, mouse, scroll_mouse_x, scroll_mouse_y); } else { scroll_mouse_x = x - (box_x + box->padding[LEFT] + box->width + box->padding[RIGHT] - SCROLLBAR_WIDTH); scroll_mouse_y = y - box_y; scrollbar_mouse_drag_end(scrollbar, mouse, scroll_mouse_x, scroll_mouse_y); } } /** * handle html mouse action when select menu is open * */ static nserror mouse_action_select_menu(html_content *html, struct browser_window *bw, browser_mouse_state mouse, int x, int y) { struct box *box; int box_x = 0; int box_y = 0; const char *status; int width, height; struct hlcache_handle *bw_content; browser_drag_type bw_drag_type; assert(html->visible_select_menu != NULL); bw_drag_type = browser_window_get_drag_type(bw); if (bw_drag_type != DRAGGING_NONE && !mouse) { /* drag end: select menu */ form_select_mouse_drag_end(html->visible_select_menu, mouse, x, y); } box = html->visible_select_menu->box; box_coords(box, &box_x, &box_y); box_x -= box->border[LEFT].width; box_y += box->height + box->border[BOTTOM].width + box->padding[BOTTOM] + box->padding[TOP]; status = form_select_mouse_action(html->visible_select_menu, mouse, x - box_x, y - box_y); if (status != NULL) { /* set status if menu still open */ union content_msg_data msg_data; msg_data.explicit_status_text = status; content_broadcast((struct content *)html, CONTENT_MSG_STATUS, &msg_data); return NSERROR_OK; } /* close menu and redraw where it was */ form_select_get_dimensions(html->visible_select_menu, &width, &height); html->visible_select_menu = NULL; bw_content = browser_window_get_content(bw); content_request_redraw(bw_content, box_x, box_y, width, height); return NSERROR_OK; } /** * handle html mouse action when a selection drag is being performed * */ static nserror mouse_action_drag_selection(html_content *html, struct browser_window *bw, browser_mouse_state mouse, int x, int y) { struct box *box; int dir = -1; int dx, dy; size_t idx; union html_drag_owner drag_owner; int pixel_offset; plot_font_style_t fstyle; if (!mouse) { /* End of selection drag */ if (selection_dragging_start(&html->sel)) { dir = 1; } idx = html_selection_drag_end(html, mouse, x, y, dir); if (idx != 0) { selection_track(&html->sel, mouse, idx); } drag_owner.no_owner = true; html_set_drag_type(html, HTML_DRAG_NONE, drag_owner, NULL); return NSERROR_OK; } if (selection_dragging_start(&html->sel)) { dir = 1; } box = box_pick_text_box(html, x, y, dir, &dx, &dy); if (box != NULL) { font_plot_style_from_css(&html->len_ctx, box->style, &fstyle); guit->layout->position(&fstyle, box->text, box->length, dx, &idx, &pixel_offset); selection_track(&html->sel, mouse, box->byte_offset + idx); } return NSERROR_OK; } /** * handle html mouse action when a scrollbar drag is being performed * */ static nserror mouse_action_drag_scrollbar(html_content *html, struct browser_window *bw, browser_mouse_state mouse, int x, int y) { struct scrollbar *scr; struct html_scrollbar_data *data; struct box *box; int box_x = 0; int box_y = 0; const char *status; int scroll_mouse_x = 0, scroll_mouse_y = 0; scrollbar_mouse_status scrollbar_status; scr = html->drag_owner.scrollbar; if (!mouse) { /* drag end: scrollbar */ html_overflow_scroll_drag_end(scr, mouse, x, y); } data = scrollbar_get_data(scr); box = data->box; box_coords(box, &box_x, &box_y); if (scrollbar_is_horizontal(scr)) { scroll_mouse_x = x - box_x ; scroll_mouse_y = y - (box_y + box->padding[TOP] + box->height + box->padding[BOTTOM] - SCROLLBAR_WIDTH); scrollbar_status = scrollbar_mouse_action(scr, mouse, scroll_mouse_x, scroll_mouse_y); } else { scroll_mouse_x = x - (box_x + box->padding[LEFT] + box->width + box->padding[RIGHT] - SCROLLBAR_WIDTH); scroll_mouse_y = y - box_y; scrollbar_status = scrollbar_mouse_action(scr, mouse, scroll_mouse_x, scroll_mouse_y); } status = scrollbar_mouse_status_to_message(scrollbar_status); if (status != NULL) { union content_msg_data msg_data; msg_data.explicit_status_text = status; content_broadcast((struct content *)html, CONTENT_MSG_STATUS, &msg_data); } return NSERROR_OK; } /** * handle mouse actions while dragging in a text area */ static nserror mouse_action_drag_textarea(html_content *html, struct browser_window *bw, browser_mouse_state mouse, int x, int y) { struct box *box; int box_x = 0; int box_y = 0; box = html->drag_owner.textarea; assert(box->gadget != NULL); assert(box->gadget->type == GADGET_TEXTAREA || box->gadget->type == GADGET_PASSWORD || box->gadget->type == GADGET_TEXTBOX); box_coords(box, &box_x, &box_y); textarea_mouse_action(box->gadget->data.text.ta, mouse, x - box_x, y - box_y); /* TODO: Set appropriate statusbar message */ return NSERROR_OK; } /** * handle mouse actions while dragging in a content */ static nserror mouse_action_drag_content(html_content *html, struct browser_window *bw, browser_mouse_state mouse, int x, int y) { struct box *box; int box_x = 0; int box_y = 0; box = html->drag_owner.content; assert(box->object != NULL); box_coords(box, &box_x, &box_y); content_mouse_track(box->object, bw, mouse, x - box_x, y - box_y); return NSERROR_OK; } /** * local structure containing all the mouse action state information */ struct mouse_action_state { struct { const char *status; /**< status text */ browser_pointer_shape pointer; /**< pointer shape */ enum { ACTION_NONE, ACTION_NOSEND, /**< do not send status and pointer message */ ACTION_SUBMIT, ACTION_GO, ACTION_JS, } action; } result; /** dom node */ struct dom_node *node; /** html object */ struct { struct box *box; int pos_x; int pos_y; } html_object; /** non html object */ hlcache_handle *object; /** iframe */ struct browser_window *iframe; /** link either from href or imagemap */ struct { struct box *box; nsurl *url; const char *target; bool is_imagemap; } link; /** gadget */ struct { struct form_control *control; struct box *box; int box_x; int box_y; const char *target; } gadget; /** title */ const char *title; /** candidate box for drag operation */ struct box *drag_candidate; /** scrollbar */ struct { struct scrollbar *bar; int mouse_x; int mouse_y; } scroll; /** text in box */ struct { struct box *box; int box_x; } text; }; /** * iterate the box tree for deepest node at coordinates * * extracts mouse action node information by descending through * visible boxes setting more specific values for: * * box - deepest box at point * html_object_box - html object * html_object_pos_x - html object * html_object_pos_y - html object * object - non html object * iframe - iframe * url - href or imagemap * target - href or imagemap or gadget * url_box - href or imagemap * imagemap - imagemap * gadget - gadget * gadget_box - gadget * gadget_box_x - gadget * gadget_box_y - gadget * title - title * pointer * * drag_candidate - first box with scroll * padding_left - box with scroll * padding_right * padding_top * padding_bottom * scrollbar - inside padding box stops decent * scroll_mouse_x - inside padding box stops decent * scroll_mouse_y - inside padding box stops decent * * text_box - text box * text_box_x - text_box */ static nserror get_mouse_action_node(html_content *html, int x, int y, struct mouse_action_state *man) { struct box *box; int box_x = 0; int box_y = 0; /* initialise the mouse action state data */ memset(man, 0, sizeof(struct mouse_action_state)); man->node = html->layout->node; /* Default dom node to the */ man->result.pointer = BROWSER_POINTER_DEFAULT; /* search the box tree for a link, imagemap, form control, or * box with scrollbars */ box = html->layout; /* Consider the margins of the html page now */ box_x = box->margin[LEFT]; box_y = box->margin[TOP]; do { /* skip hidden boxes */ if ((box->style != NULL) && (css_computed_visibility(box->style) == CSS_VISIBILITY_HIDDEN)) { goto next_box; } if (box->node != NULL) { man->node = box->node; } if (box->object) { if (content_get_type(box->object) == CONTENT_HTML) { man->html_object.box = box; man->html_object.pos_x = box_x; man->html_object.pos_y = box_y; } else { man->object = box->object; } } if (box->iframe) { man->iframe = box->iframe; } if (box->href) { man->link.url = box->href; man->link.target = box->target; man->link.box = box; man->link.is_imagemap = false; } if (box->usemap) { man->link.url = imagemap_get(html, box->usemap, box_x, box_y, x, y, &man->link.target); man->link.box = box; man->link.is_imagemap = true; } if (box->gadget) { man->gadget.control = box->gadget; man->gadget.box = box; man->gadget.box_x = box_x; man->gadget.box_y = box_y; if (box->gadget->form) { man->gadget.target = box->gadget->form->target; } } if (box->title) { man->title = box->title; } man->result.pointer = get_pointer_shape(box, false); if ((box->scroll_x != NULL) || (box->scroll_y != NULL)) { int padding_left; int padding_right; int padding_top; int padding_bottom; if (man->drag_candidate == NULL) { man->drag_candidate = box; } padding_left = box_x + scrollbar_get_offset(box->scroll_x); padding_right = padding_left + box->padding[LEFT] + box->width + box->padding[RIGHT]; padding_top = box_y + scrollbar_get_offset(box->scroll_y); padding_bottom = padding_top + box->padding[TOP] + box->height + box->padding[BOTTOM]; if ((x > padding_left) && (x < padding_right) && (y > padding_top) && (y < padding_bottom)) { /* mouse inside padding box */ if ((box->scroll_y != NULL) && (x > (padding_right - SCROLLBAR_WIDTH))) { /* mouse above vertical box scroll */ man->scroll.bar = box->scroll_y; man->scroll.mouse_x = x - (padding_right - SCROLLBAR_WIDTH); man->scroll.mouse_y = y - padding_top; break; } else if ((box->scroll_x != NULL) && (y > (padding_bottom - SCROLLBAR_WIDTH))) { /* mouse above horizontal box scroll */ man->scroll.bar = box->scroll_x; man->scroll.mouse_x = x - padding_left; man->scroll.mouse_y = y - (padding_bottom - SCROLLBAR_WIDTH); break; } } } if (box->text && !box->object) { man->text.box = box; man->text.box_x = box_x; } next_box: /* iterate to next box */ box = box_at_point(&html->len_ctx, box, x, y, &box_x, &box_y); } while (box != NULL); /* use of box_x, box_y, or content below this point is probably a * mistake; they will refer to the last box returned by box_at_point */ assert(man->node != NULL); return NSERROR_OK; } /** * process mouse activity on a form gadget */ static nserror gadget_mouse_action(html_content *html, browser_mouse_state mouse, int x, int y, struct mouse_action_state *mas) { struct content *c = (struct content *)html; textarea_mouse_status ta_status; union content_msg_data msg_data; nserror res; bool click; click = mouse & (BROWSER_MOUSE_PRESS_1 | BROWSER_MOUSE_PRESS_2 | BROWSER_MOUSE_CLICK_1 | BROWSER_MOUSE_CLICK_2 | BROWSER_MOUSE_DRAG_1 | BROWSER_MOUSE_DRAG_2); switch (mas->gadget.control->type) { case GADGET_SELECT: mas->result.status = messages_get("FormSelect"); mas->result.pointer = BROWSER_POINTER_MENU; if (mouse & BROWSER_MOUSE_CLICK_1 && nsoption_bool(core_select_menu)) { html->visible_select_menu = mas->gadget.control; res = form_open_select_menu(c, mas->gadget.control, form_select_menu_callback, c); if (res != NSERROR_OK) { NSLOG(netsurf, ERROR, "%s", messages_get_errorcode(res)); html->visible_select_menu = NULL; } mas->result.pointer = BROWSER_POINTER_DEFAULT; } else if (mouse & BROWSER_MOUSE_CLICK_1) { msg_data.select_menu.gadget = mas->gadget.control; content_broadcast(c, CONTENT_MSG_SELECTMENU, &msg_data); } break; case GADGET_CHECKBOX: mas->result.status = messages_get("FormCheckbox"); if (mouse & BROWSER_MOUSE_CLICK_1) { mas->gadget.control->selected = !mas->gadget.control->selected; dom_html_input_element_set_checked( (dom_html_input_element *)(mas->gadget.control->node), mas->gadget.control->selected); html__redraw_a_box(html, mas->gadget.box); } break; case GADGET_RADIO: mas->result.status = messages_get("FormRadio"); if (mouse & BROWSER_MOUSE_CLICK_1) { form_radio_set(mas->gadget.control); } break; case GADGET_IMAGE: /* This falls through to SUBMIT */ if (mouse & BROWSER_MOUSE_CLICK_1) { struct image_input_coords *coords, *oldcoords; /** \todo Find a way to not ignore errors */ coords = calloc(1, sizeof(*coords)); if (coords == NULL) { return NSERROR_OK; } coords->x = x - mas->gadget.box_x; coords->y = y - mas->gadget.box_y; if (dom_node_set_user_data( mas->gadget.control->node, corestring_dom___ns_key_image_coords_node_data, coords, html__image_coords_dom_user_data_handler, &oldcoords) != DOM_NO_ERR) { return NSERROR_OK; } free(oldcoords); } /* Fall through */ case GADGET_SUBMIT: if (mas->gadget.control->form) { static char status_buffer[200]; snprintf(status_buffer, sizeof status_buffer, messages_get("FormSubmit"), mas->gadget.control->form->action); mas->result.status = status_buffer; mas->result.pointer = get_pointer_shape(mas->gadget.box, false); if (mouse & (BROWSER_MOUSE_CLICK_1 | BROWSER_MOUSE_CLICK_2)) { mas->result.action = ACTION_SUBMIT; } } else { mas->result.status = messages_get("FormBadSubmit"); } break; case GADGET_TEXTBOX: case GADGET_PASSWORD: case GADGET_TEXTAREA: if (mas->gadget.control->type == GADGET_TEXTAREA) { mas->result.status = messages_get("FormTextarea"); } else { mas->result.status = messages_get("FormTextbox"); } if (click && (html->selection_type != HTML_SELECTION_TEXTAREA || html->selection_owner.textarea != mas->gadget.box)) { union html_selection_owner sel_owner; sel_owner.none = true; html_set_selection(html, HTML_SELECTION_NONE, sel_owner, true); } ta_status = textarea_mouse_action(mas->gadget.control->data.text.ta, mouse, x - mas->gadget.box_x, y - mas->gadget.box_y); if (ta_status & TEXTAREA_MOUSE_EDITOR) { mas->result.pointer = get_pointer_shape(mas->gadget.box, false); } else { mas->result.pointer = BROWSER_POINTER_DEFAULT; mas->result.status = scrollbar_mouse_status_to_message(ta_status >> 3); } break; case GADGET_HIDDEN: /* not possible: no box generated */ break; case GADGET_RESET: mas->result.status = messages_get("FormReset"); break; case GADGET_FILE: mas->result.status = messages_get("FormFile"); if (mouse & BROWSER_MOUSE_CLICK_1) { msg_data.gadget_click.gadget = mas->gadget.control; content_broadcast(c, CONTENT_MSG_GADGETCLICK, &msg_data); } break; case GADGET_BUTTON: /* This gadget cannot be activated */ mas->result.status = messages_get("FormButton"); break; } return NSERROR_OK; } /** * process mouse activity on an iframe */ static nserror iframe_mouse_action(struct browser_window *bw, browser_mouse_state mouse, int x, int y, struct mouse_action_state *mas) { int pos_x, pos_y; float scale; scale = browser_window_get_scale(bw); browser_window_get_position(mas->iframe, false, &pos_x, &pos_y); if (mouse & BROWSER_MOUSE_CLICK_1 || mouse & BROWSER_MOUSE_CLICK_2) { browser_window_mouse_click(mas->iframe, mouse, (x * scale) - pos_x, (y * scale) - pos_y); } else { browser_window_mouse_track(mas->iframe, mouse, (x * scale) - pos_x, (y * scale) - pos_y); } mas->result.action = ACTION_NOSEND; return NSERROR_OK; } /** * process mouse activity on an html object */ static nserror html_object_mouse_action(html_content *html, struct browser_window *bw, browser_mouse_state mouse, int x, int y, struct mouse_action_state *mas) { bool click; click = mouse & (BROWSER_MOUSE_PRESS_1 | BROWSER_MOUSE_PRESS_2 | BROWSER_MOUSE_CLICK_1 | BROWSER_MOUSE_CLICK_2 | BROWSER_MOUSE_DRAG_1 | BROWSER_MOUSE_DRAG_2); if (click && (html->selection_type != HTML_SELECTION_CONTENT || html->selection_owner.content != mas->html_object.box)) { union html_selection_owner sel_owner; sel_owner.none = true; html_set_selection(html, HTML_SELECTION_NONE, sel_owner, true); } if (mouse & BROWSER_MOUSE_CLICK_1 || mouse & BROWSER_MOUSE_CLICK_2) { content_mouse_action(mas->html_object.box->object, bw, mouse, x - mas->html_object.pos_x, y - mas->html_object.pos_y); } else { content_mouse_track(mas->html_object.box->object, bw, mouse, x - mas->html_object.pos_x, y - mas->html_object.pos_y); } mas->result.action = ACTION_NOSEND; return NSERROR_OK; } /** * determine if a url has a javascript scheme * * \param urm The url to check. * \return true if the url is a javascript scheme else false */ static bool is_javascript_navigate_url(nsurl *url) { bool is_js = false; lwc_string *scheme; scheme = nsurl_get_component(url, NSURL_SCHEME); if (scheme != NULL) { if (scheme == corestring_lwc_javascript) { is_js = true; } lwc_string_unref(scheme); } return is_js; } /** * process mouse activity on a link */ static nserror link_mouse_action(html_content *html, struct browser_window *bw, browser_mouse_state mouse, int x, int y, struct mouse_action_state *mas) { nserror res; char *url_s = NULL; size_t url_l = 0; static char status_buffer[200]; union content_msg_data msg_data; if (nsoption_bool(display_decoded_idn) == true) { res = nsurl_get_utf8(mas->link.url, &url_s, &url_l); if (res != NSERROR_OK) { /* Unable to obtain a decoded IDN. This is not * a fatal error. Ensure the string pointer * is NULL so we use the encoded version. */ url_s = NULL; } } if (mas->title) { snprintf(status_buffer, sizeof status_buffer, "%s: %s", url_s ? url_s : nsurl_access(mas->link.url), mas->title); } else { snprintf(status_buffer, sizeof status_buffer, "%s", url_s ? url_s : nsurl_access(mas->link.url)); } if (url_s != NULL) { free(url_s); } mas->result.status = status_buffer; mas->result.pointer = get_pointer_shape(mas->link.box, mas->link.is_imagemap); if (mouse & BROWSER_MOUSE_CLICK_1 && mouse & BROWSER_MOUSE_MOD_1) { /* force download of link */ browser_window_navigate(bw, mas->link.url, content_get_url((struct content *)html), BW_NAVIGATE_DOWNLOAD, NULL, NULL, NULL); } else if (mouse & BROWSER_MOUSE_CLICK_2 && mouse & BROWSER_MOUSE_MOD_1) { msg_data.savelink.url = mas->link.url; msg_data.savelink.title = mas->title; content_broadcast((struct content *)html, CONTENT_MSG_SAVELINK, &msg_data); } else if (mouse & (BROWSER_MOUSE_CLICK_1 | BROWSER_MOUSE_CLICK_2)) { if (is_javascript_navigate_url(mas->link.url)) { mas->result.action = ACTION_JS; } else { mas->result.action = ACTION_GO; } } return NSERROR_OK; } /** * process mouse activity if it is not anything else */ static nserror default_mouse_action(html_content *html, struct browser_window *bw, browser_mouse_state mouse, int x, int y, struct mouse_action_state *mas) { struct content *c = (struct content *)html; bool done = false; /* frame resizing */ if (browser_window_frame_resize_start(bw, mouse, x, y, &mas->result.pointer)) { if (mouse & (BROWSER_MOUSE_DRAG_1 | BROWSER_MOUSE_DRAG_2)) { mas->result.status = messages_get("FrameDrag"); } done = true; } /* if clicking in the main page, remove the selection from any * text areas */ if (!done) { union html_selection_owner sel_owner; bool click; click = mouse & (BROWSER_MOUSE_PRESS_1 | BROWSER_MOUSE_PRESS_2 | BROWSER_MOUSE_CLICK_1 | BROWSER_MOUSE_CLICK_2 | BROWSER_MOUSE_DRAG_1 | BROWSER_MOUSE_DRAG_2); if (click && html->focus_type != HTML_FOCUS_SELF) { union html_focus_owner fo; fo.self = true; html_set_focus(html, HTML_FOCUS_SELF, fo, true, 0, 0, 0, NULL); } if (click && html->selection_type != HTML_SELECTION_SELF) { sel_owner.none = true; html_set_selection(html, HTML_SELECTION_NONE, sel_owner, true); } if (mas->text.box) { int pixel_offset; size_t idx; plot_font_style_t fstyle; font_plot_style_from_css(&html->len_ctx, mas->text.box->style, &fstyle); guit->layout->position(&fstyle, mas->text.box->text, mas->text.box->length, x - mas->text.box_x, &idx, &pixel_offset); if (selection_click(&html->sel, mouse, mas->text.box->byte_offset + idx)) { /* key presses must be directed at the * main browser window, paste text * operations ignored */ html_drag_type drag_type; union html_drag_owner drag_owner; if (selection_dragging(&html->sel)) { drag_type = HTML_DRAG_SELECTION; drag_owner.no_owner = true; html_set_drag_type(html, drag_type, drag_owner, NULL); mas->result.status = messages_get("Selecting"); } done = true; } } else if (mouse & BROWSER_MOUSE_PRESS_1) { sel_owner.none = true; selection_clear(&html->sel, true); } if (selection_defined(&html->sel)) { sel_owner.none = false; html_set_selection(html, HTML_SELECTION_SELF, sel_owner, true); } else if (click && html->selection_type != HTML_SELECTION_NONE) { sel_owner.none = true; html_set_selection(html, HTML_SELECTION_NONE, sel_owner, true); } } if (!done) { union content_msg_data msg_data; if (mas->title) { mas->result.status = mas->title; } if (mouse & BROWSER_MOUSE_DRAG_1) { if (mouse & BROWSER_MOUSE_MOD_2) { msg_data.dragsave.type = CONTENT_SAVE_COMPLETE; msg_data.dragsave.content = NULL; content_broadcast(c, CONTENT_MSG_DRAGSAVE, &msg_data); } else { if (mas->drag_candidate == NULL) { browser_window_page_drag_start(bw, x, y); } else { html_box_drag_start(mas->drag_candidate, x, y); } mas->result.pointer = BROWSER_POINTER_MOVE; } } else if (mouse & BROWSER_MOUSE_DRAG_2) { if (mouse & BROWSER_MOUSE_MOD_2) { msg_data.dragsave.type = CONTENT_SAVE_SOURCE; msg_data.dragsave.content = NULL; content_broadcast(c, CONTENT_MSG_DRAGSAVE, &msg_data); } else { if (mas->drag_candidate == NULL) { browser_window_page_drag_start(bw, x, y); } else { html_box_drag_start(mas->drag_candidate, x, y); } mas->result.pointer = BROWSER_POINTER_MOVE; } } } if (mouse && mouse < BROWSER_MOUSE_MOD_1) { /* ensure key presses still act on the browser window */ union html_focus_owner fo; fo.self = true; html_set_focus(html, HTML_FOCUS_SELF, fo, true, 0, 0, 0, NULL); } return NSERROR_OK; } /** * handle non dragging mouse actions */ static nserror mouse_action_drag_none(html_content *html, struct browser_window *bw, browser_mouse_state mouse, int x, int y) { nserror res; struct content *c = (struct content *)html; union content_msg_data msg_data; lwc_string *path; /** * computed state * * not on heap to avoid allocation or stack because it is large */ static struct mouse_action_state mas; res = get_mouse_action_node(html, x, y, &mas); if (res != NSERROR_OK) { return res; } if (mas.scroll.bar) { mas.result.status = scrollbar_mouse_status_to_message( scrollbar_mouse_action(mas.scroll.bar, mouse, mas.scroll.mouse_x, mas.scroll.mouse_y)); mas.result.pointer = BROWSER_POINTER_DEFAULT; } else if (mas.gadget.control) { res = gadget_mouse_action(html, mouse, x, y, &mas); } else if ((mas.object != NULL) && (mouse & BROWSER_MOUSE_MOD_2)) { if (mouse & BROWSER_MOUSE_DRAG_2) { msg_data.dragsave.type = CONTENT_SAVE_NATIVE; msg_data.dragsave.content = mas.object; content_broadcast(c, CONTENT_MSG_DRAGSAVE, &msg_data); } else if (mouse & BROWSER_MOUSE_DRAG_1) { msg_data.dragsave.type = CONTENT_SAVE_ORIG; msg_data.dragsave.content = mas.object; content_broadcast(c, CONTENT_MSG_DRAGSAVE, &msg_data); } /* \todo should have a drag-saving object msg */ } else if (mas.iframe != NULL) { res = iframe_mouse_action(bw, mouse, x, y, &mas); } else if (mas.html_object.box != NULL) { res = html_object_mouse_action(html, bw, mouse, x, y, &mas); } else if (mas.link.url != NULL) { res = link_mouse_action(html, bw, mouse, x, y, &mas); } else { res = default_mouse_action(html, bw, mouse, x, y, &mas); } if (res != NSERROR_OK) { return res; } /* send status and pointer message */ if (mas.result.action != ACTION_NOSEND) { msg_data.explicit_status_text = mas.result.status; content_broadcast(c, CONTENT_MSG_STATUS, &msg_data); msg_data.pointer = mas.result.pointer; content_broadcast(c, CONTENT_MSG_POINTER, &msg_data); } /* fire dom click event */ if (mouse & BROWSER_MOUSE_CLICK_1) { fire_generic_dom_event(corestring_dom_click, mas.node, true, true); } /* deferred actions that can cause this browser_window to be destroyed * and must therefore be done after set_status/pointer */ switch (mas.result.action) { case ACTION_SUBMIT: res = form_submit(content_get_url(c), browser_window_find_target(bw, mas.gadget.target, mouse), mas.gadget.control->form, mas.gadget.control); break; case ACTION_GO: res = browser_window_navigate( browser_window_find_target(bw, mas.link.target, mouse), mas.link.url, content_get_url(c), BW_NAVIGATE_HISTORY, NULL, NULL, NULL); break; case ACTION_JS: path = nsurl_get_component(mas.link.url, NSURL_PATH); if (path != NULL) { html_exec(c, lwc_string_data(path), lwc_string_length(path)); lwc_string_unref(path); } break; case ACTION_NOSEND: case ACTION_NONE: res = NSERROR_OK; break; } return res; } /* exported interface documented in html/interaction.h */ nserror html_mouse_track(struct content *c, struct browser_window *bw, browser_mouse_state mouse, int x, int y) { return html_mouse_action(c, bw, mouse, x, y); } /* exported interface documented in html/interaction.h */ nserror html_mouse_action(struct content *c, struct browser_window *bw, browser_mouse_state mouse, int x, int y) { html_content *html = (html_content *)c; nserror res; /* handle open select menu */ if (html->visible_select_menu != NULL) { return mouse_action_select_menu(html, bw, mouse, x, y); } /* handle content drag */ switch (html->drag_type) { case HTML_DRAG_SELECTION: res = mouse_action_drag_selection(html, bw, mouse, x, y); break; case HTML_DRAG_SCROLLBAR: res = mouse_action_drag_scrollbar(html, bw, mouse, x, y); break; case HTML_DRAG_TEXTAREA_SELECTION: case HTML_DRAG_TEXTAREA_SCROLLBAR: res = mouse_action_drag_textarea(html, bw, mouse, x, y); break; case HTML_DRAG_CONTENT_SELECTION: case HTML_DRAG_CONTENT_SCROLL: res = mouse_action_drag_content(html, bw, mouse, x, y); break; case HTML_DRAG_NONE: res = mouse_action_drag_none(html, bw, mouse, x, y); break; default: /* Unknown content related drag type */ assert(0 && "Unknown content related drag type"); } if (res != NSERROR_OK) { NSLOG(netsurf, ERROR, "%s", messages_get_errorcode(res)); } return res; } /** * Handle keypresses. * * \param c content of type HTML * \param key The UCS4 character codepoint * \return true if key handled, false otherwise */ bool html_keypress(struct content *c, uint32_t key) { html_content *html = (html_content *) c; struct selection *sel = &html->sel; /** \todo * At the moment, the front end interface for keypress only gives * us a UCS4 key value. This doesn't doesn't have all the information * we need to fill out the event properly. We don't get to know about * modifier keys, and things like CTRL+C are passed in as * \ref NS_KEY_COPY_SELECTION, a magic value outside the valid Unicode * range. * * We need to: * * 1. Update the front end interface so that both press and release * events reach the core. * 2. Stop encoding the special keys like \ref NS_KEY_COPY_SELECTION as * magic values in the front ends, so we just get the events, e.g.: * 1. Press ctrl * 2. Press c * 3. Release c * 4. Release ctrl * 3. Pass all the new info to the DOM KeyboardEvent events. * 4. If there is a focused element, fire the event at that, instead of * `html->layout->node`. * 5. Rebuild the \ref NS_KEY_COPY_SELECTION values from the info we * now get given, and use that for the code below this * \ref fire_dom_keyboard_event call. * 6. Move the code after this \ref fire_dom_keyboard_event call into * the default action handler for DOM events. * * This will mean that if the JavaScript event listener does * `event.preventDefault()` then we won't handle the event when * we're not supposed to. */ if (html->layout != NULL && html->layout->node != NULL) { fire_dom_keyboard_event(corestring_dom_keydown, html->layout->node, true, true, key); } switch (html->focus_type) { case HTML_FOCUS_CONTENT: return content_keypress(html->focus_owner.content->object, key); case HTML_FOCUS_TEXTAREA: if (box_textarea_keypress(html, html->focus_owner.textarea, key) == NSERROR_OK) { return true; } else { return false; } default: /* Deal with it below */ break; } switch (key) { case NS_KEY_COPY_SELECTION: selection_copy_to_clipboard(sel); return true; case NS_KEY_CLEAR_SELECTION: selection_clear(sel, true); return true; case NS_KEY_SELECT_ALL: selection_select_all(sel); return true; case NS_KEY_ESCAPE: if (selection_defined(sel)) { selection_clear(sel, true); return true; } /* if there's no selection, leave Escape for the caller */ return false; } return false; } /** * Handle search. * * \param c content of type HTML * \param context front end private data * \param flags search flags * \param string search string */ void html_search(struct content *c, void *context, search_flags_t flags, const char *string) { html_content *html = (html_content *)c; assert(c != NULL); if ((string != NULL) && (html->search_string != NULL) && (strcmp(string, html->search_string) == 0) && (html->search != NULL)) { /* Continue prev. search */ search_step(html->search, flags, string); } else if (string != NULL) { /* New search */ free(html->search_string); html->search_string = strdup(string); if (html->search_string == NULL) return; if (html->search != NULL) { search_destroy_context(html->search); html->search = NULL; } html->search = search_create_context(c, CONTENT_HTML, context); if (html->search == NULL) return; search_step(html->search, flags, string); } else { /* Clear search */ html_search_clear(c); free(html->search_string); html->search_string = NULL; } } /** * Terminate a search. * * \param c content of type HTML */ void html_search_clear(struct content *c) { html_content *html = (html_content *)c; assert(c != NULL); free(html->search_string); html->search_string = NULL; if (html->search != NULL) { search_destroy_context(html->search); } html->search = NULL; } /** * Callback for in-page scrollbars. */ void html_overflow_scroll_callback(void *client_data, struct scrollbar_msg_data *scrollbar_data) { struct html_scrollbar_data *data = client_data; html_content *html = (html_content *)data->c; struct box *box = data->box; union content_msg_data msg_data; html_drag_type drag_type; union html_drag_owner drag_owner; switch(scrollbar_data->msg) { case SCROLLBAR_MSG_MOVED: if (html->reflowing == true) { /* Can't redraw during layout, and it will * be redrawn after layout anyway. */ break; } html__redraw_a_box(html, box); break; case SCROLLBAR_MSG_SCROLL_START: { struct rect rect = { .x0 = scrollbar_data->x0, .y0 = scrollbar_data->y0, .x1 = scrollbar_data->x1, .y1 = scrollbar_data->y1 }; drag_type = HTML_DRAG_SCROLLBAR; drag_owner.scrollbar = scrollbar_data->scrollbar; html_set_drag_type(html, drag_type, drag_owner, &rect); } break; case SCROLLBAR_MSG_SCROLL_FINISHED: drag_type = HTML_DRAG_NONE; drag_owner.no_owner = true; html_set_drag_type(html, drag_type, drag_owner, NULL); msg_data.pointer = BROWSER_POINTER_AUTO; content_broadcast(data->c, CONTENT_MSG_POINTER, &msg_data); break; } } /* Documented in html_internal.h */ void html_set_drag_type(html_content *html, html_drag_type drag_type, union html_drag_owner drag_owner, const struct rect *rect) { union content_msg_data msg_data; assert(html != NULL); html->drag_type = drag_type; html->drag_owner = drag_owner; switch (drag_type) { case HTML_DRAG_NONE: assert(drag_owner.no_owner == true); msg_data.drag.type = CONTENT_DRAG_NONE; break; case HTML_DRAG_SCROLLBAR: case HTML_DRAG_TEXTAREA_SCROLLBAR: case HTML_DRAG_CONTENT_SCROLL: msg_data.drag.type = CONTENT_DRAG_SCROLL; break; case HTML_DRAG_SELECTION: assert(drag_owner.no_owner == true); /* Fall through */ case HTML_DRAG_TEXTAREA_SELECTION: case HTML_DRAG_CONTENT_SELECTION: msg_data.drag.type = CONTENT_DRAG_SELECTION; break; } msg_data.drag.rect = rect; /* Inform of the content's drag status change */ content_broadcast((struct content *)html, CONTENT_MSG_DRAG, &msg_data); } /* Documented in html_internal.h */ void html_set_focus(html_content *html, html_focus_type focus_type, union html_focus_owner focus_owner, bool hide_caret, int x, int y, int height, const struct rect *clip) { union content_msg_data msg_data; int x_off = 0; int y_off = 0; struct rect cr; bool textarea_lost_focus = html->focus_type == HTML_FOCUS_TEXTAREA && focus_type != HTML_FOCUS_TEXTAREA; assert(html != NULL); switch (focus_type) { case HTML_FOCUS_SELF: assert(focus_owner.self == true); if (html->focus_type == HTML_FOCUS_SELF) /* Don't need to tell anyone anything */ return; break; case HTML_FOCUS_CONTENT: box_coords(focus_owner.content, &x_off, &y_off); break; case HTML_FOCUS_TEXTAREA: box_coords(focus_owner.textarea, &x_off, &y_off); break; } html->focus_type = focus_type; html->focus_owner = focus_owner; if (textarea_lost_focus) { msg_data.caret.type = CONTENT_CARET_REMOVE; } else if (focus_type != HTML_FOCUS_SELF && hide_caret) { msg_data.caret.type = CONTENT_CARET_HIDE; } else { if (clip != NULL) { cr = *clip; cr.x0 += x_off; cr.y0 += y_off; cr.x1 += x_off; cr.y1 += y_off; } msg_data.caret.type = CONTENT_CARET_SET_POS; msg_data.caret.pos.x = x + x_off; msg_data.caret.pos.y = y + y_off; msg_data.caret.pos.height = height; msg_data.caret.pos.clip = (clip == NULL) ? NULL : &cr; } /* Inform of the content's drag status change */ content_broadcast((struct content *)html, CONTENT_MSG_CARET, &msg_data); } /* Documented in html_internal.h */ void html_set_selection(html_content *html, html_selection_type selection_type, union html_selection_owner selection_owner, bool read_only) { union content_msg_data msg_data; struct box *box; bool changed = false; bool same_type = html->selection_type == selection_type; assert(html != NULL); if ((selection_type == HTML_SELECTION_NONE && html->selection_type != HTML_SELECTION_NONE) || (selection_type != HTML_SELECTION_NONE && html->selection_type == HTML_SELECTION_NONE)) /* Existance of selection has changed, and we'll need to * inform our owner */ changed = true; /* Clear any existing selection */ if (html->selection_type != HTML_SELECTION_NONE) { switch (html->selection_type) { case HTML_SELECTION_SELF: if (same_type) break; selection_clear(&html->sel, true); break; case HTML_SELECTION_TEXTAREA: if (same_type && html->selection_owner.textarea == selection_owner.textarea) break; box = html->selection_owner.textarea; textarea_clear_selection(box->gadget->data.text.ta); break; case HTML_SELECTION_CONTENT: if (same_type && html->selection_owner.content == selection_owner.content) break; box = html->selection_owner.content; content_clear_selection(box->object); break; default: break; } } html->selection_type = selection_type; html->selection_owner = selection_owner; if (!changed) /* Don't need to report lack of change to owner */ return; /* Prepare msg */ switch (selection_type) { case HTML_SELECTION_NONE: assert(selection_owner.none == true); msg_data.selection.selection = false; break; case HTML_SELECTION_SELF: assert(selection_owner.none == false); /* fall through */ case HTML_SELECTION_TEXTAREA: case HTML_SELECTION_CONTENT: msg_data.selection.selection = true; break; default: break; } msg_data.selection.read_only = read_only; /* Inform of the content's selection status change */ content_broadcast((struct content *)html, CONTENT_MSG_SELECTION, &msg_data); }