shoes / shoes3

a tiny graphical app kit for ruby
http://walkabout.mvmanila.com
Other
181 stars 19 forks source link

Mouse over #219

Closed Dassadar closed 7 years ago

Dassadar commented 8 years ago

Hello,

For shoes evolutions, is it planned to have a mouse over functionality compatible with stacks and flows please? For example to display a tooltip (so that would actually be two requirements! ;-) )

Regards, David

passenger94 commented 8 years ago

Hello, There is hover and leave functions on slots, you can use them to achieve some rudimentary tooltip behavior :

Shoes.app width: 300, height: 200 do

    area = stack width: 50, height: 50, top: 40, left: 40 do
        background darkorange..lime
    end

    area.hover do |slot|
        slot.contents[0].fill = lime..darkorange
        slot.refresh_slot
        @popup = window width: 100, height: 50, title: "Tooltip" do
            inscription "Time has no respect for whatever is done without itself !"
        end unless @popup
    end
    area.leave do |slot|
        slot.contents[0].fill = darkorange..lime
        slot.refresh_slot
        @popup.close if @popup
        @popup = nil
    end

end

So yes there is mouse over functionalities, no nothing planned at the moment for a real tooltip/popup behavior ... Regards

ccoupe commented 8 years ago

We don't really have a light weight tool tip. Perhaps a tooltip => string style could be added to stacks, flows and all widgets and cursor handling could be modified detect that but that would be a very large change and very difficult to debug for all platforms. We don't have menus and menu items for similar reasons - both are unbelievably complicated and would change Shoes from being fun and easy.

Request noted. Don't hold your breath waiting.

dredknight commented 8 years ago

Hello everyone !

I am working with shoes for some time and really wanted to contribute! I am not a developer so I am not sure if my tips will come in handy but still here they are!

I am currently writing a small app which has 140 elements, each of which require description pop up which had to be presented next to the mouse pointer.

I tried a few things (most of them unsuccessful). At the end I reached the desired behavior with the code. There is a hidden flow/stack named @menu. It appears where the mouse pointer is!

X and Y define the current element in array of arrays called @box, each box is 40x40 slot and may have image inside. The problem is that when empty no action should happen during interaction. When there is image there is also a hidden semi transparent bright hidden colour defined for the slot which brights the image when hovered (defined as array of arrays named @select ).

Another problem I stumbled upon is how to dynamically configure the the width and height of the @menu so the text fits. I figured out this by placing a formula which reconfigure the @menu parameters based on the length of the text (@description array).

At the end there is the toggle action which shows the pop up !

motion do |L, T|
        @move_left=L
        @move_top=T
    end

    def skillz  x, y    
        @box[x][y].hover do
            unless @select[x][y]==nil then 
                @menu.style width: 210+text.length/3, height: 90+(text.length/7)*3 
                menu_left=@move_left; menu_top=@move_top;
                if @move_left+@menu.width >= app.width+10 then menu_left=@move_left-@menu.width end
                if @move_top+@menu.height >= app.height+10 then menu_top=@move_top-@menu.height end
                @menu.move(menu_left, menu_top)
                @select[x][y].toggle;
                @menu.clear do 
                    background rgb(80,100,120,0.8)
                    border(ghostwhite, strokewidth: 2)
                    para "#{@description[x][y]}", justify: true, align: "center", stroke: white
                end
                timer(0.1) { @menu.toggle }
            end     
        end 
        @box[x][y].leave {  unless @select[x][y]==nil then @select[x][y].toggle; @menu.toggle end }
    end 

I hope this is helpful!

passenger94 commented 8 years ago

hello dredknight, Welcome Thank you very much!

Nice idea, Could you provide a little shoes app demonstrating the basic idea of your pop up, for example just one or two boxes with the hover/leave mechanism and of course all the minimum setting the demo might need

for the code markdown : type this (without the quotes) " ```ruby " the line before your code and then the same without the word "ruby" but the line after your code for details look up here : https://guides.github.com/features/mastering-markdown/

dredknight commented 8 years ago

Done! Thanks a lot!

Give me a week I will provide you the complete scenario!

P.S.

Actually it has all the things we discuss as it is now so check it out from here -> https://www.dropbox.com/s/7waei93xuctnwko/beta_skillwheel.exe?dl=0

ccoupe commented 8 years ago

@dredknight - Welcome, We can always use help. A lot of Shoes is written in Ruby. For this particular example, We could put up snippets/tips/sample category in the wiki and you can put/edit you example as an article. Getting a new article linked into wiki menus is a minor pain and uploading images to the wiki is obscure. That way your work won't be lost in a bug report.

dredknight commented 8 years ago

Thanks a lot! I I am quite new to git hub so I am not really aware where should I go and write the article. I saw that I can add new page on the wiki. Is this how it is made?

ccoupe commented 8 years ago

@dredknight , Correct. Pick a faiirly short title and edit away in markdown format. Save button is on the bottom. The let me know on this message what the title is and I'll setup the menu for that. (You could do that yourself too but that's a lot more effort for some just starting out. You can alway delete it you don't like and the title can be changed.

ccoupe commented 8 years ago

@dredknight - I took the liberty of moving your comment to https://github.com/Shoes3/shoes3/wiki/Tooltips-Fun which you can edit to your desire.

dredknight commented 8 years ago

Great thanks! I will wait a bit before I edit it. I want to make the method crystal clear easy first.

passenger94 commented 8 years ago

This is fun, we should try all kind of approaches to this, shows the power of Shoes ... @Dassadar, Building upon @dredknight idea, i tested a few things : tooltip with shadows, a bit of thickness, transparency as @dredknight suggested, using start method to draw above all other elements, also an alternative to Shoes::Widget (as in Shoes.log or Shoes.manual)

wiseness

tiny app is here : https://gist.github.com/passenger94/e1e5c86c9fbb7a225ccf

dredknight commented 8 years ago

Very nice!

I am not very good at ruby yet there is one thing I do not understand in the code

 pop_slot.instance_eval %{       ##### What instance_evail% means ? how does it work?? 
            def slide(x,y)
                move(x-#{@offset}, y-#{@offset})       ### how do you define what are you moving here?
                self
            end
            def tip=(tp)
                contents[3].text = tp           ### where is contents[] array defined?
            end
            def back_color=(pattern)
                contents[2].fill = pattern
                refresh_slot                      #### what is this? I dont see it anywhere? how come it works?
            end
        }

P.S. Alright.. just as I wrote this I got a vision ! Basically I think it works like this - pop_slot.instance_eval % open up for edit the stack above. all the things I dont understand are actually predefined variables in the STACK method. anyway any explanation will be welcomed :) ireally dont get that % sign.

passenger94 commented 8 years ago

Hi, Yes that's it ! instance_eval is one of the numerous way in ruby to add dynamically methods to an object it takes either a string or a block as argument, the %{} is one of the numerous way in ruby to write a string :-D https://en.wikibooks.org/wiki/Ruby_Programming/Syntax/Literals instance_eval is creating methods in the context of the receiver, here pop_slot, so in there self is that object and all calls to method are ending up on the object, for example pop_slot.contents[3]

refresh_slot is a new method recently added to slots, maybe we forgot to write a manual entry ! (allows us to tell lazy Shoes to do it's work and draw immediately)

dredknight commented 8 years ago

Alright... I need to experiment... see more examples of this. I hope I can create some other magic with it :D

One more thing:

At row 60 of the code there is this @pop.slide

This calls module pop_slot.slide but how??? they are different and @pop is never defined...

passenger94 commented 8 years ago

look in shoes.rb and the shoes ruby library, Tons of inspiration there :-)

dredknight commented 8 years ago

Yes this is how I learned most of the stuff. Just one thing do not comprehend...

Row 60

 @pop.slide(@mx, @my).

Basically it is not defined anywhere before that and on the top of that Shoes understsands that you call SLIDE definition from the class.

Here is how I see it though not sure if correct.

@pop is new variable with (@)self so basically it inherits the possibility to call class instances that are in the app by default (inheritance).

If it is not like this I dont get it...

passenger94 commented 8 years ago

Maybe there is 2 not so clear things going on here: first remember that instance variables are available everywhere in the Shoes app block no matter where you defined them (inside the block of course) there's a chapter which talks a bit about it in the Manual : Shoes >> Rules the second is just a matter of understanding the timing of events i could have written the stack and events callback like this (in pseudo code) :

main_slot = stack do
    caption ...
    @active_slot = flow ... do
        background ...
        para ...
    end
    edit_line ...
    para ...
end
main_slot.start { @pop = pop_over.slide @mx-1000, @my-1000 }

@active_slot.hover { @pop.slide(@mx, @my) ... }
@active_slot.leave { ... }

the @pop object is defined in the start event callback, otherwise as Shoes drawings are asynchronous, the edit_line would be drawn over the @pop ! (that's why i put that edit_line in there, just as visual feedback) so :

and now we have our @pop objet alive with his new methods you know the rest of the story :-) hope it helps !

PS both way of writing the code are equivalent, just personal taste

dredknight commented 8 years ago

Thanks!!! Back to work now when I have results I will share them :)

dredknight commented 8 years ago

I took the liberty of playing with that code you gave me. If you change some of the class code with this thing here you get dynamically resizable pop up window. The the size depends on the length of the input string.

There are a lot of flows though. if you place a new line symbol in the string the height of the block would not be increased by one line. there is also a problem with big words (try to place a word of 30 symbols for example). the string is not transferred on the line below when it reaches the end of the width.

It is very basic but this is as far as I could go for now. Let me know if you have any tips for how I can detect tabulation and new line in the string. There was some issue with dynamically resizing the shadow as well so I just commented it.

