// =============================================================== //
//                                                                 //
//   File      : TreeDisplay.cxx                                   //
//   Purpose   : phylogenetic tree display                         //
//                                                                 //
//   Institute of Microbiology (Technical University Munich)       //
//   http://www.arb-home.de/                                       //
//                                                                 //
// =============================================================== //

#include "TreeDisplay.hxx"
#include "TreeCallbacks.hxx"
#include "GroupIterator.hxx"

#include <AP_TreeColors.hxx>
#include <AP_TreeShader.hxx>
#include <AP_TreeSet.hxx>
#include <nds.h>

#include <config_manager.hxx>

#include <aw_preset.hxx>
#include <aw_awars.hxx>
#include <aw_msg.hxx>
#include <aw_root.hxx>
#include <aw_question.hxx>

#include <arb_defs.h>
#include <arb_diff.h>
#include <arb_global_defs.h>
#include <arb_strbuf.h>

#include <ad_cb.h>

#include <unistd.h>
#include <iostream>
#include <cfloat>
#include <algorithm>

#define RULER_LINEWIDTH "ruler/ruler_width" // line width of ruler
#define RULER_SIZE      "ruler/size"

#define DEFAULT_RULER_LINEWIDTH tree_defaults::LINEWIDTH
#define DEFAULT_RULER_LENGTH    tree_defaults::LENGTH

int TREE_canvas::count = 0;

const int MARKER_COLORS = 12;
static int MarkerGC[MARKER_COLORS] = {
    // double rainbow
    AWT_GC_RED,
    AWT_GC_YELLOW,
    AWT_GC_GREEN,
    AWT_GC_CYAN,
    AWT_GC_BLUE,
    AWT_GC_MAGENTA,

    AWT_GC_ORANGE,
    AWT_GC_LAWNGREEN,
    AWT_GC_AQUAMARIN,
    AWT_GC_SKYBLUE,
    AWT_GC_PURPLE,
    AWT_GC_PINK,
};

using namespace AW;

static void nocb() {}
GraphicTreeCallback AWT_graphic_tree::group_changed_cb = makeGraphicTreeCallback(nocb);

AW_gc_manager *AWT_graphic_tree::init_devices(AW_window *aww, AW_device *device, AWT_canvas* ntw) {
    AW_gc_manager *gc_manager =
        AW_manage_GC(aww,
                     ntw->get_gc_base_name(),
                     device, AWT_GC_MAX, AW_GCM_DATA_AREA,
                     makeGcChangedCallback(TREE_GC_changed_cb, ntw),
                     "#8ce",

                     // Important note :
                     // Many gc indices are shared between ABR_NTREE and ARB_PARSIMONY
                     // e.g. the tree drawing routines use same gc's for drawing both trees
                     // (check PARS_dtree.cxx AWT_graphic_parsimony::init_devices)
                     // (keep in sync with ../../PARSIMONY/PARS_dtree.cxx@init_devices)

                     // Note: in radial tree display, branches with lower gc(-index) are drawn AFTER branches
                     //       with higher gc(-index), i.e. marked branches are drawn on top of unmarked branches.

                     "Cursor$white",
                     "Branch remarks$#3d8a99",
                     "+-Bootstrap$#abe3ff",    "-B.(limited)$#cfe9ff",
                     "-IRS group box$#000",
                     "Marked$#ffe560",
                     "Some marked$#d9c45c",
                     "Not marked$#5d5d5d",
                     "Zombies etc.$#7aa3cc",

                     "+-None (black)$#000000", "-All (white)$#ffffff",

                     "+-P1(red)$#ff0000",        "+-P2(green)$#00ff00",    "-P3(blue)$#0000ff",
                     "+-P4(orange)$#ffd060",     "+-P5(aqua)$#40ffc0",     "-P6(purple)$#c040ff",
                     "+-P7(1&2,yellow)$#ffff00", "+-P8(2&3,cyan)$#00ffff", "-P9(3&1,magenta)$#ff00ff",
                     "+-P10(lawn)$#c0ff40",      "+-P11(skyblue)$#40c0ff", "-P12(pink)$#f030b0",

                     "&color_groups", // use color groups

                     // color ranges:
                     "*Linear,linear:+-lower$#a00,-upper$#0a0",
                     "*Rainbow,cyclic:+-col1$#a00,-col2$#990,"
                     /*           */ "+-col3$#0a0,-col4$#0aa,"
                     /*           */ "+-col5$#00a,-col6$#b0b",
                     "*Planar,planar:+-off$#000,-dim1$#a00,"
                     /*          */ "-dim2$#0a0",
                     "*Spatial,spatial:+-off$#000,-dim1$#a00,"
                     /*            */ "+-dim2$#0a0,-dim3$#00a",

                     NULp);

    return gc_manager;
}

long AWT_graphic_tree::mark_species_in_tree(AP_tree *at, int mark_mode) {
    /*
      mode      does

      0         unmark all
      1         mark all
      2         invert all marks
      3         count marked (=result)
    */

    if (!at) return 0;

    if (at->is_leaf()) {
        long count = 0;
        if (at->gb_node) {      // not a zombie
            switch (mark_mode) {
                case 0: GB_write_flag(at->gb_node, 0); break;
                case 1: GB_write_flag(at->gb_node, 1); break;
                case 2: GB_write_flag(at->gb_node, !GB_read_flag(at->gb_node)); break;
                case 3: count = GB_read_flag(at->gb_node); break;
                default: td_assert(0);
            }
        }
        return count;
    }

    return
        mark_species_in_tree(at->get_leftson(), mark_mode) +
        mark_species_in_tree(at->get_rightson(), mark_mode);
}

long AWT_graphic_tree::mark_species_in_tree_that(AP_tree *at, int mark_mode, bool (*condition)(GBDATA*, void*), void *cd) {
    /*
      mark_mode does

      0         unmark all
      1         mark all
      2         invert all marks
      3         count marked (=result)

      marks are only changed for those species for that condition() != 0
    */

    if (!at) return 0;

    if (at->is_leaf()) {
        long count = 0;
        if (at->gb_node) {      // not a zombie
            int oldMark = GB_read_flag(at->gb_node);
            if (oldMark != mark_mode && condition(at->gb_node, cd)) {
                switch (mark_mode) {
                    case 0: GB_write_flag(at->gb_node, 0); break;
                    case 1: GB_write_flag(at->gb_node, 1); break;
                    case 2: GB_write_flag(at->gb_node, !oldMark); break;
                    case 3: count += !!oldMark; break;
                    default: td_assert(0);
                }
            }
        }
        return count;
    }

    return
        mark_species_in_tree_that(at->get_leftson(), mark_mode, condition, cd) +
        mark_species_in_tree_that(at->get_rightson(), mark_mode, condition, cd);
}


void AWT_graphic_tree::mark_species_in_rest_of_tree(AP_tree *at, int mark_mode) {
    // same as mark_species_in_tree but works on rest of tree
    if (at) {
        AP_tree *pa = at->get_father();
        if (pa) {
            mark_species_in_tree(at->get_brother(), mark_mode);
            mark_species_in_rest_of_tree(pa, mark_mode);
        }
    }
}

bool AWT_graphic_tree::tree_has_marks(AP_tree *at) {
    if (!at) return false;

    if (at->is_leaf()) {
        if (!at->gb_node) return false; // zombie
        int marked = GB_read_flag(at->gb_node);
        return marked;
    }

    return tree_has_marks(at->get_leftson()) || tree_has_marks(at->get_rightson());
}

bool AWT_graphic_tree::rest_tree_has_marks(AP_tree *at) {
    if (!at) return false;

    AP_tree *pa = at->get_father();
    if (!pa) return false;

    return tree_has_marks(at->get_brother()) || rest_tree_has_marks(pa);
}

class AWT_graphic_tree_group_state {
    // group counters:
    int closed, opened;
    int closed_terminal, opened_terminal;
    int closed_with_marked;
    int closed_with_unmarked;

    // species counters:
    int marked_in_groups, marked_outside_groups;
    int unmarked_in_groups, unmarked_outside_groups;

    friend void AWT_graphic_tree::detect_group_state(AP_tree *at, AWT_graphic_tree_group_state *state, AP_tree *skip_this_son);

public:

    void clear() {
        closed               = 0;
        opened               = 0;
        closed_terminal      = 0;
        opened_terminal      = 0;
        closed_with_marked   = 0;
        closed_with_unmarked = 0;

        marked_in_groups        = 0;
        marked_outside_groups   = 0;
        unmarked_in_groups      = 0;
        unmarked_outside_groups = 0;
    }

    AWT_graphic_tree_group_state() { clear(); }

    bool has_groups() const { return closed+opened; }
    int marked() const { return marked_in_groups+marked_outside_groups; }
    int unmarked() const { return unmarked_in_groups+unmarked_outside_groups; }

    bool all_opened() const { return closed == 0 && opened>0; }
    bool all_closed() const { return opened == 0 && closed>0; }
    bool all_terminal_closed() const { return opened_terminal == 0 && closed_terminal == closed; }
    bool all_marked_opened() const { return marked_in_groups > 0 && closed_with_marked == 0; }

    CollapseMode next_expand_mode() const {
        if (closed_with_unmarked) {
            if (closed_with_marked) return EXPAND_MARKED;
            return EXPAND_UNMARKED;
        }
        return EXPAND_ALL;
    }

    CollapseMode next_collapse_mode() const {
        if (all_terminal_closed()) return COLLAPSE_ALL;
        return COLLAPSE_TERMINAL;
    }
};

void AWT_graphic_tree::detect_group_state(AP_tree *at, AWT_graphic_tree_group_state *state, AP_tree *skip_this_son) {
    if (!at) return;
    if (at->is_leaf()) {
        if (at->gb_node) {
            // count marked/unmarked
            if (GB_read_flag(at->gb_node)) state->marked_outside_groups++;
            else                           state->unmarked_outside_groups++;
        }
        return; // leafs never get grouped
    }

    if (at->is_normal_group()) {
        AWT_graphic_tree_group_state sub_state;
        if (at->leftson != skip_this_son) detect_group_state(at->get_leftson(), &sub_state, skip_this_son);
        if (at->rightson != skip_this_son) detect_group_state(at->get_rightson(), &sub_state, skip_this_son);

        if (at->gr.grouped) {   // a closed group
            state->closed++;
            if (!sub_state.has_groups()) state->closed_terminal++;
            if (sub_state.marked()) state->closed_with_marked++;
            if (sub_state.unmarked()) state->closed_with_unmarked++;
            state->closed += sub_state.opened;
        }
        else { // an open group
            state->opened++;
            if (!sub_state.has_groups()) state->opened_terminal++;
            state->opened += sub_state.opened;
        }

        state->marked_in_groups   += sub_state.marked();
        state->unmarked_in_groups += sub_state.unmarked();

        state->closed               += sub_state.closed;
        state->opened_terminal      += sub_state.opened_terminal;
        state->closed_terminal      += sub_state.closed_terminal;
        state->closed_with_marked   += sub_state.closed_with_marked;
        state->closed_with_unmarked += sub_state.closed_with_unmarked;
    }
    else { // not a group
        if (at->leftson != skip_this_son) detect_group_state(at->get_leftson(), state, skip_this_son);
        if (at->rightson != skip_this_son) detect_group_state(at->get_rightson(), state, skip_this_son);
    }
}

void AWT_graphic_tree::group_rest_tree(AP_tree *at, CollapseMode mode, int color_group) {
    if (at) {
        AP_tree *pa = at->get_father();
        if (pa) {
            group_tree(at->get_brother(), mode, color_group);
            group_rest_tree(pa, mode, color_group);
        }
    }
}

bool AWT_graphic_tree::group_tree(AP_tree *at, CollapseMode mode, int color_group) {
    /*! collapse/expand subtree according to mode (and color_group)
     * Run on father! (why?)
     * @return true if subtree shall expand
     */

    if (!at) return true;

    GB_transaction ta(tree_static->get_gb_main());

    bool expand_me = false;
    if (at->is_leaf()) {
        if (mode & EXPAND_ALL) expand_me = true;
        else if (at->gb_node) { // linked leaf
            if (mode & (EXPAND_MARKED|EXPAND_UNMARKED)) {
                expand_me = bool(GB_read_flag(at->gb_node)) == bool(mode & EXPAND_MARKED);
            }

            if (!expand_me && (mode & EXPAND_COLOR)) { // do not group specified color_group
                int my_color_group = GBT_get_color_group(at->gb_node);

                expand_me =
                    my_color_group == color_group || // specific or no color
                    (my_color_group != 0 && color_group == -1); // any color
            }
        }
        else { // zombie
            expand_me = mode & EXPAND_ZOMBIES;
        }
    }
    else { // inner node
        at->gr.grouped = false; // expand during descend (important because keeled group may fold 'at' from inside recursion!)

        expand_me = group_tree(at->get_leftson(), mode, color_group);
        expand_me = group_tree(at->get_rightson(), mode, color_group) || expand_me;

        if (!expand_me) { // no son requests to be expanded
            if (at->is_normal_group()) {
                at->gr.grouped = true; // group me
                if (mode & COLLAPSE_TERMINAL) expand_me = true; // do not group non-terminal groups (upwards)
            }
            if (at->is_keeled_group()) {
                at->get_father()->gr.grouped = true; // group "keeled"-me
                if (mode & COLLAPSE_TERMINAL) expand_me = true; // do not group non-terminal groups (upwards)
            }
        }
    }
    return expand_me;
}

void AWT_graphic_tree::reorderTree(TreeOrder mode) {
    AP_tree *at = get_root_node();
    if (at) {
        at->reorder_tree(mode);
        exports.request_save();
    }
}

BootstrapConfig::BootstrapConfig() :
    circle_filter(AW_SCREEN|AW_PRINTER|AW_SIZE_UNSCALED),
    text_filter  (AW_SCREEN|AW_CLICK|AW_CLICK_DROP|AW_PRINTER|AW_SIZE_UNSCALED)
{
}

void BootstrapConfig::display_remark(AW_device *device, const char *remark, const AW::Position& center, double blen, double bdist, const AW::Position& textpos, AW_pos alignment) const {
    double         dboot;
    GBT_RemarkType type = parse_remark(remark, dboot);

    bool is_bootstrap = false;
    switch (type) {
        case REMARK_BOOTSTRAP:
            is_bootstrap = true;
            break;

        case REMARK_NONE:
            td_assert(show_100_if_empty); // otherwise method should not have been called

            is_bootstrap = true;
            dboot        = 100.0;
            break;

        case REMARK_OTHER:
            break;
    }

    int bootstrap = is_bootstrap ? int(dboot) : -1;

    bool        show = true;
    const char *text = NULp;

    if (is_bootstrap) {
        if (!show_boots || // hide bootstrap when disabled,
            bootstrap<bootstrap_min || // when outside of displayed range or
            bootstrap>bootstrap_max ||
            blen == 0.0) // when branch is part of a multifurcation (i.e. "does not exist")
        {
            show = false;
        }
        else {
            static GBS_strstruct buf(10);
            buf.erase();

            if (bootstrap<1) {
                buf.put('<');
                bootstrap = 1;
            }

            if (style == BS_FLOAT) {
                buf.nprintf(4, "%4.2f", double(bootstrap/100.0));
            }
            else {
                buf.nprintf(3, "%i", bootstrap);
                if (style == BS_PERCENT) {
                    buf.put('%');
                }
            }
            text = buf.get_data();
        }
    }
    else { // non-bootstrap remark (always shown)
        text = remark;
    }

    if (show_circle && is_bootstrap && show) {
        double radius = .01 * bootstrap; // bootstrap values are given in % (0..100)

        if (radius < .1) radius = .1;

        radius  = 1.0 / sqrt(radius); // -> bootstrap->radius : 100% -> 1, 0% -> inf
        radius -= 1.0;                // -> bootstrap->radius : 100% -> 0, 0% -> inf

        radius *= zoom_factor * 2;

        // Note : radius goes against infinite, if bootstrap values go towards zero
        //        For this reason we limit radius here:

        int gc = AWT_GC_BOOTSTRAP;
        if (radius > max_radius) {
            radius = max_radius;
            gc     = AWT_GC_BOOTSTRAP_LIMITED; // draw limited bootstraps in different color
        }

        const double radiusx        = radius * blen;     // multiply with length of branch (and zoomfactor)
        const bool   circleTooSmall = radiusx<0 || nearlyZero(radiusx);
        if (!circleTooSmall) {
            double radiusy;
            if (elipsoid) {
                radiusy = bdist;
                if (radiusy > radiusx) radiusy = radiusx;
            }
            else {
                radiusy = radiusx;
            }

            device->set_grey_level(gc, fill_level);
            device->circle(gc, AW::FillStyle::SHADED_WITH_BORDER, center, Vector(radiusx, radiusy), circle_filter);
        }
    }

    if (show) {
        td_assert(text);
        device->text(AWT_GC_BRANCH_REMARK, text, textpos, alignment, text_filter);
    }
}

void BootstrapConfig::display_node_remark(AW_device *device, const AP_tree *at, const AW::Position& center, double blen, double bdist, AW::RoughDirection textArea) const {
    td_assert(!at->is_leaf()); // leafs (should) have no remarks

    AW_pos   alignment = (textArea & D_WEST) ? 1.0 : 0.0;
    Position textpos(center);
    textpos.movey(scaled_remark_ascend*((textArea & D_SOUTH) ? 1.2 : ((textArea & D_NORTH) ? -0.1 : 0.5)));

    display_remark(device, at->get_remark(), center, blen, bdist, textpos, alignment);
}

static void AWT_graphic_tree_root_changed(void *cd, AP_tree *old, AP_tree *newroot) {
    AWT_graphic_tree *agt = (AWT_graphic_tree*)cd;
    if (agt->get_logical_root() == old || agt->get_logical_root()->is_inside(old)) {
        agt->set_logical_root_to(newroot);
    }
}

static void AWT_graphic_tree_node_deleted(void *cd, AP_tree *del) {
    AWT_graphic_tree *agt = (AWT_graphic_tree*)cd;
    if (agt->get_logical_root() == del) {
        agt->set_logical_root_to(agt->get_root_node());
    }
    if (agt->get_root_node() == del) {
        agt->set_logical_root_to(NULp);
    }
}

GB_ERROR AWT_graphic_tree::create_group(AP_tree *at) {
    GB_ERROR error = NULp;

    if (at->has_group_info()) {
        // only happens for nodes representing a keeled group
        td_assert(at->keelTarget());
        error = GBS_global_string("Cannot create group at position of keeled group '%s'", at->name);
    }
    else {
        char *gname = aw_input("Enter name of new group");
        if (gname && gname[0]) {
            GBDATA         *gb_tree  = tree_static->get_gb_tree();
            GBDATA         *gb_mainT = GB_get_root(gb_tree);
            GB_transaction  ta(gb_mainT);

            GBDATA *gb_node     = GB_create_container(gb_tree, "node");
            if (!gb_node) error = GB_await_error();

            if (at->gb_node) {                                     // already have existing node info (e.g. for linewidth)
                if (!error) error = GB_copy_dropProtectMarksAndTempstate(gb_node, at->gb_node); // copy existing node and ..
                if (!error) error = GB_delete(at->gb_node);        // .. delete old one (to trigger invalidation of taxonomy-cache)
            }

            if (!error) error = GBT_write_int(gb_node, "id", 0); // force re-creation of node-id

            if (!error) {
                error = GBT_write_name_to_groupData(gb_node, true, gname, true);
            }
            if (!error) exports.request_save();
            if (!error) {
                at->gb_node = gb_node;
                at->name    = gname;

                at->setKeeledState(0); // new group is always unkeeled
            }
            error = ta.close(error);
        }
    }

    return error;
}

void AWT_graphic_tree::toggle_group(AP_tree *at) {
    GB_ERROR error = NULp;

    if (at->is_leaf()) {
        error = "Please select an inner node to create a group";
    }
    else if (at->is_clade()) { // existing group
        bool     keeled = at->is_keeled_group(); // -> prefer keeled over normal group
        AP_tree *gat    = keeled ? at->get_father() : at; // node linked to group

        const char *msg = GBS_global_string("What to do with group '%s'?", gat->name);

        switch (aw_question(NULp, msg, "KeelOver,Rename,Destroy,Cancel" + (keeled ? 0 : 9)) - (keeled ? 1 : 0)) {
            case -1: { // keel over
                td_assert(keeled);
                dislocate_selected_group();
                gat->unkeelGroup();

                // need to flush taxonomy (otherwise group is displayed with leading '!'):
                GBDATA *gb_gname = GB_entry(gat->gb_node, "group_name");
                td_assert(gb_gname);
                GB_touch(gb_gname);

                exports.request_save();
                break;
            }
            case 0: { // rename
                char *new_gname = aw_input("Rename group", "Change group name:", gat->name);
                if (new_gname) {
                    error = GBT_write_name_to_groupData(gat->gb_node, true, new_gname, true);
                    if (!error) {
                        freeset(gat->name, new_gname);
                        select_group(at);
                        exports.request_save();
                    }
                }
                break;
            }

            case 1: // destroy
                if (selected_group.at_node(at)) deselect_group(); // deselect group only if selected group gets destroyed

                gat->gr.grouped = false;
                gat->name       = NULp;
                error           = GB_delete(gat->gb_node);        // ODD: expecting this to also destroy linewidth, rot and spread - but it doesn't!
                gat->gb_node    = NULp;

                if (!error) exports.request_save();      // ODD: even when commenting out this line info is not deleted
                break;

            case 2:  break; // cancel
            default: td_assert(0); break;
        }
    }
    else {
        error = create_group(at); // create new group
        if (!error && at->has_group_info()) {
            at->gr.grouped = true;
            select_group(at);
        }
    }

    if (error) aw_message(error);
}

class Dragged : public AWT_command_data {
    /*! Is dragged and can be dropped.
     * Knows how to indicate dragging.
     */
    AWT_graphic_exports& exports;

protected:
    AWT_graphic_exports& get_exports() { return exports; }

public:
    enum DragAction { DRAGGING, DROPPED };

    Dragged(AWT_graphic_exports& exports_) : exports(exports_) {}

    static bool valid_drag_device(AW_device *device) { return device->type() == AW_DEVICE_SCREEN; }

    virtual void draw_drag_indicator(AW_device *device, int drag_gc) const = 0;
    virtual void perform(DragAction action, const AW_clicked_element *target, const Position& mousepos) = 0;
    virtual void abort() = 0;

    void do_drag(const AW_clicked_element *drag_target, const Position& mousepos) {
        perform(DRAGGING, drag_target, mousepos);
    }
    void do_drop(const AW_clicked_element *drop_target, const Position& mousepos) {
        perform(DROPPED, drop_target, mousepos);
    }

    void hide_drag_indicator(AW_device *device, int drag_gc) const {
        // hide by XOR-drawing
        draw_drag_indicator(device, drag_gc);
    }
};

bool AWT_graphic_tree::warn_inappropriate_mode(AWT_COMMAND_MODE mode) {
    if (mode == AWT_MODE_ROTATE || mode == AWT_MODE_SPREAD) {
        if (tree_style != AP_TREE_RADIAL) {
            aw_message("Please select the radial tree display mode to use this command");
            return true;
        }
    }
    return false;
}

