Skip to content

Instantly share code, notes, and snippets.

@truppelito
Last active October 15, 2017 08:34
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save truppelito/7877083 to your computer and use it in GitHub Desktop.
Script for a full-fledged calculator on the iPhone using the perfect Pythonista app.
from scene import *
from sound import *
from math import *
from random import random
import clipboard
FONT = 'HelveticaNeue-Bold'
PADDING = 6
COLORS = ((1,1,1),(0.8,0,0),(0,0,0.9),(0,0.7,0),(0.6,0.1,0.6),(1,0,0),(0.8,0.8,0),(0.9,0.5,0),(0.2,0.2,0.2))
TEXT1_HEIGHT = 20
TEXT2_HEIGHT = 18
TEXT3_HEIGHT = 14
SCREEN_HEIGHT = TEXT1_HEIGHT + TEXT2_HEIGHT + TEXT3_HEIGHT + 5 * PADDING
BUTTONS_W = 8
BUTTONS_H = 5
BUTTONS = (('=','','',6,0,2,1,5),
('Rand','Rand','random()',0,4,1,1,7),
('(','(','(',1,4,1,1,0),
(')',')',')',2,4,1,1,0),
('7','7','7',4,4,1,1,0),
('8','8','8',5,4,1,1,0),
('9','9','9',6,4,1,1,0),
('4','4','4',4,3,1,1,0),
('5','5','5',5,3,1,1,0),
('6','6','6',6,3,1,1,0),
('1','1','1',4,2,1,1,0),
('2','2','2',5,2,1,1,0),
('3','3','3',6,2,1,1,0),
('Ans','Ans','self.lastResult',4,1,1,1,0),
('0','0','0',5,1,1,1,0),
('.','.','.',6,1,1,1,0),
('+',' + ','+',7,4,1,1,2),
('-',' - ','-',7,3,1,1,2),
('*',' * ','*',7,2,1,1,2),
('/',' / ','* 1.0 /',7,1,1,1,2),
('>',' > ','>',3,4,1,1,6),
('>=',' >= ','>=',3,2,1,1,6),
('<',' < ','<',3,3,1,1,6),
('<=',' <= ','<=',3,1,1,1,6),
('==',' equals ','==',3,0,1,1,6),
('<-','','',5,0,1,1,1),
('AC','','',4,0,1,1,1),
('C','','',4,0,1,1,1),
('sin','sin(','sin(',0,3,1,1,3),
('cos','cos(','cos(',1,3,1,1,3),
('tan','tan(','tan(',2,3,1,1,3),
('asin','asin(','asin(',0,2,1,1,3),
('acos','acos(','acos(',1,2,1,1,3),
('atan','atan(','atan(',2,2,1,1,3),
('exp',' ^ ','**',0,1,1,1,7),
('sqrt','sqrt(','sqrt(', 0,0,1,1,7),
('!','!(','factorial(',1,1,1,1,7),
('ln','ln(','log(',1,0,1,1,7),
('e','e','e',2,1,1,1,4),
('pi','pi','pi',2,0,1,1,4))
def infoForInfoAtIndex(info, indexOfInfo, indexOfReturnedInfo):
for b in BUTTONS:
if b[indexOfInfo] == info:
return b[indexOfReturnedInfo]
def rounded_rect(r, bg, stroke = None, strokeWeight = 2, rad = 5):
if stroke:
no_stroke()
fill(stroke.r, stroke.g, stroke.b)
_draw_rounded_rect(r, rad)
insetRect = Rect(r.x + strokeWeight, r.y + strokeWeight, r.w - 2*strokeWeight, r.h - 2*strokeWeight)
fill(bg.r, bg.g, bg.b)
_draw_rounded_rect(insetRect, rad - strokeWeight/2)
else:
fill(bg.r, bg.g, bg.b)
_draw_rounded_rect(r, rad)
def _draw_rounded_rect(r, rad):
rect(r.x, r.y + rad, r.w, r.h - 2*rad)
rect(r.x + rad, r.y, r.w - 2*rad, r.h)
ellipse(r.x, r.y + r.h - 2*rad, 2*rad, 2*rad)
ellipse(r.x + r.w - 2*rad, r.y + r.h - 2*rad, 2*rad, 2*rad)
ellipse(r.x, r.y, 2*rad, 2*rad)
ellipse(r.x + r.w - 2*rad, r.y, 2*rad, 2*rad)
class Button (Layer):
titleFont = 'Helvetica'
rounded = True
def __init__(self, frame, title = None):
Layer.__init__(self, frame)
self.normalBG = Color(0,0,0)
self.normalStroke = Color(1,1,1)
self.normalTextColor = Color(1,1,1)
self.selectedBG = Color(0.1,0.1,0.1)
self.selectedStroke = Color(1,1,1)
self.selectedTextColor = Color(1,1,1)
self.stroke_weight = 2
self.selected = False
self.action = None
self._title = title
self._frame = frame
self.reloadBG()
def getFrame(self):
return self._frame
def setFrame(self, f):
self._frame = f
self.reloadBG()
frame = property(getFrame, setFrame)
def getTitle(self):
return self._title
def setTitle(self, t):
if t is not self._title:
self._title = t
self.reloadBG()
title = property(getTitle, setTitle)
def reloadBG(self):
if not hasattr(self, '_title'):
return
if not self._title:
self.text_image = None
else:
text_size = 18.5
self.text_image_size = Size(self.frame.w, self.frame.h)
while self.text_image_size.w >= self.frame.w - self.stroke_weight - 1 or self.text_image_size.h >= self.frame.h - self.stroke_weight - 1:
text_size -= 0.5
self.text_image, self.text_image_size = render_text(self._title, Button.titleFont, text_size)
def draw(self, a = 1):
a *= self.alpha
if a <= 0: return
if self.selected:
self.background = self.selectedBG
self.stroke = self.selectedStroke
else:
self.background = self.normalBG
self.stroke = self.normalStroke
p = self.superlayer.convert_to_screen(self.frame.origin())
r = Rect(p.x, p.y, self.frame.w, self.frame.h)
if Button.rounded:
rounded_rect(r, self.background, self.stroke, self.stroke_weight)
else:
fill(self.background.r, self.background.g, self.background.b)
stroke(self.stroke.r, self.stroke.g, self.stroke.b)
stroke_weight(self.stroke_weight)
rect(r.x,r.y,r.w,r.h)
if self.text_image:
point = Point((self.frame.w - self.text_image_size.w) / 2, (self.frame.h - self.text_image_size.h) / 2)
p = self.convert_to_screen(point)
if self.selected:
tc = self.selectedTextColor
else:
tc = self.normalTextColor
tint(tc.r, tc.g, tc.b)
image(self.text_image, p.x, p.y, self.text_image_size.w, self.text_image_size.h)
def touch_began(self, touch):
self.selected = True
def touch_moved(self, touch):
p = 10
rect = Rect(self.frame.x - p, self.frame.y - p, self.frame.w + 2*p, self.frame.h + 2*p)
if self.superlayer.convert_from_screen(touch.location) in rect:
self.selected = True
else:
self.selected = False
def touch_ended(self, touch):
self.selected = False
if self.superlayer.convert_from_screen(touch.location) in self.frame and callable(self.action):
self.action(self)
class Calculator (Scene):
def setup(self):
load_effect('Click_1')
set_volume(1)
self.root_layer = Layer(self.bounds)
self.theme = 0
self.buttons = []
self.lastCalculation = []
self.currentCalculation = []
self.changedOrientation = False
# Load keyboard
Button.titleFont = FONT
Button.rounded = False
for b in BUTTONS:
button = Button(self.rectForBttn(b[3], b[4], b[5], b[6]), b[0])
color = Color(COLORS[b[7]][0], COLORS[b[7]][1], COLORS[b[7]][2])
button.normalBG = Color(color.r*0.85,color.g*0.85,color.b*0.85)
button.normalStroke = color
button.normalTextColor = Color(0,0,0)
button.selectedBG = Color(0,0,0)
button.selectedStroke = color
button.selectedTextColor = color
button.action = self.bttnClicked
self.buttons.append(button)
self.root_layer.add_layer(button)
self.showClearBttn('AC')
# Load result screen
self.screen = Button(self.rectForScreen(), '')
color = COLORS[8]
self.screen.normalBG = Color(color[0], color[1], color[2])
self.screen.normalStroke = Color(color[0], color[1], color[2])
self.screen.selectedBG = Color(color[0]*0.7, color[1]*0.7, color[2]*0.7)
self.screen.selectedStroke = Color(color[0]*0.7, color[1]*0.7, color[2]*0.7)
self.screen.action = self.screenClicked
self.root_layer.add_layer(self.screen)
def layoutForNewOrientation(self):
for b in self.buttons:
x = infoForInfoAtIndex(b.title, 0, 3)
y = infoForInfoAtIndex(b.title, 0, 4)
sx = infoForInfoAtIndex(b.title, 0, 5)
sy = infoForInfoAtIndex(b.title, 0, 6)
b.frame = self.rectForBttn(x, y, sx, sy)
#b.animate('frame', self.rectForBttn(x, y, sx, sy))
self.screen.frame = self.rectForScreen()
#self.screen.animate('frame', self.rectForScreen())
def draw(self):
background(0,0,0)
if self.changedOrientation:
self.changedOrientation = False
self.layoutForNewOrientation()
#self.delay(0.5, self.layoutForNewOrientation)
if hasattr(self, 'error'):
self.error += self.dt
if self.error > 0.3:
color = Color(COLORS[8][0],COLORS[8][1],COLORS[8][2])
del self.error
else:
color = Color(0.5,0,0)
else:
color = Color(COLORS[8][0],COLORS[8][1],COLORS[8][2])
self.screen.normalBG = color
self.screen.normalStroke = color
self.root_layer.update(self.dt)
self.root_layer.draw()
# Draw text
tint(1, 1, 1)
txt = ''
for i in self.currentCalculation:
txt += infoForInfoAtIndex(i, 0, 1)
text(txt, x = PADDING*2, y = self.bounds.h - SCREEN_HEIGHT + PADDING, alignment = 9, font_size = TEXT1_HEIGHT, font_name = FONT)
if hasattr(self, 'lastResult'):
txt = '= ' + str(self.lastResult)
text(txt, x = PADDING*2, y = self.bounds.h - SCREEN_HEIGHT + PADDING*2 + TEXT1_HEIGHT, alignment = 9, font_size = TEXT2_HEIGHT, font_name = FONT)
txt = ''
for i in self.lastCalculation:
txt += infoForInfoAtIndex(i, 0, 1)
text(txt, x = PADDING*2, y = self.bounds.h - SCREEN_HEIGHT + PADDING*3 + TEXT1_HEIGHT+TEXT2_HEIGHT, alignment = 9, font_size = TEXT3_HEIGHT, font_name = FONT)
if not len(self.lastCalculation) and not len(self.currentCalculation) and not hasattr(self, 'lastResult'):
sr = self.rectForScreen()
r = Rect(sr.x, sr.y, sr.w, sr.h - PADDING)
text('Calculator Pro', x = r.center().x, y = r.center().y, font_size = TEXT1_HEIGHT, font_name = FONT, alignment = 8)
text('Please type something', x = r.center().x, y = r.center().y, font_size = TEXT3_HEIGHT, font_name = FONT, alignment = 2)
def bttnClicked(self, sender):
play_effect('Click_1')
if not len(self.currentCalculation) and sender.title in ('+','-','*','/','>','<','>=','<=','==','exp'):
self.currentCalculation.append('Ans')
if sender.title == '=':
if not len(self.currentCalculation):
self.currentCalculation = list(self.lastCalculation)
self.calculate()
elif sender.title == 'C':
self.currentCalculation = []
self.showClearBttn('AC')
elif sender.title == 'AC':
self.currentCalculation = []
self.lastCalculation = []
if hasattr(self, 'lastResult'):
del self.lastResult
elif sender.title == '<-':
self.currentCalculation = self.currentCalculation[:len(self.currentCalculation)-1]
if not len(self.currentCalculation):
self.showClearBttn('AC')
else:
self.currentCalculation.append(sender.title)
self.showClearBttn('C')
#if len(self.currentCalculation) is 1 and sender.title in ('sqrt','ln','!','sin','cos', 'tan', 'asin', 'acos', 'atan'):
#self.currentCalculation.append('Ans')
def screenClicked(self, sender):
play_effect('Click_1')
if hasattr(self, 'lastResult'):
clipboard.set(str(self.lastResult))
def calculate(self):
evalString = ''
for title in self.currentCalculation:
evalString += infoForInfoAtIndex(title, 0, 2)
if not hasattr(self, 'lastResult'):
self.lastResult = 0
try:
self.lastResult = eval(evalString)
except Exception:
evalString += ')'
try:
self.lastResult = eval(evalString)
except Exception:
self.error = 0
if not hasattr(self, 'error'):
self.lastCalculation = list(self.currentCalculation)
self.currentCalculation = []
self.showClearBttn('AC')
def rectForBttn(self, x, y, sx, sy):
bttnW = (self.bounds.w - PADDING * (BUTTONS_W + 1)) / (BUTTONS_W)
bttnH = (self.bounds.h - PADDING * (BUTTONS_H + 1) - SCREEN_HEIGHT) / BUTTONS_H
return Rect(PADDING * (x+1) + bttnW * x, PADDING * (y+1) + bttnH * y, bttnW + (bttnW + PADDING) * (sx-1), bttnH + (bttnH + PADDING) * (sy-1))
def rectForScreen(self):
return Rect(PADDING, self.bounds.h - SCREEN_HEIGHT, self.bounds.w - 2*PADDING, SCREEN_HEIGHT - PADDING)
def buttonForTitle(self, title):
for b in self.buttons:
if b.title == title:
return b
def showClearBttn(self, title):
show = self.buttonForTitle(title)
hide = self.buttonForTitle('C' if title == 'AC' else 'AC')
if hide in self.root_layer.sublayers:
hide.remove_layer()
if show not in self.root_layer.sublayers:
self.root_layer.add_layer(show)
def should_rotate(self, orientation):
self.changedOrientation = True
return True #orientation == LANDSCAPE
run(Calculator())
@PKHG
Copy link

PKHG commented Oct 15, 2017

p = self.superlayer.convert_to_screen(self.frame.origin())
gives an error message ;-)
using
ppp= self.frame.origin()
and
r = Rect(ppp.x, ppp.y, 100, 100)
makes the calculator working.

Missing features: int( and 1/x
Got a Button via: ('int', 'int', 'int(', 4,5,1,1,3) with new space above ... 45 * PADDING ...
But not no action yet performed

But otherwise, a very fine Calculatot '-) , with a lot of things to learn from, really PRO

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment