import tkinter as tk
import numpy as np
import time
from ._manager import manager
from ._tab_numpy import ViewerTabNumpy, matches_tab_numpy
from ._tab_struct import ViewerTabStruct, matches_tab_struct
from ._tab_text import ViewerTabText
from ._custom_notebook import CustomNotebook
[docs]class Viewer():
"""Class representing a matrix viewer window."""
def __init__(self, title="Matrix Viewer"):
"""Constructs a new viewer window."""
self.window = tk.Toplevel(manager.get_or_create_root())
self.window.title(title)
self.window.geometry('500x500')
self.window['bg'] = '#AC99F2'
self.paned = CustomNotebook(self.window)
self.paned.bind("<<NotebookTabClosed>>", self._on_tab_closed) # binding child.destroy does not work on windows because there, destroy is called on window close but not on tab close as on linux
f2 = tk.Frame(self.window)
self.paned.grid(column=0, row=0, sticky="nsew") # sticky: north south east west, specify which sides the inner widget should be tuck to
self.window.rowconfigure(0, weight=1)
self.window.columnconfigure(0, weight=1)
self._event_loop_id = None
self._destroyed = False
self.window.bind("<Destroy>", self._on_destroy)
self.window.bind("<Key>", self._on_q_pressed)
manager.register(self)
self.tabs = []
def _on_destroy(self, event):
if event.widget == self.window: # we also get destroy events for childs so we need to filter them
if not self._destroyed:
manager.unregister(self)
self._destroyed = True
else:
print('Error: double destroyed', self.window)
def _on_q_pressed(self, event):
if event.char == 'q':
self.window.destroy() # this will also call self._on_destroy
else:
# tkinter does not auto-focus the currently active window, and I also do not want to manually change the focus. Therefore to manual event propagation
selected = self.paned.select()
found = 0
for tab, top_frame in self.tabs:
if str(top_frame) == selected:
tab._on_key(event)
found += 1
if found != 1:
print('Error: frame', selected, 'not found', found)
def _on_tab_closed(self, event):
new_tab_frames = self.paned.tabs()
diff = set(new_tab_frames).symmetric_difference(set(self.tab_frames))
assert len(diff) == 1, f"invalid frame difference {new_tab_frames} vs. {self.tab_frames}"
closed_tab_top_frame, = diff
# find the ViewerTab associated with that frame
found = 0
for tab, top_frame in self.tabs:
if str(top_frame) == closed_tab_top_frame:
found += 1
tab.on_destroy()
if found != 1:
print('Error: frame', closed_tab_top_frame, 'not found', found)
self.tab_frames = new_tab_frames
[docs] def register(self, tab, top_frame, tab_title):
"""
This method should only be used by tabs. It adds the tab to the window.
"""
self.tabs.append((tab, top_frame))
self.paned.add(top_frame, text=tab_title)
self.tab_frames = self.paned.tabs() # update frame list
[docs] def unregister(self, tab):
"""
This method should only be used by tabs.
"""
self.tabs.pop(list(zip(*self.tabs))[0].index(tab))
if len(self.tabs) == 0:
self.window.destroy() # close if all tabs were closed by the user
[docs] def view(self, object, tab_title=None, font_size=None, formatter=None):
"""Adds a new tab that visualizes the specified object.
:param tab_title: The string show in the tab header.
:param font_size: The font size used in the cells and the row / column headings.
:param formatter: A function which converts the cells to string. Currently only used for
Matrix / Vector viewer.
"""
if matches_tab_numpy(object):
return ViewerTabNumpy(self, object, tab_title, font_size, formatter)
elif matches_tab_struct(object):
return ViewerTabStruct(self, object, tab_title, font_size)
else:
return ViewerTabText(self, object, tab_title, font_size)
[docs]def viewer(title="Matrix Viewer"):
"""Creates a new viewer window.
:param title: The string shown in the window header.
:return: The newly created viewer window.
"""
return Viewer(title)
[docs]def view(object, tab_title=None, font_size=None, formatter=None):
"""Creates a new tab in the current window, which shows the object.
Creates a new window if there are no opened windows.
:param object: the object that is to be visualized.
:param tab_title: The string show in the tab header.
:param font_size: The font size used in the cells and the row / column headings.
:param formatter: A function which converts the cells to string. Currently only used for
Matrix / Vector viewer.
:return: The newly created viewer tab.
"""
viewer = manager.last_viewer
if viewer is None:
viewer = Viewer()
return viewer.view(object, tab_title, font_size, formatter)
[docs]def show(block=True):
"""Runs the event loop until all windows are closed.
:param block: To keep API similarity to pyplot - if False, this will do nothing; if True, this function will block until all windows are closed."""
if len(manager.registered_viewers) > 0:
manager.show(block)
[docs]def pause(timeout):
"""Runs the event loop for the specified time interval.
:param timeout: Time interval in seconds. If 0, it will exit immediately after processing all pending GUI events."""
if len(manager.registered_viewers) > 0:
manager.pause(timeout)
else:
time.sleep(timeout)
[docs]def show_with_pyplot():
"""This function should be used instead of show when you are also using matplotlib.pyplot.
It concurrently runs the event loop for pyplot and matrix_viewer. It will run until all numpy and
matrix_viewer windows were closed by the user.
Note that unfortunately, pyplot and matrix_viewer appear to be slightly laggy if you are using a
different pyplot backend than tkinter.
"""
import matplotlib
import matplotlib.pyplot
show(block=False)
if matplotlib.get_backend() == 'TkAgg': # matplotlib is also using tkinter
matplotlib.pyplot.show() # this will also show matrix_viewer windows
if len(manager.registered_viewers) > 0: # pyplot.show() returns if all pyplot figures were closed
show() # Therefore, we have to continue running the event loop until all matrix_viewer windows are closed, too
else:
matplotlib.pyplot.show(block=False)
# this implementation is a bit laggy, eventually there is a better one
while (len(manager.registered_viewers) > 0) or (len(matplotlib.pyplot.get_fignums()) > 0):
# run both event loops in sequence, fast enough for acceptable delay
pause(0.02)
matplotlib.pyplot.pause(0.02)