calculator.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. #!/usr/bin/python3
  2. """
  3. Calculator for ToaruOS
  4. """
  5. import subprocess
  6. import sys
  7. import cairo
  8. import yutani
  9. import text_region
  10. import toaru_fonts
  11. from button import Button
  12. from menu_bar import MenuBarWidget, MenuEntryAction, MenuEntrySubmenu, MenuEntryDivider, MenuWindow
  13. from about_applet import AboutAppletWindow
  14. import yutani_mainloop
  15. import ast
  16. import operator as op
  17. operators = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul,
  18. ast.Div: op.truediv, ast.Pow: op.pow, ast.BitXor: op.xor,
  19. ast.USub: op.neg}
  20. app_name = "Calculator"
  21. version = "1.0.0"
  22. _description = f"<b>{app_name} {version}</b>\n© 2017-2018 K. Lange\n\nSimple four-function calculator using Python.\n\n<color 0x0000FF>http://github.com/klange/toaruos</color>"
  23. def eval_expr(expr):
  24. """
  25. >>> eval_expr('2^6')
  26. 4
  27. >>> eval_expr('2**6')
  28. 64
  29. >>> eval_expr('1 + 2*3**(4^5) / (6 + -7)')
  30. -5.0
  31. """
  32. return eval_(ast.parse(expr, mode='eval').body)
  33. def eval_(node):
  34. if isinstance(node, ast.Num): # <number>
  35. return node.n
  36. elif isinstance(node, ast.BinOp): # <left> <operator> <right>
  37. return operators[type(node.op)](eval_(node.left), eval_(node.right))
  38. elif isinstance(node, ast.UnaryOp): # <operator> <operand> e.g., -1
  39. return operators[type(node.op)](eval_(node.operand))
  40. else:
  41. raise TypeError("invalid operation")
  42. class CalculatorWindow(yutani.Window):
  43. base_width = 200
  44. base_height = 240
  45. def __init__(self, decorator):
  46. super(CalculatorWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title=app_name, icon="calculator", doublebuffer=True)
  47. self.move(100,100)
  48. self.decorator = decorator
  49. def add_string(button):
  50. self.add_string(button.text)
  51. def clear(button):
  52. self.clear_text()
  53. def calculate(button):
  54. self.calculate()
  55. self.buttons = [
  56. [Button("C",clear), None, Button("(",add_string), Button(")",add_string)],
  57. [Button("7",add_string), Button("8",add_string), Button("9",add_string), Button("/",add_string)],
  58. [Button("4",add_string), Button("5",add_string), Button("6",add_string), Button("*",add_string)],
  59. [Button("1",add_string), Button("2",add_string), Button("3",add_string), Button("-",add_string)],
  60. [Button("0",add_string), Button(".",add_string), Button("=",calculate), Button("+",add_string)],
  61. ]
  62. def exit_app(action):
  63. menus = [x for x in self.menus.values()]
  64. for x in menus:
  65. x.definitely_close()
  66. self.close()
  67. sys.exit(0)
  68. def about_window(action):
  69. AboutAppletWindow(self.decorator,f"About {app_name}","/usr/share/icons/48/calculator.png",_description,"calculator")
  70. def help_browser(action):
  71. subprocess.Popen(["help-browser.py","calculator.trt"])
  72. menus = [
  73. ("File", [
  74. MenuEntryAction("Exit","exit",exit_app,None),
  75. ]),
  76. ("Help", [
  77. MenuEntryAction("Contents","help",help_browser,None),
  78. MenuEntryDivider(),
  79. MenuEntryAction(f"About {app_name}","star",about_window,None),
  80. ]),
  81. ]
  82. self.menubar = MenuBarWidget(self,menus)
  83. self.tr = text_region.TextRegion(self.decorator.left_width(self)+5,self.decorator.top_height(self)+self.menubar.height,self.base_width-10,40)
  84. self.tr.set_font(toaru_fonts.Font(toaru_fonts.FONT_MONOSPACE,18))
  85. self.tr.set_text("")
  86. self.tr.set_alignment(1)
  87. self.tr.set_valignment(2)
  88. self.tr.set_one_line()
  89. self.tr.set_ellipsis()
  90. self.error = False
  91. self.hover_widget = None
  92. self.down_button = None
  93. self.menus = {}
  94. self.hovered_menu = None
  95. def calculate(self):
  96. if self.error or len(self.tr.text) == 0:
  97. self.tr.set_text("0")
  98. self.error = False
  99. try:
  100. self.tr.set_text(str(eval_expr(self.tr.text)))
  101. except Exception as e:
  102. error = str(e)
  103. if "(" in error:
  104. error = error[:error.find("(")-1]
  105. self.tr.set_richtext(f"<i><color 0xFF0000>{e.__class__.__name__}</color>: {error}</i>")
  106. self.error = True
  107. self.draw()
  108. self.flip()
  109. def add_string(self, text):
  110. if self.error:
  111. self.tr.text = ""
  112. self.error = False
  113. self.tr.set_text(self.tr.text + text)
  114. self.draw()
  115. self.flip()
  116. def clear_text(self):
  117. self.error = False
  118. self.tr.set_text("")
  119. self.draw()
  120. self.flip()
  121. def clear_last(self):
  122. if self.error:
  123. self.error = False
  124. self.tr.set_text("")
  125. if len(self.tr.text):
  126. self.tr.set_text(self.tr.text[:-1])
  127. self.draw()
  128. self.flip()
  129. def draw(self):
  130. surface = self.get_cairo_surface()
  131. WIDTH, HEIGHT = self.width - self.decorator.width(self), self.height - self.decorator.height(self)
  132. ctx = cairo.Context(surface)
  133. ctx.translate(self.decorator.left_width(self), self.decorator.top_height(self))
  134. ctx.rectangle(0,0,WIDTH,HEIGHT)
  135. ctx.set_source_rgb(204/255,204/255,204/255)
  136. ctx.fill()
  137. ctx.rectangle(0,5+self.menubar.height,WIDTH,self.tr.height-10)
  138. ctx.set_source_rgb(1,1,1)
  139. ctx.fill()
  140. self.tr.move(self.decorator.left_width(self)+5,self.decorator.top_height(self)+self.menubar.height)
  141. self.tr.resize(WIDTH-10, self.tr.height)
  142. self.tr.draw(self)
  143. offset_x = 0
  144. offset_y = self.tr.height + self.menubar.height
  145. button_height = int((HEIGHT - self.tr.height - self.menubar.height) / len(self.buttons))
  146. for row in self.buttons:
  147. button_width = int(WIDTH / len(row))
  148. for button in row:
  149. if button:
  150. button.draw(self,ctx,offset_x,offset_y,button_width,button_height)
  151. offset_x += button_width
  152. offset_x = 0
  153. offset_y += button_height
  154. self.menubar.draw(ctx,0,0,WIDTH)
  155. self.decorator.render(self)
  156. self.flip()
  157. def finish_resize(self, msg):
  158. """Accept a resize."""
  159. if msg.width < 200 or msg.height < 200:
  160. self.resize_offer(max(msg.width,200),max(msg.height,200))
  161. return
  162. self.resize_accept(msg.width, msg.height)
  163. self.reinit()
  164. self.draw()
  165. self.resize_done()
  166. self.flip()
  167. def mouse_event(self, msg):
  168. decor_event = d.handle_event(msg)
  169. if decor_event == yutani.Decor.EVENT_CLOSE:
  170. window.close()
  171. sys.exit(0)
  172. elif decor_event == yutani.Decor.EVENT_RIGHT:
  173. d.show_menu(self, msg)
  174. x,y = msg.new_x - self.decorator.left_width(self), msg.new_y - self.decorator.top_height(self)
  175. w,h = self.width - self.decorator.width(self), self.height - self.decorator.height(self)
  176. if x >= 0 and x < w and y >= 0 and y < self.menubar.height:
  177. self.menubar.mouse_event(msg, x, y)
  178. return
  179. redraw = False
  180. if self.down_button:
  181. if msg.command == yutani.MouseEvent.RAISE or msg.command == yutani.MouseEvent.CLICK:
  182. if not (msg.buttons & yutani.MouseButton.BUTTON_LEFT):
  183. if x >= self.down_button.x and \
  184. x < self.down_button.x + self.down_button.width and \
  185. y >= self.down_button.y and \
  186. y < self.down_button.y + self.down_button.height:
  187. self.down_button.focus_enter()
  188. self.down_button.callback(self.down_button)
  189. self.down_button = None
  190. redraw = True
  191. else:
  192. self.down_button.focus_leave()
  193. self.down_button = None
  194. redraw = True
  195. else:
  196. if y > self.tr.height + self.menubar.height and y < h and x >= 0 and x < w:
  197. row = int((y - self.tr.height - self.menubar.height) / (self.height - self.decorator.height() - self.tr.height - self.menubar.height) * len(self.buttons))
  198. col = int(x / (self.width - self.decorator.width(self)) * len(self.buttons[row]))
  199. button = self.buttons[row][col]
  200. if button != self.hover_widget:
  201. if button:
  202. button.focus_enter()
  203. redraw = True
  204. if self.hover_widget:
  205. self.hover_widget.focus_leave()
  206. redraw = True
  207. self.hover_widget = button
  208. if msg.command == yutani.MouseEvent.DOWN:
  209. if button:
  210. button.hilight = 2
  211. self.down_button = button
  212. redraw = True
  213. else:
  214. if self.hover_widget:
  215. self.hover_widget.focus_leave()
  216. redraw = True
  217. self.hover_widget = None
  218. if redraw:
  219. self.draw()
  220. def keyboard_event(self, msg):
  221. if msg.event.action != 0x01:
  222. return # Ignore anything that isn't a key down.
  223. if msg.event.key in b"0123456789.+-/*()":
  224. self.add_string(msg.event.key.decode('utf-8'))
  225. if msg.event.key == b"\n":
  226. self.calculate()
  227. if msg.event.key == b"c":
  228. self.clear_text()
  229. if msg.event.keycode == 8:
  230. self.clear_last()
  231. if msg.event.key == b"q":
  232. self.close()
  233. sys.exit(0)
  234. if __name__ == '__main__':
  235. yutani.Yutani()
  236. d = yutani.Decor()
  237. window = CalculatorWindow(d)
  238. window.draw()
  239. yutani_mainloop.mainloop()