Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conway game of life python code works fine in Processing 2, fails in Processing 3 #158

Open
toddwayne opened this issue Dec 9, 2019 · 4 comments

Comments

@toddwayne
Copy link

toddwayne commented Dec 9, 2019

Sorry, I couldn't find convenient resources for how to migrate Processing2 Python code to Processing3.

The pyde code below works fine in processing2, and fails with the following (useless to me) error in Processing3:

processing.app.SketchException: java.lang.NullPointerException
at processing.core.PApplet.dispose(PApplet.java:3823)
at processing.core.PApplet.die(PApplet.java:3734)
at processing.core.PApplet.die(PApplet.java:3744)
at processing.core.PApplet.loadFont(PApplet.java:6347)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.python.core.PyReflectedFunction.call(PyReflectedFunction.java:188)
at org.python.core.PyReflectedFunction.call(PyReflectedFunction.java:206)
at org.python.core.PyObject.call(PyObject.java:497)
at org.python.core.PyObject.call(PyObject.java:501)
at org.python.core.PyMethod.call(PyMethod.java:141)
at org.python.pycode._pyx11.f$0(conway3.pyde:1)
at org.python.pycode._pyx11.call_function(conway3.pyde)
at org.python.core.PyTableCode.call(PyTableCode.java:171)
at org.python.core.PyCode.call(PyCode.java:18)
at org.python.core.Py.runCode(Py.java:1614)
at org.python.core.Py.exec(Py.java:1658)
at org.python.pycode._pyx10.f$0(/Users/pokey/projects/Processing/conway3/conway3.pyde:96)
at org.python.pycode._pyx10.call_function(/Users/pokey/projects/Processing/conway3/conway3.pyde)
at org.python.core.PyTableCode.call(PyTableCode.java:171)
at org.python.core.PyCode.call(PyCode.java:18)
at org.python.core.Py.runCode(Py.java:1614)
at org.python.core.Py.exec(Py.java:1658)
at org.python.util.PythonInterpreter.exec(PythonInterpreter.java:276)
at jycessing.PAppletJythonDriver.processSketch(PAppletJythonDriver.java:230)
at jycessing.PAppletJythonDriver.findSketchMethods(PAppletJythonDriver.java:590)
at jycessing.Runner.runSketchBlocking(Runner.java:398)
at jycessing.mode.run.SketchRunner.lambda$2(SketchRunner.java:112)
at java.lang.Thread.run(Thread.java:748)

at jycessing.mode.run.SketchRunner.convertPythonSketchError(SketchRunner.java:224)
at jycessing.mode.run.SketchRunner.lambda$2(SketchRunner.java:119)
at java.lang.Thread.run(Thread.java:748)

Line 96 of conway3.pyde is referenced, but is a blank line in the actual code. I have no way to relate the Java errors to the python.

Since this interface won't let me attach a file, I'll try to just cut/paste it here (neither the .pyde, nor the .pyde.gz would attach).

# Converted to processing.py from the java example at processing.org by pokey and jacob, 26-Aug-2015
# Creeping elegance by pokey over the next several days (verbosity, color sets, speeds, help message, pre-sets...)
# Creeping elegance requested by Jake to add independent "die of old-age" option: May-2017

# Size of cells
cellSize = 10

# How likely for a cell to be alive at start (in percentage)
probabilityOfAliveAtStart = 15.0

# Variables for timer
interval = 100
lastRecordedTime = 0

# Colors for active/inactive cells
#            ("Resistor",  [black,     brown,     red,       orange,    yellow,    green,     blue,      violet,    gray,      white]
def NewColor3((r,g,b)):
    '''blend colors a'la fractals.py.'''
    minVal = 80 # must be even!
    maxVal =191 # must be odd!
    incVal = 12 # must be even!
    if (r%2 == 0):
        if (r >= (maxVal - 1)):
            r = maxVal
        else:
            r = r + incVal
            if (g%2 == 0):
                if (g >= (maxVal - 1)):
                    g = maxVal
                else:
                    g = g + incVal
            else:
                if (g <= (minVal + 1)):
                    g = minVal
                else:
                    g = g - incVal
    else:
        if (r <= (minVal + 1)):
            r = minVal
        else:
            r = r - incVal
            if (b%2 == 0):
                if (b >= (maxVal - 1)):
                    b = maxVal
                else:
                    b = b + incVal
            else:
                if (b <= (minVal + 1)):
                    b = minVal
                else:
                    b = b - incVal

    return (r,g,b)
