before refactor
This commit is contained in:
@@ -11,54 +11,38 @@ try:
|
|||||||
from .core.session_manager import SessionManager
|
from .core.session_manager import SessionManager
|
||||||
from .ui.overlay import OverlayWindow
|
from .ui.overlay import OverlayWindow
|
||||||
|
|
||||||
# Try to import platform-specific modules
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if sys.platform.startswith('win'):
|
if sys.platform.startswith('win'):
|
||||||
from .platform.windows import get_active_window, register_session_listener, get_idle_time
|
from .platform.windows import get_active_window, register_session_listener, get_idle_time
|
||||||
else:
|
else:
|
||||||
# Fallback for other platforms
|
|
||||||
def get_active_window():
|
def get_active_window():
|
||||||
return "Unsupported platform"
|
return "Unsupported platform"
|
||||||
|
|
||||||
|
|
||||||
def register_session_listener(callback):
|
def register_session_listener(callback):
|
||||||
print("Session monitoring not supported on this platform")
|
print("Session monitoring not supported")
|
||||||
|
|
||||||
|
|
||||||
def get_idle_time():
|
def get_idle_time():
|
||||||
return 0
|
return 0
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# For running as a single file
|
|
||||||
from core.config import load_config, save_config
|
from core.config import load_config, save_config
|
||||||
from core.session_manager import SessionManager
|
from core.session_manager import SessionManager
|
||||||
from ui.overlay import OverlayWindow
|
from ui.overlay import OverlayWindow
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if sys.platform.startswith('win'):
|
if sys.platform.startswith('win'):
|
||||||
from platform.windows import get_active_window, register_session_listener, get_idle_time
|
from platform.windows import get_active_window, register_session_listener, get_idle_time
|
||||||
else:
|
else:
|
||||||
def get_active_window():
|
def get_active_window():
|
||||||
return "Unsupported platform"
|
return "Unsupported platform"
|
||||||
|
|
||||||
|
|
||||||
def register_session_listener(callback):
|
def register_session_listener(callback):
|
||||||
print("Session monitoring not supported on this platform")
|
print("Session monitoring not supported")
|
||||||
|
|
||||||
|
|
||||||
def get_idle_time():
|
def get_idle_time():
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
class ConfigWrapper:
|
class ConfigWrapper:
|
||||||
"""Wrapper to allow attribute access to config dict"""
|
|
||||||
|
|
||||||
def __init__(self, config_dict: Dict):
|
def __init__(self, config_dict: Dict):
|
||||||
self.__dict__.update(config_dict)
|
self.__dict__.update(config_dict)
|
||||||
|
|
||||||
def update(self, new_config: Dict):
|
def update(self, new_config: Dict):
|
||||||
"""Update the config and underlying dict"""
|
|
||||||
self.__dict__.update(new_config)
|
self.__dict__.update(new_config)
|
||||||
|
|
||||||
|
|
||||||
@@ -74,261 +58,212 @@ class WorkTracker:
|
|||||||
self.is_tracking = self.config.tracking_enabled
|
self.is_tracking = self.config.tracking_enabled
|
||||||
self.current_task_id = self.config.current_task_id
|
self.current_task_id = self.config.current_task_id
|
||||||
|
|
||||||
# Start monitoring in background
|
# Thread safety
|
||||||
|
self._state_lock = threading.Lock()
|
||||||
|
|
||||||
self.start_monitoring()
|
self.start_monitoring()
|
||||||
|
|
||||||
def start_monitoring(self):
|
def start_monitoring(self):
|
||||||
"""Start background monitoring threads"""
|
|
||||||
# Thread for window tracking
|
|
||||||
window_thread = threading.Thread(target=self.monitor_windows, daemon=True)
|
window_thread = threading.Thread(target=self.monitor_windows, daemon=True)
|
||||||
window_thread.start()
|
window_thread.start()
|
||||||
|
|
||||||
# Thread for input activity tracking
|
|
||||||
input_thread = threading.Thread(target=self.monitor_input_activity, daemon=True)
|
input_thread = threading.Thread(target=self.monitor_input_activity, daemon=True)
|
||||||
input_thread.start()
|
input_thread.start()
|
||||||
|
|
||||||
# Register session listener
|
|
||||||
register_session_listener(self.on_session_change)
|
register_session_listener(self.on_session_change)
|
||||||
|
|
||||||
def monitor_windows(self):
|
def monitor_windows(self):
|
||||||
"""Monitor active windows and update classifications"""
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
current_window = get_active_window()
|
current_window = get_active_window()
|
||||||
|
|
||||||
# Only update if window changed
|
|
||||||
if current_window != self.active_window:
|
if current_window != self.active_window:
|
||||||
self.active_window = current_window
|
with self._state_lock:
|
||||||
self.window_classification = self.classify_window(current_window)
|
self.active_window = current_window
|
||||||
|
self.window_classification = self.classify_window(current_window)
|
||||||
# Show notification if it's a distraction or neutral app
|
|
||||||
if self.window_classification == "distraction":
|
|
||||||
self.show_distraction_notification(current_window)
|
|
||||||
elif self.window_classification == "neutral":
|
|
||||||
self.show_neutral_notification(current_window)
|
|
||||||
|
|
||||||
|
# Notifications will be shown via callbacks set in main()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in window monitoring: {e}")
|
print(f"Error in window monitoring: {e}")
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
def monitor_input_activity(self):
|
def monitor_input_activity(self):
|
||||||
"""Monitor keyboard and mouse activity"""
|
|
||||||
try:
|
try:
|
||||||
from pynput import mouse, keyboard
|
from pynput import mouse, keyboard
|
||||||
|
|
||||||
def on_activity(*args):
|
def on_activity(*args):
|
||||||
self.last_activity = time.time()
|
self.last_activity = time.time()
|
||||||
|
|
||||||
# Start listeners
|
|
||||||
mouse.Listener(on_move=on_activity, on_click=on_activity).start()
|
mouse.Listener(on_move=on_activity, on_click=on_activity).start()
|
||||||
keyboard.Listener(on_press=on_activity).start()
|
keyboard.Listener(on_press=on_activity).start()
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("pynput not available, using fallback idle detection")
|
print("pynput not available, using fallback idle detection")
|
||||||
# Fallback: just use system idle time
|
# Fallback: periodically check system idle time
|
||||||
pass
|
def idle_fallback():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
idle_sec = get_idle_time()
|
||||||
|
if idle_sec < 5: # Consider active if idle < 5 sec
|
||||||
|
self.last_activity = time.time()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(2)
|
||||||
|
threading.Thread(target=idle_fallback, daemon=True).start()
|
||||||
|
|
||||||
def classify_window(self, title: str) -> str:
|
def classify_window(self, title: str) -> str:
|
||||||
"""Classify window as work, distraction, or neutral"""
|
|
||||||
title_lower = title.lower()
|
title_lower = title.lower()
|
||||||
rules = self.config.window_rules # Access through wrapper
|
rules = self.config.window_rules
|
||||||
|
|
||||||
# Check distractions first
|
|
||||||
for keyword in rules.get("distraction", []):
|
for keyword in rules.get("distraction", []):
|
||||||
if keyword.lower() in title_lower:
|
if keyword.lower() in title_lower:
|
||||||
return "distraction"
|
return "distraction"
|
||||||
|
|
||||||
# Then check work apps
|
|
||||||
for keyword in rules.get("work", []):
|
for keyword in rules.get("work", []):
|
||||||
if keyword.lower() in title_lower:
|
if keyword.lower() in title_lower:
|
||||||
return "work"
|
return "work"
|
||||||
|
|
||||||
# Otherwise neutral
|
|
||||||
return "neutral"
|
return "neutral"
|
||||||
|
|
||||||
def on_session_change(self, event_type: str):
|
def on_session_change(self, event_type: str):
|
||||||
"""Handle session lock/unlock events"""
|
|
||||||
if event_type == "locked":
|
if event_type == "locked":
|
||||||
self.session_locked = True
|
self.session_locked = True
|
||||||
# End current session if active
|
|
||||||
if self.session_manager.current_session:
|
if self.session_manager.current_session:
|
||||||
self.session_manager.end_current_session()
|
self.session_manager.end_current_session()
|
||||||
elif event_type == "unlocked":
|
elif event_type == "unlocked":
|
||||||
self.session_locked = False
|
self.session_locked = False
|
||||||
self.last_activity = time.time() # Reset idle time on unlock
|
self.last_activity = time.time()
|
||||||
|
|
||||||
def should_count_as_work(self) -> bool:
|
def should_count_as_work(self) -> bool:
|
||||||
"""Check if current conditions should count as work time"""
|
with self._state_lock:
|
||||||
# Check if tracking is enabled
|
if not self.is_tracking:
|
||||||
if not self.is_tracking:
|
return False
|
||||||
return False
|
if self.session_locked:
|
||||||
|
return False
|
||||||
# Check if session is locked
|
idle_time = time.time() - self.last_activity
|
||||||
if self.session_locked:
|
if idle_time > 60:
|
||||||
return False
|
return False
|
||||||
|
return self.window_classification == "work"
|
||||||
# Check idle time (more than 60 seconds of inactivity)
|
|
||||||
idle_time = time.time() - self.last_activity
|
|
||||||
if idle_time > 60:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check window classification
|
|
||||||
if self.window_classification != "work":
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def run_main_loop(self):
|
def run_main_loop(self):
|
||||||
"""Main tracking loop"""
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
if self.should_count_as_work():
|
if self.should_count_as_work():
|
||||||
# Increment current session duration
|
|
||||||
if self.session_manager.current_session:
|
if self.session_manager.current_session:
|
||||||
self.session_manager.increment_duration(1)
|
self.session_manager.increment_duration(1)
|
||||||
|
|
||||||
# Record time for current window
|
|
||||||
self.session_manager.record_window_time(self.active_window, 1)
|
self.session_manager.record_window_time(self.active_window, 1)
|
||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in main loop: {e}")
|
print(f"Error in main loop: {e}")
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
def toggle_tracking(self):
|
def toggle_tracking(self):
|
||||||
"""Toggle tracking on/off"""
|
with self._state_lock:
|
||||||
self.is_tracking = not self.is_tracking
|
self.is_tracking = not self.is_tracking
|
||||||
self.config.tracking_enabled = self.is_tracking
|
self.config.tracking_enabled = self.is_tracking
|
||||||
# Update raw config too
|
raw_config = {k: v for k, v in vars(self.config).items() if not k.startswith('_')}
|
||||||
raw_config = {key: value for key, value in vars(self.config).items() if not key.startswith('_')}
|
save_config(raw_config)
|
||||||
save_config(raw_config)
|
|
||||||
|
|
||||||
def create_task(self, task_id: str):
|
def create_task(self, task_id: str):
|
||||||
"""Create a new task and start tracking it"""
|
|
||||||
project_id = task_id.split('-')[0] if '-' in task_id else task_id
|
project_id = task_id.split('-')[0] if '-' in task_id else task_id
|
||||||
self.session_manager.create_session(task_id, project_id)
|
self.session_manager.create_session(task_id, project_id)
|
||||||
self.current_task_id = task_id
|
with self._state_lock:
|
||||||
self.config.current_task_id = task_id
|
self.current_task_id = task_id
|
||||||
raw_config = {key: value for key, value in vars(self.config).items() if not key.startswith('_')}
|
self.config.current_task_id = task_id
|
||||||
|
raw_config = {k: v for k, v in vars(self.config).items() if not k.startswith('_')}
|
||||||
save_config(raw_config)
|
save_config(raw_config)
|
||||||
|
|
||||||
def switch_task(self, task_id: str):
|
def switch_task(self, task_id: str):
|
||||||
"""Switch to an existing task"""
|
|
||||||
# End current session if exists
|
|
||||||
if self.session_manager.current_session:
|
if self.session_manager.current_session:
|
||||||
self.session_manager.end_current_session()
|
self.session_manager.end_current_session()
|
||||||
|
|
||||||
# Start new session for the task
|
|
||||||
project_id = task_id.split('-')[0] if '-' in task_id else task_id
|
project_id = task_id.split('-')[0] if '-' in task_id else task_id
|
||||||
self.session_manager.create_session(task_id, project_id)
|
self.session_manager.create_session(task_id, project_id)
|
||||||
self.current_task_id = task_id
|
with self._state_lock:
|
||||||
self.config.current_task_id = task_id
|
self.current_task_id = task_id
|
||||||
raw_config = {key: value for key, value in vars(self.config).items() if not key.startswith('_')}
|
self.config.current_task_id = task_id
|
||||||
|
raw_config = {k: v for k, v in vars(self.config).items() if not k.startswith('_')}
|
||||||
save_config(raw_config)
|
save_config(raw_config)
|
||||||
|
|
||||||
def get_available_tasks(self) -> List[str]:
|
def get_available_tasks(self) -> List[str]:
|
||||||
"""Get list of all available tasks"""
|
|
||||||
tasks_dir = "tasks"
|
tasks_dir = "tasks"
|
||||||
if not os.path.exists(tasks_dir):
|
if not os.path.exists(tasks_dir):
|
||||||
return []
|
return []
|
||||||
|
return [item for item in os.listdir(tasks_dir) if os.path.isdir(os.path.join(tasks_dir, item))]
|
||||||
tasks = []
|
|
||||||
for item in os.listdir(tasks_dir):
|
|
||||||
item_path = os.path.join(tasks_dir, item)
|
|
||||||
if os.path.isdir(item_path):
|
|
||||||
tasks.append(item)
|
|
||||||
|
|
||||||
return tasks
|
|
||||||
|
|
||||||
def has_active_session(self) -> bool:
|
def has_active_session(self) -> bool:
|
||||||
"""Check if there's an active session"""
|
|
||||||
return self.session_manager.current_session is not None
|
return self.session_manager.current_session is not None
|
||||||
|
|
||||||
def add_manual_adjustment(self, seconds: int, reason: str):
|
def add_manual_adjustment(self, seconds: int, reason: str):
|
||||||
"""Add a manual time adjustment"""
|
|
||||||
if self.session_manager.current_session:
|
if self.session_manager.current_session:
|
||||||
self.session_manager.add_manual_adjustment(seconds, reason)
|
self.session_manager.add_manual_adjustment(seconds, reason)
|
||||||
|
|
||||||
def get_current_session_duration(self) -> int:
|
def get_current_session_duration(self) -> int:
|
||||||
"""Get duration of current session"""
|
|
||||||
return self.session_manager.get_current_session_duration()
|
return self.session_manager.get_current_session_duration()
|
||||||
|
|
||||||
def get_total_work_today(self) -> int:
|
def get_total_work_today(self) -> int:
|
||||||
"""Get total work time for today"""
|
|
||||||
return self.session_manager.get_total_work_today()
|
return self.session_manager.get_total_work_today()
|
||||||
|
|
||||||
def get_planned_duration(self, task_id: str) -> Optional[int]:
|
def get_planned_duration(self, task_id: str) -> Optional[int]:
|
||||||
"""Get planned duration for a task (placeholder implementation)"""
|
|
||||||
# In a full implementation, this would look up the planned time for the task
|
|
||||||
# For now, return None to indicate no planned time
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def show_distraction_notification(self, window_title: str):
|
# Notification methods will be replaced by callbacks in main()
|
||||||
"""Show notification about distraction window"""
|
|
||||||
# In a full implementation, this would show a GUI notification
|
|
||||||
print(f"DISTRACTION: {window_title}")
|
|
||||||
|
|
||||||
def show_neutral_notification(self, window_title: str):
|
|
||||||
"""Show notification about neutral window"""
|
|
||||||
# In a full implementation, this would show a GUI notification
|
|
||||||
print(f"NEUTRAL APP: {window_title}")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def setup_hotkeys(tracker, overlay):
|
||||||
# Create main Tkinter root
|
|
||||||
root = tk.Tk()
|
|
||||||
root.title("Work Tracker") # Give it a title so it appears in taskbar
|
|
||||||
root.geometry("1x1+2000+2000") # Minimal size off-screen to avoid flicker
|
|
||||||
# Don't call withdraw() so the window manager recognizes it
|
|
||||||
|
|
||||||
# Initialize tracker
|
|
||||||
tracker = WorkTracker()
|
|
||||||
|
|
||||||
# Create overlay window - this will be the visible always-on-top window
|
|
||||||
overlay = OverlayWindow(root, tracker, tracker.config)
|
|
||||||
|
|
||||||
# Start main tracking loop in background
|
|
||||||
main_loop_thread = threading.Thread(target=tracker.run_main_loop, daemon=True)
|
|
||||||
main_loop_thread.start()
|
|
||||||
|
|
||||||
# Set up hotkeys
|
|
||||||
try:
|
try:
|
||||||
from pynput import keyboard
|
from pynput import keyboard
|
||||||
|
|
||||||
def on_hotkey(key):
|
def on_toggle():
|
||||||
try:
|
tracker.toggle_tracking()
|
||||||
if key == keyboard.Key.alt_l: # Left Alt pressed
|
overlay.update_display()
|
||||||
return # Wait for combination
|
|
||||||
elif hasattr(key, 'char') and key.char == 't':
|
|
||||||
# Ctrl+Alt+T: Toggle tracking
|
|
||||||
tracker.toggle_tracking()
|
|
||||||
overlay.update_display()
|
|
||||||
elif hasattr(key, 'char') and key.char == 'n':
|
|
||||||
# Ctrl+Alt+N: New task
|
|
||||||
overlay.new_task()
|
|
||||||
elif hasattr(key, 'char') and key.char == 's':
|
|
||||||
# Ctrl+Alt+S: End session
|
|
||||||
if tracker.session_manager.current_session:
|
|
||||||
tracker.session_manager.end_current_session()
|
|
||||||
overlay.update_display()
|
|
||||||
elif hasattr(key, 'char') and key.char == 'm':
|
|
||||||
# Ctrl+Alt+M: Adjust time
|
|
||||||
overlay.adjust_time()
|
|
||||||
except AttributeError:
|
|
||||||
# Special keys like Ctrl, Alt don't have char attribute
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Start keyboard listener
|
def on_new_task():
|
||||||
keyboard.Listener(on_press=on_hotkey).start()
|
overlay.new_task()
|
||||||
|
|
||||||
|
def on_end_session():
|
||||||
|
if tracker.session_manager.current_session:
|
||||||
|
tracker.session_manager.end_current_session()
|
||||||
|
overlay.update_display()
|
||||||
|
|
||||||
|
def on_adjust():
|
||||||
|
overlay.adjust_time()
|
||||||
|
|
||||||
|
hotkey_map = {
|
||||||
|
'<ctrl>+<alt>+t': on_toggle,
|
||||||
|
'<ctrl>+<alt>+n': on_new_task,
|
||||||
|
'<ctrl>+<alt>+s': on_end_session,
|
||||||
|
'<ctrl>+<alt>+m': on_adjust,
|
||||||
|
}
|
||||||
|
|
||||||
|
listener = keyboard.GlobalHotKeys(hotkey_map)
|
||||||
|
listener.start()
|
||||||
|
return listener
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("pynput not available, hotkeys disabled")
|
print("pynput not available, hotkeys disabled")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
root = tk.Tk()
|
||||||
|
root.title("Work Tracker")
|
||||||
|
root.geometry("1x1+2000+2000")
|
||||||
|
|
||||||
|
tracker = WorkTracker()
|
||||||
|
overlay = OverlayWindow(root, tracker, tracker.config)
|
||||||
|
|
||||||
|
# Inject notification callbacks
|
||||||
|
tracker.show_distraction_notification = lambda title: overlay.show_notification("Отвлечение", f"Вы используете: {title}")
|
||||||
|
tracker.show_neutral_notification = lambda title: overlay.show_notification("Нейтральное приложение", f"Окно: {title}")
|
||||||
|
|
||||||
|
main_loop_thread = threading.Thread(target=tracker.run_main_loop, daemon=True)
|
||||||
|
main_loop_thread.start()
|
||||||
|
|
||||||
|
hotkey_listener = setup_hotkeys(tracker, overlay)
|
||||||
|
|
||||||
# Run the GUI
|
|
||||||
try:
|
try:
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nShutting down...")
|
print("\nShutting down...")
|
||||||
# End any current session before exiting
|
|
||||||
if tracker.session_manager.current_session:
|
if tracker.session_manager.current_session:
|
||||||
tracker.session_manager.end_current_session()
|
tracker.session_manager.end_current_session()
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,45 @@
|
|||||||
import win32gui
|
import win32gui
|
||||||
|
import win32process
|
||||||
import win32con
|
import win32con
|
||||||
import win32api
|
import win32api
|
||||||
import threading
|
import threading
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
|
||||||
def get_active_window() -> str:
|
def get_active_window() -> str:
|
||||||
"""Get the currently active window title on Windows"""
|
"""Returns a string like 'YouTube - Chrome (chrome.exe)'"""
|
||||||
try:
|
try:
|
||||||
hwnd = win32gui.GetForegroundWindow()
|
hwnd = win32gui.GetForegroundWindow()
|
||||||
return win32gui.GetWindowText(hwnd) or "Unknown Window"
|
title = win32gui.GetWindowText(hwnd) or "Unknown Window"
|
||||||
except:
|
|
||||||
return "Error getting window title"
|
# Get process name
|
||||||
|
_, pid = win32process.GetWindowThreadProcessId(hwnd)
|
||||||
|
try:
|
||||||
|
proc = psutil.Process(pid)
|
||||||
|
exe_name = proc.name()
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||||
|
exe_name = "unknown.exe"
|
||||||
|
|
||||||
|
return f"{title} ({exe_name})"
|
||||||
|
except Exception:
|
||||||
|
return "Error getting window info"
|
||||||
|
|
||||||
|
|
||||||
def register_session_listener(callback: Callable[[str], None]) -> None:
|
def register_session_listener(callback: Callable[[str], None]) -> None:
|
||||||
"""Register a callback for Windows session lock/unlock events"""
|
"""Register a callback for Windows session lock/unlock events"""
|
||||||
|
|
||||||
def session_event_handler(hwnd, msg, wparam, lparam):
|
def session_event_handler(hwnd, msg, wparam, lparam):
|
||||||
# Check if the constant exists before using it
|
# Fallback numeric constants
|
||||||
try:
|
WM_WTSSESSION_CHANGE = 0x02B1
|
||||||
if msg == win32con.WM_WTSSESSION_CHANGE:
|
WTS_SESSION_LOCK = 0x1
|
||||||
if wparam == getattr(win32con, 'WTS_SESSION_LOCK', 0x1):
|
WTS_SESSION_UNLOCK = 0x2
|
||||||
callback("locked")
|
|
||||||
elif wparam == getattr(win32con, 'WTS_SESSION_UNLOCK', 0x2):
|
|
||||||
callback("unlocked")
|
|
||||||
except AttributeError:
|
|
||||||
# Some versions of pywin32 might not have these constants
|
|
||||||
# Use numeric values as fallback
|
|
||||||
WM_WTSSESSION_CHANGE = 0x02B1
|
|
||||||
WTS_SESSION_LOCK = 0x1
|
|
||||||
WTS_SESSION_UNLOCK = 0x2
|
|
||||||
|
|
||||||
if msg == WM_WTSSESSION_CHANGE:
|
if msg == WM_WTSSESSION_CHANGE:
|
||||||
if wparam == WTS_SESSION_LOCK:
|
if wparam == WTS_SESSION_LOCK:
|
||||||
callback("locked")
|
callback("locked")
|
||||||
elif wparam == WTS_SESSION_UNLOCK:
|
elif wparam == WTS_SESSION_UNLOCK:
|
||||||
callback("unlocked")
|
callback("unlocked")
|
||||||
|
|
||||||
return win32gui.DefWindowProc(hwnd, msg, wparam, lparam)
|
return win32gui.DefWindowProc(hwnd, msg, wparam, lparam)
|
||||||
|
|
||||||
@@ -52,33 +55,22 @@ def register_session_listener(callback: Callable[[str], None]) -> None:
|
|||||||
except:
|
except:
|
||||||
pass # Already registered
|
pass # Already registered
|
||||||
|
|
||||||
# Corrected CreateWindow call with all required parameters
|
|
||||||
hwnd = win32gui.CreateWindow(
|
hwnd = win32gui.CreateWindow(
|
||||||
wndclass.lpszClassName, # lpClassName
|
wndclass.lpszClassName,
|
||||||
"Session Watcher", # lpWindowName
|
"Session Watcher",
|
||||||
0, # dwStyle
|
0, 0, 0, 0, 0,
|
||||||
0, # x
|
0, 0, hinst, None
|
||||||
0, # y
|
|
||||||
0, # nWidth
|
|
||||||
0, # nHeight
|
|
||||||
0, # hWndParent
|
|
||||||
0, # hMenu
|
|
||||||
hinst, # hInstance
|
|
||||||
None # lParam
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Register for session notifications
|
# Register for session notifications
|
||||||
try:
|
try:
|
||||||
from win32ts import WTSRegisterSessionNotification
|
from win32ts import WTSRegisterSessionNotification
|
||||||
WTSRegisterSessionNotification(hwnd, 1) # NOTIFY_FOR_THIS_SESSION
|
WTSRegisterSessionNotification(hwnd, 1)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# pywin32 might not have win32ts on all systems
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Start message loop
|
|
||||||
win32gui.PumpMessages()
|
win32gui.PumpMessages()
|
||||||
|
|
||||||
# Run in a separate thread
|
|
||||||
thread = threading.Thread(target=run_message_loop, daemon=True)
|
thread = threading.Thread(target=run_message_loop, daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
@@ -95,4 +87,4 @@ def get_idle_time() -> int:
|
|||||||
windll.user32.GetLastInputInfo(byref(lastInputInfo))
|
windll.user32.GetLastInputInfo(byref(lastInputInfo))
|
||||||
|
|
||||||
millis = windll.kernel32.GetTickCount() - lastInputInfo.dwTime
|
millis = windll.kernel32.GetTickCount() - lastInputInfo.dwTime
|
||||||
return millis // 1000 # Convert milliseconds to seconds
|
return millis // 1000
|
||||||
@@ -2,7 +2,7 @@ import tkinter as tk
|
|||||||
from tkinter import simpledialog, messagebox
|
from tkinter import simpledialog, messagebox
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Any, Callable
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class OverlayWindow:
|
class OverlayWindow:
|
||||||
@@ -11,108 +11,87 @@ class OverlayWindow:
|
|||||||
self.tracker = tracker
|
self.tracker = tracker
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
# Configure window to be always on top
|
# Always on top, no decorations
|
||||||
self.root.attributes("-topmost", True)
|
self.root.attributes("-topmost", True)
|
||||||
self.root.overrideredirect(True) # Remove window decorations
|
self.root.overrideredirect(True)
|
||||||
|
self.root.geometry("320x180+100+100")
|
||||||
|
|
||||||
# Force the geometry to be set correctly
|
# Semi-transparent by default
|
||||||
self.root.geometry("300x150+100+100")
|
self.root.wm_attributes("-alpha", 0.7)
|
||||||
|
|
||||||
|
# Hover effects
|
||||||
|
self.root.bind("<Enter>", self.on_hover)
|
||||||
|
self.root.bind("<Leave>", self.on_leave)
|
||||||
|
|
||||||
# Create UI elements
|
|
||||||
self.create_widgets()
|
self.create_widgets()
|
||||||
|
self.setup_drag_full()
|
||||||
# Bind drag functionality
|
|
||||||
self.setup_drag()
|
|
||||||
|
|
||||||
# Bind right-click to show context menu for closing
|
|
||||||
self.root.bind("<Button-3>", self.show_context_menu)
|
self.root.bind("<Button-3>", self.show_context_menu)
|
||||||
|
|
||||||
# Update display periodically
|
# Start periodic updates
|
||||||
self.update_display_periodically()
|
self.update_display_periodically()
|
||||||
|
|
||||||
|
def on_hover(self, event=None):
|
||||||
|
self.root.wm_attributes("-alpha", 1.0)
|
||||||
|
|
||||||
|
def on_leave(self, event=None):
|
||||||
|
self.root.wm_attributes("-alpha", 0.7)
|
||||||
|
|
||||||
|
def setup_drag_full(self):
|
||||||
|
"""Allow dragging the entire window from any point."""
|
||||||
|
def start_move(event):
|
||||||
|
self._drag_x = event.x
|
||||||
|
self._drag_y = event.y
|
||||||
|
|
||||||
|
def do_move(event):
|
||||||
|
x = self.root.winfo_x() + (event.x - self._drag_x)
|
||||||
|
y = self.root.winfo_y() + (event.y - self._drag_y)
|
||||||
|
self.root.geometry(f"+{x}+{y}")
|
||||||
|
|
||||||
|
self.root.bind("<Button-1>", start_move)
|
||||||
|
self.root.bind("<B1-Motion>", do_move)
|
||||||
|
|
||||||
def create_widgets(self):
|
def create_widgets(self):
|
||||||
"""Create all UI widgets"""
|
|
||||||
# Main frame with visible background
|
|
||||||
main_frame = tk.Frame(self.root, bg='#f0f0f0', bd=2, relief='solid')
|
main_frame = tk.Frame(self.root, bg='#f0f0f0', bd=2, relief='solid')
|
||||||
main_frame.pack(fill='both', expand=True, padx=2, pady=2)
|
main_frame.pack(fill='both', expand=True, padx=2, pady=2)
|
||||||
|
|
||||||
# Status line (tracking enabled/disabled)
|
self.status_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 9))
|
||||||
self.status_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 10))
|
|
||||||
self.status_label.pack(anchor='w', padx=5, pady=2)
|
self.status_label.pack(anchor='w', padx=5, pady=2)
|
||||||
|
|
||||||
# Task info
|
|
||||||
self.task_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 10, 'bold'))
|
self.task_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 10, 'bold'))
|
||||||
self.task_label.pack(anchor='w', padx=5, pady=2)
|
self.task_label.pack(anchor='w', padx=5, pady=2)
|
||||||
|
|
||||||
# Window info
|
|
||||||
self.window_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 9))
|
self.window_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 9))
|
||||||
self.window_label.pack(anchor='w', padx=5, pady=2)
|
self.window_label.pack(anchor='w', padx=5, pady=2)
|
||||||
|
|
||||||
# Total time info
|
|
||||||
self.total_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 9))
|
self.total_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 9))
|
||||||
self.total_label.pack(anchor='w', padx=5, pady=2)
|
self.total_label.pack(anchor='w', padx=5, pady=2)
|
||||||
|
|
||||||
# Buttons frame
|
|
||||||
buttons_frame = tk.Frame(main_frame, bg='#f0f0f0')
|
buttons_frame = tk.Frame(main_frame, bg='#f0f0f0')
|
||||||
buttons_frame.pack(fill='x', padx=5, pady=5)
|
buttons_frame.pack(fill='x', padx=5, pady=5)
|
||||||
|
|
||||||
# Tracking toggle button
|
self.track_btn = tk.Button(buttons_frame, text="▶", command=self.toggle_tracking, width=8)
|
||||||
self.track_btn = tk.Button(buttons_frame, text="●", command=self.toggle_tracking, width=3)
|
|
||||||
self.track_btn.pack(side='left', padx=2)
|
self.track_btn.pack(side='left', padx=2)
|
||||||
|
|
||||||
# New task button
|
tk.Button(buttons_frame, text="+Задача", command=self.new_task, width=8).pack(side='left', padx=2)
|
||||||
new_task_btn = tk.Button(buttons_frame, text="N", command=self.new_task, width=3)
|
tk.Button(buttons_frame, text="↔Задача", command=self.switch_task, width=8).pack(side='left', padx=2)
|
||||||
new_task_btn.pack(side='left', padx=2)
|
tk.Button(buttons_frame, text="±Время", command=self.adjust_time, width=8).pack(side='left', padx=2)
|
||||||
|
|
||||||
# Switch task button
|
|
||||||
switch_task_btn = tk.Button(buttons_frame, text="S", command=self.switch_task, width=3)
|
|
||||||
switch_task_btn.pack(side='left', padx=2)
|
|
||||||
|
|
||||||
# Adjust time button
|
|
||||||
adjust_btn = tk.Button(buttons_frame, text="±", command=self.adjust_time, width=3)
|
|
||||||
adjust_btn.pack(side='left', padx=2)
|
|
||||||
|
|
||||||
def setup_drag(self):
|
|
||||||
"""Setup window dragging functionality"""
|
|
||||||
|
|
||||||
def start_move(event):
|
|
||||||
self.root.x = event.x
|
|
||||||
self.root.y = event.y
|
|
||||||
|
|
||||||
def do_move(event):
|
|
||||||
x = self.root.winfo_x() + (event.x - self.root.x)
|
|
||||||
y = self.root.winfo_y() + (event.y - self.root.y)
|
|
||||||
self.root.geometry(f"+{x}+{y}")
|
|
||||||
|
|
||||||
# Bind drag events to the main label (covers entire window)
|
|
||||||
self.status_label.bind("<Button-1>", start_move)
|
|
||||||
self.status_label.bind("<B1-Motion>", do_move)
|
|
||||||
|
|
||||||
# Also bind to other labels for full coverage
|
|
||||||
for widget in [self.task_label, self.window_label, self.total_label]:
|
|
||||||
widget.bind("<Button-1>", start_move)
|
|
||||||
widget.bind("<B1-Motion>", do_move)
|
|
||||||
|
|
||||||
def show_context_menu(self, event):
|
def show_context_menu(self, event):
|
||||||
"""Show context menu with close option"""
|
|
||||||
context_menu = tk.Menu(self.root, tearoff=0)
|
context_menu = tk.Menu(self.root, tearoff=0)
|
||||||
context_menu.add_command(label="Закрыть", command=self.root.quit)
|
context_menu.add_command(label="Закрыть", command=self.root.quit)
|
||||||
context_menu.post(event.x_root, event.y_root)
|
context_menu.post(event.x_root, event.y_root)
|
||||||
|
|
||||||
def toggle_tracking(self):
|
def toggle_tracking(self):
|
||||||
"""Toggle tracking state"""
|
|
||||||
self.tracker.toggle_tracking()
|
self.tracker.toggle_tracking()
|
||||||
self.update_display()
|
self.update_display()
|
||||||
|
|
||||||
def new_task(self):
|
def new_task(self):
|
||||||
"""Create a new task"""
|
|
||||||
task_id = simpledialog.askstring("Новая задача", "Введите ID задачи (например, PRJ-123):")
|
task_id = simpledialog.askstring("Новая задача", "Введите ID задачи (например, PRJ-123):")
|
||||||
if task_id:
|
if task_id and task_id.strip():
|
||||||
self.tracker.create_task(task_id)
|
self.tracker.create_task(task_id.strip())
|
||||||
self.update_display()
|
self.update_display()
|
||||||
|
|
||||||
def switch_task(self):
|
def switch_task(self):
|
||||||
"""Switch to an existing task"""
|
|
||||||
available_tasks = self.tracker.get_available_tasks()
|
available_tasks = self.tracker.get_available_tasks()
|
||||||
if not available_tasks:
|
if not available_tasks:
|
||||||
messagebox.showinfo("Переключить задачу", "Нет доступных задач")
|
messagebox.showinfo("Переключить задачу", "Нет доступных задач")
|
||||||
@@ -124,146 +103,117 @@ class OverlayWindow:
|
|||||||
f"Доступные задачи:\n{task_list}\n\nВведите ID задачи:"
|
f"Доступные задачи:\n{task_list}\n\nВведите ID задачи:"
|
||||||
)
|
)
|
||||||
|
|
||||||
if selected_task and selected_task in available_tasks:
|
if selected_task and selected_task.strip() in available_tasks:
|
||||||
self.tracker.switch_task(selected_task)
|
self.tracker.switch_task(selected_task.strip())
|
||||||
self.update_display()
|
self.update_display()
|
||||||
|
|
||||||
def adjust_time(self):
|
def adjust_time(self):
|
||||||
"""Show dialog to adjust time manually"""
|
|
||||||
self.show_adjust_time_dialog()
|
self.show_adjust_time_dialog()
|
||||||
|
|
||||||
def show_adjust_time_dialog(self):
|
def show_adjust_time_dialog(self):
|
||||||
"""Show dialog for manual time adjustment"""
|
|
||||||
adjustment_window = tk.Toplevel(self.root)
|
adjustment_window = tk.Toplevel(self.root)
|
||||||
adjustment_window.title("Ручная корректировка времени")
|
adjustment_window.title("Ручная корректировка времени")
|
||||||
adjustment_window.geometry("300x150")
|
adjustment_window.geometry("350x200")
|
||||||
adjustment_window.transient(self.root)
|
adjustment_window.transient(self.root)
|
||||||
adjustment_window.grab_set() # Modal window
|
adjustment_window.grab_set()
|
||||||
|
|
||||||
# Operation selection
|
|
||||||
operation_var = tk.StringVar(value="+")
|
operation_var = tk.StringVar(value="+")
|
||||||
tk.Radiobutton(adjustment_window, text="Добавить время", variable=operation_var, value="+").pack(anchor='w',
|
tk.Radiobutton(adjustment_window, text="Добавить время", variable=operation_var, value="+").pack(anchor='w', padx=10, pady=2)
|
||||||
padx=10,
|
tk.Radiobutton(adjustment_window, text="Вычесть время", variable=operation_var, value="-").pack(anchor='w', padx=10, pady=2)
|
||||||
pady=5)
|
|
||||||
tk.Radiobutton(adjustment_window, text="Вычесть время", variable=operation_var, value="-").pack(anchor='w',
|
|
||||||
padx=10, pady=5)
|
|
||||||
|
|
||||||
# Minutes entry
|
tk.Label(adjustment_window, text="Минуты:").pack(anchor='w', padx=10, pady=(10,0))
|
||||||
tk.Label(adjustment_window, text="Минут:").pack(anchor='w', padx=10)
|
|
||||||
minutes_entry = tk.Entry(adjustment_window)
|
minutes_entry = tk.Entry(adjustment_window)
|
||||||
minutes_entry.pack(padx=10, pady=5)
|
minutes_entry.pack(padx=10, pady=2)
|
||||||
minutes_entry.insert(0, "15") # Default value
|
minutes_entry.insert(0, "15")
|
||||||
|
|
||||||
# Reason entry
|
tk.Label(adjustment_window, text="Причина:").pack(anchor='w', padx=10, pady=(10,0))
|
||||||
tk.Label(adjustment_window, text="Причина:").pack(anchor='w', padx=10)
|
|
||||||
reason_entry = tk.Entry(adjustment_window)
|
reason_entry = tk.Entry(adjustment_window)
|
||||||
reason_entry.pack(padx=10, pady=5)
|
reason_entry.pack(padx=10, pady=2)
|
||||||
reason_entry.insert(0, "Ручная корректировка") # Default value
|
reason_entry.insert(0, "Перерыв / встреча")
|
||||||
|
|
||||||
def apply_adjustment():
|
def apply_adjustment():
|
||||||
try:
|
try:
|
||||||
minutes = int(minutes_entry.get())
|
minutes = int(minutes_entry.get())
|
||||||
|
if minutes <= 0:
|
||||||
|
raise ValueError("Minutes must be positive")
|
||||||
operation = operation_var.get()
|
operation = operation_var.get()
|
||||||
seconds = minutes * 60
|
seconds = minutes * 60 * (1 if operation == "+" else -1)
|
||||||
if operation == "-":
|
reason = reason_entry.get().strip()
|
||||||
seconds = -seconds
|
if not reason:
|
||||||
|
reason = "Без причины"
|
||||||
reason = reason_entry.get()
|
|
||||||
|
|
||||||
if self.tracker.has_active_session():
|
if self.tracker.has_active_session():
|
||||||
self.tracker.add_manual_adjustment(seconds, reason)
|
self.tracker.add_manual_adjustment(seconds, reason)
|
||||||
messagebox.showinfo("Успешно", f"Время скорректировано: {operation}{minutes} мин")
|
messagebox.showinfo("Успех", f"Изменено: {operation}{minutes} мин\nПричина: {reason}")
|
||||||
adjustment_window.destroy()
|
adjustment_window.destroy()
|
||||||
self.update_display()
|
self.update_display()
|
||||||
else:
|
else:
|
||||||
messagebox.showwarning("Внимание", "Нет активной сессии для корректировки")
|
messagebox.showwarning("Ошибка", "Нет активной сессии!")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
messagebox.showerror("Ошибка", "Введите корректное число минут")
|
messagebox.showerror("Ошибка", "Введите положительное число минут")
|
||||||
|
|
||||||
# Apply button
|
tk.Button(adjustment_window, text="Применить", command=apply_adjustment, bg='lightblue').pack(pady=10)
|
||||||
apply_btn = tk.Button(adjustment_window, text="Применить", command=apply_adjustment)
|
|
||||||
apply_btn.pack(pady=10)
|
|
||||||
|
|
||||||
def update_display(self):
|
def update_display(self):
|
||||||
"""Update the display with current information"""
|
# Use lock to avoid race conditions
|
||||||
# Update tracking status
|
with self.tracker._state_lock:
|
||||||
track_status = "●" if self.config.tracking_enabled else "○"
|
track_status = "▶" if self.config.tracking_enabled else "⏸"
|
||||||
status_text = f"[{track_status}] Трекинг: {'ВКЛ' if self.config.tracking_enabled else 'ВЫКЛ'}"
|
status_text = f"[{track_status}] Трекинг: {'ВКЛ' if self.config.tracking_enabled else 'ВЫКЛ'}"
|
||||||
self.status_label.config(text=status_text)
|
self.status_label.config(text=status_text)
|
||||||
|
|
||||||
# Update task information
|
if self.config.current_task_id:
|
||||||
if self.config.current_task_id:
|
current_duration = self.tracker.get_current_session_duration()
|
||||||
# Get current session duration
|
current_str = self.format_duration(current_duration)
|
||||||
current_duration = self.tracker.get_current_session_duration()
|
task_text = f"Задача: {self.config.current_task_id} | {current_str}"
|
||||||
current_duration_str = self.format_duration(current_duration)
|
|
||||||
|
|
||||||
# Get planned duration if available
|
|
||||||
planned_duration = self.tracker.get_planned_duration(self.config.current_task_id)
|
|
||||||
if planned_duration:
|
|
||||||
planned_str = self.format_duration(planned_duration * 60) # Convert minutes to seconds
|
|
||||||
task_text = f"Задача: {self.config.current_task_id} | {current_duration_str} / {planned_str}"
|
|
||||||
else:
|
else:
|
||||||
task_text = f"Задача: {self.config.current_task_id} | {current_duration_str}"
|
task_text = "Нет активной задачи"
|
||||||
else:
|
self.task_label.config(text=task_text)
|
||||||
task_text = "Нет активной задачи"
|
|
||||||
|
|
||||||
self.task_label.config(text=task_text)
|
|
||||||
|
|
||||||
# Update window information
|
|
||||||
if hasattr(self.tracker, 'active_window'):
|
|
||||||
window_title = getattr(self.tracker, 'active_window', 'Неизвестно')
|
window_title = getattr(self.tracker, 'active_window', 'Неизвестно')
|
||||||
classification = getattr(self.tracker, 'window_classification', 'neutral')
|
classification = getattr(self.tracker, 'window_classification', 'neutral')
|
||||||
|
|
||||||
class_symbol = {"work": "✅", "distraction": "⚠", "neutral": "⚪"}[classification]
|
class_symbol = {"work": "✅", "distraction": "⚠", "neutral": "⚪"}[classification]
|
||||||
window_text = f"Окно: {window_title[:30]}{'...' if len(window_title) > 30 else ''} {class_symbol}"
|
display_title = window_title[:40] + '...' if len(window_title) > 40 else window_title
|
||||||
|
window_text = f"Окно: {display_title} {class_symbol}"
|
||||||
self.window_label.config(text=window_text)
|
self.window_label.config(text=window_text)
|
||||||
else:
|
|
||||||
self.window_label.config(text="Окно: Неизвестно")
|
|
||||||
|
|
||||||
# Update total time today
|
total_today = self.tracker.get_total_work_today()
|
||||||
total_today = self.tracker.get_total_work_today()
|
total_str = self.format_duration(total_today)
|
||||||
total_str = self.format_duration(total_today)
|
self.total_label.config(text=f"Сегодня: {total_str}")
|
||||||
self.total_label.config(text=f"Всего сегодня: {total_str}")
|
|
||||||
|
|
||||||
# Update button colors based on tracking status
|
# Update button appearance
|
||||||
if self.config.tracking_enabled:
|
self.track_btn.config(
|
||||||
self.track_btn.config(bg='lightgreen', text="●")
|
text="⏸" if self.config.tracking_enabled else "▶",
|
||||||
else:
|
bg='lightgreen' if self.config.tracking_enabled else 'lightcoral'
|
||||||
self.track_btn.config(bg='lightcoral', text="○")
|
)
|
||||||
|
|
||||||
def format_duration(self, seconds: int) -> str:
|
def format_duration(self, seconds: int) -> str:
|
||||||
"""Format seconds into human-readable string (h:mm)"""
|
"""Format as H:MM:SS or M:SS"""
|
||||||
if seconds < 0:
|
if seconds < 0:
|
||||||
seconds = 0
|
seconds = 0
|
||||||
|
hours, remainder = divmod(seconds, 3600)
|
||||||
hours = seconds // 3600
|
minutes, secs = divmod(remainder, 60)
|
||||||
minutes = (seconds % 3600) // 60
|
|
||||||
|
|
||||||
if hours > 0:
|
if hours > 0:
|
||||||
return f"{hours}ч{minutes:02d}м"
|
return f"{hours}:{minutes:02d}:{secs:02d}"
|
||||||
else:
|
else:
|
||||||
return f"{minutes}м"
|
return f"{minutes}:{secs:02d}"
|
||||||
|
|
||||||
def update_display_periodically(self):
|
def update_display_periodically(self):
|
||||||
"""Update display every second"""
|
|
||||||
self.update_display()
|
self.update_display()
|
||||||
self.root.after(1000, self.update_display_periodically)
|
self.root.after(1000, self.update_display_periodically)
|
||||||
|
|
||||||
def show_notification(self, title: str, message: str, duration: int = 5000):
|
def show_notification(self, title: str, message: str, duration: int = 5000):
|
||||||
"""Show a temporary notification"""
|
"""Show a temporary notification window"""
|
||||||
# Create notification window
|
|
||||||
notif = tk.Toplevel(self.root)
|
notif = tk.Toplevel(self.root)
|
||||||
notif.title(title)
|
notif.title(title)
|
||||||
notif.geometry("250x80+200+200")
|
notif.geometry("280x90+200+200")
|
||||||
notif.overrideredirect(True)
|
notif.overrideredirect(True)
|
||||||
notif.attributes("-topmost", True)
|
notif.attributes("-topmost", True)
|
||||||
|
notif.wm_attributes("-alpha", 0.9)
|
||||||
|
|
||||||
# Style similar to main window
|
|
||||||
frame = tk.Frame(notif, bg='lightyellow', bd=2, relief='solid')
|
frame = tk.Frame(notif, bg='lightyellow', bd=2, relief='solid')
|
||||||
frame.pack(fill='both', expand=True)
|
frame.pack(fill='both', expand=True)
|
||||||
|
|
||||||
tk.Label(frame, text=title, bg='lightyellow', font=('Arial', 10, 'bold')).pack(pady=5)
|
tk.Label(frame, text=title, bg='lightyellow', font=('Arial', 10, 'bold')).pack(pady=5)
|
||||||
tk.Label(frame, text=message, bg='lightyellow', font=('Arial', 9)).pack(pady=5)
|
tk.Label(frame, text=message[:80], bg='lightyellow', font=('Arial', 9), wraplength=260).pack(pady=5)
|
||||||
|
|
||||||
# Close after specified duration
|
|
||||||
notif.after(duration, notif.destroy)
|
notif.after(duration, notif.destroy)
|
||||||
Reference in New Issue
Block a user