diff -r 1460273f40ed -r 5743cbdff669 graphics/GraphicCommons.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/graphics/GraphicCommons.py Fri Sep 07 16:45:55 2012 +0200 @@ -0,0 +1,3269 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +#This file is part of PLCOpenEditor, a library implementing an IEC 61131-3 editor +#based on the plcopen standard. +# +#Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD +# +#See COPYING file for copyrights details. +# +#This library is free software; you can redistribute it and/or +#modify it under the terms of the GNU General Public +#License as published by the Free Software Foundation; either +#version 2.1 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 +#General Public License for more details. +# +#You should have received a copy of the GNU 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 + +import wx +from time import time as gettime +from math import * +from types import * +import datetime +from threading import Semaphore + +#------------------------------------------------------------------------------- +# Common constants +#------------------------------------------------------------------------------- + +""" +Definition of constants for dimensions of graphic elements +""" + +# FBD and SFC constants +MIN_MOVE = 5 # Minimum move before starting a element dragging +CONNECTOR_SIZE = 8 # Size of connectors +BLOCK_LINE_SIZE = 20 # Minimum size of each line in a block +HANDLE_SIZE = 6 # Size of the squares for handles +ANCHOR_DISTANCE = 5 # Distance where wire is automativally attached to a connector +POINT_RADIUS = 2 # Radius of the point of wire ends +MIN_SEGMENT_SIZE = 2 # Minimum size of the endling segments of a wire + +# LD constants +LD_LINE_SIZE = 40 # Distance between two lines in a ladder rung +LD_ELEMENT_SIZE = (21, 15) # Size (width, height) of a ladder element (contact or coil) +LD_WIRE_SIZE = 30 # Size of a wire between two contact +LD_WIRECOIL_SIZE = 70 # Size of a wire between a coil and a contact +LD_POWERRAIL_WIDTH = 3 # Width of a Powerrail +LD_OFFSET = (10, 10) # Distance (x, y) between each comment and rung of the ladder +LD_COMMENT_DEFAULTSIZE = (600, 40) # Size (width, height) of a comment box + +# SFC constants +SFC_STEP_DEFAULT_SIZE = (40, 30) # Default size of a SFC step +SFC_TRANSITION_SIZE = (20, 2) # Size of a SFC transition +SFC_DEFAULT_SEQUENCE_INTERVAL = 40 # Default size of the interval between two divergence branches +SFC_SIMULTANEOUS_SEQUENCE_EXTRA = 20 # Size of extra lines for simultaneous divergence and convergence +SFC_JUMP_SIZE = (12, 13) # Size of a SFC jump to step +SFC_WIRE_MIN_SIZE = 25 # Size of a wire between two elements +SFC_ACTION_MIN_SIZE = (100, 30) # Minimum size of an action block line + +# Type definition constants for graphic elements +[INPUT, OUTPUT, INOUT] = range(3) +[CONNECTOR, CONTINUATION] = range(2) +[LEFTRAIL, RIGHTRAIL] = range(2) +[CONTACT_NORMAL, CONTACT_REVERSE, CONTACT_RISING, CONTACT_FALLING] = range(4) +[COIL_NORMAL, COIL_REVERSE, COIL_SET, COIL_RESET, COIL_RISING, COIL_FALLING] = range(6) +[SELECTION_DIVERGENCE, SELECTION_CONVERGENCE, SIMULTANEOUS_DIVERGENCE, SIMULTANEOUS_CONVERGENCE] = range(4) + +# Constants for defining the type of dragging that has been selected +[HANDLE_MOVE, HANDLE_RESIZE, HANDLE_POINT, HANDLE_SEGMENT, HANDLE_CONNECTOR] = range(5) + +# List of value for resize handle that are valid +VALID_HANDLES = [(1,1), (1,2), (1,3), (2,3), (3,3), (3,2), (3,1), (2,1)] + +# Contants for defining the direction of a connector +[EAST, NORTH, WEST, SOUTH] = [(1,0), (0,-1), (-1,0), (0,1)] + +# Contants for defining which mode is selected for each view +[MODE_SELECTION, MODE_BLOCK, MODE_VARIABLE, MODE_CONNECTION, MODE_COMMENT, + MODE_COIL, MODE_CONTACT, MODE_POWERRAIL, MODE_INITIALSTEP, MODE_STEP, + MODE_TRANSITION, MODE_DIVERGENCE, MODE_JUMP, MODE_ACTION, MODE_MOTION] = range(15) + +# Contants for defining alignment types for graphic group +[ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT, ALIGN_TOP, ALIGN_MIDDLE, ALIGN_BOTTOM] = range(6) + +# Contants for defining which drawing mode is selected for app +[FREEDRAWING_MODE, DRIVENDRAWING_MODE] = [1, 2] + +# Color for Highlighting +HIGHLIGHTCOLOR = wx.CYAN + +# Define highlight types +ERROR_HIGHLIGHT = (wx.Colour(255, 255, 0), wx.RED) +SEARCH_RESULT_HIGHLIGHT = (wx.Colour(255, 165, 0), wx.WHITE) + +# Define highlight refresh inhibition period in second +REFRESH_HIGHLIGHT_PERIOD = 0.1 + +# Define tooltip wait for displaying period in second +TOOLTIP_WAIT_PERIOD = 0.5 + +HANDLE_CURSORS = { + (1, 1) : 2, + (3, 3) : 2, + (1, 3) : 3, + (3, 1) : 3, + (1, 2) : 4, + (3, 2) : 4, + (2, 1) : 5, + (2, 3) : 5 +} + +def round_scaling(x, n, constraint=0): + fraction = float(x) / float(n) + if constraint == - 1: + xround = int(fraction) + else: + xround = round(fraction) + if constraint == 1 and int(fraction) == xround: + xround += 1 + return xround * n + +""" +Basic vector operations for calculate wire points +""" + +# Create a vector from two points and define if vector must be normal +def vector(p1, p2, normal = True): + vector = (p2.x - p1.x, p2.y - p1.y) + if normal: + return normalize(vector) + return vector + +# Calculate the norm of a given vector +def norm(v): + return sqrt(v[0] * v[0] + v[1] * v[1]) + +# Normalize a given vector +def normalize(v): + v_norm = norm(v) + # Verifie if it is not a null vector + if v_norm > 0: + return (v[0] / v_norm, v[1] / v_norm) + else: + return v + +# Calculate the scalar product of two vectors +def is_null_vector(v): + return v == (0, 0) + +# Calculate the scalar product of two vectors +def add_vectors(v1, v2): + return (v1[0] + v2[0], v1[1] + v2[1]) + +# Calculate the scalar product of two vectors +def product(v1, v2): + return v1[0] * v2[0] + v1[1] * v2[1] + + +""" +Function that calculates the nearest point of the grid defined by scaling for the given point +""" + +def GetScaledEventPosition(event, dc, scaling): + pos = event.GetLogicalPosition(dc) + if scaling: + pos.x = round(float(pos.x) / float(scaling[0])) * scaling[0] + pos.y = round(float(pos.y) / float(scaling[1])) * scaling[1] + return pos + + +""" +Function that choose a direction during the wire points generation +""" + +def DirectionChoice(v_base, v_target, dir_target): + dir_product = product(v_base, v_target) + if dir_product < 0: + return (-v_base[0], -v_base[1]) + elif dir_product == 0 and product(v_base, dir_target) != 0: + return dir_target + return v_base + +SECOND = 1000000 +MINUTE = 60 * SECOND +HOUR = 60 * MINUTE +DAY = 24 * HOUR + +def generate_time(value): + microseconds = float(value.days * DAY + value.seconds * SECOND + value.microseconds) + negative = microseconds < 0 + microseconds = abs(microseconds) + data = "T#" + not_null = False + if negative: + data += "-" + for val, format in [(int(microseconds) / DAY, "%dd"), + ((int(microseconds) % DAY) / HOUR, "%dh"), + ((int(microseconds) % HOUR) / MINUTE, "%dm"), + ((int(microseconds) % MINUTE) / SECOND, "%ds")]: + if val > 0 or not_null: + data += format % val + not_null = True + data += "%gms" % (microseconds % SECOND / 1000.) + return data + +def generate_date(value): + base_date = datetime.datetime(1970, 1, 1) + date = base_date + value + return date.strftime("DATE#%Y-%m-%d") + +def generate_datetime(value): + base_date = datetime.datetime(1970, 1, 1) + date_time = base_date + value + return date_time.strftime("DT#%Y-%m-%d-%H:%M:%S.%f") + +def generate_timeofday(value): + microseconds = float(value.days * DAY + value.seconds * SECOND + value.microseconds) + negative = microseconds < 0 + microseconds = abs(microseconds) + data = "TOD#" + for val, format in [(int(microseconds) / HOUR, "%2.2d:"), + ((int(microseconds) % HOUR) / MINUTE, "%2.2d:"), + ((int(microseconds) % MINUTE) / SECOND, "%2.2d."), + (microseconds % SECOND, "%6.6d")]: + data += format % val + return data + +TYPE_TRANSLATOR = {"TIME": generate_time, + "DATE": generate_date, + "DT": generate_datetime, + "TOD": generate_timeofday} + +def MiterPen(colour, width=1, style=wx.SOLID): + pen = wx.Pen(colour, width, style) + pen.SetJoin(wx.JOIN_MITER) + pen.SetCap(wx.CAP_PROJECTING) + return pen + +#------------------------------------------------------------------------------- +# Debug Data Consumer Class +#------------------------------------------------------------------------------- + +class DebugDataConsumer: + + def __init__(self): + self.LastValue = None + self.Value = None + self.DataType = None + self.LastForced = False + self.Forced = False + self.Inhibited = False + + def Inhibit(self, inhibit): + self.Inhibited = inhibit + if not inhibit and self.LastValue is not None: + self.SetForced(self.LastForced) + self.SetValue(self.LastValue) + self.LastValue = None + + def SetDataType(self, data_type): + self.DataType = data_type + + def NewValue(self, tick, value, forced=False): + value = TYPE_TRANSLATOR.get(self.DataType, lambda x:x)(value) + if self.Inhibited: + self.LastValue = value + self.LastForced = forced + else: + self.SetForced(forced) + self.SetValue(value) + + def SetValue(self, value): + self.Value = value + + def GetValue(self): + return self.Value + + def SetForced(self, forced): + self.Forced = forced + + def IsForced(self): + return self.Forced + +#------------------------------------------------------------------------------- +# Debug Viewer Class +#------------------------------------------------------------------------------- + +REFRESH_PERIOD = 0.1 + +class DebugViewer: + + def __init__(self, producer, debug, register_tick=True): + self.DataProducer = None + self.Debug = debug + self.RegisterTick = register_tick + self.Inhibited = False + + self.DataConsumers = {} + + self.LastRefreshTime = gettime() + self.RefreshLock = Semaphore() + + self.RefreshTimer = wx.Timer(self, -1) + self.Bind(wx.EVT_TIMER, self.OnRefreshTimer, self.RefreshTimer) + + self.SetDataProducer(producer) + + def __del__(self): + self.DataProducer = None + self.DeleteDataConsumers() + self.RefreshTimer.Stop() + + def SetDataProducer(self, producer): + if self.RegisterTick and self.Debug: + if producer is not None: + producer.SubscribeDebugIECVariable("__tick__", self) + if self.DataProducer is not None: + self.DataProducer.UnsubscribeDebugIECVariable("__tick__", self) + self.DataProducer = producer + + def IsDebugging(self): + return self.Debug + + def Inhibit(self, inhibit): + for consumer, iec_path in self.DataConsumers.iteritems(): + consumer.Inhibit(inhibit) + self.Inhibited = inhibit + + def AddDataConsumer(self, iec_path, consumer): + if self.DataProducer is None: + return None + result = self.DataProducer.SubscribeDebugIECVariable(iec_path, consumer) + if result is not None and consumer != self: + self.DataConsumers[consumer] = iec_path + consumer.SetDataType(self.GetDataType(iec_path)) + return result + + def RemoveDataConsumer(self, consumer): + iec_path = self.DataConsumers.pop(consumer, None) + if iec_path is not None: + self.DataProducer.UnsubscribeDebugIECVariable(iec_path, consumer) + + def GetDataType(self, iec_path): + if self.DataProducer is not None: + return self.DataProducer.GetDebugIECVariableType(iec_path) + return None + + def ForceDataValue(self, iec_path, value): + if self.DataProducer is not None: + self.DataProducer.ForceDebugIECVariable(iec_path, value) + + def ReleaseDataValue(self, iec_path): + if self.DataProducer is not None: + self.DataProducer.ReleaseDebugIECVariable(iec_path) + + def DeleteDataConsumers(self): + if self.DataProducer is not None: + for consumer, iec_path in self.DataConsumers.iteritems(): + self.DataProducer.UnsubscribeDebugIECVariable(iec_path, consumer) + self.DataConsumers = {} + + def OnRefreshTimer(self, event): + self.RefreshNewData() + event.Skip() + + def NewDataAvailable(self, *args, **kwargs): + self.RefreshTimer.Stop() + if not self.Inhibited: + current_time = gettime() + if current_time - self.LastRefreshTime > REFRESH_PERIOD and self.RefreshLock.acquire(False): + self.LastRefreshTime = gettime() + self.Inhibit(True) + wx.CallAfter(self.RefreshViewOnNewData, *args, **kwargs) + + def RefreshViewOnNewData(self, *args, **kwargs): + if self: + self.RefreshNewData(*args, **kwargs) + self.RefreshTimer.Start(int(REFRESH_PERIOD * 1000), oneShot=True) + + def RefreshNewData(self, *args, **kwargs): + self.Inhibit(False) + self.RefreshLock.release() + +#------------------------------------------------------------------------------- +# Viewer Rubberband +#------------------------------------------------------------------------------- + +""" +Class that implements a rubberband +""" + +class RubberBand: + + # Create a rubberband by indicated on which window it must be drawn + def __init__(self, viewer): + self.Viewer = viewer + self.drawingSurface = viewer.Editor + self.Reset() + + # Method that initializes the internal attributes of the rubberband + def Reset(self): + self.startPoint = None + self.currentBox = None + self.lastBox = None + + # Method that return if a box is currently edited + def IsShown(self): + return self.currentBox != None + + # Method that returns the currently edited box + def GetCurrentExtent(self): + if self.currentBox is None: + return self.lastBox + return self.currentBox + + # Method called when a new box starts to be edited + def OnLeftDown(self, event, dc, scaling): + pos = GetScaledEventPosition(event, dc, scaling) + # Save the point for calculate the box position and size + self.startPoint = pos + self.currentBox = wx.Rect(pos.x, pos.y, 0, 0) + self.drawingSurface.SetCursor(wx.StockCursor(wx.CURSOR_CROSS)) + self.Redraw() + + # Method called when dragging with a box edited + def OnMotion(self, event, dc, scaling): + pos = GetScaledEventPosition(event, dc, scaling) + # Save the last position and size of the box for erasing it + self.lastBox = wx.Rect(self.currentBox.x, self.currentBox.y, self.currentBox.width, + self.currentBox.height) + # Calculate new position and size of the box + if pos.x >= self.startPoint.x: + self.currentBox.x = self.startPoint.x + self.currentBox.width = pos.x - self.startPoint.x + 1 + else: + self.currentBox.x = pos.x + self.currentBox.width = self.startPoint.x - pos.x + 1 + if pos.y >= self.startPoint.y: + self.currentBox.y = self.startPoint.y + self.currentBox.height = pos.y - self.startPoint.y + 1 + else: + self.currentBox.y = pos.y + self.currentBox.height = self.startPoint.y - pos.y + 1 + self.Redraw() + + # Method called when dragging is stopped + def OnLeftUp(self, event, dc, scaling): + self.drawingSurface.SetCursor(wx.NullCursor) + self.lastBox = self.currentBox + self.currentBox = None + self.Redraw() + + # Method that erase the last box and draw the new box + def Redraw(self, dc = None): + if dc is None: + dc = self.Viewer.GetLogicalDC() + scalex, scaley = dc.GetUserScale() + dc.SetUserScale(1, 1) + dc.SetPen(wx.Pen(wx.WHITE, 1, wx.DOT)) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.SetLogicalFunction(wx.XOR) + if self.lastBox: + # Erase last box + dc.DrawRectangle(self.lastBox.x * scalex, self.lastBox.y * scaley, + self.lastBox.width * scalex, self.lastBox.height * scaley) + if self.currentBox: + # Draw current box + dc.DrawRectangle(self.currentBox.x * scalex, self.currentBox.y * scaley, + self.currentBox.width * scalex, self.currentBox.height * scaley) + dc.SetUserScale(scalex, scaley) + + # Erase last box + def Erase(self, dc = None): + if dc is None: + dc = self.Viewer.GetLogicalDC() + scalex, scaley = dc.GetUserScale() + dc.SetUserScale(1, 1) + dc.SetPen(wx.Pen(wx.WHITE, 1, wx.DOT)) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.SetLogicalFunction(wx.XOR) + if self.lastBox: + dc.DrawRectangle(self.lastBox.x * scalex, self.lastBox.y * scaley, + self.lastBox.width * scalex, self.lastBox.height * scalex) + dc.SetUserScale(scalex, scaley) + + # Draw current box + def Draw(self, dc = None): + if dc is None: + dc = self.Viewer.GetLogicalDC() + scalex, scaley = dc.GetUserScale() + dc.SetUserScale(1, 1) + dc.SetPen(wx.Pen(wx.WHITE, 1, wx.DOT)) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.SetLogicalFunction(wx.XOR) + if self.currentBox: + # Draw current box + dc.DrawRectangle(self.currentBox.x * scalex, self.currentBox.y * scaley, + self.currentBox.width * scalex, self.currentBox.height * scaley) + dc.SetUserScale(scalex, scaley) + +#------------------------------------------------------------------------------- +# Viewer ToolTip +#------------------------------------------------------------------------------- + +""" +Class that implements a custom tool tip +""" + +if wx.Platform == '__WXMSW__': + faces = { 'times': 'Times New Roman', + 'mono' : 'Courier New', + 'helv' : 'Arial', + 'other': 'Comic Sans MS', + 'size' : 10, + } +else: + faces = { 'times': 'Times', + 'mono' : 'Courier', + 'helv' : 'Helvetica', + 'other': 'new century schoolbook', + 'size' : 12, + } + +TOOLTIP_MAX_CHARACTERS = 30 +TOOLTIP_MAX_LINE = 5 + +class ToolTip(wx.PopupWindow): + + def __init__(self, parent, tip): + wx.PopupWindow.__init__(self, parent) + + self.CurrentPosition = wx.Point(0, 0) + + self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) + self.SetTip(tip) + + self.Bind(wx.EVT_PAINT, self.OnPaint) + + def SetTip(self, tip): + lines = [] + for line in tip.splitlines(): + if line != "": + words = line.split() + new_line = words[0] + for word in words[1:]: + if len(new_line + " " + word) <= TOOLTIP_MAX_CHARACTERS: + new_line += " " + word + else: + lines.append(new_line) + new_line = word + lines.append(new_line) + else: + lines.append(line) + if len(lines) > TOOLTIP_MAX_LINE: + self.Tip = lines[:TOOLTIP_MAX_LINE] + if len(self.Tip[-1]) < TOOLTIP_MAX_CHARACTERS - 3: + self.Tip[-1] += "..." + else: + self.Tip[-1] = self.Tip[-1][:TOOLTIP_MAX_CHARACTERS - 3] + "..." + else: + self.Tip = lines + wx.CallAfter(self.RefreshTip) + + def MoveToolTip(self, pos): + self.CurrentPosition = pos + self.SetPosition(pos) + + def GetTipExtent(self): + max_width = 0 + max_height = 0 + for line in self.Tip: + w, h = self.GetTextExtent(line) + max_width = max(max_width, w) + max_height += h + return max_width, max_height + + def RefreshTip(self): + if self: + w, h = self.GetTipExtent() + self.SetSize(wx.Size(w + 4, h + 4)) + self.SetPosition(self.CurrentPosition) + self.Refresh() + + def OnPaint(self, event): + dc = wx.AutoBufferedPaintDC(self) + dc.Clear() + dc.SetPen(MiterPen(wx.BLACK)) + dc.SetBrush(wx.Brush(wx.Colour(255, 238, 170))) + dc.SetFont(wx.Font(faces["size"], wx.SWISS, wx.NORMAL, wx.NORMAL, faceName = faces["mono"])) + dc.BeginDrawing() + w, h = self.GetTipExtent() + dc.DrawRectangle(0, 0, w + 4, h + 4) + offset = 0 + for line in self.Tip: + dc.DrawText(line, 2, offset + 2) + w, h = dc.GetTextExtent(line) + offset += h + dc.EndDrawing() + event.Skip() + +#------------------------------------------------------------------------------- +# Helpers for highlighting text +#------------------------------------------------------------------------------- + +def AddHighlight(highlights, infos): + RemoveHighlight(highlights, infos) + highlights.append(infos) + +def RemoveHighlight(highlights, infos): + if infos in highlights: + highlights.remove(infos) + return True + return False + +def ClearHighlight(highlights, highlight_type=None): + if highlight_type is not None: + return [highlight for highlight in highlights if highlight[2] != highlight_type] + return [] + +def DrawHighlightedText(dc, text, highlights, x, y): + current_pen = dc.GetPen() + dc.SetPen(wx.TRANSPARENT_PEN) + for start, end, highlight_type in highlights: + dc.SetBrush(wx.Brush(highlight_type[0])) + offset_width, offset_height = dc.GetTextExtent(text[:start[1]]) + part = text[start[1]:end[1] + 1] + part_width, part_height = dc.GetTextExtent(part) + dc.DrawRectangle(x + offset_width, y, part_width, part_height) + dc.SetTextForeground(highlight_type[1]) + dc.DrawText(part, x + offset_width, y) + dc.SetPen(current_pen) + dc.SetTextForeground(wx.BLACK) + +#------------------------------------------------------------------------------- +# Graphic element base class +#------------------------------------------------------------------------------- + +""" +Class that implements a generic graphic element +""" + +class Graphic_Element: + + # Create a new graphic element + def __init__(self, parent, id = None): + self.Parent = parent + self.Id = id + self.oldPos = None + self.StartPos = None + self.CurrentDrag = None + self.Handle = (None,None) + self.Dragging = False + self.Selected = False + self.Highlighted = False + self.Pos = wx.Point(0, 0) + self.Size = wx.Size(0, 0) + self.BoundingBox = wx.Rect(0, 0, 0, 0) + self.Visible = False + self.ToolTip = None + self.ToolTipPos = None + self.ToolTipTimer = wx.Timer(self.Parent, -1) + self.Parent.Bind(wx.EVT_TIMER, self.OnToolTipTimer, self.ToolTipTimer) + + def __del__(self): + self.ToolTipTimer.Stop() + + def GetDefinition(self): + return [self.Id], [] + + def TestVisible(self, screen): + self.Visible = self.GetRedrawRect().Intersects(screen) + + def IsVisible(self): + return self.Visible + + def SpreadCurrent(self): + pass + + def GetConnectorTranslation(self, element): + return {} + + def FindNearestConnector(self, position, connectors): + distances = [] + for connector in connectors: + connector_pos = connector.GetRelPosition() + distances.append((sqrt((self.Pos.x + connector_pos.x - position.x) ** 2 + + (self.Pos.y + connector_pos.y - position.y) ** 2), + connector)) + distances.sort() + if len(distances) > 0: + return distances[0][1] + return None + + def IsOfType(self, type, reference): + return self.Parent.IsOfType(type, reference) + + def IsEndType(self, type): + return self.Parent.IsEndType(type) + + def GetDragging(self): + return self.Dragging + + # Make a clone of this element + def Clone(self, parent): + return Graphic_Element(parent, self.Id) + + # Changes the block position + def SetPosition(self, x, y): + self.Pos.x = x + self.Pos.y = y + self.RefreshConnected() + self.RefreshBoundingBox() + + # Returns the block position + def GetPosition(self): + return self.Pos.x, self.Pos.y + + # Changes the element size + def SetSize(self, width, height): + self.Size.SetWidth(width) + self.Size.SetHeight(height) + self.RefreshConnectors() + self.RefreshBoundingBox() + + # Returns the element size + def GetSize(self): + return self.Size.GetWidth(), self.Size.GetHeight() + + # Returns the minimum element size + def GetMinSize(self): + return 0, 0 + + # Refresh the element Bounding Box + def RefreshBoundingBox(self): + self.BoundingBox = wx.Rect(self.Pos.x, self.Pos.y, self.Size[0], self.Size[1]) + + # Refresh the element connectors position + def RefreshConnectors(self): + pass + + # Refresh the position of wires connected to element inputs and outputs + def RefreshConnected(self): + pass + + # Change the parent + def SetParent(self, parent): + self.Parent = parent + + # Override this method for defining the method to call for deleting this element + def Delete(self): + pass + + # Returns the Id + def GetId(self): + return self.Id + + # Returns if the point given is in the bounding box + def HitTest(self, pt, connectors=True): + if connectors: + rect = self.BoundingBox + else: + rect = wx.Rect(self.Pos.x, self.Pos.y, self.Size[0], self.Size[1]) + return rect.InsideXY(pt.x, pt.y) + + # Returns if the point given is in the bounding box + def IsInSelection(self, rect): + return rect.InsideXY(self.BoundingBox.x, self.BoundingBox.y) and rect.InsideXY(self.BoundingBox.x + self.BoundingBox.width, self.BoundingBox.y + self.BoundingBox.height) + + # Override this method for refreshing the bounding box + def RefreshBoundingBox(self): + pass + + # Returns the bounding box + def GetBoundingBox(self): + return self.BoundingBox + + # Returns the RedrawRect + def GetRedrawRect(self, movex = 0, movey = 0): + scalex, scaley = self.Parent.GetViewScale() + rect = wx.Rect() + rect.x = self.BoundingBox.x - int(HANDLE_SIZE / scalex) - 3 - abs(movex) + rect.y = self.BoundingBox.y - int(HANDLE_SIZE / scaley) - 3 - abs(movey) + rect.width = self.BoundingBox.width + 2 * (int(HANDLE_SIZE / scalex) + abs(movex) + 1) + 4 + rect.height = self.BoundingBox.height + 2 * (int(HANDLE_SIZE / scaley) + abs(movey) + 1) + 4 + return rect + + def Refresh(self, rect = None): + if self.Visible: + if rect is not None: + self.Parent.RefreshRect(self.Parent.GetScrolledRect(rect), False) + else: + self.Parent.RefreshRect(self.Parent.GetScrolledRect(self.GetRedrawRect()), False) + + # Change the variable that indicates if this element is selected + def SetSelected(self, selected): + self.Selected = selected + self.Refresh() + + # Change the variable that indicates if this element is highlighted + def SetHighlighted(self, highlighted): + self.Highlighted = highlighted + self.Refresh() + + # Test if the point is on a handle of this element + def TestHandle(self, event): + dc = self.Parent.GetLogicalDC() + scalex, scaley = dc.GetUserScale() + pos = event.GetPosition() + pt = wx.Point(*self.Parent.CalcUnscrolledPosition(pos.x, pos.y)) + + left = (self.BoundingBox.x - 2) * scalex - HANDLE_SIZE + center = (self.BoundingBox.x + self.BoundingBox.width / 2) * scalex - HANDLE_SIZE / 2 + right = (self.BoundingBox.x + self.BoundingBox.width + 2) * scalex + + top = (self.BoundingBox.y - 2) * scaley - HANDLE_SIZE + middle = (self.BoundingBox.y + self.BoundingBox.height / 2) * scaley - HANDLE_SIZE / 2 + bottom = (self.BoundingBox.y + self.BoundingBox.height + 2) * scaley + + extern_rect = wx.Rect(left, top, right + HANDLE_SIZE - left, bottom + HANDLE_SIZE - top) + intern_rect = wx.Rect(left + HANDLE_SIZE, top + HANDLE_SIZE, right - left - HANDLE_SIZE, bottom - top - HANDLE_SIZE) + + # Verify that this element is selected + if self.Selected and extern_rect.InsideXY(pt.x, pt.y) and not intern_rect.InsideXY(pt.x, pt.y): + # Find if point is on a handle horizontally + if left <= pt.x < left + HANDLE_SIZE: + handle_x = 1 + elif center <= pt.x < center + HANDLE_SIZE: + handle_x = 2 + elif right <= pt.x < right + HANDLE_SIZE: + handle_x = 3 + else: + handle_x = 0 + # Find if point is on a handle vertically + if top <= pt.y < top + HANDLE_SIZE: + handle_y = 1 + elif middle <= pt.y < middle + HANDLE_SIZE: + handle_y = 2 + elif bottom <= pt.y < bottom + HANDLE_SIZE: + handle_y = 3 + else: + handle_y = 0 + # Verify that the result is valid + if (handle_x, handle_y) in VALID_HANDLES: + return handle_x, handle_y + return 0, 0 + + # Method called when a LeftDown event have been generated + def OnLeftDown(self, event, dc, scaling): + pos = event.GetLogicalPosition(dc) + # Test if an handle have been clicked + handle = self.TestHandle(event) + # Find which type of handle have been clicked, + # Save a resize event and change the cursor + cursor = HANDLE_CURSORS.get(handle, 1) + wx.CallAfter(self.Parent.SetCurrentCursor, cursor) + if cursor > 1: + self.Handle = (HANDLE_RESIZE, handle) + else: + self.Handle = (HANDLE_MOVE, None) + self.SetSelected(False) + # Initializes the last position + self.oldPos = GetScaledEventPosition(event, dc, scaling) + self.StartPos = wx.Point(self.Pos.x, self.Pos.y) + self.CurrentDrag = wx.Point(0, 0) + + # Method called when a LeftUp event have been generated + def OnLeftUp(self, event, dc, scaling): + # If a dragging have been initiated + if self.Dragging and self.oldPos: + self.RefreshModel() + self.Parent.RefreshBuffer() + wx.CallAfter(self.Parent.SetCurrentCursor, 0) + self.SetSelected(True) + self.oldPos = None + + # Method called when a RightDown event have been generated + def OnRightDown(self, event, dc, scaling): + pass + + # Method called when a RightUp event have been generated + def OnRightUp(self, event, dc, scaling): + if self.Dragging and self.oldPos: + self.RefreshModel() + self.Parent.RefreshBuffer() + wx.CallAfter(self.Parent.SetCurrentCursor, 0) + self.SetSelected(True) + self.oldPos = None + if self.Parent.Debug: + self.Parent.PopupForceMenu() + + # Method called when a LeftDClick event have been generated + def OnLeftDClick(self, event, dc, scaling): + pass + + # Method called when a Motion event have been generated + def OnMotion(self, event, dc, scaling): + # If the cursor is dragging and the element have been clicked + if event.Dragging() and self.oldPos: + # Calculate the movement of cursor + pos = event.GetLogicalPosition(dc) + movex = pos.x - self.oldPos.x + movey = pos.y - self.oldPos.y + # If movement is greater than MIN_MOVE then a dragging is initiated + if not self.Dragging and (abs(movex) > MIN_MOVE or abs(movey) > MIN_MOVE): + self.Dragging = True + # If a dragging have been initiated, refreshes the element state + if self.Dragging: + dragx, dragy = self.ProcessDragging(movex, movey, event, scaling) + if event.ControlDown() and self.Handle[0] == HANDLE_MOVE: + self.oldPos.x = self.StartPos.x + self.CurrentDrag.x + self.oldPos.y = self.StartPos.y + self.CurrentDrag.y + else: + self.oldPos.x += dragx + self.oldPos.y += dragy + return dragx, dragy + return movex, movey + # If cursor just pass over the element, changes the cursor if it is on a handle + else: + pos = event.GetLogicalPosition(dc) + handle = self.TestHandle(event) + # Find which type of handle have been clicked, + # Save a resize event and change the cursor + cursor = HANDLE_CURSORS.get(handle, 0) + wx.CallAfter(self.Parent.SetCurrentCursor, cursor) + return 0, 0 + + # Moves the element + def Move(self, dx, dy, exclude = []): + self.Pos.x += max(-self.BoundingBox.x, dx) + self.Pos.y += max(-self.BoundingBox.y, dy) + self.RefreshConnected(exclude) + self.RefreshBoundingBox() + + # Resizes the element from position and size given + def Resize(self, x, y, width, height): + self.Move(x, y) + self.SetSize(width, height) + + # Moves and Resizes the element for fitting scaling + def AdjustToScaling(self, scaling): + if scaling is not None: + movex = round_scaling(self.Pos.x, scaling[0]) - self.Pos.x + movey = round_scaling(self.Pos.y, scaling[1]) - self.Pos.y + min_width, min_height = self.GetMinSize() + width = max(round_scaling(min_width, scaling[0], 1), + round_scaling(self.Size.width, scaling[0])) + height = max(round_scaling(min_height, scaling[1], 1), + round_scaling(self.Size.height, scaling[1])) + self.Resize(movex, movey, width, height) + return movex, movey + return 0, 0 + + # Refreshes the element state according to move defined and handle selected + def ProcessDragging(self, movex, movey, event, scaling, width_fac = 1, height_fac = 1): + handle_type, handle = self.Handle + # If it is a resize handle, calculate the values from resizing + if handle_type == HANDLE_RESIZE: + if scaling is not None: + scaling = (scaling[0] * width_fac, scaling[1] * height_fac) + x = y = start_x = start_y = 0 + width, height = start_width, start_height = self.GetSize() + if handle[0] == 1: + movex = max(-self.BoundingBox.x, movex) + if scaling is not None: + movex = -(round_scaling(width - movex, scaling[0]) - width) + x = movex + if event.ShiftDown(): + width -= 2 * movex + else: + width -= movex + elif handle[0] == 3: + if scaling is not None: + movex = round_scaling(width + movex, scaling[0]) - width + if event.ShiftDown(): + movex = min(self.BoundingBox.x, movex) + x = -movex + width += 2 * movex + else: + width += movex + if handle[1] == 1: + movey = max(-self.BoundingBox.y, movey) + if scaling is not None: + movey = -(round_scaling(height - movey, scaling[1]) - height) + y = movey + if event.ShiftDown(): + height -= 2 * movey + else: + height -= movey + elif handle[1] == 3: + if scaling is not None: + movey = round_scaling(height + movey, scaling[1]) - height + if event.ShiftDown(): + movey = min(self.BoundingBox.y, movey) + y = -movey + height += 2 * movey + else: + height += movey + # Verify that new size is not lesser than minimum + min_width, min_height = self.GetMinSize() + if handle[0] != 2 and (width >= min_width or width > self.Size[0]): + start_x = x + start_width = width + else: + movex = 0 + if handle[1] != 2 and (height >= min_height or height > self.Size[1]): + start_y = y + start_height = height + else: + movey = 0 + if movex != 0 or movey != 0: + self.Resize(start_x, start_y, start_width, start_height) + return movex, movey + # If it is a move handle, Move this element + elif handle_type == HANDLE_MOVE: + movex = max(-self.BoundingBox.x, movex) + movey = max(-self.BoundingBox.y, movey) + if scaling is not None: + movex = round_scaling(self.Pos.x + movex, scaling[0]) - self.Pos.x + movey = round_scaling(self.Pos.y + movey, scaling[1]) - self.Pos.y + if event.ControlDown(): + self.CurrentDrag.x = self.CurrentDrag.x + movex + self.CurrentDrag.y = self.CurrentDrag.y + movey + if abs(self.CurrentDrag.x) > abs(self.CurrentDrag.y): + movex = self.StartPos.x + self.CurrentDrag.x - self.Pos.x + movey = self.StartPos.y - self.Pos.y + else: + movex = self.StartPos.x - self.Pos.x + movey = self.StartPos.y + self.CurrentDrag.y - self.Pos.y + self.Move(movex, movey) + return movex, movey + return 0, 0 + + def OnToolTipTimer(self, event): + value = self.GetToolTipValue() + if value is not None and self.ToolTipPos is not None: + self.ToolTip = ToolTip(self.Parent, value) + self.ToolTip.MoveToolTip(self.ToolTipPos) + self.ToolTip.Show() + + def GetToolTipValue(self): + return None + + def CreateToolTip(self, pos): + value = self.GetToolTipValue() + if value is not None: + self.ToolTipPos = pos + self.ToolTipTimer.Start(int(TOOLTIP_WAIT_PERIOD * 1000), oneShot=True) + + def MoveToolTip(self, pos): + if self.ToolTip is not None: + self.ToolTip.MoveToolTip(pos) + elif self.ToolTipPos is not None: + self.ToolTipPos = pos + self.ToolTipTimer.Start(int(TOOLTIP_WAIT_PERIOD * 1000), oneShot=True) + + def ClearToolTip(self): + self.ToolTipTimer.Stop() + self.ToolTipPos = None + if self.ToolTip is not None: + self.ToolTip.Destroy() + self.ToolTip = None + + # Override this method for defining the method to call for adding an highlight to this element + def AddHighlight(self, infos, start, end, highlight_type): + pass + + # Override this method for defining the method to call for removing an highlight from this element + def RemoveHighlight(self, infos, start, end, highlight_type): + pass + + # Override this method for defining the method to call for removing all the highlights of one particular type from this element + def ClearHighlight(self, highlight_type=None): + pass + + # Override this method for defining the method to call for refreshing the model of this element + def RefreshModel(self, move=True): + pass + + # Draws the highlightment of this element if it is highlighted (can be overwritten) + def DrawHighlightment(self, dc): + scalex, scaley = dc.GetUserScale() + dc.SetUserScale(1, 1) + dc.SetPen(MiterPen(HIGHLIGHTCOLOR)) + dc.SetBrush(wx.Brush(HIGHLIGHTCOLOR)) + dc.SetLogicalFunction(wx.AND) + dc.DrawRectangle(int(round((self.Pos.x - 1) * scalex)) - 2, + int(round((self.Pos.y - 1) * scaley)) - 2, + int(round((self.Size.width + 3) * scalex)) + 5, + int(round((self.Size.height + 3) * scaley)) + 5) + dc.SetLogicalFunction(wx.COPY) + dc.SetUserScale(scalex, scaley) + + # Draws the handles of this element if it is selected + def Draw(self, dc): + if not getattr(dc, "printing", False): + if self.Highlighted: + self.DrawHighlightment(dc) + if self.Selected: + scalex, scaley = dc.GetUserScale() + dc.SetUserScale(1, 1) + dc.SetPen(MiterPen(wx.BLACK)) + dc.SetBrush(wx.BLACK_BRUSH) + + left = (self.BoundingBox.x - 2) * scalex - HANDLE_SIZE + center = (self.BoundingBox.x + self.BoundingBox.width / 2) * scalex - HANDLE_SIZE / 2 + right = (self.BoundingBox.x + self.BoundingBox.width + 2) * scalex + + top = (self.BoundingBox.y - 2) * scaley - HANDLE_SIZE + middle = (self.BoundingBox.y + self.BoundingBox.height / 2) * scaley - HANDLE_SIZE / 2 + bottom = (self.BoundingBox.y + self.BoundingBox.height + 2) * scaley + + for x, y in [(left, top), (center, top), (right, top), + (left, middle), (right, middle), + (left, bottom), (center, bottom), (right, bottom)]: + dc.DrawRectangle(x, y, HANDLE_SIZE, HANDLE_SIZE) + + dc.SetUserScale(scalex, scaley) + + +#------------------------------------------------------------------------------- +# Group of graphic elements +#------------------------------------------------------------------------------- + +""" +Class that implements a group of graphic elements +""" + +class Graphic_Group(Graphic_Element): + + # Create a new group of graphic elements + def __init__(self, parent): + Graphic_Element.__init__(self, parent) + self.Elements = [] + self.RefreshWireExclusion() + self.RefreshBoundingBox() + + # Destructor + def __del__(self): + self.Elements = [] + + def GetDefinition(self): + blocks = [] + wires = [] + for element in self.Elements: + block, wire = element.GetDefinition() + blocks.extend(block) + wires.extend(wire) + return blocks, wires + + # Make a clone of this element + def Clone(self, parent, pos = None): + group = Graphic_Group(parent) + connectors = {} + exclude_names = {} + wires = [] + if pos is not None: + dx, dy = pos.x - self.BoundingBox.x, pos.y - self.BoundingBox.y + for element in self.Elements: + if isinstance(element, Wire): + wires.append(element) + else: + if pos is not None: + x, y = element.GetPosition() + new_pos = wx.Point(x + dx, y + dy) + newid = parent.GetNewId() + if parent.IsNamedElement(element): + name = parent.GenerateNewName(element, exclude_names) + exclude_names[name.upper()] = True + new_element = element.Clone(parent, newid, name, pos = new_pos) + else: + new_element = element.Clone(parent, newid, pos = new_pos) + new_element.AdjustToScaling(parent.Scaling) + else: + new_element = element.Clone(parent) + connectors.update(element.GetConnectorTranslation(new_element)) + group.SelectElement(new_element) + for element in wires: + if pos is not None: + new_wire = element.Clone(parent, connectors, dx, dy) + else: + new_wire = element.Clone(parent, connectors) + if new_wire is not None: + if pos is not None: + parent.AddWire(new_wire) + group.SelectElement(new_wire) + if pos is not None: + for element in group.Elements: + if not isinstance(element, Wire): + parent.AddBlockInModel(element) + return group + + def CanAddBlocks(self, parent): + valid = True + for element in self.Elements: + if not isinstance(element, Wire): + valid &= parent.CanAddElement(element) + return valid + + def IsVisible(self): + for element in self.Elements: + if element.IsVisible(): + return True + return False + + # Refresh the list of wire excluded + def RefreshWireExclusion(self): + self.WireExcluded = [] + for element in self.Elements: + if isinstance(element, Wire): + startblock = element.StartConnected.GetParentBlock() + endblock = element.EndConnected.GetParentBlock() + if startblock in self.Elements and endblock in self.Elements: + self.WireExcluded.append(element) + + # Returns the RedrawRect + def GetRedrawRect(self, movex = 0, movey = 0): + rect = None + for element in self.Elements: + if rect is None: + rect = element.GetRedrawRect(movex, movey) + else: + rect = rect.Union(element.GetRedrawRect(movex, movey)) + return rect + + # Clean this group of elements + def Clean(self): + # Clean all the elements of the group + for element in self.Elements: + element.Clean() + + # Delete this group of elements + def Delete(self): + # Delete all the elements of the group + for element in self.Elements: + element.Delete() + self.WireExcluded = [] + + # Returns if the point given is in the bounding box of one of the elements of this group + def HitTest(self, pt, connectors=True): + result = False + for element in self.Elements: + result |= element.HitTest(pt, connectors) + return result + + # Returns if the element given is in this group + def IsElementIn(self, element): + return element in self.Elements + + # Change the elements of the group + def SetElements(self, elements): + self.Elements = elements + self.RefreshWireExclusion() + self.RefreshBoundingBox() + + # Returns the elements of the group + def GetElements(self): + return self.Elements + + # Align the group elements + def AlignElements(self, horizontally, vertically): + minx = self.BoundingBox.x + self.BoundingBox.width + miny = self.BoundingBox.y + self.BoundingBox.height + maxx = self.BoundingBox.x + maxy = self.BoundingBox.y + for element in self.Elements: + if not isinstance(element, Wire): + posx, posy = element.GetPosition() + width, height = element.GetSize() + minx = min(minx, posx) + miny = min(miny, posy) + maxx = max(maxx, posx + width) + maxy = max(maxy, posy + height) + for element in self.Elements: + if not isinstance(element, Wire): + posx, posy = element.GetPosition() + width, height = element.GetSize() + movex = movey = 0 + if horizontally == ALIGN_LEFT: + movex = minx - posx + elif horizontally == ALIGN_CENTER: + movex = (maxx + minx - width) / 2 - posx + elif horizontally == ALIGN_RIGHT: + movex = maxx - width - posx + if vertically == ALIGN_TOP: + movey = miny - posy + elif vertically == ALIGN_MIDDLE: + movey = (maxy + miny - height) / 2 - posy + elif vertically == ALIGN_BOTTOM: + movey = maxy - height - posy + if movex != 0 or movey != 0: + element.Move(movex, movey) + element.RefreshModel() + self.RefreshWireExclusion() + self.RefreshBoundingBox() + + # Remove or select the given element if it is or not in the group + def SelectElement(self, element): + if element in self.Elements: + self.Elements.remove(element) + else: + self.Elements.append(element) + self.RefreshWireExclusion() + self.RefreshBoundingBox() + + # Move this group of elements + def Move(self, movex, movey): + movex = max(-self.BoundingBox.x, movex) + movey = max(-self.BoundingBox.y, movey) + # Move all the elements of the group + for element in self.Elements: + if not isinstance(element, Wire): + element.Move(movex, movey, self.WireExcluded) + elif element in self.WireExcluded: + element.Move(movex, movey, True) + self.RefreshBoundingBox() + + # Refreshes the bounding box of this group of elements + def RefreshBoundingBox(self): + if len(self.Elements) > 0: + bbox = self.Elements[0].GetBoundingBox() + minx, miny = bbox.x, bbox.y + maxx = bbox.x + bbox.width + maxy = bbox.y + bbox.height + for element in self.Elements[1:]: + bbox = element.GetBoundingBox() + minx = min(minx, bbox.x) + miny = min(miny, bbox.y) + maxx = max(maxx, bbox.x + bbox.width) + maxy = max(maxy, bbox.y + bbox.height) + self.BoundingBox = wx.Rect(minx, miny, maxx - minx, maxy - miny) + else: + self.BoundingBox = wx.Rect(0, 0, 0, 0) + self.Pos = wx.Point(self.BoundingBox.x, self.BoundingBox.y) + self.Size = wx.Size(self.BoundingBox.width, self.BoundingBox.height) + + # Forbids to change the group position + def SetPosition(x, y): + pass + + # Returns the position of this group + def GetPosition(self): + return self.BoundingBox.x, self.BoundingBox.y + + # Forbids to change the group size + def SetSize(width, height): + pass + + # Returns the size of this group + def GetSize(self): + return self.BoundingBox.width, self.BoundingBox.height + + # Moves and Resizes the group elements for fitting scaling + def AdjustToScaling(self, scaling): + movex_max = movey_max = 0 + for element in self.Elements: + movex, movey = element.AdjustToScaling(scaling) + movex_max = max(movex_max, abs(movex)) + movey_max = max(movey_max, abs(movey)) + return movex_max, movey_max + + # Refreshes the group elements to move defined and handle selected + def ProcessDragging(self, movex, movey, event, scaling): + handle_type, handle = self.Handle + # If it is a move handle, Move this group elements + if handle_type == HANDLE_MOVE: + movex = max(-self.BoundingBox.x, movex) + movey = max(-self.BoundingBox.y, movey) + if scaling is not None: + movex = round_scaling(movex, scaling[0]) + movey = round_scaling(movey, scaling[1]) + if event.ControlDown(): + self.CurrentDrag.x = self.CurrentDrag.x + movex + self.CurrentDrag.y = self.CurrentDrag.y + movey + if abs(self.CurrentDrag.x) > abs(self.CurrentDrag.y): + movex = self.StartPos.x + self.CurrentDrag.x - self.Pos.x + movey = self.StartPos.y - self.Pos.y + else: + movex = self.StartPos.x - self.Pos.x + movey = self.StartPos.y + self.CurrentDrag.y - self.Pos.y + self.Move(movex, movey) + return movex, movey + return 0, 0 + + # Change the variable that indicates if this element is highlighted + def SetHighlighted(self, highlighted): + for element in self.Elements: + element.SetHighlighted(highlighted) + + def HighlightPoint(self, pos): + for element in self.Elements: + if isinstance(element, Wire): + element.HighlightPoint(pos) + + # Method called when a LeftDown event have been generated + def OnLeftDown(self, event, dc, scaling): + Graphic_Element.OnLeftDown(self, event, dc, scaling) + for element in self.Elements: + element.Handle = self.Handle + + # Change the variable that indicates if the elemente is selected + def SetSelected(self, selected): + for element in self.Elements: + element.SetSelected(selected) + + # Method called when a RightUp event has been generated + def OnRightUp(self, event, dc, scaling): + # Popup the menu with special items for a group + self.Parent.PopupGroupMenu() + + # Refreshes the model of all the elements of this group + def RefreshModel(self): + for element in self.Elements: + element.RefreshModel() + + +#------------------------------------------------------------------------------- +# Connector for all types of blocks +#------------------------------------------------------------------------------- + +""" +Class that implements a connector for any type of block +""" + +class Connector: + + # Create a new connector + def __init__(self, parent, name, type, position, direction, negated = False, edge = "none", onlyone = False): + self.ParentBlock = parent + self.Name = name + self.Type = type + self.Pos = position + self.Direction = direction + self.Wires = [] + if self.ParentBlock.IsOfType("BOOL", type): + self.Negated = negated + self.Edge = edge + else: + self.Negated = False + self.Edge = "none" + self.OneConnected = onlyone + self.Valid = True + self.Value = None + self.Forced = False + self.Selected = False + self.Highlights = [] + self.RefreshNameSize() + + def Flush(self): + self.ParentBlock = None + for wire, handle in self.Wires: + wire.Flush() + self.Wires = [] + + # Returns the RedrawRect + def GetRedrawRect(self, movex = 0, movey = 0): + parent_pos = self.ParentBlock.GetPosition() + x = min(parent_pos[0] + self.Pos.x, parent_pos[0] + self.Pos.x + self.Direction[0] * CONNECTOR_SIZE) + y = min(parent_pos[1] + self.Pos.y, parent_pos[1] + self.Pos.y + self.Direction[1] * CONNECTOR_SIZE) + if self.Direction[0] == 0: + width = 5 + else: + width = CONNECTOR_SIZE + if self.Direction[1] == 0: + height = 5 + else: + height = CONNECTOR_SIZE + return wx.Rect(x - abs(movex), y - abs(movey), width + 2 * abs(movex), height + 2 * abs(movey)) + + # Change the connector selection + def SetSelected(self, selected): + self.Selected = selected + + # Make a clone of the connector + def Clone(self, parent = None): + if parent is None: + parent = self.ParentBlock + return Connector(parent, self.Name, self.Type, wx.Point(self.Pos[0], self.Pos[1]), + self.Direction, self.Negated) + + # Returns the connector parent block + def GetParentBlock(self): + return self.ParentBlock + + # Returns the connector type + def GetType(self, raw = False): + if self.ParentBlock.IsEndType(self.Type) or raw: + return self.Type + elif (self.Negated or self.Edge != "none") and self.ParentBlock.IsOfType("BOOL", self.Type): + return "BOOL" + else: + return self.ParentBlock.GetConnectionResultType(self, self.Type) + + # Returns the connector type + def GetConnectedType(self): + if self.ParentBlock.IsEndType(self.Type): + return self.Type + elif len(self.Wires) == 1: + return self.Wires[0][0].GetOtherConnectedType(self.Wires[0][1]) + return self.Type + + # Returns the connector type + def GetConnectedRedrawRect(self, movex, movey): + rect = None + for wire, handle in self.Wires: + if rect is None: + rect = wire.GetRedrawRect() + else: + rect = rect.Union(wire.GetRedrawRect()) + return rect + + # Returns if connector type is compatible with type given + def IsCompatible(self, type): + reference = self.GetType() + return self.ParentBlock.IsOfType(type, reference) or self.ParentBlock.IsOfType(reference, type) + + # Changes the connector name + def SetType(self, type): + self.Type = type + for wire, handle in self.Wires: + wire.SetValid(wire.IsConnectedCompatible()) + + # Returns the connector name + def GetName(self): + return self.Name + + # Changes the connector name + def SetName(self, name): + self.Name = name + self.RefreshNameSize() + + def RefreshForced(self): + self.Forced = False + for wire, handle in self.Wires: + self.Forced |= wire.IsForced() + + def RefreshValue(self): + self.Value = self.ReceivingCurrent() + + def RefreshValid(self): + self.Valid = True + for wire, handle in self.Wires: + self.Valid &= wire.GetValid() + + def ReceivingCurrent(self): + current = False + for wire, handle in self.Wires: + value = wire.GetValue() + if current != "undefined" and isinstance(value, BooleanType): + current |= wire.GetValue() + elif value == "undefined": + current = "undefined" + return current + + def SpreadCurrent(self, spreading): + for wire, handle in self.Wires: + wire.SetValue(spreading) + + # Changes the connector name size + def RefreshNameSize(self): + if self.Name != "": + self.NameSize = self.ParentBlock.Parent.GetTextExtent(self.Name) + else: + self.NameSize = 0, 0 + + # Returns the connector name size + def GetNameSize(self): + return self.NameSize + + # Returns the wires connected to the connector + def GetWires(self): + return self.Wires + + # Returns the parent block Id + def GetBlockId(self): + return self.ParentBlock.GetId() + + # Returns the connector relative position + def GetRelPosition(self): + return self.Pos + + # Returns the connector absolute position + def GetPosition(self, size = True): + parent_pos = self.ParentBlock.GetPosition() + # If the position of the end of the connector is asked + if size: + x = parent_pos[0] + self.Pos.x + self.Direction[0] * CONNECTOR_SIZE + y = parent_pos[1] + self.Pos.y + self.Direction[1] * CONNECTOR_SIZE + else: + x = parent_pos[0] + self.Pos.x + y = parent_pos[1] + self.Pos.y + return wx.Point(x, y) + + # Change the connector relative position + def SetPosition(self, pos): + self.Pos = pos + + # Returns the connector direction + def GetDirection(self): + return self.Direction + + # Change the connector direction + def SetDirection(self, direction): + self.Direction = direction + + # Connect a wire to this connector at the last place + def Connect(self, wire, refresh = True): + self.InsertConnect(len(self.Wires), wire, refresh) + + # Connect a wire to this connector at the place given + def InsertConnect(self, idx, wire, refresh = True): + if wire not in self.Wires: + self.Wires.insert(idx, wire) + if refresh: + self.ParentBlock.RefreshModel(False) + + # Returns the index of the wire given in the list of connected + def GetWireIndex(self, wire): + for i, (tmp_wire, handle) in enumerate(self.Wires): + if tmp_wire == wire: + return i + return None + + # Unconnect a wire or all wires connected to the connector + def UnConnect(self, wire = None, unconnect = True, delete = False): + i = 0 + found = False + while i < len(self.Wires) and not found: + if not wire or self.Wires[i][0] == wire: + # If Unconnect haven't been called from a wire, disconnect the connector in the wire + if unconnect: + if self.Wires[i][1] == 0: + self.Wires[i][0].UnConnectStartPoint(delete) + else: + self.Wires[i][0].UnConnectEndPoint(delete) + # Remove wire from connected + if wire: + self.Wires.pop(i) + found = True + i += 1 + # If no wire defined, unconnect all wires + if not wire: + self.Wires = [] + self.RefreshValid() + self.ParentBlock.RefreshModel(False) + + # Returns if connector has one or more wire connected + def IsConnected(self): + return len(self.Wires) > 0 + + # Move the wires connected + def MoveConnected(self, exclude = []): + if len(self.Wires) > 0: + # Calculate the new position of the end point + parent_pos = self.ParentBlock.GetPosition() + x = parent_pos[0] + self.Pos.x + self.Direction[0] * CONNECTOR_SIZE + y = parent_pos[1] + self.Pos.y + self.Direction[1] * CONNECTOR_SIZE + # Move the corresponding point on all the wires connected + for wire, index in self.Wires: + if wire not in exclude: + if index == 0: + wire.MoveStartPoint(wx.Point(x, y)) + else: + wire.MoveEndPoint(wx.Point(x, y)) + + # Refreshes the model of all the wires connected + def RefreshWires(self): + for wire in self.Wires: + wire[0].RefreshModel() + + # Refreshes the parent block model + def RefreshParentBlock(self): + self.ParentBlock.RefreshModel(False) + + # Highlight the parent block + def HighlightParentBlock(self, highlight): + self.ParentBlock.SetHighlighted(highlight) + self.ParentBlock.Refresh() + + # Returns all the blocks connected to this connector + def GetConnectedBlocks(self): + blocks = [] + for wire, handle in self.Wires: + # Get other connector connected to each wire + if handle == 0: + connector = wire.GetEndConnected() + else: + connector = wire.GetStartConnected() + # Get parent block for this connector + if connector: + block = connector.GetParentBlock() + if block not in blocks: + blocks.append(block) + return blocks + + # Returns the connector negated property + def IsNegated(self): + return self.Negated + + # Changes the connector negated property + def SetNegated(self, negated): + if self.ParentBlock.IsOfType("BOOL", self.Type): + self.Negated = negated + self.Edge = "none" + + # Returns the connector edge property + def GetEdge(self): + return self.Edge + + # Changes the connector edge property + def SetEdge(self, edge): + if self.ParentBlock.IsOfType("BOOL", self.Type): + self.Edge = edge + self.Negated = False + + # Tests if the point given is near from the end point of this connector + def TestPoint(self, pt, direction = None, exclude = True): + parent_pos = self.ParentBlock.GetPosition() + if (not (len(self.Wires) > 0 and self.OneConnected and exclude) or self.Type == "BOOL")\ + and direction is None or self.Direction == direction: + # Calculate a square around the end point of this connector + x = parent_pos[0] + self.Pos.x + self.Direction[0] * CONNECTOR_SIZE - ANCHOR_DISTANCE + y = parent_pos[1] + self.Pos.y + self.Direction[1] * CONNECTOR_SIZE - ANCHOR_DISTANCE + width = ANCHOR_DISTANCE * 2 + abs(self.Direction[0]) * CONNECTOR_SIZE + height = ANCHOR_DISTANCE * 2 + abs(self.Direction[1]) * CONNECTOR_SIZE + rect = wx.Rect(x, y, width, height) + return rect.InsideXY(pt.x, pt.y) + return False + + # Draws the highlightment of this element if it is highlighted + def DrawHighlightment(self, dc): + scalex, scaley = dc.GetUserScale() + dc.SetUserScale(1, 1) + pen = MiterPen(HIGHLIGHTCOLOR, 2 * scalex + 5) + pen.SetCap(wx.CAP_BUTT) + dc.SetPen(pen) + dc.SetBrush(wx.Brush(HIGHLIGHTCOLOR)) + dc.SetLogicalFunction(wx.AND) + parent_pos = self.ParentBlock.GetPosition() + posx = parent_pos[0] + self.Pos.x + posy = parent_pos[1] + self.Pos.y + xstart = parent_pos[0] + self.Pos.x + ystart = parent_pos[1] + self.Pos.y + if self.Direction[0] < 0: + xstart += 1 + if self.Direction[1] < 0: + ystart += 1 + xend = xstart + CONNECTOR_SIZE * self.Direction[0] + yend = ystart + CONNECTOR_SIZE * self.Direction[1] + dc.DrawLine(round((xstart + self.Direction[0]) * scalex), round((ystart + self.Direction[1]) * scaley), + round(xend * scalex), round(yend * scaley)) + dc.SetLogicalFunction(wx.COPY) + dc.SetUserScale(scalex, scaley) + + # Adds an highlight to the connector + def AddHighlight(self, infos, start, end, highlight_type): + if highlight_type == ERROR_HIGHLIGHT: + for wire, handle in self.Wires: + wire.SetValid(False) + AddHighlight(self.Highlights, (start, end, highlight_type)) + + # Removes an highlight from the connector + def RemoveHighlight(self, infos, start, end, highlight_type): + error = False + highlights = [] + for highlight in self.Highlights: + if highlight != (start, end, highlight_type): + highlights.append(highlight) + error |= highlight == ERROR_HIGHLIGHT + self.Highlights = highlights + if not error: + for wire, handle in self.Wires: + wire.SetValid(wire.IsConnectedCompatible()) + + # Removes all the highlights of one particular type from the connector + def ClearHighlight(self, highlight_type=None): + error = False + if highlight_type is None: + self.Highlights = [] + else: + highlights = [] + for highlight in self.Highlights: + if highlight[2] != highlight_type: + highlights.append(highlight) + error |= highlight == ERROR_HIGHLIGHT + self.Highlights = highlights + if not error: + for wire, handle in self.Wires: + wire.SetValid(wire.IsConnectedCompatible()) + + # Draws the connector + def Draw(self, dc): + if self.Selected: + dc.SetPen(MiterPen(wx.BLUE, 3)) + dc.SetBrush(wx.WHITE_BRUSH) + #elif len(self.Highlights) > 0: + # dc.SetPen(MiterPen(self.Highlights[-1][1])) + # dc.SetBrush(wx.Brush(self.Highlights[-1][0])) + else: + if not self.Valid: + dc.SetPen(MiterPen(wx.RED)) + elif isinstance(self.Value, BooleanType) and self.Value: + if self.Forced: + dc.SetPen(MiterPen(wx.CYAN)) + else: + dc.SetPen(MiterPen(wx.GREEN)) + elif self.Value == "undefined": + dc.SetPen(MiterPen(wx.NamedColour("orange"))) + elif self.Forced: + dc.SetPen(MiterPen(wx.BLUE)) + else: + dc.SetPen(MiterPen(wx.BLACK)) + dc.SetBrush(wx.WHITE_BRUSH) + parent_pos = self.ParentBlock.GetPosition() + + if getattr(dc, "printing", False): + name_size = dc.GetTextExtent(self.Name) + else: + name_size = self.NameSize + + if self.Negated: + # If connector is negated, draw a circle + xcenter = parent_pos[0] + self.Pos.x + (CONNECTOR_SIZE * self.Direction[0]) / 2 + ycenter = parent_pos[1] + self.Pos.y + (CONNECTOR_SIZE * self.Direction[1]) / 2 + dc.DrawCircle(xcenter, ycenter, CONNECTOR_SIZE / 2) + else: + xstart = parent_pos[0] + self.Pos.x + ystart = parent_pos[1] + self.Pos.y + if self.Edge == "rising": + # If connector has a rising edge, draw a right arrow + dc.DrawLine(xstart, ystart, xstart - 4, ystart - 4) + dc.DrawLine(xstart, ystart, xstart - 4, ystart + 4) + elif self.Edge == "falling": + # If connector has a falling edge, draw a left arrow + dc.DrawLine(xstart, ystart, xstart + 4, ystart - 4) + dc.DrawLine(xstart, ystart, xstart + 4, ystart + 4) + if self.Direction[0] < 0: + xstart += 1 + if self.Direction[1] < 0: + ystart += 1 + if self.Selected: + xend = xstart + (CONNECTOR_SIZE - 2) * self.Direction[0] + yend = ystart + (CONNECTOR_SIZE - 2) * self.Direction[1] + dc.DrawLine(xstart + 2 * self.Direction[0], ystart + 2 * self.Direction[1], xend, yend) + else: + xend = xstart + CONNECTOR_SIZE * self.Direction[0] + yend = ystart + CONNECTOR_SIZE * self.Direction[1] + dc.DrawLine(xstart + self.Direction[0], ystart + self.Direction[1], xend, yend) + if self.Direction[0] != 0: + ytext = parent_pos[1] + self.Pos.y - name_size[1] / 2 + if self.Direction[0] < 0: + xtext = parent_pos[0] + self.Pos.x + 5 + else: + xtext = parent_pos[0] + self.Pos.x - (name_size[0] + 5) + if self.Direction[1] != 0: + xtext = parent_pos[0] + self.Pos.x - name_size[0] / 2 + if self.Direction[1] < 0: + ytext = parent_pos[1] + self.Pos.y + 5 + else: + ytext = parent_pos[1] + self.Pos.y - (name_size[1] + 5) + # Draw the text + dc.DrawText(self.Name, xtext, ytext) + if not getattr(dc, "printing", False): + DrawHighlightedText(dc, self.Name, self.Highlights, xtext, ytext) + +#------------------------------------------------------------------------------- +# Common Wire Element +#------------------------------------------------------------------------------- + +""" +Class that implements a wire for connecting two blocks +""" + +class Wire(Graphic_Element, DebugDataConsumer): + + # Create a new wire + def __init__(self, parent, start = None, end = None): + Graphic_Element.__init__(self, parent) + DebugDataConsumer.__init__(self) + self.StartPoint = start + self.EndPoint = end + self.StartConnected = None + self.EndConnected = None + # If the start and end points are defined, calculate the wire + if start and end: + self.ResetPoints() + self.GeneratePoints() + else: + self.Points = [] + self.Segments = [] + self.SelectedSegment = None + self.Valid = True + self.ValueSize = None + self.ComputedValue = None + self.OverStart = False + self.OverEnd = False + self.ComputingType = False + self.Font = parent.GetMiniFont() + + def GetDefinition(self): + if self.StartConnected is not None and self.EndConnected is not None: + startblock = self.StartConnected.GetParentBlock() + endblock = self.EndConnected.GetParentBlock() + return [], [(startblock.GetId(), endblock.GetId())] + return [], [] + + def Flush(self): + self.StartConnected = None + self.EndConnected = None + + def GetToolTipValue(self): + if self.Value is not None and self.Value != "undefined" and not isinstance(self.Value, BooleanType): + if isinstance(self.Value, StringType) and self.Value.find("#") == -1: + return "\"%s\""%self.Value + else: + return str(self.Value) + return None + + # Returns the RedrawRect + def GetRedrawRect(self, movex = 0, movey = 0): + rect = Graphic_Element.GetRedrawRect(self, movex, movey) + if self.StartConnected: + rect = rect.Union(self.StartConnected.GetRedrawRect(movex, movey)) + if self.EndConnected: + rect = rect.Union(self.EndConnected.GetRedrawRect(movex, movey)) + if self.ValueSize is None and isinstance(self.ComputedValue, (StringType, UnicodeType)): + self.ValueSize = self.Parent.GetMiniTextExtent(self.ComputedValue) + if self.ValueSize is not None: + width, height = self.ValueSize + if self.BoundingBox[2] > width * 4 or self.BoundingBox[3] > height * 4: + x = self.Points[0].x + width * self.StartPoint[1][0] / 2 + y = self.Points[0].y + height * (self.StartPoint[1][1] - 1) + rect = rect.Union(wx.Rect(x, y, width, height)) + x = self.Points[-1].x + width * self.EndPoint[1][0] / 2 + y = self.Points[-1].y + height * (self.EndPoint[1][1] - 1) + rect = rect.Union(wx.Rect(x, y, width, height)) + else: + middle = len(self.Segments) / 2 + len(self.Segments) % 2 - 1 + x = (self.Points[middle].x + self.Points[middle + 1].x - width) / 2 + if self.BoundingBox[3] > height and self.Segments[middle] in [NORTH, SOUTH]: + y = (self.Points[middle].y + self.Points[middle + 1].y - height) / 2 + else: + y = self.Points[middle].y - height + rect = rect.Union(wx.Rect(x, y, width, height)) + return rect + + def Clone(self, parent, connectors = {}, dx = 0, dy = 0): + start_connector = connectors.get(self.StartConnected, None) + end_connector = connectors.get(self.EndConnected, None) + if start_connector is not None and end_connector is not None: + wire = Wire(parent) + wire.SetPoints([(point.x + dx, point.y + dy) for point in self.Points]) + start_connector.Connect((wire, 0), False) + end_connector.Connect((wire, -1), False) + wire.ConnectStartPoint(start_connector.GetPosition(), start_connector) + wire.ConnectEndPoint(end_connector.GetPosition(), end_connector) + return wire + return None + + # Forbids to change the wire position + def SetPosition(x, y): + pass + + # Forbids to change the wire size + def SetSize(width, height): + pass + + # Moves and Resizes the element for fitting scaling + def AdjustToScaling(self, scaling): + if scaling is not None: + movex_max = movey_max = 0 + for idx, point in enumerate(self.Points): + if 0 < idx < len(self.Points) - 1: + movex = round_scaling(point.x, scaling[0]) - point.x + movey = round_scaling(point.y, scaling[1]) - point.y + if idx == 1: + if self.Segments[0][0] == 0: + movex = 0 + elif (point.x + movex - self.Points[0].x) * self.Segments[0][0] < MIN_SEGMENT_SIZE: + movex = round_scaling(self.Points[0].x + MIN_SEGMENT_SIZE * self.Segments[0][0], scaling[0], self.Segments[0][0]) - point.x + if self.Segments[0][1] == 0: + movey = 0 + elif (point.y + movey - self.Points[0].y) * self.Segments[0][1] < MIN_SEGMENT_SIZE: + movey = round_scaling(self.Points[0].y + MIN_SEGMENT_SIZE * self.Segments[0][1], scaling[0], self.Segments[0][1]) - point.y + elif idx == len(self.Points) - 2: + if self.Segments[-1][0] == 0: + movex = 0 + elif (self.Points[-1].x - (point.x + movex)) * self.Segments[-1][0] < MIN_SEGMENT_SIZE: + movex = round_scaling(self.Points[-1].x + MIN_SEGMENT_SIZE * self.Segments[0][0], scaling[0], self.Segments[0][0]) - point.x + if self.Segments[-1][1] == 0: + movey = 0 + elif (self.Points[-1].y - (point.y + movey)) * self.Segments[-1][1] < MIN_SEGMENT_SIZE: + movey = round_scaling(self.Points[-1].y - MIN_SEGMENT_SIZE * self.Segments[-1][1], scaling[1], -self.Segments[-1][1]) - point.y + movex_max = max(movex_max, movex) + movey_max = max(movey_max, movey) + point.x += movex + point.y += movey + return movex_max, movey_max + return 0, 0 + + # Returns connector to which start point is connected + def GetStartConnected(self): + return self.StartConnected + + # Returns connector to which start point is connected + def GetStartConnectedType(self): + if self.StartConnected and not self.ComputingType: + self.ComputingType = True + computed_type = self.StartConnected.GetType() + self.ComputingType = False + return computed_type + return None + + # Returns connector to which end point is connected + def GetEndConnected(self): + return self.EndConnected + + # Returns connector to which end point is connected + def GetEndConnectedType(self): + if self.EndConnected and not self.ComputingType: + self.ComputingType = True + computed_type = self.EndConnected.GetType() + self.ComputingType = False + return computed_type + return None + + def GetConnectionDirection(self): + if self.StartConnected is None and self.EndConnected is None: + return None + elif self.StartConnected is not None and self.EndConnected is None: + return (-self.StartPoint[1][0], -self.StartPoint[1][1]) + elif self.StartConnected is None and self.EndConnected is not None: + return self.EndPoint + elif self.Handle is not None: + handle_type, handle = self.Handle + # A point has been handled + if handle_type == HANDLE_POINT: + if handle == 0: + return self.EndPoint + else: + return (-self.StartPoint[1][0], -self.StartPoint[1][1]) + return None + + def GetOtherConnected(self, connector): + if self.StartConnected == connector: + return self.EndConnected + else: + return self.StartConnected + + def GetOtherConnectedType(self, handle): + if handle == 0: + return self.GetEndConnectedType() + else: + return self.GetStartConnectedType() + + def IsConnectedCompatible(self): + if self.StartConnected: + return self.StartConnected.IsCompatible(self.GetEndConnectedType()) + elif self.EndConnected: + return True + return False + + def SetForced(self, forced): + if self.Forced != forced: + self.Forced = forced + if self.StartConnected: + self.StartConnected.RefreshForced() + if self.EndConnected: + self.EndConnected.RefreshForced() + if self.Visible: + self.Parent.ElementNeedRefresh(self) + + def SetValue(self, value): + if self.Value != value: + self.Value = value + if value is not None and not isinstance(value, BooleanType): + if isinstance(value, StringType) and value.find('#') == -1: + self.ComputedValue = "\"%s\""%value + else: + self.ComputedValue = str(value) + if self.ToolTip is not None: + self.ToolTip.SetTip(self.ComputedValue) + if len(self.ComputedValue) > 4: + self.ComputedValue = self.ComputedValue[:4] + "..." + self.ValueSize = None + if self.StartConnected: + self.StartConnected.RefreshValue() + if self.EndConnected: + self.EndConnected.RefreshValue() + if self.Visible: + self.Parent.ElementNeedRefresh(self) + if isinstance(value, BooleanType) and self.StartConnected is not None: + block = self.StartConnected.GetParentBlock() + block.SpreadCurrent() + + # Unconnect the start and end points + def Clean(self): + if self.StartConnected: + self.UnConnectStartPoint() + if self.EndConnected: + self.UnConnectEndPoint() + + # Delete this wire by calling the corresponding method + def Delete(self): + self.Parent.DeleteWire(self) + + # Select a segment and not the whole wire. It's useful for Ladder Diagram + def SetSelectedSegment(self, segment): + # The last segment is indicated + if segment == -1: + segment = len(self.Segments) - 1 + # The selected segment is reinitialised + if segment == None: + if self.StartConnected: + self.StartConnected.SetSelected(False) + if self.EndConnected: + self.EndConnected.SetSelected(False) + # The segment selected is the first + elif segment == 0: + if self.StartConnected: + self.StartConnected.SetSelected(True) + if self.EndConnected: + # There is only one segment + if len(self.Segments) == 1: + self.EndConnected.SetSelected(True) + else: + self.EndConnected.SetSelected(False) + # The segment selected is the last + elif segment == len(self.Segments) - 1: + if self.StartConnected: + self.StartConnected.SetSelected(False) + if self.EndConnected: + self.EndConnected.SetSelected(True) + self.SelectedSegment = segment + self.Refresh() + + def SetValid(self, valid): + self.Valid = valid + if self.StartConnected: + self.StartConnected.RefreshValid() + if self.EndConnected: + self.EndConnected.RefreshValid() + + def GetValid(self): + return self.Valid + + # Reinitialize the wire points + def ResetPoints(self): + if self.StartPoint and self.EndPoint: + self.Points = [self.StartPoint[0], self.EndPoint[0]] + self.Segments = [self.StartPoint[1]] + else: + self.Points = [] + self.Segments = [] + + # Refresh the wire bounding box + def RefreshBoundingBox(self): + if len(self.Points) > 0: + # If startpoint or endpoint is connected, save the point radius + start_radius = end_radius = 0 + if not self.StartConnected: + start_radius = POINT_RADIUS + if not self.EndConnected: + end_radius = POINT_RADIUS + # Initialize minimum and maximum from the first point + minx, minbbxx = self.Points[0].x, self.Points[0].x - start_radius + maxx, maxbbxx = self.Points[0].x, self.Points[0].x + start_radius + miny, minbbxy = self.Points[0].y, self.Points[0].y - start_radius + maxy, maxbbxy = self.Points[0].y, self.Points[0].y + start_radius + # Actualize minimum and maximum with the other points + for point in self.Points[1:-1]: + minx, minbbxx = min(minx, point.x), min(minbbxx, point.x) + maxx, maxbbxx = max(maxx, point.x), max(maxbbxx, point.x) + miny, minbbxy = min(miny, point.y), min(minbbxy, point.y) + maxy, maxbbxy = max(maxy, point.y), max(maxbbxy, point.y) + if len(self.Points) > 1: + minx, minbbxx = min(minx, self.Points[-1].x), min(minbbxx, self.Points[-1].x - end_radius) + maxx, maxbbxx = max(maxx, self.Points[-1].x), max(maxbbxx, self.Points[-1].x + end_radius) + miny, minbbxy = min(miny, self.Points[-1].y), min(minbbxy, self.Points[-1].y - end_radius) + maxy, maxbbxy = max(maxy, self.Points[-1].y), max(maxbbxy, self.Points[-1].y + end_radius) + self.Pos.x, self.Pos.y = minx, miny + self.Size = wx.Size(maxx - minx, maxy - miny) + self.BoundingBox = wx.Rect(minbbxx, minbbxy, maxbbxx - minbbxx + 1, maxbbxy - minbbxy + 1) + + # Refresh the realpoints that permits to keep the proportionality in wire during resizing + def RefreshRealPoints(self): + if len(self.Points) > 0: + self.RealPoints = [] + # Calculate float relative position of each point with the minimum point + for point in self.Points: + self.RealPoints.append([float(point.x - self.Pos.x), float(point.y - self.Pos.y)]) + + # Returns the wire minimum size + def GetMinSize(self): + width = 1 + height = 1 + dir_product = product(self.StartPoint[1], self.EndPoint[1]) + # The directions are opposed + if dir_product < 0: + if self.StartPoint[0] != 0: + width = MIN_SEGMENT_SIZE * 2 + if self.StartPoint[1] != 0: + height = MIN_SEGMENT_SIZE * 2 + # The directions are the same + elif dir_product > 0: + if self.StartPoint[0] != 0: + width = MIN_SEGMENT_SIZE + if self.StartPoint[1] != 0: + height = MIN_SEGMENT_SIZE + # The directions are perpendiculars + else: + width = MIN_SEGMENT_SIZE + height = MIN_SEGMENT_SIZE + return width + 1, height + 1 + + # Returns if the point given is on one of the wire segments + def HitTest(self, pt, connectors=True): + test = False + for i in xrange(len(self.Points) - 1): + rect = wx.Rect(0, 0, 0, 0) + if i == 0 and self.StartConnected is not None: + x1 = self.Points[i].x - self.Segments[0][0] * CONNECTOR_SIZE + y1 = self.Points[i].y - self.Segments[0][1] * CONNECTOR_SIZE + else: + x1, y1 = self.Points[i].x, self.Points[i].y + if i == len(self.Points) - 2 and self.EndConnected is not None: + x2 = self.Points[i + 1].x + self.Segments[-1][0] * CONNECTOR_SIZE + y2 = self.Points[i + 1].y + self.Segments[-1][1] * CONNECTOR_SIZE + else: + x2, y2 = self.Points[i + 1].x, self.Points[i + 1].y + # Calculate a rectangle around the segment + rect = wx.Rect(min(x1, x2) - ANCHOR_DISTANCE, min(y1, y2) - ANCHOR_DISTANCE, + abs(x1 - x2) + 2 * ANCHOR_DISTANCE, abs(y1 - y2) + 2 * ANCHOR_DISTANCE) + test |= rect.InsideXY(pt.x, pt.y) + return test + + # Returns the wire start or end point if the point given is on one of them + def TestPoint(self, pt): + # Test the wire start point + rect = wx.Rect(self.Points[0].x - ANCHOR_DISTANCE, self.Points[0].y - ANCHOR_DISTANCE, + 2 * ANCHOR_DISTANCE, 2 * ANCHOR_DISTANCE) + if rect.InsideXY(pt.x, pt.y): + return 0 + # Test the wire end point + if len(self.Points) > 1: + rect = wx.Rect(self.Points[-1].x - ANCHOR_DISTANCE, self.Points[-1].y - ANCHOR_DISTANCE, + 2 * ANCHOR_DISTANCE, 2 * ANCHOR_DISTANCE) + if rect.InsideXY(pt.x, pt.y): + return -1 + return None + + # Returns the wire segment if the point given is on it + def TestSegment(self, pt, all=False): + for i in xrange(len(self.Segments)): + # If wire is not in a Ladder Diagram, first and last segments are excluded + if all or 0 < i < len(self.Segments) - 1: + x1, y1 = self.Points[i].x, self.Points[i].y + x2, y2 = self.Points[i + 1].x, self.Points[i + 1].y + # Calculate a rectangle around the segment + rect = wx.Rect(min(x1, x2) - ANCHOR_DISTANCE, min(y1, y2) - ANCHOR_DISTANCE, + abs(x1 - x2) + 2 * ANCHOR_DISTANCE, abs(y1 - y2) + 2 * ANCHOR_DISTANCE) + if rect.InsideXY(pt.x, pt.y): + return i, self.Segments[i] + return None + + # Define the wire points + def SetPoints(self, points, verify=True): + if len(points) > 1: + self.Points = [wx.Point(x, y) for x, y in points] + # Calculate the start and end directions + self.StartPoint = [None, vector(self.Points[0], self.Points[1])] + self.EndPoint = [None, vector(self.Points[-1], self.Points[-2])] + # Calculate the start and end points + self.StartPoint[0] = wx.Point(self.Points[0].x + CONNECTOR_SIZE * self.StartPoint[1][0], + self.Points[0].y + CONNECTOR_SIZE * self.StartPoint[1][1]) + self.EndPoint[0] = wx.Point(self.Points[-1].x + CONNECTOR_SIZE * self.EndPoint[1][0], + self.Points[-1].y + CONNECTOR_SIZE * self.EndPoint[1][1]) + self.Points[0] = self.StartPoint[0] + self.Points[-1] = self.EndPoint[0] + # Calculate the segments directions + self.Segments = [] + i = 0 + while i < len(self.Points) - 1: + if verify and 0 < i < len(self.Points) - 2 and \ + self.Points[i] == self.Points[i + 1] and \ + self.Segments[-1] == vector(self.Points[i + 1], self.Points[i + 2]): + for j in xrange(2): + self.Points.pop(i) + else: + segment = vector(self.Points[i], self.Points[i + 1]) + if is_null_vector(segment) and i > 0: + segment = (self.Segments[-1][1], self.Segments[-1][0]) + if i < len(self.Points) - 2: + next = vector(self.Points[i + 1], self.Points[i + 2]) + if next == segment or is_null_vector(add_vectors(segment, next)): + self.Points.insert(i + 1, wx.Point(self.Points[i + 1].x, self.Points[i + 1].y)) + self.Segments.append(segment) + i += 1 + self.RefreshBoundingBox() + self.RefreshRealPoints() + + # Returns the position of the point indicated + def GetPoint(self, index): + if index < len(self.Points): + return self.Points[index].x, self.Points[index].y + return None + + # Returns a list of the position of all wire points + def GetPoints(self, invert = False): + points = self.VerifyPoints() + points[0] = wx.Point(points[0].x - CONNECTOR_SIZE * self.StartPoint[1][0], + points[0].y - CONNECTOR_SIZE * self.StartPoint[1][1]) + points[-1] = wx.Point(points[-1].x - CONNECTOR_SIZE * self.EndPoint[1][0], + points[-1].y - CONNECTOR_SIZE * self.EndPoint[1][1]) + # An inversion of the list is asked + if invert: + points.reverse() + return points + + # Returns the position of the two selected segment points + def GetSelectedSegmentPoints(self): + if self.SelectedSegment != None and len(self.Points) > 1: + return self.Points[self.SelectedSegment:self.SelectedSegment + 2] + return [] + + # Returns if the selected segment is the first and/or the last of the wire + def GetSelectedSegmentConnections(self): + if self.SelectedSegment != None and len(self.Points) > 1: + return self.SelectedSegment == 0, self.SelectedSegment == len(self.Segments) - 1 + return (True, True) + + # Returns the connectors on which the wire is connected + def GetConnected(self): + connected = [] + if self.StartConnected and self.StartPoint[1] == WEST: + connected.append(self.StartConnected) + if self.EndConnected and self.EndPoint[1] == WEST: + connected.append(self.EndConnected) + return connected + + # Returns the id of the block connected to the first or the last wire point + def GetConnectedInfos(self, index): + if index == 0 and self.StartConnected: + return self.StartConnected.GetBlockId(), self.StartConnected.GetName() + elif index == -1 and self.EndConnected: + return self.EndConnected.GetBlockId(), self.EndConnected.GetName() + return None + + # Update the wire points position by keeping at most possible the current positions + def GeneratePoints(self, realpoints = True): + i = 0 + # Calculate the start enad end points with the minimum segment size in the right direction + end = wx.Point(self.EndPoint[0].x + self.EndPoint[1][0] * MIN_SEGMENT_SIZE, + self.EndPoint[0].y + self.EndPoint[1][1] * MIN_SEGMENT_SIZE) + start = wx.Point(self.StartPoint[0].x + self.StartPoint[1][0] * MIN_SEGMENT_SIZE, + self.StartPoint[0].y + self.StartPoint[1][1] * MIN_SEGMENT_SIZE) + # Evaluate the point till it's the last + while i < len(self.Points) - 1: + # The next point is the last + if i + 1 == len(self.Points) - 1: + # Calculate the direction from current point to end point + v_end = vector(self.Points[i], end) + # The current point is the first + if i == 0: + # If the end point is not in the start direction, a point is added + if v_end != self.Segments[0] or v_end == self.EndPoint[1]: + self.Points.insert(1, wx.Point(start.x, start.y)) + self.Segments.insert(1, DirectionChoice((self.Segments[0][1], + self.Segments[0][0]), v_end, self.EndPoint[1])) + # The current point is the second + elif i == 1: + # The previous direction and the target direction are mainly opposed, a point is added + if product(v_end, self.Segments[0]) < 0: + self.Points.insert(2, wx.Point(self.Points[1].x, self.Points[1].y)) + self.Segments.insert(2, DirectionChoice((self.Segments[1][1], + self.Segments[1][0]), v_end, self.EndPoint[1])) + # The previous direction and the end direction are the same or they are + # perpendiculars and the end direction points towards current segment + elif product(self.Segments[0], self.EndPoint[1]) >= 0 and product(self.Segments[1], self.EndPoint[1]) <= 0: + # Current point and end point are aligned + if self.Segments[0][0] != 0: + self.Points[1].x = end.x + if self.Segments[0][1] != 0: + self.Points[1].y = end.y + # If the previous direction and the end direction are the same, a point is added + if product(self.Segments[0], self.EndPoint[1]) > 0: + self.Points.insert(2, wx.Point(self.Points[1].x, self.Points[1].y)) + self.Segments.insert(2, DirectionChoice((self.Segments[1][1], + self.Segments[1][0]), v_end, self.EndPoint[1])) + else: + # Current point is positioned in the middle of start point + # and end point on the current direction and a point is added + if self.Segments[0][0] != 0: + self.Points[1].x = (end.x + start.x) / 2 + if self.Segments[0][1] != 0: + self.Points[1].y = (end.y + start.y) / 2 + self.Points.insert(2, wx.Point(self.Points[1].x, self.Points[1].y)) + self.Segments.insert(2, DirectionChoice((self.Segments[1][1], + self.Segments[1][0]), v_end, self.EndPoint[1])) + else: + # The previous direction and the end direction are perpendiculars + if product(self.Segments[i - 1], self.EndPoint[1]) == 0: + # The target direction and the end direction aren't mainly the same + if product(v_end, self.EndPoint[1]) <= 0: + # Current point and end point are aligned + if self.Segments[i - 1][0] != 0: + self.Points[i].x = end.x + if self.Segments[i - 1][1] != 0: + self.Points[i].y = end.y + # Previous direction is updated from the new point + if product(vector(self.Points[i - 1], self.Points[i]), self.Segments[i - 1]) < 0: + self.Segments[i - 1] = (-self.Segments[i - 1][0], -self.Segments[i - 1][1]) + else: + test = True + # If the current point is the third, test if the second + # point can be aligned with the end point + if i == 2: + test_point = wx.Point(self.Points[1].x, self.Points[1].y) + if self.Segments[1][0] != 0: + test_point.y = end.y + if self.Segments[1][1] != 0: + test_point.x = end.x + vector_test = vector(self.Points[0], test_point, False) + test = norm(vector_test) > MIN_SEGMENT_SIZE and product(self.Segments[0], vector_test) > 0 + # The previous point can be aligned + if test: + self.Points[i].x, self.Points[i].y = end.x, end.y + if self.Segments[i - 1][0] != 0: + self.Points[i - 1].y = end.y + if self.Segments[i - 1][1] != 0: + self.Points[i - 1].x = end.x + self.Segments[i] = (-self.EndPoint[1][0], -self.EndPoint[1][1]) + else: + # Current point is positioned in the middle of previous point + # and end point on the current direction and a point is added + if self.Segments[1][0] != 0: + self.Points[2].x = (self.Points[1].x + end.x) / 2 + if self.Segments[1][1] != 0: + self.Points[2].y = (self.Points[1].y + end.y) / 2 + self.Points.insert(3, wx.Point(self.Points[2].x, self.Points[2].y)) + self.Segments.insert(3, DirectionChoice((self.Segments[2][1], + self.Segments[2][0]), v_end, self.EndPoint[1])) + else: + # Current point is aligned with end point + if self.Segments[i - 1][0] != 0: + self.Points[i].x = end.x + if self.Segments[i - 1][1] != 0: + self.Points[i].y = end.y + # Previous direction is updated from the new point + if product(vector(self.Points[i - 1], self.Points[i]), self.Segments[i - 1]) < 0: + self.Segments[i - 1] = (-self.Segments[i - 1][0], -self.Segments[i - 1][1]) + # If previous direction and end direction are opposed + if product(self.Segments[i - 1], self.EndPoint[1]) < 0: + # Current point is positioned in the middle of previous point + # and end point on the current direction + if self.Segments[i - 1][0] != 0: + self.Points[i].x = (end.x + self.Points[i - 1].x) / 2 + if self.Segments[i - 1][1] != 0: + self.Points[i].y = (end.y + self.Points[i - 1].y) / 2 + # A point is added + self.Points.insert(i + 1, wx.Point(self.Points[i].x, self.Points[i].y)) + self.Segments.insert(i + 1, DirectionChoice((self.Segments[i][1], + self.Segments[i][0]), v_end, self.EndPoint[1])) + else: + # Current point is the first, and second is not mainly in the first direction + if i == 0 and product(vector(start, self.Points[1]), self.Segments[0]) < 0: + # If first and second directions aren't perpendiculars, a point is added + if product(self.Segments[0], self.Segments[1]) != 0: + self.Points.insert(1, wx.Point(start.x, start.y)) + self.Segments.insert(1, DirectionChoice((self.Segments[0][1], + self.Segments[0][0]), vector(start, self.Points[1]), self.Segments[1])) + else: + self.Points[1].x, self.Points[1].y = start.x, start.y + else: + # Next point is aligned with current point + if self.Segments[i][0] != 0: + self.Points[i + 1].y = self.Points[i].y + if self.Segments[i][1] != 0: + self.Points[i + 1].x = self.Points[i].x + # Current direction is updated from the new point + if product(vector(self.Points[i], self.Points[i + 1]), self.Segments[i]) < 0: + self.Segments[i] = (-self.Segments[i][0], -self.Segments[i][1]) + i += 1 + self.RefreshBoundingBox() + if realpoints: + self.RefreshRealPoints() + + # Verify that two consecutive points haven't the same position + def VerifyPoints(self): + points = [point for point in self.Points] + segments = [segment for segment in self.Segments] + i = 1 + while i < len(points) - 1: + if points[i] == points[i + 1] and segments[i - 1] == segments[i + 1]: + for j in xrange(2): + points.pop(i) + segments.pop(i) + else: + i += 1 + # If the wire isn't in a Ladder Diagram, save the new point list + if self.Parent.__class__.__name__ != "LD_Viewer": + self.Points = [point for point in points] + self.Segments = [segment for segment in segments] + self.RefreshBoundingBox() + self.RefreshRealPoints() + return points + + # Moves all the wire points except the first and the last if they are connected + def Move(self, dx, dy, endpoints = False): + for i, point in enumerate(self.Points): + if endpoints or not (i == 0 and self.StartConnected) and not (i == len(self.Points) - 1 and self.EndConnected): + point.x += dx + point.y += dy + self.StartPoint[0] = self.Points[0] + self.EndPoint[0] = self.Points[-1] + self.GeneratePoints() + + # Resize the wire from position and size given + def Resize(self, x, y, width, height): + if len(self.Points) > 1: + # Calculate the new position of each point for testing the new size + minx, miny = self.Pos.x, self.Pos.y + lastwidth, lastheight = self.Size.width, self.Size.height + for i, point in enumerate(self.RealPoints): + # If start or end point is connected, it's not calculate + if not (i == 0 and self.StartConnected) and not (i == len(self.Points) - 1 and self.EndConnected): + if i == 0: + dir = self.StartPoint[1] + elif i == len(self.Points) - 1: + dir = self.EndPoint[1] + else: + dir = (0, 0) + pointx = max(-dir[0] * MIN_SEGMENT_SIZE, min(int(round(point[0] * width / float(max(lastwidth, 1)))), + width - dir[0] * MIN_SEGMENT_SIZE)) + pointy = max(-dir[1] * MIN_SEGMENT_SIZE, min(int(round(point[1] * height / float(max(lastheight, 1)))), + height - dir[1] * MIN_SEGMENT_SIZE)) + self.Points[i] = wx.Point(minx + x + pointx, miny + y + pointy) + self.StartPoint[0] = self.Points[0] + self.EndPoint[0] = self.Points[-1] + self.GeneratePoints(False) + # Test if the wire position or size have changed + if x != 0 and minx == self.Pos.x: + x = 0 + width = lastwidth + if y != 0 and miny == self.Pos.y: + y = 0 + height = lastwidth + if width != lastwidth and lastwidth == self.Size.width: + width = lastwidth + if height != lastheight and lastheight == self.Size.height: + height = lastheight + # Calculate the real points from the new size, it's important for + # keeping a proportionality in the points position with the size + # during a resize dragging + for i, point in enumerate(self.RealPoints): + if not (i == 0 and self.StartConnected) and not (i == len(self.Points) - 1 and self.EndConnected): + point[0] = point[0] * width / float(max(lastwidth, 1)) + point[1] = point[1] * height / float(max(lastheight, 1)) + # Calculate the correct position of the points from real points + for i, point in enumerate(self.RealPoints): + if not (i == 0 and self.StartConnected) and not (i == len(self.Points) - 1 and self.EndConnected): + if i == 0: + dir = self.StartPoint[1] + elif i == len(self.Points) - 1: + dir = self.EndPoint[1] + else: + dir = (0, 0) + realpointx = max(-dir[0] * MIN_SEGMENT_SIZE, min(int(round(point[0])), + width - dir[0] * MIN_SEGMENT_SIZE)) + realpointy = max(-dir[1] * MIN_SEGMENT_SIZE, min(int(round(point[1])), + height - dir[1] * MIN_SEGMENT_SIZE)) + self.Points[i] = wx.Point(minx + x + realpointx, miny + y + realpointy) + self.StartPoint[0] = self.Points[0] + self.EndPoint[0] = self.Points[-1] + self.GeneratePoints(False) + + # Moves the wire start point and update the wire points + def MoveStartPoint(self, point): + if len(self.Points) > 1: + self.StartPoint[0] = point + self.Points[0] = point + self.GeneratePoints() + + # Changes the wire start direction and update the wire points + def SetStartPointDirection(self, dir): + if len(self.Points) > 1: + self.StartPoint[1] = dir + self.Segments[0] = dir + self.GeneratePoints() + + # Rotates the wire start direction by an angle of 90 degrees anticlockwise + def RotateStartPoint(self): + self.SetStartPointDirection((self.StartPoint[1][1], -self.StartPoint[1][0])) + + # Connects wire start point to the connector given and moves wire start point + # to given point + def ConnectStartPoint(self, point, connector): + if point: + self.MoveStartPoint(point) + self.StartConnected = connector + self.RefreshBoundingBox() + + # Unconnects wire start point + def UnConnectStartPoint(self, delete = False): + if delete: + self.StartConnected = None + self.Delete() + elif self.StartConnected: + self.StartConnected.UnConnect(self, unconnect = False) + self.StartConnected = None + self.RefreshBoundingBox() + + # Moves the wire end point and update the wire points + def MoveEndPoint(self, point): + if len(self.Points) > 1: + self.EndPoint[0] = point + self.Points[-1] = point + self.GeneratePoints() + + # Changes the wire end direction and update the wire points + def SetEndPointDirection(self, dir): + if len(self.Points) > 1: + self.EndPoint[1] = dir + self.GeneratePoints() + + # Rotates the wire end direction by an angle of 90 degrees anticlockwise + def RotateEndPoint(self): + self.SetEndPointDirection((self.EndPoint[1][1], -self.EndPoint[1][0])) + + # Connects wire end point to the connector given and moves wire end point + # to given point + def ConnectEndPoint(self, point, connector): + if point: + self.MoveEndPoint(point) + self.EndConnected = connector + self.RefreshBoundingBox() + + # Unconnects wire end point + def UnConnectEndPoint(self, delete = False): + if delete: + self.EndConnected = None + self.Delete() + elif self.EndConnected: + self.EndConnected.UnConnect(self, unconnect = False) + self.EndConnected = None + self.RefreshBoundingBox() + + # Moves the wire segment given by its index + def MoveSegment(self, idx, movex, movey, scaling): + if 0 < idx < len(self.Segments) - 1: + if self.Segments[idx] in (NORTH, SOUTH): + start_x = self.Points[idx].x + if scaling is not None: + movex = round_scaling(self.Points[idx].x + movex, scaling[0]) - self.Points[idx].x + if idx == 1 and (self.Points[1].x + movex - self.Points[0].x) * self.Segments[0][0] < MIN_SEGMENT_SIZE: + movex = round_scaling(self.Points[0].x + MIN_SEGMENT_SIZE * self.Segments[0][0], scaling[0], self.Segments[0][0]) - self.Points[idx].x + elif idx == len(self.Segments) - 2 and (self.Points[-1].x - (self.Points[-2].x + movex)) * self.Segments[-1][0] < MIN_SEGMENT_SIZE: + movex = round_scaling(self.Points[-1].x - MIN_SEGMENT_SIZE * self.Segments[-1][0], scaling[0], -self.Segments[-1][0]) - self.Points[idx].x + self.Points[idx].x += movex + self.Points[idx + 1].x += movex + self.GeneratePoints() + if start_x != self.Points[idx].x: + return self.Points[idx].x - start_x, 0 + elif self.Segments[idx] in (EAST, WEST): + start_y = self.Points[idx].y + if scaling is not None: + movey = round_scaling(self.Points[idx].y + movey, scaling[1]) - self.Points[idx].y + if idx == 1 and (self.Points[1].y + movey - self.Points[0].y) * self.Segments[0][1] < MIN_SEGMENT_SIZE: + movex = round_scaling(self.Points[0].y + MIN_SEGMENT_SIZE * self.Segments[0][1], scaling[0], self.Segments[0][1]) - self.Points[idx].y + elif idx == len(self.Segments) - 2 and (self.Points[-1].y - (self.Points[-2].y + movey)) * self.Segments[-1][1] < MIN_SEGMENT_SIZE: + movey = round_scaling(self.Points[idx].y - MIN_SEGMENT_SIZE * self.Segments[-1][1], scaling[1], -self.Segments[-1][1]) - self.Points[idx].y + self.Points[idx].y += movey + self.Points[idx + 1].y += movey + self.GeneratePoints() + if start_y != self.Points[idx].y: + return 0, self.Points[idx].y - start_y + return 0, 0 + + # Adds two points in the middle of the handled segment + def AddSegment(self): + handle_type, handle = self.Handle + if handle_type == HANDLE_SEGMENT: + segment, dir = handle + if len(self.Segments) > 1: + pointx = self.Points[segment].x + pointy = self.Points[segment].y + if dir[0] != 0: + pointx = (self.Points[segment].x + self.Points[segment + 1].x) / 2 + if dir[1] != 0: + pointy = (self.Points[segment].y + self.Points[segment + 1].y) / 2 + self.Points.insert(segment + 1, wx.Point(pointx, pointy)) + self.Segments.insert(segment + 1, (dir[1], dir[0])) + self.Points.insert(segment + 2, wx.Point(pointx, pointy)) + self.Segments.insert(segment + 2, dir) + else: + p1x = p2x = self.Points[segment].x + p1y = p2y = self.Points[segment].y + if dir[0] != 0: + p1x = (2 * self.Points[segment].x + self.Points[segment + 1].x) / 3 + p2x = (self.Points[segment].x + 2 * self.Points[segment + 1].x) / 3 + if dir[1] != 0: + p1y = (2 * self.Points[segment].y + self.Points[segment + 1].y) / 3 + p2y = (self.Points[segment].y + 2 * self.Points[segment + 1].y) / 3 + self.Points.insert(segment + 1, wx.Point(p1x, p1y)) + self.Segments.insert(segment + 1, (dir[1], dir[0])) + self.Points.insert(segment + 2, wx.Point(p1x, p1y)) + self.Segments.insert(segment + 2, dir) + self.Points.insert(segment + 3, wx.Point(p2x, p2y)) + self.Segments.insert(segment + 3, (dir[1], dir[0])) + self.Points.insert(segment + 4, wx.Point(p2x, p2y)) + self.Segments.insert(segment + 4, dir) + self.GeneratePoints() + + # Delete the handled segment by removing the two segment points + def DeleteSegment(self): + handle_type, handle = self.Handle + if handle_type == HANDLE_SEGMENT: + segment, dir = handle + for i in xrange(2): + self.Points.pop(segment) + self.Segments.pop(segment) + self.GeneratePoints() + self.RefreshModel() + + # Method called when a LeftDown event have been generated + def OnLeftDown(self, event, dc, scaling): + pos = GetScaledEventPosition(event, dc, scaling) + # Test if a point have been handled + #result = self.TestPoint(pos) + #if result != None: + # self.Handle = (HANDLE_POINT, result) + # wx.CallAfter(self.Parent.SetCurrentCursor, 1) + #else: + # Test if a segment have been handled + result = self.TestSegment(pos) + if result != None: + if result[1] in (NORTH, SOUTH): + wx.CallAfter(self.Parent.SetCurrentCursor, 4) + elif result[1] in (EAST, WEST): + wx.CallAfter(self.Parent.SetCurrentCursor, 5) + self.Handle = (HANDLE_SEGMENT, result) + # Execute the default method for a graphic element + else: + Graphic_Element.OnLeftDown(self, event, dc, scaling) + self.oldPos = pos + + # Method called when a RightUp event has been generated + def OnRightUp(self, event, dc, scaling): + pos = GetScaledEventPosition(event, dc, scaling) + # Test if a segment has been handled + result = self.TestSegment(pos, True) + if result != None: + self.Handle = (HANDLE_SEGMENT, result) + # Popup the menu with special items for a wire + self.Parent.PopupWireMenu(0 < result[0] < len(self.Segments) - 1) + else: + # Execute the default method for a graphic element + Graphic_Element.OnRightUp(self, event, dc, scaling) + + # Method called when a LeftDClick event has been generated + def OnLeftDClick(self, event, dc, scaling): + rect = self.GetRedrawRect() + if event.ControlDown(): + direction = (self.StartPoint[1], self.EndPoint[1]) + if direction in [(EAST, WEST), (WEST, EAST)]: + avgy = (self.StartPoint[0].y + self.EndPoint[0].y) / 2 + if scaling is not None: + avgy = round(float(avgy) / scaling[1]) * scaling[1] + if self.StartConnected is not None: + movey = avgy - self.StartPoint[0].y + startblock = self.StartConnected.GetParentBlock() + startblock.Move(0, movey) + startblock.RefreshModel() + rect.Union(startblock.GetRedrawRect(0, movey)) + else: + self.MoveStartPoint(wx.Point(self.StartPoint[0].x, avgy)) + if self.EndConnected is not None: + movey = avgy - self.EndPoint[0].y + endblock = self.EndConnected.GetParentBlock() + endblock.Move(0, movey) + endblock.RefreshModel() + rect.Union(endblock.GetRedrawRect(0, movey)) + else: + self.MoveEndPoint(wx.Point(self.EndPoint[0].x, avgy)) + self.Parent.RefreshBuffer() + elif direction in [(NORTH, SOUTH), (SOUTH, NORTH)]: + avgx = (self.StartPoint[0].x + self.EndPoint[0].x) / 2 + if scaling is not None: + avgx = round(float(avgx) / scaling[0]) * scaling[0] + if self.StartConnected is not None: + movex = avgx - self.StartPoint[0].x + startblock = self.StartConnected.GetParentBlock() + startblock.Move(movex, 0) + startblock.RefreshModel() + rect.Union(startblock.GetRedrawRect(movex, 0)) + else: + self.MoveStartPoint(wx.Point(avgx, self.StartPoint[0].y)) + if self.EndConnected is not None: + movex = avgx - self.EndPoint[0].x + endblock = self.EndConnected.GetParentBlock() + endblock.Move(movex, 0) + endblock.RefreshModel() + rect.Union(endblock.GetRedrawRect(movex, 0)) + else: + self.MoveEndPoint(wx.Point(avgx, self.EndPoint[0].y)) + self.Parent.RefreshBuffer() + else: + self.ResetPoints() + self.GeneratePoints() + self.RefreshModel() + self.Parent.RefreshBuffer() + rect.Union(self.GetRedrawRect()) + self.Parent.RefreshRect(self.Parent.GetScrolledRect(rect), False) + + # Method called when a Motion event has been generated + def OnMotion(self, event, dc, scaling): + pos = GetScaledEventPosition(event, dc, scaling) + if not event.Dragging(): + # Test if a segment has been handled + result = self.TestSegment(pos) + if result: + if result[1] in (NORTH, SOUTH): + wx.CallAfter(self.Parent.SetCurrentCursor, 4) + elif result[1] in (EAST, WEST): + wx.CallAfter(self.Parent.SetCurrentCursor, 5) + return 0, 0 + else: + # Execute the default method for a graphic element + return Graphic_Element.OnMotion(self, event, dc, scaling) + else: + # Execute the default method for a graphic element + return Graphic_Element.OnMotion(self, event, dc, scaling) + + # Refreshes the wire state according to move defined and handle selected + def ProcessDragging(self, movex, movey, event, scaling): + handle_type, handle = self.Handle + # A point has been handled + if handle_type == HANDLE_POINT: + movex = max(-self.Points[handle].x + POINT_RADIUS, movex) + movey = max(-self.Points[handle].y + POINT_RADIUS, movey) + if scaling is not None: + movex = round_scaling(self.Points[handle].x + movex, scaling[0]) - self.Points[handle].x + movey = round_scaling(self.Points[handle].y + movey, scaling[1]) - self.Points[handle].y + # Try to connect point to a connector + new_pos = wx.Point(self.Points[handle].x + movex, self.Points[handle].y + movey) + connector = self.Parent.FindBlockConnector(new_pos, self.GetConnectionDirection()) + if connector: + if handle == 0 and self.EndConnected != connector: + connector.HighlightParentBlock(True) + connector.Connect((self, handle)) + self.SetStartPointDirection(connector.GetDirection()) + self.ConnectStartPoint(connector.GetPosition(), connector) + pos = connector.GetPosition() + movex = pos.x - self.oldPos.x + movey = pos.y - self.oldPos.y + if not connector.IsCompatible(self.GetEndConnectedType()): + self.SetValid(False) + self.Dragging = False + elif handle != 0 and self.StartConnected != connector: + connector.HighlightParentBlock(True) + connector.Connect((self, handle)) + self.SetEndPointDirection(connector.GetDirection()) + self.ConnectEndPoint(connector.GetPosition(), connector) + pos = connector.GetPosition() + movex = pos.x - self.oldPos.x + movey = pos.y - self.oldPos.y + if not connector.IsCompatible(self.GetStartConnectedType()): + self.SetValid(False) + self.Dragging = False + elif handle == 0: + self.MoveStartPoint(new_pos) + else: + self.MoveEndPoint(new_pos) + # If there is no connector, move the point + elif handle == 0: + self.SetValid(True) + if self.StartConnected: + self.StartConnected.HighlightParentBlock(False) + self.UnConnectStartPoint() + self.MoveStartPoint(new_pos) + else: + self.SetValid(True) + if self.EndConnected: + self.EndConnected.HighlightParentBlock(False) + self.UnConnectEndPoint() + self.MoveEndPoint(new_pos) + return movex, movey + # A segment has been handled, move a segment + elif handle_type == HANDLE_SEGMENT: + return self.MoveSegment(handle[0], movex, movey, scaling) + # Execute the default method for a graphic element + else: + return Graphic_Element.ProcessDragging(self, movex, movey, event, scaling) + + # Refreshes the wire model + def RefreshModel(self, move=True): + if self.StartConnected and self.StartPoint[1] in [WEST, NORTH]: + self.StartConnected.RefreshParentBlock() + if self.EndConnected and self.EndPoint[1] in [WEST, NORTH]: + self.EndConnected.RefreshParentBlock() + + # Change the variable that indicates if this element is highlighted + def SetHighlighted(self, highlighted): + self.Highlighted = highlighted + if not highlighted: + self.OverStart = False + self.OverEnd = False + self.Refresh() + + def HighlightPoint(self, pos): + refresh = False + start, end = self.OverStart, self.OverEnd + self.OverStart = False + self.OverEnd = False + # Test if a point has been handled + result = self.TestPoint(pos) + if result != None: + if result == 0 and self.StartConnected is not None: + self.OverStart = True + elif result != 0 and self.EndConnected is not None: + self.OverEnd = True + if start != self.OverStart or end != self.OverEnd: + self.Refresh() + + # Draws the highlightment of this element if it is highlighted + def DrawHighlightment(self, dc): + scalex, scaley = dc.GetUserScale() + dc.SetUserScale(1, 1) + dc.SetPen(MiterPen(HIGHLIGHTCOLOR, (2 * scalex + 5))) + dc.SetBrush(wx.Brush(HIGHLIGHTCOLOR)) + dc.SetLogicalFunction(wx.AND) + # Draw the start and end points if they are not connected or the mouse is over them + if len(self.Points) > 0 and (not self.StartConnected or self.OverStart): + dc.DrawCircle(round(self.Points[0].x * scalex), + round(self.Points[0].y * scaley), + (POINT_RADIUS + 1) * scalex + 2) + if len(self.Points) > 1 and (not self.EndConnected or self.OverEnd): + dc.DrawCircle(self.Points[-1].x * scalex, self.Points[-1].y * scaley, (POINT_RADIUS + 1) * scalex + 2) + # Draw the wire lines and the last point (it seems that DrawLines stop before the last point) + if len(self.Points) > 1: + points = [wx.Point(round((self.Points[0].x - self.Segments[0][0]) * scalex), + round((self.Points[0].y - self.Segments[0][1]) * scaley))] + points.extend([wx.Point(round(point.x * scalex), round(point.y * scaley)) for point in self.Points[1:-1]]) + points.append(wx.Point(round((self.Points[-1].x + self.Segments[-1][0]) * scalex), + round((self.Points[-1].y + self.Segments[-1][1]) * scaley))) + else: + points = [] + dc.DrawLines(points) + dc.SetLogicalFunction(wx.COPY) + dc.SetUserScale(scalex, scaley) + + if self.StartConnected is not None: + self.StartConnected.DrawHighlightment(dc) + self.StartConnected.Draw(dc) + if self.EndConnected is not None: + self.EndConnected.DrawHighlightment(dc) + self.EndConnected.Draw(dc) + + # Draws the wire lines and points + def Draw(self, dc): + Graphic_Element.Draw(self, dc) + if not self.Valid: + dc.SetPen(MiterPen(wx.RED)) + dc.SetBrush(wx.RED_BRUSH) + elif isinstance(self.Value, BooleanType) and self.Value: + if self.Forced: + dc.SetPen(MiterPen(wx.CYAN)) + dc.SetBrush(wx.CYAN_BRUSH) + else: + dc.SetPen(MiterPen(wx.GREEN)) + dc.SetBrush(wx.GREEN_BRUSH) + elif self.Value == "undefined": + dc.SetPen(MiterPen(wx.NamedColour("orange"))) + dc.SetBrush(wx.Brush(wx.NamedColour("orange"))) + elif self.Forced: + dc.SetPen(MiterPen(wx.BLUE)) + dc.SetBrush(wx.BLUE_BRUSH) + else: + dc.SetPen(MiterPen(wx.BLACK)) + dc.SetBrush(wx.BLACK_BRUSH) + # Draw the start and end points if they are not connected or the mouse is over them + if len(self.Points) > 0 and (not self.StartConnected or self.OverStart): + dc.DrawCircle(self.Points[0].x, self.Points[0].y, POINT_RADIUS) + if len(self.Points) > 1 and (not self.EndConnected or self.OverEnd): + dc.DrawCircle(self.Points[-1].x, self.Points[-1].y, POINT_RADIUS) + # Draw the wire lines and the last point (it seems that DrawLines stop before the last point) + if len(self.Points) > 1: + points = [wx.Point(self.Points[0].x - self.Segments[0][0], self.Points[0].y - self.Segments[0][1])] + points.extend([point for point in self.Points[1:-1]]) + points.append(wx.Point(self.Points[-1].x + self.Segments[-1][0], self.Points[-1].y + self.Segments[-1][1])) + else: + points = [] + dc.DrawLines(points) + # Draw the segment selected in red + if not getattr(dc, "printing", False) and self.SelectedSegment is not None: + dc.SetPen(MiterPen(wx.BLUE, 3)) + if self.SelectedSegment == len(self.Segments) - 1: + end = 0 + else: + end = 1 + dc.DrawLine(self.Points[self.SelectedSegment].x - 1, self.Points[self.SelectedSegment].y, + self.Points[self.SelectedSegment + 1].x + end, self.Points[self.SelectedSegment + 1].y) + if self.Value is not None and not isinstance(self.Value, BooleanType) and self.Value != "undefined": + dc.SetFont(self.Parent.GetMiniFont()) + dc.SetTextForeground(wx.NamedColour("purple")) + if self.ValueSize is None and isinstance(self.ComputedValue, (StringType, UnicodeType)): + self.ValueSize = self.Parent.GetMiniTextExtent(self.ComputedValue) + if self.ValueSize is not None: + width, height = self.ValueSize + if self.BoundingBox[2] > width * 4 or self.BoundingBox[3] > height * 4: + x = self.Points[0].x + width * (self.StartPoint[1][0] - 1) / 2 + y = self.Points[0].y + height * (self.StartPoint[1][1] - 1) + dc.DrawText(self.ComputedValue, x, y) + x = self.Points[-1].x + width * (self.EndPoint[1][0] - 1) / 2 + y = self.Points[-1].y + height * (self.EndPoint[1][1] - 1) + dc.DrawText(self.ComputedValue, x, y) + else: + middle = len(self.Segments) / 2 + len(self.Segments) % 2 - 1 + x = (self.Points[middle].x + self.Points[middle + 1].x - width) / 2 + if self.BoundingBox[3] > height and self.Segments[middle] in [NORTH, SOUTH]: + y = (self.Points[middle].y + self.Points[middle + 1].y - height) / 2 + else: + y = self.Points[middle].y - height + dc.DrawText(self.ComputedValue, x, y) + dc.SetFont(self.Parent.GetFont()) + dc.SetTextForeground(wx.BLACK) + + +#------------------------------------------------------------------------------- +# Graphic comment element +#------------------------------------------------------------------------------- + +def FilterHighlightsByRow(highlights, row, length): + _highlights = [] + for start, end, highlight_type in highlights: + if start[0] <= row and end[0] >= row: + if start[0] < row: + start = (row, 0) + if end[0] > row: + end = (row, length) + _highlights.append((start, end, highlight_type)) + return _highlights + +def FilterHighlightsByColumn(highlights, start_col, end_col): + _highlights = [] + for start, end, highlight_type in highlights: + if end[1] > start_col and start[1] < end_col: + start = (start[0], max(start[1], start_col) - start_col) + end = (end[0], min(end[1], end_col) - start_col) + _highlights.append((start, end, highlight_type)) + return _highlights + +""" +Class that implements a comment +""" + +class Comment(Graphic_Element): + + # Create a new comment + def __init__(self, parent, content, id = None): + Graphic_Element.__init__(self, parent) + self.Id = id + self.Content = content + self.Pos = wx.Point(0, 0) + self.Size = wx.Size(0, 0) + self.Highlights = [] + + # Make a clone of this comment + def Clone(self, parent, id = None, pos = None): + comment = Comment(parent, self.Content, id) + if pos is not None: + comment.SetPosition(pos.x, pos.y) + comment.SetSize(self.Size[0], self.Size[1]) + return comment + + # Method for keeping compatibility with others + def Clean(self): + pass + + # Delete this comment by calling the corresponding method + def Delete(self): + self.Parent.DeleteComment(self) + + # Refresh the comment bounding box + def RefreshBoundingBox(self): + self.BoundingBox = wx.Rect(self.Pos.x, self.Pos.y, self.Size[0] + 1, self.Size[1] + 1) + + # Changes the comment size + def SetSize(self, width, height): + self.Size.SetWidth(width) + self.Size.SetHeight(height) + self.RefreshBoundingBox() + + # Returns the comment size + def GetSize(self): + return self.Size.GetWidth(), self.Size.GetHeight() + + # Returns the comment minimum size + def GetMinSize(self): + dc = wx.ClientDC(self.Parent) + min_width = 0 + min_height = 0 + # The comment minimum size is the maximum size of words in the content + for line in self.Content.splitlines(): + for word in line.split(" "): + wordwidth, wordheight = dc.GetTextExtent(word) + min_width = max(min_width, wordwidth) + min_height = max(min_height, wordheight) + return min_width + 20, min_height + 20 + + # Changes the comment position + def SetPosition(self, x, y): + self.Pos.x = x + self.Pos.y = y + self.RefreshBoundingBox() + + # Changes the comment content + def SetContent(self, content): + self.Content = content + min_width, min_height = self.GetMinSize() + self.Size[0] = max(self.Size[0], min_width) + self.Size[1] = max(self.Size[1], min_height) + self.RefreshBoundingBox() + + # Returns the comment content + def GetContent(self): + return self.Content + + # Returns the comment position + def GetPosition(self): + return self.Pos.x, self.Pos.y + + # Moves the comment + def Move(self, dx, dy, connected = True): + self.Pos.x += dx + self.Pos.y += dy + self.RefreshBoundingBox() + + # Resizes the comment with the position and the size given + def Resize(self, x, y, width, height): + self.Move(x, y) + self.SetSize(width, height) + + # Method called when a RightUp event have been generated + def OnRightUp(self, event, dc, scaling): + # Popup the default menu + self.Parent.PopupDefaultMenu() + + # Refreshes the wire state according to move defined and handle selected + def ProcessDragging(self, movex, movey, event, scaling): + if self.Parent.GetDrawingMode() != FREEDRAWING_MODE and self.Parent.CurrentLanguage == "LD": + movex = movey = 0 + return Graphic_Element.ProcessDragging(self, movex, movey, event, scaling) + + # Refreshes the comment model + def RefreshModel(self, move=True): + self.Parent.RefreshCommentModel(self) + + # Method called when a LeftDClick event have been generated + def OnLeftDClick(self, event, dc, scaling): + # Edit the comment content + self.Parent.EditCommentContent(self) + + # Adds an highlight to the comment + def AddHighlight(self, infos, start, end, highlight_type): + if infos[0] == "content": + AddHighlight(self.Highlights, (start, end, highlight_type)) + + # Removes an highlight from the comment + def RemoveHighlight(self, infos, start, end, highlight_type): + RemoveHighlight(self.Highlights, (start, end, highlight_type)) + + # Removes all the highlights of one particular type from the comment + def ClearHighlight(self, highlight_type=None): + self.Highlights = ClearHighlights(self.Highlights, highlight_type) + + # Draws the highlightment of this element if it is highlighted + def DrawHighlightment(self, dc): + scalex, scaley = dc.GetUserScale() + dc.SetUserScale(1, 1) + dc.SetPen(MiterPen(HIGHLIGHTCOLOR)) + dc.SetBrush(wx.Brush(HIGHLIGHTCOLOR)) + dc.SetLogicalFunction(wx.AND) + + left = (self.Pos.x - 1) * scalex - 2 + right = (self.Pos.x + self.Size[0] + 1) * scalex + 2 + top = (self.Pos.y - 1) * scaley - 2 + bottom = (self.Pos.y + self.Size[1] + 1) * scaley + 2 + angle_top = (self.Pos.x + self.Size[0] - 9) * scalex + 2 + angle_right = (self.Pos.y + 9) * scaley - 2 + + polygon = [wx.Point(left, top), wx.Point(angle_top, top), + wx.Point(right, angle_right), wx.Point(right, bottom), + wx.Point(left, bottom)] + dc.DrawPolygon(polygon) + + dc.SetLogicalFunction(wx.COPY) + dc.SetUserScale(scalex, scaley) + + # Draws the comment and its content + def Draw(self, dc): + Graphic_Element.Draw(self, dc) + dc.SetPen(MiterPen(wx.BLACK)) + dc.SetBrush(wx.WHITE_BRUSH) + # Draws the comment shape + polygon = [wx.Point(self.Pos.x, self.Pos.y), + wx.Point(self.Pos.x + self.Size[0] - 10, self.Pos.y), + wx.Point(self.Pos.x + self.Size[0], self.Pos.y + 10), + wx.Point(self.Pos.x + self.Size[0], self.Pos.y + self.Size[1]), + wx.Point(self.Pos.x, self.Pos.y + self.Size[1])] + dc.DrawPolygon(polygon) + lines = [wx.Point(self.Pos.x + self.Size[0] - 10, self.Pos.y), + wx.Point(self.Pos.x + self.Size[0] - 10, self.Pos.y + 10), + wx.Point(self.Pos.x + self.Size[0], self.Pos.y + 10)] + dc.DrawLines(lines) + # Draws the comment content + y = self.Pos.y + 10 + for idx, line in enumerate(self.Content.splitlines()): + first = True + linetext = "" + words = line.split(" ") + if not getattr(dc, "printing", False): + highlights = FilterHighlightsByRow(self.Highlights, idx, len(line)) + highlights_offset = 0 + for i, word in enumerate(words): + if first: + text = word + else: + text = linetext + " " + word + wordwidth, wordheight = dc.GetTextExtent(text) + if y + wordheight > self.Pos.y + self.Size[1] - 10: + break + if wordwidth < self.Size[0] - 20: + if i < len(words) - 1: + linetext = text + first = False + else: + dc.DrawText(text, self.Pos.x + 10, y) + if not getattr(dc, "printing", False): + DrawHighlightedText(dc, text, FilterHighlightsByColumn(highlights, highlights_offset, highlights_offset + len(text)), self.Pos.x + 10, y) + highlights_offset += len(text) + 1 + y += wordheight + 5 + else: + if not first: + dc.DrawText(linetext, self.Pos.x + 10, y) + if not getattr(dc, "printing", False): + DrawHighlightedText(dc, linetext, FilterHighlightsByColumn(highlights, highlights_offset, highlights_offset + len(linetext)), self.Pos.x + 10, y) + highlights_offset += len(linetext) + 1 + if first or i == len(words) - 1: + if not first: + y += wordheight + 5 + if y + wordheight > self.Pos.y + self.Size[1] - 10: + break + dc.DrawText(word, self.Pos.x + 10, y) + if not getattr(dc, "printing", False): + DrawHighlightedText(dc, word, FilterHighlightsByColumn(highlights, highlights_offset, highlights_offset + len(word)), self.Pos.x + 10, y) + highlights_offset += len(word) + 1 + else: + linetext = word + y += wordheight + 5 + if y + wordheight > self.Pos.y + self.Size[1] - 10: + break +