inline bool is_cursor_keycode(AW_key_code kcode) {
    return
        kcode == AW_KEY_UP ||
        kcode == AW_KEY_DOWN ||
        kcode == AW_KEY_LEFT ||
        kcode == AW_KEY_RIGHT ||
        kcode == AW_KEY_HOME ||
        kcode == AW_KEY_END;
}

AP_tree *AWT_graphic_tree::find_selected_node() const {
    /*! search node of selected species
     *  @return found node (NULp if none selected)
     */
    AP_tree *node = selSpec.get_node(); // node stored by last refresh
    if (!node && displayed_root && species_name[0]) {
        node = displayed_root->findLeafNamed(species_name);
    }
    return node;
}

AP_tree *AWT_graphic_tree::find_selected_group() {
    /*! search root-node of selected group
     * @return found node (NULp if none selected)
     */

    // @@@ could use selGroup to speed up search (if selected group was already drawn)
    AP_tree *node = NULp;
    if (selected_group.is_valid()) {
        if (!selected_group.is_located()) {
            selected_group.locate(get_root_node());
        }
        node = selected_group.get_node();
    }
    return node;
}


static GBDATA *brute_force_find_next_species(GBDATA *gb_main, GBDATA *gb_sel, bool marked_only, bool upwards) {
    if (upwards) {
        if (gb_sel && marked_only && !GB_read_flag(gb_sel)) gb_sel = GBT_next_marked_species(gb_sel);

        GBDATA *gb_prev = marked_only ? GBT_first_marked_species(gb_main) : GBT_first_species(gb_main);
        while (gb_prev) {
            GBDATA *gb_next = marked_only ? GBT_next_marked_species(gb_prev) : GBT_next_species(gb_prev);
            if (gb_next == gb_sel) {
                return gb_prev;
            }
            gb_prev = gb_next;
        }
        return gb_sel ? brute_force_find_next_species(gb_main, NULp, marked_only, upwards) : NULp;
    }

    // downwards
    GBDATA *gb_found = NULp;
    if (marked_only) {
        if (gb_sel) gb_found = GBT_next_marked_species(gb_sel);
        if (!gb_found) gb_found = GBT_first_marked_species(gb_main);
    }
    else {
        if (gb_sel) gb_found = GBT_next_species(gb_sel);
        if (!gb_found) gb_found = GBT_first_species(gb_main);
    }
    return gb_found;
}

class AP_tree_folding {
    AP_tree_set unfolded; // nodes which have been unfolded

    static void need_update(AP_tree*& subtree, AP_tree *node) {
        if (subtree) {
            subtree = DOWNCAST(AP_tree*, node->ancestor_common_with(subtree));
        }
        else {
            subtree = node;
        }
    }

public:

    AP_tree *unfold(const AP_tree_set& want_unfolded) { // set has to contain all parent group-nodes as well (use collect_enclosing_groups)
        AP_tree_set  keep_unfolded;
        AP_tree     *affected_subtree = NULp;
        for (AP_tree_set_citer g = want_unfolded.begin(); g != want_unfolded.end(); ++g) {
            AP_tree *node = *g;
            td_assert(node->has_group_info()); // ensure keeled groups add the parent node (where their group-info is stored!)
            if (node->gr.grouped) {
                node->gr.grouped = false;  // auto-unfold
                need_update(affected_subtree, node);
                keep_unfolded.insert(node);
            }
        }

        for (AP_tree_set_iter g = unfolded.begin(); g != unfolded.end(); ++g) {
            AP_tree *node = *g;
            td_assert(node->has_group_info());
            if (want_unfolded.find(node) == want_unfolded.end()) {
                node->gr.grouped = true; // auto-refold
                need_update(affected_subtree, node);
            }
            else {
                keep_unfolded.insert(node); // auto-refold later
            }
        }
        unfolded = keep_unfolded;
        return affected_subtree;
    }

    bool is_auto_unfolded() const { return !unfolded.empty(); }
    void forget() { unfolded.clear(); }

    bool node_is_auto_unfolded(AP_tree *node) const {
        return unfolded.find(node) != unfolded.end();
    }
};

void AWT_graphic_tree::auto_unfold(AP_tree *want_visible) {
    AP_tree_set parentGroups;
    if (want_visible) collect_enclosing_groups(want_visible, parentGroups);

    AP_tree *outdated_subtree = autoUnfolded->unfold(parentGroups);
    if (outdated_subtree) {
        fast_sync_changed_folding(outdated_subtree);
    }
}

void AWT_graphic_tree::forget_auto_unfolded() {
    autoUnfolded->forget();
}

bool AWT_graphic_tree::handle_cursor(AW_key_code kcode, AW_key_mod mod) {
    td_assert(is_cursor_keycode(kcode));

    bool handled = false;
    if (!displayed_root) return false;

    if (mod == AW_KEYMODE_NONE || mod == AW_KEYMODE_SHIFT || mod == AW_KEYMODE_CONTROL) { // jump next/prev (marked) species
        if (kcode != AW_KEY_LEFT && kcode != AW_KEY_RIGHT) { // cursor left/right not defined
            GBDATA  *gb_jump_to   = NULp;
            bool     marked_only  = (mod == AW_KEYMODE_CONTROL);

            bool upwards         = false;
            bool ignore_selected = false;

            switch (kcode) {
                case AW_KEY_HOME: ignore_selected = true; // fall-through
                case AW_KEY_DOWN: upwards = false; break;
                case AW_KEY_END:  ignore_selected = true; // fall-through
                case AW_KEY_UP:   upwards = true; break;
                default: break;
            }

            if (is_tree_style(tree_style)) {
                bool     descent_folded = marked_only || (mod == AW_KEYMODE_SHIFT);
                AP_tree *sel_node       = NULp;

                bool at_group = false;
                if (!ignore_selected) {
                    sel_node = find_selected_node();
                    if (!sel_node) {
                        sel_node = find_selected_group();
                        at_group = sel_node;
                    }
                }

                ARB_edge edge =
                    sel_node
                    ? (at_group
                       ? (upwards
                          ? ARB_edge(sel_node, sel_node->get_rightson()).previous()
                          : ARB_edge(sel_node->get_leftson(), sel_node))
                       : leafEdge(sel_node))
                    : rootEdge(get_tree_root());

                // limit edge iteration (to avoid deadlock, e.g. if all species are inside folded groups)
                int      maxIter      = ARB_edge::iteration_count(get_root_node()->gr.leaf_sum)+2;
                AP_tree *jump_to_node = NULp;

                while (!jump_to_node && maxIter-->0) {
                    edge = upwards ? edge.next() : edge.previous();
                    if (edge.is_edge_to_leaf()) {
                        AP_tree *leaf = DOWNCAST(AP_tree*, edge.dest());
                        if (leaf->gb_node                                                    &&       // skip zombies
                            (marked_only ? leaf->gr.mark_sum                                          // skip unmarked leafs
                             : implicated(!descent_folded, !leaf->is_inside_folded_group())) &&       // skip folded leafs if !descent_folded
                            implicated(is_logically_zoomed(), displayed_root->is_ancestor_of(leaf))) // stay inside logically zoomed subtree
                        {
                            jump_to_node = leaf;
                        }
                    }
                    // @@@ optimize: no need to descend into unmarked subtrees (if marked_only)
                    // @@@ optimize: no need to descend into folded subtrees (if !marked_only)
                }

                if (jump_to_node) {
                    // perform auto-unfolding unconditionally here
                    // (jump_to_node will only point to a hidden node here, if auto-unfolding shall happen)
                    auto_unfold(jump_to_node);
                    if (jump_to_node->is_leaf()) gb_jump_to = jump_to_node->gb_node; // select node (done below)
                }
            }
            else {
                if (nds_only_marked) marked_only      = true;
                if (!species_name[0]) ignore_selected = true;

                GBDATA *gb_sel = ignore_selected ? NULp : GBT_find_species(gb_main, species_name);
                gb_jump_to = brute_force_find_next_species(gb_main, gb_sel, marked_only, upwards);
            }

            if (gb_jump_to) {
                GB_transaction ta(gb_main);
                map_viewer_cb(gb_jump_to, ADMVT_SELECT);
                handled = true;
            }
        }
    }
    else if (mod == AW_KEYMODE_ALT) { // jump to groups
        if (is_tree_style(tree_style)) {
            AP_tree *start_node           = find_selected_group();
            bool     started_from_species = false;

            if (!start_node) { // if no group is selected => start at selected species
                start_node           = find_selected_node();
                started_from_species = start_node;
            }

            // if nothing selected -> 'iter' will point to first group (deepest one)
            GroupIterator  iter(start_node ? start_node : get_root_node());
            AP_tree       *unfold_to = NULp;

            bool at_target = false;
            if (started_from_species) {
                AP_tree *parentGroup = DOWNCAST(AP_tree*, start_node->find_parent_clade());

                if (parentGroup) {
                    iter      = GroupIterator(parentGroup);
                    at_target = (kcode == AW_KEY_LEFT || kcode == AW_KEY_RIGHT);
                }
                else {
                    at_target = true; // stick with default group
                }
            }

            while (!at_target) {
                AW_key_code  inject_kcode      = AW_KEY_NONE;
                int          start_clade_level = iter.get_clade_level();
                AP_tree     *start_group       = iter.node(); // abort iteration (handles cases where only 1 group is found)

                switch (kcode) {
                    case AW_KEY_UP:
                        do {
                            iter.previous();
                            if (iter.node() == start_group) break;
                        }
                        while (iter.get_clade_level() > start_clade_level);

                        if (iter.get_clade_level() < start_clade_level) {
                            iter         = GroupIterator(start_group);
                            inject_kcode = AW_KEY_END;
                        }
                        break;

                    case AW_KEY_DOWN:
                        if (start_node) { // otherwise iterator already points to wanted node
                            do {
                                iter.next();
                                if (iter.node() == start_group) break;
                            }
                            while (iter.get_clade_level() > start_clade_level);

                            if (iter.get_clade_level() < start_clade_level) {
                                iter         = GroupIterator(start_group);
                                inject_kcode = AW_KEY_HOME;
                            }
                        }
                        break;

                    case AW_KEY_HOME: {
                        AP_tree *parent = DOWNCAST(AP_tree*, iter.node()->find_parent_clade());
                        if (parent) {
                            iter         = GroupIterator(parent);
                            inject_kcode = AW_KEY_RIGHT;
                        }
                        else {
                            iter = GroupIterator(get_root_node());
                        }
                        break;
                    }
                    case AW_KEY_END: {
                        AP_tree *last_visited = NULp;
                        do {
                            if (iter.get_clade_level() == start_clade_level) {
                                last_visited = iter.node();
                            }
                            iter.next();
                            if (iter.node() == start_group) break;
                        }
                        while (iter.get_clade_level() >= start_clade_level);

                        td_assert(last_visited);
                        iter = GroupIterator(last_visited);
                        break;
                    }
                    case AW_KEY_LEFT: {
                        // first  keypress: fold if auto-unfolded
                        // second keypress: select parent
                        bool refoldFirst = autoUnfolded && autoUnfolded->node_is_auto_unfolded(start_group);
                        if (!refoldFirst) {
                            AP_tree *parent  = DOWNCAST(AP_tree*, iter.node()->find_parent_clade());
                            if (parent) iter = GroupIterator(parent);
                        }
                        // else just dont move (will refold group the group)
                        break;
                    }
                    case AW_KEY_RIGHT: {
                        iter.next();
                        if (iter.node()->find_parent_clade() != start_group) { // has no child group =>
                            iter      = GroupIterator(start_group); // set back ..
                            unfold_to = start_group->get_leftson(); // .. and temp. show group content
                        }
                        break;
                    }

                    default:
                        td_assert(0); // avoid
                        break;
                }

                if (inject_kcode == AW_KEY_NONE) at_target = true;
                else                             kcode     = inject_kcode; // simulate keystroke:
            }

            if (iter.valid()) {
                AP_tree *jump_to = iter.node();
                select_group(jump_to);
                auto_unfold(unfold_to ? unfold_to : jump_to);
#if defined(DEBUG)
                fprintf(stderr, "selected group '%s' (clade-level=%i)\n", jump_to->get_group_name(), jump_to->calc_clade_level());
#endif
            }
            else {
                deselect_group();
            }
            exports.request_refresh();
        }
    }

    return handled;
}

void AWT_graphic_tree::toggle_folding_at(AP_tree *at, bool force_jump) {
    if (at && !at->is_leaf() && at->is_clade()) {
        bool     wasFolded  = at->is_folded_group();
        AP_tree *top_change = NULp;

        if (at->is_normal_group()) {
            top_change = at;
            top_change->gr.grouped = !wasFolded;
        }
        if (at->is_keeled_group()) {
            top_change = at->get_father();
            top_change->gr.grouped = !wasFolded;
        }

        td_assert(top_change);

        if (!force_jump) {
            select_group(at);
        }
        fast_sync_changed_folding(top_change);
        if (force_jump) {
            deselect_group();
            select_group(at);
        }
    }
}

void AWT_graphic_tree::handle_key(AW_device *device, AWT_graphic_event& event) {
    //! handles AW_Event_Type = AW_Keyboard_Press.

    td_assert(event.type() == AW_Keyboard_Press);

    if (event.key_code() == AW_KEY_NONE) return;
    if (event.key_code() == AW_KEY_ASCII && event.key_char() == 0) return;

#if defined(DEBUG) && 0
    printf("key_char=%i (=%c)\n", int(event.key_char()), event.key_char());
#endif // DEBUG

    // ------------------------
    //      drag&drop keys

    if (event.key_code() == AW_KEY_ESCAPE) {
        AWT_command_data *cmddata = get_command_data();
        if (cmddata) {
            Dragged *dragging = dynamic_cast<Dragged*>(cmddata);
            if (dragging) {
                dragging->hide_drag_indicator(device, drag_gc);
                dragging->abort(); // abort what ever we did
                store_command_data(NULp);
            }
        }
    }

    // ----------------------------------------
    //      commands independent of tree :

    bool handled = true;
    switch (event.key_char()) {
        case 9: {     // Ctrl-i = invert all
            GBT_mark_all(gb_main, 2);
            exports.request_structure_update();
            break;
        }
        case 13: {     // Ctrl-m = mark/unmark all
            int mark   = !GBT_first_marked_species(gb_main); // no species marked -> mark all
            GBT_mark_all(gb_main, mark);
            exports.request_structure_update();
            break;
        }
        case ' ': { // Space = toggle mark(s) of selected species/group
            if (species_name[0]) {
                GB_transaction  ta(gb_main);
                GBDATA         *gb_species = GBT_find_species(gb_main, species_name);
                if (gb_species) {
                    GB_write_flag(gb_species, !GB_read_flag(gb_species));
                    exports.request_structure_update();
                }
            }
            else {
                AP_tree *subtree = find_selected_group();
                if (subtree) {
                    GB_transaction ta(gb_main);
                    mark_species_in_tree(subtree, !tree_has_marks(subtree));
                    exports.request_structure_update();
                }
            }
            break;
        }
        default: handled = false; break;
    }

    if (!handled) {
        handled = true;
        switch (event.key_code()) {
            case AW_KEY_RETURN: // Return = fold/unfold selected group
                toggle_folding_at(find_selected_group(), true);
                break;
            default: handled = false; break;
        }
    }

    // -------------------------
    //      cursor movement
    if (!handled && is_cursor_keycode(event.key_code())) {
        handled = handle_cursor(event.key_code(), event.key_modifier());
    }

    if (!handled) {
        // handle key events specific to pointed-to tree-element
        ClickedTarget pointed(this, event.best_click());

        if (pointed.species()) {
            handled = true;
            switch (event.key_char()) {
                case 'i':
                case 'm': {     // i/m = mark/unmark species
                    GB_write_flag(pointed.species(), 1-GB_read_flag(pointed.species()));
                    exports.request_structure_update();
                    break;
                }
                case 'I': {     // I = invert all but current
                    int mark = GB_read_flag(pointed.species());
                    GBT_mark_all(gb_main, 2);
                    GB_write_flag(pointed.species(), mark);
                    exports.request_structure_update();
                    break;
                }
                case 'M': {     // M = mark/unmark all but current
                    int mark = GB_read_flag(pointed.species());
                    GB_write_flag(pointed.species(), 0); // unmark current
                    GBT_mark_all(gb_main, !GBT_first_marked_species(gb_main));
                    GB_write_flag(pointed.species(), mark); // restore mark of current
                    exports.request_structure_update();
                    break;
                }
                default: handled = false; break;
            }
        }

        if (!handled && event.key_char() == '0') {
            // handle reset-key promised by
            // - KEYINFO_ABORT_AND_RESET (AWT_MODE_ROTATE, AWT_MODE_LENGTH, AWT_MODE_MULTIFURC, AWT_MODE_LINE, AWT_MODE_SPREAD)
            // - KEYINFO_RESET (AWT_MODE_LZOOM)

            if (event.cmd() == AWT_MODE_LZOOM) {
                set_logical_root_to(get_root_node());
                exports.request_zoom_reset();
            }
            else if (pointed.is_ruler()) {
                GBDATA *gb_tree = tree_static->get_gb_tree();
                td_assert(gb_tree);

                switch (event.cmd()) {
                    case AWT_MODE_ROTATE: break; // ruler has no rotation
                    case AWT_MODE_SPREAD: break; // ruler has no spread
                    case AWT_MODE_LENGTH: {
                        GB_transaction ta(gb_tree);
                        GBDATA *gb_ruler_size = GB_searchOrCreate_float(gb_tree, RULER_SIZE, DEFAULT_RULER_LENGTH);
                        GB_write_float(gb_ruler_size, DEFAULT_RULER_LENGTH);
                        exports.request_structure_update();
                        break;
                    }
                    case AWT_MODE_LINE: {
                        GB_transaction ta(gb_tree);
                        GBDATA *gb_ruler_width = GB_searchOrCreate_int(gb_tree, RULER_LINEWIDTH, DEFAULT_RULER_LINEWIDTH);
                        GB_write_int(gb_ruler_width, DEFAULT_RULER_LINEWIDTH);
                        exports.request_structure_update();
                        break;
                    }
                    default: break;
                }
            }
            else if (pointed.node()) {
                if (warn_inappropriate_mode(event.cmd())) return;
                switch (event.cmd()) {
                    case AWT_MODE_ROTATE:    pointed.node()->reset_subtree_angles();         exports.request_save(); break;
                    case AWT_MODE_LENGTH:    pointed.node()->set_branchlength_unrooted(0.0); exports.request_save(); break;
                    case AWT_MODE_MULTIFURC: pointed.node()->multifurcate();                 exports.request_save(); break;
                    case AWT_MODE_LINE:      pointed.node()->reset_subtree_linewidths();     exports.request_save(); break;
                    case AWT_MODE_SPREAD:    pointed.node()->reset_subtree_spreads();        exports.request_save(); break;
                    default: break;
                }
            }
            return;
        }

        if (!handled && pointed.node()) {
            handled = true;
            switch (event.key_char()) {
                case 'm': {     // m = mark/unmark (sub)tree
                    GB_transaction ta(gb_main);
                    mark_species_in_tree(pointed.node(), !tree_has_marks(pointed.node()));
                    exports.request_structure_update();
                    break;
                }
                case 'M': {     // M = mark/unmark all but (sub)tree
                    GB_transaction ta(gb_main);
                    mark_species_in_rest_of_tree(pointed.node(), !rest_tree_has_marks(pointed.node()));
                    exports.request_structure_update();
                    break;
                }
                // @@@ add hotkeys to count marked (subtree + resttree)?

                case 'i': {     // i = invert (sub)tree
                    GB_transaction ta(gb_main);
                    mark_species_in_tree(pointed.node(), 2);
                    exports.request_structure_update();
                    break;
                }
                case 'I': {     // I = invert all but (sub)tree
                    GB_transaction ta(gb_main);
                    mark_species_in_rest_of_tree(pointed.node(), 2);
                    exports.request_structure_update();
                    break;
                }
                case 'c':
                case 'x': {
                    AWT_graphic_tree_group_state  state;
                    AP_tree                      *at = pointed.node();

                    detect_group_state(at, &state, NULp);

                    if (!state.has_groups()) { // somewhere inside group
do_parent :
                        at  = at->get_father();
                        while (at) {
                            if (at->is_normal_group()) break;
                            at = at->get_father();
                        }

                        if (at) {
                            state.clear();
                            detect_group_state(at, &state, NULp);
                        }
                    }

                    if (at) {
                        CollapseMode next_group_mode;

                        if (event.key_char() == 'x') {  // expand
                            next_group_mode = state.next_expand_mode();
                        }
                        else { // collapse
                            if (state.all_closed()) goto do_parent;
                            next_group_mode = state.next_collapse_mode();
                        }

                        group_tree(at, next_group_mode, 0);
                        fast_sync_changed_folding(at);
                    }
                    break;
                }
                case 'C':
                case 'X': {
                    AP_tree *root_node = pointed.node();
                    while (root_node->father) root_node = root_node->get_father(); // search father

                    td_assert(root_node);

                    AWT_graphic_tree_group_state state;
                    detect_group_state(root_node, &state, pointed.node());

                    CollapseMode next_group_mode;
                    if (event.key_char() == 'X') {  // expand
                        next_group_mode = state.next_expand_mode();
                    }
                    else { // collapse
                        next_group_mode = state.next_collapse_mode();
                    }

                    group_rest_tree(pointed.node(), next_group_mode, 0);
                    fast_sync_changed_folding(root_node);

                    break;
                }
                default: handled = false; break;
            }
        }
    }
}

static bool command_on_GBDATA(GBDATA *gbd, const AWT_graphic_event& event, AD_map_viewer_cb map_viewer_cb) {
    // modes listed here are available in ALL tree-display-modes (i.e. as well in listmode)

    bool refresh = false;

    if (event.type() == AW_Mouse_Press && event.button() != AW_BUTTON_MIDDLE) {
        AD_MAP_VIEWER_TYPE selectType = ADMVT_NONE;

        switch (event.cmd()) {
            case AWT_MODE_MARK: // see also .@OTHER_MODE_MARK_HANDLER
                switch (event.button()) {
                    case AW_BUTTON_LEFT:
                        GB_write_flag(gbd, 1);
                        selectType = ADMVT_SELECT;
                        break;
                    case AW_BUTTON_RIGHT:
                        GB_write_flag(gbd, 0);
                        break;
                    default:
                        break;
                }
                refresh = true;
                break;

            case AWT_MODE_WWW:  selectType = ADMVT_WWW;    break;
            case AWT_MODE_INFO: selectType = ADMVT_INFO;   break;
            default:            selectType = ADMVT_SELECT; break;
        }

        if (selectType != ADMVT_NONE) {
            map_viewer_cb(gbd, selectType);
            refresh = true;
        }
    }

    return refresh;
}

class ClickedElement {
    /*! Stores a copy of AW_clicked_element.
     * Used as Drag&Drop source and target.
     */
    AW_clicked_element *elem;

public:
    ClickedElement(const AW_clicked_element& e) : elem(e.clone()) {}
    ClickedElement(const ClickedElement& other) : elem(other.element()->clone()) {}
    DECLARE_ASSIGNMENT_OPERATOR(ClickedElement);
    ~ClickedElement() { delete elem; }

    const AW_clicked_element *element() const { return elem; }

