diff --git a/.flake8 b/.flake8
index f2b41384..b1663a2f 100644
--- a/.flake8
+++ b/.flake8
@@ -1,3 +1,4 @@
+[flake8]
max-line-length=100
application_import_names=projectt
ignore=P102,B311,W503,E226,S311,W504,F821
diff --git a/.gitignore b/.gitignore
index 894a44cc..a93df931 100644
--- a/.gitignore
+++ b/.gitignore
@@ -102,3 +102,6 @@ venv.bak/
# mypy
.mypy_cache/
+
+# Ignores for Pycharm Interpreter
+.idea/
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..94a25f7f
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Design document/Design document.pdf b/Design document/Design document.pdf
new file mode 100644
index 00000000..82feebbc
Binary files /dev/null and b/Design document/Design document.pdf differ
diff --git a/Pipfile b/Pipfile
index 72b70b6f..576b9a17 100644
--- a/Pipfile
+++ b/Pipfile
@@ -12,4 +12,5 @@ flake8 = "*"
python_version = "3.7"
[scripts]
-lint = "python -m flake8"
\ No newline at end of file
+start = "python -m project"
+lint = "python -m flake8"
diff --git a/Pipfile.lock b/Pipfile.lock
index 79354a3c..533ae4e9 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -26,11 +26,11 @@
},
"flake8": {
"hashes": [
- "sha256:6d8c66a65635d46d54de59b027a1dda40abbe2275b3164b634835ac9c13fd048",
- "sha256:6eab21c6e34df2c05416faa40d0c59963008fff29b6f0ccfe8fa28152ab3e383"
+ "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661",
+ "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8"
],
"index": "pypi",
- "version": "==3.7.6"
+ "version": "==3.7.7"
},
"mccabe": {
"hashes": [
@@ -48,10 +48,10 @@
},
"pyflakes": {
"hashes": [
- "sha256:5e8c00e30c464c99e0b501dc160b13a14af7f27d4dffb529c556e30a159e231d",
- "sha256:f277f9ca3e55de669fba45b7393a1449009cff5a37d1af10ebb76c52765269cd"
+ "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
+ "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
],
- "version": "==2.1.0"
+ "version": "==2.1.1"
}
}
}
diff --git a/README.md b/README.md
index 697c2bf7..ffffa49d 100644
--- a/README.md
+++ b/README.md
@@ -30,16 +30,26 @@ You should be using [Pipenv](https://pipenv.readthedocs.io/en/latest/). Take a l
# Project Information
-`# TODO`
+Team: Durable Drills
## Description
-`# TODO`
+Keep track of your contact information using the Durable Drills Contact Manager™.
## Setup & Installation
-`# TODO`
+Assuming you are using pipenv, "pipenv run start" should make the program work!
## How do I use this thing?
-`# TODO`
+Using the Contact Manager is simple:
+ - To load in your current contacts, press 'Load Contacts' in the 'View Contacts' tab
+ - To save your current list of contacts, press 'Save Contacts'
+ - To view the information of a specific contact, select that contact and press 'Show Info', then spin the Wheel™ to determine what info you can see
+ - To delete a specific contact, select that contact then press 'Delete'
+
+ - To create a new contact, go to the 'New Contact' tab and enter your contacts information:
+ - For text entries, like the contact Name, Address, Email, and Notes, answer the questions provided by the EntryBot3000™
+ - For numerical entries, like the Phone Number, use the provided rotary phone, then select the type of phone number: Personal, Work, or Home.
+ - For all entries, you need to press the accompanying 'Add' button to add that information to the preview window; when the preview looks the way you want it, press 'Submit to Contacts'.
+ - To clear all entries, press 'Clear'
diff --git a/project/AlphabetGuesser/AlphabetGuesserInter.py b/project/AlphabetGuesser/AlphabetGuesserInter.py
new file mode 100644
index 00000000..7030a9b2
--- /dev/null
+++ b/project/AlphabetGuesser/AlphabetGuesserInter.py
@@ -0,0 +1,156 @@
+import tkinter as tk
+from tkinter import N, S, E, W
+from random import randint
+from project.AlphabetGuesser.letter_guesser import LetterGuesser
+
+
+class AlphabetGuesserInter(tk.Frame):
+
+ def __init__(self, master, current_entry, *args, **kwargs):
+ super().__init__(master, *args, **kwargs)
+ self.master = master
+ self.letter_guesser = LetterGuesser()
+
+ self.current_entry = current_entry
+ self.current_entry_label = None
+ self.letters_input = None
+ self.header = None
+ self.description = None
+ self.title_label = None
+ self.current_letter_id = 1
+ self.question_label = None
+ self.current_word = None
+ self.button_1 = None
+ self.button_2 = None
+ self.submit_button = None
+
+ self.__letter_found = False
+
+ self.create()
+
+ def create(self):
+ self.header = tk.Label(self, text="EntryBot 3000", font="Calibri 20")
+ self.description = tk.Label(self, text="The latest in typing technology since 700 B.C\n\n",
+ font="Calibri 10")
+
+ self.title_label = tk.Label(self,
+ text="You are entering {} {}:\nAnswer the questions below to"
+ "fill out the entry\n"
+ .format(self.get_prefix(self.current_entry),
+ self.current_entry),
+ font="Calibri 11")
+ self.question_label = tk.Label(self,
+ text="Is your {} character contained in the phrase :"
+ "".format(self.complete_number(self.current_letter_id)),
+ font="Calibri 11")
+
+ self.current_word = tk.Label(self, font="Calibri 18", relief="sunken")
+ self.update_current_asked_word()
+
+ self.button_1 = tk.Button(self, command=lambda: self.send_answer(self.button_1['text']))
+ self.button_2 = tk.Button(self, command=lambda: self.send_answer(self.button_2['text']))
+ self.button_1.config(font="Calibri 15")
+ self.button_2.config(font="Calibri 15")
+ self.randomize_buttons()
+
+ self.current_entry_label = tk.Label(self, text=self.current_entry + " :", font="Calibri 12")
+ self.letters_input = tk.Label(self, relief="sunken", font="Calibri 15")
+
+ self.submit_button = tk.Button(self, text="Submit", font="Calibri 15",
+ command=lambda: self.submit())
+
+ self.header.grid(row=0, column=0, columnspan=2, sticky=N + S + E + W)
+ self.description.grid(row=1, column=0, columnspan=2, sticky=N + S + E + W)
+
+ self.title_label.grid(column=0, row=2, columnspan=2, sticky=N + S + E + W)
+ self.question_label.grid(column=0, row=3, columnspan=2, sticky=N + S + E + W)
+ self.current_word.grid(column=0, row=4, columnspan=2, padx=10, pady=10,
+ sticky=N + S + E + W)
+
+ self.button_1.grid(column=0, row=5, sticky=N + S + E + W)
+ self.button_2.grid(column=1, row=5, sticky=N + S + E + W)
+
+ self.current_entry_label.grid(column=0, row=6, columnspan=2, sticky=N + S + E + W)
+ self.letters_input.grid(column=0, row=7, columnspan=2, padx=10, sticky=N + S + E + W)
+ self.submit_button.grid(column=0, row=8, columnspan=2, padx=10, pady=10,
+ sticky=N + S + E + W)
+
+ self.grid(row=0, column=0)
+
+ def update_current_asked_word(self):
+ self.current_word['text'] = '"' + self.letter_guesser.request_word() + '"'
+
+ def send_answer(self, answer):
+ if self.__letter_found and answer == "Yes":
+ self.current_letter_id += 1
+ if self.question_label['text'] == "I found your character! Continue?":
+ self.question_label['text'] = "Is your {} character contained in the phrase :"\
+ "".format(self.complete_number(self.current_letter_id))
+ self.restart_letter_guesser()
+ return
+ elif self.__letter_found and answer == "No":
+ # Return the current output and go back to the main window
+ self.submit()
+ return
+ self.letter_guesser.answer(self.current_word['text'], answer)
+ self.randomize_buttons()
+
+ print(self.letter_guesser.possible_characters)
+ if len(self.letter_guesser.possible_characters) == 1:
+ self.letter_found()
+ else:
+ self.update_current_asked_word()
+
+ def letter_found(self):
+ self.question_label['text'] = "I found your character! Continue?"
+ self.current_word['text'] = self.letter_guesser.possible_characters[0].upper()
+ if len(self.letters_input['text']) == 0 or self.letters_input['text'][-1] == " ":
+ self.letters_input['text'] += self.letter_guesser.possible_characters[0].upper()
+ else:
+ self.letters_input['text'] += self.letter_guesser.possible_characters[0]
+ self.__letter_found = True
+
+ def randomize_buttons(self):
+ if randint(0, 1) == 1:
+ self.button_1['text'] = 'Yes'
+ self.button_2['text'] = 'No'
+ else:
+ self.button_1['text'] = 'No'
+ self.button_2['text'] = 'Yes'
+
+ def get_answer(self):
+ return self.letters_input['text']
+
+ def submit(self):
+ self.master.event_generate("<>")
+ print("Submitted")
+
+ @staticmethod
+ def get_prefix(word):
+ if word[0] in ['A', 'E', 'I', 'O', 'U']:
+ return 'an'
+ return 'a'
+
+ @staticmethod
+ def complete_number(number):
+ if number == 1:
+ return '1st'
+ elif number == 2:
+ return '2nd'
+ elif number == 3:
+ return '3rd'
+ else:
+ return str(number) + 'th'
+
+ def restart_letter_guesser(self):
+ self.letter_guesser.__init__()
+ self.update_current_asked_word()
+ self.__letter_found = False
+
+
+if __name__ == '__main__':
+ root = tk.Tk()
+ root.geometry("300x400")
+ root.resizable(False, False)
+ a = AlphabetGuesserInter(root, "Name", width=300, height=500)
+ root.mainloop()
diff --git a/project/AlphabetGuesser/create_dictionary.py b/project/AlphabetGuesser/create_dictionary.py
new file mode 100644
index 00000000..b849bd24
--- /dev/null
+++ b/project/AlphabetGuesser/create_dictionary.py
@@ -0,0 +1,30 @@
+import pickle
+
+
+def create_dictionary() -> None:
+ """
+ Reads each word from a file with the format of words.txt & determines what letters comprise each
+ word; saved into a dictionary in the following format:
+ {'apple': [a, e, l, p], 'banana': [a, b, n], ...etc}
+ Finally, the created dictionary is saved as a pickle
+ :return: None
+ """
+ dictionary = {}
+ alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
+ 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7',
+ '8', '9', '@', '.', '-', ' ', '?', '!']
+ with open('project/Alphabetguesser/words.txt') as f:
+ for word in f:
+ word = word[:len(word) - 1]
+ letters = []
+ for letter in alphabet:
+ if letter in word:
+ letters.append(letter)
+ dictionary[word] = letters
+ f.close()
+ with open("project/AlphabetGuesser/words_pickle", 'wb') as outfile:
+ pickle.dump(dictionary, outfile)
+
+
+if __name__ == '__main__':
+ create_dictionary()
diff --git a/project/AlphabetGuesser/letter_guesser.py b/project/AlphabetGuesser/letter_guesser.py
new file mode 100644
index 00000000..36dd7392
--- /dev/null
+++ b/project/AlphabetGuesser/letter_guesser.py
@@ -0,0 +1,115 @@
+import pickle
+import random
+
+
+class LetterGuesser:
+ """
+ LetterGuesser:
+
+ === Public Attributes ===
+ dictionary
+ possible_characters
+
+ === Methods === request_word: returns a random word from the dictionary
+ remove_possible_letters: removes all characters from possible_characters that are not
+ contained in the given word/phrase remove_possible_words: removes all words/phrases from
+ dictionary that contain none of the characters in the given word/phrase
+ contains_none_letters: Returns True only if none the letters in word_2 are contained in word_1
+ answer: Handles the user's answer of either "Yes" or "No"
+ """
+ def __init__(self):
+ self.dictionary = None
+ self.possible_characters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '@', '.', '-',
+ ' ', '?', '!']
+ with open("project/alphabetguesser/words_pickle", "rb") as openfile:
+ self.dictionary = pickle.load(openfile)
+ openfile.close()
+
+ def answer(self, requested_word, answer):
+ """
+ Handles the user's answer for a given word
+ :param requested_word: The context of the user's answer
+ :param answer: string that should be either "Yes" or "No"; the program will not respond to
+ anything else
+ :return: None
+ """
+ self.remove_possible_letters(requested_word, answer)
+ self.remove_possible_words()
+
+ def request_word(self) -> str:
+ words = list(self.dictionary.keys())
+ random.shuffle(words)
+ for word in words:
+ if self.find_ratio_of_remaining_letter_in_word(word) > 30:
+ return word
+ return random.choice(words)
+
+ def remove_possible_letters(self, word, answer) -> None:
+ characters_to_delete = []
+ for character in self.possible_characters:
+ if character not in word and answer == "Yes":
+ characters_to_delete.append(character)
+ elif character in word and answer == "No":
+ characters_to_delete.append(character)
+ for character in characters_to_delete:
+ self.possible_characters.remove(character)
+
+ def remove_possible_words(self) -> None:
+ words_to_delete = []
+ for elem in self.dictionary:
+ if self.contains_none_letters(elem, self.possible_characters) or \
+ self.contains_all_letters(self.possible_characters, elem):
+ words_to_delete.append(elem)
+ # if a word contains none of the possible letters, remove it
+ for elem in words_to_delete:
+ del self.dictionary[elem]
+
+ def find_ratio_of_remaining_letter_in_word(self, word):
+ i = 0
+ for letter in self.possible_characters:
+ if letter in word:
+ i += 1
+
+ return i/len(word)*100
+
+ @staticmethod
+ def contains_none_letters(word_1, word_2) -> bool:
+ for letter in word_1:
+ if letter in word_2:
+ return False
+ return True
+
+ @staticmethod
+ def contains_all_letters(word_1, word_2) -> bool:
+ for letter in word_1:
+ if letter not in word_2:
+ return False
+ return True
+
+
+if __name__ == '__main__':
+ """
+ This is a simple demo showing how the letter guesser should be implemented
+ """
+
+ guesser = LetterGuesser()
+ question_num = 1
+ print('Pick a character, any character; keep it in mind as you answer these questions!\n')
+ while True:
+ word = guesser.request_word()
+ yes_no = input('Question #{0}:\nIs your character in the word/phrase:\n{1}\n(Yes/No):'
+ .format(question_num, word))
+ if yes_no == "Quit":
+ break
+ question_num += 1
+ guesser.answer(word, yes_no)
+ print('Possible characters:', guesser.possible_characters)
+ print('Words left to pick from:', len(list(guesser.dictionary)))
+ if len(guesser.possible_characters) == 1:
+ print('The character you want is \'{0}\''.format(guesser.possible_characters[0]))
+ break
+ if len(guesser.possible_characters) < 1:
+ print('A mistake has been made; try again')
+ break
diff --git a/project/AlphabetGuesser/words.txt b/project/AlphabetGuesser/words.txt
new file mode 100644
index 00000000..aaa4350d
--- /dev/null
+++ b/project/AlphabetGuesser/words.txt
@@ -0,0 +1,423 @@
+abject
+aberration
+abjure
+aggrandize
+alacrity
+alias
+ambivalent
+amenable
+amorphous
+anachronistic
+anathema
+annex
+antediluvian
+antiseptic
+apathetic
+antithesis
+apocryphal
+approbation
+arbitrary
+arboreal
+arcane
+archetypal
+arrogate
+ascetic
+aspersion
+assiduous
+atrophy
+bane
+bashful
+beguile
+bereft
+blandishment
+bilk
+bombastic
+cajole
+callous
+calumny
+camaraderie
+candor
+capitulate
+carouse
+carp
+caucus
+cavort
+circumlocution
+circumscribe
+circumvent
+clamor
+cleave
+cobbler
+cogent
+cognizant
+commensurate
+complement
+compunction
+concomitant
+conduit
+conflagration
+congruity
+connive
+consign
+constituent
+corpulence
+covet
+cupidity
+dearth
+debacle
+debauch
+debunk
+defunct
+demagogue
+denigrate
+derivative
+despot
+diaphanous
+didactic
+dirge
+disaffected
+discomfit
+disparate
+dispel
+disrepute
+divisive
+dogmatic
+dour
+duplicity
+duress
+eclectic
+edict
+ebullient
+egregious
+empirical
+emulate
+enervate
+enfranchise
+engender
+ephemeral
+epistolary
+equanimity
+equivocal
+espouse
+evanescent
+evince
+exacerbate
+exhort
+execrable
+exigent
+expedient
+expiate
+expunge
+extraneous
+extol
+extant
+expurgate
+fallacious
+fatuous
+fetter
+flagrant
+foil
+forbearance
+fortuitous
+fractious
+garrulous
+iconoclast
+idiosyncratic
+injunction
+inoculate
+insidious
+instigate
+insurgent
+interlocutor
+intimation
+inure
+invective
+intransigent
+inveterate
+irreverence
+knell
+laconic
+largesse
+legerdemain
+libertarian
+licentious
+linchpin
+litigant
+maelstrom
+maudlin
+maverick
+mawkish
+maxim
+mendacious
+modicum
+morass
+mores
+munificent
+multifarious
+nadir
+negligent
+neophyte
+noisome
+noxious
+obdurate
+obfuscate
+obstreperous
+officious
+onerous
+ostensible
+ostracism
+palliate
+panacea
+paradigm
+pariah
+partisan
+paucity
+pejorative
+pellucid
+penchant
+penurious
+pert
+pernicious
+pertinacious
+probity
+proclivity
+profligate
+promulgate
+proscribe
+quaint
+quixotic
+quandary
+relegate
+remiss
+reprieve
+reprobate
+rescind
+requisition
+rife
+sanctimonious
+sanguine
+scurrilous
+tome
+toady
+travesty
+trenchant
+trite
+truculent
+turpitude
+ubiquitous
+umbrage
+wanton
+winsome
+yoke
+zephyr
+wily
+tirade
+python
+disaffected@hotmail.fr
+nadi@yahoo.com
+embezzlement@hotmail.com
+caucus@gmail.com
+duress@gmail.com
+cove@outlook.com
+derivative@hotmail.fr
+corpulence@hotmail.com
+pellucio@outlook.com
+blandishment@yahoo.com
+adumbrate@outlook.com
+aspersion@yahoo.com
+concomitan@outlook.com
+elege@hotmail.com
+inchoate@yahoo.com
+proclivity@hotmail.fr
+extraneous@gmail.com
+carous@hotmail.fr
+compunction@hotmail.com
+vociferous@hotmail.fr
+equanimity@outlook.com
+travesty@gmail.com
+bashful@outlook.com
+concomitan@gmail.com
+commensurate@hotmail.fr
+surreptitious@outlook.com
+libertarian@hotmail.com
+vituperate@hotmail.fr
+trite@hotmail.com
+proscribe@yahoo.com
+panaces@hotmail.fr
+hegemone@gmail.com
+duplicite@outlook.com
+abject@outlook.com
+concomitant@hotmail.com
+derivative.co.uk
+extant.gov
+amorphous.edu
+dearth.gov
+libertarian.ca
+insurgent.io
+rife.io
+ostensible.edu
+accretion.com
+inveterate.edu
+penchant.org
+defunct.io
+inimical.es
+preclude.edu
+redoubtable.co.uk
+irreverence.es
+ambivalent.es
+disaffected.me
+inveterate.net
+debunk.com
+elegy.com
+defunct.ca
+duress.es
+inexorable.org
+quaint.edu
+inexorable.me
+beguile.co.uk
+negligent.es
+dour.com
+abstruse.gov
+emollient.net
+archetypal.ca
+impute.edu
+stolid.net
+cajole.gg
+extra-vestige
+auto-epistolary
+hypo-apocryphal
+post-camaraderie
+hypo-circumlocution
+hypo-winsome
+auto-pithy
+hypo-amorphous
+auto-arboreal
+trans-winsome
+non-foil
+auto-vicissitude
+semi-contravene
+hypo-atrophy
+un-rife
+anti-pernicious
+anti-bashful
+hypo-maverick
+non-accretion
+extra-obstreperous
+hypo-prurient
+anti-noxious
+non-empirical
+mid-contusion
+dis-veracity
+de-hegemony
+anti-impute
+auto-dispel
+mis-abjure
+anti-toady
+non-preponderance
+mid-insurgent
+extra-scurrilous
+pre-torpid
+anti-negligent
+execrable!
+sobriety!
+atrophy?
+abjure?
+callous?
+edict!
+anathema?
+connive!
+contentious?
+caucus!
+reprieve?
+edict?
+heterogenous!
+approbation?
+expunge?
+virtuoso!
+covet!
+disaffected?
+disparate!
+utilitarian!
+partisan!
+archetypal?
+heterogenous?
+pejorative!
+contentious?
+admonish?
+duress?
+emollient!
+subjugate?
+pariah!
+paradigm!
+probity?
+penchant!
+covet!
+antediluvian?
+disparate51
+vilify36
+advocate10
+penurious50
+scurrilous66
+corpulence11
+ostracism49
+cobbler11
+expedient31
+surreptitious50
+l@rgesse14
+commensurate96
+insurgent95
+plenitude63
+connive70
+utilitarian14
+idiosyncratic89
+incumbent26
+pithy99
+arbitrary17
+cleave30
+n@dir78
+demagogue22
+atrophy97
+ebullient70
+vilify91
+litigant90
+antiseptic69
+platitude22
+gourmand63
+dogmatic83
+noisome46
+obdurate12
+arboreal49
+recalcitrant21
+ascetic exacerbate
+licentious maelstrom
+annex reprieve
+congruity arrogate
+dirge forbearance
+tirade tome
+antiseptic expurgate
+apocryphal palliate
+bombastic evanescent
+duplicity yoke
+disaffected maelstrom
+forbearance ascetic
+arcane quaint
+diaphanous yoke
+circumlocution probity
+congruity fortuitous
+obstreperous probity
+zephyr duress
+morass exigent
+connive dearth
+camaraderie aspersion
+nadir probity
+pellucid antediluvian
+truculent apathetic
+exhort cobbler
+amorphous quaint
+conduit engender
+proclivity insidious
+fallacious munificent
+amenable injunction
+4 8 15 16 23 42
+3.1415
+2.71828
diff --git a/project/MouseController.py b/project/MouseController.py
new file mode 100644
index 00000000..5fb8f010
--- /dev/null
+++ b/project/MouseController.py
@@ -0,0 +1,8 @@
+class MouseController:
+ def __init__(self, root):
+ self.root = root
+
+ def get_absolute_position(self):
+ x = self.root.winfo_pointerx() - self.root.winfo_rootx()
+ y = self.root.winfo_pointery() - self.root.winfo_rooty()
+ return x, y
diff --git a/project/PhoneNumber/AddPhoneNumberInter.py b/project/PhoneNumber/AddPhoneNumberInter.py
new file mode 100644
index 00000000..ce2dda41
--- /dev/null
+++ b/project/PhoneNumber/AddPhoneNumberInter.py
@@ -0,0 +1,120 @@
+import tkinter as tk
+from project.PhoneNumber.PhoneCanvas import PhoneCanvas
+
+
+class AddPhoneNumberInter(tk.Frame):
+ """
+ This class is the frame that contains PhoneCanvas with the label that shows the phone number as
+ the user dials them. It gives the option of erasing the whole phone number.
+
+ === Public Attributes ===
+ master: root of the frame
+ text_label: Label with the text saying what the user should do
+ label_frame: Frame containing the phone number label and button
+ phone_number_label: Label in which the dialed number will be written
+ button: Button to clear the phone_number label.
+
+ === Methods ===
+ add_phone_number_to_entry: This method adds one number to the self.phone_number_label text
+ """
+
+ def __init__(self, master, *args, **kwargs, ):
+ super().__init__(master, *args, **kwargs)
+ self.master = master
+ self.page_name = 'Add Phone Number'
+ self.text_label = tk.Label(self, font=('Calibri', 10), fg='#63FF20', bg='#00536a',
+ borderwidth=2,
+ text="Please enter the phone number you wish\n to add with the"
+ "rotary phone below.")
+
+ self.label_frame = tk.Frame(self, bg='#00536a')
+ self.phone_number_label = PhoneNumberLabel(self.label_frame, font=('Calibri', 20),
+ fg='#63FF20', bg='#00536a', width=12,
+ borderwidth=2, relief='sunken')
+
+ self.button = tk.Button(self.label_frame, text='Clear', bg='#00536a', font=('Calibri', 14),
+ height=1, command=self.phone_number_label.clear_phone_number)
+
+ self.text_label.grid(row=0, column=0)
+ self.phone_number_label.grid(row=0, column=0)
+ self.button.grid(row=0, column=1)
+ self.label_frame.grid(row=1, column=0)
+
+ self.phone_canvas = PhoneCanvas(self)
+ self.phone_canvas.grid(row=2, column=0)
+ self.grid(row=0, column=0)
+
+ self.master.bind("<>", self.__get_dialed_number)
+
+ def add_phone_number_to_entry(self, num: str) -> None:
+ """
+ This method adds one number to the phone number label.
+ :param num: Number we which to add to the phone number label.
+ :return: None
+ """
+ self.phone_number_label.add_phone_number(num)
+
+ def get_complete_phone_number(self):
+ if len(self.phone_number_label['text']) == 12:
+ return self.phone_number_label['text']
+ else:
+ return ""
+
+ def __get_dialed_number(self, event) -> None:
+ """
+ This method gets the phone number from the PhoneCanvas, it is called by the
+ <> event.
+ :param event:
+ :return: None
+ """
+ number = self.phone_canvas.send_output_number()
+ if number is not None:
+ self.add_phone_number_to_entry(number)
+ if len(self.phone_number_label['text']) == 12:
+ self.master.event_generate("<>")
+
+
+class PhoneNumberLabel(tk.Label):
+ """
+ PhoneNumberLabel is a simple class inherited from tk.Label. It allows the user to add one number
+ at the time and will keep the ###-###-#### formatting.
+
+ === Public Attributes ===
+
+ === Methods ===
+ add_phone_number: This method adds one number to the displayed text. It automatically adds '-'
+ to keep the ###-###-#### formatting.
+ clear_phone_number: This method erases all the number that were displayed on the label.
+ """
+
+ def __init__(self, master, *args, **kwargs):
+ super().__init__(master, *args, **kwargs)
+
+ def add_phone_number(self, num: int) -> None:
+ """
+ This method adds a number to the label, it automatically adds '-' at the right location in
+ order to keep the ###-###-#### formatting.
+ :param num: Number we wish to add to the label.
+ :return: None
+ """
+ current_text = self['text']
+ current_length = len(current_text)
+ if current_length == 3 or current_length == 7:
+ self['text'] = self['text'] + '-'
+ self['text'] = self['text'] + num
+
+ def clear_phone_number(self) -> None:
+ """
+ This method clears the phone number displayed in the label.
+ :return: None
+ """
+ self['text'] = ''
+
+
+if __name__ == '__main__':
+ root = tk.Tk()
+ root.geometry("300x400")
+ root.configure(background='#00536a')
+ root.resizable(False, False)
+ AddPhoneNumberInter(root, bg='#00536a')
+ root.mainloop()
diff --git a/project/PhoneNumber/PhoneButton.py b/project/PhoneNumber/PhoneButton.py
new file mode 100644
index 00000000..c87016f8
--- /dev/null
+++ b/project/PhoneNumber/PhoneButton.py
@@ -0,0 +1,184 @@
+import math
+import time
+
+
+class PhoneButton:
+ """
+ Class for phone button
+
+ === Public Attributes ===
+ parent_canvas: Canvas on which the button will be drawn.
+ position_x: current x position of the button
+ position_y: Current y position of the button
+ rotation_point_x: x position of the center of rotation of the button
+ rotation_point_y: y position of the center of rotation of the button
+ radius: radius of the button
+ text: text that is written in the button
+ is_dragging: bool variable that stores if the button is currently being dragged
+ item: contains the circle item for the canvas to use
+ initial_angle: angle from the center of rotation of the initial position
+
+ === Methods ===
+ draw: Draws the circle on its canvas according to its public attributes
+ find_current_angle: calculate the current angle from the center of the rotation point
+ (in rad)
+ is_position_inside_circle: Allows the user to verify if a position (x,y) is inside the
+ circle of the PhoneButton
+ rotate: Allows the user to rotate the button (from its center of rotation) by a given angle
+ (rad)
+ on_click_down: This method is called by the parent canvas when a click is inside the button.
+ It starts the private drag methods which allows to rotate the phone.
+ on_click_release: This method is called by the parent canvas when the click is released.
+ It starts the rotating animation and sends the input to the parent canvas.
+ """
+
+ def __init__(self, parent_canvas, position_x: int, position_y: int,
+ rotation_point_x: int, rotation_point_y: int, radius: int, text: str):
+ self.parent_canvas = parent_canvas
+ self.rotating_speed = 1.5
+ self.position_x = position_x
+ self.position_y = position_y
+ self.rotation_point_x = rotation_point_x
+ self.rotation_point_y = rotation_point_y
+ self.radius = radius
+ self.text = text
+ self.is_dragging = False
+ self.item = None
+ self.initial_angle = self.find_current_angle()
+ self.current_angle = self.initial_angle
+ self.__max_angle = None
+ self.animation_timer = None
+ self.draw()
+
+ def draw(self) -> None:
+ """
+ This method draws the circle button at the location contained in it's attributes.
+ :return: None
+ """
+ self.item = self.parent_canvas.create_circle(self.position_x, self.position_y, self.radius,
+ outline='#00536a', width='3', fill='white', )
+
+ def find_current_angle(self) -> float:
+ """
+ :return: Returns the current angle (rad) positioning the center of the PhoneButton from the
+ rotation point.
+ """
+ return self.parent_canvas.find_angle_from_center(self.position_x, self.position_y)
+
+ def is_position_inside_circle(self, x: int, y: int) -> bool:
+ """
+ This method verifies if the position (x,y) is inside the circle of the PhoneButton
+ :param x: absolute X position that is getting verified
+ :param y: absolute Y position that is getting verified
+ :return: returns True if the position that is getting verified is inside the circle of the
+ PhoneButton, returns False if not.
+ """
+ if math.sqrt(math.pow(self.position_x - x, 2) + math.pow(self.position_y - y, 2)) <= \
+ self.radius:
+ return True
+ else:
+ return False
+
+ def rotate(self, angle: float) -> None:
+ """
+ This method allows the user to rotate the button by the angle of its choice around the
+ rotation point (typically the rotation point is the center of the phone)
+ :param angle: angle to rotate the button (rad)
+ :return: None
+ """
+ # Calculate the new x,y position of the PhoneButton
+ pos_radius = self.parent_canvas.phone_button_pos_radius
+ self.current_angle = self.current_angle + angle
+ self.position_x = self.parent_canvas.canvas_size / 2 - pos_radius * \
+ math.cos(self.current_angle)
+ self.position_y = self.parent_canvas.canvas_size / 2 - pos_radius * \
+ math.sin(self.current_angle)
+
+ # Move the button
+ self.parent_canvas.coords(self.item, self.position_x - self.radius,
+ self.position_y - self.radius,
+ self.position_x + self.radius, self.position_y + self.radius)
+
+ def on_click_down(self) -> None:
+ """
+ This method is called by the parent canvas when the user clicks inside the button. It switch
+ the is_dragging bool, reinitialize the max angle, and start the __drag() method.
+ :return: None
+ """
+ self.is_dragging = True
+ self.__max_angle = self.initial_angle
+ self.__drag()
+
+ def on_click_release(self) -> None:
+ """
+ This method is called by the parent canvas when the user releases the button. It switch the
+ is_dragging bool, and starts the rotating animation.
+ :return: None
+ """
+ self.is_dragging = False
+ self.__animate_rotating_buttons()
+
+ def __animate_rotating_buttons(self) -> None:
+ """
+ This function rotates the buttons until they get back to their original position. It calls
+ the __get_elapsed_time method to make sure the rotating speed is constant. It generate
+ the <> event when the animation is done.
+ :return:
+ """
+ # Making the rotation speed dependant on the time between the frame to make it more fluid.
+ elapsed_time = self.__get_elapsed_time()
+ self.animation_timer = time.time()
+ rotating_speed = self.rotating_speed * elapsed_time
+
+ total_angle_to_rotate = self.current_angle - self.initial_angle
+ if total_angle_to_rotate <= rotating_speed:
+ self.parent_canvas.rotate_all_circles(-total_angle_to_rotate)
+ self.animation_timer = None
+ self.parent_canvas.master.event_generate("<>", when="tail")
+ else:
+ self.parent_canvas.rotate_all_circles(-rotating_speed)
+ self.parent_canvas.after(33, self.__animate_rotating_buttons)
+
+ def __get_elapsed_time(self):
+ """
+ This function returns the elapsed time since the last time we updated the rotating animation
+ If it's the first frame of the animation, we assume the elapsed_time is 0.033s which
+ correspond to 30 fps.
+ :return:
+ """
+ if self.animation_timer is None:
+ elapsed_time = 0.0333
+ else:
+ elapsed_time = time.time() - self.animation_timer
+ return elapsed_time
+
+ def __drag(self) -> None:
+ if self.is_dragging:
+ angle_to_rotate = self.__find_angle_to_rotate_from_mouse_pos()
+ self.parent_canvas.rotate_all_circles(angle_to_rotate)
+ self.__update_highest_angle()
+ self.parent_canvas.after(50, self.__drag)
+
+ def __find_angle_to_rotate_from_mouse_pos(self) -> float:
+ angle_of_stopper = 5.1
+ mouse_pos_x, mouse_pos_y = self.parent_canvas.mouse_controller.get_absolute_position()
+
+ current_mouse_pos_angle = self.parent_canvas.find_angle_from_center(mouse_pos_x,
+ mouse_pos_y)
+ if current_mouse_pos_angle <= self.initial_angle:
+ angle_to_rotate = self.initial_angle - self.current_angle
+ else:
+ angle_to_rotate = current_mouse_pos_angle - self.current_angle
+ if not -2 <= angle_to_rotate <= 2:
+ # To avoid the user from taking shortcut while rotating the phone
+ return 0
+ # If the next position would be passed the stopper, we stop the rotation at
+ # the stopper angle
+ if self.current_angle + angle_to_rotate >= angle_of_stopper:
+ angle_to_rotate = angle_of_stopper - self.current_angle
+ return angle_to_rotate
+
+ def __update_highest_angle(self) -> None:
+ if self.current_angle > self.__max_angle:
+ self.__max_angle = self.current_angle
+ self.parent_canvas.update_current_phone_number()
diff --git a/project/PhoneNumber/PhoneCanvas.py b/project/PhoneNumber/PhoneCanvas.py
new file mode 100644
index 00000000..4dd619f4
--- /dev/null
+++ b/project/PhoneNumber/PhoneCanvas.py
@@ -0,0 +1,228 @@
+import tkinter as tk
+import math
+from project.MouseController import MouseController
+from project.PhoneNumber.PhoneButton import PhoneButton
+
+
+class PhoneCanvas(tk.Canvas):
+ """
+ Class for PhoneCanvas, draws a working rotary phone. The user can click and drag the button in
+ order to dial a phone number.
+
+ === Public Attributes ===
+ canvas_size: Size of the canvas (square canvas of size X size)
+ radius: Biggest radius of the phone
+ phone_button_radius: Radius of all the phone buttons
+ phone_button_pos_radius: Radius of the circle where all the phone buttons are lain out
+ circle_buttons: List containing all the phone buttons.
+ mouse_controller: MouseController object linked to the root window of the canvas.
+
+ === Methods ===
+ create_circle: Simple canvas method allowing to draw a circle.
+ create_circle_arc: Simple canvas method allowing to draw a circle arc.
+ send_output_number: This method is called by the PhoneButton when their rotating animation is
+ done. The output number is sent to the number to the __output_entry
+ rotate_all_circles: This method rotates all the PhoneButton by the given angle.
+ verify_click_position: This method verifies where the user clicked. If it's inside one of the
+ PhoneButton, it calls the PhoneButton.on_mouse_down() method.
+ mouse_release: This method is called when the mouse button is released. It calls the PhoneButton
+ method of the button that was clicked.
+ update_current_phone_number: This method verifies the position of all the PhoneButton and store
+ the current phone number that should be dialed if the user stops moving.
+ find_angle_from_center: This method takes a position (x,y) and returns the angle of this
+ position according to the center of the phone.
+ """
+
+ def __init__(self, master, width=300):
+ super().__init__(master, width=width, height=width)
+ self.master = master
+ self.configure(bg='#00536a', border=0, bd=0, highlightthickness=0, relief='ridge')
+
+ self.canvas_size = width
+ self.radius = int(self.canvas_size / 2 * 0.99)
+ self.phone_button_radius = int(self.radius * 0.10)
+ # radius of the circle where all the buttons will be lain out.
+ self.phone_button_pos_radius = int(0.7 * self.canvas_size / 2)
+ # List containing all the buttons
+ self.circle_buttons = []
+
+ self.mouse_controller = MouseController(self)
+
+ # Stores the phone number that will be output in the current click.
+ self.__is_button_animated = False
+ self.__current_phone_number = None
+ self.__clicked_button = None
+
+ # draw all the components of the phone
+ self.__draw_circles()
+ self.__draw_phone_buttons()
+ self.__draw_all_numbers()
+ self.__draw_stopper()
+
+ self.bind("", self.verify_click_position)
+ self.bind("", self.mouse_release)
+
+ def create_circle(self, x, y, r, **kwargs) -> classmethod:
+ """
+ Allows the user to draw a circle on the canvas.
+ :param x: X position of the center of the circle
+ :param y: Y position of the center of the circle
+ :param r: Radius of the circle
+ :param kwargs:
+ :return: Modified create_oval method to draw the circle with the given parameters.
+ """
+ return self.create_oval(x - r, y - r, x + r, y + r, **kwargs)
+
+ def create_circle_arc(self, x, y, r, **kwargs) -> classmethod:
+ """
+ This method draws a circle arc on the canvas
+ :param x: X position of the center of the arc
+ :param y: Y position of the center of the arc
+ :param r: Radius of the arc
+ :param kwargs: important parameter are 'start' and 'end' to setup the angle where the arc is
+ positioned.
+ :return: Returns the modified create_arc methods that will actually draw a circle_arc with
+ the given parameters.
+ """
+ if "start" in kwargs and "end" in kwargs:
+ kwargs["extent"] = kwargs["end"] - kwargs["start"]
+ del kwargs["end"]
+ return self.create_arc(x - r, y - r, x + r, y + r, **kwargs)
+
+ def send_output_number(self) -> str:
+ """
+ This method is to get the last output number that was dialed.
+ :return: A string with a number in it, or None if nothing was dialed.
+ """
+ self.__is_button_animated = False
+ if self.__current_phone_number is None:
+ return ''
+ return self.__current_phone_number.text
+
+ def rotate_all_circles(self, angle) -> None:
+ """
+ This method rotates all the PhoneButton by the given angle (rad)
+ :param angle: angle that we want to rotate all the buttons (rad)
+ :return: None
+ """
+ for circle in self.circle_buttons:
+ circle.rotate(angle)
+ # Redraw all the numbers on top of the PhoneButton.
+ self.__draw_all_numbers()
+
+ def verify_click_position(self, event) -> None:
+ """
+ This method verifies the click position and call the PhoneButton.on_click_down() method if
+ the click is inside the PhoneButton.
+ :param event: The event is a "" event.
+ :return: None
+ """
+ if self.__is_button_animated:
+ return
+ x = event.x
+ y = event.y
+ for button in self.circle_buttons:
+ if button.is_position_inside_circle(x, y):
+ self.__current_phone_number = None
+ self.__clicked_button = button
+ self.__is_button_animated = True
+ button.on_click_down()
+ return
+
+ def mouse_release(self, event) -> None:
+ """
+ This method is linked to the button release event. It verifies if a button was being dragged
+ and calls its on_click_release method if that's the case.
+ :param event: ""
+ :return: None
+ """
+ if self.__clicked_button is not None:
+ self.__clicked_button.on_click_release()
+ self.__clicked_button = None
+
+ def update_current_phone_number(self) -> None:
+ """
+ This method verifies all the PhoneButton position and change the __current_phone_number
+ variable to the number that should be dialed if the user release the current drag.
+ :return: None
+ """
+ min_angle = 5 # This is the angle of the stopper.
+ current_button = None
+ nearest_angle = math.inf
+ # We loop through the buttons to verify if any went up to the stopper,
+ # and which on went further.
+ for button in self.circle_buttons:
+ if min_angle <= button.current_angle < nearest_angle:
+ nearest_angle = button.current_angle
+ current_button = button
+ if current_button is not None:
+ if self.__current_phone_number is None:
+ self.__current_phone_number = current_button
+ elif int(current_button.text) < int(self.__current_phone_number.text):
+ self.__current_phone_number = current_button
+
+ def find_angle_from_center(self, pos_x: int, pos_y: int) -> float:
+ """
+ This methods calculates the angle of a given position x,y from the center of the phone.
+ :param pos_x: X position to calculate the angle to
+ :param pos_y: Y position to calculate the angle to
+ :return: angle in rad
+ """
+ return math.atan2(pos_y - self.canvas_size / 2, pos_x - self.canvas_size / 2) + math.pi
+
+ def __draw_circles(self):
+ self.create_circle(self.canvas_size / 2, self.canvas_size / 2, self.radius, fill='black',
+ tag='main_circle', outline='#00536a', width='5')
+ self.create_circle(self.canvas_size / 2, self.canvas_size / 2, self.radius * 0.40,
+ fill='#00536a',
+ outline='#00536a', width='3')
+
+ def __draw_phone_buttons(self):
+ angle = 30 / 180 * math.pi
+ for i in range(0, 10):
+ x_center_of_circle = int(self.canvas_size / 2 - self.phone_button_pos_radius *
+ math.cos(angle))
+ y_center_of_circle = int(
+ self.canvas_size / 2 - self.phone_button_pos_radius * math.sin(angle))
+ self.circle_buttons.append(PhoneButton(self, x_center_of_circle, y_center_of_circle,
+ int(self.canvas_size / 2),
+ int(self.canvas_size / 2),
+ self.phone_button_radius, str(i)))
+ angle += 25 / 180 * math.pi
+
+ def __draw_stopper(self):
+ stopper_angle = 60
+ stopper_radius = self.radius * 0.15
+
+ self.create_circle_arc(self.canvas_size / 2, self.canvas_size / 2, stopper_radius,
+ fill='#00536a',
+ outline='white', width=5,
+ start=stopper_angle - 90, end=stopper_angle + 90, style='arc')
+ x1 = self.canvas_size / 2 - stopper_radius * math.cos((90 - stopper_angle) / 180 * math.pi)
+ y1 = self.canvas_size / 2 - stopper_radius * math.sin((90 - stopper_angle) / 180 * math.pi)
+ x2 = self.canvas_size / 2 - self.radius * math.sin((90 - stopper_angle) / 180 * math.pi)
+ y2 = self.canvas_size / 2 + self.radius * math.cos((90 - stopper_angle) / 180 * math.pi)
+ x3 = self.canvas_size / 2 + stopper_radius * math.cos((90 - stopper_angle) / 180 * math.pi)
+ y3 = self.canvas_size / 2 + stopper_radius * math.sin((90 - stopper_angle) / 180 * math.pi)
+
+ self.create_polygon([x1, y1, x2, y2, x3, y3], fill='#00536a', outline='white', width=5)
+ self.create_circle(self.canvas_size / 2, self.canvas_size / 2, stopper_radius - 4,
+ fill='#00536a', width=0)
+
+ def __draw_all_numbers(self):
+ angle = 30 / 180 * math.pi
+ for i in range(0, 10):
+ x_center_of_circle = self.canvas_size / 2 - self.phone_button_pos_radius * math.cos(
+ angle)
+ y_center_of_circle = self.canvas_size / 2 - self.phone_button_pos_radius * math.sin(
+ angle)
+ self.create_text(x_center_of_circle, y_center_of_circle, text=i,
+ font=('Calibri', int(0.05 * self.canvas_size)))
+ angle += 25 / 180 * math.pi
+
+
+if __name__ == '__main__':
+ root = tk.Tk()
+ root.resizable(False, False)
+ PhoneCanvas(root)
+ root.mainloop()
diff --git a/project/WheelSpinner/WheelSpinner.py b/project/WheelSpinner/WheelSpinner.py
new file mode 100644
index 00000000..d3c7a65c
--- /dev/null
+++ b/project/WheelSpinner/WheelSpinner.py
@@ -0,0 +1,250 @@
+import tkinter as tk
+from random import randint
+import time
+import math
+from project.MouseController import MouseController
+
+
+class WheelSpinner(tk.Frame):
+
+ def __init__(self, master, wheel_options, radius, *args, **kwargs):
+ super().__init__(master, *args, **kwargs)
+
+ self.master = master
+ self.radius = radius
+ self.display_label = tk.Label(self, height=2)
+ self.wheel_options = wheel_options
+ self.size = radius * 2.1
+ self.canvas = tk.Canvas(self, width=self.size, height=self.size)
+ self.drawn_arc = []
+ self.count = None
+ self.angle_increment = None
+ self.winner = None
+ self.is_rotating = False
+
+ self.frame = 0
+ self.speed = 400
+ self.display_label.grid(row=0, column=0)
+ self.canvas.grid(row=1, column=0)
+
+ self.canvas.bind("", lambda event: self.verify_click_position(event))
+ self.canvas.bind("", lambda event: self.on_mouse_release(event))
+
+ self.__drawn = False
+ self.__rotation_speed_list = []
+ self.__is_dragging = False
+ self.__init_drag_pos = None
+ self.__current_time = None
+ self.__delta_time = None
+
+ self.__mouse_controller = MouseController(self.canvas)
+
+ self.update()
+
+ def draw(self):
+ self.display_label['text'] = "Spin the wheel to find out \nwhat information you'll see!"
+ self.count = len(self.wheel_options)
+ angle = 0
+ self.angle_increment = 360 / self.count
+ for option in self.wheel_options:
+
+ if self.wheel_options[self.count - 1] == option:
+ self.drawn_arc.append(RotatingArc(self, self.size / 2, self.size / 2, self.radius,
+ angle, 360, option,
+ fill=self.generate_random_color(), width=3))
+ else:
+ self.drawn_arc.append(RotatingArc(self, self.size / 2, self.size / 2, self.radius,
+ angle, angle + self.angle_increment, option,
+ fill=self.generate_random_color(), width=3))
+ angle = angle + self.angle_increment
+
+ self.__drawn = True
+
+ def erase(self):
+ self.canvas.delete('all')
+
+ def display_current_winner(self):
+ winner = None
+ for arc in self.drawn_arc:
+ if 90 >= arc.start_angle >= 90 - self.angle_increment:
+ winner = arc.text
+ if winner is not None:
+ self.display_label['text'] = winner
+
+ def update(self):
+ if not self.__drawn:
+ self.after(33, self.update)
+ return
+ if self.__current_time is None:
+ self.__delta_time = 1 / 30
+ else:
+ self.__delta_time = time.time() - self.__current_time
+
+ if self.is_rotating:
+ self.rotate_all_with_speed()
+ self.calculate_new_speed()
+ self.display_current_winner()
+
+ if self.__is_dragging:
+ self.drag()
+
+ self.after(33, self.update)
+
+ def verify_click_position(self, event):
+ if self.__is_dragging or self.is_rotating or not self.__drawn:
+ return
+
+ x, y = event.x, event.y
+
+ # self.is_rotating = True
+
+ if math.sqrt(
+ math.pow(self.size / 2 - x, 2) + math.pow(self.size / 2 - y, 2)) <= self.radius:
+ self.__is_dragging = True
+ self.__rotation_speed_list = []
+ self.__init_drag_pos = x, y
+
+ def drag(self):
+ x0, y0 = self.__init_drag_pos
+ x, y = self.__mouse_controller.get_absolute_position()
+ angle_to_rotate = math.atan2(y - self.size / 2, x - self.size / 2) - \
+ math.atan2(y0 - self.size / 2, x0 - self.size / 2)
+ if abs(angle_to_rotate) > math.pi:
+ angle_to_rotate = -math.copysign(1, angle_to_rotate) * 2 * math.pi + angle_to_rotate
+ print(angle_to_rotate)
+ self.rotate_all(-angle_to_rotate / math.pi * 180)
+ self.__rotation_speed_list.append((angle_to_rotate / math.pi * 180 / self.__delta_time))
+ self.__init_drag_pos = x, y
+
+ def on_mouse_release(self, event):
+ if self.__is_dragging:
+ self.__is_dragging = False
+ self.__calculate_initial_speed()
+
+ def __calculate_initial_speed(self):
+ if len(self.__rotation_speed_list) <= 1:
+ self.display_label['text'] = "SPIN HARDER!"
+ return
+
+ self.speed = -self.__rotation_speed_list[-1]
+ if abs(self.speed) < 300:
+ self.display_label['text'] = "SPIN HARDER!"
+ else:
+ self.is_rotating = True
+
+ def rotate_all(self, degree):
+ for arc in self.drawn_arc:
+ arc.rotate(degree)
+
+ def rotate_all_with_speed(self):
+ for arc in self.drawn_arc:
+ arc.rotate(self.speed * self.__delta_time)
+
+ def calculate_new_speed(self):
+ print(self.speed)
+ speed_pos = abs(self.speed)
+ if speed_pos >= 2000:
+ acceleration = 1200 * -math.copysign(1, self.speed)
+ elif speed_pos >= 1000:
+ acceleration = 500 * -math.copysign(1, self.speed)
+ elif speed_pos >= 600:
+ acceleration = 250 * -math.copysign(1, self.speed)
+ elif speed_pos >= 350:
+ acceleration = 120 * -math.copysign(1, self.speed)
+ elif speed_pos >= 200:
+ acceleration = 50 * -math.copysign(1, self.speed)
+ elif speed_pos >= 100:
+ acceleration = 20 * -math.copysign(1, self.speed)
+ else:
+ acceleration = 10 * -math.copysign(1, self.speed)
+
+ if math.copysign(1, self.speed) != math.copysign(1,
+ self.speed + acceleration *
+ self.__delta_time):
+ self.speed = 0
+ self.finish_rotation()
+ else:
+ self.speed = self.speed + acceleration * self.__delta_time
+ print(self.speed)
+
+ def finish_rotation(self):
+ self.winner = self.display_label['text']
+ self.is_rotating = False
+ self.erase()
+ self.__drawn = False
+ self.master.event_generate("<>", when="tail")
+ self.master.show_winning_info()
+
+ def __get_elapsed_time(self):
+ """
+ This function returns the elapsed time since the last time we updated the rotating animation
+ If it's the first frame of the animation, we assume the elapsed_time is 0.033s which
+ correspond to 30 fps.
+ :return:
+ """
+ if self.is_rotating is None:
+ elapsed_time = 0.0333
+ else:
+ elapsed_time = time.time() - self.is_rotating
+ return elapsed_time
+
+ def create_circle_arc(self, x, y, r, **kwargs) -> classmethod:
+ """
+ This method draws a circle arc on the canvas
+ :param x: X position of the center of the arc
+ :param y: Y position of the center of the arc
+ :param r: Radius of the arc
+ :param kwargs: important parameter are 'start' and 'end' to setup the angle where the arc is
+ positioned.
+ :return: Returns the modified create_arc methods that will actually draw a circle_arc with
+ the given parameters.
+ """
+ if "start" in kwargs and "end" in kwargs:
+ kwargs["extent"] = kwargs["end"] - kwargs["start"]
+ del kwargs["end"]
+ return self.canvas.create_arc(x - r, y - r, x + r, y + r, **kwargs)
+
+ @staticmethod
+ def generate_random_color():
+ r = randint(0, 255)
+ g = randint(0, 255)
+ b = randint(0, 255)
+
+ return "#%02x%02x%02x" % (r, g, b)
+
+
+class RotatingArc:
+ def __init__(self, frame, position_x, position_y, radius, start_angle, end_angle, text, *args,
+ **kwargs):
+ self.frame_parent = frame
+ self.canvas_parent = frame.canvas
+ self.radius = radius
+ self.position_x = position_x
+ self.position_y = position_y
+ self.start_angle = start_angle
+ self.end_angle = end_angle
+ self.item = None
+ self.text = text
+ self.draw(*args, **kwargs)
+
+ def draw(self, *args, **kwargs):
+ self.item = self.frame_parent.create_circle_arc(self.position_x, self.position_y,
+ self.radius,
+ start=self.start_angle, end=self.end_angle,
+ *args, **kwargs)
+
+ def rotate(self, angle, *args):
+ self.canvas_parent.itemconfigure(self.item, start=self.start_angle + angle)
+ self.start_angle += angle
+ if self.start_angle >= 360:
+ self.start_angle -= 360
+ if self.start_angle < 0:
+ self.start_angle += 360
+
+
+if __name__ == '__main__':
+ root = tk.Tk()
+ options = ['Name', 'Home Phone Numbers', 'Work Phone Numbers', 'Personal Phone Numbers',
+ 'Emails', 'Home Addresses', 'Notes']
+ WheelSpinner(root, options, width=300, height=500, radius=150)
+ root.mainloop()
diff --git a/project/__main__.py b/project/__main__.py
index e69de29b..fc3d524b 100644
--- a/project/__main__.py
+++ b/project/__main__.py
@@ -0,0 +1,677 @@
+import random
+from tkinter import Tk, Frame, Listbox, Button, Label, Scrollbar, VERTICAL, END, SINGLE, NONE, \
+ StringVar, Radiobutton, Toplevel, N, S, E, W
+from tkinter.ttk import Notebook
+from project.contact import Contact
+import pickle
+from project.WheelSpinner.WheelSpinner import WheelSpinner
+from project.PhoneNumber.AddPhoneNumberInter import AddPhoneNumberInter
+from project.AlphabetGuesser.AlphabetGuesserInter import AlphabetGuesserInter
+from project.AlphabetGuesser.create_dictionary import create_dictionary
+from project.create_contact_list_pickle import main as create_contact_list
+
+
+class Controller(Tk):
+ """
+ Controller Class:
+ - Serves as a hub for every page contained in the UI, allows for the flow of information between
+ pages
+ - Acts as the container for ContactsPage, and AddContactsPage by making use of ttk.Notebook
+
+ === Public Attributes ===
+ notebook: Widget containing tabs; each page is assigned a tab, and can be navigated to easily
+ frames: Dictionary of all pages; allows for access of information across pages
+ e.g. If I wanted to call a method from a separate class:
+ self.controller.frames[].()
+
+ === Methods ===
+ None
+ """
+
+ def __init__(self, *args, **kwargs):
+ Tk.__init__(self, *args, **kwargs)
+ self.resizable(False, False)
+ self.geometry("300x400")
+ self.title("Contact Manager")
+ self.iconbitmap("project/src/Phone.ico")
+ for i in range(5):
+ self.rowconfigure(i, weight=1)
+ self.columnconfigure(i, weight=1)
+
+ self.frames = {}
+
+ self.notebook = Notebook(self)
+ self.notebook.grid(row=0, column=0, columnspan=5, rowspan=5, sticky=N + S + E + W)
+
+ for page in [ContactsPage, AddContactPage]:
+ frame = page(self.notebook, self)
+ self.notebook.add(frame, text=frame.page_name)
+ self.frames[frame.page_name] = frame
+
+ def hide(self):
+ self.withdraw()
+
+ def show(self):
+ self.deiconify()
+
+
+class ContactsPage(Frame):
+ """
+ Contacts Page:
+ Contains the list of currently added contacts
+
+ === Public Attributes ===
+ master: The frame containing all information on this page
+ controller: Reference to the Controller Class
+
+ page_name: String containing a name for this page; used for setting the tabs
+ in the containing notebook
+
+ contacts_list: Dictionary of contacts, each contact is a Contact class instance,
+ each key is the name of the contact
+ current_contact: Contains the contact that was selected the last time we clicked on show info.
+
+ scroll_bar: Scroll bar that controls what is viewable in the contacts list;
+ won't scroll if nothing is in the list, or everything is already
+ shown.
+ contacts_field: Area where contacts are shown; 10 at a time
+ letters_field: Listbox that shows each letter of the alphabet to help the user find
+ contact they're looking for
+ show_info: Button that updates the info_field with the information of the
+ currently selected contact
+ wheel_spin: WheelSpinner object for the Show Contact Button.
+ delete: Button that deletes the selected contact
+ info_field: Listbox that contains the information of the currently selected contact
+ info_scroll: Scrollbar that controls what is viewable in the info_field; won't
+ scroll if nothing is in the list, or everything is already shown
+
+ self.load: Button to load contacts
+ self.save: Button to save contacts
+
+ === Methods ===
+ create: Initializes objects & places them on the page
+ insert_contact: Adds a contact's name to the end of the contacts field
+ show_contact_info: Shows the information of the selected contact in the info listbox
+ delete_contact: Deletes the selected contact & reloads the contacts Listbox
+ clear_fields: Clears both fields on the contacts page
+ load_contacts: Loads contacts in from a file
+ save_contacts: Saves contacts as a file
+ yview: Adjusts the view of contacts_field & letters_field at the same time
+ on_mouse_wheel: Adjusts the view of contacts_field and letters_field at the same time, for the
+ mouse wheel
+ """
+
+ def __init__(self, master, controller, **kw):
+ super().__init__(master, **kw)
+ self.master = master
+ self.controller = controller
+
+ self.page_name = "View Contacts"
+
+ # Initialize object names
+ self.contacts_list = {}
+ self.current_contact = None
+ self.alphabetical_order = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
+
+ self.scroll_bar = None
+ self.contacts_field = None
+ self.letters_field = None
+ self.show_info = None
+ self.wheel_spin = None
+ self.delete = None
+ self.info_field = None
+ self.info_scroll = None
+
+ self.bind("<>", self.show_winning_info)
+ self.bind("", self.__on_visibility)
+
+ self.load = None
+ self.save = None
+
+ self.create()
+
+ def create(self) -> None:
+ self.info_scroll = Scrollbar(self, orient=VERTICAL)
+ self.info_field = Listbox(
+ self,
+ yscrollcommand=self.info_scroll.set
+ )
+
+ self.delete = Button(self, text="Delete", command=lambda: self.delete_contact())
+ self.delete.grid(row=2, column=3, columnspan=3, sticky=N + S + E + W)
+
+ self.show_info = Button(self, text="Show Info", command=lambda: self.show_contact_info())
+ self.show_info.grid(row=2, column=0, columnspan=3, sticky=N + S + E + W)
+
+ wheel_spin_options = ['Name', 'Home Phone Numbers', 'Work Phone Numbers',
+ 'Personal Phone Numbers', 'Emails', 'Home Addresses', 'Notes']
+ self.wheel_spin = WheelSpinner(self, wheel_spin_options, width=50, height=200, radius=60)
+ self.wheel_spin.grid(row=3, column=0, columnspan=5)
+
+ self.scroll_bar = Scrollbar(self)
+ self.contacts_field = Listbox(
+ self,
+ yscrollcommand=self.scroll_bar.set,
+ selectmode=SINGLE,
+ exportselection=0
+ )
+ self.letters_field = Listbox(
+ self,
+ width=2,
+ selectmode=NONE,
+ exportselection=0
+ )
+
+ self.letters_field.bind('<>', self.scroll_to_letter)
+ self.contacts_field.grid(row=1, column=0, columnspan=5, sticky=N + S + E + W)
+ self.letters_field.grid(row=1, column=4, sticky=N + S + E + W)
+ self.scroll_bar.grid(row=1, column=5, sticky=N + S + E + W)
+ self.scroll_bar.config(command=self.yview)
+ self.contacts_field.bind("", self.on_mouse_wheel)
+ self.letters_field.bind("", self.on_letter_mouse_wheel)
+
+ self.save = Button(
+ self,
+ text="Save Contacts",
+ command=lambda: self.save_contacts()
+ )
+ self.save.grid(row=0, column=3, columnspan=4, sticky=N + S + E + W)
+
+ self.load = Button(
+ self,
+ text="Load Contacts",
+ command=lambda: self.load_contacts()
+ )
+ self.load.grid(row=0, column=0, columnspan=3, sticky=N + S + E + W)
+
+ for i in range(3):
+ self.grid_rowconfigure(i, weight=1)
+
+ for i in range(4):
+ self.grid_columnconfigure(i, weight=1)
+
+ def scroll_to_letter(self, event):
+ id = 0
+ for contact in self.order_contact():
+ if contact[0] == self.letters_field.get(self.letters_field.curselection()[0]):
+ self.contacts_field.see(id)
+ self.contacts_field.selection_clear(0, END)
+ self.contacts_field.selection_set(id)
+ self.letters_field.selection_clear(0, END)
+ return
+ id += 1
+ self.letters_field.selection_clear(0, END)
+
+ def on_mouse_wheel(self, event) -> str:
+ self.contacts_field.yview("scroll", int(-event.delta / 80), "units")
+ return "break"
+
+ def on_letter_mouse_wheel(self, event) -> str:
+ self.letters_field.yview("scroll", int(-event.delta / 80), "units")
+ return "break"
+
+ def yview(self, *args) -> None:
+ self.contacts_field.yview(*args)
+ self.letters_field.yview(*args)
+
+ def delete_contact(self) -> None:
+ name = self.contacts_field.get(self.contacts_field.curselection()[0])
+ del self.contacts_list[name]
+ self.clear_fields()
+ for contact in sorted(self.contacts_list):
+ self.insert_contact(contact)
+
+ def clear_fields(self) -> None:
+ for field in [self.contacts_field, self.info_field, self.letters_field]:
+ field.delete(0, END)
+
+ def refresh_fields(self) -> None:
+ self.clear_fields()
+ for contact in self.order_contact():
+ self.contacts_field.insert(END, contact)
+
+ for letter in self.alphabetical_order:
+ self.letters_field.insert(END, letter.upper())
+
+ def load_contacts(self) -> None:
+ self.randomize_alphabetical_order()
+ with open("project/contacts_pickle", 'rb') as infile:
+ self.contacts_list = pickle.load(infile)
+ self.refresh_fields()
+
+ def save_contacts(self) -> None:
+ with open("project/contacts_pickle", 'wb') as outfile:
+ pickle.dump(self.contacts_list, outfile)
+
+ def insert_contact(self, contact) -> None:
+ self.contacts_field.insert(END, contact)
+
+ def show_contact_info(self) -> None:
+ """
+ This method shows the spinning wheel if a contact is selected and if the wheel isn't already
+ rotating
+ It is called on the Show Contact Info button.
+ :return: None
+ """
+ if len(self.contacts_field.curselection()) == 0 or self.wheel_spin.is_rotating:
+ return
+
+ name = self.contacts_field.get(self.contacts_field.curselection()[0])
+ self.current_contact = self.contacts_list[name]
+ self.wheel_spin.draw()
+
+ def show_winning_info(self, event) -> None:
+ """
+ This method is called when the event <> is invoked. It displays the
+ current contact information that was selected by the spinning wheel.
+ :return: None
+ """
+ self.randomize_alphabetical_order()
+ self.refresh_fields()
+ winner = self.wheel_spin.winner
+ label = self.wheel_spin.display_label
+ text0 = self.current_contact.name + "'s " + winner.lower() + ':\n'
+ text1 = ''
+
+ if winner == 'Name':
+ text1 = self.current_contact.name
+ elif winner == 'Home Phone Numbers':
+ for elem in self.current_contact.phone_numbers["Home"]:
+ text1 = text1 + elem + ", "
+ elif winner == 'Work Phone Numbers':
+ for elem in self.current_contact.phone_numbers["Work"]:
+ text1 = text1 + elem + ', '
+ elif winner == 'Personal Phone Numbers':
+ for elem in self.current_contact.phone_numbers["Personal"]:
+ text1 = text1 + elem + ', '
+ elif winner == 'Emails':
+ for elem in self.current_contact.email_addresses:
+ text1 = text1 + elem + ', '
+ elif winner == 'Home Addresses':
+ for elem in self.current_contact.addresses:
+ text1 = text1 + elem + ', '
+ elif winner == 'Notes':
+ for elem in self.current_contact.notes:
+ text1 = text1 + elem + ', '
+
+ label['text'] = text0 + text1
+
+ def randomize_alphabetical_order(self):
+ random.shuffle(self.alphabetical_order)
+
+ def order_contact(self) -> list:
+ """
+ This function takes all the contacts and order them in the order stored in self.alphabetical
+ order
+ :return: The ordered list
+ """
+ i = 0
+ order = self.alphabetical_order
+ contacts = list(self.contacts_list)
+ ordered_list = []
+
+ # We loop until we have all the contact ordered.
+ while i < len(self.contacts_list):
+ current_next_contact = None
+ for contact in contacts:
+ if current_next_contact is None:
+ current_next_contact = contact
+ continue
+ # If the first letter is higher in the order than the current next contact, we
+ # change the contact.
+ if order.index(contact[0].lower()) < order.index(current_next_contact[0].lower()):
+ current_next_contact = contact
+ continue
+
+ # If the first character is the same, we loop through the other character to find
+ # which on should be
+ # added first.
+ if order.index(contact[0].lower()) == order.index(current_next_contact[0].lower()):
+ for current_character in range(1, min(len(contact), len(current_next_contact))):
+ if order.index(contact[current_character].lower()) < \
+ order.index(current_next_contact[current_character].lower()):
+ current_next_contact = contact
+ break
+ if order.index(contact[current_character].lower()) > \
+ order.index(current_next_contact[current_character].lower()):
+ break
+ # we append the contact to the list and remove it from the remaining contact to order.
+ contacts.remove(current_next_contact)
+ ordered_list.append(current_next_contact)
+ i += 1
+ return ordered_list
+
+ def __on_visibility(self, event) -> None:
+ """
+ This function is called when the user click on the contact tab. It randomizes the
+ alphabetical order
+ :param event:
+ :return: None
+ """
+ self.randomize_alphabetical_order()
+ self.refresh_fields()
+
+
+class AddContactPage(Frame):
+ """
+ Add New Contact Page:
+
+ === Public Attributes ===
+ master: The frame containing all information on this page
+ controller: Reference to the Controller Class
+
+ contact_new: Contact class instance that holds the data that the
+ user inputs; when the user submits, the information is
+ stored on the contacts_list in ContactsPage.
+
+ Each contact has 5 attributes:
+ - Name
+ - Phone Number
+ - Email
+ - Address
+ - Notes
+ Each attribute has the corresponding objects on the page:
+ - A Label
+ - A Text Entry Box
+ - A Button to add the information to the preview, & update the contact
+ The only exception being the Phone Number, which requires the user to
+ choose whether the phone number is for Home, Work, or Personal.
+ This is done using a Radiobutton, which can only have one value
+ chosen at a time. The value of the Radiobutton is tied to a
+ StringVar called phone_type_var. When the user clicks 'Add' next
+ to the phone number, the StringVar is passed into the
+ add_phone_number method.
+
+ clear: Button that clears all text entries
+ add_to_contacts: Button that adds the current contact to the contact_list
+ on ContactsPage
+ text_entries: List of all text entries; can be looped over to perform a
+ repetitive task on each entry. e.g Clearing all entries
+
+ preview_scroll: Scrollbar that control what is viewable in the preview
+ Listbox
+ preview: Listbox that shows the info of the contact being created currently
+
+ === Methods ===
+ create: Initializes objects & places them on the page
+ add_contact: Adds contact to the contact_list in ContactsPage;
+ If the contact is new, the name of the contact is added to
+ the contacts Listbox on ContactsPage
+ clear_all: Loops over all text entries and clears them
+ add_name: Changes the new contact's name and updates the preview
+ add_phone_num: Adds the phone number to the new contact and updates the preview
+ add_email: Adds the email to the new contact and updates the preview
+ add_address: Adds the address to the new contact and updates the preview
+ add_notes: Adds the note to the new contact and updates the preview
+ refresh_field: Clears the field then populates it with all the current information of the new
+ contact
+ """
+
+ def __init__(self, master, controller, **kw):
+ super().__init__(master, **kw)
+ self.master = master
+ self.controller = controller
+
+ self.page_name = "New Contact"
+
+ # Initialize object names
+ self.contact_new = Contact('')
+
+ self.enter_name = None
+ self.enter_name_label = None
+ self.enter_name_button = None
+
+ # PLACEHOLDER FOR ACTUAL PHONE NUMBER ENTRY
+ self.enter_phone_num = None
+ self.phone_type_home = None
+ self.phone_type_work = None
+ self.phone_type_personal = None
+ self.enter_phone_num_label = None
+ self.enter_phone_num_button = None
+ self.phone_type_var = None
+
+ self.enter_email = None
+ self.enter_email_label = None
+ self.enter_email_button = None
+
+ self.enter_address = None
+ self.enter_address_label = None
+ self.enter_address_button = None
+
+ self.enter_notes = None
+ self.enter_notes_label = None
+ self.enter_notes_button = None
+
+ self.clear = None
+ self.add_to_contacts = None
+
+ self.preview_scroll = None
+ self.preview = None
+
+ self.text_entries = None
+
+ # Create objects
+ self.create()
+
+ def create(self) -> None:
+ self.preview_scroll = Scrollbar(self, orient=VERTICAL)
+ self.preview = Listbox(
+ self,
+ yscrollcommand=self.preview_scroll.set
+ )
+ self.preview_scroll.config(command=self.preview.yview)
+ self.preview.grid(row=8, column=0, columnspan=5, sticky=N + S + E + W)
+ self.preview_scroll.grid(row=8, column=5, sticky=N + S + E + W)
+
+ self.add_to_contacts = Button(self, text="Submit to Contacts",
+ command=lambda: self.add_contact())
+ self.add_to_contacts.grid(row=7, column=0, columnspan=2, sticky=N + S + E + W)
+
+ self.clear = Button(self, text="Clear All", command=lambda: self.clear_all())
+ self.clear.grid(row=7, column=2, sticky=N + S + E + W)
+
+ self.enter_notes = Label(self, text="Click here to add a note.", relief="sunken")
+ self.enter_notes.bind("",
+ lambda event,
+ arg=self.enter_notes: self.input_text(event, arg, "Notes"))
+ self.enter_notes_label = Label(self, text="Notes:")
+ self.enter_notes_button = Button(
+ self,
+ text="Add",
+ command=lambda: self.add_notes(self.enter_notes['text']),
+ width=5
+ )
+ self.enter_notes_button.grid(row=6, column=4, columnspan=2, sticky=N + S + E + W)
+ self.enter_notes_label.grid(row=6, column=0, sticky=N + S + E + W)
+ self.enter_notes.grid(row=6, column=1, columnspan=3, sticky=N + S + E + W)
+
+ self.enter_address = Label(self, text="Click here to add an address.", relief="sunken")
+ self.enter_address.bind("",
+ lambda event, arg=self.enter_address: self.input_text(event, arg,
+ "Address"))
+ self.enter_address_label = Label(self, text="Address")
+ self.enter_address_button = Button(
+ self,
+ text="Add",
+ command=lambda: self.add_address(self.enter_address['text']),
+ width=5
+ )
+ self.enter_address_label.grid(row=5, column=0, sticky=N + S + E + W)
+ self.enter_address.grid(row=5, column=1, columnspan=3, sticky=N + S + E + W)
+ self.enter_address_button.grid(row=5, column=4, columnspan=2, sticky=N + S + E + W)
+
+ self.enter_email = Label(self, text="Click here to add an email.", relief="sunken")
+ self.enter_email.bind("",
+ lambda event, arg=self.enter_email: self.input_text(event, arg,
+ "Email"))
+ self.enter_email_label = Label(self, text="Email:")
+ self.enter_email_button = Button(
+ self,
+ text="Add",
+ command=lambda: self.add_email(self.enter_email['text']),
+ width=5
+ )
+ self.enter_email_label.grid(row=4, column=0, sticky=N + S + E + W)
+ self.enter_email.grid(row=4, column=1, columnspan=3, sticky=N + S + E + W)
+ self.enter_email_button.grid(row=4, column=4, columnspan=2, sticky=N + S + E + W)
+
+ # PLACEHOLDER FOR ACTUAL PHONE NUMBER ENTRY
+ self.enter_phone_num = Label(self, text="###-###-####", relief="sunken")
+ self.enter_phone_num.bind("", self.input_phone_number)
+ self.enter_phone_num_label = Label(self, text="Phone:")
+ phone_type_var = StringVar()
+ self.phone_type_home = Radiobutton(self, text="Home", variable=phone_type_var, value="Home")
+ self.phone_type_work = Radiobutton(self, text="Work", variable=phone_type_var, value="Work")
+ self.phone_type_personal = Radiobutton(self, text="Personal", variable=phone_type_var,
+ value="Personal")
+ self.enter_phone_num_button = Button(
+ self,
+ text="Add",
+ command=lambda: self.add_phone_num(phone_type_var.get(), self.enter_phone_num['text']),
+ width=5
+ )
+ self.enter_phone_num_label.grid(row=2, column=0, sticky=N + S + E + W)
+ self.enter_phone_num.grid(row=2, column=1, columnspan=3, sticky=N + S + E + W)
+ self.phone_type_home.grid(row=3, column=0, sticky=N + S + E + W)
+ self.phone_type_work.grid(row=3, column=1, sticky=N + S + E + W)
+ self.phone_type_personal.grid(row=3, column=2, sticky=N + S + E + W)
+ self.enter_phone_num_button.grid(row=2, column=4, columnspan=2, sticky=N + S + E + W)
+
+ self.enter_name = Label(self, text="Click here to add a name.", relief="sunken")
+ self.enter_name.bind("",
+ lambda event, arg=self.enter_name: self.input_text(event, arg, "Name"))
+ self.enter_name_label = Label(self, text="Name:")
+ self.enter_name_button = Button(
+ self,
+ text="Add",
+ command=lambda: self.add_name(self.enter_name['text']),
+ width=5
+ )
+ self.enter_name_button.grid(row=1, column=4, columnspan=2, sticky=N + S + E + W)
+ self.enter_name_label.grid(row=1, column=0, sticky=N + S + E + W)
+ self.enter_name.grid(row=1, column=1, columnspan=3, sticky=N + S + E + W)
+
+ for i in range(8):
+ self.grid_rowconfigure(i, weight=1)
+
+ for i in range(5):
+ self.grid_columnconfigure(i, weight=1)
+
+ def add_name(self, name):
+ self.contact_new.change_name(name)
+ self.refresh_field()
+
+ def add_phone_num(self, num_type, num) -> None:
+ if num_type != '':
+ self.contact_new.add_phone_number(num_type, num)
+ self.refresh_field()
+
+ def add_email(self, email):
+ self.contact_new.add_address("Email", email)
+ self.refresh_field()
+
+ def add_address(self, address):
+ self.contact_new.add_address("Physical", address)
+ self.refresh_field()
+
+ def add_notes(self, note):
+ self.contact_new.add_note(note)
+ self.refresh_field()
+
+ def input_phone_number(self, event):
+ new_window = Toplevel(self)
+ new_window.geometry("300x400")
+ new_window.configure(background='#00536a')
+
+ self.controller.withdraw()
+
+ def quit_phone_input():
+ self.controller.show()
+ new_window.destroy()
+
+ def send_phone_input(event):
+ self.controller.show()
+ self.enter_phone_num['text'] = phone.get_complete_phone_number()
+ new_window.destroy()
+
+ new_window.bind("<>", send_phone_input)
+ new_window.wm_protocol("WM_DELETE_WINDOW", quit_phone_input)
+
+ phone = AddPhoneNumberInter(new_window, bg='#00536a')
+
+ def input_text(self, event, entry, entry_text):
+ new_window = Toplevel(self)
+ new_window.geometry("300x400")
+ self.controller.withdraw()
+
+ def quit_text_input():
+ self.controller.show()
+ new_window.destroy()
+
+ def send_text_input(event):
+ self.controller.show()
+ entry['text'] = alpha.get_answer()
+ new_window.destroy()
+
+ new_window.bind("<>", send_text_input)
+ new_window.wm_protocol("WM_DELETE_WINDOW", quit_text_input)
+
+ alpha = AlphabetGuesserInter(new_window, entry_text, width=300, height=500)
+
+ def refresh_field(self) -> None:
+ self.preview.delete(0, END)
+ name = self.contact_new.name
+ home_phone_nums = self.contact_new.phone_numbers["Home"]
+ work_phone_nums = self.contact_new.phone_numbers["Work"]
+ personal_phone_nums = self.contact_new.phone_numbers["Personal"]
+ emails = self.contact_new.email_addresses
+ addresses = self.contact_new.addresses
+ notes = self.contact_new.notes
+ self.preview.delete(0, END)
+ self.preview.insert(END, name)
+ self.preview.insert(END, "")
+ if len(home_phone_nums) or len(work_phone_nums) or len(personal_phone_nums) > 0:
+ self.preview.insert(END, "Phone:")
+ for elem in home_phone_nums:
+ self.preview.insert(END, " Home: " + elem)
+ for elem in work_phone_nums:
+ self.preview.insert(END, " Work: " + elem)
+ for elem in personal_phone_nums:
+ self.preview.insert(END, " Personal: " + elem)
+ if len(emails) > 0:
+ self.preview.insert(END, "Emails:")
+ for elem in emails:
+ self.preview.insert(END, " " + elem)
+ if len(addresses) > 0:
+ self.preview.insert(END, "Addresses:")
+ for elem in addresses:
+ self.preview.insert(END, " " + elem)
+ if len(notes) > 0:
+ self.preview.insert(END, "Notes:")
+ for elem in notes:
+ self.preview.insert(END, " " + elem)
+
+ def add_contact(self) -> None:
+ name = self.contact_new.name
+ contacts_list = self.controller.frames["View Contacts"].contacts_list
+ if name != '':
+ contacts_list[name] = self.contact_new
+ self.controller.frames["View Contacts"].contacts_list = contacts_list
+ self.controller.frames["View Contacts"].clear_fields()
+ for contact in sorted(contacts_list):
+ self.controller.frames["View Contacts"].insert_contact(contact)
+ self.contact_new = Contact('')
+
+ def clear_all(self) -> None:
+ for entry in [self.enter_name, self.enter_phone_num, self.enter_email, self.enter_address,
+ self.enter_notes,
+ self.preview]:
+ entry.delete(0, END)
+
+
+if __name__ == "__main__":
+ create_dictionary()
+ create_contact_list()
+ app = Controller()
+ app.mainloop()
diff --git a/project/contact.py b/project/contact.py
new file mode 100644
index 00000000..89f2ad25
--- /dev/null
+++ b/project/contact.py
@@ -0,0 +1,162 @@
+from typing import List, Dict
+
+
+class Contact:
+ """
+ Class for Contacts
+
+ === Public Attributes ===
+ name: Name of the contact
+ phone_numbers: All phone numbers of the contact
+ email_addresses: All email addresses of the contact
+ addresses: All physical addresses of the contact
+ notes: Any notes the user wishes to leave for the contact
+
+ === Methods ===
+ change_name: Allows the user to change the name of the contact
+ add_phone_number: Allows the user to add a phone number to any of the
+ categories Home, Work or Personal
+ change_phone_number: Allows the user to change a number already entered, if
+ the number does not exist then nothing is done.
+ If the new_num param is '', then the number is removed
+ add_address: Allows the user to add either an email address or a physical
+ address to the contact
+ change_email_address: Allows the user to change an email address already
+ entered if the address does not exist then nothing is
+ done. If the new_add param is '', then the address is
+ removed
+ change_address: Allows the user to change an email address already entered
+ if the address does not exist then nothing is done.
+ If the new_add param is '', then the address is removed
+
+ new_note: Allows the user to leave a note for the contact
+ change_note: Allows the user to change a note already entered, if the note
+ doesn't exist then nothing is done. If the new_note param is ''
+ then the notes is deleted
+
+ """
+ name: str
+ phone_numbers: Dict[str, List[str]]
+ email_addresses: List[str]
+ addresses: List[str]
+ notes: [str]
+
+ def __init__(self, name: str) -> None:
+ self.name = name
+ self.phone_numbers = {'Home': [], 'Work': [], 'Personal': []}
+ self.email_addresses = []
+ self.addresses = []
+ self.notes = []
+
+ def change_name(self, new_name: str) -> None:
+ """
+ Method allows the user to change the name of the contact
+ :param new_name: Name to be changed to
+ :return: None
+ """
+ print("DEBUG: Set Name To:", new_name)
+ self.name = new_name
+
+ def add_phone_number(self, num_type: str, number: str) -> None:
+ """
+ Method allows the user to add a new phone number for the contact
+ :param num_type: Home or Work or Personal. The type of phone number that
+ is being assigned
+ :param number: The phone number that is being assigned
+ :return: None
+ """
+ if num_type in ['Home', 'Work', 'Personal']:
+ print("DEBUG: Phone Number:", number)
+ print("DEBUG: Num_type:", num_type)
+ self.phone_numbers[num_type].append(number)
+
+ def change_phone_number(self, orig_num: str, new_num: str) -> None:
+ """
+ Method allows the user to change a phone number
+ :param orig_num: The original number to be changed
+ :param new_num: The number to be changed to
+ :return: None
+ """
+ if orig_num in self.phone_numbers['Home']:
+ self.phone_numbers['Home'].remove(orig_num)
+ if new_num != '':
+ self.phone_numbers['Home'].append(new_num)
+
+ elif orig_num in self.phone_numbers['Work']:
+ self.phone_numbers['Work'].remove(orig_num)
+ if new_num != '':
+ self.phone_numbers['Work'].append(new_num)
+
+ elif orig_num in self.phone_numbers['Personal']:
+ self.phone_numbers['Personal'].remove(orig_num)
+ if new_num != '':
+ self.phone_numbers['Personal'].append(new_num)
+
+ def add_address(self, address_type: str, address: str) -> None:
+ """
+ Method allows the user to add a new address for the contact
+ :param address_type: Physical or Email. The type of phone number that
+ is being assigned
+ :param address: The phone number that is being assigned
+ :return: None
+ """
+ if address_type == 'Physical':
+ self.addresses.append(address)
+
+ elif address_type == 'Email':
+ self.email_addresses.append(address)
+
+ def change_email_address(self, orig_add: str, new_add: str) -> None:
+ """
+ Method allows the user to change a phone number
+ :param orig_add: The original email address to be changed
+ :param new_add: The email address to be changed to
+ :return: None
+ """
+ if orig_add in self.email_addresses:
+ self.email_addresses.remove(orig_add)
+ if new_add != '':
+ self.email_addresses.append(new_add)
+
+ def change_address(self, orig_add: str, new_add: str) -> None:
+ """
+ Method allows the user to change a phone number
+ :param orig_add: The original address to be changed
+ :param new_add: The address to be changed to
+ :return: None
+ """
+ if orig_add in self.addresses:
+ self.addresses.remove(orig_add)
+ if new_add != '':
+ self.addresses.append(new_add)
+
+ def add_note(self, note: str) -> None:
+ """
+ Method allows the user to add a note
+ :param note: The note to be added
+ :return: None
+ """
+ self.notes.append(note)
+
+ def change_note(self, orig_note: str, new_note: str) -> None:
+ """
+ Method allows the user to change a note
+ :param orig_note: The original note to be changed
+ :param new_note: The note to be changed to
+ :return: None
+ """
+ if orig_note in self.notes:
+ self.notes.remove(orig_note)
+ if new_note != '':
+ self.notes.append(new_note)
+
+ def __str__(self):
+ text_to_print = "Contact: \nName: " + str(self.name) + "\n"
+ text_to_print += "Work phone number: " + str(self.phone_numbers["Work"]) + "\n"
+ text_to_print += "Home phone number: " + str(self.phone_numbers["Home"]) + "\n"
+ text_to_print += "Personal phone number: " + str(self.phone_numbers["Personal"]) + "\n"
+ text_to_print += "Email address: " + str(self.email_addresses) + "\n"
+ text_to_print += "Home address: " + str(self.addresses) + "\n"
+ text_to_print += "Notes: " + str(self.notes) + "\n"
+
+ return text_to_print
diff --git a/project/create_contact_list_pickle.py b/project/create_contact_list_pickle.py
new file mode 100644
index 00000000..e90f1949
--- /dev/null
+++ b/project/create_contact_list_pickle.py
@@ -0,0 +1,113 @@
+from project.contact import Contact
+from random import randint
+import pickle
+"""
+This script generates a list of 40 pre-made contacts for use in the main app.
+The names are pre-made, but the phone numbers are random and some info is
+intentionally missing from contacts (as it often is in real life)
+"""
+
+
+def generate_random_phone_number() -> str:
+ """
+ Function generates a random phone number.
+ :return: Random numbers in the format ###-###-####.
+ """
+ phone_number = ""
+ i = 0
+ while i < 10:
+ if i == 0:
+ number_to_add = randint(1, 9)
+ phone_number = phone_number + str(number_to_add)
+ i += 1
+ continue
+ if i == 2:
+ number_to_add = randint(0, 9)
+ phone_number = phone_number + str(number_to_add) + "-"
+ i += 1
+ continue
+ if i == 5:
+ number_to_add = randint(0, 9)
+ phone_number = phone_number + str(number_to_add) + "-"
+ i += 1
+ continue
+ number_to_add = randint(0, 9)
+ phone_number = phone_number + str(number_to_add)
+ i += 1
+
+ return phone_number
+
+
+def generate_random_address() -> str:
+ """
+ Function generate a random address with the list of street name below.
+ :return: A number from 1 to 999 followed by one of the street name in the street_name variable.
+ """
+ street_name = ["Main Street", "River Road", "Oak Street",
+ "Campbell Avenue", "Elizabeth II Street", "North Road",
+ "Charles Avenue", "Wellington Street", "Nelson Street",
+ "Hill Road", "Thompson Avenue"]
+ number = randint(1, 999)
+ street_name = street_name[randint(0, len(street_name)-1)]
+ return str(number) + " " + street_name
+
+
+def generate_email_address(input_name: str) -> str:
+ """
+ Function takes a name and generate an email address by replace all space in the name with dot.
+ It uses one of the domain name in the domain variable randomly.
+ :param input_name: The name of the person we want to generate an email address for.
+ :return: The email address that was generated.
+ """
+ domain = ["hotmail.com", "gmail.com", "hotmail.fr", "outlook.com", "yahoo.com"]
+ input_name = input_name.replace(" ", ".")
+ return input_name.lower() + "@" + domain[randint(0, len(domain)-1)]
+
+
+def generate_note() -> str:
+ note_list = ["Cool dude!", "I don't trust this guy", "Main developer at Google!", "Nice gal",
+ "Rude person", "BFF"]
+ return note_list[randint(0, len(note_list) - 1)]
+
+
+def main():
+ name_list = ["Ray Allen", "Clarence Boisvert", "Katherina Burpee", "Nevada Dominguez",
+ "Xochitl Olivas", "Rubi Branscome", "Emely Ackley", "Etta Holton", "Pearl Addario",
+ "Kimi Pelosi", "Vernita Pennel", "Reyes Buhl", "Jovan Selle", "Rene Nicks",
+ "Tonia Perrault", "Michel Guzman", "William Sirois", "Carline Whitesell",
+ "Luella Rustin", "Jewell Wakefield", "Sanora Hamdan", "Idalia Hosmer",
+ "Dorethea Wommack", "Joanne Huth", "Wayne Sippel", "Arden Lopinto",
+ "Teena Formica", "Mary Zorn", "Young Gain", "Cayla Pohlmann", "Lea Fogg",
+ "Mack Millhouse", "Lucio Likes", "Meggan Page", "Neda Plasencia", "Anissa Venturi",
+ "Berry Furrow", "Rachell Doss", "Charlott Bledsoe", "Luann Goodman"]
+
+ contact_dictionary = {}
+ for name in name_list:
+ contact = Contact(name)
+ # For each contact, we randomly decide if we add each of the attributes.
+ if randint(0, 1):
+ contact.add_address("Physical", generate_random_address())
+ if randint(0, 1):
+ contact.add_address("Email", generate_email_address(contact.name))
+ if randint(0, 1):
+ contact.add_phone_number("Home", generate_random_phone_number())
+ if randint(0, 1):
+ contact.add_phone_number("Work", generate_random_phone_number())
+ if randint(0, 1):
+ contact.add_phone_number("Personal", generate_random_phone_number())
+ if randint(0, 1):
+ contact.add_note(generate_note())
+
+ contact_dictionary[name] = contact
+
+ # Saving the dictionary on the output pickle.
+ with open("project/contacts_pickle", 'wb') as outfile:
+ pickle.dump(contact_dictionary, outfile)
+ # Loading the dictionary to see if it worked.
+ with open("project/contacts_pickle", 'rb') as infile:
+ test = pickle.load(infile)
+ print(test["Ray Allen"])
+
+
+if __name__ == '__main__':
+ main()
diff --git a/project/src/Phone.ico b/project/src/Phone.ico
new file mode 100644
index 00000000..2b00e61b
Binary files /dev/null and b/project/src/Phone.ico differ
diff --git a/words_pickle b/words_pickle
new file mode 100644
index 00000000..f93cd6ac
Binary files /dev/null and b/words_pickle differ