Exemplo n.º 1
0
def svg_to_png(filename: str) -> Path:
    destination: Path = RESOURCE.img(filename + '.png')
    if not destination.exists():
        log.debug(f'creating transparent .png from {filename}...')
        dest_string = str(destination)
        source = RESOURCE.img(filename + '.svg')
        renderPM.drawToFile(svg2rlg(str(source)),
                            dest_string,
                            fmt="PNG",
                            bg=0x000000)
        make_black_background_transparent(dest_string)
        log.debug(f'done creating transparent .png from {filename}.')
    return destination
Exemplo n.º 2
0
def make_circle_glyph(size, rel_radius: float, color_rgb: Tuple[int, ...]):
    fp = RESOURCE.img(f'glyph-{"-".join(map(str, [size, *color_rgb]))}.png')
    if not RESOURCE.img(fp).exists():
        half = int(size / 2.)
        color = color_rgb[::-1]
        img = np.zeros((size, size, 3), dtype=np.uint8)
        img = cv2.circle(img,
                         center=(half, half),
                         radius=int((size * rel_radius) / 2),
                         color=color,
                         thickness=-1)
        cv2.imwrite(str(fp), np.dstack((img, make_alpha(img))))
    return fp
Exemplo n.º 3
0
    def __post_init__(self):
        names = [
            'percent_text',
            'fail_text',
            'pass_text',
            'label',
        ]
        self._names, spot = names[:-1], 1 / len(names)
        self._last_settings = dict()

        pass_pct = self.constants['pct'] == 'pass'
        self._make_pct = self._pass_pct if pass_pct else self._fail_pct
        self._normal_colors = (COLORS.green, COLORS.blue, COLORS.green)
        self._checking_colors = tuple([COLORS.white] * 3)

        [[
            self._load(Label(
                self,
                anchor='center',
                fg=RESOURCE.cfg('view').get('old_colors').get('metrics')[name],
                bg=COLORS.medium_grey),
                       f'{name}_{row}',
                       x=spot * column,
                       y=.5 * row,
                       height=.5,
                       width=spot) for column, name in enumerate(names)
        ] for row in range(2)]

        [
            getattr(self, f'label_{row}').text(f)
            for row, f in zip(range(2), ['h', 'd'])
        ]
        [self._set_row('text', ('-', '-', '-%'), i) for i in range(2)]
        self._numbers = -1, -1, -1, -1
        self.publish(GetMetricsMessage())
Exemplo n.º 4
0
 def style(self) -> None:
     """
     set Window-level style attributes
     """
     self.title(APP.name)
     self.config(bg=COLORS.black)
     self.font = TypeFace(RESOURCE.font(self.constants['font']))
Exemplo n.º 5
0
 def __init__(self, parent: tk.Tk) -> None:
     """
     get bindings from ini, clear state, and perform initial event binding
     """
     self.name = self.__class__.__name__
     self.parent = parent
     self.check_parent()
     self.constants = RESOURCE.cfg('view')['hid'].get(self.name.lower())
     self.bindings: Dict[str, Dict[str, str]] = self.constants['bindings']
     self.__post_init__()
     self._active = True
     self.bind()
Exemplo n.º 6
0
    def __init__(self) -> None:
        """
        lean init that only sets runtime constants and Tk init
        """
        log.debug('instantiating Window...')

        self.name = self.__class__.__name__
        self.constants = RESOURCE.cfg('view')['window']
        self.w_px, self.h_px = APP.STATION.resolution
        self.widgets_by_position = dict()
        self.categories = self.categories or dict()
        self.poll_scheduled = None

        tk.Tk.__init__(self)

        log.debug('instantiated Window')
