Mercurial > enso_core
view enso/ui/messages/primarywindow.py @ 16:34545dfacdc8
Added enso.ui.messages.primarywindow.
author | Atul Varma <varmaa@toolness.com> |
---|---|
date | Fri, 22 Feb 2008 16:04:29 -0600 |
parents | |
children | 09337777193c |
line wrap: on
line source
# Copyright (c) 2008, Humanized, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # 3. Neither the name of Enso nor the names of its contributors may # be used to endorse or promote products derived from this # software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY Humanized, Inc. ``AS IS'' AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL Humanized, Inc. BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # ---------------------------------------------------------------------------- # # enso.ui.messages.primarywindow # # ---------------------------------------------------------------------------- """ Implements the various Message windows. """ # ---------------------------------------------------------------------------- # Imports # ---------------------------------------------------------------------------- import logging from enso import graphics from enso.graphics import xmltextlayout from enso.graphics.measurement import pointsToPixels, pixelsToPoints from enso.graphics.measurement import inchesToPoints from enso.ui.graphics import drawRoundedRect, ALL_CORNERS from enso.utils.xml_tools import escapeXml from enso.ui import events from enso.ui.messages.windows import MessageWindow, computeWidth # ---------------------------------------------------------------------------- # Constants # ---------------------------------------------------------------------------- # Total length of time from dismissal to full fade-out (in ms) ANIMATION_TIME = 250 # Amount of time (in ms) to wait after primary message creation before # allowing dismissal events to trigger the animation WAIT_TIME = 80 # ---------------------------------------------------------------------------- # Visual Layout Constants # ---------------------------------------------------------------------------- # The width, height, and margins of primary messages. PRIM_MSG_WIDTH = inchesToPoints( 8 ) MAX_MSG_HEIGHT = inchesToPoints( 6 ) PRIM_MSG_MARGIN = inchesToPoints( .2 ) MSG_BGCOLOR = [ .2, .2, .2, .85 ] # Text sizes for main text and captions. SCALE = [ ( 20, 12 ), ( 24, 14 ), ( 30, 18 ), ] PRIM_TEXT_SIZE = 24 CAPTION_TEXT_SIZE = 16 LINE_SPACING = 1 # Distance between the main text block and the caption block. CAPTION_OFFSET = 0 # ---------------------------------------------------------------------------- # The Primary Message Window class # ---------------------------------------------------------------------------- class PrimaryMsgWind( MessageWindow ): """ Class that implements the primary message singleton's appearance and behavior. Essentially, setMessage() sets the current primary message. Immediately after the message is set, it is rendered, and the class goes into a brief wait cycle, so that user actions don't make the message disappear before it can be seen. After this wait cycle is completed, the singleton registers itself as a responder to dismissal events. When a dismissal event happens, the singleton animates the fading out of the primary message. Also, the singleton notifies the message manager that the primary message has been dismissed. """ def __init__( self, msgMan ): """ Initializes the PrimaryMessage singleton """ # Instantiate the underlying MessageWindow to the # maxsize suggested by the module constants. width = min( PRIM_MSG_WIDTH, pixelsToPoints(graphics.getDesktopSize()[0])-1 ) height = min( MAX_MSG_HEIGHT, pixelsToPoints(graphics.getDesktopSize()[1])-1 ) maxSize = ( width, height ) MessageWindow.__init__( self, maxSize ) self.__msgManager = msgMan self.__msg = None self.__waiting = False self.__animating = False def setMessage( self, message ): """ Sets the current primary message to "message". """ if self.__msg != None: # If there already is a primary message, then "interupt" it: self.__interupt() # Set the current primary message, and draw it. self.__msg = message self.__drawMessage() # Now, set a time-responder to wait for a bit, so that the # user doesn't accidentally clear the message before it registers # as existing. self.__timeSinceCreated = 0 eventMan = events.eventManager eventMan.registerResponder( self.waitTick, "timer" ) self.__waiting = True def onDismissal( self ): """ Called on a dismissal event, to start the animation process and make sure the underlying message does what it needs to when it ceases being a primary message. """ self.__msgManager.onDismissal() eventMan = events.eventManager eventMan.removeResponder( self.onDismissal ) self.__timeSinceDismissal = 0 eventMan.registerResponder( self.animationTick, "timer" ) self.__animating = True def animationTick( self, msPassed ): """ Called on a timer event to animate the window's fadeout. """ self.__timeSinceDismissal += msPassed if self.__timeSinceDismissal > ANIMATION_TIME: self.__onAnimationFinished() return timeLeft = ANIMATION_TIME - self.__timeSinceDismissal frac = timeLeft / float(ANIMATION_TIME) opacity = int( 255*frac ) self._wind.setOpacity( opacity ) self._wind.update() def waitTick ( self, msPassed ): """ Called on a timer event, to give some time between the message appearing and when it can disappear. """ self.__timeSinceCreated += msPassed if self.__timeSinceCreated > WAIT_TIME: eventMan = events.eventManager eventMan.registerResponder( self.onDismissal, "dismissal" ) eventMan.removeResponder( self.waitTick ) self.__waiting = False # The following message may be used by system tests. logging.info( "newMessage: %s" % self.__msg.getPrimaryXml() ) def __position( self ): """ Centers the message window horizontally using the current size. """ desksize = graphics.getDesktopSize() # getDesktopSize() returns pixels; we work in points. desksize = [ pixelsToPoints(a) for a in desksize ] xPos = ( desksize[0] - self.getSize()[0] ) / 2 # Set the height based on the "maximum" height, so that the # message always appears at the same vertical offset from the # top of the screen. yPos = ( desksize[1] - self.getMaxSize()[1] ) / 2 self.setPos( xPos, yPos ) def __interupt( self ): """ "interupts" the current primary message, terminating its animation, and/or """ eventMan = events.eventManager if self.__msg != None: # If there's an old message, then we've got an # event responder registered: eventMan = events.eventManager if self.__waiting: eventMan.removeResponder( self.waitTick ) self.__waiting = False if self.__animating: eventMan.removeResponder( self.animationTick ) self.__animating = False else: eventMan.removeResponder( self.onDismissal ) if self.__waiting: eventMan.removeResponder( self.waitTick ) def __drawMessage( self ): """ Draws the current message to the underlying Cario context. """ # This function is the master drawing function; all layout and # rendering methods are called from here. text = self.__msg.getPrimaryXml() self.clearWindow() msgText, capText = splitContent( text ) width,height = self.getMaxSize() width -= 2*PRIM_MSG_MARGIN height -= 2*PRIM_MSG_MARGIN msgDoc, capDoc = self.__layoutText( msgText, capText, width, height ) width, height, msgPos, capPos = \ self.__layoutBlocks( msgDoc, capDoc ) # Set the window size and draw the outlining rectangle self.__setupBackground( width, height ) # Draw the text. msgDoc.draw( msgPos[0], msgPos[1], self._context ) if capDoc != None: capDoc.draw( capPos[0], capPos[1], self._context ) # Set the window opacity (which can be left at 0 by the animation) self._wind.setOpacity( 255 ) # Show and update the window. self.show() def __isOneLineMsg( self, msgDoc, capDoc ): """ Determines whether msgDoc and capDoc are both one line. """ numMsgLines = 0 for block in ( msgDoc.blocks ): numMsgLines += len( block.lines ) numCapLines = 0 for block in ( capDoc.blocks ): numCapLines += len( block.lines ) return (numCapLines == 1 and numMsgLines == 1) def __layoutText( self, msgText, capText, width, height ): """ Lays out msgText and capText into two seperate document objects. Returns a tuple: ( msgDoc, capDoc ) NOTE: capDoc can be None, if capText is None. """ root = "<document>%s</document>" for msgSize, capSize in reversed( SCALE[1:] ): try: msgDoc = layoutMessageXml( xmlMarkup = root % msgText, width = width, height = height, size = msgSize, ) if capText != None: capDoc = layoutMessageXml( xmlMarkup = root % capText, width = width, height = height - msgDoc.height, size = capSize ) else: capDoc = None return msgDoc, capDoc except: # LONGTERM TODO: Lookup exact error. pass # This time, ellipsify. msgSize, capSize = SCALE[0] msgDoc = layoutMessageXml( xmlMarkup = root % msgText, width = width, height = height * .8, size = msgSize, ellipsify = "true", ) if capText != None: capDoc = layoutMessageXml( xmlMarkup = root % capText, width = width, height = height * .2, size = capSize, ellipsify = "true", ) else: capDoc = None return msgDoc, capDoc def __setupBackground( self, width, height ): """ Given a text region of width and height, sets the size of the underlying window to be that plus margins, and draws a rounded background rectangle. """ width += (2*PRIM_MSG_MARGIN)-2 height += (2*PRIM_MSG_MARGIN)-2 width = int(width) height = int(height) assert width <= self.getMaxSize()[0], \ "width %s, self.getMaxSize()[0] %s" \ % (width, self.getMaxSize()[0]) self.setSize( width, height ) self.__position() cr = self._context drawRoundedRect( context = cr, rect = ( 0, 0, width, height ), softenedCorners = ALL_CORNERS, ) cr.set_source_rgba( *MSG_BGCOLOR ) cr.fill_preserve() def __layoutBlocks( self, messageDoc, captionDoc ): """ Determines how the documents messageDoc and captionDoc should be combined to form a complete message window. Returns a tuple: ( width, height, messagePosition, captionPosition ) """ capDoc, msgDoc = captionDoc, messageDoc if capDoc == None: width = computeWidth( msgDoc ) height = msgDoc.height msgPos = ( PRIM_MSG_MARGIN, PRIM_MSG_MARGIN ) capPos = None elif self.__isOneLineMsg( msgDoc, capDoc ): msgWidth = computeWidth( msgDoc ) capWidth = computeWidth( capDoc ) width = max( msgWidth, capWidth ) height = msgDoc.height + capDoc.height msgPos = ( PRIM_MSG_MARGIN + ( ( width - msgWidth ) / 2 ), PRIM_MSG_MARGIN ) capPos = ( PRIM_MSG_MARGIN + ( ( width - capWidth ) / 2 ), msgPos[1] + msgDoc.height ) else: msgWidth = computeWidth( msgDoc ) capWidth = computeWidth( capDoc ) width = max( msgWidth, capWidth ) height = msgDoc.height + capDoc.height msgPos = ( PRIM_MSG_MARGIN, PRIM_MSG_MARGIN ) capPos = ( width - capWidth + PRIM_MSG_MARGIN, msgPos[1] + msgDoc.height ) return width, height, msgPos, capPos def __onAnimationFinished( self ): """ Called when the animation is finished. """ if self.__animating: eventMan = events.eventManager eventMan.removeResponder( self.animationTick ) self.__animating = False self.hide() self.__msg = None self.__msgManager.onPrimaryMessageFinished() # ---------------------------------------------------------------------------- # Xml Layout # ---------------------------------------------------------------------------- # The master style registry for primary messages. _styles = xmltextlayout.StyleRegistry() _styles.add( "document", margin_top = "0.0pt", margin_bottom = "0.0pt", font_family = "Gentium", font_style = "normal", max_lines = "0", ellipsify = "false", text_align = "left", ) _styles.add( "p", color = "#ffffff", margin_top = "0pt", margin_bottom = "0pt", ) _styles.add( "caption", color = "#669900", margin_top = "%spt" % CAPTION_OFFSET, margin_bottom = "0pt", ) _styles.add( "command", color = "#669900", ) # The tag aliases for primary message XML. _tagAliases = xmltextlayout.XmlMarkupTagAliases() _tagAliases.add( "p", baseElement = "block" ) _tagAliases.add( "caption", baseElement = "block" ) _tagAliases.add( "command", baseElement = "inline" ) def layoutMessageXml( xmlMarkup, width, size, height, ellipsify="false", raiseLayoutExceptions=False ): """ Lays out the xmlMarkup in a block that is width wide. if raiseLayoutExceptions is False, then this function will suppress any exceptions raised when parsing xmlMarkup and replace it with a message that tells the end-user that the message was broken, providing the end-user with as much of the original message as possible. If raiseLayoutExceptions is True, however, any exceptions raised will be passed through to the caller. """ maxLines = int( height / (size*LINE_SPACING) ) _styles.update( "document", width = "%fpt" % width, line_height = "%spt" % int(size*LINE_SPACING), max_lines = maxLines, font_size = "%spt" % size, ellipsify = ellipsify, ) try: document = xmltextlayout.xmlMarkupToDocument( xmlMarkup, _styles, _tagAliases ) except Exception, e: if raiseLayoutExceptions: raise logging.warn( "Could not layout message text %s; got error %s" % ( xmlMarkup, e ) ) document = xmltextlayout.xmlMarkupToDocument( "<document><p>%s</p>%s</document>" % ( escapeXml( xmlMarkup.strip() ), "<caption>from a broken message</caption>" ), _styles, _tagAliases ) return document def splitContent( messageXml ): """ Splits messageXml into two parts: main, and caption. """ capLocation = messageXml.find( "<caption>" ) if capLocation == -1: return ( messageXml, None ) else: return ( messageXml[:capLocation], messageXml[capLocation:] )