From f8f7832dd7a45da17efbbd79bb042d3d968882c3 Mon Sep 17 00:00:00 2001 From: EndMove Date: Mon, 5 Sep 2022 23:13:38 +0200 Subject: [PATCH] Major bug fixes, optimization + major advance --- assets/logo.ico | Bin 0 -> 270398 bytes autopy-conf.json | 8 +++ controller/Frames.py | 4 +- controller/HomeController.py | 90 ++++++++++--------------- controller/InfoController.py | 21 ++++-- controller/MainController.py | 11 ++-- main.py | 41 ++++++++++-- model/WebPicDownloader.py | 123 ++++++++++++++++++++++++++--------- view/HomeView.py | 61 +++++++++++------ view/InfoView.py | 96 +++++++++++++++++++-------- view/MainWindow.py | 28 +++++--- 11 files changed, 322 insertions(+), 161 deletions(-) create mode 100644 assets/logo.ico diff --git a/assets/logo.ico b/assets/logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..ca34922c634e02f67a07c7568623dfb82ad868d2 GIT binary patch literal 270398 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 MVg_OcT0aB-56Is=b^rhX literal 0 HcmV?d00001 diff --git a/autopy-conf.json b/autopy-conf.json index 740eed0..b8ba9be 100644 --- a/autopy-conf.json +++ b/autopy-conf.json @@ -17,6 +17,10 @@ "optionDest": "console", "value": false }, + { + "optionDest": "icon_file", + "value": "C:/Users/super/Documents/Developpement/Python/WebPicDownloader/assets/logo.ico" + }, { "optionDest": "name", "value": "WebPicDownloader_v1.0.0" @@ -68,6 +72,10 @@ { "optionDest": "argv_emulation", "value": false + }, + { + "optionDest": "datas", + "value": "C:/Users/super/Documents/Developpement/Python/WebPicDownloader/assets;assets/" } ], "nonPyinstallerOptions": { diff --git a/controller/Frames.py b/controller/Frames.py index 96a6856..5428027 100644 --- a/controller/Frames.py +++ b/controller/Frames.py @@ -14,5 +14,5 @@ class Frames(Enum): @version 1.0.0 @since 2022-08-30 """ - Home = 1 # Home view - Info = 2 # Info & copyright view + HOME = 1 # Home view + INFO = 2 # Info & copyright view diff --git a/controller/HomeController.py b/controller/HomeController.py index f7b0456..99b594b 100644 --- a/controller/HomeController.py +++ b/controller/HomeController.py @@ -1,14 +1,12 @@ -import time from controller.MainController import MainController -from model.WebPicDownloader import WebPicDownloader -from util.AsyncTask import AsyncTask +from model.WebPicDownloader import MessageType, WebPicDownloader class HomeController: """ Controller - HomeController - desc... + This controller handles all the interaction directly related to the download. @author Jérémi Nihart / EndMove @link https://git.endmove.eu/EndMove/WebPicDownloader @@ -16,13 +14,12 @@ class HomeController: @since 2022-08-30 """ # Variables - __main_controller = None + __main_controller: MainController = None __view = None __webpic: WebPicDownloader = None - __download_task = None # Constructor - def __init__(self, controller: MainController, webpic) -> None: + def __init__(self, controller: MainController, webpic: WebPicDownloader) -> None: """ Constructor @@ -33,6 +30,11 @@ class HomeController: self.__main_controller = controller self.__webpic = webpic + # setup webpic event + webpic.set_messenger_callback(self.on_webpic_messenger) + webpic.set_success_callback(self.on_webpic_success) + webpic.set_failure_callback(self.on_webpic_failure) + # Subscribe to events controller.subscribe_to_quite_event(self.on_quit) @@ -45,21 +47,9 @@ class HomeController: * :view: -> The view that this controller manage. """ self.__view = view - self.__webpic.set_messenger_callback(self.on_webpic_messenger) - self.__webpic.set_success_callback(self.on_webpic_success) - self.__webpic.set_failure_callback(self.on_webpic_failure) # END View method # START View events - def on_change_view(self, frame) -> None: - """ - [event function for view] - => Call this event method when the user requests to change the window. - - * :frame: -> The frame we want to launch. - """ - self.__main_controller.change_frame(frame) - def on_download_requested(self, url: str, name: str) -> None: """ [event function for view] @@ -69,26 +59,45 @@ class HomeController: * :name: -> The name of the folder in which put pictures. """ if url.strip() and name.strip(): + self.__view.set_interface_state(True) + self.__view.clear_logs() self.__webpic.start_downloading(url, name) else: self.__view.show_error_message("Opss, the url or folder name are not valid!") # END View events # START Webpic events - def on_webpic_messenger(self, message: str) -> None: + def on_webpic_messenger(self, message: str, type) -> None: """ + [event function for webpic] + => This event is called to communicate a message. + + * :message: -> Message that webpic send to the controller. + * :type: -> Type of message that webpic send to the controller. """ - self.__view.add_log(message) + match type: + case MessageType.LOG: + self.__view.add_log(message) + case MessageType.ERROR: + self.__view.show_error_message(message) + case MessageType.SUCCESS: + self.__view.show_success_message(message) def on_webpic_success(self) -> None: """ + [event function for webpic] + => This event is called to indicate that the download has finished successfully. """ self.__view.show_success_message("The download has been successfully completed.") + self.__view.set_interface_state(False) def on_webpic_failure(self) -> None: """ + [event function for webpic] + => This event is called to indicate that there was a problem during the download. """ self.__view.show_error_message("A critical error preventing the download occurred, check the logs.") + self.__view.set_interface_state(False) # END Webpic events # START Controller methods @@ -97,44 +106,13 @@ class HomeController: [event function for controller] => Call this event when a request to exit is thrown. """ - if self.__webpic.is_alive(): + if self.__webpic.is_download_running(): if self.__main_controller.show_question_dialog( "Are you sure?", "Do you really want to quit while the download is running?\nThis will stop the download." ): - self.__webpic.stop_downloading() - time.sleep(4) + self.__webpic.stop_downloading() # hot stop deamon return False - else: - return True - print("Quit... homecontroller END") # REMOVE + return True + self.__webpic.stop_downloading(block=True) # END Controller methods - - # START Task methods - def __async_task_start(self, url, name) -> None: - """ - [CallBack start function] - => Start Callback function for asynctask, be careful once executed in asynctask this - function will keep its controller context. In short it's as if the thread was - launched in the controller and the execution never left it. - - * :url: -> Url for webpic. - * :name: -> Working dir name for webpic. - """ - print("start callback called") # REMOVE - self.__view.clear_logs() - if self.__webpic.download(url, name): - self.__view.show_success_message("The download has been successfully completed.") - else: - self.__view.show_error_message("A critical error preventing the download occurred, check the logs.") - - def __async_task_stop(self) -> None: - """ - [CallBack stop function] - => End Callback function for asynctask, be careful once executed in asynctask this - function will keep its controller context. In short it's as if the thread was - launched in the controller and the execution never left it. - """ - print("stop callback called") # REMOVE - self.__webpic.stop() - # END Task methods \ No newline at end of file diff --git a/controller/InfoController.py b/controller/InfoController.py index e29b50c..d4681f0 100644 --- a/controller/InfoController.py +++ b/controller/InfoController.py @@ -1,3 +1,4 @@ +from controller.Frames import Frames from controller.MainController import MainController @@ -5,7 +6,7 @@ class InfoController: """ Controller - InfoController - desc... + This controller manages the display of information in the information view. @author Jérémi Nihart / EndMove @link https://git.endmove.eu/EndMove/WebPicDownloader @@ -13,11 +14,17 @@ class InfoController: @since 2022-08-30 """ # Variables - __main_controller = None + __main_controller: MainController = None __view = None # Constructor def __init__(self, controller: MainController) -> None: + """ + Constructor + + * :controller: -> The main application cpntroller. + """ + # Setup variables self.__main_controller = controller # START View methods @@ -28,14 +35,20 @@ class InfoController: :view: -> The view that this controller manage. """ self.__view = view + self.__view.set_title(self.__main_controller.get_config('about_title')) + self.__view.set_content(self.__main_controller.get_config('about_content')) + self.__view.set_version( + f"version: {self.__main_controller.get_config('app_version')} - {self.__main_controller.get_config('app_version_date')}" + ) # END View method # START View events - def on_change_view(self, frame) -> None: + def on_change_view(self, frame: Frames) -> None: """ [event function for view] + => Call this event method when the user requests to change the window. - :frame: -> The frame we want to launch. + * :frame: -> The frame we want to launch. """ self.__main_controller.change_frame(frame) # END View events diff --git a/controller/MainController.py b/controller/MainController.py index a0f7503..9ae2433 100644 --- a/controller/MainController.py +++ b/controller/MainController.py @@ -1,11 +1,13 @@ import os +from controller.Frames import Frames class MainController: """ Controller - MainController - TODO desc... + This controller manages all the main interaction, change of windows, + dialogs, stop... It is the main controller. @author Jérémi Nihart / EndMove @link https://git.endmove.eu/EndMove/WebPicDownloader @@ -44,7 +46,7 @@ class MainController: [event function for view] => Event launch when you ask to open the current folder. """ - os.startfile(self.get_config('app_folder')) + os.startfile(self.get_config('app_folder')) # Open the file explorer on working dir def on_quite(self) -> None: """ @@ -70,12 +72,11 @@ class MainController: [event function for view] => Event launched when a request for more information arise. """ - # TODO on_about - print("on_about") + self.change_frame(Frames.INFO) # END View methods # START Controller methods - def change_frame(self, frame) -> None: + def change_frame(self, frame: Frames) -> None: """ [function for controller] => Allows you to request a frame change in the main window. diff --git a/main.py b/main.py index 6043a7d..6617ef0 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ import os +import sys from controller.HomeController import HomeController from controller.InfoController import InfoController from controller.MainController import MainController @@ -9,11 +10,38 @@ from view.InfoView import InfoView from view.MainWindow import MainWindow +def get_sys_directory() -> str: + """ + Recover the path of the application's resources. + """ + try: + directory = sys._MEIPASS + except Exception: + directory = os.getcwd() + return directory + def get_config() -> dict: + """ + Retrieve the application configuration + """ return { - 'app_name': "WebPicDownloader", + 'app_name': 'WebPicDownloader', 'app_folder': os.getcwd(), - 'app_version': "1.0.0" # This version must match with the version.txt at root + 'app_version': '1.0.0', # This version must match with the version.txt at root + 'app_version_date': '2022-09-05', + + 'sys_directory': get_sys_directory(), + + 'about_title': 'About WebPicDownloader', + 'about_content': +"""This scraping software has been developed by EndMove +and is fully open-source. The source code is available +here: https://git.endmove.eu/EndMove/WebPicDownloader +EndMove is available at the following address for any +request contact@endmove.eu. In case of problemsplease +open an issue on the repository. + +The logo of the software was made by Gashila""" } if __name__ == '__main__': @@ -30,7 +58,7 @@ if __name__ == '__main__': config = get_config() # Create utli/model - webpic = WebPicDownloader(path=config.get('app_folder'), asynchrone=True) + webpic = WebPicDownloader(path=config.get('app_folder')) # Create app controllers main_controller = MainController(config) @@ -43,11 +71,12 @@ if __name__ == '__main__': info_controller = InfoView(main_window, info_controller) # Add views to main window - main_window.add_view(Frames.Home, home_view) - main_window.add_view(Frames.Info, info_controller) + main_window.add_view(Frames.HOME, home_view) + main_window.add_view(Frames.INFO, info_controller) # Choose the launching view - main_window.show_frame(Frames.Home) + main_window.show_frame(Frames.HOME) # Start main windows looping (launch program) + main_window.mainloop() diff --git a/model/WebPicDownloader.py b/model/WebPicDownloader.py index 77cc698..6dd25d8 100644 --- a/model/WebPicDownloader.py +++ b/model/WebPicDownloader.py @@ -1,10 +1,31 @@ import os +from enum import Enum from threading import Semaphore, Thread from urllib import request -from urllib.error import HTTPError, URLError from bs4 import BeautifulSoup, Tag, ResultSet +class MessageType(Enum): + """ + MessageType + + Is an enumeration to define the different types of messages sent by the webpic messenger. + + There are 3 types of messages. + - log -> log + - error -> err + - success -> suc + + @author Jérémi Nihart / EndMove + @link https://git.endmove.eu/EndMove/WebPicDownloader + @version 1.0.0 + @since 2022-09-05 + """ + LOG = 'log' + ERROR = 'err' + SUCCESS = 'suc' + + class WebPicDownloader(Thread): """ WebPicDownloader @@ -12,19 +33,18 @@ class WebPicDownloader(Thread): Webpicdownloader is a scraping tool that allows you to browse a web page, find the images and download them. This tool is easily usable and implementable in an application. It has been designed to be executed in an integrated thread - in an asynchronous way as well as more classically in a synchronous way. This - tool allows to define 3 callback functions, one for events, one in case of - success and one in case of failure. It also has an integrated entry point - allowing it to be directly executed in terminal mode. + in an asynchronous way. This tool allows to define 3 callback functions, one for + events, one in case of success and one in case of failure. It also has an + integrated entry point allowing it to be directly executed in terminal mode. @author EndMove - @version 1.2.0 + @version 1.2.1 """ # Variables __callbacks: dict = None # Callback dictionary - __settings: dict = None # - __dl_infos: dict = None # - __sem: Semaphore = None # + __settings: dict = None # Webpic basics settings + __dl_infos: dict = None # Download informations + __sem: Semaphore = None # Semaphore for the webpic worker _exit: bool = None # When set to True quit the thread @@ -39,7 +59,6 @@ class WebPicDownloader(Thread): * :path: -> Folder in which the tool will create the download folders and place the images. * :headers: -> Dictionary allowing to define the different parameters present in the header of the requests sent by WebPic. - * :asynchronous: -> True: launch the download in a thread, False: the opposite. * :messenger: -> Callback function messenger (see setter). * :success: -> Callback function success (see setter). * :failure: -> Callback function failure (see setter). @@ -60,8 +79,6 @@ class WebPicDownloader(Thread): 'website_url': 'url', 'download_name': 'name', 'download_path': 'full_path', - 'tot_image_count': 0, - 'dl_image_count': 0, 'running': False } self.__sem = Semaphore(0) @@ -69,6 +86,7 @@ class WebPicDownloader(Thread): self.start() # start deamon + # Internal functions def __get_html(self, url: str) -> str: """ @@ -82,6 +100,7 @@ class WebPicDownloader(Thread): response = request.urlopen(req) return response.read().decode('utf-8') + def __find_all_img(self, html: str) -> ResultSet: """ Internal Function #do-not-use# @@ -93,6 +112,7 @@ class WebPicDownloader(Thread): soup = BeautifulSoup(html, 'html.parser') return soup.find_all('img') + def __find_img_link(self, img: Tag) -> str: """ Internal Function #do-not-use# @@ -115,6 +135,7 @@ class WebPicDownloader(Thread): raise ValueError("Bad image url") return link + def __find_image_type(self, img_link: str) -> str: """ Internal Function #do-not-use# @@ -128,6 +149,7 @@ class WebPicDownloader(Thread): type = type.split('?')[0] return type + def __download_img(self, url: str, filename: str) -> None: """ Internal Function #do-not-use# @@ -141,6 +163,7 @@ class WebPicDownloader(Thread): with open(filename, 'wb') as img: img.write(raw_img) + def __initialize_folder(self, folder_path: str) -> None: """ Internal Function #do-not-use# @@ -154,12 +177,17 @@ class WebPicDownloader(Thread): else: raise ValueError("The folder already exists, it may already contain images") - def __msg(self, message: str) -> None: + + def __msg(self, message: str, type:MessageType=MessageType.LOG) -> None: """ Internal Function #do-not-use# => Use the messenger callback to send a message. + + * :message: -> the message to send through callback + * :type: -> message type, can be ['log', 'err', 'suc'] """ - self.__callbacks.get('messenger')(message) + self.__callbacks.get('messenger')(message, type) + # Public functions def set_success_callback(self, callback) -> None: @@ -170,6 +198,7 @@ class WebPicDownloader(Thread): """ self.__callbacks['success'] = callback + def set_failure_callback(self, callback) -> None: """ Setter to define the callback function called when the download fails. @@ -178,6 +207,7 @@ class WebPicDownloader(Thread): """ self.__callbacks['failure'] = callback + def set_messenger_callback(self, callback) -> None: """ Setter to define the callback function called when new messages arrive. @@ -186,74 +216,103 @@ class WebPicDownloader(Thread): """ self.__callbacks['messenger'] = callback + def start_downloading(self, url: str, name: str) -> None: """ - TODO desc + Start downloading all pictures of a website. + + * :url: -> The url of the website to annalyse. + * :folder_name: -> The name of the folder in which to upload the photos. """ - if self.__dl_infos.get('running'): - print("bussy") + if not self.is_alive: + self.__msg("Opss, the download thread is not running, please restart webpic.", MessageType.ERROR) + elif self.__dl_infos.get('running'): + self.__msg("Opss, the download thread is busy.", MessageType.ERROR) else: self.__dl_infos['website_url'] = url self.__dl_infos['download_name'] = name self.__sem.release() + def stop_downloading(self, block=False) -> None: """ - TODO DESC + Stops the download after the current item is processed and exit the downloading thread. + + Attention once called it will not be possible any more to download. + + * :block: -> If true, the function will block until the worker has finished working, if + False(default value), the stop message will be thrown and the program will continue. """ self.__exit = True self.__sem.release() if block: self.join() + + def is_download_running(self) -> bool: + """ + Indique si un téléchargement est en cours + + * RETURN -> True if yes, False else. + """ + return self.__dl_infos['running']; + + # Thread corp function def run(self) -> None: while True: - self.__sem.acquire() + self.__sem.acquire() # waiting the authorization to process - if self.__exit: + if self.__exit: # check if the exiting is requested return - self.__dl_infos['running'] = True # reserv run + self.__dl_infos['running'] = True # indicate that the thread is busy + try: + # parse infos from url html = self.__get_html(self.__dl_infos.get('website_url')) # website html images = self.__find_all_img(html) # find all img balises ing html - self.__dl_infos['tot_image_count'] = len(images) # count total image - self.__dl_infos['dl_image_count'] = 0 # set download count to 0 + # setting up download informaations + tot_count = len(images) # count total image + dl_count = 0 # set download count to 0 self.__dl_infos['download_path'] = f"{self.__settings.get('root_path')}/{self.__dl_infos.get('download_name')}/" # format path + # init working directory self.__initialize_folder(self.__dl_infos.get('download_path')) # Init download folder - self.__msg(f"WebPicDownloader found {self.__dl_infos.get('tot_image_count')} images on the website.") + self.__msg(f"WebPicDownloader found {tot_count} images on the website.") - # process pictures + # start images processing for i, img in enumerate(images): try: self.__msg(f"Start downloading image {i}.") + img_link = self.__find_img_link(img) # find image link self.__download_img(img_link, f"{self.__dl_infos.get('download_path')}image-{i}.{self.__find_image_type(img_link)}") # download the image + self.__msg(f"Download of image {i}, done!") - self.__dl_infos['dl_image_count'] += 1 # increment download counter + dl_count += 1 # increment download counter except Exception as err: self.__msg(f"ERROR: Unable to process image {i} -> err[{err}].") - self.__msg(f"WebPicDownloader has processed {self.__dl_infos.get('dl_image_count')} images out of {self.__dl_infos.get('tot_image_count')}.") + # end images processing + + self.__msg(f"WebPicDownloader has processed {dl_count} images out of {tot_count}.") self.__callbacks.get('success')() # success, launch callback except Exception as err: self.__msg(f"ERROR: An error occured -> err[{err}]") self.__callbacks.get('failure')() # error, launch callback - self.__dl_infos['running'] = False # free run + + self.__dl_infos['running'] = False # inficate that the thread is free + if __name__ == "__main__": # Internal entry point for testing and consol use. wpd = WebPicDownloader() - def lol(msg): - pass - wpd.set_messenger_callback(lol) while True: url = input("Website URL ? ") name = input("Folder name ? ") wpd.start_downloading(url, name) if "n" == input("Do you want to continue [Y/n] ? ").lower(): - wpd.stop_downloading() break + wpd.stop_downloading(block=True) print("Good bye !") \ No newline at end of file diff --git a/view/HomeView.py b/view/HomeView.py index 5bb25ca..24dda9c 100644 --- a/view/HomeView.py +++ b/view/HomeView.py @@ -3,23 +3,26 @@ import tkinter.font as tfont from tkinter import ttk from tkinter import scrolledtext as tst from controller.HomeController import HomeController +from view.MainWindow import MainWindow + class HomeView(ttk.Frame): """ - View - MainWindow + View - HomeWindow - dec... + This view allows you to start the scraping/downloading process, + as well as to display the progress of the process. @author Jérémi Nihart / EndMove @link https://git.endmove.eu/EndMove/WebPicDownloader - @version 1.0.0 - @since 2022-08-30 + @version 1.0.1 + @since 2022-09-05 """ # Variables __controller: HomeController = None # Constructor - def __init__(self, parent, controller: HomeController): + def __init__(self, parent: MainWindow, controller: HomeController): """ Constructor @@ -27,15 +30,15 @@ class HomeView(ttk.Frame): * :controller: -> The view controller """ super().__init__(parent) - - # Save and setup main controller - self.__controller = controller - controller.set_view(self) # Init view self.__init_content() + + # Save and setup controller + self.__controller = controller + controller.set_view(self) - # START Internal function + # START Internal functions def __init_content(self) -> None: """ [internal function] @@ -61,6 +64,7 @@ class HomeView(ttk.Frame): self.log_textarea = tst.ScrolledText(self, font=log_textarea_font, wrap=tk.WORD, state=tk.DISABLED, width=40, height=8)#, font=("Times New Roman", 15)) self.log_textarea.grid(row=3, column=0, columnspan=2, sticky=tk.EW, pady=10, padx=10) + # Message state self.message_label = ttk.Label(self, text='message label') # Download button @@ -73,13 +77,15 @@ class HomeView(ttk.Frame): => Function called when a download is requested. """ self.__controller.on_download_requested(self.web_entry.get(), self.name_entry.get()) - # END Internal function + # END Internal functions # START Controller methods def add_log(self, line: str) -> None: """ [function for controller] - TODO desc + => Add a log in the textarea where the logs are displayed. + + * :line: -> Log message to add. """ self.log_textarea.configure(state=tk.NORMAL) self.log_textarea.insert(tk.END, f"~ {line}\n") @@ -89,7 +95,7 @@ class HomeView(ttk.Frame): def clear_logs(self) -> None: """ [function for controller] - TODO desc + => Clean the textarea where the logs are displayed. """ self.log_textarea.configure(state=tk.NORMAL) self.log_textarea.delete('1.0', tk.END) @@ -98,25 +104,42 @@ class HomeView(ttk.Frame): def show_error_message(self, message) -> None: """ [function for controller] - TODO desc + => Display an error message on the interface. + + * :message: -> Message to display. """ self.message_label.configure(text=message, foreground='red') self.message_label.grid(row=4, column=0, columnspan=2, sticky=tk.NS, padx=2, pady=2) - self.message_label.after(30000, self.hide_message) + self.message_label.after(25000, self.hide_message) def show_success_message(self, message) -> None: """ [function for controller] - TODO desc + => Display a success message on the interface. + + * :message: -> Message to display. """ self.message_label.configure(text=message, foreground='green') self.message_label.grid(row=4, column=0, columnspan=2, sticky=tk.NS, padx=2, pady=2) - self.message_label.after(30000, self.hide_message) + self.message_label.after(25000, self.hide_message) def hide_message(self) -> None: """ - [function for controller] - TODO desc + [function for controller and this view] + => Hide the message on the interface. """ self.message_label.grid_forget() + + def set_interface_state(self, disable: bool) -> None: + """ + [function for controller] + => Allows to change the status of the interface with which the user + interacts by activating/deactivating it. + + * :disabled: -> True: interface disabled, False: interface enabled. + """ + state = tk.DISABLED if disable else tk.NORMAL + self.web_entry['state'] = state + self.name_entry['state'] = state + self.download_button['state'] = state # END Controller methods diff --git a/view/InfoView.py b/view/InfoView.py index 82ef8cd..30c2086 100644 --- a/view/InfoView.py +++ b/view/InfoView.py @@ -1,7 +1,10 @@ import tkinter as tk +from tkinter import font from tkinter import ttk from controller.Frames import Frames from controller.InfoController import InfoController +from view.MainWindow import MainWindow + class InfoView(ttk.Frame): """ @@ -18,7 +21,7 @@ class InfoView(ttk.Frame): __controller: InfoController = None # Constructor - def __init__(self, parent, controller: InfoController): + def __init__(self, parent: MainWindow, controller: InfoController): """ Constructor @@ -27,33 +30,72 @@ class InfoView(ttk.Frame): """ super().__init__(parent) - # create widgets - # label - self.label = ttk.Label(self, text='Email:') - self.label.grid(row=1, column=0) + # Init view + self.__init_content() - # email entry - # self.email_var = tk.StringVar() - # self.email_entry = ttk.Entry(self, textvariable=self.email_var, width=30) - # self.email_entry.grid(row=1, column=1, sticky=tk.NSEW) - - # save button - self.save_button = ttk.Button(self, text='just a button', command=self.event_btn) - self.save_button.grid(row=1, column=3, padx=10) - - # message - self.message_label = ttk.Label(self, text='Je suis super man comment allez vous heheheh je suis toutou', foreground='red') - self.message_label.grid(row=2, column=0, sticky=tk.EW) - - # place this frame - # self.grid(row=0, column=0, padx=5, pady=5, sticky=tk.NSEW) - # self.pack(fill='both', expand=True) - - # Save and setup main controller + # Save and setup controller self.__controller = controller controller.set_view(self) - def event_btn(self) -> None: - print("you clicked on the button that is on the info view!") - print("got redirected to the home view :D") - self.__controller.on_change_view(Frames.Home) + # START Internal functions + def __init_content(self) -> None: + """ + [internal function] + => Initialize the view content. + """ + self.columnconfigure(0, weight=4) + + # Back button + self.back_button = ttk.Button(self, text="Back", command=self.__event_button_back) + self.back_button.grid(row=0, column=0, sticky=tk.E, padx=5, pady=5, ipadx=1, ipady=1) + + # About title + self.title_label_font = font.Font(self, size=16, weight=font.BOLD) + self.title_label = ttk.Label(self, text="A title", font=self.title_label_font) + self.title_label.grid(row=1, column=0, sticky=tk.NS, padx=2, pady=2) + + # About content + self.content_label_font = font.Font(self, size=10) + self.content_label = ttk.Label(self, wraplength=400, justify='center', text='A long text', font=self.content_label_font, foreground='blue') + self.content_label.grid(row=2, column=0, sticky=tk.NS) + + # About version + self.version_label = ttk.Label(self, text='version : 1.0.0 - 02-02-2022') + self.version_label.grid(row=3, column=0, sticky=tk.NS, pady=15) + + def __event_button_back(self) -> None: + """ + [internal function] + => Function called when back button pressed. + """ + self.__controller.on_change_view(Frames.HOME) + # END Internal functions + + # START Controller methods + def set_title(self, title: str) -> None: + """ + [function for controller] + => Define view/page info : title + + * :title: -> Title for the view. + """ + self.title_label.configure(text=title) + + def set_content(self, content: str) -> None: + """ + [function for controller] + => Define view/page info : content + + * :content: -> Content for the view. + """ + self.content_label.configure(text=content) + + def set_version(self, version: str) -> None: + """ + [function for controller] + => Define view/page info : version + + * :version: -> Version for the view. + """ + self.version_label.configure(text=version) + # END Controller methods diff --git a/view/MainWindow.py b/view/MainWindow.py index 42d9dfa..7d76c8e 100644 --- a/view/MainWindow.py +++ b/view/MainWindow.py @@ -1,5 +1,8 @@ +import os +import sys import tkinter as tk from tkinter import messagebox +from controller.Frames import Frames from controller.MainController import MainController @@ -7,7 +10,8 @@ class MainWindow(tk.Tk): """ View - MainWindow - TODO dec... + This view is the main view of the application, it manages the different frames/views + of the application, captures the events to send them to the main controller. @author Jérémi Nihart / EndMove @link https://git.endmove.eu/EndMove/WebPicDownloader @@ -47,8 +51,9 @@ class MainWindow(tk.Tk): => Initialize window parameters """ # self.title('My tkinter app') - # self.geometry('450x250') + self.geometry('430x310') self.resizable(False, False) + self.iconbitmap(f"{self.__controller.get_config('sys_directory')}\\assets\logo.ico") # self.config(bg='#f7ef38') def __init_top_menu(self) -> None: @@ -99,7 +104,7 @@ class MainWindow(tk.Tk): """ self.title(title) - def show_frame(self, frame) -> None: + def show_frame(self, frame: Frames) -> None: """ [function for app & controller] => Allows to display the selected frame provided that it @@ -125,20 +130,23 @@ class MainWindow(tk.Tk): def show_question_dialog(self, title: str, message: str, icon: str='question') -> bool: """ [function for controller] - => TODO DESC + => Display a question dialog to the user, which he can answer with yes or no. - * :message: -> - * RETURN -> + * :title: -> Title of the dialogue. + * :message: -> Message of the dialogue displayed to the user. + * :icon: -> Icon of the dialogue displayed to the user. + * RETURN -> True id the user selected yes, False else. """ - return messagebox.askquestion(title, message, icon=icon) + return True if (messagebox.askquestion(title, message, icon=icon) == "yes") else False def show_information_dialog(self, title: str, message: str, icon: str='information') -> None: """ [function for controller] - => TODO DESC + => Display an information dialog to the user. - * :message: -> - * RETURN -> + * :title: -> Title of the dialogue. + * :message: -> Message of the dialogue displayed to the user. + * :icon: -> Icon of the dialogue displayed to the user. """ messagebox.showinfo(self, title, message, icon=icon) # END Controller methods