feat: mission/commissions/event
136
MCE/config.json
Normal file
@ -0,0 +1,136 @@
|
||||
{
|
||||
"ResetDaily": true,
|
||||
"LastRun": "2023-12-24 21:41:55",
|
||||
"ResetTime": "20:21:30",
|
||||
"RechargeAP": true,
|
||||
"PreferredTemplate": "commissions",
|
||||
"Queue": [
|
||||
[
|
||||
"N",
|
||||
"7-2",
|
||||
17
|
||||
],
|
||||
[
|
||||
"H",
|
||||
"7-3",
|
||||
6
|
||||
],
|
||||
[
|
||||
"BD",
|
||||
"01",
|
||||
1
|
||||
],
|
||||
[
|
||||
"N",
|
||||
"7-2",
|
||||
1
|
||||
],
|
||||
[
|
||||
"IR",
|
||||
"02",
|
||||
1
|
||||
],
|
||||
[
|
||||
"N",
|
||||
"7-2",
|
||||
1
|
||||
],
|
||||
[
|
||||
"N",
|
||||
"7-3",
|
||||
1
|
||||
]
|
||||
],
|
||||
"Event": false,
|
||||
"Templates": {
|
||||
"template1": [
|
||||
[
|
||||
"H",
|
||||
"5-1",
|
||||
3
|
||||
],
|
||||
[
|
||||
"H",
|
||||
"4-3",
|
||||
3
|
||||
],
|
||||
[
|
||||
"H",
|
||||
"5-3",
|
||||
3
|
||||
],
|
||||
[
|
||||
"H",
|
||||
"3-3",
|
||||
3
|
||||
],
|
||||
[
|
||||
"H",
|
||||
"2-2",
|
||||
3
|
||||
],
|
||||
[
|
||||
"H",
|
||||
"1-1",
|
||||
3
|
||||
],
|
||||
[
|
||||
"H",
|
||||
"1-3",
|
||||
3
|
||||
],
|
||||
[
|
||||
"E",
|
||||
"08",
|
||||
30
|
||||
],
|
||||
[
|
||||
"E",
|
||||
"08",
|
||||
30
|
||||
],
|
||||
[
|
||||
"E",
|
||||
"30",
|
||||
30
|
||||
]
|
||||
],
|
||||
"commissions": [
|
||||
[
|
||||
"N",
|
||||
"7-2",
|
||||
20
|
||||
],
|
||||
[
|
||||
"H",
|
||||
"7-3",
|
||||
6
|
||||
],
|
||||
[
|
||||
"BD",
|
||||
"01",
|
||||
1
|
||||
],
|
||||
[
|
||||
"N",
|
||||
"7-2",
|
||||
1
|
||||
],
|
||||
[
|
||||
"IR",
|
||||
"02",
|
||||
1
|
||||
],
|
||||
[
|
||||
"N",
|
||||
"7-2",
|
||||
1
|
||||
],
|
||||
[
|
||||
"N",
|
||||
"7-3",
|
||||
1
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
0
MCE/custom_widgets/__init__.py
Normal file
95
MCE/custom_widgets/ctk_integerspinbox.py
Normal file
@ -0,0 +1,95 @@
|
||||
import customtkinter
|
||||
import re
|
||||
from typing import Callable
|
||||
|
||||
class CTkIntegerSpinbox(customtkinter.CTkFrame):
|
||||
def __init__(self, *args,
|
||||
width: int = 100,
|
||||
height: int = 32,
|
||||
step_size: int = 1,
|
||||
min_value: int = 0,
|
||||
command: Callable = None,
|
||||
**kwargs):
|
||||
super().__init__(*args, width=width, height=height, **kwargs)
|
||||
|
||||
self.step_size = step_size
|
||||
self.min_value = min_value
|
||||
self.command = command
|
||||
self.after_id = None
|
||||
|
||||
self.grid_columnconfigure((0, 2), weight=0)
|
||||
self.grid_columnconfigure(1, weight=1)
|
||||
|
||||
self.subtract_button = customtkinter.CTkButton(self, text="-", width=height-6, height=height-6)
|
||||
self.subtract_button.grid(row=0, column=0, padx=(3, 0), pady=3)
|
||||
self.subtract_button.bind('<ButtonPress-1>', self.start_decrementing)
|
||||
self.subtract_button.bind('<ButtonRelease-1>', self.stop_decrementing)
|
||||
|
||||
self.entry = customtkinter.CTkEntry(self, width=width-(2*height), height=height-6, border_width=0)
|
||||
self.entry.grid(row=0, column=1, columnspan=1, padx=3, pady=3, sticky="ew")
|
||||
|
||||
self.add_button = customtkinter.CTkButton(self, text="+", width=height-6, height=height-6)
|
||||
self.add_button.grid(row=0, column=2, padx=(0, 3), pady=3)
|
||||
self.add_button.bind('<ButtonPress-1>', self.start_incrementing)
|
||||
self.add_button.bind('<ButtonRelease-1>', self.stop_incrementing)
|
||||
|
||||
self.entry.insert(0, "0")
|
||||
|
||||
# Configure validatecommand to allow only integers
|
||||
vcmd = (self.entry.register(self.validate_input), '%P')
|
||||
self.entry.configure(validate='key', validatecommand=vcmd)
|
||||
|
||||
def start_incrementing(self, event):
|
||||
self.increment()
|
||||
self.after_id = self.after(150, self.start_incrementing, event)
|
||||
|
||||
def stop_incrementing(self, event):
|
||||
if self.after_id:
|
||||
self.after_cancel(self.after_id)
|
||||
self.after_id = None
|
||||
|
||||
def start_decrementing(self, event):
|
||||
self.decrement()
|
||||
self.after_id = self.after(150, self.start_decrementing, event)
|
||||
|
||||
def stop_decrementing(self, event):
|
||||
if self.after_id:
|
||||
self.after_cancel(self.after_id)
|
||||
self.after_id = None
|
||||
|
||||
def increment(self):
|
||||
value = int(self.entry.get()) + self.step_size
|
||||
self.entry.delete(0, "end")
|
||||
self.entry.insert(0, max(self.min_value, value)) # Ensure the value is not less than 1
|
||||
if self.command is not None:
|
||||
self.command()
|
||||
|
||||
def decrement(self):
|
||||
value = int(self.entry.get()) - self.step_size
|
||||
self.entry.delete(0, "end")
|
||||
self.entry.insert(0, max(self.min_value, value)) # Ensure the value is not less than 0
|
||||
if self.command is not None:
|
||||
self.command()
|
||||
|
||||
def validate_input(self, new_value):
|
||||
# Validate that the input is a non-negative integer
|
||||
return re.match(r'^\d*$', new_value) is not None
|
||||
|
||||
def get(self) -> int:
|
||||
try:
|
||||
return int(self.entry.get())
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
def set(self, value: int):
|
||||
self.entry.delete(0, "end")
|
||||
self.entry.insert(0, max(self.min_value, value)) # Ensure the value is not less than 0
|
||||
|
||||
def configure(self, **kwargs):
|
||||
state = kwargs.get("state", None)
|
||||
if state is not None:
|
||||
self.subtract_button.configure(state=state)
|
||||
self.add_button.configure(state=state)
|
||||
self.entry.configure(state=state)
|
||||
kwargs.pop("state")
|
||||
super().configure(**kwargs)
|
||||
27
MCE/custom_widgets/ctk_notification.py
Normal file
@ -0,0 +1,27 @@
|
||||
import customtkinter
|
||||
class CTkNotification(customtkinter.CTkFrame):
|
||||
def __init__(self, text, master, **kwargs):
|
||||
self.master_color = master.cget("fg_color")
|
||||
super().__init__(master=master, **kwargs, fg_color=self.master_color)
|
||||
self.label = customtkinter.CTkLabel(self, text=text, text_color=self.master_color, width=200, wraplength=200, font=("Inter", 16))
|
||||
self.label.grid(row=0, column=0, sticky="nsew")
|
||||
self.close_button = customtkinter.CTkButton(
|
||||
self, width=40, text="X", text_color_disabled=self.master_color, command=self.hide, fg_color="transparent", state="disabled")
|
||||
self.close_button.grid(row=0, column=1)
|
||||
self.progress_bar = customtkinter.CTkProgressBar(self, determinate_speed=0.4, fg_color=self.master_color, progress_color=self.master_color)
|
||||
self.progress_bar.grid(row=1, column=0, columnspan=2, sticky="nsew")
|
||||
|
||||
def hide(self):
|
||||
self.configure(fg_color="transparent")
|
||||
self.progress_bar.stop()
|
||||
self.progress_bar.configure(progress_color=self.master_color)
|
||||
self.close_button.configure(state="disabled")
|
||||
self.label.configure(text_color=self.master_color)
|
||||
|
||||
def show(self):
|
||||
self.configure(fg_color="green")
|
||||
self.progress_bar.configure(progress_color="white")
|
||||
self.progress_bar.set(0)
|
||||
self.progress_bar.start()
|
||||
self.close_button.configure(state="normal")
|
||||
self.label.configure(text_color="white")
|
||||
133
MCE/custom_widgets/ctk_templatedialog.py
Normal file
@ -0,0 +1,133 @@
|
||||
from typing import Union, Tuple, Optional
|
||||
from customtkinter import CTkLabel, CTkEntry, CTkButton, ThemeManager, CTkToplevel, CTkFont, CTkOptionMenu
|
||||
|
||||
|
||||
class CTkTemplateDialog(CTkToplevel):
|
||||
"""
|
||||
Dialog with extra window, message, entry widget, cancel and ok button.
|
||||
For detailed information check out the documentation.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
|
||||
text_color: Optional[Union[str, Tuple[str, str]]] = None,
|
||||
button_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
|
||||
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
|
||||
button_text_color: Optional[Union[str, Tuple[str, str]]] = None,
|
||||
entry_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
|
||||
entry_border_color: Optional[Union[str, Tuple[str, str]]] = None,
|
||||
entry_text_color: Optional[Union[str, Tuple[str, str]]] = None,
|
||||
|
||||
title: str = "CTkDialog",
|
||||
font: Optional[Union[tuple, CTkFont]] = None,
|
||||
text: str = "CTkDialog",
|
||||
values = []):
|
||||
|
||||
super().__init__(fg_color=fg_color)
|
||||
|
||||
self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
|
||||
self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(button_hover_color)
|
||||
self._button_fg_color = ThemeManager.theme["CTkButton"]["fg_color"] if button_fg_color is None else self._check_color_type(button_fg_color)
|
||||
self._button_hover_color = ThemeManager.theme["CTkButton"]["hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
|
||||
self._button_text_color = ThemeManager.theme["CTkButton"]["text_color"] if button_text_color is None else self._check_color_type(button_text_color)
|
||||
self._entry_fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if entry_fg_color is None else self._check_color_type(entry_fg_color)
|
||||
self._entry_border_color = ThemeManager.theme["CTkEntry"]["border_color"] if entry_border_color is None else self._check_color_type(entry_border_color)
|
||||
self._entry_text_color = ThemeManager.theme["CTkEntry"]["text_color"] if entry_text_color is None else self._check_color_type(entry_text_color)
|
||||
|
||||
self._user_input = ("", "")
|
||||
self._running: bool = False
|
||||
self._title = title
|
||||
self._text = text
|
||||
self._font = font
|
||||
self._values = [""] + values
|
||||
|
||||
self.title(self._title)
|
||||
self.lift() # lift window on top
|
||||
self.attributes("-topmost", True) # stay on top
|
||||
self.protocol("WM_DELETE_WINDOW", self._on_closing)
|
||||
self.after(10, self._create_widgets) # create widgets with slight delay, to avoid white flickering of background
|
||||
self.resizable(False, False)
|
||||
self.grab_set() # make other windows not clickable
|
||||
|
||||
def _create_widgets(self):
|
||||
self.grid_columnconfigure((0, 1), weight=1)
|
||||
self.rowconfigure(0, weight=1)
|
||||
|
||||
self._label = CTkLabel(master=self,
|
||||
width=300,
|
||||
wraplength=300,
|
||||
fg_color="transparent",
|
||||
text_color=self._text_color,
|
||||
text=self._text,
|
||||
font=self._font)
|
||||
self._label.grid(row=0, column=0, columnspan=2, padx=20, pady=20, sticky="ew")
|
||||
|
||||
self._entry = CTkEntry(master=self,
|
||||
width=230,
|
||||
fg_color=self._entry_fg_color,
|
||||
border_color=self._entry_border_color,
|
||||
text_color=self._entry_text_color,
|
||||
font=self._font)
|
||||
self._entry.grid(row=1, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="ew")
|
||||
|
||||
self._label2 = CTkLabel(master=self,
|
||||
width=100,
|
||||
wraplength=100,
|
||||
fg_color="transparent",
|
||||
text_color=self._text_color,
|
||||
text="Import stages from: ",
|
||||
font=self._font)
|
||||
self._label2.grid(row=2, column=0, columnspan=1, padx=(20, 10), pady=(0, 20), sticky="ew")
|
||||
|
||||
self._template_optionmenu = CTkOptionMenu(master=self,
|
||||
width=100,
|
||||
fg_color=self._button_fg_color,
|
||||
button_hover_color=self._button_hover_color,
|
||||
text_color=self._button_text_color,
|
||||
font=self._font,
|
||||
values=self._values
|
||||
)
|
||||
self._template_optionmenu.grid(row=2, column=1, columnspan=1, padx=(10, 20), pady=(0, 20), sticky="ew")
|
||||
|
||||
|
||||
self._ok_button = CTkButton(master=self,
|
||||
width=100,
|
||||
border_width=0,
|
||||
fg_color=self._button_fg_color,
|
||||
hover_color=self._button_hover_color,
|
||||
text_color=self._button_text_color,
|
||||
text='Ok',
|
||||
font=self._font,
|
||||
command=self._ok_event)
|
||||
self._ok_button.grid(row=3, column=0, columnspan=1, padx=(20, 10), pady=(0, 20), sticky="ew")
|
||||
|
||||
self._cancel_button = CTkButton(master=self,
|
||||
width=100,
|
||||
border_width=0,
|
||||
fg_color=self._button_fg_color,
|
||||
hover_color=self._button_hover_color,
|
||||
text_color=self._button_text_color,
|
||||
text='Cancel',
|
||||
font=self._font,
|
||||
command=self._cancel_event)
|
||||
self._cancel_button.grid(row=3, column=1, columnspan=1, padx=(10, 20), pady=(0, 20), sticky="ew")
|
||||
|
||||
self.after(150, lambda: self._entry.focus()) # set focus to entry with slight delay, otherwise it won't work
|
||||
self._entry.bind("<Return>", self._ok_event)
|
||||
|
||||
def _ok_event(self, event=None):
|
||||
self._user_input = self._entry.get(), self._template_optionmenu.get()
|
||||
self.grab_release()
|
||||
self.destroy()
|
||||
|
||||
def _on_closing(self):
|
||||
self.grab_release()
|
||||
self.destroy()
|
||||
|
||||
def _cancel_event(self):
|
||||
self.grab_release()
|
||||
self.destroy()
|
||||
|
||||
def get_input(self):
|
||||
self.master.wait_window(self)
|
||||
return self._user_input
|
||||
36
MCE/custom_widgets/ctk_timeentry.py
Normal file
@ -0,0 +1,36 @@
|
||||
import customtkinter
|
||||
import tkinter as tk
|
||||
|
||||
class CTkTimeEntry(customtkinter.CTkFrame):
|
||||
def __init__(self, master=None, **kwargs):
|
||||
super().__init__(master, **kwargs)
|
||||
self.hour = tk.StringVar()
|
||||
self.minute = tk.StringVar()
|
||||
self.second = tk.StringVar()
|
||||
|
||||
self.hour_entry = customtkinter.CTkEntry(self, width=50, textvariable=self.hour, validate="key", validatecommand=(self.register(self.validate_hour), '%P'))
|
||||
self.hour_entry.pack(side=tk.LEFT)
|
||||
|
||||
self.minute_entry = customtkinter.CTkEntry(self,width=50, textvariable=self.minute, validate="key", validatecommand=(self.register(self.validate_min_sec), '%P'))
|
||||
self.minute_entry.pack(side=tk.LEFT)
|
||||
|
||||
self.second_entry = customtkinter.CTkEntry(self, width=50, textvariable=self.second, validate="key", validatecommand=(self.register(self.validate_min_sec), '%P'))
|
||||
self.second_entry.pack(side=tk.LEFT)
|
||||
|
||||
def validate_hour(self, P):
|
||||
return len(P) <= 2 and (P.isdigit() and int(P) <= 23 or P == "")
|
||||
|
||||
def validate_min_sec(self, P):
|
||||
return len(P) <= 2 and (P.isdigit() and int(P) <= 59 or P == "")
|
||||
|
||||
def set(self, time_str):
|
||||
h, m, s = map(str, time_str.split(':'))
|
||||
self.hour.set(h)
|
||||
self.minute.set(m)
|
||||
self.second.set(s)
|
||||
|
||||
def get(self):
|
||||
h = self.hour.get() if self.hour.get() else "00"
|
||||
m = self.minute.get() if self.minute.get() else "00"
|
||||
s = self.second.get() if self.second.get() else "00"
|
||||
return f"{h.zfill(2)}:{m.zfill(2)}:{s.zfill(2)}"
|
||||
212
MCE/custom_widgets/ctk_tooltip.py
Normal file
@ -0,0 +1,212 @@
|
||||
"""
|
||||
CTkToolTip Widget
|
||||
version: 0.8
|
||||
"""
|
||||
|
||||
import time
|
||||
import sys
|
||||
import customtkinter
|
||||
from tkinter import Toplevel, Frame
|
||||
|
||||
|
||||
class CTkToolTip(Toplevel):
|
||||
"""
|
||||
Creates a ToolTip (pop-up) widget for customtkinter.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
widget: any = None,
|
||||
message: str = None,
|
||||
delay: float = 0.2,
|
||||
follow: bool = True,
|
||||
x_offset: int = +20,
|
||||
y_offset: int = +10,
|
||||
bg_color: str = None,
|
||||
corner_radius: int = 10,
|
||||
border_width: int = 0,
|
||||
border_color: str = None,
|
||||
alpha: float = 0.95,
|
||||
padding: tuple = (10, 2),
|
||||
**message_kwargs):
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.widget = widget
|
||||
|
||||
self.withdraw()
|
||||
|
||||
# Disable ToolTip's title bar
|
||||
self.overrideredirect(True)
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
self.transparent_color = self.widget._apply_appearance_mode(
|
||||
customtkinter.ThemeManager.theme["CTkToplevel"]["fg_color"])
|
||||
self.attributes("-transparentcolor", self.transparent_color)
|
||||
self.transient()
|
||||
elif sys.platform.startswith("darwin"):
|
||||
self.transparent_color = 'systemTransparent'
|
||||
self.attributes("-transparent", True)
|
||||
self.transient(self.master)
|
||||
else:
|
||||
self.transparent_color = '#000001'
|
||||
corner_radius = 0
|
||||
self.transient()
|
||||
|
||||
self.resizable(width=True, height=True)
|
||||
|
||||
# Make the background transparent
|
||||
self.config(background=self.transparent_color)
|
||||
|
||||
# StringVar instance for msg string
|
||||
self.messageVar = customtkinter.StringVar()
|
||||
self.message = message
|
||||
self.messageVar.set(self.message)
|
||||
|
||||
self.delay = delay
|
||||
self.follow = follow
|
||||
self.x_offset = x_offset
|
||||
self.y_offset = y_offset
|
||||
self.corner_radius = corner_radius
|
||||
self.alpha = alpha
|
||||
self.border_width = border_width
|
||||
self.padding = padding
|
||||
self.bg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if bg_color is None else bg_color
|
||||
self.border_color = border_color
|
||||
self.disable = False
|
||||
|
||||
# visibility status of the ToolTip inside|outside|visible
|
||||
self.status = "outside"
|
||||
self.last_moved = 0
|
||||
self.attributes('-alpha', self.alpha)
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
if self.widget._apply_appearance_mode(self.bg_color) == self.transparent_color:
|
||||
self.transparent_color = "#000001"
|
||||
self.config(background=self.transparent_color)
|
||||
self.attributes("-transparentcolor", self.transparent_color)
|
||||
|
||||
# Add the message widget inside the tooltip
|
||||
self.transparent_frame = Frame(self, bg=self.transparent_color)
|
||||
self.transparent_frame.pack(padx=0, pady=0, fill="both", expand=True)
|
||||
|
||||
self.frame = customtkinter.CTkFrame(self.transparent_frame, bg_color=self.transparent_color,
|
||||
corner_radius=self.corner_radius,
|
||||
border_width=self.border_width, fg_color=self.bg_color,
|
||||
border_color=self.border_color)
|
||||
self.frame.pack(padx=0, pady=0, fill="both", expand=True)
|
||||
|
||||
self.message_label = customtkinter.CTkLabel(self.frame, textvariable=self.messageVar, **message_kwargs)
|
||||
self.message_label.pack(fill="both", padx=self.padding[0] + self.border_width,
|
||||
pady=self.padding[1] + self.border_width, expand=True)
|
||||
|
||||
if self.widget.winfo_name() != "tk":
|
||||
if self.frame.cget("fg_color") == self.widget.cget("bg_color"):
|
||||
if not bg_color:
|
||||
self._top_fg_color = self.frame._apply_appearance_mode(
|
||||
customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"])
|
||||
if self._top_fg_color != self.transparent_color:
|
||||
self.frame.configure(fg_color=self._top_fg_color)
|
||||
|
||||
# Add bindings to the widget without overriding the existing ones
|
||||
self.widget.bind("<Enter>", self.on_enter, add="+")
|
||||
self.widget.bind("<Leave>", self.on_leave, add="+")
|
||||
self.widget.bind("<Motion>", self.on_enter, add="+")
|
||||
self.widget.bind("<B1-Motion>", self.on_enter, add="+")
|
||||
self.widget.bind("<Destroy>", lambda _: self.hide(), add="+")
|
||||
|
||||
def show(self) -> None:
|
||||
"""
|
||||
Enable the widget.
|
||||
"""
|
||||
self.disable = False
|
||||
|
||||
def on_enter(self, event) -> None:
|
||||
"""
|
||||
Processes motion within the widget including entering and moving.
|
||||
"""
|
||||
|
||||
if self.disable:
|
||||
return
|
||||
self.last_moved = time.time()
|
||||
|
||||
# Set the status as inside for the very first time
|
||||
if self.status == "outside":
|
||||
self.status = "inside"
|
||||
|
||||
# If the follow flag is not set, motion within the widget will make the ToolTip dissapear
|
||||
if not self.follow:
|
||||
self.status = "inside"
|
||||
self.withdraw()
|
||||
|
||||
# Calculate available space on the right side of the widget relative to the screen
|
||||
root_width = self.winfo_screenwidth()
|
||||
widget_x = event.x_root
|
||||
space_on_right = root_width - widget_x
|
||||
|
||||
# Calculate the width of the tooltip's text based on the length of the message string
|
||||
text_width = self.message_label.winfo_reqwidth()
|
||||
|
||||
# Calculate the offset based on available space and text width to avoid going off-screen on the right side
|
||||
offset_x = self.x_offset
|
||||
if space_on_right < text_width + 20: # Adjust the threshold as needed
|
||||
offset_x = -text_width - 20 # Negative offset when space is limited on the right side
|
||||
|
||||
# Offsets the ToolTip using the coordinates od an event as an origin
|
||||
self.geometry(f"+{event.x_root + offset_x}+{event.y_root + self.y_offset}")
|
||||
|
||||
# Time is in integer: milliseconds
|
||||
self.after(int(self.delay * 1000), self._show)
|
||||
|
||||
def on_leave(self, event=None) -> None:
|
||||
"""
|
||||
Hides the ToolTip temporarily.
|
||||
"""
|
||||
|
||||
if self.disable: return
|
||||
self.status = "outside"
|
||||
self.withdraw()
|
||||
|
||||
def _show(self) -> None:
|
||||
"""
|
||||
Displays the ToolTip.
|
||||
"""
|
||||
|
||||
if not self.widget.winfo_exists():
|
||||
self.hide()
|
||||
self.destroy()
|
||||
|
||||
if self.status == "inside" and time.time() - self.last_moved >= self.delay:
|
||||
self.status = "visible"
|
||||
self.deiconify()
|
||||
|
||||
def hide(self) -> None:
|
||||
"""
|
||||
Disable the widget from appearing.
|
||||
"""
|
||||
if not self.winfo_exists():
|
||||
return
|
||||
self.withdraw()
|
||||
self.disable = True
|
||||
|
||||
def is_disabled(self) -> None:
|
||||
"""
|
||||
Return the window state
|
||||
"""
|
||||
return self.disable
|
||||
|
||||
def get(self) -> None:
|
||||
"""
|
||||
Returns the text on the tooltip.
|
||||
"""
|
||||
return self.messageVar.get()
|
||||
|
||||
def configure(self, message: str = None, delay: float = None, bg_color: str = None, **kwargs):
|
||||
"""
|
||||
Set new message or configure the label parameters.
|
||||
"""
|
||||
if delay: self.delay = delay
|
||||
if bg_color: self.frame.configure(fg_color=bg_color)
|
||||
|
||||
self.messageVar.set(message)
|
||||
self.message_label.configure(**kwargs)
|
||||
448
MCE/custom_widgets/ctkmessagebox.py
Normal file
@ -0,0 +1,448 @@
|
||||
"""
|
||||
CustomTkinter Messagebox
|
||||
Author: Akash Bora
|
||||
Version: 2.5
|
||||
"""
|
||||
|
||||
import customtkinter
|
||||
from PIL import Image, ImageTk
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from typing import Literal
|
||||
|
||||
class CTkMessagebox(customtkinter.CTkToplevel):
|
||||
ICONS = {
|
||||
"check": None,
|
||||
"cancel": None,
|
||||
"info": None,
|
||||
"question": None,
|
||||
"warning": None
|
||||
}
|
||||
ICON_BITMAP = {}
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
width: int = 400,
|
||||
height: int = 200,
|
||||
title: str = "CTkMessagebox",
|
||||
message: str = "This is a CTkMessagebox!",
|
||||
option_1: str = "OK",
|
||||
option_2: str = None,
|
||||
option_3: str = None,
|
||||
options: list = [],
|
||||
border_width: int = 1,
|
||||
border_color: str = "default",
|
||||
button_color: str = "default",
|
||||
bg_color: str = "default",
|
||||
fg_color: str = "default",
|
||||
text_color: str = "default",
|
||||
title_color: str = "default",
|
||||
button_text_color: str = "default",
|
||||
button_width: int = None,
|
||||
button_height: int = None,
|
||||
cancel_button_color: str = None,
|
||||
cancel_button: str = None, # types: circle, cross or none
|
||||
button_hover_color: str = "default",
|
||||
icon: str = "info",
|
||||
icon_size: tuple = None,
|
||||
corner_radius: int = 15,
|
||||
justify: str = "right",
|
||||
font: tuple = None,
|
||||
header: bool = False,
|
||||
topmost: bool = True,
|
||||
fade_in_duration: int = 0,
|
||||
sound: bool = False,
|
||||
option_focus: Literal[1, 2, 3] = None):
|
||||
|
||||
super().__init__()
|
||||
|
||||
|
||||
self.master_window = master
|
||||
|
||||
self.width = 250 if width<250 else width
|
||||
self.height = 150 if height<150 else height
|
||||
|
||||
if self.master_window is None:
|
||||
self.spawn_x = int((self.winfo_screenwidth()-self.width)/2)
|
||||
self.spawn_y = int((self.winfo_screenheight()-self.height)/2)
|
||||
else:
|
||||
self.spawn_x = int(self.master_window.winfo_width() * .5 + self.master_window.winfo_x() - .5 * self.width + 7)
|
||||
self.spawn_y = int(self.master_window.winfo_height() * .5 + self.master_window.winfo_y() - .5 * self.height + 20)
|
||||
|
||||
self.after(10)
|
||||
self.geometry(f"{self.width}x{self.height}+{self.spawn_x}+{self.spawn_y}")
|
||||
self.title(title)
|
||||
self.resizable(width=False, height=False)
|
||||
self.fade = fade_in_duration
|
||||
|
||||
if self.fade:
|
||||
self.fade = 20 if self.fade<20 else self.fade
|
||||
self.attributes("-alpha", 0)
|
||||
|
||||
if not header:
|
||||
self.overrideredirect(1)
|
||||
|
||||
if topmost:
|
||||
self.attributes("-topmost", True)
|
||||
else:
|
||||
self.transient(self.master_window)
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
self.transparent_color = self._apply_appearance_mode(self.cget("fg_color"))
|
||||
self.attributes("-transparentcolor", self.transparent_color)
|
||||
default_cancel_button = "cross"
|
||||
elif sys.platform.startswith("darwin"):
|
||||
self.transparent_color = 'systemTransparent'
|
||||
self.attributes("-transparent", True)
|
||||
default_cancel_button = "circle"
|
||||
else:
|
||||
self.transparent_color = '#000001'
|
||||
corner_radius = 0
|
||||
default_cancel_button = "cross"
|
||||
|
||||
self.lift()
|
||||
|
||||
self.config(background=self.transparent_color)
|
||||
self.protocol("WM_DELETE_WINDOW", self.button_event)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
self.x = self.winfo_x()
|
||||
self.y = self.winfo_y()
|
||||
self._title = title
|
||||
self.message = message
|
||||
self.font = font
|
||||
self.justify = justify
|
||||
self.sound = sound
|
||||
self.cancel_button = cancel_button if cancel_button else default_cancel_button
|
||||
self.round_corners = corner_radius if corner_radius<=30 else 30
|
||||
self.button_width = button_width if button_width else self.width/4
|
||||
self.button_height = button_height if button_height else 28
|
||||
|
||||
if self.fade: self.attributes("-alpha", 0)
|
||||
|
||||
if self.button_height>self.height/4: self.button_height = self.height/4 -20
|
||||
self.dot_color = cancel_button_color
|
||||
self.border_width = border_width if border_width<6 else 5
|
||||
|
||||
if type(options) is list and len(options)>0:
|
||||
try:
|
||||
option_1 = options[-1]
|
||||
option_2 = options[-2]
|
||||
option_3 = options[-3]
|
||||
except IndexError: None
|
||||
|
||||
if bg_color=="default":
|
||||
self.bg_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"])
|
||||
else:
|
||||
self.bg_color = bg_color
|
||||
|
||||
if fg_color=="default":
|
||||
self.fg_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"])
|
||||
else:
|
||||
self.fg_color = fg_color
|
||||
|
||||
default_button_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["fg_color"])
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
if self.bg_color==self.transparent_color or self.fg_color==self.transparent_color:
|
||||
self.configure(fg_color="#000001")
|
||||
self.transparent_color = "#000001"
|
||||
self.attributes("-transparentcolor", self.transparent_color)
|
||||
|
||||
if button_color=="default":
|
||||
self.button_color = (default_button_color, default_button_color, default_button_color)
|
||||
else:
|
||||
if type(button_color) is tuple:
|
||||
if len(button_color)==2:
|
||||
self.button_color = (button_color[0], button_color[1], default_button_color)
|
||||
elif len(button_color)==1:
|
||||
self.button_color = (button_color[0], default_button_color, default_button_color)
|
||||
else:
|
||||
self.button_color = button_color
|
||||
else:
|
||||
self.button_color = (button_color, button_color, button_color)
|
||||
|
||||
if text_color=="default":
|
||||
self.text_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkLabel"]["text_color"])
|
||||
else:
|
||||
self.text_color = text_color
|
||||
|
||||
if title_color=="default":
|
||||
self.title_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkLabel"]["text_color"])
|
||||
else:
|
||||
self.title_color = title_color
|
||||
|
||||
if button_text_color=="default":
|
||||
self.bt_text_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["text_color"])
|
||||
else:
|
||||
self.bt_text_color = button_text_color
|
||||
|
||||
if button_hover_color=="default":
|
||||
self.bt_hv_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["hover_color"])
|
||||
else:
|
||||
self.bt_hv_color = button_hover_color
|
||||
|
||||
if border_color=="default":
|
||||
self.border_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["border_color"])
|
||||
else:
|
||||
self.border_color = border_color
|
||||
|
||||
if icon_size:
|
||||
self.size_height = icon_size[1] if icon_size[1]<=self.height-100 else self.height-100
|
||||
self.size = (icon_size[0], self.size_height)
|
||||
else:
|
||||
self.size = (self.height/4, self.height/4)
|
||||
|
||||
self.icon = self.load_icon(icon, icon_size) if icon else None
|
||||
|
||||
self.frame_top = customtkinter.CTkFrame(self, corner_radius=self.round_corners, width=self.width, border_width=self.border_width,
|
||||
bg_color=self.transparent_color, fg_color=self.bg_color, border_color=self.border_color)
|
||||
self.frame_top.grid(sticky="nswe")
|
||||
|
||||
if button_width:
|
||||
self.frame_top.grid_columnconfigure(0, weight=1)
|
||||
else:
|
||||
self.frame_top.grid_columnconfigure((1,2,3), weight=1)
|
||||
|
||||
if button_height:
|
||||
self.frame_top.grid_rowconfigure((0,1,3), weight=1)
|
||||
else:
|
||||
self.frame_top.grid_rowconfigure((0,1,2), weight=1)
|
||||
|
||||
self.frame_top.bind("<B1-Motion>", self.move_window)
|
||||
self.frame_top.bind("<ButtonPress-1>", self.oldxyset)
|
||||
|
||||
if self.cancel_button=="cross":
|
||||
self.button_close = customtkinter.CTkButton(self.frame_top, corner_radius=10, width=0, height=0, hover=False, border_width=0,
|
||||
text_color=self.dot_color if self.dot_color else self.title_color,
|
||||
text="✕", fg_color="transparent", command=self.button_event)
|
||||
self.button_close.grid(row=0, column=5, sticky="ne", padx=5+self.border_width, pady=5+self.border_width)
|
||||
elif self.cancel_button=="circle":
|
||||
self.button_close = customtkinter.CTkButton(self.frame_top, corner_radius=10, width=10, height=10, hover=False, border_width=0,
|
||||
text="", fg_color=self.dot_color if self.dot_color else "#c42b1c", command=self.button_event)
|
||||
self.button_close.grid(row=0, column=5, sticky="ne", padx=10, pady=10)
|
||||
|
||||
self.title_label = customtkinter.CTkLabel(self.frame_top, width=1, text=self._title, text_color=self.title_color, font=self.font)
|
||||
self.title_label.grid(row=0, column=0, columnspan=6, sticky="nw", padx=(15,30), pady=5)
|
||||
self.title_label.bind("<B1-Motion>", self.move_window)
|
||||
self.title_label.bind("<ButtonPress-1>", self.oldxyset)
|
||||
|
||||
self.info = customtkinter.CTkButton(self.frame_top, width=1, height=self.height/2, corner_radius=0, text=self.message, font=self.font,
|
||||
fg_color=self.fg_color, hover=False, text_color=self.text_color, image=self.icon)
|
||||
self.info._text_label.configure(wraplength=self.width/2, justify="left")
|
||||
self.info.grid(row=1, column=0, columnspan=6, sticky="nwes", padx=self.border_width)
|
||||
|
||||
if self.info._text_label.winfo_reqheight()>self.height/2:
|
||||
height_offset = int((self.info._text_label.winfo_reqheight())-(self.height/2) + self.height)
|
||||
self.geometry(f"{self.width}x{height_offset}")
|
||||
|
||||
|
||||
self.option_text_1 = option_1
|
||||
|
||||
self.button_1 = customtkinter.CTkButton(self.frame_top, text=self.option_text_1, fg_color=self.button_color[0],
|
||||
width=self.button_width, font=self.font, text_color=self.bt_text_color,
|
||||
hover_color=self.bt_hv_color, height=self.button_height,
|
||||
command=lambda: self.button_event(self.option_text_1))
|
||||
|
||||
|
||||
self.option_text_2 = option_2
|
||||
if option_2:
|
||||
self.button_2 = customtkinter.CTkButton(self.frame_top, text=self.option_text_2, fg_color=self.button_color[1],
|
||||
width=self.button_width, font=self.font, text_color=self.bt_text_color,
|
||||
hover_color=self.bt_hv_color, height=self.button_height,
|
||||
command=lambda: self.button_event(self.option_text_2))
|
||||
|
||||
self.option_text_3 = option_3
|
||||
if option_3:
|
||||
self.button_3 = customtkinter.CTkButton(self.frame_top, text=self.option_text_3, fg_color=self.button_color[2],
|
||||
width=self.button_width, font=self.font, text_color=self.bt_text_color,
|
||||
hover_color=self.bt_hv_color, height=self.button_height,
|
||||
command=lambda: self.button_event(self.option_text_3))
|
||||
|
||||
if self.justify=="center":
|
||||
if button_width:
|
||||
columns = [4,3,2]
|
||||
span = 1
|
||||
else:
|
||||
columns = [4,2,0]
|
||||
span = 2
|
||||
if option_3:
|
||||
self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1)
|
||||
self.button_1.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(0,10), pady=10)
|
||||
self.button_2.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10)
|
||||
self.button_3.grid(row=2, column=columns[2], columnspan=span, sticky="news", padx=(10,0), pady=10)
|
||||
elif option_2:
|
||||
self.frame_top.columnconfigure((0,5), weight=1)
|
||||
columns = [2,3]
|
||||
self.button_1.grid(row=2, column=columns[0], sticky="news", padx=(0,5), pady=10)
|
||||
self.button_2.grid(row=2, column=columns[1], sticky="news", padx=(5,0), pady=10)
|
||||
else:
|
||||
if button_width:
|
||||
self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1)
|
||||
else:
|
||||
self.frame_top.columnconfigure((0,2,4), weight=2)
|
||||
self.button_1.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=(0,10), pady=10)
|
||||
elif self.justify=="left":
|
||||
self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1)
|
||||
if button_width:
|
||||
columns = [0,1,2]
|
||||
span = 1
|
||||
else:
|
||||
columns = [0,2,4]
|
||||
span = 2
|
||||
if option_3:
|
||||
self.button_1.grid(row=2, column=columns[2], columnspan=span, sticky="news", padx=(0,10), pady=10)
|
||||
self.button_2.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10)
|
||||
self.button_3.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(10,0), pady=10)
|
||||
elif option_2:
|
||||
self.button_1.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10)
|
||||
self.button_2.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(10,0), pady=10)
|
||||
else:
|
||||
self.button_1.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(10,0), pady=10)
|
||||
else:
|
||||
self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1)
|
||||
if button_width:
|
||||
columns = [5,4,3]
|
||||
span = 1
|
||||
else:
|
||||
columns = [4,2,0]
|
||||
span = 2
|
||||
self.button_1.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(0,10), pady=10)
|
||||
if option_2:
|
||||
self.button_2.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10)
|
||||
if option_3:
|
||||
self.button_3.grid(row=2, column=columns[2], columnspan=span, sticky="news", padx=(10,0), pady=10)
|
||||
|
||||
if header:
|
||||
self.title_label.configure(text="")
|
||||
self.title_label.grid_configure(pady=0)
|
||||
self.button_close.configure(text_color=self.bg_color)
|
||||
self.frame_top.configure(corner_radius=0)
|
||||
|
||||
if self.winfo_exists():
|
||||
self.grab_set()
|
||||
|
||||
if self.sound:
|
||||
self.bell()
|
||||
|
||||
if self.fade:
|
||||
self.fade_in()
|
||||
|
||||
if option_focus:
|
||||
self.option_focus = option_focus
|
||||
self.focus_button(self.option_focus)
|
||||
else:
|
||||
if not self.option_text_2 and not self.option_text_3:
|
||||
self.button_1.focus()
|
||||
self.button_1.bind("<Return>", lambda event: self.button_event(self.option_text_1))
|
||||
|
||||
self.bind("<Escape>", lambda e: self.button_event())
|
||||
|
||||
def focus_button(self, option_focus):
|
||||
try:
|
||||
self.selected_button = getattr(self, "button_"+str(option_focus))
|
||||
self.selected_button.focus()
|
||||
self.selected_button.configure(border_color=self.bt_hv_color, border_width=3)
|
||||
self.selected_option = getattr(self, "option_text_"+str(option_focus))
|
||||
self.selected_button.bind("<Return>", lambda event: self.button_event(self.selected_option))
|
||||
except AttributeError:
|
||||
return
|
||||
|
||||
self.bind("<Left>", lambda e: self.change_left())
|
||||
self.bind("<Right>", lambda e: self.change_right())
|
||||
|
||||
def change_left(self):
|
||||
if self.option_focus==3:
|
||||
return
|
||||
|
||||
self.selected_button.unbind("<Return>")
|
||||
self.selected_button.configure(border_width=0)
|
||||
|
||||
if self.option_focus==1:
|
||||
if self.option_text_2:
|
||||
self.option_focus = 2
|
||||
|
||||
elif self.option_focus==2:
|
||||
if self.option_text_3:
|
||||
self.option_focus = 3
|
||||
|
||||
self.focus_button(self.option_focus)
|
||||
|
||||
def change_right(self):
|
||||
if self.option_focus==1:
|
||||
return
|
||||
|
||||
self.selected_button.unbind("<Return>")
|
||||
self.selected_button.configure(border_width=0)
|
||||
|
||||
if self.option_focus==2:
|
||||
self.option_focus = 1
|
||||
|
||||
elif self.option_focus==3:
|
||||
self.option_focus = 2
|
||||
|
||||
self.focus_button(self.option_focus)
|
||||
|
||||
def load_icon(self, icon, icon_size):
|
||||
if icon not in self.ICONS or self.ICONS[icon] is None:
|
||||
if icon in ["check", "cancel", "info", "question", "warning"]:
|
||||
image_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'icons', icon + '.png')
|
||||
else:
|
||||
image_path = icon
|
||||
if icon_size:
|
||||
size_height = icon_size[1] if icon_size[1] <= self.height - 100 else self.height - 100
|
||||
size = (icon_size[0], size_height)
|
||||
else:
|
||||
size = (self.height / 4, self.height / 4)
|
||||
self.ICONS[icon] = customtkinter.CTkImage(Image.open(image_path), size=size)
|
||||
self.ICON_BITMAP[icon] = ImageTk.PhotoImage(file=image_path)
|
||||
self.after(200, lambda: self.iconphoto(False, self.ICON_BITMAP[icon]))
|
||||
return self.ICONS[icon]
|
||||
|
||||
def fade_in(self):
|
||||
for i in range(0,110,10):
|
||||
if not self.winfo_exists():
|
||||
break
|
||||
self.attributes("-alpha", i/100)
|
||||
self.update()
|
||||
time.sleep(1/self.fade)
|
||||
|
||||
def fade_out(self):
|
||||
for i in range(100,0,-10):
|
||||
if not self.winfo_exists():
|
||||
break
|
||||
self.attributes("-alpha", i/100)
|
||||
self.update()
|
||||
time.sleep(1/self.fade)
|
||||
|
||||
def get(self):
|
||||
if self.winfo_exists():
|
||||
self.master.wait_window(self)
|
||||
return self.event
|
||||
|
||||
def oldxyset(self, event):
|
||||
self.oldx = event.x
|
||||
self.oldy = event.y
|
||||
|
||||
def move_window(self, event):
|
||||
self.y = event.y_root - self.oldy
|
||||
self.x = event.x_root - self.oldx
|
||||
self.geometry(f'+{self.x}+{self.y}')
|
||||
|
||||
def button_event(self, event=None):
|
||||
try:
|
||||
self.button_1.configure(state="disabled")
|
||||
self.button_2.configure(state="disabled")
|
||||
self.button_3.configure(state="disabled")
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if self.fade:
|
||||
self.fade_out()
|
||||
self.grab_release()
|
||||
self.destroy()
|
||||
self.event = event
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = CTkMessagebox()
|
||||
app.mainloop()
|
||||
BIN
MCE/custom_widgets/icons/cancel.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
MCE/custom_widgets/icons/check.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
MCE/custom_widgets/icons/info.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
MCE/custom_widgets/icons/question.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
MCE/custom_widgets/icons/warning.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
92
MCE/utils.py
Normal file
@ -0,0 +1,92 @@
|
||||
import customtkinter
|
||||
import json
|
||||
import sys
|
||||
from MCE.custom_widgets.ctk_notification import CTkNotification
|
||||
|
||||
class Config:
|
||||
def __init__(self, linker, config_file):
|
||||
self.linker = linker
|
||||
self.config_file = config_file
|
||||
self.config_data = self.read()
|
||||
self.linker.widgets = self.set_values_to_none(self.config_data)
|
||||
self.locked = False
|
||||
linker.config = self
|
||||
|
||||
def read(self):
|
||||
# Read the JSON file
|
||||
try:
|
||||
with open(self.config_file, 'r') as json_file:
|
||||
config_data = json.load(json_file)
|
||||
return config_data
|
||||
except FileNotFoundError:
|
||||
print(f"Config file '{self.config_file}' not found.")
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError:
|
||||
print(f"Invalid JSON format in '{self.config_file}'.")
|
||||
sys.exit(1)
|
||||
|
||||
def set_values_to_none(self, input_dict):
|
||||
result = {}
|
||||
for key, value in input_dict.items():
|
||||
if isinstance(value, dict):
|
||||
result[key] = self.set_values_to_none(value)
|
||||
else:
|
||||
result[key] = None
|
||||
return result
|
||||
|
||||
def load_config(self, widgets=None, config_data=None):
|
||||
if widgets == None:
|
||||
widgets = self.linker.widgets
|
||||
config_data = self.config_data
|
||||
for key in widgets:
|
||||
if isinstance(widgets[key], dict) and isinstance(config_data[key], dict):
|
||||
self.load_config(widgets[key], config_data[key])
|
||||
else:
|
||||
if widgets[key] is not None:
|
||||
if isinstance(widgets[key], customtkinter.CTkCheckBox):
|
||||
if config_data[key] == True:
|
||||
widgets[key].select()
|
||||
else:
|
||||
widgets[key].deselect()
|
||||
elif isinstance(widgets[key], customtkinter.CTkEntry):
|
||||
widgets[key].insert(0, config_data[key])
|
||||
else:
|
||||
widgets[key].set(config_data[key])
|
||||
|
||||
def save_to_json(self, list_keys):
|
||||
widget = self.linker.widgets
|
||||
data = self.config_data
|
||||
for i in list_keys[:-1]:
|
||||
widget = widget[i]
|
||||
data = data[i]
|
||||
widget = widget[list_keys[-1]]
|
||||
value = widget.get()
|
||||
if isinstance(widget, customtkinter.CTkCheckBox):
|
||||
value = True if value==1 else False
|
||||
data[list_keys[-1]] = value
|
||||
self.save_file("Configuration")
|
||||
|
||||
def save_file(self, name=None):
|
||||
if self.locked:
|
||||
with open("MCE\config.json", "r") as config_file:
|
||||
new_config = json.load(config_file)
|
||||
self.config_data["Queue"] = new_config["Queue"]
|
||||
self.config_data["LastRun"] = new_config["LastRun"]
|
||||
with open("MCE\config.json", "w") as config_file:
|
||||
json.dump(self.config_data, config_file, indent=2)
|
||||
if name:
|
||||
self.linker.show_notification(name)
|
||||
|
||||
class Linker:
|
||||
def __init__(self):
|
||||
self.capitalise = lambda word: " ".join(x.title() for x in word.split("_"))
|
||||
self.config = None
|
||||
self.widgets = {}
|
||||
self.sidebar = None
|
||||
self.event_id = None
|
||||
|
||||
def show_notification(self, text):
|
||||
if self.event_id:
|
||||
self.sidebar.after_cancel(self.event_id)
|
||||
self.sidebar.notification.show()
|
||||
self.event_id = self.sidebar.after(2500, self.sidebar.notification.hide)
|
||||
4
aas.py
@ -58,6 +58,10 @@ class ArisuAutoSweeper(AzurLaneAutoScript):
|
||||
from tasks.momotalk.momotalk import MomoTalk
|
||||
MomoTalk(config=self.config, device=self.device).run()
|
||||
|
||||
def mission(self):
|
||||
from tasks.mission.mission import Mission
|
||||
Mission(config=self.config, device=self.device).run()
|
||||
|
||||
def data_update(self):
|
||||
from tasks.item.data_update import DataUpdate
|
||||
DataUpdate(config=self.config, device=self.device).run()
|
||||
|
||||
BIN
assets/en/mission/CHECK_BD.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
assets/en/mission/CHECK_COMMISSIONS.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/en/mission/CHECK_IR.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/en/mission/CHECK_MISSION_SWEEP.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
assets/en/mission/HARD_OFF.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
assets/en/mission/HARD_ON.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
assets/en/mission/LEFT.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
assets/en/mission/NORMAL_OFF.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
assets/en/mission/NORMAL_ON.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
assets/en/mission/OCR_AREA.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
assets/en/mission/RIGHT.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
assets/en/mission/SELECT_BD.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/en/mission/SELECT_IR.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
@ -172,12 +172,20 @@
|
||||
"Enable": true,
|
||||
"NextRun": "2020-01-01 00:00:00",
|
||||
"Command": "TacticalChallenge",
|
||||
"ServerUpdate": "15:00"
|
||||
"ServerUpdate": "14:00"
|
||||
},
|
||||
"TacticalChallenge": {
|
||||
"PlayerSelect": 0
|
||||
}
|
||||
},
|
||||
"Mission": {
|
||||
"Scheduler": {
|
||||
"Enable": false,
|
||||
"NextRun": "2020-01-01 00:00:00",
|
||||
"Command": "Mission",
|
||||
"ServerUpdate": "04:00"
|
||||
}
|
||||
},
|
||||
"Circle": {
|
||||
"Scheduler": {
|
||||
"Enable": true,
|
||||
|
||||
@ -730,7 +730,7 @@
|
||||
},
|
||||
"ServerUpdate": {
|
||||
"type": "input",
|
||||
"value": "15:00",
|
||||
"value": "14:00",
|
||||
"display": "hide"
|
||||
}
|
||||
},
|
||||
@ -747,6 +747,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Mission": {
|
||||
"Scheduler": {
|
||||
"Enable": {
|
||||
"type": "checkbox",
|
||||
"value": false,
|
||||
"option": [
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"NextRun": {
|
||||
"type": "datetime",
|
||||
"value": "2020-01-01 00:00:00",
|
||||
"validate": "datetime"
|
||||
},
|
||||
"Command": {
|
||||
"type": "input",
|
||||
"value": "Mission",
|
||||
"display": "hide"
|
||||
},
|
||||
"ServerUpdate": {
|
||||
"type": "input",
|
||||
"value": "04:00",
|
||||
"display": "hide"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Circle": {
|
||||
"Scheduler": {
|
||||
"Enable": {
|
||||
|
||||
@ -22,7 +22,8 @@
|
||||
"tasks": [
|
||||
"Bounty",
|
||||
"Scrimmage",
|
||||
"TacticalChallenge"
|
||||
"TacticalChallenge",
|
||||
"Mission"
|
||||
]
|
||||
},
|
||||
"Reward": {
|
||||
|
||||
@ -55,6 +55,8 @@ Farm:
|
||||
TacticalChallenge:
|
||||
- Scheduler
|
||||
- TacticalChallenge
|
||||
Mission:
|
||||
- Scheduler
|
||||
|
||||
# ==================== Rewards ====================
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ class ManualConfig:
|
||||
SCHEDULER_PRIORITY = """
|
||||
Restart
|
||||
> Cafe > TacticalChallenge > Circle > Mail
|
||||
> DataUpdate > Bounty > Scrimmage > Task > Shop > Momotalk
|
||||
> DataUpdate > Bounty > Scrimmage > Task > Shop > Mission > Momotalk
|
||||
"""
|
||||
|
||||
"""
|
||||
|
||||
@ -50,6 +50,10 @@
|
||||
"name": "Tactical Challenge",
|
||||
"help": ""
|
||||
},
|
||||
"Mission": {
|
||||
"name": "Mission/Commissions/Event",
|
||||
"help": "Open MCE Manager for additional settings"
|
||||
},
|
||||
"Circle": {
|
||||
"name": "Club",
|
||||
"help": ""
|
||||
|
||||
@ -50,6 +50,10 @@
|
||||
"name": "战术对抗赛",
|
||||
"help": "战术大赛 / 竞技场"
|
||||
},
|
||||
"Mission": {
|
||||
"name": "Task.Mission.name",
|
||||
"help": "Task.Mission.help"
|
||||
},
|
||||
"Circle": {
|
||||
"name": "公会",
|
||||
"help": "社团 / 小组"
|
||||
|
||||
148
tasks/mission/assets/assets_mission.py
Normal file
@ -0,0 +1,148 @@
|
||||
from module.base.button import Button, ButtonWrapper
|
||||
|
||||
# This file was auto-generated, do not modify it manually. To generate:
|
||||
# ``` python -m dev_tools.button_extract ```
|
||||
|
||||
CHECK_BD = ButtonWrapper(
|
||||
name='CHECK_BD',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/CHECK_BD.png',
|
||||
area=(94, 135, 325, 194),
|
||||
search=(74, 115, 345, 214),
|
||||
color=(208, 215, 220),
|
||||
button=(94, 135, 325, 194),
|
||||
),
|
||||
)
|
||||
CHECK_COMMISSIONS = ButtonWrapper(
|
||||
name='CHECK_COMMISSIONS',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/CHECK_COMMISSIONS.png',
|
||||
area=(646, 78, 909, 135),
|
||||
search=(626, 58, 929, 155),
|
||||
color=(70, 96, 124),
|
||||
button=(646, 78, 909, 135),
|
||||
),
|
||||
)
|
||||
CHECK_IR = ButtonWrapper(
|
||||
name='CHECK_IR',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/CHECK_IR.png',
|
||||
area=(97, 137, 340, 191),
|
||||
search=(77, 117, 360, 211),
|
||||
color=(213, 220, 223),
|
||||
button=(97, 137, 340, 191),
|
||||
),
|
||||
)
|
||||
CHECK_MISSION_SWEEP = ButtonWrapper(
|
||||
name='CHECK_MISSION_SWEEP',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/CHECK_MISSION_SWEEP.png',
|
||||
area=(654, 184, 703, 209),
|
||||
search=(634, 164, 723, 229),
|
||||
color=(208, 213, 219),
|
||||
button=(654, 184, 703, 209),
|
||||
),
|
||||
)
|
||||
HARD_OFF = ButtonWrapper(
|
||||
name='HARD_OFF',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/HARD_OFF.png',
|
||||
area=(947, 132, 1193, 182),
|
||||
search=(927, 112, 1213, 202),
|
||||
color=(242, 246, 248),
|
||||
button=(947, 132, 1193, 182),
|
||||
),
|
||||
)
|
||||
HARD_ON = ButtonWrapper(
|
||||
name='HARD_ON',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/HARD_ON.png',
|
||||
area=(940, 133, 1189, 186),
|
||||
search=(920, 113, 1209, 206),
|
||||
color=(200, 71, 63),
|
||||
button=(940, 133, 1189, 186),
|
||||
),
|
||||
)
|
||||
LEFT = ButtonWrapper(
|
||||
name='LEFT',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/LEFT.png',
|
||||
area=(0, 301, 89, 408),
|
||||
search=(0, 281, 109, 428),
|
||||
color=(193, 224, 241),
|
||||
button=(0, 301, 89, 408),
|
||||
),
|
||||
)
|
||||
NORMAL_OFF = ButtonWrapper(
|
||||
name='NORMAL_OFF',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/NORMAL_OFF.png',
|
||||
area=(682, 135, 927, 182),
|
||||
search=(662, 115, 947, 202),
|
||||
color=(238, 243, 246),
|
||||
button=(682, 135, 927, 182),
|
||||
),
|
||||
)
|
||||
NORMAL_ON = ButtonWrapper(
|
||||
name='NORMAL_ON',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/NORMAL_ON.png',
|
||||
area=(682, 137, 924, 185),
|
||||
search=(662, 117, 944, 205),
|
||||
color=(62, 81, 89),
|
||||
button=(682, 137, 924, 185),
|
||||
),
|
||||
)
|
||||
OCR_AREA = ButtonWrapper(
|
||||
name='OCR_AREA',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/OCR_AREA.png',
|
||||
area=(108, 176, 176, 219),
|
||||
search=(88, 156, 196, 239),
|
||||
color=(237, 238, 240),
|
||||
button=(108, 176, 176, 219),
|
||||
),
|
||||
)
|
||||
RIGHT = ButtonWrapper(
|
||||
name='RIGHT',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/RIGHT.png',
|
||||
area=(1202, 311, 1280, 412),
|
||||
search=(1182, 291, 1280, 432),
|
||||
color=(193, 223, 241),
|
||||
button=(1202, 311, 1280, 412),
|
||||
),
|
||||
)
|
||||
SELECT_BD = ButtonWrapper(
|
||||
name='SELECT_BD',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/SELECT_BD.png',
|
||||
area=(1016, 165, 1227, 211),
|
||||
search=(996, 145, 1247, 231),
|
||||
color=(205, 212, 220),
|
||||
button=(1016, 165, 1227, 211),
|
||||
),
|
||||
)
|
||||
SELECT_IR = ButtonWrapper(
|
||||
name='SELECT_IR',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/SELECT_IR.png',
|
||||
area=(1004, 267, 1237, 321),
|
||||
search=(984, 247, 1257, 341),
|
||||
color=(214, 220, 227),
|
||||
button=(1004, 267, 1237, 321),
|
||||
),
|
||||
)
|
||||
260
tasks/mission/mission.py
Normal file
@ -0,0 +1,260 @@
|
||||
from enum import Enum
|
||||
|
||||
from module.base.timer import Timer
|
||||
from module.exception import RequestHumanTakeover
|
||||
from module.logger import logger
|
||||
from tasks.mission.ui import MissionUI, CommissionsUI
|
||||
from tasks.stage.ap import AP
|
||||
from tasks.cafe.cafe import Cafe
|
||||
from tasks.circle.circle import Circle
|
||||
from tasks.task.task import Task
|
||||
from tasks.mail.mail import Mail
|
||||
from tasks.item.data_update import DataUpdate
|
||||
import json
|
||||
import math
|
||||
from filelock import FileLock
|
||||
from datetime import datetime
|
||||
|
||||
class MissionStatus(Enum):
|
||||
AP = 0 # Calculate AP and decide to terminate Mission module or not
|
||||
NAVIGATE = 1 # Navigate to the stage page for example the commissions page or mission page
|
||||
SELECT = 2 # Select the stage mode for example hard or normal in mission
|
||||
ENTER = 3 # Search and for the stage in the stage list and enter
|
||||
SWEEP = 4 # Sweep the stage
|
||||
RECHARGE = 5 # Recharge AP from other taks if they are enabled
|
||||
FINISH = -1 # Inidicate termination of Mission module
|
||||
|
||||
|
||||
class Mission(MissionUI, CommissionsUI):
|
||||
_stage_ap = [10, 15, 15, 15]
|
||||
|
||||
@property
|
||||
def stage_ap(self):
|
||||
return self._stage_ap
|
||||
|
||||
@property
|
||||
def mission_info(self) -> list:
|
||||
"""
|
||||
Read the config from MCE/config.json and extract the queue, a list of list.
|
||||
If queue is empty repopulate from preferred template.
|
||||
|
||||
Format of each element in queue: [mode, stage, sweep_num]
|
||||
e.g. ["N", "1-1", 3]
|
||||
|
||||
Mode Acronyms:
|
||||
"N" : Normal Mission
|
||||
"H" : Hard Mission
|
||||
"E" : Event Quest
|
||||
"IR" : Item Retrieval / Commission where you get credit
|
||||
"BD" : Base Defense / Commission where you get exp
|
||||
|
||||
Returns:
|
||||
list of list
|
||||
"""
|
||||
queue = []
|
||||
try:
|
||||
with open("MCE/config.json") as json_file:
|
||||
config_data = json.load(json_file)
|
||||
queue = config_data["Queue"]
|
||||
self.recharge_AP = config_data["RechargeAP"]
|
||||
self.reset_daily = config_data["ResetDaily"]
|
||||
self.reset_time = config_data["ResetTime"]
|
||||
self.last_run = config_data["LastRun"]
|
||||
self.event = config_data["Event"]
|
||||
if self.check_reset_daily() or not queue:
|
||||
preferred_template = config_data["PreferredTemplate"]
|
||||
queue = config_data["Templates"][preferred_template]
|
||||
if not self.event:
|
||||
queue = [x for x in queue if x[0] != "E"]
|
||||
except:
|
||||
logger.error("Failed to read configuration file")
|
||||
finally:
|
||||
return queue
|
||||
|
||||
def check_reset_daily(self):
|
||||
# Check if it's time to reset the queue
|
||||
if self.reset_daily:
|
||||
current_datetime = datetime.now().replace(microsecond=0) # Round to the nearest second
|
||||
current_date = current_datetime.date()
|
||||
current_time = current_datetime.time()
|
||||
last_run_datetime = datetime.strptime(self.last_run, "%Y-%m-%d %H:%M:%S")
|
||||
reset_time = datetime.strptime(self.reset_time, "%H:%M:%S").time()
|
||||
|
||||
if current_date != last_run_datetime.date() and current_time >= reset_time:
|
||||
self.last_run = str(datetime.now().replace(microsecond=0))
|
||||
logger.info("Reset Daily activated.")
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def valid_task(self) -> list:
|
||||
task = self.mission_info
|
||||
if not task:
|
||||
logger.warning('Mission enabled but no task set')
|
||||
#self.error_handler()
|
||||
return task
|
||||
|
||||
@property
|
||||
def current_mode(self):
|
||||
return self.task[0][0]
|
||||
|
||||
@property
|
||||
def current_stage(self):
|
||||
return self.task[0][1]
|
||||
|
||||
@property
|
||||
def current_count(self):
|
||||
if self.current_mode == "H" and self.task[0][2] > 3:
|
||||
return 3
|
||||
return self.task[0][2]
|
||||
|
||||
@current_count.setter
|
||||
def current_count(self, value):
|
||||
self.task[0][2] = value
|
||||
|
||||
def select(self) -> bool:
|
||||
"""
|
||||
A wrapper method to select the current_mode
|
||||
by calling the specific method based on its type.
|
||||
|
||||
Return
|
||||
True if selection happens without any problem, False otherwise.
|
||||
"""
|
||||
if self.current_mode in ["N", "H"]:
|
||||
return self.select_mission(self.current_mode, self.current_stage)
|
||||
elif self.current_mode in ["BD", "IR"]:
|
||||
return self.select_commission(self.current_mode)
|
||||
elif self.current_mode == "E":
|
||||
#return self.select_mode(SWITCH_QUEST)
|
||||
logger.error("Event not yet implemented")
|
||||
return False
|
||||
else:
|
||||
logger.error("Uknown mode")
|
||||
return False
|
||||
|
||||
def get_realistic_count(self) -> int:
|
||||
"""
|
||||
Calculate the possible number of sweeps based on the current AP
|
||||
"""
|
||||
ap_cost = 20 if self.current_mode == "H" else 10
|
||||
required_ap = ap_cost * self.current_count
|
||||
return math.floor(min(required_ap, self.current_ap) / ap_cost)
|
||||
|
||||
def update_task(self, failure=False):
|
||||
"""
|
||||
Update self.task and save the current state of the queue in
|
||||
MCE/config.json.
|
||||
"""
|
||||
try:
|
||||
if failure or self.current_count == self.realistic_count:
|
||||
self.previous_mode = self.current_mode
|
||||
self.task.pop(0)
|
||||
else:
|
||||
self.previous_mode = None
|
||||
self.current_count -= self.realistic_count
|
||||
with open("MCE/config.json", "r") as json_file:
|
||||
config_data = json.load(json_file)
|
||||
with open("MCE/config.json", "w") as json_file:
|
||||
config_data["Queue"] = self.task
|
||||
config_data["LastRun"] = self.last_run
|
||||
json.dump(config_data, json_file, indent=2)
|
||||
except:
|
||||
logger.error("Failed to save configuration")
|
||||
self.task = []
|
||||
|
||||
def update_ap(self):
|
||||
ap_cost = 20 if self.current_mode == "H" else 10
|
||||
ap = self.config.stored.AP
|
||||
ap_old = ap.value
|
||||
ap_new = ap_old - ap_cost * self.realistic_count
|
||||
ap.set(ap_new, ap.total)
|
||||
logger.info(f'Set AP: {ap_old} -> {ap_new}')
|
||||
|
||||
def recharge(self) -> bool:
|
||||
"""
|
||||
Check if AP related modules such as cafe, circle, task, mail are enabled and run them if they are.
|
||||
task_call only works after the current task is finished so is not suitable.
|
||||
"""
|
||||
cafe_reward = self.config.cross_get(["Cafe", "Scheduler", "Enable"]) and self.config.cross_get(["Cafe", "Cafe", "Reward"])
|
||||
circle = self.config.cross_get(["Circle", "Scheduler", "Enable"])
|
||||
task = self.config.cross_get(["Task", "Scheduler", "Enable"])
|
||||
mail = self.config.cross_get(["Mail", "Scheduler", "Enable"])
|
||||
ap_tasks = [(cafe_reward,Cafe), (circle, Circle), (task, Task), (mail, Mail)]
|
||||
modules = [x[1] for x in ap_tasks if x[0]]
|
||||
if not modules:
|
||||
logger.info("Recharge AP was enabled but no AP related modules were enabled")
|
||||
return False
|
||||
for module in modules:
|
||||
module(config=self.config, device=self.device).run()
|
||||
return True
|
||||
|
||||
def handle_mission(self, status):
|
||||
match status:
|
||||
case MissionStatus.AP:
|
||||
if not self.task:
|
||||
return MissionStatus.FINISH
|
||||
self.realistic_count = self.get_realistic_count()
|
||||
if self.realistic_count == 0 and self.recharge_AP:
|
||||
self.recharge_AP = False
|
||||
return MissionStatus.RECHARGE
|
||||
elif self.realistic_count == 0 and not self.recharge_AP:
|
||||
return MissionStatus.FINISH
|
||||
else:
|
||||
return MissionStatus.NAVIGATE
|
||||
case MissionStatus.NAVIGATE:
|
||||
self.navigate(self.previous_mode, self.current_mode)
|
||||
return MissionStatus.SELECT
|
||||
case MissionStatus.SELECT:
|
||||
if self.select():
|
||||
return MissionStatus.ENTER
|
||||
self.update_task(failure=True)
|
||||
return MissionStatus.AP
|
||||
case MissionStatus.ENTER:
|
||||
if self.enter_stage(self.current_stage):
|
||||
return MissionStatus.SWEEP
|
||||
self.update_task(failure=True)
|
||||
return MissionStatus.AP
|
||||
case MissionStatus.SWEEP:
|
||||
if self.do_sweep(self.current_mode, self.realistic_count):
|
||||
self.update_ap()
|
||||
self.update_task()
|
||||
else:
|
||||
self.update_task(failure=True)
|
||||
return MissionStatus.AP
|
||||
case MissionStatus.RECHARGE:
|
||||
if self.recharge():
|
||||
DataUpdate(config=self.config, device=self.device).run()
|
||||
return MissionStatus.AP
|
||||
return MissionStatus.FINISH
|
||||
case MissionStatus.FINISH:
|
||||
return status
|
||||
case _:
|
||||
logger.warning(f'Invalid status: {status}')
|
||||
return status
|
||||
|
||||
def run(self):
|
||||
self.lock = FileLock("MCE/config.json.lock")
|
||||
with self.lock.acquire():
|
||||
self.previous_mode = None
|
||||
self.task = self.valid_task
|
||||
action_timer = Timer(0.5, 1)
|
||||
status = MissionStatus.AP
|
||||
|
||||
"""Update the dashboard to accurately calculate AP"""
|
||||
DataUpdate(config=self.config, device=self.device).run()
|
||||
|
||||
while 1:
|
||||
self.device.screenshot()
|
||||
|
||||
if self.ui_additional():
|
||||
continue
|
||||
|
||||
if action_timer.reached_and_reset():
|
||||
logger.attr('Status', status)
|
||||
status = self.handle_mission(status)
|
||||
|
||||
if status == MissionStatus.FINISH:
|
||||
break
|
||||
|
||||
self.config.task_delay(server_update=True)
|
||||
|
||||
147
tasks/mission/ui.py
Normal file
@ -0,0 +1,147 @@
|
||||
from module.base.timer import Timer
|
||||
from module.logger import logger
|
||||
from module.ui.switch import Switch
|
||||
from module.ocr.ocr import Digit
|
||||
from tasks.base.assets.assets_base_page import BACK, MISSION_CHECK
|
||||
from tasks.base.page import page_mission, page_commissions
|
||||
from tasks.base.ui import UI
|
||||
from tasks.mission.assets.assets_mission import *
|
||||
from tasks.stage.ap import AP
|
||||
from tasks.stage.mission_list import StageList
|
||||
from tasks.stage.sweep import StageSweep
|
||||
|
||||
|
||||
SHARED_LIST = StageList('SharedList')
|
||||
MISSION_SWEEP = StageSweep('MissionSweep', 60)
|
||||
MISSION_SWEEP.set_button(button_check=CHECK_MISSION_SWEEP) # Check sweep is different for mission
|
||||
SHARED_SWEEP = StageSweep('SharedSweep', 60)
|
||||
|
||||
SWITCH_NORMAL = Switch("Normal_switch")
|
||||
SWITCH_NORMAL.add_state("on", NORMAL_ON)
|
||||
SWITCH_NORMAL.add_state("off", NORMAL_OFF)
|
||||
|
||||
SWITCH_HARD = Switch("HARD_switch")
|
||||
SWITCH_HARD.add_state("on", HARD_ON)
|
||||
SWITCH_HARD.add_state("off", HARD_OFF)
|
||||
|
||||
SWITCH_QUEST = None
|
||||
|
||||
"""
|
||||
A dictionary that maps the mode to a tuple where
|
||||
the first element is an argument to go_back and second is for ui_ensure
|
||||
Missing for "E" because there are no event in Global and no page_event
|
||||
"""
|
||||
MODE_TO_PAGE = {
|
||||
"N": (MISSION_CHECK, page_mission),
|
||||
"H": (MISSION_CHECK, page_mission),
|
||||
"BD": (CHECK_BD, page_commissions),
|
||||
"IR": (CHECK_IR, page_commissions),
|
||||
"E" : ()
|
||||
}
|
||||
|
||||
|
||||
class MissionUI(UI, AP):
|
||||
def select_mission(self, mode, stage):
|
||||
area = int(stage.split("-")[0])
|
||||
if not self.select_area(area):
|
||||
logger.warning("Area not found")
|
||||
return False
|
||||
|
||||
to_switch = {
|
||||
"N": SWITCH_NORMAL,
|
||||
"H": SWITCH_HARD
|
||||
}
|
||||
switch = to_switch[mode]
|
||||
if not self.select_mode(switch) and not self.select_area(area):
|
||||
return False
|
||||
return True
|
||||
|
||||
def select_area(self, num):
|
||||
""""
|
||||
May require further error handling for these cases.
|
||||
1. Fails to ocr area number
|
||||
2. May trigger too many click exception when clicking left or right too many times
|
||||
3. Area not unlocked. Simplest way if left or right button are still present
|
||||
but problem is it is expensive to check every time and they always keep moving.
|
||||
"""
|
||||
tries = 0
|
||||
ocr_area = Digit(OCR_AREA)
|
||||
while 1:
|
||||
try:
|
||||
self.device.screenshot()
|
||||
current_area = int(ocr_area.ocr_single_line(self.device.image))
|
||||
if current_area == num:
|
||||
return True
|
||||
elif current_area > num:
|
||||
[self.click_with_interval(LEFT, interval=1) for x in range(abs(current_area-num))]
|
||||
elif current_area < num:
|
||||
[self.click_with_interval(RIGHT, interval=1) for x in range(abs(current_area-num))]
|
||||
except:
|
||||
tries += 1
|
||||
if tries > 5:
|
||||
return False
|
||||
|
||||
def select_mode(self, switch):
|
||||
"""
|
||||
Set switch to on.
|
||||
Returns:
|
||||
True if switch is set, False if switch not found
|
||||
"""
|
||||
if not switch.appear(main=self):
|
||||
logger.info(f'{switch.name} not found')
|
||||
return False
|
||||
switch.set('on', main=self)
|
||||
return True
|
||||
|
||||
def enter_stage(self, index: int) -> bool:
|
||||
if not index:
|
||||
index = SHARED_LIST.insight_max_sweepable_index(self)
|
||||
if SHARED_LIST.select_index_enter(self, index):
|
||||
return True
|
||||
return False
|
||||
|
||||
def do_sweep(self, mode, num: int) -> bool:
|
||||
if mode in ["N", "H"]:
|
||||
return MISSION_SWEEP.do_sweep(self, num=num)
|
||||
else:
|
||||
return SHARED_SWEEP.do_sweep(self, num=num)
|
||||
|
||||
def navigate(self, prev, next):
|
||||
"""
|
||||
go_back is called when the previous stage and next stage are in
|
||||
the same game mode.
|
||||
For example, "N" and "H" are in Mission so we call go_back.
|
||||
If different, ui_ensure is called for example, "N" and "IR".
|
||||
"""
|
||||
if prev==next or (prev in ["N", "H"] and next in ["N", "H"]):
|
||||
self.go_back(MODE_TO_PAGE[next][0])
|
||||
elif prev in ["BD", "IR"] and next in ["BD", "IR"]:
|
||||
self.go_back(CHECK_COMMISSIONS)
|
||||
else:
|
||||
self.ui_ensure(MODE_TO_PAGE[next][1])
|
||||
|
||||
def go_back(self, check):
|
||||
while 1:
|
||||
self.device.screenshot()
|
||||
if self.match_color(check) and self.appear(check):
|
||||
return True
|
||||
self.click_with_interval(BACK, interval=2)
|
||||
|
||||
class CommissionsUI(UI, AP):
|
||||
"""Works the same way as select_bounty"""
|
||||
def select_commission(self, mode):
|
||||
to_button = {
|
||||
"IR": (SELECT_IR, CHECK_IR),
|
||||
"BD": (SELECT_BD, CHECK_BD)
|
||||
}
|
||||
dest_enter, dest_check = to_button[mode]
|
||||
timer = Timer(5, 10).start()
|
||||
while 1:
|
||||
self.device.screenshot()
|
||||
self.appear_then_click(dest_enter, interval=1)
|
||||
if self.appear(dest_check):
|
||||
return True
|
||||
if timer.reached():
|
||||
return False
|
||||
|
||||
|
||||
252
tasks/stage/mission_list.py
Normal file
@ -0,0 +1,252 @@
|
||||
import re
|
||||
|
||||
import numpy as np
|
||||
|
||||
from module.base.base import ModuleBase
|
||||
from module.base.timer import Timer
|
||||
from module.base.utils import area_pad, area_size, area_offset
|
||||
from module.logger import logger
|
||||
from module.ocr.ocr import Ocr
|
||||
from tasks.stage.assets.assets_stage_list import *
|
||||
|
||||
|
||||
class StageList:
|
||||
swipe_vector_range = (0.65, 0.70)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
button_list: ButtonWrapper = None,
|
||||
button_index: ButtonWrapper = None,
|
||||
button_item: ButtonWrapper = None,
|
||||
button_enter: ButtonWrapper = None,
|
||||
button_stars: ButtonWrapper = None,
|
||||
swipe_direction: str = "down"
|
||||
):
|
||||
self.name = name
|
||||
self.stage = button_list if button_list else STAGE_LIST
|
||||
self.index_ocr = Ocr(button_index if button_index else OCR_INDEX, lang='en')
|
||||
self.stage_item = (button_item if button_item else STAGE_ITEM).button
|
||||
self.enter = button_enter if button_enter else STAGE_ENTER
|
||||
self.sweepable = button_stars if button_stars else STAGE_STARS
|
||||
self.swipe_direction = swipe_direction
|
||||
|
||||
self.current_index_min = 1
|
||||
self.current_index_max = 1
|
||||
self.current_indexes: list[tuple[str, tuple]] = []
|
||||
|
||||
def __str__(self):
|
||||
return f'StageList({self.name})'
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def __eq__(self, other):
|
||||
return str(self) == str(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
@property
|
||||
def _indexes(self) -> list[int]:
|
||||
return [x[0] for x in self.current_indexes]
|
||||
|
||||
def load_stage_indexes(self, main: ModuleBase):
|
||||
self.current_indexes = list(
|
||||
filter(
|
||||
lambda x: re.match(r'^\d{1,2}-?\d?$', x[0]) and x[0] != '00',
|
||||
map(lambda x: (x.ocr_text, x.box), self.index_ocr.detect_and_ocr(main.device.image))
|
||||
)
|
||||
)
|
||||
if not self.current_indexes:
|
||||
logger.warning(f'No valid index in {self.index_ocr.name}')
|
||||
return
|
||||
indexes = self._indexes
|
||||
|
||||
self.current_index_min = min(indexes)
|
||||
self.current_index_max = max(indexes)
|
||||
logger.attr(self.index_ocr.name, f'Index range: {self.current_index_min} - {self.current_index_max}')
|
||||
|
||||
def swipe_page(self, direction: str, main: ModuleBase, vector_range=None, reverse=False):
|
||||
"""
|
||||
Args:
|
||||
direction: up, down
|
||||
main:
|
||||
vector_range (tuple[float, float]):
|
||||
reverse (bool):
|
||||
"""
|
||||
if vector_range is None:
|
||||
vector_range = self.swipe_vector_range
|
||||
vector = np.random.uniform(*vector_range)
|
||||
width, height = area_size(self.stage.button)
|
||||
if direction == 'up':
|
||||
vector = (0, vector * height)
|
||||
elif direction == 'down':
|
||||
vector = (0, -vector * height)
|
||||
else:
|
||||
logger.warning(f'Unknown swipe direction: {direction}')
|
||||
return
|
||||
|
||||
if reverse:
|
||||
vector = (-vector[0], -vector[1])
|
||||
main.device.swipe_vector(vector, self.stage.button, name=f'{self.name}_SWIPE')
|
||||
|
||||
def insight_index(self, index: int, main: ModuleBase, skip_first_screenshot=True) -> bool:
|
||||
"""
|
||||
Args:
|
||||
index:
|
||||
main:
|
||||
skip_first_screenshot:
|
||||
|
||||
Returns:
|
||||
If success
|
||||
"""
|
||||
logger.info(f'Insight index: {index}')
|
||||
last_indexes: set[int] = set()
|
||||
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
main.device.screenshot()
|
||||
|
||||
self.load_stage_indexes(main=main)
|
||||
|
||||
if self.current_index_min <= index <= self.current_index_max:
|
||||
return True
|
||||
|
||||
indexes = self._indexes
|
||||
if indexes and last_indexes == set(indexes):
|
||||
logger.warning(f'No more index {index}')
|
||||
return False
|
||||
last_indexes = set(indexes)
|
||||
|
||||
if index < self.current_index_min:
|
||||
self.swipe_page(self.swipe_direction, main, reverse=True)
|
||||
elif index > self.current_index_max:
|
||||
self.swipe_page(self.swipe_direction, main)
|
||||
|
||||
main.wait_until_stable(
|
||||
self.stage.button,
|
||||
timer=Timer(0, 0),
|
||||
timeout=Timer(1.5, 5)
|
||||
)
|
||||
|
||||
def insight_max_sweepable_index(self, main: ModuleBase, skip_first_screenshot=True) -> int:
|
||||
"""
|
||||
Args:
|
||||
main:
|
||||
skip_first_screenshot:
|
||||
|
||||
Returns:
|
||||
Index of max sweepable stage
|
||||
"""
|
||||
logger.info('Insight sweepable index')
|
||||
max_sweepable_index = 0
|
||||
last_max_sweepable_index = 0
|
||||
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
main.device.screenshot()
|
||||
|
||||
self.load_stage_indexes(main=main)
|
||||
|
||||
sweepable_index = next(
|
||||
filter(
|
||||
lambda x: not self.is_sweepable(main, self.search_box(x[-1][:2])),
|
||||
self.current_indexes
|
||||
), None
|
||||
)
|
||||
|
||||
# all sweepable
|
||||
if sweepable_index is None:
|
||||
logger.info('All sweepable')
|
||||
max_sweepable_index = self.current_index_max
|
||||
self.swipe_page(self.swipe_direction, main)
|
||||
if max_sweepable_index == last_max_sweepable_index:
|
||||
logger.info(f'Max sweepable index: {max_sweepable_index}')
|
||||
return max_sweepable_index
|
||||
last_max_sweepable_index = max_sweepable_index
|
||||
# all not sweepable
|
||||
elif int(sweepable_index[0]) == self.current_index_min:
|
||||
logger.info('All not sweepable')
|
||||
if int(sweepable_index[0]) == 1:
|
||||
logger.warning('No sweepable index')
|
||||
return 0
|
||||
self.swipe_page(self.swipe_direction, main, reverse=True)
|
||||
else:
|
||||
logger.info(f'Sweepable index: {int(sweepable_index[0]) - 1}')
|
||||
return int(sweepable_index[0]) - 1
|
||||
|
||||
main.wait_until_stable(
|
||||
self.stage.button,
|
||||
timer=Timer(0, 0),
|
||||
timeout=Timer(1.5, 5)
|
||||
)
|
||||
|
||||
def is_sweepable(self, main: ModuleBase, search_box) -> bool:
|
||||
self.sweepable.load_search(search_box)
|
||||
return main.appear(self.sweepable, similarity=0.8)
|
||||
|
||||
def search_box(
|
||||
self,
|
||||
index_cord: tuple[int, int],
|
||||
padding: tuple[int, int] = (-20, -15)
|
||||
) -> tuple[int, int, int, int]:
|
||||
stage_item_box = area_pad((*padding, *area_size(self.stage_item)))
|
||||
return area_offset(stage_item_box, index_cord)
|
||||
|
||||
def select_index_enter(
|
||||
self,
|
||||
main: ModuleBase,
|
||||
index: int,
|
||||
insight: bool = True,
|
||||
sweepable: bool = True,
|
||||
padding: tuple[int, int] = (-20, -15),
|
||||
skip_first_screenshot: bool = True,
|
||||
interval: int = 1.5
|
||||
) -> bool:
|
||||
# insight index, if failed, return False
|
||||
if insight and not self.insight_index(index, main, skip_first_screenshot):
|
||||
return False
|
||||
logger.info(f'Select index: {index}')
|
||||
click_interval = Timer(interval)
|
||||
load_index_interval = Timer(1)
|
||||
timeout = Timer(15, 10).start()
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
main.device.screenshot()
|
||||
|
||||
# load index if not insight
|
||||
if load_index_interval.reached_and_reset():
|
||||
self.load_stage_indexes(main=main)
|
||||
|
||||
# find box of index
|
||||
index_box = next(filter(lambda x: x[0] == index, self.current_indexes), None)
|
||||
|
||||
if index_box is None:
|
||||
logger.warning(f'No index {index} in {self.index_ocr.name}')
|
||||
continue
|
||||
|
||||
search_box = self.search_box(index_box[-1][:2], padding)
|
||||
if sweepable and not self.is_sweepable(main, search_box):
|
||||
logger.warning(f'Index {index} is not sweepable')
|
||||
return False
|
||||
|
||||
self.enter.load_search(search_box)
|
||||
click_button = self.enter.match_multi_template(main.device.image)
|
||||
|
||||
if not click_button:
|
||||
logger.warning(f'No clickable {self.enter.name}')
|
||||
continue
|
||||
|
||||
if click_interval.reached_and_reset():
|
||||
main.device.click(click_button[0])
|
||||
return True
|
||||
|
||||
if timeout.reached():
|
||||
logger.warning(f'{self.enter.name} failed')
|
||||
return False
|
||||
@ -5,7 +5,13 @@ from module.base.button import Button, ButtonWrapper
|
||||
|
||||
CLAIM = ButtonWrapper(
|
||||
name='CLAIM',
|
||||
jp=None,
|
||||
jp=Button(
|
||||
file='./assets/jp/task/CLAIM.png',
|
||||
area=(936, 641, 1010, 696),
|
||||
search=(916, 621, 1030, 716),
|
||||
color=(230, 210, 63),
|
||||
button=(936, 641, 1010, 696),
|
||||
),
|
||||
en=Button(
|
||||
file='./assets/en/task/CLAIM.png',
|
||||
area=(935, 639, 1015, 698),
|
||||
@ -16,7 +22,13 @@ CLAIM = ButtonWrapper(
|
||||
)
|
||||
CLAIMED = ButtonWrapper(
|
||||
name='CLAIMED',
|
||||
jp=None,
|
||||
jp=Button(
|
||||
file='./assets/jp/task/CLAIMED.png',
|
||||
area=(935, 641, 1010, 696),
|
||||
search=(915, 621, 1030, 716),
|
||||
color=(211, 211, 210),
|
||||
button=(935, 641, 1010, 696),
|
||||
),
|
||||
en=Button(
|
||||
file='./assets/en/task/CLAIMED.png',
|
||||
area=(937, 641, 1010, 696),
|
||||
@ -27,7 +39,13 @@ CLAIMED = ButtonWrapper(
|
||||
)
|
||||
CLAIMED_ALL = ButtonWrapper(
|
||||
name='CLAIMED_ALL',
|
||||
jp=None,
|
||||
jp=Button(
|
||||
file='./assets/jp/task/CLAIMED_ALL.png',
|
||||
area=(1057, 640, 1242, 701),
|
||||
search=(1037, 620, 1262, 720),
|
||||
color=(192, 193, 196),
|
||||
button=(1057, 640, 1242, 701),
|
||||
),
|
||||
en=Button(
|
||||
file='./assets/en/task/CLAIMED_ALL.png',
|
||||
area=(1058, 641, 1240, 701),
|
||||
@ -38,7 +56,13 @@ CLAIMED_ALL = ButtonWrapper(
|
||||
)
|
||||
CLAIM_ALL = ButtonWrapper(
|
||||
name='CLAIM_ALL',
|
||||
jp=None,
|
||||
jp=Button(
|
||||
file='./assets/jp/task/CLAIM_ALL.png',
|
||||
area=(1058, 641, 1242, 701),
|
||||
search=(1038, 621, 1262, 720),
|
||||
color=(235, 218, 67),
|
||||
button=(1058, 641, 1242, 701),
|
||||
),
|
||||
en=Button(
|
||||
file='./assets/en/task/CLAIM_ALL.png',
|
||||
area=(1054, 642, 1243, 700),
|
||||
|
||||