capture a mouse click and its location

Is it possible to have an example of code that capture a mouse click and its location (ideally in Python but ruby is also fine).

For instance: on click, print “You have clicked at the location X,Y of the design”

My main target is to make a small script that can ask you to click somewhere and set this new point as a new origin for the design. If it is a function that already exists, also please let me know.

Thanks a lot!

Comments

  • edited November 2018

    Hi Jonathan,

    you can capture mouse events in a "plugin". Below is a small piece of code illustrating how this works. Basically you create a plugin to implement a new mode. If selecting this mode by clicking on the respective tool bar entry, you can capture mouse events until the mode becomes inactive again.

    Plugins are slightly clumsy currently. For example, there is no method to activate a mode from your code. Hence, the only way to integrate your mode is through toolbar buttons. When I'm debugging the code, I have to execute the macro twice before the code becomes effective - I did not debug this yet as it's just a little bit annoying.

    And please note the workaround for bug 191.

    Matthias

    # Register this macro as "autorun" to enable the plugin
    
    # First thing is to implement a "plugin factory". This 
    # object is asked to provide a plugin for each view
    
    class SetOriginOnMouseClickPluginFactory(pya.PluginFactory):
    
      def __init__(self):
        super(SetOriginOnMouseClickPluginFactory, self).__init__()
        pya.MainWindow.instance()  # (workaround for bug 191)
        self.register(-1000, "set_origin", "Cell Origin")
    
      def create_plugin(self, manager, root, view):
        return SetOriginOnMouseClickPlugin()
    
      # Keeps the singleton instance
      instance = None
    
    # Create and store the singleton instance
    SetOriginOnMouseClickPluginFactory.instance = SetOriginOnMouseClickPluginFactory()
    
    
    # Second thing is the actual plugin
    
    class SetOriginOnMouseClickPlugin(pya.Plugin):
    
      def __init__(self):
        super(SetOriginOnMouseClickPlugin, self).__init__()
        pass
    
      def activated(self):
        pya.MainWindow.instance().message("Click on point to set the origin", 10000)
    
      def deactivated(self):
        pya.MainWindow.instance().message("", 0)
    
      def mouse_click_event(self, p, buttons, prio):
        # This is where everything happens
        if prio:
          print("Mouse clicked at " + str(p))
          return True
        return False
    
  • Hello Mathias, Thank you very much for this answer! and sorry for coming back on it so late.

    I would like to now share the program I now have:

    • the program does not need to be run twice if the classes are written in the order.
    • There are some points I am not so satisfied with and I wonder if there is a better way to do it:
      1- ideally I could snap the cursor to a given grid, is it visually possible?
      2- to move the layout I use layout transformation and zoom at the corrected location, is there maybe a more concise way?
      3- to release the tool I call the PluginFactory again, I made a thread which is also more general on this topic

    here is the program:

    class SetOriginOnMouseClickPlugin(pya.Plugin):
      def __init__(self):
        super(SetOriginOnMouseClickPlugin, self).__init__()
        pass
      def activated(self):
        self.grab_mouse()
        pya.MainWindow.instance().message("Click on point to set the origin", 10000)
      def deactivated(self):
        pya.MainWindow.instance().message("", 0)
      def mouse_moved_event(self, p, buttons, prio):
        if prio:
          self.set_cursor(pya.Cursor.Cross)
          return False
      def mouse_click_event(self, p, buttons, prio):
        if prio:
          view = pya.Application.instance().main_window().current_view()
          layout = view.active_cellview().layout()
          zoomRect = pya.DBox.new(view.box().p1.x-p.x,view.box().p1.y-p.y,view.box().p2.x-p.x,view.box().p2.y-p.y)
          layout.transform(pya.Trans(0, False, -p.x/layout.dbu, -p.y/layout.dbu))
          view.zoom_box(zoomRect)
          self.ungrab_mouse()
          SetOriginOnMouseClickPluginFactory.instance = SetOriginOnMouseClickPluginFactory()
          return True
        return False
    
    class SetOriginOnMouseClickPluginFactory(pya.PluginFactory):
      def __init__(self):
        super(SetOriginOnMouseClickPluginFactory, self).__init__()
        pya.MainWindow.instance()  # (workaround for bug 191)
        TheButton = self.register(-1000, "set_origin", "Cell Origin")
      def create_plugin(self, manager, root, view):
        return SetOriginOnMouseClickPlugin()
      instance = None
    SetOriginOnMouseClickPluginFactory.instance = SetOriginOnMouseClickPluginFactory()
    
  • Hi Jonathan,

    thanks for the hint about the class order.

    Regarding your points:

    1.) There is no cursor - actually there is just the mouse pointer and it can't (or should not) be manipulated. So if you want a snapping cursor, you have to implement a marker and move that with the mouse. You could then set the mouse cursor to invisible and this way you'd basically get your own cursor. I have not tried this yet - probably there is a lot of event handling - i.e. when the mouse enters or leaves the canvas etc.

    2.) Moving the layout is a valid operation, but this will not work when you're inside a child cell. So either you catch this condition as an error or implement a transformation at cell level (which is not as straightforward as layout transformation as there is no single method for this yet)

    3.) See my answers in that thread

    Matthias

  • Hello Matthias,

    Thanks again for the support: here I share again the result with the update:

    • handling mouse cursor is actually quite easy, I implemented a cursor which display also coordinates!
    • added an interface to snap the cursor to a grid
    • After click the tool is switched to Selection and therefore also run deactivated(self)

    The issues known/found:

    • The cursor has some issues when zooming around minimum 1nm grid: main one is that the text does not stick to bellow the located point
    • The script still only work on the Topcell (I may work on this later, should be possible but not necessary for me at the moment)
    • adding a marker layer on 10000.0 this layer needs to be unused (most likely the case for IC designers)
    • add_missing_layers() is running but I never hide layers ok for me
    • I wanted to extend this program to a nice visual labelling but I meet this issue

    :smile:

    GlbSnap = 10
    
    def GetLayer(num,dt):
      li = pya.Application.instance().main_window().current_view().begin_layers()
      while not li.at_end():
        lp = li.current()
        if((lp.source_layer ==num) and (lp.source_datatype == dt) ):
          return lp
        li.next()
      raise ValueError("Layer " + str(num)+"."+str(dt)+ " Not Found")
    
    
    class SetOriginOnMouseClickPlugin(pya.Plugin):
      def __init__(self):
        super(SetOriginOnMouseClickPlugin, self).__init__()
        pass
      def activated(self):
        self.grab_mouse()
        pya.MainWindow.instance().message("Click on point to set the origin", 10000)
      def activated(self):
        self.grab_mouse()
        pya.MainWindow.instance().message("Click on point to set the origin", 10000)
        self.layH =  pya.Application.instance().main_window().current_view().active_cellview().layout().layer(10000, 0)
        pya.Application.instance().main_window().current_view().add_missing_layers()
        layer = GetLayer(10000,0)
        layer.visible = True
        layer.name = "Marker_Layer"
        self.top = pya.Application.instance().main_window().current_view().active_cellview().layout().top_cell()
        self.dialog = pya.QDialog(pya.Application.instance().main_window())
        QLay = pya.QGridLayout(self.dialog)
        l1 =  pya.QLabel('snap (um)= ',self.dialog)
        self.l2 = pya.QLineEdit('{0:g}'.format(GlbSnap),self.dialog)
        self.l2.setToolTip('this will snap to grid')
        b1= pya.QPushButton('apply!',self.dialog)
        b1.clicked = self.ApplySnap
        QLay.addWidget(l1,0,0)
        QLay.addWidget(self.l2,0,1)
        QLay.addWidget(b1,3,1)
        self.dialog.show()
        print (dir(self.dialog))
      def ApplySnap(self) :
        global GlbSnap
        GlbSnap= float(self.l2.text)
      def deactivated(self):
        pya.MainWindow.instance().message("", 0)
        self.dialog.close()
      def mouse_moved_event(self, p, buttons, prio):
        if prio:
          self.set_cursor(pya.Cursor.Blank)
          pya.Application.instance().main_window().current_view().active_cellview().layout().clear_layer(self.layH)
          xMouse= int(p.x/GlbSnap)*GlbSnap
          yMouse= int(p.y/GlbSnap)*GlbSnap
          text = pya.Text('x='+str(xMouse)+' y='+str(yMouse), xMouse*1000,yMouse*1000)
          text.halign = 1
          text.valign = 2
          self.top.shapes(self.layH).insert(text)
          boxsize = (viewBox.p2.x-viewBox.p1.x)/300
          if boxsize < 0.001:
            boxsize =0.001
          poly =pya.Polygon([pya.Point(xMouse*1000,yMouse*1000),
            pya.Point((xMouse-boxsize)*1000,(yMouse+2*boxsize)*1000),
            pya.Point((xMouse+boxsize)*1000,(yMouse+2*boxsize)*1000)])
          self.top.shapes(self.layH).insert(poly)
          return False
      def mouse_click_event(self, p, buttons, prio):
        if prio:
          view = pya.Application.instance().main_window().current_view()
          layout = view.active_cellview().layout()
          zoomRect = pya.DBox.new(view.box().p1.x-p.x,view.box().p1.y-p.y,view.box().p2.x-p.x,view.box().p2.y-p.y)
          layout.transform(pya.Trans(0, False, -p.x/layout.dbu, -p.y/layout.dbu))
          view.zoom_box(zoomRect)
          self.ungrab_mouse()
          pya.Application.instance().main_window().current_view().active_cellview().layout().clear_layer(self.layH)
          pya.MainWindow.instance().menu().action("@toolbar.select").trigger()
          return True
        return False
    
    class SetOriginOnMouseClickPluginFactory(pya.PluginFactory):
      def __init__(self):
        super(SetOriginOnMouseClickPluginFactory, self).__init__()
        pya.MainWindow.instance()  # (workaround for bug 191)
        TheButton = self.register(-1000, "set_origin", "Cell Origin")
      def create_plugin(self, manager, root, view):
        return SetOriginOnMouseClickPlugin()
      instance = None
    SetOriginOnMouseClickPluginFactory.instance = SetOriginOnMouseClickPluginFactory()
    
  • Ah ... now the posts connect :-)

    Please see my comments in the other post.

    Thanks for sharing this code and best regards,

    Matthias

  • Hi,

    Sorry for not opening a new thread, I think that the answer will be good to be posted under the current thread for completeness.

    Let's say that we want to use buttons states for several jobs, and e.g. pya.ButtonState.RightButton click to deactivate the plugin. I tried to use self._destroy() inside mouse_click_event but KLayout crashed, so I imagine that this is not the correct way of doing it.

    So, my question is, which is the correct way to deactivate the plugin through a mouse button click?

    Thanks in advance,
    Chris

  • Hi Chris,

    well, a crash should not happen - but there is a always a risk of things going out of sync when an object is destroyed. I'm trying the harden the code against this, but there are too many paths to destruction ...

    Regarding your question: pya.MainWindow.instance().menu().action("@toolbar.select").trigger() is one way - I personally prefer pya.MainWindow.instance().cancel().

    The latter function will also clear the selection which is a good thing to do when you deleted something or messed with the database in other respects.

    And undo/redo support is easily added by using LayoutView#transaction/LayoutView#commit.

    Kind regards,

    Matthias

  • edited December 2019

    Hi all,

    @Matthias, I tried your suggestions unsuccessfully, but I managed to deactivate the plugin destroying the instance, by running SetOriginOnMouseClickPluginFactory.instance._destroy() instead of self._destroy(), without making KLayout crash. Is a valid way of doing it?

    UPDATE: KLayout now crashes when I reactivate the plugin, so it cannot be taken as a viable solution.

    Chris

  • edited December 2019

    @crizos : If I understood you wish to have a sequence of actions on clicks and only quit the plugin at the end of it? what I would do for that would be to set up a variable which increment on each click and tells how far you are with the clicks.
    on the last click exit/change tools as @Matthias mentioned.
    the complete escape sequence in the code is:

      self.ungrab_mouse()
      pya.MainWindow.instance().menu().action("@toolbar.select").trigger()
    

    good luck, and we are always interested in resulting codes :)

  • edited December 2019

    @jonathan : Yes, you are correct, and as for the counting variable, I don't have finite steps in my implementation, but even so, I still think that I would have the same problem in reactivation.

    I want to enable the plugin e.g. menu.action('somewhere.something').triger() and after an arbitrary sequence of events User right clicks and deactivates the plugin. I saw your example and tried your approach but it didn't work. After some (re)search in the documentation, I also saw the _object_._destroy() but that method makes KLayout crash, which means that is a no-go solution.

    Also, it seems that I don't have access to deactivated method, maybe that helps with my problem. The sample code I use is part of the one that @Matthias posted at the second comment from the current thread.

    My code sample:

    import pya
    
    class SetOriginOnMouseClickPlugin(pya.Plugin):
    
        def __init__(self):
            super(SetOriginOnMouseClickPlugin, self).__init__()
        # __init__
    
        def activated(self):
            pya.MainWindow.instance().message("Click on point to set the origin", 10000)
        # activated
    
        def deactivated(self):
            pya.MainWindow.instance().message("", 0)
            print("Deactivated")
        # deactivated
    
        def mouse_click_event(self, p, buttons, prio):
            if buttons == pya.ButtonState.RightButton :
                print("Right clicked")
    
                self.ungrab_mouse()
                #pya.MainWindow.instance().cancel()
                pya.MainWindow.instance().menu().action("@toolbar.select").trigger()
            # if
    
            return False
        # mouse_click_event
    # SetOriginOnMouseClickPlugin
    
    class SetOriginOnMouseClickPluginFactory(pya.PluginFactory):
    
        def __init__(self, plugin_name):
            super(SetOriginOnMouseClickPluginFactory, self).__init__()
    
            self.has_tool_entry = False
    
            self.tool_symbol = plugin_name
            self.tool_name   = plugin_name
            self.tool_pos    = "tools_menu.end"
            self.tool_title  = "Cell Origin"
    
            self.add_menu_entry(
                self.tool_symbol,
                self.tool_name,
                self.tool_pos,
                self.tool_title
            )# add_menu_entry
    
            self.register(-1000, self.tool_name, self.tool_title)
        # __init__
    
        def create_plugin(self, manager, root, view):
            return SetOriginOnMouseClickPlugin()
        # create_plugin
    
        # Keeps the singleton instance
        instance = None
    # SetOriginOnMouseClickPluginFactory
    
    plugin_name = "set_origin"
    
    # Create and store the singleton instance
    SetOriginOnMouseClickPluginFactory.instance = SetOriginOnMouseClickPluginFactory(plugin_name)
    
    #- Make menu entry invisible
    menu = pya.Application.instance().main_window().menu()
    menu.action("tools_menu.set_origin").visible = False
    

    Best regards,
    Chris

  • edited December 2019

    Hi @crizos,
    when you are finished with your clicks just click on a another tool and you are set, you need a trigger anyway to say that you are done with your clicks,

    However you need to ungrab mouse when the tool is desactivated otherwise you will have a bug indeed...
    This is indeed a bug in my previous code!

    I am posting now a new code with this bug corrected and few other corrections:

    GlbSnap = 10
    
    def GetLayer(num,dt):
      li = pya.Application.instance().main_window().current_view().begin_layers()
      while not li.at_end():
        lp = li.current()
        if((lp.source_layer ==num) and (lp.source_datatype == dt) ):
          return lp
        li.next()
      raise ValueError("Layer " + str(num)+"."+str(dt)+ " Not Found")
    
    
    class SetOriginOnMouseClickPlugin(pya.Plugin):
      def __init__(self):
        super(SetOriginOnMouseClickPlugin, self).__init__()
        pass
      def activated(self):
        self.grab_mouse()
        pya.MainWindow.instance().message("Click on point to set the origin", 10000)
        self.layH =  pya.Application.instance().main_window().current_view().active_cellview().layout().layer(10000, 0)
        pya.Application.instance().main_window().current_view().add_missing_layers()
        layer = GetLayer(10000,0)
        layer.visible = True
        layer.name = "Marker_Layer"
        self.top = pya.Application.instance().main_window().current_view().active_cellview().layout().top_cell()
        self.dialog = pya.QDialog(pya.Application.instance().main_window())
        QLay = pya.QGridLayout(self.dialog)
        l1 =  pya.QLabel('snap (um)= ',self.dialog)
        self.l2 = pya.QLineEdit('{0:g}'.format(GlbSnap),self.dialog)
        self.l2.setToolTip('this will snap to grid')
        b1= pya.QPushButton('apply!',self.dialog)
        b1.clicked = self.ApplySnap
        QLay.addWidget(l1,0,0)
        QLay.addWidget(self.l2,0,1)
        QLay.addWidget(b1,3,1)
        self.dialog.show()
        print (dir(self.dialog))
      def ApplySnap(self) :
        global GlbSnap
        GlbSnap= float(self.l2.text)
      def deactivated(self):
        pya.Application.instance().main_window().current_view().active_cellview().layout().clear_layer(self.layH)
        pya.Application.instance().main_window().current_view().remove_unused_layers()
        pya.MainWindow.instance().message("", 0)
        self.ungrab_mouse()
        self.dialog.close()
      def mouse_moved_event(self, p, buttons, prio):
        if prio:
          self.set_cursor(pya.Cursor.Blank)
          pya.Application.instance().main_window().current_view().active_cellview().layout().clear_layer(self.layH)
          xMouse= int(p.x/GlbSnap)*GlbSnap
          yMouse= int(p.y/GlbSnap)*GlbSnap
          text = pya.Text('x='+str(xMouse)+' y='+str(yMouse), xMouse*1000,yMouse*1000)
          text.halign = 1
          text.valign = 2
          self.top.shapes(self.layH).insert(text)
          viewBox = pya.Application.instance().main_window().current_view().box()
          rect =pya.Box(viewBox.p1.x*1000,yMouse*1000-1, viewBox.p2.x*1000,yMouse*1000+1)
          self.top.shapes(self.layH).insert(rect)
          rect =pya.Box(xMouse*1000-1,viewBox.p1.y*1000, xMouse*1000+1,viewBox.p2.y*1000)
          self.top.shapes(self.layH).insert(rect)
          return False
      def mouse_click_event(self, p, buttons, prio):
        if prio:
          view = pya.Application.instance().main_window().current_view()
          layout = view.active_cellview().layout()
          zoomRect = pya.DBox.new(view.box().p1.x-p.x,view.box().p1.y-p.y,view.box().p2.x-p.x,view.box().p2.y-p.y)
          layout.transform(pya.Trans(0, False, -p.x/layout.dbu, -p.y/layout.dbu))
          view.zoom_box(zoomRect)
          self.ungrab_mouse()
          pya.MainWindow.instance().menu().action("@toolbar.select").trigger()
          return True
        return False
    
    class SetOriginOnMouseClickPluginFactory(pya.PluginFactory):
      def __init__(self):
        super(SetOriginOnMouseClickPluginFactory, self).__init__()
        pya.MainWindow.instance()  # (workaround for bug 191)
        TheButton = self.register(-1000, "set_origin", "Cell Origin")
      def create_plugin(self, manager, root, view):
        return SetOriginOnMouseClickPlugin()
      instance = None
    SetOriginOnMouseClickPluginFactory.instance = SetOriginOnMouseClickPluginFactory()
    
  • Hi @jonathan,

    Thank you for reposting the code,

    Yes, the bug, or the reason why my implementation doesn't work, may rest in the difference that my plugin does not have a tool entry in the toolbar. The triggering should be happening from a function call, in an entirely different area, and as it said, the plugin should be deactivated with a mouse click. If you see, I ungrab the mouse and use the methods that you and @Matthias recommended.

    Also, I forgot to say something that may help. Without the toolbar entry, prio is always False, so I didn't include it in my implementation.

    Best regards,
    Chris

  • edited December 2019

    Hi @crizos my post just crossed your edit, I will look at the code later but so far I only managed to grab mouse click from the pluginfactory which are tools, so I am not sure if there is a way to separate them.

  • edited December 2019

    Hi again @crizos,

    I tried the following code and it worked very good with my script:

     #this remove the tool from visibility:
     pya.MainWindow.instance().menu().action("@toolbar.set_origin").visible = False
     #you can start the tool with:
     pya.MainWindow.instance().menu().action("@toolbar.set_origin").trigger()
    

    when you click on the select tool, it will finishes the tool or you can finish it in code with:

     pya.MainWindow.instance().cancel()
    

    In your code you have to ungrab mouse when deactivated

  • Hi @jonathan,

    You are right, thank you very much for this answer! This unlocked my misunderstanding issue about deactivation! I had never tried to hide the plugin from the toolbar itself, so it made things more complicated than it should. I still have KLayout crashing on re-activation but I should check in my code. In the example above it works fine for me also :)

    Thank you both,
    Chris

  • Hi @crizos you are welcome, but I have to also thank you for highlighting the missing ungrab mouse which is quite important there. :)

  • Thanks for these posts - my understanding of "deactivation" wasn't "hiding".

    I'd still like to debug the crash. Taking the above code from Dec/10, can I modify it in a way so I can reproduce the crash?

    Thanks,

    Matthias

  • the issue was simply that we really need to ungrab mouse when the tool is deactivated:

    def deactivated(self):
      self.ungrab_mouse()
    

    If we forget to ungrab mouse and we switch tools then there is a bug, probably a conflict between the new mouse grab from the new tool and the old mouse from our own tool.

  • Thanks for these posts - my understanding of "deactivation" wasn't "hiding".

    You are right that wasn't the issue. Firstly I wanted to move the plugin from toolbar to menu, as Tools option, so I set variable has_tool_entry to False and then I used add_menu_entry.

    So, from what I understand, if has_tool_entry is False, the prio attribute of mouse_moved_event never returns True (that means is never triggered?).

    Because of these issues, I won't add a Tools entry and I will use Jonathan's suggestions for the plugin's toolbar visibility.

    My mistake was that I thought that both 'menu' and 'tool' entries work the same way. In my sample, the plugin was never actually triggered, so normally I couldn't deactivate something that was not already activated. I think that is the reason that KLayout crashes.

    Best regards,
    Chris

Sign In or Register to comment.