Cheers!

        pop_slot = stack width: @pwidth, height: 200 do
            image width: @pwidth, height: 200 do
                #rect @offset, @offset, (@pwidth-@offset*2), 100, curve: 15, fill: rgb(0,0,0,0.4), stroke: rgb(0,0,0,0)
                #shadow radius: 10, distance: 10, fill: rgb(0,0,0,0.8)
            end
            background rgb(255,255,255,0.5)
            background rgb(255,0,255,0.5)

            para msg 
        end

        pop_slot.instance_eval %{
            def slide(x,y)
                move(x-#{@offset}, y-#{@offset})
                self
            end
            def tip=(tp)
                contents[3].text = tp
                counter = tp.length
                size = 20+tp.length*10
                height=60
                while counter > 50 do
                    size=400
                    height+=25
                    counter-=50
                end
                contents[0].style width: size , height: 200;
                #contents[0].rect.style left: #{@offset} ,top: #{@offset}, width: (size-#{@offset}),height: height 
                contents[1].style curve: 15, width: (size-#{@offset}*2-1), height: height, left: #{@offset}, top: #{@offset}
                contents[2].style curve: 15, width: (size-#{@offset}*2-3), height: height, left: #{@offset}+2, top:#{@offset}+2
                contents[3].style left: #{@offset}+10, top: #{@offset}+10, width: size-#{@offset}*2-10 
            end
            def back_color=(pattern)
                contents[2].fill = pattern
                refresh_slot
            end
        }

        pop_slot
passenger94 commented 8 years ago

Ha ha !! Cool ! :+1:
I'm wondering though if you wouldn't be better to recreate completely the slot on each new text instead of trying to accommodate because it's getting real hot in there :-D For the shadow you would have to manage the size of it also and the @offset (shadow -all effects - needs room around them otherwise you get rendering artefacts)

for a big word there is no automatic cut and line return that i'm aware of in Shoes , so you probably have to manage this. for tabulations and newlines, i think you would probably have to use some regex, scan the string, increase size, etc ... another level of complexity ...(maybe there is a better solution) Looks like a candidate for an outside method, maybe an entire Class ! Also if you go for that level of complexity, maybe a module is not good anymore and a Shoes::Widget would be better ...

Meanwhile getting a clean way to do it, you could always cheat :notes: , and calculate beforehand the needed size, and feed the slot initializer with the good numbers, in most situation one provide hard coded text anyway so why not give dimensions ...

EDIT : one more thing, use a mono font for the text so you have (more ?) predictable text size.

dredknight commented 8 years ago

passenger94 very good tips but there are some technical issues :D. I dont know how to create a widget.. never done such. Otherwise as soon as I feel more comfortable with classes I will refurbish it from top to bottom :).

passenger94 commented 8 years ago

i just wrote something about Shoes::Widget : https://github.com/Shoes3/shoes3/wiki/Of-Shoes::Widget-and-Shoes::Canvas

dredknight commented 8 years ago

Thanks this shed quite some light! I think there is an error though.

The widget class is named CheckText but it is called with the name of check_text. Why they are not the same?

passenger94 commented 8 years ago

I need to make that clear, thanks ! check_text is a factory method : it creates an instance of CheckText object, the name of that method is build dynamically from the name of the Class, following established convention (not that much shared convention , have to admit ) : https://github.com/Shoes3/shoes3/blob/master/lib/shoes.rb#L613 takes the CamelCase name of the Class, downcase it and separate 'words" delimited by Capital letters with a "_" (don't remember the word now !!) A convenience method, so you can create the widget without much boilerplate.

ccoupe commented 8 years ago

i just wrote something about Shoes::Widget :

I think I've seen my faulty memory about when Shoes.app acts like a stack. It could have been multiple flows! I'm easily confused! I've linked the article into the wiki menus.

passenger94 commented 8 years ago

It could have been multiple flows!

it traps me almost every time :-D

btw, i'm trying to collect some info about self and app not beeing the same if you want to add notes ... https://gist.github.com/passenger94/833d309b8599b1e14353#file-canvas_app_and_self-md

dredknight commented 8 years ago

Alright guys I started doing something of a widget but kind of dont understand why is not working...

class PopUp < Shoes::Widget
    def initialize(element, text)
        @element=element
        @text=text
        @offset=10
        motion { |x,y| @mx = x; @my = y }
        @pop_slot = stack width: 100, height: 100 do end.hide
    end 
    def create
        @element.hover do
            debug( "show" )
            @pop_slot.style width: header.length*10, height: header.length
            pop_left=@mx; 
            pop_top=@my;
            if @mx+@pop_slot.width >= app.width+@offset then pop_left=@mx-@pop_slot.width end
            if @my+@pop_slot.height >= app.height+@offset then pop_top=@my-@pop_slot.height end 
            @pop_slot.move(pop_left,pop_top)
            background rgb(248,248,255,0.8), curve: 15
            background rgb(80,100,120,0.8), curve: 15, width: @pop_slot.width-4, height: @pop_slot.height-4, left: 2, top: 2
            para "#{@text}", justify: true, align: "center", stroke: white, size: 10, width: @pop_slot.width-2*@offset, left: @offset, font: "Courier"
            @pop_slot.toggle
        end
        @element.leave {@pop_slot.toggle; debug( "close" )}
    end

end

Shoes.app width: 650, height: 200 do
    @h_object = flow width: 200, height:200 do
        background green
    end
    @description="tool"
    pop_up.new(@h_object, "#{@description}").create
    Shoes.show_log
end

I get wrong number of arguments (0 of 2) error... I bet it is something really stupid... What I did wrong?

passenger94 commented 8 years ago

pop_up.new is the faulty you don't call new on a Widget object to initialize it, you call directly the convenience method pop_up
It's a factory method ! so pop_up(@h_object, "#{@description}") will do. (you can chain with create of course) you might have another problem later, what is header ? probably just a part of code you don't show here ?

dredknight commented 8 years ago

Thanks ! I took this from another code and reshaped it just for the purpose of the popup widget.

Header is actually the length of the imported text. Check appropriate changes below! The @text.length formula is not correct still, I want to make it work first, later I will adjust the size in conjunction with the monotype font. Thanks a lot for the tips! :)

class PopUp < Shoes::Widget
    def initialize(element, text)
        @element=element
        @text=text
        @offset=10
        motion { |x,y| @mx = x; @my = y }
        @pop_slot = stack width: 100, height: 100 do end.hide
    end 
    def create
        @element.hover do
            debug( "show" )
            @pop_slot.style width: @text.length*10, height: @text.length
            pop_left=@mx; 
            pop_top=@my;
            if @mx+@pop_slot.width >= app.width+@offset then pop_left=@mx-@pop_slot.width end
            if @my+@pop_slot.height >= app.height+@offset then pop_top=@my-@pop_slot.height end 
            @pop_slot.move(pop_left,pop_top)
            background rgb(248,248,255,0.8), curve: 15
            background rgb(80,100,120,0.8), curve: 15, width: @pop_slot.width-4, height: @pop_slot.height-4, left: 2, top: 2
            para "#{@text}", justify: true, align: "center", stroke: white, size: 10, width: @pop_slot.width-2*@offset, left: @offset, font: "Courier"
            @pop_slot.toggle
        end
        @element.leave {@pop_slot.toggle; debug( "close" )}
    end

end

Shoes.app width: 650, height: 200 do
    @h_object = flow width: 200, height:200 do
        background green
    end
    @description="tool"
    pop_up(@h_object, "#{@description}").create
    Shoes.show_log
end
dredknight commented 8 years ago

Good news! I think it is going to work very nice :).

