11 Commits

Author SHA1 Message Date
7a8ca677ea ♻️ Refactor generators and add an about window 2024-09-13 17:51:58 +02:00
Shikiryu
5112439712 🚧 Add Quickposes model download 2024-09-13 00:59:04 +02:00
5941da9ae5 ✨ Add cheated pause
For #36
2024-06-17 14:16:58 +02:00
a8c0979ef1 ✨ Add not cheated pause
For #36
2024-06-14 17:35:28 +02:00
Shikiryu
328b7a0e49 πŸš€ Modify windows makefile 2024-06-10 21:49:04 +02:00
Shikiryu
ee8968cbab πŸ’„ Add progressbar as timer
+ remove one bug and updates dependencies
2024-05-17 23:39:29 +02:00
Shikiryu
33d6e9d4df πŸ’„ Add better displayed tooltip 2024-05-16 00:19:49 +02:00
3b17dff7be 🚧 Add icons in toolbar
For #17
2024-05-15 23:54:07 +02:00
d8b6e7bd8c πŸ› Transform Radiobutton to button to allow re-click
Fix #29
2024-05-15 17:25:18 +02:00
8087d326ef πŸ“ Add a README 2024-05-15 09:40:16 +02:00
7702a0bea3 πŸ’„ Add an app icon
Fix #15
2024-05-15 09:30:57 +02:00
14 changed files with 227 additions and 34 deletions

19
README.md Normal file
View File

