Restructuration + init test system

This commit is contained in:
2022-09-11 11:52:41 +02:00
parent a3dd6698fc
commit 6b26b89c69
22 changed files with 60 additions and 82 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@@ -0,0 +1,18 @@
from enum import Enum
class Frames(Enum):
"""
Enumeration - Frames
Lists the different windows of the program in order to facilitate
their call during the execution. Each parameter of the enumeration
represents a Frame/Tab.
@author Jérémi Nihart / EndMove
@link https://git.endmove.eu/EndMove/WebPicDownloader
@version 1.0.0
@since 2022-08-30
"""
HOME = 1 # Home view
INFO = 2 # Info & copyright view

View File

@@ -0,0 +1,118 @@
from webpicdownloader.controller.MainController import MainController
from webpicdownloader.model.WebPicDownloader import MessageType, WebPicDownloader
class HomeController:
"""
Controller - HomeController
This controller handles all the interaction directly related to the download.
@author Jérémi Nihart / EndMove
@link https://git.endmove.eu/EndMove/WebPicDownloader
@version 1.0.0
@since 2022-08-30
"""
# Variables
__main_controller: MainController = None
__view = None
__webpic: WebPicDownloader = None
# Constructor
def __init__(self, controller: MainController, webpic: WebPicDownloader) -> None:
"""
Constructor
* :controller: -> The main application cpntroller.
* :webpic: -> The webpicdownloader instance.
"""
# Setub variables
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)
# START View methods
def set_view(self, view) -> None:
"""
[function for view]
=> Define the view of this controller.
* :view: -> The view that this controller manage.
"""
self.__view = view
# END View method
# START View events
def on_download_requested(self, url: str, name: str) -> None:
"""
[event function for view]
=> Call this event method when the user requests to download
* :url: -> The url of the website to use for pic-download.
* :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, 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.
"""
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
def on_quit(self) -> bool:
"""
[event function for controller]
=> Call this event when a request to exit is thrown.
"""
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() # hot stop deamon
return False
return True
self.__webpic.stop_downloading(block=True)
# END Controller methods

View File

@@ -0,0 +1,54 @@
from webpicdownloader.controller.Frames import Frames
from webpicdownloader.controller.MainController import MainController
class InfoController:
"""
Controller - InfoController
This controller manages the display of information in the information view.
@author Jérémi Nihart / EndMove
@link https://git.endmove.eu/EndMove/WebPicDownloader
@version 1.0.0
@since 2022-08-30
"""
# Variables
__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
def set_view(self, view) -> None:
"""
[function for view]
: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: 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.
"""
self.__main_controller.change_frame(frame)
# END View events

View File

@@ -0,0 +1,134 @@
import os
from webpicdownloader.controller.Frames import Frames
class MainController:
"""
Controller - MainController
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
@version 1.0.0
@since 2022-08-30
"""
# Variables
__config: dict = None
__view = None
__quite_event_subscribers: list = None
# Constructor
def __init__(self, config: dict) -> None:
"""
Constructor
* :config: -> The application configuration (a dictionary).
"""
# Setup variables
self.__config = config
self.__quite_event_subscribers = []
# START View methods
def set_view(self, view) -> None:
"""
[function for view]
=> Allow to define the controller view.
* :view: -> The view that this controller manage and setup it.
"""
self.__view = view
view.set_window_title(self.get_config('app_name'))
def on_open_folder(self) -> None:
"""
[event function for view]
=> Event launch when you ask to open the current folder.
"""
os.startfile(self.get_config('app_folder')) # Open the file explorer on working dir
def on_quite(self) -> None:
"""
[event function for view]
=> Event launch when you ask to quit the program. This event is propagated
to the subscribers, they can eventually cancel the event
"""
for callback in self.__quite_event_subscribers:
if callback():
return
self.__view.close_window() # End the program
def on_check_for_update(self) -> None:
"""
[event function for view]
=> Event launched when a check for available updates is requested.
"""
# TODO write the function
self.show_information_dialog(self.get_config('app_name'), "Oupss, this functionality isn't available yet!\nTry it again later.")
def on_about(self) -> None:
"""
[event function for view]
=> Event launched when a request for more information arise.
"""
self.change_frame(Frames.INFO)
# END View methods
# START Controller methods
def change_frame(self, frame: Frames) -> None:
"""
[function for controller]
=> Allows you to request a frame change in the main window.
* :frame: -> The frame we want to display on the window instead of the current frame.
"""
self.__view.show_frame(frame)
def get_config(self, name: str) -> str|int:
"""
[function for controller]
=> Allows controllers to access the application's configuration.
* :name: -> The name of the configuration parameter for which we want to access the configured value.
"""
if self.__config.get(name):
return self.__config.get(name)
else:
raise ValueError("Unable to find a configuration with this name")
def show_question_dialog(self, title: str='title', message: str='question?', icon: str='question') -> bool:
"""
[function for controller]
=> Ask a question to the user and block until he answers with yes or no.
* :title: -> Title of the dialogue.
* :message: -> Message of the dialogue.
* :icon: -> Icon of the dialogue
"""
return self.__view.show_question_dialog(title, message, icon)
def show_information_dialog(self, title: str='title', message: str='informations!', icon: str='info') -> bool:
"""
[function for controller]
=> Display a pop-up information dialog to the user.
* :title: -> Title of the dialogue.
* :message: -> Message of the dialogue.
* :icon: -> Icon of the dialogue
"""
return self.__view.show_information_dialog(title, message, icon)
# END Controller methods
# START Controller events
def subscribe_to_quite_event(self, callback) -> None:
"""
[function for controller]
=> Subscription function allowing to be warned if a request to quit occurs.
In the case where the callback function returns False the process continues
but if the callback returns True the process is aborted.
* :callback: -> Callback function that will be called when a request to exit occurs.
"""
self.__quite_event_subscribers.append(callback)
# END Controller events