    bool operator == (const ClickedElement& other) const { return *element() == *other.element(); }
    bool operator != (const ClickedElement& other) const { return !(*this == other); }
};

class DragNDrop : public Dragged {
    ClickedElement Drag, Drop;

    virtual void perform_drop() = 0;

    void drag(const AW_clicked_element *drag_target)  {
        Drop = drag_target ? *drag_target : Drag;
    }
    void drop(const AW_clicked_element *drop_target) {
        drag(drop_target);
        perform_drop();
    }

    void perform(DragAction action, const AW_clicked_element *target, const Position&) FINAL_OVERRIDE {
        switch (action) {
            case DRAGGING: drag(target); break;
            case DROPPED:  drop(target); break;
        }
    }

    void abort() OVERRIDE {
        perform(DROPPED, Drag.element(), Position()); // drop dragged element onto itself to abort
    }

protected:
    const AW_clicked_element *source_element() const { return Drag.element(); }
    const AW_clicked_element *dest_element() const { return Drop.element(); }

public:
    DragNDrop(const AW_clicked_element *dragFrom, AWT_graphic_exports& exports_) :
        Dragged(exports_),
        Drag(*dragFrom),
        Drop(Drag)
    {}

    void draw_drag_indicator(AW_device *device, int drag_gc) const FINAL_OVERRIDE {
        td_assert(valid_drag_device(device));
        source_element()->indicate_selected(device, drag_gc);
        if (Drag != Drop) {
            dest_element()->indicate_selected(device, drag_gc);
            device->line(drag_gc, source_element()->get_connecting_line(*dest_element()));
        }
    }
};

class BranchMover : public DragNDrop {
    AW_MouseButton    button;
    AWT_graphic_tree& agt;

    void perform_drop() OVERRIDE {
        ClickedTarget source(source_element());
        ClickedTarget dest(dest_element());

        if (source.node() && dest.node() && source.node() != dest.node()) {
            GB_ERROR  error   = NULp;
            GBDATA   *gb_node = source.node()->gb_node;
            agt.deselect_group();
            switch (button) {
                case AW_BUTTON_LEFT:
                    error = source.node()->cantMoveNextTo(dest.node());
                    if (!error) source.node()->moveNextTo(dest.node(), dest.get_rel_attach());
                    break;

                case AW_BUTTON_RIGHT:
                    error = source.node()->move_group_to(dest.node());
                    break;
                default:
                    td_assert(0);
                    break;
            }

            if (error) {
                aw_message(error);
            }
            else {
                get_exports().request_save();
                bool group_moved = !source.node()->is_leaf() && source.node()->is_normal_group();
                if (group_moved) agt.select_group(gb_node);
            }
        }
        else {
#if defined(DEBUG)
            if (!source.node()) printf("No source.node\n");
            if (!dest.node()) printf("No dest.node\n");
            if (dest.node() == source.node()) printf("source==dest\n");
#endif
        }
    }

public:
    BranchMover(const AW_clicked_element *dragFrom, AW_MouseButton button_, AWT_graphic_tree& agt_) :
        DragNDrop(dragFrom, agt_.exports),
        button(button_),
        agt(agt_)
    {}
};


class Scaler : public Dragged {
    Position mouse_start; // screen-coordinates
    Position last_drag_pos;
    double unscale;

    virtual void draw_scale_indicator(const AW::Position& drag_pos, AW_device *device, int drag_gc) const = 0;
    virtual void do_scale(const Position& drag_pos) = 0;

    void perform(DragAction action, const AW_clicked_element *, const Position& mousepos) FINAL_OVERRIDE {
        switch (action) {
            case DRAGGING:
                last_drag_pos = mousepos;
                FALLTHROUGH; // aka instantly apply drop-action while dragging
            case DROPPED:
                do_scale(mousepos);
                break;
        }
    }
    void abort() OVERRIDE {
        perform(DROPPED, NULp, mouse_start); // drop exactly where dragging started
    }


protected:
    const Position& startpos() const { return mouse_start; }
    Vector scaling(const Position& current) const { return Vector(mouse_start, current)*unscale; } // returns world-coordinates

public:
    Scaler(const Position& start, double unscale_, AWT_graphic_exports& exports_)
        : Dragged(exports_),
          mouse_start(start),
          last_drag_pos(start),
          unscale(unscale_)
    {
        td_assert(!is_nan_or_inf(unscale));
    }

    void draw_drag_indicator(AW_device *device, int drag_gc) const FINAL_OVERRIDE {
        draw_scale_indicator(last_drag_pos, device, drag_gc);
    }
};

inline double discrete_value(double analog_value, int discretion_factor) {
    // discretion_factor:
    //     10 -> 1st digit behind dot
    //    100 -> 2nd ------- " ------
    //   1000 -> 3rd ------- " ------

    if (analog_value<0.0) return -discrete_value(-analog_value, discretion_factor);
    return int(analog_value*discretion_factor+0.5)/double(discretion_factor);
}

class DB_scalable {
    //! a DB entry scalable by dragging
    GBDATA   *gbd;
    GB_TYPES  type;
    float     min;
    float     max;
    int       discretion_factor;
    bool      inversed;

    static CONSTEXPR double INTSCALE = 100.0;

    void init() {
        min = -DBL_MAX;
        max =  DBL_MAX;

        discretion_factor = 0;
        inversed          = false;
    }

public:
    DB_scalable() : gbd(NULp), type(GB_NONE) { init(); }
    DB_scalable(GBDATA *gbd_) : gbd(gbd_), type(GB_read_type(gbd)) { init(); }

    GBDATA *data() { return gbd; }

    float read() {
        float res = 0.0;
        switch (type) {
            case GB_FLOAT: res = GB_read_float(gbd);        break;
            case GB_INT:   res = GB_read_int(gbd)/INTSCALE; break;
            default: break;
        }
        return inversed ? -res : res;
    }
    bool write(float val) {
        float old = read();

        if (inversed) val = -val;

        val = val<=min ? min : (val>=max ? max : val);
        val = discretion_factor ? discrete_value(val, discretion_factor) : val;

        switch (type) {
            case GB_FLOAT:
                GB_write_float(gbd, val);
                break;
            case GB_INT:
                GB_write_int(gbd, int(val*INTSCALE+0.5));
                break;
            default: break;
        }

        return old != read();
    }

    void set_discretion_factor(int df) { discretion_factor = df; }
    void set_min(float val) { min = (type == GB_INT) ? val*INTSCALE : val; }
    void set_max(float val) { max = (type == GB_INT) ? val*INTSCALE : val; }
    void inverse() { inversed = !inversed; }
};

class RulerScaler : public Scaler { // derived from Noncopyable
    Position    awar_start;
    DB_scalable x, y; // DB entries scaled by x/y movement

    GBDATA *gbdata() {
        GBDATA *gbd   = x.data();
        if (!gbd) gbd = y.data();
        td_assert(gbd);
        return gbd;
    }

    Position read_pos() { return Position(x.read(), y.read()); }
    bool write_pos(Position p) {
        bool xchanged = x.write(p.xpos());
        bool ychanged = y.write(p.ypos());
        return xchanged || ychanged;
    }

    void draw_scale_indicator(const AW::Position& , AW_device *, int) const {}
    void do_scale(const Position& drag_pos) {
        GB_transaction ta(gbdata());
        if (write_pos(awar_start+scaling(drag_pos))) get_exports().request_refresh();
    }
public:
    RulerScaler(const Position& start, double unscale_, const DB_scalable& xs, const DB_scalable& ys, AWT_graphic_exports& exports_)
        : Scaler(start, unscale_, exports_),
          x(xs),
          y(ys)
    {
        GB_transaction ta(gbdata());
        awar_start = read_pos();
    }
};

static void text_near_head(AW_device *device, int gc, const LineVector& line, const char *text) {
    // @@@ should keep a little distance between the line-head and the text (depending on line orientation)
    Position at = line.head();
    device->text(gc, text, at);
}

enum ScaleMode { SCALE_LENGTH, SCALE_LENGTH_PRESERVING, SCALE_SPREAD };

class BranchScaler : public Scaler { // derived from Noncopyable
    ScaleMode  mode;
    AP_tree   *node;

    float start_val;   // length or spread
    bool  zero_val_removed;

    LineVector branch;
    Position   attach; // point on 'branch' (next to click position)

    int discretion_factor;  // !=0 = > scale to discrete values

    bool allow_neg_val;

    float get_val() const {
        switch (mode) {
            case SCALE_LENGTH_PRESERVING:
            case SCALE_LENGTH: return node->get_branchlength_unrooted();
            case SCALE_SPREAD: return node->gr.spread;
        }
        td_assert(0);
        return 0.0;
    }
    void set_val(float val) {
        switch (mode) {
            case SCALE_LENGTH_PRESERVING: node->set_branchlength_preserving(val); break;
            case SCALE_LENGTH: node->set_branchlength_unrooted(val); break;
            case SCALE_SPREAD: node->gr.spread = val; break;
        }
    }

    void init_discretion_factor(bool discrete) {
        if (start_val != 0 && discrete) {
            discretion_factor = 10;
            while ((start_val*discretion_factor)<1) {
                discretion_factor *= 10;
            }
        }
        else {
            discretion_factor = 0;
        }
    }

    Position get_dragged_attach(const AW::Position& drag_pos) const {
        // return dragged position of 'attach'
        Vector moved      = scaling(drag_pos);
        Vector attach2tip = branch.head()-attach;

        if (attach2tip.length()>0) {
            Vector   moveOnBranch = orthogonal_projection(moved, attach2tip);
            return attach+moveOnBranch;
        }
        Vector attach2base = branch.start()-attach;
        if (attach2base.length()>0) {
            Vector moveOnBranch = orthogonal_projection(moved, attach2base);
            return attach+moveOnBranch;
        }
        return Position(); // no position
    }


    void draw_scale_indicator(const AW::Position& drag_pos, AW_device *device, int drag_gc) const {
        td_assert(valid_drag_device(device));
        Position attach_dragged = get_dragged_attach(drag_pos);
        if (attach_dragged.valid()) {
            Position   drag_world = device->rtransform(drag_pos);
            LineVector to_dragged(attach_dragged, drag_world);
            LineVector to_start(attach, -to_dragged.line_vector());

            device->set_line_attributes(drag_gc, 1, AW_SOLID);

            device->line(drag_gc, to_start);
            device->line(drag_gc, to_dragged);

            text_near_head(device, drag_gc, to_start,   GBS_global_string("old=%.3f", start_val));
            text_near_head(device, drag_gc, to_dragged, GBS_global_string("new=%.3f", get_val()));
        }

        device->set_line_attributes(drag_gc, 3, AW_SOLID);
        device->line(drag_gc, branch);
    }

    void do_scale(const Position& drag_pos) {
        double oldval = get_val();

        if (start_val == 0.0) { // can't scale
            if (!zero_val_removed) {
                switch (mode) {
                    case SCALE_LENGTH:
                    case SCALE_LENGTH_PRESERVING:
                        set_val(tree_defaults::LENGTH); // fake branchlength (can't scale zero-length branches)
                        aw_message("Cannot scale zero sized branches\nBranchlength has been set to 0.1\nNow you may scale the branch");
                        break;
                    case SCALE_SPREAD:
                        set_val(tree_defaults::SPREAD); // reset spread (can't scale unspreaded branches)
                        aw_message("Cannot spread unspreaded branches\nSpreading has been set to 1.0\nNow you may spread the branch"); // @@@ clumsy
                        break;
                }
                zero_val_removed = true;
            }
        }
        else {
            Position attach_dragged = get_dragged_attach(drag_pos);
            if (attach_dragged.valid()) {
                Vector to_attach(branch.start(), attach);
                Vector to_attach_dragged(branch.start(), attach_dragged);

                double tal = to_attach.length();
                double tdl = to_attach_dragged.length();

                if (tdl>0.0 && tal>0.0) {
                    bool   negate = are_antiparallel(to_attach, to_attach_dragged);
                    double scale  = tdl/tal * (negate ? -1 : 1);

                    float val = start_val * scale;
                    if (val<0.0) {
                        if (node->is_leaf() || !allow_neg_val) {
                            val = 0.0; // do NOT accept negative values
                        }
                    }
                    if (discretion_factor) {
                        val = discrete_value(val, discretion_factor);
                    }
                    set_val(NONAN(val));
                }
            }
        }

        if (oldval != get_val()) {
            get_exports().request_save();
        }
    }

public:

    BranchScaler(ScaleMode mode_, AP_tree *node_, const LineVector& branch_, const Position& attach_, const Position& start, double unscale_, bool discrete, bool allow_neg_values_, AWT_graphic_exports& exports_)
        : Scaler(start, unscale_, exports_),
          mode(mode_),
          node(node_),
          start_val(get_val()),
          zero_val_removed(false),
          branch(branch_),
          attach(attach_),
          allow_neg_val(allow_neg_values_)
    {
        init_discretion_factor(discrete);
    }
};

class BranchLinewidthScaler : public Scaler, virtual Noncopyable {
    AP_tree *node;
    int      start_width;
    bool     wholeSubtree;

public:
    BranchLinewidthScaler(AP_tree *node_, const Position& start, bool wholeSubtree_, AWT_graphic_exports& exports_)
        : Scaler(start, 0.1, exports_), // 0.1 = > change linewidth dragpixel/10
          node(node_),
          start_width(node->get_linewidth()),
          wholeSubtree(wholeSubtree_)
    {}

    void draw_scale_indicator(const AW::Position& , AW_device *, int) const OVERRIDE {}
    void do_scale(const Position& drag_pos) OVERRIDE {
        Vector moved = scaling(drag_pos);
        double ymove = -moved.y();
        int    old   = node->get_linewidth();

        int width = start_width + ymove;
        if (width<tree_defaults::LINEWIDTH) width = tree_defaults::LINEWIDTH;

        if (width != old) {
            if (wholeSubtree) {
                node->set_linewidth_recursive(width);
            }
            else {
                node->set_linewidth(width);
            }
            get_exports().request_save();
        }
    }
};

class BranchRotator FINAL_TYPE : public Dragged, virtual Noncopyable {
    AW_device  *device;
    AP_tree    *node;
    LineVector  clicked_branch;
    float       orig_angle;      // of node
    Position    hinge;
    Position    mousepos_world;

    void perform(DragAction, const AW_clicked_element *, const Position& mousepos) OVERRIDE {
        mousepos_world = device->rtransform(mousepos);

        double prev_angle = node->get_angle();

        Angle current(hinge, mousepos_world);
        Angle orig(clicked_branch.line_vector());
        Angle diff = current-orig;

        node->set_angle(orig_angle + diff.radian());

        if (node->get_angle() != prev_angle) get_exports().request_save();
    }

    void abort() OVERRIDE {
        node->set_angle(orig_angle);
        get_exports().request_save();
    }

public:
    BranchRotator(AW_device *device_, AP_tree *node_, const LineVector& clicked_branch_, const Position& mousepos, AWT_graphic_exports& exports_)
        : Dragged(exports_),
          device(device_),
          node(node_),
          clicked_branch(clicked_branch_),
          orig_angle(node->get_angle()),
          hinge(clicked_branch.start()),
          mousepos_world(device->rtransform(mousepos))
    {
        td_assert(valid_drag_device(device));
    }

    void draw_drag_indicator(AW_device *IF_DEBUG(same_device), int drag_gc) const OVERRIDE {
        td_assert(valid_drag_device(same_device));
        td_assert(device == same_device);

        device->line(drag_gc, clicked_branch);
        device->line(drag_gc, LineVector(hinge, mousepos_world));
        device->circle(drag_gc, AW::FillStyle::EMPTY, hinge, device->rtransform(Vector(5, 5)));
    }
};

inline Position calc_text_coordinates_near_tip(AW_device *device, int gc, const Position& pos, const Angle& orientation, AW_pos& alignment, double dist_factor = 1.0) {
    /*! calculates text coordinates for text placed at the tip of a vector
     * @param device      output device
     * @param gc          context
     * @param pos         tip of the vector (world coordinates)
     * @param orientation orientation of the vector (towards its tip)
     * @param alignment   result param (alignment for call to text())
     * @param dist_factor normally 1.0 (smaller => text nearer towards 'pos')
     */
    const AW_font_limits& charLimits = device->get_font_limits(gc, 'A');

    const double text_height = charLimits.get_height() * device->get_unscale();
    const double dist        = text_height * dist_factor;

    Vector shift = orientation.normal();
    // use sqrt of sin(=y) to move text faster between positions below and above branch:
    shift.sety(shift.y()>0 ? sqrt(shift.y()) : -sqrt(-shift.y()));

    Position near = pos + dist*shift;
    near.movey(.3*text_height); // @@@ just a hack. fix.

    alignment = .5 - .5*orientation.cos();

    return near;
}

inline Position calc_text_coordinates_aside_line(AW_device *device, int gc, const Position& pos, Angle orientation, bool right, AW_pos& alignment, double dist_factor = 1.0) {
    /*! calculates text coordinates for text placed aside of a vector
     * @param device      output device
     * @param gc          context
     * @param pos         position on the vector, e.g. center of vector (world coordinates)
     * @param orientation orientation of the vector (towards its tip)
     * @param right       true -> on right side of vector (otherwise on left side)
     * @param alignment   result param (alignment for call to text())
     * @param dist_factor normally 1.0 (smaller => text nearer towards 'pos')
     */

    return calc_text_coordinates_near_tip(device, gc, pos, right ? orientation.rotate90deg() : orientation.rotate270deg(), alignment, dist_factor);
}

class MarkerIdentifier : public Dragged, virtual Noncopyable {
    AW_clicked_element *marker; // maybe box, line or text!
    Position            click;
    std::string         name;

    void draw_drag_indicator(AW_device *device, int drag_gc) const OVERRIDE {
        Position  click_world = device->rtransform(click);
        Rectangle bbox        = marker->get_bounding_box();
        Position  center      = bbox.centroid();

        Vector toClick(center, click_world);
        {
            double minLen = Vector(center, bbox.nearest_corner(click_world)).length();
            if (toClick.length()<minLen) toClick.set_length(minLen);
        }
        LineVector toHead(center, 1.5*toClick);

        marker->indicate_selected(device, drag_gc);
        device->line(drag_gc, toHead);

        Angle    orientation(toHead.line_vector());
        AW_pos   alignment;
        Position textPos = calc_text_coordinates_near_tip(device, drag_gc, toHead.head(), Angle(toHead.line_vector()), alignment);

        device->text(drag_gc, name.c_str(), textPos, alignment);
    }
    void perform(DragAction, const AW_clicked_element*, const Position& mousepos) OVERRIDE {
        click = mousepos;
        get_exports().request_refresh();
    }
    void abort() OVERRIDE {
        get_exports().request_refresh();
    }

public:
    MarkerIdentifier(const AW_clicked_element *marker_, const Position& start, const char *name_, AWT_graphic_exports& exports_)
        : Dragged(exports_),
          marker(marker_->clone()),
          click(start),
          name(name_)
    {
        get_exports().request_refresh();
    }
    ~MarkerIdentifier() {
        delete marker;
    }

};

static AW_device_click::ClickPreference preferredForCommand(AWT_COMMAND_MODE mode) {
    // return preferred click target for tree-display
    // (Note: not made this function a member of AWT_graphic_event,
    //  since modes are still reused in other ARB applications,
    //  e.g. AWT_MODE_ROTATE in SECEDIT)

    switch (mode) {
        case AWT_MODE_LENGTH:
        case AWT_MODE_MULTIFURC:
        case AWT_MODE_SPREAD:
            return AW_device_click::PREFER_LINE;

        default:
            return AW_device_click::PREFER_NEARER;
    }
}