@@ -0,0 +1,19 @@
# Drawing Training
Open Source alternative to [GestureDrawing!](https://cubebrush.co/advanches/products/d9q6yq/gesturedrawing)
## Authors
- [@shikiryu](https://git.shikiryu.com/Shikiryu)
## Installation
Install my-project with python
```bash
sudo apt install cmake build-essential tcl-dev tk-dev python3-tk
pip install -r requirements.txt
```

BIN
assets/icons/logo-dt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
assets/icons/pause.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

BIN
assets/icons/sand-clock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

View File

@@ -1,5 +1,6 @@
import os import os
import sys import sys
from tkinter import PhotoImage
import customtkinter import customtkinter
@@ -15,7 +16,9 @@ def main():
root = customtkinter.CTk() root = customtkinter.CTk()
root.title("Drawing Training!") root.title("Drawing Training!")
root.geometry("300x600") root.geometry("300x300")
icon = PhotoImage(file="assets/icons/logo-dt.png")
root.iconphoto(True, icon)
App(root) App(root)

View File

@@ -1,7 +1,14 @@
.PHONY: build dist .PHONY: build dist
TOTAL_LOCATION := $(pip show customtkinter | grep Location) .ONESHELL:
LOCATION=!TOTAL_LOCATION:~0,10! VENV_DIR=.venv
ACTIVATE_VENV:=. $(VENV_DIR)/bin/activate
LOCATION:= $(shell pip show customtkinter | grep Location | cut -c 11-)
install:
python3 -m venv "$(VENV_DIR)"
$(ACTIVATE_VENV)
pip install --upgrade --requirement requirements.txt
build_linux: build_linux:
rm -rf build dist drawingtraining.spec rm -rf build dist drawingtraining.spec
@@ -9,4 +16,4 @@ build_linux:
build_windows: build_windows:
rm -rf build dist drawingtraining.spec rm -rf build dist drawingtraining.spec
pyinstaller main.py --onefile -w --hidden-import="PIL._tkinter_finder" -n drawingtraining --add-data "assets;assets" --add-data "${LOCATION}\customtkinter;customtkinter\" pyinstaller main.py --onefile -w --hidden-import="PIL._tkinter_finder" -n drawingtraining --add-data "assets;assets" --add-data "$(LOCATION)\customtkinter;customtkinter" --icon="assets/icons/logo-dt.png"

View File

@@ -1,6 +1,8 @@
pillow~=9.5.0 pillow~=10.0.1
pyinstaller==5.12.0 pyinstaller==5.13.1
pygame~=2.4.0 pygame~=2.4.0
customtkinter~=5.1.3 customtkinter~=5.1.3
configparser~=5.3.0 configparser~=5.3.0
CTkToolTip~=0.4 CTkToolTip~=0.4
requests~=2.32.3
CTkMenuBar~=0.8

View File

@@ -1,5 +1,7 @@
import copy import copy
from tkinter import * from tkinter import *
import PIL
from PIL import ImageTk, Image, ImageOps from PIL import ImageTk, Image, ImageOps
@@ -8,6 +10,7 @@ class ImagePlaceholder:
self.is_break = False self.is_break = False
self.current_image = None self.current_image = None
self.current_original_image = None self.current_original_image = None
self.paused_original_image = None
self.image_window = image_window self.image_window = image_window
self.images = images.copy() self.images = images.copy()
@@ -16,6 +19,9 @@ class ImagePlaceholder:
self.image_label.pack(side=TOP, fill=BOTH, expand=1) self.image_label.pack(side=TOP, fill=BOTH, expand=1)
def display_new_image(self): def display_new_image(self):
if len(self.images) == 0:
self.image_window.on_closing()
return
image_path = self.images.pop(0) image_path = self.images.pop(0)
if image_path == "break": if image_path == "break":
self.is_break = True self.is_break = True
@@ -35,7 +41,7 @@ class ImagePlaceholder:
if w < 21 or h < 21: if w < 21 or h < 21:
w = 1280 w = 1280
h = 1024 h = 1024
self.current_image.thumbnail((w - 20, h - 20), Image.ANTIALIAS) self.current_image.thumbnail((w - 20, h - 20), PIL.Image.Resampling.LANCZOS)
if reload: if reload:
self.load_widget() self.load_widget()
@@ -56,3 +62,16 @@ class ImagePlaceholder:
self.image_label.configure(bg="#e8d4bc" if self.is_break else "#FFFFFF") self.image_label.configure(bg="#e8d4bc" if self.is_break else "#FFFFFF")
self.image_label.configure(image=image_to_display) self.image_label.configure(image=image_to_display)
self.image_label.image = image_to_display self.image_label.image = image_to_display
def pause(self, pause=True, cheat=False):
if pause:
self.paused_original_image = self.current_original_image
if not cheat:
image_path = 'assets/images/break.jpg'
self.current_original_image = Image.open(image_path)
else:
self.current_original_image = self.paused_original_image
self.paused_original_image = None
self.current_image = copy.deepcopy(self.current_original_image)
self.apply_options()

View File

@@ -1,18 +1,21 @@
import copy
import os import os
import subprocess import subprocess
import sys import sys
from tkinter import * from tkinter import *
from tkinter.ttk import Progressbar, Style
from tkinter.messagebox import *
from CTkToolTip import *
from PIL import ImageTk, Image, ImageOps
import customtkinter import customtkinter
from CTkToolTip import *
from PIL import ImageTk, Image
from src.util import Util from src.util import Util
class Toolbar: class Toolbar:
def __init__(self, image_window, timer): def __init__(self, image_window, timer):
self.timers = [] self.timers = []
self.is_paused = False
self.current_image = None self.current_image = None
self.current_original_image = None self.current_original_image = None
self.image_window = image_window self.image_window = image_window
@@ -26,52 +29,67 @@ class Toolbar:
next_button = customtkinter.CTkButton(toolbar, image=ImageTk.PhotoImage(Image.open("assets/icons/next.png")), next_button = customtkinter.CTkButton(toolbar, image=ImageTk.PhotoImage(Image.open("assets/icons/next.png")),
text="", command=self.image_window.next_image) text="", command=self.image_window.next_image)
next_button.pack(side=LEFT, padx=0, pady=0) next_button.pack(side=LEFT, padx=0, pady=0)
CTkToolTip(next_button, delay=0.5, message="Next") CTkToolTip(next_button, delay=0.1, message="Next", y_offset=-40, x_offset=-40)
pause_button = customtkinter.CTkButton(toolbar, image=ImageTk.PhotoImage(Image.open("assets/icons/pause.png")),
text="", command=self.pause)
pause_button.pack(side=LEFT, padx=0, pady=0)
CTkToolTip(pause_button, delay=0.1, message="Next", y_offset=-40, x_offset=-40)
self.bw_button = customtkinter.CTkButton(toolbar, image=ImageTk.PhotoImage(Image.open("assets/icons/black-and" self.bw_button = customtkinter.CTkButton(toolbar, image=ImageTk.PhotoImage(Image.open("assets/icons/black-and"
"-white.png")), "-white.png")),
text="", command=self.toggle_black_white) text="", command=self.toggle_black_white)
self.bw_button.pack(side=LEFT, padx=0, pady=0) self.bw_button.pack(side=LEFT, padx=0, pady=0)
CTkToolTip(self.bw_button, delay=0.5, message="Black and white image") CTkToolTip(self.bw_button, delay=0.1, message="Black and white image", y_offset=-40, x_offset=-40)
self.mirror_button = customtkinter.CTkButton(toolbar, command=self.toggle_mirror, text="", self.mirror_button = customtkinter.CTkButton(toolbar, command=self.toggle_mirror, text="",
image=ImageTk.PhotoImage(Image.open("assets/icons/flip.png"))) image=ImageTk.PhotoImage(Image.open("assets/icons/flip.png")))
self.mirror_button.pack(side=LEFT, padx=0, pady=0) self.mirror_button.pack(side=LEFT, padx=0, pady=0)
CTkToolTip(self.mirror_button, delay=0.5, message="Mirrored image") CTkToolTip(self.mirror_button, delay=0.1, message="Mirrored image", y_offset=-40, x_offset=-40)
open_folder_button = customtkinter.CTkButton(toolbar, command=self.open_folder, text="", open_folder_button = customtkinter.CTkButton(toolbar, command=self.open_folder, text="",
image=ImageTk.PhotoImage(Image.open("assets/icons/folder.png"))) image=ImageTk.PhotoImage(Image.open("assets/icons/folder.png")))
open_folder_button.pack(side=LEFT, padx=0, pady=0) open_folder_button.pack(side=LEFT, padx=0, pady=0)
CTkToolTip(open_folder_button, delay=0.5, message="Open folder") CTkToolTip(open_folder_button, delay=0.1, message="Open folder", y_offset=-40, x_offset=-40)
self.always_on_top_button = customtkinter.CTkButton(toolbar, command=self.toggle_always_on_top, text="", self.always_on_top_button = customtkinter.CTkButton(toolbar, command=self.toggle_always_on_top, text="",
image=ImageTk.PhotoImage( image=ImageTk.PhotoImage(
Image.open("assets/icons/copy.png"))) Image.open("assets/icons/copy.png")))
self.always_on_top_button.pack(side=LEFT, padx=0, pady=0) self.always_on_top_button.pack(side=LEFT, padx=0, pady=0)
CTkToolTip(self.always_on_top_button, delay=0.5, message="Always on top") CTkToolTip(self.always_on_top_button, delay=0.1, message="Always on top", y_offset=-40, x_offset=-40)
self.fullscreen_button = customtkinter.CTkButton(toolbar, command=self.toggle_fullscreen, text="", self.fullscreen_button = customtkinter.CTkButton(toolbar, command=self.toggle_fullscreen, text="",
image=ImageTk.PhotoImage( image=ImageTk.PhotoImage(
Image.open("assets/icons/maximize.png"))) Image.open("assets/icons/maximize.png")))
self.fullscreen_button.pack(side=LEFT, padx=0, pady=0) self.fullscreen_button.pack(side=LEFT, padx=0, pady=0)
CTkToolTip(self.fullscreen_button, delay=0.5, message="Fullscreen") CTkToolTip(self.fullscreen_button, delay=0.1, message="Fullscreen", y_offset=-40, x_offset=-40)
timer_button = customtkinter.CTkButton(toolbar, command=self.toggle_timer, text="", timer_button = customtkinter.CTkButton(toolbar, command=self.toggle_timer, text="",
image=ImageTk.PhotoImage(Image.open("assets/icons/timer.png"))) image=ImageTk.PhotoImage(Image.open("assets/icons/sand-clock.png")))
timer_button.pack(side=LEFT, padx=0, pady=0) timer_button.pack(side=LEFT, padx=0, pady=0)
CTkToolTip(timer_button, delay=0.5, message="Toggle timer") CTkToolTip(timer_button, delay=0.1, message="Toggle timer", y_offset=-40, x_offset=-40)
self.timer_label = customtkinter.CTkLabel(toolbar, text=str(self.timer), self.timer_label = customtkinter.CTkLabel(toolbar, text=str(self.timer),
text_color=Util.get_default_button_color()) text_color=Util.get_default_button_color())
self.timer_label.pack(side=RIGHT, ipadx=20) self.timer_label.pack(side=RIGHT, ipadx=20)
style = Style()
style.theme_use('default')
style.configure("black.Horizontal.TProgressbar", background='black')
self.progressbar = Progressbar(toolbar, length=280)
self.progressbar.pack(side=RIGHT, ipadx=20)
def display_new_timer(self): def display_new_timer(self):
if len(self.timers) == 0:
return
self.timer = self.timers.pop(0) self.timer = self.timers.pop(0)
self.timer_label.configure(text=Util.format_seconds(self.timer)) self.timer_label.configure(text=Util.format_seconds(self.timer))
self.timer_check = self.image_window.window.after(1000, self.update_timer, self.timer) self.timer_check = self.image_window.window.after(1000, self.update_timer, self.timer)
def update_timer(self, current): def update_timer(self, current):
current -= 1 current -= 1
self.progressbar['value'] = 100 - (current * 100 / self.timer)
if current == 10: if current == 10:
self.image_window.play_countdown() self.image_window.play_countdown()
if current > 0: if current > 0:
@@ -103,8 +121,11 @@ class Toolbar:
try: try:
self.timer_label.pack_info() self.timer_label.pack_info()
self.timer_label.pack_forget() self.timer_label.pack_forget()
self.progressbar.pack_info()
self.progressbar.pack_forget()
except TclError: except TclError:
self.timer_label.pack(side=RIGHT, ipadx=20) self.timer_label.pack(side=RIGHT, ipadx=20)
self.progressbar.pack(side=RIGHT, ipadx=20)
def open_folder(self): def open_folder(self):
if sys.platform == "win32": if sys.platform == "win32":
@@ -133,3 +154,23 @@ class Toolbar:
"fullscreen"] else Util.get_default_button_color()) "fullscreen"] else Util.get_default_button_color())
self.image_window.window.attributes("-fullscreen", self.image_window.option["fullscreen"]) self.image_window.window.attributes("-fullscreen", self.image_window.option["fullscreen"])
def pause(self):
cheating = False
if not self.is_paused:
cheating = askyesno("Pause", "Laisser l'image affichΓ©e ?")
self.is_paused = True
else:
self.is_paused = False
# timer is paused whatever happened
if self.timer_check is not None:
self.image_window.window.after_cancel(self.timer_check)
self.timer_check = None
else:
self.timer_check = self.image_window.window.after(
1000, self.update_timer, int((100-self.progressbar['value'])*self.timer/100)
)
self.image_window.image.pause(pause=self.is_paused, cheat=cheating)

