""" rosegraphics.py - a simple Graphics library for Python. Its key feature is: -- USING this library provides a simple introduction to USING objects. Other key features include: -- It has a rich set of classes, methods and instance variables. -- In addition to classes like Circles that are natural for students, it has other kinds of classes like RoseWindow and FortuneTeller to provide a richer set of examples than "just" a graphics library. -- It allows one to do a reasonable set of graphics operations with reasonable efficiency. The API mimics Java's Shape API for the most part. -- It is built on top of tkinter and its extension ttk (the standard graphics libraries that come with Python). -- Unlike tkinter, it is NOT event-driven and hence can be used before students see that paradigm. (There is a behind-the-scenes facilty for listening for and responding to events, for those who want to do so.) -- It attempts to be as bullet-proof as possible, to make it easy for beginners to use it. In particular, it attempts to provide reasonable error messages when a student misuses the API. -- It was inspired by zellegraphics but is a complete re-implemenation that attempts to: -- Be more bullet-proof. -- Provide a richer set of examples for using objects. -- Have an API that is more like Java's Shape API than tkinter's (older) API. -- While it can serve as an example for defining classes, it is NOT intended to do so for beginners. It is excellent for helping students learn to USE objects; it is NOT perfect for helping students learn to WRITE CLASSES. See the MAIN function below for typical examples of its use. Authors: David Mutchler, Mark Hays, Michael Wollowswki, Matt Boutell, Chandan Rupakheti, Claude Anderson and their colleagues, with thanks to John Zelle for inspiration and hints. First completed version: September 2014. """ # FIXME (errors): # -- clone() does not really make a copy; it just makes a new one # but without cloning all the attributes. # -- _ShapeWithCenter claims that things like Ellipse are subclasses, # but they are not at this point, I think. In general, need to # deal with overlap between _ShapeWithCenter and _RectangularShape. # KEEP both of them to have some classes have corner_1 and corner_2 # while others have center and ... # FIXME (things that have yet to be implemented): # -- Allow multiple canvasses. # -- Better close_on ... ala zellegraphics. # -- Keyboard. # -- Better Mouse. # -- Add type hints. # -- Catch all Exceptions and react appropriately. # -- Implement unimplemented classes. # -- Add and allow FortuneTellers and other non-canvas classes. import tkinter from tkinter import font as tkinter_font import time import turtle # ---------------------------------------------------------------------- # All the windows that are constructed during a run share the single # _master_Tk (a tkinter.Tk object) # as their common root. The first construction of a RoseWindow # sets this _master_Tk to a Tkinter.Tk object. # ---------------------------------------------------------------------- _master_Tk = None # ---------------------------------------------------------------------- # At the risk of not being Pythonic, we provide a simple type-checking # facility that attempts to provide meaningful error messages to # students when they pass arguments that are not of the expected type. # ---------------------------------------------------------------------- class WrongTypeException(Exception): """ Not yet implemented. """ pass def check_types(pairs): """ Not yet implemented fully. """ for pair in pairs: value = pair[0] expected_type = pair[1] if not isinstance(value, expected_type): raise WrongTypeException(pair) # ---------------------------------------------------------------------- # Serialization facility # ---------------------------------------------------------------------- def _serialize_shapes(self): """Returns a list of strings representing the shapes in sorted order.""" # Idea: dump all the stats on all shapes, return a sorted list for easy comparison. # Problem: the order in which keys appear in dictionaries is random! # Solution: sort keys and manually print shapes = [shape.__dict__ for shape in self.initial_canvas.shapes] keys_by_shape = [sorted(shape) for shape in shapes] for k in range(len(shapes)): shapes[k]['_method_for_drawing'] = None shapes[k]['shape_id_by_canvas'] = None result = [] for k in range(len(keys_by_shape)): shape = shapes[k] result.append([]) for key in keys_by_shape[k]: result[-1].append(str(key) + ":" + str(shape[key])) result[-1] = str(result[-1]) return "\n".join(sorted(result)) # ---------------------------------------------------------------------- # RoseWindow is the top-level object. # It starts with a single RoseCanvas. # ---------------------------------------------------------------------- class RoseWindow(object): """ A RoseWindow is a window that pops up when constructed. It can have RoseWidgets on it and starts by default with a single RoseCanvas upon which one can draw shapes. To construct a RoseWindow, use: - rg.RoseWindow() or use any of its optional arguments, as in these examples: window = rg.RoseWindow(400, 300) # 400 wide by 300 tall window = rg.RoseWindow(400, 300, 'Funny window') # with a title Instance variables include: width: width of this window (in pixels) height: width of this window (in pixels) title: displayed on the window's bar widgets: the things attached to this window """ def __init__(self, width=400, height=300, title='Rose Graphics', color='black', canvas_color=None, make_initial_canvas=True): """ Pops up a tkinter.Toplevel window with (by default) a RoseCanvas (and associated tkinter.Canvas) on it. Arguments are: -- width, height: dimensions of the window (in pixels). -- title: title displayed on the windoww. -- color: background color of the window -- canvas_color: background color of the canvas displayed on the window by default -- make_initial_canvas: -- If True, a default canvas is placed on the window. -- Otherwise, no default canvas is placed on the window. If this is the first RoseWindow constructed, then a hidden Tk object is constructed to control the event loop. Preconditions: :type width: int :type height: int :type title: str :type color: Color :type canvas_color: Color :type make_initial_canvas: bool """ # check_types([(width, (int, float)), # (height, (int, float)), # (title, (Color, str) # -------------------------------------------------------------- # The _master_Tk controls the mainloop for ALL the RoseWindows. # If this is the first RoseWindow constructed in this run, # then construct the _master_Tk object. # -------------------------------------------------------------- global _master_Tk if not _master_Tk: _master_Tk = tkinter.Tk() _master_Tk.withdraw() else: time.sleep(0.1) # Helps the window appear on TOP of Eclipse # -------------------------------------------------------------- # Has a tkinter.Toplevel, and a tkinter.Canvas on the Toplevel. # -------------------------------------------------------------- self.toplevel = tkinter.Toplevel(_master_Tk, background=color, width=width, height=height) self.toplevel.title(title) self._is_closed = False self.toplevel.protocol("WM_DELETE_WINDOW", self.close) # FIXME: The next two need to be properties to have # setting happen correctly. Really belongs to RoseCanvas. # See comments elsewhere on this. self.width = width self.height = height if make_initial_canvas: self.initial_canvas = RoseCanvas(self, width, height, canvas_color) else: self.initial_canvas = None self.widgets = [self.initial_canvas] # FIXME: Do any other tailoring of the toplevel as desired, # e.g. borderwidth and style... # -------------------------------------------------------------- # Catch mouse clicks and key presses. # -------------------------------------------------------------- self.mouse = Mouse() self.keyboard = Keyboard() self.toplevel.bind('