I just cannot understand one thing - I try to create a new stack/flow every time a pop up appears. Unfortunately it does not seem to be removed when I use finish or remove commands.

Here is a sample code from the one above.

class PopUp < Shoes::Widget
    def initialize(element, text)
        @element=element
        @text=text
        @offset=10
        motion { |x,y| @mx = x; @my = y }
    end 
    def create
        @element.hover do
            @pop_slot = stack do end.start
            debug( "show" )
            @pop_slot.style width: @text.length*10, height: @text.length
            pop_left=@mx; 
            pop_top=@my;
            if @mx+@pop_slot.width >= app.width+@offset then pop_left=@mx-@pop_slot.width end
            if @my+@pop_slot.height >= app.height+@offset then pop_top=@my-@pop_slot.height end 
            @pop_slot.move(pop_left,pop_top)
            background rgb(248,248,255,0.8), curve: 15
            background rgb(80,100,120,0.8), curve: 15, width: @pop_slot.width-4, height: @pop_slot.height-4, left: 2, top: 2
            para "#{@text}", justify: true, align: "center", stroke: white, size: 10, width: @pop_slot.width-2*@offset, left: @offset, font: "Courier"
            @pop_slot.toggle
        end
        @element.leave {@pop_slot.finish; debug( "close" )}
    end

end

Shoes.app width: 600, height: 600 do
    @h_object = flow width: 200, height:200 do
        background green
    end
    @description="tool"
    pop_up(@h_object, "#{@description}").create
    Shoes.show_log
end
passenger94 commented 8 years ago

Great ! some remarks : All events related methods, like start and finish, expect a block as parameter, they are not meant to be used alone, you must provide a block, inside that block you describe what you want to happen when Shoes fires that event. So for example @pop_slot.finish won't do anything because you didn't tell Shoes what to do at the finish event of @pop_slot (that event happens when @pop_slot is removed). Likewise for start event, it's not a command to start @pop_slot, there Shoes expects you to tell it what to do when start event happens (i.e. when the slot is drawn). In short do as you do with hover and leave.

You have to put what will be inside your pop_up (backgrounds; para) into @pop_slot or append them later

do you need the create method ? otherwise you can make everything happening in initialize

one can also initialize the slot, move it outside visible area and @pop_slot.clear { rebuild the inners of the pop_up } and bring it back at hover event.

Not sure to understand : do you want your pop_up to appear above the element (green area here) or below like it is in your example ?

adapting (one possible way) your code, this works for me :

class PopUp < Shoes::Widget
    def initialize(element, text)
        @element=element
        @text=text
        @width = @text.length*12
        @height = @text.length * 8
        @offset=10
        motion { |x,y| @mx = x; @my = y }
    end 
    def create
        @element.hover do
            @pop_slot = stack left: 0, top: 0, width: @width, height: @height do 
                background rgb(248,248,255,0.8), curve: 15
                background rgb(80,100,120,0.8), curve: 15, width: @width-4, height: @height-4, left: 2, top: 2
                para "#{@text}", justify: true, align: "center", stroke: white, size: 10, width: @width-2*@offset, left: @offset, font: "Courier"
            end
            debug( "show" )
            pop_left=@mx; 
            pop_top=@my;
            if @mx+@width >= app.width+@offset then pop_left=@mx-@width end
            if @my+@height >= app.height+@offset then pop_top=@my-@height end 
            @pop_slot.move(pop_left,pop_top)
        end
        @element.leave {@pop_slot.remove; debug( "close" )}
    end

end

Shoes.app width: 600, height: 600 do
    @h_object = flow width: 200, height:200 do
        background green
    end
    @description="tool"
    pop_up(@h_object, "#{@description}").create
    Shoes.show_log
end
dredknight commented 8 years ago

hahaha thanks @passenger94 :) The funny thing is that I have totally misunderstood start and finish. I believed that they start and finish the event , i.e. stack.finish will do the same as stack.remove, but it was the other way around... when one task finishes (in the current example the stack) start a another one (the one in brackets). Point taken!

I always want the popup to be on top on all stacks but I assumed this will be the case by default because when I call the class instance it creates slot and the latest created slot is always on top.

dredknight commented 8 years ago

check this one now. With our mutual effort now the text box is recalibrating perfectly based on text :) It also works for multiple elements.

class PopUp < Shoes::Widget
    def initialize(element, text)
        @element=element
        @text=text
        @width = @text.length*10
        @height = 50
        while @width >250 do
            case @width
                when 501..Float::INFINITY then @height+=21; @width-=250
                when 250..500 then @height+=21; @width=250
            end
        end

        @offset=10
        motion { |x,y| @mx = x; @my = y }
    end 
    def create
        @element.hover do
            @pop_slot = stack left: 0, top: 0, width: @width, height: @height do
                background rgb(248,248,255,0.8), curve: 15
                background rgb(80,100,120,0.8), curve: 15, width: @width-4, height: @height-4, left: 2, top: 2
                para "#{@text}", justify: true, align: "center", stroke: white, size: 10, width: @width-2*@offset, left: @offset, font: "Courier"
            end
            debug( "show" )
            pop_left=@mx; 
            pop_top=@my;
            if @mx+@width >= app.width+@offset then pop_left=@mx-@width end
            if @my+@height >= app.height+@offset then pop_top=@my-@height end 
            @pop_slot.move(pop_left,pop_top)
           # @pop_slot.toggle
        end
        @element.leave {@pop_slot.remove; debug( "close" )}
    end