41
src/entity/generator.py Normal file
View File

@@ -0,0 +1,41 @@
from datetime import datetime
import os
import io
import tempfile
import requests
from PIL import Image
class Generator(object):
def __init__(self, generator=None, folder=None):
self.selected_folder = folder
if generator == 'poses':
self.generate_poses()
def generate_poses(self):
dt = datetime.now()
ts = int(datetime.timestamp(dt))
url = 'https://quickposes.com/ajax/getPosesForSlider/Chiaroscuro?_=' + str(ts)
resp_json = requests.get(url=url)
if resp_json.status_code == 200:
data = resp_json.json()
c = 0
for src in data:
if os.path.exists(os.path.join(self.selected_folder, src['src'])):
continue
if c >= 3:
break
buffer = tempfile.SpooledTemporaryFile(max_size=1e9)
r = requests.get('https://quickposes.com/assets/poses/' + src['src'], stream=True)
if r.status_code == 200:
downloaded = 0
for chunk in r.iter_content(chunk_size=1024):
downloaded += len(chunk)
buffer.write(chunk)
buffer.seek(0)
i = Image.open(io.BytesIO(buffer.read()))
i.save(os.path.join(self.selected_folder, src['src']), quality=85)
buffer.close()
c += 1

View File

@@ -47,4 +47,4 @@ class Util:
@staticmethod @staticmethod
def get_default_active_button_color(): def get_default_active_button_color():
return "gray29", "gray29" return "green", "green"