View File

@@ -0,0 +1,333 @@
import os
from enum import Enum
from threading import Semaphore, Thread
from urllib import request
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
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. 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 <contact@endmove.eu>
@link https://git.endmove.eu/EndMove/WebPicDownloader
@version 1.2.1
@since 2022-09-05
"""
# Variables
__callbacks: dict = None # Callback dictionary
__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
# Constructor
def __init__(self, path: str = None, headers: dict = None, messenger = None,
success = None, failure = None) -> None:
"""
Constructor
=> It is important to initialize the WebPicDownloader object properly. The callback
functions can be initialized after the creation of the object.
* :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.
* :messenger: -> Callback function messenger (see setter).
* :success: -> Callback function success (see setter).
* :failure: -> Callback function failure (see setter).
"""
super().__init__(daemon=True, name='WebPic download worker')
self.__callbacks = {
'messenger': messenger if messenger else lambda msg, type: print(msg),
'success': success if success else lambda: print("Success!"),
'failure': failure if failure else lambda: print("failure!")
}
self.__settings = {
'root_path': path if path else os.getcwd(),
'headers': headers if headers else {
'User-Agent': "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
}
}
self.__dl_infos = {
'website_url': 'url',
'download_name': 'name',
'download_path': 'full_path',
'running': False
}
self.__sem = Semaphore(0)
self.__exit = False
self.start() # start deamon
# Internal functions
def __get_html(self, url: str) -> str:
"""
Internal Function #do-not-use#
=> Allow to retrieve the HTML content of a website.
* :url: -> The url of the site for which we want to get the content of the HTML page.
* RETURN -> Web page content.
"""
req = request.Request(url, headers=self.__settings.get('headers'))
response = request.urlopen(req)
return response.read().decode('utf-8')
def __find_all_img(self, html: str) -> ResultSet:
"""
Internal Function #do-not-use#
=> Allow to retrieve all images of an html page.
* :html: -> Html code in which to search for image balises.
* RETURN -> Iterable with all image balises.
"""
soup = BeautifulSoup(html, 'html.parser')
return soup.find_all('img')
def __find_img_link(self, img: Tag) -> str:
"""
Internal Function #do-not-use#
=> Allow to retrieve the link of a picture.
* :img: -> Image tag {@code bs4.Tag} for which to search the link of an image.
* RETURN -> Image link.
"""
if img.get('src'):
link = img.get('src')
elif img.get('data-src'):
link = img.get('data-src')
elif img.get('data-srcset'):
link = img.get('data-srcset')
elif img.get('data-fallback-src'):
link = img.get('data-fallback-src')
else:
raise ValueError("Unable to find image url")
if not 'http' in link:
raise ValueError("Bad image url")
return link
def __find_image_type(self, img_link: str) -> str:
"""
Internal Function #do-not-use#
=> Allow to retrieve the right image type (png, jpeg...)
* :img_link: -> Lien de l'image pourllaquel trouver le bon type.
* RETURN -> Type of image.
"""
type = img_link.split('.')[-1]
if '?' in type:
type = type.split('?')[0]
return type
def __download_img(self, url: str, filename: str) -> None:
"""
Internal Function #do-not-use#
=> Allow to download a picture from internet
* :url: -> Image url on the web.
* :filename: -> Full path with name of the future image.
"""
req = request.Request(url, headers=self.__settings.get('headers'))
raw_img = request.urlopen(req).read()
with open(filename, 'wb') as img:
img.write(raw_img)
def __initialize_folder(self, folder_path: str) -> None:
"""
Internal Function #do-not-use#
=> Checks if the folder in which to place the images to be uploaded exists and if
not chalk it up. An exception is raised if this folder already exists.
* :folder_path: -> Full path to the working folder (for the download task).
"""
if not os.path.exists(folder_path):
os.mkdir(folder_path)
else:
raise ValueError("The folder already exists, it may already contain images")
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, type)
# Public functions
def set_success_callback(self, callback) -> None:
"""
Setter to define the callback function when the download succeeded.
* :callback: -> the callback function to call when the download is a success.
"""
self.__callbacks['success'] = callback
def set_failure_callback(self, callback) -> None:
"""
Setter to define the callback function called when the download fails.
* :callback: -> the callback function to call when the download is a failure.
"""
self.__callbacks['failure'] = callback
def set_messenger_callback(self, callback) -> None:
"""
Setter to define the callback function called when new messages arrive.
* :callback: -> the callback function to call when a message event is emited.
"""
self.__callbacks['messenger'] = callback
def start_downloading(self, url: str, name: str) -> None:
"""
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 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:
"""
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() # waiting the authorization to process
if self.__exit: # check if the exiting is requested
return
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
# 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 {tot_count} images on the website.")
# 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!")
dl_count += 1 # increment download counter
except Exception as err:
self.__msg(f"ERROR: Unable to process image {i} -> err[{err}].")
# 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 # inficate that the thread is free
if __name__ == "__main__":
# Internal entry point for testing and console use.
import time
wpd = WebPicDownloader() # Instance of webpic
# Callback functions
def success():
print("\nDownload completed with success.")
def failed():
print("\nDownload completed with errors.")
# Set-up callback functions for webpic
wpd.set_success_callback(success)
wpd.set_failure_callback(failed)
# Ask for download
print("\nWelcome to WebPicDownloader!")
url = input("Website URL ? ")
name = input("Folder name ? ")
wpd.start_downloading(url, name) # Start downloading
time.sleep(1) # We wait for the download to start before ask to stop it
wpd.stop_downloading(block=True) # Stop downloading but block till the download end.

View File

@@ -0,0 +1,17 @@
from http.client import HTTPException
import re
from urllib import request as http
def fetch_version(headers: str, url: str) -> str:
"""
"""
request = http.Request(url=url, headers=headers, method='GET')
response = http.urlopen(request)
if response.getcode() != 200:
raise HTTPException("Bad response returned by server")
return response.read().decode('utf-8')

View File

@@ -0,0 +1,145 @@
import tkinter as tk
import tkinter.font as tfont
from tkinter import ttk
from tkinter import scrolledtext as tst
from webpicdownloader.controller.HomeController import HomeController
from webpicdownloader.view.MainWindow import MainWindow
class HomeView(ttk.Frame):
"""
View - HomeWindow
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.1
@since 2022-09-05
"""
# Variables
__controller: HomeController = None
# Constructor
def __init__(self, parent: MainWindow, controller: HomeController):
"""
Constructor
* :parent: -> The main windows container.
* :controller: -> The view controller
"""
super().__init__(parent)
# Init view
self.__init_content()
# Save and setup controller
self.__controller = controller
controller.set_view(self)
# START Internal functions
def __init_content(self) -> None:
"""
[internal function]
=> Initialize the view content.
"""
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=3)
# Website link
self.web_label = ttk.Label(self, text="Website URL:")
self.web_label.grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
self.web_entry = ttk.Entry(self, width=50) # show="-"
self.web_entry.grid(row=0, column=1, sticky=tk.E, padx=5, pady=5, ipadx=2, ipady=2)
# Download name
self.name_label = ttk.Label(self, text="Download Name:")
self.name_label.grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
self.name_entry = ttk.Entry(self, width=50)
self.name_entry.grid(row=1, column=1, sticky=tk.E, padx=5, pady=5, ipadx=2, ipady=2)
# Logs area
log_textarea_font = tfont.Font(size=10)
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
self.download_button = ttk.Button(self, text="Start downloading", command=self.__event_button_download)
self.download_button.grid(row=5, column=1, sticky=tk.E, padx=5, pady=5, ipadx=5, ipady=2)
def __event_button_download(self) -> None:
"""
[internal function]
=> Function called when a download is requested.
"""
self.__controller.on_download_requested(self.web_entry.get(), self.name_entry.get())
# END Internal functions
# START Controller methods
def add_log(self, line: str) -> None:
"""
[function for controller]
=> 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")
self.log_textarea.see(tk.END)
self.log_textarea.configure(state=tk.DISABLED)
def clear_logs(self) -> None:
"""
[function for controller]
=> Clean the textarea where the logs are displayed.
"""
self.log_textarea.configure(state=tk.NORMAL)
self.log_textarea.delete('1.0', tk.END)
self.log_textarea.configure(state=tk.DISABLED)
def show_error_message(self, message) -> None:
"""
[function for controller]
=> 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(25000, self.hide_message)
def show_success_message(self, message) -> None:
"""
[function for controller]
=> 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(25000, self.hide_message)
def hide_message(self) -> None:
"""
[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

View File

@@ -0,0 +1,101 @@
import tkinter as tk
from tkinter import font
from tkinter import ttk
from webpicdownloader.controller.Frames import Frames
from webpicdownloader.controller.InfoController import InfoController
from webpicdownloader.view.MainWindow import MainWindow
class InfoView(ttk.Frame):
"""
View - InfoWindow
This view displays information about the program, as well as its version and release date.
@author Jérémi Nihart / EndMove
@link https://git.endmove.eu/EndMove/WebPicDownloader
@version 1.0.0
@since 2022-09-06
"""
# Variables
__controller: InfoController = None
# Constructor
def __init__(self, parent: MainWindow, controller: InfoController):
"""
Constructor
* :parent: -> The main windows container.
* :controller: -> The view controller
"""
super().__init__(parent)
# Init view
self.__init_content()
# Save and setup controller
self.__controller = controller
controller.set_view(self)
# 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

View File

@@ -0,0 +1,154 @@
import tkinter as tk
from tkinter import messagebox
from webpicdownloader.controller.Frames import Frames
from webpicdownloader.controller.MainController import MainController
class MainWindow(tk.Tk):
"""
View - MainWindow
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
@version 1.0.1
@since 2022-09-04
"""
# Variables
__controller: MainController = None
__views: dict = None
__frame_id: int = None
# Constructor
def __init__(self, controller: MainController) -> None:
"""
Constructor
* :controller: -> The main application cpntroller.
"""
super().__init__()
# Init view repository
self.__views = {}
# Save and setup main controller
self.__controller = controller
controller.set_view(self)
# Init view components & more
self.__init_window()
self.__init_top_menu()
self.__init_bind_protocol()
# START Internal methods
def __init_window(self) -> None:
"""
[internal function]
=> Initialize window parameters
"""
self.iconbitmap(f"{self.__controller.get_config('sys_directory')}\\webpicdownloader\\assets\\logo.ico") # App logo
window_width = 430 # App width
window_height = 305 # App height
x_cordinate = int((self.winfo_screenwidth()/2) - (window_width/2))
y_cordinate = int((self.winfo_screenheight()/2) - (window_height/2))
self.geometry(f"{window_width}x{window_height}+{x_cordinate}+{y_cordinate}") # App size and middle centering
self.resizable(False, False) # Disable app resizing
def __init_top_menu(self) -> None:
"""
[internal function]
=> Initialize top menu of the window.
"""
main_menu = tk.Menu(self)
# Top menu File item
col1_menu = tk.Menu(main_menu, tearoff=0)
col1_menu.add_command(label="Open app folder", command=self.__controller.on_open_folder)
col1_menu.add_separator()
col1_menu.add_command(label="Quit", command=self.__controller.on_quite)
main_menu.add_cascade(label="File", menu=col1_menu)
# Top menu Help item
col2_menu = tk.Menu(main_menu, tearoff=0)
col2_menu.add_command(label="Check for update", command=self.__controller.on_check_for_update)
col2_menu.add_command(label="About", command=self.__controller.on_about)
main_menu.add_cascade(label="Help", menu=col2_menu)
self.config(menu=main_menu)
def __init_bind_protocol(self) -> None:
"""
[internal function]
=> Initialize the function bindding on events of the main window.
"""
self.protocol("WM_DELETE_WINDOW", self.__controller.on_quite)
# END Internal methods
# START App methods
def add_view(self, frame, view) -> None:
"""
[function for app]
* :frame: -> the frame id of the view to add.
"""
self.__views[frame] = view
# END App methods
# START Controller methods
def set_window_title(self, title: str) -> None:
"""
[function for controller]
=> Sets the title of the main window.
* :title: -> Window title.
"""
self.title(title)
def show_frame(self, frame: Frames) -> None:
"""
[function for app & controller]
=> Allows to display the selected frame provided that it
has been previously added to the frame dictionary.
* :frame: -> the frame if of the view to display.
"""
if self.__views.get(frame):
if self.__frame_id:
self.__views.get(self.__frame_id).pack_forget()
self.__views.get(frame).pack(fill=tk.BOTH, expand=False)
self.__frame_id = frame
else:
raise ValueError("Unable to find the requested Frame")
def close_window(self) -> None:
"""
[function for controller]
=> Closes the main window and stops the program from the controller.
"""
self.destroy()
def show_question_dialog(self, title: str, message: str, icon: str='question') -> bool:
"""
[function for controller]
=> Display a question dialog to the user, which he can answer with yes or no.
* :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 True if (messagebox.askquestion(title, message, icon=icon) == "yes") else False
def show_information_dialog(self, title: str, message: str, icon: str='info') -> None:
"""
[function for controller]
=> Display an information dialog to the user.
* :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(title, message, icon=icon)
# END Controller methods