konawall-py/gui.py
2023-10-06 14:43:37 -07:00

274 lines
No EOL
9.9 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import wx
import wx.adv
import tempfile
from PIL import Image, ImageDraw
import os
import sys
import logging
import screeninfo
import tomllib
import subprocess
from environment import set_environment_wallpapers, detect_environment
from module_loader import import_dir, environment_handlers, source_handlers
from custom_print import kv_print
from humanfriendly import format_timespan
class Konawall(wx.adv.TaskBarIcon):
def __init__(self, file_logger):
# Prevents it from closing before it has done any work on macOS
if wx.Platform == "__WXMAC__":
self.hidden_frame = wx.Frame(None)
self.hidden_frame.Hide()
self.wallpaper_rotation_counter = 0
self.file_logger = file_logger
self.loaded_before = False
# Call the super function, make sure that the type is the statusitem for macOS
wx.adv.TaskBarIcon.__init__(self, wx.adv.TBI_CUSTOM_STATUSITEM)
# Detect environment and timer settings
self.environment = detect_environment()
self.toggle_wallpaper_rotation_item = None
self.wallpaper_rotation_timer = wx.Timer(self, wx.ID_ANY)
# Reload (actually load) the config and modules.
self.reload()
self.load_modules()
# Start the timer to run every second
self.wallpaper_rotation_timer.Start(1000)
# Set up the taskbar icon, menu, bindings, ...
icon = self.generate_icon()
self.SetIcon(icon, "Konawall")
self.create_menu()
self.create_bindings()
# Run the first time, manually
self.run(None)
# wxPython requires a wx.Bitmap, so we generate one from a PIL.Image
def generate_icon(self):
width = 128
height = 128
# Missing texture style, magenta and black checkerboard
image = Image.new('RGB', (width, height), (0, 0, 0))
dc = ImageDraw.Draw(image)
dc.rectangle((0, 0, width//2, height//2), fill=(255, 0, 255))
dc.rectangle((width//2, height//2, width, height), fill=(255, 0, 255))
if "wxMSW" in wx.PlatformInfo:
image = image.Scale(16, 16)
elif "wxGTK" in wx.PlatformInfo:
image = image.Scale(22, 22)
# Write image to temporary file
temp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
image.save(temp.name)
# Convert to wxPython icon
icon = wx.Icon()
icon.CopyFromBitmap(wx.Bitmap(temp.name))
return icon
def toggle_wallpaper_rotation_status(self):
return f"{'Dis' if self.rotate else 'En'}able Timer"
# Load in our source and environment handlers
def load_modules(self):
import_dir(os.path.join(os.path.dirname(os.path.abspath( __file__ )), "sources"))
kv_print("Loaded source handlers", ", ".join(source_handlers), level="debug")
import_dir(os.path.join(os.path.dirname(os.path.abspath( __file__ )), "environments"))
kv_print("Loaded environment handlers", ", ".join(environment_handlers), level="debug")
# Load a TOML file's key-value pairs into our class
def read_config(self):
if os.path.isfile("config.toml"):
# If the config file exists, load it as a dictionary into the config variable.
with open("config.toml", "rb") as f:
config = tomllib.load(f)
# for every key-value pair in the config variable , set the corresponding attribute of our class to it
for k, v in config.items():
kv_print(f"Loaded {k}", v)
setattr(self, k, v)
else:
# If there is no config file, get complainy.
dialog = wx.MessageDialog(
None,
"No config file found, using defaults.",
"Konawall",
wx.OK|wx.ICON_INFORMATION
)
dialog.ShowModal()
dialog.Destroy()
# Set some arbitrary defaults
self.rotate = True
self.interval = 10*60
self.tags = ["rating:s"]
self.logging = {}
self.logging["file"] = "INFO"
# Create the menu
def create_menu(self):
# Make it easier to define menu items
def create_menu_item(menu, label, func, help="", kind=wx.ITEM_NORMAL):
item = wx.MenuItem(menu, wx.ID_ANY, label)
menu.Bind(wx.EVT_MENU, func, id=item.GetId())
menu.Append(item)
return item
def create_separator(menu):
item = wx.MenuItem(menu, id=wx.ID_SEPARATOR, kind=wx.ITEM_SEPARATOR)
menu.Append(item)
return item
# Create our Menu object
self.menu = wx.Menu()
# Time remaining for automatic wallpaper rotation
self.wallpaper_rotation_status = wx.MenuItem(self.menu, -1, "Time remaining")
self.wallpaper_rotation_status.Enable(False)
self.menu.Append(self.wallpaper_rotation_status)
create_separator(self.menu)
# Change wallpapers
create_menu_item(
self.menu,
"Rotate Wallpapers",
self.run,
"Fetch new wallpapers and set them as your wallpapers"
)
# Toggle automatic wallpaper rotation
self.toggle_wallpaper_rotation_item = create_menu_item(
self.menu,
self.toggle_wallpaper_rotation_status(),
self.toggle_wallpaper_rotation,
"Toggle the automatic wallpaper rotation timer"
)
create_separator(self.menu)
# Interactive config editing
create_menu_item(
self.menu,
"Edit Config",
self.edit_config,
"Interactively edit the config file"
)
# Exit
create_menu_item(
self.menu,
"Exit",
self.Destroy,
"Quit the application"
)
# Interactively edit the config file
def edit_config(self, event):
kv_print("User is editing", "config.toml")
# Check if we're on Windows, if so use Notepad
if sys.platform == "win32":
# I don't even know how to detect the default editor on Windows
subprocess.call("notepad.exe config.toml")
else:
# Open config file in default editor
subprocess.call(f"{os.environ['EDITOR']} config.toml")
# When file is done being edited, reload config
kv_print("User has edited", "config.toml")
self.reload()
# Reload the application
def reload(self):
kv_print(f"{'Rel' if self.loaded_before else 'L'}oading config from", "config.toml")
self.read_config()
# Handle finding the log level
if "file" in self.logging:
file_log_level = getattr(logging, self.logging["file"])
else:
file_log_level = logging.INFO
self.file_logger.setLevel(file_log_level)
if self.loaded_before == True:
# If we're reloading, we need to make sure the timer and menu item reflect our current state.
self.respect_wallpaper_rotation_toggle()
# Finished loading
self.loaded_before = True
# Set whether to rotate wallpapers automatically or not
def toggle_wallpaper_rotation(self, event):
self.rotate = not self.rotate
self.respect_wallpaper_rotation_toggle()
# Update the timer and the menu item to reflect our current state
def respect_wallpaper_rotation_toggle(self):
if self.rotate and not self.wallpaper_rotation_timer.IsRunning():
self.wallpaper_rotation_timer.Start(1000)
elif not self.rotate and self.wallpaper_rotation_timer.IsRunning():
self.wallpaper_rotation_timer.Stop()
# Set the time left counter to show that it is disabled
self.wallpaper_rotation_status.SetItemLabel(f"Automatic wallpaper rotation disabled")
# Update the menu item for the toggle
self.toggle_wallpaper_rotation_item.SetItemLabel(self.toggle_wallpaper_rotation_status())
# Update wallpaper rotation time left counter
def respect_wallpaper_rotation_status(self):
self.wallpaper_rotation_status.SetItemLabel(f"{format_timespan(self.interval - self.wallpaper_rotation_counter)} remaining")
# Perform the purpose of the application; get new wallpaper media and set 'em.
def run(self, event):
displays = screeninfo.get_monitors()
count = len(displays)
files = source_handlers[self.source](count, self.tags)
set_environment_wallpapers(self.environment, files, displays)
# For macOS
def CreatePopupMenu(self):
self.PopupMenu(self.menu)
# For everybody else who has bindable events
def show_menu(self, event):
self.PopupMenu(self.menu)
# Every second, check if the wallpaper rotation timer has ticked over
def handle_timer_tick(self, event):
if self.wallpaper_rotation_counter >= self.interval:
# If it has, run the fetch and set mechanism
self.run(None)
self.wallpaper_rotation_counter = 0
else:
self.wallpaper_rotation_counter += 1
# Update the time left counter
self.respect_wallpaper_rotation_status()
# Bind application events
def create_bindings(self):
self.Bind(wx.adv.EVT_TASKBAR_LEFT_DOWN, self.run)
self.Bind(wx.adv.EVT_TASKBAR_RIGHT_DOWN, self.show_menu)
# Implement the wallpaper rotation timer
self.Bind(wx.EVT_TIMER, self.handle_timer_tick, self.wallpaper_rotation_timer)
def main():
file_logger = logging.FileHandler("app.log", mode="a")
#console_logger = logging.StreamHandler()
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[
#console_logger,
file_logger,
]
)
app = wx.App(redirect=False)
app.SetExitOnFrameDelete(False)
Konawall(file_logger)
app.MainLoop()
if __name__ == "__main__":
main()