Source code for rkviewer.utils

"""Utility functions"""
# pylint: disable=maybe-no-member
import wx
from itertools import tee
import os
import platform
import subprocess
import sys
from typing import Any, Callable, Dict, Iterable, List, Tuple, Type, TypeVar
from dataclasses import is_dataclass
from pathlib import Path


[docs]def convert_position(fn): """Decorator that converts the event position to one that is relative to the receiver.""" def ret(self, evt): if self is not evt.EventObject: client_pos = evt.GetPosition() # get raw position screen_pos = evt.EventObject.ClientToScreen(client_pos) # convert to screen position relative_pos = self.ScreenToClient(screen_pos) # convert to receiver position # call function copy = evt.Clone() copy.SetPosition(relative_pos) copy.foreign = True fn(self, copy) evt.Skip() else: copy = evt copy.foreign = False fn(self, copy) return ret
[docs]def no_rzeros(num: float, precision: int) -> str: """Returns string of the num with the given precision, but with trailing zeros removed.""" assert precision > 0 fmt = '{:.' + str(precision) + 'f}' return fmt.format(num).rstrip('0').rstrip('.')
[docs]def on_msw() -> bool: """Returns whether we are running on Windows.""" return os.name == 'nt'
[docs]def get_local_path(relative_path): """Get path relative to the executable, or the working dir if not bundled.""" return os.path.abspath(relative_path)
# def get_bundled_path(relative_path): # """Given a path relative to the application bundle, return the absolute path. # Specifically for files bundled with the application, e.g. resources. # """ # if hasattr(sys, '_MEIPASS'): # # PyInstaller creates a temp folder and stores path in _MEIPASS # base_path = getattr(sys, '_MEIPASS') # else: # base_path = os.path.abspath(".") # return os.path.join(base_path, relative_path)
[docs]def resource_path(relative_path): """Get absolute path to resource, works for dev and PyInstaller.""" source_path = Path(__file__).resolve() source_dir = source_path.parent return source_dir.joinpath('resources').joinpath(relative_path).as_posix()
[docs]def start_file(abs_path: str): # Tell OS to open the file for editing. From https://stackoverflow.com/a/435669/9171534 if platform.system() == 'Darwin': # macOS subprocess.call(('open', abs_path)) elif platform.system() == 'Windows': # Windows os.startfile(abs_path) else: # linux variants subprocess.call(('xdg-open', abs.path))
[docs]def pairwise(iterable: Iterable) -> Iterable: """s -> (s0,s1), (s1,s2), (s2, s3), ...""" a, b = tee(iterable) next(b, None) return zip(a, b)
[docs]def rgba_to_wx_colour(rgb: int, alpha: float) -> wx.Colour: """Given RGBA color, return wx.Colour. Args: rgb: RGB color in hex format. alpha: The opacity of the color, ranging from 0.0 to 1.0. """ b = rgb & 0xff g = (rgb >> 8) & 0xff r = (rgb >> 16) & 0xff return wx.Colour(r, g, b, int(alpha * 255))
[docs]def opacity_mul(color: wx.Colour, fraction: float) -> wx.Colour: return wx.Colour(color.Red(), color.Green(), color.Blue(), int(color.Alpha() * fraction))
[docs]def change_opacity(color: wx.Colour, new_op: int): return wx.Colour(color.Red(), color.Green(), color.Blue(), new_op)
[docs]class ButtonGroup: """Class for keeping track of a group of buttons, where exactly one of them can be selected. Attributes: Callback: The callback function type called with ID of the selected button as argument. """ Callback = Callable[[str], None] def __init__(self, callback: Callback): """Construct a ButtonGroup. Args: callback: The callback function called when a new button is selected. """ self.callback = callback self.buttons = [] self.selected = None # should be tuple (button, group_id)
[docs] def AddButton(self, button: wx.ToggleButton, identifier: Any): """Add a button with the given identifier. When this button is clicked, callback is called with the identifier. """ # right now there is no type info for wxPython, so this is necessary assert isinstance(button, wx.ToggleButton) self.buttons.append(button) button.Bind(wx.EVT_TOGGLEBUTTON, self._MakeToggleFn(button, identifier)) # First added button; make it selected if self.selected is None: self.selected = (button, identifier) button.SetValue(True) self.callback(identifier)
def _MakeToggleFn(self, button: wx.ToggleButton, group_id: Any): """Create the function to be called by a specific button in the group when it is clicked. """ # right now there is no type info for wxPython, so this is necessary assert isinstance(button, wx.ToggleButton) def ret(evt): assert self.selected is not None, "There must be at least one button in ButtonGroup!" if evt.IsChecked(): button.SetValue(True) selected_btn, selected_id = self.selected if selected_id != group_id: selected_btn.SetValue(False) self.selected = (button, group_id) self.callback(group_id) else: # don't allow de-select button.SetValue(True) return ret
[docs]def int_round(n: float) -> int: return int(round(round(n, 2)))
[docs]def even_round(n: float) -> int: """Round to the nearest even integer""" return int(round(n / 2)) * 2
T = TypeVar('T')
[docs]def gchain(*iterables: Iterable[T]) -> Iterable[Tuple[int, T]]: # chain('ABC', 'DEF') --> (0,A) (0,B) (0,C) (1,D) (1,E) (1,F) i = 0 for it in iterables: for element in it: yield (i, element) i += 1
# Force dataclass to use kwargs, from https://gist.github.com/mikeholler/4be180627d3f8fceb55704b729464adb#file-dataclass_utils-py _T = TypeVar("_T") _Self = TypeVar("_Self") _VarArgs = List[Any] _KWArgs = Dict[str, Any] def _kwarg_only_init_wrapper( self: _Self, init: Callable[..., None], *args: _VarArgs, **kwargs: _KWArgs ) -> None: if len(args) > 0: raise TypeError( f"{type(self).__name__}.__init__(self, ...) only allows keyword arguments. Found the " f"following positional arguments: {args}" ) init(self, **kwargs)
[docs]def require_kwargs_on_init(cls: Type[_T]) -> Type[_T]: """ Force a dataclass's init function to only work if called with keyword arguments. If parameters are not positional-only, a TypeError is thrown with a helpful message. This function may only be used on dataclasses. This works by wrapping the __init__ function and dynamically replacing it. Therefore, stacktraces for calls to the new __init__ might look a bit strange. Fear not though, all is well. Note: although this may be used as a decorator, this is not advised as IDEs will no longer suggest parameters in the constructor. Instead, this is the recommended usage:: from dataclasses import dataclass @dataclass class Foo: bar: str require_kwargs_on_init(Foo) """ if cls is None: raise TypeError("Cannot call with cls=None") if not is_dataclass(cls): raise TypeError( f"This decorator only works on dataclasses. {cls.__name__} is not a dataclass." ) original_init = cls.__init__ def new_init(self: _Self, *args: _VarArgs, **kwargs: _KWArgs) -> None: _kwarg_only_init_wrapper(self, original_init, *args, **kwargs) # noinspection PyTypeHints cls.__init__ = new_init # type: ignore return cls