# NewColor3

allColors = ['#000000'] # start with black and count by 4's to reduce the array size and make distinguishable colors
nextRGB = (101,111,121)
while True:
    (i,j,k) = nextRGB
    hexR = hex(i)
    hexG = hex(j)
    hexB = hex(k)
    hexRGB = '#'+hexR[-2:]+hexG[-2:]+hexB[-2:]
    if hexRGB not in allColors:
        allColors.append(hexRGB)
        nextRGB = NewColor3((i,j,k))
        #print (hexRGB)
    else:
        break
# allColors.append('#F0F0F0')

colorSets = [#("Name", ['list','of','colors'])
             ("Too Many ("+str(len(allColors))+")", allColors),
             ("Todd",      ['#000000', '#964A00', '#FF0000', '#FF7F00', '#FFFF00', '#DFFF00', '#00FF00', '#40E0D0', '#00FFFF', '#007FFF', '#3333FF', '#7F00FF', '#FF00FF', '#FF7F90', '#A0A0A0', '#E0E0E0']),
             ("Jake",      ['#000000', '#964B00', '#FF0000', '#FF5500', '#FFFF00', '#55FF00', '#00FF00', '#00FF55', '#00FFFF', '#0055FF', '#1111FF', '#5500FF', '#FF00FF', '#FF0055', '#808080', '#F0F0F0']),
             ("Resistor",  ['#000000', '#964B00', '#FF0000', '#FF5500', '#FFFF00', '#00FF00', '#3333FF', '#5500FF', '#808080', '#F0F0F0']),
             ("15 Shades of Gray",    ['#000000', '#222222', '#323232', '#424242', '#525252', '#626262', '#727272', '#828282', '#929292', '#A2A2A2', '#B2B2B2', '#C2C2C2', '#D2D2D2', '#E2E2E2', '#F2F2F2']),
             ("Mono-Green",['#000000', '#00C800']),
             ]
colorIndex = 0
(colorMap, colorList) = colorSets[colorIndex]

# Preset Patterns ("Name",(bounding, tuple),[(list,of),(x,y),(coordinates,to),(mark,alive)])
# Null List forces randomization of cells!
presetList = [
              ("Random",(0,0),[]),
              ("Exploder",(5,5),[(0,0),(2,0),(4,0),(0,1),(4,1),(0,2),(4,2),(0,3),(4,3),(0,4),(2,4),(4,4)]),
              ("Tumbler",(7,6),
               [(1,0),(2,0),(4,0),(5,0),(1,1),(2,1),(4,1),(5,1),(2,2),(4,2),(0,3),
                (2,3),(4,3),(6,3),(0,4),(2,4),(4,4),(6,4),(0,5),(1,5),(5,5),(6,5)]),
              ("Glider Gun",(38,15),
               [(0,2),(1,2),(0,3),(1,3),(9,2),(10,2),(8,3),(10,3),(8,4),(9,4),(16,4),(17,4),(16,5),
                (18,5),(16,6),(23,0),(24,0),(22,1),(24,1),(22,2),(23,2),(34,0),(35,0),(34,1),(35,1),
                (35,7),(36,7),(35,8),(37,8),(35,9),(24,12),(25,12),(26,12),(24,13),(25,14)])
              ]

presetIndex = 0

(presetName, presetTuple, presetCoord) = presetList[presetIndex]

