Merge lp:~samuel-buffet/entertainer/new-scrollmenu into lp:entertainer
- new-scrollmenu
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Paul Hummer |
Approved revision: | 365 |
Merged at revision: | not available |
Proposed branch: | lp:~samuel-buffet/entertainer/new-scrollmenu |
Merge into: | lp:entertainer |
Diff against target: | None lines |
To merge this branch: | bzr merge lp:~samuel-buffet/entertainer/new-scrollmenu |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Hummer | Approve | ||
Matt Layman | Approve | ||
Jamie Bennett | Pending | ||
Review via email: mp+5342@code.launchpad.net |
Commit message
The ScrollMenu is now reactive to pointer events.
Description of the change
Samuel Buffet (samuel-buffet) wrote : | # |
Samuel Buffet (samuel-buffet) wrote : | # |
As I've done some small changes since the proposal I post here the new diff.
=== modified file 'entertainerlib
--- entertainerlib/
+++ entertainerlib/
@@ -44,65 +44,88 @@
+ self.preview.
- self.active_menu = "main"
- self.rss_
-
- self.menu = None
- self.playing_item = None
- self.create_
-
- clock = ClockLabel(0.13, "screentitle", 0, 0.87)
- self.add(clock)
-
- def create_
+ self.rss_
+ self.preview.
+
+ self._create_
+ self.menu.active = True
+
+ self.add(
+
+ def get_type(self):
+ """Return screen type."""
+ return Screen.NORMAL
+
+ def get_name(self):
+ """Return screen name (human readble)"""
+ return "Main"
+
+ def _create_
"""Create main menu of the home screen"""
- self.menu = ScrollMenu(10, 60, self.config.
+ self.menu = ScrollMenu(10, 60, 0.045, "menuitem_active")
-
- # Common values for all ScrollMenu items
- size = 0.045
- color = "menuitem_active"
-
- item0 = Label(size, color, 0, 0, _("Play CD"), "disc")
- self.menu.
-
- item2 = Label(size, color, 0, 0, _("Videos"), "videos")
- self.menu.
-
- item3 = Label(size, color, 0, 0, _("Music"), "music")
- self.menu.
-
- item4 = Label(size, color, 0, 0, _("Photographs"), "photo")
- self.menu.
-
- item5 = Label(size, color, 0, 0, _("Headlines"), "rss")
- self.menu.
+ self.menu.
+ self.menu.
+ self.menu.
+
+ self.menu.
+ self.menu.
+ self.menu.
+ self.menu.
+ self.menu.
if self.config.
- item6 = Label(size, color, 0, 0, _("Weather"), "weather")
- self.menu.
+ self.menu.
if self.config.
- item7 = Label(size, color, 0, 0, _("Eject CD"), "eject_cd")
- self.menu.
+ self.menu.
if self.media_
- playing = Label(size, color, 0, 0, _("Playing now..."), "playing")
- ...
Matt Layman (mblayman) wrote : | # |
Samuel,
I'm finally getting a chance to look at some of this code. I'm going to do this review in a couple of stages because of some functional regressions that I found while doing user testing. First, I'll let you know what those problems are so that you'll get a chance to fix them, then I'll come back and review the new widget code in more detail.
But let me start by saying this is really cool stuff. Sometimes it is easy to be critical and just point out faults in code, but I just want to let you know in advance that I think this work is pretty awesome. I'm sure you put quite a bit of effort into this code, and it shows.
Okay, now for the comments:
1) The scroll menu no longer remembers its last item when you return to a screen. Every time I hit back, it started on Music again.
2) The major problem that I have so far is that the keyboard interface is mostly broken if you return to the main screen after being on a different screen. For some reason, the menu doesn't think that it is active so it won't react until the user presses the right arrow key, then it becomes active.
Now for the minor comments (keep in mind that in this stage of the review, I've only looked at the Main screen and UserInterface files):
3) I'm not a fan of variable abbreviations most of the time. I noticed that you have the menu property "visible_nb" and I understand what it is supposed to mean, but since ScrollMenu uses the term "item," I think that "visible_items" would be a much clearer property name.
4) You created some new methods in Main and UserInterface (which is fine, of course), but I don't think that they match the conventions that we use for "private" methods. Since the methods you made aren't meant to be used outside the class, I would recommend that you prepend an underscore character to make that clear (e.g., preview_activation becomes _preview_
As soon as these regressions are addressed, I'll come back and look specifically at the underlying widget code.
Thanks.
Samuel Buffet (samuel-buffet) wrote : | # |
Hi Matt,
Thanks a lot for your review. I've fixed the things you've already noticed.
> 1) The scroll menu no longer remembers its last item when you return to a
> screen. Every time I hit back, it started on Music again.
This is fixed. I've also replace "..has_media()" by "..is_playing" because I thought it was closer to what we want in the preview. This will allow us in a close future to connect "play" "stop" events of the media_player to the refresh method so we'll have a dynamic update of it. (those events are created in *reactive_
> 2) The major problem that I have so far is that the keyboard interface is
> mostly broken if you return to the main screen after being on a different
> screen. For some reason, the menu doesn't think that it is active so it won't
> react until the user presses the right arrow key, then it becomes active.
Yep didn't notice that. It's fixed now.
> 3) I'm not a fan of variable abbreviations most of the time. I noticed that
> you have the menu property "visible_nb" and I understand what it is supposed
> to mean, but since ScrollMenu uses the term "item," I think that
> "visible_items" would be a much clearer property name.
>
> 4) You created some new methods in Main and UserInterface (which is fine, of
> course), but I don't think that they match the conventions that we use for
> "private" methods. Since the methods you made aren't meant to be used outside
> the class, I would recommend that you prepend an underscore character to make
> that clear (e.g., preview_activation becomes _preview_
> directly stated in PEP8, but there is some discussion about having private
> portions start with _.
Yes, that's right. I've renamed those private methods.
Thanks again Matt
Samuel-
new diff :
=== modified file 'entertainerlib
--- entertainerlib/
+++ entertainerlib/
@@ -44,65 +44,89 @@
+ self.preview.
- self.active_menu = "main"
- self.rss_
-
- self.menu = None
- self.playing_item = None
- self.create_
-
- clock = ClockLabel(0.13, "screentitle", 0, 0.87)
- self.add(clock)
-
- def create_
+ self.rss_
+ self.preview.
+
+ self._create_
+ self.menu.active = True
+
+ self.add(
+
+ def get_type(self):
+ """Return screen type."""
+ return Screen.NORMAL
+
+ def get_name(self):
+ """Return screen name (human readble)"""
+ return "Main"
+
+ def _create_
"""Create main menu of the home screen"""
- self.menu = ScrollMenu(10, 60, self.config.
+ ...
Samuel Buffet (samuel-buffet) wrote : | # |
Some details about the LoopedPathBehaviour Class and the way it's used in the ScrollMenu.
First, you have to know that this class is inspired by ClutterBehaviou
With this standard Clutter behaviour, we can move actors along an Ellipse. Actors's position on this Ellipse are defined by angles, and a motion can be described by a angle_start and a angle_end. The angle can be seen as a parameter describing an actor's position along the ellipse.
The LoopedPathBehaviour can be seen as an extension of the regular PathBehaviour but where we specify a path (here only a line in fact => only 2 knots allowed atm) AND a couple start_index/
LoopedPathBehaviour allows us to specify a start_index and a end_index corresponding to a starting point and a ending point on the segment defined by the 2 knots.
This ability to specify a start_index and a end_index is used in the ScrollMenu that way :
* we create a LoopedPathBehaviour for every menu item and apply it to this item
* in the _update_behaviours we set the start knot and the end knot. Those knots (so the path) are the same for every menu item. And then for each item we specify a start_index and a end_index which is a function of the position of the item in the item list AND the position of the *selected item* (meaning the item which is in the middle of the visible are, the clipped area of the widget).
Trust this helps,
Samuel-
Note: I know this one is not totally obvious. It's used in a much simpler way on the ScrollArea widget because there it's applied to only one widget, the moving clutter group including the clutter Label.
On the _update_behaviours method, we
Matt Layman (mblayman) wrote : | # |
Samuel,
These are the comments that I had written down before you last push. I still haven't looked at MotionBuffer and I need to give all the files a second look through, but I think it would be a good start to address some of these things. I hope you find them helpful. Most of the comments are just an attempt to keep a unified style in the codebase. The comments might seem a little rough because they are basically just my notes. I hope that they're readable enough.
main.py:
* _create_main_menu should return the menu. And the main screen should add that menu to itself. This will ensure that the init is the place that self.menu is created. It would also allow testing of _create_main_menu in the future (if we ever figure out what we would need to test). I did this for _create_
* docstrings need improvement for _on_menu_moved and _on_menu_selected. Don't use "We" and try to reduce to one line if you can.
* hide_cursor_
scroll_menu.py:
* import order is wrong because math is a core library.
* docstring doesn't need "This is" and might fit on one line
* What is __gtype_name__ for?
* Could clean up docstrings for get_active, set_active, get_selected (no need for @return and @param).
* idx, xx, and yy aren't very intuitive variable names in _display_
* In _set_selected_
* I'd suggest you remove the @author tag in get_index and just add Joshua Scotton as an author to the top of the file. I don't think we need to identify authors at the method level (that's what bzr annotate is for), but he still has the right to credit himself with something if that is what he wrote.
special_
* docstring is wrong at top of file.
* import order is wrong. math is a core lib and clutter is not so math should be first and clutter should be below it separated by a newline.
* No need for the space between class name and parenthetical superclass.
* "custome" is actually spelled custom
* do_alpha_notify has a space before its parameter list
* Don't need "We" in the docstring for do_alpha_notify
* what are idx and idxx?
* Does path_length need to return an integer? (because it says length in pixels)
test_scrollmenu.py:
* has an empty line with spaces.
* testCreate should use a PEP8 method name. test_create (We use camel case for setUp and tearDown because that's what unittest does. Any existing test that we have that uses camel case is just because it's from a time before started being fully PEP8 compliant.)
* test ScrollMenu instead of object
Samuel Buffet (samuel-buffet) wrote : | # |
Hi Matt,
First, I'd like to thank you for all the efforts you put in this review. This really helps and I deeply appreciate.
In my last push, I've fixed all what you've already notice and more in the same spirit (mainly docstrings).
> a unified style in the codebase. The comments might seem a little rough
> because they are basically just my notes. I hope that they're readable enough.
No pb here, there are perfectly readable and not rough at all (I mean things have to be said)
> test_scrollmenu.py:
> * has an empty line with spaces.
> * testCreate should use a PEP8 method name. test_create (We use camel case
> for setUp and tearDown because that's what unittest does. Any existing test
> that we have that uses camel case is just because it's from a time before
Hmm, Matt I don't know what *camel* is about
Now, the good point is that after this review, everything will look more or less relax and easy ;)
Samuel-
Matt Layman (mblayman) wrote : | # |
Samuel,
It seems like you have been very busy making modifications to this branch. Now that thing have stabilized a bit, let me throw another few comments at you. After this round of comments, I really only have to review the MotionBuffer class (I don't the time to do that tonight). Again, thanks for the good work.
main.py:
* _move_menu is now a dead unused method that can be removed.
scroll_menu.py:
* _on_release_event has more idx variables
* line 301 could use some spacing around the less than character for clarity
* _on_motion_event: why is it showing the cursor? That is done by the UserInterface.
* _on_scroll_event: why is it showing the cursor? That is done by the UserInterface.
Samuel Buffet (samuel-buffet) wrote : | # |
Hi Matt,
> Samuel,
>
> It seems like you have been very busy making modifications to this branch. Now
> that thing have stabilized a bit, let me throw another few comments at you.
> After this round of comments, I really only have to review the MotionBuffer
> class (I don't the time to do that tonight). Again, thanks for the good work.
>
> main.py:
> * _move_menu is now a dead unused method that can be removed.
Indeed, I've removed it.
> scroll_menu.py:
> * _on_release_event has more idx variables
> * line 301 could use some spacing around the less than character for clarity
Fixed.
> * _on_motion_event: why is it showing the cursor? That is done by the
> UserInterface.
> * _on_scroll_event: why is it showing the cursor? That is done by the
> UserInterface.
Yeah that's done by the UserInterface but if for instance you place your cursor on the menu and scroll it (no motion only scroll) then after 4 seconds the cursor will be hidden without the cursor.show in _on_scroll_event because there's no motion event grabbed by the stage. Just try to comment that line out, it looks strange from the user experience point of view (I think).
Same kind of thing with _on_motion_event if you want to click on the menu and, keeping the pressure on the button you play with it up and down. It will hide the cursor after 4 seconds but here the answer to "why?" is more subtle. Because here we have motion-events but they are grabbed only by the menu because of the clutter.
So until the button is released the stage is blind. It see no event at all so the timeout is not reset and hides the cursor after 4 seconds.
Now, after having explain that, maybe I should explain why I've done this grab/ungrab stuff. Simply it's because without clutter.
I've tried another way to detect the button's release using the ClutterModifierType field of a motion event which is supposed to provide the state of button (if I understand correctly). But unfortunately, it looks like it doesn't work (maybe after more tests I'll file a bug upstream).
Cheers,
Samuel-
Matt Layman (mblayman) wrote : | # |
Okay, I finally had a good chance to sit down and look at MotionBuffer so without further adieu:
motion_buffer.py:
This is my understanding of MotionBuffer: A widget will have a private instance of MotionBuffer. When an event occurs, MotionBuffer will have some methods called to do some calculations. Once the calculations are done, the widget will access some properties of the buffer to determine what kind of motion happened and how to handle that motion. If this understanding is incorrect, please feel free to point out other details.
Comments:
* I think that the class should be a new style class (inherit from object)
* I'm not a big fan of the class doc string. I know Paul has recommended to me in the past to stay away from "This class..." wording. Additionally, the description is pretty vague. What are "some calculations"? It's not a bad thing to have a longer docstring, especially if the class is more abstract like this one is.
* init could use a comment describing what "ema" is.
* compute methods docstrings are sentences so they each need a period. And it should be "deltas and speeds".
* scroll menu is using instance variables of motion buffer without using property methods. I know that there are a lot of instance variables here. Would it make sense to turn these into property methods?
* Speaking of lots of instance variables, there seems to be a natural grouping of these variables. Would it help the class if those variable were grouped into their own class in a generic way. Maybe it would be a Moment class or something, and MotionBuffer would have three different instance of moments for start, last_motion_event, and ema. Then you could have a common compute method that would eliminate some duplicated code. I'm just trying to brainstorm here because I feel like I'm getting lost in a sea of instance variables. From the design perspective, this code seems to have a "smell" because of the long variable names. When you have a variable named "dy_from_
Paul Hummer (rockstar) wrote : | # |
Samuel-
This is a very large patch, which makes it difficult to truly review well.
I've asked lots of questions, and hope that through those questions, you might
be able to evaluate the implementation from another viewpoint, and discover
issues on your own. I'm not going to vote just yet, as I'm curious to hear
your response to my review.
> === modified file 'entertainerlib
> --- entertainerlib/
> +++ entertainerlib/
> @@ -47,16 +47,55 @@ class Main(Screen):
> self.preview.
> self.add(
>
> - self.active_menu = "main"
> self.rss_
> self.preview.
>
> - self.menu = None
> - self.playing_item = None
> - self.create_
> + self.menu = self._create_
> + self.add(self.menu)
> + self.menu.
> + self.menu.
> + self.menu.
> + self.menu.active = True
> +
> + self.add(
> +
This makes me a bit queesy. Why are you creating an instance variable, and
then calling an instance method on that instance variable. What does self.add
do? Why are we calling it? If it is required (and I'm concerned it isn't),
self._create_
should be doing all the self.menu.connect type stuff.
Also, I see the clock got moved here instead of the _create_main_menu code.
Why?
> + def get_type(self):
> + """Return screen type."""
> + return Screen.NORMAL
> +
> + def get_name(self):
> + """Return screen name (human readble)"""
> + return "Main"
> +
I've seen this convention a few times. What are we using get_name for? Is
there a better way to handle this than using a string? I'd like some answers
about this, because I thought Joshua had eliminated the need for this code.
Also, it seems that type and name would be better as properties. They never
change dynamically, so making them instance properties is a good idea. If type
or name had the ability to change within the life of the instance, we would use
accessors, but that's more for convention than anything. In this case though,
there isn't even a need for defining a function. This code could be simplified
to:
type = Screen.NORMAL
name = "Main"
> @@ -206,42 +200,23 @@ class Main(Screen):
>
> return preview
>
> - def get_type(self):
> - """Return screen type."""
> - return Screen.NORMAL
> -
> - def get_name(self):
> - """Return screen name (human readble)"""
> - return "Main"
> -
> - def update(self):
> - """
> - Update screen widgets. This is called always when screen is poped from
> - the screen history. Updates main menu widget.
> - """
> - selected_name = self.menu.
...
Samuel Buffet (samuel-buffet) wrote : | # |
Matt,
> motion_buffer.py:
>
> This is my understanding of MotionBuffer: A widget will have a private
> instance of MotionBuffer. When an event occurs, MotionBuffer will have some
> methods called to do some calculations. Once the calculations are done, the
> widget will access some properties of the buffer to determine what kind of
> motion happened and how to handle that motion. If this understanding is
> incorrect, please feel free to point out other details.
Yes this is precisely what this Class does. It's a tool Class added to widgets which need to computes cursor's motion characteristics (speeds / distances) in order to do a kinetic effect.
> Comments:
> * I think that the class should be a new style class (inherit from object)
Fixed.
> * I'm not a big fan of the class doc string. I know Paul has recommended to
> me in the past to stay away from "This class..." wording. Additionally, the
> description is pretty vague. What are "some calculations"? It's not a bad
> thing to have a longer docstring, especially if the class is more abstract
> like this one is.
> * init could use a comment describing what "ema" is.
Fixed. Yeah, that was a bit short. More detailed now.
> * compute methods docstrings are sentences so they each need a period. And it
> should be "deltas and speeds".
Fixed
> * scroll menu is using instance variables of motion buffer without using
> property methods. I know that there are a lot of instance variables here.
> Would it make sense to turn these into property methods?
Well, this is a point but I don't see the benefit.
I can understand:
@property
def foo(self):
do_something
return value
Because there we wrap a treatment before returning the value.
also I understand:
def _get_myproperty
return value
def _set_myproperty
self._value = value
do_something
myproperty = property(
Because there we wrap a treatment setting the value.
But I don't see the added value of
@property
def foo(self):
return value
In this case I think (I may be wrong) that it's cleaner, simpler and correct to access directly the variable itself.
> * Speaking of lots of instance variables, there seems to be a natural
> grouping of these variables. Would it help the class if those variable were
> grouped into their own class in a generic way. Maybe it would be a Moment
> class or something, and MotionBuffer would have three different instance of
> moments for start, last_motion_event, and ema. Then you could have a common
> compute method that would eliminate some duplicated code. I'm just trying to
> brainstorm here because I feel like I'm getting lost in a sea of instance
> variables. From the design perspective, this code seems to have a "smell"
> because of the long variable names. When you have a variable named
> "dy_from_
> to create some important abstract concept.
Well, I don't see the need to add complexity there.
I've pushed the fixes mentioned *Fixed* above.
Thanks a lot Matt.
Samuel-
Matt Layman (mblayman) wrote : | # |
Paul,
I'd like to add some comments of my own to what you've said. I think I can clear up some points about clutter since you haven't been doing much work in the frontend. I also have some general remarks based on what you've stated.
> > === modified file 'entertainerlib
> > --- entertainerlib/
> +0000
> > +++ entertainerlib/
> +0000
> > @@ -47,16 +47,55 @@ class Main(Screen):
> > self.preview.
> > self.add(
> >
> > - self.active_menu = "main"
> > self.rss_
> > self.preview.
> >
> > - self.menu = None
> > - self.playing_item = None
> > - self.create_
> > + self.menu = self._create_
> > + self.add(self.menu)
> > + self.menu.
> > + self.menu.
> > + self.menu.
> > + self.menu.active = True
> > +
> > + self.add(
> > +
>
> This makes me a bit queesy. Why are you creating an instance variable, and
> then calling an instance method on that instance variable. What does self.add
> do? Why are we calling it? If it is required (and I'm concerned it isn't),
> self._create_
> should be doing all the self.menu.connect type stuff.
I hadn't really thought about it, but I think you're correct, the connect method calls and active setting could move into _create_main_menu.
self.add() is a clutter.Group method. What's it's doing is adding the self.menu clutter.Group to the Main screen (which itself is a clutter.Group). I had suggested to Samuel that self.add does not belong in create_main_menu because it should be clear that it is the init's responsibility to add new groups to itself. That's my opinion because I've often asked myself "where the hell is the widget being added?" and if it was all the init, we wouldn't have to hunt in other methods for when things show up on the screen. Basically, it was an experience judgment call from working with some of our clutter code.
> Also, I see the clock got moved here instead of the _create_main_menu code.
> Why?
main_menu is really just an initialized version of a ScrollMenu widget. The group doesn't represent everything that could show up on the Main screen. I think declaring the ClockLabel in Main's init makes sense.
> > + def get_type(self):
> > + """Return screen type."""
> > + return Screen.NORMAL
> > +
> > + def get_name(self):
> > + """Return screen name (human readble)"""
> > + return "Main"
> > +
>
> I've seen this convention a few times. What are we using get_name for? Is
> there a better way to handle this than using a string? I'd like some answers
> about this, because I thought Joshua had eliminated the need for this code.
>
> Also, it seems that type and name ...
Samuel Buffet (samuel-buffet) wrote : | # |
Hi Paul,
> > === modified file 'entertainerlib
> > --- entertainerlib/
> +0000
> > +++ entertainerlib/
> +0000
> > @@ -47,16 +47,55 @@ class Main(Screen):
> > self.preview.
> > self.add(
> >
> > - self.active_menu = "main"
> > self.rss_
> > self.preview.
> >
> > - self.menu = None
> > - self.playing_item = None
> > - self.create_
> > + self.menu = self._create_
> > + self.add(self.menu)
> > + self.menu.
> > + self.menu.
> > + self.menu.
> > + self.menu.active = True
> > +
> > + self.add(
> > +
>
> This makes me a bit queesy. Why are you creating an instance variable, and
> then calling an instance method on that instance variable. What does self.add
> do? Why are we calling it? If it is required (and I'm concerned it isn't),
> self._create_
> should be doing all the self.menu.connect type stuff.
Well, this was discussed with Matt in a previous comment. The idea is to add widgets only in the __init__ method of a screen. I really don't know at the moment the *right* place of the connect calls. It's more tricky than it seems on the first looks. Because every actor has to be created before. Why? simply because if we create a widget (let's say a menu) and connect its *activated* signal to a *menu_activate* callback. In this call back we will deactivate (in the future when reactive widgets will be all together) the other reactive widgets of the screen. So if there not created ... error
That is why I think the good place for connections is in the end of the __init__ method. I'll take care of that (including other optimizations and simplifications) in the *reactive_global* branch I'll do in the end.
> Also, I see the clock got moved here instead of the _create_main_menu code.
> Why?
Yep, see above.
>
> > + def get_type(self):
> > + """Return screen type."""
> > + return Screen.NORMAL
> > +
> > + def get_name(self):
> > + """Return screen name (human readble)"""
> > + return "Main"
> > +
>
> I've seen this convention a few times. What are we using get_name for? Is
> there a better way to handle this than using a string? I'd like some answers
> about this, because I thought Joshua had eliminated the need for this code.
>
> Also, it seems that type and name would be better as properties. They never
> change dynamically, so making them instance properties is a good idea. If
> type
> or name had the ability to change within the life of the instance, we would
> use
> accessors, but that's more for convention than anything. In this case though,
> there isn't even a need for defining a function. This code ...
Matt Layman (mblayman) wrote : | # |
Samuel, the only thing keeping me from approving this branch is a lack of some tests. As Paul suggested, you could add some tests to test the state of some of the instance variables. It may require creating a mock event that you can feed to the ScrollMenu to probe things (rather than connecting to any handler methods).
I would suggest something like this:
1) You could add assertEqual statements to the create test so that you verify that your instance variables that get parameter values actually equal those values
2) There aren't any tests for MotionBuffer yet and I think it is important to add some for that class (because I think MotionBuffer is going to affect the behavior of most of our widgets so it needs some coverage).
* You could do a create test and check that instance variable are the values you expect them to be.
* You could pass in a mock event, compute the changes, and then make sure that those values are what you would expect. One way to do this would be to set up the test in a way that you know it will fail, then when you get the correct value from the failure, replace your faulty value with the value from the failure. You might ask "what is the value for this kind of test?" My answer is that it helps you capture the current behavior. If you think that MotionBuffer is acting the way you expect it to, then you want to preserve that. If you add the kind of test that I just described, you'll be able to find out when someone changes the behavior of MotionBuffer. In the future, you might decide that it is okay or you can decide that the change in behavior is wrong.
E.g.,
self.motion_
self.assertEqua
I'm hoping this will fail :). Once it fails, replace 'boogers' with the value that you received. You may have to use assertAlmostEqual for some of the math instance variables.
Anyway, those are just my thoughts about what you could do to get some test coverage on this widgets. Trust me, I feel your pain when it comes to testing widgets. It's a big pain in the rear.
Samuel Buffet (samuel-buffet) wrote : | # |
Matt, Paul,
Find bellow the diff of my last 3 commits :
I've made the diff between a local copy and the one pushed on lp:
So everything starting with + has been removed and everything starting with - has been added.
I've done a bit of clean up in the MotionBuffer Class and added more tests.
Samuel-
=== modified file 'entertainerlib
--- entertainerlib/
+++ entertainerlib/
@@ -22,6 +22,9 @@
+ self.speed_
+ self.speed_
+ self.speed_
@@ -84,6 +87,18 @@
+ if self.dt_from_start != 0 :
+ self.speed_
+ float(self.
+ self.speed_
+ float(self.
+ self.speed_
+ float(self.
+ else:
+ self.speed_
+ self.speed_
+ self.speed_
+
def compute_
'''Compute deltas and speeds from the last motion-event.'''
=== modified file 'entertainerlib
--- entertainerlib/
+++ entertainerlib/
@@ -117,10 +117,6 @@
active = property(
- def stop_animation(
- '''Stops the timeline driving menu animation.'''
- self._timeline.
-
def _update_
items_len = len(self._items)
=== modified file 'entertainerlib
--- entertainerlib/
+++ entertainerlib/
@@ -304,12 +304,3 @@
'''See `VideoLibrary.
return 0
-
-class MockPointerEven
- '''Mock a pointer event like those generated by Clutter'''
-
- def __init__(self):
- self.x = 0
- self.y = 0
- self.time = 0
-
=== removed file 'entertainerlib
--- entertainerlib/
+++ entertainerlib/
@@ -1,83 +0,0 @@
-"""Tests MotionBuffer"""
-
-__license__ = "GPLv2"
-__copyright__ = "2009, Samuel Buffet"
-__author__ = "Samuel Buffet <email address hidden>"
-
-from entertainerlib.
Matt Layman (mblayman) wrote : | # |
Samuel, I'll be able to look over your latest changes and tests when I get home from work today. Barring any obvious problems, I'll likely approve the proposal today. If you could set the commit message (it's an option in the proposal details), then the approval and commit message should be indicators to tarmac to commit the work to the trunk.
I think this is how we will be committing stuff to the trunk from here on out, but I need to document that on the mailing list and work with Paul to make sure that Tarmac is ready to handle that.
Matt Layman (mblayman) wrote : | # |
What a long review.... Samuel, I checked out your tests and they seem reasonable to me. Also, make test and lint passes (to the level I would expect, there are errors but they are unrelated to this code). There could probably be more tests, but I guess there could always be more tests. :)
I am satisfied enough to have this code committed to the trunk. This is a great start toward reactive widgets and I'm hoping that future ones will be easier to review now that we've covered the groundwork. I think that this branch is one step closer to making a lot of people happy who will eventually want touch screen support for Entertainer.
Nice work.
Samuel Buffet (samuel-buffet) wrote : | # |
> What a long review....
And it will get a bit longer because it's important for me to have Paul's vote on it ;)
Et has commented it and wrote that I'll give his vote later so it means that he has an opinion, and I'd like to have it. Paul ?
> Nice work.
Thanks Matt,
Samuel-
Matt Layman (mblayman) wrote : | # |
Sorry, I didn't realize Paul actually wanted to cast a vote on it for this branch.
- 364. By Samuel Buffet
-
merged with trunk 367
- 365. By Samuel Buffet
-
pylint: disable-msg=W0702 added to fix lint after last reintroduction of a try/except without particular exception catched.
Paul Hummer (rockstar) wrote : | # |
You probably don't need to block on my comments. I can make code comments, but Matt really knows the UI code a lot better than I do. Even if this branch isn't ready for production, we have a little over a month to test it out and make sure that we can actually get things ironed out before release. Since this is a core part of the next release, I think even early code is better than no code.
Preview Diff
1 | === modified file 'entertainerlib/frontend/gui/screens/main.py' |
2 | --- entertainerlib/frontend/gui/screens/main.py 2009-03-02 03:11:24 +0000 |
3 | +++ entertainerlib/frontend/gui/screens/main.py 2009-04-08 11:23:26 +0000 |
4 | @@ -44,65 +44,89 @@ |
5 | self.preview = clutter.Group() # Group that contains preview actors |
6 | self.preview.set_position(self.get_abs_x(0.07), self.get_abs_y(0.1)) |
7 | self.preview.show() |
8 | + self.preview.set_opacity(0x00) |
9 | self.add(self.preview) |
10 | |
11 | - self.active_menu = "main" |
12 | - self.rss_preview_menu = None # Pointer to the current preview menu |
13 | - |
14 | - self.menu = None |
15 | - self.playing_item = None |
16 | - self.create_main_menu() |
17 | - |
18 | - clock = ClockLabel(0.13, "screentitle", 0, 0.87) |
19 | - self.add(clock) |
20 | - |
21 | - def create_main_menu(self): |
22 | + |
23 | + self.rss_preview_menu = self._create_rss_preview_menu() |
24 | + self.preview.add(self.rss_preview_menu) |
25 | + |
26 | + self._create_main_menu() |
27 | + self.menu.active = True |
28 | + |
29 | + self.add(ClockLabel(0.13, "screentitle", 0, 0.87)) |
30 | + |
31 | + def get_type(self): |
32 | + """Return screen type.""" |
33 | + return Screen.NORMAL |
34 | + |
35 | + def get_name(self): |
36 | + """Return screen name (human readble)""" |
37 | + return "Main" |
38 | + |
39 | + def _create_main_menu(self): |
40 | """Create main menu of the home screen""" |
41 | - self.menu = ScrollMenu(10, 60, self.config.show_effects()) |
42 | + self.menu = ScrollMenu(10, 60, 0.045, "menuitem_active") |
43 | self.menu.set_name("mainmenu") |
44 | - |
45 | - # Common values for all ScrollMenu items |
46 | - size = 0.045 |
47 | - color = "menuitem_active" |
48 | - |
49 | - item0 = Label(size, color, 0, 0, _("Play CD"), "disc") |
50 | - self.menu.add(item0) |
51 | - |
52 | - item2 = Label(size, color, 0, 0, _("Videos"), "videos") |
53 | - self.menu.add(item2) |
54 | - |
55 | - item3 = Label(size, color, 0, 0, _("Music"), "music") |
56 | - self.menu.add(item3) |
57 | - |
58 | - item4 = Label(size, color, 0, 0, _("Photographs"), "photo") |
59 | - self.menu.add(item4) |
60 | - |
61 | - item5 = Label(size, color, 0, 0, _("Headlines"), "rss") |
62 | - self.menu.add(item5) |
63 | + self.menu.connect('selected', self._handle_select) |
64 | + self.menu.connect('moved', self._on_menu_moved) |
65 | + self.menu.connect('activated', self.main_menu_activation) |
66 | + |
67 | + self.menu.add_item(_("Play CD"), "disc") |
68 | + self.menu.add_item(_("Videos"), "videos") |
69 | + self.menu.add_item(_("Music"), "music") |
70 | + self.menu.add_item(_("Photographs"), "photo") |
71 | + self.menu.add_item(_("Headlines"), "rss") |
72 | |
73 | if self.config.display_weather_in_frontend(): |
74 | - item6 = Label(size, color, 0, 0, _("Weather"), "weather") |
75 | - self.menu.add(item6) |
76 | + self.menu.add_item(_("Weather"), "weather") |
77 | |
78 | if self.config.display_cd_eject_in_frontend(): |
79 | - item7 = Label(size, color, 0, 0, _("Eject CD"), "eject_cd") |
80 | - self.menu.add(item7) |
81 | + self.menu.add_item(_("Eject CD"), "eject_cd") |
82 | |
83 | if self.media_player.has_media(): |
84 | - playing = Label(size, color, 0, 0, _("Playing now..."), "playing") |
85 | - self.menu.add(playing) |
86 | - |
87 | - self.menu.set_number_of_visible_items(5) |
88 | - menu_clip = self.menu.get_number_of_visible_items() * 70 |
89 | - |
90 | + self.menu.add_item(_("Playing now..."), "playing") |
91 | + |
92 | + self.menu.visible_nb = 5 |
93 | + self.menu.selected_index = 2 |
94 | + |
95 | + menu_clip = self.menu.visible_nb * 70 |
96 | # Menu position |
97 | menu_y = int((self.config.get_stage_height() - menu_clip + 10) / 2) |
98 | self.menu.set_position(self.get_abs_x(0.75), menu_y) |
99 | |
100 | self.add(self.menu) |
101 | |
102 | - def show_playing_preview(self): |
103 | - """Create a group that displays information on current media.""" |
104 | + def _create_rss_preview_menu(self): |
105 | + '''Create the RSS preview menu that will show feed highlights. An |
106 | + uninitialized menu will be returned to prevent move errors if there |
107 | + is nothing in the feed library.''' |
108 | + menu = TextMenu(self.theme, self.config.show_effects()) |
109 | + |
110 | + if self.feed_library.is_empty() is False: |
111 | + menu.set_row_count(1) |
112 | + menu.set_position(self.get_abs_x(0.035), self.get_abs_y(0.12)) |
113 | + menu.set_item_size(self.get_abs_x(0.549), self.get_abs_y(0.078)) |
114 | + |
115 | + # List of latest entries. pack = (Feed object, Entry object) |
116 | + entries = self.feed_library.get_latest_entries(5) |
117 | + for pack in entries: |
118 | + text = pack[0].get_title() + " - " + pack[1].get_title() |
119 | + item = TextMenuItem(0.549, 0.078, |
120 | + FeedEntryParser().strip_tags(text), |
121 | + pack[1].get_date()) |
122 | + kwargs = { 'feed' : pack[0], 'entry' : pack[1] } |
123 | + item.set_userdata(kwargs) |
124 | + menu.add_actor(item) |
125 | + |
126 | + menu.set_active(False) |
127 | + |
128 | + return menu |
129 | + |
130 | + def _create_playing_preview(self): |
131 | + '''Create the Now Playing preview sidebar.''' |
132 | + preview = clutter.Group() |
133 | + |
134 | # Video preview of current media |
135 | video_texture = self.media_player.get_texture() |
136 | if video_texture == None: |
137 | @@ -141,75 +165,72 @@ |
138 | rect.set_size(int(new_width + 6), int(new_height + 6)) |
139 | rect.set_position(rect_x, rect_y) |
140 | rect.set_color((128, 128, 128, 192)) |
141 | - self.preview.add(rect) |
142 | + preview.add(rect) |
143 | |
144 | - self.preview.add(video_texture) |
145 | + preview.add(video_texture) |
146 | |
147 | title_text = _("Now Playing: %(title)s") % \ |
148 | {'title': self.media_player.get_media_title()} |
149 | title = Label(0.03, "text", 0.03, 0.74, title_text) |
150 | - self.preview.add(title) |
151 | - |
152 | - def show_rss_preview(self): |
153 | - """Rss preview""" |
154 | + preview.add(title) |
155 | + |
156 | + return preview |
157 | + |
158 | + def _create_rss_preview(self): |
159 | + '''Create the RSS preview sidebar.''' |
160 | + preview = clutter.Group() |
161 | + |
162 | + # RSS Icon |
163 | + icon = Texture(self.theme.getImage("rss_icon"), 0, 0.04) |
164 | + icon.set_scale(0.6, 0.6) |
165 | + preview.add(icon) |
166 | + |
167 | + # RSS Feed Title |
168 | + title = Label(0.075, "title", 0.045, 0.03, _("Recent headlines")) |
169 | + preview.add(title) |
170 | |
171 | if self.feed_library.is_empty() is False: |
172 | - # RSS Icon |
173 | - icon = Texture(self.theme.getImage("rss_icon"), 0, 0.04) |
174 | - icon.set_scale(0.6, 0.6) |
175 | - |
176 | - # RSS Feed Title |
177 | - title = Label(0.075, "title", 0.045, 0.03, _("Recent headlines")) |
178 | - |
179 | - menu = TextMenu(self.theme, self.config.show_effects()) |
180 | - menu.set_row_count(1) |
181 | - menu.set_position(self.get_abs_x(0.035), self.get_abs_y(0.12)) |
182 | - menu.set_item_size(self.get_abs_x(0.549), self.get_abs_y(0.078)) |
183 | - |
184 | - # List of latest entries. pack = (Feed object, Entry object) |
185 | - entries = self.feed_library.get_latest_entries(5) |
186 | - for pack in entries: |
187 | - text = pack[0].get_title() + " - " + pack[1].get_title() |
188 | - item = TextMenuItem(0.549, 0.078, |
189 | - FeedEntryParser().strip_tags(text), |
190 | - pack[1].get_date()) |
191 | - kwargs = { 'feed' : pack[0], 'entry' : pack[1] } |
192 | - item.set_userdata(kwargs) |
193 | - menu.add_actor(item) |
194 | - |
195 | - menu.set_active(False) |
196 | - |
197 | - # Fade in timeline |
198 | + menu = self._create_rss_preview_menu() |
199 | + preview.add(menu) |
200 | + self.rss_preview_menu = menu |
201 | + else: |
202 | + # No headlines available in the library |
203 | + info = Label(0.05, "title", 0.1, 0.35, _("No headlines available")) |
204 | + preview.add(info) |
205 | + |
206 | + return preview |
207 | + |
208 | + def _update_preview_area(self): |
209 | + '''Update the preview area to display the current menu item.''' |
210 | + self.preview.remove_all() |
211 | + item = self.menu.get_selected() |
212 | + |
213 | + self.preview.set_opacity(0x00) |
214 | + |
215 | + update = True |
216 | + |
217 | + if item.get_name() == "playing": |
218 | + self.preview.add(self._create_playing_preview()) |
219 | + elif item.get_name() == "rss": |
220 | + self.preview.add(self._create_rss_preview()) |
221 | + else: |
222 | + update = False |
223 | + |
224 | + # If the preview was updated fade it in |
225 | + if update: |
226 | fade_in = clutter.Timeline(20, 60) |
227 | alpha_in = clutter.Alpha(fade_in, clutter.smoothstep_inc_func) |
228 | - self.in_behaviour = clutter.BehaviourOpacity(0x00, 0xff, alpha_in) |
229 | - self.in_behaviour.apply(menu) |
230 | - self.in_behaviour.apply(title) |
231 | - self.in_behaviour.apply(icon) |
232 | - |
233 | - self.preview.add(icon) |
234 | - self.preview.add(title) |
235 | - self.rss_preview_menu = menu |
236 | - self.preview.add(menu) |
237 | - |
238 | - menu.set_opacity(0x00) |
239 | - icon.set_opacity(0x00) |
240 | - title.set_opacity(0x00) |
241 | - |
242 | + self.behaviour = clutter.BehaviourOpacity(0x00, 0xff, alpha_in) |
243 | + self.behaviour.apply(self.preview) |
244 | fade_in.start() |
245 | |
246 | + def _can_move_horizontally(self): |
247 | + '''Return a boolean indicating if horizontal movement is allowed.''' |
248 | + item = self.menu.get_selected() |
249 | + if self.feed_library.is_empty() or item.get_name() != 'rss': |
250 | + return False |
251 | else: |
252 | - # No headlines available in the library |
253 | - info = Label(0.07, "title", 0.1, 0.35, _("No headlines available")) |
254 | - self.preview.add(info) |
255 | - |
256 | - def get_type(self): |
257 | - """Return screen type.""" |
258 | - return Screen.NORMAL |
259 | - |
260 | - def get_name(self): |
261 | - """Return screen name (human readble)""" |
262 | - return "Main" |
263 | + return True |
264 | |
265 | def update(self): |
266 | """ |
267 | @@ -219,28 +240,13 @@ |
268 | selected_name = self.menu.get_selected().get_name() |
269 | |
270 | self.remove(self.menu) |
271 | - self.create_main_menu() |
272 | + self._create_main_menu() |
273 | |
274 | index = self.menu.get_index(selected_name) |
275 | if not index == -1: |
276 | - self.menu.select(index) |
277 | - |
278 | - self.menu.update() |
279 | - |
280 | - self.update_preview_area(self.menu.get_selected()) |
281 | - |
282 | - def update_preview_area(self, item): |
283 | - """ |
284 | - Update preview area. This area displayes information of currently |
285 | - selected menuitem. |
286 | - @param item: Menuitem (clutter.Actor) |
287 | - """ |
288 | - self.preview.remove_all() |
289 | - if item.get_name() == "playing": |
290 | - self.show_playing_preview() |
291 | - #only show the rss menu if it has entries |
292 | - elif item.get_name() == "rss": |
293 | - self.show_rss_preview() |
294 | + self.menu.selected_index = index |
295 | + |
296 | + self._update_preview_area() |
297 | |
298 | def _move_menu(self, menu_direction): |
299 | '''Move the menu in the given direction.''' |
300 | @@ -258,39 +264,34 @@ |
301 | if selected.get_name() == None: |
302 | scroll_direction() |
303 | selected = self.menu.get_selected() |
304 | - self.update_preview_area(selected) |
305 | + self._update_preview_area(selected) |
306 | else: |
307 | self.rss_preview_menu.move(preview_direction) |
308 | |
309 | def _handle_up(self): |
310 | '''Handle UserEvent.NAVIGATE_UP.''' |
311 | - self._move_menu(self.UP) |
312 | + if self.menu.active: |
313 | + self.menu.scroll_up() |
314 | + else: |
315 | + self.rss_preview_menu.move(TextMenu.UP) |
316 | |
317 | def _handle_down(self): |
318 | '''Handle UserEvent.NAVIGATE_DOWN.''' |
319 | - self._move_menu(self.DOWN) |
320 | + if self.menu.active: |
321 | + self.menu.scroll_down() |
322 | + else: |
323 | + self.rss_preview_menu.move(TextMenu.DOWN) |
324 | |
325 | def _handle_left(self): |
326 | '''Handle UserEvent.NAVIGATE_LEFT.''' |
327 | - if self.feed_library.is_empty(): |
328 | - return |
329 | - |
330 | - if self.active_menu == "main": |
331 | - self.active_menu = "preview" |
332 | - self.menu.set_active(False) |
333 | - self.rss_preview_menu.set_active(True) |
334 | + if self._can_move_horizontally(): |
335 | + self.preview_activation() |
336 | |
337 | def _handle_right(self): |
338 | '''Handle UserEvent.NAVIGATE_RIGHT.''' |
339 | - if self.feed_library.is_empty(): |
340 | - return |
341 | - |
342 | - if self.active_menu == "preview": |
343 | - self.active_menu = "main" |
344 | - self.menu.set_active(True) |
345 | - self.rss_preview_menu.set_active(False) |
346 | - |
347 | - def _handle_select(self): |
348 | + self.menu.active = True |
349 | + |
350 | + def _handle_select(self, actor=None): |
351 | '''Handle UserEvent.NAVIGATE_SELECT.''' |
352 | item = self.menu.get_selected() |
353 | |
354 | @@ -316,3 +317,22 @@ |
355 | kwargs = menu_item.get_userdata() |
356 | self.callback("entry", kwargs) |
357 | |
358 | + def _on_menu_moved(self, actor): |
359 | + ''' |
360 | + We update the preview area when the selected item changed on the |
361 | + Main menu. |
362 | + ''' |
363 | + self._update_preview_area() |
364 | + |
365 | + def main_menu_activation(self, actor=None): |
366 | + '''Handle the main menu activation.''' |
367 | + self.menu.active = True |
368 | + if self.rss_preview_menu: |
369 | + self.rss_preview_menu.set_active(False) |
370 | + |
371 | + def preview_activation(self, actor=None): |
372 | + '''Handle the preview activation.''' |
373 | + self.menu.active = False |
374 | + if self.rss_preview_menu: |
375 | + self.rss_preview_menu.set_active(True) |
376 | + |
377 | |
378 | === modified file 'entertainerlib/frontend/gui/user_interface.py' |
379 | --- entertainerlib/frontend/gui/user_interface.py 2009-03-22 21:00:43 +0000 |
380 | +++ entertainerlib/frontend/gui/user_interface.py 2009-04-03 16:27:16 +0000 |
381 | @@ -76,7 +76,10 @@ |
382 | #problem |
383 | clutter.set_use_mipmapped_text(False) |
384 | |
385 | + self.hide_cursor_timeout_key = None |
386 | + |
387 | self.stage.connect('key-press-event', self.handle_keyboard_event) |
388 | + self.stage.connect('motion-event', self.handle_motion_event) |
389 | self.stage.set_color(self.config.theme.get_color("background")) |
390 | self.stage.set_size( |
391 | self.config.get_stage_width(), self.config.get_stage_height()) |
392 | @@ -177,7 +180,6 @@ |
393 | |
394 | def _fullscreen(self): |
395 | '''Set the window, stage, and config to fullscreen dimensions.''' |
396 | - self.stage.hide_cursor() |
397 | self.window.fullscreen() |
398 | self.stage.fullscreen() |
399 | width, height = self.stage.get_size() |
400 | @@ -204,6 +206,7 @@ |
401 | def start_up(self): |
402 | '''Start the user interface and make it visible.''' |
403 | self.show() |
404 | + self.stage.hide_cursor() |
405 | self.current = self.create_screen("main") |
406 | self.transition.forward_effect(None, self.current) |
407 | self.enable_menu_overlay() |
408 | @@ -217,7 +220,6 @@ |
409 | if self.is_fullscreen: |
410 | self.stage.unfullscreen() |
411 | self.window.unfullscreen() |
412 | - self.stage.show_cursor() |
413 | self.config.set_stage_width(self.old_width) |
414 | self.config.set_stage_height(self.old_height) |
415 | self.is_fullscreen = False |
416 | @@ -302,6 +304,22 @@ |
417 | elif direction == Transition.BACKWARD: |
418 | self.transition.backward_effect(from_screen, screen) |
419 | |
420 | + def hide_cursor_timeout_callback(self): |
421 | + '''Hide the cursor''' |
422 | + self.stage.hide_cursor() |
423 | + return True |
424 | + |
425 | + def handle_motion_event(self, stage, clutter_event): |
426 | + ''' |
427 | + Show the cursor if it has been moved and start a timeout to |
428 | + hide it after a few seconds. |
429 | + ''' |
430 | + self.stage.show_cursor() |
431 | + if self.hide_cursor_timeout_key is not None: |
432 | + gobject.source_remove(self.hide_cursor_timeout_key) |
433 | + self.hide_cursor_timeout_key = gobject.timeout_add(3000, |
434 | + self.hide_cursor_timeout_callback) |
435 | + |
436 | def handle_keyboard_event(self, stage, clutter_event, event_handler=None): |
437 | '''Translate all received keyboard events to UserEvents.''' |
438 | if event_handler is None: |
439 | |
440 | === added file 'entertainerlib/frontend/gui/widgets/motion_buffer.py' |
441 | --- entertainerlib/frontend/gui/widgets/motion_buffer.py 1970-01-01 00:00:00 +0000 |
442 | +++ entertainerlib/frontend/gui/widgets/motion_buffer.py 2009-04-03 20:23:10 +0000 |
443 | @@ -0,0 +1,103 @@ |
444 | +"""Tools for pointer motion calculations""" |
445 | + |
446 | +__licence__ = "GPLv2" |
447 | +__copyright__ = "2009, Samuel Buffet" |
448 | +__author__ = "Samuel Buffet <samuel.buffet@gmail.com>" |
449 | + |
450 | +import math |
451 | + |
452 | +class MotionBuffer: |
453 | + """ |
454 | + This Class achieves some calculations on the pointer motion |
455 | + """ |
456 | + def __init__ (self): |
457 | + # public |
458 | + self.dt_from_start = 0 |
459 | + self.dx_from_start = 0 |
460 | + self.dy_from_start = 0 |
461 | + self.distance_from_start = 0 |
462 | + self.speed_x_from_start = 0 |
463 | + self.speed_y_from_start = 0 |
464 | + self.speed_from_start = 0 |
465 | + |
466 | + self.dt_from_last_motion_event = 0 |
467 | + self.dx_from_last_motion_event = 0 |
468 | + self.dy_from_last_motion_event = 0 |
469 | + self.distance_from_last_motion_event = 0 |
470 | + self.speed_x_from_last_motion_event = 0 |
471 | + self.speed_y_from_last_motion_event = 0 |
472 | + self.speed_from_last_motion_event = 0 |
473 | + |
474 | + # private |
475 | + self._start_x = 0 |
476 | + self._start_y = 0 |
477 | + self._start_time = 0 |
478 | + |
479 | + self._last_motion_event_x = 0 |
480 | + self._last_motion_event_y = 0 |
481 | + self._last_motion_event_time = 0 |
482 | + |
483 | + def start(self, event): |
484 | + '''Has to be called when we start a pointer motion''' |
485 | + self._start_x = event.x |
486 | + self._start_y = event.y |
487 | + self._start_time = event.time |
488 | + |
489 | + self._last_motion_event_x = event.x |
490 | + self._last_motion_event_y = event.y |
491 | + self._last_motion_event_time = event.time |
492 | + |
493 | + def take_new_motion_event(self, event): |
494 | + ''' |
495 | + Has to be called by a method connected to the *motion-event* of an |
496 | + actor |
497 | + ''' |
498 | + self._last_motion_event_x = event.x |
499 | + self._last_motion_event_y = event.y |
500 | + self._last_motion_event_time = event.time |
501 | + |
502 | + def compute_from_start(self, event): |
503 | + '''compute deltas an speeds from the beginning of the motion''' |
504 | + self.dt_from_start = event.time - self._start_time |
505 | + self.dx_from_start = event.x - self._start_x |
506 | + self.dy_from_start = event.y - self._start_y |
507 | + self.distance_from_start = math.sqrt(self.dx_from_start * \ |
508 | + self.dx_from_start + self.dy_from_start * self.dy_from_start) |
509 | + |
510 | + if self.dt_from_start != 0 : |
511 | + self.speed_x_from_start = float(self.dx_from_start) / \ |
512 | + float(self.dt_from_start) |
513 | + self.speed_y_from_start = float(self.dy_from_start) / \ |
514 | + float(self.dt_from_start) |
515 | + self.speed_from_start = float(self.distance_from_start) / \ |
516 | + float(self.dt_from_start) |
517 | + else: |
518 | + self.speed_x_from_start = 0 |
519 | + self.speed_y_from_start = 0 |
520 | + self.speed_from_start = 0 |
521 | + |
522 | + def compute_from_last_motion_event(self, event): |
523 | + '''compute deltas an speeds from the last motion-event''' |
524 | + self.dt_from_last_motion_event = event.time - \ |
525 | + self._last_motion_event_time |
526 | + self.dx_from_last_motion_event = event.x - self._last_motion_event_x |
527 | + self.dy_from_last_motion_event = event.y - self._last_motion_event_y |
528 | + self.distance_from_last_motion_event = math.sqrt( \ |
529 | + self.dx_from_last_motion_event * self.dx_from_last_motion_event + \ |
530 | + self.dy_from_last_motion_event * self.dy_from_last_motion_event) |
531 | + |
532 | + if self.dt_from_last_motion_event != 0 : |
533 | + self.speed_x_from_last_motion_event = \ |
534 | + float(self.dx_from_last_motion_event) / \ |
535 | + float(self.dt_from_last_motion_event) |
536 | + self.speed_y_from_last_motion_event = \ |
537 | + float(self.dy_from_last_motion_event) / \ |
538 | + float(self.dt_from_last_motion_event) |
539 | + self.speed_from_last_motion_event = \ |
540 | + float(self.distance_from_last_motion_event) / \ |
541 | + float(self.dt_from_last_motion_event) |
542 | + else: |
543 | + self.speed_x_from_last_motion_event = 0 |
544 | + self.speed_y_from_last_motion_event = 0 |
545 | + self.speed_from_last_motion_event = 0 |
546 | + |
547 | |
548 | === modified file 'entertainerlib/frontend/gui/widgets/scroll_menu.py' |
549 | --- entertainerlib/frontend/gui/widgets/scroll_menu.py 2009-03-02 02:27:27 +0000 |
550 | +++ entertainerlib/frontend/gui/widgets/scroll_menu.py 2009-04-08 11:23:26 +0000 |
551 | @@ -2,209 +2,271 @@ |
552 | |
553 | __licence__ = "GPLv2" |
554 | __copyright__ = "2007, Lauri Taimila" |
555 | -__author__ = "Lauri Taimila <lauri@taimila.com>" |
556 | +__author__ = ("Lauri Taimila <lauri@taimila.com>", |
557 | + "Samuel Buffet <Samuel.Buffet@gmail.com>") |
558 | |
559 | import clutter |
560 | - |
561 | -class ScrollMenu(clutter.Group): |
562 | +import math |
563 | +import gobject |
564 | + |
565 | +from entertainerlib.frontend.gui.widgets.label import Label |
566 | +from entertainerlib.frontend.gui.widgets.motion_buffer import MotionBuffer |
567 | + |
568 | +class LoopedPathBehaviour (clutter.Behaviour): |
569 | + """A custome Path behaviour driven by an index [0, 1]""" |
570 | + __gtype_name__ = 'LoopedPathBehaviour' |
571 | + |
572 | + def __init__(self, alpha=None): |
573 | + clutter.Behaviour.__init__(self) |
574 | + self.set_alpha(alpha) |
575 | + self.start_index = 0.0 |
576 | + self.end_index = 1.0 |
577 | + self.start_knot = (0, 0) |
578 | + self.end_knot = (10, 10) |
579 | + |
580 | + def do_alpha_notify (self, alpha_value): |
581 | + """ |
582 | + We position the actor on the Line defined by the 2 knots according to |
583 | + the alpha value. |
584 | + alpha = 0 position = start_knot |
585 | + alpha = MAX_ALPHA position = end_knot |
586 | + """ |
587 | + idx = alpha_value * (self.end_index - self.start_index) |
588 | + idx /= clutter.MAX_ALPHA |
589 | + idx += self.start_index |
590 | + |
591 | + if idx >= 0 : |
592 | + idxx = math.modf(idx)[0] |
593 | + else: |
594 | + idxx = 1 + math.modf(idx)[0] |
595 | + |
596 | + # Calculation of new coordinates |
597 | + x = idxx * (self.end_knot[0] - self.start_knot[0]) + self.start_knot[0] |
598 | + y = idxx * (self.end_knot[1] - self.start_knot[1]) + self.start_knot[1] |
599 | + |
600 | + for actor in self.get_actors(): |
601 | + actor.set_position(int(x), int(y)) |
602 | + |
603 | + @ property |
604 | + def path_length(self): |
605 | + """Calculation of the path's length in pixels'""" |
606 | + dx = self.end_knot[0] - self.start_knot[0] |
607 | + dy = self.end_knot[1] - self.start_knot[1] |
608 | + return math.sqrt(dx * dx + dy * dy) |
609 | + |
610 | +class ScrollMenuItem(clutter.Group): |
611 | + """ |
612 | + This is a Group containing a Label to which a LoopedPathBehaviour is applied |
613 | + """ |
614 | + __gtype_name__ = 'ScrollMenuItem' |
615 | + |
616 | + def __init__(self, alpha, text, item_height, font_size, color_name): |
617 | + clutter.Group.__init__(self) |
618 | + |
619 | + self.label = Label(font_size, color_name, 0, 0) |
620 | + self.label.set_text(text) |
621 | + |
622 | + self.behaviour = LoopedPathBehaviour(alpha) |
623 | + self.behaviour.apply(self) |
624 | + |
625 | + clutter.Group.add(self, self.label) |
626 | + |
627 | +class ScrollMenu(clutter.Group, object): |
628 | """ |
629 | Menu widget that contains text items. Menu can be scrolled up and down. |
630 | - |
631 | - - Options: zoom, opacity, display N items, display arrows top+down |
632 | - - add Clutter labels |
633 | - - go around (how to implement this) (On state_change move items from |
634 | - start->back) |
635 | """ |
636 | - |
637 | - DOWN = 0 |
638 | - UP = 1 |
639 | - |
640 | - def __init__(self, item_gap, item_height, animated=False): |
641 | - """ |
642 | - Initialize Menu |
643 | - @param item_gap: Gap between menuitems in pixels |
644 | - """ |
645 | + __gtype_name__ = 'ScrollMenu' |
646 | + __gsignals__ = { |
647 | + 'activated' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ), |
648 | + 'selected' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ), |
649 | + 'moved' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ), |
650 | + } |
651 | + |
652 | + MODE_SELECTION = 0 |
653 | + MODE_MOTION = 1 |
654 | + MODE_STOP = 2 |
655 | + |
656 | + def __init__(self, item_gap, item_height, font_size, color_name): |
657 | clutter.Group.__init__(self) |
658 | - self.__active = True |
659 | - self.__clip_size = -1 |
660 | - self.__items = [] |
661 | - self.__gap = item_gap |
662 | - self.__item_height = item_height |
663 | - self.__animated = animated |
664 | - |
665 | - self.scroll_timeline = clutter.Timeline(7, 26) |
666 | - self.scroll_timeline.connect('new-frame', self.scroll_items_cb) |
667 | - |
668 | - def add(self, actor): |
669 | - """ |
670 | - Add new menuitem to the menu. |
671 | - @param item: Label |
672 | - """ |
673 | - clutter.Group.add(self, actor) |
674 | - actor.connect('notify::y', self.actor_notify_y) |
675 | - |
676 | - y = ((len(self.__items) * self.__item_height) + |
677 | - (self.__gap * len(self.__items))) |
678 | - |
679 | - if y == 0 : |
680 | - # need to do that to update opacity of the first item added |
681 | - self.actor_notify_y(actor, None) |
682 | - actor.set_position(0, y) |
683 | - |
684 | - self.__items.append(actor) |
685 | - |
686 | - def remove(self, actor): |
687 | - """ |
688 | - Remove actor from the menu. |
689 | - """ |
690 | - clutter.Group.add(self, actor) |
691 | - for item in self.__items: |
692 | - if item is actor: |
693 | - del item |
694 | - |
695 | - def remove_all(self): |
696 | - """ |
697 | - Remove all actors from the menu. |
698 | - """ |
699 | - del self.__items |
700 | - self.__items = [] |
701 | - |
702 | - def get_index(self, text): |
703 | - """ |
704 | - Returns index of label with the text as passed, returns -1 if not found |
705 | - @author Joshua Scotton |
706 | - """ |
707 | - for item in self.__items: |
708 | - if item.get_name() == text: |
709 | - return self.__items.index(item) |
710 | - return -1 |
711 | - |
712 | - def select(self, index): |
713 | - """ |
714 | - Selects the item at the passed index, does not support animations |
715 | - @author Joshua Scotton |
716 | - """ |
717 | - #split list so that item at the index will now be at position 2 |
718 | - #which is the selected index in a scroll menu |
719 | - items = self.__items[index-2:] + self.__items[0:index-2] |
720 | - |
721 | - #Basically remove all then add in the correct order |
722 | - self.remove_all() |
723 | - for item in items: |
724 | - #Remove all doesn't remove the parent object. So in order to avoid |
725 | - #adding two parent clutter items to the item we need to remove the |
726 | - #old one first |
727 | - clutter.Group.remove(self, item) |
728 | - self.add(item) |
729 | - |
730 | - def get_number_of_items(self): |
731 | - """Get number of menuitems in this Menu""" |
732 | - return len(self.__items) |
733 | - |
734 | - def set_number_of_visible_items(self, number): |
735 | - """ |
736 | - Set how many menuitems are displayed on the screen at the same time. |
737 | - @param number: Number of items |
738 | - """ |
739 | - self.__clip_size = number |
740 | - self.set_clip(0, |
741 | - 0, |
742 | - self.get_width(), |
743 | - (self.__item_height + self.__gap) * number) |
744 | - |
745 | - def get_number_of_visible_items(self): |
746 | - """ |
747 | - Get number of menuitems that are displayed on the screen at the same |
748 | - time. |
749 | - |
750 | - @return: Ingeter |
751 | - """ |
752 | - return self.__clip_size |
753 | - |
754 | - def get_selected(self): |
755 | - """ |
756 | - Get currently selected menuitem |
757 | - @return Clutter actor object |
758 | - """ |
759 | - return self.__items[2] |
760 | - |
761 | - def scroll_down(self): |
762 | - """Scroll menu down by one menuitem.""" |
763 | - if not self.scroll_timeline.is_playing(): |
764 | - if self.__animated: |
765 | - self.__scroll_direction = self.DOWN |
766 | - self.scroll_timeline.start() |
767 | - else: |
768 | - for item in self.__items: |
769 | - item.move_by(0, self.__item_height + self.__gap) |
770 | - |
771 | - # Set last item to first (menu scrolls) |
772 | - last = self.__items[-1] |
773 | - last.set_position( |
774 | - 0, self.__items[0].get_y() - self.__gap - self.__item_height) |
775 | - self.__items = [last] + self.__items |
776 | - del self.__items[-1] |
777 | - |
778 | - def scroll_up(self): |
779 | - """Scroll menu up by one menuitem.""" |
780 | - if not self.scroll_timeline.is_playing(): |
781 | - if self.__animated: |
782 | - self.__scroll_direction = self.UP |
783 | - self.scroll_timeline.start() |
784 | - else: |
785 | - for item in self.__items: |
786 | - item.move_by(0, -(self.__item_height + self.__gap)) |
787 | - |
788 | - # Set first item to last (menu scrolls) |
789 | - first = self.__items[0] |
790 | - first.set_position( |
791 | - 0, self.__items[-1].get_y() + self.__gap + self.__item_height) |
792 | - self.__items.append(first) |
793 | - del self.__items[0] |
794 | - |
795 | - def scroll_items_cb(self, timeline, frame_num): |
796 | - """Callback function of scroll_down_timeline""" |
797 | - delta = timeline.get_delta()[0] |
798 | - offset = int((self.__item_height + self.__gap) / 7 * delta) |
799 | - if self.__scroll_direction == 1: |
800 | - offset = - offset |
801 | - for item in self.__items: |
802 | - item.move_by(0, offset) |
803 | - |
804 | - def is_active(self): |
805 | + self._motion_buffer = MotionBuffer() |
806 | + self._items = [] |
807 | + self._item_gap = item_gap |
808 | + self._item_height = item_height |
809 | + self._item_font_size = font_size |
810 | + self._item_color_name = color_name |
811 | + self._selected_index = 1 |
812 | + self._visible_nb = 5 |
813 | + self._event_mode = -1 |
814 | + self._animation_progression = 0 |
815 | + self._animation_start_index = 1 |
816 | + self._animation_end_index = 1 |
817 | + self._active = False |
818 | + self._motion_handler = 0 |
819 | + |
820 | + self._timeline = clutter.Timeline(fps=200, duration=300) |
821 | + self._alpha = clutter.Alpha(self._timeline, clutter.sine_inc_func) |
822 | + |
823 | + # preparation to pointer events handling |
824 | + self.set_reactive(True) |
825 | + self.connect('scroll-event', self._on_scroll_event) |
826 | + self.connect('button-press-event', self._on_button_press_event) |
827 | + self.connect('button-release-event', self._on_button_release_event) |
828 | + |
829 | + def add_item(self, text, name): |
830 | + """Creation of a new MenuItem and addition to the ScrollMenu""" |
831 | + item = ScrollMenuItem(self._alpha, text, self._item_height, |
832 | + self._item_font_size, self._item_color_name) |
833 | + |
834 | + item.set_name(name) |
835 | + item.connect('notify::y', self._on_item_notify_y) |
836 | + clutter.Group.add(self, item) |
837 | + |
838 | + self._items.append(item) |
839 | + |
840 | + def _get_active(self): |
841 | """ |
842 | Is this menu active. |
843 | @return boolean, True if menu is currently active, otherwise False |
844 | """ |
845 | - return self.__active |
846 | + return self._active |
847 | |
848 | - def set_active(self, boolean): |
849 | + def _set_active(self, boolean): |
850 | """ |
851 | Set this widget active or inactive |
852 | @param boolean: True to set widget active, False to set inactive |
853 | """ |
854 | - self.__active = boolean |
855 | + if self._active == boolean: |
856 | + return |
857 | + |
858 | + self._active = boolean |
859 | if boolean: |
860 | - #if self.__selector is not None: |
861 | - # self.__selector.show() |
862 | self.set_opacity(255) |
863 | + self.emit('activated') |
864 | else: |
865 | - #if self.__selector is not None: |
866 | - # self.__selector.hide() |
867 | self.set_opacity(128) |
868 | |
869 | - def update(self): |
870 | - """Update the whole menu""" |
871 | - for item in self.__items: |
872 | - opacity = self.get_opacity_for_y(item.get_y()) |
873 | - item.set_opacity(opacity) |
874 | + active = property(_get_active, _set_active) |
875 | + |
876 | + def _update_behaviours(self, target): |
877 | + """ |
878 | + Preparation of looped behaviours applied to menu items before |
879 | + we move them. |
880 | + """ |
881 | + nbi = len(self._items) |
882 | + step = 1.0 / nbi |
883 | + step_pix = self._item_gap + self._item_height |
884 | + a = int(self._visible_nb / 2) + 1 |
885 | + for x, item in enumerate(self._items): |
886 | + item.behaviour.start_index = (x + a - self._selected_index) * step |
887 | + item.behaviour.end_index = (x + a - target) * step |
888 | + |
889 | + item.behaviour.start_knot = (0.0, -step_pix) |
890 | + item.behaviour.end_knot = (0.0, (nbi - 1.0) * step_pix) |
891 | + |
892 | + def _display_items_at_target(self, target): |
893 | + """ |
894 | + This method draws the menu for a particular targeted value of the |
895 | + driving index. |
896 | + """ |
897 | + nbi = len(self._items) |
898 | + step = 1.0 / nbi |
899 | + |
900 | + a = int(self._visible_nb / 2) + 1 |
901 | + for x, item in enumerate(self._items): |
902 | + index = (x + a - target) * step |
903 | + |
904 | + if index >= 0 : |
905 | + idxx = math.modf(index)[0] |
906 | + else: |
907 | + idxx = 1 + math.modf(index)[0] |
908 | + |
909 | + # Calculation of new coordinates |
910 | + xx = idxx * (item.behaviour.end_knot[0] - \ |
911 | + item.behaviour.start_knot[0]) + item.behaviour.start_knot[0] |
912 | + yy = idxx * (item.behaviour.end_knot[1] - \ |
913 | + item.behaviour.start_knot[1]) + item.behaviour.start_knot[1] |
914 | + |
915 | + item.set_position(int(xx), int(yy)) |
916 | + |
917 | + def _get_visible_nb(self): |
918 | + """Return the visible_nb property""" |
919 | + return self._visible_nb |
920 | + |
921 | + def _set_visible_nb(self, visible_nb): |
922 | + """Set the visible_nb property""" |
923 | + self._visible_nb = visible_nb |
924 | + h = visible_nb * self._item_height + (visible_nb - 1) * self._item_gap |
925 | + self.set_clip(0, 0, self.get_width(), h) |
926 | + |
927 | + visible_nb = property(_get_visible_nb, _set_visible_nb) |
928 | + |
929 | + def get_selected(self): |
930 | + """ |
931 | + Get currently selected menuitem |
932 | + @return Clutter actor object |
933 | + """ |
934 | + return self._items[int(self._selected_index)] |
935 | + |
936 | + def _get_selected_index(self): |
937 | + """Return the selected_index property""" |
938 | + return self._selected_index |
939 | + |
940 | + def _set_selected_index(self, selected_index, duration=300): |
941 | + """Set the selected_index property""" |
942 | + if not self._timeline.is_playing(): |
943 | + nb = len(self._items) |
944 | + self._update_behaviours(selected_index) |
945 | + |
946 | + # those 2 variables are used if we want to stop the timeline |
947 | + # we use them + timeline progression to calculate the current index |
948 | + # when (if) we stop |
949 | + self._animation_start_index = self._selected_index |
950 | + self._animation_end_index = selected_index |
951 | + |
952 | + # compute selected_index to be between 0 and nb |
953 | + if selected_index >= 0: |
954 | + self._selected_index = selected_index - \ |
955 | + math.modf(selected_index / nb)[1] * nb |
956 | + else: |
957 | + self._selected_index = selected_index + \ |
958 | + (math.modf(-(selected_index + 1) / nb)[1] + 1) * nb |
959 | + |
960 | + self._timeline.set_duration(duration) |
961 | + self._timeline.start() |
962 | + |
963 | + self.emit('moved') |
964 | + |
965 | + selected_index = property(_get_selected_index, _set_selected_index) |
966 | + |
967 | + def get_index(self, text): |
968 | + """ |
969 | + Returns index of label with the text as passed, returns -1 if not found |
970 | + @author Joshua Scotton |
971 | + """ |
972 | + for item in self._items: |
973 | + if item.get_name() == text: |
974 | + return self._items.index(item) |
975 | + return -1 |
976 | + |
977 | + def scroll_by(self, nb, duration=300): |
978 | + """Set the selected_index property""" |
979 | + self._set_selected_index(self._selected_index + nb, duration) |
980 | + |
981 | + def scroll_up(self, duration=300): |
982 | + """Set the selected_index property""" |
983 | + self.scroll_by(-1, duration) |
984 | + |
985 | + def scroll_down(self, duration=300): |
986 | + """Set the selected_index property""" |
987 | + self.scroll_by(1, duration) |
988 | |
989 | def get_opacity_for_y(self, y): |
990 | - """Calculation of actor's opacity as a function of its y coordinate""" |
991 | - opacity_first_item = 30 |
992 | + """Calculation of actor's opacity as a function of its y coordinates""" |
993 | + opacity_first_item = 40 |
994 | opacity_selected_item = 255 |
995 | + o = int(self._visible_nb / 2) |
996 | |
997 | - y_medium_item = 2 * (self.__item_height + self.__gap) |
998 | + y_medium_item = o * (self._item_height + self._item_gap) |
999 | a = float(opacity_selected_item - opacity_first_item) |
1000 | a /= float(y_medium_item) |
1001 | if y <= y_medium_item : |
1002 | @@ -217,8 +279,116 @@ |
1003 | |
1004 | return int(opacity) |
1005 | |
1006 | - def actor_notify_y(self, actor, stage): |
1007 | + def _on_item_notify_y(self, item, stage): |
1008 | """Set opacity to actors when they are moving. Opacity is f(y)""" |
1009 | - opacity = self.get_opacity_for_y(actor.get_y()) |
1010 | - actor.set_opacity(opacity) |
1011 | + opacity = self.get_opacity_for_y(item.get_y()) |
1012 | + item.set_opacity(opacity) |
1013 | + |
1014 | + def _on_button_press_event(self, actor, event): |
1015 | + """button-press-event handler""" |
1016 | + clutter.grab_pointer(self) |
1017 | + if not self.handler_is_connected(self._motion_handler): |
1018 | + self._motion_handler = self.connect('motion-event', |
1019 | + self._on_motion_event) |
1020 | + self.active = True |
1021 | + |
1022 | + if self._timeline.is_playing(): |
1023 | + # before we stop the timeline, store its progression |
1024 | + self._animation_progression = self._timeline.get_progress() |
1025 | + |
1026 | + # A click with an animation pending should stop the animation |
1027 | + self._timeline.stop() |
1028 | + |
1029 | + # go to MODE_STOP to handle correctly next button-release event |
1030 | + self._event_mode = self.MODE_STOP |
1031 | + else: |
1032 | + # no animation pending so we're going to do either a menu_item |
1033 | + # selection or a menu motion. This will be decided later, right now |
1034 | + # we just take a snapshot of this button-press-event as a start. |
1035 | + self._motion_buffer.start(event) |
1036 | + self._event_mode = self.MODE_SELECTION |
1037 | + |
1038 | + return False |
1039 | + |
1040 | + def _on_button_release_event(self, actor, event): |
1041 | + """button-release-event handler""" |
1042 | + clutter.ungrab_pointer() |
1043 | + if self.handler_is_connected(self._motion_handler): |
1044 | + self.disconnect_by_func(self._on_motion_event) |
1045 | + self._motion_buffer.compute_from_last_motion_event(event) |
1046 | + |
1047 | + y = event.y - self.get_y() |
1048 | + |
1049 | + if self._event_mode == self.MODE_SELECTION: |
1050 | + # if we are in MODE_SELECTION it means that we want to select |
1051 | + # the menu item bellow the pointer |
1052 | + |
1053 | + for idx, item in enumerate(self._items): |
1054 | + b = item.get_y() |
1055 | + h = item.get_height() |
1056 | + if (y >= b) and (y <= (b + h)): |
1057 | + delta1 = idx - self._selected_index |
1058 | + delta2 = idx - self._selected_index + len(self._items) |
1059 | + delta3 = idx - self._selected_index - len(self._items) |
1060 | + |
1061 | + delta = 99999 |
1062 | + for i in [delta1, delta2, delta3]: |
1063 | + if math.fabs(i)<math.fabs(delta): |
1064 | + delta = i |
1065 | + |
1066 | + self.scroll_by(delta) |
1067 | + |
1068 | + # if delta = 0 it means we've clicked on the selected item |
1069 | + if delta == 0: |
1070 | + self.emit('selected') |
1071 | + |
1072 | + elif self._event_mode == self.MODE_MOTION: |
1073 | + speed = self._motion_buffer.speed_y_from_last_motion_event |
1074 | + target = self._selected_index - \ |
1075 | + self._motion_buffer.dy_from_start / \ |
1076 | + self._items[0].behaviour.path_length * len(self._items) |
1077 | + |
1078 | + new_index = int(target - 5 * speed) |
1079 | + self._selected_index = target |
1080 | + self._set_selected_index(new_index, 1000) |
1081 | + |
1082 | + else: |
1083 | + # If we have stopped the pending animation. Now we have to do |
1084 | + # a small other one to select the closest menu-item |
1085 | + current_index = self._animation_start_index + \ |
1086 | + (self._animation_end_index - self._animation_start_index) * \ |
1087 | + self._animation_progression |
1088 | + self._selected_index = current_index |
1089 | + target_index = int(current_index) |
1090 | + self._set_selected_index(target_index, 1000) |
1091 | + |
1092 | + return False |
1093 | + |
1094 | + def _on_motion_event(self, actor, event): |
1095 | + """motion-event handler""" |
1096 | + self._motion_buffer.compute_from_start(event) |
1097 | + if self._motion_buffer.distance_from_start > 10: |
1098 | + self._motion_buffer.take_new_motion_event(event) |
1099 | + self._event_mode = self.MODE_MOTION |
1100 | + target = self._selected_index - \ |
1101 | + self._motion_buffer.dy_from_start / \ |
1102 | + self._items[0].behaviour.path_length * len(self._items) |
1103 | + self._display_items_at_target(target) |
1104 | + |
1105 | + self.get_stage().show_cursor() |
1106 | + |
1107 | + return False |
1108 | + |
1109 | + def _on_scroll_event(self, actor, event): |
1110 | + """scroll-event handler (mouse's wheel)""" |
1111 | + self.active = True |
1112 | + |
1113 | + if event.direction == clutter.SCROLL_DOWN: |
1114 | + self.scroll_down(150) |
1115 | + else: |
1116 | + self.scroll_up(150) |
1117 | + |
1118 | + self.get_stage().show_cursor() |
1119 | + |
1120 | + return False |
1121 | |
1122 | |
1123 | === added file 'entertainerlib/tests/test_scrollmenu.py' |
1124 | --- entertainerlib/tests/test_scrollmenu.py 1970-01-01 00:00:00 +0000 |
1125 | +++ entertainerlib/tests/test_scrollmenu.py 2009-04-07 11:31:48 +0000 |
1126 | @@ -0,0 +1,36 @@ |
1127 | +"""Tests ScrollMenu""" |
1128 | + |
1129 | +__license__ = "GPLv2" |
1130 | +__copyright__ = "2009, Samuel Buffet" |
1131 | +__author__ = "Samuel Buffet <samuel.buffet@gmail.com>" |
1132 | + |
1133 | +import clutter |
1134 | + |
1135 | +from entertainerlib.frontend.gui.widgets.scroll_menu import ScrollMenu |
1136 | +from entertainerlib.tests import EntertainerTest |
1137 | + |
1138 | +class ScrollMenuTest(EntertainerTest): |
1139 | + """Test for entertainerlib.frontend.gui.widgets.scroll_menu""" |
1140 | + |
1141 | + def setUp(self): |
1142 | + """Set up the test""" |
1143 | + EntertainerTest.setUp(self) |
1144 | + |
1145 | + |
1146 | + self.menu = ScrollMenu(10, 60, 0.045, "menuitem_active") |
1147 | + self.menu.set_name("mainmenu") |
1148 | + |
1149 | + self.menu.add_item(_("Play CD"), "disc") |
1150 | + self.menu.add_item(_("Videos"), "videos") |
1151 | + self.menu.add_item(_("Music"), "music") |
1152 | + self.menu.add_item(_("Photographs"), "photo") |
1153 | + self.menu.add_item(_("Headlines"), "rss") |
1154 | + |
1155 | + def tearDown(self): |
1156 | + """Clean up after the test""" |
1157 | + EntertainerTest.tearDown(self) |
1158 | + |
1159 | + def testCreate(self): |
1160 | + """Test correct ScrollArea initialization""" |
1161 | + self.assertTrue(isinstance(self.menu, (clutter.Group, object))) |
1162 | + |
Hi,
Finally, I think this branch is robust enough to be proposed.
The goal of this new scroll menu was to improve animations and to make it pointer reactive.
So, the scroll menu core mechanism has been created from scratch but I kept some old interface methods.
A new LoopedPath behaviour was created. It's more or less a 2 knots clutter path behaviour driven by an index. Index=0 place the actor on the starting knot and Index=1 place it on the end knot. Index can be whatever value we want but the actor will always be positioned on the 2 knots "infinite" path (ie if we apply the behaviour from idx=0 to idx=2 then the actor will move 2 times along the segment).
I've also added a MotionBuffer Class to make some calculations (distances, speeds) when we move the mouse. I've placed it its own file because this is something we can use later on other widgets (probably other menus). Right now it does more than needed by the ScrollMenu widget (only a vertical speed was needed).
I've also added a hide/show cursor mechanism (the one I discussed with Matt previously).
New interaction possible with the mouse are :
click on one menu item => moves it to central position
click on the menu item in central position => *enter* that menu
click + keeping the mouse button pressed and moving => we can slowly move the menu or give it an impulse to "turn" with a first try of kinetic effect based on the mouse speed motion.
The main screen has been modified to react to the new signals.
Cheers,
Samuel