|
| 1 | +# Dependencies |
| 2 | +import pygame |
| 3 | +from vectors import PointWithinRectangle |
| 4 | + |
| 5 | +# Object Classes |
| 6 | + |
| 7 | +class Connection(): |
| 8 | + # Connection Class is used to represent an event Connection |
| 9 | + # EventList refers to the list of Connections for an event that the Connection will be in |
| 10 | + |
| 11 | + def __init__(self, EventList, Function): |
| 12 | + self.EventList = EventList |
| 13 | + self.Function = Function |
| 14 | + |
| 15 | + def Disconnect(self): |
| 16 | + self.EventList.remove(self) |
| 17 | + |
| 18 | +class UIObject(): # EventDrivenObject + special methods |
| 19 | + # UIObject contains base components of all UIObjects including Form which contains UIObjects |
| 20 | + Events = tuple(["OnPropertyChange"]) # Default UI Events |
| 21 | + |
| 22 | + def __init__(self): |
| 23 | + self.__dict__["Callbacks"] = {} # Callbacks contains Connections for events |
| 24 | + |
| 25 | + self.Active = True # Elements are activated by default (so are rendered, process events) |
| 26 | + |
| 27 | + self.AddEvents(UIObject.Events) # This AddEvents function is called at all __init__ levels if the class has events to add |
| 28 | + |
| 29 | + def __setattr__(self, PropertyName, PropertyValue): # intercepts setting values for event-handling |
| 30 | + OldValue = None |
| 31 | + |
| 32 | + if hasattr(self, PropertyName): |
| 33 | + OldValue = getattr(self, PropertyName) |
| 34 | + |
| 35 | + self.__dict__[PropertyName] = PropertyValue # set value |
| 36 | + |
| 37 | + # note that we do not need a check for Callbacks as __init__ is called before this func |
| 38 | + self.Fire("OnPropertyChange", PropertyName, OldValue, PropertyValue) # event fire (PropertyName, OldValue, NewValue) |
| 39 | + |
| 40 | + def Warn(self, Message): # Used to make specific prints for the object |
| 41 | + print("UIObject [" + self.__class__.__name__ + "] WARNING: " + Message) |
| 42 | + |
| 43 | + def AddEvents(self, EventNames): # Similar to builder design pattern, you can add more events at stages |
| 44 | + for EventName in EventNames: |
| 45 | + if not (EventName in self.Callbacks): |
| 46 | + self.Callbacks[EventName] = [] |
| 47 | + else: |
| 48 | + self.Warn(F"{EventName} is already an event!") |
| 49 | + |
| 50 | + def Connect(self, EventName, Function): # Create Connection to an event in the object |
| 51 | + if EventName in self.Callbacks: |
| 52 | + CallbackConnection = Connection(self.Callbacks[EventName], Function) |
| 53 | + self.Callbacks[EventName].append(CallbackConnection) |
| 54 | + return CallbackConnection |
| 55 | + else: |
| 56 | + self.Warn(F"{EventName} is not an event!") |
| 57 | + |
| 58 | + def Fire(self, EventName, *args, **kwargs): # Used to signal an event happening: passed arguments to ALL current Connections |
| 59 | + if EventName in self.Callbacks: |
| 60 | + for Connection in self.Callbacks[EventName]: |
| 61 | + Connection.Function(self, *args, **kwargs) # CallbackData[0] is a Function |
| 62 | + else: |
| 63 | + self.Warn(F"{EventName} is not an event!") |
| 64 | + |
| 65 | + def ProcessEvents(self, Events): # All UIObjects have this but there is no default processing (that does something) |
| 66 | + pass |
| 67 | + |
| 68 | + def Render(self, Screen): # No default rendering protocol for all objects |
| 69 | + pass |
| 70 | + |
| 71 | +class Form(UIObject): |
| 72 | + # Form Object is a container for visual UIObjects: switching between forms changes the UIObjects rendered on screen |
| 73 | + Events = tuple(["OnLoad"]) |
| 74 | + |
| 75 | + CurrentForm = None # This class-wide property is used at the end (see # Current Form Behaviour) |
| 76 | + |
| 77 | + def __init__(self, Name): |
| 78 | + super().__init__() |
| 79 | + |
| 80 | + self.Name = Name |
| 81 | + self.Active = False # disabled until set as current form; to prevent unwanted rendering and processing in between form switches |
| 82 | + self.Entities = [] |
| 83 | + |
| 84 | + self.AddEvents(Form.Events) |
| 85 | + |
| 86 | + def SetAsCurrentForm(self, PreviousForm): |
| 87 | + pygame.display.set_caption("Chess - " + self.Name) |
| 88 | + |
| 89 | + self.Connect("OnLoad", SetAsCurrentForm) |
| 90 | + |
| 91 | + def AddEntity(self, Entity): |
| 92 | + self.Entities.append(Entity) |
| 93 | + |
| 94 | + def ProcessEvents(self, Events): |
| 95 | + for Entity in self.Entities: |
| 96 | + if Entity.Active: |
| 97 | + Entity.ProcessEvents(Events) |
| 98 | + |
| 99 | + def Render(self, Screen): |
| 100 | + Screen.fill(self.BackgroundColour) |
| 101 | + |
| 102 | + for Entity in self.Entities: |
| 103 | + if Entity.Active: |
| 104 | + Entity.Render(Screen) |
| 105 | + |
| 106 | +class App(Form): |
| 107 | + # Special kind of form which contains an app that controls rendering and processing (used for ChessGame which has rendering and processing in itself) |
| 108 | + # It essentially combines the power of a unique app and a form by rendering/processing form elements on top |
| 109 | + |
| 110 | + Events = tuple(["EventProcessed"]) |
| 111 | + |
| 112 | + def __init__(self, Name, RenderFunction=Form.Render, ProcessFunction=Form.ProcessEvents): |
| 113 | + super().__init__(Name) |
| 114 | + |
| 115 | + self.RenderFunction = RenderFunction |
| 116 | + self.EventFunction = ProcessFunction |
| 117 | + |
| 118 | + self.AddEvents(App.Events) |
| 119 | + |
| 120 | + def Render(self, Screen): |
| 121 | + self.RenderFunction(self, Screen) |
| 122 | + |
| 123 | + for Entity in self.Entities: # The order means form elements are rendered on top of the app |
| 124 | + if Entity.Active: |
| 125 | + Entity.Render(Screen) |
| 126 | + |
| 127 | + def ProcessEvents(self, Events): |
| 128 | + self.Fire("EventProcessed", self.EventFunction(self, Events)) |
| 129 | + |
| 130 | + for Entity in self.Entities: |
| 131 | + if Entity.Active: |
| 132 | + Entity.ProcessEvents(Events) |
| 133 | + |
| 134 | +class Box(UIObject): |
| 135 | + # Used as a rectangle container for subclasses TextLabel, ImageLabel |
| 136 | + Events = ("OnClick","OnMouseEnter", "OnMouseLeave") |
| 137 | + |
| 138 | + def __init__(self): |
| 139 | + super().__init__() |
| 140 | + |
| 141 | + # External Variables |
| 142 | + self.BackgroundColour = (255,255,255) |
| 143 | + self.BackgroundEnabled = True |
| 144 | + self.Size = (100,100) |
| 145 | + self.Position = (100,100) |
| 146 | + |
| 147 | + #Internal Variables |
| 148 | + self.Rectangle = pygame.Rect(100,100,100,100) |
| 149 | + self.MouseIn = False |
| 150 | + |
| 151 | + self.OutlineSize = 0 |
| 152 | + self.OutlineColour = (29,20,20) |
| 153 | + self.OutlineRectangle = self.Rectangle |
| 154 | + |
| 155 | + self.AddEvents(Box.Events) |
| 156 | + |
| 157 | + def UpdatePosition(Object, PropertyName, OldValue, NewValue): # Updates the Internal variable dependent on other variables |
| 158 | + if PropertyName == "Position": |
| 159 | + self.Rectangle = pygame.Rect(NewValue[0], NewValue[1], self.Rectangle.width, self.Rectangle.height) |
| 160 | + elif PropertyName == "Size": |
| 161 | + self.Rectangle = pygame.Rect(self.Rectangle.left, self.Rectangle.top, NewValue[0], NewValue[1]) |
| 162 | + |
| 163 | + def UpdateOutline(Object, PropertyName, OldValue, NewValue): # Similar to UpdatePosition |
| 164 | + if (PropertyName == "Size") or (PropertyName == "Position") or (PropertyName == "OutlineSize") or (PropertyName == "OutlineColour"): |
| 165 | + Left = self.Rectangle.left - self.OutlineSize |
| 166 | + Top = self.Rectangle.top - self.OutlineSize |
| 167 | + Width = self.Rectangle.width + (self.OutlineSize * 2) |
| 168 | + Height = self.Rectangle.height + (self.OutlineSize * 2) |
| 169 | + |
| 170 | + self.OutlineRectangle = pygame.Rect(Left, Top, Width, Height) # OutlineRectangle rendered underneath with dimensions of box plus some padding for the outline to be visible |
| 171 | + |
| 172 | + self.Connect("OnPropertyChange", UpdatePosition) |
| 173 | + self.Connect("OnPropertyChange", UpdateOutline) |
| 174 | + |
| 175 | + def ProcessEvents(self, Events): |
| 176 | + MousePosition = pygame.mouse.get_pos() |
| 177 | + |
| 178 | + if PointWithinRectangle(MousePosition[0], MousePosition[1], self.Rectangle.left, self.Rectangle.top, self.Rectangle.width, self.Rectangle.height): |
| 179 | + if not self.MouseIn: |
| 180 | + self.MouseIn = True |
| 181 | + self.Fire("OnMouseEnter") |
| 182 | + elif self.MouseIn: |
| 183 | + self.MouseIn = False |
| 184 | + self.Fire("OnMouseLeave") |
| 185 | + |
| 186 | + for Event in Events: |
| 187 | + if Event.type == pygame.MOUSEBUTTONDOWN: |
| 188 | + if self.Rectangle.collidepoint(Event.pos): # does the same thing as PointWithinRectangle |
| 189 | + self.Fire("OnClick", Event) |
| 190 | + |
| 191 | + def Render(self, Screen): |
| 192 | + if self.OutlineSize > 0: |
| 193 | + pygame.draw.rect(Screen, self.OutlineColour, self.OutlineRectangle) |
| 194 | + |
| 195 | + if self.BackgroundEnabled: |
| 196 | + pygame.draw.rect(Screen, self.BackgroundColour, self.Rectangle) |
| 197 | + |
| 198 | +class TextLabel(Box): |
| 199 | + # A form of box with text overlayed |
| 200 | + Events = tuple([]) |
| 201 | + |
| 202 | + def __init__(self): |
| 203 | + super().__init__() |
| 204 | + |
| 205 | + self.TextSize = 32 |
| 206 | + self.FontName = "freesansbold.ttf" |
| 207 | + |
| 208 | + self.Text = "" |
| 209 | + self.TextColour = (0,0,0) |
| 210 | + |
| 211 | + self.Font = pygame.font.Font(self.FontName, self.TextSize) |
| 212 | + |
| 213 | + self.AddEvents(TextLabel.Events) |
| 214 | + |
| 215 | + def UpdateFont(Object, PropertyName, OldValue, NewValue): |
| 216 | + if (PropertyName == "TextSize") or (PropertyName == "FontName"): |
| 217 | + self.Font = pygame.font.Font(self.FontName, self.TextSize) # Font object is a container controlling TextSize + Font |
| 218 | + self.PrepareFontToRender() |
| 219 | + elif (PropertyName == "Text") or (PropertyName == "TextColour") or (PropertyName == "BackgroundColour") or (PropertyName == "Rectangle"): |
| 220 | + self.PrepareFontToRender() |
| 221 | + |
| 222 | + self.Connect("OnPropertyChange", UpdateFont) |
| 223 | + |
| 224 | + def PrepareFontToRender(self): # Font object -> object that can be drawn to screen |
| 225 | + self.FontToRender = self.Font.render(self.Text, True, self.TextColour, self.BackgroundColour) # creates an object similar to a rectangle |
| 226 | + FontRectangle = self.FontToRender.get_rect() |
| 227 | + |
| 228 | + self.AlignedX = self.Rectangle.left + (self.Rectangle.width - FontRectangle.width)/2 # align text with background box |
| 229 | + self.AlignedY = self.Rectangle.top + (self.Rectangle.height - FontRectangle.height)/2 |
| 230 | + |
| 231 | + def Render(self, Screen): |
| 232 | + Box.Render(self, Screen) |
| 233 | + |
| 234 | + Screen.blit(self.FontToRender, (self.AlignedX, self.AlignedY, self.Rectangle.width, self.Rectangle.height)) |
| 235 | + |
| 236 | +class ImageLabel(Box): |
| 237 | + # Box with an image overlayed |
| 238 | + Events = tuple([]) |
| 239 | + |
| 240 | + def __init__(self): |
| 241 | + super().__init__() |
| 242 | + |
| 243 | + self.ImageSource = None |
| 244 | + self.Image = None |
| 245 | + |
| 246 | + self.AddEvents(ImageLabel.Events) |
| 247 | + |
| 248 | + def UpdateImage(self, PropertyName, OldValue, NewValue): |
| 249 | + if PropertyName == "ImageSource": |
| 250 | + self.Image = pygame.image.load("Images\\" + self.ImageSource) |
| 251 | + self.Image = pygame.transform.scale(self.Image, self.Rectangle.size) |
| 252 | + |
| 253 | + self.Connect("OnPropertyChange", UpdateImage) |
| 254 | + |
| 255 | + def Render(self, Screen): |
| 256 | + Box.Render(self, Screen) |
| 257 | + Screen.blit(self.Image, self.Position) |
| 258 | + |
| 259 | +# Current Form Behaviour |
| 260 | + |
| 261 | +def SetCurrentForm(FormToSet): # Only one form enabled at a time |
| 262 | + if Form.CurrentForm: |
| 263 | + Form.CurrentForm.Active = False |
| 264 | + |
| 265 | + FormToSet.Fire("OnLoad", Form.CurrentForm) |
| 266 | + |
| 267 | + FormToSet.Active = True |
| 268 | + Form.CurrentForm = FormToSet |
| 269 | + |
| 270 | +# Update and Render can be called by the event loop in the main code file |
| 271 | + |
| 272 | +def Update(Events): |
| 273 | + if Form.CurrentForm and Form.CurrentForm.Active: |
| 274 | + Form.CurrentForm.ProcessEvents(Events) |
| 275 | + |
| 276 | +def Render(Screen): |
| 277 | + if Form.CurrentForm and Form.CurrentForm.Active: |
| 278 | + Form.CurrentForm.Render(Screen) |
0 commit comments