void AWT_graphic_tree::handle_command(AW_device *device, AWT_graphic_event& event) {
    td_assert(event.button()!=AW_BUTTON_MIDDLE); // shall be handled by caller

    if (!tree_static) return;                      // no tree -> no commands

    if (event.type() == AW_Keyboard_Release) return;
    if (event.type() == AW_Keyboard_Press) return handle_key(device, event);

    // @@@ move code below into separate member function handle_mouse()

    if (event.button() != AW_BUTTON_LEFT && event.button() != AW_BUTTON_RIGHT) return; // nothing else is currently handled here

    ClickedTarget clicked(this, event.best_click(preferredForCommand(event.cmd())));
    // Note: during drag/release 'clicked'
    //       - contains drop-target (only if AWT_graphic::drag_target_detection is requested)
    //       - no longer contains initially clicked element (in all other modes)
    // see also ../CANVAS/canvas.cxx@motion_event

    if (clicked.species()) {
        if (command_on_GBDATA(clicked.species(), event, map_viewer_cb)) {
            exports.request_refresh();
        }
        return;
    }

    if (!tree_static->get_root_node()) return; // no tree -> no commands

    const Position&  mousepos = event.position();

    // -------------------------------------
    //      generic drag & drop handler
    {
        AWT_command_data *cmddata = get_command_data();
        if (cmddata) {
            Dragged *dragging = dynamic_cast<Dragged*>(cmddata);
            if (dragging) {
                dragging->hide_drag_indicator(device, drag_gc);
                if (event.type() == AW_Mouse_Press) {
                    // mouse pressed while dragging (e.g. press other button)
                    dragging->abort(); // abort what ever we did
                    store_command_data(NULp);
                }
                else {
                    switch (event.type()) {
                        case AW_Mouse_Drag:
                            dragging->do_drag(clicked.element(), mousepos);
                            dragging->draw_drag_indicator(device, drag_gc);
                            break;

                        case AW_Mouse_Release:
                            dragging->do_drop(clicked.element(), mousepos);
                            store_command_data(NULp);
                            break;
                        default:
                            break;
                    }
                }
                return;
            }
        }
    }

    if (event.type() != AW_Mouse_Press) return; // no drag/drop handling below!

    if (clicked.is_ruler()) {
        DB_scalable  xdata;
        DB_scalable  ydata;
        double       unscale = device->get_unscale();
        GBDATA      *gb_tree = tree_static->get_gb_tree();

        switch (event.cmd()) {
            case AWT_MODE_LENGTH:
            case AWT_MODE_MULTIFURC: { // scale ruler
                xdata = GB_searchOrCreate_float(gb_tree, RULER_SIZE, DEFAULT_RULER_LENGTH);

                double rel  = clicked.get_rel_attach();
                if (tree_style == AP_TREE_IRS) {
                    unscale /= (rel-1)*irs_tree_ruler_scale_factor; // ruler has opposite orientation in IRS mode
                }
                else {
                    unscale /= rel;
                }

                if (event.button() == AW_BUTTON_RIGHT) xdata.set_discretion_factor(10);
                xdata.set_min(0.01);
                break;
            }
            case AWT_MODE_LINE: // scale ruler linewidth
                ydata = GB_searchOrCreate_int(gb_tree, RULER_LINEWIDTH, DEFAULT_RULER_LINEWIDTH);
                ydata.set_min(0);
                ydata.inverse();
                break;

            default: { // move ruler or ruler text
                bool isText = clicked.is_text();
                xdata = GB_searchOrCreate_float(gb_tree, ruler_awar(isText ? "text_x" : "ruler_x"), 0.0);
                ydata = GB_searchOrCreate_float(gb_tree, ruler_awar(isText ? "text_y" : "ruler_y"), 0.0);
                break;
            }
        }
        if (!is_nan_or_inf(unscale)) {
            store_command_data(new RulerScaler(mousepos, unscale, xdata, ydata, exports));
        }
        return;
    }

    if (clicked.is_marker()) {
        if (clicked.element()->get_distance() <= 3) { // accept 3 pixel distance
            display_markers->handle_click(clicked.get_markerindex(), event.button(), exports);
            if (event.button() == AW_BUTTON_LEFT) {
                const char *name = display_markers->get_marker_name(clicked.get_markerindex());
                store_command_data(new MarkerIdentifier(clicked.element(), mousepos, name, exports));
            }
        }
        return;
    }

    if (warn_inappropriate_mode(event.cmd())) {
        return;
    }

    switch (event.cmd()) {
        // -----------------------------
        //      two point commands:

        case AWT_MODE_MOVE:
            if (clicked.node() && clicked.node()->father) {
                drag_target_detection(true);
                BranchMover *mover = new BranchMover(clicked.element(), event.button(), *this);
                store_command_data(mover);
                mover->draw_drag_indicator(device, drag_gc);
            }
            break;

        case AWT_MODE_LENGTH:
        case AWT_MODE_MULTIFURC:
            if (clicked.node() && clicked.is_branch()) {
                bool allow_neg_branches = aw_root->awar(AWAR_EXPERT)->read_int();
                bool discrete_lengths   = event.button() == AW_BUTTON_RIGHT;

                const AW_clicked_line *cl = dynamic_cast<const AW_clicked_line*>(clicked.element());
                td_assert(cl);

                ScaleMode     mode   = event.cmd() == AWT_MODE_LENGTH ? SCALE_LENGTH : SCALE_LENGTH_PRESERVING;
                BranchScaler *scaler = new BranchScaler(mode, clicked.node(), cl->get_line(), clicked.element()->get_attach_point(), mousepos, device->get_unscale(), discrete_lengths, allow_neg_branches, exports);

                store_command_data(scaler);
                scaler->draw_drag_indicator(device, drag_gc);
            }
            break;

        case AWT_MODE_ROTATE:
            if (clicked.node()) {
                BranchRotator *rotator = NULp;
                if (clicked.is_branch()) {
                    const AW_clicked_line *cl = dynamic_cast<const AW_clicked_line*>(clicked.element());
                    td_assert(cl);
                    rotator = new BranchRotator(device, clicked.node(), cl->get_line(), mousepos, exports);
                }
                else { // rotate branches inside a folded group (allows to modify size of group triangle)
                    const AW_clicked_polygon *poly = dynamic_cast<const AW_clicked_polygon*>(clicked.element());
                    if (poly) {
                        int                 npos;
                        const AW::Position *pos = poly->get_polygon(npos);

                        if (npos == 3) { // only makes sense in radial mode (which uses triangles)
                            LineVector left(pos[0], pos[1]);
                            LineVector right(pos[0], pos[2]);

                            Position mousepos_world = device->rtransform(mousepos);

                            if (Distance(mousepos_world, left) < Distance(mousepos_world, right)) {
                                rotator = new BranchRotator(device, clicked.node()->get_leftson(), left, mousepos, exports);
                            }
                            else {
                                rotator = new BranchRotator(device, clicked.node()->get_rightson(), right, mousepos, exports);
                            }
                        }
                    }
                }
                if (rotator) {
                    store_command_data(rotator);
                    rotator->draw_drag_indicator(device, drag_gc);
                }
            }
            break;

        case AWT_MODE_LINE:
            if (clicked.node()) {
                BranchLinewidthScaler *widthScaler = new BranchLinewidthScaler(clicked.node(), mousepos, event.button() == AW_BUTTON_RIGHT, exports);
                store_command_data(widthScaler);
                widthScaler->draw_drag_indicator(device, drag_gc);
            }
            break;

        case AWT_MODE_SPREAD:
            if (clicked.node() && clicked.is_branch()) {
                const AW_clicked_line *cl = dynamic_cast<const AW_clicked_line*>(clicked.element());
                td_assert(cl);
                BranchScaler *spreader = new BranchScaler(SCALE_SPREAD, clicked.node(), cl->get_line(), clicked.element()->get_attach_point(), mousepos, device->get_unscale(), false, false, exports);
                store_command_data(spreader);
                spreader->draw_drag_indicator(device, drag_gc);
            }
            break;

        // -----------------------------
        //      one point commands:

        case AWT_MODE_LZOOM:
            switch (event.button()) {
                case AW_BUTTON_LEFT:
                    if (clicked.node()) {
                        set_logical_root_to(clicked.node());
                        exports.request_zoom_reset();
                    }
                    break;
                case AW_BUTTON_RIGHT:
                    if (displayed_root->father) {
                        set_logical_root_to(displayed_root->get_father());
                        exports.request_zoom_reset();
                    }
                    break;

                default: td_assert(0); break;
            }
            break;

act_like_group :
        case AWT_MODE_GROUP:
            if (clicked.node()) {
                switch (event.button()) {
                    case AW_BUTTON_LEFT:
                        toggle_folding_at(clicked.node(), false);
                        break;
                    case AW_BUTTON_RIGHT:
                        if (tree_static->get_gb_tree()) {
                            toggle_group(clicked.node());
                        }
                        break;
                    default: td_assert(0); break;
                }
            }
            break;

        case AWT_MODE_SETROOT:
            switch (event.button()) {
                case AW_BUTTON_LEFT:
                    if (clicked.node()) {
                        clicked.node()->set_root();
                        dislocate_selected_group();
                    }
                    break;
                case AW_BUTTON_RIGHT:
                    tree_static->find_innermost_edge().set_root();
                    dislocate_selected_group();
                    break;
                default: td_assert(0); break;
            }
            exports.request_save_and_zoom_reset();
            break;

        case AWT_MODE_SWAP:
            if (clicked.node()) {
                switch (event.button()) {
                    case AW_BUTTON_LEFT:  clicked.node()->swap_sons(); break;
                    case AW_BUTTON_RIGHT: clicked.node()->rotate_subtree();     break;
                    default: td_assert(0); break;
                }
                exports.request_save();
            }
            break;

        case AWT_MODE_MARK: // see also .@OTHER_MODE_MARK_HANDLER
            if (clicked.node()) {
                GB_transaction ta(tree_static->get_gb_main());

                switch (event.button()) {
                    case AW_BUTTON_LEFT:  mark_species_in_tree(clicked.node(), 1); break;
                    case AW_BUTTON_RIGHT: mark_species_in_tree(clicked.node(), 0); break;
                    default: td_assert(0); break;
                }
                tree_static->update_timers(); // do not reload the tree
                exports.request_structure_update();
            }
            break;

        case AWT_MODE_NONE:
        case AWT_MODE_SELECT:
            if (clicked.node()) {
                GB_transaction ta(tree_static->get_gb_main());
                exports.request_refresh(); // No refresh needed !! AD_map_viewer will do the refresh (needed by arb_pars)
                map_viewer_cb(clicked.node()->gb_node, ADMVT_SELECT);

                if (event.button() == AW_BUTTON_LEFT) goto act_like_group; // now do the same like in AWT_MODE_GROUP
            }
            break;

        // now handle all modes which only act on tips (aka species) and
        // shall perform identically in tree- and list-modes

        case AWT_MODE_INFO:
        case AWT_MODE_WWW: {
            if (clicked.node() && clicked.node()->gb_node) {
                if (command_on_GBDATA(clicked.node()->gb_node, event, map_viewer_cb)) {
                    exports.request_refresh();
                }
            }
            break;
        }
        default:
            break;
    }
}

void AWT_graphic_tree::set_tree_style(AP_tree_display_style style, AWT_canvas *ntw) {
    if (is_list_style(style)) {
        if (tree_style == style) { // we are already in wanted view
            nds_only_marked = !nds_only_marked; // -> toggle between 'marked' and 'all'
        }
        else {
            nds_only_marked = false; // default to all
        }
    }
    tree_style = style;
    apply_zoom_settings_for_treetype(ntw); // sets default padding

    exports.fit_mode  = AWT_FIT_LARGER;
    exports.zoom_mode = AWT_ZOOM_BOTH;

    exports.dont_scroll = 0;

    switch (style) {
        case AP_TREE_RADIAL:
            break;

        case AP_LIST_SIMPLE:
        case AP_LIST_NDS:
            exports.fit_mode  = AWT_FIT_NEVER;
            exports.zoom_mode = AWT_ZOOM_NEVER;

            break;

        case AP_TREE_IRS:    // folded dendrogram
            exports.fit_mode    = AWT_FIT_X;
            exports.zoom_mode   = AWT_ZOOM_X;
            exports.dont_scroll = 1;
            break;

        case AP_TREE_NORMAL: // normal dendrogram
            exports.fit_mode  = AWT_FIT_X;
            exports.zoom_mode = AWT_ZOOM_X;
            break;
    }
}

static void tree_change_ignore_cb(AWT_graphic_tree*) {}
static GraphicTreeCallback treeChangeIgnore_cb = makeGraphicTreeCallback(tree_change_ignore_cb);

AWT_graphic_tree::AWT_graphic_tree(AW_root *aw_root_, GBDATA *gb_main_, AD_map_viewer_cb map_viewer_cb_) :
    AWT_graphic(),
    species_name(NULp),
    baselinewidth(1),
    tree_proto(NULp),
    link_to_database(false),
    group_style(GS_TRAPEZE),
    line_filter         (AW_SCREEN|AW_CLICK|AW_TRACK|AW_CLICK_DROP|AW_PRINTER|AW_SIZE),          // horizontal lines (ie. lines towards leafs in dendro-view; all lines in radial view)
    vert_line_filter    (AW_SCREEN|AW_CLICK|AW_CLICK_DROP|AW_PRINTER),                           // vertical lines (in dendro view; @@@ should be used in IRS as well!)
    mark_filter         (AW_SCREEN|AW_CLICK|AW_TRACK|AW_CLICK_DROP|AW_PRINTER_EXT),              // diamond at open group (dendro+radial); boxes at marked species (all views); origin (radial view); cursor box (all views); group-handle (IRS)
    group_bracket_filter(AW_SCREEN|AW_CLICK|AW_CLICK_DROP|AW_PRINTER|AW_SIZE_UNSCALED),
    leaf_text_filter    (AW_SCREEN|AW_CLICK|AW_TRACK|AW_CLICK_DROP|AW_PRINTER|AW_SIZE_UNSCALED), // text at leafs (all views but IRS? @@@ should be used in IRS as well)
    group_text_filter   (AW_SCREEN|AW_CLICK|AW_CLICK_DROP|AW_PRINTER|AW_SIZE_UNSCALED),
    other_text_filter   (AW_SCREEN|AW_PRINTER|AW_SIZE_UNSCALED),
    ruler_filter        (AW_SCREEN|AW_CLICK|AW_PRINTER),                                         // appropriate size-filter added manually in code
    root_filter         (AW_SCREEN|AW_PRINTER_EXT),                                              // unused (@@@ should be used for radial root)
    marker_filter       (AW_SCREEN|AW_CLICK|AW_PRINTER_EXT|AW_SIZE_UNSCALED),                    // species markers (eg. visualizing configs)
    group_info_pos(GIP_SEPARATED),
    group_count_mode(GCM_MEMBERS),
    branch_style(BS_RECTANGULAR),
    display_markers(NULp),
    map_viewer_cb(map_viewer_cb_),
    cmd_data(NULp),
    tree_static(NULp),
    displayed_root(NULp),
    tree_changed_cb(treeChangeIgnore_cb),
    autoUnfolded(new AP_tree_folding),
    aw_root(aw_root_),
    gb_main(gb_main_),
    nds_only_marked(false)
{
    td_assert(gb_main);
    set_tree_style(AP_TREE_NORMAL, NULp);
}

AWT_graphic_tree::~AWT_graphic_tree() {
    delete cmd_data;
    free(species_name);
    destroy(tree_proto);
    delete tree_static;
    delete display_markers;
    delete autoUnfolded;
}

AP_tree_root *AWT_graphic_tree::create_tree_root(AliView *aliview, AP_sequence *seq_prototype, bool insert_delete_cbs) {
    return new AP_tree_root(aliview, seq_prototype, insert_delete_cbs, &groupScale);
}

void AWT_graphic_tree::init(AliView *aliview, AP_sequence *seq_prototype, bool link_to_database_, bool insert_delete_cbs) {
    tree_static      = create_tree_root(aliview, seq_prototype, insert_delete_cbs);
    td_assert(!insert_delete_cbs || link_to_database); // inserting delete callbacks w/o linking to DB has no effect!
    link_to_database = link_to_database_;
}

void AWT_graphic_tree::unload() {
    forget_auto_unfolded();
    if (display_markers) display_markers->flush_cache();
    deselect_group();
    destroy(tree_static->get_root_node());
    displayed_root = NULp;
}

GB_ERROR AWT_graphic_tree::load_from_DB(GBDATA *, const char *name) {
    GB_ERROR error = NULp;

    if (!name) { // happens in error-case (called by AWT_graphic::update_DB_and_model_as_requested to load previous state)
        if (tree_static) {
            name = tree_static->get_tree_name();
            td_assert(name);
        }
        else {
            error = "Please select a tree (name lost)";
        }
    }

    if (!error) {
        if (name[0] == 0 || strcmp(name, NO_TREE_SELECTED) == 0) {
            unload();
            zombies    = 0;
            duplicates = 0;
        }
        else {
            GBDATA *gb_group = get_selected_group().get_group_data(); // remember selected group
            freenull(tree_static->gone_tree_name);
            {
                char   *name_dup = strdup(name); // name might be freed by unload()
                unload();
                error            = tree_static->loadFromDB(name_dup);
                free(name_dup);
            }

            if (!error && link_to_database) {
                error = tree_static->linkToDB(&zombies, &duplicates);
            }

            if (error) {
                destroy(tree_static->get_root_node());
            }
            else {
                displayed_root = get_root_node();
                read_tree_settings();
                get_root_node()->compute_tree();

                td_assert(!display_markers || display_markers->cache_is_flushed());

                tree_static->set_root_changed_callback(AWT_graphic_tree_root_changed, this);
                tree_static->set_node_deleted_callback(AWT_graphic_tree_node_deleted, this);
            }
            select_group(gb_group);
        }
    }

    tree_changed_cb(this);
    return error;
}

GB_ERROR AWT_graphic_tree::save_to_DB(GBDATA * /* dummy */, const char * /* name */) {
    GB_ERROR error = NULp;
    if (get_root_node()) {
        error = tree_static->saveToDB();
        if (display_markers) display_markers->flush_cache();
    }
    else if (tree_static && tree_static->get_tree_name()) {
        if (tree_static->gb_tree_gone) {
            td_assert(!tree_static->gone_tree_name);
            tree_static->gone_tree_name = strdup(tree_static->get_tree_name());

            GB_transaction ta(gb_main);
            error = GB_delete(tree_static->gb_tree_gone);
            error = ta.close(error);

            if (!error) {
                aw_message(GBS_global_string("Tree '%s' lost all leafs and has been deleted", tree_static->get_tree_name()));

                // @@@ TODO: somehow update selected tree

                // solution: currently selected tree (in NTREE, maybe also in PARSIMONY)
                // needs to add a delete callback on treedata in DB
            }

            tree_static->gb_tree_gone = NULp; // do not delete twice
        }
    }
    tree_changed_cb(this);
    return error;
}

void AWT_graphic_tree::check_for_DB_update(GBDATA *) {
    td_assert(exports.flags_writeable()); // otherwise fails on requests below

    if (tree_static) {
        AP_tree_root *troot = get_tree_root();
        if (troot) {
            GB_transaction ta(gb_main);

            AP_UPDATE_FLAGS flags = troot->check_update();
            switch (flags) {
                case AP_UPDATE_OK:
                case AP_UPDATE_ERROR:
                    break;

                case AP_UPDATE_RELOADED: {
                    const char *name = tree_static->get_tree_name();
                    if (name) {
                        GB_ERROR error = load_from_DB(gb_main, name);
                        if (error) aw_message(error);
                        else exports.request_resize();
                    }
                    break;
                }
                case AP_UPDATE_RELINKED: {
                    AP_tree *tree_root = get_root_node();
                    if (tree_root) {
                        GB_ERROR error = tree_root->relink();
                        if (error) aw_message(error);
                        else exports.request_structure_update();
                    }
                    break;
                }
            }
        }
    }
}

void AWT_graphic_tree::notify_synchronized(GBDATA *) {
    if (get_tree_root()) get_tree_root()->update_timers();
}

void AWT_graphic_tree::summarizeGroupMarkers(AP_tree *at, NodeMarkers& markers) {
    /*! summarizes matches of each probe for subtree 'at' in result param 'matches'
     * uses pcoll.cache to avoid repeated calculations
     */
    td_assert(display_markers);
    td_assert(markers.getNodeSize() == 0);
    if (at->is_leaf()) {
        if (at->name) {
            display_markers->retrieve_marker_state(at->name, markers);
        }
    }
    else {
        if (at->is_clade()) {
            const NodeMarkers *cached = display_markers->read_cache(at);
            if (cached) {
                markers = *cached;
                return;
            }
        }

        summarizeGroupMarkers(at->get_leftson(), markers);
        NodeMarkers rightMarkers(display_markers->size());
        summarizeGroupMarkers(at->get_rightson(), rightMarkers);
        markers.add(rightMarkers);

        if (at->is_clade()) {
            display_markers->write_cache(at, markers);
        }
    }
}

class MarkerXPos {
    double Width;
    double Offset;
    int    markers;
public:

    static int marker_width;

    MarkerXPos(AW_pos scale, int markers_)
        : Width((marker_width-1) / scale),
          Offset(marker_width / scale),
          markers(markers_)
    {}

    double width() const  { return Width; }
    double offset() const { return Offset; }

    double leftx  (int markerIdx) const { return (markerIdx - markers - 0.0) * offset(); }
    double centerx(int markerIdx) const { return leftx(markerIdx) + width()/2; }
};

int MarkerXPos::marker_width = 3;

class MarkerPosition : public MarkerXPos {
    double y1, y2;
public:
    MarkerPosition(AW_pos scale, int markers_, double y1_, double y2_)
        : MarkerXPos(scale, markers_),
          y1(y1_),
          y2(y2_)
    {}

    Position pos(int markerIdx) const { return Position(leftx(markerIdx), y1); }
    Vector size() const { return Vector(width(), y2-y1); }
};


void AWT_graphic_tree::drawMarker(const class MarkerPosition& marker, const bool partial, const int markerIdx) {
    td_assert(display_markers);

    const int gc = MarkerGC[markerIdx % MARKER_COLORS];

    if (partial) disp_device->set_grey_level(gc, marker_greylevel);
    disp_device->box(gc, partial ? AW::FillStyle::SHADED : AW::FillStyle::SOLID, marker.pos(markerIdx), marker.size(), marker_filter);
}

void AWT_graphic_tree::detectAndDrawMarkers(AP_tree *at, const double y1, const double y2) {
    td_assert(display_markers);

    if (disp_device->type() != AW_DEVICE_SIZE) {
        // Note: extra device scaling (needed to show flags) is done by drawMarkerNames

        int            numMarkers = display_markers->size();
        MarkerPosition flag(disp_device->get_scale(), numMarkers, y1, y2);
        NodeMarkers    markers(numMarkers);

        summarizeGroupMarkers(at, markers);

        if (markers.getNodeSize()>0) {
            AW_click_cd clickflag(disp_device, 0, CL_FLAG);
            for (int markerIdx = 0 ; markerIdx < numMarkers ; markerIdx++) {
                if (markers.markerCount(markerIdx) > 0) {
                    bool draw    = at->is_leaf();
                    bool partial = false;

                    if (!draw) { // group
                        td_assert(at->is_clade());
                        double markRate = markers.getMarkRate(markerIdx);
                        if (markRate>=groupThreshold.partiallyMarked && markRate>0.0) {
                            draw    = true;
                            partial = markRate<groupThreshold.marked;
                        }
                    }

                    if (draw) {
                        clickflag.set_cd1(markerIdx);
                        drawMarker(flag, partial, markerIdx);
                    }
                }
            }
        }
    }
}

void AWT_graphic_tree::drawMarkerNames(Position& Pen) {
    td_assert(display_markers);

    int        numMarkers = display_markers->size();
    MarkerXPos flag(disp_device->get_scale(), numMarkers);

    if (disp_device->type() != AW_DEVICE_SIZE) {
        Position pl1(flag.centerx(numMarkers-1), Pen.ypos()); // upper point of thin line
        Pen.movey(scaled_branch_distance);
        Position pl2(pl1.xpos(), Pen.ypos()); // lower point of thin line

        Vector sizeb(flag.width(), scaled_branch_distance); // size of boxes
        Vector b2t(flag.offset()+scaled_branch_distance, scaled_branch_distance); // offset box->text
        Vector toNext(-flag.offset(), scaled_branch_distance); // offset to next box

        Rectangle mbox(Position(flag.leftx(numMarkers-1), pl2.ypos()), sizeb); // the marker box

        AW_click_cd clickflag(disp_device, 0, CL_FLAG);

        for (int markerIdx = numMarkers - 1 ; markerIdx >= 0 ; markerIdx--) {
            const char *markerName = display_markers->get_marker_name(markerIdx);
            if (markerName) {
                int gc = MarkerGC[markerIdx % MARKER_COLORS];

                clickflag.set_cd1(markerIdx);

                disp_device->line(gc, pl1, pl2, marker_filter);
                disp_device->box(gc, AW::FillStyle::SOLID, mbox, marker_filter);
                disp_device->text(gc, markerName, mbox.upper_left_corner()+b2t, 0, marker_filter);
            }

            pl1.movex(toNext.x());
            pl2.move(toNext);
            mbox.move(toNext);
        }

        Pen.movey(scaled_branch_distance * (numMarkers+2));
    }
    else { // just reserve space on size device
        Pen.movey(scaled_branch_distance * (numMarkers+3));
        Position leftmost(flag.leftx(0), Pen.ypos());
        disp_device->line(AWT_GC_CURSOR, Pen, leftmost, marker_filter);
    }
}

void AWT_graphic_tree::pixel_box(int gc, const AW::Position& pos, int pixel_width, AW::FillStyle filled) {
    double diameter = disp_device->rtransform_pixelsize(pixel_width);
    Vector diagonal(diameter, diameter);

    td_assert(!filled.is_shaded()); // the pixel box is either filled or empty! (by design)
    if (filled.somehow()) disp_device->set_grey_level(gc, group_greylevel); // @@@ should not be needed here, but changes test-results (xfig-shading need fixes anyway)
    else                  disp_device->set_line_attributes(gc, 1, AW_SOLID);
    disp_device->box(gc, filled, pos-0.5*diagonal, diagonal, mark_filter);
}

void AWT_graphic_tree::diamond(int gc, const Position& posIn, int pixel_radius) {
    // filled box with one corner down
    Position spos = disp_device->transform(posIn);
    Vector   hor  = Vector(pixel_radius, 0);
    Vector   ver  = Vector(0, pixel_radius);

    Position corner[4] = {
        disp_device->rtransform(spos+hor),
        disp_device->rtransform(spos+ver),
        disp_device->rtransform(spos-hor),
        disp_device->rtransform(spos-ver),
    };

    disp_device->polygon(gc, AW::FillStyle::SOLID, 4, corner, mark_filter);
}

