Observer event on closing model

I assume you mean on Windows…please describe in more detail.

PC==Windows.
The ‘However,…’ line follows on immediately after what I said about PCs.
MAC and PC vary because of the different way they load a SKP in one and multiple instances of SketchUp respectively.
However, on a PC during a session aspects of ‘ruby’ use can be carried across.
If you have a @/@@variable set up in one SKP… then in-code open another SKP that variable ought to carry over.
On a MAC since it’s the same SketchUp instance it should carry over too.

The main difference I notice between MAC and PC is that if I open a SKP from with SketchUp.
On a MAC the former SKP also stays open.
On a PC the original SKP closes and I still have one SKP open.
However, on a PC there are still several ways of opening a second SKP whilst leaving the current one open…

The observer engine, polls each attached observer in turn, using respond_to?(:callback_name) to determine IF that particular observer actaully has the named callback. IF so, it then calls the callback for that observer, … if not, it skips to the next observer in it’s attached observer stack.

I found this one hidden away,

not sure how much is my code, it’s probably a mashup…

seems to work for all mac scenarios, including startup if in plugins folder…

module ModelChangeObserverTestJcB

    # This is an example of an observer that watches the application for
    # active model changes and updates a webdialog.
    class MyAppObserverJcB < Sketchup::AppObserver
      @msg = 'no message'
      def expectsStartupModelNotifications()
        return true
      end
      def onNewModel(model)
        @msg =  "onNewModel:"
        update_window(model)
      end
      def onActivateModel(model)
        @msg =  "onActivateModel:"
        update_window(model)
      end
      def onOpenModel(model)
        @msg =  "onOpenModel:"
        update_window(model)
      end
      private
      def update_window(model)
        puts @msg
        ModelChangeObserverTestJcB.update(model)
      end
    end


    def self.create_window
      @window = UI::WebDialog.new
      @window.set_position(200,200)
      @window.set_size(440,200)
      @window.set_html('<html><body>[No Data]</body></html>')
      @window
    end


    def self.update(model)
      @window ||= self.create_window
      if @window.visible?
        @window.bring_to_front
      else
        if Sketchup.platform == :platform_osx
          @window.show_modal
        else
          @window.show
        end
      end
      html = <<EOT

<html>
<body>
<h1>#{model.title}</h1>
<h2>#{model.to_s.gsub('<', '&lt;').gsub('>', '&gt;')}</h2>
<h2>#{model.path}</h2>
</body>
</html>

EOT
      @window.set_html(html)
    end


    # Attach the observer
    Sketchup.add_observer(MyAppObserverJcB.new)
    self.update(Sketchup.active_model)

end # module


EDIT: Sorry, duplicate post

Testing on MAC, I think this ModelChangeObserverTestJcB still has the same issue that if the user closes the last model window without exiting SU, the dialog doesn’t get cleaned up.

I have a working class I’ll post here in case it helps others. Since I have several different dialogs to manage, I have a superclass that takes care of the dirty work. Thanks to all for your input.

The sub class will have to:

  • Call the super’s init function with a window title: self.init(title=‘My Nifty Dialog’)
  • Define its own html text string to display in the dialog
  • Have a “callRuby(actionName)” or similar in the JS which employs the “window.location=‘skp:send:cmd@’ + actionName” protocol
  • Call super(HTML_text, wid, len, x, y, polling) in its own initialize()
  • Contain a refresh() method which updates the contents of the dialog when needed
  • Contain a receive(action) method which receives actions from JS
  • Determine how the dialog will get instantiated (i.e. create a UI menu or the like)

Super class:

class AttrDialog
  class << self            #Store instances array in the class object so each child class has its own version
    attr_accessor(:instances, :title, :appobs)
    def init(title)
      @instances = Hash.new        if not defined?(@instances)
      @title = title            #Title for dialog window and menu item
      @appobs = nil            #application observer
    end
  end
  
# Call this any time a model becomes the current active model
# -----------------------------------------------------------------------------
  class AttrAppObserver < Sketchup::AppObserver
    def initialize(c); 
      @myClass = c
    end
    def expectsStartupModelNotifications(); return true; end        #For backward compatibility<SU2014

    def onOpenModel(model)
      @myClass.newModel(model)
    end
    def onActivateModel(model)
#printf("Activating:%s self:%s\n", model, self)
      @myClass.newModel(model)
    end
  end #AttrAppObserver

# Call this any time the selection changes
# -----------------------------------------------------------------------------
  class AttrSelObserver < Sketchup::SelectionObserver
    def initialize(dialog)
      @dialog = dialog
    end
    def onSelectionBulkChange(selection)
      @dialog.refresh
    end
    def onSelectionCleared(selection)
      @dialog.refresh
    end
  end #AttrSelObserver

# Called from menu to create/launch new dialog if necessary
# -----------------------------------------------------------------------------
  def AttrDialog.check(mm = nil)        #Class method so it can be called before any objects have been created
#printf("In check model:%s self:%s inst:%d\n", mm, self, self.instances.length)
    return self.instances[mm] if mm        #If a model specified, tell whether editing for this feature is enabled or not
    mm = Sketchup.active_model            #Otherwise, assume active model
    if self.instances[mm]                #If a dialog exists, delete it
      self.instances[mm].close
    else                    #Otherwise, create one
      self.instances[mm] = new() if !self.instances[mm]    #Create a dialog for this model if we don't already have one
      self.instances[mm].refresh            #Update the dialog widget
    end
  end #check