end

Shoes.app width: 600, height: 600 do
    @h_object = flow width: 200, height:200 do
        background green
    end
    @h_object2 = flow width: 200, height:200 do
        background blue
    end
    @description="tool ddddddddddd dddddddddddddddddddddddd  ddddddddddddv  dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd  dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd ddddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd wwwwww dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd wwwwww dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd wwwwww dddddd dddddd dddddd qqqqqq wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww wwwwwwwwwwww   weeeeeeeeweeeeeeeeweeeeeeee  weeeeeeee "
    @des="ABVADSDAS"
    pop_up(@h_object, "#{@description}").create
    pop_up(@h_object2, "#{@des}").create
    Shoes.show_log
end
dredknight commented 8 years ago

Just poppin in to say that I am still working on the popup. I am closing on to something I appreciate more and more. When I think it is ready I will share it here :)

dredknight commented 8 years ago

Hey everyone,

I bumped into a technological issue while working on the popup thing. Lately I am very glad on how the methods are working so I decided to move to the next step and merge them into a little class.

The problem is purely connected to my inability to understand ruby classes.. I just cant get the hang out of this. Let me show you what happens exactly. I have this modules defined.

def set_popup img, wide, high, header, text = "", text2 = ""
        img.hover { open_popup wide, high, header, text, text2; }       
        img.leave { close_popup }
        motion { |x, y| @move_left = x; @move_top = y }
end 

def open_popup wide, high, header = "", text = "", text2 = ""
        offset = 10
        @menu.style width: wide, height: high
        menu_left = @move_left; menu_top = @move_top;
        @move_left+@menu.width >= app.width+offset ? menu_left = @move_left-@menu.width : nil
        @move_top+@menu.height >= app.height+offset ? menu_top = @move_top-@menu.height : nil           
        @menu.move(menu_left, menu_top)
        @menu.clear do 
            background rgb(120,42,5,0.5), curve: 15
            background rgb(180,150,110,0.7), curve: 15, width: @menu.width-4, height: @menu.height-4, left: 2, top: 2
            header != "" ? ( tagline strong("#{header}"), stroke: white ) : nil
            text != "" ?   ( para "#{text}", justify: true, align: "center", stroke: white, size: 10, width: @menu.width-2*offset, left: offset ) : nil
            text2 != "" ?  (para "#{text2}", justify: true, align: "center", stroke: rgb(50,50,50), size: 10, width: @menu.width-2*offset, left: offset ) : nil
        end
        timer(0.1) { @menu.show }       
end

def close_popup
    @menu.hide
end

Basically when you have them you can define popup on pretty much everything with an ease. The only drawback that I see is that one have to define the @menu (the variable that hold the hidden window that represents the popup) in the main app so it can hover globally for the whole APP and not just the insides of specific stack/flow.

Here is an example.

Shoes.app do
     Object1 = image "shoes.png"
     Object2 = image "gloves.png"
     set_popup ( Object1, 200, 50, "", "these are shoes" )
     set_popup ( Object2, 200, 50, "", "these are gloves" )
     @menu = stack height: 10 do end.hide
end

You can see how this operate in a complicated way by checking this app.

Now what I want to do is migrate this functionality in Class. I imagine this working the following way.

class popup
    initialize 
       @menu = stack height: 10 do end.hide  ##this stack should appear globally
    end

   def  create (object, wide, high, text)
          object.hover do
              #settting text
               # setup menu width/height
              #menu appears
          end
          object.leave do
              @menu.hide
           end
end

Shoes.app do
    pop1 = popup.new

     some_image1 = "shoes.png"
     some_image2 = "gloves.png"

     popup.create some_image1, 100, 50, "these are shoes"
     popup.create some_image2, 100, 50, "these are gloves"
end

Is it possible?

I dont know where to begin. I have basic understanding of class but it seems what class is, is not what i want while what I want is something classes offer - keeping/providing all variables in one instance.

P.S. I had some numerous tries on this and all of them ended nowhere...

ccoupe commented 8 years ago

Is it possible?

It probably is but be careful and understand where Shoes only Looks Like Ruby (tm) because it's very confusing to me too when I poke around that part of Shoes. There's information scattered about in the manual and wiki about some of what you want. @menu is a class variable of what class? Are you sure? The answer might surprise you once you get ruby to tell you. You do need to learn how to do introspection in Ruby and understand how classes and module work before digging into what Shoes does to confuse things. Hint - read the doc for Object, Class and Module or a good book on Ruby.

If all the 'things' with a popup are of the same ' type' you might look at sub-classing Shoes::widget. Look at c:"Program Files (x86)\shoes\samples\expert-custom-list-box.rb to get a feel for what can be done. It might be the best choice or maybe not. It deserves the 'expert' rating because it's expert territory and I'm in that group of Ruby campers.

passenger94 commented 8 years ago

agree ! looks like a good candidate for a Shoes::Widget otherwise you need a reference to the app object inside the class (at initialize) and explicitly call shoes methods on it, like stack : ( a Ruby class knows nothing about Shoes!)

class Popup
  def initialize(theapp)
    @app = theapp
    @menu = @app.stack
  end
end

you create your class like this

Shoes.app do
  Popup.new(self)
end

but Shoes::Widget might be easier

dredknight commented 8 years ago

@passenger94 I just started reading your Widget article. It explains not only widgets but how the whole Shoes thing work. It really sounds quite manageable now! There is one issue I foresee but I will get back to you if it really appears :).

Cheers!

dredknight commented 8 years ago

Alright I bumped into the issue and it seems even harder than I thought. Actually I think the way Shoes works will not allow this widget to happen. First here is the code

