| 1 | // =============================================================== // |
|---|
| 2 | // // |
|---|
| 3 | // File : AW_device_click.cxx // |
|---|
| 4 | // Purpose : Detect which graphical element is "nearby" // |
|---|
| 5 | // a given mouse position // |
|---|
| 6 | // // |
|---|
| 7 | // Institute of Microbiology (Technical University Munich) // |
|---|
| 8 | // http://www.arb-home.de/ // |
|---|
| 9 | // // |
|---|
| 10 | // =============================================================== // |
|---|
| 11 | |
|---|
| 12 | #include "aw_common.hxx" |
|---|
| 13 | #include "aw_device_click.hxx" |
|---|
| 14 | #include <algorithm> |
|---|
| 15 | |
|---|
| 16 | using namespace AW; |
|---|
| 17 | |
|---|
| 18 | // ------------------------ |
|---|
| 19 | // AW_device_click |
|---|
| 20 | |
|---|
| 21 | AW_device_click::AW_device_click(AW_common *common_) |
|---|
| 22 | : AW_simple_device(common_) |
|---|
| 23 | { |
|---|
| 24 | init_click(Origin, AWT_NO_CATCH, AW_ALL_DEVICES); |
|---|
| 25 | } |
|---|
| 26 | |
|---|
| 27 | void AW_device_click::init_click(const AW::Position& click, int max_distance, AW_bitset filteri) { |
|---|
| 28 | mouse = click; |
|---|
| 29 | filter = filteri; |
|---|
| 30 | |
|---|
| 31 | max_distance_line = max_distance; |
|---|
| 32 | max_distance_text = max_distance; |
|---|
| 33 | |
|---|
| 34 | opt_line = AW_clicked_line(); |
|---|
| 35 | opt_text = AW_clicked_text(); |
|---|
| 36 | opt_box = AW_clicked_box(); |
|---|
| 37 | opt_polygon = AW_clicked_polygon(); |
|---|
| 38 | } |
|---|
| 39 | |
|---|
| 40 | AW_DEVICE_TYPE AW_device_click::type() { |
|---|
| 41 | return AW_DEVICE_CLICK; |
|---|
| 42 | } |
|---|
| 43 | |
|---|
| 44 | bool AW_device_click::line_impl(int /*gc*/, const AW::LineVector& Line, AW_bitset filteri) { |
|---|
| 45 | if (!(filteri & filter)) return false; // needed for motif only? |
|---|
| 46 | |
|---|
| 47 | LineVector transLine = transform(Line); |
|---|
| 48 | LineVector clippedLine; |
|---|
| 49 | bool drawflag = clip(transLine, clippedLine); |
|---|
| 50 | if (drawflag) { |
|---|
| 51 | double nearest_rel_pos; |
|---|
| 52 | Position nearest = nearest_linepoint(mouse, clippedLine, nearest_rel_pos); |
|---|
| 53 | double distance = Distance(mouse, nearest); |
|---|
| 54 | |
|---|
| 55 | if (distance < max_distance_line) { |
|---|
| 56 | max_distance_line = distance; |
|---|
| 57 | opt_line.assign(Line, distance, nearest_rel_pos, click_cd); |
|---|
| 58 | } |
|---|
| 59 | } |
|---|
| 60 | return drawflag; |
|---|
| 61 | } |
|---|
| 62 | |
|---|
| 63 | bool AW_device_click::box_impl(int gc, AW::FillStyle filled, const AW::Rectangle& rect, AW_bitset filteri) { |
|---|
| 64 | if (!(filteri & filter)) return false; // needed for motif only? |
|---|
| 65 | |
|---|
| 66 | int dist = -1; |
|---|
| 67 | if (filled.is_empty()) { |
|---|
| 68 | LocallyModify<AW_clicked_line> saveLine(opt_line, AW_clicked_line()); |
|---|
| 69 | LocallyModify<int> saveDist(max_distance_line); |
|---|
| 70 | |
|---|
| 71 | if (!generic_box(gc, rect, filteri)) return false; |
|---|
| 72 | if (!opt_line.does_exist()) return true; // no click near any borderline detected |
|---|
| 73 | |
|---|
| 74 | dist = opt_line.get_distance(); |
|---|
| 75 | } |
|---|
| 76 | else { |
|---|
| 77 | Rectangle transRect = transform(rect); |
|---|
| 78 | if (!transRect.contains(mouse)) { |
|---|
| 79 | return box_impl(gc, FillStyle::EMPTY, rect, filteri); // otherwise use min. dist to box frame |
|---|
| 80 | } |
|---|
| 81 | dist = 0; // if inside rect -> use zero distance |
|---|
| 82 | } |
|---|
| 83 | |
|---|
| 84 | aw_assert(dist != -1); |
|---|
| 85 | if (!opt_box.does_exist() || dist<opt_box.get_distance()) { |
|---|
| 86 | opt_box.assign(rect, dist, 0.0, click_cd); |
|---|
| 87 | } |
|---|
| 88 | return true; |
|---|
| 89 | } |
|---|
| 90 | |
|---|
| 91 | inline double kpt2(const Position& a, const Position& b, const Position& c) { |
|---|
| 92 | if (a.xpos() == b.xpos() && a.ypos() == b.ypos()) return 0; |
|---|
| 93 | if (a.ypos() <= b.ypos() || a.ypos() > c.ypos()) return 1; |
|---|
| 94 | |
|---|
| 95 | double d = (b.xpos()-a.xpos()) * (c.ypos()-a.ypos()) - (b.ypos()-a.ypos()) * (c.xpos()-a.xpos()); |
|---|
| 96 | return d>0 ? -1 : (d<0 ? 1 : 0); |
|---|
| 97 | } |
|---|
| 98 | inline double KreuzProdTest(const Position& a, const Position& b, const Position& c) { |
|---|
| 99 | if (a.ypos() == b.ypos() && b.ypos() == c.ypos()) { |
|---|
| 100 | if (is_between(b.xpos(), a.xpos(), c.xpos())) return 0; |
|---|
| 101 | return 1; |
|---|
| 102 | } |
|---|
| 103 | return (b.ypos()>c.ypos()) ? kpt2(a, c, b) : kpt2(a, b, c); |
|---|
| 104 | } |
|---|
| 105 | static bool polygon_contains(const Position& mouse, int npos, const Position *pos) { |
|---|
| 106 | // Jordan test for "Position inside polygon?" (see https://de.wikipedia.org/wiki/Punkt-in-Polygon-Test_nach_Jordan) |
|---|
| 107 | double t = -1 * KreuzProdTest(mouse, pos[npos-1], pos[0]); |
|---|
| 108 | for (int i = 1; i<npos; ++i) { |
|---|
| 109 | t = t*KreuzProdTest(mouse, pos[i-1], pos[i]); |
|---|
| 110 | } |
|---|
| 111 | return t>=0; |
|---|
| 112 | } |
|---|
| 113 | |
|---|
| 114 | bool AW_device_click::polygon_impl(int gc, AW::FillStyle filled, int npos, const AW::Position *pos, AW_bitset filteri) { |
|---|
| 115 | if (!(filteri & filter)) return false; // needed for motif only? |
|---|
| 116 | |
|---|
| 117 | int dist = -1; |
|---|
| 118 | aw_assert(npos>2); |
|---|
| 119 | |
|---|
| 120 | if (filled.is_empty()) { |
|---|
| 121 | LocallyModify<AW_clicked_line> saveLine(opt_line, AW_clicked_line()); |
|---|
| 122 | LocallyModify<int> saveDist(max_distance_line); |
|---|
| 123 | |
|---|
| 124 | if (!generic_polygon(gc, npos, pos, filteri)) return false; |
|---|
| 125 | if (!opt_line.does_exist()) return true; // no click near any borderline detected |
|---|
| 126 | |
|---|
| 127 | dist = opt_line.get_distance(); |
|---|
| 128 | } |
|---|
| 129 | else { |
|---|
| 130 | bool inside; |
|---|
| 131 | { |
|---|
| 132 | AW::Position *tpos = new AW::Position[npos]; |
|---|
| 133 | for (int i = 0; i<npos; ++i) { |
|---|
| 134 | tpos[i] = transform(pos[i]); |
|---|
| 135 | } |
|---|
| 136 | inside = polygon_contains(mouse, npos, tpos); |
|---|
| 137 | delete [] tpos; |
|---|
| 138 | } |
|---|
| 139 | |
|---|
| 140 | if (!inside) { |
|---|
| 141 | return polygon_impl(gc, FillStyle::EMPTY, npos, pos, filteri); |
|---|
| 142 | } |
|---|
| 143 | dist = 0; // if inside polygon -> use zero distance |
|---|
| 144 | } |
|---|
| 145 | |
|---|
| 146 | aw_assert(dist != -1); |
|---|
| 147 | if (!opt_polygon.does_exist() || dist<opt_polygon.get_distance()) { |
|---|
| 148 | opt_polygon.assign(npos, pos, dist, 0.0, click_cd); |
|---|
| 149 | } |
|---|
| 150 | return true; |
|---|
| 151 | } |
|---|
| 152 | |
|---|
| 153 | bool AW_device_click::text_impl(int gc, const SizedCstr& cstr, const AW::Position& pos, AW_pos alignment, AW_bitset filteri) { |
|---|
| 154 | if (!(filteri & filter)) return false; |
|---|
| 155 | |
|---|
| 156 | AW_pos X0, Y0; // Transformed pos |
|---|
| 157 | this->transform(pos.xpos(), pos.ypos(), X0, Y0); |
|---|
| 158 | |
|---|
| 159 | const AW_GC *gcm = get_common()->map_gc(gc); |
|---|
| 160 | const AW_font_limits& font = gcm->get_font_limits(); |
|---|
| 161 | |
|---|
| 162 | AW_pos Y1 = Y0+font.descent; |
|---|
| 163 | Y0 = Y0-font.ascent; |
|---|
| 164 | |
|---|
| 165 | // Fast check text against top/bottom clip |
|---|
| 166 | const AW_screen_area& clipRect = get_cliprect(); |
|---|
| 167 | if (clipRect.t == 0) { |
|---|
| 168 | if (Y1 < clipRect.t) return false; |
|---|
| 169 | } |
|---|
| 170 | else { |
|---|
| 171 | if (Y0 < clipRect.t) return false; |
|---|
| 172 | } |
|---|
| 173 | |
|---|
| 174 | if (clipRect.b == get_common()->get_screen().b) { |
|---|
| 175 | if (Y0 > clipRect.b) return false; |
|---|
| 176 | } |
|---|
| 177 | else { |
|---|
| 178 | if (Y1 > clipRect.b) return false; |
|---|
| 179 | } |
|---|
| 180 | |
|---|
| 181 | // vertical check mouse against textsurrounding |
|---|
| 182 | int dist2text = 0; // exact hit -> distance == 0 |
|---|
| 183 | |
|---|
| 184 | // vertical check against textborders |
|---|
| 185 | if (mouse.ypos() > Y1) { // above text |
|---|
| 186 | int ydist = mouse.ypos()-Y1; |
|---|
| 187 | if (ydist > max_distance_text) return false; // too far above |
|---|
| 188 | dist2text = ydist; |
|---|
| 189 | } |
|---|
| 190 | else if (mouse.ypos() < Y0) { // below text |
|---|
| 191 | int ydist = Y0-mouse.ypos(); |
|---|
| 192 | if (ydist > max_distance_text) return false; // too far below |
|---|
| 193 | dist2text = ydist; |
|---|
| 194 | } |
|---|
| 195 | |
|---|
| 196 | // align text |
|---|
| 197 | int text_width = gcm->get_string_size(cstr); |
|---|
| 198 | |
|---|
| 199 | X0 = x_alignment(X0, text_width, alignment); |
|---|
| 200 | AW_pos X1 = X0+text_width; |
|---|
| 201 | |
|---|
| 202 | // check against left/right clipping areas |
|---|
| 203 | if (X1 < clipRect.l) return false; |
|---|
| 204 | if (X0 > clipRect.r) return false; |
|---|
| 205 | |
|---|
| 206 | // horizontal check against textborders |
|---|
| 207 | if (mouse.xpos() > X1) { // right of text |
|---|
| 208 | int xdist = mouse.xpos()-X1; |
|---|
| 209 | if (xdist > max_distance_text) return false; // too far right |
|---|
| 210 | dist2text = std::max(xdist, dist2text); |
|---|
| 211 | } |
|---|
| 212 | else if (mouse.xpos() < X0) { // left of text |
|---|
| 213 | int xdist = X0-mouse.xpos(); |
|---|
| 214 | if (xdist > max_distance_text) return false; // too far left |
|---|
| 215 | dist2text = std::max(xdist, dist2text); |
|---|
| 216 | } |
|---|
| 217 | |
|---|
| 218 | max_distance_text = dist2text; // exact hit -> distance = 0 |
|---|
| 219 | |
|---|
| 220 | if (!opt_text.does_exist() || // first candidate |
|---|
| 221 | (opt_text.get_distance()>dist2text)) // previous candidate had greater distance to click |
|---|
| 222 | { |
|---|
| 223 | Rectangle textArea(LineVector(X0, Y0, X1, Y1)); |
|---|
| 224 | |
|---|
| 225 | LineVector orientation = textArea.bigger_extent(); |
|---|
| 226 | LineVector clippedOrientation; |
|---|
| 227 | |
|---|
| 228 | bool visible = clip(orientation, clippedOrientation); |
|---|
| 229 | if (visible) { |
|---|
| 230 | double nearest_rel_pos; |
|---|
| 231 | Position nearest = nearest_linepoint(mouse, clippedOrientation, nearest_rel_pos); |
|---|
| 232 | |
|---|
| 233 | opt_text.assign(rtransform(textArea), max_distance_text, nearest_rel_pos, click_cd); |
|---|
| 234 | } |
|---|
| 235 | } |
|---|
| 236 | return true; |
|---|
| 237 | } |
|---|
| 238 | |
|---|
| 239 | const AW_clicked_element *AW_device_click::best_click(ClickPreference prefer) { |
|---|
| 240 | // returns the element with lower distance (to mouse-click- or key-"click"-position). |
|---|
| 241 | // or NULp (if no element was found inside catch-distance) |
|---|
| 242 | // |
|---|
| 243 | // Note: during drag/drop the target element at mouse position |
|---|
| 244 | // is only updated if requested using AWT_graphic::drag_target_detection |
|---|
| 245 | // see ../AWT/AWT_canvas.cxx@motion_event |
|---|
| 246 | |
|---|
| 247 | const AW_clicked_element *bestClick = NULp; |
|---|
| 248 | |
|---|
| 249 | if (prefer == PREFER_LINE && opt_line.does_exist()) bestClick = &opt_line; |
|---|
| 250 | if (prefer == PREFER_TEXT && opt_text.does_exist()) bestClick = &opt_text; |
|---|
| 251 | |
|---|
| 252 | if (!bestClick) { |
|---|
| 253 | const AW_clicked_element *maybeClicked[] = { |
|---|
| 254 | // earlier elements are preferred over later elements |
|---|
| 255 | &opt_polygon, |
|---|
| 256 | &opt_box, |
|---|
| 257 | &opt_line, |
|---|
| 258 | &opt_text, |
|---|
| 259 | }; |
|---|
| 260 | |
|---|
| 261 | for (size_t i = 0; i<ARRAY_ELEMS(maybeClicked); ++i) { |
|---|
| 262 | if (maybeClicked[i]->does_exist()) { |
|---|
| 263 | if (!bestClick || maybeClicked[i]->get_distance()<bestClick->get_distance()) { |
|---|
| 264 | bestClick = maybeClicked[i]; |
|---|
| 265 | } |
|---|
| 266 | } |
|---|
| 267 | } |
|---|
| 268 | } |
|---|
| 269 | |
|---|
| 270 | return bestClick; |
|---|
| 271 | } |
|---|
| 272 | |
|---|
| 273 | AW::Rectangle AW_clicked_polygon::get_bounding_box() const { |
|---|
| 274 | Rectangle box = bounding_box(pos[0], pos[1]); |
|---|
| 275 | for (int i = 2; i<npos; ++i) { |
|---|
| 276 | box = bounding_box(box, pos[i]); |
|---|
| 277 | } |
|---|
| 278 | return box; |
|---|
| 279 | } |
|---|
| 280 | |
|---|
| 281 | int AW_clicked_line::indicate_selected(AW_device *d, int gc) const { |
|---|
| 282 | return d->line(gc, line); |
|---|
| 283 | } |
|---|
| 284 | int AW_clicked_text::indicate_selected(AW_device *d, int gc) const { |
|---|
| 285 | return d->box(gc, AW::FillStyle::SOLID, textArea); |
|---|
| 286 | } |
|---|
| 287 | int AW_clicked_box::indicate_selected(AW_device *d, int gc) const { |
|---|
| 288 | return d->box(gc, AW::FillStyle::SOLID, box); |
|---|
| 289 | } |
|---|
| 290 | int AW_clicked_polygon::indicate_selected(AW_device *d, int gc) const { |
|---|
| 291 | return d->polygon(gc, AW::FillStyle::SOLID, npos, pos); |
|---|
| 292 | } |
|---|
| 293 | |
|---|