/* * Copyright 2012 - 2013 Michael Drake * * 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 * * Treeview handling implementation. */ #include #include "utils/utils.h" #include "utils/log.h" #include "utils/nsurl.h" #include "utils/nsoption.h" #include "utils/config.h" #include "netsurf/bitmap.h" #include "netsurf/content.h" #include "netsurf/plotters.h" #include "netsurf/clipboard.h" #include "netsurf/layout.h" #include "netsurf/keypress.h" #include "netsurf/core_window.h" #include "content/hlcache.h" #include "css/utils.h" #include "desktop/system_colour.h" #include "desktop/knockout.h" #include "desktop/textarea.h" #include "desktop/treeview.h" #include "desktop/gui_internal.h" /** * The maximum horizontal size a treeview can possibly be. * * \todo get rid of REDRAW_MAX -- need to be able to know window size */ #define REDRAW_MAX 8000 /** * Treeview handling global context */ struct treeview_globals { unsigned initialised; int line_height; int furniture_width; int step_width; int window_padding; int icon_size; int icon_step; int move_offset; } tree_g; /** * Section type of a treeview at a point */ enum treeview_node_part { TV_NODE_PART_TOGGLE, /**< Expansion toggle */ TV_NODE_PART_ON_NODE, /**< Node content (text, icon) */ TV_NODE_PART_NONE /**< Empty area */ }; /** * Text within a treeview field or node */ struct treeview_text { const char *data; /**< Text string */ uint32_t len; /**< Length of string in bytes */ int width; /**< Width of text in px */ }; /** * a treeview field */ struct treeview_field { /** flags controlling how field is interpreted */ enum treeview_field_flags flags; lwc_string *field; /**< field contents */ struct treeview_text value; /**< field text */ }; /** * flags indicating render state of node. */ enum treeview_node_flags { TV_NFLAGS_NONE = 0, /**< No node flags set */ TV_NFLAGS_EXPANDED = (1 << 0), /**< Whether node is expanded */ TV_NFLAGS_SELECTED = (1 << 1), /**< Whether node is selected */ TV_NFLAGS_SPECIAL = (1 << 2), /**< Render as special node */ TV_NFLAGS_MATCHED = (1 << 3), /**< Whether node matches search */ }; /** * Treeview target position */ enum treeview_target_pos { TV_TARGET_ABOVE, TV_TARGET_INSIDE, TV_TARGET_BELOW, TV_TARGET_NONE }; /** * Treeview node */ struct treeview_node { enum treeview_node_flags flags; /**< Node flags */ enum treeview_node_type type; /**< Node type */ int height; /**< Includes height of any descendants (pixels) */ int inset; /**< Node's inset depending on tree depth (pixels) */ treeview_node *parent; /**< parent node */ treeview_node *prev_sib; /**< previous sibling node */ treeview_node *next_sib; /**< next sibling node */ treeview_node *children; /**< first child node */ void *client_data; /**< Passed to client on node event msg callback */ struct treeview_text text; /** Text to show for node (default field) */ }; /** * Node entry * * node entry contains a base node at the beginning allowing for * trivial containerof by cast and some number of fields. */ struct treeview_node_entry { treeview_node base; /**< Entry class inherits node base class */ struct treeview_field fields[FLEX_ARRAY_LEN_DECL]; }; /** * A mouse position wrt treeview */ struct treeview_pos { int x; /**< Mouse X coordinate */ int y; /**< Mouse Y coordinate */ int node_y; /**< Top of node at y */ int node_h; /**< Height of node at y */ }; /** * Treeview drag state */ struct treeview_drag { enum { TV_DRAG_NONE, TV_DRAG_SELECTION, TV_DRAG_MOVE, TV_DRAG_TEXTAREA, TV_DRAG_SEARCH, } type; /**< Drag type */ treeview_node *start_node; /**< Start node */ bool selected; /**< Start node is selected */ enum treeview_node_part part; /**< Node part at start */ struct treeview_pos start; /**< Start pos */ struct treeview_pos prev; /**< Previous pos */ }; /** * Treeview node move details */ struct treeview_move { treeview_node *root; /**< Head of yanked node list */ treeview_node *target; /**< Move target */ struct rect target_area; /**< Pos/size of target indicator */ enum treeview_target_pos target_pos; /**< Pos wrt render node */ }; /** * Treeview node edit details */ struct treeview_edit { treeview_node *node; /**< Node being edited, or NULL */ struct textarea *textarea; /**< Textarea for edit, or NULL */ lwc_string *field; /**< The field being edited, or NULL */ int x; /**< Textarea x position */ int y; /**< Textarea y position */ int w; /**< Textarea width */ int h; /**< Textarea height */ }; /** * Treeview search box details */ struct treeview_search { struct textarea *textarea; /**< Search box. */ bool active; /**< Whether the search box has focus. */ bool search; /**< Whether we have a search term. */ int height; /**< Current search display height. */ }; /** * The treeview context */ struct treeview { uint32_t view_width; /**< Viewport horizontal size */ treeview_flags flags; /**< Treeview behaviour settings */ treeview_node *root; /**< Root node */ struct treeview_field *fields; /**< Array of fields */ int n_fields; /**< fields[n_fields] is folder, lower are entry fields */ int field_width; /**< Max width of shown field names */ struct treeview_drag drag; /**< Drag state */ struct treeview_move move; /**< Move drag details */ struct treeview_edit edit; /**< Edit details */ struct treeview_search search; /**< Treeview search box */ const struct treeview_callback_table *callbacks; /**< For node events */ const struct core_window_callback_table *cw_t; /**< Window cb table */ struct core_window *cw_h; /**< Core window handle */ }; /** * Treeview furniture states. */ enum treeview_furniture_id { TREE_FURN_EXPAND = 0, TREE_FURN_CONTRACT, TREE_FURN_LAST }; /** * style for a node */ struct treeview_node_style { plot_style_t bg; /**< Background */ plot_font_style_t text; /**< Text */ plot_font_style_t itext; /**< Entry field text */ plot_style_t sbg; /**< Selected background */ plot_font_style_t stext; /**< Selected text */ plot_font_style_t sitext; /**< Selected entry field text */ struct { int size; struct bitmap *bmp; struct bitmap *sel; } furn[TREE_FURN_LAST]; }; /** * Plot style for odd rows */ struct treeview_node_style plot_style_odd; /** * Plot style for even rows */ struct treeview_node_style plot_style_even; /** * Treeview content resource data */ struct treeview_resource { const char *url; struct hlcache_handle *c; int height; bool ready; }; /** * treeview resource indexes */ enum treeview_resource_id { TREE_RES_ARROW = 0, TREE_RES_CONTENT, TREE_RES_FOLDER, TREE_RES_FOLDER_SPECIAL, TREE_RES_SEARCH, TREE_RES_LAST }; /** * Treeview content resources */ static struct treeview_resource treeview_res[TREE_RES_LAST] = { { "resource:icons/arrow-l.png", NULL, 0, false }, { "resource:icons/content.png", NULL, 0, false }, { "resource:icons/directory.png", NULL, 0, false }, { "resource:icons/directory2.png", NULL, 0, false }, { "resource:icons/search.png", NULL, 0, false } }; /** * Get the display height of the treeview data component of the display. * * \param[in] tree Treeview to get the height of. * \return the display height in pixels. */ static inline int treeview__get_display_height(treeview *tree) { return (tree->search.search == false) ? tree->root->height : tree->search.height; } /** * Corewindow callback wrapper: Request a redraw of the window * * \param[in] tree The treeview to request redraw on. * \param[in] r rectangle to redraw */ static inline void treeview__cw_invalidate_area( const struct treeview *tree, const struct rect *r) { if (tree->cw_t != NULL) { tree->cw_t->invalidate(tree->cw_h, r); } } /** * Corewindow callback wrapper: Update the limits of the window * * \param[in] tree The treeview to update size for. * \param[in] width the width in px, or negative if don't care * \param[in] height the height in px, or negative if don't care */ static inline void treeview__cw_update_size( const struct treeview *tree, int width, int height) { int search_height = (tree->flags & TREEVIEW_SEARCHABLE) ? tree_g.line_height : 0; if (tree->cw_t != NULL) { tree->cw_t->update_size(tree->cw_h, width, height + search_height); } } /** * Corewindow callback_wrapper: Scroll to top of window. * * \param[in] tree The treeview to scroll. */ static inline void treeview__cw_scroll_top( const struct treeview *tree) { struct rect r = { .x0 = 0, .y0 = 0, .x1 = tree_g.window_padding, .y1 = tree_g.line_height, }; if (tree->cw_t != NULL) { tree->cw_t->scroll_visible(tree->cw_h, &r); } } /** * Corewindow callback wrapper: Get window viewport dimensions * * \param[in] tree The treeview to get dimensions for. * \param[out] width to be set to viewport width in px * \param[out] height to be set to viewport height in px */ static inline void treeview__cw_get_window_dimensions( const struct treeview *tree, int *width, int *height) { if (tree->cw_t != NULL) { tree->cw_t->get_window_dimensions(tree->cw_h, width, height); } } /** * Corewindow callback wrapper: Inform corewindow owner of drag status * * \param[in] tree The treeview to report status on. * \param[in] ds the current drag status */ static inline void treeview__cw_drag_status( const struct treeview *tree, core_window_drag_status ds) { if (tree->cw_t != NULL) { tree->cw_t->drag_status(tree->cw_h, ds); } } /** * Helper function to access the given field of a node * * \param tree Treeview that node belongs to * \param n Node to get field from * \param i Index of field of interest * \return text entry for field or NULL. */ static inline struct treeview_text * treeview_get_text_for_field(treeview *tree, treeview_node *n, int i) { if (i == 0) { return &n->text; } else if (i < tree->n_fields && n->type == TREE_NODE_ENTRY) { struct treeview_node_entry *e = (struct treeview_node_entry *)n; return &e->fields[i - 1].value; } assert(0 && "Bad field index for node"); return NULL; } /** * Find the next node in depth first tree order * * \param node Start node * \param full Iff true, visit children of collapsed nodes * \return next node, or NULL if \a node is last node */ static inline treeview_node * treeview_node_next(treeview_node *node, bool full) { assert(node != NULL); if ((full || (node->flags & TV_NFLAGS_EXPANDED)) && node->children != NULL) { /* Next node is child */ node = node->children; } else { /* No children. As long as we're not at the root, * go to next sibling if present, or nearest ancestor * with a next sibling. */ while (node->parent != NULL && node->next_sib == NULL) { node = node->parent; } if (node->type == TREE_NODE_ROOT) { node = NULL; } else { node = node->next_sib; } } return node; } /** * Find node at given y-position * * \param tree Treeview object to delete node from * \param target_y Target y-position * \return node at y_target */ static treeview_node * treeview_y_node(treeview *tree, int target_y) { treeview_node *n; int y = 0; int h; assert(tree != NULL); assert(tree->root != NULL); n = treeview_node_next(tree->root, false); while (n != NULL) { h = (n->type == TREE_NODE_ENTRY) ? n->height : tree_g.line_height; if (target_y >= y && target_y < y + h) return n; y += h; n = treeview_node_next(n, false); } return NULL; } /** * Find y position of the top of a node * * \param tree Treeview object to delete node from * \param node Node to get position of * \return node's y position */ static int treeview_node_y(treeview *tree, treeview_node *node) { treeview_node *n; int y = 0; assert(tree != NULL); assert(tree->root != NULL); n = treeview_node_next(tree->root, false); while (n != NULL && n != node) { y += (n->type == TREE_NODE_ENTRY) ? n->height : tree_g.line_height; n = treeview_node_next(n, false); } return y; } /** * The treeview walk mode. Controls which nodes are visited in a walk. */ enum treeview_walk_mode { /** * Walk to all nodes in the (sub)tree. */ TREEVIEW_WALK_MODE_LOGICAL_COMPLETE, /** * Walk to expanded nodes in the (sub)tree only. Children of * collapsed nodes are not visited. */ TREEVIEW_WALK_MODE_LOGICAL_EXPANDED, /** * Walk displayed nodes. This differs from the * `TREEVIEW_WALK_MODE_LOGICAL_EXPANDED` mode when there is * an active search filter display. */ TREEVIEW_WALK_MODE_DISPLAY, }; /** * Walk a treeview subtree, calling a callback at each node (depth first) * * \param tree Treeview being walked. * \param root Root to walk tree from (doesn't get a callback call) * \param mode The treeview walk mode to use. * \param callback_bwd Function to call on each node in backwards order * \param callback_fwd Function to call on each node in forwards order * \param ctx Context to pass to callback * \return NSERROR_OK on success, or appropriate error otherwise * * \note Any node deletion must happen in callback_bwd. */ static nserror treeview_walk_internal( treeview *tree, treeview_node *root, enum treeview_walk_mode mode, nserror (*callback_bwd)( treeview_node *n, void *ctx, bool *end), nserror (*callback_fwd)( treeview_node *n, void *ctx, bool *skip_children, bool *end), void *ctx) { treeview_node *node, *child, *parent, *next_sibling; bool walking_search = (mode == TREEVIEW_WALK_MODE_DISPLAY && tree->search.search == true); bool skip_children = false; bool abort = false; bool full = false; nserror err; bool entry; assert(root != NULL); if (mode == TREEVIEW_WALK_MODE_LOGICAL_COMPLETE || walking_search) { /* We need to visit children of collapsed folders. */ full = true; } node = root; parent = node->parent; next_sibling = node->next_sib; child = (full || (node->flags & TV_NFLAGS_EXPANDED)) ? node->children : NULL; while (node != NULL) { if (child != NULL && !skip_children) { /* Down to children */ node = child; } else { /* No children. As long as we're not at the root, * go to next sibling if present, or nearest ancestor * with a next sibling. */ while (node != root && next_sibling == NULL) { entry = (node->type == TREE_NODE_ENTRY); if (callback_bwd != NULL && (entry || !walking_search)) { /* Backwards callback */ err = callback_bwd(node, ctx, &abort); if (err != NSERROR_OK) { return err; } else if (abort) { /* callback requested early * termination */ return NSERROR_OK; } } node = parent; parent = node->parent; next_sibling = node->next_sib; } if (node == root) break; if (callback_bwd != NULL) { /* Backwards callback */ err = callback_bwd(node, ctx, &abort); if (err != NSERROR_OK) { return err; } else if (abort) { /* callback requested early * termination */ return NSERROR_OK; } } node = next_sibling; } assert(node != NULL); assert(node != root); entry = (node->type == TREE_NODE_ENTRY); parent = node->parent; next_sibling = node->next_sib; child = (full || (node->flags & TV_NFLAGS_EXPANDED)) ? node->children : NULL; if (walking_search && (!entry || !(node->flags & TV_NFLAGS_MATCHED))) { continue; } if (callback_fwd != NULL) { /* Forwards callback */ err = callback_fwd(node, ctx, &skip_children, &abort); if (err != NSERROR_OK) { return err; } else if (abort) { /* callback requested early termination */ return NSERROR_OK; } } } return NSERROR_OK; } /** * Data used when doing a treeview walk for search. */ struct treeview_search_walk_data { treeview *tree; /**< The treeview to search. */ const char *text; /**< The string being searched for. */ const unsigned int len; /**< Length of string being searched for. */ int window_height; /**< Accumulate height for matching entries. */ }; /** * Treewalk node callback for handling search. * * \param[in] n Current node. * \param[in] ctx Treeview search context. * \param[in,out] skip_children Flag to allow children to be skipped. * \param[in,out] end Flag to allow iteration to be finished early. * \return NSERROR_OK on success else error code. */ static nserror treeview__search_walk_cb( treeview_node *n, void *ctx, bool *skip_children, bool *end) { struct treeview_search_walk_data *sw = ctx; if (n->type != TREE_NODE_ENTRY) { return NSERROR_OK; } if (sw->len == 0) { n->flags &= ~TV_NFLAGS_MATCHED; } else { struct treeview_node_entry *entry = (struct treeview_node_entry *)n; bool matched = false; for (int i = 0; i < sw->tree->n_fields; i++) { struct treeview_field *ef = &(sw->tree->fields[i + 1]); if (ef->flags & TREE_FLAG_SEARCHABLE) { if (strcasestr(entry->fields[i].value.data, sw->text) != NULL) { matched = true; break; } } } if (!matched && strcasestr(n->text.data, sw->text) != NULL) { matched = true; } if (matched) { n->flags |= TV_NFLAGS_MATCHED; sw->window_height += n->height; } else { n->flags &= ~TV_NFLAGS_MATCHED; } } return NSERROR_OK; } /** * Search treeview for text. * * \param[in] tree Treeview to search. * \param[in] text UTF-8 string to search for. (NULL-terminated.) * \param[in] len Byte length of UTF-8 string. * \return NSERROR_OK on success, appropriate error otherwise. */ static nserror treeview__search( treeview *tree, const char *text, unsigned int len) { nserror err; uint32_t height; uint32_t prev_height = treeview__get_display_height(tree); int search_height = (tree->flags & TREEVIEW_SEARCHABLE) ? tree_g.line_height : 0; struct treeview_search_walk_data sw = { .len = len, .text = text, .tree = tree, .window_height = 0, }; struct rect r = { .x0 = 0, .y0 = search_height, .x1 = REDRAW_MAX, }; assert(text[len] == '\0'); if (tree->root == NULL) { return NSERROR_OK; } err = treeview_walk_internal(tree, tree->root, TREEVIEW_WALK_MODE_LOGICAL_COMPLETE, NULL, treeview__search_walk_cb, &sw); if (err != NSERROR_OK) { return err; } if (len > 0) { tree->search.height = sw.window_height; tree->search.search = true; height = sw.window_height; } else { tree->search.search = false; height = tree->root->height; } r.y1 = ((height > prev_height) ? height : prev_height) + search_height; treeview__cw_invalidate_area(tree, &r); treeview__cw_update_size(tree, -1, height); treeview__cw_scroll_top(tree); return NSERROR_OK; } /** * Cancel a treeview search, optionally droping focus from search widget. * * \param[in] tree Treeview to cancel search in. * \param[in] drop_focus Iff true, drop input focus from search widget. */ static void treeview__search_cancel(treeview *tree, bool drop_focus) { struct rect r = { .x0 = tree_g.window_padding + tree_g.icon_size, .x1 = 600, .y0 = 0, .y1 = tree_g.line_height, }; tree->search.search = false; if (tree->search.active == false) { return; } if (drop_focus) { tree->search.active = false; textarea_set_caret(tree->search.textarea, -1); } else { textarea_set_caret(tree->search.textarea, 0); } textarea_set_text(tree->search.textarea, ""); treeview__cw_invalidate_area(tree, &r); } /** * Callback for textarea_create, in desktop/treeview.h * * \param data treeview context * \param msg textarea message */ static void treeview_textarea_search_callback(void *data, struct textarea_msg *msg) { treeview *tree = data; struct rect *r; if (tree->search.active == false || tree->root == NULL) { return; } switch (msg->type) { case TEXTAREA_MSG_DRAG_REPORT: if (msg->data.drag == TEXTAREA_DRAG_NONE) { /* Textarea drag finished */ tree->drag.type = TV_DRAG_NONE; } else { /* Textarea drag started */ tree->drag.type = TV_DRAG_SEARCH; } treeview__cw_drag_status(tree, tree->drag.type); break; case TEXTAREA_MSG_REDRAW_REQUEST: r = &msg->data.redraw; r->x0 += tree_g.window_padding + tree_g.icon_size; r->y0 += 0; r->x1 += 600; r->y1 += tree_g.line_height; /* Redraw the textarea */ treeview__cw_invalidate_area(tree, r); break; case TEXTAREA_MSG_TEXT_MODIFIED: /* Textarea length includes trailing NULL, so subtract it. */ treeview__search(tree, msg->data.modified.text, msg->data.modified.len - 1); break; default: break; } } /** * Update the layout for any active search. * * \param[in] tree The tree to update. */ static void treeview__search_update_display( treeview *tree) { const char *string; unsigned int len; if (tree->search.search == false) { /* No active search to update view for. */ return; } string = textarea_data(tree->search.textarea, &len); if (string == NULL || len == 0) { return; } treeview__search(tree, string, len - 1); } /** * Create treeview's root node * * \param[out] root Returns root node * \return NSERROR_OK on success, appropriate error otherwise */ static nserror treeview_create_node_root(treeview_node **root) { treeview_node *n; n = malloc(sizeof(struct treeview_node)); if (n == NULL) { return NSERROR_NOMEM; } n->flags = TV_NFLAGS_EXPANDED; n->type = TREE_NODE_ROOT; n->height = 0; n->inset = tree_g.window_padding - tree_g.step_width; n->text.data = NULL; n->text.len = 0; n->text.width = 0; n->parent = NULL; n->next_sib = NULL; n->prev_sib = NULL; n->children = NULL; n->client_data = NULL; *root = n; return NSERROR_OK; } /** * Set a node's inset from its parent * * This may be used as treeview walk callback * * \param[in] n node to set inset on * \param[in] ctx context unused * \param[out] skip_children set to false so child nodes are not skipped. * \param[out] end unused flag so treewalk in not terminated early. */ static nserror treeview_set_inset_from_parent(treeview_node *n, void *ctx, bool *skip_children, bool *end) { if (n->parent != NULL) n->inset = n->parent->inset + tree_g.step_width; *skip_children = false; return NSERROR_OK; } /** * Insert a treeview node into a treeview * * \param tree the treeview to insert node into. * \param a parentless node to insert * \param b tree node to insert a as a relation of * \param rel The relationship between \a a and \a b */ static inline void treeview_insert_node( treeview *tree, treeview_node *a, treeview_node *b, enum treeview_relationship rel) { assert(a != NULL); assert(a->parent == NULL); assert(b != NULL); switch (rel) { case TREE_REL_FIRST_CHILD: assert(b->type != TREE_NODE_ENTRY); a->parent = b; a->next_sib = b->children; if (a->next_sib) a->next_sib->prev_sib = a; b->children = a; break; case TREE_REL_NEXT_SIBLING: assert(b->type != TREE_NODE_ROOT); a->prev_sib = b; a->next_sib = b->next_sib; a->parent = b->parent; b->next_sib = a; if (a->next_sib) a->next_sib->prev_sib = a; break; default: assert(0); break; } assert(a->parent != NULL); a->inset = a->parent->inset + tree_g.step_width; if (a->children != NULL) { treeview_walk_internal(tree, a, TREEVIEW_WALK_MODE_LOGICAL_COMPLETE, NULL, treeview_set_inset_from_parent, NULL); } if (a->parent->flags & TV_NFLAGS_EXPANDED) { int height = a->height; /* Parent is expanded, so inserted node will be visible and * affect layout */ if (a->text.width == 0) { guit->layout->width(&plot_style_odd.text, a->text.data, a->text.len, &(a->text.width)); } do { a->parent->height += height; a = a->parent; } while (a->parent != NULL); } } /* Exported interface, documented in treeview.h */ nserror treeview_create_node_folder(treeview *tree, treeview_node **folder, treeview_node *relation, enum treeview_relationship rel, const struct treeview_field_data *field, void *data, treeview_node_options_flags flags) { treeview_node *n; assert(data != NULL); assert(tree != NULL); assert(tree->root != NULL); if (relation == NULL) { relation = tree->root; rel = TREE_REL_FIRST_CHILD; } n = malloc(sizeof(struct treeview_node)); if (n == NULL) { return NSERROR_NOMEM; } n->flags = (flags & TREE_OPTION_SPECIAL_DIR) ? TV_NFLAGS_SPECIAL : TV_NFLAGS_NONE; n->type = TREE_NODE_FOLDER; n->height = tree_g.line_height; n->text.data = field->value; n->text.len = field->value_len; n->text.width = 0; n->parent = NULL; n->next_sib = NULL; n->prev_sib = NULL; n->children = NULL; n->client_data = data; treeview_insert_node(tree, n, relation, rel); if (n->parent->flags & TV_NFLAGS_EXPANDED) { /* Inform front end of change in dimensions */ if (!(flags & TREE_OPTION_SUPPRESS_RESIZE)) treeview__cw_update_size(tree, -1, tree->root->height); /* Redraw */ if (!(flags & TREE_OPTION_SUPPRESS_REDRAW)) { struct rect r; r.x0 = 0; r.y0 = treeview_node_y(tree, n); r.x1 = REDRAW_MAX; r.y1 = tree->root->height; treeview__cw_invalidate_area(tree, &r); } } *folder = n; return NSERROR_OK; } /* Exported interface, documented in treeview.h */ nserror treeview_update_node_folder(treeview *tree, treeview_node *folder, const struct treeview_field_data *field, void *data) { bool match; assert(data != NULL); assert(tree != NULL); assert(folder != NULL); assert(data == folder->client_data); assert(folder->parent != NULL); assert(field != NULL); assert(lwc_string_isequal(tree->fields[tree->n_fields].field, field->field, &match) == lwc_error_ok && match == true); folder->text.data = field->value; folder->text.len = field->value_len; folder->text.width = 0; if (folder->parent->flags & TV_NFLAGS_EXPANDED) { /* Text will be seen, get its width */ guit->layout->width(&plot_style_odd.text, folder->text.data, folder->text.len, &(folder->text.width)); } else { /* Just invalidate the width, since it's not needed now */ folder->text.width = 0; } /* Redraw */ if (folder->parent->flags & TV_NFLAGS_EXPANDED) { struct rect r; r.x0 = 0; r.y0 = treeview_node_y(tree, folder); r.x1 = REDRAW_MAX; r.y1 = r.y0 + tree_g.line_height; treeview__cw_invalidate_area(tree, &r); } return NSERROR_OK; } /* Exported interface, documented in treeview.h */ nserror treeview_update_node_entry(treeview *tree, treeview_node *entry, const struct treeview_field_data fields[], void *data) { bool match; struct treeview_node_entry *e = (struct treeview_node_entry *)entry; int i; assert(data != NULL); assert(tree != NULL); assert(entry != NULL); assert(data == entry->client_data); assert(entry->parent != NULL); assert(fields != NULL); assert(fields[0].field != NULL); assert(lwc_string_isequal(tree->fields[0].field, fields[0].field, &match) == lwc_error_ok && match == true); entry->text.data = fields[0].value; entry->text.len = fields[0].value_len; entry->text.width = 0; if (entry->parent->flags & TV_NFLAGS_EXPANDED) { /* Text will be seen, get its width */ guit->layout->width(&plot_style_odd.text, entry->text.data, entry->text.len, &(entry->text.width)); } else { /* Just invalidate the width, since it's not needed now */ entry->text.width = 0; } for (i = 1; i < tree->n_fields; i++) { assert(fields[i].field != NULL); assert(lwc_string_isequal(tree->fields[i].field, fields[i].field, &match) == lwc_error_ok && match == true); e->fields[i - 1].value.data = fields[i].value; e->fields[i - 1].value.len = fields[i].value_len; if (entry->flags & TV_NFLAGS_EXPANDED) { /* Text will be seen, get its width */ guit->layout->width(&plot_style_odd.text, e->fields[i - 1].value.data, e->fields[i - 1].value.len, &(e->fields[i - 1].value.width)); } else { /* Invalidate the width, since it's not needed yet */ e->fields[i - 1].value.width = 0; } } treeview__search_update_display(tree); /* Redraw */ if (entry->parent->flags & TV_NFLAGS_EXPANDED) { struct rect r; r.x0 = 0; r.y0 = treeview_node_y(tree, entry); r.x1 = REDRAW_MAX; r.y1 = r.y0 + entry->height; treeview__cw_invalidate_area(tree, &r); } return NSERROR_OK; } /* Exported interface, documented in treeview.h */ nserror treeview_create_node_entry(treeview *tree, treeview_node **entry, treeview_node *relation, enum treeview_relationship rel, const struct treeview_field_data fields[], void *data, treeview_node_options_flags flags) { bool match; struct treeview_node_entry *e; treeview_node *n; int i; assert(data != NULL); assert(tree != NULL); assert(tree->root != NULL); if (relation == NULL) { relation = tree->root; rel = TREE_REL_FIRST_CHILD; } e = malloc(sizeof(struct treeview_node_entry) + (tree->n_fields - 1) * sizeof(struct treeview_field)); if (e == NULL) { return NSERROR_NOMEM; } n = (treeview_node *) e; n->flags = TV_NFLAGS_NONE; n->type = TREE_NODE_ENTRY; n->height = tree_g.line_height; assert(fields != NULL); assert(fields[0].field != NULL); assert(lwc_string_isequal(tree->fields[0].field, fields[0].field, &match) == lwc_error_ok && match == true); n->text.data = fields[0].value; n->text.len = fields[0].value_len; n->text.width = 0; n->parent = NULL; n->next_sib = NULL; n->prev_sib = NULL; n->children = NULL; n->client_data = data; for (i = 1; i < tree->n_fields; i++) { assert(fields[i].field != NULL); assert(lwc_string_isequal(tree->fields[i].field, fields[i].field, &match) == lwc_error_ok && match == true); e->fields[i - 1].value.data = fields[i].value; e->fields[i - 1].value.len = fields[i].value_len; e->fields[i - 1].value.width = 0; } treeview_insert_node(tree, n, relation, rel); if (n->parent->flags & TV_NFLAGS_EXPANDED) { /* Inform front end of change in dimensions */ if (!(flags & TREE_OPTION_SUPPRESS_RESIZE)) treeview__cw_update_size(tree, -1, tree->root->height); /* Redraw */ if (!(flags & TREE_OPTION_SUPPRESS_REDRAW)) { struct rect r; r.x0 = 0; r.y0 = treeview_node_y(tree, n); r.x1 = REDRAW_MAX; r.y1 = tree->root->height; treeview__cw_invalidate_area(tree, &r); } } treeview__search_update_display(tree); *entry = n; return NSERROR_OK; } /** * Treewalk iterator context */ struct treeview_walk_ctx { treeview_walk_cb enter_cb; treeview_walk_cb leave_cb; void *ctx; enum treeview_node_type type; }; /** * Treewalk node enter callback. * * \param n current node * \param ctx treewalk context * \param skip_children set if child nodes should be skipped * \param end set if iteration should end early */ static nserror treeview_walk_fwd_cb(treeview_node *n, void *ctx, bool *skip_children, bool *end) { struct treeview_walk_ctx *tw = ctx; if (n->type & tw->type) { return tw->enter_cb(tw->ctx, n->client_data, n->type, end); } return NSERROR_OK; } /** * Treewalk node leave callback. * * \param n current node * \param ctx treewalk context * \param end set if iteration should end early */ static nserror treeview_walk_bwd_cb(treeview_node *n, void *ctx, bool *end) { struct treeview_walk_ctx *tw = ctx; if (n->type & tw->type) { return tw->leave_cb(tw->ctx, n->client_data, n->type, end); } return NSERROR_OK; } /* Exported interface, documented in treeview.h */ nserror treeview_walk(treeview *tree, treeview_node *root, treeview_walk_cb enter_cb, treeview_walk_cb leave_cb, void *ctx, enum treeview_node_type type) { struct treeview_walk_ctx tw = { .enter_cb = enter_cb, .leave_cb = leave_cb, .ctx = ctx, .type = type }; assert(tree != NULL); assert(tree->root != NULL); if (root == NULL) root = tree->root; return treeview_walk_internal(tree, root, TREEVIEW_WALK_MODE_LOGICAL_COMPLETE, (leave_cb != NULL) ? treeview_walk_bwd_cb : NULL, (enter_cb != NULL) ? treeview_walk_fwd_cb : NULL, &tw); } /** * Unlink a treeview node * * \param n Node to unlink * \return true iff ancestor heights need to be reduced */ static inline bool treeview_unlink_node(treeview_node *n) { /* Unlink node from tree */ if (n->parent != NULL && n->parent->children == n) { /* Node is a first child */ n->parent->children = n->next_sib; } else if (n->prev_sib != NULL) { /* Node is not first child */ n->prev_sib->next_sib = n->next_sib; } if (n->next_sib != NULL) { /* Always need to do this */ n->next_sib->prev_sib = n->prev_sib; } /* Reduce ancestor heights */ if ((n->parent != NULL) && (n->parent->flags & TV_NFLAGS_EXPANDED)) { return true; } return false; } /** * Cancel the editing of a treeview node * * \param tree Treeview object to cancel node editing in * \param redraw Set true iff redraw of removed textarea area required */ static void treeview_edit_cancel(treeview *tree, bool redraw) { struct rect r; if (tree->edit.textarea == NULL) return; textarea_destroy(tree->edit.textarea); tree->edit.textarea = NULL; tree->edit.node = NULL; if (tree->drag.type == TV_DRAG_TEXTAREA) tree->drag.type = TV_DRAG_NONE; if (redraw) { r.x0 = tree->edit.x; r.y0 = tree->edit.y; r.x1 = tree->edit.x + tree->edit.w; r.y1 = tree->edit.y + tree->edit.h; treeview__cw_invalidate_area(tree, &r); } } /** * Complete a treeview edit * * Complete edit by informing the client with a change request msg * * \param tree Treeview object to complete edit in */ static void treeview_edit_done(treeview *tree) { int len, error; char* new_text; treeview_node *n = tree->edit.node; struct treeview_node_msg msg; msg.msg = TREE_MSG_NODE_EDIT; if (tree->edit.textarea == NULL) { return; } assert(n != NULL); /* Get new text length */ len = textarea_get_text(tree->edit.textarea, NULL, 0); new_text = malloc(len); if (new_text == NULL) { /* TODO: don't just silently ignore */ return; } /* Get the new text from textarea */ error = textarea_get_text(tree->edit.textarea, new_text, len); if (error == -1) { /* TODO: don't just silently ignore */ free(new_text); return; } /* Inform the treeview client with change request message */ msg.data.node_edit.field = tree->edit.field; msg.data.node_edit.text = new_text; switch (n->type) { case TREE_NODE_ENTRY: tree->callbacks->entry(msg, n->client_data); break; case TREE_NODE_FOLDER: tree->callbacks->folder(msg, n->client_data); break; case TREE_NODE_ROOT: break; default: break; } /* Finished with the new text */ free(new_text); /* Finally, destroy the treeview, and redraw */ treeview_edit_cancel(tree, true); } /** * context for treeview node deletion iterator */ struct treeview_node_delete { treeview *tree; int h_reduction; bool user_interaction; }; /** * Treewalk node callback deleting nodes. */ static nserror treeview_delete_node_walk_cb(treeview_node *n, void *ctx, bool *end) { struct treeview_node_delete *nd = (struct treeview_node_delete *)ctx; struct treeview_node_msg msg; msg.msg = TREE_MSG_NODE_DELETE; msg.data.delete.user = nd->user_interaction; assert(n->children == NULL); if (treeview_unlink_node(n)) nd->h_reduction += (n->type == TREE_NODE_ENTRY) ? n->height : tree_g.line_height; /* Handle any special treatment */ switch (n->type) { case TREE_NODE_ENTRY: nd->tree->callbacks->entry(msg, n->client_data); break; case TREE_NODE_FOLDER: nd->tree->callbacks->folder(msg, n->client_data); break; case TREE_NODE_ROOT: break; default: return NSERROR_BAD_PARAMETER; } /* Cancel any edit of this node */ if (nd->tree->edit.textarea != NULL && nd->tree->edit.node == n) { treeview_edit_cancel(nd->tree, false); } /* Free the node */ free(n); return NSERROR_OK; } /** * Delete a treeview node * * Will emit folder or entry deletion msg callback. * * \note this can be called from inside a treeview_walk fwd callback. * For example walking the tree and calling this for any node that's selected. * * This function does not delete empty nodes, so if TREEVIEW_DEL_EMPTY_DIRS is * set, caller must also call treeview_delete_empty. * * \param tree Treeview object to delete node from * \param n Node to delete * \param interaction Delete is result of user interaction with treeview * \param flags Treeview node options flags * \return NSERROR_OK on success, appropriate error otherwise */ static nserror treeview_delete_node_internal(treeview *tree, treeview_node *n, bool interaction, treeview_node_options_flags flags) { nserror err; treeview_node *p = n->parent; struct treeview_node_delete nd = { .tree = tree, .h_reduction = 0, .user_interaction = interaction }; if (interaction && (tree->flags & TREEVIEW_NO_DELETES)) { return NSERROR_OK; } /* Delete any children first */ err = treeview_walk_internal(tree, n, TREEVIEW_WALK_MODE_LOGICAL_COMPLETE, treeview_delete_node_walk_cb, NULL, &nd); if (err != NSERROR_OK) { return err; } /* Now delete node */ if (n == tree->root) tree->root = NULL; err = treeview_delete_node_walk_cb(n, &nd, false); if (err != NSERROR_OK) { return err; } n = p; /* Reduce ancestor heights */ while (n != NULL && n->flags & TV_NFLAGS_EXPANDED) { n->height -= nd.h_reduction; n = n->parent; } /* Inform front end of change in dimensions */ if (tree->root != NULL && p != NULL && p->flags & TV_NFLAGS_EXPANDED && nd.h_reduction > 0 && !(flags & TREE_OPTION_SUPPRESS_RESIZE)) { treeview__cw_update_size(tree, -1, tree->root->height); } treeview__search_update_display(tree); return NSERROR_OK; } /** * Delete any empty treeview folder nodes * * \param tree Treeview object to delete empty nodes from * \param interaction Delete is result of user interaction with treeview * \return NSERROR_OK on success, appropriate error otherwise * * Note this must not be called within a treeview_walk. It may delete the * walker's 'current' node, making it impossible to move on without invalid * reads. */ static nserror treeview_delete_empty_nodes(treeview *tree, bool interaction) { treeview_node *node, *child, *parent, *next_sibling, *p; bool abort = false; nserror err; struct treeview_node_delete nd = { .tree = tree, .h_reduction = 0, .user_interaction = interaction }; assert(tree != NULL); assert(tree->root != NULL); node = tree->root; parent = node->parent; next_sibling = node->next_sib; child = (node->flags & TV_NFLAGS_EXPANDED) ? node->children : NULL; while (node != NULL) { if (child != NULL) { /* Down to children */ node = child; } else { /* No children. As long as we're not at the root, * go to next sibling if present, or nearest ancestor * with a next sibling. */ while (node->parent != NULL && next_sibling == NULL) { if (node->type == TREE_NODE_FOLDER && node->children == NULL) { /* Delete node */ p = node->parent; err = treeview_delete_node_walk_cb( node, &nd, &abort); if (err != NSERROR_OK) { return err; } /* Reduce ancestor heights */ while (p != NULL && p->flags & TV_NFLAGS_EXPANDED) { p->height -= nd.h_reduction; p = p->parent; } nd.h_reduction = 0; } node = parent; parent = node->parent; next_sibling = node->next_sib; } if (node->parent == NULL) break; if (node->type == TREE_NODE_FOLDER && node->children == NULL) { /* Delete node */ p = node->parent; err = treeview_delete_node_walk_cb( node, &nd, &abort); if (err != NSERROR_OK) { return err; } /* Reduce ancestor heights */ while (p != NULL && p->flags & TV_NFLAGS_EXPANDED) { p->height -= nd.h_reduction; p = p->parent; } nd.h_reduction = 0; } node = next_sibling; } assert(node != NULL); assert(node->parent != NULL); parent = node->parent; next_sibling = node->next_sib; child = (node->flags & TV_NFLAGS_EXPANDED) ? node->children : NULL; } return NSERROR_OK; } /* Exported interface, documented in treeview.h */ nserror treeview_delete_node(treeview *tree, treeview_node *n, treeview_node_options_flags flags) { nserror err; struct rect r; bool visible; assert(tree != NULL); assert(n != NULL); assert(n->parent != NULL); visible = n->parent->flags & TV_NFLAGS_EXPANDED; r.y0 = treeview_node_y(tree, n); r.y1 = tree->root->height; err = treeview_delete_node_internal(tree, n, false, flags); if (err != NSERROR_OK) return err; if (tree->flags & TREEVIEW_DEL_EMPTY_DIRS) { int h = tree->root->height; /* Delete any empty nodes */ err = treeview_delete_empty_nodes(tree, false); if (err != NSERROR_OK) return err; /* Inform front end of change in dimensions */ if (tree->root->height != h) { r.y0 = 0; if (!(flags & TREE_OPTION_SUPPRESS_RESIZE)) { treeview__cw_update_size(tree, -1, tree->root->height); } } } /* Redraw */ if (visible && !(flags & TREE_OPTION_SUPPRESS_REDRAW)) { r.x0 = 0; r.x1 = REDRAW_MAX; treeview__cw_invalidate_area(tree, &r); } return NSERROR_OK; } /** * Helper to create a textarea. * * \param[in] tree The treeview we're creating the textarea for. * \param[in] width The width of the textarea. * \param[in] height The height of the textarea. * \param[in] border The border colour to use. * \param[in] background The background colour to use. * \param[in] foreground The foreground colour to use. * \param[in] text The text style to use for the text area. * \param[in] ta_callback The textarea callback function to give the textarea. * \return the textarea pointer on success, or NULL on failure. */ static struct textarea *treeview__create_textarea( treeview *tree, int width, int height, colour border, colour background, colour foreground, plot_font_style_t text, textarea_client_callback ta_callback) { /* Configure the textarea */ textarea_flags ta_flags = TEXTAREA_INTERNAL_CARET; textarea_setup ta_setup = { .text = text, .width = width, .height = height, .pad_top = 0, .pad_left = 2, .pad_right = 2, .pad_bottom = 0, .border_width = 1, .border_col = border, .selected_bg = foreground, .selected_text = background, }; ta_setup.text.foreground = foreground; ta_setup.text.background = background; /* Create text area */ return textarea_create(ta_flags, &ta_setup, ta_callback, tree); } /* Exported interface, documented in treeview.h */ nserror treeview_create(treeview **tree, const struct treeview_callback_table *callbacks, int n_fields, struct treeview_field_desc fields[], const struct core_window_callback_table *cw_t, struct core_window *cw, treeview_flags flags) { nserror error; int i; assert((cw_t == NULL && cw == NULL) || (cw_t != NULL && cw != NULL)); assert(callbacks != NULL); assert(fields != NULL); assert(fields[0].flags & TREE_FLAG_DEFAULT); assert(fields[n_fields - 1].flags & TREE_FLAG_DEFAULT); assert(n_fields >= 2); *tree = malloc(sizeof(struct treeview)); if (*tree == NULL) { return NSERROR_NOMEM; } (*tree)->fields = malloc(sizeof(struct treeview_field) * n_fields); if ((*tree)->fields == NULL) { free(tree); return NSERROR_NOMEM; } error = treeview_create_node_root(&((*tree)->root)); if (error != NSERROR_OK) { free((*tree)->fields); free(*tree); return error; } (*tree)->field_width = 0; for (i = 0; i < n_fields; i++) { struct treeview_field *f = &((*tree)->fields[i]); f->flags = fields[i].flags; f->field = lwc_string_ref(fields[i].field); f->value.data = lwc_string_data(fields[i].field); f->value.len = lwc_string_length(fields[i].field); guit->layout->width(&plot_style_odd.text, f->value.data, f->value.len, &(f->value.width)); if (f->flags & TREE_FLAG_SHOW_NAME) if ((*tree)->field_width < f->value.width) (*tree)->field_width = f->value.width; } (*tree)->field_width += tree_g.step_width; (*tree)->callbacks = callbacks; (*tree)->n_fields = n_fields - 1; (*tree)->drag.type = TV_DRAG_NONE; (*tree)->drag.start_node = NULL; (*tree)->drag.start.x = 0; (*tree)->drag.start.y = 0; (*tree)->drag.start.node_y = 0; (*tree)->drag.start.node_h = 0; (*tree)->drag.prev.x = 0; (*tree)->drag.prev.y = 0; (*tree)->drag.prev.node_y = 0; (*tree)->drag.prev.node_h = 0; (*tree)->move.root = NULL; (*tree)->move.target = NULL; (*tree)->move.target_pos = TV_TARGET_NONE; (*tree)->edit.textarea = NULL; (*tree)->edit.node = NULL; if (flags & TREEVIEW_SEARCHABLE) { (*tree)->search.textarea = treeview__create_textarea( *tree, 600, tree_g.line_height, plot_style_even.text.background, plot_style_even.text.background, plot_style_even.text.foreground, plot_style_odd.text, treeview_textarea_search_callback); if ((*tree)->search.textarea == NULL) { treeview_destroy(*tree); return NSERROR_NOMEM; } } else { (*tree)->search.textarea = NULL; } (*tree)->search.active = false; (*tree)->search.search = false; (*tree)->flags = flags; (*tree)->cw_t = cw_t; (*tree)->cw_h = cw; return NSERROR_OK; } /* Exported interface, documented in treeview.h */ nserror treeview_cw_attach(treeview *tree, const struct core_window_callback_table *cw_t, struct core_window *cw) { assert(cw_t != NULL); assert(cw != NULL); if (tree->cw_t != NULL || tree->cw_h != NULL) { NSLOG(netsurf, INFO, "Treeview already attached."); return NSERROR_UNKNOWN; } tree->cw_t = cw_t; tree->cw_h = cw; return NSERROR_OK; } /* Exported interface, documented in treeview.h */ nserror treeview_cw_detach(treeview *tree) { tree->cw_t = NULL; tree->cw_h = NULL; treeview__search_cancel(tree, true); return NSERROR_OK; } /* Exported interface, documented in treeview.h */ nserror treeview_destroy(treeview *tree) { int f; assert(tree != NULL); if (tree->search.textarea != NULL) { tree->search.active = false; tree->search.search = false; textarea_destroy(tree->search.textarea); } /* Destroy nodes */ treeview_delete_node_internal(tree, tree->root, false, TREE_OPTION_SUPPRESS_RESIZE | TREE_OPTION_SUPPRESS_REDRAW); /* Destroy feilds */ for (f = 0; f <= tree->n_fields; f++) { lwc_string_unref(tree->fields[f].field); } free(tree->fields); /* Free treeview */ free(tree); return NSERROR_OK; } /** * Expand a treeview's nodes * * \param tree Treeview object to expand nodes in * \param node The node to expand. * \return NSERROR_OK on success, appropriate error otherwise. */ static nserror treeview_node_expand_internal(treeview *tree, treeview_node *node) { treeview_node *child; struct treeview_node_entry *e; int additional_height = 0; int i; assert(tree != NULL); assert(node != NULL); if (node->flags & TV_NFLAGS_EXPANDED) { /* What madness is this? */ NSLOG(netsurf, INFO, "Tried to expand an expanded node."); return NSERROR_OK; } switch (node->type) { case TREE_NODE_FOLDER: child = node->children; if (child == NULL) { /* Allow expansion of empty folders */ break; } do { if (child->text.width == 0) { guit->layout->width(&plot_style_odd.text, child->text.data, child->text.len, &(child->text.width)); } additional_height += child->height; child = child->next_sib; } while (child != NULL); break; case TREE_NODE_ENTRY: assert(node->children == NULL); e = (struct treeview_node_entry *)node; for (i = 0; i < tree->n_fields - 1; i++) { if (e->fields[i].value.width == 0) { guit->layout->width(&plot_style_odd.text, e->fields[i].value.data, e->fields[i].value.len, &(e->fields[i].value.width)); } /* Add height for field */ additional_height += tree_g.line_height; } break; case TREE_NODE_ROOT: case TREE_NODE_NONE: assert(node->type != TREE_NODE_ROOT); assert(node->type != TREE_NODE_NONE); break; } /* Update the node */ node->flags |= TV_NFLAGS_EXPANDED; /* And node heights */ for (struct treeview_node *n = node; (n != NULL) && (n->flags & TV_NFLAGS_EXPANDED); n = n->parent) { n->height += additional_height; } if (tree->search.search && node->type == TREE_NODE_ENTRY && node->flags & TV_NFLAGS_MATCHED) { tree->search.height += additional_height; } /* Inform front end of change in dimensions */ if (additional_height != 0) { treeview__cw_update_size(tree, -1, treeview__get_display_height(tree)); } return NSERROR_OK; } /* Exported interface, documented in treeview.h */ nserror treeview_node_expand(treeview *tree, treeview_node *node) { nserror res; struct rect r; res = treeview_node_expand_internal(tree, node); if (res == NSERROR_OK) { /* expansion was successful, attempt redraw */ r.x0 = 0; r.y0 = treeview_node_y(tree, node); r.x1 = REDRAW_MAX; r.y1 = treeview__get_display_height(tree); treeview__cw_invalidate_area(tree, &r); } return res; } /** * context for treeview contraction callback */ struct treeview_contract_data { treeview *tree; bool only_entries; }; /** * Treewalk node callback for handling node contraction. * * \param n node * \param ctx contract iterator context * \param end flag to end iteration now * \return NSERROR_OK on success else appropriate error code */ static nserror treeview_node_contract_cb(treeview_node *n, void *ctx, bool *end) { struct treeview_contract_data *data = ctx; int h_reduction; assert(n != NULL); assert(n->type != TREE_NODE_ROOT); n->flags &= ~TV_NFLAGS_SELECTED; if ((n->flags & TV_NFLAGS_EXPANDED) == false || (n->type == TREE_NODE_FOLDER && data->only_entries)) { /* Nothing to do. */ return NSERROR_OK; } h_reduction = n->height - tree_g.line_height; assert(h_reduction >= 0); for (struct treeview_node *node = n; (node != NULL) && (node->flags & TV_NFLAGS_EXPANDED); node = node->parent) { node->height -= h_reduction; } if (data->tree->search.search) { data->tree->search.height -= h_reduction; } n->flags ^= TV_NFLAGS_EXPANDED; return NSERROR_OK; } /** * Contract a treeview node * * \param tree Treeview object to contract node in * \param node Node to contract * \return NSERROR_OK on success, appropriate error otherwise */ static nserror treeview_node_contract_internal(treeview *tree, treeview_node *node) { struct treeview_contract_data data; bool selected; assert(node != NULL); if ((node->flags & TV_NFLAGS_EXPANDED) == false) { /* What madness is this? */ NSLOG(netsurf, INFO, "Tried to contract a contracted node."); return NSERROR_OK; } data.tree = tree; data.only_entries = false; selected = node->flags & TV_NFLAGS_SELECTED; /* Contract children. */ treeview_walk_internal(tree, node, TREEVIEW_WALK_MODE_LOGICAL_EXPANDED, treeview_node_contract_cb, NULL, &data); /* Contract node */ treeview_node_contract_cb(node, &data, false); if (selected) node->flags |= TV_NFLAGS_SELECTED; /* Inform front end of change in dimensions */ treeview__cw_update_size(tree, -1, treeview__get_display_height(tree)); return NSERROR_OK; } /* Exported interface, documented in treeview.h */ nserror treeview_node_contract(treeview *tree, treeview_node *node) { nserror res; struct rect r; assert(tree != NULL); res = treeview_node_contract_internal(tree, node); if (res == NSERROR_OK) { /* successful contraction, request redraw */ r.x0 = 0; r.y0 = treeview_node_y(tree, node); r.x1 = REDRAW_MAX; r.y1 = tree->root->height; treeview__cw_invalidate_area(tree, &r); } return res; } /* Exported interface, documented in treeview.h */ nserror treeview_contract(treeview *tree, bool all) { struct treeview_contract_data data; bool selected; treeview_node *n; struct rect r; assert(tree != NULL); assert(tree->root != NULL); r.x0 = 0; r.y0 = 0; r.x1 = REDRAW_MAX; r.y1 = tree->root->height; data.tree = tree; data.only_entries = !all; for (n = tree->root->children; n != NULL; n = n->next_sib) { if ((n->flags & TV_NFLAGS_EXPANDED) == false) { continue; } selected = n->flags & TV_NFLAGS_SELECTED; /* Contract children. */ treeview_walk_internal(tree, n, TREEVIEW_WALK_MODE_LOGICAL_EXPANDED, treeview_node_contract_cb, NULL, &data); /* Contract node */ treeview_node_contract_cb(n, &data, false); if (selected) n->flags |= TV_NFLAGS_SELECTED; } /* Inform front end of change in dimensions */ treeview__cw_update_size(tree, -1, tree->root->height); /* Redraw */ treeview__cw_invalidate_area(tree, &r); return NSERROR_OK; } /** * context data for treeview expansion */ struct treeview_expand_data { treeview *tree; bool only_folders; }; /** * Treewalk node callback for handling recursive node expansion. * * \param n current node * \param ctx node expansion context * \param skip_children flag to allow children to be skipped * \param end flag to allow iteration to be finished early. * \return NSERROR_OK on success else error code. */ static nserror treeview_expand_cb(treeview_node *n, void *ctx, bool *skip_children, bool *end) { struct treeview_expand_data *data = ctx; nserror err; assert(n != NULL); assert(n->type != TREE_NODE_ROOT); if (n->flags & TV_NFLAGS_EXPANDED || (data->only_folders && n->type != TREE_NODE_FOLDER)) { /* Nothing to do. */ return NSERROR_OK; } err = treeview_node_expand_internal(data->tree, n); return err; } /* Exported interface, documented in treeview.h */ nserror treeview_expand(treeview *tree, bool only_folders) { struct treeview_expand_data data; nserror res; struct rect r; assert(tree != NULL); assert(tree->root != NULL); data.tree = tree; data.only_folders = only_folders; res = treeview_walk_internal(tree, tree->root, TREEVIEW_WALK_MODE_LOGICAL_COMPLETE, NULL, treeview_expand_cb, &data); if (res == NSERROR_OK) { /* expansion succeeded, schedule redraw */ r.x0 = 0; r.y0 = 0; r.x1 = REDRAW_MAX; r.y1 = tree->root->height; treeview__cw_invalidate_area(tree, &r); } return res; } /** * Draw a treeview normally, in tree mode. * * \param[in] tree The treeview we're rendering. * \param[in] x X coordinate we're rendering the treeview at. * \param[in] y Y coordinate we're rendering the treeview at. * \param[in,out] render_y Current vertical position in tree, updated on exit. * \param[in] r Clip rectangle. * \param[in] data Redraw data for rendering contents. * \param[in] ctx Current render context. */ static void treeview_redraw_tree( treeview *tree, const int x, const int y, int *render_y_in_out, struct rect *r, struct content_redraw_data *data, const struct redraw_context *ctx) { struct treeview_node_style *style = &plot_style_odd; enum treeview_resource_id res = TREE_RES_CONTENT; int baseline = (tree_g.line_height * 3 + 2) / 4; plot_font_style_t *infotext_style; treeview_node *root = tree->root; treeview_node *node = tree->root; int render_y = *render_y_in_out; plot_font_style_t *text_style; plot_style_t *bg_style; int sel_min, sel_max; uint32_t count = 0; struct rect rect; int inset; int x0; if (tree->drag.start.y > tree->drag.prev.y) { sel_min = tree->drag.prev.y; sel_max = tree->drag.start.y; } else { sel_min = tree->drag.start.y; sel_max = tree->drag.prev.y; } while (node != NULL) { struct treeview_node_entry *entry; struct bitmap *furniture; bool invert_selection; treeview_node *next; int height; int i; next = (node->flags & TV_NFLAGS_EXPANDED) ? node->children : NULL; if (next != NULL) { /* down to children */ node = next; } else { /* No children. As long as we're not at the root, * go to next sibling if present, or nearest ancestor * with a next sibling. */ while (node != root && node->next_sib == NULL) { node = node->parent; } if (node == root) break; node = node->next_sib; } assert(node != NULL); assert(node != root); assert(node->type == TREE_NODE_FOLDER || node->type == TREE_NODE_ENTRY); count++; inset = x + node->inset; height = (node->type == TREE_NODE_ENTRY) ? node->height : tree_g.line_height; if ((render_y + height) < r->y0) { /* This node's line is above clip region */ render_y += height; continue; } style = (count & 0x1) ? &plot_style_odd : &plot_style_even; if (tree->drag.type == TV_DRAG_SELECTION && (render_y + height >= sel_min && render_y < sel_max)) { invert_selection = true; } else { invert_selection = false; } if ((node->flags & TV_NFLAGS_SELECTED && !invert_selection) || (!(node->flags & TV_NFLAGS_SELECTED) && invert_selection)) { bg_style = &style->sbg; text_style = &style->stext; infotext_style = &style->sitext; furniture = (node->flags & TV_NFLAGS_EXPANDED) ? style->furn[TREE_FURN_CONTRACT].sel : style->furn[TREE_FURN_EXPAND].sel; } else { bg_style = &style->bg; text_style = &style->text; infotext_style = &style->itext; furniture = (node->flags & TV_NFLAGS_EXPANDED) ? style->furn[TREE_FURN_CONTRACT].bmp : style->furn[TREE_FURN_EXPAND].bmp; } /* Render background */ rect.x0 = r->x0; rect.y0 = render_y; rect.x1 = r->x1; rect.y1 = render_y + height; ctx->plot->rectangle(ctx, bg_style, &rect); /* Render toggle */ ctx->plot->bitmap(ctx, furniture, inset, render_y + tree_g.line_height / 4, style->furn[TREE_FURN_EXPAND].size, style->furn[TREE_FURN_EXPAND].size, bg_style->fill_colour, BITMAPF_NONE); /* Render icon */ if (node->type == TREE_NODE_ENTRY) { res = TREE_RES_CONTENT; } else if (node->flags & TV_NFLAGS_SPECIAL) { res = TREE_RES_FOLDER_SPECIAL; } else { res = TREE_RES_FOLDER; } if (treeview_res[res].ready) { /* Icon resource is available */ data->x = inset + tree_g.step_width; data->y = render_y + ((tree_g.line_height - treeview_res[res].height + 1) / 2); data->background_colour = bg_style->fill_colour; content_redraw(treeview_res[res].c, data, r, ctx); } /* Render text */ x0 = inset + tree_g.step_width + tree_g.icon_step; ctx->plot->text(ctx, text_style, x0, render_y + baseline, node->text.data, node->text.len); /* Rendered the node */ render_y += tree_g.line_height; if (render_y > r->y1) { /* Passed the bottom of what's in the clip region. * Done. */ break; } if (node->type != TREE_NODE_ENTRY || !(node->flags & TV_NFLAGS_EXPANDED)) /* Done everything for this node */ continue; /* Render expanded entry fields */ entry = (struct treeview_node_entry *)node; for (i = 0; i < tree->n_fields - 1; i++) { struct treeview_field *ef = &(tree->fields[i + 1]); if (ef->flags & TREE_FLAG_SHOW_NAME) { int max_width = tree->field_width; ctx->plot->text(ctx, infotext_style, x0 + max_width - ef->value.width - tree_g.step_width, render_y + baseline, ef->value.data, ef->value.len); ctx->plot->text(ctx, infotext_style, x0 + max_width, render_y + baseline, entry->fields[i].value.data, entry->fields[i].value.len); } else { ctx->plot->text(ctx, infotext_style, x0, render_y + baseline, entry->fields[i].value.data, entry->fields[i].value.len); } /* Rendered the expanded entry field */ render_y += tree_g.line_height; } /* Finished rendering expanded entry */ if (render_y > r->y1) { /* Passed the bottom of what's in the clip region. * Done. */ break; } } *render_y_in_out = render_y; } /** * Draw a treeview normally, in tree mode. * * \param[in] tree The treeview we're rendering. * \param[in] x X coordinate we're rendering the treeview at. * \param[in] y Y coordinate we're rendering the treeview at. * \param[in,out] render_y Current vertical position in tree, updated on exit. * \param[in] r Clip rectangle. * \param[in] data Redraw data for rendering contents. * \param[in] ctx Current render context. */ static void treeview_redraw_search( treeview *tree, const int x, const int y, int *render_y_in_out, struct rect *r, struct content_redraw_data *data, const struct redraw_context *ctx) { struct treeview_node_style *style = &plot_style_odd; enum treeview_resource_id res = TREE_RES_CONTENT; int baseline = (tree_g.line_height * 3 + 2) / 4; plot_font_style_t *infotext_style; treeview_node *root = tree->root; treeview_node *node = tree->root; int render_y = *render_y_in_out; plot_font_style_t *text_style; plot_style_t *bg_style; int sel_min, sel_max; uint32_t count = 0; struct rect rect; int inset; int x0; if (tree->drag.start.y > tree->drag.prev.y) { sel_min = tree->drag.prev.y; sel_max = tree->drag.start.y; } else { sel_min = tree->drag.start.y; sel_max = tree->drag.prev.y; } while (node != NULL) { struct treeview_node_entry *entry; struct bitmap *furniture; bool invert_selection; treeview_node *next; int height; int i; next = node->children; if (next != NULL) { /* down to children */ node = next; } else { /* No children. As long as we're not at the root, * go to next sibling if present, or nearest ancestor * with a next sibling. */ while (node != root && node->next_sib == NULL) { node = node->parent; } if (node == root) break; node = node->next_sib; } assert(node != NULL); assert(node != root); assert(node->type == TREE_NODE_FOLDER || node->type == TREE_NODE_ENTRY); if (node->type == TREE_NODE_FOLDER || !(node->flags & TV_NFLAGS_MATCHED)) { continue; } count++; inset = x + tree_g.window_padding; height = node->height; if ((render_y + height) < r->y0) { /* This node's line is above clip region */ render_y += height; continue; } style = (count & 0x1) ? &plot_style_odd : &plot_style_even; if (tree->drag.type == TV_DRAG_SELECTION && (render_y + height >= sel_min && render_y < sel_max)) { invert_selection = true; } else { invert_selection = false; } if ((node->flags & TV_NFLAGS_SELECTED && !invert_selection) || (!(node->flags & TV_NFLAGS_SELECTED) && invert_selection)) { bg_style = &style->sbg; text_style = &style->stext; infotext_style = &style->sitext; furniture = (node->flags & TV_NFLAGS_EXPANDED) ? style->furn[TREE_FURN_CONTRACT].sel : style->furn[TREE_FURN_EXPAND].sel; } else { bg_style = &style->bg; text_style = &style->text; infotext_style = &style->itext; furniture = (node->flags & TV_NFLAGS_EXPANDED) ? style->furn[TREE_FURN_CONTRACT].bmp : style->furn[TREE_FURN_EXPAND].bmp; } /* Render background */ rect.x0 = r->x0; rect.y0 = render_y; rect.x1 = r->x1; rect.y1 = render_y + height; ctx->plot->rectangle(ctx, bg_style, &rect); /* Render toggle */ ctx->plot->bitmap(ctx, furniture, inset, render_y + tree_g.line_height / 4, style->furn[TREE_FURN_EXPAND].size, style->furn[TREE_FURN_EXPAND].size, bg_style->fill_colour, BITMAPF_NONE); /* Render icon */ if (node->type == TREE_NODE_ENTRY) { res = TREE_RES_CONTENT; } else if (node->flags & TV_NFLAGS_SPECIAL) { res = TREE_RES_FOLDER_SPECIAL; } else { res = TREE_RES_FOLDER; } if (treeview_res[res].ready) { /* Icon resource is available */ data->x = inset + tree_g.step_width; data->y = render_y + ((tree_g.line_height - treeview_res[res].height + 1) / 2); data->background_colour = bg_style->fill_colour; content_redraw(treeview_res[res].c, data, r, ctx); } /* Render text */ x0 = inset + tree_g.step_width + tree_g.icon_step; ctx->plot->text(ctx, text_style, x0, render_y + baseline, node->text.data, node->text.len); /* Rendered the node */ render_y += tree_g.line_height; if (render_y > r->y1) { /* Passed the bottom of what's in the clip region. * Done. */ break; } if (node->type != TREE_NODE_ENTRY || !(node->flags & TV_NFLAGS_EXPANDED)) /* Done everything for this node */ continue; /* Render expanded entry fields */ entry = (struct treeview_node_entry *)node; for (i = 0; i < tree->n_fields - 1; i++) { struct treeview_field *ef = &(tree->fields[i + 1]); if (ef->flags & TREE_FLAG_SHOW_NAME) { int max_width = tree->field_width; ctx->plot->text(ctx, infotext_style, x0 + max_width - ef->value.width - tree_g.step_width, render_y + baseline, ef->value.data, ef->value.len); ctx->plot->text(ctx, infotext_style, x0 + max_width, render_y + baseline, entry->fields[i].value.data, entry->fields[i].value.len); } else { ctx->plot->text(ctx, infotext_style, x0, render_y + baseline, entry->fields[i].value.data, entry->fields[i].value.len); } /* Rendered the expanded entry field */ render_y += tree_g.line_height; } /* Finished rendering expanded entry */ if (render_y > r->y1) { /* Passed the bottom of what's in the clip region. * Done. */ break; } } *render_y_in_out = render_y; } /* Exported interface, documented in treeview.h */ void treeview_redraw(treeview *tree, const int x, const int y, struct rect *clip, const struct redraw_context *ctx) { struct redraw_context new_ctx = *ctx; struct content_redraw_data data; struct rect r; struct rect rect; int render_y = y; assert(tree != NULL); assert(tree->root != NULL); assert(tree->root->flags & TV_NFLAGS_EXPANDED); /* Start knockout rendering if it's available for this plotter */ if (ctx->plot->option_knockout) { knockout_plot_start(ctx, &new_ctx); } /* Set up clip rectangle */ r.x0 = clip->x0 + x; r.y0 = clip->y0 + y; r.x1 = clip->x1 + x; r.y1 = clip->y1 + y; new_ctx.plot->clip(&new_ctx, &r); /* Setup common content redraw data */ data.width = tree_g.icon_size; data.height = tree_g.icon_size; data.scale = 1; data.repeat_x = false; data.repeat_y = false; if (tree->flags & TREEVIEW_SEARCHABLE) { if (render_y < r.y1) { enum treeview_resource_id icon = TREE_RES_SEARCH; /* Fill the blank area at the bottom */ rect.x0 = r.x0; rect.y0 = render_y; rect.x1 = r.x1; rect.y1 = render_y + tree_g.line_height; new_ctx.plot->rectangle(&new_ctx, &plot_style_even.bg, &rect); if (treeview_res[icon].ready) { /* Icon resource is available */ data.x = tree_g.window_padding; data.y = render_y + ((tree_g.line_height - treeview_res[icon].height + 1) / 2); data.background_colour = plot_style_even.bg. fill_colour; content_redraw(treeview_res[icon].c, &data, &r, &new_ctx); } textarea_redraw(tree->search.textarea, x + tree_g.window_padding + tree_g.icon_step, y, plot_style_even.bg.fill_colour, 1.0, &r, &new_ctx); } render_y += tree_g.line_height; } /* Render the treeview data */ if (tree->search.search == true) { treeview_redraw_search(tree, x, y, &render_y, &r, &data, &new_ctx); } else { treeview_redraw_tree(tree, x, y, &render_y, &r, &data, &new_ctx); } if (render_y < r.y1) { /* Fill the blank area at the bottom */ rect.x0 = r.x0; rect.y0 = render_y; rect.x1 = r.x1; rect.y1 = r.y1; new_ctx.plot->rectangle(&new_ctx, &plot_style_even.bg, &rect); } /* All normal treeview rendering is done; render any overlays */ if ((tree->move.target_pos != TV_TARGET_NONE) && (treeview_res[TREE_RES_ARROW].ready)) { /* Got a MOVE drag; render move indicator arrow */ data.x = tree->move.target_area.x0 + x; data.y = tree->move.target_area.y0 + y; data.background_colour = plot_style_even.bg.fill_colour; content_redraw(treeview_res[TREE_RES_ARROW].c, &data, &r, &new_ctx); } else if (tree->edit.textarea != NULL) { /* Edit in progress; render textarea */ textarea_redraw(tree->edit.textarea, tree->edit.x + x, tree->edit.y + y, plot_style_even.bg.fill_colour, 1.0, &r, &new_ctx); } /* Rendering complete */ if (ctx->plot->option_knockout) { knockout_plot_end(ctx); } } /** * context for treeview selection */ struct treeview_selection_walk_data { enum { TREEVIEW_WALK_HAS_SELECTION, TREEVIEW_WALK_GET_FIRST_SELECTED, TREEVIEW_WALK_CLEAR_SELECTION, TREEVIEW_WALK_SELECT_ALL, TREEVIEW_WALK_COMMIT_SELECT_DRAG, TREEVIEW_WALK_DELETE_SELECTION, TREEVIEW_WALK_PROPAGATE_SELECTION, TREEVIEW_WALK_YANK_SELECTION, TREEVIEW_WALK_COPY_SELECTION } purpose; union { bool has_selection; struct { bool required; struct rect *rect; } redraw; struct { int sel_min; int sel_max; } drag; struct { treeview_node *prev; } yank; struct { treeview_node *n; } first; struct { char *text; uint32_t len; } copy; } data; int current_y; treeview *tree; }; /** * Treewalk node callback for handling selection related actions. * * \param n current node * \param ctx node selection context * \param skip_children flag to allow children to be skipped * \param end flag to allow iteration to be finished early. * \return NSERROR_OK on success else error code. */ static nserror treeview_node_selection_walk_cb(treeview_node *n, void *ctx, bool *skip_children, bool *end) { struct treeview_selection_walk_data *sw = ctx; int height; bool changed = false; nserror err; height = (n->type == TREE_NODE_ENTRY) ? n->height : tree_g.line_height; sw->current_y += height; switch (sw->purpose) { case TREEVIEW_WALK_HAS_SELECTION: if (n->flags & TV_NFLAGS_SELECTED) { sw->data.has_selection = true; *end = true; /* Can abort tree walk */ return NSERROR_OK; } break; case TREEVIEW_WALK_GET_FIRST_SELECTED: if (n->flags & TV_NFLAGS_SELECTED) { sw->data.first.n = n; *end = true; /* Can abort tree walk */ return NSERROR_OK; } break; case TREEVIEW_WALK_DELETE_SELECTION: if (n->flags & TV_NFLAGS_SELECTED) { err = treeview_delete_node_internal(sw->tree, n, true, TREE_OPTION_NONE); if (err != NSERROR_OK) { return err; } *skip_children = true; changed = true; } break; case TREEVIEW_WALK_PROPAGATE_SELECTION: if (n->parent != NULL && n->parent->flags & TV_NFLAGS_SELECTED && !(n->flags & TV_NFLAGS_SELECTED)) { n->flags ^= TV_NFLAGS_SELECTED; changed = true; } break; case TREEVIEW_WALK_CLEAR_SELECTION: if (n->flags & TV_NFLAGS_SELECTED) { n->flags ^= TV_NFLAGS_SELECTED; changed = true; } break; case TREEVIEW_WALK_SELECT_ALL: if (!(n->flags & TV_NFLAGS_SELECTED)) { n->flags ^= TV_NFLAGS_SELECTED; changed = true; } break; case TREEVIEW_WALK_COMMIT_SELECT_DRAG: if (sw->current_y >= sw->data.drag.sel_min && sw->current_y - height < sw->data.drag.sel_max) { n->flags ^= TV_NFLAGS_SELECTED; } return NSERROR_OK; case TREEVIEW_WALK_YANK_SELECTION: if (n->flags & TV_NFLAGS_SELECTED) { treeview_node *p = n->parent; int h = 0; if (treeview_unlink_node(n)) h = n->height; /* Reduce ancestor heights */ while (p != NULL && p->flags & TV_NFLAGS_EXPANDED) { p->height -= h; p = p->parent; } if (sw->data.yank.prev == NULL) { sw->tree->move.root = n; n->parent = NULL; n->prev_sib = NULL; n->next_sib = NULL; } else { n->parent = NULL; n->prev_sib = sw->data.yank.prev; n->next_sib = NULL; sw->data.yank.prev->next_sib = n; } sw->data.yank.prev = n; *skip_children = true; } break; case TREEVIEW_WALK_COPY_SELECTION: if (n->flags & TV_NFLAGS_SELECTED && n->type == TREE_NODE_ENTRY) { int i; char *temp; uint32_t len; const char *text; struct treeview_field *ef; struct treeview_text *val; for (i = 0; i < sw->tree->n_fields; i++) { ef = &(sw->tree->fields[i]); if (!(ef->flags & TREE_FLAG_COPY_TEXT)) { continue; } val = treeview_get_text_for_field(sw->tree, n, i); text = val->data; len = val->len; temp = realloc(sw->data.copy.text, sw->data.copy.len + len + 1); if (temp == NULL) { free(sw->data.copy.text); sw->data.copy.text = NULL; sw->data.copy.len = 0; return NSERROR_NOMEM; } if (sw->data.copy.len != 0) { temp[sw->data.copy.len - 1] = '\n'; } memcpy(temp + sw->data.copy.len, text, len); temp[sw->data.copy.len + len] = '\0'; sw->data.copy.len += len + 1; sw->data.copy.text = temp; } } break; } if (changed) { if (sw->data.redraw.required == false) { sw->data.redraw.required = true; sw->data.redraw.rect->y0 = sw->current_y - height; } if (sw->current_y > sw->data.redraw.rect->y1) { sw->data.redraw.rect->y1 = sw->current_y; } } return NSERROR_OK; } /* Exported interface, documented in treeview.h */ bool treeview_has_selection(treeview *tree) { struct treeview_selection_walk_data sw; sw.purpose = TREEVIEW_WALK_HAS_SELECTION; sw.data.has_selection = false; treeview_walk_internal(tree, tree->root, TREEVIEW_WALK_MODE_DISPLAY, NULL, treeview_node_selection_walk_cb, &sw); return sw.data.has_selection; } /** * Get first selected node (in any) * * \param tree Treeview object in which to create folder * \return the first selected treeview node, or NULL */ static treeview_node * treeview_get_first_selected(treeview *tree) { struct treeview_selection_walk_data sw; sw.purpose = TREEVIEW_WALK_GET_FIRST_SELECTED; sw.data.first.n = NULL; treeview_walk_internal(tree, tree->root, TREEVIEW_WALK_MODE_DISPLAY, NULL, treeview_node_selection_walk_cb, &sw); return sw.data.first.n; } /* Exported interface, documented in treeview.h */ enum treeview_node_type treeview_get_selection(treeview *tree, void **node_data) { treeview_node *n; assert(tree != NULL); n = treeview_get_first_selected(tree); if (n != NULL && n->type & (TREE_NODE_ENTRY | TREE_NODE_FOLDER)) { *node_data = n->client_data; return n->type; } *node_data = NULL; return TREE_NODE_NONE; } /** * Clear any selection in a treeview * * \param tree Treeview object to clear selection in * \param rect Redraw rectangle (if redraw required) * \return true iff redraw required */ static bool treeview_clear_selection(treeview *tree, struct rect *rect) { struct treeview_selection_walk_data sw; rect->x0 = 0; rect->y0 = 0; rect->x1 = REDRAW_MAX; rect->y1 = 0; sw.purpose = TREEVIEW_WALK_CLEAR_SELECTION; sw.data.redraw.required = false; sw.data.redraw.rect = rect; sw.current_y = (tree->flags & TREEVIEW_SEARCHABLE) ? tree_g.line_height : 0; treeview_walk_internal(tree, tree->root, TREEVIEW_WALK_MODE_DISPLAY, NULL, treeview_node_selection_walk_cb, &sw); return sw.data.redraw.required; } /** * Select all in a treeview * * \param tree Treeview object to select all in * \param rect Redraw rectangle (if redraw required) * \return true iff redraw required */ static bool treeview_select_all(treeview *tree, struct rect *rect) { struct treeview_selection_walk_data sw; rect->x0 = 0; rect->y0 = 0; rect->x1 = REDRAW_MAX; rect->y1 = 0; sw.purpose = TREEVIEW_WALK_SELECT_ALL; sw.data.redraw.required = false; sw.data.redraw.rect = rect; sw.current_y = (tree->flags & TREEVIEW_SEARCHABLE) ? tree_g.line_height : 0; treeview_walk_internal(tree, tree->root, TREEVIEW_WALK_MODE_DISPLAY, NULL, treeview_node_selection_walk_cb, &sw); return sw.data.redraw.required; } /** * Commit a current selection drag, modifying the node's selection state. * * \param tree Treeview object to commit drag selection in */ static void treeview_commit_selection_drag(treeview *tree) { struct treeview_selection_walk_data sw; sw.purpose = TREEVIEW_WALK_COMMIT_SELECT_DRAG; sw.current_y = (tree->flags & TREEVIEW_SEARCHABLE) ? tree_g.line_height : 0; if (tree->drag.start.y > tree->drag.prev.y) { sw.data.drag.sel_min = tree->drag.prev.y; sw.data.drag.sel_max = tree->drag.start.y; } else { sw.data.drag.sel_min = tree->drag.start.y; sw.data.drag.sel_max = tree->drag.prev.y; } treeview_walk_internal(tree, tree->root, TREEVIEW_WALK_MODE_DISPLAY, NULL, treeview_node_selection_walk_cb, &sw); } /** * Yank a selection to the node move list. * * \param tree Treeview object to yank selection from */ static void treeview_move_yank_selection(treeview *tree) { struct treeview_selection_walk_data sw; sw.purpose = TREEVIEW_WALK_YANK_SELECTION; sw.data.yank.prev = NULL; sw.tree = tree; treeview_walk_internal(tree, tree->root, TREEVIEW_WALK_MODE_DISPLAY, NULL, treeview_node_selection_walk_cb, &sw); } /** * Copy a selection to the clipboard. * * \param tree Treeview object to yank selection from */ static void treeview_copy_selection(treeview *tree) { struct treeview_selection_walk_data sw; nserror err; sw.purpose = TREEVIEW_WALK_COPY_SELECTION; sw.data.copy.text = NULL; sw.data.copy.len = 0; sw.tree = tree; err = treeview_walk_internal(tree, tree->root, TREEVIEW_WALK_MODE_DISPLAY, NULL, treeview_node_selection_walk_cb, &sw); if (err != NSERROR_OK) { return; } if (sw.data.copy.text != NULL) { guit->clipboard->set(sw.data.copy.text, sw.data.copy.len - 1, NULL, 0); free(sw.data.copy.text); } } /** * Delete a selection. * * \param tree Treeview object to delete selected nodes from * \param rect Updated to redraw rectangle * \return true iff redraw required. */ static bool treeview_delete_selection(treeview *tree, struct rect *rect) { struct treeview_selection_walk_data sw; assert(tree != NULL); assert(tree->root != NULL); rect->x0 = 0; rect->y0 = 0; rect->x1 = REDRAW_MAX; rect->y1 = tree->root->height; sw.purpose = TREEVIEW_WALK_DELETE_SELECTION; sw.data.redraw.required = false; sw.data.redraw.rect = rect; sw.current_y = 0; sw.tree = tree; treeview_walk_internal(tree, tree->root, TREEVIEW_WALK_MODE_DISPLAY, NULL, treeview_node_selection_walk_cb, &sw); return sw.data.redraw.required; } /** * Propagate selection to visible descendants of selected nodes. * * \param tree Treeview object to propagate selection in * \param rect Redraw rectangle (if redraw required) * \return true iff redraw required */ static bool treeview_propagate_selection(treeview *tree, struct rect *rect) { struct treeview_selection_walk_data sw; assert(tree != NULL); assert(tree->root != NULL); rect->x0 = 0; rect->y0 = 0; rect->x1 = REDRAW_MAX; rect->y1 = 0; sw.purpose = TREEVIEW_WALK_PROPAGATE_SELECTION; sw.data.redraw.required = false; sw.data.redraw.rect = rect; sw.current_y = 0; sw.tree = tree; treeview_walk_internal(tree, tree->root, TREEVIEW_WALK_MODE_DISPLAY, NULL, treeview_node_selection_walk_cb, &sw); return sw.data.redraw.required; } /** * Move a selection according to the current move drag. * * \param tree Treeview object to move selected nodes in * \param rect Redraw rectangle * \return NSERROR_OK on success else appropriate error code */ static nserror treeview_move_selection(treeview *tree, struct rect *rect) { treeview_node *node, *next, *parent; treeview_node *relation; enum treeview_relationship relationship; int height; assert(tree != NULL); assert(tree->root != NULL); assert(tree->root->children != NULL); assert(tree->move.target_pos != TV_TARGET_NONE); height = tree->root->height; /* Identify target location */ switch (tree->move.target_pos) { case TV_TARGET_ABOVE: if (tree->move.target == NULL) { /* Target: After last child of root */ relation = tree->root->children; while (relation->next_sib != NULL) { relation = relation->next_sib; } relationship = TREE_REL_NEXT_SIBLING; } else if (tree->move.target->prev_sib != NULL) { /* Target: After previous sibling */ relation = tree->move.target->prev_sib; relationship = TREE_REL_NEXT_SIBLING; } else { /* Target: Target: First child of parent */ assert(tree->move.target->parent != NULL); relation = tree->move.target->parent; relationship = TREE_REL_FIRST_CHILD; } break; case TV_TARGET_INSIDE: assert(tree->move.target != NULL); relation = tree->move.target; relationship = TREE_REL_FIRST_CHILD; break; case TV_TARGET_BELOW: assert(tree->move.target != NULL); relation = tree->move.target; relationship = TREE_REL_NEXT_SIBLING; break; default: NSLOG(netsurf, INFO, "Bad drop target for move."); return NSERROR_BAD_PARAMETER; } if (relationship == TREE_REL_FIRST_CHILD) { parent = relation; } else { parent = relation->parent; } /* The node that we're moving selection to can't itself be selected */ assert(!(relation->flags & TV_NFLAGS_SELECTED)); /* Move all selected nodes from treeview to tree->move.root */ treeview_move_yank_selection(tree); /* Move all nodes on tree->move.root to target location */ for (node = tree->move.root; node != NULL; node = next) { next = node->next_sib; if (!(parent->flags & TV_NFLAGS_EXPANDED)) { if (node->flags & TV_NFLAGS_EXPANDED) treeview_node_contract_internal(tree, node); node->flags &= ~TV_NFLAGS_SELECTED; } treeview_insert_node(tree, node, relation, relationship); relation = node; relationship = TREE_REL_NEXT_SIBLING; } tree->move.root = NULL; /* Tell window, if height has changed */ if (height != tree->root->height) treeview__cw_update_size(tree, -1, tree->root->height); /* TODO: Deal with redraw area properly */ rect->x0 = 0; rect->y0 = 0; rect->x1 = REDRAW_MAX; rect->y1 = REDRAW_MAX; return NSERROR_OK; } /** * context for treeview launch action */ struct treeview_launch_walk_data { int selected_depth; treeview *tree; }; /** * Treewalk node walk backward callback for tracking folder selection. */ static nserror treeview_node_launch_walk_bwd_cb(treeview_node *n, void *ctx, bool *end) { struct treeview_launch_walk_data *lw = ctx; if (n->type == TREE_NODE_FOLDER && n->flags == TV_NFLAGS_SELECTED) { lw->selected_depth--; } return NSERROR_OK; } /** * Treewalk node walk forward callback for launching nodes. * * \param n current node * \param ctx node launch context * \param skip_children flag to allow children to be skipped * \param end flag to allow iteration to be finished early. * \return NSERROR_OK on success else error code. */ static nserror treeview_node_launch_walk_fwd_cb(treeview_node *n, void *ctx, bool *skip_children, bool *end) { struct treeview_launch_walk_data *lw = ctx; nserror ret = NSERROR_OK; if (n->type == TREE_NODE_FOLDER && n->flags & TV_NFLAGS_SELECTED) { lw->selected_depth++; } else if (n->type == TREE_NODE_ENTRY && (n->flags & TV_NFLAGS_SELECTED || lw->selected_depth > 0)) { struct treeview_node_msg msg; msg.msg = TREE_MSG_NODE_LAUNCH; msg.data.node_launch.mouse = BROWSER_MOUSE_HOVER; ret = lw->tree->callbacks->entry(msg, n->client_data); } return ret; } /** * Launch a selection. * * \note Selected entries are launched. Entries that are descendants * of selected folders are also launched. * * \param tree Treeview object to launch selected nodes in * \return NSERROR_OK on success, appropriate error otherwise */ static nserror treeview_launch_selection(treeview *tree) { struct treeview_launch_walk_data lw; assert(tree != NULL); assert(tree->root != NULL); lw.selected_depth = 0; lw.tree = tree; return treeview_walk_internal(tree, tree->root, TREEVIEW_WALK_MODE_LOGICAL_COMPLETE, treeview_node_launch_walk_bwd_cb, treeview_node_launch_walk_fwd_cb, &lw); } /* Exported interface, documented in treeview.h */ nserror treeview_get_relation(treeview *tree, treeview_node **relation, enum treeview_relationship *rel, bool at_y, int y) { treeview_node *n; assert(tree != NULL); if (at_y) { n = treeview_y_node(tree, y); } else { n = treeview_get_first_selected(tree); } if (n != NULL && n->parent != NULL) { if (n == n->parent->children) { /* First child */ *relation = n->parent; *rel = TREE_REL_FIRST_CHILD; } else { /* Not first child */ *relation = n->prev_sib; *rel = TREE_REL_NEXT_SIBLING; } } else { if (tree->root->children == NULL) { /* First child of root */ *relation = tree->root; *rel = TREE_REL_FIRST_CHILD; } else { /* Last child of root */ n = tree->root->children; while (n->next_sib != NULL) n = n->next_sib; *relation = n; *rel = TREE_REL_NEXT_SIBLING; } } return NSERROR_OK; } /** * context for treeview keyboard action */ struct treeview_nav_state { treeview *tree; treeview_node *prev; treeview_node *curr; treeview_node *next; treeview_node *last; int n_selected; int prev_n_selected; }; /** * Treewalk node callback for handling mouse action. * * \param node current node * \param ctx node context * \param skip_children flag to allow children to be skipped * \param end flag to allow iteration to be finished early. * \return NSERROR_OK on success else error code. */ static nserror treeview_node_nav_cb(treeview_node *node, void *ctx, bool *skip_children, bool *end) { struct treeview_nav_state *ns = ctx; if (node == ns->tree->root) return NSERROR_OK; if (node->flags & TV_NFLAGS_SELECTED) { ns->n_selected++; if (ns->curr == NULL) { ns->curr = node; } } else { if (ns->n_selected == 0) { ns->prev = node; } else if (ns->prev_n_selected < ns->n_selected) { ns->next = node; ns->prev_n_selected = ns->n_selected; } } ns->last = node; return NSERROR_OK; } /** * Handle keyboard navigation. * * \note Selected entries are launched. * Entries that are descendants of selected folders are also launched. * * \param tree Treeview object to launch selected nodes in * \param key The ucs4 character codepoint * \param rect Updated to redraw rectangle * \return true if treeview needs redraw, false otherwise */ static bool treeview_keyboard_navigation(treeview *tree, uint32_t key, struct rect *rect) { struct treeview_nav_state ns = { .tree = tree, .prev = NULL, .curr = NULL, .next = NULL, .last = NULL, .n_selected = 0, .prev_n_selected = 0 }; int h = tree->root->height; bool redraw = false; /* Fill out the nav. state struct, by examining the current selection * state */ treeview_walk_internal(tree, tree->root, TREEVIEW_WALK_MODE_DISPLAY, NULL, treeview_node_nav_cb, &ns); if (tree->search.search == false) { if (ns.next == NULL) ns.next = tree->root->children; if (ns.prev == NULL) ns.prev = ns.last; } /* Clear any existing selection */ redraw = treeview_clear_selection(tree, rect); switch (key) { case NS_KEY_LEFT: if (tree->search.search == true) { break; } if (ns.curr != NULL && ns.curr->parent != NULL && ns.curr->parent->type != TREE_NODE_ROOT) { /* Step to parent */ ns.curr->parent->flags |= TV_NFLAGS_SELECTED; } else if (ns.curr != NULL && tree->root->children != NULL) { /* Select first node in tree */ tree->root->children->flags |= TV_NFLAGS_SELECTED; } break; case NS_KEY_RIGHT: if (ns.curr != NULL) { if (!(ns.curr->flags & TV_NFLAGS_EXPANDED)) { /* Toggle node to expanded */ treeview_node_expand_internal(tree, ns.curr); if (ns.curr->children != NULL) { /* Step to first child */ ns.curr->children->flags |= TV_NFLAGS_SELECTED; } else { /* Retain current node selection */ ns.curr->flags |= TV_NFLAGS_SELECTED; } } else { /* Toggle node to contracted */ treeview_node_contract_internal(tree, ns.curr); /* Retain current node selection */ ns.curr->flags |= TV_NFLAGS_SELECTED; } } else if (ns.curr != NULL) { /* Retain current node selection */ ns.curr->flags |= TV_NFLAGS_SELECTED; } break; case NS_KEY_UP: if (ns.prev != NULL) { /* Step to previous node */ ns.prev->flags |= TV_NFLAGS_SELECTED; } break; case NS_KEY_DOWN: if (ns.next != NULL) { /* Step to next node */ ns.next->flags |= TV_NFLAGS_SELECTED; } break; default: break; } /* TODO: Deal with redraw area properly */ rect->x0 = 0; rect->y0 = 0; rect->x1 = REDRAW_MAX; if (tree->root->height > h) rect->y1 = tree->root->height; else rect->y1 = h; redraw = true; return redraw; } /* Exported interface, documented in treeview.h */ bool treeview_keypress(treeview *tree, uint32_t key) { struct rect r; /**< Redraw rectangle */ bool redraw = false; assert(tree != NULL); /* Pass to any textarea, if editing in progress */ if (tree->edit.textarea != NULL) { switch (key) { case NS_KEY_ESCAPE: treeview_edit_cancel(tree, true); return true; case NS_KEY_NL: case NS_KEY_CR: treeview_edit_done(tree); return true; default: return textarea_keypress(tree->edit.textarea, key); } } else if (tree->search.active == true) { switch (key) { case NS_KEY_ESCAPE: treeview__search_cancel(tree, false); return true; case NS_KEY_NL: case NS_KEY_CR: return true; default: return textarea_keypress(tree->search.textarea, key); } } /* Keypress to be handled by treeview */ switch (key) { case NS_KEY_SELECT_ALL: redraw = treeview_select_all(tree, &r); break; case NS_KEY_COPY_SELECTION: treeview_copy_selection(tree); break; case NS_KEY_DELETE_LEFT: case NS_KEY_DELETE_RIGHT: redraw = treeview_delete_selection(tree, &r); if (tree->flags & TREEVIEW_DEL_EMPTY_DIRS) { int h = tree->root->height; /* Delete any empty nodes */ treeview_delete_empty_nodes(tree, false); /* Inform front end of change in dimensions */ if (tree->root->height != h) { r.y0 = 0; treeview__cw_update_size(tree, -1, tree->root->height); } } break; case NS_KEY_CR: case NS_KEY_NL: treeview_launch_selection(tree); break; case NS_KEY_ESCAPE: case NS_KEY_CLEAR_SELECTION: redraw = treeview_clear_selection(tree, &r); break; case NS_KEY_LEFT: case NS_KEY_RIGHT: case NS_KEY_UP: case NS_KEY_DOWN: redraw = treeview_keyboard_navigation(tree, key, &r); break; default: return false; } if (redraw) { treeview__cw_invalidate_area(tree, &r); } return true; } /** * Set the drag&drop drop indicator * * \param tree Treeview object to set node indicator in * \param need_redraw True iff we already have a redraw region * \param target The treeview node with mouse pointer over it * \param node_height The height of node * \param node_y The Y coord of the top of target node * \param mouse_y Y coord of mouse position * \param rect Redraw rectangle (if redraw required) * \return true iff redraw required */ static bool treeview_set_move_indicator(treeview *tree, bool need_redraw, treeview_node *target, int node_height, int node_y, int mouse_y, struct rect *rect) { treeview_node *orig = target; enum treeview_target_pos target_pos; int mouse_pos = mouse_y - node_y; int x; assert(tree != NULL); assert(tree->root != NULL); assert(tree->root->children != NULL); assert(target != NULL); if (target->flags & TV_NFLAGS_SELECTED) { /* Find top selected ancestor */ while (target->parent && target->parent->flags & TV_NFLAGS_SELECTED) { target = target->parent; } /* Find top adjacent selected sibling */ while (target->prev_sib && target->prev_sib->flags & TV_NFLAGS_SELECTED) { target = target->prev_sib; } target_pos = TV_TARGET_ABOVE; } else switch (target->type) { case TREE_NODE_FOLDER: if (mouse_pos <= node_height / 4) { target_pos = TV_TARGET_ABOVE; } else if (mouse_pos <= (3 * node_height) / 4 || target->flags & TV_NFLAGS_EXPANDED) { target_pos = TV_TARGET_INSIDE; } else { target_pos = TV_TARGET_BELOW; } break; case TREE_NODE_ENTRY: if (mouse_pos <= node_height / 2) { target_pos = TV_TARGET_ABOVE; } else { target_pos = TV_TARGET_BELOW; } break; default: assert(target->type != TREE_NODE_ROOT); return false; } if (target_pos == tree->move.target_pos && target == tree->move.target) { /* No change */ return need_redraw; } if (tree->move.target_pos != TV_TARGET_NONE) { /* Need to clear old indicator position */ if (need_redraw) { if (rect->x0 > tree->move.target_area.x0) rect->x0 = tree->move.target_area.x0; if (tree->move.target_area.x1 > rect->x1) rect->x1 = tree->move.target_area.x1; if (rect->y0 > tree->move.target_area.y0) rect->y0 = tree->move.target_area.y0; if (tree->move.target_area.y1 > rect->y1) rect->y1 = tree->move.target_area.y1; } else { *rect = tree->move.target_area; need_redraw = true; } } /* Offset for ABOVE / BELOW */ if (target_pos == TV_TARGET_ABOVE) { if (target != orig) { node_y = treeview_node_y(tree, target); } node_y -= (tree_g.line_height + 1) / 2; } else if (target_pos == TV_TARGET_BELOW) { node_y += node_height - (tree_g.line_height + 1) / 2; } /* Oftsets are all relative to centred (INSIDE) */ node_y += (tree_g.line_height - treeview_res[TREE_RES_ARROW].height + 1) / 2; x = target->inset + tree_g.move_offset; /* Update target details */ tree->move.target = target; tree->move.target_pos = target_pos; tree->move.target_area.x0 = x; tree->move.target_area.y0 = node_y; tree->move.target_area.x1 = tree_g.icon_size + x; tree->move.target_area.y1 = tree_g.icon_size + node_y; if (target_pos != TV_TARGET_NONE) { /* Need to draw new indicator position */ if (need_redraw) { if (rect->x0 > tree->move.target_area.x0) rect->x0 = tree->move.target_area.x0; if (tree->move.target_area.x1 > rect->x1) rect->x1 = tree->move.target_area.x1; if (rect->y0 > tree->move.target_area.y0) rect->y0 = tree->move.target_area.y0; if (tree->move.target_area.y1 > rect->y1) rect->y1 = tree->move.target_area.y1; } else { *rect = tree->move.target_area; need_redraw = true; } } return need_redraw; } /** * Callback for textarea_create, in desktop/treeview.h * * \param data treeview context * \param msg textarea message */ static void treeview_textarea_callback(void *data, struct textarea_msg *msg) { treeview *tree = data; struct rect *r; switch (msg->type) { case TEXTAREA_MSG_DRAG_REPORT: if (msg->data.drag == TEXTAREA_DRAG_NONE) { /* Textarea drag finished */ tree->drag.type = TV_DRAG_NONE; } else { /* Textarea drag started */ tree->drag.type = TV_DRAG_TEXTAREA; } treeview__cw_drag_status(tree, tree->drag.type); break; case TEXTAREA_MSG_REDRAW_REQUEST: r = &msg->data.redraw; r->x0 += tree->edit.x; r->y0 += tree->edit.y; r->x1 += tree->edit.x; r->y1 += tree->edit.y; /* Redraw the textarea */ treeview__cw_invalidate_area(tree, r); break; default: break; } } /** * Start edit of node field, at given y-coord, if editable * * \param tree Treeview object to consider editing in * \param n The treeview node to try editing * \param node_y The Y coord of the top of n * \param mouse_x X coord of mouse position * \param mouse_y Y coord of mouse position * \param rect Redraw rectangle (if redraw required) * \return true iff redraw required */ static bool treeview_edit_node_at_point(treeview *tree, treeview_node *n, int node_y, int mouse_x, int mouse_y, struct rect *rect) { struct treeview_text *field_data = NULL; struct treeview_field *ef, *field_desc = NULL; int pos = node_y + tree_g.line_height; int field_y = node_y; int field_x; int width, height; bool success; /* If the main field is editable, make field_data point to it */ if (n->type == TREE_NODE_ENTRY) ef = &(tree->fields[0]); else ef = &(tree->fields[tree->n_fields]); if (ef->flags & TREE_FLAG_ALLOW_EDIT) { field_data = &n->text; field_desc = ef; field_y = node_y; } /* Check for editable entry fields */ if (n->type == TREE_NODE_ENTRY && n->height != tree_g.line_height) { struct treeview_node_entry *e = (struct treeview_node_entry *)n; int i; for (i = 0; i < tree->n_fields - 1; i++) { if (mouse_y <= pos) continue; ef = &(tree->fields[i + 1]); pos += tree_g.line_height; if (mouse_y <= pos && (ef->flags & TREE_FLAG_ALLOW_EDIT)) { field_data = &e->fields[i].value; field_desc = ef; field_y = pos - tree_g.line_height; } } } if (field_data == NULL || field_desc == NULL) { /* No editable field */ return false; } /* Get window width/height */ treeview__cw_get_window_dimensions(tree, &width, &height); /* Calculate textarea width/height */ field_x = n->inset + tree_g.step_width + tree_g.icon_step - 3; width -= field_x; height = tree_g.line_height; /* Create text area */ tree->edit.textarea = treeview__create_textarea(tree, width, height, 0x000000, 0xffffff, 0x000000, plot_style_odd.text, treeview_textarea_callback); if (tree->edit.textarea == NULL) { return false; } success = textarea_set_text(tree->edit.textarea, field_data->data); if (!success) { textarea_destroy(tree->edit.textarea); return false; } tree->edit.node = n; tree->edit.field = field_desc->field; /* Position the caret */ mouse_x -= field_x; if (mouse_x < 0) mouse_x = 0; else if (mouse_x >= width) mouse_x = width - 1; textarea_mouse_action(tree->edit.textarea, BROWSER_MOUSE_PRESS_1 | BROWSER_MOUSE_CLICK_1, mouse_x, tree_g.line_height / 2); /* Position the textarea */ tree->edit.x = field_x; tree->edit.y = field_y; tree->edit.w = width; tree->edit.h = height; /* Setup redraw rectangle */ if (rect->x0 > field_x) rect->x0 = field_x; if (rect->y0 > field_y) rect->y0 = field_y; if (rect->x1 < field_x + width) rect->x1 = field_x + width; if (rect->y1 < field_y + height) rect->y1 = field_y + height; return true; } /* Exported interface, documented in treeview.h */ void treeview_edit_selection(treeview *tree) { struct rect rect; treeview_node *n; bool redraw; int y; assert(tree != NULL); assert(tree->root != NULL); /* Get first selected node */ n = treeview_get_first_selected(tree); if (n == NULL) return; /* Get node's y-position */ y = treeview_node_y(tree, n); /* Edit node at y */ redraw = treeview_edit_node_at_point(tree, n, y, 0, y + tree_g.line_height / 2, &rect); if (redraw == false) return; /* Redraw */ rect.x0 = 0; rect.y0 = y; rect.x1 = REDRAW_MAX; rect.y1 = y + tree_g.line_height; treeview__cw_invalidate_area(tree, &rect); } /** * context for treeview mouse handling */ struct treeview_mouse_action { treeview *tree; browser_mouse_state mouse; int x; int y; int current_y; /* Y coordinate value of top of current node */ }; /** * Treewalk node callback for handling mouse action. * * \param node current node * \param ctx node context * \param skip_children flag to allow children to be skipped * \param end flag to allow iteration to be finished early. * \return NSERROR_OK on success else error code. */ static nserror treeview_node_mouse_action_cb(treeview_node *node, void *ctx, bool *skip_children, bool *end) { struct treeview_mouse_action *ma = ctx; struct rect r; bool redraw = false; bool click; int height; enum { TV_NODE_ACTION_NONE = 0, TV_NODE_ACTION_SELECTION = (1 << 0) } action = TV_NODE_ACTION_NONE; enum treeview_node_part part = TV_NODE_PART_NONE; nserror err; r.x0 = 0; r.x1 = REDRAW_MAX; height = (node->type == TREE_NODE_ENTRY) ? node->height : tree_g.line_height; /* Skip line if we've not reached mouse y */ if (ma->y > ma->current_y + height) { ma->current_y += height; return NSERROR_OK; /* Don't want to abort tree walk */ } /* Find where the mouse is */ if (ma->y <= ma->current_y + tree_g.line_height) { int inset = node->inset; if (ma->tree->search.search == true) { inset = tree_g.window_padding; } if (ma->x >= inset - 1 && ma->x < inset + tree_g.step_width) { /* Over expansion toggle */ part = TV_NODE_PART_TOGGLE; } else if (ma->x >= inset + tree_g.step_width && ma->x < inset + tree_g.step_width + tree_g.icon_step + node->text.width) { /* On node */ part = TV_NODE_PART_ON_NODE; } } else if (node->type == TREE_NODE_ENTRY && height > tree_g.line_height) { /* Expanded entries */ int x = node->inset + tree_g.step_width + tree_g.icon_step; int y = ma->current_y + tree_g.line_height; int i; struct treeview_node_entry *entry = (struct treeview_node_entry *)node; for (i = 0; i < ma->tree->n_fields - 1; i++) { struct treeview_field *ef = &(ma->tree->fields[i + 1]); if (ma->y > y + tree_g.line_height) { y += tree_g.line_height; continue; } if (ef->flags & TREE_FLAG_SHOW_NAME) { int max_width = ma->tree->field_width; if (ma->x >= x + max_width - ef->value.width - tree_g.step_width && ma->x < x + max_width - tree_g.step_width) { /* On a field name */ part = TV_NODE_PART_ON_NODE; } else if (ma->x >= x + max_width && ma->x < x + max_width + entry->fields[i].value.width) { /* On a field value */ part = TV_NODE_PART_ON_NODE; } } else { if (ma->x >= x && ma->x < x + entry->fields[i].value.width) { /* On a field value */ part = TV_NODE_PART_ON_NODE; } } break; } } /* Record what position / part a drag started on */ if (ma->mouse & (BROWSER_MOUSE_PRESS_1 | BROWSER_MOUSE_PRESS_2) && ma->tree->drag.type == TV_DRAG_NONE) { ma->tree->drag.selected = node->flags & TV_NFLAGS_SELECTED; ma->tree->drag.start_node = node; ma->tree->drag.part = part; ma->tree->drag.start.x = ma->x; ma->tree->drag.start.y = ma->y; ma->tree->drag.start.node_y = ma->current_y; ma->tree->drag.start.node_h = height; ma->tree->drag.prev.x = ma->x; ma->tree->drag.prev.y = ma->y; ma->tree->drag.prev.node_y = ma->current_y; ma->tree->drag.prev.node_h = height; } /* Handle drag start */ if (ma->tree->drag.type == TV_DRAG_NONE) { if (ma->mouse & BROWSER_MOUSE_DRAG_1 && ma->tree->drag.selected == false && ma->tree->drag.part == TV_NODE_PART_NONE) { ma->tree->drag.type = TV_DRAG_SELECTION; treeview__cw_drag_status(ma->tree, CORE_WINDOW_DRAG_SELECTION); } else if (ma->tree->search.search == false && !(ma->tree->flags & TREEVIEW_NO_MOVES) && ma->mouse & BROWSER_MOUSE_DRAG_1 && (ma->tree->drag.selected == true || ma->tree->drag.part == TV_NODE_PART_ON_NODE)) { ma->tree->drag.type = TV_DRAG_MOVE; treeview__cw_drag_status(ma->tree, CORE_WINDOW_DRAG_MOVE); redraw |= treeview_propagate_selection(ma->tree, &r); } else if (ma->mouse & BROWSER_MOUSE_DRAG_2) { ma->tree->drag.type = TV_DRAG_SELECTION; treeview__cw_drag_status(ma->tree, CORE_WINDOW_DRAG_SELECTION); } if (ma->tree->drag.start_node != NULL && ma->tree->drag.type == TV_DRAG_SELECTION) { ma->tree->drag.start_node->flags ^= TV_NFLAGS_SELECTED; } } /* Handle active drags */ switch (ma->tree->drag.type) { case TV_DRAG_SELECTION: { int curr_y1 = ma->current_y + height; int prev_y1 = ma->tree->drag.prev.node_y + ma->tree->drag.prev.node_h; r.y0 = (ma->current_y < ma->tree->drag.prev.node_y) ? ma->current_y : ma->tree->drag.prev.node_y; r.y1 = (curr_y1 > prev_y1) ? curr_y1 : prev_y1; redraw = true; ma->tree->drag.prev.x = ma->x; ma->tree->drag.prev.y = ma->y; ma->tree->drag.prev.node_y = ma->current_y; ma->tree->drag.prev.node_h = height; } break; case TV_DRAG_MOVE: redraw |= treeview_set_move_indicator(ma->tree, redraw, node, height, ma->current_y, ma->y, &r); break; default: break; } click = ma->mouse & (BROWSER_MOUSE_CLICK_1 | BROWSER_MOUSE_CLICK_2); if (((node->type == TREE_NODE_FOLDER) && (ma->mouse & BROWSER_MOUSE_DOUBLE_CLICK) && click) || (part == TV_NODE_PART_TOGGLE && click)) { int h = ma->tree->root->height; /* Clear any existing selection */ redraw |= treeview_clear_selection(ma->tree, &r); /* Toggle node expansion */ if (node->flags & TV_NFLAGS_EXPANDED) { err = treeview_node_contract_internal(ma->tree, node); } else { err = treeview_node_expand_internal(ma->tree, node); } if (err != NSERROR_OK) { return err; } /* Set up redraw */ if (!redraw || r.y0 > ma->current_y) r.y0 = ma->current_y; r.y1 = h > ma->tree->root->height ? h : ma->tree->root->height; redraw = true; } else if ((node->type == TREE_NODE_ENTRY) && (ma->mouse & BROWSER_MOUSE_DOUBLE_CLICK) && click) { struct treeview_node_msg msg; msg.msg = TREE_MSG_NODE_LAUNCH; msg.data.node_launch.mouse = ma->mouse; /* Clear any existing selection */ redraw |= treeview_clear_selection(ma->tree, &r); /* Tell client an entry was launched */ ma->tree->callbacks->entry(msg, node->client_data); } else if (ma->mouse & BROWSER_MOUSE_PRESS_2 || (ma->mouse & BROWSER_MOUSE_PRESS_1 && ma->mouse & BROWSER_MOUSE_MOD_2)) { /* Toggle selection of node */ action |= TV_NODE_ACTION_SELECTION; } else if (ma->mouse & BROWSER_MOUSE_CLICK_1 && ma->mouse & (BROWSER_MOUSE_MOD_1 | BROWSER_MOUSE_MOD_3) && part != TV_NODE_PART_TOGGLE) { /* Clear any existing selection */ redraw |= treeview_clear_selection(ma->tree, &r); /* Edit node */ redraw |= treeview_edit_node_at_point(ma->tree, node, ma->current_y, ma->x, ma->y, &r); } else if (ma->mouse & BROWSER_MOUSE_PRESS_1 && !(ma->mouse & (BROWSER_MOUSE_MOD_1 | BROWSER_MOUSE_MOD_3)) && !(node->flags & TV_NFLAGS_SELECTED) && part != TV_NODE_PART_TOGGLE) { /* Clear any existing selection */ redraw |= treeview_clear_selection(ma->tree, &r); /* Select node */ action |= TV_NODE_ACTION_SELECTION; } if (action & TV_NODE_ACTION_SELECTION) { /* Handle change in selection */ node->flags ^= TV_NFLAGS_SELECTED; /* Redraw */ if (!redraw) { r.y0 = ma->current_y; r.y1 = ma->current_y + height; redraw = true; } else { if (r.y0 > ma->current_y) { r.y0 = ma->current_y; } if (r.y1 < ma->current_y + height) { r.y1 = ma->current_y + height; } } } if (redraw) { treeview__cw_invalidate_area(ma->tree, &r); } *end = true; /* Reached line with click; stop walking tree */ return NSERROR_OK; } /* Exported interface, documented in treeview.h */ void treeview_mouse_action(treeview *tree, browser_mouse_state mouse, int x, int y) { struct rect r; bool redraw = false; int search_height = (tree->flags & TREEVIEW_SEARCHABLE) ? tree_g.line_height : 0; assert(tree != NULL); assert(tree->root != NULL); /* Not interested in whether mouse leaves window. */ if (mouse == BROWSER_MOUSE_LEAVE) { return; } /* Handle mouse drag captured by textarea */ if (tree->drag.type == TV_DRAG_TEXTAREA) { textarea_mouse_action(tree->edit.textarea, mouse, x - tree->edit.x, y - tree->edit.y); return; } else if (tree->drag.type == TV_DRAG_SEARCH || (y < search_height && tree->drag.type == TV_DRAG_NONE)) { if (tree->search.active == false) { tree->search.active = true; if (treeview_clear_selection(tree, &r)) { treeview__cw_invalidate_area(tree, &r); } } textarea_mouse_action(tree->search.textarea, mouse, x - tree_g.window_padding - tree_g.icon_size, y); return; } else if (mouse & (BROWSER_MOUSE_PRESS_1 | BROWSER_MOUSE_PRESS_2) && tree->search.active == true) { tree->search.active = false; textarea_set_caret(tree->search.textarea, -1); r.x0 = 0; r.y0 = 0; r.x1 = REDRAW_MAX; r.y1 = tree_g.line_height; treeview__cw_invalidate_area(tree, &r); } /* Handle textarea related mouse action */ if (tree->edit.textarea != NULL) { int ta_x = x - tree->edit.x; int ta_y = y - tree->edit.y; if (ta_x > 0 && ta_x < tree->edit.w && ta_y > 0 && ta_y < tree->edit.h) { /* Inside textarea */ textarea_mouse_action(tree->edit.textarea, mouse, ta_x, ta_y); return; } else if (mouse != BROWSER_MOUSE_HOVER) { /* Action outside textarea */ treeview_edit_cancel(tree, true); } } /* Handle drag ends */ if (mouse == BROWSER_MOUSE_HOVER) { switch (tree->drag.type) { case TV_DRAG_SELECTION: treeview_commit_selection_drag(tree); tree->drag.type = TV_DRAG_NONE; tree->drag.start_node = NULL; treeview__cw_drag_status(tree, CORE_WINDOW_DRAG_NONE); return; case TV_DRAG_MOVE: treeview_move_selection(tree, &r); tree->drag.type = TV_DRAG_NONE; tree->drag.start_node = NULL; tree->move.target = NULL; tree->move.target_pos = TV_TARGET_NONE; treeview__cw_drag_status(tree, CORE_WINDOW_DRAG_NONE); treeview__cw_invalidate_area(tree, &r); return; default: /* No drag to end */ break; } } if (y > treeview__get_display_height(tree) + search_height) { /* Below tree */ r.x0 = 0; r.x1 = REDRAW_MAX; /* Record what position / part a drag started on */ if (mouse & (BROWSER_MOUSE_PRESS_1 | BROWSER_MOUSE_PRESS_2) && tree->drag.type == TV_DRAG_NONE) { tree->drag.selected = false; tree->drag.start_node = NULL; tree->drag.part = TV_NODE_PART_NONE; tree->drag.start.x = x; tree->drag.start.y = y; tree->drag.start.node_y = tree->root->height; tree->drag.start.node_h = 0; tree->drag.prev.x = x; tree->drag.prev.y = y; tree->drag.prev.node_y = tree->root->height; tree->drag.prev.node_h = 0; } /* Handle drag start */ if (tree->drag.type == TV_DRAG_NONE) { if (mouse & BROWSER_MOUSE_DRAG_1 && tree->drag.selected == false && tree->drag.part == TV_NODE_PART_NONE) { tree->drag.type = TV_DRAG_SELECTION; treeview__cw_drag_status(tree, CORE_WINDOW_DRAG_SELECTION); } else if (mouse & BROWSER_MOUSE_DRAG_2) { tree->drag.type = TV_DRAG_SELECTION; treeview__cw_drag_status(tree, CORE_WINDOW_DRAG_SELECTION); } if (tree->drag.start_node != NULL && tree->drag.type == TV_DRAG_SELECTION) { tree->drag.start_node->flags ^= TV_NFLAGS_SELECTED; } } /* Handle selection drags */ if (tree->drag.type == TV_DRAG_SELECTION) { int curr_y1 = tree->root->height; int prev_y1 = tree->drag.prev.node_y + tree->drag.prev.node_h; r.y0 = tree->drag.prev.node_y; r.y1 = (curr_y1 > prev_y1) ? curr_y1 : prev_y1; redraw = true; tree->drag.prev.x = x; tree->drag.prev.y = y; tree->drag.prev.node_y = curr_y1; tree->drag.prev.node_h = 0; } if (mouse & BROWSER_MOUSE_PRESS_1) { /* Clear any existing selection */ redraw |= treeview_clear_selection(tree, &r); } if (redraw) { treeview__cw_invalidate_area(tree, &r); } } else { /* On tree */ struct treeview_mouse_action ma; ma.tree = tree; ma.mouse = mouse; ma.x = x; ma.y = y; ma.current_y = search_height; treeview_walk_internal(tree, tree->root, TREEVIEW_WALK_MODE_DISPLAY, NULL, treeview_node_mouse_action_cb, &ma); } } /* Exported interface, documented in treeview.h */ int treeview_get_height(treeview *tree) { int search_height = (tree->flags & TREEVIEW_SEARCHABLE) ? tree_g.line_height : 0; int height = treeview__get_display_height(tree); assert(tree != NULL); assert(tree->root != NULL); treeview__cw_update_size(tree, -1, height); return height + search_height; } /** * Initialise the plot styles from CSS system colour values. * * \param font_pt_size font size to use * \return NSERROR_OK on success else appropriate error code */ static nserror treeview_init_plot_styles(int font_pt_size) { nserror res; /* Background colour */ plot_style_even.bg.stroke_type = PLOT_OP_TYPE_NONE; plot_style_even.bg.stroke_width = 0; plot_style_even.bg.stroke_colour = 0; plot_style_even.bg.fill_type = PLOT_OP_TYPE_SOLID; res = ns_system_colour_char("Window", &plot_style_even.bg.fill_colour); if (res != NSERROR_OK) { return res; } /* Text colour */ plot_style_even.text.family = PLOT_FONT_FAMILY_SANS_SERIF; plot_style_even.text.size = font_pt_size; plot_style_even.text.weight = 400; plot_style_even.text.flags = FONTF_NONE; res = ns_system_colour_char("WindowText", &plot_style_even.text.foreground); if (res != NSERROR_OK) { return res; } res = ns_system_colour_char("Window", &plot_style_even.text.background); if (res != NSERROR_OK) { return res; } /* Entry field text colour */ plot_style_even.itext = plot_style_even.text; plot_style_even.itext.foreground = mix_colour( plot_style_even.text.foreground, plot_style_even.text.background, 255 * 10 / 16); /* Selected background colour */ plot_style_even.sbg = plot_style_even.bg; res = ns_system_colour_char("Highlight", &plot_style_even.sbg.fill_colour); if (res != NSERROR_OK) { return res; } /* Selected text colour */ plot_style_even.stext = plot_style_even.text; res = ns_system_colour_char("HighlightText", &plot_style_even.stext.foreground); if (res != NSERROR_OK) { return res; } res = ns_system_colour_char("Highlight", &plot_style_even.stext.background); if (res != NSERROR_OK) { return res; } /* Selected entry field text colour */ plot_style_even.sitext = plot_style_even.stext; plot_style_even.sitext.foreground = mix_colour( plot_style_even.stext.foreground, plot_style_even.stext.background, 255 * 25 / 32); /* Odd numbered node styles */ plot_style_odd.bg = plot_style_even.bg; plot_style_odd.bg.fill_colour = mix_colour( plot_style_even.bg.fill_colour, plot_style_even.text.foreground, 255 * 15 / 16); plot_style_odd.text = plot_style_even.text; plot_style_odd.text.background = plot_style_odd.bg.fill_colour; plot_style_odd.itext = plot_style_odd.text; plot_style_odd.itext.foreground = mix_colour( plot_style_odd.text.foreground, plot_style_odd.text.background, 255 * 10 / 16); plot_style_odd.sbg = plot_style_even.sbg; plot_style_odd.stext = plot_style_even.stext; plot_style_odd.sitext = plot_style_even.sitext; return NSERROR_OK; } /** * Callback for hlcache retrieving resources. * * \param handle content hlcache handle * \param event The event that occurred on the content * \param pw treeview resource context */ static nserror treeview_res_cb(struct hlcache_handle *handle, const hlcache_event *event, void *pw) { struct treeview_resource *r = pw; switch (event->type) { case CONTENT_MSG_READY: case CONTENT_MSG_DONE: r->ready = true; r->height = content_get_height(handle); break; default: break; } return NSERROR_OK; } /** * Fetch content resources used by treeview. */ static void treeview_init_resources(void) { int i; for (i = 0; i < TREE_RES_LAST; i++) { nsurl *url; treeview_res[i].ready = false; treeview_res[i].height = 0; if (nsurl_create(treeview_res[i].url, &url) == NSERROR_OK) { hlcache_handle_retrieve(url, 0, NULL, NULL, treeview_res_cb, &(treeview_res[i]), NULL, CONTENT_IMAGE, &(treeview_res[i].c)); nsurl_unref(url); } } } /** * Create a right-pointing anti-aliased triangle bitmap * * \param bg background colour * \param fg foreground colour * \param size required bitmap size */ static struct bitmap * treeview_generate_triangle_bitmap(colour bg, colour fg, int size) { struct bitmap *b = NULL; int x, y; unsigned char *rpos; unsigned char *pos; size_t stride; /* Set up required colour graduations. Ignores screen gamma. */ colour colour0 = bg; colour colour1 = mix_colour(bg, fg, 255 * 3 / 4); colour colour2 = blend_colour(bg, fg); colour colour3 = mix_colour(bg, fg, 255 * 1 / 4); colour colour4 = fg; /* Create the bitmap */ b = guit->bitmap->create(size, size, BITMAP_NEW | BITMAP_OPAQUE); if (b == NULL) return NULL; rpos = guit->bitmap->get_buffer(b); stride = guit->bitmap->get_rowstride(b); /* Draw the triangle */ for (y = 0; y < size; y++) { pos = rpos; if (y < size / 2) { /* Top half */ for (x = 0; x < y * 2; x++) { *(pos++) = red_from_colour(colour4); *(pos++) = green_from_colour(colour4); *(pos++) = blue_from_colour(colour4); *(pos++) = 0xff; } *(pos++) = red_from_colour(colour3); *(pos++) = green_from_colour(colour3); *(pos++) = blue_from_colour(colour3); *(pos++) = 0xff; *(pos++) = red_from_colour(colour1); *(pos++) = green_from_colour(colour1); *(pos++) = blue_from_colour(colour1); *(pos++) = 0xff; for (x = y * 2 + 2; x < size ; x++) { *(pos++) = red_from_colour(colour0); *(pos++) = green_from_colour(colour0); *(pos++) = blue_from_colour(colour0); *(pos++) = 0xff; } } else if ((y == size / 2) && (size & 0x1)) { /* Middle row */ for (x = 0; x < size - 1; x++) { *(pos++) = red_from_colour(colour4); *(pos++) = green_from_colour(colour4); *(pos++) = blue_from_colour(colour4); *(pos++) = 0xff; } *(pos++) = red_from_colour(colour2); *(pos++) = green_from_colour(colour2); *(pos++) = blue_from_colour(colour2); *(pos++) = 0xff; } else { /* Bottom half */ for (x = 0; x < (size - y - 1) * 2; x++) { *(pos++) = red_from_colour(colour4); *(pos++) = green_from_colour(colour4); *(pos++) = blue_from_colour(colour4); *(pos++) = 0xff; } *(pos++) = red_from_colour(colour3); *(pos++) = green_from_colour(colour3); *(pos++) = blue_from_colour(colour3); *(pos++) = 0xff; *(pos++) = red_from_colour(colour1); *(pos++) = green_from_colour(colour1); *(pos++) = blue_from_colour(colour1); *(pos++) = 0xff; for (x = (size - y) * 2; x < size ; x++) { *(pos++) = red_from_colour(colour0); *(pos++) = green_from_colour(colour0); *(pos++) = blue_from_colour(colour0); *(pos++) = 0xff; } } rpos += stride; } guit->bitmap->modified(b); return b; } /** * Create bitmap copy of another bitmap * * \param orig bitmap to copy * \param size required bitmap size */ static struct bitmap * treeview_generate_copy_bitmap(struct bitmap *orig, int size) { struct bitmap *b = NULL; unsigned char *data; unsigned char *orig_data; size_t stride; if (orig == NULL) return NULL; assert(size == guit->bitmap->get_width(orig)); assert(size == guit->bitmap->get_height(orig)); /* Create the bitmap */ b = guit->bitmap->create(size, size, BITMAP_NEW | BITMAP_OPAQUE); if (b == NULL) return NULL; stride = guit->bitmap->get_rowstride(b); assert(stride == guit->bitmap->get_rowstride(orig)); data = guit->bitmap->get_buffer(b); orig_data = guit->bitmap->get_buffer(orig); /* Copy the bitmap */ memcpy(data, orig_data, stride * size); guit->bitmap->modified(b); /* We've not modified the original image, but we called * bitmap_get_buffer(), so we need to pair that with a * bitmap_modified() call to appease certain front ends. */ guit->bitmap->modified(orig); return b; } /** * Create bitmap from rotation of another bitmap * * \param orig bitmap to create rotation of * \param size required bitmap size */ static struct bitmap * treeview_generate_rotate_bitmap(struct bitmap *orig, int size) { struct bitmap *b = NULL; int x, y; unsigned char *rpos; unsigned char *pos; unsigned char *orig_data; unsigned char *orig_pos; size_t stride; if (orig == NULL) return NULL; assert(size == guit->bitmap->get_width(orig)); assert(size == guit->bitmap->get_height(orig)); /* Create the bitmap */ b = guit->bitmap->create(size, size, BITMAP_NEW | BITMAP_OPAQUE); if (b == NULL) return NULL; stride = guit->bitmap->get_rowstride(b); assert(stride == guit->bitmap->get_rowstride(orig)); rpos = guit->bitmap->get_buffer(b); orig_data = guit->bitmap->get_buffer(orig); /* Copy the rotated bitmap */ for (y = 0; y < size; y++) { pos = rpos; for (x = 0; x < size; x++) { orig_pos = orig_data + x * stride + y * 4; *(pos++) = *(orig_pos++); *(pos++) = *(orig_pos++); *(pos++) = *(orig_pos); *(pos++) = 0xff; } rpos += stride; } guit->bitmap->modified(b); /* We've not modified the original image, but we called * bitmap_get_buffer(), so we need to pair that with a * bitmap_modified() call to appease certain front ends. */ guit->bitmap->modified(orig); return b; } /** * Measures width of characters used to represent treeview furniture. * * \return NSERROR_OK on success else error code */ static nserror treeview_init_furniture(void) { int size = tree_g.line_height / 2; plot_style_odd.furn[TREE_FURN_EXPAND].size = size; plot_style_odd.furn[TREE_FURN_EXPAND].bmp = treeview_generate_triangle_bitmap( plot_style_odd.bg.fill_colour, plot_style_odd.itext.foreground, size); plot_style_odd.furn[TREE_FURN_EXPAND].sel = treeview_generate_triangle_bitmap( plot_style_odd.sbg.fill_colour, plot_style_odd.sitext.foreground, size); plot_style_even.furn[TREE_FURN_EXPAND].size = size; plot_style_even.furn[TREE_FURN_EXPAND].bmp = treeview_generate_triangle_bitmap( plot_style_even.bg.fill_colour, plot_style_even.itext.foreground, size); plot_style_even.furn[TREE_FURN_EXPAND].sel = treeview_generate_copy_bitmap( plot_style_odd.furn[TREE_FURN_EXPAND].sel, size); plot_style_odd.furn[TREE_FURN_CONTRACT].size = size; plot_style_odd.furn[TREE_FURN_CONTRACT].bmp = treeview_generate_rotate_bitmap( plot_style_odd.furn[TREE_FURN_EXPAND].bmp, size); plot_style_odd.furn[TREE_FURN_CONTRACT].sel = treeview_generate_rotate_bitmap( plot_style_odd.furn[TREE_FURN_EXPAND].sel, size); plot_style_even.furn[TREE_FURN_CONTRACT].size = size; plot_style_even.furn[TREE_FURN_CONTRACT].bmp = treeview_generate_rotate_bitmap( plot_style_even.furn[TREE_FURN_EXPAND].bmp, size); plot_style_even.furn[TREE_FURN_CONTRACT].sel = treeview_generate_rotate_bitmap( plot_style_even.furn[TREE_FURN_EXPAND].sel, size); if (plot_style_odd.furn[TREE_FURN_EXPAND].bmp == NULL || plot_style_odd.furn[TREE_FURN_EXPAND].sel == NULL || plot_style_even.furn[TREE_FURN_EXPAND].bmp == NULL || plot_style_even.furn[TREE_FURN_EXPAND].sel == NULL || plot_style_odd.furn[TREE_FURN_CONTRACT].bmp == NULL || plot_style_odd.furn[TREE_FURN_CONTRACT].sel == NULL || plot_style_even.furn[TREE_FURN_CONTRACT].bmp == NULL || plot_style_even.furn[TREE_FURN_CONTRACT].sel == NULL) return NSERROR_NOMEM; tree_g.furniture_width = size + tree_g.line_height / 4; return NSERROR_OK; } /* Exported interface, documented in treeview.h */ nserror treeview_init(void) { long long font_px_size; long long font_pt_size; nserror res; if (tree_g.initialised > 0) { tree_g.initialised++; return NSERROR_OK; } NSLOG(netsurf, INFO, "Initialising treeview module"); font_pt_size = nsoption_int(treeview_font_size); if (font_pt_size <= 0) { font_pt_size = 11 * 10; } font_px_size = (font_pt_size * FIXTOINT(nscss_screen_dpi) / 10 + 36) / 72; tree_g.line_height = (font_px_size * 8 + 3) / 6; res = treeview_init_plot_styles(font_pt_size * FONT_SIZE_SCALE / 10); if (res != NSERROR_OK) { return res; } treeview_init_resources(); res = treeview_init_furniture(); if (res != NSERROR_OK) { return res; } tree_g.step_width = tree_g.furniture_width; tree_g.window_padding = 6; tree_g.icon_size = 17; tree_g.icon_step = 23; tree_g.move_offset = 18; tree_g.initialised++; NSLOG(netsurf, INFO, "Initialised treeview module"); return NSERROR_OK; } /* Exported interface, documented in treeview.h */ nserror treeview_fini(void) { int i; if (tree_g.initialised > 1) { tree_g.initialised--; return NSERROR_OK; } else if (tree_g.initialised == 0) { NSLOG(netsurf, INFO, "Warning: tried to finalise uninitialised treeview module"); return NSERROR_OK; } NSLOG(netsurf, INFO, "Finalising treeview module"); for (i = 0; i < TREE_RES_LAST; i++) { hlcache_handle_release(treeview_res[i].c); } guit->bitmap->destroy(plot_style_odd.furn[TREE_FURN_EXPAND].bmp); guit->bitmap->destroy(plot_style_odd.furn[TREE_FURN_EXPAND].sel); guit->bitmap->destroy(plot_style_even.furn[TREE_FURN_EXPAND].bmp); guit->bitmap->destroy(plot_style_even.furn[TREE_FURN_EXPAND].sel); guit->bitmap->destroy(plot_style_odd.furn[TREE_FURN_CONTRACT].bmp); guit->bitmap->destroy(plot_style_odd.furn[TREE_FURN_CONTRACT].sel); guit->bitmap->destroy(plot_style_even.furn[TREE_FURN_CONTRACT].bmp); guit->bitmap->destroy(plot_style_even.furn[TREE_FURN_CONTRACT].sel); tree_g.initialised--; NSLOG(netsurf, INFO, "Finalised treeview module"); return NSERROR_OK; }