class PopUp < Shoes::Widget

        def initialize(img, text, options={} )
            motion { |x, y| @move_left = x; @move_top = y };
            @img = img
            @text = text
            @wide = options[:width] || 310
            @high = options[:height] || 70+(5*(text.length+text2.length))/10
            @header = options[:hdr] || ""
            @text2 = options[:txt2] || ""

            @img.hover { open_popup  }      
            @img.leave { close_popup }
        end

        def open_popup
             motion { |x, y| @move_left = x; @move_top = y };
            debug ("app width is #{app.width}, move left is #{@move_left}")
            offset = 10
            self.style width: @wide, height: @high
            menu_left = @move_left; menu_top = @move_top;
            @move_left+self.width >= app.width+offset ? menu_left = @move_left-self.width : nil
            @move_top+self.height >= app.height+offset ? menu_top = @move_top-self.height : nil         
            self.move(menu_left, menu_top)
            self.clear do 
                background rgb(120,42,5,0.5), curve: 15
                background rgb(180,150,110,0.7), curve: 15, width: self.width-4, height: self.height-4, left: 2, top: 2
                @header != "" ? ( tagline strong("#{@header}"), stroke: white ) : nil
                @text != "" ?   ( para "#{@text}", justify: true, align: "center", stroke: white, size: 10, width: self.width-2*offset, left: offset ) : nil
                @text2 != "" ?  (para "#{@text2}", justify: true, align: "center", stroke: rgb(50,50,50), size: 10, width: self.width-2*offset, left: offset ) : nil
            end
            timer(0.1) { self.show }        
        end

        def close_popup
            self.hide
        end
    end 

Here it is how you call it

pop_up object, "some text", width: 240, height: 50

Unfortunately if the object is nested in at least one layer of slots this automatically assign that slot as owner of the widget (self is the slot). This is not ok because the popup window is trimmed by the borders of the slot and cannot roam freely.

My question is how to make the whole Canvas always to be the owner of the popup slot nevertheless where the object/image that calls the popup is nested?

The same root issue also causes problems with motion detection of x and y.

passenger94 commented 8 years ago

if i understand correctly, remember a widget is ultimately just a slot, so it's inserted in the context where you initialize it unless you explicitly told it not to do so.

i think you want to initialize your widget at the app level when all other objects are drawn (to make sure it's the last one hence above the others) - maybe create a routine that redraw your widget to be always the last one, if necessary -. At initialisation don't give it specific object nor define hover/leave events. Create a function called, say "plug", give it, as parameter, an object you want to react to said events, (with other needed params) possibly maintain an array of all "plugged" objects, define the hover/leave events for the object

Not tested, not sure, just what's comes to mind ...

dredknight commented 8 years ago

Alright your idea brighten me up for sure :). Here is my tryout I hope I understood you correctly.

Unfortunately it is not working as expected. What I do is the following. No stack/flow defined, I will use the widget slot. initialize is empty. I initialize the widget at the end of the code so it can go on top of everything. Inside the code I use pop_up.set function to define the thingies and move the pop up slot wherever it is required. Unfortunately it is still NOT on top of the other slots and it is trimmed :(.

Here is an example

class PopUp < Shoes::Widget

        def initialize
            motion { |x, y| @move_left = x; @move_top = y }
        end

        def set (img, text, options={} ) 
            @img = img
            @text = text
            @wide = options[:width] || 310
            @high = options[:height] || 70+(5*(text.length+text2.length))/10
            @header = options[:hdr] || ""
            @text2 = options[:txt2] || ""
            @img.hover { open_popup  }      
            @img.leave { close_popup }
        end

        def open_popup
            debug ("app width is #{app.width}, move left is #{@move_left}")
            offset = 10
            self.style width: @wide, height: @high
            menu_left = @move_left; menu_top = @move_top;
            @move_left+self.width >= app.width+offset ? menu_left = @move_left-self.width : nil
            @move_top+self.height >= app.height+offset ? menu_top = @move_top-self.height : nil         
            self.move(menu_left, menu_top)
            self.clear do 
                background rgb(120,42,5,0.5), curve: 15
                background rgb(180,150,110,0.7), curve: 15, width: self.width-4, height: self.height-4, left: 2, top: 2
                @header != "" ? ( tagline strong("#{@header}"), stroke: white ) : nil
                @text != "" ?   ( para "#{@text}", justify: true, align: "center", stroke: white, size: 10, width: self.width-2*offset, left: offset ) : nil
                @text2 != "" ?  (para "#{@text2}", justify: true, align: "center", stroke: rgb(50,50,50), size: 10, width: self.width-2*offset, left: offset ) : nil
            end
            timer(0.1) { self.show }        
        end

        def close_popup
            self.hide
        end
    end 

Shoes.app do 
stack do
      flow width: 40, height: 40 do 
            img1 = image "shoes.png
            pop_up.set (img1, text, options)
      end
end
pop_up    #### here I define it so it can be on top of the slots
end

Unfortunately it is still trimmed. How do I redraw objects so they can remain on top? This is what I want to try next.

passenger94 commented 8 years ago

ooops i forgot ... drawings in Shoes are asynchronous, that essentially means that there is no guarantee about when the drawings are happening, fortunately there is the start event on slots to deal with this. (start event is fired when the slot is drawn) .
so for the root slot, try this

Shoes.app do 
  stack do
      flow width: 40, height: 40 do 
            img1 = image "shoes.png
            pop_up.set (img1, text, options)
      end
  end
  start { pop_up } # or app.slot.start { pop_up } 
end
dredknight commented 8 years ago

start { pop_up } - undefined method is missing. Why is that? everything is defined. I also get this error when calling...

pop_up.set bla bla

...from a method called within another method.

Are there any restrictions when it comes to nesting?

passenger94 commented 8 years ago

i didn't look into your code ... you can't call pop_up.set before creating pop_up widget !!

create all your objects, than in start event callback call pop_up to actually create the widget and assign a variable to it like @menu = pop_up Then call set like @menu.set(img1, text, options)

dredknight commented 8 years ago

Alright! so popup definition should be done once when the whole code is written. Like this..

define pop widget
Shoes.app do
        create the slots on the canvas
        create objects in the slots, all objects that are going to be hovered will be assigned as @hover_box array elements.
        initialize @menu = pop_up wdiget
       Now when the whole app is written launch a cycle and rotate through all the objects and define them hover -> @menu.set @hover_box[@]
end

This will work, though I hoped that one will be able to define the popups inbetween the code like this.

define pop widget
Shoes.app do
         Initialize @menu = pop_up
        create the left slot -> Rotate through 10 elements to create them and while creating them assign popups (@menu.set)
         create the middle slot -> Rotate through 160 elements to create them and while creating them assign popups (@menu.set)
 create the right slot -> create 2 elements and assign popup on the go (@menu.set)
end

May be I am jinxed or something but the first way seems counter-intuitive to me. Unless there is some way to tell a slot " You will always be my favourite. Please stay always on top" I will go with it.

Anyway the good thing is that finally I got your idea ;).