Exemplo n.º 7
0
    def __init__(self) -> None:
        try:
            self._dll = load_dll(RESOURCE(self._DLL_FP))
        except OSError as e:
            raise LightMeterError(
                f'failed to load DLL at {self._DLL_FP}') from e

        self.exposure_time = c_int16(self.EXPOSURE_TIME)
        self.max_exposure_time = c_int32(self.MAX_EXPOSURE_TIME_US)
        self.calibration_m_sec = c_int32(self.CALIBRATION_MS)
        self.registers = {
            k: c_int32(v)
            for k, v in self.MEASUREMENT_REGISTERS.items()
        }

        self.i = c_int32(0)
        self.ctrl = c_int32(0)
        self.mode = c_int32(0)
        self.isMonitor = c_int32(1)
        self.isAuto = c_int16(0)

        self.mk_FindFirst = DLLFunc(self._dll.mk_FindFirst, c_int8, [c_char_p])
        self.mk_Init = DLLFunc(self._dll.mk_Init,
                               c_int8, [c_int32, c_int32],
                               args=(self.isMonitor, self.calibration_m_sec))
        self.mk_Close = DLLFunc(self._dll.mk_Close)
        self.mk_OpenSpDev = DLLFunc(self._dll.mk_OpenSpDev, c_int32,
                                    [c_char_p])
        self.mk_Capture = DLLFunc(self._dll.mk_Msr_Capture,
                                  c_int8, [c_int32, c_int16, c_int16],
                                  args=(self.i, self.isAuto))
        self.mk_GetData = DLLFunc(self._dll.mk_GetData,
                                  c_int8, [c_int32, c_int32, c_char_p],
                                  args=(self.i, ))
        self.mk_AutoDarkCtrl = DLLFunc(self._dll.mk_Msr_AutoDarkCtrl,
                                       c_int8, [c_int32, c_int32],
                                       args=(self.i, self.ctrl))
        self.mk_SetExpMode = DLLFunc(self._dll.mk_Msr_SetExpMode,
                                     c_int8, [c_int32, c_int32],
                                     args=(self.i, self.mode))
        self.mk_SetMaxExpTime = DLLFunc(self._dll.mk_Msr_SetMaxExpTime,
                                        c_int8, [c_int32, c_int32],
                                        args=(self.i, self.max_exposure_time))
        self.mk_MsrDark = DLLFunc(self._dll.mk_Msr_Dark,
                                  c_int8, [c_int32],
                                  args=(self.i, ))
Exemplo n.º 8
0
    def _set_background(self) -> None:
        self.ax.imshow(
            helper.img_from_pickle(RESOURCE.img('colorspace.p')),
            origin='upper', extent=cie_extent, alpha=1, zorder=10
        )
        patches = [
            Circle(
                (x + CIE_X_OFFSET, y + CIE_Y_OFFSET), r, linewidth=0.5
            ) for x, y, r in zip(*[[
                getattr(param_row, k) for param_row in self.params.rows
            ] for k in ['x_nom', 'y_nom', 'color_dist_max']
            ])
        ]

        self.artists['params_collection'] = self.var(self.ax.add_collection(PatchCollection(
            patches, linewidths=0.5, facecolors=IP_FACE_COLOR, alpha=0.4, zorder=11
        )))
Exemplo n.º 9
0
 def _set_background(self) -> None:
     self.ax.imshow(Image.open(RESOURCE.img(f'mn{self.params.mn}.png')),
                    origin='upper',
                    extent=unit_extent,
                    alpha=0.5,
                    zorder=-1)