void AWT_graphic_tree::show_dendrogram(AP_tree *at, Position& Pen, DendroSubtreeLimits& limits, const NDS_Labeler& labeler) {
    /*! show dendrogram of subtree
     * @param at       the subtree to show
     * @param Pen      upper left corner of subtree area (eg. equals position of mark-box for tips).
     *                 Is modified and points to next subtree-area afterwards (Y only, X is undef!)
     * @param limits   reports dimension of painted subtree (output parameter)
     */

    if (disp_device->type() != AW_DEVICE_SIZE) { // tree below cliprect bottom can be cut
        Position p(0, Pen.ypos() - scaled_branch_distance *2.0);
        Position s = disp_device->transform(p);

        bool   is_clipped = false;
        double offset     = 0.0;
        if (disp_device->is_below_clip(s.ypos())) {
            offset     = scaled_branch_distance;
            is_clipped = true;
        }
        else {
            p.sety(Pen.ypos() + scaled_branch_distance *(at->gr.view_sum+2));
            s = disp_device->transform(p);

            if (disp_device->is_above_clip(s.ypos())) {
                offset     = scaled_branch_distance*at->gr.view_sum;
                is_clipped = true;
            }
        }

        if (is_clipped) {
            limits.x_right  = Pen.xpos();
            limits.y_branch = Pen.ypos();
            Pen.movey(offset);
            limits.y_top    = limits.y_bot = Pen.ypos();
            return;
        }
    }

    static int recursion_depth = 0;

    AW_click_cd cd(disp_device, (AW_CL)at, CL_NODE);
    if (at->is_leaf()) {
        if (at->gb_node && GB_read_flag(at->gb_node)) {
            set_line_attributes_for(at);
            filled_box(at->gr.gc, Pen, NT_BOX_WIDTH);
        }

        int  gc       = at->gr.gc;
        bool is_group = false;

        if (at->hasName(species_name)) {
            selSpec = PaintedNode(Pen, at);
        }
        if (at->is_keeled_group()) { // keeled groups may appear at leafs!
            is_group         = true;
            bool is_selected = selected_group.at_node(at);
            if (is_selected) {
                selGroup = PaintedNode(Pen, at);
                gc       = int(AWT_GC_CURSOR);
            }
        }

        if ((at->name || is_group) && (disp_device->get_filter() & leaf_text_filter)) {
            // display text
            const AW_font_limits&  charLimits = disp_device->get_font_limits(gc, 'A');

            double   unscale = disp_device->get_unscale();
            Position textPos = Pen + 0.5*Vector((charLimits.width+NT_BOX_WIDTH)*unscale, scaled_font.ascent);

            if (display_markers) {
                detectAndDrawMarkers(at, Pen.ypos() - scaled_branch_distance * 0.495, Pen.ypos() + scaled_branch_distance * 0.495);
            }

            const char *data = labeler.speciesLabel(this->gb_main, at->gb_node, at, tree_static->get_tree_name());
            if (is_group) {
                static SmartCharPtr buf;

                buf = strdup(data);

                const GroupInfo& info = get_group_info(at, GI_COMBINED, false, labeler); // retrieves group info for leaf!

                buf  = GBS_global_string_copy("%s (=%s)", &*buf, info.name);
                data = &*buf;
            }

            SizedCstr sdata(data);

            disp_device->text(gc, sdata, textPos, 0.0, leaf_text_filter);
            double textsize = disp_device->get_string_size(gc, sdata) * unscale;

            limits.x_right = textPos.xpos() + textsize;
        }
        else {
            limits.x_right = Pen.xpos();
        }

        limits.y_top = limits.y_bot = limits.y_branch = Pen.ypos();
        Pen.movey(scaled_branch_distance);
    }
    else if (recursion_depth>=MAX_TREEDISP_RECURSION_DEPTH) { // limit recursion depth
        const char *data = TREEDISP_TRUNCATION_MESSAGE;
        SizedCstr   sdata(data);

        int                   gc         = AWT_GC_ONLY_ZOMBIES;
        const AW_font_limits& charLimits = disp_device->get_font_limits(gc, 'A');
        double                unscale    = disp_device->get_unscale();
        Position              textPos    = Pen + 0.5*Vector((charLimits.width+NT_BOX_WIDTH)*unscale, scaled_font.ascent);
        disp_device->text(gc, sdata, textPos, 0.0, leaf_text_filter);
        double                textsize   = disp_device->get_string_size(gc, sdata) * unscale;

        limits.x_right = textPos.xpos() + textsize;
        limits.y_top   = limits.y_bot = limits.y_branch = Pen.ypos();
        Pen.movey(scaled_branch_distance);
    }

    //   s0-------------n0
    //   |
    //   attach (to father)
    //   |
    //   s1------n1

    else if (at->is_folded_group()) {
        double height     = scaled_branch_distance * at->gr.view_sum;
        double box_height = height-scaled_branch_distance;

        Position s0(Pen);
        Position s1(s0);  s1.movey(box_height);
        Position n0(s0);  n0.movex(at->gr.max_tree_depth);
        Position n1(s1);  n1.movex(at->gr.min_tree_depth);

        set_line_attributes_for(at);

        if (display_markers) {
            detectAndDrawMarkers(at, s0.ypos(), s1.ypos());
        }

        disp_device->set_grey_level(at->gr.gc, group_greylevel);

        bool      is_selected = selected_group.at_node(at);
        const int group_gc    = is_selected ? int(AWT_GC_CURSOR) : at->gr.gc;

        Position   s_attach; // parent attach point
        LineVector g_diag;   // diagonal line at right side of group ("short side" -> "long side", ie. pointing rightwards)
        {
            Position group[4] = { s0, s1, n1, n0 }; // init with long side at top (=traditional orientation)

            bool flip = false;
            switch (group_orientation) {
                case GO_TOP:      flip = false; break;
                case GO_BOTTOM:   flip = true; break;
                case GO_EXTERIOR: flip = at->is_lower_son(); break;
                case GO_INTERIOR: flip = at->is_upper_son(); break;
            }
            if (flip) { // flip triangle/trapeze vertically
                double x2 = group[2].xpos();
                group[2].setx(group[3].xpos());
                group[3].setx(x2);
                g_diag = LineVector(group[3], group[2]); // n0 -> n1
            }
            else {
                g_diag = LineVector(group[2], group[3]); // n1 -> n0
            }

            s_attach = s1+(flip ? 1.0-attach_group : attach_group)*(s0-s1);

            if (group_style == GS_TRIANGLE) {
                group[1] = s_attach;
                disp_device->polygon(at->gr.gc, AW::FillStyle::SHADED_WITH_BORDER, 3, group+1, line_filter);
                if (is_selected) disp_device->polygon(group_gc, AW::FillStyle::EMPTY, 3, group+1, line_filter);
            }
            else {
                td_assert(group_style == GS_TRAPEZE); // traditional style
                disp_device->polygon(at->gr.gc, AW::FillStyle::SHADED_WITH_BORDER, 4, group, line_filter);
                if (is_selected) disp_device->polygon(at->gr.gc, AW::FillStyle::EMPTY, 4, group, line_filter);
            }
        }

        if (is_selected) selGroup = PaintedNode(s_attach, at);

        limits.x_right = n0.xpos();

        if (disp_device->get_filter() & group_text_filter) {
            const GroupInfo&      info       = get_group_info(at, group_info_pos == GIP_SEPARATED ? GI_SEPARATED : GI_COMBINED, group_info_pos == GIP_OVERLAYED, labeler);
            const AW_font_limits& charLimits = disp_device->get_font_limits(group_gc, 'A');

            const double text_ascent = charLimits.ascent * disp_device->get_unscale();
            const double char_width  = charLimits.width * disp_device->get_unscale();

            if (info.name) { // attached info

                Position textPos;

                const double gy           = g_diag.line_vector().y();
                const double group_height = fabs(gy);

                if (group_height<=text_ascent) {
                    textPos = Position(g_diag.head().xpos(), g_diag.centroid().ypos()+text_ascent*0.5);
                }
                else {
                    Position pmin(g_diag.start()); // text position at short side of polygon (=leftmost position)
                    Position pmax(g_diag.head());  // text position at long  side of polygon (=rightmost position)

                    const double shift_right = g_diag.line_vector().x() * text_ascent / group_height; // rightward shift needed at short side (to avoid overlap with group polygon)

                    if (gy < 0.0) { // long side at top
                        pmin.movex(shift_right);
                        pmax.movey(text_ascent);
                    }
                    else { // long side at bottom
                        pmin.move(Vector(shift_right, text_ascent));
                    }

                    textPos = pmin + 0.125*(pmax-pmin);
                }

                textPos.movex(char_width);

                SizedCstr infoName(info.name, info.name_len);
                disp_device->text(group_gc, infoName, textPos, 0.0, group_text_filter);

                double textsize = disp_device->get_string_size(group_gc, infoName) * disp_device->get_unscale();
                limits.x_right  = std::max(limits.x_right, textPos.xpos()+textsize);
            }

            if (info.count) { // overlayed info
                SizedCstr    infoCount(info.count, info.count_len);
                const double textsize = disp_device->get_string_size(group_gc, infoCount) * disp_device->get_unscale();
                Position     countPos;
                if (group_style == GS_TRIANGLE) {
                    countPos = s_attach + Vector(g_diag.centroid()-s_attach)*0.666 + Vector(-textsize, text_ascent)*0.5;
                }
                else {
                    countPos = s_attach + Vector(char_width, 0.5*text_ascent);
                }
                disp_device->text(group_gc, infoCount, countPos, 0.0, group_text_filter);

                limits.x_right  = std::max(limits.x_right, countPos.xpos()+textsize);
            }
        }

        limits.y_top    = s0.ypos();
        limits.y_bot    = s1.ypos();
        limits.y_branch = s_attach.ypos();

        Pen.movey(height);
    }
    else { // furcation
        bool      is_group    = at->is_clade();
        bool      is_selected = is_group && selected_group.at_node(at);
        const int group_gc    = is_selected ? int(AWT_GC_CURSOR) : at->gr.gc;

        Position s0(Pen);

        Pen.movex(at->leftlen);
        Position n0(Pen);

        ++recursion_depth;

        show_dendrogram(at->get_leftson(), Pen, limits, labeler); // re-use limits for left branch

        n0.sety(limits.y_branch);
        s0.sety(limits.y_branch);

        Pen.setx(s0.xpos());
        Position subtree_border(Pen); subtree_border.movey(- .5*scaled_branch_distance); // attach point centered between both subtrees
        Pen.movex(at->rightlen);
        Position n1(Pen);
        {
            DendroSubtreeLimits right_lim;
            show_dendrogram(at->get_rightson(), Pen, right_lim, labeler);
            n1.sety(right_lim.y_branch);
            limits.combine(right_lim);
        }

        Position s1(s0.xpos(), n1.ypos());
        --recursion_depth;

        // calculate attach-point:
        Position attach = centroid(s0, s1);
        {
            Vector shift_by_size(ZeroVector);
            Vector shift_by_len(ZeroVector);
            int    nonZero = 0;

            if (attach_size != 0.0) {
                ++nonZero;
                shift_by_size = -attach_size * (subtree_border-attach);
            }

            if (attach_len != 0.0) {
                Position barycenter;
                if (nearlyZero(at->leftlen)) {
                    if (nearlyZero(at->rightlen)) {
                        barycenter = attach;
                    }
                    else {
                        barycenter = s1; // at(!) right branch
                    }
                }
                else {
                    if (nearlyZero(at->rightlen)) {
                        barycenter = s0; // at(!) left branch
                    }
                    else {
                        double sum = at->leftlen + at->rightlen;
                        double fraction;
                        Vector big2small;
                        if (at->leftlen < at->rightlen) {
                            fraction   = at->leftlen/sum;
                            big2small  = s0-s1;
                        }
                        else {
                            fraction = at->rightlen/sum;
                            big2small = s1-s0;
                        }
                        barycenter = attach-big2small/2+big2small*fraction;
                    }
                }

                Vector shift_to_barycenter = barycenter-attach;
                shift_by_len               = shift_to_barycenter*attach_len;

                ++nonZero;
            }

            if (nonZero>1) {
                double sum    = fabs(attach_size) + fabs(attach_len);
                double f_size = fabs(attach_size)/sum;
                double f_len  = fabs(attach_len)/sum;

                attach += f_size * shift_by_size;
                attach += f_len  * shift_by_len;
            }
            else {
                attach += shift_by_size;
                attach += shift_by_len;
            }
        }

        if (is_group && show_brackets) {
            double                unscale          = disp_device->get_unscale();
            const AW_font_limits& charLimits       = disp_device->get_font_limits(group_gc, 'A');
            double                half_text_ascent = charLimits.ascent * unscale * 0.5;

            double x1 = limits.x_right + scaled_branch_distance*0.1;
            double x2 = x1 + scaled_branch_distance * 0.3;
            double y1 = limits.y_top - half_text_ascent * 0.5;
            double y2 = limits.y_bot + half_text_ascent * 0.5;

            Rectangle bracket(Position(x1, y1), Position(x2, y2));

            set_line_attributes_for(at);

            disp_device->line(group_gc, bracket.upper_edge(), group_bracket_filter);
            disp_device->line(group_gc, bracket.lower_edge(), group_bracket_filter);
            disp_device->line(group_gc, bracket.right_edge(), group_bracket_filter);

            limits.x_right = x2;

            if (disp_device->get_filter() & group_text_filter) {
                LineVector worldBracket = disp_device->transform(bracket.right_edge());
                LineVector clippedWorldBracket;

                bool visible = disp_device->clip(worldBracket, clippedWorldBracket);
                if (visible) {
                    const GroupInfo& info = get_group_info(at, GI_SEPARATED_PARENTIZED, false, labeler);

                    if (info.name || info.count) {
                        LineVector clippedBracket = disp_device->rtransform(clippedWorldBracket);

                        if (info.name) {
                            Position namePos = clippedBracket.centroid()+Vector(half_text_ascent, -0.2*half_text_ascent); // originally y-offset was half_text_ascent (w/o counter shown)
                            SizedCstr infoName(info.name, info.name_len);
                            disp_device->text(group_gc, infoName, namePos, 0.0, group_text_filter);
                            if (info.name_len>=info.count_len) {
                                double textsize = disp_device->get_string_size(group_gc, infoName) * unscale;
                                limits.x_right  = namePos.xpos() + textsize;
                            }
                        }

                        if (info.count) {
                            Position countPos = clippedBracket.centroid()+Vector(half_text_ascent, 2.2*half_text_ascent);
                            SizedCstr infoCount(info.count, info.count_len);
                            disp_device->text(group_gc, infoCount, countPos, 0.0, group_text_filter);
                            if (info.count_len>info.name_len) {
                                double textsize = disp_device->get_string_size(group_gc, infoCount) * unscale;
                                limits.x_right  = countPos.xpos() + textsize;
                            }
                        }
                    }
                }
            }
        }

        for (int right = 0; right<2; ++right) {
            const Position& n = right ? n1 : n0; // node-position
            const Position& s = right ? s1 : s0; // upper/lower corner of rectangular branch

            AP_tree *son;
            GBT_LEN  len;
            if (right) {
                son = at->get_rightson();
                len = at->rightlen;
            }
            else {
                son = at->get_leftson();
                len = at->leftlen;
            }

            AW_click_cd cds(disp_device, (AW_CL)son, CL_NODE);

            set_line_attributes_for(son);
            unsigned int gc = son->gr.gc;

            if (branch_style == BS_RECTANGULAR) {
                draw_branch_line(gc, s, n, line_filter);
                draw_branch_line(gc, attach, s, vert_line_filter);
            }
            else {
                td_assert(branch_style == BS_DIAGONAL);
                draw_branch_line(gc, attach, n, line_filter);
            }

            if (bconf.shall_show_remark_for(son)) {
                if (son->is_son_of_root()) {
                    if (right) {            // only draw once
                        AW_click_cd cdr(disp_device, 0, CL_ROOTNODE);
                        len += at->leftlen; // sum up length of both sons of root
                        bconf.display_node_remark(disp_device, son, attach, len, scaled_branch_distance, D_EAST);
                    }
                }
                else {
                    bconf.display_node_remark(disp_device, son, n, len, scaled_branch_distance, right ? D_SOUTH_WEST : D_NORTH_WEST); // leftson is_upper_son
                }
            }
        }
        if (is_group) {
            diamond(group_gc, attach, NT_DIAMOND_RADIUS);
            if (is_selected) selGroup = PaintedNode(attach, at);
        }
        limits.y_branch = attach.ypos();
    }
}

struct Subinfo { // subtree info (used to implement branch draw precedence)
    AP_tree *at;
    double   pc; // percent of space (depends on # of species in subtree)
    Angle    orientation;
    double   len;
};

void AWT_graphic_tree::show_radial_tree(AP_tree *at, const AW::Position& base, const AW::Position& tip, const AW::Angle& orientation, const double tree_spread, const NDS_Labeler& labeler) {
    static int recursion_depth = 0;

    AW_click_cd cd(disp_device, (AW_CL)at, CL_NODE);
    set_line_attributes_for(at);
    draw_branch_line(at->gr.gc, base, tip, line_filter);

    if (at->is_leaf()) { // draw leaf node
        if (at->gb_node && GB_read_flag(at->gb_node)) { // draw mark box
            filled_box(at->gr.gc, tip, NT_BOX_WIDTH);
        }

        if (at->name && (disp_device->get_filter() & leaf_text_filter)) {
            if (at->hasName(species_name)) selSpec = PaintedNode(tip, at);

            AW_pos   alignment;
            Position textpos = calc_text_coordinates_near_tip(disp_device, at->gr.gc, tip, orientation, alignment);

            const char *data =  labeler.speciesLabel(this->gb_main, at->gb_node, at, tree_static->get_tree_name());
            disp_device->text(at->gr.gc, data,
                              textpos,
                              alignment,
                              leaf_text_filter);
        }
    }
    else if (recursion_depth>=MAX_TREEDISP_RECURSION_DEPTH) { // limit recursion depth
        const char *data    = TREEDISP_TRUNCATION_MESSAGE;
        AW_pos      alignment;
        Position    textpos = calc_text_coordinates_near_tip(disp_device, at->gr.gc, tip, orientation, alignment);

        disp_device->text(AWT_GC_ONLY_ZOMBIES,
                          data,
                          textpos,
                          alignment,
                          leaf_text_filter);
    }
    else if (at->is_folded_group()) {                                   // draw folded group
        bool      is_selected = at->name && selected_group.at_node(at); // @@@ superfluous+wrong test for group (at->name)
        const int group_gc    = is_selected ? int(AWT_GC_CURSOR) : at->gr.gc;

        if (is_selected) selGroup = PaintedNode(tip, at);

        Position corner[3];
        corner[0] = tip;
        {
            Angle left(orientation.radian() + 0.25*tree_spread + at->gr.left_angle);
            corner[1] = tip + left.normal()*at->gr.min_tree_depth;
        }
        {
            Angle right(orientation.radian() - 0.25*tree_spread + at->gr.right_angle);
            corner[2] = tip + right.normal()*at->gr.max_tree_depth;
        }

        disp_device->set_grey_level(at->gr.gc, group_greylevel);
        disp_device->polygon(at->gr.gc, AW::FillStyle::SHADED_WITH_BORDER, 3, corner, line_filter);
        if (group_gc != int(at->gr.gc)) {
            disp_device->polygon(group_gc, AW::FillStyle::EMPTY, 3, corner, line_filter);
        }

        if (disp_device->get_filter() & group_text_filter) {
            const GroupInfo& info = get_group_info(at, group_info_pos == GIP_SEPARATED ? GI_SEPARATED : GI_COMBINED, group_info_pos == GIP_OVERLAYED, labeler);
            if (info.name) {
                Angle toText = orientation;
                toText.rotate90deg();

                AW_pos   alignment;
                Position textpos = calc_text_coordinates_near_tip(disp_device, group_gc, corner[1], toText, alignment);

                disp_device->text(group_gc, SizedCstr(info.name, info.name_len), textpos, alignment, group_text_filter);
            }
            if (info.count) {
                Vector v01 = corner[1]-corner[0];
                Vector v02 = corner[2]-corner[0];

                Position incircleCenter = corner[0] + (v01*v02.length() + v02*v01.length()) / (v01.length()+v02.length()+Distance(v01.endpoint(), v02.endpoint()));

                disp_device->text(group_gc, SizedCstr(info.count, info.count_len), incircleCenter, 0.5, group_text_filter);
            }
        }
    }
    else { // draw subtrees
        bool is_selected = at->name && selected_group.at_node(at); // @@@ wrong test for group (at->name)
        if (is_selected) selGroup = PaintedNode(tip, at);

        Subinfo sub[2];
        sub[0].at = at->get_leftson();
        sub[1].at = at->get_rightson();

        sub[0].pc = sub[0].at->gr.view_sum / (double)at->gr.view_sum;
        sub[1].pc = 1.0-sub[0].pc;

        sub[0].orientation = Angle(orientation.radian() + sub[1].pc*0.5*tree_spread + at->gr.left_angle);
        sub[1].orientation = Angle(orientation.radian() - sub[0].pc*0.5*tree_spread + at->gr.right_angle);

        sub[0].len = at->leftlen;
        sub[1].len = at->rightlen;

        if (sub[0].at->gr.gc < sub[1].at->gr.gc) {
            std::swap(sub[0], sub[1]); // swap branch draw order (branches with lower gc are drawn on top of branches with higher gc)
        }

        ++recursion_depth;
        for (int s = 0; s<2; ++s) {
            show_radial_tree(sub[s].at,
                             tip,
                             tip + sub[s].len * sub[s].orientation.normal(),
                             sub[s].orientation,
                             sub[s].at->is_leaf() ? 1.0 : tree_spread * sub[s].pc * sub[s].at->gr.spread,
                             labeler);
        }
        --recursion_depth;

        for (int s = 0; s<2; ++s) {
            AP_tree *son = sub[s].at;
            if (bconf.shall_show_remark_for(son)) {
                AW_click_cd sub_cd(disp_device, (AW_CL)son, CL_NODE);

                td_assert(!son->is_leaf());
                if (son->is_son_of_root()) {
                    if (s) { // only at one son
                        AW_click_cd cdr(disp_device, 0, CL_ROOTNODE);
                        AW_pos      alignment;
                        Position    text_pos = calc_text_coordinates_aside_line(disp_device, AWT_GC_BRANCH_REMARK, tip, sub[s].orientation, true, alignment, 1.0);

                        bconf.display_remark(disp_device, son->get_remark(), tip, sub[0].len+sub[1].len, 0, text_pos, alignment);
                    }
                }
                else {
                    Position sub_branch_center = tip + (sub[s].len*.5) * sub[s].orientation.normal();

                    AW_pos   alignment;
                    Position text_pos = calc_text_coordinates_aside_line(disp_device, AWT_GC_BRANCH_REMARK, sub_branch_center, sub[s].orientation, true, alignment, 0.5);
                    bconf.display_remark(disp_device, son->get_remark(), sub_branch_center, sub[s].len, 0, text_pos, alignment);
                }
            }
        }

        if (at->is_clade()) {
            const int group_gc = selected_group.at_node(at) ? int(AWT_GC_CURSOR) : at->gr.gc;
            diamond(group_gc, tip, NT_DIAMOND_RADIUS);
        }
    }
}