# Preset Rules.  Set AgeBased FALSE to use possible counts of neighbors in birth,sustain lists.
# When AgeBased TRUE, then use min and max % of sum of ages needed to sustain life in birth,sustain lists.
# No more than 10 rules are recommended, as it is, they may fall off the bottom of small screens.
#   ("Name",AgeBased=False,[#,of,neighbors,to,be,born],[#,of,neighbors,to,stay,alive])
#   ("Name",AgeBased=True ,[min % sum of ages]        ,[max % sum of ages])
ruleSets = [ 
            ("Conway  ",False,[3],[2,3]),
            ("HighLife",False,[3,6],[2,3]),
            ("Todd Neighbor",False,[3,5,7],[1,3,5,7]),
            ("Todd Neighbor",False,[4,6],[2,3,5,7]),
            ("Todd Neighbor",False,[4,6],[2,3,4,5]),
            ("Todd Neighbor",False,[4,3],[2,5]),
            ("Todd AgeBased",True,[0.7854],[2.3562]),
            ("Todd AgeBased",True,[0.5],[15]),
            ("Todd AgeBased",True,[0.3],[1.5]),
            ("Todd AgeBased",True,[1],[75]),
            ]
ruleIndex = 0
(ruleName,ageBased,birthList,sustainList) = ruleSets[ruleIndex]

# Fixed-Width Font Me
font = loadFont("Courier-32.vlw")

# Generic initialization...
age = 0
iterations = 0
totalAlive = 0

# Dictionary of cells, Buffer to record the state of the cells and use this while changing the others in the interations
cells = {}
cellsBuffer = {}

# Booleans to remember pause state, and toroidal boundary request, show text while running, show help message for keys.
toroid = True
pause  = False
verbose= True
help   = False
dieOfAge=False

def randomCells():
    '''void function to randomize cells, once, so you don't have to init them in 2 places'''
    global cells, cellSize, probabilityOfAliveAtStart
    clearCells()
    for x in range (int(width/cellSize)):
        for y in range(int(height/cellSize)):
            state = int(random (100))
            if (state > probabilityOfAliveAtStart):
                state = 0
            else:
                state = state % len(colorList)
            cells[(x,y)] = state #  Save state of each cell
# end initCells

def setup():
    ''' void function to initialize board'''
    global cells, cellSize, cellsBuffer, font, minAlive, maxAlive
    textFont(font,32)

    # size (920, 640)
    size(displayWidth, displayHeight-42)
    wide=int(width/cellSize)
    high=int(height/cellSize)
    minAlive=wide*high
    maxAlive=0
    noSmooth()
    background('#000000') # Fill in black in case cells don't cover all the windows
    
    randomCells() # start right away with random cells
# end setup

def bufferCells(wide,high):
    '''void function to cycle through global arrays to save aside working set'''
    global cellsBuffer, cells
    
    for x in range (wide):
        for y in range (high):
            cellsBuffer[(x,y)] = cells.get((x,y),0)
# end bufferCells

def showHelp():
    ''' void function to display help message '''
    global presetList, ruleSets, font
    
    presetNames = []
    for (name,tuple,coordinates) in presetList:
        presetNames.append(name)
    presetNameString = ",".join(presetNames)
    
    ruleNames = []
    for (name, ageBased, births, sustains) in ruleSets:
        birthstr = "".join([str(n) for n in births])
        sust_str = "".join([str(n) for n in sustains])
        ruleNames.append(name+" ["+birthstr+"]["+sust_str+"]")
    rulesetNameString = "\n      ".join(ruleNames)
    
    textAlign(LEFT,TOP)
    background(64)
    fill("#FFFF80")
    fromLeft = 0
    fromTop  = 5
    messages = [" KEY: ACTION {Note: do not hit Arrow, Meta or Shift keys}",
                "   h: pause and show this help message",
                "   v: verbose, show generation, interval, etc.",
                "   p: pause the action",
                "   r: randomize cells",
                "   c: clear cells and reset generation count",
                "   t: toroidal space",
                "   d: die of Old Age (default=False; recommend True for Age rules)",
                " ,|.: slow down | speed up :  20 ms", 
                " ;|': slow down | speed up : 100 ms", 
                " n,<space>: single step an iteration (try when paused)", 
                " 1234567890: choose a new colormap",
                " [|]:  backward | forward  : preset patterns ["+presetNameString+"]",
                " -|=:  backward | forward  : rulesets (see below)",
                "   q: quit",
                "    ",
                "RULE: name [birth parameter][life parameter]",
                "      "+rulesetNameString,
                ]
    for i in range(len(messages)):
        text (messages[i], fromLeft, fromTop+i*26)
        
# end showHelp

