diff --git a/daemon.py b/daemon.py index 5f5373b..c4d7cb1 100755 --- a/daemon.py +++ b/daemon.py @@ -9,10 +9,10 @@ from modules.toggle import CommandToggleControl from modules.functional import * logging.basicConfig(level=logging.DEBUG) -logging.getLogger('modules.core').setLevel(logging.WARNING) +#logging.getLogger('modules.core').setLevel(logging.WARNING) modules.Application( - CycleControl(modules.ScreenLayoutCycleAction(name=partial(drop_from, '.')), scroll_actions=False), + modules.ScreenLayoutAction(name=partial(drop_from, '.')), modules.GroupedControl( modules.CaffeineControl(), modules.RedshiftControl(), diff --git a/modules/__init__.py b/modules/__init__.py index e0e4bb0..1e4f4d0 100644 --- a/modules/__init__.py +++ b/modules/__init__.py @@ -3,4 +3,4 @@ from .core import GroupedControl, ActionWrapperControl from .caffeine import CaffeineControl from .redshift import RedshiftControl from .volume import VolumeControl -from .screenlayout import ScreenLayoutCycleAction +from .screenlayout import ScreenLayoutAction diff --git a/modules/cycle.py b/modules/cycle.py index e466f58..2e49a0a 100644 --- a/modules/cycle.py +++ b/modules/cycle.py @@ -94,6 +94,13 @@ class OrderedDictCycleAction(AbstractCycleAction): for k in self.__items.keys(): return k + @current.setter + def current(self, value): + if value not in self.__items.keys(): + raise ValueError("The given value is not a valid item") + while self.current != value: + self.__items.move_to_end(self.current) + @property def visible(self): return len(self.__items) > 1 @@ -101,6 +108,9 @@ class OrderedDictCycleAction(AbstractCycleAction): def __str__(self): return self.__items[self.current] + def __len__(self): + return len(self.__items) + class CycleControl(WrappingControl): """ diff --git a/modules/screenlayout.py b/modules/screenlayout.py index 9fd0226..f8e4c1e 100644 --- a/modules/screenlayout.py +++ b/modules/screenlayout.py @@ -1,13 +1,40 @@ from collections import OrderedDict -from .cycle import OrderedDictCycleAction +from .cycle import OrderedDictCycleAction, CycleControl +from .core import WrappingControl, action from .util import process_reaper import logging import os +import stat import subprocess import argparse +try: + import tkinter +except ImportError: + tkinter = None +import math +import threading +import re logger = logging.getLogger(__name__) +MAX_ITEMS_BEFORE_POPUP=3 + +class ScreenLayoutAction(WrappingControl): + def __init__(self, *a, **k): + self.__screen_layout_cycle = ScreenLayoutCycleAction(*a, **k) + super().__init__(CycleControl(self.__screen_layout_cycle, scroll_actions=False)) + + def __str__(self): + parent = super().__str__() + if tkinter is not None and len(self.__screen_layout_cycle) > MAX_ITEMS_BEFORE_POPUP: + parent = re.sub(r'(?:)+([^<>]*)(?:<\/action>)+', r'\1', parent) # Clear all action tags + parent = action( + self.create_pipe_command('screenlayout'), + parent + ) + + return parent + class ScreenLayoutCycleAction(OrderedDictCycleAction): def __init__(self, name: callable): self.__od = OrderedDict() @@ -28,22 +55,25 @@ class ScreenLayoutCycleAction(OrderedDictCycleAction): def next(self): super().next() - self.__set_screen_layout(next_layout=self.next) + self.__set_screen_layout(next_layout=self.next, item=self.current) def prev(self): super().prev() - self.__set_screen_layout(next_layout=self.prev) + self.__set_screen_layout(next_layout=self.prev, item=self.current) def periodic(self): if self.__inhibited: self.__inhibited = False - self.__set_screen_layout(next_layout=self.next) + self.__set_screen_layout(next_layout=self.next, item=self.current) return True return False def respond_to(self, command: str): if command == 'screenlayout': - self.next() + if tkinter is not None and len(self) > MAX_ITEMS_BEFORE_POPUP: + threading.Thread(target=self.__create_tk()).start() + else: + self.next() return True else: return super().respond_to(command) @@ -52,18 +82,54 @@ class ScreenLayoutCycleAction(OrderedDictCycleAction): return self.__naming_func(super().__str__()) def __load_layouts(self, directory): + self.__od.clear() entries = os.scandir(directory) for entry in entries: if entry.is_file(): - logger.debug('Found file %s', entry.path) - self.__od[entry.path] = entry.name + mode = entry.stat().st_mode + if mode & stat.S_IXUSR or mode & stat.S_IXGRP or mode & stat.S_IXOTH: + logger.debug('Found file %s', entry.path) + self.__od[entry.path] = entry.name - def __set_screen_layout(self, next_layout): + def __set_screen_layout(self, next_layout, item): if self.__inhibited: logger.info('Screen layout is inhibited.') return - logger.info('Starting screenlayout %s', self.current) - layout_proc = subprocess.Popen([self.current]) + logger.info('Starting screenlayout %s', item) + layout_proc = subprocess.Popen([item]) + self.current = item if layout_proc.wait(): logger.warning('Screenlayout failed, continueing to next layout.') - next_layout() \ No newline at end of file + next_layout() + + def __create_tk(self): + options = list(self.__od.keys())[1:] # Skip the first option, it is the current one + num_options = len(options) + cols=math.ceil(math.sqrt(num_options)) + rows = math.ceil(num_options / cols) + root = tkinter.Tk(className="screenlayout") + root.update_idletasks() + width = root.winfo_screenwidth()//3 + height = root.winfo_screenheight()//3 + x = (root.winfo_screenwidth() // 2) - (width // 2) + y = (root.winfo_screenheight() // 2) - (height // 2) + root.geometry('{}x{}+{}+{}'.format(width, height, x, y)) + def create_callback(item): + def cb(): + self.__inhibited = False + self.__set_screen_layout(lambda: None, item) + root.destroy() + return cb + + for i in range(0, cols): + for j in range(0, rows): + if i + cols * j < num_options: + item = options[i + cols * j] + args = dict( + relx=i / cols, + rely=j / rows, + relwidth=1.0 / cols, + relheight=1.0 / rows, + ) + button = tkinter.Button(root, text=self.__naming_func(self.__od[item]), command=create_callback(item)).place(**args) + root.mainloop()