changeset 17:981fc94ec6d0

Added enso.ui.messages.miniwindows.
author Atul Varma <varmaa@toolness.com>
date Fri, 22 Feb 2008 16:32:02 -0600
parents 34545dfacdc8
children 09b7a34603c0
files TODO enso/config.py enso/ui/messages/__init__.py enso/ui/messages/miniwindows.py
diffstat 4 files changed, 473 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- a/TODO	Fri Feb 22 16:04:29 2008 -0600
+++ b/TODO	Fri Feb 22 16:32:02 2008 -0600
@@ -20,3 +20,7 @@
 * There are some constants in enso.ui.messages.primarywindow
   that we may want to move to enso.config.  Also consider moving
   the styles for document, p, caption tags, etc.
+
+* Consider moving some constants in enso.ui.messages.miniwindows too.
+
+* Consider turning LONGTERM TODOs in source code into TODOs.
--- a/enso/config.py	Fri Feb 22 16:04:29 2008 -0600
+++ b/enso/config.py	Fri Feb 22 16:32:02 2008 -0600
@@ -43,3 +43,8 @@
 
 # Message XML for the Splash message shown when Enso first loads.
 OPENING_MSG_XML = "<p>Welcome to Enso.</p>"
+
+# Message XML displayed when the mouse hovers over a mini message.
+MINI_MSG_HELP_XML = "<p>The <command>hide mini messages</command>" \
+    " and <command>put</command> commands control" \
+    " these mini-messages.</p>"
--- a/enso/ui/messages/__init__.py	Fri Feb 22 16:04:29 2008 -0600
+++ b/enso/ui/messages/__init__.py	Fri Feb 22 16:32:02 2008 -0600
@@ -423,14 +423,16 @@
     global messageManager
 
     if primaryMsgWindClass == None:
-        import PrimaryWindow
-        _TheMessageManager.primaryMsgWindClass = PrimaryWindow.PrimaryMsgWind
+        from enso.ui.messages import primarywindow
+
+        _TheMessageManager.primaryMsgWindClass = primarywindow.PrimaryMsgWind
     else:
         _TheMessageManager.primaryMsgWindClass = primaryMsgWindClass
 
     if miniMsgWindClass == None:
-        import MiniWindows
-        _TheMessageManager.miniMsgWindClass = MiniWindows.MiniMessageQueue
+        from enso.ui.messages import miniwindows
+
+        _TheMessageManager.miniMsgWindClass = miniwindows.MiniMessageQueue
     else:
         _TheMessageManager.miniMsgWindClass = miniMsgWindClass
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/enso/ui/messages/miniwindows.py	Fri Feb 22 16:32:02 2008 -0600
@@ -0,0 +1,458 @@
+# 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.miniwindows
+#
+# ----------------------------------------------------------------------------
+
+"""
+    Implements the mini message windows.
+
+    Two parts:
+     - the "mini message queue" for managing all mini messages.
+     - the "mini message window" for displaying each mini message.
+"""
+
+# ----------------------------------------------------------------------------
+# Imports
+# ----------------------------------------------------------------------------
+
+import cairo
+
+from enso import config
+from enso import graphics
+from enso.graphics.transparentwindow import TransparentWindow
+from enso.graphics.measurement import pointsToPixels, pixelsToPoints
+from enso.graphics.measurement import inchesToPoints
+from enso.ui.graphics import UPPER_LEFT, drawRoundedRect
+from enso.ui import events
+from enso.ui.messages.windows import MessageWindow, computeWidth
+from enso.ui.messages.primarywindow import layoutMessageXml, splitContent
+from enso.ui.messages import Message
+
+
+# ----------------------------------------------------------------------------
+# Constants
+# ----------------------------------------------------------------------------
+
+MINI_WIND_SIZE = 256, 70
+MINI_WIND_SIZE = [ pixelsToPoints( pixSize ) for pixSize in MINI_WIND_SIZE ]
+MINI_MARGIN = pixelsToPoints( 10 )
+MINI_SCALE = [ 10, 12, 14 ]
+MINI_BG_COLOR = [ .62, .75, .34, .85 ]
+
+
+# ----------------------------------------------------------------------------
+# Mini Message Queue
+# ----------------------------------------------------------------------------
+
+class MiniMessageQueue:
+    """
+    A class for controlling the behavior and animatior of mini messages.
+
+    LONGTERM TODO: More documentation for this class and its methods.
+    """
+
+    # Mode/state constants; the class is always in one of these states.
+    EMPTY = 0
+    POLLING = 1
+    APPEARING = 2
+    VANISHING = 3
+
+    def __init__( self, msgMan ):
+        self.__msgManager = msgMan
+        self.__newMessages = []
+        self.__visibleMessages = []
+
+        self.__isPolling = False
+
+        self.__status = self.EMPTY
+        self.__changingIndex = None
+        self.__hidingAll = False
+
+        self.__mouseoverIndex = None
+        self.__helpWindow = None
+        self.__mousePos = None
+        self.__mouseChanged = False
+
+    def hideAll( self ):
+        if self.__hidingAll:
+            return
+        else:
+            self.__hidingAll = True
+            self.__startPolling()
+
+    def addMessage( self, msg ):
+        if msg.isFinished():
+            return
+        else:
+            self.__newMessages.append( msg )
+            # Switch to polling to trigger the animation.
+            if self.__status == self.EMPTY:
+                self.__startPolling()
+
+    def onMouseMove( self, x, y ):
+        if self.__status != self.POLLING:
+            return
+
+        if self.__mousePos != (x,y):
+            self.__mousePos = (x,y)
+            self.__mouseChanged = True
+
+
+    def __onMouseMove( self ):
+        """
+        Checks whether x,y is inside any of the mini-windows.
+        """
+
+        if not self.__mouseChanged:
+            return
+
+        self.__mouseChanged = False
+        x, y = self.__mousePos
+
+        oldIndex = self.__mouseoverIndex
+        newIndex = None
+        for index in range( len(self.__visibleMessages) ):
+            miniWind = self.__visibleMessages[index]
+            size = miniWind.getSize()
+            size = [ pointsToPixels( i ) for i in size ]
+            pos = miniWind.getPos()
+            pos = [ pointsToPixels( i ) for i in pos ]
+            if ( x > pos[0] and x < (pos[0] + size[0]) ) \
+                   and ( y > pos[1] and y < (pos[1] + size[1]) ):
+                # The mouse is inside this miniWindow
+                if index == oldIndex:
+                    # Don't change the appearance; it's already
+                    # 'moused-over'.
+                    newIndex = oldIndex
+                    break
+                else:
+                    newIndex = index
+                    break
+
+        if newIndex != oldIndex and oldIndex != None:
+            # The mouse has changed.
+            miniWind = self.__visibleMessages[oldIndex]
+            miniWind._wind.setOpacity( 255 )
+            miniWind._wind.update()
+            self.__hideHelpMessage()
+        if newIndex != None:
+            miniWind = self.__visibleMessages[newIndex]
+            xPos, yPos = miniWind.getPos()
+            if newIndex == len( self.__visibleMessages ):
+                rounded = True
+            else:
+                rounded = False
+            self.__showHelpMessage( xPos, yPos, rounded )
+
+            miniWind._wind.setOpacity( 0 )
+            miniWind._wind.update()
+            
+        self.__mouseoverIndex = newIndex
+
+
+    def onTick( self, msPassed ):
+        if self.__status == self.POLLING:
+            self.__onMouseMove()
+            
+            if len( self.__visibleMessages ) == 0 \
+                   and len( self.__newMessages ) == 0:
+                # There are no messages to poll for!
+                self.__stopPolling()
+            elif len( self.__newMessages ) != 0:
+                self.__startAppearing( self.__newMessages.pop( 0 ) )
+            elif self.__hidingAll:
+                if len( self.__visibleMessages ) > 0:
+                    self.__startVanishing( len(self.__visibleMessages)-1 )
+            else:
+                for index in range( len(self.__visibleMessages) ):
+                    if self.__visibleMessages[index].message.isFinished():
+                        self.__startVanishing( index )
+        elif self.__status == self.APPEARING:
+            # Update the appearing animation.
+            self.__onAppearingTick( msPassed )
+        elif self.__status == self.VANISHING:
+            # Update the appearing animation.
+            self.__onVanishingTick( msPassed )
+        else:
+            # LONGTERM TODO: Decide whether this should raise an assertion
+            # error, or just set the status to polling.
+            raise Exception( "What's going on!?" )
+
+
+    def __showHelpMessage( self, xPos, yPos, rounded ):
+        if self.__helpWindow == None:
+            msgXml = config.MINI_MSG_HELP_XML
+            msg = Message( fullXml = msgXml, isPrimary = False,
+                           isMini = True )
+            newWindow = MiniMessageWindow( msg, xPos, yPos )
+            self.__helpWindow = newWindow
+        else:
+            self.__helpWindow.setPos( xPos, yPos )
+
+        self.__helpWindow._wind.setOpacity( 255 )
+        if rounded:
+            self.__helpWindow.roundTopLeftCorner()
+        else:
+            self.__helpWindow.unroundTopLeftCorner()
+        #self.__helpWindow._wind.update()
+
+
+    def __hideHelpMessage( self ):
+        self.__helpWindow.hide()
+        
+
+    def __roundTopWindow( self ):
+        for msg in self.__visibleMessages[:-1]:
+            msg.unroundTopLeftCorner()
+        if len( self.__visibleMessages ) > 0:
+            topMsg = self.__visibleMessages[-1]
+            topMsg.roundTopLeftCorner()
+
+    def __startPolling( self ):
+        self.__status = self.POLLING
+
+        if self.__isPolling:
+            return
+        else:
+            self.__isPolling = True
+
+            evtMgr = events.eventManager
+            evtMgr.registerResponder( self.onTick, "timer" )
+            evtMgr.registerResponder( self.onMouseMove, "mousemove" )
+
+    def __stopPolling( self ):
+        assert self.__status == self.POLLING
+        assert self.__isPolling
+
+        self.__isPolling = False
+        self.__hidingAll = False
+        evtMgr = events.eventManager
+        evtMgr.removeResponder( self.onTick )
+        evtMgr.removeResponder( self.onMouseMove )
+        self.__status = self.EMPTY
+
+    def __startAppearing( self, msg ):
+        xPos = pixelsToPoints( graphics.getDesktopSize()[0] )
+        xPos -= MINI_WIND_SIZE[0]
+
+        yPos = pixelsToPoints( graphics.getDesktopSize()[1] )
+        # Move up for each visible message, including this one.
+        numVisible = len( self.__visibleMessages ) + 1
+        yPos -= ( MINI_WIND_SIZE[1] * numVisible )
+        
+        # TODO: Add this code back in at some point, when
+        # the getStartBarRect() function (or some equivalent)
+        # has been added.
+
+        #taskBarPos, taskBarSize = graphics.getStartBarRect()
+        #if taskBarPos[1] != 0:
+        #    # Startbar is on bottom.
+        #    yPos -= pixelsToPoints(taskBarSize[1])
+        #if taskBarPos[0] > 0:
+        #    # Startbar is on the right.
+        #    xPos -= pixelsToPoints(taskBarSize[0])
+
+        newWindow = MiniMessageWindow( msg, xPos, yPos )
+        self.__visibleMessages.append( newWindow )
+        self.__changingIndex = len(self.__visibleMessages) - 1
+        self.__status = self.APPEARING
+        self.__roundTopWindow()
+
+    def __stopAppearing( self ):
+        self.__changingIndex = None
+        self.__startPolling()
+
+    def __startVanishing( self, index ):
+        self.__changingIndex = index
+        if self.__changingIndex == self.__mouseoverIndex:
+            miniWind = self.__visibleMessages[self.__changingIndex]
+            miniWind._wind.setOpacity( 255 )
+            miniWind._wind.update()
+            self.__hideHelpMessage()
+            
+        self.__status = self.VANISHING
+
+    def __stopVanishing( self ):
+        self.__visibleMessages.pop( self.__changingIndex )
+        if self.__mouseoverIndex != None:
+            if len( self.__visibleMessages ) == 0:
+                self.__mouseoverIndex = None
+            elif self.__changingIndex < self.__mouseoverIndex:
+                self.__mouseoverIndex = max( 0, self.__mouseoverIndex-1 )
+            elif self.__changingIndex == self.__mouseoverIndex:
+                self.__mouseoverIndex = None
+        self.__changingIndex = None
+        self.__startPolling()
+        self.__roundTopWindow()
+        self.__msgManager.onMiniMessageFinished()
+
+    def __onAppearingTick( self, msPassed ):
+        unusedArgs( msPassed )
+
+        fracPer = 0.1
+        msg = self.__visibleMessages[ self.__changingIndex ]
+        if msg.isFinishedAppearing:
+            self.__stopAppearing()
+            return
+        else:
+            msg.fadeIn( fracPer )
+
+    def __onVanishingTick( self, msPassed ):
+        unusedArgs( msPassed )
+
+        distancePer = pixelsToPoints( 1 )
+        msg = self.__visibleMessages[ self.__changingIndex ]
+        if msg.isFinishedVanishing:
+            self.__stopVanishing()
+            return
+        else:
+            msg.slideOut( distancePer )
+            if self.__changingIndex != len( self.__visibleMessages ) - 1:
+                for i in range( self.__changingIndex + 1,
+                                len( self.__visibleMessages) ):
+                    self.__visibleMessages[i].slideDown( distancePer )
+
+
+# ----------------------------------------------------------------------------
+# Generic Message Window
+# ----------------------------------------------------------------------------
+
+class MiniMessageWindow( MessageWindow ):
+    """
+    LONGTERM TODO: More documentation for this class and its methods.
+    """
+        
+    def __init__( self, msg, xPos, yPos ):
+        MessageWindow.__init__( self, MINI_WIND_SIZE )
+        self.__isRounded = False
+        self.__draw( msg, xPos, yPos )
+        self.isFinishedVanishing = False
+        self.isFinishedAppearing = False
+        self.message = msg
+        self._wind.setOpacity( 0 )
+
+    def roundTopLeftCorner( self ):
+        self.clearWindow()
+        self.__isRounded = True
+        self.__draw( self.message, *self.getPos() )
+        self._wind.update()
+
+    def unroundTopLeftCorner( self ):
+        self.clearWindow()
+        self.__isRounded = False
+        self.__draw( self.message, *self.getPos() )
+        self._wind.update()
+
+    def slideDown( self, distance ):
+        xPos, yPos = self.getPos()
+        yPos += distance
+        self.setPos( xPos, yPos )
+        self._wind.update()
+
+    def slideOut( self, distance ):
+        if self.isFinishedVanishing:
+            return
+        width, height = self.getSize()
+        xPos, yPos = self.getPos()
+        if height-distance < 1:
+            yPos += height
+            height = 1
+            self.isFinishedVanishing = True
+        else:
+            yPos += distance
+            height -= distance
+        self.setPos( xPos, yPos )
+        self.setSize( width, height )
+        self._wind.update()
+
+    def fadeIn( self, fraction ):
+        currFrac = self._wind.getOpacity() / 255.
+        currFrac = min( fraction + currFrac, 1 )
+        if currFrac == 1:
+            self.isFinishedAppearing = True
+            return
+        self._wind.setOpacity( int(currFrac*255) )
+        self._wind.update()
+
+    def __draw( self, msg, xPos, yPos ):
+        width, height = MINI_WIND_SIZE
+        self.setSize( width, height )
+
+        self.setPos( xPos, yPos )
+
+        docSize = width - 2*MINI_MARGIN, height - 2*MINI_MARGIN
+        doc = self.__layout( msg, docSize[0], docSize[1] )
+
+        afterWidth = computeWidth( doc )
+        afterHeight = doc.height
+
+        xPos = ( width - afterWidth ) / 2
+        yPos = ( height - afterHeight ) / 2
+        
+        cr = self._context
+        if self.__isRounded:
+            corners = [UPPER_LEFT]
+        else:
+            corners = []
+            
+        cr.set_source_rgba( *MINI_BG_COLOR )
+        drawRoundedRect(
+            context = cr,
+            rect = ( 0, 0, width, height),
+            softenedCorners = corners,
+            )
+        cr.fill_preserve()
+
+        doc.draw( xPos, yPos, cr )
+        
+            
+    def __layout( self, msg, width, height ):
+        text = msg.getMiniXml()
+        text = "<document>%s</document>" % text
+        for size in reversed( MINI_SCALE[1:] ):
+            try:
+                doc = layoutMessageXml( xmlMarkup = text,
+                                        width = width,
+                                        size = size,
+                                        height = height, )
+                return doc
+            except:
+                # LONGTERM TODO: Lookup actual errors.
+                pass
+
+        doc = layoutMessageXml( xmlMarkup = text,
+                                width = width,
+                                size = size,
+                                height = height,
+                                ellipsify = "true",
+                                )
+        return doc