const char *AWT_graphic_tree::ruler_awar(const char *name) {
    // return "ruler/TREETYPE/name" (path to entry below tree)
    const char *tree_awar = NULp;
    switch (tree_style) {
        case AP_TREE_NORMAL:
            tree_awar = "LIST";
            break;
        case AP_TREE_RADIAL:
            tree_awar = "RADIAL";
            break;
        case AP_TREE_IRS:
            tree_awar = "IRS";
            break;
        case AP_LIST_SIMPLE:
        case AP_LIST_NDS:
            // rulers not allowed in these display modes
            td_assert(0); // should not be called
            break;
    }

    static char awar_name[256];
    sprintf(awar_name, "ruler/%s/%s", tree_awar, name);
    return awar_name;
}

void AWT_graphic_tree::show_ruler(AW_device *device, int gc) {
    GBDATA *gb_tree = tree_static->get_gb_tree();
    if (!gb_tree) return; // no tree -> no ruler

    bool mode_has_ruler = ruler_awar(NULp);
    if (mode_has_ruler) {
        GB_transaction ta(gb_tree);

        float ruler_size = *GBT_readOrCreate_float(gb_tree, RULER_SIZE, DEFAULT_RULER_LENGTH);
        float ruler_y    = 0.0;

        const char *awar = ruler_awar("ruler_y");
        if (!GB_search(gb_tree, awar, GB_FIND)) {
            if (device->type() == AW_DEVICE_SIZE) {
                AW_world world;
                DOWNCAST(AW_device_size*, device)->get_size_information(&world);
                ruler_y = world.b * 1.3;
            }
        }

        double half_ruler_width = ruler_size*0.5;

        float ruler_add_y  = 0.0;
        float ruler_add_x  = 0.0;
        switch (tree_style) {
            case AP_TREE_IRS:
                // scale is different for IRS tree -> adjust:
                half_ruler_width *= irs_tree_ruler_scale_factor;
                ruler_y     = 0;
                ruler_add_y = this->list_tree_ruler_y;
                ruler_add_x = -half_ruler_width;
                break;
            case AP_TREE_NORMAL:
                ruler_y     = 0;
                ruler_add_y = this->list_tree_ruler_y;
                ruler_add_x = half_ruler_width;
                break;
            default:
                break;
        }
        ruler_y = ruler_add_y + *GBT_readOrCreate_float(gb_tree, awar, ruler_y);

        float ruler_x = 0.0;
        ruler_x       = ruler_add_x + *GBT_readOrCreate_float(gb_tree, ruler_awar("ruler_x"), ruler_x);

        td_assert(!is_nan_or_inf(ruler_x));

        float ruler_text_x = 0.0;
        ruler_text_x       = *GBT_readOrCreate_float(gb_tree, ruler_awar("text_x"), ruler_text_x);

        td_assert(!is_nan_or_inf(ruler_text_x));

        float ruler_text_y = 0.0;
        ruler_text_y       = *GBT_readOrCreate_float(gb_tree, ruler_awar("text_y"), ruler_text_y);

        td_assert(!is_nan_or_inf(ruler_text_y));

        int ruler_width = *GBT_readOrCreate_int(gb_tree, RULER_LINEWIDTH, DEFAULT_RULER_LINEWIDTH);

        device->set_line_attributes(gc, ruler_width+baselinewidth, AW_SOLID);

        AW_click_cd cd(device, 0, CL_RULER);
        device->line(gc,
                     ruler_x - half_ruler_width, ruler_y,
                     ruler_x + half_ruler_width, ruler_y,
                     this->ruler_filter|AW_SIZE);

        char ruler_text[20];
        sprintf(ruler_text, "%4.2f", ruler_size);
        device->text(gc, ruler_text,
                     ruler_x + ruler_text_x,
                     ruler_y + ruler_text_y,
                     0.5,
                     this->ruler_filter|AW_SIZE_UNSCALED);
    }
}

struct Column : virtual Noncopyable {
    char   *text;
    size_t  len;
    double  print_width;
    bool    is_numeric; // also true for empty text

    Column() : text(NULp) {}
    ~Column() { free(text); }

    void init(const char *text_, AW_device& device, int gc) {
        len         = strlen(text_);
        text        = ARB_strduplen(text_, len);
        print_width = device.get_string_size(gc, SizedCstr(text, len));
        is_numeric  = (strspn(text, "0123456789.") == len);
    }
};

class ListDisplayRow : virtual Noncopyable {
    GBDATA *gb_species;
    AW_pos  y_position;
    int     gc;
    size_t  part_count;                             // NDS columns
    Column *column;

public:
    ListDisplayRow(GBDATA *gb_main, GBDATA *gb_species_, AW_pos y_position_, int gc_, AW_device& device, bool use_nds, const char *tree_name, const NDS_Labeler& labeler)
        : gb_species(gb_species_),
          y_position(y_position_),
          gc(gc_)
    {
        const char *nds = use_nds
                          ? labeler.speciesLabel(gb_main, gb_species, NULp, tree_name)
                          : GBT_get_name_or_description(gb_species);

        ConstStrArray parts;
        GBT_split_string(parts, nds, "\t", SPLIT_KEEPEMPTY);
        part_count = parts.size();

        column = new Column[part_count];
        for (size_t i = 0; i<part_count; ++i) {
            column[i].init(parts[i], device, gc);
        }
    }

    ~ListDisplayRow() { delete [] column; }

    size_t get_part_count() const { return part_count; }
    const Column& get_column(size_t p) const {
        td_assert(p<part_count);
        return column[p];
    }
    double get_print_width(size_t p) const { return get_column(p).print_width; }
    const char *get_text(size_t p, size_t& len) const {
        const Column& col = get_column(p);
        len = col.len;
        return col.text;
    }
    int get_gc() const { return gc; }
    double get_ypos() const { return y_position; }
    GBDATA *get_species() const { return gb_species; }
};

void AWT_graphic_tree::show_nds_list(GBDATA *, bool use_nds, const NDS_Labeler& labeler) {
    AW_pos y_position = scaled_branch_distance;
    AW_pos x_position = NT_SELECTED_WIDTH * disp_device->get_unscale();

    disp_device->text(nds_only_marked ? AWT_GC_ALL_MARKED : AWT_GC_CURSOR,
                      GBS_global_string("%s of %s species", use_nds ? "NDS List" : "Simple list", nds_only_marked ? "marked" : "all"),
                      (AW_pos) x_position, (AW_pos) 0,
                      (AW_pos) 0, other_text_filter);

    double max_x         = 0;
    double text_y_offset = scaled_font.ascent*.5;

    GBDATA *selected_species;
    {
        GBDATA *selected_name = GB_find_string(GBT_get_species_data(gb_main), "name", this->species_name, GB_IGNORE_CASE, SEARCH_GRANDCHILD);
        selected_species      = selected_name ? GB_get_father(selected_name) : NULp;
    }

    const char *tree_name = tree_static ? tree_static->get_tree_name() : NULp;

    AW_pos y1, y2;
    {
        const AW_screen_area& clip_rect = disp_device->get_cliprect();

        AW_pos Y1 = clip_rect.t;
        AW_pos Y2 = clip_rect.b;

        AW_pos x;
        disp_device->rtransform(0, Y1, x, y1);
        disp_device->rtransform(0, Y2, x, y2);
    }

    y1 -= 2*scaled_branch_distance;                 // add two lines for safety
    y2 += 2*scaled_branch_distance;

    size_t           displayed_rows = (y2-y1)/scaled_branch_distance+1;
    ListDisplayRow **row            = new ListDisplayRow*[displayed_rows];

    size_t species_count = 0;
    size_t max_parts     = 0;

    GBDATA *gb_species = nds_only_marked ? GBT_first_marked_species(gb_main) : GBT_first_species(gb_main);
    if (gb_species) {
        int skip_over = (y1-y_position)/scaled_branch_distance-2;
        if (skip_over>0) {
            gb_species  = nds_only_marked
                          ? GB_following_marked(gb_species, "species", skip_over-1)
                          : GB_followingEntry(gb_species, skip_over-1);
            y_position += skip_over*scaled_branch_distance;
        }
    }

    const AP_TreeShader *shader = AP_tree::get_tree_shader();
    const_cast<AP_TreeShader*>(shader)->update_settings();

    for (; gb_species; gb_species = nds_only_marked ? GBT_next_marked_species(gb_species) : GBT_next_species(gb_species)) {
        y_position += scaled_branch_distance;

        if (gb_species == selected_species) selSpec = PaintedNode(Position(0, y_position), NULp);

        if (y_position>y1) {
            if (y_position>y2) break;           // no need to examine rest of species

            bool is_marked = nds_only_marked || GB_read_flag(gb_species);
            if (is_marked) {
                disp_device->set_line_attributes(AWT_GC_ALL_MARKED, baselinewidth, AW_SOLID);
                filled_box(AWT_GC_ALL_MARKED, Position(0, y_position), NT_BOX_WIDTH);
            }

            bool colorize_marked = is_marked && !nds_only_marked; // do not use mark-color if only showing marked

            int gc = shader->calc_leaf_GC(gb_species, colorize_marked);
            if (gc == AWT_GC_NONE_MARKED && shader->does_shade()) { // may show shaded color
                gc = shader->to_GC(shader->calc_shaded_leaf_GC(gb_species));
            }

            ListDisplayRow *curr = new ListDisplayRow(gb_main, gb_species, y_position+text_y_offset, gc, *disp_device, use_nds, tree_name, labeler);
            max_parts            = std::max(max_parts, curr->get_part_count());
            row[species_count++] = curr;
        }
    }

    td_assert(species_count <= displayed_rows);

    // calculate column offsets and detect column alignment
    double *max_part_width = new double[max_parts];
    bool   *align_right    = new bool[max_parts];

    for (size_t p = 0; p<max_parts; ++p) {
        max_part_width[p] = 0;
        align_right[p]    = true;
    }

    for (size_t s = 0; s<species_count; ++s) {
        size_t parts = row[s]->get_part_count();
        for (size_t p = 0; p<parts; ++p) {
            const Column& col = row[s]->get_column(p);
            max_part_width[p] = std::max(max_part_width[p], col.print_width);
            align_right[p]    = align_right[p] && col.is_numeric;
        }
    }

    double column_space = scaled_branch_distance;

    double *part_x_pos = new double[max_parts];
    for (size_t p = 0; p<max_parts; ++p) {
        part_x_pos[p]  = x_position;
        x_position    += max_part_width[p]+column_space;
    }
    max_x = x_position;

    // draw

    for (size_t s = 0; s<species_count; ++s) {
        const ListDisplayRow& Row = *row[s];

        size_t parts = Row.get_part_count();
        int    gc    = Row.get_gc();
        AW_pos y     = Row.get_ypos();

        GBDATA      *gb_sp = Row.get_species();
        AW_click_cd  cd(disp_device, (AW_CL)gb_sp, CL_SPECIES);

        for (size_t p = 0; p<parts; ++p) {
            const Column& col = Row.get_column(p);

            AW_pos x               = part_x_pos[p];
            if (align_right[p]) x += max_part_width[p] - col.print_width;

            disp_device->text(gc, SizedCstr(col.text, col.len), x, y, 0.0, leaf_text_filter);
        }
    }

    delete [] part_x_pos;
    delete [] align_right;
    delete [] max_part_width;

    for (size_t s = 0; s<species_count; ++s) delete row[s];
    delete [] row;

    disp_device->invisible(Origin);  // @@@ remove when size-dev works
    disp_device->invisible(Position(max_x, y_position+scaled_branch_distance));  // @@@ remove when size-dev works
}

void AWT_graphic_tree::read_tree_settings() {
    scaled_branch_distance = aw_root->awar(AWAR_DTREE_VERICAL_DIST)->read_float();       // not final value!
    group_greylevel        = aw_root->awar(AWAR_DTREE_GREY_LEVEL)->read_int() * 0.01;
    baselinewidth          = aw_root->awar(AWAR_DTREE_BASELINEWIDTH)->read_int();
    group_count_mode       = GroupCountMode(aw_root->awar(AWAR_DTREE_GROUPCOUNTMODE)->read_int());
    group_info_pos         = GroupInfoPosition(aw_root->awar(AWAR_DTREE_GROUPINFOPOS)->read_int());
    show_brackets          = aw_root->awar(AWAR_DTREE_SHOW_BRACKETS)->read_int();
    groupScale.pow         = aw_root->awar(AWAR_DTREE_GROUP_DOWNSCALE)->read_float();
    groupScale.linear      = aw_root->awar(AWAR_DTREE_GROUP_SCALE)->read_float();
    group_style            = GroupStyle(aw_root->awar(AWAR_DTREE_GROUP_STYLE)->read_int());
    group_orientation      = GroupOrientation(aw_root->awar(AWAR_DTREE_GROUP_ORIENT)->read_int());
    branch_style           = BranchStyle(aw_root->awar(AWAR_DTREE_BRANCH_STYLE)->read_int());
    attach_size            = aw_root->awar(AWAR_DTREE_ATTACH_SIZE)->read_float();
    attach_len             = aw_root->awar(AWAR_DTREE_ATTACH_LEN)->read_float();
    attach_group           = (aw_root->awar(AWAR_DTREE_ATTACH_GROUP)->read_float()+1)/2; // projection: [-1 .. 1] -> [0 .. 1]

    bconf.show_boots    = aw_root->awar(AWAR_DTREE_BOOTSTRAP_SHOW)->read_int();
    bconf.bootstrap_min = aw_root->awar(AWAR_DTREE_BOOTSTRAP_MIN)->read_int();
    bconf.bootstrap_max = aw_root->awar(AWAR_DTREE_BOOTSTRAP_MAX)->read_int();
    bconf.style         = BootstrapStyle(aw_root->awar(AWAR_DTREE_BOOTSTRAP_STYLE)->read_int());
    bconf.zoom_factor   = aw_root->awar(AWAR_DTREE_CIRCLE_ZOOM)->read_float();
    bconf.max_radius    = aw_root->awar(AWAR_DTREE_CIRCLE_LIMIT)->read_float();
    bconf.show_circle   = aw_root->awar(AWAR_DTREE_CIRCLE_SHOW)->read_int();
    bconf.fill_level    = aw_root->awar(AWAR_DTREE_CIRCLE_FILL)->read_int() * 0.01;
    bconf.elipsoid      = aw_root->awar(AWAR_DTREE_CIRCLE_ELLIPSE)->read_int();

    freeset(species_name, aw_root->awar(AWAR_SPECIES_NAME)->read_string());

    if (display_markers) {
        groupThreshold.marked          = aw_root->awar(AWAR_DTREE_GROUP_MARKED_THRESHOLD)->read_float() * 0.01;
        groupThreshold.partiallyMarked = aw_root->awar(AWAR_DTREE_GROUP_PARTIALLY_MARKED_THRESHOLD)->read_float() * 0.01;
        MarkerXPos::marker_width       = aw_root->awar(AWAR_DTREE_MARKER_WIDTH)->read_int();
        marker_greylevel               = aw_root->awar(AWAR_DTREE_PARTIAL_GREYLEVEL)->read_int() * 0.01;
    }
}

void AWT_graphic_tree::apply_zoom_settings_for_treetype(AWT_canvas *ntw) {
    exports.set_standard_default_padding();

    if (ntw) {
        bool zoom_fit_text       = false;
        int  left_padding  = 0;
        int  right_padding = 0;

        switch (tree_style) {
            case AP_TREE_RADIAL:
                zoom_fit_text = aw_root->awar(AWAR_DTREE_RADIAL_ZOOM_TEXT)->read_int();
                left_padding  = aw_root->awar(AWAR_DTREE_RADIAL_XPAD)->read_int();
                right_padding = left_padding;
                break;

            case AP_TREE_NORMAL:
            case AP_TREE_IRS:
                zoom_fit_text = aw_root->awar(AWAR_DTREE_DENDRO_ZOOM_TEXT)->read_int();
                left_padding  = STANDARD_PADDING;
                right_padding = aw_root->awar(AWAR_DTREE_DENDRO_XPAD)->read_int();
                break;

            default :
                break;
        }

        exports.set_default_padding(STANDARD_PADDING, STANDARD_PADDING, left_padding, right_padding);

        ntw->set_consider_text_for_zoom_reset(zoom_fit_text);
    }
}

void AWT_graphic_tree::show(AW_device *device) {
    read_tree_settings();
    bconf.update_empty_branch_behavior(get_tree_root());

    disp_device = device;
    disp_device->reset_style();

    {
        const AW_font_limits& charLimits  = disp_device->get_font_limits(AWT_GC_ALL_MARKED, 0);
        scaled_font.init(charLimits, device->get_unscale());
    }
    {
        const AW_font_limits&  remarkLimits = disp_device->get_font_limits(AWT_GC_BRANCH_REMARK, 0);
        AWT_scaled_font_limits scaledRemarkLimits;
        scaledRemarkLimits.init(remarkLimits, device->get_unscale());
        bconf.scaled_remark_ascend          = scaledRemarkLimits.ascent;
    }
    scaled_branch_distance *= scaled_font.height;

    selSpec  = PaintedNode(); // not painted yet
    selGroup = PaintedNode(); // not painted yet

    if (!displayed_root && is_tree_style(tree_style)) { // there is no tree => show message instead
        static const char *no_tree_text[] = {
            "No tree (selected)",
            "",
            "In the top area you may click on",
            "- the listview-button to see a plain list of species",
            "- the tree-selection-button to select a tree",
            NULp
        };

        Position p0(0, -3*scaled_branch_distance);
        Position cursor = p0;
        for (int i = 0; no_tree_text[i]; ++i) {
            cursor.movey(scaled_branch_distance);
            device->text(AWT_GC_CURSOR, no_tree_text[i], cursor);
        }

        // add some space between vertical line and text:
        p0.movex(-scaled_branch_distance);
        cursor.movex(-scaled_branch_distance);

        // add some space between horizontal line and text:
        p0.movey(-scaled_branch_distance);
        cursor.movey(scaled_branch_distance);

        Position horizontal = p0 + 2*Vector(p0, cursor).rotate270deg();
        device->line(AWT_GC_CURSOR, p0, cursor);
        device->line(AWT_GC_CURSOR, p0, horizontal);

        selSpec = PaintedNode(cursor, NULp);
    }
    else {
        double   range_display_size  = scaled_branch_distance;
        bool     allow_range_display = true;
        Position range_origin        = Origin;

        NDS_Labeler labeler(tree_style == AP_LIST_NDS ? NDS_OUTPUT_TAB_SEPARATED : NDS_OUTPUT_LEAFTEXT);

        switch (tree_style) {
            case AP_TREE_NORMAL: {
                DendroSubtreeLimits limits;
                Position            pen(0, 0.05);

                show_dendrogram(displayed_root, pen, limits, labeler);

                int rulerOffset = 2;
                if (display_markers) {
                    drawMarkerNames(pen);
                    ++rulerOffset;
                }
                list_tree_ruler_y = pen.ypos() + double(rulerOffset) * scaled_branch_distance;
                break;
            }
            case AP_TREE_RADIAL: {
                LocallyModify<bool> onlyUseCircles(bconf.elipsoid, false); // radial tree never shows bootstrap circles as ellipsoids

                {
                    AW_click_cd cdr(device, 0, CL_ROOTNODE);
                    empty_box(displayed_root->gr.gc, Origin, NT_ROOT_WIDTH);
                }
                show_radial_tree(displayed_root, Origin, Origin, Eastwards, 2*M_PI, labeler);

                range_display_size  = 3.0/AW_PLANAR_COLORS;
                range_origin       += Vector(-range_display_size*AW_PLANAR_COLORS/2, -range_display_size*AW_PLANAR_COLORS/2);
                break;
            }
            case AP_TREE_IRS:
                show_irs_tree(displayed_root, scaled_branch_distance, labeler);
                break;

            case AP_LIST_NDS: // this is the list all/marked species mode (no tree)
                show_nds_list(gb_main, true, labeler);
                break;

            case AP_LIST_SIMPLE:    // simple list of names (used at startup only)
                // don't see why we need to draw ANY tree at startup -> disabled
                // show_nds_list(gb_main, false);
                allow_range_display = false;
                break;
        }
        if (selSpec.was_displayed()) {
            AP_tree     *selNode = selSpec.get_node();
            AW_click_cd  cd(device, AW_CL(selNode ? selNode->gb_node : NULp), CL_SPECIES);
            empty_box(AWT_GC_CURSOR, selSpec.get_pos(), NT_SELECTED_WIDTH);
        }
        if (is_tree_style(tree_style)) show_ruler(disp_device, AWT_GC_CURSOR);

        if (allow_range_display) {
            AW_displayColorRange(disp_device, AWT_GC_FIRST_RANGE_COLOR, range_origin, range_display_size, range_display_size);
        }
    }

    if (cmd_data && Dragged::valid_drag_device(disp_device)) {
        Dragged *dragging = dynamic_cast<Dragged*>(cmd_data);
        if (dragging) {
            // if tree is redisplayed while dragging, redraw the drag indicator.
            // (happens in modes which modify the tree during drag, e.g. when scaling branches)
            dragging->draw_drag_indicator(disp_device, drag_gc);
        }
    }

    disp_device = NULp;
}

inline unsigned percentMarked(const AP_tree_members& gr) {
    double   percent = double(gr.mark_sum)/gr.leaf_sum;
    unsigned pc      = unsigned(percent*100.0+0.5);

    if (pc == 0) {
        td_assert(gr.mark_sum>0); // otherwise function should not be called
        pc = 1; // do not show '0%' for range ]0.0 .. 0.05[
    }
    else if (pc == 100) {
        if (gr.mark_sum<gr.leaf_sum) {
            pc = 99; // do not show '100%' for range [0.95 ... 1.0[
        }
    }
    return pc;
}