ccoupe commented 8 years ago

Shoes was designed for doing easy things, easily. When you go beyond that simplicity (and you are, as have many before you) then Shoes stops becoming easy and gets demanding that you do things it's way. Can you do it your way? Probably if your willing to exert great effort .Will it be worth fighting Shoes to do it your way? Most people have hit the wall will quit Shoes or abandon their project or they adapt to what work with Shoes can do.

It's a philosophical thing. There's too much low level code inside Shoes to change things now, and too few people maintaining it to for any meaningful change in the design. It's not a good thing or a bad thing. It's just Shoes.

dredknight commented 8 years ago

Good :), I will do it the way meant by the force of shoes then!

Props to @passenger94 for patiently explaining everything that i asked.

dredknight commented 8 years ago

Hey guys here are two versions you can consider PoC.

Version A: Uses an external menu_slot for the actual popup. The restrictions are that the widget should use a slot which is defined last, so it can appear on top of other slots. This means that the widget will be initialized last in the code just after the menu_slot is created.

class PopUp < Shoes::Widget

    def initialize ( menu )
        motion { |x, y| @move_left = x; @move_top = y }
        @menu = menu
    end

    def resize (wide, high, length )
        while (((high*wide/20)/length) < 8 or wide/high > 6) do
            case wide/high
                when 0..6 then high+=20
                else high+=20; wide-=wide/2
            end
            wide < 250 ? wide = 250 : nil
        end 
        return wide, high + 10
    end

    def present ( text, size, font = "", stroke, just, offset, wide, high)
        unless text == "" then 
            flow margin_left: offset, width: wide, height: high do
                para ("#{text}"), size: size, stroke: stroke, font: font, align: "center", justify: just
            end
        end
    end

    def open ( text, options={} ) 
        offset = 10
        text2 = options[:text2] || ""
        header = options[:header] || ""         
        wide = options[:width] || text.length*8
        t_high = 20
        (options[:width].is_a? Integer) == false ? (wide, t_high = resize wide, t_high, text.length) : nil
        text2 == "" ? t2_high = 0 : t2_high = 8*20*text2.length/wide + 20
        header != "" ? h_high = 60 : h_high = 0
        high = options[:height] || t_high + h_high + t2_high            
        @menu.style width: offset + wide, height: high + offset
        menu_left = @move_left; menu_top = @move_top;
        @move_left + @menu.width >= app.width + offset ? menu_left = @move_left - @menu.width : nil
        @move_top + @menu.height >= app.height + offset ? menu_top = @move_top - 2*@menu.height/3 : nil         
        @menu.move(menu_left, menu_top)
        @menu.clear do 
            background rgb(120,42,5,0.5), curve: 15
            background rgb(180,150,110,0.7), curve: 15, width: @menu.width-4, height: @menu.height - 4, left: 2, top: 2
            present  header, 17, "Bell MT", white, false, offset, wide, h_high
            present text, 9, "Courier New", white, true, offset/2, wide, t_high
            present text2, 9, "Courier New", rgb(50,50,50), true, offset/2, wide, t2_high
        end
        timer(0.1) { @menu.show }       
    end

    def close
        @menu.hide
    end
end

Shoes.app do

    flow height: 100, width: 100 do
        border("rgb(105, 105, 105)", strokewidth: 1)
        image "shoes-icon.png", left: 1, top: 1, width: 50, height: 50
        contents[1].hover { @pops.open  "morning, sunshine, cofee, beans, qeqwe ,asdasds ,wqeqwewqe 22222222222 222222222222222 2222222222222 2222 22222222222222222" }
        contents[1].leave { @pops.close } 

    end
    flow height: 100, width: 100 do
        border("rgb(105, 105, 105)", strokewidth: 1)
        image "shoes-icon.png", left: 1, top: 1, width: 50, height: 50
        contents[1].hover { @pops.open  "morning, sunshine, cofee, beans, qeqwe ,asdasds ,wqeqwewqe", text2: "Lets get it on" }
        contents[1].leave { @pops.close }
    end
    flow height: 100, width: 100 do
        border("rgb(105, 105, 105)", strokewidth: 1)
        image "shoes-icon.png", left: 1, top: 1, width: 50, height: 50
        contents[1].hover { @pops.open  "morning, sunshine, cofee, beans, qeqwe ,asdasds ,wqeqwewqe", text2: "Letsad  dad getsdaf it on1231d", header: "Header!" }
        contents[1].leave { @pops.close }
    end

    @menu = stack height: 10 do end.hide
    @pops = pop_up ( @menu )