Exemplo n.º 10
0
class History(Cell):
    """
    show MN and SN of last tested units on this station
    """
    # ? https://stackoverflow.com/a/31766128
    # ? https://www.tutorialspoint.com/python/tk_listbox.htm

    _dt_ft: str = RESOURCE.cfg('view')['widget_constants']['Time']['DT_FORMAT']
    _pad: int = 2
    _last_results_fresh: bool
    _current_button_width: Optional[int]
    vbar_packed: bool

    def __post_init__(self):
        # initialize state variables
        self.configure(bg=COLORS.black)
        self._create_kws = dict(pady=self._pad)
        self._kws = dict(
            compound=tk.LEFT,
            relief='flat',
            overrelief='flat',
            disabledforeground=COLORS.white,
            state='disabled',
            fg=COLORS.white,
            bg=COLORS.medium_grey,
            bd=0,
        )
        self._pass_ids = set()
        self._fail_ids = set()
        self._day_ids = set()
        self._day_sns = dict()
        self._hour_ids = set()
        self._model_ids = collections.defaultdict(set)
        self._lines = dict()
        self._displayed_ids = set()
        self.__last_results = set()
        self._elisions = dict()
        self._buttons = list()
        self._id_to_button_map = dict()
        self._p_f_dict = {
            HistoryPassFail.pass_string: self._pass_ids,
            HistoryPassFail.fail_string: self._fail_ids
        }
        self._len_f = HistoryLength.initial_setting
        self._pf_f = HistoryPassFail.initial_setting
        self._model_setting = HistoryPartNumber.initial_setting
        self._recency_f = HistoryRecency.initial_setting
        self._last_results_fresh = True
        self._current_button_width = None
        self.scrolled_width = None
        self._midnight = datetime.datetime.combine(
            datetime.date.today(), datetime.datetime.min.time())

        self.history_frame = tk.Frame(self)
        self.vbar = Scrollbar(self)

        # make text field
        bg_ = COLORS.black
        self.field = tk.Text(
            self,
            state='disabled',
            wrap='char',
            relief='flat',
            cursor='arrow',
            fg=COLORS.white,
            bg=bg_,
            selectbackground=bg_,
            inactiveselectbackground=bg_,
            yscrollcommand=self.vbar.set,
            pady=0,
            padx=0,
        )
        self.vbar['command'] = self.field.yview

        self.pack_history(True)
        self.history_frame.pack(fill=tk.BOTH, expand=1)

        # noinspection SpellCheckingInspection
        self._h = self.font.metrics('linespace')

        # make pass/fail glyphs
        _ts = [
            (0, 0, 0)
        ] + [self.constants['glyph'][k]['color'] for k in ('pass', 'fail')]
        self._glyphs = {
            k: self._make_image(make_circle_glyph(120, .5, c),
                                (self._h, self._h))
            for k, c in zip([None, True, False], _ts)
        }
        self.enable()
        self.last_button_selected = None

    def _button_selected(self, record: _RECORD, button: tk.Button) -> None:
        """
        called when an entry is double-clicked
        """
        if self.last_button_selected:
            try:
                self.last_button_selected.configure(bg=COLORS.medium_grey)
            except tk.TclError:
                # last button can be gone before it's reverted
                pass

        button.configure(bg=COLORS.black)
        self.last_button_selected = button
        log.info(f'button {record["id"]} selected')

    def _make_entry(self, record: _RECORD) -> tk.Button:
        if self._current_button_width:
            self._kws['width'] = self._current_button_width

        button = tk.Button(
            self,
            **self._kws,
            image=self._glyphs[record["pf"]],
            text=f'    {record["sn"]} - {record["dt"].strftime(self._dt_ft)}',
        )
        button['command'] = partial(self._button_selected,
                                    record=record,
                                    button=button)
        self._buttons.append(button)
        self._id_to_button_map[record['id']] = button
        return button

    def add_record(self, id: int, pf: bool, dt: datetime, mn: str,
                   sn: str) -> None:
        """
        add one record to the appropriate categories
        """
        id_ = id
        # happened today
        if dt > self._midnight:
            self._day_ids.add(id_)
            record = dict(id=id_, pf=pf, dt=dt, mn=mn, sn=sn)
            self._lines[id_] = record

            # happened in the last hour
            # _timestamp = dt + datetime.timedelta(hours=1)
            # TODO remove this vvv
            _timestamp = dt + datetime.timedelta(minutes=15)
            _now = datetime.datetime.now()
            if _timestamp > _now:
                self._hour_ids.add(id_)
                _timestamp -= _now
                _timestamp: datetime
                self.after(int(_timestamp.total_seconds() * 1000),
                           self.remove_from_hour, id_)

            # passed or failed
            (self._pass_ids if pf else self._fail_ids).add(id_)

            # categorize by model number
            self._model_ids[mn].add(id_)

            # most recent result for this sn
            v = self._day_sns.setdefault(sn, (id_, dt))
            if v[1] < dt:
                self._day_sns[sn] = v
                self._last_results_fresh = True

            # create and tag visible record representation
            self.field.window_create(1.0,
                                     window=self._make_entry(record),
                                     **self._create_kws)
            self.field.tag_add(str(id_), 1.0)

    def _last_results(self) -> _RECORD_ID_SET:
        """
        extract the record ids from the most recent results dict
        """
        if self._last_results_fresh:
            self.__last_results = set([v[0] for v in self._day_sns.values()])
            self._last_results_fresh = False
        return self.__last_results

    def filter_predicate(self, cls) -> bool:
        """
        determine whether to apply a filter from the button widget's class attributes
        """
        return getattr(self, cls._history_attr) != cls.initial_setting

    def filter_for_display(self) -> _RECORD_ID_SET:
        """
        use set intersection to find units that satisfy applicable filters
        """
        filters = list()
        _filter_predicate = self.filter_predicate

        if _filter_predicate(HistoryLength):
            filters.append(self._hour_ids)
        if _filter_predicate(HistoryPassFail):
            filters.append(self._p_f_dict[self._pf_f])
        if _filter_predicate(HistoryPartNumber):
            filters.append(self._model_ids[self._model_setting])
        if _filter_predicate(HistoryRecency):
            filters.append(self._last_results())

        return self._day_ids.intersection(*filters)

    def update_other_widgets(self) -> None:
        """
        update the available selections for the four history select buttons
        update the numbers on the metrics widget in the top right
        """
        self.parent_widget(HistoryPartNumber).set_options(
            list(self._model_ids.keys()))
        self.parent_widget(HistoryPassFail).set_options(
            [s for s, o in self._p_f_dict.items() if o])
        self.parent_widget(HistoryLength).set_options(self._hour_ids)
        self.parent_widget(HistoryRecency).set_options(
            self._last_results() != self._day_ids)

        hr_ = self._hour_ids
        self.parent_widget(Metrics).set_numbers(
            fail_day=len(self._fail_ids),
            pass_day=len(self._pass_ids),
            fail_hour=len(self._fail_ids.intersection(hr_)),
            pass_hour=len(self._pass_ids.intersection(hr_)),
        )

    def button_size(self) -> None:
        for button in self._buttons:
            try:
                button.configure(width=self._current_button_width)
            except Exception as e:
                log.warning(str(e))

    def change_elision(self, _ids: _RECORD_ID_SET) -> None:
        for name in self._day_ids:
            do_elide = name not in _ids
            _last = self._elisions.setdefault(name, None)
            if _last is None or (do_elide ^ _last):
                self.field.tag_configure(str(name), elide=do_elide)
                self._elisions[name] = do_elide
                self.update()

    def pack_forget_widget(self, widget) -> None:
        _ = self
        try:
            widget.pack_forget()
        except Exception as e:
            log.warning(str(e))

    def pack_history(self, scrollable: bool) -> None:
        """
        forget appropriate widget(s)
        pack field or field and vbar
        """
        self.pack_forget_widget(self.field)

        if scrollable:
            self.vbar.pack(in_=self.history_frame, side=tk.LEFT, fill=tk.Y)
            self.field.pack(in_=self.history_frame,
                            side=tk.RIGHT,
                            fill=tk.BOTH,
                            expand=1)

        else:
            self.pack_forget_widget(self.vbar)
            self.field.pack(in_=self.history_frame, fill=tk.BOTH, expand=1)

        self._current_button_width = self.scrolled_width if scrollable else self.width
        self._kws['width'] = cast(int, self._current_button_width)
        self.vbar_packed = scrollable
        self.button_size()

    def _vbar_visibility(self) -> None:
        _conditions = [
            self.field.yview() == (0., 1.),
            self.vbar.winfo_ismapped()
        ]

        if all(_conditions):
            self.pack_history(False)

        elif not any(_conditions):
            self.pack_history(True)

    def filter_and_update(self) -> None:
        """
        filter with selections, sort by date, update widget with button(s), schedule update
        """
        _new_ids = self.filter_for_display()
        self.update_other_widgets()
        self.change_elision(_new_ids)
        self._vbar_visibility()

    def _initialize_history(self, lines: List[Dict[str, Any]]) -> None:
        """
        bulk process test history records
        """

        # remove any existing records
        # delete until end-1 to avoid inserting a newline at the end of the field
        self.field.delete('1.0', 'end-1c')

        # tags aren't garbage collected over the whole life of the application
        # so they need to be explicitly destroyed
        [
            self.field.tag_delete(name) for name in self.field.tag_names()
            if name != 'sel'
        ]

        # clear state
        self._pass_ids.clear()
        self._fail_ids.clear()
        self._day_ids.clear()
        self._hour_ids.clear()
        self._model_ids.clear()
        self._lines.clear()
        self._displayed_ids.clear()
        self._day_sns.clear()
        self._buttons.clear()
        self._elisions.clear()
        self._id_to_button_map.clear()
        self._midnight = datetime.datetime.combine(
            datetime.date.today(), datetime.datetime.min.time())

        # make sub lists of record ids by category
        [self.add_record(**line) for line in lines]

    def change_contents(self, f: Callable, *arg) -> None:
        """
        perform an operation one one fresh or stale record
        """
        self.disable()
        _prev_state, self.field['state'] = self.field['state'], 'normal'

        try:
            f(*arg)

        finally:
            # filter and update regardless of success
            self.filter_and_update()
            self.field['state'] = _prev_state
            self.field.update()
            self.vbar.update()
            self.enable()

    @subscribe(HistoryAddEntryMessage)
    def add_one_to_history(self, id: int, pf: bool, dt: datetime, mn: str,
                           sn: str) -> None:
        """
        add a record to the top of the text widget
        """
        self.change_contents(self.add_record, id, pf, dt, mn, sn)

    def remove_from_hour(self, record_id: int) -> None:
        """
        this is the callback that cleans a unit from the hour list
        """
        if record_id in self._hour_ids:
            self.change_contents(self._hour_ids.remove, record_id)

    @subscribe(HistorySetAllMessage)
    def initialize_history(self,
                           records: List[HistoryAddEntryMessage]) -> None:
        """
        check for new data
        perform init and then update display
        """
        self.change_contents(self._initialize_history, records)

    def _on_show(self) -> None:
        """
        make entry Button kwargs with winfo_width
        """
        self.width = self.winfo_width()
        self.scrolled_width = self.width - self.vbar.winfo_width()
        self._kws.update(
            dict(height=self._h, width=self.scrolled_width, font=self.font))

    @subscribe(HistorySelectEntryMessage)
    def select_button(self, id_: int) -> None:
        self._id_to_button_map[id_].invoke()

    @staticmethod
    def double_click(evt: tk.EventType) -> None:
        """
        if double click lands on a button (history entry), invoke it
        """
        _button = evt.widget  # type: ignore
        invoke = getattr(_button, 'invoke', None)
        if callable(invoke):
            with_enabled(_button)(invoke)()
