import clutter import pango import pygtk import gtk import math from utils.InputQueue import InputQueue class LabelQueue(clutter.Group): DIRECTION_UP, DIRECTION_DOWN = range(2) ORIENTATION_TOP, ORIENTATION_BOTTOM = range(2) fps = 70 frames = 50 #Default values font_string = "Tahoma 30" item_height_percent = 1.00 width = 0 item_height = 0 use_clip = True def __init__(self, orientation=ORIENTATION_TOP): clutter.Group.__init__(self) self.items = [] self.orientation = orientation #Setup input queue controller self.input_queue = InputQueue() self.input_queue.set_action(InputQueue.NORTH, self.move_up) self.input_queue.set_action(InputQueue.SOUTH, self.move_down) self.selected = 0 self.displayMin = 0 #The number of menu items that will be shown at a time self.displayMax = 5 # default value self.displaySize = self.displayMax - self.displayMin self.roll_point_min = 1 #The item number at which point the list will roll down self.roll_point_max = self.displaySize -1 #The item number at which point the list will roll up #There are 2 subgroups: # 1) item_group: Contains the labels themselves # 2) display_group: Contains groups 1 & 2. Display group can optionally have a clip applied to it # Group 2 is then added to self self.item_group = clutter.Group() self.item_group.show() self.display_group = clutter.Group() self.display_group.show() self.display_group.add(self.item_group) self.inactive_item_background = None self.image_down = None self.image_up = None #Selector bar image, moves with selections to show current item self.selector_bar = None #Score is used when adding / removing items as it is a two stage process (eg 2 timelines) self.score = clutter.Score() self.score.connect("completed", self.flush_backlog) self.score.append(clutter.Timeline(20,20)) # Seems to be a bug in Clutter 0.6 if score.remove_all() is called before a timeline has been added, so just add an unusued timeline here self.backlog = [] def setup_from_theme_id(self, themeMgr, id, parent=None): context = "label_queue" element = themeMgr.search_docs(context, id).childNodes img_element = themeMgr.search_docs(context, id).getElementsByTagName("texture") #Quick check to make sure we found something if element is None: return None self.item_height_percent = float(themeMgr.find_child_value(element, "item_height_percent")) self.item_width_percent = float(themeMgr.find_child_value(element, "item_width_percent")) #Grab the font font_node = themeMgr.get_subnode(element, "font") fontString = themeMgr.get_font("main", font_node) self.font_string = fontString #Set the selection effect steps self.opacityStep0 = int(themeMgr.find_child_value(element, "opacity_step0")) self.opacityStep1 = int(themeMgr.find_child_value(element, "opacity_step1")) self.opacityStep2 = int(themeMgr.find_child_value(element, "opacity_step2")) self.fps = int(themeMgr.find_child_value(element, "transition_fps")) self.frames = int(themeMgr.find_child_value(element, "transition_frames")) clip = themeMgr.find_child_value(element, "use_clip") if not clip is None: self.use_clip = (clip == "True") if parent is None: parent = themeMgr.stage themeMgr.setup_actor(self, element, parent) (self.width, self.height) = themeMgr.get_dimensions(element, parent) #Set the up/down images + the selector bar #This assumes images go in the bottom right corner, will add flexibility later img_element_up = themeMgr.find_element(img_element, "id", "image_up") img_element_down = themeMgr.find_element(img_element, "id", "image_down") img_element_selector_bar = themeMgr.find_element(img_element, "id", "selector_bar") if not img_element_up is None: img_element_up = img_element_up.childNodes self.image_up = themeMgr.get_texture("image_up", self, element = img_element_up) self.image_up.set_opacity(180) self.image_up.set_position( self.width-self.image_up.get_width(), self.height+1) self.image_up.show() self.add(self.image_up) if not img_element_down is None: img_element_down = img_element_down.childNodes self.image_down = themeMgr.get_texture("image_up", self, element = img_element_down) self.image_down.set_opacity(180) self.image_down.set_position( self.width-self.image_down.get_width()-self.image_up.get_width(), self.height+1) self.image_down.show() self.add(self.image_down) if not img_element_selector_bar is None: img_element_selector_bar = img_element_selector_bar.childNodes self.selector_bar = themeMgr.get_texture("selector_bar", parent=self, element=img_element_selector_bar) self.selector_bar.y_offset = self.selector_bar.get_y() self.selector_bar.set_width(self.width) self.selector_bar.show() self.selector_bar.set_opacity(0) self.add(self.selector_bar) #Get the item background img img_element_item = themeMgr.find_element(img_element, "id", "inactive_background") if not img_element_item is None: img_element_item = img_element_item.childNodes self.inactive_item_background = themeMgr.get_texture("inactive_background", self, element = img_element_item) #self.displayMax = math.floor(self.height / height) #For the moment, the roll_point_x is just the ends of the list self.roll_point_min = 1 self.roll_point_max = self.displayMax-1 self.displaySize = self.displayMax - self.displayMin #This will default to true if not set in the theme if self.use_clip: self.display_group.set_clip(0, 0, self.width, self.height) return True def on_key_press_event (self, event): self.input_queue.input(event) return self.timeline def add_item(self, itemLabel, newItem=None): if self.score.is_playing(): self.backlog.append(itemLabel) return self.score.remove_all() if len(self.items) == 0: #self.displayMax = self.height / self.label_height label_width = 0 #This has to go hear for layering purposes if self.display_group.get_parent() is None: self.add(self.display_group) if newItem is None: newItem = QueueItem(self.font_string, self, label=itemLabel) newItem.set_background(clutter.CloneTexture(self.inactive_item_background), self.item_width_percent, self.item_height_percent) item_height = newItem.get_height() if self.orientation == self.ORIENTATION_TOP: item_y = 0 elif self.orientation == self.ORIENTATION_BOTTOM: running_y = int(self.height + (item_height/2)) for item in self.items: running_y += (item.get_height()) item_y = int(running_y) newItem.show() newItem.set_position(0, item_y) newItem.set_opacity(0) self.items.append(newItem) self.item_group.add(newItem) #Setup behaviours timeline1 = clutter.Timeline(20,30) alpha1 = clutter.Alpha(timeline1, clutter.ramp_inc_func) self.behaviours = [] if self.orientation == self.ORIENTATION_TOP: #All items must be moved down by the new items height for item in self.items: knots = (\ (item.get_x(), item.get_y()),\ (item.get_x(), int(item.get_y()+item_height) )\ ) tmp_behaviour = clutter.BehaviourPath(alpha1, knots) tmp_behaviour.apply(item) self.behaviours.append(tmp_behaviour) elif self.orientation == self.ORIENTATION_BOTTOM: knots = (\ (self.item_group.get_x(), self.item_group.get_y()),\ (self.item_group.get_x(), int(self.item_group.get_y()-item_height))\ ) tmp_behaviour = clutter.BehaviourPath(alpha1, knots) tmp_behaviour.apply(self.item_group) self.behaviours.append(tmp_behaviour) timeline2 = clutter.Timeline(20,30) alpha2 = clutter.Alpha(timeline2, clutter.ramp_inc_func) self.behaviour_opacity_newitem = clutter.BehaviourOpacity(opacity_start=0, opacity_end=255, alpha=alpha2) self.behaviour_opacity_newitem.apply(newItem) self.score.append(timeline1) self.score.append(timeline2, timeline1) self.score.start() #timeline1.start() return newItem def flush_backlog(self, data): if len(self.backlog) > 0: nextLabel = self.backlog.pop() self.add_item(nextLabel) #Removes all items from the list def clear(self): timeline = clutter.Timeline(15,30) timeline.connect("completed", self.reset_item_group) alpha = clutter.Alpha(timeline, clutter.ramp_inc_func) self.behaviour_opacity = clutter.BehaviourOpacity(opacity_start=255, opacity_end=0, alpha=alpha) self.behaviour_opacity.apply(self.item_group) timeline.start() def reset_item_group(self, data): for item in self.items: self.item_group.remove(item) item = None self.items = [] self.selected = 0 self.item_group.set_position(0, 0) self.item_group.set_opacity(255) self.behaviour_opacity = None def display(self): if self.displayMax > len(self.items): self.displayMax = len(self.items) self.show() def move_selection(self, direction, timeline=None): if direction == self.DIRECTION_DOWN: #Check if we're at the last item in the list if (self.selected) == (len(self.items)-1): return else: self.selected += 1 elif direction == self.DIRECTION_UP: #Check if we're at the first / last item in the list if (self.selected) == 0: return else: self.selected -= 1 if timeline is None: self.timeline = clutter.Timeline (self.frames, self.fps) else: self.timeline = timeline alpha = clutter.Alpha(self.timeline, clutter.ramp_inc_func) self.input_queue.set_timeline(self.timeline) #This horrible loop does all the scaling #This includes, the selected item and the ones on either side of it for i in range(len(self.items)): if i == self.selected: #Currently selected item self.items[i].scaleLabel(ListItem.SCALE_FULL, self.timeline) elif (i == self.selected-1) and (i >= self.displayMin): #Item above the selected self.items[i].scaleLabel(ListItem.SCALE_MEDIUM, self.timeline) elif (i == self.selected+1) and (i <= self.displayMax-1): #Item below the selected self.items[i].scaleLabel(ListItem.SCALE_MEDIUM, self.timeline) #elif (i < self.displayMin) or (i > self.displayMax): #Item is off screen # self.items[i].scaleLabel(ListItem.SCALE_OFFSCREEN, self.timeline) else: #All other items self.items[i].scaleLabel(ListItem.SCALE_NONE, self.timeline) #Check we're at the top of the viewable list if (self.selected < self.roll_point_min) and (self.displayMin > 0): #If yes, move the menu, leave the selection bar where is #self.rollList( self.items[self.displayMin-1], self.items[self.displayMax+1], self.DIRECTION_UP, self.timeline) self.rollList( self.DIRECTION_UP, self.timeline) #Check if we're at the bottom of the viewable list elif (self.selected >= self.roll_point_max) and (self.displayMax < (len(self.items))): #self.rollList( self.items[self.displayMax+1], self.items[self.displayMin-1], self.DIRECTION_DOWN, self.timeline) self.rollList( self.DIRECTION_DOWN, self.timeline) if not self.selector_bar is None: #move the selector bar abs_item = self.selected - self.displayMin abs_y = abs_item * self.item_height + self.selector_bar.y_offset knots = (\ (self.selector_bar.get_x(), self.selector_bar.get_y()), (self.selector_bar.get_x(), abs_y) ) self.behaviour_path_bar = clutter.BehaviourPath(knots=knots, alpha=alpha) self.behaviour_path_bar.apply(self.selector_bar) #self.selector_bar().selectItem(self.menuItems[self.selected], self.timeline) self.timeline.start() def move_up(self): self.move_selection(self.DIRECTION_UP) def move_down(self): self.move_selection(self.DIRECTION_DOWN) def select_first(self, frames=1, fps=75, timeline=None): if self.input_queue.is_in_queue(): "ERROR: Timeline should NOT be playing here!" return if timeline is None:self.timeline = clutter.Timeline(frames, fps) else: self.timeline = timeline alpha = clutter.Alpha(self.timeline, clutter.ramp_inc_func) self.input_queue.set_timeline(self.timeline) self.selected = 0 for i in range(0,len(self.items)): if i == 0: self.items[i].scaleLabel(ListItem.SCALE_FULL, self.timeline) elif i == 1: self.items[i].scaleLabel(ListItem.SCALE_MEDIUM, self.timeline) else: self.items[i].scaleLabel(ListItem.SCALE_NONE, self.timeline) #Show the selector bar if not self.selector_bar is None: self.behaviour_opacity = clutter.BehaviourOpacity(opacity_start=0, opacity_end=255, alpha=alpha) self.behaviour_opacity.apply(self.selector_bar) """ #move the selector bar abs_y = self.selector_bar.y_offset knots = (\ (self.selector_bar.get_x(), self.selector_bar.get_y()), (self.selector_bar.get_x(), abs_y) ) self.behaviour_path_bar = clutter.BehaviourPath(knots=knots, alpha=alpha) self.behaviour_path_bar.apply(self.selector_bar) """ #Timeline only gets started if it was created in this function if timeline is None: self.timeline.start() #Just calls the above function with different arguements to result in the first item being selected in a 'prettier' manner def select_first_elegant(self, timeline=None): self.select_first(frames=self.frames, fps=self.fps, timeline=timeline) def select_none(self, frames = 1, fps = 75): self.timeline = clutter.Timeline(frames, fps) alpha = clutter.Alpha(self.timeline, clutter.ramp_inc_func) self.input_queue.set_timeline(self.timeline) self.selected = None for i in range(0,len(self.items)): self.items[i].scaleLabel(ListItem.SCALE_NONE, self.timeline) #Hide the selector bar if not self.selector_bar is None: self.behaviour_opacity = clutter.BehaviourOpacity(opacity_start=255, opacity_end=0, alpha=alpha) self.behaviour_opacity.apply(self.selector_bar) self.selected = 0 self.timeline.start() def select_none_elegant(self): self.select_none(self.frames, self.fps) #When the menu needs to display a new item from the top or bottom, it rolls # The distance the menu moves is the distance (in pixels) between the incoming item and the selector bar def rollList(self, direction, timeline): (group_x, group_y) = self.item_group.get_position() if direction == self.DIRECTION_DOWN: #Then the incoming item is below the selector bar gap = self.item_height * -1 self.displayMin += 1 self.displayMax += 1 self.roll_point_min += 1 self.roll_point_max += 1 """ outgoing_item = self.items[self.displayMin-1] incoming_item = self.items[self.displayMax+1] """ else: #Then the incoming item is above the selector bar gap = self.item_height self.displayMin -= 1 self.displayMax -= 1 self.roll_point_min -=1 self.roll_point_max -=1 """ incoming_item = self.items[self.displayMin-1] outgoing_item = self.items[self.displayMax+1] """ #print "Gap: " + str(gap) new_y = (group_y + gap) knots = (\ (group_x, group_y),\ (group_x, new_y )\ ) alpha = clutter.Alpha(timeline, clutter.ramp_inc_func) self.behaviour_path = clutter.BehaviourPath(alpha, knots) #self.behaviour_opacity_outgoing = clutter.BehaviourOpacity(opacity_start=outgoing_item.get_opacity(), opacity_end=0, alpha=alpha) #self.behaviour_opacity_incoming = clutter.BehaviourOpacity(opacity_start=0, opacity_end=outgoing_item.get_opacity(), alpha=alpha) self.behaviour_path.apply(self.item_group) self.behaviour_path.apply(self.background_group) """ self.behaviour_opacity_outgoing.apply(outgoing_item) #The incoming opacity behaviour is only used if the incomingItem is NOT the currently selected one if not self.items[self.selected] == incoming_item: self.behaviour_opacity_incoming.apply(incoming_item) #self.behaviour_opacity_incoming.apply(incomingMenutem) """ def get_current_item(self, offset=0): selection = self.selected + offset if (selection < 0) or (selection >= len(self.items)): return None return self.items[self.selected+offset] import gobject class QueueItem(clutter.Group): #Setup signals __gsignals__ = { "selected": ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, []), "deselected": ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, []) } STATE_SELECTED, STATE_NEXT, STATE_UNSELECTED, STATE_OFFSCREEN = range(4) #Default values for zoom and opacity opacity_step_full = 255 opacity_step_medium = 135 opacity_step_none = 50 def __init__ (self, font, label_queue, label=""): clutter.Group.__init__ (self) self.set_anchor_point_from_gravity(clutter.GRAVITY_NORTH) #self.set_anchor_point_from_gravity(clutter.GRAVITY_CENTER) self.background = None self.label = clutter.Label() #Takes the scale and opacity values from a label list, if given self.opacity_step_full = label_queue.opacityStep0 self.opacity_step_medium = label_queue.opacityStep1 self.opacity_step_none = label_queue.opacityStep2 self.label_queue = label_queue self.label.set_width(label_queue.width) #setup the label/s self.add(self.label) self.label.show() self.label.set_font_name(font) self.label.set_text(label) self.color = clutter.Color(0xff, 0xff, 0xff, 0xdd) self.label.set_color(self.color) self.currentOpacity = 255 self.data = label #By default the items data is simply its label self.label.set_line_wrap(True) #Text is actually scaled down in 'regular' position so that it doesn't get jaggies when zoomed in self.currentOpacity = self.opacity_step_medium self.set_anchor_point_from_gravity(clutter.GRAVITY_WEST) self.set_opacity(self.currentOpacity) def set_selection_level(self, level, timeline): #Default values (Just in case) opacityTo = 255 STATE_SELECTED, STATE_NEXT, STATE_UNSELECTED, STATE_OFFSCREEN if level == STATE_SELECTED: opacityTo = self.opacity_step_full self.emit("selected") elif level == STATE_NEXT: opacityTo = self.opacity_step_medium self.emit("deselected") elif level == STATE_UNSELECTED: opacityTo = self.opacity_step_none elif level == STATE_OFFSCREEN: opacityTo = 0 #Do a check for any actual changes. If there's no change, just return without applying any behaviours if (opacityTo == self.currentOpacity): return None alpha = clutter.Alpha(timeline, clutter.ramp_inc_func) self.behaviourOpacity = clutter.BehaviourOpacity(opacity_start=self.currentOpacity, opacity_end=opacityTo, alpha=alpha) self.behaviourOpacity.apply(self) self.currentOpacity = opacityTo def set_background(self, texture, width_percent, height_percent): if self.background is None: texture.set_width(int(self.label_queue.width * width_percent)) texture.set_height(int(self.label.get_height() * height_percent)) else: texture.set_width(self.background.get_width()) texture.set_height(self.background.get_height()) self.remove(self.background) self.background = texture label_x = (self.background.get_width() - self.label.get_width()) / 2 label_y = (self.background.get_height() - self.label.get_height()) / 2 self.label.set_position(label_x, label_y) self.add(self.background) self.lower_child(self.label, self.background) self.background.show() def set_data(self, data): self.data = data def get_data(self): return self.data def get_menu(self): return self.menu