end

Version B: I found a way to override shoes order of elements by using timer. When the widget is initialized its wait X seconds so the whole code is executed and then it creates the menu slot. This looks far better than the one above as it can be initialized everywhere! The only problem is that we dont know if the code will manage to be executed for X seconds so timer is not very reliable if the code one is using is very complicated. Also multiple timers in the code will make this completely useless. Also the first X seconds the popup is not available for obvious reasons.

I also tried creating a slot on the canvas like this:

app.slot.elements[99] = stack do end.hide

The idea is to create the last element so it is always on top. Unfortunately Shoes does not like this code :(.

Anyway here is a sample code of version B.

class PopUp < Shoes::Widget

    def initialize ()
        motion { |x, y| @move_left = x; @move_top = y }
        timer(2) { @menu = stack height: 0 do end.hide }
    end

    def resize (wide, high, length )
        while (((high*wide/20)/length) < 8 or wide/high > 6) do
            case wide/high
                when 0..6 then high+=20
                else high+=20; wide-=wide/2
            end
            wide < 250 ? wide = 250 : nil
        end 
        return wide, high + 10
    end

    def present ( text, size, font = "", stroke, just, offset, wide, high)
        unless text == "" then 
            flow margin_left: offset, width: wide, height: high do
                para ("#{text}"), size: size, stroke: stroke, font: font, align: "center", justify: just
            end
        end
    end

    def open ( text, options={} ) 
        offset = 10
        text2 = options[:text2] || ""
        header = options[:header] || ""         
        wide = options[:width] || text.length*8
        t_high = 20
        (options[:width].is_a? Integer) == false ? (wide, t_high = resize wide, t_high, text.length) : nil
        text2 == "" ? t2_high = 0 : t2_high = 8*20*text2.length/wide + 20
        header != "" ? h_high = 60 : h_high = 0
        high = options[:height] || t_high + h_high + t2_high            
        @menu.style width: offset + wide, height: high + offset
        menu_left = @move_left; menu_top = @move_top;
        @move_left + @menu.width >= app.width + offset ? menu_left = @move_left - @menu.width : nil
        @move_top + @menu.height >= app.height + offset ? menu_top = @move_top - 2*@menu.height/3 : nil         
        @menu.move(menu_left, menu_top)
        @menu.clear do 
            background rgb(120,42,5,0.5), curve: 15
            background rgb(180,150,110,0.7), curve: 15, width: @menu.width-4, height: @menu.height - 4, left: 2, top: 2
            present  header, 17, "Bell MT", white, false, offset, wide, h_high
            present text, 9, "Courier New", white, true, offset/2, wide, t_high
            present text2, 9, "Courier New", rgb(50,50,50), true, offset/2, wide, t2_high
        end
        timer(0.1) { @menu.show }       
    end

    def close
        @menu.hide
    end
end

Shoes.app do

    @pops = pop_up()
    flow height: 100, width: 100 do
        border("rgb(105, 105, 105)", strokewidth: 1)
        image "shoes-icon.png", left: 1, top: 1, width: 50, height: 50
        contents[1].hover { @pops.open  "morning, sunshine, cofee, beans, qeqwe ,asdasds ,wqeqwewqe 22222222222 222222222222222 2222222222222 2222 22222222222222222" }
        contents[1].leave { @pops.close } 

    end
    flow height: 100, width: 100 do
        border("rgb(105, 105, 105)", strokewidth: 1)
        image "shoes-icon.png", left: 1, top: 1, width: 50, height: 50
        contents[1].hover { @pops.open  "morning, sunshine, cofee, beans, qeqwe ,asdasds ,wqeqwewqe", text2: "Lets get it on" }
        contents[1].leave { @pops.close }
    end
    flow height: 100, width: 100 do
        border("rgb(105, 105, 105)", strokewidth: 1)
        image "shoes-icon.png", left: 1, top: 1, width: 50, height: 50
        contents[1].hover { @pops.open  "morning, sunshine, cofee, beans, qeqwe ,asdasds ,wqeqwewqe", text2: "Letsad  dad getsdaf it on1231d", header: "Header!" }
        contents[1].leave { @pops.close }
    end
end

How the Widget works.

User is obliged to enter 1 variable of type string only.

@pops.open "some text"

User can add second descriptive text if he wishes. Text is below the first one and in different colour.

@pops.open "some text",  text2: "descriptive text" 

A header can be added that appears on top.

@pops.open "some text",  text2: "descriptive text" , header: "Heya"

The widget resizes the menu based on the size of the text. This is valid for all 3 boxes - text, text2 and header!

if you dont like how the widget resizes the box you can override it by entering own values like this.

@pops.open "some text",  text2: "descriptive text" , header: "Heya", width: 300, height: 400
passenger94 commented 8 years ago

@dredknight, did you tried the start event ? It's exactly doing internally what you are trying to do on B but in the flow of Shoes events, so it waits the necessary time for elements to be ready !

dredknight commented 8 years ago

@passenger94 exchanged timer with start . Works wonderfully!!! Thank you!

So this is what start do? Waits for everything and then gets executed.

Is it possible to put a couple of starts in the code? if you do that in what order are they executed?

p.s. The following things break the dynamic arrangement of the slot.

  1. The code is not aware of new lines. Each new line is counted as 1 char. if you use new lines the slot will be shorter than what you need.
  2. long words. If you have really long word the widget cannot divide in 2 rows. There is an option to divide the strings by char but it does not look good so I decided to leave it that way.

Any ideas are welcome :).