Automatically Down-save File to Previous Version

Hello all!

I’m trying to create a script that will automatically save my file to 2017 from my 2019 Pro version of Sketchup. Though it’s easy enough to manually do a Save as…, or come up with a script that does this on command, I’m trying to idiot-proof the process by doing this without having to consciously think about it.

I’ve been trying out the ModelObserver (my first foray into Observer objects) without much luck - I have the observer trigger my save to 2017 every time the model tries to save, which triggers the observer again, and I end up in a loop.

I tried to get past this by using a system of “flags” via Module variables to creat conditional statements to prevent the infinte loop, but still couldn’t figure it out.

My latest attempt tries to remove the ModelObserver when it’s time to do the 2017 save, and re-add it to the model for subsequent saves, but this somehow causes another kind of existential crisis that results in a Sketchup crash.

Anyone have any thought? Thanks for your time!

The easiest solution would be to added a menu option to the file menu ‘Save as 2017’, then assign the Ctrl+S short cut to it. Not totally idiot proof though.

If you save your ‘flag’ as a model attribute you can easily tell if the save was triggered from your observer or not. I just tested this and it automatically saves as version 2017. This is a great idea. I have several staff using various version and this would simplify sharing of models.

class MyModelObserver < Sketchup::ModelObserver
  def onPostSaveModel(model)
    status = model.get_attribute('MyPlugin', 'SaveStatus')
    if status == 'Saving as 2017'
      #this is a save triggered by the observer
      puts 'The model was saved by the observer as version 2017.'
      model.set_attribute('MyPlugin', 'SaveStatus', '')      
    else
      #this is a save triggered by the user
      puts 'The model was saved by the user. Attempting to save as version 2017.'
      model.set_attribute('MyPlugin', 'SaveStatus', 'Saving as 2017')      
      puts "Saving as 2017 #{model}"
      model.save(model.path, Sketchup::Model::VERSION_2017)       
    end
  end
end

# Attach the observer.
@model_observer = Sketchup.active_model.add_observer(MyModelObserver.new)

Does #set_attribute change the model object’s #modified? flag ?

You are changing it after a save … which means (if so) the model will always want to be saved like when you try to close it or SketchUp.

Good question. Apparently it does not.

That seems strange. I don’t know if I would rely upon that not changing in the future.
I can see that some extension devs would want to make sure that their plugin attributes get saved if changed.

I think that I logged an issue with the modified? flag with respect to the model name and description properties.

What if you instead create a extension variable that tracks the active model but doesn’t actually write an model attribute ?

Also another thing that changes after a save is the model #guid.


I’m also thinking that a SaveAs from within of the ModelObserver save callbacks should not trigger save callbacks.

Here is an updated version that uses a class variable. I also added the ability to specify the version, as well as turning it off simply by passing an invalid value.

class MyModelObserver < Sketchup::ModelObserver
  @status = ''
  @save_version = ''
  @save_version_name = ''
  def onPostSaveModel(model)
    if @status == "Saving as #{@save_version_name}"
      #this is a save triggered by the observer
      puts "The model was saved by the observer as #{@save_version_name}."
      @status = ''
    elsif @save_version && @save_version != ''
      #this is a save triggered by the user
      puts "The model was saved by the user. Attempting to save as #{@save_version_name}."
      @status = "Saving as #{@save_version_name}"
      model.save(model.path, @save_version)      
    end
  end
  
  def save_version(version = nil)
    return @save_version if version.nil?

    const_values = Sketchup::Model.constants.map { |c| Sketchup::Model.const_get(c.to_s) }  
    return unless const_values.include?(version)
    
    @save_version_name = Sketchup::Model.constants.select { |c| Sketchup::Model.const_get(c.to_s) == version }[0]
    @save_version = version
  end
end

# Attach the observer.
@model_observer = MyModelObserver.new
Sketchup.active_model.add_observer(@model_observer)
@model_observer.save_version(Sketchup::Model::VERSION_2017)

The proper way to turn off the feature would be to remove the observer.

Getting better, … but on the Mac, there can be multiple models open.

So I think that @status needs to be init’d in an initialize() method as an instance var of the observer instance (attached to only 1 model. Ie, each model instance gets it’s own observer instance.)


I wonder if the save_version setter should be a class method ?
(It sets the version option for ALL models, correct?)


I’d also collect all the version constant info into a hash referenced by a class constant at the top of the class definition:

  VERSIONS ||= Sketchup::Model.constants.grep(/^VER/).map { |sym|
    [ Sketchup::Model.const_get(sym,false), sym ]
  }.to_h
  # builds a hash from an array of 2 element [key,value] subarrays

Ie, there are a few other non-version constants in the class that also reference integer IDs, so we need to filter by those starting with "VER".

So const_values is replaced by the VERSIONS hash which has Integer version ID keys and symbol constant names as values.

In the save_version setter method …

    return unless VERSIONS.has_key?(version)

And… to get the version symbol name simply do …

    @save_version_name = VERSIONS[version]

… and no select iteration is needed.

The String interpolation (later) will call #to_s on the symbol names for you.


And … in actual use, there’d be an AppObserver to go along with this and automatically attach individual auto save observer instances to each of the models as they are opened.

This could be done with a menu item, which either detaches the observer instances, or uses a boolean @active class variable and the callback has a bailout test as the first statement …

return unless @active
1 Like

Excellent - thank you Neil & Dan! Comparing your code and what I tried with flags, I messed up the context of the variables. Between that and adding an AppObserver (which I wouldn’t have figured out so readily), it works well for me…

A sidebar comment about assigning Ctrl+S to a modified line of code - I tried that initially and probably would’ve left it like that, but it looks like Sketchup automatically reassigned the shortcut back to the default Save. I wonder if certain standard Windows shortcuts can’t be used in Sketchup?

1 Like

On Windows, user shortcut keys involving Ctrl will “stick” only for the current session. SketchUp resets them to the default menu shortcut keys each time it starts. On Mac, SketchUp doesn’t even allow you to set shortcuts using the Command key.