2787 lines
71 KiB
C
2787 lines
71 KiB
C
/*
|
|
* Copyright 2004 Richard Wilson <not_ginger_matt@users.sourceforge.net>
|
|
* Copyright 2009 Paul Blokus <paul_pl@users.sourceforge.net>
|
|
*
|
|
* This file is part of NetSurf, http://www.netsurf-browser.org/
|
|
*
|
|
* NetSurf is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; version 2 of the License.
|
|
*
|
|
* NetSurf is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/** \file
|
|
* Generic tree handling (implementation).
|
|
*/
|
|
|
|
#include <assert.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "content/content.h"
|
|
#include "content/hlcache.h"
|
|
#include "css/utils.h"
|
|
#include "desktop/browser.h"
|
|
#include "desktop/knockout.h"
|
|
#include "desktop/textarea.h"
|
|
#include "desktop/textinput.h"
|
|
#include "desktop/tree.h"
|
|
#include "desktop/options.h"
|
|
#include "desktop/plotters.h"
|
|
#include "render/font.h"
|
|
#include "utils/log.h"
|
|
#include "utils/messages.h"
|
|
#include "utils/utils.h"
|
|
#include "utils/url.h"
|
|
|
|
#define MAXIMUM_URL_LENGTH 1024
|
|
|
|
#define TREE_TEXT_SIZE_PT 11
|
|
#define TREE_ICON_SIZE 17
|
|
#define NODE_INSTEP 20
|
|
|
|
static int tree_text_size_px;
|
|
static int TREE_LINE_HEIGHT;
|
|
|
|
static char *tree_icons_dir = NULL;
|
|
|
|
static plot_font_style_t plot_fstyle = {
|
|
.family = PLOT_FONT_FAMILY_SANS_SERIF,
|
|
.size = TREE_TEXT_SIZE_PT * FONT_SIZE_SCALE,
|
|
.weight = 400,
|
|
.flags = FONTF_NONE
|
|
};
|
|
|
|
static plot_font_style_t plot_fstyle_selected = {
|
|
.family = PLOT_FONT_FAMILY_SANS_SERIF,
|
|
.size = TREE_TEXT_SIZE_PT * FONT_SIZE_SCALE,
|
|
.weight = 400,
|
|
.flags = FONTF_NONE
|
|
};
|
|
|
|
/** plot style for treeview backgrounds. */
|
|
static plot_style_t plot_style_fill_tree_background = {
|
|
.fill_type = PLOT_OP_TYPE_SOLID
|
|
};
|
|
|
|
/** plot style for treeview backgrounds. */
|
|
static plot_style_t plot_style_fill_tree_selected = {
|
|
.fill_type = PLOT_OP_TYPE_SOLID
|
|
};
|
|
|
|
/** plot style for treeview furniture lines. */
|
|
static plot_style_t plot_style_stroke_tree_furniture = {
|
|
.stroke_type = PLOT_OP_TYPE_SOLID
|
|
};
|
|
|
|
/** plot style for treeview furniture fills. */
|
|
static plot_style_t plot_style_fill_tree_furniture = {
|
|
.fill_type = PLOT_OP_TYPE_SOLID
|
|
};
|
|
|
|
struct node;
|
|
struct tree;
|
|
|
|
struct node_element_box {
|
|
int x; /**< X offset from origin */
|
|
int y; /**< Y offset from origin */
|
|
int width; /**< Element width */
|
|
int height; /**< Element height */
|
|
};
|
|
|
|
struct node_element {
|
|
struct node *parent; /**< Parent node */
|
|
node_element_type type; /**< Element type */
|
|
struct node_element_box box; /**< Element bounding box */
|
|
const char *text; /**< Text for the element */
|
|
void *bitmap; /**< Bitmap for the element */
|
|
struct node_element *next; /**< Next node element */
|
|
unsigned int flag; /**< Client specified flag for data
|
|
being represented */
|
|
bool editable; /**< Whether the node text can be
|
|
* modified, editable text is deleted
|
|
* without noticing the tree user
|
|
*/
|
|
};
|
|
|
|
struct node {
|
|
bool selected; /**< Whether the node is selected */
|
|
bool expanded; /**< Whether the node is expanded */
|
|
bool folder; /**< Whether the node is a folder */
|
|
bool retain_in_memory; /**< Whether the node remains
|
|
in memory after deletion */
|
|
bool deleted; /**< Whether the node is currently
|
|
deleted */
|
|
bool processing; /**< Internal flag used when moving */
|
|
struct node_element_box box; /**< Bounding box of all elements */
|
|
struct node_element data; /**< Data to display */
|
|
struct node *parent; /**< Parent entry (NULL for root) */
|
|
struct node *child; /**< First child */
|
|
struct node *last_child; /**< Last child */
|
|
struct node *previous; /**< Previous child of the parent */
|
|
struct node *next; /**< Next child of the parent */
|
|
|
|
/** Sorting function for the node (for folder nodes only) */
|
|
int (*sort) (struct node *, struct node *);
|
|
/** Gets called for each deleted node_element and on node launch */
|
|
tree_node_user_callback user_callback;
|
|
/** User data to be passed to delete_callback */
|
|
void *callback_data;
|
|
};
|
|
|
|
struct tree {
|
|
/* These coordinates are only added to the coordinates passed to the
|
|
plotters. This means they are invisible to the tree, what has to be
|
|
taken into account i.e in keyboard/mouse event passing */
|
|
struct node *root; /* Tree root element */
|
|
int width; /* Tree width */
|
|
int height; /* Tree height */
|
|
unsigned int flags; /* Tree flags */
|
|
struct text_area *textarea; /* Handle for UTF-8 textarea */
|
|
bool textarea_drag_start; /* whether the start of a mouse drag
|
|
was in the textarea */
|
|
struct node_element *editing; /* Node element being edited */
|
|
|
|
bool redraw; /* Flag indicating whether the tree
|
|
should be redrawn on layout
|
|
changes */
|
|
tree_drag_type drag;
|
|
const struct treeview_table *callbacks;
|
|
void *client_data; /* User assigned data for the
|
|
callbacks */
|
|
};
|
|
|
|
void tree_set_icon_dir(char *icon_dir)
|
|
{
|
|
LOG(("Tree icon directory set to %s", icon_dir));
|
|
tree_icons_dir = icon_dir;
|
|
}
|
|
|
|
/**
|
|
* Set up colours for plot styles used in tree redraw.
|
|
*/
|
|
static void tree_setup_colours(void)
|
|
{
|
|
/* Background colour */
|
|
plot_style_fill_tree_background.fill_colour = option_gui_colour_bg_1;
|
|
|
|
/* Selection background colour */
|
|
plot_style_fill_tree_selected.fill_colour = option_gui_colour_fg_1;
|
|
|
|
/* Furniture line colour */
|
|
plot_style_stroke_tree_furniture.stroke_colour = blend_colour(
|
|
option_gui_colour_bg_1, option_gui_colour_fg_1);
|
|
|
|
/* Furniture fill colour */
|
|
plot_style_fill_tree_furniture.fill_colour = option_gui_colour_fg_2;
|
|
|
|
/* Text colour */
|
|
plot_fstyle.foreground = option_gui_colour_fg_1;
|
|
plot_fstyle.background = option_gui_colour_bg_1;
|
|
|
|
/* Selected text colour */
|
|
plot_fstyle_selected.foreground = option_gui_colour_fg_2;
|
|
plot_fstyle_selected.background = option_gui_colour_fg_1;
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates and initialises a new tree.
|
|
*
|
|
* \param flags Flag word for flags to create the new tree with
|
|
* \param callbacks Callback functions to support the tree in the frontend.
|
|
* \param client_data Data to be passed to start_redraw and end_redraw
|
|
* \return The newly created tree, or NULL on memory exhaustion
|
|
*/
|
|
struct tree *tree_create(unsigned int flags,
|
|
const struct treeview_table *callbacks, void *client_data)
|
|
{
|
|
struct tree *tree;
|
|
char *title;
|
|
|
|
tree = calloc(sizeof(struct tree), 1);
|
|
if (tree == NULL) {
|
|
LOG(("calloc failed"));
|
|
warn_user("NoMemory", 0);
|
|
return NULL;
|
|
}
|
|
|
|
title = strdup("Root");
|
|
if (title == NULL) {
|
|
LOG(("malloc failed"));
|
|
warn_user("NoMemory", 0);
|
|
free(tree);
|
|
return NULL;
|
|
}
|
|
tree->root = tree_create_folder_node(NULL, NULL, title,
|
|
false, false, false);
|
|
if (tree->root == NULL) {
|
|
free(title);
|
|
free(tree);
|
|
return NULL;
|
|
}
|
|
tree->root->expanded = true;
|
|
|
|
tree->width = 0;
|
|
tree->height = 0;
|
|
tree->flags = flags;
|
|
tree->textarea = NULL;
|
|
tree->textarea_drag_start = false;
|
|
tree->editing = NULL;
|
|
tree->redraw = false;
|
|
tree->drag = TREE_NO_DRAG;
|
|
tree->callbacks = callbacks;
|
|
tree->client_data = client_data;
|
|
|
|
/* Set text height in pixels */
|
|
tree_text_size_px =
|
|
(TREE_TEXT_SIZE_PT * FIXTOINT(nscss_screen_dpi) + 36) /
|
|
72;
|
|
/* Set line height appropriate for this text height in pixels
|
|
* Using 4/3 text height */
|
|
TREE_LINE_HEIGHT = (tree_text_size_px * 8 + 3) / 6;
|
|
|
|
/* But if that's too small for the icons, base the line height on
|
|
* the icon height. */
|
|
if (TREE_LINE_HEIGHT < TREE_ICON_SIZE + 2)
|
|
TREE_LINE_HEIGHT = TREE_ICON_SIZE + 2;
|
|
|
|
tree_setup_colours();
|
|
|
|
return tree;
|
|
}
|
|
|
|
|
|
/**
|
|
* Recalculates the dimensions of a node element.
|
|
*
|
|
* \param tree the tree to which the element belongs, may be NULL
|
|
* \param element the element to recalculate
|
|
*/
|
|
static void tree_recalculate_node_element(struct tree *tree,
|
|
struct node_element *element)
|
|
{
|
|
struct bitmap *bitmap = NULL;
|
|
int width, height;
|
|
|
|
assert(element != NULL);
|
|
|
|
switch (element->type) {
|
|
case NODE_ELEMENT_TEXT_PLUS_ICON:
|
|
case NODE_ELEMENT_TEXT:
|
|
if(element->text == NULL)
|
|
break;
|
|
|
|
if (tree != NULL && element == tree->editing) {
|
|
textarea_get_dimensions(tree->textarea,
|
|
&element->box.width, NULL);
|
|
} else {
|
|
nsfont.font_width(&plot_fstyle,
|
|
element->text,
|
|
strlen(element->text),
|
|
&element->box.width);
|
|
}
|
|
|
|
element->box.width += 8;
|
|
element->box.height = TREE_LINE_HEIGHT;
|
|
|
|
if (element->type == NODE_ELEMENT_TEXT_PLUS_ICON)
|
|
element->box.width += NODE_INSTEP;
|
|
|
|
break;
|
|
|
|
case NODE_ELEMENT_BITMAP:
|
|
bitmap = element->bitmap;
|
|
if (bitmap != NULL) {
|
|
width = bitmap_get_width(bitmap);
|
|
height = bitmap_get_height(bitmap);
|
|
element->box.width = width + 1;
|
|
element->box.height = height + 2;
|
|
} else {
|
|
element->box.width = 0;
|
|
element->box.height = 0;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Calculates the height of a node including any children
|
|
*
|
|
* \param node the node to calculate the height of
|
|
* \return the total height of the node and children
|
|
*/
|
|
static int tree_get_node_height(struct node *node)
|
|
{
|
|
int y1;
|
|
|
|
assert(node != NULL);
|
|
|
|
if ((node->child == NULL) || (node->expanded == false)) {
|
|
return node->box.height;
|
|
}
|
|
|
|
y1 = node->box.y;
|
|
if (y1 < 0) {
|
|
y1 = 0;
|
|
}
|
|
node = node->child;
|
|
|
|
while ((node->next != NULL) ||
|
|
((node->child != NULL) && (node->expanded))) {
|
|
for (; node->next != NULL; node = node->next);
|
|
|
|
if ((node->child != NULL) && (node->expanded)) {
|
|
node = node->child;
|
|
}
|
|
}
|
|
return node->box.y + node->box.height - y1;
|
|
}
|
|
|
|
|
|
/**
|
|
* Calculates the width of a node including any children
|
|
*
|
|
* \param node the node to calculate the height of
|
|
* \return the total width of the node and children
|
|
*/
|
|
static int tree_get_node_width(struct node *node)
|
|
{
|
|
int width = 0;
|
|
int child_width;
|
|
|
|
assert(node != NULL);
|
|
|
|
for (; node != NULL; node = node->next) {
|
|
if (width < (node->box.x + node->box.width)) {
|
|
width = node->box.x + node->box.width;
|
|
}
|
|
|
|
if ((node->child != NULL) && (node->expanded)) {
|
|
child_width = tree_get_node_width(node->child);
|
|
if (width < child_width) {
|
|
width = child_width;
|
|
}
|
|
}
|
|
}
|
|
return width;
|
|
}
|
|
|
|
|
|
/**
|
|
* Recalculates the position of a node, its siblings and children.
|
|
*
|
|
* \param tree the tree to which 'root' belongs
|
|
* \param root the root node to update from
|
|
*/
|
|
static void tree_recalculate_node_positions(struct tree *tree,
|
|
struct node *root)
|
|
{
|
|
struct node *parent;
|
|
struct node *node;
|
|
struct node *child;
|
|
struct node_element *element;
|
|
int y;
|
|
bool has_icon;
|
|
|
|
for (node = root; node != NULL; node = node->next) {
|
|
|
|
parent = node->parent;
|
|
|
|
if (node->previous != NULL) {
|
|
node->box.x = node->previous->box.x;
|
|
node->box.y = node->previous->box.y +
|
|
tree_get_node_height(node->previous);
|
|
} else if (parent != NULL) {
|
|
node->box.x = parent->box.x + NODE_INSTEP;
|
|
node->box.y = parent->box.y +
|
|
parent->box.height;
|
|
for (child = parent->child; child != node;
|
|
child = child->next)
|
|
node->box.y += child->box.height;
|
|
} else {
|
|
node->box.x = tree->flags & TREE_NO_FURNITURE
|
|
? -NODE_INSTEP + 4 : 0;
|
|
node->box.y = -TREE_LINE_HEIGHT;
|
|
}
|
|
|
|
if (!node->expanded) {
|
|
node->data.box.x = node->box.x;
|
|
node->data.box.y = node->box.y;
|
|
continue;
|
|
}
|
|
|
|
if (node->folder) {
|
|
node->data.box.x = node->box.x;
|
|
node->data.box.y = node->box.y;
|
|
tree_recalculate_node_positions(tree, node->child);
|
|
} else {
|
|
y = node->box.y;
|
|
has_icon = false;
|
|
for (element = &node->data; element != NULL;
|
|
element = element->next)
|
|
if (element->type ==
|
|
NODE_ELEMENT_TEXT_PLUS_ICON) {
|
|
has_icon = true;
|
|
break;
|
|
}
|
|
|
|
for (element = &node->data; element != NULL;
|
|
element = element->next) {
|
|
element->box.x = node->box.x;
|
|
if (element->type !=
|
|
NODE_ELEMENT_TEXT_PLUS_ICON &&
|
|
has_icon)
|
|
element->box.x += NODE_INSTEP;
|
|
element->box.y = y;
|
|
y += element->box.height;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Recalculates the size of a node.
|
|
*
|
|
* \param tree the tree to which node belongs, may be NULL
|
|
* \param node the node to update
|
|
* \param recalculate_sizes whether the node elements have changed
|
|
*/
|
|
static void tree_recalculate_node_sizes(struct tree *tree, struct node *node,
|
|
bool recalculate_sizes)
|
|
{
|
|
struct node_element *element;
|
|
int width, height;
|
|
|
|
assert(node != NULL);
|
|
|
|
width = node->box.width;
|
|
height = node->box.height;
|
|
node->box.width = 0;
|
|
node->box.height = 0;
|
|
if (node->expanded) {
|
|
for (element = &node->data; element != NULL;
|
|
element = element->next) {
|
|
if (recalculate_sizes)
|
|
tree_recalculate_node_element(tree, element);
|
|
node->box.width = (node->box.width > element->box.x +
|
|
element->box.width - node->box.x) ?
|
|
node->box.width :
|
|
element->box.width + element->box.x -
|
|
node->box.x;
|
|
node->box.height += element->box.height;
|
|
}
|
|
} else {
|
|
if (recalculate_sizes)
|
|
for (element = &node->data; element != NULL;
|
|
element = element->next)
|
|
tree_recalculate_node_element(tree, element);
|
|
else
|
|
tree_recalculate_node_element(tree, &node->data);
|
|
node->box.width = node->data.box.width;
|
|
node->box.height = node->data.box.height;
|
|
}
|
|
|
|
if (tree != NULL && height != node->box.height)
|
|
tree_recalculate_node_positions(tree, tree->root);
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates a folder node with the specified title, and optionally links it into
|
|
* the tree.
|
|
*
|
|
* \param tree the owner tree of 'parent', may be NULL
|
|
* \param parent the parent node, or NULL not to link
|
|
* \param title the node title (not copied, used directly)
|
|
* \param editable if true, the node title will be editable
|
|
* \param retain_in_memory if true, the node will stay in memory after deletion
|
|
* \param deleted if true, the node is created with the deleted flag
|
|
* \return the newly created node.
|
|
*/
|
|
struct node *tree_create_folder_node(struct tree *tree, struct node *parent,
|
|
const char *title, bool editable, bool retain_in_memory,
|
|
bool deleted)
|
|
{
|
|
struct node *node;
|
|
|
|
assert(title != NULL);
|
|
|
|
node = calloc(sizeof(struct node), 1);
|
|
if (node == NULL) {
|
|
LOG(("calloc failed"));
|
|
warn_user("NoMemory", 0);
|
|
return NULL;
|
|
}
|
|
node->folder = true;
|
|
node->retain_in_memory = retain_in_memory;
|
|
node->deleted = deleted;
|
|
node->data.parent = node;
|
|
node->data.type = NODE_ELEMENT_TEXT;
|
|
node->data.text = title;
|
|
node->data.flag = TREE_ELEMENT_TITLE;
|
|
node->data.editable = editable;
|
|
node->sort = NULL;
|
|
node->user_callback = NULL;
|
|
node->previous = NULL;
|
|
|
|
tree_recalculate_node_sizes(tree, node, true);
|
|
if (parent != NULL)
|
|
tree_link_node(tree, parent, node, false);
|
|
|
|
return node;
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates a leaf node with the specified title, and optionally links it into
|
|
* the tree.
|
|
*
|
|
* \param tree the owner tree of 'parent', may be NULL
|
|
* \param parent the parent node, or NULL not to link
|
|
* \param title the node title (not copied, used directly)
|
|
* \param editable if true, the node title will be editable
|
|
* \param retain_in_memory if true, the node will stay in memory after deletion
|
|
* \param deleted if true, the node is created with the deleted flag
|
|
* \return the newly created node.
|
|
*/
|
|
struct node *tree_create_leaf_node(struct tree *tree, struct node *parent,
|
|
const char *title, bool editable, bool retain_in_memory,
|
|
bool deleted)
|
|
{
|
|
struct node *node;
|
|
|
|
assert(title != NULL);
|
|
|
|
node = calloc(sizeof(struct node), 1);
|
|
if (node == NULL) {
|
|
LOG(("calloc failed"));
|
|
warn_user("NoMemory", 0);
|
|
return NULL;
|
|
}
|
|
|
|
node->folder = false;
|
|
node->retain_in_memory = retain_in_memory;
|
|
node->deleted = deleted;
|
|
node->data.parent = node;
|
|
node->data.type = NODE_ELEMENT_TEXT;
|
|
node->data.text = title;
|
|
node->data.flag = TREE_ELEMENT_TITLE;
|
|
node->data.editable = editable;
|
|
node->sort = NULL;
|
|
node->user_callback = NULL;
|
|
node->previous = NULL;
|
|
|
|
tree_recalculate_node_sizes(tree, node, true);
|
|
if (parent != NULL)
|
|
tree_link_node(tree, parent, node, false);
|
|
|
|
return node;
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates an empty text node element and links it to a node.
|
|
*
|
|
* \param parent the parent node
|
|
* \param type the required element type
|
|
* \param flag user assigned flag used for searches
|
|
* \return the newly created element.
|
|
*/
|
|
struct node_element *tree_create_node_element(struct node *parent,
|
|
node_element_type type, unsigned int flag, bool editable)
|
|
{
|
|
struct node_element *element;
|
|
|
|
element = calloc(sizeof(struct node_element), 1);
|
|
if (element == NULL)
|
|
return NULL;
|
|
|
|
element->parent = parent;
|
|
element->flag = flag;
|
|
element->type = type;
|
|
element->editable = editable;
|
|
element->next = parent->data.next;
|
|
parent->data.next = element;
|
|
|
|
return element;
|
|
}
|
|
|
|
|
|
/**
|
|
* Inserts a node into the correct place according to the parent's sort function
|
|
*
|
|
* \param parent the node whose child node 'node' becomes
|
|
* \param node the node to be inserted
|
|
*/
|
|
static void tree_sort_insert(struct node *parent, struct node *node)
|
|
{
|
|
struct node *after;
|
|
|
|
assert(node != NULL);
|
|
assert(parent != NULL);
|
|
assert(parent->sort != NULL);
|
|
|
|
after = parent->last_child;
|
|
while ((after != NULL) &&
|
|
(parent->sort(node, after) == -1))
|
|
after = after->previous;
|
|
|
|
if (after != NULL) {
|
|
if (after->next != NULL)
|
|
after->next->previous = node;
|
|
node->next = after->next;
|
|
node->previous = after;
|
|
after->next = node;
|
|
} else {
|
|
node->previous = NULL;
|
|
node->next = parent->child;
|
|
if (parent->child != NULL) {
|
|
parent->child->previous = node;
|
|
}
|
|
parent->child = node;
|
|
}
|
|
|
|
if (node->next == NULL)
|
|
parent->last_child = node;
|
|
|
|
node->parent = parent;
|
|
}
|
|
|
|
|
|
/**
|
|
* Recalculates the size of a tree.
|
|
*
|
|
* \param tree the tree to recalculate
|
|
*/
|
|
static void tree_recalculate_size(struct tree *tree)
|
|
{
|
|
int width, height;
|
|
|
|
assert(tree != NULL);
|
|
|
|
width = tree->width;
|
|
height = tree->height;
|
|
|
|
tree->width = tree_get_node_width(tree->root);
|
|
tree->height = tree_get_node_height(tree->root);
|
|
|
|
if ((width != tree->width) || (height != tree->height))
|
|
tree->callbacks->resized(tree, tree->width, tree->height,
|
|
tree->client_data);
|
|
}
|
|
|
|
/**
|
|
* Recalculate the node data and redraw the relevant section of the tree.
|
|
*
|
|
* \param tree the tree to redraw, may be NULL
|
|
* \param node the node to update
|
|
* \param recalculate_sizes whether the elements have changed
|
|
* \param expansion the request is the result of a node expansion
|
|
*/
|
|
static void tree_handle_node_changed(struct tree *tree, struct node *node,
|
|
bool recalculate_sizes, bool expansion)
|
|
{
|
|
int node_width, node_height, tree_width, tree_height;
|
|
|
|
assert(node != NULL);
|
|
|
|
node_width = node->box.width;
|
|
node_height = node->box.height;
|
|
tree_width = tree->width;
|
|
tree_height = tree->height;
|
|
|
|
if ((recalculate_sizes) || (expansion)) {
|
|
tree_recalculate_node_sizes(tree, node, true);
|
|
}
|
|
|
|
if (tree != NULL) {
|
|
if ((node->box.height != node_height) || (expansion)) {
|
|
tree_recalculate_node_positions(tree, tree->root);
|
|
tree_recalculate_size(tree);
|
|
if (tree->width > tree_width)
|
|
tree_width = tree->width;
|
|
if (tree->height > tree_height)
|
|
tree_height = tree->height;
|
|
if (tree->redraw) {
|
|
tree->callbacks->redraw_request(0, node->box.y,
|
|
tree_width,
|
|
tree_height - node->box.y,
|
|
tree->client_data);
|
|
}
|
|
} else {
|
|
if (node->box.width > node_width)
|
|
node_width = node->box.width;
|
|
if (tree->redraw)
|
|
tree->callbacks->redraw_request(node->box.x,
|
|
node->box.y,
|
|
node_width, node->box.height,
|
|
tree->client_data);
|
|
if (recalculate_sizes) {
|
|
tree_recalculate_size(tree);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Links a node to another node.
|
|
*
|
|
* \param tree the tree in which the link takes place, may be NULL
|
|
* \param link the node to link before/as a child (folders)
|
|
* or before/after (link)
|
|
* \param node the node to link
|
|
* \param before whether to link siblings before or after the supplied node
|
|
*/
|
|
void tree_link_node(struct tree *tree, struct node *link, struct node *node,
|
|
bool before)
|
|
{
|
|
|
|
struct node *parent;
|
|
bool sort = false;
|
|
|
|
assert(link != NULL);
|
|
assert(node != NULL);
|
|
|
|
if ((link->folder == 0) || (before)) {
|
|
parent = node->parent = link->parent;
|
|
if (parent->sort) {
|
|
sort = true;
|
|
} else {
|
|
if (before) {
|
|
node->next = link;
|
|
node->previous = link->previous;
|
|
if (link->previous != NULL)
|
|
link->previous->next = node;
|
|
link->previous = node;
|
|
if ((parent != NULL) && (parent->child == link))
|
|
parent->child = node;
|
|
} else {
|
|
node->previous = link;
|
|
node->next = link->next;
|
|
if (link->next != NULL)
|
|
link->next->previous = node;
|
|
link->next = node;
|
|
if ((parent != NULL) &&
|
|
(parent->last_child == link))
|
|
parent->last_child = node;
|
|
}
|
|
}
|
|
} else {
|
|
parent = node->parent = link;
|
|
if (parent->sort != NULL) {
|
|
sort = true;
|
|
} else {
|
|
node->next = NULL;
|
|
if (link->child == NULL) {
|
|
link->child = link->last_child = node;
|
|
node->previous = NULL;
|
|
} else {
|
|
link->last_child->next = node;
|
|
node->previous = link->last_child;
|
|
link->last_child = node;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (sort) {
|
|
tree_sort_insert(parent, node);
|
|
}
|
|
|
|
tree_handle_node_changed(tree, link, false, true);
|
|
|
|
node->deleted = false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Recalculate the node element and redraw the relevant section of the tree.
|
|
* The tree size is not updated.
|
|
*
|
|
* \param tree the tree to redraw, may be NULL
|
|
* \param element the node element to update
|
|
*/
|
|
static void tree_handle_node_element_changed(struct tree *tree,
|
|
struct node_element *element)
|
|
{
|
|
int width, height;
|
|
|
|
assert(element != NULL);
|
|
|
|
width = element->box.width;
|
|
height = element->box.height;
|
|
tree_recalculate_node_element(tree, element);
|
|
|
|
if (element->box.height != height) {
|
|
tree_recalculate_node_sizes(tree, element->parent, false);
|
|
if ((tree != NULL) && (tree->redraw)) {
|
|
tree->callbacks->redraw_request(0, element->box.y,
|
|
tree->width + element->box.width -
|
|
width,
|
|
tree->height - element->box.y +
|
|
element->box.height - height,
|
|
tree->client_data);
|
|
}
|
|
} else {
|
|
if (element->box.width != width) {
|
|
tree_recalculate_node_sizes(tree, element->parent,
|
|
false);
|
|
}
|
|
|
|
if (tree != NULL) {
|
|
width = (width > element->box.width) ? width :
|
|
element->box.width;
|
|
if (tree->redraw) {
|
|
tree->callbacks->redraw_request(element->box.x,
|
|
element->box.y,
|
|
width,
|
|
element->box.height,
|
|
tree->client_data);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Stops editing a node_element
|
|
*
|
|
* \param tree The tree to stop editing for
|
|
* \param keep_changes If true the changes made to the text will be kept,
|
|
* if false they will be dropped
|
|
*/
|
|
static void tree_stop_edit(struct tree *tree, bool keep_changes)
|
|
{
|
|
int text_len;
|
|
char *text = NULL;
|
|
struct node_element *element;
|
|
struct node_msg_data msg_data;
|
|
node_callback_resp response;
|
|
|
|
assert(tree != NULL);
|
|
|
|
if (tree->editing == NULL || tree->textarea == NULL)
|
|
return;
|
|
|
|
element = tree->editing;
|
|
|
|
if (keep_changes) {
|
|
text_len = textarea_get_text(tree->textarea, NULL, 0);
|
|
text = malloc(text_len * sizeof(char));
|
|
if (text == NULL) {
|
|
LOG(("malloc failed"));
|
|
warn_user("NoMemory", 0);
|
|
textarea_destroy(tree->textarea);
|
|
tree->textarea = NULL;
|
|
return;
|
|
}
|
|
textarea_get_text(tree->textarea, text, text_len);
|
|
}
|
|
|
|
|
|
if (keep_changes && element->parent->user_callback != NULL) {
|
|
msg_data.msg = NODE_ELEMENT_EDIT_FINISHING;
|
|
msg_data.flag = element->flag;
|
|
msg_data.node = element->parent;
|
|
msg_data.data.text = text;
|
|
response = element->parent->user_callback(
|
|
element->parent->callback_data,
|
|
&msg_data);
|
|
|
|
switch (response) {
|
|
case NODE_CALLBACK_REJECT:
|
|
free(text);
|
|
text = NULL;
|
|
break;
|
|
case NODE_CALLBACK_CONTINUE:
|
|
free(text);
|
|
text = NULL;
|
|
return;
|
|
case NODE_CALLBACK_HANDLED:
|
|
case NODE_CALLBACK_NOT_HANDLED:
|
|
text = msg_data.data.text;
|
|
break;
|
|
}
|
|
}
|
|
|
|
textarea_destroy(tree->textarea);
|
|
tree->textarea = NULL;
|
|
tree->editing = NULL;
|
|
|
|
if (text != NULL)
|
|
tree_update_node_element(tree, element, text, NULL);
|
|
else
|
|
tree_handle_node_element_changed(tree, element);
|
|
|
|
|
|
tree_recalculate_size(tree);
|
|
if (element->parent->user_callback != NULL) {
|
|
msg_data.msg = keep_changes ? NODE_ELEMENT_EDIT_FINISHED :
|
|
NODE_ELEMENT_EDIT_CANCELLED;
|
|
msg_data.flag = element->flag;
|
|
msg_data.node = element->parent;
|
|
element->parent->user_callback(element->parent->callback_data,
|
|
&msg_data);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Delinks a node from the tree structures.
|
|
*
|
|
* \param tree the tree in which the delink takes place, may be NULL
|
|
* \param node the node to delink
|
|
*/
|
|
void tree_delink_node(struct tree *tree, struct node *node)
|
|
{
|
|
struct node *parent;
|
|
|
|
assert(node != NULL);
|
|
|
|
/* do not remove the root */
|
|
if (tree != NULL && node == tree->root)
|
|
return;
|
|
if ((tree != NULL) && (tree->editing != NULL)) {
|
|
parent = tree->editing->parent;
|
|
while (parent != NULL) {
|
|
if (node == parent) {
|
|
tree_stop_edit(tree, false);
|
|
break;
|
|
}
|
|
parent = parent->parent;
|
|
}
|
|
}
|
|
|
|
if (node->parent->child == node)
|
|
node->parent->child = node->next;
|
|
if (node->parent->last_child == node)
|
|
node->parent->last_child = node->previous;
|
|
parent = node->parent;
|
|
node->parent = NULL;
|
|
|
|
if (node->previous != NULL)
|
|
node->previous->next = node->next;
|
|
if (node->next != NULL)
|
|
node->next->previous = node->previous;
|
|
node->previous = NULL;
|
|
node->next = NULL;
|
|
|
|
tree_handle_node_changed(tree, parent, false, true);
|
|
}
|
|
|
|
|
|
/**
|
|
* Deletes a node from the tree.
|
|
*
|
|
* \param tree the tree to delete from, may be NULL
|
|
* \param node the node to delete
|
|
* \param siblings whether to delete all siblings
|
|
*/
|
|
static void tree_delete_node_internal(struct tree *tree, struct node *node,
|
|
bool siblings)
|
|
{
|
|
struct node *next, *child, *parent;
|
|
struct node_element *e, *f;
|
|
node_callback_resp response;
|
|
struct node_msg_data msg_data;
|
|
|
|
assert(node != NULL);
|
|
|
|
if (tree != NULL && tree->root == node)
|
|
return;
|
|
|
|
next = node->next;
|
|
parent = node->parent;
|
|
if (tree != NULL && parent == tree->root)
|
|
parent = NULL;
|
|
tree_delink_node(tree, node);
|
|
child = node->child;
|
|
node->child = NULL;
|
|
|
|
node->deleted = true;
|
|
if (child != NULL)
|
|
tree_delete_node_internal(tree, child, true);
|
|
|
|
if (!node->retain_in_memory) {
|
|
node->retain_in_memory = true;
|
|
for (e = &node->data; e != NULL; e = f) {
|
|
if (e->text != NULL) {
|
|
response = NODE_CALLBACK_NOT_HANDLED;
|
|
if (!e->editable &&
|
|
node->user_callback != NULL) {
|
|
msg_data.msg = NODE_DELETE_ELEMENT_TXT;
|
|
msg_data.flag = e->flag;
|
|
msg_data.node = node;
|
|
msg_data.data.text = (void *)e->text;
|
|
response = node->user_callback(
|
|
node->callback_data,
|
|
&msg_data);
|
|
}
|
|
if (response != NODE_CALLBACK_HANDLED)
|
|
free((void *)e->text);
|
|
e->text = NULL;
|
|
}
|
|
if (e->bitmap != NULL) {
|
|
response = NODE_CALLBACK_NOT_HANDLED;
|
|
if (node->user_callback != NULL) {
|
|
msg_data.msg = NODE_DELETE_ELEMENT_IMG;
|
|
msg_data.flag = e->flag;
|
|
msg_data.node = node;
|
|
msg_data.data.bitmap =
|
|
(void *)e->bitmap;
|
|
response = node->user_callback(
|
|
node->callback_data,
|
|
&msg_data);
|
|
}
|
|
/* TODO the type of this field is platform
|
|
dependent */
|
|
if (response != NODE_CALLBACK_HANDLED)
|
|
free(e->bitmap);
|
|
e->bitmap = NULL;
|
|
}
|
|
f = e->next;
|
|
if (e != &node->data)
|
|
free(e);
|
|
}
|
|
free(node);
|
|
}
|
|
|
|
if (siblings && next)
|
|
tree_delete_node_internal(tree, next, true);
|
|
if ((tree->flags & TREE_DELETE_EMPTY_DIRS) && parent != NULL &&
|
|
parent->child == NULL && !parent->deleted)
|
|
tree_delete_node_internal(tree, parent, false);
|
|
}
|
|
|
|
|
|
/**
|
|
* Deletes all nodes of a tree and the tree itself.
|
|
*
|
|
* \param tree the tree to be deleted
|
|
*/
|
|
void tree_delete(struct tree *tree)
|
|
{
|
|
tree_set_redraw(tree, false);
|
|
if (tree->root->child != NULL)
|
|
tree_delete_node_internal(tree, tree->root->child, true);
|
|
|
|
free((void *)tree->root->data.text);
|
|
free(tree->root);
|
|
free(tree);
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets the redraw property of the given tree.
|
|
*
|
|
* \param tree the tree for which to retrieve the property
|
|
* \return the redraw property of the tree
|
|
*/
|
|
bool tree_get_redraw(struct tree *tree)
|
|
{
|
|
return tree->redraw;
|
|
}
|
|
|
|
|
|
/**
|
|
* Deletes a node from the tree.
|
|
*
|
|
* \param tree the tree to delete from, may be NULL
|
|
* \param node the node to delete
|
|
* \param siblings whether to delete all siblings
|
|
*/
|
|
void tree_delete_node(struct tree *tree, struct node *node, bool siblings)
|
|
{
|
|
int y = node->box.y;
|
|
tree_delete_node_internal(tree, node, siblings);
|
|
tree_recalculate_node_positions(tree, tree->root);
|
|
if (tree->redraw)
|
|
tree->callbacks->redraw_request(0, y, tree->width, tree->height,
|
|
tree->client_data);
|
|
tree_recalculate_size(tree);
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets an icon for a node
|
|
*
|
|
* \param tree The tree to which node belongs, may be NULL
|
|
* \param node The node for which the icon is set
|
|
* \param icon the image to use
|
|
*/
|
|
void tree_set_node_icon(struct tree *tree, struct node *node,
|
|
hlcache_handle *icon)
|
|
{
|
|
node->data.type = NODE_ELEMENT_TEXT_PLUS_ICON;
|
|
tree_update_node_element(tree, &(node->data), NULL, icon);
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates all siblings and descendants of a node to an expansion state.
|
|
* No update is performed for the tree changes.
|
|
*
|
|
* \param tree the tree to which 'node' belongs
|
|
* \param node the node to set all siblings and descendants of
|
|
* \param expanded the expansion state to set
|
|
*/
|
|
static void tree_set_node_expanded_all(struct tree *tree, struct node *node,
|
|
bool expanded)
|
|
{
|
|
for (; node != NULL; node = node->next) {
|
|
if (node->expanded != expanded) {
|
|
node->expanded = expanded;
|
|
tree_recalculate_node_sizes(tree, node, false);
|
|
}
|
|
if ((node->child != NULL) && (node->expanded))
|
|
tree_set_node_expanded_all(tree, node->child, expanded);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates [all siblings and descendants of] a node to an expansion state.
|
|
*
|
|
* \param tree the tree to update
|
|
* \param node the node to set [all siblings and descendants of]
|
|
* \param expanded the expansion state to set
|
|
* \param folder whether to update folders, if this together with leaf
|
|
* will be false only 'node' will be updated
|
|
* \param leaf whether to update leaves (check also description for folder)
|
|
* \return whether any changes were made
|
|
*/
|
|
static bool tree_set_node_expanded_internal(struct tree *tree,
|
|
struct node *node, bool expanded, bool folder, bool leaf)
|
|
{
|
|
bool redraw = false;
|
|
struct node *end = (folder == false && leaf == false) ?
|
|
node->next : NULL;
|
|
|
|
if (tree->editing != NULL && node == tree->editing->parent)
|
|
tree_stop_edit(tree, false);
|
|
|
|
for (; node != end; node = node->next) {
|
|
if ((node->expanded != expanded) && (node != tree->root) &&
|
|
((folder && (node->folder)) ||
|
|
(leaf && (!node->folder)) ||
|
|
(!folder && !leaf))) {
|
|
node->expanded = expanded;
|
|
if (node->child != NULL)
|
|
tree_set_node_expanded_all(tree,
|
|
node->child, false);
|
|
if ((node->data.next != NULL) &&
|
|
(node->data.next->box.height == 0))
|
|
tree_recalculate_node_sizes(tree, node, true);
|
|
else
|
|
tree_recalculate_node_sizes(tree, node, false);
|
|
redraw = true;
|
|
}
|
|
if ((folder || leaf) && (node->child != NULL) &&
|
|
(node->expanded))
|
|
redraw |= tree_set_node_expanded_internal(tree,
|
|
node->child, expanded, folder, leaf);
|
|
}
|
|
return redraw;
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates [all siblings and descendants of] a node to an expansion state.
|
|
*
|
|
* \param tree the tree to update
|
|
* \param node the node to set [all siblings and descendants of]
|
|
* \param expanded the expansion state to set
|
|
* \param folder whether to update folders, if this together with leaf
|
|
* will be false only 'node' will be updated
|
|
* \param leaf whether to update leaves (check also description for folder)
|
|
*/
|
|
void tree_set_node_expanded(struct tree *tree, struct node *node, bool expanded,
|
|
bool folder, bool leaf)
|
|
{
|
|
if (tree_set_node_expanded_internal(tree, node, expanded, folder, leaf))
|
|
tree_handle_node_changed(tree, node, false, true);
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates a node to an selected state. The required areas of the tree are
|
|
* redrawn.
|
|
*
|
|
* \param tree the tree to update nodes for, may be NULL
|
|
* \param node the node to set all siblings and descendants of
|
|
* \param all if true update node together with its siblings and
|
|
* descendants
|
|
* \param selected the selection state to set
|
|
*/
|
|
void tree_set_node_selected(struct tree *tree, struct node *node, bool all,
|
|
bool selected)
|
|
{
|
|
struct node *end;
|
|
|
|
if (tree != NULL && node == tree->root)
|
|
node = tree->root->child;
|
|
if (node == NULL)
|
|
return;
|
|
|
|
end = all ? NULL : node->next;
|
|
|
|
for (; node != end; node = node->next) {
|
|
if (node->selected != selected) {
|
|
node->selected = selected;
|
|
if (tree != NULL && tree->redraw)
|
|
tree->callbacks->redraw_request(node->box.x,
|
|
node->box.y,
|
|
node->box.width,
|
|
node->data.box.height,
|
|
tree->client_data);
|
|
}
|
|
if (all && (node->child != NULL) && (node->expanded))
|
|
tree_set_node_selected(tree, node->child, all,
|
|
selected);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the sort function for a node
|
|
*
|
|
* \param tree the tree to which 'node' belongs, may be NULL
|
|
* \param node the node to be inserted
|
|
* \param sort pointer to the sorting function
|
|
*/
|
|
void tree_set_node_sort_function(struct tree *tree, struct node *node,
|
|
int (*sort) (struct node *, struct node *))
|
|
{
|
|
struct node *child;
|
|
|
|
node->sort = sort;
|
|
|
|
if (tree != NULL && tree->editing != NULL)
|
|
tree_stop_edit(tree, false);
|
|
|
|
/* the node had already some children so they must get sorted */
|
|
if (node->child != NULL) {
|
|
|
|
child = node->child;
|
|
node->child = NULL;
|
|
|
|
while (child != NULL) {
|
|
tree_sort_insert(node, child);
|
|
child = child->next;
|
|
}
|
|
|
|
}
|
|
|
|
if (tree != NULL)
|
|
tree_recalculate_node_positions(tree, node->child);
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the delete callback for a node.
|
|
*
|
|
* \param node the node for which the callback is set
|
|
* \param callback the callback functions to be set
|
|
* \param data user data to be passed to callback
|
|
*/
|
|
void tree_set_node_user_callback(struct node *node,
|
|
tree_node_user_callback callback, void *data)
|
|
{
|
|
node->user_callback = callback;
|
|
node->callback_data = data;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the redraw property to the given value. If redraw is true, the tree will
|
|
* be redrawn on layout/appearance changes.
|
|
*
|
|
* \param tree the tree for which the property is set
|
|
* \param redraw the value to set
|
|
*/
|
|
void tree_set_redraw(struct tree *tree, bool redraw)
|
|
{
|
|
/* the tree might have no graphical representation, do not set the
|
|
redraw flag in such case */
|
|
if (tree->callbacks == NULL)
|
|
return;
|
|
tree->redraw = redraw;
|
|
}
|
|
|
|
|
|
/**
|
|
* Checks whether a node, its siblings or any children are selected.
|
|
*
|
|
* \param node the root node to check from
|
|
* \return whether 'node', its siblings or any children are selected.
|
|
*/
|
|
bool tree_node_has_selection(struct node *node)
|
|
{
|
|
for (; node != NULL; node = node->next) {
|
|
if (node->selected)
|
|
return true;
|
|
if ((node->child != NULL) && (node->expanded) &&
|
|
(tree_node_has_selection(node->child)))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the current value of the nodes deleted property.
|
|
*
|
|
* \param node the node to be checked
|
|
* \return the current value of the nodes deleted property
|
|
*/
|
|
bool tree_node_is_deleted(struct node *node)
|
|
{
|
|
return node->deleted;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns true if the node is a folder
|
|
*
|
|
* \param node the node to be checked
|
|
* \return true if the node is a folder, false otherwise
|
|
*/
|
|
bool tree_node_is_folder(struct node *node)
|
|
{
|
|
return node->folder;
|
|
}
|
|
|
|
|
|
/**
|
|
* Update the text of a node element if it has changed.
|
|
*
|
|
* \param element The node element to update.
|
|
* \param text The text to update the element with. The ownership of
|
|
* this string is taken by this function and must not be
|
|
* referred to after the function exits.
|
|
*/
|
|
bool tree_update_element_text(struct tree *tree,
|
|
struct node_element *element, char *text)
|
|
{
|
|
const char *node_text; /* existing node text */
|
|
|
|
if (text == NULL)
|
|
return false;
|
|
|
|
if (element == NULL) {
|
|
free(text);
|
|
return false;
|
|
}
|
|
|
|
node_text = tree_node_element_get_text(element);
|
|
|
|
if ((node_text == NULL) || (strcmp(node_text, text) != 0)) {
|
|
tree_update_node_element(tree, element, text, NULL);
|
|
} else {
|
|
/* text does not need changing, free it */
|
|
free(text);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates the content of a node_element.
|
|
*
|
|
* \param tree the tree owning element, may be NULL
|
|
* \param element the element to be updated
|
|
* \param text new text to be set, may be NULL
|
|
* \param bitmap new bitmap to be set, may be NULL
|
|
*/
|
|
void tree_update_node_element(struct tree *tree, struct node_element *element,
|
|
const char *text, void *bitmap)
|
|
{
|
|
node_callback_resp response;
|
|
struct node_msg_data msg_data;
|
|
|
|
assert(element != NULL);
|
|
|
|
if (tree != NULL && element == tree->editing)
|
|
tree_stop_edit(tree, false);
|
|
|
|
if (text != NULL && (element->type == NODE_ELEMENT_TEXT ||
|
|
element->type == NODE_ELEMENT_TEXT_PLUS_ICON)) {
|
|
if (element->text != NULL) {
|
|
response = NODE_CALLBACK_NOT_HANDLED;
|
|
if (!element->editable &&
|
|
element->parent->user_callback !=
|
|
NULL) {
|
|
msg_data.msg = NODE_DELETE_ELEMENT_TXT;
|
|
msg_data.flag = element->flag;
|
|
msg_data.node = element->parent;
|
|
msg_data.data.text = (void *)element->text;
|
|
response = element->parent->user_callback(
|
|
element->parent->callback_data,
|
|
&msg_data);
|
|
}
|
|
if (response != NODE_CALLBACK_HANDLED)
|
|
free((void *)element->text);
|
|
}
|
|
element->text = text;
|
|
}
|
|
|
|
if (bitmap != NULL && (element->type == NODE_ELEMENT_BITMAP ||
|
|
element->type == NODE_ELEMENT_TEXT_PLUS_ICON)) {
|
|
if (element->bitmap != NULL) {
|
|
response = NODE_CALLBACK_NOT_HANDLED;
|
|
if (element->parent->user_callback != NULL) {
|
|
msg_data.msg = NODE_DELETE_ELEMENT_IMG;
|
|
msg_data.flag = element->flag;
|
|
msg_data.node = element->parent;
|
|
msg_data.data.bitmap = (void *)element->bitmap;
|
|
response = element->parent->user_callback(
|
|
element->parent->callback_data,
|
|
&msg_data);
|
|
}
|
|
if (response != NODE_CALLBACK_HANDLED)
|
|
free(element->bitmap);
|
|
}
|
|
element->bitmap = bitmap;
|
|
}
|
|
|
|
tree_handle_node_element_changed(tree, element);
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the node element's text
|
|
*
|
|
* \return the node element's text
|
|
*/
|
|
const char *tree_node_element_get_text(struct node_element *element)
|
|
{
|
|
return element->text;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the root node of a tree
|
|
*
|
|
* \param tree the tree to get the root of
|
|
* \return the root of the tree
|
|
*/
|
|
struct node *tree_get_root(struct tree *tree)
|
|
{
|
|
return tree->root;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns whether the current tree is being edited at this time
|
|
*
|
|
* \param tree the tree to be checked
|
|
* \return true if the tree is currently being edited
|
|
*/
|
|
bool tree_is_edited(struct tree *tree)
|
|
{
|
|
return tree->editing == NULL ? false : true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the drag state of a tree
|
|
*
|
|
* \param tree the tree to get the state of
|
|
* \return drag type (defined in desktop/tree.h)
|
|
*/
|
|
tree_drag_type tree_drag_status(struct tree *tree)
|
|
{
|
|
return tree->drag;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the first child of a node
|
|
*
|
|
* \param node the node to get the child of
|
|
* \return the nodes first child
|
|
*/
|
|
struct node *tree_node_get_child(struct node *node)
|
|
{
|
|
return node->child;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the closest sibling a node
|
|
*
|
|
* \param node the node to get the sibling of
|
|
* \return the nodes sibling
|
|
*/
|
|
struct node *tree_node_get_next(struct node *node)
|
|
{
|
|
return node->next;
|
|
}
|
|
|
|
|
|
/**
|
|
* Draws an element's expansion icon
|
|
*
|
|
* \param tree the tree to draw the expansion for
|
|
* \param element the element to draw the expansion for
|
|
* \param tree_x X coordinate of the tree
|
|
* \param tree_y Y coordinate of the tree
|
|
*/
|
|
static void tree_draw_node_expansion_toggle(struct tree *tree,
|
|
struct node *node, int tree_x, int tree_y)
|
|
{
|
|
int x, y;
|
|
|
|
assert(tree != NULL);
|
|
assert(node != NULL);
|
|
|
|
if ((node->child != NULL) || (node->data.next != NULL)) {
|
|
x = tree_x + node->box.x - (NODE_INSTEP / 2) - 4;
|
|
y = tree_y + node->box.y + (TREE_LINE_HEIGHT - 9) / 2;
|
|
plot.rectangle(x, y, x + 9, y + 9,
|
|
&plot_style_fill_tree_furniture);
|
|
plot.rectangle(x , y, x + 8, y + 8,
|
|
&plot_style_stroke_tree_furniture);
|
|
plot.line(x + 2, y + 4, x + 7, y + 4,
|
|
&plot_style_stroke_tree_furniture);
|
|
if (!node->expanded)
|
|
plot.line(x + 4, y + 2, x + 4, y + 7,
|
|
&plot_style_stroke_tree_furniture);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Draws an element, including any expansion icons
|
|
*
|
|
* \param tree the tree to draw an element for
|
|
* \param element the element to draw
|
|
* \param tree_x X coordinate to draw the tree at (wrt plot origin)
|
|
* \param tree_y Y coordinate to draw the tree at (wrt plot origin)
|
|
* \param clip clipping rectangle (wrt plot origin)
|
|
*/
|
|
static void tree_draw_node_element(struct tree *tree,
|
|
struct node_element *element, int tree_x, int tree_y,
|
|
const struct rect *clip)
|
|
{
|
|
|
|
struct bitmap *bitmap = NULL;
|
|
int x, y, width;
|
|
bool selected = false;
|
|
hlcache_handle *icon;
|
|
plot_font_style_t *fstyle;
|
|
const int icon_inset = (TREE_LINE_HEIGHT - TREE_ICON_SIZE) / 2;
|
|
|
|
assert(tree != NULL);
|
|
assert(element != NULL);
|
|
assert(element->parent != NULL);
|
|
|
|
x = tree_x + element->box.x;
|
|
y = tree_y + element->box.y;
|
|
width = element->box.width;
|
|
if (&element->parent->data == element)
|
|
if (element->parent->selected)
|
|
selected = true;
|
|
|
|
switch (element->type) {
|
|
case NODE_ELEMENT_TEXT_PLUS_ICON:
|
|
icon = element->bitmap;
|
|
if (icon != NULL && (content_get_status(icon) ==
|
|
CONTENT_STATUS_READY ||
|
|
content_get_status(icon) ==
|
|
CONTENT_STATUS_DONE) &&
|
|
x + TREE_ICON_SIZE > clip->x0 &&
|
|
x < clip->x1) {
|
|
struct rect c;
|
|
/* Clip to image area */
|
|
c.x0 = x;
|
|
c.y0 = y + icon_inset;
|
|
c.x1 = x + TREE_ICON_SIZE;
|
|
c.y1 = y + icon_inset + TREE_ICON_SIZE;
|
|
if (c.x0 < clip->x0) c.x0 = clip->x0;
|
|
if (c.y0 < clip->y0) c.y0 = clip->y0;
|
|
if (c.x1 > clip->x1) c.x1 = clip->x1;
|
|
if (c.y1 > clip->y1) c.y1 = clip->y1;
|
|
|
|
if (c.x1 > c.x0 && c.y1 > c.y0) {
|
|
/* Valid clip rectangles only */
|
|
plot.clip(&c);
|
|
content_redraw(icon , x, y + icon_inset,
|
|
TREE_ICON_SIZE, TREE_ICON_SIZE,
|
|
&c, 1, 0);
|
|
|
|
/* Restore previous clipping area */
|
|
plot.clip(clip);
|
|
}
|
|
}
|
|
|
|
x += NODE_INSTEP;
|
|
width -= NODE_INSTEP;
|
|
|
|
/* fall through */
|
|
case NODE_ELEMENT_TEXT:
|
|
if (element->text == NULL || clip->x1 < x)
|
|
break;
|
|
|
|
if (element == tree->editing)
|
|
return;
|
|
|
|
if (selected) {
|
|
fstyle = &plot_fstyle_selected;
|
|
plot.rectangle(x, y, x + width,
|
|
y + element->box.height,
|
|
&plot_style_fill_tree_selected);
|
|
} else {
|
|
fstyle = &plot_fstyle;
|
|
}
|
|
|
|
plot.text(x + 4, y + (TREE_LINE_HEIGHT * 3 + 2) / 4,
|
|
element->text, strlen(element->text),
|
|
fstyle);
|
|
break;
|
|
case NODE_ELEMENT_BITMAP:
|
|
bitmap = element->bitmap;
|
|
if (bitmap == NULL)
|
|
break;
|
|
plot.bitmap(x, y, element->box.width - 1,
|
|
element->box.height - 2,
|
|
bitmap, 0xFFFFFF, BITMAPF_NONE);
|
|
if (!(tree->flags & TREE_NO_FURNITURE))
|
|
plot.rectangle(x, y, x + element->box.width - 1,
|
|
y + element->box.height - 3,
|
|
&plot_style_stroke_tree_furniture);
|
|
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Redraws a node.
|
|
*
|
|
* \param tree the tree to draw
|
|
* \param node the node to draw children and siblings of
|
|
* \param tree_x X coordinate to draw the tree at (wrt plot origin)
|
|
* \param tree_y Y coordinate to draw the tree at (wrt plot origin)
|
|
* \param clip clipping rectangle (wrt plot origin)
|
|
*/
|
|
static void tree_draw_node(struct tree *tree, struct node *node,
|
|
int tree_x, int tree_y, struct rect clip)
|
|
{
|
|
struct node_element *element;
|
|
struct node *parent;
|
|
int x0, y0, x1, y1;
|
|
struct rect node_extents;
|
|
|
|
assert(tree != NULL);
|
|
assert(node != NULL);
|
|
|
|
/* Find node's extents, including children's area */
|
|
node_extents.x0 = tree_x + node->box.x - NODE_INSTEP;
|
|
node_extents.y0 = tree_y + node->box.y;
|
|
node_extents.x1 = tree_x + node->box.x + node->box.width + NODE_INSTEP;
|
|
if (node->next != NULL)
|
|
node_extents.y1 = tree_y + node->next->box.y;
|
|
else
|
|
node_extents.y1 = tree_y + node->box.y + node->box.height;
|
|
|
|
/* Nothing to draw, if node is outside clip region */
|
|
if ((node_extents.x1 < clip.x0) && (node_extents.y1 < clip.y0) &&
|
|
(node_extents.x0 > clip.x1) &&
|
|
(node_extents.y0 > clip.y1)) {
|
|
return;
|
|
}
|
|
|
|
/* Intersect clip region with node's extents */
|
|
if (clip.x0 < node_extents.x0) clip.x0 = node_extents.x0;
|
|
if (clip.y0 < node_extents.y0) clip.y0 = node_extents.y0;
|
|
if (clip.x1 > node_extents.x1) clip.x1 = node_extents.x1;
|
|
if (clip.y1 > node_extents.y1) clip.y1 = node_extents.y1;
|
|
|
|
if (clip.x0 >= clip.x1 || clip.y0 >= clip.y1) {
|
|
/* Invalid clip rectangle */
|
|
return;
|
|
}
|
|
|
|
/* Set up the clipping area */
|
|
plot.clip(&clip);
|
|
|
|
/* Draw node's furniture */
|
|
if (!(tree->flags & TREE_NO_FURNITURE)) {
|
|
/* Display furniture */
|
|
if (node->previous != NULL) {
|
|
/* There is a node above this
|
|
* Display furniture; line connecting up to previous */
|
|
x0 = x1 = tree_x + node->box.x - (NODE_INSTEP / 2);
|
|
y0 = tree_y + node->previous->box.y;
|
|
y1 = tree_y + node->box.y + (TREE_LINE_HEIGHT / 2);
|
|
plot.line(x0, y0, x1, y1,
|
|
&plot_style_stroke_tree_furniture);
|
|
}
|
|
if (node->next != NULL) {
|
|
/* There is a node below this
|
|
* Display furniture; line connecting down to next */
|
|
x0 = x1 = tree_x + node->box.x - (NODE_INSTEP / 2);
|
|
y0 = tree_y + node->box.y + (TREE_LINE_HEIGHT / 2);
|
|
y1 = tree_y + node->next->box.y;
|
|
plot.line(x0, y0, x1, y1,
|
|
&plot_style_stroke_tree_furniture);
|
|
}
|
|
|
|
parent = node->parent;
|
|
if ((parent != NULL) && (parent != tree->root) &&
|
|
(parent->child == node)) {
|
|
/* Node is first child */
|
|
x0 = x1 = tree_x + parent->box.x + (NODE_INSTEP / 2);
|
|
y0 = tree_y + parent->data.box.y +
|
|
parent->data.box.height;
|
|
y1 = y0 + (TREE_LINE_HEIGHT / 2);
|
|
plot.line(x0, y0, x1, y1,
|
|
&plot_style_stroke_tree_furniture);
|
|
}
|
|
/* Line from expansion toggle to icon */
|
|
x0 = tree_x + node->box.x - (NODE_INSTEP / 2);
|
|
x1 = x0 + (NODE_INSTEP / 2) - 2;
|
|
y0 = y1 = tree_y + node->data.box.y + node->data.box.height -
|
|
(TREE_LINE_HEIGHT / 2);
|
|
plot.line(x0, y0, x1, y1, &plot_style_stroke_tree_furniture);
|
|
|
|
tree_draw_node_expansion_toggle(tree, node, tree_x, tree_y);
|
|
}
|
|
|
|
/* Draw node's element(s)
|
|
* NOTE: node's children are handled later in tree_draw_tree() */
|
|
if (node->expanded) {
|
|
for (element = &node->data; element != NULL;
|
|
element = element->next) {
|
|
/* Draw each element of expanded node */
|
|
tree_draw_node_element(tree, element, tree_x, tree_y,
|
|
&clip);
|
|
}
|
|
} else {
|
|
/* Draw main title element of node */
|
|
tree_draw_node_element(tree, &node->data, tree_x, tree_y,
|
|
&clip);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Redraws a node's descendants.
|
|
*
|
|
* \param tree the tree to draw
|
|
* \param node the node to draw children and siblings of
|
|
* \param tree_x X coordinate to draw the tree at (wrt plot origin)
|
|
* \param tree_y Y coordinate to draw the tree at (wrt plot origin)
|
|
* \param clip clipping rectangle (wrt plot origin)
|
|
*/
|
|
static void tree_draw_tree(struct tree *tree, struct node *node,
|
|
int tree_x, int tree_y, struct rect clip)
|
|
{
|
|
struct node *child;
|
|
|
|
assert(tree != NULL);
|
|
assert(node != NULL);
|
|
|
|
for (child = node->child; child != NULL; child = child->next) {
|
|
/* Draw children that are inside the clip region */
|
|
|
|
if (child->next != NULL &&
|
|
(child->next->box.y + tree_y < clip.y0))
|
|
/* Child is above clip region */
|
|
continue;
|
|
if (child->box.y + tree_y > clip.y1)
|
|
/* Child is below clip region
|
|
* further siblings will be too */
|
|
return;
|
|
|
|
/* Draw current child */
|
|
tree_draw_node(tree, child, tree_x, tree_y, clip);
|
|
/* And its children */
|
|
if ((child->child != NULL) && (child->expanded)) {
|
|
/* Child has children and they are visible */
|
|
tree_draw_tree(tree, child, tree_x, tree_y, clip);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Redraws a tree.
|
|
*
|
|
* \param tree the tree to draw
|
|
* \param x X coordinate to draw the tree at (wrt plot origin)
|
|
* \param y Y coordinate to draw the tree at (wrt plot origin)
|
|
* \param clip_x minimum x of the clipping rectangle (wrt tree origin)
|
|
* \param clip_y minimum y of the clipping rectangle (wrt tree origin)
|
|
* \param clip_width width of the clipping rectangle
|
|
* \param clip_height height of the clipping rectangle
|
|
*/
|
|
void tree_draw(struct tree *tree, int x, int y,
|
|
int clip_x, int clip_y, int clip_width, int clip_height)
|
|
{
|
|
struct rect clip;
|
|
|
|
assert(tree != NULL);
|
|
assert(tree->root != NULL);
|
|
|
|
/* Start knockout rendering if it's available for this plotter */
|
|
if (plot.option_knockout)
|
|
knockout_plot_start(&plot);
|
|
|
|
/* Set up clip rectangle */
|
|
clip.x0 = x + clip_x;
|
|
clip.y0 = y + clip_y;
|
|
clip.x1 = clip.x0 + clip_width;
|
|
clip.y1 = clip.y0 + clip_height;
|
|
plot.clip(&clip);
|
|
|
|
/* Flat fill extents of clipping area */
|
|
plot.rectangle(clip.x0, clip.y0, clip.x1, clip.y1,
|
|
&plot_style_fill_tree_background);
|
|
|
|
/* don't draw empty trees or trees with redraw flag set to false */
|
|
if (tree->root->child != NULL && tree->redraw) {
|
|
|
|
/* Draw the tree */
|
|
tree_draw_tree(tree, tree->root, x, y, clip);
|
|
|
|
/* Draw textarea, if present */
|
|
if (tree->editing != NULL) {
|
|
x = x + tree->editing->box.x;
|
|
y = y + tree->editing->box.y;
|
|
if (tree->editing->type == NODE_ELEMENT_TEXT_PLUS_ICON)
|
|
x += NODE_INSTEP;
|
|
textarea_redraw(tree->textarea, x, y, &clip);
|
|
}
|
|
}
|
|
|
|
/* Rendering complete */
|
|
if (plot.option_knockout)
|
|
knockout_plot_end();
|
|
}
|
|
|
|
|
|
/**
|
|
* Finds a node element from a node with a specific user_type
|
|
*
|
|
* \param node the node to examine
|
|
* \param flag user assinged flag used is searches
|
|
* \param after if this is not NULL the search will start after the given
|
|
* node_element
|
|
* \return the corresponding element
|
|
*/
|
|
struct node_element *tree_node_find_element(struct node *node,
|
|
unsigned int flag, struct node_element *after)
|
|
{
|
|
struct node_element *element;
|
|
|
|
if (after == NULL)
|
|
element = &node->data;
|
|
else {
|
|
assert(after->parent == node);
|
|
element = after->next;
|
|
}
|
|
|
|
for (; element != NULL; element = element->next)
|
|
if (element->flag == flag) return element;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
/**
|
|
* Deletes all selected nodes from the tree.
|
|
*
|
|
* \param tree the tree to delete from
|
|
* \param node the node to delete
|
|
*/
|
|
void tree_delete_selected_nodes(struct tree *tree, struct node *node)
|
|
{
|
|
struct node *next;
|
|
|
|
if (node == tree->root) {
|
|
if (node->child != NULL)
|
|
tree_delete_selected_nodes(tree, node->child);
|
|
return;
|
|
}
|
|
|
|
while (node != NULL) {
|
|
next = node->next;
|
|
if (node->selected)
|
|
tree_delete_node(tree, node, false);
|
|
else if (node->child != NULL)
|
|
tree_delete_selected_nodes(tree, node->child);
|
|
node = next;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the selected node, or NULL if multiple nodes are selected.
|
|
*
|
|
* \param node the node to search sibling and children
|
|
* \return the selected node, or NULL if multiple nodes are selected
|
|
*/
|
|
struct node *tree_get_selected_node(struct node *node)
|
|
{
|
|
struct node *result = NULL;
|
|
struct node *temp;
|
|
|
|
for (; node != NULL; node = node->next) {
|
|
if (node->selected) {
|
|
if (result != NULL)
|
|
return NULL;
|
|
result = node;
|
|
}
|
|
if ((node->child != NULL) && (node->expanded)) {
|
|
temp = tree_get_selected_node(node->child);
|
|
if (temp != NULL) {
|
|
if (result != NULL)
|
|
return NULL;
|
|
else
|
|
result = temp;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Finds a node element at a specific location.
|
|
*
|
|
* \param node the root node to check from
|
|
* \param x the x co-ordinate
|
|
* \param y the y co-ordinate
|
|
* \param expansion_toggle whether the coordinate was in an expansion toggle
|
|
* \return the node at the specified position, or NULL for none
|
|
*/
|
|
static struct node_element *tree_get_node_element_at(struct node *node,
|
|
int x, int y, bool *expansion_toggle)
|
|
{
|
|
struct node_element *element;
|
|
int x0, x1, y0, y1;
|
|
|
|
*expansion_toggle = false;
|
|
for (; node != NULL; node = node->next) {
|
|
if (node->box.y > y) return NULL;
|
|
if ((node->box.x - NODE_INSTEP < x) && (node->box.y < y) &&
|
|
(node->box.x + node->box.width >= x) &&
|
|
(node->box.y + node->box.height >= y)) {
|
|
if (node->expanded) {
|
|
for (element = &node->data; element != NULL;
|
|
element = element->next) {
|
|
x0 = element->box.x;
|
|
y0 = element->box.y;
|
|
x1 = element->box.x +
|
|
element->box.width;
|
|
y1 = element->box.y +
|
|
element->box.height;
|
|
if ((x0 < x) && (y0 < y) && (x1 >= x)
|
|
&& (y1 >= y))
|
|
return element;
|
|
}
|
|
} else {
|
|
x0 = node->data.box.x;
|
|
y0 = node->data.box.y;
|
|
x1 = node->data.box.x + node->data.box.width;
|
|
y1 = node->data.box.y + node->data.box.height;
|
|
if ((x0 < x) && (y0 < y) && (x1 >= x) &&
|
|
(y1>= y))
|
|
return &node->data;
|
|
}
|
|
if (((node->child != NULL) ||
|
|
(node->data.next != NULL)) &&
|
|
(node->data.box.x - NODE_INSTEP + 4 < x)
|
|
&& (node->data.box.y + 4 < y) &&
|
|
(node->data.box.x > x) &&
|
|
(node->data.box.y + TREE_LINE_HEIGHT > y)) {
|
|
/* Node either has node children, or node
|
|
* has more than one element.
|
|
* Coordinate is over node expansion toggle area
|
|
*/
|
|
*expansion_toggle = true;
|
|
return &node->data;
|
|
}
|
|
}
|
|
|
|
element = tree_get_node_element_at(node->child, x, y,
|
|
expansion_toggle);
|
|
if ((node->child != NULL) && (node->expanded) &&
|
|
(element != NULL))
|
|
return element;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
/**
|
|
* Finds a node at a specific location.
|
|
*
|
|
* \param root the root node to check from
|
|
* \param x the x co-ordinate
|
|
* \param y the y co-ordinate
|
|
* \param expansion_toggle whether the coordinate was in an expansion toggle
|
|
* \return the node at the specified position, or NULL for none
|
|
*/
|
|
static struct node *tree_get_node_at(struct node *root, int x, int y,
|
|
bool *expansion_toggle)
|
|
{
|
|
struct node_element *result;
|
|
|
|
if ((result = tree_get_node_element_at(root, x, y, expansion_toggle)))
|
|
return result->parent;
|
|
return NULL;
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets link characteristics to insert a node at a specified position.
|
|
*
|
|
* \param tree the tree to find link information for
|
|
* \param x the x co-ordinate
|
|
* \param y the y co-ordinate
|
|
* \param before set to whether the node should be linked before on exit
|
|
* \return the node to link with
|
|
*/
|
|
struct node *tree_get_link_details(struct tree *tree, int x, int y,
|
|
bool *before)
|
|
{
|
|
struct node *node = NULL;
|
|
bool expansion_toggle;
|
|
|
|
assert(tree != NULL);
|
|
assert(tree->root != NULL);
|
|
|
|
*before = false;
|
|
if (tree->root->child != NULL)
|
|
node = tree_get_node_at(tree->root->child, x, y,
|
|
&expansion_toggle);
|
|
if ((node == NULL) || (expansion_toggle))
|
|
return tree->root;
|
|
|
|
if (y < (node->box.y + (node->box.height / 2))) {
|
|
*before = true;
|
|
} else if ((node->folder) && (node->expanded) &&
|
|
(node->child != NULL)) {
|
|
node = node->child;
|
|
*before = true;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
|
|
/**
|
|
* Launches all the selected nodes of the tree
|
|
*
|
|
* \param tree the tree for which all nodes will be launched
|
|
* \param node the node which will be checked together with its children
|
|
*/
|
|
static void tree_launch_selected_internal(struct tree *tree, struct node *node)
|
|
{
|
|
struct node_msg_data msg_data;
|
|
|
|
for (; node != NULL; node = node->next) {
|
|
if (node->selected && node->user_callback != NULL) {
|
|
msg_data.msg = NODE_LAUNCH;
|
|
msg_data.flag = TREE_ELEMENT_TITLE;
|
|
msg_data.node = node;
|
|
node->user_callback(node->callback_data, &msg_data);
|
|
}
|
|
if (node->child != NULL)
|
|
tree_launch_selected_internal(tree, node->child);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Launches all the selected nodes of the tree
|
|
*
|
|
* \param tree the tree for which all nodes will be launched
|
|
*/
|
|
void tree_launch_selected(struct tree *tree)
|
|
{
|
|
if (tree->root->child != NULL)
|
|
tree_launch_selected_internal(tree, tree->root->child);
|
|
}
|
|
|
|
|
|
/**
|
|
* Handles a mouse action for a tree
|
|
*
|
|
* \param tree the tree to handle a click for
|
|
* \param mouse the mouse state
|
|
* \param x X coordinate of mouse action
|
|
* \param y Y coordinate of mouse action
|
|
* \return whether the click was handled
|
|
*/
|
|
bool tree_mouse_action(struct tree *tree, browser_mouse_state mouse, int x,
|
|
int y)
|
|
{
|
|
bool expansion_toggle;
|
|
struct node *node;
|
|
struct node *last;
|
|
struct node_element *element;
|
|
struct node_msg_data msg_data;
|
|
|
|
bool double_click_1 = mouse & BROWSER_MOUSE_DOUBLE_CLICK &&
|
|
mouse & BROWSER_MOUSE_CLICK_1;
|
|
bool double_click_2 = mouse & BROWSER_MOUSE_DOUBLE_CLICK &&
|
|
mouse & BROWSER_MOUSE_CLICK_2;
|
|
|
|
assert(tree != NULL);
|
|
assert(tree->root != NULL);
|
|
|
|
if (tree->root->child == NULL)
|
|
return true;
|
|
|
|
element = tree_get_node_element_at(tree->root->child, x, y,
|
|
&expansion_toggle);
|
|
|
|
/* pass in-textarea mouse action and drags which started in it
|
|
to the textarea */
|
|
if (tree->editing != NULL) {
|
|
int x0, x1, y0, y1;
|
|
x0 = tree->editing->box.x;
|
|
if (tree->editing->type == NODE_ELEMENT_TEXT_PLUS_ICON)
|
|
x0 += NODE_INSTEP;
|
|
x1 = tree->editing->box.x + tree->editing->box.width;
|
|
y0 = tree->editing->box.y;
|
|
y1 = tree->editing->box.y + tree->editing->box.height;
|
|
|
|
if (tree->textarea_drag_start &&
|
|
(mouse & (BROWSER_MOUSE_HOLDING_1 |
|
|
BROWSER_MOUSE_HOLDING_2))) {
|
|
/* Track the drag path */
|
|
textarea_mouse_action(tree->textarea, mouse,
|
|
x - x0, y - y0);
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
if ((x >= x0) && (x < x1) && (y >= y0) && (y < y1)) {
|
|
/* Inside the textarea */
|
|
if (mouse & (BROWSER_MOUSE_DRAG_1 |
|
|
BROWSER_MOUSE_DRAG_2)) {
|
|
/* Drag starting */
|
|
tree->textarea_drag_start = true;
|
|
tree->drag = TREE_TEXTAREA_DRAG;
|
|
} else {
|
|
/* Other action */
|
|
tree->textarea_drag_start = false;
|
|
}
|
|
textarea_mouse_action(tree->textarea, mouse,
|
|
x - x0, y - y0);
|
|
return true;
|
|
|
|
}
|
|
}
|
|
|
|
tree->textarea_drag_start = false;
|
|
|
|
/* we are not interested in the drag path, return */
|
|
if (mouse & (BROWSER_MOUSE_HOLDING_1 | BROWSER_MOUSE_HOLDING_2))
|
|
return true;
|
|
|
|
/* cancel edit */
|
|
if (tree->editing != NULL)
|
|
tree_stop_edit(tree, false);
|
|
|
|
/* no item either means cancel selection on (select) click or a drag */
|
|
if (element == NULL) {
|
|
if (tree->flags & TREE_SINGLE_SELECT) {
|
|
tree_set_node_selected(tree, tree->root->child, true,
|
|
false);
|
|
return true;
|
|
}
|
|
if (mouse & (BROWSER_MOUSE_CLICK_1 | BROWSER_MOUSE_DRAG_1))
|
|
tree_set_node_selected(tree, tree->root->child, true,
|
|
false);
|
|
if (mouse & (BROWSER_MOUSE_DRAG_1 | BROWSER_MOUSE_DRAG_2)) {
|
|
|
|
/** @todo the tree window has to scroll the tree when
|
|
* mouse reaches border while dragging this isn't
|
|
* solved for the browser window too.
|
|
*/
|
|
tree->drag = TREE_SELECT_DRAG;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
node = element->parent;
|
|
|
|
/* A click on expansion toggle or double click on folder toggles node
|
|
* expansion */
|
|
if (((expansion_toggle) && (mouse & (BROWSER_MOUSE_CLICK_1 |
|
|
BROWSER_MOUSE_CLICK_2))) ||
|
|
(((!expansion_toggle) && (node->child != NULL)) &&
|
|
(double_click_1 || double_click_2))) {
|
|
|
|
/* clear any selection */
|
|
tree_set_node_selected(tree, tree->root->child, true, false);
|
|
|
|
/* expand / contract node and redraw */
|
|
tree_set_node_expanded(tree, node, !node->expanded,
|
|
false, false);
|
|
|
|
/* find the last child node if expanded */
|
|
last = node;
|
|
if ((last->child != NULL) && (last->expanded)) {
|
|
last = last->child;
|
|
while ((last->next != NULL) ||
|
|
((last->child != NULL) &&
|
|
(last->expanded))) {
|
|
if (last->next != NULL)
|
|
last = last->next;
|
|
else
|
|
last = last->child;
|
|
}
|
|
}
|
|
/* scroll to the bottom element then back to the top */
|
|
element = &last->data;
|
|
if (last->expanded)
|
|
for (; element->next != NULL; element = element->next);
|
|
tree->callbacks->scroll_visible(element->box.y,
|
|
element->box.height,
|
|
tree->client_data);
|
|
tree->callbacks->scroll_visible(node->data.box.y,
|
|
node->data.box.height,
|
|
tree->client_data);
|
|
return true;
|
|
}
|
|
|
|
/* no use for any other expansion toggle click */
|
|
if (expansion_toggle)
|
|
return true;
|
|
|
|
/* single/double ctrl+click or alt+click starts editing */
|
|
if ((element->editable) && (!tree->editing) &&
|
|
((element->type == NODE_ELEMENT_TEXT) ||
|
|
(element->type == NODE_ELEMENT_TEXT_PLUS_ICON)) &&
|
|
(mouse & BROWSER_MOUSE_CLICK_1 || double_click_1) &&
|
|
(mouse & BROWSER_MOUSE_MOD_2 ||
|
|
mouse & BROWSER_MOUSE_MOD_3)) {
|
|
tree_set_node_selected(tree, tree->root->child, true, false);
|
|
tree_start_edit(tree, element);
|
|
return true;
|
|
}
|
|
|
|
/* double click launches the leaf */
|
|
if (double_click_1 || double_click_2) {
|
|
if (node->user_callback == NULL)
|
|
return false;
|
|
msg_data.msg = NODE_LAUNCH;
|
|
msg_data.flag = TREE_ELEMENT_TITLE;
|
|
msg_data.node = node;
|
|
if (node->user_callback(node->callback_data, &msg_data) !=
|
|
NODE_CALLBACK_HANDLED)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/* single click (select) cancels current selection and selects item */
|
|
if (mouse & BROWSER_MOUSE_CLICK_1 || (mouse & BROWSER_MOUSE_CLICK_2 &&
|
|
tree->flags & TREE_SINGLE_SELECT)) {
|
|
if (tree->flags & TREE_NO_SELECT)
|
|
return true;
|
|
if (!node->selected) {
|
|
tree_set_node_selected(tree, tree->root->child, true,
|
|
false);
|
|
node->selected = true;
|
|
tree_handle_node_element_changed(tree, &node->data);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* single click (adjust) toggles item selection */
|
|
if (mouse & BROWSER_MOUSE_CLICK_2) {
|
|
if (tree->flags & TREE_NO_SELECT)
|
|
return true;
|
|
node->selected = !node->selected;
|
|
tree_handle_node_element_changed(tree, &node->data);
|
|
return true;
|
|
}
|
|
|
|
/* drag starts a drag operation */
|
|
if ((!tree->editing) && (mouse & (BROWSER_MOUSE_DRAG_1 |
|
|
BROWSER_MOUSE_DRAG_2))) {
|
|
if (tree->flags & TREE_NO_DRAGS)
|
|
return true;
|
|
|
|
if (!node->selected) {
|
|
tree_set_node_selected(tree, tree->root->child, true,
|
|
false);
|
|
node->selected = true;
|
|
tree_handle_node_element_changed(tree, &node->data);
|
|
}
|
|
|
|
if (tree->flags & TREE_MOVABLE)
|
|
tree->drag = TREE_MOVE_DRAG;
|
|
else tree->drag = TREE_UNKNOWN_DRAG;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates the selected state for a region of nodes.
|
|
*
|
|
* \param tree the tree to update
|
|
* \param node the node to update children and siblings of
|
|
* \param y the minimum y of the selection rectangle
|
|
* \param height the height of the selection rectangle
|
|
* \param invert whether to invert the selected state
|
|
*/
|
|
static void tree_handle_selection_area_node(struct tree *tree,
|
|
struct node *node, int y, int height, bool invert)
|
|
{
|
|
struct node_element *element;
|
|
struct node *update;
|
|
int y_max;
|
|
int y0, y1;
|
|
|
|
assert(tree != NULL);
|
|
assert(node != NULL);
|
|
|
|
y_max = y + height;
|
|
|
|
for (; node != NULL; node = node->next) {
|
|
if (node->box.y > y_max) return;
|
|
y0 = node->box.y;
|
|
y1 = node->box.y + node->box.height;
|
|
if ((y0 < y_max) && (y1 >= y)) {
|
|
update = NULL;
|
|
if (node->expanded) {
|
|
for (element = &node->data; element != NULL;
|
|
element = element->next) {
|
|
y0 = element->box.y;
|
|
y1 = element->box.y +
|
|
element->box.height;
|
|
if ((y0 < y_max) && (y1 >= y)) {
|
|
update = element->parent;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
y0 = node->data.box.y;
|
|
y1 = node->data.box.y + node->data.box.height;
|
|
if ((y0 < y_max) && (y1 >= y))
|
|
update = node->data.parent;
|
|
}
|
|
if ((update) && (node != tree->root)) {
|
|
if (invert) {
|
|
node->selected = !node->selected;
|
|
tree_handle_node_element_changed(tree,
|
|
&node->data);
|
|
} else if (!node->selected) {
|
|
node->selected = true;
|
|
tree_handle_node_element_changed(tree,
|
|
&node->data);
|
|
}
|
|
}
|
|
}
|
|
if ((node->child != NULL) && (node->expanded))
|
|
tree_handle_selection_area_node(tree, node->child, y,
|
|
height, invert);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates the selected state for a region of nodes.
|
|
*
|
|
* \param tree the tree to update
|
|
* \param y the minimum y of the selection rectangle
|
|
* \param height the height of the selection rectangle
|
|
* \param invert whether to invert the selected state
|
|
*/
|
|
static void tree_handle_selection_area(struct tree *tree, int y, int height,
|
|
bool invert)
|
|
{
|
|
assert(tree != NULL);
|
|
assert(tree->root != NULL);
|
|
|
|
if (tree->root->child == NULL)
|
|
return;
|
|
|
|
if (height < 0) {
|
|
y += height;
|
|
height = -height;
|
|
}
|
|
tree_handle_selection_area_node(tree, tree->root->child, y, height,
|
|
invert);
|
|
}
|
|
|
|
|
|
/**
|
|
* Clears the processing flag.
|
|
*
|
|
* \param node the node to process siblings and children of
|
|
*/
|
|
static void tree_clear_processing(struct node *node)
|
|
{
|
|
for (; node != NULL; node = node->next) {
|
|
node->processing = false;
|
|
if (node->child != NULL)
|
|
tree_clear_processing(node->child);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the processing flag to the selection state.
|
|
*
|
|
* \param node the node to process siblings and children of
|
|
*/
|
|
static void tree_selected_to_processing(struct node *node)
|
|
{
|
|
for (; node != NULL; node = node->next) {
|
|
node->processing = node->selected;
|
|
if ((node->child != NULL) && (node->expanded))
|
|
tree_selected_to_processing(node->child);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Moves the first node in a tree with the processing flag set.
|
|
*
|
|
* \param tree the tree in which the move takes place
|
|
* \param node the node to move siblings/children of
|
|
* \param link the node to link before/as a child (folders) or before/after
|
|
* (link)
|
|
* \param before whether to link siblings before or after the supplied node
|
|
* \param first whether to always link after the supplied node (ie not
|
|
* inside of folders)
|
|
* \return the node moved
|
|
*/
|
|
static struct node *tree_move_processing_node(struct tree *tree,
|
|
struct node *node, struct node *link, bool before, bool first)
|
|
{
|
|
struct node *result;
|
|
|
|
bool folder = link->folder;
|
|
for (; node != NULL; node = node->next) {
|
|
if (node->processing) {
|
|
node->processing = false;
|
|
tree_delink_node(tree, node);
|
|
if (!first)
|
|
link->folder = false;
|
|
tree_link_node(tree, link, node, before);
|
|
if (!first)
|
|
link->folder = folder;
|
|
return node;
|
|
}
|
|
if (node->child != NULL) {
|
|
result = tree_move_processing_node(tree, node->child,
|
|
link, before, first);
|
|
if (result != NULL)
|
|
return result;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
/**
|
|
* Moves nodes within a tree.
|
|
*
|
|
* \param tree the tree to process
|
|
* \param destination the node to link before/as a child (folders)
|
|
* or before/after (link)
|
|
* \param before whether to link siblings before or after the supplied
|
|
* node
|
|
*/
|
|
static void tree_move_selected_nodes(struct tree *tree,
|
|
struct node *destination, bool before)
|
|
{
|
|
struct node *link;
|
|
struct node *test;
|
|
bool error;
|
|
|
|
tree_clear_processing(tree->root);
|
|
tree_selected_to_processing(tree->root);
|
|
|
|
/* the destination node cannot be a child of any node with
|
|
the processing flag set */
|
|
error = destination->processing;
|
|
for (test = destination; test != NULL; test = test->parent)
|
|
error |= test->processing;
|
|
if (error) {
|
|
tree_clear_processing(tree->root);
|
|
return;
|
|
}
|
|
if ((destination->folder) && (!destination->expanded) && (!before)) {
|
|
tree_set_node_expanded(tree, destination, true, false, false);
|
|
}
|
|
link = tree_move_processing_node(tree, tree->root, destination, before,
|
|
true);
|
|
while (link != NULL)
|
|
link = tree_move_processing_node(tree, tree->root, link, false,
|
|
false);
|
|
|
|
tree_clear_processing(tree->root);
|
|
tree_recalculate_node_positions(tree, tree->root);
|
|
if (tree->redraw)
|
|
tree->callbacks->redraw_request(0, 0, tree->width, tree->height,
|
|
tree->client_data);
|
|
}
|
|
|
|
|
|
/**
|
|
* Handle the end of a drag operation
|
|
*
|
|
* \param tree the tree on which the drag was performed
|
|
* \param mouse mouse state during drag end
|
|
* \param x0 x coordinate of drag start
|
|
* \param y0 y coordinate of drag start
|
|
* \param x1 x coordinate of drag end
|
|
* \param y1 y coordinate of drag end
|
|
*/
|
|
void tree_drag_end(struct tree *tree, browser_mouse_state mouse, int x0, int y0,
|
|
int x1, int y1)
|
|
{
|
|
|
|
bool before;
|
|
struct node *node;
|
|
int x, y;
|
|
|
|
if (tree->textarea_drag_start) {
|
|
x = tree->editing->box.x;
|
|
y = tree->editing->box.y;
|
|
if (tree->editing->type == NODE_ELEMENT_TEXT_PLUS_ICON)
|
|
x += NODE_INSTEP;
|
|
textarea_drag_end(tree->textarea, mouse, x1 - x, y1 - y);
|
|
}
|
|
|
|
tree->textarea_drag_start = false;
|
|
|
|
switch (tree->drag) {
|
|
case TREE_NO_DRAG:
|
|
case TREE_TEXTAREA_DRAG:
|
|
case TREE_UNKNOWN_DRAG:
|
|
break;
|
|
case TREE_SELECT_DRAG:
|
|
tree_handle_selection_area(tree, y0, y1 - y0,
|
|
(mouse | BROWSER_MOUSE_HOLDING_2));
|
|
break;
|
|
case TREE_MOVE_DRAG:
|
|
if (!(tree->flags & TREE_MOVABLE))
|
|
return;
|
|
node = tree_get_link_details(tree, x1, y1, &before);
|
|
tree_move_selected_nodes(tree, node, before);
|
|
break;
|
|
}
|
|
|
|
tree->drag = TREE_NO_DRAG;
|
|
}
|
|
|
|
|
|
/**
|
|
* Key press handling for a tree.
|
|
*
|
|
* \param tree The tree which got the keypress
|
|
* \param key The ucs4 character codepoint
|
|
* \return true if the keypress is dealt with, false otherwise.
|
|
*/
|
|
bool tree_keypress(struct tree *tree, uint32_t key)
|
|
{
|
|
|
|
if (tree->editing != NULL)
|
|
switch (key) {
|
|
case KEY_ESCAPE:
|
|
tree_stop_edit(tree, false);
|
|
return true;
|
|
case KEY_NL:
|
|
case KEY_CR:
|
|
tree_stop_edit(tree, true);
|
|
return true;
|
|
default:
|
|
return textarea_keypress(tree->textarea, key);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Alphabetical comparison function for nodes
|
|
*
|
|
* \param n1 first node to compare
|
|
* \param n2 first node to compare
|
|
* \return 0 if equal, greater then zero if n1 > n2,
|
|
* less then zero if n2 < n1
|
|
*/
|
|
int tree_alphabetical_sort(struct node *n1, struct node *n2)
|
|
{
|
|
return strcmp(n1->data.text, n2->data.text);
|
|
}
|
|
|
|
|
|
/**
|
|
* Redraw requests from the textarea are piped through this because we have to
|
|
* check the redraw flag of the tree before requesting a redraw and change the
|
|
* position to tree origin relative.
|
|
*/
|
|
static void tree_textarea_redraw_request(void *data, int x, int y,
|
|
int width, int height)
|
|
{
|
|
struct tree *tree = data;
|
|
x = x + tree->editing->box.x;
|
|
y = y + tree->editing->box.y;
|
|
if (tree->editing->type == NODE_ELEMENT_TEXT_PLUS_ICON)
|
|
x += NODE_INSTEP;
|
|
|
|
if (tree->redraw)
|
|
tree->callbacks->redraw_request(x, y,
|
|
width, height,
|
|
tree->client_data);
|
|
}
|
|
|
|
|
|
/**
|
|
* Starts editing a node_element
|
|
*
|
|
* \param tree The tree to which element belongs
|
|
* \param element The element to start being edited
|
|
*/
|
|
void tree_start_edit(struct tree *tree, struct node_element *element)
|
|
{
|
|
struct node *parent;
|
|
int width, height;
|
|
|
|
assert(tree != NULL);
|
|
assert(element != NULL);
|
|
|
|
if (tree->editing != NULL)
|
|
tree_stop_edit(tree, true);
|
|
|
|
parent = element->parent;
|
|
if (&parent->data == element)
|
|
parent = parent->parent;
|
|
for (; parent != NULL; parent = parent->parent) {
|
|
if (!parent->expanded) {
|
|
tree_set_node_expanded(tree, parent, true,
|
|
false, false);
|
|
}
|
|
}
|
|
|
|
tree->editing = element;
|
|
tree->callbacks->get_window_dimensions(&width, NULL, tree->client_data);
|
|
width -= element->box.x;
|
|
height = element->box.height;
|
|
if (element->type == NODE_ELEMENT_TEXT_PLUS_ICON)
|
|
width -= NODE_INSTEP;
|
|
|
|
tree->textarea = textarea_create(width, height, 0,
|
|
&plot_fstyle, tree_textarea_redraw_request, tree);
|
|
if (tree->textarea == NULL) {
|
|
tree_stop_edit(tree, false);
|
|
return;
|
|
}
|
|
textarea_set_text(tree->textarea, element->text);
|
|
|
|
tree_handle_node_element_changed(tree, element);
|
|
tree_recalculate_size(tree);
|
|
tree->callbacks->scroll_visible(element->box.y, element->box.height,
|
|
tree->client_data);
|
|
}
|
|
|
|
|
|
/**
|
|
* Callback for fetchcache(). Should be removed once bitmaps get loaded directly
|
|
* from disc
|
|
*/
|
|
static nserror tree_icon_callback(hlcache_handle *handle,
|
|
const hlcache_event *event, void *pw)
|
|
{
|
|
return NSERROR_OK;
|
|
}
|
|
|
|
|
|
/**
|
|
* Tree utility function. Placed here so that this code doesn't have to be
|
|
* copied by each user.
|
|
*
|
|
* \param name the name of the loaded icon, if it's not a full path the icon is
|
|
* looked for in the directory specified by tree_icons_dir
|
|
* \return the icon in form of a content or NULL on failure
|
|
*/
|
|
hlcache_handle *tree_load_icon(const char *name)
|
|
{
|
|
char *url = NULL;
|
|
const char *icon_url = NULL;
|
|
int len;
|
|
hlcache_handle *c;
|
|
nserror err;
|
|
|
|
/** @todo something like bitmap_from_disc is needed here */
|
|
|
|
if (!strncmp(name, "file://", 7)) {
|
|
icon_url = name;
|
|
} else {
|
|
char *native_path;
|
|
|
|
if (tree_icons_dir == NULL)
|
|
return NULL;
|
|
|
|
/* path + separator + leafname + '\0' */
|
|
len = strlen(tree_icons_dir) + 1 + strlen(name) + 1;
|
|
native_path = malloc(len);
|
|
if (native_path == NULL) {
|
|
LOG(("malloc failed"));
|
|
warn_user("NoMemory", 0);
|
|
return NULL;
|
|
}
|
|
|
|
/* Build native path */
|
|
memcpy(native_path, tree_icons_dir,
|
|
strlen(tree_icons_dir) + 1);
|
|
path_add_part(native_path, len, name);
|
|
|
|
/* Convert native path to URL */
|
|
url = path_to_url(native_path);
|
|
|
|
free(native_path);
|
|
icon_url = url;
|
|
}
|
|
|
|
/* Fetch the icon */
|
|
err = hlcache_handle_retrieve(icon_url, 0, 0, 0,
|
|
tree_icon_callback, 0, 0, 0, &c);
|
|
|
|
|
|
/* If we built the URL here, free it */
|
|
if (url != NULL)
|
|
free(url);
|
|
|
|
if (err != NSERROR_OK) {
|
|
return NULL;
|
|
}
|
|
|
|
return c;
|
|
}
|