· 6 years ago · Feb 27, 2020, 10:30 PM
1#!/usr/bin/python3
2
3from tkinter import *
4from tkinter.messagebox import askyesnocancel, showerror, _show as show_msg
5from tkinter.filedialog import askopenfilename
6from tkinter.ttk import Combobox, Scrollbar, Entry, Button, Style
7from io import BytesIO
8import _tkinter
9import functools
10import requests
11import pickle
12import os
13import sys
14
15import googletrans
16import gtts
17
18from EditorFrame import EditorFrame
19from Hotkeys import HKManager
20from utils import yesno2bool, retrycancel2bool, help_, about, contact_me, set_window_icon, play_sound
21
22
23
24# TODO: use another way to select languages
25lang = "ua"
26exec("from lang.%s import *" % lang)
27
28# TODO: fix encoding for the .txt books
29
30class ReadIt(Tk):
31 """
32 The ReadIt main class.
33
34 :param text_filename: the text filename from the command line (optional)
35 :param vocabulary_filename: the vocabulary filename from the command line (optional)
36 :type text_filename: str
37 :type vocabulary_filename: str or none
38 :return: no value
39 :rtype: none
40 """
41
42 def __init__(self, text_filename=None, vocabulary_filename=None, *args, **kwargs):
43 super().__init__(*args, **kwargs)
44 self.text_opened = False # when application is started, the text
45 self.vocabulary_opened = False # and vocabulary are not opened yet
46 # Vocabulary filename is controlled by the EditorFrame, but the text is controlled by the ReadIt
47 # Set the default text filename, when the text was not opened
48 self.text_filename = LANG["non_opened_filename"]
49
50 self.withdraw()
51 self.protocol("WM_DELETE_WINDOW", self.exit) # call the self.exit function when user exits
52
53 self.LANGS_LIST = {value.lower(): key for key, value in
54 googletrans.LANGUAGES.items()
55 } # create a constant for the languages names in English and their ISO 639-1 codes
56
57 self.hk_man = HKManager(self)
58
59 # Create the variables for word and translation
60 self.word_variable = StringVar(self) # create a variable for word to translate
61 self.translation_variable = StringVar(self) # create a variable for translation
62
63 self.vocabulary_editor = EditorFrame(self) # create a frame for the embedded vocabulary editor
64
65 self.style = Style()
66 self.style.configure("Square.TButton", width=2, height=1)
67
68 # Create the menus
69 self.menubar = Menu(self) # create the main menu
70 self.configure(menu=self.menubar) # attach it to the ReadIt window
71
72 self.filemenu = Menu(self.menubar, tearoff=False) # create the "File" menu and add the appropriate entries
73 self.filemenu.add_command(label=LANG["open_text"], command=self.open_text, accelerator="Ctrl+O")
74 self.filemenu.add_separator()
75 self.filemenu.add_command(label=LANG["new_vocabulary"], command=self.vocabulary_editor.new, accelerator="Ctrl+Alt+N")
76 self.filemenu.add_command(label=LANG["open_vocabulary"], command=self.vocabulary_editor.open,
77 accelerator="Ctrl+Alt+O")
78 self.filemenu.add_command(label=LANG["save_vocabulary"], command=self.vocabulary_editor.save,
79 accelerator="Ctrl+Alt+S")
80 self.filemenu.add_command(label=LANG["saveas_vocabulary"], command=self.vocabulary_editor.save_as,
81 accelerator="Ctrl+Alt+Shift+S")
82 self.filemenu.add_separator()
83 self.filemenu.add_command(label=LANG["exit"], command=self.exit, accelerator="Alt+F4")
84 self.menubar.add_cascade(label=LANG["file_menu"], menu=self.filemenu) # attach it to the menubar
85
86 self.bookmarksmenu = Menu(self.menubar,
87 tearoff=False) # create the "Bookmarks" menu and add the appropriate entries
88 self.rem_bookmarksmenu = Menu(self.bookmarksmenu, tearoff=False) # create the "Remove >" embedded menu
89 self.goto_bookmarksmenu = Menu(self.bookmarksmenu, tearoff=False) # create the "Go to >" nested menu
90 self.bookmarksmenu.add_command(label=LANG["bookmarks_add"], command=self.add_bookmark)
91 self.bookmarksmenu.add_cascade(label=LANG["bookmarks_remove"], menu=self.rem_bookmarksmenu) # attach it to the "Bookmarks" menu
92 self.bookmarksmenu.add_command(label=LANG["bookmarks_clear"], command=self.clear_all_bookmarks)
93 self.bookmarksmenu.add_cascade(label=LANG["bookmarks_goto"], menu=self.goto_bookmarksmenu) # attach it to the "Bookmarks" menu
94 self.menubar.add_cascade(label=LANG["bookmarks_menu"], menu=self.bookmarksmenu) # attach it to the menubar
95 # when the program is started, the "Bookmarks" menu is disabled (because no text file was opened)
96 self.menubar.entryconfigure(LANG["bookmarks_menu"], state="disabled")
97
98 self.helpmenu = Menu(self.menubar, tearoff=False) # create the "Help" menu and add the appropriate entries
99 self.helpmenu.add_command(label=LANG["call_help"], command=help_, accelerator="F1")
100 self.helpmenu.add_separator()
101 self.helpmenu.add_command(label=LANG["about_pa"], command=about, accelerator="Ctrl+F1")
102 self.helpmenu.add_command(label=LANG["contact_me"], command=contact_me, accelerator="Ctrl+Shift+F1")
103 self.menubar.add_cascade(menu=self.helpmenu, label=LANG["help_menu"]) # attach it to the menubar
104
105 set_window_icon(self) # set the titlebar icon
106
107 # let the widgets stretch using grid_columnconfigure method
108 self.grid_columnconfigure(0, weight=1)
109
110 self.textbox = Text(self, font="Arial 14", wrap="word") # create the textbox widget,
111 self.textbox.grid(row=0, column=0, rowspan=6, sticky="nswe") # and show it using grid geometry manager
112 self.textbox.bind("<Button-3>",
113 self.select_and_translate) # select and translate the unknown words by right-click
114 self.tb_scrollbar = Scrollbar(self,
115 command=self.textbox.yview) # create a text scrollbar, attach it to the textbox,
116 self.tb_scrollbar.grid(row=0, column=1, rowspan=6, sticky="ns") # and show it using the grid geometry manager
117 self.textbox.configure(yscrollcommand=self.tb_scrollbar.set) # attach the textbox to the scrollbar
118
119 # Create the label with the toolbar caption and show it using grid geometry manager
120 Label(self, text=LANG["translator_panel"]).grid(row=0, column=2, columnspan=3, sticky="nswe")
121 self.src_var = StringVar(self)
122 self.dest_var = StringVar(self)
123 # TODO: check if this is really needed
124 self.src_var.trace("w", lambda *args: self.textbox.focus_force())
125 self.dest_var.trace("w", lambda *args: self.textbox.focus_force())
126 self.src_cbox = Combobox(self, values=["Auto"] + [name.capitalize() for name in self.LANGS_LIST.keys()],
127 state="readonly", textvariable=self.src_var) # create and configure the combobox for the source language
128 self.src_var.set("Auto") # set its default language to "Auto"
129 self.src_cbox.grid(row=2, column=2) # show it using grid geometry manager
130 self.src_cbox.bind("<<ComboboxSelected>>",
131 self.update_replace_btn) # and bind the function, that updates "Replace Langs" button, to it
132 self.replace_btn = Button(self, style="Square.TButton", text="⮂", state="disabled",
133 command=self.replace_langs) # create the "Replace Languages" button,
134 self.replace_btn.grid(row=2, column=3) # and show it using grid geometry manager
135 self.dest_cbox = Combobox(self, values=[name.capitalize() for name in self.LANGS_LIST.keys()],
136 state="readonly", textvariable=self.dest_var) # create the combobox for the final language,
137 self.dest_var.set("Ukrainian") # set its default language to "Ukrainian",
138 self.dest_cbox.grid(row=2, column=4) # and show it using grid geometry manager
139 askword_frame = Frame(self) # create the frame for the word and translation
140 askword_frame.grid(row=3, column=2, columnspan=3, sticky="we") # and show it using grid geometry manager
141 askword_frame.grid_columnconfigure(1, weight=1) # let the widgets stretch by XY inside the askword_frame
142 Label(askword_frame, text=LANG["word"]).grid(row=0, column=0, sticky="w") # create the "Word: " label
143 word_entry = Entry(askword_frame, textvariable=self.word_variable) # create an entry to enter the word
144 word_entry.grid(row=0, column=1, sticky="we") # create the Entry to enter the words
145 word_entry.bind("<Return>", self.translate_word) # it translates words when you press Enter key
146
147 Label(askword_frame, text=LANG["translation"]).grid(row=1, column=0) # create the "Translation: " label
148 self.translate_cbox = Combobox(askword_frame, textvariable=self.translation_variable
149 ) # create to show (and edit before adding into vocabulary) the translation
150 # Create an Entry to show (and modify before adding to the vocabulary) the translations
151 self.translate_cbox.grid(row=1, column=1, sticky="we")
152 # it adds a words' pair to the dict when you press Enter key
153 self.translate_cbox.bind("<Return>",
154 lambda _event=None: self.vocabulary_editor._add_pair((self.word_variable.get(),
155 self.translation_variable.get())))
156
157 controls_frame = Frame(askword_frame)
158 controls_frame.grid(row=0, column=2, rowspan=2)
159
160 # Create the button to translate the word
161 #self.translate_img = PhotoImage(file="images/16x16/translate.png")
162 Button(controls_frame, text="Translate", command=self.translate_word).grid(row=0,
163 column=1) # create the "Translate" button
164 #self.speaker_img = PhotoImage(file="images/16x16/speaker.png") # load the image for the speaker icon
165 # Create two buttons to speak both the word and its translation
166 Button(controls_frame, text="Speak", command=self.speak_word).grid(row=0, column=2)
167 Button(controls_frame, text="Speak", command=self.speak_translation).grid(row=1, column=2)
168
169 # Create the label to show the check mark if the translation is checked by community
170 #self.checked_img = PhotoImage(file="images/16x16/checked.png")
171 self.checked_label = Label(controls_frame, fg="green")
172 self.checked_label.grid(row=1, column=0)
173
174 # Create the button to add the current word to the vocabulary
175 #self.add_img = PhotoImage(file="images/16x16/add.png")
176 Button(controls_frame, text="Add", command=lambda: self.vocabulary_editor._add_pair(
177 (self.word_variable.get(), self.translation_variable.get()))).grid(row=1, column=1)
178 self.grid_rowconfigure(4, weight=1) # configure the 5-th row's widgets stretch
179 self.vocabulary_editor.grid(row=4, column=2, columnspan=3,
180 sticky="nswe") # show the vocabulary editor's frame using grid geometry manager
181 self.vocabulary_editor.set_saved(True) # when you start the program, the vocabulary is not changed
182
183 # create the keys' bindings
184 self.hk_man.add_binding("<Control-O>", self.open_text)
185 self.hk_man.add_binding("<Control-Alt-N>", self.vocabulary_editor.new)
186 self.hk_man.add_binding("<Control-Alt-O>", self.vocabulary_editor.open)
187 self.hk_man.add_binding("<Control-Alt-S>", self.vocabulary_editor.save)
188 self.hk_man.add_binding("<Control-Alt-Shift-S>", self.vocabulary_editor.save_as)
189
190 # Read bookmarks
191 self.bookmarks_data = None # before bookmarks are read, bookmarks_data is None
192 try: # try to
193 with open("bookmarks.dat", "rb") as bdata: # open bookmarks' file (bookmarks.dat)
194 self.bookmarks_data = pickle.load(bdata) # load its contents
195 except FileNotFoundError: # if the bookmarks.dat file doesn't present in the program directory
196 self.bookmarks_data = {} # bookmarks_data is an empty dict, where user can add bookmarks
197 except Exception as details: # if there is another problem,
198 showerror(LANG["error"],
199 LANG["error_load_bookmarks"] + LANG["error_details"] % (details.__class__.__name__, details)) # show the appropriate message
200 self.deiconify() # show the window again
201
202 self.deiconify()
203 # if opening some files using command-line
204 if text_filename: # if the text was specified,
205 self.open_text(text_filename=text_filename) # open it
206 if vocabulary_filename: # if the vocabulary was specified,
207 self.vocabulary_editor.open(vocabulary_filename=vocabulary_filename) # open it
208
209 def open_text(self, _event=None, text_filename=None):
210 """Opens a text.
211
212 :param _event: tkinter event
213 :param text_filename: text filename, when opening from command-line
214 :type _event: tkinter.Event
215 :type text_filename: none or str
216 :return: no value
217 :rtype: none
218 """
219 if self.can_be_closed(): # if the user confirms the text closing,
220 try: # try to
221 if text_filename: # if a text file was specified in the command line,
222 filename = text_filename # set its filename as specified
223 else: # if opening from the ReadIt,
224 filename = askopenfilename() # get the filename
225 if filename: # if user didn't click "Cancel" button or closed the dialog for opening the file
226 with open(filename, "r") as file: # open the file for reading,
227 self.textbox.delete("1.0", "end") # clear the textbox (won't work if couldn't open the file),
228 self.textbox.insert("1.0", file.read()) # and insert the data from the new file
229 self.text_filename = filename # update the text_filename attribute,
230 self.text_opened = True # text_opened attribute,
231 self.update_title() # and the title
232 if self.bookmarks_data is not None: # if the bookmarks.dat was opened without errors),
233 self.menubar.entryconfig(LANG["bookmarks_menu"], state="normal") # enable "Bookmarks menu"
234 self.update_bookmarks_menu() # updates the bookmarks menu entries as in the opened file
235 except UnicodeDecodeError as details: # if the file has unsupported encoding,
236 showerror(LANG["error"],
237 LANG["error_text_encoding"] + LANG["error_details"] % (details.__class__.__name__, details))
238 except Exception as details: # if there is an error occurred,
239 showerror(LANG["error"], LANG["error_unexpected_opening_file"] + LANG["error_details"] % (
240 details.__class__.__name__, details)) # show the appropriate message
241
242 def translate_word(self, _event=None):
243 """Translates the word or phrase when the user enters something and press Enter key.
244
245 :param _event: tkinter event
246 :type _event: tkinter.Event
247 :return: no value
248 :rtype: none
249 """
250
251 origin = self.word_variable.get().strip() # get the origin word and remove whitespace characters from ends
252 if origin: # if something is entered,
253 try: # try to
254 src = self.src_cbox.get().lower() # get the source language
255 if src != "auto": # if it is not "auto",
256 src = self.LANGS_LIST[src] # get the source language ISO 639-1 representation
257 dest = self.LANGS_LIST[self.dest_cbox.get().lower()] # get the final language
258 result = googletrans.Translator().translate(origin, dest,
259 src) # translate using the Google Translator API
260 self.translation_variable.set(result.text) # update the translation variable with translation
261 #self.checked_label.configure(image=self.checked_img if result.extra_data["translation"][0][4] else "")
262 self.checked_label.configure(text="\/" if result.extra_data["translation"][0][4] else "")
263 new_translations_list = []
264 if result.extra_data["all-translations"]:
265 for group in result.extra_data["all-translations"]:
266 for translation in group[1]:
267 new_translations_list.append(translation)
268 if result.text not in new_translations_list:
269 new_translations_list.insert(0, result.text)
270 else:
271 new_translations_list.append(result.text)
272 self.translate_cbox.configure(values=new_translations_list)
273 except requests.exceptions.ConnectionError as details:
274 showerror(LANG["error"], LANG["error_translate_internet_connection_problems"] + LANG["error_details"] % (
275 details.__class__.__name__, details))
276 except Exception as details: # if something went wrong,
277 showerror(LANG["error"],
278 LANG["error_translate_unexpected"] + LANG["error_details"] % (
279 details.__class__.__name__, details))
280 else: # if nothing was entered,
281 self.word_variable.set("") # clear the word variable (something like " " was entered -> set to "")
282 self.translation_variable.set("") # clear the translation variable
283 self.translate_cbox.configure(value=()) # remove the translations list
284 self.checked_label.configure(text="") # remove the checked mark if it was set before
285
286 def replace_langs(self):
287 """Replaces the languages.
288
289 :return: no value
290 :rtype: none
291 """
292 src = self.src_cbox.get() # get the source language,
293 dest = self.dest_cbox.get() # get the final language,
294 self.src_cbox.set(dest) # set the source language combobox value to final language,
295 self.dest_cbox.set(src) # set the final language combobox value to source language,
296 self.word_variable.set(
297 self.translation_variable.get()) # set the word_variable to translation (translation will be done again)
298 self.translate_word() # translates the word from the word_variable using Google Translator API
299
300 def update_replace_btn(self, _event=None):
301 """Updates the replace button state.
302
303 :return: no value
304 :rtype: none
305 """
306 if self.src_cbox.get() == "Auto": # when the source language is "Auto",
307 self.replace_btn.configure(state="disabled") # it is disabled,
308 else: # when it's not,
309 self.replace_btn.configure(state="normal") # it is enabled
310
311 def _speak(self, text, lang):
312 if text.strip():
313 try:
314 tmp_file = BytesIO()
315 gtts.gTTS(text=text, lang=lang).write_to_fp(tmp_file)
316 tmp_file.seek(0)
317 play_sound(tmp_file)
318 except ValueError as details:
319 showerror(LANG["error"],
320 LANG["error_speak_language_not_supported"] % googletrans.LANGUAGES[str(details).split(": ")[-1]].capitalize())
321 except requests.exceptions.ConnectionError as details:
322 showerror(LANG["error"],
323 LANG["error_translate_internet_connection_problems"] + LANG["error_details"] % (
324 details.__class__, details))
325 except Exception as details:
326 showerror(LANG["error"], LANG["error_translate_unexpected"] + LANG["error_details"] % (details.__class__.__name__, details))
327
328 def speak_word(self):
329 src = self.src_cbox.get().lower() # get the source language
330 if src == "auto": # if it is not "auto",
331 try:
332 src = googletrans.Translator().detect(self.word_variable.get()).lang
333 except requests.exceptions.ConnectionError as details:
334 showerror(LANG["error"], LANG["error_speak_internet_connection_problems"] +
335 (LANG["error_details"] % (details.__class__.__name__, details)))
336 except Exception as details:
337 showerror(LANG["error"], LANG["error_speak_unexpected"] + LANG["error_details"] %
338 (details.__class__.__name__, details))
339 else:
340 src = self.LANGS_LIST[src] # get the source language ISO 639-1 representation
341 self._speak(self.word_variable.get(), src)
342
343 def speak_translation(self):
344 dest = self.LANGS_LIST[self.dest_cbox.get().lower()] # get the source language
345 self._speak(self.translation_variable.get(), dest)
346
347 def select_and_translate(self, event):
348 """
349 Translate the word (or phrase) from the text on right click.
350
351 :param event: tkinter Event
352 :type event: tkinter.Event
353 :return: no value
354 :rtype: none
355 """
356 try: # if there is something selected (for phrases mostly),
357 selected_text = self.textbox.get(SEL_FIRST, SEL_LAST).strip()
358 start, end = (SEL_FIRST, SEL_LAST)
359 except _tkinter.TclError: # if nothing is selected (words only; by right-click)
360 start = self.textbox.index('@%s,%s wordstart' % (event.x, event.y)) # get the start position of the word,
361 end = self.textbox.index('@%s,%s wordend' % (event.x, event.y)) # and the end position of the word,
362 self.textbox.tag_add("sel", start, end) # select the right-clicked word,
363 self.textbox.update() # update the textbox to see the selection (disappears after the word is translated)
364 selected_text = self.textbox.get(start, end).strip() # get the selected text and remove whitespaces at the ends
365 finally:
366 if selected_text:
367 self.word_variable.set(selected_text) # enter to the translation field
368 self.translate_word() # translate the word
369 self.textbox.tag_remove("sel", start, end) # deselect the selection
370
371 def update_title(self):
372 """Updates the title when the text filename or vocabulary filename is changed.
373
374 :return: no value
375 :rtype: none
376 """
377 self.title("%s - %s - PolyglotAssistant 1.00 ReadIt" % (
378 self.text_filename,
379 self.vocabulary_editor.unsaved_prefix + self.vocabulary_editor.filename)) # format the title
380
381 def add_bookmark(self):
382 """
383 Adds a bookmark so the user cannot lose the text position.
384
385 :return: no value
386 :rtype: none
387 """
388 if not (self.text_filename in self.bookmarks_data): # if the bookmarks for this file were not added before
389 self.bookmarks_data[self.text_filename] = [] # create the bookmarks' list for this file
390 self.bookmarks_data[self.text_filename].append(self.textbox.yview()[0]) # add text Y-scroll coords
391 self.update_bookmarks_menu() # update the menu entries accroding to the updated bookmarks' list
392 self.update_bookmarks_file() # update the bookmarks.dat file
393
394 def remove_bookmark(self, bookmark):
395 """
396 Removes selected bookmark.
397
398 :param bookmark: the Y-scroll value (0.0-1.0) of the textbox to remove.
399 :return: no value
400 :rtype: none
401 """
402 if yesno2bool(show_msg(LANG["warning"], LANG["warning_remove_bookmark"], "warning",
403 "yesno")): # ask warning about bookmark removal
404 self.bookmarks_data[self.text_filename].remove(bookmark) # if the users says "Yes", remove it
405 self.update_bookmarks_menu() # update bookmarks_menu
406 self.update_bookmarks_file() # update the bookmarks.dat file
407
408 def goto_bookmark(self, bookmark):
409 """
410 Goes to a selected bookmark.
411
412 :param bookmark: the Y-scroll value (0.0-1.0) of the texbox to go to.
413 :type bookmark: float
414 """
415 self.textbox.yview_moveto(bookmark) # set the textbox y position to the selected bookmark record
416
417 def clear_all_bookmarks(self):
418 """
419 Clears the bookmarks' list after user's confirmation.
420
421 :return: no value
422 :rtype: none
423 """
424 if yesno2bool(show_msg(LANG["warning"], LANG["warning_clear_bookmarks_list"], "warning",
425 "yesno")): # if the user confirms the clearing
426 del self.bookmarks_data[self.text_filename] # remove all the bookmarks for the current filename
427 self.update_bookmarks_menu() # update bookmarks' menu entries
428
429 def disable_bookmarks_lst(self):
430 """
431 Disables the bookmarks' menu submenus "Remove >" and "Go to >".
432 It's useful when there aren't any bookmarks attached to the opened file.
433
434 :return: no value
435 :rtype: none
436 """
437 self.bookmarksmenu.entryconfigure(LANG["bookmarks_remove"], state="disabled") # disable "Remove >" submenu
438 self.bookmarksmenu.entryconfigure(LANG["bookmarks_goto"], state="disabled") # disable "Go to >" submenu
439
440 def enable_bookmarks_lst(self):
441 """
442 Enables the bookmarks' menu submenus "Remove >" and "Go to >"
443 It's used to enable the "Bookmarks" menu when there are some bookmarks attached to the opened file.
444
445 :return: no value
446 :rtype: none
447 """
448 self.bookmarksmenu.entryconfigure(LANG["bookmarks_remove"], state="normal") # enable "Remove >" submenu
449 self.bookmarksmenu.entryconfigure(LANG["bookmarks_goto"], state="normal") # enbale "Go to >" submenu
450
451 def update_bookmarks_menu(self):
452 """
453 Updates bookmarks' menu entries after changing bookmarks' list content.
454
455 :return: no value
456 :rtype: none
457 """
458 if self.text_filename in self.bookmarks_data: # if filename is in the bookmarks' list
459 if self.bookmarks_data[self.text_filename]: # if any bookmarks for this file are created
460 self.enable_bookmarks_lst() # enable bookmarks "Remove >" and "Go to >" submenus
461 self.rem_bookmarksmenu.delete(0, "end") # clear all the "Remove >" submenu
462 self.goto_bookmarksmenu.delete(0, "end") # clear all the "Go to >" submenu
463 for no, bookmark in enumerate(self.bookmarks_data[self.text_filename]
464 ): # create the menu entries for the new bookmarks {1; 2; 3...}
465 self.rem_bookmarksmenu.add_command(label=no + 1, command=functools.partial(self.remove_bookmark,
466 bookmark)
467 ) # create the menu entry in the "Remove >" submenu
468 self.goto_bookmarksmenu.add_command(label=no + 1, command=functools.partial(self.goto_bookmark,
469 bookmark)
470 ) # and in the "Go to >" submenu
471 else: # if there weren't any bookmarks created for this file yet,
472 self.disable_bookmarks_lst() # disable the "Remove >" and "Go to >" submenus
473 else: # if the filename was not even in the bookmarks' list
474 self.disable_bookmarks_lst() # disable the "Remove >" and "Go to >" submenus
475
476 def update_bookmarks_file(self):
477 """
478 Updates the "bookmarks.dat" file contents after changing bookmarks' list content.
479
480 :return: no value
481 :rtype: none
482 """
483 try: # try to
484 with open("bookmarks.dat", "wb") as file: # open bookmarks.dat for writing
485 pickle.dump(self.bookmarks_data, file) # update it with new bookmarks
486 except Exception as details: # if an error occurred, show an appropriate warning and ask to retry
487 while retrycancel2bool(show_msg(LANG["error"], LANG["error_add_bookmark"] +
488 LANG["error_details"] % (details.__class__.__name__,
489 details), icon="error",
490 type="retrycancel")): # while user allows to retry,
491 try: # try to
492 with open("bookmarks.dat", "wb") as file: # open the bookmarks.dat file,
493 pickle.dump(self.bookmarks_data, file) # dump the updated bookmarks list there
494 except Exception as new_details: # if something went wrong agin,
495 details = new_details # update the error details
496 else: # if all is OK,
497 break # there is no reason to retry again
498
499 def can_be_closed(self):
500 """
501 Asks the user to add bookmarks before closing the text file (when exits program, or opens a file)
502
503 :return: no value
504 :rtype: none
505 """
506 if self.text_opened and self.bookmarks_data is not None: # if text was opened and bookmarks were not disabled
507 result = askyesnocancel(LANG["bookmarks_add"],
508 LANG["warning_add_bookmark_before_continue"]) # ask user to add a bookmark
509 if result: # if he/she clicks "Yes",
510 self.add_bookmark() # add a new bookmark,
511 # and then return True
512 elif result is None: # if he clicks "Cancel",
513 return False # do not close the window or open a new text
514 return True # if "Yes" and bookmark were added or "No"
515
516 def exit(self):
517 """
518 Asks to save a bookmark and the vocabulary, and exits then.
519
520 :return: no value
521 :rtype: none
522 """
523 if self.can_be_closed() and self.vocabulary_editor.can_be_closed(): # if bookmarks added and vocabulary saved,
524 self.destroy() # destroy the ReadIt window
525
526
527def show_usage():
528 """
529 Shows the command-line usage, if called.
530
531 :return: no value
532 :rtype: none
533 """
534 Tk().withdraw() # create and hide a Tk() window (to avoid the blank window appearance on the screen)
535 showerror(LANG["error"], LANG["error_commandline_args"]) # show the command-line usage
536 os._exit(0) # terminate the application process
537
538# TODO: fix titles - opened files must have slashes accroding to the OS (Windows - \, Linux&OSX - /)
539# TODO: check sys.argv parsing
540if __name__ == "__main__":
541 # Parses the sys.argv (command-line arguments)
542 files = list(map(lambda s: s.replace("\\", "/"), sys.argv[1:])) # get the command-line arguments (probably files)
543 if len(files) > 0: # if any files specified,
544 text_filename = None # by default there is no text opened
545 vocabulary_filename = None # by default there is no vocabulary opened
546 if len(files) == 1: # if only one file specified,
547 ext = os.path.splitext(files[-1])[-1] # get its extension
548 if ext == ".pav": # if it is ".pav",
549 vocabulary_filename = files[-1] # this is a vocabulary,
550 else: # otherwise,
551 text_filename = files[-1] # this is a text file
552 elif len(files) == 2: # if two files specified,
553 fexts = {os.path.splitext(fname)[-1]: fname for fname in files} # get files' extensions
554 if ".pav" in fexts and len(
555 fexts) > 1: # if ".pav" is in extensions, and the second file has another extension
556 vocabulary_filename = fexts.pop(".pav") # the ".pav"-ending filename belongs to a vocabulary,
557 text_filename = list(fexts.values())[-1] # the other one belongs to a text file
558 else: # if there is no vocabulary (i.e. two texts),
559 show_usage() # show the command-line usage
560 else: # if more than two files specified,
561 show_usage() # show the command-line usage
562 ReadIt(text_filename, vocabulary_filename).mainloop() # open the specified files
563 else: # if no files passed in command line,
564 ReadIt().mainloop() # create the ReadIt window and start its mainloop.