def draw():
    ''' void function to draw grid for cells'''
    
    global lastRecordedTime, cellSize, cells, cellBuffer, verbose, dieOfAge
    global colorMap, interval, font, colorIndex, presetName, totalAlive
    global ruleName, ageBased, birthList, sustainList, minAlive, maxAlive
    
    wide = int(width/cellSize)
    high = int(height/cellSize)
    # Draw grid
    for x in range(wide):  # (int x=0; x<width/cellSize; x++) {
        for y in range(high): # (int y=0; y<height/cellSize; y++) {
            age = cells.get((x,y),0) # color 0, first element of colorList is dead!
            # if agebased, you can die of old age (cycle to start of colors); else stick at max age!
            if ageBased:
                fill(colorList[age%len(colorList)])
            else:
                fill(colorList[min(age,len(colorList)-1)]) # alive or dead!
            rect (x*cellSize, y*cellSize, cellSize, cellSize)

    # Iterate if timer ticks
    if (millis()-lastRecordedTime>interval) and (not pause):
        iteration()
        lastRecordedTime = millis()
        
    # Create new cells manually on pause
    if pause:
        # This stroke will draw the background grid
        if help:
            stroke('#000000')
        else:
            if toroid:
                stroke('#207020')
            else:
                stroke('#702020')
        # Map and avoid out of bound errors
        if (mousePressed):
            # force some delay in doing this to keep things calm (1/3 second)
            if (millis()-lastRecordedTime > 333):
                xCellOver = int(map(mouseX, 0, width, 0, width/cellSize))
                xCellOver = constrain(xCellOver, 0, width/cellSize-1)
                yCellOver = int(map(mouseY, 0, height, 0, height/cellSize))
                yCellOver = constrain(yCellOver, 0, height/cellSize-1)
        
                # Cycle forward through colors
                remember = cells.get((xCellOver,yCellOver),0)
                cells[(xCellOver,yCellOver)] = (cells.get((xCellOver,yCellOver),0) + 1) % len(colorList)
                # print ("set:",xCellOver,yCellOver,cells[(xCellOver,yCellOver)])
                if (remember == 0) and (cells[(xCellOver,yCellOver)] > 0):
                    totalAlive += 1
                elif (remember > 0) and (cells[(xCellOver,yCellOver)] == 0):
                    totalAlive -= 1
                lastRecordedTime = millis()
        else:
            # And then save to buffer once mouse goes up
            # Save cells to buffer (so we opeate with one array keeping the other intact)
            bufferCells(wide, high)
            
        # correctly update min/max in pauseMode
        minAlive = min(minAlive, totalAlive)
        maxAlive = max(maxAlive, totalAlive)
    else:
        if toroid:
            stroke('#003300')
        else:
            stroke('#330000')

    birthString = "".join([str(n) for n in birthList])
    sustainString = "".join([str(n) for n in sustainList])
    rulesToPrint = ruleName+" ["+birthString+"]["+sustainString+"]"
    
    if verbose:
        if toroid:
            space = "toroidal"
        else:
            space = "bounded"
        if dieOfAge:
            dieMsg= "...by old age."
        else:
            dieMsg = "eternal life!"
        textAlign(LEFT,TOP)
        fill('#FFFF80')
        text("rules: "+rulesToPrint,5,5)
        text("alive: "+str(totalAlive),5,40)
        text("       min: "+str(minAlive),5,75)
        text("       max: "+str(maxAlive),5,110)
        text(" age : "+str(iterations),5,145)
        text("delay: "+str(interval),5,180)
        text("color: "+str(colorIndex)+", \""+colorMap+"\"",5,215)
        text("space: "+space,5,250)
        text("death: "+dieMsg,5,285)
        text(" set : "+presetName,5,320)
    if help:
        showHelp()
# end draw

