Saturday, November 1, 2008

Shoes Apps With Multiple Windows

From Cuberick
Creating a shoes app with multiple windows is as easy as using the window method. Here is a simple example

#!/usr/bin/env open -a Shoes.app
class Examplish < Shoes
url "/", :index
url "/new", :new_window

def index
para link "make a new window", :click => "/new"
end

def new_window
window do
para "a new window"
end
end

end
Shoes.app


However there are a few gotchas hidden in this goodness. First notice that if you execute this code the original window is now blank. At first I was surprised, but what is happening is that since you have clicked a link shoes expects you to visit a new url. We aren't supplying one. Simply add
visit "/"
after the window block to tell shoes to forward you on to the same page you were at and now we have the expected behavior.

Now, the big gotcha is that inside the window block the self object is completely different than it is outside the window block. Even more frustrating is that methods defined on the parent aren't available to the child. This can be baffling, a cause of duplication and it turns out a work around is difficult. For example, lets say we have a method called header in our main window that puts a header on the page, since self is redefined inside the new window we no longer have access to that function, therefore we have to re-implement the header function inside the window block. We need a place to put shared code! It won't be easy to find a place. We can't use a module becuase all the methods we need to add content to the page, things like 'para', 'link', 'stack', 'flow', all only exist on classes that extend Shoes, and we need a reference to the specific instance of Shoes.app that is associated with our window.

How can we have shared code when the methods we depend on only exist on a particular instance of Shoes.app? I haven't really found a satisfactory workaround to this problem. The best I have come up with is to put all shared code into a separate class. That clearly solves the problem of giving it a place that is is accesable, but what about the problem of accessing all the magic shoes functions? For that I make our new class a proxy of self (the Shoes.app object). Here is code to illustrate what I mean.

#!/usr/bin/env open -a Shoes.app
class Examplish < Shoes
url "/", :index
url "/new", :new_window

def index
@proxy ||= ProxyHelper.new(self)
stack do
@proxy.header
para link "make a new window", :click => "/new"
end
end

def new_window
window do
@proxy ||= ProxyHelper.new(self)
stack do
@proxy.header
para "a new window"
end
end
visit "/"
end

end

class ProxyHelper
def initialize(app)
@app = app
end

def header
banner "Shared Code!"
end

def method_missing(meth, *args, &block)
@app.send(meth, *args, &block)
end
end

Shoes.app


Notice how you pass the self or Shoes.app object in when you create the proxy object. Then with a bit of simple method missing magic you can now call all the magic shoes functions seamlessly... well, almost. I have noticed that the link method doesn't work unless you call it directly on the Shoes.app object. And that brings up a bigger issue with this solution. Since all the urls are defined on the other Shoes.app object you can't use any of the navigation routes. Bummer! This is a major bummer! I hope I am missing something and somebody can tell me the right way to go about this, because if not it seriously limits the usefulness of multiple windows in shoes. To my mind the proper behavior inside of a window block is to make sure the new instance of Shoes.app has access to the methods defined on the parent window as well as all the urls. I guess the Shoes::Widget is the only supported way to share code among windows, but as cool as the Widgets are, they don't seem to fit the pattern I am talking about. I'm going to take this up with the shoes mailing list, they can probably sort this out.

3 comments:

Roger Pack said...

have you asked about these items on the shoes mailing list?

Shlomo said...

Yes. I wrote this up after having a fairly lengthy discussion on the shoes mailing list. You can read the thread here: http://code.whytheluckystiff.net/list/shoes/ search for posts with the subject "sharing code among multiple windows"

I'm not sure how much stuff has changed since then because I haven't been active in the shoes world lately. I believe that why made some changes related to this discussion.

Roger Pack said...

After some experimenting [and some scratching my head like "why did _why do it this way?"]

Don't know if this will be helpful, but..background: it appears you can share variables by using

@@variable = something

Of course, global variables are shared, as well.

You can then share code by defining "all your code outside the block, in some other class", a la http://www.restafari.org/object-oriented-sheep-running-in-ruby-shoes.html


Or you can have some shared module instance where you keep it all

ex [just include the shared module once]

Shoes.app do
@@shared = Module.new
self.class.class_eval { include @@shared }

@@shared.module_eval {
def shared_method
Shoes.debug('in shared method')
end
}

shared_method
window do
shared_method
end
end

Or if you want to just include certain methods within certain blocks:

# some helper methods

Shoes::App.class_eval {

def singleton_class
class << self; self; end
end

def singleton_include mod
singleton_class.class_eval { include mod }
end

}

Shoes.app do
@@shared = Module.new

@@shared.module_eval {
def shared_method
Shoes.debug('in shared method')
end
}
singleton_include @@shared
shared_method
window do
singleton_include @@shared # this is necessary, adds shared_method to this block
shared_method
end
end

But man that is just dang weird.

The thing that gets me now is why, if you close a window, all of its "sub timers" are interrupted and never fire.
Odd.

Cheers!
-=r