
# Yet another calculator in a sea of countless calculators? How absurd!

# h1. Equali Calculator v0.0.0
# Created by Joshua Choi
# Distributed under GNU 

# Yet, someone has gone ahead and made one for Shoes. Equali is an arithmetic calculator that supports complex mathematical expressions (well, at least with parentheses). It will have memory buttons in the future and unlimited undo.

# (Note: Currently uses Ruby's parser to evaluate, with illegal characters filtered out. If I can find out how to prevent uncontrollable things like method calls (or how to write my own expression parser!), Calky may support built-in and simple custom functions in the future.)

# h2. Log of Changes:

# *v0.0.0*: Only tested on Shoes r925 Mac OS X. Supports arbitrary arithmetic expressions with parentheses, but without any letters or symbols other than those in the buttons. Uses Ruby's eval to evaluate expressions. Has an Undo Clear button that holds one expression in its history. Has status text on the bottom that changes as the expression is entered and evaluated.

# h2. License

# Copyright © 2008 Joshua Choi
# <a href="http://creativecommons.org/licenses/GPL/2.0/">
# <img alt="CC-GNU GPL" border="0" src="http://creativecommons.org/images/public/cc-GPL-a.png" /></a><br />
# This software is licensed under the <a href="http://creativecommons.org/licenses/GPL/2.0/">CC-GNU GPL</a> version 2.0 or later.

APP_NAME = 'Equali'
PLUS_SIGN = '+'
MINUS_SIGN = '-'
TIMES_SIGN = '×'
DIVIDE_SIGN = '÷'
RAW_TIMES_SIGN = '*'
RAW_DIVIDE_SIGN = '/'
OPEN_PARENTHESIS = '('
CLOSED_PARENTHESIS = ')'
EQUALS_SIGN = '='
DECIMAL_POINT = '.'

class ::Fixnum

   def / num
      self.to_f / num.to_f
   end
	 
end

class Calculator
	ILLEGAL_RULE = / [A-Za-z\.] /x
	
  def eval expression
		expression = expression.to_s
		expression.sub! TIMES_SIGN, '*'
		expression.sub! DIVIDE_SIGN, '/'
		if ILLEGAL_RULE.match expression
			return nil
		end
		begin
			Kernel::eval(expression)
		rescue RuntimeError, SyntaxError
			nil
		end
	end
	
end

class Action
	attr_accessor :sign, :procedure
	
	def initialize sign, &procedure
		@sign = sign
		@procedure = procedure
	end
	
	def to_proc
		@procedure
	end
	
	def call
		@procedure.call
	end
	
	def to_s
		@sign
	end
	
end

Shoes.app :title => APP_NAME, :width => 250, :height => 320,
		:resizable => false do

	# Instance variables:
	# @answered: Has the user clicked the '=' button and got an answer?
	# @undo: The string of the inputted expression last cleared.
	# @display: The EditBox that displays the currently inputted 
	#   expression and answers too.
	# @status_bar: A text box that contains directions for the user.
	
	# The following Procs are for action buttons.
	
	CLEAR = proc {
		@answered = false
		@undo = @display.text
		@display.text = nil
	}
	SHOW_ANSWER = proc {
		if blank_display?
			nil
		elsif @output.nil?
			alert 'I can\'t figure out the expression. Did you leave an ' +
				'unclosed parenthesis? Or maybe there\'s an unallowed ' +
				'symbol. In any case, edit the expression until it\'s ' +
				'valid again.'
		else
			@display.text = @output
			@answered = true
			display_validation
		end
	}
	UNDO = proc {
		if @display.text or @display.text.empty?
			@display.text = @undo
		end
	}
	DELETE = proc {
		@display.text = @display.text.chop
	}
	
	# The following strings are possible information that @status_bar may
	# display.
	
	DEFAULT_STATUS = 'Use the buttons or keyboard to type in ' +
		'numbers.'
	VALID_STATUS = 'Continue entering the expression. ',
		'When you\'re done, click on the equals sign to display the value.'
	ERROR_STATUS = 'The current expression is invalid. ',
		'Close parentheses and delete invalid characters.'
	ANSWERED_STATUS = 'The expression has been solved, and its result ' +
		'is above. You can copy the result to use in another ' +
		'application, or use the result in a new expression right now.'
	
	# The following Colors are for use through the app for a color scheme.
	
	BACKGROUND_COLOR = black
	OK_COLOR = rgb 100, 255, 100
	NOT_OK_COLOR = rgb 255, 255, 100
	
	# The following methods are for performing common actions in the app.
	
	def face_button sign, options={}
		if sign.kind_of? Action
			action_button sign, options
		else
			input_button sign, options
		end
	end
	
  def input_button input, options={}
    normal_button input, options do
			@answered = false
      @display.text += input.to_s
    end
  end
	
	def action_button action, options={}
		normal_button action, options, &action
	end
	
	def normal_button label, options={}, &action
		options = {:width => 60}.merge options
		button label, options do
			action.call
			evaluate
		end
	end
	
  def button_row *signs
    flow do
      signs.each do |sign|
				face_button sign
      end
    end
  end
	
	def action sign, &procedure
		Action.new sign, &procedure
	end
	
	def evaluate
		@output = @calculator.eval @display.text
		display_validation
	end
	
	def display_validation
		@status_bar.text = if blank_display?
				DEFAULT_STATUS
			elsif @output.nil?
				ERROR_STATUS
			elsif @answered
				ANSWERED_STATUS
			else
				VALID_STATUS
			end
	end
	
	def blank_display?
		text = @display.text
		text.nil? or text.empty?
	end
	
	# Here is the actual application.
	
  @calculator = Calculator.new
	background BACKGROUND_COLOR
  stack :margin => 5 do
		@display = edit_line :margin => 5, :width => 0.95 do |line|
			evaluate
 		end
		flow do
			action_button action('Clear', &CLEAR), :width => nil
			action_button action('Undo Clear', &UNDO), :width => nil
		end
		button_row action('⌫', &DELETE), OPEN_PARENTHESIS,
			CLOSED_PARENTHESIS, DIVIDE_SIGN
		button_row 7, 8, 9, TIMES_SIGN
		button_row 4, 5, 6, MINUS_SIGN
		button_row 1, 2, 3, PLUS_SIGN
		button_row 0, DECIMAL_POINT,
			action("   #{EQUALS_SIGN}  ", &SHOW_ANSWER)
  end
	flow do
		# @status_background = background GREEN, :radius => 3
		@status_bar = inscription :size => 8, :stroke => white
	end
	evaluate
end
