Script for a full-fledged calculator on the iPhone using the perfect Pythonista app.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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