def iteration(): # When the clock ticks
    '''void function to calculate next generation of cells
    '''
    global toroid, cellSize, cells, cellsBuffer, iterations, verbose, dieOfAge
    global ruleIndex, birthList, sustainList, ageBased, colorList
    global totalAlive, minAlive, maxAlive
    
    wide = int(width/cellSize)
    high = int(height/cellSize)
    
    # Save cells to buffer (so we opeate with one array keeping the other intact)
    bufferCells(wide, high)
    # for statistics if verbose
    totalAlive = 0
    
    # Visit each cell:
    for x in range (wide):
        for y in range (high):
            # And visit all the neighbours of each cell
            neighbours = 0 # We'll count the neighbours
            sumOfAges  = 0
            nearList = [(-1,-1), (0,-1), (1,-1), (-1,0), (1,0), (-1,1), (0,1), (1,1)] # not (0,0)
            for (xx,yy) in nearList:
                if toroid: # Modulo math always checks around the edges, and is never out of bounds!
                    age = cellsBuffer.get(( (x+xx)%wide, (y+yy)%high),0)
                    if (age > 0):
                        neighbours += 1 # Check alive neighbours and count them
                        sumOfAges += age
                else: # Make sure you are not out of bounds; out of bounds are always empty neighbors!
                    if (((x+xx>=0) and (x+xx<wide)) and ((y+yy>=0) and (y+yy<high))):
                        age = cellsBuffer.get((x+xx,y+yy),0)
                        if (age > 0):
                            neighbours += 1 # Check alive neighbours and count them
                            sumOfAges += age
                        # if (neighbours > 0):
                        #     print ("how:",x+xx,y+yy,neighbours)

            # We've checked the neigbours: apply rules!
            age = cellsBuffer.get((x,y),0)

            if ageBased: # calculate % sum of ages (normalizing to max agess == 8*(len(colors)-1))
                maxAge = 8*(len(colorList)-1)
                percentOfAges = float(sumOfAges*100)/float(maxAge)
                # print (x,y,maxAge,sumOfAges,percentOfAges) # Comment!
                if (percentOfAges >= float(birthList[0]) and percentOfAges <= float(sustainList[0])):
                    cells[(x,y)] = (age+1) % len(colorList) # Use this to stick age @ max: min (age+1, len(colorList)-1)
                    totalAlive += 1
                else: # Age now cycles back to 0 (dying of old age)
                    if dieOfAge:
                        cells[(x,y)] = 0
                    else:
                        totalAlive += 1
            else: # Neighbor Count Based (traditional) rules
                if (age > 0): # The cell is alive: age or kill it if necessary
                    if (neighbours in sustainList):
                        if dieOfAge and age >= len(colorList)-1:
                            cells[(x,y)] = 0
                        else:
                            cells[(x,y)] = min( (age+1), (len(colorList)-1) ) # The cell is neither dead, nor doomed, so age it by 1 until max
                            totalAlive += 1
                    else:
                        cells[(x,y)] = 0 # Die unless it has 2 or 3 neighbours (default conway sustain list example)
                else: # The cell is dead: make it live if necessary      
                    if neighbours in birthList:
                        cells[(x,y)] = 1 # Only if it has 3 neighbours (default conway birth list example)
                        totalAlive += 1
                        
    # Keep some statistics to display if you are in verbose mode...
    iterations += 1
    minAlive = min(minAlive, totalAlive)
    maxAlive = max(maxAlive, totalAlive)

# end iteration

def clearCells():
    '''void function to clear all cells and reset age counter'''
    global cellSize, iterations, cells, minAlive, maxAlive, totalAlive
    iterations = 0
    wide=int(width/cellSize)
    high=int(height/cellSize)
    minAlive = wide*high
    maxAlive = 0
    totalAlive = 0
    for x in range(wide):
        for y in range(high):
            cells[(x,y)] = 0 # Save all to zero
# end clearCells
        
def keyPressed():
    '''void function to determine what to do when we type'''
    global pause, toroid, dieOfAge, cellSize, probabilityOfAliveAtStart, cells, colorList, colorSets, verbose
    global iterations, colorMap, interval, help, colorIndex, presetList, presetIndex, presetName
    global ruleSets, ruleIndex, ruleName, ageBased, birthList, sustainList, minAlive, maxAlive
    
    wide = int(width/cellSize)
    high = int(height/cellSize)

    if key in 'hH':
        pause = help = not help
        if help:
            textFont(font, 24)
        else:
            textFont(font, 32)
        
    # Restart: reinitialization of cells
    if key in 'rR':
        randomCells()
        presetName = "Random"

    # On/off of pause
    if key in 'pP':
        pause = not pause
        
    # On/off of toroidal shape
    if key in 'tT':
        toroid = not toroid

    # On/off die of Old Age
    if key in 'dD':
        dieOfAge = not dieOfAge

    # On/off of verbosity
    if key in 'vV':
        verbose = not verbose

    # Clear all
    if key in 'cC':
        clearCells()
    
    # Single step one generation
    if key in ' nN':
        iteration()

    # Choose ruleSet
    if key in '=': # move forward through rules
        ruleIndex = (ruleIndex+1)%len(ruleSets)
    if key in '-': # move backward through rules
        ruleIndex = (ruleIndex-1)%len(ruleSets)
    (ruleName,ageBased,birthList,sustainList) = ruleSets[ruleIndex]

    # Choose preset
    if key in '[]':
        if key in '[':
            direction = -1
        else:
            direction = +1
        presetIndex = (presetIndex+direction)%len(presetList)
        clearCells()
        (presetName,(presetX,presetY),coordList) = presetList[presetIndex]
        xStart = int(wide/2) - int(presetX/2)
        yStart = int(high/2) - int(presetY/2)
        if coordList:
            for (x,y) in coordList:
                cells[(xStart + x, yStart + y)] = 1
        else: # empty or null coordList means go random!
            randomCells()
        
    # Choose colorSet
    if key in '1234567890':
        # convert key to integer for indexing; limit indices to modulo len( colorSets )
        colorIndex = int(key) % len(colorSets)
        (colorMap, colorList) = colorSets[colorIndex]
    
    # Change Speed:
    if key in ',':
        interval = min (2000, interval+20)
    if key in ';':
        interval = min (2000, interval+100)
    if key in '.':
        interval = max (20, interval-20)
    if key in '\'':
        interval = max (20, interval-100)

    # Quit
    if key in 'qQ':
        exit()

# end keyPressed
#
@jeremydouglass
Copy link
Collaborator

jeremydouglass commented Dec 17, 2019

Please format code blocks with ``` or with the <> button.

Just quickly guessing, but two things jump out at me:

org.python.pycode._pyx10.f$0(/Users/pokey/projects/Processing/conway3/conway3.pyde:96)

Line 96 of your code.

at processing.core.PApplet.loadFont(PApplet.java:6347)

loadFont, which is invoked with

font = loadFont("Courier-32.vlw")

Those would be my starting guesses -- that you have a font problem, for example you didn't include that file in the new sketch or it isn't on the new system or it is damaged / invalid / no longer compatible for some reason... and/or or the execution chain around line 96 is where you could start debugging.

@toddwayne
Copy link
Author

Thanks for several tips -- can do on the <> for code blocks.
Part of my confusion: font=loadFont is at line 122 of the code (by either the processing Editor, or other external editor, an original source of my confusion.
The font file itself (Courier-32.vlw) is in the same relative location for Processing 2 and 3, in the data/ directory. I also found a Courier-30.vlw file. Neither file can be loaded.
I recreated the Courier-32.vlw with the in-built "create font", but I never get past this point: same error.

@toddwayne
Copy link
Author

I got this running after doing 2 things:

  1. do not use loadFont, rather switch to createFont (and relocate the code to the setup() function
  2. replace size(displayW, displayH) with fullScreen(). That's not exactly what I wanted, but close enough!

@toddwayne
Copy link
Author

toddwayne commented Dec 23, 2019

New Code Snippet:

def settings():
    fullScreen()
      
def setup():
    ''' void function to initialize board'''
    global cells, cellSize, cellsBuffer, font, minAlive, maxAlive
    # Fixed-Width Font Me
    font = createFont("Courier",32)

    textFont(font,32)

    # size (920, 640)
    # Processing2: size(displayWidth, displayHeight-42)
    # Processing3: moved to settings()
    
    wide=int(width/cellSize)
    high=int(height/cellSize)
    minAlive=wide*high
    maxAlive=0
    noSmooth()
    background('#000000') # Fill in black in case cells don't cover all the windows
    
    randomCells() # start right away with random cells
# end setup

I'd still like to use something like 'displayHeight-42' to dynamically reduce the height based on screen-size. This allows for the presence of the Mac Menu Bar and the top edge of the Mac Windowing system to appear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants