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/. &copy; 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}