const GroupInfo& AWT_graphic_tree::get_group_info(AP_tree *at, GroupInfoMode mode, bool swap, const NDS_Labeler& labeler) const {
    static GroupInfo info = { NULp, NULp, 0, 0 };

    info.name = NULp;
    if (at->father) {
        static SmartCharPtr copy;
        if (!at->is_leaf() && at->is_normal_group()) {
            if (at->is_keeled_group()) { // keeled + named
                info.name = labeler.groupLabel(gb_main, at->gb_node, at, tree_static->get_tree_name()); // normal
                copy = strdup(info.name);
                info.name = labeler.groupLabel(gb_main, at->father->gb_node, at, tree_static->get_tree_name()); // keeled

                copy = GBS_global_string_copy("%s = %s", &*copy, info.name);
                info.name = &*copy;
            }
            else { // only named group
                info.name = labeler.groupLabel(gb_main, at->gb_node, at, tree_static->get_tree_name());
            }
        }
        else if (at->is_keeled_group()) {
            info.name = labeler.groupLabel(gb_main, at->father->gb_node, at, tree_static->get_tree_name());
        }
#if defined(ASSERTION_USED)
        else {
            td_assert(0); // why was get_group_info called?
        }
#endif
    }
    else {
        if (at->gb_node) {
            td_assert(0); // if this never happens -> remove case
            info.name = tree_static->get_tree_name();
        }
    }
    if (info.name && !info.name[0]) info.name = NULp;
    info.name_len = info.name ? strlen(info.name) : 0;

    static char countBuf[50];
    countBuf[0] = 0;

    GroupCountMode count_mode = group_count_mode;

    if (!at->gr.mark_sum) { // do not display zero marked
        switch (count_mode) {
            case GCM_NONE:
            case GCM_MEMBERS: break; // unchanged

            case GCM_PERCENT:
            case GCM_MARKED: count_mode = GCM_NONE; break; // completely skip

            case GCM_BOTH:
            case GCM_BOTH_PC: count_mode = GCM_MEMBERS; break; // fallback to members-only
        }
    }

    switch (count_mode) {
        case GCM_NONE:    break;
        case GCM_MEMBERS: sprintf(countBuf, "%u",      at->gr.leaf_sum);                        break;
        case GCM_MARKED:  sprintf(countBuf, "%u",      at->gr.mark_sum);                        break;
        case GCM_BOTH:    sprintf(countBuf, "%u/%u",   at->gr.mark_sum, at->gr.leaf_sum);       break;
        case GCM_PERCENT: sprintf(countBuf, "%u%%",    percentMarked(at->gr));                  break;
        case GCM_BOTH_PC: sprintf(countBuf, "%u%%/%u", percentMarked(at->gr), at->gr.leaf_sum); break;
    }

    if (countBuf[0]) {
        info.count     = countBuf;
        info.count_len = strlen(info.count);

        bool parentize = mode != GI_SEPARATED;
        if (parentize) {
            memmove(countBuf+1, countBuf, info.count_len);
            countBuf[0] = '(';
            strcpy(countBuf+info.count_len+1, ")");
            info.count_len += 2;
        }
    }
    else {
        info.count     = NULp;
        info.count_len = 0;
    }

    if (mode == GI_COMBINED) {
        if (info.name) {
            if (info.count) {
                info.name      = GBS_global_string("%s %s", info.name, info.count);
                info.name_len += info.count_len+1;

                info.count     = NULp;
                info.count_len = 0;
            }
        }
        else if (info.count) {
            swap = !swap;
        }
    }

    if (swap) {
        std::swap(info.name, info.count);
        std::swap(info.name_len, info.count_len);
    }

    return info;
}

void AWT_graphic_tree::install_tree_changed_callback(const GraphicTreeCallback& gtcb) {
    /*! install a callback called whenever
     *  - topology changes (either by DB-change or by GUI command),
     *  - logical zoom changes or
     *  - a different tree gets displayed.
     */
    td_assert(tree_changed_cb == treeChangeIgnore_cb);
    tree_changed_cb = gtcb;
}
void AWT_graphic_tree::uninstall_tree_changed_callback() {
    td_assert(!(tree_changed_cb == treeChangeIgnore_cb));
    tree_changed_cb = treeChangeIgnore_cb;
}

void AWT_graphic_tree::fast_sync_changed_folding(AP_tree *parent_of_all_changes) {
    // Does work normally done by [save_to_DB + update_structure],
    // but works only correctly if nothing but folding has changed.

    if (!exports.needs_save()) {
        // td_assert(!exports.needs_structure_update()); // if that happens -> do what????
#if defined(ASSERTION_USED)
        bool needed_structure_update = exports.needs_structure_update();
#endif
        if (display_markers) display_markers->flush_cache();
        parent_of_all_changes->recompute_and_write_folding();

        td_assert(needed_structure_update == exports.needs_structure_update()); // structure update gets delayed (@@@ not correct, but got no idea how to fix it correctly)
        exports.request_resize();
        notify_synchronized(NULp); // avoid reload
    }
}

const char *TREE_canvas::suffixed_title_for_index(const char *primary_title, int index) {
        /*! Generates enumerated titles for additional arb main windows and their subwindows.
         *
         * @param primary_title title used on first window (index == 0).
         * @param index as provided by get_index(). If >0 then add enumeration (1 = '(#2)', 2 = '(#3)', ...) to 'primary_title'.
         *
         * @return generated title in global buffer.
         */
        if (index) {
            static char *last_suffixed_title = NULp;
            freeset(last_suffixed_title, GBS_global_string_copy("%s (#%i)", primary_title, index+1));
            return last_suffixed_title;
        }
        return primary_title;
    }

AWT_graphic_tree *TREE_generate_display(AW_root *root, GBDATA *gb_main, AD_map_viewer_cb map_viewer_cb) {
    AWT_graphic_tree *apdt = new AWT_graphic_tree(root, gb_main, map_viewer_cb);
    apdt->init(new AliView(gb_main), NULp, true, false); // tree w/o sequence data
    return apdt;
}

static void markerThresholdChanged_cb(AW_root *root, bool partChanged) {
    static bool avoid_recursion = false;
    if (!avoid_recursion) {
        LocallyModify<bool> flag(avoid_recursion, true);

        AW_awar *awar_marked     = root->awar(AWAR_DTREE_GROUP_MARKED_THRESHOLD);
        AW_awar *awar_partMarked = root->awar(AWAR_DTREE_GROUP_PARTIALLY_MARKED_THRESHOLD);

        float marked     = awar_marked->read_float();
        float partMarked = awar_partMarked->read_float();

        if (partMarked>marked) { // unwanted state
            if (partChanged) {
                awar_marked->write_float(partMarked);
            }
            else {
                awar_partMarked->write_float(marked);
            }
        }
        root->awar(AWAR_TREE_REFRESH)->touch();
    }
}

void TREE_create_awars(AW_root *aw_root, AW_default db) {
    aw_root->awar_int  (AWAR_DTREE_BASELINEWIDTH,   1)  ->set_minmax (1,    10);
    aw_root->awar_float(AWAR_DTREE_VERICAL_DIST,    1.0)->set_minmax (0.01, 30);
    aw_root->awar_int  (AWAR_DTREE_BRANCH_STYLE,    BS_RECTANGULAR);
    aw_root->awar_float(AWAR_DTREE_ATTACH_SIZE,    -1.0)->set_minmax (-1.0,  1.0);
    aw_root->awar_float(AWAR_DTREE_ATTACH_LEN,      0.0)->set_minmax (-1.0,  1.0);
    aw_root->awar_float(AWAR_DTREE_ATTACH_GROUP,    0.0)->set_minmax (-1.0,  1.0);
    aw_root->awar_float(AWAR_DTREE_GROUP_DOWNSCALE, 0.33)->set_minmax(0.0,  1.0);
    aw_root->awar_float(AWAR_DTREE_GROUP_SCALE,     1.0)->set_minmax (0.01, 10.0);

    aw_root->awar_int(AWAR_DTREE_AUTO_JUMP,      AP_JUMP_KEEP_VISIBLE);
    aw_root->awar_int(AWAR_DTREE_AUTO_JUMP_TREE, AP_JUMP_FORCE_VCENTER);
    aw_root->awar_int(AWAR_DTREE_AUTO_UNFOLD,    1);

    aw_root->awar_int(AWAR_DTREE_GROUPCOUNTMODE, GCM_MEMBERS);
    aw_root->awar_int(AWAR_DTREE_GROUPINFOPOS,   GIP_SEPARATED);

    aw_root->awar_int(AWAR_DTREE_SHOW_BRACKETS,  1);

    aw_root->awar_int  (AWAR_DTREE_BOOTSTRAP_SHOW,  1);
    aw_root->awar_int  (AWAR_DTREE_BOOTSTRAP_STYLE, BS_PERCENT);
    aw_root->awar_int  (AWAR_DTREE_BOOTSTRAP_MIN,   0)->set_minmax(0,100);
    aw_root->awar_int  (AWAR_DTREE_BOOTSTRAP_MAX,   99)->set_minmax(0,100);
    aw_root->awar_int  (AWAR_DTREE_CIRCLE_SHOW,     0);
    aw_root->awar_int  (AWAR_DTREE_CIRCLE_ELLIPSE,  1);
    aw_root->awar_int  (AWAR_DTREE_CIRCLE_FILL,     50)->set_minmax(0, 100); // draw bootstrap circles 50% greyscaled
    aw_root->awar_float(AWAR_DTREE_CIRCLE_ZOOM,     1.0)->set_minmax(0.01, 30);
    aw_root->awar_float(AWAR_DTREE_CIRCLE_LIMIT,    2.0)->set_minmax(0.01, 30);

    aw_root->awar_int(AWAR_DTREE_GROUP_STYLE,  GS_TRAPEZE);
    aw_root->awar_int(AWAR_DTREE_GROUP_ORIENT, GO_TOP);

    aw_root->awar_int(AWAR_DTREE_GREY_LEVEL, 20)->set_minmax(0, 100);

    aw_root->awar_int(AWAR_DTREE_RADIAL_ZOOM_TEXT, 0);
    aw_root->awar_int(AWAR_DTREE_RADIAL_XPAD,      150)->set_minmax(-100, 2000);
    aw_root->awar_int(AWAR_DTREE_DENDRO_ZOOM_TEXT, 0);
    aw_root->awar_int(AWAR_DTREE_DENDRO_XPAD,      300)->set_minmax(-100, 2000);

    aw_root->awar_int  (AWAR_DTREE_MARKER_WIDTH,                     3)    ->set_minmax(1, 20);
    aw_root->awar_int  (AWAR_DTREE_PARTIAL_GREYLEVEL,                37)   ->set_minmax(0, 100);
    aw_root->awar_float(AWAR_DTREE_GROUP_MARKED_THRESHOLD,           100.0)->set_minmax(0, 100);
    aw_root->awar_float(AWAR_DTREE_GROUP_PARTIALLY_MARKED_THRESHOLD, 0.0)  ->set_minmax(0, 100);

    aw_root->awar_int(AWAR_TREE_REFRESH,   0, db);
    aw_root->awar_int(AWAR_TREE_RECOMPUTE, 0, db);
}

static void TREE_recompute_and_resize_cb(UNFIXED, TREE_canvas *ntw) {
    AWT_auto_refresh  allowed_on(ntw);
    AWT_graphic_tree *gt   = DOWNCAST(AWT_graphic_tree*, ntw->gfx);
    AP_tree          *root = gt->get_root_node();
    if (root) {
        gt->read_tree_settings(); // update settings for group-scaling
        ntw->request_structure_update();
    }
    ntw->request_resize();
}
static void TREE_resize_cb(UNFIXED, TREE_canvas *ntw) {
    AWT_auto_refresh allowed_on(ntw);
    ntw->request_resize();
}

static void bootstrap_range_changed_cb(AW_root *awr, TREE_canvas *ntw, int upper_changed) {
    // couple limits of bootstrap range
    static bool in_recursion = false;
    if (!in_recursion) {
        LocallyModify<bool> avoid(in_recursion, true);

        AW_awar *alower = awr->awar(AWAR_DTREE_BOOTSTRAP_MIN);
        AW_awar *aupper = awr->awar(AWAR_DTREE_BOOTSTRAP_MAX);

        int rlower = alower->read_int();
        int rupper = aupper->read_int();

        if (rlower>rupper) { // need correction
            if (upper_changed) {
                alower->write_int(rupper);
            }
            else {
                aupper->write_int(rlower);
            }
        }

        AWT_auto_refresh allowed_on(ntw);
        ntw->request_refresh();
    }
}

void TREE_install_update_callbacks(TREE_canvas *ntw) {
    // install all callbacks needed to make the tree-display update properly

    AW_root *awr = ntw->awr;

    // bind to all options available in 'Tree options'
    RootCallback expose_cb = makeRootCallback(AWT_expose_cb, static_cast<AWT_canvas*>(ntw));
    awr->awar(AWAR_DTREE_BASELINEWIDTH)  ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_BOOTSTRAP_SHOW) ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_BOOTSTRAP_STYLE)->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_CIRCLE_SHOW)    ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_CIRCLE_FILL)    ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_SHOW_BRACKETS)  ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_CIRCLE_ZOOM)    ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_CIRCLE_LIMIT)   ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_CIRCLE_ELLIPSE) ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_GROUP_STYLE)    ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_GROUP_ORIENT)   ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_GREY_LEVEL)     ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_GROUPCOUNTMODE) ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_GROUPINFOPOS)   ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_BRANCH_STYLE)   ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_ATTACH_SIZE)    ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_ATTACH_LEN)     ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_ATTACH_GROUP)   ->add_callback(expose_cb);

    awr->awar(AWAR_DTREE_BOOTSTRAP_MIN)  ->add_callback(makeRootCallback(bootstrap_range_changed_cb, ntw, 0));
    awr->awar(AWAR_DTREE_BOOTSTRAP_MAX)  ->add_callback(makeRootCallback(bootstrap_range_changed_cb, ntw, 1));

    RootCallback reinit_treetype_cb = makeRootCallback(NT_reinit_treetype, ntw);
    awr->awar(AWAR_DTREE_RADIAL_ZOOM_TEXT)->add_callback(reinit_treetype_cb);
    awr->awar(AWAR_DTREE_RADIAL_XPAD)     ->add_callback(reinit_treetype_cb);
    awr->awar(AWAR_DTREE_DENDRO_ZOOM_TEXT)->add_callback(reinit_treetype_cb);
    awr->awar(AWAR_DTREE_DENDRO_XPAD)     ->add_callback(reinit_treetype_cb);

    RootCallback resize_cb = makeRootCallback(TREE_resize_cb, ntw);
    awr->awar(AWAR_DTREE_VERICAL_DIST)->add_callback(resize_cb);

    RootCallback recompute_and_resize_cb = makeRootCallback(TREE_recompute_and_resize_cb, ntw);
    awr->awar(AWAR_DTREE_GROUP_SCALE)    ->add_callback(recompute_and_resize_cb);
    awr->awar(AWAR_DTREE_GROUP_DOWNSCALE)->add_callback(recompute_and_resize_cb);

    // global refresh trigger (used where a refresh is/was missing)
    awr->awar(AWAR_TREE_REFRESH)->add_callback(expose_cb);
    awr->awar(AWAR_TREE_RECOMPUTE)->add_callback(recompute_and_resize_cb);

    // refresh on NDS changes
    GBDATA *gb_arb_presets = GB_search(ntw->gb_main, "arb_presets", GB_CREATE_CONTAINER);
    GB_add_callback(gb_arb_presets, GB_CB_CHANGED, makeDatabaseCallback(AWT_expose_cb, static_cast<AWT_canvas*>(ntw)));

    // track selected species (autoscroll)
    awr->awar(AWAR_SPECIES_NAME)->add_callback(makeRootCallback(TREE_auto_jump_cb, ntw, AP_JUMP_REASON_SPECIES));

    // refresh on changes of marker display settings
    awr->awar(AWAR_DTREE_MARKER_WIDTH)                    ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_PARTIAL_GREYLEVEL)               ->add_callback(expose_cb);
    awr->awar(AWAR_DTREE_GROUP_MARKED_THRESHOLD)          ->add_callback(makeRootCallback(markerThresholdChanged_cb,  false));
    awr->awar(AWAR_DTREE_GROUP_PARTIALLY_MARKED_THRESHOLD)->add_callback(makeRootCallback(markerThresholdChanged_cb,  true));
}

static void tree_insert_jump_option_menu(AW_window *aws, const char *label, const char *awar_name) {
    aws->label(label);
    aws->create_option_menu(awar_name);
    aws->insert_default_option("do nothing",        "n", AP_DONT_JUMP);
    aws->insert_option        ("keep visible",      "k", AP_JUMP_KEEP_VISIBLE);
    aws->insert_option        ("center vertically", "v", AP_JUMP_FORCE_VCENTER);
    aws->insert_option        ("center",            "c", AP_JUMP_FORCE_CENTER);
    aws->update_option_menu();
    aws->at_newline();
}

static AWT_config_mapping_def tree_setting_config_mapping[] = {

    // main tree settings:
    { AWAR_DTREE_BASELINEWIDTH,    "line_width" },
    { AWAR_DTREE_BRANCH_STYLE,     "branch_style" },
    { AWAR_DTREE_SHOW_BRACKETS,    "show_brackets" },
    { AWAR_DTREE_GROUP_STYLE,      "group_style" },
    { AWAR_DTREE_GROUP_ORIENT,     "group_orientation" },
    { AWAR_DTREE_GREY_LEVEL,       "grey_level" },
    { AWAR_DTREE_GROUPCOUNTMODE,   "group_countmode" },
    { AWAR_DTREE_GROUPINFOPOS,     "group_infopos" },
    { AWAR_DTREE_VERICAL_DIST,     "vert_dist" },
    { AWAR_DTREE_GROUP_SCALE,      "group_scale" },
    { AWAR_DTREE_GROUP_DOWNSCALE,  "group_downscale" },
    { AWAR_DTREE_AUTO_JUMP,        "auto_jump" },
    { AWAR_DTREE_AUTO_JUMP_TREE,   "auto_jump_tree" },
    { AWAR_DTREE_AUTO_UNFOLD,      "auto_unfold" },

    // bootstrap sub window:
    { AWAR_DTREE_BOOTSTRAP_SHOW,   "show_bootstrap" },
    { AWAR_DTREE_BOOTSTRAP_MIN,    "bootstrap_min" },
    { AWAR_DTREE_BOOTSTRAP_MAX,    "bootstrap_max" },
    { AWAR_DTREE_BOOTSTRAP_STYLE,  "bootstrap_style" },
    { AWAR_DTREE_CIRCLE_SHOW,      "show_circle" },
    { AWAR_DTREE_CIRCLE_FILL,      "fill_circle" },
    { AWAR_DTREE_CIRCLE_ELLIPSE,   "use_ellipse" },
    { AWAR_DTREE_CIRCLE_ZOOM,      "circle_zoom" },
    { AWAR_DTREE_CIRCLE_LIMIT,     "circle_limit" },

    // expert settings:
    { AWAR_DTREE_ATTACH_SIZE,      "attach_size" },
    { AWAR_DTREE_ATTACH_LEN,       "attach_len" },
    { AWAR_DTREE_ATTACH_GROUP,     "attach_group" },
    { AWAR_DTREE_DENDRO_ZOOM_TEXT, "dendro_zoomtext" },
    { AWAR_DTREE_DENDRO_XPAD,      "dendro_xpadding" },
    { AWAR_DTREE_RADIAL_ZOOM_TEXT, "radial_zoomtext" },
    { AWAR_DTREE_RADIAL_XPAD,      "radial_xpadding" },

    { NULp, NULp }
};

static const int SCALER_WIDTH = 250; // pixel
static const int LABEL_WIDTH  = 30;  // char

static void insert_section_header(AW_window *aws, const char *title) {
    char *button_text = GBS_global_string_copy("%*s%s ]", LABEL_WIDTH+1, "[ ", title);
    aws->create_autosize_button(NULp, button_text);
    aws->at_newline();
    free(button_text);
}

static AW_window *create_tree_expert_settings_window(AW_root *aw_root) {
    static AW_window_simple *aws = NULp;
    if (!aws) {
        aws = new AW_window_simple;
        aws->init(aw_root, "TREE_EXPERT_SETUP", "Expert tree settings");

        aws->at(5, 5);
        aws->auto_space(5, 5);
        aws->label_length(LABEL_WIDTH);
        aws->button_length(8);

        aws->callback(AW_POPDOWN);
        aws->create_button("CLOSE", "CLOSE", "C");
        aws->callback(makeHelpCallback("nt_tree_settings_expert.hlp"));
        aws->create_button("HELP", "HELP", "H");
        aws->at_newline();

        insert_section_header(aws, "parent attach position");

        aws->label("Attach by size");
        aws->create_input_field_with_scaler(AWAR_DTREE_ATTACH_SIZE, 4, SCALER_WIDTH);
        aws->at_newline();

        aws->label("Attach by len");
        aws->create_input_field_with_scaler(AWAR_DTREE_ATTACH_LEN, 4, SCALER_WIDTH);
        aws->at_newline();

        aws->label("Attach (at groups)");
        aws->create_input_field_with_scaler(AWAR_DTREE_ATTACH_GROUP, 4, SCALER_WIDTH);
        aws->at_newline();

        insert_section_header(aws, "text zooming / padding");

        const int PAD_SCALER_WIDTH = SCALER_WIDTH-39;

        aws->label("Text zoom/pad (dendro)");
        aws->create_toggle(AWAR_DTREE_DENDRO_ZOOM_TEXT);
        aws->create_input_field_with_scaler(AWAR_DTREE_DENDRO_XPAD, 4, PAD_SCALER_WIDTH);
        aws->at_newline();

        aws->label("Text zoom/pad (radial)");
        aws->create_toggle(AWAR_DTREE_RADIAL_ZOOM_TEXT);
        aws->create_input_field_with_scaler(AWAR_DTREE_RADIAL_XPAD, 4, PAD_SCALER_WIDTH);
        aws->at_newline();

        aws->window_fit();
    }
    return aws;
}

static AW_window *create_tree_bootstrap_settings_window(AW_root *aw_root) {
    static AW_window_simple *aws = NULp;
    if (!aws) {
        aws = new AW_window_simple;
        aws->init(aw_root, "TREE_BOOT_SETUP", "Bootstrap display settings");

        aws->at(5, 5);
        aws->auto_space(5, 5);
        aws->label_length(LABEL_WIDTH);
        aws->button_length(8);

        aws->callback(AW_POPDOWN);
        aws->create_button("CLOSE", "CLOSE", "C");
        aws->callback(makeHelpCallback("nt_tree_settings_bootstrap.hlp"));
        aws->create_button("HELP", "HELP", "H");
        aws->at_newline();

        insert_section_header(aws, "visibility");

        aws->label("Show bootstraps");
        aws->create_toggle(AWAR_DTREE_BOOTSTRAP_SHOW);
        aws->at_newline();

        aws->label("Hide bootstraps below");
        aws->create_input_field_with_scaler(AWAR_DTREE_BOOTSTRAP_MIN, 4, SCALER_WIDTH);
        aws->at_newline();

        aws->label("Hide bootstraps above");
        aws->create_input_field_with_scaler(AWAR_DTREE_BOOTSTRAP_MAX, 4, SCALER_WIDTH);
        aws->at_newline();

        insert_section_header(aws, "style");

        aws->label("Bootstrap style");
        aws->create_option_menu(AWAR_DTREE_BOOTSTRAP_STYLE);
        aws->insert_default_option("percent%", "p", BS_PERCENT);
        aws->insert_option        ("percent",  "c", BS_PERCENT_NOSIGN);
        aws->insert_option        ("float",    "f", BS_FLOAT);
        aws->update_option_menu();
        aws->at_newline();

        insert_section_header(aws, "circles");

        aws->label("Show bootstrap circles");
        aws->create_toggle(AWAR_DTREE_CIRCLE_SHOW);
        aws->at_newline();

        aws->label("Greylevel of circles (%)");
        aws->create_input_field_with_scaler(AWAR_DTREE_CIRCLE_FILL, 4, SCALER_WIDTH);
        aws->at_newline();

        aws->label("Use ellipses");
        aws->create_toggle(AWAR_DTREE_CIRCLE_ELLIPSE);
        aws->at_newline();

        aws->label("Bootstrap circle zoom factor");
        aws->create_input_field_with_scaler(AWAR_DTREE_CIRCLE_ZOOM, 4, SCALER_WIDTH);
        aws->at_newline();

        aws->label("Bootstrap radius limit");
        aws->create_input_field_with_scaler(AWAR_DTREE_CIRCLE_LIMIT, 4, SCALER_WIDTH, AW_SCALER_EXP_LOWER);
        aws->at_newline();

        aws->window_fit();
    }
    return aws;
}