Exemplo n.º 11
0
 def __post_init__(self):
     (self.enable if self._force_enabled else self.disable)()
     self.config(bg=COLORS.dark_grey)
     self.img = self._make_image(RESOURCE.img(self.filepath))
     self._load(tk.Label(self, anchor='center', image=self.img, bg=COLORS.black))
Exemplo n.º 12
0
class Cell(tk.Frame, register.Mixin):
    """
    wrapper around tk.Frame
    define _click(action) | _drag(action) to respond to mouse events in a widget
    """
    __font: tk.font.Font = None
    parent: View
    is_enabled: bool = True
    content: tk.Label = None
    _view_config = RESOURCE.cfg('view')

    _enable_state_vars = {True: (COLORS.medium_grey, 'enabled'),
                          False: (COLORS.dark_grey, 'disabled'), }
    _fresh_interval_ms: int = _view_config['window']['fresh_data_interval_ms']

    _greyscale = _view_config['grey']
    _fresh_sequence: List[str] = [
        tuple_to_hex_color((v, v, v)) for v in range(_greyscale['light'], _greyscale['medium'], -1)
    ]
    category: Category
    subscribed_methods: Dict[Type, str]

    def parent_widget(self, cls: Type[_T], *, fail_silently: bool = False) -> _T:
        try:
            return getattr(self.parent, cls.__name__.lower())
        except AttributeError:
            if not fail_silently:
                raise

    @register.after('__init__')
    def _add_subscribed_methods_to_parent(self):
        for _t, method_name in getattr(self, 'subscribed_methods', {}).items():
            self.parent.subscribed_methods[_t].add((self.name, method_name))

    @property
    def publish(self):
        return self.parent.publish

    @property
    def session_manager(self) -> SessionManager:
        return self.parent.session_manager

    @property
    def font(self) -> tk.font.Font:
        if not self.__font:
            f: int = self._view_config['FONTSIZE'][self.__class__.__name__.upper()]
            self.__font = self._make_font(f) if f else self.parent.font.tk_font
        return self.__font

    @font.setter
    def font(self, font: tk.font.Font) -> None:
        self.__font = font

    # SUPPRESS-LINTER <super().__init__ called immediately after first call of show()>
    # noinspection PyMissingConstructor
    def __init__(self, name: str, parent: View, x: float, y: float, w: float, h: float, ) -> None:
        """
        calc relative dimensions using padding from ini
        calc actual frame dimensions in pixels for hid.Mouse
        make widget default font
        """
        self.parent = parent
        self.name = name
        self.pos = x, y, w, h
        self._made = False
        self.labels = list()
        self.scheduled_reference = None
        self._normal_bg = None
        self.constants = self._view_config['widget_constants'].get(self.__class__.__name__, {})
        self._fresh_index = None

    def __post_init__(self):
        """
        perform subclass-specific setup here
        set to initial state, request controller update if necessary
        """
        raise NotImplementedError

    @do_if_not_done('made', True)
    def _cell_make(self) -> None:
        """
        compute dimensions and call overridden make()
        """
        tk.Frame.__init__(self, self.parent)
        self.config(bg=COLORS.dark_grey)

        # compute relative dimensions
        x, y, w, h = self.pos
        _y_pad = self._view_config['window']['PADDING']
        _x_pad = _y_pad / self.parent.screen.w_h_ratio
        _y_shift, _x_shift = _y_pad if y == 0 else 0, _x_pad if x == 0 else 0
        _rel_x = x + _x_shift
        _rel_y = y + _y_shift
        _rel_width = (w - _x_pad) - _x_shift
        _rel_height = (h - _y_pad) - _y_shift

        # dimensions relative where (1,1) is the bottom right of the Window
        self._place_dimensions = dict(relx=_rel_x, rely=_rel_y, relwidth=_rel_width, relheight=_rel_height)

        # compute absolute dimensions
        self.x_co = int(_rel_x * self.parent.w_px)
        self.y_co = int(_rel_y * self.parent.h_px)
        self.w_co = int(_rel_width * self.parent.w_px)
        self.h_co = int(_rel_height * self.parent.h_px)

        # dimensions of this widget in pixels
        self.co_dimensions_raw = (self.x_co, self.x_co + self.w_co), (self.y_co, self.y_co + self.h_co)

        self.__post_init__()

    def _remove_previous(self):
        """
        if a Cell exists in the same position, removes it
        """
        previous_widget = self.parent.widgets_by_position.get(self.pos, None)
        if previous_widget:
            previous_widget.hide()

    def _before_show(self) -> None:
        """
        called before the last widget is removed
        """

    def _on_show(self):
        """
        called just after place()
        start update scheduler etc
        """

    @do_if_not_done('showed', True)
    def show(self):
        """
        place frame on Window using relative values calculated in __init__
        if a widget already exists in this position, hides it first
        """
        self._cell_make()
        self._before_show()
        self._remove_previous()
        self.parent.widgets_by_position[self.pos] = self
        self.place(**self._place_dimensions)
        self.update()
        self._on_show()

    def _on_hide(self):
        """
        executes just before hide()
        """

    @do_if_not_done('showed', False)
    def hide(self):
        """
        remove frame from Window
        """
        self.cancel_scheduled()
        self._on_hide()
        self.place_forget()

    def _before_destroy(self):
        """
        executes just before Tk.Frame.destroy()
        """

    def destroy(self):
        """
        overridden as a template hook
        """
        try:
            self.cancel_scheduled()
            self._before_destroy()
        except Exception as e:
            print(self.name, e, 'in close')
        super().destroy()

    # # # appearance methods
    def _set_background(self, color: str) -> None:
        """
        set the background color of self and label children
        """
        _last_color = getattr(self, '__last_color', None)
        if _last_color is None or _last_color != color:
            self.config(bg=color)
            [label.color(bg=color) for label in self.labels]
            setattr(self, '__last_color', color)

    def _toggle_enabled(self, is_enabled: bool) -> None:
        """
        change frame background to color from ini
        """
        color, s = self._enable_state_vars[is_enabled]
        self.parent.after_idle(self._set_background, color)
        log.debug(f'window widget <{self.name}> {s}')
        self.is_enabled = is_enabled

    def enable(self, *args, **kwargs) -> None:
        """
        expose partial _toggle_enabled to callers
        """
        _, _ = args, kwargs
        self._toggle_enabled(True)

    def disable(self, *args, **kwargs) -> None:
        """
        expose partial _toggle_enabled to callers
        """
        _, _ = args, kwargs
        self._toggle_enabled(False)

    def _make_more_stale(self) -> None:
        """
        darken from light to enabled on interval, then enable
        """
        self._fresh_index += 1
        try:
            self.parent.after_idle(self._set_background, self._fresh_sequence[self._fresh_index])
            # self._set_background(self._fresh_sequence[self._fresh_index])
            self.schedule(self._fresh_interval_ms, self._make_more_stale)
        except IndexError:
            self.schedule(self._fresh_interval_ms, self.enable)

    def fresh_data(self):
        """
        indicate fresh data with temporary background highlight
        debounce fresh data re-fetch per ini
        """
        self._fresh_index = -1
        self._make_more_stale()

    # # # schedule methods
    def schedule(self, interval: int, callback: Callable, *args):
        """
        calls after() and registers reference for future cancelling
        """
        self.scheduled_reference = self.parent.after(interval, callback, *args)

    def cancel_scheduled(self):
        """
        cancels scheduled callback if any
        """
        if self.scheduled_reference:
            try:
                self.parent.after_cancel(self.scheduled_reference)
            except Exception as e:
                log.error(str(e))

    # # # load methods
    def _load(self, tk_widget, name='content', **kwargs):
        """
        remove existing content and
        pack new child widget to fill entire frame
        return child widget
        """
        existing = getattr(self, name, None)
        if existing:
            for method in ['forget', 'pack_forget', 'destroy']:
                # SUPPRESS-LINTER <don't care if this fails>
                # noinspection PyBroadException
                try:
                    getattr(existing, method)()
                except Exception as _:
                    pass

        setattr(self, name, tk_widget)

        if kwargs:
            getattr(self, name).place(**kwargs)

        else:
            getattr(self, name).pack(fill=tk.BOTH, expand=1)

        return getattr(self, name)

    def _make_font(self, size: int, family: str = None) -> Font:
        """
        make new Tk font instance for widget, font name can be overridden if different from parent's
        """
        return Font(family=family or self.parent.font.name, size=size)

    @staticmethod
    def _make_image(fp: Union[Path, str], dimensions_in_pixels: Tuple[int, ...] = None) -> ImageTk.PhotoImage:
        """
        take filepath from ini
        resize if necessary
        return TK-compatible object to be packed in frame
        """
        img = PIL.Image.open(str(fp))
        if dimensions_in_pixels:
            img = img.resize(dimensions_in_pixels)

        return ImageTk.PhotoImage(img)
