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