001package jmri.web.servlet.frameimage; 002 003import static jmri.server.json.JSON.NAME; 004import static jmri.server.json.JSON.URL; 005import static jmri.web.servlet.ServletUtil.UTF8; 006 007import com.fasterxml.jackson.databind.ObjectMapper; 008import com.fasterxml.jackson.databind.node.ArrayNode; 009import com.fasterxml.jackson.databind.node.ObjectNode; 010 011import java.awt.*; 012import java.awt.event.MouseEvent; 013import java.awt.event.MouseListener; 014import java.awt.image.BufferedImage; 015import java.io.ByteArrayOutputStream; 016import java.io.IOException; 017import java.io.UnsupportedEncodingException; 018import java.net.URLDecoder; 019import java.net.URLEncoder; 020import java.text.MessageFormat; 021import java.util.Arrays; 022import java.util.Date; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Map; 027 028import javax.annotation.CheckForNull; 029import javax.annotation.Nonnull; 030import javax.imageio.ImageIO; 031import javax.servlet.ServletException; 032import javax.servlet.annotation.WebServlet; 033import javax.servlet.http.HttpServlet; 034import javax.servlet.http.HttpServletRequest; 035import javax.servlet.http.HttpServletResponse; 036import javax.swing.AbstractButton; 037import javax.swing.JButton; 038import javax.swing.JCheckBox; 039import javax.swing.JDialog; 040import javax.swing.JFrame; 041import javax.swing.JRadioButton; 042import javax.swing.JToggleButton; 043 044import jmri.InstanceManager; 045import jmri.jmrit.display.Editor; 046import jmri.jmrit.display.Positionable; 047import jmri.server.json.JSON; 048import jmri.server.json.JsonException; 049import jmri.server.json.util.JsonUtilHttpService; 050import jmri.util.JmriJFrame; 051import jmri.util.swing.JDialogListener; 052import jmri.util.swing.JmriMouseEvent; 053import jmri.web.server.WebServerPreferences; 054 055import org.openide.util.lookup.ServiceProvider; 056import org.slf4j.Logger; 057import org.slf4j.LoggerFactory; 058 059/** 060 * A simple servlet that returns a JMRI window as a PNG image or enclosing HTML 061 * file. 062 * <p> 063 * The suffix of the request determines which. <dl> 064 * <dt>.html<dd>Returns a HTML file that displays the frame enabled for clicking 065 * via server side image map; see the .properties file for the content 066 * <dt>.png<dd>Just return the image <dt>no name<dd>Return an HTML page with 067 * links to available images </dl> 068 * <p> 069 * The associated .properties file contains the HTML fragments used to form 070 * replies. 071 * <p> 072 * Parts taken from Core Web Programming from Prentice Hall and Sun Microsystems 073 * Press, http://www.corewebprogramming.com/. © 2001 Marty Hall and Larry 074 * Brown; may be freely used or adapted. 075 * 076 * @author Modifications by Bob Jacobsen Copyright 2005, 2006, 2008 077 */ 078@WebServlet(name = "FrameServlet", 079 urlPatterns = {"/frame"}) 080@ServiceProvider(service = HttpServlet.class) 081public class JmriJFrameServlet extends HttpServlet { 082 083 void sendClick(String name, @Nonnull Component c, int xg, int yg, Container frameContentPane) { // global positions 084 int x = xg - c.getLocation().x; 085 int y = yg - c.getLocation().y; 086 // log.debug("component is {}", c); 087 log.debug("Local click at {},{}", x, y); 088 089 if (c.getClass().equals(JButton.class)) { 090 ((AbstractButton) c).doClick(); 091 } else if (c.getClass().equals(JToggleButton.class)) { 092 ((AbstractButton) c).doClick(); 093 } else if (c.getClass().equals(JCheckBox.class)) { 094 ((AbstractButton) c).doClick(); 095 } else if (c.getClass().equals(JRadioButton.class)) { 096 ((AbstractButton) c).doClick(); 097 } else if (MouseListener.class.isAssignableFrom(c.getClass())) { 098 log.debug("Invoke directly on MouseListener, at {},{}", x, y); 099 sendClickSequence((MouseListener) c, c, x, y); 100 } else if (c instanceof jmri.jmrit.display.MultiSensorIcon) { 101 log.debug("Invoke Clicked on MultiSensorIcon"); 102 JmriMouseEvent e = new JmriMouseEvent(c, 103 JmriMouseEvent.MOUSE_CLICKED, 104 0, // time 105 0, // modifiers 106 xg, yg, // this component expects global positions for some reason 107 1, // one click 108 false // not a popup 109 ); 110 ((Positionable) c).doMouseClicked(e); 111 } else if (Positionable.class.isAssignableFrom(c.getClass())) { 112 log.debug("Invoke Pressed, Released and Clicked on Positionable"); 113 JmriMouseEvent e = new JmriMouseEvent(c, 114 JmriMouseEvent.MOUSE_PRESSED, 115 0, // time 116 0, // modifiers 117 x, y, // x, y not in this component? 118 1, // one click 119 false // not a popup 120 ); 121 ((Positionable) c).doMousePressed(e); 122 123 e = new JmriMouseEvent(c, 124 JmriMouseEvent.MOUSE_RELEASED, 125 0, // time 126 0, // modifiers 127 x, y, // x, y not in this component? 128 1, // one click 129 false // not a popup 130 ); 131 ((Positionable) c).doMouseReleased(e); 132 133 e = new JmriMouseEvent(c, 134 JmriMouseEvent.MOUSE_CLICKED, 135 0, // time 136 0, // modifiers 137 x, y, // x, y not in this component? 138 1, // one click 139 false // not a popup 140 ); 141 ((Positionable) c).doMouseClicked(e); 142 } else { 143 if ( c instanceof JButton ){ 144 ((JButton)c).doClick(); 145 return; 146 } 147 MouseListener[] la = c.getMouseListeners(); 148 log.debug("Invoke {} contained mouse listeners", la.length); 149 log.debug("component is {}", c); 150 /* 151 * Using c.getLocation() above we adjusted the click position for 152 * the offset of the control relative to the frame. That works fine 153 * in the cases above. In this case getLocation only provides the 154 * offset of the control relative to the Component. So we also need 155 * to adjust the click position for the offset of the Component 156 * relative to the frame. 157 */ 158 // was incorrect for zoomed panels, turned off 159 // Point pc = c.getLocationOnScreen(); 160 // Point pf = FrameContentPane.getLocationOnScreen(); 161 // x -= (int)(pc.getX() - pf.getX()); 162 // y -= (int)(pc.getY() - pf.getY()); 163 for (MouseListener ml : la) { 164 log.debug("Send click sequence at {},{}", x, y); 165 sendClickSequence(ml, c, x, y); 166 } 167 } 168 } 169 170 private void sendClickSequence(MouseListener m, Component c, int x, int y) { 171 /* 172 * create the sequence of mouse events needed to click on a control: 173 * MOUSE_ENTERED MOUSE_PRESSED MOUSE_RELEASED MOUSE_CLICKED 174 */ 175 MouseEvent e = new MouseEvent(c, 176 MouseEvent.MOUSE_ENTERED, 177 0, // time 178 0, // modifiers 179 x, y, // x, y not in this component? 180 1, // one click 181 false // not a popup 182 ); 183 m.mouseEntered(e); 184 e = new MouseEvent(c, 185 MouseEvent.MOUSE_PRESSED, 186 0, // time 187 0, // modifiers 188 x, y, // x, y not in this component? 189 1, // one click 190 false, // not a popup 191 MouseEvent.BUTTON1); 192 m.mousePressed(e); 193 e = new MouseEvent(c, 194 MouseEvent.MOUSE_RELEASED, 195 0, // time 196 0, // modifiers 197 x, y, // x, y not in this component? 198 1, // one click 199 false, // not a popup 200 MouseEvent.BUTTON1); 201 m.mouseReleased(e); 202 e = new MouseEvent(c, 203 MouseEvent.MOUSE_CLICKED, 204 0, // time 205 0, // modifiers 206 x, y, // x, y not in this component? 207 1, // one click 208 false, // not a popup 209 MouseEvent.BUTTON1); 210 m.mouseClicked(e); 211 e = new MouseEvent(c, 212 MouseEvent.MOUSE_EXITED, 213 0, // time 214 0, // modifiers 215 x, y, // x, y not in this component? 216 1, // one click 217 false, // not a popup 218 MouseEvent.BUTTON1); 219 m.mouseExited(e); 220 } 221 222 @Override 223 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 224 // because we work with Swing, we do this on the AWT thread 225 226 if (javax.swing.SwingUtilities.isEventDispatchThread()) { 227 doGetOnSwing(request, response); 228 return; 229 } 230 231 try { 232 javax.swing.SwingUtilities.invokeAndWait( 233 () -> { 234 try { 235 doGetOnSwing(request, response); 236 } catch ( ServletException | IOException ex ) { 237 throw new RuntimeException(ex); 238 } 239 } 240 ); 241 } catch (InterruptedException ex) { 242 // ignore 243 log.trace("Ignoring InterruptedException"); 244 } catch (java.lang.reflect.InvocationTargetException ex) { 245 // exception thrown up, unpack and rethrow? 246 log.trace("top-level caught", ex); 247 if (ex.getCause() != null) { 248 log.trace("1st level caught", ex.getCause()); 249 if (ex.getCause().getCause() != null) { 250 // have to decode within content 251 Throwable ex2 = ex.getCause().getCause(); 252 if ( ex2 instanceof ServletException) { 253 throw (ServletException) ex2; 254 } else if ( ex2 instanceof IOException) { 255 throw (IOException) ex2; 256 } else { 257 // wrap and throw 258 throw new RuntimeException(ex); 259 } 260 } else { 261 // wrap and throw 262 throw new RuntimeException(ex); 263 } 264 } else { 265 // just wrap and rethrow the InvocationTargetException, but this should never happen 266 throw new RuntimeException(ex); 267 } 268 } 269 } 270 271 protected void doGetOnSwing(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 272 WebServerPreferences preferences = InstanceManager.getDefault(WebServerPreferences.class); 273 if (preferences.isDisableFrames()) { 274 if (preferences.isRedirectFramesToPanels()) { 275 if (JSON.JSON.equals(request.getParameter("format"))) { 276 response.sendRedirect("/panel?format=json"); 277 } else { 278 response.sendRedirect("/panel"); 279 } 280 } else { 281 response.sendError(HttpServletResponse.SC_FORBIDDEN, Bundle.getMessage(request.getLocale(), "FramesAreDisabled")); 282 } 283 return; 284 } 285 JmriJFrame frame = null; 286 String name = getFrameName(request.getRequestURI()); 287 if (name != null) { 288 List<String> disallowedFrames = Arrays.asList(preferences.getDisallowedFrames()); 289 if (disallowedFrames.contains(name)) { 290 response.sendError(HttpServletResponse.SC_FORBIDDEN, "Frame [" + name + "] not allowed (check Preferences)"); 291 return; 292 } 293 frame = JmriJFrame.getFrame(name); 294 if (frame == null) { 295 response.sendError(HttpServletResponse.SC_NOT_FOUND, "Can not find frame [" + name + "]"); 296 return; 297 } else if (!frame.isVisible()) { 298 response.sendError(HttpServletResponse.SC_FORBIDDEN, "Frame [" + name + "] hidden"); 299 } else if (!frame.getAllowInFrameServlet()) { 300 response.sendError(HttpServletResponse.SC_FORBIDDEN, "Frame [" + name + "] not allowed by design"); 301 return; 302 } 303 } 304 Map<String, String[]> parameters = this.populateParameterMap(request.getParameterMap()); 305 if (frame != null && parameters.containsKey("coords") && 306 !(parameters.containsKey("protect") && Boolean.parseBoolean(parameters.get("protect")[0]))) { // NOI18N 307 this.doClick(frame, parameters.get("coords")[0]); // NOI18N 308 } 309 if (frame != null && request.getRequestURI().contains(".html")) { // NOI18N 310 this.doHtml(frame, request, response, parameters); 311 } else if (frame != null && request.getRequestURI().contains(".png")) { // NOI18N 312 this.doImage(frame, request, response); 313 } else { 314 this.doList(request, response); 315 } 316 } 317 318 @Override 319 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 320 this.doGet(request, response); 321 } 322 323 private void doHtml(@Nonnull JmriJFrame frame, HttpServletRequest request, 324 @Nonnull HttpServletResponse response, Map<String, String[]> parameters) throws ServletException, IOException { 325 WebServerPreferences preferences = InstanceManager.getDefault(WebServerPreferences.class); 326 Date now = new Date(); 327 boolean click = false; 328 boolean useAjax = preferences.isUseAjax(); 329 boolean plain = preferences.isSimple(); 330 String clickRetryTime = Integer.toString(preferences.getClickDelay()); 331 String noclickRetryTime = Integer.toString(preferences.getRefreshDelay()); 332 boolean protect = false; 333 if (parameters.containsKey("coords")) { // NOI18N 334 click = true; 335 } 336 if (parameters.containsKey("retry")) { // NOI18N 337 noclickRetryTime = parameters.get("retry")[0]; // NOI18N 338 } 339 if (parameters.containsKey("ajax")) { // NOI18N 340 useAjax = Boolean.parseBoolean(parameters.get("ajax")[0]); // NOI18N 341 } 342 if (parameters.containsKey("plain")) { // NOI18N 343 plain = Boolean.parseBoolean(parameters.get("plain")[0]); // NOI18N 344 } 345 if (parameters.containsKey("protect")) { // NOI18N 346 protect = Boolean.parseBoolean(parameters.get("protect")[0]); // NOI18N 347 } 348 response.setStatus(HttpServletResponse.SC_OK); 349 response.setContentType("text/html"); // NOI18N 350 response.setHeader("Connection", "Keep-Alive"); // NOI18N 351 response.setDateHeader("Date", now.getTime()); // NOI18N 352 response.setDateHeader("Last-Modified", now.getTime()); // NOI18N 353 response.setDateHeader("Expires", now.getTime()); // NOI18N 354 // 0 is host 355 // 1 is frame name (after escaping special characters) 356 // 2 is retry in META tag, click or noclick retry 357 // 3 is retry in next URL, future retry 358 // 4 is state of plain 359 // 5 is the CSS stylesteet name addition, based on "plain" 360 // 6 is ajax preference 361 // 7 is protect 362 Object[] args = new String[]{"localhost", // NOI18N 363 URLEncoder.encode(frame.getTitle(), UTF8), 364 (click ? clickRetryTime : noclickRetryTime), 365 noclickRetryTime, 366 Boolean.toString(plain), 367 (plain ? "-plain" : ""), // NOI18N 368 Boolean.toString(useAjax), 369 Boolean.toString(protect)}; 370 response.getWriter().write(Bundle.getMessage(request.getLocale(), "FrameDocType")); // NOI18N 371 response.getWriter().write(MessageFormat.format(Bundle.getMessage(request.getLocale(), "FramePart1"), args)); // NOI18N 372 if (useAjax) { 373 response.getWriter().write(MessageFormat.format(Bundle.getMessage(request.getLocale(), "FramePart2Ajax"), args)); // NOI18N 374 } else { 375 response.getWriter().write(MessageFormat.format(Bundle.getMessage(request.getLocale(), "FramePart2NonAjax"), args)); // NOI18N 376 } 377 response.getWriter().write(MessageFormat.format(Bundle.getMessage(request.getLocale(), "FrameFooter"), args)); // NOI18N 378 379 log.debug("Sent jframe html with click={}", (click ? "True" : "False")); 380 } 381 382 private void doImage(@Nonnull JmriJFrame frame, HttpServletRequest request, 383 @Nonnull HttpServletResponse response) throws ServletException, IOException { 384 Date now = new Date(); 385 response.setStatus(HttpServletResponse.SC_OK); 386 response.setContentType("image/png"); // NOI18N 387 response.setDateHeader("Date", now.getTime()); // NOI18N 388 response.setDateHeader("Last-Modified", now.getTime()); // NOI18N 389 response.setHeader("Cache-Control", "no-cache"); // NOI18N 390 response.setHeader("Connection", "Keep-Alive"); // NOI18N 391 response.setHeader("Keep-Alive", "timeout=5, max=100"); // NOI18N 392 BufferedImage image = new BufferedImage(frame.getContentPane().getWidth(), 393 frame.getContentPane().getHeight(), 394 BufferedImage.TYPE_INT_RGB); 395 frame.getContentPane().paint(image.createGraphics()); 396 397 doDialog(getDialog(frame), image); 398 399 //put it in a temp file to get post-compression size 400 ByteArrayOutputStream tmpFile = new ByteArrayOutputStream(); 401 ImageIO.write(image, "png", tmpFile); // NOI18N 402 tmpFile.close(); 403 response.setContentLength(tmpFile.size()); 404 response.getOutputStream().write(tmpFile.toByteArray()); 405 log.debug("Sent [{}] as {} byte png.", frame.getTitle(), tmpFile.size()); 406 } 407 408 private void doDialog(@CheckForNull JDialog dialog, @Nonnull BufferedImage image){ 409 if ( dialog == null ) { 410 return; 411 } 412 log.debug("dialog {}", dialog); 413 414 BufferedImage dImage = new BufferedImage(dialog.getContentPane().getWidth(), 415 dialog.getContentPane().getHeight(), BufferedImage.TYPE_INT_RGB); 416 dialog.getContentPane().paint(dImage.createGraphics()); 417 image.getGraphics().drawImage(dImage, 0, 20, null); 418 419 Graphics2D g = (Graphics2D)image.getGraphics(); 420 421 g.setColor(Color.WHITE); 422 g.fillRect(0, 0, dialog.getContentPane().getWidth(), 20); 423 424 g.setColor(Color.DARK_GRAY ); 425 g.drawRect(0, 0, dialog.getContentPane().getWidth(), dialog.getContentPane().getHeight()+20); 426 427 RenderingHints hints =new RenderingHints(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 428 g.setRenderingHints(hints); 429 g.drawString(dialog.getTitle(), 10, 15); 430 } 431 432 private void doList(@Nonnull HttpServletRequest request, @Nonnull HttpServletResponse response) throws ServletException, IOException { 433 List<String> disallowedFrames = Arrays.asList(InstanceManager.getDefault(WebServerPreferences.class).getDisallowedFrames()); 434 String format = request.getParameter("format"); // NOI18N 435 ObjectMapper mapper = new ObjectMapper(); 436 Date now = new Date(); 437 boolean usePanels = Boolean.parseBoolean(request.getParameter(JSON.PANELS)); 438 response.setStatus(HttpServletResponse.SC_OK); 439 if ("json".equals(format)) { // NOI18N 440 response.setContentType("application/json"); // NOI18N 441 } else { 442 response.setContentType("text/html"); // NOI18N 443 } 444 response.setHeader("Connection", "Keep-Alive"); // NOI18N 445 response.setDateHeader("Date", now.getTime()); // NOI18N 446 response.setDateHeader("Last-Modified", now.getTime()); // NOI18N 447 response.setDateHeader("Expires", now.getTime()); // NOI18N 448 449 if ("json".equals(format)) { // NOI18N 450 ArrayNode root = mapper.createArrayNode(); 451 HashSet<JFrame> frames = new HashSet<>(); 452 JsonUtilHttpService service = new JsonUtilHttpService(new ObjectMapper()); 453 for (JmriJFrame frame : JmriJFrame.getFrameList()) { 454 if (frame == null) { 455 continue; 456 } 457 if (usePanels && frame instanceof Editor) { 458 ObjectNode node = service.getPanel((Editor) frame, JSON.XML, 0); 459 if (node != null) { 460 root.add(node); 461 frames.add(((Editor) frame).getTargetFrame()); 462 } 463 } else { 464 String title = frame.getTitle(); 465 if (!title.isEmpty() 466 && frame.getAllowInFrameServlet() 467 && !disallowedFrames.contains(title) 468 && !frames.contains(frame) 469 && frame.isVisible()) { 470 ObjectNode node = mapper.createObjectNode(); 471 try { 472 node.put(NAME, title); 473 node.put(URL, "/frame/" + URLEncoder.encode(title, UTF8) + ".html"); // NOI18N 474 node.put("png", "/frame/" + URLEncoder.encode(title, UTF8) + ".png"); // NOI18N 475 root.add(node); 476 frames.add(frame); 477 } catch (UnsupportedEncodingException ex) { 478 JsonException je = new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unable to encode panel title \"" + title + "\"", 0); 479 response.sendError(je.getCode(), mapper.writeValueAsString(je.getJsonMessage())); 480 return; 481 } 482 } 483 } 484 } 485 response.getWriter().write(mapper.writeValueAsString(root)); 486 } else { 487 response.getWriter().append(Bundle.getMessage(request.getLocale(), "FrameDocType")); // NOI18N 488 response.getWriter().append(Bundle.getMessage(request.getLocale(), "ListFront")); // NOI18N 489 response.getWriter().write(Bundle.getMessage(request.getLocale(), "TableHeader")); // NOI18N 490 // list frames, (open JMRI windows) 491 for (JmriJFrame frame : JmriJFrame.getFrameList()) { 492 String title = frame.getTitle(); 493 //don't add to list if blank or disallowed 494 if (!title.isEmpty() && frame.getAllowInFrameServlet() && !disallowedFrames.contains(title) && frame.isVisible()) { 495 String link = "/frame/" + URLEncoder.encode(title, UTF8) + ".html"; // NOI18N 496 //format a table row for each valid window (frame) 497 response.getWriter().append("<tr><td><a href='" + link + "'>"); // NOI18N 498 response.getWriter().append(title); 499 response.getWriter().append("</a></td>"); // NOI18N 500 response.getWriter().append("<td><a href='"); 501 response.getWriter().append(link); 502 response.getWriter().append("'><img src='"); // NOI18N 503 response.getWriter().append("/frame/" + URLEncoder.encode(title, UTF8) + ".png"); // NOI18N 504 response.getWriter().append("'></a></td></tr>\n"); // NOI18N 505 } 506 } 507 response.getWriter().append("</table>"); // NOI18N 508 response.getWriter().append(Bundle.getMessage(request.getLocale(), "ListFooter")); // NOI18N 509 } 510 } 511 512 // Requests for frames are always /frame/<name>.html or /frame/<name>.png 513 private String getFrameName(@Nonnull String uri) throws UnsupportedEncodingException { 514 if (!uri.contains(".")) { 515 return null; 516 } else { 517 // if request contains parameters, strip those off 518 int stop = (uri.contains("?")) ? uri.indexOf('?') : uri.length(); // NOI18N 519 String name = uri.substring(uri.lastIndexOf('/'), stop); // NOI18N 520 // URI contains a leading / at this point 521 name = name.substring(1, name.lastIndexOf('.')); // NOI18N 522 name = URLDecoder.decode(name, UTF8); //undo escaped characters 523 log.debug("Frame name is {}", name); // NOI18N 524 return name; 525 } 526 } 527 528 // The HttpServeletRequest does not like image maps, so we need to process 529 // the parameter names to see if an image map was clicked 530 protected Map<String, String[]> populateParameterMap(@Nonnull Map<String, String[]> map) { 531 Map<String, String[]> parameters = new HashMap<>(); 532 map.entrySet().stream().forEach((entry) -> { 533 String[] value = entry.getValue(); 534 String key = entry.getKey(); 535 if (value[0].contains("?")) { // NOI18N 536 // a user's click is in another key's value 537 String[] values = value[0].split("\\?"); // NOI18N 538 if (values[0].contains(",")) { 539 parameters.put(key, new String[]{values[1]}); 540 parameters.put("coords", new String[]{values[0]}); // NOI18N 541 } else { 542 parameters.put(key, new String[]{values[0]}); 543 parameters.put("coords", new String[]{values[1]}); // NOI18N 544 } 545 } else if (key.contains(",")) { // NOI18N 546 // we have a user's click 547 String[] coords = new String[1]; 548 if (key.contains("?")) { // NOI18N 549 // the key is combined 550 coords[0] = key.substring(key.indexOf("?")); // NOI18N 551 key = key.substring(0, key.indexOf("?") - 1); // NOI18N 552 parameters.put(key, value); 553 } else { 554 coords[0] = key; 555 } 556 log.debug("Setting click coords to {}", coords[0]); 557 parameters.put("coords", coords); // NOI18N 558 } else { 559 parameters.put(key, value); 560 } 561 }); 562 return parameters; 563 } 564 565 private void doClick(@Nonnull JmriJFrame frame, @Nonnull String coords) { 566 String[] click = coords.split(","); // NOI18N 567 int x = Integer.parseInt(click[0]); 568 int y = Integer.parseInt(click[1]); 569 570 JDialog dialog = getDialog(frame); 571 if ( dialog != null ) { 572 y -= 20; // offset dialog title 573 Component cc = dialog.getContentPane().findComponentAt(x, y); 574 if ( cc != null ){ 575 log.debug("click dialog {} at x:{} y:{} component:{}",dialog.getTitle(),x,y, cc); 576 sendClick(frame.getTitle(), cc, x, y, dialog.getContentPane()); 577 } 578 return; 579 } 580 581 //send click to topmost component under click spot 582 Component c = frame.getContentPane().findComponentAt(x, y); 583 if ( c == null ) { // click outside of Frame 584 return; 585 } 586 //log.debug("topmost component is class={}", c.getClass().getName()); 587 sendClick(frame.getTitle(), c, x, y, frame.getContentPane()); 588 589 //if clicked on background, search for layout editor target pane TODO: simplify id'ing background 590 if (!c.getClass().getName().equals("jmri.jmrit.display.Editor$TargetPane") // NOI18N 591 && (c instanceof jmri.jmrit.display.PositionableLabel) 592 && !(c instanceof jmri.jmrit.display.LightIcon) 593 && !(c instanceof jmri.jmrit.display.LocoIcon) 594 && !(c instanceof jmri.jmrit.display.MemoryOrGVIcon) 595 && !(c instanceof jmri.jmrit.display.MultiSensorIcon) 596 && !(c instanceof jmri.jmrit.display.PositionableIcon) 597 && !(c instanceof jmri.jmrit.display.ReporterIcon) 598 && !(c instanceof jmri.jmrit.display.RpsPositionIcon) 599 && !(c instanceof jmri.jmrit.display.SlipTurnoutIcon) 600 && !(c instanceof jmri.jmrit.display.TurnoutIcon)) { 601 clickOnEditorPane(frame.getContentPane(), x, y, frame); 602 } 603 } 604 605 //recursively search components to find editor target pane, where layout editor paints components 606 public void clickOnEditorPane(@Nonnull Component c, int x, int y, JmriJFrame f) { 607 608 if (c.getClass().getName().equals("jmri.jmrit.display.Editor$TargetPane")) { // NOI18N 609 log.debug("Sending additional click to Editor$TargetPane"); 610 //then click on it 611 sendClick(f.getTitle(), c, x, y, f); 612 613 //keep looking 614 } else if (c instanceof Container) { 615 //check this component's children 616 for (Component child : ((Container) c).getComponents()) { 617 clickOnEditorPane(child, x, y, f); 618 } 619 } 620 } 621 622 @CheckForNull 623 private static JDialog getDialog(@Nonnull JmriJFrame frame) { 624 for ( var pcl : frame.getPropertyChangeListeners() ) { 625 log.debug("PCL : {}", pcl); 626 if ( pcl instanceof JDialogListener ){ 627 return ((JDialogListener) pcl).getDialog(); 628 } 629 } 630 return null; 631 } 632 633 private static final Logger log = LoggerFactory.getLogger(JmriJFrameServlet.class); 634}