Mercurial > enso_core
changeset 9:1d38d095bd32
Added enso.ui.quasimode.layout.
author | Atul Varma <varmaa@toolness.com> |
---|---|
date | Fri, 22 Feb 2008 07:25:29 -0600 |
parents | 5283b9fedbbe |
children | 01bd04cb9ba8 |
files | TODO enso/ui/quasimode/layout.py |
diffstat | 2 files changed, 399 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- a/TODO Fri Feb 22 07:17:15 2008 -0600 +++ b/TODO Fri Feb 22 07:25:29 2008 -0600 @@ -14,3 +14,5 @@ * the setQuasimodeKey() and setModal() methods in enso.ui.quasimode are very odd; see if we can eliminate them. +* Lots of constants are in enso.ui.quasimode.layout that might + best be moved to enso.config.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/enso/ui/quasimode/layout.py Fri Feb 22 07:25:29 2008 -0600 @@ -0,0 +1,397 @@ +# 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.quasimode.layout +# +# ---------------------------------------------------------------------------- + +""" + Classes for laying out the Quasimode's transparent display. +""" + +# ---------------------------------------------------------------------------- +# Imports +# ---------------------------------------------------------------------------- + +from enso import graphics +from enso.graphics import xmltextlayout +from enso.graphics.measurement import pointsToPixels, pixelsToPoints +from enso.ui.graphics import getTextSize +from enso.utils.xml import escapeXml + +from enso.config import MAX_SUGGESTIONS + + +# ---------------------------------------------------------------------------- +# Layout Constants +# ---------------------------------------------------------------------------- + +# Constants determining the "rag-smoothing" process. The delta is how +# close two rags need to be to require "smoothing" (i.e., widening one +# to match the other), and the max cycles is the maximum number of +# times to "smooth" the rags. +RAG_DELTA = 5 +MAX_CYCLES = 8 + +# Left and right margins (in points) +L_MARGIN = 5 +R_MARGIN = 7 + +# Top and bottom margins, in proportion to the relevant font size; i.e., +# for a line with font size 20 and a top margin factor of .2, the top margin +# will be 4 (in whatever units). +TOP_MARGIN_FACTOR = .3 +BOTTOM_MARGIN_FACTOR = .20 + +# A useful factor for determining the total height of the line, i.e., +# the height of a given line is the global height factor times the font +# size. +HEIGHT_FACTOR = 1 + TOP_MARGIN_FACTOR + BOTTOM_MARGIN_FACTOR + +# Colors +WHITE = "#ffffff" +DESIGNER_GREEN = "#9fbe57" +DARK_GREEN = "#7f9845" +BLACK = "#000000" + +# Add alpha values to get transparent backgrounds. +DESCRIPTION_BACKGROUND_COLOR = DESIGNER_GREEN + "cc" +MAIN_BACKGROUND_COLOR = BLACK + "d8" + +SMALL_SCALE = [ 12, 18, 24 ] +LARGE_SCALE = [ 24, 28, 32, 36, 40, 44, 48 ] +DESCRIPTION_SCALE = SMALL_SCALE +AUTOCOMPLETE_SCALE = LARGE_SCALE +SUGGESTION_SCALE = SMALL_SCALE + + +# ---------------------------------------------------------------------------- +# Style Registries +# ---------------------------------------------------------------------------- + +def _newLineStyleRegistry(): + """ + Creates a new style registry for laying out one of the quasimode's + text lines. + """ + + styles = xmltextlayout.StyleRegistry() + styles.add( + "document", + font_family = "Gentium", + font_style = "normal", + max_lines = "1", + ) + styles.add( + "line", + text_align = "left", + color = WHITE, + margin_top = "0pt", + margin_bottom = "0pt", + ) + styles.add( + "help", + font_style = "italic", + color = "#999999", + ) + styles.add( "ins" ) + styles.add( "alt" ) + return styles + + +_AUTOCOMPLETE_STYLES = _newLineStyleRegistry() +_SUGGESTION_STYLES = _newLineStyleRegistry() +_DESCRIPTION_STYLES = _newLineStyleRegistry() +_DESCRIPTION_STYLES.update( "ins", color = DESIGNER_GREEN ) +_DESCRIPTION_STYLES.update( "alt", color = BLACK ) + +XML_ALIASES = xmltextlayout.XmlMarkupTagAliases() +XML_ALIASES.add( "line", baseElement = "block" ) +XML_ALIASES.add( "ins", baseElement = "inline" ) +XML_ALIASES.add( "alt", baseElement = "inline" ) +XML_ALIASES.add( "help", baseElement = "inline" ) + +def _updateStyleSizes( styles, size ): + """ + Updates all size-related style elements to those suggested + when the font is of 'size' points. + + styles should be a style registry. + """ + + width = pixelsToPoints( graphics.getDesktopSize()[0] ) + styles.update( + "document", + font_size = "%fpt" % size, + width = "%fpt" % width, + margin_top = "%fpt" % (TOP_MARGIN_FACTOR * size), + margin_bottom = "%fpt" % (BOTTOM_MARGIN_FACTOR *size), + line_height = "%fpt" % size, + ) + + +def _updateSuggestionColors( styles, active ): + """ + Sets the color scheme in styles ( a style registry ) + to the correct one for active or inactive suggestions, + depending on the value of active ( a boolean ). + """ + + if active: + styles.update( "line", color = WHITE ) + styles.update( "ins", color = DARK_GREEN ) + styles.update( "alt", color = BLACK ) + else: + styles.update( "line", color = DESIGNER_GREEN ) + styles.update( "ins", color = DARK_GREEN ) + styles.update( "alt", color = BLACK ) + + +def _updateStyles( styles, scale, size ): + """ + Updates size and ellipsification styling information for + the style registry 'styles', based on 'size' (a font size + in points) and 'scale' (a list of usable font sizes in points). + """ + + _updateStyleSizes( styles, size ) + if size == scale[0]: + # We're at the smallest possible size. Ellispify if needed. + styles.update( "document", ellipsify = "true" ) + else: + styles.update( "document", ellipsify = "false" ) + return styles + +def retrieveDescriptionStyles( size = DESCRIPTION_SCALE[-1] ): + """ + LONGTERM TODO: Document this. + """ + + return _updateStyles( _DESCRIPTION_STYLES, DESCRIPTION_SCALE, size ) + + +def retrieveAutocompleteStyles( active = True, size = LARGE_SCALE[-1] ): + """ + LONGTERM TODO: Document this. + """ + + styles = _updateStyles( _AUTOCOMPLETE_STYLES, AUTOCOMPLETE_SCALE, size ) + _updateSuggestionColors( styles, active ) + return styles + + +def retrieveSuggestionStyles( active = True, size = SMALL_SCALE[-1] ): + """ + LONGTERM TODO: Document this. + """ + + styles = _updateStyles( _SUGGESTION_STYLES, SUGGESTION_SCALE, size ) + _updateSuggestionColors( styles, active ) + return styles + + +def layoutXmlLine( xmlData, styles, scale ): + """ + Performs a layout of a line using xmlData and styles, doing + its best to display all of the text of xmlData at the largest + size allowed by scale (a list of font sizes). If the text will + not fit even at the smallest size of scale, then ellipsifies + the text at that size. + """ + + document = None + for size in reversed( scale ): + try: + _updateStyles( styles, scale, size ) + document = xmltextlayout.xmlMarkupToDocument( + xmlData, + styles, + XML_ALIASES, + ) + usedSize = size + break + except: + # NOTE: If the error is fundamental (not size-related), + # then it will be raised again below + pass + if document == None: + # no size above worked; use the smallest size + _updateStyles( styles, scale, scale[0] ) + document = xmltextlayout.xmlMarkupToDocument( + xmlData, + styles, + XML_ALIASES, + ) + usedSize = scale[0] + document.shrinkOffset = scale[-1] - usedSize + return document + + +# ---------------------------------------------------------------------------- +# Layout Classes +# ---------------------------------------------------------------------------- + +class QuasimodeLayout: + """ + Class for calculating and storing layout metrics of the quasimode + window. + """ + + LINE_XML = "<document><line>%s</line></document>" + + def __init__( self, quasimode ): + """ + Computes and stores the layout metrics for the quasimode. + """ + + self.newLines = self.__newCreateLines( quasimode ) + self.__newSmoothRags() + self.__newRoundCorners() + self.__setBackgroundColors() + + def __newCreateLines( self, quasimode ): + """ + LONGTERM TODO: Document this. + """ + + lines = [] + + suggestionList = quasimode.getSuggestionList() + description = suggestionList.getDescription() + description = escapeXml( description ) + suggestions = suggestionList.getSuggestions() + activeIndex = suggestionList.getActiveIndex() + + lines.append( layoutXmlLine( + xmlData = self.LINE_XML % description, + styles = retrieveDescriptionStyles(), + scale = DESCRIPTION_SCALE, + ) ) + + if len(suggestions[0].toXml()) == 0: + text = suggestions[0].getSource() + text = escapeXml( text ) + else: + text = suggestions[0].toXml() + + lines.append( layoutXmlLine( + xmlData = self.LINE_XML % text, + styles = retrieveAutocompleteStyles( active = (activeIndex==0) ), + scale = AUTOCOMPLETE_SCALE, + ) ) + + for index in range( 1, len(suggestions) ): + isActive = (activeIndex==index) + lines.append( layoutXmlLine( + xmlData = self.LINE_XML % suggestions[index].toXml(), + styles = retrieveSuggestionStyles( active = isActive ), + scale = SUGGESTION_SCALE, + ) ) + + return lines + + + def __setBackgroundColors( self ): + """ + LONGTERM TODO: Document this. + """ + + self.newLines[0].background = \ + xmltextlayout.colorHashToRgba( DESCRIPTION_BACKGROUND_COLOR ) + for i in range( 1, len(self.newLines) ): + self.newLines[i].background = \ + xmltextlayout.colorHashToRgba( MAIN_BACKGROUND_COLOR ) + + + def __newSmoothRags( self ): + """ + Uses the computed size metrics to smooth the rags of the + extended suggestions. + """ + + # To "smooth the rags" of the suggestion windows, we + # repeatedly check for adjacent lines that differ in width by + # less than a constant, and extend the smaller one to meet the + # larger one. + + # By doing this multiple times with a small constant rather + # than once or twice with a large constant, we get much better + # results. Only those lines that are nearly the same width + # get corrected, and in the case that multiple lines are near + # each other in size, the repetition causes a single smooth + # edge to emerge. + + # LONGTERM TODO:Ideally, perhaps, this algorithm should simply + # continue until all adjacent windows are either equal in + # width or have widths greater than the constant. + + def _computeWidth( doc ): + lines = [] + for b in doc.blocks: + lines.extend( b.lines ) + if len( lines ) == 0: + return 0 + return max( [ l.xMax for l in lines ] ) + + for l in self.newLines: + l.ragWidth = _computeWidth( l ) + + for i in range( MAX_CYCLES ): + widths = [ l.ragWidth for l in self.newLines ] + for i in range( len(widths) - 1 ): + if abs(widths[i]-widths[i+1]) < RAG_DELTA: + # If the widths of two adjacent windows are near + # each other, then set both to be the width of the + # wider one. + outsideWidth = max( widths[i], widths[i+1] ) + self.newLines[i].ragWidth = outsideWidth + self.newLines[i+1].ragWidth = outsideWidth + + + def __newRoundCorners( self ): + """ + Sets all the appropriate corners to be rounded. + """ + + for l in self.newLines: + l.roundLowerRight = False + l.roundUpperRight = False + + lines = self.newLines + for i in range( len(lines)-1 ): + if lines[i+1].ragWidth < lines[i].ragWidth: + lines[i].roundLowerRight = True + + for i in range( len(lines)-1 ): + if lines[i].ragWidth < lines[i+1].ragWidth: + lines[i+1].roundUpperRight = True + + lines[-1].roundLowerRight = True + self.newLines = lines