AW_window *TREE_create_settings_window(AW_root *aw_root) {
    static AW_window_simple *aws = NULp;
    if (!aws) {
        aws = new AW_window_simple;
        aws->init(aw_root, "TREE_SETUP", "Tree settings");
        aws->load_xfig("awt/tree_settings.fig");

        aws->at("close");
        aws->auto_space(5, 5);
        aws->label_length(LABEL_WIDTH);
        aws->button_length(8);

        aws->callback(AW_POPDOWN);
        aws->create_button("CLOSE", "CLOSE", "C");
        aws->callback(makeHelpCallback("nt_tree_settings.hlp"));
        aws->create_button("HELP", "HELP", "H");

        aws->at("button");

        insert_section_header(aws, "branches");

        aws->label("Base line width");
        aws->create_input_field_with_scaler(AWAR_DTREE_BASELINEWIDTH, 4, SCALER_WIDTH);
        aws->at_newline();

        aws->label("Branch style");
        aws->create_option_menu(AWAR_DTREE_BRANCH_STYLE);
        aws->insert_default_option("Rectangular",     "R", BS_RECTANGULAR);
        aws->insert_option        ("Diagonal",        "D", BS_DIAGONAL);
        aws->update_option_menu();
        aws->at_newline();

        insert_section_header(aws, "groups");

        aws->label("Show group brackets");
        aws->create_toggle(AWAR_DTREE_SHOW_BRACKETS);
        aws->at_newline();

        aws->label("Group style");
        aws->create_option_menu(AWAR_DTREE_GROUP_STYLE);
        aws->insert_default_option("Trapeze",  "z", GS_TRAPEZE);
        aws->insert_option        ("Triangle", "i", GS_TRIANGLE);
        aws->update_option_menu();
        aws->create_option_menu(AWAR_DTREE_GROUP_ORIENT);
        aws->insert_default_option("Top",      "T", GO_TOP);
        aws->insert_option        ("Bottom",   "B", GO_BOTTOM);
        aws->insert_option        ("Interior", "I", GO_INTERIOR);
        aws->insert_option        ("Exterior", "E", GO_EXTERIOR);
        aws->update_option_menu();
        aws->at_newline();

        aws->label("Greylevel of groups (%)");
        aws->create_input_field_with_scaler(AWAR_DTREE_GREY_LEVEL, 4, SCALER_WIDTH);
        aws->at_newline();

        aws->label("Show group counter");
        aws->create_option_menu(AWAR_DTREE_GROUPCOUNTMODE);
        aws->insert_default_option("None",            "N", GCM_NONE);
        aws->insert_option        ("Members",         "M", GCM_MEMBERS);
        aws->insert_option        ("Marked",          "a", GCM_MARKED);
        aws->insert_option        ("Marked/Members",  "b", GCM_BOTH);
        aws->insert_option        ("%Marked",         "%", GCM_PERCENT);
        aws->insert_option        ("%Marked/Members", "e", GCM_BOTH_PC);
        aws->update_option_menu();
        aws->at_newline();

        aws->label("Group counter position");
        aws->create_option_menu(AWAR_DTREE_GROUPINFOPOS);
        aws->insert_default_option("Attached",  "A", GIP_ATTACHED);
        aws->insert_option        ("Overlayed", "O", GIP_OVERLAYED);
        aws->insert_option        ("Separated", "a", GIP_SEPARATED);
        aws->update_option_menu();
        aws->at_newline();

        insert_section_header(aws, "vertical scaling");

        aws->label("Vertical distance");
        aws->create_input_field_with_scaler(AWAR_DTREE_VERICAL_DIST, 4, SCALER_WIDTH, AW_SCALER_EXP_LOWER);
        aws->at_newline();

        aws->label("Vertical group scaling");
        aws->create_input_field_with_scaler(AWAR_DTREE_GROUP_SCALE, 4, SCALER_WIDTH);
        aws->at_newline();

        aws->label("'Biggroup' scaling");
        aws->create_input_field_with_scaler(AWAR_DTREE_GROUP_DOWNSCALE, 4, SCALER_WIDTH);
        aws->at_newline();

        insert_section_header(aws, "auto focus");

        tree_insert_jump_option_menu(aws, "On species change", AWAR_DTREE_AUTO_JUMP);
        tree_insert_jump_option_menu(aws, "On tree change",    AWAR_DTREE_AUTO_JUMP_TREE);

        aws->label("Auto unfold selected species?");
        aws->create_toggle(AWAR_DTREE_AUTO_UNFOLD);

        // complete top area of window

        aws->at("config");
        AWT_insert_config_manager(aws, AW_ROOT_DEFAULT, "tree_settings", tree_setting_config_mapping);

        aws->button_length(19);

        aws->at("bv");
        aws->create_toggle(AWAR_DTREE_BOOTSTRAP_SHOW);

        aws->at("bootstrap");
        aws->callback(create_tree_bootstrap_settings_window);
        aws->create_button("bootstrap", "Bootstrap settings", "B");

        aws->at("expert");
        aws->callback(create_tree_expert_settings_window);
        aws->create_button("expert", "Expert settings", "E");
    }
    return aws;
}

// --------------------------------------------------------------------------------

AW_window *TREE_create_marker_settings_window(AW_root *root) {
    static AW_window_simple *aws = NULp;

    if (!aws) {
        aws = new AW_window_simple;

        aws->init(root, "MARKER_SETTINGS", "Tree marker settings");

        aws->auto_space(10, 10);

        aws->callback(AW_POPDOWN);
        aws->create_button("CLOSE", "CLOSE", "C");
        aws->callback(makeHelpCallback("nt_tree_marker_settings.hlp"));
        aws->create_button("HELP", "HELP", "H");
        aws->at_newline();

        const int FIELDSIZE  = 5;
        const int SCALERSIZE = 250;
        aws->label_length(35);

        aws->label("Group marked threshold");
        aws->create_input_field_with_scaler(AWAR_DTREE_GROUP_MARKED_THRESHOLD, FIELDSIZE, SCALERSIZE);

        aws->at_newline();

        aws->label("Group partially marked threshold");
        aws->create_input_field_with_scaler(AWAR_DTREE_GROUP_PARTIALLY_MARKED_THRESHOLD, FIELDSIZE, SCALERSIZE);

        aws->at_newline();

        aws->label("Marker width");
        aws->create_input_field_with_scaler(AWAR_DTREE_MARKER_WIDTH, FIELDSIZE, SCALERSIZE);

        aws->at_newline();

        aws->label("Partial marker greylevel");
        aws->create_input_field_with_scaler(AWAR_DTREE_PARTIAL_GREYLEVEL, FIELDSIZE, SCALERSIZE);

        aws->at_newline();
    }

    return aws;
}

// --------------------------------------------------------------------------------

#ifdef UNIT_TESTS
#include <test_unit.h>
#include <../../WINDOW/aw_common.hxx>

static void fake_AD_map_viewer_cb(GBDATA *, AD_MAP_VIEWER_TYPE) {}

static AW_rgb colors_def[] = {
    AW_NO_COLOR, AW_NO_COLOR, AW_NO_COLOR, AW_NO_COLOR, AW_NO_COLOR, AW_NO_COLOR,
    0x30b0e0,
    0xff8800, // AWT_GC_CURSOR
    0xa3b3cf, // AWT_GC_BRANCH_REMARK
    0x53d3ff, // AWT_GC_BOOTSTRAP
    0x808080, // AWT_GC_BOOTSTRAP_LIMITED
    0x000000, // AWT_GC_IRS_GROUP_BOX
    0xf0c000, // AWT_GC_ALL_MARKED
    0xbb8833, // AWT_GC_SOME_MARKED
    0x622300, // AWT_GC_NONE_MARKED
    0x977a0e, // AWT_GC_ONLY_ZOMBIES

    0x000000, // AWT_GC_BLACK
    0x808080, // AWT_GC_WHITE

    0xff0000, // AWT_GC_RED
    0x00ff00, // AWT_GC_GREEN
    0x0000ff, // AWT_GC_BLUE

    0xc0ff40, // AWT_GC_ORANGE
    0x40c0ff, // AWT_GC_AQUAMARIN
    0xf030b0, // AWT_GC_PURPLE

    0xffff00, // AWT_GC_YELLOW
    0x00ffff, // AWT_GC_CYAN
    0xff00ff, // AWT_GC_MAGENTA

    0xc0ff40, // AWT_GC_LAWNGREEN
    0x40c0ff, // AWT_GC_SKYBLUE
    0xf030b0, // AWT_GC_PINK

    0xd50000, // AWT_GC_FIRST_COLOR_GROUP
    0x00c0a0,
    0x00ff77,
    0xc700c7,
    0x0000ff,
    0xffce5b,
    0xab2323,
    0x008888,
    0x008800,
    0x880088,
    0x000088,
    0x888800,
    AW_NO_COLOR
};
static AW_rgb *fcolors       = colors_def;
static AW_rgb *dcolors       = colors_def;
static long    dcolors_count = ARRAY_ELEMS(colors_def);

class fake_AW_GC : public AW_GC {
    void wm_set_foreground_color(AW_rgb /*col*/) OVERRIDE {  }
    void wm_set_function(AW_function /*mode*/) OVERRIDE { td_assert(0); }
    void wm_set_lineattributes(short /*lwidth*/, AW_linestyle /*lstyle*/) OVERRIDE {}
    void wm_set_font(AW_font /*font_nr*/, int size, int */*found_size*/) OVERRIDE {
        unsigned int i;
        for (i = AW_FONTINFO_CHAR_ASCII_MIN; i <= AW_FONTINFO_CHAR_ASCII_MAX; i++) {
            set_char_size(i, size, 0, size-2); // good fake size for Courier 8pt
        }
    }
public:
    fake_AW_GC(AW_common *common_) : AW_GC(common_) {}
    int get_available_fontsizes(AW_font /*font_nr*/, int */*available_sizes*/) const OVERRIDE {
        td_assert(0);
        return 0;
    }
};

struct fake_AW_common : public AW_common {
    fake_AW_common()
        : AW_common(fcolors, dcolors, dcolors_count)
    {
        for (int gc = 0; gc < dcolors_count-AW_STD_COLOR_IDX_MAX; ++gc) { // gcs used in this example
            new_gc(gc);
            AW_GC *gcm = map_mod_gc(gc);
            gcm->set_line_attributes(1, AW_SOLID);
            gcm->set_function(AW_COPY);
            gcm->set_font(12, 8, NULp); // 12 is Courier (use monospaced here, cause font limits are faked)

            gcm->set_fg_color(colors_def[gc+AW_STD_COLOR_IDX_MAX]);
        }
    }
    ~fake_AW_common() OVERRIDE {}

    virtual AW_GC *create_gc() {
        return new fake_AW_GC(this);
    }
};

class fake_AWT_graphic_tree FINAL_TYPE : public AWT_graphic_tree {
    int         var_mode; // current range: [0..3]
    double      att_size, att_len;
    BranchStyle bstyle;

    void read_tree_settings() OVERRIDE {
        scaled_branch_distance = 1.0; // not final value!

        // var_mode is in range [0..3]
        // it is used to vary tree settings such that many different combinations get tested

        static const double group_attach[] = { 0.5, 1.0, 0.0, };
        static const double tested_greylevel[] = { 0.0, 0.25, 0.75, 1.0 };

        int mvar_mode = var_mode+int(tree_style)+int(group_style); // [0..3] + [0..5] + [0..1] = [0..9]

        groupScale.pow    = .33;
        groupScale.linear = (tree_style == AP_TREE_RADIAL) ? 7.0 : 1.0;
        group_greylevel   = tested_greylevel[mvar_mode%4];

        baselinewidth     = (var_mode == 3)+1;
        show_brackets     = (var_mode != 2);
        group_style       = ((var_mode%2) == (bstyle == BS_DIAGONAL)) ? GS_TRAPEZE : GS_TRIANGLE;
        group_orientation = GroupOrientation((var_mode+1)%4);
        attach_size       = att_size;
        attach_len        = att_len;
        attach_group      = group_attach[var_mode%3];
        branch_style      = bstyle;

        bconf.zoom_factor = 1.3;
        bconf.max_radius  = 1.95;
        bconf.show_circle = var_mode%3;
        bconf.fill_level  = 1.0 - group_greylevel; // for bootstrap circles use inverse shading of groups

        bconf.elipsoid      = var_mode%2;
        bconf.show_boots    = !(var_mode == 0 && bstyle == BS_DIAGONAL);        // hide BS in dendro_MN_diagonal.fig
        bconf.bootstrap_min = var_mode<2    ?   0 :  5;                         // remove BS<5%  for var_mode 0,1
        bconf.bootstrap_max = !(var_mode%2) ? 100 : 95;                         // remove BS>95% for var_mode 0,2
        if (var_mode != ((bconf.show_circle+branch_style)%3) && bconf.bootstrap_max == 100) {
            bconf.bootstrap_max = 99; // hide 100% branches in those figs previously excluded via 'auto_add_100'
        }

        bconf.style = (mvar_mode%4) ? BS_PERCENT : (int(group_style)%2 ? BS_PERCENT_NOSIGN : BS_FLOAT);

        group_info_pos = GIP_SEPARATED;
        switch (var_mode) {
            case 2: group_info_pos = GIP_ATTACHED;  break;
            case 3: group_info_pos = GIP_OVERLAYED; break;
        }

        switch (var_mode) {
            case 0: group_count_mode = GCM_MEMBERS; break;
            case 1: group_count_mode = GCM_NONE;  break;
            case 2: group_count_mode = (tree_style%2) ? GCM_MARKED : GCM_PERCENT;  break;
            case 3: group_count_mode = (tree_style%2) ? GCM_BOTH   : GCM_BOTH_PC;  break;
        }
    }

public:
    fake_AWT_graphic_tree(GBDATA *gbmain, const char *selected_species)
        : AWT_graphic_tree(NULp, gbmain, fake_AD_map_viewer_cb),
          var_mode(0),
          att_size(0),
          att_len(0),
          bstyle(BS_RECTANGULAR)
    {
        species_name      = strdup(selected_species);
        exports.modifying = 1; // hack to workaround need for AWT_auto_refresh
    }

    void set_var_mode(int mode) { var_mode = mode; }
    void set_attach(double asize, double alen) { att_size = asize; att_len  = alen; }
    void set_branchstyle(BranchStyle bstyle_) { bstyle = bstyle_; }

    void test_show_tree(AW_device *device, bool force_compute) {
        if (force_compute) {
            // force reload and recompute (otherwise changing groupScale.linear has DELAYED effect)
            read_tree_settings();
            get_root_node()->compute_tree();
        }
        check_for_DB_update(get_gbmain()); // hack to workaround need for AWT_auto_refresh
        show(device);
    }

    void test_print_tree(AW_device_print *print_device, AP_tree_display_style style, bool show_handles) {
        const int      SCREENSIZE = 541; // use a prime as screensize to reduce rounding errors
        AW_device_size size_device(print_device->get_common());

        size_device.reset();
        size_device.zoom(1.0);
        size_device.set_filter(AW_SIZE|AW_SIZE_UNSCALED);
        test_show_tree(&size_device, true);

        Rectangle drawn = size_device.get_size_information();

        td_assert(drawn.surface() >= 0.0);

        double zoomx = SCREENSIZE/drawn.width();
        double zoomy = SCREENSIZE/drawn.height();
        double zoom  = 0.0;

        switch (style) {
            case AP_LIST_SIMPLE:
            case AP_TREE_RADIAL:
                zoom = std::max(zoomx, zoomy);
                break;

            case AP_TREE_NORMAL:
            case AP_TREE_IRS:
                zoom = zoomx;
                break;

            case AP_LIST_NDS:
                zoom = 1.0;
                break;
        }

        if (!nearlyEqual(zoom, 1.0)) {
            // recalculate size
            size_device.restart_tracking();
            size_device.reset();
            size_device.zoom(zoom);
            size_device.set_filter(AW_SIZE|AW_SIZE_UNSCALED);
            test_show_tree(&size_device, false);
        }

        drawn = size_device.get_size_information();

        const AW_borders& text_overlap = size_device.get_unscaleable_overlap();
        Rectangle         drawn_text   = size_device.get_size_information_inclusive_text();

        int            EXTRA = SCREENSIZE*0.05;
        AW_screen_area clipping;

        clipping.l = 0; clipping.r = drawn.width()+text_overlap.l+text_overlap.r + 2*EXTRA;
        clipping.t = 0; clipping.b = drawn.height()+text_overlap.t+text_overlap.b + 2*EXTRA;

        print_device->get_common()->set_screen(clipping);
        print_device->set_filter(AW_PRINTER|(show_handles ? AW_PRINTER_EXT : 0));
        print_device->reset();

        print_device->zoom(zoom);

        Rectangle drawn_world      = print_device->rtransform(drawn);
        Rectangle drawn_text_world = print_device->rtransform(drawn_text);

        Vector extra_shift  = Vector(EXTRA, EXTRA);
        Vector corner_shift = -Vector(drawn.upper_left_corner());
        Vector text_shift = Vector(text_overlap.l, text_overlap.t);

        Vector offset(extra_shift+corner_shift+text_shift);
        print_device->set_offset(offset/(zoom*zoom)); // dont really understand this, but it does the right shift

        test_show_tree(print_device, false);
        print_device->box(AWT_GC_CURSOR,        AW::FillStyle::EMPTY, drawn_world);
        print_device->box(AWT_GC_IRS_GROUP_BOX, AW::FillStyle::EMPTY, drawn_text_world);
    }
};

void fake_AW_init_color_groups();
void AW_init_color_groups(AW_root *awr, AW_default def);

static bool replaceFirstRemark(TreeNode *node, const char *oldRem, const char *newRem) {
    if (!node->is_leaf()) {
        const char *rem = node->get_remark();
        if (rem && strcmp(rem, oldRem) == 0) {
            node->set_remark(newRem);
            return true;
        }

        return
            replaceFirstRemark(node->get_leftson(), oldRem, newRem) ||
            replaceFirstRemark(node->get_rightson(), oldRem, newRem);
    }
    return false;
}

void TEST_treeDisplay() {
    GB_shell  shell;
    GBDATA   *gb_main = GB_open("../../demo.arb", "r");

    fake_AWT_graphic_tree agt(gb_main, "OctSprin");
    fake_AW_common        fake_common;

    AW_device_print print_dev(&fake_common);
    AW_init_color_group_defaults(NULp);
    fake_AW_init_color_groups();

    agt.init(new AliView(gb_main), NULp, true, false);

    {
        GB_transaction ta(gb_main);
        ASSERT_RESULT(const char *, NULp, agt.load_from_DB(NULp, "tree_test")); // calls compute_tree once
    }

    const char *spoolnameof[] = {
        "dendro",
        "radial",
        "irs",
        "nds",
        NULp, // "simple", (too simple, need no test)
    };

    // modify some tree comments
    {
        AP_tree *rootNode = agt.get_root_node();
        TEST_EXPECT(replaceFirstRemark(rootNode, "97%", "hello")); // is displayed when bootstrap display is OFF (e.g. in dendro_MN_diagonal.fig)
        TEST_EXPECT(replaceFirstRemark(rootNode, "44%", "100%"));

        GB_transaction ta(gb_main);
        ASSERT_RESULT(const char *, NULp, agt.save_to_DB(NULp, "tree_test"));
    }

    for (int show_handles = 0; show_handles <= 1; ++show_handles) {
        for (int color = 0; color <= 1; ++color) {
            print_dev.set_color_mode(color);
            // for (int istyle = AP_TREE_NORMAL; istyle <= AP_LIST_SIMPLE; ++istyle) {
            for (int istyle = AP_LIST_SIMPLE; istyle >= AP_TREE_NORMAL; --istyle) {
                AP_tree_display_style style = AP_tree_display_style(istyle);
                if (spoolnameof[style]) {
                    char *spool_name = GBS_global_string_copy("display/%s_%c%c", spoolnameof[style], "MC"[color], "NH"[show_handles]);

                    agt.set_tree_style(style, NULp);

                    int var_mode = show_handles+2*color;

                    static struct AttachSettings {
                        const char *suffix;
                        double      bySize;
                        double      byLength;
                    } attach_settings[] = {
                        { "",            -1,  0 }, // [size only] traditional attach point (produces old test results)
                        { "_long",        0,  1 }, // [len only]  attach at long branch
                        { "_shortSmall", -1, -1 }, // [both]      attach at short+small branch
                        { "_centered",    0,  0 }, // [none]      center attach points
                        { NULp,           0,  0 },
                    };

                    for (int attach_style = 0; attach_settings[attach_style].suffix; ++attach_style) {
                        if (attach_style>0) {
                            if (style != AP_TREE_NORMAL) continue;  // test attach-styles only for dendrogram-view
                            if (attach_style != var_mode) continue; // do not create too many permutations
                        }

                        const AttachSettings& SETT = attach_settings[attach_style];
                        char *spool_name2 = GBS_global_string_copy("%s%s", spool_name, SETT.suffix);

                        for (BranchStyle bstyle = BS_RECTANGULAR; bstyle<=BS_DIAGONAL; bstyle = BranchStyle(bstyle+1)) {
                            if (bstyle != BS_RECTANGULAR) { // test alternate branch-styles only ..
                                if (istyle != AP_TREE_NORMAL) continue; // .. for dendrogram view
                                if (attach_style != 0 && attach_style != 3) continue; // .. for traditional and centered attach_points
                            }

                            static const char *suffix[] = {
                                "",
                                "_diagonal",
                            };

                            char *spool_name3 = GBS_global_string_copy("%s%s", spool_name2, suffix[bstyle]);

// #define TEST_AUTO_UPDATE // dont test, instead update expected results
                            {
                                char *spool_file     = GBS_global_string_copy("%s_curr.fig", spool_name3);
                                char *spool_expected = GBS_global_string_copy("%s.fig", spool_name3);

#if defined(TEST_AUTO_UPDATE)
#warning TEST_AUTO_UPDATE is active (non-default)
                                TEST_EXPECT_NO_ERROR(print_dev.open(spool_expected));
#else
                                TEST_EXPECT_NO_ERROR(print_dev.open(spool_file));
#endif

                                {
                                    GB_transaction ta(gb_main);
                                    agt.set_var_mode(var_mode);
                                    agt.set_attach(SETT.bySize, SETT.byLength);
                                    agt.set_branchstyle(bstyle);
                                    agt.test_print_tree(&print_dev, style, show_handles);
                                }

                                print_dev.close();

#if !defined(TEST_AUTO_UPDATE)
                                TEST_EXPECT_TEXTFILES_EQUAL(spool_expected, spool_file);
                                TEST_EXPECT_ZERO_OR_SHOW_ERRNO(unlink(spool_file));
#endif
                                free(spool_expected);
                                free(spool_file);
                            }
                            free(spool_name3);
                        }
                        free(spool_name2);
                    }
                    free(spool_name);
                }
            }
        }
    }

    GB_close(gb_main);
}

#endif // UNIT_TESTS