# Show only the dialog for the specified model (or hide all)
# -----------------------------------------------------------------------------
  def AttrDialog.newModel(m = nil)        #Class method so it can be called before any objects have been created
#printf("In newModel:%s self:%s\n", m, self)
    self.instances.each_pair { |mod, dia|
      return if !dia
      if (mod == m); dia.refresh; else; dia.hide; end
    }
  end #newModel

# -----------------------------------------------------------------------------
  def AttrDialog.closeAll ()            #Close all instances
    self.instances.each_value { |i| i.close}
  end #closeAll
  
# -----------------------------------------------------------------------------
  def initialize(html, x, y, xo, yo, ping = 1000)    #When object created
#printf("AttrDialog initialize\n")
    @myTitle = self.class.title
    @myDialog = UI::WebDialog.new(@myTitle, true, nil, x, y, xo, yo, true)
    @myDialog.set_html(html)            #Show our html content
    @myDialog.add_action_callback("send_cmd") { |dialog, action|    # Attach an action callback to the dialog window
      receive(action)                #Our handler for calls from ruby
    }
    @myModel = Sketchup.active_model
    @myModel.selection.add_observer(@selObserver = AttrSelObserver.new(self))
    Sketchup.add_observer(self.class.appobs = AttrAppObserver.new(self.class)) if !self.class.appobs

    @myDialog.add_action_callback("ping") { |dialog, action|    # Observers can't tell us when last model closed on MAC platform so we have to poll from javascript
#printf("Catch ping: %s %s\n", dialog, action)
      close() if (not @myModel) || (not @myModel.valid?)
    }
    @myDialog.show {                 #Polling pinger helpful on MAC to close any stray windows
      @myDialog.execute_script("pinger=setInterval(function(){ window.location='skp:ping@pong'; }, #{ping});") if ping > 0
      refresh 
    }
  end #initialize

# Turn off this dialog
# -----------------------------------------------------------------------------
  def close ()                    #Destroy object
    if @selObserver && @myModel && @myModel.selection        #Cancel our selection observer
      @myModel.selection.remove_observer(@selObserver)
      @selObserver = nil
    end
    @myDialog.execute_script("clearInterval(pinger);")
    @myDialog.close                #Close the dialog window
    self.class.instances.delete(@myModel)    #Forget about this instance
    Sketchup.remove_observer(self.class.appobs) if (self.class.instances.length <= 0)    #If this was the last instance
  end #close

# Close the dialog window without destroying the dialog object
# -----------------------------------------------------------------------------
  def hide ()
    @myDialog.execute_script("window.close()")
  end #hide

# Make sure our dialog window is visible
# -----------------------------------------------------------------------------
  def show ()
    @myDialog.show() unless @myDialog.visible?
  end #show

end #AttrDialog

Sample sub class:

# A sample dialog window which uses the AttrDialog superclass
# -----------------------------------------------------------------------------
require_relative 'attrdialog.rb'

# Dialog for examining and modifying Sketchup entity parameters
# -----------------------------------------------------------------------------
class TestDialog    < AttrDialog
  self.init(title='A test Dialog')

  # HTML code to display in a webdialog for editiing parameters for our object
# -----------------------------------------------------------------------------
  HTML_text = %q^<html><head>
   <script type="text/javascript">
    function callRuby(actionName) {
      query = 'skp:send_cmd@' + actionName;
      window.location.href = query;
    }
    function updateMessage(msg) {
      document.getElementById('message').innerHTML = msg;
    }
   </script></head>
   <body>
   A test Dialog Window
   <p>
   <div id="message">Message goes here</div>
    <input type="button" value="A Button:" onclick="callRuby('myAction')" title="Press to send a message to Ruby">
   </body>
  </html>^

# -----------------------------------------------------------------------------
  def initialize ()
    super(HTML_text, 360, 280, 80, 500)
  end #init

# -----------------------------------------------------------------------------
  def refresh ()                #Refresh the info in the dialog
    show
printf("Refreshing\n")    #Update contents of dialog with latest info...

    @myDialog.execute_script("updateMessage(#{Time.new().to_s.inspect})")
  end #refresh

# -----------------------------------------------------------------------------
  def receive (action)                #Receive commands from webdialog
printf("receive action_name:%s \n", action)
    @myDialog.execute_script("updateMessage('Got button')")
  end #receive

end #TestDialog

TestDialog.check        #Instantiate our window

Okay, I’m running Windows, so I can’t test for the real issue that OSX has.

But, rather than using a javascript interval, why not a UI timer? The following code (for the most part) leaves SU responsive. FWIW, I’ve used both timers in code. Both can be helpful for unwinding the call stack and removing code blocking…

Important - timer doesn’t stop, you have to shut down SketchUp

class ClosedModel

  def initialize
    @id_am = Sketchup.active_model.object_id
    @id_tmr = nil
    @t0 = Time.now.to_f
    start()
  end
  
  def start()
    @id_tmr = UI.start_timer(1.0, true) {
      if @id_am != Sketchup.active_model.object_id
        puts 'Model Closed  %10.2f' % (Time.now.to_f - @t0)
        # stop timer for other code?
        @id_am = Sketchup.active_model.object_id
      end
    }
  end

end
ClosedModel.new()

Greg

[]
(http://msp-greg.github.io/su_info/index.html)