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