Exemplo n.º 13
0
class Mouse(Binding):
    """
    encapsulates mouse event handling for the Window
    """
    _release_methods = ['click', 'drag_h', 'drag_v']
    press_event: Optional[tk.EventType] = None
    co_dimensions: Optional[Tuple[Tuple[int, ...], ...]] = None
    parent_methods = {
        'update',
        'winfo_rootx',
        'winfo_rooty',
    }

    # calculate drag angle categories and drag length threshold from view ini
    __responsiveness = RESOURCE.cfg('view')['hid']['mouse']['responsiveness']
    _drag = int(__responsiveness['CLICK_SWIPE_THRESHOLD_PX'])

    # square click threshold once, here, to speed up mouse action filter
    _drag_sq = _drag**2
    __fudge = __responsiveness['DISTANCE_FROM_RIGHT_ANGLE_DEGREES_ALLOWED']

    _directions: Dict[int, MouseAction] = dict()
    for deg in range(-180, 181):
        for _min, _max, direction in (
            (-__fudge, __fudge, MouseAction.RIGHT),
            (-180, -(180 - __fudge), MouseAction.LEFT),
            ((180 - __fudge), 180, MouseAction.LEFT),
            (-90 - __fudge, -90 + __fudge, MouseAction.TOP),
            (90 - __fudge, 90 + __fudge, MouseAction.BOTTOM),
        ):
            if _min <= deg < _max:
                _directions[deg] = direction

    def clear(self) -> None:
        """
        reset initial press state
        """
        self.press_event, self.co_dimensions = None, None

    def validate(self, evt: tk.EventType):
        """
        if mouse action is within the bounds of a widget, rather than in the padding,
        return the widget
        """
        if self._active and evt.widget:
            cell = evt.widget.master
            if cell is not None:
                if getattr(cell, 'is_enabled', False):
                    return cell

    # initial event handlers
    def press(self, evt: tk.EventType) -> str:
        """
        handles every press event, including the first and second in a double click
        """
        _cell = self.validate(evt)
        if _cell:
            # persist the event for other handlers
            self.press_event = evt

            # set widget root bounds from current window dimensions
            self.parent.update()
            x, y = self.parent.winfo_rootx(), self.parent.winfo_rooty()
            (x0, x1), (y0, y1) = _cell.co_dimensions_raw
            self.co_dimensions = (x + x0, x + x1), (y + y0, y + y1)

        # stop event handling chain
        return 'break'

    def double_click(self, evt: tk.EventType) -> None:
        """
        pass double click event to cell if handler is defined
        disregards initial press state
        """
        cell = self.validate(evt)
        if cell:
            call(cell, 'double_click', evt)
        self.clear()

    # gates: secondary handler if True
    @property
    def initial_target(self):
        """
        secondary events should only proceed if initial state has been set
        returns target cell widget
        """
        _cell = self.press_event
        if _cell:
            if self._active:
                return _cell.widget.master
            self.clear()

    def is_in_bounds(self, x_root: int, y_root: int) -> bool:
        """
        determine if non-first event in sequence is still within first event's widget
        """
        try:
            (x_min, x_max), (y_min, y_max) = self.co_dimensions
        except TypeError:
            return False
        else:
            return (x_min < x_root < x_max) and (y_min < y_root < y_max)

    # secondary event handlers
    def release(self, evt: tk.EventType) -> None:
        """
        perform all calculations only on mouse release
        handle if all conditions are met by press - release pair
        clear state regardless
        """
        cell = self.initial_target
        if cell:

            click, drag_h, drag_v = [
                getattr(cell, k, None) for k in self._release_methods
            ]

            if click or drag_h or drag_v:

                # unpack release coordinates
                x, y = evt.x_root, evt.y_root

                if self.is_in_bounds(x, y):
                    # if release is in pressed widget's bounds, calculate distances traveled in x and y
                    dx, dy, _th = x - self.press_event.x_root, y - self.press_event.y_root, self._drag

                    if (dx > _th) or (dy > _th) or (
                        (dx**2 + dy**2) > self._drag_sq):
                        if drag_h or drag_v:

                            # if mouse traveled far enough, determines drag angle
                            _d = self._directions.get(
                                int(math.degrees(math.atan2(dy, dx))), None)
                            if _d:
                                # if drag angle sufficiently vertical or horizontal, handles drag
                                # by direction in cell
                                _dir = _d.direction

                                if drag_h and _dir == 'horizontal':
                                    drag_h(_d)

                                elif drag_v and _dir == 'vertical':
                                    drag_v(_d)

                    elif click:
                        # handle as click in cell if it hasn't dragged past threshold
                        click()

        call(cell, 'on_release')
        self.clear()
Exemplo n.º 14
0
def make_ico(img, filename: str, sizes: Tuple[int, ...]):
    img.save(str(RESOURCE.img(filename + '.ico')),
             sizes=[(v, v) for v in sizes])