# Sketch - A Python-based interactive drawing program # Copyright (C) 1998, 1999, 2000 by Bernhard Herzog # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Library General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Library General Public License for more details. # # You should have received a copy of the GNU Library General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from app.conf import const CHANGED = const.CHANGED from app.events.connector import Publisher from app import CreateListUndo, UndoAfter, NullUndo, SketchInternalError, _ from app.events.warn import pdebug, INTERNAL from pattern import SolidPattern, EmptyPattern from color import StandardColors, StandardCMYKColors from blend import Blend class Style(Publisher): is_dynamic = 0 name = '' def __init__(self, name = '', duplicate = None, **kw): if duplicate is not None: self.__dict__ = duplicate.__dict__.copy() if hasattr(self, 'fill_pattern'): self.fill_pattern = self.fill_pattern.Copy() if hasattr(self, 'line_pattern'): self.line_pattern = self.line_pattern.Copy() else: if name: self.name = name for key, value in kw.items(): setattr(self, key, value) def SetProperty(self, prop, value): dict = self.__dict__ if dict.has_key(prop): undo = (self.SetProperty, prop, dict[prop]) else: undo = (self.DelProperty, prop) if prop == 'fill_pattern' or prop == 'line_pattern': value = value.Copy() dict[prop] = value self.issue(CHANGED, self) return undo def DelProperty(self, prop): undo = (self.SetProperty, prop, getattr(self, prop)) delattr(self, prop) self.issue(CHANGED, self) return undo def Duplicate(self): if self.is_dynamic: return self return self.__class__(duplicate = self) def Copy(self): return self.__class__(duplicate = self) def Name(self): return self.name def SetName(self, name): undo = self.SetName, self.name self.name = name return undo def AsDynamicStyle(self): result = self.Copy() result.is_dynamic = 1 return result def AsUndynamicStyle(self): result = self.Copy() if self.is_dynamic: del result.is_dynamic del result.name return result def SaveToFile(self, file): if self.is_dynamic: file.DynamicStyle(self) def IsEmpty(self): return not self.__dict__ def FillStyle(pattern): return Style(fill_pattern = pattern) EmptyFillStyle = Style(fill_pattern = EmptyPattern) def LineStyle(color = None, width = 1, cap = const.CapButt, join = const.JoinMiter, dashes = None, arrow1 = None, arrow2 = None): return Style(line_pattern = SolidPattern(color), line_width = width, line_cap = cap, line_join = join, line_dashes = dashes, line_arrow1 = arrow1, line_arrow2 = arrow2) SolidLine = LineStyle EmptyLineStyle = Style(line_pattern = EmptyPattern) class PropertyStack: update_cache = 1 def __init__(self, base = None, duplicate = None): if duplicate is not None: self.stack = [] for layer in duplicate.stack: self.stack.append(layer.Duplicate()) else: if base is None: base = factory_defaults.Duplicate() self.stack = [base] def __getattr__(self, attr): if self.update_cache: cache = self.__dict__ stack = self.stack[:] stack.reverse() for layer in stack: cache.update(layer.__dict__) self.update_cache = 0 try: return self.__dict__[attr] except KeyError: raise AttributeError, attr def _clear_cache(self): self.__dict__ = {'stack' : self.stack} return (self._clear_cache,) def prop_layer(self, prop): # return property layer containing PROP for item in self.stack: if hasattr(item, prop): return item # we should never reach this... raise SketchInternalError('unknown graphics property "%s"' % prop) def set_property(self, prop, value): layer = self.prop_layer(prop) if layer.is_dynamic: if self.stack[0].is_dynamic: layer = Style() setattr(layer, prop, value) return self.add_layer(layer) else: layer = self.stack[0] return layer.SetProperty(prop, value) def SetProperty(self, **kw): stack = self.stack undo = [] append = undo.append if len(stack) == 1 and not stack[0].is_dynamic: set = stack[0].SetProperty for prop, value in kw.items(): append(set(prop, value)) else: set = self.set_property for prop, value in kw.items(): append(set(prop, value)) if len(self.stack) > 1: undo_stack = (self.set_stack, self.stack[:]) if self.delete_shadowed_layers(): undo.append(undo_stack) undo = CreateListUndo(undo) undo = (UndoAfter, undo, self._clear_cache()) return undo def set_stack(self, stack): undo = (self.set_stack, self.stack) self.stack = stack self._clear_cache() return undo def delete_shadowed_layers(self): # check if some styles are completely hidden now stack = self.stack layers = [] dict = {'name':1, 'is_dynamic':0} dict.update(stack[0].__dict__) length = len(dict) for layer in stack[1:]: dict.update(layer.__dict__) if length != len(dict): layers.append(layer) length = len(dict) length = len(stack) stack[1:] = layers return length != len(stack) def add_layer(self, layer): undo = (self.set_stack, self.stack[:]) self.stack.insert(0, layer) return undo load_AddStyle = add_layer def AddStyle(self, style): if style.is_dynamic: undo = self.add_layer(style) self.delete_shadowed_layers() self._clear_cache() return undo else: return apply(self.SetProperty, (), style.__dict__) def HasFill(self): return self.fill_pattern is not EmptyPattern def IsAlgorithmicFill(self): return self.fill_pattern.is_procedural def ExecuteFill(self, device, rect = None): self.fill_pattern.Execute(device, rect) def HasLine(self): return self.line_pattern is not EmptyPattern def IsAlgorithmicLine(self): return self.line_pattern.is_procedural def ExecuteLine(self, device, rect = None): line_pattern = self.line_pattern if line_pattern is not EmptyPattern: line_pattern.Execute(device, rect) device.SetLineAttributes(self.line_width, self.line_cap, self.line_join, self.line_dashes) def HasFont(self): return self.font is not None def ObjectChanged(self, object): if object in self.stack: self._clear_cache() return 1 return 0 def ObjectRemoved(self, object): if object in self.stack: idx = self.stack.index(object) undo = (self.set_stack, self.stack[:]) self.stack[idx] = self.stack[idx].AsUndynamicStyle() pdebug('properties', 'made style undynamic') return undo return NullUndo def Untie(self): info = [] for i in range(len(self.stack)): style = self.stack[i] if style.is_dynamic: self.stack[i] = style.AsUndynamicStyle() info.append((i, style)) self._clear_cache() return info def Tie(self, document, info): for i, style in info: s = document.GetDynamicStyle(style.Name()) if s == style: self.stack[i] = s self._clear_cache() def Duplicate(self): return self.__class__(duplicate = self) grow_join = [5.240843064, 0.5, 0.5] grow_cap = [None, 0.5, 0.5, 0.70710678] def GrowAmount(self): return self.line_width * max(self.grow_cap[self.line_cap], self.grow_join[self.line_join]) def Blend(self, other, frac1, frac2): result = {} for prop, func in blend_functions: if func: result[prop] = func(getattr(self, prop), getattr(other, prop), frac1, frac2) else: result[prop] = getattr(self, prop) return PropertyStack(apply(Style, (), result)) def Transform(self, trafo, rects): # XXX hardcoding which properties may need to be transformed is # not really a good idea, but it's significantly faster. undo = NullUndo if len(self.stack) == 1 and not self.stack[0].is_dynamic: if self.fill_transform: pattern = self.fill_pattern if pattern.is_procedural: undo = pattern.Transform(trafo, rects) elif self.fill_transform: pattern = self.fill_pattern if pattern.is_procedural: pattern = pattern.Duplicate() if pattern.Transform(trafo, rects) is not NullUndo: undo = self.set_property('fill_pattern', pattern) if undo is not NullUndo: undo = (UndoAfter, undo, self._clear_cache()) return undo def CreateStyle(self, which_properties): properties = {} for prop in which_properties: if property_types[prop] == FontProperty and not self.HasFont(): continue properties[prop] = getattr(self, prop) return apply(Style, (), properties) def DynamicStyleNames(self): names = [] for style in self.stack: if style.is_dynamic: names.append(style.Name()) return names def condense(self): stack = self.stack last = stack[0] for style in stack[1:]: if not last.is_dynamic and not style.is_dynamic: dict = style.__dict__.copy() dict.update(last.__dict__) last.__dict__ = dict last = style length = len(stack) self.delete_shadowed_layers() def SaveToFile(self, file): file.Properties(self) class _EmptyProperties: def HasFill(self): return 0 HasLine = HasFill HasFont = HasFill def DynamicStyleNames(self): return [] EmptyProperties = _EmptyProperties() # # # factory_defaults = Style() default_graphics_style = None # set below default_text_style = None # set below blend_functions = [] property_names = [] property_titles = {} property_types = {} transform_properties = [] def blend_number(n1, n2, frac1, frac2): return n1 * frac1 + n2 * frac2 LineProperty = 1 FillProperty = 2 FontProperty = 3 OtherProperty = -1 def _set_defaults(prop, title, short_title, type, value, blend = None, transform = 0): factory_defaults.SetProperty(prop, value) property_names.append(prop) property_titles[prop] = (title, short_title) property_types[prop] = type blend_functions.append((prop, blend)) if transform: transform_properties.append(prop) black = StandardCMYKColors.black # XXX the default properties should be defined by the user. _set_defaults('fill_pattern', _("Fill Pattern"), _("Pattern"), FillProperty, EmptyPattern, blend = Blend, transform = 1) _set_defaults('fill_transform', _("Fill Transform Pattern"), _("Transform pattern"), FillProperty, 1) _set_defaults('line_pattern', _("Line Pattern"), _("Pattern"), LineProperty, SolidPattern(black), blend = Blend, transform = 1) _set_defaults('line_width', _("Line Width"), _("Width"), LineProperty, .283286 , blend = blend_number) _set_defaults('line_cap', _("Line Cap"), _("Cap"), LineProperty, const.CapButt) _set_defaults('line_join', _("Line Join"), _("Join"), LineProperty, const.JoinMiter) _set_defaults('line_dashes', _("Line Dashes"), _("Dashes"), LineProperty, ()) _set_defaults('line_arrow1', _("Line Arrow 1"), _("Arrow 1"), LineProperty, None) _set_defaults('line_arrow2', _("Line Arrow 2"), _("Arrow 2"), LineProperty, None) _set_defaults('font', _("Font"), _("Font"), FontProperty, None) _set_defaults('font_size', _("Font Size"), _("Size"), FontProperty, 12, blend = blend_number) factory_text_style = factory_defaults.Copy() factory_text_style.fill_pattern = SolidPattern(black) factory_text_style.line_pattern = EmptyPattern factory_text_style.font = None default_graphics_style = factory_defaults.Copy() default_text_style = factory_defaults.Copy() default_text_style.fill_pattern = SolidPattern(black) default_text_style.line_pattern = EmptyPattern default_text_style.font = None def FactoryTextStyle(): import app style = factory_text_style.Copy() if style.font is None: fontname = app.config.preferences.default_font style.font = app.GetFont(fontname) return style def DefaultTextProperties(): import app if default_text_style.font is None: fontname = app.config.preferences.default_font default_text_style.font = app.GetFont(fontname) return PropertyStack(base = default_text_style.Copy()) def DefaultGraphicsProperties(): return PropertyStack(base = default_graphics_style.Copy()) def set_graphics_defaults(kw): for key, value in kw.items(): if not property_types[key] == FontProperty: if key in ('fill_pattern', 'line_pattern'): value = value.Copy() default_graphics_style.SetProperty(key, value) def set_text_defaults(kw): for key, value in kw.items(): if not property_types[key] == LineProperty: if key == 'fill_pattern': value = value.Copy() default_text_style.SetProperty(key, value)