30
src/window/about.py Normal file
View File

@@ -0,0 +1,30 @@
import webbrowser
import customtkinter
from customtkinter import CTkToplevel
class AboutWindow:
def __init__(self, app):
self.app = app
self.window = CTkToplevel(app.root)
self.window.title("Γ€ propos")
self.window.geometry("600x300")
self.window.protocol("WM_DELETE_WINDOW", self.on_closing)
link_gesture_drawing = customtkinter.CTkLabel(self.window, text="Open Source alternative to GestureDrawing!")
link_gesture_drawing.pack()
link_gesture_drawing.bind("<Button-1>",
lambda e: self.open_link("https://cubebrush.co/advanches/products/d9q6yq/gesturedrawing"))
link_shikiryu = customtkinter.CTkLabel(self.window, text="by Shikiryu")
link_shikiryu.pack()
link_shikiryu.bind("<Button-1>", lambda e: self.open_link("https://shikiryu.com"))
@staticmethod
def open_link(url):
webbrowser.open_new(url)
def on_closing(self):
self.window.destroy()

View File

@@ -1,11 +1,15 @@
import tkinter
from pathlib import Path from pathlib import Path
from tkinter import * from tkinter import *
from tkinter import filedialog from tkinter import filedialog
import customtkinter import customtkinter
from src.entity.config import Config from src.entity.config import Config
from src.entity.generator import Generator
from src.util import Util from src.util import Util
from src.window.about import AboutWindow
from src.window.image import ImageWindow from src.window.image import ImageWindow
from src.window.session import SessionWindow from src.window.session import SessionWindow
from CTkMenuBar import *
class App: class App:
@@ -21,6 +25,17 @@ class App:
self.selected_folder = "" self.selected_folder = ""
self.found_images = [] self.found_images = []
self.timer = 0 self.timer = 0
menu = CTkMenuBar(master=root)
generate_button = menu.add_cascade("Generate")
help_button = menu.add_cascade("Help")
dropdown = CustomDropdownMenu(widget=generate_button)
dropdown.add_option(option="Poses", command=lambda:self.download_poses('poses'))
about = CustomDropdownMenu(widget=help_button)
about.add_option(option="Γ€ propos", command=self.show_help)
self.title = customtkinter.CTkLabel(root, text="Drawing Training") self.title = customtkinter.CTkLabel(root, text="Drawing Training")
self.title.pack() self.title.pack()
@@ -33,8 +48,8 @@ class App:
self.images_len = customtkinter.CTkLabel(root, text="Found : " + str(len(self.found_images))) self.images_len = customtkinter.CTkLabel(root, text="Found : " + str(len(self.found_images)))
self.images_len.pack() self.images_len.pack()
self.button_frame = Frame(root) self.button_frame = Frame(root, bg=root.cget("bg"))
self.button_frame.pack(fill=X) self.button_frame.pack(fill=X, expand=True)
timers = [ timers = [
Util.format_seconds(30), Util.format_seconds(30),
@@ -45,14 +60,16 @@ class App:
Util.format_seconds(600), Util.format_seconds(600),
"Custom" "Custom"
] ]
self.buttons = customtkinter.CTkSegmentedButton(
root, values=timers, self.buttons = []
command=self.set_timer_in_seconds for i, timer in enumerate(timers):
) new_button = customtkinter.CTkButton(self.button_frame, width=10, text=timer,
self.buttons.pack(fill=X) command=lambda t=timer: self.set_timer_in_seconds(t))
new_button.pack(side=tkinter.LEFT, fill=X, expand=True)
self.buttons.append(new_button)
self.launch_button = customtkinter.CTkButton(root, text="Let's draw!", command=self.lets_draw, state="disabled") self.launch_button = customtkinter.CTkButton(root, text="Let's draw!", command=self.lets_draw, state="disabled")
self.launch_button.pack(side="bottom") self.launch_button.pack(side=tkinter.BOTTOM)
self.read_config() self.read_config()
@@ -64,6 +81,9 @@ class App:
except Exception: except Exception:
pass pass
def show_help(self):
AboutWindow(self)
def lets_draw(self): def lets_draw(self):
self.image_window = ImageWindow(self) self.image_window = ImageWindow(self)
self.image_window.lets_draw(self.found_images.copy(), self.timer) self.image_window.lets_draw(self.found_images.copy(), self.timer)
@@ -73,12 +93,18 @@ class App:
self.session_window.open() self.session_window.open()
def select_folder(self): def select_folder(self):
self.selected_folder = filedialog.askdirectory() new_directory = filedialog.askdirectory()
if new_directory:
self.selected_folder = new_directory
self.folder_name.configure(text="Folder : " + self.selected_folder) self.folder_name.configure(text="Folder : " + self.selected_folder)
Config.set_config_var("default_folder", self.selected_folder).save() Config.set_config_var("default_folder", self.selected_folder).save()
self.find_images_in_folder() self.find_images_in_folder()
self.check_lets_draw() self.check_lets_draw()
def download_poses(self, generator=None):
Generator(generator=generator, folder=self.selected_folder)
self.find_images_in_folder()
def find_images_in_folder(self): def find_images_in_folder(self):
if Path(self.selected_folder).exists(): if Path(self.selected_folder).exists():
self.found_images = list( self.found_images = list(
@@ -93,6 +119,11 @@ class App:
self.launch_button.configure(state="disabled") self.launch_button.configure(state="disabled")
def set_timer_in_seconds(self, user_data): def set_timer_in_seconds(self, user_data):
# resetting buttons states
for button in self.buttons:
button.configure(fg_color=(Util.get_default_button_color() if user_data != button.cget(
"text") else Util.get_default_active_button_color()))
self.custom = False self.custom = False
if user_data == "Custom": if user_data == "Custom":
return self.custom_session() return self.custom_session()

View File

@@ -45,14 +45,14 @@ class SessionWindow:
text=session.number_of_drawings + " drawings of " + Util.format_seconds(int(session.timer)) + " each", text=session.number_of_drawings + " drawings of " + Util.format_seconds(int(session.timer)) + " each",
command=partial(self.edit_element, i), command=partial(self.edit_element, i),
fg_color=( fg_color=(
Util.get_default_button_color() if i == self.selected_element_index else Util.get_default_active_button_color()) Util.get_default_active_button_color() if i == self.selected_element_index else Util.get_default_button_color())
) )
elif isinstance(session, BreakElement): elif isinstance(session, BreakElement):
new_session = CTkButton(self.left_column, new_session = CTkButton(self.left_column,
text="Break of " + Util.format_seconds(int(session.timer)) + " each", text="Break of " + Util.format_seconds(int(session.timer)) + " each",
command=partial(self.edit_element, i), command=partial(self.edit_element, i),
fg_color=( fg_color=(
Util.get_default_button_color() if i == self.selected_element_index else Util.get_default_active_button_color()) Util.get_default_active_button_color() if i == self.selected_element_index else Util.get_default_button_color())
) )
else: else:
raise Exception("unknown type.") raise Exception("unknown type.")